feat(hayabusa): automate detached server-side processing

This commit is contained in:
igor04091968
2026-05-21 17:52:01 +03:00
parent c0b9dad534
commit 712fccb630
11 changed files with 610 additions and 0 deletions
+102
View File
@@ -1545,6 +1545,108 @@
group: root
mode: "0755"
- name: Создать server-side ops bundle для Hayabusa
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: "0755"
loop:
- /opt/activitywatch/aw-rus-ops
- /opt/activitywatch/aw-rus-ops/hayabusa
- /opt/activitywatch/aw-rus-ops/ansible
- /opt/activitywatch/aw-rus-ops/drop
- name: Положить исходный wrapper в server-side ops bundle
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa.sh"
dest: /opt/activitywatch/aw-rus-ops/hayabusa/aw-hayabusa.sh
owner: root
group: root
mode: "0755"
- name: Установить helper link-case для Hayabusa
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa-link-case.py"
dest: /usr/local/bin/aw-hayabusa-link-case
owner: root
group: root
mode: "0755"
- name: Положить helper link-case в server-side ops bundle
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa-link-case.py"
dest: /opt/activitywatch/aw-rus-ops/hayabusa/aw-hayabusa-link-case.py
owner: root
group: root
mode: "0755"
- name: Установить helper from-windows для Hayabusa
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa-from-windows.py"
dest: /usr/local/bin/aw-hayabusa-from-windows
owner: root
group: root
mode: "0755"
- name: Положить helper from-windows в server-side ops bundle
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa-from-windows.py"
dest: /opt/activitywatch/aw-rus-ops/hayabusa/aw-hayabusa-from-windows.py
owner: root
group: root
mode: "0755"
- name: Положить README Hayabusa в server-side ops bundle
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/hayabusa/README.md"
dest: /opt/activitywatch/aw-rus-ops/hayabusa/README.md
owner: root
group: root
mode: "0644"
- name: Установить helper autoprocess для Hayabusa
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa-autoprocess.py"
dest: /usr/local/bin/aw-hayabusa-autoprocess
owner: root
group: root
mode: "0755"
- name: Положить helper autoprocess в server-side ops bundle
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa-autoprocess.py"
dest: /opt/activitywatch/aw-rus-ops/hayabusa/aw-hayabusa-autoprocess.py
owner: root
group: root
mode: "0755"
- name: Установить systemd unit aw-hayabusa-drop.service
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/aw-hayabusa-drop.service"
dest: /etc/systemd/system/aw-hayabusa-drop.service
owner: root
group: root
mode: "0644"
notify: Перезагрузить systemd
- name: Установить systemd unit aw-hayabusa-drop.path
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/aw-hayabusa-drop.path"
dest: /etc/systemd/system/aw-hayabusa-drop.path
owner: root
group: root
mode: "0644"
notify: Перезагрузить systemd
- name: Включить и запустить aw-hayabusa-drop.path
ansible.builtin.systemd:
name: aw-hayabusa-drop.path
enabled: true
state: started
daemon_reload: true
- name: Проверить server-side runner через doctor
ansible.builtin.command:
cmd: /usr/local/bin/aw-hayabusa doctor
+1
View File
@@ -102,6 +102,7 @@
- file-operations-collector.ps1
- worktime-session-collector.ps1
- export-evtx-for-hayabusa.ps1
- export-upload-hayabusa-to-aw-server.ps1
- migrate-awatch-rus-paths.ps1
- deploy-domain-users.ps1
- deploy-ensemble.ps1
+10
View File
@@ -0,0 +1,10 @@
[Unit]
Description=Watch AW-RUS Hayabusa drop directory for new zip packages
[Path]
PathModified=/opt/activitywatch/aw-rus-ops/drop
PathExistsGlob=/opt/activitywatch/aw-rus-ops/drop/*.zip
Unit=aw-hayabusa-drop.service
[Install]
WantedBy=multi-user.target
+13
View File
@@ -0,0 +1,13 @@
[Unit]
Description=AW-RUS Hayabusa auto-process dropped packages
After=network-online.target activitywatch-server.service
Wants=network-online.target activitywatch-server.service
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /usr/local/bin/aw-hayabusa-autoprocess
User=root
Group=root
StandardOutput=journal
StandardError=journal
SyslogIdentifier=aw-hayabusa-drop
+106
View File
@@ -0,0 +1,106 @@
# aw-rus Hayabusa Server Ops Bundle
This directory is the server-side operational bundle for Hayabusa on `10.10.10.13`.
## Goal
Allow operators to run the full bounded DFIR path without depending on the laptop repository.
## Server paths
- wrapper: `/usr/local/bin/aw-hayabusa`
- case linker: `/usr/local/bin/aw-hayabusa-link-case`
- Windows-driven E2E helper: `/usr/local/bin/aw-hayabusa-from-windows`
- ops bundle root: `/opt/activitywatch/aw-rus-ops`
- local inventory for server-side controller mode: `/opt/activitywatch/aw-rus-ops/ansible/inventory.ini`
- local controller venv: `/opt/activitywatch/aw-rus-ops/venv`
- local drop zone for fetched EVTX zips: `/opt/activitywatch/aw-rus-ops/drop`
## Minimal operator workflow on the server
1. Check runner health:
```bash
aw-hayabusa doctor
aw-hayabusa inventory
```
2. If a zip is already on the server:
```bash
aw-hayabusa accept --package /path/to/HOST-YYYYMMDD-HHMMSS.zip --host HOST
aw-hayabusa process-inbox --mode incident
```
3. Link the latest successful run to a case:
```bash
aw-hayabusa-link-case --case-id 30 --mode incident
```
## Full no-laptop workflow from the server
Prerequisites:
- `/opt/activitywatch/aw-rus-ops/venv` contains `ansible` and `pywinrm`
- `/opt/activitywatch/aw-rus-ops/ansible/inventory.ini` contains the live Windows connection details
- WinRM from `10.10.10.13` to the Windows host is reachable
Run:
```bash
aw-hayabusa-from-windows --days-back 1 --mode incident --case-id 30
```
This performs:
- Windows EVTX export via WinRM
- fetch of the newest zip directly onto `10.10.10.13`
- `aw-hayabusa accept`
- `aw-hayabusa process-inbox`
- bounded case linkage via case API
If WinRM from the server to Windows is blocked by network policy, use the drop-zone workflow below instead.
## Drop-zone automation on 10.10.10.13
The server can auto-process packages dropped into:
- `/opt/activitywatch/aw-rus-ops/drop`
Installed units:
- `/etc/systemd/system/aw-hayabusa-drop.service`
- `/etc/systemd/system/aw-hayabusa-drop.path`
Behavior:
- any `*.zip` placed in `drop/` is automatically accepted and processed
- optional `*.caseid` sidecar with the same basename triggers automatic bounded case linkage
- processed `*.zip` is moved out of `drop/` into `report_dir/input-drop/` to avoid repeated re-trigger loops
- sidecars are archived into `report_dir/input-sidecars/`
## Windows direct upload into the drop zone
Preferred production path when server-side WinRM is unavailable:
1. On Windows, use:
```powershell
powershell.exe -ExecutionPolicy Bypass -File C:\ProgramData\AWatch-rus\export-upload-hayabusa-to-aw-server.ps1 -DaysBack 1 -CaseId 30
```
2. The script will:
- run `C:\ProgramData\AWatch-rus\export-evtx-for-hayabusa.ps1`
- upload the newest zip to `/opt/activitywatch/aw-rus-ops/drop`
- upload matching `.caseid` when `-CaseId` is specified
- let `aw-hayabusa-drop.path` process the package automatically on `10.10.10.13`
One-time SSH prerequisite on the server for user `awops`:
```bash
printf '%s\n' 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILoFWQmgoUJj1P7mp1/fB5aBkI3fVgjPme9jmK8Gh9jr igor@snb-live' | sudo tee /var/lib/awops/.ssh/authorized_keys >/dev/null
sudo chown awops:awops /var/lib/awops/.ssh/authorized_keys
sudo chmod 600 /var/lib/awops/.ssh/authorized_keys
```
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
import argparse
import fcntl
import json
import pathlib
import shutil
import subprocess
import sys
from typing import Optional
DROP_DIR = pathlib.Path('/opt/activitywatch/aw-rus-ops/drop')
LOCK_PATH = pathlib.Path('/opt/hayabusa/state/aw-hayabusa-autoprocess.lock')
WRAPPER = pathlib.Path('/usr/local/bin/aw-hayabusa')
LINKER = pathlib.Path('/usr/local/bin/aw-hayabusa-link-case')
def run(cmd):
print('RUN', ' '.join(str(x) for x in cmd), flush=True)
subprocess.run(cmd, check=True)
def read_latest_intake():
return json.loads(pathlib.Path('/opt/hayabusa/state/latest-intake.json').read_text(encoding='utf-8'))
def load_sidecars(zip_path: pathlib.Path):
base = zip_path.with_suffix('')
caseid_path = base.with_suffix('.caseid')
meta_path = base.with_suffix('.meta.json')
meta = {}
if meta_path.is_file():
meta = json.loads(meta_path.read_text(encoding='utf-8'))
case_id = meta.get('case_id')
if case_id is None and caseid_path.is_file():
raw = caseid_path.read_text(encoding='utf-8').strip()
if raw:
case_id = int(raw)
return {
'base': base,
'case_id': case_id,
'host': meta.get('host'),
'mode': meta.get('mode', 'incident'),
'link_source': meta.get('link_source', 'aw-rus-drop-autoprocess'),
'caseid_path': caseid_path,
'meta_path': meta_path,
}
def archive_sidecars(report_dir: pathlib.Path, sidecars):
target_dir = report_dir / 'input-sidecars'
target_dir.mkdir(parents=True, exist_ok=True)
for path in (sidecars['caseid_path'], sidecars['meta_path']):
if path.is_file():
shutil.move(str(path), str(target_dir / path.name))
def archive_drop_package(report_dir: pathlib.Path, zip_path: pathlib.Path):
target_dir = report_dir / 'input-drop'
target_dir.mkdir(parents=True, exist_ok=True)
target_path = target_dir / zip_path.name
if target_path.exists():
target_path.unlink()
shutil.move(str(zip_path), str(target_path))
def guess_host(zip_path: pathlib.Path, sidecars) -> Optional[str]:
if sidecars['host']:
return str(sidecars['host'])
name = zip_path.stem
if '-' in name:
return name.split('-', 1)[0]
return name or None
def process_one(zip_path: pathlib.Path):
sidecars = load_sidecars(zip_path)
host = guess_host(zip_path, sidecars)
mode = sidecars['mode'] or 'incident'
accept_cmd = [str(WRAPPER), 'accept', '--package', str(zip_path)]
if host:
accept_cmd += ['--host', host]
run(accept_cmd)
run([str(WRAPPER), 'process-inbox', '--mode', mode, '--limit', '1'])
latest = read_latest_intake()
report_dir = pathlib.Path(latest['report_dir'])
archive_sidecars(report_dir, sidecars)
archive_drop_package(report_dir, zip_path)
if sidecars['case_id'] is not None:
run([str(LINKER), '--case-id', str(sidecars['case_id']), '--mode', mode, '--link-source', sidecars['link_source']])
return latest
def main():
p = argparse.ArgumentParser(description='Auto-process Hayabusa zip packages dropped onto aw-rus server')
p.add_argument('--drop-dir', default=str(DROP_DIR))
p.add_argument('--once', action='store_true', default=True)
args = p.parse_args()
drop_dir = pathlib.Path(args.drop_dir)
drop_dir.mkdir(parents=True, exist_ok=True)
LOCK_PATH.parent.mkdir(parents=True, exist_ok=True)
with LOCK_PATH.open('w') as lock_fh:
try:
fcntl.flock(lock_fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
print('autoprocess already running', file=sys.stderr)
return 0
zips = sorted(drop_dir.glob('*.zip'))
if not zips:
print('no zip packages in drop dir')
return 0
for zip_path in zips:
latest = process_one(zip_path)
print(json.dumps({'processed': str(zip_path), 'latest_intake': latest}, ensure_ascii=False, indent=2))
return 0
if __name__ == '__main__':
raise SystemExit(main())
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
import argparse
import json
import pathlib
import re
import subprocess
import sys
OPS_ROOT = pathlib.Path('/opt/activitywatch/aw-rus-ops')
DEFAULT_INVENTORY = OPS_ROOT / 'ansible' / 'inventory.ini'
DEFAULT_DROP = OPS_ROOT / 'drop'
DEFAULT_ANSIBLE = OPS_ROOT / 'venv' / 'bin' / 'ansible'
DEFAULT_WRAPPER = pathlib.Path('/usr/local/bin/aw-hayabusa')
DEFAULT_LINKER = pathlib.Path('/usr/local/bin/aw-hayabusa-link-case')
WINDOWS_EXPORT_CMD = r"powershell.exe -ExecutionPolicy Bypass -File C:\ProgramData\AWatch-rus\export-evtx-for-hayabusa.ps1 -DaysBack {days_back} | ConvertTo-Json -Depth 8 -Compress"
WINDOWS_LATEST_ZIP_CMD = r"Get-ChildItem 'C:\ProgramData\AWatch-rus\forensics\evtx-exports' -File -Filter '*.zip' | Sort-Object LastWriteTime -Descending | Select-Object -First 1 FullName,Length,LastWriteTime | ConvertTo-Json -Compress"
def run(cmd):
proc = subprocess.run(cmd, text=True, capture_output=True)
if proc.returncode != 0:
sys.stderr.write(proc.stdout)
sys.stderr.write(proc.stderr)
raise SystemExit(proc.returncode)
return proc.stdout
def extract_json_blob(text):
matches = re.findall(r'(\{.*\}|\[.*\])', text, re.S)
for candidate in reversed(matches):
try:
return json.loads(candidate)
except Exception:
continue
raise SystemExit('cannot parse JSON from ansible output:\n' + text)
def main():
p = argparse.ArgumentParser(description='Run Windows EVTX export and Hayabusa intake directly from aw-server, without the laptop')
p.add_argument('--inventory', default=str(DEFAULT_INVENTORY))
p.add_argument('--ansible-bin', default=str(DEFAULT_ANSIBLE))
p.add_argument('--drop-dir', default=str(DEFAULT_DROP))
p.add_argument('--days-back', type=int, default=1)
p.add_argument('--mode', default='incident', choices=['quick', 'incident', 'full'])
p.add_argument('--case-id', type=int)
p.add_argument('--link-source', default='aw-rus-ops-from-windows')
p.add_argument('--windows-group', default='aw_windows')
p.add_argument('--wrapper', default=str(DEFAULT_WRAPPER))
p.add_argument('--linker', default=str(DEFAULT_LINKER))
args = p.parse_args()
inventory = pathlib.Path(args.inventory)
ansible_bin = pathlib.Path(args.ansible_bin)
drop_dir = pathlib.Path(args.drop_dir)
wrapper = pathlib.Path(args.wrapper)
linker = pathlib.Path(args.linker)
if not inventory.is_file():
raise SystemExit(f'inventory not found: {inventory}')
if not ansible_bin.is_file():
raise SystemExit(f'ansible binary not found: {ansible_bin}')
if not wrapper.is_file():
raise SystemExit(f'wrapper not found: {wrapper}')
drop_dir.mkdir(parents=True, exist_ok=True)
export_cmd = [str(ansible_bin), args.windows_group, '-i', str(inventory), '-m', 'win_shell', '-a', WINDOWS_EXPORT_CMD.format(days_back=args.days_back)]
print('RUN_EXPORT', ' '.join(export_cmd))
export_out = run(export_cmd)
export_json = extract_json_blob(export_out)
list_cmd = [str(ansible_bin), args.windows_group, '-i', str(inventory), '-m', 'win_shell', '-a', WINDOWS_LATEST_ZIP_CMD]
print('RUN_LIST', ' '.join(list_cmd))
latest_out = run(list_cmd)
latest = extract_json_blob(latest_out)
if isinstance(latest, list):
latest = latest[0]
remote_zip = latest['FullName']
filename = pathlib.PureWindowsPath(remote_zip).name
local_zip = drop_dir / filename
remote_zip_posix = remote_zip.replace('\\', '/')
fetch_cmd = [str(ansible_bin), args.windows_group, '-i', str(inventory), '-m', 'fetch', '-a', f'src={remote_zip_posix} dest={drop_dir}/ flat=yes']
print('RUN_FETCH', ' '.join(fetch_cmd))
run(fetch_cmd)
if not local_zip.is_file():
raise SystemExit(f'fetched zip not found: {local_zip}')
accept_cmd = [str(wrapper), 'accept', '--package', str(local_zip)]
host = export_json.get('hostname') or pathlib.Path(filename).stem.split('-')[0]
if host:
accept_cmd += ['--host', str(host)]
print('RUN_ACCEPT', ' '.join(accept_cmd))
subprocess.run(accept_cmd, check=True)
process_cmd = [str(wrapper), 'process-inbox', '--mode', args.mode, '--limit', '1']
print('RUN_PROCESS', ' '.join(process_cmd))
subprocess.run(process_cmd, check=True)
if args.case_id is not None:
if not linker.is_file():
raise SystemExit(f'linker not found: {linker}')
link_cmd = [str(linker), '--case-id', str(args.case_id), '--mode', args.mode, '--link-source', args.link_source]
print('RUN_LINK', ' '.join(link_cmd))
subprocess.run(link_cmd, check=True)
latest_intake = pathlib.Path('/opt/hayabusa/state/latest-intake.json')
print('LATEST_INTAKE')
print(latest_intake.read_text(encoding='utf-8'))
if __name__ == '__main__':
main()
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
import argparse
import json
import pathlib
import sys
import urllib.error
import urllib.request
def build_payload(intake, mode, link_source):
report_dir = pathlib.Path(intake['report_dir'])
return {
'tool': 'hayabusa',
'host': intake['host'],
'mode': mode,
'status': intake['status'],
'intake_id': intake['intake_id'],
'package_path': intake['package_path'],
'sha256': intake['sha256'],
'report_dir': intake['report_dir'],
'summary_html': str(report_dir / 'summary.html'),
'timeline_path': str(report_dir / 'timeline.jsonl'),
'manifest_path': str(report_dir / 'manifest.json'),
'link_source': link_source,
}
def post_json(url, payload):
data = json.dumps(payload, ensure_ascii=False).encode('utf-8')
req = urllib.request.Request(url, data=data, method='POST', headers={'Content-Type': 'application/json'})
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read().decode('utf-8'))
def get_json(url):
with urllib.request.urlopen(url) as resp:
return json.loads(resp.read().decode('utf-8'))
def main():
p = argparse.ArgumentParser(description='Link latest or specified Hayabusa intake metadata to AW-rus case management')
p.add_argument('--case-id', type=int, required=True)
p.add_argument('--intake-json', default='/opt/hayabusa/state/latest-intake.json')
p.add_argument('--case-api-base', default='http://127.0.0.1:5602')
p.add_argument('--mode', default='incident')
p.add_argument('--link-source', default='aw-rus-ops')
args = p.parse_args()
intake_path = pathlib.Path(args.intake_json)
if not intake_path.is_file():
raise SystemExit(f'intake json not found: {intake_path}')
intake = json.loads(intake_path.read_text(encoding='utf-8'))
payload = build_payload(intake, args.mode, args.link_source)
case_url = f"{args.case_api_base.rstrip('/')}/api/0/dlp/cases/{args.case_id}/forensics/hayabusa"
try:
post_json(case_url, payload)
except urllib.error.HTTPError as exc:
body = exc.read().decode('utf-8', errors='replace')
raise SystemExit(f'case API POST failed: HTTP {exc.code}: {body}')
case = get_json(f"{args.case_api_base.rstrip('/')}/api/0/dlp/cases/{args.case_id}")
print(json.dumps({'case_id': args.case_id, 'intake': intake, 'forensics': case.get('forensics')}, ensure_ascii=False, indent=2))
if __name__ == '__main__':
main()
@@ -400,6 +400,7 @@ function Copy-ActivityWatchCollectorAssets {
[Parameter(Mandatory = $true)]
[string]$SessionCollectorScriptSource,
[string]$EvtxExportScriptSource,
[string]$HayabusaUploadScriptSource,
[string]$EmailCollectorScriptSource,
[Parameter(Mandatory = $true)]
[string]$ExampleRulesSource,
@@ -419,6 +420,7 @@ function Copy-ActivityWatchCollectorAssets {
$fileCollectorTarget = Join-Path $StateRoot 'file-operations-collector.ps1'
$sessionCollectorTarget = Join-Path $StateRoot 'worktime-session-collector.ps1'
$evtxExportTarget = Join-Path $StateRoot 'export-evtx-for-hayabusa.ps1'
$hayabusaUploadTarget = Join-Path $StateRoot 'export-upload-hayabusa-to-aw-server.ps1'
$emailCollectorTarget = Join-Path $StateRoot 'email-outbound-collector.ps1'
$exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json'
$rulesTarget = Join-Path $StateRoot 'web-category-rules.json'
@@ -435,6 +437,9 @@ function Copy-ActivityWatchCollectorAssets {
if ($EvtxExportScriptSource -and (Test-Path -LiteralPath $EvtxExportScriptSource)) {
Copy-Item -LiteralPath $EvtxExportScriptSource -Destination $evtxExportTarget -Force
}
if ($HayabusaUploadScriptSource -and (Test-Path -LiteralPath $HayabusaUploadScriptSource)) {
Copy-Item -LiteralPath $HayabusaUploadScriptSource -Destination $hayabusaUploadTarget -Force
}
if ($EmailCollectorScriptSource -and (Test-Path -LiteralPath $EmailCollectorScriptSource)) {
Copy-Item -LiteralPath $EmailCollectorScriptSource -Destination $emailCollectorTarget -Force
}
@@ -464,6 +469,7 @@ function Copy-ActivityWatchCollectorAssets {
FileCollectorScript = $fileCollectorTarget
SessionCollectorScript = $sessionCollectorTarget
EvtxExportScript = $evtxExportTarget
HayabusaUploadScript = $hayabusaUploadTarget
EmailCollectorScript = $emailCollectorTarget
ExampleRules = $exampleRulesTarget
ActiveRules = $rulesTarget
+2
View File
@@ -64,6 +64,7 @@ $emailCollectorSource = Join-Path $PSScriptRoot 'email-outbound-collector.ps1'
$fileCollectorSource = Join-Path $PSScriptRoot 'file-operations-collector.ps1'
$sessionCollectorSource = Join-Path $PSScriptRoot 'worktime-session-collector.ps1'
$evtxExportScriptSource = Join-Path $PSScriptRoot 'export-evtx-for-hayabusa.ps1'
$hayabusaUploadScriptSource = Join-Path $PSScriptRoot 'export-upload-hayabusa-to-aw-server.ps1'
$exampleRulesSource = Join-Path $PSScriptRoot 'web-category-rules.example.json'
$examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json'
@@ -83,6 +84,7 @@ $assetResult = Copy-ActivityWatchCollectorAssets `
-FileCollectorScriptSource $fileCollectorSource `
-SessionCollectorScriptSource $sessionCollectorSource `
-EvtxExportScriptSource $evtxExportScriptSource `
-HayabusaUploadScriptSource $hayabusaUploadScriptSource `
-ExampleRulesSource $exampleRulesSource `
-ExamplePolicySource $examplePolicySource `
-StateRoot $StateRoot `
@@ -0,0 +1,75 @@
[CmdletBinding()]
param(
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
[int]$DaysBack = 1,
[string]$ServerHost = '10.10.10.13',
[string]$ServerUser = 'awops',
[string]$RemoteDropDir = '/opt/activitywatch/aw-rus-ops/drop',
[string]$RemoteKeyPath = 'C:\ProgramData\AWatch-rus\ssh\awops_ed25519',
[string]$Mode = 'incident',
[Nullable[int]]$CaseId,
[switch]$RunRemote,
[switch]$NoLink
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$exportScript = 'C:\ProgramData\AWatch-rus\export-evtx-for-hayabusa.ps1'
if (-not (Test-Path -LiteralPath $exportScript)) {
throw "export script not found: $exportScript"
}
if (-not (Test-Path -LiteralPath $RemoteKeyPath)) {
throw "SSH private key not found: $RemoteKeyPath"
}
$export = powershell.exe -ExecutionPolicy Bypass -File $exportScript -ConfigPath $ConfigPath -DaysBack $DaysBack | ConvertFrom-Json
$zipPath = [string]$export.zipPath
$hostName = [string]$export.hostname
if ([string]::IsNullOrWhiteSpace($zipPath) -or -not (Test-Path -LiteralPath $zipPath)) {
throw "zipPath missing or not found after export: $zipPath"
}
$zipName = Split-Path -Leaf $zipPath
$remoteTarget = "$ServerUser@$ServerHost`:$RemoteDropDir/"
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($zipPath)
$caseIdPath = Join-Path ([System.IO.Path]::GetDirectoryName($zipPath)) ($baseName + '.caseid')
& scp.exe -i $RemoteKeyPath -o StrictHostKeyChecking=no -o UserKnownHostsFile=NUL $zipPath $remoteTarget
if ($LASTEXITCODE -ne 0) {
throw "scp upload failed with rc=$LASTEXITCODE"
}
if ($CaseId.HasValue) {
Set-Content -LiteralPath $caseIdPath -Value ([string]$CaseId.Value) -Encoding ASCII
& scp.exe -i $RemoteKeyPath -o StrictHostKeyChecking=no -o UserKnownHostsFile=NUL $caseIdPath $remoteTarget
if ($LASTEXITCODE -ne 0) {
throw "scp caseid upload failed with rc=$LASTEXITCODE"
}
}
$result = [ordered]@{
exportedZip = $zipPath
uploadedTo = "$RemoteDropDir/$zipName"
caseIdSidecar = if ($CaseId.HasValue) { "$RemoteDropDir/$baseName.caseid" } else { $null }
hostname = $hostName
mode = $Mode
runRemote = [bool]$RunRemote
}
if ($RunRemote) {
$accept = "sudo /usr/local/bin/aw-hayabusa accept --package $RemoteDropDir/$zipName --host $hostName"
$process = "sudo /usr/local/bin/aw-hayabusa process-inbox --mode $Mode --limit 1"
$link = if ($CaseId.HasValue -and -not $NoLink) {
" && sudo /usr/local/bin/aw-hayabusa-link-case --case-id $($CaseId.Value) --mode $Mode --link-source windows-direct-upload"
} else {
''
}
$remoteCmd = "$accept && $process$link && sudo cat /opt/hayabusa/state/latest-intake.json"
$remoteOut = & ssh.exe -i $RemoteKeyPath -o StrictHostKeyChecking=no -o UserKnownHostsFile=NUL "$ServerUser@$ServerHost" $remoteCmd
if ($LASTEXITCODE -ne 0) {
throw "remote Hayabusa run failed with rc=$LASTEXITCODE"
}
$result.remoteOutput = $remoteOut
}
$result | ConvertTo-Json -Depth 8