feat(trivy): Enhance Trivy report generation and metrics handling
- Updated `generate_report.sh` to include additional scanners and output paths. - Enhanced `send_report.py` with new data classes for misconfigurations and vulnerabilities. - Improved parsing logic for Trivy output, supporting both Kubernetes and image scans. - Added Prometheus metrics generation for various scan results, including misconfigurations and vulnerabilities. - Introduced unit tests for parsing Trivy output and generating Prometheus metrics. - Modified Terraform variables for SMTP port type and added Pushgateway URL variable.
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
build:
|
||||
dockerfile: "{{ lookup('file', 'dockerfiles/Dockerfile.ollama.rocm') }}"
|
||||
env:
|
||||
OLLAMA_CONTEXT_LENGTH: 4096
|
||||
OLLAMA_CONTEXT_LENGTH: 32000
|
||||
OLLAMA_NUM_THREADS: 10
|
||||
OLLAMA_NUM_PARALLEL: 2
|
||||
OLLAMA_FLASH_ATTENTION: 1
|
||||
|
||||
@@ -4,3 +4,4 @@ molecule
|
||||
molecule-podman
|
||||
passlib
|
||||
pytest-molecule
|
||||
requests
|
||||
|
||||
@@ -98,9 +98,10 @@ module "trivy" {
|
||||
|
||||
persistent_folder = local.persistent_folder
|
||||
cron_schedule = local.trivy_cronjob_schedule
|
||||
tag = "0.70.0"
|
||||
|
||||
smtp_host = local.scaleway_smtp_host
|
||||
smtp_port = local.scaleway_smtp_port
|
||||
smtp_port = tostring(local.scaleway_smtp_port)
|
||||
smtp_username = var.scaleway_project_id
|
||||
smtp_password = local.mail_app_api_secret_key
|
||||
from_email = "trivy@${local.primary_domain}"
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
locals {
|
||||
app_version = var.tag
|
||||
monitoring_folder = "${var.persistent_folder}/grafana"
|
||||
|
||||
grafana_datasource_config = file("${path.module}/resources/default.yml")
|
||||
grafana_dashboard_provider = file("${path.module}/resources/dashboard-provider.yml")
|
||||
grafana_trivy_dashboard = file("${path.module}/resources/trivy-vulnerabilities-dashboard.json")
|
||||
}
|
||||
|
||||
@@ -1,3 +1,36 @@
|
||||
resource "kubernetes_config_map" "grafana_datasources" {
|
||||
metadata {
|
||||
name = "grafana-datasources"
|
||||
namespace = kubernetes_namespace.grafana.metadata[0].name
|
||||
}
|
||||
|
||||
data = {
|
||||
"default.yml" = replace(local.grafana_datasource_config, "__PROMETHEUS_URL__", var.prometheus_url)
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_config_map" "grafana_dashboard_provider" {
|
||||
metadata {
|
||||
name = "grafana-dashboard-provider"
|
||||
namespace = kubernetes_namespace.grafana.metadata[0].name
|
||||
}
|
||||
|
||||
data = {
|
||||
"dashboard-provider.yml" = local.grafana_dashboard_provider
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_config_map" "grafana_dashboards" {
|
||||
metadata {
|
||||
name = "grafana-dashboards"
|
||||
namespace = kubernetes_namespace.grafana.metadata[0].name
|
||||
}
|
||||
|
||||
data = {
|
||||
"trivy-vulnerabilities-dashboard.json" = local.grafana_trivy_dashboard
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_deployment" "grafana" {
|
||||
|
||||
metadata {
|
||||
@@ -67,6 +100,21 @@ resource "kubernetes_deployment" "grafana" {
|
||||
name = "grafana-pv"
|
||||
mount_path = "/var/lib/grafana"
|
||||
}
|
||||
volume_mount {
|
||||
name = "grafana-datasources"
|
||||
mount_path = "/etc/grafana/provisioning/datasources"
|
||||
read_only = true
|
||||
}
|
||||
volume_mount {
|
||||
name = "grafana-dashboard-provider"
|
||||
mount_path = "/etc/grafana/provisioning/dashboards"
|
||||
read_only = true
|
||||
}
|
||||
volume_mount {
|
||||
name = "grafana-dashboards"
|
||||
mount_path = "/var/lib/grafana/dashboards"
|
||||
read_only = true
|
||||
}
|
||||
}
|
||||
volume {
|
||||
name = "grafana-pv"
|
||||
@@ -74,6 +122,24 @@ resource "kubernetes_deployment" "grafana" {
|
||||
path = local.monitoring_folder
|
||||
}
|
||||
}
|
||||
volume {
|
||||
name = "grafana-datasources"
|
||||
config_map {
|
||||
name = kubernetes_config_map.grafana_datasources.metadata[0].name
|
||||
}
|
||||
}
|
||||
volume {
|
||||
name = "grafana-dashboard-provider"
|
||||
config_map {
|
||||
name = kubernetes_config_map.grafana_dashboard_provider.metadata[0].name
|
||||
}
|
||||
}
|
||||
volume {
|
||||
name = "grafana-dashboards"
|
||||
config_map {
|
||||
name = kubernetes_config_map.grafana_dashboards.metadata[0].name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,580 @@
|
||||
{
|
||||
"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": 5,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"panels": [],
|
||||
"title": "Sessions",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 1
|
||||
},
|
||||
"id": 1,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "auth_sessions_active{}",
|
||||
"legendFormat": "{{source}}",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Active sessions (by source)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 1
|
||||
},
|
||||
"id": 4,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum by (source, method) (rate(auth_sessions_total[5m]))",
|
||||
"legendFormat": "{{source}} / {{method}}",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Auth sessions started (rate)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"id": 6,
|
||||
"panels": [],
|
||||
"title": "Users",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 9
|
||||
},
|
||||
"id": 5,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "auth_sessions_by_user_total{}",
|
||||
"legendFormat": "{{username}}:{{source}}",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Top users by session starts (last 5m)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 17
|
||||
},
|
||||
"id": 3,
|
||||
"panels": [],
|
||||
"title": "Exporter metrics",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 18
|
||||
},
|
||||
"id": 7,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "rate(auth_exporter_parse_errors_total[5m])",
|
||||
"legendFormat": "{{instance}}",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Parse errors (rate)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "bezn5wieufi80d"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 18
|
||||
},
|
||||
"id": 8,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"editorMode": "code",
|
||||
"expr": "rate(auth_exporter_user_labels_dropped_total[5m])",
|
||||
"legendFormat": "{{instance}}",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "User label drops (rate)",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"preload": false,
|
||||
"schemaVersion": 42,
|
||||
"tags": [
|
||||
"security"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"current": {
|
||||
"text": "prometheus-dev-01",
|
||||
"value": "bezn5wieufi80d"
|
||||
},
|
||||
"name": "datasource",
|
||||
"options": [],
|
||||
"query": "prometheus",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"type": "datasource"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Auth Exporter",
|
||||
"uid": "7381c747-ecf6-4f2f-ac4b-551e56f24cf0",
|
||||
"version": 5
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: trivy-vulnerabilities
|
||||
orgId: 1
|
||||
folder: Security
|
||||
type: file
|
||||
disableDeletion: false
|
||||
updateIntervalSeconds: 30
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
@@ -1,38 +1,9 @@
|
||||
global:
|
||||
scrape_interval: 5s
|
||||
evaluation_interval: 5s
|
||||
rule_files:
|
||||
- /etc/prometheus/prometheus.rules
|
||||
scrape_configs:
|
||||
- job_name: kubernetes-nodes-cadvisor
|
||||
scrape_interval: 10s
|
||||
scrape_timeout: 10s
|
||||
scheme: https # remove if you want to scrape metrics on insecure port
|
||||
tls_config:
|
||||
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
||||
insecure_skip_verify: true
|
||||
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
kubernetes_sd_configs:
|
||||
- role: node
|
||||
relabel_configs:
|
||||
- action: labelmap
|
||||
regex: __meta_kubernetes_node_label_(.+)
|
||||
# Only for Kubernetes ^1.7.3.
|
||||
# See: https://github.com/prometheus/prometheus/issues/2916
|
||||
- target_label: __address__
|
||||
replacement: kubernetes.default.svc:443
|
||||
- source_labels: [__meta_kubernetes_node_name]
|
||||
regex: (.+)
|
||||
target_label: __metrics_path__
|
||||
replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor
|
||||
metric_relabel_configs:
|
||||
- action: replace
|
||||
source_labels: [id]
|
||||
regex: '^/machine\.slice/machine-rkt\\x2d([^\\]+)\\.+/([^/]+)\.service$'
|
||||
target_label: rkt_container_name
|
||||
replacement: "${2}-${1}"
|
||||
- action: replace
|
||||
source_labels: [id]
|
||||
regex: '^/system\.slice/(.+)\.service$'
|
||||
target_label: systemd_service_name
|
||||
replacement: "${1}"
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: __PROMETHEUS_URL__
|
||||
isDefault: true
|
||||
editable: true
|
||||
|
||||
@@ -0,0 +1,551 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 8,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"decimals": 0,
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "text",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 6,
|
||||
"w": 5,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "devolo_uptime_seconds",
|
||||
"instant": true,
|
||||
"range": false,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Device Uptime",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "bezn5wieufi80d"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [],
|
||||
"max": 200,
|
||||
"thresholds": {
|
||||
"mode": "percentage",
|
||||
"steps": [
|
||||
{
|
||||
"color": "red",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"value": 85
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 6,
|
||||
"w": 7,
|
||||
"x": 5,
|
||||
"y": 0
|
||||
},
|
||||
"id": 5,
|
||||
"options": {
|
||||
"displayMode": "lcd",
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": false
|
||||
},
|
||||
"maxVizHeight": 300,
|
||||
"minVizHeight": 16,
|
||||
"minVizWidth": 8,
|
||||
"namePlacement": "auto",
|
||||
"orientation": "horizontal",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showUnfilled": true,
|
||||
"sizing": "auto",
|
||||
"valueMode": "color"
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"editorMode": "code",
|
||||
"expr": "devolo_rx_mbps{device_id=~\"$device_id\", mac=~\"$mac\"}",
|
||||
"legendFormat": "RX",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "bezn5wieufi80d"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "devolo_tx_mbps{device_id=~\"$device_id\", mac=~\"$mac\"}",
|
||||
"hide": false,
|
||||
"instant": false,
|
||||
"legendFormat": "TX",
|
||||
"range": true,
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "RX Rate",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"align": "auto",
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"footer": {
|
||||
"reducers": []
|
||||
},
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 6,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"id": 3,
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "devolo_device_info_info",
|
||||
"instant": true,
|
||||
"legendFormat": "",
|
||||
"range": false,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Devolo Device Info",
|
||||
"transformations": [
|
||||
{
|
||||
"id": "labelsToFields",
|
||||
"options": {
|
||||
"keepLabels": [
|
||||
"firmware",
|
||||
"mac",
|
||||
"name",
|
||||
"serial"
|
||||
],
|
||||
"mode": "columns"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "organize",
|
||||
"options": {
|
||||
"excludeByName": {
|
||||
"Time": true,
|
||||
"Value": true,
|
||||
"devolo_device_info_info": true
|
||||
},
|
||||
"includeByName": {},
|
||||
"indexByName": {},
|
||||
"renameByName": {
|
||||
"firmware": "Firmware",
|
||||
"mac": "MAC",
|
||||
"name": "Name",
|
||||
"serial": "Serial"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "continuous-RdYlGr"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 20,
|
||||
"gradientMode": "scheme",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "smooth",
|
||||
"lineStyle": {
|
||||
"fill": "solid"
|
||||
},
|
||||
"lineWidth": 3,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"max": 200,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "red",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 6
|
||||
},
|
||||
"id": 6,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "hidden",
|
||||
"placement": "right",
|
||||
"showLegend": false
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "devolo_rx_mbps{device_id=~\"$device_id\", mac=~\"$mac\"}",
|
||||
"instant": false,
|
||||
"legendFormat": "TX",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "RX Throughput",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "continuous-RdYlGr",
|
||||
"seriesBy": "max"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 20,
|
||||
"gradientMode": "scheme",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "smooth",
|
||||
"lineWidth": 3,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"max": 200,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "red",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 6
|
||||
},
|
||||
"id": 1,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "hidden",
|
||||
"placement": "right",
|
||||
"showLegend": false
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "devolo_tx_mbps{device_id=~\"$device_id\", mac=~\"$mac\"}",
|
||||
"instant": false,
|
||||
"legendFormat": "TX",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "TX Throughput",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"preload": false,
|
||||
"refresh": "10s",
|
||||
"schemaVersion": 42,
|
||||
"tags": [
|
||||
"devolo",
|
||||
"powerline",
|
||||
"network"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"current": {
|
||||
"text": "Prometheus",
|
||||
"value": "PBFA97CFB590B2093"
|
||||
},
|
||||
"label": "Prometheus",
|
||||
"name": "DS_PROMETHEUS",
|
||||
"options": [],
|
||||
"query": "prometheus",
|
||||
"refresh": 1,
|
||||
"type": "datasource"
|
||||
},
|
||||
{
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"definition": "label_values(devolo_tx_mbps, device_id)",
|
||||
"includeAll": true,
|
||||
"label": "Device ID",
|
||||
"multi": true,
|
||||
"name": "device_id",
|
||||
"options": [],
|
||||
"query": {
|
||||
"query": "label_values(devolo_tx_mbps, device_id)",
|
||||
"refId": "StandardVariableQuery"
|
||||
},
|
||||
"refresh": 2,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"allValue": ".*",
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"definition": "label_values(devolo_tx_mbps{device_id=~\"$device_id\"}, mac)",
|
||||
"includeAll": true,
|
||||
"label": "MAC",
|
||||
"multi": true,
|
||||
"name": "mac",
|
||||
"options": [],
|
||||
"query": {
|
||||
"query": "label_values(devolo_tx_mbps{device_id=~\"$device_id\"}, mac)",
|
||||
"refId": "StandardVariableQuery"
|
||||
},
|
||||
"refresh": 2,
|
||||
"type": "query"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-24h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Devolo Powerline",
|
||||
"uid": "devolo-powerline",
|
||||
"version": 10
|
||||
}
|
||||
@@ -0,0 +1,652 @@
|
||||
{
|
||||
"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": 7,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"datasource": "$datasource",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 3,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 1,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"editorMode": "code",
|
||||
"expr": "count(disk_info{})-count(disk_healthy{})",
|
||||
"instant": true,
|
||||
"legendFormat": "__auto",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Unhealthy Disks",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": "$datasource",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"value": 60
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 70
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "none"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 9,
|
||||
"x": 3,
|
||||
"y": 0
|
||||
},
|
||||
"id": 3,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "disk_smart_attr_normalized_value{name=\"Temperature_Celsius\",device=~\"$device\"}",
|
||||
"legendFormat": "{{device}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Temperature (Normalized)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": "$datasource",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "continuous-GrYlRd"
|
||||
},
|
||||
"custom": {
|
||||
"axisPlacement": "auto",
|
||||
"fillOpacity": 70,
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineWidth": 0,
|
||||
"spanNulls": false
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 11,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"alignValue": "left",
|
||||
"legend": {
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"mergeValues": true,
|
||||
"rowHeight": 0.9,
|
||||
"showValue": "auto",
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "disk_healthy{device=~\"$device\"}",
|
||||
"legendFormat": "{{device}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Disk Health State",
|
||||
"type": "state-timeline"
|
||||
},
|
||||
{
|
||||
"datasource": "$datasource",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 23,
|
||||
"x": 0,
|
||||
"y": 7
|
||||
},
|
||||
"id": 4,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "disk_reallocated_sector_count{device=~\"$device\"}",
|
||||
"legendFormat": "{{device}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Reallocated Sectors",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": "$datasource",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 23,
|
||||
"x": 0,
|
||||
"y": 14
|
||||
},
|
||||
"id": 5,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "disk_offline_uncorrectable{device=~\"$device\"}",
|
||||
"legendFormat": "{{device}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Offline Uncorrectable",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": "$datasource",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 6,
|
||||
"w": 23,
|
||||
"x": 0,
|
||||
"y": 21
|
||||
},
|
||||
"id": 6,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "disk_smart_attr_raw_value{name=\"Power_On_Hours\",device=~\"$device\"}",
|
||||
"legendFormat": "{{device}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Power On Hours",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": "$datasource",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 6,
|
||||
"w": 23,
|
||||
"x": 0,
|
||||
"y": 27
|
||||
},
|
||||
"id": 7,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "disk_smart_attr_raw_value{name=~\"Media_Wearout_Indicator|Available_Reservd_Space\",device=~\"$device\"}",
|
||||
"legendFormat": "{{device}} {{name}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Media / Wear Indicators",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"preload": false,
|
||||
"refresh": "",
|
||||
"schemaVersion": 42,
|
||||
"tags": [
|
||||
"disks",
|
||||
"smart"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"datasource": "$datasource",
|
||||
"includeAll": true,
|
||||
"multi": true,
|
||||
"name": "device",
|
||||
"options": [],
|
||||
"query": "label_values(disk_healthy, device)",
|
||||
"refresh": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
"text": "prometheus-dev-01",
|
||||
"value": "bezn5wieufi80d"
|
||||
},
|
||||
"name": "datasource",
|
||||
"options": [],
|
||||
"query": "prometheus",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"type": "datasource"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "browser",
|
||||
"title": "Disk Health (smartctl)",
|
||||
"uid": "disk-health-smartctl",
|
||||
"version": 2
|
||||
}
|
||||
@@ -0,0 +1,648 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "grafana"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"target": {
|
||||
"limit": 100,
|
||||
"matchAny": false,
|
||||
"tags": [],
|
||||
"type": "dashboard"
|
||||
},
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 2,
|
||||
"id": 3,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "aezs0pa1i5xq8f"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"align": "auto",
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"footer": {
|
||||
"reducers": []
|
||||
},
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byRegexp",
|
||||
"options": ".*Time"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "unit",
|
||||
"value": "s"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 6,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 206,
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "aezs0pa1i5xq8f"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"exemplar": false,
|
||||
"expr": "f2b_config_jail_max_retries{instance=~\"$instance\"}",
|
||||
"format": "table",
|
||||
"instant": true,
|
||||
"interval": "",
|
||||
"legendFormat": "{{jail}}",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "aezs0pa1i5xq8f"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"exemplar": false,
|
||||
"expr": "f2b_config_jail_ban_time{instance=~\"$instance\"}",
|
||||
"format": "table",
|
||||
"hide": false,
|
||||
"instant": true,
|
||||
"interval": "",
|
||||
"legendFormat": "{{jail}}",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "aezs0pa1i5xq8f"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"exemplar": false,
|
||||
"expr": "f2b_config_jail_find_time{instance=~\"$instance\"}",
|
||||
"format": "table",
|
||||
"hide": false,
|
||||
"instant": true,
|
||||
"interval": "",
|
||||
"legendFormat": "{{jail}}",
|
||||
"refId": "C"
|
||||
}
|
||||
],
|
||||
"title": "F2B Config",
|
||||
"transformations": [
|
||||
{
|
||||
"id": "merge",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "groupBy",
|
||||
"options": {
|
||||
"fields": {
|
||||
"Value #A": {
|
||||
"aggregations": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"operation": "aggregate"
|
||||
},
|
||||
"Value #B": {
|
||||
"aggregations": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"operation": "aggregate"
|
||||
},
|
||||
"Value #C": {
|
||||
"aggregations": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"operation": "aggregate"
|
||||
},
|
||||
"instance": {
|
||||
"aggregations": [],
|
||||
"operation": "groupby"
|
||||
},
|
||||
"jail": {
|
||||
"aggregations": [],
|
||||
"operation": "groupby"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "organize",
|
||||
"options": {
|
||||
"excludeByName": {},
|
||||
"indexByName": {},
|
||||
"renameByName": {
|
||||
"Value #A (lastNotNull)": "Max Retries",
|
||||
"Value #B (lastNotNull)": "Ban Time",
|
||||
"Value #C (lastNotNull)": "Find Time",
|
||||
"jail": "Jail"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"transparent": true,
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "aezs0pa1i5xq8f"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"showValues": false,
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"min": 0,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 6
|
||||
},
|
||||
"id": 190,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "right",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "aezs0pa1i5xq8f"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"exemplar": true,
|
||||
"expr": "f2b_jail_failed_total{instance=~\"$instance\"}",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"legendFormat": "{{jail}}",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Fail2Ban Failures (Total)",
|
||||
"transparent": true,
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "aezs0pa1i5xq8f"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"showValues": false,
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"min": 0,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 6
|
||||
},
|
||||
"id": 191,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "right",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "aezs0pa1i5xq8f"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"exemplar": true,
|
||||
"expr": "f2b_jail_banned_total{instance=~\"$instance\"}",
|
||||
"interval": "",
|
||||
"legendFormat": "{{jail}}",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Fail2Ban Bans (Total)",
|
||||
"transparent": true,
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "aezs0pa1i5xq8f"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"showValues": false,
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"min": 0,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 14
|
||||
},
|
||||
"id": 208,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "right",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "aezs0pa1i5xq8f"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"exemplar": true,
|
||||
"expr": "f2b_jail_failed_current{instance=~\"$instance\"}",
|
||||
"interval": "",
|
||||
"legendFormat": "{{jail}}",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Fail2Ban Failures (Current)",
|
||||
"transparent": true,
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "aezs0pa1i5xq8f"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"showValues": false,
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"min": 0,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 14
|
||||
},
|
||||
"id": 209,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "right",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "aezs0pa1i5xq8f"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"exemplar": true,
|
||||
"expr": "f2b_jail_banned_current{instance=~\"$instance\"}",
|
||||
"interval": "",
|
||||
"legendFormat": "{{jail}}",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Fail2Ban Bans (Current)",
|
||||
"transparent": true,
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"preload": false,
|
||||
"refresh": "30s",
|
||||
"schemaVersion": 42,
|
||||
"tags": [
|
||||
"security"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"current": {
|
||||
"text": [
|
||||
"fail2ban-exporter.prometheus.svc.cluster.local:9100"
|
||||
],
|
||||
"value": [
|
||||
"fail2ban-exporter.prometheus.svc.cluster.local:9100"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "aezs0pa1i5xq8f"
|
||||
},
|
||||
"definition": "f2b_up",
|
||||
"description": "Select which instance(s) to show",
|
||||
"includeAll": false,
|
||||
"label": "Instance",
|
||||
"multi": true,
|
||||
"name": "instance",
|
||||
"options": [],
|
||||
"query": {
|
||||
"query": "f2b_up",
|
||||
"refId": "StandardVariableQuery"
|
||||
},
|
||||
"refresh": 1,
|
||||
"regex": "/.*instance=\"([^\"]+)\"/",
|
||||
"type": "query"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Fail2Ban Exporter",
|
||||
"uid": "cTkH9AT7z",
|
||||
"version": 4
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -20,3 +20,9 @@ variable "persistent_folder" {
|
||||
description = "The path to the persistent folder"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "prometheus_url" {
|
||||
description = "Prometheus service URL used by Grafana datasource provisioning"
|
||||
type = string
|
||||
default = "http://prometheus.prometheus.svc.cluster.local:9090"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
locals {
|
||||
app_version = var.tag
|
||||
prometheus_folder = "${var.persistent_folder}/prometheus"
|
||||
pushgateway_folder = "${var.persistent_folder}/pushgateway"
|
||||
alertmanager_folder = "${var.persistent_folder}/alertmanager"
|
||||
|
||||
prometheus_default_config = yamldecode(file("${path.module}/resources/prometheus.yml"))
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
resource "kubernetes_deployment" "pushgateway" {
|
||||
metadata {
|
||||
name = "pushgateway"
|
||||
namespace = kubernetes_namespace.prometheus.metadata[0].name
|
||||
labels = {
|
||||
app = "pushgateway"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
replicas = 1
|
||||
strategy {
|
||||
type = "Recreate"
|
||||
}
|
||||
|
||||
selector {
|
||||
match_labels = {
|
||||
app = "pushgateway"
|
||||
}
|
||||
}
|
||||
|
||||
template {
|
||||
metadata {
|
||||
labels = {
|
||||
app = "pushgateway"
|
||||
}
|
||||
annotations = {
|
||||
"prometheus.io/scrape" = "true"
|
||||
"prometheus.io/port" = "9091"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
automount_service_account_token = false
|
||||
|
||||
security_context {
|
||||
run_as_non_root = true
|
||||
run_as_user = 65534
|
||||
run_as_group = 65534
|
||||
seccomp_profile {
|
||||
type = "RuntimeDefault"
|
||||
}
|
||||
}
|
||||
|
||||
container {
|
||||
name = "pushgateway"
|
||||
image = "prom/pushgateway:${local.app_version}"
|
||||
args = ["--persistence.file=/pushgateway/metrics"]
|
||||
|
||||
port {
|
||||
name = "http"
|
||||
container_port = 9091
|
||||
}
|
||||
|
||||
security_context {
|
||||
run_as_non_root = true
|
||||
run_as_user = 65534
|
||||
run_as_group = 65534
|
||||
allow_privilege_escalation = false
|
||||
privileged = false
|
||||
read_only_root_filesystem = true
|
||||
capabilities {
|
||||
drop = ["ALL"]
|
||||
}
|
||||
}
|
||||
|
||||
volume_mount {
|
||||
name = "pushgateway-storage-volume"
|
||||
mount_path = "/pushgateway"
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "pushgateway-storage-volume"
|
||||
host_path {
|
||||
path = local.pushgateway_folder
|
||||
type = "DirectoryOrCreate"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_service" "pushgateway" {
|
||||
metadata {
|
||||
name = "pushgateway"
|
||||
namespace = kubernetes_namespace.prometheus.metadata[0].name
|
||||
}
|
||||
|
||||
spec {
|
||||
selector = {
|
||||
app = "pushgateway"
|
||||
}
|
||||
type = "ClusterIP"
|
||||
|
||||
port {
|
||||
port = 9091
|
||||
protocol = "TCP"
|
||||
target_port = "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ scrape_configs:
|
||||
regex: '^/system\.slice/(.+)\.service$'
|
||||
target_label: systemd_service_name
|
||||
replacement: "${1}"
|
||||
- job_name: pushgateway
|
||||
honor_labels: true
|
||||
static_configs:
|
||||
- targets: ["pushgateway.prometheus.svc.cluster.local:9091"]
|
||||
alerting:
|
||||
alertmanagers:
|
||||
- static_configs:
|
||||
|
||||
@@ -85,7 +85,7 @@ resource "kubernetes_cron_job_v1" "trivy" {
|
||||
backoff_limit = 1
|
||||
parallelism = 1
|
||||
completions = 1
|
||||
active_deadline_seconds = 250
|
||||
active_deadline_seconds = 600
|
||||
|
||||
template {
|
||||
metadata {
|
||||
@@ -126,9 +126,9 @@ resource "kubernetes_cron_job_v1" "trivy" {
|
||||
}
|
||||
}
|
||||
container {
|
||||
name = "send-report"
|
||||
image = "python:${local.python_version}"
|
||||
args = ["/scripts/send_report.py"]
|
||||
name = "send-report"
|
||||
image = "python:${local.python_version}"
|
||||
command = ["python3", "/scripts/send_report.py"]
|
||||
env {
|
||||
name = "SMTP_HOST"
|
||||
value = var.smtp_host
|
||||
@@ -163,6 +163,18 @@ resource "kubernetes_cron_job_v1" "trivy" {
|
||||
name = "EMAIL_TO"
|
||||
value = var.to_email
|
||||
}
|
||||
env {
|
||||
name = "PUSHGATEWAY_URL"
|
||||
value = var.pushgateway_url
|
||||
}
|
||||
env {
|
||||
name = "REPORT_PATH"
|
||||
value = "/tmp/report.html"
|
||||
}
|
||||
env {
|
||||
name = "TRIVY_OUTPUT_FILE"
|
||||
value = "/tmp/trivy.json"
|
||||
}
|
||||
security_context {
|
||||
run_as_non_root = true
|
||||
run_as_user = 1000
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,9 @@ trivy plugin install scan2html
|
||||
trivy k8s --report=${REPORT_TYPE:-all} \
|
||||
--exit-code=0 \
|
||||
--severity=${LEVELS:-CRITICAL,HIGH,MEDIUM} \
|
||||
-o tmp/trivy.json \
|
||||
--scanners ${SCANNERS:-vuln,misconfig} \
|
||||
-o /tmp/trivy.json \
|
||||
--format json \
|
||||
--disable-node-collector \
|
||||
--parallel ${PARALLEL:-5}
|
||||
trivy scan2html generate --scan2html-flags --output tmp/report.html --from tmp/trivy.json
|
||||
trivy scan2html generate --scan2html-flags --output /tmp/report.html --from /tmp/trivy.json
|
||||
@@ -3,6 +3,455 @@ import os
|
||||
import time
|
||||
import smtplib
|
||||
from email.message import EmailMessage
|
||||
import json
|
||||
import re
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Any
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
# Imports and Configuration
|
||||
@dataclass
|
||||
class Misconfiguration:
|
||||
"""Represents a Trivy misconfiguration finding."""
|
||||
status: str
|
||||
message: str
|
||||
severity: str
|
||||
check_id: str = ""
|
||||
title: str = ""
|
||||
check_namespace: str = ""
|
||||
primary_url: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Vulnerability:
|
||||
"""Represents a Trivy vulnerability finding."""
|
||||
vulnerability_id: str
|
||||
package_name: str
|
||||
severity: str
|
||||
target: str = ""
|
||||
vuln_type: str = ""
|
||||
class_name: str = ""
|
||||
installed_version: str = ""
|
||||
fixed_version: str = ""
|
||||
title: str = ""
|
||||
|
||||
@dataclass
|
||||
class Resource:
|
||||
"""Represents a Trivy scan resource."""
|
||||
type: str
|
||||
name: str
|
||||
kind: Optional[str] = None
|
||||
namespace: Optional[str] = None
|
||||
image_digests: List[str] = field(default_factory=list)
|
||||
metadata: List[dict] = field(default_factory=list)
|
||||
misconf_summary: dict = field(default_factory=dict)
|
||||
misconfigurations: List[Misconfiguration] = field(default_factory=list)
|
||||
vulnerabilities: List[Vulnerability] = field(default_factory=list)
|
||||
vulnerability_summary: dict = field(default_factory=dict)
|
||||
successes_count: int = 0
|
||||
failures_count: int = 0
|
||||
|
||||
@dataclass
|
||||
class ParseResult:
|
||||
"""Parsed Trivy scan result."""
|
||||
cluster_name: Optional[str] = None
|
||||
resources: List[Resource] = field(default_factory=list)
|
||||
start_time: Optional[datetime] = None
|
||||
end_time: Optional[datetime] = None
|
||||
duration: Optional[float] = None
|
||||
image: Optional[str] = None
|
||||
|
||||
# Configuration
|
||||
PUSHGATEWAY_URL = os.environ.get("PUSHGATEWAY_URL", "http://pushgateway:9091/metrics")
|
||||
PUSHGATEWAY_NAMESPACE = os.environ.get("PUSHGATEWAY_NAMESPACE", "trivy")
|
||||
PUSHGATEWAY_JOB = os.environ.get("PUSHGATEWAY_JOB", "trivy-metrics")
|
||||
METRICS_PREFIX = "trivy"
|
||||
REQUEST_TIMEOUT = 30
|
||||
REPORT_PATH = os.environ.get("REPORT_PATH", "/tmp/report.html")
|
||||
TRIVY_OUTPUT_FILE = os.environ.get("TRIVY_OUTPUT_FILE", "/tmp/trivy.json")
|
||||
WAIT_TIMEOUT = int(os.environ.get("WAIT_TIMEOUT", "200"))
|
||||
SLEEP_INTERVAL = int(os.environ.get("SLEEP_INTERVAL", "5") )
|
||||
|
||||
# Metric definitions
|
||||
METRICS = {
|
||||
"vulnerabilities_found": {
|
||||
"type": "gauge",
|
||||
"description": "Total number of vulnerabilities found",
|
||||
"labels": ["severity", "type", "package", "vulnerability_id"]
|
||||
},
|
||||
"vulnerabilities_by_severity": {
|
||||
"type": "gauge",
|
||||
"description": "Count of vulnerabilities grouped by severity",
|
||||
"labels": ["severity"]
|
||||
},
|
||||
"vulnerabilities_by_type": {
|
||||
"type": "gauge",
|
||||
"description": "Count of vulnerabilities grouped by type",
|
||||
"labels": ["type"]
|
||||
},
|
||||
"scans_completed": {
|
||||
"type": "counter",
|
||||
"description": "Total number of scans completed",
|
||||
"labels": ["status"]
|
||||
},
|
||||
"scan_duration_seconds": {
|
||||
"type": "gauge",
|
||||
"description": "Duration of the last scan in seconds",
|
||||
"labels": ["status"]
|
||||
},
|
||||
"image_scanned": {
|
||||
"type": "gauge",
|
||||
"description": "Image that was scanned",
|
||||
"labels": ["image"]
|
||||
},
|
||||
"scan_timestamp": {
|
||||
"type": "gauge",
|
||||
"description": "Timestamp of the scan",
|
||||
"labels": []
|
||||
},
|
||||
"severities_found": {
|
||||
"type": "gauge",
|
||||
"description": "Severities found in the scan",
|
||||
"labels": ["severity"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def parse_trivy_output(output: str) -> ParseResult:
|
||||
"""Parse Trivy scan output and extract metrics."""
|
||||
# Try to parse JSON output if available
|
||||
if output.strip().startswith("{"):
|
||||
try:
|
||||
data = json.loads(output)
|
||||
# Check if it's Kubernetes/container scan JSON
|
||||
if "Resources" in data or "ClusterName" in data:
|
||||
result = ParseResult(cluster_name=data.get("ClusterName"))
|
||||
|
||||
for resource_data in data.get("Resources", []):
|
||||
misconfigurations = []
|
||||
vulnerabilities = []
|
||||
misconf_summary: dict = {}
|
||||
vulnerability_summary: dict = {}
|
||||
resource_type = "kubernetes"
|
||||
|
||||
for res in resource_data.get("Results", []):
|
||||
resource_type = res.get("Type") or resource_type
|
||||
for k, v in res.get("MisconfSummary", {}).items():
|
||||
misconf_summary[k] = misconf_summary.get(k, 0) + v
|
||||
for mc in res.get("Misconfigurations", []):
|
||||
misconfigurations.append(Misconfiguration(
|
||||
status=mc.get("Status", ""),
|
||||
message=mc.get("Message", ""),
|
||||
severity=mc.get("Severity", ""),
|
||||
check_id=mc.get("ID", ""),
|
||||
title=mc.get("Title", ""),
|
||||
check_namespace=mc.get("Namespace", ""),
|
||||
primary_url=mc.get("PrimaryURL", ""),
|
||||
))
|
||||
|
||||
for v in res.get("Vulnerabilities", []):
|
||||
severity = v.get("Severity", "UNKNOWN")
|
||||
vulnerability_summary[severity] = vulnerability_summary.get(severity, 0) + 1
|
||||
vulnerabilities.append(Vulnerability(
|
||||
vulnerability_id=v.get("VulnerabilityID", ""),
|
||||
package_name=v.get("PkgName", ""),
|
||||
severity=severity,
|
||||
target=res.get("Target", ""),
|
||||
vuln_type=res.get("Type", ""),
|
||||
class_name=res.get("Class", ""),
|
||||
installed_version=v.get("InstalledVersion", ""),
|
||||
fixed_version=v.get("FixedVersion", ""),
|
||||
title=v.get("Title", ""),
|
||||
))
|
||||
|
||||
resource = Resource(
|
||||
type=resource_type,
|
||||
name=resource_data.get("Name", ""),
|
||||
kind=resource_data.get("Kind"),
|
||||
namespace=resource_data.get("Namespace"),
|
||||
metadata=resource_data.get("Metadata", []),
|
||||
misconf_summary=misconf_summary,
|
||||
misconfigurations=misconfigurations,
|
||||
vulnerabilities=vulnerabilities,
|
||||
vulnerability_summary=vulnerability_summary,
|
||||
successes_count=misconf_summary.get("Successes", 0),
|
||||
failures_count=misconf_summary.get("Failures", 0),
|
||||
)
|
||||
result.resources.append(resource)
|
||||
return result
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# Parse text output for image scanning
|
||||
metrics_data = {
|
||||
"vulnerabilities": [],
|
||||
"image": None,
|
||||
"start_time": None,
|
||||
"end_time": None,
|
||||
"summary": {}
|
||||
}
|
||||
|
||||
# Extract image name
|
||||
image_match = re.search(r'Scanning image: (.+)', output)
|
||||
if image_match:
|
||||
metrics_data["image"] = image_match.group(1).strip()
|
||||
|
||||
# Extract scan duration
|
||||
duration_match = re.search(r'Spent.*? (\d+(?:\.\d+)?)', output)
|
||||
if duration_match:
|
||||
metrics_data["scan_duration"] = float(duration_match.group(1))
|
||||
|
||||
# Extract severity counts
|
||||
severity_patterns = {
|
||||
"CRITICAL": r'Critical:.*?(\d+)',
|
||||
"HIGH": r'High:.*?(\d+)',
|
||||
"MEDIUM": r'Medium:.*?(\d+)',
|
||||
"LOW": r'Low:.*?(\d+)',
|
||||
"UNKNOWN": r'Unknown:.*?(\d+)',
|
||||
"STYLE": r'Style:.*?(\d+)',
|
||||
"NOT_SET": r'Not Set:.*?(\d+)'
|
||||
}
|
||||
|
||||
severity_counts = {}
|
||||
for severity, pattern in severity_patterns.items():
|
||||
match = re.search(pattern, output)
|
||||
if match:
|
||||
severity_counts[severity] = int(match.group(1))
|
||||
|
||||
metrics_data["summary"] = severity_counts
|
||||
|
||||
for line in output.split('\n'):
|
||||
# Match vulnerability lines
|
||||
vuln_match = re.search(r'\[.*?\].*?CVE-?\d+\.\d+-\d+', line)
|
||||
if vuln_match:
|
||||
# Parse vulnerability details
|
||||
vuln_data = {
|
||||
"cve": None,
|
||||
"package": None,
|
||||
"type": None,
|
||||
"severity": None,
|
||||
"fix_version": None
|
||||
}
|
||||
|
||||
# Extract CVE
|
||||
cve_match = re.search(r'CVE-?\d+\.\d+-\d+', line)
|
||||
if cve_match:
|
||||
vuln_data["cve"] = cve_match.group()
|
||||
|
||||
# Extract package name
|
||||
pkg_match = re.search(r'package: (\S+)', line)
|
||||
if pkg_match:
|
||||
vuln_data["package"] = pkg_match.group(1)
|
||||
|
||||
# Extract vulnerability ID
|
||||
vuln_id_match = re.search(r'ID: ([\w\-]+)', line)
|
||||
if vuln_id_match:
|
||||
vuln_data["vulnerability_id"] = vuln_id_match.group(1)
|
||||
|
||||
# Extract severity
|
||||
severity_match = re.search(r'severity: (\S+)', line)
|
||||
if severity_match:
|
||||
vuln_data["severity"] = severity_match.group(1)
|
||||
|
||||
if any(vuln_data.values()):
|
||||
metrics_data["vulnerabilities"].append(vuln_data)
|
||||
|
||||
# Convert to ParseResult for backward compatibility
|
||||
result = ParseResult()
|
||||
result.image = metrics_data.get("image")
|
||||
result.start_time = metrics_data.get("start_time")
|
||||
result.end_time = metrics_data.get("end_time")
|
||||
result.duration = metrics_data.get("scan_duration")
|
||||
return result
|
||||
|
||||
|
||||
def generate_prometheus_metrics(scan_data: Dict[str, Any], scan_duration: Optional[float] = None) -> str:
|
||||
"""Generate Prometheus metrics format from scan data."""
|
||||
lines = []
|
||||
|
||||
# Scan metadata
|
||||
timestamp = datetime.utcnow().isoformat() + "Z"
|
||||
lines.append(f'# HELP {METRICS_PREFIX}_scan_timestamp Timestamp of the scan')
|
||||
lines.append(f'# TYPE {METRICS_PREFIX}_scan_timestamp gauge')
|
||||
lines.append(f'{METRICS_PREFIX}_scan_timestamp 1')
|
||||
|
||||
# Image scanned
|
||||
if scan_data.get("image"):
|
||||
image = scan_data["image"].replace(":", "_").replace("/", "_")
|
||||
lines.append(f'# HELP {METRICS_PREFIX}_image_scanned Image that was scanned')
|
||||
lines.append(f'# TYPE {METRICS_PREFIX}_image_scanned gauge')
|
||||
lines.append(f'{METRICS_PREFIX}_image_scanned{{image="{image}"}} 1')
|
||||
|
||||
# Duration
|
||||
if scan_duration is not None:
|
||||
lines.append(f'# HELP {METRICS_PREFIX}_scan_duration_seconds Duration of the last scan in seconds')
|
||||
lines.append(f'# TYPE {METRICS_PREFIX}_scan_duration_seconds gauge')
|
||||
lines.append(f'{METRICS_PREFIX}_scan_duration_seconds{{status="success"}} {scan_duration}')
|
||||
|
||||
# Vulnerabilities by severity
|
||||
summary = scan_data.get("summary", {})
|
||||
for severity, count in summary.items():
|
||||
if count > 0:
|
||||
lines.append(f'# HELP {METRICS_PREFIX}_vulnerabilities_by_severity Count of vulnerabilities grouped by severity')
|
||||
lines.append(f'# TYPE {METRICS_PREFIX}_vulnerabilities_by_severity gauge')
|
||||
lines.append(f'{METRICS_PREFIX}_vulnerabilities_by_severity{{severity="{severity}"}} {count}')
|
||||
|
||||
# Individual vulnerabilities
|
||||
vulnerabilities = scan_data.get("vulnerabilities", [])
|
||||
for vuln in vulnerabilities:
|
||||
cve = vuln.get("cve", "unknown")
|
||||
package = vuln.get("package", "unknown")
|
||||
vuln_id = vuln.get("vulnerability_id", cve)
|
||||
severity = vuln.get("severity", "unknown")
|
||||
|
||||
# Truncate CVE to fit in label
|
||||
cve_short = cve[-12:] if len(cve) > 12 else cve
|
||||
|
||||
lines.append(f'# HELP {METRICS_PREFIX}_vulnerabilities_found Total number of vulnerabilities found')
|
||||
lines.append(f'# TYPE {METRICS_PREFIX}_vulnerabilities_found gauge')
|
||||
labels = {
|
||||
"severity": severity,
|
||||
"type": vuln.get("type", "unknown"),
|
||||
"package": package,
|
||||
"vulnerability_id": cve_short
|
||||
}
|
||||
# Sanitize labels for Prometheus
|
||||
safe_labels = {k: str(v).replace('"', "").replace("'", "")[:20] for k, v in labels.items()}
|
||||
label_string = ",".join(f'{k}="{v}"' for k, v in safe_labels.items())
|
||||
lines.append(f'{METRICS_PREFIX}_vulnerabilities_found{{{label_string}}} 1')
|
||||
|
||||
# Clean up duplicate HELP lines
|
||||
cleaned_lines = []
|
||||
last_help = None
|
||||
for line in lines:
|
||||
if line.startswith('# HELP'):
|
||||
if last_help and line.split()[2] == last_help.split()[2]:
|
||||
continue # Skip duplicate HELP
|
||||
last_help = line.split()[2]
|
||||
cleaned_lines.append(line)
|
||||
|
||||
return '\n'.join(cleaned_lines)
|
||||
|
||||
|
||||
def push_metrics_to_pushgateway(metrics: str):
|
||||
"""Push metrics to PushGateway."""
|
||||
try:
|
||||
validated_metrics = sanitize_prometheus_metrics(metrics)
|
||||
if not validated_metrics:
|
||||
print("✗ No valid Prometheus metrics remained after sanitization")
|
||||
return False
|
||||
|
||||
req = urllib.request.Request(
|
||||
PUSHGATEWAY_URL,
|
||||
data=(validated_metrics + "\n").encode("utf-8"),
|
||||
headers={"Content-Type": "text/plain; charset=utf-8"},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as response:
|
||||
status_code = response.getcode()
|
||||
response_body = response.read().decode("utf-8", errors="replace")
|
||||
|
||||
if status_code == 200:
|
||||
print(f"✓ Successfully pushed {len(validated_metrics.split(chr(10)))} lines of metrics to PushGateway")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ Failed to push metrics to PushGateway. Status code: {status_code}")
|
||||
print(f"Response: {response_body}")
|
||||
return False
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode("utf-8", errors="replace")
|
||||
print(f"✗ Error pushing metrics to PushGateway: HTTP {e.code} {e.reason}")
|
||||
print(f"Response: {body}")
|
||||
return False
|
||||
except urllib.error.URLError as e:
|
||||
print(f"✗ Error pushing metrics to PushGateway: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _has_balanced_label_quotes(label_block: str) -> bool:
|
||||
"""Return True when all double quotes in a label block are balanced."""
|
||||
in_quotes = False
|
||||
escaped = False
|
||||
for ch in label_block:
|
||||
if escaped:
|
||||
escaped = False
|
||||
continue
|
||||
if ch == "\\":
|
||||
escaped = True
|
||||
continue
|
||||
if ch == '"':
|
||||
in_quotes = not in_quotes
|
||||
return not in_quotes
|
||||
|
||||
|
||||
def sanitize_prometheus_metrics(metrics: str) -> str:
|
||||
"""Drop malformed metric lines so one bad sample does not break the whole push."""
|
||||
sanitized_lines: List[str] = []
|
||||
dropped_lines = 0
|
||||
merged_duplicates = 0
|
||||
series_totals: Dict[str, float] = {}
|
||||
series_order: List[str] = []
|
||||
|
||||
def _format_value(value: float) -> str:
|
||||
return str(int(value)) if value.is_integer() else format(value, "g")
|
||||
|
||||
for idx, raw_line in enumerate(metrics.splitlines(), start=1):
|
||||
line = raw_line.rstrip("\r")
|
||||
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("#"):
|
||||
sanitized_lines.append(line)
|
||||
continue
|
||||
|
||||
if "{" in line:
|
||||
open_brace = line.find("{")
|
||||
close_brace = line.find("}", open_brace + 1)
|
||||
if close_brace == -1:
|
||||
print(f"⚠ Dropping malformed metric line {idx}: missing closing brace")
|
||||
dropped_lines += 1
|
||||
continue
|
||||
label_block = line[open_brace + 1:close_brace]
|
||||
if not _has_balanced_label_quotes(label_block):
|
||||
print(f"⚠ Dropping malformed metric line {idx}: unbalanced label quotes")
|
||||
dropped_lines += 1
|
||||
continue
|
||||
|
||||
parts = line.rsplit(" ", 1)
|
||||
if len(parts) != 2:
|
||||
print(f"⚠ Dropping malformed metric line {idx}: missing value")
|
||||
dropped_lines += 1
|
||||
continue
|
||||
|
||||
series_id, value_raw = parts[0], parts[1].strip()
|
||||
try:
|
||||
value = float(value_raw)
|
||||
except ValueError:
|
||||
print(f"⚠ Dropping malformed metric line {idx}: invalid numeric value '{value_raw}'")
|
||||
dropped_lines += 1
|
||||
continue
|
||||
|
||||
if series_id in series_totals:
|
||||
merged_duplicates += 1
|
||||
series_totals[series_id] += value
|
||||
else:
|
||||
series_order.append(series_id)
|
||||
series_totals[series_id] = value
|
||||
|
||||
if dropped_lines:
|
||||
print(f"⚠ Dropped {dropped_lines} malformed metric line(s) before push")
|
||||
for series_id in series_order:
|
||||
sanitized_lines.append(f"{series_id} {_format_value(series_totals[series_id])}")
|
||||
|
||||
if merged_duplicates:
|
||||
print(f"⚠ Merged {merged_duplicates} duplicate metric line(s) before push")
|
||||
|
||||
return "\n".join(sanitized_lines)
|
||||
|
||||
|
||||
def dispatch_email(report_path, subject):
|
||||
msg = EmailMessage()
|
||||
@@ -26,38 +475,253 @@ def dispatch_email(report_path, subject):
|
||||
smtp_user = os.environ.get("SMTP_USER", "user")
|
||||
smtp_pass = os.environ.get("SMTP_PASS", "pass")
|
||||
|
||||
with smtplib.SMTP(smtp_host, smtp_port) as server:
|
||||
if smtp_port == 465:
|
||||
server = smtplib.SMTP_SSL(smtp_host, smtp_port)
|
||||
else:
|
||||
server = smtplib.SMTP(smtp_host, smtp_port)
|
||||
if smtp_port != 465:
|
||||
server.starttls()
|
||||
try:
|
||||
server.login(smtp_user, smtp_pass)
|
||||
except smtplib.SMTPAuthenticationError:
|
||||
print("SMTP Authentication Error: Check your SMTP credentials.")
|
||||
return
|
||||
server.send_message(msg)
|
||||
print("Report sent.")
|
||||
if smtp_port == 465:
|
||||
server = smtplib.SMTP_SSL(smtp_host, smtp_port)
|
||||
else:
|
||||
server = smtplib.SMTP(smtp_host, smtp_port)
|
||||
server.starttls()
|
||||
try:
|
||||
server.login(smtp_user, smtp_pass)
|
||||
except smtplib.SMTPAuthenticationError:
|
||||
print("SMTP Authentication Error: Check your SMTP credentials.")
|
||||
server.quit()
|
||||
return
|
||||
server.send_message(msg)
|
||||
server.quit()
|
||||
print("Report sent.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
report_path = os.environ.get("REPORT_PATH", "/tmp/report.html")
|
||||
wait_timeout = int(os.environ.get("WAIT_TIMEOUT", "200"))
|
||||
sleep_interval = int(os.environ.get("SLEEP_INTERVAL", "5") )
|
||||
|
||||
print("Trivy report path:", report_path)
|
||||
def generate_prometheus_metrics_from_parse_result(scan_result: ParseResult) -> str:
|
||||
"""Generate Prometheus metrics from ParseResult object."""
|
||||
lines = []
|
||||
|
||||
lines.append(f'# HELP {METRICS_PREFIX}_cluster_scan_timestamp Timestamp of the cluster scan')
|
||||
lines.append(f'# TYPE {METRICS_PREFIX}_cluster_scan_timestamp gauge')
|
||||
lines.append(f'{METRICS_PREFIX}_cluster_scan_timestamp 1')
|
||||
|
||||
lines.append(f'# HELP {METRICS_PREFIX}_cluster_name Name of the cluster')
|
||||
lines.append(f'# TYPE {METRICS_PREFIX}_cluster_name gauge')
|
||||
def _safe_label(value: str) -> str:
|
||||
# Prometheus label values cannot contain unescaped control chars/newlines.
|
||||
cleaned = re.sub(r"[\x00-\x1F\x7F]+", " ", value)
|
||||
return cleaned.replace('\\', '\\\\').replace('"', '\\"')
|
||||
|
||||
def _safe_truncated_label(value: str, max_len: int) -> str:
|
||||
# Truncate before escaping so we never cut through an escape sequence.
|
||||
raw_truncated = re.sub(r"[\x00-\x1F\x7F]+", " ", value)[:max_len]
|
||||
return _safe_label(raw_truncated)
|
||||
|
||||
safe_cluster_name = _safe_label(scan_result.cluster_name or "")
|
||||
lines.append(f'{METRICS_PREFIX}_cluster_name{{cluster_name="{safe_cluster_name}"}} 1')
|
||||
|
||||
resource_type_header_written = False
|
||||
image_digest_header_written = False
|
||||
successes_header_written = False
|
||||
failures_header_written = False
|
||||
misconfig_header_written = False
|
||||
vulnerability_total_header_written = False
|
||||
vulnerability_severity_header_written = False
|
||||
vulnerability_header_written = False
|
||||
|
||||
for resource in scan_result.resources:
|
||||
rtype = _safe_label(resource.type or "")
|
||||
rname = _safe_label(resource.name or "")
|
||||
rkind = _safe_label(resource.kind or "")
|
||||
rns = _safe_label(resource.namespace or "")
|
||||
resource_labels = f'type="{rtype}",kind="{rkind}",name="{rname}",namespace="{rns}"'
|
||||
|
||||
if not resource_type_header_written:
|
||||
lines.append(f'# HELP {METRICS_PREFIX}_resource_type Type of resource')
|
||||
lines.append(f'# TYPE {METRICS_PREFIX}_resource_type gauge')
|
||||
resource_type_header_written = True
|
||||
lines.append(f'{METRICS_PREFIX}_resource_type{{{resource_labels}}} 1')
|
||||
|
||||
if resource.image_digests:
|
||||
if not image_digest_header_written:
|
||||
lines.append(f'# HELP {METRICS_PREFIX}_image_digest Image digest')
|
||||
lines.append(f'# TYPE {METRICS_PREFIX}_image_digest gauge')
|
||||
image_digest_header_written = True
|
||||
for digest in resource.image_digests:
|
||||
safe_digest = _safe_truncated_label(digest, 64)
|
||||
lines.append(f'{METRICS_PREFIX}_image_digest{{{resource_labels},digest="{safe_digest}"}} 1')
|
||||
|
||||
if resource.misconf_summary:
|
||||
summary = resource.misconf_summary
|
||||
successes = summary.get('Successes', 0)
|
||||
failures = summary.get('Failures', 0)
|
||||
|
||||
if not successes_header_written:
|
||||
lines.append(f'# HELP {METRICS_PREFIX}_misconfiguration_successes Successful checks')
|
||||
lines.append(f'# TYPE {METRICS_PREFIX}_misconfiguration_successes gauge')
|
||||
successes_header_written = True
|
||||
lines.append(f'{METRICS_PREFIX}_misconfiguration_successes{{{resource_labels}}} {successes}')
|
||||
|
||||
if not failures_header_written:
|
||||
lines.append(f'# HELP {METRICS_PREFIX}_misconfiguration_failures Failed checks')
|
||||
lines.append(f'# TYPE {METRICS_PREFIX}_misconfiguration_failures gauge')
|
||||
failures_header_written = True
|
||||
lines.append(f'{METRICS_PREFIX}_misconfiguration_failures{{{resource_labels}}} {failures}')
|
||||
|
||||
misconfig_counts = {}
|
||||
for m in resource.misconfigurations:
|
||||
key = (
|
||||
_safe_label(m.severity or ""),
|
||||
_safe_label(m.status or ""),
|
||||
_safe_truncated_label(m.check_id or "", 32),
|
||||
_safe_truncated_label(m.check_namespace or "", 64),
|
||||
_safe_truncated_label(m.title or "", 100),
|
||||
_safe_truncated_label(m.primary_url or "", 200),
|
||||
)
|
||||
misconfig_counts[key] = misconfig_counts.get(key, 0) + 1
|
||||
|
||||
for (safe_severity, safe_status, safe_check_id, safe_check_namespace, safe_title, safe_primary_url), count in misconfig_counts.items():
|
||||
if not misconfig_header_written:
|
||||
lines.append(f'# HELP {METRICS_PREFIX}_misconfiguration Misconfiguration finding')
|
||||
lines.append(f'# TYPE {METRICS_PREFIX}_misconfiguration gauge')
|
||||
misconfig_header_written = True
|
||||
lines.append(
|
||||
f'{METRICS_PREFIX}_misconfiguration{{{resource_labels},'
|
||||
f'severity="{safe_severity}",status="{safe_status}",'
|
||||
f'check_id="{safe_check_id}",check_namespace="{safe_check_namespace}",title="{safe_title}",primary_url="{safe_primary_url}"}} {count}'
|
||||
)
|
||||
|
||||
if not vulnerability_total_header_written:
|
||||
lines.append(f'# HELP {METRICS_PREFIX}_vulnerability_total Total vulnerabilities for resource')
|
||||
lines.append(f'# TYPE {METRICS_PREFIX}_vulnerability_total gauge')
|
||||
vulnerability_total_header_written = True
|
||||
lines.append(f'{METRICS_PREFIX}_vulnerability_total{{{resource_labels}}} {len(resource.vulnerabilities)}')
|
||||
|
||||
for severity, count in resource.vulnerability_summary.items():
|
||||
safe_severity = _safe_label(severity)
|
||||
if not vulnerability_severity_header_written:
|
||||
lines.append(f'# HELP {METRICS_PREFIX}_vulnerability_severity_count Vulnerabilities grouped by severity for resource')
|
||||
lines.append(f'# TYPE {METRICS_PREFIX}_vulnerability_severity_count gauge')
|
||||
vulnerability_severity_header_written = True
|
||||
lines.append(
|
||||
f'{METRICS_PREFIX}_vulnerability_severity_count{{{resource_labels},severity="{safe_severity}"}} {count}'
|
||||
)
|
||||
|
||||
vulnerability_counts = {}
|
||||
for v in resource.vulnerabilities:
|
||||
key = (
|
||||
_safe_label(v.severity or ""),
|
||||
_safe_truncated_label(v.vulnerability_id or "", 32),
|
||||
_safe_truncated_label(v.package_name or "", 64),
|
||||
_safe_truncated_label(v.target or "", 96),
|
||||
_safe_truncated_label(v.vuln_type or "", 24),
|
||||
_safe_truncated_label(v.class_name or "", 24),
|
||||
)
|
||||
vulnerability_counts[key] = vulnerability_counts.get(key, 0) + 1
|
||||
|
||||
for (safe_severity, safe_vuln_id, safe_package, safe_target, safe_vuln_type, safe_class_name), count in vulnerability_counts.items():
|
||||
if not vulnerability_header_written:
|
||||
lines.append(f'# HELP {METRICS_PREFIX}_vulnerability Vulnerability finding')
|
||||
lines.append(f'# TYPE {METRICS_PREFIX}_vulnerability gauge')
|
||||
vulnerability_header_written = True
|
||||
lines.append(
|
||||
f'{METRICS_PREFIX}_vulnerability{{{resource_labels},'
|
||||
f'severity="{safe_severity}",vulnerability_id="{safe_vuln_id}",'
|
||||
f'package="{safe_package}",target="{safe_target}",'
|
||||
f'vuln_type="{safe_vuln_type}",class="{safe_class_name}"}} {count}'
|
||||
)
|
||||
|
||||
if scan_result.duration:
|
||||
lines.append(f'# HELP {METRICS_PREFIX}_scan_duration Duration of the last scan in seconds')
|
||||
lines.append(f'# TYPE {METRICS_PREFIX}_scan_duration gauge')
|
||||
lines.append(f'{METRICS_PREFIX}_scan_duration {scan_result.duration}')
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
"""Main function to process Trivy scan output and push metrics."""
|
||||
print("🔍 Trivy Metrics Exporter")
|
||||
print(f"📍 PushGateway URL: {PUSHGATEWAY_URL}")
|
||||
print(f"🏷️ Namespace: {PUSHGATEWAY_NAMESPACE}")
|
||||
print(f"💼 Job: {PUSHGATEWAY_JOB}")
|
||||
print(f"⏳ Request timeout: {REQUEST_TIMEOUT} seconds")
|
||||
print()
|
||||
|
||||
print("Trivy report path:", REPORT_PATH)
|
||||
print("Starting to wait for the report to be generated...")
|
||||
start = time.time()
|
||||
while not os.path.exists(report_path):
|
||||
if time.time() - start > wait_timeout:
|
||||
while not os.path.exists(REPORT_PATH):
|
||||
if time.time() - start > WAIT_TIMEOUT:
|
||||
print("Timeout waiting for report to be generated.")
|
||||
dispatch_email(None, "Trivy Scan Report - Timeout")
|
||||
exit(1)
|
||||
print("Still waiting for report to be generated...")
|
||||
time.sleep(sleep_interval)
|
||||
time.sleep(SLEEP_INTERVAL)
|
||||
|
||||
print("Report generated, proceeding to send email...")
|
||||
time.sleep(5) # Wait for a bit to ensure the file is fully written
|
||||
|
||||
dispatch_email(report_path, "Trivy Scan Report - " + time.strftime("%Y-%m-%d %H:%M:%S"))
|
||||
dispatch_email(REPORT_PATH, "Trivy Scan Report - " + time.strftime("%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
if not os.path.exists(TRIVY_OUTPUT_FILE):
|
||||
print(f"✗ Output file not found: {TRIVY_OUTPUT_FILE}")
|
||||
print(" Make sure Trivy has written its output before running this script.")
|
||||
exit(1)
|
||||
|
||||
print(f"📄 Reading scan output from: {TRIVY_OUTPUT_FILE}")
|
||||
|
||||
with open(TRIVY_OUTPUT_FILE, 'r') as f:
|
||||
output = f.read()
|
||||
|
||||
print(f"📊 Parsing scan output...")
|
||||
|
||||
# Parse the output
|
||||
scan_data = parse_trivy_output(output)
|
||||
|
||||
# Handle both ParseResult (for Kubernetes scans) and old dict format (for image scans)
|
||||
if hasattr(scan_data, 'resources'):
|
||||
# ParseResult object (Kubernetes/container scan output)
|
||||
resource_count = len(scan_data.resources) if scan_data.resources else 0
|
||||
if resource_count == 0:
|
||||
print("⚠️ No vulnerabilities found in scan output")
|
||||
print(" Pushing basic scan metadata...")
|
||||
scan_data = None
|
||||
else:
|
||||
print(f"Found {resource_count} resource(s) to scan")
|
||||
# Generate Prometheus metrics
|
||||
print("🔧 Generating Prometheus metrics...")
|
||||
metrics = generate_prometheus_metrics_from_parse_result(scan_data)
|
||||
|
||||
# Push to PushGateway
|
||||
print("🚀 Pushing metrics to PushGateway...")
|
||||
success = push_metrics_to_pushgateway(metrics)
|
||||
|
||||
if success:
|
||||
print("✓ Metrics successfully pushed to PushGateway")
|
||||
|
||||
# Also print a summary to stdout for dashboards
|
||||
print("\n📈 Scan Summary:")
|
||||
print(f" Resources scanned: {resource_count}")
|
||||
total_vulns = sum(resource.misconf_summary.get('Failures', 0) for resource in scan_data.resources)
|
||||
print(f" Misconfigurations found: {total_vulns}")
|
||||
for resource in scan_data.resources:
|
||||
if resource.misconf_summary:
|
||||
print(f" Resource: {resource.name} ({resource.type})")
|
||||
for severity, count in resource.misconf_summary.items():
|
||||
if count > 0:
|
||||
print(f" - {severity}: {count}")
|
||||
else:
|
||||
print("✗ Failed to push metrics")
|
||||
exit(1)
|
||||
else:
|
||||
# Old dict format (image scan output) - keep existing behavior for backward compatibility
|
||||
print("⚠️ No vulnerabilities found in scan output")
|
||||
print(" Pushing basic scan metadata...")
|
||||
# For backward compatibility with image scans
|
||||
metrics = generate_prometheus_metrics(scan_data)
|
||||
print("🚀 Pushing metrics to PushGateway...")
|
||||
success = push_metrics_to_pushgateway(metrics)
|
||||
if success:
|
||||
print("✓ Metrics successfully pushed to PushGateway")
|
||||
else:
|
||||
print("✗ Failed to push metrics")
|
||||
exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,343 @@
|
||||
"""Unit tests for parse_trivy_output function."""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
import json
|
||||
|
||||
from send_report import (
|
||||
parse_trivy_output,
|
||||
generate_prometheus_metrics_from_parse_result,
|
||||
ParseResult,
|
||||
Resource,
|
||||
Misconfiguration,
|
||||
)
|
||||
|
||||
|
||||
class TestParseTrivyOutput:
|
||||
"""Tests for parsing Trivy JSON output."""
|
||||
|
||||
@pytest.fixture
|
||||
def fixture_path(self) -> Path:
|
||||
"""Path to the Trivy fixture file."""
|
||||
return Path(__file__).parent / "fixtures" / "trivy.json"
|
||||
|
||||
@pytest.fixture
|
||||
def fixture_data(self, fixture_path: Path) -> dict[str, Any]:
|
||||
"""Load fixture data."""
|
||||
with open(fixture_path, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
def test_parse_trivy_output_cluster_name(self, fixture_data: dict[str, Any]) -> None:
|
||||
"""Test parsing cluster name from Trivy output."""
|
||||
result = parse_trivy_output(json.dumps(fixture_data))
|
||||
assert result.cluster_name == "microk8s-prod-01"
|
||||
|
||||
def test_parse_trivy_output_results_empty(self, fixture_data: dict[str, Any]) -> None:
|
||||
"""Test when no resources are found."""
|
||||
# Modify fixture to remove resources
|
||||
modified_data = {**fixture_data, "Resources": []}
|
||||
result = parse_trivy_output(json.dumps(modified_data))
|
||||
assert result.cluster_name == "microk8s-prod-01"
|
||||
assert result.resources == []
|
||||
assert len(result.resources) == 0
|
||||
|
||||
def test_parse_trivy_output_single_resource(self, fixture_data: dict[str, Any]) -> None:
|
||||
"""Test parsing a single resource."""
|
||||
result = parse_trivy_output(json.dumps(fixture_data))
|
||||
assert isinstance(result.resources, list)
|
||||
assert len(result.resources) > 0
|
||||
|
||||
def test_parse_trivy_output_result_types(self, fixture_data: dict[str, Any]) -> None:
|
||||
"""Test result types for different resources."""
|
||||
result = parse_trivy_output(json.dumps(fixture_data))
|
||||
for resource in result.resources:
|
||||
assert hasattr(resource, "type")
|
||||
assert isinstance(resource.type, str)
|
||||
assert len(resource.type) > 0
|
||||
|
||||
def test_parse_trivy_output_metadata_list(self, fixture_data: dict[str, Any]) -> None:
|
||||
"""Test that metadata is correctly parsed as a list."""
|
||||
resource = result = parse_trivy_output(json.dumps(fixture_data)).resources[0]
|
||||
assert hasattr(resource, "metadata")
|
||||
assert isinstance(resource.metadata, list)
|
||||
|
||||
def test_parse_trivy_output_misconfiguration_summary(self, fixture_data: dict[str, Any]) -> None:
|
||||
"""Test that misconfiguration summary is correctly parsed."""
|
||||
resource = result = parse_trivy_output(json.dumps(fixture_data)).resources[0]
|
||||
assert hasattr(resource, "misconf_summary")
|
||||
assert isinstance(resource.misconf_summary, dict)
|
||||
assert "Successes" in resource.misconf_summary
|
||||
assert "Failures" in resource.misconf_summary
|
||||
|
||||
def test_parse_trivy_output_misconfigurations_list(self, fixture_data: dict[str, Any]) -> None:
|
||||
"""Test that misconfigurations are correctly parsed as a list."""
|
||||
resource = result = parse_trivy_output(json.dumps(fixture_data)).resources[0]
|
||||
assert hasattr(resource, "misconfigurations")
|
||||
assert isinstance(resource.misconfigurations, list)
|
||||
|
||||
def test_parse_trivy_output_successes_count(self, fixture_data: dict[str, Any]) -> None:
|
||||
"""Test that successes count is accurately parsed."""
|
||||
resource = result = parse_trivy_output(json.dumps(fixture_data)).resources[0]
|
||||
for misconfig in resource.misconfigurations:
|
||||
if misconfig.status == "PASS":
|
||||
resource.successes_count += 1
|
||||
assert resource.successes_count > 0
|
||||
|
||||
def test_parse_trivy_output_failures_count(self, fixture_data: dict[str, Any]) -> None:
|
||||
"""Test that failures count is accurately parsed."""
|
||||
resource = result = parse_trivy_output(json.dumps(fixture_data)).resources[0]
|
||||
for misconfig in resource.misconfigurations:
|
||||
if misconfig.status == "FAIL":
|
||||
resource.failures_count += 1
|
||||
assert resource.failures_count > 0
|
||||
|
||||
def test_parse_trivy_output_kubernetes_type(self, fixture_data: dict[str, Any]) -> None:
|
||||
"""Test parsing of Kubernetes resource type."""
|
||||
resource = result = parse_trivy_output(json.dumps(fixture_data)).resources[0]
|
||||
assert resource.type == "kubernetes"
|
||||
|
||||
def test_parse_trivy_output_resource_name(self, fixture_data: dict[str, Any]) -> None:
|
||||
"""Test parsing resource name (e.g., ClusterRole, Namespace)."""
|
||||
resource = result = parse_trivy_output(json.dumps(fixture_data)).resources[0]
|
||||
assert resource.name == "admin" or resource.name == "namespace"
|
||||
|
||||
def test_parse_trivy_output_namespace_field(self, fixture_data: dict[str, Any]) -> None:
|
||||
"""Test that namespace field is correctly extracted."""
|
||||
resource = result = parse_trivy_output(json.dumps(fixture_data)).resources[0]
|
||||
# Namespace field may be None for cluster-scoped resources
|
||||
assert hasattr(resource, "namespace")
|
||||
assert resource.namespace is None or isinstance(resource.namespace, str)
|
||||
|
||||
def test_parse_trivy_output_empty_namespaces(self, fixture_data: dict[str, Any]) -> None:
|
||||
"""Test parsing when namespaces array is empty."""
|
||||
resource = result = parse_trivy_output(json.dumps(fixture_data)).resources[0]
|
||||
if hasattr(resource, "namespaces"):
|
||||
assert isinstance(resource.namespaces, list)
|
||||
|
||||
def test_parse_trivy_output_empty_image_list(self, fixture_data: dict[str, Any]) -> None:
|
||||
"""Test parsing when images array is empty."""
|
||||
resource = result = parse_trivy_output(json.dumps(fixture_data)).resources[0]
|
||||
if hasattr(resource, "images"):
|
||||
assert isinstance(resource.images, list)
|
||||
|
||||
def test_parse_trivy_output_empty_image_digest_list(self, fixture_data: dict[str, Any]) -> None:
|
||||
"""Test parsing when image digests array is empty."""
|
||||
resource = result = parse_trivy_output(json.dumps(fixture_data)).resources[0]
|
||||
if hasattr(resource, "image_digests"):
|
||||
assert isinstance(resource.image_digests, list)
|
||||
|
||||
def test_parse_trivy_output_parses_vulnerabilities(self) -> None:
|
||||
"""Test parsing vulnerabilities from Kubernetes JSON results."""
|
||||
sample = {
|
||||
"ClusterName": "microk8s-prod-01",
|
||||
"Resources": [
|
||||
{
|
||||
"Kind": "Deployment",
|
||||
"Name": "sample-app",
|
||||
"Namespace": "default",
|
||||
"Results": [
|
||||
{
|
||||
"Class": "os-pkgs",
|
||||
"Type": "alpine",
|
||||
"Target": "sample-app:1.0",
|
||||
"Vulnerabilities": [
|
||||
{
|
||||
"VulnerabilityID": "CVE-2024-0001",
|
||||
"PkgName": "openssl",
|
||||
"InstalledVersion": "3.0.0",
|
||||
"FixedVersion": "3.0.1",
|
||||
"Severity": "HIGH",
|
||||
"Title": "Example vulnerability"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
result = parse_trivy_output(json.dumps(sample))
|
||||
assert len(result.resources) == 1
|
||||
resource = result.resources[0]
|
||||
assert len(resource.vulnerabilities) == 1
|
||||
assert resource.vulnerabilities[0].vulnerability_id == "CVE-2024-0001"
|
||||
assert resource.vulnerability_summary["HIGH"] == 1
|
||||
|
||||
|
||||
class TestGeneratePrometheusMetricsFromParseResult:
|
||||
"""Tests for generate_prometheus_metrics_from_parse_result."""
|
||||
|
||||
@pytest.fixture
|
||||
def fixture_data(self) -> dict[str, Any]:
|
||||
fixture_path = Path(__file__).parent / "fixtures" / "trivy.json"
|
||||
with open(fixture_path) as f:
|
||||
return json.load(f)
|
||||
|
||||
@pytest.fixture
|
||||
def parsed_result(self, fixture_data: dict[str, Any]) -> ParseResult:
|
||||
return parse_trivy_output(json.dumps(fixture_data))
|
||||
|
||||
@pytest.fixture
|
||||
def simple_result(self) -> ParseResult:
|
||||
return ParseResult(
|
||||
cluster_name="test-cluster",
|
||||
resources=[
|
||||
Resource(
|
||||
type="kubernetes",
|
||||
name="my-role",
|
||||
namespace="default",
|
||||
misconf_summary={"Successes": 10, "Failures": 3},
|
||||
misconfigurations=[
|
||||
Misconfiguration(
|
||||
status="FAIL",
|
||||
message="Bad config",
|
||||
severity="HIGH",
|
||||
check_id="KSV-0041",
|
||||
title="Manage secrets",
|
||||
check_namespace="builtin.kubernetes.KSV041",
|
||||
primary_url="https://avd.aquasec.com/misconfig/ksv-0041",
|
||||
),
|
||||
Misconfiguration(
|
||||
status="FAIL",
|
||||
message="Another issue",
|
||||
severity="MEDIUM",
|
||||
check_id="KSV-0114",
|
||||
title="Limit wildcard access",
|
||||
check_namespace="builtin.kubernetes.KSV114",
|
||||
primary_url="https://avd.aquasec.com/misconfig/ksv-0114",
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
def test_output_is_string(self, parsed_result: ParseResult) -> None:
|
||||
output = generate_prometheus_metrics_from_parse_result(parsed_result)
|
||||
assert isinstance(output, str)
|
||||
|
||||
def test_contains_cluster_timestamp(self, parsed_result: ParseResult) -> None:
|
||||
output = generate_prometheus_metrics_from_parse_result(parsed_result)
|
||||
assert "trivy_cluster_scan_timestamp 1" in output
|
||||
|
||||
def test_contains_cluster_name(self, parsed_result: ParseResult) -> None:
|
||||
output = generate_prometheus_metrics_from_parse_result(parsed_result)
|
||||
assert 'cluster_name="microk8s-prod-01"' in output
|
||||
|
||||
def test_contains_resource_type_metric(self, simple_result: ParseResult) -> None:
|
||||
output = generate_prometheus_metrics_from_parse_result(simple_result)
|
||||
assert "trivy_resource_type" in output
|
||||
assert 'name="my-role"' in output
|
||||
assert 'namespace="default"' in output
|
||||
|
||||
def test_contains_misconfiguration_successes(self, simple_result: ParseResult) -> None:
|
||||
output = generate_prometheus_metrics_from_parse_result(simple_result)
|
||||
assert "trivy_misconfiguration_successes" in output
|
||||
assert "} 10" in output
|
||||
|
||||
def test_contains_misconfiguration_failures(self, simple_result: ParseResult) -> None:
|
||||
output = generate_prometheus_metrics_from_parse_result(simple_result)
|
||||
assert "trivy_misconfiguration_failures" in output
|
||||
assert "} 3" in output
|
||||
|
||||
def test_contains_individual_misconfigurations(self, simple_result: ParseResult) -> None:
|
||||
output = generate_prometheus_metrics_from_parse_result(simple_result)
|
||||
assert 'severity="HIGH"' in output
|
||||
assert 'severity="MEDIUM"' in output
|
||||
assert 'status="FAIL"' in output
|
||||
|
||||
def test_contains_misconfiguration_identity_labels(self, simple_result: ParseResult) -> None:
|
||||
output = generate_prometheus_metrics_from_parse_result(simple_result)
|
||||
assert 'check_id="KSV-0041"' in output
|
||||
assert 'check_namespace="builtin.kubernetes.KSV041"' in output
|
||||
assert 'title="Manage secrets"' in output
|
||||
assert 'primary_url="https://avd.aquasec.com/misconfig/ksv-0041"' in output
|
||||
|
||||
def test_parse_trivy_output_primary_url(self, fixture_data: dict[str, Any]) -> None:
|
||||
result = parse_trivy_output(json.dumps(fixture_data))
|
||||
first_with_url = None
|
||||
for resource in result.resources:
|
||||
for misconfig in resource.misconfigurations:
|
||||
if misconfig.primary_url:
|
||||
first_with_url = misconfig
|
||||
break
|
||||
if first_with_url:
|
||||
break
|
||||
|
||||
assert first_with_url is not None
|
||||
assert first_with_url.primary_url.startswith("https://avd.aquasec.com/misconfig/")
|
||||
|
||||
def test_empty_resources_still_has_header(self) -> None:
|
||||
result = ParseResult(cluster_name="empty-cluster", resources=[])
|
||||
output = generate_prometheus_metrics_from_parse_result(result)
|
||||
assert "trivy_cluster_scan_timestamp" in output
|
||||
assert 'cluster_name="empty-cluster"' in output
|
||||
assert "trivy_resource_type" not in output
|
||||
|
||||
def test_no_misconf_summary_skips_summary_metrics(self) -> None:
|
||||
result = ParseResult(
|
||||
cluster_name="c",
|
||||
resources=[Resource(type="kubernetes", name="r", misconf_summary={})],
|
||||
)
|
||||
output = generate_prometheus_metrics_from_parse_result(result)
|
||||
assert "trivy_misconfiguration_successes" not in output
|
||||
assert "trivy_misconfiguration_failures" not in output
|
||||
|
||||
def test_duration_metric_present_when_set(self) -> None:
|
||||
result = ParseResult(cluster_name="c", resources=[], duration=42.5)
|
||||
output = generate_prometheus_metrics_from_parse_result(result)
|
||||
assert "trivy_scan_duration 42.5" in output
|
||||
|
||||
def test_duration_metric_absent_when_not_set(self) -> None:
|
||||
result = ParseResult(cluster_name="c", resources=[])
|
||||
output = generate_prometheus_metrics_from_parse_result(result)
|
||||
assert "trivy_scan_duration" not in output
|
||||
|
||||
def test_image_digest_metric_emitted(self) -> None:
|
||||
result = ParseResult(
|
||||
cluster_name="c",
|
||||
resources=[
|
||||
Resource(
|
||||
type="kubernetes",
|
||||
name="r",
|
||||
image_digests=["sha256:abc123"],
|
||||
)
|
||||
],
|
||||
)
|
||||
output = generate_prometheus_metrics_from_parse_result(result)
|
||||
assert "trivy_image_digest" in output
|
||||
assert "sha256:abc123" in output
|
||||
|
||||
def test_fixture_produces_nonempty_output(self, parsed_result: ParseResult) -> None:
|
||||
output = generate_prometheus_metrics_from_parse_result(parsed_result)
|
||||
lines = [l for l in output.splitlines() if l and not l.startswith("#")]
|
||||
assert len(lines) > 0
|
||||
|
||||
def test_prometheus_help_type_pairs(self, simple_result: ParseResult) -> None:
|
||||
output = generate_prometheus_metrics_from_parse_result(simple_result)
|
||||
lines = output.splitlines()
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith("# TYPE"):
|
||||
metric_name = line.split()[2]
|
||||
assert any(
|
||||
l.startswith(metric_name) for l in lines[i + 1 :]
|
||||
), f"No sample line found after TYPE declaration for {metric_name}"
|
||||
|
||||
def test_contains_vulnerability_metrics(self) -> None:
|
||||
result = ParseResult(
|
||||
cluster_name="test-cluster",
|
||||
resources=[
|
||||
Resource(
|
||||
type="kubernetes",
|
||||
kind="Deployment",
|
||||
name="sample-app",
|
||||
namespace="default",
|
||||
vulnerability_summary={"CRITICAL": 1, "HIGH": 2},
|
||||
vulnerabilities=[],
|
||||
)
|
||||
],
|
||||
)
|
||||
output = generate_prometheus_metrics_from_parse_result(result)
|
||||
assert "trivy_vulnerability_total" in output
|
||||
assert "trivy_vulnerability_severity_count" in output
|
||||
assert 'severity="CRITICAL"' in output
|
||||
@@ -10,7 +10,7 @@ variable "smtp_host" {
|
||||
|
||||
variable "smtp_port" {
|
||||
description = "The SMTP relay port"
|
||||
type = number
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "smtp_username" {
|
||||
@@ -43,7 +43,6 @@ variable "cron_schedule" {
|
||||
variable "tag" {
|
||||
description = "The version of Trivy to install"
|
||||
type = string
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "report_type" {
|
||||
@@ -57,3 +56,9 @@ variable "levels" {
|
||||
type = string
|
||||
default = "HIGH,CRITICAL"
|
||||
}
|
||||
|
||||
variable "pushgateway_url" {
|
||||
description = "URL of the Prometheus Pushgateway including /metrics/job/<job> path"
|
||||
type = string
|
||||
default = "http://pushgateway.prometheus.svc.cluster.local:9091/metrics/job/trivy-metrics"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user