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+
510import argparse
611import 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
938def 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+
19218def 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