diff --git a/.gitignore b/.gitignore index cdf50523..b32b62ba 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,7 @@ test_* *.csv *.json *.jsonl -hayabusa* \ No newline at end of file +hayabusa* +*.html +*.htm +*.css \ No newline at end of file diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index ca7a6c4e..2b526423 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -1,14 +1,23 @@ # 変更点 -## 1.x.x [2022/XX/XX] +## 1.7.0 [2022/09/29] **新機能:** +- HTMLレポート機能 (`-H, --html-report`)の追加。 (#689) (@hitenkoku, @nishikawaakira) + **改善:** -- EventID解析のオプションをmetricsオプションに変更した。(旧: -s -> 新: -M) (#706) (@hitenkoku) - -**バグ修正:** +- EventID解析のオプションをmetricsオプションに変更した。(旧: `-s, --statistics` -> 新: `-M, --metrics`) (#706) (@hitenkoku) +- ルール更新オプション(`-u`)を利用したときにHayabusaの新バージョンがないかを確認し、表示するようにした。 (#710) (@hitenkoku) +- HTMLレポート内にロゴを追加した。 (#714) (@hitenkoku) +- メトリクスオプション(`-M --metrics`)もしくはログオン情報(`-L --logon-summary`)と`-d`オプションを利用した場合に1つのテーブルで表示されるように修正した。 (#707) (@hitenkoku) +- メトリクスオプションの結果出力にチャンネル列を追加した。 (#707) (@hitenkoku) +- メトリクスオプション(`-M --metrics`)もしくはログオン情報(`-L --logon-summary`)と`-d`オプションを利用した場合に「First Timestamp」と「Last Timestamp」の出力を行わないように修正した。 (#707) (@hitenkoku) +- メトリクスオプションとログオン情報オプションに対してcsv出力機能(`-o --output`)を追加した。 (#707) (@hitenkoku) +- メトリクスオプションの出力を検出回数と全体の割合が1つのセルで表示されていた箇所を2つの列に分けた。 (#707) (@hitenkoku) +- メトリクスオプションとログオン情報の画面出力に利用していたprettytable-rsクレートをcomfy_tableクレートに修正した. (#707) (@hitenkoku) +- HTMLレポート内にfavicon.pngを追加した。 (#722) (@hitenkoku) ## v1.6.0 [2022/09/16] diff --git a/CHANGELOG.md b/CHANGELOG.md index abb4731a..0dc700d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,24 @@ # Changes -## 1.x.x [2022/XX/XX] +## 1.7.0 [2022/09/29] **New Features:** +- Added a HTML summary report output option (`-H, --html-report`). (#689) (@hitenkoku, @nishikawaakira) + **Enhancements:** - Changed Event ID Statistics option to Event ID Metrics option. (`-s, --statistics` -> `-M, --metrics`) (#706) (@hitenkoku) (Note: `statistics_event_info.txt` was changed to `event_id_info.txt`.) - -**Bug Fixes:** +- Display new version of Hayabusa link when updating rules if there is a newer version. (#710) (@hitenkoku) +- Added logo in HTML summary output. (#714) (@hitenkoku) +- Unified output to one table when using `-M` or `-L` with the `-d` option. (#707) (@hitenkoku) +- Added Channel column to metrics output. (#707) (@hitenkoku) +- Removed First Timestamp and Last Timestamp of `-M` and `-L` option with the `-d` option. (#707) (@hitenkoku) +- Added csv output option(`-o --output`) when `-M` or `-L` option is used. (#707) (@hitenkoku) +- Separated Count and Percent columns in metric output. (#707) (@hitenkoku) +- Changed output table format of the metric option and logon information crate from prettytable-rs to comfy_table. (#707) (@hitenkoku) +- Added favicon.png in HTML summary output. (#722) (@hitenkoku) ## v1.6.0 [2022/09/16] diff --git a/Cargo.lock b/Cargo.lock index e07add2c..bbb0e316 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -244,13 +244,13 @@ dependencies = [ [[package]] name = "console" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847" +checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c" dependencies = [ - "encode_unicode 0.3.6", + "encode_unicode", + "lazy_static", "libc", - "once_cell", "terminal_size 0.1.17", "unicode-width", "winapi", @@ -262,6 +262,16 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.3" @@ -393,27 +403,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - [[package]] name = "discard" version = "1.0.4" @@ -438,12 +427,6 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - [[package]] name = "encoding" version = "0.2.33" @@ -508,6 +491,15 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -659,6 +651,18 @@ version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" +[[package]] +name = "futures-io" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" + +[[package]] +name = "futures-sink" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" + [[package]] name = "futures-task" version = "0.3.24" @@ -672,9 +676,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" dependencies = [ "futures-core", + "futures-io", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -709,6 +716,25 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "h2" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -720,7 +746,7 @@ dependencies = [ [[package]] name = "hayabusa" -version = "1.7.0-dev" +version = "1.7.0" dependencies = [ "base64", "bytesize", @@ -737,6 +763,7 @@ dependencies = [ "hashbrown", "hex", "hhmmss", + "horrorshow", "hyper", "is_elevated", "itertools", @@ -748,10 +775,11 @@ dependencies = [ "num_cpus", "openssl", "pbr", - "prettytable-rs", + "pulldown-cmark", "quick-xml", "rand", "regex", + "reqwest", "serde", "serde_derive", "serde_json", @@ -793,6 +821,12 @@ dependencies = [ "time 0.2.27", ] +[[package]] +name = "horrorshow" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8371fb981840150b1a54f7cb117bf6699f7466a1d4861daac33bc6fe2b5abea0" + [[package]] name = "http" version = "0.2.8" @@ -846,18 +880,33 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", + "h2", "http", "http-body", "httparse", "httpdate", "itoa 1.0.3", "pin-project-lite", + "socket2", "tokio", "tower-service", "tracing", "want", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "iana-time-zone" version = "0.1.50" @@ -912,6 +961,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ea37f355c05dde75b84bba2d767906ad522e97cd9e2eef2be7a4ab7fb442c06" +[[package]] +name = "ipnet" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" + [[package]] name = "is_elevated" version = "0.1.2" @@ -1107,6 +1162,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + [[package]] name = "miniz_oxide" version = "0.5.4" @@ -1128,6 +1189,24 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "native-tls" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -1201,9 +1280,9 @@ checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "openssl" -version = "0.10.41" +version = "0.10.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" +checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13" dependencies = [ "bitflags", "cfg-if", @@ -1242,9 +1321,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.75" +version = "0.9.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" +checksum = "5230151e44c0f05157effb743e8d517472843121cf9243e8b81393edb5acd9ce" dependencies = [ "autocfg", "cc", @@ -1325,20 +1404,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" -[[package]] -name = "prettytable-rs" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f375cb74c23b51d23937ffdeb48b1fbf5b6409d4b9979c1418c1de58bc8f801" -dependencies = [ - "atty", - "csv", - "encode_unicode 1.0.0", - "lazy_static", - "term", - "unicode-width", -] - [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1371,9 +1436,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "3edcd08cf4fea98d1ae6c9ddd3b8ccb1acac7c3693d62625969a7daa04a2ae36" dependencies = [ "unicode-ident", ] @@ -1477,17 +1542,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "redox_users" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" -dependencies = [ - "getrandom", - "redox_syscall", - "thiserror", -] - [[package]] name = "regex" version = "1.6.0" @@ -1520,6 +1574,43 @@ dependencies = [ "winapi", ] +[[package]] +name = "reqwest" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "rpmalloc" version = "0.2.2" @@ -1551,9 +1642,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.35.10" +version = "0.35.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af895b90e5c071badc3136fc10ff0bcfc98747eadbaf43ed8f214e07ba8f8477" +checksum = "fbb2fda4666def1433b1b05431ab402e42a1084285477222b72d6c564c417cef" dependencies = [ "bitflags", "errno", @@ -1584,12 +1675,45 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +dependencies = [ + "lazy_static", + "windows-sys", +] + [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "security-framework" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "0.9.0" @@ -1645,6 +1769,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.3", + "ryu", + "serde", +] + [[package]] name = "sha1" version = "0.6.1" @@ -1716,6 +1852,15 @@ dependencies = [ "walkdir", ] +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.9.0" @@ -1823,9 +1968,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e" +checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" dependencies = [ "proc-macro2", "quote", @@ -1846,17 +1991,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "term" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" -dependencies = [ - "dirs-next", - "rustversion", - "winapi", -] - [[package]] name = "termcolor" version = "1.1.3" @@ -1894,18 +2028,18 @@ checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" [[package]] name = "thiserror" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote", @@ -1996,9 +2130,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.21.1" +version = "1.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" dependencies = [ "autocfg", "bytes", @@ -2006,7 +2140,6 @@ dependencies = [ "memchr", "mio", "num_cpus", - "once_cell", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -2026,6 +2159,30 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -2175,6 +2332,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.83" @@ -2204,6 +2373,16 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +[[package]] +name = "web-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2278,6 +2457,15 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "winstructs" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 20401c76..0df35519 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hayabusa" -version = "1.7.0-dev" +version = "1.7.0" authors = ["Yamato Security @SecurityYamato"] edition = "2021" @@ -30,7 +30,6 @@ hashbrown = "0.12.*" hex = "0.4.*" git2 = "0.*" termcolor = "*" -prettytable-rs = "0.*" krapslog = "*" terminal_size = "*" bytesize = "1.*" @@ -39,6 +38,9 @@ lock_api = "0.4.*" crossbeam-utils = "0.8.*" num-format = "*" comfy-table = "6.*" +pulldown-cmark = { version = "0.9.*", default-features = false, features = ["simd"] } +reqwest = {version = "0.11.*", features = ["blocking", "json"]} +horrorshow = "0.8.*" [build-dependencies] static_vcruntime = "2.*" diff --git a/README-1.6.0-Japanese.pdf b/README-1.7.0-Japanese.pdf similarity index 85% rename from README-1.6.0-Japanese.pdf rename to README-1.7.0-Japanese.pdf index 1df027d5..97e6f7f3 100644 Binary files a/README-1.6.0-Japanese.pdf and b/README-1.7.0-Japanese.pdf differ diff --git a/README-1.6.0.pdf b/README-1.7.0.pdf similarity index 83% rename from README-1.6.0.pdf rename to README-1.7.0.pdf index d6be240a..30397b8f 100644 Binary files a/README-1.6.0.pdf and b/README-1.7.0.pdf differ diff --git a/README-Japanese.md b/README-Japanese.md index bbd1ab02..47fd2e1a 100644 --- a/README-Japanese.md +++ b/README-Japanese.md @@ -22,7 +22,7 @@ # Hayabusa について -Hayabusaは、日本の[Yamato Security](https://yamatosecurity.connpass.com/)グループによって作られた**Windowsイベントログのファストフォレンジックタイムライン生成**および**スレットハンティングツール**です。 Hayabusaは日本語で[「ハヤブサ」](https://en.wikipedia.org/wiki/Peregrine_falcon)を意味し、ハヤブサが世界で最も速く、狩猟(hunting)に優れ、とても訓練しやすい動物であることから選ばれました。[Rust](https://www.rust-lang.org/) で開発され、マルチスレッドに対応し、可能な限り高速に動作するよう配慮されています。[Sigma](https://github.com/SigmaHQ/Sigma)ルールをHayabusaルール形式に変換する[ツール](https://github.com/Yamato-Security/hayabusa-rules/tree/main/tools/sigmac)も提供しています。Hayabusaの検知ルールもSigmaと同様にYML形式であり、カスタマイズ性や拡張性に優れます。稼働中のシステムで実行してライブ調査することも、複数のシステムからログを収集してオフライン調査することも可能です。また、 [Velociraptor](https://docs.velociraptor.app/)と[Hayabusa artifact](https://docs.velociraptor.app/exchange/artifacts/pages/windows.eventlogs.hayabusa/)を用いることで企業向けの広範囲なスレットハンティングとインシデントレスポンスにも活用できます。出力は一つのCSVタイムラインにまとめられ、Excel、[Timeline Explorer](https://ericzimmerman.github.io/#!index.md)、[Elastic Stack](doc/ElasticStackImport/ElasticStackImport-Japanese.md)、[Timesketch](https://timesketch.org/)等で簡単に分析できるようになります。 +Hayabusaは、日本の[Yamato Security](https://yamatosecurity.connpass.com/)グループによって作られた**Windowsイベントログのファストフォレンジックタイムライン生成**および**スレットハンティングツール**です。 Hayabusaは日本語で[「ハヤブサ」](https://ja.wikipedia.org/wiki/%E3%83%8F%E3%83%A4%E3%83%96%E3%82%B5)を意味し、ハヤブサが世界で最も速く、狩猟(hunting)に優れ、とても訓練しやすい動物であることから選ばれました。[Rust](https://www.rust-lang.org/) で開発され、マルチスレッドに対応し、可能な限り高速に動作するよう配慮されています。[Sigma](https://github.com/SigmaHQ/Sigma)ルールをHayabusaルール形式に変換する[ツール](https://github.com/Yamato-Security/hayabusa-rules/tree/main/tools/sigmac)も提供しています。Hayabusaの検知ルールもSigmaと同様にYML形式であり、カスタマイズ性や拡張性に優れます。稼働中のシステムで実行してライブ調査することも、複数のシステムからログを収集してオフライン調査することも可能です。また、 [Velociraptor](https://docs.velociraptor.app/)と[Hayabusa artifact](https://docs.velociraptor.app/exchange/artifacts/pages/windows.eventlogs.hayabusa/)を用いることで企業向けの広範囲なスレットハンティングとインシデントレスポンスにも活用できます。出力は一つのCSVタイムラインにまとめられ、Excel、[Timeline Explorer](https://ericzimmerman.github.io/#!index.md)、[Elastic Stack](doc/ElasticStackImport/ElasticStackImport-Japanese.md)、[Timesketch](https://timesketch.org/)等で簡単に分析できるようになります。 ## 目次 @@ -41,6 +41,7 @@ Hayabusaは、日本の[Yamato Security](https://yamatosecurity.connpass.com/) - [Criticalアラートのフィルタリングとコンピュータごとのグルーピング](#criticalアラートのフィルタリングとコンピュータごとのグルーピング) - [Elastic Stackダッシュボードでの解析](#elastic-stackダッシュボードでの解析) - [Timesketchでの解析](#timesketchでの解析) + - [HTMLの結果サマリ](#htmlの結果サマリ) - [タイムラインのサンプル結果](#タイムラインのサンプル結果) - [特徴&機能](#特徴機能) - [ダウンロード](#ダウンロード) @@ -159,6 +160,10 @@ Hayabusaは従来のWindowsイベントログ分析解析と比較して、分 ![Timesketch](screenshots/TimesketchAnalysis.png) +## HTMLの結果サマリ + +![HTMLResultsSummary](screenshots/HTML-ResultsSummary.png) + # タイムラインのサンプル結果 CSVのタイムライン結果のサンプルは[こちら](https://github.com/Yamato-Security/hayabusa/tree/main/sample-results)で確認できます。 @@ -207,7 +212,7 @@ git clone https://github.com/Yamato-Security/hayabusa.git --recursive `git pull --recurse-submodules`コマンド、もしくは以下のコマンドで`rules`フォルダを同期し、Hayabusaの最新のルールを更新することができます: ```bash -hayabusa-1.6.0-win-x64.exe -u +hayabusa-1.7.0-win-x64.exe -u ``` アップデートが失敗した場合は、`rules`フォルダの名前を変更してから、もう一回アップデートしてみて下さい。 @@ -312,20 +317,20 @@ Windows PC起動後の初回実行時に時間がかかる場合があります コマンドプロンプトやWindows Terminalから32ビットもしくは64ビットのWindowsバイナリをHayabusaのルートディレクトリから実行します。 -例: `hayabusa-1.6.0-windows-x64.exe` +例: `hayabusa-1.7.0-windows-x64.exe` ## Linux まず、バイナリに実行権限を与える必要があります。 ```bash -chmod +x ./hayabusa-1.6.0-linux-x64-gnu +chmod +x ./hayabusa-1.7.0-linux-x64-gnu ``` 次に、Hayabusaのルートディレクトリから実行します: ```bash -./hayabusa-1.6.0-linux-x64-gnu +./hayabusa-1.7.0-linux-x64-gnu ``` ## macOS @@ -333,13 +338,13 @@ chmod +x ./hayabusa-1.6.0-linux-x64-gnu まず、ターミナルやiTerm2からバイナリに実行権限を与える必要があります。 ```bash -chmod +x ./hayabusa-1.6.0-mac-intel +chmod +x ./hayabusa-1.7.0-mac-intel ``` 次に、Hayabusaのルートディレクトリから実行してみてください: ```bash -./hayabusa-1.6.0-mac-intel +./hayabusa-1.7.0-mac-intel ``` macOSの最新版では、以下のセキュリティ警告が出る可能性があります: @@ -353,7 +358,7 @@ macOSの環境設定から「セキュリティとプライバシー」を開き その後、ターミナルからもう一回実行してみてください: ```bash -./hayabusa-1.6.0-mac-intel +./hayabusa-1.7.0-mac-intel ``` 以下の警告が出るので、「開く」をクリックしてください。 @@ -393,6 +398,7 @@ ADVANCED: --target-file-ext ... evtx以外の拡張子を解析対象に追加する。 (例1: evtx_data 例2:evtx1 evtx2) OUTPUT: + -H, --html-report HTML形式で詳細な結果を出力する (例: results.html) -j, --json タイムラインの出力をJSON形式で保存する(例: -j -o results.json) -J, --jsonl タイムラインの出力をJSONL形式で保存する (例: -J -o results.jsonl) -o, --output タイムラインをCSV形式で保存する (例: results.csv) @@ -437,91 +443,91 @@ TIME-FORMAT: * 1つのWindowsイベントログファイルに対してHayabusaを実行する: ```bash -hayabusa-1.6.0-win-x64.exe -f eventlog.evtx +hayabusa-1.7.0-win-x64.exe -f eventlog.evtx ``` * `verbose`プロファイルで複数のWindowsイベントログファイルのあるsample-evtxディレクトリに対して、Hayabusaを実行する: ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -P verbose +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx -P verbose ``` * 全てのフィールド情報も含めて1つのCSVファイルにエクスポートして、Excel、Timeline Explorer、Elastic Stack等でさらに分析することができる(注意: `verbose-details-and-all-field-info`プロファイルを使すると、出力するファイルのサイズがとても大きくなる!): ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -o results.csv -P verbose-details-and-all-field-info +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx -o results.csv -P verbose-details-and-all-field-info ``` * タイムラインをJSON形式で保存する: ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -o results.json -j +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx -o results.json -j ``` * Hayabusaルールのみを実行する(デフォルトでは`-r .\rules`にあるすべてのルールが利用される): ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa -o results.csv +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa -o results.csv ``` * Windowsでデフォルトで有効になっているログに対してのみ、Hayabusaルールを実行する: ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\default -o results.csv +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\default -o results.csv ``` * Sysmonログに対してのみHayabusaルールを実行する: ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\sysmon -o results.csv +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\sysmon -o results.csv ``` * Sigmaルールのみを実行する: ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\sigma -o results.csv +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\sigma -o results.csv ``` * 廃棄(deprecated)されたルール(`status`が`deprecated`になっているルール)とノイジールール(`.\rules\config\noisy_rules.txt`にルールIDが書かれているルール)を有効にする: ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx --enable-deprecated-rules --enable-noisy-rules -o results.csv +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx --enable-deprecated-rules --enable-noisy-rules -o results.csv ``` * ログオン情報を分析するルールのみを実行し、UTCタイムゾーンで出力する: ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\default\events\Security\Logons -U -o results.csv +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\default\events\Security\Logons -U -o results.csv ``` * 起動中のWindows端末上で実行し(Administrator権限が必要)、アラート(悪意のある可能性のある動作)のみを検知する: ```bash -hayabusa-1.6.0-win-x64.exe -l -m low +hayabusa-1.7.0-win-x64.exe -l -m low ``` * criticalレベルのアラートからピボットキーワードの一覧を作成する(結果は結果毎に`keywords-Ip Address.txt`や`keywords-Users.txt`等に出力される): ```bash -hayabusa-1.6.0-win-x64.exe -l -m critical -p -o keywords +hayabusa-1.7.0-win-x64.exe -l -m critical -p -o keywords ``` * イベントIDの統計情報を出力する: ```bash -hayabusa-1.6.0-win-x64.exe -f Security.evtx -M +hayabusa-1.7.0-win-x64.exe -f Security.evtx -M ``` * ログオンサマリを出力する: ```bash -hayabusa-1.6.0-win-x64.exe -L -f Security.evtx -M +hayabusa-1.7.0-win-x64.exe -L -f Security.evtx -M ``` * 詳細なメッセージを出力する(処理に時間がかかるファイル、パースエラー等を特定するのに便利): ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -v +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx -v ``` * Verbose出力の例: @@ -542,7 +548,7 @@ Checking target evtx FilePath: "./hayabusa-sample-evtx/YamatoSecurity/T1218.004_ * 結果を[Timesketch](https://timesketch.org/)にインポートできるCSV形式に保存する: ```bash -hayabusa-1.6.0-win-x64.exe -d ../hayabusa-sample-evtx --RFC-3339 -o timesketch-import.csv -P timesketch -U +hayabusa-1.7.0-win-x64.exe -d ../hayabusa-sample-evtx --RFC-3339 -o timesketch-import.csv -P timesketch -U ``` * エラーログの出力をさせないようにする: @@ -854,7 +860,7 @@ Hayabusaルールは、Windowsのイベントログ解析専用に設計され ## 検知レベルのlevelチューニング Hayabusaルール、Sigmaルールはそれぞれの作者が検知した際のリスクレベルを決めています。 -ユーザが独自のリスクレベルに設定するには`./rules/config/level_tuning.txt`に変換情報を書き、`hayabusa-1.6.0-win-x64.exe --level-tuning`を実行することでルールファイルが書き換えられます。 +ユーザが独自のリスクレベルに設定するには`./rules/config/level_tuning.txt`に変換情報を書き、`hayabusa-1.7.0-win-x64.exe --level-tuning`を実行することでルールファイルが書き換えられます。 ルールファイルが直接書き換えられることに注意して使用してください。 `./rules/config/level_tuning.txt`の例: @@ -903,7 +909,9 @@ id,new_level # Windowsイベントログ設定のススメ Windows機での悪性な活動を検知する為には、デフォルトのログ設定を改善することが必要です。 -以下のサイトを閲覧することをおすすめします。: +どのようなログ設定を有効にする必要があるのか、また、自動的に適切な設定を有効にするためのスクリプトを、別のプロジェクトとして作成しました: [https://github.com/Yamato-Security/EnableWindowsLogSettings](https://github.com/Yamato-Security/EnableWindowsLogSettings) + +以下のサイトを閲覧することもおすすめします。: * [JSCU-NL (Joint Sigint Cyber Unit Netherlands) Logging Essentials](https://github.com/JSCU-NL/logging-essentials) * [ACSC (Australian Cyber Security Centre) Logging and Fowarding Guide](https://www.cyber.gov.au/acsc/view-all-content/publications/windows-event-logging-and-forwarding) * [Malware Archaeology Cheat Sheets](https://www.malwarearchaeology.com/cheat-sheets) diff --git a/README.md b/README.md index d0174604..bce1b63b 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ # About Hayabusa -Hayabusa is a **Windows event log fast forensics timeline generator** and **threat hunting tool** created by the [Yamato Security](https://yamatosecurity.connpass.com/) group in Japan. Hayabusa means ["peregrine falcon"](https://en.wikipedia.org/wiki/Peregrine_falcon") in Japanese and was chosen as peregrine falcons are the fastest animal in the world, great at hunting and highly trainable. It is written in [Rust](https://www.rust-lang.org/) and supports multi-threading in order to be as fast as possible. We have provided a [tool](https://github.com/Yamato-Security/hayabusa-rules/tree/main/tools/sigmac) to convert [Sigma](https://github.com/SigmaHQ/sigma) rules into Hayabusa rule format. The Sigma-compatible Hayabusa detection rules are written in YML in order to be as easily customizable and extensible as possible. Hayabusa can be run either on single running systems for live analysis, by gathering logs from single or multiple systems for offline analysis, or by running the [Hayabusa artifact](https://docs.velociraptor.app/exchange/artifacts/pages/windows.eventlogs.hayabusa/) with [Velociraptor](https://docs.velociraptor.app/) for enterprise-wide threat hunting and incident response. The output will be consolidated into a single CSV timeline for easy analysis in Excel, [Timeline Explorer](https://ericzimmerman.github.io/#!index.md), [Elastic Stack](doc/ElasticStackImport/ElasticStackImport-English.md), [Timesketch](https://timesketch.org/), etc... +Hayabusa is a **Windows event log fast forensics timeline generator** and **threat hunting tool** created by the [Yamato Security](https://yamatosecurity.connpass.com/) group in Japan. Hayabusa means ["peregrine falcon"](https://en.wikipedia.org/wiki/Peregrine_falcon) in Japanese and was chosen as peregrine falcons are the fastest animal in the world, great at hunting and highly trainable. It is written in [Rust](https://www.rust-lang.org/) and supports multi-threading in order to be as fast as possible. We have provided a [tool](https://github.com/Yamato-Security/hayabusa-rules/tree/main/tools/sigmac) to convert [Sigma](https://github.com/SigmaHQ/sigma) rules into Hayabusa rule format. The Sigma-compatible Hayabusa detection rules are written in YML in order to be as easily customizable and extensible as possible. Hayabusa can be run either on single running systems for live analysis, by gathering logs from single or multiple systems for offline analysis, or by running the [Hayabusa artifact](https://docs.velociraptor.app/exchange/artifacts/pages/windows.eventlogs.hayabusa/) with [Velociraptor](https://docs.velociraptor.app/) for enterprise-wide threat hunting and incident response. The output will be consolidated into a single CSV timeline for easy analysis in Excel, [Timeline Explorer](https://ericzimmerman.github.io/#!index.md), [Elastic Stack](doc/ElasticStackImport/ElasticStackImport-English.md), [Timesketch](https://timesketch.org/), etc... ## Table of Contents @@ -40,6 +40,7 @@ Hayabusa is a **Windows event log fast forensics timeline generator** and **thre - [Critical Alert Filtering and Computer Grouping in Timeline Explorer](#critical-alert-filtering-and-computer-grouping-in-timeline-explorer) - [Analysis with the Elastic Stack Dashboard](#analysis-with-the-elastic-stack-dashboard) - [Analysis in Timesketch](#analysis-in-timesketch) + - [HTML Results Summary](#html-results-summary) - [Analyzing Sample Timeline Results](#analyzing-sample-timeline-results) - [Features](#features) - [Downloads](#downloads) @@ -151,6 +152,10 @@ Hayabusa hopes to let analysts get 80% of their work done in 20% of the time whe ![Timesketch](screenshots/TimesketchAnalysis.png) +## HTML Results Summary + +![HTMLResultsSummary](screenshots/HTML-ResultsSummary.png) + # Analyzing Sample Timeline Results You can check out a sample CSV timeline [here](https://github.com/Yamato-Security/hayabusa/tree/main/sample-results). @@ -199,7 +204,7 @@ Note: If you forget to use --recursive option, the `rules` folder, which is mana You can sync the `rules` folder and get latest Hayabusa rules with `git pull --recurse-submodules` or use the following command: ```bash -hayabusa-1.6.0-win-x64.exe -u +hayabusa-1.7.0-win-x64.exe -u ``` If the update fails, you may need to rename the `rules` folder and try again. @@ -304,20 +309,20 @@ You may experience slow runtime especially on the first run after a reboot due t In a Command/PowerShell Prompt or Windows Terminal, just run the appropriate 32-bit or 64-bit Windows binary. -Example: `hayabusa-1.6.0-windows-x64.exe` +Example: `hayabusa-1.7.0-windows-x64.exe` ## Linux You first need to make the binary executable. ```bash -chmod +x ./hayabusa-1.6.0-linux-x64-gnu +chmod +x ./hayabusa-1.7.0-linux-x64-gnu ``` Then run it from the Hayabusa root directory: ```bash -./hayabusa-1.6.0-linux-x64-gnu +./hayabusa-1.7.0-linux-x64-gnu ``` ## macOS @@ -325,13 +330,13 @@ Then run it from the Hayabusa root directory: From Terminal or iTerm2, you first need to make the binary executable. ```bash -chmod +x ./hayabusa-1.6.0-mac-intel +chmod +x ./hayabusa-1.7.0-mac-intel ``` Then, try to run it from the Hayabusa root directory: ```bash -./hayabusa-1.6.0-mac-intel +./hayabusa-1.7.0-mac-intel ``` On the latest version of macOS, you may receive the following security error when you try to run it: @@ -345,7 +350,7 @@ Click "Cancel" and then from System Preferences, open "Security & Privacy" and f After that, try to run it again. ```bash -./hayabusa-1.6.0-mac-intel +./hayabusa-1.7.0-mac-intel ``` The following warning will pop up, so please click "Open". @@ -384,10 +389,11 @@ ADVANCED: --target-file-ext ... Specify additional target file extensions (ex: evtx_data) (ex: evtx1 evtx2) OUTPUT: - -j, --json Save the timeline in JSON format (ex: -j -o results.json) - -J, --jsonl Save the timeline in JSONL format (ex: -J -o results.jsonl) - -o, --output Save the timeline in CSV format (ex: results.csv) - -P, --profile Specify output profile (minimal, standard, verbose, verbose-all-field-info, verbose-details-and-all-field-info) + -H, --html-report Save detail Results Summary in html (ex: results.html) + -j, --json Save the timeline in JSON format (ex: -j -o results.json) + -J, --jsonl Save the timeline in JSONL format (ex: -J -o results.jsonl) + -o, --output Save the timeline in CSV format (ex: results.csv) + -P, --profile Specify output profile (minimal, standard, verbose, verbose-all-field-info, verbose-details-and-all-field-info) DISPLAY-SETTINGS: --no-color Disable color output @@ -428,91 +434,91 @@ TIME-FORMAT: * Run hayabusa against one Windows event log file with default standard profile: ```bash -hayabusa-1.6.0-win-x64.exe -f eventlog.evtx +hayabusa-1.7.0-win-x64.exe -f eventlog.evtx ``` * Run hayabusa against the sample-evtx directory with multiple Windows event log files with the verbose profile: ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -P verbose +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx -P verbose ``` * Export to a single CSV file for further analysis with excel, timeline explorer, elastic stack, etc... and include all field information (Warning: your file output size will become much larger with the `verbose-details-and-all-field-info` profile!): ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -o results.csv -P verbose-details-and-all-field-info +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx -o results.csv -P verbose-details-and-all-field-info ``` * Save the timline in JSON format: ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -o results.json -j +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx -o results.json -j ``` * Only run hayabusa rules (the default is to run all the rules in `-r .\rules`): ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa -o results.csv +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa -o results.csv ``` * Only run hayabusa rules for logs that are enabled by default on Windows: ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\default -o results.csv +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\default -o results.csv ``` * Only run hayabusa rules for sysmon logs: ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\sysmon -o results.csv +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\sysmon -o results.csv ``` * Only run sigma rules: ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\sigma -o results.csv +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\sigma -o results.csv ``` * Enable deprecated rules (those with `status` marked as `deprecated`) and noisy rules (those whose rule ID is listed in `.\rules\config\noisy_rules.txt`): ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx --enable-noisy-rules --enable-deprecated-rules -o results.csv +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx --enable-noisy-rules --enable-deprecated-rules -o results.csv ``` * Only run rules to analyze logons and output in the UTC timezone: ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\default\events\Security\Logons -U -o results.csv +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\default\events\Security\Logons -U -o results.csv ``` * Run on a live Windows machine (requires Administrator privileges) and only detect alerts (potentially malicious behavior): ```bash -hayabusa-1.6.0-win-x64.exe -l -m low +hayabusa-1.7.0-win-x64.exe -l -m low ``` * Create a list of pivot keywords from critical alerts and save the results. (Results will be saved to `keywords-Ip Addresses.txt`, `keywords-Users.txt`, etc...): ```bash -hayabusa-1.6.0-win-x64.exe -l -m critical -p -o keywords +hayabusa-1.7.0-win-x64.exe -l -m critical -p -o keywords ``` * Print Event ID metrics: ```bash -hayabusa-1.6.0-win-x64.exe -f Security.evtx -M +hayabusa-1.7.0-win-x64.exe -f Security.evtx -M ``` * Print logon summary: ```bash -hayabusa-1.6.0-win-x64.exe -L -f Security.evtx -M +hayabusa-1.7.0-win-x64.exe -L -f Security.evtx -M ``` * Print verbose information (useful for determining which files take long to process, parsing errors, etc...): ```bash -hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -v +hayabusa-1.7.0-win-x64.exe -d .\hayabusa-sample-evtx -v ``` * Verbose output example: @@ -533,7 +539,7 @@ Checking target evtx FilePath: "./hayabusa-sample-evtx/YamatoSecurity/T1218.004_ * Output to a CSV format compatible to import into [Timesketch](https://timesketch.org/): ```bash -hayabusa-1.6.0-win-x64.exe -d ../hayabusa-sample-evtx --RFC-3339 -o timesketch-import.csv -P timesketch -U +hayabusa-1.7.0-win-x64.exe -d ../hayabusa-sample-evtx --RFC-3339 -o timesketch-import.csv -P timesketch -U ``` * Quiet error mode: @@ -845,7 +851,7 @@ You can also add a rule ID to `./rules/config/noisy_rules.txt` in order to ignor Hayabusa and Sigma rule authors will determine the risk level of the alert when writing their rules. However, the actual risk level will differ between environments. -You can tune the risk level of the rules by adding them to `./rules/config/level_tuning.txt` and executing `hayabusa-1.6.0-win-x64.exe --level-tuning` which will update the `level` line in the rule file. +You can tune the risk level of the rules by adding them to `./rules/config/level_tuning.txt` and executing `hayabusa-1.7.0-win-x64.exe --level-tuning` which will update the `level` line in the rule file. Please note that the rule file will be updated directly. `./rules/config/level_tuning.txt` sample line: @@ -894,7 +900,10 @@ There is no "one tool to rule them all" and we have found that each has its own # Windows Logging Recommendations -In order to properly detect malicious activity on Windows machines, you will need to improve the default log settings. We recommend the following sites for guidance: +In order to properly detect malicious activity on Windows machines, you will need to improve the default log settings. +We have created a seperate project to document what log settings need to be enabled as well as scripts to automatically enable the proper settings at [https://github.com/Yamato-Security/EnableWindowsLogSettings](https://github.com/Yamato-Security/EnableWindowsLogSettings). + +We also recommend the following sites for guidance: * [JSCU-NL (Joint Sigint Cyber Unit Netherlands) Logging Essentials](https://github.com/JSCU-NL/logging-essentials) * [ACSC (Australian Cyber Security Centre) Logging and Fowarding Guide](https://www.cyber.gov.au/acsc/view-all-content/publications/windows-event-logging-and-forwarding) * [Malware Archaeology Cheat Sheets](https://www.malwarearchaeology.com/cheat-sheets) diff --git a/config/html_report/background_image.jpg b/config/html_report/background_image.jpg new file mode 100644 index 00000000..227d9a80 Binary files /dev/null and b/config/html_report/background_image.jpg differ diff --git a/config/html_report/favicon.png b/config/html_report/favicon.png new file mode 100644 index 00000000..0451346e Binary files /dev/null and b/config/html_report/favicon.png differ diff --git a/config/html_report/hayabusa_report.css b/config/html_report/hayabusa_report.css new file mode 100644 index 00000000..4dbd70cb --- /dev/null +++ b/config/html_report/hayabusa_report.css @@ -0,0 +1,94 @@ +body { + margin: 0; + width: 100vw; + height: 100vh; + background-image:url(./background_image.jpg); +} + +section { + background-image: none; + background-color:white; + margin: 0 auto; + max-width:860px; + box-shadow: 10px 10px 25px 10px rgba(0, 0, 0, .5); +} + +h2 { + padding:0.25em 0.5em; + color:#494949; + background:#f4faff; + border-left : solid 5px #000000; + margin: 16px; +} + +h3 { + background-color: rgb(0, 0, 0); + padding:3px; + margin: 16px; +} + +#computers_with_most_unique_critical_detections { + color: #ff0000; + border-bottom: solid 3px #ff0000; +} + +#top_critical_alerts { + color: #ff0000; + border-bottom: solid 3px #ff0000; +} + +#computers_with_most_unique_high_detections { + color: #ffff00; + border-bottom: solid 3px #ffff00; +} + +#top_high_alerts { + color: #ffff00; + border-bottom: solid 3px #ffff00; +} + +#computers_with_most_unique_medium_detections { + color: #00ffff; + border-bottom: solid 3px #00ffff; +} + +#top_medium_alerts { + color: #00ffff; + border-bottom: solid 3px #00ffff; +} + +#computers_with_most_unique_low_detections { + color: #00ff00; + border-bottom: solid 3px #00ff00; +} + +#top_low_alerts { + color: #00ff00; + border-bottom: solid 3px #00ff00; +} + +#computers_with_most_unique_informational_detections { + color: #ffffff; + border-bottom: solid 3px #ffffff; +} + +#top_informational_alerts { + color: #ffffff; + border-bottom: solid 3px #ffffff; +} + + +li { + padding: 5px; + margin: 16px; +} +li:nth-child(odd){ + background-color: #f4faff +} + +img#logo { + width: 300px; + display: block; + margin-left: auto; + margin-right: auto; +} \ No newline at end of file diff --git a/config/html_report/logo.png b/config/html_report/logo.png new file mode 100644 index 00000000..1b58bd41 Binary files /dev/null and b/config/html_report/logo.png differ diff --git a/rules b/rules index aaf910cd..428abf7c 160000 --- a/rules +++ b/rules @@ -1 +1 @@ -Subproject commit aaf910cdcaca32e89b0f81b0af4e180228d21eb6 +Subproject commit 428abf7caa12a5170582b09342924e382bd333a0 diff --git a/screenshots/HTML-ResultsSummary.png b/screenshots/HTML-ResultsSummary.png new file mode 100644 index 00000000..ee0f6a4e Binary files /dev/null and b/screenshots/HTML-ResultsSummary.png differ diff --git a/src/afterfact.rs b/src/afterfact.rs index b8bc0ecb..13dc42b7 100644 --- a/src/afterfact.rs +++ b/src/afterfact.rs @@ -1,9 +1,7 @@ -use crate::detections::configs; -use crate::detections::configs::{CURRENT_EXE_PATH, TERM_SIZE}; -use crate::detections::message::{self, LEVEL_ABBR}; -use crate::detections::message::{AlertMessage, LEVEL_FULL}; -use crate::detections::utils::{self, format_time}; -use crate::detections::utils::{get_writable_color, write_color_buffer}; +use crate::detections::configs::{self, CURRENT_EXE_PATH, TERM_SIZE}; +use crate::detections::message::{self, AlertMessage, LEVEL_ABBR, LEVEL_FULL}; +use crate::detections::utils::{self, format_time, get_writable_color, write_color_buffer}; +use crate::options::htmlreport; use crate::options::profile::PROFILES; use bytesize::ByteSize; use chrono::{DateTime, Local, TimeZone, Utc}; @@ -24,12 +22,9 @@ use num_format::{Locale, ToFormattedString}; use std::cmp::min; use std::error::Error; -use std::fs::File; -use std::io; -use std::io::BufWriter; -use std::io::Write; +use std::io::{self, BufWriter, Write}; -use std::fs; +use std::fs::{self, File}; use std::process; use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor}; use terminal_size::Width; @@ -210,6 +205,8 @@ fn emit_csv( all_record_cnt: u128, profile: LinkedHashMap, ) -> io::Result<()> { + let mut html_output_stock: Vec = vec![]; + let html_output_flag = configs::CONFIG.read().unwrap().args.html_report.is_some(); let disp_wtr = BufferWriter::stdout(ColorChoice::Always); let mut disp_wtr_buf = disp_wtr.buffer(); let json_output_flag = configs::CONFIG.read().unwrap().args.json_timeline; @@ -238,7 +235,7 @@ fn emit_csv( HashMap::new(); let mut detect_counts_by_rule_and_level: HashMap> = HashMap::new(); - + let mut rule_title_path_map: HashMap = HashMap::new(); let levels = Vec::from(["crit", "high", "med ", "low ", "info", "undefined"]); // レベル別、日ごとの集計用変数の初期化 for level_init in levels { @@ -372,6 +369,7 @@ fn emit_csv( .unwrap() }) .clone(); + rule_title_path_map.insert(detect_info.ruletitle.clone(), detect_info.rulepath.clone()); *detect_counts_by_rules .entry(Clone::clone(&detect_info.ruletitle)) .or_insert(0) += 1; @@ -454,31 +452,35 @@ fn emit_csv( ) .ok(); write_color_buffer(&disp_wtr, get_writable_color(None), ": ", false).ok(); + let saved_alerts_output = + (all_record_cnt - reducted_record_cnt).to_formatted_string(&Locale::en); write_color_buffer( &disp_wtr, get_writable_color(Some(Color::Rgb(255, 255, 0))), - &(all_record_cnt - reducted_record_cnt).to_formatted_string(&Locale::en), + &saved_alerts_output, false, ) .ok(); write_color_buffer(&disp_wtr, get_writable_color(None), " / ", false).ok(); + let all_record_output = all_record_cnt.to_formatted_string(&Locale::en); write_color_buffer( &disp_wtr, get_writable_color(Some(Color::Rgb(0, 255, 255))), - &all_record_cnt.to_formatted_string(&Locale::en), + &all_record_output, false, ) .ok(); write_color_buffer(&disp_wtr, get_writable_color(None), " (", false).ok(); + let reduction_output = format!( + "Data reduction: {} events ({:.2}%)", + reducted_record_cnt.to_formatted_string(&Locale::en), + reducted_percent + ); write_color_buffer( &disp_wtr, get_writable_color(Some(Color::Rgb(0, 255, 0))), - &format!( - "Data reduction: {} events ({:.2}%)", - reducted_record_cnt.to_formatted_string(&Locale::en), - reducted_percent - ), + &reduction_output, false, ) .ok(); @@ -487,6 +489,15 @@ fn emit_csv( println!(); println!(); + if html_output_flag { + html_output_stock.push(format!( + "- Saved alerts and events: {}", + &saved_alerts_output + )); + html_output_stock.push(format!("- Total events analyzed: {}", &all_record_output)); + html_output_stock.push(format!("- {}", reduction_output)); + } + _print_unique_results( total_detect_counts_by_level, unique_detect_counts_by_level, @@ -496,17 +507,44 @@ fn emit_csv( ); println!(); - _print_detection_summary_by_date(detect_counts_by_date_and_level, &color_map); + _print_detection_summary_by_date( + detect_counts_by_date_and_level, + &color_map, + &mut html_output_stock, + ); println!(); println!(); + if html_output_flag { + html_output_stock.push("".to_string()); + } - _print_detection_summary_by_computer(detect_counts_by_computer_and_level, &color_map); + _print_detection_summary_by_computer( + detect_counts_by_computer_and_level, + &color_map, + &mut html_output_stock, + ); println!(); + if html_output_flag { + html_output_stock.push("".to_string()); + } - _print_detection_summary_tables(detect_counts_by_rule_and_level, &color_map); + _print_detection_summary_tables( + detect_counts_by_rule_and_level, + &color_map, + rule_title_path_map, + &mut html_output_stock, + ); println!(); + if html_output_flag { + html_output_stock.push("".to_string()); + } + } + if html_output_flag { + htmlreport::add_md_data( + "Results Summary {#results_summary}".to_string(), + html_output_stock, + ); } - Ok(()) } @@ -634,13 +672,16 @@ fn _print_unique_results( fn _print_detection_summary_by_date( detect_counts_by_date: HashMap>, color_map: &HashMap, + html_output_stock: &mut Vec, ) { let buf_wtr = BufferWriter::stdout(ColorChoice::Always); let mut wtr = buf_wtr.buffer(); wtr.set_color(ColorSpec::new().set_fg(None)).ok(); - - writeln!(wtr, "Dates with most total detections:").ok(); - + let output_header = "Dates with most total detections:"; + writeln!(wtr, "{}", output_header).ok(); + if configs::CONFIG.read().unwrap().args.html_report.is_some() { + html_output_stock.push(format!("- {}", output_header)); + } for (idx, level) in LEVEL_ABBR.values().enumerate() { // output_levelsはlevelsからundefinedを除外した配列であり、各要素は必ず初期化されているのでSomeであることが保証されているのでunwrapをそのまま実施 let detections_by_day = detect_counts_by_date.get(level).unwrap(); @@ -662,26 +703,28 @@ fn _print_detection_summary_by_date( if !exist_max_data { max_detect_str = "n/a".to_string(); } - write!( - wtr, + let output_str = format!( "{}: {}", LEVEL_FULL.get(level.as_str()).unwrap(), &max_detect_str - ) - .ok(); + ); + write!(wtr, "{}", output_str).ok(); if idx != LEVEL_ABBR.len() - 1 { wtr.set_color(ColorSpec::new().set_fg(None)).ok(); - write!(wtr, ", ").ok(); } + if configs::CONFIG.read().unwrap().args.html_report.is_some() { + html_output_stock.push(format!(" - {}", output_str)); + } } buf_wtr.print(&wtr).ok(); } -/// 各レベル毎で最も高い検知数を出した日付を出力する +/// 各レベル毎で最も高い検知数を出したコンピュータ名を出力する fn _print_detection_summary_by_computer( detect_counts_by_computer: HashMap>, color_map: &HashMap, + html_output_stock: &mut Vec, ) { let buf_wtr = BufferWriter::stdout(ColorChoice::Always); let mut wtr = buf_wtr.buffer(); @@ -700,6 +743,22 @@ fn _print_detection_summary_by_computer( sorted_detections.sort_by(|a, b| (-a.1).cmp(&(-b.1))); + // html出力は各種すべてのコンピュータ名を表示するようにする + if configs::CONFIG.read().unwrap().args.html_report.is_some() { + html_output_stock.push(format!( + "### Computers with most unique {} detections: {{#computers_with_most_unique_{}_detections}}", + LEVEL_FULL.get(level.as_str()).unwrap(), + LEVEL_FULL.get(level.as_str()).unwrap() + )); + for x in sorted_detections.iter() { + html_output_stock.push(format!( + "- {} ({})", + x.0, + x.1.to_formatted_string(&Locale::en) + )); + } + html_output_stock.push("".to_string()); + } for x in sorted_detections.iter().take(5) { result_vec.push(format!( "{} ({})", @@ -733,6 +792,8 @@ fn _print_detection_summary_by_computer( fn _print_detection_summary_tables( detect_counts_by_rule_and_level: HashMap>, color_map: &HashMap, + rule_title_path_map: HashMap, + html_output_stock: &mut Vec, ) { let buf_wtr = BufferWriter::stdout(ColorChoice::Always); let mut wtr = buf_wtr.buffer(); @@ -757,6 +818,27 @@ fn _print_detection_summary_tables( sorted_detections.sort_by(|a, b| (-a.1).cmp(&(-b.1))); + // html出力の場合はすべての内容を出力するようにする + if configs::CONFIG.read().unwrap().args.html_report.is_some() { + html_output_stock.push(format!( + "### Top {} alerts: {{#top_{}_alerts}}", + LEVEL_FULL.get(level.as_str()).unwrap(), + LEVEL_FULL.get(level.as_str()).unwrap() + )); + for x in sorted_detections.iter() { + html_output_stock.push(format!( + "- [{}]({}) ({})", + x.0, + rule_title_path_map + .get(x.0) + .unwrap_or(&"".to_string()) + .replace('\\', "/"), + x.1.to_formatted_string(&Locale::en) + )); + } + html_output_stock.push("".to_string()); + } + let take_cnt = if LEVEL_FULL.get(level.as_str()).unwrap_or(&"-".to_string()) == "informational" { 10 diff --git a/src/detections/configs.rs b/src/detections/configs.rs index 0bd52d60..5a29b104 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -1,6 +1,5 @@ use crate::detections::message::AlertMessage; -use crate::detections::pivot::PivotKeyword; -use crate::detections::pivot::PIVOT_KEYWORD; +use crate::detections::pivot::{PivotKeyword, PIVOT_KEYWORD}; use crate::detections::utils; use chrono::{DateTime, Utc}; use clap::{App, CommandFactory, Parser}; @@ -248,6 +247,10 @@ pub struct Config { /// Do not display result summary #[clap(help_heading = Some("DISPLAY-SETTINGS"), long = "no-summary")] pub no_summary: bool, + + /// Save detail Results Summary in html (ex: results.html) + #[clap(help_heading = Some("OUTPUT"), short = 'H', long="html-report", value_name = "FILE")] + pub html_report: Option, } impl ConfigReader<'_> { diff --git a/src/detections/detection.rs b/src/detections/detection.rs index ef428b6f..d6264d01 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -9,19 +9,15 @@ use chrono::{TimeZone, Utc}; use itertools::Itertools; use termcolor::{BufferWriter, Color, ColorChoice}; -use crate::detections::message::AlertMessage; -use crate::detections::message::DetectInfo; -use crate::detections::message::ERROR_LOG_STACK; -use crate::detections::message::{CH_CONFIG, DEFAULT_DETAILS, TAGS_CONFIG}; use crate::detections::message::{ - LOGONSUMMARY_FLAG, METRICS_FLAG, PIVOT_KEYWORD_LIST_FLAG, QUIET_ERRORS_FLAG, + AlertMessage, DetectInfo, CH_CONFIG, DEFAULT_DETAILS, ERROR_LOG_STACK, LOGONSUMMARY_FLAG, + METRICS_FLAG, PIVOT_KEYWORD_LIST_FLAG, QUIET_ERRORS_FLAG, TAGS_CONFIG, }; use crate::detections::pivot::insert_pivot_keyword; -use crate::detections::rule; -use crate::detections::rule::AggResult; -use crate::detections::rule::RuleNode; +use crate::detections::rule::{self, AggResult, RuleNode}; use crate::detections::utils::{get_serde_number_to_string, make_ascii_titlecase}; use crate::filter; +use crate::options::htmlreport::{self}; use crate::yaml::ParseYaml; use hashbrown::HashMap; use serde_json::Value; @@ -31,8 +27,7 @@ use std::path::Path; use std::sync::Arc; use tokio::{runtime::Runtime, spawn, task::JoinHandle}; -use super::message; -use super::message::LEVEL_ABBR; +use super::message::{self, LEVEL_ABBR}; // イベントファイルの1レコード分の情報を保持する構造体 #[derive(Clone, Debug)] @@ -605,6 +600,7 @@ impl Detection { let mut sorted_ld_rc: Vec<(&String, &u128)> = ld_rc.iter().collect(); sorted_ld_rc.sort_by(|a, b| a.0.cmp(b.0)); let args = &configs::CONFIG.read().unwrap().args; + let mut html_report_stock = Vec::new(); sorted_ld_rc.into_iter().for_each(|(key, value)| { if value != &0_u128 { @@ -614,12 +610,16 @@ impl Detection { "" }; //タイトルに利用するものはascii文字であることを前提として1文字目を大文字にするように変更する - println!( + let output_str = format!( "{} rules: {}{}", make_ascii_titlecase(key.clone().as_mut()), value, - disable_flag, + disable_flag ); + println!("{}", output_str); + if configs::CONFIG.read().unwrap().args.html_report.is_some() { + html_report_stock.push(format!("- {}", output_str)); + } } }); if err_rc != &0_u128 { @@ -644,20 +644,24 @@ impl Detection { } else { "" }; + let output_str = format!( + "{} rules: {} ({:.2}%){}", + make_ascii_titlecase(key.clone().as_mut()), + value, + rate, + deprecated_flag + ); //タイトルに利用するものはascii文字であることを前提として1文字目を大文字にするように変更する write_color_buffer( &BufferWriter::stdout(ColorChoice::Always), None, - &format!( - "{} rules: {} ({:.2}%){}", - make_ascii_titlecase(key.clone().as_mut()), - value, - rate, - deprecated_flag - ), + &output_str, true, ) .ok(); + if configs::CONFIG.read().unwrap().args.html_report.is_some() { + html_report_stock.push(format!("- {}", output_str)); + } } }); println!(); @@ -665,17 +669,32 @@ impl Detection { let mut sorted_rc: Vec<(&String, &u128)> = rc.iter().collect(); sorted_rc.sort_by(|a, b| a.0.cmp(b.0)); sorted_rc.into_iter().for_each(|(key, value)| { + let output_str = format!("{} rules: {}", key, value); write_color_buffer( &BufferWriter::stdout(ColorChoice::Always), None, - &format!("{} rules: {}", key, value), + &output_str, true, ) .ok(); + if configs::CONFIG.read().unwrap().args.html_report.is_some() { + html_report_stock.push(format!("- {}", output_str)); + } }); - println!("Total enabled detection rules: {}", total_loaded_rule_cnt); + let tmp_total_detect_output = + format!("Total enabled detection rules: {}", total_loaded_rule_cnt); + println!("{}", tmp_total_detect_output); println!(); + if configs::CONFIG.read().unwrap().args.html_report.is_some() { + html_report_stock.push(format!("- {}", tmp_total_detect_output)); + } + if !html_report_stock.is_empty() { + htmlreport::add_md_data( + "General Overview {#general_overview}".to_string(), + html_report_stock, + ); + } } } diff --git a/src/detections/message.rs b/src/detections/message.rs index 9f46e0bf..d5e90603 100644 --- a/src/detections/message.rs +++ b/src/detections/message.rs @@ -1,9 +1,6 @@ extern crate lazy_static; -use crate::detections::configs; -use crate::detections::configs::CURRENT_EXE_PATH; -use crate::detections::utils; -use crate::detections::utils::get_serde_number_to_string; -use crate::detections::utils::write_color_buffer; +use crate::detections::configs::{self, CURRENT_EXE_PATH}; +use crate::detections::utils::{self, get_serde_number_to_string, write_color_buffer}; use crate::options::profile::PROFILES; use chrono::{DateTime, Local, Utc}; use dashmap::DashMap; @@ -13,10 +10,8 @@ use linked_hash_map::LinkedHashMap; use regex::Regex; use serde_json::Value; use std::env; -use std::fs::create_dir; -use std::fs::File; -use std::io::BufWriter; -use std::io::{self, Write}; +use std::fs::{create_dir, File}; +use std::io::{self, BufWriter, Write}; use std::path::Path; use std::sync::Mutex; use termcolor::{BufferWriter, ColorChoice}; diff --git a/src/detections/utils.rs b/src/detections/utils.rs index 259dc32b..c938775d 100644 --- a/src/detections/utils.rs +++ b/src/detections/utils.rs @@ -2,17 +2,15 @@ extern crate base64; extern crate csv; extern crate regex; -use crate::detections::configs; -use crate::detections::configs::CURRENT_EXE_PATH; +use crate::detections::configs::{self, CURRENT_EXE_PATH}; + use hashbrown::HashMap; -use std::path::Path; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use chrono::Local; use termcolor::Color; -use tokio::runtime::Builder; -use tokio::runtime::Runtime; +use tokio::runtime::{Builder, Runtime}; use chrono::{DateTime, TimeZone, Utc}; use regex::Regex; @@ -28,6 +26,7 @@ use std::vec; use termcolor::{BufferWriter, ColorSpec, WriteColor}; use super::detection::EvtxRecordInfo; +use super::message::AlertMessage; pub fn concat_selection_key(key_list: &[String]) -> String { return key_list @@ -481,6 +480,15 @@ where } } +/// Check file path exist. If path is existed, output alert message. +pub fn check_file_expect_not_exist(path: &Path, exist_alert_str: String) -> bool { + let ret = path.exists(); + if ret { + AlertMessage::alert(&exist_alert_str).ok(); + } + ret +} + #[cfg(test)] mod tests { use std::path::Path; diff --git a/src/filter.rs b/src/filter.rs index c78b7880..d5c6d3c5 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -1,7 +1,5 @@ use crate::detections::configs; -use crate::detections::message::AlertMessage; -use crate::detections::message::ERROR_LOG_STACK; -use crate::detections::message::QUIET_ERRORS_FLAG; +use crate::detections::message::{AlertMessage, ERROR_LOG_STACK, QUIET_ERRORS_FLAG}; use hashbrown::HashMap; use regex::Regex; use std::fs::File; diff --git a/src/lib.rs b/src/lib.rs index 5faf0723..c8f62192 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,3 +6,8 @@ pub mod omikuji; pub mod options; pub mod timeline; pub mod yaml; +<<<<<<< HEAD +======= +#[macro_use] +extern crate horrorshow; +>>>>>>> d91fd31392813c79a33cf5dc10eae06db2ce2613 diff --git a/src/main.rs b/src/main.rs index c4454714..fa78a0cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,8 +7,9 @@ use bytesize::ByteSize; use chrono::{DateTime, Datelike, Local}; use evtx::{EvtxParser, ParserSettings}; use hashbrown::{HashMap, HashSet}; -use hayabusa::detections::configs::{load_pivot_keywords, TargetEventTime, TARGET_EXTENSIONS}; -use hayabusa::detections::configs::{CONFIG, CURRENT_EXE_PATH}; +use hayabusa::detections::configs::{ + load_pivot_keywords, TargetEventTime, CONFIG, CURRENT_EXE_PATH, TARGET_EXTENSIONS, +}; use hayabusa::detections::detection::{self, EvtxRecordInfo}; use hayabusa::detections::message::{ AlertMessage, ERROR_LOG_PATH, ERROR_LOG_STACK, LOGONSUMMARY_FLAG, METRICS_FLAG, @@ -18,8 +19,9 @@ use hayabusa::detections::pivot::PivotKeyword; use hayabusa::detections::pivot::PIVOT_KEYWORD; use hayabusa::detections::rule::{get_detection_keys, RuleNode}; use hayabusa::omikuji::Omikuji; +use hayabusa::options::htmlreport::{self, HTML_REPORTER}; use hayabusa::options::profile::PROFILES; -use hayabusa::options::{level_tuning::LevelTuning, update_rules::UpdateRules}; +use hayabusa::options::{level_tuning::LevelTuning, update::Update}; use hayabusa::{afterfact::after_fact, detections::utils}; use hayabusa::{detections::configs, timeline::timelines::Timeline}; use hayabusa::{detections::utils::write_color_buffer, filter}; @@ -91,6 +93,17 @@ impl App { return; } let analysis_start_time: DateTime = Local::now(); + if configs::CONFIG.read().unwrap().args.html_report.is_some() { + let output_data = vec![format!( + "- Start time: {}", + analysis_start_time.format("%Y/%m/%d %H:%M") + )]; + htmlreport::add_md_data( + "General Overview {#general_overview}".to_string(), + output_data, + ); + } + // Show usage when no arguments. if std::env::args().len() == 1 { self.output_logo(); @@ -107,7 +120,6 @@ impl App { &analysis_start_time.day().to_owned() )); } - if !self.is_matched_architecture_and_binary() { AlertMessage::alert( "The hayabusa version you ran does not match your PC architecture.\nPlease use the correct architecture. (Binary ending in -x64.exe for 64-bit and -x86.exe for 32-bit.)", @@ -118,9 +130,19 @@ impl App { } if configs::CONFIG.read().unwrap().args.update_rules { - match UpdateRules::update_rules( - configs::CONFIG.read().unwrap().args.rules.to_str().unwrap(), - ) { + // エラーが出た場合はインターネット接続がそもそもできないなどの問題点もあるためエラー等の出力は行わない + let latest_version_data = if let Ok(data) = Update::get_latest_hayabusa_version() { + data + } else { + None + }; + let now_version = &format!( + "v{}", + configs::CONFIG.read().unwrap().app.get_version().unwrap() + ); + + match Update::update_rules(configs::CONFIG.read().unwrap().args.rules.to_str().unwrap()) + { Ok(output) => { if output != "You currently have the latest rules." { write_color_buffer( @@ -137,6 +159,33 @@ impl App { } } println!(); + if latest_version_data.is_some() + && now_version + != &latest_version_data + .as_ref() + .unwrap_or(now_version) + .replace('\"', "") + { + write_color_buffer( + &BufferWriter::stdout(ColorChoice::Always), + None, + &format!( + "There is a new version of Hayabusa: {}", + latest_version_data.unwrap().replace('\"', "") + ), + true, + ) + .ok(); + write_color_buffer( + &BufferWriter::stdout(ColorChoice::Always), + None, + "You can download it at https://github.com/Yamato-Security/hayabusa/releases", + true, + ) + .ok(); + } + println!(); + return; } // 実行時のexeファイルのパスをベースに変更する必要があるためデフォルトの値であった場合はそのexeファイルと同一階層を探すようにする @@ -170,20 +219,21 @@ impl App { pivot_key_unions.iter().for_each(|(key, _)| { let keywords_file_name = csv_path.as_path().display().to_string() + "-" + key + ".txt"; - if Path::new(&keywords_file_name).exists() { - AlertMessage::alert(&format!( + utils::check_file_expect_not_exist( + Path::new(&keywords_file_name), + format!( " The file {} already exists. Please specify a different filename.", &keywords_file_name - )) - .ok(); - } + ), + ); }); - if csv_path.exists() { - AlertMessage::alert(&format!( + if utils::check_file_expect_not_exist( + csv_path, + format!( " The file {} already exists. Please specify a different filename.", csv_path.as_os_str().to_str().unwrap() - )) - .ok(); + ), + ) { return; } } @@ -214,6 +264,29 @@ impl App { println!(); } + if let Some(html_path) = &configs::CONFIG.read().unwrap().args.html_report { + // if already exists same html report file. output alert message and exit + if utils::check_file_expect_not_exist( + html_path.as_path(), + format!( + " The file {} already exists. Please specify a different filename.", + html_path.to_str().unwrap() + ), + ) { + return; + } + } + + write_color_buffer( + &BufferWriter::stdout(ColorChoice::Always), + None, + &format!( + "Start time: {}\n", + analysis_start_time.format("%Y/%m/%d %H:%M") + ), + true, + ) + .ok(); if configs::CONFIG.read().unwrap().args.live_analysis { let live_analysis_list = self.collect_liveanalysis_files(); if live_analysis_list.is_none() { @@ -322,15 +395,22 @@ impl App { let analysis_end_time: DateTime = Local::now(); let analysis_duration = analysis_end_time.signed_duration_since(analysis_start_time); + let elapsed_output_str = format!("Elapsed Time: {}", &analysis_duration.hhmmssxxx()); write_color_buffer( &BufferWriter::stdout(ColorChoice::Always), None, - &format!("Elapsed Time: {}", &analysis_duration.hhmmssxxx()), + &elapsed_output_str, true, ) .ok(); println!(); - + if configs::CONFIG.read().unwrap().args.html_report.is_some() { + let output_data = vec![format!("- {}", elapsed_output_str)]; + htmlreport::add_md_data( + "General Overview {#general_overview}".to_string(), + output_data, + ); + } // Qオプションを付けた場合もしくはパースのエラーがない場合はerrorのstackが0となるのでエラーログファイル自体が生成されない。 if ERROR_LOG_STACK.lock().unwrap().len() > 0 { AlertMessage::create_error_log(ERROR_LOG_PATH.to_string()); @@ -408,6 +488,22 @@ impl App { }); } } + if configs::CONFIG.read().unwrap().args.html_report.is_some() { + let html_str = HTML_REPORTER.read().unwrap().clone().create_html(); + htmlreport::create_html_file( + html_str, + configs::CONFIG + .read() + .unwrap() + .args + .html_report + .as_ref() + .unwrap() + .to_str() + .unwrap_or("") + .to_string(), + ) + } } #[cfg(not(target_os = "windows"))] @@ -504,7 +600,6 @@ impl App { } } } - fn analysis_files(&mut self, evtx_files: Vec, time_filter: &TargetEventTime) { let level = configs::CONFIG .read() @@ -525,11 +620,23 @@ impl App { let meta = fs::metadata(file_path).ok(); total_file_size += ByteSize::b(meta.unwrap().len()); } - println!("Total file size: {}", total_file_size.to_string_as(false)); + let total_size_output = format!("Total file size: {}", total_file_size.to_string_as(false)); + println!("{}", total_size_output); println!(); println!("Loading detections rules. Please wait."); println!(); + if configs::CONFIG.read().unwrap().args.html_report.is_some() { + let output_data = vec![ + format!("- Analyzed event files: {}", evtx_files.len()), + format!("- {}", total_size_output), + ]; + htmlreport::add_md_data( + "General Overview #{general_overview}".to_string(), + output_data, + ); + } + let rule_files = detection::Detection::parse_rule_files( level, &configs::CONFIG.read().unwrap().args.rules, @@ -549,15 +656,23 @@ impl App { self.rule_keys = self.get_all_keys(&rule_files); let mut detection = detection::Detection::new(rule_files); let mut total_records: usize = 0; + let mut tl = Timeline::new(); for evtx_file in evtx_files { if configs::CONFIG.read().unwrap().args.verbose { println!("Checking target evtx FilePath: {:?}", &evtx_file); } let cnt_tmp: usize; - (detection, cnt_tmp) = self.analysis_file(evtx_file, detection, time_filter); + (detection, cnt_tmp, tl) = + self.analysis_file(evtx_file, detection, time_filter, tl.clone()); total_records += cnt_tmp; pb.inc(); } + if *METRICS_FLAG { + tl.tm_stats_dsp_msg(); + } + if *LOGONSUMMARY_FLAG { + tl.tm_logon_stats_dsp_msg(); + } if configs::CONFIG.read().unwrap().args.output.is_some() { println!(); println!(); @@ -576,15 +691,15 @@ impl App { evtx_filepath: PathBuf, mut detection: detection::Detection, time_filter: &TargetEventTime, - ) -> (detection::Detection, usize) { + mut tl: Timeline, + ) -> (detection::Detection, usize, Timeline) { let path = evtx_filepath.display(); let parser = self.evtx_to_jsons(evtx_filepath.clone()); let mut record_cnt = 0; if parser.is_none() { - return (detection, record_cnt); + return (detection, record_cnt, tl); } - let mut tl = Timeline::new(); let mut parser = parser.unwrap(); let mut records = parser.records_json_value(); @@ -653,10 +768,7 @@ impl App { } } - tl.tm_stats_dsp_msg(); - tl.tm_logon_stats_dsp_msg(); - - (detection, record_cnt) + (detection, record_cnt, tl) } async fn create_rec_infos( diff --git a/src/options/htmlreport.rs b/src/options/htmlreport.rs new file mode 100644 index 00000000..cc417fab --- /dev/null +++ b/src/options/htmlreport.rs @@ -0,0 +1,165 @@ +use hashbrown::HashMap; +use horrorshow::helper::doctype; +use horrorshow::prelude::*; +use lazy_static::lazy_static; +use pulldown_cmark::{html, Options, Parser}; +use std::fs::{create_dir, File}; +use std::io::{BufWriter, Write}; +use std::path::Path; +use std::sync::RwLock; + +lazy_static! { + pub static ref HTML_REPORTER: RwLock = RwLock::new(HtmlReporter::new()); +} + +#[derive(Clone)] +pub struct HtmlReporter { + pub section_order: Vec, + pub md_datas: HashMap>, +} + +impl HtmlReporter { + pub fn new() -> HtmlReporter { + let (init_section_order, init_data) = get_init_md_data_map(); + HtmlReporter { + section_order: init_section_order, + md_datas: init_data, + } + } + + /// return converted String from md_data(markdown fmt string). + pub fn create_html(self) -> String { + let mut options = Options::empty(); + options.insert(Options::ENABLE_TABLES); + options.insert(Options::ENABLE_HEADING_ATTRIBUTES); + options.insert(Options::ENABLE_FOOTNOTES); + + let mut md_data = vec![]; + for section_name in self.section_order { + if let Some(v) = self.md_datas.get(§ion_name) { + md_data.push(format!("## {}\n", §ion_name)); + if v.is_empty() { + md_data.push("not found data.\n".to_string()); + } else { + md_data.push(v.join("\n")); + } + } + } + let md_str = md_data.join("\n"); + let parser = Parser::new_ext(&md_str, options); + + let mut ret = String::new(); + html::push_html(&mut ret, parser); + ret + } +} + +impl Default for HtmlReporter { + fn default() -> Self { + Self::new() + } +} + +/// get html report section data in LinkedHashMap +fn get_init_md_data_map() -> (Vec, HashMap>) { + let mut ret = HashMap::new(); + let section_order = vec![ + "General Overview {#general_overview}".to_string(), + "Results Summary {#results_summary}".to_string(), + ]; + for section in section_order.iter() { + ret.insert(section.to_owned(), vec![]); + } + + (section_order, ret) +} + +pub fn add_md_data(section_name: String, data: Vec) { + let mut md_with_section_data = HTML_REPORTER.write().unwrap().md_datas.clone(); + for c in data { + let entry = md_with_section_data + .entry(section_name.clone()) + .or_insert(Vec::new()); + entry.push(c); + } + HTML_REPORTER.write().unwrap().md_datas = md_with_section_data; +} + +/// create html file +pub fn create_html_file(input_html: String, path_str: String) { + let path = Path::new(&path_str); + if !path.parent().unwrap().exists() { + create_dir(path.parent().unwrap()).ok(); + } + + let mut html_writer = BufWriter::new(File::create(path).unwrap()); + + let html_data = format!( + "{}", + html! { + : doctype::HTML; + html { + head { + meta(charset="UTF-8"); + link(rel="stylesheet", type="text/css", href="./config/html_report/hayabusa_report.css"); + link(rel="icon", type="image/png", href="./config/html_report/favicon.png"); + } + body { + section { + img(id="logo", src = "./config/html_report/logo.png"); + : Raw(input_html.as_str()); + } + } + + } + } + ); + + writeln!(html_writer, "{}", html_data).ok(); + println!( + "HTML Report was generated. Please check {} for details.", + path_str + ); + println!(); +} + +#[cfg(test)] +mod tests { + + use crate::options::htmlreport::HtmlReporter; + + #[test] + fn test_create_html() { + let mut html_reporter = HtmlReporter::new(); + let general_data = vec![ + "- Analyzed event files: 581".to_string(), + "- Total file size: 148.5 MB".to_string(), + "- Excluded rules: 12".to_string(), + "- Noisy rules: 5 (Disabled)".to_string(), + "- Experimental rules: 1935 (65.97%)".to_string(), + "- Stable rules: 215 (7.33%)".to_string(), + "- Test rules: 783 (26.70%)".to_string(), + "- Hayabusa rules: 138".to_string(), + "- Sigma rules: 2795".to_string(), + "- Total enabled detection rules: 2933".to_string(), + "- Elapsed Time: 00:00:29.035".to_string(), + "".to_string(), + ]; + html_reporter.md_datas.insert( + "General Overview {#general_overview}".to_string(), + general_data.clone(), + ); + let general_overview_str = format!( + "
    \n
  • {}
  • \n
