From 9c725394015d4b94682000790595dd342400800f Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Sat, 6 Jun 2026 21:33:31 +0300 Subject: [PATCH] docs+portal: add pilot v1 demo pack --- README.md | 23 ++ adk-rust/crates/detmir-portal/src/main.rs | 23 ++ .../crates/detmir-portal/src/static/app.css | 36 ++- .../crates/detmir-portal/src/static/app.js | 11 +- .../detmir-portal/src/static/index.html | 8 + docs/DEMO_RUNBOOK_RU.md | 3 + docs/PILOT_DEMO_SCENARIO_RU.md | 190 ++++++++++++++ docs/PILOT_V1_EVIDENCE_RU.md | 9 + docs/fixtures/pilot-v1-demo/README_RU.md | 25 ++ .../pilot-v1-demo/demo-seed-data.json | 223 ++++++++++++++++ .../evidence-pack/executive-summary.md | 25 ++ .../evidence-pack/investigation-contract.json | 53 ++++ .../evidence-pack/investigation-report.md | 30 +++ .../security-technical-summary.md | 40 +++ scripts/detmir-pilot-demo-smoke.mjs | 244 ++++++++++++++++++ 15 files changed, 929 insertions(+), 14 deletions(-) create mode 100644 docs/PILOT_DEMO_SCENARIO_RU.md create mode 100644 docs/fixtures/pilot-v1-demo/README_RU.md create mode 100644 docs/fixtures/pilot-v1-demo/demo-seed-data.json create mode 100644 docs/fixtures/pilot-v1-demo/evidence-pack/executive-summary.md create mode 100644 docs/fixtures/pilot-v1-demo/evidence-pack/investigation-contract.json create mode 100644 docs/fixtures/pilot-v1-demo/evidence-pack/investigation-report.md create mode 100644 docs/fixtures/pilot-v1-demo/evidence-pack/security-technical-summary.md create mode 100644 scripts/detmir-pilot-demo-smoke.mjs diff --git a/README.md b/README.md index e2849c1..5c42522 100755 --- a/README.md +++ b/README.md @@ -73,6 +73,28 @@ Future: - React/TypeScript Enterprise UI. - Tauri Desktop Forensics. +## Pilot v1 demo + +Pilot v1 demo показывает AWatch-rus как рабочую платформу Workforce Analytics + +Security Analytics + Forensics для ролей `executive`, `manager`, `security`, +`forensics` и `admin`. + +Демо-материалы: + +- [сценарий Pilot v1 demo](docs/PILOT_DEMO_SCENARIO_RU.md); +- [demo seed data](docs/fixtures/pilot-v1-demo/demo-seed-data.json); +- [demo evidence pack](docs/fixtures/pilot-v1-demo/evidence-pack/); +- [преддемо-runbook](docs/DEMO_RUNBOOK_RU.md). + +Границы показа: + +- pfSense показывается только как `contract_only/readiness`, без заявления + production ingestion или SIEM; +- UEBA Score v1 является прозрачной rule-based моделью, без ML/LLM; +- demo fixtures не содержат реальных IP-адресов, hostname, логинов, ФИО, + подразделений заказчика или событий безопасности; +- planned/future providers не являются реализованными collectors. + ## Что видит оператор - Работал ли пользователь за компьютером или в удаленной сессии. @@ -172,6 +194,7 @@ collectors. - [Архитектурный baseline](docs/ARCHITECTURE_BASELINE_RU.md) - [Пакет пилота для заказчика](docs/CUSTOMER_PILOT_PACK_RU.md) - [Pilot v1.0](docs/PILOT_V1_RU.md) +- [Pilot v1 demo](docs/PILOT_DEMO_SCENARIO_RU.md) - [Pilot v1.0 acceptance checklist](docs/PILOT_V1_ACCEPTANCE_CHECKLIST_RU.md) - [Pilot v1.0 evidence](docs/PILOT_V1_EVIDENCE_RU.md) - [Позиционирование продукта](docs/PRODUCT_POSITIONING_RU.md) diff --git a/adk-rust/crates/detmir-portal/src/main.rs b/adk-rust/crates/detmir-portal/src/main.rs index 55c2e10..3bb59a2 100644 --- a/adk-rust/crates/detmir-portal/src/main.rs +++ b/adk-rust/crates/detmir-portal/src/main.rs @@ -3441,6 +3441,7 @@ fn role_filtered_report(report: Value, role: PortalRole) -> Value { "business_risk", "business_risk_history_summary", "risk_heatmap", + "security_events_summary", "workforce", "markdown", ] { @@ -10037,6 +10038,27 @@ mod tests { } } + #[test] + fn portal_pilot_demo_navigation_is_present() { + for marker in [ + "Pilot v1 demo", + "Executive demo", + "Manager demo", + "Security demo", + "Forensics demo", + "Admin demo", + "data-demo-view-mode=\"executive\"", + "data-demo-view-mode=\"security\"", + "data-demo-view-mode=\"forensics\"", + "data-demo-view-mode=\"admin\"", + ] { + assert!( + INDEX_HTML.contains(marker), + "demo navigation missing {marker}" + ); + } + } + #[test] fn api_contract_artifacts_are_valid_and_future_ui_ready() { let openapi: Value = @@ -10134,6 +10156,7 @@ mod tests { let executive = role_filtered_report(report.clone(), PortalRole::Executive); assert!(executive.get("workforce").is_some()); assert!(executive.get("executive_dashboard").is_some()); + assert!(executive.get("security_events_summary").is_some()); assert!(executive.get("risk_incident_candidates").is_none()); assert!(executive.get("security_correlation").is_none()); diff --git a/adk-rust/crates/detmir-portal/src/static/app.css b/adk-rust/crates/detmir-portal/src/static/app.css index 2c8daa5..16fde90 100644 --- a/adk-rust/crates/detmir-portal/src/static/app.css +++ b/adk-rust/crates/detmir-portal/src/static/app.css @@ -82,13 +82,17 @@ body.security-mode .small-button, body.security-mode a.button, body.security-mode .tab, body.security-mode .nav-link, +body.security-mode .view-button, +body.security-mode .demo-button, body.security-mode .control-strip select { background: #111827; color: var(--ink); } body.security-mode .tab.is-active, -body.security-mode .nav-link.is-active { +body.security-mode .nav-link.is-active, +body.security-mode .view-button.is-active, +body.security-mode .demo-button.is-active { background: #0f2a3a; color: var(--link); } @@ -145,14 +149,16 @@ h1 { .control-strip, .header-actions, -.view-switch { +.view-switch, +.demo-switch { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; } -.view-switch { +.view-switch, +.demo-switch { align-items: center; padding: 4px; border: 1px solid var(--line); @@ -160,14 +166,16 @@ h1 { background: var(--panel); } -.view-switch > span { +.view-switch > span, +.demo-switch > span { padding: 0 6px; color: var(--muted); font-size: 12px; font-weight: 700; } -.view-button { +.view-button, +.demo-button { min-height: 30px; border: 1px solid transparent; border-radius: 6px; @@ -180,18 +188,21 @@ h1 { cursor: pointer; } -.view-button.is-active { +.view-button.is-active, +.demo-button.is-active { border-color: #1d4ed8; background: #2563eb; color: #ffffff; } -.view-button:hover { +.view-button:hover, +.demo-button:hover { color: var(--ink); background: rgba(37, 99, 235, 0.08); } -.view-button.is-active:hover { +.view-button.is-active:hover, +.demo-button.is-active:hover { color: #ffffff; background: #1d4ed8; } @@ -1210,10 +1221,13 @@ pre { .topbar-side { justify-items: stretch; min-width: 0; width: 100%; } .control-strip, .header-actions, - .view-switch { justify-content: flex-start; } + .view-switch, + .demo-switch { justify-content: flex-start; } .control-strip label { min-width: 100%; } - .view-switch > span { flex-basis: 100%; } - .view-button { flex: 1 1 120px; } + .view-switch > span, + .demo-switch > span { flex-basis: 100%; } + .view-button, + .demo-button { flex: 1 1 120px; } h1 { font-size: 24px; } .page-head { flex-direction: column; } .period-banner { flex-direction: column; align-items: flex-start; } diff --git a/adk-rust/crates/detmir-portal/src/static/app.js b/adk-rust/crates/detmir-portal/src/static/app.js index d441be9..b75da0e 100644 --- a/adk-rust/crates/detmir-portal/src/static/app.js +++ b/adk-rust/crates/detmir-portal/src/static/app.js @@ -248,6 +248,9 @@ function updateViewModeButtons() { document.querySelectorAll("[data-view-mode]").forEach(button => { button.classList.toggle("is-active", button.dataset.viewMode === currentViewMode()); }); + document.querySelectorAll("[data-demo-view-mode]").forEach(button => { + button.classList.toggle("is-active", button.dataset.demoViewMode === currentViewMode()); + }); } function setViewMode(mode) { @@ -2298,9 +2301,7 @@ function renderSecurityEventsSummary(summary, options = {}) { const title = options.compact ? "События безопасности" : "События безопасности за 24 часа"; - const compactStateText = disabled && options.compact - ? "Детализация скрыта для роли" - : stateText; + const compactStateText = stateText; const compactModeText = disabled && options.compact ? "Без ИБ-детализации" : disabled @@ -3052,6 +3053,10 @@ document.querySelectorAll("[data-view-mode]").forEach(btn => { btn.addEventListener("click", () => setViewMode(btn.dataset.viewMode)); }); +document.querySelectorAll("[data-demo-view-mode]").forEach(btn => { + btn.addEventListener("click", () => setViewMode(btn.dataset.demoViewMode)); +}); + document.getElementById("periodFilter")?.addEventListener("change", event => { state.period = periodFromSelectValue(event.target.value); refresh({ stage: "Получение данных за выбранный период" }); diff --git a/adk-rust/crates/detmir-portal/src/static/index.html b/adk-rust/crates/detmir-portal/src/static/index.html index f491887..02012cd 100644 --- a/adk-rust/crates/detmir-portal/src/static/index.html +++ b/adk-rust/crates/detmir-portal/src/static/index.html @@ -58,6 +58,14 @@ +
+ Pilot v1 demo + + + + + +
diff --git a/docs/DEMO_RUNBOOK_RU.md b/docs/DEMO_RUNBOOK_RU.md index 330b49c..7509d01 100644 --- a/docs/DEMO_RUNBOOK_RU.md +++ b/docs/DEMO_RUNBOOK_RU.md @@ -3,6 +3,9 @@ Цель: за 10 минут показать руководителю, ИБ и эксплуатации единый путь от главного риска до итогового отчета. +Полный ролевой сценарий Pilot v1 demo: +[PILOT_DEMO_SCENARIO_RU.md](PILOT_DEMO_SCENARIO_RU.md). + ## Перед началом - Открыть портал на рабочем демонстрационном URL. diff --git a/docs/PILOT_DEMO_SCENARIO_RU.md b/docs/PILOT_DEMO_SCENARIO_RU.md new file mode 100644 index 0000000..8270728 --- /dev/null +++ b/docs/PILOT_DEMO_SCENARIO_RU.md @@ -0,0 +1,190 @@ +# Pilot v1 Demo Scenario + +Цель сценария: за 10-12 минут показать AWatch-rus как рабочую платформу +Workforce Analytics + Security Analytics + Forensics без ложных заявлений о +SIEM, классическом DLP, ML/LLM или готовом pfSense ingestion. + +Демонстрация проводится только на обезличенных данных. Demo fixtures не +являются live ingestion и не подменяют промышленную настройку источников. + +## Перед показом + +1. Открыть `docs/DEMO_RUNBOOK_RU.md`. +2. Выполнить обязательный преддемо-прогрев. +3. Убедиться, что smoke-тест завершился с `ok=true`. +4. Открыть портал AWatch-rus. +5. Проверить, что данные загружены и главный вывод отображается первым. +6. Держать рядом итоговый Markdown-отчет или evidence pack. + +## Руководитель + +Экран: `Executive demo` / роль `Руководитель`. + +Показать: + +- блок `Главный вывод`; +- сводку руководителя; +- риски подразделений; +- карту рисков; +- итоговый управленческий Markdown-отчет. + +Ценность: + +- руководитель видит главный риск без чтения технических логов; +- видно, какое подразделение требует внимания; +- риск объясняется через активность, полноту данных и подтверждающие слои. + +Честные ограничения: + +- экран не является HR-оценкой сотрудника; +- вывод требует управленческой интерпретации; +- качество вывода зависит от свежести и полноты источников. + +Что не заявлять: + +- не говорить, что система автоматически принимает кадровые решения; +- не утверждать, что все события ИБ уже подтверждены как инциденты; +- не показывать техническую ИБ-детализацию как обязательную часть Executive View. + +## Менеджер + +Экран: `Manager demo` / роль `Менеджер`. + +Показать: + +- Workforce Dashboard; +- сравнение подразделений; +- ответственных; +- daily/weekly/monthly тренды; +- признаки перегрузки и недозагрузки; +- Markdown-отчет по управленческому срезу. + +Ценность: + +- менеджер видит загрузку подразделения и отклонения; +- можно отделить просадку активности от проблемы сбора данных; +- отчет можно передать руководителю без сырых событий. + +Честные ограничения: + +- это аналитика активности, а не автоматическая оценка эффективности; +- без корректного справочника подразделений часть данных может попасть в + категорию `Не привязано к подразделению`; +- точность зависит от настроенной политики рабочего времени. + +Что не заявлять: + +- не обещать готовую интеграцию со всеми HR/ERP-системами; +- не выдавать planned providers за реализованные collectors; +- не показывать security-only очередь расследований менеджеру. + +## Безопасность + +Экран: `Security demo` / роль `Безопасность`. + +Показать: + +- список рисков; +- UEBA Score v1; +- severity `low` / `medium` / `high` / `critical`; +- события безопасности, если источник доступен; +- кандидатов на проверку; +- pfSense readiness как `contract_only`. + +Ценность: + +- ИБ получает объяснимый риск-скоринг без ML/LLM; +- кандидаты на проверку отделены от управленческого Workforce Dashboard; +- pfSense показан честно как подготовленный контракт, а не как готовый SIEM. + +Честные ограничения: + +- UEBA v1 является rule-based моделью; +- pfSense readiness не означает production ingestion; +- риск является сигналом для проверки, а не автоматическим вердиктом. + +Что не заявлять: + +- не называть продукт SIEM; +- не утверждать, что pfSense ingestion уже работает; +- не говорить, что UEBA использует ML, LLM или внешние SaaS-модели. + +## Расследования + +Экран: `Forensics demo` / роль `Расследования`. + +Показать: + +- карточку расследования; +- timeline событий; +- связку `user / host / app / network event`; +- evidence package; +- экспорт Markdown-отчета. + +Ценность: + +- расследование собирает управляемый пакет материалов; +- timeline помогает объяснить, почему кандидат попал в проверку; +- отчет пригоден для ручного разбора ответственным сотрудником. + +Честные ограничения: + +- evidence package не является процессуальной экспертизой; +- скриншоты и доказательства в demo-контуре обезличены; +- окончательное решение принимает человек. + +Что не заявлять: + +- не говорить, что система сама доказывает нарушение; +- не показывать demo identifiers как реальные пользователи или хосты; +- не смешивать Forensics View с управленческим Workforce Dashboard. + +## Администратор + +Экран: `Admin demo` / роль `Админ`. + +Показать: + +- техническую готовность; +- полноту данных; +- качество сбора; +- состояние контрактов; +- server-side role gates; +- статус ClickHouse или понятный fallback, если источник недоступен. + +Ценность: + +- эксплуатация понимает, можно ли доверять текущей витрине; +- видны ошибки сбора и устаревшие данные; +- есть проверочный путь перед пилотным показом. + +Честные ограничения: + +- admin view не заменяет полноценный NOC/SIEM; +- часть источников может быть в readiness/contract-only статусе; +- эксплуатационные выводы требуют регулярной проверки smoke-тестами. + +Что не заявлять: + +- не обещать автоматическое восстановление всех источников; +- не скрывать fallback-режимы; +- не утверждать готовность planned/future providers. + +## Evidence Pack + +Демонстрационные материалы: + +- [demo-seed-data.json](fixtures/pilot-v1-demo/demo-seed-data.json); +- [executive-summary.md](fixtures/pilot-v1-demo/evidence-pack/executive-summary.md); +- [security-technical-summary.md](fixtures/pilot-v1-demo/evidence-pack/security-technical-summary.md); +- [investigation-report.md](fixtures/pilot-v1-demo/evidence-pack/investigation-report.md); +- [investigation-contract.json](fixtures/pilot-v1-demo/evidence-pack/investigation-contract.json). + +Все материалы demo-only. Для сетевых примеров используются только RFC 5737 +адреса `192.0.2.0/24`, `198.51.100.0/24`, `203.0.113.0/24`. + +## Финальный тезис + +AWatch-rus Pilot v1 демонстрирует управленческий контур, контур ИБ, +расследования, ролевые ограничения и честный readiness-слой для будущих +интеграций. Это не SIEM, не классический DLP и не ML-платформа. diff --git a/docs/PILOT_V1_EVIDENCE_RU.md b/docs/PILOT_V1_EVIDENCE_RU.md index 2da584b..8ba12c1 100644 --- a/docs/PILOT_V1_EVIDENCE_RU.md +++ b/docs/PILOT_V1_EVIDENCE_RU.md @@ -18,6 +18,7 @@ contracts, UEBA Score v1 и pfSense readiness. | UEBA | [UEBA_SCORE_RU.md](UEBA_SCORE_RU.md) | rule-based scoring без ML/LLM | | pfSense | [PFSENSE_INTEGRATION_RU.md](PFSENSE_INTEGRATION_RU.md) | readiness=`contract_only`, без SIEM/ingestion claims | | Demo сценарий | [CUSTOMER_DEMO_SCENARIO_RU.md](CUSTOMER_DEMO_SCENARIO_RU.md) | 10-минутный показ заказчику | +| Pilot v1 demo | [PILOT_DEMO_SCENARIO_RU.md](PILOT_DEMO_SCENARIO_RU.md) | воспроизводимый demo-pack по ролям | | Преддемо | [DEMO_RUNBOOK_RU.md](DEMO_RUNBOOK_RU.md) | прогрев и порядок проверки перед показом | | Gap analysis | [PILOT_GAP_ANALYSIS_RU.md](PILOT_GAP_ANALYSIS_RU.md) | известные риски и остаточные ограничения | @@ -52,6 +53,11 @@ contracts, UEBA Score v1 и pfSense readiness. - `docs/screenshots/06-markdown-report.png`; - `docs/screenshots/07-product-architecture.png`; - `docs/fixtures/pfsense-demo-events.json`. +- `docs/fixtures/pilot-v1-demo/demo-seed-data.json`; +- `docs/fixtures/pilot-v1-demo/evidence-pack/executive-summary.md`; +- `docs/fixtures/pilot-v1-demo/evidence-pack/security-technical-summary.md`; +- `docs/fixtures/pilot-v1-demo/evidence-pack/investigation-report.md`; +- `docs/fixtures/pilot-v1-demo/evidence-pack/investigation-contract.json`. Требования: @@ -81,6 +87,7 @@ cargo build --workspace --release cd .. git diff --check node scripts/detmir-portal-tabs-smoke.mjs +node scripts/detmir-pilot-demo-smoke.mjs ``` Что подтверждают команды: @@ -93,6 +100,8 @@ node scripts/detmir-portal-tabs-smoke.mjs - `git diff --check` - нет whitespace-ошибок в diff. - `node scripts/detmir-portal-tabs-smoke.mjs` - портал, вкладки, роли и server-side role gates проходят smoke. +- `node scripts/detmir-pilot-demo-smoke.mjs` - demo-навигация, API contracts и + role-filtering для Pilot v1 demo проходят smoke. ## Приемочный вывод diff --git a/docs/fixtures/pilot-v1-demo/README_RU.md b/docs/fixtures/pilot-v1-demo/README_RU.md new file mode 100644 index 0000000..9dd82b4 --- /dev/null +++ b/docs/fixtures/pilot-v1-demo/README_RU.md @@ -0,0 +1,25 @@ +# Pilot v1 Demo Fixtures + +Этот каталог содержит воспроизводимые обезличенные материалы для демонстрации +AWatch-rus Pilot v1. + +Состав: + +- `demo-seed-data.json` - демонстрационный набор сигналов Workforce, Security, + Forensics, UEBA и pfSense readiness; +- `evidence-pack/executive-summary.md` - краткий управленческий вывод; +- `evidence-pack/security-technical-summary.md` - техническая сводка для ИБ; +- `evidence-pack/investigation-report.md` - Markdown-отчет расследования; +- `evidence-pack/investigation-contract.json` - пример JSON-контракта + investigation/evidence package. + +Границы: + +- это не live ingestion; +- это не production data; +- это не новый collector; +- pfSense остается `contract_only/readiness`; +- UEBA Score v1 остается rule-based без ML/LLM. + +В demo fixtures запрещены реальные IP-адреса, hostname, логины, ФИО, +подразделения заказчика и реальные события безопасности. diff --git a/docs/fixtures/pilot-v1-demo/demo-seed-data.json b/docs/fixtures/pilot-v1-demo/demo-seed-data.json new file mode 100644 index 0000000..e05f032 --- /dev/null +++ b/docs/fixtures/pilot-v1-demo/demo-seed-data.json @@ -0,0 +1,223 @@ +{ + "dataset": "awatch-rus-pilot-v1-demo", + "generated_at_utc": "2026-06-06T09:00:00Z", + "demo_only": true, + "privacy": { + "real_personal_data": false, + "real_customer_hosts": false, + "real_customer_networks": false, + "allowed_network_examples": [ + "192.0.2.0/24", + "198.51.100.0/24", + "203.0.113.0/24" + ] + }, + "departments": [ + { + "department_id": "dept-sales-demo", + "name": "Отдел продаж", + "activity_index": 82, + "load_state": "normal", + "risk": "low" + }, + { + "department_id": "dept-accounting-demo", + "name": "Бухгалтерия", + "activity_index": 68, + "load_state": "normal", + "risk": "medium" + }, + { + "department_id": "dept-warehouse-demo", + "name": "Склад", + "activity_index": 41, + "load_state": "low_activity", + "risk": "high" + }, + { + "department_id": "dept-support-demo", + "name": "Поддержка", + "activity_index": 91, + "load_state": "overload", + "risk": "medium" + }, + { + "department_id": "dept-it-demo", + "name": "ИТ", + "activity_index": 74, + "load_state": "normal", + "risk": "critical" + } + ], + "users": [ + { + "user_ref": "demo-user-001", + "department_id": "dept-sales-demo", + "host": "HOST-DEMO-01", + "responsible": "manager-demo-sales" + }, + { + "user_ref": "demo-user-002", + "department_id": "dept-accounting-demo", + "host": "HOST-DEMO-02", + "responsible": "manager-demo-accounting" + }, + { + "user_ref": "demo-user-003", + "department_id": "dept-warehouse-demo", + "host": "HOST-DEMO-03", + "responsible": "manager-demo-warehouse" + }, + { + "user_ref": "demo-user-004", + "department_id": "dept-support-demo", + "host": "HOST-DEMO-04", + "responsible": "manager-demo-support" + }, + { + "user_ref": "demo-user-005", + "department_id": "dept-it-demo", + "host": "HOST-DEMO-05", + "responsible": "manager-demo-it" + } + ], + "activity_events": [ + { + "timestamp": "2026-06-06T08:55:00Z", + "user_ref": "demo-user-001", + "host": "HOST-DEMO-01", + "app": "crm-demo.exe", + "kind": "normal_activity", + "duration_minutes": 42 + }, + { + "timestamp": "2026-06-06T09:10:00Z", + "user_ref": "demo-user-002", + "host": "HOST-DEMO-02", + "app": "1cv8.exe", + "kind": "business_app_activity", + "duration_minutes": 67 + }, + { + "timestamp": "2026-06-06T10:25:00Z", + "user_ref": "demo-user-003", + "host": "HOST-DEMO-03", + "app": "rdp-session", + "kind": "low_activity", + "duration_minutes": 8 + }, + { + "timestamp": "2026-06-06T21:40:00Z", + "user_ref": "demo-user-005", + "host": "HOST-DEMO-05", + "app": "admin-tool-demo.exe", + "kind": "night_access_anomaly", + "duration_minutes": 24 + } + ], + "security_events": [ + { + "event_id": "sec-demo-001", + "timestamp": "2026-06-06T11:05:00Z", + "severity": "low", + "user_ref": "demo-user-001", + "host": "HOST-DEMO-01", + "reason_code": "activity_anomaly", + "description": "Повышенная активность без сетевых признаков." + }, + { + "event_id": "sec-demo-002", + "timestamp": "2026-06-06T12:20:00Z", + "severity": "medium", + "user_ref": "demo-user-002", + "host": "HOST-DEMO-02", + "reason_code": "application_anomaly", + "description": "Редкое приложение в рабочем контексте." + }, + { + "event_id": "sec-demo-003", + "timestamp": "2026-06-06T18:35:00Z", + "severity": "high", + "user_ref": "demo-user-003", + "host": "HOST-DEMO-03", + "reason_code": "low_activity_with_late_rdp", + "description": "Низкая дневная активность и поздняя RDP-сессия." + }, + { + "event_id": "sec-demo-004", + "timestamp": "2026-06-06T21:45:00Z", + "severity": "critical", + "user_ref": "demo-user-005", + "host": "HOST-DEMO-05", + "reason_code": "night_network_admin_activity", + "description": "Ночное администрирование и сетевой признак требуют ручной проверки." + } + ], + "network_events": [ + { + "timestamp": "2026-06-06T21:46:00Z", + "source_host": "HOST-DEMO-05", + "source_ip": "192.0.2.15", + "destination": "203.0.113.20:443", + "action": "pass", + "provider_status": "contract_only" + }, + { + "timestamp": "2026-06-06T21:47:00Z", + "source_host": "HOST-DEMO-05", + "source_ip": "192.0.2.15", + "destination": "198.51.100.25:22", + "action": "block", + "provider_status": "contract_only" + } + ], + "ueba_score_examples": [ + { + "user_ref": "demo-user-001", + "score": 18, + "severity": "low", + "reason_codes": [ + "activity_anomaly" + ] + }, + { + "user_ref": "demo-user-002", + "score": 42, + "severity": "medium", + "reason_codes": [ + "application_anomaly", + "history_anomaly" + ] + }, + { + "user_ref": "demo-user-003", + "score": 63, + "severity": "high", + "reason_codes": [ + "activity_anomaly", + "time_anomaly" + ] + }, + { + "user_ref": "demo-user-005", + "score": 82, + "severity": "critical", + "reason_codes": [ + "time_anomaly", + "network_anomaly", + "history_anomaly" + ] + } + ], + "investigation": { + "investigation_id": "case-demo-004", + "candidate_id": "sec-demo-004", + "status": "in_review", + "entities": { + "user": "demo-user-005", + "host": "HOST-DEMO-05", + "app": "admin-tool-demo.exe", + "network_event": "203.0.113.20:443" + } + } +} diff --git a/docs/fixtures/pilot-v1-demo/evidence-pack/executive-summary.md b/docs/fixtures/pilot-v1-demo/evidence-pack/executive-summary.md new file mode 100644 index 0000000..d288b58 --- /dev/null +++ b/docs/fixtures/pilot-v1-demo/evidence-pack/executive-summary.md @@ -0,0 +1,25 @@ +# Executive Summary + +Demo-only evidence pack for AWatch-rus Pilot v1. + +## Главный вывод + +В демонстрационном периоде главный управленческий риск связан с сочетанием +ночной административной активности, сетевого признака и отклонения от обычной +истории работы. Риск требует ручной проверки, но не является автоматическим +вердиктом о нарушении. + +## Что видно руководителю + +- общий индекс активности по подразделениям; +- подразделение с повышенным риском; +- причина риска в понятных управленческих терминах; +- краткий вывод без технических логов; +- ссылка на evidence pack для ИБ и расследований. + +## Ограничения + +- demo data обезличены; +- UEBA Score v1 является rule-based моделью; +- pfSense readiness остается `contract_only`; +- AWatch-rus не заявляется как SIEM, классический DLP или ML-платформа. diff --git a/docs/fixtures/pilot-v1-demo/evidence-pack/investigation-contract.json b/docs/fixtures/pilot-v1-demo/evidence-pack/investigation-contract.json new file mode 100644 index 0000000..fd9765a --- /dev/null +++ b/docs/fixtures/pilot-v1-demo/evidence-pack/investigation-contract.json @@ -0,0 +1,53 @@ +{ + "contract_version": "forensics-v1", + "demo_only": true, + "investigation": { + "investigation_id": "case-demo-004", + "candidate_id": "sec-demo-004", + "status": "in_review", + "severity": "critical", + "summary": "Ночная административная активность и сетевой признак требуют ручной проверки.", + "entities": { + "user": "demo-user-005", + "host": "HOST-DEMO-05", + "app": "admin-tool-demo.exe", + "network_event": "203.0.113.20:443" + }, + "timeline": [ + { + "timestamp": "2026-06-06T21:40:00Z", + "kind": "activity", + "entity": "demo-user-005", + "summary": "Ночная активность в административном приложении.", + "source": "activity_rules" + }, + { + "timestamp": "2026-06-06T21:45:00Z", + "kind": "risk", + "entity": "sec-demo-004", + "summary": "UEBA Score v1 severity critical.", + "source": "ueba-score-v1" + }, + { + "timestamp": "2026-06-06T21:46:00Z", + "kind": "network", + "entity": "HOST-DEMO-05", + "summary": "Сетевой признак по readiness-контракту.", + "source": "pfsense-contract" + } + ], + "exports": { + "markdown": "investigation-report.md", + "executive_summary": "executive-summary.md", + "security_summary": "security-technical-summary.md" + } + }, + "claims": { + "siem": false, + "classic_dlp": false, + "ml_used": false, + "llm_used": false, + "pfsense_ingestion": false, + "pfsense_status": "contract_only" + } +} diff --git a/docs/fixtures/pilot-v1-demo/evidence-pack/investigation-report.md b/docs/fixtures/pilot-v1-demo/evidence-pack/investigation-report.md new file mode 100644 index 0000000..3d9f934 --- /dev/null +++ b/docs/fixtures/pilot-v1-demo/evidence-pack/investigation-report.md @@ -0,0 +1,30 @@ +# Investigation Report + +Demo-only investigation report for AWatch-rus Pilot v1. + +## Карточка расследования + +- Investigation ID: `case-demo-004` +- Candidate ID: `sec-demo-004` +- Status: `in_review` +- Severity: `critical` +- User: `demo-user-005` +- Host: `HOST-DEMO-05` +- App: `admin-tool-demo.exe` +- Network event: `203.0.113.20:443` + +## Timeline + +| Время UTC | Тип | Сущность | Кратко | Источник | +| --- | --- | --- | --- | --- | +| 2026-06-06T21:40:00Z | activity | `demo-user-005` | Ночная активность в административном приложении | activity_rules | +| 2026-06-06T21:45:00Z | risk | `sec-demo-004` | UEBA severity `critical` | ueba-score-v1 | +| 2026-06-06T21:46:00Z | network | `HOST-DEMO-05` | Сетевой признак по readiness-контракту | pfsense-contract | +| 2026-06-06T21:50:00Z | review | `case-demo-004` | Кейс требует ручной проверки | portal_contract | + +## Вывод расследования + +Демонстрационный кейс показывает, как AWatch-rus связывает активность, +приложение, хост и сетевой признак в единый пакет проверки. Данные обезличены. +Система не выносит автоматический вердикт и не заявляет production pfSense +ingestion. diff --git a/docs/fixtures/pilot-v1-demo/evidence-pack/security-technical-summary.md b/docs/fixtures/pilot-v1-demo/evidence-pack/security-technical-summary.md new file mode 100644 index 0000000..e3efc30 --- /dev/null +++ b/docs/fixtures/pilot-v1-demo/evidence-pack/security-technical-summary.md @@ -0,0 +1,40 @@ +# Security Technical Summary + +Demo-only security summary for AWatch-rus Pilot v1. + +## Сигнал + +Кандидат `sec-demo-004` получил severity `critical` из-за комбинации: + +- `time_anomaly`; +- `network_anomaly`; +- `history_anomaly`. + +## UEBA Score v1 + +```json +{ + "score": 82, + "severity": "critical", + "model": "ueba-score-v1", + "type": "rule_based", + "ml_used": false, + "llm_used": false +} +``` + +## Network Context + +Сетевые признаки показаны на RFC 5737 адресах и относятся к readiness-контракту: + +- source host: `HOST-DEMO-05`; +- source IP: `192.0.2.15`; +- destination: `203.0.113.20:443`; +- provider status: `contract_only`. + +## Ограничения + +- это не production pfSense ingestion; +- это не SIEM-correlation; +- риск является кандидатом на ручную проверку; +- окончательное решение принимает специалист. diff --git a/scripts/detmir-pilot-demo-smoke.mjs b/scripts/detmir-pilot-demo-smoke.mjs new file mode 100644 index 0000000..1b35198 --- /dev/null +++ b/scripts/detmir-pilot-demo-smoke.mjs @@ -0,0 +1,244 @@ +#!/usr/bin/env node +import fs from "node:fs"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); + +if (["1", "true", "yes"].includes(String(process.env.DETMIR_PORTAL_SMOKE_INSECURE_TLS || "").toLowerCase())) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; +} + +function env(name, fallback = "") { + const value = process.env[name]; + return value && value.trim() ? value.trim() : fallback; +} + +function authHeaders() { + const explicit = env("DETMIR_PORTAL_SMOKE_AUTH_HEADER"); + if (explicit) return { Authorization: explicit }; + const basic = env("DETMIR_PORTAL_SMOKE_BASIC_AUTH", env("DETMIR_BASIC_AUTH")); + if (basic) return { Authorization: `Basic ${basic}` }; + return {}; +} + +function normalizeBaseUrl(raw) { + const value = raw.endsWith("/") ? raw : `${raw}/`; + return new URL(value); +} + +function contains(text, marker) { + return String(text || "").toLowerCase().includes(String(marker || "").toLowerCase()); +} + +function hasOwn(object, key) { + return Boolean(object && Object.prototype.hasOwnProperty.call(object, key)); +} + +async function fetchText(baseUrl, relativePath, role = "executive") { + const url = new URL(relativePath, baseUrl); + const response = await fetch(url, { + headers: { + ...authHeaders(), + "X-AWatch-Role": role, + Accept: "text/html,application/json", + }, + }); + const text = await response.text(); + let json = null; + try { + json = text ? JSON.parse(text) : null; + } catch { + // HTML and text error bodies are expected for some checks. + } + return { url: url.toString(), status: response.status, ok: response.ok, text, json }; +} + +function readDemoFiles() { + const files = [ + "docs/PILOT_DEMO_SCENARIO_RU.md", + "docs/fixtures/pilot-v1-demo/README_RU.md", + "docs/fixtures/pilot-v1-demo/demo-seed-data.json", + "docs/fixtures/pilot-v1-demo/evidence-pack/executive-summary.md", + "docs/fixtures/pilot-v1-demo/evidence-pack/security-technical-summary.md", + "docs/fixtures/pilot-v1-demo/evidence-pack/investigation-report.md", + "docs/fixtures/pilot-v1-demo/evidence-pack/investigation-contract.json", + ]; + return files.map((file) => ({ + file, + text: fs.readFileSync(path.join(root, file), "utf8"), + })); +} + +function scanDemoFiles() { + const forbidden = [ + /10\.10\.\d+\.\d+/, + /192\.168\.\d+\.\d+/, + /172\.(1[6-9]|2\d|3[0-1])\.\d+\.\d+/, + new RegExp(["dm", "\\.iri"].join(""), "i"), + new RegExp(["shar", "kon"].join(""), "i"), + new RegExp(["/home/", "igor"].join(""), "i"), + /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i, + new RegExp(["Det", "Mir"].join("")), + ]; + const findings = []; + for (const item of readDemoFiles()) { + for (const pattern of forbidden) { + if (pattern.test(item.text)) { + findings.push({ file: item.file, pattern: String(pattern) }); + } + } + } + return findings; +} + +function pass(checks, name, ok, details = {}) { + checks.push({ name, ok: Boolean(ok), ...details }); +} + +async function main() { + const baseUrl = normalizeBaseUrl(env("DETMIR_PORTAL_SMOKE_URL", "http://127.0.0.1:8720/portal/")); + const checks = []; + + const html = await fetchText(baseUrl, "", "executive"); + pass(checks, "portal_html_available", html.ok && contains(html.text, "AWatch-rus"), { + status: html.status, + url: html.url, + }); + for (const marker of [ + "Pilot v1 demo", + "Executive demo", + "Manager demo", + "Security demo", + "Forensics demo", + "Admin demo", + ]) { + pass(checks, `demo_navigation:${marker}`, contains(html.text, marker), { marker }); + } + + const executiveReports = await fetchText(baseUrl, "api/reports?role=executive", "executive"); + pass(checks, "executive_reports_ok", executiveReports.ok, { status: executiveReports.status }); + pass( + checks, + "executive_no_forensics_only_data", + executiveReports.ok + && !hasOwn(executiveReports.json, "forensics") + && !hasOwn(executiveReports.json, "risk_incident_candidates") + && !hasOwn(executiveReports.json, "incident_review_audit_summary") + && !hasOwn(executiveReports.json, "security_correlation"), + ); + const executiveForensics = await fetchText(baseUrl, "api/forensics?role=executive", "executive"); + pass(checks, "executive_forensics_forbidden", executiveForensics.status === 403, { + status: executiveForensics.status, + }); + + const managerReports = await fetchText(baseUrl, "api/reports?role=manager", "manager"); + pass( + checks, + "manager_no_security_only_data", + managerReports.ok + && !hasOwn(managerReports.json, "ueba_risk") + && !hasOwn(managerReports.json, "security_events_summary") + && !hasOwn(managerReports.json, "risk_incident_candidates") + && !hasOwn(managerReports.json, "forensics"), + { status: managerReports.status }, + ); + const managerSecurity = await fetchText(baseUrl, "api/security?role=manager", "manager"); + pass(checks, "manager_security_forbidden", managerSecurity.status === 403, { + status: managerSecurity.status, + }); + + const securityWorkforce = await fetchText(baseUrl, "api/workforce?role=security", "security"); + pass(checks, "security_workforce_forbidden", securityWorkforce.status === 403, { + status: securityWorkforce.status, + }); + const securityPayload = await fetchText(baseUrl, "api/security?role=security", "security"); + pass( + checks, + "security_payload_ok", + securityPayload.ok + && hasOwn(securityPayload.json, "ueba_risk") + && hasOwn(securityPayload.json, "security_events_summary") + && !hasOwn(securityPayload.json, "workforce"), + { status: securityPayload.status }, + ); + const uebaPayload = await fetchText(baseUrl, "api/ueba?role=security", "security"); + pass( + checks, + "security_ueba_contract_ok", + uebaPayload.ok + && hasOwn(uebaPayload.json, "score") + && hasOwn(uebaPayload.json, "severity") + && uebaPayload.json?.model?.type === "rule_based" + && uebaPayload.json?.model?.ml_used === false + && uebaPayload.json?.model?.llm_used === false, + { status: uebaPayload.status }, + ); + const pfsensePayload = await fetchText(baseUrl, "api/pfsense?role=security", "security"); + pass( + checks, + "security_pfsense_readiness_contract_only", + pfsensePayload.ok + && pfsensePayload.json?.status === "contract_only" + && pfsensePayload.json?.siem === false + && pfsensePayload.json?.ingestion_available === false, + { status: pfsensePayload.status }, + ); + + const forensicsPayload = await fetchText(baseUrl, "api/forensics?role=forensics", "forensics"); + pass( + checks, + "forensics_contract_ok", + forensicsPayload.ok + && forensicsPayload.json?.forensics?.contract_version === "forensics-v1" + && Array.isArray(forensicsPayload.json?.forensics?.investigations), + { status: forensicsPayload.status }, + ); + const evidencePayload = await fetchText(baseUrl, "api/dlp/evidence?role=forensics", "forensics"); + pass(checks, "forensics_evidence_endpoint_ok", evidencePayload.ok && Boolean(evidencePayload.json), { + status: evidencePayload.status, + }); + + const adminReports = await fetchText(baseUrl, "api/reports?role=admin", "admin"); + pass( + checks, + "admin_technical_readiness_visible", + adminReports.ok + && adminReports.json?.role_context?.role === "admin" + && ( + hasOwn(adminReports.json, "agent_quality") + || hasOwn(adminReports.json, "agent_coverage_sla") + || hasOwn(adminReports.json, "security_events_summary") + || hasOwn(adminReports.json, "operator_ok") + ), + { status: adminReports.status }, + ); + const readinessPayload = await fetchText(baseUrl, "api/readiness/latest?role=admin", "admin"); + pass(checks, "admin_readiness_endpoint_ok", readinessPayload.ok && Boolean(readinessPayload.json), { + status: readinessPayload.status, + }); + + const sensitivityFindings = scanDemoFiles(); + pass(checks, "demo_pack_sensitive_string_scan", sensitivityFindings.length === 0, { + findings: sensitivityFindings, + }); + + const ok = checks.every((check) => check.ok); + process.stdout.write(`${JSON.stringify({ + ok, + generated_at_utc: new Date().toISOString(), + url: baseUrl.toString(), + checks, + }, null, 2)}\n`); + process.exitCode = ok ? 0 : 2; +} + +main().catch((error) => { + process.stdout.write(`${JSON.stringify({ + ok: false, + error: error.message, + stack: error.stack, + }, null, 2)}\n`); + process.exitCode = 2; +});