feat(dfir): add hayabusa forensic workflow integration

This commit is contained in:
igor04091968
2026-05-14 15:04:07 +03:00
parent 563bd910d1
commit 0cce6fd08e
28 changed files with 1907 additions and 25 deletions
+93
View File
@@ -1314,6 +1314,99 @@
msg: "{{ aw_hayabusa_ioc_refresh_result.stdout }}" msg: "{{ aw_hayabusa_ioc_refresh_result.stdout }}"
when: aw_hayabusa_ioc_refresh_result.stdout is defined 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) - name: Post-deploy health gate (aw-health-check)
when: when:
- not ansible_check_mode - not ansible_check_mode
+10
View File
@@ -38,6 +38,7 @@
aw_windows_incident_capture_enabled: true aw_windows_incident_capture_enabled: true
aw_windows_incident_screenshot_enabled: true aw_windows_incident_screenshot_enabled: true
aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts" 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_logon_marker_enabled: true
aw_windows_skip_hardening: false aw_windows_skip_hardening: false
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json" 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' }} IncidentCaptureEnabled = {{ '$true' if (aw_windows_incident_capture_enabled | bool) else '$false' }}
IncidentScreenshotEnabled = {{ '$true' if (aw_windows_incident_screenshot_enabled | bool) else '$false' }} IncidentScreenshotEnabled = {{ '$true' if (aw_windows_incident_screenshot_enabled | bool) else '$false' }}
IncidentArtifactsRoot = "{{ aw_windows_incident_artifacts_root }}" 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' }} LogonMarkerEnabled = {{ '$true' if (aw_windows_logon_marker_enabled | bool) else '$false' }}
PolicyMode = "{{ aw_windows_policy_mode }}" PolicyMode = "{{ aw_windows_policy_mode }}"
PolicyEngineEnabled = {{ '$true' if (aw_windows_policy_engine_enabled | bool) else '$false' }} 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 %} {% if (aw_windows_package_zip_path | default('') | string | length) > 0 %}
$params.PackageZipPath = "{{ aw_windows_package_zip_path }}" $params.PackageZipPath = "{{ aw_windows_package_zip_path }}"
{% endif %} {% 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 %} {% if (aw_windows_hostname_override | default('') | string | length) > 0 %}
$params.AwHostname = "{{ aw_windows_hostname_override }}" $params.AwHostname = "{{ aw_windows_hostname_override }}"
{% endif %} {% endif %}
+54 -6
View File
@@ -18,14 +18,24 @@
tsj_bot_default_chat_id: "{{ telegram_default_chat_id | default(telegram_allowed_chat_ids.split(',')[0]) }}" tsj_bot_default_chat_id: "{{ telegram_default_chat_id | default(telegram_allowed_chat_ids.split(',')[0]) }}"
pre_tasks: pre_tasks:
- name: Проверить наличие существующего .env бота на хосте
ansible.builtin.stat:
path: "{{ tsj_bot_env_path }}"
register: tsj_bot_existing_env
- name: Проверить обязательные переменные - name: Проверить обязательные переменные
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- telegram_bot_token is defined - >
- telegram_bot_token | length > 20 (
- telegram_allowed_chat_ids is defined telegram_bot_token is defined and
- telegram_allowed_chat_ids | length > 0 (telegram_bot_token | string | length) > 20 and
fail_msg: "Задайте telegram_bot_token и telegram_allowed_chat_ids (см. group_vars/proxmox-bot.example.yml)." 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: Проверить наличие исходного файла бота на контроллере - name: Проверить наличие исходного файла бота на контроллере
ansible.builtin.stat: ansible.builtin.stat:
@@ -70,7 +80,12 @@
mode: "0750" mode: "0750"
notify: Restart tsj bot 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: ansible.builtin.copy:
dest: "{{ tsj_bot_env_path }}" dest: "{{ tsj_bot_env_path }}"
owner: "{{ tsj_bot_user }}" 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_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_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_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_HOST={{ tsj_bot_aw_rus_host | default('SHARKON2025') }}
AW_RUS_PRIMARY_USER={{ tsj_bot_aw_rus_primary_user | default('USER1') }} AW_RUS_PRIMARY_USER={{ tsj_bot_aw_rus_primary_user | default('USER1') }}
AW_RUS_STALE_SEC={{ tsj_bot_aw_rus_stale_sec | default(900) }} AW_RUS_STALE_SEC={{ tsj_bot_aw_rus_stale_sec | default(900) }}
notify: Restart tsj bot 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 бота - name: Установить systemd unit бота
ansible.builtin.copy: ansible.builtin.copy:
dest: "/etc/systemd/system/{{ tsj_bot_service_name }}" dest: "/etc/systemd/system/{{ tsj_bot_service_name }}"
+18
View File
@@ -12,6 +12,24 @@ ansible_become_password: "{{ lookup('env', 'AW_SUDO_PASSWORD') | default(lookup(
aw_hayabusa_ioc_refresh_enabled: false aw_hayabusa_ioc_refresh_enabled: false
aw_hayabusa_rules_root: "/mnt/usb_hdd1/Projects/hayabusa/rules" aw_hayabusa_rules_root: "/mnt/usb_hdd1/Projects/hayabusa/rules"
aw_hayabusa_ioc_output_dir: "{{ aw_server_data_dir }}/dlp-ioc" 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_enabled: true
aw_dlp_policy_engine_bind_host: "0.0.0.0" aw_dlp_policy_engine_bind_host: "0.0.0.0"
aw_dlp_policy_engine_port: 5601 aw_dlp_policy_engine_port: 5601
+9
View File
@@ -41,6 +41,15 @@ aw_windows_local_agent_logs_enabled: false
aw_windows_incident_capture_enabled: true aw_windows_incident_capture_enabled: true
aw_windows_incident_screenshot_enabled: true aw_windows_incident_screenshot_enabled: true
aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts" 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_logon_marker_enabled: true
aw_windows_skip_hardening: false 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_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_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_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_host: "SHARKON2025"
tsj_bot_aw_rus_primary_user: "USER1" tsj_bot_aw_rus_primary_user: "USER1"
tsj_bot_aw_rus_stale_sec: 900 tsj_bot_aw_rus_stale_sec: 900
+9
View File
@@ -29,6 +29,15 @@ aw_windows_local_agent_logs_enabled: false
aw_windows_incident_capture_enabled: true aw_windows_incident_capture_enabled: true
aw_windows_incident_screenshot_enabled: true aw_windows_incident_screenshot_enabled: true
aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts" 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_logon_marker_enabled: true
aw_windows_skip_hardening: false aw_windows_skip_hardening: false
+14 -4
View File
@@ -1149,6 +1149,15 @@
if (!tbody) return; if (!tbody) return;
try { try {
const cases = await caseApi("/api/0/dlp/cases?host=" + encodeURIComponent(host) + "&limit=100", { method: "GET" }); 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) { const rows = (cases || []).map(function (c) {
return ( return (
"<tr>" + "<tr>" +
@@ -1158,15 +1167,16 @@
"<td>" + escapeHtml(String(c.title || "")) + "</td>" + "<td>" + escapeHtml(String(c.title || "")) + "</td>" +
"<td>" + escapeHtml(String(c.assignee || "")) + "</td>" + "<td>" + escapeHtml(String(c.assignee || "")) + "</td>" +
"<td>" + escapeHtml(String(c.incident_id || "")) + "</td>" + "<td>" + escapeHtml(String(c.incident_id || "")) + "</td>" +
"<td>" + renderCaseDfir(c) + "</td>" +
"<td>" + escapeHtml(String(c.updated_at || c.created_at || "")) + "</td>" + "<td>" + escapeHtml(String(c.updated_at || c.created_at || "")) + "</td>" +
"</tr>" "</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]"); const status = center.querySelector("[data-aw-ru-dlp-cases-status]");
if (status) status.textContent = "Кейсов: " + (cases || []).length; if (status) status.textContent = "Кейсов: " + (cases || []).length;
} catch (error) { } 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]"); const status = center.querySelector("[data-aw-ru-dlp-cases-status]");
if (status) status.textContent = "Кейсы недоступны"; if (status) status.textContent = "Кейсы недоступны";
} }
@@ -1388,8 +1398,8 @@
'<div class="aw-ru-dlp-status" data-aw-ru-dlp-cases-status>Кейсов: 0</div>' + '<div class="aw-ru-dlp-status" data-aw-ru-dlp-cases-status>Кейсов: 0</div>' +
'</div>' + '</div>' +
'<table class="aw-ru-dlp-table">' + '<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>' + '<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="7">Загрузка...</td></tr></tbody>' + '<tbody data-aw-ru-dlp-cases><tr><td colspan="8">Загрузка...</td></tr></tbody>' +
'</table>' + '</table>' +
'</div>' + '</div>' +
'<div class="aw-ru-dlp-message" data-aw-ru-dlp-message></div>'; '<div class="aw-ru-dlp-message" data-aw-ru-dlp-message></div>';
+17 -1
View File
@@ -9,6 +9,22 @@ from pydantic import BaseModel, Field
CaseStatus = Literal["open", "investigating", "resolved", "closed"] 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): class CaseCreate(BaseModel):
incident_id: str = Field(min_length=1, max_length=256) incident_id: str = Field(min_length=1, max_length=256)
host: str | None = Field(default=None, max_length=128) host: str | None = Field(default=None, max_length=128)
@@ -51,6 +67,6 @@ class CaseRecord(BaseModel):
source_bucket: str | None source_bucket: str | None
source_event_ts: str | None source_event_ts: str | None
evidence: dict | None evidence: dict | None
forensics: dict | None
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime
@@ -8,7 +8,7 @@ from typing import Any
from fastapi import FastAPI, HTTPException, Query from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware 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 from case_storage import CaseStorage
DB = Path(os.environ.get("AW_DLP_CASE_DB_PATH", "/opt/activitywatch/dlp-case-management/cases.db")) 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") @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]]: 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) return STORE.list_comments(case_id=case_id, limit=limit)
@APP.post("/api/0/dlp/cases/{case_id}/forensics/hayabusa")
def link_hayabusa(case_id: int, payload: CaseHayabusaLink) -> dict[str, Any]:
try:
return STORE.link_hayabusa(case_id=case_id, payload=payload.model_dump(exclude_none=True), actor="api")
except KeyError:
raise HTTPException(status_code=404, detail="case not found")
+67 -9
View File
@@ -43,6 +43,7 @@ class CaseStorage:
source_bucket TEXT, source_bucket TEXT,
source_event_ts TEXT, source_event_ts TEXT,
evidence_json TEXT, evidence_json TEXT,
forensics_json TEXT,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
updated_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() 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 @staticmethod
def _now() -> str: def _now() -> str:
return datetime.now(timezone.utc).isoformat() return datetime.now(timezone.utc).isoformat()
@staticmethod @staticmethod
def _to_case_dict(row: sqlite3.Row) -> dict[str, Any]: def _load_json_field(raw: Any) -> dict[str, Any] | None:
evidence = None if not raw:
if row["evidence_json"]: return None
try: try:
evidence = json.loads(row["evidence_json"]) return json.loads(raw)
except Exception: except Exception:
evidence = None 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 { return {
"id": int(row["id"]), "id": int(row["id"]),
"incident_id": row["incident_id"], "incident_id": row["incident_id"],
@@ -94,6 +110,7 @@ class CaseStorage:
"source_bucket": row["source_bucket"], "source_bucket": row["source_bucket"],
"source_event_ts": row["source_event_ts"], "source_event_ts": row["source_event_ts"],
"evidence": evidence, "evidence": evidence,
"forensics": forensics,
"created_at": row["created_at"], "created_at": row["created_at"],
"updated_at": row["updated_at"], "updated_at": row["updated_at"],
} }
@@ -114,8 +131,8 @@ class CaseStorage:
""" """
INSERT INTO cases ( INSERT INTO cases (
incident_id, host, title, severity, assignee, status, incident_id, host, title, severity, assignee, status,
source_bucket, source_event_ts, evidence_json, created_at, updated_at source_bucket, source_event_ts, evidence_json, forensics_json, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?)
""", """,
( (
payload["incident_id"], payload["incident_id"],
@@ -126,6 +143,7 @@ class CaseStorage:
payload.get("source_bucket"), payload.get("source_bucket"),
payload.get("source_event_ts"), payload.get("source_event_ts"),
json.dumps(normalized_evidence, ensure_ascii=False) if normalized_evidence is not None else None, json.dumps(normalized_evidence, ensure_ascii=False) if normalized_evidence is not None else None,
None,
now, now,
now, now,
), ),
@@ -195,6 +213,46 @@ class CaseStorage:
c.commit() c.commit()
return self.get_case(case_id, c) 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]: def add_comment(self, case_id: int, comment: str, author: str | None = None) -> dict[str, Any]:
now = self._now() now = self._now()
with self.conn() as c: with self.conn() as c:
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from case_storage import CaseStorage
class CaseStorageHayabusaLinkTest(unittest.TestCase):
def test_link_hayabusa_metadata(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "cases.db"
storage = CaseStorage(db_path)
created = storage.create_case(
{
"incident_id": "inc-1",
"host": "SHARKON2025",
"title": "DLP print incident",
"severity": "high",
},
actor="test",
)
linked = storage.link_hayabusa(
case_id=int(created["id"]),
payload={
"host": "SHARKON2025",
"mode": "incident",
"status": "ok",
"intake_id": "pkg-1",
"report_dir": "/opt/hayabusa/reports/SHARKON2025/run-1",
"package_path": "/opt/hayabusa/archive/packages/SHARKON2025/pkg-1.zip",
"sha256": "abc123",
"link_source": "unit-test",
},
actor="test",
)
hayabusa = (linked.get("forensics") or {}).get("hayabusa") or {}
self.assertEqual(hayabusa.get("tool"), "hayabusa")
self.assertEqual(hayabusa.get("host"), "SHARKON2025")
self.assertEqual(hayabusa.get("mode"), "incident")
self.assertEqual(hayabusa.get("status"), "ok")
self.assertEqual(hayabusa.get("intake_id"), "pkg-1")
self.assertEqual(hayabusa.get("link_source"), "unit-test")
audit = storage.list_audit(int(created["id"]))
self.assertTrue(any(row.get("action") == "link_hayabusa" for row in audit))
if __name__ == "__main__":
unittest.main()
+522
View File
@@ -0,0 +1,522 @@
#!/usr/bin/env bash
set -euo pipefail
HAYA_ROOT="${AW_HAYABUSA_ROOT:-/opt/hayabusa}"
HAYA_CURRENT="${HAYA_ROOT}/current"
HAYA_BIN="${HAYA_CURRENT}/hayabusa"
HAYA_RULES="${HAYA_CURRENT}/rules"
HAYA_CONFIG="${HAYA_CURRENT}/config"
HAYA_REPORTS_ROOT="${AW_HAYABUSA_REPORTS_ROOT:-${HAYA_ROOT}/reports}"
HAYA_STATE_ROOT="${AW_HAYABUSA_STATE_ROOT:-${HAYA_ROOT}/state}"
HAYA_INCOMING_DIR="${AW_HAYABUSA_INCOMING_DIR:-${HAYA_ROOT}/inbox/incoming}"
HAYA_STAGING_DIR="${AW_HAYABUSA_STAGING_DIR:-${HAYA_ROOT}/inbox/staging}"
HAYA_ARCHIVE_PACKAGES_DIR="${AW_HAYABUSA_ARCHIVE_PACKAGES_DIR:-${HAYA_ROOT}/archive/packages}"
HAYA_ARCHIVE_EXTRACTED_DIR="${AW_HAYABUSA_ARCHIVE_EXTRACTED_DIR:-${HAYA_ROOT}/archive/extracted}"
HAYA_LOGS_DIR="${AW_HAYABUSA_LOGS_DIR:-${HAYA_ROOT}/state/logs}"
LAST_REPORT_DIR=""
usage() {
cat <<'EOF'
Usage:
aw-hayabusa doctor
aw-hayabusa inventory
aw-hayabusa accept --package <zip> [--host HOST]
aw-hayabusa process-inbox [--mode <quick|incident|full>] [--limit N]
aw-hayabusa profiles
aw-hayabusa version
aw-hayabusa <quick|incident|full> --input <file-or-dir> [--host HOST] [--label LABEL] [--output-root DIR] [--threads N]
Modes:
quick Fast CSV triage with HTML summary and logon summary
incident Rich JSONL timeline for incident review with HTML summary and logon summary
full Broad JSONL timeline with all rule families enabled, HTML summary and logon summary
EOF
}
fail() {
echo "ERROR: $*" >&2
exit 1
}
sanitize() {
printf '%s' "$1" | tr ' /:@' '_' | tr -cd 'A-Za-z0-9._-'
}
ensure_layout() {
[ -x "${HAYA_BIN}" ] || fail "Hayabusa binary not found at ${HAYA_BIN}"
[ -d "${HAYA_RULES}" ] || fail "Hayabusa rules directory not found at ${HAYA_RULES}"
[ -d "${HAYA_CONFIG}" ] || fail "Hayabusa config directory not found at ${HAYA_CONFIG}"
mkdir -p \
"${HAYA_REPORTS_ROOT}" \
"${HAYA_STATE_ROOT}" \
"${HAYA_LOGS_DIR}" \
"${HAYA_ROOT}/inbox" \
"${HAYA_ROOT}/archive" \
"${HAYA_INCOMING_DIR}" \
"${HAYA_STAGING_DIR}" \
"${HAYA_ARCHIVE_PACKAGES_DIR}" \
"${HAYA_ARCHIVE_EXTRACTED_DIR}"
}
run_logged() {
local log_file="$1"
shift
{
printf '[%s] CMD:' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
printf ' %q' "$@"
printf '\n'
} | tee -a "${log_file}"
"$@" 2>&1 | tee -a "${log_file}"
return "${PIPESTATUS[0]}"
}
write_manifest() {
local manifest_path="$1"
local mode="$2"
local host="$3"
local input_path="$4"
local report_dir="$5"
local status="$6"
local output_format="$7"
cat >"${manifest_path}" <<EOF
{
"mode": "${mode}",
"host": "${host}",
"input": "${input_path}",
"report_dir": "${report_dir}",
"status": "${status}",
"output_format": "${output_format}",
"generated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
}
json_field() {
local json_path="$1"
local field_name="$2"
python3 - "$json_path" "$field_name" <<'PY'
import json, sys
path, field = sys.argv[1], sys.argv[2]
try:
with open(path, 'r', encoding='utf-8') as fh:
obj = json.load(fh)
except Exception:
sys.exit(0)
value = obj.get(field)
if value is None:
sys.exit(0)
print(str(value))
PY
}
detect_host_from_manifest() {
local manifest_path="$1"
local host=""
host="$(json_field "${manifest_path}" host || true)"
if [ -z "${host}" ]; then
host="$(json_field "${manifest_path}" hostname || true)"
fi
printf '%s' "${host}"
}
write_package_manifest() {
local manifest_path="$1"
local package_path="$2"
local host="$3"
local intake_id="$4"
local sha256="$5"
local status="$6"
local stage_dir="$7"
local report_dir="$8"
cat >"${manifest_path}" <<EOF
{
"package_path": "${package_path}",
"host": "${host}",
"intake_id": "${intake_id}",
"sha256": "${sha256}",
"status": "${status}",
"stage_dir": "${stage_dir}",
"report_dir": "${report_dir}",
"processed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
}
write_state_json() {
local state_path="$1"
local body="$2"
printf '%s\n' "${body}" > "${state_path}"
}
run_mode() {
local mode="$1"
shift
local input_path=""
local host=""
local label=""
local output_root="${HAYA_REPORTS_ROOT}"
local threads=""
while [ "$#" -gt 0 ]; do
case "$1" in
--input|-i)
[ "$#" -ge 2 ] || fail "--input requires a value"
input_path="$2"
shift 2
;;
--host)
[ "$#" -ge 2 ] || fail "--host requires a value"
host="$2"
shift 2
;;
--label)
[ "$#" -ge 2 ] || fail "--label requires a value"
label="$2"
shift 2
;;
--output-root)
[ "$#" -ge 2 ] || fail "--output-root requires a value"
output_root="$2"
shift 2
;;
--threads)
[ "$#" -ge 2 ] || fail "--threads requires a value"
threads="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
fail "Unknown argument: $1"
;;
esac
done
[ -n "${input_path}" ] || fail "--input is required"
[ -e "${input_path}" ] || fail "Input path does not exist: ${input_path}"
ensure_layout
mkdir -p "${output_root}"
if [ -z "${host}" ]; then
host="$(basename "${input_path}")"
if [ "${host}" = "." ] || [ "${host}" = "/" ]; then
host="unknown"
fi
fi
host="$(sanitize "${host}")"
[ -n "${host}" ] || host="unknown"
local label_suffix=""
if [ -n "${label}" ]; then
label_suffix="_$(sanitize "${label}")"
fi
local run_ts
run_ts="$(date -u +%Y%m%dT%H%M%SZ)"
local report_dir="${output_root}/${host}/${run_ts}_${mode}${label_suffix}"
local log_file="${report_dir}/run.log"
local manifest_file="${report_dir}/manifest.json"
local html_file="${report_dir}/summary.html"
local timeline_file=""
local output_format=""
local -a input_args=()
local -a common_args=("-w" "-q" "-C" "-r" "${HAYA_RULES}" "-c" "${HAYA_CONFIG}" "-O")
local -a mode_args=()
local -a command=()
local -a logon_command=()
mkdir -p "${report_dir}"
if [ -d "${input_path}" ]; then
input_args=("-d" "${input_path}")
else
input_args=("-f" "${input_path}")
fi
if [ -n "${threads}" ]; then
common_args+=("-t" "${threads}")
fi
case "${mode}" in
quick)
timeline_file="${report_dir}/timeline.csv"
output_format="csv"
mode_args=("-E" "-P" "-m" "medium" "-o" "${timeline_file}" "-H" "${html_file}")
command=("${HAYA_BIN}" "csv-timeline" "${input_args[@]}" "${common_args[@]}" "${mode_args[@]}")
;;
incident)
timeline_file="${report_dir}/timeline.jsonl"
output_format="jsonl"
mode_args=("-L" "-m" "low" "-o" "${timeline_file}" "-H" "${html_file}")
command=("${HAYA_BIN}" "json-timeline" "${input_args[@]}" "${common_args[@]}" "${mode_args[@]}")
;;
full)
timeline_file="${report_dir}/timeline.jsonl"
output_format="jsonl"
mode_args=("-L" "-A" "-D" "-n" "-u" "-m" "informational" "-o" "${timeline_file}" "-H" "${html_file}")
command=("${HAYA_BIN}" "json-timeline" "${input_args[@]}" "${common_args[@]}" "${mode_args[@]}")
;;
*)
fail "Unsupported mode: ${mode}"
;;
esac
logon_command=("${HAYA_BIN}" "logon-summary" "${input_args[@]}" "-q" "-C" "-c" "${HAYA_CONFIG}" "-O" "-o" "${report_dir}/logon-summary")
{
echo "mode=${mode}"
echo "host=${host}"
echo "input=${input_path}"
echo "report_dir=${report_dir}"
echo "output_format=${output_format}"
} | tee -a "${log_file}" >/dev/null
local status="ok"
if ! run_logged "${log_file}" "${command[@]}"; then
status="failed"
fi
if ! run_logged "${log_file}" "${logon_command[@]}"; then
status="failed"
fi
write_manifest "${manifest_file}" "${mode}" "${host}" "${input_path}" "${report_dir}" "${status}" "${output_format}"
ln -sfn "${report_dir}" "${HAYA_STATE_ROOT}/latest-run"
ln -sfn "${report_dir}" "${HAYA_STATE_ROOT}/latest-${host}"
LAST_REPORT_DIR="${report_dir}"
echo "Report directory: ${report_dir}"
if [ "${status}" != "ok" ]; then
echo "ERROR: Hayabusa run failed; see ${log_file}" >&2
return 1
fi
}
inventory() {
ensure_layout
local incoming_count staged_count archived_pkg_count archived_extract_count
incoming_count=$(find "${HAYA_INCOMING_DIR}" -maxdepth 1 -type f -name '*.zip' | wc -l)
staged_count=$(find "${HAYA_STAGING_DIR}" -mindepth 1 -maxdepth 1 -type d | wc -l)
archived_pkg_count=$(find "${HAYA_ARCHIVE_PACKAGES_DIR}" -type f -name '*.zip' | wc -l)
archived_extract_count=$(find "${HAYA_ARCHIVE_EXTRACTED_DIR}" -mindepth 2 -maxdepth 2 -type d | wc -l)
echo "aw-hayabusa inventory"
echo "incoming_zip=${incoming_count}"
echo "staged_dirs=${staged_count}"
echo "archived_packages=${archived_pkg_count}"
echo "archived_payloads=${archived_extract_count}"
if [ -L "${HAYA_STATE_ROOT}/latest-run" ]; then
echo "latest_run=$(readlink -f "${HAYA_STATE_ROOT}/latest-run")"
fi
}
accept_package() {
local package_path=""
local host=""
while [ "$#" -gt 0 ]; do
case "$1" in
--package)
[ "$#" -ge 2 ] || fail "--package requires a value"
package_path="$2"
shift 2
;;
--host)
[ "$#" -ge 2 ] || fail "--host requires a value"
host="$2"
shift 2
;;
*)
fail "Unknown argument: $1"
;;
esac
done
[ -n "${package_path}" ] || fail "--package is required"
[ -f "${package_path}" ] || fail "Package not found: ${package_path}"
ensure_layout
local ts base_name safe_base dest_path sha256
ts="$(date -u +%Y%m%dT%H%M%SZ)"
base_name="$(basename "${package_path}")"
safe_base="$(sanitize "${base_name}")"
[ -n "${safe_base}" ] || safe_base="incoming.zip"
dest_path="${HAYA_INCOMING_DIR}/${ts}_${safe_base}"
cp -f "${package_path}" "${dest_path}"
sha256="$(sha256sum "${dest_path}" | awk '{print $1}')"
printf '%s %s\n' "${sha256}" "$(basename "${dest_path}")" > "${dest_path}.sha256"
if [ -n "${host}" ]; then
write_state_json "${dest_path}.host" "${host}"
fi
echo "Accepted package: ${dest_path}"
}
find_manifest_path() {
local stage_dir="$1"
find "${stage_dir}" -type f -name 'manifest.json' | head -n 1
}
find_evtx_root() {
local stage_dir="$1"
if [ -d "${stage_dir}/evtx" ]; then
printf '%s' "${stage_dir}/evtx"
return 0
fi
find "${stage_dir}" -type d -name evtx | head -n 1
}
process_one_package() {
local package_path="$1"
local mode="$2"
local forced_host="${3:-}"
ensure_layout
local package_name package_base intake_id stage_dir package_sha256
package_name="$(basename "${package_path}")"
package_base="${package_name%.zip}"
intake_id="$(sanitize "${package_base}")"
stage_dir="${HAYA_STAGING_DIR}/${intake_id}"
mkdir -p "${stage_dir}"
package_sha256="$(sha256sum "${package_path}" | awk '{print $1}')"
unzip -q -o "${package_path}" -d "${stage_dir}"
local manifest_path host evtx_root archive_pkg_dir archive_pkg_path archive_extract_dir status report_dir
manifest_path="$(find_manifest_path "${stage_dir}")"
host="${forced_host}"
if [ -z "${host}" ] && [ -f "${package_path}.host" ]; then
host="$(cat "${package_path}.host" 2>/dev/null || true)"
fi
if [ -z "${host}" ] && [ -n "${manifest_path}" ]; then
host="$(detect_host_from_manifest "${manifest_path}")"
fi
if [ -z "${host}" ]; then
host="${package_base%%-*}"
fi
host="$(sanitize "${host}")"
[ -n "${host}" ] || host="unknown"
archive_pkg_dir="${HAYA_ARCHIVE_PACKAGES_DIR}/${host}"
archive_extract_dir="${HAYA_ARCHIVE_EXTRACTED_DIR}/${host}/${intake_id}"
mkdir -p "${archive_pkg_dir}" "${archive_extract_dir}"
evtx_root="$(find_evtx_root "${stage_dir}")"
status="ok"
report_dir=""
if [ -z "${evtx_root}" ] || ! find "${evtx_root}" -type f \( -iname '*.evtx' -o -iname '*.json' -o -iname '*.jsonl' \) | grep -q .; then
status="failed-no-evtx"
else
if run_mode "${mode}" --input "${evtx_root}" --host "${host}" --label "${package_base}"; then
report_dir="${LAST_REPORT_DIR}"
status="ok"
else
report_dir="${LAST_REPORT_DIR}"
status="failed-analysis"
fi
fi
mv "${package_path}" "${archive_pkg_dir}/${intake_id}.zip"
[ -f "${package_path}.sha256" ] && mv "${package_path}.sha256" "${archive_pkg_dir}/${intake_id}.zip.sha256"
[ -f "${package_path}.host" ] && mv "${package_path}.host" "${archive_pkg_dir}/${intake_id}.host"
mv "${stage_dir}" "${archive_extract_dir}/payload"
write_package_manifest "${archive_extract_dir}/intake.json" "${archive_pkg_dir}/${intake_id}.zip" "${host}" "${intake_id}" "${package_sha256}" "${status}" "${archive_extract_dir}/payload" "${report_dir}"
write_state_json "${HAYA_STATE_ROOT}/latest-intake.json" "$(cat "${archive_extract_dir}/intake.json")"
echo "Processed package: ${archive_pkg_dir}/${intake_id}.zip"
echo "Archive payload: ${archive_extract_dir}/payload"
if [ -n "${report_dir}" ]; then
echo "Report directory: ${report_dir}"
fi
[ "${status}" = "ok" ] || fail "Package workflow ended with status=${status}; archived for inspection"
}
process_inbox() {
local mode="incident"
local limit="0"
while [ "$#" -gt 0 ]; do
case "$1" in
--mode)
[ "$#" -ge 2 ] || fail "--mode requires a value"
mode="$2"
shift 2
;;
--limit)
[ "$#" -ge 2 ] || fail "--limit requires a value"
limit="$2"
shift 2
;;
*)
fail "Unknown argument: $1"
;;
esac
done
case "${mode}" in
quick|incident|full) ;;
*) fail "Unsupported mode for process-inbox: ${mode}" ;;
esac
ensure_layout
local count=0 pkg
while IFS= read -r pkg; do
process_one_package "${pkg}" "${mode}"
count=$((count + 1))
if [ "${limit}" -gt 0 ] && [ "${count}" -ge "${limit}" ]; then
break
fi
done < <(find "${HAYA_INCOMING_DIR}" -maxdepth 1 -type f -name '*.zip' | sort)
[ "${count}" -gt 0 ] || echo "No packages in ${HAYA_INCOMING_DIR}"
}
main() {
local subcommand="${1:-}"
case "${subcommand}" in
doctor)
ensure_layout
echo "aw-hayabusa doctor: OK"
echo "root=${HAYA_ROOT}"
echo "current=${HAYA_CURRENT}"
echo "binary=${HAYA_BIN}"
echo "rules=${HAYA_RULES}"
echo "config=${HAYA_CONFIG}"
echo "reports=${HAYA_REPORTS_ROOT}"
echo "state=${HAYA_STATE_ROOT}"
echo "incoming=${HAYA_INCOMING_DIR}"
echo "staging=${HAYA_STAGING_DIR}"
echo "archive_packages=${HAYA_ARCHIVE_PACKAGES_DIR}"
echo "archive_extracted=${HAYA_ARCHIVE_EXTRACTED_DIR}"
echo "logs=${HAYA_LOGS_DIR}"
;;
inventory)
inventory
;;
accept)
shift
accept_package "$@"
;;
process-inbox)
shift
process_inbox "$@"
;;
profiles)
ensure_layout
cd "${HAYA_CURRENT}"
exec "${HAYA_BIN}" list-profiles
;;
version)
ensure_layout
cd "${HAYA_CURRENT}"
exec "${HAYA_BIN}" help
;;
quick|incident|full)
shift
cd "${HAYA_CURRENT}"
run_mode "${subcommand}" "$@"
;;
""|-h|--help|help)
usage
;;
*)
fail "Unknown subcommand: ${subcommand}"
;;
esac
}
main "$@"
@@ -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`
+124
View File
@@ -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.
+2
View File
@@ -11,6 +11,8 @@
- [ИБ-профиль DLP](../dlp-security-functional-spec-ru.md) - подробное описание реализованного DLP/monitoring-контура для службы ИБ - [ИБ-профиль 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: 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 - [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 - [DLP Endpoint Monitoring](DLP-Endpoint-Monitoring) - мониторинг clipboard, печати, USB
+78
View File
@@ -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
View File
@@ -319,6 +319,7 @@ class TSJGuardianBot:
BTN_PFSENSE_CONFIRM = "Подтвердить pfSense: шаг 1" BTN_PFSENSE_CONFIRM = "Подтвердить pfSense: шаг 1"
BTN_PFSENSE_CANCEL = "Отменить pfSense изменение" BTN_PFSENSE_CANCEL = "Отменить pfSense изменение"
BTN_AW_DLP_CHECK = "Проверка AW-Rus + DLP" BTN_AW_DLP_CHECK = "Проверка AW-Rus + DLP"
BTN_AW_DFIR = "Hayabusa DFIR"
BTN_AI_CHAT_ALIASES = ("AI чат", "Чат с поддержкой", "Техподдержка", "Тех поддержка") BTN_AI_CHAT_ALIASES = ("AI чат", "Чат с поддержкой", "Техподдержка", "Тех поддержка")
BTN_OVPN_CERTS_ALIASES = ("OpenVPN certs", "OpenVPN cert", "OpenVPN серты", "OpenVPN сертификат") BTN_OVPN_CERTS_ALIASES = ("OpenVPN certs", "OpenVPN cert", "OpenVPN серты", "OpenVPN сертификат")
PFSENSE_ENV_PATH = "/home/codex/infra-admin/vendor/pfsense-mcp-server/.env.readonly" PFSENSE_ENV_PATH = "/home/codex/infra-admin/vendor/pfsense-mcp-server/.env.readonly"
@@ -361,6 +362,12 @@ class TSJGuardianBot:
"AW_RUS_DLP_HEAL_CMD", "AW_RUS_DLP_HEAL_CMD",
"", "",
).strip() ).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_host = os.getenv("AW_RUS_HOST", "SHARKON2025").strip()
self.aw_rus_primary_user = os.getenv("AW_RUS_PRIMARY_USER", "USER1").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)) self.aw_rus_stale_sec = max(60, env_int("AW_RUS_STALE_SEC", 900))
@@ -491,6 +498,7 @@ class TSJGuardianBot:
"keyboard": [ "keyboard": [
[self.BTN_STATUS, self.BTN_CHECK, self.BTN_HEAL], [self.BTN_STATUS, self.BTN_CHECK, self.BTN_HEAL],
[self.BTN_AW_DLP_CHECK], [self.BTN_AW_DLP_CHECK],
[self.BTN_AW_DFIR],
[self.BTN_ACK, self.BTN_RESOLVE], [self.BTN_ACK, self.BTN_RESOLVE],
[self.BTN_AI, self.BTN_FALLBACK], [self.BTN_AI, self.BTN_FALLBACK],
[self.BTN_AI_CHAT], [self.BTN_AI_CHAT],
@@ -1988,6 +1996,11 @@ class TSJGuardianBot:
"- Проверяет AW-Rus и DLP по свежести bucket-данных и сегодняшнему worktime.\n" "- Проверяет AW-Rus и DLP по свежести bucket-данных и сегодняшнему worktime.\n"
"- Формирует операторский итог OK/DEGRADED прямо в чате.\n" "- Формирует операторский итог OK/DEGRADED прямо в чате.\n"
"\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" f"{self.BTN_HEAL}\n"
"- Пробует автоматическое лечение проблем (рестарт нужных сервисов).\n" "- Пробует автоматическое лечение проблем (рестарт нужных сервисов).\n"
"- Для критичного заполнения ФС авто-очистка не выполняется, нужен разбор причины.\n" "- Для критичного заполнения ФС авто-очистка не выполняется, нужен разбор причины.\n"
@@ -2088,7 +2101,7 @@ class TSJGuardianBot:
"1) Нажмите кнопку создания/восстановления или используйте `/proxmox_snapshot TARGET`.\n" "1) Нажмите кнопку создания/восстановления или используйте `/proxmox_snapshot TARGET`.\n"
"2) Для восстановления после выбора узла отправьте `/proxmox_restore_apply CODE`.\n" "2) Для восстановления после выбора узла отправьте `/proxmox_restore_apply CODE`.\n"
"\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: def _cmd_status(self) -> str:
@@ -2205,12 +2218,59 @@ class TSJGuardianBot:
except Exception as exc: except Exception as exc:
return None, f"error:{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 = [ checks = [
(f"aw-watcher-window_{host}", "watcher-window"), (f"aw-watcher-window_{host}", "watcher-window"),
(f"aw-watcher-afk_{host}", "watcher-afk"), (f"aw-watcher-afk_{host}", "watcher-afk"),
(f"aw-dlp-endpoint-signals_{host}", "dlp-endpoint"), (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:"] lines = ["Проверка AW-Rus + DLP:"]
@@ -2227,6 +2287,46 @@ class TSJGuardianBot:
else: else:
lines.append(f"- {label}: OK age={age}s end={tail}") 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: try:
r = requests.get(f"{worktime_base}/reports/worktime/today?format=csv", timeout=20) r = requests.get(f"{worktime_base}/reports/worktime/today?format=csv", timeout=20)
r.raise_for_status() r.raise_for_status()
@@ -2411,6 +2511,111 @@ class TSJGuardianBot:
out.extend(after_lines) out.extend(after_lines)
return "\n".join(out) 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: def _pfsense_security_status_lines(self) -> str:
cmd = "/usr/bin/python3 /home/codex/infra-admin/scripts/pfsense_security_status.py" cmd = "/usr/bin/python3 /home/codex/infra-admin/scripts/pfsense_security_status.py"
try: try:
@@ -2519,6 +2724,29 @@ class TSJGuardianBot:
if text.startswith("/aw_dlp_check") or text == self.BTN_AW_DLP_CHECK: 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")) self._send_text(chat_id, self._run_operator_action("aw-dlp-check"))
return 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: if text.startswith("/heal") or text == self.BTN_HEAL:
self._send_text(chat_id, self._run_operator_action("heal")) self._send_text(chat_id, self._run_operator_action("heal"))
return return
+29
View File
@@ -399,6 +399,7 @@ function Copy-ActivityWatchCollectorAssets {
[string]$FileCollectorScriptSource, [string]$FileCollectorScriptSource,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$SessionCollectorScriptSource, [string]$SessionCollectorScriptSource,
[string]$EvtxExportScriptSource,
[string]$EmailCollectorScriptSource, [string]$EmailCollectorScriptSource,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$ExampleRulesSource, [string]$ExampleRulesSource,
@@ -417,6 +418,7 @@ function Copy-ActivityWatchCollectorAssets {
$policyClientTarget = Join-Path $StateRoot 'dlp-policy-client.ps1' $policyClientTarget = Join-Path $StateRoot 'dlp-policy-client.ps1'
$fileCollectorTarget = Join-Path $StateRoot 'file-operations-collector.ps1' $fileCollectorTarget = Join-Path $StateRoot 'file-operations-collector.ps1'
$sessionCollectorTarget = Join-Path $StateRoot 'worktime-session-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' $emailCollectorTarget = Join-Path $StateRoot 'email-outbound-collector.ps1'
$exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json' $exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json'
$rulesTarget = Join-Path $StateRoot 'web-category-rules.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 $FileCollectorScriptSource -Destination $fileCollectorTarget -Force
Copy-Item -LiteralPath $SessionCollectorScriptSource -Destination $sessionCollectorTarget -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)) { if ($EmailCollectorScriptSource -and (Test-Path -LiteralPath $EmailCollectorScriptSource)) {
Copy-Item -LiteralPath $EmailCollectorScriptSource -Destination $emailCollectorTarget -Force Copy-Item -LiteralPath $EmailCollectorScriptSource -Destination $emailCollectorTarget -Force
} }
@@ -458,6 +463,7 @@ function Copy-ActivityWatchCollectorAssets {
PolicyClientScript = $policyClientTarget PolicyClientScript = $policyClientTarget
FileCollectorScript = $fileCollectorTarget FileCollectorScript = $fileCollectorTarget
SessionCollectorScript = $sessionCollectorTarget SessionCollectorScript = $sessionCollectorTarget
EvtxExportScript = $evtxExportTarget
EmailCollectorScript = $emailCollectorTarget EmailCollectorScript = $emailCollectorTarget
ExampleRules = $exampleRulesTarget ExampleRules = $exampleRulesTarget
ActiveRules = $rulesTarget ActiveRules = $rulesTarget
@@ -489,6 +495,7 @@ function New-ActivityWatchDeploymentConfig {
[string]$FileCollectorScript, [string]$FileCollectorScript,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$SessionCollectorScript, [string]$SessionCollectorScript,
[string]$EvtxExportScript,
[string]$EmailCollectorScript, [string]$EmailCollectorScript,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$RulesPath, [string]$RulesPath,
@@ -507,6 +514,9 @@ function New-ActivityWatchDeploymentConfig {
[bool]$IncidentCaptureEnabled = $true, [bool]$IncidentCaptureEnabled = $true,
[bool]$IncidentScreenshotEnabled = $true, [bool]$IncidentScreenshotEnabled = $true,
[string]$IncidentArtifactsRoot, [string]$IncidentArtifactsRoot,
[string]$EvtxExportRoot,
[int]$EvtxRetentionDays = 14,
[string[]]$EvtxChannels = @(),
[bool]$LogonMarkerEnabled = $true, [bool]$LogonMarkerEnabled = $true,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$LaunchScriptPath, [string]$LaunchScriptPath,
@@ -529,6 +539,19 @@ function New-ActivityWatchDeploymentConfig {
) )
$effectiveIncidentArtifactsRoot = if ($IncidentArtifactsRoot) { $IncidentArtifactsRoot } else { Join-Path $StateRoot 'incident-artifacts' } $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 } $effectivePolicyEngineHost = if ([string]::IsNullOrWhiteSpace($PolicyEngineHost)) { $ServerHost } else { $PolicyEngineHost }
$effectivePolicyCachePath = if ([string]::IsNullOrWhiteSpace($PolicyCachePath)) { Join-Path $StateRoot 'dlp-policy-cache.json' } else { $PolicyCachePath } $effectivePolicyCachePath = if ([string]::IsNullOrWhiteSpace($PolicyCachePath)) { Join-Path $StateRoot 'dlp-policy-cache.json' } else { $PolicyCachePath }
@@ -551,6 +574,7 @@ function New-ActivityWatchDeploymentConfig {
emailCollectorScript = $EmailCollectorScript emailCollectorScript = $EmailCollectorScript
fileCollectorScript = $FileCollectorScript fileCollectorScript = $FileCollectorScript
sessionCollectorScript = $SessionCollectorScript sessionCollectorScript = $SessionCollectorScript
evtxExportScript = $EvtxExportScript
rulesPath = $RulesPath rulesPath = $RulesPath
policyPath = $PolicyPath policyPath = $PolicyPath
launchScript = $LaunchScriptPath launchScript = $LaunchScriptPath
@@ -574,6 +598,11 @@ function New-ActivityWatchDeploymentConfig {
screenshotEnabled = $IncidentScreenshotEnabled screenshotEnabled = $IncidentScreenshotEnabled
artifactsRoot = $effectiveIncidentArtifactsRoot artifactsRoot = $effectiveIncidentArtifactsRoot
} }
forensics = [pscustomobject]@{
evtxExportRoot = $effectiveEvtxExportRoot
retentionDays = $EvtxRetentionDays
evtxChannels = @($effectiveEvtxChannels)
}
sessionEvents = [pscustomobject]@{ sessionEvents = [pscustomobject]@{
logonEnabled = $LogonMarkerEnabled logonEnabled = $LogonMarkerEnabled
bucketPrefix = 'aw-session-events' bucketPrefix = 'aw-session-events'
+9
View File
@@ -23,6 +23,9 @@ param(
[bool]$IncidentCaptureEnabled = $true, [bool]$IncidentCaptureEnabled = $true,
[bool]$IncidentScreenshotEnabled = $true, [bool]$IncidentScreenshotEnabled = $true,
[string]$IncidentArtifactsRoot, [string]$IncidentArtifactsRoot,
[string]$EvtxExportRoot,
[int]$EvtxRetentionDays = 14,
[string[]]$EvtxChannels = @(),
[bool]$LogonMarkerEnabled = $true, [bool]$LogonMarkerEnabled = $true,
[string]$AwHostname, [string]$AwHostname,
[string]$CustomRulesPath, [string]$CustomRulesPath,
@@ -60,6 +63,7 @@ $policyClientSource = Join-Path $PSScriptRoot 'dlp-policy-client.ps1'
$emailCollectorSource = Join-Path $PSScriptRoot 'email-outbound-collector.ps1' $emailCollectorSource = Join-Path $PSScriptRoot 'email-outbound-collector.ps1'
$fileCollectorSource = Join-Path $PSScriptRoot 'file-operations-collector.ps1' $fileCollectorSource = Join-Path $PSScriptRoot 'file-operations-collector.ps1'
$sessionCollectorSource = Join-Path $PSScriptRoot 'worktime-session-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' $exampleRulesSource = Join-Path $PSScriptRoot 'web-category-rules.example.json'
$examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json' $examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json'
@@ -78,6 +82,7 @@ $assetResult = Copy-ActivityWatchCollectorAssets `
-EmailCollectorScriptSource $emailCollectorSource ` -EmailCollectorScriptSource $emailCollectorSource `
-FileCollectorScriptSource $fileCollectorSource ` -FileCollectorScriptSource $fileCollectorSource `
-SessionCollectorScriptSource $sessionCollectorSource ` -SessionCollectorScriptSource $sessionCollectorSource `
-EvtxExportScriptSource $evtxExportScriptSource `
-ExampleRulesSource $exampleRulesSource ` -ExampleRulesSource $exampleRulesSource `
-ExamplePolicySource $examplePolicySource ` -ExamplePolicySource $examplePolicySource `
-StateRoot $StateRoot ` -StateRoot $StateRoot `
@@ -101,6 +106,7 @@ $config = New-ActivityWatchDeploymentConfig `
-EmailCollectorScript $assetResult.EmailCollectorScript ` -EmailCollectorScript $assetResult.EmailCollectorScript `
-FileCollectorScript $assetResult.FileCollectorScript ` -FileCollectorScript $assetResult.FileCollectorScript `
-SessionCollectorScript $assetResult.SessionCollectorScript ` -SessionCollectorScript $assetResult.SessionCollectorScript `
-EvtxExportScript $assetResult.EvtxExportScript `
-RulesPath $assetResult.ActiveRules ` -RulesPath $assetResult.ActiveRules `
-PolicyPath $assetResult.ActivePolicy ` -PolicyPath $assetResult.ActivePolicy `
-PollSeconds $PollSeconds ` -PollSeconds $PollSeconds `
@@ -113,6 +119,9 @@ $config = New-ActivityWatchDeploymentConfig `
-IncidentCaptureEnabled $IncidentCaptureEnabled ` -IncidentCaptureEnabled $IncidentCaptureEnabled `
-IncidentScreenshotEnabled $IncidentScreenshotEnabled ` -IncidentScreenshotEnabled $IncidentScreenshotEnabled `
-IncidentArtifactsRoot $IncidentArtifactsRoot ` -IncidentArtifactsRoot $IncidentArtifactsRoot `
-EvtxExportRoot $EvtxExportRoot `
-EvtxRetentionDays $EvtxRetentionDays `
-EvtxChannels $EvtxChannels `
-LogonMarkerEnabled $LogonMarkerEnabled ` -LogonMarkerEnabled $LogonMarkerEnabled `
-AwHostname $AwHostname ` -AwHostname $AwHostname `
-PolicyMode $PolicyMode ` -PolicyMode $PolicyMode `
+9
View File
@@ -23,6 +23,9 @@ param(
[bool]$IncidentCaptureEnabled = $true, [bool]$IncidentCaptureEnabled = $true,
[bool]$IncidentScreenshotEnabled = $true, [bool]$IncidentScreenshotEnabled = $true,
[string]$IncidentArtifactsRoot, [string]$IncidentArtifactsRoot,
[string]$EvtxExportRoot,
[int]$EvtxRetentionDays = 14,
[string[]]$EvtxChannels = @(),
[bool]$LogonMarkerEnabled = $true, [bool]$LogonMarkerEnabled = $true,
[string]$AwHostname, [string]$AwHostname,
[string]$CustomRulesPath, [string]$CustomRulesPath,
@@ -81,6 +84,9 @@ if (-not (Test-Path -LiteralPath $deployScript)) {
-IncidentCaptureEnabled $IncidentCaptureEnabled ` -IncidentCaptureEnabled $IncidentCaptureEnabled `
-IncidentScreenshotEnabled $IncidentScreenshotEnabled ` -IncidentScreenshotEnabled $IncidentScreenshotEnabled `
-IncidentArtifactsRoot $IncidentArtifactsRoot ` -IncidentArtifactsRoot $IncidentArtifactsRoot `
-EvtxExportRoot $EvtxExportRoot `
-EvtxRetentionDays $EvtxRetentionDays `
-EvtxChannels $EvtxChannels `
-LogonMarkerEnabled $LogonMarkerEnabled ` -LogonMarkerEnabled $LogonMarkerEnabled `
-AwHostname $AwHostname ` -AwHostname $AwHostname `
-CustomRulesPath $CustomRulesPath ` -CustomRulesPath $CustomRulesPath `
@@ -113,6 +119,9 @@ if (-not $SkipHardening) {
-IncidentCaptureEnabled $IncidentCaptureEnabled ` -IncidentCaptureEnabled $IncidentCaptureEnabled `
-IncidentScreenshotEnabled $IncidentScreenshotEnabled ` -IncidentScreenshotEnabled $IncidentScreenshotEnabled `
-IncidentArtifactsRoot $IncidentArtifactsRoot ` -IncidentArtifactsRoot $IncidentArtifactsRoot `
-EvtxExportRoot $EvtxExportRoot `
-EvtxRetentionDays $EvtxRetentionDays `
-EvtxChannels $EvtxChannels `
-LogonMarkerEnabled $LogonMarkerEnabled ` -LogonMarkerEnabled $LogonMarkerEnabled `
-AwHostname $AwHostname ` -AwHostname $AwHostname `
-CustomRulesPath $CustomRulesPath ` -CustomRulesPath $CustomRulesPath `
+9
View File
@@ -21,6 +21,9 @@ param(
[bool]$IncidentCaptureEnabled = $true, [bool]$IncidentCaptureEnabled = $true,
[bool]$IncidentScreenshotEnabled = $true, [bool]$IncidentScreenshotEnabled = $true,
[string]$IncidentArtifactsRoot, [string]$IncidentArtifactsRoot,
[string]$EvtxExportRoot,
[int]$EvtxRetentionDays = 14,
[string[]]$EvtxChannels = @(),
[bool]$LogonMarkerEnabled = $true, [bool]$LogonMarkerEnabled = $true,
[string]$AwHostname, [string]$AwHostname,
[string]$CustomRulesPath, [string]$CustomRulesPath,
@@ -45,6 +48,7 @@ $collectorSource = Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1
$endpointCollectorSource = Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1' $endpointCollectorSource = Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1'
$emailCollectorSource = Join-Path $PSScriptRoot 'email-outbound-collector.ps1' $emailCollectorSource = Join-Path $PSScriptRoot 'email-outbound-collector.ps1'
$sessionCollectorSource = Join-Path $PSScriptRoot 'worktime-session-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' $exampleRulesSource = Join-Path $PSScriptRoot 'web-category-rules.example.json'
$examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json' $examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json'
@@ -60,6 +64,7 @@ $assetResult = Copy-ActivityWatchCollectorAssets `
-EndpointCollectorScriptSource $endpointCollectorSource ` -EndpointCollectorScriptSource $endpointCollectorSource `
-EmailCollectorScriptSource $emailCollectorSource ` -EmailCollectorScriptSource $emailCollectorSource `
-SessionCollectorScriptSource $sessionCollectorSource ` -SessionCollectorScriptSource $sessionCollectorSource `
-EvtxExportScriptSource $evtxExportScriptSource `
-ExampleRulesSource $exampleRulesSource ` -ExampleRulesSource $exampleRulesSource `
-ExamplePolicySource $examplePolicySource ` -ExamplePolicySource $examplePolicySource `
-StateRoot $StateRoot ` -StateRoot $StateRoot `
@@ -81,6 +86,7 @@ $config = New-ActivityWatchDeploymentConfig `
-EndpointCollectorScript $assetResult.EndpointCollectorScript ` -EndpointCollectorScript $assetResult.EndpointCollectorScript `
-EmailCollectorScript $assetResult.EmailCollectorScript ` -EmailCollectorScript $assetResult.EmailCollectorScript `
-SessionCollectorScript $assetResult.SessionCollectorScript ` -SessionCollectorScript $assetResult.SessionCollectorScript `
-EvtxExportScript $assetResult.EvtxExportScript `
-RulesPath $assetResult.ActiveRules ` -RulesPath $assetResult.ActiveRules `
-PolicyPath $assetResult.ActivePolicy ` -PolicyPath $assetResult.ActivePolicy `
-PollSeconds $PollSeconds ` -PollSeconds $PollSeconds `
@@ -92,6 +98,9 @@ $config = New-ActivityWatchDeploymentConfig `
-IncidentCaptureEnabled $IncidentCaptureEnabled ` -IncidentCaptureEnabled $IncidentCaptureEnabled `
-IncidentScreenshotEnabled $IncidentScreenshotEnabled ` -IncidentScreenshotEnabled $IncidentScreenshotEnabled `
-IncidentArtifactsRoot $IncidentArtifactsRoot ` -IncidentArtifactsRoot $IncidentArtifactsRoot `
-EvtxExportRoot $EvtxExportRoot `
-EvtxRetentionDays $EvtxRetentionDays `
-EvtxChannels $EvtxChannels `
-LogonMarkerEnabled $LogonMarkerEnabled ` -LogonMarkerEnabled $LogonMarkerEnabled `
-AwHostname $AwHostname ` -AwHostname $AwHostname `
-LaunchScriptPath $launchScriptPath ` -LaunchScriptPath $launchScriptPath `
+141
View File
@@ -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
+10
View File
@@ -20,6 +20,7 @@ param(
[bool]$IncidentCaptureEnabled, [bool]$IncidentCaptureEnabled,
[bool]$IncidentScreenshotEnabled, [bool]$IncidentScreenshotEnabled,
[string]$IncidentArtifactsRoot, [string]$IncidentArtifactsRoot,
[string]$EvtxExportRoot,
[bool]$LogonMarkerEnabled, [bool]$LogonMarkerEnabled,
[string]$AwHostname, [string]$AwHostname,
[string]$CustomRulesPath, [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' } $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' } $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' } $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' $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' } $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' } $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 } $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 } $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' } $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 } $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 } $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' } $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') ` -EmailCollectorScriptSource (Join-Path $PSScriptRoot 'email-outbound-collector.ps1') `
-FileCollectorScriptSource (Join-Path $PSScriptRoot 'file-operations-collector.ps1') ` -FileCollectorScriptSource (Join-Path $PSScriptRoot 'file-operations-collector.ps1') `
-SessionCollectorScriptSource (Join-Path $PSScriptRoot 'worktime-session-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') ` -ExampleRulesSource (Join-Path $PSScriptRoot 'web-category-rules.example.json') `
-ExamplePolicySource (Join-Path $PSScriptRoot 'dlp-policy.example.json') ` -ExamplePolicySource (Join-Path $PSScriptRoot 'dlp-policy.example.json') `
-StateRoot $effectiveStateRoot ` -StateRoot $effectiveStateRoot `
@@ -146,6 +152,7 @@ $config = New-ActivityWatchDeploymentConfig `
-EmailCollectorScript $assetResult.EmailCollectorScript ` -EmailCollectorScript $assetResult.EmailCollectorScript `
-FileCollectorScript $effectiveFileCollector ` -FileCollectorScript $effectiveFileCollector `
-SessionCollectorScript $effectiveSessionCollector ` -SessionCollectorScript $effectiveSessionCollector `
-EvtxExportScript $effectiveEvtxExportScript `
-RulesPath $effectiveRules ` -RulesPath $effectiveRules `
-PolicyPath $effectivePolicy ` -PolicyPath $effectivePolicy `
-PollSeconds $effectivePollSeconds ` -PollSeconds $effectivePollSeconds `
@@ -158,6 +165,9 @@ $config = New-ActivityWatchDeploymentConfig `
-IncidentCaptureEnabled $effectiveIncidentCaptureEnabled ` -IncidentCaptureEnabled $effectiveIncidentCaptureEnabled `
-IncidentScreenshotEnabled $effectiveIncidentScreenshotEnabled ` -IncidentScreenshotEnabled $effectiveIncidentScreenshotEnabled `
-IncidentArtifactsRoot $effectiveIncidentArtifactsRoot ` -IncidentArtifactsRoot $effectiveIncidentArtifactsRoot `
-EvtxExportRoot $effectiveEvtxExportRoot `
-EvtxRetentionDays $effectiveEvtxRetentionDays `
-EvtxChannels $effectiveEvtxChannels `
-LogonMarkerEnabled $effectiveLogonMarkerEnabled ` -LogonMarkerEnabled $effectiveLogonMarkerEnabled `
-AwHostname $effectiveAwHostname ` -AwHostname $effectiveAwHostname `
-PolicyMode $effectivePolicyMode ` -PolicyMode $effectivePolicyMode `
+15 -1
View File
@@ -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' } $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' } $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' } $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 $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' } $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' } $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, $collectorScript,
$endpointCollectorScript, $endpointCollectorScript,
$sessionCollectorScript, $sessionCollectorScript,
$evtxExportScript,
$rulesPath, $rulesPath,
$policyPath, $policyPath,
$policyClientScript, $policyClientScript,
@@ -202,8 +204,20 @@ $result = [ordered]@{
jobTitlePolicyEnabled = $printJobTitlePolicyEnabled jobTitlePolicyEnabled = $printJobTitlePolicyEnabled
ok = [bool]($printServiceOperationalEnabled -and $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 $result