Sercan Azizoğlu's Personal Website
December 18, 2025

Cyber Security Dashboard via Self-Hosted Grafana for Executives

Posted on December 18, 2025  •  4 minutes  • 810 words
Table of contents

Introduction

Purpose: Creating Security Dashboards for CEO, CIO, CISO, and Cyber Security Analyst Level Professionals.

Disclamer: This is an ongoning project which there could be improvements in the future.

Cover Image Credits: OpenAI's ChatGPT

Metrics rule the world. Cyber security industry and executives are not excluded from that aspect. Every year, companies allocate or cut budgets for Information Technology departments. Under the IT Department, Cyber Security takes its share. Budgets necessitate business justifications. While it’s not easy to justify and quantify some aspects of cyber security, still there could be some high level metrics prepared for those purposes.

Dashboards in Grafana via API Calls

As a learner in Grafana, I’ve deployed a self-hosted version as Docker container and call Microsoft’s Graph API for Secure Score calculation and visualization. For the future references, I’ll share some settings and configurations in that page. In next phases of the project, there will be a need to store some data for historical trend analysis.

For the next steps, there will also be different vendors as data sources other than Microsoft to create meaningful visualizations, e.g. Google and Cloudflare.

Deployment of Grafana

After installing Docker and deploying Grafana in the related OS, similar to Self-Hosted Vaultwarden Deployment that I shared previously, Grafan will be possible to work on locally. Grafana has a considerable community and plug-in support. There are also sample time series data in the deployment. It is possible to import or export dashboards or panels as JSON code.

Sample Grafana Compose Configuration File for Docker

services:
  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - ./data:/var/lib/grafana
      - ./provisioning:/etc/grafana/provisioning
    environment:
      - GF_SECURITY_ADMIN_USER=user
      - GF_SECURITY_ADMIN_PASSWORD=pass
      - GF_USERS_ALLOW_SIGN_UP=false
    networks:
      - grafana-net

networks:
  grafana-net:
    driver: bridge

Microsoft’s Secure Score

Microsoft’s Secure Score is one of the understandable metric for executives. There is a metholody and list of technical recommended actions, furthermore based on the application rate of those recommendations, they calculate a rate called Secure Score. Graph API provides last 90 days of data for secure score.

  1. First, to call API in Grafana a plug-in like Infinity is needed.
  2. For Microsoft Graph API access, an enterprise application should be created with at least those permissions based on Graph Explorer:
  1. For the Enterprise application, a secret should be created. Those three info will be requried in Infinity.
  1. In Grafana’s Connections, Data Sources section, configure Infinity for API calls

In allowed hosts, called API addresses should be allowed.

At the end, a health check will be possible to verify credentials.

Infinity Configurations:

Graph Explorer of Microsoft is available for potential tests and explorations.

Secure Score as Time Series for A Tenant

In that JSON dashboard panel code, which can be imported or edited for changes, we call secure score API and show the latest one as percentage in the gauge-meter visualization.

{
  "id": 1,
  "type": "gauge",
  "title": "Secure Score: Tenant 1",
  "gridPos": {
    "x": 15,
    "y": 10,
    "h": 10,
    "w": 4
  },
  "fieldConfig": {
    "defaults": {
      "mappings": [],
      "thresholds": {
        "mode": "percentage",
        "steps": [
          {
            "color": "red",
            "value": null
          },
          {
            "color": "semi-dark-orange",
            "value": 30
          },
          {
            "color": "super-light-orange",
            "value": 40
          },
          {
            "color": "yellow",
            "value": 50
          },
          {
            "color": "super-light-yellow",
            "value": 60
          },
          {
            "color": "light-green",
            "value": 70
          },
          {
            "color": "green",
            "value": 80
          },
          {
            "color": "semi-dark-green",
            "value": 90
          },
          {
            "color": "dark-green",
            "value": 100
          }
        ]
      },
      "color": {
        "mode": "thresholds"
      },
      "fieldMinMax": false,
      "unit": "percentunit"
    },
    "overrides": []
  },
  "transformations": [
    {
      "id": "convertFieldType",
      "options": {
        "conversions": [
          {
            "dateFormat": "YYYY-MM-DDThh:mm:ssZ",
            "destinationType": "time",
            "targetField": "createdDateTime"
          }
        ],
        "fields": {}
      }
    },
    {
      "id": "organize",
      "options": {
        "excludeByName": {
          "activeUserCount": true,
          "averageComparativeScores": true,
          "azureTenantId": true,
          "controlScores": true,
          "currentScore": true,
          "enabledServices": true,
          "id": true,
          "licensedUserCount": true,
          "maxScore": true,
          "vendorInformation": true
        },
        "includeByName": {},
        "indexByName": {},
        "orderByMode": "manual",
        "renameByName": {}
      }
    }
  ],
  "pluginVersion": "12.3.1",
  "targets": [
    {
      "cacheDurationSeconds": 300,
      "columns": [],
      "computed_columns": [
        {
          "selector": "(currentScore/maxScore)",
          "text": "Tenant 1",
          "type": "string"
        }
      ],
      "dimensionFilter": {},
      "displayName": {},
      "filters": [],
      "format": "as-is",
      "global_query_id": "",
      "mode": "time series",
      "parser": "backend",
      "refId": "A",
      "root_selector": "value[createdDateTime]",
      "source": "url",
      "type": "json",
      "url": "https://graph.microsoft.com/beta/security/secureScores",
      "url_options": {
        "data": "",
        "method": "GET"
      },
      "version": "v4"
    }
  ],
  "datasource": {
    "type": "yesoreyeram-infinity-datasource",
    "uid": "cf7cu03ep7pxcc"
  },
  "options": {
    "reduceOptions": {
      "values": false,
      "calcs": [
        "lastNotNull"
      ],
      "fields": ""
    },
    "orientation": "auto",
    "showThresholdLabels": true,
    "showThresholdMarkers": true,
    "sizing": "auto",
    "minVizWidth": 75,
    "minVizHeight": 75,
    "text": {}
  }
}

After the related configurations, the dashboard can be look like that:

For the testing purposes, there is not five different Microsoft Tenant’s that called. The response from the one is changed based on certain rates to have difference.

That is the Secure Score’s historical data seen in Microsoft Defender:

Social Media

LinkedIn