Skip to content

Commit 639a62c

Browse files
authored
add ddwaf_update bindings API (#24)
* Add low-level bindings for `ddwaf_update` * Add high-level `(*Handle) Update(ruleset any) (*Handle, error)` * Add Test for the high level API (please request more if needed) Signed-off-by: Eliott Bouhana <[email protected]>
1 parent d7a53c6 commit 639a62c

File tree

4 files changed

+150
-31
lines changed

4 files changed

+150
-31
lines changed

handle.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,43 @@ func (handle *Handle) Addresses() []string {
103103
return wafLib.wafRequiredAddresses(handle.cHandle)
104104
}
105105

106+
// Update the ruleset of a WAF instance into a new handle on its own
107+
// the previous handle still needs to be closed manually
108+
func (handle *Handle) Update(newRules any) (*Handle, error) {
109+
110+
encoder := newMaxEncoder()
111+
obj, err := encoder.Encode(newRules)
112+
if err != nil {
113+
return nil, fmt.Errorf("could not encode the WAF ruleset into a WAF object: %w", err)
114+
}
115+
116+
cRulesetInfo := new(wafRulesetInfo)
117+
118+
cHandle := wafLib.wafUpdate(handle.cHandle, obj, cRulesetInfo)
119+
keepAlive(encoder.cgoRefs)
120+
if cHandle == 0 {
121+
return nil, errors.New("could not update the WAF instance")
122+
}
123+
124+
defer wafLib.wafRulesetInfoFree(cRulesetInfo)
125+
126+
errorsMap, err := decodeErrors(&cRulesetInfo.errors)
127+
if err != nil { // Something is very wrong
128+
return nil, fmt.Errorf("could not decode the WAF ruleset errors: %w", err)
129+
}
130+
131+
return &Handle{
132+
cHandle: cHandle,
133+
refCounter: atomic.NewInt32(1), // We count the handle itself in the counter
134+
rulesetInfo: RulesetInfo{
135+
Loaded: cRulesetInfo.loaded,
136+
Failed: cRulesetInfo.failed,
137+
Errors: errorsMap,
138+
Version: gostring(cast[byte](cRulesetInfo.version)),
139+
},
140+
}, nil
141+
}
142+
106143
// closeContext calls ddwaf_context_destroy and eventually ddwaf_destroy on the handle
107144
func (handle *Handle) closeContext(context *Context) {
108145
wafLib.wafContextDestroy(context.cContext)

waf_dl.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type wafDl struct {
2121

2222
Ddwaf_ruleset_info_free uintptr `dlsym:"ddwaf_ruleset_info_free"`
2323
Ddwaf_init uintptr `dlsym:"ddwaf_init"`
24+
Ddwaf_update uintptr `dlsym:"ddwaf_update"`
2425
Ddwaf_destroy uintptr `dlsym:"ddwaf_destroy"`
2526
Ddwaf_required_addresses uintptr `dlsym:"ddwaf_required_addresses"`
2627
Ddwaf_get_version uintptr `dlsym:"ddwaf_get_version"`
@@ -99,14 +100,21 @@ func (waf *wafDl) wafGetVersion() string {
99100
return gostring(cast[byte](waf.syscall(waf.Ddwaf_get_version)))
100101
}
101102

102-
func (waf *wafDl) wafInit(obj *wafObject, config *wafConfig, info *wafRulesetInfo) wafHandle {
103-
handle := wafHandle(waf.syscall(waf.Ddwaf_init, ptrToUintptr(obj), ptrToUintptr(config), ptrToUintptr(info)))
104-
keepAlive(obj)
103+
func (waf *wafDl) wafInit(ruleset *wafObject, config *wafConfig, info *wafRulesetInfo) wafHandle {
104+
handle := wafHandle(waf.syscall(waf.Ddwaf_init, ptrToUintptr(ruleset), ptrToUintptr(config), ptrToUintptr(info)))
105+
keepAlive(ruleset)
105106
keepAlive(config)
106107
keepAlive(info)
107108
return handle
108109
}
109110

111+
func (waf *wafDl) wafUpdate(handle wafHandle, ruleset *wafObject, info *wafRulesetInfo) wafHandle {
112+
newHandle := wafHandle(waf.syscall(waf.Ddwaf_update, uintptr(handle), ptrToUintptr(ruleset), ptrToUintptr(info)))
113+
keepAlive(ruleset)
114+
keepAlive(info)
115+
return newHandle
116+
}
117+
110118
func (waf *wafDl) wafRulesetInfoFree(info *wafRulesetInfo) {
111119
waf.syscall(waf.Ddwaf_ruleset_info_free, ptrToUintptr(info))
112120
keepAlive(info)

waf_dl_unsupported.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ func (waf *wafDl) wafInit(obj *wafObject, config *wafConfig, info *wafRulesetInf
2222
return 0
2323
}
2424

25+
func (waf *wafDl) wafUpdate(handle wafHandle, ruleset *wafObject, info *wafRulesetInfo) wafHandle {
26+
return 0
27+
}
28+
2529
func (waf *wafDl) wafRulesetInfoFree(info *wafRulesetInfo) {
2630
}
2731

waf_test.go

Lines changed: 98 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,34 @@ var testArachniRuleTmpl = template.Must(template.New("").Parse(`
8989
}
9090
`))
9191

92+
// Test with a valid JSON but invalid rule format (field "events" should be an array)
93+
const malformedRule = `
94+
{
95+
"version": "2.1",
96+
"events": [
97+
{
98+
"id": "ua0-600-12x",
99+
"name": "Arachni",
100+
"tags": {
101+
"type": "security_scanner"
102+
},
103+
"conditions": [
104+
{
105+
"operation": "match_regex",
106+
"parameters": {
107+
"inputs": [
108+
{ "address": "server.request.headers.no_cookies" }
109+
],
110+
"regex": "^Arachni"
111+
}
112+
}
113+
],
114+
"transformers": []
115+
}
116+
]
117+
}
118+
`
119+
92120
type ruleInput struct {
93121
Address string
94122
KeyPath []string
@@ -124,43 +152,85 @@ func TestNewWAF(t *testing.T) {
124152
})
125153

126154
t.Run("invalid-rule", func(t *testing.T) {
127-
// Test with a valid JSON but invalid rule format (field events should be an array)
128-
const rule = `
129-
{
130-
"version": "2.1",
131-
"events": [
132-
{
133-
"id": "ua0-600-12x",
134-
"name": "Arachni",
135-
"tags": {
136-
"type": "security_scanner"
137-
},
138-
"conditions": [
139-
{
140-
"operation": "match_regex",
141-
"parameters": {
142-
"inputs": [
143-
{ "address": "server.request.headers.no_cookies" }
144-
],
145-
"regex": "^Arachni"
146-
}
147-
}
148-
],
149-
"transformers": []
150-
}
151-
]
152-
}
153-
`
154155
var parsed any
155156

156-
require.NoError(t, json.Unmarshal([]byte(rule), &parsed))
157+
require.NoError(t, json.Unmarshal([]byte(malformedRule), &parsed))
157158

158159
waf, err := newDefaultHandle(parsed)
159160
require.Error(t, err)
160161
require.Nil(t, waf)
161162
})
162163
}
163164

165+
func TestUpdateWAF(t *testing.T) {
166+
167+
t.Run("valid-rule", func(t *testing.T) {
168+
waf, err := newDefaultHandle(testArachniRule)
169+
require.NoError(t, err)
170+
require.NotNil(t, waf)
171+
defer waf.Close()
172+
173+
waf2, err := waf.Update(newArachniTestRule([]ruleInput{{Address: "my.input"}}, nil))
174+
require.NoError(t, err)
175+
require.NotNil(t, waf2)
176+
defer waf2.Close()
177+
})
178+
179+
t.Run("changes", func(t *testing.T) {
180+
waf, err := newDefaultHandle(newArachniTestRule([]ruleInput{{Address: "my.input"}}, nil))
181+
require.NoError(t, err)
182+
require.NotNil(t, waf)
183+
defer waf.Close()
184+
185+
wafCtx := NewContext(waf)
186+
defer wafCtx.Close()
187+
188+
// Matches
189+
values := map[string]interface{}{
190+
"my.input": "Arachni",
191+
}
192+
matches, actions, err := wafCtx.Run(values, time.Second)
193+
require.NoError(t, err)
194+
require.NotEmpty(t, matches)
195+
require.Nil(t, actions)
196+
197+
// Update
198+
waf2, err := waf.Update(newArachniTestRule([]ruleInput{{Address: "my.input"}}, []string{"block"}))
199+
require.NoError(t, err)
200+
require.NotNil(t, waf2)
201+
defer waf2.Close()
202+
203+
wafCtx2 := NewContext(waf2)
204+
defer wafCtx2.Close()
205+
206+
// Matches & Block
207+
values = map[string]interface{}{
208+
"my.input": "Arachni",
209+
}
210+
matches, actions, err = wafCtx2.Run(values, time.Second)
211+
require.NoError(t, err)
212+
require.NotEmpty(t, matches)
213+
require.NotEmpty(t, actions)
214+
215+
})
216+
217+
t.Run("invalid-rule", func(t *testing.T) {
218+
219+
waf, err := newDefaultHandle(testArachniRule)
220+
require.NoError(t, err)
221+
require.NotNil(t, waf)
222+
defer waf.Close()
223+
224+
var parsed any
225+
226+
require.NoError(t, json.Unmarshal([]byte(malformedRule), &parsed))
227+
228+
waf2, err := waf.Update(parsed)
229+
require.Error(t, err)
230+
require.Nil(t, waf2)
231+
})
232+
}
233+
164234
func TestMatching(t *testing.T) {
165235

166236
waf, err := newDefaultHandle(newArachniTestRule([]ruleInput{{Address: "my.input"}}, nil))

0 commit comments

Comments
 (0)