diff --git a/.gitignore b/.gitignore
index eec35107..cdf50523 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,6 @@ test_*
.env
/logs
*.csv
+*.json
+*.jsonl
hayabusa*
\ No newline at end of file
diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md
index 87180caa..e06b8a38 100644
--- a/CHANGELOG-Japanese.md
+++ b/CHANGELOG-Japanese.md
@@ -1,16 +1,22 @@
# 変更点
-## v1.6.0 [2022/XX/XX]
+## v1.6.0 [2022/09/16]
**新機能:**
-- XXX
+- 解析結果をJSONに出力する機能(`-j, --json-timeline`)を追加した。 (#654) (@hitenkoku)
+- 解析結果をJSONL形式で出力する機能 (`-J, --jsonl` )を追加した。 (#694) (@hitenkoku)
**改善:**
- 結果概要に各レベルで検知した上位5つのルールを表示するようにした。 (#667) (@hitenkoku)
- 結果概要を出力しないようにするために `--no-summary` オプションを追加した。 (#672) (@hitenkoku)
- 結果概要の表示を短縮させた。 (#675 #678) (@hitenkoku)
+- channel_abbreviations.txtによるChannelフィールドのチェックを大文字小文字の区別をなくした。 (#685) (@hitenkoku)
+- 出力結果の区切り文字を`|`から`‖`に変更した。 (#687) (@hitenkoku)
+- 結果概要の検知数と総イベント数の数に色付けを行い見やすくした。 (#690) (@hitenkoku)
+- evtxクレートを0.8.0にアップデート。(ヘッダーや日付の値が無効な場合の処理が改善された。)
+- 出力プロファイルの更新。(@YamatoSecurity)
**バグ修正:**
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b02f742..32092666 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,16 +1,22 @@
# Changes
-## v1.6.0 [2022/XX/XX]
+## v1.6.0 [2022/09/16]
**New Features:**
-- XXX
+- You can now save the timeline to JSON files with the `-j, --json` option. (#654) (@hitenkoku)
+- You can now save the timeline to JSONL files with the `-J, --jsonl` option. (#694) (@hitenkoku)
**Enhancements:**
- Added top alerts to results summary. (#667) (@hitenkoku)
- Added `--no-summary` option to not display the results summary. (#672) (@hitenkoku)
- Made the results summary more compact. (#675 #678) (@hitenkoku)
+- Made Channel field in channel_abbreviations.txt case-insensitive. (#685) (@hitenkoku)
+- Changed pipe separator character in output from `|` to `‖`. (#687) (@hitenkoku)
+- Added color to Saved alerts and events / Total events analyzed. (#690) (@hitenkoku)
+- Updated evtx crate to 0.8.0. (better handling when headers or date values are invalid.)
+- Updated output profiles. (@YamatoSecurity)
**Bug Fixes:**
@@ -37,6 +43,7 @@
- Customizable output of fields defined at `config/profiles.yaml` and `config/default_profile.yaml`. (#165) (@hitenkoku)
- Implemented the `null` keyword for rule detection. It is used to check if a target field exists or not. (#643) (@hitenkoku)
+- Added output to JSON option (`-j` and `--json-timeline` ) (#654) (@hitenkoku)
**Enhancements:**
diff --git a/Cargo.lock b/Cargo.lock
index 95d822b4..ed2c707e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -21,36 +21,27 @@ dependencies = [
[[package]]
name = "aho-corasick"
-version = "0.7.18"
+version = "0.7.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e"
dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
-version = "0.1.4"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
-[[package]]
-name = "ansi_term"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
-dependencies = [
- "winapi",
-]
-
[[package]]
name = "anyhow"
-version = "1.0.62"
+version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1485d4d2cc45e7b201ee3767015c96faa5904387c9d87c6efdd0fb511f12d305"
+checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
[[package]]
name = "arrayvec"
@@ -164,7 +155,7 @@ checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa"
dependencies = [
"camino",
"cargo-platform",
- "semver 1.0.13",
+ "semver 1.0.14",
"serde",
"serde_json",
]
@@ -202,24 +193,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "2.34.0"
+version = "3.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
-dependencies = [
- "ansi_term",
- "atty",
- "bitflags",
- "strsim 0.8.0",
- "textwrap 0.11.0",
- "unicode-width",
- "vec_map",
-]
-
-[[package]]
-name = "clap"
-version = "3.2.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29e724a68d9319343bb3328c9cc2dfde263f4b3142ee1059a9980580171c954b"
+checksum = "1ed5341b2301a26ab80be5cbdced622e80ed808483c52e45e3310a877d3b37d7"
dependencies = [
"atty",
"bitflags",
@@ -227,16 +203,16 @@ dependencies = [
"clap_lex",
"indexmap",
"once_cell",
- "strsim 0.10.0",
+ "strsim",
"termcolor",
- "textwrap 0.15.0",
+ "textwrap",
]
[[package]]
name = "clap_derive"
-version = "3.2.17"
+version = "3.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13547f7012c01ab4a0e8f8967730ada8f9fdf419e8b6c792788f39cf4e46eefa"
+checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
dependencies = [
"heck",
"proc-macro-error",
@@ -395,24 +371,24 @@ dependencies = [
[[package]]
name = "dashmap"
-version = "5.3.4"
+version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3495912c9c1ccf2e18976439f4443f3fee0fd61f424ff99fde6a66b15ecb448f"
+checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
dependencies = [
"cfg-if",
"hashbrown",
"lock_api",
+ "once_cell",
"parking_lot_core",
]
[[package]]
name = "dialoguer"
-version = "0.9.0"
+version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61579ada4ec0c6031cfac3f86fdba0d195a7ebeb5e36693bd53cb5999a25beeb"
+checksum = "a92e7e37ecef6857fdc0c0c5d42fd5b0938e46590c2183cc92dd310a6d078eb1"
dependencies = [
"console",
- "lazy_static",
"tempfile",
"zeroize",
]
@@ -577,21 +553,22 @@ dependencies = [
[[package]]
name = "evtx"
-version = "0.7.3"
-source = "git+https://github.com/Yamato-Security/hayabusa-evtx.git#f2689c0343d0487521b9572dc3b9e4c179bcc5c9"
+version = "0.8.2"
+source = "git+https://github.com/Yamato-Security/hayabusa-evtx.git?rev=95b1c6a#95b1c6a1eebe6e2dc7be896974e92e912ddb6780"
dependencies = [
"anyhow",
"bitflags",
"byteorder",
"chrono",
- "clap 2.34.0",
+ "clap",
"crc32fast",
"dialoguer",
"encoding",
+ "hashbrown",
"indoc",
"jemallocator",
"log",
- "quick-xml 0.23.0",
+ "quick-xml",
"rayon",
"rpmalloc",
"serde",
@@ -654,11 +631,10 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
-version = "1.0.1"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
dependencies = [
- "matches",
"percent-encoding",
]
@@ -670,30 +646,30 @@ checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394"
[[package]]
name = "futures-channel"
-version = "0.3.23"
+version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2bfc52cbddcfd745bf1740338492bb0bd83d76c67b445f91c5fb29fae29ecaa1"
+checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
-version = "0.3.23"
+version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2acedae88d38235936c3922476b10fced7b2b68136f5e3c03c2d5be348a1115"
+checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf"
[[package]]
name = "futures-task"
-version = "0.3.23"
+version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "842fc63b931f4056a24d59de13fb1272134ce261816e063e634ad0c15cdc5306"
+checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1"
[[package]]
name = "futures-util"
-version = "0.3.23"
+version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0828a5471e340229c11c77ca80017937ce3c58cb788a17e5f1c2d5c485a9577"
+checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90"
dependencies = [
"futures-core",
"futures-task",
@@ -744,12 +720,12 @@ dependencies = [
[[package]]
name = "hayabusa"
-version = "1.6.0-dev"
+version = "1.6.0"
dependencies = [
"base64",
"bytesize",
"chrono",
- "clap 3.2.17",
+ "clap",
"comfy-table",
"crossbeam-utils",
"csv",
@@ -773,7 +749,7 @@ dependencies = [
"openssl",
"pbr",
"prettytable-rs",
- "quick-xml 0.24.0",
+ "quick-xml",
"rand",
"regex",
"serde",
@@ -841,9 +817,9 @@ dependencies = [
[[package]]
name = "httparse"
-version = "1.7.1"
+version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "httpdate"
@@ -884,24 +860,24 @@ dependencies = [
[[package]]
name = "iana-time-zone"
-version = "0.1.46"
+version = "0.1.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501"
+checksum = "237a0714f28b1ee39ccec0770ccb544eb02c9ef2c82bb096230eefcffa6468b0"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"js-sys",
+ "once_cell",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "idna"
-version = "0.2.3"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
dependencies = [
- "matches",
"unicode-bidi",
"unicode-normalization",
]
@@ -960,9 +936,9 @@ dependencies = [
[[package]]
name = "itertools"
-version = "0.10.3"
+version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
+checksum = "d8bf247779e67a9082a4790b45e71ac7cfd1321331a5c856a74a9faebdab78d0"
dependencies = [
"either",
]
@@ -1011,9 +987,9 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.59"
+version = "0.3.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2"
+checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
dependencies = [
"wasm-bindgen",
]
@@ -1027,7 +1003,7 @@ dependencies = [
"anyhow",
"atty",
"chrono",
- "clap 3.2.17",
+ "clap",
"file-chunker",
"indicatif",
"memmap2",
@@ -1122,12 +1098,6 @@ dependencies = [
"cfg-if",
]
-[[package]]
-name = "matches"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
-
[[package]]
name = "memchr"
version = "2.5.0"
@@ -1154,9 +1124,9 @@ dependencies = [
[[package]]
name = "miniz_oxide"
-version = "0.5.3"
+version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc"
+checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34"
dependencies = [
"adler",
]
@@ -1229,6 +1199,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "num_threads"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "number_prefix"
version = "0.4.0"
@@ -1237,9 +1216,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "once_cell"
-version = "1.13.1"
+version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e"
+checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0"
[[package]]
name = "openssl"
@@ -1339,9 +1318,9 @@ dependencies = [
[[package]]
name = "percent-encoding"
-version = "2.1.0"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "pin-project-lite"
@@ -1445,18 +1424,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-xml"
-version = "0.23.0"
+version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9279fbdacaad3baf559d8cabe0acc3d06e30ea14931af31af79578ac0946decc"
-dependencies = [
- "memchr",
-]
-
-[[package]]
-name = "quick-xml"
-version = "0.24.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "678404d55890514fa1c01fe98cf280b674db93944fdcb70310dd3be1d0d63be7"
+checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea"
dependencies = [
"memchr",
"serde",
@@ -1494,9 +1464,9 @@ dependencies = [
[[package]]
name = "rand_core"
-version = "0.6.3"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
@@ -1658,9 +1628,9 @@ dependencies = [
[[package]]
name = "semver"
-version = "1.0.13"
+version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711"
+checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4"
dependencies = [
"serde",
]
@@ -1749,13 +1719,13 @@ dependencies = [
[[package]]
name = "simplelog"
-version = "0.10.2"
+version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85d04ae642154220ef00ee82c36fb07853c10a4f2a0ca6719f9991211d2eb959"
+checksum = "48dfff04aade74dd495b007c831cd6f4e0cee19c344dd9dc0884c0289b70a786"
dependencies = [
- "chrono",
"log",
"termcolor",
+ "time 0.3.14",
]
[[package]]
@@ -1781,9 +1751,9 @@ checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
[[package]]
name = "socket2"
-version = "0.4.6"
+version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10c98bba371b9b22a71a9414e420f92ddeb2369239af08200816169d5e2dd7aa"
+checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
dependencies = [
"libc",
"winapi",
@@ -1853,12 +1823,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
-[[package]]
-name = "strsim"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
-
[[package]]
name = "strsim"
version = "0.10.0"
@@ -1951,33 +1915,24 @@ dependencies = [
[[package]]
name = "textwrap"
-version = "0.11.0"
+version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
-dependencies = [
- "unicode-width",
-]
-
-[[package]]
-name = "textwrap"
-version = "0.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
+checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
[[package]]
name = "thiserror"
-version = "1.0.32"
+version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994"
+checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
-version = "1.0.32"
+version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21"
+checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783"
dependencies = [
"proc-macro2",
"quote",
@@ -2005,11 +1960,23 @@ dependencies = [
"libc",
"standback",
"stdweb",
- "time-macros",
+ "time-macros 0.1.1",
"version_check",
"winapi",
]
+[[package]]
+name = "time"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b"
+dependencies = [
+ "itoa 1.0.3",
+ "libc",
+ "num_threads",
+ "time-macros 0.2.4",
+]
+
[[package]]
name = "time-macros"
version = "0.1.1"
@@ -2020,6 +1987,12 @@ dependencies = [
"time-macros-impl",
]
+[[package]]
+name = "time-macros"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
+
[[package]]
name = "time-macros-impl"
version = "0.1.2"
@@ -2050,9 +2023,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
-version = "1.20.1"
+version = "1.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581"
+checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95"
dependencies = [
"autocfg",
"bytes",
@@ -2129,9 +2102,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]]
name = "unicode-ident"
-version = "1.0.3"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
+checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
[[package]]
name = "unicode-normalization"
@@ -2144,19 +2117,18 @@ dependencies = [
[[package]]
name = "unicode-width"
-version = "0.1.9"
+version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "url"
-version = "2.2.2"
+version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
+checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
dependencies = [
"form_urlencoded",
"idna",
- "matches",
"percent-encoding",
]
@@ -2166,12 +2138,6 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
-[[package]]
-name = "vec_map"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
-
[[package]]
name = "version_check"
version = "0.9.4"
@@ -2213,9 +2179,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
-version = "0.2.82"
+version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d"
+checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
@@ -2223,9 +2189,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
-version = "0.2.82"
+version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f"
+checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
dependencies = [
"bumpalo",
"log",
@@ -2238,9 +2204,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.82"
+version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602"
+checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -2248,9 +2214,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.82"
+version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da"
+checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
dependencies = [
"proc-macro2",
"quote",
@@ -2261,9 +2227,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.82"
+version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a"
+checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
[[package]]
name = "winapi"
diff --git a/Cargo.toml b/Cargo.toml
index bbe08346..b672d7c7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "hayabusa"
-version = "1.6.0-dev"
+version = "1.6.0"
authors = ["Yamato Security @SecurityYamato"]
edition = "2021"
@@ -8,8 +8,8 @@ edition = "2021"
itertools = "*"
dashmap = "*"
clap = { version = "3.*", features = ["derive", "cargo"]}
-evtx = { git = "https://github.com/Yamato-Security/hayabusa-evtx.git" , features = ["fast-alloc"]}
-quick-xml = {version = "0.*", features = ["serialize"] }
+evtx = { git = "https://github.com/Yamato-Security/hayabusa-evtx.git" , features = ["fast-alloc"] , rev = "95b1c6a" }
+quick-xml = {version = "0.23.*", features = ["serialize"] }
serde = { version = "1.*", features = ["derive"] }
serde_json = { version = "1.0"}
serde_derive = "1.*"
diff --git a/README-1.5.1-Japanese.pdf b/README-1.6.0-Japanese.pdf
similarity index 76%
rename from README-1.5.1-Japanese.pdf
rename to README-1.6.0-Japanese.pdf
index 76f89c18..1df027d5 100644
Binary files a/README-1.5.1-Japanese.pdf and b/README-1.6.0-Japanese.pdf differ
diff --git a/README-1.5.1.pdf b/README-1.6.0.pdf
similarity index 72%
rename from README-1.5.1.pdf
rename to README-1.6.0.pdf
index 16c4800a..d6be240a 100644
Binary files a/README-1.5.1.pdf and b/README-1.6.0.pdf differ
diff --git a/README-Japanese.md b/README-Japanese.md
index f3f19cb7..afef4813 100644
--- a/README-Japanese.md
+++ b/README-Japanese.md
@@ -14,8 +14,9 @@
[tag-5]: https://rust-reportcard.xuri.me/badge/github.com/Yamato-Security/hayabusa
[tag-6]: https://img.shields.io/badge/Maintenance%20Level-Actively%20Developed-brightgreen.svg
[tag-7]: https://img.shields.io/badge/Twitter-00acee?logo=twitter&logoColor=white
+[tag-8]: https://img.shields.io/badge/CODE%20BLUE%20Bluebox-2022-blue
-![tag-1] ![tag-2] ![tag-3] ![tag-4]
+![tag-1] ![tag-2] ![tag-3] ![tag-4] ![tag-8]
![tag-5] ![tag-6] ![tag-7]
@@ -68,14 +69,17 @@ Hayabusaは、日本の[Yamato Security](https://yamatosecurity.connpass.com/)
- [1. `minimal`プロファイルの出力](#1-minimalプロファイルの出力)
- [2. `standard`プロファイルの出力](#2-standardプロファイルの出力)
- [3. `verbose`プロファイルの出力](#3-verboseプロファイルの出力)
- - [4. `verbose-all-field-info`プロファイルの出力](#4-verbose-all-field-infoプロファイルの出力)
- - [5. `verbose-details-and-all-field-info`プロファイルの出力](#5-verbose-details-and-all-field-infoプロファイルの出力)
- - [6. `timesketch`プロファイルの出力](#6-timesketchプロファイルの出力)
+ - [4. `all-field-info`プロファイルの出力](#4-all-field-infoプロファイルの出力)
+ - [5. `all-field-info-verbose`プロファイルの出力](#5-all-field-info-verboseプロファイルの出力)
+ - [6. `super-verbose`プロファイルの出力](#6-super-verboseプロファイルの出力)
+ - [7. `timesketch`プロファイルの出力](#7-timesketchプロファイルの出力)
+ - [8. `timesketch`プロファイルの出力](#8-timesketchプロファイルの出力)
- [プロファイルの比較](#プロファイルの比較)
- [Profile Field Aliases](#profile-field-aliases)
- [Levelの省略](#levelの省略)
- [MITRE ATT&CK戦術の省略](#mitre-attck戦術の省略)
- [Channel情報の省略](#channel情報の省略)
+- [その他のの省略](#その他のの省略)
- [プログレスバー](#プログレスバー)
- [標準出力へのカラー設定](#標準出力へのカラー設定)
- [結果のサマリ](#結果のサマリ)
@@ -182,6 +186,7 @@ CSVのタイムラインをTimesketchにインポートする方法は[こちら
* 詳細な調査のために全フィールド情報の出力。
* 成功と失敗したユーザログオンの要約。
* [Velociraptor](https://docs.velociraptor.app/)と組み合わせた企業向けの広範囲なすべてのエンドポイントに対するスレットハンティングとDFIR。
+* CSV、JSON、JSONLの出力。
# ダウンロード
@@ -202,7 +207,7 @@ git clone https://github.com/Yamato-Security/hayabusa.git --recursive
`git pull --recurse-submodules`コマンド、もしくは以下のコマンドで`rules`フォルダを同期し、Hayabusaの最新のルールを更新することができます:
```bash
-hayabusa-1.5.1-win-x64.exe -u
+hayabusa-1.6.0-win-x64.exe -u
```
アップデートが失敗した場合は、`rules`フォルダの名前を変更してから、もう一回アップデートしてみて下さい。
@@ -307,20 +312,20 @@ Windows PC起動後の初回実行時に時間がかかる場合があります
コマンドプロンプトやWindows Terminalから32ビットもしくは64ビットのWindowsバイナリをHayabusaのルートディレクトリから実行します。
-例: `hayabusa-1.5.1-windows-x64.exe`
+例: `hayabusa-1.6.0-windows-x64.exe`
## Linux
まず、バイナリに実行権限を与える必要があります。
```bash
-chmod +x ./hayabusa-1.5.1-linux-x64-gnu
+chmod +x ./hayabusa-1.6.0-linux-x64-gnu
```
次に、Hayabusaのルートディレクトリから実行します:
```bash
-./hayabusa-1.5.1-linux-x64-gnu
+./hayabusa-1.6.0-linux-x64-gnu
```
## macOS
@@ -328,13 +333,13 @@ chmod +x ./hayabusa-1.5.1-linux-x64-gnu
まず、ターミナルやiTerm2からバイナリに実行権限を与える必要があります。
```bash
-chmod +x ./hayabusa-1.5.1-mac-intel
+chmod +x ./hayabusa-1.6.0-mac-intel
```
次に、Hayabusaのルートディレクトリから実行してみてください:
```bash
-./hayabusa-1.5.1-mac-intel
+./hayabusa-1.6.0-mac-intel
```
macOSの最新版では、以下のセキュリティ警告が出る可能性があります:
@@ -348,7 +353,7 @@ macOSの環境設定から「セキュリティとプライバシー」を開き
その後、ターミナルからもう一回実行してみてください:
```bash
-./hayabusa-1.5.1-mac-intel
+./hayabusa-1.6.0-mac-intel
```
以下の警告が出るので、「開く」をクリックしてください。
@@ -358,6 +363,7 @@ macOSの環境設定から「セキュリティとプライバシー」を開き
これで実行できるようになります。
# 使用方法
+
## 主なコマンド
* デフォルト: ファストフォレンジックタイムラインの作成。
@@ -387,6 +393,8 @@ ADVANCED:
--target-file-ext ... evtx以外の拡張子を解析対象に追加する。 (例1: evtx_data 例2:evtx1 evtx2)
OUTPUT:
+ -j, --json タイムラインの出力をJSON形式で保存する(例: -j -o results.json)
+ -J, --jsonl タイムラインの出力をJSONL形式で保存する (例: -J -o results.jsonl)
-o, --output タイムラインをCSV形式で保存する (例: results.csv)
-P, --profile 利用する出力プロファイル名を指定する (minimal, standard, verbose, verbose-all-field-info, verbose-details-and-all-field-info)
@@ -407,13 +415,13 @@ FILTERING:
--timeline-start 解析対象とするイベントログの開始時刻 (例: "2020-02-22 00:00:00 +09:00")
OTHER-ACTIONS:
- --contributors コントリビュータの一覧表示
- -L, --logon-summary 成功と失敗したログオン情報の要約を出力する
+ --contributors コントリビュータの一覧表示
+ -L, --logon-summary 成功と失敗したログオン情報の要約を出力する
--level-tuning [] ルールlevelのチューニング (デフォルト: ./rules/config/level_tuning.txt)
- -p, --pivot-keywords-list ピボットキーワードの一覧作成
- -s, --statistics イベントIDの統計情報を表示する
- --set-default-profile デフォルトの出力コンフィグを設定する
- -u, --update-rules rulesフォルダをhayabusa-rulesのgithubリポジトリの最新版に更新する
+ -p, --pivot-keywords-list ピボットキーワードの一覧作成
+ -s, --statistics イベントIDの統計情報を表示する
+ --set-default-profile デフォルトの出力コンフィグを設定する
+ -u, --update-rules rulesフォルダをhayabusa-rulesのgithubリポジトリの最新版に更新する
TIME-FORMAT:
--European-time ヨーロッパ形式で日付と時刻を出力する (例: 22-02-2022 22:00:00.123 +02:00)
@@ -429,84 +437,90 @@ TIME-FORMAT:
* 1つのWindowsイベントログファイルに対してHayabusaを実行する:
```bash
-hayabusa-1.5.1-win-x64.exe -f eventlog.evtx
+hayabusa-1.6.0-win-x64.exe -f eventlog.evtx
```
* `verbose`プロファイルで複数のWindowsイベントログファイルのあるsample-evtxディレクトリに対して、Hayabusaを実行する:
```bash
-hayabusa-1.5.1-win-x64.exe -d .\hayabusa-sample-evtx -P verbose
+hayabusa-1.6.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.5.1-win-x64.exe -d .\hayabusa-sample-evtx -o results.csv -P `verbose-details-and-all-field-info`
+hayabusa-1.6.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ルールのみを実行する(デフォルトでは`-r .\rules`にあるすべてのルールが利用される):
```bash
-hayabusa-1.5.1-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa -o results.csv
+hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa -o results.csv
```
* Windowsでデフォルトで有効になっているログに対してのみ、Hayabusaルールを実行する:
```bash
-hayabusa-1.5.1-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\default -o results.csv
+hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\default -o results.csv
```
* Sysmonログに対してのみHayabusaルールを実行する:
```bash
-hayabusa-1.5.1-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\sysmon -o results.csv
+hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\sysmon -o results.csv
```
* Sigmaルールのみを実行する:
```bash
-hayabusa-1.5.1-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\sigma -o results.csv
+hayabusa-1.6.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.5.1-win-x64.exe -d .\hayabusa-sample-evtx --enable-deprecated-rules --enable-noisy-rules -o results.csv
+hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx --enable-deprecated-rules --enable-noisy-rules -o results.csv
```
* ログオン情報を分析するルールのみを実行し、UTCタイムゾーンで出力する:
```bash
-hayabusa-1.5.1-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\default\events\Security\Logons -U -o results.csv
+hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\default\events\Security\Logons -U -o results.csv
```
* 起動中のWindows端末上で実行し(Administrator権限が必要)、アラート(悪意のある可能性のある動作)のみを検知する:
```bash
-hayabusa-1.5.1-win-x64.exe -l -m low
+hayabusa-1.6.0-win-x64.exe -l -m low
```
* criticalレベルのアラートからピボットキーワードの一覧を作成する(結果は結果毎に`keywords-Ip Address.txt`や`keywords-Users.txt`等に出力される):
```bash
-hayabusa-1.5.1-win-x64.exe -l -m critical -p -o keywords
+hayabusa-1.6.0-win-x64.exe -l -m critical -p -o keywords
```
* イベントIDの統計情報を出力する:
```bash
-hayabusa-1.5.1-win-x64.exe -f Security.evtx -s
+hayabusa-1.6.0-win-x64.exe -f Security.evtx -s
```
* ログオンサマリを出力する:
```bash
-hayabusa-1.5.1-win-x64.exe -L -f Security.evtx -s
+hayabusa-1.6.0-win-x64.exe -L -f Security.evtx -s
```
* 詳細なメッセージを出力する(処理に時間がかかるファイル、パースエラー等を特定するのに便利):
```bash
-hayabusa-1.5.1-win-x64.exe -d .\hayabusa-sample-evtx -v
+hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -v
```
* Verbose出力の例:
@@ -527,7 +541,7 @@ Checking target evtx FilePath: "./hayabusa-sample-evtx/YamatoSecurity/T1218.004_
* 結果を[Timesketch](https://timesketch.org/)にインポートできるCSV形式に保存する:
```bash
-hayabusa-1.5.1-win-x64.exe -d ../hayabusa-sample-evtx --RFC-3339 -o timesketch-import.csv -P timesketch -U
+hayabusa-1.6.0-win-x64.exe -d ../hayabusa-sample-evtx --RFC-3339 -o timesketch-import.csv -P timesketch -U
```
* エラーログの出力をさせないようにする:
@@ -574,8 +588,11 @@ Hayabusaの`config/profiles.yaml`設定ファイルでは、5つのプロフ
1. `minimal`
2. `standard` (デフォルト)
3. `verbose`
-4. `verbose-all-field-info`
-5. `verbose-details-and-all-field-info`
+4. `all-field-info`
+5. `all-field-info-verbose`
+6. `super-verbose`
+7. `timesketch-minimal`
+8. `timesketch-verbose`
このファイルを編集することで、簡単に独自のプロファイルをカスタマイズしたり、追加したりすることができます。
`--set-default-profile `オプションでデフォルトのプロファイルを変更することもできます。
@@ -586,31 +603,43 @@ Hayabusaの`config/profiles.yaml`設定ファイルでは、5つのプロフ
### 2. `standard`プロファイルの出力
-`%Timestamp%`, `%Computer%`, `%Channel%`, `%EventID%`, `%Level%`, `%MitreTactics%`, `%RecordID%`, `%RuleTitle%`, `%Details%`
+`%Timestamp%`, `%Computer%`, `%Channel%`, `%EventID%`, `%Level%`, `%RecordID%`, `%RuleTitle%`, `%Details%`
### 3. `verbose`プロファイルの出力
`%Timestamp%`, `%Computer%`, `%Channel%`, `%EventID%`, `%Level%`, `%MitreTactics`, `%MitreTags%`, `%OtherTags%`, `%RecordID%`, `%RuleTitle%`, `%Details%`, `%RuleFile%`, `%EvtxFile%`
-### 4. `verbose-all-field-info`プロファイルの出力
+### 4. `all-field-info`プロファイルの出力
最小限の`details`情報を出力する代わりに、イベントにあるすべての`EventData`フィールド情報が出力されます。
+`%Timestamp%`, `%Computer%`, `%Channel%`, `%EventID%`, `%Level%`, `%RecordID%`, `%RuleTitle%`, `%AllFieldInfo%`, `%RuleFile%`, `%EvtxFile%`
+
+### 5. `all-field-info-verbose`プロファイルの出力
+
+`all-field-info`とタグ情報が出力されます。
+
`%Timestamp%`, `%Computer%`, `%Channel%`, `%EventID%`, `%Level%`, `%MitreTactics`, `%MitreTags%`, `%OtherTags%`, `%RecordID%`, `%RuleTitle%`, `%AllFieldInfo%`, `%RuleFile%`, `%EvtxFile%`
-### 5. `verbose-details-and-all-field-info`プロファイルの出力
+### 6. `super-verbose`プロファイルの出力
`verbose`プロファイルで出力される情報とイベントにあるすべての`EventData`フィールド情報が出力されます。
(注意: 出力ファイルサイズは2倍になります!)
`%Timestamp%`, `%Computer%`, `%Channel%`, `%EventID%`, `%Level%`, `%MitreTactics`, `%MitreTags%`, `%OtherTags%`, `%RecordID%`, `%RuleTitle%`, `%Details%`, `%RuleFile%`, `%EvtxFile%`, `%AllFieldInfo%`
-### 6. `timesketch`プロファイルの出力
+### 7. `timesketch`プロファイルの出力
[Timesketch](https://timesketch.org/)にインポートできる`verbose`プロファイル。
`%Timestamp%`, `hayabusa`, `%RuleTitle%`, `%Computer%`, `%Channel%`, `%EventID%`, `%Level%`, `%MitreTactics`, `%MitreTags%`, `%OtherTags%`, `%RecordID%`, `%Details%`, `%RuleFile%`, `%EvtxFile%`
+### 8. `timesketch`プロファイルの出力
+
+[Timesketch](https://timesketch.org/)にインポートできる`verbose`プロファイル。
+
+`%Timestamp%`, `hayabusa`, `%RuleTitle%`, `%Computer%`, `%Channel%`, `%EventID%`, `%Level%`, `%MitreTactics`, `%MitreTags%`, `%OtherTags%`, `%RecordID%`, `%Details%`, `%RuleFile%`, `%EvtxFile%`, `%AllFieldInfo%`
+
### プロファイルの比較
以下のベンチマークは、2018年製のマックブックプロ上で7.5GBのEVTXデータに対して実施されました。
@@ -620,9 +649,9 @@ Hayabusaの`config/profiles.yaml`設定ファイルでは、5つのプロフ
| minimal | 16分18秒 | 690 MB |
| standard | 16分23秒 | 710 MB |
| verbose | 17分 | 990 MB |
-| timesketch | 17分 | 1015 MB |
-| verbose-all-field-info | 16分50秒 | 1.6 GB |
-| verbose-details-and-all-field-info | 17分12秒 | 2.1 GB |
+| timesketch-minimal | 17分 | 1015 MB |
+| all-field-info-verbose | 16分50秒 | 1.6 GB |
+| super-verbose | 17分12秒 | 2.1 GB |
### Profile Field Aliases
@@ -710,6 +739,39 @@ Hayabusaの`config/profiles.yaml`設定ファイルでは、5つのプロフ
* `WinRM` : `Microsoft-Windows-WinRM/Operational`
* `WMI` : `Microsoft-Windows-WMI-Activity/Operational`
+# その他のの省略
+
+できるだけ簡潔にするために、以下の略語を使用しています:
+
+- `Acct` -> Account
+- `Addr` -> Address
+- `Auth` -> Authentication
+- `Cli` -> Client
+- `Cmd` -> Command
+- `Comp` -> Computer
+- `Conn` -> Connection
+- `Dir` -> Directory
+- `Dst` -> Destination
+- `Exec` -> Execution
+- `Grp` -> Group
+- `LID` -> Logon ID
+- `Net` -> Network
+- `Obj` -> Object
+- `Proto` -> Protocol
+- `Sig` -> Signature
+- `Susp` -> Suspicious
+- `Src` -> Source
+- `Svc` -> Service
+- `Svr` -> Server
+- `Tgt` -> Target
+- `Op` -> Operation
+- `Pkg` -> Package
+- `Priv` -> Privilege
+- `Proc` -> Process
+- `PID` -> Process ID
+- `PGUID` -> Process GUID (Global Unique ID)
+- `Ver` -> Version
+
## プログレスバー
プログレス・バーは、複数のevtxファイルに対してのみ機能します。
@@ -791,7 +853,7 @@ Hayabusaルールは、Windowsのイベントログ解析専用に設計され
## 検知レベルのlevelチューニング
Hayabusaルール、Sigmaルールはそれぞれの作者が検知した際のリスクレベルを決めています。
-ユーザが独自のリスクレベルに設定するには`./rules/config/level_tuning.txt`に変換情報を書き、`hayabusa-1.5.1-win-x64.exe --level-tuning`を実行することでルールファイルが書き換えられます。
+ユーザが独自のリスクレベルに設定するには`./rules/config/level_tuning.txt`に変換情報を書き、`hayabusa-1.6.0-win-x64.exe --level-tuning`を実行することでルールファイルが書き換えられます。
ルールファイルが直接書き換えられることに注意して使用してください。
`./rules/config/level_tuning.txt`の例:
@@ -824,7 +886,7 @@ id,new_level
* [EvtxToElk](https://www.dragos.com/blog/industry-news/evtxtoelk-a-python-module-to-load-windows-event-logs-into-elasticsearch/) - Elastic StackにEvtxデータを送信するPythonツール。
* [EVTX ATTACK Samples](https://github.com/sbousseaden/EVTX-ATTACK-SAMPLES) - [SBousseaden](https://twitter.com/SBousseaden) によるEVTX攻撃サンプルイベントログファイル。
* [EVTX-to-MITRE-Attack](https://github.com/mdecrevoisier/EVTX-to-MITRE-Attack) - [Michel de CREVOISIER](https://twitter.com/mdecrevoisier)によるATT&CKにマッピングされたEVTX攻撃サンプルログのレポジトリ。
-* [EVTX parser](https://github.com/omerbenamram/evtx) - [@OBenamram](https://twitter.com/obenamram) によって書かれた、私たちが使用したRustライブラリ。
+* [EVTX parser](https://github.com/omerbenamram/evtx) - [@OBenamram](https://twitter.com/obenamram) によって書かれた、Hayabusaが使用しているRustライブラリ。
* [Grafiki](https://github.com/lucky-luk3/Grafiki) - SysmonとPowerShellログの可視化ツール。
* [LogonTracer](https://github.com/JPCERTCC/LogonTracer) - [JPCERTCC](https://twitter.com/jpcert) による、横方向の動きを検知するためにログオンを視覚化するグラフィカルなインターフェース。
* [RustyBlue](https://github.com/Yamato-Security/RustyBlue) - 大和セキュリティによるDeepBlueCLIのRust版。
@@ -850,12 +912,15 @@ Windows機での悪性な活動を検知する為には、デフォルトのロ
フォレンジックに有用な証拠を作り、高い精度で検知をさせるためには、sysmonをインストールする必要があります。以下のサイトを参考に設定することをおすすめします。:
* [Sysmon Modular](https://github.com/olafhartong/sysmon-modular)
* [TrustedSec Sysmon Community Guide](https://github.com/trustedsec/SysmonCommunityGuide)
+* [SwiftOnSecurityのSysmon設定ファイル](https://github.com/SwiftOnSecurity/sysmon-config)
+* [Neo23x0によるSwiftOnSecurityのSysmon設定ファイルのフォーク](https://github.com/Neo23x0/sysmon-config)
+* [ion-stormによるSwiftOnSecurityのSysmon設定ファイルのフォーク](https://github.com/ion-storm/sysmon-config)
# コミュニティによるドキュメンテーション
## 英語
-* 2022/06/19 [VelociraptorチュートリアルとHayabusaの統合方法](https://www.youtube.com/watch?v=Q1IoGX--814) by [Eric Cupuano](https://twitter.com/eric_capuano)
+* 2022/06/19 [VelociraptorチュートリアルとHayabusaの統合方法](https://www.youtube.com/watch?v=Q1IoGX--814) by [Eric Capuano](https://twitter.com/eric_capuano)
* 2022/01/24 [Hayabusa結果をneo4jで可視化する方法](https://www.youtube.com/watch?v=7sQqz2ek-ko) by Matthew Seyer ([@forensic_matt](https://twitter.com/forensic_matt))
## 日本語
diff --git a/README.md b/README.md
index 2f2231c1..0bc30352 100644
--- a/README.md
+++ b/README.md
@@ -14,8 +14,9 @@
[tag-5]: https://rust-reportcard.xuri.me/badge/github.com/Yamato-Security/hayabusa
[tag-6]: https://img.shields.io/badge/Maintenance%20Level-Actively%20Developed-brightgreen.svg
[tag-7]: https://img.shields.io/badge/Twitter-00acee?logo=twitter&logoColor=white
+[tag-8]: https://img.shields.io/badge/CODE%20BLUE%20Bluebox-2022-blue
-![tag-1] ![tag-2] ![tag-3] ![tag-4]
+![tag-1] ![tag-2] ![tag-3] ![tag-4] ![tag-8]
![tag-5] ![tag-6] ![tag-7]
# About Hayabusa
@@ -66,14 +67,17 @@ Hayabusa is a **Windows event log fast forensics timeline generator** and **thre
- [1. `minimal` profile output](#1-minimal-profile-output)
- [2. `standard` profile output](#2-standard-profile-output)
- [3. `verbose` profile output](#3-verbose-profile-output)
- - [4. `verbose-all-field-info` profile output](#4-verbose-all-field-info-profile-output)
- - [5. `verbose-details-and-all-field-info` profile output](#5-verbose-details-and-all-field-info-profile-output)
- - [6. `timesketch` profile output](#6-timesketch-profile-output)
+ - [4. `all-field-info` profile output](#4-all-field-info-profile-output)
+ - [5. `all-field-info-verbose` profile output](#5-all-field-info-verbose-profile-output)
+ - [6. `super-verbose` profile output](#6-super-verbose-profile-output)
+ - [7. `timesketch-minimal` profile output](#7-timesketch-minimal-profile-output)
+ - [8. `timesketch-verbose` profile output](#8-timesketch-verbose-profile-output)
- [Profile Comparison](#profile-comparison)
- [Profile Field Aliases](#profile-field-aliases)
- [Level Abbrevations](#level-abbrevations)
- [MITRE ATT&CK Tactics Abbreviations](#mitre-attck-tactics-abbreviations)
- [Channel Abbreviations](#channel-abbreviations)
+- [Other Abbreviations](#other-abbreviations)
- [Progress Bar](#progress-bar)
- [Color Output](#color-output)
- [Results Summary](#results-summary-1)
@@ -168,12 +172,13 @@ You can learn how to import CSV files into Timesketch [here](doc/TimesketchImpor
* Currently it supports the most sigma rules compared to other similar tools and even supports count rules and new aggregators such as `|equalsfield`.
* Event log statistics. (Useful for getting a picture of what types of events there are and for tuning your log settings.)
* Rule tuning configuration by excluding unneeded or noisy rules.
-* MITRE ATT&CK mapping of tactics (only in saved CSV files).
+* MITRE ATT&CK mapping of tactics.
* Rule level tuning.
* Create a list of unique pivot keywords to quickly identify abnormal users, hostnames, processes, etc... as well as correlate events.
* Output all fields for more thorough investigations.
* Successful and failed logon summary.
* Enterprise-wide threat hunting and DFIR on all endpoints with [Velociraptor](https://docs.velociraptor.app/).
+* Output to CSV, JSON or JSONL.
# Downloads
@@ -194,7 +199,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.5.1-win-x64.exe -u
+hayabusa-1.6.0-win-x64.exe -u
```
If the update fails, you may need to rename the `rules` folder and try again.
@@ -299,20 +304,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.5.1-windows-x64.exe`
+Example: `hayabusa-1.6.0-windows-x64.exe`
## Linux
You first need to make the binary executable.
```bash
-chmod +x ./hayabusa-1.5.1-linux-x64-gnu
+chmod +x ./hayabusa-1.6.0-linux-x64-gnu
```
Then run it from the Hayabusa root directory:
```bash
-./hayabusa-1.5.1-linux-x64-gnu
+./hayabusa-1.6.0-linux-x64-gnu
```
## macOS
@@ -320,13 +325,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.5.1-mac-intel
+chmod +x ./hayabusa-1.6.0-mac-intel
```
Then, try to run it from the Hayabusa root directory:
```bash
-./hayabusa-1.5.1-mac-intel
+./hayabusa-1.6.0-mac-intel
```
On the latest version of macOS, you may receive the following security error when you try to run it:
@@ -340,7 +345,7 @@ Click "Cancel" and then from System Preferences, open "Security & Privacy" and f
After that, try to run it again.
```bash
-./hayabusa-1.5.1-mac-intel
+./hayabusa-1.6.0-mac-intel
```
The following warning will pop up, so please click "Open".
@@ -379,6 +384,8 @@ 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)
@@ -421,85 +428,91 @@ TIME-FORMAT:
* Run hayabusa against one Windows event log file with default standard profile:
```bash
-hayabusa-1.5.1-win-x64.exe -f eventlog.evtx
+hayabusa-1.6.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.5.1-win-x64.exe -d .\hayabusa-sample-evtx -P verbose
+hayabusa-1.6.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.5.1-win-x64.exe -d .\hayabusa-sample-evtx -o results.csv -F
+hayabusa-1.6.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
```
* Only run hayabusa rules (the default is to run all the rules in `-r .\rules`):
```bash
-hayabusa-1.5.1-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa -o results.csv
+hayabusa-1.6.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.5.1-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\default -o results.csv
+hayabusa-1.6.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.5.1-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\sysmon -o results.csv
+hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\sysmon -o results.csv
```
* Only run sigma rules:
```bash
-hayabusa-1.5.1-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\sigma -o results.csv
+hayabusa-1.6.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.5.1-win-x64.exe -d .\hayabusa-sample-evtx --enable-noisy-rules --enable-deprecated-rules -o results.csv
+hayabusa-1.6.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.5.1-win-x64.exe -d .\hayabusa-sample-evtx -r .\rules\hayabusa\default\events\Security\Logons -U -o results.csv
+hayabusa-1.6.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.5.1-win-x64.exe -l -m low
+hayabusa-1.6.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.5.1-win-x64.exe -l -m critical -p -o keywords
+hayabusa-1.6.0-win-x64.exe -l -m critical -p -o keywords
```
* Print Event ID statistics:
```bash
-hayabusa-1.5.1-win-x64.exe -f Security.evtx -s
+hayabusa-1.6.0-win-x64.exe -f Security.evtx -s
```
* Print logon summary:
```bash
-hayabusa-1.5.1-win-x64.exe -L -f Security.evtx -s
+hayabusa-1.6.0-win-x64.exe -L -f Security.evtx -s
```
* Print verbose information (useful for determining which files take long to process, parsing errors, etc...):
```bash
-hayabusa-1.5.1-win-x64.exe -d .\hayabusa-sample-evtx -v
+hayabusa-1.6.0-win-x64.exe -d .\hayabusa-sample-evtx -v
```
* Verbose output example:
@@ -520,7 +533,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.5.1-win-x64.exe -d ../hayabusa-sample-evtx --RFC-3339 -o timesketch-import.csv -P timesketch -U
+hayabusa-1.6.0-win-x64.exe -d ../hayabusa-sample-evtx --RFC-3339 -o timesketch-import.csv -P timesketch -U
```
* Quiet error mode:
@@ -569,8 +582,11 @@ Hayabusa has 5 pre-defined profiles to use in `config/profiles.yaml`:
1. `minimal`
2. `standard` (default)
3. `verbose`
-4. `verbose-all-field-info`
-5. `verbose-details-and-all-field-info`
+4. `all-field-info`
+5. `all-field-info-verbose`
+6. `super-verbose`
+7. `timesketch-minimal`
+8. `timesketch-verbose`
You can easily customize or add your own profiles by editing this file.
You can also easily change the default profile with `--set-default-profile `.
@@ -581,30 +597,42 @@ You can also easily change the default profile with `--set-default-profile Account
+- `Addr` -> Address
+- `Auth` -> Authentication
+- `Cli` -> Client
+- `Cmd` -> Command
+- `Comp` -> Computer
+- `Conn` -> Connection
+- `Dir` -> Directory
+- `Dst` -> Destination
+- `Exec` -> Execution
+- `Grp` -> Group
+- `LID` -> Logon ID
+- `Net` -> Network
+- `Obj` -> Object
+- `Proto` -> Protocol
+- `Sig` -> Signature
+- `Susp` -> Suspicious
+- `Src` -> Source
+- `Svc` -> Service
+- `Svr` -> Server
+- `Tgt` -> Target
+- `Op` -> Operation
+- `Pkg` -> Package
+- `Priv` -> Privilege
+- `Proc` -> Process
+- `PID` -> Process ID
+- `PGUID` -> Process GUID (Global Unique ID)
+- `Ver` -> Version
+
## Progress Bar
The progress bar will only work with multiple evtx files.
@@ -784,7 +845,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.5.1-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.6.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:
@@ -818,7 +879,7 @@ There is no "one tool to rule them all" and we have found that each has its own
* [EvtxToElk](https://www.dragos.com/blog/industry-news/evtxtoelk-a-python-module-to-load-windows-event-logs-into-elasticsearch/) - Python tool to send Evtx data to Elastic Stack.
* [EVTX ATTACK Samples](https://github.com/sbousseaden/EVTX-ATTACK-SAMPLES) - EVTX attack sample event log files by [SBousseaden](https://twitter.com/SBousseaden).
* [EVTX-to-MITRE-Attack](https://github.com/mdecrevoisier/EVTX-to-MITRE-Attack) - EVTX attack sample event log files mapped to ATT&CK by [Michel de CREVOISIER](https://twitter.com/mdecrevoisier)
-* [EVTX parser](https://github.com/omerbenamram/evtx) - the Rust library we used written by [@OBenamram](https://twitter.com/obenamram).
+* [EVTX parser](https://github.com/omerbenamram/evtx) - the Rust evtx library we use written by [@OBenamram](https://twitter.com/obenamram).
* [Grafiki](https://github.com/lucky-luk3/Grafiki) - Sysmon and PowerShell log visualizer.
* [LogonTracer](https://github.com/JPCERTCC/LogonTracer) - A graphical interface to visualize logons to detect lateral movement by [JPCERTCC](https://twitter.com/jpcert_en).
* [RustyBlue](https://github.com/Yamato-Security/RustyBlue) - Rust port of DeepBlueCLI by Yamato Security.
@@ -840,15 +901,18 @@ In order to properly detect malicious activity on Windows machines, you will nee
# Sysmon Related Projects
-To create the most forensic evidence and detect with the highest accuracy, you need to install sysmon. We recommend the following sites:
-* [Sysmon Modular](https://github.com/olafhartong/sysmon-modular)
+To create the most forensic evidence and detect with the highest accuracy, you need to install sysmon. We recommend the following sites and config files:
* [TrustedSec Sysmon Community Guide](https://github.com/trustedsec/SysmonCommunityGuide)
+* [Sysmon Modular](https://github.com/olafhartong/sysmon-modular)
+* [SwiftOnSecurity Sysmon Config](https://github.com/SwiftOnSecurity/sysmon-config)
+* [SwiftOnSecurity Sysmon Config fork by Neo23x0](https://github.com/Neo23x0/sysmon-config)
+* [SwiftOnSecurity Sysmon Config fork by ion-storm](https://github.com/ion-storm/sysmon-config)
# Community Documentation
## English
-* 2022/06/19 [Velociraptor Walkthrough and Hayabusa Integration](https://www.youtube.com/watch?v=Q1IoGX--814) by [Eric Cupuano](https://twitter.com/eric_capuano)
+* 2022/06/19 [Velociraptor Walkthrough and Hayabusa Integration](https://www.youtube.com/watch?v=Q1IoGX--814) by [Eric Capuano](https://twitter.com/eric_capuano)
* 2022/01/24 [Graphing Hayabusa results in neo4j](https://www.youtube.com/watch?v=7sQqz2ek-ko) by Matthew Seyer ([@forensic_matt](https://twitter.com/forensic_matt))
## Japanese
diff --git a/config/default_profile.yaml b/config/default_profile.yaml
index 394b6546..60e099aa 100644
--- a/config/default_profile.yaml
+++ b/config/default_profile.yaml
@@ -4,7 +4,6 @@ Computer: "%Computer%"
Channel: "%Channel%"
EventID: "%EventID%"
Level: "%Level%"
-MitreTactics: "%MitreTactics%"
RecordID: "%RecordID%"
RuleTitle: "%RuleTitle%"
Details: "%Details%"
\ No newline at end of file
diff --git a/config/profiles.yaml b/config/profiles.yaml
index de51b7f8..5ca50d43 100644
--- a/config/profiles.yaml
+++ b/config/profiles.yaml
@@ -14,7 +14,6 @@ standard:
Channel: "%Channel%"
EventID: "%EventID%"
Level: "%Level%"
- MitreTactics: "%MitreTactics%"
RecordID: "%RecordID%"
RuleTitle: "%RuleTitle%"
Details: "%Details%"
@@ -35,8 +34,21 @@ verbose:
RuleFile: "%RuleFile%"
EvtxFile: "%EvtxFile%"
-#Verbose profile with all field information instead of the minimal fields defined in Details.
-verbose-all-field-info:
+#Verbose profile with all field information instead of the minimal fields defined in the Details field.
+all-field-info:
+ Timestamp: "%Timestamp%"
+ Computer: "%Computer%"
+ Channel: "%Channel%"
+ EventID: "%EventID%"
+ Level: "%Level%"
+ RecordID: "%RecordID%"
+ RuleTitle: "%RuleTitle%"
+ AllFieldInfo: "%RecordInformation%"
+ RuleFile: "%RuleFile%"
+ EvtxFile: "%EvtxFile%"
+
+#Verbose profile with all field information and tags.
+all-field-info-verbose:
Timestamp: "%Timestamp%"
Computer: "%Computer%"
Channel: "%Channel%"
@@ -52,7 +64,7 @@ verbose-all-field-info:
EvtxFile: "%EvtxFile%"
#Verbose profile plus all field information. (Warning: this will more than double the output file size!)
-verbose-details-and-all-field-info:
+super-verbose:
Timestamp: "%Timestamp%"
Computer: "%Computer%"
Channel: "%Channel%"
@@ -69,7 +81,24 @@ verbose-details-and-all-field-info:
AllFieldInfo: "%RecordInformation%"
#Output that is compatible to import the CSV into Timesketch
-timesketch:
+timesketch-minimal:
+ datetime: "%Timestamp%"
+ timestamp_desc: "hayabusa"
+ message: "%RuleTitle%"
+ Computer: "%Computer%"
+ Channel: "%Channel%"
+ EventID: "%EventID%"
+ Level: "%Level%"
+ MitreTactics: "%MitreTactics%"
+ MitreTags: "%MitreTags%"
+ OtherTags: "%OtherTags%"
+ RecordID: "%RecordID%"
+ Details: "%Details%"
+ RuleFile: "%RuleFile%"
+ EvtxFile: "%EvtxFile%"
+
+#Output that is compatible to import the CSV into Timesketch
+timesketch-verbose:
datetime: "%Timestamp%"
timestamp_desc: "hayabusa"
message: "%RuleTitle%"
diff --git a/rules b/rules
index ff5732fa..fa75078d 160000
--- a/rules
+++ b/rules
@@ -1 +1 @@
-Subproject commit ff5732fa1788b1c2281fdc3ccaa0dd0301b030d8
+Subproject commit fa75078de6763374a4a4efd10d0d74dfa35241b1
diff --git a/screenshots/Hayabusa-Results.png b/screenshots/Hayabusa-Results.png
index 61c23587..ae827e8c 100644
Binary files a/screenshots/Hayabusa-Results.png and b/screenshots/Hayabusa-Results.png differ
diff --git a/screenshots/HayabusaResultsSummary.png b/screenshots/HayabusaResultsSummary.png
index 0d91938b..8595ba1d 100644
Binary files a/screenshots/HayabusaResultsSummary.png and b/screenshots/HayabusaResultsSummary.png differ
diff --git a/src/afterfact.rs b/src/afterfact.rs
index 076e5578..b8bc0ecb 100644
--- a/src/afterfact.rs
+++ b/src/afterfact.rs
@@ -9,11 +9,14 @@ use bytesize::ByteSize;
use chrono::{DateTime, Local, TimeZone, Utc};
use comfy_table::modifiers::UTF8_ROUND_CORNERS;
use comfy_table::presets::UTF8_FULL;
-use csv::QuoteStyle;
+
+use csv::{QuoteStyle, WriterBuilder};
+use dashmap::Map;
use itertools::Itertools;
use krapslog::{build_sparkline, build_time_markers};
use lazy_static::lazy_static;
use linked_hash_map::LinkedHashMap;
+use std::str::FromStr;
use comfy_table::*;
use hashbrown::{HashMap, HashSet};
@@ -189,7 +192,13 @@ pub fn after_fact(all_record_cnt: usize) {
Box::new(BufWriter::new(io::stdout()))
};
let color_map = set_output_color();
- if let Err(err) = emit_csv(&mut target, displayflag, color_map, all_record_cnt as u128) {
+ if let Err(err) = emit_csv(
+ &mut target,
+ displayflag,
+ color_map,
+ all_record_cnt as u128,
+ PROFILES.clone().unwrap_or_default(),
+ ) {
fn_emit_csv_err(Box::new(err));
}
}
@@ -199,10 +208,22 @@ fn emit_csv(
displayflag: bool,
color_map: HashMap,
all_record_cnt: u128,
+ profile: LinkedHashMap,
) -> io::Result<()> {
let disp_wtr = BufferWriter::stdout(ColorChoice::Always);
let mut disp_wtr_buf = disp_wtr.buffer();
- let mut wtr = csv::WriterBuilder::new().from_writer(writer);
+ let json_output_flag = configs::CONFIG.read().unwrap().args.json_timeline;
+ let jsonl_output_flag = configs::CONFIG.read().unwrap().args.jsonl_timeline;
+
+ let mut wtr = if json_output_flag || jsonl_output_flag {
+ WriterBuilder::new()
+ .delimiter(b'\n')
+ .double_quote(false)
+ .quote_style(QuoteStyle::Never)
+ .from_writer(writer)
+ } else {
+ WriterBuilder::new().from_writer(writer)
+ };
disp_wtr_buf.set_color(ColorSpec::new().set_fg(None)).ok();
@@ -231,11 +252,20 @@ fn emit_csv(
let mut timestamps: Vec = Vec::new();
let mut plus_header = true;
let mut detected_record_idset: HashSet = HashSet::new();
- for time in message::MESSAGES.clone().into_read_only().keys().sorted() {
+ if json_output_flag {
+ wtr.write_field("[")?;
+ }
+ for (processed_message_cnt, time) in message::MESSAGES
+ .clone()
+ .into_read_only()
+ .keys()
+ .sorted()
+ .enumerate()
+ {
let multi = message::MESSAGES.get(time).unwrap();
let (_, detect_infos) = multi.pair();
timestamps.push(_get_timestamp(time));
- for detect_info in detect_infos {
+ for (info_idx, detect_info) in detect_infos.iter().enumerate() {
if !detect_info.detail.starts_with("[condition]") {
detected_record_idset.insert(format!("{}_{}", time, detect_info.eventid));
}
@@ -245,7 +275,7 @@ fn emit_csv(
write_color_buffer(
&disp_wtr,
get_writable_color(None),
- &_get_serialized_disp_output(PROFILES.as_ref().unwrap(), true),
+ &_get_serialized_disp_output(&profile, true),
false,
)
.ok();
@@ -263,6 +293,27 @@ fn emit_csv(
false,
)
.ok();
+ } else if json_output_flag {
+ // JSON output
+ wtr.write_field(" {")?;
+ wtr.write_field(&output_json_str(
+ &detect_info.ext_field,
+ &profile,
+ jsonl_output_flag,
+ ))?;
+ if processed_message_cnt != message::MESSAGES._len() - 1
+ || info_idx != detect_infos.len() - 1
+ {
+ wtr.write_field(" },")?;
+ } else {
+ wtr.write_field(" }")?;
+ }
+ } else if jsonl_output_flag {
+ // JSONL output format
+ wtr.write_field(format!(
+ "{{ {} }}",
+ &output_json_str(&detect_info.ext_field, &profile, jsonl_output_flag)
+ ))?;
} else {
// csv output format
if plus_header {
@@ -332,6 +383,10 @@ fn emit_csv(
.insert(detect_info.level.to_lowercase(), detect_counts_by_date);
}
}
+ if json_output_flag {
+ wtr.write_field("]")?;
+ }
+
if displayflag {
println!();
} else {
@@ -385,17 +440,51 @@ fn emit_csv(
};
write_color_buffer(
&disp_wtr,
- get_writable_color(None),
+ get_writable_color(Some(Color::Rgb(255, 255, 0))),
+ "Events with hits",
+ false,
+ )
+ .ok();
+ write_color_buffer(&disp_wtr, get_writable_color(None), " / ", false).ok();
+ write_color_buffer(
+ &disp_wtr,
+ get_writable_color(Some(Color::Rgb(0, 255, 255))),
+ "Total events",
+ false,
+ )
+ .ok();
+ write_color_buffer(&disp_wtr, get_writable_color(None), ": ", false).ok();
+ 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),
+ false,
+ )
+ .ok();
+ write_color_buffer(&disp_wtr, get_writable_color(None), " / ", false).ok();
+
+ write_color_buffer(
+ &disp_wtr,
+ get_writable_color(Some(Color::Rgb(0, 255, 255))),
+ &all_record_cnt.to_formatted_string(&Locale::en),
+ false,
+ )
+ .ok();
+ write_color_buffer(&disp_wtr, get_writable_color(None), " (", false).ok();
+ write_color_buffer(
+ &disp_wtr,
+ get_writable_color(Some(Color::Rgb(0, 255, 0))),
&format!(
- "Saved alerts and events / Total events analyzed: {} / {} (Data reduction: {} events ({:.2}%))",
- (all_record_cnt - reducted_record_cnt).to_formatted_string(&Locale::en),
- all_record_cnt.to_formatted_string(&Locale::en),
+ "Data reduction: {} events ({:.2}%)",
reducted_record_cnt.to_formatted_string(&Locale::en),
reducted_percent
),
- true,
+ false,
)
.ok();
+
+ write_color_buffer(&disp_wtr, get_writable_color(None), ")", false).ok();
+ println!();
println!();
_print_unique_results(
@@ -435,21 +524,27 @@ fn _get_serialized_disp_output(data: &LinkedHashMap, header: boo
let data_length = &data.len();
let mut ret: Vec = vec![];
if header {
- for k in data.keys() {
- ret.push(k.to_owned());
+ for (i, k) in data.keys().enumerate() {
+ if i == 0 {
+ ret.push(_format_cellpos(k, ColPos::First))
+ } else if i == data_length - 1 {
+ ret.push(_format_cellpos(k, ColPos::Last))
+ } else {
+ ret.push(_format_cellpos(k, ColPos::Other))
+ }
}
} else {
for (i, (_, v)) in data.iter().enumerate() {
if i == 0 {
- ret.push(_format_cellpos(v, ColPos::First))
+ ret.push(_format_cellpos(v, ColPos::First).replace('|', "🦅"))
} else if i == data_length - 1 {
- ret.push(_format_cellpos(v, ColPos::Last))
+ ret.push(_format_cellpos(v, ColPos::Last).replace('|', "🦅"))
} else {
- ret.push(_format_cellpos(v, ColPos::Other))
+ ret.push(_format_cellpos(v, ColPos::Other).replace('|', "🦅"))
}
}
}
- let mut disp_serializer = csv::WriterBuilder::new()
+ let mut disp_serializer = WriterBuilder::new()
.double_quote(false)
.quote_style(QuoteStyle::Never)
.delimiter(b'|')
@@ -457,7 +552,10 @@ fn _get_serialized_disp_output(data: &LinkedHashMap, header: boo
.from_writer(vec![]);
disp_serializer.write_record(ret).ok();
- String::from_utf8(disp_serializer.into_inner().unwrap_or_default()).unwrap_or_default()
+ String::from_utf8(disp_serializer.into_inner().unwrap_or_default())
+ .unwrap_or_default()
+ .replace('|', "‖")
+ .replace('🦅', "|")
}
/// return str position in output file
@@ -731,6 +829,246 @@ fn _get_timestamp(time: &DateTime) -> i64 {
}
}
+/// json出力の際に配列として対応させるdetails,MitreTactics,MitreTags,OtherTagsに該当する場合に配列を返す関数
+fn _get_json_vec(target_alias_context: &str, target_data: &String) -> Vec {
+ if target_alias_context.contains("%MitreTactics%")
+ || target_alias_context.contains("%OtherTags%")
+ || target_alias_context.contains("%MitreTags%")
+ {
+ let ret: Vec = target_data
+ .to_owned()
+ .split(": ")
+ .map(|x| x.to_string())
+ .collect();
+ ret
+ } else if target_alias_context.contains("%Details%") {
+ let ret: Vec = target_data
+ .to_owned()
+ .split(" ¦ ")
+ .map(|x| x.to_string())
+ .collect();
+ if target_data == &ret[0] && !target_data.contains(": ") {
+ vec![]
+ } else {
+ ret
+ }
+ } else {
+ vec![]
+ }
+}
+
+/// JSONの出力フォーマットに合わせた文字列を出力する関数
+fn _create_json_output_format(
+ key: &String,
+ value: &str,
+ key_quote_exclude_flag: bool,
+ concat_flag: bool,
+) -> String {
+ let head = if key_quote_exclude_flag {
+ key.to_string()
+ } else {
+ format!("\"{}\"", key)
+ };
+ // 4 space is json indent.
+ if let Ok(i) = i64::from_str(value) {
+ format!(" {}: {}", head, i)
+ } else if let Ok(b) = bool::from_str(value) {
+ format!(" {}: {}", head, b)
+ } else if concat_flag {
+ format!(" {}: {}", head, value)
+ } else {
+ format!(" {}: \"{}\"", head, value)
+ }
+}
+
+/// JSONの値に対して文字列の出力形式をJSON出力でエラーにならないようにするための変換を行う関数
+fn _convert_valid_json_str(input: &[&str], concat_flag: bool) -> String {
+ let tmp = if input.len() == 1 {
+ input[0].to_string()
+ } else if concat_flag {
+ input.join(": ")
+ } else {
+ input[1..].join(": ")
+ };
+ let char_cnt = tmp.char_indices().count();
+ let con_val = tmp.as_str();
+ if char_cnt == 0 {
+ tmp
+ } else if con_val.starts_with('\"') {
+ let addition_header = if !con_val.starts_with('\"') { "\"" } else { "" };
+ let addition_quote = if !con_val.ends_with('\"') && concat_flag {
+ "\""
+ } else if !con_val.ends_with('\"') {
+ "\\\""
+ } else {
+ ""
+ };
+ [
+ addition_header,
+ con_val
+ .to_string()
+ .replace('\\', "\\\\")
+ .replace('\"', "\\\"")
+ .trim(),
+ addition_quote,
+ ]
+ .join("")
+ } else {
+ con_val
+ .replace('\\', "\\\\")
+ .replace('\"', "\\\"")
+ .trim()
+ .to_string()
+ }
+}
+
+/// JSONに出力する1検知分のオブジェクトの文字列を出力する関数
+fn output_json_str(
+ ext_field: &LinkedHashMap,
+ profile: &LinkedHashMap,
+ jsonl_output_flag: bool,
+) -> String {
+ let mut target: Vec = vec![];
+ for (k, v) in ext_field.iter() {
+ let output_value_fmt = profile.get(k).unwrap();
+ let vec_data = _get_json_vec(output_value_fmt, v);
+ if vec_data.is_empty() {
+ let tmp_val: Vec<&str> = v.split(": ").collect();
+ let output_val =
+ _convert_valid_json_str(&tmp_val, output_value_fmt.contains("%RecordInformation%"));
+ target.push(_create_json_output_format(
+ k,
+ &output_val,
+ k.starts_with('\"'),
+ output_val.starts_with('\"'),
+ ));
+ } else if output_value_fmt.contains("%Details%") {
+ let mut stocked_value = vec![];
+ let mut key_index_stock = vec![];
+ for detail_contents in vec_data.iter() {
+ // 分解してキーとなりえる箇所を抽出する
+ let space_split: Vec<&str> = detail_contents.split(' ').collect();
+ let mut tmp_stock = vec![];
+ for sp in space_split.iter() {
+ if sp.ends_with(':') && sp != &":" {
+ stocked_value.push(tmp_stock);
+ tmp_stock = vec![];
+ key_index_stock.push(sp.replace(':', "").to_owned());
+ } else {
+ tmp_stock.push(sp.to_owned());
+ }
+ }
+ stocked_value.push(tmp_stock);
+ }
+ let mut key_idx = 0;
+ let mut output_value_stock = String::default();
+ for (value_idx, value) in stocked_value.iter().enumerate() {
+ let mut tmp = if key_idx >= key_index_stock.len() {
+ String::default()
+ } else if value_idx == 0 && !value.is_empty() {
+ k.to_string()
+ } else {
+ key_index_stock[key_idx].to_string()
+ };
+ if !output_value_stock.is_empty() {
+ output_value_stock.push_str(" | ");
+ }
+ output_value_stock.push_str(&value.join(" "));
+ //``1つまえのキーの段階で以降にvalueの配列で区切りとなる空の配列が存在しているかを確認する
+ let is_remain_split_stock = if key_idx == key_index_stock.len() - 2
+ && value_idx < stocked_value.len() - 1
+ && !output_value_stock.is_empty()
+ {
+ let mut ret = true;
+ for remain_value in stocked_value[value_idx + 1..].iter() {
+ if remain_value.is_empty() {
+ ret = false;
+ break;
+ }
+ }
+ ret
+ } else {
+ false
+ };
+ if (value_idx < stocked_value.len() - 1 && stocked_value[value_idx + 1].is_empty())
+ || is_remain_split_stock
+ {
+ // 次の要素を確認して、存在しないもしくは、キーが入っているとなった場合現在ストックしている内容が出力していいことが確定するので出力処理を行う
+ let output_tmp = format!("{}: {}", tmp, output_value_stock);
+ let output: Vec<&str> = output_tmp.split(": ").collect();
+ let key = _convert_valid_json_str(&[output[0]], false);
+ let fmted_val = _convert_valid_json_str(&output, false);
+ target.push(_create_json_output_format(
+ &key,
+ &fmted_val,
+ key.starts_with('\"'),
+ fmted_val.starts_with('\"'),
+ ));
+ output_value_stock.clear();
+ tmp = String::default();
+ key_idx += 1;
+ }
+ if value_idx == stocked_value.len() - 1 {
+ let output_tmp = format!("{}: {}", tmp, output_value_stock);
+ let output: Vec<&str> = output_tmp.split(": ").collect();
+ let key = _convert_valid_json_str(&[output[0]], false);
+ let fmted_val = _convert_valid_json_str(&output, false);
+ target.push(_create_json_output_format(
+ &key,
+ &fmted_val,
+ key.starts_with('\"'),
+ fmted_val.starts_with('\"'),
+ ));
+ key_idx += 1;
+ }
+ }
+ } else if output_value_fmt.contains("%MitreTags%")
+ || output_value_fmt.contains("%MitreTactics%")
+ || output_value_fmt.contains("%OtherTags%")
+ {
+ let tmp_val: Vec<&str> = v.split(": ").collect();
+
+ let key = _convert_valid_json_str(&[k.as_str()], false);
+ let values: Vec<&&str> = tmp_val.iter().filter(|x| x.trim() != "").collect();
+ let mut value: Vec = vec![];
+
+ if values.is_empty() {
+ continue;
+ }
+ for (idx, tag_val) in values.iter().enumerate() {
+ if idx == 0 {
+ value.push("[\n".to_string());
+ }
+ let insert_val = format!(" \"{}\"", tag_val.trim());
+ value.push(insert_val);
+ if idx != values.len() - 1 {
+ value.push(",\n".to_string());
+ }
+ }
+ value.push("\n ]".to_string());
+
+ let fmted_val = if jsonl_output_flag {
+ value.iter().map(|x| x.replace('\n', "")).join("")
+ } else {
+ value.join("")
+ };
+ target.push(_create_json_output_format(
+ &key,
+ &fmted_val,
+ key.starts_with('\"'),
+ true,
+ ));
+ }
+ }
+ if jsonl_output_flag {
+ // JSONL output
+ target.into_iter().map(|x| x.replace(" ", "")).join(",")
+ } else {
+ // JSON format output
+ target.join(",\n")
+ }
+}
+
#[cfg(test)]
mod tests {
use crate::afterfact::_get_serialized_disp_output;
@@ -795,7 +1133,7 @@ mod tests {
(
"%Channel%".to_owned(),
mock_ch_filter
- .get("Security")
+ .get(&"Security".to_ascii_lowercase())
.unwrap_or(&String::default())
.to_string(),
),
@@ -820,7 +1158,7 @@ mod tests {
eventid: test_eventid.to_string(),
detail: String::default(),
record_information: Option::Some(test_recinfo.to_string()),
- ext_field: output_profile,
+ ext_field: output_profile.clone(),
},
expect_time,
&mut profile_converter,
@@ -860,7 +1198,7 @@ mod tests {
+ test_attack
+ "\n";
let mut file: Box = Box::new(File::create("./test_emit_csv.csv").unwrap());
- assert!(emit_csv(&mut file, false, HashMap::new(), 1).is_ok());
+ assert!(emit_csv(&mut file, false, HashMap::new(), 1, output_profile).is_ok());
match read_to_string("./test_emit_csv.csv") {
Err(_) => panic!("Failed to open file."),
Ok(s) => {
@@ -884,28 +1222,28 @@ mod tests {
let test_timestamp = Utc
.datetime_from_str("1996-02-27T01:05:01Z", "%Y-%m-%dT%H:%M:%SZ")
.unwrap();
- let expect_header = "Timestamp|Computer|Channel|EventID|Level|RecordID|RuleTitle|Details|RecordInformation\n";
+ let expect_header = "Timestamp ‖ Computer ‖ Channel ‖ EventID ‖ Level ‖ RecordID ‖ RuleTitle ‖ Details ‖ RecordInformation\n";
let expect_tz = test_timestamp.with_timezone(&Local);
let expect_no_header = expect_tz
.clone()
.format("%Y-%m-%d %H:%M:%S%.3f %:z")
.to_string()
- + " | "
+ + " ‖ "
+ test_computername
- + " | "
+ + " ‖ "
+ test_channel
- + " | "
+ + " ‖ "
+ test_eventid
- + " | "
+ + " ‖ "
+ test_level
- + " | "
+ + " ‖ "
+ test_recid
- + " | "
+ + " ‖ "
+ test_title
- + " | "
+ + " ‖ "
+ output
- + " | "
+ + " ‖ "
+ test_recinfo
+ "\n";
let mut data: LinkedHashMap = LinkedHashMap::new();
diff --git a/src/detections/configs.rs b/src/detections/configs.rs
index a0c50461..f1849a65 100644
--- a/src/detections/configs.rs
+++ b/src/detections/configs.rs
@@ -237,6 +237,14 @@ pub struct Config {
#[clap(help_heading = Some("OTHER-ACTIONS"), long = "set-default-profile", value_name = "PROFILE")]
pub set_default_profile: Option,
+ /// Save the timeline in JSON format (ex: -j -o results.json)
+ #[clap(help_heading = Some("OUTPUT"), short = 'j', long = "json", requires = "output")]
+ pub json_timeline: bool,
+
+ /// Save the timeline in JSONL format (ex: -J -o results.jsonl)
+ #[clap(help_heading = Some("OUTPUT"), short = 'J', long = "jsonl", requires = "output")]
+ pub jsonl_timeline: bool,
+
/// Do not display result summary
#[clap(help_heading = Some("DISPLAY-SETTINGS"), long = "no-summary")]
pub no_summary: bool,
diff --git a/src/detections/detection.rs b/src/detections/detection.rs
index 2f4e6207..f1b5af43 100644
--- a/src/detections/detection.rs
+++ b/src/detections/detection.rs
@@ -264,7 +264,10 @@ impl Detection {
"%Channel%" => {
profile_converter.insert(
"%Channel%".to_string(),
- CH_CONFIG.get(ch_str).unwrap_or(ch_str).to_string(),
+ CH_CONFIG
+ .get(&ch_str.to_ascii_lowercase())
+ .unwrap_or(ch_str)
+ .to_string(),
);
}
"%Level%" => {
@@ -323,7 +326,7 @@ impl Detection {
.filter(|x| TAGS_CONFIG.values().contains(x))
.map(|y| y.to_owned())
.collect();
- profile_converter.insert("%MitreTactics%".to_string(), tactics.join(" : "));
+ profile_converter.insert("%MitreTactics%".to_string(), tactics.join(" ¦ "));
}
"%MitreTags%" => {
let techniques: &Vec = &tag_info
@@ -339,7 +342,7 @@ impl Detection {
make_ascii_titlecase(&mut replaced_tag)
})
.collect();
- profile_converter.insert("%MitreTags%".to_string(), techniques.join(" : "));
+ profile_converter.insert("%MitreTags%".to_string(), techniques.join(" ¦ "));
}
"%OtherTags%" => {
let tags: &Vec = &tag_info
@@ -352,7 +355,7 @@ impl Detection {
})
.map(|y| y.to_owned())
.collect();
- profile_converter.insert("%OtherTags%".to_string(), tags.join(" : "));
+ profile_converter.insert("%OtherTags%".to_string(), tags.join(" ¦ "));
}
_ => {}
@@ -455,7 +458,7 @@ impl Detection {
.filter(|x| TAGS_CONFIG.values().contains(x))
.map(|y| y.to_owned())
.collect();
- profile_converter.insert("%MitreTactics%".to_string(), tactics.join(" : "));
+ profile_converter.insert("%MitreTactics%".to_string(), tactics.join(" ¦ "));
}
"%MitreTags%" => {
let techniques: &Vec = &tag_info
@@ -471,7 +474,7 @@ impl Detection {
make_ascii_titlecase(&mut replaced_tag)
})
.collect();
- profile_converter.insert("%MitreTags%".to_string(), techniques.join(" : "));
+ profile_converter.insert("%MitreTags%".to_string(), techniques.join(" ¦ "));
}
"%OtherTags%" => {
let tags: &Vec = &tag_info
@@ -484,7 +487,7 @@ impl Detection {
})
.map(|y| y.to_owned())
.collect();
- profile_converter.insert("%OtherTags%".to_string(), tags.join(" : "));
+ profile_converter.insert("%OtherTags%".to_string(), tags.join(" ¦ "));
}
_ => {}
}
diff --git a/src/detections/message.rs b/src/detections/message.rs
index e3a3d235..fa374282 100644
--- a/src/detections/message.rs
+++ b/src/detections/message.rs
@@ -105,10 +105,10 @@ pub fn create_output_filter_config(path: &str) -> HashMap {
return;
}
- let tag_full_str = line[0].trim();
+ let tag_full_str = line[0].trim().to_ascii_lowercase();
let tag_replace_str = line[1].trim();
- ret.insert(tag_full_str.to_owned(), tag_replace_str.to_owned());
+ ret.insert(tag_full_str, tag_replace_str.to_owned());
});
ret
}
@@ -599,7 +599,7 @@ mod tests {
let actual = create_output_filter_config("test_files/config/channel_abbreviations.txt");
let actual2 = create_output_filter_config("test_files/config/channel_abbreviations.txt");
let expected: HashMap = HashMap::from([
- ("Security".to_string(), "Sec".to_string()),
+ ("Security".to_ascii_lowercase(), "Sec".to_string()),
("xxx".to_string(), "yyy".to_string()),
]);
_check_hashmap_element(&expected, actual);
diff --git a/src/detections/utils.rs b/src/detections/utils.rs
index f7ee3a14..837da55d 100644
--- a/src/detections/utils.rs
+++ b/src/detections/utils.rs
@@ -297,15 +297,10 @@ fn create_recordinfos(record: &Value) -> String {
let summary: Vec = output
.iter()
- .map(|(key, value)| format!("{}:{}", key, value))
+ .map(|(key, value)| format!("{}: {}", key, value))
.collect();
- // 標準出力する時はセルがハイプ区切りになるので、パイプ区切りにしない
- if configs::CONFIG.read().unwrap().args.output.is_some() {
- summary.join(" | ")
- } else {
- summary.join(" ")
- }
+ summary.join(" ¦ ")
}
/**
@@ -510,7 +505,7 @@ mod tests {
Ok(record) => {
let ret = utils::create_recordinfos(&record);
// Systemは除外される/属性(_attributesも除外される)/key順に並ぶ
- let expected = "AccessMask:%%1369 Process:lsass.exe User:u1".to_string();
+ let expected = "AccessMask: %%1369 ¦ Process: lsass.exe ¦ User: u1".to_string();
assert_eq!(ret, expected);
}
Err(_) => {
@@ -544,7 +539,7 @@ mod tests {
Ok(record) => {
let ret = utils::create_recordinfos(&record);
// Systemは除外される/属性(_attributesも除外される)/key順に並ぶ
- let expected = "Binary:hogehoge Data: Data:Data1 Data:DataData2 Data:DataDataData3"
+ let expected = "Binary: hogehoge ¦ Data: ¦ Data: Data1 ¦ Data: DataData2 ¦ Data: DataDataData3"
.to_string();
assert_eq!(ret, expected);
}