security-misc/usr/bin/permission-hardener

978 lines
30 KiB
Bash
Executable File

#!/bin/bash
# shellcheck disable=SC2076
## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.
## https://forums.whonix.org/t/disable-suid-binaries/7706
## https://forums.whonix.org/t/re-mount-home-and-other-with-noexec-and-nosuid-among-other-useful-mount-options-for-better-security/7707
## dpkg-statoverride does not support end-of-options ("--").
## SC2076 is disabled because ShellCheck seems to think that any use of
## [[ ... =~ ... ]] is supposed to be a regex match. But [[ '...' =~ '...' ]]
## works very well for literal matching, and it is used that way extensively
## throughout this script.
set -o errexit -o nounset -o pipefail
## Constants
# shellcheck disable=SC2034
log_level=notice
store_dir="/var/lib/permission-hardener-v2"
state_file="${store_dir}/existing_mode/statoverride"
dpkg_admindir_parameter_existing_mode="--admindir ${store_dir}/existing_mode"
dpkg_admindir_parameter_new_mode="--admindir ${store_dir}/new_mode"
delimiter="#permission-hardener-delimiter#"
## Library imports
# shellcheck disable=SC1091
source /usr/libexec/helper-scripts/safe_echo.sh
# shellcheck disable=SC1091
source /usr/libexec/helper-scripts/log_run_die.sh
## Functions
echo_wrapper_ignore() {
if [ "${1}" = 'verbose' ]; then
shift
log notice "Executing: $*"
elif [ "${1}" = 'silent' ]; then
shift
else
log error "Unrecognized command '${1}'! calling function name: '${FUNCNAME[1]}'" >&2
return
fi
"$@" 2>/dev/null || true
}
echo_wrapper_audit() {
local return_code
if [ "${1}" = 'verbose' ]; then
shift
log notice "Executing: $*"
elif [ "${1}" = 'silent' ]; then
shift
else
log error "Unrecognized command '${1}'! calling function name: '${FUNCNAME[1]}'" >&2
return
fi
return_code=0
"$@" ||
{
return_code="$?"
exit_code=203
log error "Command '$*' failed with exit code '${return_code}'! calling function name: '${FUNCNAME[1]}'" >&2
}
}
## Some tools may fail on newlines and even variable assignment to array may
## fail if a variable that will be assigned to an array element contains
## characters that are used as delimiters.
block_newlines() {
local newline_variable newline_value
newline_variable="${1:-}"
newline_value="${2:-}"
## dpkg-statoverride: error: path may not contain newlines
if [[ "${newline_value}" != "${newline_value//$'\n'/NEWLINE}" ]]; then
log warn "Skipping ${newline_variable} that contains newlines: '${newline_value}'" >&2
return 1
fi
}
output_stat() {
local file_name stat_output stat_output_newlined
declare -a arr
file_name="${1:-}"
if [ -z "${file_name}" ]; then
log error "File name is empty. file_name: '${file_name}'" >&2
return 1
fi
block_newlines file "${file_name}"
if [ ! -e "${file_name}" ]; then
log info "File does not exist. file_name: '${file_name}'" >&2
existing_mode=''
existing_owner=''
existing_group=''
file_name_from_stat=''
return 0
fi
if ! stat_output="$(stat -L \
--format="%a${delimiter}%U${delimiter}%G${delimiter}%n${delimiter}" \
-- "${file_name}")"; then
log error "Failed to run 'stat' on file: '${file_name}'!" >&2
return 1
fi
if [ -z "$stat_output" ]; then
log error "stat_output is empty.
File name: '${file_name}'
Stat output: '${stat_output}'
stat_output_newlined: '${stat_output_newlined}'
line: '${processed_config_line}'
" >&2
return 1
fi
stat_output_newlined="$(printf '%s\n' "${stat_output//${delimiter}/$'\n'}")"
if [ -z "${stat_output_newlined}" ]; then
log error "stat_output_newlined is empty.
File name: '${file_name}'
Stat output: '${stat_output}'
stat_output_newlined: '${stat_output_newlined}'
line: '${processed_config_line}'
" >&2
return 1
fi
readarray -t arr <<< "${stat_output_newlined}"
if [ "${#arr[@]}" = '0' ]; then
log error "Array length is 0.
File name: '${file_name}'
Stat output: '${stat_output}'
stat_output_newlined: '${stat_output_newlined}'
line: '${processed_config_line}'
" >&2
return 1
fi
existing_mode="${arr[0]}"
existing_owner="${arr[1]}"
existing_group="${arr[2]}"
file_name_from_stat="${arr[3]}"
if [ "$file_name" != "$file_name_from_stat" ]; then
log error "\
File name is different from file name received from stat:
File name: '${file_name}'
File name from stat: '${file_name_from_stat}'
line: '${processed_config_line}'
" >&2
return 1
fi
if [ -z "${existing_mode}" ]; then
log error "Existing mode is empty. Stat output: '${stat_output}', line: '${processed_config_line}'" >&2
return 1
fi
if [ -z "${existing_owner}" ]; then
log error "Existing owner is empty. Stat output: '${stat_output}', line: '${processed_config_line}'" >&2
return 1
fi
if [ -z "${existing_group}" ]; then
log error "Existing group is empty. Stat output: '${stat_output}', line: '${processed_config_line}'" >&2
return 1
fi
## If a symlink was passed as input, return the original file's path rather
## than the symlink to avoid problems stemming from using the wrong path
if [ -h "${file_name_from_stat}" ]; then
file_name_from_stat="$(realpath "${file_name_from_stat}")"
fi
}
print_usage(){
safe_echo "Usage: ${0##*/} enable
${0##*/} disable [FILE|all]
${0##*/} print-policy
${0##*/} print-state
${0##*/} print-policy-applied-state
${0##*/} print-diagnostics
Examples:
${0##*/} enable
${0##*/} disable all
${0##*/} disable /usr/bin/newgrp" >&2
}
add_to_policy() {
local file_name file_mode file_owner file_group updated_entry policy_idx \
file_capabilities
file_name="${1:-}"
file_mode="${2:-}"
file_owner="${3:-}"
file_group="${4:-}"
file_capabilities="${5:-}"
updated_entry=false
if [ -h "${file_name}" ]; then
file_name="$(realpath "${file_name}")" || return 1
fi
for (( policy_idx=0; policy_idx < ${#policy_file_list[@]}; policy_idx++ )); do
if [ "${policy_file_list[policy_idx]}" = "${file_name}" ]; then
policy_mode_list[policy_idx]="${file_mode}"
policy_user_owner_list[policy_idx]="${file_owner}"
policy_group_owner_list[policy_idx]="${file_group}"
policy_capability_list[policy_idx]="${file_capabilities}"
updated_entry=true
break
fi
done
if [ "${updated_entry}" != 'true' ]; then
policy_file_list+=( "${file_name}" )
policy_mode_list+=( "${file_mode}" )
policy_user_owner_list+=( "${file_owner}" )
policy_group_owner_list+=( "${file_group}" )
policy_capability_list+=( "${file_capabilities}" )
fi
}
check_nosuid_whitelist() {
local target_file match_white_list_entry
target_file="${1:-}"
## Handle whitelists, if we're supposed to
[ "${whitelists_disable_all}" = 'true' ] && return 0
## literal matching is intentional here
[[ " ${policy_disable_white_list[*]} " =~ " ${target_file} " ]] && return 0
## literal matching is intentional here too
[[ " ${policy_exact_white_list[*]} " =~ " ${target_file} " ]] && return 1
for match_white_list_entry in "${policy_match_white_list[@]:-}"; do
if safe_echo "${target_file}" \
| grep --quiet --fixed-strings -- "${match_white_list_entry}"; then
return 1
fi
done
return 0
}
load_early_nosuid_policy() {
local target_file find_list_item
target_file="${1:-}"
# shellcheck disable=SC2185
while IFS="" read -r -d "" find_list_item; do
check_nosuid_whitelist "${find_list_item}" || continue
## sets:
## exiting_mode
## existing_owner
## existing_group
output_stat "${find_list_item}"
if [ -z "${file_name_from_stat}" ]; then
continue
fi
## -h file True if file is a symbolic link.
if [ -h "${find_list_item}" ]; then
## https://forums.whonix.org/t/disable-suid-binaries/7706/14
log info "Skip symlink: '${find_list_item}'"
continue
fi
if [ -d "${find_list_item}" ]; then
log info "Skip directory: '${find_list_item}'"
continue
fi
## Remove suid / gid and execute permission for 'group' and 'others'.
## Similar to: chmod og-ugx /path/to/filename
## Removing execution permission is useful to make binaries such as 'su'
## fail closed rather than fail open if suid was removed from these.
## Do not remove read access since no security benefit and easier to
## manually undo for users.
## Are there suid or sgid binaries which are still useful if suid / sgid
## has been removed from these?
local new_mode
new_mode='744'
add_to_policy "${file_name_from_stat}" "${new_mode}" "${existing_owner}" \
"${existing_group}"
done < <(safe_echo_nonewline "${target_file}" \
| find -files0-from - -perm /u=s,g=s -print0)
}
## If the "target file" matches the start of the state file name, that's a
## likely match. This is used by load_late_nosuid_policy for detecting info
## about files that need SUID-locked that are in the state.
match_dir() {
local base_str match_str base_arr match_arr base_idx
base_str="${1}"
match_str="${2}"
[[ "${base_str}" =~ '//' ]] && return 1
[[ "${match_str}" =~ '//' ]] && return 1
IFS='/' read -r -a base_arr <<< "${base_str}"
IFS='/' read -r -a match_arr <<< "${match_str}"
(( ${#base_arr[@]} > ${#match_arr[@]} )) && return 1
for (( base_idx=0; base_idx < ${#base_arr[@]}; base_idx++ )); do
if [ "${base_arr[base_idx]}" != "${match_arr[base_idx]}" ]; then
return 1
fi
done
return 0
}
load_late_nosuid_policy() {
local target_file state_idx state_file_item state_user_owner_item \
state_group_owner_item new_mode
target_file="${1:-}"
for (( state_idx=0; state_idx < ${#state_file_list[@]}; state_idx++ )); do
state_file_item="${state_file_list[state_idx]}"
check_nosuid_whitelist "${state_file_item}" || continue
match_dir "${target_file}" "${state_file_item}" || continue
if [ -h "${state_file_item}" ]; then
## https://forums.whonix.org/t/disable-suid-binaries/7706/14
log info "Skip symlink: '${state_file_item}'"
continue
fi
if [ -d "${state_file_item}" ]; then
log info "Skip directory: '${state_file_item}'"
continue
fi
state_user_owner_item="${state_user_owner_list[state_idx]}"
state_group_owner_item="${state_group_owner_list[state_idx]}"
new_mode='744'
add_to_policy "${state_file_item}" "${new_mode}" \
"${state_user_owner_item}" "${state_group_owner_item}"
done
}
load_state_without_policy() {
local line field_list
## Load the state file from disk
if [ -f "${state_file}" ]; then
while read -r line; do
read -r -a field_list <<< "${line}"
if (( ${#field_list[@]} != 4 )); then
log info \
"Invalid number of fields in state file line: '${line}'. Skipping."
continue
fi
state_user_owner_list+=( "${field_list[0]}" )
state_group_owner_list+=( "${field_list[1]}" )
state_mode_list+=( "${field_list[2]}" )
state_file_list+=( "${field_list[3]}" )
done < "${state_file}"
fi
}
load_state() {
## Config format:
## path options
## where options is one of:
## user_owner group_owner filemode [capability-setting]
## [nosuid|exactwhitelist|matchwhitelist|disablewhitelist]
##
## Additionally, the special value 'whitelists_disable_all=true' is understood
## to mean that all whitelisting should be ignored.
local config_file line field_list policy_nosuid_file_item policy_file_item
## Load configuration, deferring whitelist handling until later
for config_file in \
/usr/lib/permission-hardener.d/*.conf \
/etc/permission-hardener.d/*.conf \
/usr/local/etc/permission-hardener.d/*.conf \
/etc/permission-hardening.d/*.conf \
/usr/local/etc/permission-hardening.d/*.conf
do
if [ ! -f "${config_file}" ]; then
continue
fi
while read -r line; do
if [ -z "${line}" ]; then
true 'DEBUG: line is empty. Skipping.'
continue
fi
if [[ "${line}" =~ ^\s*# ]]; then
continue
fi
if ! [[ "${line}" =~ ^[-0-9a-zA-Z._/[:space:]]*$ ]]; then
exit_code=200
log error "Line contains invalid characters: '${line}'" >&2
## Safer to exit with error in this case.
## https://forums.whonix.org/t/disable-suid-binaries/7706/59
exit "${exit_code}"
fi
if [ "${line}" = 'whitelists_disable_all=true' ]; then
whitelists_disable_all=true
log info "whitelists_disable_all=true"
continue
fi
processed_config_line="${line}"
IFS=' ' read -r -a field_list <<< "${line}"
case "${#field_list[@]}" in
2|4|5) true;;
*)
exit_code=200
log error "Line contains an invalid number of fields: '${line}'" >&2
exit "${exit_code}"
;;
esac
# Strip trailing slash if appropriate
field_list[0]="${field_list[0]%/}"
case "${field_list[1]}" in
'exactwhitelist')
[ ! -e "${field_list[0]}" ] && continue
policy_exact_white_list+=( "${field_list[0]}" )
continue
;;
'matchwhitelist')
policy_match_white_list+=( "${field_list[0]}" )
continue
;;
'disablewhitelist')
policy_disable_white_list+=( "${field_list[0]}" )
continue
;;
'nosuid')
[ ! -e "${field_list[0]}" ] && continue
policy_nosuid_file_list+=( "${field_list[0]}" )
;;
*)
[ ! -e "${field_list[0]}" ] && continue
add_to_policy "${field_list[@]}"
;;
esac
done < "${config_file}"
done
## We have to handle nosuid files at the end since the whitelist arrays need
## built first.
for policy_nosuid_file_item in "${policy_nosuid_file_list[@]}"; do
load_early_nosuid_policy "${policy_nosuid_file_item}"
done
load_state_without_policy
## Find any files in the policy that don't already have a matching file in
## the state. Add those files to the state, and save them to the state file
## as well.
for policy_file_item in "${policy_file_list[@]}"; do
if [[ " ${state_file_list[*]} " =~ " ${policy_file_item} " ]]; then
continue
fi
output_stat "${policy_file_item}"
if [ -z "${file_name_from_stat}" ]; then
continue
fi
state_file_list+=( "${file_name_from_stat}" )
state_user_owner_list+=( "${existing_owner}" )
state_group_owner_list+=( "${existing_group}" )
state_mode_list+=( "${existing_mode}" )
# shellcheck disable=SC2086
echo_wrapper_audit silent dpkg-statoverride \
${dpkg_admindir_parameter_existing_mode} \
--add "${existing_owner}" "${existing_group}" "${existing_mode}" \
"${file_name_from_stat}"
done
## Fix up nosuid policies using state information
for policy_nosuid_file_item in "${policy_nosuid_file_list[@]}"; do
load_late_nosuid_policy "${policy_nosuid_file_item}"
done
}
apply_policy() {
local policy_idx did_state_update state_idx
## Modify the in-memory state so that all items that the policy affects match
## the policy. DO NOT save these changes to the state file!
for (( policy_idx=0; policy_idx < ${#policy_file_list[@]}; policy_idx++ )); do
did_state_update=false
for (( state_idx=0; state_idx < ${#state_file_list[@]}; state_idx++ )); do
if [ "${state_file_list[state_idx]}" = "${policy_file_list[policy_idx]}" ]; then
state_user_owner_list[state_idx]="${policy_user_owner_list[policy_idx]}"
state_group_owner_list[state_idx]="${policy_group_owner_list[policy_idx]}"
state_mode_list[state_idx]="${policy_mode_list[policy_idx]}"
did_state_update=true
break
fi
done
if [ "${did_state_update}" = 'false' ]; then
exit_code=206
log error \
"File exists in policy but not in state! File: '${policy_file_list[policy_idx]}'"
exit "${exit_code}"
fi
done
}
commit_policy() {
local policy_idx state_idx state_file_item \
state_user_owner_item state_group_owner_item \
state_mode_item orig_main_statoverride_db orig_new_statoverride_db \
policy_file_item policy_capability_item
## Check each file on the filesystem against the state, and update it if the
## state does not match. Also ensure the consistency of the new_mode database
## so that people can compare the original permissions of files with the new
## permissions.
orig_main_statoverride_db="$(dpkg-statoverride --list)" || true
# shellcheck disable=SC2086
orig_new_statoverride_db="$(dpkg-statoverride ${dpkg_admindir_parameter_new_mode} --list)" || true
for (( state_idx=0; state_idx < ${#state_file_list[@]}; state_idx++ )); do
state_file_item="${state_file_list[state_idx]}"
state_user_owner_item="${state_user_owner_list[state_idx]}"
state_group_owner_item="${state_group_owner_list[state_idx]}"
state_mode_item="${state_mode_list[state_idx]}"
## Get rid of leading zeros, stat doesn't output them due to how we use it.
## Using BASH_REMATCH is faster than sed. We capture all leading zeros into
## one group, and the rest of the string into a second group. The second
## group is the string we want. BASH_REMATCH[0] is the entire string,
## BASH_REMATCH[1] is the first match that we want to discard, and
## BASH_REMATCH[2] is the desired second group.
[[ "${state_mode_item}" =~ ^(0*)(.*) ]] || true;
state_mode_item="${BASH_REMATCH[2]}"
output_stat "${state_file_item}"
if [ -z "${file_name_from_stat}" ]; then
continue
fi
if [ "${existing_owner}" != "${state_user_owner_item}" ] \
|| [ "${existing_group}" != "${state_group_owner_item}" ] \
|| [ "${existing_mode}" != "${state_mode_item}" ]; then
if ! [[ "${passwd_file_contents}" =~ "${state_user_owner_item}:" ]]; then
log error "Owner from config does not exist: '${state_user_owner_item}'" >&2
continue
fi
if ! [[ "${group_file_contents}" =~ "${state_group_owner_item}:" ]]; then
log error "Group from config does not exist: '${state_group_owner_item}'" >&2
continue
fi
## Remove and reapply in main list
if [[ "${orig_main_statoverride_db}" =~ "${file_name_from_stat}" ]]; then
echo_wrapper_ignore silent dpkg-statoverride --remove \
"${file_name_from_stat}"
fi
echo_wrapper_audit verbose dpkg-statoverride --add --update \
"${state_user_owner_item}" "${state_group_owner_item}" \
"${state_mode_item}" "${file_name_from_stat}"
## Update item in secondary list
if [[ "${orig_new_statoverride_db}" =~ "${file_name_from_stat}" ]]; then
# shellcheck disable=SC2086
echo_wrapper_ignore silent dpkg-statoverride \
${dpkg_admindir_parameter_new_mode} --remove \
"${file_name_from_stat}"
fi
# shellcheck disable=SC2086
echo_wrapper_audit verbose dpkg-statoverride \
${dpkg_admindir_parameter_new_mode} --add \
"${state_user_owner_item}" "${state_group_owner_item}" \
"${state_mode_item}" "${file_name_from_stat}"
fi
done
## Apply capability hardening, dpkg-statoverride can't handle this so we have
## to do this manually
for (( policy_idx=0; policy_idx < ${#policy_file_list[@]}; policy_idx++ )); do
policy_file_item="${policy_file_list[policy_idx]}"
policy_capability_item="${policy_capability_list[policy_idx]}"
if [ -z "${policy_capability_item}" ]; then
continue
fi
if [ "${policy_capability_item}" = 'none' ]; then
echo_wrapper_ignore verbose setcap -r "${policy_file_item}"
if [ -n "$(getcap -- "${policy_file_item}")" ]; then
exit_code=205
log error \
"Removing capabilities failed. File: '${policy_file_item}'" >&2
continue
fi
else
if ! capsh --print \
| grep --fixed-strings -- "Bounding set" \
| grep --quiet -- "${policy_capability_item}"; then
log error \
"Capability from config does not exist: '${policy_capability_item}'" \
>&2
continue
fi
## feature request: dpkg-statoverride: support for capabilities
## https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=502580
echo_wrapper_audit verbose setcap "${policy_capability_item}+ep" \
-- "${policy_file_item}"
fi
done
log notice "\
To compare the current and previous permission modes, install 'meld' (or preferred diff tool) for comparison of file mode changes:
sudo apt install --no-install-recommends meld
meld ${store_dir}/existing_mode/statoverride ${store_dir}/new_mode/statoverride"
}
undo_policy_for_file() {
local undo_file state_idx state_file_item did_undo \
undo_all verbose orig_main_statoverride_db orig_new_statoverride_db \
state_user_owner_item state_group_owner_item state_mode_item
undo_file="${1}"
undo_all=false
verbose='--verbose'
if [ "${undo_file}" = 'all' ]; then
undo_all=true
verbose=''
fi
if [ ! -f "${state_file}" ]; then
true 'DEBUG: State file does not exist, hardening was not applied before.'
return 0
fi
did_undo=false
for (( state_idx=0; state_idx < ${#state_file_list[@]}; state_idx++ )); do
state_file_item="${state_file_list[state_idx]}"
if [ "${undo_all}" = 'true' ]; then
undo_file="${state_file_item}"
fi
if [ "${state_file_item}" = "${undo_file}" ]; then
orig_main_statoverride_db="$(dpkg-statoverride --list)" || true
# shellcheck disable=SC2086
orig_new_statoverride_db="$(dpkg-statoverride ${dpkg_admindir_parameter_new_mode} --list)" || true
if [[ "${orig_main_statoverride_db}" =~ "${undo_file}" ]]; then
echo_wrapper_ignore silent dpkg-statoverride --remove \
"${undo_file}"
fi
if [[ "${orig_new_statoverride_db}" =~ "${undo_file}" ]]; then
# shellcheck disable=SC2086
echo_wrapper_ignore silent dpkg-statoverride \
${dpkg_admindir_parameter_new_mode} --remove \
"${undo_file}"
fi
if [ -e "${undo_file}" ]; then
state_user_owner_item="${state_user_owner_list[state_idx]}"
state_group_owner_item="${state_group_owner_list[state_idx]}"
state_mode_item="${state_mode_list[state_idx]}"
chown ${verbose} -- "${state_user_owner_item}:${state_group_owner_item}" \
"${undo_file}" || exit_code=202
## chmod needs to be run after chown since chown removes suid.
chmod ${verbose} "${state_mode_item}" "${undo_file}" || exit_code=203
else
log info "File does not exist: '${undo_file}'"
fi
did_undo=true
if [ "${undo_all}" = 'false' ]; then
break
fi
fi
done
if ! [[ "${did_undo}" = 'false' ]]; then
log info "The specified file is not hardened, leaving unchanged.
File '${undo_file}' has not been removed from SUID Disabler and Permission Hardener during this invocation. This is expected if no policy was ever applied to the file before.
This program expects the full path to the file. Example:
$0 disable /usr/bin/newgrp # absolute path: works
$0 disable newgrp # relative path: does not work
To remove all:
$0 disable all
This change might not be permanent. For full instructions, see:
https://www.kicksecure.com/wiki/SUID_Disabler_and_Permission_Hardener
To view list of changed by SUID Disabler and Permission Hardener:
https://www.kicksecure.com/wiki/SUID_Disabler_and_Permission_Hardener#View_List_of_Permissions_Changed_by_SUID_Disabler_and_Permission_Hardener
For re-enabling any specific SUID binary:
https://www.kicksecure.com/wiki/SUID_Disabler_and_Permission_Hardener#Re-Enable_Specific_SUID_Binaries
For completely disabling SUID Disabler and Permission Hardener:
https://www.kicksecure.com/wiki/SUID_Disabler_and_Permission_Hardener#Disable_SUID_Disabler_and_Permission_Hardener"
fi
}
print_columns() {
local format_str bogus_str
format_str=''
for bogus_str in "$@"; do
format_str="${format_str}%s\t"
done
format_str="${format_str}\n"
## Using a dynamically generated format string on purpose.
# shellcheck disable=SC2059
printf "${format_str}" "$@"
}
print_policy() {
local policy_idx
print_columns 'File' 'User' 'Group' 'Mode' 'Capabilities'
for (( policy_idx=0; policy_idx < ${#policy_file_list[@]}; policy_idx++ )); do
print_columns \
"${policy_file_list[policy_idx]}" \
"${policy_user_owner_list[policy_idx]}" \
"${policy_group_owner_list[policy_idx]}" \
"${policy_mode_list[policy_idx]}" \
"${policy_capability_list[policy_idx]}"
done
}
print_state() {
local state_idx
print_columns 'File' 'User' 'Group' 'Mode'
for (( state_idx=0; state_idx < ${#state_file_list[@]}; state_idx++ )); do
print_columns \
"${state_file_list[state_idx]}" \
"${state_user_owner_list[state_idx]}" \
"${state_group_owner_list[state_idx]}" \
"${state_mode_list[state_idx]}"
done
}
print_raw_policy_config() {
local config_file
for config_file in \
/usr/lib/permission-hardener.d/*.conf \
/etc/permission-hardener.d/*.conf \
/usr/local/etc/permission-hardener.d/*.conf \
/etc/permission-hardening.d/*.conf \
/usr/local/etc/permission-hardening.d/*.conf
do
if [ ! -f "${config_file}" ]; then
continue
fi
echo "*** begin ${config_file} ***"
cat "${config_file}"
echo "*** end ${config_file} ***"
done
}
print_raw_state() {
local state_file
for state_file in "${store_dir}/existing_mode/statoverride" \
"${store_dir}/new_mode/statoverride"; do
echo "*** begin ${state_file} ***"
cat "${state_file}"
echo "*** end ${state_file} ***"
done
}
print_fs_audit() {
local state_idx state_file_item state_user_owner_item state_group_owner_item \
state_mode_item
echo 'Legend:'
echo '... - Warning about an unusual, but not necessarily wrong, condition'
echo '!!! - Warning about an unusual and definitely wrong condition'
echo '*** - File permission data, actual state on filesystem is consistent with policy'
echo '^^^ - File permission data, actual state on filesystem is inconsistent with policy'
echo 'vvv - File permissions specified by state, always shown after a ^^^ item'
echo
for (( state_idx=0; state_idx < ${#state_file_list[@]}; state_idx++ )); do
state_file_item="${state_file_list[state_idx]}"
state_user_owner_item="${state_user_owner_list[state_idx]}"
state_group_owner_item="${state_group_owner_list[state_idx]}"
state_mode_item="${state_mode_list[state_idx]}"
## Get rid of leading zeros, stat doesn't output them due to how we use it.
## Using BASH_REMATCH is faster than sed. We capture all leading zeros into
## one group, and the rest of the string into a second group. The second
## group is the string we want. BASH_REMATCH[0] is the entire string,
## BASH_REMATCH[1] is the first match that we want to discard, and
## BASH_REMATCH[2] is the desired second group.
[[ "${state_mode_item}" =~ ^(0*)(.*) ]] || true;
state_mode_item="${BASH_REMATCH[2]}"
output_stat "${state_file_item}"
if [ -z "${file_name_from_stat}" ]; then
echo "... '${file_name_from_stat}' does not exist"
continue
fi
if [ "${existing_owner}" != "${state_user_owner_item}" ] \
|| [ "${existing_group}" != "${state_group_owner_item}" ] \
|| [ "${existing_mode}" != "${state_mode_item}" ]; then
if ! [[ "${passwd_file_contents}" =~ "${state_user_owner_item}:" ]]; then
echo "!!! Owner from config does not exist: '${state_user_owner_item}'"
continue
fi
if ! [[ "${group_file_contents}" =~ "${state_group_owner_item}:" ]]; then
echo "!!! Group from config does not exist: '${state_group_owner_item}'"
continue
fi
echo "^^^ ${file_name_from_stat} ${existing_owner}:${existing_group} ${existing_mode}"
echo "vvv ${file_name_from_stat} ${state_user_owner_item}:${state_group_owner_item} ${state_mode_item}"
else
echo "*** ${file_name_from_stat} ${existing_owner}:${existing_group} ${existing_mode}"
fi
done
}
reset_global_vars() {
## Global variables
policy_file_list=()
policy_user_owner_list=()
policy_group_owner_list=()
policy_mode_list=()
policy_capability_list=()
policy_exact_white_list=()
policy_match_white_list=()
policy_disable_white_list=()
policy_nosuid_file_list=()
state_file_list=()
state_user_owner_list=()
state_group_owner_list=()
state_mode_list=()
whitelists_disable_all=false
existing_mode=''
existing_owner=''
existing_group=''
processed_config_line=''
file_name_from_stat=''
passwd_file_contents="$(getent passwd)"
group_file_contents="$(getent group)"
exit_code=0
}
reset_global_vars
## Setup and sanity checking
if [ "$(id -u)" != '0' ]; then
log error "Not running as root, aborting."
exit 1
fi
mkdir --parents "${store_dir}/existing_mode"
mkdir --parents "${store_dir}/new_mode"
echo_wrapper_audit silent which capsh getcap setcap stat find \
dpkg-statoverride getent grep 1>/dev/null
## Command parsing and execution
case "${1:-}" in
enable)
shift
load_state
apply_policy
commit_policy
;;
disable)
shift
case "${1:-}" in
"")
print_usage
exit 1
;;
*)
load_state_without_policy
undo_policy_for_file "${1}"
;;
esac
;;
print-policy)
load_state
print_policy
;;
print-state)
load_state
print_state
;;
print-policy-applied-state)
load_state
apply_policy
print_state
;;
print-diagnostics)
echo '=== BEGIN PERMISSION-HARDENER DIAGNOSTICS ==='
echo '--- BEGIN State without policy ---'
load_state_without_policy
print_state
echo '--- END State without policy ---'
reset_global_vars
echo '--- BEGIN Policy without state ---'
load_state
print_policy
echo '--- END Policy without state ---'
reset_global_vars
echo '--- BEGIN Policy-applied-state ---'
load_state
apply_policy
print_state
echo '--- END Policy-applied state ---'
reset_global_vars
echo '--- BEGIN Master dpkg-statoverride database ---'
dpkg-statoverride --list
echo '--- END Master dpkg-statoverride database ---'
echo '--- BEGIN Raw policy configuration ---'
print_raw_policy_config
echo '--- END Raw policy configuration ---'
echo '--- BEGIN Raw state data ---'
print_raw_state
echo '--- END Raw state data ---'
echo '--- BEGIN Filesystem state audit ---'
load_state
apply_policy
print_fs_audit
echo '--- END Filesystem state audit ---'
echo '=== END PERMISSION-HARDENER DIAGNOSTICS ==='
;;
-h|--help)
print_usage
exit 0
;;
*)
print_usage
exit 1
;;
esac
## Exit
if test "${exit_code}" != "0"; then
log error "Exiting with non-zero exit code: '${exit_code}'" >&2
fi
exit "${exit_code}"