github.com/crowdsecurity/crowdsec@v1.6.1/pkg/acquisition/modules/appsec/appsec.go (about)

     1  package appsecacquisition
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net"
     8  	"net/http"
     9  	"os"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
    14  
    15  	"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
    16  	"github.com/crowdsecurity/crowdsec/pkg/appsec"
    17  	"github.com/crowdsecurity/crowdsec/pkg/types"
    18  	"github.com/crowdsecurity/go-cs-lib/trace"
    19  	"github.com/google/uuid"
    20  	"github.com/pkg/errors"
    21  	"github.com/prometheus/client_golang/prometheus"
    22  	log "github.com/sirupsen/logrus"
    23  	"gopkg.in/tomb.v2"
    24  	"gopkg.in/yaml.v2"
    25  )
    26  
    27  const (
    28  	InBand    = "inband"
    29  	OutOfBand = "outofband"
    30  )
    31  
    32  var (
    33  	DefaultAuthCacheDuration = (1 * time.Minute)
    34  )
    35  
    36  // configuration structure of the acquis for the application security engine
    37  type AppsecSourceConfig struct {
    38  	ListenAddr                        string         `yaml:"listen_addr"`
    39  	ListenSocket                      string         `yaml:"listen_socket"`
    40  	CertFilePath                      string         `yaml:"cert_file"`
    41  	KeyFilePath                       string         `yaml:"key_file"`
    42  	Path                              string         `yaml:"path"`
    43  	Routines                          int            `yaml:"routines"`
    44  	AppsecConfig                      string         `yaml:"appsec_config"`
    45  	AppsecConfigPath                  string         `yaml:"appsec_config_path"`
    46  	AuthCacheDuration                 *time.Duration `yaml:"auth_cache_duration"`
    47  	configuration.DataSourceCommonCfg `yaml:",inline"`
    48  }
    49  
    50  // runtime structure of AppsecSourceConfig
    51  type AppsecSource struct {
    52  	metricsLevel  int
    53  	config        AppsecSourceConfig
    54  	logger        *log.Entry
    55  	mux           *http.ServeMux
    56  	server        *http.Server
    57  	outChan       chan types.Event
    58  	InChan        chan appsec.ParsedRequest
    59  	AppsecRuntime *appsec.AppsecRuntimeConfig
    60  	AppsecConfigs map[string]appsec.AppsecConfig
    61  	lapiURL       string
    62  	AuthCache     AuthCache
    63  	AppsecRunners []AppsecRunner //one for each go-routine
    64  }
    65  
    66  // Struct to handle cache of authentication
    67  type AuthCache struct {
    68  	APIKeys map[string]time.Time
    69  	mu      sync.RWMutex
    70  }
    71  
    72  func NewAuthCache() AuthCache {
    73  	return AuthCache{
    74  		APIKeys: make(map[string]time.Time, 0),
    75  		mu:      sync.RWMutex{},
    76  	}
    77  }
    78  
    79  func (ac *AuthCache) Set(apiKey string, expiration time.Time) {
    80  	ac.mu.Lock()
    81  	ac.APIKeys[apiKey] = expiration
    82  	ac.mu.Unlock()
    83  }
    84  
    85  func (ac *AuthCache) Get(apiKey string) (time.Time, bool) {
    86  	ac.mu.RLock()
    87  	expiration, exists := ac.APIKeys[apiKey]
    88  	ac.mu.RUnlock()
    89  	return expiration, exists
    90  }
    91  
    92  // @tko + @sbl : we might want to get rid of that or improve it
    93  type BodyResponse struct {
    94  	Action string `json:"action"`
    95  }
    96  
    97  func (w *AppsecSource) UnmarshalConfig(yamlConfig []byte) error {
    98  
    99  	err := yaml.UnmarshalStrict(yamlConfig, &w.config)
   100  	if err != nil {
   101  		return errors.Wrap(err, "Cannot parse appsec configuration")
   102  	}
   103  
   104  	if w.config.ListenAddr == "" && w.config.ListenSocket == "" {
   105  		w.config.ListenAddr = "127.0.0.1:7422"
   106  	}
   107  
   108  	if w.config.Path == "" {
   109  		w.config.Path = "/"
   110  	}
   111  
   112  	if w.config.Path[0] != '/' {
   113  		w.config.Path = "/" + w.config.Path
   114  	}
   115  
   116  	if w.config.Mode == "" {
   117  		w.config.Mode = configuration.TAIL_MODE
   118  	}
   119  
   120  	// always have at least one appsec routine
   121  	if w.config.Routines == 0 {
   122  		w.config.Routines = 1
   123  	}
   124  
   125  	if w.config.AppsecConfig == "" && w.config.AppsecConfigPath == "" {
   126  		return fmt.Errorf("appsec_config or appsec_config_path must be set")
   127  	}
   128  
   129  	if w.config.Name == "" {
   130  		if w.config.ListenSocket != "" && w.config.ListenAddr == "" {
   131  			w.config.Name = w.config.ListenSocket
   132  		}
   133  		if w.config.ListenSocket == "" {
   134  			w.config.Name = fmt.Sprintf("%s%s", w.config.ListenAddr, w.config.Path)
   135  		}
   136  	}
   137  
   138  	csConfig := csconfig.GetConfig()
   139  	w.lapiURL = fmt.Sprintf("%sv1/decisions/stream", csConfig.API.Client.Credentials.URL)
   140  	w.AuthCache = NewAuthCache()
   141  
   142  	return nil
   143  }
   144  
   145  func (w *AppsecSource) GetMetrics() []prometheus.Collector {
   146  	return []prometheus.Collector{AppsecReqCounter, AppsecBlockCounter, AppsecRuleHits, AppsecOutbandParsingHistogram, AppsecInbandParsingHistogram, AppsecGlobalParsingHistogram}
   147  }
   148  
   149  func (w *AppsecSource) GetAggregMetrics() []prometheus.Collector {
   150  	return []prometheus.Collector{AppsecReqCounter, AppsecBlockCounter, AppsecRuleHits, AppsecOutbandParsingHistogram, AppsecInbandParsingHistogram, AppsecGlobalParsingHistogram}
   151  }
   152  
   153  func (w *AppsecSource) Configure(yamlConfig []byte, logger *log.Entry, MetricsLevel int) error {
   154  	err := w.UnmarshalConfig(yamlConfig)
   155  	if err != nil {
   156  		return errors.Wrap(err, "unable to parse appsec configuration")
   157  	}
   158  	w.logger = logger
   159  	w.metricsLevel = MetricsLevel
   160  	w.logger.Tracef("Appsec configuration: %+v", w.config)
   161  
   162  	if w.config.AuthCacheDuration == nil {
   163  		w.config.AuthCacheDuration = &DefaultAuthCacheDuration
   164  		w.logger.Infof("Cache duration for auth not set, using default: %v", *w.config.AuthCacheDuration)
   165  	}
   166  
   167  	w.mux = http.NewServeMux()
   168  
   169  	w.server = &http.Server{
   170  		Addr:    w.config.ListenAddr,
   171  		Handler: w.mux,
   172  	}
   173  
   174  	w.InChan = make(chan appsec.ParsedRequest)
   175  	appsecCfg := appsec.AppsecConfig{Logger: w.logger.WithField("component", "appsec_config")}
   176  
   177  	//let's load the associated appsec_config:
   178  	if w.config.AppsecConfigPath != "" {
   179  		err := appsecCfg.LoadByPath(w.config.AppsecConfigPath)
   180  		if err != nil {
   181  			return fmt.Errorf("unable to load appsec_config : %s", err)
   182  		}
   183  	} else if w.config.AppsecConfig != "" {
   184  		err := appsecCfg.Load(w.config.AppsecConfig)
   185  		if err != nil {
   186  			return fmt.Errorf("unable to load appsec_config : %s", err)
   187  		}
   188  	} else {
   189  		return fmt.Errorf("no appsec_config provided")
   190  	}
   191  
   192  	w.AppsecRuntime, err = appsecCfg.Build()
   193  	if err != nil {
   194  		return fmt.Errorf("unable to build appsec_config : %s", err)
   195  	}
   196  
   197  	err = w.AppsecRuntime.ProcessOnLoadRules()
   198  
   199  	if err != nil {
   200  		return fmt.Errorf("unable to process on load rules : %s", err)
   201  	}
   202  
   203  	w.AppsecRunners = make([]AppsecRunner, w.config.Routines)
   204  
   205  	for nbRoutine := 0; nbRoutine < w.config.Routines; nbRoutine++ {
   206  		appsecRunnerUUID := uuid.New().String()
   207  		//we copy AppsecRutime for each runner
   208  		wrt := *w.AppsecRuntime
   209  		wrt.Logger = w.logger.Dup().WithField("runner_uuid", appsecRunnerUUID)
   210  		runner := AppsecRunner{
   211  			inChan: w.InChan,
   212  			UUID:   appsecRunnerUUID,
   213  			logger: w.logger.WithFields(log.Fields{
   214  				"runner_uuid": appsecRunnerUUID,
   215  			}),
   216  			AppsecRuntime: &wrt,
   217  			Labels:        w.config.Labels,
   218  		}
   219  		err := runner.Init(appsecCfg.GetDataDir())
   220  		if err != nil {
   221  			return fmt.Errorf("unable to initialize runner : %s", err)
   222  		}
   223  		w.AppsecRunners[nbRoutine] = runner
   224  	}
   225  
   226  	w.logger.Infof("Created %d appsec runners", len(w.AppsecRunners))
   227  
   228  	//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
   229  	w.mux.HandleFunc(w.config.Path, w.appsecHandler)
   230  	return nil
   231  }
   232  
   233  func (w *AppsecSource) ConfigureByDSN(dsn string, labels map[string]string, logger *log.Entry, uuid string) error {
   234  	return fmt.Errorf("AppSec datasource does not support command line acquisition")
   235  }
   236  
   237  func (w *AppsecSource) GetMode() string {
   238  	return w.config.Mode
   239  }
   240  
   241  func (w *AppsecSource) GetName() string {
   242  	return "appsec"
   243  }
   244  
   245  func (w *AppsecSource) OneShotAcquisition(out chan types.Event, t *tomb.Tomb) error {
   246  	return fmt.Errorf("AppSec datasource does not support command line acquisition")
   247  }
   248  
   249  func (w *AppsecSource) StreamingAcquisition(out chan types.Event, t *tomb.Tomb) error {
   250  	w.outChan = out
   251  	t.Go(func() error {
   252  		defer trace.CatchPanic("crowdsec/acquis/appsec/live")
   253  
   254  		w.logger.Infof("%d appsec runner to start", len(w.AppsecRunners))
   255  		for _, runner := range w.AppsecRunners {
   256  			runner := runner
   257  			runner.outChan = out
   258  			t.Go(func() error {
   259  				defer trace.CatchPanic("crowdsec/acquis/appsec/live/runner")
   260  				return runner.Run(t)
   261  			})
   262  		}
   263  		t.Go(func() error {
   264  			if w.config.ListenSocket != "" {
   265  				w.logger.Infof("creating unix socket %s", w.config.ListenSocket)
   266  				_ = os.RemoveAll(w.config.ListenSocket)
   267  				listener, err := net.Listen("unix", w.config.ListenSocket)
   268  				if err != nil {
   269  					return errors.Wrap(err, "Appsec server failed")
   270  				}
   271  				defer listener.Close()
   272  				if w.config.CertFilePath != "" && w.config.KeyFilePath != "" {
   273  					err = w.server.ServeTLS(listener, w.config.CertFilePath, w.config.KeyFilePath)
   274  				} else {
   275  					err = w.server.Serve(listener)
   276  				}
   277  				if err != nil && err != http.ErrServerClosed {
   278  					return errors.Wrap(err, "Appsec server failed")
   279  				}
   280  			}
   281  			return nil
   282  		})
   283  		t.Go(func() error {
   284  			var err error
   285  			if w.config.ListenAddr != "" {
   286  				w.logger.Infof("creating TCP server on %s", w.config.ListenAddr)
   287  				if w.config.CertFilePath != "" && w.config.KeyFilePath != "" {
   288  					err = w.server.ListenAndServeTLS(w.config.CertFilePath, w.config.KeyFilePath)
   289  				} else {
   290  					err = w.server.ListenAndServe()
   291  				}
   292  
   293  				if err != nil && err != http.ErrServerClosed {
   294  					return errors.Wrap(err, "Appsec server failed")
   295  				}
   296  			}
   297  			return nil
   298  		})
   299  		<-t.Dying()
   300  		w.logger.Info("Shutting down Appsec server")
   301  		//xx let's clean up the appsec runners :)
   302  		appsec.AppsecRulesDetails = make(map[int]appsec.RulesDetails)
   303  		w.server.Shutdown(context.TODO())
   304  		return nil
   305  	})
   306  	return nil
   307  }
   308  
   309  func (w *AppsecSource) CanRun() error {
   310  	return nil
   311  }
   312  
   313  func (w *AppsecSource) GetUuid() string {
   314  	return w.config.UniqueId
   315  }
   316  
   317  func (w *AppsecSource) Dump() interface{} {
   318  	return w
   319  }
   320  
   321  func (w *AppsecSource) IsAuth(apiKey string) bool {
   322  	client := &http.Client{
   323  		Timeout: 200 * time.Millisecond,
   324  	}
   325  
   326  	req, err := http.NewRequest(http.MethodHead, w.lapiURL, nil)
   327  	if err != nil {
   328  		log.Errorf("Error creating request: %s", err)
   329  		return false
   330  	}
   331  
   332  	req.Header.Add("X-Api-Key", apiKey)
   333  	resp, err := client.Do(req)
   334  	if err != nil {
   335  		log.Errorf("Error performing request: %s", err)
   336  		return false
   337  	}
   338  	defer resp.Body.Close()
   339  
   340  	return resp.StatusCode == http.StatusOK
   341  
   342  }
   343  
   344  // should this be in the runner ?
   345  func (w *AppsecSource) appsecHandler(rw http.ResponseWriter, r *http.Request) {
   346  	w.logger.Debugf("Received request from '%s' on %s", r.RemoteAddr, r.URL.Path)
   347  
   348  	apiKey := r.Header.Get(appsec.APIKeyHeaderName)
   349  	clientIP := r.Header.Get(appsec.IPHeaderName)
   350  	remoteIP := r.RemoteAddr
   351  	if apiKey == "" {
   352  		w.logger.Errorf("Unauthorized request from '%s' (real IP = %s)", remoteIP, clientIP)
   353  		rw.WriteHeader(http.StatusUnauthorized)
   354  		return
   355  	}
   356  	expiration, exists := w.AuthCache.Get(apiKey)
   357  	// if the apiKey is not in cache or has expired, just recheck the auth
   358  	if !exists || time.Now().After(expiration) {
   359  		if !w.IsAuth(apiKey) {
   360  			rw.WriteHeader(http.StatusUnauthorized)
   361  			w.logger.Errorf("Unauthorized request from '%s' (real IP = %s)", remoteIP, clientIP)
   362  			return
   363  		}
   364  
   365  		// apiKey is valid, store it in cache
   366  		w.AuthCache.Set(apiKey, time.Now().Add(*w.config.AuthCacheDuration))
   367  	}
   368  
   369  	// parse the request only once
   370  	parsedRequest, err := appsec.NewParsedRequestFromRequest(r, w.logger)
   371  	if err != nil {
   372  		w.logger.Errorf("%s", err)
   373  		rw.WriteHeader(http.StatusInternalServerError)
   374  		return
   375  	}
   376  	parsedRequest.AppsecEngine = w.config.Name
   377  
   378  	logger := w.logger.WithFields(log.Fields{
   379  		"request_uuid": parsedRequest.UUID,
   380  		"client_ip":    parsedRequest.ClientIP,
   381  	})
   382  
   383  	AppsecReqCounter.With(prometheus.Labels{"source": parsedRequest.RemoteAddrNormalized, "appsec_engine": parsedRequest.AppsecEngine}).Inc()
   384  
   385  	w.InChan <- parsedRequest
   386  
   387  	/*
   388  		response is a copy of w.AppSecRuntime.Response that is safe to use.
   389  		As OutOfBand might still be running, the original one can be modified
   390  	*/
   391  	response := <-parsedRequest.ResponseChannel
   392  
   393  	if response.InBandInterrupt {
   394  		AppsecBlockCounter.With(prometheus.Labels{"source": parsedRequest.RemoteAddrNormalized, "appsec_engine": parsedRequest.AppsecEngine}).Inc()
   395  	}
   396  
   397  	statusCode, appsecResponse := w.AppsecRuntime.GenerateResponse(response, logger)
   398  	logger.Debugf("Response: %+v", appsecResponse)
   399  
   400  	rw.WriteHeader(statusCode)
   401  	body, err := json.Marshal(appsecResponse)
   402  	if err != nil {
   403  		logger.Errorf("unable to marshal response: %s", err)
   404  		rw.WriteHeader(http.StatusInternalServerError)
   405  	} else {
   406  		rw.Write(body)
   407  	}
   408  
   409  }