Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions build/config/substation.libsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,15 @@
},
inspector: {
settings: { key: null, negate: null },
condition(options=null,
settings=$.interfaces.inspector.settings): {
assert options != null : 'invalid inspector options',
assert $.helpers.inspector.validate(settings) : 'invalid inspector settings',
local s = std.mergePatch($.interfaces.inspector.settings, settings),

type: 'condition',
settings: std.mergePatch({ options: options }, s),
},
content(options=$.defaults.inspector.content.options,
settings=$.interfaces.inspector.settings): {
local opt = std.mergePatch($.defaults.inspector.content.options, options),
Expand Down
38 changes: 38 additions & 0 deletions condition/condition.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ type Inspector interface {
// NewInspector returns a configured Inspector from an Inspector configuration.
func NewInspector(cfg config.Config) (Inspector, error) {
switch cfg.Type {
case "condition":
var i inspCondition
_ = config.Decode(cfg.Settings, &i)
return i, nil
case "content":
var i inspContent
_ = config.Decode(cfg.Settings, &i)
Expand Down Expand Up @@ -244,3 +248,37 @@ type Config struct {
Operator string `json:"operator"`
Inspectors []config.Config `json:"inspectors"`
}

// condition evaluates data with a condition (operator and inspectors).
//
// This inspector supports the object handling patterns of the inspectors passed to the condition.
type inspCondition struct {
condition
Options Config `json:"options"`
}

func (c inspCondition) String() string {
return toString(c)
}

// Inspect evaluates encapsulated data with the condition inspector.
func (c inspCondition) Inspect(ctx context.Context, capsule config.Capsule) (output bool, err error) {
op, err := NewOperator(c.Options)
if err != nil {
return false, err
}

// this inspector does not directly interpret data, instead the
// capsule is passed through and each configured inspector
// applies its own data interpretation.
matched, err := op.Operate(ctx, capsule)
if err != nil {
return false, err
}

if c.Negate {
return !matched, nil
}

return matched, nil
}
203 changes: 200 additions & 3 deletions condition/condition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"github.com/brexhq/substation/config"
)

// all inspectors must return true for and to return true
var allTests = []struct {
name string
conf []config.Config
Expand Down Expand Up @@ -101,6 +100,62 @@ var allTests = []struct {
[]byte("foo"),
true,
},
// this test joins multiple ANY operators with an ALL operator, implementing the following logic:
// if ( "foo" starts with "f" OR "foo" ends with "b" ) AND ( len("foo") == 3 ) then return true
{
"condition",
[]config.Config{
{
Type: "condition",
Settings: map[string]interface{}{
"options": map[string]interface{}{
"operator": "any",
"inspectors": []config.Config{
{
Type: "strings",
Settings: map[string]interface{}{
"options": map[string]interface{}{
"expression": "f",
"type": "starts_with",
},
},
},
{
Type: "strings",
Settings: map[string]interface{}{
"options": map[string]interface{}{
"expression": "b",
"type": "ends_with",
},
},
},
},
},
},
},
{
Type: "condition",
Settings: map[string]interface{}{
"options": map[string]interface{}{
"operator": "all",
"inspectors": []config.Config{
{
Type: "length",
Settings: map[string]interface{}{
"options": map[string]interface{}{
"value": 3,
"type": "equals",
},
},
},
},
},
},
},
},
[]byte("foo"),
true,
},
}

func TestAll(t *testing.T) {
Expand Down Expand Up @@ -152,7 +207,6 @@ func BenchmarkAll(b *testing.B) {
}
}

// any inspector must return true for or to return true
var anyTests = []struct {
name string
conf []config.Config
Expand Down Expand Up @@ -243,6 +297,62 @@ var anyTests = []struct {
[]byte("foo"),
true,
},
// this test joins multiple ALL operators with an ANY operator, implementing the following logic:
// if ( len("foo") == 4 AND "foo" starts with "f" ) OR ( len("foo") == 3 ) then return true
{
"condition",
[]config.Config{
{
Type: "condition",
Settings: map[string]interface{}{
"options": map[string]interface{}{
"operator": "all",
"inspectors": []config.Config{
{
Type: "length",
Settings: map[string]interface{}{
"options": map[string]interface{}{
"value": 4,
"type": "equals",
},
},
},
{
Type: "strings",
Settings: map[string]interface{}{
"options": map[string]interface{}{
"expression": "f",
"type": "starts_with",
},
},
},
},
},
},
},
{
Type: "condition",
Settings: map[string]interface{}{
"options": map[string]interface{}{
"operator": "all",
"inspectors": []config.Config{
{
Type: "length",
Settings: map[string]interface{}{
"options": map[string]interface{}{
"value": 3,
"type": "equals",
},
},
},
},
},
},
},
},
[]byte("foo"),
true,
},
}

func TestAny(t *testing.T) {
Expand Down Expand Up @@ -294,7 +404,6 @@ func BenchmarkAny(b *testing.B) {
}
}

// any inspector must return true for none to return false
var noneTests = []struct {
name string
conf []config.Config
Expand Down Expand Up @@ -452,3 +561,91 @@ func BenchmarkNewInspector(b *testing.B) {
)
}
}

var conditionTests = []struct {
name string
inspector inspCondition
test []byte
expected bool
}{
{
"object",
inspCondition{
Options: Config{
Operator: "all",
Inspectors: []config.Config{
{
Type: "ip",
Settings: map[string]interface{}{
"key": "ip_address",
"options": map[string]interface{}{
"type": "private",
},
},
},
},
},
},
[]byte(`{"ip_address":"192.168.1.2"}`),
true,
},
{
"data",
inspCondition{
Options: Config{
Operator: "all",
Inspectors: []config.Config{
{
Type: "ip",
Settings: map[string]interface{}{
"options": map[string]interface{}{
"type": "private",
},
},
},
},
},
},
[]byte("192.168.1.2"),
true,
},
}

func TestCondition(t *testing.T) {
ctx := context.TODO()
capsule := config.NewCapsule()

for _, test := range conditionTests {
var _ Inspector = test.inspector

capsule.SetData(test.test)

check, err := test.inspector.Inspect(ctx, capsule)
if err != nil {
t.Error(err)
}

if test.expected != check {
t.Errorf("expected %v, got %v, %v", test.expected, check, string(test.test))
}
}
}

func benchmarkCondition(b *testing.B, inspector inspCondition, capsule config.Capsule) {
ctx := context.TODO()
for i := 0; i < b.N; i++ {
_, _ = inspector.Inspect(ctx, capsule)
}
}

func BenchmarkCondition(b *testing.B) {
capsule := config.NewCapsule()
for _, test := range conditionTests {
b.Run(test.name,
func(b *testing.B) {
capsule.SetData(test.test)
benchmarkCondition(b, test.inspector, capsule)
},
)
}
}