Skip to content

Commit 72ffc44

Browse files
committed
Initial buildout of check-jamf-json-schemas hook
1 parent f51b255 commit 72ffc44

1 file changed

Lines changed: 218 additions & 1 deletion

File tree

pre_commit_hooks/check_jamf_json_schemas.py

Lines changed: 218 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,37 @@
22
# -*- coding: utf-8 -*-
33
"""This hook checks Jamf JSON schemas for inconsistencies and common issues."""
44

5+
# References:
6+
# - https://developer.apple.com/library/archive/documentation/MacOSXServer/Conceptual/Preference_schema_Files/Preface/Preface.html
7+
# - https://github.com/ProfileCreator/Profileschemas/wiki/schema-Format
8+
# - https://mosen.github.io/profiledocs/schema.html
9+
510
import argparse
611
import json
12+
from datetime import datetime
13+
14+
from pre_commit_hooks.util import PLIST_TYPES, validate_required_keys
15+
16+
# Types found in the Jamf JSON schemas
17+
SCHEMA_TYPES = (
18+
"string",
19+
"boolean",
20+
"object",
21+
"integer",
22+
"array",
23+
"data",
24+
"float",
25+
"real",
26+
"date",
27+
)
28+
29+
# List keys and their expected item types
30+
SCHEMA_LIST_TYPES = {
31+
"enum_titles": str,
32+
"enum": (str, int, float, bool),
33+
"links": dict,
34+
"anyOf": dict,
35+
}
736

837

938
def build_argument_parser():
@@ -16,6 +45,176 @@ def build_argument_parser():
1645
return parser
1746

1847

48+
def validate_schema_key_types(name, schema, filename):
49+
"""Validation of schema key types."""
50+
51+
# Schema keys and their known types. Omitted keys are left unvalidated.
52+
key_types = {
53+
"description": str,
54+
"enum_titles": list,
55+
"enum": list,
56+
"href": str,
57+
"items": dict,
58+
"links": list,
59+
"options": dict,
60+
"pattern": str,
61+
"properties": dict,
62+
"property_order": int,
63+
"rel": str,
64+
"title": str,
65+
"type": str,
66+
"anyOf": list,
67+
}
68+
69+
passed = True
70+
for schema_key, expected_type in key_types.items():
71+
if schema_key in schema:
72+
if not isinstance(schema[schema_key], expected_type):
73+
print(
74+
"{}: {} key {} should be type {}, not type {}".format(
75+
filename,
76+
name,
77+
schema_key,
78+
expected_type,
79+
type(schema[schema_key]),
80+
)
81+
)
82+
passed = False
83+
84+
return passed
85+
86+
87+
def validate_type(name, property, filename):
88+
"""Ensure property type keu is present and among expected values."""
89+
passed = True
90+
type_found = None
91+
92+
if "type" in property:
93+
type_found = property.get("type")
94+
elif "anyOf" in property:
95+
for t in [x.get("type") for x in property["anyOf"]]:
96+
if t != "null":
97+
type_found = t
98+
break
99+
100+
if type_found not in SCHEMA_TYPES:
101+
print('{}: Unexpected "{}" type "{}"'.format(filename, name, type_found))
102+
passed = False
103+
104+
return passed, type_found
105+
106+
107+
def validate_list_item_types(name, schema, filename):
108+
"""Validation of list member items."""
109+
110+
passed = True
111+
for name in SCHEMA_LIST_TYPES:
112+
if name in schema:
113+
try:
114+
actual_type = type(schema[name][0])
115+
except IndexError:
116+
# Probably an empty array; no way to validate items
117+
continue
118+
if isinstance(SCHEMA_LIST_TYPES[name], tuple):
119+
desired_types = SCHEMA_LIST_TYPES[name]
120+
else:
121+
desired_types = [SCHEMA_LIST_TYPES[name]]
122+
if actual_type not in desired_types:
123+
print(
124+
'{}: "{}" items should be {}, not {}'.format(
125+
filename, name, SCHEMA_LIST_TYPES[name], actual_type
126+
)
127+
)
128+
passed = False
129+
130+
return passed
131+
132+
133+
def validate_default(name, property, type_found, filename):
134+
"""Ensure that default values have the expected type."""
135+
passed = True
136+
137+
for test_key in ("default",):
138+
if test_key in property:
139+
if type(property[test_key]) == datetime:
140+
actual_type = str
141+
else:
142+
actual_type = type(property[test_key])
143+
if actual_type != PLIST_TYPES[type_found]:
144+
print(
145+
"{}: {} value for {} should be {}, not {}".format(
146+
filename,
147+
test_key,
148+
name,
149+
PLIST_TYPES[type_found],
150+
type(property[test_key]),
151+
)
152+
)
153+
passed = False
154+
155+
return passed
156+
157+
158+
def validate_urls(name, property, filename):
159+
"""Ensure that URL values are actual URLs."""
160+
passed = True
161+
162+
url_keys = ("pfm_app_url", "pfm_documentation_url")
163+
for url_key in url_keys:
164+
if url_key in property:
165+
if not property[url_key].startswith("http"):
166+
print(
167+
"{}: {} {} value doesn't look like a URL: {}".format(
168+
filename,
169+
name,
170+
url_key,
171+
property[url_key],
172+
)
173+
)
174+
passed = False
175+
176+
return passed
177+
178+
179+
def validate_properties(properties, filename):
180+
"""Given a list of properties, run validation on their contents."""
181+
passed = True
182+
183+
for name, prop in properties.items():
184+
if name.strip() == "":
185+
name = "<unnamed property>"
186+
187+
# Validate URLs
188+
if not validate_urls(name, prop, filename):
189+
passed = False
190+
191+
# Check for presence of "type" key.
192+
type_ok, type_found = validate_type(name, prop, filename)
193+
if not type_ok:
194+
passed = False
195+
break # No need to continue checking this property
196+
197+
# Check that list items are of the expected type
198+
if not validate_list_item_types(name, prop, filename):
199+
passed = False
200+
201+
# Check default values to ensure consistent type
202+
if not validate_default(name, prop, type_found, filename):
203+
passed = False
204+
205+
# TODO: Validate pfm_conditionals
206+
# https://github.com/ProfileCreator/Profileschemas/wiki/schema-Format#example-conditions--exclusions
207+
208+
# TODO: Process $ref references
209+
210+
# Recursively validate sub-sub-properties
211+
if "properties" in prop:
212+
if not validate_properties(prop["properties"], filename):
213+
passed = False
214+
215+
return passed
216+
217+
19218
def main(argv=None):
20219
"""Main process."""
21220

@@ -28,9 +227,27 @@ def main(argv=None):
28227
try:
29228
with open(filename, "rb") as openfile:
30229
schema = json.load(openfile)
31-
except Exception as err:
230+
except json.decoder.JSONDecodeError as err:
32231
print("{}: json parsing error: {}".format(filename, err))
33232
retval = 1
233+
break # No need to continue checking this file
234+
235+
# Check for presence of required keys.
236+
required_keys = ("title", "properties", "description")
237+
if not validate_required_keys(schema, filename, required_keys):
238+
retval = 1
239+
break # No need to continue checking this file
240+
241+
# Ensure top level keys and their list items have expected types.
242+
if not validate_schema_key_types("<root>", schema, filename):
243+
retval = 1
244+
if not validate_list_item_types("<root>", schema, filename):
245+
retval = 1
246+
247+
# Run checks recursively for all properties
248+
if "properties" in schema:
249+
if not validate_properties(schema["properties"], filename):
250+
retval = 1
34251

35252
return retval
36253

0 commit comments

Comments
 (0)