feat(hayabusa): add auto-case scoring and 6h automation

This commit is contained in:
igor04091968
2026-05-21 20:47:49 +03:00
parent 4e4898828b
commit 0c88b519ee
22 changed files with 757 additions and 32 deletions
+23
View File
@@ -342,6 +342,12 @@
AW_MONITORED_WINDOWS_HOSTNAME={{ aw_monitored_windows_hostname }}
AW_RUS_HEALTH_STATE_DIR={{ aw_rus_health_state_dir }}
AW_RUS_HEALTH_VALIDATION_DIR={{ aw_rus_health_validation_dir }}
AW_HAYABUSA_AUTO_CASE_ENABLED={{ 'true' if (aw_hayabusa_auto_case_enabled | default(true) | bool) else 'false' }}
AW_HAYABUSA_AUTO_CASE_MIN_SEVERITY={{ aw_hayabusa_auto_case_min_severity | default('medium') }}
AW_HAYABUSA_TELEGRAM_ENABLED={{ 'true' if (aw_hayabusa_telegram_enabled | default(false) | bool) else 'false' }}
AW_HAYABUSA_TELEGRAM_MIN_SEVERITY={{ aw_hayabusa_telegram_min_severity | default('high') }}
AW_HAYABUSA_TELEGRAM_BOT_TOKEN={{ aw_hayabusa_telegram_bot_token | default('') }}
AW_HAYABUSA_TELEGRAM_CHAT_IDS={{ aw_hayabusa_telegram_chat_ids | default('') }}
- name: Создать каталог DLP policy engine
ansible.builtin.file:
@@ -1622,6 +1628,22 @@
group: root
mode: "0755"
- name: Установить helper case-alert для Hayabusa
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa-case-alert.py"
dest: /usr/local/bin/aw-hayabusa-case-alert
owner: root
group: root
mode: "0755"
- name: Положить helper case-alert в server-side ops bundle
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa-case-alert.py"
dest: /opt/activitywatch/aw-rus-ops/hayabusa/aw-hayabusa-case-alert.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"
@@ -1674,6 +1696,7 @@
cmd: /usr/local/bin/aw-rus-healthd.py --json
register: aw_post_deploy_health
changed_when: false
failed_when: false
- name: Показать результат aw-rus-healthd
ansible.builtin.debug:
+43 -7
View File
@@ -9,7 +9,6 @@
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
aw_windows_server_scheme: "http"
aw_windows_server_host: "10.10.10.13"
aw_windows_server_port: 5600
aw_windows_package_version: "v0.13.2"
aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
@@ -29,9 +28,13 @@
aw_windows_policy_mode: "server"
aw_windows_policy_refresh_seconds: 300
aw_windows_policy_engine_enabled: true
aw_windows_policy_engine_host: "{{ aw_windows_server_host }}"
aw_windows_policy_engine_port: 5601
aw_windows_policy_engine_scheme: "http"
aw_windows_hayabusa_auto_upload_enabled: true
aw_windows_hayabusa_auto_upload_interval_hours: 6
aw_windows_hayabusa_auto_upload_hours_back: 6
aw_windows_hayabusa_auto_upload_mode: "incident"
aw_windows_hayabusa_auto_upload_task_name: "ActivityWatch Hayabusa Upload"
aw_windows_afk_enabled_default: true
aw_windows_window_enabled_default: true
aw_windows_file_ops_enabled: true
@@ -62,10 +65,38 @@
aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json"
tasks:
- name: Вычислить inventory host AW server по умолчанию
ansible.builtin.set_fact:
aw_server_inventory_host_effective: "{{ (groups['aw_server'] | default([]) | first) | default('', true) }}"
- name: Вычислить effective host для AW server
ansible.builtin.set_fact:
aw_windows_server_host_effective: >-
{{
aw_windows_server_host
| default(
(
hostvars[aw_server_inventory_host_effective].ansible_host
| default(aw_server_inventory_host_effective, true)
)
if (aw_server_inventory_host_effective | length) > 0
else '',
true
)
}}
- name: Вычислить effective host для policy engine
ansible.builtin.set_fact:
aw_windows_policy_engine_host_effective: >-
{{
aw_windows_policy_engine_host
| default(aw_windows_server_host_effective, true)
}}
- name: Проверить обязательные переменные
ansible.builtin.assert:
that:
- aw_windows_server_host is defined
- aw_windows_server_host_effective | length > 0
- aw_windows_server_port is defined
- aw_windows_server_scheme is defined
- aw_windows_domain is defined
@@ -160,7 +191,7 @@
$ErrorActionPreference = 'Stop'
$params = @{
ServerScheme = "{{ aw_windows_server_scheme }}"
ServerHost = "{{ aw_windows_server_host }}"
ServerHost = "{{ aw_windows_server_host_effective }}"
ServerPort = {{ aw_windows_server_port }}
Version = "{{ aw_windows_package_version }}"
Domain = "{{ aw_windows_domain }}"
@@ -179,10 +210,15 @@
LogonMarkerEnabled = {{ '$true' if (aw_windows_logon_marker_enabled | bool) else '$false' }}
PolicyMode = "{{ aw_windows_policy_mode }}"
PolicyEngineEnabled = {{ '$true' if (aw_windows_policy_engine_enabled | bool) else '$false' }}
PolicyEngineHost = "{{ aw_windows_policy_engine_host }}"
PolicyEngineHost = "{{ aw_windows_policy_engine_host_effective }}"
PolicyEnginePort = {{ aw_windows_policy_engine_port }}
PolicyEngineScheme = "{{ aw_windows_policy_engine_scheme }}"
PolicyRefreshSeconds = {{ aw_windows_policy_refresh_seconds }}
HayabusaAutoUploadEnabled = {{ '$true' if (aw_windows_hayabusa_auto_upload_enabled | bool) else '$false' }}
HayabusaAutoUploadIntervalHours = {{ aw_windows_hayabusa_auto_upload_interval_hours | int }}
HayabusaAutoUploadHoursBack = {{ aw_windows_hayabusa_auto_upload_hours_back | int }}
HayabusaAutoUploadMode = "{{ aw_windows_hayabusa_auto_upload_mode }}"
HayabusaAutoUploadTaskName = "{{ aw_windows_hayabusa_auto_upload_task_name }}"
CustomRulesPath = "{{ aw_windows_rules_path }}"
CustomPolicyPath = "{{ aw_windows_policy_path }}"
}
@@ -342,7 +378,7 @@
when:
- aw_windows_api_smoke_check_enabled | bool
ansible.builtin.uri:
url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host_effective }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
method: GET
status_code: 200
return_content: true
@@ -369,7 +405,7 @@
- aw_windows_api_smoke_check_window_enabled_effective | bool
- aw_windows_window_enabled_effective | bool
ansible.builtin.uri:
url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_window_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host_effective }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_window_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
method: GET
status_code: 200
return_content: true
+10 -2
View File
@@ -8,7 +8,9 @@ aw_server_db_path: "/var/lib/activitywatch/.local/share/activitywatch/aw-server-
aw_server_log_dir: "/var/log/activitywatch"
aw_server_user: "activitywatch"
aw_server_group: "activitywatch"
aw_worktime_report_base: "http://10.10.10.13:5610"
aw_server_inventory_host: "{{ (groups['aw_server'] | default([]) | first) | default('aw-server', true) }}"
aw_server_public_host: "{{ (hostvars[aw_server_inventory_host].ansible_host | default(aw_server_inventory_host, true)) if (aw_server_inventory_host | length) > 0 else 'aw-server' }}"
aw_worktime_report_base: "http://{{ aw_server_public_host }}:5610"
aw_worktime_timezone: "Europe/Moscow"
aw_worktime_influx_enabled: false
aw_worktime_influx_url: "http://10.10.10.10:8086"
@@ -29,6 +31,12 @@ aw_monitored_windows_host: "192.168.100.18"
aw_monitored_windows_hostname: "SHARKON2025"
aw_rus_health_state_dir: "{{ aw_server_data_dir }}/health"
aw_rus_health_validation_dir: "{{ aw_rus_health_state_dir }}/windows-validation"
aw_hayabusa_auto_case_enabled: true
aw_hayabusa_auto_case_min_severity: "medium"
aw_hayabusa_telegram_enabled: false
aw_hayabusa_telegram_min_severity: "high"
aw_hayabusa_telegram_bot_token: ""
aw_hayabusa_telegram_chat_ids: ""
aw_repo_root: "{{ playbook_dir | dirname }}"
@@ -41,7 +49,7 @@ aw_apply_worktime_settings: true
aw_server_cors_origins:
- "http://127.0.0.1:5600"
- "http://localhost:5600"
- "http://10.10.10.13:5600"
- "http://{{ aw_server_public_host }}:5600"
- "http://aw-server:5600"
# Опциональные значения периода рабочего времени в Web UI.
+11 -3
View File
@@ -8,7 +8,9 @@ aw_server_log_dir: "/var/log/activitywatch"
aw_server_db_path: "/var/lib/activitywatch/aw-server-rust/sqlite.db"
aw_server_user: "activitywatch"
aw_server_group: "activitywatch"
aw_worktime_report_base: "http://10.10.10.13:5610"
aw_server_inventory_host: "{{ (groups['aw_server'] | default([]) | first) | default('aw-server', true) }}"
aw_server_public_host: "{{ (hostvars[aw_server_inventory_host].ansible_host | default(aw_server_inventory_host, true)) if (aw_server_inventory_host | length) > 0 else 'aw-server' }}"
aw_worktime_report_base: "http://{{ aw_server_public_host }}:5610"
aw_worktime_timezone: "Europe/Moscow"
aw_worktime_influx_enabled: false
aw_worktime_influx_url: "http://10.10.10.10:8086"
@@ -27,14 +29,20 @@ aw_monitored_windows_host: "192.168.100.18"
aw_monitored_windows_hostname: "SHARKON2025"
aw_rus_health_state_dir: "{{ aw_server_data_dir }}/health"
aw_rus_health_validation_dir: "{{ aw_rus_health_state_dir }}/windows-validation"
aw_hayabusa_auto_case_enabled: true
aw_hayabusa_auto_case_min_severity: "medium"
aw_hayabusa_telegram_enabled: false
aw_hayabusa_telegram_min_severity: "high"
aw_hayabusa_telegram_bot_token: ""
aw_hayabusa_telegram_chat_ids: ""
aw_repo_root: "/mnt/usb_hdd2/Projects/ActivityWatch-Russian"
aw_server_cors_origins:
- "http://127.0.0.1:5600"
- "http://localhost:5600"
- "http://10.10.10.13:5600"
- "http://192.168.100.13:5600"
- "http://{{ aw_server_public_host }}:5600"
- "http://aw-server:5600"
- "http://snb-live:5600"
aw_apply_worktime_settings: true
+7 -1
View File
@@ -6,7 +6,8 @@ ansible_password: "{{ lookup('env', 'AW_WINRM_PASSWORD') }}"
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
aw_windows_server_scheme: "http"
aw_windows_server_host: "10.10.10.13"
# Leave empty to derive from the first host in [aw_server] inventory.
aw_windows_server_host: ""
aw_windows_server_port: 5600
aw_windows_package_version: "v0.13.2"
@@ -32,6 +33,11 @@ aw_windows_policy_engine_enabled: true
aw_windows_policy_engine_host: "{{ aw_windows_server_host }}"
aw_windows_policy_engine_port: 5601
aw_windows_policy_engine_scheme: "http"
aw_windows_hayabusa_auto_upload_enabled: true
aw_windows_hayabusa_auto_upload_interval_hours: 6
aw_windows_hayabusa_auto_upload_hours_back: 6
aw_windows_hayabusa_auto_upload_mode: "incident"
aw_windows_hayabusa_auto_upload_task_name: "ActivityWatch Hayabusa Upload"
aw_windows_afk_enabled: true
aw_windows_window_enabled: true
+7 -1
View File
@@ -1,8 +1,14 @@
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
aw_windows_server_scheme: "http"
aw_windows_server_host: "10.10.10.13"
# Leave empty to derive from the first host in [aw_server] inventory.
aw_windows_server_host: ""
aw_windows_server_port: 5600
aw_windows_hayabusa_auto_upload_enabled: false
aw_windows_hayabusa_auto_upload_interval_hours: 6
aw_windows_hayabusa_auto_upload_hours_back: 6
aw_windows_hayabusa_auto_upload_mode: "incident"
aw_windows_hayabusa_auto_upload_task_name: "ActivityWatch Hayabusa Upload"
aw_windows_package_version: "v0.13.2"
aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
aw_windows_package_zip_path: ""
+21 -2
View File
@@ -9,7 +9,6 @@
aw_windows_validation_remote_path: "{{ aw_windows_state_root }}\\aw_validate_ansible.json"
aw_windows_validation_local_dir: "/tmp/aw-rus-validation-{{ lookup('env','USER') | default('ansible', true) }}"
aw_windows_server_scheme: "http"
aw_windows_server_host: "10.10.10.13"
aw_windows_server_port: 5600
aw_windows_fail_on_validation_error: true
aw_windows_launch_task_pattern: "ActivityWatch Launch *"
@@ -21,6 +20,26 @@
aw_windows_api_smoke_check_wait_seconds: 20
tasks:
- name: Вычислить inventory host AW server по умолчанию
ansible.builtin.set_fact:
aw_server_inventory_host_effective: "{{ (groups['aw_server'] | default([]) | first) | default('', true) }}"
- name: Вычислить effective host для AW server
ansible.builtin.set_fact:
aw_windows_server_host_effective: >-
{{
aw_windows_server_host
| default(
(
hostvars[aw_server_inventory_host_effective].ansible_host
| default(aw_server_inventory_host_effective, true)
)
if (aw_server_inventory_host_effective | length) > 0
else '',
true
)
}}
- name: Аккуратно запустить ActivityWatch recovery и launch tasks, если они не в Running
when: aw_windows_force_task_restart | bool
ansible.windows.win_powershell:
@@ -60,7 +79,7 @@
when: aw_windows_api_smoke_check_enabled | bool
delegate_to: localhost
ansible.builtin.uri:
url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host_effective }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
method: GET
status_code: 200
return_content: true
+10 -2
View File
@@ -10,7 +10,8 @@ fi
source "$ENV_FILE"
WEBUI_DIR="${AW_SERVER_WEBUI_DIR:-${AW_WEBUI_DIR:-/opt/activitywatch/webui-ru}}"
REPORT_BASE="${AW_WORKTIME_REPORT_BASE:-http://10.10.10.13:5610}"
SERVER_PUBLIC_HOST="${AW_SERVER_PUBLIC_HOST:-${AW_SERVER_HOST:-$(hostname -f 2>/dev/null || hostname)}}"
REPORT_BASE="${AW_WORKTIME_REPORT_BASE:-http://${SERVER_PUBLIC_HOST}:5610}"
CASE_PORT="${AW_DLP_CASE_PORT:-5602}"
CASE_BASE="${AW_DLP_CASE_PUBLIC_BASE:-}"
PATCH_JS_SRC="/root/bootstrap/aw-ru-patch.js"
@@ -62,12 +63,19 @@ worktime_panel_hash="$(sha1sum "$WORKTIME_PANEL_TARGET" | awk '{print substr($1,
if [[ -z "$CASE_BASE" ]]; then
CASE_BASE="$(python3 - "$REPORT_BASE" "$CASE_PORT" <<'PY'
from urllib.parse import urlsplit, urlunsplit
import os
import socket
import sys
report_base = sys.argv[1]
case_port = sys.argv[2]
parts = urlsplit(report_base)
hostname = parts.hostname or "10.10.10.13"
hostname = (
parts.hostname
or os.environ.get("AW_SERVER_PUBLIC_HOST")
or os.environ.get("AW_SERVER_HOST")
or socket.getfqdn()
)
scheme = parts.scheme or "http"
print(urlunsplit((scheme, f"{hostname}:{case_port}", "", "", "")))
PY
+3 -2
View File
@@ -1,10 +1,11 @@
[Unit]
Description=AW-RUS Hayabusa auto-process dropped packages
After=network-online.target activitywatch-server.service
Wants=network-online.target activitywatch-server.service
After=network-online.target activitywatch-server.service aw-dlp-case-management.service
Wants=network-online.target activitywatch-server.service aw-dlp-case-management.service
[Service]
Type=oneshot
EnvironmentFile=-/etc/activitywatch/aw-server.env
ExecStart=/usr/bin/python3 /usr/local/bin/aw-hayabusa-autoprocess
User=root
Group=root
+10 -1
View File
@@ -13,7 +13,8 @@ AW_SERVER_USER=activitywatch
AW_SERVER_GROUP=activitywatch
# Worktime API Configuration
AW_WORKTIME_REPORT_BASE=http://10.10.10.13:5610
AW_SERVER_PUBLIC_HOST=aw-server
AW_WORKTIME_REPORT_BASE=http://aw-server:5610
AW_WORKTIME_TZ=Europe/Moscow
AW_SERVER_URL=http://127.0.0.1:5600
@@ -42,5 +43,13 @@ AW_MONITORED_WINDOWS_HOSTNAME=SHARKON2025
AW_RUS_HEALTH_STATE_DIR=/var/lib/activitywatch/health
AW_RUS_HEALTH_VALIDATION_DIR=/var/lib/activitywatch/health/windows-validation
# Hayabusa auto-case / alerting
AW_HAYABUSA_AUTO_CASE_ENABLED=true
AW_HAYABUSA_AUTO_CASE_MIN_SEVERITY=medium
AW_HAYABUSA_TELEGRAM_ENABLED=false
AW_HAYABUSA_TELEGRAM_MIN_SEVERITY=high
AW_HAYABUSA_TELEGRAM_BOT_TOKEN=
AW_HAYABUSA_TELEGRAM_CHAT_IDS=
# Integration Test Configuration
AW_INTEGRATION_TEST_ENABLED=false
+21 -4
View File
@@ -12,6 +12,7 @@ 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')
CASE_ALERT = pathlib.Path('/usr/local/bin/aw-hayabusa-case-alert')
def run(cmd):
@@ -19,6 +20,11 @@ def run(cmd):
subprocess.run(cmd, check=True)
def run_capture(cmd):
print('RUN', ' '.join(str(x) for x in cmd), flush=True)
return subprocess.run(cmd, check=False, text=True, capture_output=True)
def read_latest_intake():
return json.loads(pathlib.Path('/opt/hayabusa/state/latest-intake.json').read_text(encoding='utf-8'))
@@ -83,11 +89,22 @@ def process_one(zip_path: pathlib.Path):
run([str(WRAPPER), 'process-inbox', '--mode', mode, '--limit', '1'])
latest = read_latest_intake()
report_dir = pathlib.Path(latest['report_dir'])
case_alert = None
if CASE_ALERT.is_file():
alert_cmd = [str(CASE_ALERT), '--mode', mode, '--link-source', sidecars['link_source']]
if sidecars['case_id'] is not None:
alert_cmd += ['--case-id', str(sidecars['case_id'])]
result = run_capture(alert_cmd)
case_alert = {
'returncode': result.returncode,
'stdout': result.stdout.strip(),
'stderr': result.stderr.strip(),
}
archive_sidecars(report_dir, sidecars)
archive_drop_package(report_dir, zip_path)
if sidecars['case_id'] is not None:
if sidecars['case_id'] is not None and not CASE_ALERT.is_file():
run([str(LINKER), '--case-id', str(sidecars['case_id']), '--mode', mode, '--link-source', sidecars['link_source']])
return latest
return {'latest_intake': latest, 'case_alert': case_alert}
def main():
@@ -110,8 +127,8 @@ def main():
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))
result = process_one(zip_path)
print(json.dumps({'processed': str(zip_path), **result}, ensure_ascii=False, indent=2))
return 0
@@ -0,0 +1,334 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import csv
import json
import os
import pathlib
import urllib.error
import urllib.parse
import urllib.request
from collections import Counter
from datetime import datetime, timezone
from typing import Any
SEVERITY_ORDER = {"low": 1, "medium": 2, "high": 3, "critical": 4}
LEVEL_NORMALIZATION = {
"informational": "info",
"info": "info",
"low": "low",
"med": "med",
"medium": "med",
"high": "high",
"crit": "crit",
"critical": "crit",
}
LEVEL_WEIGHTS = {
"info": 1,
"low": 4,
"med": 12,
"high": 40,
"crit": 100,
}
def env_bool(name: str, default: bool) -> bool:
value = os.environ.get(name)
if value is None:
return default
return value.strip().lower() in {"1", "true", "yes", "on"}
def normalize_level(level: str | None) -> str:
if not level:
return "info"
return LEVEL_NORMALIZATION.get(level.strip().lower(), level.strip().lower())
def severity_meets(actual: str, threshold: str) -> bool:
return SEVERITY_ORDER.get(actual, 0) >= SEVERITY_ORDER.get(threshold, 0)
def build_hayabusa_payload(intake: dict[str, Any], mode: str, link_source: str) -> dict[str, Any]:
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: str, payload: dict[str, Any]) -> dict[str, Any]:
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:
body = resp.read().decode("utf-8")
return json.loads(body) if body else {}
def patch_json(url: str, payload: dict[str, Any]) -> dict[str, Any]:
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(url, data=data, method="PATCH", headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req) as resp:
body = resp.read().decode("utf-8")
return json.loads(body) if body else {}
def get_json(url: str) -> dict[str, Any]:
with urllib.request.urlopen(url) as resp:
return json.loads(resp.read().decode("utf-8"))
def parse_timestamp(value: str | None) -> datetime | None:
if not value:
return None
try:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
return None
def read_csv_rows(path: pathlib.Path) -> int:
if not path.is_file():
return 0
with path.open("r", encoding="utf-8-sig", newline="") as fh:
reader = csv.reader(fh)
rows = list(reader)
if not rows:
return 0
return max(0, len(rows) - 1)
def analyze_report(report_dir: pathlib.Path) -> dict[str, Any]:
timeline_path = report_dir / "timeline.jsonl"
level_counts: Counter[str] = Counter()
title_counts: Counter[str] = Counter()
first_ts: datetime | None = None
last_ts: datetime | None = None
total_events = 0
if timeline_path.is_file():
with timeline_path.open("r", encoding="utf-8") as fh:
for raw_line in fh:
line = raw_line.strip()
if not line:
continue
try:
event = json.loads(line)
except json.JSONDecodeError:
continue
total_events += 1
level = normalize_level(str(event.get("Level", "")))
level_counts[level] += 1
title = str(event.get("RuleTitle") or "").strip() or "Unknown rule"
title_counts[title] += 1
ts = parse_timestamp(event.get("Timestamp"))
if ts is not None:
first_ts = ts if first_ts is None or ts < first_ts else first_ts
last_ts = ts if last_ts is None or ts > last_ts else last_ts
failed_logons = read_csv_rows(report_dir / "logon-summary-failed.csv")
successful_logons = read_csv_rows(report_dir / "logon-summary-successful.csv")
suspicious_pwsh = sum(
count
for title, count in title_counts.items()
if "pwsh" in title.lower() or "powershell" in title.lower() or "obfuscation" in title.lower()
)
credential_events = sum(count for title, count in title_counts.items() if "credential" in title.lower())
timestomp_events = sum(count for title, count in title_counts.items() if "timestomp" in title.lower())
logon_failure_events = sum(count for title, count in title_counts.items() if "logon failure" in title.lower())
score = (
sum(LEVEL_WEIGHTS.get(level, 0) * count for level, count in level_counts.items())
+ min(failed_logons, 200) * 2
+ suspicious_pwsh * 6
+ credential_events * 8
+ timestomp_events * 12
+ logon_failure_events * 2
)
crit_count = level_counts.get("crit", 0)
high_count = level_counts.get("high", 0)
med_count = level_counts.get("med", 0)
if crit_count >= 1 or score >= 240 or (high_count >= 4 and suspicious_pwsh >= 4):
severity = "critical"
elif high_count >= 1 or score >= 120 or suspicious_pwsh >= 8 or credential_events >= 5:
severity = "high"
elif med_count >= 1 or score >= 40 or failed_logons >= 10:
severity = "medium"
else:
severity = "low"
return {
"severity": severity,
"score": score,
"events_total": total_events,
"level_counts": dict(level_counts),
"top_rules": [{"title": title, "count": count} for title, count in title_counts.most_common(5)],
"first_timestamp": first_ts.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") if first_ts else None,
"last_timestamp": last_ts.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") if last_ts else None,
"failed_logon_rows": failed_logons,
"successful_logon_rows": successful_logons,
"suspicious_pwsh": suspicious_pwsh,
"credential_events": credential_events,
"timestomp_events": timestomp_events,
"logon_failure_events": logon_failure_events,
}
def build_case_title(host: str, summary: dict[str, Any]) -> str:
top_rule = summary.get("top_rules") or []
suffix = top_rule[0]["title"] if top_rule else "No dominant rule"
return f"Hayabusa {summary['severity'].upper()} · {host} · {suffix}"
def build_case_payload(intake: dict[str, Any], summary: dict[str, Any]) -> dict[str, Any]:
return {
"incident_id": f"hayabusa:{intake['host']}:{intake['intake_id']}",
"host": intake["host"],
"title": build_case_title(intake["host"], summary),
"severity": summary["severity"],
"evidence": {
"hayabusa": {
"intake_id": intake["intake_id"],
"package_path": intake["package_path"],
"sha256": intake["sha256"],
"report_dir": intake["report_dir"],
"summary": summary,
}
},
}
def build_comment(summary: dict[str, Any], intake: dict[str, Any]) -> str:
top = ", ".join(f"{item['title']} ({item['count']})" for item in summary.get("top_rules", [])[:3]) or "n/a"
return (
f"Hayabusa auto-summary\n"
f"Severity: {summary['severity']} (score={summary['score']})\n"
f"Host: {intake['host']}\n"
f"Intake: {intake['intake_id']}\n"
f"Events: {summary['events_total']}, failed_logons={summary['failed_logon_rows']}, "
f"suspicious_pwsh={summary['suspicious_pwsh']}, credential_events={summary['credential_events']}\n"
f"Top rules: {top}\n"
f"Report: {intake['report_dir']}"
)
def send_telegram(bot_token: str, chat_ids: list[str], text: str) -> list[dict[str, Any]]:
results = []
for chat_id in chat_ids:
payload = urllib.parse.urlencode({"chat_id": chat_id, "text": text}).encode("utf-8")
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
req = urllib.request.Request(url, data=payload, method="POST")
try:
with urllib.request.urlopen(req) as resp:
body = json.loads(resp.read().decode("utf-8"))
results.append({"chat_id": chat_id, "ok": True, "response": body})
except Exception as exc: # noqa: BLE001
results.append({"chat_id": chat_id, "ok": False, "error": str(exc)})
return results
def build_telegram_text(case_id: int | None, intake: dict[str, Any], summary: dict[str, Any]) -> str:
top = ", ".join(f"{item['title']} ({item['count']})" for item in summary.get("top_rules", [])[:3]) or "n/a"
case_part = f"\nCase: {case_id}" if case_id is not None else ""
return (
f"Hayabusa {summary['severity'].upper()} alert\n"
f"Host: {intake['host']}{case_part}\n"
f"Intake: {intake['intake_id']}\n"
f"Score: {summary['score']}\n"
f"Events: {summary['events_total']}, failed_logons={summary['failed_logon_rows']}, "
f"suspicious_pwsh={summary['suspicious_pwsh']}\n"
f"Top rules: {top}\n"
f"Report: {intake['report_dir']}"
)
def main() -> int:
p = argparse.ArgumentParser(description="Auto-create/update AW-rus case, compute Hayabusa severity, and send Telegram alerts")
p.add_argument("--case-id", type=int)
p.add_argument("--intake-json", default="/opt/hayabusa/state/latest-intake.json")
p.add_argument("--case-api-base", default=os.environ.get("AW_HAYABUSA_CASE_API_BASE", "http://127.0.0.1:5602"))
p.add_argument("--mode", default="incident")
p.add_argument("--link-source", default="aw-rus-drop-autoprocess")
p.add_argument("--auto-create", action="store_true", default=env_bool("AW_HAYABUSA_AUTO_CASE_ENABLED", True))
p.add_argument("--auto-create-min-severity", default=os.environ.get("AW_HAYABUSA_AUTO_CASE_MIN_SEVERITY", "medium"))
p.add_argument("--telegram-enabled", action="store_true", default=env_bool("AW_HAYABUSA_TELEGRAM_ENABLED", False))
p.add_argument("--telegram-min-severity", default=os.environ.get("AW_HAYABUSA_TELEGRAM_MIN_SEVERITY", "high"))
p.add_argument("--telegram-bot-token", default=os.environ.get("AW_HAYABUSA_TELEGRAM_BOT_TOKEN", ""))
p.add_argument("--telegram-chat-ids", default=os.environ.get("AW_HAYABUSA_TELEGRAM_CHAT_IDS", ""))
args = p.parse_args()
intake_path = pathlib.Path(args.intake_json)
intake = json.loads(intake_path.read_text(encoding="utf-8"))
summary = analyze_report(pathlib.Path(intake["report_dir"]))
case_api_base = args.case_api_base.rstrip("/")
if case_api_base.endswith("/api/0/dlp/cases"):
case_api_base = case_api_base[: -len("/api/0/dlp/cases")]
case_id = args.case_id
created_case = None
case_error = None
linked = False
comment_added = False
try:
if case_id is None and args.auto_create and severity_meets(summary["severity"], args.auto_create_min_severity):
created_case = post_json(f"{case_api_base}/api/0/dlp/cases", build_case_payload(intake, summary))
case_id = int(created_case["id"])
if case_id is not None:
patch_json(
f"{case_api_base}/api/0/dlp/cases/{case_id}",
{"severity": summary["severity"]},
)
post_json(
f"{case_api_base}/api/0/dlp/cases/{case_id}/forensics/hayabusa",
build_hayabusa_payload(intake, args.mode, args.link_source),
)
linked = True
post_json(
f"{case_api_base}/api/0/dlp/cases/{case_id}/comments",
{"comment": build_comment(summary, intake), "author": "aw-hayabusa-auto"},
)
comment_added = True
except Exception as exc: # noqa: BLE001
case_error = str(exc)
telegram_results: list[dict[str, Any]] = []
if args.telegram_enabled and args.telegram_bot_token and severity_meets(summary["severity"], args.telegram_min_severity):
chat_ids = [item.strip() for item in args.telegram_chat_ids.split(",") if item.strip()]
if chat_ids:
telegram_results = send_telegram(
bot_token=args.telegram_bot_token,
chat_ids=chat_ids,
text=build_telegram_text(case_id, intake, summary),
)
result = {
"summary": summary,
"case_id": case_id,
"case_created": created_case,
"case_linked": linked,
"case_comment_added": comment_added,
"case_error": case_error,
"telegram_results": telegram_results,
}
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0 if case_error is None else 1
if __name__ == "__main__":
raise SystemExit(main())
+107
View File
@@ -0,0 +1,107 @@
# AW-rus Security Analytics Stack v1
## Goal
Build a sufficient internal security analytics stack for the current environment without pretending to be Splunk-class infrastructure.
## Scope
Sources:
- Windows EVTX
- ActivityWatch buckets
- DLP incidents
- file operations
- outbound email
- session/logon markers
Core outcomes:
- ingest
- normalize
- detect
- correlate
- case
- notify
- investigate
## v1 Architecture
### Windows side
- collectors and DLP scripts write to `deployment-config.json`
- `export-evtx-for-hayabusa.ps1` exports bounded EVTX packages
- `export-upload-hayabusa-to-aw-server.ps1` uploads:
- `zip`
- `.meta.json`
- optional `.caseid`
- `ActivityWatch Hayabusa Upload` scheduled task runs every 6 hours
### Server side
- `aw-hayabusa-drop.path` watches `/opt/activitywatch/aw-rus-ops/drop`
- `aw-hayabusa-drop.service` runs `aw-hayabusa-autoprocess`
- `aw-hayabusa` performs:
- accept
- process-inbox
- report generation
- `aw-hayabusa-case-alert` performs:
- severity scoring from `timeline.jsonl`
- optional auto-case creation
- bounded Hayabusa linkage
- summary comment
- Telegram alerting
### Case layer
- DLP case API remains the source of truth for incident lifecycle
- Hayabusa writes bounded metadata into `forensics.hayabusa`
- auto-created cases use:
- `incident_id = hayabusa:<host>:<intake_id>`
## Severity Model v1
Inputs:
- Hayabusa `Level`
- top `RuleTitle`
- failed logon count
- suspicious PowerShell count
- credential-related detections
- timestomp detections
Outputs:
- `low`
- `medium`
- `high`
- `critical`
Rules:
- `critical` for `crit` alerts, very high score, or strong compound signals
- `high` for at least one high alert or elevated score
- `medium` for med alerts, notable failed logons, or moderate score
- `low` otherwise
## Automation Policy v1
- EVTX upload every 6 hours
- lookback window: 6 hours
- auto-case enabled from `medium`
- Telegram enabled from `high`
- human operator only for final triage/escalation
## Non-goals
Not trying to implement:
- distributed search cluster
- Splunk-style indexers/search heads
- full SIEM content ecosystem
- petabyte-scale retention design
## Definition of Done
The stack is sufficient when it can, without a dedicated analyst:
- collect relevant data
- process EVTX on schedule
- score severity
- create/update a case
- send an alert
- preserve investigation artifacts
- let a human understand what happened in a few minutes
+56
View File
@@ -502,6 +502,7 @@ function New-ActivityWatchDeploymentConfig {
[Parameter(Mandatory = $true)]
[string]$SessionCollectorScript,
[string]$EvtxExportScript,
[string]$HayabusaUploadScript,
[string]$EmailCollectorScript,
[Parameter(Mandatory = $true)]
[string]$RulesPath,
@@ -541,6 +542,11 @@ function New-ActivityWatchDeploymentConfig {
[Parameter(Mandatory = $true)]
[pscustomobject[]]$UserTasks,
[string]$PackageVersion = 'v0.13.2',
[bool]$HayabusaAutoUploadEnabled = $true,
[int]$HayabusaAutoUploadIntervalHours = 6,
[int]$HayabusaAutoUploadHoursBack = 6,
[string]$HayabusaAutoUploadMode = 'incident',
[string]$HayabusaAutoUploadTaskName = 'ActivityWatch Hayabusa Upload',
[switch]$IntegrationTestEnabled
)
@@ -581,6 +587,7 @@ function New-ActivityWatchDeploymentConfig {
fileCollectorScript = $FileCollectorScript
sessionCollectorScript = $SessionCollectorScript
evtxExportScript = $EvtxExportScript
hayabusaUploadScript = $HayabusaUploadScript
rulesPath = $RulesPath
policyPath = $PolicyPath
launchScript = $LaunchScriptPath
@@ -608,6 +615,13 @@ function New-ActivityWatchDeploymentConfig {
evtxExportRoot = $effectiveEvtxExportRoot
retentionDays = $EvtxRetentionDays
evtxChannels = @($effectiveEvtxChannels)
hayabusaAutomation = [pscustomobject]@{
enabled = [bool]$HayabusaAutoUploadEnabled
intervalHours = $HayabusaAutoUploadIntervalHours
hoursBack = $HayabusaAutoUploadHoursBack
mode = $HayabusaAutoUploadMode
taskName = $HayabusaAutoUploadTaskName
}
}
sessionEvents = [pscustomobject]@{
logonEnabled = $LogonMarkerEnabled
@@ -1539,6 +1553,48 @@ function Register-ActivityWatchRecoveryTask {
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null
}
function Register-ActivityWatchHayabusaAutoUploadTask {
param(
[Parameter(Mandatory = $true)]
[string]$ConfigPath
)
$config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath
$forensics = $config.forensics
if ($null -eq $forensics -or $forensics.PSObject.Properties.Name -notcontains 'hayabusaAutomation') {
return
}
$automation = $forensics.hayabusaAutomation
$taskName = if ($automation.PSObject.Properties.Name -contains 'taskName' -and -not [string]::IsNullOrWhiteSpace([string]$automation.taskName)) {
[string]$automation.taskName
} else {
'ActivityWatch Hayabusa Upload'
}
if (-not [bool]$automation.enabled) {
Remove-ActivityWatchScheduledTask -TaskName $taskName
return
}
$uploadScript = if ($config.paths.PSObject.Properties.Name -contains 'hayabusaUploadScript') { [string]$config.paths.hayabusaUploadScript } else { Join-Path $config.paths.stateRoot 'export-upload-hayabusa-to-aw-server.ps1' }
if (-not (Test-Path -LiteralPath $uploadScript)) {
throw "Не найден скрипт Hayabusa upload: $uploadScript"
}
$intervalHours = [Math]::Max(1, [int]$automation.intervalHours)
$hoursBack = [Math]::Max(1, [int]$automation.hoursBack)
$mode = if ($automation.PSObject.Properties.Name -contains 'mode' -and -not [string]::IsNullOrWhiteSpace([string]$automation.mode)) { [string]$automation.mode } else { 'incident' }
$powerShellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
$taskCommand = "`"$powerShellExe`" -NoProfile -ExecutionPolicy Bypass -File `"$uploadScript`" -ConfigPath `"$ConfigPath`" -HoursBack $hoursBack -Mode `"$mode`""
Remove-ActivityWatchScheduledTask -TaskName $taskName
& schtasks.exe /Create /TN $taskName /TR $taskCommand /SC HOURLY /MO $intervalHours /ST 00:00 /RU SYSTEM /RL HIGHEST /F | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "Не удалось создать scheduled task $taskName через schtasks.exe"
}
}
function Set-ActivityWatchAcl {
param(
[Parameter(Mandatory = $true)]
+12
View File
@@ -39,6 +39,11 @@ param(
[string]$PolicyEngineScheme = 'http',
[int]$PolicyRefreshSeconds = 300,
[string]$PolicyCachePath,
[bool]$HayabusaAutoUploadEnabled = $true,
[int]$HayabusaAutoUploadIntervalHours = 6,
[int]$HayabusaAutoUploadHoursBack = 6,
[string]$HayabusaAutoUploadMode = 'incident',
[string]$HayabusaAutoUploadTaskName = 'ActivityWatch Hayabusa Upload',
[switch]$IntegrationTestEnabled
)
@@ -109,6 +114,7 @@ $config = New-ActivityWatchDeploymentConfig `
-FileCollectorScript $assetResult.FileCollectorScript `
-SessionCollectorScript $assetResult.SessionCollectorScript `
-EvtxExportScript $assetResult.EvtxExportScript `
-HayabusaUploadScript $assetResult.HayabusaUploadScript `
-RulesPath $assetResult.ActiveRules `
-PolicyPath $assetResult.ActivePolicy `
-PollSeconds $PollSeconds `
@@ -133,6 +139,11 @@ $config = New-ActivityWatchDeploymentConfig `
-PolicyEngineScheme $PolicyEngineScheme `
-PolicyRefreshSeconds $PolicyRefreshSeconds `
-PolicyCachePath $PolicyCachePath `
-HayabusaAutoUploadEnabled $HayabusaAutoUploadEnabled `
-HayabusaAutoUploadIntervalHours $HayabusaAutoUploadIntervalHours `
-HayabusaAutoUploadHoursBack $HayabusaAutoUploadHoursBack `
-HayabusaAutoUploadMode $HayabusaAutoUploadMode `
-HayabusaAutoUploadTaskName $HayabusaAutoUploadTaskName `
-LaunchScriptPath $launchScriptPath `
-RecoveryScriptPath $recoveryScriptPath `
-UserTasks $taskDefinitions `
@@ -144,6 +155,7 @@ Remove-LegacyActivityWatchEntries
Set-ActivityWatchAcl -InstallRoot $InstallRoot -StateRoot $StateRoot -LogsRoot $logsRoot
Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $launchScriptPath -ConfigPath $configPath
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
Register-ActivityWatchHayabusaAutoUploadTask -ConfigPath $configPath
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
Write-Host 'ActivityWatch развёрнут для пользователей:'
+10
View File
@@ -40,6 +40,11 @@ param(
[int]$PolicyRefreshSeconds = 300,
[string]$PolicyCachePath,
[string]$ReportPath,
[bool]$HayabusaAutoUploadEnabled = $true,
[int]$HayabusaAutoUploadIntervalHours = 6,
[int]$HayabusaAutoUploadHoursBack = 6,
[string]$HayabusaAutoUploadMode = 'incident',
[string]$HayabusaAutoUploadTaskName = 'ActivityWatch Hayabusa Upload',
[switch]$SkipHardening,
[switch]$ValidateAfterDeploy,
[switch]$IntegrationTestEnabled
@@ -98,6 +103,11 @@ if (-not (Test-Path -LiteralPath $deployScript)) {
-PolicyEngineScheme $PolicyEngineScheme `
-PolicyRefreshSeconds $PolicyRefreshSeconds `
-PolicyCachePath $PolicyCachePath `
-HayabusaAutoUploadEnabled $HayabusaAutoUploadEnabled `
-HayabusaAutoUploadIntervalHours $HayabusaAutoUploadIntervalHours `
-HayabusaAutoUploadHoursBack $HayabusaAutoUploadHoursBack `
-HayabusaAutoUploadMode $HayabusaAutoUploadMode `
-HayabusaAutoUploadTaskName $HayabusaAutoUploadTaskName `
-IntegrationTestEnabled:$IntegrationTestEnabled
if (-not $SkipHardening) {
+8 -2
View File
@@ -4,6 +4,7 @@ param(
[string]$OutputRoot,
[int]$RetentionDays,
[string[]]$Channels,
[Nullable[int]]$HoursBack,
[int]$DaysBack = 3,
[switch]$NoZip
)
@@ -78,8 +79,12 @@ $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]]]"
$lookbackMs = if ($null -ne $HoursBack) {
[int64]$HoursBack * 60 * 60 * 1000
} else {
[int64]$DaysBack * 24 * 60 * 60 * 1000
}
$query = "*[System[TimeCreated[timediff(@SystemTime) <= $lookbackMs]]]"
$results = @()
foreach ($channel in @($effectiveChannels | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })) {
@@ -117,6 +122,7 @@ $manifest = [ordered]@{
batchRoot = $batchRoot
zipPath = if ($NoZip) { $null } else { $zipPath }
daysBack = $DaysBack
hoursBack = if ($null -ne $HoursBack) { [int]$HoursBack } else { $null }
retentionDays = $effectiveRetentionDays
channels = @($effectiveChannels)
exports = @($results)
@@ -1,8 +1,9 @@
[CmdletBinding()]
param(
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
[Nullable[int]]$HoursBack,
[int]$DaysBack = 1,
[string]$ServerHost = '10.10.10.13',
[string]$ServerHost = '',
[string]$ServerUser = 'awops',
[string]$RemoteDropDir = '/opt/activitywatch/aw-rus-ops/drop',
[string]$RemoteKeyPath = 'C:\ProgramData\AWatch-rus\ssh\awops_ed25519',
@@ -41,7 +42,23 @@ if (-not (Test-Path -LiteralPath $RemoteKeyPath)) {
throw "SSH private key not found: $RemoteKeyPath"
}
$export = & $exportScript -ConfigPath $ConfigPath -DaysBack $DaysBack
$config = Get-Content -Raw -LiteralPath $ConfigPath | ConvertFrom-Json
if ([string]::IsNullOrWhiteSpace($ServerHost)) {
$ServerHost = [string]$config.server.host
}
if ([string]::IsNullOrWhiteSpace($ServerHost)) {
throw "ServerHost is empty and deployment-config has no server.host: $ConfigPath"
}
$exportArgs = @{
ConfigPath = $ConfigPath
}
if ($null -ne $HoursBack) {
$exportArgs.HoursBack = [int]$HoursBack
} else {
$exportArgs.DaysBack = $DaysBack
}
$export = & $exportScript @exportArgs
$zipPath = [string]$export.zipPath
$hostName = [string]$export.hostname
if ([string]::IsNullOrWhiteSpace($zipPath) -or -not (Test-Path -LiteralPath $zipPath)) {
@@ -52,9 +69,23 @@ $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')
$metaPath = Join-Path ([System.IO.Path]::GetDirectoryName($zipPath)) ($baseName + '.meta.json')
$effectiveKeyPath = New-TemporarySshKeyCopy -SourceKeyPath $RemoteKeyPath
try {
$meta = [ordered]@{
host = $hostName
mode = $Mode
link_source = 'windows-drop-upload'
}
if ($null -ne $CaseId) {
$meta.case_id = [int]$CaseId
}
$meta | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $metaPath -Encoding UTF8
& scp.exe -i $effectiveKeyPath -o StrictHostKeyChecking=no -o UserKnownHostsFile=NUL $metaPath $remoteTarget
if ($LASTEXITCODE -ne 0) {
throw "scp meta upload failed with rc=$LASTEXITCODE"
}
if ($null -ne $CaseId) {
Set-Content -LiteralPath $caseIdPath -Value ([string]$CaseId) -Encoding ASCII
& scp.exe -i $effectiveKeyPath -o StrictHostKeyChecking=no -o UserKnownHostsFile=NUL $caseIdPath $remoteTarget
@@ -69,12 +100,14 @@ try {
}
finally {
Remove-Item -LiteralPath $effectiveKeyPath -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $metaPath -Force -ErrorAction SilentlyContinue
}
$result = [ordered]@{
exportedZip = $zipPath
uploadedTo = "$RemoteDropDir/$zipName"
caseIdSidecar = if ($null -ne $CaseId) { "$RemoteDropDir/$baseName.caseid" } else { $null }
metaSidecar = "$RemoteDropDir/$baseName.meta.json"
hostname = $hostName
mode = $Mode
runRemote = [bool]$RunRemote
+14
View File
@@ -70,6 +70,7 @@ $effectiveEndpointCollector = if ($existingConfig -and $existingConfig.paths.PSO
$effectiveFileCollector = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]$existingConfig.paths.fileCollectorScript } else { Join-Path $effectiveStateRoot 'file-operations-collector.ps1' }
$effectiveSessionCollector = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$existingConfig.paths.sessionCollectorScript } else { Join-Path $effectiveStateRoot 'worktime-session-collector.ps1' }
$effectiveEvtxExportScript = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'evtxExportScript') { [string]$existingConfig.paths.evtxExportScript } else { Join-Path $effectiveStateRoot 'export-evtx-for-hayabusa.ps1' }
$effectiveHayabusaUploadScript = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'hayabusaUploadScript') { [string]$existingConfig.paths.hayabusaUploadScript } else { Join-Path $effectiveStateRoot 'export-upload-hayabusa-to-aw-server.ps1' }
$effectiveRules = Join-Path $effectiveStateRoot 'web-category-rules.json'
$effectivePolicy = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$existingConfig.paths.policyPath } else { Join-Path $effectiveStateRoot 'dlp-policy.json' }
$effectivePolicyClientScript = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'policyClientScript') { [string]$existingConfig.paths.policyClientScript } else { Join-Path $effectiveStateRoot 'dlp-policy-client.ps1' }
@@ -100,6 +101,11 @@ $effectivePolicyEnginePort = if ($PSBoundParameters.ContainsKey('PolicyEnginePor
$effectivePolicyEngineScheme = if ($PSBoundParameters.ContainsKey('PolicyEngineScheme') -and $PolicyEngineScheme) { [string]$PolicyEngineScheme } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'policyEngine' -and $existingConfig.policyEngine.PSObject.Properties.Name -contains 'scheme') { [string]$existingConfig.policyEngine.scheme } else { 'http' }
$effectivePolicyRefreshSeconds = if ($PSBoundParameters.ContainsKey('PolicyRefreshSeconds')) { [int]$PolicyRefreshSeconds } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'policyEngine' -and $existingConfig.policyEngine.PSObject.Properties.Name -contains 'refreshSeconds') { [int]$existingConfig.policyEngine.refreshSeconds } else { 300 }
$effectivePolicyCachePath = if ($PSBoundParameters.ContainsKey('PolicyCachePath') -and $PolicyCachePath) { [string]$PolicyCachePath } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'policyEngine' -and $existingConfig.policyEngine.PSObject.Properties.Name -contains 'cachePath') { [string]$existingConfig.policyEngine.cachePath } else { Join-Path $effectiveStateRoot 'dlp-policy-cache.json' }
$effectiveHayabusaAutoUploadEnabled = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'hayabusaAutomation' -and $existingConfig.forensics.hayabusaAutomation.PSObject.Properties.Name -contains 'enabled') { [bool]$existingConfig.forensics.hayabusaAutomation.enabled } else { $true }
$effectiveHayabusaAutoUploadIntervalHours = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'hayabusaAutomation' -and $existingConfig.forensics.hayabusaAutomation.PSObject.Properties.Name -contains 'intervalHours') { [int]$existingConfig.forensics.hayabusaAutomation.intervalHours } else { 6 }
$effectiveHayabusaAutoUploadHoursBack = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'hayabusaAutomation' -and $existingConfig.forensics.hayabusaAutomation.PSObject.Properties.Name -contains 'hoursBack') { [int]$existingConfig.forensics.hayabusaAutomation.hoursBack } else { 6 }
$effectiveHayabusaAutoUploadMode = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'hayabusaAutomation' -and $existingConfig.forensics.hayabusaAutomation.PSObject.Properties.Name -contains 'mode') { [string]$existingConfig.forensics.hayabusaAutomation.mode } else { 'incident' }
$effectiveHayabusaAutoUploadTaskName = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'hayabusaAutomation' -and $existingConfig.forensics.hayabusaAutomation.PSObject.Properties.Name -contains 'taskName') { [string]$existingConfig.forensics.hayabusaAutomation.taskName } else { 'ActivityWatch Hayabusa Upload' }
$effectiveUsers = if ($Users -or $UserListPath) {
Normalize-ActivityWatchUsers -Users $Users -UserListPath $UserListPath -Domain $Domain
@@ -131,6 +137,7 @@ $assetResult = Copy-ActivityWatchCollectorAssets `
-FileCollectorScriptSource (Join-Path $PSScriptRoot 'file-operations-collector.ps1') `
-SessionCollectorScriptSource (Join-Path $PSScriptRoot 'worktime-session-collector.ps1') `
-EvtxExportScriptSource (Join-Path $PSScriptRoot 'export-evtx-for-hayabusa.ps1') `
-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') `
-StateRoot $effectiveStateRoot `
@@ -155,6 +162,7 @@ $config = New-ActivityWatchDeploymentConfig `
-FileCollectorScript $effectiveFileCollector `
-SessionCollectorScript $effectiveSessionCollector `
-EvtxExportScript $effectiveEvtxExportScript `
-HayabusaUploadScript $effectiveHayabusaUploadScript `
-RulesPath $effectiveRules `
-PolicyPath $effectivePolicy `
-PollSeconds $effectivePollSeconds `
@@ -179,6 +187,11 @@ $config = New-ActivityWatchDeploymentConfig `
-PolicyEngineScheme $effectivePolicyEngineScheme `
-PolicyRefreshSeconds $effectivePolicyRefreshSeconds `
-PolicyCachePath $effectivePolicyCachePath `
-HayabusaAutoUploadEnabled $effectiveHayabusaAutoUploadEnabled `
-HayabusaAutoUploadIntervalHours $effectiveHayabusaAutoUploadIntervalHours `
-HayabusaAutoUploadHoursBack $effectiveHayabusaAutoUploadHoursBack `
-HayabusaAutoUploadMode $effectiveHayabusaAutoUploadMode `
-HayabusaAutoUploadTaskName $effectiveHayabusaAutoUploadTaskName `
-LaunchScriptPath $effectiveLaunchScript `
-RecoveryScriptPath $effectiveRecoveryScript `
-UserTasks $taskDefinitions `
@@ -189,6 +202,7 @@ Remove-LegacyActivityWatchEntries
Set-ActivityWatchAcl -InstallRoot $effectiveInstallRoot -StateRoot $effectiveStateRoot -LogsRoot $effectiveLogsRoot
Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $effectiveLaunchScript -ConfigPath $effectiveConfigPath
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath
Register-ActivityWatchHayabusaAutoUploadTask -ConfigPath $effectiveConfigPath
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
Write-Host 'Укрепление и восстановление ActivityWatch завершены.'
@@ -2,9 +2,9 @@
#define MyAppVersion "1.0.0"
#define MyAppPublisher "AWatch-rus"
#define AwDefaultServerHost "10.10.10.13"
#define AwDefaultServerHost "aw-server"
#define AwDefaultServerPort "5600"
#define AwDefaultWorktimeReportBase "http://10.10.10.13:5610"
#define AwDefaultWorktimeReportBase "http://aw-server:5610"
#define AwDefaultWorktimeHost "SHARKON2025"
#define AwDefaultUsers "user1,user2,user3,user4,user5"
#define AwDefaultInstallRoot "C:\\Program Files\\AWatch-rus\\bin"
+1 -1
View File
@@ -29,7 +29,7 @@ The resulting installer `AWatch-rus-InstallKit.exe` is written to the same direc
The installer wizard asks only for:
- `ServerHost` / `ServerPort` (defaults to `10.10.10.13:5600`)
- `ServerHost` / `ServerPort` (defaults to `aw-server:5600`)
All other values are taken from defaults embedded in installer scripts.
+12
View File
@@ -38,6 +38,8 @@ $fileOpsExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -
function Get-LoggedOnUsers {
$users = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
$activeStates = @('Active', 'Активно')
$inactiveStates = @('Disc', 'Disconnected', 'Idle', 'Listen', 'Диск', 'Откл', 'Отключен')
try {
$lines = & quser.exe 2>$null
foreach ($line in @($lines)) {
@@ -50,6 +52,16 @@ function Get-LoggedOnUsers {
if ($parts.Count -lt 1) { continue }
$user = [string]$parts[0]
if ([string]::IsNullOrWhiteSpace($user)) { continue }
$state = $null
foreach ($part in @($parts | Select-Object -Skip 1)) {
$token = [string]$part
if ([string]::IsNullOrWhiteSpace($token)) { continue }
if ($activeStates -contains $token -or $inactiveStates -contains $token) {
$state = $token
break
}
}
if ($null -ne $state -and $activeStates -notcontains $state) { continue }
[void]$users.Add($user)
[void]$users.Add(('{0}\{1}' -f $env:COMPUTERNAME, $user))
if (-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) {