feat(dfir): add hayabusa forensic workflow integration
This commit is contained in:
@@ -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>';
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
@@ -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 "$@"
|
||||
Reference in New Issue
Block a user