utils.go 10 KB

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