diff --git a/pkg/falco/tester_output_describe.go b/pkg/falco/tester_output_describe.go new file mode 100644 index 0000000..58c629e --- /dev/null +++ b/pkg/falco/tester_output_describe.go @@ -0,0 +1,130 @@ +/* +Copyright (C) 2023 The Falco Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +*/ + +package falco + +import ( + "encoding/json" + + "github.com/sirupsen/logrus" +) + +// RulesetDescription represent the description of the knowledge of the Falco +// engine after loading one or more rules files. +type RulesetDescription struct { + RequiredEngineVersion string `json:"required_engine_version"` + RequiredPluginVersions []PluginVersionRequirementDescription `json:"required_plugin_versions"` + Lists []ListDescription `json:"lists"` + Macros []MacroDescription `json:"macros"` + Rules []RuleDescription `json:"rules"` +} + +type PluginVersionRequirement struct { + Name string `json:"name"` + Version string `json:"version"` +} + +type PluginVersionRequirementDescription struct { + Alternatives []PluginVersionRequirement `json:"alternatives"` + PluginVersionRequirement +} + +type ListDetailsDescription struct { + ItemsCompiled []string `json:"items_compiled"` + Lists []string `json:"lists"` + Plugins []string `json:"plugins"` + Used bool `json:"used"` +} + +type ListInfoDescription struct { + Items []string `json:"items"` + Name string `json:"name"` +} + +type ListDescription struct { + Details ListDetailsDescription `json:"details"` + Info ListInfoDescription `json:"info"` +} + +type MacroDetailsDescription struct { + ConditionCompiled string `json:"condition_compiled"` + ConditionFields []string `json:"condition_fields"` + ConditionOperators []string `json:"condition_operators"` + Events []string `json:"events"` + Lists []string `json:"lists"` + Macros []string `json:"macros"` + Plugins []string `json:"plugins"` + Used bool `json:"used"` +} + +type MacroInfoDescription struct { + Condition string `json:"condition"` + Name string `json:"name"` +} + +type MacroDescription struct { + Details MacroDetailsDescription `json:"details"` + Info MacroInfoDescription `json:"info"` +} + +type RuleDetailsDescription struct { + ConditionCompiled string `json:"condition_compiled"` + ConditionFields []string `json:"condition_fields"` + ConditionOperators []string `json:"condition_operators"` + Events []string `json:"events"` + ExceptionFields []string `json:"exception_fields"` + ExceptionNames []string `json:"exception_names"` + ExceptionOperators []string `json:"exception_operators"` + Lists []string `json:"lists"` + Macros []string `json:"macros"` + OutputCompiled string `json:"output_compiled"` + OutputFields []string `json:"output_fields"` + Plugins []string `json:"plugins"` +} + +type RuleInfoDescription struct { + Condition string `json:"condition"` + Description string `json:"description"` + Enabled bool `json:"enabled"` + Name string `json:"name"` + Output string `json:"output"` + Priority string `json:"priority"` + Source string `json:"source"` + Tags []string `json:"tags"` +} + +type RuleDescription struct { + Details RuleDetailsDescription `json:"details"` + Info RuleInfoDescription `json:"info"` +} + +// RulesetDescription converts the output of the Falco run into a an struct +// describing the knowledge of the Falco engine after loading one or more rules files. +// This is achieved with the Falco `-L` option combined with the JSON output enabled. +// Returns nil if Falco wasn't run for rules descriptions. +func (t *TestOutput) RulesetDescription() *RulesetDescription { + if !t.hasOutputJSON() { + logrus.Errorf("TestOutput.RulesetDescription: must use WithOutputJSON") + } + + res := &RulesetDescription{} + if err := json.Unmarshal([]byte(t.Stdout()), res); err != nil { + logrus.WithField("stdout", t.Stdout()).Errorf("TestOutput.RulesetDescription: can't parse stdout JSON") + return nil + } + return res +} diff --git a/tests/data/rules/falco.go b/tests/data/rules/falco.go index d261136..99573b0 100644 --- a/tests/data/rules/falco.go +++ b/tests/data/rules/falco.go @@ -1393,3 +1393,18 @@ var AppendUnknownSource = run.NewStringFileAccessor( condition: or evt.type=openat `, ) + +var RulesListWithPluginJSON = run.NewStringFileAccessor( + "rule-list-with-plugin-json.yaml", + ` +- required_engine_version: 11 + +- required_plugin_versions: + - name: json + version: 0.1.0 + +- rule: open_from_cat + condition: and json.value[/test] = "test" + append: true +`, +) diff --git a/tests/falco/commands_test.go b/tests/falco/commands_test.go index a7aa063..c4dc904 100644 --- a/tests/falco/commands_test.go +++ b/tests/falco/commands_test.go @@ -28,6 +28,7 @@ import ( "github.com/falcosecurity/testing/tests/data/rules" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // todo(jasondellaluce): implement tests for the non-covered Falco cmds/args: @@ -181,7 +182,19 @@ func TestFalco_Print_Rules(t *testing.T) { t.Parallel() checkDefaultConfig(t) runner := tests.NewFalcoExecutableRunner(t) - t.Run("valid-rules", func(t *testing.T) { + + t.Run("invalid-rules", func(t *testing.T) { + t.Parallel() + res := falco.Test( + runner, + falco.WithArgs("-L"), + falco.WithRules(rules.InvalidRuleOutput), + ) + assert.Error(t, res.Err(), "%s", res.Stderr()) + assert.Equal(t, res.ExitCode(), 1) + }) + + t.Run("text-valid-rules", func(t *testing.T) { t.Parallel() res := falco.Test( runner, @@ -195,15 +208,99 @@ func TestFalco_Print_Rules(t *testing.T) { assert.NoError(t, res.Err(), "%s", res.Stderr()) assert.Equal(t, res.ExitCode(), 0) }) - t.Run("invalid-rules", func(t *testing.T) { + + t.Run("json-valid-rules", func(t *testing.T) { t.Parallel() res := falco.Test( runner, falco.WithArgs("-L"), - falco.WithRules(rules.InvalidRuleOutput), + falco.WithOutputJSON(), + falco.WithArgs("-o", "load_plugins[0]=json"), + falco.WithRules(rules.RulesDir000SingleRule, rules.RulesListWithPluginJSON), ) - assert.Error(t, res.Err(), "%s", res.Stderr()) - assert.Equal(t, res.ExitCode(), 1) + + infos := res.RulesetDescription() + assert.NotNil(t, infos) + + // check required engine version + assert.Equal(t, "0.11.0", infos.RequiredEngineVersion) + + // check required plugin versions + require.Len(t, infos.RequiredPluginVersions, 1) + assert.Equal(t, "json", infos.RequiredPluginVersions[0].Name) + assert.Equal(t, "0.1.0", infos.RequiredPluginVersions[0].Version) + + // check list elements + require.Len(t, infos.Lists, 2) + + assert.Equal(t, "cat_binaries", infos.Lists[0].Info.Name) + require.Len(t, infos.Lists[0].Info.Items, 1) + assert.Equal(t, "cat", infos.Lists[0].Info.Items[0]) + assert.True(t, infos.Lists[0].Details.Used) + assert.Len(t, infos.Lists[0].Details.Lists, 0) + assert.Len(t, infos.Lists[0].Details.Plugins, 0) + assert.Len(t, infos.Lists[0].Details.ItemsCompiled, 1) + assert.Equal(t, "cat", infos.Lists[0].Info.Items[0]) + + assert.Equal(t, "cat_capable_binaries", infos.Lists[1].Info.Name) + assert.Len(t, infos.Lists[1].Info.Items, 0) + assert.True(t, infos.Lists[1].Details.Used) + require.Len(t, infos.Lists[1].Details.Lists, 1) + assert.Equal(t, "cat_binaries", infos.Lists[1].Details.Lists[0]) + assert.Len(t, infos.Lists[1].Details.Plugins, 0) + require.Len(t, infos.Lists[1].Details.ItemsCompiled, 1) + assert.Equal(t, "cat", infos.Lists[1].Details.ItemsCompiled[0]) + + // check macro elements + require.Len(t, infos.Macros, 1) + + assert.Equal(t, "is_cat", infos.Macros[0].Info.Name) + assert.Equal(t, "proc.name in (cat_capable_binaries)", infos.Macros[0].Info.Condition) + assert.True(t, infos.Macros[0].Details.Used) + assert.Len(t, infos.Macros[0].Details.Macros, 0) + assert.Len(t, infos.Macros[0].Details.Lists, 1) + assert.Equal(t, "cat_capable_binaries", infos.Macros[0].Details.Lists[0]) + assert.Len(t, infos.Macros[0].Details.Plugins, 0) + assert.NotEmpty(t, infos.Macros[0].Details.Events) + assert.Len(t, infos.Macros[0].Details.ConditionOperators, 1) + assert.Equal(t, "in", infos.Macros[0].Details.ConditionOperators[0]) + require.Len(t, infos.Macros[0].Details.ConditionFields, 1) + assert.Equal(t, "proc.name", infos.Macros[0].Details.ConditionFields[0]) + assert.Equal(t, "proc.name in (cat)", infos.Macros[0].Details.ConditionCompiled) + + // check rule elements + require.Len(t, infos.Rules, 1) + + assert.Equal(t, "open_from_cat", infos.Rules[0].Info.Name) + assert.Equal(t, `evt.type=open and is_cat and json.value[/test] = "test"`, infos.Rules[0].Info.Condition) + assert.Equal(t, "A process named cat does an open", infos.Rules[0].Info.Description) + assert.Equal(t, "An open was seen (command=%proc.cmdline)", infos.Rules[0].Info.Output) + assert.Equal(t, true, infos.Rules[0].Info.Enabled) + assert.Equal(t, "Warning", infos.Rules[0].Info.Priority) + assert.Equal(t, "syscall", infos.Rules[0].Info.Source) + assert.Empty(t, infos.Rules[0].Info.Tags) + require.Len(t, infos.Rules[0].Details.Plugins, 1) + assert.Equal(t, "json", infos.Rules[0].Details.Plugins[0]) + require.Len(t, infos.Rules[0].Details.OutputFields, 1) + assert.Equal(t, "proc.cmdline", infos.Rules[0].Details.OutputFields[0]) + assert.Equal(t, infos.Rules[0].Info.Output, infos.Rules[0].Details.OutputCompiled) + assert.Len(t, infos.Rules[0].Details.Macros, 1) + require.Equal(t, "is_cat", infos.Rules[0].Details.Macros[0]) + assert.Len(t, infos.Rules[0].Details.Lists, 0) + assert.Len(t, infos.Rules[0].Details.ExceptionFields, 0) + assert.Len(t, infos.Rules[0].Details.ExceptionOperators, 0) + assert.Len(t, infos.Rules[0].Details.ExceptionNames, 0) + assert.Len(t, infos.Rules[0].Details.Events, 2) + assert.Contains(t, infos.Rules[0].Details.Events, "open") + assert.Contains(t, infos.Rules[0].Details.Events, "asyncevent") + assert.Len(t, infos.Rules[0].Details.ConditionOperators, 2) + assert.Contains(t, infos.Rules[0].Details.ConditionOperators, "=") + assert.Contains(t, infos.Rules[0].Details.ConditionOperators, "in") + assert.Len(t, infos.Rules[0].Details.ConditionFields, 3) + assert.Contains(t, infos.Rules[0].Details.ConditionFields, "evt.type") + assert.Contains(t, infos.Rules[0].Details.ConditionFields, "proc.name") + assert.Contains(t, infos.Rules[0].Details.ConditionFields, "json.value[/test]") + assert.Equal(t, `(evt.type = open and proc.name in (cat) and json.value[/test] = test)`, infos.Rules[0].Details.ConditionCompiled) }) }