saikyo-packages-src/saikyo-av-gui/bin/saikyo-av-admin

324 lines
10 KiB
Bash

#!/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 <file>|delete <file>|enable-protection|disable-protection}" >&2
exit 2
;;
esac