8cca4346a5
Add a new datasource that: - Receives HTTP requests from remediation components - Apply rules on them to determine whether they are malicious or not - Rules can be evaluated in-band (the remediation component will block the request directly) or out-band (the RC will let the request through, but crowdsec can still process the rule matches with scenarios) The PR also adds support for 2 new hub items: - appsec-configs: Configure the Application Security Engine (which rules to load, in which phase) - appsec-rules: a rule that is added in the Application Security Engine (can use either our own format, or seclang) --------- Co-authored-by: alteredCoder <kevin@crowdsec.net> Co-authored-by: Sebastien Blot <sebastien@crowdsec.net> Co-authored-by: mmetc <92726601+mmetc@users.noreply.github.com> Co-authored-by: Marco Mariani <marco@crowdsec.net>
238 lines
5.8 KiB
Go
238 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fatih/color"
|
|
dto "github.com/prometheus/client_model/go"
|
|
"github.com/prometheus/prom2json"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/crowdsecurity/go-cs-lib/trace"
|
|
|
|
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
|
)
|
|
|
|
func ShowMetrics(hubItem *cwhub.Item) error {
|
|
switch hubItem.Type {
|
|
case cwhub.PARSERS:
|
|
metrics := GetParserMetric(csConfig.Cscli.PrometheusUrl, hubItem.Name)
|
|
parserMetricsTable(color.Output, hubItem.Name, metrics)
|
|
case cwhub.SCENARIOS:
|
|
metrics := GetScenarioMetric(csConfig.Cscli.PrometheusUrl, hubItem.Name)
|
|
scenarioMetricsTable(color.Output, hubItem.Name, metrics)
|
|
case cwhub.COLLECTIONS:
|
|
for _, sub := range hubItem.SubItems() {
|
|
if err := ShowMetrics(sub); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case cwhub.APPSEC_RULES:
|
|
log.Error("FIXME: not implemented yet")
|
|
default:
|
|
// no metrics for this item type
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetParserMetric is a complete rip from prom2json
|
|
func GetParserMetric(url string, itemName string) map[string]map[string]int {
|
|
stats := make(map[string]map[string]int)
|
|
|
|
result := GetPrometheusMetric(url)
|
|
for idx, fam := range result {
|
|
if !strings.HasPrefix(fam.Name, "cs_") {
|
|
continue
|
|
}
|
|
log.Tracef("round %d", idx)
|
|
for _, m := range fam.Metrics {
|
|
metric, ok := m.(prom2json.Metric)
|
|
if !ok {
|
|
log.Debugf("failed to convert metric to prom2json.Metric")
|
|
continue
|
|
}
|
|
name, ok := metric.Labels["name"]
|
|
if !ok {
|
|
log.Debugf("no name in Metric %v", metric.Labels)
|
|
}
|
|
if name != itemName {
|
|
continue
|
|
}
|
|
source, ok := metric.Labels["source"]
|
|
if !ok {
|
|
log.Debugf("no source in Metric %v", metric.Labels)
|
|
} else {
|
|
if srctype, ok := metric.Labels["type"]; ok {
|
|
source = srctype + ":" + source
|
|
}
|
|
}
|
|
value := m.(prom2json.Metric).Value
|
|
fval, err := strconv.ParseFloat(value, 32)
|
|
if err != nil {
|
|
log.Errorf("Unexpected int value %s : %s", value, err)
|
|
continue
|
|
}
|
|
ival := int(fval)
|
|
|
|
switch fam.Name {
|
|
case "cs_reader_hits_total":
|
|
if _, ok := stats[source]; !ok {
|
|
stats[source] = make(map[string]int)
|
|
stats[source]["parsed"] = 0
|
|
stats[source]["reads"] = 0
|
|
stats[source]["unparsed"] = 0
|
|
stats[source]["hits"] = 0
|
|
}
|
|
stats[source]["reads"] += ival
|
|
case "cs_parser_hits_ok_total":
|
|
if _, ok := stats[source]; !ok {
|
|
stats[source] = make(map[string]int)
|
|
}
|
|
stats[source]["parsed"] += ival
|
|
case "cs_parser_hits_ko_total":
|
|
if _, ok := stats[source]; !ok {
|
|
stats[source] = make(map[string]int)
|
|
}
|
|
stats[source]["unparsed"] += ival
|
|
case "cs_node_hits_total":
|
|
if _, ok := stats[source]; !ok {
|
|
stats[source] = make(map[string]int)
|
|
}
|
|
stats[source]["hits"] += ival
|
|
case "cs_node_hits_ok_total":
|
|
if _, ok := stats[source]; !ok {
|
|
stats[source] = make(map[string]int)
|
|
}
|
|
stats[source]["parsed"] += ival
|
|
case "cs_node_hits_ko_total":
|
|
if _, ok := stats[source]; !ok {
|
|
stats[source] = make(map[string]int)
|
|
}
|
|
stats[source]["unparsed"] += ival
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
return stats
|
|
}
|
|
|
|
func GetScenarioMetric(url string, itemName string) map[string]int {
|
|
stats := make(map[string]int)
|
|
|
|
stats["instantiation"] = 0
|
|
stats["curr_count"] = 0
|
|
stats["overflow"] = 0
|
|
stats["pour"] = 0
|
|
stats["underflow"] = 0
|
|
|
|
result := GetPrometheusMetric(url)
|
|
for idx, fam := range result {
|
|
if !strings.HasPrefix(fam.Name, "cs_") {
|
|
continue
|
|
}
|
|
log.Tracef("round %d", idx)
|
|
for _, m := range fam.Metrics {
|
|
metric, ok := m.(prom2json.Metric)
|
|
if !ok {
|
|
log.Debugf("failed to convert metric to prom2json.Metric")
|
|
continue
|
|
}
|
|
name, ok := metric.Labels["name"]
|
|
if !ok {
|
|
log.Debugf("no name in Metric %v", metric.Labels)
|
|
}
|
|
if name != itemName {
|
|
continue
|
|
}
|
|
value := m.(prom2json.Metric).Value
|
|
fval, err := strconv.ParseFloat(value, 32)
|
|
if err != nil {
|
|
log.Errorf("Unexpected int value %s : %s", value, err)
|
|
continue
|
|
}
|
|
ival := int(fval)
|
|
|
|
switch fam.Name {
|
|
case "cs_bucket_created_total":
|
|
stats["instantiation"] += ival
|
|
case "cs_buckets":
|
|
stats["curr_count"] += ival
|
|
case "cs_bucket_overflowed_total":
|
|
stats["overflow"] += ival
|
|
case "cs_bucket_poured_total":
|
|
stats["pour"] += ival
|
|
case "cs_bucket_underflowed_total":
|
|
stats["underflow"] += ival
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
return stats
|
|
}
|
|
|
|
func GetPrometheusMetric(url string) []*prom2json.Family {
|
|
mfChan := make(chan *dto.MetricFamily, 1024)
|
|
|
|
// Start with the DefaultTransport for sane defaults.
|
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
|
// Conservatively disable HTTP keep-alives as this program will only
|
|
// ever need a single HTTP request.
|
|
transport.DisableKeepAlives = true
|
|
// Timeout early if the server doesn't even return the headers.
|
|
transport.ResponseHeaderTimeout = time.Minute
|
|
|
|
go func() {
|
|
defer trace.CatchPanic("crowdsec/GetPrometheusMetric")
|
|
err := prom2json.FetchMetricFamilies(url, mfChan, transport)
|
|
if err != nil {
|
|
log.Fatalf("failed to fetch prometheus metrics : %v", err)
|
|
}
|
|
}()
|
|
|
|
result := []*prom2json.Family{}
|
|
for mf := range mfChan {
|
|
result = append(result, prom2json.NewFamily(mf))
|
|
}
|
|
log.Debugf("Finished reading prometheus output, %d entries", len(result))
|
|
|
|
return result
|
|
}
|
|
|
|
type unit struct {
|
|
value int64
|
|
symbol string
|
|
}
|
|
|
|
var ranges = []unit{
|
|
{value: 1e18, symbol: "E"},
|
|
{value: 1e15, symbol: "P"},
|
|
{value: 1e12, symbol: "T"},
|
|
{value: 1e9, symbol: "G"},
|
|
{value: 1e6, symbol: "M"},
|
|
{value: 1e3, symbol: "k"},
|
|
{value: 1, symbol: ""},
|
|
}
|
|
|
|
func formatNumber(num int) string {
|
|
goodUnit := unit{}
|
|
for _, u := range ranges {
|
|
if int64(num) >= u.value {
|
|
goodUnit = u
|
|
break
|
|
}
|
|
}
|
|
|
|
if goodUnit.value == 1 {
|
|
return fmt.Sprintf("%d%s", num, goodUnit.symbol)
|
|
}
|
|
|
|
res := math.Round(float64(num)/float64(goodUnit.value)*100) / 100
|
|
return fmt.Sprintf("%.2f%s", res, goodUnit.symbol)
|
|
}
|