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=