diff --git a/experimental/plugins/collections/testdata/gen_test_xml.go b/experimental/plugins/collections/testdata/gen_test_xml.go
new file mode 100644
index 000000000..52c2d27ca
--- /dev/null
+++ b/experimental/plugins/collections/testdata/gen_test_xml.go
@@ -0,0 +1,177 @@
+// Copyright 2026 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+//go:build ignore
+// +build ignore
+
+// gen_test_xml generates medium and large XML test fixtures with diverse
+// structure: namespaces, attributes, CDATA, mixed content, deep nesting,
+// processing instructions, and comments.
+package main
+
+import (
+ "fmt"
+ "os"
+ "strings"
+)
+
+func main() {
+ generateMedium("medium_mixed.xml", 50)
+ generateLarge("large_catalog.xml", 500)
+ generateDeepNesting("medium_deep.xml", 30)
+}
+
+func generateMedium(filename string, count int) {
+ var b strings.Builder
+ b.WriteString(``)
+ b.WriteString("\n")
+ b.WriteString(``)
+ b.WriteString("\n")
+ b.WriteString(``)
+ b.WriteString("\n")
+ b.WriteString(``)
+ b.WriteString("\n")
+
+ categories := []string{"fiction", "science", "history", "tech", "art"}
+ formats := []string{"hardcover", "paperback", "ebook", "audiobook"}
+
+ for i := 1; i <= count; i++ {
+ cat := categories[i%len(categories)]
+ format := formats[i%len(formats)]
+ b.WriteString(fmt.Sprintf(` `+"\n",
+ i, cat, format, i%3 != 0))
+ b.WriteString(fmt.Sprintf(` Book Title Number %d`+"\n", i))
+ b.WriteString(fmt.Sprintf(` Author %d`+"\n", i%20+1))
+ b.WriteString(fmt.Sprintf(` 978-0-%07d-%d`+"\n", i*1000+i, i%10))
+ b.WriteString(fmt.Sprintf(` %.2f`+"\n", float64(10+i%90)+0.99))
+ if i%5 == 0 {
+ b.WriteString(` characters & "quotes" that need CDATA.]]>` + "\n")
+ } else if i%3 == 0 {
+ b.WriteString(fmt.Sprintf(` A fascinating exploration of %s topics in volume %d.`+"\n", cat, i))
+ }
+ if i%4 == 0 {
+ b.WriteString(` ` + "\n")
+ for j := 1; j <= 3; j++ {
+ b.WriteString(fmt.Sprintf(` `+"\n", j+2, j%2 == 0))
+ b.WriteString(fmt.Sprintf(` User%d`+"\n", i*10+j))
+ b.WriteString(fmt.Sprintf(` Review comment %d for book %d`+"\n", j, i))
+ b.WriteString(` ` + "\n")
+ }
+ b.WriteString(` ` + "\n")
+ }
+ if i%7 == 0 {
+ b.WriteString(` ` + "\n")
+ b.WriteString(fmt.Sprintf(` `+"\n", i%5+1))
+ b.WriteString(fmt.Sprintf(` `+"\n", 100+i*3))
+ }
+ b.WriteString(` ` + "\n")
+ }
+
+ b.WriteString(`` + "\n")
+ writeFile(filename, b.String())
+}
+
+func generateLarge(filename string, count int) {
+ var b strings.Builder
+ b.WriteString(`` + "\n")
+ b.WriteString(`` + "\n")
+
+ departments := []string{"Engineering", "Marketing", "Sales", "Support", "Research", "Legal", "Finance", "HR"}
+ roles := []string{"developer", "manager", "analyst", "director", "intern", "architect", "lead", "specialist"}
+ locations := []string{"NYC", "SFO", "LON", "TKY", "BER", "SYD"}
+
+ for i := 1; i <= count; i++ {
+ dept := departments[i%len(departments)]
+ role := roles[i%len(roles)]
+ loc := locations[i%len(locations)]
+
+ b.WriteString(fmt.Sprintf(` `+"\n",
+ i, dept, i%5+1))
+ b.WriteString(fmt.Sprintf(` ` + "\n"))
+ b.WriteString(fmt.Sprintf(` FirstName%d`+"\n", i))
+ b.WriteString(fmt.Sprintf(` LastName%d`+"\n", i))
+ b.WriteString(` ` + "\n")
+ b.WriteString(fmt.Sprintf(` %s`+"\n", role))
+ b.WriteString(fmt.Sprintf(` %s Office`+"\n",
+ loc, i%3 == 0, loc))
+
+ b.WriteString(fmt.Sprintf(` ` + "\n"))
+ b.WriteString(fmt.Sprintf(` %d`+"\n", 50000+i*100))
+ b.WriteString(fmt.Sprintf(` `+"\n", float64(i%20)+5.0))
+ if i%3 == 0 {
+ b.WriteString(` ` + "\n")
+ b.WriteString(fmt.Sprintf(` `+"\n", i*100, float64(50+i%200)+0.50))
+ b.WriteString(` ` + "\n")
+ }
+ b.WriteString(` ` + "\n")
+
+ if i%5 == 0 {
+ b.WriteString(` ` + "\n")
+ for j := 1; j <= 5; j++ {
+ b.WriteString(fmt.Sprintf(` `+"\n",
+ j, j+8, []string{"read", "write", "admin"}[j%3], j%3+1))
+ }
+ b.WriteString(` ` + "\n")
+ }
+
+ if i%10 == 0 {
+ b.WriteString(` data & "sensitive" info.]]>` + "\n")
+ }
+
+ // Mixed content element
+ if i%8 == 0 {
+ b.WriteString(fmt.Sprintf(` Employee %d joined in %d and works on Project-%s.`+"\n",
+ i, 2015+i%10, dept[:3]))
+ }
+
+ b.WriteString(` ` + "\n")
+ }
+
+ b.WriteString(`` + "\n")
+ writeFile(filename, b.String())
+}
+
+func generateDeepNesting(filename string, depth int) {
+ var b strings.Builder
+ b.WriteString(`` + "\n")
+ b.WriteString(`` + "\n")
+ b.WriteString(`` + "\n")
+
+ // Generate a deeply nested structure with siblings at each level
+ for i := 1; i <= depth; i++ {
+ indent := strings.Repeat(" ", i)
+ b.WriteString(fmt.Sprintf(`%s`+"\n", indent, i, i))
+ b.WriteString(fmt.Sprintf(`%s Level %d Node`+"\n", indent, i))
+ b.WriteString(fmt.Sprintf(`%s Value at depth %d`+"\n", indent, i))
+ if i%5 == 0 {
+ b.WriteString(fmt.Sprintf(`%s Sibling A at depth %d`+"\n", indent, i))
+ b.WriteString(fmt.Sprintf(`%s Sibling B at depth %d`+"\n", indent, i))
+ }
+ }
+
+ // Close all levels
+ for i := depth; i >= 1; i-- {
+ indent := strings.Repeat(" ", i)
+ b.WriteString(fmt.Sprintf(`%s`+"\n", indent))
+ }
+
+ b.WriteString(`` + "\n")
+ writeFile(filename, b.String())
+}
+
+func writeFile(name, content string) {
+ if err := os.WriteFile(name, []byte(content), 0644); err != nil {
+ fmt.Fprintf(os.Stderr, "error writing %s: %v\n", name, err)
+ os.Exit(1)
+ }
+ fmt.Printf("Generated %s (%d bytes)\n", name, len(content))
+}
diff --git a/experimental/plugins/collections/testdata/large_catalog.xml b/experimental/plugins/collections/testdata/large_catalog.xml
new file mode 100644
index 000000000..2fe137379
--- /dev/null
+++ b/experimental/plugins/collections/testdata/large_catalog.xml
@@ -0,0 +1,7316 @@
+
+
+
+
+ FirstName1
+ LastName1
+
+ manager
+ SFO Office
+
+ 50100
+
+
+
+
+
+ FirstName2
+ LastName2
+
+ analyst
+ LON Office
+
+ 50200
+
+
+
+
+
+ FirstName3
+ LastName3
+
+ director
+ TKY Office
+
+ 50300
+
+
+
+
+
+
+
+
+ FirstName4
+ LastName4
+
+ intern
+ BER Office
+
+ 50400
+
+
+
+
+
+ FirstName5
+ LastName5
+
+ architect
+ SYD Office
+
+ 50500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName6
+ LastName6
+
+ lead
+ NYC Office
+
+ 50600
+
+
+
+
+
+
+
+
+ FirstName7
+ LastName7
+
+ specialist
+ SFO Office
+
+ 50700
+
+
+
+
+
+ FirstName8
+ LastName8
+
+ developer
+ LON Office
+
+ 50800
+
+
+ Employee 8 joined in 2023 and works on Project-Eng.
+
+
+
+ FirstName9
+ LastName9
+
+ manager
+ TKY Office
+
+ 50900
+
+
+
+
+
+
+
+
+ FirstName10
+ LastName10
+
+ analyst
+ BER Office
+
+ 51000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName11
+ LastName11
+
+ director
+ SYD Office
+
+ 51100
+
+
+
+
+
+ FirstName12
+ LastName12
+
+ intern
+ NYC Office
+
+ 51200
+
+
+
+
+
+
+
+
+ FirstName13
+ LastName13
+
+ architect
+ SFO Office
+
+ 51300
+
+
+
+
+
+ FirstName14
+ LastName14
+
+ lead
+ LON Office
+
+ 51400
+
+
+
+
+
+ FirstName15
+ LastName15
+
+ specialist
+ TKY Office
+
+ 51500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName16
+ LastName16
+
+ developer
+ BER Office
+
+ 51600
+
+
+ Employee 16 joined in 2021 and works on Project-Eng.
+
+
+
+ FirstName17
+ LastName17
+
+ manager
+ SYD Office
+
+ 51700
+
+
+
+
+
+ FirstName18
+ LastName18
+
+ analyst
+ NYC Office
+
+ 51800
+
+
+
+
+
+
+
+
+ FirstName19
+ LastName19
+
+ director
+ SFO Office
+
+ 51900
+
+
+
+
+
+ FirstName20
+ LastName20
+
+ intern
+ LON Office
+
+ 52000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName21
+ LastName21
+
+ architect
+ TKY Office
+
+ 52100
+
+
+
+
+
+
+
+
+ FirstName22
+ LastName22
+
+ lead
+ BER Office
+
+ 52200
+
+
+
+
+
+ FirstName23
+ LastName23
+
+ specialist
+ SYD Office
+
+ 52300
+
+
+
+
+
+ FirstName24
+ LastName24
+
+ developer
+ NYC Office
+
+ 52400
+
+
+
+
+
+ Employee 24 joined in 2019 and works on Project-Eng.
+
+
+
+ FirstName25
+ LastName25
+
+ manager
+ SFO Office
+
+ 52500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName26
+ LastName26
+
+ analyst
+ LON Office
+
+ 52600
+
+
+
+
+
+ FirstName27
+ LastName27
+
+ director
+ TKY Office
+
+ 52700
+
+
+
+
+
+
+
+
+ FirstName28
+ LastName28
+
+ intern
+ BER Office
+
+ 52800
+
+
+
+
+
+ FirstName29
+ LastName29
+
+ architect
+ SYD Office
+
+ 52900
+
+
+
+
+
+ FirstName30
+ LastName30
+
+ lead
+ NYC Office
+
+ 53000
+
+
+
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName31
+ LastName31
+
+ specialist
+ SFO Office
+
+ 53100
+
+
+
+
+
+ FirstName32
+ LastName32
+
+ developer
+ LON Office
+
+ 53200
+
+
+ Employee 32 joined in 2017 and works on Project-Eng.
+
+
+
+ FirstName33
+ LastName33
+
+ manager
+ TKY Office
+
+ 53300
+
+
+
+
+
+
+
+
+ FirstName34
+ LastName34
+
+ analyst
+ BER Office
+
+ 53400
+
+
+
+
+
+ FirstName35
+ LastName35
+
+ director
+ SYD Office
+
+ 53500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName36
+ LastName36
+
+ intern
+ NYC Office
+
+ 53600
+
+
+
+
+
+
+
+
+ FirstName37
+ LastName37
+
+ architect
+ SFO Office
+
+ 53700
+
+
+
+
+
+ FirstName38
+ LastName38
+
+ lead
+ LON Office
+
+ 53800
+
+
+
+
+
+ FirstName39
+ LastName39
+
+ specialist
+ TKY Office
+
+ 53900
+
+
+
+
+
+
+
+
+ FirstName40
+ LastName40
+
+ developer
+ BER Office
+
+ 54000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+ Employee 40 joined in 2015 and works on Project-Eng.
+
+
+
+ FirstName41
+ LastName41
+
+ manager
+ SYD Office
+
+ 54100
+
+
+
+
+
+ FirstName42
+ LastName42
+
+ analyst
+ NYC Office
+
+ 54200
+
+
+
+
+
+
+
+
+ FirstName43
+ LastName43
+
+ director
+ SFO Office
+
+ 54300
+
+
+
+
+
+ FirstName44
+ LastName44
+
+ intern
+ LON Office
+
+ 54400
+
+
+
+
+
+ FirstName45
+ LastName45
+
+ architect
+ TKY Office
+
+ 54500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName46
+ LastName46
+
+ lead
+ BER Office
+
+ 54600
+
+
+
+
+
+ FirstName47
+ LastName47
+
+ specialist
+ SYD Office
+
+ 54700
+
+
+
+
+
+ FirstName48
+ LastName48
+
+ developer
+ NYC Office
+
+ 54800
+
+
+
+
+
+ Employee 48 joined in 2023 and works on Project-Eng.
+
+
+
+ FirstName49
+ LastName49
+
+ manager
+ SFO Office
+
+ 54900
+
+
+
+
+
+ FirstName50
+ LastName50
+
+ analyst
+ LON Office
+
+ 55000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName51
+ LastName51
+
+ director
+ TKY Office
+
+ 55100
+
+
+
+
+
+
+
+
+ FirstName52
+ LastName52
+
+ intern
+ BER Office
+
+ 55200
+
+
+
+
+
+ FirstName53
+ LastName53
+
+ architect
+ SYD Office
+
+ 55300
+
+
+
+
+
+ FirstName54
+ LastName54
+
+ lead
+ NYC Office
+
+ 55400
+
+
+
+
+
+
+
+
+ FirstName55
+ LastName55
+
+ specialist
+ SFO Office
+
+ 55500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName56
+ LastName56
+
+ developer
+ LON Office
+
+ 55600
+
+
+ Employee 56 joined in 2021 and works on Project-Eng.
+
+
+
+ FirstName57
+ LastName57
+
+ manager
+ TKY Office
+
+ 55700
+
+
+
+
+
+
+
+
+ FirstName58
+ LastName58
+
+ analyst
+ BER Office
+
+ 55800
+
+
+
+
+
+ FirstName59
+ LastName59
+
+ director
+ SYD Office
+
+ 55900
+
+
+
+
+
+ FirstName60
+ LastName60
+
+ intern
+ NYC Office
+
+ 56000
+
+
+
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName61
+ LastName61
+
+ architect
+ SFO Office
+
+ 56100
+
+
+
+
+
+ FirstName62
+ LastName62
+
+ lead
+ LON Office
+
+ 56200
+
+
+
+
+
+ FirstName63
+ LastName63
+
+ specialist
+ TKY Office
+
+ 56300
+
+
+
+
+
+
+
+
+ FirstName64
+ LastName64
+
+ developer
+ BER Office
+
+ 56400
+
+
+ Employee 64 joined in 2019 and works on Project-Eng.
+
+
+
+ FirstName65
+ LastName65
+
+ manager
+ SYD Office
+
+ 56500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName66
+ LastName66
+
+ analyst
+ NYC Office
+
+ 56600
+
+
+
+
+
+
+
+
+ FirstName67
+ LastName67
+
+ director
+ SFO Office
+
+ 56700
+
+
+
+
+
+ FirstName68
+ LastName68
+
+ intern
+ LON Office
+
+ 56800
+
+
+
+
+
+ FirstName69
+ LastName69
+
+ architect
+ TKY Office
+
+ 56900
+
+
+
+
+
+
+
+
+ FirstName70
+ LastName70
+
+ lead
+ BER Office
+
+ 57000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName71
+ LastName71
+
+ specialist
+ SYD Office
+
+ 57100
+
+
+
+
+
+ FirstName72
+ LastName72
+
+ developer
+ NYC Office
+
+ 57200
+
+
+
+
+
+ Employee 72 joined in 2017 and works on Project-Eng.
+
+
+
+ FirstName73
+ LastName73
+
+ manager
+ SFO Office
+
+ 57300
+
+
+
+
+
+ FirstName74
+ LastName74
+
+ analyst
+ LON Office
+
+ 57400
+
+
+
+
+
+ FirstName75
+ LastName75
+
+ director
+ TKY Office
+
+ 57500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName76
+ LastName76
+
+ intern
+ BER Office
+
+ 57600
+
+
+
+
+
+ FirstName77
+ LastName77
+
+ architect
+ SYD Office
+
+ 57700
+
+
+
+
+
+ FirstName78
+ LastName78
+
+ lead
+ NYC Office
+
+ 57800
+
+
+
+
+
+
+
+
+ FirstName79
+ LastName79
+
+ specialist
+ SFO Office
+
+ 57900
+
+
+
+
+
+ FirstName80
+ LastName80
+
+ developer
+ LON Office
+
+ 58000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+ Employee 80 joined in 2015 and works on Project-Eng.
+
+
+
+ FirstName81
+ LastName81
+
+ manager
+ TKY Office
+
+ 58100
+
+
+
+
+
+
+
+
+ FirstName82
+ LastName82
+
+ analyst
+ BER Office
+
+ 58200
+
+
+
+
+
+ FirstName83
+ LastName83
+
+ director
+ SYD Office
+
+ 58300
+
+
+
+
+
+ FirstName84
+ LastName84
+
+ intern
+ NYC Office
+
+ 58400
+
+
+
+
+
+
+
+
+ FirstName85
+ LastName85
+
+ architect
+ SFO Office
+
+ 58500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName86
+ LastName86
+
+ lead
+ LON Office
+
+ 58600
+
+
+
+
+
+ FirstName87
+ LastName87
+
+ specialist
+ TKY Office
+
+ 58700
+
+
+
+
+
+
+
+
+ FirstName88
+ LastName88
+
+ developer
+ BER Office
+
+ 58800
+
+
+ Employee 88 joined in 2023 and works on Project-Eng.
+
+
+
+ FirstName89
+ LastName89
+
+ manager
+ SYD Office
+
+ 58900
+
+
+
+
+
+ FirstName90
+ LastName90
+
+ analyst
+ NYC Office
+
+ 59000
+
+
+
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName91
+ LastName91
+
+ director
+ SFO Office
+
+ 59100
+
+
+
+
+
+ FirstName92
+ LastName92
+
+ intern
+ LON Office
+
+ 59200
+
+
+
+
+
+ FirstName93
+ LastName93
+
+ architect
+ TKY Office
+
+ 59300
+
+
+
+
+
+
+
+
+ FirstName94
+ LastName94
+
+ lead
+ BER Office
+
+ 59400
+
+
+
+
+
+ FirstName95
+ LastName95
+
+ specialist
+ SYD Office
+
+ 59500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName96
+ LastName96
+
+ developer
+ NYC Office
+
+ 59600
+
+
+
+
+
+ Employee 96 joined in 2021 and works on Project-Eng.
+
+
+
+ FirstName97
+ LastName97
+
+ manager
+ SFO Office
+
+ 59700
+
+
+
+
+
+ FirstName98
+ LastName98
+
+ analyst
+ LON Office
+
+ 59800
+
+
+
+
+
+ FirstName99
+ LastName99
+
+ director
+ TKY Office
+
+ 59900
+
+
+
+
+
+
+
+
+ FirstName100
+ LastName100
+
+ intern
+ BER Office
+
+ 60000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName101
+ LastName101
+
+ architect
+ SYD Office
+
+ 60100
+
+
+
+
+
+ FirstName102
+ LastName102
+
+ lead
+ NYC Office
+
+ 60200
+
+
+
+
+
+
+
+
+ FirstName103
+ LastName103
+
+ specialist
+ SFO Office
+
+ 60300
+
+
+
+
+
+ FirstName104
+ LastName104
+
+ developer
+ LON Office
+
+ 60400
+
+
+ Employee 104 joined in 2019 and works on Project-Eng.
+
+
+
+ FirstName105
+ LastName105
+
+ manager
+ TKY Office
+
+ 60500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName106
+ LastName106
+
+ analyst
+ BER Office
+
+ 60600
+
+
+
+
+
+ FirstName107
+ LastName107
+
+ director
+ SYD Office
+
+ 60700
+
+
+
+
+
+ FirstName108
+ LastName108
+
+ intern
+ NYC Office
+
+ 60800
+
+
+
+
+
+
+
+
+ FirstName109
+ LastName109
+
+ architect
+ SFO Office
+
+ 60900
+
+
+
+
+
+ FirstName110
+ LastName110
+
+ lead
+ LON Office
+
+ 61000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName111
+ LastName111
+
+ specialist
+ TKY Office
+
+ 61100
+
+
+
+
+
+
+
+
+ FirstName112
+ LastName112
+
+ developer
+ BER Office
+
+ 61200
+
+
+ Employee 112 joined in 2017 and works on Project-Eng.
+
+
+
+ FirstName113
+ LastName113
+
+ manager
+ SYD Office
+
+ 61300
+
+
+
+
+
+ FirstName114
+ LastName114
+
+ analyst
+ NYC Office
+
+ 61400
+
+
+
+
+
+
+
+
+ FirstName115
+ LastName115
+
+ director
+ SFO Office
+
+ 61500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName116
+ LastName116
+
+ intern
+ LON Office
+
+ 61600
+
+
+
+
+
+ FirstName117
+ LastName117
+
+ architect
+ TKY Office
+
+ 61700
+
+
+
+
+
+
+
+
+ FirstName118
+ LastName118
+
+ lead
+ BER Office
+
+ 61800
+
+
+
+
+
+ FirstName119
+ LastName119
+
+ specialist
+ SYD Office
+
+ 61900
+
+
+
+
+
+ FirstName120
+ LastName120
+
+ developer
+ NYC Office
+
+ 62000
+
+
+
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+ Employee 120 joined in 2015 and works on Project-Eng.
+
+
+
+ FirstName121
+ LastName121
+
+ manager
+ SFO Office
+
+ 62100
+
+
+
+
+
+ FirstName122
+ LastName122
+
+ analyst
+ LON Office
+
+ 62200
+
+
+
+
+
+ FirstName123
+ LastName123
+
+ director
+ TKY Office
+
+ 62300
+
+
+
+
+
+
+
+
+ FirstName124
+ LastName124
+
+ intern
+ BER Office
+
+ 62400
+
+
+
+
+
+ FirstName125
+ LastName125
+
+ architect
+ SYD Office
+
+ 62500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName126
+ LastName126
+
+ lead
+ NYC Office
+
+ 62600
+
+
+
+
+
+
+
+
+ FirstName127
+ LastName127
+
+ specialist
+ SFO Office
+
+ 62700
+
+
+
+
+
+ FirstName128
+ LastName128
+
+ developer
+ LON Office
+
+ 62800
+
+
+ Employee 128 joined in 2023 and works on Project-Eng.
+
+
+
+ FirstName129
+ LastName129
+
+ manager
+ TKY Office
+
+ 62900
+
+
+
+
+
+
+
+
+ FirstName130
+ LastName130
+
+ analyst
+ BER Office
+
+ 63000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName131
+ LastName131
+
+ director
+ SYD Office
+
+ 63100
+
+
+
+
+
+ FirstName132
+ LastName132
+
+ intern
+ NYC Office
+
+ 63200
+
+
+
+
+
+
+
+
+ FirstName133
+ LastName133
+
+ architect
+ SFO Office
+
+ 63300
+
+
+
+
+
+ FirstName134
+ LastName134
+
+ lead
+ LON Office
+
+ 63400
+
+
+
+
+
+ FirstName135
+ LastName135
+
+ specialist
+ TKY Office
+
+ 63500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName136
+ LastName136
+
+ developer
+ BER Office
+
+ 63600
+
+
+ Employee 136 joined in 2021 and works on Project-Eng.
+
+
+
+ FirstName137
+ LastName137
+
+ manager
+ SYD Office
+
+ 63700
+
+
+
+
+
+ FirstName138
+ LastName138
+
+ analyst
+ NYC Office
+
+ 63800
+
+
+
+
+
+
+
+
+ FirstName139
+ LastName139
+
+ director
+ SFO Office
+
+ 63900
+
+
+
+
+
+ FirstName140
+ LastName140
+
+ intern
+ LON Office
+
+ 64000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName141
+ LastName141
+
+ architect
+ TKY Office
+
+ 64100
+
+
+
+
+
+
+
+
+ FirstName142
+ LastName142
+
+ lead
+ BER Office
+
+ 64200
+
+
+
+
+
+ FirstName143
+ LastName143
+
+ specialist
+ SYD Office
+
+ 64300
+
+
+
+
+
+ FirstName144
+ LastName144
+
+ developer
+ NYC Office
+
+ 64400
+
+
+
+
+
+ Employee 144 joined in 2019 and works on Project-Eng.
+
+
+
+ FirstName145
+ LastName145
+
+ manager
+ SFO Office
+
+ 64500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName146
+ LastName146
+
+ analyst
+ LON Office
+
+ 64600
+
+
+
+
+
+ FirstName147
+ LastName147
+
+ director
+ TKY Office
+
+ 64700
+
+
+
+
+
+
+
+
+ FirstName148
+ LastName148
+
+ intern
+ BER Office
+
+ 64800
+
+
+
+
+
+ FirstName149
+ LastName149
+
+ architect
+ SYD Office
+
+ 64900
+
+
+
+
+
+ FirstName150
+ LastName150
+
+ lead
+ NYC Office
+
+ 65000
+
+
+
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName151
+ LastName151
+
+ specialist
+ SFO Office
+
+ 65100
+
+
+
+
+
+ FirstName152
+ LastName152
+
+ developer
+ LON Office
+
+ 65200
+
+
+ Employee 152 joined in 2017 and works on Project-Eng.
+
+
+
+ FirstName153
+ LastName153
+
+ manager
+ TKY Office
+
+ 65300
+
+
+
+
+
+
+
+
+ FirstName154
+ LastName154
+
+ analyst
+ BER Office
+
+ 65400
+
+
+
+
+
+ FirstName155
+ LastName155
+
+ director
+ SYD Office
+
+ 65500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName156
+ LastName156
+
+ intern
+ NYC Office
+
+ 65600
+
+
+
+
+
+
+
+
+ FirstName157
+ LastName157
+
+ architect
+ SFO Office
+
+ 65700
+
+
+
+
+
+ FirstName158
+ LastName158
+
+ lead
+ LON Office
+
+ 65800
+
+
+
+
+
+ FirstName159
+ LastName159
+
+ specialist
+ TKY Office
+
+ 65900
+
+
+
+
+
+
+
+
+ FirstName160
+ LastName160
+
+ developer
+ BER Office
+
+ 66000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+ Employee 160 joined in 2015 and works on Project-Eng.
+
+
+
+ FirstName161
+ LastName161
+
+ manager
+ SYD Office
+
+ 66100
+
+
+
+
+
+ FirstName162
+ LastName162
+
+ analyst
+ NYC Office
+
+ 66200
+
+
+
+
+
+
+
+
+ FirstName163
+ LastName163
+
+ director
+ SFO Office
+
+ 66300
+
+
+
+
+
+ FirstName164
+ LastName164
+
+ intern
+ LON Office
+
+ 66400
+
+
+
+
+
+ FirstName165
+ LastName165
+
+ architect
+ TKY Office
+
+ 66500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName166
+ LastName166
+
+ lead
+ BER Office
+
+ 66600
+
+
+
+
+
+ FirstName167
+ LastName167
+
+ specialist
+ SYD Office
+
+ 66700
+
+
+
+
+
+ FirstName168
+ LastName168
+
+ developer
+ NYC Office
+
+ 66800
+
+
+
+
+
+ Employee 168 joined in 2023 and works on Project-Eng.
+
+
+
+ FirstName169
+ LastName169
+
+ manager
+ SFO Office
+
+ 66900
+
+
+
+
+
+ FirstName170
+ LastName170
+
+ analyst
+ LON Office
+
+ 67000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName171
+ LastName171
+
+ director
+ TKY Office
+
+ 67100
+
+
+
+
+
+
+
+
+ FirstName172
+ LastName172
+
+ intern
+ BER Office
+
+ 67200
+
+
+
+
+
+ FirstName173
+ LastName173
+
+ architect
+ SYD Office
+
+ 67300
+
+
+
+
+
+ FirstName174
+ LastName174
+
+ lead
+ NYC Office
+
+ 67400
+
+
+
+
+
+
+
+
+ FirstName175
+ LastName175
+
+ specialist
+ SFO Office
+
+ 67500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName176
+ LastName176
+
+ developer
+ LON Office
+
+ 67600
+
+
+ Employee 176 joined in 2021 and works on Project-Eng.
+
+
+
+ FirstName177
+ LastName177
+
+ manager
+ TKY Office
+
+ 67700
+
+
+
+
+
+
+
+
+ FirstName178
+ LastName178
+
+ analyst
+ BER Office
+
+ 67800
+
+
+
+
+
+ FirstName179
+ LastName179
+
+ director
+ SYD Office
+
+ 67900
+
+
+
+
+
+ FirstName180
+ LastName180
+
+ intern
+ NYC Office
+
+ 68000
+
+
+
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName181
+ LastName181
+
+ architect
+ SFO Office
+
+ 68100
+
+
+
+
+
+ FirstName182
+ LastName182
+
+ lead
+ LON Office
+
+ 68200
+
+
+
+
+
+ FirstName183
+ LastName183
+
+ specialist
+ TKY Office
+
+ 68300
+
+
+
+
+
+
+
+
+ FirstName184
+ LastName184
+
+ developer
+ BER Office
+
+ 68400
+
+
+ Employee 184 joined in 2019 and works on Project-Eng.
+
+
+
+ FirstName185
+ LastName185
+
+ manager
+ SYD Office
+
+ 68500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName186
+ LastName186
+
+ analyst
+ NYC Office
+
+ 68600
+
+
+
+
+
+
+
+
+ FirstName187
+ LastName187
+
+ director
+ SFO Office
+
+ 68700
+
+
+
+
+
+ FirstName188
+ LastName188
+
+ intern
+ LON Office
+
+ 68800
+
+
+
+
+
+ FirstName189
+ LastName189
+
+ architect
+ TKY Office
+
+ 68900
+
+
+
+
+
+
+
+
+ FirstName190
+ LastName190
+
+ lead
+ BER Office
+
+ 69000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName191
+ LastName191
+
+ specialist
+ SYD Office
+
+ 69100
+
+
+
+
+
+ FirstName192
+ LastName192
+
+ developer
+ NYC Office
+
+ 69200
+
+
+
+
+
+ Employee 192 joined in 2017 and works on Project-Eng.
+
+
+
+ FirstName193
+ LastName193
+
+ manager
+ SFO Office
+
+ 69300
+
+
+
+
+
+ FirstName194
+ LastName194
+
+ analyst
+ LON Office
+
+ 69400
+
+
+
+
+
+ FirstName195
+ LastName195
+
+ director
+ TKY Office
+
+ 69500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName196
+ LastName196
+
+ intern
+ BER Office
+
+ 69600
+
+
+
+
+
+ FirstName197
+ LastName197
+
+ architect
+ SYD Office
+
+ 69700
+
+
+
+
+
+ FirstName198
+ LastName198
+
+ lead
+ NYC Office
+
+ 69800
+
+
+
+
+
+
+
+
+ FirstName199
+ LastName199
+
+ specialist
+ SFO Office
+
+ 69900
+
+
+
+
+
+ FirstName200
+ LastName200
+
+ developer
+ LON Office
+
+ 70000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+ Employee 200 joined in 2015 and works on Project-Eng.
+
+
+
+ FirstName201
+ LastName201
+
+ manager
+ TKY Office
+
+ 70100
+
+
+
+
+
+
+
+
+ FirstName202
+ LastName202
+
+ analyst
+ BER Office
+
+ 70200
+
+
+
+
+
+ FirstName203
+ LastName203
+
+ director
+ SYD Office
+
+ 70300
+
+
+
+
+
+ FirstName204
+ LastName204
+
+ intern
+ NYC Office
+
+ 70400
+
+
+
+
+
+
+
+
+ FirstName205
+ LastName205
+
+ architect
+ SFO Office
+
+ 70500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName206
+ LastName206
+
+ lead
+ LON Office
+
+ 70600
+
+
+
+
+
+ FirstName207
+ LastName207
+
+ specialist
+ TKY Office
+
+ 70700
+
+
+
+
+
+
+
+
+ FirstName208
+ LastName208
+
+ developer
+ BER Office
+
+ 70800
+
+
+ Employee 208 joined in 2023 and works on Project-Eng.
+
+
+
+ FirstName209
+ LastName209
+
+ manager
+ SYD Office
+
+ 70900
+
+
+
+
+
+ FirstName210
+ LastName210
+
+ analyst
+ NYC Office
+
+ 71000
+
+
+
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName211
+ LastName211
+
+ director
+ SFO Office
+
+ 71100
+
+
+
+
+
+ FirstName212
+ LastName212
+
+ intern
+ LON Office
+
+ 71200
+
+
+
+
+
+ FirstName213
+ LastName213
+
+ architect
+ TKY Office
+
+ 71300
+
+
+
+
+
+
+
+
+ FirstName214
+ LastName214
+
+ lead
+ BER Office
+
+ 71400
+
+
+
+
+
+ FirstName215
+ LastName215
+
+ specialist
+ SYD Office
+
+ 71500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName216
+ LastName216
+
+ developer
+ NYC Office
+
+ 71600
+
+
+
+
+
+ Employee 216 joined in 2021 and works on Project-Eng.
+
+
+
+ FirstName217
+ LastName217
+
+ manager
+ SFO Office
+
+ 71700
+
+
+
+
+
+ FirstName218
+ LastName218
+
+ analyst
+ LON Office
+
+ 71800
+
+
+
+
+
+ FirstName219
+ LastName219
+
+ director
+ TKY Office
+
+ 71900
+
+
+
+
+
+
+
+
+ FirstName220
+ LastName220
+
+ intern
+ BER Office
+
+ 72000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName221
+ LastName221
+
+ architect
+ SYD Office
+
+ 72100
+
+
+
+
+
+ FirstName222
+ LastName222
+
+ lead
+ NYC Office
+
+ 72200
+
+
+
+
+
+
+
+
+ FirstName223
+ LastName223
+
+ specialist
+ SFO Office
+
+ 72300
+
+
+
+
+
+ FirstName224
+ LastName224
+
+ developer
+ LON Office
+
+ 72400
+
+
+ Employee 224 joined in 2019 and works on Project-Eng.
+
+
+
+ FirstName225
+ LastName225
+
+ manager
+ TKY Office
+
+ 72500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName226
+ LastName226
+
+ analyst
+ BER Office
+
+ 72600
+
+
+
+
+
+ FirstName227
+ LastName227
+
+ director
+ SYD Office
+
+ 72700
+
+
+
+
+
+ FirstName228
+ LastName228
+
+ intern
+ NYC Office
+
+ 72800
+
+
+
+
+
+
+
+
+ FirstName229
+ LastName229
+
+ architect
+ SFO Office
+
+ 72900
+
+
+
+
+
+ FirstName230
+ LastName230
+
+ lead
+ LON Office
+
+ 73000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName231
+ LastName231
+
+ specialist
+ TKY Office
+
+ 73100
+
+
+
+
+
+
+
+
+ FirstName232
+ LastName232
+
+ developer
+ BER Office
+
+ 73200
+
+
+ Employee 232 joined in 2017 and works on Project-Eng.
+
+
+
+ FirstName233
+ LastName233
+
+ manager
+ SYD Office
+
+ 73300
+
+
+
+
+
+ FirstName234
+ LastName234
+
+ analyst
+ NYC Office
+
+ 73400
+
+
+
+
+
+
+
+
+ FirstName235
+ LastName235
+
+ director
+ SFO Office
+
+ 73500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName236
+ LastName236
+
+ intern
+ LON Office
+
+ 73600
+
+
+
+
+
+ FirstName237
+ LastName237
+
+ architect
+ TKY Office
+
+ 73700
+
+
+
+
+
+
+
+
+ FirstName238
+ LastName238
+
+ lead
+ BER Office
+
+ 73800
+
+
+
+
+
+ FirstName239
+ LastName239
+
+ specialist
+ SYD Office
+
+ 73900
+
+
+
+
+
+ FirstName240
+ LastName240
+
+ developer
+ NYC Office
+
+ 74000
+
+
+
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+ Employee 240 joined in 2015 and works on Project-Eng.
+
+
+
+ FirstName241
+ LastName241
+
+ manager
+ SFO Office
+
+ 74100
+
+
+
+
+
+ FirstName242
+ LastName242
+
+ analyst
+ LON Office
+
+ 74200
+
+
+
+
+
+ FirstName243
+ LastName243
+
+ director
+ TKY Office
+
+ 74300
+
+
+
+
+
+
+
+
+ FirstName244
+ LastName244
+
+ intern
+ BER Office
+
+ 74400
+
+
+
+
+
+ FirstName245
+ LastName245
+
+ architect
+ SYD Office
+
+ 74500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName246
+ LastName246
+
+ lead
+ NYC Office
+
+ 74600
+
+
+
+
+
+
+
+
+ FirstName247
+ LastName247
+
+ specialist
+ SFO Office
+
+ 74700
+
+
+
+
+
+ FirstName248
+ LastName248
+
+ developer
+ LON Office
+
+ 74800
+
+
+ Employee 248 joined in 2023 and works on Project-Eng.
+
+
+
+ FirstName249
+ LastName249
+
+ manager
+ TKY Office
+
+ 74900
+
+
+
+
+
+
+
+
+ FirstName250
+ LastName250
+
+ analyst
+ BER Office
+
+ 75000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName251
+ LastName251
+
+ director
+ SYD Office
+
+ 75100
+
+
+
+
+
+ FirstName252
+ LastName252
+
+ intern
+ NYC Office
+
+ 75200
+
+
+
+
+
+
+
+
+ FirstName253
+ LastName253
+
+ architect
+ SFO Office
+
+ 75300
+
+
+
+
+
+ FirstName254
+ LastName254
+
+ lead
+ LON Office
+
+ 75400
+
+
+
+
+
+ FirstName255
+ LastName255
+
+ specialist
+ TKY Office
+
+ 75500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName256
+ LastName256
+
+ developer
+ BER Office
+
+ 75600
+
+
+ Employee 256 joined in 2021 and works on Project-Eng.
+
+
+
+ FirstName257
+ LastName257
+
+ manager
+ SYD Office
+
+ 75700
+
+
+
+
+
+ FirstName258
+ LastName258
+
+ analyst
+ NYC Office
+
+ 75800
+
+
+
+
+
+
+
+
+ FirstName259
+ LastName259
+
+ director
+ SFO Office
+
+ 75900
+
+
+
+
+
+ FirstName260
+ LastName260
+
+ intern
+ LON Office
+
+ 76000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName261
+ LastName261
+
+ architect
+ TKY Office
+
+ 76100
+
+
+
+
+
+
+
+
+ FirstName262
+ LastName262
+
+ lead
+ BER Office
+
+ 76200
+
+
+
+
+
+ FirstName263
+ LastName263
+
+ specialist
+ SYD Office
+
+ 76300
+
+
+
+
+
+ FirstName264
+ LastName264
+
+ developer
+ NYC Office
+
+ 76400
+
+
+
+
+
+ Employee 264 joined in 2019 and works on Project-Eng.
+
+
+
+ FirstName265
+ LastName265
+
+ manager
+ SFO Office
+
+ 76500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName266
+ LastName266
+
+ analyst
+ LON Office
+
+ 76600
+
+
+
+
+
+ FirstName267
+ LastName267
+
+ director
+ TKY Office
+
+ 76700
+
+
+
+
+
+
+
+
+ FirstName268
+ LastName268
+
+ intern
+ BER Office
+
+ 76800
+
+
+
+
+
+ FirstName269
+ LastName269
+
+ architect
+ SYD Office
+
+ 76900
+
+
+
+
+
+ FirstName270
+ LastName270
+
+ lead
+ NYC Office
+
+ 77000
+
+
+
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName271
+ LastName271
+
+ specialist
+ SFO Office
+
+ 77100
+
+
+
+
+
+ FirstName272
+ LastName272
+
+ developer
+ LON Office
+
+ 77200
+
+
+ Employee 272 joined in 2017 and works on Project-Eng.
+
+
+
+ FirstName273
+ LastName273
+
+ manager
+ TKY Office
+
+ 77300
+
+
+
+
+
+
+
+
+ FirstName274
+ LastName274
+
+ analyst
+ BER Office
+
+ 77400
+
+
+
+
+
+ FirstName275
+ LastName275
+
+ director
+ SYD Office
+
+ 77500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName276
+ LastName276
+
+ intern
+ NYC Office
+
+ 77600
+
+
+
+
+
+
+
+
+ FirstName277
+ LastName277
+
+ architect
+ SFO Office
+
+ 77700
+
+
+
+
+
+ FirstName278
+ LastName278
+
+ lead
+ LON Office
+
+ 77800
+
+
+
+
+
+ FirstName279
+ LastName279
+
+ specialist
+ TKY Office
+
+ 77900
+
+
+
+
+
+
+
+
+ FirstName280
+ LastName280
+
+ developer
+ BER Office
+
+ 78000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+ Employee 280 joined in 2015 and works on Project-Eng.
+
+
+
+ FirstName281
+ LastName281
+
+ manager
+ SYD Office
+
+ 78100
+
+
+
+
+
+ FirstName282
+ LastName282
+
+ analyst
+ NYC Office
+
+ 78200
+
+
+
+
+
+
+
+
+ FirstName283
+ LastName283
+
+ director
+ SFO Office
+
+ 78300
+
+
+
+
+
+ FirstName284
+ LastName284
+
+ intern
+ LON Office
+
+ 78400
+
+
+
+
+
+ FirstName285
+ LastName285
+
+ architect
+ TKY Office
+
+ 78500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName286
+ LastName286
+
+ lead
+ BER Office
+
+ 78600
+
+
+
+
+
+ FirstName287
+ LastName287
+
+ specialist
+ SYD Office
+
+ 78700
+
+
+
+
+
+ FirstName288
+ LastName288
+
+ developer
+ NYC Office
+
+ 78800
+
+
+
+
+
+ Employee 288 joined in 2023 and works on Project-Eng.
+
+
+
+ FirstName289
+ LastName289
+
+ manager
+ SFO Office
+
+ 78900
+
+
+
+
+
+ FirstName290
+ LastName290
+
+ analyst
+ LON Office
+
+ 79000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName291
+ LastName291
+
+ director
+ TKY Office
+
+ 79100
+
+
+
+
+
+
+
+
+ FirstName292
+ LastName292
+
+ intern
+ BER Office
+
+ 79200
+
+
+
+
+
+ FirstName293
+ LastName293
+
+ architect
+ SYD Office
+
+ 79300
+
+
+
+
+
+ FirstName294
+ LastName294
+
+ lead
+ NYC Office
+
+ 79400
+
+
+
+
+
+
+
+
+ FirstName295
+ LastName295
+
+ specialist
+ SFO Office
+
+ 79500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName296
+ LastName296
+
+ developer
+ LON Office
+
+ 79600
+
+
+ Employee 296 joined in 2021 and works on Project-Eng.
+
+
+
+ FirstName297
+ LastName297
+
+ manager
+ TKY Office
+
+ 79700
+
+
+
+
+
+
+
+
+ FirstName298
+ LastName298
+
+ analyst
+ BER Office
+
+ 79800
+
+
+
+
+
+ FirstName299
+ LastName299
+
+ director
+ SYD Office
+
+ 79900
+
+
+
+
+
+ FirstName300
+ LastName300
+
+ intern
+ NYC Office
+
+ 80000
+
+
+
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName301
+ LastName301
+
+ architect
+ SFO Office
+
+ 80100
+
+
+
+
+
+ FirstName302
+ LastName302
+
+ lead
+ LON Office
+
+ 80200
+
+
+
+
+
+ FirstName303
+ LastName303
+
+ specialist
+ TKY Office
+
+ 80300
+
+
+
+
+
+
+
+
+ FirstName304
+ LastName304
+
+ developer
+ BER Office
+
+ 80400
+
+
+ Employee 304 joined in 2019 and works on Project-Eng.
+
+
+
+ FirstName305
+ LastName305
+
+ manager
+ SYD Office
+
+ 80500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName306
+ LastName306
+
+ analyst
+ NYC Office
+
+ 80600
+
+
+
+
+
+
+
+
+ FirstName307
+ LastName307
+
+ director
+ SFO Office
+
+ 80700
+
+
+
+
+
+ FirstName308
+ LastName308
+
+ intern
+ LON Office
+
+ 80800
+
+
+
+
+
+ FirstName309
+ LastName309
+
+ architect
+ TKY Office
+
+ 80900
+
+
+
+
+
+
+
+
+ FirstName310
+ LastName310
+
+ lead
+ BER Office
+
+ 81000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName311
+ LastName311
+
+ specialist
+ SYD Office
+
+ 81100
+
+
+
+
+
+ FirstName312
+ LastName312
+
+ developer
+ NYC Office
+
+ 81200
+
+
+
+
+
+ Employee 312 joined in 2017 and works on Project-Eng.
+
+
+
+ FirstName313
+ LastName313
+
+ manager
+ SFO Office
+
+ 81300
+
+
+
+
+
+ FirstName314
+ LastName314
+
+ analyst
+ LON Office
+
+ 81400
+
+
+
+
+
+ FirstName315
+ LastName315
+
+ director
+ TKY Office
+
+ 81500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName316
+ LastName316
+
+ intern
+ BER Office
+
+ 81600
+
+
+
+
+
+ FirstName317
+ LastName317
+
+ architect
+ SYD Office
+
+ 81700
+
+
+
+
+
+ FirstName318
+ LastName318
+
+ lead
+ NYC Office
+
+ 81800
+
+
+
+
+
+
+
+
+ FirstName319
+ LastName319
+
+ specialist
+ SFO Office
+
+ 81900
+
+
+
+
+
+ FirstName320
+ LastName320
+
+ developer
+ LON Office
+
+ 82000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+ Employee 320 joined in 2015 and works on Project-Eng.
+
+
+
+ FirstName321
+ LastName321
+
+ manager
+ TKY Office
+
+ 82100
+
+
+
+
+
+
+
+
+ FirstName322
+ LastName322
+
+ analyst
+ BER Office
+
+ 82200
+
+
+
+
+
+ FirstName323
+ LastName323
+
+ director
+ SYD Office
+
+ 82300
+
+
+
+
+
+ FirstName324
+ LastName324
+
+ intern
+ NYC Office
+
+ 82400
+
+
+
+
+
+
+
+
+ FirstName325
+ LastName325
+
+ architect
+ SFO Office
+
+ 82500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName326
+ LastName326
+
+ lead
+ LON Office
+
+ 82600
+
+
+
+
+
+ FirstName327
+ LastName327
+
+ specialist
+ TKY Office
+
+ 82700
+
+
+
+
+
+
+
+
+ FirstName328
+ LastName328
+
+ developer
+ BER Office
+
+ 82800
+
+
+ Employee 328 joined in 2023 and works on Project-Eng.
+
+
+
+ FirstName329
+ LastName329
+
+ manager
+ SYD Office
+
+ 82900
+
+
+
+
+
+ FirstName330
+ LastName330
+
+ analyst
+ NYC Office
+
+ 83000
+
+
+
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName331
+ LastName331
+
+ director
+ SFO Office
+
+ 83100
+
+
+
+
+
+ FirstName332
+ LastName332
+
+ intern
+ LON Office
+
+ 83200
+
+
+
+
+
+ FirstName333
+ LastName333
+
+ architect
+ TKY Office
+
+ 83300
+
+
+
+
+
+
+
+
+ FirstName334
+ LastName334
+
+ lead
+ BER Office
+
+ 83400
+
+
+
+
+
+ FirstName335
+ LastName335
+
+ specialist
+ SYD Office
+
+ 83500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName336
+ LastName336
+
+ developer
+ NYC Office
+
+ 83600
+
+
+
+
+
+ Employee 336 joined in 2021 and works on Project-Eng.
+
+
+
+ FirstName337
+ LastName337
+
+ manager
+ SFO Office
+
+ 83700
+
+
+
+
+
+ FirstName338
+ LastName338
+
+ analyst
+ LON Office
+
+ 83800
+
+
+
+
+
+ FirstName339
+ LastName339
+
+ director
+ TKY Office
+
+ 83900
+
+
+
+
+
+
+
+
+ FirstName340
+ LastName340
+
+ intern
+ BER Office
+
+ 84000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName341
+ LastName341
+
+ architect
+ SYD Office
+
+ 84100
+
+
+
+
+
+ FirstName342
+ LastName342
+
+ lead
+ NYC Office
+
+ 84200
+
+
+
+
+
+
+
+
+ FirstName343
+ LastName343
+
+ specialist
+ SFO Office
+
+ 84300
+
+
+
+
+
+ FirstName344
+ LastName344
+
+ developer
+ LON Office
+
+ 84400
+
+
+ Employee 344 joined in 2019 and works on Project-Eng.
+
+
+
+ FirstName345
+ LastName345
+
+ manager
+ TKY Office
+
+ 84500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName346
+ LastName346
+
+ analyst
+ BER Office
+
+ 84600
+
+
+
+
+
+ FirstName347
+ LastName347
+
+ director
+ SYD Office
+
+ 84700
+
+
+
+
+
+ FirstName348
+ LastName348
+
+ intern
+ NYC Office
+
+ 84800
+
+
+
+
+
+
+
+
+ FirstName349
+ LastName349
+
+ architect
+ SFO Office
+
+ 84900
+
+
+
+
+
+ FirstName350
+ LastName350
+
+ lead
+ LON Office
+
+ 85000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName351
+ LastName351
+
+ specialist
+ TKY Office
+
+ 85100
+
+
+
+
+
+
+
+
+ FirstName352
+ LastName352
+
+ developer
+ BER Office
+
+ 85200
+
+
+ Employee 352 joined in 2017 and works on Project-Eng.
+
+
+
+ FirstName353
+ LastName353
+
+ manager
+ SYD Office
+
+ 85300
+
+
+
+
+
+ FirstName354
+ LastName354
+
+ analyst
+ NYC Office
+
+ 85400
+
+
+
+
+
+
+
+
+ FirstName355
+ LastName355
+
+ director
+ SFO Office
+
+ 85500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName356
+ LastName356
+
+ intern
+ LON Office
+
+ 85600
+
+
+
+
+
+ FirstName357
+ LastName357
+
+ architect
+ TKY Office
+
+ 85700
+
+
+
+
+
+
+
+
+ FirstName358
+ LastName358
+
+ lead
+ BER Office
+
+ 85800
+
+
+
+
+
+ FirstName359
+ LastName359
+
+ specialist
+ SYD Office
+
+ 85900
+
+
+
+
+
+ FirstName360
+ LastName360
+
+ developer
+ NYC Office
+
+ 86000
+
+
+
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+ Employee 360 joined in 2015 and works on Project-Eng.
+
+
+
+ FirstName361
+ LastName361
+
+ manager
+ SFO Office
+
+ 86100
+
+
+
+
+
+ FirstName362
+ LastName362
+
+ analyst
+ LON Office
+
+ 86200
+
+
+
+
+
+ FirstName363
+ LastName363
+
+ director
+ TKY Office
+
+ 86300
+
+
+
+
+
+
+
+
+ FirstName364
+ LastName364
+
+ intern
+ BER Office
+
+ 86400
+
+
+
+
+
+ FirstName365
+ LastName365
+
+ architect
+ SYD Office
+
+ 86500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName366
+ LastName366
+
+ lead
+ NYC Office
+
+ 86600
+
+
+
+
+
+
+
+
+ FirstName367
+ LastName367
+
+ specialist
+ SFO Office
+
+ 86700
+
+
+
+
+
+ FirstName368
+ LastName368
+
+ developer
+ LON Office
+
+ 86800
+
+
+ Employee 368 joined in 2023 and works on Project-Eng.
+
+
+
+ FirstName369
+ LastName369
+
+ manager
+ TKY Office
+
+ 86900
+
+
+
+
+
+
+
+
+ FirstName370
+ LastName370
+
+ analyst
+ BER Office
+
+ 87000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName371
+ LastName371
+
+ director
+ SYD Office
+
+ 87100
+
+
+
+
+
+ FirstName372
+ LastName372
+
+ intern
+ NYC Office
+
+ 87200
+
+
+
+
+
+
+
+
+ FirstName373
+ LastName373
+
+ architect
+ SFO Office
+
+ 87300
+
+
+
+
+
+ FirstName374
+ LastName374
+
+ lead
+ LON Office
+
+ 87400
+
+
+
+
+
+ FirstName375
+ LastName375
+
+ specialist
+ TKY Office
+
+ 87500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName376
+ LastName376
+
+ developer
+ BER Office
+
+ 87600
+
+
+ Employee 376 joined in 2021 and works on Project-Eng.
+
+
+
+ FirstName377
+ LastName377
+
+ manager
+ SYD Office
+
+ 87700
+
+
+
+
+
+ FirstName378
+ LastName378
+
+ analyst
+ NYC Office
+
+ 87800
+
+
+
+
+
+
+
+
+ FirstName379
+ LastName379
+
+ director
+ SFO Office
+
+ 87900
+
+
+
+
+
+ FirstName380
+ LastName380
+
+ intern
+ LON Office
+
+ 88000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName381
+ LastName381
+
+ architect
+ TKY Office
+
+ 88100
+
+
+
+
+
+
+
+
+ FirstName382
+ LastName382
+
+ lead
+ BER Office
+
+ 88200
+
+
+
+
+
+ FirstName383
+ LastName383
+
+ specialist
+ SYD Office
+
+ 88300
+
+
+
+
+
+ FirstName384
+ LastName384
+
+ developer
+ NYC Office
+
+ 88400
+
+
+
+
+
+ Employee 384 joined in 2019 and works on Project-Eng.
+
+
+
+ FirstName385
+ LastName385
+
+ manager
+ SFO Office
+
+ 88500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName386
+ LastName386
+
+ analyst
+ LON Office
+
+ 88600
+
+
+
+
+
+ FirstName387
+ LastName387
+
+ director
+ TKY Office
+
+ 88700
+
+
+
+
+
+
+
+
+ FirstName388
+ LastName388
+
+ intern
+ BER Office
+
+ 88800
+
+
+
+
+
+ FirstName389
+ LastName389
+
+ architect
+ SYD Office
+
+ 88900
+
+
+
+
+
+ FirstName390
+ LastName390
+
+ lead
+ NYC Office
+
+ 89000
+
+
+
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName391
+ LastName391
+
+ specialist
+ SFO Office
+
+ 89100
+
+
+
+
+
+ FirstName392
+ LastName392
+
+ developer
+ LON Office
+
+ 89200
+
+
+ Employee 392 joined in 2017 and works on Project-Eng.
+
+
+
+ FirstName393
+ LastName393
+
+ manager
+ TKY Office
+
+ 89300
+
+
+
+
+
+
+
+
+ FirstName394
+ LastName394
+
+ analyst
+ BER Office
+
+ 89400
+
+
+
+
+
+ FirstName395
+ LastName395
+
+ director
+ SYD Office
+
+ 89500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName396
+ LastName396
+
+ intern
+ NYC Office
+
+ 89600
+
+
+
+
+
+
+
+
+ FirstName397
+ LastName397
+
+ architect
+ SFO Office
+
+ 89700
+
+
+
+
+
+ FirstName398
+ LastName398
+
+ lead
+ LON Office
+
+ 89800
+
+
+
+
+
+ FirstName399
+ LastName399
+
+ specialist
+ TKY Office
+
+ 89900
+
+
+
+
+
+
+
+
+ FirstName400
+ LastName400
+
+ developer
+ BER Office
+
+ 90000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+ Employee 400 joined in 2015 and works on Project-Eng.
+
+
+
+ FirstName401
+ LastName401
+
+ manager
+ SYD Office
+
+ 90100
+
+
+
+
+
+ FirstName402
+ LastName402
+
+ analyst
+ NYC Office
+
+ 90200
+
+
+
+
+
+
+
+
+ FirstName403
+ LastName403
+
+ director
+ SFO Office
+
+ 90300
+
+
+
+
+
+ FirstName404
+ LastName404
+
+ intern
+ LON Office
+
+ 90400
+
+
+
+
+
+ FirstName405
+ LastName405
+
+ architect
+ TKY Office
+
+ 90500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName406
+ LastName406
+
+ lead
+ BER Office
+
+ 90600
+
+
+
+
+
+ FirstName407
+ LastName407
+
+ specialist
+ SYD Office
+
+ 90700
+
+
+
+
+
+ FirstName408
+ LastName408
+
+ developer
+ NYC Office
+
+ 90800
+
+
+
+
+
+ Employee 408 joined in 2023 and works on Project-Eng.
+
+
+
+ FirstName409
+ LastName409
+
+ manager
+ SFO Office
+
+ 90900
+
+
+
+
+
+ FirstName410
+ LastName410
+
+ analyst
+ LON Office
+
+ 91000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName411
+ LastName411
+
+ director
+ TKY Office
+
+ 91100
+
+
+
+
+
+
+
+
+ FirstName412
+ LastName412
+
+ intern
+ BER Office
+
+ 91200
+
+
+
+
+
+ FirstName413
+ LastName413
+
+ architect
+ SYD Office
+
+ 91300
+
+
+
+
+
+ FirstName414
+ LastName414
+
+ lead
+ NYC Office
+
+ 91400
+
+
+
+
+
+
+
+
+ FirstName415
+ LastName415
+
+ specialist
+ SFO Office
+
+ 91500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName416
+ LastName416
+
+ developer
+ LON Office
+
+ 91600
+
+
+ Employee 416 joined in 2021 and works on Project-Eng.
+
+
+
+ FirstName417
+ LastName417
+
+ manager
+ TKY Office
+
+ 91700
+
+
+
+
+
+
+
+
+ FirstName418
+ LastName418
+
+ analyst
+ BER Office
+
+ 91800
+
+
+
+
+
+ FirstName419
+ LastName419
+
+ director
+ SYD Office
+
+ 91900
+
+
+
+
+
+ FirstName420
+ LastName420
+
+ intern
+ NYC Office
+
+ 92000
+
+
+
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName421
+ LastName421
+
+ architect
+ SFO Office
+
+ 92100
+
+
+
+
+
+ FirstName422
+ LastName422
+
+ lead
+ LON Office
+
+ 92200
+
+
+
+
+
+ FirstName423
+ LastName423
+
+ specialist
+ TKY Office
+
+ 92300
+
+
+
+
+
+
+
+
+ FirstName424
+ LastName424
+
+ developer
+ BER Office
+
+ 92400
+
+
+ Employee 424 joined in 2019 and works on Project-Eng.
+
+
+
+ FirstName425
+ LastName425
+
+ manager
+ SYD Office
+
+ 92500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName426
+ LastName426
+
+ analyst
+ NYC Office
+
+ 92600
+
+
+
+
+
+
+
+
+ FirstName427
+ LastName427
+
+ director
+ SFO Office
+
+ 92700
+
+
+
+
+
+ FirstName428
+ LastName428
+
+ intern
+ LON Office
+
+ 92800
+
+
+
+
+
+ FirstName429
+ LastName429
+
+ architect
+ TKY Office
+
+ 92900
+
+
+
+
+
+
+
+
+ FirstName430
+ LastName430
+
+ lead
+ BER Office
+
+ 93000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName431
+ LastName431
+
+ specialist
+ SYD Office
+
+ 93100
+
+
+
+
+
+ FirstName432
+ LastName432
+
+ developer
+ NYC Office
+
+ 93200
+
+
+
+
+
+ Employee 432 joined in 2017 and works on Project-Eng.
+
+
+
+ FirstName433
+ LastName433
+
+ manager
+ SFO Office
+
+ 93300
+
+
+
+
+
+ FirstName434
+ LastName434
+
+ analyst
+ LON Office
+
+ 93400
+
+
+
+
+
+ FirstName435
+ LastName435
+
+ director
+ TKY Office
+
+ 93500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName436
+ LastName436
+
+ intern
+ BER Office
+
+ 93600
+
+
+
+
+
+ FirstName437
+ LastName437
+
+ architect
+ SYD Office
+
+ 93700
+
+
+
+
+
+ FirstName438
+ LastName438
+
+ lead
+ NYC Office
+
+ 93800
+
+
+
+
+
+
+
+
+ FirstName439
+ LastName439
+
+ specialist
+ SFO Office
+
+ 93900
+
+
+
+
+
+ FirstName440
+ LastName440
+
+ developer
+ LON Office
+
+ 94000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+ Employee 440 joined in 2015 and works on Project-Eng.
+
+
+
+ FirstName441
+ LastName441
+
+ manager
+ TKY Office
+
+ 94100
+
+
+
+
+
+
+
+
+ FirstName442
+ LastName442
+
+ analyst
+ BER Office
+
+ 94200
+
+
+
+
+
+ FirstName443
+ LastName443
+
+ director
+ SYD Office
+
+ 94300
+
+
+
+
+
+ FirstName444
+ LastName444
+
+ intern
+ NYC Office
+
+ 94400
+
+
+
+
+
+
+
+
+ FirstName445
+ LastName445
+
+ architect
+ SFO Office
+
+ 94500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName446
+ LastName446
+
+ lead
+ LON Office
+
+ 94600
+
+
+
+
+
+ FirstName447
+ LastName447
+
+ specialist
+ TKY Office
+
+ 94700
+
+
+
+
+
+
+
+
+ FirstName448
+ LastName448
+
+ developer
+ BER Office
+
+ 94800
+
+
+ Employee 448 joined in 2023 and works on Project-Eng.
+
+
+
+ FirstName449
+ LastName449
+
+ manager
+ SYD Office
+
+ 94900
+
+
+
+
+
+ FirstName450
+ LastName450
+
+ analyst
+ NYC Office
+
+ 95000
+
+
+
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName451
+ LastName451
+
+ director
+ SFO Office
+
+ 95100
+
+
+
+
+
+ FirstName452
+ LastName452
+
+ intern
+ LON Office
+
+ 95200
+
+
+
+
+
+ FirstName453
+ LastName453
+
+ architect
+ TKY Office
+
+ 95300
+
+
+
+
+
+
+
+
+ FirstName454
+ LastName454
+
+ lead
+ BER Office
+
+ 95400
+
+
+
+
+
+ FirstName455
+ LastName455
+
+ specialist
+ SYD Office
+
+ 95500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName456
+ LastName456
+
+ developer
+ NYC Office
+
+ 95600
+
+
+
+
+
+ Employee 456 joined in 2021 and works on Project-Eng.
+
+
+
+ FirstName457
+ LastName457
+
+ manager
+ SFO Office
+
+ 95700
+
+
+
+
+
+ FirstName458
+ LastName458
+
+ analyst
+ LON Office
+
+ 95800
+
+
+
+
+
+ FirstName459
+ LastName459
+
+ director
+ TKY Office
+
+ 95900
+
+
+
+
+
+
+
+
+ FirstName460
+ LastName460
+
+ intern
+ BER Office
+
+ 96000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName461
+ LastName461
+
+ architect
+ SYD Office
+
+ 96100
+
+
+
+
+
+ FirstName462
+ LastName462
+
+ lead
+ NYC Office
+
+ 96200
+
+
+
+
+
+
+
+
+ FirstName463
+ LastName463
+
+ specialist
+ SFO Office
+
+ 96300
+
+
+
+
+
+ FirstName464
+ LastName464
+
+ developer
+ LON Office
+
+ 96400
+
+
+ Employee 464 joined in 2019 and works on Project-Eng.
+
+
+
+ FirstName465
+ LastName465
+
+ manager
+ TKY Office
+
+ 96500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName466
+ LastName466
+
+ analyst
+ BER Office
+
+ 96600
+
+
+
+
+
+ FirstName467
+ LastName467
+
+ director
+ SYD Office
+
+ 96700
+
+
+
+
+
+ FirstName468
+ LastName468
+
+ intern
+ NYC Office
+
+ 96800
+
+
+
+
+
+
+
+
+ FirstName469
+ LastName469
+
+ architect
+ SFO Office
+
+ 96900
+
+
+
+
+
+ FirstName470
+ LastName470
+
+ lead
+ LON Office
+
+ 97000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName471
+ LastName471
+
+ specialist
+ TKY Office
+
+ 97100
+
+
+
+
+
+
+
+
+ FirstName472
+ LastName472
+
+ developer
+ BER Office
+
+ 97200
+
+
+ Employee 472 joined in 2017 and works on Project-Eng.
+
+
+
+ FirstName473
+ LastName473
+
+ manager
+ SYD Office
+
+ 97300
+
+
+
+
+
+ FirstName474
+ LastName474
+
+ analyst
+ NYC Office
+
+ 97400
+
+
+
+
+
+
+
+
+ FirstName475
+ LastName475
+
+ director
+ SFO Office
+
+ 97500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName476
+ LastName476
+
+ intern
+ LON Office
+
+ 97600
+
+
+
+
+
+ FirstName477
+ LastName477
+
+ architect
+ TKY Office
+
+ 97700
+
+
+
+
+
+
+
+
+ FirstName478
+ LastName478
+
+ lead
+ BER Office
+
+ 97800
+
+
+
+
+
+ FirstName479
+ LastName479
+
+ specialist
+ SYD Office
+
+ 97900
+
+
+
+
+
+ FirstName480
+ LastName480
+
+ developer
+ NYC Office
+
+ 98000
+
+
+
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+ Employee 480 joined in 2015 and works on Project-Eng.
+
+
+
+ FirstName481
+ LastName481
+
+ manager
+ SFO Office
+
+ 98100
+
+
+
+
+
+ FirstName482
+ LastName482
+
+ analyst
+ LON Office
+
+ 98200
+
+
+
+
+
+ FirstName483
+ LastName483
+
+ director
+ TKY Office
+
+ 98300
+
+
+
+
+
+
+
+
+ FirstName484
+ LastName484
+
+ intern
+ BER Office
+
+ 98400
+
+
+
+
+
+ FirstName485
+ LastName485
+
+ architect
+ SYD Office
+
+ 98500
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName486
+ LastName486
+
+ lead
+ NYC Office
+
+ 98600
+
+
+
+
+
+
+
+
+ FirstName487
+ LastName487
+
+ specialist
+ SFO Office
+
+ 98700
+
+
+
+
+
+ FirstName488
+ LastName488
+
+ developer
+ LON Office
+
+ 98800
+
+
+ Employee 488 joined in 2023 and works on Project-Eng.
+
+
+
+ FirstName489
+ LastName489
+
+ manager
+ TKY Office
+
+ 98900
+
+
+
+
+
+
+
+
+ FirstName490
+ LastName490
+
+ analyst
+ BER Office
+
+ 99000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
+
+ FirstName491
+ LastName491
+
+ director
+ SYD Office
+
+ 99100
+
+
+
+
+
+ FirstName492
+ LastName492
+
+ intern
+ NYC Office
+
+ 99200
+
+
+
+
+
+
+
+
+ FirstName493
+ LastName493
+
+ architect
+ SFO Office
+
+ 99300
+
+
+
+
+
+ FirstName494
+ LastName494
+
+ lead
+ LON Office
+
+ 99400
+
+
+
+
+
+ FirstName495
+ LastName495
+
+ specialist
+ TKY Office
+
+ 99500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FirstName496
+ LastName496
+
+ developer
+ BER Office
+
+ 99600
+
+
+ Employee 496 joined in 2021 and works on Project-Eng.
+
+
+
+ FirstName497
+ LastName497
+
+ manager
+ SYD Office
+
+ 99700
+
+
+
+
+
+ FirstName498
+ LastName498
+
+ analyst
+ NYC Office
+
+ 99800
+
+
+
+
+
+
+
+
+ FirstName499
+ LastName499
+
+ director
+ SFO Office
+
+ 99900
+
+
+
+
+
+ FirstName500
+ LastName500
+
+ intern
+ LON Office
+
+ 100000
+
+
+
+
+
+
+
+
+
+ data & "sensitive" info.]]>
+
+
diff --git a/experimental/plugins/collections/testdata/medium_deep.xml b/experimental/plugins/collections/testdata/medium_deep.xml
new file mode 100644
index 000000000..e1a4f49a5
--- /dev/null
+++ b/experimental/plugins/collections/testdata/medium_deep.xml
@@ -0,0 +1,136 @@
+
+
+
+
+ Level 1 Node
+ Value at depth 1
+
+ Level 2 Node
+ Value at depth 2
+
+ Level 3 Node
+ Value at depth 3
+
+ Level 4 Node
+ Value at depth 4
+
+ Level 5 Node
+ Value at depth 5
+ Sibling A at depth 5
+ Sibling B at depth 5
+
+ Level 6 Node
+ Value at depth 6
+
+ Level 7 Node
+ Value at depth 7
+
+ Level 8 Node
+ Value at depth 8
+
+ Level 9 Node
+ Value at depth 9
+
+ Level 10 Node
+ Value at depth 10
+ Sibling A at depth 10
+ Sibling B at depth 10
+
+ Level 11 Node
+ Value at depth 11
+
+ Level 12 Node
+ Value at depth 12
+
+ Level 13 Node
+ Value at depth 13
+
+ Level 14 Node
+ Value at depth 14
+
+ Level 15 Node
+ Value at depth 15
+ Sibling A at depth 15
+ Sibling B at depth 15
+
+ Level 16 Node
+ Value at depth 16
+
+ Level 17 Node
+ Value at depth 17
+
+ Level 18 Node
+ Value at depth 18
+
+ Level 19 Node
+ Value at depth 19
+
+ Level 20 Node
+ Value at depth 20
+ Sibling A at depth 20
+ Sibling B at depth 20
+
+ Level 21 Node
+ Value at depth 21
+
+ Level 22 Node
+ Value at depth 22
+
+ Level 23 Node
+ Value at depth 23
+
+ Level 24 Node
+ Value at depth 24
+
+ Level 25 Node
+ Value at depth 25
+ Sibling A at depth 25
+ Sibling B at depth 25
+
+ Level 26 Node
+ Value at depth 26
+
+ Level 27 Node
+ Value at depth 27
+
+ Level 28 Node
+ Value at depth 28
+
+ Level 29 Node
+ Value at depth 29
+
+ Level 30 Node
+ Value at depth 30
+ Sibling A at depth 30
+ Sibling B at depth 30
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/experimental/plugins/collections/testdata/medium_mixed.xml b/experimental/plugins/collections/testdata/medium_mixed.xml
new file mode 100644
index 000000000..08bdfddf0
--- /dev/null
+++ b/experimental/plugins/collections/testdata/medium_mixed.xml
@@ -0,0 +1,520 @@
+
+
+
+
+
+ Book Title Number 1
+ Author 2
+ 978-0-0001001-1
+ 11.99
+
+
+ Book Title Number 2
+ Author 3
+ 978-0-0002002-2
+ 12.99
+
+
+ Book Title Number 3
+ Author 4
+ 978-0-0003003-3
+ 13.99
+ A fascinating exploration of tech topics in volume 3.
+
+
+ Book Title Number 4
+ Author 5
+ 978-0-0004004-4
+ 14.99
+
+
+ User41
+ Review comment 1 for book 4
+
+
+ User42
+ Review comment 2 for book 4
+
+
+ User43
+ Review comment 3 for book 4
+
+
+
+
+ Book Title Number 5
+ Author 6
+ 978-0-0005005-5
+ 15.99
+ characters & "quotes" that need CDATA.]]>
+
+
+ Book Title Number 6
+ Author 7
+ 978-0-0006006-6
+ 16.99
+ A fascinating exploration of science topics in volume 6.
+
+
+ Book Title Number 7
+ Author 8
+ 978-0-0007007-7
+ 17.99
+
+
+
+
+
+ Book Title Number 8
+ Author 9
+ 978-0-0008008-8
+ 18.99
+
+
+ User81
+ Review comment 1 for book 8
+
+
+ User82
+ Review comment 2 for book 8
+
+
+ User83
+ Review comment 3 for book 8
+
+
+
+
+ Book Title Number 9
+ Author 10
+ 978-0-0009009-9
+ 19.99
+ A fascinating exploration of art topics in volume 9.
+
+
+ Book Title Number 10
+ Author 11
+ 978-0-0010010-0
+ 20.99
+ characters & "quotes" that need CDATA.]]>
+
+
+ Book Title Number 11
+ Author 12
+ 978-0-0011011-1
+ 21.99
+
+
+ Book Title Number 12
+ Author 13
+ 978-0-0012012-2
+ 22.99
+ A fascinating exploration of history topics in volume 12.
+
+
+ User121
+ Review comment 1 for book 12
+
+
+ User122
+ Review comment 2 for book 12
+
+
+ User123
+ Review comment 3 for book 12
+
+
+
+
+ Book Title Number 13
+ Author 14
+ 978-0-0013013-3
+ 23.99
+
+
+ Book Title Number 14
+ Author 15
+ 978-0-0014014-4
+ 24.99
+
+
+
+
+
+ Book Title Number 15
+ Author 16
+ 978-0-0015015-5
+ 25.99
+ characters & "quotes" that need CDATA.]]>
+
+
+ Book Title Number 16
+ Author 17
+ 978-0-0016016-6
+ 26.99
+
+
+ User161
+ Review comment 1 for book 16
+
+
+ User162
+ Review comment 2 for book 16
+
+
+ User163
+ Review comment 3 for book 16
+
+
+
+
+ Book Title Number 17
+ Author 18
+ 978-0-0017017-7
+ 27.99
+
+
+ Book Title Number 18
+ Author 19
+ 978-0-0018018-8
+ 28.99
+ A fascinating exploration of tech topics in volume 18.
+
+
+ Book Title Number 19
+ Author 20
+ 978-0-0019019-9
+ 29.99
+
+
+ Book Title Number 20
+ Author 1
+ 978-0-0020020-0
+ 30.99
+ characters & "quotes" that need CDATA.]]>
+
+
+ User201
+ Review comment 1 for book 20
+
+
+ User202
+ Review comment 2 for book 20
+
+
+ User203
+ Review comment 3 for book 20
+
+
+
+
+ Book Title Number 21
+ Author 2
+ 978-0-0021021-1
+ 31.99
+ A fascinating exploration of science topics in volume 21.
+
+
+
+
+
+ Book Title Number 22
+ Author 3
+ 978-0-0022022-2
+ 32.99
+
+
+ Book Title Number 23
+ Author 4
+ 978-0-0023023-3
+ 33.99
+
+
+ Book Title Number 24
+ Author 5
+ 978-0-0024024-4
+ 34.99
+ A fascinating exploration of art topics in volume 24.
+
+
+ User241
+ Review comment 1 for book 24
+
+
+ User242
+ Review comment 2 for book 24
+
+
+ User243
+ Review comment 3 for book 24
+
+
+
+
+ Book Title Number 25
+ Author 6
+ 978-0-0025025-5
+ 35.99
+ characters & "quotes" that need CDATA.]]>
+
+
+ Book Title Number 26
+ Author 7
+ 978-0-0026026-6
+ 36.99
+
+
+ Book Title Number 27
+ Author 8
+ 978-0-0027027-7
+ 37.99
+ A fascinating exploration of history topics in volume 27.
+
+
+ Book Title Number 28
+ Author 9
+ 978-0-0028028-8
+ 38.99
+
+
+ User281
+ Review comment 1 for book 28
+
+
+ User282
+ Review comment 2 for book 28
+
+
+ User283
+ Review comment 3 for book 28
+
+
+
+
+
+
+
+ Book Title Number 29
+ Author 10
+ 978-0-0029029-9
+ 39.99
+
+
+ Book Title Number 30
+ Author 11
+ 978-0-0030030-0
+ 40.99
+ characters & "quotes" that need CDATA.]]>
+
+
+ Book Title Number 31
+ Author 12
+ 978-0-0031031-1
+ 41.99
+
+
+ Book Title Number 32
+ Author 13
+ 978-0-0032032-2
+ 42.99
+
+
+ User321
+ Review comment 1 for book 32
+
+
+ User322
+ Review comment 2 for book 32
+
+
+ User323
+ Review comment 3 for book 32
+
+
+
+
+ Book Title Number 33
+ Author 14
+ 978-0-0033033-3
+ 43.99
+ A fascinating exploration of tech topics in volume 33.
+
+
+ Book Title Number 34
+ Author 15
+ 978-0-0034034-4
+ 44.99
+
+
+ Book Title Number 35
+ Author 16
+ 978-0-0035035-5
+ 45.99
+ characters & "quotes" that need CDATA.]]>
+
+
+
+
+
+ Book Title Number 36
+ Author 17
+ 978-0-0036036-6
+ 46.99
+ A fascinating exploration of science topics in volume 36.
+
+
+ User361
+ Review comment 1 for book 36
+
+
+ User362
+ Review comment 2 for book 36
+
+
+ User363
+ Review comment 3 for book 36
+
+
+
+
+ Book Title Number 37
+ Author 18
+ 978-0-0037037-7
+ 47.99
+
+
+ Book Title Number 38
+ Author 19
+ 978-0-0038038-8
+ 48.99
+
+
+ Book Title Number 39
+ Author 20
+ 978-0-0039039-9
+ 49.99
+ A fascinating exploration of art topics in volume 39.
+
+
+ Book Title Number 40
+ Author 1
+ 978-0-0040040-0
+ 50.99
+ characters & "quotes" that need CDATA.]]>
+
+
+ User401
+ Review comment 1 for book 40
+
+
+ User402
+ Review comment 2 for book 40
+
+
+ User403
+ Review comment 3 for book 40
+
+
+
+
+ Book Title Number 41
+ Author 2
+ 978-0-0041041-1
+ 51.99
+
+
+ Book Title Number 42
+ Author 3
+ 978-0-0042042-2
+ 52.99
+ A fascinating exploration of history topics in volume 42.
+
+
+
+
+
+ Book Title Number 43
+ Author 4
+ 978-0-0043043-3
+ 53.99
+
+
+ Book Title Number 44
+ Author 5
+ 978-0-0044044-4
+ 54.99
+
+
+ User441
+ Review comment 1 for book 44
+
+
+ User442
+ Review comment 2 for book 44
+
+
+ User443
+ Review comment 3 for book 44
+
+
+
+
+ Book Title Number 45
+ Author 6
+ 978-0-0045045-5
+ 55.99
+ characters & "quotes" that need CDATA.]]>
+
+
+ Book Title Number 46
+ Author 7
+ 978-0-0046046-6
+ 56.99
+
+
+ Book Title Number 47
+ Author 8
+ 978-0-0047047-7
+ 57.99
+
+
+ Book Title Number 48
+ Author 9
+ 978-0-0048048-8
+ 58.99
+ A fascinating exploration of tech topics in volume 48.
+
+
+ User481
+ Review comment 1 for book 48
+
+
+ User482
+ Review comment 2 for book 48
+
+
+ User483
+ Review comment 3 for book 48
+
+
+
+
+ Book Title Number 49
+ Author 10
+ 978-0-0049049-9
+ 59.99
+
+
+
+
+
+ Book Title Number 50
+ Author 11
+ 978-0-0050050-0
+ 60.99
+ characters & "quotes" that need CDATA.]]>
+
+
diff --git a/experimental/plugins/collections/testdata/small_cdata.xml b/experimental/plugins/collections/testdata/small_cdata.xml
new file mode 100644
index 000000000..8ec44651b
--- /dev/null
+++ b/experimental/plugins/collections/testdata/small_cdata.xml
@@ -0,0 +1,11 @@
+
+
+
+ alice@example.com
+ world & "friends"!]]>
+
+
+ bob@example.com
+ Plain text message
+
+
diff --git a/experimental/plugins/collections/testdata/small_simple.xml b/experimental/plugins/collections/testdata/small_simple.xml
new file mode 100644
index 000000000..7cb8b4956
--- /dev/null
+++ b/experimental/plugins/collections/testdata/small_simple.xml
@@ -0,0 +1,5 @@
+
+
+ - alpha
+ - beta
+
diff --git a/experimental/plugins/collections/testdata/small_soap.xml b/experimental/plugins/collections/testdata/small_soap.xml
new file mode 100644
index 000000000..98635fde0
--- /dev/null
+++ b/experimental/plugins/collections/testdata/small_soap.xml
@@ -0,0 +1,12 @@
+
+
+
+ secret-token-123
+
+
+
+ GOOG
+
+
+
diff --git a/experimental/plugins/collections/testdata/small_xmlrpc.xml b/experimental/plugins/collections/testdata/small_xmlrpc.xml
new file mode 100644
index 000000000..d4283b0d3
--- /dev/null
+++ b/experimental/plugins/collections/testdata/small_xmlrpc.xml
@@ -0,0 +1,10 @@
+
+
+ wp.getUsersBlogs
+
+ admin
+ password123
+ 42
+ 1
+
+
diff --git a/experimental/plugins/collections/xpath_map.go b/experimental/plugins/collections/xpath_map.go
new file mode 100644
index 000000000..3e4a333a7
--- /dev/null
+++ b/experimental/plugins/collections/xpath_map.go
@@ -0,0 +1,139 @@
+// Copyright 2026 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package collections
+
+import (
+ "regexp"
+ "strings"
+
+ "github.com/antchfx/xmlquery"
+
+ "github.com/corazawaf/coraza/v3/collection"
+ "github.com/corazawaf/coraza/v3/internal/corazarules"
+ "github.com/corazawaf/coraza/v3/types"
+ "github.com/corazawaf/coraza/v3/types/variables"
+)
+
+// XPathMap implements collection.Map with lazy XPath evaluation against
+// a parsed XML document. XPath expressions are evaluated on demand when
+// FindString, FindRegex, or Get are called.
+type XPathMap struct {
+ doc *xmlquery.Node
+ variable variables.RuleVariable
+}
+
+var _ collection.Map = &XPathMap{}
+
+// NewXPathMap creates a new XPathMap backed by the given XML document.
+func NewXPathMap(variable variables.RuleVariable, doc *xmlquery.Node) *XPathMap {
+ return &XPathMap{
+ doc: doc,
+ variable: variable,
+ }
+}
+
+// evalXPath evaluates an XPath expression against the document and returns
+// the string values of matching nodes.
+func (c *XPathMap) evalXPath(expr string) []string {
+ if c.doc == nil {
+ return nil
+ }
+ nodes, err := xmlquery.QueryAll(c.doc, expr)
+ if err != nil {
+ return nil
+ }
+ if len(nodes) == 0 {
+ return nil
+ }
+ results := make([]string, 0, len(nodes))
+ for _, n := range nodes {
+ text := n.InnerText()
+ if trimmed := strings.TrimSpace(text); trimmed != "" {
+ results = append(results, trimmed)
+ }
+ }
+ return results
+}
+
+// Get evaluates the given XPath expression against the parsed XML document
+// and returns the string values of all matching nodes.
+func (c *XPathMap) Get(key string) []string {
+ return c.evalXPath(key)
+}
+
+// FindString evaluates the given XPath expression against the parsed XML
+// document and returns MatchData for each matching node. If key is empty,
+// it delegates to FindAll.
+func (c *XPathMap) FindString(key string) []types.MatchData {
+ if key == "" {
+ return c.FindAll()
+ }
+ values := c.evalXPath(key)
+ if len(values) == 0 {
+ return nil
+ }
+ buf := make([]corazarules.MatchData, len(values))
+ result := make([]types.MatchData, len(values))
+ for i, v := range values {
+ buf[i] = corazarules.MatchData{
+ Variable_: c.variable,
+ Key_: key,
+ Value_: v,
+ }
+ result[i] = &buf[i]
+ }
+ return result
+}
+
+// FindRegex returns all document nodes whose XPath key matches the given
+// regular expression. Since regex is not meaningful over XPath expressions,
+// this falls back to FindAll and filters results by key.
+func (c *XPathMap) FindRegex(key *regexp.Regexp) []types.MatchData {
+ all := c.FindAll()
+ var result []types.MatchData
+ for _, m := range all {
+ if key.MatchString(m.Key()) {
+ result = append(result, m)
+ }
+ }
+ return result
+}
+
+// FindAll returns all text content and attribute values from the document,
+// equivalent to evaluating //@* and //* text nodes.
+func (c *XPathMap) FindAll() []types.MatchData {
+ var result []types.MatchData
+ result = append(result, c.FindString("//@*")...)
+ result = append(result, c.FindString("//*[text()]")...)
+ return result
+}
+
+// Set is a no-op for XPathMap. The underlying data comes from the parsed
+// XML DOM and is not mutable through this interface.
+func (c *XPathMap) Set(key string, values []string) {}
+
+// Add is a no-op for XPathMap. See Set for rationale.
+func (c *XPathMap) Add(key string, value string) {}
+
+// SetIndex is a no-op for XPathMap. See Set for rationale.
+func (c *XPathMap) SetIndex(key string, index int, value string) {}
+
+// Remove is a no-op for XPathMap. See Set for rationale.
+func (c *XPathMap) Remove(key string) {}
+
+// Name returns the name of the variable this collection is bound to.
+func (c *XPathMap) Name() string {
+ return c.variable.Name()
+}
+
+// Reset clears the document reference.
+func (c *XPathMap) Reset() {
+ c.doc = nil
+}
+
+// Format writes a string representation of the collection.
+func (c *XPathMap) Format(res *strings.Builder) {
+ res.WriteString(c.variable.Name())
+ res.WriteString(": (xmlquery xpath-backed collection)\n")
+}
diff --git a/experimental/plugins/collections/xpath_map_test.go b/experimental/plugins/collections/xpath_map_test.go
new file mode 100644
index 000000000..c4ab0eaea
--- /dev/null
+++ b/experimental/plugins/collections/xpath_map_test.go
@@ -0,0 +1,666 @@
+// Copyright 2026 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package collections
+
+import (
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "testing"
+
+ "github.com/antchfx/xmlquery"
+
+ "github.com/corazawaf/coraza/v3/types/variables"
+)
+
+// testdataDir returns the path to the testdata directory.
+// Go test always sets the working directory to the package directory.
+func testdataDir(t testing.TB) string {
+ t.Helper()
+ return "testdata"
+}
+
+// parseTestDoc parses an XML string into an xmlquery document.
+func parseTestDoc(t testing.TB, xml string) *xmlquery.Node {
+ t.Helper()
+ doc, err := xmlquery.Parse(strings.NewReader(xml))
+ if err != nil {
+ t.Fatal(err)
+ }
+ return doc
+}
+
+// loadTestdata loads and parses an XML file from the testdata directory.
+func loadTestdata(t testing.TB, name string) *xmlquery.Node {
+ t.Helper()
+ data, err := os.ReadFile(filepath.Join(testdataDir(t), name))
+ if err != nil {
+ t.Fatal(err)
+ }
+ return parseTestDoc(t, string(data))
+}
+
+const testXML = `
+
+ - alpha
+ - beta
+`
+
+// ---------------------------------------------------------------------------
+// Unit tests — full coverage
+// ---------------------------------------------------------------------------
+
+// TestXPathMapGet verifies that Get evaluates an XPath expression and
+// returns the string values of matching nodes.
+func TestXPathMapGet(t *testing.T) {
+ doc := parseTestDoc(t, testXML)
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ got := m.Get("//item")
+ if len(got) != 2 {
+ t.Fatalf("expected 2 results, got %d", len(got))
+ }
+ if got[0] != "alpha" || got[1] != "beta" {
+ t.Errorf("unexpected values: %v", got)
+ }
+}
+
+// TestXPathMapGetAttributes verifies attribute XPath selection.
+func TestXPathMapGetAttributes(t *testing.T) {
+ doc := parseTestDoc(t, testXML)
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ got := m.Get("//item/@key")
+ if len(got) != 2 {
+ t.Fatalf("expected 2 attributes, got %d", len(got))
+ }
+ if got[0] != "a" || got[1] != "b" {
+ t.Errorf("unexpected attribute values: %v", got)
+ }
+}
+
+// TestXPathMapGetNoMatch verifies that a non-matching XPath returns nil.
+func TestXPathMapGetNoMatch(t *testing.T) {
+ doc := parseTestDoc(t, testXML)
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ got := m.Get("//missing")
+ if got != nil {
+ t.Errorf("expected nil, got %v", got)
+ }
+}
+
+// TestXPathMapGetInvalidXPath verifies that an invalid XPath expression
+// returns nil instead of panicking.
+func TestXPathMapGetInvalidXPath(t *testing.T) {
+ doc := parseTestDoc(t, testXML)
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ got := m.Get("[invalid xpath")
+ if got != nil {
+ t.Errorf("expected nil for invalid xpath, got %v", got)
+ }
+}
+
+// TestXPathMapNilDoc verifies that a nil document returns nil for all queries.
+func TestXPathMapNilDoc(t *testing.T) {
+ m := NewXPathMap(variables.RequestXML, nil)
+
+ if got := m.Get("//item"); got != nil {
+ t.Errorf("Get: expected nil, got %v", got)
+ }
+ if got := m.FindString("//item"); got != nil {
+ t.Errorf("FindString: expected nil, got %v", got)
+ }
+ if got := m.FindAll(); got != nil {
+ t.Errorf("FindAll: expected nil, got %v", got)
+ }
+ if got := m.FindRegex(regexp.MustCompile(".")); got != nil {
+ t.Errorf("FindRegex: expected nil, got %v", got)
+ }
+}
+
+// TestXPathMapFindString verifies that FindString returns proper MatchData
+// with the correct variable, key, and value fields.
+func TestXPathMapFindString(t *testing.T) {
+ doc := parseTestDoc(t, testXML)
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ matches := m.FindString("//item")
+ if len(matches) != 2 {
+ t.Fatalf("expected 2 matches, got %d", len(matches))
+ }
+ if matches[0].Variable().Name() != "REQUEST_XML" {
+ t.Errorf("expected variable REQUEST_XML, got %q", matches[0].Variable().Name())
+ }
+ if matches[0].Key() != "//item" {
+ t.Errorf("expected key '//item', got %q", matches[0].Key())
+ }
+ if matches[0].Value() != "alpha" {
+ t.Errorf("expected value 'alpha', got %q", matches[0].Value())
+ }
+}
+
+// TestXPathMapFindStringNoMatch verifies FindString returns nil for
+// a valid XPath that matches nothing.
+func TestXPathMapFindStringNoMatch(t *testing.T) {
+ doc := parseTestDoc(t, testXML)
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ matches := m.FindString("//nonexistent")
+ if matches != nil {
+ t.Errorf("expected nil, got %d matches", len(matches))
+ }
+}
+
+// TestXPathMapFindStringEmpty verifies that FindString with an empty key
+// delegates to FindAll.
+func TestXPathMapFindStringEmpty(t *testing.T) {
+ doc := parseTestDoc(t, testXML)
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ all := m.FindString("")
+ if len(all) == 0 {
+ t.Error("expected results from FindString(\"\"), got none")
+ }
+}
+
+// TestXPathMapFindRegex verifies that FindRegex filters results by key match.
+func TestXPathMapFindRegex(t *testing.T) {
+ doc := parseTestDoc(t, testXML)
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ re := regexp.MustCompile(`//@\*`)
+ matches := m.FindRegex(re)
+ for _, match := range matches {
+ if !re.MatchString(match.Key()) {
+ t.Errorf("FindRegex returned match with non-matching key: %q", match.Key())
+ }
+ }
+}
+
+// TestXPathMapFindRegexNoMatch verifies FindRegex returns nil when
+// regex matches no keys from FindAll.
+func TestXPathMapFindRegexNoMatch(t *testing.T) {
+ doc := parseTestDoc(t, testXML)
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ re := regexp.MustCompile(`^WILL_NEVER_MATCH$`)
+ matches := m.FindRegex(re)
+ if len(matches) != 0 {
+ t.Errorf("expected 0 matches, got %d", len(matches))
+ }
+}
+
+// TestXPathMapFindAll verifies FindAll returns both attributes and text content.
+func TestXPathMapFindAll(t *testing.T) {
+ doc := parseTestDoc(t, testXML)
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ all := m.FindAll()
+ if len(all) == 0 {
+ t.Fatal("expected results from FindAll, got none")
+ }
+
+ // Should have attribute results (key="//@*") and text results (key="//*[text()]")
+ var attrCount, textCount int
+ for _, md := range all {
+ switch md.Key() {
+ case "//@*":
+ attrCount++
+ case "//*[text()]":
+ textCount++
+ }
+ }
+ if attrCount == 0 {
+ t.Error("expected attribute results in FindAll")
+ }
+ if textCount == 0 {
+ t.Error("expected text results in FindAll")
+ }
+}
+
+// TestXPathMapName verifies the collection name matches the variable.
+func TestXPathMapName(t *testing.T) {
+ m := NewXPathMap(variables.RequestXML, nil)
+ if m.Name() != "REQUEST_XML" {
+ t.Errorf("expected name 'REQUEST_XML', got %q", m.Name())
+ }
+}
+
+// TestXPathMapReset verifies that Reset clears the document reference,
+// causing subsequent queries to return nil.
+func TestXPathMapReset(t *testing.T) {
+ doc := parseTestDoc(t, testXML)
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ if got := m.Get("//item"); len(got) == 0 {
+ t.Fatal("expected results before reset")
+ }
+
+ m.Reset()
+
+ if got := m.Get("//item"); got != nil {
+ t.Errorf("expected nil after reset, got %v", got)
+ }
+}
+
+// TestXPathMapMutationNoOps verifies that Set, Add, SetIndex, and Remove
+// are safe no-ops that do not panic or alter query results.
+func TestXPathMapMutationNoOps(t *testing.T) {
+ doc := parseTestDoc(t, testXML)
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ m.Set("//item", []string{"new"})
+ m.Add("//item", "new")
+ m.SetIndex("//item", 0, "new")
+ m.Remove("//item")
+
+ got := m.Get("//item")
+ if len(got) != 2 {
+ t.Errorf("mutation methods should be no-ops, but data changed: %v", got)
+ }
+}
+
+// TestXPathMapFormat verifies that Format writes the expected representation.
+func TestXPathMapFormat(t *testing.T) {
+ m := NewXPathMap(variables.RequestXML, nil)
+ var b strings.Builder
+ m.Format(&b)
+ out := b.String()
+ if !strings.Contains(out, "REQUEST_XML") {
+ t.Errorf("Format output missing variable name: %q", out)
+ }
+ if !strings.Contains(out, "xpath-backed") {
+ t.Errorf("Format output missing type indicator: %q", out)
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Tests with diverse XML structures from testdata
+// ---------------------------------------------------------------------------
+
+// TestXPathMapSOAPNamespaces verifies XPath queries with XML namespaces.
+func TestXPathMapSOAPNamespaces(t *testing.T) {
+ doc := loadTestdata(t, "small_soap.xml")
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ got := m.Get("//*[local-name()='StockName']")
+ if len(got) != 1 || got[0] != "GOOG" {
+ t.Errorf("expected [GOOG], got %v", got)
+ }
+
+ got = m.Get("//*[local-name()='Token']")
+ if len(got) != 1 || got[0] != "secret-token-123" {
+ t.Errorf("expected [secret-token-123], got %v", got)
+ }
+}
+
+// TestXPathMapCDATA verifies that CDATA content is accessible via XPath.
+func TestXPathMapCDATA(t *testing.T) {
+ doc := loadTestdata(t, "small_cdata.xml")
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ got := m.Get("//body")
+ if len(got) != 2 {
+ t.Fatalf("expected 2 body elements, got %d: %v", len(got), got)
+ }
+ // CDATA content should be returned as-is (without the CDATA markers)
+ if !strings.Contains(got[0], "world") {
+ t.Errorf("expected CDATA content with HTML tags, got %q", got[0])
+ }
+
+ // Attribute queries on CDATA doc
+ got = m.Get("//message/@priority")
+ if len(got) != 2 || got[0] != "high" || got[1] != "low" {
+ t.Errorf("expected [high, low], got %v", got)
+ }
+}
+
+// TestXPathMapXMLRPC verifies parsing of XML-RPC structures.
+func TestXPathMapXMLRPC(t *testing.T) {
+ doc := loadTestdata(t, "small_xmlrpc.xml")
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ got := m.Get("//methodName")
+ if len(got) != 1 || got[0] != "wp.getUsersBlogs" {
+ t.Errorf("expected [wp.getUsersBlogs], got %v", got)
+ }
+
+ got = m.Get("//params/param/value/string")
+ if len(got) != 2 || got[0] != "admin" || got[1] != "password123" {
+ t.Errorf("expected [admin, password123], got %v", got)
+ }
+
+ got = m.Get("//params/param/value/i4")
+ if len(got) != 1 || got[0] != "42" {
+ t.Errorf("expected [42], got %v", got)
+ }
+}
+
+// TestXPathMapMediumMixed verifies queries against a medium-sized document
+// with namespaces, CDATA, attributes, and nested structures.
+func TestXPathMapMediumMixed(t *testing.T) {
+ doc := loadTestdata(t, "medium_mixed.xml")
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ // Count all books
+ got := m.Get("//*[local-name()='book']")
+ if len(got) != 50 {
+ t.Errorf("expected 50 books, got %d", len(got))
+ }
+
+ // Query by attribute predicate
+ got = m.Get("//*[local-name()='book'][@category='fiction']")
+ if len(got) == 0 {
+ t.Error("expected fiction books, got none")
+ }
+
+ // Query for specific attribute values
+ got = m.Get("//*[local-name()='book']/@format")
+ if len(got) != 50 {
+ t.Errorf("expected 50 format attributes, got %d", len(got))
+ }
+
+ // Nested review elements
+ got = m.Get("//*[local-name()='reviewer']")
+ if len(got) == 0 {
+ t.Error("expected reviewers, got none")
+ }
+
+ // Metadata elements (every 7th book)
+ got = m.Get("//*[local-name()='metadata']/@key")
+ if len(got) == 0 {
+ t.Error("expected metadata attributes, got none")
+ }
+}
+
+// TestXPathMapDeepNesting verifies XPath on deeply nested documents.
+func TestXPathMapDeepNesting(t *testing.T) {
+ doc := loadTestdata(t, "medium_deep.xml")
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ // Query deepest level
+ got := m.Get("//*[local-name()='level'][@depth='30']/*[local-name()='data']")
+ if len(got) != 1 || got[0] != "Value at depth 30" {
+ t.Errorf("expected [Value at depth 30], got %v", got)
+ }
+
+ // All data elements
+ got = m.Get("//*[local-name()='data']")
+ if len(got) != 30 {
+ t.Errorf("expected 30 data elements, got %d", len(got))
+ }
+
+ // Siblings (every 5th level)
+ got = m.Get("//*[local-name()='sibling']")
+ if len(got) != 12 { // 6 levels * 2 siblings
+ t.Errorf("expected 12 siblings, got %d", len(got))
+ }
+}
+
+// TestXPathMapLargeDocQueries verifies various query patterns against a
+// large document (500 employee records with multiple namespaces).
+func TestXPathMapLargeDocQueries(t *testing.T) {
+ doc := loadTestdata(t, "large_catalog.xml")
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ // Count all employees
+ got := m.Get("//*[local-name()='employee']")
+ if len(got) != 500 {
+ t.Errorf("expected 500 employees, got %d", len(got))
+ }
+
+ // Query specific namespace prefix using local-name()
+ got = m.Get("//*[local-name()='salary']")
+ if len(got) != 500 {
+ t.Errorf("expected 500 salaries, got %d", len(got))
+ }
+
+ // Predicate on attribute
+ got = m.Get("//*[local-name()='employee'][@department='Engineering']")
+ if len(got) == 0 {
+ t.Error("expected Engineering employees, got none")
+ }
+
+ // CDATA notes (every 10th employee)
+ got = m.Get("//*[local-name()='notes']")
+ if len(got) != 50 {
+ t.Errorf("expected 50 notes, got %d", len(got))
+ }
+
+ // Access log entries (every 5th employee, 5 entries each)
+ got = m.Get("//*[local-name()='entry']/@action")
+ if len(got) != 500 { // 100 employees * 5 entries
+ t.Errorf("expected 500 access log actions, got %d", len(got))
+ }
+}
+
+// TestXPathMapWhitespaceOnlyNodes verifies that nodes with only whitespace
+// text content are excluded from results.
+func TestXPathMapWhitespaceOnlyNodes(t *testing.T) {
+ xml := `
+
+
+ real value
+
+`
+ doc := parseTestDoc(t, xml)
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ got := m.Get("//*[local-name()='empty']")
+ if len(got) != 0 {
+ t.Errorf("expected whitespace-only node to be excluded, got %v", got)
+ }
+ got = m.Get("//*[local-name()='content']")
+ if len(got) != 1 || got[0] != "real value" {
+ t.Errorf("expected [real value], got %v", got)
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Benchmarks — small, medium, large documents with various XPath patterns
+// ---------------------------------------------------------------------------
+
+// benchmarkXPath is a helper that loads a document once and benchmarks
+// repeated XPath evaluation.
+func benchmarkXPath(b *testing.B, docName, xpath string) {
+ b.Helper()
+ data, err := os.ReadFile(filepath.Join(testdataDir(b), docName))
+ if err != nil {
+ b.Fatal(err)
+ }
+ doc, err := xmlquery.Parse(strings.NewReader(string(data)))
+ if err != nil {
+ b.Fatal(err)
+ }
+ m := NewXPathMap(variables.RequestXML, doc)
+
+ b.ResetTimer()
+ b.ReportAllocs()
+ for b.Loop() {
+ m.Get(xpath)
+ }
+}
+
+// benchmarkParse measures the cost of parsing the XML document itself.
+func benchmarkParse(b *testing.B, docName string) {
+ b.Helper()
+ data, err := os.ReadFile(filepath.Join(testdataDir(b), docName))
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ b.ResetTimer()
+ b.ReportAllocs()
+ for b.Loop() {
+ _, err := xmlquery.Parse(strings.NewReader(string(data)))
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+// --- Small document benchmarks ---
+
+func BenchmarkSmall_Parse(b *testing.B) {
+ benchmarkParse(b, "small_simple.xml")
+}
+
+func BenchmarkSmall_GetElement(b *testing.B) {
+ benchmarkXPath(b, "small_simple.xml", "//item")
+}
+
+func BenchmarkSmall_GetAttribute(b *testing.B) {
+ benchmarkXPath(b, "small_simple.xml", "//item/@key")
+}
+
+func BenchmarkSmall_GetAllAttributes(b *testing.B) {
+ benchmarkXPath(b, "small_simple.xml", "//@*")
+}
+
+func BenchmarkSmall_FindAll(b *testing.B) {
+ data, _ := os.ReadFile(filepath.Join(testdataDir(b), "small_simple.xml"))
+ doc, _ := xmlquery.Parse(strings.NewReader(string(data)))
+ m := NewXPathMap(variables.RequestXML, doc)
+ b.ResetTimer()
+ b.ReportAllocs()
+ for b.Loop() {
+ m.FindAll()
+ }
+}
+
+// --- Small SOAP (namespace-heavy) ---
+
+func BenchmarkSmallSOAP_Parse(b *testing.B) {
+ benchmarkParse(b, "small_soap.xml")
+}
+
+func BenchmarkSmallSOAP_LocalName(b *testing.B) {
+ benchmarkXPath(b, "small_soap.xml", "//*[local-name()='StockName']")
+}
+
+// --- Small CDATA ---
+
+func BenchmarkSmallCDATA_Parse(b *testing.B) {
+ benchmarkParse(b, "small_cdata.xml")
+}
+
+func BenchmarkSmallCDATA_GetBody(b *testing.B) {
+ benchmarkXPath(b, "small_cdata.xml", "//body")
+}
+
+// --- Medium document benchmarks (50 books, ~22KB) ---
+
+func BenchmarkMedium_Parse(b *testing.B) {
+ benchmarkParse(b, "medium_mixed.xml")
+}
+
+func BenchmarkMedium_GetAllBooks(b *testing.B) {
+ benchmarkXPath(b, "medium_mixed.xml", "//*[local-name()='book']")
+}
+
+func BenchmarkMedium_GetByAttribute(b *testing.B) {
+ benchmarkXPath(b, "medium_mixed.xml", "//*[local-name()='book'][@category='fiction']")
+}
+
+func BenchmarkMedium_GetNestedReviews(b *testing.B) {
+ benchmarkXPath(b, "medium_mixed.xml", "//*[local-name()='review']/@rating")
+}
+
+func BenchmarkMedium_GetAllAttributes(b *testing.B) {
+ benchmarkXPath(b, "medium_mixed.xml", "//@*")
+}
+
+func BenchmarkMedium_FindAll(b *testing.B) {
+ data, _ := os.ReadFile(filepath.Join(testdataDir(b), "medium_mixed.xml"))
+ doc, _ := xmlquery.Parse(strings.NewReader(string(data)))
+ m := NewXPathMap(variables.RequestXML, doc)
+ b.ResetTimer()
+ b.ReportAllocs()
+ for b.Loop() {
+ m.FindAll()
+ }
+}
+
+// --- Medium deep document benchmarks (30 levels, ~9KB) ---
+
+func BenchmarkMediumDeep_Parse(b *testing.B) {
+ benchmarkParse(b, "medium_deep.xml")
+}
+
+func BenchmarkMediumDeep_QueryDeepest(b *testing.B) {
+ benchmarkXPath(b, "medium_deep.xml", "//*[local-name()='level'][@depth='30']/*[local-name()='data']")
+}
+
+func BenchmarkMediumDeep_QueryAllData(b *testing.B) {
+ benchmarkXPath(b, "medium_deep.xml", "//*[local-name()='data']")
+}
+
+// --- Large document benchmarks (500 employees, ~298KB) ---
+
+func BenchmarkLarge_Parse(b *testing.B) {
+ benchmarkParse(b, "large_catalog.xml")
+}
+
+func BenchmarkLarge_GetAllEmployees(b *testing.B) {
+ benchmarkXPath(b, "large_catalog.xml", "//*[local-name()='employee']")
+}
+
+func BenchmarkLarge_GetByDepartment(b *testing.B) {
+ benchmarkXPath(b, "large_catalog.xml", "//*[local-name()='employee'][@department='Engineering']")
+}
+
+func BenchmarkLarge_GetSalaries(b *testing.B) {
+ benchmarkXPath(b, "large_catalog.xml", "//*[local-name()='salary']")
+}
+
+func BenchmarkLarge_GetAccessLogActions(b *testing.B) {
+ benchmarkXPath(b, "large_catalog.xml", "//*[local-name()='entry']/@action")
+}
+
+func BenchmarkLarge_GetCDATANotes(b *testing.B) {
+ benchmarkXPath(b, "large_catalog.xml", "//*[local-name()='notes']")
+}
+
+func BenchmarkLarge_GetAllAttributes(b *testing.B) {
+ benchmarkXPath(b, "large_catalog.xml", "//@*")
+}
+
+func BenchmarkLarge_FindAll(b *testing.B) {
+ data, _ := os.ReadFile(filepath.Join(testdataDir(b), "large_catalog.xml"))
+ doc, _ := xmlquery.Parse(strings.NewReader(string(data)))
+ m := NewXPathMap(variables.RequestXML, doc)
+ b.ResetTimer()
+ b.ReportAllocs()
+ for b.Loop() {
+ m.FindAll()
+ }
+}
+
+// --- FindString benchmarks (measures MatchData allocation) ---
+
+func BenchmarkLarge_FindString_Employees(b *testing.B) {
+ data, _ := os.ReadFile(filepath.Join(testdataDir(b), "large_catalog.xml"))
+ doc, _ := xmlquery.Parse(strings.NewReader(string(data)))
+ m := NewXPathMap(variables.RequestXML, doc)
+ b.ResetTimer()
+ b.ReportAllocs()
+ for b.Loop() {
+ m.FindString("//*[local-name()='employee']")
+ }
+}
+
+func BenchmarkLarge_FindRegex(b *testing.B) {
+ data, _ := os.ReadFile(filepath.Join(testdataDir(b), "large_catalog.xml"))
+ doc, _ := xmlquery.Parse(strings.NewReader(string(data)))
+ m := NewXPathMap(variables.RequestXML, doc)
+ re := regexp.MustCompile(`text`)
+ b.ResetTimer()
+ b.ReportAllocs()
+ for b.Loop() {
+ m.FindRegex(re)
+ }
+}
diff --git a/experimental/plugins/xmlquery.go b/experimental/plugins/xmlquery.go
new file mode 100644
index 000000000..974059efb
--- /dev/null
+++ b/experimental/plugins/xmlquery.go
@@ -0,0 +1,88 @@
+// Copyright 2026 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package plugins
+
+import (
+ "io"
+
+ "github.com/antchfx/xmlquery"
+
+ "github.com/corazawaf/coraza/v3/collection"
+ "github.com/corazawaf/coraza/v3/experimental/plugins/collections"
+ "github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes"
+ "github.com/corazawaf/coraza/v3/internal/bodyprocessors"
+ "github.com/corazawaf/coraza/v3/types/variables"
+)
+
+// xmlSettableVariables is an optional interface that TransactionVariables may
+// implement to allow body processors to replace the XML collection with a
+// custom implementation. This decouples the body processor from the concrete
+// transaction type.
+type xmlSettableVariables interface {
+ SetRequestXML(collection.Map)
+}
+
+// xmlQueryBodyProcessor parses XML request bodies into an in-memory DOM and
+// installs a lazy XPath collection (collections.XPathMap) as the RequestXML
+// variable. XPath expressions in rules (e.g. XML:/soap:Envelope/soap:Body)
+// are evaluated on demand against the parsed document, rather than requiring
+// pre-computed keys.
+type xmlQueryBodyProcessor struct{}
+
+var _ plugintypes.BodyProcessor = (*xmlQueryBodyProcessor)(nil)
+
+// ProcessRequest parses the request body as XML and installs a lazy XPath
+// collection on the transaction's RequestXML variable. If the transaction
+// does not support SetRequestXML, it falls back to populating the standard
+// //@* and /* keys on the existing collection.
+func (*xmlQueryBodyProcessor) ProcessRequest(reader io.Reader, v plugintypes.TransactionVariables, options plugintypes.BodyProcessorOptions) error {
+ doc, err := xmlquery.Parse(reader)
+ if err != nil {
+ return err
+ }
+
+ // If the transaction supports swapping the XML collection, install the
+ // lazy XPath-backed collection. Otherwise, fall back to populating the
+ // existing map with the two standard keys.
+ if setter, ok := v.(xmlSettableVariables); ok {
+ xpathMap := collections.NewXPathMap(variables.RequestXML, doc)
+ setter.SetRequestXML(xpathMap)
+ } else {
+ col := v.RequestXML()
+ attrs, contents := extractFromDoc(doc)
+ col.Set("//@*", attrs)
+ col.Set("/*", contents)
+ }
+
+ return nil
+}
+
+// ProcessResponse is not yet implemented for XML response bodies.
+func (*xmlQueryBodyProcessor) ProcessResponse(reader io.Reader, v plugintypes.TransactionVariables, options plugintypes.BodyProcessorOptions) error {
+ return nil
+}
+
+// extractFromDoc walks the parsed DOM and extracts all attribute values and
+// text content, mirroring the behavior of the original xml body processor.
+func extractFromDoc(doc *xmlquery.Node) (attrs []string, content []string) {
+ nodes, _ := xmlquery.QueryAll(doc, "//@*")
+ for _, n := range nodes {
+ if v := n.InnerText(); v != "" {
+ attrs = append(attrs, v)
+ }
+ }
+ textNodes, _ := xmlquery.QueryAll(doc, "//*[text()]")
+ for _, n := range textNodes {
+ if v := n.InnerText(); v != "" {
+ content = append(content, v)
+ }
+ }
+ return attrs, content
+}
+
+func init() {
+ bodyprocessors.RegisterBodyProcessor("xmlquery", func() plugintypes.BodyProcessor {
+ return &xmlQueryBodyProcessor{}
+ })
+}
diff --git a/experimental/plugins/xmlquery_test.go b/experimental/plugins/xmlquery_test.go
new file mode 100644
index 000000000..fb06b6a83
--- /dev/null
+++ b/experimental/plugins/xmlquery_test.go
@@ -0,0 +1,288 @@
+// Copyright 2026 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package plugins_test
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/corazawaf/coraza/v3/collection"
+ "github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes"
+ "github.com/corazawaf/coraza/v3/internal/bodyprocessors"
+ internalcollections "github.com/corazawaf/coraza/v3/internal/collections"
+ "github.com/corazawaf/coraza/v3/internal/corazawaf"
+ "github.com/corazawaf/coraza/v3/types/variables"
+
+ // Blank import to trigger init() registration of the xmlquery body processor.
+ _ "github.com/corazawaf/coraza/v3/experimental/plugins"
+)
+
+const bookstoreXML = `
+
+
+ Harry Potter
+ 29.99
+
+
+ Learning XML
+ 39.95
+
+`
+
+const soapXML = `
+
+
+
+ GOOG
+
+
+`
+
+const xmlRPC = `
+
+ wp.getUsersBlogs
+
+ admin
+ password123
+
+`
+
+func xmlQueryProcessor(t *testing.T) plugintypes.BodyProcessor {
+ t.Helper()
+ bp, err := bodyprocessors.GetBodyProcessor("xmlquery")
+ if err != nil {
+ t.Fatal(err)
+ }
+ return bp
+}
+
+// TestXMLQueryBodyProcessorXPath verifies that the xmlquery body processor
+// installs a lazy XPath collection that can evaluate arbitrary XPath
+// expressions against the parsed document.
+func TestXMLQueryBodyProcessorXPath(t *testing.T) {
+ v := corazawaf.NewTransactionVariables()
+ bp := xmlQueryProcessor(t)
+ err := bp.ProcessRequest(
+ bytes.NewReader([]byte(bookstoreXML)),
+ v,
+ plugintypes.BodyProcessorOptions{},
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ tests := []struct {
+ name string
+ xpath string
+ expect []string
+ }{
+ {
+ name: "all titles",
+ xpath: "//title",
+ expect: []string{"Harry Potter", "Learning XML"},
+ },
+ {
+ name: "all prices",
+ xpath: "//price",
+ expect: []string{"29.99", "39.95"},
+ },
+ {
+ name: "first book title",
+ xpath: "//book[1]/title",
+ expect: []string{"Harry Potter"},
+ },
+ {
+ name: "attribute selection",
+ xpath: "//title/@lang",
+ expect: []string{"en", "es"},
+ },
+ {
+ name: "backward compat //@*",
+ xpath: "//@*",
+ expect: []string{"en", "value", "es"},
+ },
+ {
+ name: "text nodes via //text()",
+ xpath: "//text()",
+ expect: []string{"Harry Potter", "29.99", "Learning XML", "39.95"},
+ },
+ {
+ name: "no match",
+ xpath: "//nonexistent",
+ expect: nil,
+ },
+ }
+
+ col := v.RequestXML()
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := col.Get(tt.xpath)
+ if tt.expect == nil {
+ if len(got) != 0 {
+ t.Errorf("expected no results, got %v", got)
+ }
+ return
+ }
+ if len(got) != len(tt.expect) {
+ t.Fatalf("expected %d results, got %d: %v", len(tt.expect), len(got), got)
+ }
+ for i := range tt.expect {
+ if got[i] != tt.expect[i] {
+ t.Errorf("result[%d]: expected %q, got %q", i, tt.expect[i], got[i])
+ }
+ }
+ })
+ }
+}
+
+// TestXMLQueryBodyProcessorSOAP verifies namespace-aware XPath evaluation
+// on a SOAP envelope.
+func TestXMLQueryBodyProcessorSOAP(t *testing.T) {
+ v := corazawaf.NewTransactionVariables()
+ bp := xmlQueryProcessor(t)
+ err := bp.ProcessRequest(
+ bytes.NewReader([]byte(soapXML)),
+ v,
+ plugintypes.BodyProcessorOptions{},
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ col := v.RequestXML()
+
+ // Namespace-aware query using local-name()
+ got := col.Get("//*[local-name()='StockName']")
+ if len(got) != 1 || got[0] != "GOOG" {
+ t.Errorf("expected [GOOG], got %v", got)
+ }
+}
+
+// TestXMLQueryBodyProcessorXMLRPC verifies parsing of WordPress XML-RPC
+// requests, the original scenario from issue #1441.
+func TestXMLQueryBodyProcessorXMLRPC(t *testing.T) {
+ v := corazawaf.NewTransactionVariables()
+ bp := xmlQueryProcessor(t)
+ err := bp.ProcessRequest(
+ bytes.NewReader([]byte(xmlRPC)),
+ v,
+ plugintypes.BodyProcessorOptions{},
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ col := v.RequestXML()
+
+ got := col.Get("//methodName")
+ if len(got) != 1 || got[0] != "wp.getUsersBlogs" {
+ t.Errorf("expected [wp.getUsersBlogs], got %v", got)
+ }
+
+ got = col.Get("//params/param/value/string")
+ if len(got) != 2 {
+ t.Fatalf("expected 2 params, got %d: %v", len(got), got)
+ }
+ if got[0] != "admin" || got[1] != "password123" {
+ t.Errorf("expected [admin, password123], got %v", got)
+ }
+}
+
+// TestXMLQueryBodyProcessorFindString verifies that FindString returns
+// proper MatchData for rule evaluation.
+func TestXMLQueryBodyProcessorFindString(t *testing.T) {
+ v := corazawaf.NewTransactionVariables()
+ bp := xmlQueryProcessor(t)
+ err := bp.ProcessRequest(
+ bytes.NewReader([]byte(bookstoreXML)),
+ v,
+ plugintypes.BodyProcessorOptions{},
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ col := v.RequestXML()
+ matches := col.FindString("//title")
+ if len(matches) != 2 {
+ t.Fatalf("expected 2 matches, got %d", len(matches))
+ }
+ if matches[0].Value() != "Harry Potter" {
+ t.Errorf("expected 'Harry Potter', got %q", matches[0].Value())
+ }
+ if matches[0].Key() != "//title" {
+ t.Errorf("expected key '//title', got %q", matches[0].Key())
+ }
+}
+
+// TestXMLQueryBodyProcessorFallback verifies that the body processor falls
+// back to populating the standard map keys when SetRequestXML is not available.
+func TestXMLQueryBodyProcessorFallback(t *testing.T) {
+ xmlMap := internalcollections.NewMap(variables.RequestXML)
+
+ // Use a TransactionVariables mock that does not implement xmlSettableVariables.
+ vars := &fallbackTestVars{requestXML: xmlMap}
+
+ bp := xmlQueryProcessor(t)
+ err := bp.ProcessRequest(
+ bytes.NewReader([]byte(bookstoreXML)),
+ vars,
+ plugintypes.BodyProcessorOptions{},
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ attrs := xmlMap.Get("//@*")
+ if len(attrs) != 3 {
+ t.Errorf("expected 3 attributes, got %d: %v", len(attrs), attrs)
+ }
+
+ contents := xmlMap.Get("/*")
+ if len(contents) == 0 {
+ t.Error("expected content in /*, got none")
+ }
+}
+
+// TestXMLQueryBodyProcessorInvalidXML verifies that malformed XML returns
+// an error instead of silently succeeding.
+func TestXMLQueryBodyProcessorInvalidXML(t *testing.T) {
+ v := corazawaf.NewTransactionVariables()
+ bp := xmlQueryProcessor(t)
+ err := bp.ProcessRequest(
+ bytes.NewReader([]byte("")),
+ v,
+ plugintypes.BodyProcessorOptions{},
+ )
+ if err == nil {
+ t.Error("expected error for malformed XML, got nil")
+ }
+}
+
+// TestXMLQueryBodyProcessorProcessResponse verifies that ProcessResponse
+// returns nil (not yet implemented).
+func TestXMLQueryBodyProcessorProcessResponse(t *testing.T) {
+ bp := xmlQueryProcessor(t)
+ err := bp.ProcessResponse(
+ bytes.NewReader(nil),
+ corazawaf.NewTransactionVariables(),
+ plugintypes.BodyProcessorOptions{},
+ )
+ if err != nil {
+ t.Errorf("expected nil, got %v", err)
+ }
+}
+
+// fallbackTestVars is a minimal TransactionVariables implementation that does
+// NOT implement xmlSettableVariables, forcing the body processor to use the
+// fallback path of populating the existing collection.Map directly.
+type fallbackTestVars struct {
+ plugintypes.TransactionVariables
+ requestXML collection.Map
+}
+
+// RequestXML returns the test collection without supporting SetRequestXML.
+func (v *fallbackTestVars) RequestXML() collection.Map {
+ return v.requestXML
+}
diff --git a/go.mod b/go.mod
index 3f396be4d..1ff62ec62 100644
--- a/go.mod
+++ b/go.mod
@@ -17,6 +17,7 @@ go 1.25.0
// - ocsf-schema-golang
require (
+ github.com/antchfx/xmlquery v1.5.1
github.com/anuraaga/go-modsecurity v0.0.0-20220824035035-b9a4099778df
github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc
github.com/corazawaf/libinjection-go v0.3.2
@@ -34,9 +35,11 @@ require (
)
require (
+ github.com/antchfx/xpath v1.3.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 // indirect
github.com/gotnospirit/messageformat v0.0.0-20221001023931-dfe49f1eb092 // indirect
github.com/kaptinlin/go-i18n v0.1.4 // indirect
diff --git a/go.sum b/go.sum
index a42acef57..d9ba2a8bb 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,7 @@
+github.com/antchfx/xmlquery v1.5.1 h1:T9I4Ns1EXiWHy0IqKupGhnfTQtJwlGrpXtauYOoNv78=
+github.com/antchfx/xmlquery v1.5.1/go.mod h1:bVqnl7TaDXSReKINrhZz+2E/PbCu2tUahb+wZ7WZNT8=
+github.com/antchfx/xpath v1.3.6 h1:s0y+ElRRtTQdfHP609qFu0+c6bglDv20pqOViQjjdPI=
+github.com/antchfx/xpath v1.3.6/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/anuraaga/go-modsecurity v0.0.0-20220824035035-b9a4099778df h1:YWiVl53v0R8Knj/k+4slO0SXPL67Y4dXWiOIWNzrkew=
github.com/anuraaga/go-modsecurity v0.0.0-20220824035035-b9a4099778df/go.mod h1:7jguE759ADzy2EkxGRXigiC0ER1Yq2IFk2qNtwgzc7U=
github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc h1:OlJhrgI3I+FLUCTI3JJW8MoqyM78WbqJjecqMnqG+wc=
@@ -12,6 +16,9 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 h1:b70jEaX2iaJSPZULSUxKtm73LBfsCrMsIlYCUgNGSIs=
@@ -53,10 +60,15 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -67,6 +79,9 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -75,6 +90,9 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -87,8 +105,12 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -96,6 +118,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -103,6 +128,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -111,6 +138,7 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/internal/corazawaf/transaction.go b/internal/corazawaf/transaction.go
index c0d2af84a..d36428a9e 100644
--- a/internal/corazawaf/transaction.go
+++ b/internal/corazawaf/transaction.go
@@ -1820,7 +1820,7 @@ type TransactionVariables struct {
requestProtocol *collections.Single
requestURI *collections.Single
requestURIRaw *collections.Single
- requestXML *collections.Map
+ requestXML collection.Map
responseBody *collections.Single
responseContentLength *collections.Single
responseContentType *collections.Single
@@ -1828,7 +1828,7 @@ type TransactionVariables struct {
responseHeadersNames collection.Keyed
responseProtocol *collections.Single
responseStatus *collections.Single
- responseXML *collections.Map
+ responseXML collection.Map
responseArgs *collections.Map
resBodyProcessor *collections.Single
rule *collections.Map
@@ -1839,7 +1839,7 @@ type TransactionVariables struct {
tx *collections.Map
uniqueID *collections.Single
urlencodedError *collections.Single
- xml *collections.Map
+ xml collection.Map
resBodyError *collections.Single
resBodyErrorMsg *collections.Single
resBodyProcessorError *collections.Single
@@ -2235,6 +2235,20 @@ func (v *TransactionVariables) ResponseXML() collection.Map {
return v.responseXML
}
+// SetRequestXML replaces the RequestXML collection with the given implementation.
+// This allows body processors to install custom collection types (e.g., lazy
+// XPath evaluation) without the transaction needing to know about the specifics.
+// It also updates the XML alias to point to the new collection.
+func (v *TransactionVariables) SetRequestXML(m collection.Map) {
+ v.requestXML = m
+ v.xml = m
+}
+
+// SetResponseXML replaces the ResponseXML collection with the given implementation.
+func (v *TransactionVariables) SetResponseXML(m collection.Map) {
+ v.responseXML = m
+}
+
func (v *TransactionVariables) ResponseBodyProcessor() collection.Single {
return v.resBodyProcessor
}
diff --git a/internal/corazawaf/transaction_test.go b/internal/corazawaf/transaction_test.go
index 0e765f1e8..1577e1bcd 100644
--- a/internal/corazawaf/transaction_test.go
+++ b/internal/corazawaf/transaction_test.go
@@ -2663,3 +2663,50 @@ func BenchmarkRuleEvalWithRemovedRules(b *testing.B) {
waf.Rules.Eval(types.PhaseRequestHeaders, tx)
}
}
+
+// TestTransactionVariablesSetRequestXML verifies that SetRequestXML replaces
+// the RequestXML collection and updates the XML alias.
+func TestTransactionVariablesSetRequestXML(t *testing.T) {
+ v := NewTransactionVariables()
+
+ original := v.RequestXML()
+ replacement := collections.NewMap(variables.RequestXML)
+ replacement.Add("test", "value")
+
+ v.SetRequestXML(replacement)
+
+ if v.RequestXML() != replacement {
+ t.Error("RequestXML was not replaced")
+ }
+ if v.XML() != replacement {
+ t.Error("XML alias was not updated to match RequestXML")
+ }
+ if v.RequestXML() == original {
+ t.Error("RequestXML still points to original collection")
+ }
+ if got := v.RequestXML().Get("test"); len(got) != 1 || got[0] != "value" {
+ t.Errorf("replacement collection not accessible, got %v", got)
+ }
+}
+
+// TestTransactionVariablesSetResponseXML verifies that SetResponseXML
+// replaces the ResponseXML collection.
+func TestTransactionVariablesSetResponseXML(t *testing.T) {
+ v := NewTransactionVariables()
+
+ original := v.ResponseXML()
+ replacement := collections.NewMap(variables.ResponseXML)
+ replacement.Add("test", "value")
+
+ v.SetResponseXML(replacement)
+
+ if v.ResponseXML() != replacement {
+ t.Error("ResponseXML was not replaced")
+ }
+ if v.ResponseXML() == original {
+ t.Error("ResponseXML still points to original collection")
+ }
+ if got := v.ResponseXML().Get("test"); len(got) != 1 || got[0] != "value" {
+ t.Errorf("replacement collection not accessible, got %v", got)
+ }
+}
diff --git a/testing/coreruleset/coreruleset_test.go b/testing/coreruleset/coreruleset_test.go
index 7f1863e9d..60730bd6a 100644
--- a/testing/coreruleset/coreruleset_test.go
+++ b/testing/coreruleset/coreruleset_test.go
@@ -151,6 +151,101 @@ func BenchmarkCRSLargePOST(b *testing.B) {
}
}
+func BenchmarkCRSXMLSimplePOST(b *testing.B) {
+ waf := crsWAF(b)
+
+ xmlPayload := []byte(`
+
+ wp.getUsersBlogs
+
+ admin
+ password123
+
+`)
+
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ tx := waf.NewTransaction()
+ tx.ProcessConnection("127.0.0.1", 8080, "127.0.0.1", 8080)
+ tx.ProcessURI("/xmlrpc.php", "POST", "HTTP/1.1")
+ tx.AddRequestHeader("Host", "localhost")
+ tx.AddRequestHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36")
+ tx.AddRequestHeader("Content-Type", "text/xml")
+ tx.ProcessRequestHeaders()
+ if _, _, err := tx.WriteRequestBody(xmlPayload); err != nil {
+ b.Error(err)
+ }
+ if _, err := tx.ProcessRequestBody(); err != nil {
+ b.Error(err)
+ }
+ tx.AddResponseHeader("Content-Type", "text/xml")
+ tx.ProcessResponseHeaders(200, "OK")
+ if _, err := tx.ProcessResponseBody(); err != nil {
+ b.Error(err)
+ }
+ tx.ProcessLogging()
+ if err := tx.Close(); err != nil {
+ b.Error(err)
+ }
+ }
+}
+
+func BenchmarkCRSXMLLargeSOAP(b *testing.B) {
+ waf := crsWAF(b)
+
+ // ~4KB SOAP envelope with multiple items
+ var sb strings.Builder
+ sb.WriteString(`
+
+
+
+
+
+ `)
+ for i := 0; i < 100; i++ {
+ fmt.Fprintf(&sb, `
+
+ Item number %d
+ Description for item %d with some extra text to add size
+ %d.99
+ `, i, i, i, i*10+99)
+ }
+ sb.WriteString(`
+
+
+`)
+ xmlPayload := []byte(sb.String())
+
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ tx := waf.NewTransaction()
+ tx.ProcessConnection("127.0.0.1", 8080, "127.0.0.1", 8080)
+ tx.ProcessURI("/api/batch", "POST", "HTTP/1.1")
+ tx.AddRequestHeader("Host", "localhost")
+ tx.AddRequestHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36")
+ tx.AddRequestHeader("Content-Type", "application/soap+xml")
+ tx.ProcessRequestHeaders()
+ if _, _, err := tx.WriteRequestBody(xmlPayload); err != nil {
+ b.Error(err)
+ }
+ if _, err := tx.ProcessRequestBody(); err != nil {
+ b.Error(err)
+ }
+ tx.AddResponseHeader("Content-Type", "application/soap+xml")
+ tx.ProcessResponseHeaders(200, "OK")
+ if _, err := tx.ProcessResponseBody(); err != nil {
+ b.Error(err)
+ }
+ tx.ProcessLogging()
+ if err := tx.Close(); err != nil {
+ b.Error(err)
+ }
+ }
+}
+
// BenchmarkCRSPrefilter measures CRS request processing across diverse traffic
// patterns. Run with and without the coraza.rule.rx_prefilter build tag and
// compare via benchstat:
diff --git a/testing/coreruleset/coreruleset_xmlquery_test.go b/testing/coreruleset/coreruleset_xmlquery_test.go
new file mode 100644
index 000000000..cecf03618
--- /dev/null
+++ b/testing/coreruleset/coreruleset_xmlquery_test.go
@@ -0,0 +1,337 @@
+// Copyright 2026 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+// These benchmarks don't currently compile with TinyGo
+//go:build !tinygo
+
+package coreruleset
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "io/fs"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/bmatcuk/doublestar/v4"
+ albedo "github.com/coreruleset/albedo/server"
+ "github.com/coreruleset/go-ftw/v2/config"
+ "github.com/coreruleset/go-ftw/v2/output"
+ "github.com/coreruleset/go-ftw/v2/runner"
+ "github.com/coreruleset/go-ftw/v2/test"
+ "github.com/rs/zerolog"
+
+ coreruleset "github.com/corazawaf/coraza-coreruleset/v4"
+ crstests "github.com/corazawaf/coraza-coreruleset/v4/tests"
+ "github.com/corazawaf/coraza/v3"
+ "github.com/corazawaf/coraza/v3/experimental"
+ txhttp "github.com/corazawaf/coraza/v3/http"
+ "github.com/corazawaf/coraza/v3/types"
+
+ // Register the experimental xmlquery body processor so that
+ // ctl:requestBodyProcessor=XMLQUERY is available.
+ _ "github.com/corazawaf/coraza/v3/experimental/plugins"
+)
+
+// TestFTWXMLQuery runs the full CRS FTW test suite using the experimental
+// xmlquery body processor instead of the default XML processor.
+// This verifies that the lazy XPath-backed collection is fully compatible
+// with CRS rules that inspect XML request bodies.
+func TestFTWXMLQuery(t *testing.T) {
+ conf := coraza.NewWAFConfig()
+
+ rec, err := os.ReadFile(filepath.Join("..", "..", "coraza.conf-recommended"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Override rule 200000 to use the experimental xmlquery processor.
+ // SecRuleUpdateActionById replaces the action list for the existing rule,
+ // switching from requestBodyProcessor=XML to requestBodyProcessor=XMLQUERY.
+ xmlqueryOverride := `
+SecRuleUpdateActionById 200000 "phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XMLQUERY"
+`
+
+ customTestingConfig := `
+SecResponseBodyMimeType text/plain
+SecDefaultAction "phase:3,log,auditlog,pass"
+SecDefaultAction "phase:4,log,auditlog,pass"
+SecDefaultAction "phase:5,log,auditlog,pass"
+
+# Rule 900005 from https://github.com/coreruleset/coreruleset/blob/v4.0/dev/tests/regression/README.md#requirements
+SecAction "id:900005,\
+ phase:1,\
+ nolog,\
+ pass,\
+ ctl:ruleEngine=DetectionOnly,\
+ ctl:ruleRemoveById=910000,\
+ setvar:tx.blocking_paranoia_level=4,\
+ setvar:tx.crs_validate_utf8_encoding=1,\
+ setvar:tx.arg_name_length=100,\
+ setvar:tx.arg_length=400,\
+ setvar:tx.total_arg_length=64000,\
+ setvar:tx.max_num_args=255,\
+ setvar:tx.max_file_size=64100,\
+ setvar:tx.combined_file_sizes=65535"
+
+# Write the value from the X-CRS-Test header as a marker to the log
+# Requests with X-CRS-Test header will not be matched by any rule. See https://github.com/coreruleset/go-ftw/pull/133
+SecRule REQUEST_HEADERS:X-CRS-Test "@rx ^.*$" \
+ "id:999999,\
+ phase:1,\
+ pass,\
+ t:none,\
+ log,\
+ msg:'X-CRS-Test %{MATCHED_VAR}',\
+ ctl:ruleRemoveById=1-999999"
+`
+ conf = conf.
+ WithRootFS(coreruleset.FS).
+ WithDirectives(string(rec)).
+ WithDirectives(customTestingConfig).
+ WithDirectives("Include @crs-setup.conf.example").
+ WithDirectives("Include @owasp_crs/*.conf").
+ WithDirectives(xmlqueryOverride)
+
+ errorPath := filepath.Join(t.TempDir(), "error.log")
+ errorFile, err := os.Create(errorPath)
+ if err != nil {
+ t.Fatalf("failed to create error log: %v", err)
+ }
+ defer errorFile.Close()
+
+ errorWriter := bufio.NewWriter(errorFile)
+ conf = conf.WithErrorCallback(func(rule types.MatchedRule) {
+ msg := rule.ErrorLog() + "\n"
+ if _, err := io.WriteString(errorWriter, msg); err != nil {
+ t.Fatal(err)
+ }
+ if err := errorWriter.Flush(); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ waf, err := coraza.NewWAF(conf)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if closer, ok := waf.(experimental.WAFCloser); ok {
+ defer closer.Close()
+ }
+
+ s := httptest.NewServer(txhttp.WrapHandler(waf, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ defer r.Body.Close()
+ w.Header().Set("Content-Type", "text/plain")
+ albedo.Handler().ServeHTTP(w, r)
+ })))
+ defer s.Close()
+
+ var tests []*test.FTWTest
+ err = doublestar.GlobWalk(crstests.FS, "**/*.yaml", func(path string, d os.DirEntry) error {
+ yaml, err := fs.ReadFile(crstests.FS, path)
+ if err != nil {
+ return err
+ }
+ ftwt, err := test.GetTestFromYaml(yaml, path)
+ if err != nil {
+ return err
+ }
+ tests = append(tests, ftwt)
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(tests) == 0 {
+ t.Fatal("no tests found")
+ }
+
+ u, _ := url.Parse(s.URL)
+ host := u.Hostname()
+ port, _ := strconv.Atoi(u.Port())
+ zerolog.SetGlobalLevel(zerolog.InfoLevel)
+ cfg, err := config.NewConfigFromFile(".ftw.yml")
+ if err != nil {
+ t.Fatal(err)
+ }
+ cfg.LogFile = errorPath
+ cfg.TestOverride.Overrides.DestAddr = &host
+ cfg.TestOverride.Overrides.Port = &port
+
+ if err := loadMultiphaseOverrides(cfg); err != nil {
+ t.Fatal(err)
+ }
+ runnerCfg := config.NewRunnerConfiguration(cfg)
+ runnerCfg.ReadTimeout = 3 * time.Second
+ if err := runnerCfg.LoadPlatformOverrides(".ftw-overrides.yml"); err != nil {
+ t.Fatal(err)
+ }
+ res, err := runner.Run(runnerCfg, tests, output.NewOutput("quiet", os.Stdout))
+ if err != nil {
+ t.Fatal(err)
+ }
+ totalIgnored := len(res.Stats.Ignored)
+ if totalIgnored > 0 {
+ t.Logf("[info] %d ignored tests: %v", totalIgnored, res.Stats.Ignored)
+ }
+ totalFailed := len(res.Stats.Failed)
+ if totalFailed > 0 {
+ t.Errorf("[fatal] %d failed tests: %v", totalFailed, res.Stats.Failed)
+ }
+}
+
+// crsWAFXMLQuery returns a CRS WAF configured to use the experimental xmlquery
+// body processor for XML content types instead of the default XML processor.
+func crsWAFXMLQuery(t testing.TB) coraza.WAF {
+ t.Helper()
+ rec, err := os.ReadFile(filepath.Join("..", "..", "coraza.conf-recommended"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ customTestingConfig := `
+SecResponseBodyMimeType text/plain
+SecDefaultAction "phase:3,log,auditlog,pass"
+SecDefaultAction "phase:4,log,auditlog,pass"
+
+# Rule 900005 from https://github.com/coreruleset/coreruleset/blob/v4.0/dev/tests/regression/README.md#requirements
+SecAction "id:900005,\
+ phase:1,\
+ nolog,\
+ pass,\
+ ctl:ruleEngine=DetectionOnly,\
+ ctl:ruleRemoveById=910000,\
+ setvar:tx.blocking_paranoia_level=4,\
+ setvar:tx.crs_validate_utf8_encoding=1,\
+ setvar:tx.arg_name_length=100,\
+ setvar:tx.arg_length=400,\
+ setvar:tx.total_arg_length=64000,\
+ setvar:tx.max_num_args=255,\
+ setvar:tx.max_file_size=64100,\
+ setvar:tx.combined_file_sizes=65535"
+`
+ xmlqueryOverride := `
+SecRuleUpdateActionById 200000 "phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XMLQUERY"
+`
+ conf := coraza.NewWAFConfig().
+ WithRootFS(coreruleset.FS).
+ WithDirectives(string(rec)).
+ WithDirectives(customTestingConfig).
+ WithDirectives("Include @crs-setup.conf.example").
+ WithDirectives("Include @owasp_crs/*.conf").
+ WithDirectives(xmlqueryOverride)
+
+ waf, err := coraza.NewWAF(conf)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if closer, ok := waf.(experimental.WAFCloser); ok {
+ if _, isBenchmark := t.(*testing.B); !isBenchmark {
+ t.Cleanup(func() { closer.Close() })
+ }
+ }
+
+ return waf
+}
+
+func BenchmarkCRSXMLQuerySimplePOST(b *testing.B) {
+ waf := crsWAFXMLQuery(b)
+
+ xmlPayload := []byte(`
+
+ wp.getUsersBlogs
+
+ admin
+ password123
+
+`)
+
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ tx := waf.NewTransaction()
+ tx.ProcessConnection("127.0.0.1", 8080, "127.0.0.1", 8080)
+ tx.ProcessURI("/xmlrpc.php", "POST", "HTTP/1.1")
+ tx.AddRequestHeader("Host", "localhost")
+ tx.AddRequestHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36")
+ tx.AddRequestHeader("Content-Type", "text/xml")
+ tx.ProcessRequestHeaders()
+ if _, _, err := tx.WriteRequestBody(xmlPayload); err != nil {
+ b.Error(err)
+ }
+ if _, err := tx.ProcessRequestBody(); err != nil {
+ b.Error(err)
+ }
+ tx.AddResponseHeader("Content-Type", "text/xml")
+ tx.ProcessResponseHeaders(200, "OK")
+ if _, err := tx.ProcessResponseBody(); err != nil {
+ b.Error(err)
+ }
+ tx.ProcessLogging()
+ if err := tx.Close(); err != nil {
+ b.Error(err)
+ }
+ }
+}
+
+func BenchmarkCRSXMLQueryLargeSOAP(b *testing.B) {
+ waf := crsWAFXMLQuery(b)
+
+ // ~4KB SOAP envelope with multiple items
+ var sb strings.Builder
+ sb.WriteString(`
+
+
+
+
+
+ `)
+ for i := 0; i < 100; i++ {
+ fmt.Fprintf(&sb, `
+
+ Item number %d
+ Description for item %d with some extra text to add size
+ %d.99
+ `, i, i, i, i*10+99)
+ }
+ sb.WriteString(`
+
+
+`)
+ xmlPayload := []byte(sb.String())
+
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ tx := waf.NewTransaction()
+ tx.ProcessConnection("127.0.0.1", 8080, "127.0.0.1", 8080)
+ tx.ProcessURI("/api/batch", "POST", "HTTP/1.1")
+ tx.AddRequestHeader("Host", "localhost")
+ tx.AddRequestHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36")
+ tx.AddRequestHeader("Content-Type", "application/soap+xml")
+ tx.ProcessRequestHeaders()
+ if _, _, err := tx.WriteRequestBody(xmlPayload); err != nil {
+ b.Error(err)
+ }
+ if _, err := tx.ProcessRequestBody(); err != nil {
+ b.Error(err)
+ }
+ tx.AddResponseHeader("Content-Type", "application/soap+xml")
+ tx.ProcessResponseHeaders(200, "OK")
+ if _, err := tx.ProcessResponseBody(); err != nil {
+ b.Error(err)
+ }
+ tx.ProcessLogging()
+ if err := tx.Close(); err != nil {
+ b.Error(err)
+ }
+ }
+}
diff --git a/testing/coreruleset/go.mod b/testing/coreruleset/go.mod
index 09cece66c..31dc29447 100644
--- a/testing/coreruleset/go.mod
+++ b/testing/coreruleset/go.mod
@@ -16,12 +16,15 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
+ github.com/antchfx/xmlquery v1.5.1 // indirect
+ github.com/antchfx/xpath v1.3.6 // indirect
github.com/corazawaf/libinjection-go v0.3.2 // indirect
github.com/coreruleset/ftw-tests-schema/v2 v2.3.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
+ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 // indirect
github.com/gotnospirit/messageformat v0.0.0-20221001023931-dfe49f1eb092 // indirect
diff --git a/testing/coreruleset/go.sum b/testing/coreruleset/go.sum
index 51da5c231..34cad2281 100644
--- a/testing/coreruleset/go.sum
+++ b/testing/coreruleset/go.sum
@@ -6,6 +6,10 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
+github.com/antchfx/xmlquery v1.5.1 h1:T9I4Ns1EXiWHy0IqKupGhnfTQtJwlGrpXtauYOoNv78=
+github.com/antchfx/xmlquery v1.5.1/go.mod h1:bVqnl7TaDXSReKINrhZz+2E/PbCu2tUahb+wZ7WZNT8=
+github.com/antchfx/xpath v1.3.6 h1:s0y+ElRRtTQdfHP609qFu0+c6bglDv20pqOViQjjdPI=
+github.com/antchfx/xpath v1.3.6/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc h1:OlJhrgI3I+FLUCTI3JJW8MoqyM78WbqJjecqMnqG+wc=
@@ -34,6 +38,10 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -111,29 +119,93 @@ github.com/valllabh/ocsf-schema-golang v1.0.3 h1:eR8k/3jP/OOqB8LRCtdJ4U+vlgd/gk5
github.com/valllabh/ocsf-schema-golang v1.0.3/go.mod h1:sZ3as9xqm1SSK5feFWIR2CuGeGRhsM7TR1MbpBctzPk=
github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=