From 3bc288225c86a681baf6bcba8024ce61b433fee7 Mon Sep 17 00:00:00 2001 From: Max Leske <250711+theseion@users.noreply.github.com> Date: Sun, 14 Jul 2024 20:55:42 +0200 Subject: [PATCH] feat: add support for Albedo response reflection --- spec/v2.1.0/ftw.md | 2220 +++++++++++++++++ .../waf-platform-overrides-schema-v2.1.0.json | 1 + spec/v2.1.0/waf-tests-schema-v2.1.0.json | 1 + types/examples.go | 14 +- types/types.go | 52 +- types/types_test.go | 38 +- 6 files changed, 2315 insertions(+), 11 deletions(-) create mode 100644 spec/v2.1.0/ftw.md create mode 100755 spec/v2.1.0/waf-platform-overrides-schema-v2.1.0.json create mode 100755 spec/v2.1.0/waf-tests-schema-v2.1.0.json diff --git a/spec/v2.1.0/ftw.md b/spec/v2.1.0/ftw.md new file mode 100644 index 0000000..687abe1 --- /dev/null +++ b/spec/v2.1.0/ftw.md @@ -0,0 +1,2220 @@ +## FTWTest +Welcome to the FTW YAMLFormat documentation. + In this document we will explain all the possible options that can be used within the YAML format. + Generally this is the preferred format for writing tests in as they don't require any programming skills + in order to understand and change. If you find a bug in this format please open an issue. + + + FTWTest is the base type used when unmarshaling YAML tests files + + + + + + +
+ +
+ +meta FTWTestMeta + +
+
+ +Meta describes the metadata information of this yaml test file + +
+ +
+ +
+ +rule_id uint + +
+
+ +RuleId is the ID of the rule this test targets. + + + +Examples: + + +```yaml +# RuleId +rule_id: 123456 +``` + + +
+ +
+ +
+ +tests []Test + +
+
+ +Tests is a list of FTW tests + + + +Examples: + + +```yaml +tests: + - test_title: 123456-1 + ruleid: 0 + test_id: 0 + desc: Unix RCE using `time` + stages: + - description: Get cookie from server + input: + dest_addr: 192.168.0.1 + port: 8080 + protocol: http + uri: /test + version: HTTP/1.1 + method: REPORT + headers: + Accept: '*/*' + Host: localhost + User-Agent: CRS Tests + save_cookie: false + stop_magic: true + autocomplete_headers: false + encoded_request: TXkgRGF0YQo= + output: + status: 200 + response_contains: HTTP/1.1 + log_contains: nothing + no_log_contains: everything + log: + expect_ids: + - 123456 + no_expect_ids: + - 123456 + match_regex: id[:\s"]*123456 + no_match_regex: id[:\s"]*123456 + expect_error: true +``` + + +
+ +
+ + + + + +## FTWTestMeta + +Appears in: + + +- FTWTest.meta + + + + + +
+ +
+ +author string + +
+
+ +Author is the list of authors that added content to this file + + + +Examples: + + +```yaml +# Author +author: Felipe Zipitria +``` + + +
+ +
+ +
+ +enabled bool + +
+
+ +Enabled indicates if the tests are enabled to be run by the engine or not. + + + +Examples: + + +```yaml +# Enabled +enabled: false +``` + + +
+ +
+ +
+ +name string + +
+
+ +Name is the name of the tests contained in this file. + + + +Examples: + + +```yaml +# Name +name: test01 +``` + + +
+ +
+ +
+ +description string + +
+
+ +Description is a textual description of the tests contained in this file. + + + +Examples: + + +```yaml +# Description +description: The tests here target SQL injection. +``` + + +
+ +
+ +
+ +version string + +
+
+ +Version is the version of the YAML Schema. + + + +Examples: + + +```yaml +# Version +version: v1 +``` + + +
+ +
+ +
+ +tags []string + +
+
+ +description: | + Tags is list of strings that can be used for arbitrary grouping of tests. + examples: + - name: Tags + value: ["PHP", "bug-123"] + +
+ +
+ + + + + +## Test + +Appears in: + + +- FTWTest.tests + + +```yaml +- test_title: 123456-1 + ruleid: 0 + test_id: 0 + desc: Unix RCE using `time` + stages: + - description: Get cookie from server + input: + dest_addr: 192.168.0.1 + port: 8080 + protocol: http + uri: /test + version: HTTP/1.1 + method: REPORT + headers: + Accept: '*/*' + Host: localhost + User-Agent: CRS Tests + save_cookie: false + stop_magic: true + autocomplete_headers: false + encoded_request: TXkgRGF0YQo= + output: + status: 200 + response_contains: HTTP/1.1 + log_contains: nothing + no_log_contains: everything + log: + expect_ids: + - 123456 + no_expect_ids: + - 123456 + match_regex: id[:\s"]*123456 + no_match_regex: id[:\s"]*123456 + expect_error: true +``` + + + +
+ +
+ +test_title string + +
+
+ +TestTitle is the title of this particular types. It is used for inclusion/exclusion of each run by the tool. + + + +Examples: + + +```yaml +test_title: 123456-1 +``` + + +
+ +
+ +
+ +test_id uint + +
+
+ +TestId is the ID of the test, in relation to `rule_id`. +When this field is not set, the ID will be inferred from the +position. + + + +Examples: + + +```yaml +# TestId +test_id: 4 +``` + + +
+ +
+ +
+ +desc string + +
+
+ +TestDescription is the description for this particular test. +Should be used to describe the internals of the specific things this test is targeting. + + + +Examples: + + +```yaml +desc: Unix RCE using `time` +``` + + +
+ +
+ +
+ +stages []Stage + +
+
+ +Stages is the list of all the stages to perform this test. + + + +Examples: + + +```yaml +stages: + - description: Get cookie from server + input: + dest_addr: 192.168.0.1 + port: 8080 + protocol: http + uri: /test + version: HTTP/1.1 + method: REPORT + headers: + Accept: '*/*' + Host: localhost + User-Agent: CRS Tests + save_cookie: false + stop_magic: true + autocomplete_headers: false + encoded_request: TXkgRGF0YQo= + output: + status: 200 + response_contains: HTTP/1.1 + log_contains: nothing + no_log_contains: everything + log: + expect_ids: + - 123456 + no_expect_ids: + - 123456 + match_regex: id[:\s"]*123456 + no_match_regex: id[:\s"]*123456 + expect_error: true +``` + + +
+ +
+ +
+ +tags []string + +
+
+ +description: | + Tags is list of strings that can be used for arbitrary grouping of tests. + examples: + - name: Tags + value: ["PHP", "bug-123"] + +
+ +
+ + + + + +## Stage + +Appears in: + + +- Test.stages + + +```yaml +- description: Get cookie from server + input: + dest_addr: 192.168.0.1 + port: 8080 + protocol: http + uri: /test + version: HTTP/1.1 + method: REPORT + headers: + Accept: '*/*' + Host: localhost + User-Agent: CRS Tests + save_cookie: false + stop_magic: true + autocomplete_headers: false + encoded_request: TXkgRGF0YQo= + output: + status: 200 + response_contains: HTTP/1.1 + log_contains: nothing + no_log_contains: everything + log: + expect_ids: + - 123456 + no_expect_ids: + - 123456 + match_regex: id[:\s"]*123456 + no_match_regex: id[:\s"]*123456 + expect_error: true +``` + + + +
+ +
+ +description string + +
+
+ +Describes the purpose of this stage. + + + +Examples: + + +```yaml +description: Get cookie from server +``` + + +
+ +
+ +
+ +input Input + +
+
+ +Input is the data that is passed to the test + + + +Examples: + + +```yaml +# Input +input: + dest_addr: 192.168.0.1 + port: 8080 + protocol: http + uri: /test + version: HTTP/1.1 + method: REPORT + headers: + Accept: '*/*' + Host: localhost + User-Agent: CRS Tests + save_cookie: false + stop_magic: true + autocomplete_headers: false + encoded_request: TXkgRGF0YQo= +``` + + +
+ +
+ +
+ +output Output + +
+
+ +Output is the data that is returned from the test + + + +Examples: + + +```yaml +# Output +output: + status: 200 + response_contains: HTTP/1.1 + log_contains: nothing + no_log_contains: everything + log: + expect_ids: + - 123456 + no_expect_ids: + - 123456 + match_regex: id[:\s"]*123456 + no_match_regex: id[:\s"]*123456 + expect_error: true +``` + + +
+ +
+ + + + + +## Input + +Appears in: + + +- Stage.input + + +```yaml +# Input +dest_addr: 192.168.0.1 +port: 8080 +protocol: http +uri: /test +version: HTTP/1.1 +method: REPORT +headers: + Accept: '*/*' + Host: localhost + User-Agent: CRS Tests +save_cookie: false +stop_magic: true +autocomplete_headers: false +encoded_request: TXkgRGF0YQo= +``` + + + +
+ +
+ +dest_addr string + +
+
+ +DestAddr is the IP of the destination host that the test will send the message to. + + + +Examples: + + +```yaml +# DestAddr +dest_addr: 127.0.0.1 +``` + + +
+ +
+ +
+ +port int + +
+
+ +Port allows you to declare which port on the destination host the test should connect to. + + + +Examples: + + +```yaml +# Port +port: 80 +``` + + +
+ +
+ +
+ +protocol string + +
+
+ +Protocol allows you to declare which protocol the test should use when sending the request. + + + +Examples: + + +```yaml +# Protocol +protocol: http +``` + + +
+ +
+ +
+ +uri string + +
+
+ +URI allows you to declare the URI the test should use as part of the request line. + + + +Examples: + + +```yaml +# URI +uri: /get?hello=world +``` + + +
+ +
+ +
+ +follow_redirect bool + +
+
+ +FollowRedirect will expect the previous stage of the same test to have received a +redirect response, it will fail the test otherwise. The redirect location will be used +to send the request for the current stage and any settings for port, protocol, address, +or URI will be ignored. + + + +Examples: + + +```yaml +# follow_redirect +follow_redirect: true +``` + + +
+ +
+ +
+ +version string + +
+
+ +Version allows you to declare the HTTP version the test should use as part of the request line. + + + +Examples: + + +```yaml +# Version +version: "1.1" +``` + + +
+ +
+ +
+ +method string + +
+
+ +Method allows you to declare the HTTP method the test should use as part of the request line. + + + +Examples: + + +```yaml +# Method +method: GET +``` + + +
+ +
+ +
+ +headers map[string]string + +
+
+ +Headers allows you to declare headers that the test should send. + + + +Examples: + + +```yaml +# Headers +headers: + Accept: '*/*' + Host: localhost + User-Agent: CRS Tests +``` + + +
+ +
+ +
+ +data string + +
+
+ +Data allows you to declare the payload that the test should in the request body. + + + +Examples: + + +```yaml +# Data +data: Bibitti bopi +``` + + +
+ +
+ +
+ +encoded_data string + +
+
+ +EncodedData allows you to declare the payload as a base64 encoded string, which +will be decoded into bytes and sent verbatimt to the server. This allows for complex +payloads that include invisible characters or invalid Unicode byte sequences. + + + +Examples: + + +```yaml +# encoded_data +encoded_data: c29tZXRoaW5nIHdpdGgKbmV3bGluZQo= +``` + + +
+ +
+ +
+ +save_cookie bool + +
+
+ +SaveCookie allows you to automatically provide cookies if there are multiple stages and save cookie is set + + + +Examples: + + +```yaml +# SaveCookie +save_cookie: 80 +``` + + +
+ +
+ +
+ +stop_magic bool + +
+
+ +StopMagic is deprecated. + + + +Examples: + + +```yaml +# StopMagic +stop_magic: false +``` + + +
+ +
+ +
+ +autocomplete_headers bool + +
+
+ +AutocompleteHeaders allows the test framework to automatically fill the request with Content-Type and Connection headers. +Defaults to true. + + + +Examples: + + +```yaml +# StopMagic +autocomplete_headers: false +``` + + +
+ +
+ +
+ +encoded_request string + +
+
+ +EncodedRequest will take a base64 encoded string that will be decoded and sent through as the request. +It will override all other settings + + + +Examples: + + +```yaml +# EncodedRequest +encoded_request: a +``` + + +
+ +
+ +
+ +raw_request string + +
+
+ +RAWRequest is deprecated. + + + +Examples: + + +```yaml +# RAWRequest +raw_request: TXkgRGF0YQo= +``` + + +
+ +
+ +
+ +response Response + +
+
+ +description: | + Response describes a response from the web server that a WAF is expected to analyse. + Note: This functionality requires a backend that can send the specified request to the + reverse proxy. Currently, only Albedo (https://github.com/coreruleset/albedo) is supported. + example: + - name Response + value: ExampleResponse + +
+ +
+ + + + + +## Response + +Appears in: + + +- Input.response + + + + + +
+ +
+ +headers map[string]string + +
+
+ +Headers defines the headers the response will carry. + + + +Examples: + + +```yaml +# Headers +headers: + Accept: '*/*' + Host: localhost + User-Agent: CRS Tests +``` + + +
+ +
+ +
+ +status int + +
+
+ +Status describes the HTTP status code of the response. Defaults to `200` if omitted. + + + +Examples: + + +```yaml +# Status +status: 302 +``` + + +
+ +
+ +
+ +body string + +
+
+ +Body defines the body of the response as a plain string. + + + +Examples: + + +```yaml +# Body +body: | + {"aJsonDocument": ["in the response"]} +``` + + +
+ +
+ +
+ +encoded_body string + +
+
+ +EncodedBody defines the body of the response as a base64 encoded string. This is useful if the response +needs to contain non-printable characters. + + + +Examples: + + +```yaml +# EncodedBody +encoded_body: eyJhSnNvbkRvY3VtZW50IjogWyJpbiB0aGUgcmVzcG9uc2UiXX0= +``` + + +
+ +
+ +
+ +log_message string + +
+
+ +LogMessage specifies a message to be printed in the log of the backend server that sends the response. +This can be helpful when debugging, to match resopnses sent by the backend to test executions. + + + +Examples: + + +```yaml +# LogMessage +log_message: Response splitting test 1 +``` + + +
+ +
+ + + + + +## Output + +Appears in: + + +- Stage.output + + +```yaml +# Output +status: 200 +response_contains: HTTP/1.1 +log_contains: nothing +no_log_contains: everything +log: + expect_ids: + - 123456 + no_expect_ids: + - 123456 + match_regex: id[:\s"]*123456 + no_match_regex: id[:\s"]*123456 +expect_error: true +``` + + + +
+ +
+ +status int + +
+
+ +Status describes the HTTP status code expected in the response. + + + +Examples: + + +```yaml +# Status +status: 200 +``` + + +
+ +
+ +
+ +response_contains string + +
+
+ +ResponseContains describes the text that should be contained in the HTTP response. + + + +Examples: + + +```yaml +# ResponseContains +response_contains: Hello, World +``` + + +
+ +
+ +
+ +log_contains string + +
+
+ +LogContains describes the text that should be contained in the WAF logs. + + + +Examples: + + +```yaml +# LogContains +log_contains: id 920100 +``` + + +
+ +
+ +
+ +no_log_contains string + +
+
+ +NoLogContains describes the text that should not be contained in the WAF logs. + + + +Examples: + + +```yaml +# NoLogContains +no_log_contains: id 920100 +``` + + +
+ +
+ +
+ +log Log + +
+
+ +Log is used to configure expectations about the log contents. + + + +Examples: + + +```yaml +log: + expect_ids: + - 123456 + no_expect_ids: + - 123456 + match_regex: id[:\s"]*123456 + no_match_regex: id[:\s"]*123456 +``` + + +
+ +
+ +
+ +expect_error bool + +
+
+ +When `ExpectError` is true, we don't expect an answer from the WAF, just an error. + + + +Examples: + + +```yaml +# ExpectError +expect_error: false +``` + + +
+ +
+ +
+ +retry_once bool + +
+
+ +When `RetryOnce` is true, the test run will be retried once upon failures. This options +primary purpose is to work around a race condition in phase 5, where the log entry for +a phase 5 rule may appear after the end marker of the previous test. + +
+ +
+ +
+ +isolated bool + +
+
+ +Isolated specifies that the test is expected to trigger a single rule only. +If the rule triggers any other rule than the (single) one specified in +expect_ids, the test fill be considered a failure. +Default: false + + + +Examples: + + +```yaml +# Isolated +isolated: true +``` + + +
+ +
+ + + + + +## Log + +Appears in: + + +- Output.log + + +```yaml +expect_ids: + - 123456 +no_expect_ids: + - 123456 +match_regex: id[:\s"]*123456 +no_match_regex: id[:\s"]*123456 +``` + + + +
+ +
+ +expect_ids []uint + +
+
+ +description: | + Expect the given IDs to be contained in the log output. + examples: + -value: ExampleLog.ExpectIds + +
+ +
+ +
+ +no_expect_ids []uint + +
+
+ +Expect the given IDs _not_ to be contained in the log output. + + + +Examples: + + +```yaml +no_expect_ids: + - 123456 +``` + + +
+ +
+ +
+ +match_regex string + +
+
+ +Expect the regular expression to match log content for the current types. + + + +Examples: + + +```yaml +match_regex: id[:\s"]*123456 +``` + + +
+ +
+ +
+ +no_match_regex string + +
+
+ +Expect the regular expression to _not_ match log content for the current types. + + + +Examples: + + +```yaml +no_match_regex: id[:\s"]*123456 +``` + + +
+ +
+ + + + + + + + +## FTWOverrides +FTWOverrides describes platform specific overrides for tests + + + + + + +
+ +
+ +version string + +
+
+ +The version field designates the version of the schema that validates this file + + + +Examples: + + +```yaml +version: v0.1.0 +``` + + +
+ +
+ +
+ +meta FTWOverridesMeta + +
+
+ +Meta describes the metadata information + + + +Examples: + + +```yaml +meta: + engine: libmodsecurity3 + platform: nginx + annotations: + os: Debian Bullseye + purpose: L7ASR test suite +``` + + +
+ +
+ +
+ +test_overrides []TestOverride + +
+
+ +List of test override specifications + + + +Examples: + + +```yaml +test_overrides: + - rule_id: 920100 + test_ids: [4, 6] + reason: |- + nginx returns 400 when `Content-Length` header is sent in a + `Transfer-Encoding: chunked` request. + output: + status: 200 + response_contains: HTTP/1.1 + log_contains: nothing + no_log_contains: everything + log: + expect_ids: + - 123456 + no_expect_ids: + - 123456 + match_regex: id[:\s"]*123456 + no_match_regex: id[:\s"]*123456 + expect_error: true +``` + + +
+ +
+ + + + + +## FTWOverridesMeta + +Appears in: + + +- FTWOverrides.meta + + +```yaml +engine: libmodsecurity3 +platform: nginx +annotations: + os: Debian Bullseye + purpose: L7ASR test suite +``` + + + +
+ +
+ +engine string + +
+
+ +The name of the WAF engine the tests are expected to run against + + + +Examples: + + +```yaml +engine: coraza +``` + + +
+ +
+ +
+ +platform string + +
+
+ +The name of the platform (e.g., web server) the tests are expected to run against + + + +Examples: + + +```yaml +platform: nginx +``` + + +
+ +
+ +
+ +annotations map[string]string + +
+
+ +Custom annotations; can be used to add additional meta information + + + +Examples: + + +```yaml +annotations: + os: Debian Bullseye + purpose: L7ASR test suite +``` + + +
+ +
+ + + + + +## TestOverride + +Appears in: + + +- FTWOverrides.test_overrides + + +```yaml +- rule_id: 920100 + test_ids: [4, 6] + reason: |- + nginx returns 400 when `Content-Length` header is sent in a + `Transfer-Encoding: chunked` request. + output: + status: 200 + response_contains: HTTP/1.1 + log_contains: nothing + no_log_contains: everything + log: + expect_ids: + - 123456 + no_expect_ids: + - 123456 + match_regex: id[:\s"]*123456 + no_match_regex: id[:\s"]*123456 + expect_error: true +``` + + + +
+ +
+ +rule_id uint + +
+
+ +ID of the rule this test targets. + + + +Examples: + + +```yaml +rule_id: 920100 +``` + + +
+ +
+ +
+ +test_ids []uint + +
+
+ +IDs of the tests for rule_id that overrides should be applied to. +If this field is not set, the overrides will be applied to all tests of rule_id. + + + +Examples: + + +```yaml +test_ids: + - 4 + - 6 +``` + + +
+ +
+ +
+ +stage_ids []uint + +
+
+ +IDs of the stages to which overrides should be applied. +Stage IDs listed will be overridden for all test IDs listed in `TestIds`. +If this field is not set, the overrides will be applied to all stages. + +
+ +
+ +
+ +reason string + +
+
+ +Describes why this override is necessary. + + + +Examples: + + +```yaml +reason: |- + nginx returns 400 when `Content-Length` header is sent in a + `Transfer-Encoding: chunked` request. +``` + + +
+ +
+ +
+ +retry_once bool + +
+
+ +Whether a stage should be retried once in case of failure. +This option is primarily a workaround for a race condition in phase 5, +where the log entry of a rule may be flushed after the test end marker. + + + +Examples: + + +```yaml +retry_once: true +``` + + +
+ +
+ +
+ +output types.Output + +
+
+ +Specifies overrides on the test output. +This definition *replaces* the output definition of the test. + + + +Examples: + + +```yaml +output: + status: 200 + response_contains: HTTP/1.1 + log_contains: nothing + no_log_contains: everything + log: + expect_ids: + - 123456 + no_expect_ids: + - 123456 + match_regex: id[:\s"]*123456 + no_match_regex: id[:\s"]*123456 + expect_error: true +``` + + +
+ +
+ + + + + +## types.Output +Output defines the expectations of a test + +Appears in: + + +- TestOverride.output + + +```yaml +status: 200 +response_contains: HTTP/1.1 +log_contains: nothing +no_log_contains: everything +log: + expect_ids: + - 123456 + no_expect_ids: + - 123456 + match_regex: id[:\s"]*123456 + no_match_regex: id[:\s"]*123456 +expect_error: true +``` + + + +
+ +
+ +status int + +
+
+ +Status describes the HTTP status code expected in the response. + + + +Examples: + + +```yaml +# Status +status: 200 +``` + + +
+ +
+ +
+ +response_contains string + +
+
+ +ResponseContains describes the text that should be contained in the HTTP response. + + + +Examples: + + +```yaml +# ResponseContains +response_contains: Hello, World +``` + + +
+ +
+ +
+ +log_contains string + +
+
+ +LogContains describes the text that should be contained in the WAF logs. + + + +Examples: + + +```yaml +# LogContains +log_contains: id 920100 +``` + + +
+ +
+ +
+ +no_log_contains string + +
+
+ +NoLogContains describes the text that should not be contained in the WAF logs. + + + +Examples: + + +```yaml +# NoLogContains +no_log_contains: id 920100 +``` + + +
+ +
+ +
+ +log Log + +
+
+ +Log is used to configure expectations about the log contents. + + + +Examples: + + +```yaml +log: + expect_ids: + - 123456 + no_expect_ids: + - 123456 + match_regex: id[:\s"]*123456 + no_match_regex: id[:\s"]*123456 +``` + + +
+ +
+ +
+ +expect_error types.bool + +
+
+ +When `ExpectError` is true, we don't expect an answer from the WAF, just an error. + + + +Examples: + + +```yaml +# ExpectError +expect_error: false +``` + + +
+ +
+ +
+ +retry_once types.bool + +
+
+ +When `RetryOnce` is true, the test run will be retried once upon failures. This options +primary purpose is to work around a race condition in phase 5, where the log entry for +a phase 5 rule may appear after the end marker of the previous test. + +
+ +
+ +
+ +isolated bool + +
+
+ +Isolated specifies that the test is expected to trigger a single rule only. +If the rule triggers any other rule than the (single) one specified in +expect_ids, the test fill be considered a failure. +Default: false + + + +Examples: + + +```yaml +# Isolated +isolated: true +``` + + +
+ +
+ + + + + +## types.Log + + + + + + +
+ +
+ +expect_ids []uint + +
+
+ +description: | + Expect the given IDs to be contained in the log output. + examples: + -value: ExampleLog.ExpectIds + +
+ +
+ +
+ +no_expect_ids []uint + +
+
+ +Expect the given IDs _not_ to be contained in the log output. + + + +Examples: + + +```yaml +no_expect_ids: + - 123456 +``` + + +
+ +
+ +
+ +match_regex string + +
+
+ +Expect the regular expression to match log content for the current types. + + + +Examples: + + +```yaml +match_regex: id[:\s"]*123456 +``` + + +
+ +
+ +
+ +no_match_regex string + +
+
+ +Expect the regular expression to _not_ match log content for the current types. + + + +Examples: + + +```yaml +no_match_regex: id[:\s"]*123456 +``` + + +
+ +
+ + + + diff --git a/spec/v2.1.0/waf-platform-overrides-schema-v2.1.0.json b/spec/v2.1.0/waf-platform-overrides-schema-v2.1.0.json new file mode 100755 index 0000000..138a705 --- /dev/null +++ b/spec/v2.1.0/waf-platform-overrides-schema-v2.1.0.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://github.com/coreruleset/ftw-tests-schema/v2/types/overrides/ftw-overrides","$ref":"#/$defs/FTWOverrides","$defs":{"FTWOverrides":{"properties":{"version":{"type":"string"},"meta":{"$ref":"#/$defs/FTWOverridesMeta"},"test_overrides":{"items":{"$ref":"#/$defs/TestOverride"},"type":"array"}},"additionalProperties":false,"type":"object","required":["version","meta","test_overrides"]},"FTWOverridesMeta":{"properties":{"engine":{"type":"string"},"platform":{"type":"string"},"annotations":{"additionalProperties":{"type":"string"},"type":"array"}},"additionalProperties":false,"type":"object","required":["engine","platform","annotations"]},"Log":{"properties":{"expect_ids":{"items":{"type":"integer"},"type":"array"},"no_expect_ids":{"items":{"type":"integer"},"type":"array"},"match_regex":{"type":"string"},"no_match_regex":{"type":"string"}},"additionalProperties":false,"type":"object"},"Output":{"properties":{"status":{"type":"integer"},"response_contains":{"type":"string"},"log_contains":{"type":"string"},"no_log_contains":{"type":"string"},"log":{"$ref":"#/$defs/Log"},"expect_error":{"type":"boolean"},"retry_once":{"type":"boolean"},"isolated":{"type":"boolean"}},"additionalProperties":false,"type":"object"},"TestOverride":{"properties":{"rule_id":{"type":"integer"},"test_ids":{"items":{"type":"integer"},"type":"array"},"stage_ids":{"items":{"type":"integer"},"type":"array"},"reason":{"type":"string"},"retry_once":{"type":"boolean"},"output":{"$ref":"#/$defs/Output"}},"additionalProperties":false,"type":"object","required":["rule_id","reason","output"]}}} \ No newline at end of file diff --git a/spec/v2.1.0/waf-tests-schema-v2.1.0.json b/spec/v2.1.0/waf-tests-schema-v2.1.0.json new file mode 100755 index 0000000..26c1615 --- /dev/null +++ b/spec/v2.1.0/waf-tests-schema-v2.1.0.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://github.com/coreruleset/ftw-tests-schema/v2/types/ftw-test","$ref":"#/$defs/FTWTest","$defs":{"FTWTest":{"properties":{"meta":{"$ref":"#/$defs/FTWTestMeta"},"rule_id":{"type":"integer"},"tests":{"items":{"$ref":"#/$defs/Test"},"type":"array"}},"additionalProperties":false,"type":"object","required":["meta","rule_id","tests"]},"FTWTestMeta":{"properties":{"author":{"type":"string"},"enabled":{"type":"boolean"},"name":{"type":"string"},"description":{"type":"string"},"version":{"type":"string"},"tags":{"items":{"type":"string"},"type":"array"}},"additionalProperties":false,"type":"object"},"Input":{"properties":{"dest_addr":{"type":"string"},"port":{"type":"integer"},"protocol":{"type":"string"},"uri":{"type":"string"},"follow_redirect":{"type":"boolean"},"version":{"type":"string"},"method":{"type":"string"},"headers":{"additionalProperties":{"type":"string"},"type":"object"},"data":{"type":"string"},"encoded_data":{"type":"string"},"save_cookie":{"type":"boolean"},"stop_magic":{"type":"boolean"},"autocomplete_headers":{"type":"boolean"},"encoded_request":{"type":"string"},"raw_request":{"type":"string"},"response":{"$ref":"#/$defs/Response"}},"additionalProperties":false,"type":"object"},"Log":{"properties":{"expect_ids":{"items":{"type":"integer"},"type":"array"},"no_expect_ids":{"items":{"type":"integer"},"type":"array"},"match_regex":{"type":"string"},"no_match_regex":{"type":"string"}},"additionalProperties":false,"type":"object"},"Output":{"properties":{"status":{"type":"integer"},"response_contains":{"type":"string"},"log_contains":{"type":"string"},"no_log_contains":{"type":"string"},"log":{"$ref":"#/$defs/Log"},"expect_error":{"type":"boolean"},"retry_once":{"type":"boolean"},"isolated":{"type":"boolean"}},"additionalProperties":false,"type":"object"},"Response":{"properties":{"headers":{"additionalProperties":{"type":"string"},"type":"object"},"status":{"type":"integer"},"body":{"type":"string"},"encoded_body":{"type":"string"},"log_message":{"type":"string"}},"additionalProperties":false,"type":"object"},"Stage":{"properties":{"description":{"type":"string"},"input":{"$ref":"#/$defs/Input"},"output":{"$ref":"#/$defs/Output"}},"additionalProperties":false,"type":"object","required":["input","output"]},"Test":{"properties":{"test_title":{"type":"string"},"test_id":{"type":"integer"},"desc":{"type":"string"},"stages":{"items":{"$ref":"#/$defs/Stage"},"type":"array"},"tags":{"items":{"type":"string"},"type":"array"}},"additionalProperties":false,"type":"object","required":["test_id","stages"]}}} \ No newline at end of file diff --git a/types/examples.go b/types/examples.go index 462b44a..7333a72 100644 --- a/types/examples.go +++ b/types/examples.go @@ -61,5 +61,17 @@ var ( } ReasonExample = "nginx returns 400 when `Content-Length` header is sent in a\n" + "`Transfer-Encoding: chunked` request." - ExampleEncodedData = "c29tZXRoaW5nIHdpdGgKbmV3bGluZQo=" + ExampleEncodedData = "c29tZXRoaW5nIHdpdGgKbmV3bGluZQo=" + ExampleResponseBody = "{\"aJsonDocument\": [\"in the response\"]}\n" + ExampleEncodedResponseBody = "eyJhSnNvbkRvY3VtZW50IjogWyJpbiB0aGUgcmVzcG9uc2UiXX0=" + ExampleRespone = Response{ + Status: 502, + Headers: map[string]string{ + "Content-Type": "application/problem+json", + "x-mr-burns": "excellent", + }, + Body: ExampleResponseBody, + EncodedBody: ExampleEncodedResponseBody, + LogMessage: "Response splitting test 1", + } ) diff --git a/types/types.go b/types/types.go index 0075276..81f22c0 100644 --- a/types/types.go +++ b/types/types.go @@ -231,7 +231,7 @@ type Input struct { Method *string `yaml:"method,omitempty" json:"method,omitempty" koanf:"method,omitempty"` // description: | - // Method allows you to declare headers that the test should send. + // Headers allows you to declare headers that the test should send. // examples: // - name: Headers // value: ExampleHeaders @@ -293,9 +293,57 @@ type Input struct { // // Deprecated: use `encoded_request` RAWRequest string `yaml:"raw_request,omitempty" json:"raw_request,omitempty" koanf:"raw_request,omitempty"` + + // description: | + // Response describes a response from the web server that a WAF is expected to analyse. + // Note: This functionality requires a backend that can send the specified request to the + // reverse proxy. Currently, only Albedo (https://github.com/coreruleset/albedo) is supported. + // example: + // - name Response + // value: ExampleResponse + Response Response `yaml:"response,omitempty" json:"response,omitempty" koanf:"response,omitempty"` +} + +type Response struct { + // description: | + // Headers defines the headers the response will carry. + // examples: + // - name: Headers + // value: ExampleHeaders + Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty" koanf:"headers,omitempty"` + + // description: | + // Status describes the HTTP status code of the response. Defaults to `200` if omitted. + // examples: + // - name: Status + // value: 302 + Status int `yaml:"status,omitempty" json:"status,omitempty" koanf:"status,omitempty"` + + // description: | + // Body defines the body of the response as a plain string. + // examples: + // - name: Body + // value: ExampleResponseBody + Body string `yaml:"body,omitempty" json:"body,omitempty" koanf:"body,omitempty"` + + // description: | + // EncodedBody defines the body of the response as a base64 encoded string. This is useful if the response + // needs to contain non-printable characters. + // examples: + // - name: EncodedBody + // value: ExampleEncodedResponseBody + EncodedBody string `yaml:"encoded_body,omitempty" json:"encoded_body,omitempty" koanf:"encoded_body,omitempty"` + + // description: | + // LogMessage specifies a message to be printed in the log of the backend server that sends the response. + // This can be helpful when debugging, to match resopnses sent by the backend to test executions. + // examples: + // - name: LogMessage + // value: "\"Response splitting test 1\"" + LogMessage string `yaml:"log_message,omitempty" json:"log_message,omitempty" koanf:"log_message,omitempty"` } -// Output is the response expected from the test +// Output defines the expectations of a test type Output struct { // description: | // Status describes the HTTP status code expected in the response. diff --git a/types/types_test.go b/types/types_test.go index 29c7156..9d28ee7 100644 --- a/types/types_test.go +++ b/types/types_test.go @@ -42,7 +42,6 @@ tests: response_contains: "" log_contains: "nothing" no_log_contains: "everything" - - stages: - input: dest_addr: "127.0.0.1" port: 80 @@ -50,6 +49,15 @@ tests: headers: User-Agent: "FTW Schema Tests" Host: "localhost" + response: + status: 502 + headers: + Content-Type: "application/problem+json" + x-mr-burns: excellent + body: | + {"aJsonDocument": ["in the response"]} + encoded_body: eyJhSnNvbkRvY3VtZW50IjogWyJpbiB0aGUgcmVzcG9uc2UiXX0= + log_message: Response splitting test 1 output: status: 200 ` @@ -72,10 +80,6 @@ var ftwTest = &FTWTest{ Input: ExampleInput, Output: ExampleOutput, }, - }, - }, - { - Stages: []Stage{ { Input: Input{ DestAddr: helpers.StrPtr("127.0.0.1"), @@ -85,6 +89,7 @@ var ftwTest = &FTWTest{ "User-Agent": "FTW Schema Tests", "Host": "localhost", }, + Response: ExampleRespone, }, Output: Output{ Status: 200, @@ -122,9 +127,26 @@ func TestUnmarshalFTWTest(t *testing.T) { assertions.Equal(expectedStage.Input.Method, stage.Input.Method) assertions.Len(stage.Input.Headers, len(expectedStage.Input.Headers)) - for k, header := range stage.Input.Headers { - expectedHeader := expectedStage.Input.Headers[k] - assertions.Equal(expectedHeader, header) + for name, value := range stage.Input.Headers { + assertions.Contains(expectedStage.Input.Headers, name) + assertions.Equal(expectedStage.Input.Headers[name], value) + } + + response := stage.Input.Response + if j == 1 { + assertions.NotNil(response) + assertions.Equal(502, response.Status) + assertions.Equal("{\"aJsonDocument\": [\"in the response\"]}\n", response.Body) + assertions.Equal("eyJhSnNvbkRvY3VtZW50IjogWyJpbiB0aGUgcmVzcG9uc2UiXX0=", response.EncodedBody) + assertions.Equal("Response splitting test 1", response.LogMessage) + assertions.Len(response.Headers, len(expectedStage.Input.Headers)) + + for name, value := range stage.Input.Headers { + assertions.Contains(expectedStage.Input.Headers, name) + assertions.Equal(expectedStage.Input.Headers[name], value) + } + } else { + assertions.Equal(response, Response{}) } assertions.Equal(expectedStage.Output.NoLogContains, stage.Output.NoLogContains)