Skip to content

Commit d393997

Browse files
authored
feat: expose Builder API to manage default recommended ruleset (#131)
1 parent c7178aa commit d393997

File tree

4 files changed

+109
-4
lines changed

4 files changed

+109
-4
lines changed

.github/workflows/_test_bare_metal.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,20 @@ jobs:
2828
run: sudo apt update && sudo apt install -y build-essential
2929
- name: Install gotestsum
3030
run: go install gotest.tools/gotestsum@latest
31+
- name: Generate a GitHub token
32+
id: generate-token
33+
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
34+
with:
35+
app-id: ${{ vars.DD_K9_LIBRARY_GO_APP_ID }}
36+
private-key: ${{ secrets.DD_K9_LIBRARY_GO_APP_PRIVATE_KEY }}
37+
owner: DataDog
38+
repositories: appsec-event-rules
39+
permission-contents: read
3140
- name: go test
3241
shell: bash
3342
run: ./.github/workflows/ci.sh
3443
env:
3544
DD_APPSEC_WAF_LOG_LEVEL: ${{ matrix.waf-log-level }}
3645
# We need a GITHUB_TOKEN in order to access the latest release of the AppSec rules from
3746
# the DataDog/appsec-rules repository.
38-
GITHUB_TOKEN: ${{ secrets.ACCESS_RULES_GITHUB_TOKEN }}
47+
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}

builder.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"runtime"
1212

1313
"github.com/DataDog/go-libddwaf/v4/internal/bindings"
14+
"github.com/DataDog/go-libddwaf/v4/internal/ruleset"
1415
)
1516

