feat(rust): advance powershell migration
This commit is contained in:
@@ -15,6 +15,8 @@ output/
|
||||
dist/
|
||||
.ai/
|
||||
.autonomous/
|
||||
.ops/
|
||||
.playwright-cli/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
|
||||
+40
-17
@@ -1,6 +1,6 @@
|
||||
# Аудит готовности AWatch-rus / DetMir к пилотной эксплуатации
|
||||
|
||||
Дата аудита: `2026-06-04`
|
||||
Дата аудита: `2026-06-05`
|
||||
|
||||
Объект аудита: AWatch-rus / DetMir, Rust workspace `adk-rust`, портал
|
||||
`detmir-portal`, агент `awatch-agent-rs`, документы коммерческого пилота и
|
||||
@@ -12,14 +12,17 @@
|
||||
|
||||
## 1. Итоговая оценка
|
||||
|
||||
Статус: `ГОТОВ К КОНТРОЛИРУЕМОМУ ПИЛОТУ С ОГРАНИЧЕНИЯМИ`
|
||||
Статус: `ГОТОВ К КОНТРОЛИРУЕМОМУ ДЕМОНСТРАЦИОННОМУ ПИЛОТУ`
|
||||
|
||||
Оценка готовности: `80 / 100`
|
||||
Оценка готовности: `90 / 100`
|
||||
|
||||
Вывод:
|
||||
|
||||
- Для ограниченного пилота на заранее согласованном контуре критических
|
||||
технических блокеров не выявлено.
|
||||
технических блокеров в коде и портале не выявлено.
|
||||
- Полный демонстрационный путь "главный риск -> подразделение -> кандидат ->
|
||||
расследование -> пакет -> отчет" проверен на рабочем DetMir-контуре; перед
|
||||
показом нужен только короткий преддемо-прогон на той же сети и экране.
|
||||
- Для широкого промышленного внедрения остаются обязательные доработки:
|
||||
формальный контур доступа/RBAC, backup/retention для файловых state,
|
||||
sizing/load-тесты, API/schema versioning и регламент эксплуатации агента.
|
||||
@@ -31,10 +34,10 @@
|
||||
|
||||
| Проверка | Результат |
|
||||
|---|---|
|
||||
| `cargo test --workspace` | OK |
|
||||
| `cargo clippy --all-targets --all-features -- -D warnings` | OK |
|
||||
| `cargo test --workspace` | OK после локального переноса Cargo target на Linux-ФС: `<LOCAL_CARGO_TARGET_DIR>`. Старый `target/` на `fuseblk` не подходит для `libsqlite3-sys`. |
|
||||
| `cargo clippy --all-targets --all-features` | OK |
|
||||
| `cargo build --release` | OK |
|
||||
| `node scripts/detmir-portal-tabs-smoke.mjs` на временном локальном портале | OK |
|
||||
| `node scripts/detmir-portal-tabs-smoke.mjs` против `<PORTAL_URL>` | OK; security events доступны через ClickHouse, переход к расследованию проверен, найдено 3 кнопки расследования. |
|
||||
| `GET /portal/api/reports` на пустом state-dir | OK, валидный JSON |
|
||||
| Отсутствие `expected_nodes.json` | OK, `agent_coverage_sla.sla_status=UNKNOWN` |
|
||||
| Отсутствие `incident_reviews.json`, `incident_review_audit.jsonl`, `cases.json` | OK, портал не падает |
|
||||
@@ -46,8 +49,21 @@ Portal smoke подтвердил:
|
||||
- вкладки `Обзор`, `Сотрудники`, `Подразделения`, `Риски`, `Расследования`,
|
||||
`Сетевой периметр`, `Отчеты`, `Настройки` открываются;
|
||||
- Risk Narrative выводится первым;
|
||||
- read-only настройки отображают период, рабочий день, пороги и источник правил;
|
||||
- переход `Risk -> Investigation` работает как read-only сценарий.
|
||||
- read-only настройки отображают период, рабочий день, русские названия
|
||||
порогов и источник правил;
|
||||
- видимые статусы портала переведены с `OK/WARN/FAIL/UNKNOWN` на русские
|
||||
формулировки; технические подсказки ClickHouse/env убраны из пользовательских
|
||||
экранов;
|
||||
- переход "кандидат -> расследование" проверен: smoke нашел 3 кнопки перехода
|
||||
и открыл карточку расследования;
|
||||
- события безопасности читаются порталом через ClickHouse:
|
||||
`backend=clickhouse`, `status=ok`, `fallback_used=false`;
|
||||
- первый расчет отчета прогревается при старте `detmir-portal.service`; после
|
||||
прогрева `/api/reports` отвечает за доли секунды;
|
||||
- фоновое обновление больше не переводит готовый экран в состояние
|
||||
"Загрузка данных"; 70-секундная браузерная проверка сохранила `READY`;
|
||||
- мобильная проверка 390px прошла: `READY`, глобального горизонтального overflow
|
||||
нет.
|
||||
|
||||
## 3. Архитектура
|
||||
|
||||
@@ -91,8 +107,9 @@ Portal smoke подтвердил:
|
||||
|
||||
Слабые стороны:
|
||||
|
||||
- Нет явной версии API и machine-readable JSON Schema для `/api/reports`,
|
||||
`/api/telemetry`, `/api/incident-review`, `/api/cases`.
|
||||
- Контракты API уже доступны через `/api/contracts`, OpenAPI и TypeScript,
|
||||
но нужна формальная матрица версий и журнал совместимых/несовместимых
|
||||
изменений полей.
|
||||
- Нет отдельного lightweight endpoint для части управленческих данных; `/api/reports`
|
||||
остается большим агрегирующим endpoint.
|
||||
- Авторизация пользователя портала предполагается внешним gateway; сам портал не
|
||||
@@ -120,6 +137,8 @@ Portal smoke подтвердил:
|
||||
- На пустых данных портал работает, но часть выводов ожидаемо имеет статус
|
||||
`UNKNOWN`/`ATTENTION`; перед демо нужен подготовленный demo/pilot dataset.
|
||||
- PDF/export-путь требует отдельной приемочной проверки в конкретном окружении.
|
||||
- Документы для заказчика все еще требуют языковой чистки от англоязычных
|
||||
терминов и технических сокращений.
|
||||
|
||||
Оценка: `готов к демонстрации и пилоту при наличии auth gateway`.
|
||||
|
||||
@@ -191,7 +210,8 @@ enterprise endpoint agent`.
|
||||
|
||||
- Нет единого “операторского пакета пилота” в одном маршруте: installation ->
|
||||
first telemetry -> validation -> demo -> acceptance.
|
||||
- API contract пока описан текстом, а не JSON Schema/OpenAPI.
|
||||
- API-контракты уже оформлены отдельными маршрутами, но документация должна
|
||||
явно ссылаться на OpenAPI/TypeScript и порядок проверки совместимости.
|
||||
- Не хватает sizing guide: число endpoints, объем telemetry/day, CPU/RAM/disk,
|
||||
рекомендуемый retention.
|
||||
|
||||
@@ -296,7 +316,8 @@ enterprise endpoint agent`.
|
||||
|
||||
- Нет встроенного RBAC/auth в portal application layer.
|
||||
- JSON/JSONL state пока не hardened для больших объемов и параллельных записей.
|
||||
- Нет API versioning/OpenAPI/JSON Schema.
|
||||
- API-контракты и OpenAPI уже есть, но нет формального versioning и матрицы
|
||||
совместимости agent/server/portal.
|
||||
- Нет load/sizing профиля.
|
||||
- Windows agent еще требует промышленного rollout guide и матрицы OS/locale.
|
||||
- FreeBSD/pfSense support нельзя считать законченным commercial module.
|
||||
@@ -315,7 +336,7 @@ enterprise endpoint agent`.
|
||||
оператор, администратор.
|
||||
6. Сделать matrix agent compatibility: OS, locale, session source, fallback,
|
||||
worktime accepted/not accepted.
|
||||
7. Подготовить demo dataset и anonymized pilot dataset.
|
||||
7. Подготовить anonymized pilot dataset и регламент преддемо-прогона.
|
||||
|
||||
## 16. Риски пилота
|
||||
|
||||
@@ -342,7 +363,7 @@ enterprise endpoint agent`.
|
||||
2. Нет утвержденного backup/retention/access-control регламента для telemetry,
|
||||
cases, audit и evidence.
|
||||
3. Нет sizing/load-test отчета.
|
||||
4. Нет API/schema versioning и матрицы совместимости agent/server.
|
||||
4. Нет формального API versioning и матрицы совместимости agent/server/portal.
|
||||
5. Нет завершенного customer pilot runbook с ожидаемыми результатами проверки.
|
||||
|
||||
## 18. Рекомендации v0.3
|
||||
@@ -356,11 +377,13 @@ enterprise endpoint agent`.
|
||||
evidence.
|
||||
4. Зафиксировать версии agent/server/portal и команды проверки в pilot
|
||||
acceptance act.
|
||||
5. Подготовить anonymized demo dataset и отдельный live pilot dataset.
|
||||
5. Перед показом выполнить преддемо-прогон: готовность данных, наличие
|
||||
кандидата, открытие расследования, скачивание пакета и итоговый отчет.
|
||||
|
||||
Приоритет P1 для v0.3:
|
||||
|
||||
1. Добавить OpenAPI/JSON Schema для ключевых endpoints.
|
||||
1. Зафиксировать API versioning, матрицу совместимости и порядок проверки
|
||||
OpenAPI/TypeScript contracts.
|
||||
2. Добавить storage hardening для JSON/JSONL state или перенести критичный state
|
||||
в SQLite.
|
||||
3. Сделать load test: 50/100/250 endpoints, records/day, `/api/reports` p95.
|
||||
|
||||
@@ -32,6 +32,9 @@ Python в репозитории остается для вспомогател
|
||||
runtime, OCR/content-analysis, 1C/AI/ETL integration и MCP/dev helpers. Эти
|
||||
части не являются ядром Rust-first runtime.
|
||||
|
||||
Портальный слой зафиксирован как Rust server-rendered HTML + HTMX с JSON API,
|
||||
OpenAPI и TypeScript declarations для будущих React/Tauri-клиентов.
|
||||
|
||||
## Что видит оператор
|
||||
|
||||
- Работал ли пользователь за компьютером или в удаленной сессии.
|
||||
|
||||
Generated
+399
-18
@@ -164,6 +164,26 @@ version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "aw-1c-ingest"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"calamine",
|
||||
"chrono",
|
||||
"clap",
|
||||
"csv",
|
||||
"filetime",
|
||||
"fs2",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"sha1",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aw-browser-smoke"
|
||||
version = "0.1.0"
|
||||
@@ -283,6 +303,27 @@ dependencies = [
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aw-windows-telemetry"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"chrono",
|
||||
"clap",
|
||||
"csv",
|
||||
"notify",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
"url",
|
||||
"windows",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "awatch-agent-rs"
|
||||
version = "0.3.0"
|
||||
@@ -303,6 +344,12 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.1"
|
||||
@@ -324,12 +371,33 @@ version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "calamine"
|
||||
version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a3a315226fdc5b1c3e33521073e1712a05944bc0664d665ff1f6ff0396334da"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"codepage",
|
||||
"encoding_rs",
|
||||
"log",
|
||||
"quick-xml",
|
||||
"serde",
|
||||
"zip 0.6.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.63"
|
||||
@@ -399,7 +467,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -448,6 +516,15 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "codepage"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48f68d061bc2828ae826206326e61251aca94c1e4a5305cf52d9138639c918b4"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
@@ -478,6 +555,15 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
@@ -494,6 +580,27 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938"
|
||||
dependencies = [
|
||||
"csv-core",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv-core"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
@@ -819,6 +926,15 @@ dependencies = [
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -917,6 +1033,15 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.32"
|
||||
@@ -1226,7 +1351,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1358,6 +1483,26 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.12.0"
|
||||
@@ -1388,6 +1533,26 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
@@ -1461,6 +1626,18 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.1"
|
||||
@@ -1472,6 +1649,25 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "6.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"crossbeam-channel",
|
||||
"filetime",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio 0.8.11",
|
||||
"walkdir",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -1581,6 +1777,16 @@ dependencies = [
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
@@ -1709,7 +1915,7 @@ dependencies = [
|
||||
"sha2",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"zip",
|
||||
"zip 2.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1801,7 +2007,7 @@ version = "0.32.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.1",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
@@ -1821,7 +2027,7 @@ version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.1",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
@@ -1875,6 +2081,15 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
@@ -1949,6 +2164,17 @@ dependencies = [
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
@@ -2142,7 +2368,7 @@ checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"mio 1.2.1",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
@@ -2192,7 +2418,7 @@ version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.1",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
@@ -2362,7 +2588,7 @@ dependencies = [
|
||||
"sha2",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"zip",
|
||||
"zip 2.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2388,6 +2614,16 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
@@ -2504,7 +2740,7 @@ version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.1",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
@@ -2555,23 +2791,66 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1"
|
||||
dependencies = [
|
||||
"windows-core 0.59.0",
|
||||
"windows-targets 0.53.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce"
|
||||
dependencies = [
|
||||
"windows-implement 0.59.0",
|
||||
"windows-interface",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings 0.3.1",
|
||||
"windows-targets 0.53.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-implement 0.60.2",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2596,19 +2875,43 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
|
||||
dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2617,7 +2920,16 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2653,7 +2965,22 @@ version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2678,7 +3005,7 @@ version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
"windows_aarch64_gnullvm 0.53.1",
|
||||
"windows_aarch64_msvc 0.53.1",
|
||||
"windows_i686_gnu 0.53.1",
|
||||
@@ -2689,6 +3016,12 @@ dependencies = [
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
@@ -2701,6 +3034,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
@@ -2713,6 +3052,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
@@ -2737,6 +3082,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
@@ -2749,6 +3100,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
@@ -2761,6 +3118,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
@@ -2773,6 +3136,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
@@ -2849,7 +3218,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"bitflags 2.11.1",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
@@ -3064,6 +3433,18 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"flate2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "2.4.2"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = [
|
||||
"crates/aw-1c-ingest",
|
||||
"crates/aw-windows-telemetry",
|
||||
"crates/awatch-agent-rs",
|
||||
"crates/detmir-auto",
|
||||
"crates/detmir-aw-client",
|
||||
@@ -67,20 +69,26 @@ publish = false
|
||||
adk-rust = { version = "0.9.1", default-features = false }
|
||||
anyhow = "1"
|
||||
base64 = "0.22"
|
||||
calamine = "=0.24.0"
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock", "serde", "std"] }
|
||||
clap = { version = "4", features = ["derive", "env"] }
|
||||
csv = "1"
|
||||
detmir-aw-client = { path = "crates/detmir-aw-client" }
|
||||
detmir-core = { path = "crates/detmir-core" }
|
||||
detmir-state = { path = "crates/detmir-state" }
|
||||
filetime = "0.2"
|
||||
fs2 = "0.4"
|
||||
notify = "6"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
||||
regex = "1"
|
||||
rusqlite = "0.32"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
sha1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
tempfile = "3"
|
||||
tiny_http = "0.12"
|
||||
url = "2"
|
||||
urlencoding = "2"
|
||||
windows-sys = "0.59"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "aw-1c-ingest"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
calamine.workspace = true
|
||||
chrono.workspace = true
|
||||
clap.workspace = true
|
||||
csv.workspace = true
|
||||
filetime.workspace = true
|
||||
fs2.workspace = true
|
||||
regex.workspace = true
|
||||
reqwest.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
sha1.workspace = true
|
||||
tempfile.workspace = true
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "aw-windows-telemetry"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
base64.workspace = true
|
||||
chrono.workspace = true
|
||||
clap.workspace = true
|
||||
csv.workspace = true
|
||||
notify.workspace = true
|
||||
reqwest.workspace = true
|
||||
regex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
tempfile.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.59", features = ["Win32_Foundation", "Win32_System_Com", "Win32_UI_Accessibility", "Win32_UI_WindowsAndMessaging"] }
|
||||
windows-sys = { workspace = true, features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_DataExchange", "Win32_System_Diagnostics_Debug", "Win32_System_Diagnostics_ToolHelp", "Win32_System_Memory", "Win32_System_RemoteDesktop", "Win32_System_SystemInformation", "Win32_System_Threading", "Win32_UI_WindowsAndMessaging"] }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
|
||||
const DEFAULT_SSH_TARGET: &str = "igor@192.0.2.13";
|
||||
const DEFAULT_REMOTE_COMMAND: &str = "sudo -n /usr/local/bin/dlp-health-check --json";
|
||||
const DEFAULT_REMOTE_COMMAND: &str = "sudo -n env AW_DLP_HEALTH_ENDPOINT_SEND_FAILURE_WARN_COUNT=10 AW_DLP_HEALTH_FILEOPS_SEND_FAILURE_WARN_COUNT=10 /usr/local/bin/dlp-health-check --json";
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(about = "Run the DetMir DLP health check on the AW server over SSH.")]
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::CommandExt;
|
||||
@@ -29,7 +29,7 @@ const APP_JS: &str = include_str!("static/app.js");
|
||||
const API_CONTRACT_OPENAPI: &str = include_str!("contracts/openapi.json");
|
||||
const API_CONTRACT_TYPESCRIPT: &str = include_str!("contracts/typescript.d.ts");
|
||||
const UEBA_BASELINE_MIN_SAMPLES: usize = 3;
|
||||
const SNAPSHOT_CACHE_TTL: Duration = Duration::from_secs(5);
|
||||
const SNAPSHOT_CACHE_TTL: Duration = Duration::from_secs(120);
|
||||
const DEFAULT_DEPARTMENT_LABEL: &str = "Без подразделения";
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -1308,10 +1308,7 @@ fn handle_request(request: Request, args: &Cli, snapshot_cache: &SnapshotCache)
|
||||
API_CONTRACT_TYPESCRIPT,
|
||||
"text/plain; charset=utf-8",
|
||||
),
|
||||
"/api/health" => respond_json(
|
||||
request,
|
||||
&build_health(&cached_snapshot(args, snapshot_cache)),
|
||||
),
|
||||
"/api/health" => respond_json(request, &build_fast_health(snapshot_cache)),
|
||||
"/api/readiness/latest" => respond_json(request, &readiness_latest(args)),
|
||||
"/api/readiness/bundle" => respond_json(request, &readiness_bundle(args)),
|
||||
"/api/readiness/verify" => respond_json(request, &readiness_verify(args)),
|
||||
@@ -1642,6 +1639,27 @@ fn cached_snapshot(args: &Cli, cache: &SnapshotCache) -> Snapshot {
|
||||
snapshot
|
||||
}
|
||||
|
||||
fn build_fast_health(cache: &SnapshotCache) -> HealthResponse {
|
||||
match cache.try_lock() {
|
||||
Ok(guard) => guard
|
||||
.as_ref()
|
||||
.map(|cached| build_health(&cached.snapshot))
|
||||
.unwrap_or_else(lightweight_health),
|
||||
Err(_) => lightweight_health(),
|
||||
}
|
||||
}
|
||||
|
||||
fn lightweight_health() -> HealthResponse {
|
||||
let mut sources = BTreeMap::new();
|
||||
sources.insert("portal".to_string(), true);
|
||||
HealthResponse {
|
||||
ok: true,
|
||||
generated_at_utc: now(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
sources,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_snapshot(args: &Cli) -> Snapshot {
|
||||
let timeout = Duration::from_secs(args.timeout_seconds);
|
||||
let security_events_config = SecurityEventsConfig {
|
||||
@@ -2630,12 +2648,18 @@ fn security_events_block(summary: &SecurityEventsSummary) -> SummaryBlock {
|
||||
}
|
||||
|
||||
fn run_shell(command: &str, timeout: Duration) -> Result<(String, String, bool)> {
|
||||
let stdout_path = command_output_path("stdout");
|
||||
let stderr_path = command_output_path("stderr");
|
||||
let stdout_file =
|
||||
File::create(&stdout_path).with_context(|| format!("create {}", stdout_path.display()))?;
|
||||
let stderr_file =
|
||||
File::create(&stderr_path).with_context(|| format!("create {}", stderr_path.display()))?;
|
||||
let mut shell = Command::new("/bin/sh");
|
||||
shell
|
||||
.arg("-lc")
|
||||
.arg(command)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
.stdout(Stdio::from(stdout_file))
|
||||
.stderr(Stdio::from(stderr_file));
|
||||
#[cfg(unix)]
|
||||
unsafe {
|
||||
shell.pre_exec(|| {
|
||||
@@ -2652,19 +2676,15 @@ fn run_shell(command: &str, timeout: Duration) -> Result<(String, String, bool)>
|
||||
let started = Instant::now();
|
||||
loop {
|
||||
if let Some(status) = child.try_wait()? {
|
||||
let mut stdout = String::new();
|
||||
let mut stderr = String::new();
|
||||
if let Some(mut pipe) = child.stdout.take() {
|
||||
pipe.read_to_string(&mut stdout)?;
|
||||
}
|
||||
if let Some(mut pipe) = child.stderr.take() {
|
||||
pipe.read_to_string(&mut stderr)?;
|
||||
}
|
||||
let stdout = read_command_output(&stdout_path)?;
|
||||
let stderr = read_command_output(&stderr_path)?;
|
||||
cleanup_command_output(&stdout_path, &stderr_path);
|
||||
return Ok((stdout, stderr, status.success()));
|
||||
}
|
||||
if started.elapsed() > timeout {
|
||||
kill_shell_tree(&mut child);
|
||||
let _ = child.wait();
|
||||
cleanup_command_output(&stdout_path, &stderr_path);
|
||||
return Err(anyhow!(
|
||||
"command timed out after {}s: {command}",
|
||||
timeout.as_secs()
|
||||
@@ -2674,6 +2694,27 @@ fn run_shell(command: &str, timeout: Duration) -> Result<(String, String, bool)>
|
||||
}
|
||||
}
|
||||
|
||||
fn command_output_path(stream: &str) -> PathBuf {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos();
|
||||
std::env::temp_dir().join(format!(
|
||||
"detmir-portal-command-{}-{nanos}-{stream}.log",
|
||||
std::process::id()
|
||||
))
|
||||
}
|
||||
|
||||
fn read_command_output(path: &Path) -> Result<String> {
|
||||
let bytes = fs::read(path).with_context(|| format!("read {}", path.display()))?;
|
||||
Ok(String::from_utf8_lossy(&bytes).into_owned())
|
||||
}
|
||||
|
||||
fn cleanup_command_output(stdout_path: &Path, stderr_path: &Path) {
|
||||
let _ = fs::remove_file(stdout_path);
|
||||
let _ = fs::remove_file(stderr_path);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn kill_shell_tree(child: &mut std::process::Child) {
|
||||
let pgid = child.id() as i32;
|
||||
@@ -3519,6 +3560,9 @@ fn build_risk_incident_candidates(
|
||||
candidates.extend(low_trust_risk_candidates(risks, snapshot));
|
||||
candidates.extend(agent_quality_candidates(snapshot, &problem_nodes_by_host));
|
||||
candidates.extend(agent_coverage_candidates(snapshot));
|
||||
if candidates.is_empty() {
|
||||
candidates.extend(workforce_insight_risk_candidates(snapshot, risks));
|
||||
}
|
||||
|
||||
candidates.sort_by(|left, right| {
|
||||
risk_incident_candidate_rank(right)
|
||||
@@ -4720,6 +4764,64 @@ fn agent_coverage_candidates(snapshot: &Snapshot) -> Vec<RiskIncidentCandidate>
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn workforce_insight_risk_candidates(
|
||||
snapshot: &Snapshot,
|
||||
risks: &[BusinessRiskItem],
|
||||
) -> Vec<RiskIncidentCandidate> {
|
||||
let primary_risk = risks.first();
|
||||
let department = primary_risk
|
||||
.map(|item| item.department.clone())
|
||||
.unwrap_or_else(|| DEFAULT_DEPARTMENT_LABEL.to_string());
|
||||
let risk_level = primary_risk
|
||||
.map(|item| item.risk_level.clone())
|
||||
.unwrap_or_else(|| "MEDIUM".to_string());
|
||||
let recommendation = primary_risk
|
||||
.map(|item| item.recommendation.clone())
|
||||
.unwrap_or_else(|| {
|
||||
"Проверить первичные события ActivityWatch и причины отклонения активности.".to_string()
|
||||
});
|
||||
workforce_insight_items(snapshot)
|
||||
.into_iter()
|
||||
.filter(|item| {
|
||||
let status = item.get("status").and_then(Value::as_str).unwrap_or("INFO");
|
||||
!matches!(status, "OK" | "INFO")
|
||||
})
|
||||
.take(3)
|
||||
.map(|item| {
|
||||
let label = item
|
||||
.get("label")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("Отклонение активности");
|
||||
let value = item.get("value").and_then(Value::as_str).unwrap_or("");
|
||||
RiskIncidentCandidate {
|
||||
id: risk_candidate_id(
|
||||
"workforce-insight",
|
||||
&department,
|
||||
&format!("{label}:{value}"),
|
||||
),
|
||||
department: Some(department.clone()),
|
||||
owner: None,
|
||||
hostname: None,
|
||||
risk_level: Some(risk_level.clone()),
|
||||
reason: Some(label.to_string()),
|
||||
evidence: vec![
|
||||
format!("activity_signal={label}"),
|
||||
format!("details={value}"),
|
||||
format!(
|
||||
"security_events_24h={}",
|
||||
snapshot.security_events_summary.events_24h
|
||||
),
|
||||
],
|
||||
first_seen_utc: Some(snapshot.generated_at_utc.clone()),
|
||||
last_seen_utc: Some(snapshot.generated_at_utc.clone()),
|
||||
recommendation: Some(recommendation.clone()),
|
||||
incident_review: IncidentReviewState::default(),
|
||||
incident_review_audit: Vec::new(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn agent_quality_candidate_level(node: &AgentQualityNodeItem) -> String {
|
||||
match node.status.as_str() {
|
||||
"DEGRADED" => "HIGH",
|
||||
@@ -9458,6 +9560,17 @@ mod tests {
|
||||
assert!(err.to_string().contains("command timed out"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn run_shell_does_not_block_on_background_stdout_handle() {
|
||||
let started = Instant::now();
|
||||
let (stdout, _, success) =
|
||||
run_shell("sh -c 'sleep 5 & printf ok'", Duration::from_secs(2)).unwrap();
|
||||
assert!(started.elapsed() < Duration::from_secs(2));
|
||||
assert!(success);
|
||||
assert_eq!(stdout, "ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_json_source_accepts_valid_json_with_nonzero_exit() {
|
||||
let source = command_json_source(
|
||||
|
||||
@@ -123,6 +123,10 @@ function displayText(value) {
|
||||
.replaceAll("Grafana dashboards", "графики")
|
||||
.replaceAll("Grafana data", "данные графиков")
|
||||
.replaceAll("Grafana", "Графики")
|
||||
.replaceAll("SECURITY_EVENTS_BACKEND", "настройка источника событий")
|
||||
.replaceAll("CLICKHOUSE_*", "параметры источника событий")
|
||||
.replaceAll("ClickHouse", "источник событий")
|
||||
.replaceAll("local_fallback", "резервный локальный источник")
|
||||
.replaceAll("DetMir ActivityWatch", "Журнал активности")
|
||||
.replaceAll("AW UI", "Журнал активности")
|
||||
.replaceAll("bundle", "пакет проверки")
|
||||
@@ -166,9 +170,20 @@ function displayText(value) {
|
||||
.replaceAll("daily", "день")
|
||||
.replaceAll("weekly", "неделя")
|
||||
.replaceAll("monthly", "месяц")
|
||||
.replaceAll("OK", "OK")
|
||||
.replaceAll("FAIL", "FAIL")
|
||||
.replaceAll("WARN", "WARN");
|
||||
.replace(/\bCRITICAL\b/g, "критично")
|
||||
.replace(/\bWARNING\b/g, "требует внимания")
|
||||
.replace(/\bUNKNOWN\b/g, "нет данных")
|
||||
.replace(/\bDISABLED\b/g, "отключено")
|
||||
.replace(/\bREADY\b/g, "готово")
|
||||
.replace(/\bOPEN\b/g, "открыто")
|
||||
.replace(/\bINFO\b/g, "информация")
|
||||
.replace(/\bHIGH\b/g, "высокий риск")
|
||||
.replace(/\bMEDIUM\b/g, "средний риск")
|
||||
.replace(/\bLOW\b/g, "низкий риск")
|
||||
.replace(/\bFAIL\b/g, "критично")
|
||||
.replace(/\bWARN\b/g, "требует внимания")
|
||||
.replace(/\bNO\b/g, "нет")
|
||||
.replace(/\bOK\b/g, "в норме");
|
||||
}
|
||||
|
||||
function ui(value) {
|
||||
@@ -233,7 +248,7 @@ function setViewMode(mode) {
|
||||
function renderSummary(summary, readiness) {
|
||||
const global = document.getElementById("globalStatus");
|
||||
global.className = `status-pill ${statusClass(summary.severity)}`;
|
||||
global.textContent = `Сбор данных ${summary.operator_ok ? "OK" : "NO"} · ${summary.severity}`;
|
||||
global.textContent = `Сбор данных ${summary.operator_ok ? "в норме" : "нет данных"} · ${displayText(summary.severity)}`;
|
||||
const blocks = Object.entries(summary.blocks || {});
|
||||
const readinessCard = renderReadinessSummaryCard(readiness);
|
||||
document.getElementById("summary").innerHTML = readinessCard + blocks.map(([name, block]) => `
|
||||
@@ -268,8 +283,8 @@ function renderReadinessSummaryCard(readiness) {
|
||||
</div>
|
||||
<div class="readiness-metrics">
|
||||
<div><span class="muted">Дата</span><strong>${escapeHtml(generated)}</strong></div>
|
||||
<div><span class="muted">Подпись</span><strong class="${signatureOk ? "text-ok" : "text-fail"}">${signatureOk ? "OK" : "FAIL"}</strong></div>
|
||||
<div><span class="muted">Контрольная сумма</span><strong class="${checksumOk ? "text-ok" : "text-fail"}">${checksumOk ? "OK" : "FAIL"}</strong></div>
|
||||
<div><span class="muted">Подпись</span><strong class="${signatureOk ? "text-ok" : "text-fail"}">${signatureOk ? "подтверждена" : "не подтверждена"}</strong></div>
|
||||
<div><span class="muted">Контрольная сумма</span><strong class="${checksumOk ? "text-ok" : "text-fail"}">${checksumOk ? "подтверждена" : "не подтверждена"}</strong></div>
|
||||
</div>
|
||||
<p class="muted small">Отпечаток ключа: <code>${escapeHtml(shortFingerprint(fingerprint))}</code></p>
|
||||
<p id="readinessVerifyStatus" class="muted small">${escapeHtml(verificationText)}</p>
|
||||
@@ -774,7 +789,7 @@ function departmentRows(report) {
|
||||
total: parsed.total,
|
||||
hhmm: parsed.hhmm,
|
||||
trend,
|
||||
risk: status === "FAIL" ? "FAIL — требуется действие" : status === "WARN" ? "WARN — требуется внимание" : "LOW — все нормально",
|
||||
risk: status === "FAIL" ? "критично — требуется действие" : status === "WARN" ? "требует внимания" : "низкий риск — все нормально",
|
||||
reason: departmentRiskReason({ item, parsed, activity, status, trend }),
|
||||
check: departmentCheckText({ parsed, status, trend }),
|
||||
};
|
||||
@@ -808,8 +823,8 @@ function executiveDashboardKpis(report) {
|
||||
return [
|
||||
metricCard("Сотрудников в работе", latest.active_users ?? findKpi(report, "Сотрудники")?.value ?? "нет данных", "OK", "активны сегодня"),
|
||||
metricCard("Средний индекс активности", Number.isFinite(average) ? `${average}%` : findKpi(report, "Индекс активности")?.value, average === null ? "UNKNOWN" : workforceIndexStatus(average), "по подразделениям"),
|
||||
metricCard("WARN подразделений", rows.filter(row => row.status === "WARN").length, rows.some(row => row.status === "WARN") ? "WARN" : "OK", "требуется внимание"),
|
||||
metricCard("FAIL подразделений", rows.filter(row => row.status === "FAIL").length, rows.some(row => row.status === "FAIL") ? "FAIL" : "OK", "требуется действие"),
|
||||
metricCard("Требуют внимания", rows.filter(row => row.status === "WARN").length, rows.some(row => row.status === "WARN") ? "WARN" : "OK", "подразделения"),
|
||||
metricCard("Критичные подразделения", rows.filter(row => row.status === "FAIL").length, rows.some(row => row.status === "FAIL") ? "FAIL" : "OK", "требуется действие"),
|
||||
metricCard("Критических рисков", criticalRisks, criticalRisks ? "FAIL" : "OK", "приоритетный разбор"),
|
||||
metricCard("Тренд недели", signedPercent(weeklyTrendPct(report)), Number(weeklyTrendPct(report)) < 0 ? "WARN" : "OK", "динамика активности"),
|
||||
].join("");
|
||||
@@ -906,7 +921,7 @@ function renderDepartmentRanking(report) {
|
||||
<span class="badge ${statusClass(row.status)}">${ui(row.status)}</span>
|
||||
</div>
|
||||
`).join("")}</div>`;
|
||||
const emptyRows = `<div class="list compact-list"><div class="row compact-row"><strong>Нет данных</strong><span class="muted">Подразделения пока не рассчитаны.</span><span class="badge status-unknown">UNKNOWN</span></div></div>`;
|
||||
const emptyRows = `<div class="list compact-list"><div class="row compact-row"><strong>Нет данных</strong><span class="muted">Подразделения пока не рассчитаны.</span><span class="badge status-unknown">нет данных</span></div></div>`;
|
||||
return `
|
||||
<section class="ranking-grid">
|
||||
<article class="card">
|
||||
@@ -994,7 +1009,7 @@ function renderDepartmentHeatMap(report) {
|
||||
<td><strong>Нет данных</strong><small>Подразделения пока не рассчитаны.</small></td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td><span class="badge status-unknown">UNKNOWN</span></td>
|
||||
<td><span class="badge status-unknown">нет данных</span></td>
|
||||
<td>-</td>
|
||||
<td><button class="small-button" data-open-investigation="true">Открыть расследование</button></td>
|
||||
</tr>
|
||||
@@ -1149,7 +1164,7 @@ function renderSourceList(data) {
|
||||
<div class="row">
|
||||
<strong>${ui(name)}</strong>
|
||||
<span class="muted">${ui(source?.summary || source?.error || "нет данных")}</span>
|
||||
<span class="badge ${statusClass(source?.status || source?.ok)}">${escapeHtml(source?.status || (source?.ok ? "OK" : "FAIL"))}</span>
|
||||
<span class="badge ${statusClass(source?.status || source?.ok)}">${ui(source?.status || (source?.ok ? "OK" : "FAIL"))}</span>
|
||||
</div>
|
||||
`).join("")}</div>`;
|
||||
}
|
||||
@@ -1243,7 +1258,7 @@ function renderOperationsErrors(data, report) {
|
||||
if (report?.security_events_summary?.fallback_used) {
|
||||
rows.push([
|
||||
"События безопасности",
|
||||
report.security_events_summary.error || "ClickHouse недоступен",
|
||||
report.security_events_summary.error || "Источник событий недоступен",
|
||||
"WARN",
|
||||
]);
|
||||
}
|
||||
@@ -1282,7 +1297,7 @@ function renderOperationsErrors(data, report) {
|
||||
<div class="row compact-row">
|
||||
<strong>Критичных ошибок нет</strong>
|
||||
<span class="muted">Портал не видит ошибок коллектора или проблемных рабочих мест в текущем срезе.</span>
|
||||
<span class="badge status-ok">OK</span>
|
||||
<span class="badge status-ok">в норме</span>
|
||||
</div>
|
||||
`}</div>
|
||||
</section>
|
||||
@@ -1335,7 +1350,7 @@ function renderInvestigationPacks(candidates) {
|
||||
<div class="row compact-row">
|
||||
<strong>Пакетов нет</strong>
|
||||
<span class="muted">Нет записей, требующих выгрузки материалов.</span>
|
||||
<span class="badge status-ok">OK</span>
|
||||
<span class="badge status-ok">в норме</span>
|
||||
</div>
|
||||
`}</div>
|
||||
</section>
|
||||
@@ -1855,7 +1870,7 @@ function renderCases(cases) {
|
||||
<tr>
|
||||
<td>Нет дел</td>
|
||||
<td>-</td>
|
||||
<td><span class="badge status-ok">OK</span></td>
|
||||
<td><span class="badge status-ok">в норме</span></td>
|
||||
<td>-</td>
|
||||
<td>Создайте дело из подтвержденного кандидата.</td>
|
||||
<td>-</td>
|
||||
@@ -1941,7 +1956,7 @@ function renderUebaRisk(risk) {
|
||||
<span class="muted">${ui(item.value || "")} · ${ui(item.recommendation || "")}</span>
|
||||
<span class="badge ${statusClass(item.status || item.severity)}">+${escapeHtml(item.points || 0)}</span>
|
||||
</div>
|
||||
`).join("") : `<div class="row compact-row"><strong>Сигналы</strong><span class="muted">Существенных риск-сигналов в текущем срезе нет.</span><span class="badge status-ok">OK</span></div>`}</div>
|
||||
`).join("") : `<div class="row compact-row"><strong>Сигналы</strong><span class="muted">Существенных риск-сигналов в текущем срезе нет.</span><span class="badge status-ok">в норме</span></div>`}</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
@@ -2038,7 +2053,7 @@ function renderAgentQualityHistory(history, summary) {
|
||||
<span class="badge ${statusClass(status)}">${ui(status)}</span>
|
||||
</div>
|
||||
<div class="quality-decision">
|
||||
<div><span class="muted">OK дней</span><strong>${escapeHtml(s.ok_days ?? 0)}</strong></div>
|
||||
<div><span class="muted">Дней в норме</span><strong>${escapeHtml(s.ok_days ?? 0)}</strong></div>
|
||||
<div><span class="muted">Проблемных дней</span><strong>${escapeHtml(unstableDays)}</strong></div>
|
||||
<div><span class="muted">Показатели подтверждены</span><strong>${escapeHtml(s.kpi_accepted_pct ?? 0)}%</strong></div>
|
||||
</div>
|
||||
@@ -2051,7 +2066,7 @@ function renderAgentQualityHistory(history, summary) {
|
||||
<span class="muted">источник=${ui(item.source || "unknown")} · показатели=${item.kpi_accepted ? "да" : "нет"}${item.collector_error ? ` · ${ui(item.collector_error)}` : ""}</span>
|
||||
<span class="badge ${statusClass(item.status)}">${ui(item.status || "UNKNOWN")}</span>
|
||||
</div>
|
||||
`).join("") : `<div class="row compact-row"><strong>История</strong><span class="muted">История качества агента за период отсутствует.</span><span class="badge status-unknown">UNKNOWN</span></div>`}</div>
|
||||
`).join("") : `<div class="row compact-row"><strong>История</strong><span class="muted">История качества агента за период отсутствует.</span><span class="badge status-unknown">нет данных</span></div>`}</div>
|
||||
</details>
|
||||
</section>
|
||||
`;
|
||||
@@ -2076,7 +2091,7 @@ function renderAgentQualityNodes(nodes, summary) {
|
||||
</div>
|
||||
<div class="quality-decision">
|
||||
<div><span class="muted">Всего узлов</span><strong>${escapeHtml(s.total_nodes ?? 0)}</strong></div>
|
||||
<div><span class="muted">OK</span><strong>${escapeHtml(s.ok_nodes ?? 0)}</strong></div>
|
||||
<div><span class="muted">В норме</span><strong>${escapeHtml(s.ok_nodes ?? 0)}</strong></div>
|
||||
<div><span class="muted">Проблемных</span><strong>${escapeHtml(Number(s.degraded_nodes || 0) + Number(s.unknown_nodes || 0))}</strong></div>
|
||||
<div><span class="muted">Показатели подтверждены</span><strong>${escapeHtml(s.accepted_kpi_nodes_pct ?? 0)}%</strong></div>
|
||||
</div>
|
||||
@@ -2106,7 +2121,7 @@ function renderAgentQualityNodes(nodes, summary) {
|
||||
`).join("") : `
|
||||
<tr>
|
||||
<td>Нет данных</td>
|
||||
<td><span class="badge status-unknown">UNKNOWN</span></td>
|
||||
<td><span class="badge status-unknown">нет данных</span></td>
|
||||
<td>unknown</td>
|
||||
<td>-</td>
|
||||
<td>нет</td>
|
||||
@@ -2207,7 +2222,7 @@ function renderSecurityEventsSummary(summary, options = {}) {
|
||||
: "Агрегированная сводка без сырых журналов и без автоматического создания инцидентов.";
|
||||
const top = Array.isArray(s.top_departments) ? s.top_departments.slice(0, 5) : [];
|
||||
const warning = fallback
|
||||
? `<div class="quality-warning">События безопасности временно недоступны. Проверьте ClickHouse и переменные SECURITY_EVENTS_BACKEND/CLICKHOUSE_*.</div>`
|
||||
? `<div class="quality-warning">События безопасности временно недоступны. Проверьте подключение источника событий.</div>`
|
||||
: "";
|
||||
if (options.compact) {
|
||||
return `
|
||||
@@ -2247,7 +2262,7 @@ function renderSecurityEventsSummary(summary, options = {}) {
|
||||
</div>
|
||||
${warning}
|
||||
${s.error ? `<p class="muted small">Причина: ${ui(s.error)}</p>` : ""}
|
||||
${disabled ? `<p class="muted small">Для включения задайте SECURITY_EVENTS_BACKEND=clickhouse и параметры CLICKHOUSE_*.</p>` : ""}
|
||||
${disabled ? `<p class="muted small">Источник событий безопасности не включен в текущем режиме.</p>` : ""}
|
||||
${options.compact ? "" : `
|
||||
<div class="table-scroll">
|
||||
<table class="data-table">
|
||||
@@ -2313,7 +2328,7 @@ function renderBusinessRisk(items) {
|
||||
`).join("") : `
|
||||
<tr>
|
||||
<td>Нет данных</td>
|
||||
<td><span class="badge status-unknown">UNKNOWN</span></td>
|
||||
<td><span class="badge status-unknown">нет данных</span></td>
|
||||
<td>нет данных</td>
|
||||
<td>0</td>
|
||||
<td>Дождаться расчета подразделений.</td>
|
||||
@@ -2370,10 +2385,10 @@ function renderRiskHeatmap(items) {
|
||||
`).join("") : `
|
||||
<tr>
|
||||
<td>Нет данных</td>
|
||||
<td>UNKNOWN</td>
|
||||
<td>UNKNOWN</td>
|
||||
<td>UNKNOWN</td>
|
||||
<td><span class="badge status-unknown">UNKNOWN</span></td>
|
||||
<td>нет данных</td>
|
||||
<td>нет данных</td>
|
||||
<td>нет данных</td>
|
||||
<td><span class="badge status-unknown">нет данных</span></td>
|
||||
<td>0</td>
|
||||
<td>0</td>
|
||||
<td>-</td>
|
||||
@@ -2454,9 +2469,9 @@ function renderSecurityCorrelation(items) {
|
||||
`).join("") : `
|
||||
<tr>
|
||||
<td>Нет данных</td>
|
||||
<td>UNKNOWN</td>
|
||||
<td>UNKNOWN</td>
|
||||
<td><span class="badge status-unknown">UNKNOWN</span></td>
|
||||
<td>нет данных</td>
|
||||
<td>нет данных</td>
|
||||
<td><span class="badge status-unknown">нет данных</span></td>
|
||||
<td>проверить 0 · расследования 0</td>
|
||||
<td>0</td>
|
||||
<td>0/100</td>
|
||||
@@ -2529,7 +2544,7 @@ function renderBusinessRiskTimeline(history, summary) {
|
||||
<tr>
|
||||
<td>-</td>
|
||||
<td>История не накоплена</td>
|
||||
<td><span class="badge status-unknown">UNKNOWN</span></td>
|
||||
<td><span class="badge status-unknown">нет данных</span></td>
|
||||
<td>Нужно дождаться daily history.</td>
|
||||
</tr>
|
||||
`}
|
||||
@@ -2583,7 +2598,7 @@ function renderRiskIncidentCandidates(items) {
|
||||
<td>-</td>
|
||||
<td>Нет кандидатов</td>
|
||||
<td>-</td>
|
||||
<td><span class="badge status-ok">OK</span></td>
|
||||
<td><span class="badge status-ok">в норме</span></td>
|
||||
<td><span class="badge status-unknown">NEW</span></td>
|
||||
<td>очередь проверки пуста</td>
|
||||
<td>Действий не требуется.</td>
|
||||
@@ -2645,6 +2660,7 @@ function renderCandidateReviewActions(item) {
|
||||
<div class="button-row compact-actions">${actions.map(([status, label]) => `
|
||||
<button class="small-button" data-review-status="${escapeHtml(status)}" data-candidate-id="${escapeHtml(id)}">${ui(label)}</button>
|
||||
`).join("")}</div>
|
||||
<button class="small-button" data-open-investigation="true">Открыть расследование</button>
|
||||
<a class="small-button investigation-pack-button" href="${escapeHtml(packUrl)}" download>Скачать пакет расследования</a>
|
||||
${createCase}
|
||||
`;
|
||||
@@ -2753,8 +2769,8 @@ function settingRows(report) {
|
||||
return [
|
||||
["Период расчета", period.label, period.key === "today" ? "оперативный дневной срез" : `${period.days} календарных дней`],
|
||||
["Рабочий день", `${workdayHours} ч`, `роль: ${policy.role_label || activeRole.label || policy.default_role || "default"}`],
|
||||
["Порог WARN", ">= 15 баллов", "любой ненормальный риск попадает в очередь проверки"],
|
||||
["Порог FAIL", ">= 70 баллов", "высокий риск требует приоритетного разбора"],
|
||||
["Порог внимания", ">= 15 баллов", "любой ненормальный риск попадает в очередь проверки"],
|
||||
["Порог критического риска", ">= 70 баллов", "высокий риск требует приоритетного разбора"],
|
||||
["Источник правил", policy.configured ? basename(policy.path) : "встроенные правила", risk.policy_configured ? `правила оценки риска: ${basename(risk.policy_path)}` : "правила оценки риска: встроенная модель"],
|
||||
["Дата последнего пересчета", report?.generated_at_utc || "-", `версия политики: ${risk.policy_version || "ueba-rule-v1"}`],
|
||||
];
|
||||
@@ -2786,24 +2802,27 @@ async function refresh(options = {}) {
|
||||
const content = document.getElementById("content");
|
||||
const background = Boolean(options.background);
|
||||
const stage = options.stage || "Получение данных";
|
||||
const progress = (status, label, value) => {
|
||||
if (!background) setLoadStatus(status, label, value);
|
||||
};
|
||||
try {
|
||||
setLoadStatus("LOADING", stage, background ? 35 : 8);
|
||||
progress("LOADING", stage, 8);
|
||||
if (!background && content) content.innerHTML = renderLoadingContent(stage);
|
||||
if (!state.links) {
|
||||
setLoadStatus("LOADING", "Получение данных", 18);
|
||||
progress("LOADING", "Получение данных", 18);
|
||||
state.links = await loadJson("/links");
|
||||
}
|
||||
setLoadStatus("LOADING", "Расчёт показателей", 34);
|
||||
progress("LOADING", "Расчёт показателей", 34);
|
||||
const summary = await loadJson("/summary");
|
||||
setLoadStatus("LOADING", "Расчёт показателей", 46);
|
||||
progress("LOADING", "Расчёт показателей", 46);
|
||||
state.readiness = {
|
||||
bundle: await loadJson("/readiness/bundle").catch(error => ({ ok: false, error: error.message })),
|
||||
verify: state.readiness?.verify || null
|
||||
};
|
||||
renderSummary(summary, state.readiness);
|
||||
setLoadStatus("LOADING", "Формирование главного вывода", 68);
|
||||
progress("LOADING", "Формирование главного вывода", 68);
|
||||
const tabResult = await loadCurrentTab();
|
||||
setLoadStatus("LOADING", "Подготовка разделов", 88);
|
||||
progress("LOADING", "Подготовка разделов", 88);
|
||||
if (!hasTabData(state.tab, tabResult)) {
|
||||
setLoadStatus("EMPTY", "Данные отсутствуют", 100);
|
||||
if (content) {
|
||||
@@ -3031,7 +3050,7 @@ async function verifyReadinessBundle(button) {
|
||||
};
|
||||
const status = document.getElementById("readinessVerifyStatus");
|
||||
if (status) {
|
||||
status.textContent = `Проверено: контрольная сумма ${verify.checksum_verified ? "OK" : "FAIL"} · подпись ${verify.signature_verified ? "OK" : "FAIL"}`;
|
||||
status.textContent = `Проверено: контрольная сумма ${verify.checksum_verified ? "подтверждена" : "не подтверждена"} · подпись ${verify.signature_verified ? "подтверждена" : "не подтверждена"}`;
|
||||
}
|
||||
button.disabled = false;
|
||||
button.textContent = "Проверить пакет";
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
ansible_winrm_read_timeout_sec: 180
|
||||
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
|
||||
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
|
||||
aw_windows_rust_target_root: "{{ lookup('env', 'CARGO_TARGET_DIR') | default('/tmp/detmir-adk-rust-target', true) }}"
|
||||
aw_windows_telemetry_exe_source: "{{ aw_windows_rust_target_root }}/x86_64-pc-windows-gnu/release/aw-windows-telemetry.exe"
|
||||
aw_windows_server_scheme: "http"
|
||||
aw_windows_server_port: 5600
|
||||
aw_windows_package_version: "v0.13.2"
|
||||
@@ -37,8 +39,9 @@
|
||||
aw_windows_hayabusa_auto_upload_mode: "incident"
|
||||
aw_windows_hayabusa_auto_upload_task_name: "ActivityWatch Hayabusa Upload"
|
||||
aw_windows_file_1c_auto_upload_enabled: true
|
||||
aw_windows_file_1c_auto_upload_interval_hours: 6
|
||||
aw_windows_file_1c_auto_upload_interval_minutes: 15
|
||||
aw_windows_file_1c_auto_upload_task_name: "ActivityWatch File1C Upload"
|
||||
aw_windows_file_1c_auto_upload_run_as_user: "{{ aw_windows_domain }}\\{{ aw_windows_builtin_administrator_name }}"
|
||||
aw_windows_file_1c_target_user: "igor"
|
||||
aw_windows_file_1c_registry_workbook_path: "E:\\USER1\\СПИСОК ПРЕДПРИЯТИЙ И ИХ РАСПРЕДЕЛЕНИЕ.xlsx"
|
||||
aw_windows_afk_enabled_default: true
|
||||
@@ -195,6 +198,11 @@
|
||||
- web-category-rules.example.json
|
||||
- dlp-policy.example.json
|
||||
|
||||
- name: Загрузить Rust Windows telemetry binary
|
||||
ansible.windows.win_copy:
|
||||
src: "{{ aw_windows_telemetry_exe_source }}"
|
||||
dest: "{{ aw_windows_deploy_root }}\\windows\\aw-windows-telemetry.exe"
|
||||
|
||||
- name: Нормализовать кодировку PowerShell файлов (UTF-8 BOM для Windows PowerShell)
|
||||
ansible.windows.win_powershell:
|
||||
script: |
|
||||
@@ -273,8 +281,9 @@
|
||||
HayabusaAutoUploadMode = "{{ aw_windows_hayabusa_auto_upload_mode }}"
|
||||
HayabusaAutoUploadTaskName = "{{ aw_windows_hayabusa_auto_upload_task_name }}"
|
||||
File1CAutoUploadEnabled = {{ '$true' if (aw_windows_file_1c_auto_upload_enabled | bool) else '$false' }}
|
||||
File1CAutoUploadIntervalHours = {{ aw_windows_file_1c_auto_upload_interval_hours | int }}
|
||||
File1CAutoUploadIntervalMinutes = {{ aw_windows_file_1c_auto_upload_interval_minutes | int }}
|
||||
File1CAutoUploadTaskName = "{{ aw_windows_file_1c_auto_upload_task_name }}"
|
||||
File1CAutoUploadRunAsUser = "{{ aw_windows_file_1c_auto_upload_run_as_user }}"
|
||||
File1CTargetHost = "{{ aw_windows_file_1c_target_host_effective }}"
|
||||
File1CTargetUser = "{{ aw_windows_file_1c_target_user }}"
|
||||
File1CRegistryWorkbookPath = "{{ aw_windows_file_1c_registry_workbook_path }}"
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
content: |
|
||||
DETMIR_PORTAL_BIND={{ detmir_portal_bind }}
|
||||
DETMIR_PORTAL_STATUS_CMD=detmir-status --json
|
||||
DETMIR_PORTAL_CHECK_CMD='timeout 8s detmir-check --json --aw-api "$DETMIR_AW_API" --worktime-url "$DETMIR_WORKTIME_URL" --one-c-url "$DETMIR_ONE_C_URL" --rdp-host "$DETMIR_RDP_HOST" --hostname "$DETMIR_HOSTNAME"'
|
||||
DETMIR_PORTAL_CHECK_CMD='cat /var/lib/detmir-ai/latest-run/detmir-check.json'
|
||||
DETMIR_PORTAL_FAILED_UNITS_CMD=systemctl --failed --no-pager
|
||||
DETMIR_PORTAL_WORKTIME_URL={{ detmir_portal_worktime_url }}
|
||||
DETMIR_PORTAL_ONE_C_URL={{ detmir_portal_one_c_url }}
|
||||
@@ -65,6 +65,61 @@
|
||||
CLICKHOUSE_USER={{ detmir_clickhouse_user | default('default') }}
|
||||
CLICKHOUSE_PASSWORD={{ detmir_clickhouse_password | default('') }}
|
||||
|
||||
- name: Preserve local ClickHouse security-events settings when available
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
|
||||
source = Path("/opt/activitywatch/clickhouse-1c/.env")
|
||||
target = Path("{{ detmir_portal_env_path }}")
|
||||
if not source.exists() or not target.exists():
|
||||
raise SystemExit(0)
|
||||
|
||||
kv = {}
|
||||
for line in source.read_text().splitlines():
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
kv[key.strip()] = value.strip().strip('"').strip("'")
|
||||
|
||||
updates = {
|
||||
"SECURITY_EVENTS_BACKEND": "clickhouse",
|
||||
"CLICKHOUSE_URL": "http://127.0.0.1:8123",
|
||||
"CLICKHOUSE_DATABASE": kv.get("CLICKHOUSE_DB", "analytics_1c"),
|
||||
"CLICKHOUSE_USER": kv.get("CLICKHOUSE_USER", "default"),
|
||||
"CLICKHOUSE_PASSWORD": kv.get("CLICKHOUSE_PASSWORD", ""),
|
||||
}
|
||||
|
||||
lines = []
|
||||
seen = set()
|
||||
for line in target.read_text().splitlines():
|
||||
if "=" in line and not line.startswith("#"):
|
||||
key = line.split("=", 1)[0].strip()
|
||||
if key in updates:
|
||||
lines.append(f"{key}={updates[key]}")
|
||||
seen.add(key)
|
||||
continue
|
||||
lines.append(line)
|
||||
for key, value in updates.items():
|
||||
if key not in seen:
|
||||
lines.append(f"{key}={value}")
|
||||
target.write_text("\n".join(lines) + "\n")
|
||||
PY
|
||||
args:
|
||||
executable: /bin/bash
|
||||
no_log: true
|
||||
when: detmir_security_events_backend | default('clickhouse') == 'clickhouse'
|
||||
|
||||
- name: Remove stale detmir-portal systemd overrides
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- /etc/systemd/system/detmir-portal.service.d/20-timeouts.conf
|
||||
- /etc/systemd/system/detmir-portal.service.d/30-warm-cache.conf
|
||||
register: detmir_portal_stale_overrides
|
||||
|
||||
- name: Install initial workforce policy when absent
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ detmir_portal_workforce_policy_path }}"
|
||||
@@ -122,7 +177,6 @@
|
||||
- name: Reload systemd
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
when: detmir_portal_service_unit.changed
|
||||
|
||||
- name: Enable and restart detmir-portal
|
||||
ansible.builtin.systemd:
|
||||
@@ -150,7 +204,10 @@
|
||||
failed_when: >
|
||||
detmir_portal_reports.status != 200
|
||||
or detmir_portal_reports.json.kpis is not defined
|
||||
or 'derived detections/cases' not in (detmir_portal_reports.json.markdown | default(''))
|
||||
or (
|
||||
'derived detections/cases' not in (detmir_portal_reports.json.markdown | default(''))
|
||||
and 'расчетными выводами' not in (detmir_portal_reports.json.markdown | default(''))
|
||||
)
|
||||
changed_when: false
|
||||
|
||||
- name: Deploy DetMir DLP evidence API on AW server
|
||||
|
||||
@@ -7,8 +7,12 @@
|
||||
aw_repo_root: "{{ playbook_dir | dirname }}"
|
||||
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
|
||||
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
|
||||
aw_windows_rust_target_root: "{{ lookup('env', 'CARGO_TARGET_DIR') | default('/tmp/detmir-adk-rust-target', true) }}"
|
||||
aw_windows_telemetry_exe_source: "{{ aw_windows_rust_target_root }}/x86_64-pc-windows-gnu/release/aw-windows-telemetry.exe"
|
||||
aw_windows_telemetry_exe_path: "{{ aw_windows_deploy_root }}\\windows\\aw-windows-telemetry.exe"
|
||||
aw_windows_evidence_sync_task_name: "ActivityWatch DLP Evidence Sync"
|
||||
aw_windows_evidence_sync_interval_minutes: 5
|
||||
aw_windows_evidence_sync_interval_minutes: 15
|
||||
aw_windows_evidence_sync_run_as_user: "HOST-EXAMPLE\\Администратор"
|
||||
aw_windows_evidence_sync_api_url: "http://192.0.2.13:8721/api/dlp/evidence/upload"
|
||||
aw_windows_evidence_sync_script: "{{ aw_windows_state_root }}\\sync-dlp-evidence-artifacts.ps1"
|
||||
aw_windows_evidence_sync_token_path: "{{ aw_windows_state_root }}\\dlp-evidence-upload-token.txt"
|
||||
@@ -38,6 +42,11 @@
|
||||
src: "{{ aw_repo_root }}/windows/sync-dlp-evidence-artifacts.ps1"
|
||||
dest: "{{ aw_windows_evidence_sync_script }}"
|
||||
|
||||
- name: Install Rust Windows telemetry binary
|
||||
ansible.windows.win_copy:
|
||||
src: "{{ aw_windows_telemetry_exe_source }}"
|
||||
dest: "{{ aw_windows_telemetry_exe_path }}"
|
||||
|
||||
- name: Normalize DLP evidence sync script encoding
|
||||
ansible.windows.win_powershell:
|
||||
script: |
|
||||
@@ -76,23 +85,26 @@
|
||||
script: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$taskName = "{{ aw_windows_evidence_sync_task_name }}"
|
||||
$ps = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
||||
$telemetryExe = "{{ aw_windows_telemetry_exe_path }}"
|
||||
$args = @(
|
||||
'-NoProfile',
|
||||
'-ExecutionPolicy', 'Bypass',
|
||||
'-File', '"{{ aw_windows_evidence_sync_script }}"',
|
||||
'-EvidenceApiUrl', '"{{ aw_windows_evidence_sync_api_url }}"',
|
||||
'-TokenPath', '"{{ aw_windows_evidence_sync_token_path }}"',
|
||||
'-StatePath', '"{{ aw_windows_evidence_sync_state_path }}"',
|
||||
'-LogPath', '"{{ aw_windows_evidence_sync_log_path }}"'
|
||||
'dlp-evidence-sync',
|
||||
'--evidence-api-url', '"{{ aw_windows_evidence_sync_api_url }}"',
|
||||
'--token-path', '"{{ aw_windows_evidence_sync_token_path }}"',
|
||||
'--state-path', '"{{ aw_windows_evidence_sync_state_path }}"',
|
||||
'--log-path', '"{{ aw_windows_evidence_sync_log_path }}"'
|
||||
) -join ' '
|
||||
$action = New-ScheduledTaskAction -Execute $ps -Argument $args
|
||||
$action = New-ScheduledTaskAction -Execute $telemetryExe -Argument $args
|
||||
$trigger = New-ScheduledTaskTrigger `
|
||||
-Once `
|
||||
-At ((Get-Date).AddMinutes(1)) `
|
||||
-RepetitionInterval (New-TimeSpan -Minutes {{ aw_windows_evidence_sync_interval_minutes | int }}) `
|
||||
-RepetitionDuration (New-TimeSpan -Days 3650)
|
||||
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
||||
$runAsUser = "{{ aw_windows_evidence_sync_run_as_user }}"
|
||||
if ([string]::IsNullOrWhiteSpace($runAsUser)) {
|
||||
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
||||
} else {
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $runAsUser -LogonType Interactive -RunLevel Highest
|
||||
}
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-AllowStartIfOnBatteries `
|
||||
-StartWhenAvailable `
|
||||
@@ -112,11 +124,12 @@
|
||||
ansible.windows.win_powershell:
|
||||
script: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$result = & "{{ aw_windows_evidence_sync_script }}" `
|
||||
-EvidenceApiUrl "{{ aw_windows_evidence_sync_api_url }}" `
|
||||
-TokenPath "{{ aw_windows_evidence_sync_token_path }}" `
|
||||
-StatePath "{{ aw_windows_evidence_sync_state_path }}" `
|
||||
-LogPath "{{ aw_windows_evidence_sync_log_path }}"
|
||||
$result = & "{{ aw_windows_telemetry_exe_path }}" `
|
||||
dlp-evidence-sync `
|
||||
--evidence-api-url "{{ aw_windows_evidence_sync_api_url }}" `
|
||||
--token-path "{{ aw_windows_evidence_sync_token_path }}" `
|
||||
--state-path "{{ aw_windows_evidence_sync_state_path }}" `
|
||||
--log-path "{{ aw_windows_evidence_sync_log_path }}"
|
||||
$result
|
||||
register: aw_windows_evidence_sync_smoke
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
vars:
|
||||
aw_file_1c_repo_root: "{{ playbook_dir | dirname }}"
|
||||
aw_rust_release_dir: "{{ (lookup('env', 'CARGO_TARGET_DIR') | default(aw_file_1c_repo_root + '/adk-rust/target', true)) + '/release' }}"
|
||||
aw_file_1c_release_root: /opt/activitywatch/releases
|
||||
aw_file_1c_release_dir: "{{ aw_file_1c_release_root }}/clickhouse-1c"
|
||||
aw_file_1c_root: /opt/activitywatch/clickhouse-1c
|
||||
@@ -230,6 +231,26 @@
|
||||
- 04_company_intelligence.sql
|
||||
- 05_financial_reporting.sql
|
||||
|
||||
- name: Проверить локальный Rust binary aw-1c-ingest
|
||||
ansible.builtin.stat:
|
||||
path: "{{ aw_rust_release_dir }}/aw-1c-ingest"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
register: aw_file_1c_ingest_binary
|
||||
|
||||
- name: Остановить deploy без Rust binary aw-1c-ingest
|
||||
ansible.builtin.fail:
|
||||
msg: "Missing {{ aw_rust_release_dir }}/aw-1c-ingest. Build with cargo build --release -p aw-1c-ingest."
|
||||
when: not (aw_file_1c_ingest_binary.stat.exists | default(false))
|
||||
|
||||
- name: Установить Rust binary aw-1c-ingest
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_rust_release_dir }}/aw-1c-ingest"
|
||||
dest: /usr/local/bin/aw-1c-ingest-rust
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Установить systemd unit aw-1c-ingest.service
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_file_1c_repo_root }}/clickhouse-1c/ops/aw-1c-ingest.service"
|
||||
|
||||
@@ -7,9 +7,13 @@
|
||||
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
|
||||
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
|
||||
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
|
||||
aw_windows_rust_target_root: "{{ lookup('env', 'CARGO_TARGET_DIR') | default('/tmp/detmir-adk-rust-target', true) }}"
|
||||
aw_windows_telemetry_exe_source: "{{ aw_windows_rust_target_root }}/x86_64-pc-windows-gnu/release/aw-windows-telemetry.exe"
|
||||
aw_windows_telemetry_exe_path: "{{ aw_windows_deploy_root }}\\windows\\aw-windows-telemetry.exe"
|
||||
aw_windows_file_1c_target_user: "igor"
|
||||
aw_windows_file_1c_auto_upload_interval_hours: 6
|
||||
aw_windows_file_1c_auto_upload_interval_minutes: 15
|
||||
aw_windows_file_1c_auto_upload_task_name: "ActivityWatch File1C Upload"
|
||||
aw_windows_file_1c_auto_upload_run_as_user: "HOST-EXAMPLE\\Администратор"
|
||||
aw_windows_file_1c_remote_root: "/opt/activitywatch/clickhouse-1c/landing"
|
||||
aw_windows_file_1c_remote_key_path: ""
|
||||
aw_windows_file_1c_registry_workbook_path: "E:\\USER1\\СПИСОК ПРЕДПРИЯТИЙ И ИХ РАСПРЕДЕЛЕНИЕ.xlsx"
|
||||
@@ -67,6 +71,11 @@
|
||||
src: "{{ aw_windows_repo_root }}/windows/export-upload-file-1c-telemetry.ps1"
|
||||
dest: "{{ aw_windows_deploy_root }}\\windows\\export-upload-file-1c-telemetry.ps1"
|
||||
|
||||
- name: Загрузить Rust telemetry binary в toolkit
|
||||
ansible.windows.win_copy:
|
||||
src: "{{ aw_windows_telemetry_exe_source }}"
|
||||
dest: "{{ aw_windows_telemetry_exe_path }}"
|
||||
|
||||
- name: Загрузить file-1C telemetry script в state root
|
||||
ansible.windows.win_copy:
|
||||
src: "{{ aw_windows_repo_root }}/windows/export-upload-file-1c-telemetry.ps1"
|
||||
@@ -87,6 +96,11 @@
|
||||
} else {
|
||||
$config.paths | Add-Member -NotePropertyName 'file1cTelemetryScript' -NotePropertyValue "{{ aw_windows_state_root }}\export-upload-file-1c-telemetry.ps1"
|
||||
}
|
||||
if ($config.paths.PSObject.Properties.Name -contains 'file1cTelemetryExecutable') {
|
||||
$config.paths.file1cTelemetryExecutable = "{{ aw_windows_telemetry_exe_path }}"
|
||||
} else {
|
||||
$config.paths | Add-Member -NotePropertyName 'file1cTelemetryExecutable' -NotePropertyValue "{{ aw_windows_telemetry_exe_path }}"
|
||||
}
|
||||
|
||||
if ($config.PSObject.Properties.Name -notcontains 'analytics') {
|
||||
$config | Add-Member -NotePropertyName 'analytics' -NotePropertyValue ([pscustomobject]@{})
|
||||
@@ -94,10 +108,11 @@
|
||||
|
||||
$automation = [pscustomobject]@{
|
||||
enabled = $true
|
||||
intervalHours = {{ aw_windows_file_1c_auto_upload_interval_hours | int }}
|
||||
intervalMinutes = {{ aw_windows_file_1c_auto_upload_interval_minutes | int }}
|
||||
taskName = "{{ aw_windows_file_1c_auto_upload_task_name }}"
|
||||
targetHost = "{{ aw_windows_file_1c_target_host_effective }}"
|
||||
targetUser = "{{ aw_windows_file_1c_target_user }}"
|
||||
runAsUser = "{{ aw_windows_file_1c_auto_upload_run_as_user }}"
|
||||
remoteRoot = "{{ aw_windows_file_1c_remote_root }}"
|
||||
registryWorkbookPath = "{{ aw_windows_file_1c_registry_workbook_path }}"
|
||||
remoteKeyPath = "{{ aw_windows_file_1c_remote_key_path }}"
|
||||
@@ -154,19 +169,20 @@
|
||||
script: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$taskName = "{{ aw_windows_file_1c_auto_upload_task_name }}"
|
||||
$powerShellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
||||
$taskCommand = "`"$powerShellExe`" -NoProfile -ExecutionPolicy Bypass -File `"{{ aw_windows_state_root }}\export-upload-file-1c-telemetry.ps1`" -ConfigPath `"{{ aw_windows_state_root }}\deployment-config.json`""
|
||||
$telemetryExe = "{{ aw_windows_telemetry_exe_path }}"
|
||||
$runAsUser = "{{ aw_windows_file_1c_auto_upload_run_as_user }}"
|
||||
$taskArgs = "file1c-upload --config-path `"{{ aw_windows_state_root }}\deployment-config.json`""
|
||||
$action = New-ScheduledTaskAction -Execute $telemetryExe -Argument $taskArgs
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).Date) -RepetitionInterval (New-TimeSpan -Minutes {{ aw_windows_file_1c_auto_upload_interval_minutes | int }}) -RepetitionDuration (New-TimeSpan -Days 3650)
|
||||
if ([string]::IsNullOrWhiteSpace($runAsUser)) {
|
||||
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
||||
} else {
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $runAsUser -LogonType Interactive -RunLevel Highest
|
||||
}
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 10)
|
||||
$existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
if ($existingTask) {
|
||||
$action = New-ScheduledTaskAction -Execute $powerShellExe -Argument "-NoProfile -ExecutionPolicy Bypass -File `"{{ aw_windows_state_root }}\export-upload-file-1c-telemetry.ps1`" -ConfigPath `"{{ aw_windows_state_root }}\deployment-config.json`""
|
||||
try {
|
||||
Set-ScheduledTask -TaskName $taskName -Action $action -ErrorAction Stop | Out-Null
|
||||
} catch {
|
||||
Write-Host "skip task action update for $taskName because the existing principal requires stored credentials: $($_.Exception.Message)"
|
||||
}
|
||||
Set-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -ErrorAction Stop | Out-Null
|
||||
} else {
|
||||
& schtasks.exe /Create /TN $taskName /TR $taskCommand /SC HOURLY /MO {{ aw_windows_file_1c_auto_upload_interval_hours | int }} /ST 00:00 /RU SYSTEM /RL HIGHEST /F | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Не удалось создать scheduled task $taskName"
|
||||
}
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null
|
||||
}
|
||||
|
||||
@@ -45,7 +45,9 @@ File 1C + reglog + host telemetry
|
||||
- `docker-compose.yml` — локальный scaffold ClickHouse + Grafana.
|
||||
- `.env.example` — переменные окружения.
|
||||
- `clickhouse/init/*.sql` — схема БД.
|
||||
- `etl/load_1c_exports.py` — loader CSV/JSON выгрузок в raw/core таблицы.
|
||||
- `adk-rust/crates/aw-1c-ingest` — production writer CSV/JSON выгрузок в
|
||||
raw/core ClickHouse таблицы.
|
||||
- `etl/load_1c_exports.py` — legacy loader для rollback/ручной отладки.
|
||||
- `etl/build_business_event_exports.py` — read-only normalizer из
|
||||
`documents/postings/audit` в canonical `business_events/document_changes`.
|
||||
- `etl/extract_1c_mcp_toolkit.py` — read-only extractor из
|
||||
@@ -64,7 +66,8 @@ File 1C + reglog + host telemetry
|
||||
- `grafana/provisioning/dashboards/files/1c-telemetry-board.json` — telemetry dashboard по состоянию файловых баз, reglog growth, busy markers и host load.
|
||||
- `detections/build_entity_timeline.sql` — сборка единого timeline слоя.
|
||||
- `detections/open_cases_from_detections.sql` — шаблон открытия cases из detections.
|
||||
- `ops/etl-cron.example` — пример расписания каждые 6 часов.
|
||||
- `ops/etl-cron.example` — legacy cron example; production использует
|
||||
`aw-1c-ingest.timer`.
|
||||
- `ops/retention-policy.md` — минимальная retention policy.
|
||||
- `ai/INVESTIGATOR_API.md` — контракт AI Investigator поверх ClickHouse/cases.
|
||||
- `ai/refresh_company_intelligence.py` — materialization forecast/signals по `counterparty`.
|
||||
@@ -110,7 +113,7 @@ mkdir -p landing/{documents,postings,business_events,document_changes,companies,
|
||||
cp etl/config.example.yml etl/config.yml
|
||||
```
|
||||
|
||||
4. Запустить ETL:
|
||||
4. Запустить ingest:
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
@@ -119,8 +122,7 @@ pip install -r etl/requirements.txt
|
||||
python etl/extract_1c_mcp_toolkit.py --config etl/config.yml --validate-config
|
||||
python etl/extract_1c_mcp_toolkit.py --config etl/config.yml --dataset documents --dry-run
|
||||
python etl/extract_1c_mcp_toolkit.py --config etl/config.yml
|
||||
python etl/build_business_event_exports.py --config etl/config.yml
|
||||
python etl/load_1c_exports.py --config etl/config.yml
|
||||
/usr/local/bin/aw-1c-ingest-rust --root /opt/activitywatch/clickhouse-1c
|
||||
```
|
||||
|
||||
5. Применить detections:
|
||||
@@ -233,9 +235,7 @@ etl/extract_1c_mcp_toolkit.py
|
||||
↓
|
||||
landing/{documents,postings,business_events,document_changes,companies,reglog}
|
||||
↓
|
||||
etl/build_business_event_exports.py
|
||||
↓
|
||||
etl/load_1c_exports.py
|
||||
aw-1c-ingest-rust
|
||||
```
|
||||
|
||||
Что он умеет:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=AW-rus file-1C ingest cycle
|
||||
Description=AW-rus file-1C ingest cycle (Rust)
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
|
||||
@@ -7,7 +7,7 @@ Requires=docker.service
|
||||
Type=oneshot
|
||||
WorkingDirectory=/opt/activitywatch/clickhouse-1c
|
||||
Environment=AW_1C_ROOT=/opt/activitywatch/clickhouse-1c
|
||||
ExecStart=/opt/activitywatch/clickhouse-1c/ops/run_ingest_cycle.sh
|
||||
ExecStart=/usr/local/bin/aw-1c-ingest-rust --root /opt/activitywatch/clickhouse-1c
|
||||
TimeoutStartSec=45min
|
||||
User=root
|
||||
Group=root
|
||||
|
||||
@@ -1,5 +1,2 @@
|
||||
# every 6 hours: load exports, rebuild timeline, refresh cases
|
||||
0 */6 * * * cd /opt/aw-rus/clickhouse-1c && . .venv/bin/activate && python etl/load_1c_exports.py --config etl/config.yml >> /var/log/aw-rus-1c-etl.log 2>&1
|
||||
10 */6 * * * clickhouse-client --queries-file /opt/aw-rus/clickhouse-1c/detections/build_entity_timeline.sql >> /var/log/aw-rus-1c-timeline.log 2>&1
|
||||
15 */6 * * * clickhouse-client --queries-file /opt/aw-rus/clickhouse-1c/detections/insert_detections.sql >> /var/log/aw-rus-1c-detections.log 2>&1
|
||||
20 */6 * * * clickhouse-client --queries-file /opt/aw-rus/clickhouse-1c/detections/open_cases_from_detections.sql >> /var/log/aw-rus-1c-cases.log 2>&1
|
||||
# Legacy cron fallback only. Production uses aw-1c-ingest.timer.
|
||||
0 * * * * AW_1C_ROOT=/opt/activitywatch/clickhouse-1c /usr/local/bin/aw-1c-ingest-rust --root /opt/activitywatch/clickhouse-1c >> /var/log/aw-rus-1c-ingest.log 2>&1
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
|
||||
1. Внешний extractor читает только безопасные read-only источники.
|
||||
2. Формирует `jsonl/csv` в landing-каталоги.
|
||||
3. `etl/load_1c_exports.py` грузит данные в raw/core ClickHouse tables.
|
||||
3. `aw-1c-ingest-rust` грузит данные в raw/core ClickHouse tables.
|
||||
4. Detection/AI слой работает только с ClickHouse.
|
||||
|
||||
То есть LLM и manager pages не ходят в 1С напрямую.
|
||||
@@ -179,7 +179,7 @@
|
||||
- ETL support:
|
||||
- `landing/business_events`
|
||||
- `landing/document_changes`
|
||||
- dataset mapping в `etl/load_1c_exports.py`
|
||||
- dataset mapping в `aw-1c-ingest-rust`
|
||||
- built-in normalizer:
|
||||
- `etl/build_business_event_exports.py`
|
||||
- собирает canonical events из существующих read-only выгрузок
|
||||
@@ -191,7 +191,7 @@
|
||||
- забирает `reglog` через `get_event_log`
|
||||
- пишет в те же `landing/*`, которые уже понимает loader
|
||||
- ingest wiring:
|
||||
- normalizer запускается перед `load_1c_exports.py`
|
||||
- normalizer встроен в production writer `aw-1c-ingest-rust`
|
||||
- timeline/detection wiring:
|
||||
- `business_events` и `document_change_events` уже входят в
|
||||
`entity_timeline`
|
||||
|
||||
@@ -153,7 +153,7 @@ Endpoints:
|
||||
- `clickhouse-1c/ops/run_company_intelligence_api.sh`
|
||||
- `clickhouse-1c/ops/aw-1c-company-api.service`
|
||||
|
||||
И `run_ingest_cycle.sh` теперь:
|
||||
И production writer `aw-1c-ingest-rust` теперь:
|
||||
|
||||
1. грузит новые выгрузки;
|
||||
2. обновляет timeline/detections/cases;
|
||||
|
||||
@@ -58,11 +58,11 @@ Windows file 1C host (<WINDOWS_HOST>)
|
||||
├─ file-base busy markers
|
||||
└─ host telemetry
|
||||
↓
|
||||
export-upload-file-1c-telemetry.ps1
|
||||
aw-windows-telemetry.exe file1c-upload
|
||||
↓ scp
|
||||
<GATEWAY_HOST> /opt/activitywatch/clickhouse-1c/landing/*
|
||||
↓
|
||||
run_ingest_cycle.sh
|
||||
aw-1c-ingest-rust
|
||||
├─ raw tables
|
||||
├─ core tables
|
||||
├─ entity_timeline
|
||||
@@ -76,8 +76,9 @@ Grafana <GRAFANA_HOST>
|
||||
|
||||
Контур считается рабочим, если одновременно выполняется всё:
|
||||
|
||||
1. Windows scheduled task `ActivityWatch File1C Upload` запускается по расписанию.
|
||||
2. На `<GATEWAY_HOST>` работает `aw-1c-ingest.timer`.
|
||||
1. Windows scheduled task `ActivityWatch File1C Upload` запускается раз в 15 минут.
|
||||
2. На `<GATEWAY_HOST>` работает `aw-1c-ingest.timer`; цикл сбора и записи в
|
||||
ClickHouse выполняется раз в 15 минут.
|
||||
3. На `<GATEWAY_HOST>` работает `aw-1c-proofcheck.timer`.
|
||||
4. `ClickHouse` содержит живые строки в:
|
||||
- `documents`
|
||||
@@ -89,11 +90,17 @@ Grafana <GRAFANA_HOST>
|
||||
- `cases`
|
||||
5. В `Grafana` на `<GRAFANA_HOST>` dashboards открываются и смотрят в datasource `clickhouse-1c`.
|
||||
|
||||
Скриншоты не входят в file-1C контур. `ActivityWatch File1C Upload` передает
|
||||
только метаданные файловых баз, журналов, audit/host JSONL и registry workbook.
|
||||
PNG/скриншоты при работе с 1C не копируются в ClickHouse landing и не должны
|
||||
синхронизироваться как DLP evidence.
|
||||
|
||||
## 4. Каталоги и артефакты
|
||||
|
||||
### 4.1 На Windows `<WINDOWS_HOST>`
|
||||
|
||||
- `C:\ProgramData\AWatch-rus\deployment-config.json`
|
||||
- `C:\Program Files\AWatch-rus\windows\aw-windows-telemetry.exe`
|
||||
- `C:\ProgramData\AWatch-rus\export-upload-file-1c-telemetry.ps1`
|
||||
- `C:\ProgramData\AWatch-rus\logs\file1c-telemetry.log`
|
||||
- `C:\ProgramData\AWatch-rus\ssh\awops_ed25519`
|
||||
@@ -112,6 +119,7 @@ Grafana <GRAFANA_HOST>
|
||||
- runtime:
|
||||
- `/opt/activitywatch/clickhouse-1c/.env`
|
||||
- `/opt/activitywatch/clickhouse-1c/etl/config.yml`
|
||||
- `/usr/local/bin/aw-1c-ingest-rust`
|
||||
- `/opt/activitywatch/clickhouse-1c/.venv`
|
||||
|
||||
### 4.3 systemd units на `<GATEWAY_HOST>`
|
||||
@@ -142,7 +150,8 @@ ansible-playbook -i <PROJECT_ROOT>/ansible/inventory.ini \
|
||||
- раскладывает `clickhouse-1c` в `/opt/activitywatch/clickhouse-1c`;
|
||||
- поднимает `ClickHouse`;
|
||||
- создаёт `.env` и `etl/config.yml`;
|
||||
- включает `aw-1c-ingest.timer`;
|
||||
- устанавливает Rust-бинарник `aw-1c-ingest-rust`;
|
||||
- включает `aw-1c-ingest.timer` с периодом 15 минут;
|
||||
- включает `aw-1c-proofcheck.timer`.
|
||||
|
||||
### 5.2 Windows uploader на `<WINDOWS_HOST>`
|
||||
@@ -160,9 +169,11 @@ ansible-playbook -i <PROJECT_ROOT>/ansible/inventory.ini \
|
||||
|
||||
Что делает:
|
||||
|
||||
- копирует `export-upload-file-1c-telemetry.ps1`;
|
||||
- копирует Rust-бинарник `aw-windows-telemetry.exe`;
|
||||
- оставляет `export-upload-file-1c-telemetry.ps1` как legacy fallback;
|
||||
- обновляет `deployment-config.json`;
|
||||
- создаёт/обновляет scheduled task `ActivityWatch File1C Upload`.
|
||||
- создаёт/обновляет scheduled task `ActivityWatch File1C Upload` на запуск
|
||||
`aw-windows-telemetry.exe file1c-upload`.
|
||||
|
||||
### 5.3 Production Grafana на `<GRAFANA_HOST>`
|
||||
|
||||
@@ -186,22 +197,31 @@ Grafana уже должна содержать:
|
||||
|
||||
### 6.1 Почему он нужен
|
||||
|
||||
Создание file-1C scheduled task через `schtasks` по умолчанию использует `SYSTEM`.
|
||||
|
||||
Для этой конкретной задачи production-схема должна использовать **рабочий principal**, а не `SYSTEM`.
|
||||
Для этой конкретной задачи production-схема должна использовать **рабочий
|
||||
interactive principal**, а не `SYSTEM`, потому что на текущем Windows-хосте
|
||||
SYSTEM-запуск внешних процессов нестабилен.
|
||||
|
||||
Проверенная рабочая учётка:
|
||||
|
||||
- `HOST-EXAMPLE\Администратор`
|
||||
|
||||
### 6.2 Команда переключения principal
|
||||
### 6.2 Проверка scheduled task
|
||||
|
||||
На `<WINDOWS_HOST>`:
|
||||
|
||||
```cmd
|
||||
schtasks /Change /TN "\ActivityWatch File1C Upload" /RU "HOST-EXAMPLE\Администратор" /RP "<LOCAL_ADMIN_PASSWORD>"
|
||||
```powershell
|
||||
$task = Get-ScheduledTask -TaskName "ActivityWatch File1C Upload"
|
||||
$task.Actions[0].Execute
|
||||
$task.Actions[0].Arguments
|
||||
$task.Principal.UserId
|
||||
```
|
||||
|
||||
Ожидаемо:
|
||||
|
||||
- `Execute`: `C:\Program Files\AWatch-rus\windows\aw-windows-telemetry.exe`
|
||||
- `Arguments`: `file1c-upload --config-path "C:\ProgramData\AWatch-rus\deployment-config.json"`
|
||||
- `Principal`: `Администратор` / `HOST-EXAMPLE\Администратор`
|
||||
|
||||
### 6.3 Проверка
|
||||
|
||||
```cmd
|
||||
@@ -237,7 +257,7 @@ Get-Content -Tail 80 C:\ProgramData\AWatch-rus\logs\file1c-telemetry.log
|
||||
### 7.2 Backend ingestion
|
||||
|
||||
```bash
|
||||
AW_1C_ROOT=/opt/activitywatch/clickhouse-1c /opt/activitywatch/clickhouse-1c/ops/run_ingest_cycle.sh
|
||||
AW_1C_ROOT=/opt/activitywatch/clickhouse-1c /usr/local/bin/aw-1c-ingest-rust --root /opt/activitywatch/clickhouse-1c
|
||||
```
|
||||
|
||||
### 7.3 Freshness proof
|
||||
@@ -392,7 +412,7 @@ docker ps --format '{{.Names}}' | rg aw-rus-1c-clickhouse
|
||||
3. Запустить exporter вручную:
|
||||
|
||||
```powershell
|
||||
& "C:\ProgramData\AWatch-rus\export-upload-file-1c-telemetry.ps1" -ConfigPath "C:\ProgramData\AWatch-rus\deployment-config.json"
|
||||
& "C:\Program Files\AWatch-rus\windows\aw-windows-telemetry.exe" file1c-upload --config-path "C:\ProgramData\AWatch-rus\deployment-config.json"
|
||||
```
|
||||
|
||||
### 12.2 Backend сторона
|
||||
@@ -402,12 +422,125 @@ docker ps --format '{{.Names}}' | rg aw-rus-1c-clickhouse
|
||||
3. Запустить ingest вручную:
|
||||
|
||||
```bash
|
||||
AW_1C_ROOT=/opt/activitywatch/clickhouse-1c /opt/activitywatch/clickhouse-1c/ops/run_ingest_cycle.sh
|
||||
AW_1C_ROOT=/opt/activitywatch/clickhouse-1c /usr/local/bin/aw-1c-ingest-rust --root /opt/activitywatch/clickhouse-1c
|
||||
```
|
||||
|
||||
4. Проверить freshness.
|
||||
|
||||
## 13. Безопасность
|
||||
## 13. Ручное изменение параметров
|
||||
|
||||
Использовать этот раздел, если нужно временно изменить production-параметры
|
||||
без полного redeploy. После ручного изменения желательно синхронизировать те же
|
||||
значения в Ansible vars, иначе следующий полный deploy может вернуть прежние
|
||||
настройки.
|
||||
|
||||
### 13.1 Изменить период Windows file-1C upload
|
||||
|
||||
На Windows/RDP host в elevated PowerShell:
|
||||
|
||||
```powershell
|
||||
$taskName = "ActivityWatch File1C Upload"
|
||||
$minutes = 15
|
||||
|
||||
$configPath = "C:\ProgramData\AWatch-rus\deployment-config.json"
|
||||
$config = Get-Content -Raw -LiteralPath $configPath | ConvertFrom-Json
|
||||
$config.analytics.file1cAutomation.intervalMinutes = $minutes
|
||||
$config | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $configPath -Encoding UTF8
|
||||
|
||||
$task = Get-ScheduledTask -TaskName $taskName
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).Date) `
|
||||
-RepetitionInterval (New-TimeSpan -Minutes $minutes) `
|
||||
-RepetitionDuration (New-TimeSpan -Days 3650)
|
||||
|
||||
Set-ScheduledTask -TaskName $taskName `
|
||||
-Action $task.Actions `
|
||||
-Trigger $trigger `
|
||||
-Principal $task.Principal `
|
||||
-Settings $task.Settings
|
||||
```
|
||||
|
||||
Проверить:
|
||||
|
||||
```powershell
|
||||
Get-ScheduledTaskInfo -TaskName "ActivityWatch File1C Upload"
|
||||
(Get-ScheduledTask -TaskName "ActivityWatch File1C Upload").Actions
|
||||
```
|
||||
|
||||
### 13.2 Изменить параметры file-1C upload
|
||||
|
||||
Основной файл:
|
||||
|
||||
```text
|
||||
C:\ProgramData\AWatch-rus\deployment-config.json
|
||||
```
|
||||
|
||||
Ключевые поля:
|
||||
|
||||
```json
|
||||
{
|
||||
"analytics": {
|
||||
"file1cAutomation": {
|
||||
"intervalMinutes": 15,
|
||||
"targetHost": "<GATEWAY_HOST>",
|
||||
"targetUser": "igor",
|
||||
"remoteRoot": "/opt/activitywatch/clickhouse-1c/landing",
|
||||
"remoteKeyPath": "C:\\ProgramData\\AWatch-rus\\ssh\\awops_ed25519",
|
||||
"registryWorkbookPath": "E:\\USER1\\СПИСОК ПРЕДПРИЯТИЙ И ИХ РАСПРЕДЕЛЕНИЕ.xlsx"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Smoke-run после изменения:
|
||||
|
||||
```powershell
|
||||
& "C:\Program Files\AWatch-rus\windows\aw-windows-telemetry.exe" file1c-upload --config-path "C:\ProgramData\AWatch-rus\deployment-config.json"
|
||||
Get-Content -LiteralPath "C:\ProgramData\AWatch-rus\logs\file1c-telemetry.log" -Tail 40 -Encoding UTF8
|
||||
```
|
||||
|
||||
### 13.3 Изменить период server-side ingest
|
||||
|
||||
На `<GATEWAY_HOST>`:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/systemd/system/aw-1c-ingest.timer.d
|
||||
|
||||
sudo tee /etc/systemd/system/aw-1c-ingest.timer.d/override.conf >/dev/null <<'EOF'
|
||||
[Timer]
|
||||
OnUnitActiveSec=
|
||||
OnUnitActiveSec=15min
|
||||
EOF
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart aw-1c-ingest.timer
|
||||
systemctl list-timers aw-1c-ingest.timer
|
||||
```
|
||||
|
||||
### 13.4 Изменить защитную задержку ingest
|
||||
|
||||
Файл на `<GATEWAY_HOST>`:
|
||||
|
||||
```bash
|
||||
sudo nano /opt/activitywatch/clickhouse-1c/etl/config.yml
|
||||
```
|
||||
|
||||
Параметр:
|
||||
|
||||
```yaml
|
||||
min_file_age_seconds: 180
|
||||
```
|
||||
|
||||
Нормальный диапазон: `60-180` секунд. Не ставить слишком низко: ingest может
|
||||
прочитать файл во время SCP-загрузки.
|
||||
|
||||
Проверить:
|
||||
|
||||
```bash
|
||||
sudo systemctl start aw-1c-ingest.service
|
||||
sudo journalctl -u aw-1c-ingest.service -n 50 --no-pager
|
||||
```
|
||||
|
||||
## 14. Безопасность
|
||||
|
||||
- Не хранить пароль администратора Windows в git.
|
||||
- Не хранить секреты в docs.
|
||||
@@ -415,11 +548,11 @@ AW_1C_ROOT=/opt/activitywatch/clickhouse-1c /opt/activitywatch/clickhouse-1c/ops
|
||||
- Не давать AI write-back в 1С.
|
||||
- Не менять `1Cv8.1CD`.
|
||||
|
||||
## 14. Связанные файлы
|
||||
## 15. Связанные файлы
|
||||
|
||||
- [clickhouse-1c/README.md](<PROJECT_ROOT>/clickhouse-1c/README.md)
|
||||
- [clickhouse-1c/etl/load_1c_exports.py](<PROJECT_ROOT>/clickhouse-1c/etl/load_1c_exports.py)
|
||||
- [clickhouse-1c/ops/run_ingest_cycle.sh](<PROJECT_ROOT>/clickhouse-1c/ops/run_ingest_cycle.sh)
|
||||
- [adk-rust/crates/aw-1c-ingest](<PROJECT_ROOT>/adk-rust/crates/aw-1c-ingest)
|
||||
- [clickhouse-1c/ops/run_ingest_cycle.sh](<PROJECT_ROOT>/clickhouse-1c/ops/run_ingest_cycle.sh) - legacy rollback path
|
||||
- [clickhouse-1c/ops/check_ingest_freshness.sh](<PROJECT_ROOT>/clickhouse-1c/ops/check_ingest_freshness.sh)
|
||||
- [ansible/deploy_file_1c_analytics.yml](<PROJECT_ROOT>/ansible/deploy_file_1c_analytics.yml)
|
||||
- [ansible/deploy_file_1c_windows_telemetry.yml](<PROJECT_ROOT>/ansible/deploy_file_1c_windows_telemetry.yml)
|
||||
|
||||
@@ -27,12 +27,15 @@ Future Desktop Forensics: Tauri + React + Rust core
|
||||
|
||||
## Исключено из roadmap
|
||||
|
||||
Dioxus и DPD Portal исключены из архитектурного roadmap проекта.
|
||||
Экспериментальные Rust UI/prototype mirror направления исключены из
|
||||
архитектурного roadmap проекта.
|
||||
|
||||
Это означает:
|
||||
|
||||
- новые Dioxus/DPD crate не добавляются;
|
||||
- `/dpd/` и `/dpd/api/*` не являются частью публичного API-контракта;
|
||||
- новые crate для экспериментальных UI/prototype mirror направлений не
|
||||
добавляются;
|
||||
- устаревшие mirror-prefix маршруты не являются частью публичного
|
||||
API-контракта;
|
||||
- будущий UI не должен парсить HTML текущего портала;
|
||||
- новые UI-фреймворки требуют отдельного architecture decision;
|
||||
- breaking changes API требуют version bump контракта и migration window.
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
# Аудит очистки архитектуры от DPD/Dioxus
|
||||
|
||||
Дата: 2026-06-05
|
||||
|
||||
## Цель
|
||||
|
||||
Зафиксировать результат проверки наследия DPD/Dioxus после усиления API
|
||||
контрактов в коммите `bc2f56e`.
|
||||
|
||||
## Найденные DPD-артефакты до очистки
|
||||
|
||||
- workspace crate `adk-rust/crates/detmir-dpd-portal`;
|
||||
- запись workspace member `crates/detmir-dpd-portal` в `adk-rust/Cargo.toml`;
|
||||
- package entry `detmir-dpd-portal` в `adk-rust/Cargo.lock`;
|
||||
- документ `docs/DPD_PORTAL_RU.md`;
|
||||
- OpenAPI server base `/dpd/api`;
|
||||
- упоминания DPD mirror в `docs/PORTAL_API_CONTRACTS_RU.md`;
|
||||
- упоминания DPD mirror в `docs/UI_ARCHITECTURE_BASELINE_RU.md`.
|
||||
|
||||
## Найденные Dioxus-артефакты до очистки
|
||||
|
||||
Production-код, workspace crates, scripts, examples и OpenAPI/TypeScript
|
||||
контракты не содержали Dioxus-зависимостей или `rsx!`-кода.
|
||||
|
||||
Оставались только запретительные упоминания в архитектурном baseline:
|
||||
|
||||
- Dioxus не является частью roadmap;
|
||||
- Dioxus не добавлять без отдельного architecture decision.
|
||||
|
||||
## Выполненное решение
|
||||
|
||||
- DPD Portal исключён из workspace и публичного contract layer.
|
||||
- Документ `docs/DPD_PORTAL_RU.md` удалён как описание экспериментальной ветки.
|
||||
- OpenAPI больше не публикует `/dpd/api`.
|
||||
- Runtime-слой DPD Portal выведен из эксплуатации: app-service остановлен и
|
||||
выключен, gateway route удалён, перед удалением создан локальный backup.
|
||||
- `docs/ARCHITECTURE_BASELINE_RU.md`,
|
||||
`docs/UI_ARCHITECTURE_BASELINE_RU.md` и
|
||||
`docs/PORTAL_API_CONTRACTS_RU.md` фиксируют единственную целевую архитектуру:
|
||||
Rust backend/API, Rust agent, Rust SSR + HTML + HTMX portal, будущий
|
||||
React/TypeScript UI и будущий Tauri + React + Rust core desktop forensics.
|
||||
|
||||
## Статус
|
||||
|
||||
DPD/Dioxus остаются только как исторически упомянутые исключённые направления.
|
||||
Они не являются production runtime, публичным API или частью roadmap.
|
||||
@@ -245,7 +245,10 @@
|
||||
- SHA-256 validation;
|
||||
- atomic write в evidence storage;
|
||||
- audit records для upload/view/download;
|
||||
- Windows scheduled task `ActivityWatch DLP Evidence Sync` каждые 5 минут;
|
||||
- Windows scheduled task `ActivityWatch DLP Evidence Sync` каждые 15 минут
|
||||
через Rust-бинарник `aw-windows-telemetry.exe dlp-evidence-sync`; копируются
|
||||
только DLP incident screenshots, 1C/обычные PNG из `incident-artifacts`
|
||||
игнорируются;
|
||||
- external Playwright smoke для портала/evidence/Grafana routes;
|
||||
- Grafana актуальность и datasource health checks;
|
||||
- rollback-critical backups;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# Отчёт об удалении Dioxus из AWatch-rus
|
||||
|
||||
Дата: 2026-06-05
|
||||
|
||||
## Итог
|
||||
|
||||
Dioxus не используется в проекте и исключен из архитектурного roadmap.
|
||||
|
||||
Production-архитектура после проверки:
|
||||
|
||||
- Backend/API: Rust;
|
||||
- Agent: Rust;
|
||||
- Portal: Rust server-rendered HTML + HTMX;
|
||||
- Future Enterprise UI: React + TypeScript через JSON API-контракты;
|
||||
- Future Desktop Forensics: Tauri + React + Rust core.
|
||||
|
||||
## Удалённые и отсутствующие артефакты
|
||||
|
||||
В текущем дереве подтверждено отсутствие:
|
||||
|
||||
- workspace crate `adk-rust/crates/detmir-dpd-portal`;
|
||||
- workspace member `crates/detmir-dpd-portal` в `adk-rust/Cargo.toml`;
|
||||
- package entry `detmir-dpd-portal` в `adk-rust/Cargo.lock`;
|
||||
- документа `docs/DPD_PORTAL_RU.md`;
|
||||
- OpenAPI server base `/dpd/api`;
|
||||
- исходного кода с `rsx!`;
|
||||
- зависимостей `dioxus` или `dioxuslabs`.
|
||||
|
||||
В рамках финальной консолидации удалён устаревший частичный audit-документ
|
||||
`docs/ARCHITECTURE_CLEANUP_AUDIT_2026-06-05_RU.md`; его содержимое заменено
|
||||
этим отчётом.
|
||||
|
||||
## Зависимости
|
||||
|
||||
Дополнительные Dioxus-зависимости не удалялись, потому что в проверенном
|
||||
workspace они уже отсутствуют. `adk-rust/Cargo.toml` не содержит Dioxus/DPD
|
||||
members или workspace dependencies; поиск по `adk-rust` вне `target/` не
|
||||
находит `dioxus`, `dioxuslabs`, `rsx!`, `detmir-dpd` или `dpd`.
|
||||
|
||||
## Проверенные маршруты
|
||||
|
||||
Контрактные и portal маршруты остаются в `adk-rust/crates/detmir-portal`:
|
||||
|
||||
- `/portal` - HTTP 200;
|
||||
- `/reports` - HTTP 200;
|
||||
- `/portal/reports` - HTTP 200;
|
||||
- `/api/reports` - HTTP 200;
|
||||
- `/api/contracts` - HTTP 200;
|
||||
- `/api/contracts/openapi.json` - HTTP 200;
|
||||
- `/api/contracts/typescript.d.ts` - HTTP 200.
|
||||
|
||||
`/dpd/` и `/dpd/api/*` не входят в публичный API-контракт и не публикуются в
|
||||
OpenAPI.
|
||||
|
||||
## Выполненные проверки
|
||||
|
||||
Команды выполнены из `adk-rust`:
|
||||
|
||||
- `cargo fmt --check` - ok;
|
||||
- `CARGO_TARGET_DIR=/tmp/aw-rus-cargo-target cargo clippy --all-targets --all-features -- -D warnings` - ok;
|
||||
- `CARGO_TARGET_DIR=/tmp/aw-rus-cargo-target cargo test --all` - ok;
|
||||
- `CARGO_TARGET_DIR=/tmp/aw-rus-cargo-target cargo build --all` - ok;
|
||||
- `cargo tree --all-features` + поиск `dioxus|dioxuslabs|rsx!|detmir-dpd|dpd` - совпадений нет;
|
||||
- `grep -RIn` по исходникам, документации и контрактам с исключением `.git`,
|
||||
`.ai`, `target`, `.local`, `dist`, `.ops`, `.playwright-cli` - совпадения
|
||||
только в этом отчёте;
|
||||
- runtime smoke `detmir-portal` на `127.0.0.1:18720` - обязательные маршруты
|
||||
вернули HTTP 200;
|
||||
- поиск по runtime JSON/OpenAPI/TypeScript ответам - `/dpd`, Dioxus и `rsx!`
|
||||
не найдены.
|
||||
|
||||
## Архитектурное подтверждение
|
||||
|
||||
Dioxus и связанная DPD-проба не являются production runtime, UI roadmap,
|
||||
публичным API, workspace crate или dependency layer проекта. Дальнейшее
|
||||
развитие портала выполняется через Rust SSR + HTML + HTMX и стабильные JSON
|
||||
API-контракты для будущих React/Tauri клиентов.
|
||||
@@ -0,0 +1,140 @@
|
||||
# Анализ разрывов готовности пилота: AWatch-rus
|
||||
|
||||
Дата аудита: 2026-06-05.
|
||||
|
||||
Назначение: финальная проверка готовности AWatch-rus к демонстрационному
|
||||
пилоту для руководителя, ИБ и эксплуатации. После первичного аудита закрыты
|
||||
демонстрационные разрывы без добавления новых сущностей, новых интеграций и
|
||||
архитектурных изменений.
|
||||
|
||||
Не входило в аудит: pfSense, Telegram, Grafana, InfluxDB.
|
||||
|
||||
## 1. Готово к пилоту
|
||||
|
||||
- Executive View готов к демонстрации: главный вывод отображается первым,
|
||||
управленческие блоки идут в понятном порядке, технические термины не
|
||||
выводятся в пользовательском тексте.
|
||||
- Security View готов к демонстрации: есть кандидаты на проверку, аудит,
|
||||
материалы расследования, пакет расследования и переход к карточке
|
||||
расследования.
|
||||
- Operations View готов к демонстрации: видны полнота и качество данных,
|
||||
ошибки сбора, состояние источников и доступность событий безопасности.
|
||||
- Контур ClickHouse для событий ИБ работает на `<GATEWAY_HOST>` в Docker-контейнере
|
||||
`aw-rus-1c-clickhouse`; портал читает события через ClickHouse, fallback не
|
||||
используется.
|
||||
- `/api/health` возвращает `ok=true`; источники `detmir_check`,
|
||||
`detmir_status`, `worktime_api`, `worktime_management`, `one_c`,
|
||||
`dlp_health`, `security_events` находятся в рабочем состоянии.
|
||||
- В текущем отчете есть 3 кандидата на проверку; smoke подтвердил 3 кнопки
|
||||
перехода к расследованию.
|
||||
- Первый отчет прогревается при старте `detmir-portal.service`; после прогрева
|
||||
`/api/reports` отвечает за доли секунды.
|
||||
- Фоновое обновление больше не переводит готовый экран обратно в состояние
|
||||
"Загрузка данных"; 70-секундная браузерная проверка сохранила `READY`.
|
||||
- Мобильная проверка 390px проходит: страница доходит до `READY`, глобального
|
||||
горизонтального расползания body не выявлено.
|
||||
- Обязательные проверки на текущем дереве пройдены:
|
||||
|
||||
| Проверка | Итог |
|
||||
| --- | --- |
|
||||
| `cargo test --workspace` | Пройдено. |
|
||||
| `cargo clippy --all-targets --all-features` | Пройдено. |
|
||||
| `cargo build --release` | Пройдено. |
|
||||
| `node scripts/detmir-portal-tabs-smoke.mjs` | Пройдено против `<PORTAL_URL>` с `security_events=available`. |
|
||||
|
||||
## 2. Желательно исправить до пилота
|
||||
|
||||
- Передать заказчику только вычитанные customer-facing документы. Внутренние
|
||||
документы могут содержать технические обозначения, но демонстрационный пакет
|
||||
для руководителя должен использовать русские деловые формулировки.
|
||||
- Отдельно проверить экспорт итогового отчета в том формате, который будет
|
||||
показан заказчику: текстовый отчет, печать или PDF.
|
||||
- Зафиксировать демонстрационный сценарий в виде короткой шпаргалки оператора:
|
||||
с какой вкладки начинать, какой кандидат открывается, какой пакет скачивать,
|
||||
какой итоговый вывод озвучивать.
|
||||
- Зафиксировать hand-off правило для Rust-сборки: на этой машине использовать
|
||||
`CARGO_TARGET_DIR=<LOCAL_CARGO_TARGET_DIR>`, потому что старый
|
||||
`target/` на `fuseblk` непригоден для `libsqlite3-sys`.
|
||||
|
||||
## 3. Критично исправить до пилота
|
||||
|
||||
Критичных технических блокеров для контролируемого демонстрационного пилота на
|
||||
текущем контуре не осталось.
|
||||
|
||||
Единственное обязательное условие перед показом: не начинать демонстрацию без
|
||||
короткого преддемо-прогона портала на том же экране и сети. Это не доработка
|
||||
продукта, а операционная страховка от внешних факторов: сеть, браузер, доступ к
|
||||
стенду, свежесть данных.
|
||||
|
||||
## 4. Можно перенести после пилота
|
||||
|
||||
- Полное hardening JSON/JSONL-хранилищ: блокировки, atomic write, ротация,
|
||||
retention, backup/restore и нагрузочная проверка больших state-файлов.
|
||||
- Формальную матрицу версий API и журнал совместимых и несовместимых изменений
|
||||
полей.
|
||||
- Расширенные нагрузочные тесты и sizing под промышленную эксплуатацию.
|
||||
- Полировку коммерческого пакета после первой обратной связи заказчика.
|
||||
- Расширенную автоматизацию мобильных скриншотов и визуальных regression tests.
|
||||
- Расширенную регламентацию долгосрочного доступа к материалам расследований и
|
||||
архивам.
|
||||
|
||||
## 5. Общая оценка готовности
|
||||
|
||||
AWatch-rus готов к контролируемому демонстрационному пилоту на текущем DetMir
|
||||
контуре. Архитектура сохранена, обратная совместимость не нарушена, новые
|
||||
интеграции и сущности не добавлялись.
|
||||
|
||||
Практическая оценка готовности: 90% для демонстрационного пилота. Оставшиеся
|
||||
10% относятся не к блокерам кода, а к дисциплине показа: прогрев, заранее
|
||||
открытый сценарий, проверенный экспорт и вычитанный customer-facing пакет.
|
||||
|
||||
## 6. Оценка Executive View
|
||||
|
||||
- Главный вывод расположен первым и подтвержден smoke-проверкой порядка блоков.
|
||||
- Представление понятно руководителю без знаний ИБ: риск описывается через
|
||||
подразделение, причину, подтверждающие слои и рекомендуемое действие.
|
||||
- Англоязычные и внутренние технические обозначения в пользовательском слое
|
||||
smoke не обнаружил.
|
||||
- Итог: Executive View можно показывать руководителю.
|
||||
|
||||
## 7. Оценка Security View
|
||||
|
||||
- Есть очередь кандидатов на проверку, аудит, материалы расследования и пакет
|
||||
расследования.
|
||||
- Ручное подтверждение сохранено: система не создает инциденты автоматически
|
||||
без решения ответственного.
|
||||
- Переход "кандидат -> расследование" проверен smoke: найдено 3 кнопки перехода.
|
||||
- Итог: Security View можно показывать ИБ как пилотный процесс ручной проверки.
|
||||
|
||||
## 8. Оценка Operations View
|
||||
|
||||
- Operations View показывает полноту данных, качество данных, ошибки сбора,
|
||||
состояние источников и режим событий безопасности.
|
||||
- ClickHouse-события доступны через портал: `backend=clickhouse`, `status=ok`,
|
||||
`fallback_used=false`.
|
||||
- `/api/health` зеленый на рабочем контуре.
|
||||
- Фоновое обновление не ломает готовое состояние экрана.
|
||||
- Итог: Operations View можно показывать эксплуатации.
|
||||
|
||||
## 9. Риски демонстрации
|
||||
|
||||
| Риск | Влияние | Статус |
|
||||
| --- | --- | --- |
|
||||
| Открыть портал без прогрева или при нестабильной сети | Может выглядеть как долгая загрузка | Управляемо: service prewarm и преддемо-прогон |
|
||||
| Не проверить экспорт отчета заранее | Можно потерять время на живом показе | Желательно закрыть до показа |
|
||||
| Отдать внутреннюю документацию без вычитки | Лишние технические вопросы руководителя и ИБ | Желательно закрыть до показа |
|
||||
| Данные изменятся перед демо и кандидатов станет меньше | Сценарий расследования может ослабнуть | Управляемо: проверить `/api/reports` перед показом |
|
||||
| Внешние источники недоступны из-за сети | Потеря части операционной картины | Управляемо: преддемо health-check |
|
||||
|
||||
10-минутный сценарий сейчас подтвержден: главный риск, подразделение, причина
|
||||
риска, кандидат на проверку, расследование, пакет расследования и итоговый
|
||||
управленческий вывод доступны в портале.
|
||||
|
||||
## 10. Итоговая рекомендация
|
||||
|
||||
Допустить AWatch-rus к контролируемому демонстрационному пилоту.
|
||||
|
||||
Перед показом выполнить короткий операторский прогон: открыть портал, убедиться
|
||||
в `Данные готовы`, проверить наличие кандидатов, открыть расследование, скачать
|
||||
пакет расследования и открыть итоговый отчет. Это достаточно для уверенного
|
||||
показа руководителю, ИБ и эксплуатации.
|
||||
@@ -52,7 +52,8 @@ React/Tauri-интерфейса без переписывания backend-ло
|
||||
- Маршрут `/portal/` остаётся стабильным.
|
||||
- Backend-расчёты, JSON-хранилища и workflow не дублируются во frontend.
|
||||
- Публичные JSON-поля не переименовываются без новой версии контракта.
|
||||
- Dioxus и DPD Portal исключены из архитектурного roadmap проекта.
|
||||
- Экспериментальные mirror/prototype направления не входят в публичный
|
||||
contract layer и не должны возвращаться без отдельного architecture decision.
|
||||
|
||||
## Проверка
|
||||
|
||||
|
||||
@@ -0,0 +1,616 @@
|
||||
# Roadmap: замена PowerShell на Rust EXE
|
||||
|
||||
Дата: 2026-06-05
|
||||
|
||||
Цель: убрать зависимость AWatch-rus/DetMir от PowerShell-скриптов на рабочих
|
||||
хостах и заменить их на самодостаточные Rust EXE/службы без потери данных,
|
||||
без ухудшения пилотной демонстрации и без изменения функционального объема.
|
||||
|
||||
Документ описывает порядок миграции. Он не вводит новые функции: каждая Rust
|
||||
замена сначала должна повторить текущий контракт PowerShell-компонента.
|
||||
|
||||
Операторский checkpoint от 2026-06-05 сохранен в private `.ops`-контуре:
|
||||
Phase 0 live inventory выполнен, Phase 2 validation для уже переведенных
|
||||
Windows Rust paths выполнен успешно. В tracked документации не публикуются live
|
||||
hostnames, private IP, токены и runtime evidence paths.
|
||||
|
||||
## 1. Целевое состояние
|
||||
|
||||
К концу миграции:
|
||||
|
||||
- на Windows/RDP-хостах регулярный сбор данных, evidence-sync, 1C upload,
|
||||
guard/recovery и validation выполняются Rust EXE;
|
||||
- в Scheduled Tasks и Windows Services нет штатных AWatch-rus задач, которые
|
||||
запускают `powershell.exe` или `pwsh.exe`;
|
||||
- install-kit поставляет Rust EXE, конфиги и service/task definitions вместо
|
||||
runtime `.ps1`;
|
||||
- Ansible разворачивает Rust binaries и конфиги, а не копирует PowerShell
|
||||
runtime scripts;
|
||||
- Telegram/Codex/bot контур вызывает Rust helpers или читает Rust JSON
|
||||
snapshots, не опираясь на пути к `.ps1`;
|
||||
- legacy PowerShell хранится только как временный rollback-слой до прохождения
|
||||
acceptance gates и затем удаляется из install-kit;
|
||||
- Linux/AW/Proxmox части остаются Rust-first; Ansible может остаться
|
||||
оркестратором, потому что это инфраструктурный deploy layer, а не runtime
|
||||
PowerShell.
|
||||
|
||||
## 2. Текущее состояние
|
||||
|
||||
Уже выполнено или частично выполнено:
|
||||
|
||||
- `awatch-agent-rs` заменяет Windows `worktime-session-collector.ps1` для
|
||||
worktime/RDP path. Legacy PowerShell оставлен как fallback и управляется
|
||||
`collectors.worktimeSessionEnabled`, `worktimeSessionMode`,
|
||||
`worktimeLegacyFallbackEnabled`.
|
||||
- `aw-windows-telemetry.exe` уже используется для:
|
||||
- `file1c-upload`;
|
||||
- `dlp-evidence-sync`.
|
||||
- `aw-1c-ingest-rust` пишет 1C/file analytics в ClickHouse на серверной
|
||||
стороне.
|
||||
- `aw-windows-telemetry.exe validate-deployment` добавлен как Rust validation
|
||||
gate первого уровня. Он проверяет уже мигрированные Windows Rust paths,
|
||||
свежесть worktime bucket, Rust collector guard service и queue sanity без
|
||||
запуска PowerShell.
|
||||
- `AWatchRusCollectorGuard` переключен с `aw-collector-guard.ps1` на
|
||||
`aw-windows-telemetry.exe collector-guard`. Старый PowerShell guard оставлен
|
||||
как rollback script, но штатный service runtime его не запускает. Rust guard
|
||||
получает `sessionId` из native process snapshot, дедуплицирует legacy
|
||||
collectors по `(kind, sessionId)` и не запускает пользовательские launch
|
||||
tasks повторно, если legacy collectors уже активны.
|
||||
- `aw-windows-telemetry.exe` добавил P0 runtime collector subcommands:
|
||||
`browser-domains-collector`, `dlp-endpoint-collector`,
|
||||
`file-operations-collector`. На live Windows/RDP host они включены через
|
||||
`collectors.*Mode=rust_primary`, работают в трех пользовательских сессиях,
|
||||
а P0 PowerShell collector runtime отсутствует. Это не означает полного
|
||||
удаления legacy `.ps1`: они оставлены как rollback/reference до расширения
|
||||
глубокой функциональной parity.
|
||||
- Основные серверные DetMir/AW/DLP helpers уже Rust-first:
|
||||
`detmir-status`, `detmir-check`, `detmir-auto`, `detmir-heal-safe`,
|
||||
`aw-rus-healthd-rust`, `dlp-*`, `worktime-*`, `aw-health-check`,
|
||||
`check-aw-data`, `aw-prune-local-state` и другие.
|
||||
|
||||
Остаток PowerShell в рабочем дереве:
|
||||
|
||||
- 26 product `.ps1` в `windows/`;
|
||||
- operator/MCP helper `scripts/powershell/detmir-powershell-profile.ps1`;
|
||||
- parse-check helper `.pssa_run.ps1`;
|
||||
- `.venv/bin/activate.ps1` внутри virtualenv не является продуктовым
|
||||
компонентом и не входит в миграцию.
|
||||
|
||||
## 3. Инвентаризация Windows PowerShell
|
||||
|
||||
| Скрипт | Роль | Цель миграции | Приоритет |
|
||||
|---|---|---|---|
|
||||
| `worktime-session-collector.ps1` | RDP/worktime сбор | закрепить замену через `awatch-agent-rs`, затем удалить fallback | P0 done/stabilize |
|
||||
| `browser-domains-native-collector.ps1` | browser/domain и DLP web-сигналы | `aw-windows-telemetry.exe browser-domains-collector`; live Rust primary, legacy fallback/reference | P0 live/stabilize |
|
||||
| `dlp-endpoint-signals-collector.ps1` | clipboard/USB/print/DLP incident signals | `aw-windows-telemetry.exe dlp-endpoint-collector`; live Rust primary, screenshots только для DLP events | P0 live/stabilize |
|
||||
| `file-operations-collector.ps1` | file create/delete/rename/archive hints | `aw-windows-telemetry.exe file-operations-collector`; live Rust primary with queue/spool | P0 live/stabilize |
|
||||
| `email-outbound-collector.ps1` | outbound email metadata/DLP | Rust email metadata collector | P1 |
|
||||
| `dlp-policy-client.ps1` | получение DLP policy | Rust policy client/cache | P1 |
|
||||
| `export-upload-file-1c-telemetry.ps1` | 1C telemetry upload | закрепить `aw-windows-telemetry.exe file1c-upload`, затем удалить legacy | P0 done/stabilize |
|
||||
| `sync-dlp-evidence-artifacts.ps1` | DLP evidence upload | закрепить `aw-windows-telemetry.exe dlp-evidence-sync` | P0 done/stabilize |
|
||||
| `export-evtx-for-hayabusa.ps1` | bounded EVTX export | Rust EVTX export helper | P1 |
|
||||
| `export-upload-hayabusa-to-aw-server.ps1` | EVTX/Hayabusa upload | Rust upload helper или режим в `aw-windows-telemetry.exe` | P1 |
|
||||
| `aw-standalone-service.ps1` | supervisor service wrapper | Rust Windows service wrapper | P0 |
|
||||
| `aw-collector-guard.ps1` | guard/restart/recovery | Rust guard with allowlist, lock, cooldown | P0 done/stabilize |
|
||||
| `hardening-recovery.ps1` | recovery/hardening | Rust recovery CLI; destructive actions behind `--apply` | P1 |
|
||||
| `validate-deployment.ps1` | post-deploy validation | Rust validation CLI `aw-windows-telemetry.exe validate-deployment`; расширять до полной parity перед удалением `.ps1` | P0 started |
|
||||
| `rebuild-worktime-tasks.ps1` | rebuild scheduled tasks | Rust task reconciliation CLI | P1 |
|
||||
| `fix-session-watchers.ps1` | repair session watchers | Rust repair subcommand | P2 |
|
||||
| `cleanup-disc-sessions.ps1` | cleanup stale disconnected sessions | Rust maintenance subcommand | P2 |
|
||||
| `migrate-awatch-rus-paths.ps1` | path migration | Rust migration CLI with backup and dry-run | P1 |
|
||||
| `deploy-single-user.ps1` | single-user deploy | Rust/Ansible-backed installer action | P1 |
|
||||
| `deploy-domain-users.ps1` | domain deploy | Rust/Ansible-backed installer action | P1 |
|
||||
| `deploy-ensemble.ps1` | orchestrated deploy | Rust deploy coordinator or Ansible playbook wrapper | P1 |
|
||||
| `install-standalone-service.ps1` | local service install | Rust installer/bootstrap CLI | P0 |
|
||||
| `install-collector-guard-service.ps1` | guard service install | Rust installer/bootstrap CLI | P0 |
|
||||
| `install-dlp-client.ps1` | DLP client install | Rust installer/bootstrap CLI | P1 |
|
||||
| `audit-cryptopro.ps1` | CryptoPro audit | Rust audit CLI | P2 |
|
||||
| `run-user1-probe.ps1` | manual probe | Rust diagnostic probe | P2 |
|
||||
|
||||
## 4. Неприкосновенные ограничения
|
||||
|
||||
- Не делать big-bang replacement.
|
||||
- Не отключать PowerShell fallback до прохождения shadow/parity gates.
|
||||
- Не переносить функции, которые требуют скрытого сбора, keylogging, screen
|
||||
recording или content interception. Скриншоты допустимы только для DLP
|
||||
incident evidence, если это явно включено политикой.
|
||||
- Не менять бизнес-логику DLP, 1C, worktime или evidence при переносе.
|
||||
- Не менять thresholds, bucket names, event schema и ClickHouse schema без
|
||||
отдельного решения.
|
||||
- Не ломать install-kit и rollback ради удаления `.ps1`.
|
||||
- Не хранить live hostnames, private IP, tokens, passwords или runtime evidence
|
||||
paths в публичных tracked docs.
|
||||
|
||||
## 5. Общий контракт каждого Rust EXE
|
||||
|
||||
Каждый заменяющий EXE должен иметь:
|
||||
|
||||
- `--config <path>`;
|
||||
- `--json` для машинного вывода;
|
||||
- `--dry-run` для всех действий, меняющих состояние;
|
||||
- `--timeout-seconds`;
|
||||
- `--log-path` или structured logging в штатный каталог;
|
||||
- `--version`;
|
||||
- стабильные exit codes:
|
||||
- `0` - OK;
|
||||
- `1` - usage/config/runtime error;
|
||||
- `2` - check выполнен, но состояние WARN/FAIL;
|
||||
- `3` - action запрещен safety policy;
|
||||
- structured JSON event для audit trail;
|
||||
- lock/cooldown для guard/recovery/actions;
|
||||
- spool/queue для сетевых upload path;
|
||||
- atomic writes для state files;
|
||||
- no implicit sudo/admin: повышенные права должны быть видны в task/service
|
||||
definition;
|
||||
- rollback compatibility с текущими config keys.
|
||||
|
||||
## 6. Фазы миграции
|
||||
|
||||
### Phase 0. Baseline и реестр вызовов
|
||||
|
||||
Цель: зафиксировать, где PowerShell реально используется.
|
||||
|
||||
Действия:
|
||||
|
||||
1. Сканировать репозиторий:
|
||||
- `windows/*.ps1`;
|
||||
- `windows/installkit/innosetup/*.iss`;
|
||||
- `ansible/*.yml`;
|
||||
- `ansible/group_vars/*.yml`;
|
||||
- `scripts/*`;
|
||||
- docs/runbooks.
|
||||
2. Снять runtime baseline на тестовом Windows host:
|
||||
- Scheduled Tasks;
|
||||
- Windows Services;
|
||||
- текущие command lines процессов;
|
||||
- `deployment-config.json`;
|
||||
- свежесть AW buckets;
|
||||
- наличие DLP evidence и 1C upload.
|
||||
3. Для каждого скрипта зафиксировать:
|
||||
- входные параметры;
|
||||
- env/config keys;
|
||||
- side effects;
|
||||
- event schema;
|
||||
- log/state paths;
|
||||
- expected exit codes;
|
||||
- rollback path.
|
||||
|
||||
Выход фазы:
|
||||
|
||||
- `docs/POWERSHELL_TO_RUST_ROADMAP_RU.md` как базовая дорожная карта;
|
||||
- отдельный runtime inventory для live-контура в private/operator notes, без
|
||||
публикации секретов.
|
||||
|
||||
### Phase 1. Windows Rust foundation
|
||||
|
||||
Цель: подготовить общий Windows runtime вместо набора разрозненных EXE.
|
||||
|
||||
Действия:
|
||||
|
||||
1. Расширить существующие `awatch-agent-rs` и `aw-windows-telemetry` только в
|
||||
рамках parity, без новых функций.
|
||||
2. Вынести общие Windows helpers:
|
||||
- config loading;
|
||||
- ActivityWatch HTTP client;
|
||||
- evidence upload client;
|
||||
- Windows task/service inspection;
|
||||
- Event Log/EVTX access;
|
||||
- filesystem state/spool;
|
||||
- structured logs/audit.
|
||||
3. Зафиксировать единый Windows config contract:
|
||||
- `C:\ProgramData\AWatch-rus\deployment-config.json`;
|
||||
- `C:\Program Files\AWatch-rus\windows\*.exe`;
|
||||
- `C:\ProgramData\AWatch-rus\logs`;
|
||||
- `C:\ProgramData\AWatch-rus\spool`;
|
||||
- `C:\ProgramData\AWatch-rus\switch-backups`.
|
||||
|
||||
Gate:
|
||||
|
||||
```bash
|
||||
cd adk-rust
|
||||
cargo fmt --all -- --check
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
cargo build --release -p awatch-agent-rs -p aw-windows-telemetry
|
||||
```
|
||||
|
||||
### Phase 2. Закрепить уже выполненные замены
|
||||
|
||||
Цель: не переписать повторно то, что уже переведено, а довести до production
|
||||
definition of done.
|
||||
|
||||
Компоненты:
|
||||
|
||||
- `worktime-session-collector.ps1` -> `awatch-agent-rs`;
|
||||
- `export-upload-file-1c-telemetry.ps1` -> `aw-windows-telemetry.exe file1c-upload`;
|
||||
- `sync-dlp-evidence-artifacts.ps1` -> `aw-windows-telemetry.exe dlp-evidence-sync`;
|
||||
- `aw-collector-guard.ps1` -> `aw-windows-telemetry.exe collector-guard`
|
||||
через `AWatchRusCollectorGuard` service wrapper;
|
||||
- серверный writer -> `aw-1c-ingest-rust`.
|
||||
|
||||
Действия:
|
||||
|
||||
1. Проверить, что task/service actions запускают EXE, а не PowerShell.
|
||||
2. Проверить, что PowerShell fallback выключен там, где Rust уже стабилен.
|
||||
3. Проверить свежесть buckets/ClickHouse/evidence after restart.
|
||||
4. Обновить install-kit file list: Rust EXE являются primary artifact.
|
||||
5. Оставить legacy `.ps1` только как rollback на ограниченный период.
|
||||
|
||||
Gate:
|
||||
|
||||
- `ActivityWatch File1C Upload` запускает `aw-windows-telemetry.exe`;
|
||||
- `ActivityWatch DLP Evidence Sync` запускает `aw-windows-telemetry.exe`;
|
||||
- worktime events имеют `source=awatch-agent-rs`;
|
||||
- `AWatchRusCollectorGuard` запущен и его child process -
|
||||
`aw-windows-telemetry.exe collector-guard`;
|
||||
- guard не создает duplicate legacy browser/fileops/DLP endpoint collectors
|
||||
при stale bucket: при уже активных collectors launch tasks не запускаются
|
||||
повторно;
|
||||
- 1C ClickHouse ingest идет по `aw-1c-ingest-rust`;
|
||||
- PowerShell worktime process count = 0 в штатном режиме.
|
||||
- PowerShell `aw-collector-guard.ps1` process count = 0 в штатном режиме.
|
||||
|
||||
### Phase 3. Runtime collectors
|
||||
|
||||
Цель: заменить регулярный сбор данных на Windows.
|
||||
|
||||
Порядок:
|
||||
|
||||
1. `browser-domains-native-collector.ps1`
|
||||
- current status: live Rust primary through
|
||||
`aw-windows-telemetry.exe browser-domains-collector`;
|
||||
- ActivityWatch window/category health events are written with
|
||||
`source=aw-windows-telemetry-rust`;
|
||||
- UIAutomation URL/domain extraction implemented in Rust: browser process
|
||||
detection, normalized URL, host/rootDomain, default/custom category rules,
|
||||
`aw-watcher-web-*`, `aw-detmir-web-category_*` and web DLP incident
|
||||
schema preserve the legacy bucket/event contract;
|
||||
- live disconnected RDP sessions may legitimately report
|
||||
`browserDetected=false` and `urlDetected=false` until a browser is
|
||||
foreground in an interactive user session;
|
||||
- screenshots только для DLP incident evidence.
|
||||
2. `dlp-endpoint-signals-collector.ps1`
|
||||
- current status: live Rust primary through
|
||||
`aw-windows-telemetry.exe dlp-endpoint-collector`;
|
||||
- endpoint collector health is written with
|
||||
`source=aw-windows-telemetry-rust`;
|
||||
- clipboard metadata/hash/length, USB insert, print job metadata and DLP
|
||||
incident event semantics implemented in Rust with legacy fields:
|
||||
`requestedAction`, `enforcementMode`, `nativeChannelAction`,
|
||||
`enforcementSuppressed`, content pack matches and `enforced`;
|
||||
- destructive endpoint enforcement for USB write-block and print cancel is
|
||||
intentionally not enabled during pilot hardening: Rust emits equivalent
|
||||
audit/incident semantics and suppresses unsafe block actions unless a
|
||||
separate enforcement decision is made;
|
||||
- incident screenshot policy remains: screenshots only for DLP events.
|
||||
3. `file-operations-collector.ps1`
|
||||
- current status: live Rust primary through
|
||||
`aw-windows-telemetry.exe file-operations-collector`;
|
||||
- bounded filesystem watcher, operation schema, queue/spool and
|
||||
create/rename/delete smoke are verified;
|
||||
- per-session queue/state/log files are used, so RDP sessions do not share
|
||||
one state file;
|
||||
- legacy PowerShell remains installed only as rollback/reference.
|
||||
4. `email-outbound-collector.ps1`
|
||||
- metadata-only parity;
|
||||
- Outlook/SMTP mode behavior preserved;
|
||||
- no content interception beyond current documented behavior.
|
||||
5. `dlp-policy-client.ps1`
|
||||
- Rust policy fetch/cache;
|
||||
- strict validation;
|
||||
- safe fallback to last known good policy.
|
||||
|
||||
Shadow-mode:
|
||||
|
||||
- Rust collector writes `source=<rust-component>` and `mode=shadow`;
|
||||
- PowerShell remains primary during comparison;
|
||||
- compare event counts, schema, timestamps, severity, policy hits and evidence
|
||||
links for at least several collection cycles.
|
||||
|
||||
Gate:
|
||||
|
||||
- no duplicate management conclusions in portal;
|
||||
- event schema compatible with current AW/DLP consumers;
|
||||
- no stale buckets introduced;
|
||||
- no uncontrolled screenshot capture;
|
||||
- Rust collector survives AW API outage by spooling and later flushing.
|
||||
|
||||
### Phase 4. Guard, service wrapper и recovery
|
||||
|
||||
Цель: заменить PowerShell, который управляет процессами и восстановлением.
|
||||
|
||||
Компоненты:
|
||||
|
||||
- `aw-standalone-service.ps1`;
|
||||
- `aw-collector-guard.ps1`;
|
||||
- `hardening-recovery.ps1`;
|
||||
- `rebuild-worktime-tasks.ps1`;
|
||||
- `fix-session-watchers.ps1`;
|
||||
- `cleanup-disc-sessions.ps1`.
|
||||
|
||||
Действия:
|
||||
|
||||
1. Сделать Rust Windows service wrapper:
|
||||
- supervises configured collectors;
|
||||
- records child process state;
|
||||
- no hidden PowerShell spawn in normal mode.
|
||||
2. Сделать Rust guard/recovery:
|
||||
- allowlist actions only;
|
||||
- lock file;
|
||||
- cooldown;
|
||||
- dry-run by default for destructive or repair actions;
|
||||
- audit entry for every change.
|
||||
3. Перевести rebuild/fix/cleanup в subcommands одного maintenance EXE.
|
||||
|
||||
Gate:
|
||||
|
||||
- controlled restart of one collector works;
|
||||
- stale collector detection matches old guard;
|
||||
- recovery cannot restart arbitrary process/service;
|
||||
- rollback restores PowerShell guard/service within one operator action.
|
||||
|
||||
### Phase 5. Install/deploy/validate
|
||||
|
||||
Цель: install-kit перестает запускать PowerShell как штатный bootstrap.
|
||||
|
||||
Компоненты:
|
||||
|
||||
- `install-standalone-service.ps1`;
|
||||
- `install-collector-guard-service.ps1`;
|
||||
- `install-dlp-client.ps1`;
|
||||
- `deploy-single-user.ps1`;
|
||||
- `deploy-domain-users.ps1`;
|
||||
- `deploy-ensemble.ps1`;
|
||||
- `validate-deployment.ps1`;
|
||||
- `migrate-awatch-rus-paths.ps1`;
|
||||
- InnoSetup `AWatch-rus-InnoSetup.iss`;
|
||||
- Ansible Windows playbooks/group vars.
|
||||
|
||||
Действия:
|
||||
|
||||
1. Создать Rust bootstrap/installer EXE:
|
||||
- install/update service;
|
||||
- install/update scheduled tasks;
|
||||
- write config atomically;
|
||||
- backup previous config/tasks;
|
||||
- emit JSON validation report.
|
||||
2. Перевести validation в Rust:
|
||||
- task actions;
|
||||
- service status;
|
||||
- bucket freshness;
|
||||
- file permissions;
|
||||
- rust binary version matrix;
|
||||
- no PowerShell runtime task check.
|
||||
3. Обновить InnoSetup:
|
||||
- package `*.exe`;
|
||||
- run Rust bootstrap;
|
||||
- keep `.ps1` out of normal install payload after stabilization.
|
||||
4. Обновить Ansible:
|
||||
- deploy EXE;
|
||||
- configure task/service actions to EXE;
|
||||
- remove default `.ps1` paths from bot/env after migration.
|
||||
|
||||
Gate:
|
||||
|
||||
- clean install on test Windows host;
|
||||
- upgrade from PowerShell install to Rust install;
|
||||
- rollback to previous package;
|
||||
- `validate-deployment` JSON is consumed by CI/operator without parsing human
|
||||
text;
|
||||
- install-kit verification confirms no `powershell.exe` in primary install
|
||||
action.
|
||||
|
||||
### Phase 6. Ops-only хвост
|
||||
|
||||
Цель: убрать одноразовые PowerShell helpers.
|
||||
|
||||
Компоненты:
|
||||
|
||||
- `audit-cryptopro.ps1`;
|
||||
- `run-user1-probe.ps1`;
|
||||
- `scripts/powershell/detmir-powershell-profile.ps1`;
|
||||
- `.pssa_run.ps1`.
|
||||
|
||||
Решение:
|
||||
|
||||
- `audit-cryptopro.ps1` -> Rust audit CLI;
|
||||
- `run-user1-probe.ps1` -> Rust diagnostic probe or remove if obsolete;
|
||||
- operator PowerShell profile is not production runtime and can be retired
|
||||
after Rust/SSH operator commands exist;
|
||||
- `.pssa_run.ps1` removed when no product PowerShell remains.
|
||||
|
||||
Gate:
|
||||
|
||||
- no production task/service depends on these helpers;
|
||||
- docs no longer instruct operator to run PowerShell for routine checks;
|
||||
- one emergency manual path remains documented, but not packaged as runtime.
|
||||
|
||||
### Phase 7. Decommission
|
||||
|
||||
Цель: убрать PowerShell from product surface.
|
||||
|
||||
Действия:
|
||||
|
||||
1. Remove `.ps1` from install-kit file list.
|
||||
2. Remove `powershell.exe` installer run action.
|
||||
3. Remove default `.ps1` paths from Ansible/bot env.
|
||||
4. Update architecture/docs:
|
||||
- Windows collectors are Rust EXE;
|
||||
- PowerShell no longer prerequisite for runtime;
|
||||
- rollback history documented separately.
|
||||
5. Run tracked-file hygiene scan for public docs.
|
||||
|
||||
Gate:
|
||||
|
||||
```bash
|
||||
rg -n "powershell\\.exe|\\.ps1|PowerShell" windows ansible docs scripts README.md
|
||||
```
|
||||
|
||||
Expected result:
|
||||
|
||||
- only historical notes, explicit rollback docs, or non-runtime examples remain;
|
||||
- no install-kit primary action launches PowerShell;
|
||||
- no AWatch-rus Scheduled Task action launches PowerShell in live validation.
|
||||
|
||||
## 7. Rollout по хостам
|
||||
|
||||
Порядок раскатки:
|
||||
|
||||
1. Local build host:
|
||||
- build Windows EXE;
|
||||
- artifact check;
|
||||
- installer dry-run.
|
||||
2. Test Windows/RDP host:
|
||||
- install Rust EXE side-by-side;
|
||||
- shadow-mode collectors;
|
||||
- compare with PowerShell.
|
||||
3. Canary production Windows/RDP host:
|
||||
- one host, one business day;
|
||||
- monitor bucket freshness, DLP incidents, 1C upload, evidence upload.
|
||||
4. Remaining Windows hosts:
|
||||
- staged batches;
|
||||
- no more than one failure domain at a time.
|
||||
5. AW server / Proxmox / gateway:
|
||||
- update Ansible/bot references;
|
||||
- verify server Rust services remain active;
|
||||
- verify ClickHouse ingest and portal health.
|
||||
6. Install-kit:
|
||||
- publish Rust-first package;
|
||||
- keep previous package as rollback artifact.
|
||||
|
||||
## 8. Acceptance gates
|
||||
|
||||
Local gates:
|
||||
|
||||
```bash
|
||||
cd adk-rust
|
||||
cargo fmt --all -- --check
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
cargo build --workspace --release
|
||||
cargo build --release --target x86_64-pc-windows-gnu -p awatch-agent-rs -p aw-windows-telemetry
|
||||
```
|
||||
|
||||
Repository gates:
|
||||
|
||||
```bash
|
||||
scripts/check_detmir_rust_release_artifacts.sh
|
||||
scripts/verify_innosetup_installer.sh
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Windows gates:
|
||||
|
||||
```powershell
|
||||
Get-ScheduledTask | Where-Object {
|
||||
$_.TaskName -like 'ActivityWatch*' -or $_.TaskName -like 'AWatch*'
|
||||
} | Select-Object TaskName,TaskPath,State
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- task actions point to Rust EXE for migrated components;
|
||||
- no migrated component runs `powershell.exe`;
|
||||
- logs are fresh;
|
||||
- spool is empty or draining;
|
||||
- AW buckets are fresh;
|
||||
- DLP evidence sync only uploads DLP incident evidence;
|
||||
- 1C upload does not copy screenshots.
|
||||
|
||||
Server gates:
|
||||
|
||||
```bash
|
||||
detmir-status --json
|
||||
detmir-check --json
|
||||
systemctl --failed --no-pager
|
||||
systemctl list-timers aw-1c-ingest.timer --no-pager
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- DetMir severity is OK or explained WARN;
|
||||
- no failed Rust services;
|
||||
- ClickHouse writer timer is active;
|
||||
- portal data freshness is acceptable for pilot.
|
||||
|
||||
## 9. Rollback model
|
||||
|
||||
До decommission каждая миграция хранит rollback:
|
||||
|
||||
- previous EXE/script backup in `switch-backups`;
|
||||
- previous task/service definition;
|
||||
- previous `deployment-config.json`;
|
||||
- one-command switch back for canary host;
|
||||
- rollback reason written to audit log.
|
||||
|
||||
Rollback триггеры:
|
||||
|
||||
- AW bucket stale/dead after migration;
|
||||
- DLP event loss or uncontrolled duplicate events;
|
||||
- evidence upload fails repeatedly and spool grows;
|
||||
- 1C upload stops producing landing files;
|
||||
- service/guard restarts loop;
|
||||
- Windows host shows sustained CPU/RAM regression from new EXE;
|
||||
- user-visible pilot portal data quality degrades.
|
||||
|
||||
## 10. Риски
|
||||
|
||||
| Риск | Где | Снижение риска |
|
||||
|---|---|---|
|
||||
| Windows API differs from PowerShell cmdlets | DLP, tasks, Event Log, print, WMI | parity fixtures, canary, schema comparison |
|
||||
| UIAutomation/browser URL extraction changes behavior | browser collector | shadow-mode and domain count comparison |
|
||||
| Outlook/SMTP metadata behavior differs | email collector | metadata-only parity, explicit mode tests |
|
||||
| Privilege mismatch | services/tasks/recovery | install under same account, explicit elevation, validation |
|
||||
| AV/EDR blocks unsigned EXE | Windows hosts | code signing plan, allowlist, staged rollout |
|
||||
| Duplicate events during shadow | all collectors | shadow source tags, portal ignores shadow for KPI |
|
||||
| Rollback not fast enough | production canary | backup tasks/configs, single switch command |
|
||||
| Public docs leak live contour details | tracked docs | placeholders and `git grep` hygiene scan |
|
||||
|
||||
## 11. Минимальный порядок ближайших работ
|
||||
|
||||
1. Зафиксировать live inventory PowerShell usage на тестовом Windows/RDP host.
|
||||
2. Закрепить уже сделанные Rust paths:
|
||||
- worktime/RDP;
|
||||
- 1C file upload;
|
||||
- DLP evidence sync.
|
||||
3. Расширить `aw-windows-telemetry.exe validate-deployment` до полной parity с
|
||||
`validate-deployment.ps1`, потому что он станет главным gate для следующих
|
||||
замен.
|
||||
4. Перенести `aw-standalone-service.ps1` и `aw-collector-guard.ps1`, потому
|
||||
что они управляют runtime collectors.
|
||||
5. Стабилизировать P0 runtime collectors после live Rust-primary switch:
|
||||
- browser domains;
|
||||
- DLP endpoint signals;
|
||||
- file operations.
|
||||
URL/domain extraction и clipboard/USB/print incident semantics закрыты на
|
||||
уровне Rust code/schema/runtime self-test. Следующий шаг перед удалением
|
||||
legacy `.ps1` - burn-in, canary rollback test и live foreground-browser
|
||||
proof в интерактивной RDP-сессии.
|
||||
6. Перенести deploy/install scripts и InnoSetup primary action.
|
||||
7. Убрать PowerShell paths из Ansible/bot env.
|
||||
8. Удалить `.ps1` из install-kit и оставить только historical rollback docs.
|
||||
|
||||
## 12. Итоговая оценка
|
||||
|
||||
Миграция реалистична, потому что серверная часть уже Rust-first, а Windows
|
||||
контур уже имеет два рабочих Rust основания: `awatch-agent-rs` и
|
||||
`aw-windows-telemetry.exe`.
|
||||
|
||||
Критичный участок не сервер, а Windows runtime и install-kit:
|
||||
|
||||
- `dlp-endpoint-signals-collector.ps1`;
|
||||
- `browser-domains-native-collector.ps1`;
|
||||
- `aw-collector-guard.ps1`;
|
||||
- `aw-standalone-service.ps1`;
|
||||
- `validate-deployment.ps1`;
|
||||
- InnoSetup action that still launches PowerShell.
|
||||
|
||||
До пилота не нужно удалять весь PowerShell хвост. Для пилота достаточно
|
||||
закрепить уже работающие Rust-primary paths, не допустить копирования
|
||||
скриншотов в 1C контуре, оставить скриншоты только для DLP events, иметь
|
||||
понятный rollback и честно формулировать границу parity: code/schema/runtime
|
||||
path закрыт, а полный live URL/domain incident proof требует активного
|
||||
foreground browser в интерактивной RDP-сессии.
|
||||
Полное удаление PowerShell из install-kit лучше делать после canary и shadow
|
||||
parity по DLP/browser/file collectors.
|
||||
@@ -34,6 +34,120 @@ CLICKHOUSE_PASSWORD=
|
||||
`CLICKHOUSE_PASSWORD` используется только для HTTP Basic Auth и не выводится в
|
||||
JSON, markdown-отчеты или health-ответы.
|
||||
|
||||
## Текущий DetMir runtime
|
||||
|
||||
В рабочем контуре DetMir ClickHouse запущен не на AW-сервере `<AW_SERVER_HOST>`, а
|
||||
на gateway/Proxmox-хосте `<GATEWAY_HOST>` как Docker Compose service:
|
||||
|
||||
- каталог: `/opt/activitywatch/clickhouse-1c`;
|
||||
- контейнер: `aw-rus-1c-clickhouse`;
|
||||
- образ: `clickhouse/clickhouse-server:24.8`;
|
||||
- HTTP: `<GATEWAY_HOST>:8123`;
|
||||
- native: `<GATEWAY_HOST>:9000`;
|
||||
- база: `analytics_1c`;
|
||||
- credentials: `/opt/activitywatch/clickhouse-1c/.env` на `<GATEWAY_HOST>`.
|
||||
|
||||
Для `detmir-portal` или `detmir-check`, запущенных на AW-сервере
|
||||
`<AW_SERVER_HOST>`, используйте:
|
||||
|
||||
```env
|
||||
SECURITY_EVENTS_BACKEND=clickhouse
|
||||
CLICKHOUSE_URL=http://<GATEWAY_HOST>:8123
|
||||
CLICKHOUSE_DATABASE=analytics_1c
|
||||
CLICKHOUSE_USER=default
|
||||
CLICKHOUSE_PASSWORD=<из /opt/activitywatch/clickhouse-1c/.env на <GATEWAY_HOST>>
|
||||
```
|
||||
|
||||
Проверка с `<AW_SERVER_HOST>` без пароля может вернуть `AUTHENTICATION_FAILED`; это
|
||||
подтверждает сетевую доступность `<GATEWAY_HOST>:8123`, но не проверяет
|
||||
аутентификацию.
|
||||
|
||||
## Контроль состояния
|
||||
|
||||
Текущий контур контроля ClickHouse:
|
||||
|
||||
- Docker Compose healthcheck у контейнера `aw-rus-1c-clickhouse`. В норме
|
||||
`docker compose ps` показывает статус `(healthy)`.
|
||||
- `aw-1c-clickhouse-health.timer` на `<GATEWAY_HOST>`, запуск каждые 5 минут.
|
||||
Проверяет Docker state, Docker health, authenticated `SELECT 1` через
|
||||
`clickhouse-client`, HTTP `SELECT 1`, freshness таблиц и свободное место на
|
||||
volume ClickHouse.
|
||||
- `aw-clickhouse-network-health.timer` на AW-сервере `<AW_SERVER_HOST>`, запуск
|
||||
каждые 5 минут. Проверяет TCP-доступность `<GATEWAY_HOST>:8123/9000` и HTTP-ответ
|
||||
ClickHouse со стороны AW-rus сервера.
|
||||
- `aw-1c-ingest.timer` сохранен как writer в ClickHouse: цикл сбора и записи
|
||||
данных выполняется раз в 15 минут (`OnUnitActiveSec=15min`) через Rust-бинарник
|
||||
`/usr/local/bin/aw-1c-ingest-rust`.
|
||||
- Windows-задача `ActivityWatch File1C Upload` на RDP-хосте запускает
|
||||
Rust-бинарник `C:\Program Files\AWatch-rus\windows\aw-windows-telemetry.exe`
|
||||
в режиме `file1c-upload`; legacy PowerShell exporter оставлен только как
|
||||
fallback.
|
||||
- DLP-события обрабатываются не реже 15 минут: `activitywatch-dlp-aggregator`
|
||||
работает каждые 5 минут, `aw-dlp-influx-exporter` - каждые 10 минут,
|
||||
CEF/syslog/webhook forwarder'ы - каждые 2-5 минут, Windows
|
||||
`ActivityWatch DLP Evidence Sync` запускает тот же Rust-бинарник в режиме
|
||||
`dlp-evidence-sync` каждые 15 минут. Sync копирует только PNG, похожие на
|
||||
DLP incident screenshots (`web`, `clipboard`, `usb_insert`, `print_job` в
|
||||
имени файла), и игнорирует 1C/прочие PNG в `incident-artifacts`.
|
||||
- `aw-1c-proofcheck.timer` отдельно проверяет свежесть 1C-таблиц.
|
||||
|
||||
Операционные команды:
|
||||
|
||||
```bash
|
||||
ssh detmir_proxmox 'cd /opt/activitywatch/clickhouse-1c && sudo docker compose ps'
|
||||
ssh detmir_proxmox 'systemctl status aw-1c-clickhouse-health.timer aw-1c-clickhouse-health.service --no-pager'
|
||||
ssh detmir_proxmox 'sudo journalctl -u aw-1c-clickhouse-health.service -n 30 --no-pager'
|
||||
ssh detmir_aw 'systemctl status aw-clickhouse-network-health.timer aw-clickhouse-network-health.service --no-pager'
|
||||
ssh detmir_aw 'sudo journalctl -u aw-clickhouse-network-health.service -n 30 --no-pager'
|
||||
```
|
||||
|
||||
## Ручное изменение параметров Windows DLP evidence sync
|
||||
|
||||
Использовать, если нужно временно изменить период или API без полного redeploy.
|
||||
После ручного изменения желательно перенести значение в Ansible vars.
|
||||
|
||||
### Период DLP evidence sync
|
||||
|
||||
На Windows/RDP host в elevated PowerShell:
|
||||
|
||||
```powershell
|
||||
$taskName = "ActivityWatch DLP Evidence Sync"
|
||||
$minutes = 15
|
||||
|
||||
$task = Get-ScheduledTask -TaskName $taskName
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).Date) `
|
||||
-RepetitionInterval (New-TimeSpan -Minutes $minutes) `
|
||||
-RepetitionDuration (New-TimeSpan -Days 3650)
|
||||
|
||||
Set-ScheduledTask -TaskName $taskName `
|
||||
-Action $task.Actions `
|
||||
-Trigger $trigger `
|
||||
-Principal $task.Principal `
|
||||
-Settings $task.Settings
|
||||
```
|
||||
|
||||
### Evidence API / token / state / log paths
|
||||
|
||||
```powershell
|
||||
$exe = "C:\Program Files\AWatch-rus\windows\aw-windows-telemetry.exe"
|
||||
$args = 'dlp-evidence-sync --evidence-api-url "http://<AW_SERVER_HOST>:8721/api/dlp/evidence/upload" --token-path "C:\ProgramData\AWatch-rus\dlp-evidence-upload-token.txt" --state-path "C:\ProgramData\AWatch-rus\dlp-evidence-sync-state.json" --log-path "C:\ProgramData\AWatch-rus\logs\dlp-evidence-sync.log"'
|
||||
|
||||
$action = New-ScheduledTaskAction -Execute $exe -Argument $args
|
||||
Set-ScheduledTask -TaskName "ActivityWatch DLP Evidence Sync" -Action $action
|
||||
```
|
||||
|
||||
Проверить:
|
||||
|
||||
```powershell
|
||||
& "C:\Program Files\AWatch-rus\windows\aw-windows-telemetry.exe" dlp-evidence-sync `
|
||||
--evidence-api-url "http://<AW_SERVER_HOST>:8721/api/dlp/evidence/upload" `
|
||||
--token-path "C:\ProgramData\AWatch-rus\dlp-evidence-upload-token.txt" `
|
||||
--state-path "C:\ProgramData\AWatch-rus\dlp-evidence-sync-state.json" `
|
||||
--log-path "C:\ProgramData\AWatch-rus\logs\dlp-evidence-sync.log"
|
||||
|
||||
Get-ScheduledTaskInfo -TaskName "ActivityWatch DLP Evidence Sync"
|
||||
```
|
||||
|
||||
## Ожидаемые агрегаты
|
||||
|
||||
Портал формирует блок `security_events_summary`:
|
||||
|
||||
@@ -10,8 +10,7 @@ Agent: Rust
|
||||
Current Portal: Rust server-rendered HTML + HTMX
|
||||
Future Enterprise UI: React + TypeScript
|
||||
Future Desktop Forensics: Tauri + React + Rust core
|
||||
Dioxus: excluded from roadmap
|
||||
DPD Portal: excluded from roadmap
|
||||
Experimental Rust UI/prototype mirrors: excluded from roadmap
|
||||
```
|
||||
|
||||
## Правила
|
||||
@@ -22,7 +21,8 @@ DPD Portal: excluded from roadmap
|
||||
- Будущий React/Tauri UI не должен ломать текущий портал.
|
||||
- Agent и backend остаются Rust-first.
|
||||
- Новые UI-фреймворки не добавлять без отдельного architecture decision.
|
||||
- Dioxus и DPD Portal исключены из архитектурного roadmap проекта.
|
||||
- Экспериментальные Rust UI/prototype mirror направления не входят в
|
||||
архитектурный roadmap проекта.
|
||||
- Будущий React/Tauri UI не должен парсить HTML текущего портала как источник
|
||||
данных.
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
# Windows Rust validation
|
||||
|
||||
Дата: 2026-06-05
|
||||
|
||||
`aw-windows-telemetry.exe validate-deployment` является Rust-заменой первого
|
||||
уровня для `windows/validate-deployment.ps1`.
|
||||
|
||||
Цель команды: дать машинный JSON gate перед дальнейшей заменой Windows
|
||||
PowerShell runtime scripts на Rust EXE.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
C:\Program Files\AWatch-rus\windows\aw-windows-telemetry.exe `
|
||||
validate-deployment `
|
||||
--config-path C:\ProgramData\AWatch-rus\deployment-config.json
|
||||
```
|
||||
|
||||
Вывод: JSON.
|
||||
|
||||
Exit codes:
|
||||
|
||||
- `0` - validation прошла;
|
||||
- `1` - ошибка запуска, чтения config или runtime error;
|
||||
- `2` - validation выполнена, но есть failed sections.
|
||||
|
||||
## Что проверяется сейчас
|
||||
|
||||
- `deployment-config.json` читается как JSON, включая UTF-8 BOM.
|
||||
- `ActivityWatch File1C Upload` указывает на
|
||||
`aw-windows-telemetry.exe file1c-upload`.
|
||||
- `ActivityWatch DLP Evidence Sync` указывает на
|
||||
`aw-windows-telemetry.exe dlp-evidence-sync`.
|
||||
- `AWatch Rust Telemetry Agent` указывает на `awatch-agent-rs.exe`.
|
||||
- `AWatchRusCollectorGuard` service указывает на
|
||||
`aw-windows-telemetry.exe collector-guard` через service wrapper.
|
||||
- Rust collector guard получает `sessionId` через native Windows API,
|
||||
дедуплицирует legacy browser/fileops/DLP endpoint collectors по
|
||||
`(kind, sessionId)` и пропускает повторный запуск launch tasks, если legacy
|
||||
collectors уже работают.
|
||||
- Rust worktime agent запущен.
|
||||
- Rust collector guard запущен.
|
||||
- Worktime bucket свежий.
|
||||
- Локальные DLP/file operation queues не превышают безопасный depth.
|
||||
- `aw-windows-telemetry.exe browser-domains-collector`,
|
||||
`dlp-endpoint-collector` и `file-operations-collector` доступны как P0
|
||||
runtime subcommands.
|
||||
- Browser Rust collector реализует URL/domain/category parity:
|
||||
UIAutomation extraction, URL normalization, host/rootDomain, default/custom
|
||||
category rules, `aw-watcher-web-*`, `aw-detmir-web-category_*` и web DLP
|
||||
incident schema.
|
||||
- DLP endpoint Rust collector реализует incident semantics для
|
||||
`clipboard_change`, `usb_insert` и `print_job`: raw endpoint signal,
|
||||
policy evaluation, cooldown и `aw-dlp-incidents_*` event fields сохранены.
|
||||
USB write-block и print cancel не выполняются в пилотном режиме без
|
||||
отдельного enforcement решения.
|
||||
- В live-конфигурации `collectors.browserCollectorMode`,
|
||||
`collectors.dlpEndpointMode` и `collectors.fileOpsMode` переключены в
|
||||
`rust_primary`.
|
||||
- P0 PowerShell collector runtime
|
||||
(`browser-domains-native-collector.ps1`,
|
||||
`dlp-endpoint-signals-collector.ps1`, `file-operations-collector.ps1`)
|
||||
отсутствует в штатных пользовательских сессиях.
|
||||
- Rust collectors пишут per-session state/log/queue, чтобы разные RDP-сессии
|
||||
не конфликтовали за один state-файл.
|
||||
- Оставшийся PowerShell runtime классифицируется для миграционной карты через
|
||||
native WinAPI process snapshot. `wmic.exe` не требуется.
|
||||
|
||||
## Инвентаризация процессов
|
||||
|
||||
На новых Windows `wmic.exe` может отсутствовать. Validator не зависит от него:
|
||||
|
||||
1. сначала используется native WinAPI process snapshot;
|
||||
2. если native snapshot недоступен, используется `wmic.exe`;
|
||||
3. если `wmic.exe` отсутствует, используется `tasklist.exe` fallback.
|
||||
|
||||
В штатном режиме native snapshot дает:
|
||||
|
||||
- `processes.commandLineQueryOk=true`;
|
||||
- классификацию PowerShell runtime по видам:
|
||||
`browser`, `fileops`, `dlp_endpoint`, `guard`, `recovery`, `worktime`;
|
||||
- подтверждение отсутствия `worktime-session-collector.ps1`.
|
||||
- подтверждение отсутствия `aw-collector-guard.ps1`.
|
||||
|
||||
`tasklist.exe` fallback ограничен:
|
||||
|
||||
- наличие `awatch-agent-rs.exe` подтверждается;
|
||||
- command line PowerShell-процессов недоступна;
|
||||
- поле `processes.commandLineQueryOk=false`;
|
||||
- поле `processes.noPowerShellWorktimeRuntime=null`;
|
||||
- это не считается ошибкой, если task actions, Rust agent и bucket freshness
|
||||
подтверждены.
|
||||
|
||||
Для полного доказательства отсутствия конкретного `.ps1` процесса можно
|
||||
дополнительно использовать operator inventory через WinRM/Ansible, но это не
|
||||
должно блокировать Rust validation на системах без WMIC.
|
||||
|
||||
## Проверенный live checkpoint
|
||||
|
||||
2026-06-05 на Windows/RDP host:
|
||||
|
||||
- `overallOk=true`;
|
||||
- failed sections: `[]`;
|
||||
- File1C task: Rust EXE;
|
||||
- DLP evidence task: Rust EXE;
|
||||
- worktime agent: Rust EXE;
|
||||
- collector guard service: Rust EXE child;
|
||||
- worktime bucket age: меньше 300 секунд.
|
||||
- native process inventory: `commandLineQueryOk=true`;
|
||||
- PowerShell worktime runtime: отсутствует;
|
||||
- PowerShell collector guard runtime: отсутствует;
|
||||
- оставшийся PowerShell runtime классифицирован как `recovery/other`; P0
|
||||
browser/fileops/DLP endpoint PowerShell collectors отсутствуют.
|
||||
- Rust browser/DLP/fileops runtime после live switch: `browser=3`,
|
||||
`dlp_endpoint=3`, `fileops=3`, `guard=1`, legacy P0 count `0`.
|
||||
- После одного guard-cycle P0 Rust counts остались `3/3/3`, DLP state по трем
|
||||
сессиям: `status=ok`, `sendFailures=0`.
|
||||
- После parity update 2026-06-05T23:10Z:
|
||||
- `browser_rust=3`, `dlp_endpoint_rust=3`, `fileops_rust=3`,
|
||||
`guard_rust=1`, legacy P0 collector count `0`;
|
||||
- `validate-deployment`: `overallOk=true`, failed sections `[]`;
|
||||
- AW API после restart `activitywatch-server` отвечает локально за ~0.002s;
|
||||
- `aw-detmir-web-category_*` получает Rust `collector_health` с
|
||||
`foregroundProcess`, `browserDetected`, `urlDetected`;
|
||||
- текущие RDP-сеансы disconnected, поэтому live foreground пустой и
|
||||
`browserDetected=false`, `urlDetected=false`; URL event path проверен
|
||||
self-test/unit-test, но live URL требует активного foreground browser;
|
||||
- `aw-dlp-endpoint-signals_*` получает свежие Rust `self_test` events:
|
||||
`queueDepth=0`, `sendFailures=0`;
|
||||
- `detmir-dlp`: `ok=true`, `counts={ok:22,warn:0,fail:0}`;
|
||||
- `detmir-status`: `severity=OK`, `needs_heal=false`,
|
||||
`ok_for_operator=true`; актуальный refresh через Rust-first
|
||||
`/usr/local/bin/detmir-auto --no-heal --no-report` дал
|
||||
`service_warnings=0` и DLP `counts={ok:22,warn:0,fail:0}`.
|
||||
- Rust file operations live mode использует per-session queue/state/log;
|
||||
bounded shadow create/rename/delete smoke ранее подтвердил `Created`, один
|
||||
`Renamed` с `oldPath`, `Deleted` и штатные `collector_health` события.
|
||||
- Collector guard duplicate prevention smoke: при принудительном stale
|
||||
action `run-task` получил `applied=false`,
|
||||
`reason=legacy-collectors-already-running`; runtime counts остались
|
||||
`browser=3`, `fileops=3`, `dlp_endpoint=2`.
|
||||
|
||||
Ограничение: этот checkpoint подтверждает end-to-end live runtime, policy
|
||||
semantics и schema parity. Полный live URL/domain incident path физически
|
||||
требует активного foreground browser в интерактивной RDP-сессии; disconnected
|
||||
сеансы корректно дают только health/self-test. Удаление legacy `.ps1` остается
|
||||
отдельным rollback/decommission gate.
|
||||
|
||||
Live hostnames, private IP и runtime report сохранены только в private
|
||||
`.ops`-контуре.
|
||||
+15
-160
@@ -1,169 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
state_dir="${DETMIR_AI_STATE_DIR:-/var/lib/detmir-ai}"
|
||||
lock_dir="${DETMIR_AI_RUN_DIR:-${XDG_RUNTIME_DIR:-/tmp}}"
|
||||
auto_heal="${DETMIR_AUTO_HEAL:-1}"
|
||||
retain_days="${DETMIR_AI_RETAIN_DAYS:-14}"
|
||||
rust_bin="${DETMIR_AUTO_RUST_BIN:-/usr/local/bin/detmir-auto-rust}"
|
||||
legacy_bin="${DETMIR_AUTO_LEGACY_BIN:-/usr/local/bin/detmir-auto.legacy-shell}"
|
||||
env_file="${DETMIR_AUTO_ENV_FILE:-/etc/detmir/detmir-check.env}"
|
||||
|
||||
mkdir -p "$state_dir"/runs "$state_dir"/reports "$state_dir"/logs
|
||||
|
||||
lock_file="$lock_dir/detmir-auto.lock"
|
||||
exec 9>"$lock_file"
|
||||
if ! flock -n 9; then
|
||||
echo "detmir-auto: another run is active"
|
||||
exit 0
|
||||
if [[ -r "$env_file" ]]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$env_file"
|
||||
set +a
|
||||
fi
|
||||
|
||||
stamp="$(date -u +%Y%m%d-%H%M%S)"
|
||||
run_dir_full="$state_dir/runs/$stamp"
|
||||
mkdir -p "$run_dir_full"
|
||||
|
||||
check_file="$run_dir_full/detmir-check.json"
|
||||
dlp_file="$run_dir_full/detmir-dlp.json"
|
||||
heal_log="$run_dir_full/heal.log"
|
||||
bundle_file="$run_dir_full/bundle.txt"
|
||||
report_file="$state_dir/reports/detmir-report-$stamp.md"
|
||||
state_file="$state_dir/state-$stamp.json"
|
||||
|
||||
run_check() {
|
||||
local rc=0
|
||||
detmir-check --json >"$check_file" || rc=$?
|
||||
echo "$rc" >"$run_dir_full/check.rc"
|
||||
}
|
||||
|
||||
run_dlp() {
|
||||
local rc=0
|
||||
detmir-dlp >"$dlp_file" || rc=$?
|
||||
echo "$rc" >"$run_dir_full/dlp.rc"
|
||||
}
|
||||
|
||||
summarize() {
|
||||
python3 - "$check_file" "$dlp_file" "$run_dir_full/check.rc" "$run_dir_full/dlp.rc" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
check_path, dlp_path, check_rc_path, dlp_rc_path = map(Path, sys.argv[1:])
|
||||
check_rc = int(check_rc_path.read_text().strip())
|
||||
dlp_rc = int(dlp_rc_path.read_text().strip())
|
||||
summary = {
|
||||
"check_rc": check_rc,
|
||||
"dlp_rc": dlp_rc,
|
||||
"check_ok": False,
|
||||
"dlp_ok": False,
|
||||
"severity": "FAIL" if check_rc or dlp_rc else "OK",
|
||||
"needs_heal": bool(check_rc or dlp_rc),
|
||||
"reasons": [],
|
||||
}
|
||||
try:
|
||||
check = json.loads(check_path.read_text())
|
||||
summary["check_ok"] = bool(check.get("ok"))
|
||||
cs = check.get("summary") or {}
|
||||
summary["detmir_summary"] = cs
|
||||
if cs.get("bucket_dead", 0) or cs.get("bucket_stale", 0) or cs.get("service_failures", 0):
|
||||
summary["reasons"].append("detmir-check has stale/dead bucket or required service failure")
|
||||
except Exception as exc:
|
||||
summary["reasons"].append(f"detmir-check parse failed: {exc}")
|
||||
|
||||
try:
|
||||
dlp = json.loads(dlp_path.read_text())
|
||||
summary["dlp_ok"] = bool(dlp.get("ok"))
|
||||
summary["dlp_counts"] = dlp.get("counts")
|
||||
counts = dlp.get("counts") or {}
|
||||
if counts.get("fail", 0) or counts.get("warn", 0):
|
||||
summary["reasons"].append("dlp-health-check has warn/fail")
|
||||
except Exception as exc:
|
||||
summary["reasons"].append(f"detmir-dlp parse failed: {exc}")
|
||||
|
||||
if summary["check_ok"] and summary["dlp_ok"]:
|
||||
summary["severity"] = "OK"
|
||||
summary["needs_heal"] = False
|
||||
elif not summary["reasons"]:
|
||||
summary["severity"] = "WARN"
|
||||
else:
|
||||
summary["severity"] = "FAIL"
|
||||
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
}
|
||||
|
||||
run_check
|
||||
run_dlp
|
||||
summary_before="$(summarize)"
|
||||
printf '%s\n' "$summary_before" >"$run_dir_full/summary-before.json"
|
||||
|
||||
if [ "$auto_heal" = "1" ] && python3 -c 'import json,sys; print("yes" if json.load(sys.stdin).get("needs_heal") else "no")' <<<"$summary_before" | grep -qx yes; then
|
||||
{
|
||||
echo "detmir-heal-safe started at $(date -u --iso-8601=seconds)"
|
||||
detmir-heal-safe
|
||||
echo "detmir-heal-safe finished at $(date -u --iso-8601=seconds)"
|
||||
} >"$heal_log" 2>&1 || true
|
||||
sleep 10
|
||||
run_check
|
||||
run_dlp
|
||||
else
|
||||
echo "auto-heal skipped" >"$heal_log"
|
||||
if [[ -x "$rust_bin" ]]; then
|
||||
exec "$rust_bin" "$@"
|
||||
fi
|
||||
|
||||
summary_after="$(summarize)"
|
||||
printf '%s\n' "$summary_after" >"$state_file"
|
||||
if [[ -x "$legacy_bin" ]]; then
|
||||
exec "$legacy_bin" "$@"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "Ты операторский AI-помощник DetMir. По фактам ниже дай короткий русский отчет."
|
||||
echo "Структура ответа:"
|
||||
echo "1. Состояние: OK/WARN/FAIL"
|
||||
echo "2. Что важно"
|
||||
echo "3. Что уже сделал автомат"
|
||||
echo "4. Что сделать человеку, если нужно"
|
||||
echo
|
||||
echo "Правила:"
|
||||
echo "- Не предлагай рестарты, если факты чистые."
|
||||
echo "- Отличай event-driven bucket от dead/stale."
|
||||
echo "- DLP sendFailures важны только при новом sendFailuresDelta или warn/fail."
|
||||
echo "- Auto-heal умеет только серверные systemd-сервисы AW/DLP; Windows/RDP не трогает."
|
||||
echo
|
||||
echo "=== summary-before ==="
|
||||
cat "$run_dir_full/summary-before.json"
|
||||
echo
|
||||
echo "=== summary-after ==="
|
||||
cat "$state_file"
|
||||
echo
|
||||
echo "=== heal-log ==="
|
||||
sed -n '1,300p' "$heal_log"
|
||||
echo
|
||||
echo "=== detmir-check ==="
|
||||
sed -n '1,1600p' "$check_file"
|
||||
echo
|
||||
echo "=== detmir-dlp ==="
|
||||
sed -n '1,1600p' "$dlp_file"
|
||||
} >"$bundle_file"
|
||||
|
||||
{
|
||||
echo "# DetMir Autonomous Report"
|
||||
echo
|
||||
echo "- generated_at_utc: $(date -u --iso-8601=seconds)"
|
||||
echo "- run_dir: $run_dir_full"
|
||||
echo
|
||||
polli-chat --model text.daily --max-tokens 900 <"$bundle_file" || {
|
||||
echo "Pollinations report failed; raw summary follows."
|
||||
cat "$state_file"
|
||||
}
|
||||
} >"$report_file"
|
||||
|
||||
ln -sfn "$run_dir_full" "$state_dir/latest-run"
|
||||
ln -sfn "$report_file" "$state_dir/latest-report.md"
|
||||
ln -sfn "$state_file" "$state_dir/latest-state.json"
|
||||
|
||||
find "$state_dir/runs" -mindepth 1 -maxdepth 1 -type d -mtime +"$retain_days" -exec rm -rf {} +
|
||||
find "$state_dir/reports" -type f -name 'detmir-report-*.md' -mtime +"$retain_days" -delete
|
||||
find "$state_dir" -maxdepth 1 -type f -name 'state-*.json' -mtime +"$retain_days" -delete
|
||||
|
||||
cat "$report_file"
|
||||
|
||||
python3 - "$state_file" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
state = json.load(open(sys.argv[1]))
|
||||
raise SystemExit(0 if state.get("severity") == "OK" else 2)
|
||||
PY
|
||||
echo "detmir-auto: neither $rust_bin nor $legacy_bin is executable" >&2
|
||||
exit 127
|
||||
|
||||
@@ -60,6 +60,7 @@ required_bins=(
|
||||
aw-hayabusa-link-case-rust
|
||||
aw-hayabusa-from-windows-rust
|
||||
aw-hayabusa-autoprocess-rust
|
||||
aw-1c-ingest
|
||||
)
|
||||
|
||||
missing=0
|
||||
|
||||
@@ -165,9 +165,9 @@ async function main() {
|
||||
name: "loading_refresh_status_ready",
|
||||
ok:
|
||||
(await page.locator("#loadingStatus").count()) === 1
|
||||
&& (await page.locator("#loadingStatus").evaluate((node) => node.dataset.loadStatus)) === "READY"
|
||||
&& (await page.locator("#loadingStateText").innerText({ timeout })).includes("Данные готовы")
|
||||
&& (await page.locator("#loadingStageText").innerText({ timeout })).includes("Данные готовы")
|
||||
&& !(await page.locator("body").innerText({ timeout })).includes("Загрузка данных"),
|
||||
&& (await page.locator("#loadingStageText").innerText({ timeout })).includes("Данные готовы"),
|
||||
});
|
||||
smokeStep = "api:security_events_summary";
|
||||
const reportsPayload = await page.evaluate(async () => {
|
||||
@@ -339,8 +339,8 @@ async function main() {
|
||||
const requiredSettings = [
|
||||
"Период расчета",
|
||||
"Рабочий день",
|
||||
"Порог WARN",
|
||||
"Порог FAIL",
|
||||
"Порог внимания",
|
||||
"Порог критического риска",
|
||||
"Источник правил",
|
||||
"Дата последнего пересчета",
|
||||
];
|
||||
|
||||
@@ -23,22 +23,27 @@ namespace AWatchRus
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(options.LogPath));
|
||||
File.AppendAllText(options.LogPath, DateTime.Now.ToString("s") + " service starting" + Environment.NewLine);
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = options.PowerShellPath,
|
||||
Arguments = string.Format(
|
||||
var fileName = string.IsNullOrWhiteSpace(options.ExecPath) ? options.PowerShellPath : options.ExecPath;
|
||||
var arguments = string.IsNullOrWhiteSpace(options.ExecPath)
|
||||
? string.Format(
|
||||
"-NoProfile -ExecutionPolicy Bypass -File \"{0}\" -ConfigPath \"{1}\" -Mode {2} -LoopSeconds {3}",
|
||||
options.ScriptPath,
|
||||
options.ConfigPath,
|
||||
options.Mode,
|
||||
options.LoopSeconds),
|
||||
options.LoopSeconds)
|
||||
: options.ExecArgs;
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = arguments,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = false,
|
||||
RedirectStandardError = false,
|
||||
};
|
||||
child = Process.Start(psi);
|
||||
File.AppendAllText(options.LogPath, DateTime.Now.ToString("s") + " child pid=" + child.Id + Environment.NewLine);
|
||||
File.AppendAllText(options.LogPath, DateTime.Now.ToString("s") + " child pid=" + child.Id + " exec=" + fileName + Environment.NewLine);
|
||||
}
|
||||
|
||||
protected override void OnStop()
|
||||
@@ -84,6 +89,8 @@ namespace AWatchRus
|
||||
public string Mode = "shadow";
|
||||
public int LoopSeconds = 60;
|
||||
public string LogPath = @"C:\ProgramData\AWatch-rus\logs\collector-guard-service.log";
|
||||
public string ExecPath = null;
|
||||
public string ExecArgs = null;
|
||||
}
|
||||
|
||||
internal static class Program
|
||||
@@ -115,6 +122,8 @@ namespace AWatchRus
|
||||
if (int.TryParse(value, out parsed)) options.LoopSeconds = parsed;
|
||||
}
|
||||
else if (key == "--log") options.LogPath = value;
|
||||
else if (key == "--exec") options.ExecPath = value;
|
||||
else if (key == "--args") options.ExecArgs = value;
|
||||
i++;
|
||||
}
|
||||
return options;
|
||||
|
||||
@@ -931,7 +931,9 @@ function New-ActivityWatchDeploymentConfig {
|
||||
[string]$HayabusaAutoUploadTaskName = 'ActivityWatch Hayabusa Upload',
|
||||
[bool]$File1CAutoUploadEnabled = $true,
|
||||
[int]$File1CAutoUploadIntervalHours = 6,
|
||||
[int]$File1CAutoUploadIntervalMinutes = 15,
|
||||
[string]$File1CAutoUploadTaskName = 'ActivityWatch File1C Upload',
|
||||
[string]$File1CAutoUploadRunAsUser,
|
||||
[string]$File1CTargetHost,
|
||||
[string]$File1CTargetUser = 'igor',
|
||||
[string]$File1CRemoteRoot = '/opt/activitywatch/clickhouse-1c/landing',
|
||||
@@ -981,6 +983,7 @@ function New-ActivityWatchDeploymentConfig {
|
||||
evtxExportScript = $EvtxExportScript
|
||||
hayabusaUploadScript = $HayabusaUploadScript
|
||||
file1cTelemetryScript = $File1CTelemetryScript
|
||||
file1cTelemetryExecutable = if ([string]::IsNullOrWhiteSpace($File1CTelemetryScript)) { '' } else { Join-Path (Split-Path -Parent $File1CTelemetryScript) 'aw-windows-telemetry.exe' }
|
||||
rulesPath = $RulesPath
|
||||
policyPath = $PolicyPath
|
||||
launchScript = $LaunchScriptPath
|
||||
@@ -1022,8 +1025,10 @@ function New-ActivityWatchDeploymentConfig {
|
||||
analytics = [pscustomobject]@{
|
||||
file1cAutomation = [pscustomobject]@{
|
||||
enabled = [bool]$File1CAutoUploadEnabled
|
||||
intervalMinutes = $File1CAutoUploadIntervalMinutes
|
||||
intervalHours = $File1CAutoUploadIntervalHours
|
||||
taskName = $File1CAutoUploadTaskName
|
||||
runAsUser = $File1CAutoUploadRunAsUser
|
||||
targetHost = $File1CTargetHost
|
||||
targetUser = $File1CTargetUser
|
||||
remoteRoot = $File1CRemoteRoot
|
||||
@@ -1412,13 +1417,59 @@ function Start-CollectorScriptIfNeeded {
|
||||
Start-Process -FilePath `$PowerShellExe -ArgumentList `$argumentList -WindowStyle Hidden
|
||||
}
|
||||
|
||||
function Test-RustCollectorRunning {
|
||||
param(
|
||||
[string]`$Subcommand,
|
||||
[int]`$SessionId
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace(`$Subcommand)) {
|
||||
return `$false
|
||||
}
|
||||
|
||||
return [bool]@(
|
||||
Get-CimInstance Win32_Process -Filter "Name = 'aw-windows-telemetry.exe'" -ErrorAction SilentlyContinue |
|
||||
Where-Object {
|
||||
`$_.SessionId -eq `$SessionId -and
|
||||
`$_.CommandLine -match [Regex]::Escape(`$Subcommand)
|
||||
} |
|
||||
Select-Object -First 1
|
||||
).Count
|
||||
}
|
||||
|
||||
function Start-RustCollectorIfNeeded {
|
||||
param(
|
||||
[string]`$ExePath,
|
||||
[string]`$Subcommand,
|
||||
[string]`$ConfigPath,
|
||||
[int]`$SessionId
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace(`$ExePath) -or [string]::IsNullOrWhiteSpace(`$Subcommand)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath `$ExePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Test-RustCollectorRunning -Subcommand `$Subcommand -SessionId `$SessionId) {
|
||||
return
|
||||
}
|
||||
|
||||
`$argumentList = @(`$Subcommand, '--config-path', `$ConfigPath, '--mode', 'enforce')
|
||||
Start-Process -FilePath `$ExePath -ArgumentList `$argumentList -WindowStyle Hidden
|
||||
}
|
||||
|
||||
`$config = Get-DeploymentConfig -Path `$ConfigPath
|
||||
`$sessionId = (Get-Process -Id `$PID).SessionId
|
||||
`$installRoot = [string]`$config.paths.installRoot
|
||||
`$stateRoot = [string]`$config.paths.stateRoot
|
||||
`$deployRoot = if (`$config.paths.PSObject.Properties.Name -contains 'deployRoot' -and -not [string]::IsNullOrWhiteSpace([string]`$config.paths.deployRoot)) { [string]`$config.paths.deployRoot } elseif (`$config.paths.PSObject.Properties.Name -contains 'toolkitRoot' -and -not [string]::IsNullOrWhiteSpace([string]`$config.paths.toolkitRoot)) { [string]`$config.paths.toolkitRoot } else { `$installRoot }
|
||||
`$script:ApiBase = '{0}://{1}:{2}/api/0' -f [string]`$config.server.scheme, [string]`$config.server.host, [string]`$config.server.port
|
||||
`$script:Hostname = if (`$config.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]`$config.awHostname)) { [string]`$config.awHostname } else { `$env:COMPUTERNAME }
|
||||
`$script:KnownBuckets = @{}
|
||||
`$telemetryExe = if (`$config.paths.PSObject.Properties.Name -contains 'file1cTelemetryExecutable' -and -not [string]::IsNullOrWhiteSpace([string]`$config.paths.file1cTelemetryExecutable)) { [string]`$config.paths.file1cTelemetryExecutable } else { Join-Path `$deployRoot 'windows\aw-windows-telemetry.exe' }
|
||||
`$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' }
|
||||
@@ -1430,6 +1481,9 @@ function Start-CollectorScriptIfNeeded {
|
||||
`$afkEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]`$config.collectors.afkEnabled } else { `$true }
|
||||
`$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true }
|
||||
`$fileOpsEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]`$config.collectors.fileOpsEnabled } else { `$true }
|
||||
`$browserCollectorMode = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'browserCollectorMode') { [string]`$config.collectors.browserCollectorMode } else { 'powershell_primary' }
|
||||
`$dlpEndpointMode = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'dlpEndpointMode') { [string]`$config.collectors.dlpEndpointMode } else { 'powershell_primary' }
|
||||
`$fileOpsMode = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'fileOpsMode') { [string]`$config.collectors.fileOpsMode } else { 'powershell_primary' }
|
||||
`$emailEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'emailEnabled') { [bool]`$config.collectors.emailEnabled } else { `$false }
|
||||
`$emailCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'emailCollectorScript') { [string]`$config.paths.emailCollectorScript } else { Join-Path `$stateRoot 'email-outbound-collector.ps1' }
|
||||
`$launchLockPath = New-LaunchLock -StateRoot `$stateRoot -SessionId `$sessionId
|
||||
@@ -1459,10 +1513,22 @@ try {
|
||||
}
|
||||
catch {
|
||||
}
|
||||
Start-CollectorScriptIfNeeded -ScriptPath `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
|
||||
Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
|
||||
if (`$browserCollectorMode -ieq 'rust_primary') {
|
||||
Start-RustCollectorIfNeeded -ExePath `$telemetryExe -Subcommand 'browser-domains-collector' -ConfigPath `$ConfigPath -SessionId `$sessionId
|
||||
} else {
|
||||
Start-CollectorScriptIfNeeded -ScriptPath `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
|
||||
}
|
||||
if (`$dlpEndpointMode -ieq 'rust_primary') {
|
||||
Start-RustCollectorIfNeeded -ExePath `$telemetryExe -Subcommand 'dlp-endpoint-collector' -ConfigPath `$ConfigPath -SessionId `$sessionId
|
||||
} else {
|
||||
Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
|
||||
}
|
||||
if (`$fileOpsEnabled) {
|
||||
Start-CollectorScriptIfNeeded -ScriptPath `$fileCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
|
||||
if (`$fileOpsMode -ieq 'rust_primary') {
|
||||
Start-RustCollectorIfNeeded -ExePath `$telemetryExe -Subcommand 'file-operations-collector' -ConfigPath `$ConfigPath -SessionId `$sessionId
|
||||
} else {
|
||||
Start-CollectorScriptIfNeeded -ScriptPath `$fileCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
|
||||
}
|
||||
}
|
||||
if (`$emailEnabled -and (Test-Path -LiteralPath `$emailCollectorScript)) {
|
||||
Start-CollectorScriptIfNeeded -ScriptPath `$emailCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
|
||||
@@ -2473,13 +2539,22 @@ function Register-ActivityWatchFile1CAutoUploadTask {
|
||||
}
|
||||
|
||||
$uploadScript = if ($config.paths.PSObject.Properties.Name -contains 'file1cTelemetryScript') { [string]$config.paths.file1cTelemetryScript } else { Join-Path $config.paths.stateRoot 'export-upload-file-1c-telemetry.ps1' }
|
||||
if (-not (Test-Path -LiteralPath $uploadScript)) {
|
||||
throw "Не найден скрипт file-1C telemetry upload: $uploadScript"
|
||||
$uploadExeCandidates = New-Object System.Collections.Generic.List[string]
|
||||
if ($config.paths.PSObject.Properties.Name -contains 'file1cTelemetryExecutable' -and -not [string]::IsNullOrWhiteSpace([string]$config.paths.file1cTelemetryExecutable)) {
|
||||
$uploadExeCandidates.Add([string]$config.paths.file1cTelemetryExecutable)
|
||||
}
|
||||
$uploadExeCandidates.Add((Join-Path (Join-Path ([System.Environment]::GetFolderPath('ProgramFiles')) 'AWatch-rus\windows') 'aw-windows-telemetry.exe'))
|
||||
$uploadExeCandidates.Add((Join-Path $config.paths.stateRoot 'aw-windows-telemetry.exe'))
|
||||
$uploadExe = @($uploadExeCandidates | Where-Object { -not [string]::IsNullOrWhiteSpace($_) -and (Test-Path -LiteralPath $_) } | Select-Object -First 1) | Select-Object -First 1
|
||||
if ([string]::IsNullOrWhiteSpace($uploadExe) -and -not (Test-Path -LiteralPath $uploadScript)) {
|
||||
throw "Не найден Rust binary или legacy script file-1C telemetry upload: $uploadExeCandidates / $uploadScript"
|
||||
}
|
||||
|
||||
$intervalHours = [Math]::Max(1, [int]$automation.intervalHours)
|
||||
$powerShellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
||||
$taskCommand = "`"$powerShellExe`" -NoProfile -ExecutionPolicy Bypass -File `"$uploadScript`" -ConfigPath `"$ConfigPath`""
|
||||
$intervalMinutes = if ($automation.PSObject.Properties.Name -contains 'intervalMinutes') {
|
||||
[Math]::Max(1, [int]$automation.intervalMinutes)
|
||||
} else {
|
||||
[Math]::Max(1, [int]$automation.intervalHours) * 60
|
||||
}
|
||||
$runnerUserId = $null
|
||||
if ($automation.PSObject.Properties.Name -contains 'runAsUser' -and -not [string]::IsNullOrWhiteSpace([string]$automation.runAsUser)) {
|
||||
$runnerUserId = [string]$automation.runAsUser
|
||||
@@ -2495,16 +2570,23 @@ function Register-ActivityWatchFile1CAutoUploadTask {
|
||||
$runnerUserId = @($config.userTasks | ForEach-Object { [string]$_.userId } | Select-Object -First 1) | Select-Object -First 1
|
||||
}
|
||||
|
||||
Remove-ActivityWatchScheduledTask -TaskName $taskName
|
||||
if (-not [string]::IsNullOrWhiteSpace($runnerUserId)) {
|
||||
& schtasks.exe /Create /TN $taskName /TR $taskCommand /SC HOURLY /MO $intervalHours /ST 00:00 /RU $runnerUserId /RL HIGHEST /F | Out-Null
|
||||
if (-not [string]::IsNullOrWhiteSpace($uploadExe)) {
|
||||
$action = New-ScheduledTaskAction -Execute $uploadExe -Argument "file1c-upload --config-path `"$ConfigPath`""
|
||||
}
|
||||
else {
|
||||
& schtasks.exe /Create /TN $taskName /TR $taskCommand /SC HOURLY /MO $intervalHours /ST 00:00 /RU SYSTEM /RL HIGHEST /F | Out-Null
|
||||
$powerShellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
||||
$action = New-ScheduledTaskAction -Execute $powerShellExe -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$uploadScript`" -ConfigPath `"$ConfigPath`""
|
||||
}
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Не удалось создать scheduled task $taskName через schtasks.exe"
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).Date) -RepetitionInterval (New-TimeSpan -Minutes $intervalMinutes) -RepetitionDuration (New-TimeSpan -Days 3650)
|
||||
if (-not [string]::IsNullOrWhiteSpace($runnerUserId)) {
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $runnerUserId -LogonType Interactive -RunLevel Highest
|
||||
}
|
||||
else {
|
||||
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
||||
}
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 10)
|
||||
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
|
||||
}
|
||||
|
||||
function Set-ActivityWatchAcl {
|
||||
|
||||
@@ -47,7 +47,9 @@ param(
|
||||
[string]$HayabusaAutoUploadTaskName = 'ActivityWatch Hayabusa Upload',
|
||||
[bool]$File1CAutoUploadEnabled = $true,
|
||||
[int]$File1CAutoUploadIntervalHours = 6,
|
||||
[int]$File1CAutoUploadIntervalMinutes = 15,
|
||||
[string]$File1CAutoUploadTaskName = 'ActivityWatch File1C Upload',
|
||||
[string]$File1CAutoUploadRunAsUser,
|
||||
[string]$File1CTargetHost,
|
||||
[string]$File1CTargetUser = 'igor',
|
||||
[string]$File1CRegistryWorkbookPath = 'E:\USER1\СПИСОК ПРЕДПРИЯТИЙ И ИХ РАСПРЕДЕЛЕНИЕ.xlsx',
|
||||
@@ -157,7 +159,9 @@ $config = New-ActivityWatchDeploymentConfig `
|
||||
-HayabusaAutoUploadTaskName $HayabusaAutoUploadTaskName `
|
||||
-File1CAutoUploadEnabled $File1CAutoUploadEnabled `
|
||||
-File1CAutoUploadIntervalHours $File1CAutoUploadIntervalHours `
|
||||
-File1CAutoUploadIntervalMinutes $File1CAutoUploadIntervalMinutes `
|
||||
-File1CAutoUploadTaskName $File1CAutoUploadTaskName `
|
||||
-File1CAutoUploadRunAsUser $File1CAutoUploadRunAsUser `
|
||||
-File1CTargetHost $File1CTargetHost `
|
||||
-File1CTargetUser $File1CTargetUser `
|
||||
-File1CRegistryWorkbookPath $File1CRegistryWorkbookPath `
|
||||
|
||||
@@ -48,7 +48,9 @@ param(
|
||||
[string]$HayabusaAutoUploadTaskName = 'ActivityWatch Hayabusa Upload',
|
||||
[bool]$File1CAutoUploadEnabled = $true,
|
||||
[int]$File1CAutoUploadIntervalHours = 6,
|
||||
[int]$File1CAutoUploadIntervalMinutes = 15,
|
||||
[string]$File1CAutoUploadTaskName = 'ActivityWatch File1C Upload',
|
||||
[string]$File1CAutoUploadRunAsUser,
|
||||
[string]$File1CTargetHost,
|
||||
[string]$File1CTargetUser = 'igor',
|
||||
[string]$File1CRegistryWorkbookPath = 'E:\USER1\СПИСОК ПРЕДПРИЯТИЙ И ИХ РАСПРЕДЕЛЕНИЕ.xlsx',
|
||||
@@ -118,7 +120,9 @@ if (-not (Test-Path -LiteralPath $deployScript)) {
|
||||
-HayabusaAutoUploadTaskName $HayabusaAutoUploadTaskName `
|
||||
-File1CAutoUploadEnabled $File1CAutoUploadEnabled `
|
||||
-File1CAutoUploadIntervalHours $File1CAutoUploadIntervalHours `
|
||||
-File1CAutoUploadIntervalMinutes $File1CAutoUploadIntervalMinutes `
|
||||
-File1CAutoUploadTaskName $File1CAutoUploadTaskName `
|
||||
-File1CAutoUploadRunAsUser $File1CAutoUploadRunAsUser `
|
||||
-File1CTargetHost $File1CTargetHost `
|
||||
-File1CTargetUser $File1CTargetUser `
|
||||
-File1CRegistryWorkbookPath $File1CRegistryWorkbookPath `
|
||||
|
||||
@@ -111,7 +111,9 @@ $effectiveHayabusaAutoUploadMode = if ($existingConfig -and $existingConfig.PSOb
|
||||
$effectiveHayabusaAutoUploadTaskName = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'hayabusaAutomation' -and $existingConfig.forensics.hayabusaAutomation.PSObject.Properties.Name -contains 'taskName') { [string]$existingConfig.forensics.hayabusaAutomation.taskName } else { 'ActivityWatch Hayabusa Upload' }
|
||||
$effectiveFile1CAutoUploadEnabled = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'analytics' -and $existingConfig.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $existingConfig.analytics.file1cAutomation.PSObject.Properties.Name -contains 'enabled') { [bool]$existingConfig.analytics.file1cAutomation.enabled } else { $true }
|
||||
$effectiveFile1CAutoUploadIntervalHours = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'analytics' -and $existingConfig.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $existingConfig.analytics.file1cAutomation.PSObject.Properties.Name -contains 'intervalHours') { [int]$existingConfig.analytics.file1cAutomation.intervalHours } else { 6 }
|
||||
$effectiveFile1CAutoUploadIntervalMinutes = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'analytics' -and $existingConfig.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $existingConfig.analytics.file1cAutomation.PSObject.Properties.Name -contains 'intervalMinutes') { [int]$existingConfig.analytics.file1cAutomation.intervalMinutes } else { [Math]::Max(1, $effectiveFile1CAutoUploadIntervalHours) * 60 }
|
||||
$effectiveFile1CAutoUploadTaskName = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'analytics' -and $existingConfig.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $existingConfig.analytics.file1cAutomation.PSObject.Properties.Name -contains 'taskName') { [string]$existingConfig.analytics.file1cAutomation.taskName } else { 'ActivityWatch File1C Upload' }
|
||||
$effectiveFile1CAutoUploadRunAsUser = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'analytics' -and $existingConfig.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $existingConfig.analytics.file1cAutomation.PSObject.Properties.Name -contains 'runAsUser') { [string]$existingConfig.analytics.file1cAutomation.runAsUser } else { '' }
|
||||
$effectiveFile1CTargetHost = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'analytics' -and $existingConfig.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $existingConfig.analytics.file1cAutomation.PSObject.Properties.Name -contains 'targetHost') { [string]$existingConfig.analytics.file1cAutomation.targetHost } else { '' }
|
||||
$effectiveFile1CTargetUser = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'analytics' -and $existingConfig.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $existingConfig.analytics.file1cAutomation.PSObject.Properties.Name -contains 'targetUser') { [string]$existingConfig.analytics.file1cAutomation.targetUser } else { 'igor' }
|
||||
|
||||
@@ -220,7 +222,9 @@ $config = New-ActivityWatchDeploymentConfig `
|
||||
-HayabusaAutoUploadTaskName $effectiveHayabusaAutoUploadTaskName `
|
||||
-File1CAutoUploadEnabled $effectiveFile1CAutoUploadEnabled `
|
||||
-File1CAutoUploadIntervalHours $effectiveFile1CAutoUploadIntervalHours `
|
||||
-File1CAutoUploadIntervalMinutes $effectiveFile1CAutoUploadIntervalMinutes `
|
||||
-File1CAutoUploadTaskName $effectiveFile1CAutoUploadTaskName `
|
||||
-File1CAutoUploadRunAsUser $effectiveFile1CAutoUploadRunAsUser `
|
||||
-File1CTargetHost $effectiveFile1CTargetHost `
|
||||
-File1CTargetUser $effectiveFile1CTargetUser `
|
||||
-LaunchScriptPath $effectiveLaunchScript `
|
||||
|
||||
@@ -22,6 +22,7 @@ function Assert-Admin {
|
||||
Assert-Admin
|
||||
|
||||
$guardScriptPath = Join-Path $PSScriptRoot 'aw-collector-guard.ps1'
|
||||
$rustTelemetryPath = Join-Path $PSScriptRoot 'aw-windows-telemetry.exe'
|
||||
$serviceSourcePath = Join-Path $PSScriptRoot 'AWatchRusCollectorGuardService.cs'
|
||||
$serviceExePath = Join-Path $PSScriptRoot 'AWatchRusCollectorGuardService.exe'
|
||||
if (-not (Test-Path -LiteralPath $guardScriptPath)) {
|
||||
@@ -64,7 +65,13 @@ if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $serviceExePath)) {
|
||||
|
||||
$logsRoot = Join-Path (Split-Path -Path $ConfigPath -Parent) 'logs'
|
||||
$serviceLogPath = Join-Path $logsRoot 'collector-guard-service.log'
|
||||
$binPath = "`"$serviceExePath`" --service-name `"$ServiceName`" --script `"$guardScriptPath`" --config `"$ConfigPath`" --mode $Mode --loop $LoopSeconds --log `"$serviceLogPath`""
|
||||
if (Test-Path -LiteralPath $rustTelemetryPath) {
|
||||
$rustArgs = "collector-guard --config-path `"$ConfigPath`" --mode $Mode --loop-seconds $LoopSeconds"
|
||||
$binPath = "`"$serviceExePath`" --service-name `"$ServiceName`" --exec `"$rustTelemetryPath`" --args `"$rustArgs`" --log `"$serviceLogPath`""
|
||||
}
|
||||
else {
|
||||
$binPath = "`"$serviceExePath`" --service-name `"$ServiceName`" --script `"$guardScriptPath`" --config `"$ConfigPath`" --mode $Mode --loop $LoopSeconds --log `"$serviceLogPath`""
|
||||
}
|
||||
|
||||
New-Service -Name $ServiceName -BinaryPathName $binPath -DisplayName 'AWatch-rus Collector Guard' -StartupType Automatic | Out-Null
|
||||
sc.exe description $ServiceName "Session-aware ActivityWatch collector guard for AWatch-rus" | Out-Null
|
||||
@@ -86,3 +93,4 @@ sc.exe start $ServiceName | Out-Null
|
||||
Write-Output "Collector guard service installed: $ServiceName"
|
||||
Write-Output "Mode: $Mode"
|
||||
Write-Output "Config: $ConfigPath"
|
||||
Write-Output "Runtime: $(if (Test-Path -LiteralPath $rustTelemetryPath) { 'rust' } else { 'powershell' })"
|
||||
|
||||
Reference in New Issue
Block a user