{
  "__inputs": [
    {
      "name": "DS_THREATMAP",
      "label": "Threatmap Postgres",
      "type": "datasource",
      "pluginId": "grafana-postgresql-datasource",
      "pluginName": "PostgreSQL",
      "description": "PostgreSQL database loaded by the threatmap-feeds n8n workflows"
    }
  ],
  "__requires": [
    {
      "type": "datasource",
      "id": "grafana-postgresql-datasource",
      "name": "PostgreSQL"
    },
    {
      "type": "panel",
      "id": "barchart",
      "name": "barchart"
    },
    {
      "type": "panel",
      "id": "bargauge",
      "name": "bargauge"
    },
    {
      "type": "panel",
      "id": "stat",
      "name": "stat"
    },
    {
      "type": "panel",
      "id": "table",
      "name": "table"
    },
    {
      "type": "panel",
      "id": "timeseries",
      "name": "timeseries"
    }
  ],
  "uid": "tm-pipeline",
  "title": "Threat map: pipeline health",
  "description": "Is the n8n threat-map pipeline healthy? Freshness per source, quotas and enrichment coverage.",
  "tags": [
    "threatmap"
  ],
  "timezone": "browser",
  "schemaVersion": 41,
  "editable": true,
  "graphTooltip": 1,
  "time": {
    "from": "now-24h",
    "to": "now"
  },
  "refresh": "1m",
  "templating": {
    "list": []
  },
  "annotations": {
    "list": []
  },
  "panels": [
    {
      "id": 17,
      "type": "table",
      "title": "Ingest status",
      "description": "Stale = no new rows for twice the source's expected cadence (Fortinet 30 min, feeds 1 h, AbuseIPDB blacklist 1 day, enrichment 10 min, GreyNoise 1 h). Fortinet publishes in bursts, so short gaps are normal.",
      "gridPos": {
        "x": 0,
        "y": 0,
        "w": 14,
        "h": 11
      },
      "datasource": {
        "type": "grafana-postgresql-datasource",
        "uid": "${DS_THREATMAP}"
      },
      "targets": [
        {
          "refId": "A",
          "datasource": {
            "type": "grafana-postgresql-datasource",
            "uid": "${DS_THREATMAP}"
          },
          "rawQuery": true,
          "editorMode": "code",
          "format": "table",
          "rawSql": "select source,\n       case when now() - last_ingested > 2 * \ncase\n  when source = 'fortinet' then interval '30 minutes'\n  when source = 'abuseipdb' then interval '1 day'\n  when source = 'enrich:greynoise' then interval '1 hour'\n  when source like 'enrich:%' then interval '10 minutes'\n  else interval '1 hour'\nend then 'Stale' else 'OK' end as status,\n       last_ingested,\n       round(extract(epoch from now() - last_ingested) / 60) as minutes_ago,\n       rows_last_hour, rows_total\nfrom ingest_health\norder by status desc, source"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "custom": {
            "align": "auto",
            "filterable": true
          }
        },
        "overrides": [
          {
            "matcher": {
              "id": "byName",
              "options": "status"
            },
            "properties": [
              {
                "id": "custom.cellOptions",
                "value": {
                  "type": "color-background",
                  "mode": "basic"
                }
              },
              {
                "id": "mappings",
                "value": [
                  {
                    "type": "value",
                    "options": {
                      "OK": {
                        "color": "#0ca30c",
                        "text": "✓ OK",
                        "index": 0
                      },
                      "Stale": {
                        "color": "#d03b3b",
                        "text": "✕ Stale",
                        "index": 1
                      }
                    }
                  }
                ]
              }
            ]
          },
          {
            "matcher": {
              "id": "byName",
              "options": "last_ingested"
            },
            "properties": [
              {
                "id": "unit",
                "value": "dateTimeFromNow"
              }
            ]
          }
        ]
      },
      "options": {
        "showHeader": true,
        "cellHeight": "sm"
      }
    },
    {
      "id": 18,
      "type": "stat",
      "title": "Unique IPs (all time)",
      "description": "",
      "gridPos": {
        "x": 14,
        "y": 0,
        "w": 5,
        "h": 4
      },
      "datasource": {
        "type": "grafana-postgresql-datasource",
        "uid": "${DS_THREATMAP}"
      },
      "targets": [
        {
          "refId": "A",
          "datasource": {
            "type": "grafana-postgresql-datasource",
            "uid": "${DS_THREATMAP}"
          },
          "rawQuery": true,
          "editorMode": "code",
          "format": "table",
          "rawSql": "select count(distinct ip) from ip_sightings"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "unit": "short",
          "color": {
            "mode": "fixed",
            "fixedColor": "#3987e5"
          }
        },
        "overrides": []
      },
      "options": {
        "reduceOptions": {
          "calcs": [
            "lastNotNull"
          ],
          "fields": "",
          "values": false
        },
        "colorMode": "value",
        "graphMode": "none",
        "textMode": "value",
        "justifyMode": "auto"
      }
    },
    {
      "id": 19,
      "type": "stat",
      "title": "Database size",
      "description": "",
      "gridPos": {
        "x": 19,
        "y": 0,
        "w": 5,
        "h": 4
      },
      "datasource": {
        "type": "grafana-postgresql-datasource",
        "uid": "${DS_THREATMAP}"
      },
      "targets": [
        {
          "refId": "A",
          "datasource": {
            "type": "grafana-postgresql-datasource",
            "uid": "${DS_THREATMAP}"
          },
          "rawQuery": true,
          "editorMode": "code",
          "format": "table",
          "rawSql": "select pg_database_size('threatmap')"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "unit": "bytes",
          "color": {
            "mode": "fixed",
            "fixedColor": "#3987e5"
          }
        },
        "overrides": []
      },
      "options": {
        "reduceOptions": {
          "calcs": [
            "lastNotNull"
          ],
          "fields": "",
          "values": false
        },
        "colorMode": "value",
        "graphMode": "none",
        "textMode": "value",
        "justifyMode": "auto"
      }
    },
    {
      "id": 20,
      "type": "stat",
      "title": "Stale sources",
      "description": "Count of sources with no new rows for twice their expected cadence.",
      "gridPos": {
        "x": 14,
        "y": 4,
        "w": 10,
        "h": 7
      },
      "datasource": {
        "type": "grafana-postgresql-datasource",
        "uid": "${DS_THREATMAP}"
      },
      "targets": [
        {
          "refId": "A",
          "datasource": {
            "type": "grafana-postgresql-datasource",
            "uid": "${DS_THREATMAP}"
          },
          "rawQuery": true,
          "editorMode": "code",
          "format": "table",
          "rawSql": "select count(*) from ingest_health where now() - last_ingested > 2 * \ncase\n  when source = 'fortinet' then interval '30 minutes'\n  when source = 'abuseipdb' then interval '1 day'\n  when source = 'enrich:greynoise' then interval '1 hour'\n  when source like 'enrich:%' then interval '10 minutes'\n  else interval '1 hour'\nend"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "unit": "short",
          "color": {
            "mode": "thresholds",
            "fixedColor": "#3987e5"
          },
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "#0ca30c",
                "value": null
              },
              {
                "color": "#d03b3b",
                "value": 1
              }
            ]
          }
        },
        "overrides": []
      },
      "options": {
        "reduceOptions": {
          "calcs": [
            "lastNotNull"
          ],
          "fields": "",
          "values": false
        },
        "colorMode": "value",
        "graphMode": "none",
        "textMode": "value",
        "justifyMode": "auto"
      }
    },
    {
      "id": 21,
      "type": "barchart",
      "title": "Enrichment coverage, IPs seen in the last 7 days (%)",
      "description": "Share of recently listed IPs that each provider has looked up. Limited by free-tier quotas.",
      "gridPos": {
        "x": 0,
        "y": 11,
        "w": 12,
        "h": 9
      },
      "datasource": {
        "type": "grafana-postgresql-datasource",
        "uid": "${DS_THREATMAP}"
      },
      "targets": [
        {
          "refId": "A",
          "datasource": {
            "type": "grafana-postgresql-datasource",
            "uid": "${DS_THREATMAP}"
          },
          "rawQuery": true,
          "editorMode": "code",
          "format": "table",
          "rawSql": "with recent as (select distinct ip from ip_sightings where seen_on >= current_date - 7)\nselect p.provider,\n       round(100.0 * count(e.ip) / nullif((select count(*) from recent), 0), 1) as pct\nfrom (values ('ipapi'), ('internetdb'), ('otx'), ('abuseipdb'), ('greynoise')) as p(provider)\nleft join enrichment e on e.provider = p.provider and e.ip in (select ip from recent)\ngroup by p.provider order by pct desc"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "fixed",
            "fixedColor": "#3987e5"
          },
          "custom": {
            "fillOpacity": 90,
            "lineWidth": 0
          }
        },
        "overrides": []
      },
      "options": {
        "orientation": "horizontal",
        "showValue": "auto",
        "barWidth": 0.7,
        "groupWidth": 0.7,
        "legend": {
          "showLegend": false,
          "displayMode": "list",
          "placement": "bottom"
        },
        "tooltip": {
          "mode": "single"
        },
        "xTickLabelMaxLength": 28
      }
    },
    {
      "id": 22,
      "type": "bargauge",
      "title": "API quota used today (UTC)",
      "description": "Lookups made since 00:00 UTC, against each free plan's daily limit. Re-lookups of expired rows count too.",
      "gridPos": {
        "x": 12,
        "y": 11,
        "w": 12,
        "h": 9
      },
      "datasource": {
        "type": "grafana-postgresql-datasource",
        "uid": "${DS_THREATMAP}"
      },
      "targets": [
        {
          "refId": "A",
          "datasource": {
            "type": "grafana-postgresql-datasource",
            "uid": "${DS_THREATMAP}"
          },
          "rawQuery": true,
          "editorMode": "code",
          "format": "table",
          "rawSql": "select 'AbuseIPDB checks (of 1000)' as quota, count(*) as used, 1000 as max\nfrom enrichment where provider = 'abuseipdb' and fetched_at >= date_trunc('day', now())\nunion all\nselect 'GreyNoise lookups (of 25)', count(*), 25\nfrom enrichment where provider = 'greynoise' and fetched_at >= date_trunc('day', now())"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "min": 0,
          "unit": "short",
          "thresholds": {
            "mode": "percentage",
            "steps": [
              {
                "color": "#0ca30c",
                "value": null
              },
              {
                "color": "#fab219",
                "value": 80
              },
              {
                "color": "#d03b3b",
                "value": 95
              }
            ]
          },
          "color": {
            "mode": "thresholds"
          }
        },
        "overrides": [
          {
            "matcher": {
              "id": "byName",
              "options": "max"
            },
            "properties": [
              {
                "id": "custom.hidden",
                "value": true
              }
            ]
          }
        ]
      },
      "options": {
        "orientation": "horizontal",
        "displayMode": "basic",
        "showUnfilled": true,
        "valueMode": "text",
        "reduceOptions": {
          "calcs": [
            "lastNotNull"
          ],
          "fields": "/^used$/",
          "values": true
        },
        "minVizHeight": 16,
        "maxVizHeight": 24
      }
    },
    {
      "id": 23,
      "type": "timeseries",
      "title": "Enrichment lookups per hour by provider",
      "description": "Counts each IP's latest lookup only (re-lookups replace the row).",
      "gridPos": {
        "x": 0,
        "y": 20,
        "w": 12,
        "h": 10
      },
      "datasource": {
        "type": "grafana-postgresql-datasource",
        "uid": "${DS_THREATMAP}"
      },
      "targets": [
        {
          "refId": "A",
          "datasource": {
            "type": "grafana-postgresql-datasource",
            "uid": "${DS_THREATMAP}"
          },
          "rawQuery": true,
          "editorMode": "code",
          "format": "time_series",
          "rawSql": "select $__timeGroupAlias(fetched_at, '1h'), provider as metric, count(*) as value\nfrom enrichment where $__timeFilter(fetched_at)\ngroup by 1, 2 order by 1"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "unit": "short",
          "custom": {
            "drawStyle": "line",
            "lineWidth": 2,
            "fillOpacity": 0,
            "barAlignment": 0,
            "stacking": {
              "mode": "none"
            },
            "showPoints": "never",
            "axisSoftMin": 0
          }
        },
        "overrides": [
          {
            "matcher": {
              "id": "byName",
              "options": "ipapi"
            },
            "properties": [
              {
                "id": "color",
                "value": {
                  "mode": "fixed",
                  "fixedColor": "#3987e5"
                }
              }
            ]
          },
          {
            "matcher": {
              "id": "byName",
              "options": "internetdb"
            },
            "properties": [
              {
                "id": "color",
                "value": {
                  "mode": "fixed",
                  "fixedColor": "#d95926"
                }
              }
            ]
          },
          {
            "matcher": {
              "id": "byName",
              "options": "greynoise"
            },
            "properties": [
              {
                "id": "color",
                "value": {
                  "mode": "fixed",
                  "fixedColor": "#199e70"
                }
              }
            ]
          },
          {
            "matcher": {
              "id": "byName",
              "options": "abuseipdb"
            },
            "properties": [
              {
                "id": "color",
                "value": {
                  "mode": "fixed",
                  "fixedColor": "#c98500"
                }
              }
            ]
          },
          {
            "matcher": {
              "id": "byName",
              "options": "otx"
            },
            "properties": [
              {
                "id": "color",
                "value": {
                  "mode": "fixed",
                  "fixedColor": "#d55181"
                }
              }
            ]
          }
        ]
      },
      "options": {
        "legend": {
          "displayMode": "list",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "multi",
          "sort": "desc"
        }
      }
    },
    {
      "id": 24,
      "type": "timeseries",
      "title": "Fortinet events ingested per 10 minutes",
      "description": "Fortinet's feed updates in bursts; flat stretches are normal, a flat line for hours is not.",
      "gridPos": {
        "x": 12,
        "y": 20,
        "w": 12,
        "h": 10
      },
      "datasource": {
        "type": "grafana-postgresql-datasource",
        "uid": "${DS_THREATMAP}"
      },
      "targets": [
        {
          "refId": "A",
          "datasource": {
            "type": "grafana-postgresql-datasource",
            "uid": "${DS_THREATMAP}"
          },
          "rawQuery": true,
          "editorMode": "code",
          "format": "time_series",
          "rawSql": "select $__timeGroupAlias(ingested_at, '10m'), 'fortinet' as metric, count(*) as value\nfrom attack_events where $__timeFilter(ingested_at)\ngroup by 1, 2 order by 1"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "unit": "short",
          "custom": {
            "drawStyle": "line",
            "lineWidth": 2,
            "fillOpacity": 0,
            "barAlignment": 0,
            "stacking": {
              "mode": "none"
            },
            "showPoints": "never",
            "axisSoftMin": 0
          }
        },
        "overrides": [
          {
            "matcher": {
              "id": "byName",
              "options": "fortinet"
            },
            "properties": [
              {
                "id": "color",
                "value": {
                  "mode": "fixed",
                  "fixedColor": "#008300"
                }
              }
            ]
          }
        ]
      },
      "options": {
        "legend": {
          "displayMode": "list",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "multi",
          "sort": "desc"
        }
      }
    },
    {
      "id": 25,
      "type": "table",
      "title": "Enrichment results by provider",
      "description": "not_found = the provider has no data on the IP (cached so the quota isn't spent again).",
      "gridPos": {
        "x": 0,
        "y": 30,
        "w": 12,
        "h": 8
      },
      "datasource": {
        "type": "grafana-postgresql-datasource",
        "uid": "${DS_THREATMAP}"
      },
      "targets": [
        {
          "refId": "A",
          "datasource": {
            "type": "grafana-postgresql-datasource",
            "uid": "${DS_THREATMAP}"
          },
          "rawQuery": true,
          "editorMode": "code",
          "format": "table",
          "rawSql": "select provider, status, count(*) as ips, max(fetched_at) as last_lookup\nfrom enrichment group by 1, 2 order by 1, 2"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "custom": {
            "align": "auto",
            "filterable": true
          }
        },
        "overrides": []
      },
      "options": {
        "showHeader": true,
        "cellHeight": "sm"
      }
    },
    {
      "id": 26,
      "type": "table",
      "title": "Feed rows by day",
      "description": "",
      "gridPos": {
        "x": 12,
        "y": 30,
        "w": 12,
        "h": 8
      },
      "datasource": {
        "type": "grafana-postgresql-datasource",
        "uid": "${DS_THREATMAP}"
      },
      "targets": [
        {
          "refId": "A",
          "datasource": {
            "type": "grafana-postgresql-datasource",
            "uid": "${DS_THREATMAP}"
          },
          "rawQuery": true,
          "editorMode": "code",
          "format": "table",
          "rawSql": "select seen_on as day, source, count(*) as ips from ip_sightings\nwhere seen_on >= current_date - 14 group by 1, 2 order by 1 desc, 2"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "custom": {
            "align": "auto",
            "filterable": true
          }
        },
        "overrides": []
      },
      "options": {
        "showHeader": true,
        "cellHeight": "sm"
      }
    }
  ]
}
