From 6746182f1815d3d4bb1632766efb0384d344590a Mon Sep 17 00:00:00 2001 From: Hound Date: Thu, 20 Aug 2026 22:05:09 -0500 Subject: [PATCH] houndd: replace the clamscan fork with yara-x in process MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old engine shelled out to clamscan for every scan, and clamscan reloads a 169 MB signature database on every invocation. Measured on a 68-byte EICAR file: 6.5 seconds and ~1.5 GB RSS — paid once per file, and realtime.rs called it once per inotify event. Replaces it with HoundEngine: yara-x compiled once at daemon start, held in memory, one scanner reused across a whole walk, plus a verdict cache keyed on (dev, ino, mtime, size) so an unchanged file that has been seen before never reaches the matcher. Measured after, same machine, same EICAR file: single file 6.5 s -> 4 ms 400 files cold -- -> 9 ms 400 files warm -- -> 5 ms Also here: - rules.rs: hot-swappable rule store. Built-in pack is embedded so a fresh install detects something before it has ever reached the network; on-disk packs load from $HOUNDD_RULES_DIR, /var/lib/hound or the XDG data dir. Reload swaps an Arc, so in-flight scans are never torn out from under. - cache.rs: bounded FIFO verdict cache. Any of the four key fields changing means rescan, so edits, truncates and replace-by-rename all correctly miss. - The goodware gate: every rule is scanned against all of /usr/bin, /bin and /usr/sbin in CI, and a single hit fails the build. It has already earned its keep — it caught a reverse-shell rule that matched /usr/bin/sudo, which is now removed rather than tuned. A rule that quarantines sudo is worse than no rule at all. - ScanEngine is Send + Sync and selection stays per-call, so HOUNDD_ENGINE=clamav still reaches the legacy path for comparison. - ScanResult.skipped reports files passed over for size instead of quietly counting them as clean. - Settings gain theme (auto/light/dark), tray_icon_style (color/mono), close_to_tray and confirm_quit, normalised daemon-side because clients are not trusted to send a theme we can render. 57 tests pass, up from 29. Co-Authored-By: Claude Opus 5 --- Cargo.lock | 2465 ++++++++++++++++++++++++- Cargo.toml | 1 + crates/hound-api/src/lib.rs | 65 + crates/houndd/Cargo.toml | 1 + crates/houndd/rules/hound-builtin.yar | 97 + crates/houndd/src/cache.rs | 209 +++ crates/houndd/src/engine.rs | 61 +- crates/houndd/src/main.rs | 7 +- crates/houndd/src/native.rs | 462 +++++ crates/houndd/src/rules.rs | 272 +++ crates/houndd/src/settings.rs | 53 + 11 files changed, 3677 insertions(+), 16 deletions(-) create mode 100644 crates/houndd/rules/hound-builtin.yar create mode 100644 crates/houndd/src/cache.rs create mode 100644 crates/houndd/src/native.rs create mode 100644 crates/houndd/src/rules.rs diff --git a/Cargo.lock b/Cargo.lock index a18aac3..5c1550f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,56 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli 0.33.0", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "annotate-snippets" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + [[package]] name = "anstream" version = "1.0.0" @@ -58,12 +108,204 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "ascii_tree" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6c635b3aa665c649ad1415f1573c85957dfa47690ec27aebe7ec17efe3c643" + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 2.0.20", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "serde", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "cc" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clap" version = "4.6.6" @@ -95,7 +337,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -104,6 +346,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.20", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -120,6 +371,302 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc822414b18d1f5b1b33ce1441534e311e62fef86ebb5b9d382af857d0272c9" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c646808b06f4532478d8d6057d74f15c3322f10d995d9486e7dcea405bf521a" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5996f01a686b2349cdb379083ec5ad3e8cb8767fb2d495d3a4f2ee4163a18d" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523fea83273f6a985520f57788809a4de2165794d9ab00fb1254fceb4f5aa00c" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d73d1e372730b5f64ed1a2bd9f01fe4686c8ec14a28034e3084e530c8d951878" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli 0.33.0", + "hashbrown 0.16.1", + "libm", + "log", + "pulley-interpreter", + "regalloc2", + "rustc-hash 2.1.3", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0319c18165e93dc1ebf78946a8da0b1c341c95b4a39729a69574671639bdb5f" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9195cd8aeecb55e401aa96b2eaa55921636e8246c127ed7908f7ef7e0d40f270" + +[[package]] +name = "cranelift-control" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8976c2154b74136322befc74222ab5c7249edd7e2604f8cbef2b94975541ffb9" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6038b3147c7982f4951150d5f96c7c06c1e7214b99d4b4a98607aadf8ded89d1" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbd294abe236e23cc3d907b0936226b6a8342db7636daa9c7c72be1e323420e" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a90b6ed3aba84189352a87badeb93b2126d3724225a42dc67fdce53d1b139c" + +[[package]] +name = "cranelift-native" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ec0cc1a54e22925eacf4fc3dc815f907734d3b377899d19d52bec04863e853" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "948865622f87f30907bb46fbb081b235ae63c1896a99a83c26a003305c1fa82d" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "daachorse" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5614204febbc33cc07a2806aa6440b904ac012b68eecc37f4493ea4a76455a3d" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.5.8" @@ -129,18 +676,350 @@ dependencies = [ "serde_core", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dsa" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48bc224a9084ad760195584ce5abb3c2c34a225fa312a128ad245a6b412b7689" +dependencies = [ + "digest", + "num-bigint-dig", + "num-traits", + "pkcs8", + "rfc6979", + "sha2", + "signature", + "zeroize", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures-core" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "globset" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags 2.13.1", + "ignore", + "walkdir", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "hound" version = "0.1.0" @@ -172,6 +1051,71 @@ dependencies = [ "serde", "serde_json", "time", + "yara-x", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "ignore" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", ] [[package]] @@ -180,7 +1124,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" dependencies = [ - "bitflags", + "bitflags 1.3.2", "futures-core", "inotify-sys", "libc", @@ -196,23 +1140,79 @@ dependencies = [ "libc", ] +[[package]] +name = "intaglio" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eca9188c1b20836bb561bc09bb2f54e9ca99b271031b5f3ff183a00c08c98c8" + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" @@ -220,12 +1220,132 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "logos" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff472f899b4ec2d99161c51f60ff7075eeb3097069a36050d8037a6325eb8154" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "192a3a2b90b0c05b27a0b2c43eecdb7c415e29243acc3f89cc8247a5b693045c" +dependencies = [ + "beef", + "fnv", + "lazy_static", + "proc-macro2", + "quote", + "regex-syntax", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "logos-derive" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "605d9697bcd5ef3a42d38efc51541aa3d6a4a25f7ab6d1ed0da5ac632a26b470" +dependencies = [ + "logos-codegen", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "md2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4f0f3ed25ff4f8d8d102288d92f900efc202661c884cf67dfe4f0d07c43d1f" +dependencies = [ + "digest", +] + [[package]] name = "memchr" version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix 1.1.4", +] + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.2" @@ -237,30 +1357,226 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" +dependencies = [ + "crc32fast", + "hashbrown 0.16.1", + "indexmap", + "memchr", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + [[package]] name = "once_cell_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -270,6 +1586,95 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-codegen" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-parse" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" +dependencies = [ + "anyhow", + "indexmap", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror 1.0.69", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "psl" +version = "2.1.226" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc88482eea924ca3a2f56a547454169af58deef35567965eb4fc2392a834841" +dependencies = [ + "psl-types", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "pulley-interpreter" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec12fe19a9588315a49fe5704502a9c02d6a198303314b0c7c86123b06d29e5" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f7d5ef31ebf1b46cd7e722ffef934e670d7e462f49aa01cde07b9b76dca580" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "quote" version = "1.0.47" @@ -279,6 +1684,232 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "regalloc2" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash 2.1.3", + "smallvec", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rowan" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "rustc-hash 1.1.0", + "text-size", +] + +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.229" @@ -306,7 +1937,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -315,6 +1946,7 @@ version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ + "indexmap", "itoa", "memchr", "serde", @@ -322,6 +1954,81 @@ dependencies = [ "zmij", ] +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + [[package]] name = "socket2" version = "0.6.5" @@ -332,12 +2039,63 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "3.0.3" @@ -349,6 +2107,97 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "time" version = "0.3.55" @@ -379,6 +2228,12 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinyzip" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6847a2bf223d67c916a25d5fb8d197fc1c7c3205421e05d50f3373e092034146" + [[package]] name = "tokio" version = "1.53.1" @@ -392,30 +2247,430 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "walrus" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b63a2bc6e4acb4cf69080068a531bad4530791098230e972ffcb64fd1dc266" +dependencies = [ + "anyhow", + "gimli 0.32.3", + "id-arena", + "leb128", + "log", + "walrus-macro", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "walrus-macro" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9b0525d7ea6e5f906aca581a172e5c91b4c595290dfa8ad4a2bc9ffef33b44" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" +dependencies = [ + "bitflags 2.13.1", + "hashbrown 0.16.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmprinter" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41517a3716fbb8ccf46daa9c1325f760fcbff5168e75c7392288e410b91ac8" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser", +] + +[[package]] +name = "wasmtime" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb1ed5899dde98357cfdcf647a4614498798719793898245b4b34e663addabf" +dependencies = [ + "addr2line", + "async-trait", + "bitflags 2.13.1", + "bumpalo", + "cc", + "cfg-if", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rustix 1.1.4", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasmparser", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-environ" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4172382dcc785c31d0e862c6780a18f5dd437914d22c4691351f965ef751c821" +dependencies = [ + "anyhow", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli 0.33.0", + "hashbrown 0.16.1", + "indexmap", + "log", + "object", + "postcard", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasm-encoder", + "wasmparser", + "wasmprinter", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-core" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3820b174f477d2a7083209d1ad5353fcdb11eaea434b2137b8681029460dd3" +dependencies = [ + "hashbrown 0.16.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1679d205caf9766c6aa309d45bb3e7c634d7725e3164404df33824b9f7c4fb7" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli 0.33.0", + "itertools", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.20", + "wasmparser", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e505254058be5b0df458d670ee42d9eafe2349d04c1296e9dc01071dc20a85" +dependencies = [ + "cc", + "cfg-if", + "libc", + "rustix 1.1.4", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c2e05b345f1773e59c20e6ad7298fd6857cdea245023d88bb659c96d8f0ea72" +dependencies = [ + "cc", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86701b234a4643e3f111869aa792b3a05a06e02d486ee9cb6c04dae16b52dab" +dependencies = [ + "cfg-if", + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63558d801beb83dde9b336eb4ae049019aee26627926edb32cd119d7e4c83cd" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "737c4d956fc3a848541a064afb683dd2771132a6b125be5baaf95c4379aa47df" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -498,8 +2753,212 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.20", + "time", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yara-x" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd44924487d4d3d0bc42683fbfa35036406483f7fc455d820fff8f2f6b3507bd" +dependencies = [ + "annotate-snippets", + "anyhow", + "base64", + "bincode", + "bitflags 2.13.1", + "bitvec", + "bstr", + "const-oid", + "crc32fast", + "daachorse", + "der-parser", + "digest", + "dsa", + "ecdsa", + "flate2", + "getrandom 0.2.17", + "globwalk", + "hex", + "indexmap", + "intaglio", + "inventory", + "ipnet", + "itertools", + "js-sys", + "md-5", + "md2", + "memchr", + "memmap2", + "nom 8.0.0", + "num-derive", + "num-traits", + "p256", + "p384", + "protobuf", + "protobuf-codegen", + "protobuf-parse", + "psl", + "regex", + "regex-automata", + "regex-syntax", + "roxmltree", + "rsa", + "rustc-hash 2.1.3", + "serde", + "serde_json", + "sha1", + "sha2", + "simd-adler32", + "simd_cesu8", + "smallvec", + "strum_macros", + "thiserror 2.0.20", + "tinyzip", + "uuid", + "walrus", + "wasm-bindgen", + "wasmtime", + "x509-parser", + "yara-x-macros", + "yara-x-parser", + "yara-x-proto", + "zip", +] + +[[package]] +name = "yara-x-macros" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77caafb677e73e130c13425863c49828c2206c29e8da6aa8fb08b89b3594bef" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "yara-x-parser" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5564d72253142774903eafc8f4c92bebef40d65c176a11d351258e80cc8a1c7" +dependencies = [ + "ascii_tree", + "bitflags 2.13.1", + "bstr", + "indexmap", + "itertools", + "logos", + "num-traits", + "rowan", + "rustc-hash 2.1.3", + "serde", +] + +[[package]] +name = "yara-x-proto" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01022f120b79da81aa097fa77d281b5edeebbdfef715b239e569abebe04a241d" +dependencies = [ + "base64", + "chrono", + "itertools", + "protobuf", + "protobuf-codegen", + "yansi", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zip" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +dependencies = [ + "crc32fast", + "flate2", + "indexmap", + "memchr", + "typed-path", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + [[package]] name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml index 8703287..d3b8fa0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ clap = { version = "4", features = ["derive"] } colored = "2" time = { version = "0.3", features = ["serde", "std", "formatting"] } inotify = "0.10" +yara-x = "1.19" [profile.release] lto = true diff --git a/crates/hound-api/src/lib.rs b/crates/hound-api/src/lib.rs index 82faa92..d227443 100644 --- a/crates/hound-api/src/lib.rs +++ b/crates/hound-api/src/lib.rs @@ -109,6 +109,11 @@ pub struct ScanResult { pub scanned: u64, pub clean: u64, pub infected: u64, + /// Files the engine deliberately did not read — over the size cap. + /// Reported separately because counting them as scanned would be a + /// lie and dropping them silently would be worse. + #[serde(default)] + pub skipped: u64, pub found: Vec, } @@ -166,11 +171,67 @@ pub struct Settings { /// Run the signature update automatically (daemon schedules it). pub auto_update_signatures: bool, + // Appearance (GUI-only; the daemon stores them so the setting follows + // the machine rather than a per-user GUI config file, and so the CLI + // can read and set them too). + /// Window theme: "auto" (follow the desktop) | "light" | "dark". + #[serde(default = "default_theme")] + pub theme: String, + /// Tray icon treatment. Colour uses the 4-state ladder; monochrome + /// renders a single-tone glyph that follows the panel's own theme, + /// which is what most desktop styleguides actually ask for. + #[serde(default = "default_tray_style")] + pub tray_icon_style: String, + + // Window behaviour + /// Closing the window hides it to the tray instead of exiting. Quitting + /// is only ever possible from the tray menu, and is confirmed there — + /// an antivirus that can be shut off by a stray click on the X is not + /// protecting anything. + #[serde(default = "default_true")] + pub close_to_tray: bool, + /// Whether the tray's Quit entry must be confirmed before it exits. + #[serde(default = "default_true")] + pub confirm_quit: bool, + // Global /// Master switch — when true, realtime is suspended and the tray is gray. pub paused: bool, } +fn default_theme() -> String { + "auto".into() +} + +fn default_tray_style() -> String { + "color".into() +} + +fn default_true() -> bool { + true +} + +impl Settings { + /// Accepted values for [`Settings::theme`]. + pub const THEMES: [&'static str; 3] = ["auto", "light", "dark"]; + /// Accepted values for [`Settings::tray_icon_style`]. + pub const TRAY_STYLES: [&'static str; 2] = ["color", "mono"]; + + /// Clamp free-text enum fields back to something the GUI can render. + /// + /// These arrive over a JSON socket from clients we do not control, so + /// an unknown value is normalised rather than trusted — a typo'd theme + /// must not leave the window unstyled. + pub fn normalise_appearance(&mut self) { + if !Self::THEMES.contains(&self.theme.as_str()) { + self.theme = default_theme(); + } + if !Self::TRAY_STYLES.contains(&self.tray_icon_style.as_str()) { + self.tray_icon_style = default_tray_style(); + } + } +} + impl Default for Settings { fn default() -> Self { Self { @@ -180,6 +241,10 @@ impl Default for Settings { realtime_enabled: true, realtime_watch: vec!["~/Downloads".into()], on_detect: "quarantine".into(), + theme: default_theme(), + tray_icon_style: default_tray_style(), + close_to_tray: true, + confirm_quit: true, ransomware_guard: true, ransomware_threshold_per_min: 40, rootkit_enabled: true, diff --git a/crates/houndd/Cargo.toml b/crates/houndd/Cargo.toml index 3026a66..0f6067b 100644 --- a/crates/houndd/Cargo.toml +++ b/crates/houndd/Cargo.toml @@ -17,3 +17,4 @@ serde.workspace = true serde_json.workspace = true time.workspace = true inotify.workspace = true +yara-x.workspace = true diff --git a/crates/houndd/rules/hound-builtin.yar b/crates/houndd/rules/hound-builtin.yar new file mode 100644 index 0000000..4454456 --- /dev/null +++ b/crates/houndd/rules/hound-builtin.yar @@ -0,0 +1,97 @@ +/* + * Hound built-in starter pack. + * + * Deliberately tiny and deliberately tight. Every rule here requires + * several independent strings before it fires, because a false positive + * in an antivirus is worse than a miss — one rule that quarantines a + * system binary ends the product. + * + * The real corpus lands in Phase 3 (the signed Hound Linux pack, gated + * behind the goodware CI regression suite). This pack exists so a fresh + * install detects *something* before it has ever contacted the network. + */ + +rule EICAR_Test_File +{ + meta: + name = "EICAR-Test-Signature" + severity = "info" + desc = "Industry-standard antivirus test file. Harmless." + strings: + $eicar = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" + condition: + $eicar +} + +rule Linux_Coinminer_XMRig +{ + meta: + name = "Linux.Coinminer.XMRig" + severity = "critical" + desc = "XMRig cryptocurrency miner. Requires pool protocol plus two config keys." + strings: + $pool1 = "stratum+tcp://" ascii + $pool2 = "stratum+ssl://" ascii + $cfg1 = "donate-level" ascii + $cfg2 = "rig-id" ascii + $cfg3 = "randomx" ascii nocase + $name = "xmrig" ascii nocase + condition: + ($pool1 or $pool2) and 2 of ($cfg*) and $name +} + +rule Linux_Webshell_PHP_Eval +{ + meta: + name = "Linux.Webshell.PHP-Eval" + severity = "critical" + desc = "PHP webshell: request-driven eval of decoded input." + strings: + $php = " Self { + Self { + dev: md.dev(), + ino: md.ino(), + mtime: md.mtime(), + mtime_nsec: md.mtime_nsec(), + size: md.size(), + } + } +} + +/// What a previous scan concluded. `None` is clean. +pub type Verdict = Option>; + +struct Inner { + map: HashMap, + order: VecDeque, + hits: u64, + misses: u64, +} + +/// A bounded, thread-safe cache of scan verdicts. +#[derive(Clone)] +pub struct VerdictCache { + inner: Arc>, + capacity: usize, +} + +impl VerdictCache { + pub fn new(capacity: usize) -> Self { + Self { + inner: Arc::new(Mutex::new(Inner { + map: HashMap::with_capacity(capacity.min(4096)), + order: VecDeque::with_capacity(capacity.min(4096)), + hits: 0, + misses: 0, + })), + capacity: capacity.max(1), + } + } + + /// Look up a verdict. The outer `Option` is cache presence; the inner + /// one is the verdict itself, so a cached-clean answer is + /// `Some(None)` and is deliberately distinguishable from a miss. + pub fn get(&self, key: &FileKey) -> Option { + let mut inner = self.inner.lock().expect("verdict cache poisoned"); + match inner.map.get(key) { + Some(v) => { + let v = v.clone(); + inner.hits += 1; + Some(v) + } + None => { + inner.misses += 1; + None + } + } + } + + /// Record a verdict, evicting the oldest entry when full. + pub fn put(&self, key: FileKey, verdict: Verdict) { + let mut inner = self.inner.lock().expect("verdict cache poisoned"); + if inner.map.insert(key, verdict).is_none() { + inner.order.push_back(key); + while inner.order.len() > self.capacity { + if let Some(old) = inner.order.pop_front() { + inner.map.remove(&old); + } + } + } + } + + /// Drop everything. Called whenever the ruleset changes — a verdict + /// reached under the old rules says nothing about the new ones. + pub fn clear(&self) { + let mut inner = self.inner.lock().expect("verdict cache poisoned"); + inner.map.clear(); + inner.order.clear(); + } + + /// `(entries, hits, misses)` for `hound status` and the benchmarks. + pub fn stats(&self) -> (usize, u64, u64) { + let inner = self.inner.lock().expect("verdict cache poisoned"); + (inner.map.len(), inner.hits, inner.misses) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn key(ino: u64, size: u64) -> FileKey { + FileKey { + dev: 1, + ino, + mtime: 100, + mtime_nsec: 0, + size, + } + } + + #[test] + fn miss_then_hit() { + let c = VerdictCache::new(8); + assert!(c.get(&key(1, 10)).is_none(), "cold lookup must miss"); + c.put(key(1, 10), None); + assert_eq!(c.get(&key(1, 10)), Some(None), "cached clean is a hit"); + } + + #[test] + fn cached_infected_round_trips() { + let c = VerdictCache::new(8); + c.put(key(2, 20), Some("Linux.Coinminer.XMRig".into())); + let got = c.get(&key(2, 20)).expect("should hit"); + assert_eq!(got.as_deref(), Some("Linux.Coinminer.XMRig")); + } + + #[test] + fn any_field_change_misses() { + let c = VerdictCache::new(8); + c.put(key(3, 30), None); + // Same inode, different size — the file was rewritten. + assert!(c.get(&key(3, 31)).is_none()); + // Same size, different inode — replaced by rename. + assert!(c.get(&key(4, 30)).is_none()); + } + + #[test] + fn mtime_change_misses() { + let c = VerdictCache::new(8); + let mut k = key(5, 50); + c.put(k, None); + k.mtime_nsec = 1; + assert!(c.get(&k).is_none(), "a nanosecond of edit is still an edit"); + } + + #[test] + fn evicts_fifo_at_capacity() { + let c = VerdictCache::new(2); + c.put(key(1, 1), None); + c.put(key(2, 2), None); + c.put(key(3, 3), None); + assert!(c.get(&key(1, 1)).is_none(), "oldest should be evicted"); + assert!(c.get(&key(3, 3)).is_some(), "newest should be resident"); + let (entries, _, _) = c.stats(); + assert_eq!(entries, 2, "capacity must be honoured"); + } + + #[test] + fn reinsert_does_not_grow_order_queue() { + let c = VerdictCache::new(4); + for _ in 0..50 { + c.put(key(9, 9), None); + } + let (entries, _, _) = c.stats(); + assert_eq!(entries, 1); + } + + #[test] + fn clear_empties() { + let c = VerdictCache::new(4); + c.put(key(1, 1), None); + c.clear(); + assert!(c.get(&key(1, 1)).is_none()); + assert_eq!(c.stats().0, 0); + } + + #[test] + fn stats_count_hits_and_misses() { + let c = VerdictCache::new(4); + c.get(&key(1, 1)); // miss + c.put(key(1, 1), None); + c.get(&key(1, 1)); // hit + let (_, hits, misses) = c.stats(); + assert_eq!((hits, misses), (1, 1)); + } +} diff --git a/crates/houndd/src/engine.rs b/crates/houndd/src/engine.rs index 081d749..9628a67 100644 --- a/crates/houndd/src/engine.rs +++ b/crates/houndd/src/engine.rs @@ -16,9 +16,14 @@ use anyhow::{Context, Result}; use hound_api::{DbFile, ScanResult}; use std::fs; use std::process::Command; +use std::sync::OnceLock; /// What an engine implementation must answer. -pub trait ScanEngine { +/// +/// `Send + Sync` because the daemon holds exactly one engine for its +/// whole life and hands it to every connection thread and to the +/// real-time monitor. +pub trait ScanEngine: Send + Sync { /// Stable id for the wire (`Status.engine`): "clamav" today, e.g. /// "hound-native" when the Rust engine ships. fn name(&self) -> &'static str; @@ -224,6 +229,8 @@ pub fn parse_clamscan(stdout: &[u8], exit_code: i32) -> Result { scanned, clean, infected, + // clamscan does not tell us what it skipped for size. + skipped: 0, found, }) } @@ -240,25 +247,54 @@ pub fn to_rfc3339(t: std::time::SystemTime) -> String { dt.format(&Rfc3339).unwrap_or_else(|_| "unknown".into()) } -/// The active engine, chosen at daemon startup. +/// The active engine, chosen once at daemon startup. /// -/// Default is [`ClamAvEngine`]. Set `HOUNDD_ENGINE=fake` to the -/// [`FakeEngine`] — used by the E2E test so it can drive a full -/// daemon lifecycle (status, scan, settings, quarantine, rootkit) -/// without requiring ClamAV or a real filesystem of .cld files. +/// Default is [`HoundEngine`](crate::native::HoundEngine) — yara-x in +/// process. `HOUNDD_ENGINE` overrides it: +/// +/// * `clamav` — the legacy `clamscan` subprocess path. Kept so the two +/// can be compared directly, and because it still owns the Windows +/// malware corpus that our own rules deliberately do not cover. +/// * `fake` — the synthetic engine the E2E test drives, so a full +/// daemon lifecycle can run without ClamAV or a real rule pack. /// /// The whole point of the trait is that this is the only place the /// daemon decides *which* engine it serves. pub fn engine() -> &'static dyn ScanEngine { - if std::env::var_os("HOUNDD_ENGINE").is_some_and(|v| v == "fake") { - static FAKE: FakeEngine = FakeEngine; - &FAKE - } else { - static CLAMAV: ClamAvEngine = ClamAvEngine; - &CLAMAV + // Selection is per call — one getenv — so the choice stays live and + // two tests in one process cannot contaminate each other. Only the + // expensive engine is memoised, below. + match std::env::var("HOUNDD_ENGINE").as_deref() { + Ok("fake") => { + static FAKE: FakeEngine = FakeEngine; + &FAKE + } + Ok("clamav") => { + static CLAMAV: ClamAvEngine = ClamAvEngine; + &CLAMAV + } + _ => native_engine(), } } +/// The native engine, built exactly once. Compiling the ruleset is the +/// one genuinely expensive thing the daemon does at startup, so it must +/// never happen twice. +fn native_engine() -> &'static dyn ScanEngine { + static ENGINE: OnceLock> = OnceLock::new(); + ENGINE + .get_or_init(|| match crate::native::HoundEngine::new() { + Ok(e) => Box::new(e) as Box, + Err(e) => { + // Losing detection entirely is worse than falling back to + // the slow path, so say so loudly and carry on. + eprintln!("engine: rules failed to compile ({e}) — falling back to clamav"); + Box::new(ClamAvEngine) + } + }) + .as_ref() +} + /// Test engine: reports itself present, scans anything whose name /// contains "EICAR" or ".eicar" as infected, and updates cleanly. /// Lets the E2E test exercise the full wire without ClamAV installed. @@ -301,6 +337,7 @@ impl ScanEngine for FakeEngine { scanned, clean: scanned - infected_u, infected: infected_u, + skipped: 0, found, }) } diff --git a/crates/houndd/src/main.rs b/crates/houndd/src/main.rs index 58d7bfd..96b4a72 100644 --- a/crates/houndd/src/main.rs +++ b/crates/houndd/src/main.rs @@ -40,11 +40,14 @@ //! swaps in [`engine::engine`]'s fake backend (a full daemon lifecycle //! without ClamAV installed). +mod cache; mod engine; mod events; +mod native; mod quarantine; mod realtime; mod rootkit; +mod rules; mod settings; #[cfg(test)] mod test_util; @@ -205,11 +208,13 @@ fn dispatch(req: &hound_api::Request, st: &DaemonState) -> Result { // ── settings ── "settings.get" => Ok(serde_json::to_value(st.settings.get())?), "settings.set" => { - let incoming: Settings = serde_json::from_value( + let mut incoming: Settings = serde_json::from_value( req.params .clone() .context("settings.set requires a params object")?, )?; + // Clients are not trusted to send a theme we can render. + incoming.normalise_appearance(); st.settings .set(&incoming) .map_err(|e| anyhow::anyhow!("persisting settings: {e}"))?; diff --git a/crates/houndd/src/native.rs b/crates/houndd/src/native.rs new file mode 100644 index 0000000..fbd15e8 --- /dev/null +++ b/crates/houndd/src/native.rs @@ -0,0 +1,462 @@ +//! Hound's own engine: yara-x, in process, with a verdict cache. +//! +//! This is the Phase 0 replacement for forking `clamscan`. The old path +//! spent 6.5 seconds and ~1.5 GB of RSS on a 68-byte file because every +//! invocation reloaded a 169 MB signature database. Here the ruleset is +//! compiled once at daemon start, the scanner is reused across every +//! file in a walk, and an unchanged file that has been seen before never +//! reaches the matcher at all. +//! +//! Deliberate choices worth knowing about: +//! +//! * **Symlinks are never followed.** A directory walk that follows links +//! can loop, can be steered outside the requested tree by anyone who +//! can create a link, and re-scans the same inode repeatedly. We stat +//! with `symlink_metadata` and skip links entirely. +//! * **Pseudo-filesystems are skipped.** Reading `/proc` and `/sys` is +//! meaningless here and reading some of their files blocks forever. +//! * **Oversized files are counted, not read.** Reporting them as scanned +//! would be a lie; skipping them silently would be worse. They are +//! counted separately and surfaced in the summary. + +use anyhow::{Context, Result}; +use hound_api::{DbFile, Found, ScanResult}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use crate::cache::{FileKey, VerdictCache}; +use crate::engine::{to_rfc3339, ScanEngine}; +use crate::rules::{RuleSet, RuleStore}; + +/// Files larger than this are skipped. Malware that matters is rarely +/// this big, and reading disk images on every scan makes the product +/// unusable on the developer machines we are targeting. +const DEFAULT_MAX_FILE_BYTES: u64 = 100 * 1024 * 1024; + +/// How many file verdicts to remember. ~200k entries is a few tens of MB +/// and comfortably covers a working developer tree plus the system. +const CACHE_CAPACITY: usize = 200_000; + +/// Directory prefixes that are never worth walking. +const SKIP_PREFIXES: &[&str] = &[ + "/proc", + "/sys", + "/dev", + "/run", + "/var/lib/hound/vault", +]; + +pub struct HoundEngine { + rules: RuleStore, + cache: VerdictCache, + max_file_bytes: u64, +} + +impl HoundEngine { + /// Compile the ruleset and build the cache. Called once, from the + /// engine factory, at daemon start. + pub fn new() -> Result { + let rules = RuleStore::load()?; + let max_file_bytes = std::env::var("HOUNDD_MAX_FILE_BYTES") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(DEFAULT_MAX_FILE_BYTES); + Ok(Self { + rules, + cache: VerdictCache::new(CACHE_CAPACITY), + max_file_bytes, + }) + } + + /// Cache statistics, for `hound status` and the benchmark harness. + pub fn cache_stats(&self) -> (usize, u64, u64) { + self.cache.stats() + } +} + +/// Everything a walk accumulated. Kept separate from the wire type so the +/// skipped count can be reported without changing the public API shape. +#[derive(Default)] +struct Tally { + scanned: u64, + infected: u64, + skipped_large: u64, + found: Vec, +} + +impl ScanEngine for HoundEngine { + fn name(&self) -> &'static str { + "hound" + } + + fn probe(&self) -> (bool, String, Option) { + let set = self.rules.current(); + let summary = format!( + "{} rules loaded from {} source(s) [hound {}]", + set.count, + set.sources.len(), + set.version + ); + ( + true, + summary, + Some(DbFile { + file: set.version.clone(), + updated_at: to_rfc3339(set.loaded_at), + }), + ) + } + + fn scan(&self, path: &str, recursive: bool) -> Result { + let root = std::fs::canonicalize(path).with_context(|| format!("no such path: {path}"))?; + let set = self.rules.current(); + + // One scanner for the whole walk. Constructing it per file would + // reintroduce a chunk of the per-invocation cost we just removed. + let mut scanner = yara_x::Scanner::new(&set.rules); + let mut tally = Tally::default(); + + let mut queue: Vec = vec![root.clone()]; + while let Some(current) = queue.pop() { + let Ok(md) = std::fs::symlink_metadata(¤t) else { + continue; + }; + + if md.is_symlink() { + continue; + } + + if md.is_dir() { + // The root is always descended into; deeper levels only + // when the caller asked for a recursive scan. + if current != root && !recursive { + continue; + } + if is_skipped_dir(¤t) { + continue; + } + for entry in std::fs::read_dir(¤t).into_iter().flatten().flatten() { + queue.push(entry.path()); + } + continue; + } + + if !md.is_file() { + continue; + } + if md.len() > self.max_file_bytes { + tally.skipped_large += 1; + continue; + } + + self.scan_one(¤t, &md, &set, &mut scanner, &mut tally); + } + + let clean = tally.scanned.saturating_sub(tally.infected); + Ok(ScanResult { + scanned: tally.scanned, + clean, + infected: tally.infected, + skipped: tally.skipped_large, + found: tally.found, + }) + } + + fn update(&self) -> Result<(bool, String, String)> { + let before = self.rules.current().count; + match self.rules.reload() { + Ok(set) => { + // Verdicts reached under the old rules say nothing about + // the new ones. + self.cache.clear(); + Ok(( + true, + "reload rules".to_string(), + format!( + "OK: {} rules loaded ({}), was {before}\nsources: {}\n", + set.count, + set.version, + set.sources.join(", ") + ), + )) + } + Err(e) => Ok((false, "reload rules".to_string(), format!("{e}\n"))), + } + } +} + +impl HoundEngine { + /// Scan a single regular file, consulting the cache first. + fn scan_one( + &self, + path: &Path, + md: &std::fs::Metadata, + set: &Arc, + scanner: &mut yara_x::Scanner, + tally: &mut Tally, + ) { + let key = FileKey::from_metadata(md); + + if let Some(verdict) = self.cache.get(&key) { + tally.scanned += 1; + if let Some(name) = verdict { + tally.infected += 1; + tally.found.push(Found { + path: path.to_string_lossy().into_owned(), + virus: name.to_string(), + }); + } + return; + } + + let Ok(bytes) = std::fs::read(path) else { + // Unreadable is not clean, so it is not cached and not + // counted as scanned. + return; + }; + + tally.scanned += 1; + + let Ok(results) = scanner.scan(&bytes) else { + return; + }; + + // A file can trip several rules; report it once, under the first + // match, exactly as the ClamAV path did with --allmatch. + let hit = results + .matching_rules() + .next() + .map(|r| RuleSet::detection_name(&r)); + + match hit { + Some(name) => { + tally.infected += 1; + tally.found.push(Found { + path: path.to_string_lossy().into_owned(), + virus: name.clone(), + }); + self.cache.put(key, Some(name.into())); + } + None => self.cache.put(key, None), + } + let _ = set; + } +} + +/// Pseudo-filesystems and our own vault: never walked. +fn is_skipped_dir(path: &Path) -> bool { + SKIP_PREFIXES.iter().any(|p| path.starts_with(p)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + fn tmpdir(tag: &str) -> PathBuf { + let d = std::env::temp_dir().join(format!( + "hound-native-{tag}-{}-{:?}", + std::process::id(), + std::thread::current().id() + )); + let _ = fs::remove_dir_all(&d); + fs::create_dir_all(&d).unwrap(); + d + } + + const EICAR: &str = r"X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"; + + #[test] + fn finds_eicar_in_a_directory() { + let d = tmpdir("eicar"); + fs::write(d.join("clean.txt"), b"nothing to see").unwrap(); + fs::write(d.join("eicar.com"), EICAR).unwrap(); + + let e = HoundEngine::new().unwrap(); + let r = e.scan(d.to_str().unwrap(), true).unwrap(); + + assert_eq!(r.scanned, 2); + assert_eq!(r.infected, 1); + assert_eq!(r.clean, 1); + assert_eq!(r.found[0].virus, "EICAR-Test-Signature"); + assert!(r.found[0].path.ends_with("eicar.com")); + let _ = fs::remove_dir_all(&d); + } + + #[test] + fn clean_tree_is_clean() { + let d = tmpdir("clean"); + for i in 0..20 { + fs::write(d.join(format!("f{i}.txt")), format!("file number {i}")).unwrap(); + } + let e = HoundEngine::new().unwrap(); + let r = e.scan(d.to_str().unwrap(), true).unwrap(); + assert_eq!(r.scanned, 20); + assert_eq!(r.infected, 0); + assert!(r.is_clean()); + let _ = fs::remove_dir_all(&d); + } + + #[test] + fn non_recursive_stops_at_the_top() { + let d = tmpdir("shallow"); + fs::write(d.join("top.txt"), b"top").unwrap(); + let sub = d.join("sub"); + fs::create_dir_all(&sub).unwrap(); + fs::write(sub.join("eicar.com"), EICAR).unwrap(); + + let e = HoundEngine::new().unwrap(); + let r = e.scan(d.to_str().unwrap(), false).unwrap(); + assert_eq!(r.scanned, 1, "must not descend when recursive is false"); + assert_eq!(r.infected, 0); + let _ = fs::remove_dir_all(&d); + } + + #[test] + fn symlinks_are_not_followed() { + let d = tmpdir("symlink"); + let real = d.join("real"); + fs::create_dir_all(&real).unwrap(); + fs::write(real.join("eicar.com"), EICAR).unwrap(); + // A link pointing back at the parent would loop forever if followed. + std::os::unix::fs::symlink(&d, d.join("loop")).unwrap(); + + let e = HoundEngine::new().unwrap(); + let r = e.scan(d.to_str().unwrap(), true).unwrap(); + assert_eq!(r.scanned, 1, "the linked tree must not be walked twice"); + assert_eq!(r.infected, 1); + let _ = fs::remove_dir_all(&d); + } + + #[test] + fn oversized_files_are_skipped_not_scanned() { + let d = tmpdir("large"); + fs::write(d.join("big.bin"), vec![0u8; 4096]).unwrap(); + fs::write(d.join("small.txt"), b"ok").unwrap(); + + let mut e = HoundEngine::new().unwrap(); + e.max_file_bytes = 1024; + let r = e.scan(d.to_str().unwrap(), true).unwrap(); + assert_eq!(r.scanned, 1, "the 4 KB file must not be counted as scanned"); + assert_eq!(r.skipped, 1, "and it must be reported, not silently dropped"); + let _ = fs::remove_dir_all(&d); + } + + #[test] + fn second_scan_is_served_from_cache() { + let d = tmpdir("cache"); + for i in 0..30 { + fs::write(d.join(format!("f{i}.txt")), format!("contents {i}")).unwrap(); + } + let e = HoundEngine::new().unwrap(); + + let first = e.scan(d.to_str().unwrap(), true).unwrap(); + let (_, hits_after_first, _) = e.cache_stats(); + assert_eq!(hits_after_first, 0, "a cold walk cannot hit"); + + let second = e.scan(d.to_str().unwrap(), true).unwrap(); + let (_, hits, _) = e.cache_stats(); + + assert_eq!(first.scanned, second.scanned); + assert_eq!(hits, 30, "every file should be served from cache the second time"); + let _ = fs::remove_dir_all(&d); + } + + #[test] + fn editing_a_file_invalidates_its_cache_entry() { + let d = tmpdir("invalidate"); + let f = d.join("mutable.txt"); + fs::write(&f, b"harmless").unwrap(); + + let e = HoundEngine::new().unwrap(); + let first = e.scan(d.to_str().unwrap(), true).unwrap(); + assert_eq!(first.infected, 0); + + // Rewrite the same path with EICAR. Size and mtime both change. + fs::write(&f, EICAR).unwrap(); + let second = e.scan(d.to_str().unwrap(), true).unwrap(); + assert_eq!(second.infected, 1, "a rewritten file must be rescanned"); + let _ = fs::remove_dir_all(&d); + } + + #[test] + fn scanning_a_single_file_works() { + let d = tmpdir("single"); + let f = d.join("eicar.com"); + fs::write(&f, EICAR).unwrap(); + let e = HoundEngine::new().unwrap(); + let r = e.scan(f.to_str().unwrap(), false).unwrap(); + assert_eq!(r.scanned, 1); + assert_eq!(r.infected, 1); + let _ = fs::remove_dir_all(&d); + } + + #[test] + fn missing_path_is_an_error() { + let e = HoundEngine::new().unwrap(); + assert!(e.scan("/definitely/not/here/at/all", true).is_err()); + } + + #[test] + fn probe_reports_the_ruleset() { + let e = HoundEngine::new().unwrap(); + let (present, summary, db) = e.probe(); + assert!(present); + assert!(summary.contains("rules loaded")); + assert!(db.unwrap().file.starts_with("builtin-")); + } + + #[test] + fn update_reloads_and_clears_the_cache() { + let d = tmpdir("update"); + fs::write(d.join("a.txt"), b"a").unwrap(); + let e = HoundEngine::new().unwrap(); + e.scan(d.to_str().unwrap(), true).unwrap(); + assert!(e.cache_stats().0 > 0); + + let (ok, label, log) = e.update().unwrap(); + assert!(ok, "reload should succeed: {log}"); + assert_eq!(label, "reload rules"); + assert_eq!(e.cache_stats().0, 0, "stale verdicts must be dropped"); + let _ = fs::remove_dir_all(&d); + } + + /// The Phase 0 exit criterion, asserted so it can never quietly + /// regress. The path this replaced took 6.5 seconds *per file* + /// because it reloaded a 169 MB database on every invocation. + /// + /// The bound is deliberately loose (2 s for 400 files, against a + /// measured ~9 ms) because CI machines are slow and shared, and a + /// flaky performance test gets deleted rather than fixed. It is + /// tight enough to catch the only regression that matters: someone + /// reintroducing per-file setup cost. + #[test] + fn four_hundred_files_scan_in_under_two_seconds() { + let d = tmpdir("perf"); + for i in 0..399 { + fs::write(d.join(format!("f{i}.bin")), format!("payload {i}").repeat(64)).unwrap(); + } + fs::write(d.join("eicar.com"), EICAR).unwrap(); + + let e = HoundEngine::new().unwrap(); + let started = std::time::Instant::now(); + let r = e.scan(d.to_str().unwrap(), true).unwrap(); + let elapsed = started.elapsed(); + + assert_eq!(r.scanned, 400); + assert_eq!(r.infected, 1, "EICAR must still be caught at speed"); + assert!( + elapsed < std::time::Duration::from_secs(2), + "400 files took {elapsed:?} — per-file setup cost is back" + ); + eprintln!("perf: 400 files cold in {elapsed:?}"); + let _ = fs::remove_dir_all(&d); + } + + #[test] + fn skips_pseudo_filesystems() { + assert!(is_skipped_dir(Path::new("/proc/1"))); + assert!(is_skipped_dir(Path::new("/sys/kernel"))); + assert!(!is_skipped_dir(Path::new("/home/joe/src"))); + } +} diff --git a/crates/houndd/src/rules.rs b/crates/houndd/src/rules.rs new file mode 100644 index 0000000..28ae6af --- /dev/null +++ b/crates/houndd/src/rules.rs @@ -0,0 +1,272 @@ +//! The rule store. +//! +//! Hound's detection content is YARA, compiled once at daemon start and +//! held in memory for the process lifetime. This is the whole point of +//! Phase 0: the old engine forked `clamscan` per file and paid a 6.5 +//! second signature-database load every single time. Here the ruleset is +//! compiled once and every subsequent scan is a memory operation. +//! +//! Sources, in load order: +//! +//! 1. The built-in starter pack, compiled into the binary. Deliberately +//! tiny and tight so a fresh install detects something before it has +//! ever reached the network. +//! 2. Every `*.yar` / `*.yara` in the rules directory — `$HOUNDD_RULES_DIR` +//! if set, else `/var/lib/hound/rules`, else the XDG data dir for +//! unprivileged runs. This is where the signed Hound pack lands in +//! Phase 3. +//! +//! Reload swaps a fresh `Arc` into place; in-flight scans keep +//! scanning against the ruleset they started with and the next scan picks +//! up the new one. Nothing blocks and nothing is torn out from under a +//! running scan. + +use anyhow::{Context, Result}; +use std::path::PathBuf; +use std::sync::{Arc, RwLock}; +use std::time::SystemTime; + +/// The starter pack, baked into the binary. +const BUILTIN: &str = include_str!("../rules/hound-builtin.yar"); + +/// A compiled ruleset plus the provenance a client needs to display it. +pub struct RuleSet { + pub rules: yara_x::Rules, + /// Wire version, e.g. "builtin-0.1.0" or the pack's own version file. + pub version: String, + /// How many rules compiled. + pub count: usize, + /// When this set was compiled. + pub loaded_at: SystemTime, + /// Human-readable list of what went in, for `hound status`. + pub sources: Vec, +} + +impl RuleSet { + /// Compile the built-in pack plus anything in the rules directory. + /// + /// A malformed file on disk is reported and skipped rather than + /// taking the daemon down — a bad third-party pack must not stop the + /// built-ins from protecting the machine. + fn compile() -> Result { + let mut compiler = yara_x::Compiler::new(); + let mut sources = Vec::new(); + + compiler + .add_source(yara_x::SourceCode::from(BUILTIN).with_origin("hound-builtin.yar")) + .map_err(|e| anyhow::anyhow!("built-in rules failed to compile: {e}"))?; + sources.push("hound-builtin.yar (embedded)".to_string()); + + let mut version = format!("builtin-{}", env!("CARGO_PKG_VERSION")); + + if let Some(dir) = rules_dir() { + if let Ok(v) = std::fs::read_to_string(dir.join("VERSION")) { + let v = v.trim(); + if !v.is_empty() { + version = v.to_string(); + } + } + let mut files: Vec = std::fs::read_dir(&dir) + .into_iter() + .flatten() + .flatten() + .map(|e| e.path()) + .filter(|p| { + p.extension() + .is_some_and(|x| x == "yar" || x == "yara") + }) + .collect(); + files.sort(); + + for path in files { + let name = path + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_default(); + let src = match std::fs::read_to_string(&path) { + Ok(s) => s, + Err(e) => { + eprintln!("rules: skipping {name}: {e}"); + continue; + } + }; + match compiler + .add_source(yara_x::SourceCode::from(src.as_str()).with_origin(&name)) + { + Ok(_) => sources.push(name), + Err(e) => eprintln!("rules: skipping {name}: {e}"), + } + } + } + + let rules = compiler.build(); + let count = rules.iter().count(); + + Ok(Self { + rules, + version, + count, + loaded_at: SystemTime::now(), + sources, + }) + } + + /// The detection name to report for a matching rule. + /// + /// Rules carry a `name` metadata field holding the public signature + /// name ("Linux.Coinminer.XMRig"); the rule identifier is the + /// fallback so a pack that omits the metadata still reports usefully. + pub fn detection_name(rule: &yara_x::Rule) -> String { + for (key, value) in rule.metadata() { + if key != "name" { + continue; + } + match value { + yara_x::MetaValue::String(s) => return s.to_string(), + yara_x::MetaValue::Bytes(b) => return b.to_string(), + _ => {} + } + } + rule.identifier().to_string() + } +} + +/// Where on-disk packs live. `None` when no directory exists yet. +pub fn rules_dir() -> Option { + if let Some(dir) = std::env::var_os("HOUNDD_RULES_DIR") { + let p = PathBuf::from(dir); + return p.is_dir().then_some(p); + } + let system = PathBuf::from("/var/lib/hound/rules"); + if system.is_dir() { + return Some(system); + } + let home = std::env::var_os("HOME")?; + let user = PathBuf::from(home).join(".local/share/hound/rules"); + user.is_dir().then_some(user) +} + +/// Hot-swappable handle on the current ruleset. +#[derive(Clone)] +pub struct RuleStore { + inner: Arc>>, +} + +impl RuleStore { + /// Compile at startup. A failure here is fatal for detection, so we + /// surface it rather than silently serving an empty ruleset. + pub fn load() -> Result { + let set = RuleSet::compile().context("compiling rules")?; + Ok(Self { + inner: Arc::new(RwLock::new(Arc::new(set))), + }) + } + + /// The ruleset a scan should use. Cheap — one `Arc` clone. + pub fn current(&self) -> Arc { + Arc::clone(&self.inner.read().expect("rule store poisoned")) + } + + /// Recompile from source and swap the result in. + pub fn reload(&self) -> Result> { + let fresh = Arc::new(RuleSet::compile().context("recompiling rules")?); + *self.inner.write().expect("rule store poisoned") = Arc::clone(&fresh); + Ok(fresh) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn builtin_pack_compiles() { + let set = RuleSet::compile().expect("built-in pack must always compile"); + assert!(set.count >= 4, "expected the starter rules, got {}", set.count); + assert!(set.version.starts_with("builtin-")); + assert!(!set.sources.is_empty()); + } + + #[test] + fn detects_eicar() { + let set = RuleSet::compile().unwrap(); + let mut scanner = yara_x::Scanner::new(&set.rules); + let eicar = br"X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"; + let results = scanner.scan(eicar).unwrap(); + let names: Vec = results + .matching_rules() + .map(|r| RuleSet::detection_name(&r)) + .collect(); + assert!( + names.iter().any(|n| n == "EICAR-Test-Signature"), + "EICAR must be detected, got {names:?}" + ); + } + + #[test] + fn clean_text_is_clean() { + let set = RuleSet::compile().unwrap(); + let mut scanner = yara_x::Scanner::new(&set.rules); + let results = scanner.scan(b"the quick brown fox jumps over the lazy dog\n").unwrap(); + assert_eq!(results.matching_rules().len(), 0); + } + + /// The goodware gate, in miniature. + /// + /// Every rule in the starter pack is scanned against every binary in + /// `/usr/bin` and `/bin`. A single hit fails the build. Phase 3 scales + /// this to the Debian and Ubuntu archives plus the npm and PyPI top + /// 5,000, but the principle is already the one that matters: a rule + /// that fires on a system binary never ships. + /// + /// This test has already earned its keep — it caught a reverse-shell + /// rule that matched `/usr/bin/sudo`. + #[test] + fn no_false_positives_on_system_binaries() { + let set = RuleSet::compile().unwrap(); + let mut scanner = yara_x::Scanner::new(&set.rules); + let mut checked = 0usize; + let mut failures: Vec = Vec::new(); + + for dir in ["/usr/bin", "/bin", "/usr/sbin"] { + for entry in std::fs::read_dir(dir).into_iter().flatten().flatten() { + let path = entry.path(); + let Ok(md) = std::fs::symlink_metadata(&path) else { continue }; + if md.is_symlink() || !md.is_file() || md.len() > 32 * 1024 * 1024 { + continue; + } + let Ok(bytes) = std::fs::read(&path) else { continue }; + checked += 1; + let hits: Vec = scanner + .scan(&bytes) + .map(|r| { + r.matching_rules() + .map(|m| RuleSet::detection_name(&m)) + .collect() + }) + .unwrap_or_default(); + if !hits.is_empty() { + failures.push(format!("{} -> {hits:?}", path.display())); + } + } + } + + assert!(checked > 50, "only {checked} binaries were readable — gate is not meaningful"); + assert!( + failures.is_empty(), + "{} false positive(s) across {checked} system binaries:\n {}", + failures.len(), + failures.join("\n ") + ); + eprintln!("goodware gate: {checked} system binaries, 0 false positives"); + } + + #[test] + fn reload_swaps_in_a_fresh_set() { + let store = RuleStore::load().unwrap(); + let before = store.current(); + let after = store.reload().unwrap(); + assert_eq!(before.count, after.count); + assert!(after.loaded_at >= before.loaded_at); + } +} diff --git a/crates/houndd/src/settings.rs b/crates/houndd/src/settings.rs index c26cd93..3b78278 100644 --- a/crates/houndd/src/settings.rs +++ b/crates/houndd/src/settings.rs @@ -149,3 +149,56 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); } } + +#[cfg(test)] +mod appearance_tests { + use hound_api::Settings; + + #[test] + fn appearance_defaults_are_sane() { + let s = Settings::default(); + assert_eq!(s.theme, "auto", "follow the desktop until told otherwise"); + assert_eq!(s.tray_icon_style, "color"); + assert!(s.close_to_tray, "the X must never stop protection"); + assert!(s.confirm_quit, "quitting an antivirus is a deliberate act"); + } + + #[test] + fn unknown_theme_is_normalised_not_trusted() { + let mut s = Settings::default(); + s.theme = "midnight-neon".into(); + s.tray_icon_style = "sparkles".into(); + s.normalise_appearance(); + assert_eq!(s.theme, "auto"); + assert_eq!(s.tray_icon_style, "color"); + } + + #[test] + fn valid_appearance_values_survive() { + for theme in Settings::THEMES { + for style in Settings::TRAY_STYLES { + let mut s = Settings::default(); + s.theme = theme.into(); + s.tray_icon_style = style.into(); + s.normalise_appearance(); + assert_eq!(s.theme, theme); + assert_eq!(s.tray_icon_style, style); + } + } + } + + /// Settings files written before these fields existed must still load. + #[test] + fn older_settings_json_still_deserialises() { + let legacy = r#"{ + "recursive_default": true, "max_file_size_mb": 100, "exclude_paths": [], + "realtime_enabled": true, "realtime_watch": [], "on_detect": "quarantine", + "ransomware_guard": true, "ransomware_threshold_per_min": 40, + "rootkit_enabled": true, "notify_desktop": true, + "auto_update_signatures": true, "paused": false + }"#; + let s: Settings = serde_json::from_str(legacy).expect("legacy settings must load"); + assert_eq!(s.theme, "auto"); + assert!(s.close_to_tray); + } +}