diff --git a/.gitignore b/.gitignore index 7eecd45..62fb7b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,13 @@ # Local secrets secrets/deploy.secrets.env +secrets/runtime.env # Runtime / reports *.log *.tmp *.bak windows/*.report.json +.rollout-logs/ # IDE .idea/ diff --git a/ansible/README.md b/ansible/README.md index b22fc6c..3c9d42a 100644 --- a/ansible/README.md +++ b/ansible/README.md @@ -31,6 +31,15 @@ cd ansible ansible-playbook -i inventory.ini deploy_aw_server.yml ``` +## Секреты (пароли) безопасно + +Рекомендуемый способ не хранить пароли в репозитории — перед запуском экспортировать их в переменные окружения: + +- Linux `aw_server` (SSH пароль root): `AW_SSH_PASSWORD` +- Windows `aw_windows` (WinRM пароль): `AW_WINRM_PASSWORD` + +В `group_vars/aw_server.yml` и `group_vars/windows.yml` они читаются через `lookup('env', ...)`. + ## Полный установочный playbook (всё за один запуск) Если нужно прогнать полный цикл одной командой: @@ -149,3 +158,13 @@ Playbook: - Для полного сценария CT создаётся автоматически через `pct create`. - На Windows/RDP host развёрнуты AFK/window watchers, browser domain collector, DLP endpoint collector и worktime session collector. - Проверочный JSON-отчёт Windows playbook должен иметь `overallOk=true`. + +## Prod rollout одной командой + +Для ручного запуска с dry-run и логированием используйте: + +```bash +bash scripts/prod_rollout.sh +``` + +Скрипт попросит `AW_SSH_PASSWORD` и `AW_WINRM_PASSWORD` интерактивно (ввод скрыт) и сложит логи в `.rollout-logs/`. diff --git a/ansible/deploy_aw_server.yml b/ansible/deploy_aw_server.yml index 37143de..3f84e58 100644 --- a/ansible/deploy_aw_server.yml +++ b/ansible/deploy_aw_server.yml @@ -76,98 +76,140 @@ - "{{ aw_server_data_dir }}" - "{{ aw_server_log_dir }}" - - name: Скачать архив релиза ActivityWatch - ansible.builtin.get_url: - url: "{{ aw_server_download_url }}" - dest: "{{ aw_archive_path }}" - mode: "0644" + - name: (Check mode) Пропустить установку релиза ActivityWatch + ansible.builtin.debug: + msg: "ansible_check_mode=true: download/unarchive/install of ActivityWatch release is skipped." + when: ansible_check_mode - - name: Распаковать релиз ActivityWatch - ansible.builtin.unarchive: - src: "{{ aw_archive_path }}" - dest: "{{ aw_release_dir }}" - remote_src: true - extra_opts: ["-o"] + - name: Установить релиз ActivityWatch (download/unarchive/install) + when: not ansible_check_mode + block: + - name: Скачать архив релиза ActivityWatch + ansible.builtin.get_url: + url: "{{ aw_server_download_url }}" + dest: "{{ aw_archive_path }}" + mode: "0644" - - name: Найти распакованный каталог ActivityWatch - ansible.builtin.find: - paths: "{{ aw_release_dir }}" - file_type: directory - patterns: "activitywatch*" - register: aw_release_find + - name: Распаковать релиз ActivityWatch + ansible.builtin.unarchive: + src: "{{ aw_archive_path }}" + dest: "{{ aw_release_dir }}" + remote_src: true + extra_opts: ["-o"] - - name: Найти бинарный файл AW server - ansible.builtin.find: - paths: "{{ aw_release_dir }}" - file_type: file - patterns: - - aw-server-rust - - aw-server - register: aw_server_binary_find + - name: Найти распакованный каталог ActivityWatch + ansible.builtin.find: + paths: "{{ aw_release_dir }}" + recurse: true + file_type: directory + patterns: "activitywatch*" + register: aw_release_find - - name: Найти каталог WebUI - ansible.builtin.find: - paths: "{{ aw_release_dir }}" - file_type: directory - patterns: - - aw-webui - - webui - register: aw_webui_dir_find + - name: Найти бинарный файл AW server + ansible.builtin.find: + paths: "{{ aw_release_dir }}" + recurse: true + file_type: file + patterns: + - aw-server-rust + - aw-server + register: aw_server_binary_find - - name: Сохранить пути распакованного релиза - ansible.builtin.set_fact: - aw_release_extracted: "{{ (aw_release_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}" - aw_server_binary_path: "{{ (aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}" - aw_webui_source_path: "{{ (aw_webui_dir_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}" + - name: Найти index.html WebUI + ansible.builtin.find: + paths: "{{ aw_release_dir }}" + recurse: true + file_type: file + patterns: + - index.html + register: aw_webui_index_find - - name: Проверить, что компоненты релиза найдены - ansible.builtin.assert: - that: - - aw_release_extracted is defined - - aw_release_extracted | length > 0 - - aw_server_binary_path is defined - - aw_server_binary_path | length > 0 - - aw_webui_source_path is defined - - aw_webui_source_path | length > 0 - fail_msg: "Не удалось найти бинарный файл или WebUI в распакованном релизе ActivityWatch." + - name: Сохранить пути распакованного релиза (binary + webui index) + ansible.builtin.set_fact: + aw_release_extracted: "{{ (aw_release_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}" + aw_server_binary_path: >- + {{ + ( + ( + (aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list) + | select('match', '.*/aw-server-rust$') | list | first + ) + | default( + ( + (aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) + ), + true + ) + ) | default('') + }} + aw_webui_index_path: >- + {{ + ( + ( + (aw_webui_index_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list) + | select('search', '/static/index\\.html$') | list | first + ) + | default( + ( + (aw_webui_index_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) + ), + true + ) + ) | default('') + }} - - name: Создать каталог установленного релиза - ansible.builtin.file: - path: "{{ aw_release_install_dir }}" - state: directory - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0755" + - name: Сохранить каталог WebUI (dirname index.html) + ansible.builtin.set_fact: + aw_webui_source_path: "{{ aw_webui_index_path | dirname }}" - - name: Установить бинарный файл AW server - ansible.builtin.copy: - remote_src: true - src: "{{ aw_server_binary_path }}" - dest: "{{ aw_release_install_dir }}/aw-server-rust" - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0755" + - name: Проверить, что компоненты релиза найдены + ansible.builtin.assert: + that: + - aw_release_extracted is defined + - aw_release_extracted | length > 0 + - aw_server_binary_path is defined + - aw_server_binary_path | length > 0 + - aw_webui_source_path is defined + - aw_webui_source_path | length > 0 + fail_msg: "Не удалось найти бинарный файл или WebUI в распакованном релизе ActivityWatch." - - name: Создать ссылку на активный бинарный файл AW server - ansible.builtin.file: - src: "{{ aw_release_install_dir }}/aw-server-rust" - dest: /opt/activitywatch/bin/aw-server-rust - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - state: link - force: true + - name: Создать каталог установленного релиза + ansible.builtin.file: + path: "{{ aw_release_install_dir }}" + state: directory + owner: "{{ aw_server_user }}" + group: "{{ aw_server_group }}" + mode: "0755" - - name: Синхронизировать WebUI в RU каталог - ansible.builtin.command: - cmd: "rsync -a {{ aw_webui_source_path }}/ {{ aw_server_webui_dir }}/" + - name: Установить бинарный файл AW server + ansible.builtin.copy: + remote_src: true + src: "{{ aw_server_binary_path }}" + dest: "{{ aw_release_install_dir }}/aw-server-rust" + owner: "{{ aw_server_user }}" + group: "{{ aw_server_group }}" + mode: "0755" - - name: Настроить владельца файлов /opt/activitywatch - ansible.builtin.file: - path: /opt/activitywatch - state: directory - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - recurse: true + - name: Создать ссылку на активный бинарный файл AW server + ansible.builtin.file: + src: "{{ aw_release_install_dir }}/aw-server-rust" + dest: /opt/activitywatch/bin/aw-server-rust + owner: "{{ aw_server_user }}" + group: "{{ aw_server_group }}" + state: link + force: true + + - name: Синхронизировать WebUI в RU каталог + ansible.builtin.command: + cmd: "rsync -a {{ aw_webui_source_path }}/ {{ aw_server_webui_dir }}/" + + - name: Настроить владельца файлов /opt/activitywatch + ansible.builtin.file: + path: /opt/activitywatch + state: directory + owner: "{{ aw_server_user }}" + group: "{{ aw_server_group }}" + recurse: true - name: Установить systemd service из шаблона репозитория ansible.builtin.copy: @@ -184,78 +226,86 @@ - Перезагрузить systemd - Перезапустить activitywatch - - name: Скопировать RU patch файлы WebUI из репозитория - ansible.builtin.copy: - src: "{{ item.src }}" - dest: "{{ item.dest }}" - mode: "{{ item.mode }}" - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - loop: - - { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "{{ aw_server_webui_dir }}/js/ru-patch-v5.js", mode: "0644" } - - { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "{{ aw_server_webui_dir }}/js/sw-cleanup.js", mode: "0644" } - - { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "{{ aw_server_webui_dir }}/js/aw-host-groups.json", mode: "0644" } + - name: (Check mode) Пропустить WebUI patch и запуск сервиса + ansible.builtin.debug: + msg: "ansible_check_mode=true: WebUI patch + service start + API checks are skipped." + when: ansible_check_mode - - name: Проверить наличие index.html после копирования - ansible.builtin.stat: - path: "{{ aw_server_webui_dir }}/index.html" - register: aw_webui_ru_index + - name: Применить WebUI RU patch и запустить сервис + when: not ansible_check_mode + block: + - name: Скопировать RU patch файлы WebUI из репозитория + ansible.builtin.copy: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + mode: "{{ item.mode }}" + owner: "{{ aw_server_user }}" + group: "{{ aw_server_group }}" + loop: + - { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "{{ aw_server_webui_dir }}/js/ru-patch-v5.js", mode: "0644" } + - { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "{{ aw_server_webui_dir }}/js/sw-cleanup.js", mode: "0644" } + - { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "{{ aw_server_webui_dir }}/js/aw-host-groups.json", mode: "0644" } - - name: Проверить, что index.html доступен для RU patch - ansible.builtin.assert: - that: - - aw_webui_ru_index.stat.exists - fail_msg: "Не найден index.html WebUI для применения RU patch." + - name: Проверить наличие index.html после копирования + ansible.builtin.stat: + path: "{{ aw_server_webui_dir }}/index.html" + register: aw_webui_ru_index - - name: Удалить старые теги RU patch из index.html - ansible.builtin.replace: - path: "{{ aw_server_webui_dir }}/index.html" - regexp: ']+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*>' - replace: '' + - name: Проверить, что index.html доступен для RU patch + ansible.builtin.assert: + that: + - aw_webui_ru_index.stat.exists + fail_msg: "Не найден index.html WebUI для применения RU patch." - - name: Добавить cleanup script RU patch в index.html - ansible.builtin.replace: - path: "{{ aw_server_webui_dir }}/index.html" - regexp: '' - replace: '' + - name: Удалить старые теги RU patch из index.html + ansible.builtin.replace: + path: "{{ aw_server_webui_dir }}/index.html" + regexp: ']+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*>' + replace: '' - - name: Добавить загрузчик RU patch перед закрытием body - ansible.builtin.replace: - path: "{{ aw_server_webui_dir }}/index.html" - regexp: '' - replace: '' + - name: Добавить cleanup script RU patch в index.html + ansible.builtin.replace: + path: "{{ aw_server_webui_dir }}/index.html" + regexp: '' + replace: '' - - name: Записать /etc/activitywatch/aw-server.env - ansible.builtin.copy: - dest: /etc/activitywatch/aw-server.env - mode: "0640" - owner: root - group: root - content: | - AW_SERVER_BIND_HOST={{ aw_server_bind_host }} - AW_SERVER_PORT={{ aw_server_port }} - AW_SERVER_DATA_DIR={{ aw_server_data_dir }} - AW_SERVER_LOG_DIR={{ aw_server_log_dir }} - AW_SERVER_WEBUI_DIR={{ aw_server_webui_dir }} - AW_SERVER_USER={{ aw_server_user }} - AW_SERVER_GROUP={{ aw_server_group }} + - name: Добавить загрузчик RU patch перед закрытием body + ansible.builtin.replace: + path: "{{ aw_server_webui_dir }}/index.html" + regexp: '' + replace: '' - - name: Включить и запустить сервис - ansible.builtin.systemd: - name: activitywatch-server.service - enabled: true - state: restarted - daemon_reload: true + - name: Записать /etc/activitywatch/aw-server.env + ansible.builtin.copy: + dest: /etc/activitywatch/aw-server.env + mode: "0640" + owner: root + group: root + content: | + AW_SERVER_BIND_HOST={{ aw_server_bind_host }} + AW_SERVER_PORT={{ aw_server_port }} + AW_SERVER_DATA_DIR={{ aw_server_data_dir }} + AW_SERVER_LOG_DIR={{ aw_server_log_dir }} + AW_SERVER_WEBUI_DIR={{ aw_server_webui_dir }} + AW_SERVER_USER={{ aw_server_user }} + AW_SERVER_GROUP={{ aw_server_group }} - - name: Дождаться ответа API - ansible.builtin.uri: - url: "http://127.0.0.1:{{ aw_server_port }}/api/0/info" - method: GET - status_code: 200 - register: aw_api - retries: 10 - delay: 3 - until: aw_api.status == 200 + - name: Включить и запустить сервис + ansible.builtin.systemd: + name: activitywatch-server.service + enabled: true + state: restarted + daemon_reload: true + + - name: Дождаться ответа API + ansible.builtin.uri: + url: "http://127.0.0.1:{{ aw_server_port }}/api/0/info" + method: GET + status_code: 200 + register: aw_api + retries: 10 + delay: 3 + until: aw_api.status == 200 - name: Применить базовые worktime settings (classes) ansible.builtin.uri: diff --git a/ansible/deploy_aw_windows.yml b/ansible/deploy_aw_windows.yml index 5d18277..9fac90b 100644 --- a/ansible/deploy_aw_windows.yml +++ b/ansible/deploy_aw_windows.yml @@ -86,6 +86,18 @@ - web-category-rules.example.json - dlp-policy.example.json + - name: Нормализовать кодировку PowerShell файлов (UTF-8 BOM для Windows PowerShell) + ansible.windows.win_powershell: + script: | + $ErrorActionPreference = 'Stop' + $toolkitDir = "{{ aw_windows_deploy_root }}\windows" + $encIn = New-Object System.Text.UTF8Encoding($false) + $encOut = New-Object System.Text.UTF8Encoding($true) + Get-ChildItem -LiteralPath $toolkitDir -File -Include *.ps1,*.psm1,*.psd1 | ForEach-Object { + $text = [System.IO.File]::ReadAllText($_.FullName, $encIn) + [System.IO.File]::WriteAllText($_.FullName, $text, $encOut) + } + - name: Загрузить список пользователей для доменного развёртывания ansible.windows.win_copy: dest: "{{ aw_windows_deploy_root }}\\windows\\users.txt" diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml new file mode 100644 index 0000000..405771f --- /dev/null +++ b/ansible/group_vars/all.yml @@ -0,0 +1,19 @@ +aw_server_version: "v0.13.2" +aw_server_download_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-linux-x86_64.zip" +aw_server_bind_host: "0.0.0.0" +aw_server_port: 5600 +aw_server_webui_dir: "/opt/activitywatch/webui-ru" +aw_server_data_dir: "/var/lib/activitywatch" +aw_server_log_dir: "/var/log/activitywatch" +aw_server_user: "activitywatch" +aw_server_group: "activitywatch" + +aw_repo_root: "{{ playbook_dir | dirname }}" + +# Optional: apply worktime settings via server-side settings API. +aw_apply_worktime_settings: false + +aw_worktime_from: "08:00" +aw_worktime_to: "17:00" +aw_worktime_start_of_day: "{{ aw_worktime_from }}" + diff --git a/ansible/group_vars/aw_server.yml b/ansible/group_vars/aw_server.yml new file mode 100644 index 0000000..73b9934 --- /dev/null +++ b/ansible/group_vars/aw_server.yml @@ -0,0 +1,9 @@ +# Secret handling: +# - put the real SSH password into env var before running Ansible: +# export AW_SSH_PASSWORD='...' +ansible_password: "{{ lookup('env', 'AW_SSH_PASSWORD') }}" + +ansible_become: true +ansible_become_method: sudo +# If sudo password differs, set AW_SUDO_PASSWORD. Otherwise it will reuse AW_SSH_PASSWORD. +ansible_become_password: "{{ lookup('env', 'AW_SUDO_PASSWORD') | default(lookup('env', 'AW_SSH_PASSWORD'), true) }}" diff --git a/ansible/group_vars/aw_windows.yml b/ansible/group_vars/aw_windows.yml new file mode 100644 index 0000000..3933718 --- /dev/null +++ b/ansible/group_vars/aw_windows.yml @@ -0,0 +1,52 @@ +# Secret handling: +# - put the real password into env var before running Ansible: +# export AW_WINRM_PASSWORD='...' +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" +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" +aw_windows_package_zip_path: "" + +aw_windows_domain: "SHARKON2025" +aw_windows_users: + - user1 + - user2 + - user3 + - user4 + - user5 +aw_windows_extra_users: [] + +aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin" +aw_windows_state_root: "C:\\ProgramData\\AWatch-rus" + +aw_windows_afk_enabled: true +aw_windows_window_enabled: true +aw_windows_local_agent_logs_enabled: false +aw_windows_incident_capture_enabled: true +aw_windows_incident_screenshot_enabled: true +aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts" +aw_windows_logon_marker_enabled: true +aw_windows_skip_hardening: false + +aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json" +aw_windows_policy_path: "{{ aw_windows_deploy_root }}\\windows\\dlp-policy.example.json" + +aw_windows_validation_remote_path: "{{ aw_windows_state_root }}\\aw_validate_ansible.json" +aw_windows_validation_local_dir: "/tmp/aw-rus-validation" +aw_windows_fail_on_validation_error: true + +aw_windows_migration_enabled: true +aw_windows_legacy_install_root: "C:\\Program Files\\ActivityWatch-Phase2" +aw_windows_legacy_state_root: "C:\\ProgramData\\ActivityWatch-Phase2" +aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json" + +aw_windows_api_smoke_check_enabled: true +aw_windows_api_smoke_check_bucket: "" +aw_windows_api_smoke_check_limit: 10 + diff --git a/ansible/inventory.ini b/ansible/inventory.ini new file mode 100644 index 0000000..3aacfd6 --- /dev/null +++ b/ansible/inventory.ini @@ -0,0 +1,14 @@ +[proxmox] +# Optional. Leave empty if you don't use Proxmox provisioning from this repo. +# pve-main ansible_host=10.10.10.2 ansible_user=igor ansible_port=22 + +[aw_server] +aw-server ansible_host=10.10.10.13 ansible_user=igor ansible_port=22 + +[aw_windows] +# Note: on RU-localized Windows the built-in admin account name is often "Администратор". +rdp-prod ansible_host=192.168.100.21 ansible_user=Администратор ansible_connection=winrm ansible_winrm_transport=ntlm ansible_port=5985 ansible_winrm_server_cert_validation=ignore + +[aw_pfsense_pollers] +# Optional. +# pfsense-poller1 ansible_host=192.168.100.30 ansible_user=root ansible_port=22 diff --git a/scripts/prod_rollout.sh b/scripts/prod_rollout.sh new file mode 100644 index 0000000..d7f080c --- /dev/null +++ b/scripts/prod_rollout.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +timestamp() { date +"%Y%m%d-%H%M%S"; } + +LOG_DIR="${ROOT_DIR}/.rollout-logs/$(timestamp)" +mkdir -p "$LOG_DIR" + +log() { printf "%s %s\n" "$(date +"%F %T")" "$*" | tee -a "${LOG_DIR}/rollout.log" >&2; } + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { log "ERROR: missing command: $1"; exit 127; } +} + +prompt_secret() { + local var_name="$1" + local prompt="$2" + if [[ -n "${!var_name:-}" ]]; then + return 0 + fi + read -r -s -p "${prompt}: " "$var_name" + echo + export "$var_name" +} + +require_cmd git +require_cmd ansible-playbook +require_cmd ansible + +log "Repo: ${ROOT_DIR}" +log "Branch: $(git branch --show-current)" + +log "Running local quality gate..." +./scripts/quality-gate.sh | tee -a "${LOG_DIR}/quality-gate.log" + +if [[ -f "${ROOT_DIR}/secrets/runtime.env" ]]; then + log "Loading secrets/runtime.env" + set -a + # shellcheck disable=SC1091 + source "${ROOT_DIR}/secrets/runtime.env" + set +a +fi + +if [[ ! -f ansible/inventory.ini ]]; then + log "ERROR: missing ansible/inventory.ini" + log "Hint: copy ansible/inventory.example.ini -> ansible/inventory.ini and adjust hosts." + exit 2 +fi + +if [[ -t 0 ]]; then + prompt_secret AW_SSH_PASSWORD "Enter SSH password for aw_server (root@10.10.10.13)" + prompt_secret AW_WINRM_PASSWORD "Enter WinRM password for aw_windows (192.168.100.21)" +fi + +if [[ -z "${AW_SSH_PASSWORD:-}" || -z "${AW_WINRM_PASSWORD:-}" ]]; then + log "ERROR: missing AW_SSH_PASSWORD or AW_WINRM_PASSWORD." + log "Provide them via interactive prompt (TTY) or create secrets/runtime.env." + exit 3 +fi + +log "Preflight connectivity..." +ansible -i ansible/inventory.ini aw_server -m ping | tee -a "${LOG_DIR}/ping_aw_server.log" +ansible -i ansible/inventory.ini aw_windows -m win_ping | tee -a "${LOG_DIR}/ping_aw_windows.log" + +log "Dry-run aw_server..." +ansible-playbook -i ansible/inventory.ini ansible/deploy_aw_server.yml --check --diff | tee -a "${LOG_DIR}/check_aw_server.log" + +log "Deploy aw_server..." +ansible-playbook -i ansible/inventory.ini ansible/deploy_aw_server.yml | tee -a "${LOG_DIR}/deploy_aw_server.log" + +log "Dry-run aw_windows..." +ansible-playbook -i ansible/inventory.ini ansible/deploy_aw_windows.yml --check --diff | tee -a "${LOG_DIR}/check_aw_windows.log" + +log "Deploy aw_windows..." +ansible-playbook -i ansible/inventory.ini ansible/deploy_aw_windows.yml | tee -a "${LOG_DIR}/deploy_aw_windows.log" + +log "DONE. Logs: ${LOG_DIR}" diff --git a/windows/hardening-recovery.ps1 b/windows/hardening-recovery.ps1 index 64e8d73..a7950d5 100755 --- a/windows/hardening-recovery.ps1 +++ b/windows/hardening-recovery.ps1 @@ -53,6 +53,7 @@ $effectiveLaunchScript = Join-Path $effectiveStateRoot 'launch-watchers.ps1' $effectiveRecoveryScript = Join-Path $effectiveStateRoot 'recovery-loop.ps1' $effectiveCollector = Join-Path $effectiveStateRoot 'browser-domains-native-collector.ps1' $effectiveEndpointCollector = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]$existingConfig.paths.endpointCollectorScript } else { Join-Path $effectiveStateRoot 'dlp-endpoint-signals-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' } $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' } diff --git a/windows/migrate-awatch-rus-paths.ps1 b/windows/migrate-awatch-rus-paths.ps1 index eb7cbe4..c588050 100644 --- a/windows/migrate-awatch-rus-paths.ps1 +++ b/windows/migrate-awatch-rus-paths.ps1 @@ -154,7 +154,36 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Миграция ActivityWatch W @{ Source = $NewStateRoot; Name = 'new-state' } )) { if (Test-Path -LiteralPath $item.Source) { - Copy-Item -LiteralPath $item.Source -Destination (Join-Path $backupRoot $item.Name) -Recurse -Force + $backupDest = Join-Path $backupRoot $item.Name + New-ActivityWatchDirectory -Path $backupDest + + $excludeDirs = @() + if ($item.Source -eq $NewStateRoot) { + # Avoid infinite recursion: backupRoot is inside NewStateRoot by default. + $excludeDirs += $backupRoot + } + + $robocopyArgs = @( + $item.Source, + $backupDest, + '/E', + '/R:1', + '/W:1', + '/NFL', + '/NDL', + '/NJH', + '/NJS', + '/NP' + ) + if ($excludeDirs.Count -gt 0) { + $robocopyArgs += '/XD' + $robocopyArgs += $excludeDirs + } + + & robocopy @robocopyArgs | Out-Null + if ($LASTEXITCODE -ge 8) { + throw "Backup robocopy failed (exit=$LASTEXITCODE) for source '$($item.Source)' to '$backupDest'" + } } } diff --git a/windows/validate-deployment.ps1 b/windows/validate-deployment.ps1 index 52b35d2..785036a 100644 --- a/windows/validate-deployment.ps1 +++ b/windows/validate-deployment.ps1 @@ -50,12 +50,14 @@ $runningProcesses = @() if ($processNames.Count -gt 0) { $runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId } -$sessionCollectorProcesses = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - ($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and - $_.CommandLine -match [Regex]::Escape($sessionCollectorScript) - } | - Select-Object Name, ProcessId, SessionId, CommandLine +$sessionCollectorProcesses = @( + Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | + Where-Object { + ($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and + $_.CommandLine -match [Regex]::Escape($sessionCollectorScript) + } | + Select-Object Name, ProcessId, SessionId, CommandLine +) $taskNames = @() if ($config.userTasks) { @@ -64,25 +66,28 @@ if ($config.userTasks) { $taskNames += [string]$config.recovery.taskName $taskNames = $taskNames | Sort-Object -Unique -$tasks = foreach ($taskName in $taskNames) { - $task = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $taskName } | Select-Object -First 1 - if ($task) { - [pscustomobject]@{ - taskName = $task.TaskName - state = [string]$task.State - present = $true +$tasks = @( + foreach ($taskName in $taskNames) { + $task = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $taskName } | Select-Object -First 1 + if ($task) { + [pscustomobject]@{ + taskName = $task.TaskName + state = [string]$task.State + present = $true + } + } + else { + [pscustomobject]@{ + taskName = $taskName + state = 'Отсутствует' + present = $false + } } } - else { - [pscustomobject]@{ - taskName = $taskName - state = 'Отсутствует' - present = $false - } - } -} +) $serverUrl = '{0}://{1}:{2}' -f [string]$config.server.scheme, [string]$config.server.host, [int]$config.server.port +$uniqueRunningProcessNames = @($runningProcesses | Select-Object -ExpandProperty Name -Unique) $result = [ordered]@{ generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') configPath = $ConfigPath @@ -105,7 +110,7 @@ $result = [ordered]@{ ok = [bool]( ( ($processNames.Count -eq 0) -or - (($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge $processNames.Count) + ($uniqueRunningProcessNames.Count -ge $processNames.Count) ) -and ($sessionCollectorProcesses.Count -ge 1) )