Skip to content

Commit 66524f9

Browse files
committed
Added new check for custom app proxy certificates
1 parent 8089314 commit 66524f9

File tree

6 files changed

+652
-104
lines changed

6 files changed

+652
-104
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8; py-indent-offset: 4; max-line-length: 100 -*-
3+
4+
# Copyright (C) 2025 Christopher Pommer <cp.software@outlook.de>
5+
6+
# This program is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU General Public License
8+
# as published by the Free Software Foundation; either version 2
9+
# of the License, or (at your option) any later version.
10+
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
16+
# You should have received a copy of the GNU General Public License
17+
# along with this program; if not, write to the Free Software
18+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
20+
####################################################################################################
21+
# CHECKMK CHECK PLUG-IN: Microsoft Entra App Proxy Certificates
22+
#
23+
# This plug-in generates the Checkmk services and determines their status.
24+
# This file is part of the Microsoft Entra special agent (ms_entra).
25+
####################################################################################################
26+
27+
# Example data from special agent (formatted):
28+
# <<<ms_entra_app_proxy_certs:sep(0)>>>
29+
# [
30+
# {
31+
# "app_name": "App 1",
32+
# "app_appid": "00000000-0000-0000-0000-000000000000",
33+
# "app_id": "00000000-0000-0000-0000-000000000000",
34+
# "app_notes": "App 1 description",
35+
# "internal_url": "https://app1.internal.tld/",
36+
# "external_url": "https://app1.external.tld/",
37+
# "cert_thumbprint": "0000000000000000000000000000000000000000",
38+
# "cert_subject_name": "app1.external.tld",
39+
# "cert_expiration": "1970-01-01T01:00:00Z"
40+
# },
41+
# ...
42+
# ]
43+
44+
45+
import json
46+
from collections.abc import Mapping
47+
from dataclasses import dataclass
48+
from datetime import datetime
49+
from typing import Any
50+
51+
from cmk.agent_based.v2 import (
52+
AgentSection,
53+
check_levels,
54+
CheckPlugin,
55+
CheckResult,
56+
DiscoveryResult,
57+
Metric,
58+
render,
59+
Result,
60+
Service,
61+
State,
62+
StringTable,
63+
)
64+
65+
66+
@dataclass(frozen=True)
67+
class AppProxyInfo:
68+
app_appid: str
69+
app_id: str
70+
app_name: str
71+
app_notes: str | None
72+
cert_expiration: str
73+
cert_subject_name: str
74+
cert_thumbprint: str
75+
external_url: str
76+
internal_url: str
77+
78+
79+
Section = Mapping[str, AppProxyInfo]
80+
81+
82+
def parse_ms_entra_app_proxy_certs(string_table: StringTable) -> Section:
83+
parsed = {}
84+
app_names = set()
85+
for item in json.loads("".join(string_table[0])):
86+
app_name = item["app_name"]
87+
# generate unique names, because entra app name is not unique
88+
if app_name in app_names:
89+
app_name_unique = f"{app_name} {item['app_id'][-4:]}"
90+
else:
91+
app_name_unique = app_name
92+
app_names.add(app_name)
93+
94+
parsed[app_name_unique] = AppProxyInfo(**item)
95+
96+
return parsed
97+
98+
99+
def discover_ms_entra_app_proxy_certs(section: Section) -> DiscoveryResult:
100+
for group in section:
101+
yield Service(item=group)
102+
103+
104+
def check_ms_entra_app_proxy_certs(
105+
item: str, params: Mapping[str, Any], section: Section
106+
) -> CheckResult:
107+
app = section.get(item)
108+
if not app:
109+
return
110+
111+
params_levels_cert_expiration = params["cert_expiration"]
112+
113+
# Cert expiration time and timespan calculation
114+
cert_expiration_timestamp = datetime.fromisoformat(app.cert_expiration).timestamp()
115+
cert_expiration_timestamp_render = render.datetime(cert_expiration_timestamp)
116+
cert_expiration_timespan = cert_expiration_timestamp - datetime.now().timestamp()
117+
118+
# For state calculation, check_levels is used.
119+
# It will take the expiration timespan of the app proxy certificate.
120+
if cert_expiration_timespan > 0:
121+
yield from check_levels(
122+
cert_expiration_timespan,
123+
levels_lower=(params_levels_cert_expiration),
124+
metric_name="ms_entra_app_proxy_cert_remaining_validity",
125+
label="Remaining",
126+
render_func=render.timespan,
127+
)
128+
else:
129+
yield from check_levels(
130+
cert_expiration_timespan,
131+
levels_lower=(params_levels_cert_expiration),
132+
label="Expired",
133+
render_func=lambda x: "%s ago" % render.timespan(abs(x)),
134+
)
135+
136+
# To prevent a negative value for the metric.
137+
yield Metric(
138+
name="ms_entra_app_proxy_cert_remaining_validity",
139+
value=0.0,
140+
levels=params_levels_cert_expiration[1],
141+
)
142+
143+
app_details_list = "\n".join(
144+
[
145+
f"App name: {app.app_name}",
146+
f"App ID: {app.app_appid}",
147+
f"Object ID: {app.app_id}",
148+
"",
149+
f"Description: {app.app_notes or '(Not available)'}",
150+
"",
151+
f"Internal URL: {app.internal_url}",
152+
f"External URL: {app.external_url}",
153+
"",
154+
"Certificate details",
155+
f" - Subject name: {app.cert_subject_name}",
156+
f" - Thumbprint: {app.cert_thumbprint}",
157+
f" - Expiration time: {cert_expiration_timestamp_render}",
158+
]
159+
)
160+
161+
# To display custom summary and details we need to yield Result.
162+
# The real state is calculated using the worst state of Result and check_levels.
163+
yield Result(
164+
state=State.OK,
165+
summary=f"Expiration time: {cert_expiration_timestamp_render}",
166+
details=f"\n{app_details_list}",
167+
)
168+
169+
170+
agent_section_ms_entra_app_proxy_certs = AgentSection(
171+
name="ms_entra_app_proxy_certs",
172+
parse_function=parse_ms_entra_app_proxy_certs,
173+
)
174+
175+
176+
check_plugin_ms_entra_app_proxy_certs = CheckPlugin(
177+
name="ms_entra_app_proxy_certs",
178+
service_name="Entra app proxy certificate %s",
179+
discovery_function=discover_ms_entra_app_proxy_certs,
180+
check_function=check_ms_entra_app_proxy_certs,
181+
check_ruleset_name="ms_entra_app_proxy_certs",
182+
check_default_parameters={"cert_expiration": ("fixed", (1209600.0, 432000.0))},
183+
)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
title: Microsoft Entra: App Proxy Certificates
2+
agents: special
3+
catalog: cloud/misc
4+
license: GPLv2
5+
distribution: Christopher Pommer
6+
description:
7+
This check monitors the expiration status of custom certificates uploaded
8+
to Microsoft Entra application proxy service principals. These certificates are used
9+
for TLS connections to the externally published site.
10+
11+
The check processes data collected by the {Microsoft Entra} special agent,
12+
which queries the Microsoft Graph API to retrieve all app proxy service principals
13+
in the Entra tenant and the associated app registration certificate information.
14+
15+
Default mapping:
16+
17+
{OK}: Certificate expires in more than 14 days
18+
19+
{WARN}: Certificate expires within 14 days
20+
21+
{CRIT}: Certificate expires within 5 days
22+
23+
For expired certificates, the service continues to report critical state and
24+
displays the expiration timespan as "X ago" format.
25+
26+
The service details include complete application information such as app name,
27+
app ID, object ID, description, external URL, internal URL and certificate
28+
details including thumbprint and expiration time.
29+
30+
You must configure the {Microsoft Entra} special agent to collect SAML
31+
certificate data from your Entra tenant.
32+
33+
item:
34+
The name of the app registration as configured in Entra. If multiple
35+
applications share the same name, the last 4 characters of the object ID
36+
are appended for unique identification (e.g., "SAML App 1 a1b2").
37+
38+
discovery:
39+
One service is created automatically for each app registration configured as
40+
app proxy with custom certificate. Services are named
41+
"Entra app proxy certificate {app_name}" where {app_name} is the configured
42+
application name, potentially with ID suffix for uniqueness.

entra/graphing/ms_entra.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,23 @@
3535

3636
UNIT_TIME = Unit(TimeNotation())
3737

38+
# --------------------------------------------------------------------------------------------------
39+
# Microsoft Entra App Proxy Certificate
40+
# --------------------------------------------------------------------------------------------------
41+
42+
metric_ms_entra_app_proxy_cert_remaining_validity = Metric(
43+
name="ms_entra_app_proxy_cert_remaining_validity",
44+
title=Title("Remaining app proxy cert validity time"),
45+
unit=UNIT_TIME,
46+
color=Color.YELLOW,
47+
)
48+
49+
perfometer_ms_entra_app_proxy_cert_remaining_validity = Perfometer(
50+
name="ms_entra_app_proxy_cert_remaining_validity",
51+
focus_range=FocusRange(Closed(0), Open(15552000)),
52+
segments=["ms_entra_app_proxy_cert_remaining_validity"],
53+
)
54+
3855
# --------------------------------------------------------------------------------------------------
3956
# Microsoft Entra App Credentials
4057
# --------------------------------------------------------------------------------------------------

0 commit comments

Comments
 (0)