#!/usr/bin/env python3 # filename: generate_scap.py # description: Input a keyword for the baseline, output the scap/oval/xccdf import sys import os import os.path import yaml import glob import re import warnings from pathlib import Path from datetime import datetime import shutil from time import sleep import argparse from xml.sax.saxutils import escape warnings.filterwarnings("ignore", category=DeprecationWarning) def validate_file(arg): if (file := Path(arg)).is_file(): return file else: raise FileNotFoundError(arg) def format_mobileconfig_fix(mobileconfig): """Takes a list of domains and setting from a mobileconfig, and reformats it for the output of the fix section of the guide. """ rulefix = "" for domain, settings in mobileconfig.items(): if domain == "com.apple.ManagedClient.preferences": rulefix = rulefix + \ (f"NOTE: The following settings are in the ({domain}) payload. This payload requires the additional settings to be sub-payloads within, containing their defined payload types.\n\n") rulefix = rulefix + format_mobileconfig_fix(settings) else: rulefix = rulefix + ( f"Create a configuration profile containing the following keys in the ({domain}) payload type:\n\n") rulefix = rulefix + "[source,xml]\n----\n" for item in settings.items(): rulefix = rulefix + (f"{item[0]}\n") if type(item[1]) == bool: rulefix = rulefix + \ (f"<{str(item[1]).lower()}/>\n") elif type(item[1]) == list: rulefix = rulefix + "\n" for setting in item[1]: rulefix = rulefix + \ (f" {setting}\n") rulefix = rulefix + "\n" elif type(item[1]) == int: rulefix = rulefix + \ (f"{item[1]}\n") elif type(item[1]) == str: rulefix = rulefix + \ (f"{item[1]}\n") elif type(item[1]) == dict: rulefix = rulefix + "\n" for k,v in item[1].items(): if type(v) == dict: rulefix = rulefix + \ (f" {k}\n") rulefix = rulefix + \ (f" \n") for x,y in v.items(): rulefix = rulefix + \ (f" {x}\n") rulefix = rulefix + \ (f" {y}\n") rulefix = rulefix + \ (f" \n") break if isinstance(v, list): rulefix = rulefix + " \n" for setting in v: rulefix = rulefix + \ (f" {setting}\n") rulefix = rulefix + " \n" else: rulefix = rulefix + \ (f" {k}\n") rulefix = rulefix + \ (f" {v}\n") rulefix = rulefix + "\n" rulefix = rulefix + "----\n\n" return rulefix def replace_ocil(xccdf, x): regex = r'''([\r\n].*?)(?:=?\r|\n)(.*?(?:def:{}\").*)'''.format(x) substr = '''''' result = re.sub(regex, substr, xccdf, 0, re.MULTILINE) return result def disa_stig_rules(stig_id, stig): newtitle = str() regex = r"(SRG.*\d)<\/title>.*.{}".format(stig_id) matches = re.search(regex,stig) #SRG if matches: newtitle = str(matches.group(1)) regex = r"Rule id=\"(.*\S)\" we.*.{}".format(stig_id) matches = re.search(regex,stig) #RuleID if matches: newtitle = newtitle + ", " + str(matches.group(1).split("_")[0]) # srg-123-456. SV-7891234 return newtitle def create_args(): parser = argparse.ArgumentParser( description="Easily generate xccdf, oval, or scap datastream. If no option is defined, it will generate an scap datastream file.") parser.add_argument("-x", "--xccdf", default=None, help="Generate an xccdf file.", action="store_true") parser.add_argument("-o", "--oval", default=None, help="Generate an oval file of the checks.", action="store_true") parser.add_argument("-l", "--list_tags", default=None, help="List the available keyword tags to search for.", action="store_true") parser.add_argument("-b", "--baseline", default="None", help="Choose a baseline to generate an xml file for, if none is specified it will generate for every rule found.", action="store") parser.add_argument('--disastig','-d', default=None, type=validate_file, help="DISA STIG File", required=False) return parser.parse_args() def generate_scap(all_rules, all_baselines, args, stig): export_as = "" version_file = "../VERSION.yaml" with open(version_file) as r: version_yaml = yaml.load(r, Loader=yaml.SafeLoader) if args.xccdf: export_as = "xccdf" if args.oval: export_as = "oval" if "ios" in version_yaml['cpe']: print("OVAL generation is not available on iOS") exit() if "visionOS" in version_yaml['cpe']: print("OVAL generation is not available on visionOS") exit() if args.oval == None and args.xccdf == None: export_as = "scap" if "ios" in version_yaml['cpe']: print("iOS will only export as XCCDF") export_as = "xccdf" if "visionos" in version_yaml['cpe']: print("visionOS will only export as XCCDF") export_as = "xccdf" now = datetime.now() date_time_string = now.strftime("%Y-%m-%dT%H:%M:%S") year = now.year filenameversion = version_yaml['version'].split(",")[1].replace(" ", "_")[1:] output = "../build/macOS_{0}_Security_Compliance_Benchmark-{1}".format(version_yaml['os'],filenameversion) if "ios" in version_yaml['cpe']: output = "../build/iOS_{0}_Security_Compliance_Benchmark-{1}".format(version_yaml['os'],filenameversion) if "visionos" in version_yaml['cpe']: output = "../build/visionOS_{0}_Security_Compliance_Benchmark-{1}".format(version_yaml['os'],filenameversion) if export_as == "xccdf": output = output + "_xccdf.xml" if export_as == "oval": output = output + "_oval.xml" if export_as == "scap": output = output + ".xml" oval_definition = str() oval_test = str() oval_object = str() oval_state = str() oval_variable = str() xccdf_profiles = str() total_scap = str() scap_groups = str() xccdf_rules = str() x = 1 d = 1 ovalPrefix = '''<?xml version="1.0" encoding="UTF-8"?> <oval_definitions xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5" xmlns:oval="http://oval.mitre.org/XMLSchema/oval-common-5" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://oval.mitre.org/XMLSchema/oval-definitions-5 https://raw.githubusercontent.com/OVAL-Community/OVAL/master/oval-schemas/oval-definitions-schema.xsd http://oval.mitre.org/XMLSchema/oval-definitions-5#independent https://raw.githubusercontent.com/OVAL-Community/OVAL/master/oval-schemas/independent-definitions-schema.xsd http://oval.mitre.org/XMLSchema/oval-definitions-5#macos https://raw.githubusercontent.com/OVAL-Community/OVAL/master/oval-schemas/macos-definitions-schema.xsd http://oval.mitre.org/XMLSchema/oval-definitions-5#unix https://raw.githubusercontent.com/OVAL-Community/OVAL/master/oval-schemas/unix-definitions-schema.xsd"> <generator> <oval:schema_version>5.12.1</oval:schema_version> <oval:timestamp xmlns:oval="http://oval.mitre.org/XMLSchema/oval-common-5">{0}</oval:timestamp> <terms_of_use>Copyright (c) {1}, NIST.</terms_of_use> <oval:product_name xmlns:oval="http://oval.mitre.org/XMLSchema/oval-common-5">macOS Security Compliance Project</oval:product_name> </generator>'''.format(date_time_string, year) ostype = "macOS" if "ios" in version_yaml['cpe'] or "visionos" in version_yaml['cpe']: ostype = "iOS/iPadOS" if "visionos" in version_yaml['cpe']: ostype = "visionOS" xccdfPrefix = '''<?xml version="1.0" encoding="UTF-8"?> <Benchmark xmlns="http://checklists.nist.gov/xccdf/1.2" id="xccdf_gov.nist.mscp.content_benchmark_macOS_{1}" style="SCAP_1.4" resolved="true" xml:lang="en"> <status date="{3}">draft</status> <title>{4} {1}: Security Configuration {4} {1}: Security Configuration Security Content Automation Protocol National Institute of Standards and Technology {2} National Institute of Standards and Technology National Institute of Standards and Technology https://github.com/usnistgov/macos_security/releases/latest Bob Gendler - National Institute of Standards and Technology Dan Brodjieski - National Aeronautics and Space Administration Allen Golbig - Jamf '''.format(date_time_string, version_yaml['os'], version_yaml['version'],date_time_string.split("T")[0] + "Z", ostype) scapPrefix = ''' draft macOS {1}: Security Configuration macOS {1}: Security Configuration Security Content Automation Protocol National Institute of Standards and Technology platform-cpe-dictionary platform-cpe-oval {3} National Institute of Standards and Technology National Institute of Standards and Technology https://github.com/usnistgov/macos_security/releases/latest Bob Gendler - National Institute of Standards and Technology Dan Brodjieski - National Aeronautics and Space Administration Allen Golbig - Jamf '''.format(date_time_string, version_yaml['os'], version_yaml['cpe'], version_yaml['version'],date_time_string.split("T")[0] + "Z") generated_baselines = {} for rule in all_rules: if glob.glob('../custom/rules/**/{}.yaml'.format(rule),recursive=True): rule_file = glob.glob('../custom/rules/**/{}.yaml'.format(rule),recursive=True)[0] custom=True elif glob.glob('../rules/*/{}.yaml'.format(rule)): rule_file = glob.glob('../rules/*/{}.yaml'.format(rule))[0] custom=False odv_label = str() og_rule_yaml = get_rule_yaml(rule_file, custom) loop = 1 if "odv" in og_rule_yaml: loop = len(og_rule_yaml['odv']) if args.baseline != "None": loop = 1 for a in range(0, loop): rule_yaml = get_rule_yaml(rule_file, custom) try: odv_keys = list(rule_yaml['odv'].keys()) if args.baseline != "None": if args.baseline in odv_keys: odv_label = args.baseline else: odv_label = "recommended" else: odv_label = odv_keys[a] odv_value = str(rule_yaml['odv'][odv_label]) rule_yaml['title'] = rule_yaml['title'].replace("$ODV",str(odv_value)) rule_yaml['discussion'] = rule_yaml['discussion'].replace("$ODV",odv_value) rule_yaml['check'] = rule_yaml['check'].replace("$ODV",odv_value) rule_yaml['fix'] = rule_yaml['fix'].replace("$ODV",odv_value) if "result" in rule_yaml: for result_value in rule_yaml['result']: if "$ODV" == rule_yaml['result'][result_value]: rule_yaml['result'][result_value] = rule_yaml['result'][result_value].replace("$ODV",odv_value) if rule_yaml['mobileconfig_info']: for mobileconfig_type in rule_yaml['mobileconfig_info']: if isinstance(rule_yaml['mobileconfig_info'][mobileconfig_type], dict): for mobileconfig_value in rule_yaml['mobileconfig_info'][mobileconfig_type]: if "$ODV" in str(resulting_yaml['mobileconfig_info'][mobileconfig_type][mobileconfig_value]): if type(resulting_yaml['mobileconfig_info'][mobileconfig_type][mobileconfig_value]) == dict: for k,v in resulting_yaml['mobileconfig_info'][mobileconfig_type][mobileconfig_value].items(): if v == "$ODV": resulting_yaml['mobileconfig_info'][mobileconfig_type][mobileconfig_value][k] = odv_value else: resulting_yaml['mobileconfig_info'][mobileconfig_type][mobileconfig_value] = odv_value except: odv_label = "recommended" if args.disastig and args.oval: rule_yaml['title'] = disa_stig_rules(rule_yaml['references']['disa_stig'][0], stig) for baseline in all_baselines: found_rules = [] for tag in rule_yaml['tags']: if tag == baseline: if odv_label != "recommended" and odv_label == tag or odv_label == "custom": if baseline in generated_baselines: generated_baselines[baseline].append(rule_yaml['id'] + "_" + odv_label) else: generated_baselines[baseline] = [rule_yaml['id'] + "_" + odv_label] continue elif odv_label == "recommended" or odv_label == "custom": if "odv" in rule_yaml: if baseline not in rule_yaml['odv']: if baseline in generated_baselines: generated_baselines[baseline].append(rule_yaml['id'] + "_" + odv_label) else: generated_baselines[baseline] = [rule_yaml['id'] + "_" + odv_label] else: if baseline in generated_baselines: generated_baselines[baseline].append(rule_yaml['id'] + "_" + odv_label) else: generated_baselines[baseline] = [rule_yaml['id'] + "_" + odv_label] if odv_label == "hint": continue result = str() if "result" in rule_yaml: result = "\nResult: {}".format(rule_yaml['result']) else: result = "" severity = str() if severity in rule_yaml: if isinstance(rule_yaml["severity"], str): severity = f'{rule_yaml["severity"]}' if isinstance(rule_yaml["severity"], dict): try: severity = f'{rule_yaml["severity"][args.baseline]}' except KeyError: severity = "unknown" else: severity = "unknown" check_rule = str() if "inherent" in rule_yaml['tags'] or "n_a" in rule_yaml['tags'] or "permanent" in rule_yaml['tags']: check_rule = ''' ''' else: check_rule = ''' '''.format(x) references = str() if "800-53r5" in rule_yaml['references'] and rule_yaml['references']['800-53r5'][0] != "N/A": references = references + "NIST SP 800-53r5: " for nist80053 in rule_yaml['references']['800-53r5']: references = references + nist80053 + ", " references = references[:-2] + "" if "800-53r4" in rule_yaml['references'] and rule_yaml['references']['800-53r4'][0] != "N/A": references = references + "NIST SP 800-53r4: " for nist80053 in rule_yaml['references']['800-53r4']: references = references + nist80053 + ", " references = references[:-2] + "" if "800-171r3" in rule_yaml['references'] and rule_yaml['references']['800-171r3'][0] != "N/A": references = references + "NIST SP 800-171r3: " for nist800171 in rule_yaml['references']['800-171r3']: references = references + nist800171 + ", " references = references[:-2] + "" if "disa_stig" in rule_yaml['references'] and rule_yaml['references']['disa_stig'][0] != "N/A": references = references + "DISA STIG(s): " for disa_stig in rule_yaml['references']['disa_stig']: references = references + disa_stig + ", " references = references[:-2] + "" if "cis" in rule_yaml['references']: if "benchmark" in rule_yaml['references']['cis'] and rule_yaml['references']['cis']['benchmark'][0] != "N/A": references = references + "CIS Benchmark: " for benchmark in rule_yaml['references']['cis']['benchmark']: references = references + benchmark + ", " references = references[:-2] + "" if "controls v8" in rule_yaml['references']['cis'] and rule_yaml['references']['cis']['controls v8'][0] != "N/A": references = references + "CIS Controls V8: " for v8controls in rule_yaml['references']['cis']['controls v8']: references = references + str(v8controls) + ", " references = references[:-2] + "" for k,v in rule_yaml['references'].items(): if k == "cci" or k == "srg": continue if k == "custom": for i,u in rule_yaml['references']['custom'].items(): references = references + '{0}: '.format(i) for refs in rule_yaml['references']['custom'][i]: references = references + '{0}, '.format(str(refs)) references = references[:-2] + "" cce = str() if "cce" not in rule_yaml['references'] or rule_yaml['references']['cce'] == "N/A": cce = "CCE-11111-1" else: cce = rule_yaml['references']['cce'][0] if export_as == "scap": mobileconfig_info = "" if rule_yaml['mobileconfig']: mobileconfig_info = escape(format_mobileconfig_fix(rule_yaml['mobileconfig_info'])) xccdf_rules = xccdf_rules + ''' {2} {3} {4} {5}{9} {6} {7} {8} '''.format(rule_yaml['id'] + "_" + odv_label, severity, rule_yaml['title'], escape(rule_yaml['discussion']).rstrip(), escape(rule_yaml['check']).rstrip(), result, cce,escape(rule_yaml['fix']) + "\n" + mobileconfig_info, check_rule, references) if export_as == "xccdf": mobileconfig_info = "" if rule_yaml['mobileconfig']: mobileconfig_info = escape(format_mobileconfig_fix(rule_yaml['mobileconfig_info'])) xccdf_rules = xccdf_rules + ''' {2} {3} {4} {5}{8} {6} {7} '''.format(rule_yaml['id'] + "_" + odv_label, severity, rule_yaml['title'], escape(rule_yaml['discussion']).rstrip(), escape(rule_yaml['check']).rstrip(), result, cce,escape(rule_yaml['fix']) + "\n" + mobileconfig_info, references) continue if "inherent" in rule_yaml['tags'] or "n_a" in rule_yaml['tags'] or "permanent" in rule_yaml['tags']: xccdf_rules = replace_ocil(xccdf_rules,x) x += 1 continue if "manual" in rule_yaml['tags']: print(rule_yaml['id'] + " - Manual Check") xccdf_rules = replace_ocil(xccdf_rules,x) x += 1 continue else: check_result = str() for k,v in rule_yaml['result'].items(): check_result = v count_found = False if " 2> /dev/null" in rule_yaml['check']: rule_yaml['check'] = rule_yaml['check'].replace(" 2> /dev/null","") check_existance = "all_exist" if "/usr/bin/grep -c" in rule_yaml['check']: if "echo \"1\"" not in rule_yaml['check'] or "echo \"0\"" not in rule_yaml['check']: if "/usr/bin/ssh -G ." not in rule_yaml['check']: if "auditd_enabled" not in rule_yaml['id']: if "/usr/sbin/sshd -G" not in rule_yaml['check']: rule_yaml['check'] = rule_yaml['check'].replace("/usr/bin/grep -c ", "/usr/bin/grep ") count_found = True if check_result == 0: check_existance = "none_exist" if "launchctl list" in rule_yaml['check']: rule_yaml['check'] = rule_yaml['check'].replace("launchctl list", "launchctl print system") if "auditd_enabled" in rule_yaml['id']: rule_yaml['check'] = rule_yaml['check'].replace("/usr/bin/grep -c com.apple.auditd", "/usr/bin/grep -c '\"com.apple.auditd\" => enabled'") if "/usr/bin/wc -l" in rule_yaml['check']: new_test = [] for command in rule_yaml['check'].split("|"): if "/usr/bin/wc -l" in command: break new_test.append(command.strip()) count_found = True rule_yaml['check'] = "|".join(new_test) if check_result == 0: check_existance = "none_exist" oval_definition = oval_definition + ''' {1} {4} '''.format(x,rule_yaml['title'],cce,rule_yaml['id'] + "_" + odv_label,escape(rule_yaml['discussion']).rstrip(),x) if "$CURRENT_USER" in rule_yaml['check']: rule_yaml['check'] = '''CURRENT_USER=$(/usr/bin/defaults read /Library/Preferences/com.apple.loginwindow.plist lastUserName) {}'''.format(rule_yaml['check']) oval_test = oval_test + ''' '''.format(x,rule_yaml['id'] + "_" + odv_label) oval_object = oval_object + ''' zsh {2} '''.format(x,rule_yaml['id'] + "_" + odv_label,escape(rule_yaml['check']).rstrip()) if count_found: if check_existance != "none_exist": oval_state = oval_state + ''' .* '''.format(x,rule_yaml['id'] + "_" + odv_label) else: oval_state = oval_state + ''' '''.format(x,rule_yaml['id'] + "_" + odv_label) else: oval_state = oval_state + ''' {2} '''.format(x,rule_yaml['id'] + "_" + odv_label,check_result) x += 1 check_existance = "all_exist" continue x += 1 for k in generated_baselines.keys(): xccdf_profiles = xccdf_profiles + ''' {0} This profile selects all rules tagged as {0}.'''.format(k, k.replace(" ","_")) for v in generated_baselines[k]: xccdf_profiles = xccdf_profiles + '''