Skip to content

Commit 6f43ccc

Browse files
Gracefully handle payload.schema.json
1 parent ddb8dd4 commit 6f43ccc

4 files changed

Lines changed: 155 additions & 44 deletions

File tree

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ repos:
5656
require_serial: true
5757
files: |
5858
(?x)(
59-
^src/modules/complianceengine/src/lib/payload.schema.jsonx$|
59+
^src/modules/complianceengine/src/lib/payload.schema.json$|
6060
^src/modules/complianceengine/src/lib/procedures/.*\.cpp$|
6161
^src/modules/complianceengine/src/lib/procedures/.*\.schema.json$
6262
)
@@ -70,7 +70,7 @@ repos:
7070
require_serial: true
7171
files: |
7272
(?x)(
73-
^src/modules/complianceengine/src/lib/payload.schema.jsonx$|
73+
^src/modules/complianceengine/src/lib/payload.schema.json$|
7474
^src/modules/complianceengine/src/lib/procedures/.*\.h$|
7575
^src/modules/complianceengine/src/lib/procedures/.*\.schema.json$
7676
)

src/modules/complianceengine/src/lib/GenInterface.py

Lines changed: 98 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
@dataclass
1111
class Enum:
12-
mapping: dict[str, str] = None
12+
mapping: OrderedDict[str, str] = None
1313
description: str = None
1414

1515
@dataclass
@@ -23,7 +23,7 @@ class Parameter:
2323

2424
@dataclass
2525
class ProcedureDetails:
26-
fields: list[Parameter] = None
26+
fields: List[Parameter] = None
2727
filename: str = None
2828
line: int = None # Line number of the procedure declaration
2929

@@ -96,7 +96,7 @@ def process_params_block(file, lineno: int):
9696
return fields, lineno
9797

9898
def process_enum_block(file, lineno: int) -> tuple[Enum, int]:
99-
result = Enum(mapping=dict[str, str]())
99+
result = Enum(mapping=OrderedDict[str, str]())
100100
inside = False
101101
label_value = None
102102
for line in file:
@@ -398,24 +398,8 @@ def generate_procedure_map_impl(model: Model, filename: str):
398398
f.write("};\n")
399399
f.write("} // namespace ComplianceEngine\n")
400400

