From 85919e341d2fcd09e8a1576acfa67d9dc0151767 Mon Sep 17 00:00:00 2001 From: TBX3D <88289044+TBX3D@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:35:17 -0700 Subject: [PATCH] feat(frameworks): add web application firewall detectors detect cloudflare waf, sucuri, incapsula, f5 big-ip asm and modsecurity from the headers, cookies and block-page strings the waf itself sets, so a site that merely names the vendor in prose does not trip them. grouped in one file so the category can be reviewed or excluded as a unit. --- internal/scan/frameworks/detectors/waf.go | 134 ++++++++++++++++++ .../scan/frameworks/detectors/waf_test.go | 80 +++++++++++ 2 files changed, 214 insertions(+) create mode 100644 internal/scan/frameworks/detectors/waf.go create mode 100644 internal/scan/frameworks/detectors/waf_test.go diff --git a/internal/scan/frameworks/detectors/waf.go b/internal/scan/frameworks/detectors/waf.go new file mode 100644 index 00000000..5412eae2 --- /dev/null +++ b/internal/scan/frameworks/detectors/waf.go @@ -0,0 +1,134 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +/* + + BSD 3-Clause License + (c) 2022-2026 vmfunc, xyzeva & contributors + +*/ + +package detectors + +import ( + "net/http" + + fw "github.com/vmfunc/sif/internal/scan/frameworks" +) + +// The detectors here identify the WAF in front of a target. Each keys on a +// header, Set-Cookie name, or block-page string the WAF itself sets, so a page +// that merely names the vendor in prose does not trip it. +func init() { + fw.Register(&cloudflareWAFDetector{}) + fw.Register(&sucuriDetector{}) + fw.Register(&incapsulaDetector{}) + fw.Register(&bigipASMDetector{}) + fw.Register(&modSecurityDetector{}) +} + +// cloudflareWAFDetector detects an active Cloudflare WAF or bot challenge via the +// cf-mitigated header and __cf_chl_ tokens, which appear only on an interception +// response (plain proxying is covered by the cf-ray CDN marker). +type cloudflareWAFDetector struct{} + +func (d *cloudflareWAFDetector) Name() string { return "Cloudflare WAF" } + +func (d *cloudflareWAFDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "cf-mitigated", Weight: 0.6, HeaderOnly: true}, + {Pattern: "__cf_chl_", Weight: 0.5}, + {Pattern: "Attention Required! | Cloudflare", Weight: 0.4}, + } +} + +func (d *cloudflareWAFDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + return sigmoidConfidence(base.MatchSignatures(body, headers)), "" +} + +// sucuriDetector detects the Sucuri (GoDaddy) cloud WAF via its x-sucuri-id and +// x-sucuri-cache headers, backed by the block-page string. +type sucuriDetector struct{} + +func (d *sucuriDetector) Name() string { return "Sucuri WAF" } + +func (d *sucuriDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "x-sucuri-id", Weight: 0.6, HeaderOnly: true}, + {Pattern: "x-sucuri-cache", Weight: 0.4, HeaderOnly: true}, + {Pattern: "Sucuri WebSite Firewall", Weight: 0.5}, + } +} + +func (d *sucuriDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + return sigmoidConfidence(base.MatchSignatures(body, headers)), "" +} + +// incapsulaDetector detects the Imperva Incapsula cloud WAF via its x-iinfo +// header, incap_ses_ Set-Cookie name, and block-page incident string. Each is +// Incapsula-exclusive and clears the threshold on its own. +type incapsulaDetector struct{} + +func (d *incapsulaDetector) Name() string { return "Imperva Incapsula" } + +func (d *incapsulaDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "x-iinfo", Weight: 0.6, HeaderOnly: true}, + {Pattern: "incap_ses_", Weight: 0.5, HeaderOnly: true}, + {Pattern: "Incapsula incident ID", Weight: 0.5}, + } +} + +func (d *incapsulaDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + return sigmoidConfidence(base.MatchSignatures(body, headers)), "" +} + +// bigipASMDetector detects the F5 BIG-IP Application Security Manager via its +// block-page rejection sentence and support-id line. Block-page only, so it never +// fires on a passing response. +type bigipASMDetector struct{} + +func (d *bigipASMDetector) Name() string { return "F5 BIG-IP ASM" } + +func (d *bigipASMDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "The requested URL was rejected. Please consult with your administrator.", Weight: 0.6}, + {Pattern: "Your support ID is:", Weight: 0.3}, + } +} + +func (d *bigipASMDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + return sigmoidConfidence(base.MatchSignatures(body, headers)), "" +} + +// modSecurityDetector detects ModSecurity via its generated block page (primary) +// and the Mod_Security/NOYB server-token values (supporting, since suppressible). +type modSecurityDetector struct{} + +func (d *modSecurityDetector) Name() string { return "ModSecurity" } + +func (d *modSecurityDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "This error was generated by Mod_Security", Weight: 0.6}, + {Pattern: "Mod_Security", Weight: 0.3, HeaderOnly: true}, + {Pattern: "NOYB", Weight: 0.2, HeaderOnly: true}, + } +} + +func (d *modSecurityDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + return sigmoidConfidence(base.MatchSignatures(body, headers)), "" +} diff --git a/internal/scan/frameworks/detectors/waf_test.go b/internal/scan/frameworks/detectors/waf_test.go new file mode 100644 index 00000000..3f42b208 --- /dev/null +++ b/internal/scan/frameworks/detectors/waf_test.go @@ -0,0 +1,80 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package detectors + +import ( + "net/http" + "testing" + + fw "github.com/vmfunc/sif/internal/scan/frameworks" +) + +// TestWAFDetectors_Positive fires each WAF detector on a marker shaped like the +// real header, Set-Cookie, or block-page body that WAF emits. +func TestWAFDetectors_Positive(t *testing.T) { + tests := []struct { + name string + detector fw.Detector + body string + headers http.Header + }{ + {"Cloudflare mitigated header", &cloudflareWAFDetector{}, "", http.Header{"Cf-Mitigated": {"challenge"}}}, + {"Cloudflare challenge page", &cloudflareWAFDetector{}, `Attention Required! | Cloudflare`, http.Header{}}, + {"Sucuri id header", &sucuriDetector{}, "", http.Header{"X-Sucuri-Id": {"17.5"}}}, + {"Sucuri block page", &sucuriDetector{}, `Sucuri WebSite Firewall - Access Denied`, http.Header{"X-Sucuri-Cache": {"MISS"}}}, + {"Incapsula iinfo header", &incapsulaDetector{}, "", http.Header{"X-Iinfo": {"9-12345-0 0NNN RT(1) q(0) r(0)"}}}, + {"Incapsula cookies", &incapsulaDetector{}, "", http.Header{"Set-Cookie": {"visid_incap_123=abc; path=/", "incap_ses_456=def; path=/"}}}, + {"Incapsula incident page", &incapsulaDetector{}, `Request unsuccessful. Incapsula incident ID: 123-456`, http.Header{}}, + {"F5 BIG-IP ASM block", &bigipASMDetector{}, `The requested URL was rejected. Please consult with your administrator.
Your support ID is: 987654321`, http.Header{}}, + {"ModSecurity block page", &modSecurityDetector{}, `

