Skip to content

Commit 670e268

Browse files
committed
New check "Intune App Licenses"
1 parent b736630 commit 670e268

File tree

5 files changed

+453
-0
lines changed

5 files changed

+453
-0
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
#!/usr/bin/env python3
2+
# -*- encoding: utf-8; py-indent-offset: 4 -*-
3+
4+
# Copyright (C) 2024 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+
import json
22+
from collections.abc import Mapping, Sequence
23+
from dataclasses import dataclass
24+
from typing import Any
25+
26+
from cmk.agent_based.v2 import (
27+
AgentSection,
28+
CheckPlugin,
29+
CheckResult,
30+
DiscoveryResult,
31+
Metric,
32+
render,
33+
Result,
34+
Service,
35+
State,
36+
StringTable,
37+
)
38+
39+
40+
@dataclass(frozen=True)
41+
class IntuneApps:
42+
app_type: str
43+
app_name: str
44+
app_publisher: str
45+
app_license_total: int
46+
app_license_consumed: int
47+
app_assigned: bool
48+
49+
50+
Section = Mapping[str, Sequence[IntuneApps]]
51+
52+
# Example data from special agent:
53+
# <<<ms_intune_app_licenses:sep(0)>>>
54+
# [
55+
# {
56+
# "app_type": "iOS VPP"
57+
# "app_name": "Adobe Account Access",
58+
# "app_publisher": "ADOBE SYSTEMS SOFTWARE IRELAND LIMITED",
59+
# "app_license_total": 40,
60+
# "app_license_consumed": 7
61+
# "app_assigned": true
62+
# },
63+
# {
64+
# "app_type": "MS Store for business"
65+
# "app_name": "Microsoft Outlook",
66+
# "app_publisher": "Microsoft Corporation",
67+
# "app_license_total": 100,
68+
# "app_license_consumed": 92
69+
# "app_assigned": true
70+
# },
71+
# ...
72+
# ]
73+
74+
75+
def parse_ms_intune_app_licenses(string_table: StringTable) -> Section:
76+
parsed = {}
77+
for item in json.loads("".join(string_table[0])):
78+
parsed[item["app_name"] + " - " + item["app_type"]] = item
79+
return parsed
80+
81+
82+
def discover_ms_intune_app_licenses(section: Section) -> DiscoveryResult:
83+
for group in section:
84+
yield Service(item=group)
85+
86+
87+
def check_ms_intune_app_licenses(item: str, params: Mapping[str, Any], section: Section) -> CheckResult:
88+
license = section.get(item)
89+
if not license:
90+
return
91+
92+
app_name = license["app_name"]
93+
app_type = license["app_type"]
94+
app_publisher = license["app_publisher"]
95+
app_license_total = license["app_license_total"]
96+
app_license_consumed = license["app_license_consumed"]
97+
app_assigned = license["app_assigned"]
98+
99+
app_license_consumed_pct = round(app_license_consumed / app_license_total * 100, 2)
100+
lic_units_available = app_license_total - app_license_consumed
101+
102+
result_level = ""
103+
result_state = State.OK
104+
levels_consumed_abs = (None, None)
105+
levels_consumed_pct = (None, None)
106+
params_lic_total_min = params["lic_total_min"]
107+
if app_license_total >= params_lic_total_min:
108+
params_levels_available = params["lic_unit_available_lower"]
109+
if params_levels_available[1][0] == "fixed":
110+
warning_level, critical_level = params_levels_available[1][1]
111+
112+
if params_levels_available[0] == "lic_unit_available_lower_pct":
113+
levels_consumed_pct = (100 - warning_level, 100 - critical_level)
114+
available_percent = lic_units_available / app_license_total * 100
115+
116+
if available_percent < critical_level:
117+
result_state = State.CRIT
118+
elif available_percent < warning_level:
119+
result_state = State.WARN
120+
121+
result_level = (
122+
f" (warn/crit below {render.percent(warning_level)}/{render.percent(critical_level)} available)"
123+
)
124+
125+
else:
126+
levels_consumed_abs = (app_license_total - warning_level, app_license_total - critical_level)
127+
128+
if app_license_consumed > levels_consumed_abs[1]:
129+
result_state = State.CRIT
130+
elif app_license_consumed > levels_consumed_abs[0]:
131+
result_state = State.WARN
132+
133+
result_level = f" (warn/crit below {warning_level}/{critical_level} available)"
134+
135+
result_summary = (
136+
f"Consumed: {render.percent(app_license_consumed_pct)} - {app_license_consumed} of {app_license_total}"
137+
f", Available: {lic_units_available}"
138+
f"{result_level}"
139+
)
140+
141+
result_details = (
142+
f"App: {app_name} ({app_publisher})\n - Type: {app_type}\n - Assigned: {app_assigned}"
143+
f"\n - Total: {app_license_total}\n - Used: {app_license_consumed}"
144+
)
145+
146+
yield Result(
147+
state=result_state,
148+
summary=result_summary,
149+
details=result_details,
150+
)
151+
152+
yield Metric(name="ms_intune_app_licenses_total", value=app_license_total)
153+
yield Metric(name="ms_intune_app_licenses_consumed", value=app_license_consumed, levels=levels_consumed_abs)
154+
yield Metric(name="ms_intune_app_licenses_consumed_pct", value=app_license_consumed_pct, levels=levels_consumed_pct)
155+
yield Metric(name="ms_intune_app_licenses_available", value=lic_units_available)
156+
157+
158+
agent_section_ms_intune_app_licenses = AgentSection(
159+
name="ms_intune_app_licenses",
160+
parse_function=parse_ms_intune_app_licenses,
161+
)
162+
163+
164+
check_plugin_ms_intune_app_licenses = CheckPlugin(
165+
name="ms_intune_app_licenses",
166+
service_name="Intune app %s",
167+
discovery_function=discover_ms_intune_app_licenses,
168+
check_function=check_ms_intune_app_licenses,
169+
check_ruleset_name="ms_intune_app_licenses",
170+
check_default_parameters={
171+
"lic_unit_available_lower": ("lic_unit_available_lower_pct", ("fixed", (10.0, 5.0))),
172+
"lic_total_min": 1,
173+
},
174+
)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
title: Microsoft Intune: App Licenses
2+
agents: intune
3+
catalog: cloud/Microsoft
4+
license: GPLv2
5+
distribution: Christopher Pommer
6+
description:
7+
This check monitors the licenses from published Intune applications.
8+
9+
Depending on the configured check levels, the service is in
10+
state {OK}, {WARN} or {CRIT}.
11+
12+
You have to configure the special agent {Microsoft Intune}.
13+
14+
item:
15+
The type and application name.
16+
17+
discovery:
18+
One service is created for each application name and type combination.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/env python3
2+
# -*- encoding: utf-8; py-indent-offset: 4 -*-
3+
4+
# Copyright (C) 2024 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+
from cmk.graphing.v1 import Title
22+
from cmk.graphing.v1.graphs import Graph, MinimalRange
23+
from cmk.graphing.v1.metrics import (
24+
Color,
25+
CriticalOf,
26+
DecimalNotation,
27+
Metric,
28+
StrictPrecision,
29+
Unit,
30+
WarningOf,
31+
)
32+
from cmk.graphing.v1.perfometers import Closed, FocusRange, Perfometer
33+
34+
UNIT_COUNTER = Unit(DecimalNotation(""), StrictPrecision(0))
35+
UNIT_PERCENTAGE = Unit(DecimalNotation("%"))
36+
37+
metric_ms_intune_app_licenses_consumed = Metric(
38+
name="ms_intune_app_licenses_consumed",
39+
title=Title("Consumed"),
40+
unit=UNIT_COUNTER,
41+
color=Color.CYAN,
42+
)
43+
44+
metric_ms_intune_app_licenses_consumed_pct = Metric(
45+
name="ms_intune_app_licenses_consumed_pct",
46+
title=Title("Usage"),
47+
unit=UNIT_PERCENTAGE,
48+
color=Color.BLUE,
49+
)
50+
51+
metric_ms_intune_app_licenses_total = Metric(
52+
name="ms_intune_app_licenses_total",
53+
title=Title("Total"),
54+
unit=UNIT_COUNTER,
55+
color=Color.DARK_CYAN,
56+
)
57+
58+
metric_ms_intune_app_licenses_available = Metric(
59+
name="ms_intune_app_licenses_available",
60+
title=Title("Available"),
61+
unit=UNIT_COUNTER,
62+
color=Color.LIGHT_GRAY,
63+
)
64+
65+
66+
graph_ms_intune_app_licenses_count = Graph(
67+
name="ms_intune_app_licenses_count",
68+
title=Title("License count"),
69+
compound_lines=[
70+
"ms_intune_app_licenses_consumed",
71+
"ms_intune_app_licenses_available",
72+
],
73+
simple_lines=[
74+
"ms_intune_app_licenses_total",
75+
WarningOf("ms_intune_app_licenses_consumed"),
76+
CriticalOf("ms_intune_app_licenses_consumed"),
77+
],
78+
)
79+
80+
graph_ms_intune_app_licenses_usage = Graph(
81+
name="ms_intune_app_licenses_usage",
82+
title=Title("License usage"),
83+
minimal_range=MinimalRange(0, 100),
84+
simple_lines=[
85+
"ms_intune_app_licenses_consumed_pct",
86+
WarningOf("ms_intune_app_licenses_consumed_pct"),
87+
CriticalOf("ms_intune_app_licenses_consumed_pct"),
88+
],
89+
)
90+
91+
perfometer_ms_intune_app_licenses_consumed_pct = Perfometer(
92+
name="ms_intune_app_licenses_consumed_pct",
93+
focus_range=FocusRange(Closed(0), Closed(100)),
94+
segments=["ms_intune_app_licenses_consumed_pct"],
95+
)

