Compare commits

..

6 Commits

9 changed files with 235 additions and 106 deletions

Binary file not shown.

View File

@ -3,18 +3,10 @@
import json import json
from datetime import datetime, timezone from datetime import datetime, timezone
from cmk.base.plugins.agent_based.agent_based_api.v1 import register, Result, Service, State from cmk.base.plugins.agent_based.agent_based_api.v1 import register, Result, Service, State, Metric
def check_state_below(alert_percentages, measured_percent): def check_state_below(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
def check_state_above(alert_percentages, measured_percent):
if alert_percentages: if alert_percentages:
if alert_percentages[1] >= measured_percent: if alert_percentages[1] >= measured_percent:
return State.CRIT return State.CRIT
@ -22,17 +14,27 @@ def check_state_above(alert_percentages, measured_percent):
return State.WARN return State.WARN
return State.OK 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 # 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 # 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. # 'location' and 'resource_group' fields in each object to the name.
def parse(string_table): def parse(string_table):
lookup = {} lookup = {}
for json_str in string_table: for json_data in string_table:
obj = json.loads(json_str) obj = json.loads(json_data[0])
name = obj["name"] name = obj["name"]
group = obj["resource_group"] group = obj["resource_group"]
lookup[f"{name}#{resource_group}"] = obj lookup[f"{name}#{group}"] = obj
return lookup return lookup
@ -54,7 +56,7 @@ def check_keyvault(item, params, section):
availability = metrics.get("Availability") availability = metrics.get("Availability")
capacity = metrics.get("SaturationShoebox") capacity = metrics.get("SaturationShoebox")
latency = metrics.get("ServiceApiLatency") latency = metrics.get("ServiceApiLatency", 0)
hits = metrics.get("ServiceApiHit") hits = metrics.get("ServiceApiHit")
results = metrics.get("ServiceApiResult") results = metrics.get("ServiceApiResult")
@ -62,45 +64,78 @@ def check_keyvault(item, params, section):
alert_capacity_percent = params.get("capacity") alert_capacity_percent = params.get("capacity")
alert_latency_milliseconds = params.get("latency") alert_latency_milliseconds = params.get("latency")
if availability: if availability is not None:
check_state_below(alert_availability_percent, availability) yield Result(
state=check_state_below(alert_availability_percent, availability),
summary=f"Availability: {availability}%",
)
yield Metric( yield Metric(
name="availability", name="availability",
value=availability, value=availability,
levels=alert_availability_percent, boundaries=(0, 100),
boundaries=(0, 100) )
else:
yield Result(
state=State.UNKNOWN,
summary="Availability: N/A",
) )
if capacity: if capacity is not None:
check_state_above(alert_capacity_percent, capacity) yield Result(
state=check_state_above(alert_capacity_percent, capacity),
summary=f"Capacity: {capacity}%"
)
yield Metric( yield Metric(
name="capacity", name="capacity",
value=capacity, value=capacity,
levels=alert_capacity_percent, boundaries=(0, 100),
boundaries=(0, 100) )
else:
yield Result(
state=State.UNKNOWN,
summary="Capacity: N/A",
) )
if latency:
check_state_above(alert_latency_milliseconds, latency) if latency is not None:
yield Result(
state=check_state_above(alert_latency_milliseconds, latency),
summary=f"Latency: {latency}ms",
)
yield Metric( yield Metric(
name="latency", name="latency",
value=latency, value=latency,
levels=alert_latency_milliseconds, boundaries=(0, None),
boundaries=(0, None) )
else:
yield Result(
state=State.UNKNOWN,
summary="Latency: N/A",
) )
if hits:
if hits is not None:
yield Metric( yield Metric(
name="hits", name="hits",
value=hits, value=hits,
boundaries=(0, None) boundaries=(0, None),
)
else:
yield Result(
state=State.UNKNOWN,
summary="Hits: N/A",
) )
if results: if results is not None:
yield Metric( yield Metric(
name="results", name="results",
value=results, value=results,
boundaries=(0, None) boundaries=(0, None),
)
else:
yield Result(
state=State.UNKNOWN,
summary="Results: N/A",
) )
@ -111,7 +146,7 @@ def check_firewall(item, params, section):
if firewall is None: if firewall is None:
return return
metrics = vault["metrics"] metrics = firewall["metrics"]
availability = metrics.get("FirewallHealth") availability = metrics.get("FirewallHealth")
throughput = metrics.get("Throughput") throughput = metrics.get("Throughput")
@ -120,34 +155,78 @@ def check_firewall(item, params, section):
alert_availability_percent = params.get("availability") alert_availability_percent = params.get("availability")
alert_latency_milliseconds = params.get("latency") alert_latency_milliseconds = params.get("latency")
if availability: if availability is not None:
check_state_below(alert_availability_percent, availability) yield Result(
state=check_state_below(alert_availability_percent, availability),
summary=f"Availability: {availability}%",
)
yield Metric( yield Metric(
name="availability", name="availability",
value=availability, value=availability,
levels=alert_availability_percent,
boundaries=(0, 100) boundaries=(0, 100)
) )
else:
yield Result(
state=State.UNKNOWN,
summary="Availability: N/A",
)
if latency: if latency is not None:
check_state_above(alert_latency_milliseconds, latency) yield Result(
state=check_state_above(alert_latency_milliseconds, latency),
summary=f"Latency: {latency}ms",
)
yield Metric( yield Metric(
name="latency", name="latency",
value=latency, value=latency,
levels=alert_latency_milliseconds,
boundaries=(0, None) boundaries=(0, None)
) )
else:
yield Result(
state=State.UNKNOWN,
summary="Latency: N/A",
)
if throughput: if throughput is not None:
yield Metric( yield Metric(
name="throughput", name="throughput",
value=thoughput, value=throughput,
boundaries=(0, None) boundaries=(0, None)
) )
else:
yield Result(
state=State.UNKNOWN,
summary="Throughput: N/A",
)
def check_defender(item, params, section): def check_defender(item, params, section):
yield Result(state=state, summary="Expires in %d days" % remaining_days) alert = section.get(item)
if alert is None:
return
details = alert["alert"]
status = details["status"]
if status != "Active" and status != "InProgress":
return
severity = details["severity"]
url = details["url"]
info = details["info"]
if severity == "High":
state = State.CRIT
elif severity == "Medium":
state = State.WARN
elif severity == "Low":
state = State.OK
else:
state = State.UNKNOWN
yield Result(
state=state,
summary=f"{status}: {info}: {url}"
)
register.agent_section( register.agent_section(

View File

@ -28,6 +28,8 @@ RESOURCE_GROUP_RE = re.compile('/resourceGroups/(.+?)/')
# https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/request-limits-and-throttling # https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/request-limits-and-throttling
def get_url(req, default): def get_url(req, default):
#import http.client
#http.client.HTTPConnection.debuglevel = 1
try: try:
res = request.urlopen(req) res = request.urlopen(req)
return res.read() return res.read()
@ -38,20 +40,33 @@ def get_url(req, default):
raise e raise e
def get_token(tenant, username, password): def set_proxy(req, proxy):
if proxy is None or proxy == '':
return
match = re.match('(https?)://(.+?)/?$', proxy, re.I)
req.set_proxy(match[2], match[1].lower())
# The explicit Host header is required for this to also work with a proxy.
# If we don't include it, Python sends the proxy's Host to Microsoft
# instead! So we have to set the Host to the Microsoft domain manually.
match = re.match('https://(.+?)/', req.full_url, re.I)
req.add_header('Host', match[1] + ":443")
def get_token(tenant, username, password, proxy):
data = parse.urlencode({ data = parse.urlencode({
'username': username, 'client_id': username,
'password': password, 'client_secret': password,
'grant_type': 'password', 'grant_type': 'client_credentials',
'claims': '{"access_token": {"xms_cc": {"values": ["CP1"]}}}', 'claims': '{"access_token": {"xms_cc": {"values": ["CP1"]}}}',
'scope': 'https://management.core.windows.net//.default offline_access openid profile', 'scope': 'https://management.core.windows.net//.default offline_access openid profile',
'client_info': 1, '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', req = request.Request(f'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token',
data=str.encode(data)) data=str.encode(data))
set_proxy(req, proxy)
res = get_url(req, None) res = get_url(req, None)
if res is None: if res is None:
@ -62,31 +77,32 @@ def get_token(tenant, username, password):
return token return token
def get_json(token, path, version='2023-07-01'): def get_json(token, proxy, path, version='2023-07-01'):
url = f"https://management.azure.com{path}{'?' in path and '&' or '?'}api-version={version}" url = f"https://management.azure.com{path}{'?' in path and '&' or '?'}api-version={version}"
req = request.Request(url, headers={'Authorization': f'Bearer {token}'}) req = request.Request(url, headers={'Authorization': f'Bearer {token}'})
set_proxy(req, proxy)
res = get_url(req, "[]") res = get_url(req, "[]")
data = json.loads(res) data = json.loads(res)
return data['value'] return data['value']
def list_subscriptions(token): def list_subscriptions(token, proxy):
return get_json(token, '/subscriptions') return get_json(token, proxy, '/subscriptions')
def list_vaults(token, subscription): def list_vaults(token, proxy, subscription):
return get_json(token, f'/subscriptions/{subscription}/resources?$filter=resourceType%20eq%20%27Microsoft.KeyVault%2Fvaults%27') return get_json(token, proxy, f'/subscriptions/{subscription}/resources?$filter=resourceType%20eq%20%27Microsoft.KeyVault%2Fvaults%27')
def list_firewalls(token, subscription): def list_firewalls(token, proxy, subscription):
return get_json(token, f'/subscriptions/{subscription}/resources?$filter=resourceType%20eq%20%27Microsoft.Network%2FazureFirewalls%27') return get_json(token, proxy, f'/subscriptions/{subscription}/resources?$filter=resourceType%20eq%20%27Microsoft.Network%2FazureFirewalls%27')
def list_defender_alerts(token, subscription): def list_defender_alerts(token, proxy, subscription):
return get_json(token, f'/subscriptions/{subscription}/providers/Microsoft.Security/alerts', '2022-01-01') return get_json(token, proxy, f'/subscriptions/{subscription}/providers/Microsoft.Security/alerts', '2022-01-01')
def get_recent_metrics(token, path, metrics): def get_recent_metrics(token, proxy, path, metrics):
end = datetime.now() end = datetime.now()
start = end - timedelta(minutes=2) start = end - timedelta(minutes=2)
@ -94,7 +110,7 @@ def get_recent_metrics(token, path, metrics):
end_str = end.isoformat().split('.')[0] + 'Z' end_str = end.isoformat().split('.')[0] + 'Z'
metrics_str = ','.join(metrics) metrics_str = ','.join(metrics)
return get_json(token, f'{path}/providers/microsoft.insights/metrics?metricnames={metrics_str}&timespan={start_str}/{end_str}', '2023-10-01') return get_json(token, proxy, f'{path}/providers/microsoft.insights/metrics?metricnames={metrics_str}&timespan={start_str}/{end_str}', '2023-10-01')
def metrics_to_lookup(metrics): def metrics_to_lookup(metrics):
@ -112,28 +128,26 @@ def metrics_to_lookup(metrics):
def get_args(argv): def get_args(argv):
if len(argv) != 5 or not argv[1] in ['keyvault', 'firewall', 'defender']: if (len(argv) != 5 and len(argv) != 6) or argv[1] not in ['keyvault', 'firewall', 'defender']:
print(f"{sys.argv[0]} <command> <tenand ID> <username> <password>", file=sys.stderr) print(f"{sys.argv[0]} <command> <tenant ID> <username> <password> <proxy>", file=sys.stderr)
print(f"Valid commands are: 'keyvault', 'firewall', 'defender'", file=sys.stderr) print(f"Valid commands are: 'keyvault', 'firewall', 'defender'", file=sys.stderr)
print(f"Proxy is an optional argument", file=sys.stderr)
exit(1) exit(1)
return argv[1], argv[2], argv[3], argv[4] return argv[1], argv[2], argv[3], argv[4], (argv[5] if len(argv) == 6 else None)
def print_json(obj): def print_json(obj):
print(json.dumps(obj)) print(json.dumps(obj))
command, tenant, username, password = get_args(sys.argv) command, tenant, username, password, proxy = get_args(sys.argv)
token = get_token(tenant, username, password, proxy)
print(f"<<<azure_{command}:sep(0)>>>") for subscription in list_subscriptions(token, proxy):
token = get_token(tenant, username, password)
for subscription in list_subscriptions(token):
subscription_id = subscription['subscriptionId'] subscription_id = subscription['subscriptionId']
if command == 'defender': if command == 'defender':
for alert in list_defender_alerts(token, subscription_id): for alert in list_defender_alerts(token, proxy, subscription_id):
properties = alert['properties'] properties = alert['properties']
status = properties['status'] status = properties['status']
@ -155,8 +169,8 @@ for subscription in list_subscriptions(token):
}) })
elif command == 'firewall': elif command == 'firewall':
for firewall in list_firewalls(token, subscription_id): for firewall in list_firewalls(token, proxy, subscription_id):
metrics = get_recent_metrics(token, firewall['id'], FIREWALL_METRICS) metrics = get_recent_metrics(token, proxy, firewall['id'], FIREWALL_METRICS)
print_json({ print_json({
'type': command, 'type': command,
'name': firewall['name'], 'name': firewall['name'],
@ -166,8 +180,8 @@ for subscription in list_subscriptions(token):
}) })
elif command == 'keyvault': elif command == 'keyvault':
for vault in list_vaults(token, subscription_id): for vault in list_vaults(token, proxy, subscription_id):
metrics = get_recent_metrics(token, vault['id'], VAULT_METRICS) metrics = get_recent_metrics(token, proxy, vault['id'], VAULT_METRICS)
print_json({ print_json({
'type': command, 'type': command,
'name': vault['name'], 'name': vault['name'],

View File

@ -0,0 +1,6 @@
#!/bin/bash
echo '<<<azure_defender:sep(0)>>>'
dir=$(dirname -- "${BASH_SOURCE[0]}")
"$dir"/agent_azure_common defender "$1" "$2" "$3" "$4"

View File

@ -0,0 +1,6 @@
#!/bin/bash
echo '<<<azure_firewall:sep(0)>>>'
dir=$(dirname -- "${BASH_SOURCE[0]}")
"$dir"/agent_azure_common firewall "$1" "$2" "$3" "$4"

View File

@ -0,0 +1,6 @@
#!/bin/bash
echo '<<<azure_keyvault:sep(0)>>>'
dir=$(dirname -- "${BASH_SOURCE[0]}")
"$dir"/agent_azure_common keyvault "$1" "$2" "$3" "$4"

View File

@ -1,22 +0,0 @@
#!/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(params, hostname, ipaddress):
tenant, client, secret = get_params(params)
return ["keyvault", tenant, client, secret]
def agent_azure_firewall(params, hostname, ipaddress):
tenant, client, secret = get_params(params)
return ["firewall", tenant, client, secret]
def agent_azure_defender(params, hostname, ipaddress):
tenant, client, secret = get_params(params)
return ["defender", tenant, client, secret]
special_agent_info["azure_keyvault"] = agent_azure_keyvault
special_agent_info["azure_firewall"] = agent_azure_firewall
special_agent_info["azure_defender"] = agent_azure_defender

View File

@ -0,0 +1,25 @@
#!/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', '<some password>'): password is in params directly
# ('store', '<password name>'): 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,
params.get("proxy") or "" # optional
]
special_agent_info["azure_keyvault"] = agent_azure_args
special_agent_info["azure_firewall"] = agent_azure_args
special_agent_info["azure_defender"] = agent_azure_args

View File

@ -21,38 +21,52 @@ from cmk.gui.valuespec import (
) )
def _valuespec_special_agents_azure_discovery(): def _discovery(title):
return Dictionary( return Dictionary(
title=_("Azure Discovery"), title=_(title),
required_keys=["tenant", "username", "password"],
elements=[ elements=[
( (
"tenant", "tenant",
TextInput( TextInput(
title=_("Tenant ID / Directory ID"), title=_("Tenant ID / Directory ID"),
allow_empty=False, allow_empty=False,
size=45,
), ),
), ),
( (
"client", "username",
TextInput( TextInput(
title=_("Client ID / Application ID"), title=_("Client ID / Application ID"),
allow_empty=False, allow_empty=False,
size=45,
), ),
), ),
( (
"secret", "password",
IndividualOrStoredPassword( IndividualOrStoredPassword(
# Password( # Password(
title=_("Client Secret"), title=_("Client Secret"),
allow_empty=False, allow_empty=False,
size=45,
), ),
), ),
(
"proxy",
TextInput(
title=_("Proxy"),
),
),
], ],
) )
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(): def _valuespec_special_agents_azure_keyvault_check():
return Dictionary( return Dictionary(
title=_("Azure Key Vault Metric Checks"), title=_("Azure Key Vault Metric Checks"),
@ -161,7 +175,7 @@ rulespec_registry.register(
name="special_agents:azure_keyvault", name="special_agents:azure_keyvault",
group=RulespecGroupCheckParametersDiscovery, group=RulespecGroupCheckParametersDiscovery,
match_type='dict', match_type='dict',
valuespec=_valuespec_special_agents_azure_discovery, valuespec=_valuespec_special_agents_azure_keyvault_discovery,
) )
) )
rulespec_registry.register( rulespec_registry.register(
@ -169,7 +183,7 @@ rulespec_registry.register(
name="special_agents:azure_firewall", name="special_agents:azure_firewall",
group=RulespecGroupCheckParametersDiscovery, group=RulespecGroupCheckParametersDiscovery,
match_type='dict', match_type='dict',
valuespec=_valuespec_special_agents_azure_discovery, valuespec=_valuespec_special_agents_azure_firewall_discovery,
) )
) )
rulespec_registry.register( rulespec_registry.register(
@ -177,7 +191,7 @@ rulespec_registry.register(
name="special_agents:azure_defender", name="special_agents:azure_defender",
group=RulespecGroupCheckParametersDiscovery, group=RulespecGroupCheckParametersDiscovery,
match_type='dict', match_type='dict',
valuespec=_valuespec_special_agents_azure_discovery, valuespec=_valuespec_special_agents_azure_defender_discovery,
) )
) )
@ -187,6 +201,8 @@ rulespec_registry.register(
group=RulespecGroupCheckParametersApplications, group=RulespecGroupCheckParametersApplications,
match_type="dict", match_type="dict",
parameter_valuespec=_valuespec_special_agents_azure_keyvault_check, parameter_valuespec=_valuespec_special_agents_azure_keyvault_check,
item_spec=lambda: TextInput(title=_("Key Vault")),
title=lambda: _("Azure Key Vault Metrics"),
) )
) )
rulespec_registry.register( rulespec_registry.register(
@ -195,5 +211,7 @@ rulespec_registry.register(
group=RulespecGroupCheckParametersApplications, group=RulespecGroupCheckParametersApplications,
match_type="dict", match_type="dict",
parameter_valuespec=_valuespec_special_agents_azure_firewall_check, parameter_valuespec=_valuespec_special_agents_azure_firewall_check,
item_spec=lambda: TextInput(title=_("Firewall")),
title=lambda: _("Azure Firewall Metrics"),
) )
) )