Skip to content

Commit b967515

Browse files
authored
Merge pull request #274 from datum-cloud/fix/273-neutralize-inverted-edge
fix: neutralize persisted inverted TPP on the coraza edge path (#252 follow-up)
2 parents 89f35c1 + 531e6c4 commit b967515

6 files changed

Lines changed: 396 additions & 14 deletions

File tree

api/v1alpha/trafficprotectionpolicy_types.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,19 @@ type ParanoiaLevels struct {
129129
Detection int `json:"detection,omitempty"`
130130
}
131131

132+
func (s *TrafficProtectionPolicySpec) InvertedParanoiaLevels() *ParanoiaLevels {
133+
for i := range s.RuleSets {
134+
if s.RuleSets[i].Type != TrafficProtectionPolicyOWASPCoreRuleSet {
135+
continue
136+
}
137+
levels := &s.RuleSets[i].OWASPCoreRuleSet.ParanoiaLevels
138+
if levels.Detection < levels.Blocking {
139+
return levels
140+
}
141+
}
142+
return nil
143+
}
144+
132145
type OWASPRuleExclusions struct {
133146
// Tags is a list of rule tags to disable.
134147
//
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
3+
package v1alpha_test
4+
5+
import (
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
10+
networkingv1alpha "go.datum.net/network-services-operator/api/v1alpha"
11+
)
12+
13+
func TestTrafficProtectionPolicySpec_InvertedParanoiaLevels(t *testing.T) {
14+
t.Parallel()
15+
16+
owaspRuleSet := func(blocking, detection int) networkingv1alpha.TrafficProtectionPolicyRuleSet {
17+
return networkingv1alpha.TrafficProtectionPolicyRuleSet{
18+
Type: networkingv1alpha.TrafficProtectionPolicyOWASPCoreRuleSet,
19+
OWASPCoreRuleSet: networkingv1alpha.OWASPCRS{
20+
ParanoiaLevels: networkingv1alpha.ParanoiaLevels{
21+
Blocking: blocking,
22+
Detection: detection,
23+
},
24+
},
25+
}
26+
}
27+
28+
tests := []struct {
29+
name string
30+
ruleSets []networkingv1alpha.TrafficProtectionPolicyRuleSet
31+
wantInverted bool
32+
}{
33+
{
34+
name: "no rulesets",
35+
ruleSets: nil,
36+
wantInverted: false,
37+
},
38+
{
39+
name: "equal levels",
40+
ruleSets: []networkingv1alpha.TrafficProtectionPolicyRuleSet{owaspRuleSet(2, 2)},
41+
wantInverted: false,
42+
},
43+
{
44+
name: "higher detection",
45+
ruleSets: []networkingv1alpha.TrafficProtectionPolicyRuleSet{owaspRuleSet(1, 3)},
46+
wantInverted: false,
47+
},
48+
{
49+
name: "detection below blocking",
50+
ruleSets: []networkingv1alpha.TrafficProtectionPolicyRuleSet{owaspRuleSet(2, 1)},
51+
wantInverted: true,
52+
},
53+
{
54+
name: "detection far below blocking",
55+
ruleSets: []networkingv1alpha.TrafficProtectionPolicyRuleSet{owaspRuleSet(4, 1)},
56+
wantInverted: true,
57+
},
58+
}
59+
60+
for _, tt := range tests {
61+
t.Run(tt.name, func(t *testing.T) {
62+
t.Parallel()
63+
64+
spec := networkingv1alpha.TrafficProtectionPolicySpec{RuleSets: tt.ruleSets}
65+
levels := spec.InvertedParanoiaLevels()
66+
67+
if tt.wantInverted {
68+
if assert.NotNil(t, levels, "inverted ruleset must be reported") {
69+
assert.Less(t, levels.Detection, levels.Blocking,
70+
"reported levels must be inverted")
71+
}
72+
return
73+
}
74+
assert.Nil(t, levels, "no ruleset is inverted")
75+
})
76+
}
77+
}

internal/controller/trafficprotectionpolicy_controller.go

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1235,21 +1235,16 @@ func getVHostConstraintForGateway(namespace string, gateway *gatewayv1.Gateway)
12351235
// closed on this configuration and denies every request with HTTP 500 before any
12361236
// attack evaluation, so the policy must not be attached.
12371237
func paranoiaLevelsResolveError(policy *policyContext) *gatewaystatus.PolicyResolveError {
1238-
for _, ruleSet := range policy.Spec.RuleSets {
1239-
if ruleSet.Type != networkingv1alpha.TrafficProtectionPolicyOWASPCoreRuleSet {
1240-
continue
1241-
}
1242-
levels := ruleSet.OWASPCoreRuleSet.ParanoiaLevels
1243-
if levels.Detection < levels.Blocking {
1244-
return &gatewaystatus.PolicyResolveError{
1245-
Reason: gatewayv1.PolicyReasonInvalid,
1246-
Message: fmt.Sprintf(
1247-
"OWASPCoreRuleSet detection paranoia level (%d) must be greater than or equal to blocking paranoia level (%d); this is an illegal CRS configuration that would deny all traffic with HTTP 500",
1248-
levels.Detection, levels.Blocking),
1249-
}
1250-
}
1238+
levels := policy.Spec.InvertedParanoiaLevels()
1239+
if levels == nil {
1240+
return nil
1241+
}
1242+
return &gatewaystatus.PolicyResolveError{
1243+
Reason: gatewayv1.PolicyReasonInvalid,
1244+
Message: fmt.Sprintf(
1245+
"OWASPCoreRuleSet detection paranoia level (%d) must be greater than or equal to blocking paranoia level (%d); this is an illegal CRS configuration that would deny all traffic with HTTP 500",
1246+
levels.Detection, levels.Blocking),
12511247
}
1252-
return nil
12531248
}
12541249

12551250
func (r *TrafficProtectionPolicyReconciler) getCorazaDirectivesForTrafficProtectionPolicy(

internal/extensionserver/cache/index.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,10 @@ func computeCorazaDirectives(
225225
tpp *networkingv1alpha.TrafficProtectionPolicy,
226226
baseDirectives []string,
227227
) []string {
228+
if tpp.Spec.InvertedParanoiaLevels() != nil {
229+
return nil
230+
}
231+
228232
var owaspCRS *networkingv1alpha.OWASPCRS
229233
for _, ruleSet := range tpp.Spec.RuleSets {
230234
if ruleSet.Type == networkingv1alpha.TrafficProtectionPolicyOWASPCoreRuleSet {

internal/extensionserver/cache/index_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,33 @@ func TestBuildPolicyIndexFromClient_TPPFieldsPreserved(t *testing.T) {
485485
assert.NotEmpty(t, info.Directives, "OWASP CRS rules must generate non-empty directives")
486486
}
487487

488+
func TestBuildPolicyIndexFromClient_InvertedParanoia_NoDirectivesEmitted(t *testing.T) {
489+
scheme := indexTestScheme(t)
490+
491+
inverted := newTPP("test-ns", "inverted-tpp", withOWASPCRS(5, 4, 2, 1))
492+
inverted.Spec.Mode = networkingv1alpha.TrafficProtectionPolicyEnforce
493+
494+
valid := newTPP("test-ns", "valid-tpp", withOWASPCRS(5, 4, 1, 1))
495+
valid.Spec.Mode = networkingv1alpha.TrafficProtectionPolicyEnforce
496+
497+
cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(inverted, valid).Build()
498+
499+
idx, err := BuildPolicyIndexFromClient(context.Background(), cl, nil)
500+
require.NoError(t, err)
501+
502+
byName := map[string]TPPInfo{}
503+
for _, info := range idx.TPPs["test-ns"] {
504+
byName[info.Name] = info
505+
}
506+
require.Contains(t, byName, "inverted-tpp")
507+
require.Contains(t, byName, "valid-tpp")
508+
509+
assert.Empty(t, byName["inverted-tpp"].Directives,
510+
"inverted policy must yield no directives so the mutation layer withholds it from the data plane")
511+
assert.NotEmpty(t, byName["valid-tpp"].Directives,
512+
"valid policy must still emit directives")
513+
}
514+
488515
// =============================================================================
489516
// computeCorazaDirectives unit tests
490517
//
@@ -594,6 +621,36 @@ func TestComputeCorazaDirectives_ParanoiaLevels(t *testing.T) {
594621
assert.True(t, foundDetection, "detection paranoia level 4 must appear in directives")
595622
}
596623

624+
func TestComputeCorazaDirectives_InvertedParanoiaLevels(t *testing.T) {
625+
tests := []struct {
626+
name string
627+
blocking int
628+
detection int
629+
wantEmitted bool
630+
}{
631+
{name: "equal levels emitted", blocking: 2, detection: 2, wantEmitted: true},
632+
{name: "higher detection emitted", blocking: 1, detection: 3, wantEmitted: true},
633+
{name: "inverted not emitted", blocking: 2, detection: 1, wantEmitted: false},
634+
{name: "inverted far not emitted", blocking: 4, detection: 1, wantEmitted: false},
635+
}
636+
637+
for _, tt := range tests {
638+
t.Run(tt.name, func(t *testing.T) {
639+
tpp := newTPP("ns", "tpp", withOWASPCRS(5, 4, tt.blocking, tt.detection))
640+
tpp.Spec.Mode = networkingv1alpha.TrafficProtectionPolicyEnforce
641+
642+
result := computeCorazaDirectives(tpp, nil)
643+
644+
if tt.wantEmitted {
645+
assert.NotEmpty(t, result, "valid paranoia levels must emit directives")
646+
return
647+
}
648+
assert.Nil(t, result,
649+
"inverted paranoia levels must emit no directives (withheld from the data plane)")
650+
})
651+
}
652+
}
653+
597654
func TestComputeCorazaDirectives_IncludeOWASPCRSAppendedAfterActions(t *testing.T) {
598655
tpp := newTPP("ns", "tpp", withOWASPCRS(5, 4, 1, 1))
599656

0 commit comments

Comments
 (0)