Files
nextcloud/scan_bad_signature.sh
T
chrisandClaude Sonnet 4.6 8bfc4d04b7 feat(ux): show temp log file paths after YES confirmation in all scripts
After the user confirms with YES, each script now prints the paths of
the two temporary files that are written continuously during the run,
so they can be monitored with tail -f without having to know the paths
by heart. Affects scan_, recover_, restore_, recreate_bad_signature.sh.
recreate shows per-account log file names since it writes one per user.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GfXh5sRbEaXiEX6KjAPivc
2026-09-16 09:20:55 +02:00

1027 lines
30 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
CUR_IFS=$IFS
script_name="$(basename $(realpath $0))"
script_dir="$(dirname $(realpath $0))"
conf_dir="${script_dir}/conf"
snippet_dir="${script_dir}/snippets"
report_dir="${script_dir}/reports"
declare -a unsorted_website_arr
declare -a website_arr
declare -a unsorted_account_arr
declare -a account_arr
declare -a unsorted_selected_user_arr
declare -a selected_user_arr
LOCK_DIR="/tmp/${script_name%%.*}.LOCK"
log_file="${LOCK_DIR}/${script_name%%.*}.log"
run_date=$(date +%Y-%m-%d-%H%M)
# =============
# --- Some functions
# =============
usage() {
[[ -n "$1" ]] && error "$1"
[[ $terminal ]] && echo -e "
\033[1mUsage:\033[m
$(basename $0) -s <website>
\033[1mDescription\033[m
Scans the encrypted files of one, several or all Nextcloud accounts
and reports every file that fails to decrypt (e.g. 'Bad Signature'
errors thrown by Server-Side Encryption). Every file is read once
and its content discarded - the script is READ-ONLY and changes,
moves or deletes nothing.
You will be asked interactively which account(s) to scan:
- a blank separated list of account names
- or 'all' to scan every existing account
\033[1mOptions\033[m
-s <website>
The site of the nextcloud instance.
\033[1mExample:\033[m
Runs this script on system 'cloud-01.oopen.de'
$(basename $0) -s cloud-01.oopen.de
"
clean_up 1
}
clean_up() {
# Perform program exit housekeeping
[[ -n "$scan_php_file" ]] && rm -f "$scan_php_file" 2> /dev/null
rm -rf "$LOCK_DIR"
blank_line
exit $1
}
is_number() {
return $(test ! -z "${1##*[!0-9]*}" > /dev/null 2>&1);
# - also possible
# -
#[[ ! -z "${1##*[!0-9]*}" ]] && return 0 || return 1
#return $([[ ! -z "${1##*[!0-9]*}" ]])
}
echononl(){
if $terminal ; then
echo X\\c > /tmp/shprompt$$
if [ `wc -c /tmp/shprompt$$ | awk '{print $1}'` -eq 1 ]; then
echo -e -n "$*\\c" 1>&2
else
echo -e -n "$*" 1>&2
fi
rm /tmp/shprompt$$
fi
}
echo_done() {
if $terminal ; then
echo -e "\033[75G[ \033[32mdone\033[m ]"
fi
}
echo_ok() {
if $terminal ; then
echo -e "\033[75G[ \033[32mok\033[m ]"
fi
}
echo_warning() {
if $terminal ; then
echo -e "\033[75G[ \033[33m\033[1mwarn\033[m ]"
fi
}
echo_failed(){
if $terminal ; then
echo -e "\033[75G[ \033[1;31mfailed\033[m ]"
fi
}
echo_skipped() {
if $terminal ; then
echo -e "\033[75G[ \033[37mskipped\033[m ]"
fi
}
echo_funde() {
if $terminal ; then
echo -e "\033[75G[ \033[33m\033[1mFunde!\033[m ]"
fi
}
fatal (){
echo ""
echo ""
if $terminal ; then
echo -e " [ \033[31m\033[1mFatal\033[m ]: \033[37m\033[1m$*\033[m"
echo ""
echo -e " \033[31m\033[1mScript will be interrupted..\033[m!"
else
echo " [ Fatal ]: $*"
echo ""
echo " Script was terminated...."
fi
clean_up 1
}
error(){
echo ""
if $terminal ; then
echo -e " [ \033[31m\033[1mError\033[m ]: $*"
else
echo " [ Error ]: $*"
fi
echo ""
}
warn (){
if $terminal ; then
echo ""
echo -e " [ \033[33m\033[1mWarning\033[m ]: $*"
echo ""
fi
}
info (){
if $terminal ; then
echo ""
echo -e " [ \033[32m\033[1mInfo\033[m ]: $*"
echo ""
fi
}
# - Remove leading/trailling whitespaces
# -
trim() {
local var="$*"
var="${var#"${var%%[![:space:]]*}"}" # remove leading whitespace characters
var="${var%"${var##*[![:space:]]}"}" # remove trailing whitespace characters
echo -n "$var"
}
## - Check if a given array (parameter 2) contains a given string (parameter 1)
## -
containsElement () {
local e
for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0; done
return 1
}
## - Percentage (1 decimal) of parameter 1 (part) against parameter 2 (total)
## - Returns "0.0" if total is empty, zero or non-numeric.
## -
calc_percent() {
local _part="$1"
local _total="$2"
if [[ -z "$_total" ]] || ! [[ "$_total" =~ ^[0-9]+$ ]] || [[ "$_total" -eq 0 ]] ; then
echo "0.0"
else
awk -v b="$_part" -v t="$_total" 'BEGIN { printf "%.1f", (b/t)*100 }'
fi
}
blank_line() {
if $terminal ; then
echo ""
fi
}
# - Running in a terminal?
# -
if [[ -t 1 ]] ; then
terminal=true
else
terminal=false
fi
# ----------
# - Jobhandling
# ----------
if pgrep -f "$(basename $0)" | grep -q -v $$ ; then
msg="A previos instance of script \"`basename $0`\" seems already be running."
echo ""
if $terminal ; then
echo -e "[ \033[31m\033[1mFatal\033[m ]: $msg"
echo ""
echo -e " \033[31m\033[1mScript was interupted\033[m!"
else
echo " [ Fatal ]: $msg"
echo ""
echo " Script was interupted!"
fi
echo
exit 1
else
if [[ -d "$LOCK_DIR" ]] ; then
rm -rf "$LOCK_DIR" 2> /dev/null
fi
fi
# - If job already runs, stop execution..
# -
if mkdir "$LOCK_DIR" 2> /dev/null ; then
# - Remove lockdir when the script finishes, or when it receives a signal
# -
trap clean_up SIGHUP SIGINT SIGTERM
else
msg="A previos instance of script \"`basename $0`\" seems already be running."
echo ""
if $terminal ; then
echo -e "[ \033[31m\033[1mFatal\033[m ]: $msg"
echo ""
echo -e " \033[31m\033[1mScript was interupted\033[m!"
else
echo " [ Fatal ]: $msg"
echo ""
echo " Script was interupted!"
fi
echo
exit 1
fi
# -------------
# - Read in Commandline arguments
# -------------
while getopts hs: opt ; do
case $opt in
h) usage ;;
s) WEBSITE=$OPTARG ;;
\?) usage
esac
done
if [[ -z "$WEBSITE" ]] ; then
while IFS='' read -r -d '' _conf_file ; do
source $_conf_file
if [[ -n "$WEBSITE" ]] ; then
unsorted_website_arr+=("${WEBSITE}:$_conf_file")
fi
WEBSITE=""
done < <(find "${conf_dir}" -maxdepth 1 -type f -name "*.conf" -print0)
# - Sort array
# -
IFS=$'\n' website_arr=($(sort <<<"${unsorted_website_arr[*]}"))
# Which cloud instance (website) would you like to update
#
source ${snippet_dir}/get-cloud-instance-to-update.sh
else
while IFS='' read -r -d '' _conf_file ; do
if $(grep -E -q "WEBSITE=\"?${WEBSITE}\"?" ${_conf_file} 2> /dev/null) ; then
conf_file="${_conf_file}"
break
fi
done < <(find "${conf_dir}" -maxdepth 1 -type f -name "*.conf" -print0)
fi
# - Reset IFS
# -
IFS=$CUR_IFS
DEFAULT_SRC_BASE_DIR="/usr/local/src/nextcloud"
DEFAULT_HTTP_USER="www-data"
DEFAULT_HTTP_GROUP="www-data"
DEFAULT_PHP_ENGINE='FPM'
blank_line
echononl " Include Configuration file '$(basename "${conf_file}")'.."
if [[ ! -f $conf_file ]]; then
echo_skipped
fatal "Missing configuration file '$conf_file'."
else
source $conf_file
echo_ok
fi
DEFAULT_WEB_BASE_DIR="/var/www/${WEBSITE}"
[[ -n "$WEB_BASE_DIR" ]] || WEB_BASE_DIR=$DEFAULT_WEB_BASE_DIR
if [[ ! -d ${WEB_BASE_DIR} ]] ; then
fatal "Web base directory '$WEB_BASE_DIR' not found!"
fi
DATA_DIR="$(realpath ${WEB_BASE_DIR}/data)"
[[ -n "$PHP_ENGINE" ]] || PHP_ENGINE=$DEFAULT_PHP_ENGINE
INSTALL_DIR="$(realpath ${WEB_BASE_DIR}/nextcloud)"
CURRENT_VERSION="$(basename $INSTALL_DIR | cut -d"-" -f2)"
# =============
# --- Some
# =============
# - Support systemd ?
# -
SYSTEMD_EXISTS=false
systemd=$(which systemd)
systemctl=$(which systemctl)
if [[ -n "$systemd" ]] || [[ -n "$systemctl" ]] ; then
SYSTEMD_EXISTS=true
fi
#clear
if $terminal ; then
echo ""
echo -e "\033[32m-----\033[m"
echo -e "Scan encrypted files for \033[1mBad Signature\033[m errors on system \033[1m${WEB_BASE_DIR}\033[m"
echo -e "\033[1m
Read-only: every file is decrypted once and the content discarded,
reporting every file that fails Server-Side Encryption's signature
check. Nothing is changed, moved or deleted.\033[m"
echo -e "\033[32m-----\033[m"
fi
# =============
# --- Some checks
# =============
DEFAULT_HTTP_USER="www-data"
DEFAULT_HTTP_GROUP="www-data"
NGINX_IS_ENABLED=false
APACHE2_IS_ENABLED=false
# Get Webservice environment as IS_HTTPD_RUNNING, HTTP_USER, HTTP_GROUP..
#
source ${snippet_dir}/get-webservice-environment.sh
# Check PHP Version
#
source ${snippet_dir}/get-php-major-version.sh
# Get full qualified PHP command
#
source ${snippet_dir}/get-path-of-php-command.sh
if [[ ! -x "$PHP_BIN" ]]; then
fatal "No PHP binary found!"
fi
# =============
# --- Check Server-Side Encryption status
# =============
blank_line
echononl " Check Server-Side Encryption status.."
_encryption_status_out="$(su -c "$PHP_BIN $INSTALL_DIR/occ encryption:status" -s /bin/bash $HTTP_USER 2> "$log_file")"
ENCRYPTION_ENABLED="unknown"
if [[ -s "$log_file" ]] ; then
echo_failed
error "$(cat "$log_file")"
elif echo "$_encryption_status_out" | grep -qiE 'enabled:[[:space:]]*true' ; then
echo_ok
ENCRYPTION_ENABLED="yes"
else
echo_warning
ENCRYPTION_ENABLED="no"
fi
# =============
# --- Determine existing accounts
# =============
blank_line
echononl " Get list of current accounts.."
mapfile -t unsorted_account_arr < <(su -c "${PHP_BIN} ${INSTALL_DIR}/occ user:list" -s /bin/bash $HTTP_USER 2> ${log_file} | awk -F'[:[:space:]]+' '{print $3}' 2>> ${log_file} )
if [[ $? -gt 0 ]] || [[ -s "${log_file}" ]] ; then
echo_failed
fatal "$(cat $log_file)"
else
echo_ok
> "$log_file"
fi
# - Sort array
# -
IFS=$'\n' account_arr=($(sort <<<"${unsorted_account_arr[*]}"))
IFS=$CUR_IFS
# =============
# --- Ask which account(s) to scan
# =============
echo ""
echo ""
echo -e "\033[32m-----\033[m"
echo ""
echo -e " Existing accounts on \033[1m${WEBSITE}\033[m\n"
echo -en "\033[33m"
printf " %s\n" "${account_arr[@]}"
echo -en "\033[m"
blank_line
echo ""
echo ""
echo -e "\033[32m-----\033[m"
echo ""
echo -e " Which account(s) would you like to scan for 'Bad Signature' errors?"
echo ""
echo " - enter a list separated by spaces"
echo -e " - or type \033[33mall\033[m to scan every existing account"
echo ""
SELECTED_USERS=
while [[ -z "$(trim ${SELECTED_USERS})" ]] ; do
echononl " Account(s) to scan: "
read SELECTED_USERS
if [[ -z "$(trim ${SELECTED_USERS})" ]] ; then
echo -e "\n\t\033[33m\033[1mEntry must not be empty. Type \033[m\033[1mall\033[33m to scan every account.\033[m\n"
continue
fi
if [[ "${SELECTED_USERS,,}" = "all" ]] ; then
selected_user_arr=("${account_arr[@]}")
break
fi
IFS=' ' read -ra unsorted_selected_user_arr <<< "${SELECTED_USERS}"
_all_valid=true
for _user in "${unsorted_selected_user_arr[@]}" ; do
if ! containsElement "${_user}" "${account_arr[@]}" ; then
echo -e "\n\t\033[33m\033[1m No account \033[m\033[1m${_user}\033[33m present!\033[m - Try again..\n"
SELECTED_USERS=""
_all_valid=false
break
fi
done
$_all_valid || continue
# - Sort array
# -
IFS=$'\n' selected_user_arr=($(sort <<<"${unsorted_selected_user_arr[*]}"))
IFS=$CUR_IFS
done
# =============
# --- Write out the (embedded) PHP scan script into the Nextcloud
# --- install directory - it needs to sit next to 'occ' since it
# --- bootstraps Nextcloud the same way occ does.
# =============
scan_php_file="${INSTALL_DIR}/.occ_scan_bad_signature_$$.php"
cat > "$scan_php_file" <<'PHP_SCAN_EOF'
<?php
/**
* .occ_scan_bad_signature_*.php
*
* READ-ONLY Diagnose-Skript fuer Nextcloud Server-Side Encryption.
* Wird von occ_scan_bad_signature.sh automatisch erzeugt und nach
* Gebrauch wieder geloescht - nicht von Hand aufrufen/aendern.
*
* Was es tut:
* Geht durch alle Dateien EINES uebergebenen Benutzers und liest
* jede Datei einmal vollstaendig (im Stream, ohne den Inhalt zu
* speichern). Dadurch wird fuer jede verschluesselte Datei die
* vollstaendige Entschluesselung inkl. Signaturpruefung jedes
* Blocks erzwungen - genau der Schritt, der beim "Bad Signature"
* Fehler fehlschlaegt.
*
* Was es NICHT tut:
* Es schreibt, aendert oder loescht NICHTS. Reiner Lesezugriff.
*
* Aufruf:
* php .occ_scan_bad_signature_*.php <benutzername> [--before=YYYY-MM-DD] [--after=YYYY-MM-DD]
*
* Ausgabe (TSV, nach STDOUT):
* user \t path \t size_bytes \t mtime \t error
* Eine Zeile pro Datei, bei der das Lesen fehlgeschlagen ist.
* Fortschritts-/Debug-/Zusammenfassungsmeldungen gehen nach STDERR.
*/
error_reporting(E_ALL);
ini_set('display_errors', '1');
fwrite(STDERR, "DEBUG: Skript gestartet, PID=" . getmypid() . "\n");
// Faengt fatale, NICHT als Throwable auftretende Fehler ab (z.B. Memory
// Limit exhausted), die sonst komplett stumm bleiben wuerden.
register_shutdown_function(function () {
$err = error_get_last();
if ($err !== null && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
fwrite(STDERR, "DEBUG: FATAL bei Shutdown: [{$err['type']}] {$err['message']} in {$err['file']}:{$err['line']}\n");
} else {
fwrite(STDERR, "DEBUG: Shutdown ohne erkannten Fatal-Error.\n");
}
});
// WICHTIG: Muss VOR dem require von lib/base.php gesetzt werden.
// Ohne diese Konstante fuehrt lib/base.php am Ende automatisch
// OC::handleRequest() aus (als waere dies ein normaler HTTP-Request).
define('OC_CONSOLE', 1);
$oldWorkingDir = getcwd();
if ($oldWorkingDir === false) {
fwrite(STDERR, "Konnte aktuelles Arbeitsverzeichnis nicht ermitteln. Bitte mit absolutem Pfad aufrufen.\n");
exit(1);
}
chdir(__DIR__);
fwrite(STDERR, "DEBUG: vor require lib/base.php\n");
require_once __DIR__ . '/lib/base.php';
fwrite(STDERR, "DEBUG: nach require lib/base.php - Bootstrap abgeschlossen\n");
// Nextcloud's own bootstrap (lib/base.php) unconditionally calls
// set_time_limit(3600) as part of require_once above - this OVERRIDES
// whatever -d max_execution_time was passed on the PHP command line,
// since it is a later, explicit runtime call. Undo that here, now that
// the require is done, so a full-content scan of a large account isn't
// killed after exactly one hour regardless of the CLI flag.
set_time_limit(0);
chdir($oldWorkingDir);
if (function_exists('posix_getuid') && posix_getuid() === 0) {
fwrite(STDERR, "Bitte NICHT als root ausfuehren, sondern als Webserver-User, z.B.:\n");
fwrite(STDERR, " sudo -u www-data php " . __FILE__ . "\n");
exit(1);
}
// Ab hier alles in einem Catch-All, damit ein Throwable garantiert HIER
// landet und nicht beim (auf STDOUT/STDERR ggf. still bleibenden)
// globalen Nextcloud-Handler.
try {
fwrite(STDERR, "DEBUG: hole UserManager/RootFolder ...\n");
// Argumente einlesen: erstes nicht-Options-Argument = Benutzername
// (optional, sonst alle Benutzer). --before=YYYY-MM-DD prueft nur
// Dateien, die VOR diesem Datum zuletzt geaendert wurden - spart bei
// grossen Accounts viel Zeit, sobald der Cutoff-Zeitpunkt bekannt ist.
$onlyUser = null;
$beforeTs = null;
$afterTs = null;
foreach (array_slice($argv, 1) as $arg) {
if (str_starts_with($arg, '--before=')) {
$beforeTs = strtotime(substr($arg, 9));
} elseif (str_starts_with($arg, '--after=')) {
$afterTs = strtotime(substr($arg, 8));
} elseif ($onlyUser === null) {
$onlyUser = $arg;
}
}
if ($beforeTs !== null) {
fwrite(STDERR, "DEBUG: Filter --before " . date('Y-m-d', $beforeTs) . "\n");
}
if ($afterTs !== null) {
fwrite(STDERR, "DEBUG: Filter --after " . date('Y-m-d', $afterTs) . "\n");
}
// Neuere Nextcloud-Versionen haben die alten Komfort-Methoden wie
// getUserManager()/getRootFolder() auf \OC::$server entfernt - der
// unterstuetzte Weg ist jetzt der PSR-11-Container ->get(...).
$userManager = \OC::$server->get(\OCP\IUserManager::class);
$rootFolder = \OC::$server->get(\OCP\Files\IRootFolder::class);
fwrite(STDERR, "DEBUG: UserManager/RootFolder OK\n");
$total = 0;
$skipped = 0;
$bad = 0;
$usersScanned = 0;
echo "user\tpath\tsize_bytes\tmtime\terror\n";
flush();
/**
* Rekursiv durch einen Ordner gehen und jede Datei vollstaendig lesen
* (ausser sie liegt ausserhalb von --before/--after).
*/
$scanFolder = function (\OCP\Files\Folder $folder, string $uid) use (&$scanFolder, &$total, &$skipped, &$bad, $beforeTs, $afterTs) {
foreach ($folder->getDirectoryListing() as $node) {
if ($node instanceof \OCP\Files\Folder) {
$scanFolder($node, $uid);
continue;
}
if (!($node instanceof \OCP\Files\File)) {
continue;
}
$mtimeRaw = $node->getMTime();
if (($beforeTs !== null && $mtimeRaw >= $beforeTs) || ($afterTs !== null && $mtimeRaw <= $afterTs)) {
$skipped++;
continue;
}
$total++;
$path = $node->getPath();
try {
$stream = $node->fopen('r');
if ($stream === false) {
throw new \RuntimeException('fopen lieferte false');
}
while (!feof($stream)) {
$chunk = fread($stream, 8 * 1024 * 1024);
if ($chunk === false) {
throw new \RuntimeException('fread schlug mitten im Stream fehl');
}
}
fclose($stream);
} catch (\Throwable $e) {
$bad++;
$mtime = date('c', $node->getMTime());
$msg = str_replace(["\t", "\n", "\r"], ' ', $e->getMessage());
echo "$uid\t$path\t{$node->getSize()}\t$mtime\t" . get_class($e) . ": $msg\n";
flush();
}
}
};
$scanUser = function (\OCP\IUser $user) use (&$scanFolder, $rootFolder, &$usersScanned) {
$uid = $user->getUID();
fwrite(STDERR, "Scanne Benutzer: $uid ...\n");
\OC_Util::setupFS($uid);
try {
$userFolder = $rootFolder->getUserFolder($uid);
} catch (\Throwable $e) {
fwrite(STDERR, " Ueberspringe $uid: kann User-Folder nicht laden (" . $e->getMessage() . ")\n");
return;
}
$scanFolder($userFolder, $uid);
$usersScanned++;
};
if ($onlyUser !== null) {
fwrite(STDERR, "DEBUG: suche Benutzer '$onlyUser' ...\n");
$user = $userManager->get($onlyUser);
if ($user === null) {
fwrite(STDERR, "Benutzer '$onlyUser' nicht gefunden.\n");
exit(1);
}
fwrite(STDERR, "DEBUG: Benutzer gefunden, starte Scan ...\n");
$scanUser($user);
} else {
$userManager->callForAllUsers($scanUser);
}
fwrite(STDERR, "\nFertig. Benutzer gescannt: $usersScanned, Dateien geprueft: $total, uebersprungen (Filter): $skipped, fehlgeschlagen: $bad\n");
} catch (\Throwable $e) {
fwrite(STDERR, "\n!!! UNBEHANDELTE AUSNAHME !!!\n");
fwrite(STDERR, get_class($e) . ": " . $e->getMessage() . "\n");
fwrite(STDERR, "in " . $e->getFile() . ":" . $e->getLine() . "\n");
fwrite(STDERR, $e->getTraceAsString() . "\n");
exit(2);
}
PHP_SCAN_EOF
chmod 644 "$scan_php_file" 2> /dev/null
mkdir -p "$report_dir" 2> /dev/null
report_file="${report_dir}/bad_signature_${WEBSITE}_${run_date}.tsv"
{
echo "=================================================================="
echo " Bad-Signature-Scan ${WEBSITE} ${run_date}"
echo "=================================================================="
echo ""
echo -e "user\tpath\tsize_bytes\tmtime\terror"
} > "$report_file"
if $terminal ; then
echo ""
echo -e "\033[1;32mStarting Script for \033[1;37m${WEBSITE}\033[m"
echo ""
echo -e " Cloud instance to be scanned.........: $WEBSITE"
echo ""
echo -e " Current version of nextcloud.........: $CURRENT_VERSION"
echo ""
echo -e " Web base directory...................: $WEB_BASE_DIR"
echo -e " Install directory....................: $INSTALL_DIR"
echo -e " Data directory.......................: $DATA_DIR"
echo ""
echo -e " Webserver user.......................: $HTTP_USER"
echo -e " Webserver group......................: $HTTP_GROUP"
echo ""
echo -e " PHP command..........................: $PHP_BIN"
echo ""
echo -e " Account(s) to scan...................: \033[33m${selected_user_arr[*]}\033[m"
echo ""
echo -e " Report file...........................: $report_file"
echo ""
if [[ "$ENCRYPTION_ENABLED" = "no" ]] ; then
warn "Server-Side Encryption is NOT enabled on ${WEBSITE} - this scan only makes sense on instances with encryption switched on, so it will most likely find nothing."
fi
echo ""
echo -n " Type upper case 'YES' to continue executing with this parameters: "
read OK
if [[ "$OK" = "YES" ]] ; then
echo ""
echo ""
echo -e "\033[1;32mGoing to scan encrypted files for each selected account on \033[1;37m$WEBSITE \033[m"
else
fatal "Abort by user request - Answer as not 'YES'"
fi
fi
if $terminal ; then
echo ""
echo -e " \033[1mTemporäre Dateien – werden laufend aktualisiert:\033[m"
echo ""
echo -e " Fortschritt / Debug-Meldungen (PHP-Ausgabe auf STDERR):"
echo -e " \033[1mtail -f ${log_file}\033[m"
echo ""
echo -e " Rohergebnis pro Account (PHP-Ausgabe auf STDOUT, Datei für Datei):"
echo -e " \033[1mtail -f ${LOCK_DIR}/scan_<account>.tsv\033[m"
echo ""
fi
# -----
# - Main part of the script
# -----
if $terminal ; then
echo ""
echo ""
echo -e "\033[37m\033[1mMain part of the script\033[m"
echo ""
fi
declare -i total_checked=0
declare -i total_bad=0
declare -i total_failed_users=0
overall_date_min=""
overall_date_max=""
declare -A overall_month_counts=()
for _user in "${selected_user_arr[@]}" ; do
user_tsv="${LOCK_DIR}/scan_${_user}.tsv"
# - Gut sichtbarer Account-Abschnitt im Reportfile - wird IMMER
# - angelegt, auch wenn der Scan fuer diesen Account fehlschlaegt.
# -
_sep_len=${#_user}
[[ $_sep_len -lt 9 ]] && _sep_len=9
_sep_line="$(printf '%*s' "$_sep_len" '' | tr ' ' '-')"
{
echo ""
echo ""
echo "$_sep_line"
echo "$_user"
echo "$_sep_line"
} >> "$report_file"
echononl " Scanning account \033[1;37m${_user}\033[m.."
# -d max_execution_time=0: a full-content read of every file of a
# large account can easily run well over an hour; without this the
# PHP CLI process is killed by PHP's own execution-time limit
# (typically inherited from the webserver's php.ini) mid-scan, long
# before any actual error in the account's files.
su -c "$PHP_BIN -d max_execution_time=0 $scan_php_file $_user" -s /bin/bash $HTTP_USER > "$user_tsv" 2> "$log_file"
_rc=$?
if [[ $_rc -ne 0 ]]; then
echo_failed
error "$(cat "$log_file")"
echo " [ FEHLER beim Scan - siehe Server-Log ]" >> "$report_file"
(( total_failed_users++ ))
continue
fi
# - Kopfzeile weglassen und im Account-Abschnitt anhaengen
# -
tail -n +2 "$user_tsv" >> "$report_file"
_user_bad=$(tail -n +2 "$user_tsv" | wc -l)
(( total_bad += _user_bad ))
# - Zeitraum (min/max Datum) der Dateien mit 'Bad Signature' fuer
# - diesen Account aus der mtime-Spalte des TSV ermitteln, und
# - gleich in die Gesamt-Min/Max mit einfliessen lassen.
# -
if [[ $_user_bad -gt 0 ]] ; then
_dates_sorted="$(tail -n +2 "$user_tsv" | cut -f4 | cut -c1-10 | sort)"
_user_date_min="$(echo "$_dates_sorted" | head -n1)"
_user_date_max="$(echo "$_dates_sorted" | tail -n1)"
if [[ "$_user_date_min" = "$_user_date_max" ]] ; then
_user_zeitraum="$_user_date_min"
else
_user_zeitraum="${_user_date_min} bis ${_user_date_max}"
fi
if [[ -z "$overall_date_min" || "$_user_date_min" < "$overall_date_min" ]] ; then
overall_date_min="$_user_date_min"
fi
if [[ -z "$overall_date_max" || "$_user_date_max" > "$overall_date_max" ]] ; then
overall_date_max="$_user_date_max"
fi
# - Verteilung der betroffenen Dateien nach Monat (YYYY-MM), pro
# - Account und gleich mit-aggregiert fuer das Gesamtergebnis -
# - zeigt, ob es sich um vereinzelte Cluster oder einen
# - durchgehenden Fehlerstrom handelt.
# -
declare -A _user_month_counts=()
while IFS= read -r _d ; do
[[ -z "$_d" ]] && continue
_m="${_d:0:7}"
_user_month_counts["$_m"]=$(( ${_user_month_counts["$_m"]:-0} + 1 ))
overall_month_counts["$_m"]=$(( ${overall_month_counts["$_m"]:-0} + 1 ))
done <<< "$_dates_sorted"
_user_months_sorted=($(printf '%s\n' "${!_user_month_counts[@]}" | sort))
else
_user_zeitraum="-"
fi
# - Scan lief durch: bei Funden gelb hervorheben statt schlichtem 'ok'
# -
if [[ $_user_bad -gt 0 ]] ; then
echo_funde
else
echo_ok
fi
# - Leerzeile NACH der 'Scanning account ..'-Zeile, vor der
# - Account-Statistik.
# -
$terminal && echo ""
# - Kennzahlen aus der STDERR-Zusammenfassung herausziehen
# -
_summary="$(grep -E '^Fertig\.' "$log_file")"
_checked="$(echo "$_summary" | grep -oE 'Dateien geprueft: [0-9]+' | grep -oE '[0-9]+')"
_skipped="$(echo "$_summary" | grep -oE 'uebersprungen \(Filter\): [0-9]+' | grep -oE '[0-9]+')"
[[ -z "$_skipped" ]] && _skipped="0"
if [[ "$_checked" =~ ^[0-9]+$ ]] ; then
(( total_checked += _checked ))
_pct="$(calc_percent "$_user_bad" "$_checked")"
else
_checked="?"
_pct="?"
fi
# - Ergebnis-Block pro Account - gleiches Looking wie das
# - Gesamtergebnis am Ende (Konsole + Reportfile)
# -
if $terminal ; then
echo -e " \033[1;37mAccount ${_user}\033[m"
echo -e " Gescannte Dateien........................: ${_checked}"
echo -e " Uebersprungene Dateien...................: ${_skipped}"
if [[ $_user_bad -gt 0 ]] ; then
echo -e " Dateien mit 'Bad Signature'..............: \033[1;33m${_user_bad} (${_pct} %)\033[m"
echo -e " Zeitraum betroffener Dateien.............: \033[1;33m${_user_zeitraum}\033[m"
else
echo -e " Dateien mit 'Bad Signature'..............: ${_user_bad} (${_pct} %)"
echo -e " Zeitraum betroffener Dateien.............: ${_user_zeitraum}"
fi
if [[ $_user_bad -gt 0 ]] ; then
echo -e " Verteilung nach Monat:"
for _m in "${_user_months_sorted[@]}" ; do
printf " \033[1;33m%-7s\033[m : %d\n" "$_m" "${_user_month_counts[$_m]}"
done
fi
# - Leerzeile HINTER der Account-Statistik, damit sie nicht am
# - 'Scanning account ..' des naechsten Accounts klebt.
# -
echo ""
fi
{
echo " Account ${_user}"
echo " Gescannte Dateien........................: ${_checked}"
echo " Uebersprungene Dateien...................: ${_skipped}"
echo " Dateien mit 'Bad Signature'..............: ${_user_bad} (${_pct} %)"
echo " Zeitraum betroffener Dateien.............: ${_user_zeitraum}"
if [[ $_user_bad -gt 0 ]] ; then
echo " Verteilung nach Monat:"
for _m in "${_user_months_sorted[@]}" ; do
printf " %-7s : %d\n" "$_m" "${_user_month_counts[$_m]}"
done
fi
} >> "$report_file"
done
total_pct="$(calc_percent "$total_bad" "$total_checked")"
# - Zeitraum (min/max Datum) ueber alle Dateien mit 'Bad Signature'
# - hinweg, aus den bereits pro Account ermittelten Min/Max-Werten.
# -
if [[ -n "$overall_date_min" ]] ; then
if [[ "$overall_date_min" = "$overall_date_max" ]] ; then
overall_zeitraum="$overall_date_min"
else
overall_zeitraum="${overall_date_min} bis ${overall_date_max}"
fi
else
overall_zeitraum="-"
fi
overall_months_sorted=($(printf '%s\n' "${!overall_month_counts[@]}" | sort))
# - Gesamtergebnis ans Ende des Reportfiles - gleiches Looking wie
# - die Ergebnis-Bloecke pro Account
# -
{
echo ""
echo ""
echo "=================================================================="
echo " Gesamtergebnis"
echo "=================================================================="
echo ""
echo "Gescannte Accounts..........................: ${#selected_user_arr[@]}"
[[ $total_failed_users -gt 0 ]] && echo "Accounts mit Scan-Fehler...................: ${total_failed_users}"
echo "Gescannte Dateien insgesamt.................: ${total_checked}"
echo "Dateien mit 'Bad Signature' insgesamt.......: ${total_bad} (${total_pct} %)"
echo "Zeitraum betroffener Dateien insgesamt......: ${overall_zeitraum}"
if [[ $total_bad -gt 0 ]] ; then
echo "Verteilung betroffener Dateien nach Monat (alle Accounts):"
for _m in "${overall_months_sorted[@]}" ; do
printf " %-7s : %d\n" "$_m" "${overall_month_counts[$_m]}"
done
fi
} >> "$report_file"
blank_line
if $terminal ; then
echo -e "\033[37m\033[1mErgebnis\033[m"
echo ""
echo -e " Gescannte Accounts..........................: ${#selected_user_arr[@]}"
[[ $total_failed_users -gt 0 ]] && echo -e " Accounts mit Scan-Fehler...................: \033[1;31m${total_failed_users}\033[m"
echo -e " Gescannte Dateien insgesamt.................: ${total_checked}"
echo -e " Dateien mit 'Bad Signature' insgesamt.......: \033[1;31m${total_bad} (${total_pct} %)\033[m"
if [[ $total_bad -gt 0 ]] ; then
echo -e " Zeitraum betroffener Dateien insgesamt......: \033[1;31m${overall_zeitraum}\033[m"
echo -e " Verteilung betroffener Dateien nach Monat (alle Accounts):"
for _m in "${overall_months_sorted[@]}" ; do
printf " \033[1;31m%-7s\033[m : %d\n" "$_m" "${overall_month_counts[$_m]}"
done
else
echo -e " Zeitraum betroffener Dateien insgesamt......: ${overall_zeitraum}"
fi
echo -e " Report-Datei................................: reports/$(basename "${report_file}")"
echo ""
fi
clean_up 0