401-
def main():
402-
"""Main function to process a directory and print results."""
403-
basedir = os.path.dirname(os.path.realpath(__file__))
404-
print(f"Processing procedures in {basedir}")
405-
model = Model(OrderedDict[str, Procedure](), OrderedDict[str, Enum](), set[str]())
406-
model.supported_types.add("int")
407-
model.supported_types.add("std::string")
408-
model.supported_types.add("regex")
409-
410-
# Iterate over header files and update the model
411-
process_directory(model, f"{basedir}/procedures/")
412-
413-
validate_model(model)
414-
415-
generate_procedure_map_header(model, f"{basedir}/ProcedureMap2.h")
416-
generate_procedure_map_impl(model, f"{basedir}/ProcedureMap2.cpp")
417-
418-
# Split the model into per-file schemas
401+
def split_model_by_file(model: Model) -> OrderedDict[str, Model]:
402+
"""Split the model into per-file schemas."""
419403
schema_model = OrderedDict[str, Model]()
420404
for name, details in model.procedures.items():
421405
if details.audit:
@@ -428,29 +412,105 @@ def main():
428412
if filename not in schema_model:
429413
schema_model[filename] = Model(OrderedDict[str, Procedure](), OrderedDict[str, Enum](), set[str]())
430414
schema_model[filename].procedures[name] = details
431-
for filename, model in schema_model.items():
415+
return schema_model
416+
417+
def generate_global_json_schema(split_model: OrderedDict[str, Model], basedir: str):
418+
"""Generate a global JSON schema file."""
419+
print("Reading global payload schema.")
420+
with open(f"{basedir}/payload.schema.json", "r") as f:
421+
global_schema = json.load(f)
422+
423+
# Used for model validation
424+
definitions = global_schema.get("definitions", {})
425+
audits = definitions.get("auditProcedure", {}).get("anyOf", [])
426+
audits_dict = dict()
427+
for audit in audits:
428+
pat = r"procedures/([^/]+)\.schema\.json#/definitions/audit"
429+
match = re.match(pat, audit.get("$ref",""))
430+
if not match:
431+
raise ValueError(f"invalid audit reference {audit}")
432+
fname = match.group(1)
433+
audits_dict[fname] = audit
434+
435+
remediations = definitions.get("remediationProcedure", {}).get("anyOf", [])
436+
remediations_dict = dict()
437+
for remediation in remediations:
438+
pat = r"procedures/([^/]+)\.schema\.json#/definitions/remediation"
439+
match = re.match(pat, remediation.get("$ref",""))
440+
if not match:
441+
# This may happend when the missing remediation fallback is present
442+
continue
443+
fname = match.group(1)
444+
remediations_dict[fname] = remediation
445+
446+
for filename in split_model.keys():
447+
if not filename.endswith(".h"):
448+
raise ValueError("filename must end with .h")
449+
filename = filename[:-2]
450+
audits_dict[filename] = {"$ref" : f"procedures/{filename}.schema.json#/definitions/audit" }
451+
remediations_dict[filename] = {"$ref" : f"procedures/{filename}.schema.json#/definitions/remediation" }
452+
453+
audits = []
454+
for k, v in sorted(audits_dict.items()):
455+
audits.append(v)
456+
remediations = []
457+
for k, v in sorted(audits_dict.items()):
458+
remediations.append(v)
459+
460+
definitions["auditProcedure"]["anyOf"] = audits
461+
definitions["remediationProcedure"]["anyOf"] = remediations
462+
# If there's no remediation we fall back to audit
463+
fallback = { "$ref": "#definitions/auditProcedure" }
464+
if fallback not in definitions["remediationProcedure"]["anyOf"]:
465+
definitions["remediationProcedure"]["anyOf"].append( { "$ref": "#definitions/auditProcedure" })
466+
print("Dumping global schema.")
467+
with open(f"{basedir}/payload.schema.json", "w") as f:
468+
json.dump(global_schema, f, indent=2)
469+
# Keep EOL check happy
470+
f.write("\n")
471+
472+
def generate_json_schema(model: Model, basedir: str):
473+
"""Generate JSON schema files for each procedure."""
474+
split_model = split_model_by_file(model)
475+
print("Reading global payload schema.")
476+
with open(f"{basedir}/payload.schema.json", "r") as f:
477+
global_schema = json.load(f)
478+
479+
for filename, model in split_model.items():
432480
print(f"Writing schema for {filename}.")
433-
if filename.endswith(".h"):
434-
filename = filename[:-2]
481+
if not filename.endswith(".h"):
482+
raise ValueError("filename must end with .h")
483+
filename = filename[:-2]
435484
json_schema_model = create_file_schema(model)
436485
with open(f"{basedir}/procedures/{filename}.schema.json","w") as f:
437486
json.dump(json_schema_model, f, indent=2)
438487
# Keep EOL check happy
439488
f.write("\n")
440-
# Used for model validation
441-
# print("Reading global payload schema.")
442-
# schema = None
443-
# with open(f"{basedir}/payload.schema.json", "r") as f:
444-
# schema = json.load(f)
445-
# schema["definitions"]["auditProcedure"]["anyOf"] = [ {"$ref" : f"procedures/{fname}.schema.json#/definitions/audit" } for fname in result.keys() ]
446-
# schema["definitions"]["remediationProcedure"]["anyOf"] = [ {"$ref" : f"procedures/{fname}.schema.json#/definitions/remediation" } for fname in result.keys() ]
447-
# # If there's no remediation we fall back to audit
448-
# schema["definitions"]["remediationProcedure"]["anyOf"].append( { "$ref": "#definitions/auditProcedure" })
449-
# print("Dumping global schema.")
450-
# with open(f"{basedir}/payload.schema.json", "w") as f:
451-
# json.dump(schema, f, indent=2)
452-
# # Keep EOL check happy
453-
# f.write("\n")
489+
490+
generate_global_json_schema(split_model, basedir)
491+
492+
def main():
493+
"""Main function to process a directory and print results."""
494+
basedir = os.path.dirname(os.path.realpath(__file__))
495+
model = Model(OrderedDict[str, Procedure](), OrderedDict[str, Enum](), set[str]())
496+
497+
# Supported types are used in model validation
498+
model.supported_types.add("int")
499+
model.supported_types.add("std::string")
500+
model.supported_types.add("regex")
501+
502+
# Iterate over header files and update the model
503+
process_directory(model, f"{basedir}/procedures/")
504+
505+
# Test model integrity
506+
validate_model(model)
507+
508+
# Generate C++ procedure map files
509+
generate_procedure_map_header(model, f"{basedir}/ProcedureMap2.h")
510+
generate_procedure_map_impl(model, f"{basedir}/ProcedureMap2.cpp")
511+
512+
# Generate JSON schema files
513+
generate_json_schema(model, basedir)
454514

455515
if __name__ == "__main__":
456516
import sys

