Skip to content

Commit 9a4d1d1

Browse files
authored
Merge pull request #17 from Sebitosh/constants
Utility change: reusable constants in yaml schema
2 parents 11913a3 + 431c3ac commit 9a4d1d1

File tree

7 files changed

+338
-1
lines changed

7 files changed

+338
-1
lines changed

README.md

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,103 @@ The field `input.encoded_request` allows defining a whole request encoded in bas
454454
encoded_request: R0VUIC8gSFRUUC8xLjENCkhvc3Q6IGxvY2FsaG9zdA0KDQo=
455455
```
456456

457-
### output / additional checks
457+
### Constants
458+
The yaml schema has a mechanism to handle global and local constants.
459+
460+
~~~yaml
461+
global:
462+
default_constants:
463+
one: 1
464+
TWO: 2
465+
two_in_list:
466+
- 2
467+
FOO_IN_DICT:
468+
foo: attack
469+
...
470+
471+
constants:
472+
HEADERS_IN_DICTIONARY:
473+
headers:
474+
- name: test
475+
value: test
476+
- name: one
477+
value: ~{one}~
478+
- name: 2
479+
value: ~{TWO}~
480+
template_in_list:
481+
- SecRule for TARGETS
482+
- Template with constants
483+
one: one
484+
~~~
485+
486+
Global constants are defined under the `global.default_constants` field. They are accessible across files and are reset whenever a new `global` field is defined.
487+
488+
Local constants are defined under a `constants` field at the root of a file. They are only accessible in the file they are defined in.
489+
490+
#### Syntax
491+
Constants are defined as key-value pairs where:
492+
493+
~~~yaml
494+
NAME: VALUE
495+
~~~
496+
497+
The name is used for referencing the constant and the value is used for the substitution. Referencing a constant can be done inside the value of any other key in the API. References use the `~{...}~` separators like so:
498+
499+
~~~yaml
500+
~{NAME}~
501+
~~~
502+
503+
Variable names can be lower or upper case and are case sensitive.
504+
505+
#### Properties
506+
507+
Constants can be yaml scalars, lists, or dictionaries:
508+
509+
~~~yaml
510+
scalar: 1
511+
list:
512+
- 1
513+
dictionary:
514+
1: 1
515+
~~~
516+
517+
Constants can reference other constants in their values:
518+
519+
~~~yaml
520+
headers:
521+
- name: one
522+
value: ~{one}~
523+
- name: two
524+
value: ~{TWO}~
525+
~~~
526+
527+
Local constants with the same name as global constants have precedence in their local scope:
528+
~~~yaml
529+
global:
530+
default_constants:
531+
ONE: 1
532+
...
533+
constants:
534+
ONE: one
535+
...
536+
key: ~{ONE}~ # substituted by 'one'
537+
~~~
538+
Values can contain multiple references, such as in templates:
539+
540+
~~~yaml
541+
- name: "Template with constants"
542+
template: |
543+
SecRule ~{target}~ "${OPERATOR}$ ${OPARG}$" \
544+
"id:${CURRID}$,\
545+
phase:${PHASE}$,\
546+
deny,\
547+
t:~{None}~,\
548+
log,\
549+
msg:'%{MATCHED_VAR_NAME} was caught in phase:${PHASE}$',\
550+
ver:'~{VERSION}~'"
551+
~~~
552+
553+
### Output / additional checks
458554

459555
By default, the generator will produce checks for tests with `go-ftw`'s `expect_ids` field using the current rule id as parameter. If the associated rule matches and it's id put in the log, the test will pass.
460556

feature_demo/config_tests/DEMO_000_GLOBAL.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,26 @@ global:
4646
ver:'${VERSION}$'"
4747
4848
${DIRECTIVES}$
49+
- name: "Template with constants"
50+
template: |
51+
SecRule ${TARGET}$ "${OPERATOR}$ ${OPARG}$" \
52+
"id:${CURRID}$,\
53+
phase:~{phase}~,\
54+
deny,\
55+
t:~{None}~,\
56+
log,\
57+
msg:'%{MATCHED_VAR_NAME} was caught in phase:~{phase}~',\
58+
ver:'~{VERSION}~'"
4959
default_tests_phase_methods:
5060
- 2: post
61+
default_constants:
62+
one: local constants have precedence
63+
TWO: 2
64+
two_in_list:
65+
- 2
66+
FOO_IN_DICT:
67+
foo: attack
68+
constants:
69+
phase: ${PHASE}$
70+
VERSION: ${VERSION}$
71+
None: none
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
target: ARGS
2+
rulefile: DEMO_006_CONSTANTS.conf
3+
testfile: DEMO_006_CONSTANTS.yaml
4+
constants:
5+
one: one
6+
template_in_list:
7+
- SecRule for TARGETS
8+
- Template with constants
9+
HEADERS_IN_DICTIONARY:
10+
headers:
11+
- name: test
12+
value: test
13+
- name: one
14+
value: ~{one}~
15+
- name: 2
16+
value: ~{TWO}~
17+
templates: ~{template_in_list}~
18+
colkey:
19+
- - ''
20+
operator:
21+
- '@contains'
22+
oparg:
23+
- attack
24+
phase: ~{two_in_list}~
25+
testdata:
26+
phase_methods:
27+
2: post
28+
targets:
29+
- target: ''
30+
test:
31+
data:
32+
foo: attack
33+
input:
34+
headers:
35+
- name: one
36+
value: ~{one}~
37+
- name: 2
38+
value: ~{TWO}~
39+
- target: ''
40+
test:
41+
data: ~{FOO_IN_DICT}~
42+
input: ~{HEADERS_IN_DICTIONARY}~
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
SecRule ARGS "@contains attack" \
2+
"id:100011,\
3+
phase:2,\
4+
deny,\
5+
t:none,\
6+
log,\
7+
msg:'%{MATCHED_VAR_NAME} was caught in phase:2',\
8+
ver:'MRTS/0.1'"
9+
10+
SecRule ARGS "@contains attack" \
11+
"id:100012,\
12+
phase:2,\
13+
deny,\
14+
t:none,\
15+
log,\
16+
msg:'%{MATCHED_VAR_NAME} was caught in phase:2',\
17+
ver:'MRTS/0.1'"
18+
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
meta:
3+
author: MRTS generate-rules.py
4+
enabled: true
5+
name: DEMO_006_CONSTANTS.yaml
6+
description: Desc
7+
tests:
8+
- test_title: 100011-1
9+
ruleid: 100011
10+
test_id: 1
11+
desc: 'Test case for rule 100011, #1'
12+
stages:
13+
- description: Send request
14+
input:
15+
dest_addr: 127.0.0.1
16+
port: 80
17+
protocol: http
18+
method: POST
19+
headers:
20+
User-Agent: OWASP MRTS test agent
21+
Host: localhost
22+
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
23+
one: one
24+
2: 2
25+
uri: /post
26+
version: HTTP/1.1
27+
data: foo=attack
28+
output:
29+
log:
30+
expect_ids:
31+
- 100011
32+
- test_title: 100011-2
33+
ruleid: 100011
34+
test_id: 2
35+
desc: 'Test case for rule 100011, #2'
36+
stages:
37+
- description: Send request
38+
input:
39+
dest_addr: 127.0.0.1
40+
port: 80
41+
protocol: http
42+
method: POST
43+
headers:
44+
User-Agent: OWASP MRTS test agent
45+
Host: localhost
46+
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
47+
test: test
48+
one: one
49+
2: 2
50+
uri: /post
51+
version: HTTP/1.1
52+
data: foo=attack
53+
output:
54+
log:
55+
expect_ids:
56+
- 100011
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
meta:
3+
author: MRTS generate-rules.py
4+
enabled: true
5+
name: DEMO_006_CONSTANTS.yaml
6+
description: Desc
7+
tests:
8+
- test_title: 100012-1
9+
ruleid: 100012
10+
test_id: 1
11+
desc: 'Test case for rule 100012, #1'
12+
stages:
13+
- description: Send request
14+
input:
15+
dest_addr: 127.0.0.1
16+
port: 80
17+
protocol: http
18+
method: POST
19+
headers:
20+
User-Agent: OWASP MRTS test agent
21+
Host: localhost
22+
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
23+
one: one
24+
2: 2
25+
uri: /post
26+
version: HTTP/1.1
27+
data: foo=attack
28+
output:
29+
log:
30+
expect_ids:
31+
- 100012
32+
- test_title: 100012-2
33+
ruleid: 100012
34+
test_id: 2
35+
desc: 'Test case for rule 100012, #2'
36+
stages:
37+
- description: Send request
38+
input:
39+
dest_addr: 127.0.0.1
40+
port: 80
41+
protocol: http
42+
method: POST
43+
headers:
44+
User-Agent: OWASP MRTS test agent
45+
Host: localhost
46+
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
47+
test: test
48+
one: one
49+
2: 2
50+
uri: /post
51+
version: HTTP/1.1
52+
data: foo=attack
53+
output:
54+
log:
55+
expect_ids:
56+
- 100012

mrts/generate-rules.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import string
99
import re
1010
import copy
11+
from ast import literal_eval
1112

1213
NAME = "MRTS"
1314
VERSION = "0.1"
@@ -33,6 +34,7 @@ def __init__(self, flist, expdir, testdir):
3334
'phase' : [1,2,3,4],
3435
'actions' : [],
3536
'directives' : [],
37+
'constants' : {},
3638
'phase_methods': {}
3739
}
3840
self.default_test_phase_methods = {
@@ -43,6 +45,8 @@ def __init__(self, flist, expdir, testdir):
4345
5: "post"
4446
}
4547

48+
self.default_constants = {}
49+
4650
self.current_confdata = {}
4751
self.current_testdata = {}
4852

@@ -54,6 +58,7 @@ def __init__(self, flist, expdir, testdir):
5458
self.testcontent = {}
5559

5660
self.re_tplvars = re.compile(r"""\$\{[^ \n\t$,'"]*\}\$""")
61+
self.re_constants = re.compile(r"""~\{([^ \n\t$,'"]*)\}~""")
5762

5863
self.testdict = {
5964
'header': {
@@ -115,6 +120,10 @@ def __init__(self, flist, expdir, testdir):
115120

116121
def parseconf(self, c):
117122
"""parsing a configuration file"""
123+
# Before anything, load and replace constants for the current file.
124+
if re.search(self.re_constants, str(c)) is not None:
125+
c = self.parseconstants(c)
126+
118127
# if there is a 'global' section, fill the possible global vars
119128
if 'global' in c:
120129
for k in c['global']:
@@ -156,6 +165,45 @@ def parseconf(self, c):
156165
else:
157166
pass
158167

168+
def parseconstants(self, c):
169+
"""Load and replace any used constants in current configuration"""
170+
# per-file local
171+
if 'constants' in c:
172+
self.current_confdata['constants'] = c['constants']
173+
# cross-file global
174+
if 'global' in c:
175+
if 'default_constants' in c['global']:
176+
self.default_constants = c['global']['default_constants']
177+
else: # if no global constants, reset values defined under previous 'global' field
178+
self.default_constants = {}
179+
return self.swap_constants(c)
180+
181+
def swap_constants(self, c):
182+
if isinstance(c, dict):
183+
for key, val in c.items():
184+
c[key] = self.swap_constants(val)
185+
if isinstance(c, list):
186+
for i in range(len(c)):
187+
c[i] = self.swap_constants(c[i])
188+
if isinstance(c, str):
189+
matches = re.findall(self.re_constants, c)
190+
for match in matches:
191+
if match in self.current_confdata['constants']:
192+
og_type = type(self.current_confdata['constants'][match])
193+
c = c.replace(f"~{{{match}}}~", str(self.current_confdata['constants'][match]))
194+
if og_type in (list, dict):
195+
c = literal_eval(c)
196+
else:
197+
c = og_type(c)
198+
elif match in self.default_constants:
199+
og_type = type(self.default_constants[match])
200+
c = c.replace(f"~{{{match}}}~", str(self.default_constants[match]))
201+
if og_type in (list, dict):
202+
c = literal_eval(c)
203+
else:
204+
c = og_type(c)
205+
return c
206+
159207
def genrulefromtemplate(self, tpl, current_confdata):
160208
"""
161209
generate a rule from data based on the template

0 commit comments

Comments
 (0)