crowdsec/pkg/outputs/ouputs.go
2020-05-20 10:49:17 +02:00

314 lines
8 KiB
Go

package outputs
import (
"fmt"
"io"
"os"
"strconv"
"time"
"github.com/crowdsecurity/crowdsec/pkg/cwplugin"
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
"github.com/crowdsecurity/crowdsec/pkg/types"
"github.com/crowdsecurity/crowdsec/pkg/cwapi"
"github.com/antonmedv/expr"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
type OutputFactory struct {
BackendFolder string `yaml:"backend"`
}
type Output struct {
API *cwapi.ApiCtx
bManager *cwplugin.BackendManager
}
/*
Transform an overflow (SignalOccurence) and a Profile into a BanOrder
*/
func OvflwToOrder(sig types.SignalOccurence, prof types.Profile) (*types.BanOrder, error, error) {
var ordr types.BanOrder
var warn error
//Identify remediation type
if prof.Remediation.Ban {
ordr.MeasureType = "ban"
} else if prof.Remediation.Slow {
ordr.MeasureType = "slow"
} else if prof.Remediation.Captcha {
ordr.MeasureType = "captcha"
} else {
/*if the profil has no remediation, no order */
return nil, nil, fmt.Errorf("no remediation")
}
ordr.MeasureSource = "local"
ordr.Reason = sig.Scenario
//Identify scope
v, ok := sig.Labels["scope"]
if !ok {
//if remediation_scope isn't specified, it's IP
v = "ip"
}
ordr.Scope = v
asn, err := strconv.Atoi(sig.Source.AutonomousSystemNumber)
if err != nil {
warn = fmt.Errorf("invalid as number : %s : %s", sig.Source.AutonomousSystemNumber, err)
}
ordr.TargetAS = asn
ordr.TargetASName = sig.Source.AutonomousSystemOrganization
ordr.TargetIP = sig.Source.Ip
ordr.TargetRange = sig.Source.Range
ordr.TargetCountry = sig.Source.Country
switch v {
case "range":
ordr.TxtTarget = ordr.TargetRange.String()
case "ip":
ordr.TxtTarget = ordr.TargetIP.String()
case "as":
ordr.TxtTarget = fmt.Sprintf("ban as %d (unsupported)", ordr.TargetAS)
case "country":
ordr.TxtTarget = fmt.Sprintf("ban country %s (unsupported)", ordr.TargetCountry)
default:
log.Errorf("Unknown remediation scope '%s'", sig.Labels["remediation_Scope"])
return nil, fmt.Errorf("unknown remediation scope"), nil
}
//Set deadline
ordr.Until = sig.Stop_at.Add(prof.Remediation.TimeDuration)
return &ordr, nil, warn
}
func (o *Output) FlushAll() {
if o.API != nil {
if err := o.API.Flush(); err != nil {
log.Errorf("Failing API flush : %s", err)
}
}
if o.bManager != nil {
if err := o.bManager.Flush(); err != nil {
log.Errorf("Failing Sqlite flush : %s", err)
}
}
}
func (o *Output) ProcessOutput(sig types.SignalOccurence, profiles []types.Profile) error {
var logger *log.Entry
if sig.Source != nil {
logger = log.WithFields(log.Fields{
"source_ip": sig.Source.Ip.String(),
"scenario": sig.Scenario,
"bucket_id": sig.Bucket_id,
"event_time": sig.Stop_at,
})
} else {
logger = log.WithFields(log.Fields{
"scenario": sig.Scenario,
"bucket_id": sig.Bucket_id,
"event_time": sig.Stop_at,
})
}
for _, profile := range profiles {
if profile.RunTimeFilter != nil {
//Evaluate node's filter
output, err := expr.Run(profile.RunTimeFilter, exprhelpers.GetExprEnv(map[string]interface{}{"sig": sig}))
if err != nil {
logger.Warningf("failed to run filter : %v", err)
continue
}
switch out := output.(type) {
case bool:
/* filter returned false, don't process Node */
if !out {
logger.Debugf("eval(FALSE) '%s'", profile.Filter)
continue
}
default:
logger.Warningf("Expr '%s' returned non-bool", profile.Filter)
continue
}
logger.Debugf("eval(TRUE) '%s'", profile.Filter)
}
/*the filter was ok*/
ordr, err, warn := OvflwToOrder(sig, profile)
if err != nil {
logger.Errorf("Unable to turn Overflow to Order : %v", err)
return err
}
if warn != nil {
logger.Infof("node warning : %s", warn)
}
if ordr != nil {
bans, err := types.OrderToApplications(ordr)
if err != nil {
logger.Errorf("Error turning order to ban applications : %v", err)
return err
}
logger.Warningf("%s triggered a %s %s %s remediation for [%s]", ordr.TxtTarget, ordr.Until.Sub(sig.Stop_at), ordr.Scope, ordr.MeasureType, sig.Scenario)
sig.BanApplications = bans
} else {
//Order didn't lead to concrete bans
logger.Infof("Processing Overflow with no decisions %s", sig.Alert_message)
}
// if ApiPush is nil (not specified in profile configuration) we use global api config (from default.yaml)
if profile.ApiPush == nil {
if o.API != nil { // if API is not nil, we can push
if err = o.API.AppendSignal((sig)); err != nil {
return fmt.Errorf("failed to append signal : %s", err)
}
}
}
for _, outputConfig := range profile.OutputConfigs {
if pluginName, ok := outputConfig["plugin"]; ok {
if o.bManager.IsBackendPlugin(pluginName) {
if toStore, ok := outputConfig["store"]; ok {
boolConv, err := strconv.ParseBool(toStore)
if err != nil {
log.Errorf("unable to parse boolean value of store configuration '%s' : %s", toStore, err)
}
if !boolConv {
continue
}
}
if err = o.bManager.InsertOnePlugin(sig, pluginName); err != nil {
return fmt.Errorf("failed to insert plugin %s : %s", pluginName, err)
}
}
}
}
}
return nil
}
func LoadOutputProfiles(profileConfig string) ([]types.Profile, error) {
var (
profiles []types.Profile
)
yamlFile, err := os.Open(profileConfig)
if err != nil {
log.Errorf("Can't access parsing configuration file with '%v'.", err)
return nil, err
}
//process the yaml
dec := yaml.NewDecoder(yamlFile)
dec.SetStrict(true)
for {
profile := types.Profile{}
err = dec.Decode(&profile)
if err != nil {
if err == io.EOF {
log.Tracef("End of yaml file")
break
}
log.Errorf("Error decoding profile configuration file with '%s': %v", profileConfig, err)
return nil, err
}
//compile filter if present
if profile.Filter != "" {
profile.RunTimeFilter, err = expr.Compile(profile.Filter, expr.Env(exprhelpers.GetExprEnv(map[string]interface{}{"sig": &types.SignalOccurence{}})))
if err != nil {
log.Errorf("Compilation failed %v\n", err)
return nil, err
}
}
if profile.Remediation.Ban || profile.Remediation.Slow || profile.Remediation.Captcha {
profile.Remediation.TimeDuration, err = time.ParseDuration(profile.Remediation.Duration)
if err != nil {
log.Fatalf("Unable to parse profile duration '%s'", profile.Remediation.Duration)
}
}
//ensure we have outputs :)
if profile.OutputConfigs == nil {
log.Errorf("Profile has empty OutputConfigs")
return nil, err
}
profiles = append(profiles, profile)
}
/*Initialize individual connectors*/
return profiles, nil
}
func (o *Output) InitAPI(config map[string]string) error {
var err error
o.API = &cwapi.ApiCtx{}
log.Infof("API connector init")
err = o.API.Init(config["path"], config["profile"])
if err != nil {
log.Errorf("API init failed, won't push/pull : %v", err)
return err
}
return nil
}
func (o *Output) LoadAPIConfig(configFile string) error {
var err error
o.API = &cwapi.ApiCtx{}
err = o.API.LoadConfig(configFile)
if err != nil {
return err
}
return nil
}
func (o *Output) load(config *OutputFactory, isDaemon bool) error {
var err error
if config == nil {
return fmt.Errorf("missing output plugin configuration")
}
log.Debugf("loading backend plugins ...")
o.bManager, err = cwplugin.NewBackendPlugin(config.BackendFolder, isDaemon)
if err != nil {
return err
}
return nil
}
func (o *Output) Delete(target string) (int, error) {
nbDel, err := o.bManager.Delete(target)
return nbDel, err
}
func (o *Output) DeleteAll() error {
err := o.bManager.DeleteAll()
return err
}
func (o *Output) Insert(sig types.SignalOccurence) error {
err := o.bManager.Insert(sig)
return err
}
func (o *Output) Flush() error {
err := o.bManager.Flush()
return err
}
func (o *Output) ReadAT(timeAT time.Time) ([]map[string]string, error) {
ret, err := o.bManager.ReadAT(timeAT)
if err != nil {
return nil, err
}
return ret, nil
}
func NewOutput(config *OutputFactory, isDaemon bool) (*Output, error) {
var output Output
err := output.load(config, isDaemon)
if err != nil {
return nil, err
}
return &output, nil
}