From 7353d49e6cb9e93a0e891c0c8fac0dea5921d747 Mon Sep 17 00:00:00 2001 From: Dmitry Ponomaryov Date: Fri, 13 Sep 2024 16:30:51 +0500 Subject: [PATCH] init defectdojo-exporter --- Dockerfile | 14 + Makefile | 22 + README.md | 33 + cmd/defectdojo-exporter/go.mod | 17 + cmd/defectdojo-exporter/go.sum | 24 + cmd/defectdojo-exporter/main.go | 36 + config/config.go | 29 + config/go.mod | 5 + config/go.sum | 4 + configs/config.yaml | 3 + .../DefectDojo Exporter-1726226940845.json | 936 ++++++++++++++++++ go.work | 9 + go.work.sum | 24 + internal/collector/collector.go | 166 ++++ internal/collector/go.mod | 16 + internal/collector/go.sum | 22 + internal/defectdojo/defectdojo.go | 141 +++ internal/defectdojo/go.mod | 16 + internal/defectdojo/go.sum | 22 + internal/initializer/go.mod | 3 + internal/initializer/initializer.go | 51 + 21 files changed, 1593 insertions(+) create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/defectdojo-exporter/go.mod create mode 100644 cmd/defectdojo-exporter/go.sum create mode 100644 cmd/defectdojo-exporter/main.go create mode 100644 config/config.go create mode 100644 config/go.mod create mode 100644 config/go.sum create mode 100644 configs/config.yaml create mode 100644 dashboard/DefectDojo Exporter-1726226940845.json create mode 100644 go.work create mode 100644 go.work.sum create mode 100644 internal/collector/collector.go create mode 100644 internal/collector/go.mod create mode 100644 internal/collector/go.sum create mode 100644 internal/defectdojo/defectdojo.go create mode 100644 internal/defectdojo/go.mod create mode 100644 internal/defectdojo/go.sum create mode 100644 internal/initializer/go.mod create mode 100644 internal/initializer/initializer.go diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e39fd79 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM golang:1.22.5-alpine AS builder +WORKDIR /app + +COPY . ./ + +WORKDIR /app/cmd/defectdojo-exporter + +RUN apk add --no-cache binutils \ + && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o defectdojo-exporter main.go \ + && strip /app/cmd/defectdojo-exporter/defectdojo-exporter + +FROM alpine:3.20 + +COPY --from=builder /app/cmd/defectdojo-exporter/defectdojo-exporter /usr/local/bin/defectdojo-exporter diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c77b7b3 --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +# DOCKER TASKS + +DOCKER_IMAGE_NAME := exporter +DOCKER_IMAGE_TAG := defectdojo +DOCKER_CONTAINER_NAME := defectdojo-exporter + +.PHONY: all build run extract clean + +all: build run extract clean + +build: + docker build -t $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) . + +run: + docker run --name $(DOCKER_CONTAINER_NAME) $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) + +extract: + docker cp $(DOCKER_CONTAINER_NAME):/usr/local/bin/defectdojo-exporter ./defectdojo-exporter + +clean: + docker stop $(DOCKER_CONTAINER_NAME) || true + docker rm $(DOCKER_CONTAINER_NAME) || true diff --git a/README.md b/README.md new file mode 100644 index 0000000..84c5e3b --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Vulnerability Metrics Collector from [DefectDojo](https://github.com/DefectDojo/django-DefectDojo) + +## Metrics + +The application collects and exposes the following metrics: + +- `dojo_vulnerabilities_active`: Number of active vulnerabilities. +- `dojo_vulnerabilities_duplicate`: Number of duplicate vulnerabilities. +- `dojo_vulnerabilities_under_review`: Number of vulnerabilities under review. +- `dojo_vulnerabilities_false_positive`: Number of false positive vulnerabilities. +- `dojo_vulnerabilities_out_of_scope`: Number of vulnerabilities out of scope. +- `dojo_vulnerabilities_risk_accepted`: Number of vulnerabilities with risk accepted. +- `dojo_vulnerabilities_verified`: Number of verified vulnerabilities. +- `dojo_vulnerabilities_mitigated`: Number of mitigated vulnerabilities. + +## Lables + +- `product`: The name or identifier of the product associated with the vulnerabilities. +- `severity`: The severity level of the vulnerabilities, such as informational, low, medium, high, or critical. +- `cwe`: The Common Weakness Enumeration (CWE) identifier associated with the vulnerabilities. + +## Configuration + +Create a config.yaml file in the folder with the binary exporter to configure the following variables: + +```yaml +DD_TOKEN: "12345678901234567890" +DD_URL: "https://defectdojo.com" +# port for running exporter +PORT: 8080 +``` + +Once configured and running, the collector exposes the metrics at the /metrics endpoint. diff --git a/cmd/defectdojo-exporter/go.mod b/cmd/defectdojo-exporter/go.mod new file mode 100644 index 0000000..1e3f580 --- /dev/null +++ b/cmd/defectdojo-exporter/go.mod @@ -0,0 +1,17 @@ +module defectdojo-exporter + +go 1.22.5 + +require github.com/prometheus/client_golang v1.20.3 + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + golang.org/x/sys v0.22.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect +) diff --git a/cmd/defectdojo-exporter/go.sum b/cmd/defectdojo-exporter/go.sum new file mode 100644 index 0000000..b488e49 --- /dev/null +++ b/cmd/defectdojo-exporter/go.sum @@ -0,0 +1,24 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= +github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= diff --git a/cmd/defectdojo-exporter/main.go b/cmd/defectdojo-exporter/main.go new file mode 100644 index 0000000..cd0582d --- /dev/null +++ b/cmd/defectdojo-exporter/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "collector" + "config" + "defectdojo" + "fmt" + "log" + "net/http" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +func main() { + // load config + config := config.LoadConfig() + + // register metric + prometheus.MustRegister(defectdojo.VulnActiveGauge) + prometheus.MustRegister(defectdojo.VulnDuplicateGauge) + prometheus.MustRegister(defectdojo.VulnUnderReviewGauge) + prometheus.MustRegister(defectdojo.VulnFalsePositiveGauge) + prometheus.MustRegister(defectdojo.VulnOutOfScopeGauge) + prometheus.MustRegister(defectdojo.VulnRiskAcceptedGauge) + prometheus.MustRegister(defectdojo.VulnVerifiedGauge) + prometheus.MustRegister(defectdojo.VulnMitigatedGauge) + + // start exporter + go collector.CollectMetrics(config.DD_URL, config.DD_TOKEN) + + http.Handle("/metrics", promhttp.Handler()) + log.Printf("Starting server on :%d", config.PORT) + + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", config.PORT), nil)) +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..f13674d --- /dev/null +++ b/config/config.go @@ -0,0 +1,29 @@ +package config + +import ( + "log" + "os" + + "gopkg.in/yaml.v3" +) + +type Config struct { + DD_URL string `yaml:"DD_URL"` + DD_TOKEN string `yaml:"DD_TOKEN"` + PORT int `yaml:"PORT"` +} + +// LoadConfig load config.yaml +func LoadConfig() *Config { + file, err := os.ReadFile("config.yaml") + if err != nil { + log.Fatal("error reading config file: ", err) + } + + var config Config + if err := yaml.Unmarshal(file, &config); err != nil { + log.Fatal("error unmarshalling config file: ", err) + } + + return &config +} diff --git a/config/go.mod b/config/go.mod new file mode 100644 index 0000000..5d4cedd --- /dev/null +++ b/config/go.mod @@ -0,0 +1,5 @@ +module config + +go 1.22.5 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/config/go.sum b/config/go.sum new file mode 100644 index 0000000..a62c313 --- /dev/null +++ b/config/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/configs/config.yaml b/configs/config.yaml new file mode 100644 index 0000000..d6a3699 --- /dev/null +++ b/configs/config.yaml @@ -0,0 +1,3 @@ +DD_TOKEN: "12345678901234567890" +DD_URL: "https://defectdojo.com" +PORT: 8080 diff --git a/dashboard/DefectDojo Exporter-1726226940845.json b/dashboard/DefectDojo Exporter-1726226940845.json new file mode 100644 index 0000000..85166fb --- /dev/null +++ b/dashboard/DefectDojo Exporter-1726226940845.json @@ -0,0 +1,936 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 782, + "links": [ + { + "asDropdown": false, + "icon": "info", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": false, + "title": "Finding Status Definitions", + "tooltip": "", + "type": "link", + "url": "https://support.defectdojo.com/en/articles/9043697-finding-status-definitions" + } + ], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "af1438b4-d94c-460c-833b-b6fccbcbf934" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [ + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af1438b4-d94c-460c-833b-b6fccbcbf934" + }, + "editorMode": "code", + "expr": "sum by(product, severity) (dojo_vulnerabilities_active{product=~\"$product\", severity=~\"$severity\", cwe=~\"$cwe\"})", + "instant": false, + "legendFormat": "{{product}} - {{severity}}", + "range": true, + "refId": "A" + } + ], + "title": "Active", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "af1438b4-d94c-460c-833b-b6fccbcbf934" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 6, + "options": { + "legend": { + "calcs": [ + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af1438b4-d94c-460c-833b-b6fccbcbf934" + }, + "editorMode": "code", + "expr": "sum by(product, severity) (dojo_vulnerabilities_duplicate{product=~\"$product\", severity=~\"$severity\", cwe=~\"$cwe\"})", + "instant": false, + "legendFormat": "{{product}} - {{severity}}", + "range": true, + "refId": "A" + } + ], + "title": "Duplicate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "af1438b4-d94c-460c-833b-b6fccbcbf934" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 5, + "options": { + "legend": { + "calcs": [ + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af1438b4-d94c-460c-833b-b6fccbcbf934" + }, + "editorMode": "code", + "expr": "sum by(product, severity) (dojo_vulnerabilities_false_positive{product=~\"$product\", severity=~\"$severity\", cwe=~\"$cwe\"})", + "instant": false, + "legendFormat": "{{product}} - {{severity}}", + "range": true, + "refId": "A" + } + ], + "title": "False Positive", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "af1438b4-d94c-460c-833b-b6fccbcbf934" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "calcs": [ + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af1438b4-d94c-460c-833b-b6fccbcbf934" + }, + "editorMode": "code", + "expr": "sum by(product, severity) (dojo_vulnerabilities_out_of_scope{product=~\"$product\", severity=~\"$severity\", cwe=~\"$cwe\"})", + "instant": false, + "legendFormat": "{{product}} - {{severity}}", + "range": true, + "refId": "A" + } + ], + "title": "Out Of Scope", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "af1438b4-d94c-460c-833b-b6fccbcbf934" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 3, + "options": { + "legend": { + "calcs": [ + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af1438b4-d94c-460c-833b-b6fccbcbf934" + }, + "editorMode": "code", + "expr": "sum by(product, severity) (dojo_vulnerabilities_risk_accepted{product=~\"$product\", severity=~\"$severity\", cwe=~\"$cwe\"})", + "instant": false, + "legendFormat": "{{product}} - {{severity}}", + "range": true, + "refId": "A" + } + ], + "title": "Risk Accepted", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "af1438b4-d94c-460c-833b-b6fccbcbf934" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 2, + "options": { + "legend": { + "calcs": [ + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af1438b4-d94c-460c-833b-b6fccbcbf934" + }, + "editorMode": "code", + "expr": "sum by(product, severity) (dojo_vulnerabilities_under_review{product=~\"$product\", severity=~\"$severity\", cwe=~\"$cwe\"})", + "instant": false, + "legendFormat": "{{product}} - {{severity}}", + "range": true, + "refId": "A" + } + ], + "title": "Under Review", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "af1438b4-d94c-460c-833b-b6fccbcbf934" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 8, + "options": { + "legend": { + "calcs": [ + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af1438b4-d94c-460c-833b-b6fccbcbf934" + }, + "editorMode": "code", + "expr": "sum by(product, severity) (dojo_vulnerabilities_verified{product=~\"$product\", severity=~\"$severity\", cwe=~\"$cwe\"})", + "instant": false, + "legendFormat": "{{product}} - {{severity}}", + "range": true, + "refId": "A" + } + ], + "title": "Verified", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "af1438b4-d94c-460c-833b-b6fccbcbf934" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 24 + }, + "id": 7, + "options": { + "legend": { + "calcs": [ + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af1438b4-d94c-460c-833b-b6fccbcbf934" + }, + "editorMode": "code", + "expr": "sum by(product, severity) (dojo_vulnerabilities_mitigated{product=~\"$product\", severity=~\"$severity\", cwe=~\"$cwe\"})", + "instant": false, + "legendFormat": "{{product}} - {{severity}}", + "range": true, + "refId": "A" + } + ], + "title": "Mitigated", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [ + "defectdojo" + ], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": [ + "clientpp" + ], + "value": [ + "clientpp" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "af1438b4-d94c-460c-833b-b6fccbcbf934" + }, + "definition": "label_values(dojo_vulnerabilities_active,product_name)", + "hide": 0, + "includeAll": false, + "multi": true, + "name": "product", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(dojo_vulnerabilities_active,product_name)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "allValue": "", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "af1438b4-d94c-460c-833b-b6fccbcbf934" + }, + "definition": "label_values(dojo_vulnerabilities_active,severity)", + "hide": 0, + "includeAll": true, + "multi": true, + "name": "severity", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(dojo_vulnerabilities_active,severity)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "allValue": "", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "af1438b4-d94c-460c-833b-b6fccbcbf934" + }, + "definition": "label_values(dojo_vulnerabilities_active{product=~\"$product\"},cwe)", + "hide": 0, + "includeAll": true, + "multi": true, + "name": "cwe", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(dojo_vulnerabilities_active{product=~\"$product\"},cwe)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-3h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "DefectDojo Exporter", + "uid": "fdxqlkutrij28b", + "version": 12, + "weekStart": "" +} \ No newline at end of file diff --git a/go.work b/go.work new file mode 100644 index 0000000..caa0d22 --- /dev/null +++ b/go.work @@ -0,0 +1,9 @@ +go 1.22.5 + +use ( + ./cmd/defectdojo-exporter + ./config + ./internal/collector + ./internal/defectdojo + ./internal/initializer +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..5ad4a1d --- /dev/null +++ b/go.work.sum @@ -0,0 +1,24 @@ +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/collector/collector.go b/internal/collector/collector.go new file mode 100644 index 0000000..cc782dc --- /dev/null +++ b/internal/collector/collector.go @@ -0,0 +1,166 @@ +package collector + +import ( + "defectdojo" + "fmt" + "initializer" + "log" + "strings" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +// CollectMetrics main collector +func CollectMetrics(url, token string) { + for { + products, err := defectdojo.FetchProducts(url, token) + if err != nil { + log.Printf("Error fetching products: %v", err) + time.Sleep(30 * time.Second) + continue + } + + var wg sync.WaitGroup + + for _, product := range products { + wg.Add(1) + go func(product string) { + defer wg.Done() + vulnerabilities, err := defectdojo.FetchVulnerabilities(product, url, token) + if err != nil { + log.Printf("Error fetching vulnerabilities for product %s: %v", product, err) + return + } + + severities := []string{"critical", "high", "medium", "low", "info"} + CWEs := defectdojo.CollectCWEs(vulnerabilities) + initializer.InitializeMetricsForProduct(product, severities, CWEs) + + // Aggregate counts + activeCounts := make(map[string]map[string]float64) + duplicateCounts := make(map[string]map[string]float64) + underReviewCounts := make(map[string]map[string]float64) + falsePositiveCounts := make(map[string]map[string]float64) + outOfScopeCounts := make(map[string]map[string]float64) + riskAcceptedCounts := make(map[string]map[string]float64) + verifiedCounts := make(map[string]map[string]float64) + mitigatedCounts := make(map[string]map[string]float64) + + // Initialize maps + for _, severity := range severities { + activeCounts[severity] = make(map[string]float64) + duplicateCounts[severity] = make(map[string]float64) + underReviewCounts[severity] = make(map[string]float64) + falsePositiveCounts[severity] = make(map[string]float64) + outOfScopeCounts[severity] = make(map[string]float64) + riskAcceptedCounts[severity] = make(map[string]float64) + verifiedCounts[severity] = make(map[string]float64) + mitigatedCounts[severity] = make(map[string]float64) + + // default value 0 + for _, vuln := range vulnerabilities { + cwe := fmt.Sprintf("%d", vuln.CWE) + activeCounts[severity][cwe] = 0 + duplicateCounts[severity][cwe] = 0 + underReviewCounts[severity][cwe] = 0 + falsePositiveCounts[severity][cwe] = 0 + outOfScopeCounts[severity][cwe] = 0 + riskAcceptedCounts[severity][cwe] = 0 + verifiedCounts[severity][cwe] = 0 + mitigatedCounts[severity][cwe] = 0 + } + } + + // Aggregate the number of vulnerabilities by severity and CWE + for _, vuln := range vulnerabilities { + + severity := strings.ToLower(vuln.Severity) + cwe := fmt.Sprintf("%d", vuln.CWE) + + if vuln.Active { + activeCounts[severity][cwe]++ + } + if vuln.Duplicate { + duplicateCounts[severity][cwe]++ + } + if vuln.UnderReview { + underReviewCounts[severity][cwe]++ + } + if vuln.FalseP { + falsePositiveCounts[severity][cwe]++ + } + if vuln.OutOfScope { + outOfScopeCounts[severity][cwe]++ + } + if vuln.RiskAccepted { + riskAcceptedCounts[severity][cwe]++ + } + if vuln.Verified { + verifiedCounts[severity][cwe]++ + } + if vuln.Mitigated { + mitigatedCounts[severity][cwe]++ + } + } + + for severity, cweMap := range activeCounts { + for cwe, count := range cweMap { + updateMetric(defectdojo.VulnActiveGauge, defectdojo.PrevActive, product, severity, cwe, count) + } + } + for severity, cweMap := range duplicateCounts { + for cwe, count := range cweMap { + updateMetric(defectdojo.VulnDuplicateGauge, defectdojo.PrevDuplicate, product, severity, cwe, count) + } + } + for severity, cweMap := range underReviewCounts { + for cwe, count := range cweMap { + updateMetric(defectdojo.VulnUnderReviewGauge, defectdojo.PrevUnderReview, product, severity, cwe, count) + } + } + for severity, cweMap := range falsePositiveCounts { + for cwe, count := range cweMap { + updateMetric(defectdojo.VulnFalsePositiveGauge, defectdojo.PrevFalsePositive, product, severity, cwe, count) + } + } + for severity, cweMap := range outOfScopeCounts { + for cwe, count := range cweMap { + updateMetric(defectdojo.VulnOutOfScopeGauge, defectdojo.PrevOutOfScope, product, severity, cwe, count) + } + } + for severity, cweMap := range riskAcceptedCounts { + for cwe, count := range cweMap { + updateMetric(defectdojo.VulnRiskAcceptedGauge, defectdojo.PrevRiskAccepted, product, severity, cwe, count) + } + } + for severity, cweMap := range verifiedCounts { + for cwe, count := range cweMap { + updateMetric(defectdojo.VulnVerifiedGauge, defectdojo.PrevVerified, product, severity, cwe, count) + } + } + for severity, cweMap := range mitigatedCounts { + for cwe, count := range cweMap { + updateMetric(defectdojo.VulnMitigatedGauge, defectdojo.PrevMitigated, product, severity, cwe, count) + } + } + }(product) + } + + wg.Wait() + } +} + +// updateMetric updated metrics +func updateMetric(metric *prometheus.GaugeVec, prev map[string]map[string]float64, product, severity, cwe string, value float64) { + defectdojo.MU.Lock() + defer defectdojo.MU.Unlock() + + if prev[product] == nil { + prev[product] = make(map[string]float64) + } + + metric.WithLabelValues(product, severity, cwe).Set(value) + prev[product][severity] = value +} diff --git a/internal/collector/go.mod b/internal/collector/go.mod new file mode 100644 index 0000000..b3ce907 --- /dev/null +++ b/internal/collector/go.mod @@ -0,0 +1,16 @@ +module collector + +go 1.22.5 + +require github.com/prometheus/client_golang v1.20.3 + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + golang.org/x/sys v0.22.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect +) diff --git a/internal/collector/go.sum b/internal/collector/go.sum new file mode 100644 index 0000000..565fe66 --- /dev/null +++ b/internal/collector/go.sum @@ -0,0 +1,22 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= +github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= diff --git a/internal/defectdojo/defectdojo.go b/internal/defectdojo/defectdojo.go new file mode 100644 index 0000000..ecba7a3 --- /dev/null +++ b/internal/defectdojo/defectdojo.go @@ -0,0 +1,141 @@ +package defectdojo + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +var ( + VulnActiveGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: "dojo_vulnerabilities_active", Help: "Number of active vulnerabilities in DefectDojo"}, []string{"product", "severity", "cwe"}) + VulnDuplicateGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: "dojo_vulnerabilities_duplicate", Help: "Number of duplicate vulnerabilities in DefectDojo"}, []string{"product", "severity", "cwe"}) + VulnUnderReviewGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: "dojo_vulnerabilities_under_review", Help: "Number of vulnerabilities under review in DefectDojo"}, []string{"product", "severity", "cwe"}) + VulnFalsePositiveGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: "dojo_vulnerabilities_false_positive", Help: "Number of false positive vulnerabilities in DefectDojo"}, []string{"product", "severity", "cwe"}) + VulnOutOfScopeGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: "dojo_vulnerabilities_out_of_scope", Help: "Number of vulnerabilities out of scope in DefectDojo"}, []string{"product", "severity", "cwe"}) + VulnRiskAcceptedGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: "dojo_vulnerabilities_risk_accepted", Help: "Number of vulnerabilities with risk accepted in DefectDojo"}, []string{"product", "severity", "cwe"}) + VulnVerifiedGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: "dojo_vulnerabilities_verified", Help: "Number of verified vulnerabilities in DefectDojo"}, []string{"product", "severity", "cwe"}) + VulnMitigatedGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: "dojo_vulnerabilities_mitigated", Help: "Number of mitigated vulnerabilities in DefectDojo"}, []string{"product", "severity", "cwe"}) + + MU sync.Mutex + PrevActive = make(map[string]map[string]float64) + PrevDuplicate = make(map[string]map[string]float64) + PrevUnderReview = make(map[string]map[string]float64) + PrevFalsePositive = make(map[string]map[string]float64) + PrevOutOfScope = make(map[string]map[string]float64) + PrevRiskAccepted = make(map[string]map[string]float64) + PrevVerified = make(map[string]map[string]float64) + PrevMitigated = make(map[string]map[string]float64) +) + +type Finding struct { + Active bool `json:"active"` + Severity string `json:"severity"` + CWE int `json:"cwe"` + FalseP bool `json:"false_p"` + Duplicate bool `json:"duplicate"` + OutOfScope bool `json:"out_of_scope"` + RiskAccepted bool `json:"risk_accepted"` + UnderReview bool `json:"under_review"` + Verified bool `json:"verified"` + Mitigated bool `json:"is_mitigated"` +} + +type FindingsResponse struct { + Next string `json:"next"` + Results []Finding `json:"results"` +} + +type Product struct { + Name string `json:"name"` +} + +type ProductsResponse struct { + Next string `json:"next"` + Results []Product `json:"results"` +} + +// FetchProducts go to API DefectDojo products +func FetchProducts(url, token string) ([]string, error) { + products := []string{} + endpoint := fmt.Sprintf("%s/api/v2/products/", url) + + for endpoint != "" { + resp, err := makeRequest(endpoint, token) + if err != nil { + log.Printf("Error fetching products: %v", err) + return nil, err + } + var productsResp ProductsResponse + if err := json.Unmarshal(resp, &productsResp); err != nil { + log.Printf("Error unmarshalling products response: %v", err) + return nil, err + } + + for _, product := range productsResp.Results { + products = append(products, product.Name) + } + + endpoint = productsResp.Next + } + return products, nil +} + +// FetchVulnerabilities go to api DefectDojo findings +func FetchVulnerabilities(product, url, token string) ([]Finding, error) { + vulnerabilities := []Finding{} + endpoint := fmt.Sprintf("%s/api/v2/findings/?product_name=%s&limit=100", url, product) + + for endpoint != "" { + resp, err := makeRequest(endpoint, token) + if err != nil { + log.Printf("Error fetching vulnerabilities for product %s: %v", product, err) + return nil, err + } + var findingsResp FindingsResponse + if err := json.Unmarshal(resp, &findingsResp); err != nil { + log.Printf("Error unmarshalling vulnerabilities response for product %s: %v", product, err) + return nil, err + } + + vulnerabilities = append(vulnerabilities, findingsResp.Results...) + endpoint = findingsResp.Next + } + return vulnerabilities, nil +} + +// CollectCWEs take all CWE in vulnerabilities +func CollectCWEs(vulnerabilities []Finding) map[int]bool { + CWEs := make(map[int]bool) + for _, vuln := range vulnerabilities { + CWEs[vuln.CWE] = true + } + return CWEs +} + +// makeRequest send request in API DefectDojo +func makeRequest(url, token string) ([]byte, error) { + client := &http.Client{Timeout: 10 * time.Second} + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", fmt.Sprintf("Token %s", token)) + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, resp.Status) + } + + return io.ReadAll(resp.Body) +} diff --git a/internal/defectdojo/go.mod b/internal/defectdojo/go.mod new file mode 100644 index 0000000..d232c74 --- /dev/null +++ b/internal/defectdojo/go.mod @@ -0,0 +1,16 @@ +module defectdojo + +go 1.22.5 + +require github.com/prometheus/client_golang v1.20.3 + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + golang.org/x/sys v0.22.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect +) diff --git a/internal/defectdojo/go.sum b/internal/defectdojo/go.sum new file mode 100644 index 0000000..565fe66 --- /dev/null +++ b/internal/defectdojo/go.sum @@ -0,0 +1,22 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= +github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= diff --git a/internal/initializer/go.mod b/internal/initializer/go.mod new file mode 100644 index 0000000..c65e706 --- /dev/null +++ b/internal/initializer/go.mod @@ -0,0 +1,3 @@ +module initializer + +go 1.22.5 diff --git a/internal/initializer/initializer.go b/internal/initializer/initializer.go new file mode 100644 index 0000000..e09ee76 --- /dev/null +++ b/internal/initializer/initializer.go @@ -0,0 +1,51 @@ +package initializer + +import ( + "defectdojo" + "fmt" +) + +// InitializeMetricsForProduct init all metrics +func InitializeMetricsForProduct(product string, severities []string, CWEs map[int]bool) { + defectdojo.MU.Lock() + defer defectdojo.MU.Unlock() + + if _, exists := defectdojo.PrevActive[product]; !exists { + defectdojo.PrevActive[product] = make(map[string]float64) + } + if _, exists := defectdojo.PrevDuplicate[product]; !exists { + defectdojo.PrevDuplicate[product] = make(map[string]float64) + } + if _, exists := defectdojo.PrevUnderReview[product]; !exists { + defectdojo.PrevUnderReview[product] = make(map[string]float64) + } + if _, exists := defectdojo.PrevFalsePositive[product]; !exists { + defectdojo.PrevFalsePositive[product] = make(map[string]float64) + } + if _, exists := defectdojo.PrevOutOfScope[product]; !exists { + defectdojo.PrevOutOfScope[product] = make(map[string]float64) + } + if _, exists := defectdojo.PrevRiskAccepted[product]; !exists { + defectdojo.PrevRiskAccepted[product] = make(map[string]float64) + } + if _, exists := defectdojo.PrevVerified[product]; !exists { + defectdojo.PrevVerified[product] = make(map[string]float64) + } + if _, exists := defectdojo.PrevMitigated[product]; !exists { + defectdojo.PrevMitigated[product] = make(map[string]float64) + } + + for _, severity := range severities { + for cwe := range CWEs { + cweStr := fmt.Sprintf("%d", cwe) + defectdojo.VulnActiveGauge.WithLabelValues(product, severity, cweStr) + defectdojo.VulnDuplicateGauge.WithLabelValues(product, severity, cweStr) + defectdojo.VulnUnderReviewGauge.WithLabelValues(product, severity, cweStr) + defectdojo.VulnFalsePositiveGauge.WithLabelValues(product, severity, cweStr) + defectdojo.VulnOutOfScopeGauge.WithLabelValues(product, severity, cweStr) + defectdojo.VulnRiskAcceptedGauge.WithLabelValues(product, severity, cweStr) + defectdojo.VulnVerifiedGauge.WithLabelValues(product, severity, cweStr) + defectdojo.VulnMitigatedGauge.WithLabelValues(product, severity, cweStr) + } + } +}