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 }