#!/usr/bin/env bash # STEP040 - Porting Presenze 215.6 # Audit mirato: calendario Silvelox che non salva # Server atteso: 215.6 / ax-accessi # Modalita': AUDIT ONLY - nessuna modifica applicativa, DB o Apache set -u set -o pipefail umask 077 STEP_NAME="STEP040_2156_SILVELOX_CALENDAR_SAVE_AUDIT_ONLY_VERIFIED_20260730" EXPECTED_HOST="${EXPECTED_HOST:-ax-accessi}" EXPECTED_IP_FRAGMENT="${EXPECTED_IP_FRAGMENT:-192.168.215.6}" TARGET_ROOT="${TARGET_ROOT:-/var/www/html/presenze.silvelox}" TARGET_HOST="${TARGET_HOST:-presenze.silvelox.axuno.it}" TARGET_DB="${TARGET_DB:-presenze_silvelox}" ALLOW_TEST="${ALLOW_TEST:-0}" START_TS="$(date +%Y%m%d_%H%M%S)" OUTPUT_ZIP="$PWD/${STEP_NAME}_${START_TS}.zip" WORKDIR="$(mktemp -d "$PWD/.${STEP_NAME}.XXXXXX")" REPORT="$WORKDIR/REPORT.txt" cleanup() { rm -rf -- "$WORKDIR" } trap cleanup EXIT INT TERM section() { printf '\n\n================================================================\n%s\n================================================================\n' "$1" >> "$REPORT" } append_cmd() { local title="$1" shift section "$title" printf '$' >> "$REPORT" printf ' %q' "$@" >> "$REPORT" printf '\n' >> "$REPORT" "$@" >> "$REPORT" 2>&1 || printf '[comando terminato con codice %s]\n' "$?" >> "$REPORT" } safe_tail_file() { local src="$1" local dst="$2" local lines="${3:-500}" if [[ -r "$src" ]]; then tail -n "$lines" -- "$src" > "$dst" 2>&1 || true fi } redact_stream() { sed -E \ -e "s/([Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd][[:space:]]*['\"]?[[:space:]]*[:=][[:space:]]*['\"]?)[^'\";,[:space:]]+/\\1***REDACTED***/g" \ -e "s/('password'[[:space:]]*=>[[:space:]]*')[^']*'/\\1***REDACTED***'/Ig" \ -e 's/(Authorization:[[:space:]]*Bearer[[:space:]]+)[^[:space:]]+/\1***REDACTED***/Ig' \ -e 's/(api[_-]?key[[:space:]]*[:=][[:space:]]*)[^[:space:]]+/\1***REDACTED***/Ig' } HOSTNAME_NOW="$(hostname 2>/dev/null || true)" IPS_NOW="$(hostname -I 2>/dev/null || true)" if [[ "$ALLOW_TEST" != "1" ]]; then if [[ "$HOSTNAME_NOW" != "$EXPECTED_HOST" && "$IPS_NOW" != *"$EXPECTED_IP_FRAGMENT"* ]]; then echo "ERRORE: server errato. Rilevato host='$HOSTNAME_NOW' IP='$IPS_NOW'; atteso 215.6 / ax-accessi." >&2 exit 10 fi if [[ ! -d "$TARGET_ROOT" ]]; then echo "ERRORE: sorgente Silvelox non trovata: $TARGET_ROOT" >&2 exit 11 fi fi { echo "$STEP_NAME" echo "DATA: $(date --iso-8601=seconds 2>/dev/null || date)" echo "SERVER ATTESO: 215.6 / ax-accessi" echo "ROOT: $TARGET_ROOT" echo "HOST: $TARGET_HOST" echo "DB ATTESO: $TARGET_DB" echo "MODALITA': AUDIT ONLY" echo "NESSUN RELOAD APACHE, NESSUNA QUERY DI SCRITTURA, NESSUNA MODIFICA FILE" } > "$REPORT" append_cmd "Identita' server" bash -lc 'hostname; hostname -f 2>/dev/null || true; hostname -I 2>/dev/null || true; id; uptime' append_cmd "Spazio disco e inode" bash -lc 'df -hT; echo; df -i' append_cmd "Versioni runtime" bash -lc 'apache2ctl -v 2>/dev/null || true; php -v 2>/dev/null || true; mysql --version 2>/dev/null || mariadb --version 2>/dev/null || true; curl --version 2>/dev/null | head -n 2 || true' section "Struttura e permessi principali" { stat -c '%A %a %U:%G %s %y %n' "$TARGET_ROOT" 2>&1 || true for p in \ "$TARGET_ROOT/templates_c" \ "$TARGET_ROOT/cache" \ "$TARGET_ROOT/tmp" \ "$TARGET_ROOT/temp" \ "$TARGET_ROOT/log" \ "$TARGET_ROOT/logs" \ "$TARGET_ROOT/components" \ "$TARGET_ROOT/libs"; do [[ -e "$p" ]] && stat -c '%A %a %U:%G %s %y %n' "$p" 2>&1 || true done echo echo "Test accesso www-data (sola verifica, nessuna scrittura):" for p in "$TARGET_ROOT" "$TARGET_ROOT/templates_c" "$TARGET_ROOT/cache" "$TARGET_ROOT/tmp" "$TARGET_ROOT/temp"; do if [[ -e "$p" ]]; then printf '%-80s read=' "$p" if runuser -u www-data -- test -r "$p" 2>/dev/null; then printf 'SI'; else printf 'NO'; fi printf ' execute=' if runuser -u www-data -- test -x "$p" 2>/dev/null; then printf 'SI'; else printf 'NO'; fi printf ' write=' if runuser -u www-data -- test -w "$p" 2>/dev/null; then printf 'SI'; else printf 'NO'; fi printf '\n' fi done } >> "$REPORT" 2>&1 append_cmd "Configurazione Apache e vhost" bash -lc "apache2ctl -S 2>&1; echo; grep -RniE --include='*.conf' '(${TARGET_HOST//./\\.}|presenze[.]silvelox)' /etc/apache2/sites-enabled /etc/apache2/sites-available 2>/dev/null | head -n 300 || true" append_cmd "Moduli Apache pertinenti" bash -lc "apache2ctl -M 2>/dev/null | grep -Ei 'proxy|fcgi|rewrite|headers|expires|session|php' || true" section "Test HTTP locale del portale" { for scheme in https http; do echo "--- $scheme://127.0.0.1 con Host: $TARGET_HOST ---" curl -ksS --max-time 15 -D - -o /dev/null "$scheme://127.0.0.1/" -H "Host: $TARGET_HOST" 2>&1 || true echo done } >> "$REPORT" section "Configurazione PHP sessioni e limiti" php -i 2>/dev/null | grep -Ei '^(Loaded Configuration File|Scan this dir|session.save_handler|session.save_path|session.gc_|session.cookie_|max_input_vars|max_execution_time|memory_limit|post_max_size|upload_max_filesize|date.timezone)' >> "$REPORT" || true section "Processi PHP-FPM e Apache" ps -eo user,pid,ppid,stat,etime,cmd --sort=user 2>/dev/null | grep -E '[a]pache2|[p]hp.*fpm' >> "$REPORT" || true section "File calendario / pianificazione candidati" if [[ -d "$TARGET_ROOT" ]]; then find "$TARGET_ROOT" -xdev -type f \ \( -iname '*calendar*' -o -iname '*calend*' -o -iname '*planning*' -o -iname '*schedule*' -o -iname '*turni*' -o -iname '*evento*' -o -iname '*event*' \) \ -printf '%TY-%Tm-%Td %TH:%TM:%TS %s %u:%g %m %p\n' 2>/dev/null \ | sort -r | head -n 1000 >> "$WORKDIR/calendar_candidates.txt" || true fi cat "$WORKDIR/calendar_candidates.txt" >> "$REPORT" 2>/dev/null || true section "File PHP/JS modificati recentemente" if [[ -d "$TARGET_ROOT" ]]; then find "$TARGET_ROOT" -xdev -type f \( -iname '*.php' -o -iname '*.js' -o -iname '*.html' \) -mtime -45 \ -printf '%TY-%Tm-%Td %TH:%TM:%TS %s %u:%g %m %p\n' 2>/dev/null \ | sort -r | head -n 1500 > "$WORKDIR/recent_code_files.txt" || true fi cat "$WORKDIR/recent_code_files.txt" >> "$REPORT" 2>/dev/null || true section "Ricerca endpoint e chiamate di salvataggio calendario" SEARCH_OUT="$WORKDIR/calendar_code_search.txt" : > "$SEARCH_OUT" if [[ -d "$TARGET_ROOT" ]]; then if command -v rg >/dev/null 2>&1; then rg -n -i --hidden \ --glob '!bower_components/**' --glob '!node_modules/**' --glob '!vendor/**' \ --glob '!templates_c/**' --glob '!cache/**' --glob '!*.min.js' \ -e 'calendar|calendario|fullcalendar|eventDrop|eventResize|eventClick|eventReceive|saveEvent|updateEvent|deleteEvent|ajax|\$http|fetch\(|INSERT[[:space:]]+INTO|UPDATE[[:space:]]+' \ "$TARGET_ROOT" 2>/dev/null | head -n 7000 > "$SEARCH_OUT" || true else grep -RniIE \ --exclude-dir=bower_components --exclude-dir=node_modules --exclude-dir=vendor \ --exclude-dir=templates_c --exclude-dir=cache --exclude='*.min.js' \ 'calendar|calendario|fullcalendar|eventDrop|eventResize|eventClick|eventReceive|saveEvent|updateEvent|deleteEvent|ajax|\$http|fetch\(|INSERT[[:space:]]+INTO|UPDATE[[:space:]]+' \ "$TARGET_ROOT" 2>/dev/null | head -n 7000 > "$SEARCH_OUT" || true fi fi cat "$SEARCH_OUT" >> "$REPORT" 2>/dev/null || true section "Riferimenti a token CSRF, sessione e JSON nelle aree calendario" if [[ -s "$SEARCH_OUT" ]]; then cut -d: -f1 "$SEARCH_OUT" | sort -u | head -n 500 > "$WORKDIR/calendar_file_list.txt" || true while IFS= read -r file; do [[ -f "$file" ]] || continue grep -nEi 'csrf|session|json_decode|json_encode|php://input|HTTP_X_REQUESTED_WITH|REQUEST_METHOD|POST\[|filter_input' "$file" 2>/dev/null \ | head -n 120 \ | sed "s#^#$file:#" >> "$WORKDIR/calendar_security_search.txt" || true done < "$WORKDIR/calendar_file_list.txt" fi cat "$WORKDIR/calendar_security_search.txt" >> "$REPORT" 2>/dev/null || true section "Sintassi PHP dei file calendario e file recenti" { { [[ -f "$WORKDIR/calendar_file_list.txt" ]] && cat "$WORKDIR/calendar_file_list.txt" [[ -f "$WORKDIR/recent_code_files.txt" ]] && awk '{ $1=$2=$3=$4=$5=$6=""; sub(/^ +/,""); print }' "$WORKDIR/recent_code_files.txt" } | grep -Ei '[.]php$' | sort -u | head -n 700 > "$WORKDIR/php_lint_targets.txt" || true total=0 errors=0 while IFS= read -r phpfile; do [[ -f "$phpfile" ]] || continue total=$((total + 1)) result="$(php -l "$phpfile" 2>&1)" if [[ $? -ne 0 ]]; then errors=$((errors + 1)) printf 'ERRORE: %s\n%s\n' "$phpfile" "$result" fi done < "$WORKDIR/php_lint_targets.txt" echo "File PHP controllati: $total" echo "Errori sintassi: $errors" } > "$WORKDIR/php_lint.txt" 2>&1 cat "$WORKDIR/php_lint.txt" >> "$REPORT" section "Configurazioni applicative rilevanti (segreti oscurati)" if [[ -d "$TARGET_ROOT" ]]; then find "$TARGET_ROOT" -xdev -type f \ \( -iname 'phpgen_settings.php' -o -iname '*config*.php' -o -iname '.env' -o -iname '*.ini' \) \ -size -2M 2>/dev/null | head -n 300 > "$WORKDIR/config_files.txt" || true while IFS= read -r cfg; do [[ -r "$cfg" ]] || continue echo "--- $cfg ---" grep -nEi 'host|database|dbname|username|user|password|charset|timezone|session|calendar|locale' "$cfg" 2>/dev/null \ | head -n 250 \ | redact_stream || true done < "$WORKDIR/config_files.txt" fi >> "$REPORT" 2>&1 section "Stato database MariaDB/MySQL (sole letture)" MYSQL_CMD="" if command -v mariadb >/dev/null 2>&1; then MYSQL_CMD="mariadb"; elif command -v mysql >/dev/null 2>&1; then MYSQL_CMD="mysql"; fi if [[ -n "$MYSQL_CMD" ]]; then { "$MYSQL_CMD" --batch --raw --skip-column-names -e "SELECT VERSION(); SHOW VARIABLES WHERE Variable_name IN ('read_only','super_read_only','max_connections','max_allowed_packet','sql_mode','time_zone','system_time_zone');" 2>&1 || true echo "$MYSQL_CMD" --batch --raw --skip-column-names -e "SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='${TARGET_DB//\'/\'\'}';" 2>&1 || true echo "$MYSQL_CMD" --batch --raw --skip-column-names -e "SELECT TABLE_NAME, ENGINE, TABLE_ROWS, COALESCE(DATE_FORMAT(UPDATE_TIME,'%Y-%m-%d %H:%i:%s'),'NULL') FROM information_schema.TABLES WHERE TABLE_SCHEMA='${TARGET_DB//\'/\'\'}' ORDER BY COALESCE(UPDATE_TIME,CREATE_TIME) DESC, TABLE_NAME LIMIT 150;" 2>&1 || true echo "$MYSQL_CMD" --batch --raw --skip-column-names -e "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA='${TARGET_DB//\'/\'\'}' AND (LOWER(TABLE_NAME) LIKE '%calendar%' OR LOWER(TABLE_NAME) LIKE '%event%' OR LOWER(TABLE_NAME) LIKE '%turn%' OR LOWER(TABLE_NAME) LIKE '%schedule%' OR LOWER(TABLE_NAME) LIKE '%planning%' OR LOWER(TABLE_NAME) LIKE '%assen%') ORDER BY TABLE_NAME;" 2>&1 || true } > "$WORKDIR/mysql_readonly.txt" else echo "Client mysql/mariadb non disponibile." > "$WORKDIR/mysql_readonly.txt" fi cat "$WORKDIR/mysql_readonly.txt" >> "$REPORT" section "Access log: POST/PUT/PATCH recenti e risposte 4xx/5xx" : > "$WORKDIR/access_post_errors.txt" for log in /var/log/apache2/*silvelox*access*.log /var/log/apache2/access.log; do [[ -r "$log" ]] || continue echo "--- $log ---" >> "$WORKDIR/access_post_errors.txt" tail -n 30000 "$log" 2>/dev/null \ | grep -Ei '"(POST|PUT|PATCH|DELETE) |calendar|calendario|event|turni|planning|schedule' \ | tail -n 2500 >> "$WORKDIR/access_post_errors.txt" || true echo >> "$WORKDIR/access_post_errors.txt" done cat "$WORKDIR/access_post_errors.txt" >> "$REPORT" section "Errori Apache/PHP recenti pertinenti" : > "$WORKDIR/apache_php_errors.txt" for log in /var/log/apache2/*silvelox*error*.log /var/log/apache2/error.log /var/log/php*-fpm.log; do [[ -r "$log" ]] || continue echo "--- $log ---" >> "$WORKDIR/apache_php_errors.txt" tail -n 12000 "$log" 2>/dev/null \ | grep -Ei 'silvelox|calendar|calendario|event|turni|planning|schedule|PHP (Fatal|Warning|Notice|Deprecated)|Uncaught|Exception|SQLSTATE|mysqli|PDO|permission denied|session|CSRF|JSON|403|404|500|502|503|504' \ | tail -n 3000 >> "$WORKDIR/apache_php_errors.txt" || true echo >> "$WORKDIR/apache_php_errors.txt" done cat "$WORKDIR/apache_php_errors.txt" >> "$REPORT" section "Journal Apache/PHP-FPM/MySQL ultime 24 ore" { journalctl -u apache2 --since '24 hours ago' --no-pager 2>/dev/null \ | grep -Ei 'error|fail|fatal|warning|php|session|permission|namespace' | tail -n 1500 || true for unit in php8.4-fpm php8.3-fpm php8.2-fpm php8.1-fpm mariadb mysql; do journalctl -u "$unit" --since '24 hours ago' --no-pager 2>/dev/null \ | grep -Ei 'error|fail|fatal|warning|denied|crash|timeout|connection' | tail -n 1000 || true done } > "$WORKDIR/journal_errors.txt" cat "$WORKDIR/journal_errors.txt" >> "$REPORT" section "Log applicativi recenti dentro il portale" if [[ -d "$TARGET_ROOT" ]]; then find "$TARGET_ROOT" -xdev -type f \( -iname '*.log' -o -iname '*error*.txt' \) -mtime -14 -size -20M \ -printf '%TY-%Tm-%Td %TH:%TM:%TS %s %p\n' 2>/dev/null | sort -r | head -n 200 > "$WORKDIR/app_log_list.txt" || true cat "$WORKDIR/app_log_list.txt" || true while IFS= read -r line; do logfile="${line#* * * }" [[ -r "$logfile" ]] || continue echo "--- $logfile ---" tail -n 500 "$logfile" 2>/dev/null | redact_stream || true done < "$WORKDIR/app_log_list.txt" fi >> "$REPORT" 2>&1 section "Hash dei file calendario candidati" if [[ -f "$WORKDIR/calendar_file_list.txt" ]]; then while IFS= read -r file; do [[ -f "$file" ]] && sha256sum "$file" 2>/dev/null || true done < "$WORKDIR/calendar_file_list.txt" | head -n 1000 >> "$REPORT" fi section "Riepilogo automatico" { echo "PHP lint:" grep -E 'File PHP controllati:|Errori sintassi:' "$WORKDIR/php_lint.txt" 2>/dev/null || true echo echo "Occorrenze errori HTTP 4xx/5xx nelle richieste POST/PUT/PATCH raccolte:" awk 'match($0, /" [45][0-9][0-9] /) {c++} END {print c+0}' "$WORKDIR/access_post_errors.txt" 2>/dev/null || true echo echo "Righe di errore Apache/PHP raccolte:" grep -c . "$WORKDIR/apache_php_errors.txt" 2>/dev/null || true echo echo "File calendario candidati:" wc -l < "$WORKDIR/calendar_candidates.txt" 2>/dev/null || echo 0 echo echo "NOTA: questo step non corregge nulla. Lo ZIP serve a individuare il punto esatto del mancato salvataggio prima della patch." } >> "$REPORT" cp -f "$REPORT" "$WORKDIR/${STEP_NAME}_REPORT.txt" rm -f "$REPORT" if command -v zip >/dev/null 2>&1; then ( cd "$WORKDIR" || exit 1 zip -q -9 "$OUTPUT_ZIP" ./* ) else echo "ERRORE: comando zip non disponibile." >&2 exit 20 fi zip -T "$OUTPUT_ZIP" >/dev/null if find "$WORKDIR" -mindepth 1 -maxdepth 1 -type d | grep -q .; then echo "ERRORE: directory inattese nella workdir." >&2 exit 21 fi printf 'AUDIT COMPLETATO\nZIP: %s\n' "$OUTPUT_ZIP"