intune/libexec/agent_ms_intune

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,22 @@ import requests
2828
import cmk.utils.password_store
2929

3030

31+
def map_app_type(app_type):
32+
33+
type_map = {
34+
"#microsoft.graph.androidForWorkApp": "Android for work",
35+
"#microsoft.graph.androidManagedStoreApp": "Android managed store",
36+
"#microsoft.graph.androidManagedStoreWebApp": "Android managed store web",
37+
"#microsoft.graph.iosVppApp": "iOS VPP",
38+
"#microsoft.graph.macOsVppApp": "macOS VPP",
39+
"#microsoft.graph.microsoftStoreForBusinessApp": "MS Store for business",
40+
}
41+
42+
app_type_short = type_map.get(app_type, app_type.replace("#microsoft.graph.", ""))
43+
44+
return app_type_short
45+
46+
3147
def parse_arguments():
3248
parser = argparse.ArgumentParser()
3349
parser.add_argument(
@@ -168,6 +184,54 @@ def get_intune_vpp_tokens(token):
168184
return vpp_token_list
169185

170186

187+
def get_intune_app_licenses(token):
188+
intune_apps_url = (
189+
"https://graph.microsoft.com/beta/deviceAppManagement/mobileApps?$filter=publishingState eq 'published'"
190+
)
191+
192+
headers = {"Accept": "application/json", "Authorization": "Bearer " + token}
193+
194+
intune_apps = []
195+
196+
while True:
197+
try:
198+
intune_apps_response = requests.get(intune_apps_url, headers=headers)
199+
intune_apps_response.raise_for_status()
200+
except requests.exceptions.RequestException as err:
201+
print(intune_apps_response.text)
202+
sys.stderr.write("CRITICAL | Failed to get intune apps\n")
203+
sys.stderr.write(f"Error: {err}\n")
204+
sys.exit(2)
205+
206+
intune_apps_json = intune_apps_response.json()
207+
intune_apps.extend(intune_apps_json.get("value", []))
208+
209+
next_link = intune_apps_json.get("@odata.nextLink")
210+
if next_link:
211+
intune_apps = next_link
212+
else:
213+
break
214+
215+
intune_app_licenses = [
216+
app for app in intune_apps if app.get("publishingState") == "published" and app.get("totalLicenseCount")
217+
]
218+
219+
app_list = []
220+
for app in intune_app_licenses:
221+
app_dict = {
222+
"app_type": map_app_type(app["@odata.type"]),
223+
"app_name": app["displayName"],
224+
"app_publisher": app.get("publisher"),
225+
"app_license_total": app["totalLicenseCount"],
226+
"app_license_consumed": app["usedLicenseCount"],
227+
"app_assigned": app.get("isAssigned"),
228+
}
229+
230+
app_list.append(app_dict)
231+
232+
return app_list
233+
234+
171235
def main():
172236
args = parse_arguments()
173237
tenant_id = args.tenant_id
@@ -199,6 +263,12 @@ def main():
199263
print("<<<ms_intune_apple_vpp_tokens:sep(0)>>>")
200264
print(json.dumps(intune_apple_vpp_tokens))
201265

266+
if "app_licenses" in services_to_monitor:
267+
intune_app_licenses = get_intune_app_licenses(token)
268+
if intune_app_licenses:
269+
print("<<<ms_intune_app_licenses:sep(0)>>>")
270+
print(json.dumps(intune_app_licenses))
271+
202272

203273
if __name__ == "__main__":
204274
main()

0 commit comments

Comments
 (0)