#!/usr/bin/env bash set -euo pipefail REPORTS_DIR="/var/lib/saikyo-av/reports" LOG_DIR="/var/log/saikyo-av" QUAR_DIR="/var/lib/saikyo-av/quarantine" mkdir -p "${REPORTS_DIR}" "${LOG_DIR}" "${QUAR_DIR}" || true chmod 0755 /var/lib/saikyo-av "${REPORTS_DIR}" "${LOG_DIR}" 2>/dev/null || true cmd="${1:-}" shift || true utc_ts() { date -u +%Y-%m-%dT%H:%M:%SZ } write_report() { local id="$1" local summary="$2" local severity="$3" local outfile="${REPORTS_DIR}/${id}.json" local created created="$(utc_ts)" # shellcheck disable=SC2129 { echo "{" echo " \"created_utc\": \"${created}\"," echo " \"severity\": \"${severity}\"," echo " \"summary\": \"${summary}\"," echo " \"details\": {" echo " \"action\": \"${cmd}\"," echo " \"argv\": \"$*\"" echo " }," echo " \"artifacts\": {}," echo " \"suggested_fixes\": []" echo "}" } > "${outfile}" echo "${outfile}" } run_to_file() { local out="$1" shift ("$@" 2>&1 || true) | sed -e 's/\r$//' > "${out}" } json_escape() { # Minimal JSON string escape (no unicode handling needed for our file paths) sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/\t/\\t/g' -e 's/\r/\\r/g' -e 's/\n/\\n/g' } write_scan_report() { local id="$1" local summary="$2" local severity="$3" local log_file="$4" local detections_tsv="$5" local outfile="${REPORTS_DIR}/${id}.json" local created created="$(utc_ts)" local scanned_count infected_count scanned_count="$(grep -E '^Scanned files:' "${log_file}" 2>/dev/null | awk -F': ' '{print $2}' | tail -n 1 || true)" infected_count="$(grep -E '^Infected files:' "${log_file}" 2>/dev/null | awk -F': ' '{print $2}' | tail -n 1 || true)" scanned_count="${scanned_count:-0}" infected_count="${infected_count:-0}" { echo "{" echo " \"created_utc\": \"${created}\"," echo " \"severity\": \"${severity}\"," echo " \"summary\": \"${summary}\"," echo " \"details\": {" echo " \"action\": \"${cmd}\"," echo " \"argv\": \"$*\"," echo " \"scanned_files\": ${scanned_count}," echo " \"infected_files\": ${infected_count}," echo " \"detections\": [" if [[ -s "${detections_tsv}" ]]; then first=1 while IFS=$'\t' read -r pth sig; do [[ -n "${pth}" ]] || continue pth_esc="$(printf '%s' "${pth}" | json_escape)" sig_esc="$(printf '%s' "${sig}" | json_escape)" if [[ "${first}" -eq 1 ]]; then first=0 else echo "," fi printf ' {"path":"%s","name":"%s"}' "${pth_esc}" "${sig_esc}" done < "${detections_tsv}" echo fi echo " ]" echo " }," echo " \"artifacts\": {" echo " \"clamav_log\": \"${log_file}\"," echo " \"detections_tsv\": \"${detections_tsv}\"" echo " }," echo " \"suggested_fixes\": [" echo " {\"id\":\"quarantine\",\"title\":\"Quarantine infected file\",\"description\":\"Move selected file to local quarantine.\",\"requires_consent\":true}," echo " {\"id\":\"delete\",\"title\":\"Delete infected file\",\"description\":\"Delete selected infected file (dangerous).\",\"requires_consent\":true}" echo " ]" echo "}" } > "${outfile}" echo "${outfile}" } svc_is_enabled() { systemctl is-enabled saikyo-avd.timer 2>/dev/null || true } svc_is_active() { systemctl is-active saikyo-avd.timer 2>/dev/null || true } case "${cmd}" in health) echo "ok" ;; status-protection) echo "enabled=$(svc_is_enabled) active=$(svc_is_active)" ;; run-evidence) outdir="${REPORTS_DIR}/artifacts" mkdir -p "${outdir}" || true ts="$(date -u +%Y%m%dT%H%M%SZ)" evidence_out="${outdir}/saikyo-evidence-${ts}.log" if command -v saikyo-evidence >/dev/null 2>&1; then run_to_file "${evidence_out}" saikyo-evidence elif [ -x /usr/bin/saikyo-evidence ]; then run_to_file "${evidence_out}" /usr/bin/saikyo-evidence else echo "saikyo-evidence not found" > "${evidence_out}" fi rpt="$(write_report "evidence-${ts}" "Evidence report generated" "info" "${cmd}")" # Append artifacts into report JSON (minimal, without jq dependency). sed -i "s#\"artifacts\": {}#\"artifacts\": {\"evidence_log\": \"${evidence_out}\"}#" "${rpt}" || true echo "${rpt}" ;; scan) outdir="${REPORTS_DIR}/artifacts" mkdir -p "${outdir}" || true ts="$(date -u +%Y%m%dT%H%M%SZ)" log_file="${outdir}/clamav-scan-${ts}.log" det_tsv="${outdir}/clamav-detections-${ts}.tsv" if ! command -v clamscan >/dev/null 2>&1; then echo "clamscan not found (install clamav)" > "${log_file}" rpt="$(write_report "scan-${ts}" "ClamAV scan failed: clamscan not found" "warn" "${cmd}")" sed -i "s#\"artifacts\": {}#\"artifacts\": {\"clamav_log\": \"${log_file}\"}#" "${rpt}" || true echo "${rpt}" exit 1 fi # Full scan of / with exclusions. # - exclude hidden paths (/.*/) # - exclude virtual/system dirs # - keep a log we can parse ( clamscan \ -r \ --infected \ --no-summary \ --exclude-dir='^/proc/' \ --exclude-dir='^/sys/' \ --exclude-dir='^/dev/' \ --exclude-dir='^/run/' \ --exclude-dir='^/tmp/' \ --exclude-dir='/.*/' \ / \ 2>&1 echo clamscan -r --infected --exclude-dir='^/proc/' --exclude-dir='^/sys/' --exclude-dir='^/dev/' --exclude-dir='^/run/' --exclude-dir='^/tmp/' --exclude-dir='/.*/' / --summary 2>/dev/null || true ) | sed -e 's/\r$//' > "${log_file}" : > "${det_tsv}" # Parse "PATH: SIGNATURE FOUND" lines. grep -E ' FOUND$' "${log_file}" | sed -E 's/: (.+) FOUND$//;t;d' >/dev/null 2>&1 || true while IFS= read -r line; do # Example: /path/file: Eicar-Test-Signature FOUND pth="${line%%:*}" rest="${line#*: }" sig="${rest% FOUND}" printf '%s\t%s\n' "${pth}" "${sig}" >> "${det_tsv}" done < <(grep -E ' FOUND$' "${log_file}" || true) infected_count="$(wc -l < "${det_tsv}" 2>/dev/null || echo 0)" severity="info" summary="ClamAV scan completed: no threats" if [[ "${infected_count}" -gt 0 ]]; then severity="bad" summary="ClamAV scan completed: ${infected_count} threat(s) found" fi rpt="$(write_scan_report "scan-${ts}" "${summary}" "${severity}" "${log_file}" "${det_tsv}" "${cmd}")" echo "${rpt}" ;; quarantine) target="${1:-}" if [[ -z "${target}" ]]; then echo "missing file path" >&2 exit 2 fi ts="$(date -u +%Y%m%dT%H%M%SZ)" bn="$(basename -- "${target}" 2>/dev/null || echo file)" dest="${QUAR_DIR}/${ts}-${bn}" if [[ ! -f "${target}" ]]; then rpt="$(write_report "quarantine-${ts}" "Quarantine failed: file not found" "warn" "${cmd}")" echo "${rpt}" exit 1 fi mv -f -- "${target}" "${dest}" 2>>"${LOG_DIR}/saikyo-av-admin.log" || { rpt="$(write_report "quarantine-${ts}" "Quarantine failed: move error" "warn" "${cmd}")" echo "${rpt}" exit 1 } rpt="$(write_report "quarantine-${ts}" "File quarantined" "warn" "${cmd}")" sed -i "s#\"artifacts\": {}#\"artifacts\": {\"from\": \"${target}\", \"to\": \"${dest}\"}#" "${rpt}" || true echo "${rpt}" ;; delete) target="${1:-}" if [[ -z "${target}" ]]; then echo "missing file path" >&2 exit 2 fi ts="$(date -u +%Y%m%dT%H%M%SZ)" if [[ ! -e "${target}" ]]; then rpt="$(write_report "delete-${ts}" "Delete failed: path not found" "warn" "${cmd}")" echo "${rpt}" exit 1 fi rm -rf -- "${target}" 2>>"${LOG_DIR}/saikyo-av-admin.log" || { rpt="$(write_report "delete-${ts}" "Delete failed" "warn" "${cmd}")" echo "${rpt}" exit 1 } rpt="$(write_report "delete-${ts}" "File deleted" "warn" "${cmd}")" sed -i "s#\"artifacts\": {}#\"artifacts\": {\"path\": \"${target}\"}#" "${rpt}" || true echo "${rpt}" ;; run-audit) outdir="${REPORTS_DIR}/artifacts" mkdir -p "${outdir}" || true ts="$(date -u +%Y%m%dT%H%M%SZ)" audit_out="${outdir}/saikyo-audit-report-${ts}.txt" if command -v saikyo-audit-report >/dev/null 2>&1; then run_to_file "${audit_out}" saikyo-audit-report else echo "saikyo-audit-report not installed" > "${audit_out}" fi rpt="$(write_report "audit-${ts}" "Audit report executed" "info" "${cmd}")" sed -i "s#\"artifacts\": {}#\"artifacts\": {\"audit_stdout\": \"${audit_out}\"}#" "${rpt}" || true echo "${rpt}" ;; collect-artifacts) outdir="${REPORTS_DIR}/artifacts" mkdir -p "${outdir}" || true ts="$(date -u +%Y%m%dT%H%M%SZ)" collect_out="${outdir}/collect-artifacts-${ts}.log" if [ -x /usr/share/saikyo-os/forensics/collect-artifacts.sh ]; then run_to_file "${collect_out}" /usr/share/saikyo-os/forensics/collect-artifacts.sh else echo "collect-artifacts.sh not found" > "${collect_out}" fi rpt="$(write_report "collect-${ts}" "Artifacts collection executed" "info" "${cmd}")" sed -i "s#\"artifacts\": {}#\"artifacts\": {\"collector_log\": \"${collect_out}\"}#" "${rpt}" || true echo "${rpt}" ;; enable-protection) ts="$(date -u +%Y%m%dT%H%M%SZ)" (systemctl daemon-reload 2>&1 || true) >> "${LOG_DIR}/saikyo-av-admin.log" || true if systemctl enable --now saikyo-avd.timer >/dev/null 2>&1; then rpt="$(write_report "enable-${ts}" "Protection enabled" "info" "${cmd}")" echo "${rpt}" exit 0 else rpt="$(write_report "enable-${ts}" "Protection enable failed" "warn" "${cmd}")" echo "${rpt}" exit 1 fi ;; disable-protection) ts="$(date -u +%Y%m%dT%H%M%SZ)" (systemctl daemon-reload 2>&1 || true) >> "${LOG_DIR}/saikyo-av-admin.log" || true if systemctl disable --now saikyo-avd.timer >/dev/null 2>&1; then rpt="$(write_report "disable-${ts}" "Protection disabled" "warn" "${cmd}")" echo "${rpt}" exit 0 else rpt="$(write_report "disable-${ts}" "Protection disable failed" "warn" "${cmd}")" echo "${rpt}" exit 1 fi ;; *) echo "Usage: saikyo-av-admin {health|status-protection|run-evidence|run-audit|collect-artifacts|scan|quarantine |delete |enable-protection|disable-protection}" >&2 exit 2 ;; esac