-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathcheck_jamf_json_manifests.py
More file actions
executable file
·247 lines (196 loc) · 7.61 KB
/
Copy pathcheck_jamf_json_manifests.py
File metadata and controls
executable file
·247 lines (196 loc) · 7.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
#!/usr/bin/python
"""This hook checks Jamf JSON schema custom app manifests for inconsistencies and common issues."""
# References:
# - https://docs.jamf.com/technical-papers/jamf-pro/json-schema/10.19.0/Understanding_the_Structure_of_a_JSON_Schema_Manifest.html
# - https://github.com/Jamf-Custom-Profile-Schemas
import argparse
import json
from datetime import datetime
from typing import Any
from pre_commit_macadmin_hooks.util import validate_required_keys
# Types found in the Jamf JSON manifests
MANIFEST_TYPES = {
"array": list,
"boolean": bool,
"data": str,
"date": datetime,
"float": float,
"integer": int,
"number": int,
"object": dict,
"real": float,
"string": str,
}
# List keys and their expected item types
MANIFEST_LIST_TYPES = {
"enum_titles": str,
"enum": (str, int, float, bool),
"links": dict,
"anyOf": dict,
}
def build_argument_parser() -> argparse.ArgumentParser:
"""Build and return the argument parser."""
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument("filenames", nargs="*", help="Filenames to check.")
return parser
def validate_key_types(name: str, manifest: dict[str, Any], filename: str) -> bool:
"""Validation of manifest key types."""
# Manifest keys and their known types. Omitted keys are left unvalidated.
key_types = {
"description": str,
"enum_titles": list,
"enum": list,
"href": str,
"items": dict,
"links": list,
"options": dict,
"pattern": str,
"properties": dict,
"property_order": int,
"rel": str,
"title": str,
"type": str,
"anyOf": list,
}
passed = True
for manifest_key, expected_type in key_types.items():
if manifest_key in manifest:
if not isinstance(manifest[manifest_key], expected_type):
print(
f"{filename}: {name} key {manifest_key} should be type "
f"{expected_type}, not type {type(manifest[manifest_key])}"
)
passed = False
return passed
def validate_type(
name: str, property: dict[str, Any], filename: str
) -> tuple[bool, str | None]: # noqa: A002
"""Ensure property type keu is present and among expected values."""
passed = True
type_found = None
if "type" in property:
type_found = property.get("type")
elif "anyOf" in property:
for t in [x.get("type") for x in property["anyOf"]]:
if t != "null":
type_found = t
break
if type_found not in MANIFEST_TYPES:
print(f'{filename}: Unexpected "{name}" type "{type_found}"')
passed = False
return passed, type_found
def validate_list_item_types(
name: str, manifest: dict[str, Any], filename: str
) -> bool:
"""Validation of list member items."""
passed = True
for name in MANIFEST_LIST_TYPES:
if name in manifest:
try:
actual_type = type(manifest[name][0])
except IndexError:
# Probably an empty array; no way to validate items
continue
manifest_list_type = MANIFEST_LIST_TYPES[name]
if isinstance(manifest_list_type, tuple):
# MANIFEST_LIST_TYPES[name] is a tuple of types
desired_types = list(manifest_list_type)
else:
# MANIFEST_LIST_TYPES[name] is a single type
desired_types = [manifest_list_type]
if actual_type not in desired_types:
print(
f'{filename}: "{name}" items should be {MANIFEST_LIST_TYPES[name]}, not {actual_type}'
)
passed = False
return passed
def validate_default(
name: str, prop: dict[str, Any], type_found: str | None, filename: str
) -> bool:
"""Ensure that default values have the expected type."""
passed = True
for test_key in ("default",):
if test_key in prop:
if isinstance(prop[test_key], datetime):
actual_type = str
else:
actual_type = type(prop[test_key])
if actual_type != MANIFEST_TYPES.get(type_found) if type_found else None:
print(
f"{filename}: {test_key} value for {name} should be {MANIFEST_TYPES.get(type_found) if type_found else 'Unknown'}, not {type(prop[test_key])}"
)
passed = False
return passed
def validate_urls(name: str, prop: dict[str, Any], filename: str) -> bool:
"""Ensure that URL values are actual URLs."""
passed = True
url_keys = ("pfm_app_url", "pfm_documentation_url")
for url_key in url_keys:
if url_key in prop:
if not prop[url_key].startswith("http"):
print(
f"{filename}: {name} {url_key} value doesn't look like a URL: {prop[url_key]}"
)
passed = False
return passed
def validate_properties(properties: dict[str, Any], filename: str) -> bool:
"""Given a list of properties, run validation on their contents."""
passed = True
for name, prop in properties.items():
if name.strip() == "":
name = "<unnamed property>"
# Validate URLs
if not validate_urls(name, prop, filename):
passed = False
# Check for presence of "type" key.
type_ok, type_found = validate_type(name, prop, filename)
if not type_ok:
passed = False
break # No need to continue checking this property
# Check that list items are of the expected type
if not validate_list_item_types(name, prop, filename):
passed = False
# Check default values to ensure consistent type
if not validate_default(name, prop, type_found, filename):
passed = False
# TODO: Validate pfm_conditionals
# https://github.com/ProfileCreator/ProfileManifests/wiki/Manifest-Format#example-conditions--exclusions
# TODO: Process $ref references
# Recursively validate sub-sub-properties
if "properties" in prop:
if not validate_properties(prop["properties"], filename):
passed = False
return passed
def main(argv: list[str] | None = None) -> int:
"""Main process."""
# Parse command line arguments.
argparser = build_argument_parser()
args = argparser.parse_args(argv)
retval = 0
for filename in args.filenames:
try:
with open(filename, "rb") as openfile:
manifest = json.load(openfile)
except json.decoder.JSONDecodeError as err:
print(f"{filename}: json parsing error: {err}")
retval = 1
break # No need to continue checking this file
# Check for presence of required keys.
required_keys = ["title", "properties", "description"]
if not validate_required_keys(manifest, filename, required_keys):
retval = 1
break # No need to continue checking this file
# Ensure top level keys and their list items have expected types.
if not validate_key_types("<root>", manifest, filename):
retval = 1
if not validate_list_item_types("<root>", manifest, filename):
retval = 1
# Run checks recursively for all properties
if "properties" in manifest:
if not validate_properties(manifest["properties"], filename):
retval = 1
return retval
if __name__ == "__main__":
exit(main())