#!/usr/bin/env python3 # filename: generate_guidance.py # description: Process a given baseline, and output guidance files import types import sys import os.path import plistlib import xlwt import io import glob import os import yaml import re import argparse import subprocess from xlwt import Workbook from string import Template from itertools import groupby from uuid import uuid4 from collections import namedtuple class MacSecurityRule(): def __init__(self, title, rule_id, severity, discussion, check, fix, cci, cce, nist_controls, disa_stig, srg, tags, result_value, mobileconfig, mobileconfig_info): self.rule_title = title self.rule_id = rule_id self.rule_severity = severity self.rule_discussion = discussion self.rule_check = check self.rule_fix = fix self.rule_cci = cci self.rule_cce = cce self.rule_80053r4 = nist_controls self.rule_disa_stig = disa_stig self.rule_srg = srg self.rule_result_value = result_value self.rule_tags = tags self.rule_mobileconfig = mobileconfig self.rule_mobileconfig_info = mobileconfig_info def create_asciidoc(self, adoc_rule_template): """Pass an AsciiDoc template as file object to return formatted AsciiDOC""" rule_adoc = "" rule_adoc = adoc_rule_template.substitute( rule_title=self.rule_title, rule_id=self.rule_id, rule_severity=self.rule_severity, rule_discussion=self.rule_discussion, rule_check=self.rule_check, rule_fix=self.rule_fix, rule_cci=self.rule_cci, rule_80053r4=self.rule_80053r4, rule_disa_stig=self.rule_disa_stig, rule_srg=self.rule_srg, rule_result=self.rule_result_value ) return rule_adoc def create_mobileconfig(self): pass # Convert a list to AsciiDoc def ulify(elements): string = "\n" for s in elements: string += "* " + str(s) + "\n" return string def group_ulify(elements): string = "\n * " for s in elements: string += str(s) + ", " return string[:-2] def group_ulify_comment(elements): string = "\n * " for s in elements: string += str(s) + ", " return string[:-2] def get_check_code(check_yaml): try: check_string = check_yaml.split("[source,bash]")[1] except: return check_yaml #print check_string check_code = re.search('(?:----((?:.*?\r?\n?)*)----)+', check_string) #print(check_code.group(1).rstrip()) return(check_code.group(1).strip()) def quotify(fix_code): string = fix_code.replace("'", "\'\"\'\"\'") string = string.replace("%", "%%") return string def get_fix_code(fix_yaml): fix_string = fix_yaml.split("[source,bash]")[1] fix_code = re.search('(?:----((?:.*?\r?\n?)*)----)+', fix_string) return(fix_code.group(1)) 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 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") rulefix = rulefix + "----\n\n" return rulefix class AdocTemplate: def __init__(self, name, path, template_file): self.name = name self.path = path self.template_file = template_file class PayloadDict: """Class to create and manipulate Configuration Profiles. The actual plist content can be accessed as a dictionary via the 'data' attribute. """ def __init__(self, identifier, uuid=False, removal_allowed=False, description='', organization='', displayname=''): self.data = {} self.data['PayloadVersion'] = 1 self.data['PayloadOrganization'] = organization if uuid: self.data['PayloadUUID'] = uuid else: self.data['PayloadUUID'] = makeNewUUID() if removal_allowed: self.data['PayloadRemovalDisallowed'] = False else: self.data['PayloadRemovalDisallowed'] = True self.data['PayloadType'] = 'Configuration' self.data['PayloadScope'] = 'System' self.data['PayloadDescription'] = description self.data['PayloadDisplayName'] = displayname self.data['PayloadIdentifier'] = identifier self.data['ConsentText'] = {"default": "THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE. IN NO EVENT SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM, OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY, CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER."} # An empty list for 'sub payloads' that we'll fill later self.data['PayloadContent'] = [] def _updatePayload(self, payload_content_dict, baseline_name): """Update the profile with the payload settings. Takes the settings dictionary which will be the PayloadContent dict within the payload. Handles the boilerplate, naming and descriptive elements. """ #description = "Configuration settings for the {} preference domain.".format(payload_type) payload_dict = {} # Boilerplate payload_dict['PayloadVersion'] = 1 payload_dict['PayloadUUID'] = makeNewUUID() payload_dict['PayloadEnabled'] = True payload_dict['PayloadType'] = payload_content_dict['PayloadType'] payload_dict['PayloadIdentifier'] = f"alacarte.macOS.{baseline_name}.{payload_dict['PayloadUUID']}" payload_dict['PayloadContent'] = payload_content_dict # Add the payload to the profile self.data.update(payload_dict) def _addPayload(self, payload_content_dict, baseline_name): """Add a payload to the profile. Takes the settings dictionary which will be the PayloadContent dict within the payload. Handles the boilerplate, naming and descriptive elements. """ #description = "Configuration settings for the {} preference domain.".format(payload_type) payload_dict = {} # Boilerplate payload_dict['PayloadVersion'] = 1 payload_dict['PayloadUUID'] = makeNewUUID() payload_dict['PayloadEnabled'] = True payload_dict['PayloadType'] = payload_content_dict['PayloadType'] payload_dict['PayloadIdentifier'] = f"alacarte.macOS.{baseline_name}.{payload_dict['PayloadUUID']}" payload_dict['PayloadContent'] = payload_content_dict # Add the payload to the profile #print payload_dict del payload_dict['PayloadContent']['PayloadType'] self.data['PayloadContent'].append(payload_dict) def addNewPayload(self, payload_type, settings, baseline_name): """Add a payload to the profile. Takes the settings dictionary which will be the PayloadContent dict within the payload. Handles the boilerplate, naming and descriptive elements. """ #description = "Configuration settings for the {} preference domain.".format(payload_type) payload_dict = {} # Boilerplate payload_dict['PayloadVersion'] = 1 payload_dict['PayloadUUID'] = makeNewUUID() payload_dict['PayloadEnabled'] = True payload_dict['PayloadType'] = payload_type payload_dict['PayloadIdentifier'] = f"alacarte.macOS.{baseline_name}.{payload_dict['PayloadUUID']}" # Add the settings to the payload for setting in settings: for k, v in setting.items(): payload_dict[k] = v # Add the payload to the profile # self.data['PayloadContent'].append(payload_dict) def addMCXPayload(self, settings, baseline_name): """Add a payload to the profile. Takes the settings dictionary which will be the PayloadContent dict within the payload. Handles the boilerplate, naming and descriptive elements. """ keys = settings[1] plist_dict = {} for key in keys.split(): plist_dict[key] = settings[2] #description = "Configuration settings for the {} preference domain.".format(payload_type) payload_dict = {} state = "Forced" domain = settings[0] # Boilerplate payload_dict[domain] = {} payload_dict[domain][state] = [] payload_dict[domain][state].append({}) payload_dict[domain][state][0]['mcx_preference_settings'] = plist_dict payload_dict['PayloadType'] = "com.apple.ManagedClient.preferences" self._addPayload(payload_dict, baseline_name) def finalizeAndSave(self, output_path): """Perform last modifications and save to an output plist. """ plistlib.dump(self.data, output_path) print(f"Configuration profile written to {output_path.name}") def makeNewUUID(): return str(uuid4()) def concatenate_payload_settings(settings): """Takes a list of dictionaries, removed duplicate entries and concatenates an array of settings for the same key """ settings_list = [] settings_dict = {} for item in settings: for key, value in item.items(): if isinstance(value, list): settings_dict.setdefault(key, []).append(value[0]) else: settings_dict.setdefault(key, value) if item not in settings_list: settings_list.append(item) return [settings_dict] def generate_profiles(baseline_name, build_path, parent_dir, baseline_yaml): """Generate the configuration profiles for the rules in the provided baseline YAML file """ organization = "macOS Security Compliance Project" displayname = f"macOS {baseline_name} Baseline settings" # import profile_manifests.plist manifests_file = os.path.join( parent_dir, 'includes', 'supported_payloads.yaml') with open(manifests_file) as r: manifests = yaml.load(r, Loader=yaml.SafeLoader) # Output folder mobileconfig_output_path = os.path.join( f'{build_path}', 'mobileconfigs') if not (os.path.isdir(mobileconfig_output_path)): try: os.makedirs(mobileconfig_output_path) except OSError: print("Creation of the directory %s failed" % mobileconfig_output_path) # setup lists and dictionaries profile_errors = [] profile_types = {} for sections in baseline_yaml['profile']: for profile_rule in sections['rules']: for rule in glob.glob('../rules/*/{}.yaml'.format(profile_rule)): rule_yaml = get_rule_yaml(rule) if rule_yaml['mobileconfig']: for payload_type, info in rule_yaml['mobileconfig_info'].items(): try: if payload_type not in manifests['payloads_types']: profile_errors.append(rule) raise ValueError( "{}: Payload Type is not supported".format(payload_type)) else: pass except (KeyError, ValueError) as e: profile_errors.append(rule) #print(e) pass try: if isinstance(info, list): raise ValueError( "Payload key is non-conforming") else: pass except (KeyError, ValueError) as e: profile_errors.append(rule) #print(e) pass if payload_type == "com.apple.ManagedClient.preferences": for payload_domain, settings in info.items(): for key, value in settings.items(): payload_settings = ( payload_domain, key, value) profile_types.setdefault( payload_type, []).append(payload_settings) else: for profile_key, key_value in info.items(): payload_settings = {profile_key: key_value} profile_types.setdefault( payload_type, []).append(payload_settings) if len(profile_errors) > 0: print("There are errors in the following files, please correct the .yaml file(s)!") for error in profile_errors: print(error) # process the payloads from the yaml file and generate new config profile for each type for payload, settings in profile_types.items(): mobileconfig_file_path = os.path.join( mobileconfig_output_path, payload + '.mobileconfig') identifier = payload + f".{baseline_name}" description = "Configuration settings for the {} preference domain.".format( payload) newProfile = PayloadDict(identifier=identifier, uuid=False, removal_allowed=False, organization=organization, displayname=displayname, description=description) config_file = open(mobileconfig_file_path, "wb") if payload == "com.apple.ManagedClient.preferences": for item in settings: newProfile.addMCXPayload(item, baseline_name) # handle these payloads for array settings elif (payload == "com.apple.applicationaccess.new") or (payload == 'com.apple.systempreferences'): newProfile.addNewPayload( payload, concatenate_payload_settings(settings), baseline_name) else: newProfile.addNewPayload(payload, settings, baseline_name) newProfile.finalizeAndSave(config_file) config_file.close() print(f""" CAUTION: These configuration profiles are intended for evaluation in a TEST environment. Certain configuration profiles (Smartcards), when applied could leave a system in a state where a user can no longer login with a password. Please use caution when applying configuration settings to a system. NOTE: If an MDM is already being leveraged, many of these profile settings may be available through the vendor. """) def generate_script(baseline_name, build_path, baseline_yaml): """Generates the zsh script from the rules in the baseline YAML """ compliance_script_file = open( build_path + '/' + baseline_name + '_compliance.sh', 'w') check_function_string = "" fix_function_string = "" # create header of fix zsh script check_zsh_header = f"""#!/bin/zsh ## This script will attempt to audit all of the settings based on the installed profile. ## This script is provided as-is and should be fully tested on a system that is not in a production environment. ################### COMMANDS START BELOW THIS LINE ################### ## Must be run as root if [[ $EUID -ne 0 ]]; then echo "This script must be run as root" exit 1 fi # get the currently logged in user CURRENT_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ && ! /loginwindow/ {{ print $3 }}') # configure colors for text RED='\e[31m' STD='\033[0;0;39m' GREEN='\e[32m' YELLOW='\e[33m' # setup files audit_plist="/Library/Preferences/org.{baseline_name}.audit.plist" audit_log="/Library/Logs/{baseline_name}_baseline.log" lastComplianceScan=$(defaults read /Library/Preferences/org.{baseline_name}.audit.plist lastComplianceCheck) if [[ $lastComplianceScan == "" ]];then lastComplianceScan="No scans have been run" fi # pause function pause(){{ vared -p "Press [Enter] key to continue..." -c fackEnterKey }} ask() {{ while true; do if [ "${{2:-}}" = "Y" ]; then prompt="Y/n" default=Y elif [ "${{2:-}}" = "N" ]; then prompt="y/N" default=N else prompt="y/n" default= fi # Ask the question - use /dev/tty in case stdin is redirected from somewhere else printf "${{YELLOW}} $1 [$prompt] ${{STD}}" read REPLY # Default? if [ -z "$REPLY" ]; then REPLY=$default fi # Check if the reply is valid case "$REPLY" in Y*|y*) return 0 ;; N*|n*) return 1 ;; esac done }} # function to display menus show_menus() {{ clear echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" echo " M A I N - M E N U" echo " macOS Security Compliance Tool" echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" echo "Last compliance scan: $lastComplianceScan\n" echo "1. View Last Compliance Report" echo "2. Run New Compliance Scan" echo "3. Run Commands to remediate non-compliant settings" echo "4. Exit" }} # function to read options read_options(){{ local choice vared -p "Enter choice [ 1 - 4 ] " -c choice case $choice in 1) view_report ;; 2) run_scan ;; 3) run_fix ;; 4) exit 0;; *) echo -e "${{RED}}Error: please choose an option 1-4...${{STD}}" && sleep 1 esac }} generate_report(){{ non_compliant=0 compliant=0 results=$(/usr/libexec/PlistBuddy -c "Print" /Library/Preferences/org.{baseline_name}.audit.plist) while IFS= read -r line; do if [[ "$line" =~ "true" ]]; then non_compliant=$((non_compliant+1)) fi if [[ "$line" =~ "false" ]]; then compliant=$((compliant+1)) fi done <<< "$results" total=$((non_compliant + compliant)) percentage=$(printf %.2f $(( compliant * 100. / total )) ) echo echo "Number of tests passed: ${{GREEN}}$compliant${{STD}}" echo "Number of test FAILED: ${{RED}}$non_compliant${{STD}}" echo "You are ${{YELLOW}}$percentage%${{STD}} percent compliant!" pause }} view_report(){{ if [[ $lastComplianceScan == "No scans have been run" ]];then echo "no report to run, please run new scan" pause else generate_report fi }} run_scan(){{ # append to existing logfile echo "$(date -u) Beginning {baseline_name} baseline scan" >> "$audit_log" # write timestamp of last compliance check defaults write "$audit_plist" lastComplianceCheck "$(date)" """ #compliance_script_file.write(check_zsh_header) # Read all rules in the section and output the check functions for sections in baseline_yaml['profile']: for profile_rule in sections['rules']: for rule in glob.glob('../rules/*/{}.yaml'.format(profile_rule)): rule_yaml = get_rule_yaml(rule) if rule_yaml['id'].startswith("supplemental"): continue if "manual" in rule_yaml['tags']: continue # grab the 800-53 controls try: rule_yaml['references']['800-53r4'] except KeyError: nist_80053r4 = 'N/A' else: nist_80053r4 = rule_yaml['references']['800-53r4'] # group the controls nist_80053r4.sort() res = [list(i) for j, i in groupby( nist_80053r4, lambda a: a.split('(')[0])] nist_controls = '' for i in res: nist_controls += group_ulify(i) # print checks and result try: check = rule_yaml['check'] except KeyError: print("no check found for {}".format(rule_yaml['id'])) continue try: result = rule_yaml['result'] except KeyError: #print("no result found for {}".format(rule_yaml['id'])) continue if "integer" in result: result_value = result['integer'] elif "boolean" in result: result_value = result['boolean'] elif "string" in result: result_value = result['string'] else: continue # write the checks zsh_check_text = """ #####----- Rule: {0} -----##### ## Addresses the following NIST 800-53 controls: {1} echo 'Running the command to check the settings for: {0} ...' | tee -a "$audit_log" result_value=$({2}) # expected result {3} if [[ $result_value == "{4}" ]]; then echo "{0} passed..." | tee -a "$audit_log" defaults write "$audit_plist" {0} -bool NO else echo "{0} FAILED..." | tee -a "$audit_log" defaults write "$audit_plist" {0} -bool YES fi """.format(rule_yaml['id'], nist_controls.replace("\n", "\n#"), check.strip(), result, result_value) check_function_string = check_function_string + zsh_check_text # print fix and result try: rule_yaml['fix'] except KeyError: fix_text = 'N/A' else: fix_text = rule_yaml['fix'] or ["n/a"] # write the fixes if "[source,bash]" in fix_text: nist_controls_commented = nist_controls.replace('\n', '\n#') zsh_fix_text = f""" #####----- Rule: {rule_yaml['id']} -----##### ## Addresses the following NIST 800-53 controls: {nist_controls_commented} {rule_yaml['id']}_audit_score=$(defaults read $audit_plist {rule_yaml['id']}) if [[ ${rule_yaml['id']}_audit_score == 1 ]]; then ask '{rule_yaml['id']} - Run the command(s)-> {quotify(get_fix_code(rule_yaml['fix']).strip())} ' N if [[ $? == 0 ]]; then echo 'Running the command to configure the settings for: {rule_yaml['id']} ...' | tee -a "$audit_log" {get_fix_code(rule_yaml['fix']).strip()} fi else echo 'Settings for: {rule_yaml['id']} already configured, continuing...' | tee -a "$audit_log" fi """ fix_function_string = fix_function_string + zsh_fix_text # write the footer for the check functions zsh_check_footer = """ lastComplianceScan=$(defaults read "$audit_plist" lastComplianceCheck) echo "Results written to $audit_plist" pause } run_fix(){ if [[ ! -e "$audit_plist" ]]; then echo "Audit plist doesn't exist, please run Audit Check First" | tee -a "$audit_log" pause show_menus read_options fi ask 'THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY OF ANY KIND, EITHER EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE. IN NO EVENT SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM, OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY, CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER. WOULD YOU LIKE TO CONTINUE? ' N if [[ $? != 0 ]]; then show_menus read_options fi # append to existing logfile echo "$(date -u) Beginning FISMA fixes" >> "$audit_log" """ # write the footer for the script zsh_fix_footer = """ } while true; do show_menus read_options done """ #write out the compliance script compliance_script_file.write(check_zsh_header) compliance_script_file.write(check_function_string) compliance_script_file.write(zsh_check_footer) compliance_script_file.write(fix_function_string) compliance_script_file.write(zsh_fix_footer) print(f"Finished building {compliance_script_file.name}") # make the compliance script executable os.chmod(compliance_script_file.name, 0o755) #fix_script_file.close() compliance_script_file.close() def get_rule_yaml(rule_file): """ Takes a rule file, checks for a custom version, and returns the yaml for the rule """ if os.path.basename(rule_file) in glob.glob1('../custom/rules/', '*.yaml'): #print(f"Custom settings found for rule: {rule_file}") override_rule = os.path.join( '../custom/rules', os.path.basename(rule_file)) with open(override_rule) as r: rule_yaml = yaml.load(r, Loader=yaml.SafeLoader) else: with open(rule_file) as r: rule_yaml = yaml.load(r, Loader=yaml.SafeLoader) return rule_yaml def generate_xls(baseline_name, build_path, baseline_yaml): """Using the baseline yaml file, create an XLS document containing the YAML fields """ baseline_rules = create_rules(baseline_yaml) # File path setup file_dir = os.path.dirname(os.path.abspath(__file__)) parent_dir = os.path.dirname(file_dir) # Output files xls_output_file = f"{build_path}/{baseline_name}.xls" wb = Workbook() sheet1 = wb.add_sheet('Sheet 1') topWrap = xlwt.easyxf("align: vert top; alignment: wrap True") top = xlwt.easyxf("align: vert top") headers = xlwt.easyxf("font: bold on") counter = 1 sheet1.write(0, 0, "CCE", headers) sheet1.write(0, 1, "Rule ID", headers) sheet1.write(0, 2, "Title", headers) sheet1.write(0, 3, "Discussion", headers) sheet1.write(0, 4, "Mechanism", headers) sheet1.write(0, 5, "Check", headers) sheet1.write(0, 6, "Check Result", headers) sheet1.write(0, 7, "Fix", headers) sheet1.write(0, 8, "800-53r4", headers) sheet1.write(0, 9, "SRG", headers) sheet1.write(0, 10, "DISA STIG", headers) sheet1.write(0, 11, "CCI", headers) sheet1.set_panes_frozen(True) sheet1.set_horz_split_pos(1) sheet1.set_vert_split_pos(2) for rule in baseline_rules: if rule.rule_id.startswith("supplemental") or rule.rule_id.startswith("srg"): continue sheet1.write(counter, 0, rule.rule_cce, top) sheet1.col(0).width = 256 * 15 sheet1.write(counter, 1, rule.rule_id, top) sheet1.col(1).width = 512 * 25 sheet1.write(counter, 2, rule.rule_title, top) sheet1.col(2).width = 600 * 30 sheet1.write(counter, 3, str(rule.rule_discussion), topWrap) sheet1.col(3).width = 700 * 35 mechanism = "Manual" if "[source,bash]" in rule.rule_fix: mechanism = "Script" if "This is implemented by a Configuration Profile." in rule.rule_fix: mechanism = "Configuration Profile" if "inherent" in rule.rule_tags: mechanism = "The control cannot be configured out of compliance." if "permanent" in rule.rule_tags: mechanism = "The control is not able to be configure to meet the requirement. It is recommended to implement a third-party solution to meet the control." if "not_applicable" in rule.rule_tags: mechanism = " The control is not applicable when configuring a macOS system." sheet1.write(counter, 4, mechanism, top) sheet1.col(4).width = 256 * 25 sheet1.write(counter, 5, rule.rule_check, topWrap) sheet1.col(5).width = 750 * 50 sheet1.write(counter, 6, str(rule.rule_result_value), topWrap) sheet1.col(6).width = 256 * 25 if rule.rule_mobileconfig: sheet1.write(counter, 7, format_mobileconfig_fix( rule.rule_mobileconfig_info), topWrap) #print(format_mobileconfig_fix(rule.rule_mobileconfig_info)) # sheet1.write(counter, 7, str( # configProfile(rule_file)), topWrap) else: sheet1.write(counter, 7, str(rule.rule_fix), topWrap) sheet1.col(7).width = 1000 * 50 baseline_refs = ( str(rule.rule_80053r4)).strip('[]\'') baseline_refs = baseline_refs.replace(", ", "\n").replace("\'", "") sheet1.write(counter, 8, baseline_refs, topWrap) sheet1.col(8).width = 256 * 15 srg_refs = (str(rule.rule_srg)).strip('[]\'') srg_refs = srg_refs.replace(", ", "\n").replace("\'", "") sheet1.write(counter, 9, srg_refs, topWrap) sheet1.col(9).width = 500 * 15 disa_refs = (str(rule.rule_disa_stig)).strip('[]\'') disa_refs = srg_refs.replace(", ", "\n").replace("\'", "") sheet1.write(counter, 10, disa_refs, topWrap) sheet1.col(10).width = 500 * 15 cci = (str(rule.rule_cci)).strip('[]\'') cci = cci.replace(", ", "\n").replace("\'", "") sheet1.write(counter, 11, cci, topWrap) sheet1.col(11).width = 400 * 15 tall_style = xlwt.easyxf('font:height 640;') # 36pt sheet1.row(counter).set_style(tall_style) counter = counter + 1 wb.save(xls_output_file) print(f"Finished building {xls_output_file}") def create_rules(baseline_yaml): """Takes a baseline yaml file and parses the rules, returns a list of containing rules """ all_rules = [] #expected keys and references keys = ['mobileconfig', 'macOS', 'severity', 'title', 'check', 'fix', 'tags', 'id', 'references', 'result', 'discussion'] references = ['disa_stig', 'cci', 'cce', '800-53r4', 'srg'] for sections in baseline_yaml['profile']: for profile_rule in sections['rules']: for rule in glob.glob('../rules/*/{}.yaml'.format(profile_rule)): rule_yaml = get_rule_yaml(rule) for key in keys: try: rule_yaml[key] except: #print "{} key missing ..for {}".format(key, rule) rule_yaml.update({key: "missing"}) if key == "references": for reference in references: try: rule_yaml[key][reference] except: #print "expected reference '{}' is missing in key '{}' for rule{}".format(reference, key, rule) rule_yaml[key].update({reference: ["None"]}) all_rules.append(MacSecurityRule(rule_yaml['title'].replace('|', '\|'), rule_yaml['id'].replace('|', '\|'), rule_yaml['severity'].replace('|', '\|'), rule_yaml['discussion'].replace('|', '\|'), rule_yaml['check'].replace('|', '\|'), rule_yaml['fix'].replace('|', '\|'), rule_yaml['references']['cci'], rule_yaml['references']['cce'], rule_yaml['references']['800-53r4'], rule_yaml['references']['disa_stig'], rule_yaml['references']['srg'], rule_yaml['tags'], rule_yaml['result'], rule_yaml['mobileconfig'], rule_yaml['mobileconfig_info'] )) return all_rules def create_args(): """configure the arguments used in the script, returns the parsed arguements """ parser = argparse.ArgumentParser( description='Given a baseline, create guidance documents and files.') parser.add_argument("baseline", default=None, help="Baseline YAML file used to create the guide.", type=argparse.FileType('rt')) parser.add_argument("-l", "--logo", default=None, help="Full path to logo file to be inlcuded in the guide.", action="store") parser.add_argument("-p", "--profiles", default=None, help="Generate configuration profiles for the rules.", action="store_true") parser.add_argument("-s", "--script", default=None, help="Generate the compliance script for the rules.", action="store_true") # add gary argument to include tags for XCCDF generation, with a nod to Gary the SCAP guru parser.add_argument("-g", "--gary", default=None, help=argparse.SUPPRESS, action="store_true") parser.add_argument("-x", "--xls", default=None, help="Generate the excel (xls) document for the rules.", action="store_true") return parser.parse_args() def is_asciidoctor_installed(): """Checks to see if the ruby gem for asciidoctor is installed """ cmd = "gem list asciidoctor -i" process = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) output, error = process.communicate() return process.returncode def is_asciidoctor_pdf_installed(): """Checks to see if the ruby gem for asciidoctor-pdf is installed """ cmd = "gem list asciidoctor-pdf -i" process = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) output, error = process.communicate() return process.returncode def main(): args = create_args() try: output_basename = os.path.basename(args.baseline.name) output_filename = os.path.splitext(output_basename)[0] baseline_name = os.path.splitext(output_basename)[0].capitalize() file_dir = os.path.dirname(os.path.abspath(__file__)) parent_dir = os.path.dirname(file_dir) # stash current working directory original_working_directory = os.getcwd() # switch to the scripts directory os.chdir(file_dir) if args.logo: logo = args.logo else: logo = "../../templates/images/nist.png" build_path = os.path.join(parent_dir, 'build', f'{baseline_name}') if not (os.path.isdir(build_path)): try: os.makedirs(build_path) except OSError: print(f"Creation of the directory {build_path} failed") adoc_output_file = open(f"{build_path}/{output_filename}.adoc", 'w') print('Profile YAML:', args.baseline.name) print('Output path:', adoc_output_file.name) except IOError as msg: parser.error(str(msg)) baseline_yaml = yaml.load(args.baseline, Loader=yaml.SafeLoader) adoc_templates = [ "adoc_rule", "adoc_supplemental", "adoc_rule_no_setting", "adoc_section", "adoc_header", "adoc_footer", "adoc_foreword", "adoc_authors", "adoc_acronyms", "adoc_additional_docs" ] adoc_templates_dict = {} for template in adoc_templates: # custom template exists if template + ".adoc" in glob.glob1('../custom/templates/', '*.adoc'): print(f"Custom template found for : {template}") adoc_templates_dict[template] = f"../custom/templates/{template}.adoc" else: adoc_templates_dict[template] = f"../templates/{template}.adoc" # Setup AsciiDoc templates with open(adoc_templates_dict['adoc_rule']) as adoc_rule_file: adoc_rule_template = Template(adoc_rule_file.read()) with open(adoc_templates_dict['adoc_supplemental']) as adoc_supplemental_file: adoc_supplemental_template = Template(adoc_supplemental_file.read()) with open(adoc_templates_dict['adoc_rule_no_setting']) as adoc_rule_no_setting_file: adoc_rule_no_setting_template = Template(adoc_rule_no_setting_file.read()) with open(adoc_templates_dict['adoc_section']) as adoc_section_file: adoc_section_template = Template(adoc_section_file.read()) with open(adoc_templates_dict['adoc_header']) as adoc_header_file: adoc_header_template = Template(adoc_header_file.read()) with open(adoc_templates_dict['adoc_footer']) as adoc_footer_file: adoc_footer_template = Template(adoc_footer_file.read()) with open(adoc_templates_dict['adoc_foreword']) as adoc_foreword_file: adoc_foreword_template = adoc_foreword_file.read() + "\n" with open(adoc_templates_dict['adoc_authors']) as adoc_authors_file: adoc_authors_template = adoc_authors_file.read() + "\n" with open(adoc_templates_dict['adoc_acronyms']) as adoc_acronyms_file: adoc_acronyms_template = adoc_acronyms_file.read() + "\n" with open(adoc_templates_dict['adoc_additional_docs']) as adoc_additional_docs_file: adoc_additional_docs_template = adoc_additional_docs_file.read() + "\n" # set tag attribute if args.gary: adoc_tag_show=":show_tags:" else: adoc_tag_show=":show_tags!:" if "STIG" in baseline_yaml['title']: adoc_STIG_show=":show_STIG:" adoc_SRG_show=":show_SRG:" else: adoc_STIG_show=":show_STIG!:" adoc_SRG_show=":show_SRG!:" adoc_171_show=":show_171:" # Create header header_adoc = adoc_header_template.substitute( profile_title=baseline_yaml['title'], description=baseline_yaml['description'], html_header_title=baseline_yaml['title'], html_title=baseline_yaml['title'].split(':')[0], html_subtitle=baseline_yaml['title'].split(':')[1], logo=logo, tag_attribute=adoc_tag_show, nist171_attribute=adoc_171_show, stig_attribute=adoc_STIG_show, srg_attribute=adoc_SRG_show, ) # Output header adoc_output_file.write(header_adoc) # write foreword, authors, acronyms, supporting docs adoc_output_file.write(adoc_foreword_template) adoc_output_file.write(adoc_authors_template) adoc_output_file.write(adoc_acronyms_template) adoc_output_file.write(adoc_additional_docs_template) # Create sections and rules for sections in baseline_yaml['profile']: section_yaml_file = sections['section'].lower() + '.yaml' #check for custom section if section_yaml_file in glob.glob1('../custom/sections/', '*.yaml'): print(f"Custom settings found for section: {sections['section']}") override_section = os.path.join( '../custom/sections', sections['section'] + '.yaml') with open(override_section) as r: section_yaml = yaml.load(r, Loader=yaml.SafeLoader) else: with open('../sections/' + sections['section'] + '.yaml') as s: section_yaml = yaml.load(s, Loader=yaml.SafeLoader) # Read section info and output it section_adoc = adoc_section_template.substitute( section_name=section_yaml['name'], description=section_yaml['description'] ) adoc_output_file.write(section_adoc) # Read all rules in the section and output them for rule in sections['rules']: # print(rule) rule_path = glob.glob('../rules/*/{}.yaml'.format(rule)) rule_file = (os.path.basename(rule_path[0])) #check for custom rule if rule_file in glob.glob1('../custom/rules/', '*.yaml'): print(f"Custom settings found for rule: {rule_file}") override_rule = os.path.join('../custom/rules', rule_file) with open(override_rule) as r: rule_yaml = yaml.load(r, Loader=yaml.SafeLoader) else: with open(rule_path[0]) as r: rule_yaml = yaml.load(r, Loader=yaml.SafeLoader) # Determine if the references exist and set accordingly try: rule_yaml['references']['cci'] except KeyError: cci = 'N/A' else: cci = ulify(rule_yaml['references']['cci']) try: rule_yaml['references']['cce'] except KeyError: cce = 'N/A' else: cce = ulify(rule_yaml['references']['cce']) try: rule_yaml['references']['800-53r4'] except KeyError: nist_80053r4 = 'N/A' else: #nist_80053r4 = ulify(rule_yaml['references']['800-53r4']) nist_80053r4 = rule_yaml['references']['800-53r4'] try: rule_yaml['references']['800-171r2'] except KeyError: nist_800171 = '• N/A' else: #nist_80053r4 = ulify(rule_yaml['references']['800-53r4']) nist_800171 = ulify(rule_yaml['references']['800-171r2']) try: rule_yaml['references']['disa_stig'] except KeyError: disa_stig = 'N/A' else: disa_stig = ulify(rule_yaml['references']['disa_stig']) try: rule_yaml['references']['srg'] except KeyError: srg = 'N/A' else: srg = ulify(rule_yaml['references']['srg']) try: rule_yaml['fix'] except KeyError: rulefix = "No fix Found" else: rulefix = rule_yaml['fix'] # .replace('|', '\|') try: rule_yaml['tags'] except KeyError: tags = 'none' else: tags = ulify(rule_yaml['tags']) try: result = rule_yaml['result'] except KeyError: result = 'N/A' if "integer" in result: result_value = result['integer'] result_type = "integer" elif "boolean" in result: result_value = result['boolean'] result_type = "boolean" elif "string" in result: result_value = result['string'] result_type = "string" else: result_value = 'N/A' # deteremine if configprofile try: rule_yaml['mobileconfig'] except KeyError: pass else: if rule_yaml['mobileconfig']: rulefix = format_mobileconfig_fix( rule_yaml['mobileconfig_info']) # process nist controls for grouping nist_80053r4.sort() res = [list(i) for j, i in groupby( nist_80053r4, lambda a: a.split('(')[0])] nist_controls = '' for i in res: nist_controls += group_ulify(i) if 'supplemental' in tags: rule_adoc = adoc_supplemental_template.substitute( rule_title=rule_yaml['title'].replace('|', '\|'), rule_id=rule_yaml['id'].replace('|', '\|'), rule_discussion=rule_yaml['discussion'], ) elif ('permanent' in tags) or ('inherent' in tags) or ('n_a' in tags): rule_adoc = adoc_rule_no_setting_template.substitute( rule_title=rule_yaml['title'].replace('|', '\|'), rule_id=rule_yaml['id'].replace('|', '\|'), rule_discussion=rule_yaml['discussion'].replace('|', '\|'), rule_check=rule_yaml['check'], # .replace('|', '\|'), rule_fix=rulefix, rule_80053r4=nist_controls, rule_800171=nist_800171, rule_disa_stig=disa_stig, rule_srg=srg ) else: rule_adoc = adoc_rule_template.substitute( rule_title=rule_yaml['title'].replace('|', '\|'), rule_id=rule_yaml['id'].replace('|', '\|'), rule_discussion=rule_yaml['discussion'].replace('|', '\|'), rule_check=rule_yaml['check'], # .replace('|', '\|'), rule_fix=rulefix, rule_cci=cci, rule_80053r4=nist_controls, rule_800171=nist_800171, rule_disa_stig=disa_stig, rule_cce=cce, rule_tags=tags, rule_srg=srg, rule_result=result_value ) adoc_output_file.write(rule_adoc) # Create footer footer_adoc = adoc_footer_template.substitute( ) # Output footer adoc_output_file.write(footer_adoc) if args.profiles: print("Generating configuration profiles...") generate_profiles(baseline_name, build_path, parent_dir, baseline_yaml) if args.script: print("Generating compliance script...") generate_script(baseline_name, build_path, baseline_yaml) if args.xls: print('Generating excel document...') generate_xls(baseline_name, build_path, baseline_yaml) if is_asciidoctor_installed() == 0: print('Generating HTML file from AsciiDoc...') cmd = f"/usr/local/bin/asciidoctor {adoc_output_file.name}" process = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) output, error = process.communicate() else: print("If you would like to generate the HTML file from the AsciiDoc file, install the ruby gem for asciidoctor") if is_asciidoctor_pdf_installed() == 0: print('Generating PDF file from AsciiDoc...') cmd = f"/usr/local/bin/asciidoctor-pdf {adoc_output_file.name}" process = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) output, error = process.communicate() else: print("If you would like to generate the PDF file from the AsciiDoc file, install the ruby gem for asciidoctor-pdf") # finally revert back to the prior directory os.chdir(original_working_directory) if __name__ == "__main__": main()