This error was generated by Mod_Security.

`, http.Header{}}, + {"ModSecurity server token", &modSecurityDetector{}, `This error was generated by Mod_Security.`, http.Header{"Server": {"Apache Mod_Security"}}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf, _ := tt.detector.Detect(tt.body, tt.headers) + if conf <= 0.5 { + t.Errorf("%s: confidence = %.3f, want > 0.5", tt.name, conf) + } + }) + } +} + +// TestWAFDetectors_Negative keeps the detectors quiet on prose that names the +// vendor and on unrelated proxy headers, so a mention or a coincidental header +// cannot trip a WAF finding. +func TestWAFDetectors_Negative(t *testing.T) { + tests := []struct { + name string + detector fw.Detector + body string + headers http.Header + }{ + {"Cloudflare plain proxy", &cloudflareWAFDetector{}, "", http.Header{"Cf-Ray": {"8a1-EWR"}}}, + {"Cloudflare prose", &cloudflareWAFDetector{}, `

We compared Cloudflare and Fastly for WAF coverage.

`, http.Header{}}, + {"Sucuri prose", &sucuriDetector{}, `

Sucuri is a popular website firewall.

`, http.Header{}}, + {"Incapsula prose", &incapsulaDetector{}, `

Imperva Incapsula competes with Cloudflare.

`, http.Header{}}, + {"F5 unrelated body", &bigipASMDetector{}, `

Our support team can help with your F5 BIG-IP setup.

`, http.Header{}}, + {"ModSecurity prose", &modSecurityDetector{}, `

We enabled ModSecurity rules on the origin.

`, http.Header{}}, + {"plain nginx", &sucuriDetector{}, "", http.Header{"Server": {"nginx/1.25.3"}}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf, _ := tt.detector.Detect(tt.body, tt.headers) + if conf > 0.5 { + t.Errorf("%s: confidence = %.3f, want <= 0.5", tt.name, conf) + } + }) + } +}