From ef4b4b7390dd7b4f08a12d8fd324e38e00fc5fe2 Mon Sep 17 00:00:00 2001 From: TQ Hirsch Date: Sat, 4 May 2024 03:17:42 +0200 Subject: [PATCH] Initial commit. Data extraction vaguely tested; report writing tasks written but untested --- .gitignore | 1 + Cargo.lock | 1106 ++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 19 + src/bin/cert_dump.rs | 17 + src/config.rs | 277 +++++++++++ src/lib.rs | 2 + src/main.rs | 10 + src/report.rs | 320 ++++++++++++ src/scanner.rs | 4 + 9 files changed, 1756 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/bin/cert_dump.rs create mode 100644 src/config.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/report.rs create mode 100644 src/scanner.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..dd944fa --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1106 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" + +[[package]] +name = "ascertain" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "chrono", + "futures", + "hex", + "openssl", + "serde", + "serde_json", + "serde_with", + "thiserror", + "tokio", + "toml", + "tracing", +] + +[[package]] +name = "autocfg" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cc" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.5", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "darling" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-sys" +version = "0.9.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.200" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.200" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_with" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.6", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tokio" +version = "1.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +dependencies = [ + "indexmap 2.2.6", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "winnow" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b9415ee827af173ebb3f15f9083df5a122eb93572ec28741fb153356ea2578" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..59c1258 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ascertain" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.82" +base64 = "0.22.1" +chrono = { version = "0.4.38", features = ["serde"] } +futures = "0.3.30" +hex = { version = "0.4.3", features = ["serde"] } +openssl = "0.10.64" +serde = { version = "1.0.200", features = ["derive", "rc"] } +serde_json = "1.0.116" +serde_with = { version = "3.8.1", features = ["base64"] } +thiserror = "1.0.59" +tokio = { version = "1.37.0", features = ["rt-multi-thread", "fs", "io-util", "net", "sync", "time", "macros", "parking_lot"] } +toml = "0.8.12" +tracing = { version = "0.1.40" } diff --git a/src/bin/cert_dump.rs b/src/bin/cert_dump.rs new file mode 100644 index 0000000..4b4577e --- /dev/null +++ b/src/bin/cert_dump.rs @@ -0,0 +1,17 @@ +use std::io::Read; + +use openssl::x509::X509; + +pub fn main() -> anyhow::Result<()> { + let mut buf = Vec::new(); + std::io::stdin().read_to_end(&mut buf)?; + let cert = if buf.starts_with(b"-----BEGIN CERTIFICATE-----") { + openssl::x509::X509::from_pem(buf.as_slice())? + } else { + X509::from_der(buf.as_slice())? + }; + + eprint!("{}", toml::to_string(&ascertain::report::CertInfo::extract(cert.as_ref())?)?); + + Ok(()) +} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..e9b32b1 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,277 @@ +use std::collections::HashSet; +use std::fmt::Formatter; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::num::NonZeroU16; +use std::ops::Add; +use std::path::Path; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; + +use serde::{de, Deserialize, Deserializer}; +use serde::de::{Error, Unexpected}; +use tokio::sync::Semaphore; + +use crate::report::JsonConfig; + +#[derive(Copy, Clone, Debug)] +pub enum IpRange { + V4{ + start: Ipv4Addr, + end: Ipv4Addr, + }, + V6{ + start: Ipv6Addr, + end: Ipv6Addr, + } +} + +impl IpRange { + + pub fn contains(&self, addr: IpAddr) -> bool { + match (self, addr) { + (Self::V4{start, end}, IpAddr::V4(addr)) => &addr >= start && &addr <= end, + (Self::V6{start, end}, IpAddr::V6(addr)) => &addr >= start && &addr <= end, + (_, _) => false, + } + } + pub fn empty(&self) -> bool { + match self { + IpRange::V4 { start, end } => {start > end} + IpRange::V6 { start, end } => {start > end} + } + } + + /// Actually returns size-1, as 0 can be checked via empty and the maximum (::/0) doesn't fit in u128 + pub fn size_128(&self) -> u128 { + match self { + IpRange::V4 { start, end } => { + if start <= end { + (u32::from_be_bytes(start.octets()) as u128) - (u32::from_be_bytes(start.octets()) as u128) + } else { + 0 + } + } + IpRange::V6 { start, end } => { + if start <= end { + u128::from_be_bytes(start.octets()) - u128::from_be_bytes(end.octets()) + } else { + 0 + } + } + } + } +} + +impl Iterator for IpRange { + type Item = IpAddr; + + fn next(&mut self) -> Option { + match self { + IpRange::V4{start, end} => { + if *start <= *end { + let ret = *start; + *start = u32::from_be_bytes(start.octets()).add(1).to_be_bytes().into(); + Some(IpAddr::V4(ret)) + } else { None } + }, + IpRange::V6 {start, end } => { + if *start <= *end { + let ret = *start; + *start = u128::from_be_bytes(start.octets()).add(1).to_be_bytes().into(); + Some(IpAddr::V6(ret)) + } else { None } + } + } + } + + fn size_hint(&self) -> (usize, Option) { + if self.empty() { + (0, Some(0)) + } else { + let sz = self.size_128(); + if sz >= usize::MAX as u128 { + (usize::MAX, None) + } else { + let sz = sz as usize; + (sz+1, Some(sz+1)) + } + } + } + // TODO: other methods could have more efficient implementations, but I don't think we need them +} + +impl<'de> Deserialize<'de> for IpRange { + fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { + struct IpRangeVisitor; + impl<'de> de::Visitor<'de> for IpRangeVisitor { + type Value = IpRange; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + write!(formatter, "ip, ip/prefix, or ip-ip") + } + + fn visit_str(self, v: &str) -> Result where E: Error { + if let Some((network, pfx)) = v.split_once('/') { + // TODO: check for likely misconfiguration + let network = IpAddr::from_str(network).map_err(|_| E::invalid_value(Unexpected::Str(v), &self))?; + let prefix = u32::from_str_radix(pfx, 10).map_err(|_| E::invalid_value(Unexpected::Str(v), &self))?; + match network { + IpAddr::V4(addr) => { + let addr: u32 = addr.into(); + if prefix > 32 { + return Err(E::invalid_value(Unexpected::Str(v), &self)) + } else if prefix == 0 { + // This is probably an error, but trust the user ¯\_(ツ)_/¯ + return Ok(IpRange::V4 {start: Ipv4Addr::from(1), end: Ipv4Addr::from(!1)}) + } + let mask = (1 << (32-prefix)) - 1; + return Ok(IpRange::V4 { start: Ipv4Addr::from(addr & !mask), end: Ipv4Addr::from(addr | mask) }) + } + IpAddr::V6(addr) => { + let addr: u128 = addr.into(); + if prefix > 128 { + return Err(E::invalid_value(Unexpected::Str(v), &self)) + } else if prefix == 0 { + // This is *definitely* an error, but trust the user ¯\_(ツ)_/¯ + return Ok(IpRange::V6 {start: Ipv6Addr::from(1), end: Ipv6Addr::from(!1)}) + } + let mask = (1 << (128-prefix as u128)) - 1; + return Ok(IpRange::V6 { start: Ipv6Addr::from(addr & !mask), end: Ipv6Addr::from(addr | mask) }) + } + } + } else if let Some((n1, n2)) = v.split_once('-') { + let (start, end) = IpAddr::from_str(n1) + .and_then(|start| Ok((start, IpAddr::from_str(n2)?))) + .map_err(|_| E::invalid_value(Unexpected::Str(v), &self))?; + match (start, end) { + (IpAddr::V4(start), IpAddr::V4(end)) => Ok(IpRange::V4 {start, end}), + (IpAddr::V6(start), IpAddr::V6(end)) => Ok(IpRange::V6 {start, end}), + _ => Err(E::invalid_value(Unexpected::Str(v), &self)) + } + } else { + match IpAddr::from_str(v) { + Err(_) => Err(E::invalid_value(Unexpected::Str(v), &self)), + Ok(IpAddr::V4(addr)) => Ok(IpRange::V4 {start: addr, end: addr}), + Ok(IpAddr::V6(addr)) => Ok(IpRange::V6 {start: addr, end: addr}), + } + } + } + } + + deserializer.deserialize_str(IpRangeVisitor) + } +} + +#[derive(Deserialize)] +pub struct TargetConfig { + /// List of IP address ranges to scan. Required + #[serde(default)] + pub hosts: Vec, + + /// List of ports to scan on each host. Required + #[serde(default)] + pub ports: Vec, + + /// How many probes are allowed in parallel per host + #[serde(default)] + pub host_parallelism: Option, + + /// How many probes are allowed in parallel, globally + #[serde(default)] + pub global_parallelism: Option, + + /// How long to wait for a connection, in seconds. + #[serde(default)] + pub connect_timeout: Option, + + /// How long to wait for the TLS handshake, in seconds. + #[serde(default)] + pub handshake_timeout: Option, + + /// How many attempts to make to contact a host + #[serde(default)] + pub retry_count: Option, + + /// Port to use to check host liveness. + #[serde(default)] + pub host_live_port: u16, +} + +#[derive(Deserialize)] +#[serde(rename_all="lowercase", tag="format")] +pub enum OutputFormat { + Json(JsonConfig) +} + +#[derive(Deserialize)] +pub struct TopConfig { + pub targets: TargetConfig, + pub output: OutputFormat, +} + +pub struct Host { + pub ip: IpAddr, + pub semaphore: Arc, + pub ports: Vec, + pub live_port: Option, +} + +/// The subset of config needed by probe tasks. +#[derive(Clone)] +pub struct ProbeConfig { + pub retry_count: u32, + pub connect_timeout: Duration, + pub handshake_timeout: Duration, + pub global_semaphore: Arc, +} + +pub struct Config { + pub hosts: Vec, + pub probe_config: ProbeConfig, +} + +pub fn load_config(path: impl AsRef) -> anyhow::Result { + let content = std::fs::read_to_string(path)?; + let top_config = toml::from_str::(&content)?; + let target_config = top_config.targets; + + let host_parallelism = target_config.host_parallelism.unwrap_or(5); // conservative default + // Construct the list of hosts and host/port pairs. + + let mut hosts_seen = HashSet::new(); + let mut hosts = Vec::new(); + let mut probes: usize = 0; + + for range in target_config.hosts { + for ip in range { + if !hosts_seen.insert(ip) { + continue; + } + hosts.push(Host { + ip, + live_port: NonZeroU16::new(target_config.host_live_port), + semaphore: Arc::new(Semaphore::new(host_parallelism)), + ports: target_config.ports.clone(), + }); + probes += target_config.ports.len(); + } + } + + // Configure the reporter + let (reporter, backend) = crate::report::configure_backend(top_config.output)?; + + let probe_config = ProbeConfig { + retry_count: target_config.retry_count.unwrap_or(1), + connect_timeout: Duration::from_secs_f32(target_config.handshake_timeout.unwrap_or(5.)), + handshake_timeout: Duration::from_secs_f32(target_config.handshake_timeout.unwrap_or(5.)), + // 900 is a sane default in case we're crossing a NAT boundary; if not, this can safely be 100's + // of thousands, depending on system resources. + global_semaphore: Arc::new(Semaphore::new(target_config.global_parallelism.unwrap_or(900))), + }; + + Ok(Config{ + hosts, + probe_config, + }) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a6e4a7c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2 @@ +pub mod config; +pub mod report; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..76ecdcb --- /dev/null +++ b/src/main.rs @@ -0,0 +1,10 @@ +use ascertain::{report, config}; + +pub mod scanner; + + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + eprintln!("{}", std::mem::size_of::()); + Ok(()) +} diff --git a/src/report.rs b/src/report.rs new file mode 100644 index 0000000..3563437 --- /dev/null +++ b/src/report.rs @@ -0,0 +1,320 @@ +use std::borrow::Cow; +use std::collections::HashSet; +use std::future::Future; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::path::PathBuf; +use std::sync::Arc; + +use base64::Engine; +use base64::prelude::BASE64_STANDARD; +use chrono::{Local, Utc}; +use openssl::asn1::{Asn1Time, Asn1TimeRef}; +use openssl::hash::MessageDigest; +use openssl::pkey::{Id, PKeyRef, Public}; +use openssl::stack::StackRef; +use openssl::x509::{GeneralNameRef, X509, X509NameEntryRef, X509NameRef, X509Ref}; +use serde::{Deserialize, Serialize}; +use serde_with::{base64::Base64, serde_as}; +use thiserror::Error; +use tokio::io::{AsyncWriteExt, BufWriter}; +use tokio::sync::{mpsc, RwLock}; +use tokio::sync::mpsc::Sender; +use tracing::{error, warn}; + +use crate::config::OutputFormat; + +#[derive(Error, Debug, Serialize, Copy, Clone)] +pub enum ReportError { + #[error("Connection timed out")] + ConnectionTimeout, + #[error("Connection refused")] + ConnectionRefused, + #[error("Handshake timed out")] + HandshakeTimeout, + #[error("TLS Protocol error: probably not a TLS server")] + ProtocolError, +} + +#[derive(Serialize, Debug, Clone)] +#[serde(rename="lowercase", tag="status", content="report")] +pub enum ReportPayload { + Success{ + certificate: CertInfo, + // TODO: define fields for SSL implementation + }, + Error{msg: ReportError}, +} + +#[derive(Serialize, Debug, Clone)] +pub struct ProbeReport { + pub host: SocketAddr, + pub scan_date: chrono::DateTime, + #[serde(flatten)] + pub result: ReportPayload +} + + +#[serde_as] +#[derive(Serialize, Debug, Clone)] +pub struct CertInfo { + #[serde_as(as="Base64")] + pub cert_digest: Vec, + #[serde_as(as="Base64")] + pub issuer_subject_der: Vec, + pub issuer_subject: Vec, + #[serde_as(as="Base64")] + pub certificate_der: Vec, + pub subject: Vec, + #[serde_as(as="Base64")] + pub subject_der: Vec, + pub san: Vec, + pub not_before: chrono::DateTime, + pub not_after: chrono::DateTime, + pub key_type: String, + pub signature_type: String, + #[serde(with="hex")] + pub authority_key_id: Vec, + #[serde(with="hex")] + pub subject_key_id: Vec, + +} + +fn asn1time_to_datetime(date: &Asn1TimeRef) -> anyhow::Result> { + let res = Asn1Time::from_unix(0).unwrap().diff(date)?; + let timestamp = res.days as i64 * 86400 + res.secs as i64; + chrono::DateTime::from_timestamp(timestamp, 0) + .ok_or(anyhow::anyhow!("Constructing timestamp failed")) +} + +fn describe_key(key: &PKeyRef) -> String { + match key.id() { + Id::RSA => format!("RSA-{}", key.bits()), + Id::RSA_PSS => format!("RSA-PSS-{}", key.bits()), + Id::DSA => format!("DSA-{}", key.bits()), + Id::EC => format!("EC-P{}", key.bits()), + Id::ED25519 => "Ed25519".to_owned(), + Id::ED448 => "Ed448".to_owned(), + id => format!("UNKNOWN-{}", id.as_raw()), + } +} + +impl CertInfo { + pub fn extract(data: &X509Ref) -> anyhow::Result { + let md = MessageDigest::sha256(); + let cert_digest = data.digest(md)?.to_vec(); + let issuer_subject = data.subject_name(); + + + Ok(CertInfo { + cert_digest, + issuer_subject_der: issuer_subject.to_der()?, + issuer_subject: issuer_subject.entries().map(format_x509_name_entry).collect(), + certificate_der: data.to_der()?, + subject: data.subject_name().entries().map(format_x509_name_entry).collect(), + subject_der: data.subject_name().to_der()?, + san: data.subject_alt_names() + .map(|stack| stack.iter().map(format_general_name).collect()) + .unwrap_or(Vec::new()), + not_before: asn1time_to_datetime(data.not_before())?, + not_after: asn1time_to_datetime(data.not_after())?, + key_type: describe_key(data.public_key()?.as_ref()), + signature_type: data.signature_algorithm().object().nid().short_name()?.to_owned(), + authority_key_id: data.authority_key_id().map_or(Vec::new(), |id| id.as_slice().to_vec()), + subject_key_id: data.subject_key_id().map_or(Vec::new(), |id| id.as_slice().to_vec()) + }) + } +} + +#[derive(Deserialize)] +pub struct JsonConfig { + /// File which receives discovered certificates. + output_file: PathBuf, + + /// File which receives discovered issuer certificates. Optional; if not included, do not store issuers. + #[serde(default)] + issuer_file: Option, + + /// Enable an outer container so that the output file contains a single JSON list. + /// If disabled (default), the file is in json-lines format. + #[serde(default)] + container: bool, +} + +#[allow(unused)] +#[derive(Clone)] +pub struct Reporter { + issuer_chan: Sender, + report_chan: Sender, + digests: Arc>>>, + collect_digests: bool, +} + +#[derive(Error, Debug)] +pub enum ReportingError { + #[error("Report formatter terminated")] + ReportFormatterFailed, +} + +fn format_x509_name_entry(entry: &X509NameEntryRef) -> String { + let name = entry.object().nid().short_name() + .map(Cow::Borrowed) + .unwrap_or_else(|_| Cow::Owned(format!("{:?}", entry.object()))); + let value = entry.data().as_utf8().map(|data| data.to_string()).unwrap_or_else(|_| + BASE64_STANDARD.encode(entry.data().as_slice()) + ); + format!("{name}={value}") +} +fn format_x509_name(name: &X509NameRef) -> String { + let mut result = "".to_owned(); + for entry in name.entries() { + if !result.is_empty() { + result.push_str(", ") + } + result.push_str(format_x509_name_entry(entry).as_ref()); + } + result +} + +fn format_general_name(name: &GeneralNameRef) -> String { + // TODO: there's other types that aren't supported by the safe wrapper. + if let Some(name) = name.email() { + return format!("EMAIL:{name}") + } else if let Some(name) = name.dnsname() { + return format!("DNS:{name}") + } else if let Some(name) = name.uri() { + return format!("URI:{name}") + } else if let Some(ip) = name.ipaddress() { + match ip.len() { + 4 => format!("IP:{}", Ipv4Addr::from(<[u8;4]>::try_from(ip).unwrap())), + 16 => format!("IP:{}", Ipv6Addr::from(<[u8;16]>::try_from(ip).unwrap())), + _ => format!("IPx:{}", hex::encode(ip)) + } + } else if let Some(dn) = name.directory_name() { + format!("DN:{}", format_x509_name(dn)) + } else { + "UNKNOWN".to_string() + } +} +impl Reporter { + pub async fn report_issuers(&self, issuers: &StackRef) { + if !self.collect_digests { + return; + } + // TODO This is always in whatever order the client returns the certificates :-/ + for issuer in issuers.iter() { + self.note_issuer(issuer).await; + } + } + + async fn note_issuer(&self, x509: &X509Ref) -> Option<()> { + let der_digest = x509.digest(MessageDigest::sha256()).ok()?.to_vec(); + // We try with a read lock first in order to increase parallelism. + // Most of the time, the cert will already be in the store. + let already = self.digests.read().await.contains(der_digest.as_slice()); + if !already { + if self.digests.write().await.insert(der_digest) { + if self.issuer_chan.send(x509.to_owned()).await.is_err() { + + } + } + } + Some(()) + } + + pub async fn report_probe(&self, report: ProbeReport) -> Result<(), ReportingError> { + if self.report_chan.send(report).await.is_err() { + error!("Report formatter has exited early"); + Err(ReportingError::ReportFormatterFailed) + } else { + Ok(()) + } + } +} + +fn start_json(config: JsonConfig) -> anyhow::Result<(impl Future+Send, Reporter)> { + let (issuer_send, mut issuer_recv) = mpsc::channel::(5); + let (report_send, mut report_recv) = mpsc::channel(5); + + let report_file = tokio::fs::File::from_std(std::fs::File::create(config.output_file)?); + let issuer_writer = config.issuer_file.map(std::fs::File::create).transpose()?.map(tokio::fs::File::from_std); + let has_issuer = issuer_writer.is_some(); + let container = config.container; + let issuer_fut = async move { + if let Some(issuer_file) = issuer_writer { + let mut first_record = true; + let mut issuer_file = tokio::io::BufWriter::new(issuer_file); + if container { + issuer_file.write_u8(b'[').await?; + } + while let Some(issuer) = issuer_recv.recv().await { + match CertInfo::extract(issuer.as_ref()) { + Ok(info) => { + if container && !first_record { + issuer_file.write_u8(b',').await?; + first_record = false; + } + let info = serde_json::to_vec(&info)?; + issuer_file.write_all(info.as_slice()).await?; + issuer_file.write_u8(b'\n').await?; + } + Err(err) => { + warn!(%err, "Failed to extract data from certificate") + } + } + } + if container { + issuer_file.write_all(b"]").await?; + } + issuer_file.flush().await?; + } + Ok(()) as anyhow::Result<()> + }; + + let report_fut = async move { + let mut file = BufWriter::new(report_file); + if container { + file.write_u8(b'[').await?; + } + let mut first_record = true; + while let Some(report) = report_recv.recv().await { + if container && !first_record { + file.write_u8(b',').await?; + first_record = false; + } + let json = serde_json::to_vec(&report)?; + file.write_all(json.as_slice()).await?; + file.write_u8(b'\n').await?; + } + if container { + file.write_u8(b']').await?; + } + file.flush().await?; + Ok(()) as anyhow::Result<()> + }; + + let task = async move { + let (reporter, issuer) = futures::future::join(report_fut, issuer_fut).await; + if let Err(error) = reporter { + error!(%error, "Report writer failed") + } + if let Err(error) = issuer { + error!(%error, "Issuer certificate writer failed") + } + }; + + let reporter = Reporter { + issuer_chan: issuer_send, + report_chan: report_send, + digests: Arc::new(Default::default()), + collect_digests: has_issuer, + }; + Ok((task, reporter)) +} + + +/// Configure the reporting backend +pub(crate) fn configure_backend(config: OutputFormat) -> anyhow::Result<(impl Future+Send, Reporter)> { + match config { + OutputFormat::Json(json) => start_json(json) + } +} \ No newline at end of file diff --git a/src/scanner.rs b/src/scanner.rs new file mode 100644 index 0000000..767dbdb --- /dev/null +++ b/src/scanner.rs @@ -0,0 +1,4 @@ +use crate::config::Config; + +pub fn scan(config: Config) { +} \ No newline at end of file