From bd7027e93bd8973d1a5cfd326a6858bdb3edf7bb Mon Sep 17 00:00:00 2001 From: Marsell Kukuljevic Date: Sat, 24 Aug 2024 12:26:38 +0200 Subject: [PATCH] Initial commit. --- .../base/plugins/agent_based/azure.py | 99 +++++++++++ .../share/check_mk/agents/special/agent_azure | 160 ++++++++++++++++++ .../special/agent_azure_defender_alerts | 1 + .../special/agent_azure_firewall_metrics | 1 + .../special/agent_azure_keyvault_metrics | 1 + .../local/share/check_mk/checks/agent_azure | 22 +++ .../share/check_mk/web/plugins/wato/azure.py | 125 ++++++++++++++ 7 files changed, 409 insertions(+) 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 120000 check_mk-azure/local/share/check_mk/agents/special/agent_azure_defender_alerts create mode 120000 check_mk-azure/local/share/check_mk/agents/special/agent_azure_firewall_metrics create mode 120000 check_mk-azure/local/share/check_mk/agents/special/agent_azure_keyvault_metrics 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/local/lib/check_mk/base/plugins/agent_based/azure.py b/check_mk-azure/local/lib/check_mk/base/plugins/agent_based/azure.py new file mode 100644 index 0000000..616983c --- /dev/null +++ b/check_mk-azure/local/lib/check_mk/base/plugins/agent_based/azure.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# Copyright (C) 2024 Spearhead Systems SRL + +import json +from datetime import datetime, timezone +from cmk.base.plugins.agent_based.agent_based_api.v1 import register, Result, Service, State + + +# 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_str in string_table: + obj = json.loads(json_str) + name = obj["name"] + group = obj["resource_group"] + lookup[f"{name}#{resource_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 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): + warn_days = params.get("warn_days") + crit_days = params.get("crit_days") + + cert = section.get(item) + if cert is None: + return + + expires = datetime.fromisoformat(cert["attributes"]["expires"]) + now = datetime.now(timezone.utc) + remaining_days = (expires - now).days + + state = State.OK + if crit_days is not None and remaining_days < crit_days: + state = State.CRIT + elif warn_days is not None and remaining_days < warn_days: + state = State.WARN + + yield Result(state=state, summary="Expires in %d days" % remaining_days) + + +register.agent_section( + name="azure_keyvault_metrics", + parse_function=parse +) + +register.check_plugin( + name="azure_keyvault_metrics", + service_name="Azure Keyvault Metric %s", + + check_function=check_keyvault, + check_default_parameters={}, + check_ruleset_name="azure_keyvault_metrics", + + discovery_function=discover, +) + +register.agent_section( + name="azure_firewall_metrics", + parse_function=parse +) + +register.check_plugin( + name="azure_firewall_metrics", + service_name="Azure Firewall Metric %s", + + check_function=check_keyvault, + check_default_parameters={}, + check_ruleset_name="azure_firewall_metrics", + + discovery_function=discover, +) + +register.agent_section( + name="azure_defender_alerts", + parse_function=parse +) + +register.check_plugin( + name="azure_defender_alerts", + service_name="Azure Defender Alert %s", + + check_function=check_keyvault, + check_default_parameters={}, + check_ruleset_name="azure_defender_alerts", + + 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..7fe82f7 --- /dev/null +++ b/check_mk-azure/local/share/check_mk/agents/special/agent_azure @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# Copyright (C) 2024 Spearhead Systems SRL + +from urllib import request, parse +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/(.+?)/') + + +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 = request.urlopen(req) + + token_data = json.loads(res.read()) + 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 = request.urlopen(req) + data = json.loads(res.read()) + 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 not argv[1] 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_alerts b/check_mk-azure/local/share/check_mk/agents/special/agent_azure_defender_alerts new file mode 120000 index 0000000..a488f6d --- /dev/null +++ b/check_mk-azure/local/share/check_mk/agents/special/agent_azure_defender_alerts @@ -0,0 +1 @@ +agent_azure \ No newline at end of file diff --git a/check_mk-azure/local/share/check_mk/agents/special/agent_azure_firewall_metrics b/check_mk-azure/local/share/check_mk/agents/special/agent_azure_firewall_metrics new file mode 120000 index 0000000..a488f6d --- /dev/null +++ b/check_mk-azure/local/share/check_mk/agents/special/agent_azure_firewall_metrics @@ -0,0 +1 @@ +agent_azure \ No newline at end of file diff --git a/check_mk-azure/local/share/check_mk/agents/special/agent_azure_keyvault_metrics b/check_mk-azure/local/share/check_mk/agents/special/agent_azure_keyvault_metrics new file mode 120000 index 0000000..a488f6d --- /dev/null +++ b/check_mk-azure/local/share/check_mk/agents/special/agent_azure_keyvault_metrics @@ -0,0 +1 @@ +agent_azure \ No newline at end of file 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..4e17882 --- /dev/null +++ b/check_mk-azure/local/share/check_mk/checks/agent_azure @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# Copyright (C) 2024 Spearhead Systems SRL + +def get_params(params): + return params["tenant"], params["client"], params["secret"] + +def agent_azure_keyvault_metrics(params, hostname, ipaddress): + tenant, client, secret = get_params(params) + return ["keyvault", tenant, client, secret] + +def agent_azure_firewall_metrics(params, hostname, ipaddress): + tenant, client, secret = get_params(params) + return ["firewall", tenant, client, secret] + +def agent_azure_defender_alerts(params, hostname, ipaddress): + tenant, client, secret = get_params(params) + return ["defender", tenant, client, secret] + +special_agent_info["azure_keyvault_metrics"] = agent_azure_keyvault_metrics +special_agent_info["azure_firewall_metrics"] = agent_azure_firewall_metrics +special_agent_info["azure_defender_alerts"] = agent_azure_defender_alerts + 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..ef1f3e2 --- /dev/null +++ b/check_mk-azure/local/share/check_mk/web/plugins/wato/azure.py @@ -0,0 +1,125 @@ +#!/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 _valuespec_special_agents_azure_discovery(): + return Dictionary( + title=_("Azure Discovery"), + elements=[ + ( + "tenant", + TextInput( + title=_("Tenant ID / Directory ID"), + allow_empty=False, + size=45, + ), + ), + ( + "client", + TextInput( + title=_("Client ID / Application ID"), + allow_empty=False, + size=45, + ), + ), + ( + "secret", + IndividualOrStoredPassword( +# Password( + title=_("Client Secret"), + allow_empty=False, + size=45, + ), + ), + ], + ) + +def _valuespec_special_agents_azure_keyvault_metric_check(): + return Dictionary( + title=_("Azure Key Vault Metric Checks"), + optional_keys=["warn_percent", "crit_percent"], + elements=[ + ( + "warn_percent", + Integer( + minvalue=0, + default_value=98, + title=_("Warn when percentage falls below this threshold"), + ), + ), + ( + "crit_percent", + Integer( + minvalue=0, + default_value=90, + title=_("Warn when percentage falls below this threshold"), + ), + ), + ], + ) + +def _valuespec_special_agents_azure_firewall_metric_check(): + return _valuespec_special_agents_azure_keyvault_metric_check() + + +rulespec_registry.register( + HostRulespec( + name="special_agents:azure_keyvault_metrics", + group=RulespecGroupCheckParametersDiscovery, + match_type='dict', + valuespec=_valuespec_special_agents_azure_discovery, + ) +) +rulespec_registry.register( + HostRulespec( + name="special_agents:azure_firewall_metrics", + group=RulespecGroupCheckParametersDiscovery, + match_type='dict', + valuespec=_valuespec_special_agents_azure_discovery, + ) +) +rulespec_registry.register( + HostRulespec( + name="special_agents:azure_defender_alerts", + group=RulespecGroupCheckParametersDiscovery, + match_type='dict', + valuespec=_valuespec_special_agents_azure_discovery, + ) +) + +rulespec_registry.register( + CheckParameterRulespecWithItem( + check_group_name="azure_keyvault_metric", + group=RulespecGroupCheckParametersApplications, + match_type="dict", + parameter_valuespec=_valuespec_special_agents_azure_keyvault_metric_check, + ) +) +rulespec_registry.register( + CheckParameterRulespecWithItem( + check_group_name="azure_firewall_metric", + group=RulespecGroupCheckParametersApplications, + match_type="dict", + parameter_valuespec=_valuespec_special_agents_azure_keyvault_metric_check, + ) +) +