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
1027 lines
30 KiB
Bash
Executable File
1027 lines
30 KiB
Bash
Executable File
#!/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
|