diff --git a/filters/audits/fortiweb.md b/filters/audits/fortiweb.md new file mode 100644 index 000000000..f1c71f6ae --- /dev/null +++ b/filters/audits/fortiweb.md @@ -0,0 +1,177 @@ +# FortiWeb parsing and detection contract + +This draft fixes the FortiWeb filter and all seven existing FortiWeb rules, and adds +separate XSS, Known Exploits and Trojans detections. It targets UTMStack `v11`. +No customer configuration or indexed data was changed. Public fixtures are synthetic. + +## Confirmed defects + +Read-only inspection of retained production events and the loaded configuration confirmed: + +- Space-delimited KV truncates quoted classifications: `sub_type="SQL Injection"` + becomes `log.subtype="\"SQL"`; `main_type="Signature Detection"` is also truncated. + The vendor keys are sanitized to `subtype`, `maintype`, `attacktype`, etc.; rules + querying `log.attack_type` do not read those fields. +- The old message parser stops at embedded `key=` text, including a policy name + inside `msg`. A truncated message cannot reliably identify the attack. +- The rules compare lowercase actions against observed `Alert` and `Alert_Deny`. + `Alert_Deny` and `Period_Block` were not normalized to `actionResult=denied`. + A detection with `Alert` is not evidence of successful exploitation. +- The deployed web-shell expression fails SDK CEL compilation because of invalid + string escapes. Its old generic upload-restriction alternative also does not + establish web-shell activity. +- Several history queries counted every event from an address, including unrelated + traffic and requests to another target. Dedicated XSS and Known Exploits rules + were missing; the retained Trojans class also warrants a discrete detection. + +Source/destination direction in the inspected events was already correct. This is +not evidence of an IP inversion. YAML newline escaping, `afterEvents`, custom +`log.*` names and empty noncapturing grok field names are supported, not defects. + +## Producer and consumer changes + +The filter keeps KV for other vendor metadata and re-extracts authoritative fields +with quote-aware RE2 token boundaries. Spaces and embedded assignments survive; +assignments inside quoted request/message values cannot supply IPs, decisions or +classifications. The existing sanitized `log.*` names remain available. + +| Vendor value | Output and use | +| --- | --- | +| `src` / `src_ip`, `dst` / `dst_ip` / `dest_ip` | `origin.ip` / `target.ip`; rules map origin to adversary | +| Source/destination port aliases | SDK numeric ports; invalid values retained under `log.unparsed*Port` | +| Invalid/unspecified IP | Retained under canonical vendor `log.src` / `log.dst`; rejected at the original rename before geolocation | +| `sub_type`, `main_type`, `attack_type` | Full `log.subtype`, `log.maintype`, `log.attacktype` for detection | +| `signature_id`, `signature_subclass`, OWASP fields | Full existing sanitized vendor fields | +| `action` | Original decision in `log.action`; attack/traffic activity becomes `action=http_request` | +| Explicit deny/block variants | `actionResult=denied`; `Alert` and unknown decisions leave the outcome unset | +| `proto` | Recognized names/identifiers become lowercase `protocol`; original remains in `log.proto` | +| `severity_level` | Low → info, Medium → warning, High → error; critical/debug retain their standard names; original remains | +| `HTTP_agent` | Complete quoted user-agent retained in `log.httpagent`; no SDK standard user-agent field | +| `HTTP_url`, `HTTP_host` | Relative request URL stays in `log.httpurl`; full HTTP(S) URL goes to `target.url`; Host header stays in `log.httphost` | + +All ten rules read the fields this filter produces and accept the observed decision +casing. SQLi, XSS, known exploits, malware, authentication bypass and SSRF/web-shell +signatures use discrete triggers, following the wiki's high-fidelity detection guidance. +SQLi/XSS prefer classification over message text. SSRF needs explicit SSRF evidence; +`localhost` alone does not qualify. Authentication notification policy names and +account lockouts do not establish bypass. Generic script uploads do not establish a web shell. + +Generic attacks, upload-policy violations and selected medium-or-higher HTTP/OWASP +violations keep history thresholds. They constrain source, target, attack subtype, +event type and vendor decision, and use alert-side deduplication. The filter's +`log.fileUploadViolation` classification also constrains upload history to eligible +violations, without an exact-term dependency on potentially long message strings. +OWASP history additionally constrains severity, main type and OWASP category. +Routine GEO/IP-reputation blocks, low-severity missing Content-Type, duplicate +parameters and low-severity information disclosure do not match these rules. + +## Meeting follow-up and current evidence + +The follow-up uses the same SDK authority as the initial incident review: +`plugins/alerts/go.mod` pins go-sdk v1.1.31, and names/types come from its protobuf; +the official wiki supplies field semantics. In particular, `Side.path` is a +filesystem directory, not a relative HTTP request URL. The new draft removes that +promotion, retaining `log.httpurl` and existing rule consumers. No rule depends +on `target.path`. Consumers of the earlier draft's relative `target.path` should +use `log.httpurl`; no synthetic scheme or Host-header identity is invented. + +IP checks now occur at the original `log.src` / `log.dst` promotion, after quote +cleanup. The existing first-matching-token precedence across `src`/`src_ip` and +`dst`/`dst_ip`/`dest_ip` is unchanged. Invalid selected values remain in those +canonical vendor fields, rather than being promoted and moved to an auxiliary +`log.unparsed*Ip` field afterward. Such auxiliary fields are supported by the SDK; +the change concerns validation order and preservation of vendor values. Semantic CIDR exclusions reject all unspecified +representations, including expanded IPv6 and IPv4-mapped zero. Valid IPv6 and +mapped nonzero IPv4 retain their identities. The existing anchored envelope and +quote-aware extraction need no parser rewrite; the added tests also exercise +residual fields created by naive KV parsing before authoritative recovery. + +The bounded current sample contains 24 distinct records from two instances +(12 per instance), selected by recent/subtype strata. Both deployed filter +copies have SHA-256 +`d88a8909e39f84c70050270f553310b6a9bcf8abfff804e613e69c5b235d8bb6`. +Private document IDs, raw payloads and instance provenance remain in the private +evidence pack. Public fixtures are fabricated; this stratified sample is not a +population-rate estimate. + +- All 24 stored source/destination IP pairs and ports agree with the raw values; + no role inversion was observed. The model preserves the stored network fields. +- 23 of 24 stored user-agent values were truncated. All 24 complete values are + now recovered and compared directly with their raw quoted value. User-agent + stays vendor-specific because the SDK has no equivalent standard field. +- Eight explicit blocking decisions lack a stored standardized outcome; the + model produces `actionResult=denied` for those eight. Existing native decision + classifications are preserved. `Alert` and other decisions remain unset without explicit outcome evidence; + the sampled `Erase` value is not assigned an unverified success meaning. +- All 24 request URLs are relative. All observed usernames are placeholders and + do not establish a real user identity. Signature CVE data is either a + placeholder or a multi-value string; it remains under `log.signaturecveid`. +- Native event time is retained as a 19-digit Unix-nanosecond value, while indexed + `deviceTime` equals ingestion `@timestamp`. A supported epoch-nanosecond + conversion is not documented in the available filter wiki. Preserve the native + field rather than putting that integer string into the timestamp schema. +- HTTP response codes and request/response byte fields were not present in this + sample. Vendor documentation of such fields does not prove a mapping defect + in these observed events. + +## Verification and limits + +- The standalone suite has **102 fabricated raw cases**, each evaluated against + all ten actual SDK rule predicates, with strict configuration decoding and + final SDK Event conversion. Original incident fixtures remain covered. +- Raw tests now include observed space-delimited KV behavior, so fake fields + inside quoted messages are created before the filter must clear/recover them. + They test malformed quotes, absent original IPs, alias precedence, unspecified + representations, quoted IPs, embedded headers, user-agent recovery and relative + URLs. External geolocation is not called; every address reaching that step is + checked as valid and non-unspecified. +- All three threshold rules execute actual SDK history requests against a local + loopback HTTP mock: count-minus-one/count, inside/expired windows, identity and + classification mismatches, benign raw histories and all 19 required placeholder + deletions are covered. Existing counts/windows remain 3/15 minutes for Generic + Attacks, 3/30 minutes for upload violations and 5/15 minutes for OWASP. Missing + required values are rejected by the trigger and by direct SDK placeholder + resolution. The seven discrete detections need no history queries. +- The optional 24-document private replay preserves source/destination IPs, + ports and ingress source; all ten predicates execute without CEL errors. + Modeled predicate candidates are one Known Exploits, one SQLi, one OWASP and + two Generic Attacks events. These are neither observed alerts nor proof that + live history thresholds were met; no matches for another class is not a defect. +- The shared manifest lists the filter and all ten consumers, with one explicit + negative control showing that absent extracted identities match none of the ten + predicates. This normalization-only control does not prove raw extraction or + IP validation; the standalone raw suite supplies that model coverage. +- The current shared-runner overlay passes 133 test/subtest records; the optional + private replay is skipped in that overlay and passes separately when enabled. +- `go test ./...` in `plugins/alerts` and `git diff --check` pass. SDK history tests + use localhost only; no customer endpoint is contacted by the test suite. + +The raw harness is an explicit offline model of documented grok concatenation, +Go RE2, observed KV splitting and filter transforms. It does not run the closed +EventProcessor, external enrichment, live OpenSearch, deduplication or alert +publication. Actual history windows use a processing-time lower bound on +`@timestamp`, not strict event-time sequencing. Classification markers on older +indexed documents are not backfilled; stage the filter and rule contracts +together and allow applicable windows to warm up. The standalone tests need no +shared runner. Review shared alert grouping fix #2590 before rollout, since it +affects actual grouping/deduplication behavior. Added user-agent recovery is one +bounded field; assess parser throughput with representative message sizes in +staging. Cross-appliance history/grouping isolation is not established by this +sample; the current rules retain their existing source/target/class scope. + +Before rollout, stage the filter and rules together and replay sanitized representative +payloads through the actual collector/engine. Confirm resulting alert IDs, blocked +versus monitored outcomes, history thresholds, ingestion delay, and burst grouping. +Existing indexed truncated fields are not repaired automatically. Custom consumers +of the old root `action` decision must migrate to `log.action`. Severity mapping and +noise thresholds are explicit policy choices for team review. No production alert +reduction or end-to-end live validation is claimed. + +## References + +- [Fortinet header/body field semantics](https://docs.fortinet.com/document/fortiweb/7.2.2/log-message-reference/578387/header-body-fields) +- [Fortinet attack log semantics](https://docs.fortinet.com/document/fortiweb/7.2.2/log-message-reference/445549/attack) +- [SDK schema](https://github.com/threatwinds/go-sdk/blob/v1.1.31/plugins/plugins.proto) +- [Filter steps](https://github.com/threatwinds/go-sdk/wiki/Filter-Steps-Reference) +- [Standard event schema](https://github.com/threatwinds/go-sdk/wiki/Standard-Event-Schema) +- [Rule implementation and trigger guidance](https://github.com/threatwinds/go-sdk/wiki/Implementing-Rules) diff --git a/filters/fortinet/fortiweb.yml b/filters/fortinet/fortiweb.yml index 686a9356e..f8fc2be5a 100644 --- a/filters/fortinet/fortiweb.yml +++ b/filters/fortinet/fortiweb.yml @@ -1,138 +1,447 @@ -# FortiWeb firewall module filter, version 2.0.1 -# Based on real logs provided -# -# Documentations -# 1- https://docs.fortinet.com/document/fortiweb/7.2.2/log-message-reference/470577/introduction -# 2- https://docs.fortinet.com/document/fortiweb/7.2.2/log-message-reference/252057/event -# 3- https://docs.fortinet.com/document/fortiweb/7.2.2/log-message-reference/280306/how-to-interpret-fortiweb-logs -# -# Implementation -# 1. Parsing the RAW field containing the Fortiweb firewall -# 2. Parsing header Sylog -# 3. Using the kv filter to parse key-value logs -# 4. Using grok to parse kv issued fields - +# FortiWeb filter, version 2.2.0 +# https://docs.fortinet.com/document/fortiweb/7.2.2/log-message-reference/578387/header-body-fields +# https://docs.fortinet.com/document/fortiweb/7.2.2/log-message-reference/445549/attack +# KV retains other vendor fields. Authoritative fields are re-extracted with quote-aware +# token boundaries: spaces, escaped quotes and embedded key=value text must survive. +# Retain the existing sanitized log.* names so saved searches remain compatible. +# Alert means detection/logging, not successful exploitation. Only explicit blocking +# decisions set actionResult=denied. Raw decisions remain in log.action. +# HTTP_url is a request path; HTTP_host is an untrusted header, retained in log.httphost. +# Severity policy: Low -> info, Medium -> warning, High -> error; preserve severitylevel. +patterns: + fortiwebKVPrefix: ^(?:[A-Za-z0-9_.-]+=(?:"(?:\\.|[^"\\])*"|[^\s"]*)\s+)*? + fortiwebKVValue: '"(?:\\.|[^"\\])*"|[^\s"]+' pipeline: - - dataTypes: - - firewall-fortiweb - steps: - - #Using grok to parse priority if present - - grok: - patterns: - - fieldName: log.priority - pattern: '\<{{.data}}\>' - - fieldName: log.kvMessage - pattern: '{{.greedy}}' - - #Using the kv filter with default config, usefull in key-value logs - - kv: - source: log.kvMessage - fieldSplit: " " - valueSplit: "=" - - # Remove fields that have issues with kv filter - - delete: - fields: - - log.msg - - # Using grok to parse kv issued fields - - grok: - patterns: - - fieldName: log.irrelevant - pattern: '{{.data}}(msg=)' - - fieldName: log.msg - pattern: '{{.data}}({{.word}}=)' - - fieldName: log.irrelevant - pattern: '{{.greedy}}' - source: log.kvMessage - - # Using grok to remove irrelevant data - - grok: - patterns: - - fieldName: log.msg - pattern: '{{.greedy}}{{.space}}' - - fieldName: log.irrelevant - pattern: '{{.word}}(=)' - source: log.msg - - # Rename standard fields and fields out of kv result - - rename: - from: - - log.action - to: action - - rename: - from: - - log.dst - to: target.ip - - rename: - from: - - log.dstport - to: target.port - - rename: - from: - - log.src - to: origin.ip - - rename: - from: - - log.srcport - to: origin.port - - - rename: - from: - - log.dest_ip - to: target.ip - - rename: - from: - - log.dest_port - to: target.port - - rename: - from: - - log.src_ip - to: origin.ip - - rename: - from: - - log.src_port - to: origin.port - - # Adding action result - - add: - function: string - params: - key: actionResult - value: "blocked" - where: 'exists("action") && equalsIgnoreCase("action", "Deny")' - - # Removing unused caracters - - trim: - function: prefix - substring: '<' - fields: - - log.priority - - trim: - function: suffix - substring: '>' - fields: - - log.priority - - # Adding geolocation to origin.ip - - dynamic: - plugin: com.utmstack.geolocation - params: - source: origin.ip - destination: origin.geolocation - where: exists("origin.ip") - - # Adding geolocation to target.ip - - dynamic: - plugin: com.utmstack.geolocation - params: - source: target.ip - destination: target.geolocation - where: exists("target.ip") - - # Removing unused fields - - delete: - fields: - - log.kvMessage - - log.irrelevant \ No newline at end of file +- dataTypes: + - firewall-fortiweb + steps: + - grok: + patterns: + - fieldName: '' + pattern: ^(?:<[0-9]+>)?(?:[A-Z][a-z]{2} +[0-9]{1,2} [0-9:]{8} [^ ]+ )? + - fieldName: log.kvMessage + pattern: '[A-Za-z0-9_.-]+=.*' + - grok: + patterns: + - fieldName: '' + pattern: ^< + - fieldName: log.priority + pattern: '[0-9]+' + - fieldName: '' + pattern: '>' + - kv: + source: log.kvMessage + fieldSplit: ' ' + valueSplit: '=' + - delete: + fields: + - log.type + - log.subtype + - log.maintype + - log.attacktype + - log.signatureid + - log.signaturesubclass + - log.signaturecveid + - log.severitylevel + - log.msg + - log.action + - log.proto + - log.src + - log.dst + - log.srcport + - log.dstport + - log.httpurl + - log.httphost + - log.httpmethod + - log.httpagent + - log.owasptop10 + - log.owaspapitop10 + - log.triggerpolicy + - log.srcip + - log.dstip + - log.destip + - log.destport + - log.fileUploadViolation + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:type)=' + - fieldName: log.type + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:sub_type|subtype)=' + - fieldName: log.subtype + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:main_type)=' + - fieldName: log.maintype + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:attack_type)=' + - fieldName: log.attacktype + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:signature_id)=' + - fieldName: log.signatureid + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:signature_subclass)=' + - fieldName: log.signaturesubclass + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:signature_cve_id)=' + - fieldName: log.signaturecveid + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:severity_level)=' + - fieldName: log.severitylevel + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:msg)=' + - fieldName: log.msg + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:action)=' + - fieldName: log.action + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:proto)=' + - fieldName: log.proto + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:src|src_ip)=' + - fieldName: log.src + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:dst|dst_ip|dest_ip)=' + - fieldName: log.dst + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:src_port|srcport)=' + - fieldName: log.srcport + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:dst_port|dest_port|dstport)=' + - fieldName: log.dstport + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:(?i:http_url))=' + - fieldName: log.httpurl + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:(?i:http_host))=' + - fieldName: log.httphost + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:(?i:http_method))=' + - fieldName: log.httpmethod + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:owasp_top10)=' + - fieldName: log.owasptop10 + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:(?i:http_agent))=' + - fieldName: log.httpagent + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:owasp_api_top10)=' + - fieldName: log.owaspapitop10 + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - grok: + source: log.kvMessage + patterns: + - fieldName: '' + pattern: '{{.fortiwebKVPrefix}}(?:trigger_policy)=' + - fieldName: log.triggerpolicy + pattern: '{{.fortiwebKVValue}}' + - fieldName: '' + pattern: (?:\s|$) + - trim: + function: prefix + substring: '"' + fields: + - log.type + - log.subtype + - log.maintype + - log.attacktype + - log.signatureid + - log.signaturesubclass + - log.signaturecveid + - log.severitylevel + - log.msg + - log.action + - log.proto + - log.src + - log.dst + - log.srcport + - log.dstport + - log.httpurl + - log.httphost + - log.httpmethod + - log.httpagent + - log.owasptop10 + - log.owaspapitop10 + - log.triggerpolicy + - trim: + function: suffix + substring: '"' + fields: + - log.type + - log.subtype + - log.maintype + - log.attacktype + - log.signatureid + - log.signaturesubclass + - log.signaturecveid + - log.severitylevel + - log.msg + - log.action + - log.proto + - log.src + - log.dst + - log.srcport + - log.dstport + - log.httpurl + - log.httphost + - log.httpmethod + - log.httpagent + - log.owasptop10 + - log.owaspapitop10 + - log.triggerpolicy + # Keep repeated upload-policy correlation tied to classified violations. + - add: + function: string + params: + key: log.fileUploadViolation + value: 'true' + where: >- + ((regexMatch("log.subtype", "(?i)^(File Upload(?: Restrictions?)?|File Security|Anti.?Virus|AV Detection)$") || regexMatch("log.attacktype", "(?i)^(File Upload(?: Restrictions?)?|File Security|Anti.?Virus|AV Detection)$") || regexMatch("log.signaturesubclass", "(?i)^(File Upload(?: Restrictions?)?|File Security|Anti.?Virus|AV Detection)$")) || ((equalsIgnoreCase("log.maintype", "Signature Detection") || (!exists("log.subtype") && !exists("log.attacktype"))) && regexMatch("log.msg", "(?i)\\b(file upload (?:violation|blocked|restriction)|malicious file|anti-virus|antivirus)\\b"))) + - rename: + from: + - log.src + to: origin.ip + # Retain an invalid selected alias in its canonical vendor field. + where: (inCIDR("log.src", "0.0.0.0/0") || inCIDR("log.src", "::/0")) && !inCIDR("log.src", "0.0.0.0/32") && !inCIDR("log.src", "::/128") + - rename: + from: + - log.dst + to: target.ip + where: (inCIDR("log.dst", "0.0.0.0/0") || inCIDR("log.dst", "::/0")) && !inCIDR("log.dst", "0.0.0.0/32") && !inCIDR("log.dst", "::/128") + - rename: + from: + - log.srcport + to: origin.port + - rename: + from: + - log.dstport + to: target.port + - grok: + source: log.action + patterns: + - fieldName: action + pattern: '{{.greedy}}' + where: exists("log.action") + - add: + function: string + params: + key: action + value: http_request + where: equalsIgnoreCase("log.type", "attack") || equalsIgnoreCase("log.type", "traffic") + - add: + function: string + params: + key: actionResult + value: denied + where: regexMatch("log.action", "(?i)^(deny|denied|alert_deny|block|blocked|period_block)$") + - add: + function: string + params: + key: protocol + value: tcp + where: regexMatch("log.proto", "(?i)^(tcp|6)$") + - add: + function: string + params: + key: protocol + value: udp + where: regexMatch("log.proto", "(?i)^(udp|17)$") + - add: + function: string + params: + key: protocol + value: icmp + where: regexMatch("log.proto", "(?i)^(icmp|1)$") + - add: + function: string + params: + key: protocol + value: icmpv6 + where: regexMatch("log.proto", "(?i)^(icmpv6|ipv6-icmp|58)$") + - add: + function: string + params: + key: severity + value: info + where: regexMatch("log.severitylevel", "(?i)^(low|information|informational|info)$") + - add: + function: string + params: + key: severity + value: warning + where: regexMatch("log.severitylevel", "(?i)^(medium|warning)$") + - add: + function: string + params: + key: severity + value: error + where: regexMatch("log.severitylevel", "(?i)^(high|error)$") + - add: + function: string + params: + key: severity + value: critical + where: regexMatch("log.severitylevel", "(?i)^(critical)$") + - add: + function: string + params: + key: severity + value: debug + where: regexMatch("log.severitylevel", "(?i)^(debug)$") + # Relative request paths stay in log.httpurl; SDK path is a file directory. + - grok: + source: log.httpurl + patterns: + - fieldName: target.url + pattern: '{{.greedy}}' + where: regexMatch("log.httpurl", "(?i)^https?://") + - rename: + from: + - origin.port + to: log.unparsedOriginPort + where: exists("origin.port") && !(regexMatch("origin.port", "^[0-9]{1,5}$") && lessOrEqual("origin.port", + 65535)) + - dynamic: + plugin: com.utmstack.geolocation + params: + source: origin.ip + destination: origin.geolocation + where: exists("origin.ip") + - rename: + from: + - target.port + to: log.unparsedTargetPort + where: exists("target.port") && !(regexMatch("target.port", "^[0-9]{1,5}$") && lessOrEqual("target.port", + 65535)) + - dynamic: + plugin: com.utmstack.geolocation + params: + source: target.ip + destination: target.geolocation + where: exists("target.ip") + - delete: + fields: + - log.kvMessage diff --git a/plugins/alerts/fortiweb_contract_test.go b/plugins/alerts/fortiweb_contract_test.go new file mode 100644 index 000000000..dfd58e0e8 --- /dev/null +++ b/plugins/alerts/fortiweb_contract_test.go @@ -0,0 +1,454 @@ +package main + +// These are offline parser/rule contracts, not the closed EventProcessor. The +// model concatenates documented grok patterns, executes them with Go RE2, applies +// the filter's transforms, and uses the real SDK for CEL and Event conversion. +// KV models observed space splitting so quoted payload tokens can contaminate +// intermediate fields; authoritative recovery must remove them. External +// geolocation is not run, but its input addresses are checked before enrichment. +import ( + "bytes" + "encoding/json" + "fmt" + "net" + "os" + "path/filepath" + "reflect" + "regexp" + "strings" + "testing" + "text/template" + + "github.com/threatwinds/go-sdk/plugins" + "github.com/threatwinds/go-sdk/utils" + "github.com/tidwall/gjson" + "google.golang.org/protobuf/encoding/protojson" +) + +type fwFixture struct { + Name string `json:"name"` + Raw string `json:"raw"` + Expected map[string]any `json:"expected"` + Absent []string `json:"absent"` + Matches []string `json:"matches"` +} + +func fwPut(m map[string]any, path string, value any, remove bool) { + p := strings.Split(path, ".") + for _, k := range p[:len(p)-1] { + n, ok := m[k].(map[string]any) + if !ok { + if remove { + return + } + n = map[string]any{} + m[k] = n + } + m = n + } + if remove { + delete(m, p[len(p)-1]) + } else { + m[p[len(p)-1]] = value + } +} +func fwGet(m map[string]any, p string) (any, bool) { + var v any = m + for _, k := range strings.Split(p, ".") { + n, ok := v.(map[string]any) + if !ok { + return nil, false + } + v, ok = n[k] + if !ok { + return nil, false + } + } + return v, true +} +func fwConfig(t *testing.T) *plugins.Config { + t.Helper() + b, e := utils.ReadPbYaml("../../filters/fortinet/fortiweb.yml") + if e != nil { + t.Fatal(e) + } + c := new(plugins.Config) + if e = protojson.Unmarshal(b, c); e != nil { + t.Fatal(e) + } + return c +} +func fwRegex(t *testing.T, g *plugins.Grok, cfg *plugins.Config) *regexp.Regexp { + t.Helper() + var pattern strings.Builder + for i, p := range g.Patterns { + if p.FieldName != "" { + fmt.Fprintf(&pattern, "(?P%s)", i, p.Pattern) + } else { + pattern.WriteString("(?:" + p.Pattern + ")") + } + } + pats := map[string]string{"greedy": ".*"} + for k, v := range cfg.Patterns { + pats[k] = v + } + tmpl, e := template.New("grok").Option("missingkey=error").Parse(pattern.String()) + if e != nil { + t.Fatal(e) + } + var b bytes.Buffer + if e = tmpl.Execute(&b, pats); e != nil { + t.Fatal(e) + } + r, e := regexp.Compile(b.String()) + if e != nil { + t.Fatal(e) + } + return r +} +func fwParse(t *testing.T, cfg *plugins.Config, raw string, cache *plugins.CELCache) string { + return fwParseSource(t, cfg, raw, "synthetic-fortiweb", cache) +} +func fwParseSource(t *testing.T, cfg *plugins.Config, raw, dataSource string, cache *plugins.CELCache) string { + t.Helper() + draft := map[string]any{"raw": raw, "dataType": "firewall-fortiweb", "dataSource": dataSource, "log": map[string]any{}} + for _, stage := range cfg.Pipeline { + matched := false + for _, dataType := range stage.DataTypes { + if dataType == "firewall-fortiweb" { + matched = true + } + } + if !matched { + continue + } + for _, s := range stage.Steps { + b, e := protojson.Marshal(s) + if e != nil { + t.Fatal(e) + } + var step map[string]map[string]any + if e = json.Unmarshal(b, &step); e != nil { + t.Fatal(e) + } + for kind, body := range step { + if w, ok := body["where"].(string); ok && w != "" { + snapshot, err := json.Marshal(draft) + if err != nil { + t.Fatal(err) + } + match, e := cache.Eval(w, string(snapshot)) + if e != nil { + t.Fatal(e) + } + if !match { + continue + } + } + switch kind { + case "grok": + g := s.Grok + src := g.Source + if src == "" { + src = "raw" + } + v, ok := fwGet(draft, src) + if !ok { + continue + } + str, ok := v.(string) + if !ok { + t.Fatalf("non-string grok source %s", src) + } + r := fwRegex(t, g, cfg) + m := r.FindStringSubmatch(str) + if m == nil { + continue + } + for i, p := range g.Patterns { + if p.FieldName != "" { + fwPut(draft, p.FieldName, m[r.SubexpIndex(fmt.Sprintf("f%d", i))], false) + } + } + case "rename": + for _, p := range s.Rename.From { + if v, ok := fwGet(draft, p); ok { + fwPut(draft, s.Rename.To, v, false) + fwPut(draft, p, nil, true) + break + } + } + case "trim": + for _, p := range s.Trim.Fields { + if v, ok := fwGet(draft, p); ok { + str := v.(string) + switch s.Trim.Function { + case "prefix": + str = strings.TrimPrefix(str, s.Trim.Substring) + case "suffix": + str = strings.TrimSuffix(str, s.Trim.Substring) + default: + t.Fatalf("unsupported trim %s", s.Trim.Function) + } + fwPut(draft, p, str, false) + } + } + case "add": + if s.Add.Function != "string" { + t.Fatalf("unsupported add %s", s.Add.Function) + } + fwPut(draft, s.Add.Params["key"].GetStringValue(), s.Add.Params["value"].AsInterface(), false) + case "delete": + for _, p := range s.Delete.Fields { + fwPut(draft, p, nil, true) + } + case "kv": + if s.Kv.FieldSplit != " " || s.Kv.ValueSplit != "=" { + t.Fatal("unsupported KV separators") + } + value, ok := fwGet(draft, s.Kv.Source) + if !ok { + continue + } + for _, token := range strings.Split(value.(string), " ") { + pair := strings.SplitN(token, "=", 2) + if len(pair) == 2 { + key := pair[0] + utils.SanitizeField(&key) + fwPut(draft, "log."+key, pair[1], false) + } + } + case "dynamic": + if s.Dynamic.Plugin != "com.utmstack.geolocation" { + t.Fatal("unsupported dynamic plugin") + } + field := s.Dynamic.Params["source"].GetStringValue() + value, ok := fwGet(draft, field) + if !ok || net.ParseIP(fmt.Sprint(value)) == nil || net.ParseIP(fmt.Sprint(value)).IsUnspecified() { + t.Errorf("invalid address reached geolocation at %s", field) + } + default: + t.Fatalf("unsupported filter step %s", kind) + } + } + } + } + b, e := json.Marshal(draft) + if e != nil { + t.Fatal(e) + } + in := string(b) + ev := new(plugins.Event) + if e = utils.StringToProtoMessage(&in, ev); e != nil { + t.Fatal(e) + } + out, e := utils.ProtoMessageToString(ev) + if e != nil { + t.Fatal(e) + } + return *out +} + +// Private source paths are opt-in and may be a bounded glob. No customer raw +// strings, IDs or addresses are logged or included in committed fixtures. +func TestFortiWebPrivateEvidence(t *testing.T) { + pattern := os.Getenv("UTM_FORTIWEB_EVIDENCE") + if pattern == "" { + t.Skip("set UTM_FORTIWEB_EVIDENCE to private sampled-hit JSON paths") + } + paths, e := filepath.Glob(pattern) + if e != nil || len(paths) == 0 { + t.Fatal("no private evidence paths") + } + cfg, rules, cache := fwConfig(t), fwRules(t), plugins.NewCELCache("fortiweb-private") + populated, matched := map[string]int{}, map[string]int{} + checkedAgent := 0 + count := 0 + for _, path := range paths { + b, e := os.ReadFile(path) + if e != nil { + t.Fatal(e) + } + var hits []struct { + Source map[string]any `json:"_source"` + } + if e = json.Unmarshal(b, &hits); e != nil { + t.Fatal(e) + } + for _, hit := range hits { + raw, ok := hit.Source["raw"].(string) + if !ok { + t.Fatal("raw missing from private evidence") + } + source, _ := hit.Source["dataSource"].(string) + out := fwParseSource(t, cfg, raw, source, cache) + stored, e := json.Marshal(hit.Source) + if e != nil { + t.Fatal(e) + } + for _, field := range []string{"dataSource", "origin.ip", "target.ip", "origin.port", "target.port"} { + before, after := gjson.GetBytes(stored, field), gjson.Get(out, field) + if before.Exists() && before.String() != after.String() { + t.Errorf("existing private network field changed: %s", field) + } + } + // Independent direct comparison for the demonstrated quoted-string + // truncation; field contents remain private even on a failure. + if match := regexp.MustCompile(`(?i)(?:^|\s)HTTP_agent=("(?:\\.|[^"\\])*"|[^\s]+)`).FindStringSubmatch(raw); len(match) > 1 { + want := strings.TrimSuffix(strings.TrimPrefix(match[1], `"`), `"`) + if gjson.Get(out, "log.httpagent").String() != want { + t.Error("private user-agent differs from complete raw value") + } + checkedAgent++ + } + for _, field := range []string{"origin.ip", "target.ip", "actionResult", "protocol", "severity", "target.url", "target.path", "log.msg", "log.subtype", "log.httpagent", "log.fileUploadViolation"} { + if gjson.Get(out, field).Exists() { + populated[field]++ + } + } + for name, rule := range rules { + yes, e := cache.Eval(rule.Where, out) + if e != nil { + t.Fatalf("%s CEL: %v", name, e) + } + if yes { + matched[name]++ + for _, search := range rule.Correlation { + for _, term := range search.With { + value := term.Value.GetStringValue() + if strings.HasPrefix(value, "{{.") { + field := strings.TrimSuffix(strings.TrimPrefix(value, "{{."), "}}") + if !gjson.Get(out, field).Exists() { + t.Errorf("%s missing private history placeholder %s", name, field) + } + } + } + } + } + } + count++ + } + } + if count == 0 { + t.Fatal("empty private evidence") + } + t.Logf("private documents=%d complete user-agents compared=%d populated=%v predicates=%v", count, checkedAgent, populated, matched) +} +func fwRules(t *testing.T) map[string]*plugins.Rule { + t.Helper() + paths, e := filepath.Glob("../../rules/fortinet/fortiweb/*.yml") + if e != nil { + t.Fatal(e) + } + out := map[string]*plugins.Rule{} + for _, p := range paths { + b, e := utils.ReadPbYaml(p) + if e != nil { + t.Fatal(e) + } + r := new(plugins.Rule) + if e = protojson.Unmarshal(b, r); e != nil { + t.Fatal(e) + } + r.Normalize() + out[strings.TrimSuffix(filepath.Base(p), ".yml")] = r + } + return out +} +func TestFortiWebRawContracts(t *testing.T) { + cfg := fwConfig(t) + rules := fwRules(t) + cache := plugins.NewCELCache("fortiweb-raw-contract") + b, e := os.ReadFile("testdata/fortiweb_raw.json") + if e != nil { + t.Fatal(e) + } + var fixtures []fwFixture + if e = json.Unmarshal(b, &fixtures); e != nil { + t.Fatal(e) + } + for _, f := range fixtures { + t.Run(f.Name, func(t *testing.T) { + out := fwParse(t, cfg, f.Raw, cache) + for p, want := range f.Expected { + got := gjson.Get(out, p) + if !got.Exists() || !reflect.DeepEqual(got.Value(), want) { + t.Errorf("%s: got %v, want %v", p, got.Value(), want) + } + } + for _, p := range f.Absent { + if gjson.Get(out, p).Exists() { + t.Errorf("unexpected %s", p) + } + } + if gjson.Get(out, "raw").String() != f.Raw { + t.Error("raw changed") + } + want := map[string]bool{} + for _, name := range f.Matches { + if rules[name] == nil { + t.Fatalf("unknown expected rule %s", name) + } + want[name] = true + } + for name, r := range rules { + got, e := cache.Eval(r.Where, out) + if e != nil { + t.Errorf("%s: %v", name, e) + } else if got != want[name] { + t.Errorf("%s trigger=%v, want %v", name, got, want[name]) + } + if got { + for _, search := range r.Correlation { + for _, term := range search.With { + value := term.Value.GetStringValue() + if strings.HasPrefix(value, "{{.") { + field := strings.TrimSuffix(strings.TrimPrefix(value, "{{."), "}}") + if !gjson.Get(out, field).Exists() { + t.Errorf("%s: matched event lacks history placeholder %s", name, field) + } + } + } + } + } + } + }) + } +} +func TestFortiWebRuleHistoryAndGrouping(t *testing.T) { + for name, r := range fwRules(t) { + t.Run(name, func(t *testing.T) { + if r.Adversary != "origin" { + t.Error("request source must be adversary") + } + if len(r.GroupBy) > 0 && len(r.DeduplicateBy) > 0 { + t.Error("grouping and deduplication are exclusive") + } + for _, p := range append(r.GroupBy, r.DeduplicateBy...) { + if p != "adversary.ip" && p != "target.ip" && p != "lastEvent.log.subtype" { + t.Errorf("unexpected alert path %s", p) + } + } + for _, s := range r.Correlation { + if s.Count <= 1 { + t.Error("single detections must not require a history query") + } + fields := map[string]bool{} + for _, x := range s.With { + if x.Operator != "filter_term" { + t.Error("history must use exact matches") + } + fields[x.Field] = true + if x.Value.GetStringValue() != "{{."+x.Field+"}}" { + t.Errorf("history value does not use trigger's %s", x.Field) + } + } + for _, p := range []string{"origin.ip", "target.ip", "log.type", "log.subtype", "log.action"} { + if !fields[p] { + t.Errorf("history missing %s", p) + } + } + } + }) + } +} diff --git a/plugins/alerts/fortiweb_history_test.go b/plugins/alerts/fortiweb_history_test.go new file mode 100644 index 000000000..474b79c4e --- /dev/null +++ b/plugins/alerts/fortiweb_history_test.go @@ -0,0 +1,249 @@ +package main + +// SDK history contracts for synthetic FortiWeb events. Extraction uses the +// documented offline model in fortiweb_contract_test.go; all search requests, +// placeholder expansion, mapping lookup and count decisions use the pinned SDK. +// The HTTP server below is loopback-only and never queries a customer instance. +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "strings" + "testing" + "time" + + sdkos "github.com/threatwinds/go-sdk/os" + "github.com/threatwinds/go-sdk/plugins" + "github.com/tidwall/gjson" +) + +func TestFortiWebSDKHistory(t *testing.T) { + // The SDK owns a process-wide OpenSearch singleton. Isolate this local mock + // so other technology tests can initialize their own clients in this suite. + if os.Getenv("UTM_FORTIWEB_HISTORY_CHILD") != "1" { + command := exec.Command(os.Args[0], "-test.run=^TestFortiWebSDKHistory$") + command.Env = append(os.Environ(), "UTM_FORTIWEB_HISTORY_CHILD=1") + if out, e := command.CombinedOutput(); e != nil { + t.Fatalf("isolated history test: %v\n%s", e, out) + } + return + } + + cfg, rules, cache := fwConfig(t), fwRules(t), plugins.NewCELCache("fortiweb-history") + var history []string + var expectedClauses int + var searchRequests int + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if strings.HasSuffix(r.URL.Path, "/_mapping") { + // Text fields exercise the SDK's .keyword mapping resolution; IP + // fields exercise exact mappings without that suffix. + _, _ = io.WriteString(w, `{"v11-log-firewall-fortiweb-test":{"mappings":{"properties":{"@timestamp":{"type":"date"},"origin":{"properties":{"ip":{"type":"ip"}}},"target":{"properties":{"ip":{"type":"ip"}}},"log":{"properties":{"type":{"type":"text","fields":{"keyword":{"type":"keyword"}}},"subtype":{"type":"text","fields":{"keyword":{"type":"keyword"}}},"action":{"type":"text","fields":{"keyword":{"type":"keyword"}}},"fileUploadViolation":{"type":"text","fields":{"keyword":{"type":"keyword"}}},"maintype":{"type":"text","fields":{"keyword":{"type":"keyword"}}},"severitylevel":{"type":"text","fields":{"keyword":{"type":"keyword"}}},"owasptop10":{"type":"text","fields":{"keyword":{"type":"keyword"}}}}}}}}}`) + return + } + if r.URL.Path != "/v11-log-firewall-fortiweb-*/_search" { + t.Errorf("unexpected path %s", r.URL.Path) + http.Error(w, "unsupported request", http.StatusBadRequest) + return + } + searchRequests++ + b, e := io.ReadAll(r.Body) + if e != nil { + t.Error(e) + return + } + query := string(b) + clauses := append(gjson.Get(query, "query.bool.must").Array(), gjson.Get(query, "query.bool.filter").Array()...) + if len(clauses) != expectedClauses { + t.Errorf("history clauses: got %d want %d: %s", len(clauses), expectedClauses, query) + } + hits := []map[string]any{} + for _, doc := range history { + yes := true + for _, clause := range clauses { + if term := clause.Get("term"); term.Exists() { + for field, v := range term.Map() { + value := gjson.Get(doc, strings.TrimSuffix(field, ".keyword")) + if !value.Exists() || value.String() != v.Get("value").String() { + yes = false + } + } + } else if span := clause.Get("range"); span.Exists() { + for field, limits := range span.Map() { + stamp, e := time.Parse(time.RFC3339Nano, gjson.Get(doc, field).String()) + if e != nil { + t.Error(e) + } + cutoff, e := time.Parse(time.RFC3339Nano, limits.Get("gte").String()) + if e != nil { + t.Error(e) + } + if stamp.Before(cutoff) { + yes = false + } + } + } else { + t.Errorf("unsupported clause %s", clause.Raw) + yes = false + } + } + if yes { + hits = append(hits, map[string]any{"_id": fmt.Sprint(len(hits)), "_index": "v11-log-firewall-fortiweb-test", "_source": map[string]any{}}) + } + } + _ = json.NewEncoder(w).Encode(map[string]any{"took": 1, "hits": map[string]any{"total": map[string]any{"value": len(hits), "relation": "eq"}, "hits": hits}}) + })) + defer server.Close() + if e := sdkos.Connect([]string{server.URL}, "", ""); e != nil { + t.Fatal(e) + } + mutate := func(doc, field string, value any) string { + var m map[string]any + if e := json.Unmarshal([]byte(doc), &m); e != nil { + t.Fatal(e) + } + fwPut(m, field, value, value == nil) + b, e := json.Marshal(m) + if e != nil { + t.Fatal(e) + } + return string(b) + } + + // These fixtures contain fabricated raw logs, not retained customer payloads. + // Require each named case so a missing fixture cannot silently omit a rule. + data, e := os.ReadFile("testdata/fortiweb_raw.json") + if e != nil { + t.Fatal(e) + } + var rawFixtures []fwFixture + if e = json.Unmarshal(data, &rawFixtures); e != nil { + t.Fatal(e) + } + fixtures := map[string]fwFixture{} + for _, fixture := range rawFixtures { + fixtures[fixture.Name] = fixture + } + cases := []struct { + rule, fixture string + count uint64 + within string + }{ + {"web_application_attacks_detection", "Generic Attacks_Alert_Deny", 3, "15m"}, + {"file_upload_security_violations", "ordinary_php_upload", 3, "30m"}, + {"owasp_top10_violations", "HTTP_Illegal URL Parameter Value", 5, "15m"}, + } + for _, test := range cases { + t.Run(test.rule, func(t *testing.T) { + fixture, ok := fixtures[test.fixture] + if !ok { + t.Fatalf("required raw fixture %q missing", test.fixture) + } + rule := rules[test.rule] + if rule == nil || len(rule.Correlation) != 1 { + t.Fatalf("expected exactly one history request for %s", test.rule) + } + search := rule.Correlation[0] + if search.Count != test.count || search.Within != test.within { + t.Fatalf("unexpected threshold %d/%s", search.Count, search.Within) + } + expectedClauses = len(search.With) + 1 + parse := func(raw string) string { return fwParse(t, cfg, raw, cache) } + out := parse(fixture.Raw) + if match, e := cache.Eval(rule.Where, out); e != nil || !match { + t.Fatalf("positive raw fixture does not match: %v %v", match, e) + } + prior := mutate(out, "@timestamp", time.Now().Add(-time.Minute).UTC().Format(time.RFC3339Nano)) + check := func(name, historical string, count uint64, want bool) { + t.Run(name, func(t *testing.T) { + history = nil + for i := uint64(0); i < count; i++ { + history = append(history, historical) + } + yes, _, e := search.Execute(&out) + if e != nil || yes != want { + t.Fatalf("history result got %v want %v: %v", yes, want, e) + } + }) + } + check("below_threshold", prior, search.Count-1, false) + check("at_threshold", prior, search.Count, true) + duration, e := time.ParseDuration(search.Within) + if e != nil { + t.Fatal(e) + } + // The SDK's window is a processing-time lower bound on @timestamp, + // not a proof of vendor event-time ordering or customer alert creation. + check("inside_window", mutate(prior, "@timestamp", time.Now().Add(-duration+10*time.Second).UTC().Format(time.RFC3339Nano)), search.Count, true) + check("expired", mutate(prior, "@timestamp", time.Now().Add(-duration-10*time.Second).UTC().Format(time.RFC3339Nano)), search.Count, false) + for _, change := range []struct{ field, value string }{ + {"origin.ip", "198.51.100.99"}, {"target.ip", "192.0.2.99"}, + {"log.type", "traffic"}, {"log.subtype", "unrelated-class"}, {"log.action", "Pass"}, + } { + check("different_"+change.field, mutate(prior, change.field, change.value), search.Count, false) + } + if test.rule == "owasp_top10_violations" { + for _, change := range []struct{ field, value string }{ + {"log.severitylevel", "Low"}, {"log.maintype", "unrelated-main-class"}, {"log.owasptop10", "unrelated-category"}, + } { + check("different_"+change.field, mutate(prior, change.field, change.value), search.Count, false) + } + } + // Reparse a benign raw event; all endpoints are unchanged. For upload + // history, its exact class/action are also unchanged, leaving only the + // filter-derived eligibility marker to exclude the ordinary request. + var benignRaw string + switch test.rule { + case "web_application_attacks_detection": + benignRaw = strings.ReplaceAll(fixture.Raw, "Generic Attacks", "Information Disclosure") + case "file_upload_security_violations": + benignRaw = strings.ReplaceAll(fixture.Raw, "file upload violation for document.php", "ordinary document request") + case "owasp_top10_violations": + benignRaw = strings.ReplaceAll(fixture.Raw, "severity_level=Medium", "severity_level=Low") + } + benign := parse(benignRaw) + if match, e := cache.Eval(rule.Where, benign); e != nil || match { + t.Fatalf("benign raw event retains eligibility: %v %v", match, e) + } + for _, field := range []string{"origin.ip", "target.ip"} { + if gjson.Get(benign, field).String() != gjson.Get(out, field).String() { + t.Fatalf("benign history changed %s, masking candidate exclusion", field) + } + } + if test.rule == "file_upload_security_violations" { + if gjson.Get(benign, "log.fileUploadViolation").Exists() { + t.Fatal("ordinary request acquired an upload violation marker") + } + for _, field := range []string{"log.type", "log.subtype", "log.action"} { + if gjson.Get(benign, field).String() != gjson.Get(out, field).String() { + t.Fatalf("benign upload history changed %s, masking marker exclusion", field) + } + } + } + check("benign_raw_history", mutate(benign, "@timestamp", time.Now().Add(-time.Minute).UTC().Format(time.RFC3339Nano)), search.Count, false) + for _, term := range search.With { + value := term.Value.GetStringValue() + if !strings.HasPrefix(value, "{{.") || !strings.HasSuffix(value, "}}") { + t.Fatalf("unexpected non-placeholder history term %s", term.Field) + } + field := strings.TrimSuffix(strings.TrimPrefix(value, "{{."), "}}") + t.Run("missing_"+field, func(t *testing.T) { + missing := mutate(out, field, nil) + if match, e := cache.Eval(rule.Where, missing); e != nil || match { + t.Fatalf("where must reject missing history field: %v %v", match, e) + } + // Prove the failure avoided by the predicate guard. A missing + // placeholder aborts before any history HTTP search is issued. + before := searchRequests + if yes, _, e := search.Execute(&missing); e == nil || yes || searchRequests != before { + t.Error("missing history field did not abort before search") + } + }) + } + }) + } +} diff --git a/plugins/alerts/testdata/filter-contracts/fortiweb.json b/plugins/alerts/testdata/filter-contracts/fortiweb.json new file mode 100644 index 000000000..c236bf231 --- /dev/null +++ b/plugins/alerts/testdata/filter-contracts/fortiweb.json @@ -0,0 +1,46 @@ +{ + "technology": "FortiWeb", + "filters": [ + "filters/fortinet/fortiweb.yml" + ], + "rules": [ + "rules/fortinet/fortiweb/authentication_bypass_attempts.yml", + "rules/fortinet/fortiweb/file_upload_security_violations.yml", + "rules/fortinet/fortiweb/fortiweb_known_exploits.yml", + "rules/fortinet/fortiweb/fortiweb_malware_detection.yml", + "rules/fortinet/fortiweb/fortiweb_sqli_detection.yml", + "rules/fortinet/fortiweb/fortiweb_ssrf_detection.yml", + "rules/fortinet/fortiweb/fortiweb_webshell_upload.yml", + "rules/fortinet/fortiweb/fortiweb_xss_detection.yml", + "rules/fortinet/fortiweb/owasp_top10_violations.yml", + "rules/fortinet/fortiweb/web_application_attacks_detection.yml" + ], + "fixtures": [ + { + "name": "FortiWeb absent extracted identity negative control (not raw parsing or IP validation)", + "filter": "fortinet/fortiweb.yml", + "input": { + "dataType": "firewall-fortiweb", + "log": {} + }, + "expected": {}, + "absent": [ + "origin.ip", + "target.ip", + "actionResult" + ], + "rules": { + "rules/fortinet/fortiweb/authentication_bypass_attempts.yml": false, + "rules/fortinet/fortiweb/file_upload_security_violations.yml": false, + "rules/fortinet/fortiweb/fortiweb_known_exploits.yml": false, + "rules/fortinet/fortiweb/fortiweb_malware_detection.yml": false, + "rules/fortinet/fortiweb/fortiweb_sqli_detection.yml": false, + "rules/fortinet/fortiweb/fortiweb_ssrf_detection.yml": false, + "rules/fortinet/fortiweb/fortiweb_webshell_upload.yml": false, + "rules/fortinet/fortiweb/fortiweb_xss_detection.yml": false, + "rules/fortinet/fortiweb/owasp_top10_violations.yml": false, + "rules/fortinet/fortiweb/web_application_attacks_detection.yml": false + } + } + ] +} diff --git a/plugins/alerts/testdata/fortiweb_raw.json b/plugins/alerts/testdata/fortiweb_raw.json new file mode 100644 index 000000000..b46565ccf --- /dev/null +++ b/plugins/alerts/testdata/fortiweb_raw.json @@ -0,0 +1,1546 @@ +[ + { + "name": "SQL Injection_Alert", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "SQL Injection", + "log.attacktype": "SQL Injection", + "log.action": "Alert", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "actionResult", + "target.path" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "SQL Injection_Alert_Deny", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "SQL Injection", + "log.attacktype": "SQL Injection", + "log.action": "Alert_Deny", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "target.path" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "SQL Injection(Extended)_Alert", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection(Extended)\" attack_type=\"SQL Injection(Extended)\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "SQL Injection(Extended)", + "log.attacktype": "SQL Injection(Extended)", + "log.action": "Alert", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "actionResult", + "target.path" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "SQL Injection(Extended)_Alert_Deny", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection(Extended)\" attack_type=\"SQL Injection(Extended)\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "SQL Injection(Extended)", + "log.attacktype": "SQL Injection(Extended)", + "log.action": "Alert_Deny", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "target.path" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "Cross Site Scripting_Alert", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"Cross Site Scripting\" attack_type=\"Cross Site Scripting\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "Cross Site Scripting", + "log.attacktype": "Cross Site Scripting", + "log.action": "Alert", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "actionResult", + "target.path" + ], + "matches": [ + "fortiweb_xss_detection" + ] + }, + { + "name": "Cross Site Scripting_Alert_Deny", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"Cross Site Scripting\" attack_type=\"Cross Site Scripting\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "Cross Site Scripting", + "log.attacktype": "Cross Site Scripting", + "log.action": "Alert_Deny", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "target.path" + ], + "matches": [ + "fortiweb_xss_detection" + ] + }, + { + "name": "Cross Site Scripting (Extended)_Alert", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"Cross Site Scripting (Extended)\" attack_type=\"Cross Site Scripting (Extended)\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "Cross Site Scripting (Extended)", + "log.attacktype": "Cross Site Scripting (Extended)", + "log.action": "Alert", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "actionResult", + "target.path" + ], + "matches": [ + "fortiweb_xss_detection" + ] + }, + { + "name": "Cross Site Scripting (Extended)_Alert_Deny", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"Cross Site Scripting (Extended)\" attack_type=\"Cross Site Scripting (Extended)\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "Cross Site Scripting (Extended)", + "log.attacktype": "Cross Site Scripting (Extended)", + "log.action": "Alert_Deny", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "target.path" + ], + "matches": [ + "fortiweb_xss_detection" + ] + }, + { + "name": "Known Exploits_Alert", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"Known Exploits\" attack_type=\"Known Exploits\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "Known Exploits", + "log.attacktype": "Known Exploits", + "log.action": "Alert", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "actionResult", + "target.path" + ], + "matches": [ + "fortiweb_known_exploits" + ] + }, + { + "name": "Known Exploits_Alert_Deny", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"Known Exploits\" attack_type=\"Known Exploits\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "Known Exploits", + "log.attacktype": "Known Exploits", + "log.action": "Alert_Deny", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "target.path" + ], + "matches": [ + "fortiweb_known_exploits" + ] + }, + { + "name": "Generic Attacks_Alert", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"Generic Attacks\" attack_type=\"Generic Attacks\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "Generic Attacks", + "log.attacktype": "Generic Attacks", + "log.action": "Alert", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "actionResult", + "target.path" + ], + "matches": [ + "web_application_attacks_detection" + ] + }, + { + "name": "Generic Attacks_Alert_Deny", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"Generic Attacks\" attack_type=\"Generic Attacks\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "Generic Attacks", + "log.attacktype": "Generic Attacks", + "log.action": "Alert_Deny", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "target.path" + ], + "matches": [ + "web_application_attacks_detection" + ] + }, + { + "name": "Generic Attacks(Extended)_Alert", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"Generic Attacks(Extended)\" attack_type=\"Generic Attacks(Extended)\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "Generic Attacks(Extended)", + "log.attacktype": "Generic Attacks(Extended)", + "log.action": "Alert", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "actionResult", + "target.path" + ], + "matches": [ + "web_application_attacks_detection" + ] + }, + { + "name": "Generic Attacks(Extended)_Alert_Deny", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"Generic Attacks(Extended)\" attack_type=\"Generic Attacks(Extended)\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "Generic Attacks(Extended)", + "log.attacktype": "Generic Attacks(Extended)", + "log.action": "Alert_Deny", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "target.path" + ], + "matches": [ + "web_application_attacks_detection" + ] + }, + { + "name": "Trojans_Alert", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=Trojans attack_type=Trojans signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "Trojans", + "log.attacktype": "Trojans", + "log.action": "Alert", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "actionResult", + "target.path" + ], + "matches": [ + "fortiweb_malware_detection" + ] + }, + { + "name": "Trojans_Alert_Deny", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=Trojans attack_type=Trojans signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "Trojans", + "log.attacktype": "Trojans", + "log.action": "Alert_Deny", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "target.path" + ], + "matches": [ + "fortiweb_malware_detection" + ] + }, + { + "name": "denial_Deny", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "actionResult": "denied", + "log.action": "Deny" + }, + "absent": [], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "denial_deny", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "actionResult": "denied", + "log.action": "deny" + }, + "absent": [], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "denial_DENY", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=DENY severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "actionResult": "denied", + "log.action": "DENY" + }, + "absent": [], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "denial_Period_Block", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Period_Block severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "actionResult": "denied", + "log.action": "Period_Block" + }, + "absent": [], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "denial_Block", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Block severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "actionResult": "denied", + "log.action": "Block" + }, + "absent": [], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "denial_blocked", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=blocked severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "actionResult": "denied", + "log.action": "blocked" + }, + "absent": [], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "unknown_outcome_Pass", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Pass severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.action": "Pass" + }, + "absent": [ + "actionResult" + ], + "matches": [] + }, + { + "name": "unknown_outcome_Accept", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Accept severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.action": "Accept" + }, + "absent": [ + "actionResult" + ], + "matches": [] + }, + { + "name": "unknown_outcome_Monitor", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Monitor severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.action": "Monitor" + }, + "absent": [ + "actionResult" + ], + "matches": [] + }, + { + "name": "unknown_outcome_unknown", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=unknown severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.action": "unknown" + }, + "absent": [ + "actionResult" + ], + "matches": [] + }, + { + "name": "HTTP_Illegal URL Parameter Value", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=Medium main_type=\"HTTP Protocol Constraints\" sub_type=\"Illegal URL Parameter Value\" attack_type=\"HTTP Protocol Constraints\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=\"A05:2021-Security Misconfiguration\"", + "expected": { + "log.subtype": "Illegal URL Parameter Value", + "severity": "warning" + }, + "absent": [ + "actionResult" + ], + "matches": [ + "owasp_top10_violations" + ] + }, + { + "name": "HTTP_Illegal Character in URL", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=Medium main_type=\"HTTP Protocol Constraints\" sub_type=\"Illegal Character in URL\" attack_type=\"HTTP Protocol Constraints\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=\"A05:2021-Security Misconfiguration\"", + "expected": { + "log.subtype": "Illegal Character in URL", + "severity": "warning" + }, + "absent": [ + "actionResult" + ], + "matches": [ + "owasp_top10_violations" + ] + }, + { + "name": "HTTP_Illegal HTTP Method", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=Medium main_type=\"HTTP Protocol Constraints\" sub_type=\"Illegal HTTP Method\" attack_type=\"HTTP Protocol Constraints\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=\"A05:2021-Security Misconfiguration\"", + "expected": { + "log.subtype": "Illegal HTTP Method", + "severity": "warning" + }, + "absent": [ + "actionResult" + ], + "matches": [ + "owasp_top10_violations" + ] + }, + { + "name": "HTTP_NULL Character in URL", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=Medium main_type=\"HTTP Protocol Constraints\" sub_type=\"NULL Character in URL\" attack_type=\"HTTP Protocol Constraints\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=\"A05:2021-Security Misconfiguration\"", + "expected": { + "log.subtype": "NULL Character in URL", + "severity": "warning" + }, + "absent": [ + "actionResult" + ], + "matches": [ + "owasp_top10_violations" + ] + }, + { + "name": "HTTP_NULL Character in Paramter Value", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=Medium main_type=\"HTTP Protocol Constraints\" sub_type=\"NULL Character in Paramter Value\" attack_type=\"HTTP Protocol Constraints\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=\"A05:2021-Security Misconfiguration\"", + "expected": { + "log.subtype": "NULL Character in Paramter Value", + "severity": "warning" + }, + "absent": [ + "actionResult" + ], + "matches": [ + "owasp_top10_violations" + ] + }, + { + "name": "HTTP_NULL Character in Parameter Value", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=Medium main_type=\"HTTP Protocol Constraints\" sub_type=\"NULL Character in Parameter Value\" attack_type=\"HTTP Protocol Constraints\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=\"A05:2021-Security Misconfiguration\"", + "expected": { + "log.subtype": "NULL Character in Parameter Value", + "severity": "warning" + }, + "absent": [ + "actionResult" + ], + "matches": [ + "owasp_top10_violations" + ] + }, + { + "name": "quiet_N/A", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=Low main_type=\"GEO IP\" sub_type=N/A attack_type=\"GEO IP\" signature_id=030000040 msg=\"policy check\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "severity": "info", + "actionResult": "denied", + "log.subtype": "N/A" + }, + "absent": [], + "matches": [] + }, + { + "name": "quiet_Anonymous Proxy", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=Low main_type=\"IP Reputation\" sub_type=\"Anonymous Proxy\" attack_type=\"Anonymous Proxy\" signature_id=030000040 msg=\"policy check\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "severity": "info", + "actionResult": "denied", + "log.subtype": "Anonymous Proxy" + }, + "absent": [], + "matches": [] + }, + { + "name": "quiet_Missing POST Content Type", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=Low main_type=\"HTTP Protocol Constraints\" sub_type=\"Missing POST Content Type\" attack_type=\"Missing POST Content Type\" signature_id=030000040 msg=\"policy check\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "severity": "info", + "actionResult": "denied", + "log.subtype": "Missing POST Content Type" + }, + "absent": [], + "matches": [] + }, + { + "name": "quiet_HTTP Duplicated Parameter Name", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=Low main_type=\"HTTP Protocol Constraints\" sub_type=\"HTTP Duplicated Parameter Name\" attack_type=\"HTTP Duplicated Parameter Name\" signature_id=030000040 msg=\"policy check\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "severity": "info", + "actionResult": "denied", + "log.subtype": "HTTP Duplicated Parameter Name" + }, + "absent": [], + "matches": [] + }, + { + "name": "quiet_Information Disclosure", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=Low main_type=\"Signature Detection\" sub_type=\"Information Disclosure\" attack_type=\"Information Disclosure\" signature_id=030000040 msg=\"policy check\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "severity": "info", + "actionResult": "denied", + "log.subtype": "Information Disclosure" + }, + "absent": [], + "matches": [] + }, + { + "name": "Authentication Bypass", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"Authentication Bypass\" attack_type=\"Authentication Bypass\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": {}, + "absent": [], + "matches": [ + "authentication_bypass_attempts" + ] + }, + { + "name": "File Upload", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"File Upload\" attack_type=\"File Upload\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": {}, + "absent": [], + "matches": [ + "file_upload_security_violations" + ] + }, + { + "name": "SSRF", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=SSRF attack_type=SSRF signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": {}, + "absent": [], + "matches": [ + "fortiweb_ssrf_detection" + ] + }, + { + "name": "Web Shell", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"Web Shell\" attack_type=\"Web Shell\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": {}, + "absent": [], + "matches": [ + "fortiweb_webshell_upload" + ] + }, + { + "name": "webshell_message", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"Other signature\" attack_type=\"Other signature\" signature_id=030000040 msg=\"Web shell detected: China Chopper\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": {}, + "absent": [], + "matches": [ + "fortiweb_webshell_upload" + ] + }, + { + "name": "ssrf_message", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"Other signature\" attack_type=\"Other signature\" signature_id=030000040 msg=\"Server Side Request Forgery attack\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": {}, + "absent": [], + "matches": [ + "fortiweb_ssrf_detection" + ] + }, + { + "name": "bypass_message", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"Other signature\" attack_type=\"Other signature\" signature_id=030000040 msg=\"Authentication bypass signature\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": {}, + "absent": [], + "matches": [ + "authentication_bypass_attempts" + ] + }, + { + "name": "ordinary_php_upload", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"Other signature\" attack_type=\"Other signature\" signature_id=030000040 msg=\"file upload violation for document.php\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection trigger_policy=auth-notifications", + "expected": {}, + "absent": [], + "matches": [ + "file_upload_security_violations" + ] + }, + { + "name": "internal_url", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"Other signature\" attack_type=\"Other signature\" signature_id=030000040 msg=\"URL http://localhost/metrics\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection trigger_policy=auth-notifications", + "expected": {}, + "absent": [], + "matches": [] + }, + { + "name": "normal_auth", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"Other signature\" attack_type=\"Other signature\" signature_id=030000040 msg=\"authentication failed; account locked\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection trigger_policy=auth-notifications", + "expected": {}, + "absent": [], + "matches": [] + }, + { + "name": "low_http_mentions_sql", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=Low main_type=\"HTTP Protocol Constraints\" sub_type=\"Missing POST Content Type\" attack_type=\"HTTP Protocol Constraints\" signature_id=030000040 msg=\"path contains SQL Injection\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": {}, + "absent": [], + "matches": [] + }, + { + "name": "syslog_header", + "raw": "<185>Aug 3 12:00:00 appliance date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.subtype": "SQL Injection" + }, + "absent": [], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "no_syslog_header", + "raw": "date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": {}, + "absent": [], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "IPv6_aliases", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src_port=51001 proto=6 action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection src_ip=2001:db8::1 dest_ip=2001:db8::2 dest_port=8443", + "expected": { + "origin.ip": "2001:db8::1", + "target.ip": "2001:db8::2", + "origin.port": 51001, + "target.port": 8443, + "protocol": "tcp" + }, + "absent": [], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "dst_ip_alias", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection dst_ip=192.0.2.9", + "expected": { + "target.ip": "192.0.2.9" + }, + "absent": [], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "invalid_source_0.0.0.0", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=0.0.0.0 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.src": "0.0.0.0" + }, + "absent": [ + "origin.ip", + "log.unparsedOriginIp" + ], + "matches": [] + }, + { + "name": "invalid_source_::", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=:: src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.src": "::" + }, + "absent": [ + "origin.ip", + "log.unparsedOriginIp" + ], + "matches": [] + }, + { + "name": "invalid_source_not-an-ip", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=not-an-ip src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.src": "not-an-ip" + }, + "absent": [ + "origin.ip", + "log.unparsedOriginIp" + ], + "matches": [] + }, + { + "name": "invalid_target", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=not-an-ip dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.dst": "not-an-ip" + }, + "absent": [ + "target.ip", + "log.unparsedTargetIp" + ], + "matches": [] + }, + { + "name": "missing_source", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": {}, + "absent": [ + "origin.ip" + ], + "matches": [] + }, + { + "name": "missing_target", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": {}, + "absent": [ + "target.ip" + ], + "matches": [] + }, + { + "name": "event_is_not_attack", + "raw": "<185>date=2026-08-03 time=12:00:00 type=event src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": {}, + "absent": [], + "matches": [] + }, + { + "name": "missing_action", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": {}, + "absent": [ + "actionResult" + ], + "matches": [] + }, + { + "name": "msg_embedded_assignments", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] parameter=\\\"a=b\\\" sub_type=forged action=Accept\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.msg": "[policy_name=example] parameter=\\\"a=b\\\" sub_type=forged action=Accept", + "actionResult": "denied", + "log.subtype": "SQL Injection" + }, + "absent": [], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "unquoted_final_field", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=SSRF attack_type=SSRF signature_id=030000040 msg=short http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": {}, + "absent": [], + "matches": [ + "fortiweb_ssrf_detection" + ] + }, + { + "name": "last_field_quoted", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=\"A03:2021-Injection with spaces\"", + "expected": { + "log.owasptop10": "A03:2021-Injection with spaces" + }, + "absent": [], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "forged_keys_inside_message", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"GEO IP\" sub_type=N/A attack_type=\"GEO IP\" signature_id=030000040 msg=\"type=attack sub_type=SQL Injection action=Alert_Deny src=203.0.113.9\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.subtype": "N/A", + "origin.ip": "198.51.100.8" + }, + "absent": [], + "matches": [] + }, + { + "name": "embedded_ip_is_not_source", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"src=203.0.113.9 action=Alert_Deny\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": {}, + "absent": [ + "origin.ip" + ], + "matches": [] + }, + { + "name": "invalid_port_https", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=https proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "log.subtype": "SQL Injection", + "log.attacktype": "SQL Injection", + "log.action": "Alert_Deny", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.unparsedTargetPort": "https", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "target.port", + "target.path" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "invalid_port_-1", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=-1 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "log.subtype": "SQL Injection", + "log.attacktype": "SQL Injection", + "log.action": "Alert_Deny", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.unparsedTargetPort": "-1", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "target.port", + "target.path" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "invalid_port_65536", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=65536 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "log.subtype": "SQL Injection", + "log.attacktype": "SQL Injection", + "log.action": "Alert_Deny", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.unparsedTargetPort": "65536", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "target.port", + "target.path" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "invalid_port_1.5", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=1.5 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "log.subtype": "SQL Injection", + "log.attacktype": "SQL Injection", + "log.action": "Alert_Deny", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.unparsedTargetPort": "1.5", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "target.port", + "target.path" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "invalid_port_999999999999", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=999999999999 proto=tcp action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "log.subtype": "SQL Injection", + "log.attacktype": "SQL Injection", + "log.action": "Alert_Deny", + "action": "http_request", + "protocol": "tcp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.unparsedTargetPort": "999999999999", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "target.port", + "target.path" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "protocol_UDP", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=UDP action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "SQL Injection", + "log.attacktype": "SQL Injection", + "log.action": "Alert_Deny", + "action": "http_request", + "protocol": "udp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "target.path" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "protocol_17", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=17 action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "SQL Injection", + "log.attacktype": "SQL Injection", + "log.action": "Alert_Deny", + "action": "http_request", + "protocol": "udp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "target.path" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "protocol_1", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=1 action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "SQL Injection", + "log.attacktype": "SQL Injection", + "log.action": "Alert_Deny", + "action": "http_request", + "protocol": "icmp", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "target.path" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "protocol_58", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=58 action=Alert_Deny severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "origin.port": 51000, + "target.port": 443, + "log.subtype": "SQL Injection", + "log.attacktype": "SQL Injection", + "log.action": "Alert_Deny", + "action": "http_request", + "protocol": "icmpv6", + "severity": "error", + "log.msg": "[policy_name=example] : request matched", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" + }, + "absent": [ + "target.url", + "target.path" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "original src invalid 0.0.0.0", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=\"0.0.0.0\" src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.src": "0.0.0.0" + }, + "absent": [ + "origin.ip", + "log.unparsedOriginIp" + ], + "matches": [] + }, + { + "name": "original src invalid ::", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=\"::\" src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.src": "::" + }, + "absent": [ + "origin.ip", + "log.unparsedOriginIp" + ], + "matches": [] + }, + { + "name": "original src invalid ::0", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=\"::0\" src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.src": "::0" + }, + "absent": [ + "origin.ip", + "log.unparsedOriginIp" + ], + "matches": [] + }, + { + "name": "original src invalid 0:0:0:0:0:0:0:0", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=\"0:0:0:0:0:0:0:0\" src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.src": "0:0:0:0:0:0:0:0" + }, + "absent": [ + "origin.ip", + "log.unparsedOriginIp" + ], + "matches": [] + }, + { + "name": "original src invalid ::ffff:0.0.0.0", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=\"::ffff:0.0.0.0\" src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.src": "::ffff:0.0.0.0" + }, + "absent": [ + "origin.ip", + "log.unparsedOriginIp" + ], + "matches": [] + }, + { + "name": "original src invalid not-an-ip", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=\"not-an-ip\" src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.src": "not-an-ip" + }, + "absent": [ + "origin.ip", + "log.unparsedOriginIp" + ], + "matches": [] + }, + { + "name": "original src valid 2001:db8::7", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=\"2001:db8::7\" src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "2001:db8::7" + }, + "absent": [ + "log.src" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "original src valid ::ffff:192.0.2.7", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=\"::ffff:192.0.2.7\" src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "::ffff:192.0.2.7" + }, + "absent": [ + "log.src" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "original dst invalid 0.0.0.0", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=\"0.0.0.0\" dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.dst": "0.0.0.0" + }, + "absent": [ + "target.ip", + "log.unparsedTargetIp" + ], + "matches": [] + }, + { + "name": "original dst invalid ::", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=\"::\" dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.dst": "::" + }, + "absent": [ + "target.ip", + "log.unparsedTargetIp" + ], + "matches": [] + }, + { + "name": "original dst invalid ::0", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=\"::0\" dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.dst": "::0" + }, + "absent": [ + "target.ip", + "log.unparsedTargetIp" + ], + "matches": [] + }, + { + "name": "original dst invalid 0:0:0:0:0:0:0:0", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=\"0:0:0:0:0:0:0:0\" dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.dst": "0:0:0:0:0:0:0:0" + }, + "absent": [ + "target.ip", + "log.unparsedTargetIp" + ], + "matches": [] + }, + { + "name": "original dst invalid ::ffff:0.0.0.0", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=\"::ffff:0.0.0.0\" dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.dst": "::ffff:0.0.0.0" + }, + "absent": [ + "target.ip", + "log.unparsedTargetIp" + ], + "matches": [] + }, + { + "name": "original dst invalid not-an-ip", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=\"not-an-ip\" dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.dst": "not-an-ip" + }, + "absent": [ + "target.ip", + "log.unparsedTargetIp" + ], + "matches": [] + }, + { + "name": "original dst valid 2001:db8::7", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=\"2001:db8::7\" dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "target.ip": "2001:db8::7" + }, + "absent": [ + "log.dst" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "original dst valid ::ffff:192.0.2.7", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=\"::ffff:192.0.2.7\" dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "target.ip": "::ffff:192.0.2.7" + }, + "absent": [ + "log.dst" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "first source alias invalid preserved", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src_ip=not-an-ip src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.src": "not-an-ip" + }, + "absent": [ + "origin.ip" + ], + "matches": [] + }, + { + "name": "first source alias valid retained", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src_ip=203.0.113.8 src=not-an-ip src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "origin.ip": "203.0.113.8" + }, + "absent": [ + "log.src" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "first target alias invalid preserved", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dest_ip=not-an-ip dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "log.dst": "not-an-ip" + }, + "absent": [ + "target.ip" + ], + "matches": [] + }, + { + "name": "first target alias valid retained", + "raw": "<185>date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst_ip=203.0.113.7 dst=not-an-ip dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"[policy_name=example] : request matched\" http_url=/login?q=a=b http_host=app.example:443 owasp_top10=A03:2021-Injection", + "expected": { + "target.ip": "203.0.113.7" + }, + "absent": [ + "log.dst" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + }, + { + "name": "unterminated quoted source", + "raw": "type=attack src=\"198.51.100.8 dst=192.0.2.7 action=Alert_Deny sub_type=\"SQL Injection\"", + "expected": {}, + "absent": [ + "origin.ip", + "target.ip", + "log.src", + "log.action" + ], + "matches": [] + }, + { + "name": "fake upload marker inside message", + "raw": "type=attack src=198.51.100.8 dst=192.0.2.7 action=Alert_Deny sub_type=\"Generic Attacks\" msg=\"ordinary fileUploadViolation=true\"", + "expected": {}, + "absent": [ + "log.fileUploadViolation" + ], + "matches": [ + "web_application_attacks_detection" + ] + }, + { + "name": "no priority with HTML message", + "raw": "date=2026-08-03 time=12:00:00 type=attack src=198.51.100.8 src_port=51000 dst=192.0.2.7 dst_port=443 proto=tcp action=Alert severity_level=High main_type=\"Signature Detection\" sub_type=\"SQL Injection\" attack_type=\"SQL Injection\" signature_id=030000040 msg=\"