docs+portal: add pilot v1 demo pack

This commit is contained in:
igor04091968
2026-06-06 21:33:31 +03:00
parent b062237168
commit 9c72539401
15 changed files with 929 additions and 14 deletions
+23
View File
@@ -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)
+23
View File
@@ -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());
@@ -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; }
@@ -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: "Получение данных за выбранный период" });
@@ -58,6 +58,14 @@
<button class="view-button" data-view-mode="forensics" type="button">Расследования</button>
<button class="view-button" data-view-mode="admin" type="button">Админ</button>
</div>
<div class="demo-switch" aria-label="Pilot v1 demo navigation">
<span>Pilot v1 demo</span>
<button class="demo-button is-active" data-demo-view-mode="executive" type="button">Executive demo</button>
<button class="demo-button" data-demo-view-mode="manager" type="button">Manager demo</button>
<button class="demo-button" data-demo-view-mode="security" type="button">Security demo</button>
<button class="demo-button" data-demo-view-mode="forensics" type="button">Forensics demo</button>
<button class="demo-button" data-demo-view-mode="admin" type="button">Admin demo</button>
</div>
<div class="header-actions">
<button class="small-button primary" data-open-reports="true">Сформировать отчет</button>
<button class="small-button" data-export-markdown="true">Текстовый отчет</button>
+3
View File
@@ -3,6 +3,9 @@
Цель: за 10 минут показать руководителю, ИБ и эксплуатации единый путь от
главного риска до итогового отчета.
Полный ролевой сценарий Pilot v1 demo:
[PILOT_DEMO_SCENARIO_RU.md](PILOT_DEMO_SCENARIO_RU.md).
## Перед началом
- Открыть портал на рабочем демонстрационном URL.
+190
View File
@@ -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-платформа.
+9
View File
@@ -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.
## Приемочный вывод
+25
View File
@@ -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, логины, ФИО,
подразделения заказчика и реальные события безопасности.
@@ -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"
}
}
}
@@ -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-платформа.
@@ -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"
}
}
@@ -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.
@@ -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;
- риск является кандидатом на ручную проверку;
- окончательное решение принимает специалист.
+244
View File
@@ -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;
});