utils.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. package appsecacquisition
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "time"
  6. "github.com/crowdsecurity/coraza/v3/collection"
  7. "github.com/crowdsecurity/coraza/v3/types/variables"
  8. "github.com/crowdsecurity/crowdsec/pkg/appsec"
  9. "github.com/crowdsecurity/crowdsec/pkg/models"
  10. "github.com/crowdsecurity/crowdsec/pkg/types"
  11. "github.com/crowdsecurity/go-cs-lib/ptr"
  12. "github.com/prometheus/client_golang/prometheus"
  13. log "github.com/sirupsen/logrus"
  14. )
  15. func AppsecEventGeneration(inEvt types.Event) (*types.Event, error) {
  16. //if the request didnd't trigger inband rules, we don't want to generate an event to LAPI/CAPI
  17. if !inEvt.Appsec.HasInBandMatches {
  18. return nil, nil
  19. }
  20. evt := types.Event{}
  21. evt.Type = types.APPSEC
  22. evt.Process = true
  23. source := models.Source{
  24. Value: ptr.Of(inEvt.Parsed["source_ip"]),
  25. IP: inEvt.Parsed["source_ip"],
  26. Scope: ptr.Of(types.Ip),
  27. }
  28. evt.Overflow.Sources = make(map[string]models.Source)
  29. evt.Overflow.Sources["ip"] = source
  30. alert := models.Alert{}
  31. alert.Capacity = ptr.Of(int32(1))
  32. alert.Events = make([]*models.Event, 0)
  33. alert.Meta = make(models.Meta, 0)
  34. for _, key := range []string{"target_uri", "method"} {
  35. valueByte, err := json.Marshal([]string{inEvt.Parsed[key]})
  36. if err != nil {
  37. log.Debugf("unable to serialize key %s", key)
  38. continue
  39. }
  40. meta := models.MetaItems0{
  41. Key: key,
  42. Value: string(valueByte),
  43. }
  44. alert.Meta = append(alert.Meta, &meta)
  45. }
  46. matchedZones := inEvt.Appsec.GetMatchedZones()
  47. if matchedZones != nil {
  48. valueByte, err := json.Marshal(matchedZones)
  49. if err != nil {
  50. log.Debugf("unable to serialize key matched_zones")
  51. } else {
  52. meta := models.MetaItems0{
  53. Key: "matched_zones",
  54. Value: string(valueByte),
  55. }
  56. alert.Meta = append(alert.Meta, &meta)
  57. }
  58. }
  59. alert.EventsCount = ptr.Of(int32(1))
  60. alert.Leakspeed = ptr.Of("")
  61. alert.Scenario = ptr.Of(inEvt.Appsec.MatchedRules.GetName())
  62. alert.ScenarioHash = ptr.Of(inEvt.Appsec.MatchedRules.GetHash())
  63. alert.ScenarioVersion = ptr.Of(inEvt.Appsec.MatchedRules.GetVersion())
  64. alert.Simulated = ptr.Of(false)
  65. alert.Source = &source
  66. msg := fmt.Sprintf("AppSec block: %s from %s (%s)", inEvt.Appsec.MatchedRules.GetName(),
  67. alert.Source.IP, inEvt.Parsed["remediation_cmpt_ip"])
  68. alert.Message = &msg
  69. alert.StartAt = ptr.Of(time.Now().UTC().Format(time.RFC3339))
  70. alert.StopAt = ptr.Of(time.Now().UTC().Format(time.RFC3339))
  71. evt.Overflow.APIAlerts = []models.Alert{alert}
  72. evt.Overflow.Alert = &alert
  73. return &evt, nil
  74. }
  75. func EventFromRequest(r *appsec.ParsedRequest, labels map[string]string) (types.Event, error) {
  76. evt := types.Event{}
  77. //we might want to change this based on in-band vs out-of-band ?
  78. evt.Type = types.LOG
  79. evt.ExpectMode = types.LIVE
  80. //def needs fixing
  81. evt.Stage = "s00-raw"
  82. evt.Parsed = map[string]string{
  83. "source_ip": r.ClientIP,
  84. "target_host": r.Host,
  85. "target_uri": r.URI,
  86. "method": r.Method,
  87. "req_uuid": r.Tx.ID(),
  88. "source": "crowdsec-appsec",
  89. "remediation_cmpt_ip": r.RemoteAddrNormalized,
  90. //TBD:
  91. //http_status
  92. //user_agent
  93. }
  94. evt.Line = types.Line{
  95. Time: time.Now(),
  96. //should we add some info like listen addr/port/path ?
  97. Labels: labels,
  98. Process: true,
  99. Module: "appsec",
  100. Src: "appsec",
  101. Raw: "dummy-appsec-data", //we discard empty Line.Raw items :)
  102. }
  103. evt.Appsec = types.AppsecEvent{}
  104. return evt, nil
  105. }
  106. func LogAppsecEvent(evt *types.Event, logger *log.Entry) {
  107. req := evt.Parsed["target_uri"]
  108. if len(req) > 12 {
  109. req = req[:10] + ".."
  110. }
  111. if evt.Meta["appsec_interrupted"] == "true" {
  112. logger.WithFields(log.Fields{
  113. "module": "appsec",
  114. "source": evt.Parsed["source_ip"],
  115. "target_uri": req,
  116. }).Infof("%s blocked on %s (%d rules) [%v]", evt.Parsed["source_ip"], req, len(evt.Appsec.MatchedRules), evt.Appsec.GetRuleIDs())
  117. } else if evt.Parsed["outofband_interrupted"] == "true" {
  118. logger.WithFields(log.Fields{
  119. "module": "appsec",
  120. "source": evt.Parsed["source_ip"],
  121. "target_uri": req,
  122. }).Infof("%s out-of-band blocking rules on %s (%d rules) [%v]", evt.Parsed["source_ip"], req, len(evt.Appsec.MatchedRules), evt.Appsec.GetRuleIDs())
  123. } else {
  124. logger.WithFields(log.Fields{
  125. "module": "appsec",
  126. "source": evt.Parsed["source_ip"],
  127. "target_uri": req,
  128. }).Debugf("%s triggered non-blocking rules on %s (%d rules) [%v]", evt.Parsed["source_ip"], req, len(evt.Appsec.MatchedRules), evt.Appsec.GetRuleIDs())
  129. }
  130. }
  131. func (r *AppsecRunner) AccumulateTxToEvent(evt *types.Event, req *appsec.ParsedRequest) error {
  132. if evt == nil {
  133. //an error was already emitted, let's not spam the logs
  134. return nil
  135. }
  136. if !req.Tx.IsInterrupted() {
  137. //if the phase didn't generate an interruption, we don't have anything to add to the event
  138. return nil
  139. }
  140. //if one interruption was generated, event is good for processing :)
  141. evt.Process = true
  142. if evt.Meta == nil {
  143. evt.Meta = map[string]string{}
  144. }
  145. if evt.Parsed == nil {
  146. evt.Parsed = map[string]string{}
  147. }
  148. if req.IsInBand {
  149. evt.Meta["appsec_interrupted"] = "true"
  150. evt.Meta["appsec_action"] = req.Tx.Interruption().Action
  151. evt.Parsed["inband_interrupted"] = "true"
  152. evt.Parsed["inband_action"] = req.Tx.Interruption().Action
  153. } else {
  154. evt.Parsed["outofband_interrupted"] = "true"
  155. evt.Parsed["outofband_action"] = req.Tx.Interruption().Action
  156. }
  157. if evt.Appsec.Vars == nil {
  158. evt.Appsec.Vars = map[string]string{}
  159. }
  160. req.Tx.Variables().All(func(v variables.RuleVariable, col collection.Collection) bool {
  161. for _, variable := range col.FindAll() {
  162. key := variable.Variable().Name()
  163. if variable.Key() != "" {
  164. key += "." + variable.Key()
  165. }
  166. if variable.Value() == "" {
  167. continue
  168. }
  169. for _, collectionToKeep := range r.AppsecRuntime.CompiledVariablesTracking {
  170. match := collectionToKeep.MatchString(key)
  171. if match {
  172. evt.Appsec.Vars[key] = variable.Value()
  173. r.logger.Debugf("%s.%s = %s", variable.Variable().Name(), variable.Key(), variable.Value())
  174. } else {
  175. r.logger.Debugf("%s.%s != %s (%s) (not kept)", variable.Variable().Name(), variable.Key(), collectionToKeep, variable.Value())
  176. }
  177. }
  178. }
  179. return true
  180. })
  181. for _, rule := range req.Tx.MatchedRules() {
  182. if rule.Message() == "" || rule.DisruptiveAction() == "pass" || rule.DisruptiveAction() == "allow" {
  183. r.logger.Tracef("discarding rule %d (action: %s)", rule.Rule().ID(), rule.DisruptiveAction())
  184. continue
  185. }
  186. kind := "outofband"
  187. if req.IsInBand {
  188. kind = "inband"
  189. evt.Appsec.HasInBandMatches = true
  190. } else {
  191. evt.Appsec.HasOutBandMatches = true
  192. }
  193. var name string
  194. version := ""
  195. hash := ""
  196. ruleNameProm := fmt.Sprintf("%d", rule.Rule().ID())
  197. if details, ok := appsec.AppsecRulesDetails[rule.Rule().ID()]; ok {
  198. //Only set them for custom rules, not for rules written in seclang
  199. name = details.Name
  200. version = details.Version
  201. hash = details.Hash
  202. ruleNameProm = details.Name
  203. r.logger.Debugf("custom rule for event, setting name: %s, version: %s, hash: %s", name, version, hash)
  204. } else {
  205. name = fmt.Sprintf("native_rule:%d", rule.Rule().ID())
  206. }
  207. AppsecRuleHits.With(prometheus.Labels{"rule_name": ruleNameProm, "type": kind, "source": req.RemoteAddrNormalized, "appsec_engine": req.AppsecEngine}).Inc()
  208. matchedZones := make([]string, 0)
  209. for _, matchData := range rule.MatchedDatas() {
  210. zone := matchData.Variable().Name()
  211. varName := matchData.Key()
  212. if varName != "" {
  213. zone += "." + varName
  214. }
  215. matchedZones = append(matchedZones, zone)
  216. }
  217. corazaRule := map[string]interface{}{
  218. "id": rule.Rule().ID(),
  219. "uri": evt.Parsed["uri"],
  220. "rule_type": kind,
  221. "method": evt.Parsed["method"],
  222. "disruptive": rule.Disruptive(),
  223. "tags": rule.Rule().Tags(),
  224. "file": rule.Rule().File(),
  225. "file_line": rule.Rule().Line(),
  226. "revision": rule.Rule().Revision(),
  227. "secmark": rule.Rule().SecMark(),
  228. "accuracy": rule.Rule().Accuracy(),
  229. "msg": rule.Message(),
  230. "severity": rule.Rule().Severity().String(),
  231. "name": name,
  232. "hash": hash,
  233. "version": version,
  234. "matched_zones": matchedZones,
  235. }
  236. evt.Appsec.MatchedRules = append(evt.Appsec.MatchedRules, corazaRule)
  237. }
  238. return nil
  239. }