Skip to content

Commit 97ecc70

Browse files
committed
Improvements and comments
1 parent 11ec055 commit 97ecc70

File tree

4 files changed

+89
-59
lines changed

4 files changed

+89
-59
lines changed

entra/agent_based/ms_entra_app_creds.py

Lines changed: 60 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020

2121
####################################################################################################
22-
# Checkmk check plugin for monitoring the expiration of secrests and certificates from
22+
# Checkmk check plugin for monitoring the expiration of secrets and certificates from
2323
# Microsoft Entra App Registrations.
2424
# The plugin works with data from the Microsoft Entra Special Agent (ms_entra).
2525

@@ -36,6 +36,7 @@
3636
# {
3737
# "cred_id": "00000000-0000-0000-0000-000000000000",
3838
# "cred_name": "Cert Name 1",
39+
# "cred_identifier": 239527ECF41F3FCFADBF68F93689FD4EBE19A3B0,
3940
# "cred_expiration": "1970-01-01T01:00:00Z"
4041
# }
4142
# ]
@@ -49,12 +50,14 @@
4950
# "app_creds": [
5051
# {
5152
# "cred_id": "00000000-0000-0000-0000-000000000000",
52-
# "cred_name": "Secret Name 1",
53+
# "cred_name": null",
54+
# "cred_identifier": "Q1dBUF9BdXRoU2VjcmV0",
5355
# "cred_expiration": "1970-01-01T01:00:00Z"
5456
# },
5557
# {
5658
# "cred_id": "00000000-0000-0000-0000-000000000000",
5759
# "cred_name": "Secret Name 2",
60+
# "cred_identifier": null,
5861
# "cred_expiration": "1970-01-01T01:00:00Z"
5962
# }
6063
# ]
@@ -126,35 +129,36 @@ def check_ms_entra_app_creds(item: str, params: Mapping[str, Any], section: Sect
126129

127130
compiled_patterns = [re.compile(pattern) for pattern in params_cred_exclude_list]
128131

132+
# The type of the credentials is capitalized for the check result details.
129133
cred_type = app.cred_type.capitalize()
130134

131-
result_details_list = []
135+
result_details_cred_list = []
132136
cred_earliest_expiration = None
133137
for cred in app.app_creds:
134-
cred_name = cred.get("cred_name")
135-
cred_identifier = cred.get("cred_identifier")
136-
137-
if cred_name:
138-
cred_description = cred_name
139-
elif cred_identifier:
140-
cred_description = base64.b64decode(cred_identifier).decode("utf-8", errors="ignore")
141-
else:
142-
cred_description = ""
143-
144-
cred_expiration_datetime = datetime.fromisoformat(cred["cred_expiration"])
145-
cred_expiration_timestamp = cred_expiration_datetime.timestamp()
146-
cred_expiration_timestamp_render = render.datetime(cred_expiration_timestamp)
138+
cred_description = cred["cred_name"] or ""
139+
cred_identifier = cred["cred_identifier"]
140+
141+
# It is possible that the credential displayName (cred_name) is not set, but the
142+
# customKeyIdentifier (cred_identifier) is. For secrets, both values are used as
143+
# the description. For certificates, only the displayName is used as the description.
144+
# The customKeyIdentifier for certificates is usually the certificate thumbprint, but
145+
# not always a valid one. The identifier is base64 encoded and will be decoded for
146+
# secrets if possible, because sometimes it is not a valid base64 string. It also
147+
# happens that both values are set or not. If both are set, the dsiplayName is used.
148+
if not cred_description and cred_identifier and cred_type == "Secret":
149+
try:
150+
cred_description = base64.b64decode(cred_identifier).decode("utf-8")
151+
except (base64.binascii.Error, UnicodeDecodeError):
152+
pass
153+
154+
cred_expiration_timestamp = datetime.fromisoformat(cred["cred_expiration"]).timestamp()
147155

148156
cred_id = cred["cred_id"]
149-
cred_details = (
150-
f"{cred_type} ({cred_description})" if cred_description else f"{cred_type}"
151-
) + (f"\n - ID: {cred_id}\n - Expiration time: {cred_expiration_timestamp_render}")
152-
result_details_list.append(cred_details)
153-
154-
if any(pattern.match(cred_description) for pattern in compiled_patterns):
155-
continue
156157

157-
if (
158+
# This is used to find the credential with the earliest expiration time.
159+
# The expiration time of this credential will be used for the check result.
160+
# Only credentials that are not excluded by a Checkmk rule are considered.
161+
if not any(pattern.match(cred_description) for pattern in compiled_patterns) and (
158162
cred_earliest_expiration is None
159163
or cred_expiration_timestamp < cred_earliest_expiration["cred_expiration_timestamp"]
160164
):
@@ -164,32 +168,47 @@ def check_ms_entra_app_creds(item: str, params: Mapping[str, Any], section: Sect
164168
"cred_description": cred_description,
165169
}
166170

167-
result_details = (
168-
f"App name: {app.app_name}\nApp ID: {app.app_appid}\nObject ID: {app.app_id}"
169-
"\n\nDescription: "
170-
+ (f"{app.app_notes}" if app.app_notes else "---")
171-
+ f"\n\n{'\n\n'.join(result_details_list)}"
172-
)
173-
171+
# Build a list of credential details to be displayed in the check result details.
172+
cred_details_list = [
173+
f"{cred_type} ID: {cred_id}",
174+
f" - Description: {cred_description or '(Not available)'}",
175+
f" - Expiration time: {render.datetime(cred_expiration_timestamp)}",
176+
]
177+
result_details_cred_list.append("\n".join(cred_details_list))
178+
179+
# This content will be used to display the application details in the check result details with
180+
# all available credentials.
181+
app_details_list = [
182+
f"App name: {app.app_name}",
183+
f"App ID: {app.app_appid}",
184+
f"Object ID: {app.app_id}",
185+
"",
186+
f"Description: {app.app_notes or '(Not available)'}",
187+
]
188+
result_details = "\n".join(app_details_list) + "\n\n" + "\n\n".join(result_details_cred_list)
189+
190+
# It will only be None, if all credentials are excluded by a Checkmk rule.
174191
if cred_earliest_expiration is not None:
175-
cred_earliest_expiration_name = cred_earliest_expiration["cred_description"]
192+
cred_earliest_expiration_description = cred_earliest_expiration["cred_description"]
176193
cred_earliest_expiration_timestamp = int(
177194
cred_earliest_expiration["cred_expiration_timestamp"]
178195
)
179-
cred_earliest_expiration_timestamp_render = render.datetime(
180-
cred_earliest_expiration_timestamp
181-
)
196+
197+
# Calculate the timespan until the earliest credential expires or has expired.
182198
cred_expiration_timespan = cred_earliest_expiration_timestamp - datetime.now().timestamp()
183199

184-
result_summary = f"Expiration time: {cred_earliest_expiration_timestamp_render}"
200+
# This content will be used as the check result summary.
201+
result_summary = f"Expiration time: {render.datetime(cred_earliest_expiration_timestamp)}"
185202
result_summary += (
186-
f", Description: {cred_earliest_expiration_name}"
187-
if cred_earliest_expiration_name
203+
f", Description: {cred_earliest_expiration_description}"
204+
if cred_earliest_expiration_description
188205
else ""
189206
)
190207

191208
params_cred_expiration_levels = params.get("cred_expiration")
192209

210+
# For state calculation, check_levels is used.
211+
# It will take the expiration time of the credential with the earliest expiration time.
193212
if cred_expiration_timespan > 0:
194213
yield from check_levels(
195214
cred_expiration_timespan,
@@ -208,6 +227,9 @@ def check_ms_entra_app_creds(item: str, params: Mapping[str, Any], section: Sect
208227
else:
209228
result_summary = "All application credentials are excluded"
210229

230+
# To display custom summary and details we need to yield Result.
231+
# The real state is calculated by check_levels.
232+
# Also if all credentials are excluded, we need to yield Result with state OK.
211233
yield Result(
212234
state=State.OK,
213235
summary=result_summary,

entra/agent_based/ms_entra_saml_certs.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,27 +111,30 @@ def check_ms_entra_saml_certs(
111111
params_levels_cert_expiration = params.get("cert_expiration")
112112

113113
# Cert expiration time and timespan calculation
114-
cert_expiration_datetime = datetime.fromisoformat(app.cert_expiration)
115-
cert_expiration_timestamp = cert_expiration_datetime.timestamp()
114+
cert_expiration_timestamp = datetime.fromisoformat(app.cert_expiration).timestamp()
116115
cert_expiration_timestamp_render = render.datetime(int(cert_expiration_timestamp))
117116
cert_expiration_timespan = cert_expiration_timestamp - datetime.now().timestamp()
118117

119-
# Build result details
120-
details = [
118+
# This content will be used to display the application details in the check result details with
119+
# the available SAML certificate.
120+
app_details_list = [
121121
f"App name: {app.app_name}",
122122
f"App ID: {app.app_appid}",
123123
f"Object ID: {app.app_id}",
124124
"",
125-
f"Description: {app.app_notes or '---'}",
125+
f"Description: {app.app_notes or '(Not available)'}",
126126
"",
127127
"Certificate",
128128
f" - Thumbprint: {app.cert_thumbprint}",
129129
f" - Expiration time: {cert_expiration_timestamp_render}",
130130
]
131-
result_details = "\n".join(details)
131+
result_details = "\n".join(app_details_list)
132132

133+
# This content will be used as the check result summary.
133134
result_summary = f"Expiration time: {cert_expiration_timestamp_render}"
134135

136+
# For state calculation, check_levels is used.
137+
# It will take the expiration time of the SAML certificate.
135138
if cert_expiration_timespan > 0:
136139
yield from check_levels(
137140
cert_expiration_timespan,
@@ -147,6 +150,8 @@ def check_ms_entra_saml_certs(
147150
render_func=lambda x: "%s ago" % render.timespan(abs(x)),
148151
)
149152

153+
# To display custom summary and details we need to yield Result.
154+
# The real state is calculated by check_levels.
150155
yield Result(
151156
state=State.OK,
152157
summary=result_summary,

entra/agent_based/ms_entra_sync.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def discover_ms_entra_sync(section: Section) -> DiscoveryResult:
6969

7070

7171
def check_ms_entra_sync(params: Mapping[str, Any], section: Section) -> CheckResult:
72+
# If sync is not enabled, the check will return UNKNOWN.
7273
if section.sync_enabled is not True:
7374
yield Result(
7475
state=State.UNKNOWN,
@@ -78,21 +79,24 @@ def check_ms_entra_sync(params: Mapping[str, Any], section: Section) -> CheckRes
7879

7980
params_levels_sync_period = params.get("sync_period")
8081

81-
# Last sync time and timespan calculation
82-
sync_last_datetime = datetime.fromisoformat(section.sync_last)
83-
sync_last_timestamp = sync_last_datetime.timestamp()
84-
sync_last_timestamp_render = render.datetime(int(sync_last_timestamp))
82+
# Calculation of the timespan since the last sync.
83+
sync_last_timestamp = datetime.fromisoformat(section.sync_last).timestamp()
8584
sync_last_timespan = datetime.now().timestamp() - sync_last_timestamp
8685

87-
result_summary = f"Sync time: {sync_last_timestamp_render}"
86+
# This content will be used as the check result summary.
87+
result_summary = f"Sync time: {render.datetime(int(sync_last_timestamp))}"
8888

89+
# For state calculation, check_levels is used.
90+
# It will take the last sync time of the Entra connect/cloud sync.
8991
yield from check_levels(
9092
sync_last_timespan,
9193
levels_upper=(params_levels_sync_period),
9294
label="Last sync",
9395
render_func=lambda x: f"{render.timespan(abs(x))} ago",
9496
)
9597

98+
# To display custom summary we need to yield Result.
99+
# The real state is calculated by check_levels.
96100
yield Result(
97101
state=State.OK,
98102
summary=result_summary,

entra/libexec/agent_ms_entra

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,11 @@ def get_entra_sync(
171171
2,
172172
)
173173

174-
entra_sync_json = entra_sync_response.json()
174+
entra_sync_dict = entra_sync_response.json()
175175

176176
entra_sync = {
177-
"sync_enabled": entra_sync_json["onPremisesSyncEnabled"],
178-
"sync_last": entra_sync_json["onPremisesLastSyncDateTime"],
177+
"sync_enabled": entra_sync_dict["onPremisesSyncEnabled"],
178+
"sync_last": entra_sync_dict["onPremisesLastSyncDateTime"],
179179
}
180180

181181
return entra_sync
@@ -230,12 +230,11 @@ def get_entra_app_registration_creds(
230230
"Failed to get Entra app registrations. Please check your application permissions.",
231231
3,
232232
)
233-
234-
entra_app_registration_json = entra_app_registration_response.json()
235-
entra_app_registrations.extend(entra_app_registration_json.get("value", []))
233+
entra_app_registration_dict = entra_app_registration_response.json()
234+
entra_app_registrations.extend(entra_app_registration_dict.get("value", []))
236235

237236
# get next page if available (pagination)
238-
entra_app_registrations_url = entra_app_registration_json.get("@odata.nextLink")
237+
entra_app_registrations_url = entra_app_registration_dict.get("@odata.nextLink")
239238

240239
app_names = set()
241240
app_list = []
@@ -329,11 +328,11 @@ def get_entra_saml_certs(
329328
4,
330329
)
331330

332-
entra_saml_certs_json = entra_saml_certs_response.json()
333-
entra_saml_certs.extend(entra_saml_certs_json.get("value", []))
331+
entra_saml_certs_dict = entra_saml_certs_response.json()
332+
entra_saml_certs.extend(entra_saml_certs_dict.get("value", []))
334333

335334
# get next page if available (pagination)
336-
entra_saml_certs_url = entra_saml_certs_json.get("@odata.nextLink")
335+
entra_saml_certs_url = entra_saml_certs_dict.get("@odata.nextLink")
337336

338337
app_list = []
339338
for app in entra_saml_certs:

0 commit comments

Comments
 (0)