docs(pilot): freeze readiness and demo safety guardrails
This commit is contained in:
@@ -5,7 +5,10 @@ AWatch-rus - программный комплекс операционного
|
|||||||
корпоративной ИТ-инфраструктуры на базе ActivityWatch, Rust-сервисов
|
корпоративной ИТ-инфраструктуры на базе ActivityWatch, Rust-сервисов
|
||||||
автоматизации, Grafana/Prometheus-витрин и модулей расследования инцидентов.
|
автоматизации, Grafana/Prometheus-витрин и модулей расследования инцидентов.
|
||||||
|
|
||||||
Проект не позиционируется как сертифицированная DLP/SIEM/EDR/XDR/СЗИ,хотя DLP,evidence и Hayabusa используются в проекте.
|
Проект не позиционируется как сертифицированная DLP/SIEM/EDR/XDR/СЗИ,
|
||||||
|
не заявляет ML/LLM UEBA и не подменяет штатные средства защиты, хотя
|
||||||
|
DLP-сигналы, evidence и Hayabusa используются как аналитические и
|
||||||
|
расследовательские слои.
|
||||||
|
|
||||||
## Назначение
|
## Назначение
|
||||||
|
|
||||||
@@ -24,6 +27,11 @@ AWatch-rus - программный комплекс операционного
|
|||||||
Основной серверный runtime AWatch-rus переведен на Rust: status/check/auto-heal,
|
Основной серверный runtime AWatch-rus переведен на Rust: status/check/auto-heal,
|
||||||
SLO, worktime, DLP server-side helpers, evidence и install-kit tooling.
|
SLO, worktime, DLP server-side helpers, evidence и install-kit tooling.
|
||||||
|
|
||||||
|
Это Rust-primary направление, а не заявление о полном удалении PowerShell.
|
||||||
|
Оставшиеся PowerShell runtime/fallback/installer/repair scripts сохраняются
|
||||||
|
как документированный слой отката, установки и поддержки до отдельной задачи
|
||||||
|
удаления с burn-in периодом, canary test, rollback plan и acceptance gate.
|
||||||
|
|
||||||
Python, присутствующий в коде репозитория, остается для вспомогательных направлений: Telegram bot
|
Python, присутствующий в коде репозитория, остается для вспомогательных направлений: Telegram bot
|
||||||
runtime(для оперативного оповещения), OCR/content-analysis, 1C/AI/ETL integration и MCP/dev helpers. Эти части не являются ядром Rust-first runtime.
|
runtime(для оперативного оповещения), OCR/content-analysis, 1C/AI/ETL integration и MCP/dev helpers. Эти части не являются ядром Rust-first runtime.
|
||||||
|
|
||||||
@@ -55,7 +63,8 @@ Implemented:
|
|||||||
Planned:
|
Planned:
|
||||||
|
|
||||||
- Provider detail expansion under `/portal/architecture`.
|
- Provider detail expansion under `/portal/architecture`.
|
||||||
- PowerShell Provider.
|
- PowerShell Provider как planned/agentless direction, не как возврат новых
|
||||||
|
runtime-функций на PowerShell.
|
||||||
- SSH Provider.
|
- SSH Provider.
|
||||||
- Syslog Provider.
|
- Syslog Provider.
|
||||||
- 1C Provider как формализация текущего file-based 1C analytics направления.
|
- 1C Provider как формализация текущего file-based 1C analytics направления.
|
||||||
@@ -89,6 +98,7 @@ Security Analytics + Forensics для ролей `executive`, `manager`, `securi
|
|||||||
Pilot validation:
|
Pilot validation:
|
||||||
|
|
||||||
- [чеклист проверки пилота](docs/PILOT_VALIDATION_CHECKLIST_RU.md);
|
- [чеклист проверки пилота](docs/PILOT_VALIDATION_CHECKLIST_RU.md);
|
||||||
|
- [pilot freeze readiness](docs/PILOT_FREEZE_READINESS_RU.md);
|
||||||
- [gap analysis пилота](docs/PILOT_GAP_ANALYSIS_RU.md);
|
- [gap analysis пилота](docs/PILOT_GAP_ANALYSIS_RU.md);
|
||||||
- [вопросы для discovery с заказчиком](docs/CUSTOMER_DISCOVERY_QUESTIONS_RU.md);
|
- [вопросы для discovery с заказчиком](docs/CUSTOMER_DISCOVERY_QUESTIONS_RU.md);
|
||||||
- [критерии успеха пилота](docs/PILOT_SUCCESS_CRITERIA_RU.md);
|
- [критерии успеха пилота](docs/PILOT_SUCCESS_CRITERIA_RU.md);
|
||||||
@@ -98,6 +108,8 @@ Pilot validation:
|
|||||||
|
|
||||||
- pfSense показывается только как `contract_only/readiness`, без заявления
|
- pfSense показывается только как `contract_only/readiness`, без заявления
|
||||||
production ingestion или SIEM;
|
production ingestion или SIEM;
|
||||||
|
- pfSense в текущем пилоте допускается только как contract/readiness/optional
|
||||||
|
integration layer;
|
||||||
- UEBA Score v1 является прозрачной rule-based моделью, без ML/LLM;
|
- UEBA Score v1 является прозрачной rule-based моделью, без ML/LLM;
|
||||||
- demo fixtures не содержат реальных IP-адресов, hostname, логинов, ФИО,
|
- demo fixtures не содержат реальных IP-адресов, hostname, логинов, ФИО,
|
||||||
подразделений заказчика или событий безопасности;
|
подразделений заказчика или событий безопасности;
|
||||||
|
|||||||
@@ -101,6 +101,9 @@ SIEM, классическом DLP, ML/LLM .
|
|||||||
|
|
||||||
- UEBA v1 является rule-based моделью;
|
- UEBA v1 является rule-based моделью;
|
||||||
- pfSense readiness не означает production ingestion;
|
- pfSense readiness не означает production ingestion;
|
||||||
|
- Не заявлять готовый pfSense production ingestion / SIEM ingestion. pfSense
|
||||||
|
в текущем пилоте допускается только как contract/readiness/optional
|
||||||
|
integration layer.
|
||||||
- риск является сигналом для проверки, а не автоматическим вердиктом.
|
- риск является сигналом для проверки, а не автоматическим вердиктом.
|
||||||
|
|
||||||
Что не заявлять:
|
Что не заявлять:
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
# Pilot freeze readiness AWatch-rus
|
||||||
|
|
||||||
|
Статус: pilot freeze candidate.
|
||||||
|
|
||||||
|
Документ фиксирует gate перед заморозкой текущего `main` для пилота. Freeze не
|
||||||
|
добавляет новую функциональность, не меняет архитектуру и не упрощает
|
||||||
|
существующие контуры. Цель gate - подтвердить, что текущий main можно
|
||||||
|
демонстрировать и донастраивать в пилоте без расширения заявлений о продукте.
|
||||||
|
|
||||||
|
## 1. Границы продукта
|
||||||
|
|
||||||
|
AWatch-rus Pilot v1 позиционируется как Workforce-first платформа с отдельными
|
||||||
|
ролями Security и Forensics.
|
||||||
|
|
||||||
|
В рамках pilot freeze запрещено заявлять AWatch-rus как:
|
||||||
|
|
||||||
|
- SIEM;
|
||||||
|
- enterprise DLP;
|
||||||
|
- EDR/XDR;
|
||||||
|
- сертифицированное СЗИ;
|
||||||
|
- ML/LLM UEBA;
|
||||||
|
- готовый pfSense production ingestion или SIEM ingestion.
|
||||||
|
|
||||||
|
UEBA v1 является rule-based risk scoring для ручной проверки. Риск не является
|
||||||
|
автоматическим вердиктом и не доказывает нарушение без анализа человеком.
|
||||||
|
|
||||||
|
pfSense в текущем пилоте допускается только как
|
||||||
|
contract/readiness/optional integration layer. Его статус в demo/pilot
|
||||||
|
материалах: `contract_only` или `readiness`, если отдельный production
|
||||||
|
ingestion не включен и не принят отдельным gate.
|
||||||
|
|
||||||
|
## 2. Архитектурная фиксация
|
||||||
|
|
||||||
|
Pilot freeze candidate сохраняет текущую архитектуру:
|
||||||
|
|
||||||
|
- Rust Backend;
|
||||||
|
- Rust Agent;
|
||||||
|
- Rust-first operational CLIs and services;
|
||||||
|
- HTML/HTMX Portal;
|
||||||
|
- role-based Pilot v1;
|
||||||
|
- documented PowerShell runtime/fallback/installer/repair layer до отдельного
|
||||||
|
retirement gate;
|
||||||
|
- future React/TypeScript UI только как future direction;
|
||||||
|
- Tauri только как future desktop Forensics;
|
||||||
|
- Dioxus не рассматривается.
|
||||||
|
|
||||||
|
Breaking changes, architecture changes и simplifications в рамках freeze gate
|
||||||
|
запрещены.
|
||||||
|
|
||||||
|
## 3. Роли Pilot v1
|
||||||
|
|
||||||
|
Ожидаемая видимость ролей:
|
||||||
|
|
||||||
|
| Роль | Видимость |
|
||||||
|
|---|---|
|
||||||
|
| `executive` | Только executive-level: главный вывод, агрегированные риски, итоговые управленческие действия без сырых технических очередей. |
|
||||||
|
| `manager` | Workforce/department/owner/activity: загрузка, подразделения, ответственные, тренды и управленческие отчеты. |
|
||||||
|
| `security` | Security/UEBA/pfSense readiness: candidates, severity, rule-based UEBA, security context и `contract_only` readiness. |
|
||||||
|
| `forensics` | Investigation/evidence/reporting: карточки расследований, timeline, evidence package и Markdown export. |
|
||||||
|
| `admin` | Административные и технические функции: полнота данных, качество сбора, источники, fallback, service/readiness status. |
|
||||||
|
|
||||||
|
Если документация или тесты расходятся с этой матрицей, исправлять нужно
|
||||||
|
документацию или тестовые ожидания. Бизнес-логику менять только при отдельном
|
||||||
|
подтвержденном дефекте.
|
||||||
|
|
||||||
|
## 4. Обязательные проверки
|
||||||
|
|
||||||
|
Перед фиксацией pilot freeze candidate выполнить:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd adk-rust
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
cargo test --workspace
|
||||||
|
cargo clippy --workspace --all-targets -- -D warnings
|
||||||
|
cargo build --workspace --release
|
||||||
|
```
|
||||||
|
|
||||||
|
Из корня репозитория выполнить:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/verify_release_assets.sh dist/release-v0.2
|
||||||
|
bash scripts/check_private_config_guard.sh
|
||||||
|
bash scripts/check_production_inventory_placeholders.sh
|
||||||
|
bash scripts/check_demo_safety.sh
|
||||||
|
node scripts/pilot-validation-smoke.mjs
|
||||||
|
node scripts/detmir-pilot-demo-smoke.mjs
|
||||||
|
node scripts/detmir-portal-tabs-smoke.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
Для Windows/PowerShell слоя выполнить синтаксические и policy-safe проверки на
|
||||||
|
Windows-хосте или в CI-среде, где доступен PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-ChildItem .\windows -Filter *.ps1 -Recurse | ForEach-Object {
|
||||||
|
$tokens = $null
|
||||||
|
$errors = $null
|
||||||
|
$null = [System.Management.Automation.Language.Parser]::ParseFile(
|
||||||
|
$_.FullName,
|
||||||
|
[ref]$tokens,
|
||||||
|
[ref]$errors
|
||||||
|
)
|
||||||
|
if ($errors.Count -gt 0) { throw "$($_.FullName): $($errors[0].Message)" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Также проверить release/demo artifacts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
bash scripts/check_demo_safety.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемый результат для публичных tracked docs: нет live IP, hostnames, ФИО,
|
||||||
|
логинов сотрудников заказчика, названий подразделений заказчика, live case IDs,
|
||||||
|
runtime evidence paths, токенов и паролей. Допустимы только placeholders,
|
||||||
|
TEST-NET адреса, `.example` файлы и явно обезличенные demo fixtures.
|
||||||
|
|
||||||
|
## 5. PowerShell/Rust migration gate
|
||||||
|
|
||||||
|
Rust-primary направление сохраняется.
|
||||||
|
|
||||||
|
Это не заявление о полном удалении PowerShell. В текущем pilot freeze часть
|
||||||
|
PowerShell допустима как documented fallback/installer/support layer.
|
||||||
|
|
||||||
|
PowerShell scripts не удалять автоматически. Оставшиеся runtime/fallback пути
|
||||||
|
нельзя удалять до выполнения всех условий:
|
||||||
|
|
||||||
|
- Rust replacement имеет parity по данным и ошибкам;
|
||||||
|
- прошел burn-in на пилотном контуре;
|
||||||
|
- выполнен canary/rollback gate;
|
||||||
|
- rollback path документирован и проверен;
|
||||||
|
- Scheduled Tasks/Services больше не ссылаются на удаляемый script;
|
||||||
|
- acceptance gate явно разрешает удаление.
|
||||||
|
|
||||||
|
Installer/repair PowerShell layer можно заменять постепенно: Rust dry-run,
|
||||||
|
structured report, apply mode, затем удаление старого слоя только после
|
||||||
|
проверки ссылок и отката.
|
||||||
|
|
||||||
|
Полное удаление PowerShell не входит в текущий pilot freeze.
|
||||||
|
|
||||||
|
## 6. Demo data hygiene
|
||||||
|
|
||||||
|
Demo data, screenshots, evidence pack и отчеты не должны содержать:
|
||||||
|
|
||||||
|
- реальные IP-адреса;
|
||||||
|
- реальные hostnames;
|
||||||
|
- ФИО сотрудников заказчика;
|
||||||
|
- логины сотрудников заказчика;
|
||||||
|
- названия подразделений заказчика;
|
||||||
|
- реальные security events;
|
||||||
|
- live case IDs;
|
||||||
|
- runtime evidence paths.
|
||||||
|
|
||||||
|
Для сетевых примеров использовать только RFC 5737 TEST-NET адреса:
|
||||||
|
`192.0.2.0/24`, `198.51.100.0/24`, `203.0.113.0/24`.
|
||||||
|
|
||||||
|
Автоматический gate:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/check_demo_safety.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Gate проверяет корневой `README.md`, demo/pilot/customer-facing документы,
|
||||||
|
`docs/demo/`, `docs/fixtures/`, evidence-pack и screenshots metadata. Перед
|
||||||
|
публикацией demo-pack дополнительно нужен ручной визуальный просмотр PNG:
|
||||||
|
скрипт проверяет PNG-сигнатуру, размер и текстовые метаданные, но не выполняет
|
||||||
|
OCR содержимого изображения.
|
||||||
|
|
||||||
|
## 7. Known limitations
|
||||||
|
|
||||||
|
- Pilot v1 не является сертифицированным средством защиты информации.
|
||||||
|
- UEBA v1 rule-based и не использует ML/LLM claims.
|
||||||
|
- pfSense находится в `contract_only/readiness` контуре, если ingestion не
|
||||||
|
включен отдельным production gate.
|
||||||
|
- Некоторые PowerShell runtime/fallback пути сохраняются до завершения
|
||||||
|
burn-in/canary/rollback gate.
|
||||||
|
- Demo fixtures не являются production ingestion и не доказывают полноту
|
||||||
|
production-интеграций.
|
||||||
|
- Security candidates и derived cases являются сигналами для ручной проверки,
|
||||||
|
а не подтвержденными инцидентами.
|
||||||
|
|
||||||
|
## 8. Follow-up после freeze
|
||||||
|
|
||||||
|
- Update GitHub Actions versions / Node runtime deprecation cleanup.
|
||||||
|
|
||||||
|
Этот пункт является техническим долгом и не блокирует pilot freeze. Менять
|
||||||
|
workflow в рамках freeze можно только минимально, без изменения release
|
||||||
|
semantics и только при сохранении зеленого CI.
|
||||||
|
|
||||||
|
## 9. Freeze decision
|
||||||
|
|
||||||
|
Pilot freeze candidate можно фиксировать только если:
|
||||||
|
|
||||||
|
- обязательные проверки выполнены или documented exception внесен в release
|
||||||
|
notes;
|
||||||
|
- границы продукта в README, pilot docs и acceptance docs согласованы;
|
||||||
|
- demo data hygiene подтверждена;
|
||||||
|
- PowerShell fallback не удален без gate;
|
||||||
|
- known limitations включены в материалы пилота.
|
||||||
@@ -13,6 +13,9 @@
|
|||||||
и Executive Action Center.
|
и Executive Action Center.
|
||||||
- Есть Rust Backend, Rust Agent baseline, HTML/HTMX portal и role-based
|
- Есть Rust Backend, Rust Agent baseline, HTML/HTMX portal и role-based
|
||||||
контракты Pilot v1.
|
контракты Pilot v1.
|
||||||
|
- Rust-primary runtime зафиксирован честно: PowerShell не заявлен полностью
|
||||||
|
удаленным, оставшиеся runtime/fallback/installer/repair scripts сохраняются
|
||||||
|
как documented fallback/support layer.
|
||||||
- Подготовлен demo pack: сценарии руководителя, ИБ и расследований, синтетический
|
- Подготовлен demo pack: сценарии руководителя, ИБ и расследований, синтетический
|
||||||
demo dataset, evidence pack, пример итогового отчета и value proposition.
|
demo dataset, evidence pack, пример итогового отчета и value proposition.
|
||||||
- Подготовлены registry readiness документы без ложных заявлений о сертификации.
|
- Подготовлены registry readiness документы без ложных заявлений о сертификации.
|
||||||
@@ -57,6 +60,8 @@
|
|||||||
## Что отложено на roadmap
|
## Что отложено на roadmap
|
||||||
|
|
||||||
- Расширенные agentless providers: PowerShell, SSH, Syslog, 1C, VPN и SCUD.
|
- Расширенные agentless providers: PowerShell, SSH, Syslog, 1C, VPN и SCUD.
|
||||||
|
- Отдельная PowerShell retirement-задача после burn-in периода, canary test,
|
||||||
|
rollback plan и acceptance gate.
|
||||||
- Production validation российских ОС: Astra Linux, РЕД ОС, Альт и РОСА.
|
- Production validation российских ОС: Astra Linux, РЕД ОС, Альт и РОСА.
|
||||||
- Расширенный React/TypeScript Enterprise UI и Tauri Desktop Forensics.
|
- Расширенный React/TypeScript Enterprise UI и Tauri Desktop Forensics.
|
||||||
- Глубокие enterprise-коннекторы AD/LDAP, SIEM и корпоративных учетных систем.
|
- Глубокие enterprise-коннекторы AD/LDAP, SIEM и корпоративных учетных систем.
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ Portal smoke подтвердил:
|
|||||||
Risk -> Investigation -> Report`.
|
Risk -> Investigation -> Report`.
|
||||||
- Rust-first runtime покрывает критичные серверные helpers, DLP/worktime paths,
|
- Rust-first runtime покрывает критичные серверные helpers, DLP/worktime paths,
|
||||||
readiness, portal и собственный агент.
|
readiness, portal и собственный агент.
|
||||||
|
- Rust-first здесь означает Rust-primary направление, а не полное удаление
|
||||||
|
PowerShell: runtime/fallback/installer/repair scripts остаются
|
||||||
|
документированным слоем отката, установки и поддержки.
|
||||||
- pfSense/network perimeter оставлен optional/read-only слоем, что снижает риск
|
- pfSense/network perimeter оставлен optional/read-only слоем, что снижает риск
|
||||||
пилота и соответствует текущим ограничениям проекта.
|
пилота и соответствует текущим ограничениям проекта.
|
||||||
- Python сохранен только для оговоренных исключений, не как ядро продукта.
|
- Python сохранен только для оговоренных исключений, не как ядро продукта.
|
||||||
@@ -227,6 +230,8 @@ enterprise endpoint agent`.
|
|||||||
- Readiness bundle, checksum/signature и Prometheus/Grafana readiness ideas уже
|
- Readiness bundle, checksum/signature и Prometheus/Grafana readiness ideas уже
|
||||||
заложены в документах и тестах.
|
заложены в документах и тестах.
|
||||||
- Legacy fallback сохранен там, где это нужно для rollback.
|
- Legacy fallback сохранен там, где это нужно для rollback.
|
||||||
|
- Полное удаление PowerShell должно быть отдельной задачей после burn-in,
|
||||||
|
canary test, rollback plan и acceptance gate.
|
||||||
|
|
||||||
Слабые стороны:
|
Слабые стороны:
|
||||||
|
|
||||||
@@ -246,6 +251,7 @@ enterprise endpoint agent`.
|
|||||||
поломки старого API.
|
поломки старого API.
|
||||||
- Отсутствие JSON state файлов обрабатывается.
|
- Отсутствие JSON state файлов обрабатывается.
|
||||||
- PowerShell collector сохранен как legacy fallback, а не удален.
|
- PowerShell collector сохранен как legacy fallback, а не удален.
|
||||||
|
- Pilot freeze не включает автоматическое удаление PowerShell scripts.
|
||||||
|
|
||||||
Слабые стороны:
|
Слабые стороны:
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,10 @@
|
|||||||
## Agent сценарий
|
## Agent сценарий
|
||||||
|
|
||||||
- Rust Agent baseline описан и проверяется через документацию.
|
- Rust Agent baseline описан и проверяется через документацию.
|
||||||
|
- Rust-primary направление не трактуется как полное удаление PowerShell.
|
||||||
|
- Runtime/fallback/installer/repair PowerShell scripts остаются
|
||||||
|
documented fallback/support layer до отдельного burn-in/canary/rollback
|
||||||
|
и acceptance gate.
|
||||||
- Для пилота определены источники данных, ожидаемое покрытие и допустимая
|
- Для пилота определены источники данных, ожидаемое покрытие и допустимая
|
||||||
задержка.
|
задержка.
|
||||||
- Потеря части данных снижает confidence и фиксируется как operational gap.
|
- Потеря части данных снижает confidence и фиксируется как operational gap.
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
Дата актуализации: 2026-06-17
|
Дата актуализации: 2026-06-17
|
||||||
|
|
||||||
Документ фиксирует фактический статус оставшихся PowerShell-файлов после
|
Документ фиксирует фактический статус оставшихся PowerShell-файлов после
|
||||||
перехода на Rust-first контур. Цель матрицы: отделить рабочий runtime от
|
перехода на Rust-first контур. Это не означает, что PowerShell полностью
|
||||||
установочных действий, аварийного отката и устаревших проверочных скриптов.
|
удален. Цель матрицы: отделить рабочий runtime от установочных действий,
|
||||||
|
аварийного отката и устаревших проверочных скриптов.
|
||||||
|
|
||||||
В tracked документации не публикуются имена рабочих хостов, private IP,
|
В tracked документации не публикуются имена рабочих хостов, private IP,
|
||||||
учетные записи, токены и runtime evidence paths. Live-проверка выполнялась по
|
учетные записи, токены и runtime evidence paths. Live-проверка выполнялась по
|
||||||
@@ -90,8 +91,11 @@
|
|||||||
|
|
||||||
## Правила дальнейшей миграции
|
## Правила дальнейшей миграции
|
||||||
|
|
||||||
1. Не удалять `runtime` и `fallback` скрипты без acceptance gate и проверенного
|
Полное удаление PowerShell не входит в текущий pilot freeze. Это должна быть
|
||||||
rollback-плана.
|
отдельная задача после подтверждения Rust parity и отката.
|
||||||
|
|
||||||
|
1. Не удалять `runtime` и `fallback` скрипты без burn-in/canary/rollback gate,
|
||||||
|
acceptance gate и проверенного rollback-плана.
|
||||||
2. Любой PowerShell runtime, который остается в Scheduled Tasks или Services,
|
2. Любой PowerShell runtime, который остается в Scheduled Tasks или Services,
|
||||||
должен иметь явный владелец, причину сохранения и целевой Rust replacement.
|
должен иметь явный владелец, причину сохранения и целевой Rust replacement.
|
||||||
3. `installer`-скрипты можно заменять постепенно: сначала Rust dry-run и
|
3. `installer`-скрипты можно заменять постепенно: сначала Rust dry-run и
|
||||||
@@ -100,3 +104,7 @@
|
|||||||
Scheduled Tasks/Services и контрольного deploy/validation прогона.
|
Scheduled Tasks/Services и контрольного deploy/validation прогона.
|
||||||
5. Новые runtime-функции не добавлять на PowerShell. Для runtime, long-running
|
5. Новые runtime-функции не добавлять на PowerShell. Для runtime, long-running
|
||||||
collectors, upload, guard и validation целевой путь: Rust EXE или service.
|
collectors, upload, guard и validation целевой путь: Rust EXE или service.
|
||||||
|
6. Pilot freeze не является основанием для автоматического удаления
|
||||||
|
PowerShell layer: оставшиеся runtime/fallback пути сохраняются до
|
||||||
|
подтвержденной стабильности Rust replacement, canary-наблюдения и
|
||||||
|
документированного отката.
|
||||||
|
|||||||
@@ -128,6 +128,9 @@ git ls-files | grep -E '(^|/)secrets(/|$)|\.env$|inventory\.ini$' || true
|
|||||||
- Live inventory, live domains, private IPs, case IDs, forensic paths,
|
- Live inventory, live domains, private IPs, case IDs, forensic paths,
|
||||||
screenshots с реальными пользователями и runtime evidence не входят в Git.
|
screenshots с реальными пользователями и runtime evidence не входят в Git.
|
||||||
- Telegram runtime остается Python.
|
- Telegram runtime остается Python.
|
||||||
|
- PowerShell не считается полностью удаленным: оставшиеся
|
||||||
|
runtime/fallback/installer/repair scripts допустимы как документированный
|
||||||
|
слой отката и поддержки до отдельного burn-in/canary/rollback/acceptance gate.
|
||||||
- pfSense/network layer не входит в этот release-readiness этап.
|
- pfSense/network layer не входит в этот release-readiness этап.
|
||||||
|
|
||||||
## 8. Следующий уровень
|
## 8. Следующий уровень
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
| CI checksum/signature self-test | `.github/workflows/release-assets.yml` |
|
| CI checksum/signature self-test | `.github/workflows/release-assets.yml` |
|
||||||
| Pilot acceptance act | `docs/CUSTOMER_PILOT_ACCEPTANCE_RU.md` |
|
| Pilot acceptance act | `docs/CUSTOMER_PILOT_ACCEPTANCE_RU.md` |
|
||||||
| pfSense perimeter positioning | `docs/NETWORK_PERIMETER_PFSENSE_RU.md` |
|
| pfSense perimeter positioning | `docs/NETWORK_PERIMETER_PFSENSE_RU.md` |
|
||||||
|
| Pilot freeze gate | `docs/PILOT_FREEZE_READINESS_RU.md` |
|
||||||
|
|
||||||
## 2. Machine SBOM as GitHub Release asset
|
## 2. Machine SBOM as GitHub Release asset
|
||||||
|
|
||||||
@@ -94,6 +95,22 @@ docs/CUSTOMER_PILOT_ACCEPTANCE_RU.md
|
|||||||
Использовать для коммерческого пилота после установки и первичной настройки.
|
Использовать для коммерческого пилота после установки и первичной настройки.
|
||||||
Публичная версия должна оставаться обезличенной.
|
Публичная версия должна оставаться обезличенной.
|
||||||
|
|
||||||
|
Перед фиксацией pilot freeze candidate использовать:
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/PILOT_FREEZE_READINESS_RU.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Этот gate не добавляет функциональность. Он проверяет, что текущий main
|
||||||
|
остается в границах Pilot v1: Rust Backend, Rust Agent, HTML/HTMX Portal,
|
||||||
|
role-based views, rule-based UEBA v1, pfSense только как
|
||||||
|
contract/readiness/optional integration layer и сохраненный PowerShell
|
||||||
|
runtime/fallback до burn-in/canary/rollback gate.
|
||||||
|
|
||||||
|
Полное удаление PowerShell не входит в `release-readiness-v0.2` или текущий
|
||||||
|
pilot freeze: оставшиеся runtime/fallback/installer/repair scripts считаются
|
||||||
|
документированным слоем отката, установки и поддержки.
|
||||||
|
|
||||||
## 7. pfSense perimeter
|
## 7. pfSense perimeter
|
||||||
|
|
||||||
Документ:
|
Документ:
|
||||||
@@ -115,3 +132,6 @@ docs/NETWORK_PERIMETER_PFSENSE_RU.md
|
|||||||
- GitHub release содержит SBOM JSON/TXT, manifest, checksum и signature.
|
- GitHub release содержит SBOM JSON/TXT, manifest, checksum и signature.
|
||||||
- `CUSTOMER_PILOT_ACCEPTANCE_RU.md` заполнен для пилотного заказчика.
|
- `CUSTOMER_PILOT_ACCEPTANCE_RU.md` заполнен для пилотного заказчика.
|
||||||
- pfSense не описан как обязательный компонент продукта.
|
- pfSense не описан как обязательный компонент продукта.
|
||||||
|
- `PILOT_FREEZE_READINESS_RU.md` заполнен для pilot freeze candidate.
|
||||||
|
- PowerShell retirement не заявлен как выполненный без отдельного
|
||||||
|
burn-in/canary/rollback/acceptance gate.
|
||||||
|
|||||||
@@ -41,4 +41,7 @@ v0.3 добавляет объяснительный audit layer поверх э
|
|||||||
- Документы v0.3 не содержат live IP/domains/secrets/evidence.
|
- Документы v0.3 не содержат live IP/domains/secrets/evidence.
|
||||||
- pfSense описан только как optional integration/perimeter layer.
|
- pfSense описан только как optional integration/perimeter layer.
|
||||||
- SIEM/UEBA/DLP не заявлены как основной сертифицированный класс.
|
- SIEM/UEBA/DLP не заявлены как основной сертифицированный класс.
|
||||||
|
- Rust-primary runtime описан честно: PowerShell не заявлен полностью
|
||||||
|
удаленным, а оставшиеся runtime/fallback/installer/repair scripts сохраняются
|
||||||
|
до отдельного burn-in/canary/rollback/acceptance gate.
|
||||||
- Pilot checklist и acceptance act готовы для customer-facing заполнения.
|
- Pilot checklist и acceptance act готовы для customer-facing заполнения.
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
python3 - "$@" <<'PY'
|
||||||
|
import ipaddress
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path.cwd()
|
||||||
|
|
||||||
|
JSON_MODE = "--json" in sys.argv[1:]
|
||||||
|
|
||||||
|
TEXT_SUFFIXES = {
|
||||||
|
".csv",
|
||||||
|
".html",
|
||||||
|
".htm",
|
||||||
|
".json",
|
||||||
|
".md",
|
||||||
|
".mjs",
|
||||||
|
".sql",
|
||||||
|
".txt",
|
||||||
|
".yaml",
|
||||||
|
".yml",
|
||||||
|
}
|
||||||
|
|
||||||
|
SAFE_IP_NETWORKS = [
|
||||||
|
ipaddress.ip_network("192.0.2.0/24"),
|
||||||
|
ipaddress.ip_network("198.51.100.0/24"),
|
||||||
|
ipaddress.ip_network("203.0.113.0/24"),
|
||||||
|
]
|
||||||
|
|
||||||
|
SAFE_IPS = {
|
||||||
|
ipaddress.ip_address("0.0.0.0"),
|
||||||
|
ipaddress.ip_address("127.0.0.1"),
|
||||||
|
}
|
||||||
|
|
||||||
|
SAFE_EMAIL_DOMAINS = {
|
||||||
|
"example.com",
|
||||||
|
"example.net",
|
||||||
|
"example.org",
|
||||||
|
"invalid",
|
||||||
|
"localhost",
|
||||||
|
}
|
||||||
|
|
||||||
|
SAFE_SECRET_VALUES = {
|
||||||
|
"",
|
||||||
|
"***",
|
||||||
|
"****",
|
||||||
|
"xxxxx",
|
||||||
|
"xxxx",
|
||||||
|
"change_me",
|
||||||
|
"changeme",
|
||||||
|
"redacted",
|
||||||
|
"placeholder",
|
||||||
|
"example",
|
||||||
|
"demo",
|
||||||
|
"demo-only",
|
||||||
|
"<token>",
|
||||||
|
"<secret>",
|
||||||
|
"<password>",
|
||||||
|
"<cookie>",
|
||||||
|
}
|
||||||
|
|
||||||
|
IPV4_RE = re.compile(r"(?<![\d.])(?:\d{1,3}\.){3}\d{1,3}(?![\d.])")
|
||||||
|
EMAIL_RE = re.compile(r"\b[A-Za-z0-9._%+-]+@([A-Za-z0-9.-]+\.[A-Za-z]{2,}|localhost|invalid)\b")
|
||||||
|
FORBIDDEN_DOMAIN_RE = re.compile(
|
||||||
|
r"\b(?:dm\.iri|sevnb\.ru|msk\.sevnb\.ru|dns\.sevnb\.ru|iri1968|SHARKON2025)\b",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
SECRET_ASSIGN_RE = re.compile(
|
||||||
|
r"""(?ix)
|
||||||
|
\b(password|passwd|pwd|token|secret|api[_-]?key|bearer|cookie|session[_-]?(?:id|secret|token)?)\b
|
||||||
|
\s*[:=]\s*
|
||||||
|
["']?([^"'\s,;}]+)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
BEARER_RE = re.compile(r"\bAuthorization\s*:\s*Bearer\s+([A-Za-z0-9._~+/=-]+)", re.IGNORECASE)
|
||||||
|
WORKSTATION_RE = re.compile(r"\b(?:DESKTOP|LAPTOP|WIN|WS)-[A-Z0-9][A-Z0-9-]{3,}\b")
|
||||||
|
CYRILLIC_FIO_RE = re.compile(r"\b[А-ЯЁ][а-яё]{2,}\s+[А-ЯЁ][а-яё]{2,}\s+[А-ЯЁ][а-яё]{2,}\b")
|
||||||
|
|
||||||
|
|
||||||
|
def is_safe_ip(value: str) -> bool:
|
||||||
|
try:
|
||||||
|
ip = ipaddress.ip_address(value)
|
||||||
|
except ValueError:
|
||||||
|
return True
|
||||||
|
return ip in SAFE_IPS or any(ip in network for network in SAFE_IP_NETWORKS)
|
||||||
|
|
||||||
|
|
||||||
|
def is_safe_email(domain: str, email: str) -> bool:
|
||||||
|
normalized = domain.lower()
|
||||||
|
if normalized in SAFE_EMAIL_DOMAINS:
|
||||||
|
return True
|
||||||
|
if normalized.endswith(".example") or normalized.endswith(".invalid"):
|
||||||
|
return True
|
||||||
|
return "demo" in email.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def is_safe_secret_value(value: str) -> bool:
|
||||||
|
cleaned = value.strip().strip("\"'").strip()
|
||||||
|
lowered = cleaned.lower()
|
||||||
|
if lowered in SAFE_SECRET_VALUES:
|
||||||
|
return True
|
||||||
|
if cleaned.startswith("<") and cleaned.endswith(">"):
|
||||||
|
return True
|
||||||
|
if set(cleaned) <= {"*", "x", "X", "-"}:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def collect_scope() -> list[Path]:
|
||||||
|
files = [ROOT / "README.md"]
|
||||||
|
for path in (ROOT / "docs").rglob("*"):
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
rel = path.relative_to(ROOT).as_posix()
|
||||||
|
name = path.name
|
||||||
|
if rel.startswith(("docs/demo/", "docs/fixtures/", "docs/screenshots/", "docs/assets/screenshots/")):
|
||||||
|
files.append(path)
|
||||||
|
continue
|
||||||
|
if any(marker in name for marker in ("DEMO", "PILOT", "CUSTOMER", "RELEASE_READINESS", "FREEZE")):
|
||||||
|
files.append(path)
|
||||||
|
return sorted(set(files))
|
||||||
|
|
||||||
|
|
||||||
|
def add_finding(findings: list[dict], path: Path, line_no: int, rule: str, value: str) -> None:
|
||||||
|
findings.append(
|
||||||
|
{
|
||||||
|
"file": path.relative_to(ROOT).as_posix(),
|
||||||
|
"line": line_no,
|
||||||
|
"rule": rule,
|
||||||
|
"value": value,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def scan_line(findings: list[dict], path: Path, line_no: int, line: str) -> None:
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith("#") and "grep -n" in stripped:
|
||||||
|
return
|
||||||
|
|
||||||
|
for match in IPV4_RE.finditer(line):
|
||||||
|
value = match.group(0)
|
||||||
|
if not is_safe_ip(value):
|
||||||
|
add_finding(findings, path, line_no, "real_ipv4_not_in_demo_range", value)
|
||||||
|
|
||||||
|
for match in EMAIL_RE.finditer(line):
|
||||||
|
email = match.group(0)
|
||||||
|
domain = match.group(1)
|
||||||
|
if not is_safe_email(domain, email):
|
||||||
|
add_finding(findings, path, line_no, "non_demo_email", email)
|
||||||
|
|
||||||
|
for match in FORBIDDEN_DOMAIN_RE.finditer(line):
|
||||||
|
add_finding(findings, path, line_no, "known_live_domain_or_codename", match.group(0))
|
||||||
|
|
||||||
|
for match in SECRET_ASSIGN_RE.finditer(line):
|
||||||
|
value = match.group(2)
|
||||||
|
if not is_safe_secret_value(value):
|
||||||
|
add_finding(findings, path, line_no, "credential_like_assignment", match.group(0))
|
||||||
|
|
||||||
|
for match in BEARER_RE.finditer(line):
|
||||||
|
value = match.group(1)
|
||||||
|
if not is_safe_secret_value(value):
|
||||||
|
add_finding(findings, path, line_no, "bearer_secret", "Authorization: Bearer ...")
|
||||||
|
|
||||||
|
for match in WORKSTATION_RE.finditer(line):
|
||||||
|
value = match.group(0)
|
||||||
|
if "DEMO" not in value:
|
||||||
|
add_finding(findings, path, line_no, "realistic_workstation_name", value)
|
||||||
|
|
||||||
|
for match in CYRILLIC_FIO_RE.finditer(line):
|
||||||
|
add_finding(findings, path, line_no, "possible_cyrillic_fio", match.group(0))
|
||||||
|
|
||||||
|
|
||||||
|
def scan_text_file(path: Path) -> list[dict]:
|
||||||
|
findings: list[dict] = []
|
||||||
|
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||||
|
for line_no, line in enumerate(text.splitlines(), start=1):
|
||||||
|
scan_line(findings, path, line_no, line)
|
||||||
|
return findings
|
||||||
|
|
||||||
|
|
||||||
|
def scan_png(path: Path) -> list[dict]:
|
||||||
|
findings: list[dict] = []
|
||||||
|
data = path.read_bytes()
|
||||||
|
png_signature = b"\x89PNG\r\n\x1a\n"
|
||||||
|
if len(data) <= 1000 or not data.startswith(png_signature):
|
||||||
|
add_finding(findings, path, 0, "screenshot_not_png_or_too_small", str(len(data)))
|
||||||
|
return findings
|
||||||
|
metadata_text = data.decode("latin-1", errors="ignore")
|
||||||
|
for line_no, line in enumerate(metadata_text.splitlines(), start=1):
|
||||||
|
scan_line(findings, path, line_no, line)
|
||||||
|
return findings
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
files = collect_scope()
|
||||||
|
findings: list[dict] = []
|
||||||
|
|
||||||
|
for path in files:
|
||||||
|
if path.suffix.lower() == ".png":
|
||||||
|
findings.extend(scan_png(path))
|
||||||
|
elif path.suffix.lower() in TEXT_SUFFIXES:
|
||||||
|
findings.extend(scan_text_file(path))
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"ok": not findings,
|
||||||
|
"scope_files": [path.relative_to(ROOT).as_posix() for path in files],
|
||||||
|
"findings": findings,
|
||||||
|
}
|
||||||
|
|
||||||
|
if JSON_MODE:
|
||||||
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||||
|
elif findings:
|
||||||
|
print("Demo safety check failed:", file=sys.stderr)
|
||||||
|
for finding in findings:
|
||||||
|
print(
|
||||||
|
f"{finding['file']}:{finding['line']}: {finding['rule']}: {finding['value']}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"Demo safety check passed ({len(files)} files scanned).")
|
||||||
|
|
||||||
|
return 0 if not findings else 2
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
PY
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import process from "node:process";
|
import process from "node:process";
|
||||||
@@ -175,27 +176,37 @@ function checkRequiredContent() {
|
|||||||
return findings;
|
return findings;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkSensitiveStrings(files) {
|
function checkDemoSafety() {
|
||||||
const patterns = [
|
const result = spawnSync("bash", ["scripts/check_demo_safety.sh", "--json"], {
|
||||||
{ name: "private_network_10", re: /10\.10\.10\./ },
|
cwd: root,
|
||||||
{ name: "private_network_192", re: /192\.168\./ },
|
encoding: "utf8",
|
||||||
{ name: "private_network_172", re: /172\.(1[6-9]|2\d|3[0-1])\./ },
|
});
|
||||||
{ name: "private_operator_domain", re: /dm\.iri/i },
|
if (result.error) {
|
||||||
{ name: "customer_codename", re: new RegExp(`${["Det", "Mir"].join("")}|${["SHARKON", "2025"].join("")}`, "i") },
|
return {
|
||||||
{ name: "local_operator_path", re: /\/home\/igor/i },
|
ok: false,
|
||||||
{ name: "mts_phishing_context", re: /\b(lk|l)\.mts\.ru/i },
|
error: result.error.message,
|
||||||
];
|
findings: [],
|
||||||
const findings = [];
|
};
|
||||||
for (const file of files) {
|
|
||||||
if (!file.endsWith(".md") && !file.endsWith(".json")) continue;
|
|
||||||
const text = readText(file);
|
|
||||||
for (const pattern of patterns) {
|
|
||||||
if (pattern.re.test(text)) {
|
|
||||||
findings.push({ file, pattern: pattern.name });
|
|
||||||
}
|
}
|
||||||
|
let parsed = null;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(result.stdout || "{}");
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: `invalid_json: ${error.message}`,
|
||||||
|
stdout: result.stdout,
|
||||||
|
stderr: result.stderr,
|
||||||
|
findings: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
return {
|
||||||
return findings;
|
ok: result.status === 0 && parsed.ok === true,
|
||||||
|
status: result.status,
|
||||||
|
stderr: result.stderr,
|
||||||
|
findings: parsed.findings || [],
|
||||||
|
scope_files: parsed.scope_files || [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function pass(checks, name, ok, details = {}) {
|
function pass(checks, name, ok, details = {}) {
|
||||||
@@ -213,12 +224,6 @@ function main() {
|
|||||||
...roadmapDocs,
|
...roadmapDocs,
|
||||||
...reportsAndRunbooks,
|
...reportsAndRunbooks,
|
||||||
];
|
];
|
||||||
const sensitiveScanFiles = [
|
|
||||||
...validationDocs,
|
|
||||||
"docs/fixtures/pilot-v1-demo/README_RU.md",
|
|
||||||
"docs/fixtures/pilot-v1-demo/demo-seed-data.json",
|
|
||||||
];
|
|
||||||
|
|
||||||
pass(checks, "validation_docs_exist", checkFiles(validationDocs).length === 0, {
|
pass(checks, "validation_docs_exist", checkFiles(validationDocs).length === 0, {
|
||||||
missing: checkFiles(validationDocs),
|
missing: checkFiles(validationDocs),
|
||||||
});
|
});
|
||||||
@@ -253,9 +258,13 @@ function main() {
|
|||||||
findings: linkFindings,
|
findings: linkFindings,
|
||||||
});
|
});
|
||||||
|
|
||||||
const sensitiveFindings = checkSensitiveStrings(sensitiveScanFiles);
|
const demoSafety = checkDemoSafety();
|
||||||
pass(checks, "sensitive_string_scan", sensitiveFindings.length === 0, {
|
pass(checks, "demo_safety_scan", demoSafety.ok, {
|
||||||
findings: sensitiveFindings,
|
findings: demoSafety.findings,
|
||||||
|
scope_files: demoSafety.scope_files,
|
||||||
|
status: demoSafety.status,
|
||||||
|
stderr: demoSafety.stderr,
|
||||||
|
error: demoSafety.error,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ok = checks.every((check) => check.ok);
|
const ok = checks.every((check) => check.ok);
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ rust_candidates=()
|
|||||||
if [[ -n "$RUST_BIN" ]]; then
|
if [[ -n "$RUST_BIN" ]]; then
|
||||||
rust_candidates+=("$RUST_BIN")
|
rust_candidates+=("$RUST_BIN")
|
||||||
fi
|
fi
|
||||||
|
if [[ -n "${AW_RUS_CARGO_TARGET_DIR:-}" ]]; then
|
||||||
|
rust_candidates+=("$AW_RUS_CARGO_TARGET_DIR/release/quality-gate")
|
||||||
|
fi
|
||||||
rust_candidates+=(
|
rust_candidates+=(
|
||||||
"$TARGET_ROOT/release/quality-gate"
|
"$TARGET_ROOT/release/quality-gate"
|
||||||
"$ROOT_DIR/adk-rust/target/release/quality-gate"
|
"$ROOT_DIR/adk-rust/target/release/quality-gate"
|
||||||
@@ -93,7 +96,7 @@ fi
|
|||||||
violations=()
|
violations=()
|
||||||
for path in "${tracked_py[@]}"; do
|
for path in "${tracked_py[@]}"; do
|
||||||
case "$path" in
|
case "$path" in
|
||||||
aw-server/dlp-content-analysis/*|clickhouse-1c/ai/*|clickhouse-1c/etl/*|detmir-mcp/main.py|grafana-1c/*|pfsense/*|proxmox/tsj_guardian_bot.py|proxmox/test_tsj_guardian_bot.py)
|
aw-server/dlp-content-analysis/*|clickhouse-1c/ai/*|clickhouse-1c/etl/*|detmir-mcp/main.py|grafana-1c/*|pfsense/*|proxmox/tsj_guardian_bot.py|proxmox/test_tsj_guardian_bot.py|scripts/package_rust_release_binaries.py)
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
Reference in New Issue
Block a user