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_MONITORED_WINDOWS_HOSTNAME={{ aw_monitored_windows_hostname }}
|
||||||
AW_RUS_HEALTH_STATE_DIR={{ aw_rus_health_state_dir }}
|
AW_RUS_HEALTH_STATE_DIR={{ aw_rus_health_state_dir }}
|
||||||
AW_RUS_HEALTH_VALIDATION_DIR={{ aw_rus_health_validation_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
|
- name: Создать каталог DLP policy engine
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
@@ -1622,6 +1628,22 @@
|
|||||||
group: root
|
group: root
|
||||||
mode: "0755"
|
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
|
- name: Установить systemd unit aw-hayabusa-drop.service
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
src: "{{ aw_repo_root }}/aw-server/aw-hayabusa-drop.service"
|
src: "{{ aw_repo_root }}/aw-server/aw-hayabusa-drop.service"
|
||||||
@@ -1674,6 +1696,7 @@
|
|||||||
cmd: /usr/local/bin/aw-rus-healthd.py --json
|
cmd: /usr/local/bin/aw-rus-healthd.py --json
|
||||||
register: aw_post_deploy_health
|
register: aw_post_deploy_health
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
- name: Показать результат aw-rus-healthd
|
- name: Показать результат aw-rus-healthd
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
|
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
|
||||||
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
|
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
|
||||||
aw_windows_server_scheme: "http"
|
aw_windows_server_scheme: "http"
|
||||||
aw_windows_server_host: "10.10.10.13"
|
|
||||||
aw_windows_server_port: 5600
|
aw_windows_server_port: 5600
|
||||||
aw_windows_package_version: "v0.13.2"
|
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_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_mode: "server"
|
||||||
aw_windows_policy_refresh_seconds: 300
|
aw_windows_policy_refresh_seconds: 300
|
||||||
aw_windows_policy_engine_enabled: true
|
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_port: 5601
|
||||||
aw_windows_policy_engine_scheme: "http"
|
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_afk_enabled_default: true
|
||||||
aw_windows_window_enabled_default: true
|
aw_windows_window_enabled_default: true
|
||||||
aw_windows_file_ops_enabled: 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"
|
aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json"
|
||||||
|
|
||||||
tasks:
|
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: Проверить обязательные переменные
|
- name: Проверить обязательные переменные
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- aw_windows_server_host is defined
|
- aw_windows_server_host_effective | length > 0
|
||||||
- aw_windows_server_port is defined
|
- aw_windows_server_port is defined
|
||||||
- aw_windows_server_scheme is defined
|
- aw_windows_server_scheme is defined
|
||||||
- aw_windows_domain is defined
|
- aw_windows_domain is defined
|
||||||
@@ -160,7 +191,7 @@
|
|||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$params = @{
|
$params = @{
|
||||||
ServerScheme = "{{ aw_windows_server_scheme }}"
|
ServerScheme = "{{ aw_windows_server_scheme }}"
|
||||||
ServerHost = "{{ aw_windows_server_host }}"
|
ServerHost = "{{ aw_windows_server_host_effective }}"
|
||||||
ServerPort = {{ aw_windows_server_port }}
|
ServerPort = {{ aw_windows_server_port }}
|
||||||
Version = "{{ aw_windows_package_version }}"
|
Version = "{{ aw_windows_package_version }}"
|
||||||
Domain = "{{ aw_windows_domain }}"
|
Domain = "{{ aw_windows_domain }}"
|
||||||
@@ -179,10 +210,15 @@
|
|||||||
LogonMarkerEnabled = {{ '$true' if (aw_windows_logon_marker_enabled | bool) else '$false' }}
|
LogonMarkerEnabled = {{ '$true' if (aw_windows_logon_marker_enabled | bool) else '$false' }}
|
||||||
PolicyMode = "{{ aw_windows_policy_mode }}"
|
PolicyMode = "{{ aw_windows_policy_mode }}"
|
||||||
PolicyEngineEnabled = {{ '$true' if (aw_windows_policy_engine_enabled | bool) else '$false' }}
|
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 }}
|
PolicyEnginePort = {{ aw_windows_policy_engine_port }}
|
||||||
PolicyEngineScheme = "{{ aw_windows_policy_engine_scheme }}"
|
PolicyEngineScheme = "{{ aw_windows_policy_engine_scheme }}"
|
||||||
PolicyRefreshSeconds = {{ aw_windows_policy_refresh_seconds }}
|
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 }}"
|
CustomRulesPath = "{{ aw_windows_rules_path }}"
|
||||||
CustomPolicyPath = "{{ aw_windows_policy_path }}"
|
CustomPolicyPath = "{{ aw_windows_policy_path }}"
|
||||||
}
|
}
|
||||||
@@ -342,7 +378,7 @@
|
|||||||
when:
|
when:
|
||||||
- aw_windows_api_smoke_check_enabled | bool
|
- aw_windows_api_smoke_check_enabled | bool
|
||||||
ansible.builtin.uri:
|
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
|
method: GET
|
||||||
status_code: 200
|
status_code: 200
|
||||||
return_content: true
|
return_content: true
|
||||||
@@ -369,7 +405,7 @@
|
|||||||
- aw_windows_api_smoke_check_window_enabled_effective | bool
|
- aw_windows_api_smoke_check_window_enabled_effective | bool
|
||||||
- aw_windows_window_enabled_effective | bool
|
- aw_windows_window_enabled_effective | bool
|
||||||
ansible.builtin.uri:
|
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
|
method: GET
|
||||||
status_code: 200
|
status_code: 200
|
||||||
return_content: true
|
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_log_dir: "/var/log/activitywatch"
|
||||||
aw_server_user: "activitywatch"
|
aw_server_user: "activitywatch"
|
||||||
aw_server_group: "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_timezone: "Europe/Moscow"
|
||||||
aw_worktime_influx_enabled: false
|
aw_worktime_influx_enabled: false
|
||||||
aw_worktime_influx_url: "http://10.10.10.10:8086"
|
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_monitored_windows_hostname: "SHARKON2025"
|
||||||
aw_rus_health_state_dir: "{{ aw_server_data_dir }}/health"
|
aw_rus_health_state_dir: "{{ aw_server_data_dir }}/health"
|
||||||
aw_rus_health_validation_dir: "{{ aw_rus_health_state_dir }}/windows-validation"
|
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 }}"
|
aw_repo_root: "{{ playbook_dir | dirname }}"
|
||||||
|
|
||||||
@@ -41,7 +49,7 @@ aw_apply_worktime_settings: true
|
|||||||
aw_server_cors_origins:
|
aw_server_cors_origins:
|
||||||
- "http://127.0.0.1:5600"
|
- "http://127.0.0.1:5600"
|
||||||
- "http://localhost:5600"
|
- "http://localhost:5600"
|
||||||
- "http://10.10.10.13:5600"
|
- "http://{{ aw_server_public_host }}:5600"
|
||||||
- "http://aw-server:5600"
|
- "http://aw-server:5600"
|
||||||
|
|
||||||
# Опциональные значения периода рабочего времени в Web UI.
|
# Опциональные значения периода рабочего времени в 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_db_path: "/var/lib/activitywatch/aw-server-rust/sqlite.db"
|
||||||
aw_server_user: "activitywatch"
|
aw_server_user: "activitywatch"
|
||||||
aw_server_group: "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_timezone: "Europe/Moscow"
|
||||||
aw_worktime_influx_enabled: false
|
aw_worktime_influx_enabled: false
|
||||||
aw_worktime_influx_url: "http://10.10.10.10:8086"
|
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_monitored_windows_hostname: "SHARKON2025"
|
||||||
aw_rus_health_state_dir: "{{ aw_server_data_dir }}/health"
|
aw_rus_health_state_dir: "{{ aw_server_data_dir }}/health"
|
||||||
aw_rus_health_validation_dir: "{{ aw_rus_health_state_dir }}/windows-validation"
|
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_repo_root: "/mnt/usb_hdd2/Projects/ActivityWatch-Russian"
|
||||||
|
|
||||||
aw_server_cors_origins:
|
aw_server_cors_origins:
|
||||||
- "http://127.0.0.1:5600"
|
- "http://127.0.0.1:5600"
|
||||||
- "http://localhost:5600"
|
- "http://localhost:5600"
|
||||||
- "http://10.10.10.13:5600"
|
- "http://{{ aw_server_public_host }}:5600"
|
||||||
- "http://192.168.100.13:5600"
|
- "http://aw-server:5600"
|
||||||
- "http://snb-live:5600"
|
- "http://snb-live:5600"
|
||||||
|
|
||||||
aw_apply_worktime_settings: true
|
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_repo_root: "{{ playbook_dir | dirname }}"
|
||||||
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
|
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
|
||||||
aw_windows_server_scheme: "http"
|
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_server_port: 5600
|
||||||
|
|
||||||
aw_windows_package_version: "v0.13.2"
|
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_host: "{{ aw_windows_server_host }}"
|
||||||
aw_windows_policy_engine_port: 5601
|
aw_windows_policy_engine_port: 5601
|
||||||
aw_windows_policy_engine_scheme: "http"
|
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_afk_enabled: true
|
||||||
aw_windows_window_enabled: true
|
aw_windows_window_enabled: true
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
|
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
|
||||||
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
|
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
|
||||||
aw_windows_server_scheme: "http"
|
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_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_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_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
|
||||||
aw_windows_package_zip_path: ""
|
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_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_validation_local_dir: "/tmp/aw-rus-validation-{{ lookup('env','USER') | default('ansible', true) }}"
|
||||||
aw_windows_server_scheme: "http"
|
aw_windows_server_scheme: "http"
|
||||||
aw_windows_server_host: "10.10.10.13"
|
|
||||||
aw_windows_server_port: 5600
|
aw_windows_server_port: 5600
|
||||||
aw_windows_fail_on_validation_error: true
|
aw_windows_fail_on_validation_error: true
|
||||||
aw_windows_launch_task_pattern: "ActivityWatch Launch *"
|
aw_windows_launch_task_pattern: "ActivityWatch Launch *"
|
||||||
@@ -21,6 +20,26 @@
|
|||||||
aw_windows_api_smoke_check_wait_seconds: 20
|
aw_windows_api_smoke_check_wait_seconds: 20
|
||||||
|
|
||||||
tasks:
|
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
|
- name: Аккуратно запустить ActivityWatch recovery и launch tasks, если они не в Running
|
||||||
when: aw_windows_force_task_restart | bool
|
when: aw_windows_force_task_restart | bool
|
||||||
ansible.windows.win_powershell:
|
ansible.windows.win_powershell:
|
||||||
@@ -60,7 +79,7 @@
|
|||||||
when: aw_windows_api_smoke_check_enabled | bool
|
when: aw_windows_api_smoke_check_enabled | bool
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
ansible.builtin.uri:
|
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
|
method: GET
|
||||||
status_code: 200
|
status_code: 200
|
||||||
return_content: true
|
return_content: true
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ fi
|
|||||||
source "$ENV_FILE"
|
source "$ENV_FILE"
|
||||||
|
|
||||||
WEBUI_DIR="${AW_SERVER_WEBUI_DIR:-${AW_WEBUI_DIR:-/opt/activitywatch/webui-ru}}"
|
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_PORT="${AW_DLP_CASE_PORT:-5602}"
|
||||||
CASE_BASE="${AW_DLP_CASE_PUBLIC_BASE:-}"
|
CASE_BASE="${AW_DLP_CASE_PUBLIC_BASE:-}"
|
||||||
PATCH_JS_SRC="/root/bootstrap/aw-ru-patch.js"
|
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
|
if [[ -z "$CASE_BASE" ]]; then
|
||||||
CASE_BASE="$(python3 - "$REPORT_BASE" "$CASE_PORT" <<'PY'
|
CASE_BASE="$(python3 - "$REPORT_BASE" "$CASE_PORT" <<'PY'
|
||||||
from urllib.parse import urlsplit, urlunsplit
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
report_base = sys.argv[1]
|
report_base = sys.argv[1]
|
||||||
case_port = sys.argv[2]
|
case_port = sys.argv[2]
|
||||||
parts = urlsplit(report_base)
|
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"
|
scheme = parts.scheme or "http"
|
||||||
print(urlunsplit((scheme, f"{hostname}:{case_port}", "", "", "")))
|
print(urlunsplit((scheme, f"{hostname}:{case_port}", "", "", "")))
|
||||||
PY
|
PY
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=AW-RUS Hayabusa auto-process dropped packages
|
Description=AW-RUS Hayabusa auto-process dropped packages
|
||||||
After=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
|
Wants=network-online.target activitywatch-server.service aw-dlp-case-management.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
|
EnvironmentFile=-/etc/activitywatch/aw-server.env
|
||||||
ExecStart=/usr/bin/python3 /usr/local/bin/aw-hayabusa-autoprocess
|
ExecStart=/usr/bin/python3 /usr/local/bin/aw-hayabusa-autoprocess
|
||||||
User=root
|
User=root
|
||||||
Group=root
|
Group=root
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ AW_SERVER_USER=activitywatch
|
|||||||
AW_SERVER_GROUP=activitywatch
|
AW_SERVER_GROUP=activitywatch
|
||||||
|
|
||||||
# Worktime API Configuration
|
# 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_WORKTIME_TZ=Europe/Moscow
|
||||||
AW_SERVER_URL=http://127.0.0.1:5600
|
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_STATE_DIR=/var/lib/activitywatch/health
|
||||||
AW_RUS_HEALTH_VALIDATION_DIR=/var/lib/activitywatch/health/windows-validation
|
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
|
# Integration Test Configuration
|
||||||
AW_INTEGRATION_TEST_ENABLED=false
|
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')
|
LOCK_PATH = pathlib.Path('/opt/hayabusa/state/aw-hayabusa-autoprocess.lock')
|
||||||
WRAPPER = pathlib.Path('/usr/local/bin/aw-hayabusa')
|
WRAPPER = pathlib.Path('/usr/local/bin/aw-hayabusa')
|
||||||
LINKER = pathlib.Path('/usr/local/bin/aw-hayabusa-link-case')
|
LINKER = pathlib.Path('/usr/local/bin/aw-hayabusa-link-case')
|
||||||
|
CASE_ALERT = pathlib.Path('/usr/local/bin/aw-hayabusa-case-alert')
|
||||||
|
|
||||||
|
|
||||||
def run(cmd):
|
def run(cmd):
|
||||||
@@ -19,6 +20,11 @@ def run(cmd):
|
|||||||
subprocess.run(cmd, check=True)
|
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():
|
def read_latest_intake():
|
||||||
return json.loads(pathlib.Path('/opt/hayabusa/state/latest-intake.json').read_text(encoding='utf-8'))
|
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'])
|
run([str(WRAPPER), 'process-inbox', '--mode', mode, '--limit', '1'])
|
||||||
latest = read_latest_intake()
|
latest = read_latest_intake()
|
||||||
report_dir = pathlib.Path(latest['report_dir'])
|
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_sidecars(report_dir, sidecars)
|
||||||
archive_drop_package(report_dir, zip_path)
|
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']])
|
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():
|
def main():
|
||||||
@@ -110,8 +127,8 @@ def main():
|
|||||||
print('no zip packages in drop dir')
|
print('no zip packages in drop dir')
|
||||||
return 0
|
return 0
|
||||||
for zip_path in zips:
|
for zip_path in zips:
|
||||||
latest = process_one(zip_path)
|
result = process_one(zip_path)
|
||||||
print(json.dumps({'processed': str(zip_path), 'latest_intake': latest}, ensure_ascii=False, indent=2))
|
print(json.dumps({'processed': str(zip_path), **result}, ensure_ascii=False, indent=2))
|
||||||
return 0
|
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)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$SessionCollectorScript,
|
[string]$SessionCollectorScript,
|
||||||
[string]$EvtxExportScript,
|
[string]$EvtxExportScript,
|
||||||
|
[string]$HayabusaUploadScript,
|
||||||
[string]$EmailCollectorScript,
|
[string]$EmailCollectorScript,
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$RulesPath,
|
[string]$RulesPath,
|
||||||
@@ -541,6 +542,11 @@ function New-ActivityWatchDeploymentConfig {
|
|||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[pscustomobject[]]$UserTasks,
|
[pscustomobject[]]$UserTasks,
|
||||||
[string]$PackageVersion = 'v0.13.2',
|
[string]$PackageVersion = 'v0.13.2',
|
||||||
|
[bool]$HayabusaAutoUploadEnabled = $true,
|
||||||
|
[int]$HayabusaAutoUploadIntervalHours = 6,
|
||||||
|
[int]$HayabusaAutoUploadHoursBack = 6,
|
||||||
|
[string]$HayabusaAutoUploadMode = 'incident',
|
||||||
|
[string]$HayabusaAutoUploadTaskName = 'ActivityWatch Hayabusa Upload',
|
||||||
[switch]$IntegrationTestEnabled
|
[switch]$IntegrationTestEnabled
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -581,6 +587,7 @@ function New-ActivityWatchDeploymentConfig {
|
|||||||
fileCollectorScript = $FileCollectorScript
|
fileCollectorScript = $FileCollectorScript
|
||||||
sessionCollectorScript = $SessionCollectorScript
|
sessionCollectorScript = $SessionCollectorScript
|
||||||
evtxExportScript = $EvtxExportScript
|
evtxExportScript = $EvtxExportScript
|
||||||
|
hayabusaUploadScript = $HayabusaUploadScript
|
||||||
rulesPath = $RulesPath
|
rulesPath = $RulesPath
|
||||||
policyPath = $PolicyPath
|
policyPath = $PolicyPath
|
||||||
launchScript = $LaunchScriptPath
|
launchScript = $LaunchScriptPath
|
||||||
@@ -608,6 +615,13 @@ function New-ActivityWatchDeploymentConfig {
|
|||||||
evtxExportRoot = $effectiveEvtxExportRoot
|
evtxExportRoot = $effectiveEvtxExportRoot
|
||||||
retentionDays = $EvtxRetentionDays
|
retentionDays = $EvtxRetentionDays
|
||||||
evtxChannels = @($effectiveEvtxChannels)
|
evtxChannels = @($effectiveEvtxChannels)
|
||||||
|
hayabusaAutomation = [pscustomobject]@{
|
||||||
|
enabled = [bool]$HayabusaAutoUploadEnabled
|
||||||
|
intervalHours = $HayabusaAutoUploadIntervalHours
|
||||||
|
hoursBack = $HayabusaAutoUploadHoursBack
|
||||||
|
mode = $HayabusaAutoUploadMode
|
||||||
|
taskName = $HayabusaAutoUploadTaskName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sessionEvents = [pscustomobject]@{
|
sessionEvents = [pscustomobject]@{
|
||||||
logonEnabled = $LogonMarkerEnabled
|
logonEnabled = $LogonMarkerEnabled
|
||||||
@@ -1539,6 +1553,48 @@ function Register-ActivityWatchRecoveryTask {
|
|||||||
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null
|
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 {
|
function Set-ActivityWatchAcl {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ param(
|
|||||||
[string]$PolicyEngineScheme = 'http',
|
[string]$PolicyEngineScheme = 'http',
|
||||||
[int]$PolicyRefreshSeconds = 300,
|
[int]$PolicyRefreshSeconds = 300,
|
||||||
[string]$PolicyCachePath,
|
[string]$PolicyCachePath,
|
||||||
|
[bool]$HayabusaAutoUploadEnabled = $true,
|
||||||
|
[int]$HayabusaAutoUploadIntervalHours = 6,
|
||||||
|
[int]$HayabusaAutoUploadHoursBack = 6,
|
||||||
|
[string]$HayabusaAutoUploadMode = 'incident',
|
||||||
|
[string]$HayabusaAutoUploadTaskName = 'ActivityWatch Hayabusa Upload',
|
||||||
[switch]$IntegrationTestEnabled
|
[switch]$IntegrationTestEnabled
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -109,6 +114,7 @@ $config = New-ActivityWatchDeploymentConfig `
|
|||||||
-FileCollectorScript $assetResult.FileCollectorScript `
|
-FileCollectorScript $assetResult.FileCollectorScript `
|
||||||
-SessionCollectorScript $assetResult.SessionCollectorScript `
|
-SessionCollectorScript $assetResult.SessionCollectorScript `
|
||||||
-EvtxExportScript $assetResult.EvtxExportScript `
|
-EvtxExportScript $assetResult.EvtxExportScript `
|
||||||
|
-HayabusaUploadScript $assetResult.HayabusaUploadScript `
|
||||||
-RulesPath $assetResult.ActiveRules `
|
-RulesPath $assetResult.ActiveRules `
|
||||||
-PolicyPath $assetResult.ActivePolicy `
|
-PolicyPath $assetResult.ActivePolicy `
|
||||||
-PollSeconds $PollSeconds `
|
-PollSeconds $PollSeconds `
|
||||||
@@ -133,6 +139,11 @@ $config = New-ActivityWatchDeploymentConfig `
|
|||||||
-PolicyEngineScheme $PolicyEngineScheme `
|
-PolicyEngineScheme $PolicyEngineScheme `
|
||||||
-PolicyRefreshSeconds $PolicyRefreshSeconds `
|
-PolicyRefreshSeconds $PolicyRefreshSeconds `
|
||||||
-PolicyCachePath $PolicyCachePath `
|
-PolicyCachePath $PolicyCachePath `
|
||||||
|
-HayabusaAutoUploadEnabled $HayabusaAutoUploadEnabled `
|
||||||
|
-HayabusaAutoUploadIntervalHours $HayabusaAutoUploadIntervalHours `
|
||||||
|
-HayabusaAutoUploadHoursBack $HayabusaAutoUploadHoursBack `
|
||||||
|
-HayabusaAutoUploadMode $HayabusaAutoUploadMode `
|
||||||
|
-HayabusaAutoUploadTaskName $HayabusaAutoUploadTaskName `
|
||||||
-LaunchScriptPath $launchScriptPath `
|
-LaunchScriptPath $launchScriptPath `
|
||||||
-RecoveryScriptPath $recoveryScriptPath `
|
-RecoveryScriptPath $recoveryScriptPath `
|
||||||
-UserTasks $taskDefinitions `
|
-UserTasks $taskDefinitions `
|
||||||
@@ -144,6 +155,7 @@ Remove-LegacyActivityWatchEntries
|
|||||||
Set-ActivityWatchAcl -InstallRoot $InstallRoot -StateRoot $StateRoot -LogsRoot $logsRoot
|
Set-ActivityWatchAcl -InstallRoot $InstallRoot -StateRoot $StateRoot -LogsRoot $logsRoot
|
||||||
Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $launchScriptPath -ConfigPath $configPath
|
Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $launchScriptPath -ConfigPath $configPath
|
||||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
||||||
|
Register-ActivityWatchHayabusaAutoUploadTask -ConfigPath $configPath
|
||||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||||
|
|
||||||
Write-Host 'ActivityWatch развёрнут для пользователей:'
|
Write-Host 'ActivityWatch развёрнут для пользователей:'
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ param(
|
|||||||
[int]$PolicyRefreshSeconds = 300,
|
[int]$PolicyRefreshSeconds = 300,
|
||||||
[string]$PolicyCachePath,
|
[string]$PolicyCachePath,
|
||||||
[string]$ReportPath,
|
[string]$ReportPath,
|
||||||
|
[bool]$HayabusaAutoUploadEnabled = $true,
|
||||||
|
[int]$HayabusaAutoUploadIntervalHours = 6,
|
||||||
|
[int]$HayabusaAutoUploadHoursBack = 6,
|
||||||
|
[string]$HayabusaAutoUploadMode = 'incident',
|
||||||
|
[string]$HayabusaAutoUploadTaskName = 'ActivityWatch Hayabusa Upload',
|
||||||
[switch]$SkipHardening,
|
[switch]$SkipHardening,
|
||||||
[switch]$ValidateAfterDeploy,
|
[switch]$ValidateAfterDeploy,
|
||||||
[switch]$IntegrationTestEnabled
|
[switch]$IntegrationTestEnabled
|
||||||
@@ -98,6 +103,11 @@ if (-not (Test-Path -LiteralPath $deployScript)) {
|
|||||||
-PolicyEngineScheme $PolicyEngineScheme `
|
-PolicyEngineScheme $PolicyEngineScheme `
|
||||||
-PolicyRefreshSeconds $PolicyRefreshSeconds `
|
-PolicyRefreshSeconds $PolicyRefreshSeconds `
|
||||||
-PolicyCachePath $PolicyCachePath `
|
-PolicyCachePath $PolicyCachePath `
|
||||||
|
-HayabusaAutoUploadEnabled $HayabusaAutoUploadEnabled `
|
||||||
|
-HayabusaAutoUploadIntervalHours $HayabusaAutoUploadIntervalHours `
|
||||||
|
-HayabusaAutoUploadHoursBack $HayabusaAutoUploadHoursBack `
|
||||||
|
-HayabusaAutoUploadMode $HayabusaAutoUploadMode `
|
||||||
|
-HayabusaAutoUploadTaskName $HayabusaAutoUploadTaskName `
|
||||||
-IntegrationTestEnabled:$IntegrationTestEnabled
|
-IntegrationTestEnabled:$IntegrationTestEnabled
|
||||||
|
|
||||||
if (-not $SkipHardening) {
|
if (-not $SkipHardening) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ param(
|
|||||||
[string]$OutputRoot,
|
[string]$OutputRoot,
|
||||||
[int]$RetentionDays,
|
[int]$RetentionDays,
|
||||||
[string[]]$Channels,
|
[string[]]$Channels,
|
||||||
|
[Nullable[int]]$HoursBack,
|
||||||
[int]$DaysBack = 3,
|
[int]$DaysBack = 3,
|
||||||
[switch]$NoZip
|
[switch]$NoZip
|
||||||
)
|
)
|
||||||
@@ -78,8 +79,12 @@ $zipPath = Join-Path $effectiveOutputRoot "$hostName-$timestamp.zip"
|
|||||||
New-Directory -Path $batchRoot
|
New-Directory -Path $batchRoot
|
||||||
New-Directory -Path $evtxRoot
|
New-Directory -Path $evtxRoot
|
||||||
|
|
||||||
$daysBackMs = [int64]$DaysBack * 24 * 60 * 60 * 1000
|
$lookbackMs = if ($null -ne $HoursBack) {
|
||||||
$query = "*[System[TimeCreated[timediff(@SystemTime) <= $daysBackMs]]]"
|
[int64]$HoursBack * 60 * 60 * 1000
|
||||||
|
} else {
|
||||||
|
[int64]$DaysBack * 24 * 60 * 60 * 1000
|
||||||
|
}
|
||||||
|
$query = "*[System[TimeCreated[timediff(@SystemTime) <= $lookbackMs]]]"
|
||||||
$results = @()
|
$results = @()
|
||||||
|
|
||||||
foreach ($channel in @($effectiveChannels | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })) {
|
foreach ($channel in @($effectiveChannels | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })) {
|
||||||
@@ -117,6 +122,7 @@ $manifest = [ordered]@{
|
|||||||
batchRoot = $batchRoot
|
batchRoot = $batchRoot
|
||||||
zipPath = if ($NoZip) { $null } else { $zipPath }
|
zipPath = if ($NoZip) { $null } else { $zipPath }
|
||||||
daysBack = $DaysBack
|
daysBack = $DaysBack
|
||||||
|
hoursBack = if ($null -ne $HoursBack) { [int]$HoursBack } else { $null }
|
||||||
retentionDays = $effectiveRetentionDays
|
retentionDays = $effectiveRetentionDays
|
||||||
channels = @($effectiveChannels)
|
channels = @($effectiveChannels)
|
||||||
exports = @($results)
|
exports = @($results)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
|
[Nullable[int]]$HoursBack,
|
||||||
[int]$DaysBack = 1,
|
[int]$DaysBack = 1,
|
||||||
[string]$ServerHost = '10.10.10.13',
|
[string]$ServerHost = '',
|
||||||
[string]$ServerUser = 'awops',
|
[string]$ServerUser = 'awops',
|
||||||
[string]$RemoteDropDir = '/opt/activitywatch/aw-rus-ops/drop',
|
[string]$RemoteDropDir = '/opt/activitywatch/aw-rus-ops/drop',
|
||||||
[string]$RemoteKeyPath = 'C:\ProgramData\AWatch-rus\ssh\awops_ed25519',
|
[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"
|
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
|
$zipPath = [string]$export.zipPath
|
||||||
$hostName = [string]$export.hostname
|
$hostName = [string]$export.hostname
|
||||||
if ([string]::IsNullOrWhiteSpace($zipPath) -or -not (Test-Path -LiteralPath $zipPath)) {
|
if ([string]::IsNullOrWhiteSpace($zipPath) -or -not (Test-Path -LiteralPath $zipPath)) {
|
||||||
@@ -52,9 +69,23 @@ $zipName = Split-Path -Leaf $zipPath
|
|||||||
$remoteTarget = "$ServerUser@$ServerHost`:$RemoteDropDir/"
|
$remoteTarget = "$ServerUser@$ServerHost`:$RemoteDropDir/"
|
||||||
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($zipPath)
|
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($zipPath)
|
||||||
$caseIdPath = Join-Path ([System.IO.Path]::GetDirectoryName($zipPath)) ($baseName + '.caseid')
|
$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
|
$effectiveKeyPath = New-TemporarySshKeyCopy -SourceKeyPath $RemoteKeyPath
|
||||||
|
|
||||||
try {
|
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) {
|
if ($null -ne $CaseId) {
|
||||||
Set-Content -LiteralPath $caseIdPath -Value ([string]$CaseId) -Encoding ASCII
|
Set-Content -LiteralPath $caseIdPath -Value ([string]$CaseId) -Encoding ASCII
|
||||||
& scp.exe -i $effectiveKeyPath -o StrictHostKeyChecking=no -o UserKnownHostsFile=NUL $caseIdPath $remoteTarget
|
& scp.exe -i $effectiveKeyPath -o StrictHostKeyChecking=no -o UserKnownHostsFile=NUL $caseIdPath $remoteTarget
|
||||||
@@ -69,12 +100,14 @@ try {
|
|||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
Remove-Item -LiteralPath $effectiveKeyPath -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $effectiveKeyPath -Force -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item -LiteralPath $metaPath -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = [ordered]@{
|
$result = [ordered]@{
|
||||||
exportedZip = $zipPath
|
exportedZip = $zipPath
|
||||||
uploadedTo = "$RemoteDropDir/$zipName"
|
uploadedTo = "$RemoteDropDir/$zipName"
|
||||||
caseIdSidecar = if ($null -ne $CaseId) { "$RemoteDropDir/$baseName.caseid" } else { $null }
|
caseIdSidecar = if ($null -ne $CaseId) { "$RemoteDropDir/$baseName.caseid" } else { $null }
|
||||||
|
metaSidecar = "$RemoteDropDir/$baseName.meta.json"
|
||||||
hostname = $hostName
|
hostname = $hostName
|
||||||
mode = $Mode
|
mode = $Mode
|
||||||
runRemote = [bool]$RunRemote
|
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' }
|
$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' }
|
$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' }
|
$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'
|
$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' }
|
$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' }
|
$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' }
|
$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 }
|
$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' }
|
$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) {
|
$effectiveUsers = if ($Users -or $UserListPath) {
|
||||||
Normalize-ActivityWatchUsers -Users $Users -UserListPath $UserListPath -Domain $Domain
|
Normalize-ActivityWatchUsers -Users $Users -UserListPath $UserListPath -Domain $Domain
|
||||||
@@ -131,6 +137,7 @@ $assetResult = Copy-ActivityWatchCollectorAssets `
|
|||||||
-FileCollectorScriptSource (Join-Path $PSScriptRoot 'file-operations-collector.ps1') `
|
-FileCollectorScriptSource (Join-Path $PSScriptRoot 'file-operations-collector.ps1') `
|
||||||
-SessionCollectorScriptSource (Join-Path $PSScriptRoot 'worktime-session-collector.ps1') `
|
-SessionCollectorScriptSource (Join-Path $PSScriptRoot 'worktime-session-collector.ps1') `
|
||||||
-EvtxExportScriptSource (Join-Path $PSScriptRoot 'export-evtx-for-hayabusa.ps1') `
|
-EvtxExportScriptSource (Join-Path $PSScriptRoot 'export-evtx-for-hayabusa.ps1') `
|
||||||
|
-HayabusaUploadScriptSource (Join-Path $PSScriptRoot 'export-upload-hayabusa-to-aw-server.ps1') `
|
||||||
-ExampleRulesSource (Join-Path $PSScriptRoot 'web-category-rules.example.json') `
|
-ExampleRulesSource (Join-Path $PSScriptRoot 'web-category-rules.example.json') `
|
||||||
-ExamplePolicySource (Join-Path $PSScriptRoot 'dlp-policy.example.json') `
|
-ExamplePolicySource (Join-Path $PSScriptRoot 'dlp-policy.example.json') `
|
||||||
-StateRoot $effectiveStateRoot `
|
-StateRoot $effectiveStateRoot `
|
||||||
@@ -155,6 +162,7 @@ $config = New-ActivityWatchDeploymentConfig `
|
|||||||
-FileCollectorScript $effectiveFileCollector `
|
-FileCollectorScript $effectiveFileCollector `
|
||||||
-SessionCollectorScript $effectiveSessionCollector `
|
-SessionCollectorScript $effectiveSessionCollector `
|
||||||
-EvtxExportScript $effectiveEvtxExportScript `
|
-EvtxExportScript $effectiveEvtxExportScript `
|
||||||
|
-HayabusaUploadScript $effectiveHayabusaUploadScript `
|
||||||
-RulesPath $effectiveRules `
|
-RulesPath $effectiveRules `
|
||||||
-PolicyPath $effectivePolicy `
|
-PolicyPath $effectivePolicy `
|
||||||
-PollSeconds $effectivePollSeconds `
|
-PollSeconds $effectivePollSeconds `
|
||||||
@@ -179,6 +187,11 @@ $config = New-ActivityWatchDeploymentConfig `
|
|||||||
-PolicyEngineScheme $effectivePolicyEngineScheme `
|
-PolicyEngineScheme $effectivePolicyEngineScheme `
|
||||||
-PolicyRefreshSeconds $effectivePolicyRefreshSeconds `
|
-PolicyRefreshSeconds $effectivePolicyRefreshSeconds `
|
||||||
-PolicyCachePath $effectivePolicyCachePath `
|
-PolicyCachePath $effectivePolicyCachePath `
|
||||||
|
-HayabusaAutoUploadEnabled $effectiveHayabusaAutoUploadEnabled `
|
||||||
|
-HayabusaAutoUploadIntervalHours $effectiveHayabusaAutoUploadIntervalHours `
|
||||||
|
-HayabusaAutoUploadHoursBack $effectiveHayabusaAutoUploadHoursBack `
|
||||||
|
-HayabusaAutoUploadMode $effectiveHayabusaAutoUploadMode `
|
||||||
|
-HayabusaAutoUploadTaskName $effectiveHayabusaAutoUploadTaskName `
|
||||||
-LaunchScriptPath $effectiveLaunchScript `
|
-LaunchScriptPath $effectiveLaunchScript `
|
||||||
-RecoveryScriptPath $effectiveRecoveryScript `
|
-RecoveryScriptPath $effectiveRecoveryScript `
|
||||||
-UserTasks $taskDefinitions `
|
-UserTasks $taskDefinitions `
|
||||||
@@ -189,6 +202,7 @@ Remove-LegacyActivityWatchEntries
|
|||||||
Set-ActivityWatchAcl -InstallRoot $effectiveInstallRoot -StateRoot $effectiveStateRoot -LogsRoot $effectiveLogsRoot
|
Set-ActivityWatchAcl -InstallRoot $effectiveInstallRoot -StateRoot $effectiveStateRoot -LogsRoot $effectiveLogsRoot
|
||||||
Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $effectiveLaunchScript -ConfigPath $effectiveConfigPath
|
Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $effectiveLaunchScript -ConfigPath $effectiveConfigPath
|
||||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath
|
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath
|
||||||
|
Register-ActivityWatchHayabusaAutoUploadTask -ConfigPath $effectiveConfigPath
|
||||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||||
|
|
||||||
Write-Host 'Укрепление и восстановление ActivityWatch завершены.'
|
Write-Host 'Укрепление и восстановление ActivityWatch завершены.'
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
#define MyAppVersion "1.0.0"
|
#define MyAppVersion "1.0.0"
|
||||||
#define MyAppPublisher "AWatch-rus"
|
#define MyAppPublisher "AWatch-rus"
|
||||||
|
|
||||||
#define AwDefaultServerHost "10.10.10.13"
|
#define AwDefaultServerHost "aw-server"
|
||||||
#define AwDefaultServerPort "5600"
|
#define AwDefaultServerPort "5600"
|
||||||
#define AwDefaultWorktimeReportBase "http://10.10.10.13:5610"
|
#define AwDefaultWorktimeReportBase "http://aw-server:5610"
|
||||||
#define AwDefaultWorktimeHost "SHARKON2025"
|
#define AwDefaultWorktimeHost "SHARKON2025"
|
||||||
#define AwDefaultUsers "user1,user2,user3,user4,user5"
|
#define AwDefaultUsers "user1,user2,user3,user4,user5"
|
||||||
#define AwDefaultInstallRoot "C:\\Program Files\\AWatch-rus\\bin"
|
#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:
|
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.
|
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 {
|
function Get-LoggedOnUsers {
|
||||||
$users = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
$users = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
$activeStates = @('Active', 'Активно')
|
||||||
|
$inactiveStates = @('Disc', 'Disconnected', 'Idle', 'Listen', 'Диск', 'Откл', 'Отключен')
|
||||||
try {
|
try {
|
||||||
$lines = & quser.exe 2>$null
|
$lines = & quser.exe 2>$null
|
||||||
foreach ($line in @($lines)) {
|
foreach ($line in @($lines)) {
|
||||||
@@ -50,6 +52,16 @@ function Get-LoggedOnUsers {
|
|||||||
if ($parts.Count -lt 1) { continue }
|
if ($parts.Count -lt 1) { continue }
|
||||||
$user = [string]$parts[0]
|
$user = [string]$parts[0]
|
||||||
if ([string]::IsNullOrWhiteSpace($user)) { continue }
|
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($user)
|
||||||
[void]$users.Add(('{0}\{1}' -f $env:COMPUTERNAME, $user))
|
[void]$users.Add(('{0}\{1}' -f $env:COMPUTERNAME, $user))
|
||||||
if (-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) {
|
if (-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user