feat(dfir): add hayabusa forensic workflow integration

This commit is contained in:
igor04091968
2026-05-14 15:04:07 +03:00
parent 563bd910d1
commit 0cce6fd08e
28 changed files with 1907 additions and 25 deletions
+14 -4
View File
@@ -1149,6 +1149,15 @@
if (!tbody) return;
try {
const cases = await caseApi("/api/0/dlp/cases?host=" + encodeURIComponent(host) + "&limit=100", { method: "GET" });
function renderCaseDfir(c) {
const hayabusa = c && c.forensics && c.forensics.hayabusa;
if (!hayabusa) return "";
const status = String(hayabusa.status || "");
const mode = String(hayabusa.mode || "");
const reportDir = String(hayabusa.report_dir || "");
const title = reportDir ? ' title="' + escapeHtml(reportDir) + '"' : "";
return '<span' + title + '>Hayabusa ' + escapeHtml(status) + (mode ? " · " + escapeHtml(mode) : "") + '</span>';
}
const rows = (cases || []).map(function (c) {
return (
"<tr>" +
@@ -1158,15 +1167,16 @@
"<td>" + escapeHtml(String(c.title || "")) + "</td>" +
"<td>" + escapeHtml(String(c.assignee || "")) + "</td>" +
"<td>" + escapeHtml(String(c.incident_id || "")) + "</td>" +
"<td>" + renderCaseDfir(c) + "</td>" +
"<td>" + escapeHtml(String(c.updated_at || c.created_at || "")) + "</td>" +
"</tr>"
);
});
tbody.innerHTML = rows.length ? rows.join("") : '<tr><td colspan="7">Кейсов нет.</td></tr>';
tbody.innerHTML = rows.length ? rows.join("") : '<tr><td colspan="8">Кейсов нет.</td></tr>';
const status = center.querySelector("[data-aw-ru-dlp-cases-status]");
if (status) status.textContent = "Кейсов: " + (cases || []).length;
} catch (error) {
tbody.innerHTML = '<tr><td colspan="7">Ошибка загрузки кейсов: ' + escapeHtml(error.message) + '</td></tr>';
tbody.innerHTML = '<tr><td colspan="8">Ошибка загрузки кейсов: ' + escapeHtml(error.message) + '</td></tr>';
const status = center.querySelector("[data-aw-ru-dlp-cases-status]");
if (status) status.textContent = "Кейсы недоступны";
}
@@ -1388,8 +1398,8 @@
'<div class="aw-ru-dlp-status" data-aw-ru-dlp-cases-status>Кейсов: 0</div>' +
'</div>' +
'<table class="aw-ru-dlp-table">' +
'<thead><tr><th>ID</th><th>Статус</th><th>Severity</th><th>Заголовок</th><th>Исполнитель</th><th>Incident ID</th><th>Обновлено</th></tr></thead>' +
'<tbody data-aw-ru-dlp-cases><tr><td colspan="7">Загрузка...</td></tr></tbody>' +
'<thead><tr><th>ID</th><th>Статус</th><th>Severity</th><th>Заголовок</th><th>Исполнитель</th><th>Incident ID</th><th>DFIR</th><th>Обновлено</th></tr></thead>' +
'<tbody data-aw-ru-dlp-cases><tr><td colspan="8">Загрузка...</td></tr></tbody>' +
'</table>' +
'</div>' +
'<div class="aw-ru-dlp-message" data-aw-ru-dlp-message></div>';
+17 -1
View File
@@ -9,6 +9,22 @@ from pydantic import BaseModel, Field
CaseStatus = Literal["open", "investigating", "resolved", "closed"]
class CaseHayabusaLink(BaseModel):
tool: Literal["hayabusa"] = "hayabusa"
host: str = Field(min_length=1, max_length=128)
mode: str = Field(min_length=1, max_length=32)
status: str = Field(min_length=1, max_length=64)
intake_id: str | None = Field(default=None, max_length=256)
package_path: str | None = Field(default=None, max_length=1024)
sha256: str | None = Field(default=None, max_length=128)
report_dir: str | None = Field(default=None, max_length=1024)
summary_html: str | None = Field(default=None, max_length=1024)
timeline_path: str | None = Field(default=None, max_length=1024)
manifest_path: str | None = Field(default=None, max_length=1024)
linked_at: str | None = Field(default=None, max_length=64)
link_source: str | None = Field(default=None, max_length=64)
class CaseCreate(BaseModel):
incident_id: str = Field(min_length=1, max_length=256)
host: str | None = Field(default=None, max_length=128)
@@ -51,6 +67,6 @@ class CaseRecord(BaseModel):
source_bucket: str | None
source_event_ts: str | None
evidence: dict | None
forensics: dict | None
created_at: datetime
updated_at: datetime
@@ -8,7 +8,7 @@ from typing import Any
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from case_schema import CaseCommentCreate, CaseCreate, CaseUpdate
from case_schema import CaseCommentCreate, CaseCreate, CaseHayabusaLink, CaseUpdate
from case_storage import CaseStorage
DB = Path(os.environ.get("AW_DLP_CASE_DB_PATH", "/opt/activitywatch/dlp-case-management/cases.db"))
@@ -76,3 +76,11 @@ def add_comment(case_id: int, payload: CaseCommentCreate) -> dict[str, Any]:
@APP.get("/api/0/dlp/cases/{case_id}/comments")
def list_comments(case_id: int, limit: int = Query(default=200, ge=1, le=2000)) -> list[dict[str, Any]]:
return STORE.list_comments(case_id=case_id, limit=limit)
@APP.post("/api/0/dlp/cases/{case_id}/forensics/hayabusa")
def link_hayabusa(case_id: int, payload: CaseHayabusaLink) -> dict[str, Any]:
try:
return STORE.link_hayabusa(case_id=case_id, payload=payload.model_dump(exclude_none=True), actor="api")
except KeyError:
raise HTTPException(status_code=404, detail="case not found")
+67 -9
View File
@@ -43,6 +43,7 @@ class CaseStorage:
source_bucket TEXT,
source_event_ts TEXT,
evidence_json TEXT,
forensics_json TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
@@ -69,20 +70,35 @@ class CaseStorage:
);
"""
)
self._ensure_column(c, "cases", "forensics_json", "TEXT")
c.commit()
@staticmethod
def _ensure_column(c: sqlite3.Connection, table: str, column: str, definition: str) -> None:
columns = {
str(row["name"])
for row in c.execute(f"PRAGMA table_info({table})").fetchall()
}
if column not in columns:
c.execute(f"ALTER TABLE {table} ADD COLUMN {column} {definition}")
@staticmethod
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
@staticmethod
def _to_case_dict(row: sqlite3.Row) -> dict[str, Any]:
evidence = None
if row["evidence_json"]:
try:
evidence = json.loads(row["evidence_json"])
except Exception:
evidence = None
def _load_json_field(raw: Any) -> dict[str, Any] | None:
if not raw:
return None
try:
return json.loads(raw)
except Exception:
return None
@classmethod
def _to_case_dict(cls, row: sqlite3.Row) -> dict[str, Any]:
evidence = cls._load_json_field(row["evidence_json"])
forensics = cls._load_json_field(row["forensics_json"])
return {
"id": int(row["id"]),
"incident_id": row["incident_id"],
@@ -94,6 +110,7 @@ class CaseStorage:
"source_bucket": row["source_bucket"],
"source_event_ts": row["source_event_ts"],
"evidence": evidence,
"forensics": forensics,
"created_at": row["created_at"],
"updated_at": row["updated_at"],
}
@@ -114,8 +131,8 @@ class CaseStorage:
"""
INSERT INTO cases (
incident_id, host, title, severity, assignee, status,
source_bucket, source_event_ts, evidence_json, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?, ?, ?)
source_bucket, source_event_ts, evidence_json, forensics_json, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?)
""",
(
payload["incident_id"],
@@ -126,6 +143,7 @@ class CaseStorage:
payload.get("source_bucket"),
payload.get("source_event_ts"),
json.dumps(normalized_evidence, ensure_ascii=False) if normalized_evidence is not None else None,
None,
now,
now,
),
@@ -195,6 +213,46 @@ class CaseStorage:
c.commit()
return self.get_case(case_id, c)
def link_hayabusa(self, case_id: int, payload: dict[str, Any], actor: str | None = None) -> dict[str, Any]:
now = self._now()
with self.conn() as c:
existing = self.get_case(case_id, c)
forensics = existing.get("forensics") or {}
forensics["hayabusa"] = {
"tool": "hayabusa",
"host": payload["host"],
"mode": payload["mode"],
"status": payload["status"],
"intake_id": payload.get("intake_id"),
"package_path": payload.get("package_path"),
"sha256": payload.get("sha256"),
"report_dir": payload.get("report_dir"),
"summary_html": payload.get("summary_html"),
"timeline_path": payload.get("timeline_path"),
"manifest_path": payload.get("manifest_path"),
"linked_at": payload.get("linked_at") or now,
"link_source": payload.get("link_source") or "api",
}
c.execute(
"UPDATE cases SET forensics_json = ?, updated_at = ? WHERE id = ?",
(json.dumps(forensics, ensure_ascii=False), now, int(case_id)),
)
self._insert_audit(
c,
case_id=case_id,
action="link_hayabusa",
actor=actor,
details={
"host": payload["host"],
"mode": payload["mode"],
"status": payload["status"],
"intake_id": payload.get("intake_id"),
"report_dir": payload.get("report_dir"),
},
)
c.commit()
return self.get_case(case_id, c)
def add_comment(self, case_id: int, comment: str, author: str | None = None) -> dict[str, Any]:
now = self._now()
with self.conn() as c:
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from case_storage import CaseStorage
class CaseStorageHayabusaLinkTest(unittest.TestCase):
def test_link_hayabusa_metadata(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "cases.db"
storage = CaseStorage(db_path)
created = storage.create_case(
{
"incident_id": "inc-1",
"host": "SHARKON2025",
"title": "DLP print incident",
"severity": "high",
},
actor="test",
)
linked = storage.link_hayabusa(
case_id=int(created["id"]),
payload={
"host": "SHARKON2025",
"mode": "incident",
"status": "ok",
"intake_id": "pkg-1",
"report_dir": "/opt/hayabusa/reports/SHARKON2025/run-1",
"package_path": "/opt/hayabusa/archive/packages/SHARKON2025/pkg-1.zip",
"sha256": "abc123",
"link_source": "unit-test",
},
actor="test",
)
hayabusa = (linked.get("forensics") or {}).get("hayabusa") or {}
self.assertEqual(hayabusa.get("tool"), "hayabusa")
self.assertEqual(hayabusa.get("host"), "SHARKON2025")
self.assertEqual(hayabusa.get("mode"), "incident")
self.assertEqual(hayabusa.get("status"), "ok")
self.assertEqual(hayabusa.get("intake_id"), "pkg-1")
self.assertEqual(hayabusa.get("link_source"), "unit-test")
audit = storage.list_audit(int(created["id"]))
self.assertTrue(any(row.get("action") == "link_hayabusa" for row in audit))
if __name__ == "__main__":
unittest.main()
+522
View File
@@ -0,0 +1,522 @@
#!/usr/bin/env bash
set -euo pipefail
HAYA_ROOT="${AW_HAYABUSA_ROOT:-/opt/hayabusa}"
HAYA_CURRENT="${HAYA_ROOT}/current"
HAYA_BIN="${HAYA_CURRENT}/hayabusa"
HAYA_RULES="${HAYA_CURRENT}/rules"
HAYA_CONFIG="${HAYA_CURRENT}/config"
HAYA_REPORTS_ROOT="${AW_HAYABUSA_REPORTS_ROOT:-${HAYA_ROOT}/reports}"
HAYA_STATE_ROOT="${AW_HAYABUSA_STATE_ROOT:-${HAYA_ROOT}/state}"
HAYA_INCOMING_DIR="${AW_HAYABUSA_INCOMING_DIR:-${HAYA_ROOT}/inbox/incoming}"
HAYA_STAGING_DIR="${AW_HAYABUSA_STAGING_DIR:-${HAYA_ROOT}/inbox/staging}"
HAYA_ARCHIVE_PACKAGES_DIR="${AW_HAYABUSA_ARCHIVE_PACKAGES_DIR:-${HAYA_ROOT}/archive/packages}"
HAYA_ARCHIVE_EXTRACTED_DIR="${AW_HAYABUSA_ARCHIVE_EXTRACTED_DIR:-${HAYA_ROOT}/archive/extracted}"
HAYA_LOGS_DIR="${AW_HAYABUSA_LOGS_DIR:-${HAYA_ROOT}/state/logs}"
LAST_REPORT_DIR=""
usage() {
cat <<'EOF'
Usage:
aw-hayabusa doctor
aw-hayabusa inventory
aw-hayabusa accept --package <zip> [--host HOST]
aw-hayabusa process-inbox [--mode <quick|incident|full>] [--limit N]
aw-hayabusa profiles
aw-hayabusa version
aw-hayabusa <quick|incident|full> --input <file-or-dir> [--host HOST] [--label LABEL] [--output-root DIR] [--threads N]
Modes:
quick Fast CSV triage with HTML summary and logon summary
incident Rich JSONL timeline for incident review with HTML summary and logon summary
full Broad JSONL timeline with all rule families enabled, HTML summary and logon summary
EOF
}
fail() {
echo "ERROR: $*" >&2
exit 1
}
sanitize() {
printf '%s' "$1" | tr ' /:@' '_' | tr -cd 'A-Za-z0-9._-'
}
ensure_layout() {
[ -x "${HAYA_BIN}" ] || fail "Hayabusa binary not found at ${HAYA_BIN}"
[ -d "${HAYA_RULES}" ] || fail "Hayabusa rules directory not found at ${HAYA_RULES}"
[ -d "${HAYA_CONFIG}" ] || fail "Hayabusa config directory not found at ${HAYA_CONFIG}"
mkdir -p \
"${HAYA_REPORTS_ROOT}" \
"${HAYA_STATE_ROOT}" \
"${HAYA_LOGS_DIR}" \
"${HAYA_ROOT}/inbox" \
"${HAYA_ROOT}/archive" \
"${HAYA_INCOMING_DIR}" \
"${HAYA_STAGING_DIR}" \
"${HAYA_ARCHIVE_PACKAGES_DIR}" \
"${HAYA_ARCHIVE_EXTRACTED_DIR}"
}
run_logged() {
local log_file="$1"
shift
{
printf '[%s] CMD:' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
printf ' %q' "$@"
printf '\n'
} | tee -a "${log_file}"
"$@" 2>&1 | tee -a "${log_file}"
return "${PIPESTATUS[0]}"
}
write_manifest() {
local manifest_path="$1"
local mode="$2"
local host="$3"
local input_path="$4"
local report_dir="$5"
local status="$6"
local output_format="$7"
cat >"${manifest_path}" <<EOF
{
"mode": "${mode}",
"host": "${host}",
"input": "${input_path}",
"report_dir": "${report_dir}",
"status": "${status}",
"output_format": "${output_format}",
"generated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
}
json_field() {
local json_path="$1"
local field_name="$2"
python3 - "$json_path" "$field_name" <<'PY'
import json, sys
path, field = sys.argv[1], sys.argv[2]
try:
with open(path, 'r', encoding='utf-8') as fh:
obj = json.load(fh)
except Exception:
sys.exit(0)
value = obj.get(field)
if value is None:
sys.exit(0)
print(str(value))
PY
}
detect_host_from_manifest() {
local manifest_path="$1"
local host=""
host="$(json_field "${manifest_path}" host || true)"
if [ -z "${host}" ]; then
host="$(json_field "${manifest_path}" hostname || true)"
fi
printf '%s' "${host}"
}
write_package_manifest() {
local manifest_path="$1"
local package_path="$2"
local host="$3"
local intake_id="$4"
local sha256="$5"
local status="$6"
local stage_dir="$7"
local report_dir="$8"
cat >"${manifest_path}" <<EOF
{
"package_path": "${package_path}",
"host": "${host}",
"intake_id": "${intake_id}",
"sha256": "${sha256}",
"status": "${status}",
"stage_dir": "${stage_dir}",
"report_dir": "${report_dir}",
"processed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
}
write_state_json() {
local state_path="$1"
local body="$2"
printf '%s\n' "${body}" > "${state_path}"
}
run_mode() {
local mode="$1"
shift
local input_path=""
local host=""
local label=""
local output_root="${HAYA_REPORTS_ROOT}"
local threads=""
while [ "$#" -gt 0 ]; do
case "$1" in
--input|-i)
[ "$#" -ge 2 ] || fail "--input requires a value"
input_path="$2"
shift 2
;;
--host)
[ "$#" -ge 2 ] || fail "--host requires a value"
host="$2"
shift 2
;;
--label)
[ "$#" -ge 2 ] || fail "--label requires a value"
label="$2"
shift 2
;;
--output-root)
[ "$#" -ge 2 ] || fail "--output-root requires a value"
output_root="$2"
shift 2
;;
--threads)
[ "$#" -ge 2 ] || fail "--threads requires a value"
threads="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
fail "Unknown argument: $1"
;;
esac
done
[ -n "${input_path}" ] || fail "--input is required"
[ -e "${input_path}" ] || fail "Input path does not exist: ${input_path}"
ensure_layout
mkdir -p "${output_root}"
if [ -z "${host}" ]; then
host="$(basename "${input_path}")"
if [ "${host}" = "." ] || [ "${host}" = "/" ]; then
host="unknown"
fi
fi
host="$(sanitize "${host}")"
[ -n "${host}" ] || host="unknown"
local label_suffix=""
if [ -n "${label}" ]; then
label_suffix="_$(sanitize "${label}")"
fi
local run_ts
run_ts="$(date -u +%Y%m%dT%H%M%SZ)"
local report_dir="${output_root}/${host}/${run_ts}_${mode}${label_suffix}"
local log_file="${report_dir}/run.log"
local manifest_file="${report_dir}/manifest.json"
local html_file="${report_dir}/summary.html"
local timeline_file=""
local output_format=""
local -a input_args=()
local -a common_args=("-w" "-q" "-C" "-r" "${HAYA_RULES}" "-c" "${HAYA_CONFIG}" "-O")
local -a mode_args=()
local -a command=()
local -a logon_command=()
mkdir -p "${report_dir}"
if [ -d "${input_path}" ]; then
input_args=("-d" "${input_path}")
else
input_args=("-f" "${input_path}")
fi
if [ -n "${threads}" ]; then
common_args+=("-t" "${threads}")
fi
case "${mode}" in
quick)
timeline_file="${report_dir}/timeline.csv"
output_format="csv"
mode_args=("-E" "-P" "-m" "medium" "-o" "${timeline_file}" "-H" "${html_file}")
command=("${HAYA_BIN}" "csv-timeline" "${input_args[@]}" "${common_args[@]}" "${mode_args[@]}")
;;
incident)
timeline_file="${report_dir}/timeline.jsonl"
output_format="jsonl"
mode_args=("-L" "-m" "low" "-o" "${timeline_file}" "-H" "${html_file}")
command=("${HAYA_BIN}" "json-timeline" "${input_args[@]}" "${common_args[@]}" "${mode_args[@]}")
;;
full)
timeline_file="${report_dir}/timeline.jsonl"
output_format="jsonl"
mode_args=("-L" "-A" "-D" "-n" "-u" "-m" "informational" "-o" "${timeline_file}" "-H" "${html_file}")
command=("${HAYA_BIN}" "json-timeline" "${input_args[@]}" "${common_args[@]}" "${mode_args[@]}")
;;
*)
fail "Unsupported mode: ${mode}"
;;
esac
logon_command=("${HAYA_BIN}" "logon-summary" "${input_args[@]}" "-q" "-C" "-c" "${HAYA_CONFIG}" "-O" "-o" "${report_dir}/logon-summary")
{
echo "mode=${mode}"
echo "host=${host}"
echo "input=${input_path}"
echo "report_dir=${report_dir}"
echo "output_format=${output_format}"
} | tee -a "${log_file}" >/dev/null
local status="ok"
if ! run_logged "${log_file}" "${command[@]}"; then
status="failed"
fi
if ! run_logged "${log_file}" "${logon_command[@]}"; then
status="failed"
fi
write_manifest "${manifest_file}" "${mode}" "${host}" "${input_path}" "${report_dir}" "${status}" "${output_format}"
ln -sfn "${report_dir}" "${HAYA_STATE_ROOT}/latest-run"
ln -sfn "${report_dir}" "${HAYA_STATE_ROOT}/latest-${host}"
LAST_REPORT_DIR="${report_dir}"
echo "Report directory: ${report_dir}"
if [ "${status}" != "ok" ]; then
echo "ERROR: Hayabusa run failed; see ${log_file}" >&2
return 1
fi
}
inventory() {
ensure_layout
local incoming_count staged_count archived_pkg_count archived_extract_count
incoming_count=$(find "${HAYA_INCOMING_DIR}" -maxdepth 1 -type f -name '*.zip' | wc -l)
staged_count=$(find "${HAYA_STAGING_DIR}" -mindepth 1 -maxdepth 1 -type d | wc -l)
archived_pkg_count=$(find "${HAYA_ARCHIVE_PACKAGES_DIR}" -type f -name '*.zip' | wc -l)
archived_extract_count=$(find "${HAYA_ARCHIVE_EXTRACTED_DIR}" -mindepth 2 -maxdepth 2 -type d | wc -l)
echo "aw-hayabusa inventory"
echo "incoming_zip=${incoming_count}"
echo "staged_dirs=${staged_count}"
echo "archived_packages=${archived_pkg_count}"
echo "archived_payloads=${archived_extract_count}"
if [ -L "${HAYA_STATE_ROOT}/latest-run" ]; then
echo "latest_run=$(readlink -f "${HAYA_STATE_ROOT}/latest-run")"
fi
}
accept_package() {
local package_path=""
local host=""
while [ "$#" -gt 0 ]; do
case "$1" in
--package)
[ "$#" -ge 2 ] || fail "--package requires a value"
package_path="$2"
shift 2
;;
--host)
[ "$#" -ge 2 ] || fail "--host requires a value"
host="$2"
shift 2
;;
*)
fail "Unknown argument: $1"
;;
esac
done
[ -n "${package_path}" ] || fail "--package is required"
[ -f "${package_path}" ] || fail "Package not found: ${package_path}"
ensure_layout
local ts base_name safe_base dest_path sha256
ts="$(date -u +%Y%m%dT%H%M%SZ)"
base_name="$(basename "${package_path}")"
safe_base="$(sanitize "${base_name}")"
[ -n "${safe_base}" ] || safe_base="incoming.zip"
dest_path="${HAYA_INCOMING_DIR}/${ts}_${safe_base}"
cp -f "${package_path}" "${dest_path}"
sha256="$(sha256sum "${dest_path}" | awk '{print $1}')"
printf '%s %s\n' "${sha256}" "$(basename "${dest_path}")" > "${dest_path}.sha256"
if [ -n "${host}" ]; then
write_state_json "${dest_path}.host" "${host}"
fi
echo "Accepted package: ${dest_path}"
}
find_manifest_path() {
local stage_dir="$1"
find "${stage_dir}" -type f -name 'manifest.json' | head -n 1
}
find_evtx_root() {
local stage_dir="$1"
if [ -d "${stage_dir}/evtx" ]; then
printf '%s' "${stage_dir}/evtx"
return 0
fi
find "${stage_dir}" -type d -name evtx | head -n 1
}
process_one_package() {
local package_path="$1"
local mode="$2"
local forced_host="${3:-}"
ensure_layout
local package_name package_base intake_id stage_dir package_sha256
package_name="$(basename "${package_path}")"
package_base="${package_name%.zip}"
intake_id="$(sanitize "${package_base}")"
stage_dir="${HAYA_STAGING_DIR}/${intake_id}"
mkdir -p "${stage_dir}"
package_sha256="$(sha256sum "${package_path}" | awk '{print $1}')"
unzip -q -o "${package_path}" -d "${stage_dir}"
local manifest_path host evtx_root archive_pkg_dir archive_pkg_path archive_extract_dir status report_dir
manifest_path="$(find_manifest_path "${stage_dir}")"
host="${forced_host}"
if [ -z "${host}" ] && [ -f "${package_path}.host" ]; then
host="$(cat "${package_path}.host" 2>/dev/null || true)"
fi
if [ -z "${host}" ] && [ -n "${manifest_path}" ]; then
host="$(detect_host_from_manifest "${manifest_path}")"
fi
if [ -z "${host}" ]; then
host="${package_base%%-*}"
fi
host="$(sanitize "${host}")"
[ -n "${host}" ] || host="unknown"
archive_pkg_dir="${HAYA_ARCHIVE_PACKAGES_DIR}/${host}"
archive_extract_dir="${HAYA_ARCHIVE_EXTRACTED_DIR}/${host}/${intake_id}"
mkdir -p "${archive_pkg_dir}" "${archive_extract_dir}"
evtx_root="$(find_evtx_root "${stage_dir}")"
status="ok"
report_dir=""
if [ -z "${evtx_root}" ] || ! find "${evtx_root}" -type f \( -iname '*.evtx' -o -iname '*.json' -o -iname '*.jsonl' \) | grep -q .; then
status="failed-no-evtx"
else
if run_mode "${mode}" --input "${evtx_root}" --host "${host}" --label "${package_base}"; then
report_dir="${LAST_REPORT_DIR}"
status="ok"
else
report_dir="${LAST_REPORT_DIR}"
status="failed-analysis"
fi
fi
mv "${package_path}" "${archive_pkg_dir}/${intake_id}.zip"
[ -f "${package_path}.sha256" ] && mv "${package_path}.sha256" "${archive_pkg_dir}/${intake_id}.zip.sha256"
[ -f "${package_path}.host" ] && mv "${package_path}.host" "${archive_pkg_dir}/${intake_id}.host"
mv "${stage_dir}" "${archive_extract_dir}/payload"
write_package_manifest "${archive_extract_dir}/intake.json" "${archive_pkg_dir}/${intake_id}.zip" "${host}" "${intake_id}" "${package_sha256}" "${status}" "${archive_extract_dir}/payload" "${report_dir}"
write_state_json "${HAYA_STATE_ROOT}/latest-intake.json" "$(cat "${archive_extract_dir}/intake.json")"
echo "Processed package: ${archive_pkg_dir}/${intake_id}.zip"
echo "Archive payload: ${archive_extract_dir}/payload"
if [ -n "${report_dir}" ]; then
echo "Report directory: ${report_dir}"
fi
[ "${status}" = "ok" ] || fail "Package workflow ended with status=${status}; archived for inspection"
}
process_inbox() {
local mode="incident"
local limit="0"
while [ "$#" -gt 0 ]; do
case "$1" in
--mode)
[ "$#" -ge 2 ] || fail "--mode requires a value"
mode="$2"
shift 2
;;
--limit)
[ "$#" -ge 2 ] || fail "--limit requires a value"
limit="$2"
shift 2
;;
*)
fail "Unknown argument: $1"
;;
esac
done
case "${mode}" in
quick|incident|full) ;;
*) fail "Unsupported mode for process-inbox: ${mode}" ;;
esac
ensure_layout
local count=0 pkg
while IFS= read -r pkg; do
process_one_package "${pkg}" "${mode}"
count=$((count + 1))
if [ "${limit}" -gt 0 ] && [ "${count}" -ge "${limit}" ]; then
break
fi
done < <(find "${HAYA_INCOMING_DIR}" -maxdepth 1 -type f -name '*.zip' | sort)
[ "${count}" -gt 0 ] || echo "No packages in ${HAYA_INCOMING_DIR}"
}
main() {
local subcommand="${1:-}"
case "${subcommand}" in
doctor)
ensure_layout
echo "aw-hayabusa doctor: OK"
echo "root=${HAYA_ROOT}"
echo "current=${HAYA_CURRENT}"
echo "binary=${HAYA_BIN}"
echo "rules=${HAYA_RULES}"
echo "config=${HAYA_CONFIG}"
echo "reports=${HAYA_REPORTS_ROOT}"
echo "state=${HAYA_STATE_ROOT}"
echo "incoming=${HAYA_INCOMING_DIR}"
echo "staging=${HAYA_STAGING_DIR}"
echo "archive_packages=${HAYA_ARCHIVE_PACKAGES_DIR}"
echo "archive_extracted=${HAYA_ARCHIVE_EXTRACTED_DIR}"
echo "logs=${HAYA_LOGS_DIR}"
;;
inventory)
inventory
;;
accept)
shift
accept_package "$@"
;;
process-inbox)
shift
process_inbox "$@"
;;
profiles)
ensure_layout
cd "${HAYA_CURRENT}"
exec "${HAYA_BIN}" list-profiles
;;
version)
ensure_layout
cd "${HAYA_CURRENT}"
exec "${HAYA_BIN}" help
;;
quick|incident|full)
shift
cd "${HAYA_CURRENT}"
run_mode "${subcommand}" "$@"
;;
""|-h|--help|help)
usage
;;
*)
fail "Unknown subcommand: ${subcommand}"
;;
esac
}
main "$@"