mirror of
https://github.com/ParrotXray/Mantis.git
synced 2026-08-24 19:00:27 +09:00
Merge pull request #1 from ParrotXray/feat/rule-research
Feat/rule research
This commit is contained in:
parent
0d5fca3380
commit
5f907c073c
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@ -10,7 +10,12 @@ target/
|
|||||||
|
|
||||||
.idea
|
.idea
|
||||||
logs
|
logs
|
||||||
TODO
|
|
||||||
.log
|
.log
|
||||||
.txt
|
.txt
|
||||||
net-guardia/static/web
|
net-guardia/static/web
|
||||||
|
*.mmdb
|
||||||
|
node_modules/
|
||||||
|
.next/
|
||||||
|
|
||||||
|
*.rules
|
||||||
|
*.db
|
||||||
|
|||||||
380
Cargo.lock
generated
380
Cargo.lock
generated
@ -301,6 +301,15 @@ version = "0.2.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.100"
|
version = "1.0.100"
|
||||||
@ -535,6 +544,12 @@ dependencies = [
|
|||||||
"alloc-stdlib",
|
"alloc-stdlib",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@ -621,6 +636,19 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clang-sys"
|
name = "clang-sys"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
@ -632,6 +660,15 @@ dependencies = [
|
|||||||
"libloading",
|
"libloading",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cmake"
|
||||||
|
version = "0.1.58"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "common"
|
name = "common"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -662,6 +699,12 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@ -891,6 +934,18 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"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]]
|
[[package]]
|
||||||
name = "filetime"
|
name = "filetime"
|
||||||
version = "0.2.27"
|
version = "0.2.27"
|
||||||
@ -936,6 +991,33 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||||
|
dependencies = [
|
||||||
|
"foreign-types-macros",
|
||||||
|
"foreign-types-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types-macros"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.98",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types-shared"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
@ -1135,6 +1217,15 @@ dependencies = [
|
|||||||
"foldhash 0.2.0",
|
"foldhash 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashlink"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
@ -1168,6 +1259,30 @@ version = "1.0.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.65"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@ -1307,6 +1422,15 @@ dependencies = [
|
|||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "imapprotolens"
|
||||||
|
version = "0.16.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f4436252f26379481b02b023a59530310d17e1d68cee692416b162a44aa68db9"
|
||||||
|
dependencies = [
|
||||||
|
"nom 7.1.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "impl-more"
|
name = "impl-more"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@ -1391,6 +1515,16 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.94"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kstring"
|
name = "kstring"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
@ -1457,6 +1591,17 @@ dependencies = [
|
|||||||
"redox_syscall 0.7.0",
|
"redox_syscall 0.7.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libsqlite3-sys"
|
||||||
|
version = "0.30.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libxdp-sys"
|
name = "libxdp-sys"
|
||||||
version = "0.2.3+1.5.6"
|
version = "0.2.3+1.5.6"
|
||||||
@ -1704,6 +1849,21 @@ dependencies = [
|
|||||||
"rawpointer",
|
"rawpointer",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ndarray"
|
||||||
|
version = "0.17.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d"
|
||||||
|
dependencies = [
|
||||||
|
"matrixmultiply",
|
||||||
|
"num-complex",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
"portable-atomic",
|
||||||
|
"portable-atomic-util",
|
||||||
|
"rawpointer",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "net-guardia"
|
name = "net-guardia"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -1715,6 +1875,7 @@ dependencies = [
|
|||||||
"aya",
|
"aya",
|
||||||
"aya-log",
|
"aya-log",
|
||||||
"cargo_metadata",
|
"cargo_metadata",
|
||||||
|
"chrono",
|
||||||
"common",
|
"common",
|
||||||
"crossbeam",
|
"crossbeam",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
@ -1726,7 +1887,11 @@ dependencies = [
|
|||||||
"maxminddb",
|
"maxminddb",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"network-types",
|
"network-types",
|
||||||
|
"ort",
|
||||||
|
"ort-tract",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
"protolens",
|
||||||
|
"rusqlite",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -1740,6 +1905,7 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"tract-onnx",
|
"tract-onnx",
|
||||||
"url",
|
"url",
|
||||||
|
"vectorscan-rs",
|
||||||
"xsk-rs",
|
"xsk-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1901,6 +2067,35 @@ version = "1.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ort"
|
||||||
|
version = "2.0.0-rc.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7de3af33d24a745ffb8fab904b13478438d1cd52868e6f17735ef6e1f8bf133"
|
||||||
|
dependencies = [
|
||||||
|
"ndarray 0.17.2",
|
||||||
|
"ort-sys",
|
||||||
|
"smallvec",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ort-sys"
|
||||||
|
version = "2.0.0-rc.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7b497d21a8b6fbb4b5a544f8fadb77e801a09ae0add9e411d31c6f89e3c1e90"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ort-tract"
|
||||||
|
version = "0.3.0+0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6cf5d858eff9d54a3a8935eff574c01a2962cb1ae7dc2dc0f531f75cafb8b17e"
|
||||||
|
dependencies = [
|
||||||
|
"ort-sys",
|
||||||
|
"parking_lot",
|
||||||
|
"tract-onnx",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@ -1985,6 +2180,48 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
||||||
|
dependencies = [
|
||||||
|
"phf_macros",
|
||||||
|
"phf_shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_generator"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||||
|
dependencies = [
|
||||||
|
"phf_shared",
|
||||||
|
"rand 0.8.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_macros"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator",
|
||||||
|
"phf_shared",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.98",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_shared"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
||||||
|
dependencies = [
|
||||||
|
"siphasher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@ -2107,6 +2344,22 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protolens"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c040ea146711a09a3e22158263859f189232b235626a922037a5dccab6926f6a"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"futures",
|
||||||
|
"futures-util",
|
||||||
|
"imapprotolens",
|
||||||
|
"libc",
|
||||||
|
"memchr",
|
||||||
|
"nom 7.1.3",
|
||||||
|
"phf",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.43"
|
version = "1.0.43"
|
||||||
@ -2250,6 +2503,20 @@ version = "0.8.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusqlite"
|
||||||
|
version = "0.32.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"fallible-iterator",
|
||||||
|
"fallible-streaming-iterator",
|
||||||
|
"hashlink",
|
||||||
|
"libsqlite3-sys",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-embed"
|
name = "rust-embed"
|
||||||
version = "8.7.2"
|
version = "8.7.2"
|
||||||
@ -2481,6 +2748,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "siphasher"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
@ -2879,9 +3152,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tract-core"
|
name = "tract-core"
|
||||||
version = "0.22.0"
|
version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7d72bdfb1d8809fc16b7e3496c8a3a31e8c55eeec8f648f2715d87bd25e9db1c"
|
checksum = "b65d67f5190132365dda73fe215bfc5e01b031e8cbfbea9d486bb5b0dbba3545"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"anymap3",
|
"anymap3",
|
||||||
@ -2892,7 +3165,7 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"maplit",
|
"maplit",
|
||||||
"ndarray",
|
"ndarray 0.16.1",
|
||||||
"num-complex",
|
"num-complex",
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
@ -2905,9 +3178,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tract-data"
|
name = "tract-data"
|
||||||
version = "0.22.0"
|
version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fb9833e90b72a7a8e7abc517e79a90c1463d88550531deaabd4b5dbf706a091b"
|
checksum = "73cd7fda1e5e8b854ea3abdd09126a87fc4af81e6d1e29ec1710a8a4abf4f13a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"downcast-rs",
|
"downcast-rs",
|
||||||
@ -2918,7 +3191,7 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libm",
|
"libm",
|
||||||
"maplit",
|
"maplit",
|
||||||
"ndarray",
|
"ndarray 0.16.1",
|
||||||
"nom 8.0.0",
|
"nom 8.0.0",
|
||||||
"nom-language",
|
"nom-language",
|
||||||
"num-integer",
|
"num-integer",
|
||||||
@ -2931,9 +3204,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tract-hir"
|
name = "tract-hir"
|
||||||
version = "0.22.0"
|
version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fe98f1a0fe9d7bcc39a64258729940da99a26e0ecac8d474d17cfd15f9e4ecf"
|
checksum = "554df991b647dba8af0547ee5838b6912ed20b424f2adda0ea0b7faf8db1b151"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive-new",
|
"derive-new",
|
||||||
"log",
|
"log",
|
||||||
@ -2942,9 +3215,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tract-linalg"
|
name = "tract-linalg"
|
||||||
version = "0.22.0"
|
version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d09562926176740991a4e74ada093ed4edc155a585c45e2dd6fa13994a89f04f"
|
checksum = "e72097a89cc4e7c5f1bc4f854b9294dd30fa6f6d8f7f409c556953b49078c94f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"cc",
|
"cc",
|
||||||
@ -2970,9 +3243,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tract-nnef"
|
name = "tract-nnef"
|
||||||
version = "0.22.0"
|
version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "160625a1b79132698ac292ff555c575a567a2c27cd2371c53c13225cbda8d1de"
|
checksum = "45b3755dd0948111b407085d11033ba218cb85b85ce8d795cec2b8353db552ea"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"flate2",
|
"flate2",
|
||||||
@ -2990,9 +3263,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tract-onnx"
|
name = "tract-onnx"
|
||||||
version = "0.22.0"
|
version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e96a3bf1d24b5ca9e4371dac498c6c3cb47d1e4eb7fede41a5750e611da45274"
|
checksum = "ac23ad1d2d5da3256ae1a78757b1072a8a3fac2a4b28d27cfb561c5942ec2701"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"derive-new",
|
"derive-new",
|
||||||
@ -3008,9 +3281,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tract-onnx-opl"
|
name = "tract-onnx-opl"
|
||||||
version = "0.22.0"
|
version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6d1e8a95ae93fdc53143586ec00d816c2831c6958a8a59cc4122ca5b8dba6070"
|
checksum = "87561bf0b84f74a124afc0f1997682728da6cd821083511e0357432954fd24f6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
"log",
|
"log",
|
||||||
@ -3128,6 +3401,36 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vectorscan-rs"
|
||||||
|
version = "0.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "657b43a26c41ea3e5b0de95240a518a011561d204b83f8ea213fddf2cc224fb0"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"foreign-types",
|
||||||
|
"libc",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"vectorscan-rs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vectorscan-rs-sys"
|
||||||
|
version = "0.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "195d07e20b78923c2e8542f95a9bfd31d0cacf19f9134181c292b84055c5c7c1"
|
||||||
|
dependencies = [
|
||||||
|
"cmake",
|
||||||
|
"flate2",
|
||||||
|
"tar",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
@ -3159,6 +3462,51 @@ dependencies = [
|
|||||||
"wit-bindgen",
|
"wit-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.98",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "which"
|
name = "which"
|
||||||
version = "8.0.0"
|
version = "8.0.0"
|
||||||
|
|||||||
7
TODO
Normal file
7
TODO
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
1. 加入 .csv 的紀錄資料夾,實現 .csv 過一天後自動歸檔成 xxx-yyyy-oo-zz.csv
|
||||||
|
2. will change tract-onnx engine to ort-tract engine
|
||||||
|
3. 改善推論效能及速度,在大量資料時
|
||||||
|
4. 實現 ML + RULE 的共用 HashMap 實現共同決策結果
|
||||||
|
5. 代碼最佳化,檢查是否除了 build.rs 以外有無 .unwarp() and eprintln() [x]
|
||||||
|
6. 完成前端 detection 頁面
|
||||||
|
7. 使用 sqllite 實現帳號系統、白黑名單永久記錄
|
||||||
@ -3,7 +3,6 @@ ingress_ifname = "enp4s0f1" # Ingress NIC Name
|
|||||||
egress_ifname = "enp4s0f0" # Egress NIC Name
|
egress_ifname = "enp4s0f0" # Egress NIC Name
|
||||||
geoip_db_name = "GeoLite2-City.mmdb"
|
geoip_db_name = "GeoLite2-City.mmdb"
|
||||||
deep_autoencoder_name = "deep_autoencoder.onnx"
|
deep_autoencoder_name = "deep_autoencoder.onnx"
|
||||||
classifier_name = "classifier.onnx"
|
|
||||||
models_config_name = "inference_config.json"
|
models_config_name = "inference_config.json"
|
||||||
combined_queue_count = 8 # NIC Combined Queue Count (ethtool -l <NIC>)
|
combined_queue_count = 8 # NIC Combined Queue Count (ethtool -l <NIC>)
|
||||||
channel_size = 4096
|
channel_size = 4096
|
||||||
@ -19,8 +18,10 @@ refresh_interval = 5 # Statistics Refresh Time
|
|||||||
max_concurrent_flows = 10000 # max_flows: track up to 10000 concurrent flows
|
max_concurrent_flows = 10000 # max_flows: track up to 10000 concurrent flows
|
||||||
min_packets_for_inference = 5 # min_packets: minimum 10 packets per flow for inference
|
min_packets_for_inference = 5 # min_packets: minimum 10 packets per flow for inference
|
||||||
inference_interval_secs = 5 # interval_secs: run inference every 5 seconds
|
inference_interval_secs = 5 # interval_secs: run inference every 5 seconds
|
||||||
|
min_signature_matches = 3
|
||||||
aggregator_window_secs = 30
|
aggregator_window_secs = 30
|
||||||
inference_batch_size = 200
|
inference_batch_size = 200
|
||||||
|
flow_timeout_us = 60_000_000
|
||||||
|
|
||||||
traffic_logging_mode = true # When true, disables ML inference and records all ingress/egress packets to CSV
|
traffic_logging_mode = true # When true, disables ML inference and records all ingress/egress packets to CSV
|
||||||
traffic_log_csv_path = "traffic_log.csv" # Output CSV file path for traffic logging mode
|
traffic_log_csv_path = "traffic_log.csv" # Output CSV file path for traffic logging mode
|
||||||
Binary file not shown.
Binary file not shown.
@ -1,289 +0,0 @@
|
|||||||
{
|
|
||||||
"created_at": "2026-03-05T14:04:08.736531",
|
|
||||||
"framework": "PyTorch",
|
|
||||||
"model": {
|
|
||||||
"deep_autoencoder": {
|
|
||||||
"file": "deep_autoencoder.onnx",
|
|
||||||
"input_dim": 27,
|
|
||||||
"encoding_dim": 16,
|
|
||||||
"ae_feature_names": [
|
|
||||||
"flow_duration",
|
|
||||||
"fwd_packets",
|
|
||||||
"bwd_packets",
|
|
||||||
"fwd_bytes",
|
|
||||||
"bwd_bytes",
|
|
||||||
"flow_bytes_per_sec",
|
|
||||||
"flow_pkts_per_sec",
|
|
||||||
"fwd_win_bytes",
|
|
||||||
"bwd_win_bytes",
|
|
||||||
"fwd_pkt_len_mean",
|
|
||||||
"bwd_pkt_len_mean",
|
|
||||||
"fwd_iat_mean",
|
|
||||||
"bwd_iat_mean",
|
|
||||||
"flow_iat_mean",
|
|
||||||
"pkt_len_mean",
|
|
||||||
"dst_port",
|
|
||||||
"protocol",
|
|
||||||
"psh_flag_cnt",
|
|
||||||
"ack_flag_cnt",
|
|
||||||
"syn_flag_cnt",
|
|
||||||
"fin_flag_cnt",
|
|
||||||
"rst_flag_cnt",
|
|
||||||
"pkt_len_std",
|
|
||||||
"fwd_pkt_len_std",
|
|
||||||
"bwd_pkt_len_std",
|
|
||||||
"fwd_seg_size_min",
|
|
||||||
"fwd_act_data_pkts"
|
|
||||||
],
|
|
||||||
"ae_threshold": 0.17166012525558472
|
|
||||||
},
|
|
||||||
"classifier": {
|
|
||||||
"file": "classifier.onnx",
|
|
||||||
"type": "ResNet MLP",
|
|
||||||
"n_features": 28,
|
|
||||||
"n_classes": 5,
|
|
||||||
"classifier_feature_names": [
|
|
||||||
"flow_duration",
|
|
||||||
"fwd_packets",
|
|
||||||
"bwd_packets",
|
|
||||||
"fwd_bytes",
|
|
||||||
"bwd_bytes",
|
|
||||||
"flow_bytes_per_sec",
|
|
||||||
"flow_pkts_per_sec",
|
|
||||||
"fwd_win_bytes",
|
|
||||||
"bwd_win_bytes",
|
|
||||||
"fwd_pkt_len_mean",
|
|
||||||
"bwd_pkt_len_mean",
|
|
||||||
"fwd_iat_mean",
|
|
||||||
"bwd_iat_mean",
|
|
||||||
"flow_iat_mean",
|
|
||||||
"pkt_len_mean",
|
|
||||||
"dst_port",
|
|
||||||
"protocol",
|
|
||||||
"psh_flag_cnt",
|
|
||||||
"ack_flag_cnt",
|
|
||||||
"syn_flag_cnt",
|
|
||||||
"fin_flag_cnt",
|
|
||||||
"rst_flag_cnt",
|
|
||||||
"pkt_len_std",
|
|
||||||
"fwd_pkt_len_std",
|
|
||||||
"bwd_pkt_len_std",
|
|
||||||
"fwd_seg_size_min",
|
|
||||||
"fwd_act_data_pkts",
|
|
||||||
"ae_anomaly_score"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"preprocessing": {
|
|
||||||
"ae_clip_params": {
|
|
||||||
"flow_duration": {
|
|
||||||
"lower": 1.0,
|
|
||||||
"upper": 118485085.19000001
|
|
||||||
},
|
|
||||||
"fwd_packets": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 54.0
|
|
||||||
},
|
|
||||||
"bwd_packets": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 82.0
|
|
||||||
},
|
|
||||||
"fwd_bytes": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 6960.0
|
|
||||||
},
|
|
||||||
"bwd_bytes": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 89558.6950000003
|
|
||||||
},
|
|
||||||
"flow_bytes_per_sec": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 12500000.0
|
|
||||||
},
|
|
||||||
"flow_pkts_per_sec": {
|
|
||||||
"lower": 0.0226829350855,
|
|
||||||
"upper": 2000000.0
|
|
||||||
},
|
|
||||||
"fwd_win_bytes": {
|
|
||||||
"lower": -1.0,
|
|
||||||
"upper": 65535.0
|
|
||||||
},
|
|
||||||
"bwd_win_bytes": {
|
|
||||||
"lower": -1.0,
|
|
||||||
"upper": 64000.0
|
|
||||||
},
|
|
||||||
"fwd_pkt_len_mean": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 744.7272727272729
|
|
||||||
},
|
|
||||||
"bwd_pkt_len_mean": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 1393.9601153594992
|
|
||||||
},
|
|
||||||
"fwd_iat_mean": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 86366722.035
|
|
||||||
},
|
|
||||||
"bwd_iat_mean": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 21700000.0
|
|
||||||
},
|
|
||||||
"flow_iat_mean": {
|
|
||||||
"lower": 1.0,
|
|
||||||
"upper": 86353400.64500001
|
|
||||||
},
|
|
||||||
"pkt_len_mean": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 945.0294113974604
|
|
||||||
},
|
|
||||||
"dst_port": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 64499.0
|
|
||||||
},
|
|
||||||
"protocol": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 17.0
|
|
||||||
},
|
|
||||||
"psh_flag_cnt": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 240.0
|
|
||||||
},
|
|
||||||
"ack_flag_cnt": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 655.0
|
|
||||||
},
|
|
||||||
"syn_flag_cnt": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 4.0
|
|
||||||
},
|
|
||||||
"fin_flag_cnt": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 2.0
|
|
||||||
},
|
|
||||||
"rst_flag_cnt": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 1.0
|
|
||||||
},
|
|
||||||
"pkt_len_std": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 943.9221842600019
|
|
||||||
},
|
|
||||||
"fwd_pkt_len_std": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 721.4199648708689
|
|
||||||
},
|
|
||||||
"bwd_pkt_len_std": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 911.0272448420104
|
|
||||||
},
|
|
||||||
"fwd_seg_size_min": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 40.0
|
|
||||||
},
|
|
||||||
"fwd_act_data_pkts": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 58.0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ae_scaler": {
|
|
||||||
"mean": [
|
|
||||||
9783470.902880548,
|
|
||||||
3.6532605393470496,
|
|
||||||
3.2219949045990472,
|
|
||||||
311.35281195367173,
|
|
||||||
1407.508354811673,
|
|
||||||
298997.28705051675,
|
|
||||||
32378.36735569276,
|
|
||||||
6158.3256759444475,
|
|
||||||
6125.023325143747,
|
|
||||||
58.1683929498305,
|
|
||||||
149.72168204849987,
|
|
||||||
2991407.86700246,
|
|
||||||
543337.1970583026,
|
|
||||||
2727511.340181723,
|
|
||||||
105.03829359866097,
|
|
||||||
13463.473246959382,
|
|
||||||
8.920585011866685,
|
|
||||||
5.457814320208867,
|
|
||||||
12.214631683524651,
|
|
||||||
0.4885976379312313,
|
|
||||||
0.246598391240437,
|
|
||||||
0.08478201266637703,
|
|
||||||
151.71809799252043,
|
|
||||||
76.89649795414182,
|
|
||||||
143.21014925828936,
|
|
||||||
21.447338538177014,
|
|
||||||
3.2607463735155715
|
|
||||||
],
|
|
||||||
"std": [
|
|
||||||
27895851.239715174,
|
|
||||||
6.4263677035863696,
|
|
||||||
8.326275195918974,
|
|
||||||
739.2548064174216,
|
|
||||||
7898.711708881367,
|
|
||||||
1187804.3239195002,
|
|
||||||
190280.1548511611,
|
|
||||||
15556.664474188405,
|
|
||||||
17012.235322987635,
|
|
||||||
98.89458775902833,
|
|
||||||
242.79522892109904,
|
|
||||||
13146157.964503227,
|
|
||||||
2242915.166631539,
|
|
||||||
12878527.145353919,
|
|
||||||
151.95316919115754,
|
|
||||||
21264.915159327567,
|
|
||||||
4.943209145195101,
|
|
||||||
23.02750953458606,
|
|
||||||
57.0633472601567,
|
|
||||||
1.2662749497077417,
|
|
||||||
0.4553262784517569,
|
|
||||||
0.278557037237646,
|
|
||||||
211.1373807979287,
|
|
||||||
143.47790203327,
|
|
||||||
228.24531573376805,
|
|
||||||
9.323458649060111,
|
|
||||||
7.228913366949205
|
|
||||||
],
|
|
||||||
"feature_names": [
|
|
||||||
"flow_duration",
|
|
||||||
"fwd_packets",
|
|
||||||
"bwd_packets",
|
|
||||||
"fwd_bytes",
|
|
||||||
"bwd_bytes",
|
|
||||||
"flow_bytes_per_sec",
|
|
||||||
"flow_pkts_per_sec",
|
|
||||||
"fwd_win_bytes",
|
|
||||||
"bwd_win_bytes",
|
|
||||||
"fwd_pkt_len_mean",
|
|
||||||
"bwd_pkt_len_mean",
|
|
||||||
"fwd_iat_mean",
|
|
||||||
"bwd_iat_mean",
|
|
||||||
"flow_iat_mean",
|
|
||||||
"pkt_len_mean",
|
|
||||||
"dst_port",
|
|
||||||
"protocol",
|
|
||||||
"psh_flag_cnt",
|
|
||||||
"ack_flag_cnt",
|
|
||||||
"syn_flag_cnt",
|
|
||||||
"fin_flag_cnt",
|
|
||||||
"rst_flag_cnt",
|
|
||||||
"pkt_len_std",
|
|
||||||
"fwd_pkt_len_std",
|
|
||||||
"bwd_pkt_len_std",
|
|
||||||
"fwd_seg_size_min",
|
|
||||||
"fwd_act_data_pkts"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"post_scaling_clip": {
|
|
||||||
"min": -5.0,
|
|
||||||
"max": 5.0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"attack_labels": {
|
|
||||||
"0": "Brute Force",
|
|
||||||
"1": "DDoS",
|
|
||||||
"2": "DoS",
|
|
||||||
"3": "Exploitation",
|
|
||||||
"4": "Reconnaissance"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,239 +0,0 @@
|
|||||||
{
|
|
||||||
"ae_feature_names": [
|
|
||||||
"flow_duration",
|
|
||||||
"fwd_packets",
|
|
||||||
"bwd_packets",
|
|
||||||
"fwd_bytes",
|
|
||||||
"bwd_bytes",
|
|
||||||
"flow_bytes_per_sec",
|
|
||||||
"flow_pkts_per_sec",
|
|
||||||
"fwd_win_bytes",
|
|
||||||
"bwd_win_bytes",
|
|
||||||
"fwd_pkt_len_mean",
|
|
||||||
"bwd_pkt_len_mean",
|
|
||||||
"fwd_iat_mean",
|
|
||||||
"bwd_iat_mean",
|
|
||||||
"flow_iat_mean",
|
|
||||||
"pkt_len_mean",
|
|
||||||
"dst_port",
|
|
||||||
"protocol",
|
|
||||||
"psh_flag_cnt",
|
|
||||||
"ack_flag_cnt",
|
|
||||||
"syn_flag_cnt",
|
|
||||||
"fin_flag_cnt",
|
|
||||||
"rst_flag_cnt",
|
|
||||||
"pkt_len_std",
|
|
||||||
"fwd_pkt_len_std",
|
|
||||||
"bwd_pkt_len_std",
|
|
||||||
"fwd_seg_size_min",
|
|
||||||
"fwd_act_data_pkts"
|
|
||||||
],
|
|
||||||
"ae_clip_params": {
|
|
||||||
"flow_duration": {
|
|
||||||
"lower": 1.0,
|
|
||||||
"upper": 118485085.19000001
|
|
||||||
},
|
|
||||||
"fwd_packets": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 54.0
|
|
||||||
},
|
|
||||||
"bwd_packets": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 82.0
|
|
||||||
},
|
|
||||||
"fwd_bytes": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 6960.0
|
|
||||||
},
|
|
||||||
"bwd_bytes": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 89558.6950000003
|
|
||||||
},
|
|
||||||
"flow_bytes_per_sec": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 12500000.0
|
|
||||||
},
|
|
||||||
"flow_pkts_per_sec": {
|
|
||||||
"lower": 0.0226829350855,
|
|
||||||
"upper": 2000000.0
|
|
||||||
},
|
|
||||||
"fwd_win_bytes": {
|
|
||||||
"lower": -1.0,
|
|
||||||
"upper": 65535.0
|
|
||||||
},
|
|
||||||
"bwd_win_bytes": {
|
|
||||||
"lower": -1.0,
|
|
||||||
"upper": 64000.0
|
|
||||||
},
|
|
||||||
"fwd_pkt_len_mean": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 744.7272727272729
|
|
||||||
},
|
|
||||||
"bwd_pkt_len_mean": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 1393.9601153594992
|
|
||||||
},
|
|
||||||
"fwd_iat_mean": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 86366722.035
|
|
||||||
},
|
|
||||||
"bwd_iat_mean": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 21700000.0
|
|
||||||
},
|
|
||||||
"flow_iat_mean": {
|
|
||||||
"lower": 1.0,
|
|
||||||
"upper": 86353400.64500001
|
|
||||||
},
|
|
||||||
"pkt_len_mean": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 945.0294113974604
|
|
||||||
},
|
|
||||||
"dst_port": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 64499.0
|
|
||||||
},
|
|
||||||
"protocol": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 17.0
|
|
||||||
},
|
|
||||||
"psh_flag_cnt": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 240.0
|
|
||||||
},
|
|
||||||
"ack_flag_cnt": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 655.0
|
|
||||||
},
|
|
||||||
"syn_flag_cnt": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 4.0
|
|
||||||
},
|
|
||||||
"fin_flag_cnt": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 2.0
|
|
||||||
},
|
|
||||||
"rst_flag_cnt": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 1.0
|
|
||||||
},
|
|
||||||
"pkt_len_std": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 943.9221842600019
|
|
||||||
},
|
|
||||||
"fwd_pkt_len_std": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 721.4199648708689
|
|
||||||
},
|
|
||||||
"bwd_pkt_len_std": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 911.0272448420104
|
|
||||||
},
|
|
||||||
"fwd_seg_size_min": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 40.0
|
|
||||||
},
|
|
||||||
"fwd_act_data_pkts": {
|
|
||||||
"lower": 0.0,
|
|
||||||
"upper": 58.0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ae_scaler_mean": [
|
|
||||||
9783470.902880548,
|
|
||||||
3.6532605393470496,
|
|
||||||
3.2219949045990472,
|
|
||||||
311.35281195367173,
|
|
||||||
1407.508354811673,
|
|
||||||
298997.28705051675,
|
|
||||||
32378.36735569276,
|
|
||||||
6158.3256759444475,
|
|
||||||
6125.023325143747,
|
|
||||||
58.1683929498305,
|
|
||||||
149.72168204849987,
|
|
||||||
2991407.86700246,
|
|
||||||
543337.1970583026,
|
|
||||||
2727511.340181723,
|
|
||||||
105.03829359866097,
|
|
||||||
13463.473246959382,
|
|
||||||
8.920585011866685,
|
|
||||||
5.457814320208867,
|
|
||||||
12.214631683524651,
|
|
||||||
0.4885976379312313,
|
|
||||||
0.246598391240437,
|
|
||||||
0.08478201266637703,
|
|
||||||
151.71809799252043,
|
|
||||||
76.89649795414182,
|
|
||||||
143.21014925828936,
|
|
||||||
21.447338538177014,
|
|
||||||
3.2607463735155715
|
|
||||||
],
|
|
||||||
"ae_scaler_std": [
|
|
||||||
27895851.239715174,
|
|
||||||
6.4263677035863696,
|
|
||||||
8.326275195918974,
|
|
||||||
739.2548064174216,
|
|
||||||
7898.711708881367,
|
|
||||||
1187804.3239195002,
|
|
||||||
190280.1548511611,
|
|
||||||
15556.664474188405,
|
|
||||||
17012.235322987635,
|
|
||||||
98.89458775902833,
|
|
||||||
242.79522892109904,
|
|
||||||
13146157.964503227,
|
|
||||||
2242915.166631539,
|
|
||||||
12878527.145353919,
|
|
||||||
151.95316919115754,
|
|
||||||
21264.915159327567,
|
|
||||||
4.943209145195101,
|
|
||||||
23.02750953458606,
|
|
||||||
57.0633472601567,
|
|
||||||
1.2662749497077417,
|
|
||||||
0.4553262784517569,
|
|
||||||
0.278557037237646,
|
|
||||||
211.1373807979287,
|
|
||||||
143.47790203327,
|
|
||||||
228.24531573376805,
|
|
||||||
9.323458649060111,
|
|
||||||
7.228913366949205
|
|
||||||
],
|
|
||||||
"ae_post_clip_min": -5.0,
|
|
||||||
"ae_post_clip_max": 5.0,
|
|
||||||
"ae_threshold": 0.17166012525558472,
|
|
||||||
"classifier_feature_names": [
|
|
||||||
"flow_duration",
|
|
||||||
"fwd_packets",
|
|
||||||
"bwd_packets",
|
|
||||||
"fwd_bytes",
|
|
||||||
"bwd_bytes",
|
|
||||||
"flow_bytes_per_sec",
|
|
||||||
"flow_pkts_per_sec",
|
|
||||||
"fwd_win_bytes",
|
|
||||||
"bwd_win_bytes",
|
|
||||||
"fwd_pkt_len_mean",
|
|
||||||
"bwd_pkt_len_mean",
|
|
||||||
"fwd_iat_mean",
|
|
||||||
"bwd_iat_mean",
|
|
||||||
"flow_iat_mean",
|
|
||||||
"pkt_len_mean",
|
|
||||||
"dst_port",
|
|
||||||
"protocol",
|
|
||||||
"psh_flag_cnt",
|
|
||||||
"ack_flag_cnt",
|
|
||||||
"syn_flag_cnt",
|
|
||||||
"fin_flag_cnt",
|
|
||||||
"rst_flag_cnt",
|
|
||||||
"pkt_len_std",
|
|
||||||
"fwd_pkt_len_std",
|
|
||||||
"bwd_pkt_len_std",
|
|
||||||
"fwd_seg_size_min",
|
|
||||||
"fwd_act_data_pkts",
|
|
||||||
"ae_anomaly_score"
|
|
||||||
],
|
|
||||||
"attack_labels": {
|
|
||||||
"0": "Brute Force",
|
|
||||||
"1": "DDoS",
|
|
||||||
"2": "DoS",
|
|
||||||
"3": "Exploitation",
|
|
||||||
"4": "Reconnaissance"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
net-guardia-frontend
Submodule
1
net-guardia-frontend
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit c7f50458778c70a124ac07d2356a37c460b82745
|
||||||
6
net-guardia-frontend/package-lock.json
generated
6
net-guardia-frontend/package-lock.json
generated
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "net-guardia-frontend",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {}
|
|
||||||
}
|
|
||||||
@ -35,13 +35,21 @@ xsk-rs = { workspace = true }
|
|||||||
maxminddb = "0.27.1"
|
maxminddb = "0.27.1"
|
||||||
lru = "0.16.2"
|
lru = "0.16.2"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
tract-onnx = "0.22.0"
|
tract-onnx = "0.22.1"
|
||||||
|
chrono = "0.4"
|
||||||
|
vectorscan-rs = "0.0.6"
|
||||||
|
protolens = "0.2.3"
|
||||||
|
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||||
|
|
||||||
|
ort-tract = "0.3.0+0.22"
|
||||||
|
ort = { version = "=2.0.0-rc.12", default-features = false, features = ["std", "ndarray", "alternative-backend"] }
|
||||||
#csv = "1.4.0"
|
#csv = "1.4.0"
|
||||||
#anyhow = "1.0.100"
|
#anyhow = "1.0.100"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
cargo_metadata = { workspace = true }
|
cargo_metadata = { workspace = true }
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
|
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "net-guardia"
|
name = "net-guardia"
|
||||||
|
|||||||
@ -1,16 +1,58 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{BufRead as _, BufReader};
|
use std::io::{BufRead as _, BufReader};
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Child, Command, Stdio};
|
use std::process::{Child, Command, Stdio};
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use cargo_metadata::{Artifact, CompilerMessage, Message, Metadata, MetadataCommand, Package, Target, TargetKind};
|
use cargo_metadata::{Artifact, CompilerMessage, Message, Metadata, MetadataCommand, Package, Target, TargetKind};
|
||||||
|
|
||||||
|
/// Buffer IDs — must stay in sync with app_layer/mod.rs and rule_engine.rs.
|
||||||
|
const BUF_RAW: u8 = 0;
|
||||||
|
const BUF_HTTP_URI: u8 = 1;
|
||||||
|
const BUF_HTTP_HEADER: u8 = 2;
|
||||||
|
const BUF_HTTP_CLIENT_BODY: u8 = 3;
|
||||||
|
const BUF_HTTP_SERVER_BODY: u8 = 4;
|
||||||
|
const BUF_HTTP_METHOD: u8 = 5;
|
||||||
|
const BUF_HTTP_USER_AGENT: u8 = 6;
|
||||||
|
const BUF_HTTP_HOST: u8 = 7;
|
||||||
|
const BUF_HTTP_STAT_CODE: u8 = 8;
|
||||||
|
const BUF_HTTP_COOKIE: u8 = 9;
|
||||||
|
const BUF_HTTP_RAW_URI: u8 = 10;
|
||||||
|
const BUF_TLS_SNI: u8 = 11;
|
||||||
|
const BUF_DNS_QUERY: u8 = 12;
|
||||||
|
|
||||||
|
struct ContentMatch {
|
||||||
|
pattern: Vec<u8>,
|
||||||
|
nocase: bool,
|
||||||
|
negated: bool,
|
||||||
|
is_fast_pattern: bool,
|
||||||
|
has_distance: bool,
|
||||||
|
has_within: bool,
|
||||||
|
has_offset: bool,
|
||||||
|
has_depth: bool,
|
||||||
|
distance: i32,
|
||||||
|
within: i32,
|
||||||
|
offset: u16,
|
||||||
|
depth: u16,
|
||||||
|
/// Target buffer (0 = raw payload, 1 = http_uri, etc.)
|
||||||
|
buffer: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SigEntry {
|
||||||
|
sid: u32,
|
||||||
|
flow_dir: u8,
|
||||||
|
dports: Vec<u16>,
|
||||||
|
msg: String,
|
||||||
|
chain: Vec<ContentMatch>,
|
||||||
|
fast_pattern_idx: usize,
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
build_ingress_ebpf();
|
build_ingress_ebpf();
|
||||||
build_egress_ebpf();
|
build_egress_ebpf();
|
||||||
build_frontend();
|
build_frontend();
|
||||||
|
build_vectorscan_db();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_ingress_ebpf() {
|
fn build_ingress_ebpf() {
|
||||||
@ -88,10 +130,10 @@ fn build_ingress_ebpf() {
|
|||||||
#[allow(clippy::collapsible_match)]
|
#[allow(clippy::collapsible_match)]
|
||||||
match message.expect("valid JSON") {
|
match message.expect("valid JSON") {
|
||||||
Message::CompilerArtifact(Artifact {
|
Message::CompilerArtifact(Artifact {
|
||||||
executable,
|
executable,
|
||||||
target: Target { name, .. },
|
target: Target { name, .. },
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
if let Some(executable) = executable {
|
if let Some(executable) = executable {
|
||||||
executables.push((name, executable.into_std_path_buf()));
|
executables.push((name, executable.into_std_path_buf()));
|
||||||
}
|
}
|
||||||
@ -208,10 +250,10 @@ fn build_egress_ebpf() {
|
|||||||
#[allow(clippy::collapsible_match)]
|
#[allow(clippy::collapsible_match)]
|
||||||
match message.expect("valid JSON") {
|
match message.expect("valid JSON") {
|
||||||
Message::CompilerArtifact(Artifact {
|
Message::CompilerArtifact(Artifact {
|
||||||
executable,
|
executable,
|
||||||
target: Target { name, .. },
|
target: Target { name, .. },
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
if let Some(executable) = executable {
|
if let Some(executable) = executable {
|
||||||
executables.push((name, executable.into_std_path_buf()));
|
executables.push((name, executable.into_std_path_buf()));
|
||||||
}
|
}
|
||||||
@ -429,3 +471,635 @@ fn copy_dir_all(src: &PathBuf, dst: &PathBuf) -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn build_vectorscan_db() {
|
||||||
|
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||||
|
let out_dir = &manifest_dir.join("static").join("db");
|
||||||
|
let artifact_dir = &manifest_dir.join("static").join("artifacts");
|
||||||
|
let rules_dir = &manifest_dir.join("static").join("rules");
|
||||||
|
|
||||||
|
println!("cargo:rerun-if-changed={}", rules_dir.display());
|
||||||
|
if let Ok(rd) = fs::read_dir(&rules_dir) {
|
||||||
|
for entry in rd.flatten() {
|
||||||
|
let p = entry.path();
|
||||||
|
if p.extension().and_then(|e| e.to_str()) == Some("rules") {
|
||||||
|
println!("cargo:rerun-if-changed={}", p.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sigs = collect_sig_entries(&rules_dir);
|
||||||
|
|
||||||
|
let db_path = out_dir.join("rules.db");
|
||||||
|
write_rules_db(&db_path, &sigs);
|
||||||
|
|
||||||
|
println!("cargo:rustc-env=RULES_DB_PATH={}", out_dir.display());
|
||||||
|
println!("cargo:rustc-env=ARTIFACTCS_PATH={}", artifact_dir.display());
|
||||||
|
|
||||||
|
let total_contents: usize = sigs.iter().map(|s| s.chain.len()).sum();
|
||||||
|
println!(
|
||||||
|
"cargo:warning=NetGuardia: {} signatures ({} content entries) ready",
|
||||||
|
sigs.len(),
|
||||||
|
total_contents
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_sig_entries(rules_dir: &PathBuf) -> Vec<SigEntry> {
|
||||||
|
if !rules_dir.exists() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut paths: Vec<_> = fs::read_dir(rules_dir)
|
||||||
|
.unwrap()
|
||||||
|
.flatten()
|
||||||
|
.map(|e| e.path())
|
||||||
|
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("rules"))
|
||||||
|
.collect();
|
||||||
|
paths.sort();
|
||||||
|
|
||||||
|
let mut sigs = Vec::new();
|
||||||
|
|
||||||
|
for path in paths {
|
||||||
|
let text = match fs::read_to_string(&path) {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
for line in text.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.starts_with('#') || line.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if line.contains("noalert") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let chain = extract_content_chain(line);
|
||||||
|
if chain.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(fast_idx) = select_fast_pattern(&chain) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
sigs.push(SigEntry {
|
||||||
|
sid: extract_sid(line).unwrap_or(0),
|
||||||
|
flow_dir: extract_flow_dir(line),
|
||||||
|
dports: extract_dst_ports(line),
|
||||||
|
msg: extract_msg(line).unwrap_or_default(),
|
||||||
|
chain,
|
||||||
|
fast_pattern_idx: fast_idx,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sigs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map a Suricata/Snort buffer keyword to its buffer ID.
|
||||||
|
/// Handles both new-style (`http.uri`) and old-style (`http_uri`) forms.
|
||||||
|
fn detect_buffer_keyword(kw: &str) -> Option<u8> {
|
||||||
|
match kw {
|
||||||
|
"http.uri" | "http_uri" => Some(BUF_HTTP_URI),
|
||||||
|
"http.header" | "http_header" => Some(BUF_HTTP_HEADER),
|
||||||
|
"http.request_body" | "http_client_body" => Some(BUF_HTTP_CLIENT_BODY),
|
||||||
|
"http.response_body"| "http_server_body" => Some(BUF_HTTP_SERVER_BODY),
|
||||||
|
"http.method" | "http_method" => Some(BUF_HTTP_METHOD),
|
||||||
|
"http.user_agent" | "http_user_agent" => Some(BUF_HTTP_USER_AGENT),
|
||||||
|
"http.host" | "http_host" => Some(BUF_HTTP_HOST),
|
||||||
|
"http.stat_code" | "http_stat_code" => Some(BUF_HTTP_STAT_CODE),
|
||||||
|
"http.cookie" | "http_cookie" => Some(BUF_HTTP_COOKIE),
|
||||||
|
"http.raw_uri" | "http_raw_uri" => Some(BUF_HTTP_RAW_URI),
|
||||||
|
"tls.sni" | "tls_sni" => Some(BUF_TLS_SNI),
|
||||||
|
"dns_query" | "dns.query" => Some(BUF_DNS_QUERY),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tokenise the rule options section into `(keyword, value?)` pairs,
|
||||||
|
/// splitting on `;` while respecting double-quoted strings.
|
||||||
|
fn tokenize_options(rule: &str) -> Vec<String> {
|
||||||
|
let opts_start = rule.find('(').map(|p| p + 1).unwrap_or(0);
|
||||||
|
let opts_end = rule.rfind(')').unwrap_or(rule.len());
|
||||||
|
let opts = &rule[opts_start..opts_end.max(opts_start)];
|
||||||
|
|
||||||
|
let mut tokens: Vec<String> = Vec::new();
|
||||||
|
let mut current = String::new();
|
||||||
|
let mut in_quotes = false;
|
||||||
|
|
||||||
|
for ch in opts.chars() {
|
||||||
|
match ch {
|
||||||
|
'"' => {
|
||||||
|
in_quotes = !in_quotes;
|
||||||
|
current.push(ch);
|
||||||
|
}
|
||||||
|
';' if !in_quotes => {
|
||||||
|
let t = current.trim().to_string();
|
||||||
|
if !t.is_empty() {
|
||||||
|
tokens.push(t);
|
||||||
|
}
|
||||||
|
current.clear();
|
||||||
|
}
|
||||||
|
_ => current.push(ch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let t = current.trim().to_string();
|
||||||
|
if !t.is_empty() {
|
||||||
|
tokens.push(t);
|
||||||
|
}
|
||||||
|
tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_content_chain(rule: &str) -> Vec<ContentMatch> {
|
||||||
|
let tokens = tokenize_options(rule);
|
||||||
|
let mut entries: Vec<ContentMatch> = Vec::new();
|
||||||
|
let mut sticky_buffer: u8 = BUF_RAW;
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
while i < tokens.len() {
|
||||||
|
let tok = tokens[i].as_str();
|
||||||
|
|
||||||
|
// New-style sticky buffer keyword (e.g. `http.uri`)
|
||||||
|
if let Some(buf) = detect_buffer_keyword(tok) {
|
||||||
|
sticky_buffer = buf;
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// content: keyword
|
||||||
|
let content_rest = if let Some(r) = tok.strip_prefix("content:") {
|
||||||
|
r
|
||||||
|
} else {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let negated = content_rest.starts_with('!');
|
||||||
|
let quoted = if negated { &content_rest[1..] } else { content_rest };
|
||||||
|
if !quoted.starts_with('"') {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unquote: content token already has the full quoted string because
|
||||||
|
// tokenize_options preserves quotes inside options.
|
||||||
|
let inner = unquote_content(quoted);
|
||||||
|
let Some(raw_bytes) = parse_content_bytes(&inner) else {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if raw_bytes.contains(&0u8) || raw_bytes.len() < 2 {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut entry = ContentMatch {
|
||||||
|
pattern: raw_bytes,
|
||||||
|
nocase: false,
|
||||||
|
negated,
|
||||||
|
is_fast_pattern: false,
|
||||||
|
has_distance: false,
|
||||||
|
has_within: false,
|
||||||
|
has_offset: false,
|
||||||
|
has_depth: false,
|
||||||
|
distance: 0,
|
||||||
|
within: 0,
|
||||||
|
offset: 0,
|
||||||
|
depth: 0,
|
||||||
|
buffer: sticky_buffer,
|
||||||
|
};
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
|
||||||
|
// Collect modifier tokens until the next content: or new buffer keyword.
|
||||||
|
while i < tokens.len() {
|
||||||
|
let mod_tok = tokens[i].as_str();
|
||||||
|
|
||||||
|
// Stop at next content: keyword — it will be handled in the outer loop.
|
||||||
|
if mod_tok.starts_with("content:") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old-style buffer keyword overrides the sticky buffer for this entry.
|
||||||
|
if let Some(buf) = detect_buffer_keyword(mod_tok) {
|
||||||
|
entry.buffer = buf;
|
||||||
|
// A new-style sticky keyword also updates the global sticky state.
|
||||||
|
if mod_tok.contains('.') {
|
||||||
|
sticky_buffer = buf;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
// Stop if new-style (becomes sticky for next content).
|
||||||
|
if mod_tok.contains('.') { break; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply other modifiers.
|
||||||
|
match mod_tok {
|
||||||
|
"nocase" => entry.nocase = true,
|
||||||
|
"fast_pattern" => entry.is_fast_pattern = true,
|
||||||
|
_ => {
|
||||||
|
if let Some(v) = mod_tok.strip_prefix("distance:") {
|
||||||
|
if let Ok(n) = v.trim().parse::<i32>() {
|
||||||
|
entry.distance = n;
|
||||||
|
entry.has_distance = true;
|
||||||
|
}
|
||||||
|
} else if let Some(v) = mod_tok.strip_prefix("within:") {
|
||||||
|
if let Ok(n) = v.trim().parse::<i32>() {
|
||||||
|
entry.within = n;
|
||||||
|
entry.has_within = true;
|
||||||
|
}
|
||||||
|
} else if let Some(v) = mod_tok.strip_prefix("offset:") {
|
||||||
|
if let Ok(n) = v.trim().parse::<u16>() {
|
||||||
|
entry.offset = n;
|
||||||
|
entry.has_offset = true;
|
||||||
|
}
|
||||||
|
} else if let Some(v) = mod_tok.strip_prefix("depth:") {
|
||||||
|
if let Ok(n) = v.trim().parse::<u16>() {
|
||||||
|
entry.depth = n;
|
||||||
|
entry.has_depth = true;
|
||||||
|
}
|
||||||
|
} else if mod_tok.starts_with("fast_pattern:") {
|
||||||
|
entry.is_fast_pattern = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
entries
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Strip the surrounding double-quotes from a content token like `"foo|0d0a|"`.
|
||||||
|
fn unquote_content(s: &str) -> String {
|
||||||
|
if s.starts_with('"') && s.len() >= 2 {
|
||||||
|
// Find the closing unescaped quote.
|
||||||
|
let inner = &s[1..];
|
||||||
|
let mut out = String::new();
|
||||||
|
let mut chars = inner.chars();
|
||||||
|
loop {
|
||||||
|
match chars.next() {
|
||||||
|
None | Some('"') => break,
|
||||||
|
Some('\\') => {
|
||||||
|
if let Some(c) = chars.next() {
|
||||||
|
out.push('\\');
|
||||||
|
out.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(c) => out.push(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
} else {
|
||||||
|
s.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select the fast-pattern index for vectorscan prefilter.
|
||||||
|
/// Prefers explicit fast_pattern keyword, then the longest non-negated pattern.
|
||||||
|
fn select_fast_pattern(chain: &[ContentMatch]) -> Option<usize> {
|
||||||
|
if let Some(idx) = chain.iter().position(|e| e.is_fast_pattern && !e.negated) {
|
||||||
|
return Some(idx);
|
||||||
|
}
|
||||||
|
chain.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, e)| !e.negated && e.pattern.len() >= 4)
|
||||||
|
.max_by_key(|(_, e)| e.pattern.len())
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write all signature data to a SQLite rules.db.
|
||||||
|
///
|
||||||
|
/// Schema:
|
||||||
|
/// patterns(id, expression BLOB, nocase) — vectorscan prefilter; id = sig index
|
||||||
|
/// signatures(id, sid, flow_dir, msg)
|
||||||
|
/// signature_ports(sig_id, dport)
|
||||||
|
/// content_entries(sig_id, entry_order, pattern BLOB, nocase, negated,
|
||||||
|
/// has_distance, has_within, has_offset, has_depth,
|
||||||
|
/// distance, within, off, depth, buffer)
|
||||||
|
fn write_rules_db(path: &Path, sigs: &[SigEntry]) {
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
|
||||||
|
let conn = rusqlite::Connection::open(path)
|
||||||
|
.unwrap_or_else(|e| panic!("failed to open rules.db at {path:?}: {e}"));
|
||||||
|
|
||||||
|
conn.execute_batch("
|
||||||
|
CREATE TABLE patterns (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
expression BLOB NOT NULL,
|
||||||
|
nocase INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
CREATE TABLE signatures (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
sid INTEGER NOT NULL,
|
||||||
|
flow_dir INTEGER NOT NULL,
|
||||||
|
msg TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE signature_ports (
|
||||||
|
sig_id INTEGER NOT NULL,
|
||||||
|
dport INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE content_entries (
|
||||||
|
sig_id INTEGER NOT NULL,
|
||||||
|
entry_order INTEGER NOT NULL,
|
||||||
|
pattern BLOB NOT NULL,
|
||||||
|
nocase INTEGER NOT NULL DEFAULT 0,
|
||||||
|
negated INTEGER NOT NULL DEFAULT 0,
|
||||||
|
has_distance INTEGER NOT NULL DEFAULT 0,
|
||||||
|
has_within INTEGER NOT NULL DEFAULT 0,
|
||||||
|
has_offset INTEGER NOT NULL DEFAULT 0,
|
||||||
|
has_depth INTEGER NOT NULL DEFAULT 0,
|
||||||
|
distance INTEGER NOT NULL DEFAULT 0,
|
||||||
|
within INTEGER NOT NULL DEFAULT 0,
|
||||||
|
off INTEGER NOT NULL DEFAULT 0,
|
||||||
|
depth INTEGER NOT NULL DEFAULT 0,
|
||||||
|
buffer INTEGER NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (sig_id, entry_order)
|
||||||
|
);
|
||||||
|
").unwrap_or_else(|e| panic!("rules.db schema failed: {e}"));
|
||||||
|
|
||||||
|
let tx = conn.unchecked_transaction()
|
||||||
|
.unwrap_or_else(|e| panic!("rules.db transaction failed: {e}"));
|
||||||
|
|
||||||
|
for (sig_idx, sig) in sigs.iter().enumerate() {
|
||||||
|
let id = sig_idx as i64;
|
||||||
|
|
||||||
|
let fp = &sig.chain[sig.fast_pattern_idx];
|
||||||
|
let expression = regex_escape(&fp.pattern);
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO patterns (id, expression, nocase) VALUES (?1, ?2, ?3)",
|
||||||
|
rusqlite::params![id, expression, fp.nocase as i64],
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO signatures (id, sid, flow_dir, msg) VALUES (?1, ?2, ?3, ?4)",
|
||||||
|
rusqlite::params![id, sig.sid as i64, sig.flow_dir as i64, &sig.msg],
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
for &dport in &sig.dports {
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO signature_ports (sig_id, dport) VALUES (?1, ?2)",
|
||||||
|
rusqlite::params![id, dport as i64],
|
||||||
|
).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (order, entry) in sig.chain.iter().enumerate() {
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO content_entries \
|
||||||
|
(sig_id, entry_order, pattern, nocase, negated, \
|
||||||
|
has_distance, has_within, has_offset, has_depth, \
|
||||||
|
distance, within, off, depth, buffer) \
|
||||||
|
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14)",
|
||||||
|
rusqlite::params![
|
||||||
|
id,
|
||||||
|
order as i64,
|
||||||
|
&entry.pattern,
|
||||||
|
entry.nocase as i64,
|
||||||
|
entry.negated as i64,
|
||||||
|
entry.has_distance as i64,
|
||||||
|
entry.has_within as i64,
|
||||||
|
entry.has_offset as i64,
|
||||||
|
entry.has_depth as i64,
|
||||||
|
entry.distance as i64,
|
||||||
|
entry.within as i64,
|
||||||
|
entry.offset as i64,
|
||||||
|
entry.depth as i64,
|
||||||
|
entry.buffer as i64,
|
||||||
|
],
|
||||||
|
).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit().unwrap_or_else(|e| panic!("rules.db commit failed: {e}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_sid(rule: &str) -> Option<u32> {
|
||||||
|
let pos = rule.find("sid:")?;
|
||||||
|
let rest = &rule[pos + 4..];
|
||||||
|
let end = rest.find(|c: char| !c.is_ascii_digit()).unwrap_or(rest.len());
|
||||||
|
rest[..end].parse().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_msg(rule: &str) -> Option<String> {
|
||||||
|
let pos = rule.find("msg:\"")?;
|
||||||
|
let rest = &rule[pos + 5..];
|
||||||
|
let mut out = String::new();
|
||||||
|
let mut chars = rest.chars();
|
||||||
|
loop {
|
||||||
|
match chars.next() {
|
||||||
|
None | Some('"') => break,
|
||||||
|
Some('\\') => {
|
||||||
|
if let Some(c) = chars.next() {
|
||||||
|
out.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(c) => out.push(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a Snort/Suricata content string into raw bytes.
|
||||||
|
/// Handles `|XX XX|` hex sections and `\;` / `\\` escapes.
|
||||||
|
fn parse_content_bytes(s: &str) -> Option<Vec<u8>> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut chars = s.chars();
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
match c {
|
||||||
|
'|' => {
|
||||||
|
let mut hex = String::new();
|
||||||
|
loop {
|
||||||
|
match chars.next() {
|
||||||
|
Some('|') | None => break,
|
||||||
|
Some(h) => hex.push(h),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let hex = hex.replace(' ', "");
|
||||||
|
if hex.len() % 2 != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
for i in (0..hex.len()).step_by(2) {
|
||||||
|
out.push(u8::from_str_radix(&hex[i..i + 2], 16).ok()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'\\' => match chars.next() {
|
||||||
|
Some(';') => out.push(b';'),
|
||||||
|
Some('\\') => out.push(b'\\'),
|
||||||
|
Some(c) => {
|
||||||
|
out.push(b'\\');
|
||||||
|
out.push(c as u8);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
},
|
||||||
|
c => out.push(c as u8),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Escape regex metacharacters so literal byte patterns work in vectorscan.
|
||||||
|
fn regex_escape(bytes: &[u8]) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(bytes.len() * 2);
|
||||||
|
for &b in bytes {
|
||||||
|
if matches!(
|
||||||
|
b,
|
||||||
|
b'.' | b'^' | b'$' | b'*' | b'+' | b'?' | b'(' | b')' |
|
||||||
|
b'[' | b']' | b'{' | b'}' | b'\\' | b'|'
|
||||||
|
) {
|
||||||
|
out.push(b'\\');
|
||||||
|
}
|
||||||
|
out.push(b);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns flow direction: 0=any, 1=to_server (ingress), 2=to_client (egress).
|
||||||
|
fn extract_flow_dir(rule: &str) -> u8 {
|
||||||
|
let Some(pos) = rule.find("flow:") else { return 0 };
|
||||||
|
let rest = &rule[pos + 5..];
|
||||||
|
let seg_end = rest.find(';').unwrap_or(rest.len());
|
||||||
|
let opts = &rest[..seg_end];
|
||||||
|
if opts.contains("to_server") { 1 }
|
||||||
|
else if opts.contains("to_client") { 2 }
|
||||||
|
else { 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse destination ports from the rule header (token index 6, before the '(').
|
||||||
|
/// Returns empty Vec for "any" or negation-only groups (= match all).
|
||||||
|
/// Expands common Suricata/Snort port variables.
|
||||||
|
fn extract_dst_ports(rule: &str) -> Vec<u16> {
|
||||||
|
// Rule header ends at '('
|
||||||
|
let header = match rule.find('(') {
|
||||||
|
Some(pos) => &rule[..pos],
|
||||||
|
None => return Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tokenise the header — we need token index 6 (0-based):
|
||||||
|
// action proto src_ip src_port direction dst_ip dst_port
|
||||||
|
let tokens: Vec<&str> = header.split_whitespace().collect();
|
||||||
|
let port_token = match tokens.get(6) {
|
||||||
|
Some(t) => *t,
|
||||||
|
None => return Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
expand_port_token(port_token)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_port_token(token: &str) -> Vec<u16> {
|
||||||
|
match token {
|
||||||
|
// HTTP / Web
|
||||||
|
"$HTTP_PORTS" => return vec![80, 443, 8080, 8443, 8000, 8888],
|
||||||
|
"$HTTP_PORTS2" => return vec![80, 8080],
|
||||||
|
"$HTTPS_PORTS" => return vec![443, 8443],
|
||||||
|
"$FILE_DATA_PORTS" => return vec![80, 443, 8080, 8443, 110, 143],
|
||||||
|
"$PROXY_PORTS" => return vec![3128, 8080, 8118, 8888],
|
||||||
|
// Mail
|
||||||
|
"$SMTP_PORTS" => return vec![25, 587, 465],
|
||||||
|
"$IMAP_PORTS" => return vec![143, 993],
|
||||||
|
"$POP3_PORTS" => return vec![110, 995],
|
||||||
|
// File transfer / Remote
|
||||||
|
"$FTP_PORTS" => return vec![21],
|
||||||
|
"$FTP_DATA" => return vec![20],
|
||||||
|
"$SSH_PORTS" => return vec![22],
|
||||||
|
"$TELNET_PORTS" => return vec![23],
|
||||||
|
"$RDP_PORTS" => return vec![3389],
|
||||||
|
"$VNC_PORTS" => return vec![5900, 5901, 5902, 5903],
|
||||||
|
// Database
|
||||||
|
"$SQL_PORTS" => return vec![3306, 5432, 1433, 1521],
|
||||||
|
"$ORACLE_PORTS" => return vec![1521, 1526],
|
||||||
|
"$MSSQL_PORTS" => return vec![1433, 1434],
|
||||||
|
"$MYSQL_PORTS" => return vec![3306],
|
||||||
|
"$PGSQL_PORTS" => return vec![5432],
|
||||||
|
"$MONGODB_PORTS" => return vec![27017, 27018],
|
||||||
|
"$REDIS_PORTS" => return vec![6379],
|
||||||
|
"$MEMCACHED_PORTS" => return vec![11211],
|
||||||
|
"$ELASTICSEARCH_PORTS" => return vec![9200, 9300],
|
||||||
|
// DNS / Directory
|
||||||
|
"$DNS_PORTS" => return vec![53],
|
||||||
|
"$LDAP_PORTS" => return vec![389, 636, 3268, 3269],
|
||||||
|
"$KERBEROS_PORTS" => return vec![88, 464],
|
||||||
|
// VoIP / Messaging
|
||||||
|
"$SIP_PORTS" => return vec![5060, 5061],
|
||||||
|
"$IRC_PORTS" => return vec![6667, 6668, 6669, 7000],
|
||||||
|
"$JABBER_PORTS" => return vec![5222, 5223],
|
||||||
|
"$MSN_PORTS" => return vec![1863],
|
||||||
|
"$AIM_PORTS" => return vec![5190],
|
||||||
|
// Network management
|
||||||
|
"$SNMP_PORTS" => return vec![161, 162],
|
||||||
|
"$SYSLOG_PORTS" => return vec![514],
|
||||||
|
// Industrial / SCADA
|
||||||
|
"$MODBUS_PORTS" => return vec![502],
|
||||||
|
"$DNP3_PORTS" => return vec![20000],
|
||||||
|
"$ENIP_PORTS" => return vec![44818],
|
||||||
|
"$BACnet_PORTS" => return vec![47808],
|
||||||
|
"$VXLAN_PORTS" => return vec![4789],
|
||||||
|
"$TEREDO_PORTS" => return vec![3544],
|
||||||
|
// Shellcode / generic catch-alls
|
||||||
|
// Suricata default: !80 — we can't enumerate the complement, treat as any
|
||||||
|
"$SHELLCODE_PORTS" => return Vec::new(),
|
||||||
|
// Wildcard
|
||||||
|
"any" | "!any" => return Vec::new(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = token; // keep the &str binding
|
||||||
|
|
||||||
|
// Any remaining unknown $VAR — treat as any (no port filter) to avoid
|
||||||
|
// infinite recursion in the group-expansion loop below.
|
||||||
|
if token.starts_with('$') {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negation-only → treat as any (we can't enumerate the complement)
|
||||||
|
if token.starts_with('!') && !token.starts_with("![") {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port group: [80,443,!8080] — strip outer brackets
|
||||||
|
let inner = if token.starts_with('[') && token.ends_with(']') {
|
||||||
|
&token[1..token.len() - 1]
|
||||||
|
} else {
|
||||||
|
token
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ports = Vec::new();
|
||||||
|
let mut has_negation_only = true;
|
||||||
|
for part in inner.split(',') {
|
||||||
|
let part = part.trim();
|
||||||
|
if part.is_empty() { continue; }
|
||||||
|
if part.starts_with('!') {
|
||||||
|
// negated entry — skip but don't block the group
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
has_negation_only = false;
|
||||||
|
// Recurse only for variable names (start with '$') or nested groups
|
||||||
|
if part.starts_with('$') || part.starts_with('[') {
|
||||||
|
ports.extend(expand_port_token(part));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Port range: 1024:2048
|
||||||
|
if let Some(colon) = part.find(':') {
|
||||||
|
let lo: u16 = part[..colon].parse().unwrap_or(0);
|
||||||
|
let hi: u16 = part[colon + 1..].parse().unwrap_or(65535);
|
||||||
|
// Don't expand huge ranges — treat as any
|
||||||
|
if hi.saturating_sub(lo) > 1024 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
for p in lo..=hi { ports.push(p); }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Ok(p) = part.parse::<u16>() {
|
||||||
|
ports.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the group contained only negations, fall back to any
|
||||||
|
if has_negation_only && ports.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
ports
|
||||||
|
}
|
||||||
@ -169,7 +169,7 @@ impl<T: NativeConvert + Pod> MapWrapper<T> {
|
|||||||
Err(EbpfError::RuleReachLimit)?;
|
Err(EbpfError::RuleReachLimit)?;
|
||||||
}
|
}
|
||||||
new_ports.copy_from_slice(&ports);
|
new_ports.copy_from_slice(&ports);
|
||||||
new_ports[index.unwrap()] = port;
|
new_ports[index.expect("index is Some; None returns early above")] = port;
|
||||||
} else {
|
} else {
|
||||||
new_ports[0] = port;
|
new_ports[0] = port;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ use crate::core::ebpf::xsk_manager::XskManager;
|
|||||||
use crate::core::infrastructure::app_config::AppConfig;
|
use crate::core::infrastructure::app_config::AppConfig;
|
||||||
use crate::model::error::system::SystemError;
|
use crate::model::error::system::SystemError;
|
||||||
use crate::model::error::Error;
|
use crate::model::error::Error;
|
||||||
use crate::ml::engine::Engine;
|
use crate::detection::ml::engine::Engine;
|
||||||
|
|
||||||
pub struct EbpfServices {
|
pub struct EbpfServices {
|
||||||
pub xsk_manager: Arc<XskManager>,
|
pub xsk_manager: Arc<XskManager>,
|
||||||
|
|||||||
@ -141,7 +141,7 @@ impl Statistics {
|
|||||||
let boot_time = self.boot_time;
|
let boot_time = self.boot_time;
|
||||||
let now = std::time::SystemTime::now()
|
let now = std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.unwrap()
|
.expect("system time is after UNIX_EPOCH")
|
||||||
.as_nanos() as u64;
|
.as_nanos() as u64;
|
||||||
for ((_, _, time_type), map) in self.ipv4_maps.iter() {
|
for ((_, _, time_type), map) in self.ipv4_maps.iter() {
|
||||||
map.write().await.cleanup(boot_time, now, time_type.duration())
|
map.write().await.cleanup(boot_time, now, time_type.duration())
|
||||||
@ -156,14 +156,16 @@ impl Statistics {
|
|||||||
direction: Direction,
|
direction: Direction,
|
||||||
flow_direction: FlowDirection,
|
flow_direction: FlowDirection,
|
||||||
time_type: TimeType,
|
time_type: TimeType,
|
||||||
) -> HashMap<SocketAddrV4, FlowStatsWithGeo> {
|
) -> Result<HashMap<SocketAddrV4, FlowStatsWithGeo>, EbpfError> {
|
||||||
let flow_data = self
|
let map = self
|
||||||
.ipv4_maps
|
.ipv4_maps
|
||||||
.get(&(direction, flow_direction, time_type))
|
.get(&(direction, flow_direction, time_type))
|
||||||
.unwrap()
|
.ok_or_else(|| EbpfError::FlowMapKeyMissing(
|
||||||
.write()
|
format!("{:?}", direction),
|
||||||
.await
|
format!("{:?}", flow_direction),
|
||||||
.get_map();
|
format!("{:?}", time_type),
|
||||||
|
))?;
|
||||||
|
let flow_data = map.write().await.get_map();
|
||||||
|
|
||||||
if let Some(ref geo_ip) = self.geo_ip {
|
if let Some(ref geo_ip) = self.geo_ip {
|
||||||
let futures: Vec<_> = flow_data
|
let futures: Vec<_> = flow_data
|
||||||
@ -177,12 +179,12 @@ impl Statistics {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
join_all(futures).await.into_iter().collect()
|
Ok(join_all(futures).await.into_iter().collect())
|
||||||
} else {
|
} else {
|
||||||
flow_data
|
Ok(flow_data
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(addr, stats)| (addr, FlowStatsWithGeo { stats, geo: None }))
|
.map(|(addr, stats)| (addr, FlowStatsWithGeo { stats, geo: None }))
|
||||||
.collect()
|
.collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,14 +193,16 @@ impl Statistics {
|
|||||||
direction: Direction,
|
direction: Direction,
|
||||||
flow_direction: FlowDirection,
|
flow_direction: FlowDirection,
|
||||||
time_type: TimeType,
|
time_type: TimeType,
|
||||||
) -> HashMap<SocketAddrV6, FlowStatsWithGeo> {
|
) -> Result<HashMap<SocketAddrV6, FlowStatsWithGeo>, EbpfError> {
|
||||||
let flow_data = self
|
let map = self
|
||||||
.ipv6_maps
|
.ipv6_maps
|
||||||
.get(&(direction, flow_direction, time_type))
|
.get(&(direction, flow_direction, time_type))
|
||||||
.unwrap()
|
.ok_or_else(|| EbpfError::FlowMapKeyMissing(
|
||||||
.write()
|
format!("{:?}", direction),
|
||||||
.await
|
format!("{:?}", flow_direction),
|
||||||
.get_map();
|
format!("{:?}", time_type),
|
||||||
|
))?;
|
||||||
|
let flow_data = map.write().await.get_map();
|
||||||
|
|
||||||
if let Some(ref geo_ip) = self.geo_ip {
|
if let Some(ref geo_ip) = self.geo_ip {
|
||||||
let futures: Vec<_> = flow_data
|
let futures: Vec<_> = flow_data
|
||||||
@ -212,12 +216,12 @@ impl Statistics {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
join_all(futures).await.into_iter().collect()
|
Ok(join_all(futures).await.into_iter().collect())
|
||||||
} else {
|
} else {
|
||||||
flow_data
|
Ok(flow_data
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(addr, stats)| (addr, FlowStatsWithGeo { stats, geo: None }))
|
.map(|(addr, stats)| (addr, FlowStatsWithGeo { stats, geo: None }))
|
||||||
.collect()
|
.collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,13 +17,17 @@ use xsk_rs::config::{BindFlags, FrameSize, Interface, LibxdpFlags, QueueSize, So
|
|||||||
use xsk_rs::{CompQueue, FillQueue, FrameDesc, RxQueue, Socket, TxQueue, Umem};
|
use xsk_rs::{CompQueue, FillQueue, FrameDesc, RxQueue, Socket, TxQueue, Umem};
|
||||||
|
|
||||||
use crate::core::infrastructure::app_config::AppConfig;
|
use crate::core::infrastructure::app_config::AppConfig;
|
||||||
use crate::ml::engine::{Engine, PacketProcessor};
|
use crate::detection::ml::engine::Engine;
|
||||||
|
use crate::detection::rule::rule_engine::RuleEngine;
|
||||||
|
use crate::detection::rule::stream_reassembler::StreamReassembler;
|
||||||
use crate::model::config::Config;
|
use crate::model::config::Config;
|
||||||
use crate::model::direction::Direction;
|
use crate::model::direction::Direction;
|
||||||
use crate::model::error::ebpf::EbpfError;
|
use crate::model::error::ebpf::EbpfError;
|
||||||
use crate::model::error::system::SystemError;
|
use crate::model::error::system::SystemError;
|
||||||
use crate::model::error::Error;
|
use crate::model::error::Error;
|
||||||
use crate::model::log::ebpf::EbpfLog;
|
use crate::model::log::ebpf::EbpfLog;
|
||||||
|
use crate::model::log::ml::MLLog;
|
||||||
|
use crate::model::log::rule::RuleLog;
|
||||||
|
|
||||||
pub struct XskManager {
|
pub struct XskManager {
|
||||||
app_config: Arc<AppConfig>,
|
app_config: Arc<AppConfig>,
|
||||||
@ -48,11 +52,22 @@ impl XskManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(&self, ml_engine: Option<Arc<Engine>>, shutdowns: &SegQueue<oneshot::Sender<()>>) -> Result<(), Error> {
|
pub fn run(&self, engine: Option<Arc<Engine>>, shutdowns: &SegQueue<oneshot::Sender<()>>) -> Result<(), Error> {
|
||||||
let config = self.app_config.config.clone();
|
let config = self.app_config.config.clone();
|
||||||
let combined_queue_count = config.combined_queue_count;
|
let combined_queue_count = config.combined_queue_count;
|
||||||
|
|
||||||
let packet_processor = ml_engine.map(|engine| Arc::new(PacketProcessor::new(engine)));
|
let min_sig = config.min_signature_matches;
|
||||||
|
|
||||||
|
let rule_engine = match RuleEngine::new() {
|
||||||
|
Some(re) => {
|
||||||
|
log!(RuleLog::RulesLoaded(re.pattern_count()));
|
||||||
|
Some(Arc::new(re))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
log!(RuleLog::RulesDisabled);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for queue_id in 0..combined_queue_count {
|
for queue_id in 0..combined_queue_count {
|
||||||
let (ingress_to_egress_tx, ingress_to_egress_rx) = bounded(config.channel_size);
|
let (ingress_to_egress_tx, ingress_to_egress_rx) = bounded(config.channel_size);
|
||||||
@ -64,7 +79,9 @@ impl XskManager {
|
|||||||
&config.ingress_ifname,
|
&config.ingress_ifname,
|
||||||
&config.egress_ifname,
|
&config.egress_ifname,
|
||||||
Direction::Ingress,
|
Direction::Ingress,
|
||||||
packet_processor.clone(),
|
engine.clone(),
|
||||||
|
rule_engine.clone(),
|
||||||
|
min_sig,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let egress_xsk = XskPair::new(
|
let egress_xsk = XskPair::new(
|
||||||
@ -73,7 +90,9 @@ impl XskManager {
|
|||||||
&config.egress_ifname,
|
&config.egress_ifname,
|
||||||
&config.ingress_ifname,
|
&config.ingress_ifname,
|
||||||
Direction::Egress,
|
Direction::Egress,
|
||||||
packet_processor.clone(),
|
engine.clone(),
|
||||||
|
rule_engine.clone(),
|
||||||
|
min_sig,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mut xsk_map = self.xsk_map.lock();
|
let mut xsk_map = self.xsk_map.lock();
|
||||||
@ -110,8 +129,10 @@ pub struct XskPair {
|
|||||||
comp_queue: CompQueue,
|
comp_queue: CompQueue,
|
||||||
tx: TxQueue,
|
tx: TxQueue,
|
||||||
rx: RxQueue,
|
rx: RxQueue,
|
||||||
frame_pool: Arc<Mutex<Vec<FrameDesc>>>, // SegQueue
|
frame_pool: Arc<Mutex<Vec<FrameDesc>>>,
|
||||||
packet_processor: Option<Arc<PacketProcessor>>,
|
engine: Option<Arc<Engine>>,
|
||||||
|
rule_engine: Option<Arc<RuleEngine>>,
|
||||||
|
min_signature_matches: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl XskPair {
|
impl XskPair {
|
||||||
@ -121,7 +142,9 @@ impl XskPair {
|
|||||||
rx_ifname: &str,
|
rx_ifname: &str,
|
||||||
tx_ifname: &str,
|
tx_ifname: &str,
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
packet_processor: Option<Arc<PacketProcessor>>,
|
engine: Option<Arc<Engine>>,
|
||||||
|
rule_engine: Option<Arc<RuleEngine>>,
|
||||||
|
min_signature_matches: u32,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let rx_ifname_c = CString::new(rx_ifname).map_err(|_| SystemError::UnknownError)?;
|
let rx_ifname_c = CString::new(rx_ifname).map_err(|_| SystemError::UnknownError)?;
|
||||||
|
|
||||||
@ -176,7 +199,9 @@ impl XskPair {
|
|||||||
tx,
|
tx,
|
||||||
rx,
|
rx,
|
||||||
frame_pool: Arc::new(Mutex::new(pool_frames)),
|
frame_pool: Arc::new(Mutex::new(pool_frames)),
|
||||||
packet_processor,
|
engine,
|
||||||
|
rule_engine,
|
||||||
|
min_signature_matches,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(xsk_pair)
|
Ok(xsk_pair)
|
||||||
@ -194,8 +219,17 @@ impl XskPair {
|
|||||||
thread::Builder::new()
|
thread::Builder::new()
|
||||||
.name(thread_name.clone())
|
.name(thread_name.clone())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
|
// StreamReassembler is !Send (Rc inside protolens), so create it here.
|
||||||
|
let min_sig_matches = self.min_signature_matches;
|
||||||
|
let is_ingress = self.direction == Direction::Ingress;
|
||||||
|
let mut reassembler: Option<StreamReassembler> = self
|
||||||
|
.rule_engine
|
||||||
|
.take()
|
||||||
|
.map(|re| StreamReassembler::new(re, min_sig_matches, is_ingress));
|
||||||
|
|
||||||
let mut shutdown_rx = Some(shutdown_rx);
|
let mut shutdown_rx = Some(shutdown_rx);
|
||||||
let mut idle_count: u32 = 0;
|
let mut idle_count: u32 = 0;
|
||||||
|
let mut last_cleanup = std::time::Instant::now();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Some(ref mut rx) = shutdown_rx {
|
if let Some(ref mut rx) = shutdown_rx {
|
||||||
@ -214,7 +248,7 @@ impl XskPair {
|
|||||||
Err(e) => log!(EbpfLog::CompQueueError(format!("{:?}", e))),
|
Err(e) => log!(EbpfLog::CompQueueError(format!("{:?}", e))),
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.process_rx_queue(&forward_tx) {
|
match self.process_rx_queue(&forward_tx, &mut reassembler) {
|
||||||
Ok(count) => total_activity += count,
|
Ok(count) => total_activity += count,
|
||||||
Err(e) => log!(EbpfLog::RXQueueError(format!("{:?}", e))),
|
Err(e) => log!(EbpfLog::RXQueueError(format!("{:?}", e))),
|
||||||
}
|
}
|
||||||
@ -230,6 +264,20 @@ impl XskPair {
|
|||||||
idle_count = 0;
|
idle_count = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Purge idle TCP flows every 60 s of wall time.
|
||||||
|
if last_cleanup.elapsed() >= Duration::from_secs(60) {
|
||||||
|
if let Some(r) = &mut reassembler {
|
||||||
|
r.cleanup_stale(Duration::from_secs(60));
|
||||||
|
log!(RuleLog::SignatureStats(
|
||||||
|
format!("{:?}", self.direction),
|
||||||
|
r.flow_count(),
|
||||||
|
r.min_matches(),
|
||||||
|
format!("{:?}", r.match_counts()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
last_cleanup = std::time::Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
let sleep_us = match idle_count {
|
let sleep_us = match idle_count {
|
||||||
0..=10 => 1,
|
0..=10 => 1,
|
||||||
11..=100 => 10,
|
11..=100 => 10,
|
||||||
@ -264,7 +312,11 @@ impl XskPair {
|
|||||||
Ok(nb_completed)
|
Ok(nb_completed)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_rx_queue(&mut self, forward_tx: &Sender<Vec<u8>>) -> Result<usize, EbpfError> {
|
fn process_rx_queue(
|
||||||
|
&mut self,
|
||||||
|
forward_tx: &Sender<Vec<u8>>,
|
||||||
|
reassembler: &mut Option<StreamReassembler>,
|
||||||
|
) -> Result<usize, EbpfError> {
|
||||||
let mut rx_descs = vec![FrameDesc::default(); 64];
|
let mut rx_descs = vec![FrameDesc::default(); 64];
|
||||||
let rx_count = unsafe { self.rx.consume(&mut rx_descs) };
|
let rx_count = unsafe { self.rx.consume(&mut rx_descs) };
|
||||||
|
|
||||||
@ -276,8 +328,25 @@ impl XskPair {
|
|||||||
let data = unsafe { self.umem.data(rx_desc) };
|
let data = unsafe { self.umem.data(rx_desc) };
|
||||||
let packet_data = data.contents()[..packet_len].to_vec();
|
let packet_data = data.contents()[..packet_len].to_vec();
|
||||||
|
|
||||||
if let Some(ref processor) = self.packet_processor {
|
if let Some(r) = &mut *reassembler {
|
||||||
processor.process(&packet_data, self.direction == Direction::Ingress);
|
match r.process(&packet_data) {
|
||||||
|
Ok(matches) => {
|
||||||
|
for m in matches {
|
||||||
|
log!(RuleLog::SignatureMatch(
|
||||||
|
format!("{:?}", self.direction),
|
||||||
|
m.src,
|
||||||
|
m.dst,
|
||||||
|
m.sid,
|
||||||
|
m.msg,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => log!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref engine) = self.engine {
|
||||||
|
engine.process_packet(&packet_data, self.direction == Direction::Ingress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = forward_tx.try_send(packet_data) {
|
if let Err(e) = forward_tx.try_send(packet_data) {
|
||||||
@ -365,4 +434,4 @@ impl XskPair {
|
|||||||
|
|
||||||
Ok(nb_submitted)
|
Ok(nb_submitted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -28,7 +28,7 @@ impl GeoIpService {
|
|||||||
) -> Result<Self, MaxMindDbError> {
|
) -> Result<Self, MaxMindDbError> {
|
||||||
let reader = Reader::open_readfile(db_path)?;
|
let reader = Reader::open_readfile(db_path)?;
|
||||||
let cache_capacity = NonZeroUsize::new(cache_size)
|
let cache_capacity = NonZeroUsize::new(cache_size)
|
||||||
.unwrap_or_else(|| NonZeroUsize::new(10000).unwrap());
|
.unwrap_or_else(|| NonZeroUsize::new(10000).expect("10000 is non-zero"));
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
reader: Arc::new(reader),
|
reader: Arc::new(reader),
|
||||||
|
|||||||
@ -112,7 +112,7 @@ impl SystemHealth {
|
|||||||
) -> SystemHealthMetrics {
|
) -> SystemHealthMetrics {
|
||||||
let timestamp = std::time::SystemTime::now()
|
let timestamp = std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.unwrap()
|
.expect("system time is after UNIX_EPOCH")
|
||||||
.as_secs();
|
.as_secs();
|
||||||
|
|
||||||
let boot_time = System::boot_time();
|
let boot_time = System::boot_time();
|
||||||
|
|||||||
@ -1,48 +1,9 @@
|
|||||||
use serde::Serialize;
|
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::model::ml_detection::DetectionResult;
|
use crate::model::ml_detection::{AlertMessage, DetectionResult};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
pub struct MLAlert {
|
||||||
pub struct AlertMessage {
|
|
||||||
pub timestamp: u64,
|
|
||||||
pub flow_key: String,
|
|
||||||
pub src_ip: String,
|
|
||||||
pub dst_ip: String,
|
|
||||||
pub src_port: u16,
|
|
||||||
pub dst_port: u16,
|
|
||||||
pub protocol: u8,
|
|
||||||
pub is_attack: bool,
|
|
||||||
pub attack_type: Option<String>,
|
|
||||||
pub confidence: f32,
|
|
||||||
pub ae_score: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AlertMessage {
|
|
||||||
pub fn from_detection_result(result: &DetectionResult) -> Self {
|
|
||||||
let timestamp = std::time::SystemTime::now()
|
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_secs();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
timestamp,
|
|
||||||
flow_key: result.flow_key.clone(),
|
|
||||||
src_ip: result.flow_key_raw.src_ip.clone(),
|
|
||||||
dst_ip: result.flow_key_raw.dst_ip.clone(),
|
|
||||||
src_port: result.flow_key_raw.src_port,
|
|
||||||
dst_port: result.flow_key_raw.dst_port,
|
|
||||||
protocol: result.flow_key_raw.protocol,
|
|
||||||
is_attack: result.is_attack,
|
|
||||||
attack_type: result.attack_type.clone(),
|
|
||||||
confidence: result.confidence,
|
|
||||||
ae_score: result.ae_score,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct MLAlert {
|
|
||||||
broadcast_tx: broadcast::Sender<AlertMessage>,
|
broadcast_tx: broadcast::Sender<AlertMessage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,9 +11,7 @@ impl MLAlert {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let (broadcast_tx, _) = broadcast::channel(100);
|
let (broadcast_tx, _) = broadcast::channel(100);
|
||||||
|
|
||||||
MLAlert {
|
MLAlert { broadcast_tx }
|
||||||
broadcast_tx,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subscribe_to_alerts(&self) -> broadcast::Receiver<AlertMessage> {
|
pub fn subscribe_to_alerts(&self) -> broadcast::Receiver<AlertMessage> {
|
||||||
@ -77,4 +36,4 @@ impl Default for MLAlert {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,15 +14,15 @@ use tokio::sync::oneshot;
|
|||||||
use crate::core::infrastructure::app_config::AppConfig;
|
use crate::core::infrastructure::app_config::AppConfig;
|
||||||
use crate::core::infrastructure::health::SystemHealth;
|
use crate::core::infrastructure::health::SystemHealth;
|
||||||
use crate::core::infrastructure::ml_alert::MLAlert;
|
use crate::core::infrastructure::ml_alert::MLAlert;
|
||||||
use crate::ml::config_loader::InferenceConfig;
|
use crate::detection::ml::config_loader::InferenceConfig;
|
||||||
use crate::ml::engine::Engine;
|
use crate::detection::ml::engine::Engine;
|
||||||
use crate::ml::feature_extractor::FlowFeatures;
|
use crate::detection::ml::feature_extractor::FlowFeatures;
|
||||||
use crate::ml::model_loader::MLModels;
|
use crate::detection::ml::model_loader::MLModels;
|
||||||
use crate::model::error::misc::MiscError;
|
use crate::model::error::misc::MiscError;
|
||||||
use crate::model::error::system::SystemError;
|
|
||||||
use crate::model::error::Error;
|
use crate::model::error::Error;
|
||||||
|
use crate::model::error::system::SystemError;
|
||||||
use crate::model::log::system::SystemLog;
|
use crate::model::log::system::SystemLog;
|
||||||
use crate::ml::traffic_logger::TrafficLogger;
|
use crate::detection::ml::traffic_logger::TrafficLogger;
|
||||||
|
|
||||||
pub struct AppServices {
|
pub struct AppServices {
|
||||||
pub health: Arc<SystemHealth>,
|
pub health: Arc<SystemHealth>,
|
||||||
@ -39,9 +39,11 @@ impl AppServices {
|
|||||||
let ml_models = Arc::new(MLModels::load_models(&app_config, &inference_config)?);
|
let ml_models = Arc::new(MLModels::load_models(&app_config, &inference_config)?);
|
||||||
let ml_alert = Arc::new(MLAlert::new());
|
let ml_alert = Arc::new(MLAlert::new());
|
||||||
|
|
||||||
|
// TODO: Need Edit
|
||||||
let traffic_logger = if app_config.traffic_logging_mode {
|
let traffic_logger = if app_config.traffic_logging_mode {
|
||||||
let csv_path = app_config.traffic_log_csv_path.clone();
|
let csv_path = app_config.traffic_log_csv_path.clone();
|
||||||
let mut header = FlowFeatures::all_feature_names_owned();
|
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());
|
header.push("Label".to_string());
|
||||||
let logger = TrafficLogger::new(&csv_path, header)
|
let logger = TrafficLogger::new(&csv_path, header)
|
||||||
.map_err(|e| MiscError::TrafficLogCreateError(csv_path.clone(), e.to_string()))?;
|
.map_err(|e| MiscError::TrafficLogCreateError(csv_path.clone(), e.to_string()))?;
|
||||||
@ -60,9 +62,11 @@ impl AppServices {
|
|||||||
app_config.inference_batch_size,
|
app_config.inference_batch_size,
|
||||||
app_config.inference_interval_secs,
|
app_config.inference_interval_secs,
|
||||||
app_config.aggregator_window_secs,
|
app_config.aggregator_window_secs,
|
||||||
|
app_config.flow_timeout_us,
|
||||||
traffic_logger,
|
traffic_logger,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
health: Arc::new(health),
|
health: Arc::new(health),
|
||||||
ml_alert,
|
ml_alert,
|
||||||
|
|||||||
@ -12,7 +12,7 @@ use macros::log;
|
|||||||
use crate::core::ebpf::EbpfServices;
|
use crate::core::ebpf::EbpfServices;
|
||||||
use crate::core::infrastructure::app_config::AppConfig;
|
use crate::core::infrastructure::app_config::AppConfig;
|
||||||
use crate::core::infrastructure::AppServices;
|
use crate::core::infrastructure::AppServices;
|
||||||
use crate::ml::config_loader::InferenceConfig;
|
use crate::detection::ml::config_loader::InferenceConfig;
|
||||||
use crate::model::error::ebpf::EbpfError;
|
use crate::model::error::ebpf::EbpfError;
|
||||||
use crate::model::error::http::HttpError;
|
use crate::model::error::http::HttpError;
|
||||||
use crate::model::error::misc::MiscError;
|
use crate::model::error::misc::MiscError;
|
||||||
@ -73,13 +73,9 @@ impl System {
|
|||||||
log!(MLLog::ModelsLoaded(
|
log!(MLLog::ModelsLoaded(
|
||||||
self.app_services.ml_models.get_model_info("deep_autoencoder")
|
self.app_services.ml_models.get_model_info("deep_autoencoder")
|
||||||
));
|
));
|
||||||
log!(MLLog::ModelsLoaded(
|
|
||||||
self.app_services.ml_models.get_model_info("classifier")
|
|
||||||
));
|
|
||||||
|
|
||||||
log!(MLLog::ConfigLoaded {
|
log!(MLLog::ConfigLoaded {
|
||||||
features: self.inference_config.num_ae_features(),
|
features: self.inference_config.num_ae_features(),
|
||||||
attacks: self.inference_config.num_attack_types()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
self.aya_log_init()?;
|
self.aya_log_init()?;
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use std::path::PathBuf;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::model::error::ml::MLError;
|
use crate::model::error::ml::MLError;
|
||||||
use crate::model::ml_detection::{AENormalization, ClipParams, PrecisionLevels};
|
use crate::model::ml_detection::ClipParams;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct InferenceConfig {
|
pub struct InferenceConfig {
|
||||||
@ -15,15 +15,15 @@ pub struct InferenceConfig {
|
|||||||
pub ae_post_clip_min: f64,
|
pub ae_post_clip_min: f64,
|
||||||
pub ae_post_clip_max: f64,
|
pub ae_post_clip_max: f64,
|
||||||
pub ae_threshold: f32,
|
pub ae_threshold: f32,
|
||||||
pub classifier_feature_names: Vec<String>,
|
pub ae_threshold_method: Option<String>,
|
||||||
pub attack_labels: HashMap<String, String>,
|
pub window_size: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InferenceConfig {
|
impl InferenceConfig {
|
||||||
pub fn load_file(file: &str) -> Result<Self, MLError> {
|
pub fn load_file(file: &str) -> Result<Self, MLError> {
|
||||||
let path = PathBuf::from("models").join(file);
|
let path = PathBuf::from(env!("ARTIFACTCS_PATH")).join(file);
|
||||||
let content = fs::read_to_string(&path)
|
let content = fs::read_to_string(&path)
|
||||||
.map_err(|_| MLError::ConfigLoadFailed { path: path.to_path_buf() })?;
|
.map_err(|_| MLError::ConfigLoadFailed { path: path.clone() })?;
|
||||||
let config: InferenceConfig = serde_json::from_str(&content)
|
let config: InferenceConfig = serde_json::from_str(&content)
|
||||||
.map_err(|e| MLError::ConfigParseFailed { reason: e.to_string() })?;
|
.map_err(|e| MLError::ConfigParseFailed { reason: e.to_string() })?;
|
||||||
Ok(config)
|
Ok(config)
|
||||||
@ -32,16 +32,4 @@ impl InferenceConfig {
|
|||||||
pub fn num_ae_features(&self) -> usize {
|
pub fn num_ae_features(&self) -> usize {
|
||||||
self.ae_feature_names.len()
|
self.ae_feature_names.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn num_classifier_features(&self) -> usize {
|
|
||||||
self.classifier_feature_names.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn num_attack_types(&self) -> usize {
|
|
||||||
self.attack_labels.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_attack_label(&self, id: usize) -> Option<&String> {
|
|
||||||
self.attack_labels.get(&id.to_string())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -14,18 +14,20 @@ use super::model_loader::MLModels;
|
|||||||
use super::traffic_logger::TrafficLogger;
|
use super::traffic_logger::TrafficLogger;
|
||||||
|
|
||||||
use crate::core::infrastructure::ml_alert::MLAlert;
|
use crate::core::infrastructure::ml_alert::MLAlert;
|
||||||
|
use crate::model::error::ml::MLError;
|
||||||
use crate::model::log::ml::MLLog;
|
use crate::model::log::ml::MLLog;
|
||||||
use crate::model::ml_detection::{EngineStats, InferenceStats};
|
use crate::model::ml_detection::{EngineStats, InferenceStats};
|
||||||
use crate::utils::packet_parser::parse_packet;
|
use crate::utils::packet_parser::parse_packet;
|
||||||
|
|
||||||
pub struct Engine {
|
pub struct Engine {
|
||||||
flow_tracker: Arc<FlowTracker>,
|
tracker: Arc<Mutex<FlowTracker>>,
|
||||||
inference_pipeline: Arc<Inference>,
|
inference_pipeline: Arc<Inference>,
|
||||||
aggregator: Arc<Mutex<AttackAggregator>>,
|
aggregator: Arc<Mutex<AttackAggregator>>,
|
||||||
ml_alert: Arc<MLAlert>,
|
ml_alert: Arc<MLAlert>,
|
||||||
min_packets: usize,
|
min_packets: usize,
|
||||||
batch_size: usize,
|
batch_size: usize,
|
||||||
inference_interval_secs: u64,
|
inference_interval_secs: u64,
|
||||||
|
flow_timeout_us: u64,
|
||||||
traffic_logger: Option<Arc<TrafficLogger>>,
|
traffic_logger: Option<Arc<TrafficLogger>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,22 +41,26 @@ impl Engine {
|
|||||||
batch_size: usize,
|
batch_size: usize,
|
||||||
interval_secs: u64,
|
interval_secs: u64,
|
||||||
window_secs: u64,
|
window_secs: u64,
|
||||||
|
flow_timeout_us: u64,
|
||||||
traffic_logger: Option<Arc<TrafficLogger>>,
|
traffic_logger: Option<Arc<TrafficLogger>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let flow_tracker = Arc::new(FlowTracker::new(max_flows));
|
let tracker = Arc::new(Mutex::new(FlowTracker::new(max_flows)));
|
||||||
let inference_pipeline = Arc::new(Inference::new(models, config));
|
let inference_pipeline = Arc::new(Inference::new(models, config));
|
||||||
|
|
||||||
let min_detections = ((window_secs / interval_secs) / 2).max(1) as usize;
|
let min_detections = ((window_secs / interval_secs) / 2).max(1) as usize;
|
||||||
let aggregator = Arc::new(Mutex::new(AttackAggregator::new(window_secs, min_detections)));
|
let aggregator = Arc::new(Mutex::new(AttackAggregator::new(window_secs, min_detections)));
|
||||||
|
|
||||||
|
let effective_min_packets = if traffic_logger.is_some() { 1 } else { min_packets };
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
flow_tracker,
|
tracker,
|
||||||
inference_pipeline,
|
inference_pipeline,
|
||||||
aggregator,
|
aggregator,
|
||||||
ml_alert,
|
ml_alert,
|
||||||
min_packets,
|
min_packets: effective_min_packets,
|
||||||
batch_size,
|
batch_size,
|
||||||
inference_interval_secs: interval_secs,
|
inference_interval_secs: interval_secs,
|
||||||
|
flow_timeout_us,
|
||||||
traffic_logger,
|
traffic_logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,8 +73,8 @@ impl Engine {
|
|||||||
shutdown_tx
|
shutdown_tx
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_flow_tracker(&self) -> Arc<FlowTracker> {
|
pub fn tracker(&self) -> &Arc<Mutex<FlowTracker>> {
|
||||||
self.flow_tracker.clone()
|
&self.tracker
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_inference_loop(&self, mut shutdown_rx: oneshot::Receiver<()>) {
|
async fn run_inference_loop(&self, mut shutdown_rx: oneshot::Receiver<()>) {
|
||||||
@ -76,15 +82,25 @@ impl Engine {
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = &mut shutdown_rx => break,
|
_ = &mut shutdown_rx => break,
|
||||||
_ = ticker.tick() => {}
|
_ = ticker.tick() => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let total_flows = self.flow_tracker.flow_count();
|
let Ok(mut t) = self.tracker.lock() else {
|
||||||
let all_flows = self.flow_tracker.get_flows_snapshot();
|
log!(MLError::TrackerLockPoisoned);
|
||||||
let packet_counts: Vec<usize> = all_flows.iter().map(|f| f.packet_count()).collect();
|
continue;
|
||||||
|
};
|
||||||
let flows = self.flow_tracker.get_flows_for_inference(self.min_packets);
|
let total_flows = t.flow_count();
|
||||||
|
let packet_counts: Vec<usize> = t.get_flows_snapshot()
|
||||||
|
.iter()
|
||||||
|
.map(|f| f.packet_count())
|
||||||
|
.collect();
|
||||||
|
let flows = if self.traffic_logger.is_some() {
|
||||||
|
t.drain_flows(self.min_packets)
|
||||||
|
} else {
|
||||||
|
t.get_flows_for_inference(self.min_packets)
|
||||||
|
};
|
||||||
|
drop(t);
|
||||||
|
|
||||||
log!(MLLog::FlowStats(
|
log!(MLLog::FlowStats(
|
||||||
total_flows,
|
total_flows,
|
||||||
@ -94,10 +110,19 @@ impl Engine {
|
|||||||
));
|
));
|
||||||
|
|
||||||
if flows.is_empty() {
|
if flows.is_empty() {
|
||||||
log!(MLLog::InferenceSkipped(format!(
|
if self.traffic_logger.is_none() {
|
||||||
"No flows with sufficient packets (total flows: {}, min packets: {})",
|
if let Ok(mut t) = self.tracker.lock() {
|
||||||
total_flows, self.min_packets
|
t.cleanup_old_flows(self.flow_timeout_us);
|
||||||
)));
|
let active_ips: std::collections::HashSet<String> = t
|
||||||
|
.get_flows_snapshot().iter()
|
||||||
|
.map(|f| f.flow_key.src_ip.clone())
|
||||||
|
.collect();
|
||||||
|
drop(t);
|
||||||
|
self.inference_pipeline.cleanup_buffers(&active_ips);
|
||||||
|
} else {
|
||||||
|
log!(MLError::TrackerLockPoisoned);
|
||||||
|
}
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,18 +132,25 @@ impl Engine {
|
|||||||
let features = FlowFeatures::extract(flow, &feature_names);
|
let features = FlowFeatures::extract(flow, &feature_names);
|
||||||
logger.log_row(features.to_csv_record());
|
logger.log_row(features.to_csv_record());
|
||||||
}
|
}
|
||||||
self.flow_tracker.cleanup_old_flows(60_000_000);
|
if let Ok(mut t) = self.tracker.lock() {
|
||||||
|
t.cleanup_old_flows(self.flow_timeout_us);
|
||||||
|
} else {
|
||||||
|
log!(MLError::TrackerLockPoisoned);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let batch = &flows[..flows.len().min(self.batch_size)];
|
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)
|
||||||
|
.then_with(|| a.start_time_us.cmp(&b.start_time_us))
|
||||||
|
});
|
||||||
|
|
||||||
log!(MLLog::RunningInference(batch.len()));
|
log!(MLLog::RunningInference(batch.len()));
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let results = self.inference_pipeline.infer_batch(batch);
|
let results = self.inference_pipeline.infer_batch(&batch);
|
||||||
let elapsed_us = start.elapsed().as_micros() as u64;
|
let elapsed_us = start.elapsed().as_micros() as u64;
|
||||||
|
|
||||||
let stats = InferenceStats::from_results(&results, elapsed_us);
|
let stats = InferenceStats::from_results(&results, elapsed_us);
|
||||||
|
|
||||||
if results.len() != batch.len() {
|
if results.len() != batch.len() {
|
||||||
@ -136,9 +168,11 @@ impl Engine {
|
|||||||
if let Ok(mut aggregator) = self.aggregator.lock() {
|
if let Ok(mut aggregator) = self.aggregator.lock() {
|
||||||
for result in &results {
|
for result in &results {
|
||||||
if result.is_attack {
|
if result.is_attack {
|
||||||
let should_alert =
|
let should_alert = aggregator.should_alert(
|
||||||
aggregator.should_alert(&result.flow_key_raw, result.ae_score, result.threshold);
|
&result.flow_key_raw,
|
||||||
|
result.ae_score,
|
||||||
|
result.threshold,
|
||||||
|
);
|
||||||
if should_alert {
|
if should_alert {
|
||||||
log!(MLLog::ThreatDetected(
|
log!(MLLog::ThreatDetected(
|
||||||
format!("{:?}", result.direction),
|
format!("{:?}", result.direction),
|
||||||
@ -147,16 +181,24 @@ impl Engine {
|
|||||||
result.confidence,
|
result.confidence,
|
||||||
result.ae_score,
|
result.ae_score,
|
||||||
));
|
));
|
||||||
|
|
||||||
self.ml_alert.broadcast_alert(result);
|
self.ml_alert.broadcast_alert(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
aggregator.cleanup();
|
aggregator.cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.flow_tracker.cleanup_old_flows(60_000_000);
|
if let Ok(mut t) = self.tracker.lock() {
|
||||||
|
t.cleanup_old_flows(self.flow_timeout_us);
|
||||||
|
let active_ips: std::collections::HashSet<String> = t
|
||||||
|
.get_flows_snapshot().iter()
|
||||||
|
.map(|f| f.flow_key.src_ip.clone())
|
||||||
|
.collect();
|
||||||
|
drop(t);
|
||||||
|
self.inference_pipeline.cleanup_buffers(&active_ips);
|
||||||
|
} else {
|
||||||
|
log!(MLError::TrackerLockPoisoned);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,35 +206,24 @@ impl Engine {
|
|||||||
match parse_packet(packet_data) {
|
match parse_packet(packet_data) {
|
||||||
Some((packet_info, payload_start)) => {
|
Some((packet_info, payload_start)) => {
|
||||||
let payload = packet_data.get(payload_start..).unwrap_or(&[]);
|
let payload = packet_data.get(payload_start..).unwrap_or(&[]);
|
||||||
self.flow_tracker.process_packet(packet_info, is_ingress, payload);
|
if let Ok(mut t) = self.tracker.lock() {
|
||||||
|
t.process_packet(packet_info, is_ingress, payload);
|
||||||
|
} else {
|
||||||
|
log!(MLError::TrackerLockPoisoned);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None => log!(MLLog::ParsePacketFailed(packet_data.len())),
|
None => log!(MLLog::ParsePacketFailed(packet_data.len())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_stats(&self) -> EngineStats {
|
pub fn get_stats(&self) -> EngineStats {
|
||||||
EngineStats {
|
let active_flows = match self.tracker.lock() {
|
||||||
active_flows: self.flow_tracker.flow_count(),
|
Ok(t) => t.flow_count(),
|
||||||
}
|
Err(_) => {
|
||||||
}
|
log!(MLError::TrackerLockPoisoned);
|
||||||
}
|
0
|
||||||
|
}
|
||||||
pub struct PacketProcessor {
|
};
|
||||||
ml_engine: Arc<Engine>,
|
EngineStats { active_flows }
|
||||||
}
|
|
||||||
|
|
||||||
impl PacketProcessor {
|
|
||||||
pub fn new(ml_engine: Arc<Engine>) -> Self {
|
|
||||||
Self { ml_engine }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn process(&self, packet_data: &[u8], is_ingress: bool) {
|
|
||||||
self.ml_engine.process_packet(packet_data, is_ingress);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn process_batch(&self, packets: &[Vec<u8>], is_ingress: bool) {
|
|
||||||
for packet in packets {
|
|
||||||
self.process(packet, is_ingress);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use chrono::{DateTime, Utc, TimeZone};
|
||||||
|
|
||||||
use super::flow_tracker::FlowData;
|
use super::flow_tracker::FlowData;
|
||||||
use crate::model::ml_detection::{ClipParams, PacketData};
|
use crate::model::ml_detection::{ClipParams, PacketData};
|
||||||
@ -7,6 +8,10 @@ use crate::model::ml_detection::{ClipParams, PacketData};
|
|||||||
pub struct FlowFeatures {
|
pub struct FlowFeatures {
|
||||||
pub features: Vec<f64>,
|
pub features: Vec<f64>,
|
||||||
pub feature_num: usize,
|
pub feature_num: usize,
|
||||||
|
|
||||||
|
pub src_ip: String,
|
||||||
|
pub dst_ip: String,
|
||||||
|
pub timestamp: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FlowFeatures {
|
impl FlowFeatures {
|
||||||
@ -15,17 +20,20 @@ impl FlowFeatures {
|
|||||||
let mut features = Vec::with_capacity(feature_num);
|
let mut features = Vec::with_capacity(feature_num);
|
||||||
|
|
||||||
for name in feature_names {
|
for name in feature_names {
|
||||||
let value = Self::get_feature_by_name(flow, name.trim());
|
features.push(Self::get_feature_by_name(flow, name.trim()));
|
||||||
features.push(value);
|
}
|
||||||
|
Self {
|
||||||
|
features,
|
||||||
|
feature_num,
|
||||||
|
src_ip: flow.flow_key.src_ip.clone(),
|
||||||
|
dst_ip: flow.flow_key.dst_ip.clone(),
|
||||||
|
timestamp: flow.start_time_us,
|
||||||
}
|
}
|
||||||
|
|
||||||
Self { features, feature_num }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_feature_by_name(flow: &FlowData, feature_name: &str) -> f64 {
|
fn get_feature_by_name(flow: &FlowData, feature_name: &str) -> f64 {
|
||||||
let safe_div = |a: f64, b: f64| if b > 0.0 { a / b } else { 0.0 };
|
let safe_div = |a: f64, b: f64| if b > 0.0 { a / b } else { 0.0 };
|
||||||
|
|
||||||
// 1-5
|
|
||||||
let fwd_count = flow.fwd_packets.len() as f64;
|
let fwd_count = flow.fwd_packets.len() as f64;
|
||||||
let bwd_count = flow.bwd_packets.len() as f64;
|
let bwd_count = flow.bwd_packets.len() as f64;
|
||||||
let total_count = fwd_count + bwd_count;
|
let total_count = fwd_count + bwd_count;
|
||||||
@ -34,64 +42,48 @@ impl FlowFeatures {
|
|||||||
let duration_s = duration_us / 1_000_000.0;
|
let duration_s = duration_us / 1_000_000.0;
|
||||||
let duration_s = if duration_s > 0.0 { duration_s } else { 1e-6 };
|
let duration_s = if duration_s > 0.0 { duration_s } else { 1e-6 };
|
||||||
|
|
||||||
// 6-9
|
let fwd_lengths: Vec<f64> = flow.fwd_packets.iter().map(|p| p.payload_length as f64).collect();
|
||||||
let fwd_lengths: Vec<f64> = flow.fwd_packets.iter().map(|p| p.length as f64).collect();
|
|
||||||
let (fwd_max, fwd_min, fwd_mean, fwd_std) = compute_stats(&fwd_lengths);
|
let (fwd_max, fwd_min, fwd_mean, fwd_std) = compute_stats(&fwd_lengths);
|
||||||
|
|
||||||
// 10-13
|
let bwd_lengths: Vec<f64> = flow.bwd_packets.iter().map(|p| p.payload_length as f64).collect();
|
||||||
let bwd_lengths: Vec<f64> = flow.bwd_packets.iter().map(|p| p.length as f64).collect();
|
|
||||||
let (bwd_max, bwd_min, bwd_mean, bwd_std) = compute_stats(&bwd_lengths);
|
let (bwd_max, bwd_min, bwd_mean, bwd_std) = compute_stats(&bwd_lengths);
|
||||||
|
|
||||||
// 14-15
|
|
||||||
let total_bytes = (flow.fwd_total_bytes + flow.bwd_total_bytes) as f64;
|
let total_bytes = (flow.fwd_total_bytes + flow.bwd_total_bytes) as f64;
|
||||||
|
|
||||||
// 16-19
|
|
||||||
let flow_iats = compute_flow_iats(&flow.fwd_packets, &flow.bwd_packets);
|
let flow_iats = compute_flow_iats(&flow.fwd_packets, &flow.bwd_packets);
|
||||||
let (flow_iat_max, flow_iat_min, flow_iat_mean, flow_iat_std) = compute_stats(&flow_iats);
|
let (flow_iat_max, flow_iat_min, flow_iat_mean, flow_iat_std) = compute_stats(&flow_iats);
|
||||||
|
|
||||||
// 20-24
|
|
||||||
let fwd_iats = compute_iats(&flow.fwd_packets);
|
let fwd_iats = compute_iats(&flow.fwd_packets);
|
||||||
let fwd_iat_total: f64 = fwd_iats.iter().sum();
|
let fwd_iat_total: f64 = fwd_iats.iter().sum();
|
||||||
let (fwd_iat_max, fwd_iat_min, fwd_iat_mean, fwd_iat_std) = compute_stats(&fwd_iats);
|
let (fwd_iat_max, fwd_iat_min, fwd_iat_mean, fwd_iat_std) = compute_stats(&fwd_iats);
|
||||||
|
|
||||||
// 25-29
|
|
||||||
let bwd_iats = compute_iats(&flow.bwd_packets);
|
let bwd_iats = compute_iats(&flow.bwd_packets);
|
||||||
let bwd_iat_total: f64 = bwd_iats.iter().sum();
|
let bwd_iat_total: f64 = bwd_iats.iter().sum();
|
||||||
let (bwd_iat_max, bwd_iat_min, bwd_iat_mean, bwd_iat_std) = compute_stats(&bwd_iats);
|
let (bwd_iat_max, bwd_iat_min, bwd_iat_mean, bwd_iat_std) = compute_stats(&bwd_iats);
|
||||||
|
|
||||||
// 30-37
|
|
||||||
let fwd_psh = flow.fwd_packets.iter().filter(|p| p.flags.psh).count() as f64;
|
let fwd_psh = flow.fwd_packets.iter().filter(|p| p.flags.psh).count() as f64;
|
||||||
let bwd_psh = flow.bwd_packets.iter().filter(|p| p.flags.psh).count() as f64;
|
let bwd_psh = flow.bwd_packets.iter().filter(|p| p.flags.psh).count() as f64;
|
||||||
let fwd_urg = flow.fwd_packets.iter().filter(|p| p.flags.urg).count() as f64;
|
let fwd_urg = flow.fwd_packets.iter().filter(|p| p.flags.urg).count() as f64;
|
||||||
let bwd_urg = flow.bwd_packets.iter().filter(|p| p.flags.urg).count() as f64;
|
let bwd_urg = flow.bwd_packets.iter().filter(|p| p.flags.urg).count() as f64;
|
||||||
|
|
||||||
// 38-55
|
|
||||||
let all_lengths: Vec<f64> = flow
|
let all_lengths: Vec<f64> = flow
|
||||||
.fwd_packets
|
.fwd_packets.iter().chain(flow.bwd_packets.iter())
|
||||||
.iter()
|
.map(|p| p.payload_length as f64)
|
||||||
.chain(flow.bwd_packets.iter())
|
|
||||||
.map(|p| p.length as f64)
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let (max_len, min_len, mean_len, std_len) = compute_stats(&all_lengths);
|
let (max_len, min_len, mean_len, std_len) = compute_stats(&all_lengths);
|
||||||
|
|
||||||
// 56-67
|
|
||||||
let fwd_bulk = &flow.fwd_bulk_state;
|
let fwd_bulk = &flow.fwd_bulk_state;
|
||||||
let bwd_bulk = &flow.bwd_bulk_state;
|
let bwd_bulk = &flow.bwd_bulk_state;
|
||||||
|
|
||||||
// 68-69
|
let fwd_header_sizes: Vec<f64> = flow
|
||||||
let fwd_seg_sizes: Vec<f64> = flow
|
|
||||||
.fwd_packets
|
.fwd_packets
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|p| p.payload_length > 0)
|
|
||||||
.map(|p| p.header_length as f64)
|
.map(|p| p.header_length as f64)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// 70-73
|
|
||||||
let (active_max, active_min, active_mean, active_std) =
|
let (active_max, active_min, active_mean, active_std) =
|
||||||
compute_stats(&flow.active_periods.iter().map(|&x| x as f64).collect::<Vec<_>>());
|
compute_stats(&flow.active_periods.iter().map(|&x| x as f64).collect::<Vec<_>>());
|
||||||
|
|
||||||
// 74-77
|
|
||||||
let (idle_max, idle_min, idle_mean, idle_std) =
|
let (idle_max, idle_min, idle_mean, idle_std) =
|
||||||
compute_stats(&flow.idle_periods.iter().map(|&x| x as f64).collect::<Vec<_>>());
|
compute_stats(&flow.idle_periods.iter().map(|&x| x as f64).collect::<Vec<_>>());
|
||||||
|
|
||||||
@ -171,8 +163,8 @@ impl FlowFeatures {
|
|||||||
"Subflow Bwd Bytes" => flow.bwd_total_bytes as f64,
|
"Subflow Bwd Bytes" => flow.bwd_total_bytes as f64,
|
||||||
"Init_Win_bytes_forward" | "Init Fwd Win Byts" | "fwd_win_bytes" => flow.init_win_bytes_fwd as f64,
|
"Init_Win_bytes_forward" | "Init Fwd Win Byts" | "fwd_win_bytes" => flow.init_win_bytes_fwd as f64,
|
||||||
"Init_Win_bytes_backward" | "Init Bwd Win Byts" | "bwd_win_bytes" => flow.init_win_bytes_bwd as f64,
|
"Init_Win_bytes_backward" | "Init Bwd Win Byts" | "bwd_win_bytes" => flow.init_win_bytes_bwd as f64,
|
||||||
"act_data_pkt_fwd" | "Fwd Act Data Pkts" | "fwd_act_data_pkts" => fwd_seg_sizes.len() as f64,
|
"act_data_pkt_fwd" | "Fwd Act Data Pkts" | "fwd_act_data_pkts" => flow.act_data_pkt_fwd as f64,
|
||||||
"min_seg_size_forward" | "Fwd Seg Size Min" | "fwd_seg_size_min" => fwd_seg_sizes
|
"min_seg_size_forward" | "Fwd Seg Size Min" | "fwd_seg_size_min" => fwd_header_sizes
|
||||||
.iter()
|
.iter()
|
||||||
.min_by(|a, b| a.total_cmp(b))
|
.min_by(|a, b| a.total_cmp(b))
|
||||||
.copied()
|
.copied()
|
||||||
@ -185,7 +177,6 @@ impl FlowFeatures {
|
|||||||
"Idle Std" => idle_std,
|
"Idle Std" => idle_std,
|
||||||
"Idle Max" => idle_max,
|
"Idle Max" => idle_max,
|
||||||
"Idle Min" => idle_min,
|
"Idle Min" => idle_min,
|
||||||
|
|
||||||
_ => 0.0,
|
_ => 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -309,7 +300,14 @@ impl FlowFeatures {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_csv_record(&self) -> Vec<String> {
|
pub fn to_csv_record(&self) -> Vec<String> {
|
||||||
let mut record: Vec<String> = self.features.iter().map(|f| f.to_string()).collect();
|
let ts_ms = self.timestamp / 1000;
|
||||||
|
let ts_str = match Utc.timestamp_millis_opt(ts_ms as i64) {
|
||||||
|
chrono::LocalResult::Single(dt) => dt.format("%m/%d/%Y %H:%M:%S").to_string(),
|
||||||
|
_ => ts_ms.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut record = vec![self.src_ip.clone(), self.dst_ip.clone(), ts_str];
|
||||||
|
record.extend(self.features.iter().map(|f| f.to_string()));
|
||||||
record.push("BENIGN".to_string());
|
record.push("BENIGN".to_string());
|
||||||
record
|
record
|
||||||
}
|
}
|
||||||
@ -327,7 +325,11 @@ fn compute_stats(values: &[f64]) -> (f64, f64, f64, f64) {
|
|||||||
let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
||||||
let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
|
let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
|
||||||
|
|
||||||
let variance: f64 = values.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n;
|
let variance: f64 = if n > 1.0 {
|
||||||
|
values.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / (n - 1.0)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
let std = variance.sqrt();
|
let std = variance.sqrt();
|
||||||
|
|
||||||
(max, min, mean, std)
|
(max, min, mean, std)
|
||||||
@ -1,5 +1,4 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use std::time;
|
use std::time;
|
||||||
|
|
||||||
use common::model::event::Event;
|
use common::model::event::Event;
|
||||||
@ -34,6 +33,10 @@ pub struct FlowData {
|
|||||||
pub last_packet_time: u64,
|
pub last_packet_time: u64,
|
||||||
pub fwd_bulk_state: BulkState,
|
pub fwd_bulk_state: BulkState,
|
||||||
pub bwd_bulk_state: BulkState,
|
pub bwd_bulk_state: BulkState,
|
||||||
|
pub act_data_pkt_fwd: u32,
|
||||||
|
active_start_us: u64,
|
||||||
|
fwd_fin_seen: bool,
|
||||||
|
bwd_fin_seen: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FlowData {
|
impl FlowData {
|
||||||
@ -72,10 +75,17 @@ impl FlowData {
|
|||||||
last_packet_time: first_packet.timestamp_us(),
|
last_packet_time: first_packet.timestamp_us(),
|
||||||
fwd_bulk_state: BulkState::default(),
|
fwd_bulk_state: BulkState::default(),
|
||||||
bwd_bulk_state: BulkState::default(),
|
bwd_bulk_state: BulkState::default(),
|
||||||
|
act_data_pkt_fwd: 0,
|
||||||
|
active_start_us: first_packet.timestamp_us(),
|
||||||
|
fwd_fin_seen: false,
|
||||||
|
bwd_fin_seen: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_packet(&mut self, packet: &Event) {
|
pub fn add_packet(&mut self, packet: &Event) {
|
||||||
|
const MAX_PACKETS_PER_DIRECTION: usize = 1000;
|
||||||
|
const MAX_PERIODS: usize = 10000;
|
||||||
|
|
||||||
let packet_data = PacketData {
|
let packet_data = PacketData {
|
||||||
timestamp_us: packet.timestamp_us(),
|
timestamp_us: packet.timestamp_us(),
|
||||||
length: packet.packet_length(),
|
length: packet.packet_length(),
|
||||||
@ -86,69 +96,86 @@ impl FlowData {
|
|||||||
|
|
||||||
if packet.tcp_flags().fin {
|
if packet.tcp_flags().fin {
|
||||||
self.fin_count += 1;
|
self.fin_count += 1;
|
||||||
|
// Track per-direction FIN for proper 4-way teardown detection
|
||||||
|
if packet.is_forward() {
|
||||||
|
self.fwd_fin_seen = true;
|
||||||
|
} else {
|
||||||
|
self.bwd_fin_seen = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if packet.tcp_flags().syn {
|
if packet.tcp_flags().syn { self.syn_count += 1; }
|
||||||
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().rst {
|
if packet.tcp_flags().ack { self.ack_count += 1; }
|
||||||
self.rst_count += 1;
|
if packet.tcp_flags().urg { self.urg_count += 1; }
|
||||||
}
|
if packet.tcp_flags().cwr { self.cwe_count += 1; }
|
||||||
if packet.tcp_flags().psh {
|
if packet.tcp_flags().ece { self.ece_count += 1; }
|
||||||
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);
|
let iat = packet.timestamp_us().saturating_sub(self.last_packet_time);
|
||||||
const IDLE_THRESHOLD_US: u64 = 1_000_000;
|
const IDLE_THRESHOLD_US: u64 = 1_000_000;
|
||||||
|
|
||||||
if iat > IDLE_THRESHOLD_US {
|
if iat > IDLE_THRESHOLD_US {
|
||||||
self.idle_periods.push(iat);
|
// Record cumulative active period before this idle gap
|
||||||
} else if iat > 0 {
|
let active_dur = self.last_packet_time.saturating_sub(self.active_start_us);
|
||||||
self.active_periods.push(iat);
|
if active_dur > 0 && self.active_periods.len() < MAX_PERIODS {
|
||||||
|
self.active_periods.push(active_dur);
|
||||||
|
}
|
||||||
|
if self.idle_periods.len() < MAX_PERIODS {
|
||||||
|
self.idle_periods.push(iat);
|
||||||
|
}
|
||||||
|
self.active_start_us = packet.timestamp_us();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.last_packet_time = packet.timestamp_us();
|
self.last_packet_time = packet.timestamp_us();
|
||||||
self.last_time_us = packet.timestamp_us();
|
self.last_time_us = packet.timestamp_us();
|
||||||
|
|
||||||
if packet.is_forward() {
|
if packet.is_forward() && packet.payload_length() > 0 {
|
||||||
self.fwd_packets.push(packet_data.clone());
|
self.act_data_pkt_fwd += 1;
|
||||||
self.fwd_total_bytes += packet.packet_length() as u64;
|
}
|
||||||
self.fwd_header_bytes += packet.header_length() as u64;
|
|
||||||
|
|
||||||
|
if packet.is_forward() {
|
||||||
|
if self.fwd_packets.len() < MAX_PACKETS_PER_DIRECTION {
|
||||||
|
self.fwd_packets.push(packet_data.clone());
|
||||||
|
}
|
||||||
|
self.fwd_total_bytes += packet.payload_length() as u64;
|
||||||
|
self.fwd_header_bytes += packet.header_length() as u64;
|
||||||
if self.init_win_bytes_fwd == 0 {
|
if self.init_win_bytes_fwd == 0 {
|
||||||
self.init_win_bytes_fwd = packet.tcp_window_size();
|
self.init_win_bytes_fwd = packet.tcp_window_size();
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::update_bulk_state(&mut self.fwd_bulk_state, &packet_data);
|
Self::update_bulk_state(&mut self.fwd_bulk_state, &packet_data);
|
||||||
} else {
|
} else {
|
||||||
self.bwd_packets.push(packet_data.clone());
|
if self.bwd_packets.len() < MAX_PACKETS_PER_DIRECTION {
|
||||||
self.bwd_total_bytes += packet.packet_length() as u64;
|
self.bwd_packets.push(packet_data.clone());
|
||||||
|
}
|
||||||
|
self.bwd_total_bytes += packet.payload_length() as u64;
|
||||||
self.bwd_header_bytes += packet.header_length() as u64;
|
self.bwd_header_bytes += packet.header_length() as u64;
|
||||||
|
|
||||||
if self.init_win_bytes_bwd == 0 {
|
if self.init_win_bytes_bwd == 0 {
|
||||||
self.init_win_bytes_bwd = packet.tcp_window_size();
|
self.init_win_bytes_bwd = packet.tcp_window_size();
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::update_bulk_state(&mut self.bwd_bulk_state, &packet_data);
|
Self::update_bulk_state(&mut self.bwd_bulk_state, &packet_data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TCP flow is finished when both sides have sent FIN, or either side sent RST.
|
||||||
|
pub fn is_finished(&self) -> bool {
|
||||||
|
(self.fwd_fin_seen && self.bwd_fin_seen) || self.rst_count > 0
|
||||||
|
}
|
||||||
|
|
||||||
fn update_bulk_state(bulk_state: &mut BulkState, packet: &PacketData) {
|
fn update_bulk_state(bulk_state: &mut BulkState, packet: &PacketData) {
|
||||||
const BULK_MIN_PACKETS: u64 = 4;
|
const BULK_IDLE_US: u64 = 1_000_000;
|
||||||
const BULK_MIN_BYTES: u64 = 1000;
|
|
||||||
|
|
||||||
if packet.payload_length > 0 {
|
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
|
||||||
|
{
|
||||||
|
bulk_state.in_bulk = false;
|
||||||
|
bulk_state.last_bulk_bytes = 0;
|
||||||
|
bulk_state.last_bulk_packets = 0;
|
||||||
|
bulk_state.last_bulk_start_us = 0;
|
||||||
|
bulk_state.last_bulk_packet_us = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if !bulk_state.in_bulk {
|
if !bulk_state.in_bulk {
|
||||||
bulk_state.in_bulk = true;
|
bulk_state.in_bulk = true;
|
||||||
bulk_state.last_bulk_bytes = packet.length as u64;
|
bulk_state.last_bulk_bytes = packet.length as u64;
|
||||||
@ -156,22 +183,28 @@ impl FlowData {
|
|||||||
bulk_state.last_bulk_start_us = packet.timestamp_us;
|
bulk_state.last_bulk_start_us = packet.timestamp_us;
|
||||||
bulk_state.last_bulk_packet_us = packet.timestamp_us;
|
bulk_state.last_bulk_packet_us = packet.timestamp_us;
|
||||||
} else {
|
} else {
|
||||||
|
let prev_us = bulk_state.last_bulk_packet_us;
|
||||||
bulk_state.last_bulk_bytes += packet.length as u64;
|
bulk_state.last_bulk_bytes += packet.length as u64;
|
||||||
bulk_state.last_bulk_packets += 1;
|
bulk_state.last_bulk_packets += 1;
|
||||||
bulk_state.last_bulk_packet_us = packet.timestamp_us;
|
bulk_state.last_bulk_packet_us = packet.timestamp_us;
|
||||||
|
|
||||||
|
if bulk_state.last_bulk_packets == 4 {
|
||||||
|
// Threshold first reached: commit initial 4 packets
|
||||||
|
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);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if bulk_state.in_bulk
|
// Zero-payload: end bulk sequence (no commit — incomplete bulks discarded)
|
||||||
&& bulk_state.last_bulk_packets >= BULK_MIN_PACKETS
|
|
||||||
&& bulk_state.last_bulk_bytes >= BULK_MIN_BYTES
|
|
||||||
{
|
|
||||||
bulk_state.bulk_count += 1;
|
|
||||||
bulk_state.total_bytes += bulk_state.last_bulk_bytes;
|
|
||||||
bulk_state.total_packets += bulk_state.last_bulk_packets;
|
|
||||||
bulk_state.total_duration_us += bulk_state
|
|
||||||
.last_bulk_packet_us
|
|
||||||
.saturating_sub(bulk_state.last_bulk_start_us);
|
|
||||||
}
|
|
||||||
bulk_state.in_bulk = false;
|
bulk_state.in_bulk = false;
|
||||||
bulk_state.last_bulk_bytes = 0;
|
bulk_state.last_bulk_bytes = 0;
|
||||||
bulk_state.last_bulk_packets = 0;
|
bulk_state.last_bulk_packets = 0;
|
||||||
@ -189,43 +222,39 @@ impl FlowData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 每個 thread 獨立擁有,不共享,無鎖。
|
||||||
|
/// RSS 保證同一條 flow 永遠落在同一個 queue。
|
||||||
pub struct FlowTracker {
|
pub struct FlowTracker {
|
||||||
flows: Arc<Mutex<HashMap<FlowKey, FlowData>>>,
|
flows: HashMap<FlowKey, FlowData>,
|
||||||
max_flows: usize,
|
max_flows: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FlowTracker {
|
impl FlowTracker {
|
||||||
pub fn new(max_flows: usize) -> Self {
|
pub fn new(max_flows: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
flows: Arc::new(Mutex::new(HashMap::new())),
|
flows: HashMap::new(),
|
||||||
max_flows,
|
max_flows,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process_packet(&self, mut packet: Event, is_ingress: bool, payload: &[u8]) {
|
pub fn process_packet(&mut self, mut packet: Event, is_ingress: bool, payload: &[u8]) {
|
||||||
let direction = if is_ingress {
|
let direction = if is_ingress {
|
||||||
Direction::Ingress
|
Direction::Ingress
|
||||||
} else {
|
} else {
|
||||||
Direction::Egress
|
Direction::Egress
|
||||||
};
|
};
|
||||||
let packet_key = FlowKey::from_packet(&packet);
|
|
||||||
let proto = packet_key.protocol;
|
|
||||||
let src_port = packet_key.src_port;
|
|
||||||
let dst_port = packet_key.dst_port;
|
|
||||||
let reversed_key = packet_key.clone().reverse();
|
|
||||||
|
|
||||||
let Ok(mut flows) = self.flows.lock() else {
|
let packet_key = FlowKey::from_packet(&packet);
|
||||||
return;
|
let reversed_key = packet_key.clone().reverse();
|
||||||
};
|
|
||||||
|
|
||||||
// Try-both: canonical key is whichever orientation already exists in the flow table.
|
// Try-both: canonical key is whichever orientation already exists in the flow table.
|
||||||
// For new flows, identify the initiator using (in priority order):
|
// For new flows, identify the initiator using (in priority order):
|
||||||
// 1. TCP SYN / SYN+ACK flags
|
// 1. TCP SYN / SYN+ACK flags
|
||||||
// 2. DPI: TLS ClientHello/ServerHello, HTTP request/response, DNS QR bit
|
// 2. DPI: TLS ClientHello/ServerHello, HTTP request/response, DNS QR bit
|
||||||
// 3. Best effort: use packet as-is
|
// 3. Best effort: use packet as-is
|
||||||
let (actual_key, is_forward) = if flows.contains_key(&packet_key) {
|
let (actual_key, is_forward) = if self.flows.contains_key(&packet_key) {
|
||||||
(packet_key, true)
|
(packet_key, true)
|
||||||
} else if flows.contains_key(&reversed_key) {
|
} else if self.flows.contains_key(&reversed_key) {
|
||||||
(reversed_key, false)
|
(reversed_key, false)
|
||||||
} else {
|
} else {
|
||||||
let flags = packet.tcp_flags();
|
let flags = packet.tcp_flags();
|
||||||
@ -240,7 +269,7 @@ impl FlowTracker {
|
|||||||
} else if flags.syn {
|
} else if flags.syn {
|
||||||
(packet_key, true)
|
(packet_key, true)
|
||||||
} else {
|
} else {
|
||||||
match detect_initiator(payload, proto, src_port, dst_port) {
|
match detect_initiator(payload, packet_key.protocol, packet_key.src_port, packet_key.dst_port) {
|
||||||
Some(true) => (packet_key, true),
|
Some(true) => (packet_key, true),
|
||||||
Some(false) => (reversed_key, false),
|
Some(false) => (reversed_key, false),
|
||||||
None => (packet_key, true),
|
None => (packet_key, true),
|
||||||
@ -250,64 +279,64 @@ impl FlowTracker {
|
|||||||
|
|
||||||
packet.set_is_forward(is_forward);
|
packet.set_is_forward(is_forward);
|
||||||
|
|
||||||
// `direction` should reflect the initiator's interface.
|
|
||||||
// If this packet is backward (is_forward = false), the initiator is on the opposite side.
|
|
||||||
let initiator_direction = if is_forward { direction } else { direction.flip() };
|
let initiator_direction = if is_forward { direction } else { direction.flip() };
|
||||||
|
|
||||||
let flow = flows
|
let flow = self.flows
|
||||||
.entry(actual_key.clone())
|
.entry(actual_key.clone())
|
||||||
.or_insert_with(|| FlowData::new(actual_key, &packet, initiator_direction));
|
.or_insert_with(|| FlowData::new(actual_key, &packet, initiator_direction));
|
||||||
|
|
||||||
flow.add_packet(&packet);
|
flow.add_packet(&packet);
|
||||||
|
|
||||||
if flows.len() > self.max_flows {
|
if self.flows.len() > self.max_flows {
|
||||||
if let Some(key) = flows.keys().next().cloned() {
|
if let Some(key) = self.flows.keys().next().cloned() {
|
||||||
flows.remove(&key);
|
self.flows.remove(&key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn drain_flows(&mut self, min_packets: usize) -> Vec<FlowData> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
self.flows.retain(|_, flow| {
|
||||||
|
if flow.packet_count() >= min_packets {
|
||||||
|
result.push(flow.clone());
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_flows_snapshot(&self) -> Vec<FlowData> {
|
pub fn get_flows_snapshot(&self) -> Vec<FlowData> {
|
||||||
let Ok(flows) = self.flows.lock() else {
|
self.flows.values().cloned().collect()
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
flows.values().cloned().collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_flows_for_inference(&self, min_packets: usize) -> Vec<FlowData> {
|
pub fn get_flows_for_inference(&self, min_packets: usize) -> Vec<FlowData> {
|
||||||
let Ok(flows) = self.flows.lock() else {
|
self.flows
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
flows
|
|
||||||
.values()
|
.values()
|
||||||
.filter(|flow| flow.packet_count() >= min_packets)
|
.filter(|flow| flow.packet_count() >= min_packets)
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cleanup_old_flows(&self, max_age_us: u64) {
|
pub fn cleanup_old_flows(&mut self, max_age_us: u64) {
|
||||||
let now = time::SystemTime::now()
|
let now = time::SystemTime::now()
|
||||||
.duration_since(time::UNIX_EPOCH)
|
.duration_since(time::UNIX_EPOCH)
|
||||||
.map(|d| d.as_micros() as u64)
|
.map(|d| d.as_micros() as u64)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
self.flows.retain(|_, flow| {
|
||||||
let Ok(mut flows) = self.flows.lock() else {
|
// Remove flows that have completed TCP teardown (both FIN or RST)
|
||||||
return;
|
// or have exceeded the idle timeout
|
||||||
};
|
!flow.is_finished()
|
||||||
flows.retain(|_, flow| now.saturating_sub(flow.last_time_us) < max_age_us);
|
&& now.saturating_sub(flow.last_time_us) < max_age_us
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn flow_count(&self) -> usize {
|
pub fn flow_count(&self) -> usize {
|
||||||
let Ok(flows) = self.flows.lock() else {
|
self.flows.len()
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
flows.len()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inspect payload bytes to determine which side is the flow initiator.
|
|
||||||
/// Returns Some(true) if this packet is from the initiator, Some(false) if from the responder,
|
|
||||||
/// or None if the payload gives no useful signal.
|
|
||||||
fn detect_initiator(payload: &[u8], protocol: u8, src_port: u16, dst_port: u16) -> Option<bool> {
|
fn detect_initiator(payload: &[u8], protocol: u8, src_port: u16, dst_port: u16) -> Option<bool> {
|
||||||
if payload.is_empty() {
|
if payload.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
153
net-guardia/src/detection/ml/inference.rs
Normal file
153
net-guardia/src/detection/ml/inference.rs
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use macros::log;
|
||||||
|
use tract_onnx::prelude::*;
|
||||||
|
|
||||||
|
use super::config_loader::InferenceConfig;
|
||||||
|
use super::feature_extractor::FlowFeatures;
|
||||||
|
use super::flow_tracker::FlowData;
|
||||||
|
use super::model_loader::MLModels;
|
||||||
|
use crate::model::error::ml::MLError;
|
||||||
|
use crate::model::log::ml::MLLog;
|
||||||
|
use crate::model::ml_detection::DetectionResult;
|
||||||
|
|
||||||
|
pub struct Inference {
|
||||||
|
pub models: Arc<MLModels>,
|
||||||
|
pub config: Arc<InferenceConfig>,
|
||||||
|
// per-src_ip sliding window buffer: src_ip -> deque of feature vectors
|
||||||
|
flow_buffers: Mutex<HashMap<String, VecDeque<Vec<f32>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Inference {
|
||||||
|
pub fn new(models: Arc<MLModels>, config: Arc<InferenceConfig>) -> Self {
|
||||||
|
Self {
|
||||||
|
models,
|
||||||
|
config,
|
||||||
|
flow_buffers: Mutex::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cleanup_buffers(&self, active_src_ips: &std::collections::HashSet<String>) {
|
||||||
|
let Ok(mut buffers) = self.flow_buffers.lock() else {
|
||||||
|
log!(MLError::InferenceLockPoisoned);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
buffers.retain(|ip, _| active_src_ips.contains(ip));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn infer_batch(&self, flows: &[FlowData]) -> Vec<DetectionResult> {
|
||||||
|
flows.iter().filter_map(|flow| self.infer_single(flow)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn infer_single(&self, flow: &FlowData) -> Option<DetectionResult> {
|
||||||
|
let features = self.preprocess_ae_features(flow);
|
||||||
|
let window_size = self.config.window_size;
|
||||||
|
|
||||||
|
// Update per-src_ip buffer
|
||||||
|
let sequence = {
|
||||||
|
let Ok(mut buffers) = self.flow_buffers.lock() else {
|
||||||
|
log!(MLError::InferenceLockPoisoned);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let buf = buffers
|
||||||
|
.entry(flow.flow_key.src_ip.clone())
|
||||||
|
.or_insert_with(VecDeque::new);
|
||||||
|
|
||||||
|
buf.push_back(features.clone());
|
||||||
|
if buf.len() > window_size {
|
||||||
|
buf.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Buffer [{}->{}]: {}/{} | contents: {:?}",
|
||||||
|
flow.flow_key.src_ip,
|
||||||
|
flow.flow_key.dst_ip,
|
||||||
|
buf.len(),
|
||||||
|
window_size,
|
||||||
|
buf.iter().map(|v| format!("{:.3}", v[0])).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
if buf.len() < window_size {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left-pad with zeros if not enough history
|
||||||
|
let mut seq: Vec<Vec<f32>> = Vec::with_capacity(window_size);
|
||||||
|
let pad_len = window_size.saturating_sub(buf.len());
|
||||||
|
let feat_len = self.config.num_ae_features();
|
||||||
|
for _ in 0..pad_len {
|
||||||
|
seq.push(vec![0.0f32; feat_len]);
|
||||||
|
}
|
||||||
|
for v in buf.iter() {
|
||||||
|
seq.push(v.clone());
|
||||||
|
}
|
||||||
|
seq
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build 3D tensor (1, window_size, features)
|
||||||
|
let feat_len = self.config.num_ae_features();
|
||||||
|
let ae_input = tract_ndarray::Array3::from_shape_fn(
|
||||||
|
(1, window_size, feat_len),
|
||||||
|
|(_, t, f)| sequence[t][f],
|
||||||
|
);
|
||||||
|
|
||||||
|
let ae_score = match self.run_autoencoder(&ae_input) {
|
||||||
|
Ok(score) => score,
|
||||||
|
Err(e) => {
|
||||||
|
log!(MLLog::InferenceFailed("LSTMAutoEncoder".to_string(), e.to_string()));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_attack = ae_score >= self.config.ae_threshold;
|
||||||
|
|
||||||
|
let flow_key = format!(
|
||||||
|
"{}:{} -> {}:{} (proto {}) [{}]",
|
||||||
|
flow.flow_key.src_ip,
|
||||||
|
flow.flow_key.src_port,
|
||||||
|
flow.flow_key.dst_ip,
|
||||||
|
flow.flow_key.dst_port,
|
||||||
|
flow.flow_key.protocol,
|
||||||
|
flow.direction
|
||||||
|
);
|
||||||
|
|
||||||
|
Some(DetectionResult {
|
||||||
|
flow_key,
|
||||||
|
flow_key_raw: flow.flow_key.clone(),
|
||||||
|
direction: flow.direction,
|
||||||
|
is_attack,
|
||||||
|
attack_type: if is_attack { Some("ANOMALY".to_string()) } else { None },
|
||||||
|
confidence: ae_score,
|
||||||
|
ae_score,
|
||||||
|
threshold: self.config.ae_threshold,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preprocess_ae_features(&self, flow: &FlowData) -> Vec<f32> {
|
||||||
|
let mut features = FlowFeatures::extract(flow, &self.config.ae_feature_names);
|
||||||
|
features.winsorize(&self.config.ae_clip_params, &self.config.ae_feature_names);
|
||||||
|
features.normalize(&self.config.ae_scaler_mean, &self.config.ae_scaler_std);
|
||||||
|
features.clip(self.config.ae_post_clip_min, self.config.ae_post_clip_max);
|
||||||
|
features.features.iter().map(|&x| x as f32).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_autoencoder(&self, input: &tract_ndarray::Array3<f32>) -> TractResult<f32> {
|
||||||
|
let result = self
|
||||||
|
.models
|
||||||
|
.deep_autoencoder
|
||||||
|
.run(tvec![input.clone().into_tensor().into()])?;
|
||||||
|
|
||||||
|
let output = result[0]
|
||||||
|
.to_array_view::<f32>()?
|
||||||
|
.into_dimensionality::<tract_ndarray::Ix3>()?;
|
||||||
|
|
||||||
|
// MSE between input and reconstructed output
|
||||||
|
let input_view = input.view();
|
||||||
|
let diff = &input_view - &output;
|
||||||
|
let n = (self.config.window_size * self.config.num_ae_features()) as f32;
|
||||||
|
let mse = (&diff * &diff).sum() / n;
|
||||||
|
|
||||||
|
Ok(mse)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,4 +5,4 @@ pub mod feature_extractor;
|
|||||||
pub mod inference;
|
pub mod inference;
|
||||||
pub mod engine;
|
pub mod engine;
|
||||||
pub mod aggregator;
|
pub mod aggregator;
|
||||||
pub mod traffic_logger;
|
pub mod traffic_logger;
|
||||||
58
net-guardia/src/detection/ml/model_loader.rs
Normal file
58
net-guardia/src/detection/ml/model_loader.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use tract_onnx::prelude::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::core::infrastructure::app_config::AppConfig;
|
||||||
|
use crate::model::error::ml::MLError;
|
||||||
|
use crate::model::ml_detection::RunnableModel;
|
||||||
|
|
||||||
|
use super::config_loader::InferenceConfig;
|
||||||
|
|
||||||
|
pub struct MLModels {
|
||||||
|
pub deep_autoencoder: RunnableModel,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MLModels {
|
||||||
|
pub fn load_models(
|
||||||
|
app_config: &Arc<AppConfig>,
|
||||||
|
inference_config: &Arc<InferenceConfig>,
|
||||||
|
) -> Result<Self, MLError> {
|
||||||
|
Ok(Self {
|
||||||
|
deep_autoencoder: Self::load_lstm_ae(
|
||||||
|
&app_config.deep_autoencoder_name,
|
||||||
|
inference_config.window_size,
|
||||||
|
inference_config.num_ae_features(),
|
||||||
|
)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_lstm_ae(model_name: &str, window_size: usize, features: usize) -> Result<RunnableModel, MLError> {
|
||||||
|
let model_path = PathBuf::from(env!("ARTIFACTCS_PATH")).join(model_name);
|
||||||
|
|
||||||
|
let mut model = onnx()
|
||||||
|
.model_for_path(&model_path)
|
||||||
|
.map_err(|_| MLError::ModelLoadFailed { path: model_path.clone() })?;
|
||||||
|
|
||||||
|
// LSTM AE: input shape = (batch=1, seq_len=window_size, features)
|
||||||
|
model
|
||||||
|
.set_input_fact(0, f32::fact(&[1, window_size, features]).into())
|
||||||
|
.map_err(|_| MLError::ModelLoadFailed { path: model_path.clone() })?;
|
||||||
|
|
||||||
|
model
|
||||||
|
.into_optimized()
|
||||||
|
.map_err(|_| MLError::ModelLoadFailed { path: model_path.clone() })?
|
||||||
|
.into_runnable()
|
||||||
|
.map_err(|_| MLError::ModelLoadFailed { path: model_path })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_model_info(&self, name: &str) -> String {
|
||||||
|
match name {
|
||||||
|
"deep_autoencoder" => {
|
||||||
|
let inputs = self.deep_autoencoder.model().inputs.len();
|
||||||
|
let outputs = self.deep_autoencoder.model().outputs.len();
|
||||||
|
format!("{}: inputs: {}, outputs: {}", name, inputs, outputs)
|
||||||
|
}
|
||||||
|
_ => "unknown model".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,8 @@ use std::fs::OpenOptions;
|
|||||||
use std::io::{BufWriter, Write};
|
use std::io::{BufWriter, Write};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
|
use tracing;
|
||||||
|
|
||||||
use crossbeam::channel::{bounded, Sender, TrySendError};
|
use crossbeam::channel::{bounded, Sender, TrySendError};
|
||||||
|
|
||||||
pub struct TrafficLogger {
|
pub struct TrafficLogger {
|
||||||
@ -27,7 +29,7 @@ impl TrafficLogger {
|
|||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
for record in receiver {
|
for record in receiver {
|
||||||
if let Err(e) = writeln!(writer, "{}", record.join(",")) {
|
if let Err(e) = writeln!(writer, "{}", record.join(",")) {
|
||||||
eprintln!("[traffic-logger] write error: {}", e);
|
tracing::warn!("[traffic-logger] write error: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = writer.flush();
|
let _ = writer.flush();
|
||||||
2
net-guardia/src/detection/mod.rs
Normal file
2
net-guardia/src/detection/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod ml;
|
||||||
|
pub mod rule;
|
||||||
101
net-guardia/src/detection/rule/app_layer/dns.rs
Normal file
101
net-guardia/src/detection/rule/app_layer/dns.rs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
/// Minimal DNS query parser — extracts the QNAME from the first question.
|
||||||
|
///
|
||||||
|
/// DNS over UDP: the payload starts directly at the DNS message header.
|
||||||
|
/// DNS over TCP: the first two bytes are the message length prefix — we
|
||||||
|
/// skip them when the first two bytes look like a length (i.e. the payload
|
||||||
|
/// is longer than the length prefix value).
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct DnsInfo {
|
||||||
|
/// First QNAME decoded to dotted-label ASCII.
|
||||||
|
pub query_name: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_dns(data: &[u8]) -> Option<DnsInfo> {
|
||||||
|
// DNS header is 12 bytes.
|
||||||
|
if data.len() < 12 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heuristic: if this looks like DNS-over-TCP (length-prefixed),
|
||||||
|
// skip the 2-byte length prefix.
|
||||||
|
let base = if has_tcp_length_prefix(data) { 2 } else { 0 };
|
||||||
|
let hdr = data.get(base..base + 12)?;
|
||||||
|
|
||||||
|
// QR bit: must be 0 for a query (we also handle responses for dns_query buffer).
|
||||||
|
// QDCOUNT must be >= 1.
|
||||||
|
let qdcount = u16::from_be_bytes([hdr[4], hdr[5]]);
|
||||||
|
if qdcount == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Questions start after 12-byte header.
|
||||||
|
let mut pos = base + 12;
|
||||||
|
let name = decode_qname(data, &mut pos)?;
|
||||||
|
|
||||||
|
Some(DnsInfo { query_name: name })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a DNS QNAME starting at `*pos`, advancing it past the name and the
|
||||||
|
/// two-byte QTYPE/QCLASS fields. Returns the name in dotted-label form.
|
||||||
|
fn decode_qname(data: &[u8], pos: &mut usize) -> Option<Vec<u8>> {
|
||||||
|
let mut out: Vec<u8> = Vec::new();
|
||||||
|
let mut jumps = 0usize;
|
||||||
|
let mut cur = *pos;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if cur >= data.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let len = data[cur] as usize;
|
||||||
|
|
||||||
|
if len == 0 {
|
||||||
|
// End of name
|
||||||
|
cur += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pointer compression: top 2 bits are 11.
|
||||||
|
if len & 0xC0 == 0xC0 {
|
||||||
|
if cur + 1 >= data.len() { return None; }
|
||||||
|
let offset = ((len & 0x3F) << 8 | data[cur + 1] as usize) as usize;
|
||||||
|
// Only update the caller's position on the first pointer.
|
||||||
|
if jumps == 0 {
|
||||||
|
*pos = cur + 2;
|
||||||
|
}
|
||||||
|
cur = offset;
|
||||||
|
jumps += 1;
|
||||||
|
if jumps > 10 { return None; } // avoid infinite loops
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cur += 1;
|
||||||
|
if cur + len > data.len() { return None; }
|
||||||
|
|
||||||
|
if !out.is_empty() {
|
||||||
|
out.push(b'.');
|
||||||
|
}
|
||||||
|
out.extend_from_slice(&data[cur..cur + len]);
|
||||||
|
cur += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update caller's position only if no pointer compression was used.
|
||||||
|
if jumps == 0 {
|
||||||
|
*pos = cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip QTYPE (2) + QCLASS (2).
|
||||||
|
// (pos is now pointing past the name's null terminator)
|
||||||
|
// We don't update *pos further since callers only need the name.
|
||||||
|
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True if the first two bytes look like a TCP DNS length prefix
|
||||||
|
/// (i.e. `data.len() >= u16_be(data[0..2]) + 2`).
|
||||||
|
fn has_tcp_length_prefix(data: &[u8]) -> bool {
|
||||||
|
if data.len() < 14 { return false; }
|
||||||
|
let len = u16::from_be_bytes([data[0], data[1]]) as usize;
|
||||||
|
// Plausible if the declared length fits in the buffer and is at least 12.
|
||||||
|
len >= 12 && len + 2 <= data.len()
|
||||||
|
}
|
||||||
175
net-guardia/src/detection/rule/app_layer/http.rs
Normal file
175
net-guardia/src/detection/rule/app_layer/http.rs
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
/// Lightweight HTTP/1.x request and response parser.
|
||||||
|
///
|
||||||
|
/// Extracts the fields used by Suricata/Snort HTTP sticky buffers so that
|
||||||
|
/// content rules targeted at `http.uri`, `http.user_agent`, etc. can be
|
||||||
|
/// verified against the correct byte slice instead of the raw TCP payload.
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct HttpRequest {
|
||||||
|
pub method: Vec<u8>,
|
||||||
|
pub uri: Vec<u8>,
|
||||||
|
pub raw_uri: Vec<u8>,
|
||||||
|
pub host: Vec<u8>,
|
||||||
|
pub user_agent: Vec<u8>,
|
||||||
|
pub cookie: Vec<u8>,
|
||||||
|
/// Full request header section (everything before the blank line).
|
||||||
|
pub headers: Vec<u8>,
|
||||||
|
/// Request body (everything after the blank line).
|
||||||
|
pub client_body: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct HttpResponse {
|
||||||
|
pub stat_code: Vec<u8>,
|
||||||
|
/// Full response header section.
|
||||||
|
pub headers: Vec<u8>,
|
||||||
|
/// Response body.
|
||||||
|
pub server_body: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static HTTP_METHODS: &[&[u8]] = &[
|
||||||
|
b"GET", b"POST", b"PUT", b"DELETE", b"HEAD",
|
||||||
|
b"OPTIONS", b"PATCH", b"CONNECT", b"TRACE",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Try to parse `data` as an HTTP/1.x request.
|
||||||
|
/// Returns `None` when the data does not start with a recognised method.
|
||||||
|
pub fn parse_http_request(data: &[u8]) -> Option<HttpRequest> {
|
||||||
|
// Find first space — end of method token.
|
||||||
|
let sp1 = data.iter().position(|&b| b == b' ')?;
|
||||||
|
let method = &data[..sp1];
|
||||||
|
|
||||||
|
// Validate method
|
||||||
|
if !HTTP_METHODS.contains(&method) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find second space — end of URI.
|
||||||
|
let after_sp1 = sp1 + 1;
|
||||||
|
let sp2 = data[after_sp1..].iter().position(|&b| b == b' ')?;
|
||||||
|
let uri = data[after_sp1..after_sp1 + sp2].to_vec();
|
||||||
|
|
||||||
|
// Find end of first line.
|
||||||
|
let crlf1 = find_crlf(data, after_sp1 + sp2)?;
|
||||||
|
let headers_start = crlf1 + 2;
|
||||||
|
|
||||||
|
// Find end of headers (blank line).
|
||||||
|
let (headers_end, body_start) = find_header_end(data, headers_start);
|
||||||
|
let headers_bytes = &data[..headers_end]; // includes request line
|
||||||
|
|
||||||
|
let mut req = HttpRequest {
|
||||||
|
method: method.to_vec(),
|
||||||
|
raw_uri: uri.clone(),
|
||||||
|
uri: decode_uri(&uri),
|
||||||
|
headers: headers_bytes.to_vec(),
|
||||||
|
client_body: data.get(body_start..).unwrap_or(&[]).to_vec(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Walk individual header lines to extract named fields.
|
||||||
|
let mut pos = headers_start;
|
||||||
|
while pos < headers_end {
|
||||||
|
let line_end = find_crlf(data, pos).unwrap_or(headers_end);
|
||||||
|
let line = &data[pos..line_end];
|
||||||
|
extract_header_field(line, &mut req);
|
||||||
|
pos = line_end + 2;
|
||||||
|
if pos >= headers_end { break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to parse `data` as an HTTP/1.x response.
|
||||||
|
/// Returns `None` when the data does not start with `HTTP/`.
|
||||||
|
pub fn parse_http_response(data: &[u8]) -> Option<HttpResponse> {
|
||||||
|
if !data.starts_with(b"HTTP/") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "HTTP/1.x NNN " — status code starts after version + space.
|
||||||
|
let sp1 = data.iter().position(|&b| b == b' ')?;
|
||||||
|
let code_start = sp1 + 1;
|
||||||
|
let sp2 = data[code_start..].iter().position(|&b| b == b' ' || b == b'\r' || b == b'\n')
|
||||||
|
.unwrap_or(3); // 3-digit code
|
||||||
|
let stat_code = data[code_start..code_start + sp2].to_vec();
|
||||||
|
|
||||||
|
let crlf1 = find_crlf(data, code_start + sp2)?;
|
||||||
|
let headers_start = crlf1 + 2;
|
||||||
|
let (headers_end, body_start) = find_header_end(data, headers_start);
|
||||||
|
|
||||||
|
Some(HttpResponse {
|
||||||
|
stat_code,
|
||||||
|
headers: data[..headers_end].to_vec(),
|
||||||
|
server_body: data.get(body_start..).unwrap_or(&[]).to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Populate named fields from a single HTTP header line (`Name: value`).
|
||||||
|
fn extract_header_field(line: &[u8], req: &mut HttpRequest) {
|
||||||
|
let colon = match line.iter().position(|&b| b == b':') {
|
||||||
|
Some(p) => p,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let name = &line[..colon];
|
||||||
|
let value = line.get(colon + 1..).map(|v| v.trim_ascii_start()).unwrap_or(&[]);
|
||||||
|
|
||||||
|
if name.eq_ignore_ascii_case(b"Host") {
|
||||||
|
req.host = value.to_vec();
|
||||||
|
} else if name.eq_ignore_ascii_case(b"User-Agent") {
|
||||||
|
req.user_agent = value.to_vec();
|
||||||
|
} else if name.eq_ignore_ascii_case(b"Cookie") {
|
||||||
|
req.cookie = value.to_vec();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the first CRLF (`\r\n`) in `data` starting at `from`.
|
||||||
|
/// Returns the offset of `\r`.
|
||||||
|
fn find_crlf(data: &[u8], from: usize) -> Option<usize> {
|
||||||
|
let slice = data.get(from..)?;
|
||||||
|
let pos = slice.windows(2).position(|w| w == b"\r\n")?;
|
||||||
|
Some(from + pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find end-of-headers (blank line `\r\n\r\n`) starting at `from`.
|
||||||
|
/// Returns `(headers_end, body_start)` where `headers_end` is the offset of
|
||||||
|
/// the first `\r` of the blank line, and `body_start` is after the blank line.
|
||||||
|
fn find_header_end(data: &[u8], from: usize) -> (usize, usize) {
|
||||||
|
let slice = match data.get(from..) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => return (data.len(), data.len()),
|
||||||
|
};
|
||||||
|
if let Some(pos) = slice.windows(4).position(|w| w == b"\r\n\r\n") {
|
||||||
|
let abs = from + pos;
|
||||||
|
(abs + 2, abs + 4) // end at blank line start; body after blank line
|
||||||
|
} else {
|
||||||
|
(data.len(), data.len())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Percent-decode a URI byte slice.
|
||||||
|
fn decode_uri(uri: &[u8]) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(uri.len());
|
||||||
|
let mut i = 0;
|
||||||
|
while i < uri.len() {
|
||||||
|
if uri[i] == b'%' && i + 2 < uri.len() {
|
||||||
|
if let (Some(h), Some(l)) = (hex_val(uri[i + 1]), hex_val(uri[i + 2])) {
|
||||||
|
out.push((h << 4) | l);
|
||||||
|
i += 3;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push(uri[i]);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn hex_val(b: u8) -> Option<u8> {
|
||||||
|
match b {
|
||||||
|
b'0'..=b'9' => Some(b - b'0'),
|
||||||
|
b'a'..=b'f' => Some(b - b'a' + 10),
|
||||||
|
b'A'..=b'F' => Some(b - b'A' + 10),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
121
net-guardia/src/detection/rule/app_layer/mod.rs
Normal file
121
net-guardia/src/detection/rule/app_layer/mod.rs
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
pub mod dns;
|
||||||
|
pub mod http;
|
||||||
|
pub mod tls;
|
||||||
|
|
||||||
|
use http::{HttpRequest, HttpResponse};
|
||||||
|
|
||||||
|
pub use crate::model::rule_detection::{ParsedPacket, buf};
|
||||||
|
|
||||||
|
/// Well-known TLS server ports. When either sport or dport matches, the flow
|
||||||
|
/// is treated as TLS unconditionally — even for mid-stream captures where the
|
||||||
|
/// TCP payload no longer starts at a TLS record boundary.
|
||||||
|
fn is_tls_port(port: u16) -> bool {
|
||||||
|
matches!(port,
|
||||||
|
443 // HTTPS
|
||||||
|
| 8443 // HTTPS alt
|
||||||
|
| 465 // SMTPS
|
||||||
|
| 993 // IMAPS
|
||||||
|
| 995 // POP3S
|
||||||
|
| 636 // LDAPS
|
||||||
|
| 853 // DNS-over-TLS
|
||||||
|
| 4433 // HTTPS alt
|
||||||
|
| 8883 // MQTT over TLS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse `data` into a `ParsedPacket`.
|
||||||
|
///
|
||||||
|
/// Both `sport` and `dport` are needed so that we can identify TLS flows by
|
||||||
|
/// server port even when the callback only has the ephemeral client port stored
|
||||||
|
/// in `current_dport` (e.g. the flow key was created from an inbound Google
|
||||||
|
/// packet: sip=142.251.x.x:443 → dip=local:52873, so sport=443).
|
||||||
|
///
|
||||||
|
/// Detection order:
|
||||||
|
/// 1. TLS — content-based header check OR port-based hint (catches mid-stream)
|
||||||
|
/// 2. HTTP request/response
|
||||||
|
/// 3. DNS (port 53 / 5353)
|
||||||
|
pub fn detect_and_parse(data: &[u8], sport: u16, dport: u16) -> ParsedPacket {
|
||||||
|
let raw = data.to_vec();
|
||||||
|
let mut pkt = ParsedPacket {
|
||||||
|
raw,
|
||||||
|
tls_app_data: false,
|
||||||
|
is_tls: false,
|
||||||
|
http_uri: Vec::new(),
|
||||||
|
http_raw_uri: Vec::new(),
|
||||||
|
http_method: Vec::new(),
|
||||||
|
http_host: Vec::new(),
|
||||||
|
http_user_agent: Vec::new(),
|
||||||
|
http_cookie: Vec::new(),
|
||||||
|
http_header: Vec::new(),
|
||||||
|
http_client_body: Vec::new(),
|
||||||
|
http_stat_code: Vec::new(),
|
||||||
|
http_server_body: Vec::new(),
|
||||||
|
tls_sni: Vec::new(),
|
||||||
|
dns_query: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if data.is_empty() {
|
||||||
|
return pkt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. TLS — content-based detection first (handles normal cases, extracts SNI).
|
||||||
|
if let Some(tls) = tls::detect_tls(data) {
|
||||||
|
pkt.raw.clear();
|
||||||
|
pkt.is_tls = true;
|
||||||
|
if tls.is_app_data {
|
||||||
|
pkt.tls_app_data = true;
|
||||||
|
return pkt;
|
||||||
|
}
|
||||||
|
pkt.tls_sni = tls.sni;
|
||||||
|
return pkt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. HTTP request / response — must come BEFORE port-based TLS fallback
|
||||||
|
// so that plaintext HTTP on port 443 is identified correctly.
|
||||||
|
if let Some(req) = http::parse_http_request(data) {
|
||||||
|
apply_request(&mut pkt, req);
|
||||||
|
return pkt;
|
||||||
|
}
|
||||||
|
if let Some(resp) = http::parse_http_response(data) {
|
||||||
|
apply_response(&mut pkt, resp);
|
||||||
|
return pkt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. DNS
|
||||||
|
if dport == 53 || dport == 5353 || sport == 53 || sport == 5353 {
|
||||||
|
if let Some(dns) = dns::parse_dns(data) {
|
||||||
|
pkt.dns_query = dns.query_name;
|
||||||
|
}
|
||||||
|
return pkt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Port-based TLS fallback — reached only when content detection found
|
||||||
|
// nothing. Handles mid-stream captures of TLS flows where the first
|
||||||
|
// byte is not a TLS record header (connection established before we
|
||||||
|
// started capturing). Suricata equivalent: AppLayerProtoDetectGetProto
|
||||||
|
// with port hints as last resort.
|
||||||
|
if is_tls_port(sport) || is_tls_port(dport) {
|
||||||
|
pkt.raw.clear();
|
||||||
|
pkt.is_tls = true;
|
||||||
|
return pkt;
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_request(pkt: &mut ParsedPacket, req: HttpRequest) {
|
||||||
|
pkt.http_uri = req.uri;
|
||||||
|
pkt.http_raw_uri = req.raw_uri;
|
||||||
|
pkt.http_method = req.method;
|
||||||
|
pkt.http_host = req.host;
|
||||||
|
pkt.http_user_agent = req.user_agent;
|
||||||
|
pkt.http_cookie = req.cookie;
|
||||||
|
pkt.http_header = req.headers;
|
||||||
|
pkt.http_client_body = req.client_body;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_response(pkt: &mut ParsedPacket, resp: HttpResponse) {
|
||||||
|
pkt.http_stat_code = resp.stat_code;
|
||||||
|
pkt.http_header = resp.headers;
|
||||||
|
pkt.http_server_body = resp.server_body;
|
||||||
|
}
|
||||||
141
net-guardia/src/detection/rule/app_layer/tls.rs
Normal file
141
net-guardia/src/detection/rule/app_layer/tls.rs
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
/// TLS record-layer parser.
|
||||||
|
///
|
||||||
|
/// Detects TLS by the 5-byte record header and extracts the SNI hostname
|
||||||
|
/// from a ClientHello handshake message. Most importantly, it identifies
|
||||||
|
/// TLS Application Data records (type 0x17) so that the rule engine can
|
||||||
|
/// skip scanning encrypted payload — the primary source of false positives.
|
||||||
|
|
||||||
|
/// Recognised TLS record content types.
|
||||||
|
const RT_CHANGE_CIPHER: u8 = 0x14;
|
||||||
|
const RT_ALERT: u8 = 0x15;
|
||||||
|
const RT_HANDSHAKE: u8 = 0x16;
|
||||||
|
const RT_APP_DATA: u8 = 0x17;
|
||||||
|
|
||||||
|
const HS_CLIENT_HELLO: u8 = 0x01;
|
||||||
|
const EXT_SNI: u16 = 0x0000;
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct TlsInfo {
|
||||||
|
/// True when the first record in `data` is Application Data (encrypted).
|
||||||
|
/// The rule engine MUST skip all content scanning for such payloads.
|
||||||
|
pub is_app_data: bool,
|
||||||
|
/// SNI hostname bytes extracted from a ClientHello, if present.
|
||||||
|
pub sni: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to parse `data` as a TLS record stream.
|
||||||
|
/// Returns `None` when `data` does not look like a TLS record.
|
||||||
|
pub fn detect_tls(data: &[u8]) -> Option<TlsInfo> {
|
||||||
|
if data.len() < 5 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let record_type = data[0];
|
||||||
|
if !matches!(record_type, RT_CHANGE_CIPHER | RT_ALERT | RT_HANDSHAKE | RT_APP_DATA) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLS major version must be 3.
|
||||||
|
if data[1] != 3 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Minor version 0-4 (SSL3..TLS1.3).
|
||||||
|
if data[2] > 4 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if record_type == RT_APP_DATA {
|
||||||
|
return Some(TlsInfo { is_app_data: true, sni: Vec::new() });
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut info = TlsInfo::default();
|
||||||
|
|
||||||
|
if record_type == RT_HANDSHAKE {
|
||||||
|
info.sni = extract_sni(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the SNI hostname from a TLS ClientHello record.
|
||||||
|
/// Returns an empty Vec when SNI is absent or the record is malformed.
|
||||||
|
fn extract_sni(data: &[u8]) -> Vec<u8> {
|
||||||
|
// TLS record header: type(1) + version(2) + length(2) = 5 bytes
|
||||||
|
// Handshake header: type(1) + length(3) = 4 bytes
|
||||||
|
// ClientHello: version(2) + random(32) + session_id_len(1) + ...
|
||||||
|
if data.len() < 5 + 4 + 2 + 32 + 1 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let hs = &data[5..]; // start of Handshake layer
|
||||||
|
if hs[0] != HS_CLIENT_HELLO {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let hs_len = u24_be(&hs[1..4]) as usize;
|
||||||
|
if hs.len() < 4 + hs_len {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let ch = &hs[4..4 + hs_len]; // ClientHello body
|
||||||
|
// version(2) + random(32) = 34 bytes minimum
|
||||||
|
if ch.len() < 34 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pos = 34usize; // skip version + random
|
||||||
|
|
||||||
|
// Session ID
|
||||||
|
if pos >= ch.len() { return Vec::new(); }
|
||||||
|
let sid_len = ch[pos] as usize;
|
||||||
|
pos += 1 + sid_len;
|
||||||
|
|
||||||
|
// Cipher suites
|
||||||
|
if pos + 2 > ch.len() { return Vec::new(); }
|
||||||
|
let cs_len = u16_be(&ch[pos..]) as usize;
|
||||||
|
pos += 2 + cs_len;
|
||||||
|
|
||||||
|
// Compression methods
|
||||||
|
if pos >= ch.len() { return Vec::new(); }
|
||||||
|
let cm_len = ch[pos] as usize;
|
||||||
|
pos += 1 + cm_len;
|
||||||
|
|
||||||
|
// Extensions
|
||||||
|
if pos + 2 > ch.len() { return Vec::new(); }
|
||||||
|
let ext_total = u16_be(&ch[pos..]) as usize;
|
||||||
|
pos += 2;
|
||||||
|
let ext_end = pos + ext_total;
|
||||||
|
if ext_end > ch.len() { return Vec::new(); }
|
||||||
|
|
||||||
|
while pos + 4 <= ext_end {
|
||||||
|
let ext_type = u16_be(&ch[pos..]);
|
||||||
|
let ext_len = u16_be(&ch[pos + 2..]) as usize;
|
||||||
|
pos += 4;
|
||||||
|
if pos + ext_len > ext_end { break; }
|
||||||
|
|
||||||
|
if ext_type == EXT_SNI && ext_len >= 5 {
|
||||||
|
// SNI list: list_len(2) + name_type(1) + name_len(2) + name
|
||||||
|
let list_len = u16_be(&ch[pos..]) as usize;
|
||||||
|
if list_len + 2 > ext_len { break; }
|
||||||
|
let name_type = ch[pos + 2];
|
||||||
|
let name_len = u16_be(&ch[pos + 3..]) as usize;
|
||||||
|
if name_type == 0 && pos + 5 + name_len <= ext_end {
|
||||||
|
return ch[pos + 5..pos + 5 + name_len].to_vec();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pos += ext_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn u16_be(b: &[u8]) -> u16 {
|
||||||
|
u16::from_be_bytes([b[0], b[1]])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn u24_be(b: &[u8]) -> u32 {
|
||||||
|
(b[0] as u32) << 16 | (b[1] as u32) << 8 | b[2] as u32
|
||||||
|
}
|
||||||
3
net-guardia/src/detection/rule/mod.rs
Normal file
3
net-guardia/src/detection/rule/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod app_layer;
|
||||||
|
pub mod rule_engine;
|
||||||
|
pub mod stream_reassembler;
|
||||||
315
net-guardia/src/detection/rule/rule_engine.rs
Normal file
315
net-guardia/src/detection/rule/rule_engine.rs
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use rusqlite::{Connection, params};
|
||||||
|
use vectorscan_rs::{BlockDatabase, Flag, Pattern, Scan};
|
||||||
|
|
||||||
|
use crate::model::error::rule::RuleError;
|
||||||
|
use crate::model::rule_detection::{RuleMatch, RuleSig, ContentEntry, ParsedPacket};
|
||||||
|
|
||||||
|
pub struct RuleEngine {
|
||||||
|
db: BlockDatabase,
|
||||||
|
sigs: Vec<RuleSig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RuleEngine {
|
||||||
|
pub fn new() -> Option<Self> {
|
||||||
|
let conn = Connection::open(PathBuf::from(env!("RULES_DB_PATH")).join("rules.db")).ok()?;
|
||||||
|
let patterns = load_patterns(&conn)?;
|
||||||
|
let sigs = load_sigs(&conn)?;
|
||||||
|
|
||||||
|
if patterns.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let db = match BlockDatabase::new(patterns) {
|
||||||
|
Ok(db) => db,
|
||||||
|
Err(e) => {
|
||||||
|
RuleError::BlockDatabaseFailed { error: e.to_string() };
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Self { db, sigs })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pattern_count(&self) -> usize {
|
||||||
|
self.sigs.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scan a parsed application-layer packet.
|
||||||
|
///
|
||||||
|
/// Returns an empty list immediately for TLS Application Data (encrypted)
|
||||||
|
/// because scanning random cipher bytes produces only false positives.
|
||||||
|
pub fn scan(&self, pkt: &ParsedPacket, is_ingress: bool, dst_port: u16) -> Vec<RuleMatch> {
|
||||||
|
// Never scan encrypted TLS application data.
|
||||||
|
if pkt.tls_app_data {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
if pkt.raw.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut scanner = match self.db.create_scanner() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prefilter: run vectorscan against the raw payload.
|
||||||
|
// For HTTP traffic the raw bytes include the full request/response, so
|
||||||
|
// URI / header patterns are still found here. Buffer-specific chain
|
||||||
|
// verification then confirms using the correct parsed field.
|
||||||
|
let mut candidates: Vec<u32> = Vec::new();
|
||||||
|
let _ = scanner.scan(&pkt.raw, |id, _from, _to, _flags| {
|
||||||
|
candidates.push(id);
|
||||||
|
Scan::Continue
|
||||||
|
});
|
||||||
|
candidates.sort_unstable();
|
||||||
|
candidates.dedup();
|
||||||
|
|
||||||
|
let mut results = Vec::new();
|
||||||
|
|
||||||
|
for sig_idx in candidates {
|
||||||
|
let Some(sig) = self.sigs.get(sig_idx as usize) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Direction filter
|
||||||
|
let dir_ok = match sig.flow_dir {
|
||||||
|
1 => is_ingress,
|
||||||
|
2 => !is_ingress,
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
|
if !dir_ok {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port filter
|
||||||
|
if !sig.dports.is_empty() && !sig.dports.contains(&dst_port) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full chain verification with per-entry buffer routing.
|
||||||
|
if verify_chain(&sig.chain, pkt) {
|
||||||
|
results.push(RuleMatch {
|
||||||
|
sid: sig.sid,
|
||||||
|
msg: sig.msg.clone(),
|
||||||
|
src: String::new(),
|
||||||
|
dst: String::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── SQLite loaders ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn load_patterns(conn: &Connection) -> Option<Vec<Pattern>> {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare("SELECT id, expression, nocase FROM patterns ORDER BY id")
|
||||||
|
.ok()?;
|
||||||
|
let patterns: Vec<Pattern> = stmt
|
||||||
|
.query_map([], |row| {
|
||||||
|
let id: i64 = row.get(0)?;
|
||||||
|
let expression: Vec<u8> = row.get(1)?;
|
||||||
|
let nocase: i64 = row.get(2)?;
|
||||||
|
let flags = if nocase != 0 { Flag::CASELESS } else { Flag::default() };
|
||||||
|
Ok(Pattern::new(expression, flags, Some(id as u32)))
|
||||||
|
})
|
||||||
|
.ok()?
|
||||||
|
.filter_map(|r| r.ok())
|
||||||
|
.collect();
|
||||||
|
Some(patterns)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_sigs(conn: &Connection) -> Option<Vec<RuleSig>> {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare("SELECT id, sid, flow_dir, msg FROM signatures ORDER BY id")
|
||||||
|
.ok()?;
|
||||||
|
let sig_rows: Vec<(i64, u32, u8, String)> = stmt
|
||||||
|
.query_map([], |row| {
|
||||||
|
Ok((
|
||||||
|
row.get::<_, i64>(0)?,
|
||||||
|
row.get::<_, u32>(1)?,
|
||||||
|
row.get::<_, u8>(2)?,
|
||||||
|
row.get::<_, String>(3)?,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.ok()?
|
||||||
|
.filter_map(|r| r.ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut sigs = Vec::with_capacity(sig_rows.len());
|
||||||
|
for (sig_id, sid, flow_dir, msg) in sig_rows {
|
||||||
|
let mut port_stmt = conn
|
||||||
|
.prepare("SELECT dport FROM signature_ports WHERE sig_id = ?1")
|
||||||
|
.ok()?;
|
||||||
|
let dports: Vec<u16> = port_stmt
|
||||||
|
.query_map(params![sig_id], |row| row.get::<_, u16>(0))
|
||||||
|
.ok()?
|
||||||
|
.filter_map(|r| r.ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut entry_stmt = conn
|
||||||
|
.prepare(
|
||||||
|
"SELECT pattern, nocase, negated, \
|
||||||
|
has_distance, has_within, has_offset, has_depth, \
|
||||||
|
distance, within, off, depth, buffer \
|
||||||
|
FROM content_entries WHERE sig_id = ?1 ORDER BY entry_order",
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
let chain: Vec<ContentEntry> = entry_stmt
|
||||||
|
.query_map(params![sig_id], |row| {
|
||||||
|
Ok(ContentEntry {
|
||||||
|
pattern: row.get(0)?,
|
||||||
|
nocase: row.get::<_, i64>(1)? != 0,
|
||||||
|
negated: row.get::<_, i64>(2)? != 0,
|
||||||
|
has_distance: row.get::<_, i64>(3)? != 0,
|
||||||
|
has_within: row.get::<_, i64>(4)? != 0,
|
||||||
|
has_offset: row.get::<_, i64>(5)? != 0,
|
||||||
|
has_depth: row.get::<_, i64>(6)? != 0,
|
||||||
|
distance: row.get::<_, i32>(7)?,
|
||||||
|
within: row.get::<_, i32>(8)?,
|
||||||
|
offset: row.get::<_, u16>(9)?,
|
||||||
|
depth: row.get::<_, u16>(10)?,
|
||||||
|
buffer: row.get::<_, u8>(11)?,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.ok()?
|
||||||
|
.filter_map(|r| r.ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
sigs.push(RuleSig { sid, flow_dir, dports, msg, chain });
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(sigs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Chain verification ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn verify_chain(chain: &[ContentEntry], pkt: &ParsedPacket) -> bool {
|
||||||
|
verify_from(chain, pkt, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively verify the content chain starting at `idx` with `prev_end` as
|
||||||
|
/// the anchor for relative constraints.
|
||||||
|
///
|
||||||
|
/// Each `ContentEntry` carries a `buffer` id; entries within the same chain
|
||||||
|
/// can target different buffers. When a required buffer is empty (e.g. no
|
||||||
|
/// HTTP was detected), a non-negated entry immediately fails the chain.
|
||||||
|
fn verify_from(chain: &[ContentEntry], pkt: &ParsedPacket, idx: usize, prev_end: usize) -> bool {
|
||||||
|
if idx >= chain.len() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry = &chain[idx];
|
||||||
|
let payload = pkt.buffer(entry.buffer);
|
||||||
|
|
||||||
|
// If the required buffer was not populated (e.g. http_uri for non-HTTP
|
||||||
|
// traffic), the pattern can never match → fail any non-negated chain.
|
||||||
|
if payload.is_empty() && !entry.negated {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let buf_len = payload.len();
|
||||||
|
let (ws, we) = compute_window(entry, prev_end, buf_len);
|
||||||
|
|
||||||
|
// Degenerate window
|
||||||
|
if ws >= we || ws >= buf_len {
|
||||||
|
return if entry.negated {
|
||||||
|
verify_from(chain, pkt, idx + 1, prev_end)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let window = &payload[ws..we];
|
||||||
|
|
||||||
|
if entry.negated {
|
||||||
|
if find_in(window, &entry.pattern, entry.nocase).is_some() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return verify_from(chain, pkt, idx + 1, prev_end);
|
||||||
|
}
|
||||||
|
|
||||||
|
let next_is_relative = idx + 1 < chain.len() && {
|
||||||
|
let next = &chain[idx + 1];
|
||||||
|
next.has_distance || next.has_within
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut search_from = 0usize;
|
||||||
|
loop {
|
||||||
|
let sub = &window[search_from..];
|
||||||
|
let Some(rel_pos) = find_in(sub, &entry.pattern, entry.nocase) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
let match_end = ws + search_from + rel_pos + entry.pattern.len();
|
||||||
|
|
||||||
|
if verify_from(chain, pkt, idx + 1, match_end) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !next_is_relative {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
search_from += rel_pos + 1;
|
||||||
|
if search_from >= window.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the [start, end) search window for a content entry.
|
||||||
|
fn compute_window(entry: &ContentEntry, prev_end: usize, buf_len: usize) -> (usize, usize) {
|
||||||
|
let (mut start, mut end) = if entry.has_distance || entry.has_within {
|
||||||
|
let dist = entry.distance;
|
||||||
|
let s = if dist >= 0 {
|
||||||
|
prev_end.saturating_add(dist as usize)
|
||||||
|
} else {
|
||||||
|
prev_end.saturating_sub((-dist) as usize)
|
||||||
|
};
|
||||||
|
let e = if entry.has_within {
|
||||||
|
let dist_non_neg = dist.max(0) as usize;
|
||||||
|
prev_end.saturating_add(dist_non_neg).saturating_add(entry.within as usize)
|
||||||
|
} else {
|
||||||
|
buf_len
|
||||||
|
};
|
||||||
|
(s, e)
|
||||||
|
} else {
|
||||||
|
let s = if entry.has_offset { entry.offset as usize } else { 0 };
|
||||||
|
let e = if entry.has_depth { entry.depth as usize } else { buf_len };
|
||||||
|
(s, e)
|
||||||
|
};
|
||||||
|
|
||||||
|
if entry.has_offset {
|
||||||
|
start = start.max(entry.offset as usize);
|
||||||
|
}
|
||||||
|
if entry.has_depth {
|
||||||
|
end = end.min(entry.depth as usize);
|
||||||
|
}
|
||||||
|
|
||||||
|
(start, end.min(buf_len))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_in(haystack: &[u8], needle: &[u8], nocase: bool) -> Option<usize> {
|
||||||
|
if needle.is_empty() {
|
||||||
|
return Some(0);
|
||||||
|
}
|
||||||
|
if haystack.len() < needle.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if nocase {
|
||||||
|
let needle_lc: Vec<u8> = needle.iter().map(|b| b.to_ascii_lowercase()).collect();
|
||||||
|
haystack
|
||||||
|
.windows(needle.len())
|
||||||
|
.position(|w| w.iter().zip(&needle_lc).all(|(a, b)| a.to_ascii_lowercase() == *b))
|
||||||
|
} else {
|
||||||
|
haystack.windows(needle.len()).position(|w| w == needle)
|
||||||
|
}
|
||||||
|
}
|
||||||
389
net-guardia/src/detection/rule/stream_reassembler.rs
Normal file
389
net-guardia/src/detection/rule/stream_reassembler.rs
Normal file
@ -0,0 +1,389 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::ffi::c_void;
|
||||||
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use protolens::{Prolens, TransProto};
|
||||||
|
|
||||||
|
use macros::log;
|
||||||
|
|
||||||
|
use super::app_layer;
|
||||||
|
use super::rule_engine::RuleEngine;
|
||||||
|
use crate::model::error::rule::RuleError;
|
||||||
|
use crate::model::rule_detection::{FlowKey, FlowProto, FlowState, RawPkt, RuleMatch};
|
||||||
|
|
||||||
|
|
||||||
|
/// Per-thread TCP stream reassembler backed by protolens.
|
||||||
|
/// Not Send (Prolens uses Rc/RefCell) — must be created inside the XSK thread.
|
||||||
|
pub struct StreamReassembler {
|
||||||
|
engine: Prolens<RawPkt>,
|
||||||
|
flows: HashMap<FlowKey, FlowState>,
|
||||||
|
rule_engine: Arc<RuleEngine>,
|
||||||
|
pending: Rc<RefCell<Vec<RuleMatch>>>,
|
||||||
|
|
||||||
|
/// Source port of the active flow — set before each run_task call.
|
||||||
|
current_sport: Rc<RefCell<u16>>,
|
||||||
|
|
||||||
|
/// Destination port of the active flow.
|
||||||
|
current_dport: Rc<RefCell<u16>>,
|
||||||
|
|
||||||
|
/// Protocol already identified for the active flow (FlowProto as u8).
|
||||||
|
/// Callbacks write back here when they first detect the protocol so that
|
||||||
|
/// process_tcp() can persist it into FlowState.detected_proto.
|
||||||
|
current_proto: Rc<RefCell<u8>>,
|
||||||
|
|
||||||
|
/// (flow_key, sid) -> cumulative match count for min_matches threshold
|
||||||
|
flow_sid_counts: HashMap<(FlowKey, u32), u32>,
|
||||||
|
min_matches: u32,
|
||||||
|
is_ingress: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StreamReassembler {
|
||||||
|
pub fn new(rule_engine: Arc<RuleEngine>, min_matches: u32, is_ingress: bool) -> Self {
|
||||||
|
let pending = Rc::new(RefCell::new(Vec::<RuleMatch>::new()));
|
||||||
|
let current_sport = Rc::new(RefCell::new(0u16));
|
||||||
|
let current_dport = Rc::new(RefCell::new(0u16));
|
||||||
|
let current_proto = Rc::new(RefCell::new(0u8));
|
||||||
|
|
||||||
|
let p_c2s = Rc::clone(&pending);
|
||||||
|
let re_c2s = Arc::clone(&rule_engine);
|
||||||
|
let sp_c2s = Rc::clone(¤t_sport);
|
||||||
|
let port_c2s = Rc::clone(¤t_dport);
|
||||||
|
let pr_c2s = Rc::clone(¤t_proto);
|
||||||
|
|
||||||
|
let p_s2c = Rc::clone(&pending);
|
||||||
|
let re_s2c = Arc::clone(&rule_engine);
|
||||||
|
let sp_s2c = Rc::clone(¤t_sport);
|
||||||
|
let port_s2c = Rc::clone(¤t_dport);
|
||||||
|
let pr_s2c = Rc::clone(¤t_proto);
|
||||||
|
|
||||||
|
let mut engine = Prolens::<RawPkt>::default();
|
||||||
|
|
||||||
|
engine.set_cb_task_c2s(move |data: &[u8], _seq: u32, _ctx: *const c_void| {
|
||||||
|
// If the flow was already identified as TLS, skip entirely —
|
||||||
|
// mirrors Suricata's per-flow alproto check.
|
||||||
|
if *pr_c2s.borrow() == FlowProto::Tls as u8 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let sport = *sp_c2s.borrow();
|
||||||
|
let dport = *port_c2s.borrow();
|
||||||
|
let pkt = app_layer::detect_and_parse(data, sport, dport);
|
||||||
|
if pkt.is_tls {
|
||||||
|
*pr_c2s.borrow_mut() = FlowProto::Tls as u8;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
p_c2s.borrow_mut().extend(re_c2s.scan(&pkt, true, dport));
|
||||||
|
});
|
||||||
|
|
||||||
|
engine.set_cb_task_s2c(move |data: &[u8], _seq: u32, _ctx: *const c_void| {
|
||||||
|
if *pr_s2c.borrow() == FlowProto::Tls as u8 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let sport = *sp_s2c.borrow();
|
||||||
|
let dport = *port_s2c.borrow();
|
||||||
|
let pkt = app_layer::detect_and_parse(data, sport, dport);
|
||||||
|
if pkt.is_tls {
|
||||||
|
*pr_s2c.borrow_mut() = FlowProto::Tls as u8;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
p_s2c.borrow_mut().extend(re_s2c.scan(&pkt, false, dport));
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
engine,
|
||||||
|
flows: HashMap::new(),
|
||||||
|
rule_engine,
|
||||||
|
pending,
|
||||||
|
current_sport,
|
||||||
|
current_dport,
|
||||||
|
current_proto,
|
||||||
|
flow_sid_counts: HashMap::new(),
|
||||||
|
min_matches,
|
||||||
|
is_ingress,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Feed a raw Ethernet frame. Returns alerts that have crossed min_matches threshold.
|
||||||
|
pub fn process(&mut self, packet_data: &[u8]) -> Result<Vec<RuleMatch>, RuleError> {
|
||||||
|
let Some(pkt) = parse_raw(packet_data) else {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Skip multicast and broadcast destinations.
|
||||||
|
// C2 malware never communicates via multicast; scanning these produces
|
||||||
|
// only false positives (SSDP, mDNS, LLMNR, OSPF, PIM, etc.).
|
||||||
|
if is_multicast_or_broadcast(&pkt.dip) {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let src = format!("{}:{}", pkt.sip, pkt.sport);
|
||||||
|
let dst = format!("{}:{}", pkt.dip, pkt.dport);
|
||||||
|
let flow_key = FlowKey {
|
||||||
|
sip: pkt.sip,
|
||||||
|
sport: pkt.sport,
|
||||||
|
dip: pkt.dip,
|
||||||
|
dport: pkt.dport,
|
||||||
|
};
|
||||||
|
|
||||||
|
let raw_matches = if matches!(pkt.trans_proto, TransProto::Udp) {
|
||||||
|
let parsed = app_layer::detect_and_parse(&pkt.payload, pkt.sport, pkt.dport);
|
||||||
|
self.rule_engine.scan(&parsed, self.is_ingress, pkt.dport)
|
||||||
|
} else {
|
||||||
|
self.process_tcp(pkt, &flow_key)?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(self.apply_threshold(raw_matches, &flow_key, src, dst))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_tcp(&mut self, pkt: RawPkt, key: &FlowKey) -> Result<Vec<RuleMatch>, RuleError> {
|
||||||
|
let rev = key.reversed();
|
||||||
|
let is_rst = pkt.rst;
|
||||||
|
let is_fin = pkt.fin;
|
||||||
|
|
||||||
|
let actual_key = if self.flows.contains_key(key) {
|
||||||
|
key.clone()
|
||||||
|
} else if self.flows.contains_key(&rev) {
|
||||||
|
rev
|
||||||
|
} else {
|
||||||
|
let task = self.engine.new_task(TransProto::Tcp);
|
||||||
|
self.flows.insert(
|
||||||
|
key.clone(),
|
||||||
|
FlowState {
|
||||||
|
task,
|
||||||
|
last_seen: Instant::now(),
|
||||||
|
fin_count: 0,
|
||||||
|
rst_seen: false,
|
||||||
|
detected_proto: FlowProto::Unknown,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
key.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(state) = self.flows.get_mut(&actual_key) else {
|
||||||
|
return Err(RuleError::FlowStateMissing);
|
||||||
|
};
|
||||||
|
state.last_seen = Instant::now();
|
||||||
|
if is_rst {
|
||||||
|
state.rst_seen = true;
|
||||||
|
}
|
||||||
|
if is_fin {
|
||||||
|
state.fin_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore per-flow protocol state so callbacks can fast-path TLS flows.
|
||||||
|
*self.current_sport.borrow_mut() = actual_key.sport;
|
||||||
|
*self.current_dport.borrow_mut() = actual_key.dport;
|
||||||
|
*self.current_proto.borrow_mut() = state.detected_proto as u8;
|
||||||
|
|
||||||
|
self.engine.run_task(&mut state.task, pkt);
|
||||||
|
|
||||||
|
// Persist any protocol the callbacks just identified back into FlowState.
|
||||||
|
let proto_byte = *self.current_proto.borrow();
|
||||||
|
if let Some(s) = self.flows.get_mut(&actual_key) {
|
||||||
|
if s.detected_proto == FlowProto::Unknown && proto_byte != 0 {
|
||||||
|
s.detected_proto = if proto_byte == FlowProto::Tls as u8 {
|
||||||
|
FlowProto::Tls
|
||||||
|
} else {
|
||||||
|
FlowProto::Unknown
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let matches: Vec<RuleMatch> = self.pending.borrow_mut().drain(..).collect();
|
||||||
|
|
||||||
|
if self
|
||||||
|
.flows
|
||||||
|
.get(&actual_key)
|
||||||
|
.map(|s| s.rst_seen || s.fin_count >= 2)
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
self.flows.remove(&actual_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(matches)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_threshold(&mut self, raw: Vec<RuleMatch>, flow_key: &FlowKey, src: String, dst: String) -> Vec<RuleMatch> {
|
||||||
|
let threshold = self.min_matches.max(1);
|
||||||
|
let mut alerts = Vec::new();
|
||||||
|
for mut m in raw {
|
||||||
|
let count = self.flow_sid_counts.entry((flow_key.clone(), m.sid)).or_insert(0);
|
||||||
|
*count += 1;
|
||||||
|
if *count == threshold {
|
||||||
|
m.src = src.clone();
|
||||||
|
m.dst = dst.clone();
|
||||||
|
alerts.push(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
alerts
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drop flows idle longer than `max_idle`. Call periodically from the XSK thread.
|
||||||
|
pub fn cleanup_stale(&mut self, max_idle: Duration) {
|
||||||
|
let now = Instant::now();
|
||||||
|
self.flows.retain(|_, s| now.duration_since(s.last_seen) < max_idle);
|
||||||
|
self.flow_sid_counts.retain(|(k, _), _| self.flows.contains_key(k));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flow_count(&self) -> usize {
|
||||||
|
self.flows.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Match counts per (flow, sid) — for periodic stats logging.
|
||||||
|
pub fn match_counts(&self) -> Vec<u32> {
|
||||||
|
self.flow_sid_counts.values().cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn min_matches(&self) -> u32 {
|
||||||
|
self.min_matches
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raw frame parser
|
||||||
|
|
||||||
|
fn parse_raw(data: &[u8]) -> Option<RawPkt> {
|
||||||
|
if data.len() < 14 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match u16::from_be_bytes([data[12], data[13]]) {
|
||||||
|
0x0800 => parse_ipv4(data),
|
||||||
|
0x86DD => parse_ipv6(data),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_ipv4(data: &[u8]) -> Option<RawPkt> {
|
||||||
|
if data.len() < 34 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let ip = &data[14..];
|
||||||
|
let proto = ip[9];
|
||||||
|
if proto != 6 && proto != 17 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sip = IpAddr::V4(Ipv4Addr::from(u32::from_be_bytes([ip[12], ip[13], ip[14], ip[15]])));
|
||||||
|
let dip = IpAddr::V4(Ipv4Addr::from(u32::from_be_bytes([ip[16], ip[17], ip[18], ip[19]])));
|
||||||
|
let ihl = (ip[0] & 0x0F) as usize * 4;
|
||||||
|
let total_len = u16::from_be_bytes([ip[2], ip[3]]) as usize;
|
||||||
|
|
||||||
|
if data.len() < 14 + ihl + 4 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let tp = &ip[ihl..];
|
||||||
|
let sport = u16::from_be_bytes([tp[0], tp[1]]);
|
||||||
|
let dport = u16::from_be_bytes([tp[2], tp[3]]);
|
||||||
|
|
||||||
|
if proto == 6 {
|
||||||
|
if data.len() < 14 + ihl + 20 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let seq = u32::from_be_bytes([tp[4], tp[5], tp[6], tp[7]]);
|
||||||
|
let hlen = (tp[12] >> 4) as usize * 4;
|
||||||
|
let flags = tp[13];
|
||||||
|
let payload_start = 14 + ihl + hlen;
|
||||||
|
let payload_end = (14 + total_len).min(data.len());
|
||||||
|
Some(RawPkt {
|
||||||
|
trans_proto: TransProto::Tcp,
|
||||||
|
sip,
|
||||||
|
dip,
|
||||||
|
sport,
|
||||||
|
dport,
|
||||||
|
seq,
|
||||||
|
syn: flags & 0x02 != 0,
|
||||||
|
fin: flags & 0x01 != 0,
|
||||||
|
rst: flags & 0x04 != 0,
|
||||||
|
payload: data.get(payload_start..payload_end).unwrap_or(&[]).to_vec(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let payload_start = 14 + ihl + 8;
|
||||||
|
let payload_end = (14 + total_len).min(data.len());
|
||||||
|
Some(RawPkt {
|
||||||
|
trans_proto: TransProto::Udp,
|
||||||
|
sip,
|
||||||
|
dip,
|
||||||
|
sport,
|
||||||
|
dport,
|
||||||
|
seq: 0,
|
||||||
|
syn: false,
|
||||||
|
fin: false,
|
||||||
|
rst: false,
|
||||||
|
payload: data.get(payload_start..payload_end).unwrap_or(&[]).to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_ipv6(data: &[u8]) -> Option<RawPkt> {
|
||||||
|
if data.len() < 54 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let ip = &data[14..];
|
||||||
|
let proto = ip[6];
|
||||||
|
if proto != 6 && proto != 17 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut s = [0u8; 16];
|
||||||
|
s.copy_from_slice(&ip[8..24]);
|
||||||
|
let mut d = [0u8; 16];
|
||||||
|
d.copy_from_slice(&ip[24..40]);
|
||||||
|
let sip = IpAddr::V6(Ipv6Addr::from(s));
|
||||||
|
let dip = IpAddr::V6(Ipv6Addr::from(d));
|
||||||
|
|
||||||
|
let payload_len = u16::from_be_bytes([ip[4], ip[5]]) as usize;
|
||||||
|
let tp = &ip[40..];
|
||||||
|
let sport = u16::from_be_bytes([tp[0], tp[1]]);
|
||||||
|
let dport = u16::from_be_bytes([tp[2], tp[3]]);
|
||||||
|
|
||||||
|
if proto == 6 {
|
||||||
|
if data.len() < 54 + 20 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let seq = u32::from_be_bytes([tp[4], tp[5], tp[6], tp[7]]);
|
||||||
|
let hlen = (tp[12] >> 4) as usize * 4;
|
||||||
|
let flags = tp[13];
|
||||||
|
let payload_start = 14 + 40 + hlen;
|
||||||
|
let payload_end = (14 + 40 + payload_len).min(data.len());
|
||||||
|
Some(RawPkt {
|
||||||
|
trans_proto: TransProto::Tcp,
|
||||||
|
sip,
|
||||||
|
dip,
|
||||||
|
sport,
|
||||||
|
dport,
|
||||||
|
seq,
|
||||||
|
syn: flags & 0x02 != 0,
|
||||||
|
fin: flags & 0x01 != 0,
|
||||||
|
rst: flags & 0x04 != 0,
|
||||||
|
payload: data.get(payload_start..payload_end).unwrap_or(&[]).to_vec(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let payload_start = 14 + 40 + 8;
|
||||||
|
let payload_end = (14 + 40 + payload_len).min(data.len());
|
||||||
|
Some(RawPkt {
|
||||||
|
trans_proto: TransProto::Udp,
|
||||||
|
sip,
|
||||||
|
dip,
|
||||||
|
sport,
|
||||||
|
dport,
|
||||||
|
seq: 0,
|
||||||
|
syn: false,
|
||||||
|
fin: false,
|
||||||
|
rst: false,
|
||||||
|
payload: data.get(payload_start..payload_end).unwrap_or(&[]).to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True for IPv4 multicast (224.0.0.0/4), limited broadcast (255.255.255.255),
|
||||||
|
/// and IPv6 multicast (ff00::/8).
|
||||||
|
/// These destinations are never used by unicast C2 malware; scanning them
|
||||||
|
/// generates only false positives from UPnP/SSDP, mDNS, LLMNR, OSPF, etc.
|
||||||
|
fn is_multicast_or_broadcast(ip: &IpAddr) -> bool {
|
||||||
|
match ip {
|
||||||
|
IpAddr::V4(v4) => v4.is_multicast() || v4.is_broadcast(),
|
||||||
|
IpAddr::V6(v6) => v6.is_multicast(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ mod core;
|
|||||||
mod model;
|
mod model;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod web;
|
mod web;
|
||||||
mod ml;
|
mod detection;
|
||||||
|
|
||||||
use crate::core::system::System;
|
use crate::core::system::System;
|
||||||
use crate::model::error::Error;
|
use crate::model::error::Error;
|
||||||
|
|||||||
@ -1,140 +0,0 @@
|
|||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use macros::log;
|
|
||||||
use tract_onnx::prelude::*;
|
|
||||||
|
|
||||||
use super::config_loader::InferenceConfig;
|
|
||||||
use super::feature_extractor::FlowFeatures;
|
|
||||||
use super::flow_tracker::FlowData;
|
|
||||||
use super::model_loader::MLModels;
|
|
||||||
use crate::model::log::ml::MLLog;
|
|
||||||
use crate::model::ml_detection::DetectionResult;
|
|
||||||
|
|
||||||
pub struct Inference {
|
|
||||||
pub models: Arc<MLModels>,
|
|
||||||
pub config: Arc<InferenceConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Inference {
|
|
||||||
pub fn new(models: Arc<MLModels>, config: Arc<InferenceConfig>) -> Self {
|
|
||||||
Self { models, config }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn infer_batch(&self, flows: &[FlowData]) -> Vec<DetectionResult> {
|
|
||||||
flows.iter().filter_map(|flow| self.infer_single(flow)).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn infer_single(&self, flow: &FlowData) -> Option<DetectionResult> {
|
|
||||||
// extract
|
|
||||||
let ae_features = self.preprocess_ae_features(flow);
|
|
||||||
|
|
||||||
// 2. Deep Autoencoder
|
|
||||||
let ae_input = Self::vec_to_array2(&ae_features);
|
|
||||||
let ae_score = match self.run_autoencoder(&ae_input) {
|
|
||||||
Ok(score) => score,
|
|
||||||
Err(e) => {
|
|
||||||
log!(MLLog::InferenceFailed("DeepAutoEncoder".to_string(), e.to_string()));
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let cls_input = self.build_classifier_input(&ae_features, ae_score);
|
|
||||||
|
|
||||||
let (attack_type, confidence) = match self.run_classifier(&cls_input) {
|
|
||||||
Ok(result) => result,
|
|
||||||
Err(e) => {
|
|
||||||
log!(MLLog::InferenceFailed("LightGBM".to_string(), e.to_string()));
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let is_attack = ae_score >= self.config.ae_threshold;
|
|
||||||
|
|
||||||
let flow_key = format!(
|
|
||||||
"{}:{} -> {}:{} (proto {}) [{}]",
|
|
||||||
flow.flow_key.src_ip,
|
|
||||||
flow.flow_key.src_port,
|
|
||||||
flow.flow_key.dst_ip,
|
|
||||||
flow.flow_key.dst_port,
|
|
||||||
flow.flow_key.protocol,
|
|
||||||
flow.direction
|
|
||||||
);
|
|
||||||
|
|
||||||
Some(DetectionResult {
|
|
||||||
flow_key,
|
|
||||||
flow_key_raw: flow.flow_key.clone(),
|
|
||||||
direction: flow.direction,
|
|
||||||
is_attack,
|
|
||||||
attack_type: if is_attack { Some(attack_type) } else { None },
|
|
||||||
confidence,
|
|
||||||
ae_score,
|
|
||||||
threshold: self.config.ae_threshold,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn preprocess_ae_features(&self, flow: &FlowData) -> Vec<f32> {
|
|
||||||
let mut features = FlowFeatures::extract(flow, &self.config.ae_feature_names);
|
|
||||||
features.winsorize(&self.config.ae_clip_params, &self.config.ae_feature_names);
|
|
||||||
features.normalize(&self.config.ae_scaler_mean, &self.config.ae_scaler_std);
|
|
||||||
features.clip(self.config.ae_post_clip_min, self.config.ae_post_clip_max);
|
|
||||||
features.features.iter().map(|&x| x as f32).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn vec_to_array2(v: &[f32]) -> tract_ndarray::Array2<f32> {
|
|
||||||
tract_ndarray::Array2::from_shape_fn((1, v.len()), |(_, j)| v[j])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Classifier 輸入 = 已預處理的 ae_features ++ [ae_anomaly_score]
|
|
||||||
fn build_classifier_input(&self, ae_features: &[f32], ae_score: f32) -> tract_ndarray::Array2<f32> {
|
|
||||||
let n = ae_features.len() + 1;
|
|
||||||
tract_ndarray::Array2::from_shape_fn((1, n), |(_, j)| {
|
|
||||||
if j < ae_features.len() {
|
|
||||||
ae_features[j]
|
|
||||||
} else {
|
|
||||||
ae_score
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_autoencoder(&self, input: &tract_ndarray::Array2<f32>) -> TractResult<f32> {
|
|
||||||
let result = self
|
|
||||||
.models
|
|
||||||
.deep_autoencoder
|
|
||||||
.run(tvec![input.clone().into_tensor().into()])?;
|
|
||||||
|
|
||||||
let output = result[0]
|
|
||||||
.to_array_view::<f32>()?
|
|
||||||
.into_dimensionality::<tract_ndarray::Ix2>()?;
|
|
||||||
|
|
||||||
let diff = input - &output;
|
|
||||||
let mse = (&diff * &diff).sum() / self.config.ae_feature_names.len() as f32;
|
|
||||||
|
|
||||||
Ok(mse)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_classifier(&self, input: &tract_ndarray::Array2<f32>) -> TractResult<(String, f32)> {
|
|
||||||
let input_tensor = input.clone().into_tensor();
|
|
||||||
let result = self.models.classifier.run(tvec![input_tensor.into()])?;
|
|
||||||
|
|
||||||
let output = result[0].to_array_view::<f32>()?;
|
|
||||||
|
|
||||||
let mut max_prob: f32 = 0.0;
|
|
||||||
let mut predicted_class: usize = 0;
|
|
||||||
|
|
||||||
for (i, &prob) in output.iter().enumerate() {
|
|
||||||
if prob > max_prob {
|
|
||||||
max_prob = prob;
|
|
||||||
predicted_class = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let attack_type = self
|
|
||||||
.config
|
|
||||||
.attack_labels
|
|
||||||
.get(&predicted_class.to_string())
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| "UNKNOWN".to_string());
|
|
||||||
|
|
||||||
Ok((attack_type, max_prob))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
use tract_onnx::prelude::*;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use crate::core::infrastructure::app_config::AppConfig;
|
|
||||||
use crate::model::error::ml::MLError;
|
|
||||||
use crate::model::ml_detection::RunnableModel;
|
|
||||||
|
|
||||||
use super::config_loader::InferenceConfig;
|
|
||||||
|
|
||||||
pub struct MLModels {
|
|
||||||
pub deep_autoencoder: RunnableModel,
|
|
||||||
pub classifier: RunnableModel,
|
|
||||||
}
|
|
||||||
impl MLModels {
|
|
||||||
pub fn load_models(app_config: &Arc<AppConfig>, inference_config: &Arc<InferenceConfig>) -> Result<Self, MLError> {
|
|
||||||
Ok(Self {
|
|
||||||
deep_autoencoder: Self::loader(&app_config.deep_autoencoder_name, inference_config.num_ae_features())?,
|
|
||||||
classifier: Self::loader(&app_config.classifier_name, inference_config.num_classifier_features())?
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn loader(model: &str, features: usize) -> Result<RunnableModel, MLError> {
|
|
||||||
let model_path = PathBuf::from("models").join(model);
|
|
||||||
|
|
||||||
let mut model = onnx()
|
|
||||||
.model_for_path(&model_path)
|
|
||||||
.map_err(|_| {
|
|
||||||
MLError::ModelLoadFailed { path: model_path.clone() }
|
|
||||||
})?;
|
|
||||||
|
|
||||||
model.set_input_fact(0, f32::fact(&[1, features]).into())
|
|
||||||
.map_err(|_| {
|
|
||||||
MLError::ModelLoadFailed { path: model_path.clone() }
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let runnable_model = model
|
|
||||||
.into_optimized()
|
|
||||||
.map_err(|_| {
|
|
||||||
MLError::ModelLoadFailed { path: model_path.clone() }
|
|
||||||
})?
|
|
||||||
.into_runnable()
|
|
||||||
.map_err(|_| {
|
|
||||||
MLError::ModelLoadFailed { path: model_path }
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(runnable_model)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_model_info(&self, name: &str) -> String {
|
|
||||||
let model = match name {
|
|
||||||
"deep_autoencoder" => &self.deep_autoencoder,
|
|
||||||
"classifier" => &self.classifier,
|
|
||||||
_ => return "unknown model".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let inputs = model.model().inputs.len();
|
|
||||||
let outputs = model.model().outputs.len();
|
|
||||||
format!("{}: inputs: {}, outputs: {}", name, inputs, outputs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -12,7 +12,6 @@ pub struct Config {
|
|||||||
pub egress_ifname: String,
|
pub egress_ifname: String,
|
||||||
pub geoip_db_name: String,
|
pub geoip_db_name: String,
|
||||||
pub deep_autoencoder_name: String,
|
pub deep_autoencoder_name: String,
|
||||||
pub classifier_name: String,
|
|
||||||
pub models_config_name: String,
|
pub models_config_name: String,
|
||||||
pub combined_queue_count: u32,
|
pub combined_queue_count: u32,
|
||||||
pub channel_size: usize,
|
pub channel_size: usize,
|
||||||
@ -29,6 +28,8 @@ pub struct Config {
|
|||||||
pub inference_interval_secs: u64,
|
pub inference_interval_secs: u64,
|
||||||
pub aggregator_window_secs: u64,
|
pub aggregator_window_secs: u64,
|
||||||
pub inference_batch_size: usize,
|
pub inference_batch_size: usize,
|
||||||
|
pub flow_timeout_us: u64,
|
||||||
pub traffic_logging_mode: bool,
|
pub traffic_logging_mode: bool,
|
||||||
pub traffic_log_csv_path: String,
|
pub traffic_log_csv_path: String,
|
||||||
|
pub min_signature_matches: u32,
|
||||||
}
|
}
|
||||||
@ -25,7 +25,7 @@ impl std::fmt::Display for Direction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum FlowDirection {
|
pub enum FlowDirection {
|
||||||
Source,
|
Source,
|
||||||
|
|||||||
@ -64,6 +64,10 @@ traceable! {
|
|||||||
|
|
||||||
#[error("TX queue processing failed")]
|
#[error("TX queue processing failed")]
|
||||||
TXQueueError => tracing::Level::ERROR,
|
TXQueueError => tracing::Level::ERROR,
|
||||||
|
|
||||||
|
#[no_source]
|
||||||
|
#[error("Flow statistics map key missing for ({direction}, {flow_direction}, {time_type})")]
|
||||||
|
FlowMapKeyMissing { direction: String, flow_direction: String, time_type: String } => tracing::Level::ERROR,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,5 +19,13 @@ traceable! {
|
|||||||
#[no_source]
|
#[no_source]
|
||||||
#[error("Failed to parse inference configuration: {reason}")]
|
#[error("Failed to parse inference configuration: {reason}")]
|
||||||
ConfigParseFailed { reason: String } => tracing::Level::ERROR,
|
ConfigParseFailed { reason: String } => tracing::Level::ERROR,
|
||||||
|
|
||||||
|
#[no_source]
|
||||||
|
#[error("Tracker mutex poisoned; skipping operation")]
|
||||||
|
TrackerLockPoisoned => tracing::Level::ERROR,
|
||||||
|
|
||||||
|
#[no_source]
|
||||||
|
#[error("Inference flow buffers mutex poisoned; skipping operation")]
|
||||||
|
InferenceLockPoisoned => tracing::Level::ERROR,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4,6 +4,7 @@ pub mod io;
|
|||||||
pub mod misc;
|
pub mod misc;
|
||||||
pub mod ml;
|
pub mod ml;
|
||||||
pub mod system;
|
pub mod system;
|
||||||
|
pub mod rule;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@ -12,6 +13,7 @@ use crate::model::error::http::HttpError;
|
|||||||
use crate::model::error::io::IOError;
|
use crate::model::error::io::IOError;
|
||||||
use crate::model::error::misc::MiscError;
|
use crate::model::error::misc::MiscError;
|
||||||
use crate::model::error::ml::MLError;
|
use crate::model::error::ml::MLError;
|
||||||
|
use crate::model::error::rule::RuleError;
|
||||||
use crate::model::error::system::SystemError;
|
use crate::model::error::system::SystemError;
|
||||||
|
|
||||||
#[derive(Clone, Debug, thiserror::Error, Serialize, Deserialize)]
|
#[derive(Clone, Debug, thiserror::Error, Serialize, Deserialize)]
|
||||||
@ -27,6 +29,8 @@ pub enum Error {
|
|||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Misc(MiscError),
|
Misc(MiscError),
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
|
Rule(RuleError),
|
||||||
|
#[error("{0}")]
|
||||||
System(SystemError),
|
System(SystemError),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,4 +68,10 @@ impl From<MLError> for Error {
|
|||||||
fn from(error: MLError) -> Self {
|
fn from(error: MLError) -> Self {
|
||||||
Self::ML(error)
|
Self::ML(error)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RuleError> for Error {
|
||||||
|
fn from(error: RuleError) -> Self {
|
||||||
|
Self::Rule(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
13
net-guardia/src/model/error/rule.rs
Normal file
13
net-guardia/src/model/error/rule.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
use macros::traceable;
|
||||||
|
|
||||||
|
traceable! {
|
||||||
|
RuleError {
|
||||||
|
#[no_source]
|
||||||
|
#[error("BlockDatabase::new failed: {error}")]
|
||||||
|
BlockDatabaseFailed { error: String } => tracing::Level::ERROR,
|
||||||
|
|
||||||
|
#[no_source]
|
||||||
|
#[error("Flow state missing for TCP key; skipping packet processing")]
|
||||||
|
FlowStateMissing => tracing::Level::ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -39,11 +39,11 @@ loggable! {
|
|||||||
#[error("Queue pair {queue_id} started successfully")]
|
#[error("Queue pair {queue_id} started successfully")]
|
||||||
QueuePairStarted { queue_id: u32 } => tracing::Level::INFO,
|
QueuePairStarted { queue_id: u32 } => tracing::Level::INFO,
|
||||||
|
|
||||||
#[error("ML models loaded - {info}")]
|
#[error("ML artifacts loaded - {info}")]
|
||||||
ModelsLoaded { info: String } => tracing::Level::INFO,
|
ModelsLoaded { info: String } => tracing::Level::INFO,
|
||||||
|
|
||||||
#[error("Inference configuration loaded: {features} features, {attacks} attack types")]
|
#[error("Inference configuration loaded: {features} features")]
|
||||||
ConfigLoaded { features: usize, attacks: usize } => tracing::Level::INFO,
|
ConfigLoaded { features: usize } => tracing::Level::INFO,
|
||||||
|
|
||||||
#[error("Inference completed: {total_flows} flows ({anomaly} anomaly, {benign} benign) in {duration_ms}ms ({throughput:.1} flows/s)")]
|
#[error("Inference completed: {total_flows} flows ({anomaly} anomaly, {benign} benign) in {duration_ms}ms ({throughput:.1} flows/s)")]
|
||||||
InferenceCompleted { total_flows: usize, anomaly: usize, benign: usize, duration_ms: u32, throughput: f32 } => tracing::Level::INFO,
|
InferenceCompleted { total_flows: usize, anomaly: usize, benign: usize, duration_ms: u32, throughput: f32 } => tracing::Level::INFO,
|
||||||
@ -68,5 +68,6 @@ loggable! {
|
|||||||
|
|
||||||
#[error("Failed to parse packet (length: {len})")]
|
#[error("Failed to parse packet (length: {len})")]
|
||||||
ParsePacketFailed { len: usize } => tracing::Level::INFO,
|
ParsePacketFailed { len: usize } => tracing::Level::INFO,
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4,3 +4,4 @@ pub mod ml;
|
|||||||
pub mod system;
|
pub mod system;
|
||||||
pub mod misc;
|
pub mod misc;
|
||||||
pub mod health;
|
pub mod health;
|
||||||
|
pub mod rule;
|
||||||
|
|||||||
18
net-guardia/src/model/log/rule.rs
Normal file
18
net-guardia/src/model/log/rule.rs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
use macros::loggable;
|
||||||
|
use tracing;
|
||||||
|
|
||||||
|
loggable! {
|
||||||
|
RuleLog {
|
||||||
|
#[error("Signature rules loaded: {count} patterns")]
|
||||||
|
RulesLoaded { count: usize } => tracing::Level::INFO,
|
||||||
|
|
||||||
|
#[error("Signature rules disabled (no rules found in rules/)")]
|
||||||
|
RulesDisabled => tracing::Level::INFO,
|
||||||
|
|
||||||
|
#[error("Signature match [{direction}]: {src}->{dst} sid={sid} \"{msg}\"")]
|
||||||
|
SignatureMatch { direction: String, src: String, dst: String, sid: u32, msg: String } => tracing::Level::WARN,
|
||||||
|
|
||||||
|
#[error("Signature stats [{direction}]: active_flows={active_flows}, min_matches={min_matches}, match_counts: {counts}")]
|
||||||
|
SignatureStats { direction: String, active_flows: usize, min_matches: u32, counts: String } => tracing::Level::INFO,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,5 +26,6 @@ loggable! {
|
|||||||
|
|
||||||
#[error("Traffic logging mode enabled — writing packets to: {path}")]
|
#[error("Traffic logging mode enabled — writing packets to: {path}")]
|
||||||
TrafficLoggingEnabled { path: String } => tracing::Level::INFO,
|
TrafficLoggingEnabled { path: String } => tracing::Level::INFO,
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -12,26 +12,6 @@ pub struct ClipParams {
|
|||||||
pub lower: f64,
|
pub lower: f64,
|
||||||
pub upper: f64,
|
pub upper: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct AENormalization {
|
|
||||||
pub min: f64,
|
|
||||||
pub max: f64,
|
|
||||||
pub norm_max: f64,
|
|
||||||
pub mean: f64,
|
|
||||||
pub std: f64,
|
|
||||||
pub median: f64,
|
|
||||||
pub p90: f64,
|
|
||||||
pub p95: f64,
|
|
||||||
pub p99: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct PrecisionLevels {
|
|
||||||
pub threshold: f64,
|
|
||||||
pub precision: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct FlowKey {
|
pub struct FlowKey {
|
||||||
pub src_ip: String,
|
pub src_ip: String,
|
||||||
@ -141,3 +121,41 @@ impl InferenceStats {
|
|||||||
pub struct EngineStats {
|
pub struct EngineStats {
|
||||||
pub active_flows: usize,
|
pub active_flows: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct AlertMessage {
|
||||||
|
pub timestamp: u64,
|
||||||
|
pub flow_key: String,
|
||||||
|
pub src_ip: String,
|
||||||
|
pub dst_ip: String,
|
||||||
|
pub src_port: u16,
|
||||||
|
pub dst_port: u16,
|
||||||
|
pub protocol: u8,
|
||||||
|
pub is_attack: bool,
|
||||||
|
pub attack_type: Option<String>,
|
||||||
|
pub confidence: f32,
|
||||||
|
pub ae_score: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlertMessage {
|
||||||
|
pub fn from_detection_result(result: &DetectionResult) -> Self {
|
||||||
|
let timestamp = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.expect("system time is after UNIX_EPOCH")
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
timestamp,
|
||||||
|
flow_key: result.flow_key.clone(),
|
||||||
|
src_ip: result.flow_key_raw.src_ip.clone(),
|
||||||
|
dst_ip: result.flow_key_raw.dst_ip.clone(),
|
||||||
|
src_port: result.flow_key_raw.src_port,
|
||||||
|
dst_port: result.flow_key_raw.dst_port,
|
||||||
|
protocol: result.flow_key_raw.protocol,
|
||||||
|
is_attack: result.is_attack,
|
||||||
|
attack_type: result.attack_type.clone(),
|
||||||
|
confidence: result.confidence,
|
||||||
|
ae_score: result.ae_score,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -8,3 +8,4 @@ pub mod log;
|
|||||||
pub mod time_type;
|
pub mod time_type;
|
||||||
pub mod ml_detection;
|
pub mod ml_detection;
|
||||||
pub mod health;
|
pub mod health;
|
||||||
|
pub mod rule_detection;
|
||||||
|
|||||||
164
net-guardia/src/model/rule_detection.rs
Normal file
164
net-guardia/src/model/rule_detection.rs
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
use std::net::IpAddr;
|
||||||
|
use std::time::Instant;
|
||||||
|
use protolens::{Packet as ProtoPacket, Task, TransProto};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RuleMatch {
|
||||||
|
pub sid: u32,
|
||||||
|
pub msg: String,
|
||||||
|
pub src: String,
|
||||||
|
pub dst: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ContentEntry {
|
||||||
|
pub pattern: Vec<u8>,
|
||||||
|
pub nocase: bool,
|
||||||
|
pub negated: bool,
|
||||||
|
pub has_distance: bool,
|
||||||
|
pub has_within: bool,
|
||||||
|
pub has_offset: bool,
|
||||||
|
pub has_depth: bool,
|
||||||
|
pub distance: i32,
|
||||||
|
pub within: i32,
|
||||||
|
pub offset: u16,
|
||||||
|
pub depth: u16,
|
||||||
|
|
||||||
|
/// Target buffer ID (0 = raw; see buf module for named constants).
|
||||||
|
pub buffer: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Buffer IDs — must match the constants used in `build.rs` and `rule_engine.rs`.
|
||||||
|
/// 0 (Raw) has no named constant; it is the default when no sticky buffer is set.
|
||||||
|
pub mod buf {
|
||||||
|
pub const HTTP_URI: u8 = 1;
|
||||||
|
pub const HTTP_HEADER: u8 = 2;
|
||||||
|
pub const HTTP_CLIENT_BODY: u8 = 3;
|
||||||
|
pub const HTTP_SERVER_BODY: u8 = 4;
|
||||||
|
pub const HTTP_METHOD: u8 = 5;
|
||||||
|
pub const HTTP_USER_AGENT: u8 = 6;
|
||||||
|
pub const HTTP_HOST: u8 = 7;
|
||||||
|
pub const HTTP_STAT_CODE: u8 = 8;
|
||||||
|
pub const HTTP_COOKIE: u8 = 9;
|
||||||
|
pub const HTTP_RAW_URI: u8 = 10;
|
||||||
|
pub const TLS_SNI: u8 = 11;
|
||||||
|
pub const DNS_QUERY: u8 = 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All application-layer fields extracted from a single payload chunk.
|
||||||
|
/// Empty `Vec<u8>` means the field was not present / not parsed.
|
||||||
|
pub struct ParsedPacket {
|
||||||
|
pub raw: Vec<u8>,
|
||||||
|
/// True when the payload is TLS Application Data (encrypted).
|
||||||
|
pub tls_app_data: bool,
|
||||||
|
/// True when this payload was identified as TLS (any record type, or port-based fallback).
|
||||||
|
pub is_tls: bool,
|
||||||
|
|
||||||
|
pub http_uri: Vec<u8>,
|
||||||
|
pub http_raw_uri: Vec<u8>,
|
||||||
|
pub http_method: Vec<u8>,
|
||||||
|
pub http_host: Vec<u8>,
|
||||||
|
pub http_user_agent: Vec<u8>,
|
||||||
|
pub http_cookie: Vec<u8>,
|
||||||
|
pub http_header: Vec<u8>,
|
||||||
|
pub http_client_body: Vec<u8>,
|
||||||
|
|
||||||
|
pub http_stat_code: Vec<u8>,
|
||||||
|
pub http_server_body: Vec<u8>,
|
||||||
|
|
||||||
|
pub tls_sni: Vec<u8>,
|
||||||
|
|
||||||
|
pub dns_query: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParsedPacket {
|
||||||
|
/// Return the byte slice for the given buffer ID.
|
||||||
|
pub fn buffer(&self, id: u8) -> &[u8] {
|
||||||
|
match id {
|
||||||
|
buf::HTTP_URI => &self.http_uri,
|
||||||
|
buf::HTTP_HEADER => &self.http_header,
|
||||||
|
buf::HTTP_CLIENT_BODY => &self.http_client_body,
|
||||||
|
buf::HTTP_SERVER_BODY => &self.http_server_body,
|
||||||
|
buf::HTTP_METHOD => &self.http_method,
|
||||||
|
buf::HTTP_USER_AGENT => &self.http_user_agent,
|
||||||
|
buf::HTTP_HOST => &self.http_host,
|
||||||
|
buf::HTTP_STAT_CODE => &self.http_stat_code,
|
||||||
|
buf::HTTP_COOKIE => &self.http_cookie,
|
||||||
|
buf::HTTP_RAW_URI => &self.http_raw_uri,
|
||||||
|
buf::TLS_SNI => &self.tls_sni,
|
||||||
|
buf::DNS_QUERY => &self.dns_query,
|
||||||
|
_ => &self.raw,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RuleSig {
|
||||||
|
pub sid: u32,
|
||||||
|
/// 0=any, 1=to_server (ingress), 2=to_client (egress)
|
||||||
|
pub flow_dir: u8,
|
||||||
|
/// empty = any port
|
||||||
|
pub dports: Vec<u16>,
|
||||||
|
pub msg: String,
|
||||||
|
pub chain: Vec<ContentEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RawPkt {
|
||||||
|
pub trans_proto: TransProto,
|
||||||
|
pub sip: IpAddr,
|
||||||
|
pub dip: IpAddr,
|
||||||
|
pub sport: u16,
|
||||||
|
pub dport: u16,
|
||||||
|
pub seq: u32,
|
||||||
|
pub syn: bool,
|
||||||
|
pub fin: bool,
|
||||||
|
pub rst: bool,
|
||||||
|
pub payload: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProtoPacket for RawPkt {
|
||||||
|
fn trans_proto(&self) -> TransProto { self.trans_proto }
|
||||||
|
fn sip(&self) -> IpAddr { self.sip }
|
||||||
|
fn dip(&self) -> IpAddr { self.dip }
|
||||||
|
fn tu_sport(&self) -> u16 { self.sport }
|
||||||
|
fn tu_dport(&self) -> u16 { self.dport }
|
||||||
|
fn seq(&self) -> u32 { self.seq }
|
||||||
|
fn syn(&self) -> bool { self.syn }
|
||||||
|
fn fin(&self) -> bool { self.fin }
|
||||||
|
fn payload_len(&self) -> usize { self.payload.len() }
|
||||||
|
fn payload(&self) -> &[u8] { &self.payload }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Hash, Eq, PartialEq, Clone)]
|
||||||
|
pub struct FlowKey {
|
||||||
|
pub sip: IpAddr,
|
||||||
|
pub sport: u16,
|
||||||
|
pub dip: IpAddr,
|
||||||
|
pub dport: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlowKey {
|
||||||
|
pub fn reversed(&self) -> Self {
|
||||||
|
Self { sip: self.dip, sport: self.dport, dip: self.sip, dport: self.sport }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Application-layer protocol detected for a flow.
|
||||||
|
/// Mirrors Suricata's `flow->alproto` — once set, never cleared.
|
||||||
|
/// Only TLS is tracked for now; HTTP and DNS don't benefit from per-flow
|
||||||
|
/// skip logic because they're cheap to detect and should still be scanned.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum FlowProto {
|
||||||
|
#[default]
|
||||||
|
Unknown = 0,
|
||||||
|
Tls = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FlowState {
|
||||||
|
pub task: Task<RawPkt>,
|
||||||
|
pub last_seen: Instant,
|
||||||
|
pub fin_count: u8,
|
||||||
|
pub rst_seen: bool,
|
||||||
|
|
||||||
|
/// Protocol identified on the first parseable chunk of this flow.
|
||||||
|
pub detected_proto: FlowProto,
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum TimeType {
|
pub enum TimeType {
|
||||||
#[serde(rename = "1min")]
|
#[serde(rename = "1min")]
|
||||||
|
|||||||
@ -184,4 +184,4 @@ pub fn format_ipv6(addr: u128) -> String {
|
|||||||
bytes[14],
|
bytes[14],
|
||||||
bytes[15]
|
bytes[15]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use actix_web::{get, web, HttpRequest, HttpResponse, Responder, Scope};
|
use actix_web::{get, web, HttpRequest, HttpResponse, Responder, Scope};
|
||||||
|
use macros::log;
|
||||||
|
|
||||||
use crate::core::ebpf::statistics::Statistics;
|
use crate::core::ebpf::statistics::Statistics;
|
||||||
use crate::core::infrastructure::app_config::AppConfig;
|
use crate::core::infrastructure::app_config::AppConfig;
|
||||||
@ -22,10 +23,13 @@ async fn get_ipv4_flow(
|
|||||||
statistics: web::Data<Arc<Statistics>>,
|
statistics: web::Data<Arc<Statistics>>,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let (direction, flow_direction, time_type) = path.into_inner();
|
let (direction, flow_direction, time_type) = path.into_inner();
|
||||||
let flow_data = statistics
|
match statistics.get_ipv4_flow_data(direction, flow_direction, time_type).await {
|
||||||
.get_ipv4_flow_data(direction, flow_direction, time_type)
|
Ok(flow_data) => HttpResponse::Ok().json(web::Json(flow_data)),
|
||||||
.await;
|
Err(e) => {
|
||||||
HttpResponse::Ok().json(web::Json(flow_data))
|
log!(e);
|
||||||
|
HttpResponse::InternalServerError().finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/get/ipv6/{direction}/{flow_direction}/{time_type}")]
|
#[get("/get/ipv6/{direction}/{flow_direction}/{time_type}")]
|
||||||
@ -34,10 +38,13 @@ async fn get_ipv6_flow(
|
|||||||
statistics: web::Data<Arc<Statistics>>,
|
statistics: web::Data<Arc<Statistics>>,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let (direction, flow_direction, time_type) = path.into_inner();
|
let (direction, flow_direction, time_type) = path.into_inner();
|
||||||
let flow_data = statistics
|
match statistics.get_ipv6_flow_data(direction, flow_direction, time_type).await {
|
||||||
.get_ipv6_flow_data(direction, flow_direction, time_type)
|
Ok(flow_data) => HttpResponse::Ok().json(web::Json(flow_data)),
|
||||||
.await;
|
Err(e) => {
|
||||||
HttpResponse::Ok().json(web::Json(flow_data))
|
log!(e);
|
||||||
|
HttpResponse::InternalServerError().finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/websocket/ipv4/{direction}/{flow_direction}/{time_type}")]
|
#[get("/websocket/ipv4/{direction}/{flow_direction}/{time_type}")]
|
||||||
|
|||||||
@ -4,7 +4,8 @@ use futures_util::StreamExt;
|
|||||||
use macros::log;
|
use macros::log;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
use crate::core::infrastructure::ml_alert::{MLAlert, AlertMessage};
|
use crate::core::infrastructure::ml_alert::MLAlert;
|
||||||
|
use crate::model::ml_detection::AlertMessage;
|
||||||
use crate::model::error::http::HttpError;
|
use crate::model::error::http::HttpError;
|
||||||
use crate::model::error::misc::MiscError;
|
use crate::model::error::misc::MiscError;
|
||||||
use crate::model::log::http::HttpLog;
|
use crate::model::log::http::HttpLog;
|
||||||
|
|||||||
@ -158,9 +158,10 @@ async fn send_ipv4_flow_data(
|
|||||||
flow_direction: FlowDirection,
|
flow_direction: FlowDirection,
|
||||||
time_type: TimeType,
|
time_type: TimeType,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let flow_data = statistics
|
let flow_data = match statistics.get_ipv4_flow_data(direction, flow_direction, time_type).await {
|
||||||
.get_ipv4_flow_data(direction, flow_direction, time_type)
|
Ok(data) => data,
|
||||||
.await;
|
Err(e) => { log!(e); return false; }
|
||||||
|
};
|
||||||
match serde_json::to_string(&flow_data) {
|
match serde_json::to_string(&flow_data) {
|
||||||
Ok(json) => session.text(json).await.is_ok(),
|
Ok(json) => session.text(json).await.is_ok(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@ -177,9 +178,10 @@ async fn send_ipv6_flow_data(
|
|||||||
flow_direction: FlowDirection,
|
flow_direction: FlowDirection,
|
||||||
time_type: TimeType,
|
time_type: TimeType,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let flow_data = statistics
|
let flow_data = match statistics.get_ipv6_flow_data(direction, flow_direction, time_type).await {
|
||||||
.get_ipv6_flow_data(direction, flow_direction, time_type)
|
Ok(data) => data,
|
||||||
.await;
|
Err(e) => { log!(e); return false; }
|
||||||
|
};
|
||||||
match serde_json::to_string(&flow_data) {
|
match serde_json::to_string(&flow_data) {
|
||||||
Ok(json) => session.text(json).await.is_ok(),
|
Ok(json) => session.text(json).await.is_ok(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|||||||
BIN
net-guardia/static/artifacts/deep_autoencoder.onnx
Normal file
BIN
net-guardia/static/artifacts/deep_autoencoder.onnx
Normal file
Binary file not shown.
251
net-guardia/static/artifacts/full_config.json
Normal file
251
net-guardia/static/artifacts/full_config.json
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
{
|
||||||
|
"created_at": "2026-04-21T10:38:25.314054",
|
||||||
|
"framework": "PyTorch",
|
||||||
|
"model": {
|
||||||
|
"lstm_deep_autoencoder": {
|
||||||
|
"file": "deep_autoencoder.onnx",
|
||||||
|
"type": "LSTM Autoencoder",
|
||||||
|
"input_dim": 27,
|
||||||
|
"hidden_size": 128,
|
||||||
|
"num_layers": 2,
|
||||||
|
"encoding_dim": 32,
|
||||||
|
"window_size": 10,
|
||||||
|
"ae_feature_names": [
|
||||||
|
"flow_duration",
|
||||||
|
"fwd_packets",
|
||||||
|
"bwd_packets",
|
||||||
|
"fwd_bytes",
|
||||||
|
"bwd_bytes",
|
||||||
|
"flow_bytes_per_sec",
|
||||||
|
"flow_pkts_per_sec",
|
||||||
|
"fwd_win_bytes",
|
||||||
|
"bwd_win_bytes",
|
||||||
|
"fwd_pkt_len_mean",
|
||||||
|
"bwd_pkt_len_mean",
|
||||||
|
"fwd_iat_mean",
|
||||||
|
"bwd_iat_mean",
|
||||||
|
"flow_iat_mean",
|
||||||
|
"pkt_len_mean",
|
||||||
|
"dst_port",
|
||||||
|
"protocol",
|
||||||
|
"psh_flag_cnt",
|
||||||
|
"ack_flag_cnt",
|
||||||
|
"syn_flag_cnt",
|
||||||
|
"fin_flag_cnt",
|
||||||
|
"rst_flag_cnt",
|
||||||
|
"pkt_len_std",
|
||||||
|
"fwd_pkt_len_std",
|
||||||
|
"bwd_pkt_len_std",
|
||||||
|
"fwd_seg_size_min",
|
||||||
|
"fwd_act_data_pkts"
|
||||||
|
],
|
||||||
|
"ae_threshold": 0.04424155503511429,
|
||||||
|
"ae_threshold_method": "mean+1std"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preprocessing": {
|
||||||
|
"ae_clip_params": {
|
||||||
|
"flow_duration": {
|
||||||
|
"lower": 13203.665,
|
||||||
|
"upper": 90041362.345
|
||||||
|
},
|
||||||
|
"fwd_packets": {
|
||||||
|
"lower": 2.0,
|
||||||
|
"upper": 1000.0
|
||||||
|
},
|
||||||
|
"bwd_packets": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 1000.0
|
||||||
|
},
|
||||||
|
"fwd_bytes": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 12828460.719999954
|
||||||
|
},
|
||||||
|
"bwd_bytes": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 1782168.224999995
|
||||||
|
},
|
||||||
|
"flow_bytes_per_sec": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 3787763.16328191
|
||||||
|
},
|
||||||
|
"flow_pkts_per_sec": {
|
||||||
|
"lower": 0.066635115985,
|
||||||
|
"upper": 2235.532986379341
|
||||||
|
},
|
||||||
|
"fwd_win_bytes": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 65535.0
|
||||||
|
},
|
||||||
|
"bwd_win_bytes": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 65535.0
|
||||||
|
},
|
||||||
|
"fwd_pkt_len_mean": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 1448.0
|
||||||
|
},
|
||||||
|
"bwd_pkt_len_mean": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 1415.023504181184
|
||||||
|
},
|
||||||
|
"fwd_iat_mean": {
|
||||||
|
"lower": 205.2674874874875,
|
||||||
|
"upper": 45017828.5025
|
||||||
|
},
|
||||||
|
"bwd_iat_mean": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 45017992.675
|
||||||
|
},
|
||||||
|
"flow_iat_mean": {
|
||||||
|
"lower": 378.36644698085416,
|
||||||
|
"upper": 18008565.868
|
||||||
|
},
|
||||||
|
"pkt_len_mean": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 1140.389183169216
|
||||||
|
},
|
||||||
|
"dst_port": {
|
||||||
|
"lower": 53.0,
|
||||||
|
"upper": 63575.0
|
||||||
|
},
|
||||||
|
"protocol": {
|
||||||
|
"lower": 6.0,
|
||||||
|
"upper": 17.0
|
||||||
|
},
|
||||||
|
"psh_flag_cnt": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 3443.0
|
||||||
|
},
|
||||||
|
"ack_flag_cnt": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 12277.0
|
||||||
|
},
|
||||||
|
"syn_flag_cnt": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 3.0
|
||||||
|
},
|
||||||
|
"fin_flag_cnt": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 2.0
|
||||||
|
},
|
||||||
|
"rst_flag_cnt": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 3.0
|
||||||
|
},
|
||||||
|
"pkt_len_std": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 724.1810679032936
|
||||||
|
},
|
||||||
|
"fwd_pkt_len_std": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 677.6756844784483
|
||||||
|
},
|
||||||
|
"bwd_pkt_len_std": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 723.5706185235359
|
||||||
|
},
|
||||||
|
"fwd_seg_size_min": {
|
||||||
|
"lower": 8.0,
|
||||||
|
"upper": 32.0
|
||||||
|
},
|
||||||
|
"fwd_act_data_pkts": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 8950.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ae_scaler": {
|
||||||
|
"mean": [
|
||||||
|
5951288.750077261,
|
||||||
|
59.11059332512558,
|
||||||
|
64.43133138509893,
|
||||||
|
222150.9194105303,
|
||||||
|
43569.19774830678,
|
||||||
|
172594.2514672874,
|
||||||
|
144.20395486021135,
|
||||||
|
17024.939887041404,
|
||||||
|
16926.870109581538,
|
||||||
|
178.341350807249,
|
||||||
|
271.73465038994414,
|
||||||
|
1866741.291807338,
|
||||||
|
1837758.5476123434,
|
||||||
|
887050.3497480378,
|
||||||
|
275.91961765390863,
|
||||||
|
15713.397431840604,
|
||||||
|
12.181819994841344,
|
||||||
|
60.86333020445825,
|
||||||
|
263.60134098445224,
|
||||||
|
0.5260804590671343,
|
||||||
|
0.45693985380264684,
|
||||||
|
0.2458391722751531,
|
||||||
|
232.14679728383396,
|
||||||
|
96.51238144611007,
|
||||||
|
156.93610306173713,
|
||||||
|
15.867236423296912,
|
||||||
|
162.04478590251205
|
||||||
|
],
|
||||||
|
"std": [
|
||||||
|
13021032.695784755,
|
||||||
|
189.6836628295549,
|
||||||
|
186.13916771977108,
|
||||||
|
1366100.0137844363,
|
||||||
|
175343.2394873705,
|
||||||
|
495919.246600428,
|
||||||
|
332.6340993004248,
|
||||||
|
28139.884220659886,
|
||||||
|
27367.235623472247,
|
||||||
|
346.83550077192035,
|
||||||
|
422.80683501529063,
|
||||||
|
5937390.371629928,
|
||||||
|
6302828.0876488965,
|
||||||
|
2607765.266228357,
|
||||||
|
345.84585519370796,
|
||||||
|
19782.572561725658,
|
||||||
|
5.457574689789829,
|
||||||
|
358.5939678553331,
|
||||||
|
1365.024880946303,
|
||||||
|
0.8897314831847252,
|
||||||
|
0.8326227579585829,
|
||||||
|
0.6757562381733568,
|
||||||
|
261.91355522213735,
|
||||||
|
145.97497775092612,
|
||||||
|
209.49550260032348,
|
||||||
|
9.755976125742986,
|
||||||
|
953.2974129459069
|
||||||
|
],
|
||||||
|
"feature_names": [
|
||||||
|
"flow_duration",
|
||||||
|
"fwd_packets",
|
||||||
|
"bwd_packets",
|
||||||
|
"fwd_bytes",
|
||||||
|
"bwd_bytes",
|
||||||
|
"flow_bytes_per_sec",
|
||||||
|
"flow_pkts_per_sec",
|
||||||
|
"fwd_win_bytes",
|
||||||
|
"bwd_win_bytes",
|
||||||
|
"fwd_pkt_len_mean",
|
||||||
|
"bwd_pkt_len_mean",
|
||||||
|
"fwd_iat_mean",
|
||||||
|
"bwd_iat_mean",
|
||||||
|
"flow_iat_mean",
|
||||||
|
"pkt_len_mean",
|
||||||
|
"dst_port",
|
||||||
|
"protocol",
|
||||||
|
"psh_flag_cnt",
|
||||||
|
"ack_flag_cnt",
|
||||||
|
"syn_flag_cnt",
|
||||||
|
"fin_flag_cnt",
|
||||||
|
"rst_flag_cnt",
|
||||||
|
"pkt_len_std",
|
||||||
|
"fwd_pkt_len_std",
|
||||||
|
"bwd_pkt_len_std",
|
||||||
|
"fwd_seg_size_min",
|
||||||
|
"fwd_act_data_pkts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"post_scaling_clip": {
|
||||||
|
"min": -5.0,
|
||||||
|
"max": 5.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
204
net-guardia/static/artifacts/inference_config.json
Normal file
204
net-guardia/static/artifacts/inference_config.json
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
{
|
||||||
|
"ae_feature_names": [
|
||||||
|
"flow_duration",
|
||||||
|
"fwd_packets",
|
||||||
|
"bwd_packets",
|
||||||
|
"fwd_bytes",
|
||||||
|
"bwd_bytes",
|
||||||
|
"flow_bytes_per_sec",
|
||||||
|
"flow_pkts_per_sec",
|
||||||
|
"fwd_win_bytes",
|
||||||
|
"bwd_win_bytes",
|
||||||
|
"fwd_pkt_len_mean",
|
||||||
|
"bwd_pkt_len_mean",
|
||||||
|
"fwd_iat_mean",
|
||||||
|
"bwd_iat_mean",
|
||||||
|
"flow_iat_mean",
|
||||||
|
"pkt_len_mean",
|
||||||
|
"dst_port",
|
||||||
|
"protocol",
|
||||||
|
"psh_flag_cnt",
|
||||||
|
"ack_flag_cnt",
|
||||||
|
"syn_flag_cnt",
|
||||||
|
"fin_flag_cnt",
|
||||||
|
"rst_flag_cnt",
|
||||||
|
"pkt_len_std",
|
||||||
|
"fwd_pkt_len_std",
|
||||||
|
"bwd_pkt_len_std",
|
||||||
|
"fwd_seg_size_min",
|
||||||
|
"fwd_act_data_pkts"
|
||||||
|
],
|
||||||
|
"ae_clip_params": {
|
||||||
|
"flow_duration": {
|
||||||
|
"lower": 13203.665,
|
||||||
|
"upper": 90041362.345
|
||||||
|
},
|
||||||
|
"fwd_packets": {
|
||||||
|
"lower": 2.0,
|
||||||
|
"upper": 1000.0
|
||||||
|
},
|
||||||
|
"bwd_packets": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 1000.0
|
||||||
|
},
|
||||||
|
"fwd_bytes": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 12828460.719999954
|
||||||
|
},
|
||||||
|
"bwd_bytes": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 1782168.224999995
|
||||||
|
},
|
||||||
|
"flow_bytes_per_sec": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 3787763.16328191
|
||||||
|
},
|
||||||
|
"flow_pkts_per_sec": {
|
||||||
|
"lower": 0.066635115985,
|
||||||
|
"upper": 2235.532986379341
|
||||||
|
},
|
||||||
|
"fwd_win_bytes": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 65535.0
|
||||||
|
},
|
||||||
|
"bwd_win_bytes": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 65535.0
|
||||||
|
},
|
||||||
|
"fwd_pkt_len_mean": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 1448.0
|
||||||
|
},
|
||||||
|
"bwd_pkt_len_mean": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 1415.023504181184
|
||||||
|
},
|
||||||
|
"fwd_iat_mean": {
|
||||||
|
"lower": 205.2674874874875,
|
||||||
|
"upper": 45017828.5025
|
||||||
|
},
|
||||||
|
"bwd_iat_mean": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 45017992.675
|
||||||
|
},
|
||||||
|
"flow_iat_mean": {
|
||||||
|
"lower": 378.36644698085416,
|
||||||
|
"upper": 18008565.868
|
||||||
|
},
|
||||||
|
"pkt_len_mean": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 1140.389183169216
|
||||||
|
},
|
||||||
|
"dst_port": {
|
||||||
|
"lower": 53.0,
|
||||||
|
"upper": 63575.0
|
||||||
|
},
|
||||||
|
"protocol": {
|
||||||
|
"lower": 6.0,
|
||||||
|
"upper": 17.0
|
||||||
|
},
|
||||||
|
"psh_flag_cnt": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 3443.0
|
||||||
|
},
|
||||||
|
"ack_flag_cnt": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 12277.0
|
||||||
|
},
|
||||||
|
"syn_flag_cnt": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 3.0
|
||||||
|
},
|
||||||
|
"fin_flag_cnt": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 2.0
|
||||||
|
},
|
||||||
|
"rst_flag_cnt": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 3.0
|
||||||
|
},
|
||||||
|
"pkt_len_std": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 724.1810679032936
|
||||||
|
},
|
||||||
|
"fwd_pkt_len_std": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 677.6756844784483
|
||||||
|
},
|
||||||
|
"bwd_pkt_len_std": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 723.5706185235359
|
||||||
|
},
|
||||||
|
"fwd_seg_size_min": {
|
||||||
|
"lower": 8.0,
|
||||||
|
"upper": 32.0
|
||||||
|
},
|
||||||
|
"fwd_act_data_pkts": {
|
||||||
|
"lower": 0.0,
|
||||||
|
"upper": 8950.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ae_scaler_mean": [
|
||||||
|
5951288.750077261,
|
||||||
|
59.11059332512558,
|
||||||
|
64.43133138509893,
|
||||||
|
222150.9194105303,
|
||||||
|
43569.19774830678,
|
||||||
|
172594.2514672874,
|
||||||
|
144.20395486021135,
|
||||||
|
17024.939887041404,
|
||||||
|
16926.870109581538,
|
||||||
|
178.341350807249,
|
||||||
|
271.73465038994414,
|
||||||
|
1866741.291807338,
|
||||||
|
1837758.5476123434,
|
||||||
|
887050.3497480378,
|
||||||
|
275.91961765390863,
|
||||||
|
15713.397431840604,
|
||||||
|
12.181819994841344,
|
||||||
|
60.86333020445825,
|
||||||
|
263.60134098445224,
|
||||||
|
0.5260804590671343,
|
||||||
|
0.45693985380264684,
|
||||||
|
0.2458391722751531,
|
||||||
|
232.14679728383396,
|
||||||
|
96.51238144611007,
|
||||||
|
156.93610306173713,
|
||||||
|
15.867236423296912,
|
||||||
|
162.04478590251205
|
||||||
|
],
|
||||||
|
"ae_scaler_std": [
|
||||||
|
13021032.695784755,
|
||||||
|
189.6836628295549,
|
||||||
|
186.13916771977108,
|
||||||
|
1366100.0137844363,
|
||||||
|
175343.2394873705,
|
||||||
|
495919.246600428,
|
||||||
|
332.6340993004248,
|
||||||
|
28139.884220659886,
|
||||||
|
27367.235623472247,
|
||||||
|
346.83550077192035,
|
||||||
|
422.80683501529063,
|
||||||
|
5937390.371629928,
|
||||||
|
6302828.0876488965,
|
||||||
|
2607765.266228357,
|
||||||
|
345.84585519370796,
|
||||||
|
19782.572561725658,
|
||||||
|
5.457574689789829,
|
||||||
|
358.5939678553331,
|
||||||
|
1365.024880946303,
|
||||||
|
0.8897314831847252,
|
||||||
|
0.8326227579585829,
|
||||||
|
0.6757562381733568,
|
||||||
|
261.91355522213735,
|
||||||
|
145.97497775092612,
|
||||||
|
209.49550260032348,
|
||||||
|
9.755976125742986,
|
||||||
|
953.2974129459069
|
||||||
|
],
|
||||||
|
"ae_post_clip_min": -5.0,
|
||||||
|
"ae_post_clip_max": 5.0,
|
||||||
|
"ae_threshold": 0.04424155503511429,
|
||||||
|
"ae_threshold_method": "mean+1std",
|
||||||
|
"window_size": 10
|
||||||
|
}
|
||||||
1
net-guardia/static/artifacts/info.txt
Normal file
1
net-guardia/static/artifacts/info.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
// this's ML model folder, so you can use Trainer output model
|
||||||
2
net-guardia/static/geo/info.txt
Normal file
2
net-guardia/static/geo/info.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// this's GeoLite folder, so you can download the open rule from https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-City.mmdb or https://git.io/GeoLite2-City.mmdb
|
||||||
|
// you can access to https://github.com/P3TERX/GeoLite.mmdb for origin repo
|
||||||
1
net-guardia/static/rules/info.txt
Normal file
1
net-guardia/static/rules/info.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
// this's open rule folder, so you can download the open rule from https://rules.emergingthreats.net/open/suricata-8.0/emerging.rules.tar.gz
|
||||||
Loading…
x
Reference in New Issue
Block a user