feat(hayabusa): automate detached server-side processing
This commit is contained in:
@@ -1545,6 +1545,108 @@
|
|||||||
group: root
|
group: root
|
||||||
mode: "0755"
|
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
|
- name: Проверить server-side runner через doctor
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
cmd: /usr/local/bin/aw-hayabusa doctor
|
cmd: /usr/local/bin/aw-hayabusa doctor
|
||||||
|
|||||||
@@ -102,6 +102,7 @@
|
|||||||
- file-operations-collector.ps1
|
- file-operations-collector.ps1
|
||||||
- worktime-session-collector.ps1
|
- worktime-session-collector.ps1
|
||||||
- export-evtx-for-hayabusa.ps1
|
- export-evtx-for-hayabusa.ps1
|
||||||
|
- export-upload-hayabusa-to-aw-server.ps1
|
||||||
- migrate-awatch-rus-paths.ps1
|
- migrate-awatch-rus-paths.ps1
|
||||||
- deploy-domain-users.ps1
|
- deploy-domain-users.ps1
|
||||||
- deploy-ensemble.ps1
|
- deploy-ensemble.ps1
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$SessionCollectorScriptSource,
|
[string]$SessionCollectorScriptSource,
|
||||||
[string]$EvtxExportScriptSource,
|
[string]$EvtxExportScriptSource,
|
||||||
|
[string]$HayabusaUploadScriptSource,
|
||||||
[string]$EmailCollectorScriptSource,
|
[string]$EmailCollectorScriptSource,
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$ExampleRulesSource,
|
[string]$ExampleRulesSource,
|
||||||
@@ -419,6 +420,7 @@ function Copy-ActivityWatchCollectorAssets {
|
|||||||
$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'
|
$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'
|
$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'
|
||||||
@@ -435,6 +437,9 @@ function Copy-ActivityWatchCollectorAssets {
|
|||||||
if ($EvtxExportScriptSource -and (Test-Path -LiteralPath $EvtxExportScriptSource)) {
|
if ($EvtxExportScriptSource -and (Test-Path -LiteralPath $EvtxExportScriptSource)) {
|
||||||
Copy-Item -LiteralPath $EvtxExportScriptSource -Destination $evtxExportTarget -Force
|
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)) {
|
if ($EmailCollectorScriptSource -and (Test-Path -LiteralPath $EmailCollectorScriptSource)) {
|
||||||
Copy-Item -LiteralPath $EmailCollectorScriptSource -Destination $emailCollectorTarget -Force
|
Copy-Item -LiteralPath $EmailCollectorScriptSource -Destination $emailCollectorTarget -Force
|
||||||
}
|
}
|
||||||
@@ -464,6 +469,7 @@ function Copy-ActivityWatchCollectorAssets {
|
|||||||
FileCollectorScript = $fileCollectorTarget
|
FileCollectorScript = $fileCollectorTarget
|
||||||
SessionCollectorScript = $sessionCollectorTarget
|
SessionCollectorScript = $sessionCollectorTarget
|
||||||
EvtxExportScript = $evtxExportTarget
|
EvtxExportScript = $evtxExportTarget
|
||||||
|
HayabusaUploadScript = $hayabusaUploadTarget
|
||||||
EmailCollectorScript = $emailCollectorTarget
|
EmailCollectorScript = $emailCollectorTarget
|
||||||
ExampleRules = $exampleRulesTarget
|
ExampleRules = $exampleRulesTarget
|
||||||
ActiveRules = $rulesTarget
|
ActiveRules = $rulesTarget
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ $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'
|
$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'
|
$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'
|
||||||
|
|
||||||
@@ -83,6 +84,7 @@ $assetResult = Copy-ActivityWatchCollectorAssets `
|
|||||||
-FileCollectorScriptSource $fileCollectorSource `
|
-FileCollectorScriptSource $fileCollectorSource `
|
||||||
-SessionCollectorScriptSource $sessionCollectorSource `
|
-SessionCollectorScriptSource $sessionCollectorSource `
|
||||||
-EvtxExportScriptSource $evtxExportScriptSource `
|
-EvtxExportScriptSource $evtxExportScriptSource `
|
||||||
|
-HayabusaUploadScriptSource $hayabusaUploadScriptSource `
|
||||||
-ExampleRulesSource $exampleRulesSource `
|
-ExampleRulesSource $exampleRulesSource `
|
||||||
-ExamplePolicySource $examplePolicySource `
|
-ExamplePolicySource $examplePolicySource `
|
||||||
-StateRoot $StateRoot `
|
-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
|
||||||
Reference in New Issue
Block a user