merge: apply windows standalone service installer and awHostname hardening
This commit is contained in:
@@ -0,0 +1,89 @@
|
|||||||
|
# Copilot instructions for ActivityWatch-Russian
|
||||||
|
|
||||||
|
Purpose: help future Copilot sessions quickly understand how to build, validate, and modify this repo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build / test / lint (how-to)
|
||||||
|
|
||||||
|
- Shell script checks (CI & local):
|
||||||
|
- Full: ./scripts/quality-gate.sh
|
||||||
|
- Single file (syntax): bash -n <script>. Example: bash -n scripts/install_aw_linux_client.sh
|
||||||
|
- Run shellcheck locally (same checks as CI): install shellcheck then run:
|
||||||
|
find . -type f -name "*.sh" -print0 | xargs -0 -r shellcheck -e SC1007,SC1090,SC2016
|
||||||
|
|
||||||
|
- PowerShell checks (Windows / CI):
|
||||||
|
- Single-file analysis (locally in PowerShell):
|
||||||
|
Invoke-ScriptAnalyzer -Path windows/deploy-ensemble.ps1
|
||||||
|
- CI installs PSScriptAnalyzer and runs against windows/*.ps1, *.psm1, *.psd1
|
||||||
|
|
||||||
|
- Python scripts / utilities:
|
||||||
|
- Run a single utility: python3 scripts/aggregate_dlp_events.py
|
||||||
|
- Many scripts are helpers for operations; no test harness in repo.
|
||||||
|
|
||||||
|
- Monitoring stack (Docker Compose):
|
||||||
|
- Start: cd grafana-1c && docker-compose up -d
|
||||||
|
- Start a single service: docker-compose up -d grafana
|
||||||
|
|
||||||
|
- Server install / deploy helpers:
|
||||||
|
- AW server install: aw-server/install_aw_server.sh
|
||||||
|
- Apply RU WebUI patch: aw-server/apply_webui_ru_patch.sh
|
||||||
|
- Windows deploy/validation: windows/deploy-ensemble.ps1 and windows/validate-deployment.ps1
|
||||||
|
|
||||||
|
Notes: there is no unified unit-test suite. Use the script checks and CI pipeline (.github/workflows/ci.yml) as the canonical validation steps.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## High-level architecture (short)
|
||||||
|
|
||||||
|
- Windows collectors (PowerShell) run on endpoints and POST events to the ActivityWatch Server HTTP API.
|
||||||
|
- ActivityWatch Server (deployed on Linux CT/LXC via Proxmox or Debian VM) stores events in PostgreSQL and serves WebUI.
|
||||||
|
- Integration layer: pollers and aggregators (Python) for pfSense, DLP aggregation, Prometheus exporter.
|
||||||
|
- Monitoring: Prometheus + Grafana (docker-compose in grafana-1c) and a SQL exporter for direct DB dashboards.
|
||||||
|
|
||||||
|
Key ports: AW API 5600/5666, PostgreSQL 5432, Prometheus 9090, Grafana 3000, exporter 9398.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key repository conventions
|
||||||
|
|
||||||
|
- Branching / commits:
|
||||||
|
- Use feature branches. Commit style follows Conventional Commits (feat/fix/docs/chore).
|
||||||
|
|
||||||
|
- Secrets and envs:
|
||||||
|
- Secrets live in secrets/*.env templates and must NOT be committed. Use secrets/deploy.secrets.env locally; CI and scripts expect templates (.example).
|
||||||
|
|
||||||
|
- Preflight / PR checks:
|
||||||
|
- Run bash -n for shell scripts and Invoke-ScriptAnalyzer for PowerShell before opening PRs.
|
||||||
|
- Update docs/runbook.md and related runbooks when behavior changes.
|
||||||
|
|
||||||
|
- RU patching:
|
||||||
|
- WebUI localization is applied via aw-server/aw-ru-patch.js and aw-server/apply_webui_ru_patch.sh — treat these as idempotent patch steps during deploy.
|
||||||
|
|
||||||
|
- Systemd / deploy units:
|
||||||
|
- activitywatch-server.service / aw-worktime-api.service / aw-worktime-ui-bridge.service are included in aw-server/ for production use.
|
||||||
|
|
||||||
|
- CI expectations:
|
||||||
|
- .github/workflows/ci.yml runs shellcheck and PSScriptAnalyzer. Use scripts/quality-gate.sh locally to replicate preflight.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Important files & quick references
|
||||||
|
|
||||||
|
- docs/ (onboarding, deployment, runbook) — start here for operational context.
|
||||||
|
- aw-server/ — server install script, env template, RU patch, systemd units.
|
||||||
|
- ansible/ — automated provisioning playbooks for CT/Proxmox and Windows deploys.
|
||||||
|
- windows/ — PowerShell collectors and orchestration; validation scripts are here.
|
||||||
|
- scripts/ — helpers (aggregate_dlp_events.py, installers, quality-gate.sh).
|
||||||
|
- grafana-1c/ — docker-compose monitoring stack and dashboards.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AI assistant & other tool configs to check
|
||||||
|
|
||||||
|
- No Copilot-specific instruction file existed before this addition.
|
||||||
|
- No CLAUDE.md, .cursorrules, AGENTS.md, .windsurfrules, CONVENTIONS.md, or AIDER_CONVENTIONS.md detected at repo root. If adding automated assistant rules, place them in repo root or .github and document cross-references here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
If you need the Copilot instructions extended (e.g., adding run examples for specific scripts, more detailed CI breakdown, or mapping tests to files), say which area to expand.
|
||||||
+23
@@ -8,6 +8,29 @@ secrets/runtime.env
|
|||||||
*.bak
|
*.bak
|
||||||
windows/*.report.json
|
windows/*.report.json
|
||||||
.rollout-logs/
|
.rollout-logs/
|
||||||
|
reports/
|
||||||
|
tmp/
|
||||||
|
graphify-out/cache/
|
||||||
|
graphify-out/powershell-parse-results*.json
|
||||||
|
graphify-out/powershell-pssa-warn-results.json
|
||||||
|
graphify-out/shellcheck-*.txt
|
||||||
|
graphify-out/validate_dryrun_out*.txt
|
||||||
|
graphify-out/pssa_diffs.txt
|
||||||
|
.graphify_analysis.json
|
||||||
|
.graphify_ast.json
|
||||||
|
.graphify_cached.json
|
||||||
|
.graphify_chunk_list_*.txt
|
||||||
|
.graphify_detect.json
|
||||||
|
.graphify_extract.json
|
||||||
|
.graphify_labels.json
|
||||||
|
.graphify_python
|
||||||
|
.graphify_semantic.json
|
||||||
|
.graphify_uncached.txt
|
||||||
|
graphify-out/GRAPH_REPORT.md
|
||||||
|
graphify-out/graph.html
|
||||||
|
graphify-out/graph.json
|
||||||
|
.pssa_run.ps1
|
||||||
|
data/
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
-11236
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
|
|||||||
|
# ActivityWatch-Russian
|
||||||
|
|
||||||
|
## Current Milestone: v1.0 Data Pipeline Stability
|
||||||
|
|
||||||
|
**Goal:** Ensure stable collection and rendering of endpoint activity data in AW Web UI.
|
||||||
|
|
||||||
|
**Target features:**
|
||||||
|
- Reliable ingestion from Windows collectors (`endpoint-signals`, `browser-domains`).
|
||||||
|
- Stable AW server query/render path for worktime/activity pages.
|
||||||
|
- Deployment/runbook consistency (Ansible + install kit + rollback notes).
|
||||||
|
|
||||||
|
## Evolution
|
||||||
|
|
||||||
|
This document evolves at phase transitions and milestone boundaries.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# ROADMAP
|
||||||
|
|
||||||
|
## 🚧 v1.0 Data Pipeline Stability
|
||||||
|
|
||||||
|
- [ ] Phase 1: Collectors and API flow hardening
|
||||||
|
- [ ] Phase 2: UI reports consistency and release proof
|
||||||
|
|
||||||
|
### Phase 1: Collectors and API flow hardening
|
||||||
|
**Goal:** Stabilize collectors-to-server pipeline and remove known transport/runtime failure modes.
|
||||||
|
**Depends on:** none
|
||||||
|
|
||||||
|
### Phase 2: UI reports consistency and release proof
|
||||||
|
**Goal:** Validate report links/output, enforce rollout checks, and freeze reproducible release state.
|
||||||
|
**Depends on:** Phase 1
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
milestone: v1.0
|
||||||
|
milestone_name: Data Pipeline Stability
|
||||||
|
phase: "0"
|
||||||
|
phase_name: Not started
|
||||||
|
current_plan: 0
|
||||||
|
total_plans_in_phase: 0
|
||||||
|
status: planning
|
||||||
|
progress_percent: 0
|
||||||
|
last_activity: 2026-05-07
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Position
|
||||||
|
|
||||||
|
Phase: Not started (defining requirements)
|
||||||
|
Plan: —
|
||||||
|
Status: Defining requirements
|
||||||
|
Last activity: 2026-05-07 — Milestone v1.0 started
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- Prioritize reliable data presence in activity/worktime views over UI extensions.
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
- None.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Phase 1 Context
|
||||||
|
|
||||||
|
## Phase
|
||||||
|
|
||||||
|
Phase 1: Collectors and API flow hardening
|
||||||
|
|
||||||
|
## Focus
|
||||||
|
|
||||||
|
- Endpoint collectors must continuously send data without silent hangs.
|
||||||
|
- AW server must accept/query data for UI pages consistently.
|
||||||
|
- Failure points around transport/CORS/runtime must be explicitly checked.
|
||||||
|
|
||||||
|
## Initial Acceptance Targets
|
||||||
|
|
||||||
|
- Endpoint collector heartbeats arrive regularly.
|
||||||
|
- Browser domains and endpoint signals appear in corresponding buckets.
|
||||||
|
- Activity page for target host shows non-zero timeline/events for active period.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# PLAN — Phase 01: collectors-and-api-flow-hardening
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Deliver stable collector-to-server data flow so activity/worktime pages have consistent data.
|
||||||
|
|
||||||
|
## Work Items
|
||||||
|
|
||||||
|
1. Validate collector runtime and log rotation behavior.
|
||||||
|
2. Validate server ingest endpoints and bucket write/read checks.
|
||||||
|
3. Validate CORS/origin and report link consistency.
|
||||||
|
4. Add/adjust scripts or runbook checks to detect zero-data regressions early.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Manual and scripted checks show fresh events in target buckets.
|
||||||
|
- Host activity page reflects real activity (not `0s`) for active sessions.
|
||||||
|
- No repeating transport errors in collector logs during test window.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Planned.
|
||||||
|
|
||||||
|
## 2. Варианты доработки DLP
|
||||||
|
|
||||||
|
### Вариант A: “Hardening” — Стабилизация текущего
|
||||||
|
|
||||||
|
Цель: довести текущие коллекторы до production-grade уровня надёжности.
|
||||||
|
|
||||||
|
| # | Задача | Усилие | Влияние |
|
||||||
|
|---|---|---|---|
|
||||||
|
| A1 | HTTP retry + exponential backoff во всех коллекторах | 3-5 дней | Высокое — перестанут теряться события |
|
||||||
|
| A2 | Локальный WAL (Write-Ahead Log) — буферизация событий при недоступности сервера | 1-2 нед | Критическое — гарантия доставки |
|
||||||
|
| A3 | Healthcheck endpoint и self-diagnostics в каждом коллекторе | 3-5 дней | Среднее — видимость состояния агентов |
|
||||||
|
| A4 | Расширить aggregator: добавить `aw-email-monitor_` и `aw-dlp-endpoint-signals_` в сбор | 1 день | Среднее |
|
||||||
|
| A5 | Systemd timer / Windows Task для aggregator (автоматический запуск) | 1 день | Среднее |
|
||||||
|
| A6 | Убрать пароль из `inventory.ini` → использовать Ansible Vault или env var | 1 час | Критическое (безопасность) |
|
||||||
|
| A7 | Graceful shutdown и cleanup event subscriptions во всех коллекторах | 2-3 дня | Среднее |
|
||||||
|
|
||||||
|
Общее усилие: ~3-4 недели.
|
||||||
|
|
||||||
|
Рекомендация: обязательно сделать перед любым масштабированием. Без этого DLP — “best effort” мониторинг, а не надёжная система.
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
- `docs/deployment.md` — пошаговый деплой LXC и ActivityWatch Server.
|
- `docs/deployment.md` — пошаговый деплой LXC и ActivityWatch Server.
|
||||||
- `docs/runbook.md` — быстрый runbook для оператора.
|
- `docs/runbook.md` — быстрый runbook для оператора.
|
||||||
- `docs/operations.md` — регламент сопровождения, бэкапов, обновлений и rollback.
|
- `docs/operations.md` — регламент сопровождения, бэкапов, обновлений и rollback.
|
||||||
|
- `docs/artifacts-policy.md` — политика generated-артефактов и rollout-gates.
|
||||||
- `docs/windows/ensemble.md` — orchestration-пакет для Windows-деплоя и проверки.
|
- `docs/windows/ensemble.md` — orchestration-пакет для Windows-деплоя и проверки.
|
||||||
- `docs/linux-client.md` — user-space rollout Linux-клиента ActivityWatch на удалённый `AW server`.
|
- `docs/linux-client.md` — user-space rollout Linux-клиента ActivityWatch на удалённый `AW server`.
|
||||||
- `docs/linux-remote-worker.md` — полный Linux remote-worker stack: GUI, SSH/console и browser admin UI вроде Proxmox `:8006`.
|
- `docs/linux-remote-worker.md` — полный Linux remote-worker stack: GUI, SSH/console и browser admin UI вроде Proxmox `:8006`.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
# shellcheck disable=SC1007
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
REPO_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
REPO_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
# shellcheck disable=SC1007
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
REPO_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
REPO_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
|||||||
@@ -160,6 +160,9 @@
|
|||||||
{% if (aw_windows_package_zip_path | default('') | string | length) > 0 %}
|
{% if (aw_windows_package_zip_path | default('') | string | length) > 0 %}
|
||||||
$params.PackageZipPath = "{{ aw_windows_package_zip_path }}"
|
$params.PackageZipPath = "{{ aw_windows_package_zip_path }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if (aw_windows_hostname_override | default('') | string | length) > 0 %}
|
||||||
|
$params.AwHostname = "{{ aw_windows_hostname_override }}"
|
||||||
|
{% endif %}
|
||||||
{% if aw_windows_skip_hardening | bool %}
|
{% if aw_windows_skip_hardening | bool %}
|
||||||
$params.SkipHardening = $true
|
$params.SkipHardening = $true
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -183,6 +186,44 @@
|
|||||||
ansible.windows.win_powershell:
|
ansible.windows.win_powershell:
|
||||||
script: |
|
script: |
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Get-CollectorKey {
|
||||||
|
param([string]$CommandLine)
|
||||||
|
if (-not $CommandLine) { return $null }
|
||||||
|
$cl = $CommandLine.ToLowerInvariant()
|
||||||
|
if ($cl -like '*browser-domains-native-collector.ps1*') { return 'browser' }
|
||||||
|
if ($cl -like '*file-operations-collector.ps1*') { return 'fileops' }
|
||||||
|
if ($cl -like '*dlp-endpoint-signals-collector.ps1*') { return 'endpoint' }
|
||||||
|
if ($cl -like '*email-outbound-collector.ps1*') { return 'email' }
|
||||||
|
if ($cl -like '*worktime-session-collector.ps1*') { return 'worktime' }
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$collectorProcs = Get-CimInstance Win32_Process |
|
||||||
|
Where-Object { $_.Name -eq 'powershell.exe' -and $_.CommandLine } |
|
||||||
|
ForEach-Object {
|
||||||
|
$key = Get-CollectorKey -CommandLine $_.CommandLine
|
||||||
|
if ($key) {
|
||||||
|
[pscustomobject]@{
|
||||||
|
ProcessId = [int]$_.ProcessId
|
||||||
|
SessionId = [int]$_.SessionId
|
||||||
|
CreationDate = $_.CreationDate
|
||||||
|
CollectorKey = $key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} |
|
||||||
|
Where-Object { $_ -ne $null }
|
||||||
|
|
||||||
|
# Keep only one process per (collector, session): newest survives, older duplicates are stopped.
|
||||||
|
foreach ($group in ($collectorProcs | Group-Object CollectorKey, SessionId)) {
|
||||||
|
$ordered = @($group.Group | Sort-Object CreationDate -Descending)
|
||||||
|
if ($ordered.Count -le 1) { continue }
|
||||||
|
foreach ($dup in $ordered | Select-Object -Skip 1) {
|
||||||
|
Stop-Process -Id $dup.ProcessId -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
Start-ScheduledTask -TaskName "{{ aw_windows_recovery_task_name }}"
|
Start-ScheduledTask -TaskName "{{ aw_windows_recovery_task_name }}"
|
||||||
Get-ScheduledTask |
|
Get-ScheduledTask |
|
||||||
Where-Object TaskName -like "{{ aw_windows_launch_task_pattern }}" |
|
Where-Object TaskName -like "{{ aw_windows_launch_task_pattern }}" |
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ aw_windows_extra_users: []
|
|||||||
|
|
||||||
aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
|
aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
|
||||||
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
|
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
|
||||||
|
aw_windows_hostname_override: ""
|
||||||
|
|
||||||
aw_windows_afk_enabled: true
|
aw_windows_afk_enabled: true
|
||||||
aw_windows_window_enabled: true
|
aw_windows_window_enabled: true
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ aw_windows_extra_users: []
|
|||||||
# Единые Windows/RDP пути: те же, что использует InnoSetup.
|
# Единые Windows/RDP пути: те же, что использует InnoSetup.
|
||||||
aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
|
aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
|
||||||
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
|
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
|
||||||
|
aw_windows_hostname_override: "" # Например: SHARKON2025
|
||||||
aw_windows_afk_enabled: true
|
aw_windows_afk_enabled: true
|
||||||
aw_windows_window_enabled: true
|
aw_windows_window_enabled: true
|
||||||
aw_windows_file_ops_enabled: true
|
aw_windows_file_ops_enabled: true
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ aw-ct ansible_host=10.20.30.13 ansible_user=root ansible_port=22
|
|||||||
|
|
||||||
[aw_windows]
|
[aw_windows]
|
||||||
# Примечание: в русифицированных Windows часто нужен "Администратор", а не "Administrator".
|
# Примечание: в русифицированных Windows часто нужен "Администратор", а не "Administrator".
|
||||||
win-node1 ansible_host=192.168.100.21 ansible_user=Администратор ansible_password=CHANGE_ME ansible_connection=winrm ansible_winrm_transport=ntlm ansible_port=5985 ansible_winrm_server_cert_validation=ignore
|
win-node1 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]
|
[aw_pfsense_pollers]
|
||||||
# pfsense-poller1 ansible_host=192.168.100.30 ansible_user=root ansible_port=22
|
# pfsense-poller1 ansible_host=192.168.100.30 ansible_user=root ansible_port=22
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
localhost ansible_connection=local ansible_user=root
|
localhost ansible_connection=local ansible_user=root
|
||||||
|
|
||||||
[aw_windows]
|
[aw_windows]
|
||||||
rdp-prod ansible_host=192.168.100.21 ansible_user=Администратор ansible_password=Sergei2009@ ansible_connection=winrm ansible_winrm_transport=ntlm ansible_port=5985 ansible_winrm_server_cert_validation=ignore
|
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
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ echo "=== ActivityWatch Data Check: $HOSTNAME_FILTER ==="
|
|||||||
echo ""
|
echo ""
|
||||||
echo -n "Server connectivity... "
|
echo -n "Server connectivity... "
|
||||||
RESP=$(no_proxy=10.10.10.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/info" 2>&1)
|
RESP=$(no_proxy=10.10.10.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/info" 2>&1)
|
||||||
if [ $? -eq 0 ] && echo "$RESP" | jq -e '.version' > /dev/null 2>&1; then
|
if echo "$RESP" | jq -e '.version' > /dev/null 2>&1; then
|
||||||
VERSION=$(echo "$RESP" | jq -r '.version')
|
VERSION=$(echo "$RESP" | jq -r '.version')
|
||||||
echo -e "${GREEN}OK${NC} (aw-server v$VERSION)"
|
echo -e "${GREEN}OK${NC} (aw-server v$VERSION)"
|
||||||
else
|
else
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ echo ""
|
|||||||
echo -e "${CYAN}--- 1. AW Server ($SERVER) ---${NC}"
|
echo -e "${CYAN}--- 1. AW Server ($SERVER) ---${NC}"
|
||||||
echo -n " Connectivity... "
|
echo -n " Connectivity... "
|
||||||
RESP=$(no_proxy=10.10.10.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/info" 2>&1)
|
RESP=$(no_proxy=10.10.10.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/info" 2>&1)
|
||||||
if [ $? -eq 0 ] && echo "$RESP" | jq -e '.version' > /dev/null 2>&1; then
|
if echo "$RESP" | jq -e '.version' > /dev/null 2>&1; then
|
||||||
VERSION=$(echo "$RESP" | jq -r '.version')
|
VERSION=$(echo "$RESP" | jq -r '.version')
|
||||||
echo -e " ${GREEN}OK${NC} (aw-server $VERSION)"
|
echo -e " ${GREEN}OK${NC} (aw-server $VERSION)"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Artifacts Policy
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Define which files are source-of-truth and which are generated runtime/research artifacts that must not block or pollute production rollouts.
|
||||||
|
|
||||||
|
## Source of Truth
|
||||||
|
|
||||||
|
Tracked and reviewable:
|
||||||
|
|
||||||
|
- `ansible/`
|
||||||
|
- `aw-server/`
|
||||||
|
- `windows/`
|
||||||
|
- `scripts/`
|
||||||
|
- `docs/`
|
||||||
|
- install-kit templates and manifests under `windows/installkit/innosetup/`
|
||||||
|
|
||||||
|
## Generated / Volatile Artifacts
|
||||||
|
|
||||||
|
Not for production commits:
|
||||||
|
|
||||||
|
- `.graphify_*` cache/analysis outputs
|
||||||
|
- `graphify-out/cache/*`
|
||||||
|
- `graphify-out/shellcheck-*.txt`
|
||||||
|
- `graphify-out/validate_dryrun_out*.txt`
|
||||||
|
- `graphify-out/powershell-parse-results*.json`
|
||||||
|
- `graphify-out/powershell-pssa-warn-results.json`
|
||||||
|
- `graphify-out/pssa_diffs.txt`
|
||||||
|
- `reports/*`
|
||||||
|
- `tmp/*`
|
||||||
|
|
||||||
|
These paths are ignored by `.gitignore` and additionally guarded by `scripts/quality-gate.sh`.
|
||||||
|
|
||||||
|
## Rollout Gate
|
||||||
|
|
||||||
|
`scripts/prod_rollout.sh` must run only when:
|
||||||
|
|
||||||
|
1. `AW_MAINTENANCE_ACK=YES` is set.
|
||||||
|
2. `scripts/quality-gate.sh` passes.
|
||||||
|
3. Preflight checks pass:
|
||||||
|
- `ansible ping`/`win_ping`
|
||||||
|
- `./check-aw-data.sh`
|
||||||
|
- `./check-aw-full.sh`
|
||||||
|
|
||||||
|
If any gate fails, rollout stops.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Secrets policy remains temporary by operator choice; credentials may still exist in local `inventory.ini` during this phase.
|
||||||
|
- Dedicated secrets hardening (vault/env-only enforcement) is a separate follow-up track.
|
||||||
@@ -8,6 +8,8 @@ The prototype reads:
|
|||||||
|
|
||||||
- `aw-file-operations_*` (`aw.file.operation`) — file create/delete/rename telemetry, including `archiveHint`.
|
- `aw-file-operations_*` (`aw.file.operation`) — file create/delete/rename telemetry, including `archiveHint`.
|
||||||
- `aw-dlp-incidents_*` (`aw.dlp.incident`) — browser/endpoint DLP incidents and screenshot metadata when available.
|
- `aw-dlp-incidents_*` (`aw.dlp.incident`) — browser/endpoint DLP incidents and screenshot metadata when available.
|
||||||
|
- `aw-dlp-endpoint-signals_*` (`aw.dlp.endpoint.signal`) — endpoint signal heartbeats/events.
|
||||||
|
- `aw-email-monitor_*` (`aw.email.signal`) — outbound email signal stream.
|
||||||
|
|
||||||
## SQLite smoke test
|
## SQLite smoke test
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
- секреты не хранить в git;
|
- секреты не хранить в git;
|
||||||
- каждое изменение фиксировать в ticket/run log;
|
- каждое изменение фиксировать в ticket/run log;
|
||||||
- публичную публикацию делать через отдельный proxy/security layer.
|
- публичную публикацию делать через отдельный proxy/security layer.
|
||||||
|
- generated-артефакты и исследовательские кэши вести по [artifacts-policy.md](/mnt/usb_hdd2/Projects/ActivityWatch-Russian/docs/artifacts-policy.md).
|
||||||
|
|
||||||
## Регулярные проверки
|
## Регулярные проверки
|
||||||
|
|
||||||
@@ -124,3 +125,4 @@ systemctl restart activitywatch-server.service
|
|||||||
- не обновлять поверх рабочего бинарника без backup;
|
- не обновлять поверх рабочего бинарника без backup;
|
||||||
- не открывать `5600/tcp` наружу без отдельной защиты;
|
- не открывать `5600/tcp` наружу без отдельной защиты;
|
||||||
- не править `index.html` вручную без backup.
|
- не править `index.html` вручную без backup.
|
||||||
|
- не запускать `scripts/prod_rollout.sh` без `AW_MAINTENANCE_ACK=YES`.
|
||||||
|
|||||||
@@ -196,6 +196,15 @@ systemctl restart activitywatch-server.service
|
|||||||
|
|
||||||
## Перед любыми изменениями
|
## Перед любыми изменениями
|
||||||
|
|
||||||
|
0. Подтвердить maintenance window и gate:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export AW_MAINTENANCE_ACK=YES
|
||||||
|
./scripts/quality-gate.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Если `quality-gate` падает (например, drift install-kit vs repo), rollout не запускать.
|
||||||
|
|
||||||
1. Сделать snapshot или `vzdump`.
|
1. Сделать snapshot или `vzdump`.
|
||||||
2. Сохранить текущий `/etc/activitywatch/aw-server.env`.
|
2. Сохранить текущий `/etc/activitywatch/aw-server.env`.
|
||||||
3. Сохранить текущий `index.html`.
|
3. Сохранить текущий `index.html`.
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
# ActivityWatch-Russian Knowledge Graph Report
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
- **Total Nodes**: 404
|
|
||||||
- **Total Edges**: 933
|
|
||||||
- **Communities**: 27
|
|
||||||
- **Source**: AST extraction (code-only corpus)
|
|
||||||
|
|
||||||
## Communities by Size
|
|
||||||
|
|
||||||
### Community 1 (62 nodes)
|
|
||||||
- install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1
|
|
||||||
- dlp_endpoint_signals_collector_get_deploymentconfig
|
|
||||||
- dlp_endpoint_signals_collector_write_endpointlog
|
|
||||||
- dlp_endpoint_signals_collector_invoke_awjsonpost
|
|
||||||
- dlp_endpoint_signals_collector_ensure_bucket
|
|
||||||
- ... and 57 more
|
|
||||||
|
|
||||||
### Community 2 (56 nodes)
|
|
||||||
- aw_server_aw_ru_patch_js
|
|
||||||
- aw_ru_patch_injectstyles
|
|
||||||
- aw_ru_patch_hidenoisenavigation
|
|
||||||
- aw_ru_patch_getcurrenthostfromhash
|
|
||||||
- aw_ru_patch_ispvelikehost
|
|
||||||
- ... and 51 more
|
|
||||||
|
|
||||||
### Community 3 (54 nodes)
|
|
||||||
- install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1
|
|
||||||
- browser_domains_native_collector_get_deploymentconfig
|
|
||||||
- browser_domains_native_collector_write_collectorlog
|
|
||||||
- browser_domains_native_collector_write_dlpincidentlog
|
|
||||||
- browser_domains_native_collector_test_domainmatch
|
|
||||||
- ... and 49 more
|
|
||||||
|
|
||||||
### Community 0 (38 nodes)
|
|
||||||
- scripts_aggregate_dlp_events_py
|
|
||||||
- aggregate_dlp_events_bucket
|
|
||||||
- aggregate_dlp_events_awevent
|
|
||||||
- aggregate_dlp_events_psycopgconnection
|
|
||||||
- protocol
|
|
||||||
- ... and 33 more
|
|
||||||
|
|
||||||
### Community 5 (34 nodes)
|
|
||||||
- aw_ru_patch_isdlpsignalbucketroute
|
|
||||||
- aw_ru_patch_getdlphostfrombucketid
|
|
||||||
- aw_ru_patch_builddlpkey
|
|
||||||
- aw_ru_patch_loadbucketevents
|
|
||||||
- aw_ru_patch_serializerulematch
|
|
||||||
- ... and 29 more
|
|
||||||
|
|
||||||
### Community 4 (34 nodes)
|
|
||||||
- install_kit_awindows_20260427_211240_windows_email_outbound_collector_ps1
|
|
||||||
- email_outbound_collector_get_deploymentconfig
|
|
||||||
- email_outbound_collector_write_collectorlog
|
|
||||||
- email_outbound_collector_invoke_awjsonpost
|
|
||||||
- email_outbound_collector_ensure_bucket
|
|
||||||
- ... and 29 more
|
|
||||||
|
|
||||||
### Community 8 (28 nodes)
|
|
||||||
- aw_ru_patch_replacetext
|
|
||||||
- aw_ru_patch_walk
|
|
||||||
- aw_ru_patch_translateattributes
|
|
||||||
- aw_ru_patch_ishomeroute
|
|
||||||
- aw_ru_patch_getdefaulthostgroupsconfig
|
|
||||||
- ... and 23 more
|
|
||||||
|
|
||||||
### Community 9 (18 nodes)
|
|
||||||
- aw_ru_patch_getdlpbucketidfromhash
|
|
||||||
- aw_ru_patch_generatedlpid
|
|
||||||
- aw_ru_patch_awapijson
|
|
||||||
- aw_ru_patch_ensureawbucket
|
|
||||||
- aw_ru_patch_saveawheartbeat
|
|
||||||
- ... and 13 more
|
|
||||||
|
|
||||||
### Community 10 (14 nodes)
|
|
||||||
- install_kit_awindows_20260427_211240_windows_migrate_awatch_rus_paths_ps1
|
|
||||||
- migrate_awatch_rus_paths_copy_directorycontents
|
|
||||||
- migrate_awatch_rus_paths_copy_ifexists
|
|
||||||
- migrate_awatch_rus_paths_convert_pathvalue
|
|
||||||
- migrate_awatch_rus_paths_stop_awatchtaskset
|
|
||||||
- ... and 9 more
|
|
||||||
|
|
||||||
### Community 6 (12 nodes)
|
|
||||||
- grafana_1c_sql_exporter_collectors_aw_activitywatch_py
|
|
||||||
- aw_activitywatch_activitywatchexporter
|
|
||||||
- aw_activitywatch_activitywatchexporter_init
|
|
||||||
- aw_activitywatch_activitywatchexporter_get_buckets
|
|
||||||
- aw_activitywatch_activitywatchexporter_get_bucket_events
|
|
||||||
- ... and 7 more
|
|
||||||
|
|
||||||
## File Types
|
|
||||||
The graph was built from code files including:
|
|
||||||
- PowerShell scripts (.ps1)
|
|
||||||
- Python scripts (.py)
|
|
||||||
- JavaScript patches (.js)
|
|
||||||
- Configuration files
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- This is a structural (AST-based) graph showing code relationships
|
|
||||||
- No semantic extraction was performed (no docs/papers in corpus)
|
|
||||||
- Communities represent clusters of related functions and modules
|
|
||||||
File diff suppressed because one or more lines are too long
-11645
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,15 @@ cd ansible
|
|||||||
ansible-playbook -i inventory.ini deploy_aw_server.yml
|
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 (всё за один запуск)
|
## Полный установочный playbook (всё за один запуск)
|
||||||
|
|
||||||
Если нужно прогнать полный цикл одной командой:
|
Если нужно прогнать полный цикл одной командой:
|
||||||
@@ -149,3 +158,13 @@ Playbook:
|
|||||||
- Для полного сценария CT создаётся автоматически через `pct create`.
|
- Для полного сценария CT создаётся автоматически через `pct create`.
|
||||||
- На Windows/RDP host развёрнуты AFK/window watchers, browser domain collector, DLP endpoint collector и worktime session collector.
|
- На Windows/RDP host развёрнуты AFK/window watchers, browser domain collector, DLP endpoint collector и worktime session collector.
|
||||||
- Проверочный JSON-отчёт Windows playbook должен иметь `overallOk=true`.
|
- Проверочный JSON-отчёт Windows playbook должен иметь `overallOk=true`.
|
||||||
|
|
||||||
|
## Prod rollout одной командой
|
||||||
|
|
||||||
|
Для ручного запуска с dry-run и логированием используйте:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/prod_rollout.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт попросит `AW_SSH_PASSWORD` и `AW_WINRM_PASSWORD` интерактивно (ввод скрыт) и сложит логи в `.rollout-logs/`.
|
||||||
|
|||||||
@@ -54,6 +54,11 @@
|
|||||||
- "{{ aw_server_webui_dir }}"
|
- "{{ aw_server_webui_dir }}"
|
||||||
- "{{ aw_server_webui_dir }}/js"
|
- "{{ aw_server_webui_dir }}/js"
|
||||||
- "{{ aw_server_data_dir }}"
|
- "{{ aw_server_data_dir }}"
|
||||||
|
- "{{ aw_server_db_path | dirname }}"
|
||||||
|
- "{{ aw_server_data_dir }}/.config"
|
||||||
|
- "{{ aw_server_data_dir }}/.config/activitywatch"
|
||||||
|
- "{{ aw_server_data_dir }}/.config/activitywatch/aw-server-rust"
|
||||||
|
- "{{ aw_server_data_dir }}/backups"
|
||||||
- "{{ aw_server_log_dir }}"
|
- "{{ aw_server_log_dir }}"
|
||||||
- /etc/activitywatch
|
- /etc/activitywatch
|
||||||
- "{{ aw_bootstrap_dir }}"
|
- "{{ aw_bootstrap_dir }}"
|
||||||
@@ -74,100 +79,147 @@
|
|||||||
- "{{ aw_server_webui_dir }}"
|
- "{{ aw_server_webui_dir }}"
|
||||||
- "{{ aw_server_webui_dir }}/js"
|
- "{{ aw_server_webui_dir }}/js"
|
||||||
- "{{ aw_server_data_dir }}"
|
- "{{ aw_server_data_dir }}"
|
||||||
|
- "{{ aw_server_db_path | dirname }}"
|
||||||
|
- "{{ aw_server_data_dir }}/.config"
|
||||||
|
- "{{ aw_server_data_dir }}/.config/activitywatch"
|
||||||
|
- "{{ aw_server_data_dir }}/.config/activitywatch/aw-server-rust"
|
||||||
|
- "{{ aw_server_data_dir }}/backups"
|
||||||
- "{{ aw_server_log_dir }}"
|
- "{{ aw_server_log_dir }}"
|
||||||
|
|
||||||
- name: Скачать архив релиза ActivityWatch
|
- name: (Check mode) Пропустить установку релиза ActivityWatch
|
||||||
ansible.builtin.get_url:
|
ansible.builtin.debug:
|
||||||
url: "{{ aw_server_download_url }}"
|
msg: "ansible_check_mode=true: download/unarchive/install of ActivityWatch release is skipped."
|
||||||
dest: "{{ aw_archive_path }}"
|
when: ansible_check_mode
|
||||||
mode: "0644"
|
|
||||||
|
|
||||||
- name: Распаковать релиз ActivityWatch
|
- name: Установить релиз ActivityWatch (download/unarchive/install)
|
||||||
ansible.builtin.unarchive:
|
when: not ansible_check_mode
|
||||||
src: "{{ aw_archive_path }}"
|
block:
|
||||||
dest: "{{ aw_release_dir }}"
|
- name: Скачать архив релиза ActivityWatch
|
||||||
remote_src: true
|
ansible.builtin.get_url:
|
||||||
extra_opts: ["-o"]
|
url: "{{ aw_server_download_url }}"
|
||||||
|
dest: "{{ aw_archive_path }}"
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
- name: Найти распакованный каталог ActivityWatch
|
- name: Распаковать релиз ActivityWatch
|
||||||
ansible.builtin.find:
|
ansible.builtin.unarchive:
|
||||||
paths: "{{ aw_release_dir }}"
|
src: "{{ aw_archive_path }}"
|
||||||
file_type: directory
|
dest: "{{ aw_release_dir }}"
|
||||||
patterns: "activitywatch*"
|
remote_src: true
|
||||||
register: aw_release_find
|
extra_opts: ["-o"]
|
||||||
|
|
||||||
- name: Найти бинарный файл AW server
|
- name: Найти распакованный каталог ActivityWatch
|
||||||
ansible.builtin.find:
|
ansible.builtin.find:
|
||||||
paths: "{{ aw_release_dir }}"
|
paths: "{{ aw_release_dir }}"
|
||||||
file_type: file
|
recurse: true
|
||||||
patterns:
|
file_type: directory
|
||||||
- aw-server-rust
|
patterns: "activitywatch*"
|
||||||
- aw-server
|
register: aw_release_find
|
||||||
register: aw_server_binary_find
|
|
||||||
|
|
||||||
- name: Найти каталог WebUI
|
- name: Найти бинарный файл AW server
|
||||||
ansible.builtin.find:
|
ansible.builtin.find:
|
||||||
paths: "{{ aw_release_dir }}"
|
paths: "{{ aw_release_dir }}"
|
||||||
file_type: directory
|
recurse: true
|
||||||
patterns:
|
file_type: file
|
||||||
- aw-webui
|
patterns:
|
||||||
- webui
|
- aw-server-rust
|
||||||
register: aw_webui_dir_find
|
- aw-server
|
||||||
|
register: aw_server_binary_find
|
||||||
|
|
||||||
- name: Сохранить пути распакованного релиза
|
- name: Найти index.html WebUI
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.find:
|
||||||
aw_release_extracted: "{{ (aw_release_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
|
paths: "{{ aw_release_dir }}"
|
||||||
aw_server_binary_path: "{{ (aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
|
recurse: true
|
||||||
aw_webui_source_path: "{{ (aw_webui_dir_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
|
file_type: file
|
||||||
|
patterns:
|
||||||
|
- index.html
|
||||||
|
register: aw_webui_index_find
|
||||||
|
|
||||||
- name: Проверить, что компоненты релиза найдены
|
- name: Сохранить пути распакованного релиза (binary + webui index)
|
||||||
ansible.builtin.assert:
|
ansible.builtin.set_fact:
|
||||||
that:
|
aw_release_extracted: "{{ (aw_release_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
|
||||||
- aw_release_extracted is defined
|
aw_server_binary_path: >-
|
||||||
- aw_release_extracted | length > 0
|
{{
|
||||||
- aw_server_binary_path is defined
|
(
|
||||||
- aw_server_binary_path | length > 0
|
(
|
||||||
- aw_webui_source_path is defined
|
(aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list)
|
||||||
- aw_webui_source_path | length > 0
|
| select('match', '.*/aw-server-rust$') | list | first
|
||||||
fail_msg: "Не удалось найти бинарный файл или WebUI в распакованном релизе ActivityWatch."
|
)
|
||||||
|
| 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: Создать каталог установленного релиза
|
- name: Сохранить каталог WebUI (dirname index.html)
|
||||||
ansible.builtin.file:
|
ansible.builtin.set_fact:
|
||||||
path: "{{ aw_release_install_dir }}"
|
aw_webui_source_path: "{{ aw_webui_index_path | dirname }}"
|
||||||
state: directory
|
|
||||||
owner: "{{ aw_server_user }}"
|
|
||||||
group: "{{ aw_server_group }}"
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
- name: Установить бинарный файл AW server
|
- name: Проверить, что компоненты релиза найдены
|
||||||
ansible.builtin.copy:
|
ansible.builtin.assert:
|
||||||
remote_src: true
|
that:
|
||||||
src: "{{ aw_server_binary_path }}"
|
- aw_release_extracted is defined
|
||||||
dest: "{{ aw_release_install_dir }}/aw-server-rust"
|
- aw_release_extracted | length > 0
|
||||||
owner: "{{ aw_server_user }}"
|
- aw_server_binary_path is defined
|
||||||
group: "{{ aw_server_group }}"
|
- aw_server_binary_path | length > 0
|
||||||
mode: "0755"
|
- aw_webui_source_path is defined
|
||||||
|
- aw_webui_source_path | length > 0
|
||||||
|
fail_msg: "Не удалось найти бинарный файл или WebUI в распакованном релизе ActivityWatch."
|
||||||
|
|
||||||
- name: Создать ссылку на активный бинарный файл AW server
|
- name: Создать каталог установленного релиза
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
src: "{{ aw_release_install_dir }}/aw-server-rust"
|
path: "{{ aw_release_install_dir }}"
|
||||||
dest: /opt/activitywatch/bin/aw-server-rust
|
state: directory
|
||||||
owner: "{{ aw_server_user }}"
|
owner: "{{ aw_server_user }}"
|
||||||
group: "{{ aw_server_group }}"
|
group: "{{ aw_server_group }}"
|
||||||
state: link
|
mode: "0755"
|
||||||
force: true
|
|
||||||
|
|
||||||
- name: Синхронизировать WebUI в RU каталог
|
- name: Установить бинарный файл AW server
|
||||||
ansible.builtin.command:
|
ansible.builtin.copy:
|
||||||
cmd: "rsync -a {{ aw_webui_source_path }}/ {{ aw_server_webui_dir }}/"
|
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
|
- name: Создать ссылку на активный бинарный файл AW server
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: /opt/activitywatch
|
src: "{{ aw_release_install_dir }}/aw-server-rust"
|
||||||
state: directory
|
dest: /opt/activitywatch/bin/aw-server-rust
|
||||||
owner: "{{ aw_server_user }}"
|
owner: "{{ aw_server_user }}"
|
||||||
group: "{{ aw_server_group }}"
|
group: "{{ aw_server_group }}"
|
||||||
recurse: true
|
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 из шаблона репозитория
|
- name: Установить systemd service из шаблона репозитория
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
@@ -184,143 +236,401 @@
|
|||||||
- Перезагрузить systemd
|
- Перезагрузить systemd
|
||||||
- Перезапустить activitywatch
|
- Перезапустить activitywatch
|
||||||
|
|
||||||
- name: Скопировать RU patch файлы WebUI из репозитория
|
- name: (Check mode) Пропустить WebUI patch и запуск сервиса
|
||||||
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-worktime-panel.js", dest: "{{ aw_server_webui_dir }}/js/aw-worktime-panel.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: Создать каталог /root/bootstrap для apply_webui_ru_patch.sh
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: /root/bootstrap
|
|
||||||
state: directory
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
- name: Скопировать RU patch файлы для apply_webui_ru_patch.sh (хотфиксы compiled JS чанков)
|
|
||||||
ansible.builtin.copy:
|
|
||||||
src: "{{ item.src }}"
|
|
||||||
dest: "{{ item.dest }}"
|
|
||||||
mode: "{{ item.mode }}"
|
|
||||||
loop:
|
|
||||||
- { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "/root/bootstrap/aw-ru-patch.js", mode: "0644" }
|
|
||||||
- { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "/root/bootstrap/aw-sw-cleanup.js", mode: "0644" }
|
|
||||||
- { src: "{{ aw_repo_root }}/aw-server/aw-worktime-panel.js", dest: "/root/bootstrap/aw-worktime-panel.js", mode: "0644" }
|
|
||||||
- { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "/root/bootstrap/aw-host-groups.json", mode: "0644" }
|
|
||||||
|
|
||||||
- name: Скопировать apply_webui_ru_patch.sh скрипт
|
|
||||||
ansible.builtin.copy:
|
|
||||||
src: "{{ aw_repo_root }}/aw-server/apply_webui_ru_patch.sh"
|
|
||||||
dest: /opt/activitywatch/aw-server/apply_webui_ru_patch.sh
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
- 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_DB_PATH={{ aw_server_db_path }}
|
|
||||||
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 }}
|
|
||||||
AW_WORKTIME_REPORT_BASE={{ aw_worktime_report_base }}
|
|
||||||
AW_WORKTIME_TZ={{ aw_worktime_timezone }}
|
|
||||||
XDG_DATA_HOME={{ aw_server_data_dir }}/.local/share
|
|
||||||
XDG_CONFIG_HOME={{ aw_server_data_dir }}/.config
|
|
||||||
|
|
||||||
- name: Установить скрипт AW worktime API
|
|
||||||
ansible.builtin.copy:
|
|
||||||
src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.py"
|
|
||||||
dest: /usr/local/bin/aw-worktime-api.py
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
- name: Установить systemd unit AW worktime API
|
|
||||||
ansible.builtin.copy:
|
|
||||||
src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.service"
|
|
||||||
dest: /etc/systemd/system/aw-worktime-api.service
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0644"
|
|
||||||
|
|
||||||
- name: Перезагрузить systemd после установки AW worktime API
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
daemon_reload: true
|
|
||||||
|
|
||||||
- name: Включить и перезапустить AW worktime API
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: aw-worktime-api.service
|
|
||||||
enabled: true
|
|
||||||
state: restarted
|
|
||||||
|
|
||||||
- name: Применить хотфиксы compiled JS чанков (Trends, Timespiral, Category helper)
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: "/opt/activitywatch/aw-server/apply_webui_ru_patch.sh"
|
|
||||||
register: apply_ru_patch_result
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: Вывести результат применения хотфиксов
|
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
msg: "apply_webui_ru_patch.sh: {{ apply_ru_patch_result.stdout }}"
|
msg: "ansible_check_mode=true: WebUI patch + service start + API checks are skipped."
|
||||||
|
when: ansible_check_mode
|
||||||
|
|
||||||
- name: Проверить наличие index.html после копирования
|
- name: Применить WebUI RU patch и запустить сервис
|
||||||
ansible.builtin.stat:
|
when: not ansible_check_mode
|
||||||
path: "{{ aw_server_webui_dir }}/index.html"
|
block:
|
||||||
register: aw_webui_ru_index
|
- 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-worktime-panel.js", dest: "{{ aw_server_webui_dir }}/js/aw-worktime-panel.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
|
- name: Создать каталог /root/bootstrap для apply_webui_ru_patch.sh
|
||||||
ansible.builtin.assert:
|
ansible.builtin.file:
|
||||||
that:
|
path: /root/bootstrap
|
||||||
- aw_webui_ru_index.stat.exists
|
state: directory
|
||||||
fail_msg: "Не найден index.html WebUI для применения RU patch."
|
mode: "0755"
|
||||||
|
|
||||||
- name: Удалить старые теги RU patch из index.html
|
- name: Скопировать RU patch файлы для apply_webui_ru_patch.sh (хотфиксы compiled JS чанков)
|
||||||
ansible.builtin.replace:
|
ansible.builtin.copy:
|
||||||
path: "{{ aw_server_webui_dir }}/index.html"
|
src: "{{ item.src }}"
|
||||||
regexp: '<script[^>]+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*></script>'
|
dest: "{{ item.dest }}"
|
||||||
replace: ''
|
mode: "{{ item.mode }}"
|
||||||
|
loop:
|
||||||
|
- { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "/root/bootstrap/aw-ru-patch.js", mode: "0644" }
|
||||||
|
- { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "/root/bootstrap/aw-sw-cleanup.js", mode: "0644" }
|
||||||
|
- { src: "{{ aw_repo_root }}/aw-server/aw-worktime-panel.js", dest: "/root/bootstrap/aw-worktime-panel.js", mode: "0644" }
|
||||||
|
- { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "/root/bootstrap/aw-host-groups.json", mode: "0644" }
|
||||||
|
|
||||||
- name: Добавить cleanup script RU patch в index.html
|
- name: Скопировать apply_webui_ru_patch.sh скрипт
|
||||||
ansible.builtin.replace:
|
ansible.builtin.copy:
|
||||||
path: "{{ aw_server_webui_dir }}/index.html"
|
src: "{{ aw_repo_root }}/aw-server/apply_webui_ru_patch.sh"
|
||||||
regexp: '</head>'
|
dest: /opt/activitywatch/aw-server/apply_webui_ru_patch.sh
|
||||||
replace: '<script src="/js/sw-cleanup.js?v={{ aw_sw_cleanup_cache_bust }}"></script></head>'
|
mode: "0755"
|
||||||
|
|
||||||
- name: Добавить загрузчик RU patch перед закрытием body
|
- name: Записать /etc/activitywatch/aw-server.env перед хотфиксами
|
||||||
ansible.builtin.replace:
|
ansible.builtin.copy:
|
||||||
path: "{{ aw_server_webui_dir }}/index.html"
|
dest: /etc/activitywatch/aw-server.env
|
||||||
regexp: '</body>'
|
mode: "0640"
|
||||||
replace: '<script defer="defer" src="/js/ru-patch-v5.js?v={{ aw_ru_patch_cache_bust }}"></script></body>'
|
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_DB_PATH={{ aw_server_db_path }}
|
||||||
|
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 }}
|
||||||
|
AW_WORKTIME_REPORT_BASE={{ aw_worktime_report_base }}
|
||||||
|
AW_WORKTIME_TZ={{ aw_worktime_timezone }}
|
||||||
|
XDG_DATA_HOME={{ aw_server_data_dir }}/.local/share
|
||||||
|
XDG_CONFIG_HOME={{ aw_server_data_dir }}/.config
|
||||||
|
|
||||||
- name: Включить и запустить сервис
|
- name: Установить скрипт AW worktime API
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.copy:
|
||||||
name: activitywatch-server.service
|
src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.py"
|
||||||
enabled: true
|
dest: /usr/local/bin/aw-worktime-api.py
|
||||||
state: restarted
|
owner: root
|
||||||
daemon_reload: true
|
group: root
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
- name: Дождаться ответа API
|
- name: Установить systemd unit AW worktime API
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.service"
|
||||||
|
dest: /etc/systemd/system/aw-worktime-api.service
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Установить скрипт AW worktime UI bridge
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ aw_repo_root }}/aw-server/aw-worktime-ui-bridge.py"
|
||||||
|
dest: /usr/local/bin/aw-worktime-ui-bridge.py
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Установить systemd unit AW worktime UI bridge
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ aw_repo_root }}/aw-server/aw-worktime-ui-bridge.service"
|
||||||
|
dest: /etc/systemd/system/aw-worktime-ui-bridge.service
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Установить systemd timer AW worktime UI bridge
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ aw_repo_root }}/aw-server/aw-worktime-ui-bridge.timer"
|
||||||
|
dest: /etc/systemd/system/aw-worktime-ui-bridge.timer
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Перезагрузить systemd после установки AW worktime API
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
|
|
||||||
|
- name: Включить и перезапустить AW worktime API
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: aw-worktime-api.service
|
||||||
|
enabled: true
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
- name: Отключить legacy timer aw-worktime-afk-bridge (если есть)
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: aw-worktime-afk-bridge.timer
|
||||||
|
enabled: false
|
||||||
|
state: stopped
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Включить и перезапустить AW worktime UI bridge timer
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: aw-worktime-ui-bridge.timer
|
||||||
|
enabled: true
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
- name: Выполнить разовый прогон AW worktime UI bridge
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: aw-worktime-ui-bridge.service
|
||||||
|
state: started
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Применить хотфиксы compiled JS чанков (Trends, Timespiral, Category helper)
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: "/opt/activitywatch/aw-server/apply_webui_ru_patch.sh"
|
||||||
|
register: apply_ru_patch_result
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Вывести результат применения хотфиксов
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "apply_webui_ru_patch.sh: {{ apply_ru_patch_result.stdout }}"
|
||||||
|
|
||||||
|
- name: Проверить наличие index.html после копирования
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ aw_server_webui_dir }}/index.html"
|
||||||
|
register: aw_webui_ru_index
|
||||||
|
|
||||||
|
- name: Проверить, что index.html доступен для RU patch
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- aw_webui_ru_index.stat.exists
|
||||||
|
fail_msg: "Не найден index.html WebUI для применения RU patch."
|
||||||
|
|
||||||
|
- name: Удалить старые теги RU patch из index.html
|
||||||
|
ansible.builtin.replace:
|
||||||
|
path: "{{ aw_server_webui_dir }}/index.html"
|
||||||
|
regexp: '<script[^>]+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*></script>'
|
||||||
|
replace: ''
|
||||||
|
|
||||||
|
- name: Добавить cleanup script RU patch в index.html
|
||||||
|
ansible.builtin.replace:
|
||||||
|
path: "{{ aw_server_webui_dir }}/index.html"
|
||||||
|
regexp: '</head>'
|
||||||
|
replace: '<script src="/js/sw-cleanup.js?v={{ aw_sw_cleanup_cache_bust }}"></script></head>'
|
||||||
|
|
||||||
|
- name: Добавить загрузчик RU patch перед закрытием body
|
||||||
|
ansible.builtin.replace:
|
||||||
|
path: "{{ aw_server_webui_dir }}/index.html"
|
||||||
|
regexp: '</body>'
|
||||||
|
replace: '<script defer="defer" src="/js/ru-patch-v5.js?v={{ aw_ru_patch_cache_bust }}"></script></body>'
|
||||||
|
|
||||||
|
- name: Скопировать merge script AW DB на сервер
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ aw_repo_root }}/scripts/merge_aw_server_dbs.py"
|
||||||
|
dest: /usr/local/bin/merge_aw_server_dbs.py
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Проверить наличие legacy root DB
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: /root/.local/share/activitywatch/aw-server-rust/sqlite.db
|
||||||
|
register: aw_legacy_root_db
|
||||||
|
|
||||||
|
- name: Проверить наличие target DB
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ aw_server_db_path }}"
|
||||||
|
register: aw_target_db
|
||||||
|
|
||||||
|
- name: Остановить сервис перед merge server DB
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: activitywatch-server.service
|
||||||
|
state: stopped
|
||||||
|
when: aw_legacy_root_db.stat.exists | default(false)
|
||||||
|
|
||||||
|
- name: Создать backup каталоги server DB
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ aw_server_data_dir }}/backups/db"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ aw_server_user }}"
|
||||||
|
group: "{{ aw_server_group }}"
|
||||||
|
mode: "0755"
|
||||||
|
when: aw_legacy_root_db.stat.exists | default(false)
|
||||||
|
|
||||||
|
- name: Backup target DB перед merge
|
||||||
|
ansible.builtin.copy:
|
||||||
|
remote_src: true
|
||||||
|
src: "{{ aw_server_db_path }}"
|
||||||
|
dest: "{{ aw_server_data_dir }}/backups/db/target-before-merge-{{ ansible_date_time.iso8601_basic_short }}.sqlite.db"
|
||||||
|
owner: "{{ aw_server_user }}"
|
||||||
|
group: "{{ aw_server_group }}"
|
||||||
|
mode: "0644"
|
||||||
|
when:
|
||||||
|
- aw_legacy_root_db.stat.exists | default(false)
|
||||||
|
- aw_target_db.stat.exists | default(false)
|
||||||
|
|
||||||
|
- name: Backup legacy root DB перед merge
|
||||||
|
ansible.builtin.copy:
|
||||||
|
remote_src: true
|
||||||
|
src: /root/.local/share/activitywatch/aw-server-rust/sqlite.db
|
||||||
|
dest: "{{ aw_server_data_dir }}/backups/db/legacy-root-{{ ansible_date_time.iso8601_basic_short }}.sqlite.db"
|
||||||
|
owner: "{{ aw_server_user }}"
|
||||||
|
group: "{{ aw_server_group }}"
|
||||||
|
mode: "0644"
|
||||||
|
when: aw_legacy_root_db.stat.exists | default(false)
|
||||||
|
|
||||||
|
- name: Merge legacy root DB в target DB
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- python3
|
||||||
|
- /usr/local/bin/merge_aw_server_dbs.py
|
||||||
|
- --base
|
||||||
|
- /root/.local/share/activitywatch/aw-server-rust/sqlite.db
|
||||||
|
- --overlay
|
||||||
|
- "{{ aw_server_db_path }}"
|
||||||
|
- --output
|
||||||
|
- "{{ aw_server_db_path }}.merged"
|
||||||
|
when:
|
||||||
|
- aw_legacy_root_db.stat.exists | default(false)
|
||||||
|
- aw_target_db.stat.exists | default(false)
|
||||||
|
|
||||||
|
- name: Install merged DB as active target DB
|
||||||
|
ansible.builtin.copy:
|
||||||
|
remote_src: true
|
||||||
|
src: "{{ aw_server_db_path }}.merged"
|
||||||
|
dest: "{{ aw_server_db_path }}"
|
||||||
|
owner: "{{ aw_server_user }}"
|
||||||
|
group: "{{ aw_server_group }}"
|
||||||
|
mode: "0644"
|
||||||
|
when:
|
||||||
|
- aw_legacy_root_db.stat.exists | default(false)
|
||||||
|
- aw_target_db.stat.exists | default(false)
|
||||||
|
|
||||||
|
- name: Скопировать legacy root DB в target DB если target ещё не существует
|
||||||
|
ansible.builtin.copy:
|
||||||
|
remote_src: true
|
||||||
|
src: /root/.local/share/activitywatch/aw-server-rust/sqlite.db
|
||||||
|
dest: "{{ aw_server_db_path }}"
|
||||||
|
owner: "{{ aw_server_user }}"
|
||||||
|
group: "{{ aw_server_group }}"
|
||||||
|
mode: "0644"
|
||||||
|
when:
|
||||||
|
- aw_legacy_root_db.stat.exists | default(false)
|
||||||
|
- not (aw_target_db.stat.exists | default(false))
|
||||||
|
|
||||||
|
- name: Записать aw-server-rust config.toml с разрешёнными CORS origin
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "{{ aw_server_data_dir }}/.config/activitywatch/aw-server-rust/config.toml"
|
||||||
|
owner: "{{ aw_server_user }}"
|
||||||
|
group: "{{ aw_server_group }}"
|
||||||
|
mode: "0644"
|
||||||
|
content: |
|
||||||
|
cors = [
|
||||||
|
{% for origin in aw_server_cors_origins | default([]) %}
|
||||||
|
"{{ origin }}"{% if not loop.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
]
|
||||||
|
|
||||||
|
- 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: Считать текущие server-side settings
|
||||||
ansible.builtin.uri:
|
ansible.builtin.uri:
|
||||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/info"
|
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/"
|
||||||
method: GET
|
method: GET
|
||||||
status_code: 200
|
status_code: 200
|
||||||
register: aw_api
|
register: aw_settings_current
|
||||||
retries: 10
|
when: aw_apply_worktime_settings | default(false) | bool
|
||||||
delay: 3
|
|
||||||
until: aw_api.status == 200
|
- name: Считать текущие server-side views
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/views"
|
||||||
|
method: GET
|
||||||
|
status_code: 200
|
||||||
|
register: aw_views_current
|
||||||
|
when: aw_apply_worktime_settings | default(false) | bool
|
||||||
|
|
||||||
|
- name: Считать текущие server-side classes
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/classes"
|
||||||
|
method: GET
|
||||||
|
status_code: 200
|
||||||
|
register: aw_classes_current
|
||||||
|
when: aw_apply_worktime_settings | default(false) | bool
|
||||||
|
|
||||||
|
- name: Создать backup текущих server-side settings/views/classes
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "{{ aw_server_data_dir }}/backups/{{ item.name }}-{{ ansible_date_time.iso8601_basic_short }}.json"
|
||||||
|
owner: "{{ aw_server_user }}"
|
||||||
|
group: "{{ aw_server_group }}"
|
||||||
|
mode: "0644"
|
||||||
|
content: "{{ item.payload | to_nice_json }}"
|
||||||
|
loop:
|
||||||
|
- name: settings
|
||||||
|
payload: "{{ aw_settings_current.json | default({}) }}"
|
||||||
|
- name: views
|
||||||
|
payload: "{{ aw_views_current.json | default(none) }}"
|
||||||
|
- name: classes
|
||||||
|
payload: "{{ aw_classes_current.json | default(none) }}"
|
||||||
|
when: aw_apply_worktime_settings | default(false) | bool
|
||||||
|
|
||||||
|
- name: Настроить DLP Aggregator (Phase 2)
|
||||||
|
block:
|
||||||
|
- name: Создать каталог для скриптов
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "/opt/activitywatch/scripts"
|
||||||
|
state: directory
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Скопировать агрегатор событий DLP
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ aw_repo_root }}/scripts/aggregate_dlp_events.py"
|
||||||
|
dest: "/opt/activitywatch/scripts/aggregate_dlp_events.py"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Установить systemd unit для агрегатора
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /etc/systemd/system/activitywatch-dlp-aggregator.service
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=ActivityWatch DLP Event Aggregator
|
||||||
|
After=activitywatch-server.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User={{ aw_server_user }}
|
||||||
|
WorkingDirectory={{ aw_server_data_dir }}
|
||||||
|
ExecStart=/usr/bin/python3 /opt/activitywatch/scripts/aggregate_dlp_events.py \
|
||||||
|
--aw-url http://127.0.0.1:{{ aw_server_port }}/api/0 \
|
||||||
|
--sqlite-path {{ aw_server_data_dir }}/dlp_warehouse.sqlite \
|
||||||
|
--state-path {{ aw_server_data_dir }}/dlp-aggregator-state.json
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
|
- name: Установить systemd timer для агрегатора
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /etc/systemd/system/activitywatch-dlp-aggregator.timer
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=Run ActivityWatch DLP Aggregator every 5 minutes
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=1min
|
||||||
|
OnUnitActiveSec=5min
|
||||||
|
AccuracySec=1s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
|
||||||
|
- name: Включить и запустить таймер агрегатора
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: activitywatch-dlp-aggregator.timer
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
daemon_reload: true
|
||||||
|
|
||||||
- name: Применить базовые worktime settings (classes)
|
- name: Применить базовые worktime settings (classes)
|
||||||
ansible.builtin.uri:
|
ansible.builtin.uri:
|
||||||
@@ -342,16 +652,12 @@
|
|||||||
|
|
||||||
- name: Вычислить worktime durationDefault из aw_worktime_from/to
|
- name: Вычислить worktime durationDefault из aw_worktime_from/to
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
aw_worktime_from_h: "{{ (aw_worktime_from | default('08:00')).split(':')[0] | int }}"
|
|
||||||
aw_worktime_from_m: "{{ (aw_worktime_from | default('08:00')).split(':')[1] | int }}"
|
|
||||||
aw_worktime_to_h: "{{ (aw_worktime_to | default('17:00')).split(':')[0] | int }}"
|
|
||||||
aw_worktime_to_m: "{{ (aw_worktime_to | default('17:00')).split(':')[1] | int }}"
|
|
||||||
aw_worktime_duration_default_derived: >-
|
aw_worktime_duration_default_derived: >-
|
||||||
{{
|
{{
|
||||||
(
|
(
|
||||||
(
|
(
|
||||||
((aw_worktime_to_h | int) * 60 + (aw_worktime_to_m | int)) -
|
(((aw_worktime_to | default('17:00')).split(':')[0] | int) * 60 + ((aw_worktime_to | default('17:00')).split(':')[1] | int)) -
|
||||||
((aw_worktime_from_h | int) * 60 + (aw_worktime_from_m | int))
|
(((aw_worktime_from | default('08:00')).split(':')[0] | int) * 60 + ((aw_worktime_from | default('08:00')).split(':')[1] | int))
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
@@ -379,20 +685,46 @@
|
|||||||
ansible.builtin.uri:
|
ansible.builtin.uri:
|
||||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/startOfDay"
|
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/startOfDay"
|
||||||
method: POST
|
method: POST
|
||||||
body: "{{ aw_worktime_start_of_day }}"
|
body: "\"{{ aw_worktime_start_of_day }}\""
|
||||||
body_format: json
|
headers:
|
||||||
status_code: 200
|
Content-Type: application/json
|
||||||
|
status_code: [200, 201]
|
||||||
when: aw_apply_worktime_settings | default(false) | bool
|
when: aw_apply_worktime_settings | default(false) | bool
|
||||||
|
|
||||||
- name: Применить базовый период worktime (durationDefault seconds)
|
- name: Применить базовый период worktime (durationDefault seconds)
|
||||||
ansible.builtin.uri:
|
ansible.builtin.uri:
|
||||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/durationDefault"
|
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/durationDefault"
|
||||||
method: POST
|
method: POST
|
||||||
body: "{{ aw_worktime_duration_default_effective }}"
|
body: "{{ aw_worktime_duration_default_effective | string }}"
|
||||||
body_format: json
|
headers:
|
||||||
status_code: 200
|
Content-Type: application/json
|
||||||
|
status_code: [200, 201]
|
||||||
when: aw_apply_worktime_settings | default(false) | bool
|
when: aw_apply_worktime_settings | default(false) | bool
|
||||||
|
|
||||||
|
- name: Применить always_active_pattern для fallback без AFK
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/always_active_pattern"
|
||||||
|
method: POST
|
||||||
|
body: "\"{{ aw_server_always_active_pattern }}\""
|
||||||
|
headers:
|
||||||
|
Content-Type: application/json
|
||||||
|
status_code: [200, 201]
|
||||||
|
when:
|
||||||
|
- aw_apply_worktime_settings | default(false) | bool
|
||||||
|
- (aw_server_always_active_pattern | default('') | string | length) > 0
|
||||||
|
|
||||||
|
- name: Применить landingpage профиля
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/landingpage"
|
||||||
|
method: POST
|
||||||
|
body: "\"{{ aw_server_landingpage }}\""
|
||||||
|
headers:
|
||||||
|
Content-Type: application/json
|
||||||
|
status_code: [200, 201]
|
||||||
|
when:
|
||||||
|
- aw_apply_worktime_settings | default(false) | bool
|
||||||
|
- (aw_server_landingpage | default('') | string | length) > 0
|
||||||
|
|
||||||
handlers:
|
handlers:
|
||||||
- name: Перезагрузить systemd
|
- name: Перезагрузить systemd
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.systemd:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
|
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
|
||||||
aw_windows_afk_enabled: true
|
aw_windows_afk_enabled: true
|
||||||
aw_windows_window_enabled: true
|
aw_windows_window_enabled: true
|
||||||
|
aw_windows_file_ops_enabled: true
|
||||||
aw_windows_local_agent_logs_enabled: false
|
aw_windows_local_agent_logs_enabled: false
|
||||||
aw_windows_incident_capture_enabled: true
|
aw_windows_incident_capture_enabled: true
|
||||||
aw_windows_incident_screenshot_enabled: true
|
aw_windows_incident_screenshot_enabled: true
|
||||||
@@ -78,6 +79,7 @@
|
|||||||
- browser-domains-native-collector.ps1
|
- browser-domains-native-collector.ps1
|
||||||
- dlp-endpoint-signals-collector.ps1
|
- dlp-endpoint-signals-collector.ps1
|
||||||
- email-outbound-collector.ps1
|
- email-outbound-collector.ps1
|
||||||
|
- file-operations-collector.ps1
|
||||||
- worktime-session-collector.ps1
|
- worktime-session-collector.ps1
|
||||||
- migrate-awatch-rus-paths.ps1
|
- migrate-awatch-rus-paths.ps1
|
||||||
- deploy-domain-users.ps1
|
- deploy-domain-users.ps1
|
||||||
@@ -87,6 +89,18 @@
|
|||||||
- web-category-rules.example.json
|
- web-category-rules.example.json
|
||||||
- dlp-policy.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: Загрузить список пользователей для доменного развёртывания
|
- name: Загрузить список пользователей для доменного развёртывания
|
||||||
ansible.windows.win_copy:
|
ansible.windows.win_copy:
|
||||||
dest: "{{ aw_windows_deploy_root }}\\windows\\users.txt"
|
dest: "{{ aw_windows_deploy_root }}\\windows\\users.txt"
|
||||||
@@ -131,6 +145,7 @@
|
|||||||
StateRoot = "{{ aw_windows_state_root }}"
|
StateRoot = "{{ aw_windows_state_root }}"
|
||||||
AfkEnabled = {{ '$true' if (aw_windows_afk_enabled | bool) else '$false' }}
|
AfkEnabled = {{ '$true' if (aw_windows_afk_enabled | bool) else '$false' }}
|
||||||
WindowEnabled = {{ '$true' if (aw_windows_window_enabled | bool) else '$false' }}
|
WindowEnabled = {{ '$true' if (aw_windows_window_enabled | bool) else '$false' }}
|
||||||
|
FileOpsEnabled = {{ '$true' if (aw_windows_file_ops_enabled | bool) else '$false' }}
|
||||||
LocalAgentLogsEnabled = {{ '$true' if (aw_windows_local_agent_logs_enabled | bool) else '$false' }}
|
LocalAgentLogsEnabled = {{ '$true' if (aw_windows_local_agent_logs_enabled | bool) else '$false' }}
|
||||||
IncidentCaptureEnabled = {{ '$true' if (aw_windows_incident_capture_enabled | bool) else '$false' }}
|
IncidentCaptureEnabled = {{ '$true' if (aw_windows_incident_capture_enabled | bool) else '$false' }}
|
||||||
IncidentScreenshotEnabled = {{ '$true' if (aw_windows_incident_screenshot_enabled | bool) else '$false' }}
|
IncidentScreenshotEnabled = {{ '$true' if (aw_windows_incident_screenshot_enabled | bool) else '$false' }}
|
||||||
@@ -150,6 +165,19 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
& "{{ aw_windows_deploy_root }}\windows\deploy-ensemble.ps1" @params
|
& "{{ aw_windows_deploy_root }}\windows\deploy-ensemble.ps1" @params
|
||||||
|
|
||||||
|
- name: Удалить лишние ActivityWatch Launch tasks вне текущего deployment-config
|
||||||
|
ansible.windows.win_powershell:
|
||||||
|
script: |
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$config = Get-Content -Raw -LiteralPath "{{ aw_windows_state_root }}\deployment-config.json" | ConvertFrom-Json
|
||||||
|
$desired = @($config.userTasks | ForEach-Object { [string]$_.LaunchTaskName })
|
||||||
|
foreach ($task in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch *' })) {
|
||||||
|
if ($desired -notcontains [string]$task.TaskName) {
|
||||||
|
Unregister-ScheduledTask -TaskName $task.TaskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||||
|
& cmd.exe /c "schtasks /Delete /TN `"$($task.TaskName)`" /F >nul 2>&1" | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
- name: Принудительно запустить ActivityWatch recovery и launch tasks
|
- name: Принудительно запустить ActivityWatch recovery и launch tasks
|
||||||
when: aw_windows_force_task_restart | bool
|
when: aw_windows_force_task_restart | bool
|
||||||
ansible.windows.win_powershell:
|
ansible.windows.win_powershell:
|
||||||
@@ -172,6 +200,7 @@
|
|||||||
when:
|
when:
|
||||||
- aw_windows_api_smoke_check_enabled | bool
|
- aw_windows_api_smoke_check_enabled | bool
|
||||||
- aw_windows_afk_enabled | bool
|
- aw_windows_afk_enabled | bool
|
||||||
|
- aw_windows_hostname_result.stdout is defined
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
aw_windows_api_smoke_check_bucket_effective: >-
|
aw_windows_api_smoke_check_bucket_effective: >-
|
||||||
{{
|
{{
|
||||||
@@ -180,54 +209,51 @@
|
|||||||
else 'aw-watcher-afk_' ~ (aw_windows_hostname_result.stdout | trim)
|
else 'aw-watcher-afk_' ~ (aw_windows_hostname_result.stdout | trim)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
- name: Дождаться свежих AFK событий на AW server
|
- name: Выполнить AW API smoke-check (проверка наличия свежих событий в AFK бакете)
|
||||||
when:
|
when:
|
||||||
- aw_windows_api_smoke_check_enabled | bool
|
- aw_windows_api_smoke_check_enabled | bool
|
||||||
- aw_windows_afk_enabled | bool
|
- aw_windows_afk_enabled | bool
|
||||||
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 }}:{{ 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
|
||||||
return_content: true
|
status_code: 200
|
||||||
register: aw_windows_api_smoke
|
register: aw_windows_api_smoke_result
|
||||||
until: >
|
until: aw_windows_api_smoke_result.json | length > 0
|
||||||
aw_windows_api_smoke.status == 200 and
|
retries: 5
|
||||||
(aw_windows_api_smoke.json | length) > 0 and
|
delay: 5
|
||||||
(
|
ignore_errors: true
|
||||||
aw_windows_api_smoke.json
|
|
||||||
| selectattr('data.status', 'equalto', 'not-afk')
|
|
||||||
| list
|
|
||||||
| length
|
|
||||||
) > 0
|
|
||||||
retries: 10
|
|
||||||
delay: 6
|
|
||||||
|
|
||||||
- name: Выполнить валидацию и сохранить отчёт на целевом Windows host
|
- name: Валидировать развёртывание на эндпоинте
|
||||||
ansible.windows.win_powershell:
|
ansible.windows.win_powershell:
|
||||||
script: |
|
script: |
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$report = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" `
|
$result = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" `
|
||||||
-ConfigPath "{{ aw_windows_state_root }}\deployment-config.json"
|
-ConfigPath "{{ aw_windows_state_root }}\deployment-config.json"
|
||||||
$report | ConvertTo-Json -Depth 12 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8
|
$result | ConvertTo-Json -Depth 8 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8
|
||||||
if ({{ '$true' if (aw_windows_fail_on_validation_error | bool) else '$false' }} -and -not [bool]$report.overallOk) {
|
return $result
|
||||||
throw "Проверка развёртывания ActivityWatch завершилась ошибкой. Отчёт: {{ aw_windows_validation_remote_path }}"
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Создать локальный каталог для validation reports
|
- name: Создать локальную директорию для отчётов валидации
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ aw_windows_validation_local_dir }}"
|
path: "{{ aw_windows_validation_local_dir }}"
|
||||||
state: directory
|
state: directory
|
||||||
mode: "0755"
|
mode: "0755"
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|
||||||
- name: Забрать validation report
|
- name: Стянуть отчёт валидации с эндпоинта
|
||||||
ansible.builtin.fetch:
|
ansible.builtin.fetch:
|
||||||
src: "{{ aw_windows_validation_remote_path }}"
|
src: "{{ aw_windows_validation_remote_path }}"
|
||||||
dest: "{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
|
dest: "{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
|
||||||
flat: true
|
flat: true
|
||||||
|
|
||||||
- name: Показать путь к отчёту
|
- name: Проверить статус валидации
|
||||||
ansible.builtin.debug:
|
ansible.builtin.shell: |
|
||||||
msg:
|
python3 - <<'PY'
|
||||||
- "Windows/RDP развёртывание завершено на {{ inventory_hostname }}."
|
import json, sys
|
||||||
- "Отчёт проверки: {{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
|
with open('{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json', 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if not data.get('overallOk', False):
|
||||||
|
print(f"Validation failed for {{ inventory_hostname }}: {data.get('summary', 'Unknown error')}")
|
||||||
|
sys.exit(1)
|
||||||
|
PY
|
||||||
|
delegate_to: localhost
|
||||||
|
when: aw_windows_fail_on_validation_error | bool
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ aw_server_bind_host: "0.0.0.0"
|
|||||||
aw_server_port: 5600
|
aw_server_port: 5600
|
||||||
aw_server_webui_dir: "/opt/activitywatch/webui-ru"
|
aw_server_webui_dir: "/opt/activitywatch/webui-ru"
|
||||||
aw_server_data_dir: "/var/lib/activitywatch"
|
aw_server_data_dir: "/var/lib/activitywatch"
|
||||||
|
aw_server_db_path: "/var/lib/activitywatch/.local/share/activitywatch/aw-server-rust/sqlite.db"
|
||||||
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"
|
||||||
@@ -12,9 +13,17 @@ aw_worktime_timezone: "Europe/Moscow"
|
|||||||
|
|
||||||
aw_repo_root: "{{ playbook_dir | dirname }}"
|
aw_repo_root: "{{ playbook_dir | dirname }}"
|
||||||
|
|
||||||
# Опционально: применить базовые категории и views для рабочего времени через AW settings API.
|
# Применить базовые категории и views для рабочего времени через AW settings API.
|
||||||
# Внимание: это перезаписывает существующие server-side settings/classes/views.
|
# При прод-обновлениях это нужно оставлять включённым, иначе UI остаётся без views/classes.
|
||||||
aw_apply_worktime_settings: false
|
aw_apply_worktime_settings: true
|
||||||
|
|
||||||
|
# Дополнительные origin для aw-server-rust CORS.
|
||||||
|
# Обязательно включите тот origin, с которого реально открывается Web UI.
|
||||||
|
aw_server_cors_origins:
|
||||||
|
- "http://127.0.0.1:5600"
|
||||||
|
- "http://localhost:5600"
|
||||||
|
- "http://10.10.10.13:5600"
|
||||||
|
- "http://aw-server:5600"
|
||||||
|
|
||||||
# Опциональные значения периода рабочего времени в Web UI.
|
# Опциональные значения периода рабочего времени в Web UI.
|
||||||
# startOfDay задаёт границу дня и стартовое время окна отчёта.
|
# startOfDay задаёт границу дня и стартовое время окна отчёта.
|
||||||
@@ -24,3 +33,5 @@ aw_apply_worktime_settings: false
|
|||||||
aw_worktime_from: "08:00"
|
aw_worktime_from: "08:00"
|
||||||
aw_worktime_to: "17:00"
|
aw_worktime_to: "17:00"
|
||||||
aw_worktime_start_of_day: "{{ aw_worktime_from }}"
|
aw_worktime_start_of_day: "{{ aw_worktime_from }}"
|
||||||
|
aw_server_always_active_pattern: "aw-watcher-window"
|
||||||
|
aw_server_landingpage: "/activity/SHARKON2025/view/"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
|
|||||||
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
|
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
|
||||||
aw_windows_afk_enabled: true
|
aw_windows_afk_enabled: true
|
||||||
aw_windows_window_enabled: true
|
aw_windows_window_enabled: true
|
||||||
|
aw_windows_file_ops_enabled: true
|
||||||
aw_windows_local_agent_logs_enabled: false
|
aw_windows_local_agent_logs_enabled: false
|
||||||
aw_windows_incident_capture_enabled: true
|
aw_windows_incident_capture_enabled: true
|
||||||
aw_windows_incident_screenshot_enabled: true
|
aw_windows_incident_screenshot_enabled: true
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
- activitywatch-server.service
|
- activitywatch-server.service
|
||||||
- aw-worktime-api.py
|
- aw-worktime-api.py
|
||||||
- aw-worktime-api.service
|
- aw-worktime-api.service
|
||||||
|
- aw-worktime-ui-bridge.py
|
||||||
|
- aw-worktime-ui-bridge.service
|
||||||
|
- aw-worktime-ui-bridge.timer
|
||||||
- aw-worktime-panel.js
|
- aw-worktime-panel.js
|
||||||
- aw-server.env.example
|
- aw-server.env.example
|
||||||
- aw-ru-patch.js
|
- aw-ru-patch.js
|
||||||
|
|||||||
+3
@@ -11,6 +11,9 @@
|
|||||||
- activitywatch-server.service
|
- activitywatch-server.service
|
||||||
- aw-worktime-api.py
|
- aw-worktime-api.py
|
||||||
- aw-worktime-api.service
|
- aw-worktime-api.service
|
||||||
|
- aw-worktime-ui-bridge.py
|
||||||
|
- aw-worktime-ui-bridge.service
|
||||||
|
- aw-worktime-ui-bridge.timer
|
||||||
- aw-worktime-panel.js
|
- aw-worktime-panel.js
|
||||||
- aw-server.env.example
|
- aw-server.env.example
|
||||||
- aw-ru-patch.js
|
- aw-ru-patch.js
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ EnvironmentFile=/etc/activitywatch/aw-server.env
|
|||||||
User=__AW_SERVER_USER__
|
User=__AW_SERVER_USER__
|
||||||
Group=__AW_SERVER_GROUP__
|
Group=__AW_SERVER_GROUP__
|
||||||
WorkingDirectory=__AW_SERVER_DATA_DIR__
|
WorkingDirectory=__AW_SERVER_DATA_DIR__
|
||||||
ExecStart=/bin/sh -lc 'exec /opt/activitywatch/bin/aw-server-rust --host "$AW_SERVER_BIND_HOST" --port "$AW_SERVER_PORT"'
|
ExecStart=/bin/sh -lc 'exec /opt/activitywatch/bin/aw-server-rust --host "$AW_SERVER_BIND_HOST" --port "$AW_SERVER_PORT" --dbpath "$AW_SERVER_DB_PATH" --webpath "$AW_SERVER_WEBUI_DIR"'
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
StateDirectory=activitywatch
|
StateDirectory=activitywatch
|
||||||
@@ -17,7 +17,7 @@ LogsDirectory=activitywatch
|
|||||||
NoNewPrivileges=true
|
NoNewPrivileges=true
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
ProtectSystem=full
|
ProtectSystem=full
|
||||||
ProtectHome=true
|
ProtectHome=read-only
|
||||||
LimitNOFILE=65535
|
LimitNOFILE=65535
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
|||||||
@@ -29,6 +29,21 @@
|
|||||||
{ "label": "DLP", "type": "bucket", "bucket_prefix": "aw-dlp-endpoint-signals_" }
|
{ "label": "DLP", "type": "bucket", "bucket_prefix": "aw-dlp-endpoint-signals_" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "linux-remote",
|
||||||
|
"name": "Linux remote workers",
|
||||||
|
"description": "Linux-хосты удалённых сотрудников: GUI активность, SSH/console и browser admin UI.",
|
||||||
|
"patterns": [
|
||||||
|
"^(LINUX-WS|LINUX-DESKTOP|LX-|DESKTOP-|ADMIN-|WORKSTATION-|DEVBOX-)"
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{ "label": "Активность", "type": "activity" },
|
||||||
|
{ "label": "SSH сессии", "type": "bucket", "bucket_prefix": "aw-ssh-sessions_" },
|
||||||
|
{ "label": "Команды shell", "type": "bucket", "bucket_prefix": "aw-console-commands_" },
|
||||||
|
{ "label": "Web категории", "type": "bucket", "bucket_prefix": "aw-detmir-web-category_" },
|
||||||
|
{ "label": "Все бакеты", "type": "buckets" }
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "virtual-infra",
|
"id": "virtual-infra",
|
||||||
"name": "Virtual servers + Proxmox",
|
"name": "Virtual servers + Proxmox",
|
||||||
|
|||||||
@@ -370,6 +370,16 @@
|
|||||||
return /^pve[-_]/i.test(String(host || ""));
|
return /^pve[-_]/i.test(String(host || ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLikelyClientHost(host) {
|
||||||
|
const value = String(host || "").trim();
|
||||||
|
if (!value) return false;
|
||||||
|
if (/^(?:unknown|undefined|null)$/i.test(value)) return false;
|
||||||
|
if (/^(?:localhost|127\.0\.0\.1|0\.0\.0\.0|::1)$/i.test(value)) return false;
|
||||||
|
if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(value)) return false;
|
||||||
|
if (value.indexOf(":") !== -1 && /^[0-9a-f:\[\]]+$/i.test(value)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function enforceSafeActivityViewForPveHost() {
|
function enforceSafeActivityViewForPveHost() {
|
||||||
const hash = window.location.hash || "";
|
const hash = window.location.hash || "";
|
||||||
const match = hash.match(/^#\/activity\/([^/]+)\/day\/([^/]+)\/view\/([^/?#]+)/i);
|
const match = hash.match(/^#\/activity\/([^/]+)\/day\/([^/]+)\/view\/([^/?#]+)/i);
|
||||||
@@ -386,9 +396,9 @@
|
|||||||
|
|
||||||
function getDlpHostFromSettings(settings) {
|
function getDlpHostFromSettings(settings) {
|
||||||
const routeHost = getCurrentHostFromHash();
|
const routeHost = getCurrentHostFromHash();
|
||||||
if (routeHost) return routeHost;
|
if (isLikelyClientHost(routeHost)) return routeHost;
|
||||||
const bucketHost = getDlpHostFromBucketId(getDlpBucketIdFromHash());
|
const bucketHost = getDlpHostFromBucketId(getDlpBucketIdFromHash());
|
||||||
if (bucketHost) return bucketHost;
|
if (isLikelyClientHost(bucketHost)) return bucketHost;
|
||||||
return getTrendsHostFromSettings(settings);
|
return getTrendsHostFromSettings(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,6 +690,19 @@
|
|||||||
{ label: "DLP", type: "bucket", bucket_prefix: "aw-dlp-endpoint-signals_" }
|
{ label: "DLP", type: "bucket", bucket_prefix: "aw-dlp-endpoint-signals_" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "linux-remote",
|
||||||
|
name: "Linux remote workers",
|
||||||
|
description: "Linux-хосты удалённых сотрудников: GUI активность, SSH/console и browser admin UI.",
|
||||||
|
patterns: ["^(LINUX-WS|LINUX-DESKTOP|LX-|DESKTOP-|ADMIN-|WORKSTATION-|DEVBOX-)"],
|
||||||
|
links: [
|
||||||
|
{ label: "Активность", type: "activity" },
|
||||||
|
{ label: "SSH сессии", type: "bucket", bucket_prefix: "aw-ssh-sessions_" },
|
||||||
|
{ label: "Команды shell", type: "bucket", bucket_prefix: "aw-console-commands_" },
|
||||||
|
{ label: "Web категории", type: "bucket", bucket_prefix: "aw-detmir-web-category_" },
|
||||||
|
{ label: "Все бакеты", type: "buckets" }
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "virtual-infra",
|
id: "virtual-infra",
|
||||||
name: "Virtual servers + Proxmox",
|
name: "Virtual servers + Proxmox",
|
||||||
@@ -740,7 +763,15 @@
|
|||||||
const prefixes = [
|
const prefixes = [
|
||||||
"aw-watcher-window_",
|
"aw-watcher-window_",
|
||||||
"aw-watcher-afk_",
|
"aw-watcher-afk_",
|
||||||
|
"aw-console-commands_",
|
||||||
|
"aw-ssh-sessions_",
|
||||||
|
"aw-linux-web-context_",
|
||||||
|
"aw-detmir-web-category_",
|
||||||
"aw-dlp-endpoint-signals_",
|
"aw-dlp-endpoint-signals_",
|
||||||
|
"aw-session-events_",
|
||||||
|
"aw-worktime-sessions_",
|
||||||
|
"aw-pve-webadmin-events_",
|
||||||
|
"aw-pve-task-events_",
|
||||||
"aw-dlp-incidents_",
|
"aw-dlp-incidents_",
|
||||||
"aw-pfsense-health_",
|
"aw-pfsense-health_",
|
||||||
"aw-pfsense-gateways_",
|
"aw-pfsense-gateways_",
|
||||||
@@ -770,7 +801,27 @@
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchHostGroup(host, groups) {
|
function hostHasBucketPrefix(hostBuckets, prefix) {
|
||||||
|
return (hostBuckets || []).some(function (bucketId) {
|
||||||
|
return String(bucketId || "").indexOf(prefix) === 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchHostGroup(host, groups, hostBuckets) {
|
||||||
|
const bucketList = hostBuckets || [];
|
||||||
|
if (hostHasBucketPrefix(bucketList, "aw-dlp-endpoint-signals_") || hostHasBucketPrefix(bucketList, "aw-session-events_")) {
|
||||||
|
return "windows-rdp";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
hostHasBucketPrefix(bucketList, "aw-console-commands_") ||
|
||||||
|
hostHasBucketPrefix(bucketList, "aw-ssh-sessions_") ||
|
||||||
|
hostHasBucketPrefix(bucketList, "aw-linux-web-context_") ||
|
||||||
|
hostHasBucketPrefix(bucketList, "aw-detmir-web-category_")
|
||||||
|
) {
|
||||||
|
if (!hostHasBucketPrefix(bucketList, "aw-pve-webadmin-events_") && !hostHasBucketPrefix(bucketList, "aw-pve-task-events_")) {
|
||||||
|
return "linux-remote";
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
const patterns = Array.isArray(group.patterns) ? group.patterns : [];
|
const patterns = Array.isArray(group.patterns) ? group.patterns : [];
|
||||||
for (const pattern of patterns) {
|
for (const pattern of patterns) {
|
||||||
@@ -813,7 +864,7 @@
|
|||||||
grouped.set("__ungrouped__", []);
|
grouped.set("__ungrouped__", []);
|
||||||
|
|
||||||
Array.from(hostBuckets.keys()).sort().forEach(function (host) {
|
Array.from(hostBuckets.keys()).sort().forEach(function (host) {
|
||||||
const groupId = matchHostGroup(host, groups) || "__ungrouped__";
|
const groupId = matchHostGroup(host, groups, hostBuckets.get(host) || []) || "__ungrouped__";
|
||||||
grouped.get(groupId).push(host);
|
grouped.get(groupId).push(host);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -869,7 +920,7 @@
|
|||||||
center.setAttribute("data-aw-ru-host-groups", "1");
|
center.setAttribute("data-aw-ru-host-groups", "1");
|
||||||
center.innerHTML =
|
center.innerHTML =
|
||||||
'<h4>Разделы хостов</h4>' +
|
'<h4>Разделы хостов</h4>' +
|
||||||
'<p>Здесь хосты разделены на пользовательские Windows RDP и инфраструктурные виртуальные серверы/Proxmox.</p>' +
|
'<p>Здесь хосты разделены на Windows RDP, Linux remote workers и инфраструктурные узлы.</p>' +
|
||||||
'<div class="aw-ru-host-groups-grid" data-aw-ru-host-groups-grid><section class="aw-ru-host-group-card"><p>Загрузка...</p></section></div>';
|
'<div class="aw-ru-host-groups-grid" data-aw-ru-host-groups-grid><section class="aw-ru-host-group-card"><p>Загрузка...</p></section></div>';
|
||||||
heading.parentElement.insertBefore(center, heading.nextSibling);
|
heading.parentElement.insertBefore(center, heading.nextSibling);
|
||||||
}
|
}
|
||||||
@@ -1425,7 +1476,8 @@
|
|||||||
if (!settings || typeof settings !== "object") return "";
|
if (!settings || typeof settings !== "object") return "";
|
||||||
const landingpage = typeof settings.landingpage === "string" ? settings.landingpage : "";
|
const landingpage = typeof settings.landingpage === "string" ? settings.landingpage : "";
|
||||||
const match = landingpage.match(/\/activity\/([^/]+)/);
|
const match = landingpage.match(/\/activity\/([^/]+)/);
|
||||||
return match && match[1] ? match[1] : "";
|
const host = match && match[1] ? decodeURIComponent(match[1]) : "";
|
||||||
|
return isLikelyClientHost(host) ? host : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTrendsPath(hash) {
|
function getTrendsPath(hash) {
|
||||||
@@ -1492,8 +1544,7 @@
|
|||||||
.map(function (bucketId) { return bucketId.replace(/^aw-watcher-window_/i, ""); })
|
.map(function (bucketId) { return bucketId.replace(/^aw-watcher-window_/i, ""); })
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.filter(function (host) { return !/^unknown$/i.test(host); });
|
.filter(function (host) { return !/^unknown$/i.test(host); });
|
||||||
if (settingsHost && hosts.indexOf(settingsHost) >= 0) return settingsHost;
|
if (isLikelyClientHost(settingsHost) && hosts.indexOf(settingsHost) >= 0) return settingsHost;
|
||||||
if (settingsHost) return settingsHost;
|
|
||||||
hosts.sort();
|
hosts.sort();
|
||||||
return hosts[0] || "";
|
return hosts[0] || "";
|
||||||
}
|
}
|
||||||
@@ -1533,7 +1584,8 @@
|
|||||||
window.fetch = function (input, init) {
|
window.fetch = function (input, init) {
|
||||||
try {
|
try {
|
||||||
const url = typeof input === "string" ? input : String(input && input.url || "");
|
const url = typeof input === "string" ? input : String(input && input.url || "");
|
||||||
if (/\/api\/0\/query\/?$/i.test(url) && init && typeof init.body === "string") {
|
const isCategoryBuilderRoute = /^#\/settings\/category-builder(?:[/?#]|$)/i.test(window.location.hash || "");
|
||||||
|
if (isCategoryBuilderRoute && /\/api\/0\/query\/?$/i.test(url) && init && typeof init.body === "string") {
|
||||||
init = Object.assign({}, init, {
|
init = Object.assign({}, init, {
|
||||||
body: rewriteUnknownCategoryBuilderQueryBody(init.body)
|
body: rewriteUnknownCategoryBuilderQueryBody(init.body)
|
||||||
});
|
});
|
||||||
@@ -1557,7 +1609,8 @@
|
|||||||
proto.send = function (body) {
|
proto.send = function (body) {
|
||||||
try {
|
try {
|
||||||
const url = String(this.__awRuUrl || "");
|
const url = String(this.__awRuUrl || "");
|
||||||
if (/\/api\/0\/query\/?$/i.test(url) && typeof body === "string") {
|
const isCategoryBuilderRoute = /^#\/settings\/category-builder(?:[/?#]|$)/i.test(window.location.hash || "");
|
||||||
|
if (isCategoryBuilderRoute && /\/api\/0\/query\/?$/i.test(url) && typeof body === "string") {
|
||||||
body = rewriteUnknownCategoryBuilderQueryBody(body);
|
body = rewriteUnknownCategoryBuilderQueryBody(body);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ VIEWS_JSON="$BOOTSTRAP_DIR/settings/views-default.json"
|
|||||||
CLASSES_JSON="$BOOTSTRAP_DIR/settings/classes-worktime.json"
|
CLASSES_JSON="$BOOTSTRAP_DIR/settings/classes-worktime.json"
|
||||||
WORKTIME_API_SRC="$BOOTSTRAP_DIR/aw-worktime-api.py"
|
WORKTIME_API_SRC="$BOOTSTRAP_DIR/aw-worktime-api.py"
|
||||||
WORKTIME_API_SERVICE_SRC="$BOOTSTRAP_DIR/aw-worktime-api.service"
|
WORKTIME_API_SERVICE_SRC="$BOOTSTRAP_DIR/aw-worktime-api.service"
|
||||||
|
WORKTIME_UI_BRIDGE_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.py"
|
||||||
|
WORKTIME_UI_BRIDGE_SERVICE_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.service"
|
||||||
|
WORKTIME_UI_BRIDGE_TIMER_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.timer"
|
||||||
|
|
||||||
for var_name in "${required_vars[@]}"; do
|
for var_name in "${required_vars[@]}"; do
|
||||||
if [[ -z "${!var_name:-}" ]]; then
|
if [[ -z "${!var_name:-}" ]]; then
|
||||||
@@ -103,6 +106,24 @@ if [[ -f "$WORKTIME_API_SERVICE_SRC" ]]; then
|
|||||||
systemctl --no-pager --full status aw-worktime-api.service || true
|
systemctl --no-pager --full status aw-worktime-api.service || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$WORKTIME_UI_BRIDGE_SRC" ]]; then
|
||||||
|
install -m 0755 "$WORKTIME_UI_BRIDGE_SRC" /usr/local/bin/aw-worktime-ui-bridge.py
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$WORKTIME_UI_BRIDGE_SERVICE_SRC" ]]; then
|
||||||
|
install -m 0644 "$WORKTIME_UI_BRIDGE_SERVICE_SRC" /etc/systemd/system/aw-worktime-ui-bridge.service
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$WORKTIME_UI_BRIDGE_TIMER_SRC" ]]; then
|
||||||
|
install -m 0644 "$WORKTIME_UI_BRIDGE_TIMER_SRC" /etc/systemd/system/aw-worktime-ui-bridge.timer
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl disable --now aw-worktime-afk-bridge.timer >/dev/null 2>&1 || true
|
||||||
|
systemctl enable aw-worktime-ui-bridge.timer
|
||||||
|
systemctl restart aw-worktime-ui-bridge.timer
|
||||||
|
systemctl start aw-worktime-ui-bridge.service || true
|
||||||
|
systemctl --no-pager --full status aw-worktime-ui-bridge.timer || true
|
||||||
|
fi
|
||||||
|
|
||||||
for _ in $(seq 1 20); do
|
for _ in $(seq 1 20); do
|
||||||
if curl -fsS "http://127.0.0.1:${AW_SERVER_PORT}/api/0/info" >/dev/null 2>&1; then
|
if curl -fsS "http://127.0.0.1:${AW_SERVER_PORT}/api/0/info" >/dev/null 2>&1; then
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"name": ["Работа", "Документы"],
|
"name": ["Работа", "Документы"],
|
||||||
"rule": {
|
"rule": {
|
||||||
"type": "regex",
|
"type": "regex",
|
||||||
"regex": "\\b(winword|excel|powerpnt|outlook|acrord32|acrord64)\\.exe\\b|Adobe Reader|Acrobat",
|
"regex": "\\b(winword|excel|powerpnt|outlook|acrord32|acrord64|libreoffice|writer|calc)\\.exe\\b|LibreOffice|OnlyOffice|Adobe Reader|Acrobat",
|
||||||
"ignore_case": true
|
"ignore_case": true
|
||||||
},
|
},
|
||||||
"data": { "color": "#2E7D32" }
|
"data": { "color": "#2E7D32" }
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"name": ["Работа", "Администрирование"],
|
"name": ["Работа", "Администрирование"],
|
||||||
"rule": {
|
"rule": {
|
||||||
"type": "regex",
|
"type": "regex",
|
||||||
"regex": "\\b(mstsc|putty|kitty|winscp|anydesk|teamviewer|vncviewer|mmc|regedit|services|control|powershell|cmd)\\.exe\\b",
|
"regex": "\\b(mstsc|putty|kitty|winscp|anydesk|teamviewer|vncviewer|mmc|regedit|services|control|powershell|cmd|gnome-terminal|gnome-terminal-server|xfce4-terminal|konsole|tilix|alacritty|xterm|remmina|virt-manager)\\.exe\\b|\\b(gnome-terminal|gnome-terminal-server|xfce4-terminal|konsole|tilix|alacritty|xterm|remmina|virt-manager)\\b|Proxmox Virtual Environment|\\bpfSense\\b|\\bGrafana\\b|\\bKibana\\b|\\bPortainer\\b",
|
||||||
"ignore_case": true
|
"ignore_case": true
|
||||||
},
|
},
|
||||||
"data": { "color": "#6D4C41" }
|
"data": { "color": "#6D4C41" }
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
"name": ["Интернет", "Браузер"],
|
"name": ["Интернет", "Браузер"],
|
||||||
"rule": {
|
"rule": {
|
||||||
"type": "regex",
|
"type": "regex",
|
||||||
"regex": "\\b(chrome|msedge|firefox|opera|brave|vivaldi|browser)\\.exe\\b",
|
"regex": "\\b(chrome|msedge|firefox|opera|brave|vivaldi|browser|chromium)\\.exe\\b|\\b(chrome|chromium|firefox|opera|brave|vivaldi)\\b",
|
||||||
"ignore_case": true
|
"ignore_case": true
|
||||||
},
|
},
|
||||||
"data": { "color": "#00897B" }
|
"data": { "color": "#00897B" }
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
"name": ["ActivityWatch"],
|
"name": ["ActivityWatch"],
|
||||||
"rule": {
|
"rule": {
|
||||||
"type": "regex",
|
"type": "regex",
|
||||||
"regex": "ActivityWatch|\\baw-(watcher|qt)\\.exe\\b",
|
"regex": "ActivityWatch|\\baw-(watcher|qt)\\.exe\\b|\\baw-(watcher|qt)\\b",
|
||||||
"ignore_case": true
|
"ignore_case": true
|
||||||
},
|
},
|
||||||
"data": {}
|
"data": {}
|
||||||
|
|||||||
@@ -306,6 +306,9 @@ function Copy-ActivityWatchCollectorAssets {
|
|||||||
$resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop
|
$resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop
|
||||||
Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force
|
Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
Copy-Item -LiteralPath $exampleRulesTarget -Destination $rulesTarget -Force
|
||||||
|
}
|
||||||
|
|
||||||
if ($CustomPolicySource) {
|
if ($CustomPolicySource) {
|
||||||
$resolvedPolicy = Resolve-Path -LiteralPath $CustomPolicySource -ErrorAction Stop
|
$resolvedPolicy = Resolve-Path -LiteralPath $CustomPolicySource -ErrorAction Stop
|
||||||
@@ -534,11 +537,7 @@ function Get-CollectorPowerShellProcessCount {
|
|||||||
function New-LaunchLock {
|
function New-LaunchLock {
|
||||||
param([string]`$StateRoot, [int]`$SessionId)
|
param([string]`$StateRoot, [int]`$SessionId)
|
||||||
|
|
||||||
if (-not (Test-Path -LiteralPath `$StateRoot)) {
|
`$lockPath = Join-Path `$env:TEMP ("launch-watchers-session-{0}.lock" -f `$SessionId)
|
||||||
New-Item -Path `$StateRoot -ItemType Directory -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
`$lockPath = Join-Path `$StateRoot ("launch-watchers-session-{0}.lock" -f `$SessionId)
|
|
||||||
if (Test-Path -LiteralPath `$lockPath) {
|
if (Test-Path -LiteralPath `$lockPath) {
|
||||||
try {
|
try {
|
||||||
`$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json
|
`$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json
|
||||||
@@ -731,13 +730,11 @@ function Start-CollectorScriptIfNeeded {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Start-Process -FilePath `$PowerShellExe -ArgumentList @(
|
`$staParam = if (`$ScriptPath -like "*endpoint-signals*") { "-STA" } else { `$null }
|
||||||
'-NoProfile',
|
`$argumentList = @('-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass')
|
||||||
'-WindowStyle', 'Hidden',
|
if (`$staParam) { `$argumentList += `$staParam }
|
||||||
'-ExecutionPolicy', 'Bypass',
|
`$argumentList += @('-File', `$ScriptPath, '-ConfigPath', `$ConfigPath)
|
||||||
'-File', `$ScriptPath,
|
Start-Process -FilePath `$PowerShellExe -ArgumentList `$argumentList -WindowStyle Hidden
|
||||||
'-ConfigPath', `$ConfigPath
|
|
||||||
) -WindowStyle Hidden
|
|
||||||
}
|
}
|
||||||
|
|
||||||
`$config = Get-DeploymentConfig -Path `$ConfigPath
|
`$config = Get-DeploymentConfig -Path `$ConfigPath
|
||||||
|
|||||||
+3354
-5
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$ServerHost,
|
[string]$ServerHost,
|
||||||
@@ -112,8 +112,8 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
|
|||||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
||||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||||
|
|
||||||
Write-Host 'ActivityWatch развёрнут для пользователей:'
|
Write-Output 'ActivityWatch развёрнут для пользователей:'
|
||||||
$targetUsers | ForEach-Object { Write-Host " - $_" }
|
$targetUsers | ForEach-Object { Write-Output " - $_" }
|
||||||
Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
Write-Output "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
||||||
Write-Host "Каталог данных: $StateRoot"
|
Write-Output "Каталог данных: $StateRoot"
|
||||||
Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
Write-Output "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$ServerHost,
|
[string]$ServerHost,
|
||||||
@@ -136,6 +136,6 @@ if ($reportDirectory) {
|
|||||||
|
|
||||||
$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $effectiveReportPath -Encoding UTF8
|
$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $effectiveReportPath -Encoding UTF8
|
||||||
|
|
||||||
Write-Host 'Комплексное развёртывание ActivityWatch завершено.'
|
Write-Output 'Комплексное развёртывание ActivityWatch завершено.'
|
||||||
Write-Host "Пользователи: $($resolvedUsers -join ', ')"
|
Write-Output "Пользователи: $($resolvedUsers -join ', ')"
|
||||||
Write-Host "Отчёт: $effectiveReportPath"
|
Write-Output "Отчёт: $effectiveReportPath"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$ServerHost,
|
[string]$ServerHost,
|
||||||
@@ -104,9 +104,9 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
|
|||||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
||||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||||
|
|
||||||
Write-Host "ActivityWatch развёрнут для пользователя: $TargetUser"
|
Write-Output "ActivityWatch развёрнут для пользователя: $TargetUser"
|
||||||
Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
Write-Output "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
||||||
Write-Host "Каталог установки: $InstallRoot"
|
Write-Output "Каталог установки: $InstallRoot"
|
||||||
Write-Host "Каталог данных: $StateRoot"
|
Write-Output "Каталог данных: $StateRoot"
|
||||||
Write-Host "Файл правил: $($assetResult.ActiveRules)"
|
Write-Output "Файл правил: $($assetResult.ActiveRules)"
|
||||||
Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
Write-Output "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
[string]$ServerHost,
|
[string]$ServerHost,
|
||||||
@@ -151,6 +151,6 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
|
|||||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath
|
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath
|
||||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||||
|
|
||||||
Write-Host 'Укрепление и восстановление ActivityWatch завершены.'
|
Write-Output 'Укрепление и восстановление ActivityWatch завершены.'
|
||||||
Write-Host "Конфигурация: $effectiveConfigPath"
|
Write-Output "Конфигурация: $effectiveConfigPath"
|
||||||
Write-Host "Пользователи восстановлены: $($effectiveUsers -join ', ')"
|
Write-Output "Пользователи восстановлены: $($effectiveUsers -join ', ')"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||||
param(
|
param(
|
||||||
[string]$OldInstallRoot = 'C:\Program Files\ActivityWatch-Phase2',
|
[string]$OldInstallRoot = 'C:\Program Files\ActivityWatch-Phase2',
|
||||||
[string]$OldStateRoot = 'C:\ProgramData\ActivityWatch-Phase2',
|
[string]$OldStateRoot = 'C:\ProgramData\ActivityWatch-Phase2',
|
||||||
@@ -154,7 +154,36 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Миграция ActivityWatch W
|
|||||||
@{ Source = $NewStateRoot; Name = 'new-state' }
|
@{ Source = $NewStateRoot; Name = 'new-state' }
|
||||||
)) {
|
)) {
|
||||||
if (Test-Path -LiteralPath $item.Source) {
|
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'"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json'
|
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json'
|
||||||
)
|
)
|
||||||
|
|
||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
@@ -13,49 +13,26 @@ $config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath
|
|||||||
$installRoot = [string]$config.paths.installRoot
|
$installRoot = [string]$config.paths.installRoot
|
||||||
$stateRoot = [string]$config.paths.stateRoot
|
$stateRoot = [string]$config.paths.stateRoot
|
||||||
$collectorScript = [string]$config.paths.collectorScript
|
$collectorScript = [string]$config.paths.collectorScript
|
||||||
$endpointCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]$config.paths.endpointCollectorScript } else { Join-Path $stateRoot 'dlp-endpoint-signals-collector.ps1' }
|
|
||||||
$sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' }
|
|
||||||
$rulesPath = [string]$config.paths.rulesPath
|
$rulesPath = [string]$config.paths.rulesPath
|
||||||
$policyPath = if ($config.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$config.paths.policyPath } else { Join-Path $stateRoot 'dlp-policy.json' }
|
|
||||||
$launchScript = [string]$config.paths.launchScript
|
$launchScript = [string]$config.paths.launchScript
|
||||||
$recoveryScript = [string]$config.paths.recoveryScript
|
$recoveryScript = [string]$config.paths.recoveryScript
|
||||||
|
|
||||||
$afkExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]$config.collectors.afkEnabled } else { $true }
|
|
||||||
$windowExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]$config.collectors.windowEnabled } else { $true }
|
|
||||||
$requiredFiles = @(
|
$requiredFiles = @(
|
||||||
|
(Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe'),
|
||||||
|
(Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe'),
|
||||||
$collectorScript,
|
$collectorScript,
|
||||||
$endpointCollectorScript,
|
|
||||||
$sessionCollectorScript,
|
|
||||||
$rulesPath,
|
$rulesPath,
|
||||||
$policyPath,
|
|
||||||
$launchScript,
|
$launchScript,
|
||||||
$recoveryScript,
|
$recoveryScript,
|
||||||
$ConfigPath
|
$ConfigPath
|
||||||
)
|
)
|
||||||
if ($afkExpected) {
|
|
||||||
$requiredFiles += (Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe')
|
|
||||||
}
|
|
||||||
if ($windowExpected) {
|
|
||||||
$requiredFiles += (Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe')
|
|
||||||
}
|
|
||||||
|
|
||||||
$missingFiles = @(
|
$missingFiles = @(
|
||||||
$requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) }
|
$requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) }
|
||||||
)
|
)
|
||||||
|
|
||||||
$processNames = @()
|
$processNames = @('aw-watcher-afk', 'aw-watcher-window')
|
||||||
if ($afkExpected) { $processNames += 'aw-watcher-afk' }
|
$runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId
|
||||||
if ($windowExpected) { $processNames += 'aw-watcher-window' }
|
|
||||||
$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
|
|
||||||
|
|
||||||
$taskNames = @()
|
$taskNames = @()
|
||||||
if ($config.userTasks) {
|
if ($config.userTasks) {
|
||||||
@@ -65,7 +42,7 @@ $taskNames += [string]$config.recovery.taskName
|
|||||||
$taskNames = $taskNames | Sort-Object -Unique
|
$taskNames = $taskNames | Sort-Object -Unique
|
||||||
|
|
||||||
$tasks = foreach ($taskName in $taskNames) {
|
$tasks = foreach ($taskName in $taskNames) {
|
||||||
$task = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $taskName } | Select-Object -First 1
|
$task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||||
if ($task) {
|
if ($task) {
|
||||||
[pscustomobject]@{
|
[pscustomobject]@{
|
||||||
taskName = $task.TaskName
|
taskName = $task.TaskName
|
||||||
@@ -76,7 +53,7 @@ $tasks = foreach ($taskName in $taskNames) {
|
|||||||
else {
|
else {
|
||||||
[pscustomobject]@{
|
[pscustomobject]@{
|
||||||
taskName = $taskName
|
taskName = $taskName
|
||||||
state = 'Отсутствует'
|
state = 'Missing'
|
||||||
present = $false
|
present = $false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,16 +76,8 @@ $result = [ordered]@{
|
|||||||
ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.present }))
|
ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.present }))
|
||||||
}
|
}
|
||||||
processes = [ordered]@{
|
processes = [ordered]@{
|
||||||
expected = $processNames
|
|
||||||
list = @($runningProcesses)
|
list = @($runningProcesses)
|
||||||
sessionCollectors = @($sessionCollectorProcesses)
|
ok = [bool](($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge 2)
|
||||||
ok = [bool](
|
|
||||||
(
|
|
||||||
($processNames.Count -eq 0) -or
|
|
||||||
(($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge $processNames.Count)
|
|
||||||
) -and
|
|
||||||
($sessionCollectorProcesses.Count -ge 1)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,44 @@
|
|||||||
param(
|
param(
|
||||||
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
|
[string]$Hostname,
|
||||||
|
[int]$PollSeconds = 30
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Get-Config {
|
||||||
|
param([string]$Path)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) {
|
||||||
|
throw "Конфигурация не найдена: $Path"
|
||||||
|
}
|
||||||
|
|
||||||
|
Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-AwJsonPost {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$Uri,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Json
|
||||||
|
)
|
||||||
|
|
||||||
|
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||||
|
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-Bucket {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$ApiBase,
|
||||||
|
[Parameter(Mandatory = $true)][string]$BucketId,
|
||||||
|
[Parameter(Mandatory = $true)][string]$HostnameValue
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
catch { Write-Error param(
|
||||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
[string]$Hostname,
|
[string]$Hostname,
|
||||||
[int]$PollSeconds = 30
|
[int]$PollSeconds = 30
|
||||||
@@ -47,7 +87,12 @@ function Ensure-Bucket {
|
|||||||
hostname = $HostnameValue
|
hostname = $HostnameValue
|
||||||
} | ConvertTo-Json -Compress
|
} | ConvertTo-Json -Compress
|
||||||
|
|
||||||
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-SessionRecords {
|
function Get-SessionRecords {
|
||||||
@@ -159,3 +204,458 @@ while ($true) {
|
|||||||
|
|
||||||
Start-Sleep -Seconds $sleepSec
|
Start-Sleep -Seconds $sleepSec
|
||||||
}
|
}
|
||||||
|
; }
|
||||||
|
|
||||||
|
$body = @{
|
||||||
|
client = 'aw-worktime-session-collector'
|
||||||
|
type = 'aw.worktime.session'
|
||||||
|
hostname = $HostnameValue
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SessionRecords {
|
||||||
|
$records = @()
|
||||||
|
|
||||||
|
try {
|
||||||
|
$lines = quser 2>$null
|
||||||
|
if (-not $lines) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($line in ($lines | Select-Object -Skip 1)) {
|
||||||
|
$clean = ($line -replace '^\s*>?', '').Trim()
|
||||||
|
if (-not $clean) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = $clean -split '\s+'
|
||||||
|
if ($parts.Count -lt 4) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionName = ''
|
||||||
|
$sessionIdIndex = 2
|
||||||
|
if ($parts[1] -match '^\d+$') {
|
||||||
|
$sessionIdIndex = 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$sessionName = $parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionId = 0
|
||||||
|
if ($parts[$sessionIdIndex] -match '^\d+$') {
|
||||||
|
$sessionId = [int]$parts[$sessionIdIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
$records += [pscustomobject]@{
|
||||||
|
username = $parts[0]
|
||||||
|
sessionName = $sessionName
|
||||||
|
sessionId = $sessionId
|
||||||
|
state = $parts[$sessionIdIndex + 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { Write-Error param(
|
||||||
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
|
[string]$Hostname,
|
||||||
|
[int]$PollSeconds = 30
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Get-Config {
|
||||||
|
param([string]$Path)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) {
|
||||||
|
throw "Конфигурация не найдена: $Path"
|
||||||
|
}
|
||||||
|
|
||||||
|
Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-AwJsonPost {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$Uri,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Json
|
||||||
|
)
|
||||||
|
|
||||||
|
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||||
|
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-Bucket {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$ApiBase,
|
||||||
|
[Parameter(Mandatory = $true)][string]$BucketId,
|
||||||
|
[Parameter(Mandatory = $true)][string]$HostnameValue
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = @{
|
||||||
|
client = 'aw-worktime-session-collector'
|
||||||
|
type = 'aw.worktime.session'
|
||||||
|
hostname = $HostnameValue
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SessionRecords {
|
||||||
|
$records = @()
|
||||||
|
|
||||||
|
try {
|
||||||
|
$lines = quser 2>$null
|
||||||
|
if (-not $lines) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($line in ($lines | Select-Object -Skip 1)) {
|
||||||
|
$clean = ($line -replace '^\s*>?', '').Trim()
|
||||||
|
if (-not $clean) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = $clean -split '\s+'
|
||||||
|
if ($parts.Count -lt 4) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionName = ''
|
||||||
|
$sessionIdIndex = 2
|
||||||
|
if ($parts[1] -match '^\d+$') {
|
||||||
|
$sessionIdIndex = 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$sessionName = $parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionId = 0
|
||||||
|
if ($parts[$sessionIdIndex] -match '^\d+$') {
|
||||||
|
$sessionId = [int]$parts[$sessionIdIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
$records += [pscustomobject]@{
|
||||||
|
username = $parts[0]
|
||||||
|
sessionName = $sessionName
|
||||||
|
sessionId = $sessionId
|
||||||
|
state = $parts[$sessionIdIndex + 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
|
||||||
|
return $records
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-SessionIsActive {
|
||||||
|
param([AllowNull()][string]$State)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($State)) { return $false }
|
||||||
|
$s = $State.Trim().ToLowerInvariant()
|
||||||
|
return ($s -eq 'active') -or ($s -like 'актив*')
|
||||||
|
}
|
||||||
|
|
||||||
|
$cfg = Get-Config -Path $ConfigPath
|
||||||
|
$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME }
|
||||||
|
$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port
|
||||||
|
$bucketId = 'aw-worktime-sessions_' + $hostValue
|
||||||
|
$pulse = 120
|
||||||
|
$sleepSec = if ($PollSeconds -gt 0) {
|
||||||
|
$PollSeconds
|
||||||
|
}
|
||||||
|
elseif ($cfg.collector -and $cfg.collector.pollSeconds) {
|
||||||
|
[int]$cfg.collector.pollSeconds
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
30
|
||||||
|
}
|
||||||
|
|
||||||
|
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||||
|
$records = Get-SessionRecords
|
||||||
|
if (-not $records -or $records.Count -eq 0) {
|
||||||
|
$records = @([pscustomobject]@{
|
||||||
|
username = $env:USERNAME
|
||||||
|
sessionName = ''
|
||||||
|
sessionId = (Get-Process -Id $PID).SessionId
|
||||||
|
state = 'Unknown'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rec in $records) {
|
||||||
|
$payload = @{
|
||||||
|
timestamp = $now
|
||||||
|
duration = 0
|
||||||
|
data = @{
|
||||||
|
username = [string]$rec.username
|
||||||
|
userId = "$($env:USERDOMAIN)\$($rec.username)"
|
||||||
|
sessionId = [int]$rec.sessionId
|
||||||
|
sessionName = [string]$rec.sessionName
|
||||||
|
state = [string]$rec.state
|
||||||
|
active = (Test-SessionIsActive -State ([string]$rec.state))
|
||||||
|
hostname = $hostValue
|
||||||
|
source = 'worktime-session-collector'
|
||||||
|
}
|
||||||
|
} | ConvertTo-Json -Depth 6 -Compress
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Seconds $sleepSec
|
||||||
|
}
|
||||||
|
; }
|
||||||
|
|
||||||
|
return $records
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-SessionIsActive {
|
||||||
|
param([AllowNull()][string]$State)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($State)) { return $false }
|
||||||
|
$s = $State.Trim().ToLowerInvariant()
|
||||||
|
return ($s -eq 'active') -or ($s -like 'актив*')
|
||||||
|
}
|
||||||
|
|
||||||
|
$cfg = Get-Config -Path $ConfigPath
|
||||||
|
$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME }
|
||||||
|
$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port
|
||||||
|
$bucketId = 'aw-worktime-sessions_' + $hostValue
|
||||||
|
$pulse = 120
|
||||||
|
$sleepSec = if ($PollSeconds -gt 0) {
|
||||||
|
$PollSeconds
|
||||||
|
}
|
||||||
|
elseif ($cfg.collector -and $cfg.collector.pollSeconds) {
|
||||||
|
[int]$cfg.collector.pollSeconds
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
30
|
||||||
|
}
|
||||||
|
|
||||||
|
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||||
|
$records = Get-SessionRecords
|
||||||
|
if (-not $records -or $records.Count -eq 0) {
|
||||||
|
$records = @([pscustomobject]@{
|
||||||
|
username = $env:USERNAME
|
||||||
|
sessionName = ''
|
||||||
|
sessionId = (Get-Process -Id $PID).SessionId
|
||||||
|
state = 'Unknown'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rec in $records) {
|
||||||
|
$payload = @{
|
||||||
|
timestamp = $now
|
||||||
|
duration = 0
|
||||||
|
data = @{
|
||||||
|
username = [string]$rec.username
|
||||||
|
userId = "$($env:USERDOMAIN)\$($rec.username)"
|
||||||
|
sessionId = [int]$rec.sessionId
|
||||||
|
sessionName = [string]$rec.sessionName
|
||||||
|
state = [string]$rec.state
|
||||||
|
active = (Test-SessionIsActive -State ([string]$rec.state))
|
||||||
|
hostname = $hostValue
|
||||||
|
source = 'worktime-session-collector'
|
||||||
|
}
|
||||||
|
} | ConvertTo-Json -Depth 6 -Compress
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload
|
||||||
|
}
|
||||||
|
catch { Write-Error param(
|
||||||
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
|
[string]$Hostname,
|
||||||
|
[int]$PollSeconds = 30
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Get-Config {
|
||||||
|
param([string]$Path)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) {
|
||||||
|
throw "Конфигурация не найдена: $Path"
|
||||||
|
}
|
||||||
|
|
||||||
|
Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-AwJsonPost {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$Uri,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Json
|
||||||
|
)
|
||||||
|
|
||||||
|
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||||
|
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-Bucket {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$ApiBase,
|
||||||
|
[Parameter(Mandatory = $true)][string]$BucketId,
|
||||||
|
[Parameter(Mandatory = $true)][string]$HostnameValue
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = @{
|
||||||
|
client = 'aw-worktime-session-collector'
|
||||||
|
type = 'aw.worktime.session'
|
||||||
|
hostname = $HostnameValue
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SessionRecords {
|
||||||
|
$records = @()
|
||||||
|
|
||||||
|
try {
|
||||||
|
$lines = quser 2>$null
|
||||||
|
if (-not $lines) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($line in ($lines | Select-Object -Skip 1)) {
|
||||||
|
$clean = ($line -replace '^\s*>?', '').Trim()
|
||||||
|
if (-not $clean) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = $clean -split '\s+'
|
||||||
|
if ($parts.Count -lt 4) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionName = ''
|
||||||
|
$sessionIdIndex = 2
|
||||||
|
if ($parts[1] -match '^\d+$') {
|
||||||
|
$sessionIdIndex = 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$sessionName = $parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionId = 0
|
||||||
|
if ($parts[$sessionIdIndex] -match '^\d+$') {
|
||||||
|
$sessionId = [int]$parts[$sessionIdIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
$records += [pscustomobject]@{
|
||||||
|
username = $parts[0]
|
||||||
|
sessionName = $sessionName
|
||||||
|
sessionId = $sessionId
|
||||||
|
state = $parts[$sessionIdIndex + 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
|
||||||
|
return $records
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-SessionIsActive {
|
||||||
|
param([AllowNull()][string]$State)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($State)) { return $false }
|
||||||
|
$s = $State.Trim().ToLowerInvariant()
|
||||||
|
return ($s -eq 'active') -or ($s -like 'актив*')
|
||||||
|
}
|
||||||
|
|
||||||
|
$cfg = Get-Config -Path $ConfigPath
|
||||||
|
$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME }
|
||||||
|
$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port
|
||||||
|
$bucketId = 'aw-worktime-sessions_' + $hostValue
|
||||||
|
$pulse = 120
|
||||||
|
$sleepSec = if ($PollSeconds -gt 0) {
|
||||||
|
$PollSeconds
|
||||||
|
}
|
||||||
|
elseif ($cfg.collector -and $cfg.collector.pollSeconds) {
|
||||||
|
[int]$cfg.collector.pollSeconds
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
30
|
||||||
|
}
|
||||||
|
|
||||||
|
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||||
|
$records = Get-SessionRecords
|
||||||
|
if (-not $records -or $records.Count -eq 0) {
|
||||||
|
$records = @([pscustomobject]@{
|
||||||
|
username = $env:USERNAME
|
||||||
|
sessionName = ''
|
||||||
|
sessionId = (Get-Process -Id $PID).SessionId
|
||||||
|
state = 'Unknown'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rec in $records) {
|
||||||
|
$payload = @{
|
||||||
|
timestamp = $now
|
||||||
|
duration = 0
|
||||||
|
data = @{
|
||||||
|
username = [string]$rec.username
|
||||||
|
userId = "$($env:USERDOMAIN)\$($rec.username)"
|
||||||
|
sessionId = [int]$rec.sessionId
|
||||||
|
sessionName = [string]$rec.sessionName
|
||||||
|
state = [string]$rec.state
|
||||||
|
active = (Test-SessionIsActive -State ([string]$rec.state))
|
||||||
|
hostname = $hostValue
|
||||||
|
source = 'worktime-session-collector'
|
||||||
|
}
|
||||||
|
} | ConvertTo-Json -Depth 6 -Compress
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Seconds $sleepSec
|
||||||
|
}
|
||||||
|
; }
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Seconds $sleepSec
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
# shellcheck disable=SC1007
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
# shellcheck disable=SC1007
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
|||||||
@@ -18,7 +18,12 @@ JsonScalar: TypeAlias = str | int | float | bool | None
|
|||||||
JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"]
|
JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"]
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_BUCKET_PREFIXES = ("aw-file-operations_", "aw-dlp-incidents_")
|
DEFAULT_BUCKET_PREFIXES = (
|
||||||
|
"aw-file-operations_",
|
||||||
|
"aw-dlp-incidents_",
|
||||||
|
"aw-dlp-endpoint-signals_",
|
||||||
|
"aw-email-monitor_",
|
||||||
|
)
|
||||||
DEFAULT_SQLITE_PATH = "data/dlp-events.sqlite3"
|
DEFAULT_SQLITE_PATH = "data/dlp-events.sqlite3"
|
||||||
EVENT_COLUMNS = (
|
EVENT_COLUMNS = (
|
||||||
"bucket_id",
|
"bucket_id",
|
||||||
@@ -134,6 +139,10 @@ def bucket_stream_type(bucket: Bucket) -> str | None:
|
|||||||
return "file_operation"
|
return "file_operation"
|
||||||
if bucket.id.startswith("aw-dlp-incidents_") or bucket.type == "aw.dlp.incident":
|
if bucket.id.startswith("aw-dlp-incidents_") or bucket.type == "aw.dlp.incident":
|
||||||
return "dlp_incident"
|
return "dlp_incident"
|
||||||
|
if bucket.id.startswith("aw-dlp-endpoint-signals_") or bucket.type == "aw.dlp.endpoint.signal":
|
||||||
|
return "dlp_endpoint_signal"
|
||||||
|
if bucket.id.startswith("aw-email-monitor_") or bucket.type == "aw.email.signal":
|
||||||
|
return "email_monitor"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,26 @@ set -euo pipefail
|
|||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
cd "$ROOT_DIR"
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
# shellcheck disable=SC2034
|
||||||
KIT_DIR="install-kit-awindows-20260427-211240"
|
KIT_DIR="install-kit-awindows-20260427-211240"
|
||||||
|
|
||||||
python - <<'PY'
|
PY_BIN="${PY_BIN:-}"
|
||||||
|
if [[ -z "$PY_BIN" ]]; then
|
||||||
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
|
PY_BIN="python3"
|
||||||
|
elif command -v python >/dev/null 2>&1; then
|
||||||
|
PY_BIN="python"
|
||||||
|
else
|
||||||
|
echo "ERROR: python3/python not found"
|
||||||
|
exit 127
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$PY_BIN" - <<'PY'
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
root=Path('.')
|
root=Path('.')
|
||||||
kit=Path('install-kit-awindows-20260427-211240')
|
kit=Path('install-kit-awindows-20260427-211240')
|
||||||
@@ -24,6 +39,13 @@ missing_in_repo=[]
|
|||||||
|
|
||||||
for kp in sorted(p for p in kit.rglob('*') if p.is_file() and p.name!='MANIFEST.txt'):
|
for kp in sorted(p for p in kit.rglob('*') if p.is_file() and p.name!='MANIFEST.txt'):
|
||||||
rel=kp.relative_to(kit)
|
rel=kp.relative_to(kit)
|
||||||
|
rel_s=str(rel)
|
||||||
|
if rel_s.startswith("server-configs-192.168.100.21/"):
|
||||||
|
continue
|
||||||
|
if rel_s == "README-INSTALL-KIT.txt":
|
||||||
|
continue
|
||||||
|
if "__pycache__" in kp.parts or kp.suffix == ".pyc":
|
||||||
|
continue
|
||||||
rp=root/rel
|
rp=root/rel
|
||||||
if not rp.exists():
|
if not rp.exists():
|
||||||
missing_in_repo.append(str(rel))
|
missing_in_repo.append(str(rel))
|
||||||
@@ -50,4 +72,9 @@ if ps_mismatches:
|
|||||||
print('--- PowerShell mismatches ---')
|
print('--- PowerShell mismatches ---')
|
||||||
for p in ps_mismatches:
|
for p in ps_mismatches:
|
||||||
print(p)
|
print(p)
|
||||||
|
|
||||||
|
strict = os.getenv("ALLOW_KIT_DRIFT", "").lower() not in {"1", "true", "yes"}
|
||||||
|
if strict and (missing_in_repo or mismatches):
|
||||||
|
print("ERROR: install-kit drift detected. Set ALLOW_KIT_DRIFT=1 to bypass.")
|
||||||
|
sys.exit(1)
|
||||||
PY
|
PY
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ while [ "$#" -gt 0 ]; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# shellcheck disable=SC1007
|
||||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
|
||||||
sh "${SCRIPT_DIR}/install_aw_linux_client.sh" \
|
sh "${SCRIPT_DIR}/install_aw_linux_client.sh" \
|
||||||
|
|||||||
+14
-2
@@ -21,9 +21,11 @@ prompt_secret() {
|
|||||||
if [[ -n "${!var_name:-}" ]]; then
|
if [[ -n "${!var_name:-}" ]]; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
read -r -s -p "${prompt}: " "$var_name"
|
local _val
|
||||||
|
read -r -s -p "${prompt}: " _val
|
||||||
echo
|
echo
|
||||||
export "$var_name"
|
printf -v "$var_name" '%s' "$_val"
|
||||||
|
declare -gx "$var_name"
|
||||||
}
|
}
|
||||||
|
|
||||||
require_cmd git
|
require_cmd git
|
||||||
@@ -33,6 +35,12 @@ require_cmd ansible
|
|||||||
log "Repo: ${ROOT_DIR}"
|
log "Repo: ${ROOT_DIR}"
|
||||||
log "Branch: $(git branch --show-current)"
|
log "Branch: $(git branch --show-current)"
|
||||||
|
|
||||||
|
if [[ "${AW_MAINTENANCE_ACK:-}" != "YES" ]]; then
|
||||||
|
log "ERROR: maintenance window is required."
|
||||||
|
log "Set AW_MAINTENANCE_ACK=YES to proceed."
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
log "Running local quality gate..."
|
log "Running local quality gate..."
|
||||||
./scripts/quality-gate.sh | tee -a "${LOG_DIR}/quality-gate.log"
|
./scripts/quality-gate.sh | tee -a "${LOG_DIR}/quality-gate.log"
|
||||||
|
|
||||||
@@ -65,6 +73,10 @@ 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_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"
|
ansible -i ansible/inventory.ini aw_windows -m win_ping | tee -a "${LOG_DIR}/ping_aw_windows.log"
|
||||||
|
|
||||||
|
log "Preflight ActivityWatch API/data checks..."
|
||||||
|
./check-aw-data.sh | tee -a "${LOG_DIR}/check_aw_data.log"
|
||||||
|
./check-aw-full.sh | tee -a "${LOG_DIR}/check_aw_full.log"
|
||||||
|
|
||||||
log "Dry-run aw_server..."
|
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"
|
ansible-playbook -i ansible/inventory.ini ansible/deploy_aw_server.yml --check --diff | tee -a "${LOG_DIR}/check_aw_server.log"
|
||||||
|
|
||||||
|
|||||||
+15
-6
@@ -4,33 +4,42 @@ set -euo pipefail
|
|||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
cd "$ROOT_DIR"
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
echo "[1/4] Bash syntax check"
|
echo "[1/6] Bash syntax check"
|
||||||
find aw-server proxmox -type f -name "*.sh" -print0 | xargs -0 -r -n1 bash -n
|
find aw-server proxmox -type f -name "*.sh" -print0 | xargs -0 -r -n1 bash -n
|
||||||
|
|
||||||
echo "[2/4] Shellcheck (if available)"
|
echo "[2/6] Shellcheck (if available)"
|
||||||
if command -v shellcheck >/dev/null 2>&1; then
|
if command -v shellcheck >/dev/null 2>&1; then
|
||||||
find aw-server proxmox -type f -name "*.sh" -print0 | xargs -0 -r shellcheck -e SC1007,SC1090,SC2016
|
find aw-server proxmox -type f -name "*.sh" -print0 | xargs -0 -r shellcheck -e SC1007,SC1090,SC2016
|
||||||
else
|
else
|
||||||
echo "shellcheck not found, skipping."
|
echo "shellcheck not found, skipping."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[3/4] PowerShell parse check (if pwsh available)"
|
echo "[3/6] PowerShell parse check (if pwsh available)"
|
||||||
if command -v pwsh >/dev/null 2>&1; then
|
if command -v pwsh >/dev/null 2>&1; then
|
||||||
pwsh -NoLogo -NoProfile -Command '
|
if ! pwsh -NoLogo -NoProfile -Command '
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
Get-ChildItem windows -Filter *.ps1 | ForEach-Object {
|
Get-ChildItem windows -Filter *.ps1 | ForEach-Object {
|
||||||
[void][System.Management.Automation.Language.Parser]::ParseFile($_.FullName,[ref]$null,[ref]$null)
|
[void][System.Management.Automation.Language.Parser]::ParseFile($_.FullName,[ref]$null,[ref]$null)
|
||||||
}
|
}
|
||||||
[void][System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path "windows/ActivityWatch.Windows.Common.psm1"),[ref]$null,[ref]$null)
|
[void][System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path "windows/ActivityWatch.Windows.Common.psm1"),[ref]$null,[ref]$null)
|
||||||
[void][System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path "windows/ActivityWatch.Windows.Common.psd1"),[ref]$null,[ref]$null)
|
[void][System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path "windows/ActivityWatch.Windows.Common.psd1"),[ref]$null,[ref]$null)
|
||||||
'
|
'; then
|
||||||
|
echo "pwsh parse check failed due runtime environment; skipping."
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "pwsh not found, skipping."
|
echo "pwsh not found, skipping."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "[4/6] Install-kit consistency check"
|
||||||
|
./scripts/check_install_kit_vs_repo.sh
|
||||||
|
|
||||||
|
echo "[5/6] Generated-artifacts guard"
|
||||||
|
if git status --short | grep -E '^(\\?\\?| M|M ) (\\.graphify_|graphify-out/|reports/|tmp/|data/)'; then
|
||||||
|
echo "ERROR: generated artifacts detected in working tree. Clean or ignore them before rollout."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[4/4] Ansible syntax check (if ansible-playbook available)"
|
echo "[6/6] Ansible syntax check (if ansible-playbook available)"
|
||||||
if command -v ansible-playbook >/dev/null 2>&1; then
|
if command -v ansible-playbook >/dev/null 2>&1; then
|
||||||
for playbook in ansible/*.yml; do
|
for playbook in ansible/*.yml; do
|
||||||
ansible-playbook --syntax-check "$playbook" -i ansible/inventory.example.ini >/dev/null
|
ansible-playbook --syntax-check "$playbook" -i ansible/inventory.example.ini >/dev/null
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ cd "$ROOT_DIR"
|
|||||||
|
|
||||||
KIT_DIR="install-kit-awindows-20260427-211240"
|
KIT_DIR="install-kit-awindows-20260427-211240"
|
||||||
MANIFEST="$KIT_DIR/MANIFEST.txt"
|
MANIFEST="$KIT_DIR/MANIFEST.txt"
|
||||||
|
# ZIP/TAR variables are declared for archive checks in this script; keep them for clarity
|
||||||
|
# shellcheck disable=SC2034
|
||||||
ZIP_ARCHIVE="install-kit-awindows-20260427-211240.zip"
|
ZIP_ARCHIVE="install-kit-awindows-20260427-211240.zip"
|
||||||
|
# shellcheck disable=SC2034
|
||||||
TAR_ARCHIVE="install-kit-awindows-20260427-211240.tar.gz"
|
TAR_ARCHIVE="install-kit-awindows-20260427-211240.tar.gz"
|
||||||
|
|
||||||
required_files=(
|
required_files=(
|
||||||
|
|||||||
@@ -376,6 +376,7 @@ function New-ActivityWatchDeploymentConfig {
|
|||||||
[string]$LaunchScriptPath,
|
[string]$LaunchScriptPath,
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$RecoveryScriptPath,
|
[string]$RecoveryScriptPath,
|
||||||
|
[string]$AwHostname,
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[pscustomobject[]]$UserTasks,
|
[pscustomobject[]]$UserTasks,
|
||||||
[string]$PackageVersion = 'v0.13.2'
|
[string]$PackageVersion = 'v0.13.2'
|
||||||
@@ -386,6 +387,7 @@ function New-ActivityWatchDeploymentConfig {
|
|||||||
return [pscustomobject]@{
|
return [pscustomobject]@{
|
||||||
version = 1
|
version = 1
|
||||||
generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
|
generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
|
||||||
|
awHostname = if ([string]::IsNullOrWhiteSpace($AwHostname)) { [string]$env:COMPUTERNAME } else { [string]$AwHostname }
|
||||||
server = [pscustomobject]@{
|
server = [pscustomobject]@{
|
||||||
host = $ServerHost
|
host = $ServerHost
|
||||||
port = $ServerPort
|
port = $ServerPort
|
||||||
@@ -742,7 +744,7 @@ function Start-CollectorScriptIfNeeded {
|
|||||||
`$installRoot = [string]`$config.paths.installRoot
|
`$installRoot = [string]`$config.paths.installRoot
|
||||||
`$stateRoot = [string]`$config.paths.stateRoot
|
`$stateRoot = [string]`$config.paths.stateRoot
|
||||||
`$script:ApiBase = '{0}://{1}:{2}/api/0' -f [string]`$config.server.scheme, [string]`$config.server.host, [string]`$config.server.port
|
`$script:ApiBase = '{0}://{1}:{2}/api/0' -f [string]`$config.server.scheme, [string]`$config.server.host, [string]`$config.server.port
|
||||||
`$script:Hostname = `$env:COMPUTERNAME
|
`$script:Hostname = if (`$config.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]`$config.awHostname)) { [string]`$config.awHostname } else { `$env:COMPUTERNAME }
|
||||||
`$script:KnownBuckets = @{}
|
`$script:KnownBuckets = @{}
|
||||||
`$collectorScript = [string]`$config.paths.collectorScript
|
`$collectorScript = [string]`$config.paths.collectorScript
|
||||||
`$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { Join-Path `$stateRoot 'dlp-endpoint-signals-collector.ps1' }
|
`$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { Join-Path `$stateRoot 'dlp-endpoint-signals-collector.ps1' }
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
|
[int]$LoopSeconds = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Get-Config {
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) {
|
||||||
|
throw "Config not found: $Path"
|
||||||
|
}
|
||||||
|
Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-ServiceLog {
|
||||||
|
param([string]$Message)
|
||||||
|
try {
|
||||||
|
Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message)
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Start-CollectorIfNeeded {
|
||||||
|
param(
|
||||||
|
[string]$ScriptPath,
|
||||||
|
[string]$ConfigPath
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ScriptPath) -or -not (Test-Path -LiteralPath $ScriptPath)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$escaped = [Regex]::Escape($ScriptPath)
|
||||||
|
$running = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object {
|
||||||
|
$_.Name -eq 'powershell.exe' -and
|
||||||
|
$_.CommandLine -match $escaped -and
|
||||||
|
$_.CommandLine -match [Regex]::Escape($ConfigPath)
|
||||||
|
} |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if ($running) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = @('-NoProfile', '-ExecutionPolicy', 'Bypass')
|
||||||
|
if ($ScriptPath -like '*dlp-endpoint-signals*') {
|
||||||
|
$args += '-STA'
|
||||||
|
}
|
||||||
|
$args += @('-File', $ScriptPath, '-ConfigPath', $ConfigPath)
|
||||||
|
Start-Process -FilePath 'powershell.exe' -ArgumentList $args -WindowStyle Hidden | Out-Null
|
||||||
|
Write-ServiceLog ("started collector: {0}" -f $ScriptPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
$cfg = Get-Config -Path $ConfigPath
|
||||||
|
$stateRoot = if ($cfg.paths -and $cfg.paths.stateRoot) { [string]$cfg.paths.stateRoot } else { 'C:\ProgramData\AWatch-rus' }
|
||||||
|
$logsRoot = Join-Path $stateRoot 'logs'
|
||||||
|
if (-not (Test-Path -LiteralPath $logsRoot)) {
|
||||||
|
New-Item -Path $logsRoot -ItemType Directory -Force | Out-Null
|
||||||
|
}
|
||||||
|
$script:LogPath = Join-Path $logsRoot 'standalone-agent-service.log'
|
||||||
|
|
||||||
|
Write-ServiceLog ('service loop started, config={0}' -f $ConfigPath)
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
try {
|
||||||
|
$cfg = Get-Config -Path $ConfigPath
|
||||||
|
$paths = $cfg.paths
|
||||||
|
|
||||||
|
Start-CollectorIfNeeded -ScriptPath ([string]$paths.collectorScript) -ConfigPath $ConfigPath
|
||||||
|
Start-CollectorIfNeeded -ScriptPath ([string]$paths.endpointCollectorScript) -ConfigPath $ConfigPath
|
||||||
|
Start-CollectorIfNeeded -ScriptPath ([string]$paths.fileCollectorScript) -ConfigPath $ConfigPath
|
||||||
|
if ($paths.PSObject.Properties.Name -contains 'emailCollectorScript') {
|
||||||
|
Start-CollectorIfNeeded -ScriptPath ([string]$paths.emailCollectorScript) -ConfigPath $ConfigPath
|
||||||
|
}
|
||||||
|
if ($paths.PSObject.Properties.Name -contains 'sessionCollectorScript') {
|
||||||
|
Start-CollectorIfNeeded -ScriptPath ([string]$paths.sessionCollectorScript) -ConfigPath $ConfigPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-ServiceLog ("loop error: {0}" -f $_.Exception.Message)
|
||||||
|
}
|
||||||
|
Start-Sleep -Seconds ([Math]::Max($LoopSeconds, 5))
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
[string]$ServerHost,
|
[string]$ServerHost,
|
||||||
@@ -59,16 +59,18 @@ $resolvedPulseSeconds = if ($PSBoundParameters.ContainsKey('PulseSeconds')) { $P
|
|||||||
$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' }
|
$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' }
|
||||||
$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("browser-domains-{0}.log" -f $env:USERNAME) }
|
$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("browser-domains-{0}.log" -f $env:USERNAME) }
|
||||||
$resolvedIncidentLogPath = if ($IncidentLogPath) { $IncidentLogPath } else { Join-Path $resolvedLogsRoot ("dlp-incidents-{0}.log" -f $env:USERNAME) }
|
$resolvedIncidentLogPath = if ($IncidentLogPath) { $IncidentLogPath } else { Join-Path $resolvedLogsRoot ("dlp-incidents-{0}.log" -f $env:USERNAME) }
|
||||||
|
$resolvedHealthPath = Join-Path $resolvedLogsRoot ("health-browser-domains-{0}.json" -f $env:USERNAME)
|
||||||
$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true }
|
$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true }
|
||||||
$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' }
|
$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' }
|
||||||
$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true }
|
$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true }
|
||||||
|
$resolvedHostname = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$deploymentConfig.awHostname)) { [string]$deploymentConfig.awHostname } else { [string]$env:COMPUTERNAME }
|
||||||
|
|
||||||
if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) {
|
if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) {
|
||||||
New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null
|
New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort
|
$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort
|
||||||
$script:Hostname = $env:COMPUTERNAME
|
$script:Hostname = $resolvedHostname
|
||||||
$script:SessionId = (Get-Process -Id $PID).SessionId
|
$script:SessionId = (Get-Process -Id $PID).SessionId
|
||||||
$script:KnownBuckets = @{}
|
$script:KnownBuckets = @{}
|
||||||
$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled
|
$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled
|
||||||
@@ -85,6 +87,7 @@ $script:DlpDefaults = [ordered]@{
|
|||||||
action = 'log'
|
action = 'log'
|
||||||
severity = 'low'
|
severity = 'low'
|
||||||
}
|
}
|
||||||
|
$script:HealthPath = $resolvedHealthPath
|
||||||
$script:BrowserMap = @{
|
$script:BrowserMap = @{
|
||||||
msedge = 'edge'
|
msedge = 'edge'
|
||||||
chrome = 'chrome'
|
chrome = 'chrome'
|
||||||
@@ -134,6 +137,23 @@ function Write-DlpIncidentLog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Write-CollectorHealth {
|
||||||
|
param([string]$Status = 'running')
|
||||||
|
try {
|
||||||
|
$health = @{
|
||||||
|
collector = 'browser-domains-native'
|
||||||
|
hostname = $script:Hostname
|
||||||
|
sessionId = $script:SessionId
|
||||||
|
status = $Status
|
||||||
|
apiBase = $script:ApiBase
|
||||||
|
ts = (Get-Date).ToUniversalTime().ToString('o')
|
||||||
|
} | ConvertTo-Json -Depth 4
|
||||||
|
Set-Content -LiteralPath $script:HealthPath -Value $health -Encoding UTF8
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Test-DomainMatch {
|
function Test-DomainMatch {
|
||||||
param(
|
param(
|
||||||
[string]$DomainHost,
|
[string]$DomainHost,
|
||||||
@@ -790,46 +810,52 @@ Load-CustomCategoryRules -Path $resolvedRulesPath
|
|||||||
Load-DlpPolicy -Path $resolvedPolicyPath
|
Load-DlpPolicy -Path $resolvedPolicyPath
|
||||||
Write-CollectorLog ("коллектор запущен для {0}" -f $script:ApiBase)
|
Write-CollectorLog ("коллектор запущен для {0}" -f $script:ApiBase)
|
||||||
|
|
||||||
while ($true) {
|
try {
|
||||||
try {
|
while ($true) {
|
||||||
$context = Get-ForegroundWindowContext
|
try {
|
||||||
if ($context -and $script:BrowserMap.ContainsKey($context.ProcessName)) {
|
Write-CollectorHealth -Status 'running'
|
||||||
$url = Get-BrowserUrlFromWindow -Handle $context.Handle
|
$context = Get-ForegroundWindowContext
|
||||||
if ($url) {
|
if ($context -and $script:BrowserMap.ContainsKey($context.ProcessName)) {
|
||||||
$browserKey = $script:BrowserMap[$context.ProcessName]
|
$url = Get-BrowserUrlFromWindow -Handle $context.Handle
|
||||||
$domain = Get-HostFromUrl -Url $url
|
if ($url) {
|
||||||
if (-not $domain) {
|
$browserKey = $script:BrowserMap[$context.ProcessName]
|
||||||
$domain = 'unknown'
|
$domain = Get-HostFromUrl -Url $url
|
||||||
}
|
if (-not $domain) {
|
||||||
|
$domain = 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
$rootDomain = Get-RootDomain -DomainHost $domain
|
$rootDomain = Get-RootDomain -DomainHost $domain
|
||||||
if (-not $rootDomain) {
|
if (-not $rootDomain) {
|
||||||
$rootDomain = $domain
|
$rootDomain = $domain
|
||||||
}
|
}
|
||||||
|
|
||||||
$category = Get-WebCategory -DomainHost $domain
|
$category = Get-WebCategory -DomainHost $domain
|
||||||
$bucketId = 'aw-watcher-web-{0}_{1}' -f $browserKey, $script:Hostname
|
$bucketId = 'aw-watcher-web-{0}_{1}' -f $browserKey, $script:Hostname
|
||||||
Ensure-Bucket -BucketId $bucketId -ClientName ('aw-watcher-web-' + $browserKey)
|
Ensure-Bucket -BucketId $bucketId -ClientName ('aw-watcher-web-' + $browserKey)
|
||||||
Send-Heartbeat -BucketId $bucketId -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName
|
Send-Heartbeat -BucketId $bucketId -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName
|
||||||
Send-CategoryHeartbeat -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group -CategoryRule $category.Rule
|
Send-CategoryHeartbeat -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group -CategoryRule $category.Rule
|
||||||
|
|
||||||
$decision = Get-DlpDecision -Domain $domain -RootDomain $rootDomain -Url $url -Title $context.Title -BrowserKey $browserKey -Category $category.Name -CategoryGroup $category.Group
|
$decision = Get-DlpDecision -Domain $domain -RootDomain $rootDomain -Url $url -Title $context.Title -BrowserKey $browserKey -Category $category.Name -CategoryGroup $category.Group
|
||||||
if ($decision) {
|
if ($decision) {
|
||||||
$fingerprint = '{0}|{1}|{2}|{3}' -f $decision.id, $browserKey, $rootDomain, $env:USERNAME
|
$fingerprint = '{0}|{1}|{2}|{3}' -f $decision.id, $browserKey, $rootDomain, $env:USERNAME
|
||||||
$cooldown = [Math]::Max([int]$decision.cooldownSeconds, 30)
|
$cooldown = [Math]::Max([int]$decision.cooldownSeconds, 30)
|
||||||
if (Should-EmitIncident -Fingerprint $fingerprint -CooldownSeconds $cooldown) {
|
if (Should-EmitIncident -Fingerprint $fingerprint -CooldownSeconds $cooldown) {
|
||||||
Write-DlpIncidentLog ("{0} {1} {2} {3}" -f $decision.severity, $decision.action, $decision.id, $url)
|
Write-DlpIncidentLog ("{0} {1} {2} {3}" -f $decision.severity, $decision.action, $decision.id, $url)
|
||||||
if (@('alert', 'block', 'quarantine') -contains ([string]$decision.action).ToLowerInvariant()) {
|
if (@('alert', 'block', 'quarantine') -contains ([string]$decision.action).ToLowerInvariant()) {
|
||||||
Send-DlpIncidentHeartbeat -Decision $decision -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group
|
Send-DlpIncidentHeartbeat -Decision $decision -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
catch {
|
||||||
catch {
|
Write-CollectorLog ("ошибка коллектора: {0}" -f $_.Exception.Message)
|
||||||
Write-CollectorLog ("ошибка коллектора: {0}" -f $_.Exception.Message)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Start-Sleep -Seconds $resolvedPollSeconds
|
Start-Sleep -Seconds $resolvedPollSeconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Write-CollectorHealth -Status 'stopped'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$ServerHost,
|
[string]$ServerHost,
|
||||||
@@ -24,6 +24,7 @@ param(
|
|||||||
[bool]$IncidentScreenshotEnabled = $true,
|
[bool]$IncidentScreenshotEnabled = $true,
|
||||||
[string]$IncidentArtifactsRoot,
|
[string]$IncidentArtifactsRoot,
|
||||||
[bool]$LogonMarkerEnabled = $true,
|
[bool]$LogonMarkerEnabled = $true,
|
||||||
|
[string]$AwHostname,
|
||||||
[string]$CustomRulesPath,
|
[string]$CustomRulesPath,
|
||||||
[string]$CustomPolicyPath
|
[string]$CustomPolicyPath
|
||||||
)
|
)
|
||||||
@@ -100,6 +101,7 @@ $config = New-ActivityWatchDeploymentConfig `
|
|||||||
-IncidentScreenshotEnabled $IncidentScreenshotEnabled `
|
-IncidentScreenshotEnabled $IncidentScreenshotEnabled `
|
||||||
-IncidentArtifactsRoot $IncidentArtifactsRoot `
|
-IncidentArtifactsRoot $IncidentArtifactsRoot `
|
||||||
-LogonMarkerEnabled $LogonMarkerEnabled `
|
-LogonMarkerEnabled $LogonMarkerEnabled `
|
||||||
|
-AwHostname $AwHostname `
|
||||||
-LaunchScriptPath $launchScriptPath `
|
-LaunchScriptPath $launchScriptPath `
|
||||||
-RecoveryScriptPath $recoveryScriptPath `
|
-RecoveryScriptPath $recoveryScriptPath `
|
||||||
-UserTasks $taskDefinitions `
|
-UserTasks $taskDefinitions `
|
||||||
@@ -112,8 +114,8 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
|
|||||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
||||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||||
|
|
||||||
Write-Host 'ActivityWatch развёрнут для пользователей:'
|
Write-Output 'ActivityWatch развёрнут для пользователей:'
|
||||||
$targetUsers | ForEach-Object { Write-Host " - $_" }
|
$targetUsers | ForEach-Object { Write-Output " - $_" }
|
||||||
Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
Write-Output "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
||||||
Write-Host "Каталог данных: $StateRoot"
|
Write-Output "Каталог данных: $StateRoot"
|
||||||
Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
Write-Output "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$ServerHost,
|
[string]$ServerHost,
|
||||||
@@ -24,6 +24,7 @@ param(
|
|||||||
[bool]$IncidentScreenshotEnabled = $true,
|
[bool]$IncidentScreenshotEnabled = $true,
|
||||||
[string]$IncidentArtifactsRoot,
|
[string]$IncidentArtifactsRoot,
|
||||||
[bool]$LogonMarkerEnabled = $true,
|
[bool]$LogonMarkerEnabled = $true,
|
||||||
|
[string]$AwHostname,
|
||||||
[string]$CustomRulesPath,
|
[string]$CustomRulesPath,
|
||||||
[string]$CustomPolicyPath,
|
[string]$CustomPolicyPath,
|
||||||
[string]$ReportPath,
|
[string]$ReportPath,
|
||||||
@@ -71,6 +72,7 @@ if (-not (Test-Path -LiteralPath $deployScript)) {
|
|||||||
-IncidentScreenshotEnabled $IncidentScreenshotEnabled `
|
-IncidentScreenshotEnabled $IncidentScreenshotEnabled `
|
||||||
-IncidentArtifactsRoot $IncidentArtifactsRoot `
|
-IncidentArtifactsRoot $IncidentArtifactsRoot `
|
||||||
-LogonMarkerEnabled $LogonMarkerEnabled `
|
-LogonMarkerEnabled $LogonMarkerEnabled `
|
||||||
|
-AwHostname $AwHostname `
|
||||||
-CustomRulesPath $CustomRulesPath `
|
-CustomRulesPath $CustomRulesPath `
|
||||||
-CustomPolicyPath $CustomPolicyPath
|
-CustomPolicyPath $CustomPolicyPath
|
||||||
|
|
||||||
@@ -94,6 +96,7 @@ if (-not $SkipHardening) {
|
|||||||
-IncidentScreenshotEnabled $IncidentScreenshotEnabled `
|
-IncidentScreenshotEnabled $IncidentScreenshotEnabled `
|
||||||
-IncidentArtifactsRoot $IncidentArtifactsRoot `
|
-IncidentArtifactsRoot $IncidentArtifactsRoot `
|
||||||
-LogonMarkerEnabled $LogonMarkerEnabled `
|
-LogonMarkerEnabled $LogonMarkerEnabled `
|
||||||
|
-AwHostname $AwHostname `
|
||||||
-CustomRulesPath $CustomRulesPath `
|
-CustomRulesPath $CustomRulesPath `
|
||||||
-CustomPolicyPath $CustomPolicyPath
|
-CustomPolicyPath $CustomPolicyPath
|
||||||
}
|
}
|
||||||
@@ -136,6 +139,6 @@ if ($reportDirectory) {
|
|||||||
|
|
||||||
$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $effectiveReportPath -Encoding UTF8
|
$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $effectiveReportPath -Encoding UTF8
|
||||||
|
|
||||||
Write-Host 'Комплексное развёртывание ActivityWatch завершено.'
|
Write-Output 'Комплексное развёртывание ActivityWatch завершено.'
|
||||||
Write-Host "Пользователи: $($resolvedUsers -join ', ')"
|
Write-Output "Пользователи: $($resolvedUsers -join ', ')"
|
||||||
Write-Host "Отчёт: $effectiveReportPath"
|
Write-Output "Отчёт: $effectiveReportPath"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$ServerHost,
|
[string]$ServerHost,
|
||||||
@@ -22,6 +22,7 @@ param(
|
|||||||
[bool]$IncidentScreenshotEnabled = $true,
|
[bool]$IncidentScreenshotEnabled = $true,
|
||||||
[string]$IncidentArtifactsRoot,
|
[string]$IncidentArtifactsRoot,
|
||||||
[bool]$LogonMarkerEnabled = $true,
|
[bool]$LogonMarkerEnabled = $true,
|
||||||
|
[string]$AwHostname,
|
||||||
[string]$CustomRulesPath,
|
[string]$CustomRulesPath,
|
||||||
[string]$CustomPolicyPath
|
[string]$CustomPolicyPath
|
||||||
)
|
)
|
||||||
@@ -92,6 +93,7 @@ $config = New-ActivityWatchDeploymentConfig `
|
|||||||
-IncidentScreenshotEnabled $IncidentScreenshotEnabled `
|
-IncidentScreenshotEnabled $IncidentScreenshotEnabled `
|
||||||
-IncidentArtifactsRoot $IncidentArtifactsRoot `
|
-IncidentArtifactsRoot $IncidentArtifactsRoot `
|
||||||
-LogonMarkerEnabled $LogonMarkerEnabled `
|
-LogonMarkerEnabled $LogonMarkerEnabled `
|
||||||
|
-AwHostname $AwHostname `
|
||||||
-LaunchScriptPath $launchScriptPath `
|
-LaunchScriptPath $launchScriptPath `
|
||||||
-RecoveryScriptPath $recoveryScriptPath `
|
-RecoveryScriptPath $recoveryScriptPath `
|
||||||
-UserTasks $taskDefinitions `
|
-UserTasks $taskDefinitions `
|
||||||
@@ -104,9 +106,9 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
|
|||||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
||||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||||
|
|
||||||
Write-Host "ActivityWatch развёрнут для пользователя: $TargetUser"
|
Write-Output "ActivityWatch развёрнут для пользователя: $TargetUser"
|
||||||
Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
Write-Output "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
||||||
Write-Host "Каталог установки: $InstallRoot"
|
Write-Output "Каталог установки: $InstallRoot"
|
||||||
Write-Host "Каталог данных: $StateRoot"
|
Write-Output "Каталог данных: $StateRoot"
|
||||||
Write-Host "Файл правил: $($assetResult.ActiveRules)"
|
Write-Output "Файл правил: $($assetResult.ActiveRules)"
|
||||||
Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
Write-Output "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -53,13 +53,79 @@ function Write-CollectorLog {
|
|||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
function Invoke-AwJsonPost {
|
function Add-WalEntry {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)][string]$Uri,
|
[Parameter(Mandatory = $true)][string]$Uri,
|
||||||
[Parameter(Mandatory = $true)][string]$Json
|
[Parameter(Mandatory = $true)][string]$Json
|
||||||
)
|
)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($script:WalPath)) { return }
|
||||||
|
try {
|
||||||
|
$entry = @{ ts = (Get-Date).ToUniversalTime().ToString('o'); uri = $Uri; json = $Json } | ConvertTo-Json -Compress
|
||||||
|
Add-Content -LiteralPath $script:WalPath -Value $entry -Encoding UTF8
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Flush-Wal {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($script:WalPath) -or -not (Test-Path -LiteralPath $script:WalPath)) { return }
|
||||||
|
$remaining = New-Object System.Collections.Generic.List[string]
|
||||||
|
try {
|
||||||
|
$script:WalFlushing = $true
|
||||||
|
foreach ($line in (Get-Content -LiteralPath $script:WalPath -ErrorAction SilentlyContinue)) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($line)) { continue }
|
||||||
|
try {
|
||||||
|
$entry = $line | ConvertFrom-Json
|
||||||
|
if ($null -eq $entry -or -not $entry.uri -or -not $entry.json) { continue }
|
||||||
|
if (-not (Invoke-AwJsonPost -Uri ([string]$entry.uri) -Json ([string]$entry.json))) { $remaining.Add($line) }
|
||||||
|
} catch { $remaining.Add($line) }
|
||||||
|
}
|
||||||
|
if ($remaining.Count -eq 0) {
|
||||||
|
Remove-Item -LiteralPath $script:WalPath -Force -ErrorAction SilentlyContinue
|
||||||
|
} else {
|
||||||
|
Set-Content -LiteralPath $script:WalPath -Value ($remaining -join [Environment]::NewLine) -Encoding UTF8
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
$script:WalFlushing = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-CollectorHealth {
|
||||||
|
param([string]$Status = 'running')
|
||||||
|
if ([string]::IsNullOrWhiteSpace($script:HealthPath)) { return }
|
||||||
|
try {
|
||||||
|
$walDepth = 0
|
||||||
|
if ($script:WalPath -and (Test-Path -LiteralPath $script:WalPath)) { $walDepth = @((Get-Content -LiteralPath $script:WalPath)).Count }
|
||||||
|
$health = @{
|
||||||
|
collector = 'email-outbound'; hostname = $script:Hostname; sessionId = $script:SessionId;
|
||||||
|
status = $Status; apiBase = $script:ApiBase; walDepth = $walDepth; ts = (Get-Date).ToUniversalTime().ToString('o')
|
||||||
|
} | ConvertTo-Json -Depth 5
|
||||||
|
Set-Content -LiteralPath $script:HealthPath -Value $health -Encoding UTF8
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-AwJsonPost {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$Uri,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Json,
|
||||||
|
[int]$MaxAttempts = 5,
|
||||||
|
[int]$InitialBackoffMs = 500
|
||||||
|
)
|
||||||
|
$attempt = 1
|
||||||
|
$backoff = [Math]::Max(100, $InitialBackoffMs)
|
||||||
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||||
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
while ($attempt -le $MaxAttempts) {
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
if ($attempt -ge $MaxAttempts) {
|
||||||
|
if (-not $script:WalFlushing) { Add-WalEntry -Uri $Uri -Json $Json }
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
Start-Sleep -Milliseconds $backoff
|
||||||
|
$backoff = [Math]::Min($backoff * 2, 10000)
|
||||||
|
$attempt++
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Ensure-Bucket {
|
function Ensure-Bucket {
|
||||||
@@ -517,6 +583,10 @@ $script:SeenSmtpConnections = @{}
|
|||||||
$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30)
|
$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30)
|
||||||
$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled
|
$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled
|
||||||
$script:LogPath = $resolvedLogPath
|
$script:LogPath = $resolvedLogPath
|
||||||
|
$stateRoot = if ($deploymentConfig -and $deploymentConfig.paths -and $deploymentConfig.paths.stateRoot) { [string]$deploymentConfig.paths.stateRoot } else { 'C:\ProgramData\AWatch-rus' }
|
||||||
|
$script:WalPath = Join-Path $stateRoot 'wal-email-outbound.ndjson'
|
||||||
|
$script:HealthPath = Join-Path $stateRoot 'health-email-outbound.json'
|
||||||
|
$script:WalFlushing = $false
|
||||||
$script:OutlookApp = $null
|
$script:OutlookApp = $null
|
||||||
$script:OutlookNamespace = $null
|
$script:OutlookNamespace = $null
|
||||||
$script:SentFolder = $null
|
$script:SentFolder = $null
|
||||||
@@ -540,43 +610,65 @@ if ($useOutlook) {
|
|||||||
# Main loop
|
# Main loop
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
while ($true) {
|
try {
|
||||||
try {
|
while ($true) {
|
||||||
if (-not $script:Policy.defaults.enabled) {
|
try {
|
||||||
Start-Sleep -Seconds $resolvedPollSeconds
|
Flush-Wal
|
||||||
continue
|
Write-CollectorHealth -Status 'running'
|
||||||
}
|
if (-not $script:Policy.defaults.enabled) {
|
||||||
|
Start-Sleep -Seconds $resolvedPollSeconds
|
||||||
if ($useOutlook) {
|
continue
|
||||||
if (-not $outlookReady) {
|
|
||||||
$outlookReady = Initialize-OutlookCom
|
|
||||||
}
|
}
|
||||||
if ($outlookReady) {
|
|
||||||
|
if ($useOutlook) {
|
||||||
|
if (-not $outlookReady) {
|
||||||
|
$outlookReady = Initialize-OutlookCom
|
||||||
|
}
|
||||||
|
if ($outlookReady) {
|
||||||
|
try {
|
||||||
|
Poll-OutlookSentItems
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-CollectorLog ("outlook poll error: {0}" -f $_.Exception.Message)
|
||||||
|
$outlookReady = $false
|
||||||
|
$script:OutlookApp = $null
|
||||||
|
$script:OutlookNamespace = $null
|
||||||
|
$script:SentFolder = $null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($useSmtp) {
|
||||||
try {
|
try {
|
||||||
Poll-OutlookSentItems
|
Poll-SmtpConnections
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
Write-CollectorLog ("outlook poll error: {0}" -f $_.Exception.Message)
|
Write-CollectorLog ("smtp poll error: {0}" -f $_.Exception.Message)
|
||||||
$outlookReady = $false
|
|
||||||
$script:OutlookApp = $null
|
|
||||||
$script:OutlookNamespace = $null
|
|
||||||
$script:SentFolder = $null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
|
Write-CollectorLog ("collector error: {0}" -f $_.Exception.Message)
|
||||||
|
}
|
||||||
|
|
||||||
if ($useSmtp) {
|
Start-Sleep -Seconds $resolvedPollSeconds
|
||||||
try {
|
}
|
||||||
Poll-SmtpConnections
|
}
|
||||||
}
|
finally {
|
||||||
catch {
|
Write-CollectorHealth -Status 'stopped'
|
||||||
Write-CollectorLog ("smtp poll error: {0}" -f $_.Exception.Message)
|
try {
|
||||||
}
|
if ($null -ne $script:SentFolder) {
|
||||||
|
[void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($script:SentFolder)
|
||||||
|
$script:SentFolder = $null
|
||||||
|
}
|
||||||
|
if ($null -ne $script:OutlookNamespace) {
|
||||||
|
[void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($script:OutlookNamespace)
|
||||||
|
$script:OutlookNamespace = $null
|
||||||
|
}
|
||||||
|
if ($null -ne $script:OutlookApp) {
|
||||||
|
[void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($script:OutlookApp)
|
||||||
|
$script:OutlookApp = $null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {}
|
||||||
Write-CollectorLog ("collector error: {0}" -f $_.Exception.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
Start-Sleep -Seconds $resolvedPollSeconds
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
[string]$ServerHost,
|
[string]$ServerHost,
|
||||||
@@ -22,6 +22,9 @@ Add-Type -AssemblyName System.Net.Http
|
|||||||
$script:KnownBuckets = @{}
|
$script:KnownBuckets = @{}
|
||||||
$script:Hostname = $env:COMPUTERNAME
|
$script:Hostname = $env:COMPUTERNAME
|
||||||
$script:SessionId = [System.Diagnostics.Process]::GetCurrentProcess().SessionId
|
$script:SessionId = [System.Diagnostics.Process]::GetCurrentProcess().SessionId
|
||||||
|
$script:WalPath = $null
|
||||||
|
$script:HealthPath = $null
|
||||||
|
$script:WalFlushing = $false
|
||||||
|
|
||||||
# Настройка логирования
|
# Настройка логирования
|
||||||
$script:LogPath = $LogPath
|
$script:LogPath = $LogPath
|
||||||
@@ -43,28 +46,88 @@ function Write-FileCollectorLog {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Invoke-AwJsonPost {
|
function Add-WalEntry {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)][string]$Uri,
|
[Parameter(Mandatory = $true)][string]$Uri,
|
||||||
[Parameter(Mandatory = $true)][string]$Json
|
[Parameter(Mandatory = $true)][string]$Json
|
||||||
)
|
)
|
||||||
$httpClient = $null
|
if ([string]::IsNullOrWhiteSpace($script:WalPath)) { return }
|
||||||
try {
|
try {
|
||||||
$httpClient = New-Object System.Net.Http.HttpClient
|
$entry = @{ ts = (Get-Date).ToUniversalTime().ToString('o'); uri = $Uri; json = $Json } | ConvertTo-Json -Compress
|
||||||
$content = New-Object System.Net.Http.StringContent($Json, [System.Text.Encoding]::UTF8, "application/json")
|
Add-Content -LiteralPath $script:WalPath -Value $entry -Encoding UTF8
|
||||||
$response = $httpClient.PostAsync($Uri, $content).Result
|
} catch {}
|
||||||
if (-not $response.IsSuccessStatusCode) {
|
}
|
||||||
$status = [int]$response.StatusCode
|
|
||||||
$reason = [string]$response.ReasonPhrase
|
function Flush-Wal {
|
||||||
$body = $response.Content.ReadAsStringAsync().Result
|
if ([string]::IsNullOrWhiteSpace($script:WalPath) -or -not (Test-Path -LiteralPath $script:WalPath)) { return }
|
||||||
Write-FileCollectorLog ("POST failed: uri={0} status={1} reason={2} body={3}" -f $Uri, $status, $reason, $body)
|
$remaining = New-Object System.Collections.Generic.List[string]
|
||||||
|
try {
|
||||||
|
$script:WalFlushing = $true
|
||||||
|
foreach ($line in (Get-Content -LiteralPath $script:WalPath -ErrorAction SilentlyContinue)) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($line)) { continue }
|
||||||
|
try {
|
||||||
|
$entry = $line | ConvertFrom-Json
|
||||||
|
if ($null -eq $entry -or -not $entry.uri -or -not $entry.json) { continue }
|
||||||
|
if (-not (Invoke-AwJsonPost -Uri ([string]$entry.uri) -Json ([string]$entry.json))) { $remaining.Add($line) }
|
||||||
|
} catch { $remaining.Add($line) }
|
||||||
|
}
|
||||||
|
if ($remaining.Count -eq 0) {
|
||||||
|
Remove-Item -LiteralPath $script:WalPath -Force -ErrorAction SilentlyContinue
|
||||||
|
} else {
|
||||||
|
Set-Content -LiteralPath $script:WalPath -Value ($remaining -join [Environment]::NewLine) -Encoding UTF8
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
Write-FileCollectorLog "POST Error: $($_.Exception.Message)"
|
|
||||||
} finally {
|
} finally {
|
||||||
if ($null -ne $httpClient) {
|
$script:WalFlushing = $false
|
||||||
$httpClient.Dispose()
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-CollectorHealth {
|
||||||
|
param([string]$Status = 'running')
|
||||||
|
if ([string]::IsNullOrWhiteSpace($script:HealthPath)) { return }
|
||||||
|
try {
|
||||||
|
$walDepth = 0
|
||||||
|
if ($script:WalPath -and (Test-Path -LiteralPath $script:WalPath)) { $walDepth = @((Get-Content -LiteralPath $script:WalPath)).Count }
|
||||||
|
$health = @{
|
||||||
|
collector = 'file-operations'; hostname = $script:Hostname; sessionId = $script:SessionId;
|
||||||
|
status = $Status; apiBase = $script:ApiBase; walDepth = $walDepth; ts = (Get-Date).ToUniversalTime().ToString('o')
|
||||||
|
} | ConvertTo-Json -Depth 5
|
||||||
|
Set-Content -LiteralPath $script:HealthPath -Value $health -Encoding UTF8
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-AwJsonPost {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$Uri,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Json,
|
||||||
|
[int]$MaxAttempts = 5,
|
||||||
|
[int]$InitialBackoffMs = 500
|
||||||
|
)
|
||||||
|
$attempt = 1
|
||||||
|
$backoff = [Math]::Max(100, $InitialBackoffMs)
|
||||||
|
while ($attempt -le $MaxAttempts) {
|
||||||
|
$httpClient = $null
|
||||||
|
try {
|
||||||
|
$httpClient = New-Object System.Net.Http.HttpClient
|
||||||
|
$content = New-Object System.Net.Http.StringContent($Json, [System.Text.Encoding]::UTF8, "application/json")
|
||||||
|
$response = $httpClient.PostAsync($Uri, $content).Result
|
||||||
|
if ($response.IsSuccessStatusCode) { return $true }
|
||||||
|
if ($attempt -ge $MaxAttempts) {
|
||||||
|
if (-not $script:WalFlushing) { Add-WalEntry -Uri $Uri -Json $Json }
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if ($attempt -ge $MaxAttempts) {
|
||||||
|
if (-not $script:WalFlushing) { Add-WalEntry -Uri $Uri -Json $Json }
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if ($null -ne $httpClient) {
|
||||||
|
$httpClient.Dispose()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Start-Sleep -Milliseconds $backoff
|
||||||
|
$backoff = [Math]::Min($backoff * 2, 10000)
|
||||||
|
$attempt++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,11 +208,15 @@ function Send-FileOperationEvent {
|
|||||||
|
|
||||||
$config = Get-DeploymentConfig -Path $ConfigPath
|
$config = Get-DeploymentConfig -Path $ConfigPath
|
||||||
if (-not $config) { throw "Configuration file not found: $ConfigPath" }
|
if (-not $config) { throw "Configuration file not found: $ConfigPath" }
|
||||||
|
$script:Hostname = if ($config.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$config.awHostname)) { [string]$config.awHostname } else { [string]$env:COMPUTERNAME }
|
||||||
|
|
||||||
$scheme = if ($ServerScheme) { $ServerScheme } elseif ($config.server.scheme) { $config.server.scheme } else { 'http' }
|
$scheme = if ($ServerScheme) { $ServerScheme } elseif ($config.server.scheme) { $config.server.scheme } else { 'http' }
|
||||||
$hostName = if ($ServerHost) { $ServerHost } elseif ($config.server.host) { $config.server.host } else { 'localhost' }
|
$hostName = if ($ServerHost) { $ServerHost } elseif ($config.server.host) { $config.server.host } else { 'localhost' }
|
||||||
$port = if ($ServerPort) { $ServerPort } elseif ($config.server.port) { $config.server.port } else { 5600 }
|
$port = if ($ServerPort) { $ServerPort } elseif ($config.server.port) { $config.server.port } else { 5600 }
|
||||||
$script:ApiBase = "{0}://{1}:{2}/api/0" -f $scheme, $hostName, $port
|
$script:ApiBase = "{0}://{1}:{2}/api/0" -f $scheme, $hostName, $port
|
||||||
|
$stateRoot = if ($config.paths -and $config.paths.stateRoot) { [string]$config.paths.stateRoot } else { 'C:\ProgramData\AWatch-rus' }
|
||||||
|
$script:WalPath = Join-Path $stateRoot 'wal-file-operations.ndjson'
|
||||||
|
$script:HealthPath = Join-Path $stateRoot 'health-file-operations.json'
|
||||||
|
|
||||||
$bucketId = 'aw-file-operations_' + $script:Hostname
|
$bucketId = 'aw-file-operations_' + $script:Hostname
|
||||||
Ensure-Bucket -BucketId $bucketId -ClientName 'aw-file-operations' -BucketType 'aw.file.operation'
|
Ensure-Bucket -BucketId $bucketId -ClientName 'aw-file-operations' -BucketType 'aw.file.operation'
|
||||||
@@ -206,11 +273,14 @@ Write-FileCollectorLog "Collector started. Waiting for events..."
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
while ($true) {
|
while ($true) {
|
||||||
|
Flush-Wal
|
||||||
|
Write-CollectorHealth -Status 'running'
|
||||||
Start-Sleep -Seconds $PollSeconds
|
Start-Sleep -Seconds $PollSeconds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
Write-FileCollectorLog "Stopping collector..."
|
Write-FileCollectorLog "Stopping collector..."
|
||||||
|
Write-CollectorHealth -Status 'stopped'
|
||||||
foreach ($sub in @($subscriptions)) {
|
foreach ($sub in @($subscriptions)) {
|
||||||
try {
|
try {
|
||||||
if ($sub -and $sub.Id) {
|
if ($sub -and $sub.Id) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
[string]$ServerHost,
|
[string]$ServerHost,
|
||||||
@@ -21,6 +21,7 @@ param(
|
|||||||
[bool]$IncidentScreenshotEnabled,
|
[bool]$IncidentScreenshotEnabled,
|
||||||
[string]$IncidentArtifactsRoot,
|
[string]$IncidentArtifactsRoot,
|
||||||
[bool]$LogonMarkerEnabled,
|
[bool]$LogonMarkerEnabled,
|
||||||
|
[string]$AwHostname,
|
||||||
[string]$CustomRulesPath,
|
[string]$CustomRulesPath,
|
||||||
[string]$CustomPolicyPath,
|
[string]$CustomPolicyPath,
|
||||||
[switch]$RepairPackage,
|
[switch]$RepairPackage,
|
||||||
@@ -73,6 +74,7 @@ $effectiveIncidentCaptureEnabled = if ($PSBoundParameters.ContainsKey('IncidentC
|
|||||||
$effectiveIncidentScreenshotEnabled = if ($PSBoundParameters.ContainsKey('IncidentScreenshotEnabled')) { [bool]$IncidentScreenshotEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $existingConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$existingConfig.incidentCapture.screenshotEnabled } else { $true }
|
$effectiveIncidentScreenshotEnabled = if ($PSBoundParameters.ContainsKey('IncidentScreenshotEnabled')) { [bool]$IncidentScreenshotEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $existingConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$existingConfig.incidentCapture.screenshotEnabled } else { $true }
|
||||||
$effectiveIncidentArtifactsRoot = if ($PSBoundParameters.ContainsKey('IncidentArtifactsRoot') -and $IncidentArtifactsRoot) { $IncidentArtifactsRoot } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $existingConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$existingConfig.incidentCapture.artifactsRoot } else { Join-Path $effectiveStateRoot 'incident-artifacts' }
|
$effectiveIncidentArtifactsRoot = if ($PSBoundParameters.ContainsKey('IncidentArtifactsRoot') -and $IncidentArtifactsRoot) { $IncidentArtifactsRoot } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $existingConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$existingConfig.incidentCapture.artifactsRoot } else { Join-Path $effectiveStateRoot 'incident-artifacts' }
|
||||||
$effectiveLogonMarkerEnabled = if ($PSBoundParameters.ContainsKey('LogonMarkerEnabled')) { [bool]$LogonMarkerEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'sessionEvents' -and $existingConfig.sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]$existingConfig.sessionEvents.logonEnabled } else { $true }
|
$effectiveLogonMarkerEnabled = if ($PSBoundParameters.ContainsKey('LogonMarkerEnabled')) { [bool]$LogonMarkerEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'sessionEvents' -and $existingConfig.sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]$existingConfig.sessionEvents.logonEnabled } else { $true }
|
||||||
|
$effectiveAwHostname = if ($PSBoundParameters.ContainsKey('AwHostname') -and -not [string]::IsNullOrWhiteSpace($AwHostname)) { [string]$AwHostname } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$existingConfig.awHostname)) { [string]$existingConfig.awHostname } else { [string]$env:COMPUTERNAME }
|
||||||
$effectiveVersion = if ($Version) { $Version } elseif ($existingConfig) { [string]$existingConfig.package.version } else { 'v0.13.2' }
|
$effectiveVersion = if ($Version) { $Version } elseif ($existingConfig) { [string]$existingConfig.package.version } else { 'v0.13.2' }
|
||||||
|
|
||||||
$effectiveUsers = if ($Users -or $UserListPath) {
|
$effectiveUsers = if ($Users -or $UserListPath) {
|
||||||
@@ -139,6 +141,7 @@ $config = New-ActivityWatchDeploymentConfig `
|
|||||||
-IncidentScreenshotEnabled $effectiveIncidentScreenshotEnabled `
|
-IncidentScreenshotEnabled $effectiveIncidentScreenshotEnabled `
|
||||||
-IncidentArtifactsRoot $effectiveIncidentArtifactsRoot `
|
-IncidentArtifactsRoot $effectiveIncidentArtifactsRoot `
|
||||||
-LogonMarkerEnabled $effectiveLogonMarkerEnabled `
|
-LogonMarkerEnabled $effectiveLogonMarkerEnabled `
|
||||||
|
-AwHostname $effectiveAwHostname `
|
||||||
-LaunchScriptPath $effectiveLaunchScript `
|
-LaunchScriptPath $effectiveLaunchScript `
|
||||||
-RecoveryScriptPath $effectiveRecoveryScript `
|
-RecoveryScriptPath $effectiveRecoveryScript `
|
||||||
-UserTasks $taskDefinitions `
|
-UserTasks $taskDefinitions `
|
||||||
@@ -151,6 +154,6 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
|
|||||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath
|
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath
|
||||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||||
|
|
||||||
Write-Host 'Укрепление и восстановление ActivityWatch завершены.'
|
Write-Output 'Укрепление и восстановление ActivityWatch завершены.'
|
||||||
Write-Host "Конфигурация: $effectiveConfigPath"
|
Write-Output "Конфигурация: $effectiveConfigPath"
|
||||||
Write-Host "Пользователи восстановлены: $($effectiveUsers -join ', ')"
|
Write-Output "Пользователи восстановлены: $($effectiveUsers -join ', ')"
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ServerHost,
|
||||||
|
[int]$ServerPort = 5600,
|
||||||
|
[ValidateSet('http', 'https')]
|
||||||
|
[string]$ServerScheme = 'http',
|
||||||
|
[string]$StateRoot = 'C:\ProgramData\AWatch-rus',
|
||||||
|
[string]$InstallRoot = 'C:\Program Files\AWatch-rus\bin',
|
||||||
|
[string]$ServiceName = 'AWatchRusStandaloneAgent',
|
||||||
|
[string]$AwHostname
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Assert-Admin {
|
||||||
|
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
|
$p = [Security.Principal.WindowsPrincipal]::new($id)
|
||||||
|
if (-not $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||||
|
throw 'Run as Administrator.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-Dir {
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) {
|
||||||
|
New-Item -Path $Path -ItemType Directory -Force | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-Admin
|
||||||
|
|
||||||
|
$logsRoot = Join-Path $StateRoot 'logs'
|
||||||
|
Ensure-Dir -Path $StateRoot
|
||||||
|
Ensure-Dir -Path $logsRoot
|
||||||
|
|
||||||
|
$collectorScript = Join-Path $StateRoot 'browser-domains-native-collector.ps1'
|
||||||
|
$endpointCollectorScript = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1'
|
||||||
|
$fileCollectorScript = Join-Path $StateRoot 'file-operations-collector.ps1'
|
||||||
|
$emailCollectorScript = Join-Path $StateRoot 'email-outbound-collector.ps1'
|
||||||
|
$sessionCollectorScript = Join-Path $StateRoot 'worktime-session-collector.ps1'
|
||||||
|
$rulesPath = Join-Path $StateRoot 'web-category-rules.json'
|
||||||
|
$policyPath = Join-Path $StateRoot 'dlp-policy.json'
|
||||||
|
$configPath = Join-Path $StateRoot 'deployment-config.json'
|
||||||
|
$serviceScriptPath = Join-Path $PSScriptRoot 'aw-standalone-service.ps1'
|
||||||
|
|
||||||
|
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1') -Destination $collectorScript -Force
|
||||||
|
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1') -Destination $endpointCollectorScript -Force
|
||||||
|
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'file-operations-collector.ps1') -Destination $fileCollectorScript -Force
|
||||||
|
if (Test-Path -LiteralPath (Join-Path $PSScriptRoot 'email-outbound-collector.ps1')) {
|
||||||
|
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'email-outbound-collector.ps1') -Destination $emailCollectorScript -Force
|
||||||
|
}
|
||||||
|
if (Test-Path -LiteralPath (Join-Path $PSScriptRoot 'worktime-session-collector.ps1')) {
|
||||||
|
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'worktime-session-collector.ps1') -Destination $sessionCollectorScript -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $rulesPath)) {
|
||||||
|
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'web-category-rules.example.json') -Destination $rulesPath -Force
|
||||||
|
}
|
||||||
|
if (-not (Test-Path -LiteralPath $policyPath)) {
|
||||||
|
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'dlp-policy.example.json') -Destination $policyPath -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
$effectiveHostname = if ([string]::IsNullOrWhiteSpace($AwHostname)) { [string]$env:COMPUTERNAME } else { [string]$AwHostname }
|
||||||
|
|
||||||
|
$config = [pscustomobject]@{
|
||||||
|
version = 1
|
||||||
|
generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
|
||||||
|
awHostname = $effectiveHostname
|
||||||
|
server = [pscustomobject]@{
|
||||||
|
host = $ServerHost
|
||||||
|
port = $ServerPort
|
||||||
|
scheme = $ServerScheme
|
||||||
|
}
|
||||||
|
paths = [pscustomobject]@{
|
||||||
|
installRoot = $InstallRoot
|
||||||
|
stateRoot = $StateRoot
|
||||||
|
logsRoot = $logsRoot
|
||||||
|
collectorScript = $collectorScript
|
||||||
|
endpointCollectorScript = $endpointCollectorScript
|
||||||
|
fileCollectorScript = $fileCollectorScript
|
||||||
|
emailCollectorScript = $emailCollectorScript
|
||||||
|
sessionCollectorScript = $sessionCollectorScript
|
||||||
|
rulesPath = $rulesPath
|
||||||
|
policyPath = $policyPath
|
||||||
|
}
|
||||||
|
collector = [pscustomobject]@{
|
||||||
|
pollSeconds = 5
|
||||||
|
pulseSeconds = 30
|
||||||
|
}
|
||||||
|
collectors = [pscustomobject]@{
|
||||||
|
afkEnabled = $false
|
||||||
|
windowEnabled = $false
|
||||||
|
fileOpsEnabled = $true
|
||||||
|
emailEnabled = $true
|
||||||
|
}
|
||||||
|
logging = [pscustomobject]@{
|
||||||
|
localAgentLogsEnabled = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$config | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $configPath -Encoding UTF8
|
||||||
|
|
||||||
|
$existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
||||||
|
if ($existing) {
|
||||||
|
sc.exe stop $ServiceName | Out-Null
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
sc.exe delete $ServiceName | Out-Null
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$binPath = "`"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`" -NoProfile -ExecutionPolicy Bypass -File `"$serviceScriptPath`" -ConfigPath `"$configPath`""
|
||||||
|
sc.exe create $ServiceName binPath= "$binPath" start= auto DisplayName= "AWatch-rus Standalone Agent" | Out-Null
|
||||||
|
sc.exe description $ServiceName "Standalone AWatch-rus DLP agent service wrapper" | Out-Null
|
||||||
|
sc.exe failure $ServiceName reset= 60 actions= restart/5000/restart/5000/restart/5000 | Out-Null
|
||||||
|
sc.exe start $ServiceName | Out-Null
|
||||||
|
|
||||||
|
Write-Output "Standalone service installed: $ServiceName"
|
||||||
|
Write-Output "Config: $configPath"
|
||||||
|
Write-Output ("Host: {0} -> {1}://{2}:{3}" -f $effectiveHostname, $ServerScheme, $ServerHost, $ServerPort)
|
||||||
@@ -34,6 +34,8 @@ Name: "validate"; Description: "Запустить validate-deployment (чере
|
|||||||
[Files]
|
[Files]
|
||||||
Source: "..\..\ActivityWatch.Windows.Common.psd1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
Source: "..\..\ActivityWatch.Windows.Common.psd1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||||
Source: "..\..\ActivityWatch.Windows.Common.psm1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
Source: "..\..\ActivityWatch.Windows.Common.psm1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||||
|
Source: "..\..\install-standalone-service.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||||
|
Source: "..\..\aw-standalone-service.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||||
Source: "..\..\deploy-single-user.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
Source: "..\..\deploy-single-user.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||||
Source: "..\..\deploy-domain-users.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
Source: "..\..\deploy-domain-users.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||||
Source: "..\..\deploy-ensemble.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
Source: "..\..\deploy-ensemble.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||||
@@ -43,6 +45,7 @@ Source: "..\..\migrate-awatch-rus-paths.ps1"; DestDir: "{app}\windows"; Flags: i
|
|||||||
Source: "..\..\worktime-session-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
Source: "..\..\worktime-session-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||||
Source: "..\..\browser-domains-native-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
Source: "..\..\browser-domains-native-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||||
Source: "..\..\dlp-endpoint-signals-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
Source: "..\..\dlp-endpoint-signals-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||||
|
Source: "..\..\file-operations-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||||
Source: "..\..\email-outbound-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
Source: "..\..\email-outbound-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||||
Source: "..\..\web-category-rules.example.json"; DestDir: "{app}\windows"; Flags: ignoreversion
|
Source: "..\..\web-category-rules.example.json"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||||
Source: "..\..\dlp-policy.example.json"; DestDir: "{app}\windows"; Flags: ignoreversion
|
Source: "..\..\dlp-policy.example.json"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||||
@@ -51,95 +54,11 @@ Source: "payload\{#AwDefaultZipName}"; DestDir: "{app}\payload"; Flags: ignoreve
|
|||||||
Source: "innosetup-rdp-package-filelist.md"; DestDir: "{app}\windows\installkit\innosetup"; Flags: ignoreversion
|
Source: "innosetup-rdp-package-filelist.md"; DestDir: "{app}\windows\installkit\innosetup"; Flags: ignoreversion
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
Filename: "powershell.exe"; Parameters: "{code:GetDeployEnsembleParams}"; Flags: runhidden; Tasks: deploy
|
Filename: "powershell.exe"; Parameters: "{code:GetStandaloneInstallParams}"; Flags: runhidden; Tasks: deploy
|
||||||
|
|
||||||
[Code]
|
[Code]
|
||||||
var
|
var
|
||||||
ServerHostPage: TInputQueryWizardPage;
|
ServerHostPage: TInputQueryWizardPage;
|
||||||
UsersPage: TInputQueryWizardPage;
|
|
||||||
OptionsPage: TInputOptionWizardPage;
|
|
||||||
|
|
||||||
function NormalizeUserCsv(const UserCsv: string): string;
|
|
||||||
var
|
|
||||||
i: Integer;
|
|
||||||
s: string;
|
|
||||||
token: string;
|
|
||||||
begin
|
|
||||||
Result := '';
|
|
||||||
s := UserCsv;
|
|
||||||
while True do
|
|
||||||
begin
|
|
||||||
i := Pos(',', s);
|
|
||||||
if i = 0 then
|
|
||||||
begin
|
|
||||||
token := Trim(s);
|
|
||||||
s := '';
|
|
||||||
end
|
|
||||||
else
|
|
||||||
begin
|
|
||||||
token := Trim(Copy(s, 1, i - 1));
|
|
||||||
Delete(s, 1, i);
|
|
||||||
end;
|
|
||||||
|
|
||||||
if token <> '' then
|
|
||||||
begin
|
|
||||||
if Result <> '' then
|
|
||||||
Result := Result + ',';
|
|
||||||
Result := Result + token;
|
|
||||||
end;
|
|
||||||
|
|
||||||
if s = '' then
|
|
||||||
Break;
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function BuildUsersPowerShellArg(const UserCsv: string): string;
|
|
||||||
var
|
|
||||||
i: Integer;
|
|
||||||
s: string;
|
|
||||||
token: string;
|
|
||||||
quoted: string;
|
|
||||||
begin
|
|
||||||
Result := '';
|
|
||||||
s := UserCsv;
|
|
||||||
while True do
|
|
||||||
begin
|
|
||||||
i := Pos(',', s);
|
|
||||||
if i = 0 then
|
|
||||||
begin
|
|
||||||
token := Trim(s);
|
|
||||||
s := '';
|
|
||||||
end
|
|
||||||
else
|
|
||||||
begin
|
|
||||||
token := Trim(Copy(s, 1, i - 1));
|
|
||||||
Delete(s, 1, i);
|
|
||||||
end;
|
|
||||||
|
|
||||||
if token <> '' then
|
|
||||||
begin
|
|
||||||
quoted := '"' + token + '"';
|
|
||||||
if Result <> '' then
|
|
||||||
Result := Result + ',';
|
|
||||||
Result := Result + quoted;
|
|
||||||
end;
|
|
||||||
|
|
||||||
if s = '' then
|
|
||||||
Break;
|
|
||||||
end;
|
|
||||||
if Result <> '' then
|
|
||||||
Result := '-Users ' + Result;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function PayloadZipPath: string;
|
|
||||||
begin
|
|
||||||
Result := ExpandConstant('{app}\payload\{#AwDefaultZipName}');
|
|
||||||
end;
|
|
||||||
|
|
||||||
function HasPayloadZip: Boolean;
|
|
||||||
begin
|
|
||||||
Result := FileExists(ExpandConstant('{src}\payload\{#AwDefaultZipName}'));
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure InitializeWizard;
|
procedure InitializeWizard;
|
||||||
begin
|
begin
|
||||||
@@ -155,62 +74,24 @@ begin
|
|||||||
ServerHostPage.Add('ServerPort', False);
|
ServerHostPage.Add('ServerPort', False);
|
||||||
ServerHostPage.Values[0] := '{#AwDefaultServerHost}';
|
ServerHostPage.Values[0] := '{#AwDefaultServerHost}';
|
||||||
ServerHostPage.Values[1] := '{#AwDefaultServerPort}';
|
ServerHostPage.Values[1] := '{#AwDefaultServerPort}';
|
||||||
|
|
||||||
UsersPage := CreateInputQueryPage(
|
|
||||||
ServerHostPage.ID,
|
|
||||||
'Пользователи (RDP)',
|
|
||||||
'Перечень пользователей, для которых разворачиваем агенты.',
|
|
||||||
'Введите список через запятую. Пример: user1,user2,user3'
|
|
||||||
);
|
|
||||||
UsersPage.Add('Users (CSV)', False);
|
|
||||||
UsersPage.Values[0] := '{#AwDefaultUsers}';
|
|
||||||
|
|
||||||
OptionsPage := CreateInputOptionPage(
|
|
||||||
UsersPage.ID,
|
|
||||||
'Опции деплоя',
|
|
||||||
'Выберите опции для установки/валидации.',
|
|
||||||
'',
|
|
||||||
False,
|
|
||||||
False
|
|
||||||
);
|
|
||||||
OptionsPage.Add('Использовать offline payload (встроенный ZIP)');
|
|
||||||
OptionsPage.Add('Запустить validate-deployment после деплоя');
|
|
||||||
OptionsPage.Values[0] := HasPayloadZip;
|
|
||||||
OptionsPage.Values[1] := True;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function GetDeployEnsembleParams(Param: string): string;
|
function GetStandaloneInstallParams(Param: string): string;
|
||||||
var
|
var
|
||||||
serverHost: string;
|
serverHost: string;
|
||||||
serverPort: string;
|
serverPort: string;
|
||||||
usersCsv: string;
|
|
||||||
usersArg: string;
|
|
||||||
zipArg: string;
|
|
||||||
validateArg: string;
|
|
||||||
begin
|
begin
|
||||||
serverHost := Trim(ServerHostPage.Values[0]);
|
serverHost := Trim(ServerHostPage.Values[0]);
|
||||||
serverPort := Trim(ServerHostPage.Values[1]);
|
serverPort := Trim(ServerHostPage.Values[1]);
|
||||||
usersCsv := NormalizeUserCsv(UsersPage.Values[0]);
|
if serverHost = '' then
|
||||||
|
RaiseException('ServerHost is empty.');
|
||||||
usersArg := BuildUsersPowerShellArg(usersCsv);
|
if serverPort = '' then
|
||||||
if usersArg = '' then
|
RaiseException('ServerPort is empty.');
|
||||||
RaiseException('Users list is empty.');
|
|
||||||
|
|
||||||
zipArg := '';
|
|
||||||
if OptionsPage.Values[0] then
|
|
||||||
zipArg := ' -PackageZipPath "' + PayloadZipPath + '"';
|
|
||||||
|
|
||||||
validateArg := '';
|
|
||||||
if OptionsPage.Values[1] and WizardIsTaskSelected('validate') then
|
|
||||||
validateArg := ' -ValidateAfterDeploy';
|
|
||||||
|
|
||||||
Result :=
|
Result :=
|
||||||
'-NoProfile -ExecutionPolicy Bypass -File "' + ExpandConstant('{app}\windows\deploy-ensemble.ps1') + '"' +
|
'-NoProfile -ExecutionPolicy Bypass -File "' + ExpandConstant('{app}\windows\install-standalone-service.ps1') + '"' +
|
||||||
' -ServerHost "' + serverHost + '"' +
|
' -ServerHost "' + serverHost + '"' +
|
||||||
' -ServerPort ' + serverPort +
|
' -ServerPort ' + serverPort +
|
||||||
' ' + usersArg +
|
|
||||||
zipArg +
|
|
||||||
' -InstallRoot "{#AwDefaultInstallRoot}"' +
|
' -InstallRoot "{#AwDefaultInstallRoot}"' +
|
||||||
' -StateRoot "{#AwDefaultStateRoot}"' +
|
' -StateRoot "{#AwDefaultStateRoot}"';
|
||||||
validateArg;
|
|
||||||
end;
|
end;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||||
param(
|
param(
|
||||||
[string]$OldInstallRoot = 'C:\Program Files\ActivityWatch-Phase2',
|
[string]$OldInstallRoot = 'C:\Program Files\ActivityWatch-Phase2',
|
||||||
[string]$OldStateRoot = 'C:\ProgramData\ActivityWatch-Phase2',
|
[string]$OldStateRoot = 'C:\ProgramData\ActivityWatch-Phase2',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json'
|
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json'
|
||||||
)
|
)
|
||||||
|
|
||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
@@ -13,70 +13,26 @@ $config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath
|
|||||||
$installRoot = [string]$config.paths.installRoot
|
$installRoot = [string]$config.paths.installRoot
|
||||||
$stateRoot = [string]$config.paths.stateRoot
|
$stateRoot = [string]$config.paths.stateRoot
|
||||||
$collectorScript = [string]$config.paths.collectorScript
|
$collectorScript = [string]$config.paths.collectorScript
|
||||||
$endpointCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]$config.paths.endpointCollectorScript } else { Join-Path $stateRoot 'dlp-endpoint-signals-collector.ps1' }
|
|
||||||
$fileCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]$config.paths.fileCollectorScript } else { Join-Path $stateRoot 'file-operations-collector.ps1' }
|
|
||||||
$sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' }
|
|
||||||
$rulesPath = [string]$config.paths.rulesPath
|
$rulesPath = [string]$config.paths.rulesPath
|
||||||
$policyPath = if ($config.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$config.paths.policyPath } else { Join-Path $stateRoot 'dlp-policy.json' }
|
|
||||||
$launchScript = [string]$config.paths.launchScript
|
$launchScript = [string]$config.paths.launchScript
|
||||||
$recoveryScript = [string]$config.paths.recoveryScript
|
$recoveryScript = [string]$config.paths.recoveryScript
|
||||||
|
|
||||||
$afkExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]$config.collectors.afkEnabled } else { $true }
|
|
||||||
$windowExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]$config.collectors.windowEnabled } else { $true }
|
|
||||||
$fileOpsExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]$config.collectors.fileOpsEnabled } else { $true }
|
|
||||||
$printServiceOperationalEnabled = $false
|
|
||||||
try {
|
|
||||||
$printServiceLog = Get-WinEvent -ListLog 'Microsoft-Windows-PrintService/Operational' -ErrorAction Stop
|
|
||||||
$printServiceOperationalEnabled = [bool]$printServiceLog.IsEnabled
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
}
|
|
||||||
$printJobTitlePolicyEnabled = $false
|
|
||||||
try {
|
|
||||||
$printPolicy = Get-ItemProperty -LiteralPath 'HKLM:\Software\Policies\Microsoft\Windows NT\Printers' -Name 'ShowJobTitleInEventLogs' -ErrorAction Stop
|
|
||||||
$printJobTitlePolicyEnabled = ([int]$printPolicy.ShowJobTitleInEventLogs -eq 1)
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
}
|
|
||||||
$requiredFiles = @(
|
$requiredFiles = @(
|
||||||
|
(Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe'),
|
||||||
|
(Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe'),
|
||||||
$collectorScript,
|
$collectorScript,
|
||||||
$endpointCollectorScript,
|
|
||||||
$sessionCollectorScript,
|
|
||||||
$rulesPath,
|
$rulesPath,
|
||||||
$policyPath,
|
|
||||||
$launchScript,
|
$launchScript,
|
||||||
$recoveryScript,
|
$recoveryScript,
|
||||||
$ConfigPath
|
$ConfigPath
|
||||||
)
|
)
|
||||||
if ($fileOpsExpected) {
|
|
||||||
$requiredFiles += $fileCollectorScript
|
|
||||||
}
|
|
||||||
if ($afkExpected) {
|
|
||||||
$requiredFiles += (Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe')
|
|
||||||
}
|
|
||||||
if ($windowExpected) {
|
|
||||||
$requiredFiles += (Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe')
|
|
||||||
}
|
|
||||||
|
|
||||||
$missingFiles = @(
|
$missingFiles = @(
|
||||||
$requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) }
|
$requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) }
|
||||||
)
|
)
|
||||||
|
|
||||||
$processNames = @()
|
$processNames = @('aw-watcher-afk', 'aw-watcher-window')
|
||||||
if ($afkExpected) { $processNames += 'aw-watcher-afk' }
|
$runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId
|
||||||
if ($windowExpected) { $processNames += 'aw-watcher-window' }
|
|
||||||
$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
|
|
||||||
)
|
|
||||||
|
|
||||||
$taskNames = @()
|
$taskNames = @()
|
||||||
if ($config.userTasks) {
|
if ($config.userTasks) {
|
||||||
@@ -85,28 +41,25 @@ if ($config.userTasks) {
|
|||||||
$taskNames += [string]$config.recovery.taskName
|
$taskNames += [string]$config.recovery.taskName
|
||||||
$taskNames = $taskNames | Sort-Object -Unique
|
$taskNames = $taskNames | Sort-Object -Unique
|
||||||
|
|
||||||
$tasks = @(
|
$tasks = foreach ($taskName in $taskNames) {
|
||||||
foreach ($taskName in $taskNames) {
|
$task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||||
$task = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $taskName } | Select-Object -First 1
|
if ($task) {
|
||||||
if ($task) {
|
[pscustomobject]@{
|
||||||
[pscustomobject]@{
|
taskName = $task.TaskName
|
||||||
taskName = $task.TaskName
|
state = [string]$task.State
|
||||||
state = [string]$task.State
|
present = $true
|
||||||
present = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
[pscustomobject]@{
|
|
||||||
taskName = $taskName
|
|
||||||
state = 'Отсутствует'
|
|
||||||
present = $false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
else {
|
||||||
|
[pscustomobject]@{
|
||||||
|
taskName = $taskName
|
||||||
|
state = 'Missing'
|
||||||
|
present = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$serverUrl = '{0}://{1}:{2}' -f [string]$config.server.scheme, [string]$config.server.host, [int]$config.server.port
|
$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]@{
|
$result = [ordered]@{
|
||||||
generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
|
generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
|
||||||
configPath = $ConfigPath
|
configPath = $ConfigPath
|
||||||
@@ -123,24 +76,11 @@ $result = [ordered]@{
|
|||||||
ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.present }))
|
ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.present }))
|
||||||
}
|
}
|
||||||
processes = [ordered]@{
|
processes = [ordered]@{
|
||||||
expected = $processNames
|
|
||||||
list = @($runningProcesses)
|
list = @($runningProcesses)
|
||||||
sessionCollectors = @($sessionCollectorProcesses)
|
ok = [bool](($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge 2)
|
||||||
ok = [bool](
|
|
||||||
(
|
|
||||||
($processNames.Count -eq 0) -or
|
|
||||||
($uniqueRunningProcessNames.Count -ge $processNames.Count)
|
|
||||||
) -and
|
|
||||||
($sessionCollectorProcesses.Count -ge 1)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
printTelemetry = [ordered]@{
|
|
||||||
operationalLogEnabled = $printServiceOperationalEnabled
|
|
||||||
jobTitlePolicyEnabled = $printJobTitlePolicyEnabled
|
|
||||||
ok = [bool]($printServiceOperationalEnabled -and $printJobTitlePolicyEnabled)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$result.overallOk = [bool]($result.files.ok -and $result.tasks.ok -and $result.processes.ok -and $result.printTelemetry.ok)
|
$result.overallOk = [bool]($result.files.ok -and $result.tasks.ok -and $result.processes.ok)
|
||||||
|
|
||||||
$result
|
$result
|
||||||
|
|||||||
@@ -1,4 +1,268 @@
|
|||||||
param(
|
param(
|
||||||
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
|
[string]$Hostname,
|
||||||
|
[int]$PollSeconds = 30
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Get-Config {
|
||||||
|
param([string]$Path)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) {
|
||||||
|
throw "Конфигурация не найдена: $Path"
|
||||||
|
}
|
||||||
|
|
||||||
|
Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-AwJsonPost {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$Uri,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Json
|
||||||
|
)
|
||||||
|
|
||||||
|
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||||
|
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-Bucket {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$ApiBase,
|
||||||
|
[Parameter(Mandatory = $true)][string]$BucketId,
|
||||||
|
[Parameter(Mandatory = $true)][string]$HostnameValue
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
catch { Write-Error param(
|
||||||
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
|
[string]$Hostname,
|
||||||
|
[int]$PollSeconds = 30
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Get-Config {
|
||||||
|
param([string]$Path)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) {
|
||||||
|
throw "Конфигурация не найдена: $Path"
|
||||||
|
}
|
||||||
|
|
||||||
|
Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-AwJsonPost {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$Uri,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Json
|
||||||
|
)
|
||||||
|
|
||||||
|
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||||
|
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-Bucket {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$ApiBase,
|
||||||
|
[Parameter(Mandatory = $true)][string]$BucketId,
|
||||||
|
[Parameter(Mandatory = $true)][string]$HostnameValue
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = @{
|
||||||
|
client = 'aw-worktime-session-collector'
|
||||||
|
type = 'aw.worktime.session'
|
||||||
|
hostname = $HostnameValue
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SessionRecords {
|
||||||
|
$records = @()
|
||||||
|
|
||||||
|
try {
|
||||||
|
$lines = quser 2>$null
|
||||||
|
if (-not $lines) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($line in ($lines | Select-Object -Skip 1)) {
|
||||||
|
$clean = ($line -replace '^\s*>?', '').Trim()
|
||||||
|
if (-not $clean) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = $clean -split '\s+'
|
||||||
|
if ($parts.Count -lt 4) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionName = ''
|
||||||
|
$sessionIdIndex = 2
|
||||||
|
if ($parts[1] -match '^\d+$') {
|
||||||
|
$sessionIdIndex = 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$sessionName = $parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionId = 0
|
||||||
|
if ($parts[$sessionIdIndex] -match '^\d+$') {
|
||||||
|
$sessionId = [int]$parts[$sessionIdIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
$records += [pscustomobject]@{
|
||||||
|
username = $parts[0]
|
||||||
|
sessionName = $sessionName
|
||||||
|
sessionId = $sessionId
|
||||||
|
state = $parts[$sessionIdIndex + 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
|
||||||
|
return $records
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-SessionIsActive {
|
||||||
|
param([AllowNull()][string]$State)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($State)) { return $false }
|
||||||
|
$s = $State.Trim().ToLowerInvariant()
|
||||||
|
return ($s -eq 'active') -or ($s -like 'актив*')
|
||||||
|
}
|
||||||
|
|
||||||
|
$cfg = Get-Config -Path $ConfigPath
|
||||||
|
$hostValue = if ($Hostname) { $Hostname } elseif ($cfg.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$cfg.awHostname)) { [string]$cfg.awHostname } else { [string]$env:COMPUTERNAME }
|
||||||
|
$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port
|
||||||
|
$bucketId = 'aw-worktime-sessions_' + $hostValue
|
||||||
|
$pulse = 120
|
||||||
|
$sleepSec = if ($PollSeconds -gt 0) {
|
||||||
|
$PollSeconds
|
||||||
|
}
|
||||||
|
elseif ($cfg.collector -and $cfg.collector.pollSeconds) {
|
||||||
|
[int]$cfg.collector.pollSeconds
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
30
|
||||||
|
}
|
||||||
|
|
||||||
|
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||||
|
$records = Get-SessionRecords
|
||||||
|
if (-not $records -or $records.Count -eq 0) {
|
||||||
|
$records = @([pscustomobject]@{
|
||||||
|
username = $env:USERNAME
|
||||||
|
sessionName = ''
|
||||||
|
sessionId = (Get-Process -Id $PID).SessionId
|
||||||
|
state = 'Unknown'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rec in $records) {
|
||||||
|
$payload = @{
|
||||||
|
timestamp = $now
|
||||||
|
duration = 0
|
||||||
|
data = @{
|
||||||
|
username = [string]$rec.username
|
||||||
|
userId = "$($env:USERDOMAIN)\$($rec.username)"
|
||||||
|
sessionId = [int]$rec.sessionId
|
||||||
|
sessionName = [string]$rec.sessionName
|
||||||
|
state = [string]$rec.state
|
||||||
|
active = (Test-SessionIsActive -State ([string]$rec.state))
|
||||||
|
hostname = $hostValue
|
||||||
|
source = 'worktime-session-collector'
|
||||||
|
}
|
||||||
|
} | ConvertTo-Json -Depth 6 -Compress
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Seconds $sleepSec
|
||||||
|
}
|
||||||
|
; }
|
||||||
|
|
||||||
|
$body = @{
|
||||||
|
client = 'aw-worktime-session-collector'
|
||||||
|
type = 'aw.worktime.session'
|
||||||
|
hostname = $HostnameValue
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SessionRecords {
|
||||||
|
$records = @()
|
||||||
|
|
||||||
|
try {
|
||||||
|
$lines = quser 2>$null
|
||||||
|
if (-not $lines) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($line in ($lines | Select-Object -Skip 1)) {
|
||||||
|
$clean = ($line -replace '^\s*>?', '').Trim()
|
||||||
|
if (-not $clean) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = $clean -split '\s+'
|
||||||
|
if ($parts.Count -lt 4) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionName = ''
|
||||||
|
$sessionIdIndex = 2
|
||||||
|
if ($parts[1] -match '^\d+$') {
|
||||||
|
$sessionIdIndex = 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$sessionName = $parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionId = 0
|
||||||
|
if ($parts[$sessionIdIndex] -match '^\d+$') {
|
||||||
|
$sessionId = [int]$parts[$sessionIdIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
$records += [pscustomobject]@{
|
||||||
|
username = $parts[0]
|
||||||
|
sessionName = $sessionName
|
||||||
|
sessionId = $sessionId
|
||||||
|
state = $parts[$sessionIdIndex + 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { Write-Error param(
|
||||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
[string]$Hostname,
|
[string]$Hostname,
|
||||||
[int]$PollSeconds = 30
|
[int]$PollSeconds = 30
|
||||||
@@ -164,3 +428,234 @@ while ($true) {
|
|||||||
|
|
||||||
Start-Sleep -Seconds $sleepSec
|
Start-Sleep -Seconds $sleepSec
|
||||||
}
|
}
|
||||||
|
; }
|
||||||
|
|
||||||
|
return $records
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-SessionIsActive {
|
||||||
|
param([AllowNull()][string]$State)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($State)) { return $false }
|
||||||
|
$s = $State.Trim().ToLowerInvariant()
|
||||||
|
return ($s -eq 'active') -or ($s -like 'актив*')
|
||||||
|
}
|
||||||
|
|
||||||
|
$cfg = Get-Config -Path $ConfigPath
|
||||||
|
$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME }
|
||||||
|
$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port
|
||||||
|
$bucketId = 'aw-worktime-sessions_' + $hostValue
|
||||||
|
$pulse = 120
|
||||||
|
$sleepSec = if ($PollSeconds -gt 0) {
|
||||||
|
$PollSeconds
|
||||||
|
}
|
||||||
|
elseif ($cfg.collector -and $cfg.collector.pollSeconds) {
|
||||||
|
[int]$cfg.collector.pollSeconds
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
30
|
||||||
|
}
|
||||||
|
|
||||||
|
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||||
|
$records = Get-SessionRecords
|
||||||
|
if (-not $records -or $records.Count -eq 0) {
|
||||||
|
$records = @([pscustomobject]@{
|
||||||
|
username = $env:USERNAME
|
||||||
|
sessionName = ''
|
||||||
|
sessionId = (Get-Process -Id $PID).SessionId
|
||||||
|
state = 'Unknown'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rec in $records) {
|
||||||
|
$payload = @{
|
||||||
|
timestamp = $now
|
||||||
|
duration = 0
|
||||||
|
data = @{
|
||||||
|
username = [string]$rec.username
|
||||||
|
userId = "$($env:USERDOMAIN)\$($rec.username)"
|
||||||
|
sessionId = [int]$rec.sessionId
|
||||||
|
sessionName = [string]$rec.sessionName
|
||||||
|
state = [string]$rec.state
|
||||||
|
active = (Test-SessionIsActive -State ([string]$rec.state))
|
||||||
|
hostname = $hostValue
|
||||||
|
source = 'worktime-session-collector'
|
||||||
|
}
|
||||||
|
} | ConvertTo-Json -Depth 6 -Compress
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload
|
||||||
|
}
|
||||||
|
catch { Write-Error param(
|
||||||
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
|
[string]$Hostname,
|
||||||
|
[int]$PollSeconds = 30
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Get-Config {
|
||||||
|
param([string]$Path)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) {
|
||||||
|
throw "Конфигурация не найдена: $Path"
|
||||||
|
}
|
||||||
|
|
||||||
|
Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-AwJsonPost {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$Uri,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Json
|
||||||
|
)
|
||||||
|
|
||||||
|
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||||
|
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-Bucket {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$ApiBase,
|
||||||
|
[Parameter(Mandatory = $true)][string]$BucketId,
|
||||||
|
[Parameter(Mandatory = $true)][string]$HostnameValue
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = @{
|
||||||
|
client = 'aw-worktime-session-collector'
|
||||||
|
type = 'aw.worktime.session'
|
||||||
|
hostname = $HostnameValue
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SessionRecords {
|
||||||
|
$records = @()
|
||||||
|
|
||||||
|
try {
|
||||||
|
$lines = quser 2>$null
|
||||||
|
if (-not $lines) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($line in ($lines | Select-Object -Skip 1)) {
|
||||||
|
$clean = ($line -replace '^\s*>?', '').Trim()
|
||||||
|
if (-not $clean) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = $clean -split '\s+'
|
||||||
|
if ($parts.Count -lt 4) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionName = ''
|
||||||
|
$sessionIdIndex = 2
|
||||||
|
if ($parts[1] -match '^\d+$') {
|
||||||
|
$sessionIdIndex = 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$sessionName = $parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionId = 0
|
||||||
|
if ($parts[$sessionIdIndex] -match '^\d+$') {
|
||||||
|
$sessionId = [int]$parts[$sessionIdIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
$records += [pscustomobject]@{
|
||||||
|
username = $parts[0]
|
||||||
|
sessionName = $sessionName
|
||||||
|
sessionId = $sessionId
|
||||||
|
state = $parts[$sessionIdIndex + 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
|
||||||
|
return $records
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-SessionIsActive {
|
||||||
|
param([AllowNull()][string]$State)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($State)) { return $false }
|
||||||
|
$s = $State.Trim().ToLowerInvariant()
|
||||||
|
return ($s -eq 'active') -or ($s -like 'актив*')
|
||||||
|
}
|
||||||
|
|
||||||
|
$cfg = Get-Config -Path $ConfigPath
|
||||||
|
$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME }
|
||||||
|
$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port
|
||||||
|
$bucketId = 'aw-worktime-sessions_' + $hostValue
|
||||||
|
$pulse = 120
|
||||||
|
$sleepSec = if ($PollSeconds -gt 0) {
|
||||||
|
$PollSeconds
|
||||||
|
}
|
||||||
|
elseif ($cfg.collector -and $cfg.collector.pollSeconds) {
|
||||||
|
[int]$cfg.collector.pollSeconds
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
30
|
||||||
|
}
|
||||||
|
|
||||||
|
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||||
|
$records = Get-SessionRecords
|
||||||
|
if (-not $records -or $records.Count -eq 0) {
|
||||||
|
$records = @([pscustomobject]@{
|
||||||
|
username = $env:USERNAME
|
||||||
|
sessionName = ''
|
||||||
|
sessionId = (Get-Process -Id $PID).SessionId
|
||||||
|
state = 'Unknown'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rec in $records) {
|
||||||
|
$payload = @{
|
||||||
|
timestamp = $now
|
||||||
|
duration = 0
|
||||||
|
data = @{
|
||||||
|
username = [string]$rec.username
|
||||||
|
userId = "$($env:USERDOMAIN)\$($rec.username)"
|
||||||
|
sessionId = [int]$rec.sessionId
|
||||||
|
sessionName = [string]$rec.sessionName
|
||||||
|
state = [string]$rec.state
|
||||||
|
active = (Test-SessionIsActive -State ([string]$rec.state))
|
||||||
|
hostname = $hostValue
|
||||||
|
source = 'worktime-session-collector'
|
||||||
|
}
|
||||||
|
} | ConvertTo-Json -Depth 6 -Compress
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Seconds $sleepSec
|
||||||
|
}
|
||||||
|
; }
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Seconds $sleepSec
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user