", + general_data[..general_data.len() - 1] + .join("\n
  • ") + .replace("- ", "") + ); + let expect_str = format!( + "

    General Overview

    \n{}\n

    Results Summary

    \n

    not found data.

    \n", + general_overview_str + ); + + assert_eq!(html_reporter.create_html(), expect_str); + } +} diff --git a/src/options/mod.rs b/src/options/mod.rs index f63dd2b9..26cca6fb 100644 --- a/src/options/mod.rs +++ b/src/options/mod.rs @@ -1,3 +1,4 @@ +pub mod htmlreport; pub mod level_tuning; pub mod profile; -pub mod update_rules; +pub mod update; diff --git a/src/options/update_rules.rs b/src/options/update.rs similarity index 86% rename from src/options/update_rules.rs rename to src/options/update.rs index 6d501777..8f896512 100644 --- a/src/options/update_rules.rs +++ b/src/options/update.rs @@ -4,7 +4,8 @@ use crate::filter; use crate::yaml::ParseYaml; use chrono::{DateTime, Local, TimeZone}; use git2::Repository; -use std::fs::{self}; +use serde_json::Value; +use std::fs::{self, create_dir}; use std::path::Path; use hashbrown::{HashMap, HashSet}; @@ -12,13 +13,28 @@ use std::cmp::Ordering; use std::time::SystemTime; -use std::fs::create_dir; - use termcolor::{BufferWriter, ColorChoice}; -pub struct UpdateRules {} +pub struct Update {} + +impl Update { + /// get latest hayabusa version number. + pub fn get_latest_hayabusa_version() -> Result, Box> { + let res = reqwest::blocking::Client::new() + .get("https://api.github.com/repos/Yamato-Security/hayabusa/releases/latest") + .header("User-Agent", "HayabusaUpdateChecker") + .header("Accept", "application/vnd.github.v3+json") + .send()?; + let text = res.text()?; + let json_res: Value = serde_json::from_str(&text)?; + + if json_res["tag_name"].is_null() { + Ok(None) + } else { + Ok(Some(json_res["tag_name"].to_string())) + } + } -impl UpdateRules { /// update rules(hayabusa-rules subrepository) pub fn update_rules(rule_path: &str) -> Result { let mut result; @@ -35,14 +51,14 @@ impl UpdateRules { ) .ok(); // execution git clone of hayabusa-rules repository when failed open hayabusa repository. - result = UpdateRules::clone_rules(Path::new(rule_path)); + result = Update::clone_rules(Path::new(rule_path)); } else if hayabusa_rule_repo.is_ok() { // case of exist hayabusa-rules repository - UpdateRules::_repo_main_reset_hard(hayabusa_rule_repo.as_ref().unwrap())?; + Update::_repo_main_reset_hard(hayabusa_rule_repo.as_ref().unwrap())?; // case of failed fetching origin/main, git clone is not executed so network error has occurred possibly. - prev_modified_rules = UpdateRules::get_updated_rules(rule_path, &prev_modified_time); + prev_modified_rules = Update::get_updated_rules(rule_path, &prev_modified_time); prev_modified_time = fs::metadata(rule_path).unwrap().modified().unwrap(); - result = UpdateRules::pull_repository(&hayabusa_rule_repo.unwrap()); + result = Update::pull_repository(&hayabusa_rule_repo.unwrap()); } else { // case of no exist hayabusa-rules repository in rules. // execute update because submodule information exists if hayabusa repository exists submodule information. @@ -61,7 +77,7 @@ impl UpdateRules { for mut submodule in submodules { submodule.update(true, None)?; let submodule_repo = submodule.open()?; - if let Err(e) = UpdateRules::pull_repository(&submodule_repo) { + if let Err(e) = Update::pull_repository(&submodule_repo) { AlertMessage::alert(&format!("Failed submodule update. {}", e)).ok(); is_success_submodule_update = false; } @@ -80,16 +96,13 @@ impl UpdateRules { ) .ok(); // execution git clone of hayabusa-rules repository when failed open hayabusa repository. - result = UpdateRules::clone_rules(rules_path); + result = Update::clone_rules(rules_path); } } if result.is_ok() { - let updated_modified_rules = - UpdateRules::get_updated_rules(rule_path, &prev_modified_time); - result = UpdateRules::print_diff_modified_rule_dates( - prev_modified_rules, - updated_modified_rules, - ); + let updated_modified_rules = Update::get_updated_rules(rule_path, &prev_modified_time); + result = + Update::print_diff_modified_rule_dates(prev_modified_rules, updated_modified_rules); } result } @@ -254,7 +267,7 @@ impl UpdateRules { #[cfg(test)] mod tests { - use crate::options::update_rules::UpdateRules; + use crate::options::update::Update; use std::time::SystemTime; #[test] @@ -262,12 +275,12 @@ mod tests { let prev_modified_time: SystemTime = SystemTime::UNIX_EPOCH; let prev_modified_rules = - UpdateRules::get_updated_rules("test_files/rules/level_yaml", &prev_modified_time); + Update::get_updated_rules("test_files/rules/level_yaml", &prev_modified_time); assert_eq!(prev_modified_rules.len(), 5); let target_time: SystemTime = SystemTime::now(); let prev_modified_rules2 = - UpdateRules::get_updated_rules("test_files/rules/level_yaml", &target_time); + Update::get_updated_rules("test_files/rules/level_yaml", &target_time); assert_eq!(prev_modified_rules2.len(), 0); } } diff --git a/src/timeline/metrics.rs b/src/timeline/metrics.rs index cf064b00..1126e88f 100644 --- a/src/timeline/metrics.rs +++ b/src/timeline/metrics.rs @@ -2,25 +2,13 @@ use crate::detections::message::{LOGONSUMMARY_FLAG, METRICS_FLAG}; use crate::detections::{detection::EvtxRecordInfo, utils}; use hashbrown::HashMap; -#[derive(Debug)] -pub struct LogEventInfo { - pub channel: String, - pub eventid: String, -} - -impl LogEventInfo { - pub fn new(channel: String, eventid: String) -> LogEventInfo { - LogEventInfo { channel, eventid } - } -} - -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct EventMetrics { pub total: usize, pub filepath: String, pub start_time: String, pub end_time: String, - pub stats_list: HashMap, + pub stats_list: HashMap<(String, String), usize>, pub stats_login_list: HashMap, } /** @@ -32,7 +20,7 @@ impl EventMetrics { filepath: String, start_time: String, end_time: String, - stats_list: HashMap, + stats_list: HashMap<(String, String), usize>, stats_login_list: HashMap, ) -> EventMetrics { EventMetrics { @@ -78,87 +66,71 @@ impl EventMetrics { self.filepath = records[0].evtx_filepath.as_str().to_owned(); // sortしなくてもイベントログのTimeframeを取得できるように修正しました。 // sortしないことにより計算量が改善されています。 - // もうちょっと感じに書けるといえば書けます。 for record in records.iter() { - let evttime = utils::get_event_value( + if let Some(evttime) = utils::get_event_value( "Event.System.TimeCreated_attributes.SystemTime", &record.record, ) - .map(|evt_value| evt_value.to_string()); - if evttime.is_none() { - continue; - } - - let evttime = evttime.unwrap(); - if self.start_time.is_empty() || evttime < self.start_time { - self.start_time = evttime.to_string(); - } - if self.end_time.is_empty() || evttime > self.end_time { - self.end_time = evttime; - } + .map(|evt_value| evt_value.to_string()) + { + if self.start_time.is_empty() || evttime < self.start_time { + self.start_time = evttime.to_string(); + } + if self.end_time.is_empty() || evttime > self.end_time { + self.end_time = evttime; + } + }; } self.total += records.len(); } - // EventIDで集計 + /// EventID`で集計 fn stats_eventid(&mut self, records: &[EvtxRecordInfo]) { // let mut evtstat_map = HashMap::new(); for record in records.iter() { - let channel = utils::get_event_value("Channel", &record.record); - let evtid = utils::get_event_value("EventID", &record.record); - if channel.is_none() { - continue; - } - if evtid.is_none() { - continue; - } - let ch = channel.unwrap().to_string(); - let id = evtid.unwrap().to_string(); - let mut chandid = ch + "," + &id; - chandid.retain(|c| c != '"'); - //let logdata = LogEventInfo::new(ch , id); - //println!("{:?},{:?}", logdata.channel, logdata.eventid); - let count: &mut usize = self.stats_list.entry(chandid).or_insert(0); - *count += 1; + let channel = if let Some(ch) = utils::get_event_value("Channel", &record.record) { + ch.to_string() + } else { + "-".to_string() + }; + if let Some(idnum) = utils::get_event_value("EventID", &record.record) { + let count: &mut usize = self + .stats_list + .entry((idnum.to_string(), channel)) + .or_insert(0); + *count += 1; + }; } - // return evtstat_map; } // Login event fn stats_login_eventid(&mut self, records: &[EvtxRecordInfo]) { for record in records.iter() { - let evtid = utils::get_event_value("EventID", &record.record); - if evtid.is_none() { - continue; - } - let idnum: i64 = if evtid.unwrap().is_number() { - evtid.unwrap().as_i64().unwrap() - } else { - evtid - .unwrap() - .as_str() - .unwrap() - .parse::() - .unwrap_or_default() - }; - if !(idnum == 4624 || idnum == 4625) { - continue; - } + if let Some(evtid) = utils::get_event_value("EventID", &record.record) { + let idnum: i64 = if evtid.is_number() { + evtid.as_i64().unwrap() + } else { + evtid.as_str().unwrap().parse::().unwrap_or_default() + }; + if !(idnum == 4624 || idnum == 4625) { + continue; + } - let username = utils::get_event_value("TargetUserName", &record.record); - let countlist: [usize; 2] = [0, 0]; - if idnum == 4624 { - let count: &mut [usize; 2] = self - .stats_login_list - .entry(username.unwrap().to_string()) - .or_insert(countlist); - count[0] += 1; - } else if idnum == 4625 { - let count: &mut [usize; 2] = self - .stats_login_list - .entry(username.unwrap().to_string()) - .or_insert(countlist); - count[1] += 1; - } + let username = utils::get_event_value("TargetUserName", &record.record); + let countlist: [usize; 2] = [0, 0]; + if idnum == 4624 { + let count: &mut [usize; 2] = self + .stats_login_list + .entry(username.unwrap().to_string()) + .or_insert(countlist); + count[0] += 1; + } else if idnum == 4625 { + let count: &mut [usize; 2] = self + .stats_login_list + .entry(username.unwrap().to_string()) + .or_insert(countlist); + count[1] += 1; + } + }; } } } diff --git a/src/timeline/timelines.rs b/src/timeline/timelines.rs index aeaa0d12..697566f0 100644 --- a/src/timeline/timelines.rs +++ b/src/timeline/timelines.rs @@ -1,11 +1,18 @@ -use crate::detections::message::{LOGONSUMMARY_FLAG, METRICS_FLAG}; +use std::fs::File; +use std::io::BufWriter; + +use crate::detections::message::{AlertMessage, CH_CONFIG, LOGONSUMMARY_FLAG, METRICS_FLAG}; use crate::detections::{configs::CONFIG, detection::EvtxRecordInfo}; +use comfy_table::modifiers::UTF8_ROUND_CORNERS; +use comfy_table::presets::UTF8_FULL; use comfy_table::*; +use csv::WriterBuilder; +use downcast_rs::__std::process; use super::metrics::EventMetrics; use hashbrown::HashMap; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Timeline { pub stats: EventMetrics, } @@ -41,21 +48,62 @@ impl Timeline { } // 出力メッセージ作成 let mut sammsges: Vec = Vec::new(); - sammsges.push("---------------------------------------".to_string()); - sammsges.push(format!("Evtx File Path: {}", self.stats.filepath)); - sammsges.push(format!("Total Event Records: {}\n", self.stats.total)); - sammsges.push(format!("First Timestamp: {}", self.stats.start_time)); - sammsges.push(format!("Last Timestamp: {}\n", self.stats.end_time)); + let total_event_record = format!("\nTotal Event Records: {}\n", self.stats.total); + if CONFIG.read().unwrap().args.filepath.is_some() { + sammsges.push(format!("Evtx File Path: {}", self.stats.filepath)); + sammsges.push(total_event_record); + sammsges.push(format!("First Timestamp: {}", self.stats.start_time)); + sammsges.push(format!("Last Timestamp: {}\n", self.stats.end_time)); + } else { + sammsges.push(total_event_record); + } + + let header = vec!["Count", "Percent", "Channel", "ID", "Event"]; + let target; + let mut wtr = if let Some(csv_path) = &CONFIG.read().unwrap().args.output { + // output to file + match File::create(csv_path) { + Ok(file) => { + target = Box::new(BufWriter::new(file)); + Some(WriterBuilder::new().from_writer(target)) + } + Err(err) => { + AlertMessage::alert(&format!("Failed to open file. {}", err)).ok(); + process::exit(1); + } + } + } else { + None + }; + if let Some(ref mut w) = wtr { + w.write_record(&header).ok(); + } + + let mut stats_tb = Table::new(); + stats_tb + .load_preset(UTF8_FULL) + .apply_modifier(UTF8_ROUND_CORNERS); + stats_tb.set_header(header); // 集計件数でソート let mut mapsorted: Vec<_> = self.stats.stats_list.iter().collect(); mapsorted.sort_by(|x, y| y.1.cmp(x.1)); + // イベントID毎の出力メッセージ生成 + let stats_msges: Vec> = self.tm_stats_set_msg(mapsorted); + for msgprint in sammsges.iter() { println!("{}", msgprint); } - // イベントID毎の出力メッセージ生成 - self.tm_stats_set_msg(mapsorted); + if CONFIG.read().unwrap().args.output.is_some() { + for msg in stats_msges.iter() { + if let Some(ref mut w) = wtr { + w.write_record(msg).ok(); + } + } + } + stats_tb.add_rows(stats_msges); + println!("{stats_tb}"); } pub fn tm_logon_stats_dsp_msg(&mut self) { @@ -64,12 +112,15 @@ impl Timeline { } // 出力メッセージ作成 let mut sammsges: Vec = Vec::new(); - sammsges.push("---------------------------------------".to_string()); - sammsges.push(format!("Evtx File Path: {}", self.stats.filepath)); - sammsges.push(format!("Total Event Records: {}\n", self.stats.total)); - sammsges.push(format!("First Timestamp: {}", self.stats.start_time)); - sammsges.push(format!("Last Timestamp: {}\n", self.stats.end_time)); - sammsges.push("---------------------------------------".to_string()); + let total_event_record = format!("\nTotal Event Records: {}\n", self.stats.total); + if CONFIG.read().unwrap().args.filepath.is_some() { + sammsges.push(format!("Evtx File Path: {}", self.stats.filepath)); + sammsges.push(total_event_record); + sammsges.push(format!("First Timestamp: {}", self.stats.start_time)); + sammsges.push(format!("Last Timestamp: {}\n", self.stats.end_time)); + } else { + sammsges.push(total_event_record); + } for msgprint in sammsges.iter() { println!("{}", msgprint); } @@ -78,11 +129,13 @@ impl Timeline { } // イベントID毎の出力メッセージ生成 - fn tm_stats_set_msg(&self, mapsorted: Vec<(&std::string::String, &usize)>) { - let mut eid_metrics_tb = Table::new(); - eid_metrics_tb.set_header(vec!["Count", "Percent(%)", "channel,ID", "Eventtitle"]); + fn tm_stats_set_msg( + &self, + mapsorted: Vec<(&(std::string::String, std::string::String), &usize)>, + ) -> Vec> { + let mut msges: Vec> = Vec::new(); - for (event_id, event_cnt) in mapsorted.iter() { + for ((event_id, channel), event_cnt) in mapsorted.iter() { // 件数の割合を算出 let rate: f32 = **event_cnt as f32 / self.stats.total as f32; @@ -98,38 +151,44 @@ impl Timeline { .read() .unwrap() .event_timeline_config - .get_event_id(*event_id) + .get_event_id(event_id) .is_some(); // event_id_info.txtに登録あるものは情報設定 + // 出力メッセージ1行作成 + let fmted_channel = channel.replace('\"', ""); + let ch = CH_CONFIG + .get(fmted_channel.to_lowercase().as_str()) + .unwrap_or(&fmted_channel) + .to_string(); if conf { - eid_metrics_tb.add_row(vec![ - Cell::new(&event_cnt), - Cell::new(&rate), - Cell::new(&event_id), - Cell::new( - &CONFIG - .read() - .unwrap() - .event_timeline_config - .get_event_id(*event_id) - .unwrap() - .evttitle, - ), + msges.push(vec![ + event_cnt.to_string(), + format!("{:.1}%", (rate * 1000.0).round() / 10.0), + ch, + event_id.to_string(), + CONFIG + .read() + .unwrap() + .event_timeline_config + .get_event_id(event_id) + .unwrap() + .evttitle + .to_string(), ]); } else { - // 出力メッセージ1行作成 - eid_metrics_tb.add_row(vec![ - Cell::new(&event_cnt), - Cell::new(&rate), - Cell::new(&event_id), - Cell::new(&"Unknown".to_string()), + msges.push(vec![ + event_cnt.to_string(), + format!("{:.1}%", (rate * 1000.0).round() / 10.0), + ch, + event_id.replace('\"', ""), + "Unknown".to_string(), ]); } } - println!("{eid_metrics_tb}"); - println!(); + msges } - // ユーザ毎のログイン統計情報出力メッセージ生成 + + /// ユーザ毎のログイン統計情報出力メッセージ生成 fn tm_loginstats_tb_set_msg(&self) { println!("Logon Summary"); if self.stats.stats_login_list.is_empty() { @@ -141,23 +200,45 @@ impl Timeline { println!("{}", msgprint); } } else { + let header = vec!["User", "Failed", "Successful"]; + let target; + let mut wtr = if let Some(csv_path) = &CONFIG.read().unwrap().args.output { + // output to file + match File::create(csv_path) { + Ok(file) => { + target = Box::new(BufWriter::new(file)); + Some(WriterBuilder::new().from_writer(target)) + } + Err(err) => { + AlertMessage::alert(&format!("Failed to open file. {}", err)).ok(); + process::exit(1); + } + } + } else { + None + }; + if let Some(ref mut w) = wtr { + w.write_record(&header).ok(); + } + let mut logins_stats_tb = Table::new(); - logins_stats_tb.set_header(vec!["User", "Failed", "Successful"]); + logins_stats_tb + .load_preset(UTF8_FULL) + .apply_modifier(UTF8_ROUND_CORNERS); + logins_stats_tb.set_header(&header); // 集計件数でソート let mut mapsorted: Vec<_> = self.stats.stats_login_list.iter().collect(); mapsorted.sort_by(|x, y| x.0.cmp(y.0)); for (key, values) in &mapsorted { let mut username: String = key.to_string(); - //key.to_string().retain(|c| c != '\"'); - //key.to_string().pop(); username.pop(); username.remove(0); - logins_stats_tb.add_row(vec![ - Cell::new(&username), - Cell::new(&values[1].to_string()), - Cell::new(&values[0].to_string()), - ]); + let record_data = vec![username, values[1].to_string(), values[0].to_string()]; + if let Some(ref mut w) = wtr { + w.write_record(&record_data).ok(); + } + logins_stats_tb.add_row(record_data); } println!("{logins_stats_tb}"); println!();