diff --git a/ansible/deploy_aw_server.yml b/ansible/deploy_aw_server.yml index 46f3eb7..bcea8d5 100644 --- a/ansible/deploy_aw_server.yml +++ b/ansible/deploy_aw_server.yml @@ -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 diff --git a/ansible/deploy_aw_windows.yml b/ansible/deploy_aw_windows.yml index 52e924d..8323143 100644 --- a/ansible/deploy_aw_windows.yml +++ b/ansible/deploy_aw_windows.yml @@ -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 diff --git a/aw-server/aw-hayabusa-drop.path b/aw-server/aw-hayabusa-drop.path new file mode 100644 index 0000000..c847bd9 --- /dev/null +++ b/aw-server/aw-hayabusa-drop.path @@ -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 diff --git a/aw-server/aw-hayabusa-drop.service b/aw-server/aw-hayabusa-drop.service new file mode 100644 index 0000000..f614373 --- /dev/null +++ b/aw-server/aw-hayabusa-drop.service @@ -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 diff --git a/aw-server/hayabusa/README.md b/aw-server/hayabusa/README.md new file mode 100644 index 0000000..4ac3790 --- /dev/null +++ b/aw-server/hayabusa/README.md @@ -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 +``` diff --git a/aw-server/hayabusa/aw-hayabusa-autoprocess.py b/aw-server/hayabusa/aw-hayabusa-autoprocess.py new file mode 100644 index 0000000..9b34a5c --- /dev/null +++ b/aw-server/hayabusa/aw-hayabusa-autoprocess.py @@ -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()) diff --git a/aw-server/hayabusa/aw-hayabusa-from-windows.py b/aw-server/hayabusa/aw-hayabusa-from-windows.py new file mode 100644 index 0000000..6d157ee --- /dev/null +++ b/aw-server/hayabusa/aw-hayabusa-from-windows.py @@ -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() diff --git a/aw-server/hayabusa/aw-hayabusa-link-case.py b/aw-server/hayabusa/aw-hayabusa-link-case.py new file mode 100644 index 0000000..5853ade --- /dev/null +++ b/aw-server/hayabusa/aw-hayabusa-link-case.py @@ -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() diff --git a/windows/ActivityWatch.Windows.Common.psm1 b/windows/ActivityWatch.Windows.Common.psm1 index 2f7f461..b5ece7b 100755 --- a/windows/ActivityWatch.Windows.Common.psm1 +++ b/windows/ActivityWatch.Windows.Common.psm1 @@ -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 diff --git a/windows/deploy-domain-users.ps1 b/windows/deploy-domain-users.ps1 index 0f9cb1f..0b93b1e 100755 --- a/windows/deploy-domain-users.ps1 +++ b/windows/deploy-domain-users.ps1 @@ -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 ` diff --git a/windows/export-upload-hayabusa-to-aw-server.ps1 b/windows/export-upload-hayabusa-to-aw-server.ps1 new file mode 100644 index 0000000..d088a81 --- /dev/null +++ b/windows/export-upload-hayabusa-to-aw-server.ps1 @@ -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