Application Security Engine Support (#2273)
Add a new datasource that: - Receives HTTP requests from remediation components - Apply rules on them to determine whether they are malicious or not - Rules can be evaluated in-band (the remediation component will block the request directly) or out-band (the RC will let the request through, but crowdsec can still process the rule matches with scenarios) The PR also adds support for 2 new hub items: - appsec-configs: Configure the Application Security Engine (which rules to load, in which phase) - appsec-rules: a rule that is added in the Application Security Engine (can use either our own format, or seclang) --------- Co-authored-by: alteredCoder <kevin@crowdsec.net> Co-authored-by: Sebastien Blot <sebastien@crowdsec.net> Co-authored-by: mmetc <92726601+mmetc@users.noreply.github.com> Co-authored-by: Marco Mariani <marco@crowdsec.net>
This commit is contained in:
parent
90d3a21853
commit
8cca4346a5
52 changed files with 5074 additions and 787 deletions
105
cmd/crowdsec-cli/hubappsec.go
Normal file
105
cmd/crowdsec-cli/hubappsec.go
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/text/cases"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/appsec"
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/appsec/appsec_rule"
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewAppsecConfigCLI() *itemCLI {
|
||||||
|
return &itemCLI{
|
||||||
|
name: cwhub.APPSEC_CONFIGS,
|
||||||
|
singular: "appsec-config",
|
||||||
|
oneOrMore: "appsec-config(s)",
|
||||||
|
help: cliHelp{
|
||||||
|
example: `cscli appsec-configs list -a
|
||||||
|
cscli appsec-configs install crowdsecurity/vpatch
|
||||||
|
cscli appsec-configs inspect crowdsecurity/vpatch
|
||||||
|
cscli appsec-configs upgrade crowdsecurity/vpatch
|
||||||
|
cscli appsec-configs remove crowdsecurity/vpatch
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
installHelp: cliHelp{
|
||||||
|
example: `cscli appsec-configs install crowdsecurity/vpatch`,
|
||||||
|
},
|
||||||
|
removeHelp: cliHelp{
|
||||||
|
example: `cscli appsec-configs remove crowdsecurity/vpatch`,
|
||||||
|
},
|
||||||
|
upgradeHelp: cliHelp{
|
||||||
|
example: `cscli appsec-configs upgrade crowdsecurity/vpatch`,
|
||||||
|
},
|
||||||
|
inspectHelp: cliHelp{
|
||||||
|
example: `cscli appsec-configs inspect crowdsecurity/vpatch`,
|
||||||
|
},
|
||||||
|
listHelp: cliHelp{
|
||||||
|
example: `cscli appsec-configs list
|
||||||
|
cscli appsec-configs list -a
|
||||||
|
cscli appsec-configs list crowdsecurity/vpatch`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAppsecRuleCLI() *itemCLI {
|
||||||
|
inspectDetail := func(item *cwhub.Item) error {
|
||||||
|
appsecRule := appsec.AppsecCollectionConfig{}
|
||||||
|
yamlContent, err := os.ReadFile(item.State.LocalPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to read file %s : %s", item.State.LocalPath, err)
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(yamlContent, &appsecRule); err != nil {
|
||||||
|
return fmt.Errorf("unable to unmarshal yaml file %s : %s", item.State.LocalPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ruleType := range appsec_rule.SupportedTypes() {
|
||||||
|
fmt.Printf("\n%s format:\n", cases.Title(language.Und, cases.NoLower).String(ruleType))
|
||||||
|
for _, rule := range appsecRule.Rules {
|
||||||
|
convertedRule, _, err := rule.Convert(ruleType, appsecRule.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to convert rule %s : %s", rule.Name, err)
|
||||||
|
}
|
||||||
|
fmt.Println(convertedRule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &itemCLI{
|
||||||
|
name: "appsec-rules",
|
||||||
|
singular: "appsec-rule",
|
||||||
|
oneOrMore: "appsec-rule(s)",
|
||||||
|
help: cliHelp{
|
||||||
|
example: `cscli appsec-rules list -a
|
||||||
|
cscli appsec-rules install crowdsecurity/crs
|
||||||
|
cscli appsec-rules inspect crowdsecurity/crs
|
||||||
|
cscli appsec-rules upgrade crowdsecurity/crs
|
||||||
|
cscli appsec-rules remove crowdsecurity/crs
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
installHelp: cliHelp{
|
||||||
|
example: `cscli appsec-rules install crowdsecurity/crs`,
|
||||||
|
},
|
||||||
|
removeHelp: cliHelp{
|
||||||
|
example: `cscli appsec-rules remove crowdsecurity/crs`,
|
||||||
|
},
|
||||||
|
upgradeHelp: cliHelp{
|
||||||
|
example: `cscli appsec-rules upgrade crowdsecurity/crs`,
|
||||||
|
},
|
||||||
|
inspectHelp: cliHelp{
|
||||||
|
example: `cscli appsec-rules inspect crowdsecurity/crs`,
|
||||||
|
},
|
||||||
|
inspectDetail: inspectDetail,
|
||||||
|
listHelp: cliHelp{
|
||||||
|
example: `cscli appsec-rules list
|
||||||
|
cscli appsec-rules list -a
|
||||||
|
cscli appsec-rules list crowdsecurity/crs`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
40
cmd/crowdsec-cli/hubcollection.go
Normal file
40
cmd/crowdsec-cli/hubcollection.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewCollectionCLI() *itemCLI {
|
||||||
|
return &itemCLI{
|
||||||
|
name: cwhub.COLLECTIONS,
|
||||||
|
singular: "collection",
|
||||||
|
oneOrMore: "collection(s)",
|
||||||
|
help: cliHelp{
|
||||||
|
example: `cscli collections list -a
|
||||||
|
cscli collections install crowdsecurity/http-cve crowdsecurity/iptables
|
||||||
|
cscli collections inspect crowdsecurity/http-cve crowdsecurity/iptables
|
||||||
|
cscli collections upgrade crowdsecurity/http-cve crowdsecurity/iptables
|
||||||
|
cscli collections remove crowdsecurity/http-cve crowdsecurity/iptables
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
installHelp: cliHelp{
|
||||||
|
example: `cscli collections install crowdsecurity/http-cve crowdsecurity/iptables`,
|
||||||
|
},
|
||||||
|
removeHelp: cliHelp{
|
||||||
|
example: `cscli collections remove crowdsecurity/http-cve crowdsecurity/iptables`,
|
||||||
|
},
|
||||||
|
upgradeHelp: cliHelp{
|
||||||
|
example: `cscli collections upgrade crowdsecurity/http-cve crowdsecurity/iptables`,
|
||||||
|
},
|
||||||
|
inspectHelp: cliHelp{
|
||||||
|
example: `cscli collections inspect crowdsecurity/http-cve crowdsecurity/iptables`,
|
||||||
|
},
|
||||||
|
listHelp: cliHelp{
|
||||||
|
example: `cscli collections list
|
||||||
|
cscli collections list -a
|
||||||
|
cscli collections list crowdsecurity/http-cve crowdsecurity/iptables
|
||||||
|
|
||||||
|
List only enabled collections unless "-a" or names are specified.`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
40
cmd/crowdsec-cli/hubparser.go
Normal file
40
cmd/crowdsec-cli/hubparser.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewParserCLI() *itemCLI {
|
||||||
|
return &itemCLI{
|
||||||
|
name: cwhub.PARSERS,
|
||||||
|
singular: "parser",
|
||||||
|
oneOrMore: "parser(s)",
|
||||||
|
help: cliHelp{
|
||||||
|
example: `cscli parsers list -a
|
||||||
|
cscli parsers install crowdsecurity/caddy-logs crowdsecurity/sshd-logs
|
||||||
|
cscli parsers inspect crowdsecurity/caddy-logs crowdsecurity/sshd-logs
|
||||||
|
cscli parsers upgrade crowdsecurity/caddy-logs crowdsecurity/sshd-logs
|
||||||
|
cscli parsers remove crowdsecurity/caddy-logs crowdsecurity/sshd-logs
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
installHelp: cliHelp{
|
||||||
|
example: `cscli parsers install crowdsecurity/caddy-logs crowdsecurity/sshd-logs`,
|
||||||
|
},
|
||||||
|
removeHelp: cliHelp{
|
||||||
|
example: `cscli parsers remove crowdsecurity/caddy-logs crowdsecurity/sshd-logs`,
|
||||||
|
},
|
||||||
|
upgradeHelp: cliHelp{
|
||||||
|
example: `cscli parsers upgrade crowdsecurity/caddy-logs crowdsecurity/sshd-logs`,
|
||||||
|
},
|
||||||
|
inspectHelp: cliHelp{
|
||||||
|
example: `cscli parsers inspect crowdsecurity/httpd-logs crowdsecurity/sshd-logs`,
|
||||||
|
},
|
||||||
|
listHelp: cliHelp{
|
||||||
|
example: `cscli parsers list
|
||||||
|
cscli parsers list -a
|
||||||
|
cscli parsers list crowdsecurity/caddy-logs crowdsecurity/sshd-logs
|
||||||
|
|
||||||
|
List only enabled parsers unless "-a" or names are specified.`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
40
cmd/crowdsec-cli/hubpostoverflow.go
Normal file
40
cmd/crowdsec-cli/hubpostoverflow.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewPostOverflowCLI() *itemCLI {
|
||||||
|
return &itemCLI{
|
||||||
|
name: cwhub.POSTOVERFLOWS,
|
||||||
|
singular: "postoverflow",
|
||||||
|
oneOrMore: "postoverflow(s)",
|
||||||
|
help: cliHelp{
|
||||||
|
example: `cscli postoverflows list -a
|
||||||
|
cscli postoverflows install crowdsecurity/cdn-whitelist crowdsecurity/rdns
|
||||||
|
cscli postoverflows inspect crowdsecurity/cdn-whitelist crowdsecurity/rdns
|
||||||
|
cscli postoverflows upgrade crowdsecurity/cdn-whitelist crowdsecurity/rdns
|
||||||
|
cscli postoverflows remove crowdsecurity/cdn-whitelist crowdsecurity/rdns
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
installHelp: cliHelp{
|
||||||
|
example: `cscli postoverflows install crowdsecurity/cdn-whitelist crowdsecurity/rdns`,
|
||||||
|
},
|
||||||
|
removeHelp: cliHelp{
|
||||||
|
example: `cscli postoverflows remove crowdsecurity/cdn-whitelist crowdsecurity/rdns`,
|
||||||
|
},
|
||||||
|
upgradeHelp: cliHelp{
|
||||||
|
example: `cscli postoverflows upgrade crowdsecurity/cdn-whitelist crowdsecurity/rdns`,
|
||||||
|
},
|
||||||
|
inspectHelp: cliHelp{
|
||||||
|
example: `cscli postoverflows inspect crowdsecurity/cdn-whitelist crowdsecurity/rdns`,
|
||||||
|
},
|
||||||
|
listHelp: cliHelp{
|
||||||
|
example: `cscli postoverflows list
|
||||||
|
cscli postoverflows list -a
|
||||||
|
cscli postoverflows list crowdsecurity/cdn-whitelist crowdsecurity/rdns
|
||||||
|
|
||||||
|
List only enabled postoverflows unless "-a" or names are specified.`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
40
cmd/crowdsec-cli/hubscenario.go
Normal file
40
cmd/crowdsec-cli/hubscenario.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewScenarioCLI() *itemCLI {
|
||||||
|
return &itemCLI{
|
||||||
|
name: cwhub.SCENARIOS,
|
||||||
|
singular: "scenario",
|
||||||
|
oneOrMore: "scenario(s)",
|
||||||
|
help: cliHelp{
|
||||||
|
example: `cscli scenarios list -a
|
||||||
|
cscli scenarios install crowdsecurity/ssh-bf crowdsecurity/http-probing
|
||||||
|
cscli scenarios inspect crowdsecurity/ssh-bf crowdsecurity/http-probing
|
||||||
|
cscli scenarios upgrade crowdsecurity/ssh-bf crowdsecurity/http-probing
|
||||||
|
cscli scenarios remove crowdsecurity/ssh-bf crowdsecurity/http-probing
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
installHelp: cliHelp{
|
||||||
|
example: `cscli scenarios install crowdsecurity/ssh-bf crowdsecurity/http-probing`,
|
||||||
|
},
|
||||||
|
removeHelp: cliHelp{
|
||||||
|
example: `cscli scenarios remove crowdsecurity/ssh-bf crowdsecurity/http-probing`,
|
||||||
|
},
|
||||||
|
upgradeHelp: cliHelp{
|
||||||
|
example: `cscli scenarios upgrade crowdsecurity/ssh-bf crowdsecurity/http-probing`,
|
||||||
|
},
|
||||||
|
inspectHelp: cliHelp{
|
||||||
|
example: `cscli scenarios inspect crowdsecurity/ssh-bf crowdsecurity/http-probing`,
|
||||||
|
},
|
||||||
|
listHelp: cliHelp{
|
||||||
|
example: `cscli scenarios list
|
||||||
|
cscli scenarios list -a
|
||||||
|
cscli scenarios list crowdsecurity/ssh-bf crowdsecurity/http-probing
|
||||||
|
|
||||||
|
List only enabled scenarios unless "-a" or names are specified.`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var HubTest hubtest.HubTest
|
var HubTest hubtest.HubTest
|
||||||
|
var HubAppsecTests hubtest.HubTest
|
||||||
|
var hubPtr *hubtest.HubTest
|
||||||
|
var isAppsecTest bool
|
||||||
|
|
||||||
func NewHubTestCmd() *cobra.Command {
|
func NewHubTestCmd() *cobra.Command {
|
||||||
var hubPath string
|
var hubPath string
|
||||||
|
@ -33,11 +36,20 @@ func NewHubTestCmd() *cobra.Command {
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
var err error
|
var err error
|
||||||
HubTest, err = hubtest.NewHubTest(hubPath, crowdsecPath, cscliPath)
|
HubTest, err = hubtest.NewHubTest(hubPath, crowdsecPath, cscliPath, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to load hubtest: %+v", err)
|
return fmt.Errorf("unable to load hubtest: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HubAppsecTests, err = hubtest.NewHubTest(hubPath, crowdsecPath, cscliPath, true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to load appsec specific hubtest: %+v", err)
|
||||||
|
}
|
||||||
|
/*commands will use the hubPtr, will point to the default hubTest object, or the one dedicated to appsec tests*/
|
||||||
|
hubPtr = &HubTest
|
||||||
|
if isAppsecTest {
|
||||||
|
hubPtr = &HubAppsecTests
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -45,6 +57,7 @@ func NewHubTestCmd() *cobra.Command {
|
||||||
cmdHubTest.PersistentFlags().StringVar(&hubPath, "hub", ".", "Path to hub folder")
|
cmdHubTest.PersistentFlags().StringVar(&hubPath, "hub", ".", "Path to hub folder")
|
||||||
cmdHubTest.PersistentFlags().StringVar(&crowdsecPath, "crowdsec", "crowdsec", "Path to crowdsec")
|
cmdHubTest.PersistentFlags().StringVar(&crowdsecPath, "crowdsec", "crowdsec", "Path to crowdsec")
|
||||||
cmdHubTest.PersistentFlags().StringVar(&cscliPath, "cscli", "cscli", "Path to cscli")
|
cmdHubTest.PersistentFlags().StringVar(&cscliPath, "cscli", "cscli", "Path to cscli")
|
||||||
|
cmdHubTest.PersistentFlags().BoolVar(&isAppsecTest, "appsec", false, "Command relates to appsec tests")
|
||||||
|
|
||||||
cmdHubTest.AddCommand(NewHubTestCreateCmd())
|
cmdHubTest.AddCommand(NewHubTestCreateCmd())
|
||||||
cmdHubTest.AddCommand(NewHubTestRunCmd())
|
cmdHubTest.AddCommand(NewHubTestRunCmd())
|
||||||
|
@ -76,7 +89,7 @@ cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
testName := args[0]
|
testName := args[0]
|
||||||
testPath := filepath.Join(HubTest.HubTestPath, testName)
|
testPath := filepath.Join(hubPtr.HubTestPath, testName)
|
||||||
if _, err := os.Stat(testPath); os.IsExist(err) {
|
if _, err := os.Stat(testPath); os.IsExist(err) {
|
||||||
return fmt.Errorf("test '%s' already exists in '%s', exiting", testName, testPath)
|
return fmt.Errorf("test '%s' already exists in '%s', exiting", testName, testPath)
|
||||||
}
|
}
|
||||||
|
@ -89,6 +102,25 @@ cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios
|
||||||
return fmt.Errorf("unable to create folder '%s': %+v", testPath, err)
|
return fmt.Errorf("unable to create folder '%s': %+v", testPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configFilePath := filepath.Join(testPath, "config.yaml")
|
||||||
|
|
||||||
|
configFileData := &hubtest.HubTestItemConfig{}
|
||||||
|
if logType == "appsec" {
|
||||||
|
//create empty nuclei template file
|
||||||
|
nucleiFileName := fmt.Sprintf("%s.yaml", testName)
|
||||||
|
nucleiFilePath := filepath.Join(testPath, nucleiFileName)
|
||||||
|
nucleiFile, err := os.Create(nucleiFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
nucleiFile.Close()
|
||||||
|
configFileData.AppsecRules = []string{"your_rule_here.yaml"}
|
||||||
|
configFileData.NucleiTemplate = nucleiFileName
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf(" Test name : %s\n", testName)
|
||||||
|
fmt.Printf(" Test path : %s\n", testPath)
|
||||||
|
fmt.Printf(" Nuclei Template : %s\n", nucleiFileName)
|
||||||
|
} else {
|
||||||
// create empty log file
|
// create empty log file
|
||||||
logFileName := fmt.Sprintf("%s.log", testName)
|
logFileName := fmt.Sprintf("%s.log", testName)
|
||||||
logFilePath := filepath.Join(testPath, logFileName)
|
logFilePath := filepath.Join(testPath, logFileName)
|
||||||
|
@ -105,7 +137,6 @@ cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
parserAssertFile.Close()
|
parserAssertFile.Close()
|
||||||
|
|
||||||
// create empty scenario assertion file
|
// create empty scenario assertion file
|
||||||
scenarioAssertFilePath := filepath.Join(testPath, hubtest.ScenarioAssertFileName)
|
scenarioAssertFilePath := filepath.Join(testPath, hubtest.ScenarioAssertFileName)
|
||||||
scenarioAssertFile, err := os.Create(scenarioAssertFilePath)
|
scenarioAssertFile, err := os.Create(scenarioAssertFilePath)
|
||||||
|
@ -124,18 +155,23 @@ cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios
|
||||||
if len(postoverflows) == 0 {
|
if len(postoverflows) == 0 {
|
||||||
postoverflows = append(postoverflows, "")
|
postoverflows = append(postoverflows, "")
|
||||||
}
|
}
|
||||||
|
configFileData.Parsers = parsers
|
||||||
|
configFileData.Scenarios = scenarios
|
||||||
|
configFileData.PostOverflows = postoverflows
|
||||||
|
configFileData.LogFile = logFileName
|
||||||
|
configFileData.LogType = logType
|
||||||
|
configFileData.IgnoreParsers = ignoreParsers
|
||||||
|
configFileData.Labels = labels
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf(" Test name : %s\n", testName)
|
||||||
|
fmt.Printf(" Test path : %s\n", testPath)
|
||||||
|
fmt.Printf(" Log file : %s (please fill it with logs)\n", logFilePath)
|
||||||
|
fmt.Printf(" Parser assertion file : %s (please fill it with assertion)\n", parserAssertFilePath)
|
||||||
|
fmt.Printf(" Scenario assertion file : %s (please fill it with assertion)\n", scenarioAssertFilePath)
|
||||||
|
fmt.Printf(" Configuration File : %s (please fill it with parsers, scenarios...)\n", configFilePath)
|
||||||
|
|
||||||
configFileData := &hubtest.HubTestItemConfig{
|
|
||||||
Parsers: parsers,
|
|
||||||
Scenarios: scenarios,
|
|
||||||
PostOVerflows: postoverflows,
|
|
||||||
LogFile: logFileName,
|
|
||||||
LogType: logType,
|
|
||||||
IgnoreParsers: ignoreParsers,
|
|
||||||
Labels: labels,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
configFilePath := filepath.Join(testPath, "config.yaml")
|
|
||||||
fd, err := os.Create(configFilePath)
|
fd, err := os.Create(configFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("open: %s", err)
|
return fmt.Errorf("open: %s", err)
|
||||||
|
@ -151,14 +187,6 @@ cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios
|
||||||
if err := fd.Close(); err != nil {
|
if err := fd.Close(); err != nil {
|
||||||
return fmt.Errorf("close: %s", err)
|
return fmt.Errorf("close: %s", err)
|
||||||
}
|
}
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf(" Test name : %s\n", testName)
|
|
||||||
fmt.Printf(" Test path : %s\n", testPath)
|
|
||||||
fmt.Printf(" Log file : %s (please fill it with logs)\n", logFilePath)
|
|
||||||
fmt.Printf(" Parser assertion file : %s (please fill it with assertion)\n", parserAssertFilePath)
|
|
||||||
fmt.Printf(" Scenario assertion file : %s (please fill it with assertion)\n", scenarioAssertFilePath)
|
|
||||||
fmt.Printf(" Configuration File : %s (please fill it with parsers, scenarios...)\n", configFilePath)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -188,12 +216,12 @@ func NewHubTestRunCmd() *cobra.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
if runAll {
|
if runAll {
|
||||||
if err := HubTest.LoadAllTests(); err != nil {
|
if err := hubPtr.LoadAllTests(); err != nil {
|
||||||
return fmt.Errorf("unable to load all tests: %+v", err)
|
return fmt.Errorf("unable to load all tests: %+v", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, testName := range args {
|
for _, testName := range args {
|
||||||
_, err := HubTest.LoadTestItem(testName)
|
_, err := hubPtr.LoadTestItem(testName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to load test '%s': %s", testName, err)
|
return fmt.Errorf("unable to load test '%s': %s", testName, err)
|
||||||
}
|
}
|
||||||
|
@ -202,8 +230,7 @@ func NewHubTestRunCmd() *cobra.Command {
|
||||||
|
|
||||||
// set timezone to avoid DST issues
|
// set timezone to avoid DST issues
|
||||||
os.Setenv("TZ", "UTC")
|
os.Setenv("TZ", "UTC")
|
||||||
|
for _, test := range hubPtr.Tests {
|
||||||
for _, test := range HubTest.Tests {
|
|
||||||
if csConfig.Cscli.Output == "human" {
|
if csConfig.Cscli.Output == "human" {
|
||||||
log.Infof("Running test '%s'", test.Name)
|
log.Infof("Running test '%s'", test.Name)
|
||||||
}
|
}
|
||||||
|
@ -218,8 +245,8 @@ func NewHubTestRunCmd() *cobra.Command {
|
||||||
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
|
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
success := true
|
success := true
|
||||||
testResult := make(map[string]bool)
|
testResult := make(map[string]bool)
|
||||||
for _, test := range HubTest.Tests {
|
for _, test := range hubPtr.Tests {
|
||||||
if test.AutoGen {
|
if test.AutoGen && !isAppsecTest {
|
||||||
if test.ParserAssert.AutoGenAssert {
|
if test.ParserAssert.AutoGenAssert {
|
||||||
log.Warningf("Assert file '%s' is empty, generating assertion:", test.ParserAssert.File)
|
log.Warningf("Assert file '%s' is empty, generating assertion:", test.ParserAssert.File)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
@ -341,7 +368,7 @@ func NewHubTestCleanCmd() *cobra.Command {
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
for _, testName := range args {
|
for _, testName := range args {
|
||||||
test, err := HubTest.LoadTestItem(testName)
|
test, err := hubPtr.LoadTestItem(testName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to load test '%s': %s", testName, err)
|
return fmt.Errorf("unable to load test '%s': %s", testName, err)
|
||||||
}
|
}
|
||||||
|
@ -364,17 +391,23 @@ func NewHubTestInfoCmd() *cobra.Command {
|
||||||
Args: cobra.MinimumNArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
for _, testName := range args {
|
for _, testName := range args {
|
||||||
test, err := HubTest.LoadTestItem(testName)
|
test, err := hubPtr.LoadTestItem(testName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to load test '%s': %s", testName, err)
|
return fmt.Errorf("unable to load test '%s': %s", testName, err)
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf(" Test name : %s\n", test.Name)
|
fmt.Printf(" Test name : %s\n", test.Name)
|
||||||
fmt.Printf(" Test path : %s\n", test.Path)
|
fmt.Printf(" Test path : %s\n", test.Path)
|
||||||
|
if isAppsecTest {
|
||||||
|
fmt.Printf(" Nuclei Template : %s\n", test.Config.NucleiTemplate)
|
||||||
|
fmt.Printf(" Appsec Rules : %s\n", strings.Join(test.Config.AppsecRules, ", "))
|
||||||
|
} else {
|
||||||
fmt.Printf(" Log file : %s\n", filepath.Join(test.Path, test.Config.LogFile))
|
fmt.Printf(" Log file : %s\n", filepath.Join(test.Path, test.Config.LogFile))
|
||||||
fmt.Printf(" Parser assertion file : %s\n", filepath.Join(test.Path, hubtest.ParserAssertFileName))
|
fmt.Printf(" Parser assertion file : %s\n", filepath.Join(test.Path, hubtest.ParserAssertFileName))
|
||||||
fmt.Printf(" Scenario assertion file : %s\n", filepath.Join(test.Path, hubtest.ScenarioAssertFileName))
|
fmt.Printf(" Scenario assertion file : %s\n", filepath.Join(test.Path, hubtest.ScenarioAssertFileName))
|
||||||
|
}
|
||||||
fmt.Printf(" Configuration File : %s\n", filepath.Join(test.Path, "config.yaml"))
|
fmt.Printf(" Configuration File : %s\n", filepath.Join(test.Path, "config.yaml"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -391,15 +424,15 @@ func NewHubTestListCmd() *cobra.Command {
|
||||||
Short: "list",
|
Short: "list",
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if err := HubTest.LoadAllTests(); err != nil {
|
if err := hubPtr.LoadAllTests(); err != nil {
|
||||||
return fmt.Errorf("unable to load all tests: %s", err)
|
return fmt.Errorf("unable to load all tests: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch csConfig.Cscli.Output {
|
switch csConfig.Cscli.Output {
|
||||||
case "human":
|
case "human":
|
||||||
hubTestListTable(color.Output, HubTest.Tests)
|
hubTestListTable(color.Output, hubPtr.Tests)
|
||||||
case "json":
|
case "json":
|
||||||
j, err := json.MarshalIndent(HubTest.Tests, " ", " ")
|
j, err := json.MarshalIndent(hubPtr.Tests, " ", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -419,23 +452,27 @@ func NewHubTestCoverageCmd() *cobra.Command {
|
||||||
var showParserCov bool
|
var showParserCov bool
|
||||||
var showScenarioCov bool
|
var showScenarioCov bool
|
||||||
var showOnlyPercent bool
|
var showOnlyPercent bool
|
||||||
|
var showAppsecCov bool
|
||||||
|
|
||||||
var cmdHubTestCoverage = &cobra.Command{
|
var cmdHubTestCoverage = &cobra.Command{
|
||||||
Use: "coverage",
|
Use: "coverage",
|
||||||
Short: "coverage",
|
Short: "coverage",
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
//for this one we explicitly don't do for appsec
|
||||||
if err := HubTest.LoadAllTests(); err != nil {
|
if err := HubTest.LoadAllTests(); err != nil {
|
||||||
return fmt.Errorf("unable to load all tests: %+v", err)
|
return fmt.Errorf("unable to load all tests: %+v", err)
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
scenarioCoverage := []hubtest.Coverage{}
|
scenarioCoverage := []hubtest.Coverage{}
|
||||||
parserCoverage := []hubtest.Coverage{}
|
parserCoverage := []hubtest.Coverage{}
|
||||||
|
appsecRuleCoverage := []hubtest.Coverage{}
|
||||||
scenarioCoveragePercent := 0
|
scenarioCoveragePercent := 0
|
||||||
parserCoveragePercent := 0
|
parserCoveragePercent := 0
|
||||||
|
appsecRuleCoveragePercent := 0
|
||||||
|
|
||||||
// if both are false (flag by default), show both
|
// if both are false (flag by default), show both
|
||||||
showAll := !showScenarioCov && !showParserCov
|
showAll := !showScenarioCov && !showParserCov && !showAppsecCov
|
||||||
|
|
||||||
if showParserCov || showAll {
|
if showParserCov || showAll {
|
||||||
parserCoverage, err = HubTest.GetParsersCoverage()
|
parserCoverage, err = HubTest.GetParsersCoverage()
|
||||||
|
@ -467,13 +504,30 @@ func NewHubTestCoverageCmd() *cobra.Command {
|
||||||
scenarioCoveragePercent = int(math.Round((float64(scenarioTested) / float64(len(scenarioCoverage)) * 100)))
|
scenarioCoveragePercent = int(math.Round((float64(scenarioTested) / float64(len(scenarioCoverage)) * 100)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if showAppsecCov || showAll {
|
||||||
|
appsecRuleCoverage, err = HubTest.GetAppsecCoverage()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("while getting scenario coverage: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
appsecRuleTested := 0
|
||||||
|
for _, test := range appsecRuleCoverage {
|
||||||
|
if test.TestsCount > 0 {
|
||||||
|
appsecRuleTested++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appsecRuleCoveragePercent = int(math.Round((float64(appsecRuleTested) / float64(len(appsecRuleCoverage)) * 100)))
|
||||||
|
}
|
||||||
|
|
||||||
if showOnlyPercent {
|
if showOnlyPercent {
|
||||||
if showAll {
|
if showAll {
|
||||||
fmt.Printf("parsers=%d%%\nscenarios=%d%%", parserCoveragePercent, scenarioCoveragePercent)
|
fmt.Printf("parsers=%d%%\nscenarios=%d%%\nappsec_rules=%d%%", parserCoveragePercent, scenarioCoveragePercent, appsecRuleCoveragePercent)
|
||||||
} else if showParserCov {
|
} else if showParserCov {
|
||||||
fmt.Printf("parsers=%d%%", parserCoveragePercent)
|
fmt.Printf("parsers=%d%%", parserCoveragePercent)
|
||||||
} else if showScenarioCov {
|
} else if showScenarioCov {
|
||||||
fmt.Printf("scenarios=%d%%", scenarioCoveragePercent)
|
fmt.Printf("scenarios=%d%%", scenarioCoveragePercent)
|
||||||
|
} else if showAppsecCov {
|
||||||
|
fmt.Printf("appsec_rules=%d%%", appsecRuleCoveragePercent)
|
||||||
}
|
}
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
@ -487,6 +541,11 @@ func NewHubTestCoverageCmd() *cobra.Command {
|
||||||
if showScenarioCov || showAll {
|
if showScenarioCov || showAll {
|
||||||
hubTestScenarioCoverageTable(color.Output, scenarioCoverage)
|
hubTestScenarioCoverageTable(color.Output, scenarioCoverage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if showAppsecCov || showAll {
|
||||||
|
hubTestAppsecRuleCoverageTable(color.Output, appsecRuleCoverage)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
if showParserCov || showAll {
|
if showParserCov || showAll {
|
||||||
fmt.Printf("PARSERS : %d%% of coverage\n", parserCoveragePercent)
|
fmt.Printf("PARSERS : %d%% of coverage\n", parserCoveragePercent)
|
||||||
|
@ -494,6 +553,9 @@ func NewHubTestCoverageCmd() *cobra.Command {
|
||||||
if showScenarioCov || showAll {
|
if showScenarioCov || showAll {
|
||||||
fmt.Printf("SCENARIOS : %d%% of coverage\n", scenarioCoveragePercent)
|
fmt.Printf("SCENARIOS : %d%% of coverage\n", scenarioCoveragePercent)
|
||||||
}
|
}
|
||||||
|
if showAppsecCov || showAll {
|
||||||
|
fmt.Printf("APPSEC RULES : %d%% of coverage\n", appsecRuleCoveragePercent)
|
||||||
|
}
|
||||||
case "json":
|
case "json":
|
||||||
dump, err := json.MarshalIndent(parserCoverage, "", " ")
|
dump, err := json.MarshalIndent(parserCoverage, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -505,6 +567,11 @@ func NewHubTestCoverageCmd() *cobra.Command {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf("%s", dump)
|
fmt.Printf("%s", dump)
|
||||||
|
dump, err = json.MarshalIndent(appsecRuleCoverage, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("%s", dump)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("only human/json output modes are supported")
|
return fmt.Errorf("only human/json output modes are supported")
|
||||||
}
|
}
|
||||||
|
@ -516,6 +583,7 @@ func NewHubTestCoverageCmd() *cobra.Command {
|
||||||
cmdHubTestCoverage.PersistentFlags().BoolVar(&showOnlyPercent, "percent", false, "Show only percentages of coverage")
|
cmdHubTestCoverage.PersistentFlags().BoolVar(&showOnlyPercent, "percent", false, "Show only percentages of coverage")
|
||||||
cmdHubTestCoverage.PersistentFlags().BoolVar(&showParserCov, "parsers", false, "Show only parsers coverage")
|
cmdHubTestCoverage.PersistentFlags().BoolVar(&showParserCov, "parsers", false, "Show only parsers coverage")
|
||||||
cmdHubTestCoverage.PersistentFlags().BoolVar(&showScenarioCov, "scenarios", false, "Show only scenarios coverage")
|
cmdHubTestCoverage.PersistentFlags().BoolVar(&showScenarioCov, "scenarios", false, "Show only scenarios coverage")
|
||||||
|
cmdHubTestCoverage.PersistentFlags().BoolVar(&showAppsecCov, "appsec", false, "Show only appsec coverage")
|
||||||
|
|
||||||
return cmdHubTestCoverage
|
return cmdHubTestCoverage
|
||||||
}
|
}
|
||||||
|
@ -529,7 +597,7 @@ func NewHubTestEvalCmd() *cobra.Command {
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
for _, testName := range args {
|
for _, testName := range args {
|
||||||
test, err := HubTest.LoadTestItem(testName)
|
test, err := hubPtr.LoadTestItem(testName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("can't load test: %+v", err)
|
return fmt.Errorf("can't load test: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,26 @@ func hubTestParserCoverageTable(out io.Writer, coverage []hubtest.Coverage) {
|
||||||
t.Render()
|
t.Render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hubTestAppsecRuleCoverageTable(out io.Writer, coverage []hubtest.Coverage) {
|
||||||
|
t := newLightTable(out)
|
||||||
|
t.SetHeaders("Appsec Rule", "Status", "Number of tests")
|
||||||
|
t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft)
|
||||||
|
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft)
|
||||||
|
|
||||||
|
parserTested := 0
|
||||||
|
|
||||||
|
for _, test := range coverage {
|
||||||
|
status := emoji.RedCircle.String()
|
||||||
|
if test.TestsCount > 0 {
|
||||||
|
status = emoji.GreenCircle.String()
|
||||||
|
parserTested++
|
||||||
|
}
|
||||||
|
t.AddRow(test.Name, status, fmt.Sprintf("%d times (across %d tests)", test.TestsCount, len(test.PresentIn)))
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Render()
|
||||||
|
}
|
||||||
|
|
||||||
func hubTestScenarioCoverageTable(out io.Writer, coverage []hubtest.Coverage) {
|
func hubTestScenarioCoverageTable(out io.Writer, coverage []hubtest.Coverage) {
|
||||||
t := newLightTable(out)
|
t := newLightTable(out)
|
||||||
t.SetHeaders("Scenario", "Status", "Number of tests")
|
t.SetHeaders("Scenario", "Status", "Number of tests")
|
||||||
|
|
|
@ -32,6 +32,8 @@ func ShowMetrics(hubItem *cwhub.Item) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case cwhub.APPSEC_RULES:
|
||||||
|
log.Error("FIXME: not implemented yet")
|
||||||
default:
|
default:
|
||||||
// no metrics for this item type
|
// no metrics for this item type
|
||||||
}
|
}
|
||||||
|
|
454
cmd/crowdsec-cli/itemcli.go
Normal file
454
cmd/crowdsec-cli/itemcli.go
Normal file
|
@ -0,0 +1,454 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/crowdsecurity/go-cs-lib/coalesce"
|
||||||
|
|
||||||
|
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cliHelp struct {
|
||||||
|
// Example is required, the others have a default value
|
||||||
|
// generated from the item type
|
||||||
|
use string
|
||||||
|
short string
|
||||||
|
long string
|
||||||
|
example string
|
||||||
|
}
|
||||||
|
|
||||||
|
type itemCLI struct {
|
||||||
|
name string // plural, as used in the hub index
|
||||||
|
singular string
|
||||||
|
oneOrMore string // parenthetical pluralizaion: "parser(s)"
|
||||||
|
help cliHelp
|
||||||
|
installHelp cliHelp
|
||||||
|
removeHelp cliHelp
|
||||||
|
upgradeHelp cliHelp
|
||||||
|
inspectHelp cliHelp
|
||||||
|
inspectDetail func(item *cwhub.Item) error
|
||||||
|
listHelp cliHelp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it itemCLI) NewCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: coalesce.String(it.help.use, fmt.Sprintf("%s <action> [item]...", it.name)),
|
||||||
|
Short: coalesce.String(it.help.short, fmt.Sprintf("Manage hub %s", it.name)),
|
||||||
|
Long: it.help.long,
|
||||||
|
Example: it.help.example,
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
Aliases: []string{it.singular},
|
||||||
|
DisableAutoGenTag: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(it.NewInstallCmd())
|
||||||
|
cmd.AddCommand(it.NewRemoveCmd())
|
||||||
|
cmd.AddCommand(it.NewUpgradeCmd())
|
||||||
|
cmd.AddCommand(it.NewInspectCmd())
|
||||||
|
cmd.AddCommand(it.NewListCmd())
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it itemCLI) Install(cmd *cobra.Command, args []string) error {
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
downloadOnly, err := flags.GetBool("download-only")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
force, err := flags.GetBool("force")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreError, err := flags.GetBool("ignore")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hub, err := require.Hub(csConfig, require.RemoteHub(csConfig))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range args {
|
||||||
|
item := hub.GetItem(it.name, name)
|
||||||
|
if item == nil {
|
||||||
|
msg := suggestNearestMessage(hub, it.name, name)
|
||||||
|
if !ignoreError {
|
||||||
|
return fmt.Errorf(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Errorf(msg)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := item.Install(force, downloadOnly); err != nil {
|
||||||
|
if !ignoreError {
|
||||||
|
return fmt.Errorf("error while installing '%s': %w", item.Name, err)
|
||||||
|
}
|
||||||
|
log.Errorf("Error while installing '%s': %s", item.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof(ReloadMessage())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it itemCLI) NewInstallCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: coalesce.String(it.installHelp.use, "install [item]..."),
|
||||||
|
Short: coalesce.String(it.installHelp.short, fmt.Sprintf("Install given %s", it.oneOrMore)),
|
||||||
|
Long: coalesce.String(it.installHelp.long, fmt.Sprintf("Fetch and install one or more %s from the hub", it.name)),
|
||||||
|
Example: it.installHelp.example,
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
DisableAutoGenTag: true,
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return compAllItems(it.name, args, toComplete)
|
||||||
|
},
|
||||||
|
RunE: it.Install,
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.BoolP("download-only", "d", false, "Only download packages, don't enable")
|
||||||
|
flags.Bool("force", false, "Force install: overwrite tainted and outdated files")
|
||||||
|
flags.Bool("ignore", false, fmt.Sprintf("Ignore errors when installing multiple %s", it.name))
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the names of the installed parents of an item, used to check if we can remove it
|
||||||
|
func istalledParentNames(item *cwhub.Item) []string {
|
||||||
|
ret := make([]string, 0)
|
||||||
|
|
||||||
|
for _, parent := range item.Ancestors() {
|
||||||
|
if parent.State.Installed {
|
||||||
|
ret = append(ret, parent.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it itemCLI) Remove(cmd *cobra.Command, args []string) error {
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
purge, err := flags.GetBool("purge")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
force, err := flags.GetBool("force")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
all, err := flags.GetBool("all")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hub, err := require.Hub(csConfig, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if all {
|
||||||
|
getter := hub.GetInstalledItems
|
||||||
|
if purge {
|
||||||
|
getter = hub.GetAllItems
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := getter(it.name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
removed := 0
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
didRemove, err := item.Remove(purge, force)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if didRemove {
|
||||||
|
log.Infof("Removed %s", item.Name)
|
||||||
|
removed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Removed %d %s", removed, it.name)
|
||||||
|
if removed > 0 {
|
||||||
|
log.Infof(ReloadMessage())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
return fmt.Errorf("specify at least one %s to remove or '--all'", it.singular)
|
||||||
|
}
|
||||||
|
|
||||||
|
removed := 0
|
||||||
|
|
||||||
|
for _, itemName := range args {
|
||||||
|
item := hub.GetItem(it.name, itemName)
|
||||||
|
if item == nil {
|
||||||
|
return fmt.Errorf("can't find '%s' in %s", itemName, it.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
parents := istalledParentNames(item)
|
||||||
|
|
||||||
|
if !force && len(parents) > 0 {
|
||||||
|
log.Warningf("%s belongs to collections: %s", item.Name, parents)
|
||||||
|
log.Warningf("Run 'sudo cscli %s remove %s --force' if you want to force remove this %s", item.Type, item.Name, it.singular)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
didRemove, err := item.Remove(purge, force)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if didRemove {
|
||||||
|
log.Infof("Removed %s", item.Name)
|
||||||
|
removed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Removed %d %s", removed, it.name)
|
||||||
|
if removed > 0 {
|
||||||
|
log.Infof(ReloadMessage())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it itemCLI) NewRemoveCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: coalesce.String(it.removeHelp.use, "remove [item]..."),
|
||||||
|
Short: coalesce.String(it.removeHelp.short, fmt.Sprintf("Remove given %s", it.oneOrMore)),
|
||||||
|
Long: coalesce.String(it.removeHelp.long, fmt.Sprintf("Remove one or more %s", it.name)),
|
||||||
|
Example: it.removeHelp.example,
|
||||||
|
Aliases: []string{"delete"},
|
||||||
|
DisableAutoGenTag: true,
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return compInstalledItems(it.name, args, toComplete)
|
||||||
|
},
|
||||||
|
RunE: it.Remove,
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.Bool("purge", false, "Delete source file too")
|
||||||
|
flags.Bool("force", false, "Force remove: remove tainted and outdated files")
|
||||||
|
flags.Bool("all", false, fmt.Sprintf("Remove all the %s", it.name))
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it itemCLI) Upgrade(cmd *cobra.Command, args []string) error {
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
force, err := flags.GetBool("force")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
all, err := flags.GetBool("all")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hub, err := require.Hub(csConfig, require.RemoteHub(csConfig))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if all {
|
||||||
|
items, err := hub.GetInstalledItems(it.name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := 0
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
didUpdate, err := item.Upgrade(force)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if didUpdate {
|
||||||
|
updated++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Updated %d %s", updated, it.name)
|
||||||
|
|
||||||
|
if updated > 0 {
|
||||||
|
log.Infof(ReloadMessage())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
return fmt.Errorf("specify at least one %s to upgrade or '--all'", it.singular)
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := 0
|
||||||
|
|
||||||
|
for _, itemName := range args {
|
||||||
|
item := hub.GetItem(it.name, itemName)
|
||||||
|
if item == nil {
|
||||||
|
return fmt.Errorf("can't find '%s' in %s", itemName, it.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
didUpdate, err := item.Upgrade(force)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if didUpdate {
|
||||||
|
log.Infof("Updated %s", item.Name)
|
||||||
|
updated++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if updated > 0 {
|
||||||
|
log.Infof(ReloadMessage())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it itemCLI) NewUpgradeCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: coalesce.String(it.upgradeHelp.use, "upgrade [item]..."),
|
||||||
|
Short: coalesce.String(it.upgradeHelp.short, fmt.Sprintf("Upgrade given %s", it.oneOrMore)),
|
||||||
|
Long: coalesce.String(it.upgradeHelp.long, fmt.Sprintf("Fetch and upgrade one or more %s from the hub", it.name)),
|
||||||
|
Example: it.upgradeHelp.example,
|
||||||
|
DisableAutoGenTag: true,
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return compInstalledItems(it.name, args, toComplete)
|
||||||
|
},
|
||||||
|
RunE: it.Upgrade,
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.BoolP("all", "a", false, fmt.Sprintf("Upgrade all the %s", it.name))
|
||||||
|
flags.Bool("force", false, "Force upgrade: overwrite tainted and outdated files")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it itemCLI) Inspect(cmd *cobra.Command, args []string) error {
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
url, err := flags.GetString("url")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if url != "" {
|
||||||
|
csConfig.Cscli.PrometheusUrl = url
|
||||||
|
}
|
||||||
|
|
||||||
|
noMetrics, err := flags.GetBool("no-metrics")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hub, err := require.Hub(csConfig, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range args {
|
||||||
|
item := hub.GetItem(it.name, name)
|
||||||
|
if item == nil {
|
||||||
|
return fmt.Errorf("can't find '%s' in %s", name, it.name)
|
||||||
|
}
|
||||||
|
if err = InspectItem(item, !noMetrics); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if it.inspectDetail != nil {
|
||||||
|
if err = it.inspectDetail(item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it itemCLI) NewInspectCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: coalesce.String(it.inspectHelp.use, "inspect [item]..."),
|
||||||
|
Short: coalesce.String(it.inspectHelp.short, fmt.Sprintf("Inspect given %s", it.oneOrMore)),
|
||||||
|
Long: coalesce.String(it.inspectHelp.long, fmt.Sprintf("Inspect the state of one or more %s", it.name)),
|
||||||
|
Example: it.inspectHelp.example,
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
DisableAutoGenTag: true,
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return compInstalledItems(it.name, args, toComplete)
|
||||||
|
},
|
||||||
|
RunE: it.Inspect,
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.StringP("url", "u", "", "Prometheus url")
|
||||||
|
flags.Bool("no-metrics", false, "Don't show metrics (when cscli.output=human)")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it itemCLI) List(cmd *cobra.Command, args []string) error {
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
all, err := flags.GetBool("all")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hub, err := require.Hub(csConfig, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make(map[string][]*cwhub.Item)
|
||||||
|
|
||||||
|
items[it.name], err = selectItems(hub, it.name, args, !all)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = listItems(color.Output, []string{it.name}, items, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it itemCLI) NewListCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: coalesce.String(it.listHelp.use, "list [item... | -a]"),
|
||||||
|
Short: coalesce.String(it.listHelp.short, fmt.Sprintf("List %s", it.oneOrMore)),
|
||||||
|
Long: coalesce.String(it.listHelp.long, fmt.Sprintf("List of installed/available/specified %s", it.name)),
|
||||||
|
Example: it.listHelp.example,
|
||||||
|
DisableAutoGenTag: true,
|
||||||
|
RunE: it.List,
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.BoolP("all", "a", false, "List disabled items as well")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
|
@ -1,609 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/crowdsecurity/go-cs-lib/coalesce"
|
|
||||||
|
|
||||||
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
|
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
|
||||||
)
|
|
||||||
|
|
||||||
type cmdHelp struct {
|
|
||||||
// Example is required, the others have a default value
|
|
||||||
// generated from the item type
|
|
||||||
use string
|
|
||||||
short string
|
|
||||||
long string
|
|
||||||
example string
|
|
||||||
}
|
|
||||||
|
|
||||||
type hubItemType struct {
|
|
||||||
name string // plural, as used in the hub index
|
|
||||||
singular string
|
|
||||||
oneOrMore string // parenthetical pluralizaion: "parser(s)"
|
|
||||||
help cmdHelp
|
|
||||||
installHelp cmdHelp
|
|
||||||
removeHelp cmdHelp
|
|
||||||
upgradeHelp cmdHelp
|
|
||||||
inspectHelp cmdHelp
|
|
||||||
listHelp cmdHelp
|
|
||||||
}
|
|
||||||
|
|
||||||
var hubItemTypes = map[string]hubItemType{
|
|
||||||
"parsers": {
|
|
||||||
name: cwhub.PARSERS,
|
|
||||||
singular: "parser",
|
|
||||||
oneOrMore: "parser(s)",
|
|
||||||
help: cmdHelp{
|
|
||||||
example: `cscli parsers list -a
|
|
||||||
cscli parsers install crowdsecurity/caddy-logs crowdsecurity/sshd-logs
|
|
||||||
cscli parsers inspect crowdsecurity/caddy-logs crowdsecurity/sshd-logs
|
|
||||||
cscli parsers upgrade crowdsecurity/caddy-logs crowdsecurity/sshd-logs
|
|
||||||
cscli parsers remove crowdsecurity/caddy-logs crowdsecurity/sshd-logs
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
installHelp: cmdHelp{
|
|
||||||
example: `cscli parsers install crowdsecurity/caddy-logs crowdsecurity/sshd-logs`,
|
|
||||||
},
|
|
||||||
removeHelp: cmdHelp{
|
|
||||||
example: `cscli parsers remove crowdsecurity/caddy-logs crowdsecurity/sshd-logs`,
|
|
||||||
},
|
|
||||||
upgradeHelp: cmdHelp{
|
|
||||||
example: `cscli parsers upgrade crowdsecurity/caddy-logs crowdsecurity/sshd-logs`,
|
|
||||||
},
|
|
||||||
inspectHelp: cmdHelp{
|
|
||||||
example: `cscli parsers inspect crowdsecurity/httpd-logs crowdsecurity/sshd-logs`,
|
|
||||||
},
|
|
||||||
listHelp: cmdHelp{
|
|
||||||
example: `cscli parsers list
|
|
||||||
cscli parsers list -a
|
|
||||||
cscli parsers list crowdsecurity/caddy-logs crowdsecurity/sshd-logs
|
|
||||||
|
|
||||||
List only enabled parsers unless "-a" or names are specified.`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"postoverflows": {
|
|
||||||
name: cwhub.POSTOVERFLOWS,
|
|
||||||
singular: "postoverflow",
|
|
||||||
oneOrMore: "postoverflow(s)",
|
|
||||||
help: cmdHelp{
|
|
||||||
example: `cscli postoverflows list -a
|
|
||||||
cscli postoverflows install crowdsecurity/cdn-whitelist crowdsecurity/rdns
|
|
||||||
cscli postoverflows inspect crowdsecurity/cdn-whitelist crowdsecurity/rdns
|
|
||||||
cscli postoverflows upgrade crowdsecurity/cdn-whitelist crowdsecurity/rdns
|
|
||||||
cscli postoverflows remove crowdsecurity/cdn-whitelist crowdsecurity/rdns
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
installHelp: cmdHelp{
|
|
||||||
example: `cscli postoverflows install crowdsecurity/cdn-whitelist crowdsecurity/rdns`,
|
|
||||||
},
|
|
||||||
removeHelp: cmdHelp{
|
|
||||||
example: `cscli postoverflows remove crowdsecurity/cdn-whitelist crowdsecurity/rdns`,
|
|
||||||
},
|
|
||||||
upgradeHelp: cmdHelp{
|
|
||||||
example: `cscli postoverflows upgrade crowdsecurity/cdn-whitelist crowdsecurity/rdns`,
|
|
||||||
},
|
|
||||||
inspectHelp: cmdHelp{
|
|
||||||
example: `cscli postoverflows inspect crowdsecurity/cdn-whitelist crowdsecurity/rdns`,
|
|
||||||
},
|
|
||||||
listHelp: cmdHelp{
|
|
||||||
example: `cscli postoverflows list
|
|
||||||
cscli postoverflows list -a
|
|
||||||
cscli postoverflows list crowdsecurity/cdn-whitelist crowdsecurity/rdns
|
|
||||||
|
|
||||||
List only enabled postoverflows unless "-a" or names are specified.`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"scenarios": {
|
|
||||||
name: cwhub.SCENARIOS,
|
|
||||||
singular: "scenario",
|
|
||||||
oneOrMore: "scenario(s)",
|
|
||||||
help: cmdHelp{
|
|
||||||
example: `cscli scenarios list -a
|
|
||||||
cscli scenarios install crowdsecurity/ssh-bf crowdsecurity/http-probing
|
|
||||||
cscli scenarios inspect crowdsecurity/ssh-bf crowdsecurity/http-probing
|
|
||||||
cscli scenarios upgrade crowdsecurity/ssh-bf crowdsecurity/http-probing
|
|
||||||
cscli scenarios remove crowdsecurity/ssh-bf crowdsecurity/http-probing
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
installHelp: cmdHelp{
|
|
||||||
example: `cscli scenarios install crowdsecurity/ssh-bf crowdsecurity/http-probing`,
|
|
||||||
},
|
|
||||||
removeHelp: cmdHelp{
|
|
||||||
example: `cscli scenarios remove crowdsecurity/ssh-bf crowdsecurity/http-probing`,
|
|
||||||
},
|
|
||||||
upgradeHelp: cmdHelp{
|
|
||||||
example: `cscli scenarios upgrade crowdsecurity/ssh-bf crowdsecurity/http-probing`,
|
|
||||||
},
|
|
||||||
inspectHelp: cmdHelp{
|
|
||||||
example: `cscli scenarios inspect crowdsecurity/ssh-bf crowdsecurity/http-probing`,
|
|
||||||
},
|
|
||||||
listHelp: cmdHelp{
|
|
||||||
example: `cscli scenarios list
|
|
||||||
cscli scenarios list -a
|
|
||||||
cscli scenarios list crowdsecurity/ssh-bf crowdsecurity/http-probing
|
|
||||||
|
|
||||||
List only enabled scenarios unless "-a" or names are specified.`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"collections": {
|
|
||||||
name: cwhub.COLLECTIONS,
|
|
||||||
singular: "collection",
|
|
||||||
oneOrMore: "collection(s)",
|
|
||||||
help: cmdHelp{
|
|
||||||
example: `cscli collections list -a
|
|
||||||
cscli collections install crowdsecurity/http-cve crowdsecurity/iptables
|
|
||||||
cscli collections inspect crowdsecurity/http-cve crowdsecurity/iptables
|
|
||||||
cscli collections upgrade crowdsecurity/http-cve crowdsecurity/iptables
|
|
||||||
cscli collections remove crowdsecurity/http-cve crowdsecurity/iptables
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
installHelp: cmdHelp{
|
|
||||||
example: `cscli collections install crowdsecurity/http-cve crowdsecurity/iptables`,
|
|
||||||
},
|
|
||||||
removeHelp: cmdHelp{
|
|
||||||
example: `cscli collections remove crowdsecurity/http-cve crowdsecurity/iptables`,
|
|
||||||
},
|
|
||||||
upgradeHelp: cmdHelp{
|
|
||||||
example: `cscli collections upgrade crowdsecurity/http-cve crowdsecurity/iptables`,
|
|
||||||
},
|
|
||||||
inspectHelp: cmdHelp{
|
|
||||||
example: `cscli collections inspect crowdsecurity/http-cve crowdsecurity/iptables`,
|
|
||||||
},
|
|
||||||
listHelp: cmdHelp{
|
|
||||||
example: `cscli collections list
|
|
||||||
cscli collections list -a
|
|
||||||
cscli collections list crowdsecurity/http-cve crowdsecurity/iptables
|
|
||||||
|
|
||||||
List only enabled collections unless "-a" or names are specified.`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewItemsCmd(typeName string) *cobra.Command {
|
|
||||||
it := hubItemTypes[typeName]
|
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: coalesce.String(it.help.use, fmt.Sprintf("%s <action> [item]...", it.name)),
|
|
||||||
Short: coalesce.String(it.help.short, fmt.Sprintf("Manage hub %s", it.name)),
|
|
||||||
Long: it.help.long,
|
|
||||||
Example: it.help.example,
|
|
||||||
Args: cobra.MinimumNArgs(1),
|
|
||||||
Aliases: []string{it.singular},
|
|
||||||
DisableAutoGenTag: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.AddCommand(NewItemsInstallCmd(typeName))
|
|
||||||
cmd.AddCommand(NewItemsRemoveCmd(typeName))
|
|
||||||
cmd.AddCommand(NewItemsUpgradeCmd(typeName))
|
|
||||||
cmd.AddCommand(NewItemsInspectCmd(typeName))
|
|
||||||
cmd.AddCommand(NewItemsListCmd(typeName))
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func itemsInstallRunner(it hubItemType) func(cmd *cobra.Command, args []string) error {
|
|
||||||
run := func(cmd *cobra.Command, args []string) error {
|
|
||||||
flags := cmd.Flags()
|
|
||||||
|
|
||||||
downloadOnly, err := flags.GetBool("download-only")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
force, err := flags.GetBool("force")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ignoreError, err := flags.GetBool("ignore")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
hub, err := require.Hub(csConfig, require.RemoteHub(csConfig))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, name := range args {
|
|
||||||
item := hub.GetItem(it.name, name)
|
|
||||||
if item == nil {
|
|
||||||
msg := suggestNearestMessage(hub, it.name, name)
|
|
||||||
if !ignoreError {
|
|
||||||
return fmt.Errorf(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Errorf(msg)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := item.Install(force, downloadOnly); err != nil {
|
|
||||||
if !ignoreError {
|
|
||||||
return fmt.Errorf("error while installing '%s': %w", item.Name, err)
|
|
||||||
}
|
|
||||||
log.Errorf("Error while installing '%s': %s", item.Name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof(ReloadMessage())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return run
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewItemsInstallCmd(typeName string) *cobra.Command {
|
|
||||||
it := hubItemTypes[typeName]
|
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: coalesce.String(it.installHelp.use, "install [item]..."),
|
|
||||||
Short: coalesce.String(it.installHelp.short, fmt.Sprintf("Install given %s", it.oneOrMore)),
|
|
||||||
Long: coalesce.String(it.installHelp.long, fmt.Sprintf("Fetch and install one or more %s from the hub", it.name)),
|
|
||||||
Example: it.installHelp.example,
|
|
||||||
Args: cobra.MinimumNArgs(1),
|
|
||||||
DisableAutoGenTag: true,
|
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
||||||
return compAllItems(typeName, args, toComplete)
|
|
||||||
},
|
|
||||||
RunE: itemsInstallRunner(it),
|
|
||||||
}
|
|
||||||
|
|
||||||
flags := cmd.Flags()
|
|
||||||
flags.BoolP("download-only", "d", false, "Only download packages, don't enable")
|
|
||||||
flags.Bool("force", false, "Force install: overwrite tainted and outdated files")
|
|
||||||
flags.Bool("ignore", false, fmt.Sprintf("Ignore errors when installing multiple %s", it.name))
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// return the names of the installed parents of an item, used to check if we can remove it
|
|
||||||
func istalledParentNames(item *cwhub.Item) []string {
|
|
||||||
ret := make([]string, 0)
|
|
||||||
|
|
||||||
for _, parent := range item.Ancestors() {
|
|
||||||
if parent.State.Installed {
|
|
||||||
ret = append(ret, parent.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func itemsRemoveRunner(it hubItemType) func(cmd *cobra.Command, args []string) error {
|
|
||||||
run := func(cmd *cobra.Command, args []string) error {
|
|
||||||
flags := cmd.Flags()
|
|
||||||
|
|
||||||
purge, err := flags.GetBool("purge")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
force, err := flags.GetBool("force")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
all, err := flags.GetBool("all")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
hub, err := require.Hub(csConfig, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if all {
|
|
||||||
getter := hub.GetInstalledItems
|
|
||||||
if purge {
|
|
||||||
getter = hub.GetAllItems
|
|
||||||
}
|
|
||||||
|
|
||||||
items, err := getter(it.name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
removed := 0
|
|
||||||
|
|
||||||
for _, item := range items {
|
|
||||||
didRemove, err := item.Remove(purge, force)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if didRemove {
|
|
||||||
log.Infof("Removed %s", item.Name)
|
|
||||||
removed++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("Removed %d %s", removed, it.name)
|
|
||||||
if removed > 0 {
|
|
||||||
log.Infof(ReloadMessage())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) == 0 {
|
|
||||||
return fmt.Errorf("specify at least one %s to remove or '--all'", it.singular)
|
|
||||||
}
|
|
||||||
|
|
||||||
removed := 0
|
|
||||||
|
|
||||||
for _, itemName := range args {
|
|
||||||
item := hub.GetItem(it.name, itemName)
|
|
||||||
if item == nil {
|
|
||||||
return fmt.Errorf("can't find '%s' in %s", itemName, it.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
parents := istalledParentNames(item)
|
|
||||||
|
|
||||||
if !force && len(parents) > 0 {
|
|
||||||
log.Warningf("%s belongs to collections: %s", item.Name, parents)
|
|
||||||
log.Warningf("Run 'sudo cscli %s remove %s --force' if you want to force remove this %s", item.Type, item.Name, it.singular)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
didRemove, err := item.Remove(purge, force)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if didRemove {
|
|
||||||
log.Infof("Removed %s", item.Name)
|
|
||||||
removed++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("Removed %d %s", removed, it.name)
|
|
||||||
if removed > 0 {
|
|
||||||
log.Infof(ReloadMessage())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return run
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewItemsRemoveCmd(typeName string) *cobra.Command {
|
|
||||||
it := hubItemTypes[typeName]
|
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: coalesce.String(it.removeHelp.use, "remove [item]..."),
|
|
||||||
Short: coalesce.String(it.removeHelp.short, fmt.Sprintf("Remove given %s", it.oneOrMore)),
|
|
||||||
Long: coalesce.String(it.removeHelp.long, fmt.Sprintf("Remove one or more %s", it.name)),
|
|
||||||
Example: it.removeHelp.example,
|
|
||||||
Aliases: []string{"delete"},
|
|
||||||
DisableAutoGenTag: true,
|
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
||||||
return compInstalledItems(it.name, args, toComplete)
|
|
||||||
},
|
|
||||||
RunE: itemsRemoveRunner(it),
|
|
||||||
}
|
|
||||||
|
|
||||||
flags := cmd.Flags()
|
|
||||||
flags.Bool("purge", false, "Delete source file too")
|
|
||||||
flags.Bool("force", false, "Force remove: remove tainted and outdated files")
|
|
||||||
flags.Bool("all", false, fmt.Sprintf("Remove all the %s", it.name))
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func itemsUpgradeRunner(it hubItemType) func(cmd *cobra.Command, args []string) error {
|
|
||||||
run := func(cmd *cobra.Command, args []string) error {
|
|
||||||
flags := cmd.Flags()
|
|
||||||
|
|
||||||
force, err := flags.GetBool("force")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
all, err := flags.GetBool("all")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
hub, err := require.Hub(csConfig, require.RemoteHub(csConfig))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if all {
|
|
||||||
items, err := hub.GetInstalledItems(it.name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
updated := 0
|
|
||||||
|
|
||||||
for _, item := range items {
|
|
||||||
didUpdate, err := item.Upgrade(force)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if didUpdate {
|
|
||||||
updated++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("Updated %d %s", updated, it.name)
|
|
||||||
|
|
||||||
if updated > 0 {
|
|
||||||
log.Infof(ReloadMessage())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) == 0 {
|
|
||||||
return fmt.Errorf("specify at least one %s to upgrade or '--all'", it.singular)
|
|
||||||
}
|
|
||||||
|
|
||||||
updated := 0
|
|
||||||
|
|
||||||
for _, itemName := range args {
|
|
||||||
item := hub.GetItem(it.name, itemName)
|
|
||||||
if item == nil {
|
|
||||||
return fmt.Errorf("can't find '%s' in %s", itemName, it.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
didUpdate, err := item.Upgrade(force)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if didUpdate {
|
|
||||||
log.Infof("Updated %s", item.Name)
|
|
||||||
updated++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if updated > 0 {
|
|
||||||
log.Infof(ReloadMessage())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return run
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewItemsUpgradeCmd(typeName string) *cobra.Command {
|
|
||||||
it := hubItemTypes[typeName]
|
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: coalesce.String(it.upgradeHelp.use, "upgrade [item]..."),
|
|
||||||
Short: coalesce.String(it.upgradeHelp.short, fmt.Sprintf("Upgrade given %s", it.oneOrMore)),
|
|
||||||
Long: coalesce.String(it.upgradeHelp.long, fmt.Sprintf("Fetch and upgrade one or more %s from the hub", it.name)),
|
|
||||||
Example: it.upgradeHelp.example,
|
|
||||||
DisableAutoGenTag: true,
|
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
||||||
return compInstalledItems(it.name, args, toComplete)
|
|
||||||
},
|
|
||||||
RunE: itemsUpgradeRunner(it),
|
|
||||||
}
|
|
||||||
|
|
||||||
flags := cmd.Flags()
|
|
||||||
flags.BoolP("all", "a", false, fmt.Sprintf("Upgrade all the %s", it.name))
|
|
||||||
flags.Bool("force", false, "Force upgrade: overwrite tainted and outdated files")
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func itemsInspectRunner(it hubItemType) func(cmd *cobra.Command, args []string) error {
|
|
||||||
run := func(cmd *cobra.Command, args []string) error {
|
|
||||||
flags := cmd.Flags()
|
|
||||||
|
|
||||||
url, err := flags.GetString("url")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if url != "" {
|
|
||||||
csConfig.Cscli.PrometheusUrl = url
|
|
||||||
}
|
|
||||||
|
|
||||||
noMetrics, err := flags.GetBool("no-metrics")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
hub, err := require.Hub(csConfig, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, name := range args {
|
|
||||||
item := hub.GetItem(it.name, name)
|
|
||||||
if item == nil {
|
|
||||||
return fmt.Errorf("can't find '%s' in %s", name, it.name)
|
|
||||||
}
|
|
||||||
if err = InspectItem(item, !noMetrics); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return run
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewItemsInspectCmd(typeName string) *cobra.Command {
|
|
||||||
it := hubItemTypes[typeName]
|
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: coalesce.String(it.inspectHelp.use, "inspect [item]..."),
|
|
||||||
Short: coalesce.String(it.inspectHelp.short, fmt.Sprintf("Inspect given %s", it.oneOrMore)),
|
|
||||||
Long: coalesce.String(it.inspectHelp.long, fmt.Sprintf("Inspect the state of one or more %s", it.name)),
|
|
||||||
Example: it.inspectHelp.example,
|
|
||||||
Args: cobra.MinimumNArgs(1),
|
|
||||||
DisableAutoGenTag: true,
|
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
||||||
return compInstalledItems(it.name, args, toComplete)
|
|
||||||
},
|
|
||||||
RunE: itemsInspectRunner(it),
|
|
||||||
}
|
|
||||||
|
|
||||||
flags := cmd.Flags()
|
|
||||||
flags.StringP("url", "u", "", "Prometheus url")
|
|
||||||
flags.Bool("no-metrics", false, "Don't show metrics (when cscli.output=human)")
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func itemsListRunner(it hubItemType) func(cmd *cobra.Command, args []string) error {
|
|
||||||
run := func(cmd *cobra.Command, args []string) error {
|
|
||||||
flags := cmd.Flags()
|
|
||||||
|
|
||||||
all, err := flags.GetBool("all")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
hub, err := require.Hub(csConfig, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
items := make(map[string][]*cwhub.Item)
|
|
||||||
|
|
||||||
items[it.name], err = selectItems(hub, it.name, args, !all)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = listItems(color.Output, []string{it.name}, items, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return run
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewItemsListCmd(typeName string) *cobra.Command {
|
|
||||||
it := hubItemTypes[typeName]
|
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: coalesce.String(it.listHelp.use, "list [item... | -a]"),
|
|
||||||
Short: coalesce.String(it.listHelp.short, fmt.Sprintf("List %s", it.oneOrMore)),
|
|
||||||
Long: coalesce.String(it.listHelp.long, fmt.Sprintf("List of installed/available/specified %s", it.name)),
|
|
||||||
Example: it.listHelp.example,
|
|
||||||
DisableAutoGenTag: true,
|
|
||||||
RunE: itemsListRunner(it),
|
|
||||||
}
|
|
||||||
|
|
||||||
flags := cmd.Flags()
|
|
||||||
flags.BoolP("all", "a", false, "List disabled items as well")
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
|
@ -6,12 +6,13 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
cc "github.com/ivanpirog/coloredcobra"
|
cc "github.com/ivanpirog/coloredcobra"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/cobra/doc"
|
"github.com/spf13/cobra/doc"
|
||||||
"slices"
|
|
||||||
|
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/cwversion"
|
"github.com/crowdsecurity/crowdsec/pkg/cwversion"
|
||||||
|
@ -240,10 +241,13 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall
|
||||||
rootCmd.AddCommand(NewHubTestCmd())
|
rootCmd.AddCommand(NewHubTestCmd())
|
||||||
rootCmd.AddCommand(NewNotificationsCmd())
|
rootCmd.AddCommand(NewNotificationsCmd())
|
||||||
rootCmd.AddCommand(NewSupportCmd())
|
rootCmd.AddCommand(NewSupportCmd())
|
||||||
rootCmd.AddCommand(NewItemsCmd("collections"))
|
|
||||||
rootCmd.AddCommand(NewItemsCmd("parsers"))
|
rootCmd.AddCommand(NewCollectionCLI().NewCommand())
|
||||||
rootCmd.AddCommand(NewItemsCmd("scenarios"))
|
rootCmd.AddCommand(NewParserCLI().NewCommand())
|
||||||
rootCmd.AddCommand(NewItemsCmd("postoverflows"))
|
rootCmd.AddCommand(NewScenarioCLI().NewCommand())
|
||||||
|
rootCmd.AddCommand(NewPostOverflowCLI().NewCommand())
|
||||||
|
rootCmd.AddCommand(NewAppsecConfigCLI().NewCommand())
|
||||||
|
rootCmd.AddCommand(NewAppsecRuleCLI().NewCommand())
|
||||||
|
|
||||||
if fflag.CscliSetup.IsEnabled() {
|
if fflag.CscliSetup.IsEnabled() {
|
||||||
rootCmd.AddCommand(NewSetupCmd())
|
rootCmd.AddCommand(NewSetupCmd())
|
||||||
|
|
|
@ -63,6 +63,8 @@ func FormatPrometheusMetrics(out io.Writer, url string, formatType string) error
|
||||||
lapi_machine_stats := map[string]map[string]map[string]int{}
|
lapi_machine_stats := map[string]map[string]map[string]int{}
|
||||||
lapi_bouncer_stats := map[string]map[string]map[string]int{}
|
lapi_bouncer_stats := map[string]map[string]map[string]int{}
|
||||||
decisions_stats := map[string]map[string]map[string]int{}
|
decisions_stats := map[string]map[string]map[string]int{}
|
||||||
|
appsec_engine_stats := map[string]map[string]int{}
|
||||||
|
appsec_rule_stats := map[string]map[string]map[string]int{}
|
||||||
alerts_stats := map[string]int{}
|
alerts_stats := map[string]int{}
|
||||||
stash_stats := map[string]struct {
|
stash_stats := map[string]struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -226,10 +228,30 @@ func FormatPrometheusMetrics(out io.Writer, url string, formatType string) error
|
||||||
Type string
|
Type string
|
||||||
Count int
|
Count int
|
||||||
}{Type: mtype, Count: ival}
|
}{Type: mtype, Count: ival}
|
||||||
|
case "cs_appsec_reqs_total":
|
||||||
|
if _, ok := appsec_engine_stats[metric.Labels["appsec_engine"]]; !ok {
|
||||||
|
appsec_engine_stats[metric.Labels["appsec_engine"]] = make(map[string]int, 0)
|
||||||
|
}
|
||||||
|
appsec_engine_stats[metric.Labels["appsec_engine"]]["processed"] = ival
|
||||||
|
case "cs_appsec_block_total":
|
||||||
|
if _, ok := appsec_engine_stats[metric.Labels["appsec_engine"]]; !ok {
|
||||||
|
appsec_engine_stats[metric.Labels["appsec_engine"]] = make(map[string]int, 0)
|
||||||
|
}
|
||||||
|
appsec_engine_stats[metric.Labels["appsec_engine"]]["blocked"] = ival
|
||||||
|
case "cs_appsec_rule_hits":
|
||||||
|
appsecEngine := metric.Labels["appsec_engine"]
|
||||||
|
ruleID := metric.Labels["rule_name"]
|
||||||
|
if _, ok := appsec_rule_stats[appsecEngine]; !ok {
|
||||||
|
appsec_rule_stats[appsecEngine] = make(map[string]map[string]int, 0)
|
||||||
|
}
|
||||||
|
if _, ok := appsec_rule_stats[appsecEngine][ruleID]; !ok {
|
||||||
|
appsec_rule_stats[appsecEngine][ruleID] = make(map[string]int, 0)
|
||||||
|
}
|
||||||
|
appsec_rule_stats[appsecEngine][ruleID]["triggered"] = ival
|
||||||
default:
|
default:
|
||||||
|
log.Debugf("unknown: %+v", fam.Name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,6 +266,8 @@ func FormatPrometheusMetrics(out io.Writer, url string, formatType string) error
|
||||||
decisionStatsTable(out, decisions_stats)
|
decisionStatsTable(out, decisions_stats)
|
||||||
alertStatsTable(out, alerts_stats)
|
alertStatsTable(out, alerts_stats)
|
||||||
stashStatsTable(out, stash_stats)
|
stashStatsTable(out, stash_stats)
|
||||||
|
appsecMetricsToTable(out, appsec_engine_stats)
|
||||||
|
appsecRulesToTable(out, appsec_rule_stats)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,7 +306,6 @@ func FormatPrometheusMetrics(out io.Writer, url string, formatType string) error
|
||||||
|
|
||||||
var noUnit bool
|
var noUnit bool
|
||||||
|
|
||||||
|
|
||||||
func runMetrics(cmd *cobra.Command, args []string) error {
|
func runMetrics(cmd *cobra.Command, args []string) error {
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
@ -314,7 +337,6 @@ func runMetrics(cmd *cobra.Command, args []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func NewMetricsCmd() *cobra.Command {
|
func NewMetricsCmd() *cobra.Command {
|
||||||
cmdMetrics := &cobra.Command{
|
cmdMetrics := &cobra.Command{
|
||||||
Use: "metrics",
|
Use: "metrics",
|
||||||
|
|
|
@ -90,7 +90,7 @@ func bucketStatsTable(out io.Writer, stats map[string]map[string]int) {
|
||||||
keys := []string{"curr_count", "overflow", "instantiation", "pour", "underflow"}
|
keys := []string{"curr_count", "overflow", "instantiation", "pour", "underflow"}
|
||||||
|
|
||||||
if numRows, err := metricsToTable(t, stats, keys); err != nil {
|
if numRows, err := metricsToTable(t, stats, keys); err != nil {
|
||||||
log.Warningf("while collecting acquis stats: %s", err)
|
log.Warningf("while collecting bucket stats: %s", err)
|
||||||
} else if numRows > 0 {
|
} else if numRows > 0 {
|
||||||
renderTableTitle(out, "\nBucket Metrics:")
|
renderTableTitle(out, "\nBucket Metrics:")
|
||||||
t.Render()
|
t.Render()
|
||||||
|
@ -113,6 +113,37 @@ func acquisStatsTable(out io.Writer, stats map[string]map[string]int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func appsecMetricsToTable(out io.Writer, metrics map[string]map[string]int) {
|
||||||
|
t := newTable(out)
|
||||||
|
t.SetRowLines(false)
|
||||||
|
t.SetHeaders("Appsec Engine", "Processed", "Blocked")
|
||||||
|
t.SetAlignment(table.AlignLeft, table.AlignLeft)
|
||||||
|
keys := []string{"processed", "blocked"}
|
||||||
|
if numRows, err := metricsToTable(t, metrics, keys); err != nil {
|
||||||
|
log.Warningf("while collecting appsec stats: %s", err)
|
||||||
|
} else if numRows > 0 {
|
||||||
|
renderTableTitle(out, "\nAppsec Metrics:")
|
||||||
|
t.Render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appsecRulesToTable(out io.Writer, metrics map[string]map[string]map[string]int) {
|
||||||
|
for appsecEngine, appsecEngineRulesStats := range metrics {
|
||||||
|
t := newTable(out)
|
||||||
|
t.SetRowLines(false)
|
||||||
|
t.SetHeaders("Rule ID", "Triggered")
|
||||||
|
t.SetAlignment(table.AlignLeft, table.AlignLeft)
|
||||||
|
keys := []string{"triggered"}
|
||||||
|
if numRows, err := metricsToTable(t, appsecEngineRulesStats, keys); err != nil {
|
||||||
|
log.Warningf("while collecting appsec rules stats: %s", err)
|
||||||
|
} else if numRows > 0 {
|
||||||
|
renderTableTitle(out, fmt.Sprintf("\nAppsec '%s' Rules Metrics:", appsecEngine))
|
||||||
|
t.Render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func parserStatsTable(out io.Writer, stats map[string]map[string]int) {
|
func parserStatsTable(out io.Writer, stats map[string]map[string]int) {
|
||||||
t := newTable(out)
|
t := newTable(out)
|
||||||
t.SetRowLines(false)
|
t.SetRowLines(false)
|
||||||
|
@ -122,7 +153,7 @@ func parserStatsTable(out io.Writer, stats map[string]map[string]int) {
|
||||||
keys := []string{"hits", "parsed", "unparsed"}
|
keys := []string{"hits", "parsed", "unparsed"}
|
||||||
|
|
||||||
if numRows, err := metricsToTable(t, stats, keys); err != nil {
|
if numRows, err := metricsToTable(t, stats, keys); err != nil {
|
||||||
log.Warningf("while collecting acquis stats: %s", err)
|
log.Warningf("while collecting parsers stats: %s", err)
|
||||||
} else if numRows > 0 {
|
} else if numRows > 0 {
|
||||||
renderTableTitle(out, "\nParser Metrics:")
|
renderTableTitle(out, "\nParser Metrics:")
|
||||||
t.Render()
|
t.Render()
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/crowdsecurity/go-cs-lib/trace"
|
"github.com/crowdsecurity/go-cs-lib/trace"
|
||||||
|
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/acquisition"
|
"github.com/crowdsecurity/crowdsec/pkg/acquisition"
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/appsec"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||||
leaky "github.com/crowdsecurity/crowdsec/pkg/leakybucket"
|
leaky "github.com/crowdsecurity/crowdsec/pkg/leakybucket"
|
||||||
|
@ -33,6 +34,10 @@ func initCrowdsec(cConfig *csconfig.Config, hub *cwhub.Hub) (*parser.Parsers, er
|
||||||
return nil, fmt.Errorf("while loading scenarios: %w", err)
|
return nil, fmt.Errorf("while loading scenarios: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := appsec.LoadAppsecRules(hub); err != nil {
|
||||||
|
return nil, fmt.Errorf("while loading appsec rules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := LoadAcquisition(cConfig); err != nil {
|
if err := LoadAcquisition(cConfig); err != nil {
|
||||||
return nil, fmt.Errorf("while loading acquisition config: %w", err)
|
return nil, fmt.Errorf("while loading acquisition config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -161,7 +161,8 @@ func registerPrometheus(config *csconfig.PrometheusCfg) {
|
||||||
leaky.BucketsUnderflow, leaky.BucketsCanceled, leaky.BucketsInstantiation, leaky.BucketsOverflow,
|
leaky.BucketsUnderflow, leaky.BucketsCanceled, leaky.BucketsInstantiation, leaky.BucketsOverflow,
|
||||||
v1.LapiRouteHits,
|
v1.LapiRouteHits,
|
||||||
leaky.BucketsCurrentCount,
|
leaky.BucketsCurrentCount,
|
||||||
cache.CacheMetrics, exprhelpers.RegexpCacheMetrics)
|
cache.CacheMetrics, exprhelpers.RegexpCacheMetrics,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
log.Infof("Loading prometheus collectors")
|
log.Infof("Loading prometheus collectors")
|
||||||
prometheus.MustRegister(globalParserHits, globalParserHitsOk, globalParserHitsKo,
|
prometheus.MustRegister(globalParserHits, globalParserHitsOk, globalParserHitsKo,
|
||||||
|
@ -170,7 +171,8 @@ func registerPrometheus(config *csconfig.PrometheusCfg) {
|
||||||
v1.LapiRouteHits, v1.LapiMachineHits, v1.LapiBouncerHits, v1.LapiNilDecisions, v1.LapiNonNilDecisions, v1.LapiResponseTime,
|
v1.LapiRouteHits, v1.LapiMachineHits, v1.LapiBouncerHits, v1.LapiNilDecisions, v1.LapiNonNilDecisions, v1.LapiResponseTime,
|
||||||
leaky.BucketsPour, leaky.BucketsUnderflow, leaky.BucketsCanceled, leaky.BucketsInstantiation, leaky.BucketsOverflow, leaky.BucketsCurrentCount,
|
leaky.BucketsPour, leaky.BucketsUnderflow, leaky.BucketsCanceled, leaky.BucketsInstantiation, leaky.BucketsOverflow, leaky.BucketsCurrentCount,
|
||||||
globalActiveDecisions, globalAlerts,
|
globalActiveDecisions, globalAlerts,
|
||||||
cache.CacheMetrics, exprhelpers.RegexpCacheMetrics)
|
cache.CacheMetrics, exprhelpers.RegexpCacheMetrics,
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,13 @@ LOOP:
|
||||||
if !event.Process {
|
if !event.Process {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
/*Application security engine is going to generate 2 events:
|
||||||
|
- one that is treated as a log and can go to scenarios
|
||||||
|
- another one that will go directly to LAPI*/
|
||||||
|
if event.Type == types.APPSEC {
|
||||||
|
outputEventChan <- event
|
||||||
|
continue
|
||||||
|
}
|
||||||
if event.Line.Module == "" {
|
if event.Line.Module == "" {
|
||||||
log.Errorf("empty event.Line.Module field, the acquisition module must set it ! : %+v", event.Line)
|
log.Errorf("empty event.Line.Module field, the acquisition module must set it ! : %+v", event.Line)
|
||||||
continue
|
continue
|
||||||
|
|
25
go.mod
25
go.mod
|
@ -80,14 +80,19 @@ require (
|
||||||
github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26
|
github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26
|
||||||
github.com/wasilibs/go-re2 v1.3.0
|
github.com/wasilibs/go-re2 v1.3.0
|
||||||
github.com/xhit/go-simple-mail/v2 v2.16.0
|
github.com/xhit/go-simple-mail/v2 v2.16.0
|
||||||
golang.org/x/crypto v0.15.0
|
golang.org/x/crypto v0.16.0
|
||||||
golang.org/x/mod v0.11.0
|
golang.org/x/mod v0.11.0
|
||||||
golang.org/x/sys v0.14.0
|
golang.org/x/sys v0.15.0
|
||||||
google.golang.org/grpc v1.56.3
|
google.golang.org/grpc v1.56.3
|
||||||
google.golang.org/protobuf v1.31.0
|
google.golang.org/protobuf v1.31.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
|
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/crowdsecurity/coraza/v3 v3.0.0-20231206171741-c5b03c916879
|
||||||
|
golang.org/x/text v0.14.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gotest.tools/v3 v3.5.0
|
gotest.tools/v3 v3.5.0
|
||||||
k8s.io/apiserver v0.28.4
|
k8s.io/apiserver v0.28.4
|
||||||
|
@ -103,6 +108,7 @@ require (
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bytedance/sonic v1.9.1 // indirect
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
|
github.com/corazawaf/libinjection-go v0.1.2 // indirect
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
github.com/creack/pty v1.1.18 // indirect
|
github.com/creack/pty v1.1.18 // indirect
|
||||||
|
@ -149,7 +155,7 @@ require (
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||||
github.com/leodido/go-urn v1.2.4 // indirect
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||||
github.com/magefile/mage v1.14.0 // indirect
|
github.com/magefile/mage v1.15.0 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||||
|
@ -168,6 +174,7 @@ require (
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
|
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||||
|
github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.18 // indirect
|
github.com/pierrec/lz4/v4 v4.1.18 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
|
@ -181,7 +188,9 @@ require (
|
||||||
github.com/spf13/cast v1.3.1 // indirect
|
github.com/spf13/cast v1.3.1 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/tetratelabs/wazero v1.2.1 // indirect
|
github.com/tetratelabs/wazero v1.2.1 // indirect
|
||||||
github.com/tidwall/gjson v1.13.0 // indirect
|
github.com/tidwall/gjson v1.17.0 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.11 // indirect
|
github.com/tklauser/go-sysconf v0.3.11 // indirect
|
||||||
github.com/tklauser/numcpus v0.6.0 // indirect
|
github.com/tklauser/numcpus v0.6.0 // indirect
|
||||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
|
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
|
||||||
|
@ -192,10 +201,9 @@ require (
|
||||||
github.com/zclconf/go-cty v1.8.0 // indirect
|
github.com/zclconf/go-cty v1.8.0 // indirect
|
||||||
go.mongodb.org/mongo-driver v1.9.4 // indirect
|
go.mongodb.org/mongo-driver v1.9.4 // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/net v0.18.0 // indirect
|
golang.org/x/net v0.19.0 // indirect
|
||||||
golang.org/x/sync v0.2.0 // indirect
|
golang.org/x/sync v0.5.0 // indirect
|
||||||
golang.org/x/term v0.14.0 // indirect
|
golang.org/x/term v0.15.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/time v0.3.0 // indirect
|
||||||
golang.org/x/tools v0.8.1-0.20230428195545-5283a0178901 // indirect
|
golang.org/x/tools v0.8.1-0.20230428195545-5283a0178901 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
|
@ -207,6 +215,7 @@ require (
|
||||||
k8s.io/apimachinery v0.28.4 // indirect
|
k8s.io/apimachinery v0.28.4 // indirect
|
||||||
k8s.io/klog/v2 v2.100.1 // indirect
|
k8s.io/klog/v2 v2.100.1 // indirect
|
||||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
|
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
|
||||||
|
rsc.io/binaryregexp v0.2.0 // indirect
|
||||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||||
)
|
)
|
||||||
|
|
43
go.sum
43
go.sum
|
@ -84,6 +84,8 @@ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhD
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||||
|
github.com/corazawaf/libinjection-go v0.1.2 h1:oeiV9pc5rvJ+2oqOqXEAMJousPpGiup6f7Y3nZj5GoM=
|
||||||
|
github.com/corazawaf/libinjection-go v0.1.2/go.mod h1:OP4TM7xdJ2skyXqNX1AN1wN5nNZEmJNuWbNPOItn7aw=
|
||||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||||
|
@ -96,6 +98,16 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
||||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||||
|
github.com/crowdsecurity/coraza/v3 v3.0.0-20231114091225-b0f8bc435a75 h1:Kp1sY2PE1H5nbr7xgAQeEWDqDW/o3HNL1rHvcVqzWT4=
|
||||||
|
github.com/crowdsecurity/coraza/v3 v3.0.0-20231114091225-b0f8bc435a75/go.mod h1:jNww1Y9SujXQc89zDR+XOb70bkC7mZ6ep7iKhUBBsiI=
|
||||||
|
github.com/crowdsecurity/coraza/v3 v3.0.0-20231204125126-35deffad7734 h1:THMSMkBW/DLG5NvMAr/Mdg/eQOrEnMJ9Y+UdFG4yV8k=
|
||||||
|
github.com/crowdsecurity/coraza/v3 v3.0.0-20231204125126-35deffad7734/go.mod h1:jNww1Y9SujXQc89zDR+XOb70bkC7mZ6ep7iKhUBBsiI=
|
||||||
|
github.com/crowdsecurity/coraza/v3 v3.0.0-20231204135226-6c45fc2dedf9 h1:vFJiYtKOW5DwGQ9gxQi8+XDNc+YvuXXsJyWXXuiOn+M=
|
||||||
|
github.com/crowdsecurity/coraza/v3 v3.0.0-20231204135226-6c45fc2dedf9/go.mod h1:jNww1Y9SujXQc89zDR+XOb70bkC7mZ6ep7iKhUBBsiI=
|
||||||
|
github.com/crowdsecurity/coraza/v3 v3.0.0-20231204135508-23eef9bf7f39 h1:vY0KZvoS4Xl9IfGucBA4l1CV1auRPPJtjZSTz/Rl6iQ=
|
||||||
|
github.com/crowdsecurity/coraza/v3 v3.0.0-20231204135508-23eef9bf7f39/go.mod h1:jNww1Y9SujXQc89zDR+XOb70bkC7mZ6ep7iKhUBBsiI=
|
||||||
|
github.com/crowdsecurity/coraza/v3 v3.0.0-20231206171741-c5b03c916879 h1:dhAc0AelASC3BbfuLURJeai1LYgFNgpMds0KPd9whbo=
|
||||||
|
github.com/crowdsecurity/coraza/v3 v3.0.0-20231206171741-c5b03c916879/go.mod h1:jNww1Y9SujXQc89zDR+XOb70bkC7mZ6ep7iKhUBBsiI=
|
||||||
github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26 h1:r97WNVC30Uen+7WnLs4xDScS/Ex988+id2k6mDf8psU=
|
github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26 h1:r97WNVC30Uen+7WnLs4xDScS/Ex988+id2k6mDf8psU=
|
||||||
github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26/go.mod h1:zpv7r+7KXwgVUZnUNjyP22zc/D7LKjyoY02weH2RBbk=
|
github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26/go.mod h1:zpv7r+7KXwgVUZnUNjyP22zc/D7LKjyoY02weH2RBbk=
|
||||||
github.com/crowdsecurity/go-cs-lib v0.0.5 h1:eVLW+BRj3ZYn0xt5/xmgzfbbB8EBo32gM4+WpQQk2e8=
|
github.com/crowdsecurity/go-cs-lib v0.0.5 h1:eVLW+BRj3ZYn0xt5/xmgzfbbB8EBo32gM4+WpQQk2e8=
|
||||||
|
@ -125,6 +137,8 @@ github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkK
|
||||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||||
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||||
|
github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI=
|
||||||
|
github.com/foxcpp/go-mockdns v1.0.0/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||||
|
@ -459,8 +473,8 @@ github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffkt
|
||||||
github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
|
github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
@ -496,6 +510,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfr
|
||||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
|
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
||||||
|
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
||||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||||
|
@ -548,6 +564,8 @@ github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUr
|
||||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||||
|
github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e h1:POJco99aNgosh92lGqmx7L1ei+kCymivB/419SD15PQ=
|
||||||
|
github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
|
||||||
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
||||||
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
|
@ -652,13 +670,14 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
||||||
github.com/tetratelabs/wazero v1.2.1 h1:J4X2hrGzJvt+wqltuvcSjHQ7ujQxA9gb6PeMs4qlUWs=
|
github.com/tetratelabs/wazero v1.2.1 h1:J4X2hrGzJvt+wqltuvcSjHQ7ujQxA9gb6PeMs4qlUWs=
|
||||||
github.com/tetratelabs/wazero v1.2.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ=
|
github.com/tetratelabs/wazero v1.2.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ=
|
||||||
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/gjson v1.13.0 h1:3TFY9yxOQShrvmjdM76K+jc66zJeT6D3/VFFYCGQf7M=
|
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
|
||||||
github.com/tidwall/gjson v1.13.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
|
||||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
|
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
|
||||||
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
|
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
|
||||||
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
|
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
|
||||||
|
@ -748,6 +767,8 @@ golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
|
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
|
||||||
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
|
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
|
||||||
|
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
||||||
|
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
@ -783,6 +804,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
|
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
|
||||||
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
|
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
|
||||||
|
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||||
|
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
@ -792,8 +815,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
|
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||||
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
@ -835,6 +858,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
@ -844,6 +869,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8=
|
golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8=
|
||||||
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
|
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
|
||||||
|
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||||
|
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
@ -943,6 +970,8 @@ k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
|
||||||
k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
|
k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
|
||||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
|
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
|
||||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||||
|
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
|
||||||
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/crowdsecurity/go-cs-lib/trace"
|
"github.com/crowdsecurity/go-cs-lib/trace"
|
||||||
|
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
|
"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
|
||||||
|
appsecacquisition "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/appsec"
|
||||||
cloudwatchacquisition "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/cloudwatch"
|
cloudwatchacquisition "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/cloudwatch"
|
||||||
dockeracquisition "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/docker"
|
dockeracquisition "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/docker"
|
||||||
fileacquisition "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/file"
|
fileacquisition "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/file"
|
||||||
|
@ -76,6 +77,7 @@ var AcquisitionSources = map[string]func() DataSource{
|
||||||
"k8s-audit": func() DataSource { return &k8sauditacquisition.KubernetesAuditSource{} },
|
"k8s-audit": func() DataSource { return &k8sauditacquisition.KubernetesAuditSource{} },
|
||||||
"loki": func() DataSource { return &lokiacquisition.LokiSource{} },
|
"loki": func() DataSource { return &lokiacquisition.LokiSource{} },
|
||||||
"s3": func() DataSource { return &s3acquisition.S3Source{} },
|
"s3": func() DataSource { return &s3acquisition.S3Source{} },
|
||||||
|
"appsec": func() DataSource { return &appsecacquisition.AppsecSource{} },
|
||||||
}
|
}
|
||||||
|
|
||||||
var transformRuntimes = map[string]*vm.Program{}
|
var transformRuntimes = map[string]*vm.Program{}
|
||||||
|
|
371
pkg/acquisition/modules/appsec/appsec.go
Normal file
371
pkg/acquisition/modules/appsec/appsec.go
Normal file
|
@ -0,0 +1,371 @@
|
||||||
|
package appsecacquisition
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||||
|
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/appsec"
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||||
|
"github.com/crowdsecurity/go-cs-lib/trace"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/tomb.v2"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
InBand = "inband"
|
||||||
|
OutOfBand = "outofband"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DefaultAuthCacheDuration = (1 * time.Minute)
|
||||||
|
)
|
||||||
|
|
||||||
|
// configuration structure of the acquis for the application security engine
|
||||||
|
type AppsecSourceConfig struct {
|
||||||
|
ListenAddr string `yaml:"listen_addr"`
|
||||||
|
CertFilePath string `yaml:"cert_file"`
|
||||||
|
KeyFilePath string `yaml:"key_file"`
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
Routines int `yaml:"routines"`
|
||||||
|
AppsecConfig string `yaml:"appsec_config"`
|
||||||
|
AppsecConfigPath string `yaml:"appsec_config_path"`
|
||||||
|
AuthCacheDuration *time.Duration `yaml:"auth_cache_duration"`
|
||||||
|
configuration.DataSourceCommonCfg `yaml:",inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// runtime structure of AppsecSourceConfig
|
||||||
|
type AppsecSource struct {
|
||||||
|
config AppsecSourceConfig
|
||||||
|
logger *log.Entry
|
||||||
|
mux *http.ServeMux
|
||||||
|
server *http.Server
|
||||||
|
outChan chan types.Event
|
||||||
|
InChan chan appsec.ParsedRequest
|
||||||
|
AppsecRuntime *appsec.AppsecRuntimeConfig
|
||||||
|
AppsecConfigs map[string]appsec.AppsecConfig
|
||||||
|
lapiURL string
|
||||||
|
AuthCache AuthCache
|
||||||
|
AppsecRunners []AppsecRunner //one for each go-routine
|
||||||
|
}
|
||||||
|
|
||||||
|
// Struct to handle cache of authentication
|
||||||
|
type AuthCache struct {
|
||||||
|
APIKeys map[string]time.Time
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthCache() AuthCache {
|
||||||
|
return AuthCache{
|
||||||
|
APIKeys: make(map[string]time.Time, 0),
|
||||||
|
mu: sync.RWMutex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *AuthCache) Set(apiKey string, expiration time.Time) {
|
||||||
|
ac.mu.Lock()
|
||||||
|
ac.APIKeys[apiKey] = expiration
|
||||||
|
ac.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *AuthCache) Get(apiKey string) (time.Time, bool) {
|
||||||
|
ac.mu.RLock()
|
||||||
|
expiration, exists := ac.APIKeys[apiKey]
|
||||||
|
ac.mu.RUnlock()
|
||||||
|
return expiration, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// @tko + @sbl : we might want to get rid of that or improve it
|
||||||
|
type BodyResponse struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecSource) UnmarshalConfig(yamlConfig []byte) error {
|
||||||
|
|
||||||
|
err := yaml.UnmarshalStrict(yamlConfig, &w.config)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Cannot parse appsec configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.config.ListenAddr == "" {
|
||||||
|
w.config.ListenAddr = "127.0.0.1:7422"
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.config.Path == "" {
|
||||||
|
w.config.Path = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.config.Path[0] != '/' {
|
||||||
|
w.config.Path = "/" + w.config.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.config.Mode == "" {
|
||||||
|
w.config.Mode = configuration.TAIL_MODE
|
||||||
|
}
|
||||||
|
|
||||||
|
// always have at least one appsec routine
|
||||||
|
if w.config.Routines == 0 {
|
||||||
|
w.config.Routines = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.config.AppsecConfig == "" && w.config.AppsecConfigPath == "" {
|
||||||
|
return fmt.Errorf("appsec_config or appsec_config_path must be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.config.Name == "" {
|
||||||
|
w.config.Name = fmt.Sprintf("%s%s", w.config.ListenAddr, w.config.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
csConfig := csconfig.GetConfig()
|
||||||
|
w.lapiURL = fmt.Sprintf("%sv1/decisions/stream", csConfig.API.Client.Credentials.URL)
|
||||||
|
w.AuthCache = NewAuthCache()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecSource) GetMetrics() []prometheus.Collector {
|
||||||
|
return []prometheus.Collector{AppsecReqCounter, AppsecBlockCounter, AppsecRuleHits, AppsecOutbandParsingHistogram, AppsecInbandParsingHistogram, AppsecGlobalParsingHistogram}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecSource) GetAggregMetrics() []prometheus.Collector {
|
||||||
|
return []prometheus.Collector{AppsecReqCounter, AppsecBlockCounter, AppsecRuleHits, AppsecOutbandParsingHistogram, AppsecInbandParsingHistogram, AppsecGlobalParsingHistogram}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecSource) Configure(yamlConfig []byte, logger *log.Entry) error {
|
||||||
|
err := w.UnmarshalConfig(yamlConfig)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "unable to parse appsec configuration")
|
||||||
|
}
|
||||||
|
w.logger = logger
|
||||||
|
|
||||||
|
w.logger.Tracef("Appsec configuration: %+v", w.config)
|
||||||
|
|
||||||
|
if w.config.AuthCacheDuration == nil {
|
||||||
|
w.config.AuthCacheDuration = &DefaultAuthCacheDuration
|
||||||
|
w.logger.Infof("Cache duration for auth not set, using default: %v", *w.config.AuthCacheDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.mux = http.NewServeMux()
|
||||||
|
|
||||||
|
w.server = &http.Server{
|
||||||
|
Addr: w.config.ListenAddr,
|
||||||
|
Handler: w.mux,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.InChan = make(chan appsec.ParsedRequest)
|
||||||
|
appsecCfg := appsec.AppsecConfig{Logger: w.logger.WithField("component", "appsec_config")}
|
||||||
|
|
||||||
|
//let's load the associated appsec_config:
|
||||||
|
if w.config.AppsecConfigPath != "" {
|
||||||
|
err := appsecCfg.LoadByPath(w.config.AppsecConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to load appsec_config : %s", err)
|
||||||
|
}
|
||||||
|
} else if w.config.AppsecConfig != "" {
|
||||||
|
err := appsecCfg.Load(w.config.AppsecConfig)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to load appsec_config : %s", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("no appsec_config provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.AppsecRuntime, err = appsecCfg.Build()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to build appsec_config : %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.AppsecRuntime.ProcessOnLoadRules()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to process on load rules : %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.AppsecRunners = make([]AppsecRunner, w.config.Routines)
|
||||||
|
|
||||||
|
for nbRoutine := 0; nbRoutine < w.config.Routines; nbRoutine++ {
|
||||||
|
appsecRunnerUUID := uuid.New().String()
|
||||||
|
//we copy AppsecRutime for each runner
|
||||||
|
wrt := *w.AppsecRuntime
|
||||||
|
wrt.Logger = w.logger.Dup().WithField("runner_uuid", appsecRunnerUUID)
|
||||||
|
runner := AppsecRunner{
|
||||||
|
inChan: w.InChan,
|
||||||
|
UUID: appsecRunnerUUID,
|
||||||
|
logger: w.logger.WithFields(log.Fields{
|
||||||
|
"runner_uuid": appsecRunnerUUID,
|
||||||
|
}),
|
||||||
|
AppsecRuntime: &wrt,
|
||||||
|
Labels: w.config.Labels,
|
||||||
|
}
|
||||||
|
err := runner.Init(appsecCfg.GetDataDir())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to initialize runner : %s", err)
|
||||||
|
}
|
||||||
|
w.AppsecRunners[nbRoutine] = runner
|
||||||
|
}
|
||||||
|
|
||||||
|
w.logger.Infof("Created %d appsec runners", len(w.AppsecRunners))
|
||||||
|
|
||||||
|
//We don´t use the wrapper provided by coraza because we want to fully control what happens when a rule match to send the information in crowdsec
|
||||||
|
w.mux.HandleFunc(w.config.Path, w.appsecHandler)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecSource) ConfigureByDSN(dsn string, labels map[string]string, logger *log.Entry, uuid string) error {
|
||||||
|
return fmt.Errorf("AppSec datasource does not support command line acquisition")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecSource) GetMode() string {
|
||||||
|
return w.config.Mode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecSource) GetName() string {
|
||||||
|
return "appsec"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecSource) OneShotAcquisition(out chan types.Event, t *tomb.Tomb) error {
|
||||||
|
return fmt.Errorf("AppSec datasource does not support command line acquisition")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecSource) StreamingAcquisition(out chan types.Event, t *tomb.Tomb) error {
|
||||||
|
w.outChan = out
|
||||||
|
t.Go(func() error {
|
||||||
|
defer trace.CatchPanic("crowdsec/acquis/appsec/live")
|
||||||
|
|
||||||
|
w.logger.Infof("%d appsec runner to start", len(w.AppsecRunners))
|
||||||
|
for _, runner := range w.AppsecRunners {
|
||||||
|
runner := runner
|
||||||
|
runner.outChan = out
|
||||||
|
t.Go(func() error {
|
||||||
|
defer trace.CatchPanic("crowdsec/acquis/appsec/live/runner")
|
||||||
|
return runner.Run(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
w.logger.Infof("Starting Appsec server on %s%s", w.config.ListenAddr, w.config.Path)
|
||||||
|
t.Go(func() error {
|
||||||
|
var err error
|
||||||
|
if w.config.CertFilePath != "" && w.config.KeyFilePath != "" {
|
||||||
|
err = w.server.ListenAndServeTLS(w.config.CertFilePath, w.config.KeyFilePath)
|
||||||
|
} else {
|
||||||
|
err = w.server.ListenAndServe()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && err != http.ErrServerClosed {
|
||||||
|
return errors.Wrap(err, "Appsec server failed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
<-t.Dying()
|
||||||
|
w.logger.Infof("Stopping Appsec server on %s%s", w.config.ListenAddr, w.config.Path)
|
||||||
|
w.server.Shutdown(context.TODO())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecSource) CanRun() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecSource) GetUuid() string {
|
||||||
|
return w.config.UniqueId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecSource) Dump() interface{} {
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecSource) IsAuth(apiKey string) bool {
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 200 * time.Millisecond,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodHead, w.lapiURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error creating request: %s", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("X-Api-Key", apiKey)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error performing request: %s", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return resp.StatusCode == http.StatusOK
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// should this be in the runner ?
|
||||||
|
func (w *AppsecSource) appsecHandler(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
w.logger.Debugf("Received request from '%s' on %s", r.RemoteAddr, r.URL.Path)
|
||||||
|
|
||||||
|
apiKey := r.Header.Get(appsec.APIKeyHeaderName)
|
||||||
|
clientIP := r.Header.Get(appsec.IPHeaderName)
|
||||||
|
remoteIP := r.RemoteAddr
|
||||||
|
if apiKey == "" {
|
||||||
|
w.logger.Errorf("Unauthorized request from '%s' (real IP = %s)", remoteIP, clientIP)
|
||||||
|
rw.WriteHeader(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expiration, exists := w.AuthCache.Get(apiKey)
|
||||||
|
// if the apiKey is not in cache or has expired, just recheck the auth
|
||||||
|
if !exists || time.Now().After(expiration) {
|
||||||
|
if !w.IsAuth(apiKey) {
|
||||||
|
rw.WriteHeader(http.StatusUnauthorized)
|
||||||
|
w.logger.Errorf("Unauthorized request from '%s' (real IP = %s)", remoteIP, clientIP)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiKey is valid, store it in cache
|
||||||
|
w.AuthCache.Set(apiKey, time.Now().Add(*w.config.AuthCacheDuration))
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the request only once
|
||||||
|
parsedRequest, err := appsec.NewParsedRequestFromRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("%s", err)
|
||||||
|
rw.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parsedRequest.AppsecEngine = w.config.Name
|
||||||
|
|
||||||
|
logger := w.logger.WithFields(log.Fields{
|
||||||
|
"request_uuid": parsedRequest.UUID,
|
||||||
|
"client_ip": parsedRequest.ClientIP,
|
||||||
|
})
|
||||||
|
|
||||||
|
AppsecReqCounter.With(prometheus.Labels{"source": parsedRequest.RemoteAddrNormalized, "appsec_engine": parsedRequest.AppsecEngine}).Inc()
|
||||||
|
|
||||||
|
w.InChan <- parsedRequest
|
||||||
|
|
||||||
|
response := <-parsedRequest.ResponseChannel
|
||||||
|
if response.InBandInterrupt {
|
||||||
|
AppsecBlockCounter.With(prometheus.Labels{"source": parsedRequest.RemoteAddrNormalized, "appsec_engine": parsedRequest.AppsecEngine}).Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
appsecResponse := w.AppsecRuntime.GenerateResponse(response, logger)
|
||||||
|
|
||||||
|
rw.WriteHeader(appsecResponse.HTTPStatus)
|
||||||
|
body, err := json.Marshal(BodyResponse{Action: appsecResponse.Action})
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("unable to marshal response: %s", err)
|
||||||
|
rw.WriteHeader(http.StatusInternalServerError)
|
||||||
|
} else {
|
||||||
|
rw.Write(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
350
pkg/acquisition/modules/appsec/appsec_runner.go
Normal file
350
pkg/acquisition/modules/appsec/appsec_runner.go
Normal file
|
@ -0,0 +1,350 @@
|
||||||
|
package appsecacquisition
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/crowdsecurity/coraza/v3"
|
||||||
|
corazatypes "github.com/crowdsecurity/coraza/v3/types"
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/appsec"
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/tomb.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// that's the runtime structure of the Application security engine as seen from the acquis
|
||||||
|
type AppsecRunner struct {
|
||||||
|
outChan chan types.Event
|
||||||
|
inChan chan appsec.ParsedRequest
|
||||||
|
UUID string
|
||||||
|
AppsecRuntime *appsec.AppsecRuntimeConfig //this holds the actual appsec runtime config, rules, remediations, hooks etc.
|
||||||
|
AppsecInbandEngine coraza.WAF
|
||||||
|
AppsecOutbandEngine coraza.WAF
|
||||||
|
Labels map[string]string
|
||||||
|
logger *log.Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AppsecRunner) Init(datadir string) error {
|
||||||
|
var err error
|
||||||
|
fs := os.DirFS(datadir)
|
||||||
|
|
||||||
|
inBandRules := ""
|
||||||
|
outOfBandRules := ""
|
||||||
|
|
||||||
|
for _, collection := range r.AppsecRuntime.InBandRules {
|
||||||
|
inBandRules += collection.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, collection := range r.AppsecRuntime.OutOfBandRules {
|
||||||
|
outOfBandRules += collection.String()
|
||||||
|
}
|
||||||
|
inBandLogger := r.logger.Dup().WithField("band", "inband")
|
||||||
|
outBandLogger := r.logger.Dup().WithField("band", "outband")
|
||||||
|
|
||||||
|
//setting up inband engine
|
||||||
|
inbandCfg := coraza.NewWAFConfig().WithDirectives(inBandRules).WithRootFS(fs).WithDebugLogger(appsec.NewCrzLogger(inBandLogger))
|
||||||
|
if !r.AppsecRuntime.Config.InbandOptions.DisableBodyInspection {
|
||||||
|
inbandCfg = inbandCfg.WithRequestBodyAccess()
|
||||||
|
} else {
|
||||||
|
log.Warningf("Disabling body inspection, Inband rules will not be able to match on body's content.")
|
||||||
|
}
|
||||||
|
if r.AppsecRuntime.Config.InbandOptions.RequestBodyInMemoryLimit != nil {
|
||||||
|
inbandCfg = inbandCfg.WithRequestBodyInMemoryLimit(*r.AppsecRuntime.Config.InbandOptions.RequestBodyInMemoryLimit)
|
||||||
|
}
|
||||||
|
r.AppsecInbandEngine, err = coraza.NewWAF(inbandCfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to initialize inband engine : %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//setting up outband engine
|
||||||
|
outbandCfg := coraza.NewWAFConfig().WithDirectives(outOfBandRules).WithRootFS(fs).WithDebugLogger(appsec.NewCrzLogger(outBandLogger))
|
||||||
|
if !r.AppsecRuntime.Config.OutOfBandOptions.DisableBodyInspection {
|
||||||
|
outbandCfg = outbandCfg.WithRequestBodyAccess()
|
||||||
|
} else {
|
||||||
|
log.Warningf("Disabling body inspection, Out of band rules will not be able to match on body's content.")
|
||||||
|
}
|
||||||
|
if r.AppsecRuntime.Config.OutOfBandOptions.RequestBodyInMemoryLimit != nil {
|
||||||
|
outbandCfg = outbandCfg.WithRequestBodyInMemoryLimit(*r.AppsecRuntime.Config.OutOfBandOptions.RequestBodyInMemoryLimit)
|
||||||
|
}
|
||||||
|
r.AppsecOutbandEngine, err = coraza.NewWAF(outbandCfg)
|
||||||
|
|
||||||
|
if r.AppsecRuntime.DisabledInBandRulesTags != nil {
|
||||||
|
for _, tag := range r.AppsecRuntime.DisabledInBandRulesTags {
|
||||||
|
r.AppsecInbandEngine.GetRuleGroup().DeleteByTag(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.AppsecRuntime.DisabledOutOfBandRulesTags != nil {
|
||||||
|
for _, tag := range r.AppsecRuntime.DisabledOutOfBandRulesTags {
|
||||||
|
r.AppsecOutbandEngine.GetRuleGroup().DeleteByTag(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.AppsecRuntime.DisabledInBandRuleIds != nil {
|
||||||
|
for _, id := range r.AppsecRuntime.DisabledInBandRuleIds {
|
||||||
|
r.AppsecInbandEngine.GetRuleGroup().DeleteByID(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.AppsecRuntime.DisabledOutOfBandRuleIds != nil {
|
||||||
|
for _, id := range r.AppsecRuntime.DisabledOutOfBandRuleIds {
|
||||||
|
r.AppsecOutbandEngine.GetRuleGroup().DeleteByID(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Tracef("Loaded inband rules: %+v", r.AppsecInbandEngine.GetRuleGroup().GetRules())
|
||||||
|
r.logger.Tracef("Loaded outband rules: %+v", r.AppsecOutbandEngine.GetRuleGroup().GetRules())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to initialize outband engine : %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AppsecRunner) processRequest(tx appsec.ExtendedTransaction, request *appsec.ParsedRequest) error {
|
||||||
|
var in *corazatypes.Interruption
|
||||||
|
var err error
|
||||||
|
request.Tx = tx
|
||||||
|
|
||||||
|
if request.Tx.IsRuleEngineOff() {
|
||||||
|
r.logger.Debugf("rule engine is off, skipping")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
request.Tx.ProcessLogging()
|
||||||
|
//We don't close the transaction here, as it will reset coraza internal state and break variable tracking
|
||||||
|
}()
|
||||||
|
|
||||||
|
//pre eval (expr) rules
|
||||||
|
err = r.AppsecRuntime.ProcessPreEvalRules(request)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Errorf("unable to process PreEval rules: %s", err)
|
||||||
|
//FIXME: should we abort here ?
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Tx.Tx.ProcessConnection(request.RemoteAddr, 0, "", 0)
|
||||||
|
|
||||||
|
for k, v := range request.Args {
|
||||||
|
for _, vv := range v {
|
||||||
|
request.Tx.AddGetRequestArgument(k, vv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Tx.ProcessURI(request.URI, request.Method, request.Proto)
|
||||||
|
|
||||||
|
for k, vr := range request.Headers {
|
||||||
|
for _, v := range vr {
|
||||||
|
request.Tx.AddRequestHeader(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.ClientHost != "" {
|
||||||
|
request.Tx.AddRequestHeader("Host", request.ClientHost)
|
||||||
|
request.Tx.SetServerName(request.ClientHost)
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.TransferEncoding != nil {
|
||||||
|
request.Tx.AddRequestHeader("Transfer-Encoding", request.TransferEncoding[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
in = request.Tx.ProcessRequestHeaders()
|
||||||
|
|
||||||
|
if in != nil {
|
||||||
|
r.logger.Infof("inband rules matched for headers : %s", in.Action)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.Body != nil && len(request.Body) > 0 {
|
||||||
|
in, _, err = request.Tx.WriteRequestBody(request.Body)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Errorf("unable to write request body : %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if in != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
in, err = request.Tx.ProcessRequestBody()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Errorf("unable to process request body : %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if in != nil {
|
||||||
|
r.logger.Debugf("rules matched for body : %d", in.RuleID)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.AppsecRuntime.ProcessPostEvalRules(request)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Errorf("unable to process PostEval rules: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AppsecRunner) ProcessInBandRules(request *appsec.ParsedRequest) error {
|
||||||
|
tx := appsec.NewExtendedTransaction(r.AppsecInbandEngine, request.UUID)
|
||||||
|
r.AppsecRuntime.InBandTx = tx
|
||||||
|
err := r.processRequest(tx, request)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AppsecRunner) ProcessOutOfBandRules(request *appsec.ParsedRequest) error {
|
||||||
|
r.logger.Debugf("Processing out of band rules")
|
||||||
|
tx := appsec.NewExtendedTransaction(r.AppsecOutbandEngine, request.UUID)
|
||||||
|
r.AppsecRuntime.OutOfBandTx = tx
|
||||||
|
err := r.processRequest(tx, request)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AppsecRunner) handleInBandInterrupt(request *appsec.ParsedRequest) {
|
||||||
|
//create the associated event for crowdsec itself
|
||||||
|
evt, err := EventFromRequest(request, r.Labels)
|
||||||
|
if err != nil {
|
||||||
|
//let's not interrupt the pipeline for this
|
||||||
|
r.logger.Errorf("unable to create event from request : %s", err)
|
||||||
|
}
|
||||||
|
err = r.AccumulateTxToEvent(&evt, request)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Errorf("unable to accumulate tx to event : %s", err)
|
||||||
|
}
|
||||||
|
if in := request.Tx.Interruption(); in != nil {
|
||||||
|
r.logger.Debugf("inband rules matched : %d", in.RuleID)
|
||||||
|
r.AppsecRuntime.Response.InBandInterrupt = true
|
||||||
|
r.AppsecRuntime.Response.HTTPResponseCode = r.AppsecRuntime.Config.BlockedHTTPCode
|
||||||
|
r.AppsecRuntime.Response.Action = r.AppsecRuntime.DefaultRemediation
|
||||||
|
|
||||||
|
if _, ok := r.AppsecRuntime.RemediationById[in.RuleID]; ok {
|
||||||
|
r.AppsecRuntime.Response.Action = r.AppsecRuntime.RemediationById[in.RuleID]
|
||||||
|
}
|
||||||
|
|
||||||
|
for tag, remediation := range r.AppsecRuntime.RemediationByTag {
|
||||||
|
if slices.Contains[[]string, string](in.Tags, tag) {
|
||||||
|
r.AppsecRuntime.Response.Action = remediation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.AppsecRuntime.ProcessOnMatchRules(request, evt)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Errorf("unable to process OnMatch rules: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Should the in band match trigger an event ?
|
||||||
|
if r.AppsecRuntime.Response.SendEvent {
|
||||||
|
r.outChan <- evt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should the in band match trigger an overflow ?
|
||||||
|
if r.AppsecRuntime.Response.SendAlert {
|
||||||
|
appsecOvlfw, err := AppsecEventGeneration(evt)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Errorf("unable to generate appsec event : %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.outChan <- *appsecOvlfw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AppsecRunner) handleOutBandInterrupt(request *appsec.ParsedRequest) {
|
||||||
|
evt, err := EventFromRequest(request, r.Labels)
|
||||||
|
if err != nil {
|
||||||
|
//let's not interrupt the pipeline for this
|
||||||
|
r.logger.Errorf("unable to create event from request : %s", err)
|
||||||
|
}
|
||||||
|
err = r.AccumulateTxToEvent(&evt, request)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Errorf("unable to accumulate tx to event : %s", err)
|
||||||
|
}
|
||||||
|
if in := request.Tx.Interruption(); in != nil {
|
||||||
|
r.logger.Debugf("inband rules matched : %d", in.RuleID)
|
||||||
|
r.AppsecRuntime.Response.OutOfBandInterrupt = true
|
||||||
|
|
||||||
|
err = r.AppsecRuntime.ProcessOnMatchRules(request, evt)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Errorf("unable to process OnMatch rules: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Should the match trigger an event ?
|
||||||
|
if r.AppsecRuntime.Response.SendEvent {
|
||||||
|
r.outChan <- evt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should the match trigger an overflow ?
|
||||||
|
if r.AppsecRuntime.Response.SendAlert {
|
||||||
|
appsecOvlfw, err := AppsecEventGeneration(evt)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Errorf("unable to generate appsec event : %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.outChan <- *appsecOvlfw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AppsecRunner) handleRequest(request *appsec.ParsedRequest) {
|
||||||
|
r.AppsecRuntime.Logger = r.AppsecRuntime.Logger.WithField("request_uuid", request.UUID)
|
||||||
|
logger := r.logger.WithField("request_uuid", request.UUID)
|
||||||
|
logger.Debug("Request received in runner")
|
||||||
|
r.AppsecRuntime.ClearResponse()
|
||||||
|
|
||||||
|
request.IsInBand = true
|
||||||
|
request.IsOutBand = false
|
||||||
|
|
||||||
|
//to measure the time spent in the Application Security Engine
|
||||||
|
startParsing := time.Now()
|
||||||
|
|
||||||
|
//inband appsec rules
|
||||||
|
err := r.ProcessInBandRules(request)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("unable to process InBand rules: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.Tx.IsInterrupted() {
|
||||||
|
r.handleInBandInterrupt(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed := time.Since(startParsing)
|
||||||
|
AppsecInbandParsingHistogram.With(prometheus.Labels{"source": request.RemoteAddr}).Observe(elapsed.Seconds())
|
||||||
|
|
||||||
|
// send back the result to the HTTP handler for the InBand part
|
||||||
|
request.ResponseChannel <- r.AppsecRuntime.Response
|
||||||
|
|
||||||
|
//Now let's process the out of band rules
|
||||||
|
|
||||||
|
request.IsInBand = false
|
||||||
|
request.IsOutBand = true
|
||||||
|
r.AppsecRuntime.Response.SendAlert = false
|
||||||
|
r.AppsecRuntime.Response.SendEvent = true
|
||||||
|
|
||||||
|
err = r.ProcessOutOfBandRules(request)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("unable to process OutOfBand rules: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.Tx.IsInterrupted() {
|
||||||
|
r.handleOutBandInterrupt(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AppsecRunner) Run(t *tomb.Tomb) error {
|
||||||
|
r.logger.Infof("Appsec Runner ready to process event")
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-t.Dying():
|
||||||
|
r.logger.Infof("Appsec Runner is dying")
|
||||||
|
return nil
|
||||||
|
case request := <-r.inChan:
|
||||||
|
r.handleRequest(&request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
pkg/acquisition/modules/appsec/metrics.go
Normal file
54
pkg/acquisition/modules/appsec/metrics.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package appsecacquisition
|
||||||
|
|
||||||
|
import "github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
|
var AppsecGlobalParsingHistogram = prometheus.NewHistogramVec(
|
||||||
|
prometheus.HistogramOpts{
|
||||||
|
Help: "Time spent processing a request by the Application Security Engine.",
|
||||||
|
Name: "cs_appsec_parsing_time_seconds",
|
||||||
|
Buckets: []float64{0.0005, 0.001, 0.0015, 0.002, 0.0025, 0.003, 0.004, 0.005, 0.0075, 0.01},
|
||||||
|
},
|
||||||
|
[]string{"source"},
|
||||||
|
)
|
||||||
|
|
||||||
|
var AppsecInbandParsingHistogram = prometheus.NewHistogramVec(
|
||||||
|
prometheus.HistogramOpts{
|
||||||
|
Help: "Time spent processing a request by the inband Application Security Engine.",
|
||||||
|
Name: "cs_appsec_inband_parsing_time_seconds",
|
||||||
|
Buckets: []float64{0.0005, 0.001, 0.0015, 0.002, 0.0025, 0.003, 0.004, 0.005, 0.0075, 0.01},
|
||||||
|
},
|
||||||
|
[]string{"source"},
|
||||||
|
)
|
||||||
|
|
||||||
|
var AppsecOutbandParsingHistogram = prometheus.NewHistogramVec(
|
||||||
|
prometheus.HistogramOpts{
|
||||||
|
Help: "Time spent processing a request by the Application Security Engine.",
|
||||||
|
Name: "cs_appsec_outband_parsing_time_seconds",
|
||||||
|
Buckets: []float64{0.0005, 0.001, 0.0015, 0.002, 0.0025, 0.003, 0.004, 0.005, 0.0075, 0.01},
|
||||||
|
},
|
||||||
|
[]string{"source"},
|
||||||
|
)
|
||||||
|
|
||||||
|
var AppsecReqCounter = prometheus.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "cs_appsec_reqs_total",
|
||||||
|
Help: "Total events processed by the Application Security Engine.",
|
||||||
|
},
|
||||||
|
[]string{"source", "appsec_engine"},
|
||||||
|
)
|
||||||
|
|
||||||
|
var AppsecBlockCounter = prometheus.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "cs_appsec_block_total",
|
||||||
|
Help: "Total events blocked by the Application Security Engine.",
|
||||||
|
},
|
||||||
|
[]string{"source", "appsec_engine"},
|
||||||
|
)
|
||||||
|
|
||||||
|
var AppsecRuleHits = prometheus.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "cs_appsec_rule_hits",
|
||||||
|
Help: "Count of triggered rule, by rule_name, type (inband/outofband), appsec_engine and source",
|
||||||
|
},
|
||||||
|
[]string{"rule_name", "type", "appsec_engine", "source"},
|
||||||
|
)
|
94
pkg/acquisition/modules/appsec/rx_operator.go
Normal file
94
pkg/acquisition/modules/appsec/rx_operator.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
package appsecacquisition
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/crowdsecurity/coraza/v3/experimental/plugins"
|
||||||
|
"github.com/crowdsecurity/coraza/v3/experimental/plugins/plugintypes"
|
||||||
|
"github.com/wasilibs/go-re2"
|
||||||
|
"github.com/wasilibs/go-re2/experimental"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rx struct {
|
||||||
|
re *re2.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ plugintypes.Operator = (*rx)(nil)
|
||||||
|
|
||||||
|
func newRX(options plugintypes.OperatorOptions) (plugintypes.Operator, error) {
|
||||||
|
// (?sm) enables multiline mode which makes 942522-7 work, see
|
||||||
|
// - https://stackoverflow.com/a/27680233
|
||||||
|
// - https://groups.google.com/g/golang-nuts/c/jiVdamGFU9E
|
||||||
|
data := fmt.Sprintf("(?sm)%s", options.Arguments)
|
||||||
|
|
||||||
|
var re *re2.Regexp
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if matchesArbitraryBytes(data) {
|
||||||
|
re, err = experimental.CompileLatin1(data)
|
||||||
|
} else {
|
||||||
|
re, err = re2.Compile(data)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &rx{re: re}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *rx) Evaluate(tx plugintypes.TransactionState, value string) bool {
|
||||||
|
if tx.Capturing() {
|
||||||
|
match := o.re.FindStringSubmatch(value)
|
||||||
|
if len(match) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, c := range match {
|
||||||
|
if i == 9 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
tx.CaptureField(i, c)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return o.re.MatchString(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRX registers the rx operator using a WASI implementation instead of Go.
|
||||||
|
func RegisterRX() {
|
||||||
|
plugins.RegisterOperator("rx", newRX)
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchesArbitraryBytes checks for control sequences for byte matches in the expression.
|
||||||
|
// If the sequences are not valid utf8, it returns true.
|
||||||
|
func matchesArbitraryBytes(expr string) bool {
|
||||||
|
decoded := make([]byte, 0, len(expr))
|
||||||
|
for i := 0; i < len(expr); i++ {
|
||||||
|
c := expr[i]
|
||||||
|
if c != '\\' {
|
||||||
|
decoded = append(decoded, c)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i+3 >= len(expr) {
|
||||||
|
decoded = append(decoded, expr[i:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if expr[i+1] != 'x' {
|
||||||
|
decoded = append(decoded, expr[i])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
v, mb, _, err := strconv.UnquoteChar(expr[i:], 0)
|
||||||
|
if err != nil || mb {
|
||||||
|
// Wasn't a byte escape sequence, shouldn't happen in practice.
|
||||||
|
decoded = append(decoded, expr[i])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded = append(decoded, byte(v))
|
||||||
|
i += 3
|
||||||
|
}
|
||||||
|
|
||||||
|
return !utf8.Valid(decoded)
|
||||||
|
}
|
280
pkg/acquisition/modules/appsec/utils.go
Normal file
280
pkg/acquisition/modules/appsec/utils.go
Normal file
|
@ -0,0 +1,280 @@
|
||||||
|
package appsecacquisition
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/crowdsecurity/coraza/v3/collection"
|
||||||
|
"github.com/crowdsecurity/coraza/v3/types/variables"
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/appsec"
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/models"
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||||
|
"github.com/crowdsecurity/go-cs-lib/ptr"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AppsecEventGeneration(inEvt types.Event) (*types.Event, error) {
|
||||||
|
//if the request didnd't trigger inband rules, we don't want to generate an event to LAPI/CAPI
|
||||||
|
if !inEvt.Appsec.HasInBandMatches {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
evt := types.Event{}
|
||||||
|
evt.Type = types.APPSEC
|
||||||
|
evt.Process = true
|
||||||
|
source := models.Source{
|
||||||
|
Value: ptr.Of(inEvt.Parsed["source_ip"]),
|
||||||
|
IP: inEvt.Parsed["source_ip"],
|
||||||
|
Scope: ptr.Of(types.Ip),
|
||||||
|
}
|
||||||
|
|
||||||
|
evt.Overflow.Sources = make(map[string]models.Source)
|
||||||
|
evt.Overflow.Sources["ip"] = source
|
||||||
|
|
||||||
|
alert := models.Alert{}
|
||||||
|
alert.Capacity = ptr.Of(int32(1))
|
||||||
|
alert.Events = make([]*models.Event, 0)
|
||||||
|
alert.Meta = make(models.Meta, 0)
|
||||||
|
for _, key := range []string{"target_uri", "method"} {
|
||||||
|
|
||||||
|
valueByte, err := json.Marshal([]string{inEvt.Parsed[key]})
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("unable to serialize key %s", key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
meta := models.MetaItems0{
|
||||||
|
Key: key,
|
||||||
|
Value: string(valueByte),
|
||||||
|
}
|
||||||
|
alert.Meta = append(alert.Meta, &meta)
|
||||||
|
}
|
||||||
|
matchedZones := inEvt.Appsec.GetMatchedZones()
|
||||||
|
if matchedZones != nil {
|
||||||
|
valueByte, err := json.Marshal(matchedZones)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("unable to serialize key matched_zones")
|
||||||
|
} else {
|
||||||
|
meta := models.MetaItems0{
|
||||||
|
Key: "matched_zones",
|
||||||
|
Value: string(valueByte),
|
||||||
|
}
|
||||||
|
alert.Meta = append(alert.Meta, &meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, key := range evt.Appsec.MatchedRules.GetMatchedZones() {
|
||||||
|
valueByte, err := json.Marshal([]string{key})
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("unable to serialize key %s", key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
meta := models.MetaItems0{
|
||||||
|
Key: "matched_zones",
|
||||||
|
Value: string(valueByte),
|
||||||
|
}
|
||||||
|
alert.Meta = append(alert.Meta, &meta)
|
||||||
|
}
|
||||||
|
alert.EventsCount = ptr.Of(int32(1))
|
||||||
|
alert.Leakspeed = ptr.Of("")
|
||||||
|
alert.Scenario = ptr.Of(inEvt.Appsec.MatchedRules.GetName())
|
||||||
|
alert.ScenarioHash = ptr.Of(inEvt.Appsec.MatchedRules.GetHash())
|
||||||
|
alert.ScenarioVersion = ptr.Of(inEvt.Appsec.MatchedRules.GetVersion())
|
||||||
|
alert.Simulated = ptr.Of(false)
|
||||||
|
alert.Source = &source
|
||||||
|
msg := fmt.Sprintf("AppSec block: %s from %s (%s)", inEvt.Appsec.MatchedRules.GetName(),
|
||||||
|
alert.Source.IP, inEvt.Parsed["remediation_cmpt_ip"])
|
||||||
|
alert.Message = &msg
|
||||||
|
alert.StartAt = ptr.Of(time.Now().UTC().Format(time.RFC3339))
|
||||||
|
alert.StopAt = ptr.Of(time.Now().UTC().Format(time.RFC3339))
|
||||||
|
evt.Overflow.APIAlerts = []models.Alert{alert}
|
||||||
|
evt.Overflow.Alert = &alert
|
||||||
|
return &evt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func EventFromRequest(r *appsec.ParsedRequest, labels map[string]string) (types.Event, error) {
|
||||||
|
evt := types.Event{}
|
||||||
|
//we might want to change this based on in-band vs out-of-band ?
|
||||||
|
evt.Type = types.LOG
|
||||||
|
evt.ExpectMode = types.LIVE
|
||||||
|
//def needs fixing
|
||||||
|
evt.Stage = "s00-raw"
|
||||||
|
evt.Parsed = map[string]string{
|
||||||
|
"source_ip": r.ClientIP,
|
||||||
|
"target_host": r.Host,
|
||||||
|
"target_uri": r.URI,
|
||||||
|
"method": r.Method,
|
||||||
|
"req_uuid": r.Tx.ID(),
|
||||||
|
"source": "crowdsec-appsec",
|
||||||
|
"remediation_cmpt_ip": r.RemoteAddrNormalized,
|
||||||
|
//TBD:
|
||||||
|
//http_status
|
||||||
|
//user_agent
|
||||||
|
|
||||||
|
}
|
||||||
|
evt.Line = types.Line{
|
||||||
|
Time: time.Now(),
|
||||||
|
//should we add some info like listen addr/port/path ?
|
||||||
|
Labels: labels,
|
||||||
|
Process: true,
|
||||||
|
Module: "appsec",
|
||||||
|
Src: "appsec",
|
||||||
|
Raw: "dummy-appsec-data", //we discard empty Line.Raw items :)
|
||||||
|
}
|
||||||
|
evt.Appsec = types.AppsecEvent{}
|
||||||
|
|
||||||
|
return evt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LogAppsecEvent(evt *types.Event, logger *log.Entry) {
|
||||||
|
req := evt.Parsed["target_uri"]
|
||||||
|
if len(req) > 12 {
|
||||||
|
req = req[:10] + ".."
|
||||||
|
}
|
||||||
|
|
||||||
|
if evt.Meta["appsec_interrupted"] == "true" {
|
||||||
|
logger.WithFields(log.Fields{
|
||||||
|
"module": "appsec",
|
||||||
|
"source": evt.Parsed["source_ip"],
|
||||||
|
"target_uri": req,
|
||||||
|
}).Infof("%s blocked on %s (%d rules) [%v]", evt.Parsed["source_ip"], req, len(evt.Appsec.MatchedRules), evt.Appsec.GetRuleIDs())
|
||||||
|
} else if evt.Parsed["outofband_interrupted"] == "true" {
|
||||||
|
logger.WithFields(log.Fields{
|
||||||
|
"module": "appsec",
|
||||||
|
"source": evt.Parsed["source_ip"],
|
||||||
|
"target_uri": req,
|
||||||
|
}).Infof("%s out-of-band blocking rules on %s (%d rules) [%v]", evt.Parsed["source_ip"], req, len(evt.Appsec.MatchedRules), evt.Appsec.GetRuleIDs())
|
||||||
|
} else {
|
||||||
|
logger.WithFields(log.Fields{
|
||||||
|
"module": "appsec",
|
||||||
|
"source": evt.Parsed["source_ip"],
|
||||||
|
"target_uri": req,
|
||||||
|
}).Debugf("%s triggered non-blocking rules on %s (%d rules) [%v]", evt.Parsed["source_ip"], req, len(evt.Appsec.MatchedRules), evt.Appsec.GetRuleIDs())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AppsecRunner) AccumulateTxToEvent(evt *types.Event, req *appsec.ParsedRequest) error {
|
||||||
|
|
||||||
|
if evt == nil {
|
||||||
|
//an error was already emitted, let's not spam the logs
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !req.Tx.IsInterrupted() {
|
||||||
|
//if the phase didn't generate an interruption, we don't have anything to add to the event
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
//if one interruption was generated, event is good for processing :)
|
||||||
|
evt.Process = true
|
||||||
|
|
||||||
|
if evt.Meta == nil {
|
||||||
|
evt.Meta = map[string]string{}
|
||||||
|
}
|
||||||
|
if evt.Parsed == nil {
|
||||||
|
evt.Parsed = map[string]string{}
|
||||||
|
}
|
||||||
|
if req.IsInBand {
|
||||||
|
evt.Meta["appsec_interrupted"] = "true"
|
||||||
|
evt.Meta["appsec_action"] = req.Tx.Interruption().Action
|
||||||
|
evt.Parsed["inband_interrupted"] = "true"
|
||||||
|
evt.Parsed["inband_action"] = req.Tx.Interruption().Action
|
||||||
|
} else {
|
||||||
|
evt.Parsed["outofband_interrupted"] = "true"
|
||||||
|
evt.Parsed["outofband_action"] = req.Tx.Interruption().Action
|
||||||
|
}
|
||||||
|
|
||||||
|
if evt.Appsec.Vars == nil {
|
||||||
|
evt.Appsec.Vars = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Tx.Variables().All(func(v variables.RuleVariable, col collection.Collection) bool {
|
||||||
|
for _, variable := range col.FindAll() {
|
||||||
|
key := ""
|
||||||
|
if variable.Key() == "" {
|
||||||
|
key = variable.Variable().Name()
|
||||||
|
} else {
|
||||||
|
key = variable.Variable().Name() + "." + variable.Key()
|
||||||
|
}
|
||||||
|
if variable.Value() == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, collectionToKeep := range r.AppsecRuntime.CompiledVariablesTracking {
|
||||||
|
match := collectionToKeep.MatchString(key)
|
||||||
|
if match {
|
||||||
|
evt.Appsec.Vars[key] = variable.Value()
|
||||||
|
r.logger.Debugf("%s.%s = %s", variable.Variable().Name(), variable.Key(), variable.Value())
|
||||||
|
} else {
|
||||||
|
r.logger.Debugf("%s.%s != %s (%s) (not kept)", variable.Variable().Name(), variable.Key(), collectionToKeep, variable.Value())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, rule := range req.Tx.MatchedRules() {
|
||||||
|
if rule.Message() == "" {
|
||||||
|
r.logger.Tracef("discarding rule %d", rule.Rule().ID())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kind := "outofband"
|
||||||
|
if req.IsInBand {
|
||||||
|
kind = "inband"
|
||||||
|
evt.Appsec.HasInBandMatches = true
|
||||||
|
} else {
|
||||||
|
evt.Appsec.HasOutBandMatches = true
|
||||||
|
}
|
||||||
|
|
||||||
|
name := ""
|
||||||
|
version := ""
|
||||||
|
hash := ""
|
||||||
|
ruleNameProm := fmt.Sprintf("%d", rule.Rule().ID())
|
||||||
|
|
||||||
|
if details, ok := appsec.AppsecRulesDetails[rule.Rule().ID()]; ok {
|
||||||
|
//Only set them for custom rules, not for rules written in seclang
|
||||||
|
name = details.Name
|
||||||
|
version = details.Version
|
||||||
|
hash = details.Hash
|
||||||
|
ruleNameProm = details.Name
|
||||||
|
r.logger.Debugf("custom rule for event, setting name: %s, version: %s, hash: %s", name, version, hash)
|
||||||
|
} else {
|
||||||
|
name = fmt.Sprintf("native_rule:%d", rule.Rule().ID())
|
||||||
|
}
|
||||||
|
|
||||||
|
AppsecRuleHits.With(prometheus.Labels{"rule_name": ruleNameProm, "type": kind, "source": req.RemoteAddrNormalized, "appsec_engine": req.AppsecEngine}).Inc()
|
||||||
|
|
||||||
|
matchedZones := make([]string, 0)
|
||||||
|
for _, matchData := range rule.MatchedDatas() {
|
||||||
|
zone := matchData.Variable().Name()
|
||||||
|
varName := matchData.Key()
|
||||||
|
if varName != "" {
|
||||||
|
zone += "." + varName
|
||||||
|
}
|
||||||
|
matchedZones = append(matchedZones, zone)
|
||||||
|
}
|
||||||
|
|
||||||
|
corazaRule := map[string]interface{}{
|
||||||
|
"id": rule.Rule().ID(),
|
||||||
|
"uri": evt.Parsed["uri"],
|
||||||
|
"rule_type": kind,
|
||||||
|
"method": evt.Parsed["method"],
|
||||||
|
"disruptive": rule.Disruptive(),
|
||||||
|
"tags": rule.Rule().Tags(),
|
||||||
|
"file": rule.Rule().File(),
|
||||||
|
"file_line": rule.Rule().Line(),
|
||||||
|
"revision": rule.Rule().Revision(),
|
||||||
|
"secmark": rule.Rule().SecMark(),
|
||||||
|
"accuracy": rule.Rule().Accuracy(),
|
||||||
|
"msg": rule.Message(),
|
||||||
|
"severity": rule.Rule().Severity().String(),
|
||||||
|
"name": name,
|
||||||
|
"hash": hash,
|
||||||
|
"version": version,
|
||||||
|
"matched_zones": matchedZones,
|
||||||
|
}
|
||||||
|
evt.Appsec.MatchedRules = append(evt.Appsec.MatchedRules, corazaRule)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
579
pkg/appsec/appsec.go
Normal file
579
pkg/appsec/appsec.go
Normal file
|
@ -0,0 +1,579 @@
|
||||||
|
package appsec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/antonmedv/expr"
|
||||||
|
"github.com/antonmedv/expr/vm"
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Hook struct {
|
||||||
|
Filter string `yaml:"filter"`
|
||||||
|
FilterExpr *vm.Program `yaml:"-"`
|
||||||
|
|
||||||
|
OnSuccess string `yaml:"on_success"`
|
||||||
|
Apply []string `yaml:"apply"`
|
||||||
|
ApplyExpr []*vm.Program `yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
hookOnLoad = iota
|
||||||
|
hookPreEval
|
||||||
|
hookPostEval
|
||||||
|
hookOnMatch
|
||||||
|
)
|
||||||
|
|
||||||
|
// @tko : todo - debug mode
|
||||||
|
func (h *Hook) Build(hookStage int) error {
|
||||||
|
|
||||||
|
ctx := map[string]interface{}{}
|
||||||
|
switch hookStage {
|
||||||
|
case hookOnLoad:
|
||||||
|
ctx = GetOnLoadEnv(&AppsecRuntimeConfig{})
|
||||||
|
case hookPreEval:
|
||||||
|
ctx = GetPreEvalEnv(&AppsecRuntimeConfig{}, &ParsedRequest{})
|
||||||
|
case hookPostEval:
|
||||||
|
ctx = GetPostEvalEnv(&AppsecRuntimeConfig{}, &ParsedRequest{})
|
||||||
|
case hookOnMatch:
|
||||||
|
ctx = GetOnMatchEnv(&AppsecRuntimeConfig{}, &ParsedRequest{}, types.Event{})
|
||||||
|
}
|
||||||
|
opts := exprhelpers.GetExprOptions(ctx)
|
||||||
|
if h.Filter != "" {
|
||||||
|
program, err := expr.Compile(h.Filter, opts...) //FIXME: opts
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to compile filter %s : %w", h.Filter, err)
|
||||||
|
}
|
||||||
|
h.FilterExpr = program
|
||||||
|
}
|
||||||
|
for _, apply := range h.Apply {
|
||||||
|
program, err := expr.Compile(apply, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to compile apply %s : %w", apply, err)
|
||||||
|
}
|
||||||
|
h.ApplyExpr = append(h.ApplyExpr, program)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppsecTempResponse struct {
|
||||||
|
InBandInterrupt bool
|
||||||
|
OutOfBandInterrupt bool
|
||||||
|
Action string //allow, deny, captcha, log
|
||||||
|
HTTPResponseCode int
|
||||||
|
SendEvent bool //do we send an internal event on rule match
|
||||||
|
SendAlert bool //do we send an alert on rule match
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppsecSubEngineOpts struct {
|
||||||
|
DisableBodyInspection bool `yaml:"disable_body_inspection"`
|
||||||
|
RequestBodyInMemoryLimit *int `yaml:"request_body_in_memory_limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// runtime version of AppsecConfig
|
||||||
|
type AppsecRuntimeConfig struct {
|
||||||
|
Name string
|
||||||
|
OutOfBandRules []AppsecCollection
|
||||||
|
|
||||||
|
InBandRules []AppsecCollection
|
||||||
|
|
||||||
|
DefaultRemediation string
|
||||||
|
RemediationByTag map[string]string //Also used for ByName, as the name (for modsec rules) is a tag crowdsec-NAME
|
||||||
|
RemediationById map[int]string
|
||||||
|
CompiledOnLoad []Hook
|
||||||
|
CompiledPreEval []Hook
|
||||||
|
CompiledPostEval []Hook
|
||||||
|
CompiledOnMatch []Hook
|
||||||
|
CompiledVariablesTracking []*regexp.Regexp
|
||||||
|
Config *AppsecConfig
|
||||||
|
//CorazaLogger debuglog.Logger
|
||||||
|
|
||||||
|
//those are ephemeral, created/destroyed with every req
|
||||||
|
OutOfBandTx ExtendedTransaction //is it a good idea ?
|
||||||
|
InBandTx ExtendedTransaction //is it a good idea ?
|
||||||
|
Response AppsecTempResponse
|
||||||
|
//should we store matched rules here ?
|
||||||
|
|
||||||
|
Logger *log.Entry
|
||||||
|
|
||||||
|
//Set by on_load to ignore some rules on loading
|
||||||
|
DisabledInBandRuleIds []int
|
||||||
|
DisabledInBandRulesTags []string //Also used for ByName, as the name (for modsec rules) is a tag crowdsec-NAME
|
||||||
|
|
||||||
|
DisabledOutOfBandRuleIds []int
|
||||||
|
DisabledOutOfBandRulesTags []string //Also used for ByName, as the name (for modsec rules) is a tag crowdsec-NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppsecConfig struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
OutOfBandRules []string `yaml:"outofband_rules"`
|
||||||
|
InBandRules []string `yaml:"inband_rules"`
|
||||||
|
DefaultRemediation string `yaml:"default_remediation"`
|
||||||
|
DefaultPassAction string `yaml:"default_pass_action"`
|
||||||
|
BlockedHTTPCode int `yaml:"blocked_http_code"`
|
||||||
|
PassedHTTPCode int `yaml:"passed_http_code"`
|
||||||
|
OnLoad []Hook `yaml:"on_load"`
|
||||||
|
PreEval []Hook `yaml:"pre_eval"`
|
||||||
|
PostEval []Hook `yaml:"post_eval"`
|
||||||
|
OnMatch []Hook `yaml:"on_match"`
|
||||||
|
VariablesTracking []string `yaml:"variables_tracking"`
|
||||||
|
InbandOptions AppsecSubEngineOpts `yaml:"inband_options"`
|
||||||
|
OutOfBandOptions AppsecSubEngineOpts `yaml:"outofband_options"`
|
||||||
|
|
||||||
|
LogLevel *log.Level `yaml:"log_level"`
|
||||||
|
Logger *log.Entry `yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) ClearResponse() {
|
||||||
|
log.Debugf("#-> %p", w)
|
||||||
|
w.Response = AppsecTempResponse{}
|
||||||
|
log.Debugf("-> %p", w.Config)
|
||||||
|
w.Response.Action = w.Config.DefaultPassAction
|
||||||
|
w.Response.HTTPResponseCode = w.Config.PassedHTTPCode
|
||||||
|
w.Response.SendEvent = true
|
||||||
|
w.Response.SendAlert = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wc *AppsecConfig) LoadByPath(file string) error {
|
||||||
|
|
||||||
|
wc.Logger.Debugf("loading config %s", file)
|
||||||
|
|
||||||
|
yamlFile, err := os.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to read file %s : %s", file, err)
|
||||||
|
}
|
||||||
|
err = yaml.UnmarshalStrict(yamlFile, wc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to parse yaml file %s : %s", file, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if wc.Name == "" {
|
||||||
|
return fmt.Errorf("name cannot be empty")
|
||||||
|
}
|
||||||
|
if wc.LogLevel == nil {
|
||||||
|
lvl := wc.Logger.Logger.GetLevel()
|
||||||
|
wc.LogLevel = &lvl
|
||||||
|
}
|
||||||
|
wc.Logger = wc.Logger.Dup().WithField("name", wc.Name)
|
||||||
|
wc.Logger.Logger.SetLevel(*wc.LogLevel)
|
||||||
|
if wc.DefaultRemediation == "" {
|
||||||
|
return fmt.Errorf("default_remediation cannot be empty")
|
||||||
|
}
|
||||||
|
switch wc.DefaultRemediation {
|
||||||
|
case "ban", "captcha", "log":
|
||||||
|
//those are the officially supported remediation(s)
|
||||||
|
default:
|
||||||
|
wc.Logger.Warningf("default '%s' remediation of %s is none of [ban,captcha,log] ensure bouncer compatbility!", wc.DefaultRemediation, file)
|
||||||
|
}
|
||||||
|
if wc.BlockedHTTPCode == 0 {
|
||||||
|
wc.BlockedHTTPCode = 403
|
||||||
|
}
|
||||||
|
if wc.PassedHTTPCode == 0 {
|
||||||
|
wc.PassedHTTPCode = 200
|
||||||
|
}
|
||||||
|
if wc.DefaultPassAction == "" {
|
||||||
|
wc.DefaultPassAction = "allow"
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wc *AppsecConfig) Load(configName string) error {
|
||||||
|
appsecConfigs := hub.GetItemMap(cwhub.APPSEC_CONFIGS)
|
||||||
|
|
||||||
|
for _, hubAppsecConfigItem := range appsecConfigs {
|
||||||
|
if !hubAppsecConfigItem.State.Installed {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if hubAppsecConfigItem.Name != configName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wc.Logger.Infof("loading %s", hubAppsecConfigItem.State.LocalPath)
|
||||||
|
err := wc.LoadByPath(hubAppsecConfigItem.State.LocalPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to load appsec-config %s : %s", hubAppsecConfigItem.State.LocalPath, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("no appsec-config found for %s", configName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wc *AppsecConfig) GetDataDir() string {
|
||||||
|
return hub.GetDataDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wc *AppsecConfig) Build() (*AppsecRuntimeConfig, error) {
|
||||||
|
ret := &AppsecRuntimeConfig{Logger: wc.Logger.WithField("component", "appsec_runtime_config")}
|
||||||
|
ret.Name = wc.Name
|
||||||
|
ret.Config = wc
|
||||||
|
ret.DefaultRemediation = wc.DefaultRemediation
|
||||||
|
|
||||||
|
wc.Logger.Tracef("Loading config %+v", wc)
|
||||||
|
//load rules
|
||||||
|
for _, rule := range wc.OutOfBandRules {
|
||||||
|
wc.Logger.Infof("loading outofband rule %s", rule)
|
||||||
|
collections, err := LoadCollection(rule, wc.Logger.WithField("component", "appsec_collection_loader"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to load outofband rule %s : %s", rule, err)
|
||||||
|
}
|
||||||
|
ret.OutOfBandRules = append(ret.OutOfBandRules, collections...)
|
||||||
|
}
|
||||||
|
|
||||||
|
wc.Logger.Infof("Loaded %d outofband rules", len(ret.OutOfBandRules))
|
||||||
|
for _, rule := range wc.InBandRules {
|
||||||
|
wc.Logger.Infof("loading inband rule %s", rule)
|
||||||
|
collections, err := LoadCollection(rule, wc.Logger.WithField("component", "appsec_collection_loader"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to load inband rule %s : %s", rule, err)
|
||||||
|
}
|
||||||
|
ret.InBandRules = append(ret.InBandRules, collections...)
|
||||||
|
}
|
||||||
|
|
||||||
|
wc.Logger.Infof("Loaded %d inband rules", len(ret.InBandRules))
|
||||||
|
|
||||||
|
//load hooks
|
||||||
|
for _, hook := range wc.OnLoad {
|
||||||
|
err := hook.Build(hookOnLoad)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to build on_load hook : %s", err)
|
||||||
|
}
|
||||||
|
ret.CompiledOnLoad = append(ret.CompiledOnLoad, hook)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, hook := range wc.PreEval {
|
||||||
|
err := hook.Build(hookPreEval)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to build pre_eval hook : %s", err)
|
||||||
|
}
|
||||||
|
ret.CompiledPreEval = append(ret.CompiledPreEval, hook)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, hook := range wc.PostEval {
|
||||||
|
err := hook.Build(hookPostEval)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to build post_eval hook : %s", err)
|
||||||
|
}
|
||||||
|
ret.CompiledPostEval = append(ret.CompiledPostEval, hook)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, hook := range wc.OnMatch {
|
||||||
|
err := hook.Build(hookOnMatch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to build on_match hook : %s", err)
|
||||||
|
}
|
||||||
|
ret.CompiledOnMatch = append(ret.CompiledOnMatch, hook)
|
||||||
|
}
|
||||||
|
|
||||||
|
//variable tracking
|
||||||
|
for _, variable := range wc.VariablesTracking {
|
||||||
|
compiledVariableRule, err := regexp.Compile(variable)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot compile variable regexp %s: %w", variable, err)
|
||||||
|
}
|
||||||
|
ret.CompiledVariablesTracking = append(ret.CompiledVariablesTracking, compiledVariableRule)
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) ProcessOnLoadRules() error {
|
||||||
|
for _, rule := range w.CompiledOnLoad {
|
||||||
|
if rule.FilterExpr != nil {
|
||||||
|
output, err := exprhelpers.Run(rule.FilterExpr, GetOnLoadEnv(w), w.Logger, w.Logger.Level >= log.DebugLevel)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to run appsec on_load filter %s : %w", rule.Filter, err)
|
||||||
|
}
|
||||||
|
switch t := output.(type) {
|
||||||
|
case bool:
|
||||||
|
if !t {
|
||||||
|
log.Debugf("filter didnt match")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.Errorf("Filter must return a boolean, can't filter")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, applyExpr := range rule.ApplyExpr {
|
||||||
|
_, err := exprhelpers.Run(applyExpr, GetOnLoadEnv(w), w.Logger, w.Logger.Level >= log.DebugLevel)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("unable to apply appsec on_load expr: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) ProcessOnMatchRules(request *ParsedRequest, evt types.Event) error {
|
||||||
|
|
||||||
|
for _, rule := range w.CompiledOnMatch {
|
||||||
|
if rule.FilterExpr != nil {
|
||||||
|
output, err := exprhelpers.Run(rule.FilterExpr, GetOnMatchEnv(w, request, evt), w.Logger, w.Logger.Level >= log.DebugLevel)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to run appsec on_match filter %s : %w", rule.Filter, err)
|
||||||
|
}
|
||||||
|
switch t := output.(type) {
|
||||||
|
case bool:
|
||||||
|
if !t {
|
||||||
|
log.Debugf("filter didnt match")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.Errorf("Filter must return a boolean, can't filter")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, applyExpr := range rule.ApplyExpr {
|
||||||
|
_, err := exprhelpers.Run(applyExpr, GetOnMatchEnv(w, request, evt), w.Logger, w.Logger.Level >= log.DebugLevel)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("unable to apply appsec on_match expr: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) ProcessPreEvalRules(request *ParsedRequest) error {
|
||||||
|
for _, rule := range w.CompiledPreEval {
|
||||||
|
if rule.FilterExpr != nil {
|
||||||
|
output, err := exprhelpers.Run(rule.FilterExpr, GetPreEvalEnv(w, request), w.Logger, w.Logger.Level >= log.DebugLevel)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to run appsec pre_eval filter %s : %w", rule.Filter, err)
|
||||||
|
}
|
||||||
|
switch t := output.(type) {
|
||||||
|
case bool:
|
||||||
|
if !t {
|
||||||
|
log.Debugf("filter didnt match")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.Errorf("Filter must return a boolean, can't filter")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// here means there is no filter or the filter matched
|
||||||
|
for _, applyExpr := range rule.ApplyExpr {
|
||||||
|
_, err := exprhelpers.Run(applyExpr, GetPreEvalEnv(w, request), w.Logger, w.Logger.Level >= log.DebugLevel)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("unable to apply appsec pre_eval expr: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) ProcessPostEvalRules(request *ParsedRequest) error {
|
||||||
|
for _, rule := range w.CompiledPostEval {
|
||||||
|
if rule.FilterExpr != nil {
|
||||||
|
output, err := exprhelpers.Run(rule.FilterExpr, GetPostEvalEnv(w, request), w.Logger, w.Logger.Level >= log.DebugLevel)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to run appsec post_eval filter %s : %w", rule.Filter, err)
|
||||||
|
}
|
||||||
|
switch t := output.(type) {
|
||||||
|
case bool:
|
||||||
|
if !t {
|
||||||
|
log.Debugf("filter didnt match")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.Errorf("Filter must return a boolean, can't filter")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// here means there is no filter or the filter matched
|
||||||
|
for _, applyExpr := range rule.ApplyExpr {
|
||||||
|
_, err := exprhelpers.Run(applyExpr, GetPostEvalEnv(w, request), w.Logger, w.Logger.Level >= log.DebugLevel)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("unable to apply appsec post_eval expr: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) RemoveInbandRuleByID(id int) error {
|
||||||
|
w.Logger.Debugf("removing inband rule %d", id)
|
||||||
|
return w.InBandTx.RemoveRuleByIDWithError(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) RemoveOutbandRuleByID(id int) error {
|
||||||
|
w.Logger.Debugf("removing outband rule %d", id)
|
||||||
|
return w.OutOfBandTx.RemoveRuleByIDWithError(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) RemoveInbandRuleByTag(tag string) error {
|
||||||
|
w.Logger.Debugf("removing inband rule with tag %s", tag)
|
||||||
|
return w.InBandTx.RemoveRuleByTagWithError(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) RemoveOutbandRuleByTag(tag string) error {
|
||||||
|
w.Logger.Debugf("removing outband rule with tag %s", tag)
|
||||||
|
return w.OutOfBandTx.RemoveRuleByTagWithError(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) RemoveInbandRuleByName(name string) error {
|
||||||
|
tag := fmt.Sprintf("crowdsec-%s", name)
|
||||||
|
w.Logger.Debugf("removing inband rule %s", tag)
|
||||||
|
return w.InBandTx.RemoveRuleByTagWithError(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) RemoveOutbandRuleByName(name string) error {
|
||||||
|
tag := fmt.Sprintf("crowdsec-%s", name)
|
||||||
|
w.Logger.Debugf("removing outband rule %s", tag)
|
||||||
|
return w.OutOfBandTx.RemoveRuleByTagWithError(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) CancelEvent() error {
|
||||||
|
w.Logger.Debugf("canceling event")
|
||||||
|
w.Response.SendEvent = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable a rule at load time, meaning it will not run for any request
|
||||||
|
func (w *AppsecRuntimeConfig) DisableInBandRuleByID(id int) error {
|
||||||
|
w.DisabledInBandRuleIds = append(w.DisabledInBandRuleIds, id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable a rule at load time, meaning it will not run for any request
|
||||||
|
func (w *AppsecRuntimeConfig) DisableInBandRuleByName(name string) error {
|
||||||
|
tagValue := fmt.Sprintf("crowdsec-%s", name)
|
||||||
|
w.DisabledInBandRulesTags = append(w.DisabledInBandRulesTags, tagValue)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable a rule at load time, meaning it will not run for any request
|
||||||
|
func (w *AppsecRuntimeConfig) DisableInBandRuleByTag(tag string) error {
|
||||||
|
w.DisabledInBandRulesTags = append(w.DisabledInBandRulesTags, tag)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable a rule at load time, meaning it will not run for any request
|
||||||
|
func (w *AppsecRuntimeConfig) DisableOutBandRuleByID(id int) error {
|
||||||
|
w.DisabledOutOfBandRuleIds = append(w.DisabledOutOfBandRuleIds, id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable a rule at load time, meaning it will not run for any request
|
||||||
|
func (w *AppsecRuntimeConfig) DisableOutBandRuleByName(name string) error {
|
||||||
|
tagValue := fmt.Sprintf("crowdsec-%s", name)
|
||||||
|
w.DisabledOutOfBandRulesTags = append(w.DisabledOutOfBandRulesTags, tagValue)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable a rule at load time, meaning it will not run for any request
|
||||||
|
func (w *AppsecRuntimeConfig) DisableOutBandRuleByTag(tag string) error {
|
||||||
|
w.DisabledOutOfBandRulesTags = append(w.DisabledOutOfBandRulesTags, tag)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) SendEvent() error {
|
||||||
|
w.Logger.Debugf("sending event")
|
||||||
|
w.Response.SendEvent = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) SendAlert() error {
|
||||||
|
w.Logger.Debugf("sending alert")
|
||||||
|
w.Response.SendAlert = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) CancelAlert() error {
|
||||||
|
w.Logger.Debugf("canceling alert")
|
||||||
|
w.Response.SendAlert = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) SetActionByTag(tag string, action string) error {
|
||||||
|
if w.RemediationByTag == nil {
|
||||||
|
w.RemediationByTag = make(map[string]string)
|
||||||
|
}
|
||||||
|
w.Logger.Debugf("setting action of %s to %s", tag, action)
|
||||||
|
w.RemediationByTag[tag] = action
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) SetActionByID(id int, action string) error {
|
||||||
|
if w.RemediationById == nil {
|
||||||
|
w.RemediationById = make(map[int]string)
|
||||||
|
}
|
||||||
|
w.Logger.Debugf("setting action of %d to %s", id, action)
|
||||||
|
w.RemediationById[id] = action
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) SetActionByName(name string, action string) error {
|
||||||
|
if w.RemediationByTag == nil {
|
||||||
|
w.RemediationByTag = make(map[string]string)
|
||||||
|
}
|
||||||
|
tag := fmt.Sprintf("crowdsec-%s", name)
|
||||||
|
w.Logger.Debugf("setting action of %s to %s", tag, action)
|
||||||
|
w.RemediationByTag[tag] = action
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) SetAction(action string) error {
|
||||||
|
//log.Infof("setting to %s", action)
|
||||||
|
w.Logger.Debugf("setting action to %s", action)
|
||||||
|
switch action {
|
||||||
|
case "allow":
|
||||||
|
w.Response.Action = action
|
||||||
|
w.Response.HTTPResponseCode = w.Config.PassedHTTPCode
|
||||||
|
//@tko how should we handle this ? it seems bouncer only understand bans, but it might be misleading ?
|
||||||
|
case "deny", "ban", "block":
|
||||||
|
w.Response.Action = "ban"
|
||||||
|
case "log":
|
||||||
|
w.Response.Action = action
|
||||||
|
w.Response.HTTPResponseCode = w.Config.PassedHTTPCode
|
||||||
|
case "captcha":
|
||||||
|
w.Response.Action = action
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown action %s", action)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) SetHTTPCode(code int) error {
|
||||||
|
w.Logger.Debugf("setting http code to %d", code)
|
||||||
|
w.Response.HTTPResponseCode = code
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type BodyResponse struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
HTTPStatus int `json:"http_status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *AppsecRuntimeConfig) GenerateResponse(response AppsecTempResponse, logger *log.Entry) BodyResponse {
|
||||||
|
resp := BodyResponse{}
|
||||||
|
//if there is no interrupt, we should allow with default code
|
||||||
|
if !response.InBandInterrupt {
|
||||||
|
resp.Action = w.Config.DefaultPassAction
|
||||||
|
resp.HTTPStatus = w.Config.PassedHTTPCode
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
resp.Action = response.Action
|
||||||
|
if resp.Action == "" {
|
||||||
|
resp.Action = w.Config.DefaultRemediation
|
||||||
|
}
|
||||||
|
logger.Debugf("action is %s", resp.Action)
|
||||||
|
|
||||||
|
resp.HTTPStatus = response.HTTPResponseCode
|
||||||
|
if resp.HTTPStatus == 0 {
|
||||||
|
resp.HTTPStatus = w.Config.BlockedHTTPCode
|
||||||
|
}
|
||||||
|
logger.Debugf("http status is %d", resp.HTTPStatus)
|
||||||
|
return resp
|
||||||
|
}
|
67
pkg/appsec/appsec_rule/appsec_rule.go
Normal file
67
pkg/appsec/appsec_rule/appsec_rule.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package appsec_rule
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
rules:
|
||||||
|
- name: "test"
|
||||||
|
and:
|
||||||
|
- zones:
|
||||||
|
- BODY_ARGS
|
||||||
|
variables:
|
||||||
|
- foo
|
||||||
|
- bar
|
||||||
|
transform:
|
||||||
|
- lowercase|uppercase|b64decode|...
|
||||||
|
match:
|
||||||
|
type: regex
|
||||||
|
value: "[^a-zA-Z]"
|
||||||
|
- zones:
|
||||||
|
- ARGS
|
||||||
|
variables:
|
||||||
|
- bla
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
type match struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
Value string `yaml:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomRule struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
|
||||||
|
Zones []string `yaml:"zones"`
|
||||||
|
Variables []string `yaml:"variables"`
|
||||||
|
|
||||||
|
Match match `yaml:"match"`
|
||||||
|
Transform []string `yaml:"transform"` //t:lowercase, t:uppercase, etc
|
||||||
|
And []CustomRule `yaml:"and,omitempty"`
|
||||||
|
Or []CustomRule `yaml:"or,omitempty"`
|
||||||
|
BodyType string `yaml:"body_type,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *CustomRule) Convert(ruleType string, appsecRuleName string) (string, []uint32, error) {
|
||||||
|
|
||||||
|
if v.Zones == nil && v.And == nil && v.Or == nil {
|
||||||
|
return "", nil, fmt.Errorf("no zones defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Match.Type == "" && v.And == nil && v.Or == nil {
|
||||||
|
return "", nil, fmt.Errorf("no match type defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Match.Value == "" && v.And == nil && v.Or == nil {
|
||||||
|
return "", nil, fmt.Errorf("no match value defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ruleType {
|
||||||
|
case ModsecurityRuleType:
|
||||||
|
r := ModsecurityRule{}
|
||||||
|
return r.Build(v, appsecRuleName)
|
||||||
|
default:
|
||||||
|
return "", nil, fmt.Errorf("unknown rule format '%s'", ruleType)
|
||||||
|
}
|
||||||
|
}
|
118
pkg/appsec/appsec_rule/modsec_rule_test.go
Normal file
118
pkg/appsec/appsec_rule/modsec_rule_test.go
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
package appsec_rule
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestVPatchRuleString(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
rule CustomRule
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Base Rule",
|
||||||
|
rule: CustomRule{
|
||||||
|
Zones: []string{"ARGS"},
|
||||||
|
Variables: []string{"foo"},
|
||||||
|
Match: match{Type: "regex", Value: "[^a-zA-Z]"},
|
||||||
|
Transform: []string{"lowercase"},
|
||||||
|
},
|
||||||
|
expected: `SecRule ARGS_GET:foo "@rx [^a-zA-Z]" "id:2203944045,phase:2,deny,log,msg:'Base Rule',tag:'crowdsec-Base Rule',t:lowercase"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple Zones",
|
||||||
|
rule: CustomRule{
|
||||||
|
Zones: []string{"ARGS", "BODY_ARGS"},
|
||||||
|
Variables: []string{"foo"},
|
||||||
|
Match: match{Type: "regex", Value: "[^a-zA-Z]"},
|
||||||
|
Transform: []string{"lowercase"},
|
||||||
|
},
|
||||||
|
expected: `SecRule ARGS_GET:foo|ARGS_POST:foo "@rx [^a-zA-Z]" "id:3387135861,phase:2,deny,log,msg:'Multiple Zones',tag:'crowdsec-Multiple Zones',t:lowercase"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Basic AND",
|
||||||
|
rule: CustomRule{
|
||||||
|
And: []CustomRule{
|
||||||
|
{
|
||||||
|
|
||||||
|
Zones: []string{"ARGS"},
|
||||||
|
Variables: []string{"foo"},
|
||||||
|
Match: match{Type: "regex", Value: "[^a-zA-Z]"},
|
||||||
|
Transform: []string{"lowercase"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Zones: []string{"ARGS"},
|
||||||
|
Variables: []string{"bar"},
|
||||||
|
Match: match{Type: "regex", Value: "[^a-zA-Z]"},
|
||||||
|
Transform: []string{"lowercase"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `SecRule ARGS_GET:foo "@rx [^a-zA-Z]" "id:4145519614,phase:2,deny,log,msg:'Basic AND',tag:'crowdsec-Basic AND',t:lowercase,chain"
|
||||||
|
SecRule ARGS_GET:bar "@rx [^a-zA-Z]" "id:1865217529,phase:2,deny,log,msg:'Basic AND',tag:'crowdsec-Basic AND',t:lowercase"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Basic OR",
|
||||||
|
rule: CustomRule{
|
||||||
|
Or: []CustomRule{
|
||||||
|
{
|
||||||
|
Zones: []string{"ARGS"},
|
||||||
|
Variables: []string{"foo"},
|
||||||
|
Match: match{Type: "regex", Value: "[^a-zA-Z]"},
|
||||||
|
Transform: []string{"lowercase"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Zones: []string{"ARGS"},
|
||||||
|
Variables: []string{"bar"},
|
||||||
|
Match: match{Type: "regex", Value: "[^a-zA-Z]"},
|
||||||
|
Transform: []string{"lowercase"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `SecRule ARGS_GET:foo "@rx [^a-zA-Z]" "id:651140804,phase:2,deny,log,msg:'Basic OR',tag:'crowdsec-Basic OR',t:lowercase,skip:1"
|
||||||
|
SecRule ARGS_GET:bar "@rx [^a-zA-Z]" "id:271441587,phase:2,deny,log,msg:'Basic OR',tag:'crowdsec-Basic OR',t:lowercase"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OR AND mix",
|
||||||
|
rule: CustomRule{
|
||||||
|
And: []CustomRule{
|
||||||
|
{
|
||||||
|
Zones: []string{"ARGS"},
|
||||||
|
Variables: []string{"foo"},
|
||||||
|
Match: match{Type: "regex", Value: "[^a-zA-Z]"},
|
||||||
|
Transform: []string{"lowercase"},
|
||||||
|
Or: []CustomRule{
|
||||||
|
{
|
||||||
|
Zones: []string{"ARGS"},
|
||||||
|
Variables: []string{"foo"},
|
||||||
|
Match: match{Type: "regex", Value: "[^a-zA-Z]"},
|
||||||
|
Transform: []string{"lowercase"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Zones: []string{"ARGS"},
|
||||||
|
Variables: []string{"bar"},
|
||||||
|
Match: match{Type: "regex", Value: "[^a-zA-Z]"},
|
||||||
|
Transform: []string{"lowercase"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `SecRule ARGS_GET:foo "@rx [^a-zA-Z]" "id:1714963250,phase:2,deny,log,msg:'OR AND mix',tag:'crowdsec-OR AND mix',t:lowercase,skip:1"
|
||||||
|
SecRule ARGS_GET:bar "@rx [^a-zA-Z]" "id:1519945803,phase:2,deny,log,msg:'OR AND mix',tag:'crowdsec-OR AND mix',t:lowercase"
|
||||||
|
SecRule ARGS_GET:foo "@rx [^a-zA-Z]" "id:1519945803,phase:2,deny,log,msg:'OR AND mix',tag:'crowdsec-OR AND mix',t:lowercase"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
actual, _, err := tt.rule.Convert(ModsecurityRuleType, tt.name)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error converting rule: %s", err)
|
||||||
|
}
|
||||||
|
if actual != tt.expected {
|
||||||
|
t.Errorf("Expected:\n%s\nGot:\n%s", tt.expected, actual)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
181
pkg/appsec/appsec_rule/modsecurity.go
Normal file
181
pkg/appsec/appsec_rule/modsecurity.go
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
package appsec_rule
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModsecurityRule struct {
|
||||||
|
ids []uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
var zonesMap map[string]string = map[string]string{
|
||||||
|
"ARGS": "ARGS_GET",
|
||||||
|
"ARGS_NAMES": "ARGS_GET_NAMES",
|
||||||
|
"BODY_ARGS": "ARGS_POST",
|
||||||
|
"BODY_ARGS_NAMES": "ARGS_POST_NAMES",
|
||||||
|
"HEADERS": "REQUEST_HEADERS",
|
||||||
|
"METHOD": "REQUEST_METHOD",
|
||||||
|
"PROTOCOL": "REQUEST_PROTOCOL",
|
||||||
|
"URI": "REQUEST_URI",
|
||||||
|
}
|
||||||
|
|
||||||
|
var transformMap map[string]string = map[string]string{
|
||||||
|
"lowercase": "t:lowercase",
|
||||||
|
"uppercase": "t:uppercase",
|
||||||
|
"b64decode": "t:base64Decode",
|
||||||
|
"hexdecode": "t:hexDecode",
|
||||||
|
"length": "t:length",
|
||||||
|
}
|
||||||
|
|
||||||
|
var matchMap map[string]string = map[string]string{
|
||||||
|
"regex": "@rx",
|
||||||
|
"equal": "@streq",
|
||||||
|
"startsWith": "@beginsWith",
|
||||||
|
"endsWith": "@endsWith",
|
||||||
|
"contains": "@contains",
|
||||||
|
"libinjectionSQL": "@detectSQLi",
|
||||||
|
"libinjectionXSS": "@detectXSS",
|
||||||
|
"gt": "@gt",
|
||||||
|
"lt": "@lt",
|
||||||
|
"ge": "@ge",
|
||||||
|
"le": "@le",
|
||||||
|
}
|
||||||
|
|
||||||
|
var bodyTypeMatch map[string]string = map[string]string{
|
||||||
|
"json": "JSON",
|
||||||
|
"xml": "XML",
|
||||||
|
"multipart": "MULTIPART",
|
||||||
|
"urlencoded": "URLENCODED",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ModsecurityRule) Build(rule *CustomRule, appsecRuleName string) (string, []uint32, error) {
|
||||||
|
|
||||||
|
rules, err := m.buildRules(rule, appsecRuleName, false, 0, 0)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//We return the id of the first generated rule, as it's the interesting one in case of chain or skip
|
||||||
|
return strings.Join(rules, "\n"), m.ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ModsecurityRule) generateRuleID(rule *CustomRule, appsecRuleName string, depth int) uint32 {
|
||||||
|
h := fnv.New32a()
|
||||||
|
h.Write([]byte(appsecRuleName))
|
||||||
|
h.Write([]byte(rule.Match.Type))
|
||||||
|
h.Write([]byte(rule.Match.Value))
|
||||||
|
h.Write([]byte(fmt.Sprintf("%d", depth)))
|
||||||
|
for _, zone := range rule.Zones {
|
||||||
|
h.Write([]byte(zone))
|
||||||
|
}
|
||||||
|
for _, transform := range rule.Transform {
|
||||||
|
h.Write([]byte(transform))
|
||||||
|
}
|
||||||
|
id := h.Sum32()
|
||||||
|
m.ids = append(m.ids, id)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ModsecurityRule) buildRules(rule *CustomRule, appsecRuleName string, and bool, toSkip int, depth int) ([]string, error) {
|
||||||
|
ret := make([]string, 0)
|
||||||
|
|
||||||
|
if len(rule.And) != 0 && len(rule.Or) != 0 {
|
||||||
|
return nil, fmt.Errorf("cannot have both 'and' and 'or' in the same rule")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.And != nil {
|
||||||
|
for c, andRule := range rule.And {
|
||||||
|
depth++
|
||||||
|
lastRule := c == len(rule.And)-1 // || len(rule.Or) == 0
|
||||||
|
rules, err := m.buildRules(&andRule, appsecRuleName, !lastRule, 0, depth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ret = append(ret, rules...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.Or != nil {
|
||||||
|
for c, orRule := range rule.Or {
|
||||||
|
depth++
|
||||||
|
skip := len(rule.Or) - c - 1
|
||||||
|
rules, err := m.buildRules(&orRule, appsecRuleName, false, skip, depth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ret = append(ret, rules...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r := strings.Builder{}
|
||||||
|
|
||||||
|
r.WriteString("SecRule ")
|
||||||
|
|
||||||
|
if rule.Zones == nil {
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, zone := range rule.Zones {
|
||||||
|
mappedZone, ok := zonesMap[zone]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unknown zone '%s'", zone)
|
||||||
|
}
|
||||||
|
if len(rule.Variables) == 0 {
|
||||||
|
r.WriteString(mappedZone)
|
||||||
|
} else {
|
||||||
|
for j, variable := range rule.Variables {
|
||||||
|
if idx > 0 || j > 0 {
|
||||||
|
r.WriteByte('|')
|
||||||
|
}
|
||||||
|
r.WriteString(fmt.Sprintf("%s:%s", mappedZone, variable))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.WriteByte(' ')
|
||||||
|
|
||||||
|
if rule.Match.Type != "" {
|
||||||
|
if match, ok := matchMap[rule.Match.Type]; ok {
|
||||||
|
r.WriteString(fmt.Sprintf(`"%s %s"`, match, rule.Match.Value))
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("unknown match type '%s'", rule.Match.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Should phase:2 be configurable?
|
||||||
|
r.WriteString(fmt.Sprintf(` "id:%d,phase:2,deny,log,msg:'%s',tag:'crowdsec-%s'`, m.generateRuleID(rule, appsecRuleName, depth), appsecRuleName, appsecRuleName))
|
||||||
|
|
||||||
|
if rule.Transform != nil {
|
||||||
|
for _, transform := range rule.Transform {
|
||||||
|
r.WriteByte(',')
|
||||||
|
if mappedTransform, ok := transformMap[transform]; ok {
|
||||||
|
r.WriteString(mappedTransform)
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("unknown transform '%s'", transform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.BodyType != "" {
|
||||||
|
if mappedBodyType, ok := bodyTypeMatch[rule.BodyType]; ok {
|
||||||
|
r.WriteString(fmt.Sprintf(",ctl:requestBodyProcessor=%s", mappedBodyType))
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("unknown body type '%s'", rule.BodyType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if and {
|
||||||
|
r.WriteString(",chain")
|
||||||
|
}
|
||||||
|
|
||||||
|
if toSkip > 0 {
|
||||||
|
r.WriteString(fmt.Sprintf(",skip:%d", toSkip))
|
||||||
|
}
|
||||||
|
|
||||||
|
r.WriteByte('"')
|
||||||
|
|
||||||
|
ret = append(ret, r.String())
|
||||||
|
return ret, nil
|
||||||
|
}
|
9
pkg/appsec/appsec_rule/types.go
Normal file
9
pkg/appsec/appsec_rule/types.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package appsec_rule
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModsecurityRuleType = "modsecurity"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SupportedTypes() []string {
|
||||||
|
return []string{ModsecurityRuleType}
|
||||||
|
}
|
144
pkg/appsec/appsec_rules_collection.go
Normal file
144
pkg/appsec/appsec_rules_collection.go
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
package appsec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/appsec/appsec_rule"
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AppsecCollection struct {
|
||||||
|
collectionName string
|
||||||
|
Rules []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var APPSEC_RULE = "appsec-rule"
|
||||||
|
|
||||||
|
// to be filled w/ seb update
|
||||||
|
type AppsecCollectionConfig struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Debug bool `yaml:"debug"`
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
SecLangFilesRules []string `yaml:"seclang_files_rules"`
|
||||||
|
SecLangRules []string `yaml:"seclang_rules"`
|
||||||
|
Rules []appsec_rule.CustomRule `yaml:"rules"`
|
||||||
|
|
||||||
|
Labels map[string]interface{} `yaml:"labels"` //Labels is K:V list aiming at providing context the overflow
|
||||||
|
|
||||||
|
Data interface{} `yaml:"data"` //Ignore it
|
||||||
|
hash string `yaml:"-"`
|
||||||
|
version string `yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RulesDetails struct {
|
||||||
|
LogLevel log.Level
|
||||||
|
Hash string
|
||||||
|
Version string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should it be a global ?
|
||||||
|
// Is using the id is a good idea ? might be too specific to coraza and not easily reusable
|
||||||
|
var AppsecRulesDetails = make(map[int]RulesDetails)
|
||||||
|
|
||||||
|
func LoadCollection(pattern string, logger *log.Entry) ([]AppsecCollection, error) {
|
||||||
|
ret := make([]AppsecCollection, 0)
|
||||||
|
|
||||||
|
for _, appsecRule := range appsecRules {
|
||||||
|
|
||||||
|
tmpMatch, err := exprhelpers.Match(pattern, appsecRule.Name)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("unable to match %s with %s : %s", appsecRule.Name, pattern, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
matched, ok := tmpMatch.(bool)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
logger.Errorf("unable to match %s with %s : %s", appsecRule.Name, pattern, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matched {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
appsecCol := AppsecCollection{
|
||||||
|
collectionName: appsecRule.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if appsecRule.SecLangFilesRules != nil {
|
||||||
|
for _, rulesFile := range appsecRule.SecLangFilesRules {
|
||||||
|
logger.Debugf("Adding rules from %s", rulesFile)
|
||||||
|
fullPath := filepath.Join(hub.GetDataDir(), rulesFile)
|
||||||
|
c, err := os.ReadFile(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("unable to read file %s : %s", rulesFile, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(c), "\n") {
|
||||||
|
if strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
appsecCol.Rules = append(appsecCol.Rules, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if appsecRule.SecLangRules != nil {
|
||||||
|
logger.Tracef("Adding inline rules %+v", appsecRule.SecLangRules)
|
||||||
|
appsecCol.Rules = append(appsecCol.Rules, appsecRule.SecLangRules...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if appsecRule.Rules != nil {
|
||||||
|
for _, rule := range appsecRule.Rules {
|
||||||
|
strRule, rulesId, err := rule.Convert(appsec_rule.ModsecurityRuleType, appsecRule.Name)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("unable to convert rule %s : %s", rule.Name, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
logger.Debugf("Adding rule %s", strRule)
|
||||||
|
appsecCol.Rules = append(appsecCol.Rules, strRule)
|
||||||
|
|
||||||
|
//We only take the first id, as it's the one of the "main" rule
|
||||||
|
if _, ok := AppsecRulesDetails[int(rulesId[0])]; !ok {
|
||||||
|
AppsecRulesDetails[int(rulesId[0])] = RulesDetails{
|
||||||
|
LogLevel: log.InfoLevel,
|
||||||
|
Hash: appsecRule.hash,
|
||||||
|
Version: appsecRule.version,
|
||||||
|
Name: appsecRule.Name,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Warnf("conflicting id %d for rule %s !", rulesId[0], rule.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range rulesId {
|
||||||
|
SetRuleDebug(int(id), appsecRule.Debug)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret = append(ret, appsecCol)
|
||||||
|
}
|
||||||
|
if len(ret) == 0 {
|
||||||
|
return nil, fmt.Errorf("no appsec-rules found for pattern %s", pattern)
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w AppsecCollection) String() string {
|
||||||
|
ret := ""
|
||||||
|
for _, rule := range w.Rules {
|
||||||
|
ret += rule + "\n"
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
194
pkg/appsec/coraza_logger.go
Normal file
194
pkg/appsec/coraza_logger.go
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
package appsec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
dbg "github.com/crowdsecurity/coraza/v3/debuglog"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DebugRules map[int]bool = map[int]bool{}
|
||||||
|
|
||||||
|
func SetRuleDebug(id int, debug bool) {
|
||||||
|
DebugRules[id] = debug
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRuleDebug(id int) bool {
|
||||||
|
if val, ok := DebugRules[id]; ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// type ContextField func(Event) Event
|
||||||
|
|
||||||
|
type crzLogEvent struct {
|
||||||
|
fields log.Fields
|
||||||
|
logger *log.Entry
|
||||||
|
muted bool
|
||||||
|
level log.Level
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *crzLogEvent) Msg(msg string) {
|
||||||
|
if e.muted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*this is a hack. As we want to have per-level rule debug but it's not allowed by coraza/modsec, if a rule ID is flagged to be in debug mode, the
|
||||||
|
.Int("rule_id", <ID>) call will set the log_level of the event to debug. However, given the logger is global to the appsec-runner,
|
||||||
|
we are switching forth and back the log level of the logger*/
|
||||||
|
oldLvl := e.logger.Logger.GetLevel()
|
||||||
|
|
||||||
|
if e.level != oldLvl {
|
||||||
|
e.logger.Logger.SetLevel(e.level)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.fields) == 0 {
|
||||||
|
e.logger.Log(e.level, msg)
|
||||||
|
} else {
|
||||||
|
e.logger.WithFields(e.fields).Log(e.level, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.level != oldLvl {
|
||||||
|
e.logger.Logger.SetLevel(oldLvl)
|
||||||
|
e.level = oldLvl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *crzLogEvent) Str(key, val string) dbg.Event {
|
||||||
|
if e.muted {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
e.fields[key] = val
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *crzLogEvent) Err(err error) dbg.Event {
|
||||||
|
if e.muted {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
e.fields["error"] = err
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *crzLogEvent) Bool(key string, b bool) dbg.Event {
|
||||||
|
if e.muted {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
e.fields[key] = b
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *crzLogEvent) Int(key string, i int) dbg.Event {
|
||||||
|
if e.muted {
|
||||||
|
//this allows us to have per-rule debug logging
|
||||||
|
if key == "rule_id" && GetRuleDebug(i) {
|
||||||
|
e.muted = false
|
||||||
|
e.fields = map[string]interface{}{}
|
||||||
|
e.level = log.DebugLevel
|
||||||
|
} else {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.fields[key] = i
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *crzLogEvent) Uint(key string, i uint) dbg.Event {
|
||||||
|
if e.muted {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
e.fields[key] = i
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *crzLogEvent) Stringer(key string, val fmt.Stringer) dbg.Event {
|
||||||
|
if e.muted {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
e.fields[key] = val
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e crzLogEvent) IsEnabled() bool {
|
||||||
|
return !e.muted
|
||||||
|
}
|
||||||
|
|
||||||
|
type crzLogger struct {
|
||||||
|
logger *log.Entry
|
||||||
|
defaultFields log.Fields
|
||||||
|
logLevel log.Level
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCrzLogger(logger *log.Entry) crzLogger {
|
||||||
|
return crzLogger{logger: logger, logLevel: logger.Logger.GetLevel()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c crzLogger) NewMutedEvt(lvl log.Level) dbg.Event {
|
||||||
|
return &crzLogEvent{muted: true, logger: c.logger, level: lvl}
|
||||||
|
}
|
||||||
|
func (c crzLogger) NewEvt(lvl log.Level) dbg.Event {
|
||||||
|
evt := &crzLogEvent{fields: map[string]interface{}{}, logger: c.logger, level: lvl}
|
||||||
|
if c.defaultFields != nil {
|
||||||
|
for k, v := range c.defaultFields {
|
||||||
|
evt.fields[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return evt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c crzLogger) WithOutput(w io.Writer) dbg.Logger {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c crzLogger) WithLevel(lvl dbg.Level) dbg.Logger {
|
||||||
|
c.logLevel = log.Level(lvl)
|
||||||
|
c.logger.Logger.SetLevel(c.logLevel)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c crzLogger) With(fs ...dbg.ContextField) dbg.Logger {
|
||||||
|
var e dbg.Event = c.NewEvt(c.logLevel)
|
||||||
|
for _, f := range fs {
|
||||||
|
e = f(e)
|
||||||
|
}
|
||||||
|
c.defaultFields = e.(*crzLogEvent).fields
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c crzLogger) Trace() dbg.Event {
|
||||||
|
if c.logLevel < log.TraceLevel {
|
||||||
|
return c.NewMutedEvt(log.TraceLevel)
|
||||||
|
}
|
||||||
|
return c.NewEvt(log.TraceLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c crzLogger) Debug() dbg.Event {
|
||||||
|
if c.logLevel < log.DebugLevel {
|
||||||
|
return c.NewMutedEvt(log.DebugLevel)
|
||||||
|
|
||||||
|
}
|
||||||
|
return c.NewEvt(log.DebugLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c crzLogger) Info() dbg.Event {
|
||||||
|
if c.logLevel < log.InfoLevel {
|
||||||
|
return c.NewMutedEvt(log.InfoLevel)
|
||||||
|
}
|
||||||
|
return c.NewEvt(log.InfoLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c crzLogger) Warn() dbg.Event {
|
||||||
|
if c.logLevel < log.WarnLevel {
|
||||||
|
return c.NewMutedEvt(log.WarnLevel)
|
||||||
|
}
|
||||||
|
return c.NewEvt(log.WarnLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c crzLogger) Error() dbg.Event {
|
||||||
|
if c.logLevel < log.ErrorLevel {
|
||||||
|
return c.NewMutedEvt(log.ErrorLevel)
|
||||||
|
}
|
||||||
|
return c.NewEvt(log.ErrorLevel)
|
||||||
|
}
|
52
pkg/appsec/loader.go
Normal file
52
pkg/appsec/loader.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package appsec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var appsecRules map[string]AppsecCollectionConfig = make(map[string]AppsecCollectionConfig) //FIXME: would probably be better to have a struct for this
|
||||||
|
|
||||||
|
var hub *cwhub.Hub //FIXME: this is a temporary hack to make the hub available in the package
|
||||||
|
|
||||||
|
func LoadAppsecRules(hubInstance *cwhub.Hub) error {
|
||||||
|
|
||||||
|
hub = hubInstance
|
||||||
|
|
||||||
|
for _, hubAppsecRuleItem := range hub.GetItemMap(cwhub.APPSEC_RULES) {
|
||||||
|
if !hubAppsecRuleItem.State.Installed {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(hubAppsecRuleItem.State.LocalPath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("unable to read file %s : %s", hubAppsecRuleItem.State.LocalPath, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var rule AppsecCollectionConfig
|
||||||
|
|
||||||
|
err = yaml.UnmarshalStrict(content, &rule)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("unable to unmarshal file %s : %s", hubAppsecRuleItem.State.LocalPath, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rule.hash = hubAppsecRuleItem.State.LocalHash
|
||||||
|
rule.version = hubAppsecRuleItem.Version
|
||||||
|
|
||||||
|
log.Infof("Adding %s to appsec rules", rule.Name)
|
||||||
|
|
||||||
|
appsecRules[rule.Name] = rule
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(appsecRules) == 0 {
|
||||||
|
log.Debugf("No appsec rules found")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
345
pkg/appsec/request.go
Normal file
345
pkg/appsec/request.go
Normal file
|
@ -0,0 +1,345 @@
|
||||||
|
package appsec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
URIHeaderName = "X-Crowdsec-Appsec-Uri"
|
||||||
|
VerbHeaderName = "X-Crowdsec-Appsec-Verb"
|
||||||
|
HostHeaderName = "X-Crowdsec-Appsec-Host"
|
||||||
|
IPHeaderName = "X-Crowdsec-Appsec-Ip"
|
||||||
|
APIKeyHeaderName = "X-Crowdsec-Appsec-Api-Key"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ParsedRequest struct {
|
||||||
|
RemoteAddr string `json:"remote_addr,omitempty"`
|
||||||
|
Host string `json:"host,omitempty"`
|
||||||
|
ClientIP string `json:"client_ip,omitempty"`
|
||||||
|
URI string `json:"uri,omitempty"`
|
||||||
|
Args url.Values `json:"args,omitempty"`
|
||||||
|
ClientHost string `json:"client_host,omitempty"`
|
||||||
|
Headers http.Header `json:"headers,omitempty"`
|
||||||
|
URL *url.URL `json:"url,omitempty"`
|
||||||
|
Method string `json:"method,omitempty"`
|
||||||
|
Proto string `json:"proto,omitempty"`
|
||||||
|
Body []byte `json:"body,omitempty"`
|
||||||
|
TransferEncoding []string `json:"transfer_encoding,omitempty"`
|
||||||
|
UUID string `json:"uuid,omitempty"`
|
||||||
|
Tx ExtendedTransaction `json:"transaction,omitempty"`
|
||||||
|
ResponseChannel chan AppsecTempResponse `json:"-"`
|
||||||
|
IsInBand bool `json:"-"`
|
||||||
|
IsOutBand bool `json:"-"`
|
||||||
|
AppsecEngine string `json:"appsec_engine,omitempty"`
|
||||||
|
RemoteAddrNormalized string `json:"normalized_remote_addr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReqDumpFilter struct {
|
||||||
|
req *ParsedRequest
|
||||||
|
HeadersContentFilters []string
|
||||||
|
HeadersNameFilters []string
|
||||||
|
HeadersDrop bool
|
||||||
|
|
||||||
|
BodyDrop bool
|
||||||
|
//BodyContentFilters []string TBD
|
||||||
|
|
||||||
|
ArgsContentFilters []string
|
||||||
|
ArgsNameFilters []string
|
||||||
|
ArgsDrop bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ParsedRequest) DumpRequest(params ...any) *ReqDumpFilter {
|
||||||
|
filter := ReqDumpFilter{}
|
||||||
|
filter.BodyDrop = true
|
||||||
|
filter.HeadersNameFilters = []string{"cookie", "authorization"}
|
||||||
|
filter.req = r
|
||||||
|
return &filter
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear filters
|
||||||
|
func (r *ReqDumpFilter) NoFilters() *ReqDumpFilter {
|
||||||
|
r2 := ReqDumpFilter{}
|
||||||
|
r2.req = r.req
|
||||||
|
return &r2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithEmptyHeadersFilters() *ReqDumpFilter {
|
||||||
|
r.HeadersContentFilters = []string{}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithHeadersContentFilters(filter string) *ReqDumpFilter {
|
||||||
|
r.HeadersContentFilters = append(r.HeadersContentFilters, filter)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithHeadersNameFilter(filter string) *ReqDumpFilter {
|
||||||
|
r.HeadersNameFilters = append(r.HeadersNameFilters, filter)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithNoHeaders() *ReqDumpFilter {
|
||||||
|
r.HeadersDrop = true
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithHeaders() *ReqDumpFilter {
|
||||||
|
r.HeadersDrop = false
|
||||||
|
r.HeadersNameFilters = []string{}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithBody() *ReqDumpFilter {
|
||||||
|
r.BodyDrop = false
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithNoBody() *ReqDumpFilter {
|
||||||
|
r.BodyDrop = true
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithEmptyArgsFilters() *ReqDumpFilter {
|
||||||
|
r.ArgsContentFilters = []string{}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithArgsContentFilters(filter string) *ReqDumpFilter {
|
||||||
|
r.ArgsContentFilters = append(r.ArgsContentFilters, filter)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithArgsNameFilter(filter string) *ReqDumpFilter {
|
||||||
|
r.ArgsNameFilters = append(r.ArgsNameFilters, filter)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) FilterBody(out *ParsedRequest) error {
|
||||||
|
if r.BodyDrop {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out.Body = r.req.Body
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) FilterArgs(out *ParsedRequest) error {
|
||||||
|
if r.ArgsDrop {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(r.ArgsContentFilters) == 0 && len(r.ArgsNameFilters) == 0 {
|
||||||
|
out.Args = r.req.Args
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out.Args = make(url.Values)
|
||||||
|
for k, vals := range r.req.Args {
|
||||||
|
reject := false
|
||||||
|
//exclude by match on name
|
||||||
|
for _, filter := range r.ArgsNameFilters {
|
||||||
|
ok, err := regexp.MatchString("(?i)"+filter, k)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("error while matching string '%s' with '%s': %s", filter, k, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
reject = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range vals {
|
||||||
|
//exclude by content
|
||||||
|
for _, filter := range r.ArgsContentFilters {
|
||||||
|
ok, err := regexp.MatchString("(?i)"+filter, v)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("error while matching string '%s' with '%s': %s", filter, v, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
reject = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//if it was not rejected, let's add it
|
||||||
|
if !reject {
|
||||||
|
out.Args[k] = vals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) FilterHeaders(out *ParsedRequest) error {
|
||||||
|
if r.HeadersDrop {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.HeadersContentFilters) == 0 && len(r.HeadersNameFilters) == 0 {
|
||||||
|
out.Headers = r.req.Headers
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out.Headers = make(http.Header)
|
||||||
|
for k, vals := range r.req.Headers {
|
||||||
|
reject := false
|
||||||
|
//exclude by match on name
|
||||||
|
for _, filter := range r.HeadersNameFilters {
|
||||||
|
ok, err := regexp.MatchString("(?i)"+filter, k)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("error while matching string '%s' with '%s': %s", filter, k, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
reject = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range vals {
|
||||||
|
//exclude by content
|
||||||
|
for _, filter := range r.HeadersContentFilters {
|
||||||
|
ok, err := regexp.MatchString("(?i)"+filter, v)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("error while matching string '%s' with '%s': %s", filter, v, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
reject = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//if it was not rejected, let's add it
|
||||||
|
if !reject {
|
||||||
|
out.Headers[k] = vals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) GetFilteredRequest() *ParsedRequest {
|
||||||
|
//if there are no filters, we return the original request
|
||||||
|
if len(r.HeadersContentFilters) == 0 &&
|
||||||
|
len(r.HeadersNameFilters) == 0 &&
|
||||||
|
len(r.ArgsContentFilters) == 0 &&
|
||||||
|
len(r.ArgsNameFilters) == 0 &&
|
||||||
|
!r.BodyDrop && !r.HeadersDrop && !r.ArgsDrop {
|
||||||
|
log.Warningf("no filters, returning original request")
|
||||||
|
return r.req
|
||||||
|
}
|
||||||
|
|
||||||
|
r2 := ParsedRequest{}
|
||||||
|
r.FilterHeaders(&r2)
|
||||||
|
r.FilterBody(&r2)
|
||||||
|
r.FilterArgs(&r2)
|
||||||
|
return &r2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) ToJSON() error {
|
||||||
|
fd, err := os.CreateTemp("/tmp/", "crowdsec_req_dump_*.json")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("while creating temp file: %w", err)
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
enc := json.NewEncoder(fd)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
|
||||||
|
req := r.GetFilteredRequest()
|
||||||
|
|
||||||
|
log.Warningf("dumping : %+v", req)
|
||||||
|
|
||||||
|
if err := enc.Encode(req); err != nil {
|
||||||
|
return fmt.Errorf("while encoding request: %w", err)
|
||||||
|
}
|
||||||
|
log.Warningf("request dumped to %s", fd.Name())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a ParsedRequest from a http.Request. ParsedRequest can be consumed by the App security Engine
|
||||||
|
func NewParsedRequestFromRequest(r *http.Request) (ParsedRequest, error) {
|
||||||
|
var err error
|
||||||
|
body := make([]byte, 0)
|
||||||
|
|
||||||
|
if r.Body != nil {
|
||||||
|
body, err = io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
return ParsedRequest{}, fmt.Errorf("unable to read body: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the real source of the request is set in 'x-client-ip'
|
||||||
|
clientIP := r.Header.Get(IPHeaderName)
|
||||||
|
if clientIP == "" {
|
||||||
|
return ParsedRequest{}, fmt.Errorf("missing '%s' header", IPHeaderName)
|
||||||
|
}
|
||||||
|
// the real target Host of the request is set in 'x-client-host'
|
||||||
|
clientHost := r.Header.Get(HostHeaderName)
|
||||||
|
if clientHost == "" {
|
||||||
|
return ParsedRequest{}, fmt.Errorf("missing '%s' header", HostHeaderName)
|
||||||
|
}
|
||||||
|
// the real URI of the request is set in 'x-client-uri'
|
||||||
|
clientURI := r.Header.Get(URIHeaderName)
|
||||||
|
if clientURI == "" {
|
||||||
|
return ParsedRequest{}, fmt.Errorf("missing '%s' header", URIHeaderName)
|
||||||
|
}
|
||||||
|
// the real VERB of the request is set in 'x-client-uri'
|
||||||
|
clientMethod := r.Header.Get(VerbHeaderName)
|
||||||
|
if clientMethod == "" {
|
||||||
|
return ParsedRequest{}, fmt.Errorf("missing '%s' header", VerbHeaderName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete those headers before coraza process the request
|
||||||
|
delete(r.Header, IPHeaderName)
|
||||||
|
delete(r.Header, HostHeaderName)
|
||||||
|
delete(r.Header, URIHeaderName)
|
||||||
|
delete(r.Header, VerbHeaderName)
|
||||||
|
|
||||||
|
parsedURL, err := url.Parse(clientURI)
|
||||||
|
if err != nil {
|
||||||
|
return ParsedRequest{}, fmt.Errorf("unable to parse url '%s': %s", clientURI, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteAddrNormalized := ""
|
||||||
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Invalid appsec remote IP source %v: %s", r.RemoteAddr, err.Error())
|
||||||
|
remoteAddrNormalized = r.RemoteAddr
|
||||||
|
} else {
|
||||||
|
ip := net.ParseIP(host)
|
||||||
|
if ip == nil {
|
||||||
|
log.Errorf("Invalid appsec remote IP address source %v: %s", r.RemoteAddr, err.Error())
|
||||||
|
remoteAddrNormalized = r.RemoteAddr
|
||||||
|
} else {
|
||||||
|
remoteAddrNormalized = ip.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParsedRequest{
|
||||||
|
RemoteAddr: r.RemoteAddr,
|
||||||
|
UUID: uuid.New().String(),
|
||||||
|
ClientHost: clientHost,
|
||||||
|
ClientIP: clientIP,
|
||||||
|
URI: parsedURL.Path,
|
||||||
|
Method: clientMethod,
|
||||||
|
Host: r.Host,
|
||||||
|
Headers: r.Header,
|
||||||
|
URL: r.URL,
|
||||||
|
Proto: r.Proto,
|
||||||
|
Body: body,
|
||||||
|
Args: parsedURL.Query(), //TODO: Check if there's not potential bypass as it excludes malformed args
|
||||||
|
TransferEncoding: r.TransferEncoding,
|
||||||
|
ResponseChannel: make(chan AppsecTempResponse),
|
||||||
|
RemoteAddrNormalized: remoteAddrNormalized,
|
||||||
|
}, nil
|
||||||
|
}
|
181
pkg/appsec/request_test.go
Normal file
181
pkg/appsec/request_test.go
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
package appsec
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestBodyDumper(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
req *ParsedRequest
|
||||||
|
expect *ParsedRequest
|
||||||
|
filter func(r *ReqDumpFilter) *ReqDumpFilter
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default filter (cookie+authorization stripped + no body)",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Body: []byte("yo some body"),
|
||||||
|
Headers: map[string][]string{"cookie": {"toto"}, "authorization": {"tata"}, "foo": {"bar", "baz"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Body: []byte{},
|
||||||
|
Headers: map[string][]string{"foo": {"bar", "baz"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "explicit empty filter",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Body: []byte("yo some body"),
|
||||||
|
Headers: map[string][]string{"cookie": {"toto"}, "authorization": {"tata"}, "foo": {"bar", "baz"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Body: []byte("yo some body"),
|
||||||
|
Headers: map[string][]string{"cookie": {"toto"}, "authorization": {"tata"}, "foo": {"bar", "baz"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.NoFilters()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "filter header",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Body: []byte{},
|
||||||
|
Headers: map[string][]string{"test1": {"toto"}, "test2": {"tata"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Body: []byte{},
|
||||||
|
Headers: map[string][]string{"test1": {"toto"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.WithNoBody().WithHeadersNameFilter("test2")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "filter header content",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Body: []byte{},
|
||||||
|
Headers: map[string][]string{"test1": {"toto"}, "test2": {"tata"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Body: []byte{},
|
||||||
|
Headers: map[string][]string{"test1": {"toto"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.WithHeadersContentFilters("tata")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with headers",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Body: []byte{},
|
||||||
|
Headers: map[string][]string{"cookie1": {"lol"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Body: []byte{},
|
||||||
|
Headers: map[string][]string{"cookie1": {"lol"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.WithHeaders()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "drop headers",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Body: []byte{},
|
||||||
|
Headers: map[string][]string{"toto": {"lol"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Body: []byte{},
|
||||||
|
Headers: map[string][]string{},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.WithNoHeaders()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with body",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Body: []byte("toto"),
|
||||||
|
Headers: map[string][]string{"toto": {"lol"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Body: []byte("toto"),
|
||||||
|
Headers: map[string][]string{"toto": {"lol"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.WithBody()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with empty args filter",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Args: map[string][]string{"toto": {"lol"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Args: map[string][]string{"toto": {"lol"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.WithEmptyArgsFilters()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with args name filter",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Args: map[string][]string{"toto": {"lol"}, "totolol": {"lol"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Args: map[string][]string{"totolol": {"lol"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.WithArgsNameFilter("toto")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WithEmptyHeadersFilters",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Args: map[string][]string{"cookie": {"lol"}, "totolol": {"lol"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Args: map[string][]string{"cookie": {"lol"}, "totolol": {"lol"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.WithEmptyHeadersFilters()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WithArgsContentFilters",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Args: map[string][]string{"test": {"lol"}, "test2": {"toto"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Args: map[string][]string{"test": {"lol"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.WithArgsContentFilters("toto")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, test := range tests {
|
||||||
|
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
orig_dr := test.req.DumpRequest()
|
||||||
|
result := test.filter(orig_dr).GetFilteredRequest()
|
||||||
|
|
||||||
|
if len(result.Body) != len(test.expect.Body) {
|
||||||
|
t.Fatalf("test %d (%s) failed, got %d, expected %d", idx, test.name, len(test.req.Body), len(test.expect.Body))
|
||||||
|
}
|
||||||
|
if len(result.Headers) != len(test.expect.Headers) {
|
||||||
|
t.Fatalf("test %d (%s) failed, got %d, expected %d", idx, test.name, len(test.req.Headers), len(test.expect.Headers))
|
||||||
|
}
|
||||||
|
for k, v := range result.Headers {
|
||||||
|
if len(v) != len(test.expect.Headers[k]) {
|
||||||
|
t.Fatalf("test %d (%s) failed, got %d, expected %d", idx, test.name, len(v), len(test.expect.Headers[k]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
93
pkg/appsec/tx.go
Normal file
93
pkg/appsec/tx.go
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
package appsec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/crowdsecurity/coraza/v3"
|
||||||
|
"github.com/crowdsecurity/coraza/v3/experimental"
|
||||||
|
"github.com/crowdsecurity/coraza/v3/experimental/plugins/plugintypes"
|
||||||
|
"github.com/crowdsecurity/coraza/v3/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExtendedTransaction struct {
|
||||||
|
Tx experimental.FullTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExtendedTransaction(engine coraza.WAF, uuid string) ExtendedTransaction {
|
||||||
|
inBoundTx := engine.NewTransactionWithID(uuid)
|
||||||
|
expTx := inBoundTx.(experimental.FullTransaction)
|
||||||
|
tx := NewTransaction(expTx)
|
||||||
|
return tx
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTransaction(tx experimental.FullTransaction) ExtendedTransaction {
|
||||||
|
return ExtendedTransaction{Tx: tx}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExtendedTransaction) RemoveRuleByIDWithError(id int) error {
|
||||||
|
t.Tx.RemoveRuleByID(id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExtendedTransaction) RemoveRuleByTagWithError(tag string) error {
|
||||||
|
t.Tx.RemoveRuleByTag(tag)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExtendedTransaction) IsRuleEngineOff() bool {
|
||||||
|
return t.Tx.IsRuleEngineOff()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExtendedTransaction) ProcessLogging() {
|
||||||
|
t.Tx.ProcessLogging()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExtendedTransaction) ProcessConnection(client string, cPort int, server string, sPort int) {
|
||||||
|
t.Tx.ProcessConnection(client, cPort, server, sPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExtendedTransaction) AddGetRequestArgument(name string, value string) {
|
||||||
|
t.Tx.AddGetRequestArgument(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExtendedTransaction) ProcessURI(uri string, method string, httpVersion string) {
|
||||||
|
t.Tx.ProcessURI(uri, method, httpVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExtendedTransaction) AddRequestHeader(name string, value string) {
|
||||||
|
t.Tx.AddRequestHeader(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExtendedTransaction) SetServerName(name string) {
|
||||||
|
t.Tx.SetServerName(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExtendedTransaction) ProcessRequestHeaders() *types.Interruption {
|
||||||
|
return t.Tx.ProcessRequestHeaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExtendedTransaction) ProcessRequestBody() (*types.Interruption, error) {
|
||||||
|
return t.Tx.ProcessRequestBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExtendedTransaction) WriteRequestBody(body []byte) (*types.Interruption, int, error) {
|
||||||
|
return t.Tx.WriteRequestBody(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExtendedTransaction) Interruption() *types.Interruption {
|
||||||
|
return t.Tx.Interruption()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExtendedTransaction) IsInterrupted() bool {
|
||||||
|
return t.Tx.IsInterrupted()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExtendedTransaction) Variables() plugintypes.TransactionVariables {
|
||||||
|
return t.Tx.Variables()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExtendedTransaction) MatchedRules() []types.MatchedRule {
|
||||||
|
return t.Tx.MatchedRules()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExtendedTransaction) ID() string {
|
||||||
|
return t.Tx.ID()
|
||||||
|
}
|
59
pkg/appsec/waf_helpers.go
Normal file
59
pkg/appsec/waf_helpers.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
package appsec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetOnLoadEnv(w *AppsecRuntimeConfig) map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"RemoveInBandRuleByID": w.DisableInBandRuleByID,
|
||||||
|
"RemoveInBandRuleByTag": w.DisableInBandRuleByTag,
|
||||||
|
"RemoveInBandRuleByName": w.DisableInBandRuleByName,
|
||||||
|
"RemoveOutBandRuleByID": w.DisableOutBandRuleByID,
|
||||||
|
"RemoveOutBandRuleByTag": w.DisableOutBandRuleByTag,
|
||||||
|
"RemoveOutBandRuleByName": w.DisableOutBandRuleByName,
|
||||||
|
"SetRemediationByTag": w.SetActionByTag,
|
||||||
|
"SetRemediationByID": w.SetActionByID,
|
||||||
|
"SetRemediationByName": w.SetActionByName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPreEvalEnv(w *AppsecRuntimeConfig, request *ParsedRequest) map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"IsInBand": request.IsInBand,
|
||||||
|
"IsOutBand": request.IsOutBand,
|
||||||
|
"RemoveInBandRuleByID": w.RemoveInbandRuleByID,
|
||||||
|
"RemoveInBandRuleByName": w.RemoveInbandRuleByName,
|
||||||
|
"RemoveInBandRuleByTag": w.RemoveInbandRuleByTag,
|
||||||
|
"RemoveOutBandRuleByID": w.RemoveOutbandRuleByID,
|
||||||
|
"RemoveOutBandRuleByTag": w.RemoveOutbandRuleByTag,
|
||||||
|
"RemoveOutBandRuleByName": w.RemoveOutbandRuleByName,
|
||||||
|
"SetRemediationByTag": w.SetActionByTag,
|
||||||
|
"SetRemediationByID": w.SetActionByID,
|
||||||
|
"SetRemediationByName": w.SetActionByName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPostEvalEnv(w *AppsecRuntimeConfig, request *ParsedRequest) map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"IsInBand": request.IsInBand,
|
||||||
|
"IsOutBand": request.IsOutBand,
|
||||||
|
"DumpRequest": request.DumpRequest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOnMatchEnv(w *AppsecRuntimeConfig, request *ParsedRequest, evt types.Event) map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"evt": evt,
|
||||||
|
"req": request,
|
||||||
|
"IsInBand": request.IsInBand,
|
||||||
|
"IsOutBand": request.IsOutBand,
|
||||||
|
"SetRemediation": w.SetAction,
|
||||||
|
"SetReturnCode": w.SetHTTPCode,
|
||||||
|
"CancelEvent": w.CancelEvent,
|
||||||
|
"SendEvent": w.SendEvent,
|
||||||
|
"CancelAlert": w.CancelAlert,
|
||||||
|
"SendAlert": w.SendAlert,
|
||||||
|
"DumpRequest": request.DumpRequest,
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,8 @@ var defaultConfigDir = "/etc/crowdsec"
|
||||||
// defaultDataDir is the base path to all data files, to be overridden in the Makefile */
|
// defaultDataDir is the base path to all data files, to be overridden in the Makefile */
|
||||||
var defaultDataDir = "/var/lib/crowdsec/data/"
|
var defaultDataDir = "/var/lib/crowdsec/data/"
|
||||||
|
|
||||||
|
var globalConfig = Config{}
|
||||||
|
|
||||||
// Config contains top-level defaults -> overridden by configuration file -> overridden by CLI flags
|
// Config contains top-level defaults -> overridden by configuration file -> overridden by CLI flags
|
||||||
type Config struct {
|
type Config struct {
|
||||||
//just a path to ourselves :p
|
//just a path to ourselves :p
|
||||||
|
@ -89,9 +91,15 @@ func NewConfig(configFile string, disableAgent bool, disableAPI bool, quiet bool
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
globalConfig = cfg
|
||||||
|
|
||||||
return &cfg, configData, nil
|
return &cfg, configData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetConfig() Config {
|
||||||
|
return globalConfig
|
||||||
|
}
|
||||||
|
|
||||||
func NewDefaultConfig() *Config {
|
func NewDefaultConfig() *Config {
|
||||||
logLevel := log.InfoLevel
|
logLevel := log.InfoLevel
|
||||||
commonCfg := CommonCfg{
|
commonCfg := CommonCfg{
|
||||||
|
|
|
@ -16,6 +16,8 @@ const (
|
||||||
PARSERS = "parsers"
|
PARSERS = "parsers"
|
||||||
POSTOVERFLOWS = "postoverflows"
|
POSTOVERFLOWS = "postoverflows"
|
||||||
SCENARIOS = "scenarios"
|
SCENARIOS = "scenarios"
|
||||||
|
APPSEC_CONFIGS = "appsec-configs"
|
||||||
|
APPSEC_RULES = "appsec-rules"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -27,7 +29,7 @@ const (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// The order is important, as it is used to range over sub-items in collections.
|
// The order is important, as it is used to range over sub-items in collections.
|
||||||
ItemTypes = []string{PARSERS, POSTOVERFLOWS, SCENARIOS, COLLECTIONS}
|
ItemTypes = []string{PARSERS, POSTOVERFLOWS, SCENARIOS, APPSEC_CONFIGS, APPSEC_RULES, COLLECTIONS}
|
||||||
)
|
)
|
||||||
|
|
||||||
type HubItems map[string]map[string]*Item
|
type HubItems map[string]map[string]*Item
|
||||||
|
@ -118,6 +120,8 @@ type Item struct {
|
||||||
PostOverflows []string `json:"postoverflows,omitempty" yaml:"postoverflows,omitempty"`
|
PostOverflows []string `json:"postoverflows,omitempty" yaml:"postoverflows,omitempty"`
|
||||||
Scenarios []string `json:"scenarios,omitempty" yaml:"scenarios,omitempty"`
|
Scenarios []string `json:"scenarios,omitempty" yaml:"scenarios,omitempty"`
|
||||||
Collections []string `json:"collections,omitempty" yaml:"collections,omitempty"`
|
Collections []string `json:"collections,omitempty" yaml:"collections,omitempty"`
|
||||||
|
AppsecConfigs []string `json:"appsec-configs,omitempty" yaml:"appsec-configs,omitempty"`
|
||||||
|
AppsecRules []string `json:"appsec-rules,omitempty" yaml:"appsec-rules,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// installPath returns the location of the symlink to the item in the hub, or the path of the item itself if it's local
|
// installPath returns the location of the symlink to the item in the hub, or the path of the item itself if it's local
|
||||||
|
@ -227,6 +231,24 @@ func (i *Item) SubItems() []*Item {
|
||||||
sub = append(sub, s)
|
sub = append(sub, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, name := range i.AppsecConfigs {
|
||||||
|
s := i.hub.GetItem(APPSEC_CONFIGS, name)
|
||||||
|
if s == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sub = append(sub, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range i.AppsecRules {
|
||||||
|
s := i.hub.GetItem(APPSEC_RULES, name)
|
||||||
|
if s == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sub = append(sub, s)
|
||||||
|
}
|
||||||
|
|
||||||
for _, name := range i.Collections {
|
for _, name := range i.Collections {
|
||||||
s := i.hub.GetItem(COLLECTIONS, name)
|
s := i.hub.GetItem(COLLECTIONS, name)
|
||||||
if s == nil {
|
if s == nil {
|
||||||
|
@ -262,6 +284,18 @@ func (i *Item) logMissingSubItems() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, subName := range i.AppsecConfigs {
|
||||||
|
if i.hub.GetItem(APPSEC_CONFIGS, subName) == nil {
|
||||||
|
log.Errorf("can't find %s in %s, required by %s", subName, APPSEC_CONFIGS, i.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, subName := range i.AppsecRules {
|
||||||
|
if i.hub.GetItem(APPSEC_RULES, subName) == nil {
|
||||||
|
log.Errorf("can't find %s in %s, required by %s", subName, APPSEC_RULES, i.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, subName := range i.Collections {
|
for _, subName := range i.Collections {
|
||||||
if i.hub.GetItem(COLLECTIONS, subName) == nil {
|
if i.hub.GetItem(COLLECTIONS, subName) == nil {
|
||||||
log.Errorf("can't find %s in %s, required by %s", subName, COLLECTIONS, i.Name)
|
log.Errorf("can't find %s in %s, required by %s", subName, COLLECTIONS, i.Name)
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -112,17 +113,15 @@ func (h *Hub) getItemFileInfo(path string) (*itemFileInfo, error) {
|
||||||
|
|
||||||
log.Tracef("stage:%s ftype:%s", ret.stage, ret.ftype)
|
log.Tracef("stage:%s ftype:%s", ret.stage, ret.ftype)
|
||||||
|
|
||||||
if ret.stage == SCENARIOS {
|
if ret.ftype != PARSERS && ret.ftype != POSTOVERFLOWS {
|
||||||
ret.ftype = SCENARIOS
|
if !slices.Contains(ItemTypes, ret.stage) {
|
||||||
ret.stage = ""
|
|
||||||
} else if ret.stage == COLLECTIONS {
|
|
||||||
ret.ftype = COLLECTIONS
|
|
||||||
ret.stage = ""
|
|
||||||
} else if ret.ftype != PARSERS && ret.ftype != POSTOVERFLOWS {
|
|
||||||
// it's a PARSER / POSTOVERFLOW with a stage
|
|
||||||
return nil, fmt.Errorf("unknown configuration type for file '%s'", path)
|
return nil, fmt.Errorf("unknown configuration type for file '%s'", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ret.ftype = ret.stage
|
||||||
|
ret.stage = ""
|
||||||
|
}
|
||||||
|
|
||||||
log.Tracef("CORRECTED [%s] by [%s] in stage [%s] of type [%s]", ret.fname, ret.fauthor, ret.stage, ret.ftype)
|
log.Tracef("CORRECTED [%s] by [%s] in stage [%s] of type [%s]", ret.fname, ret.fauthor, ret.stage, ret.ftype)
|
||||||
|
|
||||||
return ret, nil
|
return ret, nil
|
||||||
|
|
|
@ -20,6 +20,21 @@ var exprFuncs = []exprCustomFunc{
|
||||||
new(func(string) (*cticlient.SmokeItem, error)),
|
new(func(string) (*cticlient.SmokeItem, error)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Flatten",
|
||||||
|
function: Flatten,
|
||||||
|
signature: []interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Distinct",
|
||||||
|
function: Distinct,
|
||||||
|
signature: []interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "FlattenDistinct",
|
||||||
|
function: FlattenDistinct,
|
||||||
|
signature: []interface{}{},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Distance",
|
name: "Distance",
|
||||||
function: Distance,
|
function: Distance,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -176,6 +177,54 @@ func FileInit(fileFolder string, filename string, fileType string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expr helpers
|
||||||
|
|
||||||
|
func Distinct(params ...any) (any, error) {
|
||||||
|
|
||||||
|
if rt := reflect.TypeOf(params[0]).Kind(); rt != reflect.Slice && rt != reflect.Array {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
array := params[0].([]interface{})
|
||||||
|
if array == nil {
|
||||||
|
return []interface{}{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var exists map[any]bool = make(map[any]bool)
|
||||||
|
var ret []interface{} = make([]interface{}, 0)
|
||||||
|
|
||||||
|
for _, val := range array {
|
||||||
|
if _, ok := exists[val]; !ok {
|
||||||
|
exists[val] = true
|
||||||
|
ret = append(ret, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func FlattenDistinct(params ...any) (any, error) {
|
||||||
|
return Distinct(flatten(nil, reflect.ValueOf(params))) //nolint:asasalint
|
||||||
|
}
|
||||||
|
|
||||||
|
func Flatten(params ...any) (any, error) {
|
||||||
|
return flatten(nil, reflect.ValueOf(params)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func flatten(args []interface{}, v reflect.Value) []interface{} {
|
||||||
|
if v.Kind() == reflect.Interface {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Kind() == reflect.Array || v.Kind() == reflect.Slice {
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
args = flatten(args, v.Index(i))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args = append(args, v.Interface())
|
||||||
|
}
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
func existsInFileMaps(filename string, ftype string) (bool, error) {
|
func existsInFileMaps(filename string, ftype string) (bool, error) {
|
||||||
ok := false
|
ok := false
|
||||||
var err error
|
var err error
|
||||||
|
|
|
@ -7,9 +7,10 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
"github.com/crowdsecurity/crowdsec/pkg/appsec/appsec_rule"
|
||||||
|
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Coverage struct {
|
type Coverage struct {
|
||||||
|
@ -18,6 +19,65 @@ type Coverage struct {
|
||||||
PresentIn map[string]bool //poorman's set
|
PresentIn map[string]bool //poorman's set
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HubTest) GetAppsecCoverage() ([]Coverage, error) {
|
||||||
|
if len(h.HubIndex.GetItemMap(cwhub.APPSEC_RULES)) == 0 {
|
||||||
|
return nil, fmt.Errorf("no appsec rules in hub index")
|
||||||
|
}
|
||||||
|
|
||||||
|
// populate from hub, iterate in alphabetical order
|
||||||
|
pkeys := sortedMapKeys(h.HubIndex.GetItemMap(cwhub.APPSEC_RULES))
|
||||||
|
coverage := make([]Coverage, len(pkeys))
|
||||||
|
|
||||||
|
for i, name := range pkeys {
|
||||||
|
coverage[i] = Coverage{
|
||||||
|
Name: name,
|
||||||
|
TestsCount: 0,
|
||||||
|
PresentIn: make(map[string]bool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parser the expressions a-la-oneagain
|
||||||
|
appsecTestConfigs, err := filepath.Glob(".appsec-tests/*/config.yaml")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("while find appsec-tests config: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, appsecTestConfigPath := range appsecTestConfigs {
|
||||||
|
configFileData := &HubTestItemConfig{}
|
||||||
|
yamlFile, err := os.ReadFile(appsecTestConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("unable to open appsec test config file '%s': %s", appsecTestConfigPath, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = yaml.Unmarshal(yamlFile, configFileData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, appsecRulesFile := range configFileData.AppsecRules {
|
||||||
|
appsecRuleData := &appsec_rule.CustomRule{}
|
||||||
|
yamlFile, err := os.ReadFile(appsecRulesFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("unable to open appsec rule '%s': %s", appsecRulesFile, err)
|
||||||
|
}
|
||||||
|
err = yaml.Unmarshal(yamlFile, appsecRuleData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
appsecRuleName := appsecRuleData.Name
|
||||||
|
|
||||||
|
for idx, cov := range coverage {
|
||||||
|
if cov.Name == appsecRuleName {
|
||||||
|
coverage[idx].TestsCount++
|
||||||
|
coverage[idx].PresentIn[appsecTestConfigPath] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return coverage, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *HubTest) GetParsersCoverage() ([]Coverage, error) {
|
func (h *HubTest) GetParsersCoverage() ([]Coverage, error) {
|
||||||
if len(h.HubIndex.GetItemMap(cwhub.PARSERS)) == 0 {
|
if len(h.HubIndex.GetItemMap(cwhub.PARSERS)) == 0 {
|
||||||
return nil, fmt.Errorf("no parsers in hub index")
|
return nil, fmt.Errorf("no parsers in hub index")
|
||||||
|
@ -127,7 +187,6 @@ func (h *HubTest) GetScenariosCoverage() ([]Coverage, error) {
|
||||||
return nil, fmt.Errorf("while find scenario asserts : %s", err)
|
return nil, fmt.Errorf("while find scenario asserts : %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
for _, assert := range passerts {
|
for _, assert := range passerts {
|
||||||
file, err := os.Open(assert)
|
file, err := os.Open(assert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -14,11 +14,14 @@ type HubTest struct {
|
||||||
CrowdSecPath string
|
CrowdSecPath string
|
||||||
CscliPath string
|
CscliPath string
|
||||||
HubPath string
|
HubPath string
|
||||||
HubTestPath string
|
HubTestPath string //generic parser/scenario tests .tests
|
||||||
|
HubAppsecTestPath string //dir specific to appsec tests .appsec-tests
|
||||||
HubIndexFile string
|
HubIndexFile string
|
||||||
TemplateConfigPath string
|
TemplateConfigPath string
|
||||||
TemplateProfilePath string
|
TemplateProfilePath string
|
||||||
TemplateSimulationPath string
|
TemplateSimulationPath string
|
||||||
|
TemplateAcquisPath string
|
||||||
|
TemplateAppsecProfilePath string
|
||||||
HubIndex *cwhub.Hub
|
HubIndex *cwhub.Hub
|
||||||
Tests []*HubTestItem
|
Tests []*HubTestItem
|
||||||
}
|
}
|
||||||
|
@ -27,9 +30,11 @@ const (
|
||||||
templateConfigFile = "template_config.yaml"
|
templateConfigFile = "template_config.yaml"
|
||||||
templateSimulationFile = "template_simulation.yaml"
|
templateSimulationFile = "template_simulation.yaml"
|
||||||
templateProfileFile = "template_profiles.yaml"
|
templateProfileFile = "template_profiles.yaml"
|
||||||
|
templateAcquisFile = "template_acquis.yaml"
|
||||||
|
templateAppsecProfilePath = "template_appsec-profile.yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewHubTest(hubPath string, crowdsecPath string, cscliPath string) (HubTest, error) {
|
func NewHubTest(hubPath string, crowdsecPath string, cscliPath string, isAppsecTest bool) (HubTest, error) {
|
||||||
hubPath, err := filepath.Abs(hubPath)
|
hubPath, err := filepath.Abs(hubPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return HubTest{}, fmt.Errorf("can't get absolute path of hub: %+v", err)
|
return HubTest{}, fmt.Errorf("can't get absolute path of hub: %+v", err)
|
||||||
|
@ -39,9 +44,6 @@ func NewHubTest(hubPath string, crowdsecPath string, cscliPath string) (HubTest,
|
||||||
if _, err = os.Stat(hubPath); os.IsNotExist(err) {
|
if _, err = os.Stat(hubPath); os.IsNotExist(err) {
|
||||||
return HubTest{}, fmt.Errorf("path to hub '%s' doesn't exist, can't run", hubPath)
|
return HubTest{}, fmt.Errorf("path to hub '%s' doesn't exist, can't run", hubPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
HubTestPath := filepath.Join(hubPath, "./.tests/")
|
|
||||||
|
|
||||||
// we can't use hubtest without crowdsec binary
|
// we can't use hubtest without crowdsec binary
|
||||||
if _, err = exec.LookPath(crowdsecPath); err != nil {
|
if _, err = exec.LookPath(crowdsecPath); err != nil {
|
||||||
if _, err = os.Stat(crowdsecPath); os.IsNotExist(err) {
|
if _, err = os.Stat(crowdsecPath); os.IsNotExist(err) {
|
||||||
|
@ -56,6 +58,39 @@ func NewHubTest(hubPath string, crowdsecPath string, cscliPath string) (HubTest,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isAppsecTest {
|
||||||
|
HubTestPath := filepath.Join(hubPath, "./.appsec-tests/")
|
||||||
|
hubIndexFile := filepath.Join(hubPath, ".index.json")
|
||||||
|
|
||||||
|
local := &csconfig.LocalHubCfg{
|
||||||
|
HubDir: hubPath,
|
||||||
|
HubIndexFile: hubIndexFile,
|
||||||
|
InstallDir: HubTestPath,
|
||||||
|
InstallDataDir: HubTestPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
hub, err := cwhub.NewHub(local, nil, false)
|
||||||
|
if err != nil {
|
||||||
|
return HubTest{}, fmt.Errorf("unable to load hub: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return HubTest{
|
||||||
|
CrowdSecPath: crowdsecPath,
|
||||||
|
CscliPath: cscliPath,
|
||||||
|
HubPath: hubPath,
|
||||||
|
HubTestPath: HubTestPath,
|
||||||
|
HubIndexFile: hubIndexFile,
|
||||||
|
TemplateConfigPath: filepath.Join(HubTestPath, templateConfigFile),
|
||||||
|
TemplateProfilePath: filepath.Join(HubTestPath, templateProfileFile),
|
||||||
|
TemplateSimulationPath: filepath.Join(HubTestPath, templateSimulationFile),
|
||||||
|
TemplateAppsecProfilePath: filepath.Join(HubTestPath, templateAppsecProfilePath),
|
||||||
|
TemplateAcquisPath: filepath.Join(HubTestPath, templateAcquisFile),
|
||||||
|
HubIndex: hub,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
HubTestPath := filepath.Join(hubPath, "./.tests/")
|
||||||
|
|
||||||
hubIndexFile := filepath.Join(hubPath, ".index.json")
|
hubIndexFile := filepath.Join(hubPath, ".index.json")
|
||||||
|
|
||||||
local := &csconfig.LocalHubCfg{
|
local := &csconfig.LocalHubCfg{
|
||||||
|
@ -70,19 +105,15 @@ func NewHubTest(hubPath string, crowdsecPath string, cscliPath string) (HubTest,
|
||||||
return HubTest{}, fmt.Errorf("unable to load hub: %s", err)
|
return HubTest{}, fmt.Errorf("unable to load hub: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
templateConfigFilePath := filepath.Join(HubTestPath, templateConfigFile)
|
|
||||||
templateProfilePath := filepath.Join(HubTestPath, templateProfileFile)
|
|
||||||
templateSimulationPath := filepath.Join(HubTestPath, templateSimulationFile)
|
|
||||||
|
|
||||||
return HubTest{
|
return HubTest{
|
||||||
CrowdSecPath: crowdsecPath,
|
CrowdSecPath: crowdsecPath,
|
||||||
CscliPath: cscliPath,
|
CscliPath: cscliPath,
|
||||||
HubPath: hubPath,
|
HubPath: hubPath,
|
||||||
HubTestPath: HubTestPath,
|
HubTestPath: HubTestPath,
|
||||||
HubIndexFile: hubIndexFile,
|
HubIndexFile: hubIndexFile,
|
||||||
TemplateConfigPath: templateConfigFilePath,
|
TemplateConfigPath: filepath.Join(HubTestPath, templateConfigFile),
|
||||||
TemplateProfilePath: templateProfilePath,
|
TemplateProfilePath: filepath.Join(HubTestPath, templateProfileFile),
|
||||||
TemplateSimulationPath: templateSimulationPath,
|
TemplateSimulationPath: filepath.Join(HubTestPath, templateSimulationFile),
|
||||||
HubIndex: hub,
|
HubIndex: hub,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package hubtest
|
package hubtest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -16,14 +18,17 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type HubTestItemConfig struct {
|
type HubTestItemConfig struct {
|
||||||
Parsers []string `yaml:"parsers"`
|
Parsers []string `yaml:"parsers,omitempty"`
|
||||||
Scenarios []string `yaml:"scenarios"`
|
Scenarios []string `yaml:"scenarios,omitempty"`
|
||||||
PostOVerflows []string `yaml:"postoverflows"`
|
PostOverflows []string `yaml:"postoverflows,omitempty"`
|
||||||
LogFile string `yaml:"log_file"`
|
AppsecRules []string `yaml:"appsec-rules,omitempty"`
|
||||||
LogType string `yaml:"log_type"`
|
NucleiTemplate string `yaml:"nuclei_template,omitempty"`
|
||||||
Labels map[string]string `yaml:"labels"`
|
ExpectedNucleiFailure bool `yaml:"expect_failure,omitempty"`
|
||||||
IgnoreParsers bool `yaml:"ignore_parsers"` // if we test a scenario, we don't want to assert on Parser
|
LogFile string `yaml:"log_file,omitempty"`
|
||||||
OverrideStatics []parser.ExtraField `yaml:"override_statics"` //Allow to override statics. Executed before s00
|
LogType string `yaml:"log_type,omitempty"`
|
||||||
|
Labels map[string]string `yaml:"labels,omitempty"`
|
||||||
|
IgnoreParsers bool `yaml:"ignore_parsers,omitempty"` // if we test a scenario, we don't want to assert on Parser
|
||||||
|
OverrideStatics []parser.ExtraField `yaml:"override_statics,omitempty"` //Allow to override statics. Executed before s00
|
||||||
}
|
}
|
||||||
|
|
||||||
type HubTestItem struct {
|
type HubTestItem struct {
|
||||||
|
@ -40,6 +45,7 @@ type HubTestItem struct {
|
||||||
RuntimeConfigFilePath string
|
RuntimeConfigFilePath string
|
||||||
RuntimeProfileFilePath string
|
RuntimeProfileFilePath string
|
||||||
RuntimeSimulationFilePath string
|
RuntimeSimulationFilePath string
|
||||||
|
RuntimeAcquisFilePath string
|
||||||
RuntimeHubConfig *csconfig.LocalHubCfg
|
RuntimeHubConfig *csconfig.LocalHubCfg
|
||||||
|
|
||||||
ResultsPath string
|
ResultsPath string
|
||||||
|
@ -53,6 +59,8 @@ type HubTestItem struct {
|
||||||
TemplateConfigPath string
|
TemplateConfigPath string
|
||||||
TemplateProfilePath string
|
TemplateProfilePath string
|
||||||
TemplateSimulationPath string
|
TemplateSimulationPath string
|
||||||
|
TemplateAcquisPath string
|
||||||
|
TemplateAppsecProfilePath string
|
||||||
HubIndex *cwhub.Hub
|
HubIndex *cwhub.Hub
|
||||||
|
|
||||||
Config *HubTestItemConfig
|
Config *HubTestItemConfig
|
||||||
|
@ -75,6 +83,11 @@ const (
|
||||||
ScenarioResultFileName = "bucket-dump.yaml"
|
ScenarioResultFileName = "bucket-dump.yaml"
|
||||||
|
|
||||||
BucketPourResultFileName = "bucketpour-dump.yaml"
|
BucketPourResultFileName = "bucketpour-dump.yaml"
|
||||||
|
|
||||||
|
TestBouncerApiKey = "this_is_a_bad_password"
|
||||||
|
|
||||||
|
DefaultNucleiTarget = "http://127.0.0.1:80/"
|
||||||
|
DefaultAppsecHost = "127.0.0.1:4241"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewTest(name string, hubTest *HubTest) (*HubTestItem, error) {
|
func NewTest(name string, hubTest *HubTest) (*HubTestItem, error) {
|
||||||
|
@ -115,6 +128,7 @@ func NewTest(name string, hubTest *HubTest) (*HubTestItem, error) {
|
||||||
RuntimeConfigFilePath: filepath.Join(runtimeFolder, "config.yaml"),
|
RuntimeConfigFilePath: filepath.Join(runtimeFolder, "config.yaml"),
|
||||||
RuntimeProfileFilePath: filepath.Join(runtimeFolder, "profiles.yaml"),
|
RuntimeProfileFilePath: filepath.Join(runtimeFolder, "profiles.yaml"),
|
||||||
RuntimeSimulationFilePath: filepath.Join(runtimeFolder, "simulation.yaml"),
|
RuntimeSimulationFilePath: filepath.Join(runtimeFolder, "simulation.yaml"),
|
||||||
|
RuntimeAcquisFilePath: filepath.Join(runtimeFolder, "acquis.yaml"),
|
||||||
ResultsPath: resultPath,
|
ResultsPath: resultPath,
|
||||||
ParserResultFile: filepath.Join(resultPath, ParserResultFileName),
|
ParserResultFile: filepath.Join(resultPath, ParserResultFileName),
|
||||||
ScenarioResultFile: filepath.Join(resultPath, ScenarioResultFileName),
|
ScenarioResultFile: filepath.Join(resultPath, ScenarioResultFileName),
|
||||||
|
@ -132,6 +146,8 @@ func NewTest(name string, hubTest *HubTest) (*HubTestItem, error) {
|
||||||
TemplateConfigPath: hubTest.TemplateConfigPath,
|
TemplateConfigPath: hubTest.TemplateConfigPath,
|
||||||
TemplateProfilePath: hubTest.TemplateProfilePath,
|
TemplateProfilePath: hubTest.TemplateProfilePath,
|
||||||
TemplateSimulationPath: hubTest.TemplateSimulationPath,
|
TemplateSimulationPath: hubTest.TemplateSimulationPath,
|
||||||
|
TemplateAcquisPath: hubTest.TemplateAcquisPath,
|
||||||
|
TemplateAppsecProfilePath: hubTest.TemplateAppsecProfilePath,
|
||||||
HubIndex: hubTest.HubIndex,
|
HubIndex: hubTest.HubIndex,
|
||||||
ScenarioAssert: ScenarioAssert,
|
ScenarioAssert: ScenarioAssert,
|
||||||
ParserAssert: ParserAssert,
|
ParserAssert: ParserAssert,
|
||||||
|
@ -297,8 +313,81 @@ func (t *HubTestItem) InstallHub() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// install appsec-rules in runtime environment
|
||||||
|
for _, appsecrule := range t.Config.AppsecRules {
|
||||||
|
log.Infof("adding rule '%s'", appsecrule)
|
||||||
|
if appsecrule == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if hubAppsecRule, ok := t.HubIndex.GetItemMap(cwhub.APPSEC_RULES)[appsecrule]; ok {
|
||||||
|
appsecRuleSource, err := filepath.Abs(filepath.Join(t.HubPath, hubAppsecRule.RemotePath))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't get absolute path of '%s': %s", appsecRuleSource, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
appsecRuleFilename := filepath.Base(appsecRuleSource)
|
||||||
|
|
||||||
|
// runtime/hub/appsec-rules/author/appsec-rule
|
||||||
|
hubDirAppsecRuleDest := filepath.Join(t.RuntimeHubPath, filepath.Dir(hubAppsecRule.RemotePath))
|
||||||
|
|
||||||
|
// runtime/appsec-rules/
|
||||||
|
appsecRuleDirDest := fmt.Sprintf("%s/appsec-rules/", t.RuntimePath)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(hubDirAppsecRuleDest, os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("unable to create folder '%s': %s", hubDirAppsecRuleDest, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(appsecRuleDirDest, os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("unable to create folder '%s': %s", appsecRuleDirDest, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// runtime/hub/appsec-rules/crowdsecurity/rule.yaml
|
||||||
|
hubDirAppsecRulePath := filepath.Join(appsecRuleDirDest, appsecRuleFilename)
|
||||||
|
if err := Copy(appsecRuleSource, hubDirAppsecRulePath); err != nil {
|
||||||
|
return fmt.Errorf("unable to copy '%s' to '%s': %s", appsecRuleSource, hubDirAppsecRulePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// runtime/appsec-rules/rule.yaml
|
||||||
|
appsecRulePath := filepath.Join(appsecRuleDirDest, appsecRuleFilename)
|
||||||
|
if err := os.Symlink(hubDirAppsecRulePath, appsecRulePath); err != nil {
|
||||||
|
if !os.IsExist(err) {
|
||||||
|
return fmt.Errorf("unable to symlink appsec-rule '%s' to '%s': %s", hubDirAppsecRulePath, appsecRulePath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
customAppsecRuleExist := false
|
||||||
|
for _, customPath := range t.CustomItemsLocation {
|
||||||
|
// we check if its a custom appsec-rule
|
||||||
|
customAppsecRulePath := filepath.Join(customPath, appsecrule)
|
||||||
|
if _, err := os.Stat(customAppsecRulePath); os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
customAppsecRulePathSplit := strings.Split(customAppsecRulePath, "/")
|
||||||
|
customAppsecRuleName := customAppsecRulePathSplit[len(customAppsecRulePathSplit)-1]
|
||||||
|
|
||||||
|
appsecRuleDirDest := fmt.Sprintf("%s/appsec-rules/", t.RuntimePath)
|
||||||
|
if err := os.MkdirAll(appsecRuleDirDest, os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("unable to create folder '%s': %s", appsecRuleDirDest, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// runtime/appsec-rules/
|
||||||
|
customAppsecRuleDest := fmt.Sprintf("%s/appsec-rules/%s", t.RuntimePath, customAppsecRuleName)
|
||||||
|
// if path to postoverflow exist, copy it
|
||||||
|
if err := Copy(customAppsecRulePath, customAppsecRuleDest); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
customAppsecRuleExist = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !customAppsecRuleExist {
|
||||||
|
return fmt.Errorf("couldn't find custom appsec-rule '%s' in the following location: %+v", appsecrule, t.CustomItemsLocation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// install postoverflows in runtime environment
|
// install postoverflows in runtime environment
|
||||||
for _, postoverflow := range t.Config.PostOVerflows {
|
for _, postoverflow := range t.Config.PostOverflows {
|
||||||
if postoverflow == "" {
|
if postoverflow == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -449,16 +538,114 @@ func (t *HubTestItem) Clean() error {
|
||||||
return os.RemoveAll(t.RuntimePath)
|
return os.RemoveAll(t.RuntimePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *HubTestItem) Run() error {
|
func (t *HubTestItem) RunWithNucleiTemplate() error {
|
||||||
t.Success = false
|
|
||||||
t.ErrorsList = make([]string, 0)
|
|
||||||
|
|
||||||
testPath := filepath.Join(t.HubTestPath, t.Name)
|
testPath := filepath.Join(t.HubTestPath, t.Name)
|
||||||
if _, err := os.Stat(testPath); os.IsNotExist(err) {
|
if _, err := os.Stat(testPath); os.IsNotExist(err) {
|
||||||
return fmt.Errorf("test '%s' doesn't exist in '%s', exiting", t.Name, t.HubTestPath)
|
return fmt.Errorf("test '%s' doesn't exist in '%s', exiting", t.Name, t.HubTestPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentDir, err := os.Getwd()
|
if err := os.Chdir(testPath); err != nil {
|
||||||
|
return fmt.Errorf("can't 'cd' to '%s': %s", testPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//machine add
|
||||||
|
cmdArgs := []string{"-c", t.RuntimeConfigFilePath, "machines", "add", "testMachine", "--auto"}
|
||||||
|
cscliRegisterCmd := exec.Command(t.CscliPath, cmdArgs...)
|
||||||
|
|
||||||
|
output, err := cscliRegisterCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
if !strings.Contains(string(output), "unable to create machine: user 'testMachine': user already exist") {
|
||||||
|
fmt.Println(string(output))
|
||||||
|
return fmt.Errorf("fail to run '%s' for test '%s': %v", cscliRegisterCmd.String(), t.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//hardcode bouncer key
|
||||||
|
cmdArgs = []string{"-c", t.RuntimeConfigFilePath, "bouncers", "add", "appsectests", "-k", TestBouncerApiKey}
|
||||||
|
cscliBouncerCmd := exec.Command(t.CscliPath, cmdArgs...)
|
||||||
|
|
||||||
|
output, err = cscliBouncerCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
if !strings.Contains(string(output), "unable to create bouncer: bouncer appsectests already exists") {
|
||||||
|
fmt.Println(string(output))
|
||||||
|
return fmt.Errorf("fail to run '%s' for test '%s': %v", cscliRegisterCmd.String(), t.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//start crowdsec service
|
||||||
|
cmdArgs = []string{"-c", t.RuntimeConfigFilePath}
|
||||||
|
crowdsecDaemon := exec.Command(t.CrowdSecPath, cmdArgs...)
|
||||||
|
|
||||||
|
crowdsecDaemon.Start()
|
||||||
|
|
||||||
|
//wait for the appsec port to be available
|
||||||
|
if _, err := IsAlive(DefaultAppsecHost); err != nil {
|
||||||
|
return fmt.Errorf("appsec is down: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the target is available
|
||||||
|
nucleiTargetParsedURL, err := url.Parse(DefaultNucleiTarget)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to parse target '%s': %s", DefaultNucleiTarget, err)
|
||||||
|
}
|
||||||
|
nucleiTargetHost := nucleiTargetParsedURL.Host
|
||||||
|
if _, err := IsAlive(nucleiTargetHost); err != nil {
|
||||||
|
return fmt.Errorf("target is down: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nucleiConfig := NucleiConfig{
|
||||||
|
Path: "nuclei",
|
||||||
|
OutputDir: t.RuntimePath,
|
||||||
|
CmdLineOptions: []string{"-ev", //allow variables from environment
|
||||||
|
"-nc", //no colors in output
|
||||||
|
"-dresp", //dump response
|
||||||
|
"-j", //json output
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = nucleiConfig.RunNucleiTemplate(t.Name, t.Config.NucleiTemplate, DefaultNucleiTarget)
|
||||||
|
crowdsecLogFile := fmt.Sprintf("%s/log/crowdsec.log", nucleiConfig.OutputDir)
|
||||||
|
if t.Config.ExpectedNucleiFailure {
|
||||||
|
if err != nil && errors.Is(err, NucleiTemplateFail) {
|
||||||
|
log.Infof("Appsec test %s failed as expected", t.Name)
|
||||||
|
t.Success = true
|
||||||
|
} else {
|
||||||
|
log.Errorf("Appsec test %s failed: %s", t.Name, err)
|
||||||
|
crowdsecLog, err := os.ReadFile(crowdsecLogFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("unable to read crowdsec log file '%s': %s", crowdsecLogFile, err)
|
||||||
|
} else {
|
||||||
|
log.Errorf("crowdsec log file '%s'", crowdsecLogFile)
|
||||||
|
log.Errorf("%s\n", string(crowdsecLog))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err == nil {
|
||||||
|
log.Infof("Appsec test %s succeeded", t.Name)
|
||||||
|
t.Success = true
|
||||||
|
} else {
|
||||||
|
log.Errorf("Appsec test %s failed: %s", t.Name, err)
|
||||||
|
crowdsecLog, err := os.ReadFile(crowdsecLogFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("unable to read crowdsec log file '%s': %s", crowdsecLogFile, err)
|
||||||
|
} else {
|
||||||
|
log.Errorf("crowdsec log file '%s'", crowdsecLogFile)
|
||||||
|
log.Errorf("%s\n", string(crowdsecLog))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crowdsecDaemon.Process.Kill()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *HubTestItem) RunWithLogFile() error {
|
||||||
|
testPath := filepath.Join(t.HubTestPath, t.Name)
|
||||||
|
if _, err := os.Stat(testPath); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("test '%s' doesn't exist in '%s', exiting", t.Name, t.HubTestPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDir, err := os.Getwd() //xx
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("can't get current directory: %+v", err)
|
return fmt.Errorf("can't get current directory: %+v", err)
|
||||||
}
|
}
|
||||||
|
@ -650,3 +837,92 @@ func (t *HubTestItem) Run() error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *HubTestItem) Run() error {
|
||||||
|
var err error
|
||||||
|
t.Success = false
|
||||||
|
t.ErrorsList = make([]string, 0)
|
||||||
|
|
||||||
|
// create runtime folder
|
||||||
|
if err = os.MkdirAll(t.RuntimePath, os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("unable to create folder '%s': %+v", t.RuntimePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create runtime data folder
|
||||||
|
if err = os.MkdirAll(t.RuntimeDataPath, os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("unable to create folder '%s': %+v", t.RuntimeDataPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create runtime hub folder
|
||||||
|
if err = os.MkdirAll(t.RuntimeHubPath, os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("unable to create folder '%s': %+v", t.RuntimeHubPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = Copy(t.HubIndexFile, filepath.Join(t.RuntimeHubPath, ".index.json")); err != nil {
|
||||||
|
return fmt.Errorf("unable to copy .index.json file in '%s': %s", filepath.Join(t.RuntimeHubPath, ".index.json"), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create results folder
|
||||||
|
if err = os.MkdirAll(t.ResultsPath, os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("unable to create folder '%s': %+v", t.ResultsPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy template config file to runtime folder
|
||||||
|
if err = Copy(t.TemplateConfigPath, t.RuntimeConfigFilePath); err != nil {
|
||||||
|
return fmt.Errorf("unable to copy '%s' to '%s': %v", t.TemplateConfigPath, t.RuntimeConfigFilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy template profile file to runtime folder
|
||||||
|
if err = Copy(t.TemplateProfilePath, t.RuntimeProfileFilePath); err != nil {
|
||||||
|
return fmt.Errorf("unable to copy '%s' to '%s': %v", t.TemplateProfilePath, t.RuntimeProfileFilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy template simulation file to runtime folder
|
||||||
|
if err = Copy(t.TemplateSimulationPath, t.RuntimeSimulationFilePath); err != nil {
|
||||||
|
return fmt.Errorf("unable to copy '%s' to '%s': %v", t.TemplateSimulationPath, t.RuntimeSimulationFilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
crowdsecPatternsFolder := csconfig.DefaultConfigPath("patterns")
|
||||||
|
|
||||||
|
// copy template patterns folder to runtime folder
|
||||||
|
if err = CopyDir(crowdsecPatternsFolder, t.RuntimePatternsPath); err != nil {
|
||||||
|
return fmt.Errorf("unable to copy 'patterns' from '%s' to '%s': %s", crowdsecPatternsFolder, t.RuntimePatternsPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the appsec-configs dir
|
||||||
|
if err = os.MkdirAll(filepath.Join(t.RuntimePath, "appsec-configs"), os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("unable to create folder '%s': %+v", t.RuntimePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//if it's an appsec rule test, we need acquis and appsec profile
|
||||||
|
if len(t.Config.AppsecRules) > 0 {
|
||||||
|
// copy template acquis file to runtime folder
|
||||||
|
log.Infof("copying %s to %s", t.TemplateAcquisPath, t.RuntimeAcquisFilePath)
|
||||||
|
if err = Copy(t.TemplateAcquisPath, t.RuntimeAcquisFilePath); err != nil {
|
||||||
|
return fmt.Errorf("unable to copy '%s' to '%s': %v", t.TemplateAcquisPath, t.RuntimeAcquisFilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("copying %s to %s", t.TemplateAppsecProfilePath, filepath.Join(t.RuntimePath, "appsec-configs", "config.yaml"))
|
||||||
|
// copy template appsec-config file to runtime folder
|
||||||
|
if err = Copy(t.TemplateAppsecProfilePath, filepath.Join(t.RuntimePath, "appsec-configs", "config.yaml")); err != nil {
|
||||||
|
return fmt.Errorf("unable to copy '%s' to '%s': %v", t.TemplateAppsecProfilePath, filepath.Join(t.RuntimePath, "appsec-configs", "config.yaml"), err)
|
||||||
|
}
|
||||||
|
} else { //otherwise we drop a blank acquis file
|
||||||
|
if err = os.WriteFile(t.RuntimeAcquisFilePath, []byte(""), os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("unable to write blank acquis file '%s': %s", t.RuntimeAcquisFilePath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// install the hub in the runtime folder
|
||||||
|
if err = t.InstallHub(); err != nil {
|
||||||
|
return fmt.Errorf("unable to install hub in '%s': %s", t.RuntimeHubPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Config.LogFile != "" {
|
||||||
|
return t.RunWithLogFile()
|
||||||
|
} else if t.Config.NucleiTemplate != "" {
|
||||||
|
return t.RunWithNucleiTemplate()
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("log file or nuclei template must be set in '%s'", t.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
66
pkg/hubtest/nucleirunner.go
Normal file
66
pkg/hubtest/nucleirunner.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package hubtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NucleiConfig struct {
|
||||||
|
Path string `yaml:"nuclei_path"`
|
||||||
|
OutputDir string `yaml:"output_dir"`
|
||||||
|
CmdLineOptions []string `yaml:"cmdline_options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var NucleiTemplateFail = errors.New("Nuclei template failed")
|
||||||
|
|
||||||
|
func (nc *NucleiConfig) RunNucleiTemplate(testName string, templatePath string, target string) error {
|
||||||
|
tstamp := time.Now().Unix()
|
||||||
|
//templatePath is the full path to the template, we just want the name ie. "sqli-random-test"
|
||||||
|
tmp := strings.Split(templatePath, "/")
|
||||||
|
template := strings.Split(tmp[len(tmp)-1], ".")[0]
|
||||||
|
|
||||||
|
outputPrefix := fmt.Sprintf("%s/%s_%s-%d", nc.OutputDir, testName, template, tstamp)
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-u", target,
|
||||||
|
"-t", templatePath,
|
||||||
|
"-o", outputPrefix + ".json",
|
||||||
|
}
|
||||||
|
args = append(args, nc.CmdLineOptions...)
|
||||||
|
cmd := exec.Command(nc.Path, args...)
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
var outErr bytes.Buffer
|
||||||
|
|
||||||
|
cmd.Stdout = &out
|
||||||
|
cmd.Stderr = &outErr
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
|
||||||
|
if err := os.WriteFile(outputPrefix+"_stdout.txt", out.Bytes(), 0644); err != nil {
|
||||||
|
log.Warningf("Error writing stdout: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(outputPrefix+"_stderr.txt", outErr.Bytes(), 0644); err != nil {
|
||||||
|
log.Warningf("Error writing stderr: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("Error running nuclei: %s", err)
|
||||||
|
log.Warningf("Stdout saved to %s", outputPrefix+"_stdout.txt")
|
||||||
|
log.Warningf("Stderr saved to %s", outputPrefix+"_stderr.txt")
|
||||||
|
log.Warningf("Nuclei generated output saved to %s", outputPrefix+".json")
|
||||||
|
return err
|
||||||
|
} else if len(out.String()) == 0 {
|
||||||
|
//No stdout means no finding, it means our test failed
|
||||||
|
return NucleiTemplateFail
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -2,9 +2,13 @@ package hubtest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func sortedMapKeys[V any](m map[string]V) []string {
|
func sortedMapKeys[V any](m map[string]V) []string {
|
||||||
|
@ -106,3 +110,19 @@ func CopyDir(src string, dest string) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsAlive(target string) (bool, error) {
|
||||||
|
start := time.Now()
|
||||||
|
for {
|
||||||
|
conn, err := net.Dial("tcp", target)
|
||||||
|
if err == nil {
|
||||||
|
log.Debugf("appsec is up after %s", time.Since(start))
|
||||||
|
conn.Close()
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
if time.Since(start) > 10*time.Second {
|
||||||
|
return false, fmt.Errorf("took more than 10s for %s to be available", target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -214,6 +214,10 @@ func (n *Node) process(p *types.Event, ctx UnixParserCtx, expressionEnv map[stri
|
||||||
switch out := output.(type) {
|
switch out := output.(type) {
|
||||||
case string:
|
case string:
|
||||||
gstr = out
|
gstr = out
|
||||||
|
case int:
|
||||||
|
gstr = fmt.Sprintf("%d", out)
|
||||||
|
case float64, float32:
|
||||||
|
gstr = fmt.Sprintf("%f", out)
|
||||||
default:
|
default:
|
||||||
clog.Errorf("unexpected return type for RunTimeValue : %T", output)
|
clog.Errorf("unexpected return type for RunTimeValue : %T", output)
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,6 +127,8 @@ func (n *Node) ProcessStatics(statics []ExtraField, event *types.Event) error {
|
||||||
value = out
|
value = out
|
||||||
case int:
|
case int:
|
||||||
value = strconv.Itoa(out)
|
value = strconv.Itoa(out)
|
||||||
|
case float64, float32:
|
||||||
|
value = fmt.Sprintf("%f", out)
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
clog.Warnf("Expression '%s' returned a map, please use ToJsonString() to convert it to string if you want to keep it as is, or refine your expression to extract a string", static.ExpValue)
|
clog.Warnf("Expression '%s' returned a map, please use ToJsonString() to convert it to string if you want to keep it as is, or refine your expression to extract a string", static.ExpValue)
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
|
@ -134,7 +136,7 @@ func (n *Node) ProcessStatics(statics []ExtraField, event *types.Event) error {
|
||||||
case nil:
|
case nil:
|
||||||
clog.Debugf("Expression '%s' returned nil, skipping", static.ExpValue)
|
clog.Debugf("Expression '%s' returned nil, skipping", static.ExpValue)
|
||||||
default:
|
default:
|
||||||
clog.Errorf("unexpected return type for RunTimeValue : %T", output)
|
clog.Errorf("unexpected return type for '%s' : %T", static.ExpValue, output)
|
||||||
return errors.New("unexpected return type for RunTimeValue")
|
return errors.New("unexpected return type for RunTimeValue")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
240
pkg/types/appsec_event.go
Normal file
240
pkg/types/appsec_event.go
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. If user triggered a rule that is for a CVE, that has high confidence and that is blocking, ban
|
||||||
|
2. If user triggered 3 distinct rules with medium confidence across 3 different requests, ban
|
||||||
|
|
||||||
|
|
||||||
|
any(evt.Waf.ByTag("CVE"), {.confidence == "high" && .action == "block"})
|
||||||
|
|
||||||
|
len(evt.Waf.ByTagRx("*CVE*").ByConfidence("high").ByAction("block")) > 1
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
type MatchedRules []map[string]interface{}
|
||||||
|
|
||||||
|
type AppsecEvent struct {
|
||||||
|
HasInBandMatches, HasOutBandMatches bool
|
||||||
|
MatchedRules
|
||||||
|
Vars map[string]string
|
||||||
|
}
|
||||||
|
type Field string
|
||||||
|
|
||||||
|
func (f Field) String() string {
|
||||||
|
return string(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ID Field = "id"
|
||||||
|
RuleType Field = "rule_type"
|
||||||
|
Tags Field = "tags"
|
||||||
|
File Field = "file"
|
||||||
|
Confidence Field = "confidence"
|
||||||
|
Revision Field = "revision"
|
||||||
|
SecMark Field = "secmark"
|
||||||
|
Accuracy Field = "accuracy"
|
||||||
|
Msg Field = "msg"
|
||||||
|
Severity Field = "severity"
|
||||||
|
Kind Field = "kind"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (w AppsecEvent) GetVar(varName string) string {
|
||||||
|
if w.Vars == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if val, ok := w.Vars[varName]; ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
log.Infof("var %s not found. Available variables: %+v", varName, w.Vars)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// getters
|
||||||
|
func (w MatchedRules) GetField(field Field) []interface{} {
|
||||||
|
ret := make([]interface{}, 0)
|
||||||
|
for _, rule := range w {
|
||||||
|
ret = append(ret, rule[field.String()])
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w MatchedRules) GetURI() string {
|
||||||
|
for _, rule := range w {
|
||||||
|
return rule["uri"].(string)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w MatchedRules) GetHash() string {
|
||||||
|
for _, rule := range w {
|
||||||
|
//@sbl : let's fix this
|
||||||
|
return rule["hash"].(string)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w MatchedRules) GetVersion() string {
|
||||||
|
for _, rule := range w {
|
||||||
|
//@sbl : let's fix this
|
||||||
|
return rule["version"].(string)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w MatchedRules) GetName() string {
|
||||||
|
for _, rule := range w {
|
||||||
|
//@sbl : let's fix this
|
||||||
|
return rule["name"].(string)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w MatchedRules) GetMethod() string {
|
||||||
|
for _, rule := range w {
|
||||||
|
return rule["method"].(string)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w MatchedRules) GetRuleIDs() []int {
|
||||||
|
ret := make([]int, 0)
|
||||||
|
for _, rule := range w {
|
||||||
|
ret = append(ret, rule["id"].(int))
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w MatchedRules) Kinds() []string {
|
||||||
|
ret := make([]string, 0)
|
||||||
|
for _, rule := range w {
|
||||||
|
exists := false
|
||||||
|
for _, val := range ret {
|
||||||
|
if val == rule["kind"] {
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
ret = append(ret, rule["kind"].(string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w MatchedRules) GetMatchedZones() []string {
|
||||||
|
ret := make([]string, 0)
|
||||||
|
|
||||||
|
for _, rule := range w {
|
||||||
|
ret = append(ret, rule["matched_zones"].([]string)...)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// filters
|
||||||
|
func (w MatchedRules) ByID(id int) MatchedRules {
|
||||||
|
ret := MatchedRules{}
|
||||||
|
|
||||||
|
for _, rule := range w {
|
||||||
|
if rule["id"] == id {
|
||||||
|
ret = append(ret, rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w MatchedRules) ByKind(kind string) MatchedRules {
|
||||||
|
ret := MatchedRules{}
|
||||||
|
for _, rule := range w {
|
||||||
|
if rule["kind"] == kind {
|
||||||
|
ret = append(ret, rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w MatchedRules) ByTags(match []string) MatchedRules {
|
||||||
|
ret := MatchedRules{}
|
||||||
|
for _, rule := range w {
|
||||||
|
for _, tag := range rule["tags"].([]string) {
|
||||||
|
for _, match_tag := range match {
|
||||||
|
if tag == match_tag {
|
||||||
|
ret = append(ret, rule)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w MatchedRules) ByTag(match string) MatchedRules {
|
||||||
|
ret := MatchedRules{}
|
||||||
|
for _, rule := range w {
|
||||||
|
for _, tag := range rule["tags"].([]string) {
|
||||||
|
if tag == match {
|
||||||
|
ret = append(ret, rule)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w MatchedRules) ByTagRx(rx string) MatchedRules {
|
||||||
|
ret := MatchedRules{}
|
||||||
|
re := regexp.MustCompile(rx)
|
||||||
|
if re == nil {
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
for _, rule := range w {
|
||||||
|
for _, tag := range rule["tags"].([]string) {
|
||||||
|
log.Debugf("ByTagRx: %s = %s -> %t", rx, tag, re.MatchString(tag))
|
||||||
|
if re.MatchString(tag) {
|
||||||
|
ret = append(ret, rule)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w MatchedRules) ByDisruptiveness(is bool) MatchedRules {
|
||||||
|
ret := MatchedRules{}
|
||||||
|
for _, rule := range w {
|
||||||
|
if rule["disruptive"] == is {
|
||||||
|
ret = append(ret, rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Debugf("ByDisruptiveness(%t) -> %d", is, len(ret))
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w MatchedRules) BySeverity(severity string) MatchedRules {
|
||||||
|
ret := MatchedRules{}
|
||||||
|
for _, rule := range w {
|
||||||
|
if rule["severity"] == severity {
|
||||||
|
ret = append(ret, rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Debugf("BySeverity(%s) -> %d", severity, len(ret))
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w MatchedRules) ByAccuracy(accuracy string) MatchedRules {
|
||||||
|
ret := MatchedRules{}
|
||||||
|
for _, rule := range w {
|
||||||
|
if rule["accuracy"] == accuracy {
|
||||||
|
ret = append(ret, rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Debugf("ByAccuracy(%s) -> %d", accuracy, len(ret))
|
||||||
|
return ret
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import (
|
||||||
const (
|
const (
|
||||||
LOG = iota
|
LOG = iota
|
||||||
OVFLW
|
OVFLW
|
||||||
|
APPSEC
|
||||||
)
|
)
|
||||||
|
|
||||||
// Event is the structure representing a runtime event (log or overflow)
|
// Event is the structure representing a runtime event (log or overflow)
|
||||||
|
@ -40,6 +41,7 @@ type Event struct {
|
||||||
StrTimeFormat string `yaml:"StrTimeFormat,omitempty" json:"StrTimeFormat,omitempty"`
|
StrTimeFormat string `yaml:"StrTimeFormat,omitempty" json:"StrTimeFormat,omitempty"`
|
||||||
MarshaledTime string `yaml:"MarshaledTime,omitempty" json:"MarshaledTime,omitempty"`
|
MarshaledTime string `yaml:"MarshaledTime,omitempty" json:"MarshaledTime,omitempty"`
|
||||||
Process bool `yaml:"Process,omitempty" json:"Process,omitempty"` //can be set to false to avoid processing line
|
Process bool `yaml:"Process,omitempty" json:"Process,omitempty"` //can be set to false to avoid processing line
|
||||||
|
Appsec AppsecEvent `yaml:"Appsec,omitempty" json:"Appsec,omitempty"`
|
||||||
/* Meta is the only part that will make it to the API - it should be normalized */
|
/* Meta is the only part that will make it to the API - it should be normalized */
|
||||||
Meta map[string]string `yaml:"Meta,omitempty" json:"Meta,omitempty"`
|
Meta map[string]string `yaml:"Meta,omitempty" json:"Meta,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ teardown() {
|
||||||
rune -0 cscli hub list
|
rune -0 cscli hub list
|
||||||
assert_output "No items to display"
|
assert_output "No items to display"
|
||||||
rune -0 cscli hub list -o json
|
rune -0 cscli hub list -o json
|
||||||
assert_json '{parsers:[],scenarios:[],collections:[],postoverflows:[]}'
|
assert_json '{"appsec-configs":[],"appsec-rules":[],parsers:[],scenarios:[],collections:[],postoverflows:[]}'
|
||||||
rune -0 cscli hub list -o raw
|
rune -0 cscli hub list -o raw
|
||||||
assert_output 'name,status,version,description,type'
|
assert_output 'name,status,version,description,type'
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ teardown() {
|
||||||
assert_line "collections"
|
assert_line "collections"
|
||||||
rune -0 cscli hub types -o human
|
rune -0 cscli hub types -o human
|
||||||
rune -0 yq -o json <(output)
|
rune -0 yq -o json <(output)
|
||||||
assert_json '["parsers","postoverflows","scenarios","collections"]'
|
assert_json '["parsers","postoverflows","scenarios","appsec-configs","appsec-rules","collections"]'
|
||||||
rune -0 cscli hub types -o json
|
rune -0 cscli hub types -o json
|
||||||
assert_json '["parsers","postoverflows","scenarios","collections"]'
|
assert_json '["parsers","postoverflows","scenarios","appsec-configs","appsec-rules","collections"]'
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 78fa631d1370562d2cd4a1390989e706158e7bf0
|
Subproject commit 44913ffe6020d1561c4c4d1e26cda8e07a1f374f
|
Loading…
Reference in a new issue