diff --git a/plugins/cloudtrail_rs/Cargo.lock b/plugins/cloudtrail_rs/Cargo.lock new file mode 100644 index 00000000..7fbdb94e --- /dev/null +++ b/plugins/cloudtrail_rs/Cargo.lock @@ -0,0 +1,2873 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "attribute-derive" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77" +dependencies = [ + "attribute-derive-macro", + "derive-where", + "manyhow", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61" +dependencies = [ + "collection_literals", + "interpolator", + "manyhow", + "proc-macro-utils", + "proc-macro2", + "quote", + "quote-use", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-config" +version = "1.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11493b0bad143270fb8ad284a096dd529ba91924c5409adeac856cc1bf047dbc" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.4.0", + "sha1", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "aws-runtime" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc0651c57e384202e47153c1260b84a9936e19803d747615edf199dc3b98d17" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "bytes-utils", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.125.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223f5c95650d9557925a91f4c2db3def189e8f659452134a29e5cd2d37d708ed" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "hex", + "hmac", + "http 0.2.12", + "http 1.4.0", + "http-body 1.0.1", + "lru", + "percent-encoding", + "regex-lite", + "sha2", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sqs" +version = "1.96.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161a1c4332072b8e44a3a0a6f817440f72c00ab1ae9b6b68ed63e882cd97394c" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.96.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64a6eded248c6b453966e915d32aeddb48ea63ad17932682774eb026fbef5b1" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.98.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db96d720d3c622fcbe08bae1c4b04a72ce6257d8b0584cb5418da00ae20a344f" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fafbdda43b93f57f699c5dfe8328db590b967b8a820a13ccdd6687355dfcc7ca" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b660013a6683ab23797778e21f1f854744fdf05f68204b4cca4c8c04b5d1f4" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint 0.5.5", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.4.0", + "p256", + "percent-encoding", + "ring", + "sha2", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.64.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6750f3dd509b0694a4377f0293ed2f9630d710b1cebe281fa8bac8f099f88bc6" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "crc-fast", + "hex", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "md-5", + "pin-project-lite", + "sha1", + "sha2", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.60.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf09d74e5e32f76b8762da505a3cd59303e367a664ca67295387baa8c1d7548" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.63.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.13", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.8.1", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.7", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.37", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.62.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9648b0bb82a2eedd844052c6ad2a1a822d1f8e3adee5fbf668366717e428856a" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028999056d2d2fd58a697232f9eec4a643cf73a71cf327690a7edad1d2af2110" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876ab3c9c29791ba4ba02b780a3049e21ec63dabda09268b175272c3733a79e6" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.4.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b1117b3b2bbe166d11199b540ceed0d0f7676e36e7b962b5a437a9971eac75" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c8323699dd9b3c8d5b3c13051ae9cdef58fd179957c882f8374dd8725962d9" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cloudtrail" +version = "0.16.0" +dependencies = [ + "anyhow", + "aws-config", + "aws-sdk-s3", + "aws-sdk-sqs", + "aws-types", + "chrono", + "falco_event", + "falco_plugin", + "flate2", + "regex", + "schemars", + "serde", + "serde_json", + "serde_spanned", + "tokio", +] + +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "collection_literals" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc-fast" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd92aca2c6001b1bf5ba0ff84ee74ec8501b52bbef0cac80bf25a6c1d87a83d" +dependencies = [ + "crc", + "digest", + "rustversion", + "spin", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive-where" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct", + "crypto-bigint 0.4.9", + "der", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "falco_event" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f308debc02a31b1f6f2f4ed99f489bdca992d91179d6e0e39cafd6e46c27c8bf" +dependencies = [ + "anyhow", + "chrono", + "falco_event_derive", + "nix", + "thiserror", + "typed-path", +] + +[[package]] +name = "falco_event_derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e363bdc10de8d70c243bdc226b01e18301e931fe7a2ac4659f0a99d2a2fd66b1" +dependencies = [ + "attribute-derive", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "falco_plugin" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b49e7eb3fc78558f87bda401fe76831e387621308bab8b204c176a24fe63d56" +dependencies = [ + "anyhow", + "bumpalo", + "falco_event", + "falco_plugin_api", + "falco_plugin_derive", + "lock_api", + "log", + "memchr", + "num-derive", + "num-traits", + "phf", + "refcell-lock-api", + "schemars", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "falco_plugin_api" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df234b83eb74c62476fe92d6f31dc7489874341ee1a59d3787326fe62ef7ade" + +[[package]] +name = "falco_plugin_derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4a4a8e105e1f04f0bf3128143107bd00567d3cadb92538c5bcfd454d464af3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.4.0", + "hyper 1.8.1", + "hyper-util", + "rustls 0.23.37", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.8.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.3", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +dependencies = [ + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cbb1126afed61dd6368748dae63b1ee7dc480191c6262a3b4ff1e29d86a6c5b" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d713258393a82f091ead52047ca779d37e5766226d009de21696c4e667044368" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quote-use" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" +dependencies = [ + "quote", + "quote-use-macros", +] + +[[package]] +name = "quote-use-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "refcell-lock-api" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef5aec54439264f95d29cba927a8546d1950a38948ee4c80aba74fbfaea1116" +dependencies = [ + "cfg_aliases", + "lock_api", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint 0.4.9", + "hmac", + "zeroize", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.103.9", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.37", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-path" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c462d18470a2857aa657d338af5fa67170bb48bcc80a296710ce3b0802a32566" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/plugins/cloudtrail_rs/Cargo.toml b/plugins/cloudtrail_rs/Cargo.toml new file mode 100644 index 00000000..82fbb73c --- /dev/null +++ b/plugins/cloudtrail_rs/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "cloudtrail" +version = "0.16.0" +edition = "2021" +authors = ["The Falco Authors"] +license = "Apache-2.0" +description = "reads cloudtrail JSON data saved to file in the directory specified in the settings" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +falco_event = "0.5.1" +falco_plugin = "0.5.1" +anyhow = "1" +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1", features = ["preserve_order"] } +serde_spanned = "1" +schemars = "1" +aws-config = "1" +aws-sdk-s3 = "1" +aws-sdk-sqs = "1" +aws-types = "1" +tokio = { version = "1", features = ["rt-multi-thread", "sync"] } +flate2 = "1" +regex = "1" +chrono = "0.4" + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +strip = true diff --git a/plugins/cloudtrail_rs/Makefile b/plugins/cloudtrail_rs/Makefile new file mode 100644 index 00000000..f63e56a0 --- /dev/null +++ b/plugins/cloudtrail_rs/Makefile @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (C) 2026 The Falco Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# + +.PHONY: all build clean release debug test + +NAME := cloudtrail +OUTPUT := lib$(NAME).so + +all: release + +build: release + +release: + @echo "Building release version..." + cargo build --release + @cp target/release/lib$(NAME).so $(OUTPUT) || cp target/release/lib$(NAME).dylib $(OUTPUT) 2>/dev/null || true + +debug: + @echo "Building debug version..." + cargo build + @cp target/debug/lib$(NAME).so $(OUTPUT) || cp target/debug/lib$(NAME).dylib $(OUTPUT) 2>/dev/null || true + +clean: + cargo clean + rm -f $(OUTPUT) + +test: + cargo test + +check: + cargo clippy -- -D warnings + cargo fmt -- --check + +fmt: + cargo fmt + +readme: + @echo "Generate README with falco plugin tool if available" diff --git a/plugins/cloudtrail_rs/README.md b/plugins/cloudtrail_rs/README.md new file mode 100644 index 00000000..f9636d5d --- /dev/null +++ b/plugins/cloudtrail_rs/README.md @@ -0,0 +1,246 @@ +# Falcosecurity Cloudtrail Plugin, Rust version + +This directory contains a Rust version of the cloudtrail plugin, which can fetch log files containing [cloudtrail](https://aws.amazon.com/cloudtrail/) events, parse the log files, and emit sinsp/scap events (e.g. the events used by Falco) for each cloudtrail log entry. + +The plugin can be configured to obtain log files from: + +* A S3 bucket +* A SQS queue that passes along SNS notifications about new log files +* A local filesystem path + +The plugin also exports fields that extract information from a cloudtrail event, such as the event time, the aws region, S3 bucket/EC2 instance names, etc. + +## Event Source + +The event source for cloudtrail events is `aws_cloudtrail`. + +## Supported Fields + +Here is the current set of supported fields: + + +| NAME | TYPE | ARG | DESCRIPTION | +|------------------------------------------|----------|------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ct.id` | `string` | None | the unique ID of the cloudtrail event (eventID in the json). | +| `ct.error` | `string` | None | The error code from the event. Will be "" (e.g. the NULL/empty/none value) if there was no error. | +| `ct.errormessage` | `string` | None | The description of an error. Will be "" (e.g. the NULL/empty/none value) if there was no error. | +| `ct.time` | `string` | None | the timestamp of the cloudtrail event (eventTime in the json). | +| `ct.src` | `string` | None | the source of the cloudtrail event (eventSource in the json). | +| `ct.shortsrc` | `string` | None | the source of the cloudtrail event (eventSource in the json, without the '.amazonaws.com' trailer). | +| `ct.name` | `string` | None | the name of the cloudtrail event (eventName in the json). | +| `ct.user` | `string` | None | the user of the cloudtrail event (userIdentity.userName in the json). For AssumedRole events, this is the role name from sessionContext.sessionIssuer.userName. | +| `ct.originaluser` | `string` | None | the user name as seen in CloudTrail. For AssumedRole events, this is the session name extracted from userIdentity.arn or userIdentity.principalId. For all other identity types, this is userIdentity.userName. | +| `ct.user.accountid` | `string` | None | the account id of the user of the cloudtrail event. | +| `ct.user.identitytype` | `string` | None | the kind of user identity (e.g. Root, IAMUser,AWSService, etc.) | +| `ct.user.principalid` | `string` | None | A unique identifier for the user that made the request. | +| `ct.user.arn` | `string` | None | the Amazon Resource Name (ARN) of the user that made the request. | +| `ct.region` | `string` | None | the region of the cloudtrail event (awsRegion in the json). | +| `ct.response.subnetid` | `string` | None | the subnet ID included in the response. | +| `ct.response.reservationid` | `string` | None | the reservation ID included in the response. | +| `ct.response` | `string` | None | All response elements. | +| `ct.request.availabilityzone` | `string` | None | the availability zone included in the request. | +| `ct.request.cluster` | `string` | None | the cluster included in the request. | +| `ct.request.functionname` | `string` | None | the function name included in the request. | +| `ct.request.groupname` | `string` | None | the group name included in the request. | +| `ct.request.host` | `string` | None | the host included in the request | +| `ct.request.name` | `string` | None | the name of the entity being acted on in the request. | +| `ct.request.policy` | `string` | None | the policy included in the request | +| `ct.request.reason` | `string` | None | the reason included in the request. | +| `ct.request.target` | `string` | None | the target included in the request. | +| `ct.request.documentname` | `string` | None | the document name included in the request. | +| `ct.request.serialnumber` | `string` | None | the serial number provided in the request. | +| `ct.request.servicename` | `string` | None | the service name provided in the request. | +| `ct.request.subnetid` | `string` | None | the subnet ID provided in the request. | +| `ct.request.taskdefinition` | `string` | None | the task definition prrovided in the request. | +| `ct.request.username` | `string` | None | the username provided in the request. | +| `ct.request` | `string` | None | All request parameters. | +| `ct.srcip` | `string` | None | the IP address generating the event (sourceIPAddress in the json). | +| `ct.useragent` | `string` | None | the user agent generating the event (userAgent in the json). | +| `ct.info` | `string` | None | summary information about the event. This varies depending on the event type and, for some events, it contains event-specific details. | +| `ct.managementevent` | `string` | None | 'true' if the event is a management event (AwsApiCall, AwsConsoleAction, AwsConsoleSignIn, or AwsServiceEvent), 'false' otherwise. | +| `ct.readonly` | `string` | None | 'true' if the event only reads information (e.g. DescribeInstances), 'false' if the event modifies the state (e.g. RunInstances, CreateLoadBalancer...). | +| `ct.requestid` | `string` | None | The value that identifies the request. | +| `ct.eventtype` | `string` | None | Identifies the type of event that generated the event record. | +| `ct.apiversion` | `string` | None | The API version associated with the AwsApiCall eventType value. | +| `ct.resources` | `string` | None | A list of resources accessed in the event. | +| `ct.recipientaccountid` | `string` | None | The account ID that received this event. | +| `ct.serviceeventdetails` | `string` | None | Identifies the service event, including what triggered the event and the result. | +| `ct.sharedeventid` | `string` | None | GUID generated by CloudTrail to uniquely identify CloudTrail events. | +| `ct.vpcendpointid` | `string` | None | Identifies the VPC endpoint in which requests were made. | +| `ct.eventcategory` | `string` | None | Shows the event category that is used in LookupEvents calls. | +| `ct.addendum.reason` | `string` | None | The reason that the event or some of its contents were missing. | +| `ct.addendum.updatedfields` | `string` | None | The event record fields that are updated by the addendum. | +| `ct.addendum.originalrequestid` | `string` | None | The original unique ID of the request. | +| `ct.addendum.originaleventid` | `string` | None | The original event ID. | +| `ct.sessioncredentialfromconsole` | `string` | None | Shows whether or not an event originated from an AWS Management Console session. | +| `ct.edgedevicedetails` | `string` | None | Information about edge devices that are targets of a request. | +| `ct.tlsdetails.tlsversion` | `string` | None | The TLS version of a request. | +| `ct.tlsdetails.ciphersuite` | `string` | None | The cipher suite (combination of security algorithms used) of a request. | +| `ct.tlsdetails.clientprovidedhostheader` | `string` | None | The client-provided host name used in the service API call. | +| `ct.additionaleventdata` | `string` | None | All additional event data attributes. | +| `s3.uri` | `string` | None | the s3 URI (s3:///). | +| `s3.bucket` | `string` | None | the bucket name for s3 events. | +| `s3.key` | `string` | None | the S3 key name. | +| `s3.bytes` | `uint64` | None | the size of an s3 download or upload, in bytes. | +| `s3.bytes.in` | `uint64` | None | the size of an s3 upload, in bytes. | +| `s3.bytes.out` | `uint64` | None | the size of an s3 download, in bytes. | +| `s3.cnt.get` | `uint64` | None | the number of get operations. This field is 1 for GetObject events, 0 otherwise. | +| `s3.cnt.put` | `uint64` | None | the number of put operations. This field is 1 for PutObject events, 0 otherwise. | +| `s3.cnt.other` | `uint64` | None | the number of non I/O operations. This field is 0 for GetObject and PutObject events, 1 for all the other events. | +| `ec2.name` | `string` | None | the name of the ec2 instances, typically stored in the instance tags. | +| `ec2.imageid` | `string` | None | the ID for the image used to run the ec2 instance in the response. | +| `ecr.repository` | `string` | None | the name of the ecr Repository specified in the request. | +| `ecr.imagetag` | `string` | None | the tag of the image specified in the request. | +| `iam.role` | `string` | None | the IAM role specified in the request. | +| `iam.policy` | `string` | None | the IAM policy specified in the request. | + + +## Handling AWS Authentication + +When reading log files from a S3 bucket or when reading SNS notifications from a SQS queue, the plugin needs authentication credentials and to be configured with an AWS Region. The plugin relies on the same authentication mechanisms used by the [AWS Go SDK](https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/#specifying-credentials): + +* Environment Variables: specify the aws region with `AWS_REGION=xxx`, the access key id with `AWS_ACCESS_KEY_ID=xxx`, and the secret key with `AWS_SECRET_ACCESS_KEY=xxx`. Here's a sample command line: + +```shell +AWS_DEFAULT_REGION=us-west-1 AWS_ACCESS_KEY_ID=XXX AWS_SECRET_ACCESS_KEY=XXX falco -c -r +``` + +* Shared Configuration Files: specify the aws region in a file at `$HOME/.aws/config` and the credentials in a file at `$HOME/.aws/credentials`. Here are example files: + +#### **`$HOME/.aws/config`** +```shell +[default] +region = us-west-1 +``` + +#### **`$HOME/.aws/credentials`** +```shell +[default] +aws_access_key_id= +aws_secret_access_key= +``` + +## Configuration + +### Plugin Initialization + +The format of the initialization string is a json object. Here's an example: + +```json +{"sqsDelete": false, "s3DownloadConcurrency": 64, "useS3SNS": true} +``` + +The json object has the following properties: + +* `sqsDelete`: value is boolean. If true, then the plugin will delete sqs messages from the queue immediately after receiving them. (Default: true) +* `s3DownloadConcurrency`: value is numeric. Controls the number of background goroutines used to download S3 files. (Default: 1) +* `s3Interval`: value is string. Download log files matching the specified time interval. Note that this matches log file *names*, not event timestamps. CloudTrail logs usually cover [the previous 5 minutes of activity](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/get-and-view-cloudtrail-log-files.html). See *Time Intervals* below for possible formats. +* `useS3SNS`: value is boolean. If true, then the plugin will expect SNS messages to originate from S3 instead of directly from Cloudtrail (Default: false) +* `s3AccountList`: value is string. Download log files matching the specified account IDs (in a comma separated list) in an organization trail. See *Read From S3 Bucket Directly* below for more details. +* `sqsOwnerAccount`: value is string. The AWS account ID that owns the SQS queue in case the queue is owned by a different account. Not required by default. +* `aws`: value is object. AWS SDK config override block. + * `profile`: value is string. Overrides shared AWS profile (for example default). (Default: empty) + * `region`: value is string. Overrides AWS region used by the plugin. (Default: empty) + * `config`: value is string. Overrides shared config file path (for example ~/.aws/config). (Default: empty) + * `credentials`: value is string. Overrides shared credentials file path (for example ~/.aws/credentials). (Default: empty) +* `useAsync`: value is boolean. Enables async extraction optimization. (Default: true) + +The init string can be the empty string, which is treated identically to `{}`. + +### Time Intervals + +S3Interval values can be individual duration values or RFC 3339-style timestamps. In this case the interval will start at the specified time and end at the current time: +* `5d`: A simple duration relative to the current time. +* `2021-03-30T18:07:17Z`: A duration starting from the specified RFC 3339 formatted time. + +Simple durations must be a positive integer followed by `w` for weeks, `d` for days, `h` for hours, `m` for minutes, or `s` for seconds. +RFC 3339-style times must be formatted as a datestamp, the letter `T`, a timestamp with no fractional seconds, and the letter `Z`, e.g. `2021-03-30T18:07:17Z`. + +Values can also cover a range: +* `5d-2d`: A simple duration interval relative to the current time. +* `2023-04-05T06:00:00Z-2023-04-05T12:00:10Z`: An RFC 3339-style timestamp interval. +* `2023-04-05T06:00:00Z-5d`: A combination of an RFC 3339-style timestamp and a duration. + +### Plugin Open Params + +The format of the open params string is a uri-like string with one of the following forms: + +* `s3://[/]` +* `sqs://` +* `` + +We describe each of these below. + +#### Read From S3 Bucket Directly + +When using `s3:///[]`, the plugin will scan the bucket a single time for all objects. Characters up to the first slash/end of string will be used as the S3 bucket name, and any remaining characters will be treated as a key prefix. After reading all objects, the plugin will return EOF. + +All objects below the bucket, or below the bucket + prefix, will be considered cloudtrail logs. Any object ending in .json.gz will be decompressed first. + +For example, if a bucket `my-s3-bucket` contained cloudtrail logs below a prefix `AWSLogs/411571310278/CloudTrail/us-west-1/2021/09/23/`, Using an open params of `s3://my-s3-bucket/AWSLogs/411571310278/CloudTrail/us-west-1/2021/09/23/` would configure the plugin to read all files below `AWSLogs/411571310278/CloudTrail/us-west-1/2021/09/23/` as cloudtrail logs and then return EOF. No other files in the bucket will be read. + +For organization trails the files are normally stored like `s3://bucket_name/prefix_name/AWSLogs/O-ID/Account ID/CloudTrail/Region/YYYY/MM/DD/file_name.json.gz`. Using an open parameter of `s3//my-s3-bucket/AWSLogs/o-123abc/` would configure the plugin to read all files for all account IDs in the organization `o-123abc`, for all regions and the entire retention time. Therefore it makes sense to combine this open parameter with `S3AccountList` and `S3Interval` parameters. `S3AccountList` is a comma separated string with account IDs to query. + +Setting `S3AccountList` to `012345678912,987654321012` and `S3Interval` to `3d-1d` with open parameter `s3://my-s3-bucket/AWSLogs/o-123abc/` would get all events for account IDs 12345678912 and 987654321012 for all regions from 3 days ago up to to 1 day ago. + +#### Read from SQS Queue + +When using `sqs://`, the plugin will read messages from the provided SQS Queue. The messages are assumed to be [SNS Notifications](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/configure-sns-notifications-for-cloudtrail.html) that announce the presence of new Cloudtrail log files in a S3 bucket. Each new file will be read from the provided s3 bucket. + +In case the queue is owned by another AWS account, use the `SQSOwnerAccount` parameter to specify the account ID of the queue's owner. Note that the queue owner must grant you the necessary permissions to access the queue. + +In this mode, the plugin polls the queue forever, waiting for new log files. + +#### Read single file + +All other open params are interpreted as a filesystem path to a single cloudtrail log file. This fill will be read and parsed. When complete, the plugin returns EOF. + +### `falco.yaml` Example + +Here is a complete `falco.yaml` snippet showing valid configurations for the cloudtrail plugin: + +```yaml +# Cloudtrail reading from a S3 Bucket +plugins: + - name: cloudtrail + library_path: libcloudtrail.so + init_config: "" + open_params: "s3://my-s3-bucket/AWSLogs/411571310278/CloudTrail/us-west-1/2021/09/23/" + +# Optional. If not specified the first entry in plugins is used. +load_plugins: [cloudtrail, json] +``` + +```yaml +# Cloudtrail reading from a SQS Queue +plugins: + - name: cloudtrail + library_path: libcloudtrail.so + init_config: '{"sqsDelete": true}' + open_params: "sqs://my-sqs-queue" + +# Optional. If not specified the first entry in plugins is used. +load_plugins: [cloudtrail, json] +``` + +```yaml +# Cloudtrail reading from a single file +plugins: + - name: cloudtrail + library_path: libcloudtrail.so + init_config: "" + open_params: "/home/user/cloudtrail-logs/059797578166_CloudTrail_us-east-1_20210209T0130Z_65lDDH3uferZH5Br.json.gz" + +# Optional. If not specified the first entry in plugins is used. +load_plugins: [cloudtrail, json] +``` + +### Using SNS/SQS to Route Cloudtrail Events to the Plugin + +Note that the plugin does not create any Cloudtrails, S3 Buckets, SNS Notifications, or SQS Queues. It assumes that those resources have already been created. + +The general steps involve: + +1. Creating a Cloudtrail with the events you would like to monitor: https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-create-and-update-a-trail.html +2. Configuring SNS Notifications for Cloudtrail: https://docs.aws.amazon.com/awscloudtrail/latest/userguide/configure-sns-notifications-for-cloudtrail.html +3. Creating a SQS Queue to receive the SNS Notifications: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-configure-subscribe-queue-sns-topic.html diff --git a/plugins/cloudtrail_rs/src/aws.rs b/plugins/cloudtrail_rs/src/aws.rs new file mode 100644 index 00000000..1cda17ef --- /dev/null +++ b/plugins/cloudtrail_rs/src/aws.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +/* +Copyright (C) 2026 The Falco Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use anyhow::Result; +use aws_types::region::Region; +use aws_types::SdkConfig; + +use crate::config::AwsConfig; + +/// Load the AWS SDK config from the given AwsConfig settings +pub async fn load_aws_config(aws: &AwsConfig) -> Result { + // Apply config and credentials file overrides via env vars. + // Safety: called once during initialization before any concurrent access. + if !aws.config.is_empty() { + unsafe { + std::env::set_var("AWS_CONFIG_FILE", &aws.config); + } + } + if !aws.credentials.is_empty() { + unsafe { + std::env::set_var("AWS_SHARED_CREDENTIALS_FILE", &aws.credentials); + } + } + + let mut loader = aws_config::defaults(aws_config::BehaviorVersion::latest()); + + if !aws.profile.is_empty() { + loader = loader.profile_name(&aws.profile); + } + + if !aws.region.is_empty() { + loader = loader.region(Region::new(aws.region.clone())); + } + + Ok(loader.load().await) +} diff --git a/plugins/cloudtrail_rs/src/config.rs b/plugins/cloudtrail_rs/src/config.rs new file mode 100644 index 00000000..5647d493 --- /dev/null +++ b/plugins/cloudtrail_rs/src/config.rs @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +/* +Copyright (C) 2026 The Falco Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// AWS SDK configuration +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(default)] +pub struct AwsConfig { + #[serde(rename = "profile")] + #[schemars( + title = "AWS Profile", + description = "If non-empty overrides the AWS shared configuration profile (e.g. 'default') and environment variables such as AWS_PROFILE (Default: empty)" + )] + pub profile: String, + + #[serde(rename = "region")] + #[schemars( + title = "AWS Region", + description = "If non-empty overrides the AWS region specified in the profile (e.g. 'us-east-1') and environment variables such as AWS_REGION (Default: empty)" + )] + pub region: String, + + #[serde(rename = "config")] + #[schemars( + title = "Shared AWS Config File", + description = "If non-empty overrides the AWS shared configuration filepath (e.g. ~/.aws/config) and env variables such as AWS_CONFIG_FILE (Default: empty)" + )] + pub config: String, + + #[serde(rename = "credentials")] + #[schemars( + title = "Shared AWS Credentials File", + description = "If non-empty overrides the AWS shared credentials filepath (e.g. ~/.aws/credentials) and env variables such as AWS_SHARED_CREDENTIALS_FILE (Default: empty)" + )] + pub credentials: String, +} + +impl Default for AwsConfig { + fn default() -> Self { + AwsConfig { + profile: String::new(), + region: String::new(), + config: String::new(), + credentials: String::new(), + } + } +} + +/// Plugin configuration for the CloudTrail plugin +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(default)] +pub struct PluginConfig { + #[serde(rename = "s3DownloadConcurrency")] + #[schemars( + title = "S3 download concurrency", + description = "Controls the number of background goroutines used to download S3 files (Default: 32)" + )] + pub s3_download_concurrency: i32, + + #[serde(rename = "s3Interval")] + #[schemars( + title = "S3 log interval", + description = "Download log files over the specified interval (Default: no interval)" + )] + pub s3_interval: String, + + #[serde(rename = "sqsDelete")] + #[schemars( + title = "Delete SQS messages", + description = "If true then the plugin will delete SQS messages from the queue immediately after receiving them (Default: true)" + )] + pub sqs_delete: bool, + + #[serde(rename = "useAsync")] + #[schemars( + title = "Use async extraction (ignored)", + description = "Ignored. This option is present for compatibility with the original Go version of this plugin." + )] + pub use_async: bool, + + #[serde(rename = "useS3SNS")] + #[schemars( + title = "Use S3 SNS", + description = "If true then the plugin will expect SNS messages to originate from S3 instead of directly from Cloudtrail (Default: false)" + )] + pub use_s3_sns: bool, + + #[serde(rename = "s3AccountList")] + #[schemars( + title = "S3 account list", + description = "A comma separated list of account IDs for organizational Cloudtrails (Default: no account IDs)" + )] + pub s3_account_list: String, + + #[serde(rename = "sqsOwnerAccount")] + #[schemars( + title = "SQS owner account", + description = "The AWS account ID that owns the SQS queue in case the queue is owned by a different account (Default: no account ID)" + )] + pub sqs_owner_account: String, + + #[serde(rename = "aws")] + pub aws: AwsConfig, +} + +impl Default for PluginConfig { + fn default() -> Self { + PluginConfig { + s3_download_concurrency: 32, + s3_interval: String::new(), + sqs_delete: true, + use_async: true, + use_s3_sns: false, + s3_account_list: String::new(), + sqs_owner_account: String::new(), + aws: AwsConfig::default(), + } + } +} diff --git a/plugins/cloudtrail_rs/src/extract.rs b/plugins/cloudtrail_rs/src/extract.rs new file mode 100644 index 00000000..04e50e08 --- /dev/null +++ b/plugins/cloudtrail_rs/src/extract.rs @@ -0,0 +1,1725 @@ +// SPDX-License-Identifier: Apache-2.0 +/* +Copyright (C) 2026 The Falco Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use crate::CloudTrailPlugin; +use falco_plugin::anyhow::{anyhow, Error}; +use falco_plugin::event::{PluginEvent, events::Event}; +use falco_plugin::extract::{ + field, EventInput, ExtractByteRange, ExtractFieldInfo, ExtractPlugin, ExtractRequest, +}; +use serde_spanned::Spanned; +use std::ffi::CString; +use std::ops::Range; + +#[derive(Default)] +pub struct CloudTrailContext { + last_event_num: usize, + json_value: Option, + raw_event_data: Vec, +} + +impl CloudTrailPlugin { + fn ensure_json<'a>( + context: &'a mut CloudTrailContext, + event: &EventInput<'_, Event>>, + ) -> Result<&'a serde_json::Value, Error> { + let event_num = event.event_number(); + if event_num != context.last_event_num || context.json_value.is_none() { + let evt = event.event()?; + context.raw_event_data = evt.params.event_data.to_vec(); + // Trim trailing null bytes + while context.raw_event_data.last() == Some(&0) { + context.raw_event_data.pop(); + } + context.json_value = Some(serde_json::from_slice(&context.raw_event_data)?); + context.last_event_num = event_num; + } + context + .json_value + .as_ref() + .ok_or_else(|| anyhow!("No JSON value parsed")) + } + + /// Extract a string value at a JSON pointer path. + fn do_extract_string( + context: &mut CloudTrailContext, + event: &EventInput<'_, Event>>, + path: &str, + ) -> Result>, Error> { + let _ = Self::ensure_json(context, event); + let value = context + .json_value + .as_ref() + .and_then(|json| json.pointer(path)?.as_str().map(String::from)); + match value { + Some(s) => { + let span = find_json_pointer_range(&context.raw_event_data, path) + .unwrap_or(0..0); + Ok(Some(Spanned::new(span, CString::new(s)?))) + } + None => Ok(None), + } + } + + /// Extract any JSON value at a path, serializing non-strings to JSON. + fn do_extract_value( + context: &mut CloudTrailContext, + event: &EventInput<'_, Event>>, + path: &str, + ) -> Result>, Error> { + let _ = Self::ensure_json(context, event); + let value = context.json_value.as_ref().and_then(|json| { + let val = json.pointer(path)?; + let s = if let Some(s) = val.as_str() { + s.to_string() + } else { + serde_json::to_string(val).ok()? + }; + Some(s) + }); + match value { + Some(s) => { + let span = find_json_pointer_range(&context.raw_event_data, path) + .unwrap_or(0..0); + Ok(Some(Spanned::new(span, CString::new(s)?))) + } + None => Ok(None), + } + } + + fn finish_extract( + result: Result>, Error>, + offset: &mut ExtractByteRange, + ) -> Result, Error> { + match result? { + Some(spanned) => { + if matches!(*offset, ExtractByteRange::Requested) { + let span = spanned.span(); + if !span.is_empty() { + *offset = ExtractByteRange::in_plugin_data(span); + } + } + Ok(Some(spanned.into_inner())) + } + None => Ok(None), + } + } + + // ----------------------------------------------------------------------- + // ct.* fields + // ----------------------------------------------------------------------- + + fn extract_id(&mut self, req: ExtractRequest) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/eventID"), + req.offset, + ) + } + + fn extract_error(&mut self, req: ExtractRequest) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/errorCode"), + req.offset, + ) + } + + fn extract_errormessage( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/errorMessage"), + req.offset, + ) + } + + fn extract_time(&mut self, req: ExtractRequest) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/eventTime"), + req.offset, + ) + } + + fn extract_src(&mut self, req: ExtractRequest) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/eventSource"), + req.offset, + ) + } + + fn extract_shortsrc(&mut self, req: ExtractRequest) -> Result, Error> { + let context = req.context; + let event = req.event; + let offset = req.offset; + + let result = Self::do_extract_string(context, event, "/eventSource")?; + match result { + Some(spanned) => { + let src = spanned.get_ref().to_str().map_err(|e| anyhow!("{}", e))?; + let suffix = ".amazonaws.com"; + let trimmed = if src.ends_with(suffix) { + &src[..src.len() - suffix.len()] + } else { + src + }; + if matches!(*offset, ExtractByteRange::Requested) { + let span = spanned.span(); + if !span.is_empty() { + *offset = ExtractByteRange::in_plugin_data(span); + } + } + Ok(Some(CString::new(trimmed)?)) + } + None => Ok(None), + } + } + + fn extract_name(&mut self, req: ExtractRequest) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/eventName"), + req.offset, + ) + } + + fn extract_user(&mut self, req: ExtractRequest) -> Result, Error> { + let context = req.context; + let event = req.event; + let _ = Self::ensure_json(context, event); + + let json = match context.json_value.as_ref() { + Some(j) => j, + None => return Ok(None), + }; + + let utype = match json.pointer("/userIdentity/type").and_then(|v| v.as_str()) { + Some(t) => t.to_string(), + None => return Ok(None), + }; + + let username = match utype.as_str() { + "Root" | "IAMUser" => json + .pointer("/userIdentity/userName") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()), + "AWSService" => json + .pointer("/userIdentity/invokedBy") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()), + "AssumedRole" => { + let v = json + .pointer("/userIdentity/sessionContext/sessionIssuer/userName") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + if v.is_none() { + Some("AssumedRole".to_string()) + } else { + v + } + } + "AWSAccount" => Some("AWSAccount".to_string()), + "FederatedUser" => Some("FederatedUser".to_string()), + _ => return Ok(None), + }; + + match username { + Some(name) => Ok(Some(CString::new(name)?)), + None => Ok(Some(CString::new("")?)), + } + } + + fn extract_originaluser( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + let context = req.context; + let event = req.event; + let _ = Self::ensure_json(context, event); + + let json = match context.json_value.as_ref() { + Some(j) => j, + None => return Ok(None), + }; + + let utype = json + .pointer("/userIdentity/type") + .and_then(|v| v.as_str()); + + if utype == Some("AssumedRole") { + // Try ARN: split by '/', if 3 parts return last + if let Some(arn) = json + .pointer("/userIdentity/arn") + .and_then(|v| v.as_str()) + { + let parts: Vec<&str> = arn.split('/').collect(); + if parts.len() == 3 { + return Ok(Some(CString::new(parts[2])?)); + } + } + + // Try principalId: split by ':', if 2 parts return second + if let Some(principal) = json + .pointer("/userIdentity/principalId") + .and_then(|v| v.as_str()) + { + let parts: Vec<&str> = principal.split(':').collect(); + if parts.len() == 2 { + return Ok(Some(CString::new(parts[1])?)); + } + } + + return Ok(None); + } + + // For all other identity types, return userIdentity.userName + Self::finish_extract( + Self::do_extract_string(context, event, "/userIdentity/userName"), + req.offset, + ) + } + + fn extract_user_accountid( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + let context = req.context; + let event = req.event; + let offset = req.offset; + + let result = Self::do_extract_string(context, event, "/userIdentity/accountId")?; + if result.is_some() { + return Self::finish_extract(Ok(result), offset); + } + Self::finish_extract( + Self::do_extract_string(context, event, "/recipientAccountId"), + offset, + ) + } + + fn extract_user_identitytype( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/userIdentity/type"), + req.offset, + ) + } + + fn extract_user_principalid( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/userIdentity/principalId"), + req.offset, + ) + } + + fn extract_user_arn(&mut self, req: ExtractRequest) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/userIdentity/arn"), + req.offset, + ) + } + + fn extract_region(&mut self, req: ExtractRequest) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/awsRegion"), + req.offset, + ) + } + + fn extract_response_subnetid( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/responseElements/subnetId"), + req.offset, + ) + } + + fn extract_response_reservationid( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string( + req.context, + req.event, + "/responseElements/reservationId", + ), + req.offset, + ) + } + + fn extract_response(&mut self, req: ExtractRequest) -> Result, Error> { + Self::finish_extract( + Self::do_extract_value(req.context, req.event, "/responseElements"), + req.offset, + ) + } + + fn extract_request_availabilityzone( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string( + req.context, + req.event, + "/requestParameters/availabilityZone", + ), + req.offset, + ) + } + + fn extract_request_cluster( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/requestParameters/cluster"), + req.offset, + ) + } + + fn extract_request_functionname( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string( + req.context, + req.event, + "/requestParameters/functionName", + ), + req.offset, + ) + } + + fn extract_request_groupname( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/requestParameters/groupName"), + req.offset, + ) + } + + fn extract_request_host( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/requestParameters/Host"), + req.offset, + ) + } + + fn extract_request_name( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/requestParameters/name"), + req.offset, + ) + } + + fn extract_request_policy( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/requestParameters/policy"), + req.offset, + ) + } + + fn extract_request_reason( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/requestParameters/reason"), + req.offset, + ) + } + + fn extract_request_target( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/requestParameters/target"), + req.offset, + ) + } + + fn extract_request_documentname( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string( + req.context, + req.event, + "/requestParameters/documentName", + ), + req.offset, + ) + } + + fn extract_request_serialnumber( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string( + req.context, + req.event, + "/requestParameters/serialNumber", + ), + req.offset, + ) + } + + fn extract_request_servicename( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string( + req.context, + req.event, + "/requestParameters/serviceName", + ), + req.offset, + ) + } + + fn extract_request_subnetid( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/requestParameters/subnetId"), + req.offset, + ) + } + + fn extract_request_taskdefinition( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string( + req.context, + req.event, + "/requestParameters/taskDefinition", + ), + req.offset, + ) + } + + fn extract_request_username( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/requestParameters/userName"), + req.offset, + ) + } + + fn extract_request(&mut self, req: ExtractRequest) -> Result, Error> { + Self::finish_extract( + Self::do_extract_value(req.context, req.event, "/requestParameters"), + req.offset, + ) + } + + fn extract_srcip(&mut self, req: ExtractRequest) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/sourceIPAddress"), + req.offset, + ) + } + + fn extract_useragent(&mut self, req: ExtractRequest) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/userAgent"), + req.offset, + ) + } + + fn extract_info(&mut self, req: ExtractRequest) -> Result, Error> { + let context = req.context; + let event = req.event; + let _ = Self::ensure_json(context, event); + + let json = match context.json_value.as_ref() { + Some(j) => j, + None => return Ok(None), + }; + + // Get user + let user = get_user_string(json); + + // Get source IP + let srcip = json + .pointer("/sourceIPAddress") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + // Get event name + let evtname = match json.pointer("/eventName").and_then(|v| v.as_str()) { + Some(n) => n, + None => { + return Ok(Some(CString::new( + "", + )?)); + } + }; + + // Error symbol + let err_symbol = if json.get("errorCode").is_some() { + "!" + } else { + "" + }; + + // Read/write symbol + let readonly = json + .pointer("/readOnly") + .and_then(|v| v.as_bool()) + .unwrap_or(true); + let rw_symbol = if readonly { "←" } else { "→" }; + + // Build info string + let base = if user == srcip { + format!("{} {}{} {}", user, err_symbol, rw_symbol, evtname) + } else { + format!( + "{} via {} {}{} {}", + user, srcip, err_symbol, rw_symbol, evtname + ) + }; + + // Add s3 info if available + let mut info = base; + + // Check for s3 bytes + let bytes_in = json + .pointer("/additionalEventData/bytesTransferredIn") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0) as u64; + let bytes_out = json + .pointer("/additionalEventData/bytesTransferredOut") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0) as u64; + let total_bytes = bytes_in + bytes_out; + if total_bytes > 0 { + info.push_str(&format!(" Size={}", total_bytes)); + } + + // Check for s3 URI + if let (Some(bucket), Some(key)) = ( + json.pointer("/requestParameters/bucketName") + .and_then(|v| v.as_str()), + json.pointer("/requestParameters/key") + .and_then(|v| v.as_str()), + ) { + info.push_str(&format!(" URI=s3://{}/{}", bucket, key)); + return Ok(Some(CString::new(info)?)); + } + + if let Some(bucket) = json + .pointer("/requestParameters/bucketName") + .and_then(|v| v.as_str()) + { + info.push_str(&format!(" Bucket={}", bucket)); + return Ok(Some(CString::new(info)?)); + } + + if let Some(key) = json + .pointer("/requestParameters/key") + .and_then(|v| v.as_str()) + { + info.push_str(&format!(" Key={}", key)); + return Ok(Some(CString::new(info)?)); + } + + if let Some(host) = json + .pointer("/requestParameters/Host") + .and_then(|v| v.as_str()) + { + info.push_str(&format!(" Host={}", host)); + return Ok(Some(CString::new(info)?)); + } + + Ok(Some(CString::new(info)?)) + } + + fn extract_managementevent( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + let context = req.context; + let event = req.event; + let offset = req.offset; + let _ = Self::ensure_json(context, event); + + let val = context + .json_value + .as_ref() + .and_then(|json| json.get("managementEvent")) + .and_then(|v| v.as_bool()); + + match val { + Some(b) => { + let span = + find_json_pointer_range(&context.raw_event_data, "/managementEvent") + .unwrap_or(0..0); + if matches!(*offset, ExtractByteRange::Requested) && !span.is_empty() { + *offset = ExtractByteRange::in_plugin_data(span); + } + let s = if b { "true" } else { "false" }; + Ok(Some(CString::new(s)?)) + } + None => Ok(None), + } + } + + fn extract_readonly(&mut self, req: ExtractRequest) -> Result, Error> { + let context = req.context; + let event = req.event; + let offset = req.offset; + let _ = Self::ensure_json(context, event); + + let json = match context.json_value.as_ref() { + Some(j) => j, + None => return Ok(None), + }; + + let ro_val = json.get("readOnly"); + + if ro_val.is_none() { + // Heuristic based on event name prefix + let evtname = json + .pointer("/eventName") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + let write_prefixes = [ + "Start", + "Stop", + "Create", + "Destroy", + "Delete", + "Add", + "Remove", + "Terminate", + "Put", + "Associate", + "Disassociate", + "Attach", + "Detach", + "Open", + "Close", + "Wipe", + "Update", + "Upgrade", + "Unlink", + "Assign", + "Unassign", + "Suspend", + "Set", + "Run", + "Register", + "Deregister", + "Reboot", + "Purchase", + "Modify", + "Initialize", + "Enable", + "Disable", + "Cancel", + "Admin", + "Activate", + ]; + + let is_write = write_prefixes + .iter() + .any(|prefix| evtname.starts_with(prefix)); + let s = if is_write { "false" } else { "true" }; + return Ok(Some(CString::new(s)?)); + } + + let ro = ro_val.unwrap().as_bool().unwrap_or(false); + let span = find_json_pointer_range(&context.raw_event_data, "/readOnly") + .unwrap_or(0..0); + if matches!(*offset, ExtractByteRange::Requested) && !span.is_empty() { + *offset = ExtractByteRange::in_plugin_data(span); + } + let s = if ro { "true" } else { "false" }; + Ok(Some(CString::new(s)?)) + } + + fn extract_requestid( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/requestID"), + req.offset, + ) + } + + fn extract_eventtype( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/eventType"), + req.offset, + ) + } + + fn extract_apiversion( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/apiVersion"), + req.offset, + ) + } + + fn extract_resources(&mut self, req: ExtractRequest) -> Result, Error> { + let context = req.context; + let event = req.event; + let offset = req.offset; + let _ = Self::ensure_json(context, event); + + let arr = context + .json_value + .as_ref() + .and_then(|json| json.get("resources")) + .and_then(|v| v.as_array()) + .filter(|a| !a.is_empty()); + + match arr { + Some(resources) => { + let joined: String = resources + .iter() + .map(|r| serde_json::to_string(r).unwrap_or_default()) + .collect::>() + .join(","); + if joined.is_empty() { + return Ok(None); + } + let span = + find_json_pointer_range(&context.raw_event_data, "/resources") + .unwrap_or(0..0); + if matches!(*offset, ExtractByteRange::Requested) && !span.is_empty() { + *offset = ExtractByteRange::in_plugin_data(span); + } + Ok(Some(CString::new(joined)?)) + } + None => Ok(None), + } + } + + fn extract_recipientaccountid( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/recipientAccountId"), + req.offset, + ) + } + + fn extract_serviceeventdetails( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_value(req.context, req.event, "/serviceEventDetails"), + req.offset, + ) + } + + fn extract_sharedeventid( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/sharedEventID"), + req.offset, + ) + } + + fn extract_vpcendpointid( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/vpcEndpointId"), + req.offset, + ) + } + + fn extract_eventcategory( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/eventCategory"), + req.offset, + ) + } + + fn extract_addendum_reason( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/addendum/reason"), + req.offset, + ) + } + + fn extract_addendum_updatedfields( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/addendum/updatedFields"), + req.offset, + ) + } + + fn extract_addendum_originalrequestid( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/addendum/originalRequestID"), + req.offset, + ) + } + + fn extract_addendum_originaleventid( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/addendum/originalEventID"), + req.offset, + ) + } + + fn extract_sessioncredentialfromconsole( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + let context = req.context; + let event = req.event; + let offset = req.offset; + let _ = Self::ensure_json(context, event); + + let val = context + .json_value + .as_ref() + .and_then(|json| json.get("sessionCredentialFromConsole")) + .and_then(|v| v.as_bool()); + + match val { + Some(b) => { + let span = find_json_pointer_range( + &context.raw_event_data, + "/sessionCredentialFromConsole", + ) + .unwrap_or(0..0); + if matches!(*offset, ExtractByteRange::Requested) && !span.is_empty() { + *offset = ExtractByteRange::in_plugin_data(span); + } + let s = if b { "true" } else { "false" }; + Ok(Some(CString::new(s)?)) + } + None => Ok(None), + } + } + + fn extract_edgedevicedetails( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_value(req.context, req.event, "/edgeDeviceDetails"), + req.offset, + ) + } + + fn extract_tlsdetails_tlsversion( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/tlsDetails/tlsVersion"), + req.offset, + ) + } + + fn extract_tlsdetails_ciphersuite( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/tlsDetails/cipherSuite"), + req.offset, + ) + } + + fn extract_tlsdetails_clientprovidedhostheader( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string( + req.context, + req.event, + "/tlsDetails/clientProvidedHostHeader", + ), + req.offset, + ) + } + + fn extract_additionaleventdata( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_value(req.context, req.event, "/additionalEventData"), + req.offset, + ) + } + + // ----------------------------------------------------------------------- + // s3.* fields + // ----------------------------------------------------------------------- + + fn extract_s3_uri(&mut self, req: ExtractRequest) -> Result, Error> { + let context = req.context; + let event = req.event; + let _ = Self::ensure_json(context, event); + + let json = match context.json_value.as_ref() { + Some(j) => j, + None => return Ok(None), + }; + + let bucket = json + .pointer("/requestParameters/bucketName") + .and_then(|v| v.as_str()); + let key = json + .pointer("/requestParameters/key") + .and_then(|v| v.as_str()); + + match (bucket, key) { + (Some(b), Some(k)) => Ok(Some(CString::new(format!("s3://{}/{}", b, k))?)), + _ => Ok(None), + } + } + + fn extract_s3_bucket(&mut self, req: ExtractRequest) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/requestParameters/bucketName"), + req.offset, + ) + } + + fn extract_s3_key(&mut self, req: ExtractRequest) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/requestParameters/key"), + req.offset, + ) + } + + fn extract_s3_bytes(&mut self, req: ExtractRequest) -> Result, Error> { + let context = req.context; + let event = req.event; + let _ = Self::ensure_json(context, event); + + let json = match context.json_value.as_ref() { + Some(j) => j, + None => return Ok(None), + }; + + let bytes_in = json + .pointer("/additionalEventData/bytesTransferredIn") + .and_then(get_value_u64); + let bytes_out = json + .pointer("/additionalEventData/bytesTransferredOut") + .and_then(get_value_u64); + + if bytes_in.is_none() && bytes_out.is_none() { + return Ok(None); + } + + Ok(Some(bytes_in.unwrap_or(0) + bytes_out.unwrap_or(0))) + } + + fn extract_s3_bytes_in(&mut self, req: ExtractRequest) -> Result, Error> { + let context = req.context; + let event = req.event; + let _ = Self::ensure_json(context, event); + + Ok(context + .json_value + .as_ref() + .and_then(|json| json.pointer("/additionalEventData/bytesTransferredIn")) + .and_then(get_value_u64)) + } + + fn extract_s3_bytes_out(&mut self, req: ExtractRequest) -> Result, Error> { + let context = req.context; + let event = req.event; + let _ = Self::ensure_json(context, event); + + Ok(context + .json_value + .as_ref() + .and_then(|json| json.pointer("/additionalEventData/bytesTransferredOut")) + .and_then(get_value_u64)) + } + + fn extract_s3_cnt_get(&mut self, req: ExtractRequest) -> Result, Error> { + let context = req.context; + let event = req.event; + let _ = Self::ensure_json(context, event); + + let is_get = context + .json_value + .as_ref() + .and_then(|json| json.pointer("/eventName")) + .and_then(|v| v.as_str()) + == Some("GetObject"); + + if is_get { + Ok(Some(1)) + } else { + Ok(None) + } + } + + fn extract_s3_cnt_put(&mut self, req: ExtractRequest) -> Result, Error> { + let context = req.context; + let event = req.event; + let _ = Self::ensure_json(context, event); + + let is_put = context + .json_value + .as_ref() + .and_then(|json| json.pointer("/eventName")) + .and_then(|v| v.as_str()) + == Some("PutObject"); + + if is_put { + Ok(Some(1)) + } else { + Ok(None) + } + } + + fn extract_s3_cnt_other(&mut self, req: ExtractRequest) -> Result, Error> { + let context = req.context; + let event = req.event; + let _ = Self::ensure_json(context, event); + + let evtname = context + .json_value + .as_ref() + .and_then(|json| json.pointer("/eventName")) + .and_then(|v| v.as_str()) + .unwrap_or(""); + + if evtname == "GetObject" || evtname == "PutObject" { + Ok(None) + } else { + Ok(Some(1)) + } + } + + // ----------------------------------------------------------------------- + // ec2.* fields + // ----------------------------------------------------------------------- + + fn extract_ec2_name(&mut self, req: ExtractRequest) -> Result, Error> { + let context = req.context; + let event = req.event; + let _ = Self::ensure_json(context, event); + + let json = match context.json_value.as_ref() { + Some(j) => j, + None => return Ok(None), + }; + + let items = json + .pointer("/requestParameters/tagSpecificationSet/items") + .and_then(|v| v.as_array()); + + let items = match items { + Some(i) => i, + None => return Ok(None), + }; + + for item in items { + let resource_type = item + .pointer("/resourceType") + .and_then(|v| v.as_str()) + .unwrap_or(""); + if resource_type != "instance" { + continue; + } + if let Some(tags) = item.get("tags").and_then(|v| v.as_array()) { + for tag in tags { + let key = tag.get("key").and_then(|v| v.as_str()).unwrap_or(""); + if key == "Name" { + if let Some(value) = tag.get("value").and_then(|v| v.as_str()) { + if !value.is_empty() { + return Ok(Some(CString::new(value)?)); + } + } + } + } + } + } + + Ok(None) + } + + fn extract_ec2_imageid( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + let context = req.context; + let event = req.event; + let _ = Self::ensure_json(context, event); + + let json = match context.json_value.as_ref() { + Some(j) => j, + None => return Ok(None), + }; + + let items = json + .pointer("/responseElements/tagSpecificationSet/items") + .and_then(|v| v.as_array()) + .filter(|a| !a.is_empty()); + + match items { + Some(items) => { + let image_id = items[0] + .get("imageId") + .and_then(|v| v.as_str()) + .unwrap_or(""); + if image_id.is_empty() { + Ok(None) + } else { + Ok(Some(CString::new(image_id)?)) + } + } + None => Ok(None), + } + } + + // ----------------------------------------------------------------------- + // ecr.* fields + // ----------------------------------------------------------------------- + + fn extract_ecr_repository( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string( + req.context, + req.event, + "/requestParameters/repositoryName", + ), + req.offset, + ) + } + + fn extract_ecr_imagetag( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/requestParameters/imageTag"), + req.offset, + ) + } + + // ----------------------------------------------------------------------- + // iam.* fields + // ----------------------------------------------------------------------- + + fn extract_iam_role(&mut self, req: ExtractRequest) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string(req.context, req.event, "/requestParameters/roleName"), + req.offset, + ) + } + + fn extract_iam_policy( + &mut self, + req: ExtractRequest, + ) -> Result, Error> { + Self::finish_extract( + Self::do_extract_string( + req.context, + req.event, + "/requestParameters/policyName", + ), + req.offset, + ) + } +} + +impl ExtractPlugin for CloudTrailPlugin { + type Event<'a> = Event>; + type ExtractContext = CloudTrailContext; + const EXTRACT_FIELDS: &'static [ExtractFieldInfo] = &[ + // ct.* fields + field("ct.id", &Self::extract_id) + .with_display("Event ID") + .with_description("the unique ID of the cloudtrail event (eventID in the json)."), + field("ct.error", &Self::extract_error) + .with_display("Error Code") + .with_description("The error code from the event. Will be \"\" (e.g. the NULL/empty/none value) if there was no error."), + field("ct.errormessage", &Self::extract_errormessage) + .with_display("Error Message") + .with_description("The description of an error. Will be \"\" (e.g. the NULL/empty/none value) if there was no error."), + field("ct.time", &Self::extract_time) + .with_display("Timestamp") + .with_description("the timestamp of the cloudtrail event (eventTime in the json)."), + field("ct.src", &Self::extract_src) + .with_display("AWS Service") + .with_description("the source of the cloudtrail event (eventSource in the json)."), + field("ct.shortsrc", &Self::extract_shortsrc) + .with_display("AWS Service") + .with_description("the source of the cloudtrail event (eventSource in the json, without the '.amazonaws.com' trailer)."), + field("ct.name", &Self::extract_name) + .with_display("Event Name") + .with_description("the name of the cloudtrail event (eventName in the json)."), + field("ct.user", &Self::extract_user) + .with_display("User Name") + .with_description("the user of the cloudtrail event (userIdentity.userName in the json). For AssumedRole events, this is the role name from sessionContext.sessionIssuer.userName."), + field("ct.originaluser", &Self::extract_originaluser) + .with_display("Original User Name") + .with_description("the user name as seen in CloudTrail. For AssumedRole events, this is the session name extracted from userIdentity.arn or userIdentity.principalId. For all other identity types, this is userIdentity.userName."), + field("ct.user.accountid", &Self::extract_user_accountid) + .with_display("User Account ID") + .with_description("the account id of the user of the cloudtrail event."), + field("ct.user.identitytype", &Self::extract_user_identitytype) + .with_display("User Identity Type") + .with_description("the kind of user identity (e.g. Root, IAMUser,AWSService, etc.)"), + field("ct.user.principalid", &Self::extract_user_principalid) + .with_display("User Principal Id") + .with_description("A unique identifier for the user that made the request."), + field("ct.user.arn", &Self::extract_user_arn) + .with_display("User ARN") + .with_description("the Amazon Resource Name (ARN) of the user that made the request."), + field("ct.region", &Self::extract_region) + .with_display("Region") + .with_description("the region of the cloudtrail event (awsRegion in the json)."), + field("ct.response.subnetid", &Self::extract_response_subnetid) + .with_display("Response Subnet ID") + .with_description("the subnet ID included in the response."), + field("ct.response.reservationid", &Self::extract_response_reservationid) + .with_display("Response Reservation ID") + .with_description("the reservation ID included in the response."), + field("ct.response", &Self::extract_response) + .with_display("Response Elements") + .with_description("All response elements."), + field("ct.request.availabilityzone", &Self::extract_request_availabilityzone) + .with_display("Request Availability Zone") + .with_description("the availability zone included in the request."), + field("ct.request.cluster", &Self::extract_request_cluster) + .with_display("Request Cluster") + .with_description("the cluster included in the request."), + field("ct.request.functionname", &Self::extract_request_functionname) + .with_display("Request Function Name") + .with_description("the function name included in the request."), + field("ct.request.groupname", &Self::extract_request_groupname) + .with_display("Request Group Name") + .with_description("the group name included in the request."), + field("ct.request.host", &Self::extract_request_host) + .with_display("Request Host Name") + .with_description("the host included in the request"), + field("ct.request.name", &Self::extract_request_name) + .with_display("Host Name") + .with_description("the name of the entity being acted on in the request."), + field("ct.request.policy", &Self::extract_request_policy) + .with_display("Host Policy") + .with_description("the policy included in the request"), + field("ct.request.reason", &Self::extract_request_reason) + .with_display("Request Reason") + .with_description("the reason included in the request."), + field("ct.request.target", &Self::extract_request_target) + .with_display("Request Target") + .with_description("the target included in the request."), + field("ct.request.documentname", &Self::extract_request_documentname) + .with_display("Request Document Name") + .with_description("the document name included in the request."), + field("ct.request.serialnumber", &Self::extract_request_serialnumber) + .with_display("Request Serial Number") + .with_description("the serial number provided in the request."), + field("ct.request.servicename", &Self::extract_request_servicename) + .with_display("Request Service") + .with_description("the service name provided in the request."), + field("ct.request.subnetid", &Self::extract_request_subnetid) + .with_display("Request Subnet ID") + .with_description("the subnet ID provided in the request."), + field("ct.request.taskdefinition", &Self::extract_request_taskdefinition) + .with_display("Request Task Definition") + .with_description("the task definition provided in the request."), + field("ct.request.username", &Self::extract_request_username) + .with_display("Request User Name") + .with_description("the username provided in the request."), + field("ct.request", &Self::extract_request) + .with_display("Request Parameters") + .with_description("All request parameters."), + field("ct.srcip", &Self::extract_srcip) + .with_display("Source IP") + .with_description("the IP address generating the event (sourceIPAddress in the json)."), + field("ct.useragent", &Self::extract_useragent) + .with_display("User Agent") + .with_description("the user agent generating the event (userAgent in the json)."), + field("ct.info", &Self::extract_info) + .with_display("Info") + .with_description("summary information about the event. This varies depending on the event type and, for some events, it contains event-specific details."), + field("ct.managementevent", &Self::extract_managementevent) + .with_display("Management Event") + .with_description("'true' if the event is a management event (AwsApiCall, AwsConsoleAction, AwsConsoleSignIn, or AwsServiceEvent), 'false' otherwise."), + field("ct.readonly", &Self::extract_readonly) + .with_display("Read Only") + .with_description("'true' if the event only reads information (e.g. DescribeInstances), 'false' if the event modifies the state (e.g. RunInstances, CreateLoadBalancer...)."), + field("ct.requestid", &Self::extract_requestid) + .with_display("Request ID") + .with_description("The value that identifies the request."), + field("ct.eventtype", &Self::extract_eventtype) + .with_display("Event Type") + .with_description("Identifies the type of event that generated the event record."), + field("ct.apiversion", &Self::extract_apiversion) + .with_display("API Version") + .with_description("The API version associated with the AwsApiCall eventType value."), + field("ct.resources", &Self::extract_resources) + .with_display("Resources") + .with_description("A list of resources accessed in the event."), + field("ct.recipientaccountid", &Self::extract_recipientaccountid) + .with_display("Recipient Account Id") + .with_description("The account ID that received this event."), + field("ct.serviceeventdetails", &Self::extract_serviceeventdetails) + .with_display("Service Event Details") + .with_description("Identifies the service event, including what triggered the event and the result."), + field("ct.sharedeventid", &Self::extract_sharedeventid) + .with_display("Shared Event ID") + .with_description("GUID generated by CloudTrail to uniquely identify CloudTrail events."), + field("ct.vpcendpointid", &Self::extract_vpcendpointid) + .with_display("VPC Endpoint ID") + .with_description("Identifies the VPC endpoint in which requests were made."), + field("ct.eventcategory", &Self::extract_eventcategory) + .with_display("Event Category") + .with_description("Shows the event category that is used in LookupEvents calls."), + field("ct.addendum.reason", &Self::extract_addendum_reason) + .with_display("Reason") + .with_description("The reason that the event or some of its contents were missing."), + field("ct.addendum.updatedfields", &Self::extract_addendum_updatedfields) + .with_display("Updated Fields") + .with_description("The event record fields that are updated by the addendum."), + field("ct.addendum.originalrequestid", &Self::extract_addendum_originalrequestid) + .with_display("Original Request ID") + .with_description("The original unique ID of the request."), + field("ct.addendum.originaleventid", &Self::extract_addendum_originaleventid) + .with_display("Original Event ID") + .with_description("The original event ID."), + field("ct.sessioncredentialfromconsole", &Self::extract_sessioncredentialfromconsole) + .with_display("Session Credential From Console") + .with_description("Shows whether or not an event originated from an AWS Management Console session."), + field("ct.edgedevicedetails", &Self::extract_edgedevicedetails) + .with_display("Edge Device Details") + .with_description("Information about edge devices that are targets of a request."), + field("ct.tlsdetails.tlsversion", &Self::extract_tlsdetails_tlsversion) + .with_display("TLS Version") + .with_description("The TLS version of a request."), + field("ct.tlsdetails.ciphersuite", &Self::extract_tlsdetails_ciphersuite) + .with_display("TLS Cipher Suite") + .with_description("The cipher suite (combination of security algorithms used) of a request."), + field("ct.tlsdetails.clientprovidedhostheader", &Self::extract_tlsdetails_clientprovidedhostheader) + .with_display("Client Provided Host Header") + .with_description("The client-provided host name used in the service API call."), + field("ct.additionaleventdata", &Self::extract_additionaleventdata) + .with_display("Additional Event Data") + .with_description("All additional event data attributes."), + // s3.* fields + field("s3.uri", &Self::extract_s3_uri) + .with_display("Key URI") + .with_description("the s3 URI (s3:///)."), + field("s3.bucket", &Self::extract_s3_bucket) + .with_display("Bucket Name") + .with_description("the bucket name for s3 events."), + field("s3.key", &Self::extract_s3_key) + .with_display("Key Name") + .with_description("the S3 key name."), + field("s3.bytes", &Self::extract_s3_bytes) + .with_display("Total Bytes") + .with_description("the size of an s3 download or upload, in bytes."), + field("s3.bytes.in", &Self::extract_s3_bytes_in) + .with_display("Bytes In") + .with_description("the size of an s3 upload, in bytes."), + field("s3.bytes.out", &Self::extract_s3_bytes_out) + .with_display("Bytes Out") + .with_description("the size of an s3 download, in bytes."), + field("s3.cnt.get", &Self::extract_s3_cnt_get) + .with_display("N Get Ops") + .with_description("the number of get operations. This field is 1 for GetObject events, 0 otherwise."), + field("s3.cnt.put", &Self::extract_s3_cnt_put) + .with_display("N Put Ops") + .with_description("the number of put operations. This field is 1 for PutObject events, 0 otherwise."), + field("s3.cnt.other", &Self::extract_s3_cnt_other) + .with_display("N Other Ops") + .with_description("the number of non I/O operations. This field is 0 for GetObject and PutObject events, 1 for all the other events."), + // ec2.* fields + field("ec2.name", &Self::extract_ec2_name) + .with_display("Instance Name") + .with_description("the name of the ec2 instances, typically stored in the instance tags."), + field("ec2.imageid", &Self::extract_ec2_imageid) + .with_display("Image Id") + .with_description("the ID for the image used to run the ec2 instance in the response."), + // ecr.* fields + field("ecr.repository", &Self::extract_ecr_repository) + .with_display("ECR Repository name") + .with_description("the name of the ecr Repository specified in the request."), + field("ecr.imagetag", &Self::extract_ecr_imagetag) + .with_display("Image Tag") + .with_description("the tag of the image specified in the request."), + // iam.* fields + field("iam.role", &Self::extract_iam_role) + .with_display("IAM Role") + .with_description("the IAM role specified in the request."), + field("iam.policy", &Self::extract_iam_policy) + .with_display("IAM Policy") + .with_description("the IAM policy specified in the request."), + ]; +} + +// --------------------------------------------------------------------------- +// Helper functions +// --------------------------------------------------------------------------- + +/// Extract user string from CloudTrail JSON for the ct.info field +fn get_user_string(json: &serde_json::Value) -> String { + let utype = match json.pointer("/userIdentity/type").and_then(|v| v.as_str()) { + Some(t) => t, + None => return String::new(), + }; + + match utype { + "Root" | "IAMUser" => json + .pointer("/userIdentity/userName") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(), + "AWSService" => json + .pointer("/userIdentity/invokedBy") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(), + "AssumedRole" => json + .pointer("/userIdentity/sessionContext/sessionIssuer/userName") + .and_then(|v| v.as_str()) + .unwrap_or("AssumedRole") + .to_string(), + "AWSAccount" => "AWSAccount".to_string(), + "FederatedUser" => "FederatedUser".to_string(), + _ => "".to_string(), + } +} + +/// Extract a numeric value from a JSON value, handling both int and float representations. +fn get_value_u64(value: &serde_json::Value) -> Option { + if let Some(u) = value.as_u64() { + return Some(u); + } + if let Some(f) = value.as_f64() { + return Some(f as u64); + } + None +} + +// --------------------------------------------------------------------------- +// JSON byte-offset helpers (from gcpaudit-rs) +// --------------------------------------------------------------------------- + +fn skip_ws(raw: &[u8], mut pos: usize) -> usize { + while pos < raw.len() && raw[pos].is_ascii_whitespace() { + pos += 1; + } + pos +} + +fn skip_json_string(raw: &[u8], pos: usize) -> Option { + if pos >= raw.len() || raw[pos] != b'"' { + return None; + } + let mut p = pos + 1; + while p < raw.len() { + match raw[p] { + b'\\' => p += 2, + b'"' => return Some(p + 1), + _ => p += 1, + } + } + None +} + +fn read_json_key(raw: &[u8], pos: usize) -> Option<(String, usize)> { + let end = skip_json_string(raw, pos)?; + let slice = std::str::from_utf8(&raw[pos..end]).ok()?; + let key: String = serde_json::from_str(slice).ok()?; + Some((key, end)) +} + +fn skip_json_value(raw: &[u8], pos: usize) -> Option { + if pos >= raw.len() { + return None; + } + match raw[pos] { + b'"' => skip_json_string(raw, pos), + b'{' | b'[' => { + let mut p = pos + 1; + let mut depth: u32 = 1; + while p < raw.len() && depth > 0 { + match raw[p] { + b'{' | b'[' => depth += 1, + b'}' | b']' => depth -= 1, + b'"' => { + p = skip_json_string(raw, p)?; + continue; + } + _ => {} + } + p += 1; + } + Some(p) + } + b't' => Some(pos + 4), + b'f' => Some(pos + 5), + b'n' => Some(pos + 4), + _ if raw[pos] == b'-' || raw[pos].is_ascii_digit() => { + let mut p = pos + 1; + while p < raw.len() + && matches!(raw[p], b'0'..=b'9' | b'.' | b'e' | b'E' | b'+' | b'-') + { + p += 1; + } + Some(p) + } + _ => None, + } +} + +/// Walk a JSON Pointer path (RFC 6901) through raw JSON bytes and return the +/// byte range of the value found at that path. +fn find_json_pointer_range(raw: &[u8], path: &str) -> Option> { + if !path.starts_with('/') { + return None; + } + let segments: Vec<&str> = path[1..].split('/').collect(); + let mut pos: usize = 0; + + for (seg_idx, segment) in segments.iter().enumerate() { + pos = skip_ws(raw, pos); + if pos >= raw.len() { + return None; + } + let decoded = segment.replace("~1", "/").replace("~0", "~"); + let is_last = seg_idx == segments.len() - 1; + + match raw[pos] { + b'{' => { + pos += 1; + loop { + pos = skip_ws(raw, pos); + if pos >= raw.len() || raw[pos] == b'}' { + return None; + } + if raw[pos] == b',' { + pos += 1; + continue; + } + let (key, key_end) = read_json_key(raw, pos)?; + pos = skip_ws(raw, key_end); + if pos >= raw.len() || raw[pos] != b':' { + return None; + } + pos = skip_ws(raw, pos + 1); + if key == decoded { + if is_last { + let value_end = skip_json_value(raw, pos)?; + return Some(pos..value_end); + } + break; + } + pos = skip_json_value(raw, pos)?; + } + } + b'[' => { + let index: usize = decoded.parse().ok()?; + pos += 1; + for i in 0..=index { + pos = skip_ws(raw, pos); + if pos >= raw.len() || raw[pos] == b']' { + return None; + } + if i > 0 { + if raw[pos] != b',' { + return None; + } + pos = skip_ws(raw, pos + 1); + } + if i == index { + if is_last { + let value_end = skip_json_value(raw, pos)?; + return Some(pos..value_end); + } + break; + } + pos = skip_json_value(raw, pos)?; + } + } + _ => return None, + } + } + None +} diff --git a/plugins/cloudtrail_rs/src/interval.rs b/plugins/cloudtrail_rs/src/interval.rs new file mode 100644 index 00000000..e11982e3 --- /dev/null +++ b/plugins/cloudtrail_rs/src/interval.rs @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +/* +Copyright (C) 2026 The Falco Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use anyhow::{anyhow, Result}; +use chrono::{Duration, Utc}; +use regex::Regex; + +const RFC3339_SIMPLE: &str = "%Y-%m-%dT%H:%M:%SZ"; + +fn parse_endpoint(endpoint: &str) -> Result> { + let duration_re = Regex::new(r"^(\d+)([wdhms])$")?; + + if let Some(caps) = duration_re.captures(endpoint) { + let amount: i64 = caps[1].parse()?; + let duration = match &caps[2] { + "w" => Duration::weeks(amount), + "d" => Duration::days(amount), + "h" => Duration::hours(amount), + "m" => Duration::minutes(amount), + "s" => Duration::seconds(amount), + _ => return Err(anyhow!("invalid duration unit")), + }; + Ok(Utc::now() - duration) + } else { + let dt = chrono::NaiveDateTime::parse_from_str(endpoint, RFC3339_SIMPLE) + .map_err(|e| anyhow!("failed to parse time '{}': {}", endpoint, e))?; + Ok(dt.and_utc()) + } +} + +/// Parse an interval string into start and end times. +/// The end time will be None if no end interval was supplied. +pub fn parse_interval( + interval: &str, +) -> Result<( + Option>, + Option>, +)> { + if interval.is_empty() { + return Ok((None, None)); + } + + let interval_re = + Regex::new(r"(.*)\s*-\s*(\d+[wdhms]|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)$")?; + + if let Some(caps) = interval_re.captures(interval) { + let start = parse_endpoint(caps[1].trim())?; + let end = parse_endpoint(&caps[2])?; + Ok((Some(start), Some(end))) + } else { + let start = parse_endpoint(interval)?; + Ok((Some(start), None)) + } +} diff --git a/plugins/cloudtrail_rs/src/lib.rs b/plugins/cloudtrail_rs/src/lib.rs new file mode 100644 index 00000000..8cd7a3ef --- /dev/null +++ b/plugins/cloudtrail_rs/src/lib.rs @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +/* +Copyright (C) 2026 The Falco Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use std::ffi::{CStr, CString}; + +use anyhow::{anyhow, Result}; +use aws_types::SdkConfig; +use config::PluginConfig; +use falco_plugin::base::{Json, Plugin}; +use falco_plugin::event::{PluginEvent, events::Event}; +use falco_plugin::source::{EventInput, SourcePlugin}; +use falco_plugin::tables::TablesInput; +use falco_plugin::{extract_plugin, plugin, source_plugin}; +use source::CloudTrailInstance; +use tokio::runtime::Runtime; + +pub mod config; +mod aws; +mod extract; +mod interval; +mod source; + +pub struct CloudTrailPlugin { + config: PluginConfig, + aws_config: SdkConfig, +} + +impl Plugin for CloudTrailPlugin { + const NAME: &'static CStr = c"cloudtrail"; + const PLUGIN_VERSION: &'static CStr = c"0.16.0"; + const DESCRIPTION: &'static CStr = + c"reads cloudtrail JSON data saved to file in the directory specified in the settings"; + const CONTACT: &'static CStr = c"github.com/falcosecurity/plugins/"; + type ConfigType = Json; + + fn new(_input: Option<&TablesInput>, config: Self::ConfigType) -> Result { + let config = config.0; + let rt = Runtime::new()?; + let aws_config = rt.block_on(aws::load_aws_config(&config.aws))?; + Ok(CloudTrailPlugin { config, aws_config }) + } + + fn set_config(&mut self, config: Self::ConfigType) -> Result<()> { + self.config = config.0; + let rt = Runtime::new()?; + self.aws_config = rt.block_on(aws::load_aws_config(&self.config.aws))?; + Ok(()) + } +} + +impl SourcePlugin for CloudTrailPlugin { + type Instance = CloudTrailInstance; + type Event<'a> = Event>; + const EVENT_SOURCE: &'static CStr = c"aws_cloudtrail"; + const PLUGIN_ID: u32 = 2; + + fn open(&mut self, params: Option<&str>) -> Result { + let params = params + .ok_or_else(|| anyhow!("no input provided"))?; + + if params.starts_with("s3://") { + let runtime = Runtime::new()?; + let aws_config = self.aws_config.clone(); + CloudTrailInstance::open_s3(params, &self.config, aws_config, runtime) + } else if params.starts_with("sqs://") { + let runtime = Runtime::new()?; + let aws_config = self.aws_config.clone(); + CloudTrailInstance::open_sqs(params, &self.config, aws_config, runtime) + } else { + CloudTrailInstance::open_local(params, &self.config) + } + } + + fn event_to_string(&mut self, event: &EventInput>) -> Result { + let evt = event.event()?; + Ok(CString::new(evt.params.event_data.to_vec())?) + } +} + +plugin!(CloudTrailPlugin); +source_plugin!(CloudTrailPlugin); +extract_plugin!(CloudTrailPlugin); diff --git a/plugins/cloudtrail_rs/src/source.rs b/plugins/cloudtrail_rs/src/source.rs new file mode 100644 index 00000000..2cdc0f97 --- /dev/null +++ b/plugins/cloudtrail_rs/src/source.rs @@ -0,0 +1,771 @@ +// SPDX-License-Identifier: Apache-2.0 +/* +Copyright (C) 2026 The Falco Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use std::io::Read; +use std::path::Path; + +use anyhow::{anyhow, Result}; +use aws_sdk_s3::Client as S3Client; +use aws_sdk_sqs::Client as SqsClient; +use aws_types::SdkConfig; +use falco_plugin::source::{EventBatch, SourcePluginInstance}; +use regex::Regex; +use tokio::runtime::Runtime; + +use crate::config::PluginConfig; +use crate::interval::parse_interval; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OpenMode { + File, + S3, + Sqs, +} + +#[derive(Debug, Clone)] +pub struct FileInfo { + pub name: String, + pub is_compressed: bool, +} + +struct S3State { + bucket: String, + client: S3Client, + download_bufs: Vec>, + last_downloaded_file_num: usize, + n_filled_bufs: usize, + cur_buf: usize, +} + +struct SqsState { + client: SqsClient, + queue_url: String, +} + +pub struct CloudTrailInstance { + mode: OpenMode, + config: PluginConfig, + files: Vec, + cur_file_num: usize, + evt_json_strings: Vec>, + evt_json_list_pos: usize, + s3_state: Option, + sqs_state: Option, + runtime: Option, +} + +/// Extract individual record entries from a CloudTrail JSON file. +/// CloudTrail files have the format: {"Records":[{evt1},{evt2},...]} +/// We split at the top-level record boundaries to preserve original JSON. +fn extract_record_strings(json_str: &[u8], res: &mut Vec>) { + let mut depth: i32 = 0; + let mut entry_start: usize = 0; + let mut in_string = false; + let mut escape = false; + + for (pos, &ch) in json_str.iter().enumerate() { + if escape { + escape = false; + continue; + } + if in_string { + if ch == b'\\' { + escape = true; + } else if ch == b'"' { + in_string = false; + } + continue; + } + match ch { + b'"' => in_string = true, + b'{' => { + if depth == 1 { + entry_start = pos; + } + depth += 1; + } + b'}' => { + depth -= 1; + if depth == 1 { + res.push(json_str[entry_start..=pos].to_vec()); + } + } + _ => {} + } + } +} + +impl CloudTrailInstance { + pub fn open_local(params: &str, config: &PluginConfig) -> Result { + if params.is_empty() { + return Err(anyhow!("cloudtrail plugin error: missing input directory argument")); + } + + let path = Path::new(params); + if !path.exists() { + return Err(anyhow!( + "cloudtrail plugin error: cannot open {}", + params + )); + } + + let mut files = Vec::new(); + collect_local_files(path, &mut files)?; + + if files.is_empty() { + return Err(anyhow!( + "cloudtrail plugin error: no json files found in {}", + params + )); + } + + Ok(CloudTrailInstance { + mode: OpenMode::File, + config: config.clone(), + files, + cur_file_num: 0, + evt_json_strings: Vec::new(), + evt_json_list_pos: 0, + s3_state: None, + sqs_state: None, + runtime: None, + }) + } + + pub fn open_s3( + params: &str, + config: &PluginConfig, + aws_config: SdkConfig, + runtime: Runtime, + ) -> Result { + if config.s3_download_concurrency < 1 { + return Err(anyhow!( + "cloudtrail invalid S3DownloadConcurrency: \"{}\"", + config.s3_download_concurrency + )); + } + + // Remove "s3://" prefix + let input = ¶ms[5..]; + let (bucket, prefix) = match input.find('/') { + Some(idx) => (input[..idx].to_string(), input[idx + 1..].to_string()), + None => (input.to_string(), String::new()), + }; + + let s3_client = S3Client::new(&aws_config); + let download_bufs = vec![Vec::new(); config.s3_download_concurrency as usize]; + + let s3_state = S3State { + bucket: bucket.clone(), + client: s3_client, + download_bufs, + last_downloaded_file_num: 0, + n_filled_bufs: 0, + cur_buf: 0, + }; + + let mut inst = CloudTrailInstance { + mode: OpenMode::S3, + config: config.clone(), + files: Vec::new(), + cur_file_num: 0, + evt_json_strings: Vec::new(), + evt_json_list_pos: 0, + s3_state: Some(s3_state), + sqs_state: None, + runtime: Some(runtime), + }; + + // List S3 keys + inst.list_s3_keys(&bucket, &prefix)?; + + Ok(inst) + } + + pub fn open_sqs( + params: &str, + config: &PluginConfig, + aws_config: SdkConfig, + runtime: Runtime, + ) -> Result { + let sqs_client = SqsClient::new(&aws_config); + let s3_client = S3Client::new(&aws_config); + + let queue_name = ¶ms[6..]; + + // Get the queue URL + let queue_url = { + let mut req = sqs_client.get_queue_url().queue_name(queue_name); + if !config.sqs_owner_account.is_empty() { + req = req.queue_owner_aws_account_id(&config.sqs_owner_account); + } + let result = runtime.block_on(req.send())?; + result + .queue_url() + .ok_or_else(|| anyhow!("failed to get queue URL"))? + .to_string() + }; + + let download_bufs = vec![Vec::new(); config.s3_download_concurrency as usize]; + + let s3_state = S3State { + bucket: String::new(), + client: s3_client, + download_bufs, + last_downloaded_file_num: 0, + n_filled_bufs: 0, + cur_buf: 0, + }; + + let sqs_state = SqsState { + client: sqs_client, + queue_url, + }; + + let mut inst = CloudTrailInstance { + mode: OpenMode::Sqs, + config: config.clone(), + files: Vec::new(), + cur_file_num: 0, + evt_json_strings: Vec::new(), + evt_json_list_pos: 0, + s3_state: Some(s3_state), + sqs_state: Some(sqs_state), + runtime: Some(runtime), + }; + + // Get initial batch of SQS files + inst.get_more_sqs_files()?; + + Ok(inst) + } + + fn list_s3_keys(&mut self, bucket: &str, prefix: &str) -> Result<()> { + let runtime = self + .runtime + .as_ref() + .ok_or_else(|| anyhow!("no runtime"))?; + let s3 = self + .s3_state + .as_ref() + .ok_or_else(|| anyhow!("no S3 state"))?; + + let (start_time, end_time) = parse_interval(&self.config.s3_interval)?; + + let start_ts_format = "%Y%m%dT%H%M"; + let start_ts = start_time.map(|t| t.format(start_ts_format).to_string()); + let end_ts = end_time.map(|t| t.format(start_ts_format).to_string()); + + if let (Some(ref s), Some(ref e)) = (&start_ts, &end_ts) { + if e < s { + return Err(anyhow!( + "cloudtrail start time must be less than end time" + )); + } + } + + let account_list_re = Regex::new(r"^(?: *\d{12} *,?)*$")?; + if !self.config.s3_account_list.is_empty() + && !account_list_re.is_match(&self.config.s3_account_list) + { + return Err(anyhow!( + "cloudtrail invalid account list: \"{}\"", + self.config.s3_account_list + )); + } + + // Determine the prefixes to list based on the CloudTrail path structure + let aws_logs_re = Regex::new(r"/AWSLogs/(?:o-[a-z0-9]{10,32}/)?\d{12}/?$")?; + let aws_logs_org_re = Regex::new(r"/AWSLogs(?:/o-[a-z0-9]{10,32})?/?$")?; + + let mut interval_prefix_list: Vec = Vec::new(); + let mut interval_prefix = prefix.to_string(); + + if aws_logs_re.is_match(&interval_prefix) { + if !interval_prefix.ends_with('/') { + interval_prefix.push('/'); + } + interval_prefix.push_str("CloudTrail/"); + interval_prefix_list.push(interval_prefix); + } else if aws_logs_org_re.is_match(&interval_prefix) { + if !interval_prefix.ends_with('/') { + interval_prefix.push('/'); + } + if !self.config.s3_account_list.is_empty() { + for account in self.config.s3_account_list.split(',') { + let account = account.trim(); + interval_prefix_list + .push(format!("{}{}/CloudTrail/", interval_prefix, account)); + } + } else { + // List account IDs from the bucket + let accounts = runtime.block_on(list_common_prefixes( + &s3.client, + bucket, + &interval_prefix, + ))?; + for acct_prefix in accounts { + if aws_logs_re.is_match(&acct_prefix) { + interval_prefix_list.push(format!("{}CloudTrail/", acct_prefix)); + } + } + } + } else { + interval_prefix_list.push(interval_prefix); + } + + // For each prefix, list regions and then list keys + let mut input_prefixes: Vec<(String, Option)> = Vec::new(); + + for ip in &interval_prefix_list { + if ip.ends_with("/CloudTrail/") { + let regions = + runtime.block_on(list_common_prefixes(&s3.client, bucket, ip))?; + for region_prefix in regions { + let start_after = start_time.map(|t| { + format!("{}{}", region_prefix, t.format("%Y/%m/%d/")) + }); + input_prefixes.push((region_prefix, start_after)); + } + } + } + + if input_prefixes.is_empty() { + input_prefixes.push((prefix.to_string(), None)); + } + + // List keys for all prefixes + let filepath_re = Regex::new(r".*_CloudTrail_[^_]+_([^_]+)Z_")?; + + for (pfx, start_after) in &input_prefixes { + let keys = runtime.block_on(list_s3_keys( + &s3.client, + bucket, + pfx, + start_after.as_deref(), + ))?; + + for key in keys { + // Apply interval filter based on filepath timestamp + if let Some(ref sts) = start_ts { + if let Some(caps) = filepath_re.captures(&key) { + let path_ts = &caps[1]; + if path_ts < sts.as_str() { + continue; + } + if let Some(ref ets) = end_ts { + if path_ts > ets.as_str() { + continue; + } + } + } + } + + let is_compressed = key.ends_with(".json.gz"); + if !key.ends_with(".json") && !is_compressed { + continue; + } + + self.files.push(FileInfo { + name: key, + is_compressed, + }); + } + } + + Ok(()) + } + + fn get_more_sqs_files(&mut self) -> Result<()> { + let runtime = self + .runtime + .as_ref() + .ok_or_else(|| anyhow!("no runtime"))?; + let sqs = self + .sqs_state + .as_ref() + .ok_or_else(|| anyhow!("no SQS state"))?; + + let result = runtime.block_on( + sqs.client + .receive_message() + .queue_url(&sqs.queue_url) + .max_number_of_messages(1) + .send(), + )?; + + let messages = result.messages(); + if messages.is_empty() { + return Ok(()); + } + + let message = &messages[0]; + + // Delete the message if configured + if self.config.sqs_delete { + if let Some(receipt_handle) = message.receipt_handle() { + let _ = runtime.block_on( + sqs.client + .delete_message() + .queue_url(&sqs.queue_url) + .receipt_handle(receipt_handle) + .send(), + ); + } + } + + let body = message + .body() + .ok_or_else(|| anyhow!("SQS message has no body"))?; + let sqs_msg: serde_json::Value = serde_json::from_str(body)?; + + let msg_type = sqs_msg + .get("Type") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow!("received SQS message that did not have a Type property"))?; + + if msg_type != "Notification" { + return Err(anyhow!( + "received SQS message that was not a SNS Notification" + )); + } + + let sns_message = sqs_msg + .get("Message") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow!("SNS notification missing Message field"))?; + + if self.config.use_s3_sns { + // Process SNS message coming from S3 + let s3_event: serde_json::Value = serde_json::from_str(sns_message)?; + let records = s3_event + .get("Records") + .and_then(|v| v.as_array()) + .ok_or_else(|| anyhow!("S3 event missing Records"))?; + + for record in records { + let bucket_name = record + .pointer("/s3/bucket/name") + .and_then(|v| v.as_str()) + .unwrap_or_default(); + let key = record + .pointer("/s3/object/key") + .and_then(|v| v.as_str()) + .unwrap_or_default(); + + if let Some(s3) = self.s3_state.as_mut() { + s3.bucket = bucket_name.to_string(); + } + + let is_compressed = key.ends_with(".json.gz"); + self.files.push(FileInfo { + name: key.to_string(), + is_compressed, + }); + } + } else { + // Process direct CloudTrail SNS notification + #[derive(serde::Deserialize)] + struct SnsMessage { + #[serde(rename = "s3Bucket")] + bucket: String, + #[serde(rename = "s3ObjectKey")] + keys: Vec, + } + + let notification: SnsMessage = serde_json::from_str(sns_message)?; + + if let Some(s3) = self.s3_state.as_mut() { + s3.bucket = notification.bucket; + } + + for key in notification.keys { + let is_compressed = key.ends_with(".json.gz"); + self.files.push(FileInfo { + name: key, + is_compressed, + }); + } + } + + Ok(()) + } + + fn read_next_file_s3(&mut self) -> Result> { + let runtime = self + .runtime + .as_ref() + .ok_or_else(|| anyhow!("no runtime"))?; + let s3 = self + .s3_state + .as_mut() + .ok_or_else(|| anyhow!("no S3 state"))?; + + // Check if we still have buffered data + if s3.cur_buf < s3.n_filled_bufs { + let buf = std::mem::take(&mut s3.download_bufs[s3.cur_buf]); + s3.cur_buf += 1; + return Ok(buf); + } + + // Download the next batch of files concurrently + let k = s3.last_downloaded_file_num; + let concurrency = self.config.s3_download_concurrency as usize; + let n_to_download = std::cmp::min(concurrency, self.files.len() - k); + + let bucket = s3.bucket.clone(); + let files_to_download: Vec = self.files[k..k + n_to_download] + .iter() + .map(|f| f.name.clone()) + .collect(); + + let client = s3.client.clone(); + let results = runtime.block_on(async { + let mut handles = Vec::new(); + for key in files_to_download { + let client = client.clone(); + let bucket = bucket.clone(); + handles.push(tokio::spawn(async move { + let result = client + .get_object() + .bucket(&bucket) + .key(&key) + .send() + .await?; + let data = result.body.collect().await?; + Ok::, anyhow::Error>(data.into_bytes().to_vec()) + })); + } + let mut results = Vec::new(); + for handle in handles { + results.push(handle.await??); + } + Ok::>, anyhow::Error>(results) + })?; + + let s3 = self + .s3_state + .as_mut() + .ok_or_else(|| anyhow!("no S3 state"))?; + + s3.n_filled_bufs = results.len(); + for (i, buf) in results.into_iter().enumerate() { + s3.download_bufs[i] = buf; + } + s3.last_downloaded_file_num += s3.n_filled_bufs; + s3.cur_buf = 1; + + let buf = std::mem::take(&mut s3.download_bufs[0]); + Ok(buf) + } + + /// Core event production: loads the next event from the current or next file. + fn next_event(&mut self) -> Result>> { + // Check if we still have events from the current file + if self.evt_json_list_pos < self.evt_json_strings.len() { + let evt_data = self.evt_json_strings[self.evt_json_list_pos].clone(); + self.evt_json_list_pos += 1; + + // Validate the event has required fields + if let Ok(parsed) = serde_json::from_slice::(&evt_data) { + // Skip events without eventTime + if parsed.get("eventTime").is_none() { + return Ok(None); // skip + } + // Skip AwsCloudTrailInsight events + if parsed + .get("eventType") + .and_then(|v| v.as_str()) + == Some("AwsCloudTrailInsight") + { + return Ok(None); // skip + } + } + + return Ok(Some(evt_data)); + } + + // Need to read the next file + if self.cur_file_num >= self.files.len() { + if self.mode == OpenMode::Sqs { + self.get_more_sqs_files()?; + if self.cur_file_num >= self.files.len() { + return Ok(None); // no more files yet + } + } else { + return Err(anyhow!("EOF")); + } + } + + let file = self.files[self.cur_file_num].clone(); + self.cur_file_num += 1; + + // Read the file content + let raw_data = match self.mode { + OpenMode::S3 | OpenMode::Sqs => self.read_next_file_s3()?, + OpenMode::File => std::fs::read(&file.name)?, + }; + + // Decompress if gzipped + let data = if file.is_compressed { + let mut decoder = flate2::read::GzDecoder::new(&raw_data[..]); + let mut decompressed = Vec::new(); + decoder.read_to_end(&mut decompressed)?; + decompressed + } else { + raw_data + }; + + // Extract individual records from the CloudTrail JSON + self.evt_json_strings.clear(); + extract_record_strings(&data, &mut self.evt_json_strings); + self.evt_json_list_pos = 0; + + // Recurse to get the first event from this file + self.next_event() + } +} + +impl SourcePluginInstance for CloudTrailInstance { + type Plugin = crate::CloudTrailPlugin; + + fn next_batch( + &mut self, + _plugin: &mut Self::Plugin, + batch: &mut EventBatch, + ) -> Result<()> { + match self.next_event() { + Ok(Some(data)) => { + batch.add(Self::plugin_event(&data))?; + Ok(()) + } + Ok(None) => { + // No events available right now (timeout equivalent) + Ok(()) + } + Err(e) => Err(e), + } + } +} + +fn collect_local_files(dir: &Path, files: &mut Vec) -> Result<()> { + if dir.is_file() { + let name = dir.to_string_lossy().to_string(); + let is_compressed = name.ends_with(".json.gz"); + if name.ends_with(".json") || is_compressed { + files.push(FileInfo { + name, + is_compressed, + }); + } + return Ok(()); + } + + for entry in std::fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + collect_local_files(&path, files)?; + } else { + let name = path.to_string_lossy().to_string(); + let is_compressed = name.ends_with(".json.gz"); + if name.ends_with(".json") || is_compressed { + files.push(FileInfo { + name, + is_compressed, + }); + } + } + } + + Ok(()) +} + +async fn list_common_prefixes( + client: &S3Client, + bucket: &str, + prefix: &str, +) -> Result> { + let mut prefixes = Vec::new(); + let mut continuation_token: Option = None; + + loop { + let mut req = client + .list_objects_v2() + .bucket(bucket) + .prefix(prefix) + .delimiter("/"); + + if let Some(token) = continuation_token { + req = req.continuation_token(token); + } + + let output = req.send().await?; + + for cp in output.common_prefixes() { + if let Some(p) = cp.prefix() { + prefixes.push(p.to_string()); + } + } + + if output.is_truncated() == Some(true) { + continuation_token = output.next_continuation_token().map(|s| s.to_string()); + } else { + break; + } + } + + Ok(prefixes) +} + +async fn list_s3_keys( + client: &S3Client, + bucket: &str, + prefix: &str, + start_after: Option<&str>, +) -> Result> { + let mut keys = Vec::new(); + let mut continuation_token: Option = None; + + loop { + let mut req = client.list_objects_v2().bucket(bucket).prefix(prefix); + + if let Some(sa) = start_after { + req = req.start_after(sa); + } + + if let Some(token) = continuation_token { + req = req.continuation_token(token); + } + + let output = req.send().await?; + + for obj in output.contents() { + if let Some(key) = obj.key() { + keys.push(key.to_string()); + } + } + + if output.is_truncated() == Some(true) { + continuation_token = output.next_continuation_token().map(|s| s.to_string()); + } else { + break; + } + } + + Ok(keys) +}