1617
// Builder manages an evolving WAF configuration over time. Its lifecycle is
@@ -20,7 +21,8 @@ import (
2021
// or similar when sharing it across multiple goroutines. All methods of this
2122
// type are safe to call with a nil receiver.
2223
type Builder struct {
23-
handle bindings.WAFBuilder
24+
handle bindings.WAFBuilder
25+
defaultLoaded bool
2426
}
2527

2628
// NewBuilder creates a new [Builder] instance. Its lifecycle is typically tied
@@ -58,6 +60,37 @@ var (
5860
errBuilderClosed = errors.New("builder has already been closed")
5961
)
6062

63+
const defaultRecommendedRulesetPath = "::/go-libddwaf/default/recommended.json"
64+
65+
// AddDefaultRecommendedRuleset adds the default recommended ruleset to the
66+
// receiving [Builder], and returns the [Diagnostics] produced in the process.
67+
func (b *Builder) AddDefaultRecommendedRuleset() (Diagnostics, error) {
68+
var pinner runtime.Pinner
69+
defer pinner.Unpin()
70+
71+
ruleset, err := ruleset.DefaultRuleset(&pinner)
72+
if err != nil {
73+
return Diagnostics{}, fmt.Errorf("failed to load default recommended ruleset: %w", err)
74+
}
75+
76+
diag, err := b.addOrUpdateConfig(defaultRecommendedRulesetPath, &ruleset)
77+
if err == nil {
78+
b.defaultLoaded = true
79+
}
80+
return diag, err
81+
}
82+
83+
// RemoveDefaultRecommendedRuleset removes the default recommended ruleset from
84+
// the receiving [Builder]. Returns true if the removal occurred (meaning the
85+
// default recommended ruleset was indeed present in the builder).
86+
func (b *Builder) RemoveDefaultRecommendedRuleset() bool {
87+
if b.RemoveConfig(defaultRecommendedRulesetPath) {
88+
b.defaultLoaded = false
89+
return true
90+
}
91+
return false
92+
}
93+
6194
// AddOrUpdateConfig adds or updates a configuration fragment to this [Builder].
6295
// Returns the [Diagnostics] produced by adding or updating this configuration.
6396
func (b *Builder) AddOrUpdateConfig(path string, fragment any) (Diagnostics, error) {
@@ -82,15 +115,22 @@ func (b *Builder) AddOrUpdateConfig(path string, fragment any) (Diagnostics, err
82115
return Diagnostics{}, fmt.Errorf("could not encode the config fragment into a WAF object; %w", err)
83116
}
84117

118+
return b.addOrUpdateConfig(path, frag)
119+
}
120+
121+
// addOrUpdateConfig adds or updates a configuration fragment to this [Builder].
122+
// Returns the [Diagnostics] produced by adding or updating this configuration.
123+
func (b *Builder) addOrUpdateConfig(path string, cfg *bindings.WAFObject) (Diagnostics, error) {
85124
var diagnosticsWafObj bindings.WAFObject
86125
defer wafLib.ObjectFree(&diagnosticsWafObj)
87126

88-
res := wafLib.BuilderAddOrUpdateConfig(b.handle, path, frag, &diagnosticsWafObj)
127+
res := wafLib.BuilderAddOrUpdateConfig(b.handle, path, cfg, &diagnosticsWafObj)
89128

90129
var diags Diagnostics
91130
if !diagnosticsWafObj.IsInvalid() {
92131
// The Diagnostics object will be invalid if the config was completely
93132
// rejected.
133+
var err error
94134
diags, err = decodeDiagnostics(&diagnosticsWafObj)
95135
if err != nil {
96136
return diags, fmt.Errorf("failed to decode WAF diagnostics: %w", err)

builder_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,58 @@ func TestBuilder(t *testing.T) {
4646
},
4747
}
4848

49+
t.Run("recommended ruleset", func(t *testing.T) {
50+
builder, err := NewBuilder("", "")
51+
require.NoError(t, err)
52+
require.NotNil(t, builder)
53+
defer builder.Close()
54+
55+
// The config is currently empty...
56+
require.Equal(t, []string{}, builder.ConfigPaths(""))
57+
handle := builder.Build()
58+
require.Nil(t, handle)
59+
60+
// We can add the default recommended ruleset alright...
61+
diag, err := builder.AddDefaultRecommendedRuleset()
62+
require.NoError(t, err)
63+
assert.NotEmpty(t, diag.Version)
64+
assert.NotEmpty(t, diag.Rules.Loaded)
65+
66+
// The default recommended ruleset is now indeed in there...
67+
require.Equal(t, []string{defaultRecommendedRulesetPath}, builder.ConfigPaths(""))
68+
69+
// Adding again is idempotent...
70+
diag, err = builder.AddDefaultRecommendedRuleset()
71+
require.NoError(t, err)
72+
assert.NotEmpty(t, diag.Version)
73+
assert.NotEmpty(t, diag.Rules.Loaded)
74+
require.Equal(t, []string{defaultRecommendedRulesetPath}, builder.ConfigPaths(""))
75+
76+
// We can actually build a handle with the default recommended ruleset...
77+
hdl := builder.Build()
78+
require.NotNil(t, hdl)
79+
hdl.Close()
80+
81+
// And we can remove it...
82+
require.True(t, builder.RemoveDefaultRecommendedRuleset())
83+
require.Equal(t, []string{}, builder.ConfigPaths(""))
84+
hdl = builder.Build()
85+
require.Nil(t, hdl)
86+
87+
// Removing it again is "idempotent" (almost, it returns false)
88+
require.False(t, builder.RemoveDefaultRecommendedRuleset())
89+
require.Equal(t, []string{}, builder.ConfigPaths(""))
90+
91+
// Finally, we can add the default recommended ruleset again after deleting it...
92+
diag, err = builder.AddDefaultRecommendedRuleset()
93+
require.NoError(t, err)
94+
assert.NotEmpty(t, diag.Version)
95+
assert.NotEmpty(t, diag.Rules.Loaded)
96+
hdl = builder.Build()
97+
require.NotNil(t, hdl)
98+
hdl.Close()
99+
})
100+
49101
t.Run("accepts a valid ruleset", func(t *testing.T) {
50102
builder, err := NewBuilder("", "")
51103
require.NoError(t, err)

decoder.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,11 @@ func decodeStringArray(obj *bindings.WAFObject) ([]string, error) {
143143
return nil, waferrors.ErrNilObjectPtr
144144
}
145145

146-
var strArr []string
146+
if obj.NbEntries == 0 {
147+
return nil, nil
148+
}
149+
150+
strArr := make([]string, 0, obj.NbEntries)
147151
for i := uint64(0); i < obj.NbEntries; i++ {
148152
objElem := unsafe.CastWithOffset[bindings.WAFObject](obj.Value, i)
149153
if objElem.Type != bindings.WAFStringType {

0 commit comments

Comments
 (0)