From 9c3566f524f748b9f7c98a36b3f2b1064cdba3ed Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Mon, 22 Jul 2024 16:01:14 +0200 Subject: [PATCH 1/5] Delimit file names with null terminator --- usr/bin/permission-hardener | 55 ++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/usr/bin/permission-hardener b/usr/bin/permission-hardener index 1d4c868..026e290 100755 --- a/usr/bin/permission-hardener +++ b/usr/bin/permission-hardener @@ -65,18 +65,18 @@ add_nosuid_statoverride_entry() { counter_actual=0 local dummy_line - while read -r dummy_line; do + while IFS="" read -r -d "" dummy_line; do true "DEBUG: test would evaluate parse" "${dummy_line}" should_be_counter=$((should_be_counter + 1)) done < <(find "${fso_to_process}" -perm /u=s,g=s -print0 | xargs -I{} -0 stat -c "%n %a %U %G" {}) local line - while read -r line; do + while IFS="" read -r -d "" line; do true "line: ${line}" counter_actual="$((counter_actual + 1))" local arr file_name existing_mode existing_owner existing_group - IFS=" " read -r -a arr <<< "${line}" + read -r -a arr <<< "${line}" file_name="${arr[0]}" existing_mode="${arr[1]}" existing_owner="${arr[2]}" @@ -153,7 +153,7 @@ add_nosuid_statoverride_entry() { local is_exact_whitelisted is_exact_whitelisted="" - for white_list_entry in ${exact_white_list}; do + for white_list_entry in "${exact_white_list[@]:-}"; do if test "${file_name}" = "${white_list_entry}"; then is_exact_whitelisted="true" ## Stop looping through the whitelist. @@ -163,7 +163,7 @@ add_nosuid_statoverride_entry() { local is_match_whitelisted is_match_whitelisted="" - for matchwhite_list_entry in ${match_white_list}; do + for matchwhite_list_entry in "${match_white_list[@]:-}"; do if echo "${file_name}" | grep --quiet --fixed-strings "${matchwhite_list_entry}"; then is_match_whitelisted="true" ## Stop looping through the match_white_list. @@ -173,7 +173,7 @@ add_nosuid_statoverride_entry() { local is_disable_whitelisted is_disable_whitelisted="" - for disablematch_list_entry in ${disable_white_list:-}; do + for disablematch_list_entry in "${disable_white_list[@]:-}"; do if echo "${file_name}" | grep --quiet --fixed-strings "${disablematch_list_entry}"; then is_disable_whitelisted="true" ## Stop looping through the disablewhitelist. @@ -234,10 +234,9 @@ add_nosuid_statoverride_entry() { ## Sanity test. if test ! "${should_be_counter}" = "${counter_actual}"; then - echo "INFO: fso_to_process: '${fso_to_process}' | counter_actual : '${counter_actual}'" - echo "INFO: fso_to_process: '${fso_to_process}' | should_be_counter: '${should_be_counter}'" + echo "INFO: file system object (parsed/wanted): '${fso_to_process}': (${counter_actual}/${should_be_counter})" + echo "ERROR: expected number of files to be parsed was not met." >&2 exit_code=202 - echo "ERROR: counter does not check out." >&2 fi } @@ -249,7 +248,7 @@ set_file_perms() { continue fi - if [[ "${line}" =~ ^# ]]; then + if [[ "${line}" =~ ^\s*# ]]; then continue fi @@ -291,23 +290,21 @@ set_file_perms() { local fso_without_trailing_slash fso_without_trailing_slash="${fso%/}" - if test "${mode_from_config}" = "disablewhitelist"; then - ## TODO: test/add white spaces inside file name support - disable_white_list+="${fso} " - continue - fi - - if test "${mode_from_config}" = "exactwhitelist"; then - ## TODO: test/add white spaces inside file name support - exact_white_list+="${fso} " - continue - fi - - if test "${mode_from_config}" = "matchwhitelist"; then - ## TODO: test/add white spaces inside file name support - match_white_list+="${fso} " - continue - fi + ## TODO: test/add white spaces inside file name support + case "${mode_from_config}" in + disablewhitelist) + disable_white_list+=("${fso}") + continue + ;; + exactwhitelist) + exact_white_list+=("${fso}") + continue + ;; + matchwhitelist) + match_white_list+=("${fso}") + continue + ;; + esac if test ! -e "${fso}"; then true "INFO: fso: '${fso}' - does not exist. This is likely normal." @@ -358,7 +355,7 @@ set_file_perms() { fi local arr file_name existing_mode existing_owner existing_group - IFS=" " read -r -a arr <<< "${stat_output}" + read -r -a arr <<< "${stat_output}" file_name="${arr[0]}" existing_mode="${arr[1]}" existing_owner="${arr[2]}" @@ -548,7 +545,7 @@ spare() { fi local line - while read -r line; do + while IFS="" read -r -d "" line; do ## example line: ## root root 4755 /usr/lib/eject/dmcrypt-get-device From 7ee1ea2cc7dd62feee3243d64b414130e68d35e9 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Mon, 22 Jul 2024 17:06:07 +0200 Subject: [PATCH 2/5] Unify functions that evaluate commands --- usr/bin/permission-hardener | 58 +++++++++++++++---------------------- 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/usr/bin/permission-hardener b/usr/bin/permission-hardener index 026e290..1011372 100755 --- a/usr/bin/permission-hardener +++ b/usr/bin/permission-hardener @@ -14,33 +14,23 @@ dpkg_admindir_parameter_existing_mode="--admindir ${store_dir}/existing_mode" dpkg_admindir_parameter_new_mode="--admindir ${store_dir}/new_mode" echo_wrapper_ignore() { - echo "INFO: run: $*" - "$@" 2>/dev/null || true -} - -echo_wrapper_silent_ignore() { - #echo "INFO: run: $@" + if test "${1}" = "verbose"; then + echo "INFO: run: $*" + fi + shift "$@" 2>/dev/null || true } echo_wrapper_audit() { - echo "INFO: run: $*" + if test "${1}" = "verbose"; then + echo "INFO: run: $*" + fi + shift return_code=0 "$@" || { return_code="$?" exit_code=203 - echo "ERROR: above command failed with exit code '${return_code}'! calling function name: '${FUNCNAME[1]}'" >&2 - } -} - -echo_wrapper_silent_audit() { - #echo "run (debugging): $@" - return_code=0 - "$@" || - { - return_code="$?" - exit_code=204 echo "ERROR: above command '$*' failed with exit code '${return_code}'! calling function name: '${FUNCNAME[1]}'" >&2 } } @@ -52,7 +42,7 @@ make_store_dir(){ } sanity_tests() { - echo_wrapper_silent_audit which \ + echo_wrapper_audit silent which \ capsh getcap setcap stat find dpkg-statoverride getent xargs grep 1>/dev/null } @@ -205,7 +195,7 @@ add_nosuid_statoverride_entry() { ## Save existing_mode in separate database. ## Not using --update as not intending to enforce existing_mode. # shellcheck disable=SC2086 - echo_wrapper_silent_audit dpkg-statoverride ${dpkg_admindir_parameter_existing_mode} --add "${existing_owner}" "${existing_group}" "${existing_mode}" "${file_name}" + echo_wrapper_audit silent dpkg-statoverride ${dpkg_admindir_parameter_existing_mode} --add "${existing_owner}" "${existing_group}" "${existing_mode}" "${file_name}" fi ## No need to check "dpkg-statoverride --list" for existing entries. @@ -214,18 +204,18 @@ add_nosuid_statoverride_entry() { ## and re-add. ## Remove from real database. - echo_wrapper_silent_ignore dpkg-statoverride --remove "${file_name}" + echo_wrapper_ignore silent dpkg-statoverride --remove "${file_name}" ## Remove from separate database. # shellcheck disable=SC2086 - echo_wrapper_silent_ignore dpkg-statoverride ${dpkg_admindir_parameter_new_mode} --remove "${file_name}" + echo_wrapper_ignore silent dpkg-statoverride ${dpkg_admindir_parameter_new_mode} --remove "${file_name}" ## Add to real database and use --update to make changes on disk. - echo_wrapper_audit dpkg-statoverride --add --update "${existing_owner}" "${existing_group}" "${new_mode}" "${file_name}" + echo_wrapper_audit verbose dpkg-statoverride --add --update "${existing_owner}" "${existing_group}" "${new_mode}" "${file_name}" ## Not using --update as this is only for recording. # shellcheck disable=SC2086 - echo_wrapper_silent_audit dpkg-statoverride ${dpkg_admindir_parameter_new_mode} --add "${existing_owner}" "${existing_group}" "${new_mode}" "${file_name}" + echo_wrapper_audit silent dpkg-statoverride ${dpkg_admindir_parameter_new_mode} --add "${existing_owner}" "${existing_group}" "${new_mode}" "${file_name}" ## /usr/lib will hit ARG_MAX if using bash 'shopt -s globstar' and '/usr/lib/**'. ## Using 'find' with '-perm /u=s,g=s' is faster and avoids ARG_MAX. @@ -414,20 +404,20 @@ set_file_perms() { ## Save existing_mode in separate database. ## Not using --update as not intending to enforce existing_mode. # shellcheck disable=SC2086 - echo_wrapper_silent_audit dpkg-statoverride ${dpkg_admindir_parameter_existing_mode} --add "${existing_owner}" "${existing_group}" "${existing_mode}" "${fso_without_trailing_slash}" + echo_wrapper_audit silent dpkg-statoverride ${dpkg_admindir_parameter_existing_mode} --add "${existing_owner}" "${existing_group}" "${existing_mode}" "${fso_without_trailing_slash}" fi # shellcheck disable=SC2086 - echo_wrapper_silent_ignore dpkg-statoverride ${dpkg_admindir_parameter_new_mode} --remove "${fso_without_trailing_slash}" + echo_wrapper_ignore silent dpkg-statoverride ${dpkg_admindir_parameter_new_mode} --remove "${fso_without_trailing_slash}" ## Remove from and add to real database. - echo_wrapper_silent_ignore dpkg-statoverride --remove "${fso_without_trailing_slash}" - echo_wrapper_audit dpkg-statoverride --add --update "${owner_from_config}" "${group_from_config}" "${mode_from_config}" "${fso_without_trailing_slash}" + echo_wrapper_ignore silent dpkg-statoverride --remove "${fso_without_trailing_slash}" + echo_wrapper_audit verbose dpkg-statoverride --add --update "${owner_from_config}" "${group_from_config}" "${mode_from_config}" "${fso_without_trailing_slash}" ## Save in separate database. ## Not using --update as this is only for saving. # shellcheck disable=SC2086 - echo_wrapper_silent_audit dpkg-statoverride ${dpkg_admindir_parameter_new_mode} --add "${owner_from_config}" "${group_from_config}" "${mode_from_config}" "${fso_without_trailing_slash}" + echo_wrapper_audit silent dpkg-statoverride ${dpkg_admindir_parameter_new_mode} --add "${owner_from_config}" "${group_from_config}" "${mode_from_config}" "${fso_without_trailing_slash}" fi else true "There is no fso entry. Therefore add one." @@ -439,16 +429,16 @@ set_file_perms() { ## Save existing_mode in separate database. ## Not using --update as not intending to enforce existing_mode. # shellcheck disable=SC2086 - echo_wrapper_silent_audit dpkg-statoverride ${dpkg_admindir_parameter_existing_mode} --add "${existing_owner}" "${existing_group}" "${existing_mode}" "${fso_without_trailing_slash}" + echo_wrapper_audit silent dpkg-statoverride ${dpkg_admindir_parameter_existing_mode} --add "${existing_owner}" "${existing_group}" "${existing_mode}" "${fso_without_trailing_slash}" fi ## Add to real database. - echo_wrapper_audit dpkg-statoverride --add --update "${owner_from_config}" "${group_from_config}" "${mode_from_config}" "${fso_without_trailing_slash}" + echo_wrapper_audit verbose dpkg-statoverride --add --update "${owner_from_config}" "${group_from_config}" "${mode_from_config}" "${fso_without_trailing_slash}" ## Save in separate database. ## Not using --update as this is only for saving. # shellcheck disable=SC2086 - echo_wrapper_silent_audit dpkg-statoverride ${dpkg_admindir_parameter_new_mode} --add "${owner_from_config}" "${group_from_config}" "${mode_from_config}" "${fso_without_trailing_slash}" + echo_wrapper_audit silent dpkg-statoverride ${dpkg_admindir_parameter_new_mode} --add "${owner_from_config}" "${group_from_config}" "${mode_from_config}" "${fso_without_trailing_slash}" fi fi if test -z "${capability_from_config}"; then @@ -462,7 +452,7 @@ set_file_perms() { ## The value of the capability argument is not permitted for a file. Or ## the file is not a regular (non-symlink) file ## Therefore use echo_wrapper_ignore. - echo_wrapper_ignore setcap -r "${fso}" + echo_wrapper_ignore verbose setcap -r "${fso}" getcap_output="$(getcap "${fso}")" if test -n "${getcap_output}"; then exit_code=205 @@ -477,7 +467,7 @@ set_file_perms() { ## feature request: dpkg-statoverride: support for capabilities ## https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=502580 - echo_wrapper_audit setcap "${capability_from_config}+ep" "${fso}" + echo_wrapper_audit verbose setcap "${capability_from_config}+ep" "${fso}" fi done <"${config_file}" true "INFO: END parsing config_file: '${config_file}'" From 06fbcdac1de6f1830d911f05a4f7c14fd522fad4 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Tue, 23 Jul 2024 09:55:02 +0200 Subject: [PATCH 3/5] Prettify log messages --- usr/bin/permission-hardener | 107 +++++++++++++++++------------------- 1 file changed, 49 insertions(+), 58 deletions(-) diff --git a/usr/bin/permission-hardener b/usr/bin/permission-hardener index 1011372..6a78a28 100755 --- a/usr/bin/permission-hardener +++ b/usr/bin/permission-hardener @@ -31,7 +31,7 @@ echo_wrapper_audit() { { return_code="$?" exit_code=203 - echo "ERROR: above command '$*' failed with exit code '${return_code}'! calling function name: '${FUNCNAME[1]}'" >&2 + echo "ERROR: command '$*' failed with exit code '${return_code}'! calling function name: '${FUNCNAME[1]}'" >&2 } } @@ -56,13 +56,13 @@ add_nosuid_statoverride_entry() { local dummy_line while IFS="" read -r -d "" dummy_line; do - true "DEBUG: test would evaluate parse" "${dummy_line}" + true "DEBUG: test would parse line:" "${dummy_line}" should_be_counter=$((should_be_counter + 1)) done < <(find "${fso_to_process}" -perm /u=s,g=s -print0 | xargs -I{} -0 stat -c "%n %a %U %G" {}) local line while IFS="" read -r -d "" line; do - true "line: ${line}" + true "DEBUG: line: ${line}" counter_actual="$((counter_actual + 1))" local arr file_name existing_mode existing_owner existing_group @@ -73,23 +73,23 @@ add_nosuid_statoverride_entry() { existing_group="${arr[3]}" if test "${#arr[@]}" = 0; then - echo "ERROR: arr is empty. line: '${line}'" >&2 + echo "ERROR: line is empty: '${line}'" >&2 continue fi if test -z "${file_name}"; then - echo "ERROR: file_name is empty. line: '${line}'" >&2 + echo "ERROR: file name is empty. line: '${line}'" >&2 continue fi if test -z "${existing_mode}"; then - echo "ERROR: existing_mode is empty. line: '${line}'" >&2 + echo "ERROR: existing mode is empty. line: '${line}'" >&2 continue fi if test -z "${existing_owner}"; then - echo "ERROR: existing_owner is empty. line: '${line}'" >&2 + echo "ERROR: existing owner is empty. line: '${line}'" >&2 continue fi if test -z "${existing_group}"; then - echo "ERROR: existing_group is empty. line: '${line}'" >&2 + echo "ERROR: existing group is empty. line: '${line}'" >&2 continue fi @@ -99,12 +99,12 @@ add_nosuid_statoverride_entry() { if test -h "${file_name}"; then ## https://forums.whonix.org/t/disable-suid-binaries/7706/14 - true "skip symlink: ${file_name}" + true "DEBUG: skip symlink: ${file_name}" continue fi if test -d "${file_name}"; then - true "skip directory: ${file_name}" + true "DEBUG: skip directory: ${file_name}" continue fi @@ -171,26 +171,27 @@ add_nosuid_statoverride_entry() { fi done + clean_output="setuid=${setuid_output} setgid=${setsgid_output} existing_mode=${existing_mode} new_mode=${new_mode} file='${file_name}'" if test "${whitelists_disable_all:-}" = "true"; then - true "INFO: whitelists_disable_all=true - ${setuid_output} ${setsgid_output} found - file_name: '${file_name}' | existing_mode: '${existing_mode}'" + echo "INFO: whitelists_disable_all=true ${clean_output}" elif test "${is_disable_whitelisted}" = "true"; then - true "INFO: white list disabled - ${setuid_output} ${setsgid_output} found - file_name: '${file_name}' | existing_mode: '${existing_mode}'" + true "INFO: white list disabled ${clean_output}" else if test "${is_exact_whitelisted}" = "true"; then - true "INFO: SKIP whitelisted - ${setuid_output} ${setsgid_output} found - file_name: '${file_name}' | existing_mode: '${existing_mode}'" + true "INFO: is_exact_whitelisted=true ${clean_output}" continue fi if test "${is_match_whitelisted}" = "true"; then - true "INFO: SKIP matchwhitelisted - ${setuid_output} ${setsgid_output} found - file_name: '${file_name}' | existing_mode: '${existing_mode}' | matchwhite_list_entry: '${matchwhite_list_entry}'" + true "INFO: is_match_whitelisted=true ${clean_output} matchwhite_list_entry: '${matchwhite_list_entry}'" continue fi fi - echo "INFO: ${setuid_output} ${setsgid_output} found - file_name: '${file_name}' | existing_mode: '${existing_mode}' | new_mode: '${new_mode}'" + echo "INFO: ${clean_output}" # shellcheck disable=SC2086 if dpkg-statoverride ${dpkg_admindir_parameter_existing_mode} --list "${file_name}" >/dev/null; then - true "OK Existing mode already saved previously. Not saving again." + true "INFO: Existing mode already saved previously. Not saving again." else ## Save existing_mode in separate database. ## Not using --update as not intending to enforce existing_mode. @@ -224,14 +225,14 @@ add_nosuid_statoverride_entry() { ## Sanity test. if test ! "${should_be_counter}" = "${counter_actual}"; then - echo "INFO: file system object (parsed/wanted): '${fso_to_process}': (${counter_actual}/${should_be_counter})" + echo "INFO: file (parsed/wanted): '${fso_to_process}': (${counter_actual}/${should_be_counter})" echo "ERROR: expected number of files to be parsed was not met." >&2 exit_code=202 fi } set_file_perms() { - true "INFO: START parsing config_file: '${config_file}'" + true "INFO: START parsing config file: '${config_file}'" local line while read -r line || test -n "${line}"; do if test -z "${line}"; then @@ -243,10 +244,10 @@ set_file_perms() { fi if [[ "${line}" =~ [0-9a-zA-Z/] ]]; then - true "OK line contains only white listed characters." + true "INFO: line contains only white listed characters." else exit_code=200 - echo "ERROR: cannot parse line with invalid character. line: '${line}'" >&2 + echo "ERROR: cannot parse line with invalid character in line: '${line}'" >&2 ## Safer to exit with error in this case. ## https://forums.whonix.org/t/disable-suid-binaries/7706/59 exit "${exit_code}" @@ -254,7 +255,7 @@ set_file_perms() { if test "${line}" = 'whitelists_disable_all=true'; then whitelists_disable_all=true - echo "INFO: whitelists_disable_all=true - all whitelists disabled." + echo "INFO: whitelists_disable_all=true" continue fi @@ -262,7 +263,7 @@ set_file_perms() { local mode_from_config owner_from_config group_from_config capability_from_config if ! read -r fso mode_from_config owner_from_config group_from_config capability_from_config <<<"${line}"; then exit_code=201 - echo "ERROR: cannot parse. line: '${line}'" >&2 + echo "ERROR: cannot parse line: '${line}'" >&2 ## Debugging. du -hs /tmp || true echo "test -w /tmp: '$(test -w /tmp)'" >&2 || true @@ -297,7 +298,7 @@ set_file_perms() { esac if test ! -e "${fso}"; then - true "INFO: fso: '${fso}' - does not exist. This is likely normal." + true "INFO: file does not exist: '${fso}'" continue fi @@ -311,21 +312,21 @@ set_file_perms() { local string_length_of_mode_from_config string_length_of_mode_from_config="${#mode_from_config}" if test "${string_length_of_mode_from_config}" -gt "4"; then - echo "ERROR: Mode '${mode_from_config}' is invalid!" >&2 + echo "ERROR: Invalid mode: '${mode_from_config}'" >&2 continue fi if test "${string_length_of_mode_from_config}" -lt "3"; then - echo "ERROR: Mode '${mode_from_config}' is invalid!" >&2 + echo "ERROR: Invalid mode: '${mode_from_config}'" >&2 continue fi if ! grep --quiet --fixed-strings "${owner_from_config}:" "${store_dir}/private/passwd"; then - echo "ERROR: owner_from_config '${owner_from_config}' does not exist!" >&2 + echo "ERROR: owner from config does not exist: '${owner_from_config}'" >&2 continue fi if ! grep --quiet --fixed-strings "${group_from_config}:" "${store_dir}/private/group"; then - echo "ERROR: group_from_config '${group_from_config}' does not exist!" >&2 + echo "ERROR: group from config does not exist: '${group_from_config}'" >&2 continue fi @@ -340,7 +341,7 @@ set_file_perms() { local stat_output stat_output="" if ! stat_output="$(stat -c "%n %a %U %G" "${fso_without_trailing_slash}")"; then - echo "ERROR: failed to run 'stat' for fso_without_trailing_slash: '${fso_without_trailing_slash}'!" >&2 + echo "ERROR: failed to run 'stat' on file: '${fso_without_trailing_slash}'!" >&2 continue fi @@ -352,15 +353,15 @@ set_file_perms() { existing_group="${arr[3]}" if test "${#arr[@]}" = 0; then - echo "ERROR: arr is empty. stat_output: '${stat_output}' | line: '${line}'" >&2 + echo "ERROR: line is empty. Stat output: '${stat_output}', line: '${line}'" >&2 continue fi if test -z "${file_name}"; then - echo "ERROR: file_name is empty. stat_output: '${stat_output}' | line: '${line}'" >&2 + echo "ERROR: file name is empty. Stat output: '${stat_output}', line: '${line}'" >&2 continue fi if test -z "${existing_mode}"; then - echo "ERROR: existing_mode is empty. stat_output: '${stat_output}' | line: '${line}'" >&2 + echo "ERROR: existing mode is empty. Stat output: '${stat_output}', line: '${line}'" >&2 continue fi if test -z "${existing_owner}"; then @@ -368,7 +369,7 @@ set_file_perms() { continue fi if test -z "${existing_group}"; then - echo "ERROR: ${existing_group} is empty. stat_output: '${stat_output}' | line: '${line}'" >&2 + echo "ERROR: existing group is empty. Stat output: '${stat_output}', line: '${line}'" >&2 continue fi @@ -387,19 +388,19 @@ set_file_perms() { } if test "${dpkg_statoverride_list_exit_code}" = "0"; then - true "There is an fso entry. Check if owner/group/mode match." + true "INFO: There is an fso entry. Check if owner/group/mode matches." local grep_line grep_line="${owner_from_config} ${group_from_config} ${mode_for_grep} ${fso_without_trailing_slash}" if echo "${dpkg_statoverride_list_output}" | grep --quiet --fixed-strings "${grep_line}"; then - true "OK The owner/group/mode matches. No further action required." + true "INFO: The owner/group/mode matches. No further action required." else - true "The owner/group/mode do not match, therefore remove and re-add the entry to update it." + true "INFO: The owner/group/mode does not match, removing and re-adding the entry to update it." ## fso_without_trailing_slash instead of fso to prevent ## "dpkg-statoverride: warning: stripping trailing /" # shellcheck disable=SC2086 if dpkg-statoverride ${dpkg_admindir_parameter_existing_mode} --list "${fso_without_trailing_slash}" >/dev/null; then - true "OK Existing mode already saved previously. No need to save again." + true "INFO: Existing mode already saved previously. Not saving again." else ## Save existing_mode in separate database. ## Not using --update as not intending to enforce existing_mode. @@ -420,11 +421,11 @@ set_file_perms() { echo_wrapper_audit silent dpkg-statoverride ${dpkg_admindir_parameter_new_mode} --add "${owner_from_config}" "${group_from_config}" "${mode_from_config}" "${fso_without_trailing_slash}" fi else - true "There is no fso entry. Therefore add one." + true "INFO: There is no fso entry, adding one." # shellcheck disable=SC2086 if dpkg-statoverride ${dpkg_admindir_parameter_existing_mode} --list "${fso_without_trailing_slash}" >/dev/null; then - true "OK Existing mode already saved previously. No need to save again." + true "INFO: Existing mode already saved previously. Not saving again." else ## Save existing_mode in separate database. ## Not using --update as not intending to enforce existing_mode. @@ -456,12 +457,12 @@ set_file_perms() { getcap_output="$(getcap "${fso}")" if test -n "${getcap_output}"; then exit_code=205 - echo "ERROR: removing capabilities for fso '${fso}' failed!" >&2 + echo "ERROR: removing capabilities failed. File: '${fso}'" >&2 continue fi else if ! capsh --print | grep --fixed-strings "Bounding set" | grep --quiet "${capability_from_config}"; then - echo "ERROR: capability_from_config '${capability_from_config}' does not exist!" >&2 + echo "ERROR: capability from config does not exist: '${capability_from_config}'" >&2 continue fi @@ -470,7 +471,7 @@ set_file_perms() { echo_wrapper_audit verbose setcap "${capability_from_config}+ep" "${fso}" fi done <"${config_file}" - true "INFO: END parsing config_file: '${config_file}'" + true "INFO: END parsing config file: '${config_file}'" } parse_config_folder() { @@ -513,11 +514,8 @@ apply() { parse_config_folder echo "\ -INFO: To compare the current and previous permission modes: - Install 'meld' (or preferred diff tool) for comparison of file mode changes: +INFO: 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 - - Use 'meld' or another diff tool to view the differences: meld ${store_dir}/existing_mode/statoverride ${store_dir}/new_mode/statoverride" } @@ -545,7 +543,7 @@ spare() { echo "ERROR: cannot parse line: ${line}" >&2 continue fi - true "owner: '${owner}' group: '${group}' mode: '${mode}' file_name: '${file_name}'" + true "INFO: owner=${owner} group=${group} mode=${mode} file_name='${file_name}'" if test "${remove_file}" = "all"; then verbose="" @@ -573,7 +571,7 @@ spare() { # shellcheck disable=SC2086 chmod ${verbose} "${mode}" "${file_name}" || exit_code=203 else - echo "INFO: file_name: '${file_name}' - does not exist. This is likely normal." + echo "INFO: file doesn't exist: '${file_name}'" fi dpkg-statoverride --remove "${file_name}" &>/dev/null || true @@ -593,18 +591,11 @@ spare() { if test "$(cat "${store_dir}/remove_one")" = "false"; then echo "INFO: no file was removed. - File '${remove_file}' has not been removed from SUID Disabler and Permission Hardener during this invocation of this program. + File '${remove_file}' has not been removed from SUID Disabler and Permission Hardener during this invocation. This is expected if already done earlier. - Note: This is expected if already done earlier. - - Note: This program expects the full path to the file. Example: - $0 disable /usr/bin/newgrp - - The following syntax will not work: - $0 disable program-name - - The following example will not work: - $0 disable newgrp + 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 From aa99de68d307cd88462665424996d9b730ab5087 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Tue, 23 Jul 2024 18:46:47 +0200 Subject: [PATCH 4/5] Log output with defined levels --- usr/bin/permission-hardener | 140 ++++++++++++++++++++---------------- 1 file changed, 79 insertions(+), 61 deletions(-) diff --git a/usr/bin/permission-hardener b/usr/bin/permission-hardener index 6a78a28..9eff886 100755 --- a/usr/bin/permission-hardener +++ b/usr/bin/permission-hardener @@ -13,25 +13,33 @@ store_dir="/var/lib/permission-hardener" dpkg_admindir_parameter_existing_mode="--admindir ${store_dir}/existing_mode" dpkg_admindir_parameter_new_mode="--admindir ${store_dir}/new_mode" +log_level=info +# shellcheck disable=SC1091 +source /usr/libexec/helper-scripts/log_run_die.sh + echo_wrapper_ignore() { if test "${1}" = "verbose"; then - echo "INFO: run: $*" + shift + log info "Run: $*" + else + shift fi - shift "$@" 2>/dev/null || true } echo_wrapper_audit() { if test "${1}" = "verbose"; then - echo "INFO: run: $*" + shift + log info "Run: $*" + else + shift fi - shift return_code=0 "$@" || { return_code="$?" exit_code=203 - echo "ERROR: command '$*' failed with exit code '${return_code}'! calling function name: '${FUNCNAME[1]}'" >&2 + log error "Command '$*' failed with exit code '${return_code}'! calling function name: '${FUNCNAME[1]}'" >&2 } } @@ -55,14 +63,13 @@ add_nosuid_statoverride_entry() { counter_actual=0 local dummy_line - while IFS="" read -r -d "" dummy_line; do - true "DEBUG: test would parse line:" "${dummy_line}" + while IFS="" read -r dummy_line; do + log info "Test would parse line: ${dummy_line}" should_be_counter=$((should_be_counter + 1)) done < <(find "${fso_to_process}" -perm /u=s,g=s -print0 | xargs -I{} -0 stat -c "%n %a %U %G" {}) local line - while IFS="" read -r -d "" line; do - true "DEBUG: line: ${line}" + while IFS="" read -r line; do counter_actual="$((counter_actual + 1))" local arr file_name existing_mode existing_owner existing_group @@ -73,23 +80,23 @@ add_nosuid_statoverride_entry() { existing_group="${arr[3]}" if test "${#arr[@]}" = 0; then - echo "ERROR: line is empty: '${line}'" >&2 + log error "Line is empty: '${line}'" >&2 continue fi if test -z "${file_name}"; then - echo "ERROR: file name is empty. line: '${line}'" >&2 + log error "File name is empty. line: '${line}'" >&2 continue fi if test -z "${existing_mode}"; then - echo "ERROR: existing mode is empty. line: '${line}'" >&2 + log error "Existing mode is empty. line: '${line}'" >&2 continue fi if test -z "${existing_owner}"; then - echo "ERROR: existing owner is empty. line: '${line}'" >&2 + log error "Existing owner is empty. line: '${line}'" >&2 continue fi if test -z "${existing_group}"; then - echo "ERROR: existing group is empty. line: '${line}'" >&2 + log error "Existing group is empty. line: '${line}'" >&2 continue fi @@ -99,12 +106,12 @@ add_nosuid_statoverride_entry() { if test -h "${file_name}"; then ## https://forums.whonix.org/t/disable-suid-binaries/7706/14 - true "DEBUG: skip symlink: ${file_name}" + log info "Skip symlink: ${file_name}" continue fi if test -d "${file_name}"; then - true "DEBUG: skip directory: ${file_name}" + log info "Skip directory: ${file_name}" continue fi @@ -144,6 +151,9 @@ add_nosuid_statoverride_entry() { local is_exact_whitelisted is_exact_whitelisted="" for white_list_entry in "${exact_white_list[@]:-}"; do + if test -z "${white_list_entry}"; then + continue + fi if test "${file_name}" = "${white_list_entry}"; then is_exact_whitelisted="true" ## Stop looping through the whitelist. @@ -154,6 +164,9 @@ add_nosuid_statoverride_entry() { local is_match_whitelisted is_match_whitelisted="" for matchwhite_list_entry in "${match_white_list[@]:-}"; do + if test -z "${matchwhite_list_entry}"; then + continue + fi if echo "${file_name}" | grep --quiet --fixed-strings "${matchwhite_list_entry}"; then is_match_whitelisted="true" ## Stop looping through the match_white_list. @@ -164,6 +177,9 @@ add_nosuid_statoverride_entry() { local is_disable_whitelisted is_disable_whitelisted="" for disablematch_list_entry in "${disable_white_list[@]:-}"; do + if test -z "${disablematch_list_entry}"; then + continue + fi if echo "${file_name}" | grep --quiet --fixed-strings "${disablematch_list_entry}"; then is_disable_whitelisted="true" ## Stop looping through the disablewhitelist. @@ -171,27 +187,26 @@ add_nosuid_statoverride_entry() { fi done + clean_output_prefix="Managing (S|G)UID of line:" clean_output="setuid=${setuid_output} setgid=${setsgid_output} existing_mode=${existing_mode} new_mode=${new_mode} file='${file_name}'" if test "${whitelists_disable_all:-}" = "true"; then - echo "INFO: whitelists_disable_all=true ${clean_output}" + log info "${clean_output_prefix} whitelists_disable_all=true ${clean_output}" elif test "${is_disable_whitelisted}" = "true"; then - true "INFO: white list disabled ${clean_output}" + log info "${clean_output_prefix} is_disable_whitelisted=true ${clean_output}" else if test "${is_exact_whitelisted}" = "true"; then - true "INFO: is_exact_whitelisted=true ${clean_output}" + log info "${clean_output_prefix} is_exact_whitelisted=true ${clean_output}" continue fi if test "${is_match_whitelisted}" = "true"; then - true "INFO: is_match_whitelisted=true ${clean_output} matchwhite_list_entry: '${matchwhite_list_entry}'" + log info "${clean_output_prefix} is_match_whitelisted=true matchwhite_list_entry=${matchwhite_list_entry} ${clean_output}" continue fi fi - echo "INFO: ${clean_output}" - # shellcheck disable=SC2086 if dpkg-statoverride ${dpkg_admindir_parameter_existing_mode} --list "${file_name}" >/dev/null; then - true "INFO: Existing mode already saved previously. Not saving again." + log info "Existing mode already saved previously. Not saving again." else ## Save existing_mode in separate database. ## Not using --update as not intending to enforce existing_mode. @@ -225,14 +240,14 @@ add_nosuid_statoverride_entry() { ## Sanity test. if test ! "${should_be_counter}" = "${counter_actual}"; then - echo "INFO: file (parsed/wanted): '${fso_to_process}': (${counter_actual}/${should_be_counter})" - echo "ERROR: expected number of files to be parsed was not met." >&2 + log info "File (parsed/wanted): '${fso_to_process}': (${counter_actual}/${should_be_counter})" + log error "Expected number of files to be parsed was not met." >&2 exit_code=202 fi } set_file_perms() { - true "INFO: START parsing config file: '${config_file}'" + log info "START parsing config file: ${config_file}" local line while read -r line || test -n "${line}"; do if test -z "${line}"; then @@ -243,11 +258,9 @@ set_file_perms() { continue fi - if [[ "${line}" =~ [0-9a-zA-Z/] ]]; then - true "INFO: line contains only white listed characters." - else + if ! [[ "${line}" =~ [0-9a-zA-Z/] ]]; then exit_code=200 - echo "ERROR: cannot parse line with invalid character in line: '${line}'" >&2 + 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}" @@ -255,7 +268,7 @@ set_file_perms() { if test "${line}" = 'whitelists_disable_all=true'; then whitelists_disable_all=true - echo "INFO: whitelists_disable_all=true" + log info "whitelists_disable_all=true" continue fi @@ -263,7 +276,7 @@ set_file_perms() { local mode_from_config owner_from_config group_from_config capability_from_config if ! read -r fso mode_from_config owner_from_config group_from_config capability_from_config <<<"${line}"; then exit_code=201 - echo "ERROR: cannot parse line: '${line}'" >&2 + log error "Cannot parse line: '${line}'" >&2 ## Debugging. du -hs /tmp || true echo "test -w /tmp: '$(test -w /tmp)'" >&2 || true @@ -272,6 +285,8 @@ set_file_perms() { exit "${exit_code}" fi + log info "Parsing line: fso=${fso} mode_from_config=${mode_from_config} owner_from_config=${owner_from_config} group_from_config=${group_from_config} capability_from_config=${capability_from_config}" + ## Debugging. #echo "line: '${line}'" #echo "fso: '${fso}'" @@ -282,6 +297,7 @@ set_file_perms() { fso_without_trailing_slash="${fso%/}" ## TODO: test/add white spaces inside file name support + declare -g disable_white_list exact_white_list match_white_list case "${mode_from_config}" in disablewhitelist) disable_white_list+=("${fso}") @@ -298,7 +314,7 @@ set_file_perms() { esac if test ! -e "${fso}"; then - true "INFO: file does not exist: '${fso}'" + log warn "File does not exist: '${fso}'" continue fi @@ -312,21 +328,21 @@ set_file_perms() { local string_length_of_mode_from_config string_length_of_mode_from_config="${#mode_from_config}" if test "${string_length_of_mode_from_config}" -gt "4"; then - echo "ERROR: Invalid mode: '${mode_from_config}'" >&2 + log error "Invalid mode: '${mode_from_config}'" >&2 continue fi if test "${string_length_of_mode_from_config}" -lt "3"; then - echo "ERROR: Invalid mode: '${mode_from_config}'" >&2 + log error "Invalid mode: '${mode_from_config}'" >&2 continue fi if ! grep --quiet --fixed-strings "${owner_from_config}:" "${store_dir}/private/passwd"; then - echo "ERROR: owner from config does not exist: '${owner_from_config}'" >&2 + log error "Owner from config does not exist: '${owner_from_config}'" >&2 continue fi if ! grep --quiet --fixed-strings "${group_from_config}:" "${store_dir}/private/group"; then - echo "ERROR: group from config does not exist: '${group_from_config}'" >&2 + log error "Group from config does not exist: '${group_from_config}'" >&2 continue fi @@ -341,7 +357,7 @@ set_file_perms() { local stat_output stat_output="" if ! stat_output="$(stat -c "%n %a %U %G" "${fso_without_trailing_slash}")"; then - echo "ERROR: failed to run 'stat' on file: '${fso_without_trailing_slash}'!" >&2 + log error "Failed to run 'stat' on file: '${fso_without_trailing_slash}'!" >&2 continue fi @@ -353,23 +369,23 @@ set_file_perms() { existing_group="${arr[3]}" if test "${#arr[@]}" = 0; then - echo "ERROR: line is empty. Stat output: '${stat_output}', line: '${line}'" >&2 + log error "Line is empty. Stat output: '${stat_output}', line: '${line}'" >&2 continue fi if test -z "${file_name}"; then - echo "ERROR: file name is empty. Stat output: '${stat_output}', line: '${line}'" >&2 + log error "File name is empty. Stat output: '${stat_output}', line: '${line}'" >&2 continue fi if test -z "${existing_mode}"; then - echo "ERROR: existing mode is empty. Stat output: '${stat_output}', line: '${line}'" >&2 + log error "Existing mode is empty. Stat output: '${stat_output}', line: '${line}'" >&2 continue fi if test -z "${existing_owner}"; then - echo "ERROR: existing_owner is empty. stat_output: '${stat_output}' | line: '${line}'" >&2 + log error "Existing_owner is empty. stat_output: '${stat_output}' | line: '${line}'" >&2 continue fi if test -z "${existing_group}"; then - echo "ERROR: existing group is empty. Stat output: '${stat_output}', line: '${line}'" >&2 + log error "Existing group is empty. Stat output: '${stat_output}', line: '${line}'" >&2 continue fi @@ -388,19 +404,18 @@ set_file_perms() { } if test "${dpkg_statoverride_list_exit_code}" = "0"; then - true "INFO: There is an fso entry. Check if owner/group/mode matches." local grep_line grep_line="${owner_from_config} ${group_from_config} ${mode_for_grep} ${fso_without_trailing_slash}" if echo "${dpkg_statoverride_list_output}" | grep --quiet --fixed-strings "${grep_line}"; then - true "INFO: The owner/group/mode matches. No further action required." + log info "The owner/group/mode matches fso entry. No further action required." else - true "INFO: The owner/group/mode does not match, removing and re-adding the entry to update it." + log info "The owner/group/mode does not match fso entry, updating entry." ## fso_without_trailing_slash instead of fso to prevent ## "dpkg-statoverride: warning: stripping trailing /" # shellcheck disable=SC2086 if dpkg-statoverride ${dpkg_admindir_parameter_existing_mode} --list "${fso_without_trailing_slash}" >/dev/null; then - true "INFO: Existing mode already saved previously. Not saving again." + log info "Existing mode already saved previously. Not saving again." else ## Save existing_mode in separate database. ## Not using --update as not intending to enforce existing_mode. @@ -421,11 +436,11 @@ set_file_perms() { echo_wrapper_audit silent dpkg-statoverride ${dpkg_admindir_parameter_new_mode} --add "${owner_from_config}" "${group_from_config}" "${mode_from_config}" "${fso_without_trailing_slash}" fi else - true "INFO: There is no fso entry, adding one." + log info "There is no fso entry, adding one." # shellcheck disable=SC2086 if dpkg-statoverride ${dpkg_admindir_parameter_existing_mode} --list "${fso_without_trailing_slash}" >/dev/null; then - true "INFO: Existing mode already saved previously. Not saving again." + log info "Existing mode already saved previously. Not saving again." else ## Save existing_mode in separate database. ## Not using --update as not intending to enforce existing_mode. @@ -457,12 +472,12 @@ set_file_perms() { getcap_output="$(getcap "${fso}")" if test -n "${getcap_output}"; then exit_code=205 - echo "ERROR: removing capabilities failed. File: '${fso}'" >&2 + log error "Removing capabilities failed. File: '${fso}'" >&2 continue fi else if ! capsh --print | grep --fixed-strings "Bounding set" | grep --quiet "${capability_from_config}"; then - echo "ERROR: capability from config does not exist: '${capability_from_config}'" >&2 + log error "Capability from config does not exist: '${capability_from_config}'" >&2 continue fi @@ -470,8 +485,9 @@ set_file_perms() { ## https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=502580 echo_wrapper_audit verbose setcap "${capability_from_config}+ep" "${fso}" fi + done <"${config_file}" - true "INFO: END parsing config file: '${config_file}'" + log info "END parsing config file: ${config_file}" } parse_config_folder() { @@ -504,6 +520,7 @@ parse_config_folder() { /usr/local/etc/permission-hardening.d/*.conf do set_file_perms + done } @@ -513,8 +530,8 @@ apply() { sanity_tests parse_config_folder - echo "\ -INFO: To compare the current and previous permission modes, install 'meld' (or preferred diff tool) for comparison of file mode changes: + log info "\ +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" } @@ -529,21 +546,22 @@ spare() { dpkg_admindir_parameter_new_mode="--admindir ${store_dir}/new_mode" if test ! -f "${store_dir}/existing_mode/statoverride"; then + true debug "Stat file does not exist, hardening was not applied not applied before" return 0 fi local line - while IFS="" read -r -d "" line; do + while read -r line; do ## example line: ## root root 4755 /usr/lib/eject/dmcrypt-get-device local owner group mode file_name if ! read -r owner group mode file_name <<< "${line}"; then exit_code=201 - echo "ERROR: cannot parse line: ${line}" >&2 + log error "Cannot parse line: ${line}" >&2 continue fi - true "INFO: owner=${owner} group=${group} mode=${mode} file_name='${file_name}'" + log info "Parsing line: owner=${owner} group=${group} mode=${mode} file_name='${file_name}'" if test "${remove_file}" = "all"; then verbose="" @@ -571,7 +589,7 @@ spare() { # shellcheck disable=SC2086 chmod ${verbose} "${mode}" "${file_name}" || exit_code=203 else - echo "INFO: file doesn't exist: '${file_name}'" + log warn "File does not exist: '${file_name}'" fi dpkg-statoverride --remove "${file_name}" &>/dev/null || true @@ -589,7 +607,7 @@ spare() { if test ! "${remove_file}" = "all"; then if test "$(cat "${store_dir}/remove_one")" = "false"; then - echo "INFO: no file was removed. + log info "No file was removed. File '${remove_file}' has not been removed from SUID Disabler and Permission Hardener during this invocation. This is expected if already done earlier. @@ -617,7 +635,7 @@ spare() { check_root(){ if test "$(id -u)" != "0"; then - echo "ERROR: Not running as root, aborting." + log error "Not running as root, aborting." exit 1 fi } @@ -647,7 +665,7 @@ case "${1:-}" in esac if test "${exit_code}" != "0"; then - echo "ERROR: Exiting with non-zero exit code: '${exit_code}'" >&2 + log error "Exiting with non-zero exit code: '${exit_code}'" >&2 fi exit "${exit_code}" From 8be21b6eff40fdd3909ef63468463fc52e8bf45f Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Tue, 23 Jul 2024 19:36:12 +0200 Subject: [PATCH 5/5] Handle newlines in file names --- usr/bin/permission-hardener | 45 +++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/usr/bin/permission-hardener b/usr/bin/permission-hardener index 9eff886..87f4307 100755 --- a/usr/bin/permission-hardener +++ b/usr/bin/permission-hardener @@ -63,40 +63,47 @@ add_nosuid_statoverride_entry() { counter_actual=0 local dummy_line - while IFS="" read -r dummy_line; do + while IFS="" read -r -d "" dummy_line; do log info "Test would parse line: ${dummy_line}" should_be_counter=$((should_be_counter + 1)) - done < <(find "${fso_to_process}" -perm /u=s,g=s -print0 | xargs -I{} -0 stat -c "%n %a %U %G" {}) + done < <(find "${fso_to_process}" -perm /u=s,g=s -print0) local line - while IFS="" read -r line; do + while IFS="" read -r -d "" line; do counter_actual="$((counter_actual + 1))" local arr file_name existing_mode existing_owner existing_group - read -r -a arr <<< "${line}" - file_name="${arr[0]}" - existing_mode="${arr[1]}" - existing_owner="${arr[2]}" - existing_group="${arr[3]}" + file_name="${line}" + stat_output="$(stat -c "%a %U %G" "${line}")" + read -r -a arr <<< "${stat_output}" + existing_mode="${arr[0]}" + existing_owner="${arr[1]}" + existing_group="${arr[2]}" if test "${#arr[@]}" = 0; then log error "Line is empty: '${line}'" >&2 continue fi if test -z "${file_name}"; then - log error "File name is empty. line: '${line}'" >&2 + log error "File name is empty in line: ${line}" >&2 continue fi if test -z "${existing_mode}"; then - log error "Existing mode is empty. line: '${line}'" >&2 + log error "Existing mode is empty in line: ${line}" >&2 continue fi if test -z "${existing_owner}"; then - log error "Existing owner is empty. line: '${line}'" >&2 + log error "Existing owner is empty in line: ${line}" >&2 continue fi if test -z "${existing_group}"; then - log error "Existing group is empty. line: '${line}'" >&2 + log error "Existing group is empty in line: ${line}" >&2 + continue + fi + + ## dpkg-statoverride: error: path may not contain newlines + if [[ "${file_name}" == *$'\n'* ]]; then + log warn "Skipping file name that contains newlines: ${file_name}" >&2 continue fi @@ -236,7 +243,7 @@ add_nosuid_statoverride_entry() { ## /usr/lib will hit ARG_MAX if using bash 'shopt -s globstar' and '/usr/lib/**'. ## Using 'find' with '-perm /u=s,g=s' is faster and avoids ARG_MAX. ## https://forums.whonix.org/t/disable-suid-binaries/7706/17 - done < <(find "${fso_to_process}" -perm /u=s,g=s -print0 | xargs -I{} -0 stat -c "%n %a %U %G" {}) + done < <(find "${fso_to_process}" -perm /u=s,g=s -print0) ## Sanity test. if test ! "${should_be_counter}" = "${counter_actual}"; then @@ -356,17 +363,17 @@ set_file_perms() { local stat_output stat_output="" - if ! stat_output="$(stat -c "%n %a %U %G" "${fso_without_trailing_slash}")"; then + if ! stat_output="$(stat -c "%a %U %G" "${fso_without_trailing_slash}")"; then log error "Failed to run 'stat' on file: '${fso_without_trailing_slash}'!" >&2 continue fi local arr file_name existing_mode existing_owner existing_group read -r -a arr <<< "${stat_output}" - file_name="${arr[0]}" - existing_mode="${arr[1]}" - existing_owner="${arr[2]}" - existing_group="${arr[3]}" + file_name="${fso_without_trailing_slash}" + existing_mode="${arr[0]}" + existing_owner="${arr[1]}" + existing_group="${arr[2]}" if test "${#arr[@]}" = 0; then log error "Line is empty. Stat output: '${stat_output}', line: '${line}'" >&2 @@ -589,7 +596,7 @@ spare() { # shellcheck disable=SC2086 chmod ${verbose} "${mode}" "${file_name}" || exit_code=203 else - log warn "File does not exist: '${file_name}'" + log warn "File does not exist: ${file_name}" fi dpkg-statoverride --remove "${file_name}" &>/dev/null || true