#!/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_RULES_CONFIG="${HAYA_RULES}/config" 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 [--host HOST] aw-hayabusa process-inbox [--mode ] [--limit N] aw-hayabusa profiles aw-hayabusa version aw-hayabusa --input [--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}" [ -d "${HAYA_RULES_CONFIG}" ] || fail "Hayabusa rules config directory not found at ${HAYA_RULES_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}" </dev/null 2>&1 || fail "jq is required to read ${json_path}" jq -er --arg field "${field_name}" '.[$field] // empty | tostring' "${json_path}" 2>/dev/null || true } 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}" } extract_zip_normalized() { local package_path="$1" local dest_dir="$2" command -v zipinfo >/dev/null 2>&1 || fail "zipinfo is required to inspect ${package_path}" command -v unzip >/dev/null 2>&1 || fail "unzip is required to extract ${package_path}" mkdir -p "${dest_dir}" local entry normalized while IFS= read -r entry; do normalized="${entry//\\//}" case "${normalized}" in ""|.|/*|*"/../"*|../*|*"..") fail "unsafe zip entry: ${entry}" ;; esac done < <(zipinfo -1 "${package_path}") unzip -q "${package_path}" -d "${dest_dir}" } 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}" < "${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}" "-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[@]}" "-c" "${HAYA_RULES_CONFIG}" "${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[@]}" "-c" "${HAYA_RULES_CONFIG}" "${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[@]}" "-c" "${HAYA_RULES_CONFIG}" "${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}')" if ! extract_zip_normalized "${package_path}" "${stage_dir}"; then fail "normalized zip extraction failed for ${package_path}" fi local manifest_path host evtx_root archive_pkg_dir 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 "rules_config=${HAYA_RULES_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 "$@"