src/modules/complianceengine/src/lib/GenJSONSchemas.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ def process_directory(directory_path):
182182
file_result = process_cpp_file(filepath)
183183
schema = create_file_schema(file_result)
184184
definitions = schema.get("definitions")
185-
if len(definitions.get("audit").get("anyOf")) > 0 and len(definitions.get("remediation").get("anyOf")) > 0:
185+
if len(definitions.get("audit").get("anyOf")) > 0 or len(definitions.get("remediation").get("anyOf")) > 0:
186186
result[basename] = schema
187187

188188
return result
@@ -203,10 +203,46 @@ def main():
203203
schema = None
204204
with open(f"{basedir}/payload.schema.json", "r") as f:
205205
schema = json.load(f)
206-
schema["definitions"]["auditProcedure"]["anyOf"] = [ {"$ref" : f"procedures/{fname}.schema.json#/definitions/audit" } for fname in result.keys() ]
207-
schema["definitions"]["remediationProcedure"]["anyOf"] = [ {"$ref" : f"procedures/{fname}.schema.json#/definitions/remediation" } for fname in result.keys() ]
206+
207+
definitions = schema.get("definitions", {})
208+
audits = definitions.get("auditProcedure", {}).get("anyOf", [])
209+
audits_dict = dict()
210+
for audit in audits:
211+
pat = r"procedures/([^/]+)\.schema\.json#/definitions/audit"
212+
match = re.match(pat, audit.get("$ref",""))
213+
if not match:
214+
continue
215+
fname = match.group(1)
216+
audits_dict[fname] = audit
217+
218+
remediations = definitions.get("remediationProcedure", {}).get("anyOf", [])
219+
remediations_dict = dict()
220+
for remediation in remediations:
221+
pat = r"procedures/([^/]+)\.schema\.json#/definitions/remediation"
222+
match = re.match(pat, remediation.get("$ref",""))
223+
if not match:
224+
continue
225+
fname = match.group(1)
226+
remediations_dict[fname] = remediation
227+
228+
for fname in result.keys():
229+
audits_dict[fname] = {"$ref" : f"procedures/{fname}.schema.json#/definitions/audit" }
230+
remediations_dict[fname] = {"$ref" : f"procedures/{fname}.schema.json#/definitions/remediation" }
231+
232+
audits = []
233+
for k, v in sorted(audits_dict.items()):
234+
audits.append(v)
235+
remediations = []
236+
for k, v in sorted(audits_dict.items()):
237+
remediations.append(v)
238+
239+
# If there's no remediation we fall back to audit
240+
schema["definitions"]["auditProcedure"]["anyOf"] = audits
241+
schema["definitions"]["remediationProcedure"]["anyOf"] = remediations
208242
# If there's no remediation we fall back to audit
209-
schema["definitions"]["remediationProcedure"]["anyOf"].append( { "$ref": "#definitions/auditProcedure" })
243+
fallback = { "$ref": "#definitions/auditProcedure" }
244+
if fallback not in definitions["remediationProcedure"]["anyOf"]:
245+
definitions["remediationProcedure"]["anyOf"].append( { "$ref": "#definitions/auditProcedure" })
210246
print("Dumping global schema.")
211247
with open(f"{basedir}/payload.schema.json", "w") as f:
212248
json.dump(schema, f, indent=2)

src/modules/complianceengine/src/lib/payload.schema.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,9 @@
205205
{
206206
"$ref": "procedures/ExecuteCommandGrep.schema.json#/definitions/audit"
207207
},
208+
{
209+
"$ref": "procedures/FileRegexMatch.schema.json#/definitions/audit"
210+
},
208211
{
209212
"$ref": "procedures/PackageInstalled.schema.json#/definitions/audit"
210213
},
@@ -219,6 +222,9 @@
219222
},
220223
{
221224
"$ref": "procedures/TestingProcedures.schema.json#/definitions/audit"
225+
},
226+
{
227+
"$ref": "procedures/UfwStatus.schema.json#/definitions/audit"
222228
}
223229
]
224230
},
@@ -299,6 +305,9 @@
299305
{
300306
"$ref": "procedures/ExecuteCommandGrep.schema.json#/definitions/audit"
301307
},
308+
{
309+
"$ref": "procedures/FileRegexMatch.schema.json#/definitions/audit"
310+
},
302311
{
303312
"$ref": "procedures/PackageInstalled.schema.json#/definitions/audit"
304313
},
@@ -313,6 +322,12 @@
313322
},
314323
{
315324
"$ref": "procedures/TestingProcedures.schema.json#/definitions/audit"
325+
},
326+
{
327+
"$ref": "procedures/UfwStatus.schema.json#/definitions/audit"
328+
},
329+
{
330+
"$ref": "#definitions/auditProcedure"
316331
}
317332
]
318333
}

0 commit comments

Comments
 (0)