diff --git a/cmd/crowdsec-cli/main.go b/cmd/crowdsec-cli/main.go index beab47913..8572d1570 100644 --- a/cmd/crowdsec-cli/main.go +++ b/cmd/crowdsec-cli/main.go @@ -256,6 +256,7 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall rootCmd.AddCommand(NewHubTestCmd()) rootCmd.AddCommand(NewNotificationsCmd()) rootCmd.AddCommand(NewSupportCmd()) + rootCmd.AddCommand(NewWafRulesCmd()) if fflag.CscliSetup.IsEnabled() { rootCmd.AddCommand(NewSetupCmd()) diff --git a/cmd/crowdsec-cli/utils.go b/cmd/crowdsec-cli/utils.go index e7a520172..1a99c2acf 100644 --- a/cmd/crowdsec-cli/utils.go +++ b/cmd/crowdsec-cli/utils.go @@ -131,6 +131,8 @@ func compInstalledItems(itemType string, args []string, toComplete string) ([]st items, err = cwhub.GetInstalledPostOverflowsAsString() case cwhub.COLLECTIONS: items, err = cwhub.GetInstalledCollectionsAsString() + case cwhub.WAF_RULES: + items, err = cwhub.GetInstalledWafRulesAsString() default: return nil, cobra.ShellCompDirectiveDefault } @@ -324,6 +326,8 @@ func ShowMetrics(hubItem *cwhub.Item) { } ShowMetrics(hubItem) } + case cwhub.WAF_RULES: + log.Fatalf("FIXME: not implemented yet") default: log.Errorf("item of type '%s' is unknown", hubItem.Type) } diff --git a/cmd/crowdsec-cli/waf_rules.go b/cmd/crowdsec-cli/waf_rules.go new file mode 100644 index 000000000..dcfbe009e --- /dev/null +++ b/cmd/crowdsec-cli/waf_rules.go @@ -0,0 +1,196 @@ +package main + +import ( + "fmt" + + "github.com/fatih/color" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/crowdsecurity/crowdsec/pkg/cwhub" +) + +func NewWafRulesCmd() *cobra.Command { + var cmdWafRules = &cobra.Command{ + Use: "waf-rules [action] [config]", + Short: "Install/Remove/Upgrade/Inspect waf-rule(s) from hub", + Example: `cscli waf-rules install crowdsecurity/core-rule-set +cscli waf-rules inspect crowdsecurity/core-rule-set +cscli waf-rules upgrade crowdsecurity/core-rule-set +cscli waf-rules list +cscli waf-rules remove crowdsecurity/core-rule-set +`, + Args: cobra.MinimumNArgs(1), + Aliases: []string{"waf-rule"}, + DisableAutoGenTag: true, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if err := csConfig.LoadHub(); err != nil { + log.Fatal(err) + } + if csConfig.Hub == nil { + return fmt.Errorf("you must configure cli before interacting with hub") + } + + if err := cwhub.SetHubBranch(); err != nil { + return fmt.Errorf("error while setting hub branch: %s", err) + } + + if err := cwhub.GetHubIdx(csConfig.Hub); err != nil { + log.Info("Run 'sudo cscli hub update' to get the hub index") + log.Fatalf("Failed to get Hub index : %v", err) + } + return nil + }, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + if cmd.Name() == "inspect" || cmd.Name() == "list" { + return + } + log.Infof(ReloadMessage()) + }, + } + + cmdWafRules.AddCommand(NewWafRulesInstallCmd()) + cmdWafRules.AddCommand(NewWafRulesRemoveCmd()) + cmdWafRules.AddCommand(NewWafRulesUpgradeCmd()) + cmdWafRules.AddCommand(NewWafRulesInspectCmd()) + cmdWafRules.AddCommand(NewWafRulesListCmd()) + + return cmdWafRules +} + +func NewWafRulesInstallCmd() *cobra.Command { + var ignoreError bool + + var cmdWafRulesInstall = &cobra.Command{ + Use: "install [config]", + Short: "Install given waf-rule(s)", + Long: `Fetch and install given waf-rule(s) from hub`, + Example: `cscli waf-rules install crowdsec/xxx crowdsec/xyz`, + Args: cobra.MinimumNArgs(1), + DisableAutoGenTag: true, + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return compAllItems(cwhub.WAF_RULES, args, toComplete) + }, + Run: func(cmd *cobra.Command, args []string) { + for _, name := range args { + t := cwhub.GetItem(cwhub.WAF_RULES, name) + if t == nil { + nearestItem, score := GetDistance(cwhub.WAF_RULES, name) + Suggest(cwhub.WAF_RULES, name, nearestItem.Name, score, ignoreError) + continue + } + if err := cwhub.InstallItem(csConfig, name, cwhub.WAF_RULES, forceAction, downloadOnly); err != nil { + if ignoreError { + log.Errorf("Error while installing '%s': %s", name, err) + } else { + log.Fatalf("Error while installing '%s': %s", name, err) + } + } + } + }, + } + cmdWafRulesInstall.PersistentFlags().BoolVarP(&downloadOnly, "download-only", "d", false, "Only download packages, don't enable") + cmdWafRulesInstall.PersistentFlags().BoolVar(&forceAction, "force", false, "Force install : Overwrite tainted and outdated files") + cmdWafRulesInstall.PersistentFlags().BoolVar(&ignoreError, "ignore", false, "Ignore errors when installing multiple waf rules") + + return cmdWafRulesInstall +} + +func NewWafRulesRemoveCmd() *cobra.Command { + var cmdWafRulesRemove = &cobra.Command{ + Use: "remove [config]", + Short: "Remove given waf-rule(s)", + Long: `Remove given waf-rule(s) from hub`, + Aliases: []string{"delete"}, + Example: `cscli waf-rules remove crowdsec/xxx crowdsec/xyz`, + DisableAutoGenTag: true, + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return compInstalledItems(cwhub.WAF_RULES, args, toComplete) + }, + Run: func(cmd *cobra.Command, args []string) { + if all { + cwhub.RemoveMany(csConfig, cwhub.WAF_RULES, "", all, purge, forceAction) + return + } + + if len(args) == 0 { + log.Fatalf("Specify at least one waf rule to remove or '--all' flag.") + } + + for _, name := range args { + cwhub.RemoveMany(csConfig, cwhub.WAF_RULES, name, all, purge, forceAction) + } + }, + } + cmdWafRulesRemove.PersistentFlags().BoolVar(&purge, "purge", false, "Delete source file too") + cmdWafRulesRemove.PersistentFlags().BoolVar(&forceAction, "force", false, "Force remove : Remove tainted and outdated files") + cmdWafRulesRemove.PersistentFlags().BoolVar(&all, "all", false, "Delete all the waf rules") + + return cmdWafRulesRemove +} + +func NewWafRulesUpgradeCmd() *cobra.Command { + var cmdWafRulesUpgrade = &cobra.Command{ + Use: "upgrade [config]", + Short: "Upgrade given waf-rule(s)", + Long: `Fetch and upgrade given waf-rule(s) from hub`, + Example: `cscli waf-rules upgrade crowdsec/xxx crowdsec/xyz`, + DisableAutoGenTag: true, + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return compInstalledItems(cwhub.WAF_RULES, args, toComplete) + }, + Run: func(cmd *cobra.Command, args []string) { + if all { + cwhub.UpgradeConfig(csConfig, cwhub.WAF_RULES, "", forceAction) + } else { + if len(args) == 0 { + log.Fatalf("no target waf rule to upgrade") + } + for _, name := range args { + cwhub.UpgradeConfig(csConfig, cwhub.WAF_RULES, name, forceAction) + } + } + }, + } + cmdWafRulesUpgrade.PersistentFlags().BoolVar(&all, "all", false, "Upgrade all the waf rules") + cmdWafRulesUpgrade.PersistentFlags().BoolVar(&forceAction, "force", false, "Force upgrade : Overwrite tainted and outdated files") + + return cmdWafRulesUpgrade +} + +func NewWafRulesInspectCmd() *cobra.Command { + var cmdWafRulesInspect = &cobra.Command{ + Use: "inspect [name]", + Short: "Inspect given waf rule", + Long: `Inspect given waf rule`, + Example: `cscli waf-rules inspect crowdsec/xxx`, + DisableAutoGenTag: true, + Args: cobra.MinimumNArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return compInstalledItems(cwhub.WAF_RULES, args, toComplete) + }, + Run: func(cmd *cobra.Command, args []string) { + InspectItem(args[0], cwhub.WAF_RULES) + }, + } + cmdWafRulesInspect.PersistentFlags().StringVarP(&prometheusURL, "url", "u", "", "Prometheus url") + + return cmdWafRulesInspect +} + +func NewWafRulesListCmd() *cobra.Command { + var cmdWafRulesList = &cobra.Command{ + Use: "list [name]", + Short: "List all waf rules or given one", + Long: `List all waf rules or given one`, + Example: `cscli waf-rules list +cscli waf-rules list crowdsecurity/xxx`, + DisableAutoGenTag: true, + Run: func(cmd *cobra.Command, args []string) { + ListItems(color.Output, []string{cwhub.WAF_RULES}, args, false, true, all) + }, + } + cmdWafRulesList.PersistentFlags().BoolVarP(&all, "all", "a", false, "List disabled items as well") + + return cmdWafRulesList +} diff --git a/go.mod b/go.mod index 9fc6743cc..d64e1c075 100644 --- a/go.mod +++ b/go.mod @@ -70,7 +70,7 @@ require ( github.com/blackfireio/osinfo v1.0.3 github.com/bluele/gcache v0.0.2 github.com/cespare/xxhash/v2 v2.1.2 - github.com/corazawaf/coraza/v3 v3.0.0-rc.2 + github.com/corazawaf/coraza/v3 v3.0.0-00010101000000-000000000000 github.com/coreos/go-systemd/v22 v22.5.0 github.com/crowdsecurity/go-cs-lib v0.0.0-20230531105801-4c1535c2b3bd github.com/goccy/go-yaml v1.9.7 diff --git a/pkg/acquisition/modules/waf/waf.go b/pkg/acquisition/modules/waf/waf.go index e4ae38926..8a8eea2f6 100644 --- a/pkg/acquisition/modules/waf/waf.go +++ b/pkg/acquisition/modules/waf/waf.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "net/http" "net/url" + "os" "strings" "time" @@ -14,6 +15,7 @@ import ( corazatypes "github.com/corazawaf/coraza/v3/types" "github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration" "github.com/crowdsecurity/crowdsec/pkg/types" + "github.com/crowdsecurity/crowdsec/pkg/waf" "github.com/crowdsecurity/go-cs-lib/pkg/trace" "github.com/google/uuid" "github.com/pkg/errors" @@ -48,23 +50,6 @@ type WafSourceConfig struct { configuration.DataSourceCommonCfg `yaml:",inline"` } -/* -type DataSource interface { - GetMetrics() []prometheus.Collector // Returns pointers to metrics that are managed by the module - GetAggregMetrics() []prometheus.Collector // Returns pointers to metrics that are managed by the module (aggregated mode, limits cardinality) - UnmarshalConfig([]byte) error // Decode and pre-validate the YAML datasource - anything that can be checked before runtime - Configure([]byte, *log.Entry) error // Complete the YAML datasource configuration and perform runtime checks. - ConfigureByDSN(string, map[string]string, *log.Entry, string) error // Configure the datasource - GetMode() string // Get the mode (TAIL, CAT or SERVER) - GetName() string // Get the name of the module - OneShotAcquisition(chan types.Event, *tomb.Tomb) error // Start one shot acquisition(eg, cat a file) - StreamingAcquisition(chan types.Event, *tomb.Tomb) error // Start live acquisition (eg, tail a file) - CanRun() error // Whether the datasource can run or not (eg, journalctl on BSD is a non-sense) - GetUuid() string // Get the unique identifier of the datasource - Dump() interface{} -} -*/ - func (w *WafSource) GetMetrics() []prometheus.Collector { return nil } @@ -102,6 +87,7 @@ func (w *WafSource) UnmarshalConfig(yamlConfig []byte) error { if w.config.Mode == "" { w.config.Mode = configuration.TAIL_MODE } + return nil } @@ -129,11 +115,32 @@ func (w *WafSource) Configure(yamlConfig []byte, logger *log.Entry) error { Handler: w.mux, } + crowdsecWafConfig := waf.NewWafConfig() + + err = crowdsecWafConfig.LoadWafRules() + + if err != nil { + return fmt.Errorf("cannot load WAF rules: %w", err) + } + + var inBandRules string + + for _, rule := range crowdsecWafConfig.InbandRules { + + inBandRules += rule.String() + "\n" + } + + w.logger.Infof("Loading %d in-band rules", len(strings.Split(inBandRules, "\n"))) + + //w.logger.Infof("Loading rules %+v", inBandRules) + + fs := os.DirFS(crowdsecWafConfig.Datadir) + //in-band waf : kill on sight inbandwaf, err := coraza.NewWAF( coraza.NewWAFConfig(). WithErrorCallback(logError). - WithDirectivesFromFile("coraza_inband.conf"), + WithDirectives(inBandRules).WithRootFS(fs), ) if err != nil { @@ -144,8 +151,8 @@ func (w *WafSource) Configure(yamlConfig []byte, logger *log.Entry) error { //out-of-band waf : log only outofbandwaf, err := coraza.NewWAF( coraza.NewWAFConfig(). - WithErrorCallback(logError). - WithDirectivesFromFile("coraza_outofband.conf"), + WithErrorCallback(logError), //. + //WithDirectivesFromFile("coraza_outofband.conf"), ) if err != nil { return errors.Wrap(err, "Cannot create WAF") @@ -269,6 +276,8 @@ func processReqWithEngine(waf coraza.WAF, r ParsedRequest, uuid string, wafType //this method is not exported by coraza, so we have to do it ourselves. //ideally, this would be dealt with by expr code, and we provide helpers to manipulate the transaction object?\ //var txx experimental.FullTransaction + + //txx := experimental.ToFullInterface(tx) //txx = tx.(experimental.FullTransaction) //txx.RemoveRuleByID(1) diff --git a/pkg/csconfig/crowdsec_service.go b/pkg/csconfig/crowdsec_service.go index 2642603cf..60484c916 100644 --- a/pkg/csconfig/crowdsec_service.go +++ b/pkg/csconfig/crowdsec_service.go @@ -11,6 +11,8 @@ import ( "github.com/crowdsecurity/go-cs-lib/pkg/ptr" ) +var DataDir string // FIXME: find a better way to pass this to the waf + // CrowdsecServiceCfg contains the location of parsers/scenarios/... and acquisition files type CrowdsecServiceCfg struct { Enable *bool `yaml:"enable"` @@ -106,6 +108,8 @@ func (c *Config) LoadCrowdsec() error { c.Crowdsec.HubDir = c.ConfigPaths.HubDir c.Crowdsec.HubIndexFile = c.ConfigPaths.HubIndexFile + DataDir = c.Crowdsec.DataDir // FIXME: find a better way to give it to the waf + if c.Crowdsec.ParserRoutinesCount <= 0 { c.Crowdsec.ParserRoutinesCount = 1 } diff --git a/pkg/cwhub/cwhub.go b/pkg/cwhub/cwhub.go index f48dc223f..0e508d9f1 100644 --- a/pkg/cwhub/cwhub.go +++ b/pkg/cwhub/cwhub.go @@ -20,7 +20,8 @@ var PARSERS = "parsers" var PARSERS_OVFLW = "postoverflows" var SCENARIOS = "scenarios" var COLLECTIONS = "collections" -var ItemTypes = []string{PARSERS, PARSERS_OVFLW, SCENARIOS, COLLECTIONS} +var WAF_RULES = "waf-rules" +var ItemTypes = []string{PARSERS, PARSERS_OVFLW, SCENARIOS, COLLECTIONS, WAF_RULES} var hubIdx map[string]map[string]Item @@ -42,7 +43,7 @@ type ItemHubStatus struct { Status string `json:"status"` } -//Item can be : parsed, scenario, collection +// Item can be : parsed, scenario, collection type Item struct { /*descriptive info*/ Type string `yaml:"type,omitempty" json:"type,omitempty"` //parser|postoverflows|scenario|collection(|enrich) @@ -77,6 +78,7 @@ type Item struct { PostOverflows []string `yaml:"postoverflows,omitempty" json:"postoverflows,omitempty"` Scenarios []string `yaml:"scenarios,omitempty" json:"scenarios,omitempty"` Collections []string `yaml:"collections,omitempty" json:"collections,omitempty"` + WafRules []string `yaml:"waf_rules,omitempty" json:"waf_rules,omitempty"` } func (i *Item) toHubStatus() ItemHubStatus { @@ -107,7 +109,7 @@ var skippedTainted = 0 var ReferenceMissingError = errors.New("Reference(s) missing in collection") var MissingHubIndex = errors.New("hub index can't be found") -//GetVersionStatus : semver requires 'v' prefix +// GetVersionStatus : semver requires 'v' prefix func GetVersionStatus(v *Item) int { return semver.Compare("v"+v.Version, "v"+v.LocalVersion) } @@ -140,7 +142,7 @@ func GetItemMap(itemType string) map[string]Item { return m } -//GetItemByPath retrieves the item from hubIdx based on the path. To achieve this it will resolve symlink to find associated hub item. +// GetItemByPath retrieves the item from hubIdx based on the path. To achieve this it will resolve symlink to find associated hub item. func GetItemByPath(itemType string, itemPath string) (*Item, error) { /*try to resolve symlink*/ finalName := "" @@ -199,14 +201,14 @@ func AddItem(itemType string, item Item) error { } func DisplaySummary() { - log.Printf("Loaded %d collecs, %d parsers, %d scenarios, %d post-overflow parsers", len(hubIdx[COLLECTIONS]), - len(hubIdx[PARSERS]), len(hubIdx[SCENARIOS]), len(hubIdx[PARSERS_OVFLW])) + log.Printf("Loaded %d collecs, %d parsers, %d scenarios, %d post-overflow parsers, %d waf rules", len(hubIdx[COLLECTIONS]), + len(hubIdx[PARSERS]), len(hubIdx[SCENARIOS]), len(hubIdx[PARSERS_OVFLW]), len(hubIdx[WAF_RULES])) if skippedLocal > 0 || skippedTainted > 0 { log.Printf("unmanaged items : %d local, %d tainted", skippedLocal, skippedTainted) } } -//returns: human-text, Enabled, Warning, Unmanaged +// returns: human-text, Enabled, Warning, Unmanaged func ItemStatus(v Item) (string, bool, bool, bool) { strret := "disabled" Ok := false @@ -341,7 +343,34 @@ func GetInstalledCollections() ([]Item, error) { return retItems, nil } -//Returns a list of entries for packages : name, status, local_path, local_version, utf8_status (fancy) +func GetInstalledWafRules() ([]Item, error) { + var retItems []Item + + if _, ok := hubIdx[WAF_RULES]; !ok { + return nil, fmt.Errorf("no waf rules in hubIdx") + } + for _, item := range hubIdx[WAF_RULES] { + if item.Installed { + retItems = append(retItems, item) + } + } + return retItems, nil +} + +func GetInstalledWafRulesAsString() ([]string, error) { + var retStr []string + + items, err := GetInstalledWafRules() + if err != nil { + return nil, errors.Wrap(err, "while fetching waf rules") + } + for _, it := range items { + retStr = append(retStr, it.Name) + } + return retStr, nil +} + +// Returns a list of entries for packages : name, status, local_path, local_version, utf8_status (fancy) func GetHubStatusForItemType(itemType string, name string, all bool) []ItemHubStatus { if _, ok := hubIdx[itemType]; !ok { log.Errorf("type %s doesn't exist", itemType) diff --git a/pkg/cwhub/loader.go b/pkg/cwhub/loader.go index aa63510a8..961e9a02e 100644 --- a/pkg/cwhub/loader.go +++ b/pkg/cwhub/loader.go @@ -90,8 +90,11 @@ func parser_visit(path string, f os.DirEntry, err error) error { } else if stage == COLLECTIONS { ftype = COLLECTIONS stage = "" + } else if stage == WAF_RULES { + ftype = WAF_RULES + stage = "" } else if ftype != PARSERS && ftype != PARSERS_OVFLW /*its a PARSER / PARSER_OVFLW with a stage */ { - return fmt.Errorf("unknown configuration type for file '%s'", path) + return fmt.Errorf("unknown configuration type %s for file '%s'", ftype, path) } log.Tracef("CORRECTED [%s] by [%s] in stage [%s] of type [%s]", fname, fauthor, stage, ftype) @@ -102,7 +105,7 @@ func parser_visit(path string, f os.DirEntry, err error) error { when the collection is installed, both files are created */ //non symlinks are local user files or hub files - if f.Type() & os.ModeSymlink == 0 { + if f.Type()&os.ModeSymlink == 0 { local = true log.Tracef("%s isn't a symlink", path) } else { @@ -406,7 +409,7 @@ func LoadPkgIndex(buff []byte) (map[string]map[string]Item, error) { /*if it's a collection, check its sub-items are present*/ //XX should be done later if itemType == COLLECTIONS { - var tmp = [][]string{item.Parsers, item.PostOverflows, item.Scenarios, item.Collections} + var tmp = [][]string{item.Parsers, item.PostOverflows, item.Scenarios, item.Collections, item.WafRules} for idx, ptr := range tmp { ptrtype := ItemTypes[idx] for _, p := range ptr { diff --git a/pkg/waf/waf.go b/pkg/waf/waf.go new file mode 100644 index 000000000..a979d9d0a --- /dev/null +++ b/pkg/waf/waf.go @@ -0,0 +1,196 @@ +package waf + +import ( + "os" + "path/filepath" + "strings" + + "github.com/antonmedv/expr" + "github.com/antonmedv/expr/vm" + "github.com/crowdsecurity/crowdsec/pkg/csconfig" + "github.com/crowdsecurity/crowdsec/pkg/cwhub" + "github.com/crowdsecurity/crowdsec/pkg/types" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" +) + +type Hook struct { + Filter string `yaml:"filter"` + FilterExpr *vm.Program `yaml:"-"` + OnSuccess string `yaml:"on_success"` + Apply []string `yaml:"apply"` + ApplyExpr []*vm.Program `yaml:"-"` +} + +type CompiledHook struct { + Filter *vm.Program `yaml:"-"` + Apply []*vm.Program `yaml:"-"` +} + +type WafRule struct { + SecLangFilesRules []string `yaml:"seclang_files_rules"` + SecLangRules []string `yaml:"seclang_rules"` + OnLoad []Hook `yaml:"on_load"` + PreEval []Hook `yaml:"pre_eval"` + OnMatch []Hook `yaml:"on_match"` + + CompiledOnLoad []CompiledHook `yaml:"-"` + CompiledPreEval []CompiledHook `yaml:"-"` + CompiledOnMatch []CompiledHook `yaml:"-"` + + MergedRules []string `yaml:"-"` + OutOfBand bool `yaml:"-"` +} + +type WafConfig struct { + InbandRules []WafRule + OutOfBandRules []WafRule + Datadir string + logger *log.Entry +} + +func buildHook(hook Hook) (CompiledHook, error) { + compiledHook := CompiledHook{} + if hook.Filter != "" { + program, err := expr.Compile(hook.Filter) //FIXME: opts + if err != nil { + log.Errorf("unable to compile filter %s : %s", hook.Filter, err) + return CompiledHook{}, err + } + compiledHook.Filter = program + } + for _, apply := range hook.Apply { + program, err := expr.Compile(apply, GetExprWAFOptions(map[string]interface{}{ + "WafRules": []WafRule{}, + })...) + if err != nil { + log.Errorf("unable to compile apply %s : %s", apply, err) + return CompiledHook{}, err + } + compiledHook.Apply = append(compiledHook.Apply, program) + } + return compiledHook, nil +} + +func (w *WafConfig) LoadWafRules() error { + var files []string + for _, hubWafRuleItem := range cwhub.GetItemMap(cwhub.WAF_RULES) { + if hubWafRuleItem.Installed { + files = append(files, hubWafRuleItem.LocalPath) + } + } + w.logger.Infof("Loading %d waf files", len(files)) + for _, file := range files { + + fileContent, err := os.ReadFile(file) //FIXME: actually read from datadir + if err != nil { + w.logger.Errorf("unable to read file %s : %s", file, err) + continue + } + wafRule := WafRule{} + err = yaml.Unmarshal(fileContent, &wafRule) + if err != nil { + w.logger.Errorf("unable to unmarshal file %s : %s", file, err) + continue + } + if wafRule.SecLangFilesRules != nil { + for _, rulesFile := range wafRule.SecLangFilesRules { + fullPath := filepath.Join(w.Datadir, rulesFile) + c, err := os.ReadFile(fullPath) + if err != nil { + w.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 + } + wafRule.MergedRules = append(wafRule.MergedRules, line) + } + } + } + if wafRule.SecLangRules != nil { + wafRule.MergedRules = append(wafRule.MergedRules, wafRule.SecLangRules...) + } + + //compile hooks + for _, hook := range wafRule.OnLoad { + compiledHook, err := buildHook(hook) + if err != nil { + w.logger.Errorf("unable to build hook %s : %s", hook.Filter, err) + continue + } + wafRule.CompiledOnLoad = append(wafRule.CompiledOnLoad, compiledHook) + } + + for _, hook := range wafRule.PreEval { + compiledHook, err := buildHook(hook) + if err != nil { + w.logger.Errorf("unable to build hook %s : %s", hook.Filter, err) + continue + } + wafRule.CompiledPreEval = append(wafRule.CompiledPreEval, compiledHook) + } + + for _, hook := range wafRule.OnMatch { + compiledHook, err := buildHook(hook) + if err != nil { + w.logger.Errorf("unable to build hook %s : %s", hook.Filter, err) + continue + } + wafRule.CompiledOnMatch = append(wafRule.CompiledOnMatch, compiledHook) + } + + //Run the on_load hooks + + if len(wafRule.CompiledOnLoad) > 0 { + w.logger.Infof("Running %d on_load hooks", len(wafRule.CompiledOnLoad)) + for hookIdx, onLoadHook := range wafRule.CompiledOnLoad { + //Ignore filter for on load ? + if onLoadHook.Apply != nil { + for exprIdx, applyExpr := range onLoadHook.Apply { + _, err := expr.Run(applyExpr, nil) //FIXME: give proper env + if err != nil { + w.logger.Errorf("unable to run apply for on_load rule %s : %s", wafRule.OnLoad[hookIdx].Apply[exprIdx], err) + continue + } + } + } + } + } + + if wafRule.MergedRules != nil { + if wafRule.OutOfBand { + w.OutOfBandRules = append(w.OutOfBandRules, wafRule) + } else { + w.InbandRules = append(w.InbandRules, wafRule) + } + } else { + w.logger.Warnf("no rules found in file %s ??", file) + } + } + return nil +} + +func NewWafConfig() *WafConfig { + //FIXME: find a better way to get the datadir + clog := log.New() + if err := types.ConfigureLogger(clog); err != nil { + //return nil, fmt.Errorf("while configuring datasource logger: %w", err) + return nil + } + logger := clog.WithFields(log.Fields{ + "type": "waf-config", + }) + + initWafHelpers() + + return &WafConfig{Datadir: csconfig.DataDir, logger: logger} +} + +func (w *WafRule) String() string { + return strings.Join(w.MergedRules, "\n") +} diff --git a/pkg/waf/waf_expr_lib.go b/pkg/waf/waf_expr_lib.go new file mode 100644 index 000000000..79d693458 --- /dev/null +++ b/pkg/waf/waf_expr_lib.go @@ -0,0 +1,26 @@ +package waf + +//This is a copy paste from expr_lib.go, we probably want to only have one ? + +type exprCustomFunc struct { + name string + function func(params ...any) (any, error) + signature []interface{} +} + +var exprFuncs = []exprCustomFunc{ + { + name: "SetRulesToInband", + function: SetRulesToInband, + signature: []interface{}{ + new(func() error), + }, + }, + { + name: "SetRulesToOutOfBand", + function: SetRulesToOutOfBand, + signature: []interface{}{ + new(func() error), + }, + }, +} diff --git a/pkg/waf/waf_helpers.go b/pkg/waf/waf_helpers.go new file mode 100644 index 000000000..b99732ca3 --- /dev/null +++ b/pkg/waf/waf_helpers.go @@ -0,0 +1,41 @@ +package waf + +import ( + "github.com/antonmedv/expr" + "github.com/crowdsecurity/crowdsec/pkg/exprhelpers" +) + +var exprFunctionOptions []expr.Option + +func initWafHelpers() { + exprFunctionOptions = []expr.Option{} + for _, function := range exprFuncs { + exprFunctionOptions = append(exprFunctionOptions, + expr.Function(function.name, + function.function, + function.signature..., + )) + } +} + +func GetExprWAFOptions(ctx map[string]interface{}) []expr.Option { + baseHelpers := exprhelpers.GetExprOptions(ctx) + + for _, function := range exprFuncs { + baseHelpers = append(baseHelpers, + expr.Function(function.name, + function.function, + function.signature..., + )) + } + return baseHelpers +} + +func SetRulesToInband(params ...any) (any, error) { + + return nil, nil +} + +func SetRulesToOutOfBand(params ...any) (any, error) { + return nil, nil +}