feat(hayabusa): add auto-case scoring and 6h automation
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 развёрнут для пользователей:'
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user