From 5fa472f45066954b2f6840109040052ad5f82063 Mon Sep 17 00:00:00 2001 From: Marsell Kukuljevic Date: Sun, 25 Aug 2024 23:00:54 +0200 Subject: [PATCH] Add initial plugin for monitoring Azure KeyVault and Firewall metrics, and Defender alerts. --- check_mk-azure/azure-spearhead-0.1.0.mkp | Bin 0 -> 5343 bytes .../base/plugins/agent_based/azure.py | 276 ++++++++++++++++++ .../share/check_mk/agents/special/agent_azure | 174 +++++++++++ .../agents/special/agent_azure_defender | 6 + .../agents/special/agent_azure_firewall | 6 + .../agents/special/agent_azure_keyvault | 6 + .../local/share/check_mk/checks/agent_azure | 24 ++ .../share/check_mk/web/plugins/wato/azure.py | 210 +++++++++++++ 8 files changed, 702 insertions(+) create mode 100755 check_mk-azure/azure-spearhead-0.1.0.mkp create mode 100644 check_mk-azure/local/lib/check_mk/base/plugins/agent_based/azure.py create mode 100755 check_mk-azure/local/share/check_mk/agents/special/agent_azure create mode 100755 check_mk-azure/local/share/check_mk/agents/special/agent_azure_defender create mode 100755 check_mk-azure/local/share/check_mk/agents/special/agent_azure_firewall create mode 100755 check_mk-azure/local/share/check_mk/agents/special/agent_azure_keyvault create mode 100644 check_mk-azure/local/share/check_mk/checks/agent_azure create mode 100644 check_mk-azure/local/share/check_mk/web/plugins/wato/azure.py diff --git a/check_mk-azure/azure-spearhead-0.1.0.mkp b/check_mk-azure/azure-spearhead-0.1.0.mkp new file mode 100755 index 0000000000000000000000000000000000000000..a790b0a31ccd084947d335fb93ebd255a9b1122b GIT binary patch literal 5343 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 measured_percent: + return State.CRIT + elif alert_percentages[0] >= measured_percent: + return State.WARN + return State.OK + + +def check_state_above(alert_percentages, measured_percent): + if alert_percentages: + if alert_percentages[1] <= measured_percent: + return State.CRIT + elif alert_percentages[0] <= measured_percent: + return State.WARN + return State.OK + + +# Convert JSON entries into dictionaries indexed by name. We're assuming here +# that the name is unique across AZs and resource groups. If not, add the +# 'location' and 'resource_group' fields in each object to the name. +def parse(string_table): + lookup = {} + + for json_data in string_table: + obj = json.loads(json_data[0]) + name = obj["name"] + group = obj["resource_group"] + lookup[f"{name}#{group}"] = obj + + return lookup + + +# Produce a list of Azure objects for discovery. +def discover(section): + for name, details in sorted(section.items()): + yield Service(item=name) + + +# Given a specific keyvault metric, look it up in the parsed output, and produce +# results on that service based upon the metric's range. +def check_keyvault(item, params, section): + vault = section.get(item) + if vault is None: + return + + metrics = vault["metrics"] + + availability = metrics.get("Availability") + capacity = metrics.get("SaturationShoebox") + latency = metrics.get("ServiceApiLatency") + hits = metrics.get("ServiceApiHit") + results = metrics.get("ServiceApiResult") + + alert_availability_percent = params.get("availability") + alert_capacity_percent = params.get("capacity") + alert_latency_milliseconds = params.get("latency") + + if availability is not None: + yield Result( + state=check_state_below(alert_availability_percent, availability), + summary=f"Availability: {availability}%", + ) + yield Metric( + name="availability", + value=availability, + boundaries=(0, 100), + ) + else: + yield Result( + state=State.UNKNOWN, + summary="Availability: N/A", + ) + + if capacity is not None: + yield Result( + state=check_state_above(alert_capacity_percent, capacity), + summary=f"Capacity: {capacity}%" + ) + yield Metric( + name="capacity", + value=capacity, + boundaries=(0, 100), + ) + else: + yield Result( + state=State.UNKNOWN, + summary="Capacity: N/A", + ) + + + if latency is not None: + yield Result( + state=check_state_above(alert_latency_milliseconds, latency), + summary=f"Latency: {latency}ms", + ) + yield Metric( + name="latency", + value=latency, + boundaries=(0, None), + ) + else: + yield Result( + state=State.UNKNOWN, + summary="Latency: N/A", + ) + + + if hits is not None: + yield Metric( + name="hits", + value=hits, + boundaries=(0, None), + ) + else: + yield Result( + state=State.UNKNOWN, + summary="Hits: N/A", + ) + + if results is not None: + yield Metric( + name="results", + value=results, + boundaries=(0, None), + ) + else: + yield Result( + state=State.UNKNOWN, + summary="Results: N/A", + ) + + +# Given a specific firewall metric, look it up in the parsed output, and produce +# results on that service based upon the metric's range. +def check_firewall(item, params, section): + firewall = section.get(item) + if firewall is None: + return + + metrics = firewall["metrics"] + + availability = metrics.get("FirewallHealth") + throughput = metrics.get("Throughput") + latency = metrics.get("FirewallLatencyPng") + + alert_availability_percent = params.get("availability") + alert_latency_milliseconds = params.get("latency") + + if availability is not None: + yield Result( + state=check_state_below(alert_availability_percent, availability), + summary=f"Availability: {availability}%", + ) + yield Metric( + name="availability", + value=availability, + boundaries=(0, 100) + ) + else: + yield Result( + state=State.UNKNOWN, + summary="Availability: N/A", + ) + + if latency is not None: + yield Result( + state=check_state_above(alert_latency_milliseconds, latency), + summary=f"Latency: {latency}ms", + ) + yield Metric( + name="latency", + value=latency, + boundaries=(0, None) + ) + else: + yield Result( + state=State.UNKNOWN, + summary="Latency: N/A", + ) + + if throughput is not None: + yield Metric( + name="throughput", + value=throughput, + boundaries=(0, None) + ) + else: + yield Result( + state=State.UNKNOWN, + summary="Throughput: N/A", + ) + +def check_defender(item, params, section): + alert = section.get(item) + if alert is None: + return + + details = alert["alert"] + status = details["status"] + + if status != "Active" and status != "InProgress": + return + + severity = details["status"] + url = details["url"] + info = details["info"] + + if severity == "High": + state = State.CRIT + elif severity == "Medium": + state = State.WARN + else: + state = State.OK + + yield Result( + state=state, + summary=f"{status}: {info}: {url}" + ) + + +register.agent_section( + name="azure_keyvault", + parse_function=parse +) + +register.check_plugin( + name="azure_keyvault", + service_name="Azure Keyvault Metric %s", + + check_function=check_keyvault, + check_default_parameters={}, + check_ruleset_name="azure_keyvault", + + discovery_function=discover, +) + +register.agent_section( + name="azure_firewall", + parse_function=parse +) + +register.check_plugin( + name="azure_firewall", + service_name="Azure Firewall Metric %s", + + check_function=check_firewall, + check_default_parameters={}, + check_ruleset_name="azure_firewall", + + discovery_function=discover, +) + +register.agent_section( + name="azure_defender", + parse_function=parse +) + +register.check_plugin( + name="azure_defender", + service_name="Azure Defender Alert %s", + + check_function=check_defender, + check_default_parameters={}, + check_ruleset_name="azure_defender", + + discovery_function=discover, +) diff --git a/check_mk-azure/local/share/check_mk/agents/special/agent_azure b/check_mk-azure/local/share/check_mk/agents/special/agent_azure new file mode 100755 index 0000000..e4567fb --- /dev/null +++ b/check_mk-azure/local/share/check_mk/agents/special/agent_azure @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# Copyright (C) 2024 Spearhead Systems SRL + +from urllib import request, parse, error +from datetime import datetime, timezone, timedelta +import json +import sys +import re + + +VAULT_METRICS = [ + 'Availability', + 'SaturationShoebox', + 'ServiceApiLatency', + 'ServiceApiHit', + 'ServiceApiResult', +] + +FIREWALL_METRICS = [ + 'FirewallHealth', + 'Throughput', + 'FirewallLatencyPng', +] + +REGION_RE = re.compile('/locations/(.+?)/') +RESOURCE_GROUP_RE = re.compile('/resourceGroups/(.+?)/') + + +# https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/request-limits-and-throttling +def get_url(req, default): + try: + res = request.urlopen(req) + return res.read() + except error.HTTPError as e: + if e.code == 429: + return default + else: + raise e + + +def get_token(tenant, username, password): + data = parse.urlencode({ + 'username': username, + 'password': password, + 'grant_type': 'password', + 'claims': '{"access_token": {"xms_cc": {"values": ["CP1"]}}}', + 'scope': 'https://management.core.windows.net//.default offline_access openid profile', + 'client_info': 1, + # This is actually the client ID of the Azure CLI tools + 'client_id': '04b07795-8ddb-461a-bbee-02f9e1bf7b46', + }) + + req = request.Request(f'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token', + data=str.encode(data)) + + res = get_url(req, None) + if res is None: + return + + token_data = json.loads(res) + token = token_data['access_token'] + return token + + +def get_json(token, path, version='2023-07-01'): + url = f"https://management.azure.com{path}{'?' in path and '&' or '?'}api-version={version}" + req = request.Request(url, headers={'Authorization': f'Bearer {token}'}) + res = get_url(req, "[]") + data = json.loads(res) + return data['value'] + + +def list_subscriptions(token): + return get_json(token, '/subscriptions') + + +def list_vaults(token, subscription): + return get_json(token, f'/subscriptions/{subscription}/resources?$filter=resourceType%20eq%20%27Microsoft.KeyVault%2Fvaults%27') + + +def list_firewalls(token, subscription): + return get_json(token, f'/subscriptions/{subscription}/resources?$filter=resourceType%20eq%20%27Microsoft.Network%2FazureFirewalls%27') + + +def list_defender_alerts(token, subscription): + return get_json(token, f'/subscriptions/{subscription}/providers/Microsoft.Security/alerts', '2022-01-01') + + +def get_recent_metrics(token, path, metrics): + end = datetime.now() + start = end - timedelta(minutes=2) + + start_str = start.isoformat().split('.')[0] + 'Z' + end_str = end.isoformat().split('.')[0] + 'Z' + metrics_str = ','.join(metrics) + + return get_json(token, f'{path}/providers/microsoft.insights/metrics?metricnames={metrics_str}×pan={start_str}/{end_str}', '2023-10-01') + + +def metrics_to_lookup(metrics): + lookup = {} + + for metric in metrics: + name = metric['name']['value'] + series = metric['timeseries'] + if series: + value = series[0]['data'][-1] + key = next(filter(lambda foo: foo != 'timeStamp', value), None) + lookup[name] = value.get(key) + + return lookup + + +def get_args(argv): + if len(argv) != 5 or argv[1] not in ['keyvault', 'firewall', 'defender']: + print(f"{sys.argv[0]} ", file=sys.stderr) + print(f"Valid commands are: 'keyvault', 'firewall', 'defender'", file=sys.stderr) + exit(1) + return argv[1], argv[2], argv[3], argv[4] + + +def print_json(obj): + print(json.dumps(obj)) + + +command, tenant, username, password = get_args(sys.argv) +token = get_token(tenant, username, password) + +for subscription in list_subscriptions(token): + subscription_id = subscription['subscriptionId'] + + if command == 'defender': + for alert in list_defender_alerts(token, subscription_id): + properties = alert['properties'] + status = properties['status'] + + if not status in ['Active', 'InProgress']: + continue + + print_json({ + 'type': command, + 'name': alert['name'], + 'location': re.search(REGION_RE, alert['id'])[1], + 'resource_group': re.search(RESOURCE_GROUP_RE, alert['id'])[1], + 'alert': { + 'status': status, + 'severity': properties['severity'], + 'url': properties['alertUri'], + 'info': properties['alertDisplayName'] + + } + }) + + elif command == 'firewall': + for firewall in list_firewalls(token, subscription_id): + metrics = get_recent_metrics(token, firewall['id'], FIREWALL_METRICS) + print_json({ + 'type': command, + 'name': firewall['name'], + 'location': firewall['location'], + 'resource_group': re.search(RESOURCE_GROUP_RE, firewall['id'])[1], + 'metrics': metrics_to_lookup(metrics), + }) + + elif command == 'keyvault': + for vault in list_vaults(token, subscription_id): + metrics = get_recent_metrics(token, vault['id'], VAULT_METRICS) + print_json({ + 'type': command, + 'name': vault['name'], + 'location': vault['location'], + 'resource_group': re.search(RESOURCE_GROUP_RE, vault['id'])[1], + 'metrics': metrics_to_lookup(metrics), + }) diff --git a/check_mk-azure/local/share/check_mk/agents/special/agent_azure_defender b/check_mk-azure/local/share/check_mk/agents/special/agent_azure_defender new file mode 100755 index 0000000..da0d32d --- /dev/null +++ b/check_mk-azure/local/share/check_mk/agents/special/agent_azure_defender @@ -0,0 +1,6 @@ +#!/bin/bash + +echo '<<>>' + +dir=$(dirname -- "${BASH_SOURCE[0]}") +"$dir"/agent_azure defender "$1" "$2" "$3" diff --git a/check_mk-azure/local/share/check_mk/agents/special/agent_azure_firewall b/check_mk-azure/local/share/check_mk/agents/special/agent_azure_firewall new file mode 100755 index 0000000..1a63013 --- /dev/null +++ b/check_mk-azure/local/share/check_mk/agents/special/agent_azure_firewall @@ -0,0 +1,6 @@ +#!/bin/bash + +echo '<<>>' + +dir=$(dirname -- "${BASH_SOURCE[0]}") +"$dir"/agent_azure firewall "$1" "$2" "$3" diff --git a/check_mk-azure/local/share/check_mk/agents/special/agent_azure_keyvault b/check_mk-azure/local/share/check_mk/agents/special/agent_azure_keyvault new file mode 100755 index 0000000..8b6e087 --- /dev/null +++ b/check_mk-azure/local/share/check_mk/agents/special/agent_azure_keyvault @@ -0,0 +1,6 @@ +#!/bin/bash + +echo '<<>>' + +dir=$(dirname -- "${BASH_SOURCE[0]}") +"$dir"/agent_azure keyvault "$1" "$2" "$3" diff --git a/check_mk-azure/local/share/check_mk/checks/agent_azure b/check_mk-azure/local/share/check_mk/checks/agent_azure new file mode 100644 index 0000000..8fc1f47 --- /dev/null +++ b/check_mk-azure/local/share/check_mk/checks/agent_azure @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# Copyright (C) 2024 Spearhead Systems SRL + +import cmk.utils.password_store + +def agent_azure_args(params, hostname, ipaddress): + # Extract password either from params, or from password store: + # ('password', ''): password is in params directly + # ('store', ''): password must be looked up in store by name + password_info = params["password"] + if password_info[0] == "password": + password = password_info[1] + else: + password = cmk.utils.password_store.extract(password_info[1]) + + return [ + params["tenant"], + params["username"], + password + ] + +special_agent_info["azure_keyvault"] = agent_azure_args +special_agent_info["azure_firewall"] = agent_azure_args +special_agent_info["azure_defender"] = agent_azure_args diff --git a/check_mk-azure/local/share/check_mk/web/plugins/wato/azure.py b/check_mk-azure/local/share/check_mk/web/plugins/wato/azure.py new file mode 100644 index 0000000..a23173c --- /dev/null +++ b/check_mk-azure/local/share/check_mk/web/plugins/wato/azure.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +# Copyright (C) 2024 Spearhead Systems SRL + +import copy +from cmk.gui.i18n import _ +from cmk.gui.plugins.wato.utils import ( + rulespec_registry, + HostRulespec, + IndividualOrStoredPassword, + RulespecGroupCheckParametersDiscovery, + CheckParameterRulespecWithItem, + RulespecGroupCheckParametersApplications, +) +from cmk.gui.watolib.rulespecs import Rulespec +from cmk.gui.valuespec import ( + Dictionary, + TextInput, + Integer, + ListOfStrings, + Password +) + + +def _discovery(title): + return Dictionary( + title=_(title), + required_keys=["tenant", "username", "password"], + elements=[ + ( + "tenant", + TextInput( + title=_("Tenant ID"), + allow_empty=False, + ), + ), + ( + "username", + TextInput( + title=_("Username"), + allow_empty=False, + ), + ), + ( + "password", + IndividualOrStoredPassword( +# Password( + title=_("Password"), + allow_empty=False, + ), + ), + ], + ) + +def _valuespec_special_agents_azure_keyvault_discovery(): + return _discovery("Azure Key Vault Metrics Discovery") + +def _valuespec_special_agents_azure_firewall_discovery(): + return _discovery("Azure Firewall Metrics Discovery") + +def _valuespec_special_agents_azure_defender_discovery(): + return _discovery("Azure Defender Alerts Discovery") + +def _valuespec_special_agents_azure_keyvault_check(): + return Dictionary( + title=_("Azure Key Vault Metric Checks"), + elements=[ + ( + "availability", + Tuple( + title=_("Availability"), + help=_("If drops below these percentages over the past minute, issue alert"), + elements=[ + Percentage( + title=_("Warn if below"), + default_value=98 + ), + Percentage( + title=_("Crit if below"), + default_value=90 + ) + ] + ) + ), + ( + "capacity", + Tuple( + title=_("Capacity used"), + help=_("If goes above these percentages over the past minute, issue alert"), + elements=[ + Percentage( + title=_("Warn if above"), + default_value=80 + ), + Percentage( + title=_("Crit if above"), + default_value=98 + ) + ] + ) + ), + ( + "latency", + Tuple( + title=_("Request latency"), + help=_("If goes above the average milliseconds over the past minute, issue alert"), + elements=[ + Integer( + title=_("Warn if above"), + default_value=100, + minvalue=0, + ), + Integer( + title=_("Crit if above"), + default_value=2000, + minvalue=0, + ) + ] + ) + ), + ], + ) + +def _valuespec_special_agents_azure_firewall_check(): + return Dictionary( + title=_("Azure Firewall Metric Checks"), + elements=[ + ( + "availability", + Tuple( + title=_("Availability"), + help=_("If drops below these percentages over the past minute, issue alert"), + elements=[ + Percentage( + title=_("Warn if below"), + default_value=98 + ), + Percentage( + title=_("Crit if below"), + default_value=90 + ) + ] + ) + ), + ( + "latency", + Tuple( + title=_("Request latency"), + help=_("If goes above the average milliseconds over the past minute, issue alert"), + elements=[ + Integer( + title=_("Warn if above"), + default_value=100, + minvalue=0, + ), + Integer( + title=_("Crit if above"), + default_value=2000, + minvalue=0, + ) + ] + ) + ), + ], + ) + +rulespec_registry.register( + HostRulespec( + name="special_agents:azure_keyvault", + group=RulespecGroupCheckParametersDiscovery, + match_type='dict', + valuespec=_valuespec_special_agents_azure_keyvault_discovery, + ) +) +rulespec_registry.register( + HostRulespec( + name="special_agents:azure_firewall", + group=RulespecGroupCheckParametersDiscovery, + match_type='dict', + valuespec=_valuespec_special_agents_azure_firewall_discovery, + ) +) +rulespec_registry.register( + HostRulespec( + name="special_agents:azure_defender", + group=RulespecGroupCheckParametersDiscovery, + match_type='dict', + valuespec=_valuespec_special_agents_azure_defender_discovery, + ) +) + +rulespec_registry.register( + CheckParameterRulespecWithItem( + check_group_name="azure_keyvault", + group=RulespecGroupCheckParametersApplications, + match_type="dict", + parameter_valuespec=_valuespec_special_agents_azure_keyvault_check, + item_spec=lambda: TextInput(title=_("Key Vault")), + title=lambda: _("Azure Key Vault Metrics"), + ) +) +rulespec_registry.register( + CheckParameterRulespecWithItem( + check_group_name="azure_firewall", + group=RulespecGroupCheckParametersApplications, + match_type="dict", + parameter_valuespec=_valuespec_special_agents_azure_firewall_check, + item_spec=lambda: TextInput(title=_("Firewall")), + title=lambda: _("Azure Firewall Metrics"), + ) +)