feat(dfir): add hayabusa forensic workflow integration
This commit is contained in:
@@ -1314,6 +1314,99 @@
|
||||
msg: "{{ aw_hayabusa_ioc_refresh_result.stdout }}"
|
||||
when: aw_hayabusa_ioc_refresh_result.stdout is defined
|
||||
|
||||
- name: Установить server-side Hayabusa runner
|
||||
when: aw_hayabusa_runner_enabled | default(false) | bool
|
||||
block:
|
||||
- name: Создать каталоги Hayabusa
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
loop:
|
||||
- "{{ aw_hayabusa_root }}"
|
||||
- "{{ aw_hayabusa_root }}/releases"
|
||||
- "{{ aw_hayabusa_release_dir }}"
|
||||
- "{{ aw_hayabusa_reports_dir }}"
|
||||
- "{{ aw_hayabusa_state_dir }}"
|
||||
- "{{ aw_hayabusa_inbox_dir }}"
|
||||
- "{{ aw_hayabusa_archive_dir }}"
|
||||
- "{{ aw_hayabusa_incoming_dir }}"
|
||||
- "{{ aw_hayabusa_staging_dir }}"
|
||||
- "{{ aw_hayabusa_archive_packages_dir }}"
|
||||
- "{{ aw_hayabusa_archive_extracted_dir }}"
|
||||
- "{{ aw_hayabusa_logs_dir }}"
|
||||
|
||||
- name: Скачать архив Hayabusa
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ aw_hayabusa_download_url }}"
|
||||
dest: "{{ aw_hayabusa_archive_path }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Распаковать pinned Hayabusa release
|
||||
ansible.builtin.unarchive:
|
||||
src: "{{ aw_hayabusa_archive_path }}"
|
||||
dest: "{{ aw_hayabusa_release_dir }}"
|
||||
remote_src: true
|
||||
creates: "{{ aw_hayabusa_release_dir }}/{{ aw_hayabusa_binary_name }}"
|
||||
|
||||
- name: Нормализовать права release Hayabusa
|
||||
ansible.builtin.file:
|
||||
path: "{{ aw_hayabusa_release_dir }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
recurse: true
|
||||
|
||||
- name: Сделать бинарь Hayabusa исполняемым
|
||||
ansible.builtin.file:
|
||||
path: "{{ aw_hayabusa_release_dir }}/{{ aw_hayabusa_binary_name }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
state: file
|
||||
|
||||
- name: Создать canonical symlink на текущий релиз Hayabusa
|
||||
ansible.builtin.file:
|
||||
src: "{{ aw_hayabusa_release_dir }}"
|
||||
dest: "{{ aw_hayabusa_current_link }}"
|
||||
state: link
|
||||
force: true
|
||||
|
||||
- name: Создать canonical symlink на бинарь Hayabusa
|
||||
ansible.builtin.file:
|
||||
src: "{{ aw_hayabusa_binary_name }}"
|
||||
dest: "{{ aw_hayabusa_release_dir }}/hayabusa"
|
||||
state: link
|
||||
force: true
|
||||
|
||||
- name: Установить wrapper aw-hayabusa
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa.sh"
|
||||
dest: /usr/local/bin/aw-hayabusa
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Проверить server-side runner через doctor
|
||||
ansible.builtin.command:
|
||||
cmd: /usr/local/bin/aw-hayabusa doctor
|
||||
changed_when: false
|
||||
|
||||
- name: Проверить загрузку profiles Hayabusa
|
||||
ansible.builtin.shell: /usr/local/bin/aw-hayabusa profiles > /tmp/aw-hayabusa-profiles.txt
|
||||
args:
|
||||
executable: /bin/bash
|
||||
changed_when: false
|
||||
|
||||
- name: Проверить наличие standard profile у Hayabusa
|
||||
ansible.builtin.shell: "grep -q 'standard:' /tmp/aw-hayabusa-profiles.txt"
|
||||
args:
|
||||
executable: /bin/bash
|
||||
changed_when: false
|
||||
|
||||
- name: Post-deploy health gate (aw-health-check)
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
aw_windows_incident_capture_enabled: true
|
||||
aw_windows_incident_screenshot_enabled: true
|
||||
aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts"
|
||||
aw_windows_forensics_root: "{{ aw_windows_state_root }}\\forensics\\evtx-exports"
|
||||
aw_windows_logon_marker_enabled: true
|
||||
aw_windows_skip_hardening: false
|
||||
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
|
||||
@@ -169,6 +170,8 @@
|
||||
IncidentCaptureEnabled = {{ '$true' if (aw_windows_incident_capture_enabled | bool) else '$false' }}
|
||||
IncidentScreenshotEnabled = {{ '$true' if (aw_windows_incident_screenshot_enabled | bool) else '$false' }}
|
||||
IncidentArtifactsRoot = "{{ aw_windows_incident_artifacts_root }}"
|
||||
EvtxExportRoot = "{{ aw_windows_forensics_root }}"
|
||||
EvtxRetentionDays = {{ aw_windows_evtx_retention_days | int }}
|
||||
LogonMarkerEnabled = {{ '$true' if (aw_windows_logon_marker_enabled | bool) else '$false' }}
|
||||
PolicyMode = "{{ aw_windows_policy_mode }}"
|
||||
PolicyEngineEnabled = {{ '$true' if (aw_windows_policy_engine_enabled | bool) else '$false' }}
|
||||
@@ -185,6 +188,13 @@
|
||||
{% if (aw_windows_package_zip_path | default('') | string | length) > 0 %}
|
||||
$params.PackageZipPath = "{{ aw_windows_package_zip_path }}"
|
||||
{% endif %}
|
||||
{% if (aw_windows_evtx_channels | default([]) | length) > 0 %}
|
||||
$params.EvtxChannels = @(
|
||||
{% for channel in aw_windows_evtx_channels %}
|
||||
"{{ channel }}"{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
)
|
||||
{% endif %}
|
||||
{% if (aw_windows_hostname_override | default('') | string | length) > 0 %}
|
||||
$params.AwHostname = "{{ aw_windows_hostname_override }}"
|
||||
{% endif %}
|
||||
|
||||
@@ -18,14 +18,24 @@
|
||||
tsj_bot_default_chat_id: "{{ telegram_default_chat_id | default(telegram_allowed_chat_ids.split(',')[0]) }}"
|
||||
|
||||
pre_tasks:
|
||||
- name: Проверить наличие существующего .env бота на хосте
|
||||
ansible.builtin.stat:
|
||||
path: "{{ tsj_bot_env_path }}"
|
||||
register: tsj_bot_existing_env
|
||||
|
||||
- name: Проверить обязательные переменные
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- telegram_bot_token is defined
|
||||
- telegram_bot_token | length > 20
|
||||
- telegram_allowed_chat_ids is defined
|
||||
- telegram_allowed_chat_ids | length > 0
|
||||
fail_msg: "Задайте telegram_bot_token и telegram_allowed_chat_ids (см. group_vars/proxmox-bot.example.yml)."
|
||||
- >
|
||||
(
|
||||
telegram_bot_token is defined and
|
||||
(telegram_bot_token | string | length) > 20 and
|
||||
telegram_allowed_chat_ids is defined and
|
||||
(telegram_allowed_chat_ids | string | length) > 0
|
||||
)
|
||||
or
|
||||
(tsj_bot_existing_env.stat.exists | default(false))
|
||||
fail_msg: "Задайте telegram_bot_token и telegram_allowed_chat_ids или оставьте на хосте существующий {{ tsj_bot_env_path }}."
|
||||
|
||||
- name: Проверить наличие исходного файла бота на контроллере
|
||||
ansible.builtin.stat:
|
||||
@@ -70,7 +80,12 @@
|
||||
mode: "0750"
|
||||
notify: Restart tsj bot
|
||||
|
||||
- name: Сгенерировать .env бота
|
||||
- name: Сгенерировать полный .env бота
|
||||
when:
|
||||
- telegram_bot_token is defined
|
||||
- (telegram_bot_token | string | length) > 20
|
||||
- telegram_allowed_chat_ids is defined
|
||||
- (telegram_allowed_chat_ids | string | length) > 0
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ tsj_bot_env_path }}"
|
||||
owner: "{{ tsj_bot_user }}"
|
||||
@@ -133,11 +148,44 @@
|
||||
AW_RUS_WORKTIME_BASE={{ tsj_bot_aw_rus_worktime_base | default('http://10.10.10.13:5610') }}
|
||||
AW_RUS_WORKTIME_HEAL_CMD={{ tsj_bot_aw_rus_worktime_heal_cmd | default("sshpass -p '04091968' ssh -o PubkeyAuthentication=no -o StrictHostKeyChecking=no igor@10.10.10.13 'sudo -S /usr/local/bin/aw-worktime-autoheal.sh && sudo -S systemctl start aw-worktime-ui-bridge.service'") }}
|
||||
AW_RUS_DLP_HEAL_CMD={{ tsj_bot_aw_rus_dlp_heal_cmd | default("sshpass -p '04091968' ssh -o PubkeyAuthentication=no -o StrictHostKeyChecking=no igor@10.10.10.13 'sudo -S systemctl restart activitywatch-server.service && sudo -S systemctl start activitywatch-dlp-aggregator.service || true && sudo -S /usr/local/bin/aw-health-check && sudo -S /usr/local/bin/dlp-health-check'") }}
|
||||
AW_RUS_CASE_API_BASE={{ tsj_bot_aw_rus_case_api_base | default('http://10.10.10.13:5602') }}
|
||||
AW_RUS_HAYABUSA_ENABLED={{ tsj_bot_aw_rus_hayabusa_enabled | default('true') }}
|
||||
AW_RUS_HAYABUSA_SSH_CMD={{ tsj_bot_aw_rus_hayabusa_ssh_cmd | default("sshpass -p '04091968' ssh -o PubkeyAuthentication=no -o StrictHostKeyChecking=no igor@10.10.10.13") }}
|
||||
AW_RUS_HOST={{ tsj_bot_aw_rus_host | default('SHARKON2025') }}
|
||||
AW_RUS_PRIMARY_USER={{ tsj_bot_aw_rus_primary_user | default('USER1') }}
|
||||
AW_RUS_STALE_SEC={{ tsj_bot_aw_rus_stale_sec | default(900) }}
|
||||
notify: Restart tsj bot
|
||||
|
||||
- name: Обновить только AW-Rus/Hayabusa env ключи в существующем .env
|
||||
when:
|
||||
- not (
|
||||
telegram_bot_token is defined and
|
||||
(telegram_bot_token | string | length) > 20 and
|
||||
telegram_allowed_chat_ids is defined and
|
||||
(telegram_allowed_chat_ids | string | length) > 0
|
||||
)
|
||||
- tsj_bot_existing_env.stat.exists | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ tsj_bot_env_path }}"
|
||||
regexp: "^{{ item.key }}="
|
||||
line: "{{ item.key }}={{ item.value }}"
|
||||
create: false
|
||||
owner: "{{ tsj_bot_user }}"
|
||||
group: "{{ tsj_bot_group }}"
|
||||
mode: "0640"
|
||||
loop:
|
||||
- { key: "AW_RUS_API_BASE", value: "{{ tsj_bot_aw_rus_api_base | default('http://10.10.10.13:5600/api/0') }}" }
|
||||
- { key: "AW_RUS_WORKTIME_BASE", value: "{{ tsj_bot_aw_rus_worktime_base | default('http://10.10.10.13:5610') }}" }
|
||||
- { key: "AW_RUS_WORKTIME_HEAL_CMD", value: "{{ tsj_bot_aw_rus_worktime_heal_cmd | default(\"sshpass -p '04091968' ssh -o PubkeyAuthentication=no -o StrictHostKeyChecking=no igor@10.10.10.13 'sudo -S /usr/local/bin/aw-worktime-autoheal.sh && sudo -S systemctl start aw-worktime-ui-bridge.service'\") }}" }
|
||||
- { key: "AW_RUS_DLP_HEAL_CMD", value: "{{ tsj_bot_aw_rus_dlp_heal_cmd | default(\"sshpass -p '04091968' ssh -o PubkeyAuthentication=no -o StrictHostKeyChecking=no igor@10.10.10.13 'sudo -S systemctl restart activitywatch-server.service && sudo -S systemctl start activitywatch-dlp-aggregator.service || true && sudo -S /usr/local/bin/aw-health-check && sudo -S /usr/local/bin/dlp-health-check'\") }}" }
|
||||
- { key: "AW_RUS_CASE_API_BASE", value: "{{ tsj_bot_aw_rus_case_api_base | default('http://10.10.10.13:5602') }}" }
|
||||
- { key: "AW_RUS_HAYABUSA_ENABLED", value: "{{ tsj_bot_aw_rus_hayabusa_enabled | default('true') }}" }
|
||||
- { key: "AW_RUS_HAYABUSA_SSH_CMD", value: "{{ tsj_bot_aw_rus_hayabusa_ssh_cmd | default(\"sshpass -p '04091968' ssh -o PubkeyAuthentication=no -o StrictHostKeyChecking=no igor@10.10.10.13\") }}" }
|
||||
- { key: "AW_RUS_HOST", value: "{{ tsj_bot_aw_rus_host | default('SHARKON2025') }}" }
|
||||
- { key: "AW_RUS_PRIMARY_USER", value: "{{ tsj_bot_aw_rus_primary_user | default('USER1') }}" }
|
||||
- { key: "AW_RUS_STALE_SEC", value: "{{ tsj_bot_aw_rus_stale_sec | default(900) }}" }
|
||||
notify: Restart tsj bot
|
||||
|
||||
- name: Установить systemd unit бота
|
||||
ansible.builtin.copy:
|
||||
dest: "/etc/systemd/system/{{ tsj_bot_service_name }}"
|
||||
|
||||
@@ -12,6 +12,24 @@ ansible_become_password: "{{ lookup('env', 'AW_SUDO_PASSWORD') | default(lookup(
|
||||
aw_hayabusa_ioc_refresh_enabled: false
|
||||
aw_hayabusa_rules_root: "/mnt/usb_hdd1/Projects/hayabusa/rules"
|
||||
aw_hayabusa_ioc_output_dir: "{{ aw_server_data_dir }}/dlp-ioc"
|
||||
aw_hayabusa_runner_enabled: true
|
||||
aw_hayabusa_version: "v3.9.0"
|
||||
aw_hayabusa_asset_name: "hayabusa-3.9.0-lin-x64-gnu.zip"
|
||||
aw_hayabusa_binary_name: "hayabusa-3.9.0-lin-x64-gnu"
|
||||
aw_hayabusa_download_url: "https://github.com/Yamato-Security/hayabusa/releases/download/v3.9.0/hayabusa-3.9.0-lin-x64-gnu.zip"
|
||||
aw_hayabusa_root: "/opt/hayabusa"
|
||||
aw_hayabusa_release_dir: "{{ aw_hayabusa_root }}/releases/{{ aw_hayabusa_version }}"
|
||||
aw_hayabusa_current_link: "{{ aw_hayabusa_root }}/current"
|
||||
aw_hayabusa_archive_path: "/tmp/{{ aw_hayabusa_asset_name }}"
|
||||
aw_hayabusa_reports_dir: "{{ aw_hayabusa_root }}/reports"
|
||||
aw_hayabusa_state_dir: "{{ aw_hayabusa_root }}/state"
|
||||
aw_hayabusa_inbox_dir: "{{ aw_hayabusa_root }}/inbox"
|
||||
aw_hayabusa_archive_dir: "{{ aw_hayabusa_root }}/archive"
|
||||
aw_hayabusa_incoming_dir: "{{ aw_hayabusa_inbox_dir }}/incoming"
|
||||
aw_hayabusa_staging_dir: "{{ aw_hayabusa_inbox_dir }}/staging"
|
||||
aw_hayabusa_archive_packages_dir: "{{ aw_hayabusa_archive_dir }}/packages"
|
||||
aw_hayabusa_archive_extracted_dir: "{{ aw_hayabusa_archive_dir }}/extracted"
|
||||
aw_hayabusa_logs_dir: "{{ aw_hayabusa_state_dir }}/logs"
|
||||
aw_dlp_policy_engine_enabled: true
|
||||
aw_dlp_policy_engine_bind_host: "0.0.0.0"
|
||||
aw_dlp_policy_engine_port: 5601
|
||||
|
||||
@@ -41,6 +41,15 @@ aw_windows_local_agent_logs_enabled: false
|
||||
aw_windows_incident_capture_enabled: true
|
||||
aw_windows_incident_screenshot_enabled: true
|
||||
aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts"
|
||||
aw_windows_forensics_root: "{{ aw_windows_state_root }}\\forensics\\evtx-exports"
|
||||
aw_windows_evtx_retention_days: 14
|
||||
aw_windows_evtx_channels:
|
||||
- Security
|
||||
- System
|
||||
- Application
|
||||
- Microsoft-Windows-PowerShell/Operational
|
||||
- Microsoft-Windows-TerminalServices-LocalSessionManager/Operational
|
||||
- Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational
|
||||
aw_windows_logon_marker_enabled: true
|
||||
aw_windows_skip_hardening: false
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@ tsj_bot_aw_rus_api_base: "http://10.10.10.13:5600/api/0"
|
||||
tsj_bot_aw_rus_worktime_base: "http://10.10.10.13:5610"
|
||||
tsj_bot_aw_rus_worktime_heal_cmd: "sshpass -p 'CHANGE_ME' ssh -o PubkeyAuthentication=no -o StrictHostKeyChecking=no igor@10.10.10.13 'sudo -S /usr/local/bin/aw-worktime-autoheal.sh && sudo -S systemctl start aw-worktime-ui-bridge.service'"
|
||||
tsj_bot_aw_rus_dlp_heal_cmd: "sshpass -p 'CHANGE_ME' ssh -o PubkeyAuthentication=no -o StrictHostKeyChecking=no igor@10.10.10.13 'sudo -S systemctl restart activitywatch-server.service && sudo -S systemctl start activitywatch-dlp-aggregator.service || true && sudo -S /usr/local/bin/aw-health-check && sudo -S /usr/local/bin/dlp-health-check'"
|
||||
tsj_bot_aw_rus_case_api_base: "http://10.10.10.13:5602"
|
||||
tsj_bot_aw_rus_hayabusa_enabled: "true"
|
||||
tsj_bot_aw_rus_hayabusa_ssh_cmd: "sshpass -p 'CHANGE_ME' ssh -o PubkeyAuthentication=no -o StrictHostKeyChecking=no igor@10.10.10.13"
|
||||
tsj_bot_aw_rus_host: "SHARKON2025"
|
||||
tsj_bot_aw_rus_primary_user: "USER1"
|
||||
tsj_bot_aw_rus_stale_sec: 900
|
||||
|
||||
@@ -29,6 +29,15 @@ aw_windows_local_agent_logs_enabled: false
|
||||
aw_windows_incident_capture_enabled: true
|
||||
aw_windows_incident_screenshot_enabled: true
|
||||
aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts"
|
||||
aw_windows_forensics_root: "{{ aw_windows_state_root }}\\forensics\\evtx-exports"
|
||||
aw_windows_evtx_retention_days: 14
|
||||
aw_windows_evtx_channels:
|
||||
- Security
|
||||
- System
|
||||
- Application
|
||||
- Microsoft-Windows-PowerShell/Operational
|
||||
- Microsoft-Windows-TerminalServices-LocalSessionManager/Operational
|
||||
- Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational
|
||||
aw_windows_logon_marker_enabled: true
|
||||
aw_windows_skip_hardening: false
|
||||
|
||||
|
||||
@@ -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 "$@"
|
||||
@@ -0,0 +1,76 @@
|
||||
# Hayabusa Artifact Workflow 2026-05-14
|
||||
|
||||
This document records the server-side EVTX intake and archive workflow on `10.10.10.13`.
|
||||
|
||||
## Directories
|
||||
|
||||
- incoming packages:
|
||||
- `/opt/hayabusa/inbox/incoming`
|
||||
- transient staging:
|
||||
- `/opt/hayabusa/inbox/staging`
|
||||
- generated reports:
|
||||
- `/opt/hayabusa/reports`
|
||||
- archived raw packages:
|
||||
- `/opt/hayabusa/archive/packages/<HOST>/`
|
||||
- archived extracted payloads:
|
||||
- `/opt/hayabusa/archive/extracted/<HOST>/<INTAKE_ID>/payload/`
|
||||
- state:
|
||||
- `/opt/hayabusa/state/latest-intake.json`
|
||||
- `/opt/hayabusa/state/latest-run`
|
||||
- `/opt/hayabusa/state/latest-<HOST>`
|
||||
- `/opt/hayabusa/state/logs`
|
||||
|
||||
## Operator flow
|
||||
|
||||
1. Accept a package into server inbox:
|
||||
|
||||
```bash
|
||||
aw-hayabusa accept --package /path/to/HOST-YYYYMMDD-HHMMSS.zip --host HOST
|
||||
```
|
||||
|
||||
2. Inspect queue:
|
||||
|
||||
```bash
|
||||
aw-hayabusa inventory
|
||||
```
|
||||
|
||||
3. Process queued packages:
|
||||
|
||||
```bash
|
||||
aw-hayabusa process-inbox --mode incident
|
||||
```
|
||||
|
||||
## Processing behavior
|
||||
|
||||
- the package is extracted into staging;
|
||||
- host is resolved from explicit `--host`, sidecar `.host`, embedded `manifest.json`, or package name fallback;
|
||||
- if EVTX payload exists, Hayabusa analysis is launched through the existing runner modes;
|
||||
- regardless of success, the package and extracted payload are moved into archive;
|
||||
- `intake.json` records:
|
||||
- package path
|
||||
- host
|
||||
- intake id
|
||||
- sha256
|
||||
- status
|
||||
- extracted payload path
|
||||
- report directory
|
||||
- processed timestamp
|
||||
|
||||
## Failure semantics
|
||||
|
||||
- malformed or empty packages are not lost;
|
||||
- the workflow archives them with `status=failed-*`;
|
||||
- the operator can inspect archived payloads without touching AW runtime storage.
|
||||
|
||||
## Validation evidence
|
||||
|
||||
- `aw-hayabusa inventory` shows queue and archive counts
|
||||
- a synthetic package was accepted, archived, and recorded with:
|
||||
- `status=failed-no-evtx`
|
||||
- synthetic artifacts were removed after validation so production storage stayed clean
|
||||
|
||||
## Boundaries
|
||||
|
||||
- this phase does not yet move packages from Windows automatically
|
||||
- this phase does not yet attach reports to AW-rus incidents or cases
|
||||
- successful report generation from real EVTX remains a later validation phase
|
||||
@@ -0,0 +1,76 @@
|
||||
# Hayabusa AW-rus Integration 2026-05-14
|
||||
|
||||
This document defines the bounded integration between Hayabusa DFIR and the normal AW-rus operator path.
|
||||
|
||||
## Purpose
|
||||
|
||||
Hayabusa is used as DFIR enrichment after incidents, not as a new real-time detector.
|
||||
|
||||
## When to use Hayabusa follow-up
|
||||
|
||||
Recommended triggers:
|
||||
|
||||
- high-severity DLP incidents that justify host-side forensic review;
|
||||
- repeated incidents on the same host or user;
|
||||
- suspicious print, USB, email, or document-export activity that needs Windows event corroboration;
|
||||
- operator-driven escalation where case review needs EVTX-based timeline evidence.
|
||||
|
||||
Not recommended:
|
||||
|
||||
- routine low-signal incidents;
|
||||
- replacing normal AW-rus health/runtime checks;
|
||||
- pushing raw Sigma detections into AW buckets.
|
||||
|
||||
## Operator path
|
||||
|
||||
1. Export EVTX package on Windows with `export-evtx-for-hayabusa.ps1`.
|
||||
2. Transfer the resulting zip package to `10.10.10.13`.
|
||||
3. Run one of:
|
||||
|
||||
```bash
|
||||
aw-hayabusa accept --package /path/to/HOST-YYYYMMDD-HHMMSS.zip --host HOST
|
||||
aw-hayabusa process-inbox --mode incident
|
||||
```
|
||||
|
||||
or from Telegram bot:
|
||||
|
||||
```text
|
||||
/aw_dfir /path/to/HOST-YYYYMMDD-HHMMSS.zip HOST [CASE_ID] [MODE]
|
||||
```
|
||||
|
||||
Default mode is `incident`.
|
||||
|
||||
## What gets linked to a case
|
||||
|
||||
Case management stores only bounded metadata:
|
||||
|
||||
- `tool=hayabusa`
|
||||
- `host`
|
||||
- `mode`
|
||||
- `status`
|
||||
- `intake_id`
|
||||
- `package_path`
|
||||
- `sha256`
|
||||
- `report_dir`
|
||||
- `summary_html`
|
||||
- `timeline_path`
|
||||
- `manifest_path`
|
||||
- `linked_at`
|
||||
- `link_source`
|
||||
|
||||
The raw Sigma output, full timelines, and extracted payloads stay under `/opt/hayabusa`, not inside AW buckets or case comments.
|
||||
|
||||
## UI behavior
|
||||
|
||||
Case Management shows a short `DFIR` field:
|
||||
|
||||
- `Hayabusa ok · incident`
|
||||
- `Hayabusa failed-* · incident`
|
||||
|
||||
This is intentionally short; report paths remain operator-facing metadata, not primary UI content.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No raw forensic output is copied into normal AW runtime buckets.
|
||||
- No automatic case creation from Hayabusa findings.
|
||||
- Hayabusa remains an enrichment layer around incidents and investigations.
|
||||
@@ -0,0 +1,163 @@
|
||||
# Hayabusa Operator and IB Guide 2026-05-14
|
||||
|
||||
This document explains the role of Hayabusa inside `AW-rus` for operators and IB.
|
||||
|
||||
## What Hayabusa adds
|
||||
|
||||
Hayabusa adds a bounded DFIR layer for Windows Event Log analysis:
|
||||
|
||||
- EVTX-based timeline review
|
||||
- Sigma-based detection enrichment
|
||||
- logon and activity context around an already interesting host or incident
|
||||
- forensic artifacts that can be attached to case review
|
||||
|
||||
It is useful when `AW-rus` or DLP already surfaced something worth investigating further.
|
||||
|
||||
## What Hayabusa does not replace
|
||||
|
||||
Hayabusa is not:
|
||||
|
||||
- a replacement for normal `AW-rus` runtime monitoring
|
||||
- a replacement for DLP policy enforcement
|
||||
- a real-time SIEM
|
||||
- a reason to copy raw Sigma output into AW buckets or case comments
|
||||
|
||||
The normal operational path remains:
|
||||
|
||||
- `AW-rus` for activity/runtime visibility
|
||||
- DLP collectors and policy engine for signal generation
|
||||
- case management for operator workflow
|
||||
- Hayabusa for bounded forensic enrichment
|
||||
|
||||
## When operators should run it
|
||||
|
||||
Recommended cases:
|
||||
|
||||
- high-severity DLP incidents
|
||||
- repeated suspicious incidents on one host or user
|
||||
- print, USB, file export, or email activity that needs Windows event corroboration
|
||||
- investigation requests from IB after an incident is already known
|
||||
|
||||
Do not run it for every minor signal. It is meant for escalation and investigation, not daily noise.
|
||||
|
||||
## Operator workflow
|
||||
|
||||
1. Export EVTX package on Windows:
|
||||
|
||||
```powershell
|
||||
powershell.exe -ExecutionPolicy Bypass -File C:\ProgramData\AWatch-rus\export-evtx-for-hayabusa.ps1
|
||||
```
|
||||
|
||||
2. Transfer the resulting zip package to `10.10.10.13`.
|
||||
|
||||
3. Run server-side processing:
|
||||
|
||||
```bash
|
||||
aw-hayabusa accept --package /path/to/HOST-YYYYMMDD-HHMMSS.zip --host HOST
|
||||
aw-hayabusa process-inbox --mode incident
|
||||
```
|
||||
|
||||
Or use the Telegram operator path:
|
||||
|
||||
```text
|
||||
/aw_dfir /path/to/HOST-YYYYMMDD-HHMMSS.zip HOST [CASE_ID] [MODE]
|
||||
```
|
||||
|
||||
4. If a case already exists, link only bounded metadata to the case.
|
||||
|
||||
## Where artifacts live
|
||||
|
||||
Windows export staging:
|
||||
|
||||
- `C:\ProgramData\AWatch-rus\forensics\evtx-exports`
|
||||
|
||||
Server-side intake and reports:
|
||||
|
||||
- incoming packages:
|
||||
- `/opt/hayabusa/inbox/incoming`
|
||||
- transient staging:
|
||||
- `/opt/hayabusa/inbox/staging`
|
||||
- archived raw packages:
|
||||
- `/opt/hayabusa/archive/packages/<HOST>/`
|
||||
- archived extracted payloads:
|
||||
- `/opt/hayabusa/archive/extracted/<HOST>/<INTAKE_ID>/payload/`
|
||||
- reports:
|
||||
- `/opt/hayabusa/reports/<HOST>/<UTC_TIMESTAMP>_<MODE>[_LABEL]/`
|
||||
- run state and logs:
|
||||
- `/opt/hayabusa/state`
|
||||
|
||||
## What is stored in AW-rus
|
||||
|
||||
Only bounded metadata is attached to a case:
|
||||
|
||||
- tool
|
||||
- host
|
||||
- mode
|
||||
- status
|
||||
- intake id
|
||||
- package path
|
||||
- sha256
|
||||
- report directory
|
||||
- summary path
|
||||
- timeline path
|
||||
- manifest path
|
||||
- linked timestamp
|
||||
- link source
|
||||
|
||||
Raw forensic output stays under `/opt/hayabusa`.
|
||||
|
||||
## Retention and storage notes
|
||||
|
||||
Windows-side export retention:
|
||||
|
||||
- controlled by `aw_windows_evtx_retention_days`
|
||||
- default: `14` days
|
||||
|
||||
Windows-side export channels:
|
||||
|
||||
- controlled by `aw_windows_evtx_channels`
|
||||
- default set:
|
||||
- `Security`
|
||||
- `System`
|
||||
- `Application`
|
||||
- `Microsoft-Windows-PowerShell/Operational`
|
||||
- `Microsoft-Windows-TerminalServices-LocalSessionManager/Operational`
|
||||
- `Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational`
|
||||
|
||||
Server-side storage:
|
||||
|
||||
- kept outside standard AW buckets
|
||||
- kept outside normal DLP screenshot artifacts
|
||||
- intended for forensic review, not for routine dashboarding
|
||||
|
||||
## IB view
|
||||
|
||||
From an IB perspective, Hayabusa in this project is:
|
||||
|
||||
- a post-incident enrichment layer
|
||||
- useful for Windows event corroboration and timeline reconstruction
|
||||
- intentionally separated from the main activity-monitoring data plane
|
||||
|
||||
This design keeps the main operator UI readable while preserving forensic detail when needed.
|
||||
|
||||
## Limits and false expectations to avoid
|
||||
|
||||
- Sigma detections depend on the quality and completeness of Windows logging.
|
||||
- Missing or weak audit policy reduces value immediately.
|
||||
- No EVTX means no meaningful Hayabusa result.
|
||||
- A successful Hayabusa run does not prove malicious activity by itself.
|
||||
- A clean Hayabusa run does not prove the absence of suspicious behavior.
|
||||
- This contour is deliberately not an always-on detector and not a SIEM replacement.
|
||||
|
||||
## Canonical companion docs
|
||||
|
||||
- source and packaging:
|
||||
- `docs/hayabusa-source-packaging-2026-05-14.md`
|
||||
- server runner:
|
||||
- `docs/hayabusa-server-runner-2026-05-14.md`
|
||||
- artifact workflow:
|
||||
- `docs/hayabusa-artifact-workflow-2026-05-14.md`
|
||||
- AW-rus integration:
|
||||
- `docs/hayabusa-aw-rus-integration-2026-05-14.md`
|
||||
- Windows EVTX export:
|
||||
- `docs/windows-hayabusa-evtx-export.md`
|
||||
@@ -0,0 +1,124 @@
|
||||
# Hayabusa Server-Side Runner 2026-05-14
|
||||
|
||||
This document records the production runner model for Hayabusa on `10.10.10.13`.
|
||||
|
||||
## Install layout
|
||||
|
||||
- root: `/opt/hayabusa`
|
||||
- pinned release: `/opt/hayabusa/releases/v3.9.0`
|
||||
- active symlink: `/opt/hayabusa/current`
|
||||
- operator entrypoint: `/usr/local/bin/aw-hayabusa`
|
||||
|
||||
## Runtime directories
|
||||
|
||||
- inbox: `/opt/hayabusa/inbox`
|
||||
- archive: `/opt/hayabusa/archive`
|
||||
- reports: `/opt/hayabusa/reports`
|
||||
- state: `/opt/hayabusa/state`
|
||||
|
||||
## Operator entrypoint
|
||||
|
||||
Supported helper subcommands:
|
||||
|
||||
- `aw-hayabusa doctor`
|
||||
- `aw-hayabusa inventory`
|
||||
- `aw-hayabusa accept --package <zip> [--host HOST]`
|
||||
- `aw-hayabusa process-inbox [--mode incident] [--limit N]`
|
||||
- `aw-hayabusa profiles`
|
||||
- `aw-hayabusa version`
|
||||
|
||||
Supported analysis modes:
|
||||
|
||||
- `aw-hayabusa quick --input <file-or-dir> [--host HOST]`
|
||||
- `aw-hayabusa incident --input <file-or-dir> [--host HOST]`
|
||||
- `aw-hayabusa full --input <file-or-dir> [--host HOST]`
|
||||
|
||||
## Mode intent
|
||||
|
||||
- `quick`
|
||||
- fast CSV timeline
|
||||
- HTML summary
|
||||
- logon summary
|
||||
- intended for first-pass triage
|
||||
|
||||
- `incident`
|
||||
- JSONL timeline
|
||||
- HTML summary
|
||||
- logon summary
|
||||
- intended for normal incident review
|
||||
|
||||
- `full`
|
||||
- JSONL timeline
|
||||
- deprecated/noisy/unsupported rules enabled
|
||||
- HTML summary
|
||||
- logon summary
|
||||
- intended for deeper DFIR review
|
||||
|
||||
## Output naming
|
||||
|
||||
Reports are stored under:
|
||||
|
||||
- `/opt/hayabusa/reports/<HOST>/<UTC_TIMESTAMP>_<MODE>[_LABEL]/`
|
||||
|
||||
Typical contents:
|
||||
|
||||
- `timeline.csv` or `timeline.jsonl`
|
||||
- `summary.html`
|
||||
- `logon-summary-*.csv`
|
||||
- `run.log`
|
||||
- `manifest.json`
|
||||
|
||||
Latest-run symlinks:
|
||||
|
||||
- `/opt/hayabusa/state/latest-run`
|
||||
- `/opt/hayabusa/state/latest-<HOST>`
|
||||
|
||||
## Intake and archive workflow
|
||||
|
||||
Incoming packages:
|
||||
|
||||
- `/opt/hayabusa/inbox/incoming/*.zip`
|
||||
|
||||
Transient staging:
|
||||
|
||||
- `/opt/hayabusa/inbox/staging/<INTAKE_ID>/`
|
||||
|
||||
Archived raw packages:
|
||||
|
||||
- `/opt/hayabusa/archive/packages/<HOST>/<INTAKE_ID>.zip`
|
||||
|
||||
Archived extracted payloads:
|
||||
|
||||
- `/opt/hayabusa/archive/extracted/<HOST>/<INTAKE_ID>/payload/`
|
||||
- intake metadata:
|
||||
- `/opt/hayabusa/archive/extracted/<HOST>/<INTAKE_ID>/intake.json`
|
||||
|
||||
State/log helpers:
|
||||
|
||||
- `/opt/hayabusa/state/latest-intake.json`
|
||||
- `/opt/hayabusa/state/logs/`
|
||||
|
||||
## Minimal operator flow
|
||||
|
||||
1. Drop or copy an export package:
|
||||
- `aw-hayabusa accept --package /path/to/HOST-YYYYMMDD-HHMMSS.zip`
|
||||
2. Check queue:
|
||||
- `aw-hayabusa inventory`
|
||||
3. Process packages:
|
||||
- `aw-hayabusa process-inbox --mode incident`
|
||||
|
||||
## Validation baseline
|
||||
|
||||
Minimum server-side validation:
|
||||
|
||||
```bash
|
||||
aw-hayabusa doctor
|
||||
aw-hayabusa profiles
|
||||
aw-hayabusa inventory
|
||||
```
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Hayabusa is not deployed as a daemon.
|
||||
- No AW bucket ingestion happens in this phase.
|
||||
- EVTX intake orchestration remains a later phase.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Hayabusa Source and Packaging Decision 2026-05-14
|
||||
|
||||
## Decision
|
||||
|
||||
- upstream source of truth: `Yamato-Security/hayabusa`
|
||||
- fork policy: do not use a fork unless a concrete required patch exists and is documented
|
||||
- runtime role in this project: `DFIR enrichment`
|
||||
|
||||
## Pinned release
|
||||
|
||||
- release tag: `v3.9.0`
|
||||
- server target platform: `x86_64`, `debian 13`
|
||||
- selected asset:
|
||||
- `hayabusa-3.9.0-lin-x64-gnu.zip`
|
||||
- selected asset URL:
|
||||
- `https://github.com/Yamato-Security/hayabusa/releases/download/v3.9.0/hayabusa-3.9.0-lin-x64-gnu.zip`
|
||||
|
||||
## Packaging model
|
||||
|
||||
- analysis host: `10.10.10.13`
|
||||
- install root: `/opt/hayabusa`
|
||||
- versioned release root: `/opt/hayabusa/releases/v3.9.0`
|
||||
- active symlink target:
|
||||
- `/opt/hayabusa/current`
|
||||
- suggested executable path:
|
||||
- `/opt/hayabusa/current/hayabusa`
|
||||
- suggested wrapper path:
|
||||
- `/usr/local/bin/aw-hayabusa`
|
||||
|
||||
## Artifact boundaries
|
||||
|
||||
- raw incoming EVTX:
|
||||
- `/opt/hayabusa/inbox`
|
||||
- processed EVTX archive:
|
||||
- `/opt/hayabusa/archive`
|
||||
- generated reports:
|
||||
- `/opt/hayabusa/reports`
|
||||
- run logs / metadata:
|
||||
- `/opt/hayabusa/state`
|
||||
|
||||
These paths are intentionally outside normal ActivityWatch buckets and outside ordinary DLP artifact roots.
|
||||
|
||||
## Integrity note
|
||||
|
||||
The official release currently does not publish a separate checksum asset in the GitHub release asset list.
|
||||
|
||||
Therefore the deployment model should:
|
||||
|
||||
1. download the pinned asset URL;
|
||||
2. calculate `sha256` locally during automation;
|
||||
3. store the computed value in deployment logs or a local manifest;
|
||||
4. fail deployment if the downloaded asset name or pinned tag does not match expectations.
|
||||
|
||||
## Why this model
|
||||
|
||||
- no dependency on an unreviewed fork;
|
||||
- reproducible server-side installation;
|
||||
- no attempt to run Hayabusa as a real-time daemon;
|
||||
- clean separation between `AW-rus` runtime data and forensic artifacts.
|
||||
@@ -11,6 +11,8 @@
|
||||
- [ИБ-профиль DLP](../dlp-security-functional-spec-ru.md) - подробное описание реализованного DLP/monitoring-контура для службы ИБ
|
||||
- [Runtime status: DLP chain](../dlp-runtime-chain-status-2026-05-13.md) - фактический live-статус policy/cases/integrations/compliance
|
||||
- [Runtime status: Content analysis](../dlp-content-analysis-runtime-status-2026-05-13.md) - фактический live-статус dictionary/regex/OCR/IOC
|
||||
- [Hayabusa AW-rus integration](../hayabusa-aw-rus-integration-2026-05-14.md) - bounded DFIR enrichment path для incidents/cases/operator flow
|
||||
- [Hayabusa operator and IB guide](../hayabusa-operator-ib-guide-2026-05-14.md) - когда запускать forensic path, где лежат артефакты и какие у него границы
|
||||
|
||||
### Компоненты
|
||||
- [DLP Endpoint Monitoring](DLP-Endpoint-Monitoring) - мониторинг clipboard, печати, USB
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# Windows EVTX Export for Hayabusa
|
||||
|
||||
This document defines the Windows-side export path for Hayabusa DFIR enrichment.
|
||||
|
||||
## Purpose
|
||||
|
||||
Windows hosts do not analyze EVTX locally for this contour.
|
||||
|
||||
They export selected event logs into a bounded forensic staging area, and the server-side Hayabusa workflow on `10.10.10.13` analyzes those artifacts later.
|
||||
|
||||
## Export script
|
||||
|
||||
- script: `windows/export-evtx-for-hayabusa.ps1`
|
||||
- deployed path on Windows host:
|
||||
- `<StateRoot>\export-evtx-for-hayabusa.ps1`
|
||||
|
||||
Default config path:
|
||||
|
||||
- `C:\ProgramData\AWatch-rus\deployment-config.json`
|
||||
|
||||
## Default export root
|
||||
|
||||
- `<StateRoot>\forensics\evtx-exports`
|
||||
- Ansible override variable: `aw_windows_forensics_root`
|
||||
- retention override variable: `aw_windows_evtx_retention_days`
|
||||
- channel override variable: `aw_windows_evtx_channels`
|
||||
|
||||
Example:
|
||||
|
||||
- `C:\ProgramData\AWatch-rus\forensics\evtx-exports`
|
||||
|
||||
Each run creates:
|
||||
|
||||
- `<forensics-root>\<HOST>-<YYYYMMDD-HHMMSS>\evtx\*.evtx`
|
||||
- `<forensics-root>\<HOST>-<YYYYMMDD-HHMMSS>\manifest.json`
|
||||
- optional zip:
|
||||
- `<forensics-root>\<HOST>-<YYYYMMDD-HHMMSS>.zip`
|
||||
|
||||
## Default channel set
|
||||
|
||||
- `Security`
|
||||
- `System`
|
||||
- `Application`
|
||||
- `Microsoft-Windows-PowerShell/Operational`
|
||||
- `Microsoft-Windows-TerminalServices-LocalSessionManager/Operational`
|
||||
- `Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational`
|
||||
|
||||
Notes:
|
||||
|
||||
- `Sysmon` is intentionally not assumed by default.
|
||||
- If `Sysmon` exists in the environment, it should be added later as an explicit extension.
|
||||
- the channel list is now carried through deployment config and validation, not left as an implicit script default.
|
||||
|
||||
## Retention
|
||||
|
||||
- default retention: `14` days
|
||||
- cleanup is local to the forensic export root
|
||||
- old export directories and zip packages are removed after the retention cutoff
|
||||
- retention is now exposed as `aw_windows_evtx_retention_days` in Ansible vars
|
||||
|
||||
## Example run
|
||||
|
||||
```powershell
|
||||
powershell.exe -ExecutionPolicy Bypass -File C:\ProgramData\AWatch-rus\export-evtx-for-hayabusa.ps1
|
||||
```
|
||||
|
||||
Example with custom window:
|
||||
|
||||
```powershell
|
||||
powershell.exe -ExecutionPolicy Bypass -File C:\ProgramData\AWatch-rus\export-evtx-for-hayabusa.ps1 -DaysBack 1
|
||||
```
|
||||
|
||||
## Boundaries
|
||||
|
||||
- output stays outside standard AW buckets
|
||||
- output stays outside normal DLP screenshot artifacts
|
||||
- this phase only defines and validates Windows export
|
||||
- transfer to `10.10.10.13` and Hayabusa execution belong to later phases
|
||||
+231
-3
@@ -319,6 +319,7 @@ class TSJGuardianBot:
|
||||
BTN_PFSENSE_CONFIRM = "Подтвердить pfSense: шаг 1"
|
||||
BTN_PFSENSE_CANCEL = "Отменить pfSense изменение"
|
||||
BTN_AW_DLP_CHECK = "Проверка AW-Rus + DLP"
|
||||
BTN_AW_DFIR = "Hayabusa DFIR"
|
||||
BTN_AI_CHAT_ALIASES = ("AI чат", "Чат с поддержкой", "Техподдержка", "Тех поддержка")
|
||||
BTN_OVPN_CERTS_ALIASES = ("OpenVPN certs", "OpenVPN cert", "OpenVPN серты", "OpenVPN сертификат")
|
||||
PFSENSE_ENV_PATH = "/home/codex/infra-admin/vendor/pfsense-mcp-server/.env.readonly"
|
||||
@@ -361,6 +362,12 @@ class TSJGuardianBot:
|
||||
"AW_RUS_DLP_HEAL_CMD",
|
||||
"",
|
||||
).strip()
|
||||
self.aw_rus_case_api_base = os.getenv("AW_RUS_CASE_API_BASE", "http://10.10.10.13:5602").strip()
|
||||
self.aw_rus_hayabusa_enabled = env_bool("AW_RUS_HAYABUSA_ENABLED", True)
|
||||
self.aw_rus_hayabusa_ssh_cmd = os.getenv(
|
||||
"AW_RUS_HAYABUSA_SSH_CMD",
|
||||
"sshpass -p '04091968' ssh -o PubkeyAuthentication=no -o StrictHostKeyChecking=no igor@10.10.10.13",
|
||||
).strip()
|
||||
self.aw_rus_host = os.getenv("AW_RUS_HOST", "SHARKON2025").strip()
|
||||
self.aw_rus_primary_user = os.getenv("AW_RUS_PRIMARY_USER", "USER1").strip()
|
||||
self.aw_rus_stale_sec = max(60, env_int("AW_RUS_STALE_SEC", 900))
|
||||
@@ -491,6 +498,7 @@ class TSJGuardianBot:
|
||||
"keyboard": [
|
||||
[self.BTN_STATUS, self.BTN_CHECK, self.BTN_HEAL],
|
||||
[self.BTN_AW_DLP_CHECK],
|
||||
[self.BTN_AW_DFIR],
|
||||
[self.BTN_ACK, self.BTN_RESOLVE],
|
||||
[self.BTN_AI, self.BTN_FALLBACK],
|
||||
[self.BTN_AI_CHAT],
|
||||
@@ -1988,6 +1996,11 @@ class TSJGuardianBot:
|
||||
"- Проверяет AW-Rus и DLP по свежести bucket-данных и сегодняшнему worktime.\n"
|
||||
"- Формирует операторский итог OK/DEGRADED прямо в чате.\n"
|
||||
"\n"
|
||||
f"{self.BTN_AW_DFIR}\n"
|
||||
"- Показывает, как запустить bounded DFIR-путь через Hayabusa.\n"
|
||||
"- Рабочий формат: `/aw_dfir /path/to/package.zip HOST [CASE_ID] [MODE]`.\n"
|
||||
"- В кейс пишется только metadata/linkage, без raw Sigma output.\n"
|
||||
"\n"
|
||||
f"{self.BTN_HEAL}\n"
|
||||
"- Пробует автоматическое лечение проблем (рестарт нужных сервисов).\n"
|
||||
"- Для критичного заполнения ФС авто-очистка не выполняется, нужен разбор причины.\n"
|
||||
@@ -2088,7 +2101,7 @@ class TSJGuardianBot:
|
||||
"1) Нажмите кнопку создания/восстановления или используйте `/proxmox_snapshot TARGET`.\n"
|
||||
"2) Для восстановления после выбора узла отправьте `/proxmox_restore_apply CODE`.\n"
|
||||
"\n"
|
||||
"Резервные slash-команды: /status /check /aw_dlp_check /heal /ack /resolve /run ... /openvpn_certs [filter] /openvpn_expiring /openvpn_config USER /openvpn_config_confirm /openvpn_config_cancel /openvpn_config_apply CODE /pfsense_confirm /pfsense_cancel /pfsense_apply CODE /proxmox_snapshot TARGET /proxmox_restore TARGET /proxmox_restore_apply CODE /proxmox_restore_cancel /proxmox_selection_cancel"
|
||||
"Резервные slash-команды: /status /check /aw_dlp_check /aw_dfir PACKAGE HOST [CASE_ID] [MODE] /heal /ack /resolve /run ... /openvpn_certs [filter] /openvpn_expiring /openvpn_config USER /openvpn_config_confirm /openvpn_config_cancel /openvpn_config_apply CODE /pfsense_confirm /pfsense_cancel /pfsense_apply CODE /proxmox_snapshot TARGET /proxmox_restore TARGET /proxmox_restore_apply CODE /proxmox_restore_cancel /proxmox_selection_cancel"
|
||||
)
|
||||
|
||||
def _cmd_status(self) -> str:
|
||||
@@ -2205,12 +2218,59 @@ class TSJGuardianBot:
|
||||
except Exception as exc:
|
||||
return None, f"error:{exc}"
|
||||
|
||||
def load_worktime_activity() -> Tuple[Optional[Dict[str, Dict[str, Optional[int]]]], Optional[str]]:
|
||||
try:
|
||||
r = requests.get(f"{base}/buckets", timeout=20)
|
||||
r.raise_for_status()
|
||||
buckets = r.json()
|
||||
except Exception as exc:
|
||||
return None, f"error:{exc}"
|
||||
|
||||
prefix = "aw-worktime-sessions_"
|
||||
activity: Dict[str, Dict[str, Optional[int]]] = {}
|
||||
for bucket_id in sorted(key for key in buckets if key.startswith(prefix)):
|
||||
bucket_host = bucket_id[len(prefix):]
|
||||
latest_ts = None
|
||||
latest_active = False
|
||||
try:
|
||||
r = requests.get(f"{base}/buckets/{bucket_id}/events?limit=20", timeout=20)
|
||||
r.raise_for_status()
|
||||
events = r.json()
|
||||
except Exception as exc:
|
||||
activity[bucket_host] = {
|
||||
"active": False,
|
||||
"age_seconds": None,
|
||||
"error": str(exc),
|
||||
}
|
||||
continue
|
||||
|
||||
if isinstance(events, list):
|
||||
for event in events:
|
||||
raw_ts = event.get("timestamp")
|
||||
if not raw_ts:
|
||||
continue
|
||||
try:
|
||||
event_ts = datetime.fromisoformat(raw_ts.replace("Z", "+00:00")).astimezone(timezone.utc)
|
||||
except Exception:
|
||||
continue
|
||||
if latest_ts is None or event_ts > latest_ts:
|
||||
latest_ts = event_ts
|
||||
latest_active = bool((event.get("data") or {}).get("active"))
|
||||
|
||||
age_seconds = None
|
||||
if latest_ts is not None:
|
||||
age_seconds = max(0, int((now - latest_ts).total_seconds()))
|
||||
activity[bucket_host] = {
|
||||
"active": bool(latest_ts and latest_active and (age_seconds or 0) <= self.aw_rus_stale_sec),
|
||||
"age_seconds": age_seconds,
|
||||
}
|
||||
|
||||
return activity, None
|
||||
|
||||
checks = [
|
||||
(f"aw-watcher-window_{host}", "watcher-window"),
|
||||
(f"aw-watcher-afk_{host}", "watcher-afk"),
|
||||
(f"aw-dlp-endpoint-signals_{host}", "dlp-endpoint"),
|
||||
(f"aw-file-operations_{host}", "dlp-fileops-host"),
|
||||
("aw-file-operations_10.10.10.13", "dlp-fileops-server"),
|
||||
]
|
||||
|
||||
lines = ["Проверка AW-Rus + DLP:"]
|
||||
@@ -2227,6 +2287,46 @@ class TSJGuardianBot:
|
||||
else:
|
||||
lines.append(f"- {label}: OK age={age}s end={tail}")
|
||||
|
||||
worktime_activity, worktime_error = load_worktime_activity()
|
||||
fileops_checks = [
|
||||
(f"aw-file-operations_{host}", "dlp-fileops-host", host),
|
||||
("aw-file-operations_10.10.10.13", "dlp-fileops-server", "10.10.10.13"),
|
||||
]
|
||||
for bucket_id, label, bucket_host in fileops_checks:
|
||||
if worktime_activity is None:
|
||||
age, tail = bucket_age(bucket_id)
|
||||
if age is None:
|
||||
lines.append(f"- {label}: FAIL (worktime-map {worktime_error}; bucket {tail})")
|
||||
failures.append(label)
|
||||
continue
|
||||
if age > self.aw_rus_stale_sec:
|
||||
lines.append(f"- {label}: STALE age={age}s end={tail} (worktime-map unavailable)")
|
||||
failures.append(label)
|
||||
else:
|
||||
lines.append(f"- {label}: OK age={age}s end={tail} (worktime-map unavailable)")
|
||||
continue
|
||||
|
||||
host_meta = worktime_activity.get(bucket_host)
|
||||
if host_meta is None:
|
||||
lines.append(f"- {label}: OK unmanaged host={bucket_host}")
|
||||
continue
|
||||
if not host_meta.get("active"):
|
||||
age_seconds = host_meta.get("age_seconds")
|
||||
age_tail = f" age={age_seconds}s" if age_seconds is not None else ""
|
||||
lines.append(f"- {label}: OK inactive host={bucket_host}{age_tail}")
|
||||
continue
|
||||
|
||||
age, tail = bucket_age(bucket_id)
|
||||
if age is None:
|
||||
lines.append(f"- {label}: FAIL (active host bucket missing or unreadable: {tail})")
|
||||
failures.append(label)
|
||||
continue
|
||||
if age > self.aw_rus_stale_sec:
|
||||
lines.append(f"- {label}: STALE age={age}s end={tail}")
|
||||
failures.append(label)
|
||||
else:
|
||||
lines.append(f"- {label}: OK age={age}s end={tail}")
|
||||
|
||||
try:
|
||||
r = requests.get(f"{worktime_base}/reports/worktime/today?format=csv", timeout=20)
|
||||
r.raise_for_status()
|
||||
@@ -2411,6 +2511,111 @@ class TSJGuardianBot:
|
||||
out.extend(after_lines)
|
||||
return "\n".join(out)
|
||||
|
||||
def _aw_rus_hayabusa_usage_text(self) -> str:
|
||||
return (
|
||||
"Hayabusa DFIR:\n"
|
||||
"- bounded forensic path для EVTX package -> Hayabusa -> case linkage.\n"
|
||||
"- рабочий запуск: /aw_dfir /path/to/package.zip HOST [CASE_ID] [MODE]\n"
|
||||
"- MODE по умолчанию: incident\n"
|
||||
"- в кейс пишутся только metadata и ссылки на артефакты, без raw Sigma output."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _extract_marked_block(text: str, begin_marker: str, end_marker: str) -> str:
|
||||
start = text.find(begin_marker)
|
||||
end = text.find(end_marker)
|
||||
if start < 0 or end < 0 or end <= start:
|
||||
return ""
|
||||
return text[start + len(begin_marker):end].strip()
|
||||
|
||||
def _aw_rus_case_link_hayabusa(self, case_id: int, payload: Dict) -> None:
|
||||
response = requests.post(
|
||||
f"{self.aw_rus_case_api_base.rstrip('/')}/api/0/dlp/cases/{int(case_id)}/forensics/hayabusa",
|
||||
json=payload,
|
||||
timeout=20,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
def _aw_rus_hayabusa_run(
|
||||
self,
|
||||
package_path: str,
|
||||
host: str | None = None,
|
||||
case_id: int | None = None,
|
||||
mode: str = "incident",
|
||||
) -> str:
|
||||
if not self.aw_rus_hayabusa_enabled:
|
||||
return "Hayabusa DFIR trigger отключён."
|
||||
package_path = (package_path or "").strip()
|
||||
host = (host or "").strip() or None
|
||||
mode = (mode or "incident").strip().lower() or "incident"
|
||||
if not package_path:
|
||||
return self._aw_rus_hayabusa_usage_text()
|
||||
if mode not in {"quick", "incident", "full"}:
|
||||
return f"Неверный mode: {mode}. Допустимо: quick, incident, full."
|
||||
|
||||
accept_cmd = f"sudo /usr/local/bin/aw-hayabusa accept --package {shlex.quote(package_path)}"
|
||||
if host:
|
||||
accept_cmd += f" --host {shlex.quote(host)}"
|
||||
remote_script = (
|
||||
"set -eu\n"
|
||||
f"{accept_cmd}\n"
|
||||
"process_rc=0\n"
|
||||
f"sudo /usr/local/bin/aw-hayabusa process-inbox --mode {shlex.quote(mode)} --limit 1 || process_rc=$?\n"
|
||||
"echo '__AW_HAYA_INTAKE_JSON_BEGIN__'\n"
|
||||
"sudo cat /opt/hayabusa/state/latest-intake.json\n"
|
||||
"echo '__AW_HAYA_INTAKE_JSON_END__'\n"
|
||||
"exit \"$process_rc\"\n"
|
||||
)
|
||||
cmd = f"{self.aw_rus_hayabusa_ssh_cmd} bash -lc {shlex.quote(remote_script)}"
|
||||
rc, out = self._run_shell(cmd, timeout_sec=900)
|
||||
json_block = self._extract_marked_block(out, "__AW_HAYA_INTAKE_JSON_BEGIN__", "__AW_HAYA_INTAKE_JSON_END__")
|
||||
if not json_block:
|
||||
tail = "\n".join((out or "").splitlines()[-20:])
|
||||
return f"Hayabusa DFIR: не удалось получить intake metadata.\nrc={rc}\n{tail}"
|
||||
try:
|
||||
intake = json.loads(json_block)
|
||||
except Exception as exc:
|
||||
tail = "\n".join((out or "").splitlines()[-20:])
|
||||
return f"Hayabusa DFIR: intake metadata повреждены ({exc}).\nrc={rc}\n{tail}"
|
||||
|
||||
report_dir = intake.get("report_dir") or ""
|
||||
report_dir = str(report_dir)
|
||||
status = str(intake.get("status") or ("ok" if rc == 0 else f"rc-{rc}"))
|
||||
link_payload = {
|
||||
"host": str(intake.get("host") or host or self.aw_rus_host),
|
||||
"mode": mode,
|
||||
"status": status,
|
||||
"intake_id": intake.get("intake_id"),
|
||||
"package_path": intake.get("package_path"),
|
||||
"sha256": intake.get("sha256"),
|
||||
"report_dir": report_dir or None,
|
||||
"summary_html": f"{report_dir}/summary.html" if report_dir else None,
|
||||
"timeline_path": f"{report_dir}/timeline.jsonl" if report_dir else None,
|
||||
"manifest_path": f"{report_dir}/manifest.json" if report_dir else None,
|
||||
"link_source": "telegram-bot",
|
||||
}
|
||||
linked_line = "- case linkage: skipped"
|
||||
if case_id is not None:
|
||||
self._aw_rus_case_link_hayabusa(case_id, link_payload)
|
||||
linked_line = f"- case linkage: OK case_id={case_id}"
|
||||
|
||||
verdict = "OK" if rc == 0 else f"DEGRADED rc={rc}"
|
||||
lines = [
|
||||
f"Hayabusa DFIR: {verdict}",
|
||||
f"- host: {link_payload['host']}",
|
||||
f"- mode: {mode}",
|
||||
f"- status: {status}",
|
||||
f"- intake_id: {intake.get('intake_id') or '-'}",
|
||||
f"- package: {intake.get('package_path') or package_path}",
|
||||
f"- report_dir: {report_dir or '-'}",
|
||||
linked_line,
|
||||
]
|
||||
if rc != 0:
|
||||
tail = "\n".join((out or "").splitlines()[-20:])
|
||||
lines.append("- runner tail:")
|
||||
lines.append(tail)
|
||||
return "\n".join(lines)
|
||||
|
||||
def _pfsense_security_status_lines(self) -> str:
|
||||
cmd = "/usr/bin/python3 /home/codex/infra-admin/scripts/pfsense_security_status.py"
|
||||
try:
|
||||
@@ -2519,6 +2724,29 @@ class TSJGuardianBot:
|
||||
if text.startswith("/aw_dlp_check") or text == self.BTN_AW_DLP_CHECK:
|
||||
self._send_text(chat_id, self._run_operator_action("aw-dlp-check"))
|
||||
return
|
||||
if text == self.BTN_AW_DFIR:
|
||||
self._send_text(chat_id, self._aw_rus_hayabusa_usage_text())
|
||||
return
|
||||
if text.strip() == "/aw_dfir":
|
||||
self._send_text(chat_id, self._aw_rus_hayabusa_usage_text())
|
||||
return
|
||||
if text.startswith("/aw_dfir "):
|
||||
parts = text.split()
|
||||
if len(parts) < 3:
|
||||
self._send_text(chat_id, self._aw_rus_hayabusa_usage_text())
|
||||
return
|
||||
package_path = parts[1]
|
||||
host = parts[2]
|
||||
case_id = None
|
||||
mode = "incident"
|
||||
if len(parts) >= 4 and parts[3].isdigit():
|
||||
case_id = int(parts[3])
|
||||
if len(parts) >= 5:
|
||||
mode = parts[4]
|
||||
elif len(parts) >= 4:
|
||||
mode = parts[3]
|
||||
self._send_text(chat_id, self._aw_rus_hayabusa_run(package_path=package_path, host=host, case_id=case_id, mode=mode))
|
||||
return
|
||||
if text.startswith("/heal") or text == self.BTN_HEAL:
|
||||
self._send_text(chat_id, self._run_operator_action("heal"))
|
||||
return
|
||||
|
||||
@@ -399,6 +399,7 @@ function Copy-ActivityWatchCollectorAssets {
|
||||
[string]$FileCollectorScriptSource,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SessionCollectorScriptSource,
|
||||
[string]$EvtxExportScriptSource,
|
||||
[string]$EmailCollectorScriptSource,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ExampleRulesSource,
|
||||
@@ -417,6 +418,7 @@ function Copy-ActivityWatchCollectorAssets {
|
||||
$policyClientTarget = Join-Path $StateRoot 'dlp-policy-client.ps1'
|
||||
$fileCollectorTarget = Join-Path $StateRoot 'file-operations-collector.ps1'
|
||||
$sessionCollectorTarget = Join-Path $StateRoot 'worktime-session-collector.ps1'
|
||||
$evtxExportTarget = Join-Path $StateRoot 'export-evtx-for-hayabusa.ps1'
|
||||
$emailCollectorTarget = Join-Path $StateRoot 'email-outbound-collector.ps1'
|
||||
$exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json'
|
||||
$rulesTarget = Join-Path $StateRoot 'web-category-rules.json'
|
||||
@@ -430,6 +432,9 @@ function Copy-ActivityWatchCollectorAssets {
|
||||
}
|
||||
Copy-Item -LiteralPath $FileCollectorScriptSource -Destination $fileCollectorTarget -Force
|
||||
Copy-Item -LiteralPath $SessionCollectorScriptSource -Destination $sessionCollectorTarget -Force
|
||||
if ($EvtxExportScriptSource -and (Test-Path -LiteralPath $EvtxExportScriptSource)) {
|
||||
Copy-Item -LiteralPath $EvtxExportScriptSource -Destination $evtxExportTarget -Force
|
||||
}
|
||||
if ($EmailCollectorScriptSource -and (Test-Path -LiteralPath $EmailCollectorScriptSource)) {
|
||||
Copy-Item -LiteralPath $EmailCollectorScriptSource -Destination $emailCollectorTarget -Force
|
||||
}
|
||||
@@ -458,6 +463,7 @@ function Copy-ActivityWatchCollectorAssets {
|
||||
PolicyClientScript = $policyClientTarget
|
||||
FileCollectorScript = $fileCollectorTarget
|
||||
SessionCollectorScript = $sessionCollectorTarget
|
||||
EvtxExportScript = $evtxExportTarget
|
||||
EmailCollectorScript = $emailCollectorTarget
|
||||
ExampleRules = $exampleRulesTarget
|
||||
ActiveRules = $rulesTarget
|
||||
@@ -489,6 +495,7 @@ function New-ActivityWatchDeploymentConfig {
|
||||
[string]$FileCollectorScript,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SessionCollectorScript,
|
||||
[string]$EvtxExportScript,
|
||||
[string]$EmailCollectorScript,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RulesPath,
|
||||
@@ -507,6 +514,9 @@ function New-ActivityWatchDeploymentConfig {
|
||||
[bool]$IncidentCaptureEnabled = $true,
|
||||
[bool]$IncidentScreenshotEnabled = $true,
|
||||
[string]$IncidentArtifactsRoot,
|
||||
[string]$EvtxExportRoot,
|
||||
[int]$EvtxRetentionDays = 14,
|
||||
[string[]]$EvtxChannels = @(),
|
||||
[bool]$LogonMarkerEnabled = $true,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$LaunchScriptPath,
|
||||
@@ -529,6 +539,19 @@ function New-ActivityWatchDeploymentConfig {
|
||||
)
|
||||
|
||||
$effectiveIncidentArtifactsRoot = if ($IncidentArtifactsRoot) { $IncidentArtifactsRoot } else { Join-Path $StateRoot 'incident-artifacts' }
|
||||
$effectiveEvtxExportRoot = if ($EvtxExportRoot) { $EvtxExportRoot } else { Join-Path $StateRoot 'forensics\evtx-exports' }
|
||||
$effectiveEvtxChannels = if ($EvtxChannels -and $EvtxChannels.Count -gt 0) {
|
||||
@($EvtxChannels)
|
||||
} else {
|
||||
@(
|
||||
'Security',
|
||||
'System',
|
||||
'Application',
|
||||
'Microsoft-Windows-PowerShell/Operational',
|
||||
'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational',
|
||||
'Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational'
|
||||
)
|
||||
}
|
||||
$effectivePolicyEngineHost = if ([string]::IsNullOrWhiteSpace($PolicyEngineHost)) { $ServerHost } else { $PolicyEngineHost }
|
||||
$effectivePolicyCachePath = if ([string]::IsNullOrWhiteSpace($PolicyCachePath)) { Join-Path $StateRoot 'dlp-policy-cache.json' } else { $PolicyCachePath }
|
||||
|
||||
@@ -551,6 +574,7 @@ function New-ActivityWatchDeploymentConfig {
|
||||
emailCollectorScript = $EmailCollectorScript
|
||||
fileCollectorScript = $FileCollectorScript
|
||||
sessionCollectorScript = $SessionCollectorScript
|
||||
evtxExportScript = $EvtxExportScript
|
||||
rulesPath = $RulesPath
|
||||
policyPath = $PolicyPath
|
||||
launchScript = $LaunchScriptPath
|
||||
@@ -574,6 +598,11 @@ function New-ActivityWatchDeploymentConfig {
|
||||
screenshotEnabled = $IncidentScreenshotEnabled
|
||||
artifactsRoot = $effectiveIncidentArtifactsRoot
|
||||
}
|
||||
forensics = [pscustomobject]@{
|
||||
evtxExportRoot = $effectiveEvtxExportRoot
|
||||
retentionDays = $EvtxRetentionDays
|
||||
evtxChannels = @($effectiveEvtxChannels)
|
||||
}
|
||||
sessionEvents = [pscustomobject]@{
|
||||
logonEnabled = $LogonMarkerEnabled
|
||||
bucketPrefix = 'aw-session-events'
|
||||
|
||||
@@ -23,6 +23,9 @@ param(
|
||||
[bool]$IncidentCaptureEnabled = $true,
|
||||
[bool]$IncidentScreenshotEnabled = $true,
|
||||
[string]$IncidentArtifactsRoot,
|
||||
[string]$EvtxExportRoot,
|
||||
[int]$EvtxRetentionDays = 14,
|
||||
[string[]]$EvtxChannels = @(),
|
||||
[bool]$LogonMarkerEnabled = $true,
|
||||
[string]$AwHostname,
|
||||
[string]$CustomRulesPath,
|
||||
@@ -60,6 +63,7 @@ $policyClientSource = Join-Path $PSScriptRoot 'dlp-policy-client.ps1'
|
||||
$emailCollectorSource = Join-Path $PSScriptRoot 'email-outbound-collector.ps1'
|
||||
$fileCollectorSource = Join-Path $PSScriptRoot 'file-operations-collector.ps1'
|
||||
$sessionCollectorSource = Join-Path $PSScriptRoot 'worktime-session-collector.ps1'
|
||||
$evtxExportScriptSource = Join-Path $PSScriptRoot 'export-evtx-for-hayabusa.ps1'
|
||||
$exampleRulesSource = Join-Path $PSScriptRoot 'web-category-rules.example.json'
|
||||
$examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json'
|
||||
|
||||
@@ -78,6 +82,7 @@ $assetResult = Copy-ActivityWatchCollectorAssets `
|
||||
-EmailCollectorScriptSource $emailCollectorSource `
|
||||
-FileCollectorScriptSource $fileCollectorSource `
|
||||
-SessionCollectorScriptSource $sessionCollectorSource `
|
||||
-EvtxExportScriptSource $evtxExportScriptSource `
|
||||
-ExampleRulesSource $exampleRulesSource `
|
||||
-ExamplePolicySource $examplePolicySource `
|
||||
-StateRoot $StateRoot `
|
||||
@@ -101,6 +106,7 @@ $config = New-ActivityWatchDeploymentConfig `
|
||||
-EmailCollectorScript $assetResult.EmailCollectorScript `
|
||||
-FileCollectorScript $assetResult.FileCollectorScript `
|
||||
-SessionCollectorScript $assetResult.SessionCollectorScript `
|
||||
-EvtxExportScript $assetResult.EvtxExportScript `
|
||||
-RulesPath $assetResult.ActiveRules `
|
||||
-PolicyPath $assetResult.ActivePolicy `
|
||||
-PollSeconds $PollSeconds `
|
||||
@@ -113,6 +119,9 @@ $config = New-ActivityWatchDeploymentConfig `
|
||||
-IncidentCaptureEnabled $IncidentCaptureEnabled `
|
||||
-IncidentScreenshotEnabled $IncidentScreenshotEnabled `
|
||||
-IncidentArtifactsRoot $IncidentArtifactsRoot `
|
||||
-EvtxExportRoot $EvtxExportRoot `
|
||||
-EvtxRetentionDays $EvtxRetentionDays `
|
||||
-EvtxChannels $EvtxChannels `
|
||||
-LogonMarkerEnabled $LogonMarkerEnabled `
|
||||
-AwHostname $AwHostname `
|
||||
-PolicyMode $PolicyMode `
|
||||
|
||||
@@ -23,6 +23,9 @@ param(
|
||||
[bool]$IncidentCaptureEnabled = $true,
|
||||
[bool]$IncidentScreenshotEnabled = $true,
|
||||
[string]$IncidentArtifactsRoot,
|
||||
[string]$EvtxExportRoot,
|
||||
[int]$EvtxRetentionDays = 14,
|
||||
[string[]]$EvtxChannels = @(),
|
||||
[bool]$LogonMarkerEnabled = $true,
|
||||
[string]$AwHostname,
|
||||
[string]$CustomRulesPath,
|
||||
@@ -81,6 +84,9 @@ if (-not (Test-Path -LiteralPath $deployScript)) {
|
||||
-IncidentCaptureEnabled $IncidentCaptureEnabled `
|
||||
-IncidentScreenshotEnabled $IncidentScreenshotEnabled `
|
||||
-IncidentArtifactsRoot $IncidentArtifactsRoot `
|
||||
-EvtxExportRoot $EvtxExportRoot `
|
||||
-EvtxRetentionDays $EvtxRetentionDays `
|
||||
-EvtxChannels $EvtxChannels `
|
||||
-LogonMarkerEnabled $LogonMarkerEnabled `
|
||||
-AwHostname $AwHostname `
|
||||
-CustomRulesPath $CustomRulesPath `
|
||||
@@ -113,6 +119,9 @@ if (-not $SkipHardening) {
|
||||
-IncidentCaptureEnabled $IncidentCaptureEnabled `
|
||||
-IncidentScreenshotEnabled $IncidentScreenshotEnabled `
|
||||
-IncidentArtifactsRoot $IncidentArtifactsRoot `
|
||||
-EvtxExportRoot $EvtxExportRoot `
|
||||
-EvtxRetentionDays $EvtxRetentionDays `
|
||||
-EvtxChannels $EvtxChannels `
|
||||
-LogonMarkerEnabled $LogonMarkerEnabled `
|
||||
-AwHostname $AwHostname `
|
||||
-CustomRulesPath $CustomRulesPath `
|
||||
|
||||
@@ -21,6 +21,9 @@ param(
|
||||
[bool]$IncidentCaptureEnabled = $true,
|
||||
[bool]$IncidentScreenshotEnabled = $true,
|
||||
[string]$IncidentArtifactsRoot,
|
||||
[string]$EvtxExportRoot,
|
||||
[int]$EvtxRetentionDays = 14,
|
||||
[string[]]$EvtxChannels = @(),
|
||||
[bool]$LogonMarkerEnabled = $true,
|
||||
[string]$AwHostname,
|
||||
[string]$CustomRulesPath,
|
||||
@@ -45,6 +48,7 @@ $collectorSource = Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1
|
||||
$endpointCollectorSource = Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1'
|
||||
$emailCollectorSource = Join-Path $PSScriptRoot 'email-outbound-collector.ps1'
|
||||
$sessionCollectorSource = Join-Path $PSScriptRoot 'worktime-session-collector.ps1'
|
||||
$evtxExportScriptSource = Join-Path $PSScriptRoot 'export-evtx-for-hayabusa.ps1'
|
||||
$exampleRulesSource = Join-Path $PSScriptRoot 'web-category-rules.example.json'
|
||||
$examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json'
|
||||
|
||||
@@ -60,6 +64,7 @@ $assetResult = Copy-ActivityWatchCollectorAssets `
|
||||
-EndpointCollectorScriptSource $endpointCollectorSource `
|
||||
-EmailCollectorScriptSource $emailCollectorSource `
|
||||
-SessionCollectorScriptSource $sessionCollectorSource `
|
||||
-EvtxExportScriptSource $evtxExportScriptSource `
|
||||
-ExampleRulesSource $exampleRulesSource `
|
||||
-ExamplePolicySource $examplePolicySource `
|
||||
-StateRoot $StateRoot `
|
||||
@@ -81,6 +86,7 @@ $config = New-ActivityWatchDeploymentConfig `
|
||||
-EndpointCollectorScript $assetResult.EndpointCollectorScript `
|
||||
-EmailCollectorScript $assetResult.EmailCollectorScript `
|
||||
-SessionCollectorScript $assetResult.SessionCollectorScript `
|
||||
-EvtxExportScript $assetResult.EvtxExportScript `
|
||||
-RulesPath $assetResult.ActiveRules `
|
||||
-PolicyPath $assetResult.ActivePolicy `
|
||||
-PollSeconds $PollSeconds `
|
||||
@@ -92,6 +98,9 @@ $config = New-ActivityWatchDeploymentConfig `
|
||||
-IncidentCaptureEnabled $IncidentCaptureEnabled `
|
||||
-IncidentScreenshotEnabled $IncidentScreenshotEnabled `
|
||||
-IncidentArtifactsRoot $IncidentArtifactsRoot `
|
||||
-EvtxExportRoot $EvtxExportRoot `
|
||||
-EvtxRetentionDays $EvtxRetentionDays `
|
||||
-EvtxChannels $EvtxChannels `
|
||||
-LogonMarkerEnabled $LogonMarkerEnabled `
|
||||
-AwHostname $AwHostname `
|
||||
-LaunchScriptPath $launchScriptPath `
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[string]$OutputRoot,
|
||||
[int]$RetentionDays,
|
||||
[string[]]$Channels,
|
||||
[int]$DaysBack = 3,
|
||||
[switch]$NoZip
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function New-Directory {
|
||||
param([string]$Path)
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
New-Item -ItemType Directory -Path $Path -Force | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ConfigValue {
|
||||
param(
|
||||
[object]$Config,
|
||||
[string]$Section,
|
||||
[string]$Name,
|
||||
$DefaultValue
|
||||
)
|
||||
if ($null -eq $Config) { return $DefaultValue }
|
||||
if ($Config.PSObject.Properties.Name -notcontains $Section) { return $DefaultValue }
|
||||
$sectionValue = $Config.$Section
|
||||
if ($null -eq $sectionValue) { return $DefaultValue }
|
||||
if ($sectionValue.PSObject.Properties.Name -notcontains $Name) { return $DefaultValue }
|
||||
return $sectionValue.$Name
|
||||
}
|
||||
|
||||
$config = $null
|
||||
if (Test-Path -LiteralPath $ConfigPath) {
|
||||
$config = Get-Content -LiteralPath $ConfigPath -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
$effectiveOutputRoot = if ($OutputRoot) {
|
||||
$OutputRoot
|
||||
} else {
|
||||
[string](Get-ConfigValue -Config $config -Section 'forensics' -Name 'evtxExportRoot' -DefaultValue 'C:\ProgramData\AWatch-rus\forensics\evtx-exports')
|
||||
}
|
||||
$effectiveRetentionDays = if ($PSBoundParameters.ContainsKey('RetentionDays')) {
|
||||
$RetentionDays
|
||||
} else {
|
||||
[int](Get-ConfigValue -Config $config -Section 'forensics' -Name 'retentionDays' -DefaultValue 14)
|
||||
}
|
||||
$effectiveChannels = if ($Channels -and $Channels.Count -gt 0) {
|
||||
@($Channels)
|
||||
} else {
|
||||
@(Get-ConfigValue -Config $config -Section 'forensics' -Name 'evtxChannels' -DefaultValue @(
|
||||
'Security',
|
||||
'System',
|
||||
'Application',
|
||||
'Microsoft-Windows-PowerShell/Operational',
|
||||
'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational',
|
||||
'Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational'
|
||||
))
|
||||
}
|
||||
|
||||
New-Directory -Path $effectiveOutputRoot
|
||||
|
||||
$hostName = if ($config -and $config.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$config.awHostname)) {
|
||||
[string]$config.awHostname
|
||||
} else {
|
||||
[string]$env:COMPUTERNAME
|
||||
}
|
||||
|
||||
$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$batchRoot = Join-Path $effectiveOutputRoot "$hostName-$timestamp"
|
||||
$evtxRoot = Join-Path $batchRoot 'evtx'
|
||||
$metaPath = Join-Path $batchRoot 'manifest.json'
|
||||
$zipPath = Join-Path $effectiveOutputRoot "$hostName-$timestamp.zip"
|
||||
|
||||
New-Directory -Path $batchRoot
|
||||
New-Directory -Path $evtxRoot
|
||||
|
||||
$daysBackMs = [int64]$DaysBack * 24 * 60 * 60 * 1000
|
||||
$query = "*[System[TimeCreated[timediff(@SystemTime) <= $daysBackMs]]]"
|
||||
$results = @()
|
||||
|
||||
foreach ($channel in @($effectiveChannels | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })) {
|
||||
$safeName = (($channel -replace '[\\/:*?""<>| ]', '_').Trim('_'))
|
||||
$targetPath = Join-Path $evtxRoot ($safeName + '.evtx')
|
||||
try {
|
||||
& wevtutil.exe epl $channel $targetPath /ow:true /q:$query | Out-Null
|
||||
$exists = Test-Path -LiteralPath $targetPath
|
||||
$size = if ($exists) { (Get-Item -LiteralPath $targetPath).Length } else { 0 }
|
||||
$results += [pscustomobject]@{
|
||||
channel = $channel
|
||||
path = $targetPath
|
||||
exported = $exists
|
||||
size = $size
|
||||
status = if ($exists) { 'ok' } else { 'empty' }
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$results += [pscustomobject]@{
|
||||
channel = $channel
|
||||
path = $targetPath
|
||||
exported = $false
|
||||
size = 0
|
||||
status = 'error'
|
||||
error = $_.Exception.Message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$manifest = [ordered]@{
|
||||
generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
|
||||
hostname = $hostName
|
||||
configPath = $ConfigPath
|
||||
outputRoot = $effectiveOutputRoot
|
||||
batchRoot = $batchRoot
|
||||
zipPath = if ($NoZip) { $null } else { $zipPath }
|
||||
daysBack = $DaysBack
|
||||
retentionDays = $effectiveRetentionDays
|
||||
channels = @($effectiveChannels)
|
||||
exports = @($results)
|
||||
}
|
||||
$manifest | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $metaPath -Encoding UTF8
|
||||
|
||||
if (-not $NoZip) {
|
||||
if (Test-Path -LiteralPath $zipPath) {
|
||||
Remove-Item -LiteralPath $zipPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
Compress-Archive -Path (Join-Path $batchRoot '*') -DestinationPath $zipPath -Force
|
||||
}
|
||||
|
||||
$cutoff = (Get-Date).AddDays(-1 * [Math]::Max(1, $effectiveRetentionDays))
|
||||
Get-ChildItem -LiteralPath $effectiveOutputRoot -Directory -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.LastWriteTime -lt $cutoff } |
|
||||
ForEach-Object { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
Get-ChildItem -LiteralPath $effectiveOutputRoot -File -Filter '*.zip' -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.LastWriteTime -lt $cutoff } |
|
||||
ForEach-Object { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue }
|
||||
|
||||
$manifest
|
||||
@@ -20,6 +20,7 @@ param(
|
||||
[bool]$IncidentCaptureEnabled,
|
||||
[bool]$IncidentScreenshotEnabled,
|
||||
[string]$IncidentArtifactsRoot,
|
||||
[string]$EvtxExportRoot,
|
||||
[bool]$LogonMarkerEnabled,
|
||||
[string]$AwHostname,
|
||||
[string]$CustomRulesPath,
|
||||
@@ -66,6 +67,7 @@ $effectiveCollector = Join-Path $effectiveStateRoot 'browser-domains-native-coll
|
||||
$effectiveEndpointCollector = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]$existingConfig.paths.endpointCollectorScript } else { Join-Path $effectiveStateRoot 'dlp-endpoint-signals-collector.ps1' }
|
||||
$effectiveFileCollector = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]$existingConfig.paths.fileCollectorScript } else { Join-Path $effectiveStateRoot 'file-operations-collector.ps1' }
|
||||
$effectiveSessionCollector = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$existingConfig.paths.sessionCollectorScript } else { Join-Path $effectiveStateRoot 'worktime-session-collector.ps1' }
|
||||
$effectiveEvtxExportScript = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'evtxExportScript') { [string]$existingConfig.paths.evtxExportScript } else { Join-Path $effectiveStateRoot 'export-evtx-for-hayabusa.ps1' }
|
||||
$effectiveRules = Join-Path $effectiveStateRoot 'web-category-rules.json'
|
||||
$effectivePolicy = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$existingConfig.paths.policyPath } else { Join-Path $effectiveStateRoot 'dlp-policy.json' }
|
||||
$effectivePolicyClientScript = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'policyClientScript') { [string]$existingConfig.paths.policyClientScript } else { Join-Path $effectiveStateRoot 'dlp-policy-client.ps1' }
|
||||
@@ -83,6 +85,9 @@ $effectiveLocalAgentLogsEnabled = if ($PSBoundParameters.ContainsKey('LocalAgent
|
||||
$effectiveIncidentCaptureEnabled = if ($PSBoundParameters.ContainsKey('IncidentCaptureEnabled')) { [bool]$IncidentCaptureEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $existingConfig.incidentCapture.PSObject.Properties.Name -contains 'enabled') { [bool]$existingConfig.incidentCapture.enabled } else { $true }
|
||||
$effectiveIncidentScreenshotEnabled = if ($PSBoundParameters.ContainsKey('IncidentScreenshotEnabled')) { [bool]$IncidentScreenshotEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $existingConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$existingConfig.incidentCapture.screenshotEnabled } else { $true }
|
||||
$effectiveIncidentArtifactsRoot = if ($PSBoundParameters.ContainsKey('IncidentArtifactsRoot') -and $IncidentArtifactsRoot) { $IncidentArtifactsRoot } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $existingConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$existingConfig.incidentCapture.artifactsRoot } else { Join-Path $effectiveStateRoot 'incident-artifacts' }
|
||||
$effectiveEvtxExportRoot = if ($PSBoundParameters.ContainsKey('EvtxExportRoot') -and $EvtxExportRoot) { $EvtxExportRoot } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'evtxExportRoot') { [string]$existingConfig.forensics.evtxExportRoot } else { Join-Path $effectiveStateRoot 'forensics\evtx-exports' }
|
||||
$effectiveEvtxRetentionDays = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'retentionDays') { [int]$existingConfig.forensics.retentionDays } else { 14 }
|
||||
$effectiveEvtxChannels = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'evtxChannels') { @($existingConfig.forensics.evtxChannels) } else { @() }
|
||||
$effectiveLogonMarkerEnabled = if ($PSBoundParameters.ContainsKey('LogonMarkerEnabled')) { [bool]$LogonMarkerEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'sessionEvents' -and $existingConfig.sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]$existingConfig.sessionEvents.logonEnabled } else { $true }
|
||||
$effectiveAwHostname = if ($PSBoundParameters.ContainsKey('AwHostname') -and -not [string]::IsNullOrWhiteSpace($AwHostname)) { [string]$AwHostname } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$existingConfig.awHostname)) { [string]$existingConfig.awHostname } else { [string]$env:COMPUTERNAME }
|
||||
$effectiveVersion = if ($Version) { $Version } elseif ($existingConfig) { [string]$existingConfig.package.version } else { 'v0.13.2' }
|
||||
@@ -123,6 +128,7 @@ $assetResult = Copy-ActivityWatchCollectorAssets `
|
||||
-EmailCollectorScriptSource (Join-Path $PSScriptRoot 'email-outbound-collector.ps1') `
|
||||
-FileCollectorScriptSource (Join-Path $PSScriptRoot 'file-operations-collector.ps1') `
|
||||
-SessionCollectorScriptSource (Join-Path $PSScriptRoot 'worktime-session-collector.ps1') `
|
||||
-EvtxExportScriptSource (Join-Path $PSScriptRoot 'export-evtx-for-hayabusa.ps1') `
|
||||
-ExampleRulesSource (Join-Path $PSScriptRoot 'web-category-rules.example.json') `
|
||||
-ExamplePolicySource (Join-Path $PSScriptRoot 'dlp-policy.example.json') `
|
||||
-StateRoot $effectiveStateRoot `
|
||||
@@ -146,6 +152,7 @@ $config = New-ActivityWatchDeploymentConfig `
|
||||
-EmailCollectorScript $assetResult.EmailCollectorScript `
|
||||
-FileCollectorScript $effectiveFileCollector `
|
||||
-SessionCollectorScript $effectiveSessionCollector `
|
||||
-EvtxExportScript $effectiveEvtxExportScript `
|
||||
-RulesPath $effectiveRules `
|
||||
-PolicyPath $effectivePolicy `
|
||||
-PollSeconds $effectivePollSeconds `
|
||||
@@ -158,6 +165,9 @@ $config = New-ActivityWatchDeploymentConfig `
|
||||
-IncidentCaptureEnabled $effectiveIncidentCaptureEnabled `
|
||||
-IncidentScreenshotEnabled $effectiveIncidentScreenshotEnabled `
|
||||
-IncidentArtifactsRoot $effectiveIncidentArtifactsRoot `
|
||||
-EvtxExportRoot $effectiveEvtxExportRoot `
|
||||
-EvtxRetentionDays $effectiveEvtxRetentionDays `
|
||||
-EvtxChannels $effectiveEvtxChannels `
|
||||
-LogonMarkerEnabled $effectiveLogonMarkerEnabled `
|
||||
-AwHostname $effectiveAwHostname `
|
||||
-PolicyMode $effectivePolicyMode `
|
||||
|
||||
@@ -16,6 +16,7 @@ $collectorScript = [string]$config.paths.collectorScript
|
||||
$endpointCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]$config.paths.endpointCollectorScript } else { Join-Path $stateRoot 'dlp-endpoint-signals-collector.ps1' }
|
||||
$fileCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]$config.paths.fileCollectorScript } else { Join-Path $stateRoot 'file-operations-collector.ps1' }
|
||||
$sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' }
|
||||
$evtxExportScript = if ($config.paths.PSObject.Properties.Name -contains 'evtxExportScript') { [string]$config.paths.evtxExportScript } else { Join-Path $stateRoot 'export-evtx-for-hayabusa.ps1' }
|
||||
$rulesPath = [string]$config.paths.rulesPath
|
||||
$policyPath = if ($config.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$config.paths.policyPath } else { Join-Path $stateRoot 'dlp-policy.json' }
|
||||
$policyClientScript = if ($config.paths.PSObject.Properties.Name -contains 'policyClientScript') { [string]$config.paths.policyClientScript } else { Join-Path $stateRoot 'dlp-policy-client.ps1' }
|
||||
@@ -43,6 +44,7 @@ $requiredFiles = @(
|
||||
$collectorScript,
|
||||
$endpointCollectorScript,
|
||||
$sessionCollectorScript,
|
||||
$evtxExportScript,
|
||||
$rulesPath,
|
||||
$policyPath,
|
||||
$policyClientScript,
|
||||
@@ -202,8 +204,20 @@ $result = [ordered]@{
|
||||
jobTitlePolicyEnabled = $printJobTitlePolicyEnabled
|
||||
ok = [bool]($printServiceOperationalEnabled -and $printJobTitlePolicyEnabled)
|
||||
}
|
||||
forensics = [ordered]@{
|
||||
evtxExportRoot = if ($config.PSObject.Properties.Name -contains 'forensics' -and $config.forensics.PSObject.Properties.Name -contains 'evtxExportRoot') { [string]$config.forensics.evtxExportRoot } else { $null }
|
||||
retentionDays = if ($config.PSObject.Properties.Name -contains 'forensics' -and $config.forensics.PSObject.Properties.Name -contains 'retentionDays') { [int]$config.forensics.retentionDays } else { $null }
|
||||
evtxChannels = if ($config.PSObject.Properties.Name -contains 'forensics' -and $config.forensics.PSObject.Properties.Name -contains 'evtxChannels') { @($config.forensics.evtxChannels) } else { @() }
|
||||
ok = [bool](
|
||||
($config.PSObject.Properties.Name -contains 'forensics') -and
|
||||
($config.forensics.PSObject.Properties.Name -contains 'evtxExportRoot') -and
|
||||
($config.forensics.PSObject.Properties.Name -contains 'retentionDays') -and
|
||||
($config.forensics.PSObject.Properties.Name -contains 'evtxChannels') -and
|
||||
(@($config.forensics.evtxChannels).Count -gt 0)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
$result.overallOk = [bool]($result.files.ok -and $result.tasks.ok -and $result.processes.ok -and $result.printTelemetry.ok)
|
||||
$result.overallOk = [bool]($result.files.ok -and $result.tasks.ok -and $result.processes.ok -and $result.printTelemetry.ok -and $result.forensics.ok)
|
||||
|
||||
$result
|
||||
|
||||
Reference in New Issue
Block a user