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
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# ]
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 } \n App ID: { app .app_appid } \n Object ID: { app .app_id } "
169- "\n \n Description: "
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 ,
0 commit comments