1- import { ParsedLog } from ' ./log-parser' ;
1+ import { ParsedLog } from " ./log-parser" ;
22
3- export type WhitelistLevel = 'global' | 'hostname' | 'uri' | 'method' | 'ip' | 'combined' ;
3+ export type WhitelistLevel =
4+ | "global"
5+ | "hostname"
6+ | "uri"
7+ | "method"
8+ | "ip"
9+ | "combined" ;
410
511export interface WhitelistRule {
612 level : WhitelistLevel ;
@@ -15,127 +21,134 @@ export interface WhitelistRule {
1521
1622export function generateWhitelistRules ( parsedLog : ParsedLog ) : WhitelistRule [ ] {
1723 const rules : WhitelistRule [ ] = [ ] ;
18- const ruleId = parsedLog . ruleId || '0' ;
24+ const ruleId = parsedLog . ruleId || "0" ;
1925
2026 // 1. Global whitelist - Disable rule completely (most permissive)
2127 rules . push ( {
22- level : ' global' ,
23- description : ' Disable rule globally (most permissive)' ,
24- descriptionKey : ' descGlobal' ,
25- rule : `SecRuleRemoveById ${ ruleId } `
28+ level : " global" ,
29+ description : " Disable rule globally (most permissive)" ,
30+ descriptionKey : " descGlobal" ,
31+ rule : `SecRuleRemoveById ${ ruleId } ` ,
2632 } ) ;
2733
2834 // 2. Hostname-based whitelist
2935 if ( parsedLog . hostname ) {
36+ const escapedHostname = escapeModSecString ( parsedLog . hostname ) ;
3037 rules . push ( {
31- level : ' hostname' ,
38+ level : " hostname" ,
3239 description : `Disable rule for hostname: ${ parsedLog . hostname } ` ,
33- descriptionKey : ' descHostname' ,
40+ descriptionKey : " descHostname" ,
3441 hostname : parsedLog . hostname ,
35- rule : `SecRule REQUEST_HEADERS:Host "@streq ${ parsedLog . hostname } " \\
42+ rule : `SecRule REQUEST_HEADERS:Host "@rx ^ ${ escapedHostname } $ " \\
3643 "id:${ generateCustomId ( ) } ,\\
3744 phase:1,\\
3845 pass,\\
3946 nolog,\\
40- ctl:ruleRemoveById=${ ruleId } "`
47+ ctl:ruleRemoveById=${ ruleId } "` ,
4148 } ) ;
4249 }
4350
4451 // 3. URI-based whitelist
4552 if ( parsedLog . uri ) {
53+ const escapedUri = escapeModSecString ( parsedLog . uri ) ;
4654 rules . push ( {
47- level : ' uri' ,
55+ level : " uri" ,
4856 description : `Disable rule for URI: ${ parsedLog . uri } ` ,
49- descriptionKey : ' descUri' ,
57+ descriptionKey : " descUri" ,
5058 uri : parsedLog . uri ,
51- rule : `SecRule REQUEST_URI "@streq ${ parsedLog . uri } " \\
59+ rule : `SecRule REQUEST_URI "@rx ^ ${ escapedUri } " \\
5260 "id:${ generateCustomId ( ) } ,\\
5361 phase:1,\\
5462 pass,\\
5563 nolog,\\
56- ctl:ruleRemoveById=${ ruleId } "`
64+ ctl:ruleRemoveById=${ ruleId } "` ,
5765 } ) ;
5866 }
5967
6068 // 4. Request Method-based whitelist
6169 if ( parsedLog . requestMethod ) {
6270 rules . push ( {
63- level : ' method' ,
71+ level : " method" ,
6472 description : `Disable rule for method: ${ parsedLog . requestMethod } ` ,
65- descriptionKey : ' descMethod' ,
73+ descriptionKey : " descMethod" ,
6674 method : parsedLog . requestMethod ,
67- rule : `SecRule REQUEST_METHOD "@streq ${ parsedLog . requestMethod } " \\
75+ rule : `SecRule REQUEST_METHOD "@rx ^ ${ parsedLog . requestMethod } $ " \\
6876 "id:${ generateCustomId ( ) } ,\\
6977 phase:1,\\
7078 pass,\\
7179 nolog,\\
72- ctl:ruleRemoveById=${ ruleId } "`
80+ ctl:ruleRemoveById=${ ruleId } "` ,
7381 } ) ;
7482 }
7583
7684 // 5. IP-based whitelist
7785 if ( parsedLog . clientIp ) {
7886 rules . push ( {
79- level : 'ip' ,
87+ level : "ip" ,
8088 description : `Disable rule for IP: ${ parsedLog . clientIp } ` ,
81- descriptionKey : ' descIp' ,
89+ descriptionKey : " descIp" ,
8290 ip : parsedLog . clientIp ,
8391 rule : `SecRule REMOTE_ADDR "@ipMatch ${ parsedLog . clientIp } " \\
8492 "id:${ generateCustomId ( ) } ,\\
8593 phase:1,\\
8694 pass,\\
8795 nolog,\\
88- ctl:ruleRemoveById=${ ruleId } "`
96+ ctl:ruleRemoveById=${ ruleId } "` ,
8997 } ) ;
9098 }
9199
92100 // 6. Combined whitelist - Hostname + URI (recommended)
93101 if ( parsedLog . hostname && parsedLog . uri ) {
102+ const escapedHostname = escapeModSecString ( parsedLog . hostname ) ;
103+ const escapedUri = escapeModSecString ( parsedLog . uri ) ;
94104 rules . push ( {
95- level : ' combined' ,
105+ level : " combined" ,
96106 description : `Disable rule for hostname + URI (recommended)` ,
97- descriptionKey : ' descCombinedHostUri' ,
107+ descriptionKey : " descCombinedHostUri" ,
98108 hostname : parsedLog . hostname ,
99109 uri : parsedLog . uri ,
100- rule : `SecRule REQUEST_HEADERS:Host "@streq ${ parsedLog . hostname } " \\
110+ rule : `SecRule REQUEST_HEADERS:Host "@rx ^ ${ escapedHostname } $ " \\
101111 "id:${ generateCustomId ( ) } ,\\
102112 phase:1,\\
103113 pass,\\
104114 nolog,\\
105115 chain"
106- SecRule REQUEST_URI "@streq ${ parsedLog . uri } " \\
107- "ctl:ruleRemoveById=${ ruleId } "`
116+ SecRule REQUEST_URI "@rx ^ ${ escapedUri } " \\
117+ "ctl:ruleRemoveById=${ ruleId } "` ,
108118 } ) ;
109119 }
110120
111121 // 7. Combined whitelist - Hostname + URI + Method (most restrictive)
112122 if ( parsedLog . hostname && parsedLog . uri && parsedLog . requestMethod ) {
123+ const escapedHostname = escapeModSecString ( parsedLog . hostname ) ;
124+ const escapedUri = escapeModSecString ( parsedLog . uri ) ;
113125 rules . push ( {
114- level : ' combined' ,
126+ level : " combined" ,
115127 description : `Disable rule for hostname + URI + method (most restrictive)` ,
116- descriptionKey : ' descCombinedHostUriMethod' ,
128+ descriptionKey : " descCombinedHostUriMethod" ,
117129 hostname : parsedLog . hostname ,
118130 uri : parsedLog . uri ,
119131 method : parsedLog . requestMethod ,
120- rule : `SecRule REQUEST_HEADERS:Host "@streq ${ parsedLog . hostname } " \\
132+ rule : `SecRule REQUEST_HEADERS:Host "@rx ^ ${ escapedHostname } $ " \\
121133 "id:${ generateCustomId ( ) } ,\\
122134 phase:1,\\
123135 pass,\\
124136 nolog,\\
125137 chain"
126- SecRule REQUEST_URI "@streq ${ parsedLog . uri } " \\
127- "chain"
128- SecRule REQUEST_METHOD "@streq ${ parsedLog . requestMethod } " \\
129- "ctl:ruleRemoveById=${ ruleId } "`
138+ SecRule REQUEST_URI "@rx ^ ${ escapedUri } " \\
139+ "chain"
140+ SecRule REQUEST_METHOD "@rx ^ ${ parsedLog . requestMethod } $ " \\
141+ "ctl:ruleRemoveById=${ ruleId } "` ,
130142 } ) ;
131143 }
132144
133145 // 8. Combined whitelist - IP + URI
134146 if ( parsedLog . clientIp && parsedLog . uri ) {
147+ const escapedUri = escapeModSecString ( parsedLog . uri ) ;
135148 rules . push ( {
136- level : ' combined' ,
149+ level : " combined" ,
137150 description : `Disable rule for IP + URI` ,
138- descriptionKey : ' descCombinedIpUri' ,
151+ descriptionKey : " descCombinedIpUri" ,
139152 ip : parsedLog . clientIp ,
140153 uri : parsedLog . uri ,
141154 rule : `SecRule REMOTE_ADDR "@ipMatch ${ parsedLog . clientIp } " \\
@@ -144,14 +157,20 @@ SecRule REQUEST_METHOD "@streq ${parsedLog.requestMethod}" \\
144157 pass,\\
145158 nolog,\\
146159 chain"
147- SecRule REQUEST_URI "@streq ${ parsedLog . uri } " \\
148- "ctl:ruleRemoveById=${ ruleId } "`
160+ SecRule REQUEST_URI "@rx ^ ${ escapedUri } " \\
161+ "ctl:ruleRemoveById=${ ruleId } "` ,
149162 } ) ;
150163 }
151164
152165 return rules ;
153166}
154167
168+ // Escape special regex characters for ModSecurity
169+ function escapeModSecString ( str : string ) : string {
170+ // Escape special regex characters
171+ return str . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) ;
172+ }
173+
155174// Generate a custom rule ID in range 88,000,000 - 89,999,999
156175// This range ensures no conflicts with existing ModSecurity rules
157176const MIN_CUSTOM_ID = 88000000 ;
0 commit comments