diff --git a/.cargo/config.toml b/.cargo/config.toml index 3b569aa16..a8fb09b6f 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,4 +1,3 @@ [alias] license = "dd-rust-license-tool check" format = "fmt --all && clippy --workspace --all-features --fix" - diff --git a/.gitlab/Dockerfile b/.gitlab/Dockerfile index f33fdacbb..fc4e17e1d 100644 --- a/.gitlab/Dockerfile +++ b/.gitlab/Dockerfile @@ -1,7 +1,7 @@ FROM registry.ddbuild.io/images/docker:24.0.5 RUN apt-get update && apt-get install -y --fix-missing --no-install-recommends \ - curl gcc gnupg g++ make cmake unzip openssl g++ uuid-runtime + curl gcc gnupg g++ make cmake unzip openssl g++ uuid-runtime libclang-dev # Install AWS CLI RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" @@ -16,8 +16,6 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \ sh -s -- --profile minimal --default-toolchain stable -y RUN source $HOME/.cargo/env -ENV PATH /root/.cargo/bin/:$PATH +ENV PATH=$PATH:/root/.cargo/bin/ RUN rustup component add rust-src --toolchain stable - - diff --git a/.gitlab/datasources/flavors.yaml b/.gitlab/datasources/flavors.yaml index beed9216c..5cac07431 100644 --- a/.gitlab/datasources/flavors.yaml +++ b/.gitlab/datasources/flavors.yaml @@ -6,7 +6,7 @@ flavors: needs_layer_publish: true suffix: amd64 layer_name_base_suffix: "" - max_layer_compressed_size_mb: 26 + max_layer_compressed_size_mb: 27 max_layer_uncompressed_size_mb: 54 - name: arm64 @@ -16,7 +16,7 @@ flavors: needs_layer_publish: true suffix: arm64 layer_name_base_suffix: "-ARM" - max_layer_compressed_size_mb: 23 + max_layer_compressed_size_mb: 24 max_layer_uncompressed_size_mb: 50 - name: amd64, alpine @@ -40,7 +40,7 @@ flavors: needs_layer_publish: true suffix: amd64-fips layer_name_base_suffix: "-FIPS" - max_layer_compressed_size_mb: 26 + max_layer_compressed_size_mb: 27 max_layer_uncompressed_size_mb: 56 - name: arm64, fips @@ -50,7 +50,7 @@ flavors: needs_layer_publish: true suffix: arm64-fips layer_name_base_suffix: "-ARM-FIPS" - max_layer_compressed_size_mb: 23 + max_layer_compressed_size_mb: 24 max_layer_uncompressed_size_mb: 52 - name: amd64, fips, alpine diff --git a/bottlecap/Cargo.lock b/bottlecap/Cargo.lock index 1ff2d4d81..1639930ac 100644 --- a/bottlecap/Cargo.lock +++ b/bottlecap/Cargo.lock @@ -38,6 +38,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.98" @@ -272,7 +287,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d9c2e952a1f57e8cbc78b058a968639e70c4ce8b9c0a5e6363d4e5670eed795" dependencies = [ - "bindgen", + "bindgen 0.69.5", "cc", "cmake", "dunce", @@ -297,13 +312,33 @@ version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa9b6986f250236c27e5a204062434a773a13243d2ffc2955f37bdba4c5c6a1" dependencies = [ - "bindgen", + "bindgen 0.69.5", "cc", "cmake", "dunce", "fs_extra", ] +[[package]] +name = "aws_lambda_events" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144ec7565561115498a288850cc6a42b279e09b6c4b88f623eecb9c8ca96c08c" +dependencies = [ + "base64 0.22.1", + "bytes 1.10.1", + "chrono", + "flate2", + "http 1.3.1", + "http-body 1.0.1", + "http-serde", + "query_map", + "serde", + "serde_dynamo", + "serde_json", + "serde_with", +] + [[package]] name = "axum" version = "0.8.4" @@ -358,6 +393,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" +dependencies = [ + "axum", + "axum-core", + "bytes 1.10.1", + "cookie", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -419,6 +477,26 @@ dependencies = [ "which", ] +[[package]] +name = "bindgen" +version = "0.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" +dependencies = [ + "bitflags 2.9.0", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.1", + "shlex", + "syn 2.0.100", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -488,7 +566,9 @@ name = "bottlecap" version = "0.1.0" dependencies = [ "async-trait", + "aws_lambda_events", "axum", + "axum-extra", "base64 0.22.1", "bytes 1.10.1", "chrono", @@ -503,6 +583,7 @@ dependencies = [ "ddsketch-agent 0.1.0 (git+https://github.com/DataDog/saluki/)", "dogstatsd", "figment", + "fnv", "futures 0.3.31", "hex", "hmac", @@ -512,10 +593,15 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "lazy_static", + "libddwaf", "log", + "mime", + "mock_instant", + "multer", "nix 0.26.4", "opentelemetry-proto", "opentelemetry-semantic-conventions", + "ordered_hash_map", "proptest", "prost", "protobuf", @@ -527,9 +613,11 @@ dependencies = [ "rustls-native-certs", "serde", "serde-aux", + "serde_html_form", "serde_json", "serial_test", "sha2", + "tempfile", "thiserror 1.0.69", "tikv-jemallocator", "tokio", @@ -573,6 +661,9 @@ name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +dependencies = [ + "serde", +] [[package]] name = "cc" @@ -612,8 +703,11 @@ version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ + "android-tzdata", + "iana-time-zone", "num-traits", "serde", + "windows-link", ] [[package]] @@ -665,6 +759,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.10.0" @@ -721,6 +826,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.100", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.100", +] + [[package]] name = "datadog-fips" version = "0.1.0" @@ -899,6 +1039,16 @@ dependencies = [ "smallvec", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "derive_more" version = "1.0.0" @@ -1008,6 +1158,15 @@ dependencies = [ "log", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1073,6 +1232,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -1313,6 +1484,15 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.15.2" @@ -1458,6 +1638,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-serde" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f056c8559e3757392c8d091e796416e4649d8e49e88b8d76df6c002f05027fd" +dependencies = [ + "http 1.3.1", + "serde", +] + [[package]] name = "httparse" version = "1.10.1" @@ -1614,6 +1804,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -1732,6 +1945,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -1761,6 +1980,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -1771,6 +1991,7 @@ checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown 0.15.2", + "serde", ] [[package]] @@ -1779,6 +2000,17 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "libc", +] + [[package]] name = "iovec" version = "0.1.4" @@ -1899,6 +2131,29 @@ version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +[[package]] +name = "libddwaf" +version = "1.26.0" +source = "git+https://github.com/DataDog/libddwaf-rust?rev=f567dec1685e39af5549421f491b1babacafb173#f567dec1685e39af5549421f491b1babacafb173" +dependencies = [ + "libddwaf-sys", + "serde", +] + +[[package]] +name = "libddwaf-sys" +version = "1.26.0" +source = "git+https://github.com/DataDog/libddwaf-rust?rev=f567dec1685e39af5549421f491b1babacafb173#f567dec1685e39af5549421f491b1babacafb173" +dependencies = [ + "bindgen 0.72.0", + "flate2", + "hyper-rustls", + "libc", + "reqwest", + "rustls", + "tar", +] + [[package]] name = "libloading" version = "0.8.6" @@ -1917,6 +2172,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.9.0", "libc", + "redox_syscall", ] [[package]] @@ -2009,6 +2265,29 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mock_instant" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce6dd36094cac388f119d2e9dc82dc730ef91c32a6222170d630e5414b956e6" + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes 1.10.1", + "encoding_rs", + "futures-util", + "http 1.3.1", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + [[package]] name = "multimap" version = "0.10.0" @@ -2054,6 +2333,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.19" @@ -2155,6 +2440,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered_hash_map" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab0e5f22bf6dd04abd854a8874247813a8fa2c8c1260eba6fbb150270ce7c176" +dependencies = [ + "hashbrown 0.13.2", +] + [[package]] name = "parking" version = "2.2.1" @@ -2308,6 +2602,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2479,6 +2779,17 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "query_map" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eab6b8b1074ef3359a863758dae650c7c0c6027927a085b7af911c8e0bf3a15" +dependencies = [ + "form_urlencoded", + "serde", + "serde_derive", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -2694,6 +3005,7 @@ checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" dependencies = [ "base64 0.22.1", "bytes 1.10.1", + "futures-channel", "futures-core", "futures-util", "h2", @@ -2823,9 +3135,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.26" +version = "0.23.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" dependencies = [ "aws-lc-rs", "once_cell", @@ -2859,18 +3171,19 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "web-time", + "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.1" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "aws-lc-rs", "ring", @@ -3014,6 +3327,28 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "serde_dynamo" +version = "4.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36c1b1792cfd9de29eb373ee6a4b74650369c096f55db7198ceb7b8921d1f7f" +dependencies = [ + "base64 0.21.7", + "serde", +] + +[[package]] +name = "serde_html_form" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4" +dependencies = [ + "form_urlencoded", + "indexmap 2.9.0", + "itoa 1.0.15", + "serde", +] + [[package]] name = "serde_json" version = "1.0.140" @@ -3058,6 +3393,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.9.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -3179,6 +3544,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -3203,6 +3574,12 @@ dependencies = [ "precomputed-hash", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -3251,11 +3628,22 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" -version = "3.19.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", "getrandom 0.3.2", @@ -3345,6 +3733,37 @@ dependencies = [ "tikv-jemalloc-sys", ] +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa 1.0.15", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -3389,16 +3808,18 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.2" +version = "1.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" dependencies = [ "backtrace", "bytes 1.10.1", + "io-uring", "libc", "mio", "pin-project-lite", "signal-hook-registry", + "slab", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -3897,6 +4318,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-link" version = "0.1.1" @@ -4099,6 +4529,16 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "xattr" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" +dependencies = [ + "libc", + "rustix 1.0.5", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/bottlecap/Cargo.toml b/bottlecap/Cargo.toml index 39a25f46e..035f131d7 100644 --- a/bottlecap/Cargo.toml +++ b/bottlecap/Cargo.toml @@ -6,9 +6,13 @@ publish = false [dependencies] async-trait = { version = "0.1", default-features = false } +aws_lambda_events = { version = "0.16", default-features = false, features = ["alb", "apigw", "cloudwatch_events", "cloudwatch_logs", "dynamodb", "eventbridge", "kinesis", "s3","sns", "sqs", "lambda_function_urls"] } +axum = { version = "0.8.4", default-features = false, features = ["default"] } +axum-extra = { version = "0.10.1", default-features = false, features = ["cookie"] } bytes = { version = "1.2", default-features = false } chrono = { version = "0.4", features = ["serde", "std", "now"], default-features = false } figment = { version = "0.10", default-features = false, features = ["yaml", "env"] } +fnv = { version = "1.0", default-features = false } hyper = { version = "1.6", default-features = false, features = ["server"] } hyper-util = { version = "0.1.10", features = [ "http1", @@ -17,13 +21,17 @@ hyper-util = { version = "0.1.10", features = [ ] } http-body = "0.1" http-body-util = "0.1" +mime = { version = "0.3", default-features = false } lazy_static = { version = "1.5", default-features = false } log = { version = "0.4", default-features = false } +multer = { version = "3.1", default-features = false } nix = { version = "0.26", default-features = false, features = ["feature", "fs"] } +ordered_hash_map = { version = "0.4", default-features = false } protobuf = { version = "3.5", default-features = false } regex = { version = "1.10", default-features = false } reqwest = { version = "0.12.11", features = ["json", "http2"], default-features = false } serde = { version = "1.0", default-features = false, features = ["derive"] } +serde_html_form = { version = "0.2", default-features = false } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } thiserror = { version = "1.0", default-features = false } tokio = { version = "1.37", default-features = false, features = ["macros", "rt-multi-thread"] } @@ -50,7 +58,7 @@ rustls-native-certs = { version = "0.8.1", optional = true } # method in favor of our # datadog_fips::reqwest_adapter::create_reqwest_client_builder. An example can # be found in the clippy.toml file adjacent to this Cargo.toml. -datadog-protos = { version = "0.1.0", default-features = false, git = "https://github.com/DataDog/saluki/", rev = "c89b58e5784b985819baf11f13f7d35876741222"} +datadog-protos = { version = "0.1.0", default-features = false, git = "https://github.com/DataDog/saluki/", rev = "c89b58e5784b985819baf11f13f7d35876741222" } ddsketch-agent = { version = "0.1.0", default-features = false, git = "https://github.com/DataDog/saluki/" } ddcommon = { git = "https://github.com/DataDog/libdatadog", rev = "8a49c7df2d9cbf05118bfd5b85772676f71b34f2" } datadog-trace-protobuf = { git = "https://github.com/DataDog/libdatadog", rev = "8a49c7df2d9cbf05118bfd5b85772676f71b34f2" } @@ -60,13 +68,15 @@ datadog-trace-obfuscation = { git = "https://github.com/DataDog/libdatadog", rev dogstatsd = { git = "https://github.com/DataDog/serverless-components", rev = "d131de8419c191ce21c91bb30b5915c4d8a2cc5a", default-features = false } datadog-trace-agent = { git = "https://github.com/DataDog/serverless-components", rev = "d131de8419c191ce21c91bb30b5915c4d8a2cc5a" } datadog-fips = { git = "https://github.com/DataDog/serverless-components", rev = "d131de8419c191ce21c91bb30b5915c4d8a2cc5a", default-features = false } -axum = { version = "0.8.4", default-features = false, features = ["default"] } +libddwaf = { version = "1.26.0", git = "https://github.com/DataDog/libddwaf-rust", rev = "f567dec1685e39af5549421f491b1babacafb173", default-features = false, features = ["default", "static"] } [dev-dependencies] figment = { version = "0.10", default-features = false, features = ["yaml", "env", "test"] } proptest = "1.4" httpmock = "0.7" +mock_instant = "0.6" serial_test = "3.1" +tempfile = "3.20" [build-dependencies] # No external dependencies needed for the build script @@ -95,7 +105,25 @@ fips = [ "datadog-trace-utils/fips", "dogstatsd/fips", "datadog-fips/fips", + "libddwaf/fips", "reqwest/rustls-tls-native-roots-no-provider", "rustls/fips", "rustls-native-certs", ] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage,coverage_nightly)'] } + +# libddwaf's bindgen uses `clang-sys` which by default dynamically opens +# `libclang.so`; however this is not supported on Alpine Linux (the usual musl +# target). +# Enabling the `static` feature of `bindgen`/`clang-sys` creates additional +# build requirements (`libLLVM.a`), which Alpine's `llvm-static` does not +# provide (it has its content presented as many `libLLVM*.a` files). Disabling +# statically linking to the C runtime alleviates this issue, but can result in +# `proc-macro` not being usable; which notably causes doc tests to fail with +# a big linker error. +[target.aarch64-unknown-linux-musl] +rustflags = ["-C", "target-feature=-crt-static"] +[target.x86_64-unknown-linux-musl] +rustflags = ["-C", "target-feature=-crt-static"] diff --git a/bottlecap/LICENSE-3rdparty.csv b/bottlecap/LICENSE-3rdparty.csv index b8205f160..53116ac26 100644 --- a/bottlecap/LICENSE-3rdparty.csv +++ b/bottlecap/LICENSE-3rdparty.csv @@ -3,14 +3,18 @@ addr2line,https://github.com/gimli-rs/addr2line,Apache-2.0 OR MIT,The addr2line adler2,https://github.com/oyvindln/adler2,0BSD OR MIT OR Apache-2.0,"Jonas Schievink , oyvindln " ahash,https://github.com/tkaitchuck/ahash,MIT OR Apache-2.0,Tom Kaitchuck aho-corasick,https://github.com/BurntSushi/aho-corasick,Unlicense OR MIT,Andrew Gallant +android-tzdata,https://github.com/RumovZ/android-tzdata,MIT OR Apache-2.0,RumovZ +android_system_properties,https://github.com/nical/android_system_properties,MIT OR Apache-2.0,Nicolas Silva anyhow,https://github.com/dtolnay/anyhow,MIT OR Apache-2.0,David Tolnay async-trait,https://github.com/dtolnay/async-trait,MIT OR Apache-2.0,David Tolnay atomic,https://github.com/Amanieu/atomic-rs,Apache-2.0 OR MIT,Amanieu d'Antras atomic-waker,https://github.com/smol-rs/atomic-waker,Apache-2.0 OR MIT,"Stjepan Glavina , Contributors to futures-rs" aws-lc-rs,https://github.com/aws/aws-lc-rs,ISC AND (Apache-2.0 OR ISC),AWS-LibCrypto aws-lc-sys,https://github.com/aws/aws-lc-rs,ISC AND (Apache-2.0 OR ISC) AND OpenSSL,AWS-LC +aws_lambda_events,https://github.com/awslabs/aws-lambda-rust-runtime,MIT,"Christian Legnitto , Sam Rijs , David Calavera " axum,https://github.com/tokio-rs/axum,MIT,The axum Authors axum-core,https://github.com/tokio-rs/axum,MIT,The axum-core Authors +axum-extra,https://github.com/tokio-rs/axum,MIT,The axum-extra Authors backtrace,https://github.com/rust-lang/backtrace-rs,MIT OR Apache-2.0,The Rust Project Developers base64,https://github.com/marshallpierce/rust-base64,MIT OR Apache-2.0,"Alice Maz , Marshall Pierce " base64,https://github.com/marshallpierce/rust-base64,MIT OR Apache-2.0,Marshall Pierce @@ -26,10 +30,12 @@ cfg-if,https://github.com/alexcrichton/cfg-if,MIT OR Apache-2.0,Alex Crichton const_format_proc_macros,https://github.com/rodrimati1992/const_format_crates,Zlib,rodrimati1992 +cookie,https://github.com/SergioBenitez/cookie-rs,MIT OR Apache-2.0,"Sergio Benitez , Alex Crichton " core-foundation,https://github.com/servo/core-foundation-rs,MIT OR Apache-2.0,The Servo Project Developers cpufeatures,https://github.com/RustCrypto/utils,MIT OR Apache-2.0,RustCrypto Developers crc32fast,https://github.com/srijs/rust-crc32fast,MIT OR Apache-2.0,"Sam Rijs , Alex Crichton " crypto-common,https://github.com/RustCrypto/traits,MIT OR Apache-2.0,RustCrypto Developers +darling,https://github.com/TedDriggs/darling,MIT,Ted Driggs datadog-fips,https://github.com/DataDog/serverless-components,Apache-2.0,The datadog-fips Authors datadog-protos,https://github.com/DataDog/saluki,Apache-2.0,The datadog-protos Authors datadog-trace-agent,https://github.com/DataDog/serverless-components,Apache-2.0,The datadog-trace-agent Authors @@ -39,11 +45,13 @@ datadog-trace-protobuf,https://github.com/DataDog/libdatadog,Apache-2.0,David Le datadog-trace-utils,https://github.com/DataDog/libdatadog,Apache-2.0,The datadog-trace-utils Authors ddcommon,https://github.com/DataDog/libdatadog,Apache-2.0,The ddcommon Authors ddsketch-agent,https://github.com/DataDog/saluki,Apache-2.0,The ddsketch-agent Authors +deranged,https://github.com/jhpratt/deranged,MIT OR Apache-2.0,Jacob Pratt derive_more,https://github.com/JelteF/derive_more,MIT,Jelte Fennema digest,https://github.com/RustCrypto/traits,MIT OR Apache-2.0,RustCrypto Developers displaydoc,https://github.com/yaahc/displaydoc,MIT OR Apache-2.0,Jane Lusby dogstatsd,https://github.com/DataDog/serverless-components,Apache-2.0,The dogstatsd Authors either,https://github.com/rayon-rs/either,MIT OR Apache-2.0,bluss +encoding_rs,https://github.com/hsivonen/encoding_rs,(Apache-2.0 OR MIT) AND BSD-3-Clause,Henri Sivonen equivalent,https://github.com/indexmap-rs/equivalent,Apache-2.0 OR MIT,The equivalent Authors errno,https://github.com/lambda-fairy/rust-errno,MIT OR Apache-2.0,"Chris Wong , Dan Gohman " fastrand,https://github.com/smol-rs/fastrand,Apache-2.0 OR MIT,Stjepan Glavina @@ -73,6 +81,7 @@ hmac,https://github.com/RustCrypto/MACs,MIT OR Apache-2.0,RustCrypto Developers http,https://github.com/hyperium/http,MIT OR Apache-2.0,"Alex Crichton , Carl Lerche , Sean McArthur " http-body,https://github.com/hyperium/http-body,MIT,Carl Lerche http-body,https://github.com/hyperium/http-body,MIT,"Carl Lerche , Lucio Franco , Sean McArthur " +http-serde,https://gitlab.com/kornelski/http-serde,Apache-2.0 OR MIT,Kornel httparse,https://github.com/seanmonstar/httparse,MIT OR Apache-2.0,Sean McArthur httpdate,https://github.com/pyfisch/httpdate,MIT OR Apache-2.0,Pyfisch hyper,https://github.com/hyperium/hyper,MIT,Sean McArthur @@ -80,6 +89,8 @@ hyper-http-proxy,https://github.com/metalbear-co/hyper-http-proxy,MIT,MetalBear hyper-rustls,https://github.com/rustls/hyper-rustls,Apache-2.0 OR ISC OR MIT,The hyper-rustls Authors hyper-timeout,https://github.com/hjr3/hyper-timeout,MIT OR Apache-2.0,Herman J. Radtke III hyper-util,https://github.com/hyperium/hyper-util,MIT,Sean McArthur +iana-time-zone,https://github.com/strawlab/iana-time-zone,MIT OR Apache-2.0,"Andrew Straw , René Kijewski , Ryan Lopopolo " +iana-time-zone-haiku,https://github.com/strawlab/iana-time-zone,MIT OR Apache-2.0,René Kijewski icu_collections,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers icu_locid,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers icu_locid_transform,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers @@ -90,10 +101,12 @@ icu_properties,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Projec icu_properties_data,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers icu_provider,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers icu_provider_macros,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers +ident_case,https://github.com/TedDriggs/ident_case,MIT OR Apache-2.0,Ted Driggs idna_adapter,https://github.com/hsivonen/idna_adapter,Apache-2.0 OR MIT,The rust-url developers indexmap,https://github.com/bluss/indexmap,Apache-2.0 OR MIT,The indexmap Authors indexmap,https://github.com/indexmap-rs/indexmap,Apache-2.0 OR MIT,The indexmap Authors inlinable_string,https://github.com/fitzgen/inlinable_string,Apache-2.0 OR MIT,Nick Fitzgerald +io-uring,https://github.com/tokio-rs/io-uring,MIT OR Apache-2.0,quininer iovec,https://github.com/carllerche/iovec,MIT OR Apache-2.0,Carl Lerche ipnet,https://github.com/krisprice/ipnet,MIT OR Apache-2.0,Kris Price itertools,https://github.com/rust-itertools/itertools,MIT OR Apache-2.0,bluss @@ -102,6 +115,8 @@ jobserver,https://github.com/rust-lang/jobserver-rs,MIT OR Apache-2.0,Alex Crich js-sys,https://github.com/rustwasm/wasm-bindgen/tree/master/crates/js-sys,MIT OR Apache-2.0,The wasm-bindgen Developers lazy_static,https://github.com/rust-lang-nursery/lazy-static.rs,MIT OR Apache-2.0,Marvin Löbel libc,https://github.com/rust-lang/libc,MIT OR Apache-2.0,The Rust Project Developers +libddwaf,https://github.com/DataDog/libddwaf-rust,Apache-2.0,"DataDog, Inc. " +libddwaf-sys,https://github.com/DataDog/libddwaf-rust,Apache-2.0,"DataDog, Inc. " linux-raw-sys,https://github.com/sunfishcode/linux-raw-sys,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,Dan Gohman litemap,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers log,https://github.com/rust-lang/log,MIT OR Apache-2.0,The Rust Project Developers @@ -111,7 +126,9 @@ memchr,https://github.com/BurntSushi/memchr,Unlicense OR MIT,"Andrew Gallant miniz_oxide,https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide,MIT OR Zlib OR Apache-2.0,"Frommi , oyvindln , Rich Geldreich richgel99@gmail.com" mio,https://github.com/tokio-rs/mio,MIT,"Carl Lerche , Thomas de Zeeuw , Tokio Contributors " +multer,https://github.com/rwf2/multer,MIT,Rousan Ali nix,https://github.com/nix-rust/nix,MIT,The nix-rust Project Developers +num-conv,https://github.com/jhpratt/num-conv,MIT OR Apache-2.0,Jacob Pratt num-traits,https://github.com/rust-num/num-traits,MIT OR Apache-2.0,The Rust Project Developers object,https://github.com/gimli-rs/object,Apache-2.0 OR MIT,The object Authors once_cell,https://github.com/matklad/once_cell,MIT OR Apache-2.0,Aleksey Kladov @@ -121,6 +138,7 @@ opentelemetry-proto,https://github.com/open-telemetry/opentelemetry-rust/tree/ma opentelemetry-semantic-conventions,https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-semantic-conventions,Apache-2.0,The opentelemetry-semantic-conventions Authors opentelemetry_sdk,https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-sdk,Apache-2.0,The opentelemetry_sdk Authors ordered-float,https://github.com/reem/rust-ordered-float,MIT,"Jonathan Reem , Matt Brubeck " +ordered_hash_map,https://gitlab.com/kelderon/rs-collections,BSD-2-Clause,William Correia parking_lot,https://github.com/Amanieu/parking_lot,MIT OR Apache-2.0,Amanieu d'Antras paste,https://github.com/dtolnay/paste,MIT OR Apache-2.0,David Tolnay pear,https://github.com/SergioBenitez/Pear,MIT OR Apache-2.0,Sergio Benitez @@ -129,11 +147,13 @@ pin-project,https://github.com/taiki-e/pin-project,Apache-2.0 OR MIT,The pin-pro pin-project-internal,https://github.com/taiki-e/pin-project,Apache-2.0 OR MIT,The pin-project-internal Authors pin-project-lite,https://github.com/taiki-e/pin-project-lite,Apache-2.0 OR MIT,The pin-project-lite Authors pin-utils,https://github.com/rust-lang-nursery/pin-utils,MIT OR Apache-2.0,Josef Brandl +powerfmt,https://github.com/jhpratt/powerfmt,MIT OR Apache-2.0,Jacob Pratt ppv-lite86,https://github.com/cryptocorrosion/cryptocorrosion,MIT OR Apache-2.0,The CryptoCorrosion Contributors proc-macro2,https://github.com/dtolnay/proc-macro2,MIT OR Apache-2.0,"David Tolnay , Alex Crichton " proc-macro2-diagnostics,https://github.com/SergioBenitez/proc-macro2-diagnostics,MIT OR Apache-2.0,Sergio Benitez prost,https://github.com/tokio-rs/prost,Apache-2.0,"Dan Burkert , Lucio Franco , Casper Meijn , Tokio Contributors " protobuf,https://github.com/stepancheg/rust-protobuf,MIT,Stepan Koltsov +query_map,https://github.com/calavera/query-map-rs,MIT,The query_map Authors quinn,https://github.com/quinn-rs/quinn,MIT OR Apache-2.0,The quinn Authors quinn-proto,https://github.com/quinn-rs/quinn,MIT OR Apache-2.0,The quinn-proto Authors quinn-udp,https://github.com/quinn-rs/quinn,MIT OR Apache-2.0,The quinn-udp Authors @@ -169,9 +189,13 @@ serde,https://github.com/serde-rs/serde,MIT OR Apache-2.0,"Erick Tryzelaar serde-value,https://github.com/arcnmx/serde-value,MIT,arcnmx serde_bytes,https://github.com/serde-rs/bytes,MIT OR Apache-2.0,David Tolnay +serde_dynamo,https://github.com/zenlist/serde_dynamo,MIT,Bryan Burgers +serde_html_form,https://github.com/jplatte/serde_html_form,MIT,The serde_html_form Authors serde_json,https://github.com/serde-rs/json,MIT OR Apache-2.0,"Erick Tryzelaar , David Tolnay " serde_path_to_error,https://github.com/dtolnay/path-to-error,MIT OR Apache-2.0,David Tolnay serde_urlencoded,https://github.com/nox/serde_urlencoded,MIT OR Apache-2.0,Anthony Ramine +serde_with,https://github.com/jonasbb/serde_with,MIT OR Apache-2.0,"Jonas Bushart, Marcin Kaźmierczak" +serde_with_macros,https://github.com/jonasbb/serde_with,MIT OR Apache-2.0,Jonas Bushart serde_yaml,https://github.com/dtolnay/serde-yaml,MIT OR Apache-2.0,David Tolnay sha1,https://github.com/RustCrypto/hashes,MIT OR Apache-2.0,RustCrypto Developers sha2,https://github.com/RustCrypto/hashes,MIT OR Apache-2.0,RustCrypto Developers @@ -181,8 +205,10 @@ signal-hook-registry,https://github.com/vorner/signal-hook,Apache-2.0 OR MIT,"Mi slab,https://github.com/tokio-rs/slab,MIT,Carl Lerche smallvec,https://github.com/servo/rust-smallvec,MIT OR Apache-2.0,The Servo Project Developers socket2,https://github.com/rust-lang/socket2,MIT OR Apache-2.0,"Alex Crichton , Thomas de Zeeuw " +spin,https://github.com/mvdnes/spin-rs,MIT,"Mathijs van de Nes , John Ericson , Joshua Barretto " stable_deref_trait,https://github.com/storyyeller/stable_deref_trait,MIT OR Apache-2.0,Robert Grosse static_assertions,https://github.com/nvzqz/static-assertions-rs,MIT OR Apache-2.0,Nikolai Vazquez +strsim,https://github.com/rapidfuzz/strsim-rs,MIT,"Danny Guo , maxbachmann " subtle,https://github.com/dalek-cryptography/subtle,BSD-3-Clause,"Isis Lovecruft , Henry de Valence " syn,https://github.com/dtolnay/syn,MIT OR Apache-2.0,David Tolnay sync_wrapper,https://github.com/Actyx/sync_wrapper,Apache-2.0,Actyx AG @@ -192,6 +218,7 @@ thiserror,https://github.com/dtolnay/thiserror,MIT OR Apache-2.0,David Tolnay tikv-jemalloc-sys,https://github.com/tikv/jemallocator,MIT OR Apache-2.0,"Alex Crichton , Gonzalo Brito Gadeschi , The TiKV Project Developers" tikv-jemallocator,https://github.com/tikv/jemallocator,MIT OR Apache-2.0,"Alex Crichton , Gonzalo Brito Gadeschi , Simon Sapin , Steven Fackler , The TiKV Project Developers" +time,https://github.com/time-rs/time,MIT OR Apache-2.0,"Jacob Pratt , Time contributors" tinybytes,https://github.com/DataDog/libdatadog,Apache-2.0,The tinybytes Authors tinystr,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers tinyvec,https://github.com/Lokathor/tinyvec,Zlib OR Apache-2.0 OR MIT,Lokathor @@ -231,6 +258,7 @@ wasm-bindgen-shared,https://github.com/rustwasm/wasm-bindgen/tree/master/crates/ web-sys,https://github.com/rustwasm/wasm-bindgen/tree/master/crates/web-sys,MIT OR Apache-2.0,The wasm-bindgen Developers web-time,https://github.com/daxpedda/web-time,MIT OR Apache-2.0,The web-time Authors webpki-roots,https://github.com/rustls/webpki-roots,MPL-2.0,The webpki-roots Authors +windows-core,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft windows-link,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft windows-registry,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft windows-result,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Microsoft diff --git a/bottlecap/src/appsec/mod.rs b/bottlecap/src/appsec/mod.rs new file mode 100644 index 000000000..5077cc300 --- /dev/null +++ b/bottlecap/src/appsec/mod.rs @@ -0,0 +1,30 @@ +use std::env; + +use crate::config::Config; + +mod payload; +pub mod processor; +mod sampler; + +#[must_use] +pub const fn is_enabled(config: &Config) -> bool { + config.serverless_appsec_enabled +} + +/// Reads the `DD_APM_TRACING_ENABLED` environment variable to determine whether ASM runs in +/// standalone mode. +/// +/// This is a direct port of the Go logic and that is why we are not using the +/// `config` crate. +#[must_use] +pub fn is_standalone() -> bool { + let apm_tracing_enabled = env::var("DD_APM_TRACING_ENABLED"); + let is_set = apm_tracing_enabled.is_ok(); + + let enabled = apm_tracing_enabled + .unwrap_or("false".to_string()) + .to_lowercase() + == "true"; + + is_set && enabled +} diff --git a/bottlecap/src/appsec/payload/body.rs b/bottlecap/src/appsec/payload/body.rs new file mode 100644 index 000000000..12a5ee9c8 --- /dev/null +++ b/bottlecap/src/appsec/payload/body.rs @@ -0,0 +1,341 @@ +use base64::Engine; +use libddwaf::object::{WafMap, WafObject, WafString}; +use mime::Mime; +use tracing::{debug, warn}; + +pub(super) async fn parse_body( + body: impl AsRef<[u8]>, + is_base64_encoded: bool, + content_type: Option<&str>, +) -> Result, Box> { + if is_base64_encoded { + let body = base64::engine::general_purpose::STANDARD.decode(body)?; + return Box::pin(parse_body(body, false, content_type)).await; + } + + let body = body.as_ref(); + let mime_type = match content_type + .unwrap_or("application/json") + .parse::() + { + Ok(mime) => mime, + Err(e) => return Err(e.into()), + }; + + Ok(match (mime_type.type_(), mime_type.subtype()) { + // text/json | application/json | application/vnd.api+json + (mime::APPLICATION, sub) + if sub == mime::JSON + || (sub == "vnd.api" && mime_type.suffix() == Some(mime::JSON)) => + { + Some(serde_json::from_slice(body)?) + } + (mime::APPLICATION, mime::WWW_FORM_URLENCODED) => { + let pairs: Vec<(String, String)> = serde_html_form::from_bytes(body)?; + let mut res = WafMap::new(pairs.len() as u64); + for (i, (key, value)) in pairs.into_iter().enumerate() { + res[i] = (key.as_str(), value.as_str()).into(); + } + Some(res.into()) + } + (mime::APPLICATION | mime::TEXT, mime::XML) => { + // XML parsing is not currently supported due to serde_xml_rs limitations + debug!( + "appsec: XML parsing not supported, returning None for content type: {mime_type}" + ); + None + } + (mime::MULTIPART, mime::FORM_DATA) => { + let Some(boundary) = mime_type.get_param("boundary") else { + warn!("appsec: cannot attempt parsing multipart/form-data without boundary"); + return Ok(None); + }; + // We have to go through this dance because [`multer::Multipart`] requires an async stream. + let body = body.to_vec(); + let reader = futures::stream::iter([Result::, std::io::Error>::Ok(body)]); + let mut multipart = multer::Multipart::new(reader, boundary.as_str()); + + let mut fields = Vec::new(); + while let Some(field) = multipart.next_field().await? { + let Some(name) = field.name().map(str::to_string) else { + continue; + }; + let Some(content_type) = field.content_type().map(Mime::to_string) else { + continue; + }; + let Some(value) = Box::pin(parse_body( + field.bytes().await?, + false, + Some(content_type.as_ref()), + )) + .await? + else { + continue; + }; + fields.push((name, value)); + } + let mut res = WafMap::new(fields.len() as u64); + for (i, (name, value)) in fields.into_iter().enumerate() { + res[i] = (name.as_str(), value).into(); + } + Some(res.into()) + } + (mime::TEXT, mime::PLAIN) => Some(WafString::new(body).into()), + _ => { + debug!("appsec: unsupported content type: {mime_type}"); + None + } + }) +} + +#[cfg_attr(coverage_nightly, coverage(off))] // Test modules skew coverage metrics +#[cfg(test)] +mod tests { + use libddwaf::{waf_array, waf_map, waf_object}; + + use super::*; + + #[tokio::test] + async fn test_parse_json_body_with_default_content_type() { + let json_body = r#"{"test": "value"}"#; + + let result = parse_body(json_body, false, None) + .await + .expect("should parse application/json") + .expect("should produce some value"); + assert_eq!(result, waf_map!(("test", "value"))); + } + + #[tokio::test] + async fn test_parse_json_body() { + let json_body = r#"{"test": "value"}"#; + + for content_type in [ + "application/json", + "application/vnd.api+json", + "application/json; charset=utf-8", + ] { + let result = parse_body(json_body, false, Some(content_type)) + .await + .expect("should parse successfully") + .expect("should produce some value"); + assert_eq!(result, waf_map!(("test", "value"))); + } + } + + #[tokio::test] + async fn test_parse_invalid_json_body() { + let invalid_json = r#"{"key": "value", "invalid": }"#; + let result = parse_body(invalid_json, false, Some("application/json")).await; + + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_parse_xml_body() { + let xml_body = r#"value"#; + + // Test application/xml - currently not supported, should return None + let result = parse_body(xml_body, false, Some("application/xml")).await; + assert!(result.is_ok()); + let parsed = result.expect("should handle XML content type"); + assert!(parsed.is_none(), "XML parsing is not currently supported"); + + // Test text/xml - currently not supported, should return None + let result = parse_body(xml_body, false, Some("text/xml")).await; + assert!(result.is_ok()); + let parsed = result.expect("should handle XML content type"); + assert!(parsed.is_none(), "XML parsing is not currently supported"); + } + + #[tokio::test] + async fn test_parse_invalid_xml_body() { + let invalid_xml = "value"; + let result = parse_body(invalid_xml, false, Some("application/xml")).await; + + // XML parsing is not currently supported, so it should return Ok(None) regardless of validity + assert!(result.is_ok()); + let parsed = result.expect("should handle XML content type"); + assert!(parsed.is_none(), "XML parsing is not currently supported"); + } + + #[tokio::test] + async fn test_parse_url_encoded_body() { + let form_data = "key1=value1&key2=value2&key3=value%20with%20spaces"; + let result = parse_body(form_data, false, Some("application/x-www-form-urlencoded")) + .await + .expect("should parse URL-encoded data") + .expect("should produce some value"); + assert_eq!( + result, + waf_map!( + ("key1", "value1"), + ("key2", "value2"), + ("key3", "value with spaces") + ) + ); + } + + #[tokio::test] + async fn test_parse_invalid_url_encoded_body() { + let invalid_form_data = "key1=value1&key2=&=value3"; + let result = parse_body( + invalid_form_data, + false, + Some("application/x-www-form-urlencoded"), + ) + .await; + + // URL-encoded parsing should be permissive and not fail on malformed data + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_parse_multipart_form_data() { + let boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"; + let multipart_body = format!( + "--{boundary}\r\nContent-Disposition: form-data; name=\"field1\"\r\nContent-Type: text/plain\r\n\r\nvalue1\r\n--{boundary}\r\nContent-Disposition: form-data; name=\"field2\"\r\nContent-Type: application/json\r\n\r\n{{\"key\": \"value\"}}\r\n--{boundary}--\r\n" + ); + + let content_type = format!("multipart/form-data; boundary={boundary}"); + let result = parse_body(multipart_body, false, Some(&content_type)) + .await + .expect("should parse multipart form data") + .expect("should produce some value"); + assert_eq!( + result, + waf_map!(("field1", "value1"), ("field2", waf_map!(("key", "value")))) + ); + } + + #[tokio::test] + async fn test_parse_multipart_form_data_without_boundary() { + let multipart_body = "some content"; + let result = parse_body(multipart_body, false, Some("multipart/form-data")).await; + + assert!(result.is_ok()); + let parsed = result.expect("should handle multipart without boundary"); + assert!(parsed.is_none()); // Should return None due to missing boundary + } + + #[tokio::test] + async fn test_parse_text_plain_body() { + let text_body = "This is plain text content"; + let result = parse_body(text_body, false, Some("text/plain")) + .await + .expect("should parse plain text") + .expect("should produce some value"); + assert_eq!(result, waf_object!("This is plain text content")); + } + + #[tokio::test] + async fn test_parse_base64_encoded_body() { + let json_body = r#"{"key": "value"}"#; + let base64_body = base64::engine::general_purpose::STANDARD.encode(json_body); + + let result = parse_body(base64_body, true, Some("application/json")) + .await + .expect("should parse base64 encoded JSON") + .expect("should produce some value"); + assert_eq!(result, waf_map!(("key", "value"))); + } + + #[tokio::test] + async fn test_parse_invalid_base64_encoded_body() { + let invalid_base64 = "invalid_base64!@#$%"; + let result = parse_body(invalid_base64, true, Some("application/json")).await; + + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_parse_unsupported_content_type() { + let body = "some content"; + let result = parse_body(body, false, Some("application/octet-stream")).await; + + assert!(result.is_ok()); + let parsed = result.expect("should handle unsupported content type"); + assert!(parsed.is_none()); // Should return None for unsupported content types + } + + #[tokio::test] + async fn test_parse_invalid_content_type() { + let body = "some content"; + let result = parse_body(body, false, Some("invalid/content-type")).await; + + assert!(result.is_ok()); + let parsed = result.expect("should handle invalid content type"); + assert!(parsed.is_none()); // Should return None for invalid content type + } + + #[tokio::test] + async fn test_parse_empty_body() { + let result = parse_body("", false, Some("application/json")).await; + + assert!(result.is_err()); // Empty JSON should fail to parse + } + + #[tokio::test] + async fn test_parse_malformed_mime_type() { + let body = "some content"; + let result = parse_body(body, false, Some("not-a-valid-mime-type")).await; + + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_parse_nested_json_body() { + let nested_json = r#"{"user": {"name": "John", "age": 30}, "tags": ["tag1", "tag2"]}"#; + let result = parse_body(nested_json, false, Some("application/json")) + .await + .expect("should parse nested JSON") + .expect("should produce some value"); + assert_eq!( + result, + waf_map!( + ("user", waf_map!(("name", "John"), ("age", 30u64))), + ("tags", waf_array!("tag1", "tag2")) + ) + ); + } + + #[tokio::test] + async fn test_parse_large_json_body() { + let large_json = format!(r#"{{"data": "{}"}}"#, "x".repeat(10000)); + let result = parse_body(large_json, false, Some("application/json")) + .await + .expect("should parse large JSON") + .expect("should produce some value"); + assert_eq!(result, waf_map!(("data", "x".repeat(10000).as_str()))); + } + + #[tokio::test] + async fn test_parse_multipart_with_missing_field_name() { + let boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"; + let multipart_body = format!( + "--{boundary}\r\nContent-Disposition: form-data\r\nContent-Type: text/plain\r\n\r\nvalue1\r\n--{boundary}--\r\n" + ); + + let content_type = format!("multipart/form-data; boundary={boundary}"); + let result = parse_body(multipart_body, false, Some(&content_type)).await; + + assert!(result.is_ok()); + let parsed = result.expect("should handle multipart with missing field name"); + assert!(parsed.is_some()); + } + + #[tokio::test] + async fn test_parse_multipart_with_missing_content_type() { + let boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"; + let multipart_body = format!( + "--{boundary}\r\nContent-Disposition: form-data; name=\"field1\"\r\n\r\nvalue1\r\n--{boundary}--\r\n" + ); + + let content_type = format!("multipart/form-data; boundary={boundary}"); + let result = parse_body(multipart_body, false, Some(&content_type)).await; + + assert!(result.is_ok()); + let parsed = result.expect("should handle multipart with missing content type"); + assert!(parsed.is_some()); + } +} diff --git a/bottlecap/src/appsec/payload/mod.rs b/bottlecap/src/appsec/payload/mod.rs new file mode 100644 index 000000000..fdefeb8a2 --- /dev/null +++ b/bottlecap/src/appsec/payload/mod.rs @@ -0,0 +1,1013 @@ +use std::collections::HashMap; + +use aws_lambda_events::{ + alb, apigw, cloudwatch_events, cloudwatch_logs, dynamodb, eventbridge, kinesis, + lambda_function_urls, s3, sns, sqs, +}; +use bytes::{Buf, Bytes}; +use libddwaf::object::{WafArray, WafMap, WafObject}; +use tracing::warn; + +mod body; +mod request; +mod response; + +trait IsValid { + fn is_valid(map: &serde_json::Map) -> bool; +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub(crate) enum HttpData { + Request { + raw_uri: Option, + method: Option, + route: Option, + client_ip: Option, + headers: Option>>, + cookies: Option>>, + query: Option>>, + path_params: Option>, + body: Option, + }, + Response { + status_code: Option, + headers: Option>>, + body: Option, + }, +} + +pub(crate) trait ToWafMap { + fn into_waf_map(self) -> WafMap; +} +impl HttpData { + pub(crate) fn into_waf_map(self, extras: Vec<(&str, WafObject)>) -> WafMap { + match self { + HttpData::Request { + client_ip, + raw_uri, + headers: request_headers, + cookies, + query, + path_params, + body, + .. + } => { + let count = [ + client_ip.is_some(), + raw_uri.is_some(), + request_headers.is_some(), + cookies.is_some(), + query.is_some(), + path_params.is_some(), + body.is_some(), + ] + .into_iter() + .filter(|b| *b) + .count(); + let mut map = WafMap::new(count as u64 + extras.len() as u64); + let mut i = 0; + + if let Some(client_ip) = client_ip { + map[i] = ("http.client_ip", client_ip.as_str()).into(); + i += 1; + } + if let Some(raw_uri) = raw_uri { + map[i] = ("server.request.uri.raw", raw_uri.as_str()).into(); + i += 1; + } + if let Some(headers) = request_headers { + map[i] = ("server.request.headers.no_cookies", headers.into_waf_map()).into(); + i += 1; + } + if let Some(cookies) = cookies { + map[i] = ("server.request.cookies", cookies.into_waf_map()).into(); + i += 1; + } + if let Some(query) = query { + map[i] = ("server.request.query", query.into_waf_map()).into(); + i += 1; + } + if let Some(path_params) = path_params { + map[i] = ("server.request.path_params", path_params.into_waf_map()).into(); + i += 1; + } + if let Some(body) = body { + map[i] = ("server.request.body", body).into(); + i += 1; + } + debug_assert_eq!(i, count); // Sanity check that we didn't over-allocate + + map + } + HttpData::Response { + status_code, + headers, + body, + .. + } => { + let count = [status_code.is_some(), headers.is_some(), body.is_some()] + .into_iter() + .filter(|b| *b) + .count(); + let mut map = WafMap::new(count as u64 + extras.len() as u64); + let mut i = 0; + + if let Some(response_status) = status_code { + map[i] = ("server.response.status", response_status).into(); + i += 1; + } + if let Some(headers) = headers { + let mut headers = headers.clone(); + headers.remove("set-cookie"); + map[i] = ("server.response.headers.no_cookies", headers.into_waf_map()).into(); + i += 1; + } + if let Some(response_body) = body { + map[i] = ("server.response.body", response_body).into(); + i += 1; + } + debug_assert_eq!(i, count); // Sanity check that we didn't over-allocate + + let extras_len = extras.len(); + for (k, v) in extras { + map[i] = (k, v).into(); + i += 1; + } + debug_assert_eq!(i, count + extras_len); // Sanity check that we didn't over-allocate + + map + } + } + } +} +impl ToWafMap for HashMap> { + fn into_waf_map(self) -> WafMap { + let mut map = WafMap::new(self.len() as u64); + + for (i, (k, v)) in self.into_iter().enumerate() { + let mut arr = WafArray::new(v.len() as u64); + for (j, v) in v.into_iter().enumerate() { + arr[j] = v.as_str().into(); + } + + map[i] = (k.as_str(), arr).into(); + } + + map + } +} +impl ToWafMap for HashMap { + fn into_waf_map(self) -> WafMap { + let mut map = WafMap::new(self.len() as u64); + + for (i, (k, v)) in self.into_iter().enumerate() { + map[i] = (k.as_str(), v.as_str()).into(); + } + + map + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum RequestType { + APIGatewayV1, // Or Kong + APIGatewayV2Http, + APIGatewayV2Websocket, + APIGatewayLambdaAuthorizerToken, + APIGatewayLambdaAuthorizerRequest, + Alb, + LambdaFunctionUrl, +} + +trait ExtractRequest { + const TYPE: RequestType; + async fn extract(self) -> HttpData; +} +trait ExtractResponse { + async fn extract(self) -> HttpData; +} + +pub(super) async fn extract_request_address_data(body: &Bytes) -> Option<(HttpData, RequestType)> { + let reader = body.clone().reader(); + let data: serde_json::Map = match serde_json::from_reader(reader) { + Ok(data) => data, + Err(e) => { + warn!("Failed to parse request body as JSON: {e}"); + return None; + } + }; + + macro_rules! try_type { + ($ty:ty, unsupported) => { + if <$ty>::is_valid(&data) { + return None; + } + }; + ($ty:ty) => { + if <$ty>::is_valid(&data) { + let Ok(val) = serde_json::from_value::<$ty>(serde_json::Value::Object(data)) else { + return None; + }; + return Some((val.extract().await, <$ty>::TYPE)); + } + }; + } + + // We try a bunch of types in a specific order to reduce the likelihood of incorrectly + // identifying a payload as a different type. The "unsupported" variants are there to further + // reduce the likelihood of us incorrectly identifying an unsupported payload as a supported + // one. + + try_type!(apigw::ApiGatewayProxyRequest); + try_type!(apigw::ApiGatewayV2httpRequest); + try_type!(apigw::ApiGatewayWebsocketProxyRequest); + try_type!(apigw::ApiGatewayCustomAuthorizerRequest); + try_type!(apigw::ApiGatewayCustomAuthorizerRequestTypeRequest); + try_type!(alb::AlbTargetGroupRequest); + // CloudFrontEvent unsupported + try_type!(cloudwatch_events::CloudWatchEvent, unsupported); + try_type!(cloudwatch_logs::LogsEvent, unsupported); + try_type!(dynamodb::Event, unsupported); + try_type!(kinesis::KinesisEvent, unsupported); + try_type!(s3::S3Event, unsupported); + try_type!(sns::SnsEvent, unsupported); + // SqsSnsEvent unsupported + try_type!(sqs::SqsEvent, unsupported); + // AppSyncResolverEvent unsupported // NB: This is GraphQL and maybe could be interesting + try_type!(eventbridge::EventBridgeEvent, unsupported); + try_type!(lambda_function_urls::LambdaFunctionUrlRequest); + // StepFunctionEvent unsupported + // LegacyStepFunctionEvent unsupported + // NestedStepFunctionEvent unsupported + // LegacyNestedStepFunctionEvent unsupported + // LambdaRootStepFunctionPayload unsupported + // LegacyLambdaRootStepFunctionPayload unsupported + try_type!(request::KongAPIGatewayEvent); // IMPORTANT: Must ALWAYS be AFTER all the API Gateway payload types! + + // None of the payloads matched, so we don't have any address data to work with. + None +} + +pub(super) async fn extract_response_address_data( + request_type: RequestType, + body: &Bytes, +) -> Option { + request_type.extract_response_address_data(body).await +} +impl RequestType { + async fn extract_response_address_data(self, body: &Bytes) -> Option { + macro_rules! match_types { + ($($name:ident => $ty:ty),+) => { + match self {$( + RequestType::$name => { + let body: $ty = + match serde_json::from_reader(body.clone().reader()) { + Ok(body) => body, + Err(e) => { + warn!(concat!("appsec: failed to parse response payload from JSON as ", stringify!($ty),": {}"), e); + return None; + } + }; + body.extract().await + } + ),+} + } + } + + Some(match_types! { + APIGatewayV1 => apigw::ApiGatewayProxyResponse, + APIGatewayV2Http => apigw::ApiGatewayV2httpResponse, + APIGatewayV2Websocket => response::Opaque, + APIGatewayLambdaAuthorizerToken => response::Opaque, + APIGatewayLambdaAuthorizerRequest => response::Opaque, + Alb => alb::AlbTargetGroupResponse, + LambdaFunctionUrl => lambda_function_urls::LambdaFunctionUrlResponse + }) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] // Test modules skew coverage metrics +#[cfg(test)] +mod tests { + use super::*; + use bytes::Bytes; + use libddwaf::waf_map; + + #[tokio::test] + async fn test_extract_api_gateway_v1_request() { + let payload = include_str!("../../../tests/payloads/api_gateway_proxy_event.json"); + + let bytes = Bytes::from(payload); + let result = extract_request_address_data(&bytes).await; + + let (http_data, request_type) = result.expect("Expected result to be Some"); + assert_eq!(request_type, RequestType::APIGatewayV1); + + match http_data { + HttpData::Request { + method, + route, + client_ip, + body, + .. + } => { + assert_eq!(method, Some("POST".to_string())); + assert_eq!(route, Some("/{proxy+}".to_string())); + assert_eq!(client_ip, Some("127.0.0.1".to_string())); + assert_eq!(body, Some(waf_map!(("test", "body")).into())); + } + HttpData::Response { .. } => panic!("Expected Request HttpData"), + } + } + + #[tokio::test] + async fn test_extract_api_gateway_v2_http_request() { + let payload = include_str!("../../../tests/payloads/api_gateway_http_event.json"); + + let bytes = Bytes::from(payload); + let result = extract_request_address_data(&bytes).await; + + let (http_data, request_type) = result.expect("Expected result to be Some"); + assert_eq!(request_type, RequestType::APIGatewayV2Http); + + match http_data { + HttpData::Request { + method, + route, + client_ip, + body, + .. + } => { + assert_eq!(method, Some("GET".to_string())); + assert_eq!(route, Some("GET /httpapi/get".to_string())); + assert_eq!(client_ip, Some("38.122.226.210".to_string())); + assert_eq!(body, None); + } + HttpData::Response { .. } => panic!("Expected Request HttpData"), + } + } + + #[tokio::test] + async fn test_extract_api_gateway_websocket_request() { + let payload = + include_str!("../../../tests/payloads/api_gateway_websocket_message_event.json"); + + let bytes = Bytes::from(payload); + let result = extract_request_address_data(&bytes).await; + + let (http_data, request_type) = result.expect("Expected result to be Some"); + assert_eq!(request_type, RequestType::APIGatewayV2Websocket); + + match http_data { + HttpData::Request { client_ip, .. } => { + assert_eq!(client_ip, Some("24.193.182.233".to_string())); + } + HttpData::Response { .. } => panic!("Expected Request HttpData"), + } + } + + #[tokio::test] + async fn test_extract_alb_request() { + let payload = include_str!("../../../tests/payloads/application_load_balancer.json"); + + let bytes = Bytes::from(payload); + let result = extract_request_address_data(&bytes).await; + + let (http_data, request_type) = result.expect("Expected result to be Some"); + assert_eq!(request_type, RequestType::Alb); + + match http_data { + HttpData::Request { + method, + client_ip, + body, + .. + } => { + assert_eq!(method, Some("GET".to_string())); + // ALB implementation doesn't extract client IP from X-Forwarded-For header + assert_eq!(client_ip, None); + assert_eq!(body, None); + } + HttpData::Response { .. } => panic!("Expected Request HttpData"), + } + } + + #[tokio::test] + async fn test_extract_lambda_function_url_request() { + let payload = include_str!("../../../tests/payloads/lambda_function_url_event.json"); + + let bytes = Bytes::from(payload); + let result = extract_request_address_data(&bytes).await; + + let (http_data, request_type) = result.expect("Expected result to be Some"); + assert_eq!(request_type, RequestType::LambdaFunctionUrl); + + match http_data { + HttpData::Request { + method, + client_ip, + body, + .. + } => { + assert_eq!(method, Some("GET".to_string())); + assert_eq!(client_ip, Some("71.195.30.42".to_string())); + assert_eq!(body, None); + } + HttpData::Response { .. } => panic!("Expected Request HttpData"), + } + } + + #[tokio::test] + async fn test_extract_unsupported_sns_event() { + let payload = include_str!("../../../tests/payloads/sns_event.json"); + + let bytes = Bytes::from(payload); + let result = extract_request_address_data(&bytes).await; + + // SNS events are explicitly unsupported + assert!(result.is_none()); + } + + #[tokio::test] + async fn test_extract_unsupported_sqs_event() { + let payload = include_str!("../../../tests/payloads/sqs_event.json"); + + let bytes = Bytes::from(payload); + let result = extract_request_address_data(&bytes).await; + + // SQS events are explicitly unsupported + assert!(result.is_none()); + } + + #[tokio::test] + async fn test_extract_invalid_json() { + let payload = r#"{"invalid": json}"#; + + let bytes = Bytes::from(payload); + let result = extract_request_address_data(&bytes).await; + + assert!(result.is_none()); + } + + #[tokio::test] + async fn test_extract_unrecognized_event_structure() { + let payload = r#"{ "some": "unrecognized", "event": "structure", "that": "doesnt", "match": "any known patterns" }"#; + + let bytes = Bytes::from(payload); + let result = extract_request_address_data(&bytes).await; + + assert!(result.is_none()); + } + + #[tokio::test] + async fn test_extract_empty_payload() { + let payload = "{}"; + + let bytes = Bytes::from(payload); + let result = extract_request_address_data(&bytes).await; + + assert!(result.is_none()); + } + + #[tokio::test] + async fn test_extract_malformed_api_gateway_event() { + let payload = r#"{ + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "headers": { + "Content-Type": "application/json" + }, + "requestContext": { + "stage": "prod" + } + }"#; + + let bytes = Bytes::from(payload); + let result = extract_request_address_data(&bytes).await; + + // This malformed event should not be recognized since it's missing required fields + assert!(result.is_none()); + } + + #[tokio::test] + async fn test_extract_with_base64_encoded_body() { + let payload = r#"{ + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "headers": { + "Content-Type": "application/json" + }, + "multiValueHeaders": { + "Content-Type": ["application/json"] + }, + "requestContext": { + "resourceId": "123456", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "stage": "prod", + "identity": { + "sourceIp": "127.0.0.1" + } + }, + "body": "eyJ0ZXN0IjoiYm9keSJ9", + "isBase64Encoded": true + }"#; + + let bytes = Bytes::from(payload); + let result = extract_request_address_data(&bytes).await; + + let (http_data, request_type) = result.expect("Expected result to be Some"); + assert_eq!(request_type, RequestType::APIGatewayV1); + + match http_data { + HttpData::Request { body, .. } => { + assert_eq!(body, Some(waf_map!(("test", "body")).into())); + } + HttpData::Response { .. } => panic!("Expected Request HttpData"), + } + } + + #[tokio::test] + async fn test_extract_api_gateway_v2_http_response() { + let payload = r#"{ + "statusCode": 200, + "multiValueHeaders": { + "Content-Type": ["application/json"], + "X-Custom-Header": ["custom-value"] + }, + "body": "{\"message\": \"success\", \"data\": \"test\"}", + "isBase64Encoded": false, + "cookies": [] + }"#; + + let bytes = Bytes::from(payload); + let result = extract_response_address_data(RequestType::APIGatewayV2Http, &bytes).await; + + let http_data = result.expect("Expected result to be Some"); + match http_data { + HttpData::Response { + status_code, + headers, + body, + } => { + assert_eq!(status_code, Some(200)); + assert_eq!( + headers, + Some(HashMap::from([ + ( + "content-type".to_string(), + vec!["application/json".to_string()] + ), + ( + "x-custom-header".to_string(), + vec!["custom-value".to_string()] + ), + ])) + ); + assert_eq!( + body, + Some(waf_map!(("message", "success"), ("data", "test")).into()) + ); + } + HttpData::Request { .. } => panic!("Expected Response HttpData"), + } + } + + #[tokio::test] + async fn test_extract_api_gateway_v2_http_response_no_body() { + let payload = r#"{ + "statusCode": 204, + "headers": { "Content-Length": "0" }, + "cookies": [] + }"#; + + let bytes = Bytes::from(payload); + let result = extract_response_address_data(RequestType::APIGatewayV2Http, &bytes).await; + + let http_data = result.expect("Expected result to be Some"); + match http_data { + HttpData::Response { + status_code, + headers, + body, + } => { + assert_eq!(status_code, Some(204)); + assert_eq!( + headers, + Some(HashMap::from([( + "content-length".to_string(), + vec!["0".to_string()] + )])) + ); + assert_eq!(body, None); + } + HttpData::Request { .. } => panic!("Expected Response HttpData"), + } + } + + #[tokio::test] + async fn test_extract_api_gateway_v2_http_response_base64_body() { + let payload = r#"{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "eyJtZXNzYWdlIjogImVuY29kZWQifQ==", + "isBase64Encoded": true, + "cookies": [] + }"#; + + let bytes = Bytes::from(payload); + let result = extract_response_address_data(RequestType::APIGatewayV2Http, &bytes).await; + + let http_data = result.expect("Expected result to be Some"); + match http_data { + HttpData::Response { body, .. } => { + assert_eq!(body, Some(waf_map!(("message", "encoded")).into())); + } + HttpData::Request { .. } => panic!("Expected Response HttpData"), + } + } + + #[tokio::test] + async fn test_extract_api_gateway_websocket_response() { + let payload = r#"{ + "statusCode": 200, + "body": "Message sent" + }"#; + + let bytes = Bytes::from(payload); + let result = + extract_response_address_data(RequestType::APIGatewayV2Websocket, &bytes).await; + + let http_data = result.expect("Expected result to be Some"); + match http_data { + HttpData::Response { + status_code, + headers, + body, + } => { + // Websocket responses use Opaque type, which returns None for all fields + assert_eq!(status_code, None); + assert_eq!(headers, None); + assert_eq!(body, None); + } + HttpData::Request { .. } => panic!("Expected Response HttpData"), + } + } + + #[tokio::test] + async fn test_extract_api_gateway_lambda_authorizer_token_response() { + let payload = r#"{ + "principalId": "user123", + "policyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "execute-api:Invoke", + "Resource": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/users" + } + ] + } + }"#; + + let bytes = Bytes::from(payload); + let result = + extract_response_address_data(RequestType::APIGatewayLambdaAuthorizerToken, &bytes) + .await; + + let http_data = result.expect("Expected result to be Some"); + match http_data { + HttpData::Response { + status_code, + headers, + body, + } => { + // Lambda authorizer responses use Opaque type, which returns None for all fields + assert_eq!(status_code, None); + assert_eq!(headers, None); + assert_eq!(body, None); + } + HttpData::Request { .. } => panic!("Expected Response HttpData"), + } + } + + #[tokio::test] + async fn test_extract_api_gateway_lambda_authorizer_request_response() { + let payload = r#"{ + "principalId": "user123", + "policyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "execute-api:Invoke", + "Resource": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/users" + } + ] + }, + "context": { + "userId": "user123" + } + }"#; + + let bytes = Bytes::from(payload); + let result = + extract_response_address_data(RequestType::APIGatewayLambdaAuthorizerRequest, &bytes) + .await; + + let http_data = result.expect("Expected result to be Some"); + match http_data { + HttpData::Response { + status_code, + headers, + body, + } => { + // Lambda authorizer responses use Opaque type, which returns None for all fields + assert_eq!(status_code, None); + assert_eq!(headers, None); + assert_eq!(body, None); + } + HttpData::Request { .. } => panic!("Expected Response HttpData"), + } + } + + #[tokio::test] + async fn test_extract_alb_target_group_response() { + let payload = r#"{ + "statusCode": 200, + "statusDescription": "200 OK", + "headers": { + "Content-Type": "text/html", + "Set-Cookie": "cookie1=value1; Path=/", + "X-Custom-Header": "custom-value" + }, + "body": "

Hello from ALB!

", + "isBase64Encoded": false + }"#; + + let bytes = Bytes::from(payload); + let result = extract_response_address_data(RequestType::Alb, &bytes).await; + + let http_data = result.expect("Expected result to be Some"); + match http_data { + HttpData::Response { + status_code, + headers, + body, + } => { + assert_eq!(status_code, Some(200)); + assert_eq!( + headers, + Some(HashMap::from([ + ("content-type".to_string(), vec!["text/html".to_string()]), + ( + "x-custom-header".to_string(), + vec!["custom-value".to_string()] + ), + ])) + ); + assert!(body.is_none()); // text/html is not relevant for security + } + HttpData::Request { .. } => panic!("Expected Response HttpData"), + } + } + + #[tokio::test] + async fn test_extract_alb_target_group_response_json_body() { + let payload = r#"{ + "statusCode": 201, + "statusDescription": "201 Created", + "headers": { + "Content-Type": "application/json" + }, + "body": "{\"id\": 123, \"name\": \"test\", \"active\": true}", + "isBase64Encoded": false + }"#; + + let bytes = Bytes::from(payload); + let result = extract_response_address_data(RequestType::Alb, &bytes).await; + + let http_data = result.expect("Expected result to be Some"); + match http_data { + HttpData::Response { + status_code, body, .. + } => { + assert_eq!(status_code, Some(201)); + assert_eq!( + body, + Some(waf_map!(("id", 123u64), ("name", "test"), ("active", true)).into()) + ); + } + HttpData::Request { .. } => panic!("Expected Response HttpData"), + } + } + + #[tokio::test] + async fn test_extract_alb_target_group_response_empty_body() { + let payload = r#"{ + "statusCode": 204, + "statusDescription": "204 No Content", + "headers": { + "Content-Type": "text/plain" + }, + "isBase64Encoded": false + }"#; + + let bytes = Bytes::from(payload); + let result = extract_response_address_data(RequestType::Alb, &bytes).await; + + let http_data = result.expect("Expected result to be Some"); + match http_data { + HttpData::Response { + status_code, body, .. + } => { + assert_eq!(status_code, Some(204)); + assert_eq!(body, None); + } + HttpData::Request { .. } => panic!("Expected Response HttpData"), + } + } + + #[tokio::test] + async fn test_extract_lambda_function_url_response() { + let payload = r#"{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json", + "X-Custom-Header": "custom-value", + "Cache-Control": "no-cache" + }, + "body": "{\"message\": \"Hello from Lambda function URL!\", \"timestamp\": 1234567890}", + "isBase64Encoded": false, + "cookies": [] + }"#; + + let bytes = Bytes::from(payload); + let result = extract_response_address_data(RequestType::LambdaFunctionUrl, &bytes).await; + + let http_data = result.expect("Expected result to be Some"); + match http_data { + HttpData::Response { + status_code, + headers, + body, + } => { + assert_eq!(status_code, Some(200)); + assert_eq!( + headers, + Some(HashMap::from([ + ( + "content-type".to_string(), + vec!["application/json".to_string()] + ), + ( + "x-custom-header".to_string(), + vec!["custom-value".to_string()] + ), + ("cache-control".to_string(), vec!["no-cache".to_string()]), + ])) + ); + assert_eq!( + body, + Some( + waf_map!( + ("message", "Hello from Lambda function URL!"), + ("timestamp", 1_234_567_890u64) + ) + .into() + ) + ); + } + HttpData::Request { .. } => panic!("Expected Response HttpData"), + } + } + + #[tokio::test] + async fn test_extract_lambda_function_url_response_base64_body() { + let payload = r#"{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "eyJzdGF0dXMiOiAib2sifQ==", + "isBase64Encoded": true, + "cookies": [] + }"#; + + let bytes = Bytes::from(payload); + let result = extract_response_address_data(RequestType::LambdaFunctionUrl, &bytes).await; + + let http_data = result.expect("Expected result to be Some"); + match http_data { + HttpData::Response { body, .. } => { + assert_eq!(body, Some(waf_map!(("status", "ok")).into())); + } + HttpData::Request { .. } => panic!("Expected Response HttpData"), + } + } + + #[tokio::test] + async fn test_extract_lambda_function_url_response_error_status() { + let payload = r#"{ + "statusCode": 500, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\"error\": \"Internal Server Error\", \"code\": 500}", + "isBase64Encoded": false, + "cookies": [] + }"#; + + let bytes = Bytes::from(payload); + let result = extract_response_address_data(RequestType::LambdaFunctionUrl, &bytes).await; + + let http_data = result.expect("Expected result to be Some"); + match http_data { + HttpData::Response { + status_code, body, .. + } => { + assert_eq!(status_code, Some(500)); + assert_eq!( + body, + Some(waf_map!(("error", "Internal Server Error"), ("code", 500u64)).into()) + ); + } + HttpData::Request { .. } => panic!("Expected Response HttpData"), + } + } + + #[tokio::test] + async fn test_extract_response_invalid_json() { + let payload = r#"{"invalid": json}"#; + + let bytes = Bytes::from(payload); + let result = extract_response_address_data(RequestType::APIGatewayV2Http, &bytes).await; + + // Should return None for invalid JSON + assert!(result.is_none()); + } + + #[tokio::test] + async fn test_extract_response_malformed_structure() { + let payload = r#"{ + "some": "unrecognized", + "structure": "that doesnt match expected response format" + }"#; + + let bytes = Bytes::from(payload); + let result = extract_response_address_data(RequestType::APIGatewayV2Http, &bytes).await; + + // Should return None for malformed structure + assert!(result.is_none()); + } + + #[tokio::test] + async fn test_extract_opaque_response_shouldnt_cause_failures() { + // Test that opaque payloads (like custom authorizer responses) don't cause failures + let complex_payload = r#"{ + "principalId": "user123", + "policyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "execute-api:Invoke", + "Resource": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/users" + } + ] + }, + "context": { + "userId": "user123", + "department": "engineering", + "permissions": ["read", "write"] + }, + "usageIdentifierKey": "some-key" + }"#; + + let bytes = Bytes::from(complex_payload); + let result = + extract_response_address_data(RequestType::APIGatewayLambdaAuthorizerToken, &bytes) + .await; + + // Should handle complex opaque payloads gracefully + let http_data = result.expect("Expected result to be Some"); + match http_data { + HttpData::Response { + status_code, + headers, + body, + } => { + // Opaque responses should return None for all fields + assert_eq!(status_code, None); + assert_eq!(headers, None); + assert_eq!(body, None); + } + HttpData::Request { .. } => panic!("Expected Response HttpData"), + } + } +} diff --git a/bottlecap/src/appsec/payload/request.rs b/bottlecap/src/appsec/payload/request.rs new file mode 100644 index 000000000..ab7011c80 --- /dev/null +++ b/bottlecap/src/appsec/payload/request.rs @@ -0,0 +1,504 @@ +use super::{ExtractRequest, HttpData, IsValid, RequestType, body::parse_body}; + +use std::collections::HashMap; +use std::collections::hash_map::Entry; + +use aws_lambda_events::{ + alb, apigw, cloudwatch_events, cloudwatch_logs, dynamodb, eventbridge, kinesis, + lambda_function_urls, s3, sns, sqs, +}; + +/// Kong API Gateway events are a subset of [`apigw::ApiGatewayProxyRequest`]. +#[derive(serde::Deserialize)] +pub(super) struct KongAPIGatewayEvent(apigw::ApiGatewayProxyRequest); + +trait RecordSet { + const RECORD_KEY: &'static str; +} +impl IsValid for T { + fn is_valid(map: &serde_json::Map) -> bool { + match map.get("Records") { + Some(serde_json::Value::Array(records)) => records.iter().any(|record| match record { + serde_json::Value::Object(record) => { + matches!( + record.get(Self::RECORD_KEY), + Some(serde_json::Value::Object(_)) + ) + } + _ => false, + }), + _ => false, + } + } +} + +impl IsValid for apigw::ApiGatewayProxyRequest { + fn is_valid(map: &serde_json::Map) -> bool { + let Some(serde_json::Value::Object(request_context)) = map.get("requestContext") else { + return false; + }; + matches!( + request_context.get("stage"), + Some(serde_json::Value::String(_)) + ) && matches!(map.get("httpMethod"), Some(serde_json::Value::String(_))) + && matches!(map.get("resource"), Some(serde_json::Value::String(_))) + && !apigw::ApiGatewayCustomAuthorizerRequestTypeRequest::is_valid(map) + } +} +impl ExtractRequest for apigw::ApiGatewayProxyRequest { + const TYPE: RequestType = RequestType::APIGatewayV1; + async fn extract(self) -> HttpData { + let (headers, cookies) = filter_headers(self.multi_value_headers); + + // Headers are normalized to lowercase by [`filter_headers`]. + let content_type = headers + .get("content-type") + .and_then(|v| v.first()) + .map(String::as_str); + let body = if let Some(body) = self.body { + parse_body(body, self.is_base64_encoded, content_type) + .await + .ok() + .flatten() + } else { + None + }; + + HttpData::Request { + raw_uri: self.path, + method: Some(self.http_method.to_string()), + route: self.resource, + client_ip: self.request_context.identity.source_ip, // API Gateway exposes the Client IP as the Source IP + headers: Some(headers), + cookies, + query: query_to_optional_map(self.query_string_parameters), + path_params: Some(self.path_parameters), + body, + } + } +} +impl IsValid for apigw::ApiGatewayV2httpRequest { + fn is_valid(map: &serde_json::Map) -> bool { + match map.get("version") { + Some(serde_json::Value::String(version)) => { + version == "2.0" + && matches!( + map.get("rawQueryString"), + Some(serde_json::Value::String(_)) + ) + && match map.get("requestContext") { + Some(serde_json::Value::Object(request_context)) => { + if let Some(serde_json::Value::String(domain_name)) = + request_context.get("domainName") + { + !domain_name.contains(".lambda-url.") + } else { + false + } + } + _ => false, + } + } + _ => false, + } + } +} +impl ExtractRequest for apigw::ApiGatewayV2httpRequest { + const TYPE: RequestType = RequestType::APIGatewayV2Http; + async fn extract(self) -> HttpData { + let (headers, cookies) = filter_headers(self.headers); + + let content_type = headers + .get("content-type") + .and_then(|v| v.first()) + .map(String::as_str); + let body = if let Some(body) = self.body { + parse_body(body, self.is_base64_encoded, content_type) + .await + .ok() + .flatten() + } else { + None + }; + + HttpData::Request { + raw_uri: self.raw_path, + route: self.route_key, + method: Some(self.http_method.to_string()), + client_ip: self.request_context.http.source_ip, // API Gateway exposes the Client IP as the Source IP + headers: Some(headers), + cookies, + query: query_to_optional_map(self.query_string_parameters), + path_params: Some(self.path_parameters), + body, + } + } +} +impl IsValid for KongAPIGatewayEvent { + fn is_valid(map: &serde_json::Map) -> bool { + // NB -- This is checked last, so we no longer need to worry about it possibly being a custom authorizer request + matches!(map.get("httpMethod"), Some(serde_json::Value::String(_))) + && matches!(map.get("resource"), Some(serde_json::Value::String(_))) + } +} +impl ExtractRequest for KongAPIGatewayEvent { + const TYPE: RequestType = RequestType::APIGatewayV1; + async fn extract(self) -> HttpData { + self.0.extract().await + } +} +impl IsValid for apigw::ApiGatewayWebsocketProxyRequest { + fn is_valid(map: &serde_json::Map) -> bool { + match map.get("requestContext") { + Some(serde_json::Value::Object(request_context)) => { + matches!( + request_context.get("messageDirection"), + Some(serde_json::Value::String(_)) + ) + } + _ => false, + } + } +} +impl ExtractRequest for apigw::ApiGatewayWebsocketProxyRequest { + const TYPE: RequestType = RequestType::APIGatewayV2Websocket; + async fn extract(self) -> HttpData { + let (headers, cookies) = filter_headers(self.multi_value_headers); + + let content_type = headers + .get("content-type") + .and_then(|v| v.first()) + .map(String::as_str); + let body = if let Some(body) = self.body { + parse_body(body, self.is_base64_encoded, content_type) + .await + .ok() + .flatten() + } else { + None + }; + + HttpData::Request { + raw_uri: self.path, + method: self.http_method.map(|m| m.to_string()), + route: self.resource, + client_ip: self.request_context.identity.source_ip, // API Gateway exposes the Client IP as the Source IP + headers: Some(headers), + cookies, + query: query_to_optional_map(self.multi_value_query_string_parameters), + path_params: Some(self.path_parameters), + body, + } + } +} +impl IsValid for apigw::ApiGatewayCustomAuthorizerRequest { + fn is_valid(map: &serde_json::Map) -> bool { + match map.get("type") { + Some(serde_json::Value::String(t)) => { + t == "TOKEN" + && matches!( + map.get("authorizationToken"), + Some(serde_json::Value::String(_)) + ) + && matches!(map.get("methodArn"), Some(serde_json::Value::String(_))) + } + _ => false, + } + } +} +impl ExtractRequest for apigw::ApiGatewayCustomAuthorizerRequest { + const TYPE: RequestType = RequestType::APIGatewayLambdaAuthorizerToken; + async fn extract(self) -> HttpData { + HttpData::Request { + raw_uri: None, + method: None, + route: None, + client_ip: None, + headers: self.authorization_token.map(|token| { + HashMap::from([("Authorization".to_string(), vec![token.to_string()])]) + }), + cookies: None, + query: None, + path_params: None, + body: None, + } + } +} +impl IsValid for apigw::ApiGatewayCustomAuthorizerRequestTypeRequest { + fn is_valid(map: &serde_json::Map) -> bool { + match map.get("type") { + Some(serde_json::Value::String(t)) => { + t == "REQUEST" + && matches!(map.get("methodArn"), Some(serde_json::Value::String(_))) + && matches!(map.get("headers"), Some(serde_json::Value::Object(_))) + && matches!( + map.get("queryStringParameters"), + Some(serde_json::Value::Object(_)) + ) + && match map.get("requestContext") { + Some(serde_json::Value::Object(request_context)) => { + matches!( + request_context.get("apiId"), + Some(serde_json::Value::String(_)) + ) + } + _ => false, + } + } + _ => false, + } + } +} +impl ExtractRequest for apigw::ApiGatewayCustomAuthorizerRequestTypeRequest { + const TYPE: RequestType = RequestType::APIGatewayLambdaAuthorizerRequest; + async fn extract(self) -> HttpData { + let source_ip = self.request_context.identity.and_then(|i| i.source_ip); + + let (headers, cookies) = filter_headers(self.headers); + + HttpData::Request { + raw_uri: self.path, + method: self.http_method.map(|m| m.to_string()), + route: self.resource, + client_ip: source_ip, + headers: Some(headers), + cookies, + query: query_to_optional_map(self.multi_value_query_string_parameters), + path_params: Some(self.path_parameters), + body: None, + } + } +} +impl IsValid for alb::AlbTargetGroupRequest { + fn is_valid(map: &serde_json::Map) -> bool { + match map.get("requestContext") { + Some(serde_json::Value::Object(request_context)) => { + matches!( + request_context.get("elb"), + Some(serde_json::Value::Object(_)) + ) + } + _ => false, + } + } +} +impl ExtractRequest for alb::AlbTargetGroupRequest { + const TYPE: RequestType = RequestType::Alb; + async fn extract(self) -> HttpData { + // Based on configuration, ALB provides headers EITHER in multi-value form OR in single-value form, never both. + let (headers, cookies) = filter_headers(if self.multi_value_headers.is_empty() { + self.headers + } else { + self.multi_value_headers + }); + + let query = if self.multi_value_query_string_parameters.is_empty() { + query_to_optional_map(self.query_string_parameters) + } else { + query_to_optional_map(self.multi_value_query_string_parameters) + }; + + let content_type = headers + .get("content-type") + .and_then(|v| v.first()) + .map(String::as_str); + let body = if let Some(body) = self.body { + parse_body(body, self.is_base64_encoded, content_type) + .await + .ok() + .flatten() + } else { + None + }; + + HttpData::Request { + raw_uri: self.path, + method: Some(self.http_method.to_string()), + route: None, + client_ip: None, + headers: Some(headers), + cookies, + query, + path_params: None, + body, + } + } +} +impl IsValid for cloudwatch_events::CloudWatchEvent { + fn is_valid(map: &serde_json::Map) -> bool { + match map.get("source") { + Some(serde_json::Value::String(source)) => source == "aws.events", + _ => false, + } + } +} +impl IsValid for cloudwatch_logs::LogsEvent { + fn is_valid(map: &serde_json::Map) -> bool { + matches!(map.get("awslogs"), Some(serde_json::Value::Object(_))) + } +} +// TODO: CloudFrontRequestEvent +impl RecordSet for dynamodb::Event { + const RECORD_KEY: &'static str = "dynamodb"; +} +impl RecordSet for kinesis::KinesisEvent { + const RECORD_KEY: &'static str = "kinesis"; +} +impl RecordSet for s3::S3Event { + const RECORD_KEY: &'static str = "s3"; +} +impl RecordSet for sns::SnsEvent { + const RECORD_KEY: &'static str = "sns"; +} +impl IsValid for sqs::SqsEvent { + fn is_valid(map: &serde_json::Map) -> bool { + match map.get("Records") { + Some(serde_json::Value::Array(records)) => records.iter().any(|record| match record { + serde_json::Value::Object(record) => match record.get("eventSource") { + Some(serde_json::Value::String(source)) => source == "aws:sqs", + _ => false, + }, + _ => false, + }), + _ => false, + } + } +} +// TODO:: SQSSNSEvent +// TODO:: AppSyncResolverEvent +impl IsValid for eventbridge::EventBridgeEvent { + fn is_valid(map: &serde_json::Map) -> bool { + matches!(map.get("detail-type"), Some(serde_json::Value::String(_))) + && match map.get("source") { + Some(serde_json::Value::String(source)) => source != "aws.events", + _ => false, + } + } +} +impl IsValid for lambda_function_urls::LambdaFunctionUrlRequest { + fn is_valid(map: &serde_json::Map) -> bool { + match map.get("requestContext") { + Some(serde_json::Value::Object(request_context)) => { + match request_context.get("domainName") { + Some(serde_json::Value::String(domain_name)) => { + domain_name.contains(".lambda-url.") + } + _ => false, + } + } + _ => false, + } + } +} +impl ExtractRequest for lambda_function_urls::LambdaFunctionUrlRequest { + const TYPE: RequestType = RequestType::LambdaFunctionUrl; + async fn extract(self) -> HttpData { + let (headers, cookies) = filter_headers(self.headers); + + let content_type = headers + .get("content-type") + .and_then(|v| v.first()) + .map(String::as_str); + let body = if let Some(body) = self.body { + parse_body(body, self.is_base64_encoded, content_type) + .await + .ok() + .flatten() + } else { + None + }; + + HttpData::Request { + raw_uri: self.raw_path, + method: self.request_context.http.method, + route: None, + client_ip: self.request_context.http.source_ip, + headers: Some(headers), + cookies, + query: to_optional_multimap(self.query_string_parameters), + path_params: None, + body, + } + } +} + +#[allow(clippy::type_complexity)] // Come on! +fn filter_headers( + headers: aws_lambda_events::http::HeaderMap, +) -> ( + HashMap>, + Option>>, +) { + let mut filtered_headers = HashMap::with_capacity(headers.keys_len()); + let mut parsed_cookies = HashMap::new(); + + for hdr in headers.keys() { + let hdr = hdr.as_str(); + let val = headers.get_all(hdr).iter().filter_map(|v| v.to_str().ok()); + if hdr.eq_ignore_ascii_case("cookie") { + for val in val { + let cookies = axum_extra::extract::cookie::Cookie::split_parse_encoded(val); + for cookie in cookies { + let Ok(cookie) = cookie else { + continue; + }; + match parsed_cookies.entry(cookie.name().to_string()) { + Entry::Vacant(entry) => { + entry.insert(vec![cookie.value().to_string()]); + } + Entry::Occupied(entry) => { + entry.into_mut().push(cookie.value().to_string()); + } + } + } + } + continue; + } + filtered_headers.insert(hdr.to_lowercase(), val.map(str::to_string).collect()); + } + + ( + filtered_headers, + if parsed_cookies.is_empty() { + None + } else { + Some(parsed_cookies) + }, + ) +} + +fn query_to_optional_map( + query: aws_lambda_events::query_map::QueryMap, +) -> Option>> { + if query.is_empty() { + return None; + } + + let iter = query.iter(); + let (lower, upper) = iter.size_hint(); + let mut query = HashMap::with_capacity(upper.unwrap_or(lower)); + for (k, v) in iter { + match query.entry(k.to_string()) { + Entry::Vacant(entry) => { + entry.insert(vec![v.to_string()]); + } + Entry::Occupied(entry) => { + entry.into_mut().push(v.to_string()); + } + } + } + Some(query) +} + +fn to_optional_multimap(map: HashMap) -> Option>> { + if map.is_empty() { + return None; + } + let mut multimap = HashMap::with_capacity(map.len()); + + for (k, v) in map { + multimap.insert(k, vec![v]); + } + + Some(multimap) +} diff --git a/bottlecap/src/appsec/payload/response.rs b/bottlecap/src/appsec/payload/response.rs new file mode 100644 index 000000000..a6fb472f4 --- /dev/null +++ b/bottlecap/src/appsec/payload/response.rs @@ -0,0 +1,187 @@ +use std::collections::HashMap; + +use super::{ExtractResponse, HttpData, body::parse_body}; + +use aws_lambda_events::{alb, apigw, lambda_function_urls}; +use tracing::warn; + +impl ExtractResponse for apigw::ApiGatewayProxyResponse { + async fn extract(self) -> HttpData { + let body = if let Some(body) = self.body { + match parse_body( + body, + self.is_base64_encoded, + self.headers + .get("content-type") + .and_then(|v| v.to_str().ok()), + ) + .await + { + Ok(body) => body, + Err(e) => { + warn!("appsec: failed to parse response body: {e}"); + None + } + } + } else { + None + }; + + HttpData::Response { + status_code: Some(self.status_code), + headers: normalize_headers(self.multi_value_headers, self.headers), + body, + } + } +} + +impl ExtractResponse for apigw::ApiGatewayV2httpResponse { + async fn extract(self) -> HttpData { + let body = if let Some(body) = self.body { + match parse_body( + body, + self.is_base64_encoded, + self.headers + .get("content-type") + .and_then(|v| v.to_str().ok()), + ) + .await + { + Ok(body) => body, + Err(e) => { + warn!("appsec: failed to parse response body: {e}"); + None + } + } + } else { + None + }; + + HttpData::Response { + status_code: Some(self.status_code), + headers: normalize_headers(self.multi_value_headers, self.headers), + body, + } + } +} + +/// Used for response payloads from which no data can be readily extracted. +#[derive(serde::Deserialize)] +#[repr(transparent)] +pub(super) struct Opaque(serde_json::Value); +impl ExtractResponse for Opaque { + async fn extract(self) -> HttpData { + HttpData::Response { + status_code: None, + headers: None, + body: None, + } + } +} + +impl ExtractResponse for alb::AlbTargetGroupResponse { + async fn extract(self) -> HttpData { + let body = if let Some(body) = self.body { + match parse_body( + body, + self.is_base64_encoded, + self.headers + .get("content-type") + .and_then(|v| v.to_str().ok()), + ) + .await + { + Ok(body) => body, + Err(e) => { + warn!("appsec: failed to parse response body: {e}"); + None + } + } + } else { + None + }; + + HttpData::Response { + status_code: Some(self.status_code), + headers: normalize_headers(self.multi_value_headers, self.headers), + body, + } + } +} + +impl ExtractResponse for lambda_function_urls::LambdaFunctionUrlResponse { + async fn extract(self) -> HttpData { + let body = if let Some(body) = self.body { + match parse_body( + body, + self.is_base64_encoded, + self.headers + .get("content-type") + .and_then(|v| v.to_str().ok()), + ) + .await + { + Ok(body) => body, + Err(e) => { + warn!("appsec: failed to parse response body: {e}"); + None + } + } + } else { + None + }; + + HttpData::Response { + status_code: Some(self.status_code), + headers: normalize_headers(aws_lambda_events::http::HeaderMap::default(), self.headers), + body, + } + } +} + +/// Converts a header multimap + single-map into a normalized multi-map where all header names are +/// normalized to the lower case form. It also removes the `Set-Cookie` header, which should not be +/// carried any further due to its potential for including sensitive information. +pub(super) fn normalize_headers( + multi_value_headers: aws_lambda_events::http::HeaderMap, + headers: aws_lambda_events::http::HeaderMap, +) -> Option>> { + if multi_value_headers.is_empty() && headers.is_empty() { + return None; + } + + let mut normalized = + HashMap::with_capacity(multi_value_headers.keys_len() + headers.keys_len()); + + for key in multi_value_headers.keys() { + let key = key.as_str(); + if key.eq_ignore_ascii_case("set-cookie") { + continue; + } + + normalized.insert( + key.to_lowercase(), + multi_value_headers + .get_all(key) + .iter() + .filter_map(|v| v.to_str().ok()) + .map(str::to_string) + .collect(), + ); + } + + for key in headers.keys() { + let key = key.as_str(); + if key.eq_ignore_ascii_case("set-cookie") { + continue; + } + let Some(value) = headers.get(key).and_then(|v| v.to_str().ok()) else { + continue; + }; + normalized + .entry(key.to_lowercase()) + .or_insert_with(move || vec![value.to_string()]); + } + + Some(normalized) +} diff --git a/bottlecap/src/appsec/processor.rs b/bottlecap/src/appsec/processor.rs new file mode 100644 index 000000000..7609a31a0 --- /dev/null +++ b/bottlecap/src/appsec/processor.rs @@ -0,0 +1,1361 @@ +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use crate::appsec::sampler::Sampler; +use crate::appsec::{is_enabled, is_standalone, payload}; +use crate::config::Config; + +use bytes::Bytes; +use libddwaf::object::{WafMap, WafObject, WafOwned}; +use libddwaf::{Builder, Config as WAFConfig, Context, Handle, RunResult, waf_map}; +use tracing::{debug, info, warn}; +/// The App & API Protection processor. +/// +/// It is used to try to identify invoke requests that are supported, extract the relevant data from +/// the request payload, and evaluate in-app WAF rules against that data. +pub struct Processor { + handle: Handle, + waf_timeout: Duration, + api_sec_sampler: Mutex, +} +impl Processor { + /// Creates a new [`Processor`] instance using the provided [`Config`]. + /// + /// # Errors + /// - If [`Config::serverless_appsec_enabled`] is `false`; + /// - If the [`Config::appsec_rules`] points to a non-existent file; + /// - If the [`Config::appsec_rules`] points to a file that is not a valid JSON-encoded ruleset; + /// - If the in-app WAF fails to initialize, integrate the ruleset, or build the WAF instance. + pub fn new(config: &Config) -> Result { + if !is_enabled(config) { + return Err(Error::FeatureDisabled); + } + debug!("Starting ASM processor"); + + if is_standalone() { + info!( + "Starting ASM in standalone mode. APM tracing will be disabled for this service." + ); + } + + let Some(mut builder) = Builder::new(&WAFConfig::default()) else { + return Err(Error::BuilderCreationFailed); + }; + + let rules = Self::get_rules(config)?; + let mut diagnostics = WafOwned::::default(); + if !builder.add_or_update_config("rules", &rules, Some(&mut diagnostics)) { + return Err(Error::RulesetAdditionFailed(diagnostics)); + } + + let Some(handle) = builder.build() else { + return Err(Error::WafCreationFailed); + }; + + if let Some(version) = diagnostics.get(b"ruleset_version").and_then(|o| o.to_str()) { + debug!("appsec: loaded ruleset vesion: {version}"); + } + + Ok(Self { + handle, + waf_timeout: config.appsec_waf_timeout, + api_sec_sampler: Mutex::new(Sampler::with_interval(config.api_security_sample_delay)), + }) + } + + /// Process the `/runtime/invocation/next` payload, which is sent to Lambda to request an + /// invocation event. + #[must_use] + pub async fn process_invocation_next(&self, body: &Bytes) -> Option { + let (address_data, request_type) = payload::extract_request_address_data(body).await?; + + let mut context = AppSecContext { + request_type, + waf_context: Arc::new(Mutex::new(self.handle.new_context())), + waf_timeout: self.waf_timeout, + waf_duration: Duration::ZERO, + timeouts: 0, + keep: false, + attributes: HashMap::new(), + tags_always: HashMap::new(), + tags_on_event: HashMap::new(), + events: Vec::new(), + }; + + context + .tags_always + .insert("_dd.origin".to_string(), "appsec".to_string()); + + context.absorb_data( + address_data, + None, // API Security samplingdecision is taken when the response is processed + ); + Some(context) + } + + /// Process the `/runtime/invocation//response>` payload, which is sent to Lambda + /// after the invocation has run to completion, to provide the result of the invocation. + pub async fn process_invocation_response(&self, context: &mut AppSecContext, body: &Bytes) { + let address_data = payload::extract_response_address_data(context.request_type, body) + .await + .unwrap_or(payload::HttpData::Response { + status_code: None, + headers: None, + body: None, + }); + + let mut api_sec_sampler = self + .api_sec_sampler + .lock() + .inspect_err(|e| warn!("appsec: API security sampler mutex was poisoned: {e}")) + .ok(); + context.absorb_data(address_data, api_sec_sampler.as_deref_mut()); + } + + /// Parses the App & API Protection ruleset from the provided [Config], falling back to the + /// default built-in ruleset if the [Config] has [None]. + fn get_rules(config: &Config) -> Result { + // Default on recommended rules + match &config.appsec_rules { + None => { + let default_rules = include_bytes!("rules.json"); + Ok(serde_json::from_slice(default_rules).map_err(Error::RulesetParseError)?) + } + Some(path) => { + let rules = std::fs::File::open(path).map_err(|e| Error::RulesetFileError { + path: path.clone(), + cause: e, + })?; + Ok(serde_json::from_reader(rules).map_err(Error::RulesetParseError)?) + } + } + } +} +impl std::fmt::Debug for Processor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct(stringify!(Processor)) + .field("waf_timeout", &self.waf_timeout) + .finish_non_exhaustive() + } +} + +/// Errors that can occur when calling [`Processor::new`]. +#[derive(Debug)] +pub enum Error { + FeatureDisabled, + BuilderCreationFailed, + RulesetFileError { path: String, cause: std::io::Error }, + RulesetParseError(serde_json::Error), + RulesetAdditionFailed(libddwaf::object::WafOwned), + WafCreationFailed, +} +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::FeatureDisabled => write!(f, "appsec: feature disabled"), + Self::BuilderCreationFailed => write!(f, "appsec: failed to create WAF builder"), + Self::RulesetFileError { path, cause } => { + write!(f, "appsec: failed to open ruleset file {path:#}: {cause}") + } + Self::RulesetParseError(e) => write!(f, "appsec: failed to parse ruleset: {e}"), + Self::RulesetAdditionFailed(diags) => { + write!( + f, + "appsec: failed to add ruleset to the WAF builder: {diags:?}" + ) + } + Self::WafCreationFailed => write!(f, "appsec: failed to build WAF instance"), + } + } +} +impl std::error::Error for Error {} + +/// Request headers that are always collected as long as AAP is enabled. +/// +/// This list should contain lowercase-normalized header names. +/// +/// See: . +const REQUEST_HEADERS_ON_EVENT: &[&str] = &[ + // IP address releated headers + "x-forwarded-for", + "x-real-ip", + "true-client-ip", + "x-client-ip", + "x-forwarded", + "forwarded-for", + "x-cluster-client-ip", + "fastly-client-ip", + "cf-connecting-ip", + "cf-connecting-ipv6", + "forwarded", + "via", + // Message body information + "content-length", + "content-encoding", + "content-language", + // Host request context + "host", + // Content negotiation + "accept-encoding", + "accept-language", +]; +/// Request headers that are collected only if AAP is enabled, and security activity has been +/// detected. +/// +/// This list should contain lowercase-normalized header names. +/// +/// See: . +const REQUEST_HEADERS_ALWAYS: &[&str] = &[ + // Message body information + "content-type", + // Client user agent + "user-agent", + // Content negotiation + "accept", + // AWS WAF logs to traces (RFC 0996) + "x-amzn-trace-id", + // WAF Integration - Identify Requests (RFC 0992) + "cloudfront-viewer-ja3-fingerprint", + "cf-ray", + "x-cloud-trace-context", + "x-appgw-trace-id", + "x-sigsci-requestid", + "x-sigsci-tags", + "akamai-user-risk", +]; +/// Response headers that are always collected as long as AAP is enabled. +/// +/// See: . +const RESPONSE_HEADERS_ALWAYS: &[&str] = &[ + // Message body information + "content-length", + "content-type", + "content-encoding", + "content-language", +]; + +/// The WAF context for a single invocation. +/// +/// This is used to process both the request & response of a given invocation. +#[derive(Clone)] +pub struct AppSecContext { + request_type: payload::RequestType, + // This must be clone-able due to how the request contexts are handled, so we have to wrap the + // WAF context in an Arc-Mutex. + waf_context: Arc>, + waf_timeout: Duration, + pub waf_duration: Duration, + pub timeouts: u32, + pub keep: bool, + attributes: HashMap, + /// The trace tags that are added to the trace unconditionally. + tags_always: HashMap, + /// The trace tags that are added to the trace ONLY if there is a security event. + tags_on_event: HashMap, + pub events: Vec, +} +impl AppSecContext { + /// Returns the list of trace tags to add to the trace. + pub fn tags(&self) -> impl Iterator { + let next: Box> = if self.events.is_empty() { + Box::new(std::iter::empty()) + } else { + Box::new(self.tags_on_event.iter()) + }; + self.attributes + .iter() + .chain(self.tags_always.iter()) + .chain(next) + .map(|(k, v)| (k.as_str(), v.as_str())) + } + + /// Evaluates the appsec rules against the provided request data, and creates any relevant + /// attributes from it. + fn absorb_data( + &mut self, + address_data: payload::HttpData, + api_sec_sampler: Option<&mut Sampler>, + ) { + let timeout = self.remaining_time_budget(); + if timeout == Duration::ZERO { + warn!( + "appsec: WAF timeout already reached ({waf:?}), not evaluating request with {address_data:?}", + waf = &self.waf_duration, + ); + return; + } + + let recording_start = Instant::now(); + self.record_tags(&address_data); + let recording_duration = recording_start.elapsed(); + debug!("appsec: processing tags took {recording_duration:?}"); + + let extras = if let Some(sampler) = api_sec_sampler { + let method = self + .tags_always + .get("http.method") + .map_or("", String::as_str); + let route = self + .tags_on_event + .get("http.route") + .or_else(|| self.tags_always.get("http.url")) + .map_or("", String::as_str); + let status_code = self + .tags_always + .get("http.status_code") + .map_or("", String::as_str); + + if sampler.decision_for(method, route, status_code) { + vec![( + "waf.context.processor", + waf_map!(("extract-schema", true)).into(), + )] + } else { + vec![] + } + } else { + vec![] + }; + + let encoding_start = Instant::now(); + let address_data = address_data.into_waf_map(extras); + let encoding_duration = encoding_start.elapsed(); + debug!("appsec: encoding address data took {encoding_duration:?}"); + + self.run(address_data, timeout); + } + + /// Records the tags from the provided [`payload::HttpData`] into the [`Self::tags_always`] and + /// [`Self::tags_on_event`] maps. + fn record_tags(&mut self, address_data: &payload::HttpData) { + match address_data { + payload::HttpData::Request { + raw_uri, + method, + route, + client_ip, + headers, + .. + } => { + if let Some(uri) = raw_uri { + self.tags_always + .entry("http.url".to_string()) + .or_insert(uri.clone()); + } + if let Some(method) = method { + self.tags_always + .entry("http.method".to_string()) + .or_insert(method.clone()); + } + if let Some(route) = route { + self.tags_on_event + .entry("http.route".to_string()) + .or_insert(route.clone()); + } + if let Some(headers) = headers { + for name in REQUEST_HEADERS_ALWAYS { + let Some(values) = headers.get(*name) else { + continue; + }; + self.tags_always + .entry(format!("http.request.headers.{name}")) + .or_insert(values.join(",")); + } + for name in REQUEST_HEADERS_ON_EVENT { + let Some(values) = headers.get(*name) else { + continue; + }; + self.tags_on_event + .entry(format!("http.request.headers.{name}")) + .or_insert(values.join(",")); + } + } + if let Some(client_ip) = client_ip { + self.tags_on_event + .entry("network.client.ip".to_string()) + .or_insert(client_ip.clone()); + } + } + payload::HttpData::Response { + status_code, + headers, + .. + } => { + if let Some(status_code) = status_code { + self.tags_always + .entry("http.status_code".to_string()) + .or_insert(status_code.to_string()); + } + if let Some(headers) = headers { + for name in RESPONSE_HEADERS_ALWAYS { + let Some(values) = headers.get(*name) else { + continue; + }; + self.tags_always + .entry(format!("http.response.headers.{name}")) + .or_insert(values.join(",")); + } + } + } + } + } + + /// Evaluates the in-app WAF rules against the provided address data. + fn run(&mut self, address_data: WafMap, timeout: Duration) { + let mut waf_context = match self.waf_context.lock() { + Ok(waf_context) => waf_context, + Err(e) => { + warn!("appsec: failed to lock WAF context: {e}"); + return; + } + }; + let result = match waf_context.run(Some(address_data), None, timeout) { + Ok(RunResult::Match(result) | RunResult::NoMatch(result)) => result, + Err(e) => { + warn!("appsec: failed to evalute in-app WAF rules against request: {e}"); + return; + } + }; + + self.waf_duration += result.duration(); + if result.timeout() { + self.timeouts += 1; + } + if result.keep() { + self.keep = true; + } + if let Some(attributes) = result.attributes() { + self.attributes.reserve(attributes.len()); + for attr in attributes.iter() { + let Ok(key) = attr.key_str() else { continue }; + let value = if let Some(value) = attr.to_str() { + value.to_string() + } else if let Some(value) = attr.to_u64() { + value.to_string() + } else if let Some(value) = attr.to_i64() { + value.to_string() + } else if let Some(value) = attr.to_f64() { + value.to_string() + } else if let Some(value) = attr.to_bool() { + value.to_string() + } else { + let attr: &WafObject = attr; // Forcing deref type conversion + match serde_json::to_string(attr) { + Ok(value) => value, + Err(e) => { + warn!("appsec: unable to encode WAF attribute to JSON: {e}\n{attr:?}"); + continue; + } + } + }; + self.attributes.insert(key.to_string(), value); + } + } + if let Some(events) = result.events() { + self.events.reserve(events.len()); + for event in events.iter() { + let enc = match serde_json::to_string(event) { + Ok(enc) => enc, + Err(e) => { + warn!("appsec: unable to encode WAF event: {e}\n{event:?}"); + continue; + } + }; + self.events.push(enc); + } + } + } + + /// Returns the time remaining to process WAF rules. + const fn remaining_time_budget(&self) -> Duration { + self.waf_timeout.saturating_add(self.waf_duration) + } +} +impl std::fmt::Debug for AppSecContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct(stringify!(AppSecContext)) + .field("request_type", &self.request_type) + .field("waf_timeout", &self.waf_timeout) + .field("duration", &self.waf_duration) + .field("timeouts", &self.timeouts) + .field("keep", &self.keep) + .field("attributes", &self.attributes) + .field("events", &self.events) + .finish_non_exhaustive() + } +} +impl std::cmp::PartialEq for AppSecContext { + fn eq(&self, other: &Self) -> bool { + self.request_type == other.request_type + && self.waf_timeout == other.waf_timeout + && self.waf_duration == other.waf_duration + && self.timeouts == other.timeouts + && self.keep == other.keep + && self.attributes == other.attributes + && self.events == other.events + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] // Test modules skew coverage metrics +#[cfg(test)] +mod tests { + use std::io::Write; + + use crate::config::Config; + + use super::*; + + use serde_json::json; + + #[test] + fn test_new_with_default_config() { + let config = Config { + serverless_appsec_enabled: true, + ..Config::default() + }; + let _ = Processor::new(&config).expect("Should not fail"); + } + + #[test] + fn test_new_disabled() { + let config = Config { + serverless_appsec_enabled: false, // Explicitly testing this condition + ..Config::default() + }; + assert!(matches!( + Processor::new(&config), + Err(Error::FeatureDisabled) + )); + } + + #[test] + fn test_new_with_invalid_config() { + let tmp = tempfile::NamedTempFile::new().expect("Failed to create tempfile"); + + let config = Config { + serverless_appsec_enabled: true, + appsec_rules: Some( + tmp.path() + .to_str() + .expect("Failed to get tempfile path") + .to_string(), + ), + ..Config::default() + }; + assert!(matches!( + Processor::new(&config), + Err(Error::RulesetParseError(_)) + )); + } + + #[test] + fn test_new_with_no_rules_or_processors() { + let mut tmp = tempfile::NamedTempFile::new().expect("Failed to create tempfile"); + tmp.write_all( + br#"{ + "version": "2.2", + "metadata":{ + "ruleset_version": "0.0.0-blank" + }, + "scanners":[{ + "id": "406f8606-52c4-4663-8db9-df70f9e8766c", + "name": "ZIP Code", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:zip|postal)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "^[0-9]{5}(?:-[0-9]{4})?$", + "options": { + "case_sensitive": true, + "min_length": 5 + } + } + }, + "tags": { + "type": "zipcode", + "category": "address" + } + }] + }"#, + ) + .expect("Failed to write to temp file"); + tmp.flush().expect("Failed to flush temp file"); + + let config = Config { + serverless_appsec_enabled: true, + appsec_rules: Some( + tmp.path() + .to_str() + .expect("Failed to get tempfile path") + .to_string(), + ), + ..Config::default() + }; + let result = Processor::new(&config); + assert!( + matches!( + result, + Err(Error::WafCreationFailed), // There is no rule nor processor in the ruleset + ), + concat!( + "should have failed with ", + stringify!(Error::WafCreationFailed), + " but was {:?}" + ), + result + ); + } + + #[test] + fn test_new_with_inexistent_ruleset_file() { + let config = Config { + serverless_appsec_enabled: true, + appsec_rules: Some("/definitely/not/a/file/that/exists".to_string()), + ..Config::default() + }; + assert!(matches!( + Processor::new(&config), + Err(Error::RulesetFileError { .. }) + )); + } + + #[tokio::test] + async fn test_new_timed_out() { + let config = Config { + serverless_appsec_enabled: true, + appsec_waf_timeout: Duration::ZERO, // Immediate timeout! + ..Config::default() + }; + let processor = Processor::new(&config).expect("Should not fail"); + let context = processor + .process_invocation_next(&Bytes::from(include_str!( + "../../tests/payloads/api_gateway_proxy_event.json" + ))) + .await + .expect("context should be Some"); + let tags = context.tags().collect::>(); + assert_eq!(tags, HashMap::from([("_dd.origin", "appsec")])); + } + + #[tokio::test] + async fn test_process_invocation_next_with_api_gateway_v1() { + let config = Config { + serverless_appsec_enabled: true, + appsec_waf_timeout: Duration::from_secs(3600), // Avoids falkes on slower CI hardware + ..Config::default() + }; + let processor = Processor::new(&config).expect("Should not fail"); + + let context = processor + .process_invocation_next(&Bytes::from(include_str!( + "../../tests/payloads/api_gateway_proxy_event.appsec_event.json" + ))) + .await + .expect("an AppSec context should have been created"); + assert_eq!(context.request_type, payload::RequestType::APIGatewayV1); + // Duration will be greater than zero due to WAF processing + assert!(context.waf_duration > Duration::ZERO); + assert_eq!(context.timeouts, 0); + assert!(context.keep); + assert!(!context.events.is_empty(), "should have at least one event"); + assert!( + context.events.iter().any(|e| e.contains("Arachni/v2")), + "at least one of the events should mention Arachni/v2" + ); + assert_eq!( + context.tags().collect::>(), + HashMap::from([ + // Fingerprints added by the WAF + ( + "_dd.appsec.fp.http.header", + "hdr-0010100011-40b52535-12-d7bf5e5b" + ), + ("_dd.appsec.fp.http.network", "net-2-1000000000"), + // Origin tag + ("_dd.origin", "appsec"), + // Extracted HTTP request information (complete, there is a security event here) + ("http.method", "POST"), + ( + "http.request.headers.accept-encoding", + "gzip, deflate, sdch" + ), + ("http.request.headers.accept-language", "en-US,en;q=0.8"), + ( + "http.request.headers.accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" + ), + ( + "http.request.headers.host", + "0123456789.execute-api.us-east-1.amazonaws.com" + ), + ("http.request.headers.user-agent", "Arachni/v2"), + ( + "http.request.headers.via", + "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" + ), + ( + "http.request.headers.x-forwarded-for", + "127.0.0.1, 127.0.0.2" + ), + ("http.route", "/{proxy+}"), + ("http.url", "/path/to/resource"), + ("network.client.ip", "127.0.0.1"), + ]) + ); + } + + #[tokio::test] + async fn test_process_invocation_next_with_api_gateway_v2() { + let config = Config { + serverless_appsec_enabled: true, + ..Config::default() + }; + let processor = Processor::new(&config).expect("Should not fail"); + + let context = processor + .process_invocation_next(&Bytes::from(include_str!( + "../../tests/payloads/api_gateway_http_event.json" + ))) + .await + .expect("an AppSec context should have been created"); + assert_eq!(context.request_type, payload::RequestType::APIGatewayV2Http); + assert!( + context.events.is_empty(), + "should not have produced any AppSec event" + ); + assert_eq!( + context.tags_always.get("_dd.origin"), + Some(&"appsec".to_string()) + ); + } + + #[tokio::test] + async fn test_process_invocation_next_with_unsupported_payload() { + let config = Config { + serverless_appsec_enabled: true, + ..Config::default() + }; + let processor = Processor::new(&config).expect("Should not fail"); + + let context = processor + .process_invocation_next(&Bytes::from(include_str!( + "../../tests/payloads/sns_event.json" + ))) + .await; + + // SNS events are not supported, so should return None + assert!(context.is_none()); + } + + #[tokio::test] + async fn test_process_invocation_next_with_invalid_json() { + let config = Config { + serverless_appsec_enabled: true, + ..Config::default() + }; + let processor = Processor::new(&config).expect("Should not fail"); + + let payload = r#"{"invalid": json}"#; + + let bytes = Bytes::from(payload); + let context = processor.process_invocation_next(&bytes).await; + + // Invalid JSON should return None + assert!(context.is_none()); + } + + #[tokio::test] + async fn test_process_invocation_next_with_empty_payload() { + let config = Config { + serverless_appsec_enabled: true, + ..Config::default() + }; + let processor = Processor::new(&config).expect("Should not fail"); + + let payload = ""; + + let bytes = Bytes::from(payload); + let context = processor.process_invocation_next(&bytes).await; + + // Empty payload should return None + assert!(context.is_none()); + } + + #[tokio::test] + async fn test_process_invocation_response_with_api_gateway_v1() { + let config = Config { + serverless_appsec_enabled: true, + appsec_waf_timeout: Duration::from_secs(3600), // Avoids falkes on slower CI hardware + ..Config::default() + }; + let processor = Processor::new(&config).expect("Should not fail"); + + // First create a context with a request + let mut context = processor + .process_invocation_next(&Bytes::from(include_str!( + "../../tests/payloads/api_gateway_proxy_event.json" + ))) + .await + .expect("Should create context"); + + // Now test the response processing + let response_payload = r#"{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\"response\":\"success\"}", + "isBase64Encoded": false + }"#; + + let response_bytes = Bytes::from(response_payload); + + // This should not panic and should complete successfully + processor + .process_invocation_response(&mut context, &response_bytes) + .await; + + // Verify context state after response processing (unchanged) + assert_eq!(context.request_type, payload::RequestType::APIGatewayV1); + assert_eq!(context.timeouts, 0); + assert!(context.events.is_empty()); + assert_eq!( + context + .tags() + .filter(|(k, _)| !k.starts_with("_dd.appsec.s.")) + .collect::>(), + HashMap::from([ + // Fingerprints added by the WAF + ( + "_dd.appsec.fp.http.header", + "hdr-0010100011-8a1b5aba-12-d7bf5e5b" + ), + ("_dd.appsec.fp.http.network", "net-2-1000000000"), + // Origin tag + ("_dd.origin", "appsec"), + // Extracted HTTP request information (shallow, no security event here) + ("http.method", "POST"), + ( + "http.request.headers.accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" + ), + ( + "http.request.headers.user-agent", + "Custom User Agent String" + ), + ("http.response.headers.content-type", "application/json"), + ("http.status_code", "200"), + ("http.url", "/path/to/resource"), + ]) + ); + + // Schemas extracted by the WAF (encoded order is not necessarily deterministic, so we compare after parsing back) + assert_eq!( + context + .tags() + .filter(|(k, _)| k.starts_with("_dd.appsec.s.")) + .map(|(k, v)| (k, serde_json::from_str(v).expect("should be valid JSON"))) + .collect::>(), + HashMap::from([ + ("_dd.appsec.s.res.body", json!([{"response":[8]}])), + ( + "_dd.appsec.s.res.headers", + json!([{"content-type":[[[8]],{"len":1}]}]) + ), + ("_dd.appsec.s.req.body", json!([{"test":[8]}])), + ( + "_dd.appsec.s.req.headers", + json!( + [{"host":[[[8]],{"len":1}],"accept-encoding":[[[8]],{"len":1}],"x-amz-cf-id":[[[8]],{"len":1}],"via":[[[8]],{"len":1}],"cloudfront-is-desktop-viewer":[[[8]],{"len":1}],"accept-language":[[[8]],{"len":1}],"x-forwarded-port":[[[8]],{"len":1}],"cloudfront-viewer-country":[[[8]],{"len":1}],"cloudfront-is-mobile-viewer":[[[8]],{"len":1}],"cache-control":[[[8]],{"len":1}],"cloudfront-forwarded-proto":[[[8]],{"len":1}],"x-forwarded-proto":[[[8]],{"len":1}],"cloudfront-is-tablet-viewer":[[[8]],{"len":1}],"upgrade-insecure-requests":[[[8]],{"len":1}],"user-agent":[[[8]],{"len":1}],"cloudfront-is-smarttv-viewer":[[[8]],{"len":1}],"x-forwarded-for":[[[8]],{"len":1}],"accept":[[[8]],{"len":1}]}] + ) + ), + ("_dd.appsec.s.req.params", json!([{"proxy":[8]}])), + ("_dd.appsec.s.req.query", json!([{"foo":[[[8]],{"len":1}]}])), + ]) + ); + } + + #[tokio::test] + async fn test_process_invocation_response_with_api_gateway_v2() { + let config = Config { + serverless_appsec_enabled: true, + ..Config::default() + }; + let processor = Processor::new(&config).expect("Should not fail"); + + // First create a context with a request + let mut context = processor + .process_invocation_next(&Bytes::from(include_str!( + "../../tests/payloads/api_gateway_http_event.json" + ))) + .await + .expect("Should create context"); + + // Now test the response processing + let response_payload = r#"{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\"response\":\"success\"}", + "isBase64Encoded": false + }"#; + + let response_bytes = Bytes::from(response_payload); + + // This should not panic and should complete successfully + processor + .process_invocation_response(&mut context, &response_bytes) + .await; + + // Verify context state after response processing (unchanged) + assert_eq!(context.request_type, payload::RequestType::APIGatewayV2Http); + //TODO(romain.marcadier): Verify additional side-effects + } + + #[tokio::test] + async fn test_process_invocation_response_with_invalid_response() { + let config = Config { + serverless_appsec_enabled: true, + ..Config::default() + }; + let processor = Processor::new(&config).expect("Should not fail"); + + // First create a context with a request + let mut context = processor + .process_invocation_next(&Bytes::from(include_str!( + "../../tests/payloads/api_gateway_proxy_event.json" + ))) + .await + .expect("Should create context"); + + // Now test the response processing with invalid JSON + let response_payload = r#"{"invalid": json}"#; + + let response_bytes = Bytes::from(response_payload); + + // This should not panic even with invalid response JSON + processor + .process_invocation_response(&mut context, &response_bytes) + .await; + + // Verify context state is still valid (unchanged) + assert_eq!(context.request_type, payload::RequestType::APIGatewayV1); + + // Schemas extracted by the WAF (encoded order is not necessarily deterministic, so we compare after parsing back) + // We cannot have observed the response body (it's not valid), but we have been able to observe the request... + assert_eq!( + context + .tags() + .filter(|(k, _)| k.starts_with("_dd.appsec.s.")) + .map(|(k, v)| (k, serde_json::from_str(v).expect("should be valid JSON"))) + .collect::>(), + HashMap::from([ + ("_dd.appsec.s.req.body", json!([{"test":[8]}])), + ( + "_dd.appsec.s.req.headers", + json!( + [{"host":[[[8]],{"len":1}],"accept-encoding":[[[8]],{"len":1}],"x-amz-cf-id":[[[8]],{"len":1}],"via":[[[8]],{"len":1}],"cloudfront-is-desktop-viewer":[[[8]],{"len":1}],"accept-language":[[[8]],{"len":1}],"x-forwarded-port":[[[8]],{"len":1}],"cloudfront-viewer-country":[[[8]],{"len":1}],"cloudfront-is-mobile-viewer":[[[8]],{"len":1}],"cache-control":[[[8]],{"len":1}],"cloudfront-forwarded-proto":[[[8]],{"len":1}],"x-forwarded-proto":[[[8]],{"len":1}],"cloudfront-is-tablet-viewer":[[[8]],{"len":1}],"upgrade-insecure-requests":[[[8]],{"len":1}],"user-agent":[[[8]],{"len":1}],"cloudfront-is-smarttv-viewer":[[[8]],{"len":1}],"x-forwarded-for":[[[8]],{"len":1}],"accept":[[[8]],{"len":1}]}] + ) + ), + ("_dd.appsec.s.req.params", json!([{"proxy":[8]}])), + ("_dd.appsec.s.req.query", json!([{"foo":[[[8]],{"len":1}]}])), + ]) + ); + } + + #[tokio::test] + async fn test_process_invocation_response_with_empty_response() { + let config = Config { + serverless_appsec_enabled: true, + ..Config::default() + }; + let processor = Processor::new(&config).expect("Should not fail"); + + // First create a context with a request + let mut context = processor + .process_invocation_next(&Bytes::from(include_str!( + "../../tests/payloads/api_gateway_proxy_event.json" + ))) + .await + .expect("Should create context"); + + // Now test the response processing with empty response + let response_payload = ""; + + let response_bytes = Bytes::from(response_payload); + + // This should not panic even with empty response + processor + .process_invocation_response(&mut context, &response_bytes) + .await; + + // Verify context state is still valid (unchanged) + assert_eq!(context.request_type, payload::RequestType::APIGatewayV1); + } + + #[tokio::test] + async fn test_process_invocation_next_with_custom_authorizer_token_full() { + let config = Config { + serverless_appsec_enabled: true, + ..Config::default() + }; + let processor = Processor::new(&config).expect("Should not fail"); + + let payload = r#"{ + "type": "TOKEN", + "methodArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/request", + "authorizationToken": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + }"#; + + let bytes = Bytes::from(payload); + let context = processor.process_invocation_next(&bytes).await; + + // Token style authorizers may or may not be supported depending on available data + if let Some(context) = context { + assert_eq!( + context.request_type, + payload::RequestType::APIGatewayLambdaAuthorizerToken + ); + } + } + + #[tokio::test] + async fn test_process_invocation_next_with_custom_authorizer_token_minimal() { + let config = Config { + serverless_appsec_enabled: true, + ..Config::default() + }; + let processor = Processor::new(&config).expect("Should not fail"); + + let payload = r#"{ + "type": "TOKEN", + "methodArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/request", + "authorizationToken": "allow" + }"#; + + let bytes = Bytes::from(payload); + let context = processor.process_invocation_next(&bytes).await; + + // Token style authorizers may or may not be supported depending on available data + if let Some(context) = context { + assert_eq!( + context.request_type, + payload::RequestType::APIGatewayLambdaAuthorizerToken + ); + } + } + + #[tokio::test] + async fn test_process_invocation_next_with_custom_authorizer_request_full() { + let config = Config { + serverless_appsec_enabled: true, + ..Config::default() + }; + let processor = Processor::new(&config).expect("Should not fail"); + + let payload = r#"{ + "type": "REQUEST", + "methodArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/request", + "resource": "/request", + "path": "/request", + "httpMethod": "GET", + "headers": { + "Accept": "*/*", + "Authorization": "Bearer token123", + "Content-Type": "application/json", + "Host": "example.execute-api.us-east-1.amazonaws.com", + "User-Agent": "Mozilla/5.0", + "X-Forwarded-For": "192.168.1.1, 10.0.0.1" + }, + "multiValueHeaders": { + "Accept": ["*/*"], + "Authorization": ["Bearer token123"], + "Content-Type": ["application/json"], + "Host": ["example.execute-api.us-east-1.amazonaws.com"], + "User-Agent": ["Mozilla/5.0"], + "X-Forwarded-For": ["192.168.1.1, 10.0.0.1"] + }, + "queryStringParameters": {}, + "multiValueQueryStringParameters": {}, + "pathParameters": {}, + "stageVariables": {}, + "requestContext": { + "resourceId": "123456", + "resourcePath": "/request", + "httpMethod": "GET", + "extendedRequestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "path": "/test/request", + "accountId": "123456789012", + "protocol": "HTTP/1.1", + "stage": "test", + "domainPrefix": "example", + "requestTimeEpoch": 1428582896000, + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "192.168.1.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Mozilla/5.0", + "user": null + }, + "domainName": "example.execute-api.us-east-1.amazonaws.com", + "apiId": "abcdef123" + } + }"#; + + let bytes = Bytes::from(payload); + let context = processor + .process_invocation_next(&bytes) + .await + .expect("Should create context for request style authorizer"); + + assert_eq!( + context.request_type, + payload::RequestType::APIGatewayLambdaAuthorizerRequest + ); + + // Verify that some basic tags are present + let tags: HashMap<_, _> = context.tags().collect(); + + assert_eq!(tags.get("_dd.origin"), Some(&"appsec")); + assert_eq!(tags.get("http.method"), Some(&"GET")); + assert_eq!(tags.get("http.url"), Some(&"/request")); + assert_eq!( + tags.get("http.request.headers.user-agent"), + Some(&"Mozilla/5.0") + ); + assert_eq!(tags.get("network.client.ip"), None); // Only collected if there is a security event + } + + #[tokio::test] + async fn test_process_invocation_next_with_custom_authorizer_request_with_body() { + let config = Config { + serverless_appsec_enabled: true, + ..Config::default() + }; + let processor = Processor::new(&config).expect("Should not fail"); + + let payload = r#"{ + "type": "REQUEST", + "methodArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/POST/request", + "resource": "/request", + "path": "/request", + "httpMethod": "POST", + "headers": { + "Accept": "application/json", + "Content-Type": "application/json", + "Host": "example.execute-api.us-east-1.amazonaws.com" + }, + "multiValueHeaders": { + "Accept": ["application/json"], + "Content-Type": ["application/json"], + "Host": ["example.execute-api.us-east-1.amazonaws.com"] + }, + "queryStringParameters": {}, + "multiValueQueryStringParameters": {}, + "pathParameters": {}, + "stageVariables": {}, + "requestContext": { + "resourceId": "123456", + "resourcePath": "/request", + "httpMethod": "POST", + "extendedRequestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "path": "/test/request", + "accountId": "123456789012", + "protocol": "HTTP/1.1", + "stage": "test", + "domainPrefix": "example", + "requestTimeEpoch": 1428582896000, + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "identity": { + "sourceIp": "10.0.0.1", + "userAgent": "curl/7.64.1" + }, + "domainName": "example.execute-api.us-east-1.amazonaws.com", + "apiId": "abcdef123" + }, + "body": "{\"key\":\"value\"}", + "isBase64Encoded": false + }"#; + + let bytes = Bytes::from(payload); + let context = processor + .process_invocation_next(&bytes) + .await + .expect("Should create context for request style authorizer with body"); + + assert_eq!( + context.request_type, + payload::RequestType::APIGatewayLambdaAuthorizerRequest + ); + + // Verify that some basic tags are present + let tags: HashMap<_, _> = context.tags().collect(); + + assert_eq!(tags.get("_dd.origin"), Some(&"appsec")); + assert_eq!(tags.get("http.method"), Some(&"POST")); + assert_eq!(tags.get("http.url"), Some(&"/request")); + assert_eq!( + tags.get("http.request.headers.content-type"), + Some(&"application/json") + ); + assert_eq!(tags.get("network.client.ip"), None); // Only collected if there is a security event + } + + #[tokio::test] + async fn test_process_invocation_next_with_custom_authorizer_request_minimal() { + let config = Config { + serverless_appsec_enabled: true, + ..Config::default() + }; + let processor = Processor::new(&config).expect("Should not fail"); + + let payload = r#"{ + "type": "REQUEST", + "methodArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/request", + "resource": "/request", + "path": "/request", + "httpMethod": "GET", + "headers": {}, + "multiValueHeaders": {}, + "queryStringParameters": {}, + "multiValueQueryStringParameters": {}, + "pathParameters": {}, + "stageVariables": {}, + "requestContext": { + "resourceId": "123456", + "resourcePath": "/request", + "httpMethod": "GET", + "path": "/test/request", + "accountId": "123456789012", + "protocol": "HTTP/1.1", + "stage": "test", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "identity": { + "sourceIp": "127.0.0.1" + }, + "domainName": "example.execute-api.us-east-1.amazonaws.com", + "apiId": "abcdef123" + } + }"#; + + let bytes = Bytes::from(payload); + let context = processor + .process_invocation_next(&bytes) + .await + .expect("Should create context for minimal request style authorizer"); + + assert_eq!( + context.request_type, + payload::RequestType::APIGatewayLambdaAuthorizerRequest + ); + + // Verify that basic tags are present even with minimal payload + let tags: HashMap<_, _> = context.tags().collect(); + + assert_eq!(tags.get("_dd.origin"), Some(&"appsec")); + assert_eq!(tags.get("http.method"), Some(&"GET")); + assert_eq!(tags.get("http.url"), Some(&"/request")); + assert_eq!(tags.get("network.client.ip"), None); // Only collected if there is a security event + } + + #[tokio::test] + async fn test_custom_authorizer_payload_extraction_debug() { + let config = Config { + serverless_appsec_enabled: true, + ..Config::default() + }; + let processor = Processor::new(&config).expect("Should not fail"); + + // Test TOKEN style payload + let token_payload = r#"{ + "type": "TOKEN", + "methodArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/request", + "authorizationToken": "Bearer test-token" + }"#; + + let bytes = Bytes::from(token_payload); + let context = processor.process_invocation_next(&bytes).await; + + if let Some(context) = context { + assert_eq!( + context.request_type, + payload::RequestType::APIGatewayLambdaAuthorizerToken + ); + } + + // Test REQUEST style payload + let request_payload = r#"{ + "type": "REQUEST", + "methodArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/request", + "resource": "/request", + "path": "/request", + "httpMethod": "GET", + "headers": { + "Authorization": "Bearer test-token", + "Host": "example.execute-api.us-east-1.amazonaws.com" + }, + "multiValueHeaders": { + "Authorization": ["Bearer test-token"], + "Host": ["example.execute-api.us-east-1.amazonaws.com"] + }, + "queryStringParameters": {}, + "multiValueQueryStringParameters": {}, + "pathParameters": {}, + "stageVariables": {}, + "requestContext": { + "resourceId": "123456", + "resourcePath": "/request", + "httpMethod": "GET", + "path": "/test/request", + "accountId": "123456789012", + "protocol": "HTTP/1.1", + "stage": "test", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "identity": { + "sourceIp": "192.168.1.1" + }, + "domainName": "example.execute-api.us-east-1.amazonaws.com", + "apiId": "abcdef123" + } + }"#; + + let bytes = Bytes::from(request_payload); + let context = processor + .process_invocation_next(&bytes) + .await + .expect("Should create context for request style authorizer"); + + assert_eq!( + context.request_type, + payload::RequestType::APIGatewayLambdaAuthorizerRequest + ); + + // Verify extracted data + let tags: HashMap<_, _> = context.tags().collect(); + + assert_eq!(tags.get("_dd.origin"), Some(&"appsec")); + assert_eq!(tags.get("http.method"), Some(&"GET")); + assert_eq!(tags.get("http.url"), Some(&"/request")); + assert_eq!(tags.get("network.client.ip"), None); // Only collected if there is a security event + } +} diff --git a/bottlecap/src/appsec/rules.json b/bottlecap/src/appsec/rules.json new file mode 100644 index 000000000..4dc987ddf --- /dev/null +++ b/bottlecap/src/appsec/rules.json @@ -0,0 +1,10235 @@ +{ + "version": "2.2", + "metadata": { + "rules_version": "1.15.0" + }, + "rules": [ + { + "id": "blk-001-001", + "name": "Block IP Addresses", + "tags": { + "type": "block_ip", + "category": "security_response", + "module": "network-acl" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "http.client_ip" + } + ], + "data": "blocked_ips" + }, + "operator": "ip_match" + } + ], + "transformers": [], + "on_match": [ + "block" + ] + }, + { + "id": "blk-001-002", + "name": "Block User Addresses", + "tags": { + "type": "block_user", + "category": "security_response", + "module": "authentication-acl" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "usr.id" + } + ], + "data": "blocked_users" + }, + "operator": "exact_match" + } + ], + "transformers": [], + "on_match": [ + "block" + ] + }, + { + "id": "crs-913-110", + "name": "Acunetix", + "tags": { + "type": "commercial_scanner", + "crs_id": "913110", + "category": "attack_attempt", + "tool_name": "Acunetix", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies" + } + ], + "list": [ + "acunetix-product", + "(acunetix web vulnerability scanner", + "acunetix-scanning-agreement", + "acunetix-user-agreement", + "md5(acunetix_wvs_security_test)" + ] + }, + "operator": "phrase_match" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-913-120", + "name": "Known security scanner filename/argument", + "tags": { + "type": "security_scanner", + "crs_id": "913120", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "list": [ + "/.adsensepostnottherenonobook", + "/hello.html", + "/actsensepostnottherenonotive", + "/acunetix-wvs-test-for-some-inexistent-file", + "/antidisestablishmentarianism", + "/appscan_fingerprint/mac_address", + "/arachni-", + "/cybercop", + "/nessus_is_probing_you_", + "/nessustest", + "/netsparker-", + "/rfiinc.txt", + "/thereisnowaythat-you-canbethere", + "/w3af/remotefileinclude.html", + "appscan_fingerprint", + "w00tw00t.at.isc.sans.dfind", + "w00tw00t.at.blackhats.romanian.anti-sec" + ], + "options": { + "enforce_word_boundary": true + } + }, + "operator": "phrase_match" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-920-260", + "name": "Unicode Full/Half Width Abuse Attack Attempt", + "tags": { + "type": "http_protocol_violation", + "crs_id": "920260", + "category": "attack_attempt", + "cwe": "176", + "capec": "1000/255/153/267/71", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.uri.raw" + } + ], + "regex": "\\%u[fF]{2}[0-9a-fA-F]{2}", + "options": { + "case_sensitive": true, + "min_length": 6 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-921-110", + "name": "HTTP Request Smuggling Attack", + "tags": { + "type": "http_protocol_violation", + "crs_id": "921110", + "category": "attack_attempt", + "cwe": "444", + "capec": "1000/210/272/220/33", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "(?:get|post|head|options|connect|put|delete|trace|track|patch|propfind|propatch|mkcol|copy|move|lock|unlock)\\s+[^\\s]+\\s+http/\\d", + "options": { + "case_sensitive": true, + "min_length": 12 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-921-160", + "name": "HTTP Header Injection Attack via payload (CR/LF and header-name detected)", + "tags": { + "type": "http_protocol_violation", + "crs_id": "921160", + "category": "attack_attempt", + "cwe": "113", + "capec": "1000/210/272/220/105", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "[\\n\\r]+(?:refresh|(?:set-)?cookie|(?:x-)?(?:forwarded-(?:for|host|server)|via|remote-ip|remote-addr|originating-IP))\\s*:", + "options": { + "case_sensitive": true, + "min_length": 3 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-930-100", + "name": "Obfuscated Path Traversal Attack (/../)", + "tags": { + "type": "lfi", + "crs_id": "930100", + "category": "attack_attempt", + "cwe": "22", + "capec": "1000/255/153/126", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.uri.raw" + }, + { + "address": "server.request.headers.no_cookies" + } + ], + "regex": "(?:%(?:c(?:0%(?:[2aq]f|5c|9v)|1%(?:[19p]c|8s|af))|2(?:5(?:c(?:0%25af|1%259c)|2f|5c)|%46|f)|(?:(?:f(?:8%8)?0%8|e)0%80%a|bg%q)f|%3(?:2(?:%(?:%6|4)6|F)|5%%63)|u(?:221[56]|002f|EFC8|F025)|1u|5c)|0x(?:2f|5c)|\\/|\\x5c)(?:%(?:(?:f(?:(?:c%80|8)%8)?0%8|e)0%80%ae|2(?:(?:5(?:c0%25a|2))?e|%45)|u(?:(?:002|ff0)e|2024)|%32(?:%(?:%6|4)5|E)|c0(?:%[256aef]e|\\.))|\\.(?:%0[01])?|0x2e){2,3}(?:%(?:c(?:0%(?:[2aq]f|5c|9v)|1%(?:[19p]c|8s|af))|2(?:5(?:c(?:0%25af|1%259c)|2f|5c)|%46|f)|(?:(?:f(?:8%8)?0%8|e)0%80%a|bg%q)f|%3(?:2(?:%(?:%6|4)6|F)|5%%63)|u(?:221[56]|002f|EFC8|F025)|1u|5c)|0x(?:2f|5c)|\\/|\\x5c)", + "options": { + "min_length": 4 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "normalizePath" + ] + }, + { + "id": "crs-930-110", + "name": "Simple Path Traversal Attack (/../)", + "tags": { + "type": "lfi", + "crs_id": "930110", + "category": "attack_attempt", + "cwe": "22", + "capec": "1000/255/153/126", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.uri.raw" + }, + { + "address": "server.request.headers.no_cookies" + } + ], + "regex": "(?:(?:^|[\\x5c/])\\.{2,3}[\\x5c/]|[\\x5c/]\\.{2,3}(?:[\\x5c/]|$))", + "options": { + "case_sensitive": true, + "min_length": 3 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls" + ] + }, + { + "id": "crs-930-120", + "name": "OS File Access Attempt", + "tags": { + "type": "lfi", + "crs_id": "930120", + "category": "attack_attempt", + "cwe": "22", + "capec": "1000/255/153/126", + "confidence": "1" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "list": [ + "/.htaccess", + "/.htdigest", + "/.htpasswd", + "/.addressbook", + "/.aptitude/config", + ".aws/config", + ".aws/credentials", + "/.bash_config", + "/.bash_history", + "/.bash_logout", + "/.bash_profile", + "/.bashrc", + ".cache/notify-osd.log", + ".config/odesk/odesk team.conf", + "/.cshrc", + "/.dockerignore", + ".drush/", + "/.eslintignore", + "/.fbcindex", + "/.forward", + "/.git", + ".git/", + "/.gitattributes", + "/.gitconfig", + ".gnupg/", + ".hplip/hplip.conf", + "/.ksh_history", + "/.lesshst", + ".lftp/", + "/.lhistory", + "/.lldb-history", + ".local/share/mc/", + "/.lynx_cookies", + "/.my.cnf", + "/.mysql_history", + "/.nano_history", + "/.node_repl_history", + "/.pearrc", + "/.pgpass", + "/.php_history", + "/.pinerc", + ".pki/", + "/.proclog", + "/.procmailrc", + "/.psql_history", + "/.python_history", + "/.rediscli_history", + "/.rhistory", + "/.rhosts", + "/.sh_history", + "/.sqlite_history", + ".ssh/authorized_keys", + ".ssh/config", + ".ssh/id_dsa", + ".ssh/id_dsa.pub", + ".ssh/id_rsa", + ".ssh/id_rsa.pub", + ".ssh/identity", + ".ssh/identity.pub", + ".ssh/id_ecdsa", + ".ssh/id_ecdsa.pub", + ".ssh/known_hosts", + ".subversion/auth", + ".subversion/config", + ".subversion/servers", + ".tconn/tconn.conf", + "/.tcshrc", + ".vidalia/vidalia.conf", + "/.viminfo", + "/.vimrc", + "/.www_acl", + "/.wwwacl", + "/.xauthority", + "/.zhistory", + "/.zshrc", + "/.zsh_history", + "/.nsconfig", + "data/elasticsearch", + "data/kafka", + "etc/ansible", + "etc/bind", + "etc/centos-release", + "etc/centos-release-upstream", + "etc/clam.d", + "etc/elasticsearch", + "etc/freshclam.conf", + "etc/gshadow", + "etc/gshadow-", + "etc/httpd", + "etc/kafka", + "etc/kibana", + "etc/logstash", + "etc/lvm", + "etc/mongod.conf", + "etc/my.cnf", + "etc/nuxeo.conf", + "etc/pki", + "etc/postfix", + "etc/scw-release", + "etc/subgid", + "etc/subgid-", + "etc/sudoers.d", + "etc/sysconfig", + "etc/system-release-cpe", + "opt/nuxeo", + "opt/tomcat", + "tmp/kafka-logs", + "usr/lib/rpm/rpm.log", + "var/data/elasticsearch", + "var/lib/elasticsearch", + "etc/.java", + "etc/acpi", + "etc/alsa", + "etc/alternatives", + "etc/apache2", + "etc/apm", + "etc/apparmor", + "etc/apparmor.d", + "etc/apport", + "etc/apt", + "etc/asciidoc", + "etc/avahi", + "etc/bash_completion.d", + "etc/binfmt.d", + "etc/bluetooth", + "etc/bonobo-activation", + "etc/brltty", + "etc/ca-certificates", + "etc/calendar", + "etc/chatscripts", + "etc/chromium-browser", + "etc/clamav", + "etc/cni", + "etc/console-setup", + "etc/coraza-waf", + "etc/cracklib", + "etc/cron.d", + "etc/cron.daily", + "etc/cron.hourly", + "etc/cron.monthly", + "etc/cron.weekly", + "etc/cups", + "etc/cups.save", + "etc/cupshelpers", + "etc/dbus-1", + "etc/dconf", + "etc/default", + "etc/depmod.d", + "etc/dhcp", + "etc/dictionaries-common", + "etc/dkms", + "etc/dnsmasq.d", + "etc/dockeretc/dpkg", + "etc/emacs", + "etc/environment.d", + "etc/fail2ban", + "etc/firebird", + "etc/firefox", + "etc/fonts", + "etc/fwupd", + "etc/gconf", + "etc/gdb", + "etc/gdm3", + "etc/geoclue", + "etc/ghostscript", + "etc/gimp", + "etc/glvnd", + "etc/gnome", + "etc/gnome-vfs-2.0", + "etc/gnucash", + "etc/gnustep", + "etc/groff", + "etc/grub.d", + "etc/gss", + "etc/gtk-2.0", + "etc/gtk-3.0", + "etc/hp", + "etc/ifplugd", + "etc/imagemagick-6", + "etc/init", + "etc/init.d", + "etc/initramfs-tools", + "etc/insserv.conf.d", + "etc/iproute2", + "etc/iptables", + "etc/java", + "etc/java-11-openjdk", + "etc/java-17-oracle", + "etc/java-8-openjdk", + "etc/kernel", + "etc/ld.so.conf.d", + "etc/ldap", + "etc/libblockdev", + "etc/libibverbs.d", + "etc/libnl-3", + "etc/libpaper.d", + "etc/libreoffice", + "etc/lighttpd", + "etc/logcheck", + "etc/logrotate.d", + "etc/lynx", + "etc/mail", + "etc/mc", + "etc/menu", + "etc/menu-methods", + "etc/modprobe.d", + "etc/modsecurity", + "etc/modules-load.d", + "etc/monit", + "etc/mono", + "etc/mplayer", + "etc/mpv", + "etc/muttrc.d", + "etc/mysql", + "etc/netplan", + "etc/network", + "etc/networkd-dispatcher", + "etc/networkmanager", + "etc/newt", + "etc/nghttpx", + "etc/nikto", + "etc/odbcdatasources", + "etc/openal", + "etc/openmpi", + "etc/opt", + "etc/osync", + "etc/packagekit", + "etc/pam.d", + "etc/pcmcia", + "etc/perl", + "etc/php", + "etc/pki", + "etc/pm", + "etc/polkit-1", + "etc/postfix", + "etc/ppp", + "etc/profile.d", + "etc/proftpd", + "etc/pulse", + "etc/python", + "etc/rc0.d", + "etc/rc1.d", + "etc/rc2.d", + "etc/rc3.d", + "etc/rc4.d", + "etc/rc5.d", + "etc/rc6.d", + "etc/rcs.d", + "etc/resolvconf", + "etc/rsyslog.d", + "etc/samba", + "etc/sane.d", + "etc/security", + "etc/selinux", + "etc/sensors.d", + "etc/sgml", + "etc/signon-ui", + "etc/skel", + "etc/snmp", + "etc/sound", + "etc/spamassassin", + "etc/speech-dispatcher", + "etc/ssh", + "etc/ssl", + "etc/sudoers.d", + "etc/sysctl.d", + "etc/sysstat", + "etc/systemd", + "etc/terminfo", + "etc/texmf", + "etc/thermald", + "etc/thnuclnt", + "etc/thunderbird", + "etc/timidity", + "etc/tmpfiles.d", + "etc/ubuntu-advantage", + "etc/udev", + "etc/udisks2", + "etc/ufw", + "etc/update-manager", + "etc/update-motd.d", + "etc/update-notifier", + "etc/upower", + "etc/urlview", + "etc/usb_modeswitch.d", + "etc/vim", + "etc/vmware", + "etc/vmware-installer", + "etc/vmware-vix", + "etc/vulkan", + "etc/w3m", + "etc/wireshark", + "etc/wpa_supplicant", + "etc/x11", + "etc/xdg", + "etc/xml", + "etc/redis.conf", + "etc/redis-sentinel.conf", + "etc/php.ini", + "bin/php.ini", + "etc/httpd/php.ini", + "usr/lib/php.ini", + "usr/lib/php/php.ini", + "usr/local/etc/php.ini", + "usr/local/lib/php.ini", + "usr/local/php/lib/php.ini", + "usr/local/php4/lib/php.ini", + "usr/local/php5/lib/php.ini", + "usr/local/apache/conf/php.ini", + "etc/php4.4/fcgi/php.ini", + "etc/php4/apache/php.ini", + "etc/php4/apache2/php.ini", + "etc/php5/apache/php.ini", + "etc/php5/apache2/php.ini", + "etc/php/php.ini", + "etc/php/php4/php.ini", + "etc/php/apache/php.ini", + "etc/php/apache2/php.ini", + "web/conf/php.ini", + "usr/local/zend/etc/php.ini", + "opt/xampp/etc/php.ini", + "var/local/www/conf/php.ini", + "etc/php/cgi/php.ini", + "etc/php4/cgi/php.ini", + "etc/php5/cgi/php.ini", + "home2/bin/stable/apache/php.ini", + "home/bin/stable/apache/php.ini", + "etc/httpd/conf.d/php.conf", + "php5/php.ini", + "php4/php.ini", + "php/php.ini", + "windows/php.ini", + "winnt/php.ini", + "apache/php/php.ini", + "xampp/apache/bin/php.ini", + "netserver/bin/stable/apache/php.ini", + "volumes/macintosh_hd1/usr/local/php/lib/php.ini", + "etc/mono/1.0/machine.config", + "etc/mono/2.0/machine.config", + "etc/mono/2.0/web.config", + "etc/mono/config", + "usr/local/cpanel/logs/stats_log", + "usr/local/cpanel/logs/access_log", + "usr/local/cpanel/logs/error_log", + "usr/local/cpanel/logs/license_log", + "usr/local/cpanel/logs/login_log", + "var/cpanel/cpanel.config", + "usr/local/psa/admin/logs/httpsd_access_log", + "usr/local/psa/admin/logs/panel.log", + "usr/local/psa/admin/conf/php.ini", + "etc/sw-cp-server/applications.d/plesk.conf", + "usr/local/psa/admin/conf/site_isolation_settings.ini", + "usr/local/sb/config", + "etc/sw-cp-server/applications.d/00-sso-cpserver.conf", + "etc/sso/sso_config.ini", + "etc/mysql/conf.d/old_passwords.cnf", + "var/mysql.log", + "var/mysql-bin.index", + "var/data/mysql-bin.index", + "program files/mysql/mysql server 5.0/data/{host}.err", + "program files/mysql/mysql server 5.0/data/mysql.log", + "program files/mysql/mysql server 5.0/data/mysql.err", + "program files/mysql/mysql server 5.0/data/mysql-bin.log", + "program files/mysql/mysql server 5.0/data/mysql-bin.index", + "program files/mysql/data/{host}.err", + "program files/mysql/data/mysql.log", + "program files/mysql/data/mysql.err", + "program files/mysql/data/mysql-bin.log", + "program files/mysql/data/mysql-bin.index", + "mysql/data/{host}.err", + "mysql/data/mysql.log", + "mysql/data/mysql.err", + "mysql/data/mysql-bin.log", + "mysql/data/mysql-bin.index", + "usr/local/mysql/data/mysql.log", + "usr/local/mysql/data/mysql.err", + "usr/local/mysql/data/mysql-bin.log", + "usr/local/mysql/data/mysql-slow.log", + "usr/local/mysql/data/mysqlderror.log", + "usr/local/mysql/data/{host}.err", + "usr/local/mysql/data/mysql-bin.index", + "var/lib/mysql/my.cnf", + "etc/mysql/my.cnf", + "etc/my.cnf", + "program files/mysql/mysql server 5.0/my.ini", + "program files/mysql/mysql server 5.0/my.cnf", + "program files/mysql/my.ini", + "program files/mysql/my.cnf", + "mysql/my.ini", + "mysql/my.cnf", + "mysql/bin/my.ini", + "var/postgresql/log/postgresql.log", + "usr/internet/pgsql/data/postmaster.log", + "usr/local/pgsql/data/postgresql.log", + "usr/local/pgsql/data/pg_log", + "postgresql/log/pgadmin.log", + "var/lib/pgsql/data/postgresql.conf", + "var/postgresql/db/postgresql.conf", + "var/nm2/postgresql.conf", + "usr/local/pgsql/data/postgresql.conf", + "usr/local/pgsql/data/pg_hba.conf", + "usr/internet/pgsql/data/pg_hba.conf", + "usr/local/pgsql/data/passwd", + "usr/local/pgsql/bin/pg_passwd", + "etc/postgresql/postgresql.conf", + "etc/postgresql/pg_hba.conf", + "home/postgres/data/postgresql.conf", + "home/postgres/data/pg_version", + "home/postgres/data/pg_ident.conf", + "home/postgres/data/pg_hba.conf", + "program files/postgresql/8.3/data/pg_hba.conf", + "program files/postgresql/8.3/data/pg_ident.conf", + "program files/postgresql/8.3/data/postgresql.conf", + "program files/postgresql/8.4/data/pg_hba.conf", + "program files/postgresql/8.4/data/pg_ident.conf", + "program files/postgresql/8.4/data/postgresql.conf", + "program files/postgresql/9.0/data/pg_hba.conf", + "program files/postgresql/9.0/data/pg_ident.conf", + "program files/postgresql/9.0/data/postgresql.conf", + "program files/postgresql/9.1/data/pg_hba.conf", + "program files/postgresql/9.1/data/pg_ident.conf", + "program files/postgresql/9.1/data/postgresql.conf", + "wamp/logs/access.log", + "wamp/logs/apache_error.log", + "wamp/logs/genquery.log", + "wamp/logs/mysql.log", + "wamp/logs/slowquery.log", + "wamp/bin/apache/apache2.2.22/logs/access.log", + "wamp/bin/apache/apache2.2.22/logs/error.log", + "wamp/bin/apache/apache2.2.21/logs/access.log", + "wamp/bin/apache/apache2.2.21/logs/error.log", + "wamp/bin/mysql/mysql5.5.24/data/mysql-bin.index", + "wamp/bin/mysql/mysql5.5.16/data/mysql-bin.index", + "wamp/bin/apache/apache2.2.21/conf/httpd.conf", + "wamp/bin/apache/apache2.2.22/conf/httpd.conf", + "wamp/bin/apache/apache2.2.21/wampserver.conf", + "wamp/bin/apache/apache2.2.22/wampserver.conf", + "wamp/bin/apache/apache2.2.22/conf/wampserver.conf", + "wamp/bin/mysql/mysql5.5.24/my.ini", + "wamp/bin/mysql/mysql5.5.24/wampserver.conf", + "wamp/bin/mysql/mysql5.5.16/my.ini", + "wamp/bin/mysql/mysql5.5.16/wampserver.conf", + "wamp/bin/php/php5.3.8/php.ini", + "wamp/bin/php/php5.4.3/php.ini", + "xampp/apache/logs/access.log", + "xampp/apache/logs/error.log", + "xampp/mysql/data/mysql-bin.index", + "xampp/mysql/data/mysql.err", + "xampp/mysql/data/{host}.err", + "xampp/sendmail/sendmail.log", + "xampp/apache/conf/httpd.conf", + "xampp/filezillaftp/filezilla server.xml", + "xampp/mercurymail/mercury.ini", + "xampp/php/php.ini", + "xampp/phpmyadmin/config.inc.php", + "xampp/sendmail/sendmail.ini", + "xampp/webalizer/webalizer.conf", + "opt/lampp/etc/httpd.conf", + "xampp/htdocs/aca.txt", + "xampp/htdocs/admin.php", + "xampp/htdocs/leer.txt", + "usr/local/apache/logs/audit_log", + "usr/local/apache2/logs/audit_log", + "logs/security_debug_log", + "logs/security_log", + "usr/local/apache/conf/modsec.conf", + "usr/local/apache2/conf/modsec.conf", + "winnt/system32/logfiles/msftpsvc", + "winnt/system32/logfiles/msftpsvc1", + "winnt/system32/logfiles/msftpsvc2", + "windows/system32/logfiles/msftpsvc", + "windows/system32/logfiles/msftpsvc1", + "windows/system32/logfiles/msftpsvc2", + "etc/logrotate.d/proftpd", + "www/logs/proftpd.system.log", + "etc/pam.d/proftpd", + "etc/proftp.conf", + "etc/protpd/proftpd.conf", + "etc/vhcs2/proftpd/proftpd.conf", + "etc/proftpd/modules.conf", + "etc/vsftpd.chroot_list", + "etc/logrotate.d/vsftpd.log", + "etc/vsftpd/vsftpd.conf", + "etc/vsftpd.conf", + "etc/chrootusers", + "var/adm/log/xferlog", + "etc/wu-ftpd/ftpaccess", + "etc/wu-ftpd/ftphosts", + "etc/wu-ftpd/ftpusers", + "logs/pure-ftpd.log", + "usr/sbin/pure-config.pl", + "usr/etc/pure-ftpd.conf", + "etc/pure-ftpd/pure-ftpd.conf", + "usr/local/etc/pure-ftpd.conf", + "usr/local/etc/pureftpd.pdb", + "usr/local/pureftpd/etc/pureftpd.pdb", + "usr/local/pureftpd/sbin/pure-config.pl", + "usr/local/pureftpd/etc/pure-ftpd.conf", + "etc/pure-ftpd.conf", + "etc/pure-ftpd/pure-ftpd.pdb", + "etc/pureftpd.pdb", + "etc/pureftpd.passwd", + "etc/pure-ftpd/pureftpd.pdb", + "usr/ports/ftp/pure-ftpd/pure-ftpd.conf", + "usr/ports/ftp/pure-ftpd/pureftpd.pdb", + "usr/ports/ftp/pure-ftpd/pureftpd.passwd", + "usr/ports/net/pure-ftpd/pure-ftpd.conf", + "usr/ports/net/pure-ftpd/pureftpd.pdb", + "usr/ports/net/pure-ftpd/pureftpd.passwd", + "usr/pkgsrc/net/pureftpd/pure-ftpd.conf", + "usr/pkgsrc/net/pureftpd/pureftpd.pdb", + "usr/pkgsrc/net/pureftpd/pureftpd.passwd", + "usr/ports/contrib/pure-ftpd/pure-ftpd.conf", + "usr/ports/contrib/pure-ftpd/pureftpd.pdb", + "usr/ports/contrib/pure-ftpd/pureftpd.passwd", + "usr/sbin/mudlogd", + "etc/muddleftpd/mudlog", + "etc/muddleftpd.com", + "etc/muddleftpd/mudlogd.conf", + "etc/muddleftpd/muddleftpd.conf", + "usr/sbin/mudpasswd", + "etc/muddleftpd/muddleftpd.passwd", + "etc/muddleftpd/passwd", + "etc/logrotate.d/ftp", + "etc/ftpchroot", + "etc/ftphosts", + "etc/ftpusers", + "winnt/system32/logfiles/smtpsvc", + "winnt/system32/logfiles/smtpsvc1", + "winnt/system32/logfiles/smtpsvc2", + "winnt/system32/logfiles/smtpsvc3", + "winnt/system32/logfiles/smtpsvc4", + "winnt/system32/logfiles/smtpsvc5", + "windows/system32/logfiles/smtpsvc", + "windows/system32/logfiles/smtpsvc1", + "windows/system32/logfiles/smtpsvc2", + "windows/system32/logfiles/smtpsvc3", + "windows/system32/logfiles/smtpsvc4", + "windows/system32/logfiles/smtpsvc5", + "etc/osxhttpd/osxhttpd.conf", + "system/library/webobjects/adaptors/apache2.2/apache.conf", + "etc/apache2/sites-available/default", + "etc/apache2/sites-available/default-ssl", + "etc/apache2/sites-enabled/000-default", + "etc/apache2/sites-enabled/default", + "etc/apache2/apache2.conf", + "etc/apache2/ports.conf", + "usr/local/etc/apache/httpd.conf", + "usr/pkg/etc/httpd/httpd.conf", + "usr/pkg/etc/httpd/httpd-default.conf", + "usr/pkg/etc/httpd/httpd-vhosts.conf", + "etc/httpd/mod_php.conf", + "etc/httpd/extra/httpd-ssl.conf", + "etc/rc.d/rc.httpd", + "usr/local/apache/conf/httpd.conf.default", + "usr/local/apache/conf/access.conf", + "usr/local/apache22/conf/httpd.conf", + "usr/local/apache22/httpd.conf", + "usr/local/etc/apache22/conf/httpd.conf", + "usr/local/apps/apache22/conf/httpd.conf", + "etc/apache22/conf/httpd.conf", + "etc/apache22/httpd.conf", + "opt/apache22/conf/httpd.conf", + "usr/local/etc/apache2/vhosts.conf", + "usr/local/apache/conf/vhosts.conf", + "usr/local/apache2/conf/vhosts.conf", + "usr/local/apache/conf/vhosts-custom.conf", + "usr/local/apache2/conf/vhosts-custom.conf", + "etc/apache/default-server.conf", + "etc/apache2/default-server.conf", + "usr/local/apache2/conf/extra/httpd-ssl.conf", + "usr/local/apache2/conf/ssl.conf", + "etc/httpd/conf.d", + "usr/local/etc/apache22/httpd.conf", + "usr/local/etc/apache2/httpd.conf", + "etc/apache2/httpd2.conf", + "etc/apache2/ssl-global.conf", + "etc/apache2/vhosts.d/00_default_vhost.conf", + "apache/conf/httpd.conf", + "etc/apache/httpd.conf", + "etc/httpd/conf", + "http/httpd.conf", + "usr/local/apache1.3/conf/httpd.conf", + "usr/local/etc/httpd/conf", + "var/apache/conf/httpd.conf", + "var/www/conf", + "www/apache/conf/httpd.conf", + "www/conf/httpd.conf", + "etc/init.d", + "etc/apache/access.conf", + "etc/rc.conf", + "www/logs/freebsddiary-error.log", + "www/logs/freebsddiary-access_log", + "library/webserver/documents/index.html", + "library/webserver/documents/index.htm", + "library/webserver/documents/default.html", + "library/webserver/documents/default.htm", + "library/webserver/documents/index.php", + "library/webserver/documents/default.php", + "usr/local/etc/webmin/miniserv.conf", + "etc/webmin/miniserv.conf", + "usr/local/etc/webmin/miniserv.users", + "etc/webmin/miniserv.users", + "winnt/system32/logfiles/w3svc/inetsvn1.log", + "winnt/system32/logfiles/w3svc1/inetsvn1.log", + "winnt/system32/logfiles/w3svc2/inetsvn1.log", + "winnt/system32/logfiles/w3svc3/inetsvn1.log", + "windows/system32/logfiles/w3svc/inetsvn1.log", + "windows/system32/logfiles/w3svc1/inetsvn1.log", + "windows/system32/logfiles/w3svc2/inetsvn1.log", + "windows/system32/logfiles/w3svc3/inetsvn1.log", + "apache/logs/error.log", + "apache/logs/access.log", + "apache2/logs/error.log", + "apache2/logs/access.log", + "logs/error.log", + "logs/access.log", + "etc/httpd/logs/access_log", + "etc/httpd/logs/access.log", + "etc/httpd/logs/error_log", + "etc/httpd/logs/error.log", + "usr/local/apache/logs/access_log", + "usr/local/apache/logs/access.log", + "usr/local/apache/logs/error_log", + "usr/local/apache/logs/error.log", + "usr/local/apache2/logs/access_log", + "usr/local/apache2/logs/access.log", + "usr/local/apache2/logs/error_log", + "usr/local/apache2/logs/error.log", + "var/www/logs/access_log", + "var/www/logs/access.log", + "var/www/logs/error_log", + "var/www/logs/error.log", + "opt/lampp/logs/access_log", + "opt/lampp/logs/error_log", + "opt/xampp/logs/access_log", + "opt/xampp/logs/error_log", + "opt/lampp/logs/access.log", + "opt/lampp/logs/error.log", + "opt/xampp/logs/access.log", + "opt/xampp/logs/error.log", + "program files/apache group/apache/logs/access.log", + "program files/apache group/apache/logs/error.log", + "program files/apache software foundation/apache2.2/logs/error.log", + "program files/apache software foundation/apache2.2/logs/access.log", + "opt/apache/apache.conf", + "opt/apache/conf/apache.conf", + "opt/apache2/apache.conf", + "opt/apache2/conf/apache.conf", + "opt/httpd/apache.conf", + "opt/httpd/conf/apache.conf", + "etc/httpd/apache.conf", + "etc/apache2/apache.conf", + "etc/httpd/conf/apache.conf", + "usr/local/apache/apache.conf", + "usr/local/apache/conf/apache.conf", + "usr/local/apache2/apache.conf", + "usr/local/apache2/conf/apache.conf", + "usr/local/php/apache.conf.php", + "usr/local/php4/apache.conf.php", + "usr/local/php5/apache.conf.php", + "usr/local/php/apache.conf", + "usr/local/php4/apache.conf", + "usr/local/php5/apache.conf", + "private/etc/httpd/apache.conf", + "opt/apache/apache2.conf", + "opt/apache/conf/apache2.conf", + "opt/apache2/apache2.conf", + "opt/apache2/conf/apache2.conf", + "opt/httpd/apache2.conf", + "opt/httpd/conf/apache2.conf", + "etc/httpd/apache2.conf", + "etc/httpd/conf/apache2.conf", + "usr/local/apache/apache2.conf", + "usr/local/apache/conf/apache2.conf", + "usr/local/apache2/apache2.conf", + "usr/local/apache2/conf/apache2.conf", + "usr/local/php/apache2.conf.php", + "usr/local/php4/apache2.conf.php", + "usr/local/php5/apache2.conf.php", + "usr/local/php/apache2.conf", + "usr/local/php4/apache2.conf", + "usr/local/php5/apache2.conf", + "private/etc/httpd/apache2.conf", + "usr/local/apache/conf/httpd.conf", + "usr/local/apache2/conf/httpd.conf", + "etc/httpd/conf/httpd.conf", + "etc/apache/apache.conf", + "etc/apache/conf/httpd.conf", + "etc/apache2/httpd.conf", + "usr/apache2/conf/httpd.conf", + "usr/apache/conf/httpd.conf", + "usr/local/etc/apache/conf/httpd.conf", + "usr/local/apache/httpd.conf", + "usr/local/apache2/httpd.conf", + "usr/local/httpd/conf/httpd.conf", + "usr/local/etc/apache2/conf/httpd.conf", + "usr/local/etc/httpd/conf/httpd.conf", + "usr/local/apps/apache2/conf/httpd.conf", + "usr/local/apps/apache/conf/httpd.conf", + "usr/local/php/httpd.conf.php", + "usr/local/php4/httpd.conf.php", + "usr/local/php5/httpd.conf.php", + "usr/local/php/httpd.conf", + "usr/local/php4/httpd.conf", + "usr/local/php5/httpd.conf", + "etc/apache2/conf/httpd.conf", + "etc/http/conf/httpd.conf", + "etc/httpd/httpd.conf", + "etc/http/httpd.conf", + "etc/httpd.conf", + "opt/apache/conf/httpd.conf", + "opt/apache2/conf/httpd.conf", + "var/www/conf/httpd.conf", + "private/etc/httpd/httpd.conf", + "private/etc/httpd/httpd.conf.default", + "etc/apache2/vhosts.d/default_vhost.include", + "etc/apache2/conf.d/charset", + "etc/apache2/conf.d/security", + "etc/apache2/envvars", + "etc/apache2/mods-available/autoindex.conf", + "etc/apache2/mods-available/deflate.conf", + "etc/apache2/mods-available/dir.conf", + "etc/apache2/mods-available/mem_cache.conf", + "etc/apache2/mods-available/mime.conf", + "etc/apache2/mods-available/proxy.conf", + "etc/apache2/mods-available/setenvif.conf", + "etc/apache2/mods-available/ssl.conf", + "etc/apache2/mods-enabled/alias.conf", + "etc/apache2/mods-enabled/deflate.conf", + "etc/apache2/mods-enabled/dir.conf", + "etc/apache2/mods-enabled/mime.conf", + "etc/apache2/mods-enabled/negotiation.conf", + "etc/apache2/mods-enabled/php5.conf", + "etc/apache2/mods-enabled/status.conf", + "program files/apache group/apache/conf/httpd.conf", + "program files/apache group/apache2/conf/httpd.conf", + "program files/xampp/apache/conf/apache.conf", + "program files/xampp/apache/conf/apache2.conf", + "program files/xampp/apache/conf/httpd.conf", + "program files/apache group/apache/apache.conf", + "program files/apache group/apache/conf/apache.conf", + "program files/apache group/apache2/conf/apache.conf", + "program files/apache group/apache/apache2.conf", + "program files/apache group/apache/conf/apache2.conf", + "program files/apache group/apache2/conf/apache2.conf", + "program files/apache software foundation/apache2.2/conf/httpd.conf", + "volumes/macintosh_hd1/opt/httpd/conf/httpd.conf", + "volumes/macintosh_hd1/opt/apache/conf/httpd.conf", + "volumes/macintosh_hd1/opt/apache2/conf/httpd.conf", + "volumes/macintosh_hd1/usr/local/php/httpd.conf.php", + "volumes/macintosh_hd1/usr/local/php4/httpd.conf.php", + "volumes/macintosh_hd1/usr/local/php5/httpd.conf.php", + "volumes/webbackup/opt/apache2/conf/httpd.conf", + "volumes/webbackup/private/etc/httpd/httpd.conf", + "volumes/webbackup/private/etc/httpd/httpd.conf.default", + "usr/local/etc/apache/vhosts.conf", + "usr/local/jakarta/tomcat/conf/jakarta.conf", + "usr/local/jakarta/tomcat/conf/server.xml", + "usr/local/jakarta/tomcat/conf/context.xml", + "usr/local/jakarta/tomcat/conf/workers.properties", + "usr/local/jakarta/tomcat/conf/logging.properties", + "usr/local/jakarta/dist/tomcat/conf/jakarta.conf", + "usr/local/jakarta/dist/tomcat/conf/server.xml", + "usr/local/jakarta/dist/tomcat/conf/context.xml", + "usr/local/jakarta/dist/tomcat/conf/workers.properties", + "usr/local/jakarta/dist/tomcat/conf/logging.properties", + "usr/share/tomcat6/conf/server.xml", + "usr/share/tomcat6/conf/context.xml", + "usr/share/tomcat6/conf/workers.properties", + "usr/share/tomcat6/conf/logging.properties", + "var/cpanel/tomcat.options", + "usr/local/jakarta/tomcat/logs/catalina.out", + "usr/local/jakarta/tomcat/logs/catalina.err", + "opt/tomcat/logs/catalina.out", + "opt/tomcat/logs/catalina.err", + "usr/share/logs/catalina.out", + "usr/share/logs/catalina.err", + "usr/share/tomcat/logs/catalina.out", + "usr/share/tomcat/logs/catalina.err", + "usr/share/tomcat6/logs/catalina.out", + "usr/share/tomcat6/logs/catalina.err", + "usr/local/apache/logs/mod_jk.log", + "usr/local/jakarta/tomcat/logs/mod_jk.log", + "usr/local/jakarta/dist/tomcat/logs/mod_jk.log", + "opt/[jboss]/server/default/conf/jboss-minimal.xml", + "opt/[jboss]/server/default/conf/jboss-service.xml", + "opt/[jboss]/server/default/conf/jndi.properties", + "opt/[jboss]/server/default/conf/log4j.xml", + "opt/[jboss]/server/default/conf/login-config.xml", + "opt/[jboss]/server/default/conf/standardjaws.xml", + "opt/[jboss]/server/default/conf/standardjboss.xml", + "opt/[jboss]/server/default/conf/server.log.properties", + "opt/[jboss]/server/default/deploy/jboss-logging.xml", + "usr/local/[jboss]/server/default/conf/jboss-minimal.xml", + "usr/local/[jboss]/server/default/conf/jboss-service.xml", + "usr/local/[jboss]/server/default/conf/jndi.properties", + "usr/local/[jboss]/server/default/conf/log4j.xml", + "usr/local/[jboss]/server/default/conf/login-config.xml", + "usr/local/[jboss]/server/default/conf/standardjaws.xml", + "usr/local/[jboss]/server/default/conf/standardjboss.xml", + "usr/local/[jboss]/server/default/conf/server.log.properties", + "usr/local/[jboss]/server/default/deploy/jboss-logging.xml", + "private/tmp/[jboss]/server/default/conf/jboss-minimal.xml", + "private/tmp/[jboss]/server/default/conf/jboss-service.xml", + "private/tmp/[jboss]/server/default/conf/jndi.properties", + "private/tmp/[jboss]/server/default/conf/log4j.xml", + "private/tmp/[jboss]/server/default/conf/login-config.xml", + "private/tmp/[jboss]/server/default/conf/standardjaws.xml", + "private/tmp/[jboss]/server/default/conf/standardjboss.xml", + "private/tmp/[jboss]/server/default/conf/server.log.properties", + "private/tmp/[jboss]/server/default/deploy/jboss-logging.xml", + "tmp/[jboss]/server/default/conf/jboss-minimal.xml", + "tmp/[jboss]/server/default/conf/jboss-service.xml", + "tmp/[jboss]/server/default/conf/jndi.properties", + "tmp/[jboss]/server/default/conf/log4j.xml", + "tmp/[jboss]/server/default/conf/login-config.xml", + "tmp/[jboss]/server/default/conf/standardjaws.xml", + "tmp/[jboss]/server/default/conf/standardjboss.xml", + "tmp/[jboss]/server/default/conf/server.log.properties", + "tmp/[jboss]/server/default/deploy/jboss-logging.xml", + "program files/[jboss]/server/default/conf/jboss-minimal.xml", + "program files/[jboss]/server/default/conf/jboss-service.xml", + "program files/[jboss]/server/default/conf/jndi.properties", + "program files/[jboss]/server/default/conf/log4j.xml", + "program files/[jboss]/server/default/conf/login-config.xml", + "program files/[jboss]/server/default/conf/standardjaws.xml", + "program files/[jboss]/server/default/conf/standardjboss.xml", + "program files/[jboss]/server/default/conf/server.log.properties", + "program files/[jboss]/server/default/deploy/jboss-logging.xml", + "[jboss]/server/default/conf/jboss-minimal.xml", + "[jboss]/server/default/conf/jboss-service.xml", + "[jboss]/server/default/conf/jndi.properties", + "[jboss]/server/default/conf/log4j.xml", + "[jboss]/server/default/conf/login-config.xml", + "[jboss]/server/default/conf/standardjaws.xml", + "[jboss]/server/default/conf/standardjboss.xml", + "[jboss]/server/default/conf/server.log.properties", + "[jboss]/server/default/deploy/jboss-logging.xml", + "opt/[jboss]/server/default/log/server.log", + "opt/[jboss]/server/default/log/boot.log", + "usr/local/[jboss]/server/default/log/server.log", + "usr/local/[jboss]/server/default/log/boot.log", + "private/tmp/[jboss]/server/default/log/server.log", + "private/tmp/[jboss]/server/default/log/boot.log", + "tmp/[jboss]/server/default/log/server.log", + "tmp/[jboss]/server/default/log/boot.log", + "program files/[jboss]/server/default/log/server.log", + "program files/[jboss]/server/default/log/boot.log", + "[jboss]/server/default/log/server.log", + "[jboss]/server/default/log/boot.log", + "var/lighttpd.log", + "var/logs/access.log", + "usr/local/apache2/logs/lighttpd.error.log", + "usr/local/apache2/logs/lighttpd.log", + "usr/local/apache/logs/lighttpd.error.log", + "usr/local/apache/logs/lighttpd.log", + "usr/local/lighttpd/log/lighttpd.error.log", + "usr/local/lighttpd/log/access.log", + "usr/home/user/var/log/lighttpd.error.log", + "usr/home/user/var/log/apache.log", + "home/user/lighttpd/lighttpd.conf", + "usr/home/user/lighttpd/lighttpd.conf", + "etc/lighttpd/lighthttpd.conf", + "usr/local/etc/lighttpd.conf", + "usr/local/lighttpd/conf/lighttpd.conf", + "usr/local/etc/lighttpd.conf.new", + "var/www/.lighttpdpassword", + "logs/access_log", + "logs/error_log", + "etc/nginx/nginx.conf", + "usr/local/etc/nginx/nginx.conf", + "usr/local/nginx/conf/nginx.conf", + "usr/local/zeus/web/global.cfg", + "usr/local/zeus/web/log/errors", + "opt/lsws/conf/httpd_conf.xml", + "usr/local/lsws/conf/httpd_conf.xml", + "opt/lsws/logs/error.log", + "opt/lsws/logs/access.log", + "usr/local/lsws/logs/error.log", + "usr/local/logs/access.log", + "usr/local/samba/lib/log.user", + "usr/local/logs/samba.log", + "etc/samba/netlogon", + "etc/smbpasswd", + "etc/smb.conf", + "etc/samba/dhcp.conf", + "etc/samba/smb.conf", + "etc/samba/samba.conf", + "etc/samba/smb.conf.user", + "etc/samba/smbpasswd", + "etc/samba/smbusers", + "etc/samba/private/smbpasswd", + "usr/local/etc/smb.conf", + "usr/local/samba/lib/smb.conf.user", + "etc/dhcp3/dhclient.conf", + "etc/dhcp3/dhcpd.conf", + "etc/dhcp/dhclient.conf", + "program files/vidalia bundle/polipo/polipo.conf", + "etc/tor/tor-tsocks.conf", + "etc/stunnel/stunnel.conf", + "etc/tsocks.conf", + "etc/tinyproxy/tinyproxy.conf", + "etc/miredo-server.conf", + "etc/miredo.conf", + "etc/miredo/miredo-server.conf", + "etc/miredo/miredo.conf", + "etc/wicd/dhclient.conf.template.default", + "etc/wicd/manager-settings.conf", + "etc/wicd/wired-settings.conf", + "etc/wicd/wireless-settings.conf", + "etc/ipfw.rules", + "etc/ipfw.conf", + "etc/firewall.rules", + "winnt/system32/logfiles/firewall/pfirewall.log", + "winnt/system32/logfiles/firewall/pfirewall.log.old", + "windows/system32/logfiles/firewall/pfirewall.log", + "windows/system32/logfiles/firewall/pfirewall.log.old", + "etc/clamav/clamd.conf", + "etc/clamav/freshclam.conf", + "etc/x11/xorg.conf", + "etc/x11/xorg.conf-vesa", + "etc/x11/xorg.conf-vmware", + "etc/x11/xorg.conf.beforevmwaretoolsinstall", + "etc/x11/xorg.conf.orig", + "etc/bluetooth/input.conf", + "etc/bluetooth/main.conf", + "etc/bluetooth/network.conf", + "etc/bluetooth/rfcomm.conf", + "etc/bash_completion.d/debconf", + "root/.bash_logout", + "root/.bash_history", + "root/.bash_config", + "root/.bashrc", + "etc/bash.bashrc", + "var/adm/syslog", + "var/adm/sulog", + "var/adm/utmp", + "var/adm/utmpx", + "var/adm/wtmp", + "var/adm/wtmpx", + "var/adm/lastlog/username", + "usr/spool/lp/log", + "var/adm/lp/lpd-errs", + "usr/lib/cron/log", + "var/adm/loginlog", + "var/adm/pacct", + "var/adm/dtmp", + "var/adm/acct/sum/loginlog", + "var/adm/x0msgs", + "var/adm/crash/vmcore", + "var/adm/crash/unix", + "etc/newsyslog.conf", + "var/adm/qacct", + "var/adm/ras/errlog", + "var/adm/ras/bootlog", + "var/adm/cron/log", + "etc/utmp", + "etc/security/lastlog", + "etc/security/failedlogin", + "usr/spool/mqueue/syslog", + "var/adm/messages", + "var/adm/aculogs", + "var/adm/aculog", + "var/adm/vold.log", + "var/adm/log/asppp.log", + "var/lp/logs/lpsched", + "var/lp/logs/lpnet", + "var/lp/logs/requests", + "var/cron/log", + "var/saf/_log", + "var/saf/port/log", + "tmp/access.log", + "etc/sensors.conf", + "etc/sensors3.conf", + "etc/host.conf", + "etc/pam.conf", + "etc/resolv.conf", + "etc/apt/apt.conf", + "etc/inetd.conf", + "etc/syslog.conf", + "etc/sysctl.conf", + "etc/sysctl.d/10-console-messages.conf", + "etc/sysctl.d/10-network-security.conf", + "etc/sysctl.d/10-process-security.conf", + "etc/sysctl.d/wine.sysctl.conf", + "etc/security/access.conf", + "etc/security/group.conf", + "etc/security/limits.conf", + "etc/security/namespace.conf", + "etc/security/pam_env.conf", + "etc/security/sepermit.conf", + "etc/security/time.conf", + "etc/ssh/sshd_config", + "etc/adduser.conf", + "etc/deluser.conf", + "etc/avahi/avahi-daemon.conf", + "etc/ca-certificates.conf", + "etc/ca-certificates.conf.dpkg-old", + "etc/casper.conf", + "etc/chkrootkit.conf", + "etc/debconf.conf", + "etc/dns2tcpd.conf", + "etc/e2fsck.conf", + "etc/esound/esd.conf", + "etc/etter.conf", + "etc/fuse.conf", + "etc/foremost.conf", + "etc/hdparm.conf", + "etc/kernel-img.conf", + "etc/kernel-pkg.conf", + "etc/ld.so.conf", + "etc/ltrace.conf", + "etc/mail/sendmail.conf", + "etc/manpath.config", + "etc/kbd/config", + "etc/ldap/ldap.conf", + "etc/logrotate.conf", + "etc/mtools.conf", + "etc/smi.conf", + "etc/updatedb.conf", + "etc/pulse/client.conf", + "usr/share/adduser/adduser.conf", + "etc/hostname", + "etc/networks", + "etc/timezone", + "etc/modules", + "etc/passwd", + "etc/shadow", + "etc/fstab", + "etc/motd", + "etc/hosts", + "etc/group", + "etc/alias", + "etc/crontab", + "etc/crypttab", + "etc/exports", + "etc/mtab", + "etc/hosts.allow", + "etc/hosts.deny", + "etc/os-release", + "etc/password.master", + "etc/profile", + "etc/default/grub", + "etc/resolvconf/update-libc.d/sendmail", + "etc/inittab", + "etc/issue", + "etc/issue.net", + "etc/login.defs", + "etc/sudoers", + "etc/sysconfig/network-scripts/ifcfg-eth0", + "etc/redhat-release", + "etc/scw-release", + "etc/system-release-cpe", + "etc/debian_version", + "etc/fedora-release", + "etc/mandrake-release", + "etc/slackware-release", + "etc/suse-release", + "etc/security/group", + "etc/security/passwd", + "etc/security/user", + "etc/security/environ", + "etc/security/limits", + "etc/security/opasswd", + "boot/grub/grub.cfg", + "boot/grub/menu.lst", + "root/.ksh_history", + "root/.xauthority", + "usr/lib/security/mkuser.default", + "var/lib/squirrelmail/prefs/squirrelmail.log", + "etc/squirrelmail/apache.conf", + "etc/squirrelmail/config_local.php", + "etc/squirrelmail/default_pref", + "etc/squirrelmail/index.php", + "etc/squirrelmail/config_default.php", + "etc/squirrelmail/config.php", + "etc/squirrelmail/filters_setup.php", + "etc/squirrelmail/sqspell_config.php", + "etc/squirrelmail/config/config.php", + "etc/httpd/conf.d/squirrelmail.conf", + "usr/share/squirrelmail/config/config.php", + "private/etc/squirrelmail/config/config.php", + "srv/www/htdos/squirrelmail/config/config.php", + "var/www/squirrelmail/config/config.php", + "var/www/html/squirrelmail/config/config.php", + "var/www/html/squirrelmail-1.2.9/config/config.php", + "usr/share/squirrelmail/plugins/squirrel_logger/setup.php", + "usr/local/squirrelmail/www/readme", + "windows/system32/drivers/etc/hosts", + "windows/system32/drivers/etc/lmhosts.sam", + "windows/system32/drivers/etc/networks", + "windows/system32/drivers/etc/protocol", + "windows/system32/drivers/etc/services", + "/boot.ini", + "windows/debug/netsetup.log", + "windows/comsetup.log", + "windows/repair/setup.log", + "windows/setupact.log", + "windows/setupapi.log", + "windows/setuperr.log", + "windows/updspapi.log", + "windows/wmsetup.log", + "windows/windowsupdate.log", + "windows/odbc.ini", + "usr/local/psa/admin/htdocs/domains/databases/phpmyadmin/libraries/config.default.php", + "etc/apache2/conf.d/phpmyadmin.conf", + "etc/phpmyadmin/config.inc.php", + "etc/openldap/ldap.conf", + "etc/cups/acroread.conf", + "etc/cups/cupsd.conf", + "etc/cups/cupsd.conf.default", + "etc/cups/pdftops.conf", + "etc/cups/printers.conf", + "windows/system32/macromed/flash/flashinstall.log", + "windows/system32/macromed/flash/install.log", + "etc/cvs-cron.conf", + "etc/cvs-pserver.conf", + "etc/subversion/config", + "etc/modprobe.d/vmware-tools.conf", + "etc/updatedb.conf.beforevmwaretoolsinstall", + "etc/vmware-tools/config", + "etc/vmware-tools/tpvmlp.conf", + "etc/vmware-tools/vmware-tools-libraries.conf", + "var/log", + "var/log/sw-cp-server/error_log", + "var/log/sso/sso.log", + "var/log/dpkg.log", + "var/log/btmp", + "var/log/utmp", + "var/log/wtmp", + "var/log/mysql/mysql-bin.log", + "var/log/mysql/mysql-bin.index", + "var/log/mysql/data/mysql-bin.index", + "var/log/mysql.log", + "var/log/mysql.err", + "var/log/mysqlderror.log", + "var/log/mysql/mysql.log", + "var/log/mysql/mysql-slow.log", + "var/log/mysql-bin.index", + "var/log/data/mysql-bin.index", + "var/log/postgresql/postgresql.log", + "var/log/postgres/pg_backup.log", + "var/log/postgres/postgres.log", + "var/log/postgresql.log", + "var/log/pgsql/pgsql.log", + "var/log/postgresql/postgresql-8.1-main.log", + "var/log/postgresql/postgresql-8.3-main.log", + "var/log/postgresql/postgresql-8.4-main.log", + "var/log/postgresql/postgresql-9.0-main.log", + "var/log/postgresql/postgresql-9.1-main.log", + "var/log/pgsql8.log", + "var/log/postgresql/postgres.log", + "var/log/pgsql_log", + "var/log/postgresql/main.log", + "var/log/cron", + "var/log/postgres.log", + "var/log/proftpd", + "var/log/proftpd/xferlog.legacy", + "var/log/proftpd.access_log", + "var/log/proftpd.xferlog", + "var/log/vsftpd.log", + "var/log/xferlog", + "var/log/pure-ftpd/pure-ftpd.log", + "var/log/pureftpd.log", + "var/log/muddleftpd", + "var/log/muddleftpd.conf", + "var/log/ftp-proxy/ftp-proxy.log", + "var/log/ftp-proxy", + "var/log/ftplog", + "var/log/exim_mainlog", + "var/log/exim/mainlog", + "var/log/maillog", + "var/log/exim_paniclog", + "var/log/exim/paniclog", + "var/log/exim/rejectlog", + "var/log/exim_rejectlog", + "var/log/webmin/miniserv.log", + "var/log/httpd/access_log", + "var/log/httpd/error_log", + "var/log/httpd/access.log", + "var/log/httpd/error.log", + "var/log/apache/access_log", + "var/log/apache/access.log", + "var/log/apache/error_log", + "var/log/apache/error.log", + "var/log/apache2/access_log", + "var/log/apache2/access.log", + "var/log/apache2/error_log", + "var/log/apache2/error.log", + "var/log/access_log", + "var/log/access.log", + "var/log/error_log", + "var/log/error.log", + "var/log/tomcat6/catalina.out", + "var/log/lighttpd.error.log", + "var/log/lighttpd.access.log", + "var/logs/access.log", + "var/log/lighttpd/", + "var/log/lighttpd/error.log", + "var/log/lighttpd/access.www.log", + "var/log/lighttpd/error.www.log", + "var/log/lighttpd/access.log", + "var/log/lighttpd/{domain}/access.log", + "var/log/lighttpd/{domain}/error.log", + "var/log/nginx/access_log", + "var/log/nginx/error_log", + "var/log/nginx/access.log", + "var/log/nginx/error.log", + "var/log/nginx.access_log", + "var/log/nginx.error_log", + "var/log/samba/log.smbd", + "var/log/samba/log.nmbd", + "var/log/samba.log", + "var/log/samba.log1", + "var/log/samba.log2", + "var/log/log.smb", + "var/log/ipfw.log", + "var/log/ipfw", + "var/log/ipfw/ipfw.log", + "var/log/ipfw.today", + "var/log/poplog", + "var/log/authlog", + "var/log/news.all", + "var/log/news/news.all", + "var/log/news/news.crit", + "var/log/news/news.err", + "var/log/news/news.notice", + "var/log/news/suck.err", + "var/log/news/suck.notice", + "var/log/messages", + "var/log/messages.1", + "var/log/user.log", + "var/log/user.log.1", + "var/log/auth.log", + "var/log/pm-powersave.log", + "var/log/xorg.0.log", + "var/log/daemon.log", + "var/log/daemon.log.1", + "var/log/kern.log", + "var/log/kern.log.1", + "var/log/mail.err", + "var/log/mail.info", + "var/log/mail.warn", + "var/log/ufw.log", + "var/log/boot.log", + "var/log/syslog", + "var/log/syslog.1", + "var/log/squirrelmail.log", + "var/log/apache2/squirrelmail.log", + "var/log/apache2/squirrelmail.err.log", + "var/log/mail.log", + "var/log/vmware/hostd.log", + "var/log/vmware/hostd-1.log", + "/wp-config.php", + "/wp-config.bak", + "/wp-config.old", + "/wp-config.temp", + "/wp-config.tmp", + "/wp-config.txt", + "/config.yml", + "/config_dev.yml", + "/config_prod.yml", + "/config_test.yml", + "/parameters.yml", + "/routing.yml", + "/security.yml", + "/services.yml", + "sites/default/default.settings.php", + "sites/default/settings.php", + "sites/default/settings.local.php", + "app/etc/local.xml", + "/sftp-config.json", + "/web.config", + "includes/config.php", + "includes/configure.php", + "/config.inc.php", + "/localsettings.php", + "inc/config.php", + "typo3conf/localconf.php", + "config/app.php", + "config/custom.php", + "config/database.php", + "/configuration.php", + "/config.php", + "var/mail/www-data", + "etc/network/", + "etc/init/", + "inetpub/wwwroot/global.asa", + "system32/inetsrv/config/applicationhost.config", + "system32/inetsrv/config/administration.config", + "system32/inetsrv/config/redirection.config", + "system32/config/default", + "system32/config/sam", + "system32/config/system", + "system32/config/software", + "winnt/repair/sam._", + "/package.json", + "/package-lock.json", + "/gruntfile.js", + "/npm-debug.log", + "/ormconfig.json", + "/tsconfig.json", + "/webpack.config.js", + "/yarn.lock", + "proc/0", + "proc/1", + "proc/2", + "proc/3", + "proc/4", + "proc/5", + "proc/6", + "proc/7", + "proc/8", + "proc/9", + "proc/acpi", + "proc/asound", + "proc/bootconfig", + "proc/buddyinfo", + "proc/bus", + "proc/cgroups", + "proc/cmdline", + "proc/config.gz", + "proc/consoles", + "proc/cpuinfo", + "proc/crypto", + "proc/devices", + "proc/diskstats", + "proc/dma", + "proc/docker", + "proc/driver", + "proc/dynamic_debug", + "proc/execdomains", + "proc/fb", + "proc/filesystems", + "proc/fs", + "proc/interrupts", + "proc/iomem", + "proc/ioports", + "proc/ipmi", + "proc/irq", + "proc/kallsyms", + "proc/kcore", + "proc/keys", + "proc/keys", + "proc/key-users", + "proc/kmsg", + "proc/kpagecgroup", + "proc/kpagecount", + "proc/kpageflags", + "proc/latency_stats", + "proc/loadavg", + "proc/locks", + "proc/mdstat", + "proc/meminfo", + "proc/misc", + "proc/modules", + "proc/mounts", + "proc/mpt", + "proc/mtd", + "proc/mtrr", + "proc/net", + "proc/net/tcp", + "proc/net/udp", + "proc/pagetypeinfo", + "proc/partitions", + "proc/pressure", + "proc/sched_debug", + "proc/schedstat", + "proc/scsi", + "proc/self", + "proc/self/cmdline", + "proc/self/environ", + "proc/self/fd/0", + "proc/self/fd/1", + "proc/self/fd/10", + "proc/self/fd/11", + "proc/self/fd/12", + "proc/self/fd/13", + "proc/self/fd/14", + "proc/self/fd/15", + "proc/self/fd/2", + "proc/self/fd/3", + "proc/self/fd/4", + "proc/self/fd/5", + "proc/self/fd/6", + "proc/self/fd/7", + "proc/self/fd/8", + "proc/self/fd/9", + "proc/self/mounts", + "proc/self/stat", + "proc/self/status", + "proc/slabinfo", + "proc/softirqs", + "proc/stat", + "proc/swaps", + "proc/sys", + "proc/sysrq-trigger", + "proc/sysvipc", + "proc/thread-self", + "proc/timer_list", + "proc/timer_stats", + "proc/tty", + "proc/uptime", + "proc/version", + "proc/version_signature", + "proc/vmallocinfo", + "proc/vmstat", + "proc/zoneinfo", + "sys/block", + "sys/bus", + "sys/class", + "sys/dev", + "sys/devices", + "sys/firmware", + "sys/fs", + "sys/hypervisor", + "sys/kernel", + "sys/module", + "sys/power", + "windows\\win.ini", + "default\\ntuser.dat", + "/var/run/secrets/kubernetes.io/serviceaccount" + ], + "options": { + "enforce_word_boundary": true + } + }, + "operator": "phrase_match" + } + ], + "transformers": [ + "lowercase", + "normalizePath" + ] + }, + { + "id": "crs-931-110", + "name": "RFI: Common RFI Vulnerable Parameter Name used w/ URL Payload", + "tags": { + "type": "rfi", + "crs_id": "931110", + "category": "attack_attempt", + "cwe": "98", + "capec": "1000/152/175/253/193", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + } + ], + "regex": "(?:\\binclude\\s*\\([^)]*|mosConfig_absolute_path|_CONF\\[path\\]|_SERVER\\[DOCUMENT_ROOT\\]|GALLERY_BASEDIR|path\\[docroot\\]|appserv_root|config\\[root_dir\\])=(?:file|ftps?|https?)://", + "options": { + "min_length": 15 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-931-120", + "name": "RFI: URL Payload Used w/Trailing Question Mark Character (?)", + "tags": { + "type": "rfi", + "crs_id": "931120", + "category": "attack_attempt", + "cwe": "98", + "capec": "1000/152/175/253/193", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "^(?i:file|ftps?)://.*?\\?+$", + "options": { + "case_sensitive": true, + "min_length": 4 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-932-160", + "name": "Remote Command Execution: Unix Shell Code Found", + "tags": { + "type": "command_injection", + "crs_id": "932160", + "category": "attack_attempt", + "cwe": "77", + "capec": "1000/152/248/88", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "options": { + "enforce_word_boundary": true + }, + "list": [ + "${cdpath}", + "${dirstack}", + "${home}", + "${hostname}", + "${ifs}", + "${oldpwd}", + "${ostype}", + "${path}", + "${pwd}", + "$cdpath", + "$dirstack", + "$home", + "$hostname", + "$ifs", + "$oldpwd", + "$ostype", + "$pwd", + "dev/fd/", + "dev/null", + "dev/stderr", + "dev/stdin", + "dev/stdout", + "dev/tcp/", + "dev/udp/", + "dev/zero", + "etc/master.passwd", + "etc/pwd.db", + "etc/shells", + "etc/spwd.db", + "proc/self/", + "bin/7z", + "bin/7za", + "bin/7zr", + "bin/ab", + "bin/agetty", + "bin/ansible-playbook", + "bin/apt", + "bin/apt-get", + "bin/ar", + "bin/aria2c", + "bin/arj", + "bin/arp", + "bin/as", + "bin/ascii-xfr", + "bin/ascii85", + "bin/ash", + "bin/aspell", + "bin/at", + "bin/atobm", + "bin/awk", + "bin/base32", + "bin/base64", + "bin/basenc", + "bin/bash", + "bin/bpftrace", + "bin/bridge", + "bin/bundler", + "bin/bunzip2", + "bin/busctl", + "bin/busybox", + "bin/byebug", + "bin/bzcat", + "bin/bzcmp", + "bin/bzdiff", + "bin/bzegrep", + "bin/bzexe", + "bin/bzfgrep", + "bin/bzgrep", + "bin/bzip2", + "bin/bzip2recover", + "bin/bzless", + "bin/bzmore", + "bin/bzz", + "bin/c89", + "bin/c99", + "bin/cancel", + "bin/capsh", + "bin/cat", + "bin/cc", + "bin/certbot", + "bin/check_by_ssh", + "bin/check_cups", + "bin/check_log", + "bin/check_memory", + "bin/check_raid", + "bin/check_ssl_cert", + "bin/check_statusfile", + "bin/chmod", + "bin/choom", + "bin/chown", + "bin/chroot", + "bin/clang", + "bin/clang++", + "bin/cmp", + "bin/cobc", + "bin/column", + "bin/comm", + "bin/composer", + "bin/core_perl/zipdetails", + "bin/cowsay", + "bin/cowthink", + "bin/cp", + "bin/cpan", + "bin/cpio", + "bin/cpulimit", + "bin/crash", + "bin/crontab", + "bin/csh", + "bin/csplit", + "bin/csvtool", + "bin/cupsfilter", + "bin/curl", + "bin/cut", + "bin/dash", + "bin/date", + "bin/dd", + "bin/dev/fd/", + "bin/dev/null", + "bin/dev/stderr", + "bin/dev/stdin", + "bin/dev/stdout", + "bin/dev/tcp/", + "bin/dev/udp/", + "bin/dev/zero", + "bin/dialog", + "bin/diff", + "bin/dig", + "bin/dmesg", + "bin/dmidecode", + "bin/dmsetup", + "bin/dnf", + "bin/docker", + "bin/dosbox", + "bin/dpkg", + "bin/du", + "bin/dvips", + "bin/easy_install", + "bin/eb", + "bin/echo", + "bin/ed", + "bin/efax", + "bin/emacs", + "bin/env", + "bin/eqn", + "bin/es", + "bin/esh", + "bin/etc/group", + "bin/etc/master.passwd", + "bin/etc/passwd", + "bin/etc/pwd.db", + "bin/etc/shadow", + "bin/etc/shells", + "bin/etc/spwd.db", + "bin/ex", + "bin/exiftool", + "bin/expand", + "bin/expect", + "bin/expr", + "bin/facter", + "bin/fetch", + "bin/file", + "bin/find", + "bin/finger", + "bin/fish", + "bin/flock", + "bin/fmt", + "bin/fold", + "bin/fping", + "bin/ftp", + "bin/gawk", + "bin/gcc", + "bin/gcore", + "bin/gdb", + "bin/gem", + "bin/genie", + "bin/genisoimage", + "bin/ghc", + "bin/ghci", + "bin/gimp", + "bin/ginsh", + "bin/git", + "bin/grc", + "bin/grep", + "bin/gtester", + "bin/gunzip", + "bin/gzexe", + "bin/gzip", + "bin/hd", + "bin/head", + "bin/hexdump", + "bin/highlight", + "bin/hping3", + "bin/iconv", + "bin/id", + "bin/iftop", + "bin/install", + "bin/ionice", + "bin/ip", + "bin/irb", + "bin/ispell", + "bin/jjs", + "bin/join", + "bin/journalctl", + "bin/jq", + "bin/jrunscript", + "bin/knife", + "bin/ksh", + "bin/ksshell", + "bin/latex", + "bin/ld", + "bin/ldconfig", + "bin/less", + "bin/lftp", + "bin/ln", + "bin/loginctl", + "bin/logsave", + "bin/look", + "bin/lp", + "bin/ls", + "bin/ltrace", + "bin/lua", + "bin/lualatex", + "bin/luatex", + "bin/lwp-download", + "bin/lwp-request", + "bin/lz", + "bin/lz4", + "bin/lz4c", + "bin/lz4cat", + "bin/lzcat", + "bin/lzcmp", + "bin/lzdiff", + "bin/lzegrep", + "bin/lzfgrep", + "bin/lzgrep", + "bin/lzless", + "bin/lzma", + "bin/lzmadec", + "bin/lzmainfo", + "bin/lzmore", + "bin/mail", + "bin/make", + "bin/man", + "bin/mawk", + "bin/mkfifo", + "bin/mknod", + "bin/more", + "bin/mosquitto", + "bin/mount", + "bin/msgattrib", + "bin/msgcat", + "bin/msgconv", + "bin/msgfilter", + "bin/msgmerge", + "bin/msguniq", + "bin/mtr", + "bin/mv", + "bin/mysql", + "bin/nano", + "bin/nasm", + "bin/nawk", + "bin/nc", + "bin/ncat", + "bin/neofetch", + "bin/nice", + "bin/nl", + "bin/nm", + "bin/nmap", + "bin/node", + "bin/nohup", + "bin/npm", + "bin/nroff", + "bin/nsenter", + "bin/octave", + "bin/od", + "bin/openssl", + "bin/openvpn", + "bin/openvt", + "bin/opkg", + "bin/paste", + "bin/pax", + "bin/pdb", + "bin/pdflatex", + "bin/pdftex", + "bin/pdksh", + "bin/perf", + "bin/perl", + "bin/pg", + "bin/php", + "bin/php-cgi", + "bin/php5", + "bin/php7", + "bin/pic", + "bin/pico", + "bin/pidstat", + "bin/pigz", + "bin/pip", + "bin/pkexec", + "bin/pkg", + "bin/pr", + "bin/printf", + "bin/proc/self/", + "bin/pry", + "bin/ps", + "bin/psed", + "bin/psftp", + "bin/psql", + "bin/ptx", + "bin/puppet", + "bin/pxz", + "bin/python", + "bin/python2", + "bin/python3", + "bin/rake", + "bin/rbash", + "bin/rc", + "bin/readelf", + "bin/red", + "bin/redcarpet", + "bin/restic", + "bin/rev", + "bin/rlogin", + "bin/rlwrap", + "bin/rpm", + "bin/rpmquery", + "bin/rsync", + "bin/ruby", + "bin/run-mailcap", + "bin/run-parts", + "bin/rview", + "bin/rvim", + "bin/sash", + "bin/sbin/capsh", + "bin/sbin/logsave", + "bin/sbin/service", + "bin/sbin/start-stop-daemon", + "bin/scp", + "bin/screen", + "bin/script", + "bin/sed", + "bin/service", + "bin/setarch", + "bin/sftp", + "bin/sg", + "bin/sh", + "bin/shuf", + "bin/sleep", + "bin/slsh", + "bin/smbclient", + "bin/snap", + "bin/socat", + "bin/soelim", + "bin/sort", + "bin/split", + "bin/sqlite3", + "bin/ss", + "bin/ssh", + "bin/ssh-keygen", + "bin/ssh-keyscan", + "bin/sshpass", + "bin/start-stop-daemon", + "bin/stdbuf", + "bin/strace", + "bin/strings", + "bin/su", + "bin/sysctl", + "bin/systemctl", + "bin/systemd-resolve", + "bin/tac", + "bin/tail", + "bin/tar", + "bin/task", + "bin/taskset", + "bin/tbl", + "bin/tclsh", + "bin/tcpdump", + "bin/tcsh", + "bin/tee", + "bin/telnet", + "bin/tex", + "bin/tftp", + "bin/tic", + "bin/time", + "bin/timedatectl", + "bin/timeout", + "bin/tmux", + "bin/top", + "bin/troff", + "bin/tshark", + "bin/ul", + "bin/uname", + "bin/uncompress", + "bin/unexpand", + "bin/uniq", + "bin/unlz4", + "bin/unlzma", + "bin/unpigz", + "bin/unrar", + "bin/unshare", + "bin/unxz", + "bin/unzip", + "bin/unzstd", + "bin/update-alternatives", + "bin/uudecode", + "bin/uuencode", + "bin/valgrind", + "bin/vi", + "bin/view", + "bin/vigr", + "bin/vim", + "bin/vimdiff", + "bin/vipw", + "bin/virsh", + "bin/volatility", + "bin/wall", + "bin/watch", + "bin/wc", + "bin/wget", + "bin/whiptail", + "bin/who", + "bin/whoami", + "bin/whois", + "bin/wireshark", + "bin/wish", + "bin/xargs", + "bin/xelatex", + "bin/xetex", + "bin/xmodmap", + "bin/xmore", + "bin/xpad", + "bin/xxd", + "bin/xz", + "bin/xzcat", + "bin/xzcmp", + "bin/xzdec", + "bin/xzdiff", + "bin/xzegrep", + "bin/xzfgrep", + "bin/xzgrep", + "bin/xzless", + "bin/xzmore", + "bin/yarn", + "bin/yelp", + "bin/yes", + "bin/yum", + "bin/zathura", + "bin/zip", + "bin/zipcloak", + "bin/zipcmp", + "bin/zipdetails", + "bin/zipgrep", + "bin/zipinfo", + "bin/zipmerge", + "bin/zipnote", + "bin/zipsplit", + "bin/ziptool", + "bin/zsh", + "bin/zsoelim", + "bin/zstd", + "bin/zstdcat", + "bin/zstdgrep", + "bin/zstdless", + "bin/zstdmt", + "bin/zypper" + ] + }, + "operator": "phrase_match" + } + ], + "transformers": [ + "lowercase", + "cmdLine" + ] + }, + { + "id": "crs-932-171", + "name": "Remote Command Execution: Shellshock (CVE-2014-6271)", + "tags": { + "type": "command_injection", + "crs_id": "932171", + "category": "attack_attempt", + "cwe": "77", + "capec": "1000/152/248/88", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "^\\(\\s*\\)\\s+{", + "options": { + "case_sensitive": true, + "min_length": 4 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-932-180", + "name": "Restricted File Upload Attempt", + "tags": { + "type": "command_injection", + "crs_id": "932180", + "category": "attack_attempt", + "cwe": "706", + "capec": "1000/225/122/17/177", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "x-filename" + ] + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "x_filename" + ] + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "x-file-name" + ] + } + ], + "list": [ + ".htaccess", + ".htdigest", + ".htpasswd", + "wp-config.php", + "config.yml", + "config_dev.yml", + "config_prod.yml", + "config_test.yml", + "parameters.yml", + "routing.yml", + "security.yml", + "services.yml", + "default.settings.php", + "settings.php", + "settings.local.php", + "local.xml", + ".env" + ], + "options": { + "enforce_word_boundary": true + } + }, + "operator": "phrase_match" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-933-111", + "name": "PHP Injection Attack: PHP Script File Upload Found", + "tags": { + "type": "unrestricted_file_upload", + "crs_id": "933111", + "category": "attack_attempt", + "cwe": "434", + "capec": "1000/225/122/17/650", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "x-filename" + ] + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "x_filename" + ] + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "x.filename" + ] + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "x-file-name" + ] + } + ], + "regex": ".*\\.(?:php\\d*|phtml)\\..*$", + "options": { + "case_sensitive": true, + "min_length": 5 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-933-130", + "name": "PHP Injection Attack: Global Variables Found", + "tags": { + "type": "php_code_injection", + "crs_id": "933130", + "category": "attack_attempt", + "cwe": "94", + "capec": "1000/225/122/17/650", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "options": { + "enforce_word_boundary": true + }, + "list": [ + "$globals", + "$_cookie", + "$_env", + "$_files", + "$_get", + "$_post", + "$_request", + "$_server", + "$_session", + "$argc", + "$argv", + "$http_\\u200bresponse_\\u200bheader", + "$php_\\u200berrormsg", + "$http_cookie_vars", + "$http_env_vars", + "$http_get_vars", + "$http_post_files", + "$http_post_vars", + "$http_raw_post_data", + "$http_request_vars", + "$http_server_vars" + ] + }, + "operator": "phrase_match" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-933-131", + "name": "PHP Injection Attack: HTTP Headers Values Found", + "tags": { + "type": "php_code_injection", + "crs_id": "933131", + "category": "attack_attempt", + "cwe": "94", + "capec": "1000/225/122/17/650", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?:HTTP_(?:ACCEPT(?:_(?:ENCODING|LANGUAGE|CHARSET))?|(?:X_FORWARDED_FO|REFERE)R|(?:USER_AGEN|HOS)T|CONNECTION|KEEP_ALIVE)|PATH_(?:TRANSLATED|INFO)|ORIG_PATH_INFO|QUERY_STRING|REQUEST_URI|AUTH_TYPE)", + "options": { + "case_sensitive": true, + "min_length": 9 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-933-140", + "name": "PHP Injection Attack: I/O Stream Found", + "tags": { + "type": "php_code_injection", + "crs_id": "933140", + "category": "attack_attempt", + "cwe": "94", + "capec": "1000/225/122/17/650", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "php://(?:std(?:in|out|err)|(?:in|out)put|fd|memory|temp|filter)", + "options": { + "min_length": 8 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-933-150", + "name": "PHP Injection Attack: High-Risk PHP Function Name Found", + "tags": { + "type": "php_code_injection", + "crs_id": "933150", + "category": "attack_attempt", + "cwe": "94", + "capec": "1000/225/122/17/650", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "list": [ + "__halt_compiler", + "apache_child_terminate", + "base64_decode", + "bzdecompress", + "call_user_func", + "call_user_func_array", + "call_user_method", + "call_user_method_array", + "convert_uudecode", + "file_get_contents", + "file_put_contents", + "fsockopen", + "get_class_methods", + "get_class_vars", + "get_defined_constants", + "get_defined_functions", + "get_defined_vars", + "gzdecode", + "gzinflate", + "gzuncompress", + "include_once", + "invokeargs", + "pcntl_exec", + "pcntl_fork", + "pfsockopen", + "posix_getcwd", + "posix_getpwuid", + "posix_getuid", + "posix_uname", + "reflectionfunction", + "require_once", + "shell_exec", + "str_rot13", + "sys_get_temp_dir", + "wp_remote_fopen", + "wp_remote_get", + "wp_remote_head", + "wp_remote_post", + "wp_remote_request", + "wp_safe_remote_get", + "wp_safe_remote_head", + "wp_safe_remote_post", + "wp_safe_remote_request", + "zlib_decode" + ], + "options": { + "enforce_word_boundary": true + } + }, + "operator": "phrase_match" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-933-160", + "name": "PHP Injection Attack: High-Risk PHP Function Call Found", + "tags": { + "type": "php_code_injection", + "crs_id": "933160", + "category": "attack_attempt", + "cwe": "94", + "capec": "1000/225/122/17/650", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "\\b(?:s(?:e(?:t(?:_(?:e(?:xception|rror)_handler|magic_quotes_runtime|include_path)|defaultstub)|ssion_s(?:et_save_handler|tart))|qlite_(?:(?:(?:unbuffered|single|array)_)?query|create_(?:aggregate|function)|p?open|exec)|tr(?:eam_(?:context_create|socket_client)|ipc?slashes|rev)|implexml_load_(?:string|file)|ocket_c(?:onnect|reate)|h(?:ow_sourc|a1_fil)e|pl_autoload_register|ystem)|p(?:r(?:eg_(?:replace(?:_callback(?:_array)?)?|match(?:_all)?|split)|oc_(?:(?:terminat|clos|nic)e|get_status|open)|int_r)|o(?:six_(?:get(?:(?:e[gu]|g)id|login|pwnam)|mk(?:fifo|nod)|ttyname|kill)|pen)|hp(?:_(?:strip_whitespac|unam)e|version|info)|g_(?:(?:execut|prepar)e|connect|query)|a(?:rse_(?:ini_file|str)|ssthru)|utenv)|r(?:unkit_(?:function_(?:re(?:defin|nam)e|copy|add)|method_(?:re(?:defin|nam)e|copy|add)|constant_(?:redefine|add))|e(?:(?:gister_(?:shutdown|tick)|name)_function|ad(?:(?:gz)?file|_exif_data|dir))|awurl(?:de|en)code)|i(?:mage(?:createfrom(?:(?:jpe|pn)g|x[bp]m|wbmp|gif)|(?:jpe|pn)g|g(?:d2?|if)|2?wbmp|xbm)|s_(?:(?:(?:execut|write?|read)ab|fi)le|dir)|ni_(?:get(?:_all)?|set)|terator_apply|ptcembed)|g(?:et(?:_(?:c(?:urrent_use|fg_va)r|meta_tags)|my(?:[gpu]id|inode)|(?:lastmo|cw)d|imagesize|env)|z(?:(?:(?:defla|wri)t|encod|fil)e|compress|open|read)|lob)|a(?:rray_(?:u(?:intersect(?:_u?assoc)?|diff(?:_u?assoc)?)|intersect_u(?:assoc|key)|diff_u(?:assoc|key)|filter|reduce|map)|ssert(?:_options)?|tob)|h(?:tml(?:specialchars(?:_decode)?|_entity_decode|entities)|(?:ash(?:_(?:update|hmac))?|ighlight)_file|e(?:ader_register_callback|x2bin))|f(?:i(?:le(?:(?:[acm]tim|inod)e|(?:_exist|perm)s|group)?|nfo_open)|tp_(?:nb_(?:ge|pu)|connec|ge|pu)t|(?:unction_exis|pu)ts|write|open)|o(?:b_(?:get_(?:c(?:ontents|lean)|flush)|end_(?:clean|flush)|clean|flush|start)|dbc_(?:result(?:_all)?|exec(?:ute)?|connect)|pendir)|m(?:b_(?:ereg(?:_(?:replace(?:_callback)?|match)|i(?:_replace)?)?|parse_str)|(?:ove_uploaded|d5)_file|ethod_exists|ysql_query|kdir)|e(?:x(?:if_(?:t(?:humbnail|agname)|imagetype|read_data)|ec)|scapeshell(?:arg|cmd)|rror_reporting|val)|c(?:url_(?:file_create|exec|init)|onvert_uuencode|reate_function|hr)|u(?:n(?:serialize|pack)|rl(?:de|en)code|[ak]?sort)|b(?:(?:son_(?:de|en)|ase64_en)code|zopen|toa)|(?:json_(?:de|en)cod|debug_backtrac|tmpfil)e|var_dump)(?:\\s|/\\*.*\\*/|//.*|#.*|\\\"|')*\\((?:(?:\\s|/\\*.*\\*/|//.*|#.*)*(?:\\$\\w+|[A-Z\\d]\\w*|\\w+\\(.*\\)|\\\\?\"(?:[^\"]|\\\\\"|\"\"|\"\\+\")*\\\\?\"|\\\\?'(?:[^']|''|'\\+')*\\\\?')(?:\\s|/\\*.*\\*/|//.*|#.*)*(?:(?:::|\\.|->)(?:\\s|/\\*.*\\*/|//.*|#.*)*\\w+(?:\\(.*\\))?)?,)*(?:(?:\\s|/\\*.*\\*/|//.*|#.*)*(?:\\$\\w+|[A-Z\\d]\\w*|\\w+\\(.*\\)|\\\\?\"(?:[^\"]|\\\\\"|\"\"|\"\\+\")*\\\\?\"|\\\\?'(?:[^']|''|'\\+')*\\\\?')(?:\\s|/\\*.*\\*/|//.*|#.*)*(?:(?:::|\\.|->)(?:\\s|/\\*.*\\*/|//.*|#.*)*\\w+(?:\\(.*\\))?)?)?\\)\\s*(?:[;\\.)}\\]|\\\\]|\\?>|%>|$)", + "options": { + "case_sensitive": true, + "min_length": 5 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-933-170", + "name": "PHP Injection Attack: Serialized Object Injection", + "tags": { + "type": "php_code_injection", + "crs_id": "933170", + "category": "attack_attempt", + "cwe": "502", + "capec": "1000/152/586", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "[oOcC]:\\d+:\\\".+?\\\":\\d+:{[\\W\\w]*}", + "options": { + "case_sensitive": true, + "min_length": 12 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-933-200", + "name": "PHP Injection Attack: Wrapper scheme detected", + "tags": { + "type": "php_code_injection", + "crs_id": "933200", + "category": "attack_attempt", + "cwe": "502", + "capec": "1000/152/586", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?:(?:bzip|ssh)2|z(?:lib|ip)|(?:ph|r)ar|expect|glob|ogg)://", + "options": { + "case_sensitive": true, + "min_length": 6 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls" + ] + }, + { + "id": "crs-934-100", + "name": "Node.js Injection Attack 1/2", + "tags": { + "type": "js_code_injection", + "crs_id": "934100", + "category": "attack_attempt", + "cwe": "94", + "capec": "1000/152/242", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "\\b(?:(?:l(?:(?:utimes|chmod)(?:Sync)?|(?:stat|ink)Sync)|w(?:rite(?:(?:File|v)(?:Sync)?|Sync)|atchFile)|u(?:n(?:watchFile|linkSync)|times(?:Sync)?)|s(?:(?:ymlink|tat)Sync|pawn(?:File|Sync))|ex(?:ec(?:File(?:Sync)?|Sync)|istsSync)|a(?:ppendFile|ccess)(?:Sync)?|(?:Caveat|Inode)s|open(?:dir)?Sync|new\\s+Function|Availability|\\beval)\\s*\\(|m(?:ain(?:Module\\s*(?:\\W*\\s*(?:constructor|require)|\\[)|\\s*(?:\\W*\\s*(?:constructor|require)|\\[))|kd(?:temp(?:Sync)?|irSync)\\s*\\(|odule\\.exports\\s*=)|c(?:(?:(?:h(?:mod|own)|lose)Sync|reate(?:Write|Read)Stream|p(?:Sync)?)\\s*\\(|o(?:nstructor\\s*(?:\\W*\\s*_load|\\[)|pyFile(?:Sync)?\\s*\\())|f(?:(?:(?:s(?:(?:yncS)?|tatS)|datas(?:yncS)?)ync|ch(?:mod|own)(?:Sync)?)\\s*\\(|u(?:nction\\s*\\(\\s*\\)\\s*{|times(?:Sync)?\\s*\\())|r(?:e(?:(?:ad(?:(?:File|link|dir)?Sync|v(?:Sync)?)|nameSync)\\s*\\(|quire\\s*(?:\\W*\\s*main\\b|\\[))|m(?:Sync)?\\s*\\()|process\\s*(?:\\W*\\s*(?:mainModule|binding)|\\[)|t(?:his\\.constructor|runcateSync\\s*\\()|_(?:\\$\\$ND_FUNC\\$\\$_|_js_function)|global\\s*(?:\\W*\\s*process|\\[)|String\\s*\\.\\s*fromCharCode|binding\\s*\\[)", + "options": { + "case_sensitive": true, + "min_length": 3 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-934-101", + "name": "Node.js Injection Attack 2/2", + "tags": { + "type": "js_code_injection", + "crs_id": "934101", + "category": "attack_attempt", + "confidence": "1", + "cwe": "94", + "capec": "1000/152/242", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "\\b(?:w(?:atch|rite)|(?:spaw|ope)n|exists|close|fork|read)\\s*\\(", + "options": { + "case_sensitive": true, + "min_length": 5 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-941-110", + "name": "XSS Filter - Category 1: Script Tag Vector", + "tags": { + "type": "xss", + "crs_id": "941110", + "category": "attack_attempt", + "cwe": "80", + "capec": "1000/152/242/63/591", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "referer" + ] + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "]*>[\\s\\S]*?", + "options": { + "case_sensitive": false, + "min_length": 8 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls", + "urlDecodeUni" + ] + }, + { + "id": "crs-941-120", + "name": "XSS Filter - Category 2: Event Handler Vector", + "tags": { + "type": "xss", + "crs_id": "941120", + "category": "attack_attempt", + "cwe": "83", + "capec": "1000/152/242/63/591/243", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "referer" + ] + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "\\bon(?:d(?:r(?:ag(?:en(?:ter|d)|leave|start|over)?|op)|urationchange|blclick)|s(?:e(?:ek(?:ing|ed)|arch|lect)|u(?:spend|bmit)|talled|croll|how)|m(?:ouse(?:(?:lea|mo)ve|o(?:ver|ut)|enter|down|up)|essage)|p(?:a(?:ge(?:hide|show)|(?:st|us)e)|lay(?:ing)?|rogress|aste|ointer(?:cancel|down|enter|leave|move|out|over|rawupdate|up))|c(?:anplay(?:through)?|o(?:ntextmenu|py)|hange|lick|ut)|a(?:nimation(?:iteration|start|end)|(?:fterprin|bor)t|uxclick|fterscriptexecute)|t(?:o(?:uch(?:cancel|start|move|end)|ggle)|imeupdate)|f(?:ullscreen(?:change|error)|ocus(?:out|in)?|inish)|(?:(?:volume|hash)chang|o(?:ff|n)lin)e|b(?:efore(?:unload|print)|lur)|load(?:ed(?:meta)?data|start|end)?|r(?:es(?:ize|et)|atechange)|key(?:press|down|up)|w(?:aiting|heel)|in(?:valid|put)|e(?:nded|rror)|unload)[\\s\\x0B\\x09\\x0C\\x3B\\x2C\\x28\\x3B]*?=[^=]", + "options": { + "min_length": 8 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls", + "urlDecodeUni" + ] + }, + { + "id": "crs-941-140", + "name": "XSS Filter - Category 4: Javascript URI Vector", + "tags": { + "type": "xss", + "crs_id": "941140", + "category": "attack_attempt", + "cwe": "84", + "capec": "1000/152/242/63/591/244", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "referer" + ] + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "[a-z]+=(?:[^:=]+:.+;)*?[^:=]+:url\\(javascript", + "options": { + "min_length": 18 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls", + "urlDecodeUni" + ] + }, + { + "id": "crs-941-170", + "name": "NoScript XSS InjectionChecker: Attribute Injection", + "tags": { + "type": "xss", + "crs_id": "941170", + "category": "attack_attempt", + "cwe": "83", + "capec": "1000/152/242/63/591/243", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "referer" + ] + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?:\\W|^)(?:javascript:(?:[\\s\\S]+[=\\x5c\\(\\[\\.<]|[\\s\\S]*?(?:\\bname\\b|\\x5c[ux]\\d)))|@\\W*?i\\W*?m\\W*?p\\W*?o\\W*?r\\W*?t\\W*?(?:/\\*[\\s\\S]*?)?(?:[\\\"']|\\W*?u\\W*?r\\W*?l[\\s\\S]*?\\()|[^-]*?-\\W*?m\\W*?o\\W*?z\\W*?-\\W*?b\\W*?i\\W*?n\\W*?d\\W*?i\\W*?n\\W*?g[^:]*?:\\W*?u\\W*?r\\W*?l[\\s\\S]*?\\(", + "options": { + "min_length": 6 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls", + "urlDecodeUni" + ] + }, + { + "id": "crs-941-180", + "name": "Node-Validator Deny List Keywords", + "tags": { + "type": "xss", + "crs_id": "941180", + "category": "attack_attempt", + "cwe": "79", + "capec": "1000/152/242/63/591", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "options": { + "enforce_word_boundary": true + }, + "list": [ + "document.cookie", + "document.write", + ".parentnode", + ".innerhtml", + "window.location", + "-moz-binding" + ] + }, + "operator": "phrase_match" + } + ], + "transformers": [ + "removeNulls", + "lowercase" + ] + }, + { + "id": "crs-941-200", + "name": "IE XSS Filters - Attack Detected via vmlframe tag", + "tags": { + "type": "xss", + "crs_id": "941200", + "category": "attack_attempt", + "cwe": "80", + "capec": "1000/152/242/63/591", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?i:<.*[:]?vmlframe.*?[\\s/+]*?src[\\s/+]*=)", + "options": { + "case_sensitive": true, + "min_length": 13 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls" + ] + }, + { + "id": "crs-941-210", + "name": "IE XSS Filters - Obfuscated Attack Detected via javascript injection", + "tags": { + "type": "xss", + "crs_id": "941210", + "category": "attack_attempt", + "cwe": "80", + "capec": "1000/152/242/63/591", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?i:(?:j|&#x?0*(?:74|4A|106|6A);?)(?:\\t|\\n|\\r|&(?:#x?0*(?:9|13|10|A|D);?|tab;|newline;))*(?:a|&#x?0*(?:65|41|97|61);?)(?:\\t|\\n|\\r|&(?:#x?0*(?:9|13|10|A|D);?|tab;|newline;))*(?:v|&#x?0*(?:86|56|118|76);?)(?:\\t|\\n|\\r|&(?:#x?0*(?:9|13|10|A|D);?|tab;|newline;))*(?:a|&#x?0*(?:65|41|97|61);?)(?:\\t|\\n|\\r|&(?:#x?0*(?:9|13|10|A|D);?|tab;|newline;))*(?:s|&#x?0*(?:83|53|115|73);?)(?:\\t|\\n|\\r|&(?:#x?0*(?:9|13|10|A|D);?|tab;|newline;))*(?:c|&#x?0*(?:67|43|99|63);?)(?:\\t|\\n|\\r|&(?:#x?0*(?:9|13|10|A|D);?|tab;|newline;))*(?:r|&#x?0*(?:82|52|114|72);?)(?:\\t|\\n|\\r|&(?:#x?0*(?:9|13|10|A|D);?|tab;|newline;))*(?:i|&#x?0*(?:73|49|105|69);?)(?:\\t|\\n|\\r|&(?:#x?0*(?:9|13|10|A|D);?|tab;|newline;))*(?:p|&#x?0*(?:80|50|112|70);?)(?:\\t|\\n|\\r|&(?:#x?0*(?:9|13|10|A|D);?|tab;|newline;))*(?:t|&#x?0*(?:84|54|116|74);?)(?:\\t|\\n|\\r|&(?:#x?0*(?:9|13|10|A|D);?|tab;|newline;))*(?::|&(?:#x?0*(?:58|3A);?|colon;)).)", + "options": { + "case_sensitive": true, + "min_length": 12 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls" + ] + }, + { + "id": "crs-941-220", + "name": "IE XSS Filters - Obfuscated Attack Detected via vbscript injection", + "tags": { + "type": "xss", + "crs_id": "941220", + "category": "attack_attempt", + "cwe": "80", + "capec": "1000/152/242/63/591", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?i:(?:v|&#x?0*(?:86|56|118|76);?)(?:\\t|&(?:#x?0*(?:9|13|10|A|D);?|tab;|newline;))*(?:b|&#x?0*(?:66|42|98|62);?)(?:\\t|&(?:#x?0*(?:9|13|10|A|D);?|tab;|newline;))*(?:s|&#x?0*(?:83|53|115|73);?)(?:\\t|&(?:#x?0*(?:9|13|10|A|D);?|tab;|newline;))*(?:c|&#x?0*(?:67|43|99|63);?)(?:\\t|&(?:#x?0*(?:9|13|10|A|D);?|tab;|newline;))*(?:r|&#x?0*(?:82|52|114|72);?)(?:\\t|&(?:#x?0*(?:9|13|10|A|D);?|tab;|newline;))*(?:i|&#x?0*(?:73|49|105|69);?)(?:\\t|&(?:#x?0*(?:9|13|10|A|D);?|tab;|newline;))*(?:p|&#x?0*(?:80|50|112|70);?)(?:\\t|&(?:#x?0*(?:9|13|10|A|D);?|tab;|newline;))*(?:t|&#x?0*(?:84|54|116|74);?)(?:\\t|&(?:#x?0*(?:9|13|10|A|D);?|tab;|newline;))*(?::|&(?:#x?0*(?:58|3A);?|colon;)).)", + "options": { + "case_sensitive": true, + "min_length": 10 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls" + ] + }, + { + "id": "crs-941-230", + "name": "IE XSS Filters - Attack Detected via embed tag", + "tags": { + "type": "xss", + "crs_id": "941230", + "category": "attack_attempt", + "cwe": "83", + "capec": "1000/152/242/63/591/243", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "]", + "options": { + "min_length": 8 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls" + ] + }, + { + "id": "crs-941-300", + "name": "IE XSS Filters - Attack Detected via object tag", + "tags": { + "type": "xss", + "crs_id": "941300", + "category": "attack_attempt", + "cwe": "83", + "capec": "1000/152/242/63/591/243", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": ")|<.*\\+AD4-", + "options": { + "case_sensitive": true, + "min_length": 6 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-941-360", + "name": "JSFuck / Hieroglyphy obfuscation detected", + "tags": { + "type": "xss", + "crs_id": "941360", + "category": "attack_attempt", + "cwe": "87", + "capec": "1000/152/242/63/591/199", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "![!+ ]\\[\\]", + "options": { + "case_sensitive": true, + "min_length": 4 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-941-390", + "name": "Javascript method detected", + "tags": { + "type": "xss", + "crs_id": "941390", + "category": "attack_attempt", + "confidence": "1", + "cwe": "79", + "capec": "1000/152/242/63/591", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "\\b(?i:eval|settimeout|setinterval|new\\s+Function|alert|prompt)[\\s+]*\\([^\\)]", + "options": { + "case_sensitive": true, + "min_length": 5 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-942-100", + "name": "SQL Injection Attack Detected via libinjection", + "tags": { + "type": "sql_injection", + "crs_id": "942100", + "category": "attack_attempt", + "cwe": "89", + "capec": "1000/152/248/66", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ] + }, + "operator": "is_sqli" + } + ], + "transformers": [ + "removeNulls" + ] + }, + { + "id": "crs-942-160", + "name": "Detects blind sqli tests using sleep() or benchmark()", + "tags": { + "type": "sql_injection", + "crs_id": "942160", + "category": "attack_attempt", + "cwe": "89", + "capec": "1000/152/248/66/7", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?i:sleep\\(\\s*?\\d*?\\s*?\\)|benchmark\\(.*?\\,.*?\\))", + "options": { + "case_sensitive": true, + "min_length": 7 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-942-240", + "name": "Detects MySQL charset switch and MSSQL DoS attempts", + "tags": { + "type": "sql_injection", + "crs_id": "942240", + "category": "attack_attempt", + "cwe": "89", + "capec": "1000/152/248/66/7", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?:[\\\"'`](?:;*?\\s*?waitfor\\s+(?:delay|time)\\s+[\\\"'`]|;.*?:\\s*?goto)|alter\\s*?\\w+.*?cha(?:racte)?r\\s+set\\s+\\w+)", + "options": { + "min_length": 7 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-942-250", + "name": "Detects MATCH AGAINST, MERGE and EXECUTE IMMEDIATE injections", + "tags": { + "type": "sql_injection", + "crs_id": "942250", + "category": "attack_attempt", + "cwe": "89", + "capec": "1000/152/248/66", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?i:merge.*?using\\s*?\\(|execute\\s*?immediate\\s*?[\\\"'`]|match\\s*?[\\w(?:),+-]+\\s*?against\\s*?\\()", + "options": { + "case_sensitive": true, + "min_length": 11 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-942-270", + "name": "Basic SQL injection", + "tags": { + "type": "sql_injection", + "crs_id": "942270", + "category": "attack_attempt", + "cwe": "89", + "capec": "1000/152/248/66", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "union.*?select.*?from", + "options": { + "min_length": 15 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-942-280", + "name": "SQL Injection with delay functions", + "tags": { + "type": "sql_injection", + "crs_id": "942280", + "category": "attack_attempt", + "cwe": "89", + "capec": "1000/152/248/66/7", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?:;\\s*?shutdown\\s*?(?:[#;{]|\\/\\*|--)|waitfor\\s*?delay\\s?[\\\"'`]+\\s?\\d|select\\s*?pg_sleep)", + "options": { + "min_length": 10 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-942-290", + "name": "Finds basic MongoDB SQL injection attempts", + "tags": { + "type": "nosql_injection", + "crs_id": "942290", + "category": "attack_attempt", + "cwe": "943", + "capec": "1000/152/248/676", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?i:(?:\\[?\\$(?:(?:s(?:lic|iz)|wher)e|e(?:lemMatch|xists|q)|n(?:o[rt]|in?|e)|l(?:ike|te?)|t(?:ext|ype)|a(?:ll|nd)|jsonSchema|between|regex|x?or|div|mod)\\]?)\\b)", + "options": { + "case_sensitive": true, + "min_length": 3 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "keys_only" + ] + }, + { + "id": "crs-942-360", + "name": "Detects concatenated basic SQL injection and SQLLFI attempts", + "tags": { + "type": "sql_injection", + "crs_id": "942360", + "category": "attack_attempt", + "cwe": "89", + "capec": "1000/152/248/66/470", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?:^[\\W\\d]+\\s*?(?:alter\\s*(?:a(?:(?:pplication\\s*rol|ggregat)e|s(?:ymmetric\\s*ke|sembl)y|u(?:thorization|dit)|vailability\\s*group)|c(?:r(?:yptographic\\s*provider|edential)|o(?:l(?:latio|um)|nversio)n|ertificate|luster)|s(?:e(?:rv(?:ice|er)|curity|quence|ssion|arch)|y(?:mmetric\\s*key|nonym)|togroup|chema)|m(?:a(?:s(?:ter\\s*key|k)|terialized)|e(?:ssage\\s*type|thod)|odule)|l(?:o(?:g(?:file\\s*group|in)|ckdown)|a(?:ngua|r)ge|ibrary)|t(?:(?:abl(?:espac)?|yp)e|r(?:igger|usted)|hreshold|ext)|p(?:a(?:rtition|ckage)|ro(?:cedur|fil)e|ermission)|d(?:i(?:mension|skgroup)|atabase|efault|omain)|r(?:o(?:l(?:lback|e)|ute)|e(?:sourc|mot)e)|f(?:u(?:lltext|nction)|lashback|oreign)|e(?:xte(?:nsion|rnal)|(?:ndpoi|ve)nt)|in(?:dex(?:type)?|memory|stance)|b(?:roker\\s*priority|ufferpool)|x(?:ml\\s*schema|srobject)|w(?:ork(?:load)?|rapper)|hi(?:erarchy|stogram)|o(?:perator|utline)|(?:nicknam|queu)e|us(?:age|er)|group|java|view)|union\\s*(?:(?:distin|sele)ct|all))\\b|\\b(?:(?:(?:trunc|cre|upd)at|renam)e|(?:inser|selec)t|de(?:lete|sc)|alter|load)\\s+(?:group_concat|load_file|char)\\b\\s*\\(?|[\\s(]load_file\\s*?\\(|[\\\"'`]\\s+regexp\\W)", + "options": { + "min_length": 5 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-942-500", + "name": "MySQL in-line comment detected", + "tags": { + "type": "sql_injection", + "crs_id": "942500", + "category": "attack_attempt", + "cwe": "89", + "capec": "1000/152/248/66", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?i:/\\*[!+](?:[\\w\\s=_\\-(?:)]+)?\\*/)", + "options": { + "case_sensitive": true, + "min_length": 5 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-943-100", + "name": "Possible Session Fixation Attack: Setting Cookie Values in HTML", + "tags": { + "type": "http_protocol_violation", + "crs_id": "943100", + "category": "attack_attempt", + "cwe": "384", + "capec": "1000/225/21/593/61", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?i:\\.cookie\\b.*?;\\W*?(?:expires|domain)\\W*?=|\\bhttp-equiv\\W+set-cookie\\b)", + "options": { + "case_sensitive": true, + "min_length": 15 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-944-100", + "name": "Remote Command Execution: Suspicious Java class detected", + "tags": { + "type": "java_code_injection", + "crs_id": "944100", + "category": "attack_attempt", + "cwe": "94", + "capec": "1000/152/242", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "java\\.lang\\.(?:runtime|processbuilder)", + "options": { + "case_sensitive": true, + "min_length": 17 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-944-110", + "name": "Remote Command Execution: Java process spawn (CVE-2017-9805)", + "tags": { + "type": "java_code_injection", + "category": "attack_attempt", + "cwe": "94", + "capec": "1000/152/242", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?:unmarshaller|base64data|java\\.).*(?:runtime|processbuilder)", + "options": { + "case_sensitive": false, + "min_length": 13 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-944-130", + "name": "Suspicious Java class detected", + "tags": { + "type": "java_code_injection", + "crs_id": "944130", + "category": "attack_attempt", + "cwe": "94", + "capec": "1000/152/242", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "list": [ + "com.opensymphony.xwork2", + "com.sun.org.apache", + "java.io.bufferedinputstream", + "java.io.bufferedreader", + "java.io.bytearrayinputstream", + "java.io.bytearrayoutputstream", + "java.io.chararrayreader", + "java.io.datainputstream", + "java.io.file", + "java.io.fileoutputstream", + "java.io.filepermission", + "java.io.filewriter", + "java.io.filterinputstream", + "java.io.filteroutputstream", + "java.io.filterreader", + "java.io.inputstream", + "java.io.inputstreamreader", + "java.io.linenumberreader", + "java.io.objectoutputstream", + "java.io.outputstream", + "java.io.pipedoutputstream", + "java.io.pipedreader", + "java.io.printstream", + "java.io.pushbackinputstream", + "java.io.reader", + "java.io.stringreader", + "java.lang.class", + "java.lang.integer", + "java.lang.number", + "java.lang.object", + "java.lang.process", + "java.lang.reflect", + "java.lang.runtime", + "java.lang.string", + "java.lang.stringbuilder", + "java.lang.system", + "javax.script.scriptenginemanager", + "org.apache.commons", + "org.apache.struts", + "org.apache.struts2", + "org.omg.corba", + "java.beans.xmldecode" + ], + "options": { + "enforce_word_boundary": true + } + }, + "operator": "phrase_match" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-944-260", + "name": "Remote Command Execution: Malicious class-loading payload", + "tags": { + "type": "java_code_injection", + "crs_id": "944260", + "category": "attack_attempt", + "cwe": "94", + "capec": "1000/152/242", + "confidence": "1" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?:class\\.module\\.classLoader\\.resources\\.context\\.parent\\.pipeline|springframework\\.context\\.support\\.FileSystemXmlApplicationContext)", + "options": { + "case_sensitive": true, + "min_length": 58 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-000-001", + "name": "Look for Cassandra injections", + "tags": { + "type": "nosql_injection", + "category": "attack_attempt", + "cwe": "943", + "capec": "1000/152/248/676", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + }, + { + "address": "server.request.headers.no_cookies" + } + ], + "regex": "\\ballow\\s+filtering\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeComments" + ] + }, + { + "id": "dog-000-002", + "name": "OGNL - Look for formatting injection patterns", + "tags": { + "type": "java_code_injection", + "category": "attack_attempt", + "cwe": "94", + "capec": "1000/152/242", + "module": "waf" + }, + "conditions": [ + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.request.uri.raw" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + }, + { + "address": "server.request.headers.no_cookies" + } + ], + "regex": "[#%$]{(?:[^}]+[^\\w\\s}\\-_][^}]+|\\d+-\\d+)}", + "options": { + "case_sensitive": true + } + } + } + ], + "transformers": [] + }, + { + "id": "dog-000-003", + "name": "OGNL - Detect OGNL exploitation primitives", + "tags": { + "type": "java_code_injection", + "category": "attack_attempt", + "cwe": "94", + "capec": "1000/152/242", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "[@#]ognl", + "options": { + "case_sensitive": true + } + } + } + ], + "transformers": [] + }, + { + "id": "dog-000-004", + "name": "Spring4Shell - Attempts to exploit the Spring4shell vulnerability", + "tags": { + "type": "exploit_detection", + "category": "attack_attempt", + "cwe": "94", + "capec": "1000/152/242", + "confidence": "1" + }, + "conditions": [ + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.request.body" + } + ], + "regex": "^class\\.module\\.classLoader\\.", + "options": { + "case_sensitive": false + } + } + } + ], + "transformers": [ + "keys_only" + ] + }, + { + "id": "dog-000-005", + "name": "Node.js: Prototype pollution through __proto__", + "tags": { + "type": "js_code_injection", + "category": "attack_attempt", + "cwe": "1321", + "capec": "1000/152/242", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + } + ], + "regex": "^__proto__$" + }, + "operator": "match_regex" + } + ], + "transformers": [ + "keys_only" + ] + }, + { + "id": "dog-000-006", + "name": "Node.js: Prototype pollution through constructor.prototype", + "tags": { + "type": "js_code_injection", + "category": "attack_attempt", + "cwe": "1321", + "capec": "1000/152/242", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + } + ], + "regex": "^constructor$" + }, + "operator": "match_regex" + }, + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + } + ], + "regex": "^prototype$" + }, + "operator": "match_regex" + } + ], + "transformers": [ + "keys_only" + ] + }, + { + "id": "dog-000-007", + "name": "Server side template injection: Velocity & Freemarker", + "tags": { + "type": "java_code_injection", + "category": "attack_attempt", + "cwe": "1336", + "capec": "1000/152/242/19", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "#(?:set|foreach|macro|parse|if)\\(.*\\)|<#assign.*>" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-68x", + "name": "xorbot", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "xorbot", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "\\bmasjesu\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-913-001", + "name": "BurpCollaborator OOB domain", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "tool_name": "BurpCollaborator", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "\\b(?:burpcollaborator\\.net|oastify\\.com)\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-913-002", + "name": "Qualys OOB domain", + "tags": { + "type": "commercial_scanner", + "category": "attack_attempt", + "tool_name": "Qualys", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "\\bqualysperiscope\\.com\\b|\\.oscomm\\." + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-913-003", + "name": "Probely OOB domain", + "tags": { + "type": "commercial_scanner", + "category": "attack_attempt", + "tool_name": "Probely", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "\\bprbly\\.win\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-913-004", + "name": "Known malicious out-of-band interaction domain", + "tags": { + "type": "security_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "\\b(?:webhook\\.site|\\.canarytokens\\.com|vii\\.one|act1on3\\.ru|gdsburp\\.com|arcticwolf\\.net|oob\\.li|htbiw\\.com|h4\\.vc|mochan\\.cloud|imshopping\\.com|bootstrapnodejs\\.com|mooo-ng\\.com|securitytrails\\.com|canyouhackit\\.io|7bae\\.xyz)\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-913-005", + "name": "Known suspicious out-of-band interaction domain", + "tags": { + "type": "security_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "\\b(?:\\.ngrok\\.io|requestbin\\.com|requestbin\\.net)\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-913-006", + "name": "Rapid7 OOB domain", + "tags": { + "type": "commercial_scanner", + "category": "attack_attempt", + "tool_name": "Rapid7", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "\\bappspidered\\.rapid7\\." + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-913-007", + "name": "Interact.sh OOB domain", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "tool_name": "interact.sh", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "\\b(?:interact\\.sh|oast\\.(?:pro|live|site|online|fun|me)|indusfacefinder\\.in|where\\.land|syhunt\\.net|tssrt\\.de|boardofcyber\\.io|assetnote-callback\\.com|praetorianlabs\\.dev|netspi\\.sh)\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-913-008", + "name": "Netsparker OOB domain", + "tags": { + "type": "commercial_scanner", + "category": "attack_attempt", + "tool_name": "Netsparker", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "\\b(?:\\.|(?:\\\\|&#)(?:0*46|x0*2e);)?r87(?:\\.|(?:\\\\|&#)(?:0*46|x0*2e);)(?:me|com)\\b", + "options": { + "case_sensitive": false, + "min_length": 7 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-913-009", + "name": "WhiteHat Security OOB domain", + "tags": { + "type": "commercial_scanner", + "category": "attack_attempt", + "tool_name": "WhiteHatSecurity", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "\\bwhsec(?:\\.|(?:\\\\|&#)(?:0*46|x0*2e);)us\\b", + "options": { + "case_sensitive": false, + "min_length": 8 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-913-010", + "name": "Nessus OOB domain", + "tags": { + "type": "commercial_scanner", + "category": "attack_attempt", + "tool_name": "Nessus", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "\\b\\.nessus\\.org\\b", + "options": { + "case_sensitive": false, + "min_length": 8 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-913-011", + "name": "Watchtowr OOB domain", + "tags": { + "type": "commercial_scanner", + "category": "attack_attempt", + "tool_name": "Watchtowr", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "\\bwatchtowr\\.com\\b", + "options": { + "case_sensitive": false, + "min_length": 8 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-913-012", + "name": "AppCheck NG OOB domain", + "tags": { + "type": "commercial_scanner", + "category": "attack_attempt", + "tool_name": "AppCheckNG", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "\\bptst\\.io\\b", + "options": { + "case_sensitive": false, + "min_length": 7 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-913-013", + "name": "Public PoC for CVE-2025-24813", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.uri.raw" + } + ], + "regex": "/iSee857/session", + "options": { + "case_sensitive": false, + "min_length": 16 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-913-014", + "name": "Exploit attempt for Next.js Middleware Exploit (CVE-2025-29927)", + "tags": { + "type": "security_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "x-middleware-subrequest" + ] + } + ], + "regex": ".*", + "options": { + "min_length": 1 + } + }, + "operator": "match_regex" + }, + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "x-middleware-subrequest" + ] + } + ], + "regex": "[0-9a-fA-F]{40}|\\[\\w+\\]" + }, + "operator": "!match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-920-001", + "name": "JWT authentication bypass", + "tags": { + "type": "http_protocol_violation", + "category": "attack_attempt", + "cwe": "287", + "capec": "1000/225/115", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "authorization" + ] + } + ], + "regex": "^(?:Bearer )?ey[A-Za-z0-9+_\\-/]*([QY][UW]x[Hn]Ij([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gOiAi[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]Ij([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]IDogI[km]5[Pv][Tb][km][U-X]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]IiA6ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]IDoi[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]Ij([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gOiAi[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciOiAi[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*IDogI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]IDogI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yIgO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciIDoi[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*IDogI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yIgOiJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]IDoi[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]IDogI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yI6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yI6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]IiA6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*IDogI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yIgO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]Ij([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciOiJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*IDoi[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gOiJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yIgOiAi[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]IDoi[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]IjogI[km]5[Pv][Tb][km][U-X]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]IiA6I[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6I[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yI6I[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yI6ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciIDogI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[QY][UW]x[Hn]IiA6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]IiA6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*IDoi[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ciO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6I[km]5[Pv][Tb][km][U-X]|[QY][UW]x[Hn]IiA6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yI6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yIgO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gOiJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ID([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gI[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yIgO([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[\\x2b\\x2f-9A-Za-z]ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*ICJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]I([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*IDoi[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]A6I[km]5[Pv][Tb][km][U-X]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]y([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gOiJ[Ou][Tb][02]5[Fl]|[QY][UW]x[Hn]Ijoi[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx][Bh][Tb][EG]ci([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[048AEIMQUYcgkosw]gOiAi[Tb][km]9[Ou][RZ][Q-Za-f]|[\\x2b\\x2f-9A-Za-z][02EGUWkm]F[Ms][RZ]yI6([048ACEIMQSUYcgikoswy]|[\\x2b\\x2f-9A-Za-z]I)*[CSiy]Ai[Tb][km]9[Ou][RZ][Q-Za-f])[A-Za-z0-9+-/]*\\.[A-Za-z0-9+_\\-/]+\\.(?:[A-Za-z0-9+_\\-/]+)?$", + "options": { + "case_sensitive": true + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-931-001", + "name": "RFI: URL Payload to well known RFI target", + "tags": { + "type": "rfi", + "category": "attack_attempt", + "cwe": "98", + "capec": "1000/152/175/253/193", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "^(?i:file|ftps?|https?).*/rfiinc\\.txt\\?+$", + "options": { + "case_sensitive": true, + "min_length": 17 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-932-100", + "name": "Shell spawn executing network command", + "tags": { + "type": "command_injection", + "category": "attack_attempt", + "cwe": "77", + "capec": "1000/152/248/88", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?:(?:['\"\\x60({|;&]|(?:^|['\"\\x60({|;&])(?:cmd(?:\\.exe)?\\s+(?:/\\w(?::\\w+)?\\s+)*))(?:ping|curl|wget|telnet)|\\bnslookup)[\\s,]", + "options": { + "case_sensitive": true, + "min_length": 5 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-932-110", + "name": "Python: Subprocess-based command injection", + "tags": { + "type": "command_injection", + "category": "attack_attempt", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?s)\\bsubprocess\\b.*\\b(?:check_output|run|Popen|call|check_call)\\b", + "options": { + "case_sensitive": true, + "min_length": 14 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-934-001", + "name": "XXE - XML file loads external entity", + "tags": { + "type": "xxe", + "category": "attack_attempt", + "cwe": "91", + "capec": "1000/152/248/250", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.body" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?:<\\?xml[^>]*>.*)]+SYSTEM\\s+[^>]+>", + "options": { + "case_sensitive": false, + "min_length": 24 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "dog-941-001", + "name": "XSS in source property", + "tags": { + "type": "xss", + "category": "attack_attempt", + "cwe": "83", + "capec": "1000/152/242/63/591/243", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "referer" + ] + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "<(?:iframe|esi:include)(?:(?:\\s|/)*\\w+=[\"'\\w]+)*(?:\\s|/)*src(?:doc)?=[\"']?(?:data:|javascript:|http:|dns:|//)[^\\s'\"]+['\"]?", + "options": { + "min_length": 14 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls", + "urlDecodeUni" + ] + }, + { + "id": "dog-942-001", + "name": "Blind XSS callback domains", + "tags": { + "type": "xss", + "category": "attack_attempt", + "cwe": "83", + "capec": "1000/152/242/63/591/243", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "https?:\\/\\/(?:.*\\.)?(?:bxss\\.(?:in|me)|xss\\.ht|js\\.rip)", + "options": { + "case_sensitive": false + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "nfd-000-001", + "name": "Detect common directory discovery scans", + "tags": { + "type": "security_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "1" + }, + "conditions": [ + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.response.status" + } + ], + "regex": "^404$", + "options": { + "case_sensitive": true + } + } + }, + { + "operator": "phrase_match", + "parameters": { + "options": { + "enforce_word_boundary": true + }, + "inputs": [ + { + "address": "server.request.uri.raw" + } + ], + "list": [ + "/wordpress/", + "/etc/", + "/login.php", + "/install.php", + "/administrator", + "/admin.php", + "/wp-config", + "/phpmyadmin", + "/fckeditor", + "/mysql", + "/manager/html", + ".htaccess", + "/config.php", + "/configuration", + "/cgi-bin/php", + "/search.php", + "/tinymce", + "/tiny_mce", + "/settings.php", + "../../..", + "/install/", + "/download.php", + "/webdav", + "/forum.php", + "/user.php", + "/style.php", + "/jmx-console", + "/modules.php", + "/include.php", + "/default.asp", + "/help.php", + "/database.yml", + "/database.yml.pgsql", + "/database.yml.sqlite3", + "/database.yml.sqlite", + "/database.yml.mysql", + ".%2e/", + "/view.php", + "/header.php", + "/search.asp", + "%5c%5c", + "/server/php/", + "/invoker/jmxinvokerservlet", + "/phpmyadmin/index.php", + "/data/admin/allowurl.txt", + "/verify.php", + "/misc/ajax.js", + "/.idea", + "/module.php", + "/backup.rar", + "/backup.tar", + "/backup.zip", + "/backup.7z", + "/backup.gz", + "/backup.tgz", + "/backup.tar.gz", + "waitfor%20delay", + "/calendar.php", + "/news.php", + "/dompdf.php", + "))))))))))))))))", + "/web.config", + "tree.php", + "/cgi-bin-sdb/printenv", + "/comments.php", + "/detail.asp", + "/license.txt", + "/admin.asp", + "/auth.php", + "/list.php", + "/content.php", + "/mod.php", + "/mini.php", + "/install.pgsql", + "/install.mysql", + "/install.sqlite", + "/install.sqlite3", + "/install.txt", + "/install.md", + "/doku.php", + "/main.asp", + "/myadmin", + "/force-download.php", + "/iisprotect/admin", + "/.gitignore", + "/print.php", + "/common.php", + "/mainfile.php", + "/functions.php", + "/scripts/setup.php", + "/faq.php", + "/op/op.login.php", + "/home.php", + "/includes/hnmain.inc.php3", + "/preview.php", + "/dump.rar", + "/dump.tar", + "/dump.zip", + "/dump.7z", + "/dump.gz", + "/dump.tgz", + "/dump.tar.gz", + "/thumbnail.php", + "/sendcard.php", + "/global.asax", + "/directory.php", + "/footer.php", + "/error.asp", + "/forum.asp", + "/save.php", + "/htmlsax3.php", + "/adm/krgourl.php", + "/includes/converter.inc.php", + "/nucleus/libs/pluginadmin.php", + "/base_qry_common.php", + "/fileadmin", + "/bitrix/admin/", + "/adm.php", + "/util/barcode.php", + "/action.php", + "/rss.asp", + "/downloads.php", + "/page.php", + "/snarf_ajax.php", + "/fck/editor", + "/sendmail.php", + "/detail.php", + "/iframe.php", + "/swfupload.swf", + "/jenkins/login", + "/phpmyadmin/main.php", + "/phpmyadmin/scripts/setup.php", + "/user/index.php", + "/checkout.php", + "/process.php", + "/ks_inc/ajax.js", + "/export.php", + "/register.php", + "/cart.php", + "/console.php", + "/friend.php", + "/readmsg.php", + "/install.asp", + "/dagent/downloadreport.asp", + "/system/index.php", + "/core/changelog.txt", + "/js/util.js", + "/interna.php", + "/gallery.php", + "/links.php", + "/data/admin/ver.txt", + "/language/zh-cn.xml", + "/productdetails.asp", + "/admin/template/article_more/config.htm", + "/components/com_moofaq/includes/file_includer.php", + "/licence.txt", + "/rss.xsl", + "/vtigerservice.php", + "/mysql/main.php", + "/passwiki.php", + "/scr/soustab.php", + "/global.php", + "/email.php", + "/user.asp", + "/msd", + "/products.php", + "/cultbooking.php", + "/cron.php", + "/static/js/admincp.js", + "/comment.php", + "/maintainers", + "/modules/plain/adminpart/addplain.php", + "/wp-content/plugins/ungallery/source_vuln.php", + "/upgrade.txt", + "/category.php", + "/index_logged.php", + "/members.asp", + "/script/html.js", + "/images/ad.js", + "/awstats/awstats.pl", + "/includes/esqueletos/skel_null.php", + "/modules/profile/user.php", + "/window_top.php", + "/openbrowser.php", + "/thread.php", + "tinfoil_xss", + "/includes/include.php", + "/urheber.php", + "/header.inc.php", + "/mysqldumper", + "/display.php", + "/website.php", + "/stats.php", + "/assets/plugins/mp3_id/mp3_id.php", + "/siteminderagent/forms/smpwservices.fcc", + "/eval-stdin.php" + ] + } + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "nfd-000-002", + "name": "Detect failed attempt to fetch readme files", + "tags": { + "type": "security_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.response.status" + } + ], + "regex": "^404$", + "options": { + "case_sensitive": true + } + } + }, + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.request.uri.raw" + } + ], + "regex": "readme\\.[\\.a-z0-9]+$", + "options": { + "case_sensitive": false + } + } + } + ], + "transformers": [] + }, + { + "id": "nfd-000-003", + "name": "Detect failed attempt to fetch Java EE resource files", + "tags": { + "type": "security_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.response.status" + } + ], + "regex": "^404$", + "options": { + "case_sensitive": true + } + } + }, + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.request.uri.raw" + } + ], + "regex": "^(?:.*web\\-inf)(?:.*web\\.xml).*$", + "options": { + "case_sensitive": false + } + } + } + ], + "transformers": [] + }, + { + "id": "nfd-000-004", + "name": "Detect failed attempt to fetch code files", + "tags": { + "type": "security_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.response.status" + } + ], + "regex": "^404$", + "options": { + "case_sensitive": true + } + } + }, + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.request.uri.raw" + } + ], + "regex": "\\.(java|pyc?|rb|class)\\b", + "options": { + "case_sensitive": false + } + } + } + ], + "transformers": [] + }, + { + "id": "nfd-000-005", + "name": "Detect failed attempt to fetch source code archives", + "tags": { + "type": "security_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.response.status" + } + ], + "regex": "^404$", + "options": { + "case_sensitive": true + } + } + }, + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.request.uri.raw" + } + ], + "regex": "\\.(sql|log|ndb|gz|zip|tar\\.gz|tar|regVV|reg|conf|bz2|ini|db|war|bat|inc|btr|server|ds|conf|config|admin|master|sln|bak)\\b(?:[^.]|$)", + "options": { + "case_sensitive": false + } + } + } + ], + "transformers": [] + }, + { + "id": "nfd-000-006", + "name": "Detect failed attempt to fetch sensitive files", + "tags": { + "type": "security_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.response.status" + } + ], + "regex": "^404$", + "options": { + "case_sensitive": true + } + } + }, + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.request.uri.raw" + } + ], + "regex": "\\.(cgi|bat|dll|exe|key|cert|crt|pem|der|pkcs|pkcs|pkcs[0-9]*|nsf|jsa|war|java|class|vb|vba|so|git|svn|hg|cvs)([?#&/]|$)", + "options": { + "case_sensitive": false + } + } + } + ], + "transformers": [] + }, + { + "id": "nfd-000-007", + "name": "Detect failed attempt to fetch archives", + "tags": { + "type": "security_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.response.status" + } + ], + "regex": "^404$", + "options": { + "case_sensitive": true + } + } + }, + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.request.uri.raw" + } + ], + "regex": "/[\\d\\-_]*\\.(rar|tar|zip|7z|gz|tgz|tar.gz)", + "options": { + "case_sensitive": false + } + } + } + ], + "transformers": [] + }, + { + "id": "nfd-000-008", + "name": "Detect failed attempt to trigger incorrect application behavior", + "tags": { + "type": "security_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.response.status" + } + ], + "regex": "^404$", + "options": { + "case_sensitive": true + } + } + }, + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.request.uri.raw" + } + ], + "regex": "(/(administrator/components/com.*\\.php|response\\.write\\(.+\\))|select\\(.+\\)from|\\(.*sleep\\(.+\\)|(%[a-zA-Z0-9]{2}[a-zA-Z]{0,1})+\\))", + "options": { + "case_sensitive": false + } + } + } + ], + "transformers": [] + }, + { + "id": "nfd-000-009", + "name": "Detect failed attempt to leak the structure of the application", + "tags": { + "type": "security_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.response.status" + } + ], + "regex": "^404$", + "options": { + "case_sensitive": true + } + } + }, + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.request.uri.raw" + } + ], + "regex": "/(login\\.rol|LICENSE|[\\w-]+\\.(plx|pwd))$", + "options": { + "case_sensitive": false + } + } + } + ], + "transformers": [] + }, + { + "id": "nfd-000-010", + "name": "Detect failed attempts to find API documentation", + "tags": { + "type": "security_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.response.status" + } + ], + "regex": "^404$", + "options": { + "case_sensitive": true + } + } + }, + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.request.uri.raw" + } + ], + "regex": "(?:^|/)(?:swagger|api[-/]?docs?|openapi)\\b", + "options": { + "case_sensitive": false + } + } + } + ], + "transformers": [] + }, + { + "id": "rasp-930-100", + "name": "Local file inclusion exploit", + "tags": { + "type": "lfi", + "category": "vulnerability_trigger", + "cwe": "22", + "capec": "1000/255/153/126", + "confidence": "1", + "module": "rasp" + }, + "conditions": [ + { + "parameters": { + "resource": [ + { + "address": "server.io.fs.file" + } + ], + "params": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ] + }, + "operator": "lfi_detector@v2" + } + ], + "transformers": [], + "on_match": [ + "stack_trace" + ] + }, + { + "id": "rasp-932-100", + "name": "Shell command injection exploit", + "tags": { + "type": "command_injection", + "category": "vulnerability_trigger", + "cwe": "77", + "capec": "1000/152/248/88", + "confidence": "1", + "module": "rasp" + }, + "conditions": [ + { + "parameters": { + "resource": [ + { + "address": "server.sys.shell.cmd" + } + ], + "params": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ] + }, + "operator": "shi_detector" + } + ], + "transformers": [], + "on_match": [ + "stack_trace" + ] + }, + { + "id": "rasp-932-110", + "name": "OS command injection exploit", + "tags": { + "type": "command_injection", + "category": "vulnerability_trigger", + "cwe": "77", + "capec": "1000/152/248/88", + "confidence": "1", + "module": "rasp" + }, + "conditions": [ + { + "parameters": { + "resource": [ + { + "address": "server.sys.exec.cmd" + } + ], + "params": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ] + }, + "operator": "cmdi_detector" + } + ], + "transformers": [], + "on_match": [ + "stack_trace" + ] + }, + { + "id": "rasp-934-100", + "name": "Server-side request forgery exploit", + "tags": { + "type": "ssrf", + "category": "vulnerability_trigger", + "cwe": "918", + "capec": "1000/225/115/664", + "confidence": "0", + "module": "rasp" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.io.net.url" + } + ], + "regex": "^(jar:)?https?:\\/\\/\\W*([0-9oq]{1,5}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}|[0-9]{1,10}|(\\[)?[:0-9a-f\\.x]{2,}(\\])?|metadata\\.google\\.internal|(?:[a-z0-9:@\\.\\-]*\\.)?(?:burpcollaborator\\.net|localtest\\.me|mail\\.ebc\\.apple\\.com|bugbounty\\.dod\\.network|.*\\.[nx]ip\\.io|oastify\\.com|oast\\.(?:pro|live|site|online|fun|me)|sslip\\.io|requestbin\\.com|requestbin\\.net|hookbin\\.com|webhook\\.site|canarytokens\\.com|interact\\.sh|ngrok\\.io|bugbounty\\.click|prbly\\.win|qualysperiscope\\.com|vii\\.one|act1on3\\.ru|ifconfig\\.pro|dnslog\\.\\w+))(:[0-9]{1,5})?(\\/[^:@]*)?$", + "options": { + "case_sensitive": false + } + }, + "operator": "match_regex" + }, + { + "parameters": { + "resource": [ + { + "address": "server.io.net.url" + } + ], + "params": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ] + }, + "operator": "ssrf_detector" + } + ], + "transformers": [], + "on_match": [ + "stack_trace" + ] + }, + { + "id": "rasp-942-100", + "name": "SQL injection exploit", + "tags": { + "type": "sql_injection", + "category": "vulnerability_trigger", + "cwe": "89", + "capec": "1000/152/248/66", + "confidence": "1", + "module": "rasp" + }, + "conditions": [ + { + "parameters": { + "resource": [ + { + "address": "server.db.statement" + } + ], + "params": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "db_type": [ + { + "address": "server.db.system" + } + ] + }, + "operator": "sqli_detector@v2" + } + ], + "transformers": [], + "on_match": [ + "stack_trace" + ] + }, + { + "id": "sqr-000-001", + "name": "SSRF: Try to access the credential manager of the main cloud services", + "tags": { + "type": "ssrf", + "category": "attack_attempt", + "cwe": "918", + "capec": "1000/225/115/664", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?i)^\\W*((http|ftp)s?://)?\\W*((::f{4}:)?(169|(0x)?0*a9|0+251)\\.?(254|(0x)?0*fe|0+376)[0-9a-fx\\.:]+|metadata\\.google\\.internal|metadata\\.goog)\\W*/", + "options": { + "min_length": 4 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls" + ] + }, + { + "id": "sqr-000-002", + "name": "Server-side Javascript injection: Try to detect obvious JS injection", + "tags": { + "type": "js_code_injection", + "category": "attack_attempt", + "cwe": "94", + "capec": "1000/152/242", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "require\\(['\"][\\w\\.]+['\"]\\)|process\\.\\w+\\([\\w\\.]*\\)|\\.toString\\(\\)", + "options": { + "min_length": 4 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls" + ] + }, + { + "id": "sqr-000-008", + "name": "Windows: Detect attempts to exfiltrate .ini files", + "tags": { + "type": "command_injection", + "category": "attack_attempt", + "cwe": "78", + "capec": "1000/152/248/88", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?i)[&|]\\s*type\\s+%\\w+%\\\\+\\w+\\.ini\\s*[&|]" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "sqr-000-009", + "name": "Linux: Detect attempts to exfiltrate passwd files", + "tags": { + "type": "command_injection", + "category": "attack_attempt", + "cwe": "78", + "capec": "1000/152/248/88", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?i)[&|]\\s*cat\\s*\\/etc\\/[\\w\\.\\/]*passwd\\s*[&|]" + }, + "operator": "match_regex" + } + ], + "transformers": [ + "cmdLine" + ] + }, + { + "id": "sqr-000-010", + "name": "Windows: Detect attempts to timeout a shell", + "tags": { + "type": "command_injection", + "category": "attack_attempt", + "cwe": "78", + "capec": "1000/152/248/88", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(?i)[&|]\\s*timeout\\s+/t\\s+\\d+\\s*[&|]" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "sqr-000-011", + "name": "SSRF: Try to access internal OMI service (CVE-2021-38647)", + "tags": { + "type": "ssrf", + "category": "attack_attempt", + "cwe": "918", + "capec": "1000/225/115/664", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "http(s?):\\/\\/([A-Za-z0-9\\.\\-\\_]+|\\[[A-Fa-f0-9\\:]+\\]|):5986\\/wsman", + "options": { + "min_length": 4 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "sqr-000-012", + "name": "SSRF: Detect SSRF attempt on internal service", + "tags": { + "type": "ssrf", + "category": "attack_attempt", + "cwe": "918", + "capec": "1000/225/115/664", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "^(jar:)?(http|https):\\/\\/([0-9oq]{1,5}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}|[0-9]{1,10})(:[0-9]{1,5})?(\\/[^:@]*)?$" + }, + "operator": "match_regex" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "sqr-000-013", + "name": "SSRF: Detect SSRF attempts using IPv6 or octal/hexdecimal obfuscation", + "tags": { + "type": "ssrf", + "category": "attack_attempt", + "cwe": "918", + "capec": "1000/225/115/664", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "^(jar:)?(http|https):\\/\\/((\\[)?[:0-9a-f\\.x]{2,}(\\])?)(:[0-9]{1,5})?(\\/[^:@]*)?$" + }, + "operator": "match_regex" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "sqr-000-014", + "name": "SSRF: Detect SSRF domain redirection bypass", + "tags": { + "type": "ssrf", + "category": "attack_attempt", + "cwe": "918", + "capec": "1000/225/115/664", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "(http|https):\\/\\/(?:.*\\.)?(?:burpcollaborator\\.net|localtest\\.me|mail\\.ebc\\.apple\\.com|bugbounty\\.dod\\.network|.*\\.[nx]ip\\.io|oastify\\.com|oast\\.(?:pro|live|site|online|fun|me)|sslip\\.io|requestbin\\.com|requestbin\\.net|hookbin\\.com|webhook\\.site|canarytokens\\.com|interact\\.sh|ngrok\\.io|bugbounty\\.click|prbly\\.win|qualysperiscope\\.com|vii\\.one|act1on3\\.ru|dnslog\\.\\w+)" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "sqr-000-015", + "name": "SSRF: Detect SSRF attempt using non HTTP protocol", + "tags": { + "type": "ssrf", + "category": "attack_attempt", + "cwe": "918", + "capec": "1000/225/115/664", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "^(jar:)?((file|netdoc):\\/\\/[\\\\\\/]+|(dict|gopher|ldap|sftp|tftp):\\/\\/.*:[0-9]{1,5})" + }, + "operator": "match_regex" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "sqr-000-017", + "name": "Log4shell: Attempt to exploit log4j CVE-2021-44228", + "tags": { + "type": "exploit_detection", + "category": "attack_attempt", + "cwe": "94", + "capec": "1000/152/242", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.uri.raw" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "regex": "\\${[^j]*j[^n]*n[^d]*d[^i]*i[^:]*:[^}]*}" + }, + "operator": "match_regex" + } + ], + "transformers": [ + "unicode_normalize" + ] + }, + { + "id": "ua0-600-0xx", + "name": "Joomla exploitation tool", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Joomla exploitation tool", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "JDatabaseDriverMysqli" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-10x", + "name": "Nessus", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Nessus", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)^Nessus(/|([ :]+SOAP))" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-12x", + "name": "Arachni", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Arachni", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "^Arachni\\/v" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-13x", + "name": "Jorgee", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Jorgee", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bJorgee\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-14x", + "name": "Probely", + "tags": { + "type": "commercial_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Probely", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bProbely\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-15x", + "name": "Metis", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Metis", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bmetis\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-16x", + "name": "SQL power injector", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "SQLPowerInjector", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "sql power injector" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-18x", + "name": "N-Stealth", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "N-Stealth", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bn-stealth\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-19x", + "name": "Brutus", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Brutus", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bbrutus\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-1xx", + "name": "Shellshock exploitation tool", + "tags": { + "type": "security_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "\\(\\) \\{ :; *\\}" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-20x", + "name": "Netsparker", + "tags": { + "type": "commercial_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Netsparker", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "\\bnetsparker\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-22x", + "name": "JAASCois", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "JAASCois", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bjaascois\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-26x", + "name": "Nsauditor", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Nsauditor", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bnsauditor\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-27x", + "name": "Paros", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Paros", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)Mozilla/.* Paros/" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-28x", + "name": "DirBuster", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "DirBuster", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bdirbuster\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-29x", + "name": "Pangolin", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Pangolin", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bpangolin\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-2xx", + "name": "Qualys", + "tags": { + "type": "commercial_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Qualys", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bqualys\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-30x", + "name": "SQLNinja", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "SQLNinja", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bsqlninja\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-31x", + "name": "Nikto", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Nikto", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "\\(Nikto/[\\d\\.]+\\)" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-33x", + "name": "BlackWidow", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "BlackWidow", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bblack\\s?widow\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-34x", + "name": "Grendel-Scan", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Grendel-Scan", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bgrendel-scan\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-35x", + "name": "Havij", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Havij", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bhavij\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-36x", + "name": "w3af", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "w3af", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bw3af\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-37x", + "name": "Nmap", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Nmap", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "nmap (nse|scripting engine|icap-client/)" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-39x", + "name": "Nessus Scripted", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Nessus", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)^'?[a-z0-9_]+\\.nasl'?$" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-3xx", + "name": "Evil Scanner", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "EvilScanner", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bevilScanner\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-40x", + "name": "WebFuck", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "WebFuck", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bWebFuck\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-42x", + "name": "OpenVAS", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "OpenVAS", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)OpenVAS\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-43x", + "name": "Spider-Pig", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Spider-Pig", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "Powered by Spider-Pig by tinfoilsecurity\\.com" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-44x", + "name": "Zgrab", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Zgrab", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "Mozilla/\\d+.\\d+ zgrab" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-45x", + "name": "Zmeu", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Zmeu", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bZmEu\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-47x", + "name": "GoogleSecurityScanner", + "tags": { + "type": "commercial_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "GoogleSecurityScanner", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bGoogleSecurityScanner\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-48x", + "name": "Commix", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Commix", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "^commix\\/" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-49x", + "name": "Gobuster", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Gobuster", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "^gobuster\\/" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-4xx", + "name": "CGIchk", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "CGIchk", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bcgichk\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-51x", + "name": "FFUF", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "FFUF", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)^Fuzz Faster U Fool\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-52x", + "name": "Nuclei", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Nuclei", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)^Nuclei\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-53x", + "name": "Tsunami", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Tsunami", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bTsunamiSecurityScanner\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-54x", + "name": "Nimbostratus", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Nimbostratus", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bnimbostratus-bot\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-55x", + "name": "Datadog test scanner: user-agent", + "tags": { + "type": "security_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Datadog Canary Test", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + }, + { + "address": "grpc.server.request.metadata", + "key_path": [ + "dd-canary" + ] + } + ], + "regex": "^dd-test-scanner-log(?:$|/|\\s)" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-56x", + "name": "Datadog test scanner - blocking version: user-agent", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Datadog Canary Test", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + }, + { + "address": "grpc.server.request.metadata", + "key_path": [ + "dd-canary" + ] + } + ], + "regex": "^dd-test-scanner-log-block(?:$|/|\\s)" + }, + "operator": "match_regex" + } + ], + "transformers": [], + "on_match": [ + "block" + ] + }, + { + "id": "ua0-600-57x", + "name": "AlertLogic", + "tags": { + "type": "commercial_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "AlertLogic", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "\\bAlertLogic-MDR-" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-58x", + "name": "wfuzz", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "wfuzz", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "\\bwfuzz\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-59x", + "name": "Detectify", + "tags": { + "type": "commercial_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Detectify", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "\\bdetectify\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-5xx", + "name": "Blind SQL Injection Brute Forcer", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "BSQLBF", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bbsqlbf\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-60x", + "name": "masscan", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "masscan", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "^masscan/" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-61x", + "name": "WPScan", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "WPScan", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "^wpscan\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-62x", + "name": "Aon pentesting services", + "tags": { + "type": "commercial_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Aon", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "^Aon/" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-63x", + "name": "FeroxBuster", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "feroxbuster", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "^feroxbuster/" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-64x", + "name": "ddg_win", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "ddg_win", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "\\bddg_win\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-65x", + "name": "ISS", + "tags": { + "type": "commercial_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "iss", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "\\bisscyberriskcrawler/\\d\\.\\d" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-66x", + "name": "BountyBot", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "bountybot", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "\\bbountybot\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-67x", + "name": "ZumBot", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "zumbot", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "\\bzumbot\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-6xx", + "name": "Stealthy scanner", + "tags": { + "type": "security_scanner", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "mozilla/4\\.0 \\(compatible(; msie (?:6\\.0; (?:win32|Windows NT 5\\.0)|4\\.0; Windows NT))?\\)", + "options": { + "case_sensitive": false + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-7xx", + "name": "SQLmap", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "SQLmap", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "^sqlmap/" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-9xx", + "name": "Skipfish", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "cwe": "200", + "capec": "1000/118/169", + "tool_name": "Skipfish", + "confidence": "1", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)mozilla/5\\.0 sf/" + }, + "operator": "match_regex" + } + ], + "transformers": [] + } + ], + "processors": [ + { + "id": "http-endpoint-fingerprint", + "generator": "http_endpoint_fingerprint", + "conditions": [], + "parameters": { + "mappings": [ + { + "method": [ + { + "address": "server.request.method" + } + ], + "uri_raw": [ + { + "address": "server.request.uri.raw" + } + ], + "body": [ + { + "address": "server.request.body" + } + ], + "query": [ + { + "address": "server.request.query" + } + ], + "output": "_dd.appsec.fp.http.endpoint" + } + ] + }, + "evaluate": true, + "output": true + }, + { + "id": "extract-content", + "generator": "extract_schema", + "conditions": [ + { + "operator": "equals", + "parameters": { + "inputs": [ + { + "address": "waf.context.processor", + "key_path": [ + "extract-schema" + ] + } + ], + "type": "boolean", + "value": true + } + } + ], + "parameters": { + "mappings": [ + { + "inputs": [ + { + "address": "server.request.body" + } + ], + "output": "_dd.appsec.s.req.body" + }, + { + "inputs": [ + { + "address": "server.request.cookies" + } + ], + "output": "_dd.appsec.s.req.cookies" + }, + { + "inputs": [ + { + "address": "server.request.query" + } + ], + "output": "_dd.appsec.s.req.query" + }, + { + "inputs": [ + { + "address": "server.request.path_params" + } + ], + "output": "_dd.appsec.s.req.params" + }, + { + "inputs": [ + { + "address": "server.response.body" + } + ], + "output": "_dd.appsec.s.res.body" + }, + { + "inputs": [ + { + "address": "graphql.server.all_resolvers" + } + ], + "output": "_dd.appsec.s.graphql.all_resolvers" + }, + { + "inputs": [ + { + "address": "graphql.server.resolver" + } + ], + "output": "_dd.appsec.s.graphql.resolver" + } + ], + "scanners": [ + { + "tags": { + "category": "payment" + } + }, + { + "tags": { + "category": "pii" + } + } + ] + }, + "evaluate": false, + "output": true + }, + { + "id": "extract-headers", + "generator": "extract_schema", + "conditions": [ + { + "operator": "equals", + "parameters": { + "inputs": [ + { + "address": "waf.context.processor", + "key_path": [ + "extract-schema" + ] + } + ], + "type": "boolean", + "value": true + } + } + ], + "parameters": { + "mappings": [ + { + "inputs": [ + { + "address": "server.request.headers.no_cookies" + } + ], + "output": "_dd.appsec.s.req.headers" + }, + { + "inputs": [ + { + "address": "server.response.headers.no_cookies" + } + ], + "output": "_dd.appsec.s.res.headers" + } + ], + "scanners": [ + { + "tags": { + "category": "credentials" + } + }, + { + "tags": { + "category": "pii" + } + } + ] + }, + "evaluate": false, + "output": true + }, + { + "id": "http-header-fingerprint", + "generator": "http_header_fingerprint", + "conditions": [], + "parameters": { + "mappings": [ + { + "headers": [ + { + "address": "server.request.headers.no_cookies" + } + ], + "output": "_dd.appsec.fp.http.header" + } + ] + }, + "evaluate": true, + "output": true + }, + { + "id": "decode-auth-jwt", + "generator": "jwt_decode", + "min_version": "1.25.0", + "parameters": { + "mappings": [ + { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "authorization" + ] + } + ], + "output": "server.request.jwt" + } + ] + }, + "evaluate": true, + "output": false + }, + { + "id": "http-network-fingerprint", + "generator": "http_network_fingerprint", + "conditions": [], + "parameters": { + "mappings": [ + { + "headers": [ + { + "address": "server.request.headers.no_cookies" + } + ], + "output": "_dd.appsec.fp.http.network" + } + ] + }, + "evaluate": true, + "output": true + }, + { + "id": "session-fingerprint", + "generator": "session_fingerprint", + "conditions": [], + "parameters": { + "mappings": [ + { + "cookies": [ + { + "address": "server.request.cookies" + } + ], + "session_id": [ + { + "address": "usr.session_id" + } + ], + "user_id": [ + { + "address": "usr.id" + } + ], + "output": "_dd.appsec.fp.session" + } + ] + }, + "evaluate": true, + "output": true + } + ], + "scanners": [ + { + "id": "406f8606-52c4-4663-8db9-df70f9e8766c", + "name": "ZIP Code", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:zip|postal)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "^[0-9]{5}(?:-[0-9]{4})?$", + "options": { + "case_sensitive": true, + "min_length": 5 + } + } + }, + "tags": { + "type": "zipcode", + "category": "address" + } + }, + { + "id": "JU1sRk3mSzqSUJn6GrVn7g", + "name": "American Express Card Scanner (4+4+4+3 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b3[47]\\d{2}(?:(?:\\s\\d{4}\\s\\d{4}\\s\\d{3})|(?:\\,\\d{4}\\,\\d{4}\\,\\d{3})|(?:-\\d{4}-\\d{4}-\\d{3})|(?:\\.\\d{4}\\.\\d{4}\\.\\d{3}))\\b", + "options": { + "case_sensitive": false, + "min_length": 16 + } + } + }, + "tags": { + "type": "card", + "card_type": "amex", + "category": "payment" + } + }, + { + "id": "edmH513UTQWcRiQ9UnzHlw-mod", + "name": "American Express Card Scanner (4+6|5+5|6 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b3[47]\\d{2}(?:(?:\\s\\d{5,6}\\s\\d{5,6})|(?:\\.\\d{5,6}\\.\\d{5,6})|(?:-\\d{5,6}-\\d{5,6})|(?:,\\d{5,6},\\d{5,6}))\\b", + "options": { + "case_sensitive": false, + "min_length": 17 + } + } + }, + "tags": { + "type": "card", + "card_type": "amex", + "category": "payment" + } + }, + { + "id": "e6K4h_7qTLaMiAbaNXoSZA", + "name": "American Express Card Scanner (8+7 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b3[47]\\d{6}(?:(?:\\s\\d{7})|(?:\\,\\d{7})|(?:-\\d{7})|(?:\\.\\d{7}))\\b", + "options": { + "case_sensitive": false, + "min_length": 16 + } + } + }, + "tags": { + "type": "card", + "card_type": "amex", + "category": "payment" + } + }, + { + "id": "K2rZflWzRhGM9HiTc6whyQ", + "name": "American Express Card Scanner (1x15 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b3[47]\\d{13}\\b", + "options": { + "case_sensitive": false, + "min_length": 15 + } + } + }, + "tags": { + "type": "card", + "card_type": "amex", + "category": "payment" + } + }, + { + "id": "9d7756e343cefa22a5c098e1092590f806eb5446", + "name": "Basic Authentication Scanner", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\bauthorization\\b", + "options": { + "case_sensitive": false, + "min_length": 13 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "^basic\\s+[A-Za-z0-9+/=]+", + "options": { + "case_sensitive": false, + "min_length": 7 + } + } + }, + "tags": { + "type": "basic_auth", + "category": "credentials" + } + }, + { + "id": "mZy8XjZLReC9smpERXWnnw", + "name": "Bearer Authentication Scanner", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\bauthorization\\b", + "options": { + "case_sensitive": false, + "min_length": 13 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "^bearer\\s+[-a-z0-9._~+/]{4,}", + "options": { + "case_sensitive": false, + "min_length": 11 + } + } + }, + "tags": { + "type": "bearer_token", + "category": "credentials" + } + }, + { + "id": "450239afc250a19799b6c03dc0e16fd6a4b2a1af", + "name": "Canadian Social Insurance Number Scanner", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:social[\\s_]?(?:insurance(?:\\s+number)?)?|SIN|Canadian[\\s_]?(?:social[\\s_]?(?:insurance)?|insurance[\\s_]?number)?)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b\\d{3}-\\d{3}-\\d{3}\\b", + "options": { + "case_sensitive": false, + "min_length": 11 + } + } + }, + "tags": { + "type": "canadian_sin", + "category": "pii" + } + }, + { + "id": "87a879ff33693b46c8a614d8211f5a2c289beca0", + "name": "Digest Authentication Scanner", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\bauthorization\\b", + "options": { + "case_sensitive": false, + "min_length": 13 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "^digest\\s+", + "options": { + "case_sensitive": false, + "min_length": 7 + } + } + }, + "tags": { + "type": "digest_auth", + "category": "credentials" + } + }, + { + "id": "qWumeP1GQUa_E4ffAnT-Yg", + "name": "American Express Card Scanner (1x14 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "(?:30[0-59]\\d|3[689]\\d{2})(?:\\d{10})", + "options": { + "case_sensitive": false, + "min_length": 14 + } + } + }, + "tags": { + "type": "card", + "card_type": "diners", + "category": "payment" + } + }, + { + "id": "NlTWWM5LS6W0GSqBLuvtRw", + "name": "Diners Card Scanner (4+4+4+2 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:30[0-59]\\d|3[689]\\d{2})(?:(?:\\s\\d{4}\\s\\d{4}\\s\\d{2})|(?:\\,\\d{4}\\,\\d{4}\\,\\d{2})|(?:-\\d{4}-\\d{4}-\\d{2})|(?:\\.\\d{4}\\.\\d{4}\\.\\d{2}))\\b", + "options": { + "case_sensitive": false, + "min_length": 17 + } + } + }, + "tags": { + "type": "card", + "card_type": "diners", + "category": "payment" + } + }, + { + "id": "Xr5VdbQSTXitYGGiTfxBpw", + "name": "Diners Card Scanner (4+6+4 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:30[0-59]\\d|3[689]\\d{2})(?:(?:\\s\\d{6}\\s\\d{4})|(?:\\.\\d{6}\\.\\d{4})|(?:-\\d{6}-\\d{4})|(?:,\\d{6},\\d{4}))\\b", + "options": { + "case_sensitive": false, + "min_length": 16 + } + } + }, + "tags": { + "type": "card", + "card_type": "diners", + "category": "payment" + } + }, + { + "id": "gAbunN_WQNytxu54DjcbAA-mod", + "name": "Diners Card Scanner (8+6 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:30[0-59]\\d{5}|3[689]\\d{6})\\s?(?:(?:\\s\\d{6})|(?:\\,\\d{6})|(?:-\\d{6})|(?:\\.\\d{6}))\\b", + "options": { + "case_sensitive": false, + "min_length": 14 + } + } + }, + "tags": { + "type": "card", + "card_type": "diners", + "category": "payment" + } + }, + { + "id": "9cs4qCfEQBeX17U7AepOvQ", + "name": "MasterCard Scanner (2x8 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:6221(?:2[6-9]|[3-9][0-9])\\d{2}(?:,\\d{8}|\\s\\d{8}|-\\d{8}|\\.\\d{8})|6229(?:[01][0-9]|2[0-5])\\d{2}(?:,\\d{8}|\\s\\d{8}|-\\d{8}|\\.\\d{8})|(?:6011|65\\d{2}|64[4-9]\\d|622[2-8])\\d{4}(?:,\\d{8}|\\s\\d{8}|-\\d{8}|\\.\\d{8}))\\b", + "options": { + "case_sensitive": false, + "min_length": 16 + } + } + }, + "tags": { + "type": "card", + "card_type": "discover", + "category": "payment" + } + }, + { + "id": "YBIDWJIvQWW_TFOyU0CGJg", + "name": "Discover Card Scanner (4x4 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:(?:(?:6221(?:2[6-9]|[3-9][0-9])\\d{2}(?:,\\d{4}){2})|(?:6221\\s(?:2[6-9]|[3-9][0-9])\\d{2}(?:\\s\\d{4}){2})|(?:6221\\.(?:2[6-9]|[3-9][0-9])\\d{2}(?:\\.\\d{4}){2})|(?:6221-(?:2[6-9]|[3-9][0-9])\\d{2}(?:-\\d{4}){2}))|(?:(?:6229(?:[01][0-9]|2[0-5])\\d{2}(?:,\\d{4}){2})|(?:6229\\s(?:[01][0-9]|2[0-5])\\d{2}(?:\\s\\d{4}){2})|(?:6229\\.(?:[01][0-9]|2[0-5])\\d{2}(?:\\.\\d{4}){2})|(?:6229-(?:[01][0-9]|2[0-5])\\d{2}(?:-\\d{4}){2}))|(?:(?:6011|65\\d{2}|64[4-9]\\d|622[2-8])(?:(?:\\s\\d{4}){3}|(?:\\.\\d{4}){3}|(?:-\\d{4}){3}|(?:,\\d{4}){3})))\\b", + "options": { + "case_sensitive": false, + "min_length": 16 + } + } + }, + "tags": { + "type": "card", + "card_type": "discover", + "category": "payment" + } + }, + { + "id": "12cpbjtVTMaMutFhh9sojQ", + "name": "Discover Card Scanner (1x16 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:6221(?:2[6-9]|[3-9][0-9])\\d{10}|6229(?:[01][0-9]|2[0-5])\\d{10}|(?:6011|65\\d{2}|64[4-9]\\d|622[2-8])\\d{12})\\b", + "options": { + "case_sensitive": false, + "min_length": 16 + } + } + }, + "tags": { + "type": "card", + "card_type": "discover", + "category": "payment" + } + }, + { + "id": "PuXiVTCkTHOtj0Yad1ppsw", + "name": "Standard E-mail Address", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:(?:e[-\\s]?)?mail|address|sender|\\bto\\b|from|recipient)\\b", + "options": { + "case_sensitive": false, + "min_length": 2 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b[\\w!#$%&'*+/=?`{|}~^-]+(?:\\.[\\w!#$%&'*+/=?`{|}~^-]+)*(%40|@)(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}\\b", + "options": { + "case_sensitive": false, + "min_length": 5 + } + } + }, + "tags": { + "type": "email", + "category": "pii" + } + }, + { + "id": "8VS2RKxzR8a_95L5fuwaXQ", + "name": "IBAN", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:iban|account|sender|receiver)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:NO\\d{2}(?:[ \\-]?\\d{4}){2}[ \\-]?\\d{3}|BE\\d{2}(?:[ \\-]?\\d{4}){3}|(?:DK|FO|FI|GL|SD)\\d{2}(?:[ \\-]?\\d{4}){3}[ \\-]?\\d{2}|NL\\d{2}[ \\-]?[A-Z]{4}(?:[ \\-]?\\d{4}){2}[ \\-]?\\d{2}|MK\\d{2}[ \\-]?\\d{3}[A-Z0-9](?:[ \\-]?[A-Z0-9]{4}){2}[ \\-]?[A-Z0-9]\\d{2}|SI\\d{17}|(?:AT|BA|EE|LT|XK)\\d{18}|(?:LU|KZ|EE|LT)\\d{5}[A-Z0-9]{13}|LV\\d{2}[A-Z]{4}[A-Z0-9]{13}|(?:LI|CH)\\d{2}[ \\-]?\\d{4}[ \\-]?\\d[A-Z0-9]{3}(?:[ \\-]?[A-Z0-9]{4}){2}[ \\-]?[A-Z0-9]|HR\\d{2}(?:[ \\-]?\\d{4}){4}[ \\-]?\\d|GE\\d{2}[ \\-]?[A-Z0-9]{2}\\d{2}\\d{14}|VA\\d{20}|BG\\d{2}[A-Z]{4}\\d{6}[A-Z0-9]{8}|BH\\d{2}[A-Z]{4}[A-Z0-9]{14}|GB\\d{2}[A-Z]{4}(?:[ \\-]?\\d{4}){3}[ \\-]?\\d{2}|IE\\d{2}[ \\-]?[A-Z0-9]{4}(?:[ \\-]?\\d{4}){3}[ \\-]?\\d{2}|(?:CR|DE|ME|RS)\\d{2}(?:[ \\-]?\\d{4}){4}[ \\-]?\\d{2}|(?:AE|TL|IL)\\d{2}(?:[ \\-]?\\d{4}){4}[ \\-]?\\d{3}|GI\\d{2}[ \\-]?[A-Z]{4}(?:[ \\-]?[A-Z0-9]{4}){3}[ \\-]?[A-Z0-9]{3}|IQ\\d{2}[ \\-]?[A-Z]{4}(?:[ \\-]?\\d{4}){3}[ \\-]?\\d{3}|MD\\d{2}(?:[ \\-]?[A-Z0-9]{4}){5}|SA\\d{2}[ \\-]?\\d{2}[A-Z0-9]{2}(?:[ \\-]?[A-Z0-9]{4}){4}|RO\\d{2}[ \\-]?[A-Z]{4}(?:[ \\-]?[A-Z0-9]{4}){4}|(?:PK|VG)\\d{2}[ \\-]?[A-Z0-9]{4}(?:[ \\-]?\\d{4}){4}|AD\\d{2}(?:[ \\-]?\\d{4}){2}(?:[ \\-]?[A-Z0-9]{4}){3}|(?:CZ|SK|ES|SE|TN)\\d{2}(?:[ \\-]?\\d{4}){5}|(?:LY|PT|ST)\\d{2}(?:[ \\-]?\\d{4}){5}[ \\-]?\\d|TR\\d{2}[ \\-]?\\d{4}[ \\-]?\\d[A-Z0-9]{3}(?:[ \\-]?[A-Z0-9]{4}){3}[ \\-]?[A-Z0-9]{2}|IS\\d{2}(?:[ \\-]?\\d{4}){5}[ \\-]?\\d{2}|(?:IT|SM)\\d{2}[ \\-]?[A-Z]\\d{3}[ \\-]?\\d{4}[ \\-]?\\d{3}[A-Z0-9](?:[ \\-]?[A-Z0-9]{4}){2}[ \\-]?[A-Z0-9]{3}|GR\\d{2}[ \\-]?\\d{4}[ \\-]?\\d{3}[A-Z0-9](?:[ \\-]?[A-Z0-9]{4}){3}[A-Z0-9]{3}|(?:FR|MC)\\d{2}(?:[ \\-]?\\d{4}){2}[ \\-]?\\d{2}[A-Z0-9]{2}(?:[ \\-]?[A-Z0-9]{4}){2}[ \\-]?[A-Z0-9]\\d{2}|MR\\d{2}(?:[ \\-]?\\d{4}){5}[ \\-]?\\d{3}|(?:SV|DO)\\d{2}[ \\-]?[A-Z]{4}(?:[ \\-]?\\d{4}){5}|BY\\d{2}[ \\-]?[A-Z]{4}[ \\-]?\\d{4}(?:[ \\-]?[A-Z0-9]{4}){4}|GT\\d{2}(?:[ \\-]?[A-Z0-9]{4}){6}|AZ\\d{2}[ \\-]?[A-Z0-9]{4}(?:[ \\-]?\\d{5}){4}|LB\\d{2}[ \\-]?\\d{4}(?:[ \\-]?[A-Z0-9]{5}){4}|(?:AL|CY)\\d{2}(?:[ \\-]?\\d{4}){2}(?:[ \\-]?[A-Z0-9]{4}){4}|(?:HU|PL)\\d{2}(?:[ \\-]?\\d{4}){6}|QA\\d{2}[ \\-]?[A-Z]{4}(?:[ \\-]?[A-Z0-9]{4}){5}[ \\-]?[A-Z0-9]|PS\\d{2}[ \\-]?[A-Z0-9]{4}(?:[ \\-]?\\d{4}){5}[ \\-]?\\d|UA\\d{2}[ \\-]?\\d{4}[ \\-]?\\d{2}[A-Z0-9]{2}(?:[ \\-]?[A-Z0-9]{4}){4}[ \\-]?[A-Z0-9]|BR\\d{2}(?:[ \\-]?\\d{4}){5}[ \\-]?\\d{3}[A-Z0-9][ \\-]?[A-Z0-9]|EG\\d{2}(?:[ \\-]?\\d{4}){6}\\d|MU\\d{2}[ \\-]?[A-Z]{4}(?:[ \\-]?\\d{4}){4}\\d{3}[A-Z][ \\-]?[A-Z]{2}|(?:KW|JO)\\d{2}[ \\-]?[A-Z]{4}(?:[ \\-]?[A-Z0-9]{4}){5}[ \\-]?[A-Z0-9]{2}|MT\\d{2}[ \\-]?[A-Z]{4}[ \\-]?\\d{4}[ \\-]?\\d[A-Z0-9]{3}(?:[ \\-]?[A-Z0-9]{3}){4}[ \\-]?[A-Z0-9]{3}|SC\\d{2}[ \\-]?[A-Z]{4}(?:[ \\-]?\\d{4}){5}[ \\-]?[A-Z]{3}|LC\\d{2}[ \\-]?[A-Z]{4}(?:[ \\-]?[A-Z0-9]{4}){6})\\b", + "options": { + "case_sensitive": false, + "min_length": 15 + } + } + }, + "tags": { + "type": "iban", + "category": "payment" + } + }, + { + "id": "h6WJcecQTwqvN9KeEtwDvg", + "name": "JCB Card Scanner (1x16 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b35(?:2[89]|[3-9][0-9])(?:\\d{12})\\b", + "options": { + "case_sensitive": false, + "min_length": 16 + } + } + }, + "tags": { + "type": "card", + "card_type": "jcb", + "category": "payment" + } + }, + { + "id": "gcEaMu_VSJ2-bGCEkgyC0w", + "name": "JCB Card Scanner (2x8 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b35(?:2[89]|[3-9][0-9])\\d{4}(?:(?:,\\d{8})|(?:-\\d{8})|(?:\\s\\d{8})|(?:\\.\\d{8}))\\b", + "options": { + "case_sensitive": false, + "min_length": 17 + } + } + }, + "tags": { + "type": "card", + "card_type": "jcb", + "category": "payment" + } + }, + { + "id": "imTliuhXT5GAeRNhqChXQQ", + "name": "JCB Card Scanner (4x4 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b35(?:2[89]|[3-9][0-9])(?:(?:\\s\\d{4}){3}|(?:\\.\\d{4}){3}|(?:-\\d{4}){3}|(?:,\\d{4}){3})\\b", + "options": { + "case_sensitive": false, + "min_length": 16 + } + } + }, + "tags": { + "type": "card", + "card_type": "jcb", + "category": "payment" + } + }, + { + "id": "9osY3xc9Q7ONAV0zw9Uz4A", + "name": "JSON Web Token", + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\bey[I-L][\\w=-]+\\.ey[I-L][\\w=-]+(\\.[\\w.+\\/=-]+)?\\b", + "options": { + "case_sensitive": false, + "min_length": 20 + } + } + }, + "tags": { + "type": "json_web_token", + "category": "credentials" + } + }, + { + "id": "d1Q9D3YMRxuVKf6CZInJPw", + "name": "Maestro Card Scanner (1x16 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:5[06-9]\\d{2}|6\\d{3})(?:\\d{12})\\b", + "options": { + "case_sensitive": false, + "min_length": 16 + } + } + }, + "tags": { + "type": "card", + "card_type": "maestro", + "category": "payment" + } + }, + { + "id": "M3YIQKKjRVmoeQuM3pjzrw", + "name": "Maestro Card Scanner (2x8 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:5[06-9]\\d{6}|6\\d{7})(?:\\s\\d{8}|\\.\\d{8}|-\\d{8}|,\\d{8})\\b", + "options": { + "case_sensitive": false, + "min_length": 17 + } + } + }, + "tags": { + "type": "card", + "card_type": "maestro", + "category": "payment" + } + }, + { + "id": "hRxiQBlSSVKcjh5U7LZYLA", + "name": "Maestro Card Scanner (4x4 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:5[06-9]\\d{2}|6\\d{3})(?:(?:\\s\\d{4}){3}|(?:\\.\\d{4}){3}|(?:-\\d{4}){3}|(?:,\\d{4}){3})\\b", + "options": { + "case_sensitive": false, + "min_length": 16 + } + } + }, + "tags": { + "type": "card", + "card_type": "maestro", + "category": "payment" + } + }, + { + "id": "NwhIYNS4STqZys37WlaIKA", + "name": "MasterCard Scanner (2x8 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:(?:5[1-5]\\d{2})|(?:222[1-9])|(?:22[3-9]\\d)|(?:2[3-6]\\d{2})|(?:27[0-1]\\d)|(?:2720))(?:(?:\\d{4}(?:(?:,\\d{8})|(?:-\\d{8})|(?:\\s\\d{8})|(?:\\.\\d{8}))))\\b", + "options": { + "case_sensitive": false, + "min_length": 16 + } + } + }, + "tags": { + "type": "card", + "card_type": "mastercard", + "category": "payment" + } + }, + { + "id": "axxJkyjhRTOuhjwlsA35Vw", + "name": "MasterCard Scanner (4x4 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:(?:5[1-5]\\d{2})|(?:222[1-9])|(?:22[3-9]\\d)|(?:2[3-6]\\d{2})|(?:27[0-1]\\d)|(?:2720))(?:(?:\\s\\d{4}){3}|(?:\\.\\d{4}){3}|(?:-\\d{4}){3}|(?:,\\d{4}){3})\\b", + "options": { + "case_sensitive": false, + "min_length": 16 + } + } + }, + "tags": { + "type": "card", + "card_type": "mastercard", + "category": "payment" + } + }, + { + "id": "76EhmoK3TPqJcpM-fK0pLw", + "name": "MasterCard Scanner (1x16 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:(?:5[1-5]\\d{2})|(?:222[1-9])|(?:22[3-9]\\d)|(?:2[3-6]\\d{2})|(?:27[0-1]\\d)|(?:2720))(?:\\d{12})\\b", + "options": { + "case_sensitive": false, + "min_length": 16 + } + } + }, + "tags": { + "type": "card", + "card_type": "mastercard", + "category": "payment" + } + }, + { + "id": "c542c147-3883-43d6-a067-178e4a7bd65d", + "name": "Password", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\bpass(?:[_-]?word|wd)?\\b|\\bpwd\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "tags": { + "type": "password", + "category": "credentials" + } + }, + { + "id": "18b608bd7a764bff5b2344c0", + "name": "Phone number", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\bphone|number|mobile\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "^(?:\\(\\+\\d{1,3}\\)|\\+\\d{1,3}|00\\d{1,3})?[-\\s\\.]?(?:\\(\\d{3}\\)[-\\s\\.]?)?(?:\\d[-\\s\\.]?){6,10}$", + "options": { + "case_sensitive": false, + "min_length": 6 + } + } + }, + "tags": { + "type": "phone", + "category": "pii" + } + }, + { + "id": "de0899e0cbaaa812bb624cf04c912071012f616d-mod", + "name": "UK National Insurance Number Scanner", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "^nin$|\\binsurance\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b[A-Z]{2}[\\s-]?\\d{6}[\\s-]?[A-Z]?\\b", + "options": { + "case_sensitive": false, + "min_length": 8 + } + } + }, + "tags": { + "type": "uk_nin", + "category": "pii" + } + }, + { + "id": "d962f7ddb3f55041e39195a60ff79d4814a7c331", + "name": "US Passport Scanner", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\bpassport\\b", + "options": { + "case_sensitive": false, + "min_length": 8 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b[0-9A-Z]{9}\\b|\\b[0-9]{6}[A-Z][0-9]{2}\\b", + "options": { + "case_sensitive": false, + "min_length": 8 + } + } + }, + "tags": { + "type": "passport_number", + "category": "pii" + } + }, + { + "id": "7771fc3b-b205-4b93-bcef-28608c5c1b54", + "name": "United States Social Security Number Scanner", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:SSN|(?:(?:social)?[\\s_]?(?:security)?[\\s_]?(?:number)?)?)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b\\d{3}[-\\s\\.]{1}\\d{2}[-\\s\\.]{1}\\d{4}\\b", + "options": { + "case_sensitive": false, + "min_length": 11 + } + } + }, + "tags": { + "type": "us_ssn", + "category": "pii" + } + }, + { + "id": "ac6d683cbac77f6e399a14990793dd8fd0fca333", + "name": "US Vehicle Identification Number Scanner", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:vehicle[_\\s-]*identification[_\\s-]*number|vin)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b[A-HJ-NPR-Z0-9]{17}\\b", + "options": { + "case_sensitive": false, + "min_length": 17 + } + } + }, + "tags": { + "type": "vin", + "category": "pii" + } + }, + { + "id": "wJIgOygRQhKkR69b_9XbRQ", + "name": "Visa Card Scanner (2x8 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b4\\d{3}(?:(?:\\d{4}(?:(?:,\\d{8})|(?:-\\d{8})|(?:\\s\\d{8})|(?:\\.\\d{8}))))\\b", + "options": { + "case_sensitive": false, + "min_length": 16 + } + } + }, + "tags": { + "type": "card", + "card_type": "visa", + "category": "payment" + } + }, + { + "id": "0o71SJxXQNK7Q6gMbBesFQ", + "name": "Visa Card Scanner (4x4 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "\\b4\\d{3}(?:(?:,\\d{4}){3}|(?:\\s\\d{4}){3}|(?:\\.\\d{4}){3}|(?:-\\d{4}){3})\\b", + "options": { + "case_sensitive": false, + "min_length": 16 + } + } + }, + "tags": { + "type": "card", + "card_type": "visa", + "category": "payment" + } + }, + { + "id": "QrHD6AfgQm6z-j0wStxTvA", + "name": "Visa Card Scanner (1x15 & 1x16 & 1x19 digits)", + "key": { + "operator": "match_regex", + "parameters": { + "regex": "\\b(?:card|cc|credit|debit|payment|amex|visa|mastercard|maestro|discover|jcb|diner)\\b", + "options": { + "case_sensitive": false, + "min_length": 3 + } + } + }, + "value": { + "operator": "match_regex", + "parameters": { + "regex": "4[0-9]{12}(?:[0-9]{3})?", + "options": { + "case_sensitive": false, + "min_length": 13 + } + } + }, + "tags": { + "type": "card", + "card_type": "visa", + "category": "payment" + } + } + ] +} diff --git a/bottlecap/src/appsec/sampler.rs b/bottlecap/src/appsec/sampler.rs new file mode 100644 index 000000000..2d6aa457b --- /dev/null +++ b/bottlecap/src/appsec/sampler.rs @@ -0,0 +1,174 @@ +use std::hash::{BuildHasher, BuildHasherDefault, Hasher}; +use std::num::NonZero; +use std::time::Duration; + +#[cfg(test)] +use mock_instant::global::Instant; +#[cfg(not(test))] +use std::time::Instant; + +use fnv::FnvBuildHasher; +use ordered_hash_map::OrderedHashMap; + +/// A sampler that implements time-based, per-endpoint sampling. +/// +/// It is an implementation of +/// that does not have adaptations for thread-based concurrency, as the +/// Serverless extension does not actually present significant risk of +/// contention. +#[derive(Debug)] +pub(crate) struct Sampler { + interval: Duration, + data: OrderedHashMap, + hasher_builder: FnvBuildHasher, +} +impl Sampler { + /// Creates a new sampler with the given interval and a default capacity. + pub(crate) fn with_interval(interval: Duration) -> Self { + Self::with_interval_and_capacity(interval, unsafe { NonZero::new_unchecked(4_096) }) + } + + /// Creates a new sampler with the given interval and capacity. + pub(crate) fn with_interval_and_capacity(interval: Duration, capacity: NonZero) -> Self { + Self { + interval, + data: OrderedHashMap::with_capacity_and_hasher( + capacity.get(), + BuildIdentityHasher::default(), + ), + hasher_builder: FnvBuildHasher::default(), + } + } + + /// Make a sampling decision for a given request signature. + pub(crate) fn decision_for(&mut self, method: &str, route: &str, status_code: &str) -> bool { + let mut hasher = self.hasher_builder.build_hasher(); + hasher.write(method.as_bytes()); + hasher.write_u8(0); + hasher.write(route.as_bytes()); + hasher.write_u8(0); + hasher.write(status_code.as_bytes()); + let hash = hasher.finish(); + + if let Some(state) = self.data.get_mut(&hash) { + if state.last_sample.elapsed() >= self.interval { + state.last_sample = Instant::now(); + // This entry is now the most recent one! + self.data.move_to_back(&hash); + true + } else { + // This entry is now the most recent one! + self.data.move_to_back(&hash); + false + } + } else { + if self.data.len() >= self.data.capacity() { + // We're full, drop the oldest entry... + self.data.pop_front(); + } + self.data.insert( + hash, + SamplerState { + last_sample: Instant::now(), + }, + ); + true + } + } +} + +#[repr(transparent)] +#[derive(Debug, Clone, Copy)] +struct SamplerState { + last_sample: Instant, +} + +type BuildIdentityHasher = BuildHasherDefault; + +/// A [Hasher] that forwards the value that is being hashed without any additonal processing. +#[derive(Debug, Default, Clone, Copy)] +struct IdentityHasher(u64); +#[allow(clippy::cast_sign_loss)] // This is not relevant in this Hasher implementation +impl Hasher for IdentityHasher { + #[cfg_attr(coverage_nightly, coverage(off))] // Unsupported + fn write(&mut self, _: &[u8]) { + unimplemented!("IdentityHasher does not support hashing arbitrary data") + } + #[cfg_attr(coverage_nightly, coverage(off))] // Supported, but unused + fn write_u8(&mut self, v: u8) { + self.0 = u64::from(v); + } + #[cfg_attr(coverage_nightly, coverage(off))] // Supported, but unused + fn write_u16(&mut self, v: u16) { + self.0 = u64::from(v); + } + #[cfg_attr(coverage_nightly, coverage(off))] // Supported, but unused + fn write_u32(&mut self, v: u32) { + self.0 = u64::from(v); + } + fn write_u64(&mut self, v: u64) { + self.0 = v; + } + #[cfg_attr(coverage_nightly, coverage(off))] // Supported, but unused + fn write_usize(&mut self, v: usize) { + self.0 = v as u64; + } + #[cfg_attr(coverage_nightly, coverage(off))] // Supported, but unused + fn write_i8(&mut self, v: i8) { + self.0 = v as u64; + } + #[cfg_attr(coverage_nightly, coverage(off))] // Supported, but unused + fn write_i16(&mut self, v: i16) { + self.0 = v as u64; + } + #[cfg_attr(coverage_nightly, coverage(off))] // Supported, but unused + fn write_i32(&mut self, v: i32) { + self.0 = v as u64; + } + #[cfg_attr(coverage_nightly, coverage(off))] // Supported, but unused + fn write_i64(&mut self, v: i64) { + self.0 = v as u64; + } + #[cfg_attr(coverage_nightly, coverage(off))] // Supported, but unused + fn write_isize(&mut self, v: isize) { + self.0 = v as u64; + } + fn finish(&self) -> u64 { + self.0 + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] // Test modules skew coverage metrics +#[cfg(test)] +mod tests { + use mock_instant::global::MockClock; + + use super::*; + + #[test] + fn test_default_sampler() { + let mut sampler = Sampler::with_interval(Duration::from_secs(30)); + // First call should be sampled + assert!(sampler.decision_for("GET", "/", "200")); + + MockClock::advance(Duration::from_secs(15)); + // Second call should not be sampled (less than 30 seconds have passed) + assert!(!sampler.decision_for("GET", "/", "200")); + + MockClock::advance(Duration::from_secs(15)); + // 30 seconds have passed, the call should be sampled in again! + assert!(sampler.decision_for("GET", "/", "200")); + } + + #[test] + fn test_sampler_capacity() { + let mut sampler = Sampler::with_interval_and_capacity(Duration::from_secs(30), unsafe { + NonZero::new_unchecked(1) + }); + + for i in 0..100 { + // All requests should be sampled in here since we run with a capacity of 1 and use a differen route each time... + assert!(sampler.decision_for("GET", &format!("/{i}"), "200")); + } + } +} diff --git a/bottlecap/src/bin/bottlecap/main.rs b/bottlecap/src/bin/bottlecap/main.rs index 52dbf7a98..8d2583f63 100644 --- a/bottlecap/src/bin/bottlecap/main.rs +++ b/bottlecap/src/bin/bottlecap/main.rs @@ -518,7 +518,7 @@ async fn extension_loop_active( ); let api_runtime_proxy_shutdown_signal = - start_api_runtime_proxy(config, aws_config, &invocation_processor); + start_api_runtime_proxy(config, &aws_config, &invocation_processor); let lifecycle_listener = LifecycleListener { invocation_processor: Arc::clone(&invocation_processor), @@ -1180,14 +1180,16 @@ fn start_otlp_agent( fn start_api_runtime_proxy( config: &Arc, - aws_config: Arc, + aws_config: &Arc, invocation_processor: &Arc>, ) -> Option { - if !should_start_proxy(config, Arc::clone(&aws_config)) { + if !should_start_proxy(config, aws_config) { debug!("Skipping API runtime proxy, no LWA proxy or datadog wrapper found"); return None; } + let config = config.clone(); + let aws_config = aws_config.clone(); let invocation_processor = invocation_processor.clone(); - interceptor::start(aws_config, invocation_processor).ok() + interceptor::start(config, aws_config, invocation_processor).ok() } diff --git a/bottlecap/src/config/env.rs b/bottlecap/src/config/env.rs index ad91db2f2..6ef8df437 100644 --- a/bottlecap/src/config/env.rs +++ b/bottlecap/src/config/env.rs @@ -1,6 +1,7 @@ use figment::{Figment, providers::Env}; use serde::Deserialize; use std::collections::HashMap; +use std::time::Duration; use datadog_trace_obfuscation::replacer::ReplaceRule; @@ -10,7 +11,9 @@ use crate::{ additional_endpoints::deserialize_additional_endpoints, apm_replace_rule::deserialize_apm_replace_rules, deserialize_array_from_comma_separated_string, deserialize_key_value_pairs, - deserialize_optional_bool_from_anything, deserialize_string_or_int, + deserialize_optional_bool_from_anything, + deserialize_optional_duration_from_optional_microseconds, + deserialize_optional_duration_from_optional_seconds, deserialize_string_or_int, flush_strategy::FlushStrategy, log_level::LogLevel, logs_additional_endpoints::{ @@ -301,17 +304,33 @@ pub struct EnvConfig { /// The maximum depth of the Lambda payload to capture. /// Default is `10`. Requires `capture_lambda_payload` to be `true`. pub capture_lambda_payload_max_depth: Option, + /// @env `DD_EXTENSION_VERSION` + /// + /// Used to decide which version of the Datadog Lambda Extension to use. + /// When set to `compatibility`, the extension will boot up in legacy mode. + pub extension_version: Option, + + // AppSec /// @env `DD_SERVERLESS_APPSEC_ENABLED` /// /// Enable Application and API Protection (AAP), previously known as AppSec/ASM, for AWS Lambda. /// Default is `false`. #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] pub serverless_appsec_enabled: Option, - /// @env `DD_EXTENSION_VERSION` + /// @env `DD_APPSEC_RULES` /// - /// Used to decide which version of the Datadog Lambda Extension to use. - /// When set to `compatibility`, the extension will boot up in legacy mode. - pub extension_version: Option, + /// The path to the App & API Protection (AAP) rules file. + pub appsec_rules: Option, + /// @env `DD_APPSEC_WAF_TIMEOUT` + /// + /// The timeout for the App & API Protection (AAP) WAF (in microseconds). + #[serde(deserialize_with = "deserialize_optional_duration_from_optional_microseconds")] + pub appsec_waf_timeout: Option, + /// @env `DD_API_SECURITY_SAMPLE_DELAY` + /// + /// The delay between two API Security samples for a given endpoint (HTTP method + route + status code). + #[serde(deserialize_with = "deserialize_optional_duration_from_optional_seconds")] + pub api_security_sample_delay: Option, } #[allow(clippy::too_many_lines)] @@ -453,8 +472,13 @@ fn merge_config(config: &mut Config, env_config: &EnvConfig) { merge_option_to_value!(config, env_config, lambda_proc_enhanced_metrics); merge_option_to_value!(config, env_config, capture_lambda_payload); merge_option_to_value!(config, env_config, capture_lambda_payload_max_depth); - merge_option_to_value!(config, env_config, serverless_appsec_enabled); merge_option!(config, env_config, extension_version); + + // AppSec + merge_option_to_value!(config, env_config, serverless_appsec_enabled); + merge_option!(config, env_config, appsec_rules); + merge_option_to_value!(config, env_config, appsec_waf_timeout); + merge_option_to_value!(config, env_config, api_security_sample_delay); } #[derive(Debug, PartialEq, Clone, Copy)] @@ -627,6 +651,9 @@ mod tests { jail.set_env("DD_CAPTURE_LAMBDA_PAYLOAD", "true"); jail.set_env("DD_CAPTURE_LAMBDA_PAYLOAD_MAX_DEPTH", "5"); jail.set_env("DD_SERVERLESS_APPSEC_ENABLED", "true"); + jail.set_env("DD_APPSEC_RULES", "/path/to/rules.json"); + jail.set_env("DD_APPSEC_WAF_TIMEOUT", "3000"); // Microseconds + jail.set_env("DD_API_SECURITY_SAMPLE_DELAY", "60.0"); // Seconds jail.set_env("DD_EXTENSION_VERSION", "compatibility"); let mut config = Config::default(); @@ -751,6 +778,9 @@ mod tests { capture_lambda_payload: true, capture_lambda_payload_max_depth: 5, serverless_appsec_enabled: true, + appsec_rules: Some("/path/to/rules.json".to_string()), + appsec_waf_timeout: Duration::from_millis(3), + api_security_sample_delay: Duration::from_secs(60), extension_version: Some("compatibility".to_string()), }; diff --git a/bottlecap/src/config/mod.rs b/bottlecap/src/config/mod.rs index 2f7d43085..59e9b4f82 100644 --- a/bottlecap/src/config/mod.rs +++ b/bottlecap/src/config/mod.rs @@ -18,6 +18,7 @@ use serde_aux::prelude::deserialize_bool_from_anything; use serde_json::Value; use std::path::Path; +use std::time::Duration; use std::{collections::HashMap, fmt}; use tracing::{debug, error}; @@ -328,6 +329,9 @@ pub struct Config { pub capture_lambda_payload: bool, pub capture_lambda_payload_max_depth: u32, pub serverless_appsec_enabled: bool, + pub appsec_rules: Option, + pub appsec_waf_timeout: Duration, + pub api_security_sample_delay: Duration, pub extension_version: Option, } @@ -412,6 +416,9 @@ impl Default for Config { capture_lambda_payload: false, capture_lambda_payload_max_depth: 10, serverless_appsec_enabled: false, + appsec_rules: None, + appsec_waf_timeout: Duration::from_millis(5), + api_security_sample_delay: Duration::from_secs(30), extension_version: None, } } @@ -436,13 +443,6 @@ fn fallback(config: &Config) -> Result<(), ConfigError> { )); } - if config.serverless_appsec_enabled { - log_fallback_reason("appsec_enabled"); - return Err(ConfigError::UnsupportedField( - "serverless_appsec_enabled".to_string(), - )); - } - // OTLP let has_otlp_config = config .otlp_config_receiver_protocols_grpc_endpoint @@ -519,6 +519,26 @@ where } } +pub fn deserialize_optional_duration_from_optional_microseconds<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let micros: Option = Option::deserialize(deserializer)?; + Ok(micros.map(Duration::from_micros)) +} + +pub fn deserialize_optional_duration_from_optional_seconds<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let micros: Option = Option::deserialize(deserializer)?; + Ok(micros.map(Duration::from_secs_f64)) +} + pub fn deserialize_optional_bool_from_anything<'de, D>( deserializer: D, ) -> Result, D::Error> @@ -611,10 +631,11 @@ pub mod tests { use super::*; - use crate::config::flush_strategy::{FlushStrategy, PeriodicStrategy}; - use crate::config::log_level::LogLevel; - use crate::config::processing_rule; - use crate::config::trace_propagation_style::TracePropagationStyle; + use crate::config::{ + flush_strategy::{FlushStrategy, PeriodicStrategy}, + log_level::LogLevel, + trace_propagation_style::TracePropagationStyle, + }; #[test] fn test_reject_on_opted_out() { @@ -748,21 +769,6 @@ pub mod tests { }); } - #[test] - fn test_allowed_but_disabled() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_SERVERLESS_APPSEC_ENABLED", "true"); - - let config = get_config(Path::new("")).expect_err("should reject unknown fields"); - assert_eq!( - config, - ConfigError::UnsupportedField("serverless_appsec_enabled".to_string()) - ); - Ok(()) - }); - } - #[test] fn test_precedence() { figment::Jail::expect_with(|jail| { diff --git a/bottlecap/src/config/yaml.rs b/bottlecap/src/config/yaml.rs index 1045fd660..2c0c032ea 100644 --- a/bottlecap/src/config/yaml.rs +++ b/bottlecap/src/config/yaml.rs @@ -1,11 +1,13 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::{collections::HashMap, path::PathBuf, time::Duration}; use crate::{ config::{ Config, ConfigError, ConfigSource, ProcessingRule, additional_endpoints::deserialize_additional_endpoints, deserialize_apm_replace_rules, deserialize_key_value_pair_array_to_hashmap, - deserialize_optional_bool_from_anything, deserialize_processing_rules, + deserialize_optional_bool_from_anything, + deserialize_optional_duration_from_optional_microseconds, + deserialize_optional_duration_from_optional_seconds, deserialize_processing_rules, deserialize_string_or_int, flush_strategy::FlushStrategy, log_level::LogLevel, @@ -89,9 +91,16 @@ pub struct YamlConfig { #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] pub capture_lambda_payload: Option, pub capture_lambda_payload_max_depth: Option, + pub extension_version: Option, + + // AppSec #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] pub serverless_appsec_enabled: Option, - pub extension_version: Option, + pub appsec_rules: Option, + #[serde(deserialize_with = "deserialize_optional_duration_from_optional_microseconds")] + pub appsec_waf_timeout: Option, + #[serde(deserialize_with = "deserialize_optional_duration_from_optional_seconds")] + pub api_security_sample_delay: Option, } /// Proxy Config @@ -600,6 +609,12 @@ fn merge_config(config: &mut Config, yaml_config: &YamlConfig) { merge_option_to_value!(config, yaml_config, capture_lambda_payload_max_depth); merge_option_to_value!(config, yaml_config, serverless_appsec_enabled); merge_option!(config, yaml_config, extension_version); + + // AppSec + merge_option_to_value!(config, yaml_config, serverless_appsec_enabled); + merge_option!(config, yaml_config, appsec_rules); + merge_option_to_value!(config, yaml_config, appsec_waf_timeout); + merge_option_to_value!(config, yaml_config, api_security_sample_delay); } #[derive(Debug, PartialEq, Clone)] @@ -628,6 +643,7 @@ impl ConfigSource for YamlConfigSource { #[cfg(test)] mod tests { use std::path::Path; + use std::time::Duration; use crate::config::{flush_strategy::PeriodicStrategy, processing_rule::Kind}; @@ -757,8 +773,13 @@ enhanced_metrics: false lambda_proc_enhanced_metrics: false capture_lambda_payload: true capture_lambda_payload_max_depth: 5 -serverless_appsec_enabled: true extension_version: "compatibility" + +# AppSec +serverless_appsec_enabled: true +appsec_rules: "/path/to/rules.json" +appsec_waf_timeout: 3000 +api_security_sample_delay: 60.0 "#, )?; @@ -881,6 +902,9 @@ extension_version: "compatibility" capture_lambda_payload: true, capture_lambda_payload_max_depth: 5, serverless_appsec_enabled: true, + appsec_rules: Some("/path/to/rules.json".to_string()), + appsec_waf_timeout: Duration::from_millis(3), + api_security_sample_delay: Duration::from_secs(60), extension_version: Some("compatibility".to_string()), }; diff --git a/bottlecap/src/lib.rs b/bottlecap/src/lib.rs index 7cd1968de..fb9f8cc19 100644 --- a/bottlecap/src/lib.rs +++ b/bottlecap/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] + //! Crate for the `bottlecap` project #![deny(clippy::all)] #![deny(clippy::pedantic)] @@ -17,6 +19,7 @@ #![allow(clippy::cast_precision_loss)] #![allow(clippy::needless_pass_by_value)] +pub mod appsec; pub mod config; pub mod event_bus; pub mod events; diff --git a/bottlecap/src/lifecycle/invocation/context.rs b/bottlecap/src/lifecycle/invocation/context.rs index 7f4003739..5c54624e8 100644 --- a/bottlecap/src/lifecycle/invocation/context.rs +++ b/bottlecap/src/lifecycle/invocation/context.rs @@ -1,3 +1,5 @@ +use crate::appsec::processor::AppSecContext; +use crate::lifecycle::invocation::processor::TAG_SAMPLING_PRIORITY; use crate::{ lifecycle::invocation::processor::MS_TO_NS, metrics::enhanced::lambda::EnhancedMetricData, traces::context::SpanContext, @@ -8,9 +10,9 @@ use std::{ }; use datadog_trace_protobuf::pb::Span; -use tracing::debug; +use tracing::{debug, warn}; -#[derive(Debug, Clone, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct Context { /// The timestamp when the context was created. created_at: i64, @@ -38,6 +40,56 @@ pub struct Context { /// tracing. /// pub extracted_span_context: Option, + /// The [`AppSecContext`] for this invocation, if one exists. + appsec_context: Option, +} +impl Context { + pub fn absorb_appsec_tags(&mut self) { + let Some(appsec_context) = self.appsec_context.as_mut() else { + // Nothing to do -- we don't have AppSec context data for this invocation. + return; + }; + + self.invocation_span + .metrics + .insert("_dd.appsec.enabled".to_string(), 1f64); + + // Up-sert any attributes from the AppSec context into the invocation span. This includes + // synthetic attributes produced by the WAF (fingerprints, schemas, ...) as well as tracking + // spans expected by AAP. We do upsert to avoid overwriting anything previously set. + for (key, value) in appsec_context.tags() { + self.invocation_span + .meta + .entry(key.to_string()) + .or_insert(value.to_string()); + } + + if !appsec_context.events.is_empty() { + self.invocation_span + .meta + .insert("appsec.event".to_string(), "true".to_string()); + match serde_json::to_string(&appsec_context.events) { + Ok(encoded) => { + self.invocation_span + .meta + .insert("_dd.appsec.json".to_string(), encoded); + } + Err(e) => { + warn!("appsec: unable to encode WAF events: {e}"); + } + } + } + + // Note: We intentionally don't set `actor.ip` because we don't have a definitive signal here. + + //TODO(romain.marcadier): Figure out whether we can set "_dd.runtime_family" here; and to what value. + + if appsec_context.keep { + self.invocation_span + .metrics + .insert(TAG_SAMPLING_PRIORITY.to_string(), 2f64 /* USER_KEEP */); + } + } } /// Struct containing the information needed to reparent a span. @@ -88,6 +140,7 @@ impl Default for Context { cold_start_span: None, tracer_span: None, extracted_span_context: None, + appsec_context: None, } } } @@ -123,6 +176,9 @@ pub struct ContextBuffer { universal_instrumentation_start_events: VecDeque, universal_instrumentation_end_events: VecDeque, pub sorted_reparenting_info: VecDeque, + + /// The buffer of appsec contexts that are bound before the corresponding [`Context`] is created. + appsec_contexts: VecDeque<(String, AppSecContext)>, } struct UniversalInstrumentationData { @@ -148,27 +204,79 @@ impl ContextBuffer { universal_instrumentation_start_events: VecDeque::with_capacity(capacity), universal_instrumentation_end_events: VecDeque::with_capacity(capacity), sorted_reparenting_info: VecDeque::with_capacity(capacity), + appsec_contexts: VecDeque::with_capacity(capacity), } } /// Inserts a context into the buffer. If the buffer is full, the oldest `Context` is removed. /// - fn insert(&mut self, context: Context) { + fn insert(&mut self, mut context: Context) { + if let Some(i) = self + .appsec_contexts + .iter() + .position(|(id, _)| id == &context.request_id) + { + context.appsec_context = self.appsec_contexts.remove(i).map(|(_, c)| c); + } + if self.size() == self.buffer.capacity() { self.buffer.pop_front(); - self.buffer.push_back(context); - } else { - if self.get(&context.request_id).is_some() { - self.remove(&context.request_id); - } + } else if self.get(&context.request_id).is_some() { + self.remove(&context.request_id); + } + self.buffer.push_back(context); + } - self.buffer.push_back(context); + /// Binds the provided security context to the invocation context for the given `request_id`. + pub fn bind_security_context(&mut self, request_id: &str, appsec_context: AppSecContext) { + // If we already have a [`Context`] for this `request_id`, we can just directly bind to it. + if let Some(context) = self.get_mut(request_id) { + context.appsec_context = Some(appsec_context); + return; } + + if self.appsec_contexts.len() == self.appsec_contexts.capacity() { + self.appsec_contexts.pop_front(); + } else if let Some(i) = self + .appsec_contexts + .iter() + .position(|(id, _)| id == request_id) + { + self.appsec_contexts.remove(i); + } + self.appsec_contexts + .push_back((request_id.to_string(), appsec_context)); + } + + /// Retrieves the security context bound to the given `request_id`, if one exists. + pub fn get_security_context_mut(&mut self, request_id: &str) -> Option<&mut AppSecContext> { + // Try to get from the context buffer first; not using `get_mut` because this causes the borrow checker to + // consider `self` as mutably borrowed for the entire function call, preventing mutable access to + // `self.appsec_contexts`... + for context in &mut self.buffer { + if context.request_id == request_id { + return context.appsec_context.as_mut(); + } + } + + // If not found in context buffer, try the appsec_contexts buffer + self.appsec_contexts + .iter_mut() + .find_map(|(rid, ctx)| if rid == request_id { Some(ctx) } else { None }) } /// Removes a context from the buffer. Returns the removed `Context` if found. /// pub fn remove(&mut self, request_id: &String) -> Option { + // Purge the associated AppSec context, if any exists. + if let Some(i) = self + .appsec_contexts + .iter() + .position(|(id, _)| id == request_id) + { + self.appsec_contexts.remove(i); + } + if let Some(i) = self .buffer .iter() @@ -184,7 +292,7 @@ impl ContextBuffer { /// Returns a reference to a `Context` from the buffer if found. /// #[must_use] - pub fn get(&self, request_id: &String) -> Option<&Context> { + pub fn get(&self, request_id: &str) -> Option<&Context> { self.buffer .iter() .find(|context| context.request_id == *request_id) @@ -193,7 +301,7 @@ impl ContextBuffer { /// Returns a mutable reference to a `Context` from the buffer if found. /// #[must_use] - pub fn get_mut(&mut self, request_id: &String) -> Option<&mut Context> { + pub fn get_mut(&mut self, request_id: &str) -> Option<&mut Context> { self.buffer .iter_mut() .find(|context| context.request_id == *request_id) @@ -423,6 +531,7 @@ impl ContextBuffer { #[allow(clippy::unwrap_used)] mod tests { use crate::proc::{CPUData, NetworkData}; + use bytes::Bytes; use std::collections::HashMap; use tokio::sync::watch; @@ -773,4 +882,266 @@ mod tests { assert!(result.is_some()); assert_eq!(result.unwrap(), request_id2); } + + #[tokio::test] + async fn test_appsec_context_buffer_capacity() { + let appsec = crate::appsec::processor::Processor::new(&crate::config::Config { + serverless_appsec_enabled: true, + ..crate::config::Config::default() + }) + .expect("Failed to create appsec processor"); + + let ctx = appsec + .process_invocation_next(&Bytes::from_static(include_bytes!( + "../../../tests/payloads/api_gateway_http_event.json" + ))) + .await + .expect("should have produced a security context"); + + let mut buffer = ContextBuffer::with_capacity(3); + buffer.bind_security_context("1", ctx.clone()); + buffer.bind_security_context("2", ctx.clone()); + + // We replace the context for 1 three times, but that should not cause 2 to slip out of the buffer + buffer.bind_security_context("1", ctx.clone()); + buffer.bind_security_context("1", ctx.clone()); + buffer.bind_security_context("1", ctx.clone()); + + assert_eq!(buffer.appsec_contexts.len(), 2); // Capped + assert!( + buffer.get_security_context_mut("1").is_some(), + "request 1 should still be in the buffer" + ); + assert!( + buffer.get_security_context_mut("2").is_some(), + "request 2 should still be in the buffer" + ); + + buffer.bind_security_context("3", ctx.clone()); + // At this point, 2 should slip out of the buffer (1 was replaced later) + buffer.bind_security_context("4", ctx.clone()); + + assert_eq!(buffer.appsec_contexts.len(), 3); // Capped + assert!( + buffer.get_security_context_mut("1").is_some(), + "request 1 should still be in the buffer" + ); + assert!( + buffer.get_security_context_mut("2").is_none(), + "request 2 should still have slipped out of the buffer" + ); + assert!( + buffer.get_security_context_mut("3").is_some(), + "request 3 should still be in the buffer" + ); + assert!( + buffer.get_security_context_mut("4").is_some(), + "request 4 should still be in the buffer" + ); + } + + #[tokio::test] + async fn test_appsec_context_buffer_skip() { + let appsec = crate::appsec::processor::Processor::new(&crate::config::Config { + serverless_appsec_enabled: true, + ..crate::config::Config::default() + }) + .expect("Failed to create appsec processor"); + + let ctx = appsec + .process_invocation_next(&Bytes::from_static(include_bytes!( + "../../../tests/payloads/api_gateway_http_event.json" + ))) + .await + .expect("should have produced a security context"); + + let mut buffer = ContextBuffer::with_capacity(3); + buffer.start_context("1", Span::default()); + buffer.bind_security_context("1", ctx.clone()); + assert!( + buffer.appsec_contexts.is_empty(), + "the appsec context buffer should have been skipped" + ); + + assert!( + buffer + .get("1") + .expect("should have a context for request 1") + .appsec_context + .is_some(), + "the appsec context should have been bound to the invocation context" + ); + assert!( + buffer.get_security_context_mut("1").is_some(), + "the appsec context should be available through the security context buffer" + ); + } + + #[tokio::test] + async fn test_appsec_context_hoist_from_buffer() { + let appsec = crate::appsec::processor::Processor::new(&crate::config::Config { + serverless_appsec_enabled: true, + ..crate::config::Config::default() + }) + .expect("Failed to create appsec processor"); + + let ctx = appsec + .process_invocation_next(&Bytes::from_static(include_bytes!( + "../../../tests/payloads/api_gateway_http_event.json" + ))) + .await + .expect("should have produced a security context"); + + let mut buffer = ContextBuffer::with_capacity(3); + buffer.bind_security_context("1", ctx.clone()); + buffer.start_context("1", Span::default()); + + assert!( + buffer.appsec_contexts.is_empty(), + "the appsec context buffer should have been hoisted from the buffer" + ); + + assert!( + buffer + .get("1") + .expect("should have a context for request 1") + .appsec_context + .is_some(), + "the appsec context should have been bound to the invocation context" + ); + assert!( + buffer.get_security_context_mut("1").is_some(), + "the appsec context should be available through the security context buffer" + ); + } + + #[tokio::test] + async fn test_context_absorb_appsec_tags() { + let appsec = crate::appsec::processor::Processor::new(&crate::config::Config { + serverless_appsec_enabled: true, + ..crate::config::Config::default() + }) + .expect("Failed to create appsec processor"); + + let ctx = appsec + .process_invocation_next(&Bytes::from_static(include_bytes!( + "../../../tests/payloads/api_gateway_http_event.appsec_event.json" + ))) + .await + .expect("should have produced a security context"); + + let mut buffer = ContextBuffer::with_capacity(3); + buffer.start_context("1", Span::default()); + buffer.bind_security_context("1", ctx.clone()); + + let context = buffer + .get_mut("1") + .expect("should have a context for request 1"); + context.absorb_appsec_tags(); + + assert_eq!( + context.invocation_span.meta, + HashMap::from([ + ("_dd.appsec.fp.http.header".to_string(), "hdr-0000000010-40b52535-5-1e6648af".to_string()), + ("_dd.appsec.fp.http.network".to_string(), "net-1-1000000000".to_string()), + ("_dd.appsec.json".to_string(), "[\"{\\\"rule\\\":{\\\"id\\\":\\\"ua0-600-12x\\\",\\\"name\\\":\\\"Arachni\\\",\\\"tags\\\":{\\\"type\\\":\\\"attack_tool\\\",\\\"category\\\":\\\"attack_attempt\\\",\\\"confidence\\\":\\\"1\\\",\\\"module\\\":\\\"waf\\\",\\\"tool_name\\\":\\\"Arachni\\\",\\\"cwe\\\":\\\"200\\\",\\\"capec\\\":\\\"1000/118/169\\\"},\\\"on_match\\\":[]},\\\"rule_matches\\\":[{\\\"operator\\\":\\\"match_regex\\\",\\\"operator_value\\\":\\\"^Arachni\\\\\\\\/v\\\",\\\"parameters\\\":[{\\\"address\\\":\\\"server.request.headers.no_cookies\\\",\\\"key_path\\\":[\\\"user-agent\\\",\\\"0\\\"],\\\"value\\\":\\\"Arachni/v2\\\",\\\"highlight\\\":[\\\"Arachni/v\\\"]}]}]}\"]".to_string()), + ("_dd.origin".to_string(), "appsec".to_string()), + ("appsec.event".to_string(), "true".to_string()), + ("http.route".to_string(), "GET /httpapi/get".to_string()), + ("http.method".to_string(), "GET".to_string()), + ("http.request.headers.accept".to_string(), "*/*".to_string()), + ("http.request.headers.content-length".to_string(), "0".to_string()), + ("http.request.headers.host".to_string(), "x02yirxc7a.execute-api.sa-east-1.amazonaws.com".to_string()), + ("http.request.headers.user-agent".to_string(), "Arachni/v2".to_string()), + ("http.request.headers.x-amzn-trace-id".to_string(), "Root=1-613a52fb-4c43cfc95e0241c1471bfa05".to_string()), + ("http.request.headers.x-forwarded-for".to_string(), "38.122.226.210".to_string()), + ("http.url".to_string(), "/httpapi/get".to_string()), + ("network.client.ip".to_string(), "38.122.226.210".to_string()), + ("request_id".to_string(), "1".to_string()), + ]) + ); + assert_eq!( + context.invocation_span.metrics, + HashMap::from([ + ("_dd.appsec.enabled".to_string(), 1f64), + ( + "_sampling_priority_v1".to_string(), + 2f64 /* USER_KEEP */ + ) + ]) + ); + } + + #[tokio::test] + async fn test_context_absorb_appsec_tags_no_event() { + let appsec = crate::appsec::processor::Processor::new(&crate::config::Config { + serverless_appsec_enabled: true, + ..crate::config::Config::default() + }) + .expect("Failed to create appsec processor"); + + let ctx = appsec + .process_invocation_next(&Bytes::from_static(include_bytes!( + "../../../tests/payloads/api_gateway_http_event.json" + ))) + .await + .expect("should have produced a security context"); + + let mut buffer = ContextBuffer::with_capacity(3); + buffer.start_context("1", Span::default()); + buffer.bind_security_context("1", ctx.clone()); + + let context = buffer + .get_mut("1") + .expect("should have a context for request 1"); + context.absorb_appsec_tags(); + + assert_eq!( + context.invocation_span.meta, + HashMap::from([ + ( + "_dd.appsec.fp.http.header".to_string(), + "hdr-0000000010-47a6a72c-5-1e6648af".to_string() + ), + ( + "_dd.appsec.fp.http.network".to_string(), + "net-1-1000000000".to_string() + ), + ("_dd.origin".to_string(), "appsec".to_string()), + ("http.method".to_string(), "GET".to_string()), + ("http.request.headers.accept".to_string(), "*/*".to_string()), + ( + "http.request.headers.user-agent".to_string(), + "curl/7.64.1".to_string() + ), + ( + "http.request.headers.x-amzn-trace-id".to_string(), + "Root=1-613a52fb-4c43cfc95e0241c1471bfa05".to_string() + ), + ("http.url".to_string(), "/httpapi/get".to_string()), + ("request_id".to_string(), "1".to_string()), + ]) + ); + assert_eq!( + context.invocation_span.metrics, + HashMap::from([("_dd.appsec.enabled".to_string(), 1f64)]) + ); + } + + #[tokio::test] + async fn test_context_absorb_appsec_tags_no_appsec_context() { + let mut buffer = ContextBuffer::with_capacity(3); + buffer.start_context("1", Span::default()); + + let context = buffer + .get_mut("1") + .expect("should have a context for request 1"); + context.absorb_appsec_tags(); + + assert_eq!( + context.invocation_span.meta, + HashMap::from([("request_id".to_string(), "1".to_string()),]) + ); + assert!(context.invocation_span.metrics.is_empty()); + } } diff --git a/bottlecap/src/lifecycle/invocation/processor.rs b/bottlecap/src/lifecycle/invocation/processor.rs index 297b68e4d..44cb156d7 100644 --- a/bottlecap/src/lifecycle/invocation/processor.rs +++ b/bottlecap/src/lifecycle/invocation/processor.rs @@ -12,6 +12,7 @@ use serde_json::{Value, json}; use tokio::sync::{mpsc::Sender, watch}; use tracing::{debug, warn}; +use crate::appsec::processor::AppSecContext; use crate::{ config::{self, aws::AwsConfig}, lifecycle::invocation::{ @@ -48,7 +49,7 @@ pub const DATADOG_INVOCATION_ERROR_MESSAGE_KEY: &str = "x-datadog-invocation-err pub const DATADOG_INVOCATION_ERROR_TYPE_KEY: &str = "x-datadog-invocation-error-type"; pub const DATADOG_INVOCATION_ERROR_STACK_KEY: &str = "x-datadog-invocation-error-stack"; pub const DATADOG_INVOCATION_ERROR_KEY: &str = "x-datadog-invocation-error"; -const TAG_SAMPLING_PRIORITY: &str = "_sampling_priority_v1"; +pub const TAG_SAMPLING_PRIORITY: &str = "_sampling_priority_v1"; pub struct Processor { /// Buffer containing context of the previous 5 invocations @@ -111,6 +112,23 @@ impl Processor { } } + /// Binds the provided security context to the invocation context for the given `request_id`. + /// + /// This may happen before [`on_invoke_event`][Self::on_invoke_event] is called and hence cannot assume any + /// request-specific state has been initialized at this time. + pub(crate) fn bind_security_context(&mut self, request_id: &str, context: AppSecContext) { + self.context_buffer + .bind_security_context(request_id, context); + } + + /// Retrieves the security context bound to the given `request_id`, if one exists. + pub(crate) fn get_security_context_mut( + &mut self, + request_id: &str, + ) -> Option<&mut AppSecContext> { + self.context_buffer.get_security_context_mut(request_id) + } + /// Given a `request_id`, creates the context and adds the enhanced metric offsets to the context buffer. /// pub fn on_invoke_event(&mut self, request_id: String) { @@ -390,7 +408,8 @@ impl Processor { _ = offsets.process_chan_tx.send(()); } - // todo(duncanista): Add missing metric tags for ASM + context.absorb_appsec_tags(); + // Add dynamic and trigger tags context .invocation_span @@ -541,12 +560,7 @@ impl Processor { /// If the `request_id` is not found in the context buffer, return `None`. /// If the `runtime_duration_ms` hasn't been seen, return `None`. /// - pub fn on_platform_report( - &mut self, - request_id: &String, - metrics: ReportMetrics, - timestamp: i64, - ) { + pub fn on_platform_report(&mut self, request_id: &str, metrics: ReportMetrics, timestamp: i64) { // Set the report log metrics self.enhanced_metrics .set_report_log_metrics(&metrics, timestamp); diff --git a/bottlecap/src/logs/lambda/processor.rs b/bottlecap/src/logs/lambda/processor.rs index a165f6cea..438e68f04 100644 --- a/bottlecap/src/logs/lambda/processor.rs +++ b/bottlecap/src/logs/lambda/processor.rs @@ -163,7 +163,7 @@ impl LambdaProcessor { error!("Failed to send PlatformRuntimeDone to the main event bus: {}", e); } - let mut message = format!("END RequestId: {request_id}"); + let mut message = format!("END RequestId: {request_id}"); let mut result_status = "info".to_string(); if let Some(metrics) = metrics { self.invocation_context.runtime_duration_ms = metrics.duration_ms; diff --git a/bottlecap/src/proxy/interceptor.rs b/bottlecap/src/proxy/interceptor.rs index dbb0100fc..26bfbc93e 100644 --- a/bottlecap/src/proxy/interceptor.rs +++ b/bottlecap/src/proxy/interceptor.rs @@ -1,6 +1,11 @@ +use crate::appsec; use crate::{ - EXTENSION_HOST, config::aws::AwsConfig, http::extract_request_body, - lifecycle::invocation::processor::Processor as InvocationProcessor, lwa, + EXTENSION_HOST, + appsec::processor::Processor as AppSecProcessor, + config::{Config, aws::AwsConfig}, + http::extract_request_body, + lifecycle::invocation::processor::Processor as InvocationProcessor, + lwa, }; use axum::{ Router, @@ -30,10 +35,12 @@ type InterceptorState = ( Arc, Arc>, Arc>, + Option>, Arc>>, ); pub fn start( + config: Arc, aws_config: Arc, invocation_processor: Arc>, ) -> Result> { @@ -48,15 +55,21 @@ pub fn start( .pool_max_idle_per_host(8) .build(connector); + let appsec_processor = if appsec::is_enabled(&config) { + Some(Arc::new(AppSecProcessor::new(config.as_ref())?)) + } else { + None + }; + let tasks = Arc::new(Mutex::new(JoinSet::new())); let state: InterceptorState = ( aws_config, Arc::new(client), invocation_processor, - tasks.clone(), + appsec_processor, + Arc::clone(&tasks), ); - let tasks_clone = tasks.clone(); let shutdown_token_clone = shutdown_token.clone(); tokio::spawn(async move { let server = TcpListener::bind(&socket) @@ -65,7 +78,7 @@ pub fn start( let router = make_router(state); debug!("PROXY | Starting API runtime proxy on {socket}"); axum::serve(server, router) - .with_graceful_shutdown(graceful_shutdown(tasks_clone, shutdown_token_clone)) + .with_graceful_shutdown(graceful_shutdown(tasks, shutdown_token_clone)) .await .expect("Failed to start API runtime proxy"); }); @@ -125,7 +138,9 @@ fn get_proxy_socket_address(aws_lwa_proxy_lambda_runtime_api: Option<&String>) - async fn invocation_next_proxy( Path(api_version): Path, - State((aws_config, client, invocation_processor, tasks)): State, + State((aws_config, client, invocation_processor, appsec_processor, tasks)): State< + InterceptorState, + >, request: Request, ) -> Response { debug!("PROXY | invocation_next_proxy | api_version: {api_version}"); @@ -147,12 +162,13 @@ async fn invocation_next_proxy( error!("PROXY | passthrough_proxy | error proxying request: {e}"); return ( StatusCode::INTERNAL_SERVER_ERROR, - format!("Failed to proxy request: {e}"), + format!("Failed to build forward response: {e}"), ) .into_response(); } }; + // LWA if aws_config.aws_lwa_proxy_lambda_runtime_api.is_some() { let mut tasks = tasks.lock().await; @@ -169,6 +185,25 @@ async fn invocation_next_proxy( }); } + // K9 / ASM + if let Some(appsec_processor) = appsec_processor { + if let Some(request_id) = intercepted_parts + .headers + .get("Lambda-Runtime-Aws-Request-Id") + { + if let Ok(request_id) = request_id.to_str() { + if let Some(context) = appsec_processor + .process_invocation_next(&intercepted_bytes) + .await + { + let invocation_processor = invocation_processor.clone(); + let mut invocation_processor = invocation_processor.lock().await; + invocation_processor.bind_security_context(request_id, context); + } + } + } + } + match build_forward_response(intercepted_parts, intercepted_bytes) { Ok(r) => r, Err(e) => { @@ -184,7 +219,9 @@ async fn invocation_next_proxy( async fn invocation_response_proxy( Path((api_version, request_id)): Path<(String, String)>, - State((aws_config, client, invocation_processor, tasks)): State, + State((aws_config, client, invocation_processor, appsec_processor, tasks)): State< + InterceptorState, + >, request: Request, ) -> Response { debug!( @@ -201,6 +238,7 @@ async fn invocation_response_proxy( } }; + // LWA if aws_config.aws_lwa_proxy_lambda_runtime_api.is_some() { let mut tasks = tasks.lock().await; @@ -211,6 +249,17 @@ async fn invocation_response_proxy( }); } + // K9 / ASM + if let Some(appsec_processor) = appsec_processor { + let invocation_processor = invocation_processor.clone(); + let mut invocation_processor = invocation_processor.lock().await; + if let Some(appsec_context) = invocation_processor.get_security_context_mut(&request_id) { + appsec_processor + .process_invocation_response(appsec_context, &body_bytes) + .await; + } + } + let (intercepted_parts, intercepted_bytes) = match proxy_request(&client, &aws_config, parts, body_bytes).await { Ok(r) => r, @@ -238,7 +287,7 @@ async fn invocation_response_proxy( } async fn passthrough_proxy( - State((aws_config, client, _, _)): State, + State((aws_config, client, _, _, _)): State, request: Request, ) -> Response { let (parts, body_bytes) = match extract_request_body(request).await { @@ -340,6 +389,7 @@ fn build_proxy_request( Ok(request) } +#[allow(clippy::unwrap_used)] #[cfg(test)] mod tests { use std::{ @@ -411,8 +461,8 @@ mod tests { metrics_aggregator, ))); - let proxy_handle = - start(aws_config, invocation_processor).expect("Failed to start API runtime proxy"); + let proxy_handle = start(config.clone(), aws_config, invocation_processor) + .expect("Failed to start API runtime proxy"); let https = HttpConnector::new(); let client = Client::builder(hyper_util::rt::TokioExecutor::new()) .build::<_, http_body_util::Full>(https); @@ -445,4 +495,158 @@ mod tests { proxy_handle.cancel(); final_destination.abort(); } + + #[tokio::test] + async fn test_proxy_with_appsec_enabled_and_valid_config() { + let config = Arc::new(Config { + serverless_appsec_enabled: true, + ..Config::default() + }); + let aws_config = Arc::new(AwsConfig { + region: "us-east-1".to_string(), + function_name: "test-function".to_string(), + sandbox_init_time: Instant::now(), + runtime_api: "127.0.0.1:9001".to_string(), + aws_lwa_proxy_lambda_runtime_api: None, + exec_wrapper: Some("/opt/datadog_wrapper".to_string()), + }); + + let tags_provider = Arc::new(Provider::new( + Arc::clone(&config), + LAMBDA_RUNTIME_SLUG.to_string(), + &HashMap::new(), + )); + let metrics_aggregator = Arc::new(Mutex::new( + MetricsAggregator::new(EMPTY_TAGS, 1024).unwrap(), + )); + + let invocation_processor = Arc::new(TokioMutex::new(InvocationProcessor::new( + Arc::clone(&tags_provider), + Arc::clone(&config), + aws_config.clone(), + metrics_aggregator, + ))); + + // Should be able to start proxy successfully with valid AppSec config + let result = start(config, aws_config, invocation_processor); + assert!(result.is_ok()); + + // Clean up + if let Ok(handle) = result { + handle.cancel(); + } + } + + #[test] + fn test_proxy_with_appsec_enabled_and_invalid_rules() { + let config = Arc::new(Config { + serverless_appsec_enabled: true, + appsec_rules: Some("/nonexistent/path/to/rules.json".to_string()), + ..Config::default() + }); + let aws_config = Arc::new(AwsConfig { + region: "us-east-1".to_string(), + function_name: "test-function".to_string(), + sandbox_init_time: Instant::now(), + runtime_api: "127.0.0.1:9001".to_string(), + aws_lwa_proxy_lambda_runtime_api: None, + exec_wrapper: Some("/opt/datadog_wrapper".to_string()), + }); + + let tags_provider = Arc::new(Provider::new( + Arc::clone(&config), + LAMBDA_RUNTIME_SLUG.to_string(), + &HashMap::new(), + )); + let metrics_aggregator = Arc::new(Mutex::new( + MetricsAggregator::new(EMPTY_TAGS, 1024).unwrap(), + )); + + let invocation_processor = Arc::new(TokioMutex::new(InvocationProcessor::new( + Arc::clone(&tags_provider), + Arc::clone(&config), + aws_config.clone(), + metrics_aggregator, + ))); + + // Should fail to start proxy with invalid AppSec rules + let result = start(config, aws_config, invocation_processor); + assert!(result.is_err()); + + // The error should be related to AppSec processor initialization + let error = result.expect_err("Expected an error"); + // The actual error message might vary, but it should indicate a file system issue + assert!( + error.to_string().contains("Failed to open") + || error.to_string().contains("No such file") + ); + } + + #[tokio::test] + async fn test_proxy_with_appsec_disabled() { + let config = Arc::new(Config { + serverless_appsec_enabled: false, + ..Config::default() + }); + let aws_config = Arc::new(AwsConfig { + region: "us-east-1".to_string(), + function_name: "test-function".to_string(), + sandbox_init_time: Instant::now(), + runtime_api: "127.0.0.1:9001".to_string(), + aws_lwa_proxy_lambda_runtime_api: None, + exec_wrapper: Some("/opt/datadog_wrapper".to_string()), + }); + + let tags_provider = Arc::new(Provider::new( + Arc::clone(&config), + LAMBDA_RUNTIME_SLUG.to_string(), + &HashMap::new(), + )); + let metrics_aggregator = Arc::new(Mutex::new( + MetricsAggregator::new(EMPTY_TAGS, 1024).unwrap(), + )); + + let invocation_processor = Arc::new(TokioMutex::new(InvocationProcessor::new( + Arc::clone(&tags_provider), + Arc::clone(&config), + aws_config.clone(), + metrics_aggregator, + ))); + + // Should be able to start proxy successfully with AppSec disabled + let result = start(config, aws_config, invocation_processor); + assert!(result.is_ok()); + + // Clean up + if let Ok(handle) = result { + handle.cancel(); + } + } + + #[test] + fn test_get_proxy_socket_address_with_lwa_proxy() { + let aws_lwa_proxy_lambda_runtime_api = Some("127.0.0.1:12345".to_string()); + let socket_addr = get_proxy_socket_address(aws_lwa_proxy_lambda_runtime_api.as_ref()); + + // Should use the LWA proxy address + assert_eq!(socket_addr.to_string(), "127.0.0.1:12345"); + } + + #[test] + fn test_get_proxy_socket_address_without_lwa_proxy() { + let aws_lwa_proxy_lambda_runtime_api = None; + let socket_addr = get_proxy_socket_address(aws_lwa_proxy_lambda_runtime_api.as_ref()); + + // Should use the default interceptor address (0.0.0.0:9000) + assert_eq!(socket_addr.to_string(), "0.0.0.0:9000"); + } + + #[test] + fn test_get_proxy_socket_address_with_invalid_lwa_proxy() { + let aws_lwa_proxy_lambda_runtime_api = Some("invalid-address".to_string()); + let socket_addr = get_proxy_socket_address(aws_lwa_proxy_lambda_runtime_api.as_ref()); + + // Should fall back to default interceptor address when LWA proxy is invalid + assert_eq!(socket_addr.to_string(), "0.0.0.0:9000"); + } } diff --git a/bottlecap/src/proxy/mod.rs b/bottlecap/src/proxy/mod.rs index 94081c636..99af59dc3 100644 --- a/bottlecap/src/proxy/mod.rs +++ b/bottlecap/src/proxy/mod.rs @@ -11,7 +11,7 @@ pub mod interceptor; /// - ASM is enabled and the `AWS_LAMBDA_EXEC_WRAPPER` environment variable is set to `/opt/datadog_wrapper` #[must_use] #[allow(clippy::module_name_repetitions)] -pub fn should_start_proxy(config: &Arc, aws_config: Arc) -> bool { +pub fn should_start_proxy(config: &Arc, aws_config: &Arc) -> bool { let lwa_proxy_set = aws_config.aws_lwa_proxy_lambda_runtime_api.is_some(); let datadog_wrapper_set = aws_config .exec_wrapper @@ -42,7 +42,7 @@ mod tests { sandbox_init_time: Instant::now(), exec_wrapper: Some("/opt/datadog_wrapper".to_string()), }); - assert!(should_start_proxy(&config, aws_config)); + assert!(should_start_proxy(&config, &aws_config)); } #[test] fn test_should_start_proxy_lwa_proxy_set() { @@ -56,7 +56,7 @@ mod tests { sandbox_init_time: Instant::now(), exec_wrapper: None, }); - assert!(should_start_proxy(&config, aws_config)); + assert!(should_start_proxy(&config, &aws_config)); } #[test] @@ -74,7 +74,7 @@ mod tests { sandbox_init_time: Instant::now(), exec_wrapper: Some("/opt/datadog_wrapper".to_string()), }); - assert!(should_start_proxy(&config, aws_config)); + assert!(should_start_proxy(&config, &aws_config)); } #[test] @@ -92,7 +92,7 @@ mod tests { sandbox_init_time: Instant::now(), exec_wrapper: Some("/opt/datadog_wrapper".to_string()), }); - assert!(!should_start_proxy(&config, aws_config)); + assert!(!should_start_proxy(&config, &aws_config)); } #[test] @@ -111,6 +111,6 @@ mod tests { // Datadog wrapper is not set, so we should not start the proxy exec_wrapper: Some("/opt/not_datadog".to_string()), }); - assert!(!should_start_proxy(&config, aws_config)); + assert!(!should_start_proxy(&config, &aws_config)); } } diff --git a/bottlecap/tests/payloads/api_gateway_http_event.appsec_event.json b/bottlecap/tests/payloads/api_gateway_http_event.appsec_event.json new file mode 100644 index 000000000..aae47ed10 --- /dev/null +++ b/bottlecap/tests/payloads/api_gateway_http_event.appsec_event.json @@ -0,0 +1,38 @@ +{ + "version": "2.0", + "routeKey": "GET /httpapi/get", + "rawPath": "/httpapi/get", + "rawQueryString": "", + "headers": { + "Accept": "*/*", + "Content-Length": "0", + "Host": "x02yirxc7a.execute-api.sa-east-1.amazonaws.com", + "User-Agent": "Arachni/v2", + "X-amzn-trace-id": "Root=1-613a52fb-4c43cfc95e0241c1471bfa05", + "X-forwarded-for": "38.122.226.210", + "X-forwarded-port": "443", + "X-forwarded-proto": "https", + "X-datadog-trace-id": "12345", + "X-datadog-parent-id": "67890", + "X-datadog-sampling-priority": "2" + }, + "requestContext": { + "accountId": "425362996713", + "apiId": "x02yirxc7a", + "domainName": "x02yirxc7a.execute-api.sa-east-1.amazonaws.com", + "domainPrefix": "x02yirxc7a", + "http": { + "method": "GET", + "path": "/httpapi/get", + "protocol": "HTTP/1.1", + "sourceIp": "38.122.226.210", + "userAgent": "curl/7.64.1" + }, + "requestId": "FaHnXjKCGjQEJ7A=", + "routeKey": "GET /httpapi/get", + "stage": "$default", + "time": "09/Sep/2021:18:31:23 +0000", + "timeEpoch": 1631212283738 + }, + "isBase64Encoded": false +} diff --git a/bottlecap/tests/payloads/api_gateway_proxy_event.appsec_event.json b/bottlecap/tests/payloads/api_gateway_proxy_event.appsec_event.json new file mode 100644 index 000000000..9715c3a2f --- /dev/null +++ b/bottlecap/tests/payloads/api_gateway_proxy_event.appsec_event.json @@ -0,0 +1,127 @@ +{ + "body": "eyJ0ZXN0IjoiYm9keSJ9", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": true, + "queryStringParameters": { + "foo": "bar" + }, + "multiValueQueryStringParameters": { + "foo": [ + "bar" + ] + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Arachni/v2", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https", + "X-Datadog-Trace-Id": "12345", + "X-Datadog-Parent-Id": "67890", + "x-datadog-sampling-priority": "2" + }, + "multiValueHeaders": { + "Accept": [ + "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" + ], + "Accept-Encoding": [ + "gzip, deflate, sdch" + ], + "Accept-Language": [ + "en-US,en;q=0.8" + ], + "Cache-Control": [ + "max-age=0" + ], + "CloudFront-Forwarded-Proto": [ + "https" + ], + "CloudFront-Is-Desktop-Viewer": [ + "true" + ], + "CloudFront-Is-Mobile-Viewer": [ + "false" + ], + "CloudFront-Is-SmartTV-Viewer": [ + "false" + ], + "CloudFront-Is-Tablet-Viewer": [ + "false" + ], + "CloudFront-Viewer-Country": [ + "US" + ], + "Host": [ + "0123456789.execute-api.us-east-1.amazonaws.com" + ], + "Upgrade-Insecure-Requests": [ + "1" + ], + "User-Agent": [ + "Arachni/v2" + ], + "Via": [ + "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" + ], + "X-Amz-Cf-Id": [ + "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" + ], + "X-Forwarded-For": [ + "127.0.0.1, 127.0.0.2" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "domainName": "70ixmpl4fl.execute-api.us-east-2.amazonaws.com", + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/images/Dockerfile.bottlecap.alpine.compile b/images/Dockerfile.bottlecap.alpine.compile index 412d88d5c..39aaf9aa9 100644 --- a/images/Dockerfile.bottlecap.alpine.compile +++ b/images/Dockerfile.bottlecap.alpine.compile @@ -1,10 +1,12 @@ -FROM registry.ddbuild.io/images/mirror/alpine:3.16 AS compiler +FROM registry.ddbuild.io/images/mirror/alpine:3.22.0 AS compiler ARG PLATFORM ARG FIPS # Install dependencies -RUN apk add --no-cache curl gcc musl-dev make unzip bash autoconf automake libtool g++ perl go cmake linux-headers; +RUN apk add --no-cache curl gcc build-base make unzip bash \ + autoconf automake libtool g++ perl go cmake linux-headers \ + clang-libclang SHELL ["/bin/bash", "-c"] @@ -26,11 +28,15 @@ COPY ./bottlecap/Cargo.lock /tmp/dd/bottlecap/Cargo.lock # Build the binary # -# Added `-C link-arg=-lgcc` for alpine. -ENV RUSTFLAGS="-C panic=abort -C link-arg=-lgcc" +# Added `-C link-arg=-lgcc` and `-Ctarget-feature=-crt-static` for alpine, as otherwise `bindgen` is +# unable to load `libclang` dynamically (on `musl`, the C runtime is static by default, and that +# makes it incapable of dynamically loading libraries - static-linking `libclang` would be possible +# if it was not such a pain to build in that case). +ENV RUSTFLAGS="-Cpanic=abort -Clink-arg=-lgcc -Ctarget-feature=-crt-static" WORKDIR /tmp/dd/bottlecap -RUN --mount=type=cache,target=/root/.cargo/registry \ +RUN --mount=type=cache,target=/root/.cargo/git \ + --mount=type=cache,target=/root/.cargo/registry \ export PROFILE="release"; \ if [ "$FIPS" = "1" ]; then \ export FEATURES=fips; \ @@ -41,8 +47,12 @@ RUN --mount=type=cache,target=/root/.cargo/registry \ export FEATURES=default; \ fi; \ env; \ - cargo +stable build --verbose --no-default-features --features $FEATURES --profile $PROFILE --target $PLATFORM-unknown-linux-musl; \ - cp /tmp/dd/bottlecap/target/$PLATFORM-unknown-linux-musl/$PROFILE/bottlecap /tmp/dd/bottlecap/bottlecap + # Explicitly NOT passing `--target` as this makes the `RUSTFLAGS` (from any source) not apply to `build.rs` + # compilation, which in turns means `libddwaf`'s builder fails to build because on MUSL platforms it can only work + # with `-Ctarget-feature=-crt-runtime` being specified. There is currently no way to set `RUSTFLAGS` for `build.rs` + # compilation if `--target` is passed (see: https://github.com/rust-lang/cargo/issues/4423). + cargo +stable build --verbose --no-default-features --features $FEATURES --profile $PROFILE; \ + cp /tmp/dd/bottlecap/target/$PROFILE/bottlecap /tmp/dd/bottlecap/bottlecap # Use the smallest image possible FROM scratch diff --git a/images/Dockerfile.bottlecap.compile b/images/Dockerfile.bottlecap.compile index 4194996da..97b59c529 100644 --- a/images/Dockerfile.bottlecap.compile +++ b/images/Dockerfile.bottlecap.compile @@ -4,7 +4,7 @@ ARG PLATFORM ARG FIPS # Install dependencies -RUN yum install -y curl clang make unzip cmake3 perl go +RUN yum install -y curl clang compiler-rt make unzip cmake3 perl go # Install Protocol Buffers compiler by hand, since AL2 does not have a recent enough version. COPY ./scripts/install-protoc.sh / @@ -24,7 +24,7 @@ COPY ./bottlecap/Cargo.toml /tmp/dd/bottlecap/Cargo.toml COPY ./bottlecap/Cargo.lock /tmp/dd/bottlecap/Cargo.lock # Build the binary -ENV RUSTFLAGS="-C panic=abort" +ENV RUSTFLAGS="-Cpanic=abort" ENV AWS_LC_FIPS_SYS_CC=clang ENV AWS_LC_FIPS_SYS_CXX=clang++ @@ -35,6 +35,7 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \ else \ export FEATURES=default; \ fi; \ + export RUSTFLAGS="${RUSTFLAGS} -Clinker=clang -L$(dirname $(clang --print-file-name="libclang_rt.builtins-$(uname -m).a")) -lclang_rt.builtins-$(uname -m)"; \ cargo +stable build --no-default-features --features $FEATURES --release --target $PLATFORM-unknown-linux-gnu; RUN cp /tmp/dd/bottlecap/target/$PLATFORM-unknown-linux-gnu/release/bottlecap /tmp/dd/bottlecap/bottlecap diff --git a/scripts/build_bottlecap_layer.sh b/scripts/build_bottlecap_layer.sh index d18fe8623..89fea9ee1 100755 --- a/scripts/build_bottlecap_layer.sh +++ b/scripts/build_bottlecap_layer.sh @@ -37,7 +37,7 @@ rm -rf ${EXTENSION_PATH} 2>/dev/null cd $ROOT_DIR -ALPINE=0 +ALPINE=${ALPINE:-0} ARCHITECTURE=$ARCHITECTURE ALPINE=$ALPINE FILE_SUFFIX=$FILE_SUFFIX .gitlab/scripts/compile_bottlecap.sh docker buildx build --platform linux/${ARCHITECTURE} \