From 4d5de861243b129c6248a7138d2da05fbc6cff4c Mon Sep 17 00:00:00 2001 From: Ricardo Valdes Date: Wed, 16 Sep 2026 20:34:27 -0400 Subject: [PATCH 1/3] fix: align FortiWeb filter and rule contracts --- filters/audits/fortiweb.md | 40 ++++++++++++++ filters/fortinet/fortiweb.yml | 18 +++++-- .../testdata/filter-contracts/fortiweb.json | 54 +++++++++++++++++++ .../fortiweb/fortiweb_webshell_upload.yml | 2 +- 4 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 filters/audits/fortiweb.md create mode 100644 plugins/alerts/testdata/filter-contracts/fortiweb.json diff --git a/filters/audits/fortiweb.md b/filters/audits/fortiweb.md new file mode 100644 index 000000000..16346e56a --- /dev/null +++ b/filters/audits/fortiweb.md @@ -0,0 +1,40 @@ +# FortiWeb normalization and rule review + +Normalize denial/IP values and repair the web-shell rule CEL regex escapes. + +This draft targets UTMStack `v11`. It contains 1 filter changes +and 1 rule changes for this technology only. Review covered +1 filter configurations and 7 matching shipped rule files. +Unchanged rules are listed in the regression manifest; they are not duplicated in the diff. + +## Contract and validation + +- Compared exact standard names/types with go-sdk v1.1.31 and the supplied UTMStack dictionaries. +- Checked documented pipeline ordering, rename/move behavior, open vendor log fields, + event-side versus alert-side fields, and surviving fields used by affected rule predicates/history/grouping. +- Strict SDK configuration decoding and actual CEL compilation pass for this scope. +- 2 synthetic normalization cases pass, including SDK Event conversion and any + trigger predicate assertions recorded in the manifest. +- The scoped alerts module tests and `git diff --check` pass with the shared contract runner applied. + +The shared alert-contract PR supplies the reusable Go runner for the manifest in +`plugins/alerts/testdata/filter-contracts/fortiweb.json`. Apply that support before running `go test ./...` in `plugins/alerts`. + +The model starts from synthetic extraction results. It does not run complex grok, +JSON/KV/XML/CSV extraction, time conversion, dynamic plugins, historical OpenSearch +queries, or the closed EventProcessor. Raw vendor logs and resulting alerts must +still be checked in staging before rollout. No customer false-positive reduction +has been measured and no production rollout is included. + + + +## References + +- [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](https://github.com/threatwinds/go-sdk/wiki/Implementing-Rules) + +`afterEvents`, empty noncapturing grok names, supported numeric strings, and custom +`log.*` fields are accepted. Existing textual protocol casing and vendor action names +are preserved unless a concrete consumer mismatch requires correction. diff --git a/filters/fortinet/fortiweb.yml b/filters/fortinet/fortiweb.yml index 686a9356e..fa136b068 100644 --- a/filters/fortinet/fortiweb.yml +++ b/filters/fortinet/fortiweb.yml @@ -100,8 +100,8 @@ pipeline: function: string params: key: actionResult - value: "blocked" - where: 'exists("action") && equalsIgnoreCase("action", "Deny")' + value: denied + where: exists("action") && equalsIgnoreCase("action", "Deny") # Removing unused caracters - trim: @@ -135,4 +135,16 @@ pipeline: - delete: fields: - log.kvMessage - - log.irrelevant \ No newline at end of file + - log.irrelevant + + # Keep addresses in IP fields and retain other source values under log. + - rename: + from: + - origin.ip + to: log.unparsedOriginIp + where: exists("origin.ip") && (!(inCIDR("origin.ip","0.0.0.0/0") || inCIDR("origin.ip","::/0")) || oneOf("origin.ip",["0.0.0.0","::"])) + - rename: + from: + - target.ip + to: log.unparsedTargetIp + where: exists("target.ip") && (!(inCIDR("target.ip","0.0.0.0/0") || inCIDR("target.ip","::/0")) || oneOf("target.ip",["0.0.0.0","::"])) diff --git a/plugins/alerts/testdata/filter-contracts/fortiweb.json b/plugins/alerts/testdata/filter-contracts/fortiweb.json new file mode 100644 index 000000000..341304c46 --- /dev/null +++ b/plugins/alerts/testdata/filter-contracts/fortiweb.json @@ -0,0 +1,54 @@ +{ + "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_sqli_detection.yml", + "rules/fortinet/fortiweb/fortiweb_ssrf_detection.yml", + "rules/fortinet/fortiweb/fortiweb_webshell_upload.yml", + "rules/fortinet/fortiweb/owasp_top10_violations.yml", + "rules/fortinet/fortiweb/web_application_attacks_detection.yml" + ], + "fixtures": [ + { + "name": "fortiweb_Deny", + "filter": "fortinet/fortiweb.yml", + "input": { + "log": { + "action": "Deny", + "src": "198.51.100.8", + "dst": "192.0.2.7" + } + }, + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7", + "actionResult": "denied" + }, + "absent": [], + "rules": {} + }, + { + "name": "fortiweb_Alert", + "filter": "fortinet/fortiweb.yml", + "input": { + "log": { + "action": "Alert", + "src": "198.51.100.8", + "dst": "192.0.2.7" + } + }, + "expected": { + "origin.ip": "198.51.100.8", + "target.ip": "192.0.2.7" + }, + "absent": [ + "actionResult" + ], + "rules": {} + } + ] +} diff --git a/rules/fortinet/fortiweb/fortiweb_webshell_upload.yml b/rules/fortinet/fortiweb/fortiweb_webshell_upload.yml index 9313b051d..e8e1a9bcc 100644 --- a/rules/fortinet/fortiweb/fortiweb_webshell_upload.yml +++ b/rules/fortinet/fortiweb/fortiweb_webshell_upload.yml @@ -25,7 +25,7 @@ description: | 6. If a web shell was successfully uploaded, immediately isolate the server where: | oneOf("action", ["deny", "block", "alert_deny", "alert"]) && - (regexMatch("log.msg", "(?i)(web.*shell|backdoor.*upload|cmd.*shell|reverse.*shell|\.php.*upload|\.asp.*upload|\.jsp.*upload|c99|r57|china.*chopper|weevely|b374k)") || + (regexMatch("log.msg", "(?i)(web.*shell|backdoor.*upload|cmd.*shell|reverse.*shell|\\.php.*upload|\\.asp.*upload|\\.jsp.*upload|c99|r57|china.*chopper|weevely|b374k)") || (contains("log.attack_type", "file_upload") && contains("log.msg", ["shell", "backdoor", "malicious"])) || regexMatch("log.msg", "(?i)(file.*upload.*violat|upload.*restrict|dangerous.*file.*type)")) groupBy: From 3ec9b18f88258e05d669e3b91de410db6cd65f7f Mon Sep 17 00:00:00 2001 From: Ricardo Valdes Date: Thu, 17 Sep 2026 09:23:15 -0400 Subject: [PATCH 2/3] fix(fortiweb): preserve attack classifications and align detections --- filters/audits/fortiweb.md | 117 +- filters/fortinet/fortiweb.yml | 597 ++++++--- plugins/alerts/fortiweb_contract_test.go | 323 +++++ .../testdata/filter-contracts/fortiweb.json | 38 +- plugins/alerts/testdata/fortiweb_raw.json | 1181 +++++++++++++++++ .../authentication_bypass_attempts.yml | 52 +- .../file_upload_security_violations.yml | 73 +- .../fortiweb/fortiweb_known_exploits.yml | 26 + .../fortiweb/fortiweb_malware_detection.yml | 26 + .../fortiweb/fortiweb_sqli_detection.yml | 39 +- .../fortiweb/fortiweb_ssrf_detection.yml | 39 +- .../fortiweb/fortiweb_webshell_upload.yml | 31 +- .../fortiweb/fortiweb_xss_detection.yml | 26 + .../fortiweb/owasp_top10_violations.yml | 72 +- .../web_application_attacks_detection.yml | 77 +- 15 files changed, 2299 insertions(+), 418 deletions(-) create mode 100644 plugins/alerts/fortiweb_contract_test.go create mode 100644 plugins/alerts/testdata/fortiweb_raw.json create mode 100644 rules/fortinet/fortiweb/fortiweb_known_exploits.yml create mode 100644 rules/fortinet/fortiweb/fortiweb_malware_detection.yml create mode 100644 rules/fortinet/fortiweb/fortiweb_xss_detection.yml diff --git a/filters/audits/fortiweb.md b/filters/audits/fortiweb.md index 16346e56a..e64767674 100644 --- a/filters/audits/fortiweb.md +++ b/filters/audits/fortiweb.md @@ -1,40 +1,105 @@ -# FortiWeb normalization and rule review +# FortiWeb parsing and detection contract -Normalize denial/IP values and repair the web-shell rule CEL regex escapes. +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. -This draft targets UTMStack `v11`. It contains 1 filter changes -and 1 rule changes for this technology only. Review covered -1 filter configurations and 7 matching shipped rule files. -Unchanged rules are listed in the regression manifest; they are not duplicated in the diff. +## Confirmed defects -## Contract and validation +Read-only inspection of retained production events and the loaded configuration confirmed: -- Compared exact standard names/types with go-sdk v1.1.31 and the supplied UTMStack dictionaries. -- Checked documented pipeline ordering, rename/move behavior, open vendor log fields, - event-side versus alert-side fields, and surviving fields used by affected rule predicates/history/grouping. -- Strict SDK configuration decoding and actual CEL compilation pass for this scope. -- 2 synthetic normalization cases pass, including SDK Event conversion and any - trigger predicate assertions recorded in the manifest. -- The scoped alerts module tests and `git diff --check` pass with the shared contract runner applied. +- 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. -The shared alert-contract PR supplies the reusable Go runner for the manifest in -`plugins/alerts/testdata/filter-contracts/fortiweb.json`. Apply that support before running `go test ./...` in `plugins/alerts`. +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. -The model starts from synthetic extraction results. It does not run complex grok, -JSON/KV/XML/CSV extraction, time conversion, dynamic plugins, historical OpenSearch -queries, or the closed EventProcessor. Raw vendor logs and resulting alerts must -still be checked in staging before rollout. No customer false-positive reduction -has been measured and no production rollout is included. +## 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 `log.unparsed*Ip`, removed 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_url`, `HTTP_host` | Relative URL goes to `target.path`; full HTTP(S) URL 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. + +## Verification and limits + +- Read the filter/rule wiki and SDK v1.1.31 implementation; checked standard fields + against the supplied dictionaries and SDK schema. The wiki's lowercase protocol + convention resolves the older dictionary's conflicting uppercase recommendation. +- `go test ./...` in `plugins/alerts` runs the standalone FortiWeb tests: **74 synthetic + raw-log cases, each evaluated against all 10 rule predicates**, plus history and + alert-side grouping contracts. Strict SDK YAML decoding, real CEL evaluation and + final SDK Event conversion are exercised. +- With the shared runner from PR #2590 temporarily applied, all **96 subtests** pass. + The technology manifest supplies an additional invalid-address normalization case. +- Read-only category aggregates were projected through the new filter/rule contract + to check coverage of retained classifications. These are predicate candidates, + not observed alerts or a full historical replay. +- `git diff --check` passes. + +The raw contract harness models documented grok concatenation and transforms with +Go RE2. It deliberately omits KV, dynamic plugins and the closed EventProcessor; +all asserted/detection fields must therefore come from the quote-aware extraction. +It does not execute live OpenSearch correlation, deduplication or alert publication. +The standalone tests need no shared runner. Review shared alert grouping fix #2590 +before rollout, since it affects actual grouping/deduplication behavior. + +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](https://github.com/threatwinds/go-sdk/wiki/Implementing-Rules) - -`afterEvents`, empty noncapturing grok names, supported numeric strings, and custom -`log.*` fields are accepted. Existing textual protocol casing and vendor action names -are preserved unless a concrete consumer mismatch requires correction. +- [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 fa136b068..21272c40b 100644 --- a/filters/fortinet/fortiweb.yml +++ b/filters/fortinet/fortiweb.yml @@ -1,150 +1,449 @@ -# 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.1.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: denied - 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 - - # Keep addresses in IP fields and retain other source values under log. - - rename: - from: - - origin.ip - to: log.unparsedOriginIp - where: exists("origin.ip") && (!(inCIDR("origin.ip","0.0.0.0/0") || inCIDR("origin.ip","::/0")) || oneOf("origin.ip",["0.0.0.0","::"])) - - rename: - from: - - target.ip - to: log.unparsedTargetIp - where: exists("target.ip") && (!(inCIDR("target.ip","0.0.0.0/0") || inCIDR("target.ip","::/0")) || oneOf("target.ip",["0.0.0.0","::"])) +- 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.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}}(?: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.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.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 + - rename: + from: + - log.dst + to: target.ip + - 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)$") + - grok: + source: log.httpurl + patterns: + - fieldName: target.path + pattern: '{{.greedy}}' + where: startsWith("log.httpurl", "/") + - 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)) + - rename: + from: + - origin.ip + to: log.unparsedOriginIp + where: exists("origin.ip") && (!(inCIDR("origin.ip", "0.0.0.0/0") || inCIDR("origin.ip", "::/0")) || oneOf("origin.ip", + ["0.0.0.0", "::"])) + - 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)) + - rename: + from: + - target.ip + to: log.unparsedTargetIp + where: exists("target.ip") && (!(inCIDR("target.ip", "0.0.0.0/0") || inCIDR("target.ip", "::/0")) || oneOf("target.ip", + ["0.0.0.0", "::"])) + - 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..5a87d5a4c --- /dev/null +++ b/plugins/alerts/fortiweb_contract_test.go @@ -0,0 +1,323 @@ +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 is omitted deliberately: every asserted/detection field must be recovered +// by the quote-aware grok steps. Dynamic enrichment and live correlation are not run. +import ( + "bytes" + "encoding/json" + "fmt" + "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 { + t.Helper() + draft := map[string]any{"raw": raw, "dataType": "firewall-fortiweb", "log": map[string]any{}} + for _, stage := range cfg.Pipeline { + 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": + 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", "dynamic": // Intentional exclusions described above. + 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 +} +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 { + if !gjson.Get(out, term.Field).Exists() { + t.Errorf("%s: matched event lacks history field %s", name, term.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/testdata/filter-contracts/fortiweb.json b/plugins/alerts/testdata/filter-contracts/fortiweb.json index 341304c46..92d04fce6 100644 --- a/plugins/alerts/testdata/filter-contracts/fortiweb.json +++ b/plugins/alerts/testdata/filter-contracts/fortiweb.json @@ -6,46 +6,34 @@ "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_Deny", + "name": "fortiweb_non_ip_is_not_enriched", "filter": "fortinet/fortiweb.yml", "input": { - "log": { - "action": "Deny", - "src": "198.51.100.8", - "dst": "192.0.2.7" + "origin": { + "ip": "not-an-ip" + }, + "target": { + "ip": "0.0.0.0" } }, "expected": { - "origin.ip": "198.51.100.8", - "target.ip": "192.0.2.7", - "actionResult": "denied" - }, - "absent": [], - "rules": {} - }, - { - "name": "fortiweb_Alert", - "filter": "fortinet/fortiweb.yml", - "input": { - "log": { - "action": "Alert", - "src": "198.51.100.8", - "dst": "192.0.2.7" - } - }, - "expected": { - "origin.ip": "198.51.100.8", - "target.ip": "192.0.2.7" + "log.unparsedOriginIp": "not-an-ip", + "log.unparsedTargetIp": "0.0.0.0" }, "absent": [ + "origin.ip", + "target.ip", "actionResult" ], "rules": {} diff --git a/plugins/alerts/testdata/fortiweb_raw.json b/plugins/alerts/testdata/fortiweb_raw.json new file mode 100644 index 000000000..8e1441cfb --- /dev/null +++ b/plugins/alerts/testdata/fortiweb_raw.json @@ -0,0 +1,1181 @@ +[ + { + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443" + }, + "absent": [ + "target.url", + "actionResult" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443", + "actionResult": "denied" + }, + "absent": [ + "target.url" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443" + }, + "absent": [ + "target.url", + "actionResult" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443", + "actionResult": "denied" + }, + "absent": [ + "target.url" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443" + }, + "absent": [ + "target.url", + "actionResult" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443", + "actionResult": "denied" + }, + "absent": [ + "target.url" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443" + }, + "absent": [ + "target.url", + "actionResult" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443", + "actionResult": "denied" + }, + "absent": [ + "target.url" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443" + }, + "absent": [ + "target.url", + "actionResult" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443", + "actionResult": "denied" + }, + "absent": [ + "target.url" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443" + }, + "absent": [ + "target.url", + "actionResult" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443", + "actionResult": "denied" + }, + "absent": [ + "target.url" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443" + }, + "absent": [ + "target.url", + "actionResult" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443", + "actionResult": "denied" + }, + "absent": [ + "target.url" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443" + }, + "absent": [ + "target.url", + "actionResult" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443", + "actionResult": "denied" + }, + "absent": [ + "target.url" + ], + "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.unparsedOriginIp": "0.0.0.0" + }, + "absent": [ + "origin.ip" + ], + "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.unparsedOriginIp": "::" + }, + "absent": [ + "origin.ip" + ], + "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.unparsedOriginIp": "not-an-ip" + }, + "absent": [ + "origin.ip" + ], + "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.unparsedTargetIp": "not-an-ip" + }, + "absent": [ + "target.ip" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.unparsedTargetPort": "https" + }, + "absent": [ + "target.url", + "target.port" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.unparsedTargetPort": "-1" + }, + "absent": [ + "target.url", + "target.port" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.unparsedTargetPort": "65536" + }, + "absent": [ + "target.url", + "target.port" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.unparsedTargetPort": "1.5" + }, + "absent": [ + "target.url", + "target.port" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443", + "actionResult": "denied", + "log.unparsedTargetPort": "999999999999" + }, + "absent": [ + "target.url", + "target.port" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443", + "actionResult": "denied" + }, + "absent": [ + "target.url" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443", + "actionResult": "denied" + }, + "absent": [ + "target.url" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443", + "actionResult": "denied" + }, + "absent": [ + "target.url" + ], + "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", + "target.path": "/login?q=a=b", + "log.httphost": "app.example:443", + "actionResult": "denied" + }, + "absent": [ + "target.url" + ], + "matches": [ + "fortiweb_sqli_detection" + ] + } +] diff --git a/rules/fortinet/fortiweb/authentication_bypass_attempts.yml b/rules/fortinet/fortiweb/authentication_bypass_attempts.yml index 2e039b6a8..a76afa8e0 100644 --- a/rules/fortinet/fortiweb/authentication_bypass_attempts.yml +++ b/rules/fortinet/fortiweb/authentication_bypass_attempts.yml @@ -1,51 +1,27 @@ -# Rule version v1.0.0 +# Rule version v1.1.0 dataTypes: - - firewall-fortiweb +- firewall-fortiweb name: FortiWeb Authentication Bypass Attempt Detected impact: confidentiality: 3 integrity: 3 availability: 1 -category: Defense Evasion, Persistence, Privilege Escalation, Initial Access -technique: "T1078 - Valid Accounts" +category: Initial Access +technique: T1190 - Exploit Public-Facing Application adversary: origin references: - - https://docs.fortinet.com/document/fortiweb/7.2.2/log-message-reference/252057/event - - https://attack.mitre.org/techniques/T1078/ - - https://attack.mitre.org/techniques/T1190/ +- https://docs.fortinet.com/document/fortiweb/7.2.2/log-message-reference/252057/event +- https://attack.mitre.org/techniques/T1078/ +- https://attack.mitre.org/techniques/T1190/ description: | - Detects authentication bypass attempts including direct access to protected resources, authentication flow manipulation, and credential stuffing attacks on FortiWeb protected applications. + Detects explicit authentication or authorization bypass classifications/messages. A notification policy name, login failure or account lockout alone is insufficient. - Next Steps: - 1. Review the source IP address and check if it's from a known location or VPN service - 2. Examine the specific authentication bypass technique attempted (check log.attack_type and log.msg fields) - 3. Verify if any successful authentication occurred from the same IP address - 4. Check for other attack patterns from the same source IP in the last 24 hours - 5. Consider blocking the source IP if multiple bypass attempts are confirmed - 6. Review application logs for any successful unauthorized access - 7. Validate that authentication mechanisms are properly configured and up to date + Both Alert (log-only) and explicit blocking decisions are covered. Check log.action and actionResult to distinguish detection from prevention; neither establishes successful exploitation. Review log.subtype, log.attacktype, log.signatureid and log.msg, then validate the request and outcome in application logs before escalating. where: | - exists("origin.ip") && - equals("log.type", "attack") && - ( - equals("log.subtype", "account-lockout-detection") || - contains("log.msg", "authentication") || - contains("log.msg", "Authentication") || - contains("log.msg", "bypass") || - contains("log.msg", "unauthorized access") || - equals("log.attack_type", "auth_bypass") || - contains("log.trigger_policy", "auth") - ) && - oneOf("action", ["deny", "alert", "block"]) -afterEvents: - - indexPattern: v11-log-firewall-fortiweb-* - with: - - field: origin.ip - operator: filter_term - value: '{{.origin.ip}}' - within: 15m - count: 3 + exists("origin.ip") && exists("target.ip") && equalsIgnoreCase("log.type", "attack") && + regexMatch("log.action", "(?i)^(alert|alert_deny|deny|denied|block|blocked|period_block)$") && + ((regexMatch("log.subtype", "(?i)^(Authentication Bypass|Authorization Bypass|auth_bypass)$") || regexMatch("log.attacktype", "(?i)^(Authentication Bypass|Authorization Bypass|auth_bypass)$") || regexMatch("log.signaturesubclass", "(?i)^(Authentication Bypass|Authorization Bypass|auth_bypass)$")) || ((equalsIgnoreCase("log.maintype", "Signature Detection") || (!exists("log.subtype") && !exists("log.attacktype"))) && regexMatch("log.msg", "(?i)\\b(auth(?:entication|orization)?[ _-]*bypass)\\b"))) groupBy: - - adversary.ip - - target.ip +- adversary.ip +- target.ip diff --git a/rules/fortinet/fortiweb/file_upload_security_violations.yml b/rules/fortinet/fortiweb/file_upload_security_violations.yml index 27bb24c24..0eb83004b 100644 --- a/rules/fortinet/fortiweb/file_upload_security_violations.yml +++ b/rules/fortinet/fortiweb/file_upload_security_violations.yml @@ -1,50 +1,51 @@ -# Rule version v1.0.0 +# Rule version v1.1.0 dataTypes: - - firewall-fortiweb +- firewall-fortiweb name: FortiWeb File Upload Security Violation Detected impact: confidentiality: 3 integrity: 3 availability: 2 category: Resource Development -technique: "T1608 - Stage Capabilities: Upload Malware" +technique: 'T1608 - Stage Capabilities: Upload Malware' adversary: origin references: - - https://docs.fortinet.com/document/fortiweb/7.2.2/log-message-reference/252057/event - - https://attack.mitre.org/techniques/T1608/ - - https://attack.mitre.org/techniques/T1505/003/ +- https://docs.fortinet.com/document/fortiweb/7.2.2/log-message-reference/252057/event +- https://attack.mitre.org/techniques/T1608/ +- https://attack.mitre.org/techniques/T1505/003/ description: | - Detects file upload security violations including malicious file uploads, oversized files, restricted file types, and potential web shell upload attempts on FortiWeb protected applications. These violations could indicate attempts to upload malicious files, web shells, or bypass upload restrictions. + Detects repeated file-upload or antivirus policy violations against the same application and attack subtype. A policy violation is not proof of a web shell or successful malware upload. - Next Steps: - 1. Review the blocked file upload attempt details, including file name, type, and size - 2. Check if the source IP has made multiple file upload attempts in a short period - 3. Examine the web application logs for the specific URL path being targeted - 4. Verify if the file upload restriction policy is appropriate for the application - 5. If malicious activity is confirmed, consider blocking the source IP temporarily - 6. Review other requests from the same source IP for reconnaissance patterns - 7. Check if similar file upload attempts have been made to other applications + Both Alert (log-only) and explicit blocking decisions are covered. Check log.action and actionResult to distinguish detection from prevention; neither establishes successful exploitation. Review log.subtype, log.attacktype, log.signatureid and log.msg, then validate the request and outcome in application logs before escalating. where: | - exists("origin.ip") && - ( - contains("log.msg", "file upload") || - contains("log.msg", "File upload") || - contains("log.msg", "malicious file") || - contains("log.msg", "file type") || - contains("log.msg", "file size") || - contains("log.msg", "anti-virus") || - contains("log.msg", "upload violation") || - contains("log.msg", "upload blocked") - ) + exists("origin.ip") && exists("target.ip") && equalsIgnoreCase("log.type", "attack") && + regexMatch("log.action", "(?i)^(alert|alert_deny|deny|denied|block|blocked|period_block)$") && + equals("log.fileUploadViolation", "true") && exists("log.subtype") afterEvents: - - indexPattern: v11-log-firewall-fortiweb-* - with: - - field: origin.ip - operator: filter_term - value: '{{.origin.ip}}' - within: 30m - count: 3 -groupBy: - - adversary.ip - - target.ip +- indexPattern: v11-log-firewall-fortiweb-* + with: + - field: origin.ip + operator: filter_term + value: '{{.origin.ip}}' + - field: target.ip + operator: filter_term + value: '{{.target.ip}}' + - field: log.type + operator: filter_term + value: '{{.log.type}}' + - field: log.subtype + operator: filter_term + value: '{{.log.subtype}}' + - field: log.action + operator: filter_term + value: '{{.log.action}}' + - field: log.fileUploadViolation + operator: filter_term + value: '{{.log.fileUploadViolation}}' + within: 30m + count: 3 +deduplicateBy: +- adversary.ip +- target.ip +- lastEvent.log.subtype diff --git a/rules/fortinet/fortiweb/fortiweb_known_exploits.yml b/rules/fortinet/fortiweb/fortiweb_known_exploits.yml new file mode 100644 index 000000000..1cb3ce312 --- /dev/null +++ b/rules/fortinet/fortiweb/fortiweb_known_exploits.yml @@ -0,0 +1,26 @@ +# Rule version v1.1.0 + +dataTypes: +- firewall-fortiweb +name: FortiWeb Known Exploit Attempt Detection +impact: + confidentiality: 3 + integrity: 3 + availability: 2 +category: Web Application Attack +technique: T1190 - Exploit Public-Facing Application +adversary: origin +references: +- https://docs.fortinet.com/document/fortiweb/7.2.2/log-message-reference/445549/attack +- https://attack.mitre.org/techniques/T1190/ +description: | + Detects the Known Exploits signature category reported by FortiWeb. This records an exploit attempt and does not establish that the application was compromised. + + Both Alert (log-only) and explicit blocking decisions are covered. Check log.action and actionResult to distinguish detection from prevention; neither establishes successful exploitation. Review log.subtype, log.attacktype, log.signatureid and log.msg, then validate the request and outcome in application logs before escalating. +where: | + exists("origin.ip") && exists("target.ip") && equalsIgnoreCase("log.type", "attack") && + regexMatch("log.action", "(?i)^(alert|alert_deny|deny|denied|block|blocked|period_block)$") && + ((regexMatch("log.subtype", "(?i)^(Known Exploits)$") || regexMatch("log.attacktype", "(?i)^(Known Exploits)$") || regexMatch("log.signaturesubclass", "(?i)^(Known Exploits)$"))) +groupBy: +- adversary.ip +- target.ip diff --git a/rules/fortinet/fortiweb/fortiweb_malware_detection.yml b/rules/fortinet/fortiweb/fortiweb_malware_detection.yml new file mode 100644 index 000000000..6e2c505e8 --- /dev/null +++ b/rules/fortinet/fortiweb/fortiweb_malware_detection.yml @@ -0,0 +1,26 @@ +# Rule version v1.1.0 + +dataTypes: +- firewall-fortiweb +name: FortiWeb Malware Detection +impact: + confidentiality: 3 + integrity: 3 + availability: 2 +category: Malware +technique: T1190 - Exploit Public-Facing Application +adversary: origin +references: +- https://docs.fortinet.com/document/fortiweb/7.2.2/log-message-reference/445549/attack +- https://attack.mitre.org/techniques/T1190/ +description: | + Detects the Trojans signature category reported by FortiWeb. A single malware detection is sufficient; prevention status is carried separately. + + Both Alert (log-only) and explicit blocking decisions are covered. Check log.action and actionResult to distinguish detection from prevention; neither establishes successful exploitation. Review log.subtype, log.attacktype, log.signatureid and log.msg, then validate the request and outcome in application logs before escalating. +where: | + exists("origin.ip") && exists("target.ip") && equalsIgnoreCase("log.type", "attack") && + regexMatch("log.action", "(?i)^(alert|alert_deny|deny|denied|block|blocked|period_block)$") && + ((regexMatch("log.subtype", "(?i)^(Trojans)$") || regexMatch("log.attacktype", "(?i)^(Trojans)$") || regexMatch("log.signaturesubclass", "(?i)^(Trojans)$"))) +groupBy: +- adversary.ip +- target.ip diff --git a/rules/fortinet/fortiweb/fortiweb_sqli_detection.yml b/rules/fortinet/fortiweb/fortiweb_sqli_detection.yml index c82e9d137..1e305c3a3 100644 --- a/rules/fortinet/fortiweb/fortiweb_sqli_detection.yml +++ b/rules/fortinet/fortiweb/fortiweb_sqli_detection.yml @@ -1,41 +1,26 @@ -# Rule version v1.0.0 +# Rule version v1.1.0 dataTypes: - - firewall-fortiweb +- firewall-fortiweb name: FortiWeb SQL Injection Attack Detection impact: confidentiality: 3 integrity: 3 availability: 2 category: Web Application Attack -technique: "T1190 - Exploit Public-Facing Application" +technique: T1190 - Exploit Public-Facing Application adversary: origin references: - - https://docs.fortinet.com/document/fortiweb/7.4.3/administration-guide/210196/blocking-known-attacks - - https://attack.mitre.org/techniques/T1190/ +- https://docs.fortinet.com/document/fortiweb/7.4.3/administration-guide/210196/blocking-known-attacks +- https://attack.mitre.org/techniques/T1190/ description: | - Detects SQL injection attacks identified by FortiWeb WAF signatures. Multiple SQLi attempts from the same source indicate a targeted attack against database-backed applications. + Detects SQL injection classified by FortiWeb, including extended signatures. A single classified attack is sufficient; unrelated requests must not be required to satisfy a history threshold. - Next Steps: - 1. Review the specific SQL injection payloads to assess severity - 2. Check if the target application is vulnerable to the attempted injection - 3. Verify the attack was blocked by FortiWeb - 4. Review application logs for any successful SQLi exploitation - 5. Investigate the source IP for additional attack patterns - 6. Update WAF signatures and consider implementing parameterized queries + Both Alert (log-only) and explicit blocking decisions are covered. Check log.action and actionResult to distinguish detection from prevention; neither establishes successful exploitation. Review log.subtype, log.attacktype, log.signatureid and log.msg, then validate the request and outcome in application logs before escalating. where: | - oneOf("action", ["deny", "block", "alert_deny", "alert"]) && - (regexMatch("log.msg", "(?i)(sql.*inject|union.*select|concat.*0x|xp_cmdshell|information_schema|benchmark.*sleep|extractvalue|updatexml|load_file|into.*outfile)") || - contains("log.attack_type", "sql_injection") || - contains("log.signature", "SQL Injection")) -afterEvents: - - indexPattern: v11-log-firewall-fortiweb-* - with: - - field: origin.ip - operator: filter_term - value: '{{.origin.ip}}' - within: 15m - count: 5 + exists("origin.ip") && exists("target.ip") && equalsIgnoreCase("log.type", "attack") && + regexMatch("log.action", "(?i)^(alert|alert_deny|deny|denied|block|blocked|period_block)$") && + ((regexMatch("log.subtype", "(?i)^(SQL Injection(?:\\s*\\(Extended\\))?|SQLi)$") || regexMatch("log.attacktype", "(?i)^(SQL Injection(?:\\s*\\(Extended\\))?|SQLi)$") || regexMatch("log.signaturesubclass", "(?i)^(SQL Injection(?:\\s*\\(Extended\\))?|SQLi)$")) || ((!exists("log.subtype") && !exists("log.attacktype")) && regexMatch("log.msg", "(?i)\\b(sql[ _-]*injection|sqli)\\b"))) groupBy: - - adversary.ip - - target.ip +- adversary.ip +- target.ip diff --git a/rules/fortinet/fortiweb/fortiweb_ssrf_detection.yml b/rules/fortinet/fortiweb/fortiweb_ssrf_detection.yml index 2dfe9b15d..6751bbbe0 100644 --- a/rules/fortinet/fortiweb/fortiweb_ssrf_detection.yml +++ b/rules/fortinet/fortiweb/fortiweb_ssrf_detection.yml @@ -1,41 +1,26 @@ -# Rule version v1.0.0 +# Rule version v1.1.0 dataTypes: - - firewall-fortiweb +- firewall-fortiweb name: FortiWeb SSRF Attack Detection impact: confidentiality: 3 integrity: 2 availability: 1 category: Web Application Attack -technique: "T1190 - Exploit Public-Facing Application" +technique: T1190 - Exploit Public-Facing Application adversary: origin references: - - https://docs.fortinet.com/document/fortiweb/7.4.3/administration-guide/210196/blocking-known-attacks - - https://attack.mitre.org/techniques/T1190/ +- https://docs.fortinet.com/document/fortiweb/7.4.3/administration-guide/210196/blocking-known-attacks +- https://attack.mitre.org/techniques/T1190/ description: | - Detects Server-Side Request Forgery (SSRF) attacks identified by FortiWeb. SSRF attacks force the server to make requests to internal resources, potentially accessing cloud metadata services, internal APIs, or other restricted endpoints. + Detects an explicit SSRF classification or a vendor message identifying server-side request forgery. A localhost URL or internal address alone is insufficient. - Next Steps: - 1. Review the SSRF payload to identify the target internal resource - 2. Check if cloud metadata endpoints (169.254.169.254) were targeted - 3. Verify the application is not vulnerable to SSRF - 4. Review application code for user-controlled URL parameters - 5. Implement URL allowlisting for server-side requests - 6. Check for any successful SSRF exploitation in application logs + Both Alert (log-only) and explicit blocking decisions are covered. Check log.action and actionResult to distinguish detection from prevention; neither establishes successful exploitation. Review log.subtype, log.attacktype, log.signatureid and log.msg, then validate the request and outcome in application logs before escalating. where: | - oneOf("action", ["deny", "block", "alert_deny", "alert"]) && - (regexMatch("log.msg", "(?i)(ssrf|server.*side.*request.*forg|169\\.254\\.169\\.254|metadata.*api|internal.*service|localhost|127\\.0\\.0\\.1.*redirect|0x7f000001|file:///|gopher://)") || - contains("log.attack_type", "ssrf") || - contains("log.signature", "SSRF")) -afterEvents: - - indexPattern: v11-log-firewall-fortiweb-* - with: - - field: origin.ip - operator: filter_term - value: '{{.origin.ip}}' - within: 15m - count: 3 + exists("origin.ip") && exists("target.ip") && equalsIgnoreCase("log.type", "attack") && + regexMatch("log.action", "(?i)^(alert|alert_deny|deny|denied|block|blocked|period_block)$") && + ((regexMatch("log.subtype", "(?i)^(SSRF|Server.Side Request Forgery)$") || regexMatch("log.attacktype", "(?i)^(SSRF|Server.Side Request Forgery)$") || regexMatch("log.signaturesubclass", "(?i)^(SSRF|Server.Side Request Forgery)$")) || ((equalsIgnoreCase("log.maintype", "Signature Detection") || (!exists("log.subtype") && !exists("log.attacktype"))) && regexMatch("log.msg", "(?i)\\b(ssrf|server[ _-]*side[ _-]*request[ _-]*forgery)\\b"))) groupBy: - - adversary.ip - - target.ip +- adversary.ip +- target.ip diff --git a/rules/fortinet/fortiweb/fortiweb_webshell_upload.yml b/rules/fortinet/fortiweb/fortiweb_webshell_upload.yml index e8e1a9bcc..7371d0a38 100644 --- a/rules/fortinet/fortiweb/fortiweb_webshell_upload.yml +++ b/rules/fortinet/fortiweb/fortiweb_webshell_upload.yml @@ -1,33 +1,26 @@ -# Rule version v1.0.0 +# Rule version v1.1.0 dataTypes: - - firewall-fortiweb +- firewall-fortiweb name: FortiWeb Web Shell Upload Detection impact: confidentiality: 3 integrity: 3 availability: 3 category: Persistence -technique: "T1505.003 - Server Software Component: Web Shell" +technique: 'T1505.003 - Server Software Component: Web Shell' adversary: origin references: - - https://docs.fortinet.com/document/fortiweb/7.4.3/administration-guide/210196/blocking-known-attacks - - https://attack.mitre.org/techniques/T1505/003/ +- https://docs.fortinet.com/document/fortiweb/7.4.3/administration-guide/210196/blocking-known-attacks +- https://attack.mitre.org/techniques/T1505/003/ description: | - Detects web shell upload attempts blocked or detected by FortiWeb. Web shells provide persistent remote access and command execution on compromised web servers. + Detects an explicit web-shell classification or named web-shell evidence in a FortiWeb attack message. Generic upload restrictions and script file extensions alone are insufficient. - Next Steps: - 1. Verify if the upload attempt was blocked or if it succeeded - 2. Review the uploaded file name and extension for known web shell patterns - 3. Scan the web server for existing web shells that may have been uploaded earlier - 4. Check the upload endpoint for proper file validation controls - 5. Review the source IP for additional attack attempts - 6. If a web shell was successfully uploaded, immediately isolate the server + Both Alert (log-only) and explicit blocking decisions are covered. Check log.action and actionResult to distinguish detection from prevention; neither establishes successful exploitation. Review log.subtype, log.attacktype, log.signatureid and log.msg, then validate the request and outcome in application logs before escalating. where: | - oneOf("action", ["deny", "block", "alert_deny", "alert"]) && - (regexMatch("log.msg", "(?i)(web.*shell|backdoor.*upload|cmd.*shell|reverse.*shell|\\.php.*upload|\\.asp.*upload|\\.jsp.*upload|c99|r57|china.*chopper|weevely|b374k)") || - (contains("log.attack_type", "file_upload") && contains("log.msg", ["shell", "backdoor", "malicious"])) || - regexMatch("log.msg", "(?i)(file.*upload.*violat|upload.*restrict|dangerous.*file.*type)")) + exists("origin.ip") && exists("target.ip") && equalsIgnoreCase("log.type", "attack") && + regexMatch("log.action", "(?i)^(alert|alert_deny|deny|denied|block|blocked|period_block)$") && + ((regexMatch("log.subtype", "(?i)^(Web.?Shells?)$") || regexMatch("log.attacktype", "(?i)^(Web.?Shells?)$") || regexMatch("log.signaturesubclass", "(?i)^(Web.?Shells?)$")) || ((equalsIgnoreCase("log.maintype", "Signature Detection") || (!exists("log.subtype") && !exists("log.attacktype"))) && regexMatch("log.msg", "(?i)\\b(web[ _-]*shell|backdoor[ _-]*upload|China[ _-]*Chopper|c99shell|r57shell|weevely|b374k)\\b"))) groupBy: - - adversary.ip - - target.ip +- adversary.ip +- target.ip diff --git a/rules/fortinet/fortiweb/fortiweb_xss_detection.yml b/rules/fortinet/fortiweb/fortiweb_xss_detection.yml new file mode 100644 index 000000000..6e59907bc --- /dev/null +++ b/rules/fortinet/fortiweb/fortiweb_xss_detection.yml @@ -0,0 +1,26 @@ +# Rule version v1.1.0 + +dataTypes: +- firewall-fortiweb +name: FortiWeb Cross Site Scripting Attack Detection +impact: + confidentiality: 3 + integrity: 3 + availability: 2 +category: Web Application Attack +technique: T1190 - Exploit Public-Facing Application +adversary: origin +references: +- https://docs.fortinet.com/document/fortiweb/7.2.2/log-message-reference/445549/attack +- https://attack.mitre.org/techniques/T1190/ +description: | + Detects cross-site scripting classified by FortiWeb, including extended signatures. + + Both Alert (log-only) and explicit blocking decisions are covered. Check log.action and actionResult to distinguish detection from prevention; neither establishes successful exploitation. Review log.subtype, log.attacktype, log.signatureid and log.msg, then validate the request and outcome in application logs before escalating. +where: | + exists("origin.ip") && exists("target.ip") && equalsIgnoreCase("log.type", "attack") && + regexMatch("log.action", "(?i)^(alert|alert_deny|deny|denied|block|blocked|period_block)$") && + ((regexMatch("log.subtype", "(?i)^(Cross Site Scripting(?:\\s*\\(Extended\\))?|XSS)$") || regexMatch("log.attacktype", "(?i)^(Cross Site Scripting(?:\\s*\\(Extended\\))?|XSS)$") || regexMatch("log.signaturesubclass", "(?i)^(Cross Site Scripting(?:\\s*\\(Extended\\))?|XSS)$")) || ((!exists("log.subtype") && !exists("log.attacktype")) && regexMatch("log.msg", "(?i)\\b(cross[ _-]*site[ _-]*scripting|xss)\\b"))) +groupBy: +- adversary.ip +- target.ip diff --git a/rules/fortinet/fortiweb/owasp_top10_violations.yml b/rules/fortinet/fortiweb/owasp_top10_violations.yml index d48a85d47..443bf447e 100644 --- a/rules/fortinet/fortiweb/owasp_top10_violations.yml +++ b/rules/fortinet/fortiweb/owasp_top10_violations.yml @@ -1,42 +1,58 @@ -# Rule version v1.0.0 +# Rule version v1.1.0 dataTypes: - - firewall-fortiweb +- firewall-fortiweb name: FortiWeb OWASP Top 10 Violation Attempts impact: confidentiality: 3 integrity: 3 availability: 2 category: Initial Access -technique: "T1190 - Exploit Public-Facing Application" +technique: T1190 - Exploit Public-Facing Application adversary: origin references: - - https://docs.fortinet.com/document/fortiweb-cloud/23.3.0/user-guide/552829/known-attacks - - https://owasp.org/www-project-top-ten/ - - https://attack.mitre.org/techniques/T1190/ +- https://docs.fortinet.com/document/fortiweb-cloud/23.3.0/user-guide/552829/known-attacks +- https://owasp.org/www-project-top-ten/ +- https://attack.mitre.org/techniques/T1190/ description: | - Detects attempts to exploit OWASP Top 10 vulnerabilities including injection flaws, broken authentication, sensitive data exposure, XXE, broken access control, security misconfiguration, and more. This rule triggers when FortiWeb blocks or alerts on attack attempts that match known OWASP Top 10 vulnerability patterns. + Detects repeated medium-or-higher HTTP protocol violations that FortiWeb assigns to an OWASP category. Low-severity missing Content-Type, duplicate parameters and information disclosures are excluded from this rule. - Next Steps: - 1. Review the blocked request details in FortiWeb logs to understand the attack vector - 2. Check if the source IP has attempted multiple different attack types - 3. Verify if the targeted application has any known vulnerabilities - 4. Consider blocking the source IP if attacks persist - 5. Review application security headers and WAF rules for the targeted resource - 6. Check for any successful requests from the same source IP before the block - 7. Analyze the specific OWASP category and implement additional protections - 8. Review application code for the detected vulnerability type + Both Alert (log-only) and explicit blocking decisions are covered. Check log.action and actionResult to distinguish detection from prevention; neither establishes successful exploitation. Review log.subtype, log.attacktype, log.signatureid and log.msg, then validate the request and outcome in application logs before escalating. where: | - oneOf("action", ["deny", "alert_deny", "block", "blocked"]) && - regexMatch("log.msg", "(?i)(injection|traversal|xxe|xml external entity|broken auth|sensitive data|security misconfig|owasp|sqli|xss|csrf|command injection|path traversal|lfi|rfi|deserialization)") + exists("origin.ip") && exists("target.ip") && equalsIgnoreCase("log.type", "attack") && + regexMatch("log.action", "(?i)^(alert|alert_deny|deny|denied|block|blocked|period_block)$") && + (equalsIgnoreCase("log.maintype", "HTTP Protocol Constraints") && regexMatch("log.owasptop10", "(?i)^A[0-9]{1,2}:") && regexMatch("log.severitylevel", "(?i)^(Medium|High|Critical)$") && regexMatch("log.subtype", "(?i)^(Illegal (URL Parameter Value|Character in URL|HTTP Method)|NULL Character in (URL|Param[e]?ter Value|Parameter Value))$")) && + exists("log.subtype") afterEvents: - - indexPattern: v11-log-firewall-fortiweb-* - with: - - field: origin.ip - operator: filter_term - value: '{{.origin.ip}}' - within: 15m - count: 5 -groupBy: - - adversary.ip - - target.ip +- indexPattern: v11-log-firewall-fortiweb-* + with: + - field: origin.ip + operator: filter_term + value: '{{.origin.ip}}' + - field: target.ip + operator: filter_term + value: '{{.target.ip}}' + - field: log.type + operator: filter_term + value: '{{.log.type}}' + - field: log.subtype + operator: filter_term + value: '{{.log.subtype}}' + - field: log.action + operator: filter_term + value: '{{.log.action}}' + - field: log.maintype + operator: filter_term + value: '{{.log.maintype}}' + - field: log.severitylevel + operator: filter_term + value: '{{.log.severitylevel}}' + - field: log.owasptop10 + operator: filter_term + value: '{{.log.owasptop10}}' + within: 15m + count: 5 +deduplicateBy: +- adversary.ip +- target.ip +- lastEvent.log.subtype diff --git a/rules/fortinet/fortiweb/web_application_attacks_detection.yml b/rules/fortinet/fortiweb/web_application_attacks_detection.yml index d5b42f86c..f27501191 100644 --- a/rules/fortinet/fortiweb/web_application_attacks_detection.yml +++ b/rules/fortinet/fortiweb/web_application_attacks_detection.yml @@ -1,57 +1,48 @@ -# Rule version v1.0.0 +# Rule version v1.1.0 dataTypes: - - firewall-fortiweb +- firewall-fortiweb name: FortiWeb Web Application Attack Detection impact: confidentiality: 3 integrity: 3 availability: 2 category: Initial Access -technique: "T1190 - Exploit Public-Facing Application" +technique: T1190 - Exploit Public-Facing Application adversary: origin references: - - https://docs.fortinet.com/document/fortiweb/7.6.0/administration-guide/981691/syntax-based-sql-xss-injection-detection - - https://attack.mitre.org/techniques/T1190/ +- https://docs.fortinet.com/document/fortiweb/7.6.0/administration-guide/981691/syntax-based-sql-xss-injection-detection +- https://attack.mitre.org/techniques/T1190/ description: | - Detects SQL injection, XSS, and other web application attacks blocked by FortiWeb WAF based on signature or syntax-based detection. These attacks attempt to exploit vulnerabilities in web applications to gain unauthorized access, steal data, or compromise application integrity. + Detects repeated Generic Attacks (including Extended) classifications from FortiWeb against the same application. Dedicated SQLi, XSS Known Exploits and malware rules handle those classes. Routine geographic/IP-reputation blocks are excluded. - Next Steps: - 1. Review the specific attack type and payload in the FortiWeb logs - 2. Check if the source IP has a history of malicious activity - 3. Verify if the targeted application has known vulnerabilities - 4. Review application logs for any successful exploitation attempts - 5. Consider blocking the source IP if attacks persist - 6. Ensure WAF rules are up-to-date and properly configured - 7. Implement additional application-level security controls if needed - 8. Check for any bypass attempts or evasion techniques being used + Both Alert (log-only) and explicit blocking decisions are covered. Check log.action and actionResult to distinguish detection from prevention; neither establishes successful exploitation. Review log.subtype, log.attacktype, log.signatureid and log.msg, then validate the request and outcome in application logs before escalating. where: | - equals("action", "deny") && - ( - equals("log.type", "attack") || - equals("log.subtype", "waf") || - oneOf("log.attack_type", ["SQL Injection", "Cross Site Scripting", "XSS", "SQLi"]) || - contains("log.message", "sql injection") || - contains("log.message", "SQL injection") || - contains("log.message", "cross site scripting") || - contains("log.message", "Cross Site Scripting") || - contains("log.message", "xss") || - contains("log.message", "XSS") || - contains("log.message", "sqli") || - contains("log.message", "SQLi") || - equals("log.main_type", "SQL/XSS Syntax Based Detection") - ) + exists("origin.ip") && exists("target.ip") && equalsIgnoreCase("log.type", "attack") && + regexMatch("log.action", "(?i)^(alert|alert_deny|deny|denied|block|blocked|period_block)$") && + regexMatch("log.subtype", "(?i)^Generic Attacks(?:\\s*\\(Extended\\))?$") && + exists("log.subtype") afterEvents: - - indexPattern: v11-log-firewall-fortiweb-* - with: - - field: origin.ip - operator: filter_term - value: '{{.origin.ip}}' - - field: action - operator: filter_term - value: 'deny' - within: 15m - count: 3 -groupBy: - - adversary.ip - - target.ip +- indexPattern: v11-log-firewall-fortiweb-* + with: + - field: origin.ip + operator: filter_term + value: '{{.origin.ip}}' + - field: target.ip + operator: filter_term + value: '{{.target.ip}}' + - field: log.type + operator: filter_term + value: '{{.log.type}}' + - field: log.subtype + operator: filter_term + value: '{{.log.subtype}}' + - field: log.action + operator: filter_term + value: '{{.log.action}}' + within: 15m + count: 3 +deduplicateBy: +- adversary.ip +- target.ip +- lastEvent.log.subtype From f67ef3c2e16ca4eaf6d89c936d0d582b88474c1e Mon Sep 17 00:00:00 2001 From: Ricardo Valdes Date: Thu, 17 Sep 2026 14:58:05 -0400 Subject: [PATCH 3/3] fix(fortiweb): validate vendor IPs and preserve HTTP metadata --- filters/audits/fortiweb.md | 116 +++- filters/fortinet/fortiweb.yml | 36 +- plugins/alerts/fortiweb_contract_test.go | 143 ++++- plugins/alerts/fortiweb_history_test.go | 249 ++++++++ .../testdata/filter-contracts/fortiweb.json | 28 +- plugins/alerts/testdata/fortiweb_raw.json | 531 +++++++++++++++--- 6 files changed, 961 insertions(+), 142 deletions(-) create mode 100644 plugins/alerts/fortiweb_history_test.go diff --git a/filters/audits/fortiweb.md b/filters/audits/fortiweb.md index e64767674..f1c71f6ae 100644 --- a/filters/audits/fortiweb.md +++ b/filters/audits/fortiweb.md @@ -39,14 +39,15 @@ classifications. The existing sanitized `log.*` names remain available. | --- | --- | | `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 `log.unparsed*Ip`, removed before geolocation | +| 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_url`, `HTTP_host` | Relative URL goes to `target.path`; full HTTP(S) URL to `target.url`; Host header stays in `log.httphost` | +| `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 @@ -64,28 +65,99 @@ 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 -- Read the filter/rule wiki and SDK v1.1.31 implementation; checked standard fields - against the supplied dictionaries and SDK schema. The wiki's lowercase protocol - convention resolves the older dictionary's conflicting uppercase recommendation. -- `go test ./...` in `plugins/alerts` runs the standalone FortiWeb tests: **74 synthetic - raw-log cases, each evaluated against all 10 rule predicates**, plus history and - alert-side grouping contracts. Strict SDK YAML decoding, real CEL evaluation and - final SDK Event conversion are exercised. -- With the shared runner from PR #2590 temporarily applied, all **96 subtests** pass. - The technology manifest supplies an additional invalid-address normalization case. -- Read-only category aggregates were projected through the new filter/rule contract - to check coverage of retained classifications. These are predicate candidates, - not observed alerts or a full historical replay. -- `git diff --check` passes. - -The raw contract harness models documented grok concatenation and transforms with -Go RE2. It deliberately omits KV, dynamic plugins and the closed EventProcessor; -all asserted/detection fields must therefore come from the quote-aware extraction. -It does not execute live OpenSearch correlation, deduplication or alert publication. -The standalone tests need no shared runner. Review shared alert grouping fix #2590 -before rollout, since it affects actual grouping/deduplication behavior. +- 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 diff --git a/filters/fortinet/fortiweb.yml b/filters/fortinet/fortiweb.yml index 21272c40b..f8fc2be5a 100644 --- a/filters/fortinet/fortiweb.yml +++ b/filters/fortinet/fortiweb.yml @@ -1,4 +1,4 @@ -# FortiWeb filter, version 2.1.0 +# 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 @@ -53,6 +53,7 @@ pipeline: - log.httpurl - log.httphost - log.httpmethod + - log.httpagent - log.owasptop10 - log.owaspapitop10 - log.triggerpolicy @@ -232,6 +233,15 @@ pipeline: 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: @@ -272,6 +282,7 @@ pipeline: - log.httpurl - log.httphost - log.httpmethod + - log.httpagent - log.owasptop10 - log.owaspapitop10 - log.triggerpolicy @@ -297,6 +308,7 @@ pipeline: - log.httpurl - log.httphost - log.httpmethod + - log.httpagent - log.owasptop10 - log.owaspapitop10 - log.triggerpolicy @@ -312,10 +324,13 @@ pipeline: 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 @@ -396,12 +411,7 @@ pipeline: key: severity value: debug where: regexMatch("log.severitylevel", "(?i)^(debug)$") - - grok: - source: log.httpurl - patterns: - - fieldName: target.path - pattern: '{{.greedy}}' - where: startsWith("log.httpurl", "/") + # Relative request paths stay in log.httpurl; SDK path is a file directory. - grok: source: log.httpurl patterns: @@ -414,12 +424,6 @@ pipeline: to: log.unparsedOriginPort where: exists("origin.port") && !(regexMatch("origin.port", "^[0-9]{1,5}$") && lessOrEqual("origin.port", 65535)) - - rename: - from: - - origin.ip - to: log.unparsedOriginIp - where: exists("origin.ip") && (!(inCIDR("origin.ip", "0.0.0.0/0") || inCIDR("origin.ip", "::/0")) || oneOf("origin.ip", - ["0.0.0.0", "::"])) - dynamic: plugin: com.utmstack.geolocation params: @@ -432,12 +436,6 @@ pipeline: to: log.unparsedTargetPort where: exists("target.port") && !(regexMatch("target.port", "^[0-9]{1,5}$") && lessOrEqual("target.port", 65535)) - - rename: - from: - - target.ip - to: log.unparsedTargetIp - where: exists("target.ip") && (!(inCIDR("target.ip", "0.0.0.0/0") || inCIDR("target.ip", "::/0")) || oneOf("target.ip", - ["0.0.0.0", "::"])) - dynamic: plugin: com.utmstack.geolocation params: diff --git a/plugins/alerts/fortiweb_contract_test.go b/plugins/alerts/fortiweb_contract_test.go index 5a87d5a4c..dfd58e0e8 100644 --- a/plugins/alerts/fortiweb_contract_test.go +++ b/plugins/alerts/fortiweb_contract_test.go @@ -3,12 +3,14 @@ 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 is omitted deliberately: every asserted/detection field must be recovered -// by the quote-aware grok steps. Dynamic enrichment and live correlation are not run. +// 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" @@ -105,9 +107,21 @@ func fwRegex(t *testing.T, g *plugins.Grok, cfg *plugins.Config) *regexp.Regexp 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", "log": map[string]any{}} + 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 { @@ -180,12 +194,39 @@ func fwParse(t *testing.T, cfg *plugins.Config, raw string, cache *plugins.CELCa } } 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", "dynamic": // Intentional exclusions described above. + 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) } @@ -207,6 +248,92 @@ func fwParse(t *testing.T, cfg *plugins.Config, raw string, cache *plugins.CELCa } 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") @@ -274,8 +401,12 @@ func TestFortiWebRawContracts(t *testing.T) { if got { for _, search := range r.Correlation { for _, term := range search.With { - if !gjson.Get(out, term.Field).Exists() { - t.Errorf("%s: matched event lacks history field %s", name, term.Field) + 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) + } } } } 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 index 92d04fce6..c236bf231 100644 --- a/plugins/alerts/testdata/filter-contracts/fortiweb.json +++ b/plugins/alerts/testdata/filter-contracts/fortiweb.json @@ -17,26 +17,30 @@ ], "fixtures": [ { - "name": "fortiweb_non_ip_is_not_enriched", + "name": "FortiWeb absent extracted identity negative control (not raw parsing or IP validation)", "filter": "fortinet/fortiweb.yml", "input": { - "origin": { - "ip": "not-an-ip" - }, - "target": { - "ip": "0.0.0.0" - } - }, - "expected": { - "log.unparsedOriginIp": "not-an-ip", - "log.unparsedTargetIp": "0.0.0.0" + "dataType": "firewall-fortiweb", + "log": {} }, + "expected": {}, "absent": [ "origin.ip", "target.ip", "actionResult" ], - "rules": {} + "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 index 8e1441cfb..b46565ccf 100644 --- a/plugins/alerts/testdata/fortiweb_raw.json +++ b/plugins/alerts/testdata/fortiweb_raw.json @@ -14,12 +14,13 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", - "log.httphost": "app.example:443" + "log.httphost": "app.example:443", + "log.httpurl": "/login?q=a=b" }, "absent": [ "target.url", - "actionResult" + "actionResult", + "target.path" ], "matches": [ "fortiweb_sqli_detection" @@ -40,12 +41,13 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", "log.httphost": "app.example:443", - "actionResult": "denied" + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" }, "absent": [ - "target.url" + "target.url", + "target.path" ], "matches": [ "fortiweb_sqli_detection" @@ -66,12 +68,13 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", - "log.httphost": "app.example:443" + "log.httphost": "app.example:443", + "log.httpurl": "/login?q=a=b" }, "absent": [ "target.url", - "actionResult" + "actionResult", + "target.path" ], "matches": [ "fortiweb_sqli_detection" @@ -92,12 +95,13 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", "log.httphost": "app.example:443", - "actionResult": "denied" + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" }, "absent": [ - "target.url" + "target.url", + "target.path" ], "matches": [ "fortiweb_sqli_detection" @@ -118,12 +122,13 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", - "log.httphost": "app.example:443" + "log.httphost": "app.example:443", + "log.httpurl": "/login?q=a=b" }, "absent": [ "target.url", - "actionResult" + "actionResult", + "target.path" ], "matches": [ "fortiweb_xss_detection" @@ -144,12 +149,13 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", "log.httphost": "app.example:443", - "actionResult": "denied" + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" }, "absent": [ - "target.url" + "target.url", + "target.path" ], "matches": [ "fortiweb_xss_detection" @@ -170,12 +176,13 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", - "log.httphost": "app.example:443" + "log.httphost": "app.example:443", + "log.httpurl": "/login?q=a=b" }, "absent": [ "target.url", - "actionResult" + "actionResult", + "target.path" ], "matches": [ "fortiweb_xss_detection" @@ -196,12 +203,13 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", "log.httphost": "app.example:443", - "actionResult": "denied" + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" }, "absent": [ - "target.url" + "target.url", + "target.path" ], "matches": [ "fortiweb_xss_detection" @@ -222,12 +230,13 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", - "log.httphost": "app.example:443" + "log.httphost": "app.example:443", + "log.httpurl": "/login?q=a=b" }, "absent": [ "target.url", - "actionResult" + "actionResult", + "target.path" ], "matches": [ "fortiweb_known_exploits" @@ -248,12 +257,13 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", "log.httphost": "app.example:443", - "actionResult": "denied" + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" }, "absent": [ - "target.url" + "target.url", + "target.path" ], "matches": [ "fortiweb_known_exploits" @@ -274,12 +284,13 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", - "log.httphost": "app.example:443" + "log.httphost": "app.example:443", + "log.httpurl": "/login?q=a=b" }, "absent": [ "target.url", - "actionResult" + "actionResult", + "target.path" ], "matches": [ "web_application_attacks_detection" @@ -300,12 +311,13 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", "log.httphost": "app.example:443", - "actionResult": "denied" + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" }, "absent": [ - "target.url" + "target.url", + "target.path" ], "matches": [ "web_application_attacks_detection" @@ -326,12 +338,13 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", - "log.httphost": "app.example:443" + "log.httphost": "app.example:443", + "log.httpurl": "/login?q=a=b" }, "absent": [ "target.url", - "actionResult" + "actionResult", + "target.path" ], "matches": [ "web_application_attacks_detection" @@ -352,12 +365,13 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", "log.httphost": "app.example:443", - "actionResult": "denied" + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" }, "absent": [ - "target.url" + "target.url", + "target.path" ], "matches": [ "web_application_attacks_detection" @@ -378,12 +392,13 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", - "log.httphost": "app.example:443" + "log.httphost": "app.example:443", + "log.httpurl": "/login?q=a=b" }, "absent": [ "target.url", - "actionResult" + "actionResult", + "target.path" ], "matches": [ "fortiweb_malware_detection" @@ -404,12 +419,13 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", "log.httphost": "app.example:443", - "actionResult": "denied" + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" }, "absent": [ - "target.url" + "target.url", + "target.path" ], "matches": [ "fortiweb_malware_detection" @@ -813,10 +829,11 @@ "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.unparsedOriginIp": "0.0.0.0" + "log.src": "0.0.0.0" }, "absent": [ - "origin.ip" + "origin.ip", + "log.unparsedOriginIp" ], "matches": [] }, @@ -824,10 +841,11 @@ "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.unparsedOriginIp": "::" + "log.src": "::" }, "absent": [ - "origin.ip" + "origin.ip", + "log.unparsedOriginIp" ], "matches": [] }, @@ -835,10 +853,11 @@ "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.unparsedOriginIp": "not-an-ip" + "log.src": "not-an-ip" }, "absent": [ - "origin.ip" + "origin.ip", + "log.unparsedOriginIp" ], "matches": [] }, @@ -846,10 +865,11 @@ "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.unparsedTargetIp": "not-an-ip" + "log.dst": "not-an-ip" }, "absent": [ - "target.ip" + "target.ip", + "log.unparsedTargetIp" ], "matches": [] }, @@ -953,14 +973,15 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", "log.httphost": "app.example:443", "actionResult": "denied", - "log.unparsedTargetPort": "https" + "log.unparsedTargetPort": "https", + "log.httpurl": "/login?q=a=b" }, "absent": [ "target.url", - "target.port" + "target.port", + "target.path" ], "matches": [ "fortiweb_sqli_detection" @@ -980,14 +1001,15 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", "log.httphost": "app.example:443", "actionResult": "denied", - "log.unparsedTargetPort": "-1" + "log.unparsedTargetPort": "-1", + "log.httpurl": "/login?q=a=b" }, "absent": [ "target.url", - "target.port" + "target.port", + "target.path" ], "matches": [ "fortiweb_sqli_detection" @@ -1007,14 +1029,15 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", "log.httphost": "app.example:443", "actionResult": "denied", - "log.unparsedTargetPort": "65536" + "log.unparsedTargetPort": "65536", + "log.httpurl": "/login?q=a=b" }, "absent": [ "target.url", - "target.port" + "target.port", + "target.path" ], "matches": [ "fortiweb_sqli_detection" @@ -1034,14 +1057,15 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", "log.httphost": "app.example:443", "actionResult": "denied", - "log.unparsedTargetPort": "1.5" + "log.unparsedTargetPort": "1.5", + "log.httpurl": "/login?q=a=b" }, "absent": [ "target.url", - "target.port" + "target.port", + "target.path" ], "matches": [ "fortiweb_sqli_detection" @@ -1061,14 +1085,15 @@ "protocol": "tcp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", "log.httphost": "app.example:443", "actionResult": "denied", - "log.unparsedTargetPort": "999999999999" + "log.unparsedTargetPort": "999999999999", + "log.httpurl": "/login?q=a=b" }, "absent": [ "target.url", - "target.port" + "target.port", + "target.path" ], "matches": [ "fortiweb_sqli_detection" @@ -1089,12 +1114,13 @@ "protocol": "udp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", "log.httphost": "app.example:443", - "actionResult": "denied" + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" }, "absent": [ - "target.url" + "target.url", + "target.path" ], "matches": [ "fortiweb_sqli_detection" @@ -1115,12 +1141,13 @@ "protocol": "udp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", "log.httphost": "app.example:443", - "actionResult": "denied" + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" }, "absent": [ - "target.url" + "target.url", + "target.path" ], "matches": [ "fortiweb_sqli_detection" @@ -1141,12 +1168,13 @@ "protocol": "icmp", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", "log.httphost": "app.example:443", - "actionResult": "denied" + "actionResult": "denied", + "log.httpurl": "/login?q=a=b" }, "absent": [ - "target.url" + "target.url", + "target.path" ], "matches": [ "fortiweb_sqli_detection" @@ -1167,15 +1195,352 @@ "protocol": "icmpv6", "severity": "error", "log.msg": "[policy_name=example] : request matched", - "target.path": "/login?q=a=b", "log.httphost": "app.example:443", - "actionResult": "denied" + "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": [ - "target.url" + "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=\"