mirror of
https://github.com/usnistgov/macos_security.git
synced 2026-02-03 14:03:24 +00:00
feat[script]: add restore script generation
if a default_state is defined in a rule, then when generating the scripts, a new restore script will be included that has the code to restore the state for those defined rules.
This commit is contained in:
@@ -1,8 +1,41 @@
|
||||
#!/bin/zsh --no-rcs
|
||||
|
||||
## 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.
|
||||
#######################################################################################
|
||||
#
|
||||
# THIS SCRIPT IS PROVIDED BY THE MACOS SECURITY COMPLIANCE PROJECT (MSCP) "AS IS".
|
||||
# IN NO EVENT SHALL MSCP BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
||||
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SCRIPT, EVEN IF ADVISED
|
||||
# OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
#######################################################################################
|
||||
#
|
||||
# MSCP COMPLIANCE SCRIPT BUILD INFORMATION
|
||||
#
|
||||
# MSCP RELEASE: {{ mscp_version }}
|
||||
# BASELINE: {{ baseline_name }}
|
||||
# DATE: {{ todays_date }}
|
||||
#
|
||||
# The information above represents the version and build of MSCP that this script
|
||||
# was generated from along with which baseline was used and the date it was generated.
|
||||
#
|
||||
#######################################################################################
|
||||
#
|
||||
# Purpose and Use
|
||||
#
|
||||
# The purpose of this script is to perform compliance check and remediation actions
|
||||
# against the baseline generated by {{ mscp_version }}.
|
||||
# Implementation of this script should undergo thorough testing and understanding
|
||||
# prior to deployment in production environments.
|
||||
#
|
||||
# The script can be run interactively or silently.
|
||||
#
|
||||
# For additional information on MSCP, please visit the project's homepage at:
|
||||
# https://github.com/usnistgov/macos_security
|
||||
#
|
||||
#######################################################################################
|
||||
|
||||
################### Variables ###################
|
||||
|
||||
|
||||
13
config/default/templates/shell_scripts/restore.jinja
Normal file
13
config/default/templates/shell_scripts/restore.jinja
Normal file
@@ -0,0 +1,13 @@
|
||||
{% set check_tags = ["permanent", "inherent", "n_a", "not_applicable"] %}
|
||||
{% if "supplemental" not in rule.rule_id and not (rule.tags | select("in", check_tags) | list) %}
|
||||
{% if rule.default_state is not none and rule.default_state | length > 0 %}
|
||||
#####----- Rule: {{ rule.rule_id }} -----#####
|
||||
## Addresses the following NIST 800-53 controls:
|
||||
{{ rule.references.nist.nist_800_53r5 | group_ulify if rule.references.nist.nist_800_53r5 is not none else "# * N/A" }}
|
||||
|
||||
default_state_code="{{ rule.default_state | replace("\\\\", "\\") | replace('\"', '\\\"') | replace('$', '\$') | trim | safe }}"
|
||||
|
||||
rule_default_state "{{ rule.rule_id }}" "$default_state_code"
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
205
config/default/templates/shell_scripts/restore_script.sh.jinja
Normal file
205
config/default/templates/shell_scripts/restore_script.sh.jinja
Normal file
@@ -0,0 +1,205 @@
|
||||
#!/bin/zsh --no-rcs
|
||||
|
||||
#######################################################################################
|
||||
#
|
||||
# THIS SCRIPT IS PROVIDED BY THE MACOS SECURITY COMPLIANCE PROJECT (MSCP) "AS IS".
|
||||
# IN NO EVENT SHALL MSCP BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
||||
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SCRIPT, EVEN IF ADVISED
|
||||
# OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
#######################################################################################
|
||||
#
|
||||
# MSCP COMPLIANCE SCRIPT BUILD INFORMATION
|
||||
#
|
||||
# MSCP RELEASE: {{ mscp_version }}
|
||||
# BASELINE: {{ baseline_name }}
|
||||
# DATE: {{ todays_date }}
|
||||
#
|
||||
# The information above represents the version and build of MSCP that this script
|
||||
# was generated from along with which baseline was used and the date it was generated.
|
||||
#
|
||||
#######################################################################################
|
||||
#
|
||||
# Purpose and Use
|
||||
#
|
||||
# The purpose of this script is to restore the default macOS state for the settings
|
||||
# applied using the baseline generated by {{ mscp_version }}.
|
||||
# Implementation of this script should undergo thorough testing and understanding
|
||||
# prior to deployment in production environments.
|
||||
#
|
||||
# The script can be run interactively or silently.
|
||||
#
|
||||
# For additional information on MSCP, please visit the project's homepage at:
|
||||
# https://github.com/usnistgov/macos_security
|
||||
#
|
||||
#######################################################################################
|
||||
|
||||
|
||||
################### DEBUG MODE - hold shift when running the script ###################
|
||||
|
||||
shiftKeyDown=$(osascript -l JavaScript -e "ObjC.import('Cocoa'); ($.NSEvent.modifierFlags & $.NSEventModifierFlagShift) > 1")
|
||||
|
||||
if [[ $shiftKeyDown == "true" ]]; then
|
||||
echo "-----DEBUG-----"
|
||||
set -o xtrace -o verbose
|
||||
fi
|
||||
|
||||
################### COMMANDS START BELOW THIS LINE ###################
|
||||
|
||||
# Check if the current shell is Zsh
|
||||
if [[ -z "$ZSH_NAME" ]]; then
|
||||
echo "ERROR: This script must be run in Zsh."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
## Must be run as root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "This script must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# path to PlistBuddy
|
||||
plb="/usr/libexec/PlistBuddy"
|
||||
|
||||
# get the currently logged in user
|
||||
CURRENT_USER=$( /usr/sbin/scutil <<< "show State:/Users/ConsoleUser" | /usr/bin/awk '/Name :/ && ! /loginwindow/ { print $3 }')
|
||||
CURR_USER_UID=$(/usr/bin/id -u $CURRENT_USER)
|
||||
|
||||
# get system architecture
|
||||
arch=$(/usr/bin/arch)
|
||||
|
||||
# configure colors for text
|
||||
RED='\e[31m'
|
||||
STD='\e[39m'
|
||||
GREEN='\e[32m'
|
||||
YELLOW='\e[33m'
|
||||
|
||||
audit_log="/Library/Logs/{{ audit_name }}_baseline.log"
|
||||
|
||||
baseline_name="{{ baseline_name }}"
|
||||
|
||||
# pause function
|
||||
pause(){
|
||||
vared -p "Press [Enter] key to continue..." -c fackEnterKey
|
||||
}
|
||||
|
||||
# logging function
|
||||
logmessage(){
|
||||
local level="${2:-INFO}"
|
||||
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
local message="$1"
|
||||
|
||||
# Format: [LEVEL] [TIMESTAMP] MESSAGE
|
||||
local log_entry="[$level] [$timestamp] $message"
|
||||
|
||||
# Always log to file
|
||||
echo "$log_entry" >> "$audit_log"
|
||||
|
||||
# Handle quiet levels for console output
|
||||
if [[ ! $quiet ]]; then
|
||||
echo "$log_entry"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
ask() {
|
||||
# if fix flag is passed, assume YES for everything
|
||||
if [[ $all ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
rule_default_state(){
|
||||
ask "$1 - Run the command(s)-> $2" N
|
||||
if [[ $? == 0 ]]; then
|
||||
logmessage "Running the command to restore the settings for: $1 ..."
|
||||
eval "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
run_default_state(){
|
||||
# append to existing logfile
|
||||
logmessage "Beginning restoration of default settings"
|
||||
|
||||
if [[ ! $all ]]; then
|
||||
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
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
{% for profile in baseline.profile %}
|
||||
{% for rule in profile.rules %}
|
||||
{% include "restore.jinja" %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
} 2>/dev/null
|
||||
|
||||
usage=(
|
||||
"Usage: ${CMD:=${0##*/}} [--all] [--quiet=<value>]"
|
||||
" "
|
||||
"Optional parameters:"
|
||||
"--all : run the restore to default state on all rules"
|
||||
"--quiet : do not display log messages to stdout"
|
||||
)
|
||||
|
||||
set -- "$@" "${EOL:=$(printf '\1\3\3\7')}"
|
||||
|
||||
# Look for managed arguments for compliance script
|
||||
if [[ $# -eq 0 ]];then
|
||||
compliance_args=$(/usr/bin/osascript -l JavaScript << 'EOS'
|
||||
var defaults = $.NSUserDefaults.alloc.initWithSuiteName('org.{{ baseline_name }}.audit');
|
||||
var args = defaults.objectForKey('compliance_args');
|
||||
if (args && args.count > 0) {
|
||||
var result = [];
|
||||
for (var i = 0; i < args.count; i++) {
|
||||
result.push(ObjC.unwrap(args.objectAtIndex(i)));
|
||||
}
|
||||
result.join(' ');
|
||||
}
|
||||
EOS
|
||||
)
|
||||
if [[ -n "$compliance_args" ]]; then
|
||||
logmessage "Managed arguments found for compliance script, setting: $compliance_args"
|
||||
set -- ${(z)compliance_args}
|
||||
fi
|
||||
fi
|
||||
|
||||
zparseopts -D -E -help=flag_help -all=all -quiet:=quiet || { print -l $usage && return }
|
||||
|
||||
[[ -z "$flag_help" ]] || { print -l $usage && return }
|
||||
|
||||
run_default_state
|
||||
@@ -228,6 +228,7 @@ class Macsecurityrule(BaseModelWithAccessors):
|
||||
check: str | None = None
|
||||
fix: str | None = None
|
||||
severity: str | None = None
|
||||
default_state: str | None = None
|
||||
|
||||
@classmethod
|
||||
def load_rules(
|
||||
@@ -281,6 +282,7 @@ class Macsecurityrule(BaseModelWithAccessors):
|
||||
result_value: str | int | bool | None = None
|
||||
check_value: str | None = None
|
||||
fix_value: str | None = None
|
||||
default_state_value: str | None = None
|
||||
mechanism: str = "Manual"
|
||||
payloads: list[Mobileconfigpayload] | None = []
|
||||
severity: str | None = None
|
||||
@@ -352,6 +354,9 @@ class Macsecurityrule(BaseModelWithAccessors):
|
||||
check_result = enforcement_info.get("check", {}).get("result")
|
||||
fix_shell = enforcement_info.get("fix", {}).get("shell")
|
||||
additional_info = enforcement_info.get("fix", {}).get("additional_info")
|
||||
default_state_shell = enforcement_info.get("default_state", {}).get(
|
||||
"shell"
|
||||
)
|
||||
|
||||
if check_result:
|
||||
for k, v in rule_yaml["platforms"][os_type]["enforcement_info"][
|
||||
@@ -374,6 +379,9 @@ class Macsecurityrule(BaseModelWithAccessors):
|
||||
if check_shell:
|
||||
check_value = check_shell
|
||||
|
||||
if default_state_shell:
|
||||
default_state_value = default_state_shell
|
||||
|
||||
if (
|
||||
not check_shell
|
||||
or not fix_shell
|
||||
@@ -530,6 +538,7 @@ class Macsecurityrule(BaseModelWithAccessors):
|
||||
os_version=os_version,
|
||||
check=check_value,
|
||||
fix=fix_value,
|
||||
default_state=default_state_value,
|
||||
severity=severity,
|
||||
)
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ from ..generate.guidance_support import (
|
||||
generate_excel,
|
||||
generate_profiles,
|
||||
generate_script,
|
||||
generate_restore_script,
|
||||
)
|
||||
|
||||
|
||||
@@ -139,6 +140,9 @@ def generate_guidance(args: argparse.Namespace) -> None:
|
||||
if args.script and args.os_name == "macos":
|
||||
logger.info("Generating compliance script")
|
||||
generate_script(build_path, baseline_name, audit_name, baseline, log_reference)
|
||||
generate_restore_script(
|
||||
build_path, baseline_name, audit_name, baseline, log_reference
|
||||
)
|
||||
|
||||
if args.xlsx:
|
||||
logger.info("Generating Excel document")
|
||||
@@ -174,7 +178,20 @@ def generate_guidance(args: argparse.Namespace) -> None:
|
||||
if args.os_name == "macos":
|
||||
logger.info("Generating compliance script")
|
||||
generate_script(
|
||||
build_path, baseline_name, audit_name, baseline, log_reference
|
||||
build_path,
|
||||
baseline_name,
|
||||
audit_name,
|
||||
baseline,
|
||||
log_reference,
|
||||
current_version_data,
|
||||
)
|
||||
generate_restore_script(
|
||||
build_path,
|
||||
baseline_name,
|
||||
audit_name,
|
||||
baseline,
|
||||
log_reference,
|
||||
current_version_data,
|
||||
)
|
||||
|
||||
# logger.info("Generating Excel document")
|
||||
|
||||
@@ -6,6 +6,7 @@ __all__ = [
|
||||
"generate_excel",
|
||||
"generate_profiles",
|
||||
"generate_script",
|
||||
"generate_restore_script",
|
||||
]
|
||||
|
||||
|
||||
@@ -13,4 +14,4 @@ from .ddm import generate_ddm
|
||||
from .documents import generate_documents
|
||||
from .excel import generate_excel
|
||||
from .profiles import generate_profiles
|
||||
from .script import generate_script
|
||||
from .script import generate_script, generate_restore_script
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# mscp/generate/script.py
|
||||
|
||||
# Standard python modules
|
||||
from datetime import date
|
||||
from itertools import groupby
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -140,6 +141,7 @@ def generate_script(
|
||||
audit_name: str,
|
||||
baseline: Baseline,
|
||||
log_reference: str,
|
||||
current_version_data: dict,
|
||||
) -> None:
|
||||
output_file: Path = Path(build_path, f"{baseline_name}_compliance.sh")
|
||||
env: Environment = Environment(
|
||||
@@ -160,8 +162,52 @@ def generate_script(
|
||||
baseline_name=baseline_name,
|
||||
audit_name=audit_name,
|
||||
reference_log_id=log_reference,
|
||||
todays_date=date.today().strftime("%Y-%m-%d"),
|
||||
mscp_version=current_version_data["compliance_version"],
|
||||
)
|
||||
|
||||
generate_audit_plist(build_path, baseline_name, baseline)
|
||||
output_file.write_text(rendered_output, encoding="UTF-8")
|
||||
output_file.chmod(0o755)
|
||||
|
||||
|
||||
def generate_restore_script(
|
||||
build_path: Path,
|
||||
baseline_name: str,
|
||||
audit_name: str,
|
||||
baseline: Baseline,
|
||||
log_reference: str,
|
||||
current_version_data: dict,
|
||||
) -> None:
|
||||
output_file: Path = Path(build_path, f"{baseline_name}_restore.sh")
|
||||
env: Environment = Environment(
|
||||
loader=FileSystemLoader(config["defaults"]["shell_template_dir"]),
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
)
|
||||
script_template = env.get_template("restore_script.sh.jinja")
|
||||
|
||||
env.filters["group_ulify"] = group_ulify
|
||||
env.filters["log_reference"] = generate_log_reference
|
||||
env.filters["quotify"] = quotify
|
||||
|
||||
baseline_dict: dict[str, Any] = dict(baseline)
|
||||
|
||||
any_rendered = any(
|
||||
rule.get("default_state")
|
||||
for p in baseline_dict["profile"]
|
||||
for rule in p["rules"]
|
||||
)
|
||||
|
||||
rendered_output = script_template.render(
|
||||
baseline=baseline_dict,
|
||||
baseline_name=baseline_name,
|
||||
audit_name=audit_name,
|
||||
reference_log_id=log_reference,
|
||||
todays_date=date.today().strftime("%Y-%m-%d"),
|
||||
mscp_version=current_version_data["compliance_version"],
|
||||
)
|
||||
|
||||
if any_rendered:
|
||||
output_file.write_text(rendered_output, encoding="UTF-8")
|
||||
output_file.chmod(0o755)
|
||||
|
||||
Reference in New Issue
Block a user