Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfeaf772ea | ||
|
|
f800c676ce | ||
|
|
73578e8388 | ||
|
|
9b4b847723 | ||
|
|
c9ad5ecca7 | ||
|
|
ced3b0fb20 | ||
|
|
5f8a25e056 | ||
|
|
66dc0a09b1 | ||
|
|
4c96879915 | ||
|
|
371a1e2728 | ||
|
|
b7209771f6 | ||
|
|
525f45e0f7 | ||
|
|
66b0b6cffb | ||
|
|
57ef5abf7e | ||
|
|
ae1909a28a | ||
|
|
064fbd05fb |
@@ -5,7 +5,10 @@ AWatch-rus - программный комплекс операционного
|
||||
корпоративной ИТ-инфраструктуры на базе ActivityWatch, Rust-сервисов
|
||||
автоматизации, 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,
|
||||
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
|
||||
runtime(для оперативного оповещения), OCR/content-analysis, 1C/AI/ETL integration и MCP/dev helpers. Эти части не являются ядром Rust-first runtime.
|
||||
|
||||
@@ -55,7 +63,8 @@ Implemented:
|
||||
Planned:
|
||||
|
||||
- Provider detail expansion under `/portal/architecture`.
|
||||
- PowerShell Provider.
|
||||
- PowerShell Provider как planned/agentless direction, не как возврат новых
|
||||
runtime-функций на PowerShell.
|
||||
- SSH Provider.
|
||||
- Syslog Provider.
|
||||
- 1C Provider как формализация текущего file-based 1C analytics направления.
|
||||
@@ -89,6 +98,7 @@ Security Analytics + Forensics для ролей `executive`, `manager`, `securi
|
||||
Pilot validation:
|
||||
|
||||
- [чеклист проверки пилота](docs/PILOT_VALIDATION_CHECKLIST_RU.md);
|
||||
- [pilot freeze readiness](docs/PILOT_FREEZE_READINESS_RU.md);
|
||||
- [gap analysis пилота](docs/PILOT_GAP_ANALYSIS_RU.md);
|
||||
- [вопросы для discovery с заказчиком](docs/CUSTOMER_DISCOVERY_QUESTIONS_RU.md);
|
||||
- [критерии успеха пилота](docs/PILOT_SUCCESS_CRITERIA_RU.md);
|
||||
@@ -98,6 +108,8 @@ Pilot validation:
|
||||
|
||||
- pfSense показывается только как `contract_only/readiness`, без заявления
|
||||
production ingestion или SIEM;
|
||||
- pfSense в текущем пилоте допускается только как contract/readiness/optional
|
||||
integration layer;
|
||||
- UEBA Score v1 является прозрачной rule-based моделью, без ML/LLM;
|
||||
- demo fixtures не содержат реальных IP-адресов, hostname, логинов, ФИО,
|
||||
подразделений заказчика или событий безопасности;
|
||||
@@ -296,3 +308,4 @@ collectors.
|
||||
- [Windows Collector Suite](docs/wiki/Windows-Collector-Suite.md)
|
||||
- [Worktime API and UI Bridge](docs/wiki/Worktime-API-and-UI-Bridge.md)
|
||||
- [Russian WebUI Patch and Localization](docs/wiki/Russian-WebUI-Patch-and-Localization.md)
|
||||
- Актуальные ссылки по этой тематике: https://www.securitylab.ru/analytics/573771.php (Как собрать ролевую модель доступа при хаосе в инфраструктуре)
|
||||
|
||||
+35
-1
@@ -22,7 +22,9 @@ scripts with durable standalone Rust modules.
|
||||
- `detmir-dlp` - SSH wrapper replacement for remote DLP health JSON collection.
|
||||
- `dlp-health-check` - AW server DLP health check replacement.
|
||||
- `aw-db-maintenance` - guarded weekly SQLite maintenance for old allowlisted
|
||||
process-level session events, with backup-before-delete.
|
||||
process-level session events, with backup-before-delete; nightly SQLite
|
||||
compaction is handled by the same binary in `--vacuum` mode and scheduled
|
||||
separately from the trim job.
|
||||
- `aw-ensure-reliability` - safe dry-run/apply planner for AW service
|
||||
reliability repair actions that were previously immediate Bash mutations.
|
||||
- `aw-linux-install` - safe dry-run/apply planner for Linux ActivityWatch
|
||||
@@ -58,6 +60,38 @@ scripts with durable standalone Rust modules.
|
||||
- `detmir-status` - read-only DetMir state normalizer with text, JSON, and ADK
|
||||
`Content` output. Also builds `detmir-adk-status` as a compatibility binary.
|
||||
|
||||
## SQLite Maintenance Safety
|
||||
|
||||
`aw-db-maintenance` has two separate modes:
|
||||
|
||||
- default trim mode removes only old allowlisted `process_start` /
|
||||
`process_stop` rows from the configured session bucket and is dry-run unless
|
||||
`--apply` is passed;
|
||||
- `--vacuum` compacts the SQLite DB with `VACUUM INTO`, checks
|
||||
`PRAGMA integrity_check`, preserves owner/mode, and replaces the DB only after
|
||||
backup and integrity success.
|
||||
|
||||
Both apply modes use `AW_DB_MAINTENANCE_LOCK_PATH` /
|
||||
`--lock-path` to block concurrent trim/VACUUM runs. VACUUM also checks the
|
||||
configured `activitywatch-server.service` through systemd, refuses unknown or
|
||||
failed unit states, stops the service before compaction, and starts it again
|
||||
through a guard on success or error.
|
||||
|
||||
Do not run VACUUM during business hours, active incident response, evidence
|
||||
collection, active backup/restore, or when the ActivityWatch service/unit state
|
||||
is unclear. Rollback is replacing the SQLite DB from
|
||||
`/var/lib/activitywatch/backups/db/aw-sqlite-before-db-vacuum-*.db` while
|
||||
`activitywatch-server.service` is stopped, then starting the service and
|
||||
checking `aw-db-health`/`detmir-status`.
|
||||
|
||||
The Ansible deploy installs the VACUUM unit files but does not enable the
|
||||
nightly timer unless `aw_db_vacuum_timer_enabled=true` is set explicitly.
|
||||
Disable it with:
|
||||
|
||||
```bash
|
||||
systemctl disable --now aw-db-vacuum.timer
|
||||
```
|
||||
|
||||
## Migration Runbook
|
||||
|
||||
Use `RUNBOOK.md` as the operational plan for replacing Python and shell modules
|
||||
|
||||
@@ -1118,6 +1118,45 @@ systemctl is-active tsj-guardian-bot tsj-guardian-watchdog gost-tg
|
||||
(`sqlite.db=359.6MiB`, WAL `4.0MiB`, session rows `174`, recent process
|
||||
events `0`, latest `eventType=logon`), DetMir status OK with
|
||||
`dlp_counts={ok:22,warn:0,fail:0}`, `ok_for_operator=true`.
|
||||
35.2. `[done]` Закрепить nightly VACUUM для AW SQLite:
|
||||
- `aw-db-maintenance` получил отдельный `--vacuum` режим; weekly trim
|
||||
остается отдельной задачей и не смешивается с compaction;
|
||||
- добавлены `aw-server/aw-db-vacuum.service` и
|
||||
`aw-server/aw-db-vacuum.timer`;
|
||||
- timer schedule: `OnCalendar=*-*-* 02:10:00`,
|
||||
`RandomizedDelaySec=10m`, `Persistent=true`; Ansible не включает timer
|
||||
без явного opt-in `aw_db_vacuum_timer_enabled=true`;
|
||||
- если opt-in не задан, `deploy_aw_server.yml` оставляет unit-файлы на
|
||||
сервере, но держит `aw-db-vacuum.timer` в `disabled/stopped`;
|
||||
- nightly job перед VACUUM останавливает `activitywatch-server.service`,
|
||||
делает rollback backup SQLite DB, выполняет `VACUUM INTO`, проверяет
|
||||
`PRAGMA integrity_check`, сохраняет владельца и режим файла и
|
||||
поднимает server обратно;
|
||||
- apply-режимы weekly trim и nightly VACUUM используют общий lock
|
||||
`/run/aw-db-maintenance.lock`, поэтому одновременный запуск завершается
|
||||
fail-closed без записи в SQLite;
|
||||
- VACUUM нельзя запускать в рабочее время, во время активного
|
||||
расследования/снятия доказательств, backup/restore, неизвестного
|
||||
состояния `activitywatch-server.service` или если уже есть maintenance
|
||||
lock;
|
||||
- dry-run проверки:
|
||||
`aw-db-maintenance --vacuum --json` и обычный
|
||||
`aw-db-maintenance --json`; они не создают backup и не пишут в DB;
|
||||
- включить timer явно:
|
||||
`ansible-playbook -i inventory.ini deploy_aw_server.yml -e aw_db_vacuum_timer_enabled=true`;
|
||||
- отключить timer:
|
||||
`systemctl disable --now aw-db-vacuum.timer`;
|
||||
- rollback: остановить `activitywatch-server.service`, заменить
|
||||
`/var/lib/activitywatch/aw-server-rust/sqlite.db` из последнего
|
||||
`/var/lib/activitywatch/backups/db/aw-sqlite-before-db-vacuum-*.db`,
|
||||
вернуть владельца/режим, запустить service, проверить `aw-db-health` и
|
||||
`detmir-status`;
|
||||
- local gates: `cargo fmt --all -- --check`, `cargo test --workspace`,
|
||||
`cargo clippy --workspace --all-targets -- -D warnings`,
|
||||
`cargo build --workspace --release`, systemd unit verify,
|
||||
`ansible-playbook -i inventory.ini deploy_aw_server.yml --syntax-check`,
|
||||
`scripts/check_detmir_rust_release_artifacts.sh`,
|
||||
`scripts/quality-gate.sh`;
|
||||
36. `[done]` Устранить blocker полного AW server deploy на Influx token:
|
||||
- проблема: `deploy_aw_server.yml` падал на assert
|
||||
`aw_worktime_influx_enabled=true`, потому что локальные env
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::fs;
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
@@ -12,6 +14,8 @@ use serde_json::Value;
|
||||
const DEFAULT_DB_PATH: &str = "/var/lib/activitywatch/aw-server-rust/sqlite.db";
|
||||
const DEFAULT_BACKUP_DIR: &str = "/var/lib/activitywatch/backups/db";
|
||||
const DEFAULT_HOST: &str = "HOST-EXAMPLE";
|
||||
const DEFAULT_SERVICE_UNIT: &str = "activitywatch-server.service";
|
||||
const DEFAULT_LOCK_PATH: &str = "/run/aw-db-maintenance.lock";
|
||||
const ALLOWED_EVENT_TYPES: &[&str] = &["process_start", "process_stop"];
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -38,6 +42,19 @@ struct Cli {
|
||||
#[arg(long)]
|
||||
apply: bool,
|
||||
|
||||
#[arg(long)]
|
||||
vacuum: bool,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
default_value = DEFAULT_SERVICE_UNIT,
|
||||
env = "AW_DB_MAINTENANCE_SERVICE_UNIT"
|
||||
)]
|
||||
service_unit: String,
|
||||
|
||||
#[arg(long, default_value = DEFAULT_LOCK_PATH, env = "AW_DB_MAINTENANCE_LOCK_PATH")]
|
||||
lock_path: PathBuf,
|
||||
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
}
|
||||
@@ -56,9 +73,52 @@ struct Report {
|
||||
planned_delete_rows: usize,
|
||||
deleted_rows: usize,
|
||||
backup_created: bool,
|
||||
lock_path: String,
|
||||
skipped_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct VacuumReport {
|
||||
apply: bool,
|
||||
generated_at_utc: String,
|
||||
db_path: String,
|
||||
service_unit: String,
|
||||
service_was_active: bool,
|
||||
service_restarted: bool,
|
||||
backup_path: Option<String>,
|
||||
backup_created: bool,
|
||||
lock_path: String,
|
||||
db_size_before_bytes: Option<u64>,
|
||||
vacuumed_path: Option<String>,
|
||||
vacuumed_size_bytes: Option<u64>,
|
||||
integrity_check: Option<String>,
|
||||
replaced_db: bool,
|
||||
skipped_reason: Option<String>,
|
||||
}
|
||||
|
||||
struct VacuumResult {
|
||||
backup_path: PathBuf,
|
||||
vacuumed_path: PathBuf,
|
||||
db_size_before_bytes: u64,
|
||||
vacuumed_size_bytes: u64,
|
||||
integrity_check: String,
|
||||
}
|
||||
|
||||
struct ServiceGuard {
|
||||
unit: String,
|
||||
was_active: bool,
|
||||
restored: bool,
|
||||
}
|
||||
|
||||
struct TempFileGuard {
|
||||
path: PathBuf,
|
||||
keep: bool,
|
||||
}
|
||||
|
||||
struct LockFileGuard {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let code = match run() {
|
||||
Ok(code) => code,
|
||||
@@ -72,11 +132,20 @@ fn main() {
|
||||
|
||||
fn run() -> Result<i32> {
|
||||
let cli = Cli::parse();
|
||||
let report = build_report(&cli)?;
|
||||
if cli.json {
|
||||
println!("{}", serde_json::to_string_pretty(&report)?);
|
||||
if cli.vacuum {
|
||||
let report = build_vacuum_report(&cli)?;
|
||||
if cli.json {
|
||||
println!("{}", serde_json::to_string_pretty(&report)?);
|
||||
} else {
|
||||
print_vacuum_text(&report);
|
||||
}
|
||||
} else {
|
||||
print_text(&report);
|
||||
let report = build_report(&cli)?;
|
||||
if cli.json {
|
||||
println!("{}", serde_json::to_string_pretty(&report)?);
|
||||
} else {
|
||||
print_text(&report);
|
||||
}
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
@@ -116,10 +185,15 @@ fn build_report(cli: &Cli) -> Result<Report> {
|
||||
let mut backup_file = None;
|
||||
let mut backup_created = false;
|
||||
let mut deleted = 0;
|
||||
let _lock_guard = if cli.apply && planned > 0 {
|
||||
Some(LockFileGuard::acquire(&cli.lock_path)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if cli.apply && planned > 0 {
|
||||
fs::create_dir_all(&cli.backup_dir)
|
||||
.with_context(|| format!("create backup dir {}", cli.backup_dir.display()))?;
|
||||
let backup = backup_path(&cli.backup_dir);
|
||||
let backup = backup_path(&cli.backup_dir, "aw-sqlite-before-db-maintenance");
|
||||
copy_sqlite_via_backup(&cli.db_path, &backup)?;
|
||||
backup_file = Some(backup);
|
||||
backup_created = true;
|
||||
@@ -139,6 +213,60 @@ fn build_report(cli: &Cli) -> Result<Report> {
|
||||
))
|
||||
}
|
||||
|
||||
fn build_vacuum_report(cli: &Cli) -> Result<VacuumReport> {
|
||||
if !cli.db_path.exists() {
|
||||
return Ok(vacuum_report(
|
||||
cli,
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some("database not found".to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
if !cli.apply {
|
||||
return Ok(vacuum_report(
|
||||
cli,
|
||||
false,
|
||||
Some(file_size(&cli.db_path)?),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some("dry-run".to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
let _lock_guard = LockFileGuard::acquire(&cli.lock_path)?;
|
||||
let mut service_guard = ServiceGuard::stop_if_active(&cli.service_unit)?;
|
||||
let service_was_active = service_guard.was_active;
|
||||
let result = vacuum_sqlite_db(&cli.db_path, &cli.backup_dir)?;
|
||||
let service_restarted = service_guard.restore()?;
|
||||
|
||||
Ok(vacuum_report(
|
||||
cli,
|
||||
true,
|
||||
Some(result.db_size_before_bytes),
|
||||
true,
|
||||
service_was_active,
|
||||
service_restarted,
|
||||
Some(result.backup_path),
|
||||
Some(result.vacuumed_path),
|
||||
Some(result.vacuumed_size_bytes),
|
||||
Some(result.integrity_check),
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn base_report(
|
||||
cli: &Cli,
|
||||
@@ -164,6 +292,40 @@ fn base_report(
|
||||
planned_delete_rows,
|
||||
deleted_rows,
|
||||
backup_created,
|
||||
lock_path: cli.lock_path.display().to_string(),
|
||||
skipped_reason,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn vacuum_report(
|
||||
cli: &Cli,
|
||||
apply: bool,
|
||||
db_size_before_bytes: Option<u64>,
|
||||
backup_created: bool,
|
||||
service_was_active: bool,
|
||||
service_restarted: bool,
|
||||
backup_path: Option<PathBuf>,
|
||||
vacuumed_path: Option<PathBuf>,
|
||||
vacuumed_size_bytes: Option<u64>,
|
||||
integrity_check: Option<String>,
|
||||
skipped_reason: Option<String>,
|
||||
) -> VacuumReport {
|
||||
VacuumReport {
|
||||
apply,
|
||||
generated_at_utc: Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
|
||||
db_path: cli.db_path.display().to_string(),
|
||||
service_unit: cli.service_unit.clone(),
|
||||
service_was_active,
|
||||
service_restarted,
|
||||
backup_path: backup_path.map(|path| path.display().to_string()),
|
||||
backup_created,
|
||||
lock_path: cli.lock_path.display().to_string(),
|
||||
db_size_before_bytes,
|
||||
vacuumed_path: vacuumed_path.map(|path| path.display().to_string()),
|
||||
vacuumed_size_bytes,
|
||||
integrity_check,
|
||||
replaced_db: apply && skipped_reason.is_none(),
|
||||
skipped_reason,
|
||||
}
|
||||
}
|
||||
@@ -237,13 +399,256 @@ fn delete_events(conn: &Connection, ids: &[i64], chunk_size: usize) -> Result<us
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
fn backup_path(backup_dir: &Path) -> PathBuf {
|
||||
fn backup_path(backup_dir: &Path, prefix: &str) -> PathBuf {
|
||||
backup_dir.join(format!(
|
||||
"aw-sqlite-before-db-maintenance-{}.db",
|
||||
"{}-{}.db",
|
||||
prefix,
|
||||
Utc::now().format("%Y%m%dT%H%M%SZ")
|
||||
))
|
||||
}
|
||||
|
||||
fn vacuum_sqlite_db(db_path: &Path, backup_dir: &Path) -> Result<VacuumResult> {
|
||||
fs::create_dir_all(backup_dir)
|
||||
.with_context(|| format!("create backup dir {}", backup_dir.display()))?;
|
||||
let db_size_before_bytes = file_size(db_path)?;
|
||||
let backup_path = backup_path(backup_dir, "aw-sqlite-before-db-vacuum");
|
||||
copy_sqlite_via_backup(db_path, &backup_path)?;
|
||||
let vacuumed_path = vacuumed_path(db_path)?;
|
||||
let mut vacuum_cleanup = TempFileGuard::new(vacuumed_path.clone());
|
||||
vacuum_into(db_path, &vacuumed_path)?;
|
||||
preserve_sqlite_metadata(db_path, &vacuumed_path)?;
|
||||
let vacuumed_size_bytes = file_size(&vacuumed_path)?;
|
||||
let integrity_check = integrity_check(&vacuumed_path)?;
|
||||
remove_sqlite_sidecars(db_path)?;
|
||||
fs::rename(&vacuumed_path, db_path).with_context(|| {
|
||||
format!(
|
||||
"replace {} with {}",
|
||||
db_path.display(),
|
||||
vacuumed_path.display()
|
||||
)
|
||||
})?;
|
||||
vacuum_cleanup.disarm();
|
||||
Ok(VacuumResult {
|
||||
backup_path,
|
||||
vacuumed_path,
|
||||
db_size_before_bytes,
|
||||
vacuumed_size_bytes,
|
||||
integrity_check,
|
||||
})
|
||||
}
|
||||
|
||||
fn vacuum_into(src: &Path, dst: &Path) -> Result<()> {
|
||||
let conn = open_connection(src, true)?;
|
||||
let sql = format!("VACUUM INTO {}", sqlite_string_literal(dst));
|
||||
conn.execute_batch(&sql)
|
||||
.with_context(|| format!("VACUUM INTO {}", dst.display()))
|
||||
}
|
||||
|
||||
fn integrity_check(path: &Path) -> Result<String> {
|
||||
let conn = open_connection(path, false)?;
|
||||
let result: String = conn.query_row("PRAGMA integrity_check", [], |row| row.get(0))?;
|
||||
if result != "ok" {
|
||||
bail!("integrity_check failed for {}: {result}", path.display());
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn remove_sqlite_sidecars(db_path: &Path) -> Result<()> {
|
||||
for suffix in ["-wal", "-shm", "-journal"] {
|
||||
let sidecar = sqlite_sidecar_path(db_path, suffix)?;
|
||||
match fs::remove_file(&sidecar) {
|
||||
Ok(()) => {}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(err) => return Err(err).with_context(|| format!("remove {}", sidecar.display())),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sqlite_sidecar_path(db_path: &Path, suffix: &str) -> Result<PathBuf> {
|
||||
let file_name = db_path
|
||||
.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.context("database path must have a file name")?;
|
||||
Ok(db_path.with_file_name(format!("{file_name}{suffix}")))
|
||||
}
|
||||
|
||||
fn vacuumed_path(db_path: &Path) -> Result<PathBuf> {
|
||||
let file_name = db_path
|
||||
.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.context("database path must have a file name")?;
|
||||
Ok(db_path.with_file_name(format!(
|
||||
"{file_name}.vacuumed-{}",
|
||||
Utc::now().format("%Y%m%dT%H%M%SZ")
|
||||
)))
|
||||
}
|
||||
|
||||
fn file_size(path: &Path) -> Result<u64> {
|
||||
Ok(fs::metadata(path)
|
||||
.with_context(|| format!("stat {}", path.display()))?
|
||||
.len())
|
||||
}
|
||||
|
||||
fn sqlite_string_literal(path: &Path) -> String {
|
||||
format!("'{}'", path.display().to_string().replace('\'', "''"))
|
||||
}
|
||||
|
||||
fn preserve_sqlite_metadata(src: &Path, dst: &Path) -> Result<()> {
|
||||
let metadata = fs::metadata(src).with_context(|| format!("stat {}", src.display()))?;
|
||||
let permissions = metadata.permissions();
|
||||
fs::set_permissions(dst, permissions)
|
||||
.with_context(|| format!("preserve permissions for {}", dst.display()))?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
let dst_metadata = fs::metadata(dst).with_context(|| format!("stat {}", dst.display()))?;
|
||||
if dst_metadata.uid() != metadata.uid() || dst_metadata.gid() != metadata.gid() {
|
||||
let status = Command::new("chown")
|
||||
.arg(format!("{}:{}", metadata.uid(), metadata.gid()))
|
||||
.arg(dst)
|
||||
.status()
|
||||
.context("run chown for vacuumed SQLite DB")?;
|
||||
if !status.success() {
|
||||
bail!("chown failed for {}", dst.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn systemctl_is_active(unit: &str) -> Result<bool> {
|
||||
let load_state = systemctl_load_state(unit)?;
|
||||
if load_state != "loaded" {
|
||||
bail!("refusing SQLite VACUUM because systemd unit {unit} load_state={load_state:?}");
|
||||
}
|
||||
let output = Command::new("systemctl")
|
||||
.args(["is-active", unit])
|
||||
.output()
|
||||
.with_context(|| format!("systemctl is-active {unit}"))?;
|
||||
let state = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if output.status.success() && state == "active" {
|
||||
return Ok(true);
|
||||
}
|
||||
if output.status.code() == Some(3) && state == "inactive" {
|
||||
return Ok(false);
|
||||
}
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
bail!(
|
||||
"refusing SQLite VACUUM because systemctl is-active {unit} returned state={state:?}, status={}, stderr={stderr:?}",
|
||||
output.status
|
||||
);
|
||||
}
|
||||
|
||||
fn systemctl_load_state(unit: &str) -> Result<String> {
|
||||
let output = Command::new("systemctl")
|
||||
.args(["show", "-p", "LoadState", "--value", unit])
|
||||
.output()
|
||||
.with_context(|| format!("systemctl show LoadState {unit}"))?;
|
||||
let state = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if output.status.success() && !state.is_empty() {
|
||||
return Ok(state);
|
||||
}
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
bail!(
|
||||
"refusing SQLite VACUUM because systemctl show LoadState {unit} failed with status={}, stderr={stderr:?}",
|
||||
output.status
|
||||
);
|
||||
}
|
||||
|
||||
fn systemctl_action(action: &str, unit: &str) -> Result<()> {
|
||||
let status = Command::new("systemctl")
|
||||
.args([action, unit])
|
||||
.status()
|
||||
.with_context(|| format!("systemctl {action} {unit}"))?;
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("systemctl {action} {unit} failed with status {status}");
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceGuard {
|
||||
fn stop_if_active(unit: &str) -> Result<Self> {
|
||||
let was_active = systemctl_is_active(unit)?;
|
||||
if was_active {
|
||||
systemctl_action("stop", unit)?;
|
||||
}
|
||||
Ok(Self {
|
||||
unit: unit.to_string(),
|
||||
was_active,
|
||||
restored: !was_active,
|
||||
})
|
||||
}
|
||||
|
||||
fn restore(&mut self) -> Result<bool> {
|
||||
if self.was_active && !self.restored {
|
||||
systemctl_action("start", &self.unit)?;
|
||||
self.restored = true;
|
||||
}
|
||||
Ok(self.was_active)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ServiceGuard {
|
||||
fn drop(&mut self) {
|
||||
if self.was_active && !self.restored {
|
||||
let _ = systemctl_action("start", &self.unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TempFileGuard {
|
||||
fn new(path: PathBuf) -> Self {
|
||||
Self { path, keep: false }
|
||||
}
|
||||
|
||||
fn disarm(&mut self) {
|
||||
self.keep = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempFileGuard {
|
||||
fn drop(&mut self) {
|
||||
if !self.keep {
|
||||
let _ = fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LockFileGuard {
|
||||
fn acquire(path: &Path) -> Result<Self> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("create lock parent {}", parent.display()))?;
|
||||
}
|
||||
let mut file = match OpenOptions::new().write(true).create_new(true).open(path) {
|
||||
Ok(file) => file,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||
bail!("maintenance lock already exists: {}", path.display());
|
||||
}
|
||||
Err(err) => return Err(err).with_context(|| format!("create lock {}", path.display())),
|
||||
};
|
||||
writeln!(
|
||||
file,
|
||||
"pid={} generated_at_utc={}",
|
||||
std::process::id(),
|
||||
Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true)
|
||||
)
|
||||
.with_context(|| format!("write lock {}", path.display()))?;
|
||||
Ok(Self {
|
||||
path: path.to_path_buf(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LockFileGuard {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_text(report: &Report) {
|
||||
println!(
|
||||
"aw-db-maintenance: {}",
|
||||
@@ -255,6 +660,7 @@ fn print_text(report: &Report) {
|
||||
println!("planned_delete_rows: {}", report.planned_delete_rows);
|
||||
println!("deleted_rows: {}", report.deleted_rows);
|
||||
println!("backup_created: {}", report.backup_created);
|
||||
println!("lock_path: {}", report.lock_path);
|
||||
if let Some(path) = &report.backup_path {
|
||||
println!("backup_path: {path}");
|
||||
}
|
||||
@@ -263,6 +669,38 @@ fn print_text(report: &Report) {
|
||||
}
|
||||
}
|
||||
|
||||
fn print_vacuum_text(report: &VacuumReport) {
|
||||
println!(
|
||||
"aw-db-vacuum: {}",
|
||||
if report.apply { "apply" } else { "dry-run" }
|
||||
);
|
||||
println!("db_path: {}", report.db_path);
|
||||
println!("service_unit: {}", report.service_unit);
|
||||
println!("service_was_active: {}", report.service_was_active);
|
||||
println!("service_restarted: {}", report.service_restarted);
|
||||
println!("backup_created: {}", report.backup_created);
|
||||
println!("lock_path: {}", report.lock_path);
|
||||
if let Some(path) = &report.backup_path {
|
||||
println!("backup_path: {path}");
|
||||
}
|
||||
if let Some(size) = report.db_size_before_bytes {
|
||||
println!("db_size_before_bytes: {size}");
|
||||
}
|
||||
if let Some(path) = &report.vacuumed_path {
|
||||
println!("vacuumed_path: {path}");
|
||||
}
|
||||
if let Some(size) = report.vacuumed_size_bytes {
|
||||
println!("vacuumed_size_bytes: {size}");
|
||||
}
|
||||
if let Some(check) = &report.integrity_check {
|
||||
println!("integrity_check: {check}");
|
||||
}
|
||||
println!("replaced_db: {}", report.replaced_db);
|
||||
if let Some(reason) = &report.skipped_reason {
|
||||
println!("skipped_reason: {reason}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -290,6 +728,9 @@ mod tests {
|
||||
retention_days: 7,
|
||||
chunk_size: 100,
|
||||
apply: false,
|
||||
vacuum: false,
|
||||
service_unit: DEFAULT_SERVICE_UNIT.to_string(),
|
||||
lock_path: dir.path().join("maintenance.lock"),
|
||||
json: true,
|
||||
};
|
||||
let report = build_report(&cli).unwrap();
|
||||
@@ -312,6 +753,9 @@ mod tests {
|
||||
retention_days: 7,
|
||||
chunk_size: 1,
|
||||
apply: true,
|
||||
vacuum: false,
|
||||
service_unit: DEFAULT_SERVICE_UNIT.to_string(),
|
||||
lock_path: dir.path().join("maintenance.lock"),
|
||||
json: true,
|
||||
};
|
||||
let report = build_report(&cli).unwrap();
|
||||
@@ -321,6 +765,73 @@ mod tests {
|
||||
assert_eq!(count_events(&db), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vacuum_apply_compacts_database_and_preserves_rows() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let db = dir.path().join("sqlite.db");
|
||||
create_vacuum_fixture_db(&db);
|
||||
|
||||
let before = file_size(&db).unwrap();
|
||||
let result = vacuum_sqlite_db(&db, dir.path()).unwrap();
|
||||
let after = file_size(&db).unwrap();
|
||||
|
||||
assert!(result.vacuumed_size_bytes < result.db_size_before_bytes);
|
||||
assert!(after < before);
|
||||
assert_eq!(result.integrity_check, "ok");
|
||||
assert!(result.backup_path.exists());
|
||||
assert_eq!(count_rows(&db), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vacuum_dry_run_skips_mutation() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let db = dir.path().join("sqlite.db");
|
||||
create_vacuum_fixture_db(&db);
|
||||
let cli = Cli {
|
||||
db_path: db.clone(),
|
||||
backup_dir: dir.path().join("backups"),
|
||||
session_bucket: None,
|
||||
host: None,
|
||||
retention_days: 7,
|
||||
chunk_size: 100,
|
||||
apply: false,
|
||||
vacuum: true,
|
||||
service_unit: DEFAULT_SERVICE_UNIT.to_string(),
|
||||
lock_path: dir.path().join("maintenance.lock"),
|
||||
json: true,
|
||||
};
|
||||
let report = build_vacuum_report(&cli).unwrap();
|
||||
assert!(!report.backup_created);
|
||||
assert!(!report.replaced_db);
|
||||
assert_eq!(report.skipped_reason.as_deref(), Some("dry-run"));
|
||||
assert_eq!(count_rows(&db), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_refuses_when_lock_exists() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let db = dir.path().join("aw.db");
|
||||
create_fixture_db(&db);
|
||||
let lock_path = dir.path().join("maintenance.lock");
|
||||
fs::write(&lock_path, "busy").unwrap();
|
||||
let cli = Cli {
|
||||
db_path: db.clone(),
|
||||
backup_dir: dir.path().join("backups"),
|
||||
session_bucket: Some("aw-session-events_TEST".to_string()),
|
||||
host: None,
|
||||
retention_days: 7,
|
||||
chunk_size: 1,
|
||||
apply: true,
|
||||
vacuum: false,
|
||||
service_unit: DEFAULT_SERVICE_UNIT.to_string(),
|
||||
lock_path,
|
||||
json: true,
|
||||
};
|
||||
let err = build_report(&cli).unwrap_err().to_string();
|
||||
assert!(err.contains("maintenance lock already exists"));
|
||||
assert_eq!(count_events(&db), 3);
|
||||
}
|
||||
|
||||
fn create_fixture_db(path: &Path) {
|
||||
let conn = Connection::open(path).unwrap();
|
||||
conn.execute_batch(
|
||||
@@ -350,10 +861,34 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn create_vacuum_fixture_db(path: &Path) {
|
||||
let conn = Connection::open(path).unwrap();
|
||||
conn.execute_batch(
|
||||
"create table items (id integer primary key autoincrement, payload text);",
|
||||
)
|
||||
.unwrap();
|
||||
let payload = "x".repeat(4096);
|
||||
for _ in 0..64 {
|
||||
conn.execute("insert into items (payload) values (?1)", [&payload])
|
||||
.unwrap();
|
||||
}
|
||||
for id in 1..=32 {
|
||||
conn.execute("delete from items where id = ?1", [id])
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn count_events(path: &Path) -> i64 {
|
||||
Connection::open(path)
|
||||
.unwrap()
|
||||
.query_row("select count(*) from events", [], |row| row.get(0))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn count_rows(path: &Path) -> i64 {
|
||||
Connection::open(path)
|
||||
.unwrap()
|
||||
.query_row("select count(*) from items", [], |row| row.get(0))
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ mod risk_narrative;
|
||||
mod role_access;
|
||||
mod snapshot_cache;
|
||||
mod static_assets;
|
||||
mod telemetry_ingest;
|
||||
mod workforce_kpi_explain;
|
||||
|
||||
use api_contracts::api_contract_summary;
|
||||
@@ -51,8 +52,8 @@ use path_query::{
|
||||
use portal_roles::PortalRole;
|
||||
use production::{
|
||||
build_healthz, build_readyz, build_version, is_limited_api_route, mark_request_started,
|
||||
record_ingestion_accepted, record_ingestion_rejected, record_report_generated,
|
||||
render_prometheus_metrics, validate_api_query_limits, validate_portal_config,
|
||||
record_report_generated, render_prometheus_metrics, validate_api_query_limits,
|
||||
validate_portal_config,
|
||||
};
|
||||
use readiness_api::{readiness_bundle, readiness_latest, readiness_verify};
|
||||
use risk_narrative::{
|
||||
@@ -65,6 +66,12 @@ use snapshot_cache::{
|
||||
use static_assets::{
|
||||
API_CONTRACT_OPENAPI, API_CONTRACT_TYPESCRIPT, APP_CSS, APP_JS, ARCHITECTURE_HTML, INDEX_HTML,
|
||||
};
|
||||
#[cfg(test)]
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use telemetry_ingest::{
|
||||
apply_telemetry_ingest, telemetry_authorized, validate_telemetry_payload,
|
||||
};
|
||||
pub(crate) use telemetry_ingest::{bearer_token, constant_time_eq, handle_telemetry_ingest};
|
||||
use workforce_kpi_explain::{KpiExplainQuery, build_workforce_kpi_explain};
|
||||
|
||||
const UEBA_BASELINE_MIN_SAMPLES: usize = 3;
|
||||
@@ -8558,7 +8565,7 @@ fn handle_incident_action(mut request: Request, args: &Cli) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_limited_body(request: &mut Request, limit: u64) -> Result<String> {
|
||||
pub(crate) fn read_limited_body(request: &mut Request, limit: u64) -> Result<String> {
|
||||
let mut body = String::new();
|
||||
request
|
||||
.as_reader()
|
||||
@@ -8570,12 +8577,12 @@ fn read_limited_body(request: &mut Request, limit: u64) -> Result<String> {
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
fn is_payload_too_large(err: &anyhow::Error) -> bool {
|
||||
pub(crate) fn is_payload_too_large(err: &anyhow::Error) -> bool {
|
||||
err.to_string()
|
||||
.contains("request payload exceeds configured limit")
|
||||
}
|
||||
|
||||
fn respond_payload_too_large(request: Request) -> Result<()> {
|
||||
pub(crate) fn respond_payload_too_large(request: Request) -> Result<()> {
|
||||
respond_json_status(
|
||||
request,
|
||||
StatusCode(413),
|
||||
@@ -8734,130 +8741,6 @@ fn handle_case_details(
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_telemetry_ingest(mut request: Request, args: &Cli) -> Result<()> {
|
||||
if !telemetry_authorized(&request, args) {
|
||||
record_ingestion_rejected();
|
||||
return respond_json_status(
|
||||
request,
|
||||
StatusCode(401),
|
||||
&json!({
|
||||
"ok": false,
|
||||
"error": "telemetry api key is missing or invalid"
|
||||
}),
|
||||
);
|
||||
}
|
||||
let telemetry_limit = args.max_request_body_bytes.min(1024 * 1024);
|
||||
let body = match read_limited_body(&mut request, telemetry_limit) {
|
||||
Ok(body) => body,
|
||||
Err(err) if is_payload_too_large(&err) => {
|
||||
record_ingestion_rejected();
|
||||
return respond_payload_too_large(request);
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
let response = apply_telemetry_ingest(args, &body);
|
||||
match response {
|
||||
Ok(response) => {
|
||||
record_ingestion_accepted();
|
||||
respond_json(request, &response)
|
||||
}
|
||||
Err(err) => {
|
||||
record_ingestion_rejected();
|
||||
respond_json_status(
|
||||
request,
|
||||
StatusCode(400),
|
||||
&json!({
|
||||
"ok": false,
|
||||
"error": err.to_string()
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_telemetry_ingest(args: &Cli, body: &str) -> Result<Value> {
|
||||
let payload: Value =
|
||||
serde_json::from_str(body).map_err(|err| anyhow!("invalid telemetry JSON: {err}"))?;
|
||||
validate_telemetry_payload(&payload)?;
|
||||
let received_at_utc = now();
|
||||
let envelope = json!({
|
||||
"received_at_utc": received_at_utc,
|
||||
"prototype": true,
|
||||
"record": payload,
|
||||
});
|
||||
if let Some(parent) = args.telemetry_store_path.parent() {
|
||||
fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
|
||||
}
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&args.telemetry_store_path)
|
||||
.with_context(|| format!("open {}", args.telemetry_store_path.display()))?;
|
||||
writeln!(file, "{}", serde_json::to_string(&envelope)?)
|
||||
.with_context(|| format!("append {}", args.telemetry_store_path.display()))?;
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"prototype": true,
|
||||
"stored": "file-backed-jsonl",
|
||||
"received_at_utc": received_at_utc,
|
||||
}))
|
||||
}
|
||||
|
||||
fn validate_telemetry_payload(payload: &Value) -> Result<()> {
|
||||
let Some(object) = payload.as_object() else {
|
||||
return Err(anyhow!("telemetry payload must be a JSON object"));
|
||||
};
|
||||
for field in [
|
||||
"agent_id",
|
||||
"hostname",
|
||||
"os_name",
|
||||
"os_version",
|
||||
"platform",
|
||||
"username",
|
||||
"timestamp",
|
||||
"uptime_seconds",
|
||||
"cpu_usage_percent",
|
||||
"memory_total",
|
||||
"memory_used",
|
||||
"active_sessions",
|
||||
"rdp_sessions",
|
||||
"ssh_sessions",
|
||||
"processes",
|
||||
"network_interfaces",
|
||||
"network_connections",
|
||||
"workforce_activity",
|
||||
"security_events",
|
||||
"collector_version",
|
||||
] {
|
||||
if !object.contains_key(field) {
|
||||
return Err(anyhow!("telemetry field is missing: {field}"));
|
||||
}
|
||||
}
|
||||
for field in [
|
||||
"active_sessions",
|
||||
"rdp_sessions",
|
||||
"ssh_sessions",
|
||||
"processes",
|
||||
"network_interfaces",
|
||||
"network_connections",
|
||||
"security_events",
|
||||
] {
|
||||
if payload.get(field).and_then(Value::as_array).is_none() {
|
||||
return Err(anyhow!("telemetry field must be an array: {field}"));
|
||||
}
|
||||
}
|
||||
if payload
|
||||
.get("workforce_activity")
|
||||
.and_then(Value::as_object)
|
||||
.is_none()
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"telemetry field must be an object: workforce_activity"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_incident_action(args: &Cli, actor: &str, body: &str) -> Result<IncidentActionResponse> {
|
||||
let action: IncidentActionRequest =
|
||||
serde_json::from_str(body).map_err(|err| anyhow!("invalid incident action JSON: {err}"))?;
|
||||
@@ -9968,47 +9851,6 @@ fn upload_authorized(request: &Request, args: &Cli) -> bool {
|
||||
constant_time_eq(actual.as_bytes(), expected.as_bytes())
|
||||
}
|
||||
|
||||
fn telemetry_authorized(request: &Request, args: &Cli) -> bool {
|
||||
let expected = args.telemetry_api_key.trim();
|
||||
if expected.is_empty() || expected == "change-me" {
|
||||
return false;
|
||||
}
|
||||
let actual = request
|
||||
.headers()
|
||||
.iter()
|
||||
.find(|header| header.field.equiv("x-api-key"))
|
||||
.map(|header| header.value.as_str().trim().to_string())
|
||||
.or_else(|| bearer_token(request));
|
||||
actual
|
||||
.as_deref()
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| constant_time_eq(value.as_bytes(), expected.as_bytes()))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn bearer_token(request: &Request) -> Option<String> {
|
||||
request
|
||||
.headers()
|
||||
.iter()
|
||||
.find(|header| header.field.equiv("Authorization"))
|
||||
.map(|header| header.value.as_str().trim())
|
||||
.and_then(|value| value.strip_prefix("Bearer "))
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToString::to_string)
|
||||
}
|
||||
|
||||
fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
|
||||
if left.len() != right.len() {
|
||||
return false;
|
||||
}
|
||||
let mut diff = 0_u8;
|
||||
for (a, b) in left.iter().zip(right.iter()) {
|
||||
diff |= a ^ b;
|
||||
}
|
||||
diff == 0
|
||||
}
|
||||
|
||||
fn request_actor(request: &Request) -> String {
|
||||
for name in ["X-Remote-User", "X-Gateway-User", "Remote-User"] {
|
||||
if let Some(value) = request
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
//! Telemetry ingest API for Rust endpoint/agent diagnostics.
|
||||
//!
|
||||
//! CONTRACT: this module owns `/api/telemetry` authentication, request-body
|
||||
//! validation and JSONL append semantics. Keep accepted fields, status codes,
|
||||
//! metrics counters and response shape stable unless telemetry contracts are
|
||||
//! updated in the same PR.
|
||||
|
||||
use std::fs;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use serde_json::{Value, json};
|
||||
use tiny_http::{Request, StatusCode};
|
||||
|
||||
use crate::production::{record_ingestion_accepted, record_ingestion_rejected};
|
||||
use crate::{
|
||||
Cli, is_payload_too_large, now, read_limited_body, respond_json, respond_json_status,
|
||||
respond_payload_too_large,
|
||||
};
|
||||
|
||||
pub(crate) fn telemetry_authorized(request: &Request, args: &Cli) -> bool {
|
||||
let expected = args.telemetry_api_key.trim();
|
||||
if expected.is_empty() || expected == "change-me" {
|
||||
return false;
|
||||
}
|
||||
let actual = request
|
||||
.headers()
|
||||
.iter()
|
||||
.find(|header| header.field.equiv("x-api-key"))
|
||||
.map(|header| header.value.as_str().trim().to_string())
|
||||
.or_else(|| bearer_token(request));
|
||||
actual
|
||||
.as_deref()
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| constant_time_eq(value.as_bytes(), expected.as_bytes()))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub(crate) fn bearer_token(request: &Request) -> Option<String> {
|
||||
request
|
||||
.headers()
|
||||
.iter()
|
||||
.find(|header| header.field.equiv("Authorization"))
|
||||
.map(|header| header.value.as_str().trim())
|
||||
.and_then(|value| value.strip_prefix("Bearer "))
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToString::to_string)
|
||||
}
|
||||
|
||||
pub(crate) fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
|
||||
if left.len() != right.len() {
|
||||
return false;
|
||||
}
|
||||
let mut diff = 0_u8;
|
||||
for (a, b) in left.iter().zip(right.iter()) {
|
||||
diff |= a ^ b;
|
||||
}
|
||||
diff == 0
|
||||
}
|
||||
|
||||
pub(crate) fn handle_telemetry_ingest(mut request: Request, args: &Cli) -> Result<()> {
|
||||
if !telemetry_authorized(&request, args) {
|
||||
record_ingestion_rejected();
|
||||
return respond_json_status(
|
||||
request,
|
||||
StatusCode(401),
|
||||
&json!({
|
||||
"ok": false,
|
||||
"error": "telemetry api key is missing or invalid"
|
||||
}),
|
||||
);
|
||||
}
|
||||
let telemetry_limit = args.max_request_body_bytes.min(1024 * 1024);
|
||||
let body = match read_limited_body(&mut request, telemetry_limit) {
|
||||
Ok(body) => body,
|
||||
Err(err) if is_payload_too_large(&err) => {
|
||||
record_ingestion_rejected();
|
||||
return respond_payload_too_large(request);
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
let response = apply_telemetry_ingest(args, &body);
|
||||
match response {
|
||||
Ok(response) => {
|
||||
record_ingestion_accepted();
|
||||
respond_json(request, &response)
|
||||
}
|
||||
Err(err) => {
|
||||
record_ingestion_rejected();
|
||||
respond_json_status(
|
||||
request,
|
||||
StatusCode(400),
|
||||
&json!({
|
||||
"ok": false,
|
||||
"error": err.to_string()
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn apply_telemetry_ingest(args: &Cli, body: &str) -> Result<Value> {
|
||||
let payload: Value =
|
||||
serde_json::from_str(body).map_err(|err| anyhow!("invalid telemetry JSON: {err}"))?;
|
||||
validate_telemetry_payload(&payload)?;
|
||||
let received_at_utc = now();
|
||||
let envelope = json!({
|
||||
"received_at_utc": received_at_utc,
|
||||
"prototype": true,
|
||||
"record": payload,
|
||||
});
|
||||
if let Some(parent) = args.telemetry_store_path.parent() {
|
||||
fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
|
||||
}
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&args.telemetry_store_path)
|
||||
.with_context(|| format!("open {}", args.telemetry_store_path.display()))?;
|
||||
writeln!(file, "{}", serde_json::to_string(&envelope)?)
|
||||
.with_context(|| format!("append {}", args.telemetry_store_path.display()))?;
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"prototype": true,
|
||||
"stored": "file-backed-jsonl",
|
||||
"received_at_utc": received_at_utc,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn validate_telemetry_payload(payload: &Value) -> Result<()> {
|
||||
let Some(object) = payload.as_object() else {
|
||||
return Err(anyhow!("telemetry payload must be a JSON object"));
|
||||
};
|
||||
for field in [
|
||||
"agent_id",
|
||||
"hostname",
|
||||
"os_name",
|
||||
"os_version",
|
||||
"platform",
|
||||
"username",
|
||||
"timestamp",
|
||||
"uptime_seconds",
|
||||
"cpu_usage_percent",
|
||||
"memory_total",
|
||||
"memory_used",
|
||||
"active_sessions",
|
||||
"rdp_sessions",
|
||||
"ssh_sessions",
|
||||
"processes",
|
||||
"network_interfaces",
|
||||
"network_connections",
|
||||
"workforce_activity",
|
||||
"security_events",
|
||||
"collector_version",
|
||||
] {
|
||||
if !object.contains_key(field) {
|
||||
return Err(anyhow!("telemetry field is missing: {field}"));
|
||||
}
|
||||
}
|
||||
for field in [
|
||||
"active_sessions",
|
||||
"rdp_sessions",
|
||||
"ssh_sessions",
|
||||
"processes",
|
||||
"network_interfaces",
|
||||
"network_connections",
|
||||
"security_events",
|
||||
] {
|
||||
if payload.get(field).and_then(Value::as_array).is_none() {
|
||||
return Err(anyhow!("telemetry field must be an array: {field}"));
|
||||
}
|
||||
}
|
||||
if payload
|
||||
.get("workforce_activity")
|
||||
.and_then(Value::as_object)
|
||||
.is_none()
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"telemetry field must be an object: workforce_activity"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -285,6 +285,7 @@ fn is_allowed_python_runtime_path(rel: &str) -> bool {
|
||||
|| rel.starts_with("pfsense/")
|
||||
|| rel == "proxmox/tsj_guardian_bot.py"
|
||||
|| rel == "proxmox/test_tsj_guardian_bot.py"
|
||||
|| rel == "scripts/package_rust_release_binaries.py"
|
||||
}
|
||||
|
||||
fn is_detmir_retired_runtime_path(rel: &str) -> bool {
|
||||
@@ -344,6 +345,9 @@ mod tests {
|
||||
assert!(is_allowed_python_runtime_path(
|
||||
"pfsense/pfsense-aw-poller.py"
|
||||
));
|
||||
assert!(is_allowed_python_runtime_path(
|
||||
"scripts/package_rust_release_binaries.py"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
aw_worktime_classes: "{{ lookup('file', aw_repo_root + '/aw-server/settings/classes-worktime.json') | from_json }}"
|
||||
aw_default_views: "{{ lookup('file', aw_repo_root + '/aw-server/settings/views-default.json') | from_json }}"
|
||||
aw_rust_release_dir: "{{ (lookup('env', 'CARGO_TARGET_DIR') | default(aw_repo_root + '/adk-rust/target', true)) + '/release' }}"
|
||||
aw_db_vacuum_timer_enabled: false
|
||||
|
||||
tasks:
|
||||
- name: Установить базовые пакеты
|
||||
@@ -589,6 +590,44 @@
|
||||
daemon_reload: true
|
||||
when: aw_db_maintenance_rust_binary_early.stat.exists | default(false)
|
||||
|
||||
- name: Установить aw-db-vacuum service до Influx проверок
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/aw-db-vacuum.service"
|
||||
dest: /etc/systemd/system/aw-db-vacuum.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
when: aw_db_maintenance_rust_binary_early.stat.exists | default(false)
|
||||
|
||||
- name: Установить aw-db-vacuum timer до Influx проверок
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/aw-db-vacuum.timer"
|
||||
dest: /etc/systemd/system/aw-db-vacuum.timer
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
when: aw_db_maintenance_rust_binary_early.stat.exists | default(false)
|
||||
|
||||
- name: Включить nightly aw-db-vacuum timer до Influx проверок
|
||||
ansible.builtin.systemd:
|
||||
name: aw-db-vacuum.timer
|
||||
enabled: true
|
||||
state: started
|
||||
daemon_reload: true
|
||||
when:
|
||||
- aw_db_maintenance_rust_binary_early.stat.exists | default(false)
|
||||
- aw_db_vacuum_timer_enabled | bool
|
||||
|
||||
- name: Отключить nightly aw-db-vacuum timer если opt-in не задан
|
||||
ansible.builtin.systemd:
|
||||
name: aw-db-vacuum.timer
|
||||
enabled: false
|
||||
state: stopped
|
||||
daemon_reload: true
|
||||
when:
|
||||
- aw_db_maintenance_rust_binary_early.stat.exists | default(false)
|
||||
- not (aw_db_vacuum_timer_enabled | bool)
|
||||
|
||||
- name: Прочитать текущий aw-server.env для сохранения Influx token
|
||||
ansible.builtin.slurp:
|
||||
path: /etc/activitywatch/aw-server.env
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Description=ActivityWatch SQLite nightly VACUUM
|
||||
After=activitywatch-server.service
|
||||
ConditionPathExists=/usr/local/bin/aw-db-maintenance
|
||||
ConditionPathExists=/var/lib/activitywatch/aw-server-rust/sqlite.db
|
||||
RequiresMountsFor=/var/lib/activitywatch
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
EnvironmentFile=-/etc/activitywatch/aw-server.env
|
||||
ExecStart=/usr/local/bin/aw-db-maintenance --vacuum --apply --json
|
||||
TimeoutStartSec=2h
|
||||
SyslogIdentifier=aw-db-vacuum
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
Nice=10
|
||||
IOSchedulingClass=best-effort
|
||||
IOSchedulingPriority=7
|
||||
UMask=0077
|
||||
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Nightly ActivityWatch SQLite VACUUM
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 02:10:00
|
||||
RandomizedDelaySec=10m
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,55 @@
|
||||
# Скрипты выполнения TЗ поддержки DetMir
|
||||
|
||||
В репозитории добавлены скрипты для регламентных проверок по
|
||||
`docs/DETMIR_SUPPORT_TASKS_RU.md`:
|
||||
|
||||
- `scripts/detmir-support-run.sh --scope {daily|weekly|monthly}`
|
||||
- `scripts/detmir-support-daily.sh`
|
||||
- `scripts/detmir-support-weekly.sh`
|
||||
- `scripts/detmir-support-monthly.sh`
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
1. Скопировать шаблон окружения:
|
||||
|
||||
```bash
|
||||
cp scripts/detmir-support.env.example scripts/detmir-support.env
|
||||
```
|
||||
|
||||
2. Внести фактические IP/имена хостов, сервисы, пути бэкапов, VM IDs и SSH-данные.
|
||||
|
||||
3. Запустить нужный режим:
|
||||
|
||||
```bash
|
||||
./scripts/detmir-support-daily.sh
|
||||
./scripts/detmir-support-weekly.sh
|
||||
./scripts/detmir-support-monthly.sh
|
||||
```
|
||||
|
||||
Также можно указать отдельный каталог отчётов:
|
||||
|
||||
```bash
|
||||
./scripts/detmir-support-run.sh --scope daily --output-dir /var/log/detmir-support
|
||||
```
|
||||
|
||||
## Что делает скрипт
|
||||
|
||||
Собираются проверки по режиму:
|
||||
|
||||
- **daily**: ключевая доступность, сервисы, диски, VM/CT, бэкапы, task log, Suricata.
|
||||
- **weekly**: всё из `daily` + расширенные проверки логов и журналов.
|
||||
- **monthly**: всё из `weekly` + проверка апдейтов и базовая фиксация документов/DR-процесса.
|
||||
|
||||
## Результаты
|
||||
|
||||
В каталоге отчётов создаются файлы:
|
||||
|
||||
- `support-<scope>-<timestamp>.log` — детальный лог запуска
|
||||
- `support-<scope>-<timestamp>.csv` — машинный реестр проверок
|
||||
- `support-<scope>-<timestamp>.md` — человекочитаемый отчёт
|
||||
|
||||
Коды выхода:
|
||||
|
||||
- `0` — без ошибок и предупреждений
|
||||
- `1` — есть WARN
|
||||
- `2` — есть FAIL
|
||||
@@ -0,0 +1,281 @@
|
||||
# Задачи поддержки базового комплекса DetMir
|
||||
|
||||
Документ описывает задачи сопровождения того комплекса серверов,
|
||||
виртуализации, сетевых сервисов и прикладного ПО, который существовал до
|
||||
начала разработки AWatch-rus.
|
||||
|
||||
AWatch-rus, ActivityWatch-коллекторы, новые дашборды, новые контуры
|
||||
телеметрии, ClickHouse-аналитика AWatch-rus и разработка продукта в этот
|
||||
объем не входят. Их нужно оформлять отдельной следующей задачей.
|
||||
|
||||
Документ является рамочным ТЗ на постоянную техническую поддержку базового
|
||||
контура. Он не заменяет договор, перечень доступов, инвентарную ведомость и
|
||||
схему сети, но задает границы работ, ожидаемый режим сопровождения и результат,
|
||||
который должен получать владелец.
|
||||
|
||||
## Цель поддержки
|
||||
|
||||
Поддержка должна обеспечивать штатную работу базовой ИТ-инфраструктуры
|
||||
DetMir: серверы доступны, виртуальные машины запущены, web-сервер и
|
||||
прикладное ПО отвечают штатно, данные сохраняются, резервные копии создаются,
|
||||
удаленный доступ контролируется, а аварии устраняются с понятной фиксацией
|
||||
причины.
|
||||
|
||||
## Границы ответственности
|
||||
|
||||
В рамках этой поддержки исполнитель отвечает за техническое сопровождение
|
||||
существующего базового контура: диагностику, регламентные проверки,
|
||||
административные настройки, восстановление штатной работы и документирование
|
||||
изменений.
|
||||
|
||||
В поддержку входят только те серверы, сервисы, сетевые устройства, VM/CT,
|
||||
правила доступа, VPN-подключения, web-сайты, базы данных и файловые каталоги,
|
||||
которые переданы владельцем как часть существующего контура DetMir.
|
||||
|
||||
Если в процессе работ обнаруживается новый сервис, неизвестная VM, новый
|
||||
публичный endpoint, сторонний сервис или компонент без понятного владельца, он
|
||||
сначала фиксируется как находка и риск. Постоянное сопровождение такого
|
||||
компонента начинается только после подтверждения владельцем, что он включается
|
||||
в контур поддержки.
|
||||
|
||||
Работы по изменению бизнес-логики прикладного ПО, разработке новых функций,
|
||||
миграции на новые платформы, покупке оборудования, продлению лицензий,
|
||||
работам провайдеров и физическому монтажу кабельной инфраструктуры не
|
||||
считаются включенными в базовую поддержку, если отдельно не согласованы.
|
||||
|
||||
## Состав поддерживаемого контура
|
||||
|
||||
- Узел виртуализации и размещенные на нем виртуальные машины или контейнеры.
|
||||
- Web-сервер: HTTP/HTTPS-доступность, виртуальные хосты, TLS-сертификаты,
|
||||
reverse proxy, access/error logs и место под web-контент.
|
||||
- Существующие прикладные сервисы и связанные файловые каталоги.
|
||||
- Существующие базы данных и служебные хранилища, если они уже использовались
|
||||
до проекта AWatch-rus.
|
||||
- Proxmox: состояние узла, VM/CT, storage, snapshots, backups, web-доступ.
|
||||
- pfSense: firewall, NAT, маршрутизация, VLAN/interface status, DHCP, DNS,
|
||||
gateway monitoring, логи и правила доступа.
|
||||
- OpenVPN: сервер, клиентские профили, сертификаты, маршруты, доступность
|
||||
подключений и контроль истекающих ключей.
|
||||
- Suricata: состояние IDS/IPS, обновление правил, алерты, false positive,
|
||||
исключения и влияние блокировок на рабочий трафик.
|
||||
- Сетевой контур: маршрутизация, VPN, firewall, DNS, DHCP, удаленный доступ.
|
||||
- Системные сервисы Linux, планировщики задач и фоновые службы.
|
||||
- Резервное копирование, журналы, учетные записи и права доступа.
|
||||
|
||||
## Ежедневные задачи
|
||||
|
||||
- Проверять доступность ключевых серверов и виртуальных машин.
|
||||
- Проверять состояние узла виртуализации: нагрузка CPU/RAM, свободное место,
|
||||
состояние storage, отсутствие зависших VM/CT.
|
||||
- Проверять Proxmox: состояние node, VM/CT, storage, backup jobs, snapshots и
|
||||
ошибки в task log.
|
||||
- Проверять web-сервер: HTTP/HTTPS-ответ, корректность виртуальных хостов,
|
||||
срок действия TLS-сертификатов, ошибки 4xx/5xx и переполнение access/error
|
||||
logs.
|
||||
- Проверять работоспособность прикладных сервисов: запуск, доступность,
|
||||
отсутствие явных ошибок в журналах.
|
||||
- Проверять сетевую связность между основными узлами, VPN и удаленный доступ.
|
||||
- Проверять состояние firewall/NAT/маршрутов, если были жалобы на доступ.
|
||||
- Проверять pfSense: gateway status, interface status, DHCP leases,
|
||||
firewall/NAT rules, логи блокировок по жалобам на доступ.
|
||||
- Проверять OpenVPN server и клиентов: активные подключения, маршруты,
|
||||
reachability внутренних сетей, ошибки TLS/auth/route push.
|
||||
- Проверять Suricata: запущен ли сервис, нет ли массовых блокировок рабочего
|
||||
трафика, критичных алертов и переполнения логов.
|
||||
- Проверять, что критичные systemd services, timers и cron jobs
|
||||
находятся в ожидаемом состоянии.
|
||||
- Проверять наличие свежих резервных копий по базовым системам.
|
||||
- Фиксировать найденные проблемы и выполненные действия в журнале поддержки.
|
||||
|
||||
## Еженедельные задачи
|
||||
|
||||
- Проверять свободное место на всех важных дисках и хранилищах.
|
||||
- Проверять накопление старых логов, временных файлов, дампов и архивов.
|
||||
- Проверять успешность резервного копирования за неделю.
|
||||
- Проверять, что резервные копии реально читаются и доступны для восстановления.
|
||||
- Проверять системные журналы Linux и web-сервера на повторяющиеся ошибки.
|
||||
- Проверять web-сервер: рост 5xx, подозрительные запросы, устаревающие
|
||||
сертификаты, переполнение логов, доступность ключевых URL и корректность
|
||||
reverse proxy.
|
||||
- Проверять pfSense и сетевые журналы: повторяющиеся блокировки, падение
|
||||
gateway, flapping интерфейсов, ошибки DNS/DHCP/VPN.
|
||||
- Проверять OpenVPN: список активных клиентов, неиспользуемые профили,
|
||||
приближающиеся к истечению сертификаты, корректность клиентских маршрутов.
|
||||
- Проверять Suricata alerts: отделять реальные риски от false positive,
|
||||
фиксировать исключения и изменения правил.
|
||||
- Проверять состояние учетных записей, срок действия паролей, лишние
|
||||
административные доступы.
|
||||
- Проверять стабильность VPN/удаленного доступа и отсутствие неожиданных
|
||||
открытых портов.
|
||||
- Готовить короткий отчет владельцу: что проверено, что исправлено, какие
|
||||
риски остаются.
|
||||
|
||||
## Ежемесячные задачи
|
||||
|
||||
- Проводить контрольный тест восстановления из резервной копии на безопасном
|
||||
участке данных или тестовой копии.
|
||||
- Проверять обновления ОС и прикладного ПО, отдельно выделяя безопасные и
|
||||
рискованные обновления.
|
||||
- Планировать окно обслуживания для обновлений, перезагрузок и чистки.
|
||||
- Проверять состояние сертификатов, доменных имен, VPN-профилей и учетных
|
||||
данных удаленного доступа.
|
||||
- Проводить аудит firewall-правил, пробросов портов и внешне доступных
|
||||
сервисов.
|
||||
- Проводить аудит web-сервера: виртуальные хосты, TLS-настройки, redirects,
|
||||
reverse proxy, права на каталоги, logrotate, backup web-конфигурации и
|
||||
отсутствие лишних публичных endpoints.
|
||||
- Проводить аудит pfSense: WAN/LAN/VPN rules, NAT, aliases, floating rules,
|
||||
DHCP/DNS settings, gateway groups и резервную копию конфигурации.
|
||||
- Проводить аудит OpenVPN: server mode, client-specific overrides,
|
||||
сертификаты, отозванные клиенты, push routes и доступы по пользователям.
|
||||
- Проводить аудит Suricata: режим IDS/IPS, включенные rulesets, suppress list,
|
||||
pass/drop правила и влияние на критичные сервисы.
|
||||
- Проводить аудит Proxmox: версии, состояние repositories, backup schedule,
|
||||
storage utilization, snapshots, права пользователей и доступ к web UI.
|
||||
- Обновлять эксплуатационную документацию: IP-адреса, учетные записи без
|
||||
паролей, роли серверов, схемы доступа, процедуры восстановления.
|
||||
- Проверять, что нет устаревших или неизвестных сервисов, запущенных без
|
||||
владельца и назначения.
|
||||
|
||||
## Результаты работ
|
||||
|
||||
Поддержка должна оставлять проверяемый результат, а не только устное сообщение
|
||||
о выполненных действиях.
|
||||
|
||||
Минимальные результаты:
|
||||
|
||||
- журнал выполненных проверок и изменений;
|
||||
- отметка о ежедневной проверке базового состояния;
|
||||
- еженедельный отчет владельцу с перечнем проверок, исправлений и рисков;
|
||||
- краткий инцидентный отчет после серьезных сбоев;
|
||||
- актуализация схемы доступа, списка серверов, VM/CT, сетевых сервисов и
|
||||
ответственных лиц при выявлении изменений;
|
||||
- список открытых рисков и отложенных работ, которые требуют отдельного
|
||||
решения владельца.
|
||||
|
||||
Еженедельный отчет должен быть коротким и практическим: что проверено, что
|
||||
исправлено, что требует решения, какие риски остаются и какие действия
|
||||
предлагаются на следующую неделю.
|
||||
|
||||
## Инцидентные задачи
|
||||
|
||||
- Восстановление доступа к серверу, VM, web-серверу или прикладному сервису.
|
||||
- Устранение нехватки места, памяти, зависших процессов и служб.
|
||||
- Восстановление работы прикладных сервисов, файловых каталогов, баз данных
|
||||
или сетевых шар.
|
||||
- Разбор web-инцидентов: недоступность HTTP/HTTPS, ошибки TLS, неверный
|
||||
redirect, поломка reverse proxy, массовые 4xx/5xx, переполнение логов или
|
||||
нехватка места под web-контент.
|
||||
- Разбор проблем VPN, маршрутизации, DNS, firewall и удаленного доступа.
|
||||
- Разбор и восстановление pfSense после ошибки правил, NAT, gateway, DHCP,
|
||||
DNS, interface/VLAN или некорректного firewall reload.
|
||||
- Разбор и восстановление OpenVPN server/clients: TLS/auth ошибки, истекшие
|
||||
сертификаты, невыданные маршруты, недоступность внутренних сетей.
|
||||
- Разбор Suricata-инцидентов: ложная блокировка рабочего трафика, всплеск
|
||||
алертов, обновление правил, отключение/исключение проблемной сигнатуры.
|
||||
- Восстановление Proxmox VM/CT после зависания, нехватки storage, ошибки
|
||||
backup/snapshot или некорректного shutdown.
|
||||
- Восстановление после неудачного обновления, сбоя питания или некорректной
|
||||
перезагрузки.
|
||||
- Восстановление данных из резервной копии.
|
||||
- Проверка подозрительной активности: неизвестные подключения, новые учетные
|
||||
записи, неожиданные открытые порты, странные timers или cron jobs.
|
||||
- Подготовка краткого инцидентного отчета: причина, влияние, что сделано, что
|
||||
нужно изменить, чтобы сбой не повторился.
|
||||
|
||||
## Критичность заявок
|
||||
|
||||
- P1, полный простой: недоступен ключевой сервис, web-сервер, VM, VPN,
|
||||
firewall/gateway или хранилище, из-за чего пользователи не могут выполнять
|
||||
основную работу.
|
||||
- P2, деградация: сервис работает частично, есть ошибки, падение
|
||||
производительности, нестабильность VPN/сети, переполнение диска или риск
|
||||
скорого отказа.
|
||||
- P3, плановая заявка: настройка, проверка, обновление, добавление правила,
|
||||
выпуск сертификата, изменение доступа или регламентная работа без текущего
|
||||
простоя.
|
||||
- P4, консультация и документация: уточнение схемы, подготовка инструкции,
|
||||
разбор логов без текущего влияния на работу, рекомендации владельцу.
|
||||
|
||||
Критичность может быть повышена, если проблема затрагивает удаленный доступ,
|
||||
резервное копирование, безопасность периметра, публичный web-доступ или риск
|
||||
потери данных.
|
||||
|
||||
## Правила выполнения работ
|
||||
|
||||
- Перед рискованными изменениями делать резервную копию конфигурации или
|
||||
snapshot, если это технически возможно.
|
||||
- Перед изменениями pfSense/OpenVPN/Suricata сохранять текущий config backup и
|
||||
фиксировать путь отката.
|
||||
- Перед изменениями Proxmox делать snapshot/backup для затронутой VM/CT, если
|
||||
это не ухудшает ситуацию и есть достаточно места.
|
||||
- Не менять сетевые маршруты, firewall, VPN и удаленный доступ без понимания
|
||||
пути отката.
|
||||
- Не устанавливать обновления на production без оценки риска и окна
|
||||
обслуживания.
|
||||
- Не удалять старые данные и логи без проверки, что они не нужны для бизнеса
|
||||
или расследования.
|
||||
- После каждого изменения проверять фактический результат: доступность сервиса,
|
||||
состояние VM, HTTP/HTTPS-ответ web-сервера, журнал ошибок, место на диске.
|
||||
- Все изменения фиксировать: дата, сервер, действие, причина, результат,
|
||||
способ отката.
|
||||
|
||||
## Условия выполнения поддержки
|
||||
|
||||
Для нормального выполнения поддержки владелец должен предоставить и
|
||||
поддерживать актуальными:
|
||||
|
||||
- административный доступ к Proxmox, pfSense, Linux-серверам, web-серверу,
|
||||
VPN и другим компонентам, входящим в контур;
|
||||
- безопасный способ хранения и передачи учетных данных;
|
||||
- контакт ответственного лица для согласования рискованных изменений;
|
||||
- список критичных сервисов и допустимые окна обслуживания;
|
||||
- сведения о провайдерах, доменах, сертификатах, внешних адресах и каналах
|
||||
связи;
|
||||
- возможность получить физический доступ к оборудованию через сотрудника на
|
||||
месте, если удаленная диагностика недостаточна;
|
||||
- подтверждение, какие сервисы являются основными, резервными или
|
||||
историческими.
|
||||
|
||||
Если доступы, контакты, резервные копии или сведения о контуре отсутствуют,
|
||||
сроки диагностики и восстановления могут увеличиваться. Такие ограничения
|
||||
должны фиксироваться как отдельный риск.
|
||||
|
||||
## Минимальный SLA
|
||||
|
||||
- Плановая проверка: один раз в рабочий день.
|
||||
- Реакция на P1, полный простой ключевого сервиса: начало диагностики в течение
|
||||
2-4 часов в рабочее время.
|
||||
- Реакция на P2, частичная деградация: начало диагностики в течение рабочего
|
||||
дня.
|
||||
- Реакция на P3/P4: планирование и выполнение по согласованию с владельцем.
|
||||
- Еженедельный отчет владельцу.
|
||||
- Плановые изменения только с резервной копией и проверкой результата.
|
||||
- После каждого серьезного инцидента: краткий отчет и список мер против
|
||||
повторения.
|
||||
|
||||
SLA задает срок реакции и начала диагностики. Срок полного устранения зависит
|
||||
от причины инцидента, доступности резервных копий, необходимости участия
|
||||
провайдера, наличия оборудования, доступа к учетным данным и возможности
|
||||
провести работы без риска для действующего контура.
|
||||
|
||||
## Что не входит в этот объем
|
||||
|
||||
- Разработка и сопровождение AWatch-rus как продукта.
|
||||
- Новые ActivityWatch-коллекторы и новые telemetry pipelines.
|
||||
- Новые Grafana-дашборды и управленческая аналитика AWatch-rus.
|
||||
- Новые аналитические контуры, созданные специально под AWatch-rus.
|
||||
- Коммерческая упаковка, roadmap, CI/CD и разработка новых модулей AWatch-rus.
|
||||
- Покупка, поставка и замена оборудования, если это не оформлено отдельно.
|
||||
- Оплата, продление и сопровождение лицензий, доменов, сертификатов и услуг
|
||||
провайдеров, если это требует отдельного договора или платежа.
|
||||
- Физическая прокладка кабелей, монтаж СКС, электрика и работы в серверной,
|
||||
требующие присутствия монтажной организации.
|
||||
- Полноценный аудит информационной безопасности, пентест, расследование
|
||||
инцидента ИБ и внедрение новых средств защиты сверх текущих pfSense,
|
||||
OpenVPN и Suricata.
|
||||
- Разработка нового прикладного ПО, изменение бизнес-логики существующих
|
||||
систем и перенос данных между платформами.
|
||||
|
||||
Эти работы нужно оформлять отдельным приложением к поддержке, когда владелец
|
||||
готов заказывать сопровождение уже разработанной системы AWatch-rus.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Цель сценария: за 10-12 минут показать AWatch-rus как рабочую платформу
|
||||
Workforce Analytics + Security Analytics + Forensics без ложных заявлений о
|
||||
SIEM, классическом DLP, ML/LLM или готовом pfSense ingestion.
|
||||
SIEM, классическом DLP, ML/LLM .
|
||||
|
||||
Демонстрация проводится только на обезличенных данных. Demo fixtures не
|
||||
являются live ingestion и не подменяют промышленную настройку источников.
|
||||
@@ -101,6 +101,9 @@ SIEM, классическом DLP, ML/LLM или готовом pfSense ingesti
|
||||
|
||||
- UEBA v1 является rule-based моделью;
|
||||
- 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.
|
||||
- Есть Rust Backend, Rust Agent baseline, HTML/HTMX portal и role-based
|
||||
контракты Pilot v1.
|
||||
- Rust-primary runtime зафиксирован честно: PowerShell не заявлен полностью
|
||||
удаленным, оставшиеся runtime/fallback/installer/repair scripts сохраняются
|
||||
как documented fallback/support layer.
|
||||
- Подготовлен demo pack: сценарии руководителя, ИБ и расследований, синтетический
|
||||
demo dataset, evidence pack, пример итогового отчета и value proposition.
|
||||
- Подготовлены registry readiness документы без ложных заявлений о сертификации.
|
||||
@@ -57,6 +60,8 @@
|
||||
## Что отложено на roadmap
|
||||
|
||||
- Расширенные agentless providers: PowerShell, SSH, Syslog, 1C, VPN и SCUD.
|
||||
- Отдельная PowerShell retirement-задача после burn-in периода, canary test,
|
||||
rollback plan и acceptance gate.
|
||||
- Production validation российских ОС: Astra Linux, РЕД ОС, Альт и РОСА.
|
||||
- Расширенный React/TypeScript Enterprise UI и Tauri Desktop Forensics.
|
||||
- Глубокие enterprise-коннекторы AD/LDAP, SIEM и корпоративных учетных систем.
|
||||
|
||||
@@ -73,6 +73,9 @@ Portal smoke подтвердил:
|
||||
Risk -> Investigation -> Report`.
|
||||
- Rust-first runtime покрывает критичные серверные helpers, DLP/worktime paths,
|
||||
readiness, portal и собственный агент.
|
||||
- Rust-first здесь означает Rust-primary направление, а не полное удаление
|
||||
PowerShell: runtime/fallback/installer/repair scripts остаются
|
||||
документированным слоем отката, установки и поддержки.
|
||||
- pfSense/network perimeter оставлен optional/read-only слоем, что снижает риск
|
||||
пилота и соответствует текущим ограничениям проекта.
|
||||
- Python сохранен только для оговоренных исключений, не как ядро продукта.
|
||||
@@ -227,6 +230,8 @@ enterprise endpoint agent`.
|
||||
- Readiness bundle, checksum/signature и Prometheus/Grafana readiness ideas уже
|
||||
заложены в документах и тестах.
|
||||
- Legacy fallback сохранен там, где это нужно для rollback.
|
||||
- Полное удаление PowerShell должно быть отдельной задачей после burn-in,
|
||||
canary test, rollback plan и acceptance gate.
|
||||
|
||||
Слабые стороны:
|
||||
|
||||
@@ -246,6 +251,7 @@ enterprise endpoint agent`.
|
||||
поломки старого API.
|
||||
- Отсутствие JSON state файлов обрабатывается.
|
||||
- PowerShell collector сохранен как legacy fallback, а не удален.
|
||||
- Pilot freeze не включает автоматическое удаление PowerShell scripts.
|
||||
|
||||
Слабые стороны:
|
||||
|
||||
|
||||
@@ -50,6 +50,10 @@
|
||||
## Agent сценарий
|
||||
|
||||
- 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.
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# Матрица PowerShell-скриптов AWatch-rus
|
||||
|
||||
Дата актуализации: 2026-06-17
|
||||
|
||||
Документ фиксирует фактический статус оставшихся PowerShell-файлов после
|
||||
перехода на Rust-first контур. Это не означает, что PowerShell полностью
|
||||
удален. Цель матрицы: отделить рабочий runtime от установочных действий,
|
||||
аварийного отката и устаревших проверочных скриптов.
|
||||
|
||||
В tracked документации не публикуются имена рабочих хостов, private IP,
|
||||
учетные записи, токены и runtime evidence paths. Live-проверка выполнялась по
|
||||
фактическим Scheduled Tasks, процессам и deployment config, но приватные
|
||||
идентификаторы здесь заменены на обобщенные описания.
|
||||
|
||||
## Классы
|
||||
|
||||
| Класс | Значение |
|
||||
|---|---|
|
||||
| `runtime` | Скрипт участвует в штатном выполнении или является зависимостью штатного выполнения. |
|
||||
| `installer` | Скрипт нужен для установки, настройки, проверки или ремонта, но не должен постоянно работать. |
|
||||
| `fallback` | Скрипт оставлен как аварийный откат или reference-слой после перевода штатного пути на Rust. |
|
||||
| `obsolete` | Скрипт не нужен для штатной работы; кандидат на удаление после проверки ссылок и задач. |
|
||||
|
||||
## Фактический runtime-базис
|
||||
|
||||
Штатный контур уже Rust-first для основных Windows-сборщиков:
|
||||
|
||||
- загрузка 1C/file telemetry выполняется через `aw-windows-telemetry.exe file1c-upload`;
|
||||
- синхронизация DLP evidence выполняется через `aw-windows-telemetry.exe dlp-evidence-sync`;
|
||||
- browser/DLP/file-operations collectors выполняются через подкоманды `aw-windows-telemetry.exe`;
|
||||
- учет рабочего времени выполняется через `awatch-agent-rs.exe`;
|
||||
- collector guard выполняется через `aw-windows-telemetry.exe collector-guard`.
|
||||
|
||||
Оставшийся рабочий PowerShell runtime на момент проверки:
|
||||
|
||||
- сгенерированный recovery loop;
|
||||
- сгенерированный per-user launcher;
|
||||
- Hayabusa/EVTX upload path.
|
||||
|
||||
## Runtime и generated runtime
|
||||
|
||||
| Файл | Класс | Фактическая роль | Штатный путь сейчас | Решение |
|
||||
|---|---|---|---|---|
|
||||
| `C:\ProgramData\AWatch-rus\launch-watchers.ps1` | `runtime` | Сгенерированный пользовательский launcher для watchers/collectors. | Запускается через скрытый VBS-wrapper из per-user Scheduled Tasks. | Оставить до замены launcher-а на Rust/service-friendly task reconciler. |
|
||||
| `C:\ProgramData\AWatch-rus\recovery-loop.ps1` | `runtime` | Сгенерированный recovery loop. | Запускается через скрытый VBS-wrapper из recovery Scheduled Task. | Мигрировать в Rust recovery/guard, затем убрать PowerShell runtime. |
|
||||
| `windows/export-upload-hayabusa-to-aw-server.ps1` | `runtime` | Выгрузка Hayabusa/EVTX evidence на сервер. | Используется отдельной Scheduled Task. | P1: перенести в `aw-windows-telemetry.exe` или отдельный Rust EXE. До миграции не удалять. |
|
||||
| `windows/export-evtx-for-hayabusa.ps1` | `runtime` | Экспорт EVTX для Hayabusa upload path. | Используется как зависимость Hayabusa upload path. | P1: перенести bounded EVTX export в Rust. До миграции не удалять. |
|
||||
|
||||
## Fallback после Rust-first миграции
|
||||
|
||||
| Файл | Класс | Фактическая роль | Штатный путь сейчас | Решение |
|
||||
|---|---|---|---|---|
|
||||
| `windows/worktime-session-collector.ps1` | `fallback` | Legacy worktime/RDP collector. | `awatch-agent-rs.exe` является основным runtime. | Оставить до отключения `worktimeLegacyFallbackEnabled` после acceptance gate. |
|
||||
| `windows/browser-domains-native-collector.ps1` | `fallback` | Legacy browser/domain collector. | `aw-windows-telemetry.exe browser-domains-collector` является основным runtime. | Оставить как rollback/reference до расширенной parity-проверки. |
|
||||
| `windows/dlp-endpoint-signals-collector.ps1` | `fallback` | Legacy DLP endpoint collector. | `aw-windows-telemetry.exe dlp-endpoint-collector` является основным runtime. | Оставить как rollback/reference до расширенной parity-проверки. |
|
||||
| `windows/file-operations-collector.ps1` | `fallback` | Legacy file operations collector. | `aw-windows-telemetry.exe file-operations-collector` является основным runtime. | Оставить как rollback/reference до расширенной parity-проверки. |
|
||||
| `windows/dlp-policy-client.ps1` | `fallback` | Получение policy для PowerShell DLP collector. | Нужен только если включается legacy DLP collector. | Удалять вместе с PowerShell DLP fallback, не раньше. |
|
||||
| `windows/export-upload-file-1c-telemetry.ps1` | `fallback` | Legacy 1C/file telemetry upload. | `aw-windows-telemetry.exe file1c-upload` является основным runtime. | Оставить rollback до нескольких успешных циклов production upload. |
|
||||
| `windows/sync-dlp-evidence-artifacts.ps1` | `fallback` | Legacy DLP evidence sync. | `aw-windows-telemetry.exe dlp-evidence-sync` является основным runtime. | Оставить rollback до закрепления Rust evidence sync. |
|
||||
| `windows/aw-collector-guard.ps1` | `fallback` | Legacy collector guard. | `aw-windows-telemetry.exe collector-guard` является основным runtime. | Оставить rollback до подтверждения guard/recovery parity. |
|
||||
| `windows/aw-standalone-service.ps1` | `fallback` | Legacy standalone supervisor wrapper. | Не является целевым runtime для нового Rust-first контура. | Держать только для отката старого install-kit, затем удалить. |
|
||||
|
||||
## Installer, repair и operator layer
|
||||
|
||||
| Файл | Класс | Фактическая роль | Штатный путь сейчас | Решение |
|
||||
|---|---|---|---|---|
|
||||
| `windows/ActivityWatch.Windows.Common.psm1` | `installer` | Общие PowerShell-функции для установки/обслуживания. | Используется установочными и repair-скриптами. | Оставить до замены installer layer. |
|
||||
| `windows/ActivityWatch.Windows.Common.psd1` | `installer` | Manifest общего PowerShell-модуля. | Используется вместе с `.psm1`. | Оставить до замены installer layer. |
|
||||
| `windows/deploy-single-user.ps1` | `installer` | Установка для одного пользователя. | Установочный сценарий, не постоянный runtime. | Постепенно заменить Rust bootstrap/Ansible action. |
|
||||
| `windows/deploy-domain-users.ps1` | `installer` | Установка для доменных пользователей. | Установочный сценарий, не постоянный runtime. | Постепенно заменить Rust bootstrap/Ansible action. |
|
||||
| `windows/deploy-ensemble.ps1` | `installer` | Оркестрация Windows-компонентов установки. | Установочный сценарий, не постоянный runtime. | Постепенно заменить Rust deploy coordinator или Ansible wrapper. |
|
||||
| `windows/install-standalone-service.ps1` | `installer` | Установка standalone service. | Установочное действие. | Заменить Rust installer/bootstrap CLI. |
|
||||
| `windows/install-collector-guard-service.ps1` | `installer` | Установка collector guard service. | Установочное действие. | Заменить Rust installer/bootstrap CLI. |
|
||||
| `windows/install-dlp-client.ps1` | `installer` | Установка DLP client части. | Установочное действие. | Заменить Rust installer/bootstrap CLI. |
|
||||
| `windows/migrate-awatch-rus-paths.ps1` | `installer` | Миграция путей AWatch-rus. | Разовое migration/repair действие. | Заменить Rust migration CLI с backup/dry-run. |
|
||||
| `windows/rebuild-worktime-tasks.ps1` | `installer` | Пересборка worktime Scheduled Tasks. | Repair/installer действие. | Заменить Rust task reconciliation CLI. |
|
||||
| `windows/fix-session-watchers.ps1` | `installer` | Ремонт watcher-ов по сессиям. | Repair действие. | Заменить Rust repair subcommand. |
|
||||
| `windows/cleanup-disc-sessions.ps1` | `installer` | Очистка устаревших disconnected-session артефактов. | Maintenance/repair действие. | Заменить Rust maintenance subcommand. |
|
||||
| `windows/hardening-recovery.ps1` | `installer` | Восстановление и hardening после повреждения установки. | Emergency repair, не постоянный runtime. | Заменить Rust recovery CLI; опасные действия только через явный apply-режим. |
|
||||
| `windows/validate-deployment.ps1` | `installer` | Проверка установки. | Частично заменен `aw-windows-telemetry.exe validate-deployment`. | Расширить Rust validation до полной parity, затем удалить `.ps1`. |
|
||||
| `windows/audit-cryptopro.ps1` | `installer` | Аудит CryptoPro/сертификатного окружения. | Операторская проверка, не постоянный runtime. | Оставить до появления Rust audit CLI или отдельного Ansible-only gate. |
|
||||
| `scripts/powershell/detmir-powershell-profile.ps1` | `installer` | Операторский PowerShell profile/helper. | Не является runtime AWatch-rus. | Оставить как operator helper, либо заменить документацией/CLI aliases. |
|
||||
|
||||
## Obsolete / parked
|
||||
|
||||
| Файл | Класс | Фактическая роль | Штатный путь сейчас | Решение |
|
||||
|---|---|---|---|---|
|
||||
| `windows/run-user1-probe.ps1` | `obsolete` | Ручной диагностический probe одного пользователя. | Не нужен для штатного runtime. | Удалить после проверки отсутствия ссылок в runbook/CI/tasks. |
|
||||
| `.pssa_run.ps1` | `obsolete` | Локальный helper для PSScriptAnalyzer. | Не является продуктовым компонентом. | Удалить после завершения PowerShell retirement или заменить CI-командой. |
|
||||
| `windows/email-outbound-collector.ps1` | `obsolete` | Legacy outbound email metadata collector. | Функция не включена в текущий runtime. | Не возвращать в production как PowerShell; при необходимости реализовывать заново в Rust с отдельным privacy/legal gate. |
|
||||
|
||||
## Правила дальнейшей миграции
|
||||
|
||||
Полное удаление PowerShell не входит в текущий pilot freeze. Это должна быть
|
||||
отдельная задача после подтверждения Rust parity и отката.
|
||||
|
||||
1. Не удалять `runtime` и `fallback` скрипты без burn-in/canary/rollback gate,
|
||||
acceptance gate и проверенного rollback-плана.
|
||||
2. Любой PowerShell runtime, который остается в Scheduled Tasks или Services,
|
||||
должен иметь явный владелец, причину сохранения и целевой Rust replacement.
|
||||
3. `installer`-скрипты можно заменять постепенно: сначала Rust dry-run и
|
||||
structured report, затем apply-режим, затем удаление PowerShell layer.
|
||||
4. `obsolete`-скрипты удалять только после `rg`-проверки ссылок, проверки
|
||||
Scheduled Tasks/Services и контрольного deploy/validation прогона.
|
||||
5. Новые runtime-функции не добавлять на PowerShell. Для runtime, long-running
|
||||
collectors, upload, guard и validation целевой путь: Rust EXE или service.
|
||||
6. Pilot freeze не является основанием для автоматического удаления
|
||||
PowerShell layer: оставшиеся runtime/fallback пути сохраняются до
|
||||
подтвержденной стабильности Rust replacement, canary-наблюдения и
|
||||
документированного отката.
|
||||
@@ -9,6 +9,9 @@
|
||||
Документ описывает порядок миграции. Он не вводит новые функции: каждая Rust
|
||||
замена сначала должна повторить текущий контракт PowerShell-компонента.
|
||||
|
||||
Актуальная статусная матрица оставшихся PowerShell-файлов:
|
||||
[`POWERSHELL_SCRIPT_STATUS_MATRIX_RU.md`](POWERSHELL_SCRIPT_STATUS_MATRIX_RU.md).
|
||||
|
||||
Операторский checkpoint от 2026-06-05 сохранен в private `.ops`-контуре:
|
||||
Phase 0 live inventory выполнен, Phase 2 validation для уже переведенных
|
||||
Windows Rust paths выполнен успешно. В tracked документации не публикуются live
|
||||
|
||||
@@ -128,6 +128,9 @@ git ls-files | grep -E '(^|/)secrets(/|$)|\.env$|inventory\.ini$' || true
|
||||
- Live inventory, live domains, private IPs, case IDs, forensic paths,
|
||||
screenshots с реальными пользователями и runtime evidence не входят в Git.
|
||||
- Telegram runtime остается Python.
|
||||
- PowerShell не считается полностью удаленным: оставшиеся
|
||||
runtime/fallback/installer/repair scripts допустимы как документированный
|
||||
слой отката и поддержки до отдельного burn-in/canary/rollback/acceptance gate.
|
||||
- pfSense/network layer не входит в этот release-readiness этап.
|
||||
|
||||
## 8. Следующий уровень
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
| CI checksum/signature self-test | `.github/workflows/release-assets.yml` |
|
||||
| Pilot acceptance act | `docs/CUSTOMER_PILOT_ACCEPTANCE_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
|
||||
|
||||
@@ -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
|
||||
|
||||
Документ:
|
||||
@@ -115,3 +132,6 @@ docs/NETWORK_PERIMETER_PFSENSE_RU.md
|
||||
- GitHub release содержит SBOM JSON/TXT, manifest, checksum и signature.
|
||||
- `CUSTOMER_PILOT_ACCEPTANCE_RU.md` заполнен для пилотного заказчика.
|
||||
- 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.
|
||||
- pfSense описан только как optional integration/perimeter layer.
|
||||
- SIEM/UEBA/DLP не заявлены как основной сертифицированный класс.
|
||||
- Rust-primary runtime описан честно: PowerShell не заявлен полностью
|
||||
удаленным, а оставшиеся runtime/fallback/installer/repair scripts сохраняются
|
||||
до отдельного burn-in/canary/rollback/acceptance gate.
|
||||
- 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
|
||||
@@ -0,0 +1,41 @@
|
||||
# Полная диагностика развернутого контура DetMir
|
||||
|
||||
Пакет предназначен для расширенной проверки уже развернутого контура
|
||||
DetMir, не заменяет и не дублирует скрипты TЗ поддержки.
|
||||
|
||||
Состав:
|
||||
- `detmir-full-diagnostics.sh` — оркестратор:
|
||||
- `aw-contour-diag.sh` (по-умолчанию полный прогон),
|
||||
- `detmir-support-run.sh` (daily/weekly/monthly),
|
||||
- `check_production_inventory_placeholders.sh`.
|
||||
- `detmir-full-diagnostics-daily.sh` — быстрый старт с `--scope daily`.
|
||||
- `detmir-full-diagnostics-weekly.sh` — запуск с `--scope weekly`.
|
||||
- `detmir-full-diagnostics-monthly.sh` — запуск с `--scope monthly`.
|
||||
- `detmir-full-diagnostics.env.example` — шаблон параметров.
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
1. Скопировать файл конфигурации:
|
||||
|
||||
```bash
|
||||
cp scripts/detmir-full-diagnostics/detmir-full-diagnostics.env.example \
|
||||
scripts/detmir-full-diagnostics/detmir-full-diagnostics.env
|
||||
```
|
||||
|
||||
2. Заполнить пути/хосты под текущий контур.
|
||||
|
||||
3. Запуск:
|
||||
|
||||
```bash
|
||||
./scripts/detmir-full-diagnostics/detmir-full-diagnostics-daily.sh
|
||||
./scripts/detmir-full-diagnostics/detmir-full-diagnostics.sh --scope weekly --quick
|
||||
./scripts/detmir-full-diagnostics/detmir-full-diagnostics-monthly.sh --output-dir /var/log/detmir-full-diagnostics
|
||||
```
|
||||
|
||||
## Примечание
|
||||
|
||||
- Для запуска в `~/root/scripts/support` пакет может использовать скопированные
|
||||
`aw-contour-diag.sh` и `check_production_inventory_placeholders.sh`.
|
||||
Если локально эти файлы не требуются — удалите их или отключите шаги флагами.
|
||||
- Для запуска в окружении с root-доступом через `sudo` выполняйте скрипты как
|
||||
пользователь с правом `sudo` без `NOPASSWD`.
|
||||
@@ -0,0 +1,507 @@
|
||||
#!/usr/bin/env bash
|
||||
# aw-contour-diag.sh - Диагностика всего контура ActivityWatch-Russian
|
||||
# Запускать с машины администратора (где есть доступ по SSH/curl ко всем узлам).
|
||||
#
|
||||
# Использование:
|
||||
# ./scripts/aw-contour-diag.sh # полная диагностика
|
||||
# ./scripts/aw-contour-diag.sh --quick # быстрая (только AW server + buckets)
|
||||
# ./scripts/aw-contour-diag.sh --skip-windows # без RDP/WinRM проверок
|
||||
#
|
||||
# При красных проверках в скрипте указаны разделы 'REMEDIATION: ...'
|
||||
# с конкретными командами для восстановления.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ANSIBLE_DIR="$REPO_ROOT/ansible"
|
||||
INVENTORY="$ANSIBLE_DIR/inventory.ini"
|
||||
AW_SERVER="http://10.10.10.13:5600"
|
||||
AW_WORKTIME_API="http://10.10.10.13:5610"
|
||||
INFLUXDB_URL="http://10.10.10.10:8086"
|
||||
GRAFANA_URL="http://10.10.10.11:3000"
|
||||
PROXMOX_HOST="10.10.10.2"
|
||||
AW_HOST="10.10.10.13"
|
||||
GRAFANA_HOST="10.10.10.11"
|
||||
INFLUXDB_HOST="10.10.10.10"
|
||||
WINDOWS_HOST="192.168.100.18"
|
||||
CLICKHOUSE_HOST="10.10.10.2"
|
||||
SOURCE_HOSTNAME="SHARKON2025"
|
||||
|
||||
QUICK_MODE=0
|
||||
SKIP_WINDOWS=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--quick) QUICK_MODE=1; shift ;;
|
||||
--skip-windows) SKIP_WINDOWS=1; shift ;;
|
||||
-h|--help)
|
||||
echo "Usage: $(basename "$0") [--quick] [--skip-windows]"
|
||||
exit 0 ;;
|
||||
*) echo "Unknown: $1"; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
export no_proxy="localhost,127.0.0.1,$PROXMOX_HOST,$AW_HOST,$GRAFANA_HOST,$INFLUXDB_HOST,$WINDOWS_HOST,$CLICKHOUSE_HOST,10.10.10.0/24,192.168.100.0/24"
|
||||
export NO_PROXY="$no_proxy"
|
||||
|
||||
OK_COUNT=0; WARN_COUNT=0; FAIL_COUNT=0; SKIP_COUNT=0
|
||||
|
||||
if [ -t 1 ]; then
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||
else
|
||||
RED=''; GREEN=''; YELLOW=''; CYAN=''; NC=''
|
||||
fi
|
||||
|
||||
pass() { OK_COUNT=$((OK_COUNT+1)); printf "%b[OK]%b %s\n" "$GREEN" "$NC" "$*"; }
|
||||
warn() { WARN_COUNT=$((WARN_COUNT+1)); printf "%b[WARN]%b %s\n" "$YELLOW" "$NC" "$*"; }
|
||||
fail() { FAIL_COUNT=$((FAIL_COUNT+1)); printf "%b[FAIL]%b %s\n" "$RED" "$NC" "$*"; }
|
||||
skip() { SKIP_COUNT=$((SKIP_COUNT+1)); printf "%b[SKIP]%b %s\n" "$YELLOW" "$NC" "$*"; }
|
||||
section() { printf "\n%b=== %s ===%b\n" "$CYAN" "$*" "$NC"; }
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
check_tcp() {
|
||||
local name="$1" host="$2" port="$3"
|
||||
if timeout 4 bash -c ":</dev/tcp/${host}/${port}" >/dev/null 2>&1; then
|
||||
pass "TCP $host:$port ($name)"
|
||||
else
|
||||
fail "TCP $host:$port ($name)"
|
||||
echo " REMEDIATION: Проверьте, запущен ли сервис на $host:$port."
|
||||
echo " Для systemd: ssh igor@$host 'systemctl status <unit>'"
|
||||
echo " Для Docker: ssh igor@$PROXMOX_HOST 'sudo docker ps | grep <container>'"
|
||||
fi
|
||||
}
|
||||
|
||||
check_http_code() {
|
||||
local name="$1" url="$2" expected="${3:-^2[0-9][0-9]$}"
|
||||
local tmp code
|
||||
tmp="$(mktemp)"
|
||||
code="$(curl -k -sS --connect-timeout 5 --max-time 15 -o "$tmp" -w '%{http_code}' "$url" 2>"$tmp.err")"
|
||||
if printf "%s" "$code" | grep -Eq "$expected"; then
|
||||
pass "HTTP $code $url ($name)"
|
||||
else
|
||||
fail "HTTP $code $url ($name)"
|
||||
sed 's/^/ /' "$tmp.err" "$tmp" 2>/dev/null | head -20
|
||||
fi
|
||||
rm -f "$tmp" "$tmp.err"
|
||||
}
|
||||
|
||||
check_http_json_key() {
|
||||
local name="$1" url="$2" jq_filter="$3" remediation="$4"
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
if curl -k -fsS --connect-timeout 5 --max-time 20 "$url" -o "$tmp" 2>"$tmp.err" && jq -e "$jq_filter" "$tmp" >/dev/null 2>&1; then
|
||||
pass "$name"
|
||||
else
|
||||
fail "$name"
|
||||
sed 's/^/ /' "$tmp.err" "$tmp" 2>/dev/null | head -10
|
||||
echo " REMEDIATION: $remediation"
|
||||
fi
|
||||
rm -f "$tmp" "$tmp.err"
|
||||
}
|
||||
|
||||
check_bucket_freshness() {
|
||||
local bucket="$1" label="$2" remediation="$3"
|
||||
local bucket_id="${bucket}_${SOURCE_HOSTNAME}"
|
||||
local tmp last_ts event_epoch now age_sec
|
||||
tmp="$(mktemp)"
|
||||
if ! curl -fsS --connect-timeout 5 --max-time 15 "$AW_SERVER/api/0/buckets/$bucket_id/events?limit=1" -o "$tmp" 2>"$tmp.err"; then
|
||||
fail "bucket $label ($bucket_id) — запрос не удался"
|
||||
sed 's/^/ /' "$tmp.err" | head -5
|
||||
echo " REMEDIATION: $remediation"
|
||||
rm -f "$tmp" "$tmp.err"
|
||||
return
|
||||
fi
|
||||
last_ts="$(jq -r '.[0].timestamp // empty' "$tmp" 2>/dev/null)"
|
||||
rm -f "$tmp"
|
||||
if [ -z "$last_ts" ]; then
|
||||
warn "bucket $label ($bucket_id) — нет событий"
|
||||
echo " REMEDIATION: $remediation"
|
||||
return
|
||||
fi
|
||||
event_epoch="$(date -d "$last_ts" +%s 2>/dev/null || echo 0)"
|
||||
now="$(date -u +%s)"
|
||||
age_sec=$((now - event_epoch))
|
||||
|
||||
case "$bucket" in
|
||||
aw-dlp-incidents|aw-dlp-review|aw-dlp-rules|aw-session-events)
|
||||
if [ "$age_sec" -lt 86400 ]; then
|
||||
pass "bucket $label — ${age_sec}s назад"
|
||||
else
|
||||
warn "bucket $label — ${age_sec}s назад (event-driven)"
|
||||
fi ;;
|
||||
aw-watcher-window|aw-dlp-endpoint-signals)
|
||||
if [ "$age_sec" -lt 7200 ]; then
|
||||
pass "bucket $label — ${age_sec}s назад"
|
||||
else
|
||||
warn "bucket $label — ${age_sec}s назад (INACTIVE)"
|
||||
fi ;;
|
||||
*)
|
||||
if [ "$age_sec" -lt 3600 ]; then
|
||||
pass "bucket $label — ${age_sec}s назад"
|
||||
elif [ "$age_sec" -lt 86400 ]; then
|
||||
warn "bucket $label — ${age_sec}s назад (STALE)"
|
||||
echo " REMEDIATION: $remediation"
|
||||
else
|
||||
fail "bucket $label — ${age_sec}s назад (DEAD)"
|
||||
echo " REMEDIATION: $remediation"
|
||||
fi ;;
|
||||
esac
|
||||
}
|
||||
|
||||
check_ansible_shell() {
|
||||
local name="$1" group="$2" command="$3"
|
||||
if ! have ansible; then
|
||||
skip "$name (ansible not available)"
|
||||
return
|
||||
fi
|
||||
if [ ! -f "$INVENTORY" ]; then
|
||||
skip "$name (inventory not found: $INVENTORY)"
|
||||
return
|
||||
fi
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
if ANSIBLE_NOCOLOR=1 ansible "$group" -i "$INVENTORY" -m shell -a "$command" >"$tmp" 2>&1; then
|
||||
pass "$name"
|
||||
else
|
||||
fail "$name"
|
||||
sed 's/^/ /' "$tmp" | head -20
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
check_ansible_win_shell() {
|
||||
local name="$1" command="$2"
|
||||
if ! have ansible; then skip "$name (ansible not available)"; return; fi
|
||||
if [ ! -f "$INVENTORY" ]; then skip "$name (inventory not found)"; return; fi
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
if ANSIBLE_NOCOLOR=1 ansible aw_windows -i "$INVENTORY" -m win_shell -a "$command" >"$tmp" 2>&1; then
|
||||
pass "$name"
|
||||
else
|
||||
fail "$name"
|
||||
sed 's/^/ /' "$tmp" | head -20
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
check_ansible_module() {
|
||||
local name="$1" group="$2" module="$3" args="${4:-}"
|
||||
if ! have ansible; then skip "$name (ansible not available)"; return; fi
|
||||
if [ ! -f "$INVENTORY" ]; then skip "$name (inventory not found)"; return; fi
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
if ANSIBLE_NOCOLOR=1 ansible "$group" -i "$INVENTORY" -m "$module" ${args:+-a "$args"} >"$tmp" 2>&1; then
|
||||
pass "$name"
|
||||
else
|
||||
fail "$name"
|
||||
sed 's/^/ /' "$tmp" | head -20
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
ssh_with_diag_password() {
|
||||
local host="$1"
|
||||
shift
|
||||
if [[ -z "${AW_DIAG_SSH_PASSWORD:-}" ]]; then
|
||||
return 125
|
||||
fi
|
||||
sshpass -p "$AW_DIAG_SSH_PASSWORD" ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no "igor@$host" "$@"
|
||||
}
|
||||
|
||||
ssh_aw() { ssh_with_diag_password 10.10.10.13 "$@"; }
|
||||
ssh_pve() { ssh_with_diag_password 10.10.10.2 "$@"; }
|
||||
|
||||
check_service_remote() {
|
||||
local name="$1" host="$2" unit="$3" remediation="$4"
|
||||
local result
|
||||
result=$(ssh_with_diag_password "$host" "systemctl is-active $unit 2>/dev/null || echo not_found" 2>/dev/null)
|
||||
local rc=$?
|
||||
if [ "$rc" -eq 125 ]; then
|
||||
skip "$name ($unit on $host — set AW_DIAG_SSH_PASSWORD for SSH checks)"
|
||||
return
|
||||
fi
|
||||
if [ "$rc" -ne 0 ] || [ "$result" = "not_found" ]; then
|
||||
skip "$name ($unit on $host — не удалось проверить)"
|
||||
return
|
||||
fi
|
||||
if [ "$result" = "active" ]; then
|
||||
pass "$name ($unit active on $host)"
|
||||
else
|
||||
fail "$name ($unit $result on $host)"
|
||||
echo " REMEDIATION: $remediation"
|
||||
fi
|
||||
}
|
||||
|
||||
printf "%b=== ActivityWatch-Russian: Диагностика контура ===%b\n" "$CYAN" "$NC"
|
||||
echo " $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||
echo ""
|
||||
|
||||
# ============================================================
|
||||
section "1. Локальные предусловия"
|
||||
# ============================================================
|
||||
for cmd in bash curl jq timeout ssh sshpass; do
|
||||
if have "$cmd"; then pass "утилита $cmd найдена"; else fail "утилита $cmd не найдена (установите: apt install $cmd)"; fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
# ============================================================
|
||||
section "2. TCP доступность узлов"
|
||||
# ============================================================
|
||||
check_tcp "AW Server" "$AW_HOST" 5600
|
||||
check_tcp "Worktime API" "$AW_HOST" 5610
|
||||
check_tcp "RDP WinRM" "$WINDOWS_HOST" 5985
|
||||
check_tcp "Proxmox SSH" "$PROXMOX_HOST" 22
|
||||
check_tcp "Proxmox HTTPS" "$PROXMOX_HOST" 443
|
||||
check_tcp "1C Company API" "$PROXMOX_HOST" 8710
|
||||
check_tcp "ClickHouse HTTP" "$CLICKHOUSE_HOST" 8123
|
||||
check_tcp "ClickHouse Native" "$CLICKHOUSE_HOST" 9000
|
||||
check_tcp "InfluxDB" "$INFLUXDB_HOST" 8086
|
||||
check_tcp "Grafana" "$GRAFANA_HOST" 3000
|
||||
|
||||
if [ "$QUICK_MODE" = "1" ]; then
|
||||
# В быстром режиме проверяем только AW Server и buckets
|
||||
echo ""
|
||||
section "3. AW Server (быстрый режим)"
|
||||
check_http_code "AW Server info" "$AW_SERVER/api/0/info" '^200$'
|
||||
check_http_code "AW Server CORS" "$AW_SERVER/api/0/settings/" '^200$'
|
||||
check_http_code "Worktime API health" "$AW_WORKTIME_API/health" '^200$'
|
||||
|
||||
section "4. Buckets (быстрый режим)"
|
||||
for entry in \
|
||||
"aw-watcher-afk|AFK watcher|Запустите на RDP: schtasks /Run /TN \"ActivityWatch Recovery\" или schtasks /Run /TN \"ActivityWatch Launch [SHARKON2025_Администратор]\"" \
|
||||
"aw-watcher-window|Window watcher|Запустите через ansible: ansible aw_windows -i $INVENTORY -m win_shell -a 'Start-Process -FilePath \"C:\\Program Files\\AWatch-rus\\bin\\aw-watcher-window\\aw-watcher-window.exe\" -ArgumentList @(\"--host\", \"10.10.10.13\", \"--port\", \"5600\") -WindowStyle Hidden'" \
|
||||
"aw-worktime-sessions|Worktime sessions|Проверьте работу worktime-api: systemctl status aw-worktime-api на AW сервере" \
|
||||
"aw-session-events|Session events|Проверьте collector-guard и aw-session-events-collector на RDP" \
|
||||
"aw-dlp-endpoint-signals|DLP signals|Запустите: ansible aw_windows -i $INVENTORY -m win_shell -a 'schtasks /Run /TN \"ActivityWatch Launch [SHARKON2025_Администратор]\"; Start-Sleep 30'" \
|
||||
"aw-dlp-incidents|DLP incidents|Проверьте aw-detmir-dlp-collector.ps1 на RDP (логи: C:\\ProgramData\\AWatch-rus\\logs\\)" \
|
||||
; do
|
||||
bucket="${entry%%|*}"; rest="${entry#*|}"
|
||||
label="${rest%%|*}"; remediation="${rest#*|}"
|
||||
check_bucket_freshness "$bucket" "$label" "$remediation"
|
||||
done
|
||||
echo ""
|
||||
echo "=== Быстрая диагностика завершена ==="
|
||||
printf "OK=%s WARN=%s FAIL=%s SKIP=%s\n" "$OK_COUNT" "$WARN_COUNT" "$FAIL_COUNT" "$SKIP_COUNT"
|
||||
[ "$FAIL_COUNT" -gt 0 ] && exit 2 || exit 0
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
section "3. AW Server (10.10.10.13)"
|
||||
# ============================================================
|
||||
check_http_json_key "AW Server info" "$AW_SERVER/api/0/info" \
|
||||
'.version' \
|
||||
"Проверьте: ssh igor@$AW_HOST 'systemctl status activitywatch-server'"
|
||||
check_http_code "AW Server CORS" "$AW_SERVER/api/0/settings/" '^200$'
|
||||
check_http_code "AW WebUI" "$AW_SERVER/" '^200$'
|
||||
check_http_json_key "Worktime API health" "$AW_WORKTIME_API/health" \
|
||||
'.status // .ok' \
|
||||
"Проверьте: ssh igor@$AW_HOST 'systemctl status aw-worktime-api && journalctl -u aw-worktime-api -n 20'"
|
||||
|
||||
# ============================================================
|
||||
section "4. Buckets (свежесть данных)"
|
||||
# ============================================================
|
||||
for entry in \
|
||||
"aw-watcher-afk|AFK watcher|Запустите на RDP: ansible aw_windows -i $INVENTORY -m win_shell -a 'schtasks /Run /TN \"ActivityWatch Recovery\"'" \
|
||||
"aw-watcher-window|Window watcher|Запустите: ansible aw_windows -i $INVENTORY -m win_shell -a 'schtasks /Run /TN \"ActivityWatch Launch [SHARKON2025_Администратор]\"; Start-Process -FilePath \"C:\\Program Files\\AWatch-rus\\bin\\aw-watcher-window\\aw-watcher-window.exe\" -ArgumentList @(\"--host\", \"10.10.10.13\", \"--port\", \"5600\") -WindowStyle Hidden'" \
|
||||
"aw-worktime-sessions|Worktime sessions|Проверьте: ssh igor@$AW_HOST 'systemctl status aw-worktime-api && journalctl -u aw-worktime-api -n 20'" \
|
||||
"aw-session-events|Session events|Проверьте collector-guard на RDP: ansible aw_windows -i $INVENTORY -m win_shell -a 'Get-Process -Name aw-session-events-* -ErrorAction SilentlyContinue'" \
|
||||
"aw-dlp-endpoint-signals|DLP endpoint signals|Запустите: ansible aw_windows -i $INVENTORY -m win_shell -a 'schtasks /Run /TN \"ActivityWatch Launch [SHARKON2025_Администратор]\"; Start-Sleep 60'" \
|
||||
"aw-dlp-incidents|DLP incidents|Проверьте: ansible aw_windows -i $INVENTORY -m win_shell -a \"Get-Content 'C:\\ProgramData\\AWatch-rus\\logs\\dlp-*.log' -Tail 20\"" \
|
||||
"aw-dlp-review|DLP review|Проверьте: ssh igor@$AW_HOST 'journalctl -u aw-dlp-policy-engine.service -n 20 --no-pager'" \
|
||||
"aw-dlp-rules|DLP rules|Проверьте: ssh igor@$AW_HOST 'journalctl -u aw-dlp-ioc-refresh.service -n 20 --no-pager'" \
|
||||
; do
|
||||
bucket="${entry%%|*}"; rest="${entry#*|}"
|
||||
label="${rest%%|*}"; remediation="${rest#*|}"
|
||||
check_bucket_freshness "$bucket" "$label" "$remediation"
|
||||
done
|
||||
|
||||
# ============================================================
|
||||
section "5. InfluxDB (10.10.10.10:8086)"
|
||||
# ============================================================
|
||||
check_http_json_key "InfluxDB health" "$INFLUXDB_URL/health" \
|
||||
'.status == "pass"' \
|
||||
"Проверьте InfluxDB на LXC 200: ssh igor@$PROXMOX_HOST 'sudo pct exec 200 -- systemctl status influxdb'"
|
||||
|
||||
# ============================================================
|
||||
section "6. Grafana (10.10.10.11:3000)"
|
||||
# ============================================================
|
||||
check_http_json_key "Grafana health" "$GRAFANA_URL/api/health" \
|
||||
'.database == "ok"' \
|
||||
"Проверьте: ssh igor@$PROXMOX_HOST 'sudo pct exec 201 -- systemctl status grafana-server'"
|
||||
check_http_code "Grafana datasources API" "$GRAFANA_URL/api/datasources" '^200$|^302$|^401$'
|
||||
|
||||
# ============================================================
|
||||
section "7. ClickHouse (10.10.10.2:8123)"
|
||||
# ============================================================
|
||||
# Проверяем через прямой HTTP — AUTHENTICATION_FAILED = сервер жив
|
||||
local_ch_ok=0
|
||||
ch_code=$(curl -sS --max-time 5 "http://$CLICKHOUSE_HOST:8123/?query=SELECT%201" 2>/dev/null | head -1)
|
||||
if echo "$ch_code" | grep -q "AUTHENTICATION_FAILED"; then
|
||||
pass "ClickHouse HTTP — отвечает (требуется аутентификация, это нормально)"
|
||||
local_ch_ok=1
|
||||
elif echo "$ch_code" | grep -q "1"; then
|
||||
pass "ClickHouse HTTP — SELECT 1 OK"
|
||||
local_ch_ok=1
|
||||
else
|
||||
fail "ClickHouse HTTP — не отвечает: $ch_code"
|
||||
echo " REMEDIATION: ssh igor@$PROXMOX_HOST 'cd /opt/activitywatch/clickhouse-1c && sudo docker compose ps; sudo docker compose logs --tail=20'"
|
||||
fi
|
||||
|
||||
# Проверка Docker контейнера через SSH
|
||||
container_status=$(ssh_pve 'sudo docker ps --filter name=aw-rus-1c-clickhouse --format "{{.Status}}" 2>/dev/null' 2>/dev/null)
|
||||
if [ -n "$container_status" ]; then
|
||||
pass "ClickHouse Docker контейнер: $container_status"
|
||||
else
|
||||
fail "ClickHouse Docker контейнер не запущен"
|
||||
echo " REMEDIATION: ssh igor@$PROXMOX_HOST 'cd /opt/activitywatch/clickhouse-1c && sudo docker compose up -d'"
|
||||
fi
|
||||
|
||||
# ClickHouse health timer
|
||||
check_service_remote "ClickHouse health timer" "$PROXMOX_HOST" "aw-1c-clickhouse-health.timer" \
|
||||
"Проверьте: ssh igor@$PROXMOX_HOST 'sudo journalctl -u aw-1c-clickhouse-health.service -n 30 --no-pager'"
|
||||
|
||||
# ClickHouse network health timer (с AW сервера)
|
||||
check_service_remote "ClickHouse network health timer" "$AW_HOST" "aw-clickhouse-network-health.timer" \
|
||||
"Проверьте: ssh igor@$AW_HOST 'sudo journalctl -u aw-clickhouse-network-health.service -n 30 --no-pager'"
|
||||
|
||||
# 1C-ingest timer
|
||||
check_service_remote "1C ingest timer" "$PROXMOX_HOST" "aw-1c-ingest.timer" \
|
||||
"Проверьте: ssh igor@$PROXMOX_HOST 'sudo systemctl status aw-1c-ingest.timer; sudo journalctl -u aw-1c-ingest.service -n 20'"
|
||||
|
||||
# ============================================================
|
||||
section "8. 1C Manager API (10.10.10.2:8710)"
|
||||
# ============================================================
|
||||
check_http_json_key "1C /api/health" "http://$PROXMOX_HOST:8710/api/health" \
|
||||
'.status == "ok"' \
|
||||
"Проверьте Python процесс: ssh igor@$PROXMOX_HOST 'ps aux | grep 8710 | grep -v grep'"
|
||||
check_http_code "1C /manager/brief" "http://$PROXMOX_HOST:8710/manager/brief" '^200$'
|
||||
|
||||
# ============================================================
|
||||
section "9. Nginx Gateway (10.10.10.2)"
|
||||
# ============================================================
|
||||
check_http_code "Gateway /healthz" "https://$PROXMOX_HOST/healthz" '^200$'
|
||||
check_http_code "Gateway /go/proxmox-gui (401=protected, OK)" "https://$PROXMOX_HOST/go/proxmox-gui" '^30[1278]$|^401$'
|
||||
check_http_code "Gateway /go/file1c-brief (401=protected, OK)" "https://$PROXMOX_HOST/go/file1c-brief" '^30[1278]$|^401$'
|
||||
|
||||
check_service_remote "Nginx service" "$PROXMOX_HOST" "nginx.service" \
|
||||
"Проверьте: ssh igor@$PROXMOX_HOST 'sudo systemctl status nginx; sudo nginx -t'"
|
||||
|
||||
# ============================================================
|
||||
section "10. DLP Pipeline (10.10.10.13)"
|
||||
# ============================================================
|
||||
# DLP Policy Engine — должен быть active (running)
|
||||
check_service_remote "DLP Policy Engine" "$AW_HOST" "aw-dlp-policy-engine.service" \
|
||||
"Проверьте: ssh igor@$AW_HOST 'sudo journalctl -u aw-dlp-policy-engine.service -n 30 --no-pager'"
|
||||
|
||||
# DLP Case Management
|
||||
check_service_remote "DLP Case Management" "$AW_HOST" "aw-dlp-case-management.service" \
|
||||
"Проверьте: ssh igor@$AW_HOST 'sudo journalctl -u aw-dlp-case-management.service -n 30 --no-pager'"
|
||||
|
||||
# DLP Aggregator
|
||||
aggr_status=$(ssh_aw 'systemctl is-active activitywatch-dlp-aggregator.timer 2>/dev/null || echo not_found' 2>/dev/null)
|
||||
if [ "$aggr_status" = "active" ]; then
|
||||
pass "DLP Aggregator timer (active)"
|
||||
else
|
||||
fail "DLP Aggregator timer ($aggr_status)"
|
||||
echo " REMEDIATION: ssh igor@$AW_HOST 'sudo systemctl enable --now activitywatch-dlp-aggregator.timer; sudo journalctl -u activitywatch-dlp-aggregator.service -n 30'"
|
||||
fi
|
||||
|
||||
# DLP Influx Exporter
|
||||
influx_exp_status=$(ssh_aw 'systemctl is-active aw-dlp-influx-exporter.timer 2>/dev/null || echo not_found' 2>/dev/null)
|
||||
if [ "$influx_exp_status" = "active" ]; then
|
||||
pass "DLP Influx Exporter timer (active)"
|
||||
else
|
||||
fail "DLP Influx Exporter timer ($influx_exp_status)"
|
||||
echo " REMEDIATION: ssh igor@$AW_HOST 'sudo systemctl enable --now aw-dlp-influx-exporter.timer; sudo journalctl -u aw-dlp-influx-exporter.service -n 30'"
|
||||
fi
|
||||
|
||||
# DLP CEF Exporter
|
||||
check_service_remote "DLP CEF Exporter timer" "$AW_HOST" "aw-dlp-cef-exporter.timer" \
|
||||
"ssh igor@$AW_HOST 'sudo systemctl enable --now aw-dlp-cef-exporter.timer; journalctl -u aw-dlp-cef-exporter.service -n 20'"
|
||||
|
||||
# DLP IOC Refresh
|
||||
check_service_remote "DLP IOC Refresh timer" "$AW_HOST" "aw-dlp-ioc-refresh.timer" \
|
||||
"ssh igor@$AW_HOST 'sudo systemctl enable --now aw-dlp-ioc-refresh.timer'"
|
||||
|
||||
# DLP Syslog Forwarder
|
||||
check_service_remote "DLP Syslog Forwarder timer" "$AW_HOST" "aw-dlp-syslog-forwarder.timer" \
|
||||
"ssh igor@$AW_HOST 'sudo systemctl enable --now aw-dlp-syslog-forwarder.timer'"
|
||||
|
||||
# DLP Webhook Sender
|
||||
check_service_remote "DLP Webhook Sender timer" "$AW_HOST" "aw-dlp-webhook-sender.timer" \
|
||||
"ssh igor@$AW_HOST 'sudo systemctl enable --now aw-dlp-webhook-sender.timer'"
|
||||
|
||||
# DLP Report Scheduler
|
||||
check_service_remote "DLP Report Scheduler timer" "$AW_HOST" "aw-dlp-report-scheduler.timer" \
|
||||
"ssh igor@$AW_HOST 'sudo systemctl enable --now aw-dlp-report-scheduler.timer'"
|
||||
|
||||
# Worktime Influx Exporter
|
||||
check_service_remote "Worktime Influx Exporter timer" "$AW_HOST" "aw-worktime-influx-exporter.timer" \
|
||||
"ssh igor@$AW_HOST 'sudo systemctl enable --now aw-worktime-influx-exporter.timer; journalctl -u aw-worktime-influx-exporter.service -n 20'"
|
||||
|
||||
# ============================================================
|
||||
if [ "$SKIP_WINDOWS" = "1" ]; then
|
||||
skip "Проверки RDP хоста пропущены (--skip-windows)"
|
||||
else
|
||||
section "11. RDP хост (192.168.100.18)"
|
||||
if have ansible && [ -f "$INVENTORY" ]; then
|
||||
check_ansible_module "WinRM ping" aw_windows win_ping
|
||||
|
||||
check_ansible_win_shell "Сессии RDP" 'query user 2>&1'
|
||||
|
||||
check_ansible_win_shell "Процессы watcher" \
|
||||
'Get-Process aw-watcher-afk,aw-watcher-window -ErrorAction SilentlyContinue | Select-Object Name,Id,SessionId,StartTime | Format-Table -AutoSize'
|
||||
|
||||
check_ansible_win_shell "Количество процессов powershell" \
|
||||
'(Get-Process powershell -ErrorAction SilentlyContinue | Measure-Object).Count'
|
||||
|
||||
check_ansible_win_shell "Scheduled tasks (Recovery)" \
|
||||
'schtasks /Query /TN "ActivityWatch Recovery" /FO LIST /V | Select-String "Status|Run|Next"'
|
||||
|
||||
check_ansible_win_shell "Scheduled tasks (Launch Admin)" \
|
||||
'schtasks /Query /TN "ActivityWatch Launch [SHARKON2025_Администратор]" /FO LIST /V | Select-String "Status|Run|Next"'
|
||||
else
|
||||
skip "Ansible или inventory не найдены"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
section "12. Systemd health (AW server)"
|
||||
# ============================================================
|
||||
check_ansible_shell "AW server core units" aw_server \
|
||||
'systemctl is-active activitywatch-server aw-worktime-api aw-dlp-policy-engine aw-dlp-case-management aw-worktime-influx-exporter.timer aw-dlp-influx-exporter.timer activitywatch-dlp-aggregator.timer aw-clickhouse-network-health.timer | paste -sd,'
|
||||
|
||||
check_ansible_shell "AW server — нет failed units" aw_server \
|
||||
'failed=$(systemctl --failed --no-legend | awk "{print \$1}" | grep -E "activitywatch|aw-|dlp" || true); test -z "$failed" && echo "no AW-related failed units" || { echo "$failed"; exit 1; }'
|
||||
|
||||
# ============================================================
|
||||
section "13. Systemd health (Proxmox)"
|
||||
# ============================================================
|
||||
check_ansible_shell "Proxmox core units" proxmox \
|
||||
'systemctl is-active nginx docker aw-1c-clickhouse-health.timer aw-1c-ingest.timer 2>/dev/null | paste -sd,'
|
||||
|
||||
# ============================================================
|
||||
section "14. Диск и память"
|
||||
# ============================================================
|
||||
check_ansible_shell "Диски AW server" aw_server 'df -h / /var /opt 2>/dev/null | tail -5'
|
||||
check_ansible_shell "Память AW server" aw_server 'free -h | tail -5'
|
||||
|
||||
# ============================================================
|
||||
section "Итог диагностики"
|
||||
# ============================================================
|
||||
printf " OK=%s WARN=%s FAIL=%s SKIP=%s\n" "$OK_COUNT" "$WARN_COUNT" "$FAIL_COUNT" "$SKIP_COUNT"
|
||||
|
||||
if [ "$FAIL_COUNT" -gt 0 ]; then
|
||||
echo ""
|
||||
echo " Есть проблемы! Смотрите REMEDIATION выше для каждого FAIL."
|
||||
echo " После исправления запустите повторно: $0"
|
||||
exit 2
|
||||
elif [ "$WARN_COUNT" -gt 0 ]; then
|
||||
echo ""
|
||||
echo " Есть предупреждения (WARN) — стоит проверить, но не критично."
|
||||
exit 1
|
||||
else
|
||||
echo ""
|
||||
echo " Все проверки пройдены. Контур в рабочем состоянии."
|
||||
exit 0
|
||||
fi
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/check_production_inventory_placeholders.sh [--allow-missing] [--strict] FILE...
|
||||
DETMIR_PRODUCTION_CONFIG_PATHS="file1:file2" scripts/check_production_inventory_placeholders.sh
|
||||
scripts/check_production_inventory_placeholders.sh --self-test
|
||||
|
||||
Fails when production inventory/env files contain public placeholder values:
|
||||
TEST-NET addresses, HOST-EXAMPLE, WINDOWS_USER_EXAMPLE, CHANGE_ME, YOUR_*,
|
||||
replace-me, or angle-bracket placeholders.
|
||||
|
||||
Use --strict for private production override files. Strict mode requires at
|
||||
least one existing path and rejects public/example/default files.
|
||||
EOF
|
||||
}
|
||||
|
||||
pattern='(192\.0\.2\.|198\.51\.100\.|203\.0\.113\.|HOST-EXAMPLE|WINDOWS_USER_EXAMPLE|CHANGE_ME|CHANGEME|REPLACE_ME|replace-me|YOUR_[A-Z0-9_]*|<[A-Z0-9_ -]+>)'
|
||||
allow_missing=0
|
||||
strict=0
|
||||
self_test=0
|
||||
paths=()
|
||||
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--allow-missing)
|
||||
allow_missing=1
|
||||
;;
|
||||
--strict)
|
||||
strict=1
|
||||
;;
|
||||
--self-test)
|
||||
self_test=1
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
paths+=("$1")
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
run_scan() {
|
||||
local file
|
||||
local found=0
|
||||
for file in "$@"; do
|
||||
if (( strict == 1 )) && [[ "$file" == ansible/group_vars* || "$file" == ansible/inventory.ini || "$file" == *".example."* || "$file" == *"example."* ]]; then
|
||||
printf 'strict mode refuses public/default config path: %s\n' "$file" >&2
|
||||
found=1
|
||||
continue
|
||||
fi
|
||||
if [[ ! -e "$file" ]]; then
|
||||
if (( allow_missing == 0 )); then
|
||||
printf 'missing production config path: %s\n' "$file" >&2
|
||||
found=1
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if [[ -d "$file" ]]; then
|
||||
while IFS= read -r -d '' child; do
|
||||
if grep -nE "$pattern" "$child" >/dev/null; then
|
||||
printf 'placeholder found in %s\n' "$child" >&2
|
||||
grep -nE "$pattern" "$child" | sed -E 's/^([0-9]+):.*/ line \1: placeholder marker/' >&2
|
||||
found=1
|
||||
fi
|
||||
done < <(find "$file" -type f \( -name '*.env' -o -name '*.ini' -o -name '*.yml' -o -name '*.yaml' \) -print0)
|
||||
elif grep -nE "$pattern" "$file" >/dev/null; then
|
||||
printf 'placeholder found in %s\n' "$file" >&2
|
||||
grep -nE "$pattern" "$file" | sed -E 's/^([0-9]+):.*/ line \1: placeholder marker/' >&2
|
||||
found=1
|
||||
fi
|
||||
done
|
||||
return "$found"
|
||||
}
|
||||
|
||||
if (( self_test == 1 )); then
|
||||
tmp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
good="$tmp_dir/good.env"
|
||||
bad="$tmp_dir/bad.env"
|
||||
cat >"$good" <<'EOF'
|
||||
AW_WORKTIME_INFLUX_URL=http://influxdb.internal:8086
|
||||
AW_WORKTIME_INFLUX_HOSTS=WINDOWS-HOST
|
||||
AW_WORKTIME_INFLUX_TOKEN=prod-write-token-value
|
||||
EOF
|
||||
cat >"$bad" <<'EOF'
|
||||
AW_WORKTIME_INFLUX_URL=http://192.0.2.10:8086
|
||||
AW_WORKTIME_INFLUX_HOSTS=HOST-EXAMPLE
|
||||
AW_WORKTIME_INFLUX_TOKEN=CHANGE_ME
|
||||
EOF
|
||||
run_scan "$good"
|
||||
if run_scan "$bad" >/dev/null 2>&1; then
|
||||
echo "self-test failed: bad fixture was accepted" >&2
|
||||
exit 1
|
||||
fi
|
||||
saved_strict="$strict"
|
||||
strict=1
|
||||
if run_scan "ansible/group_vars/all.yml" >/dev/null 2>&1; then
|
||||
echo "self-test failed: strict mode accepted public/default file name" >&2
|
||||
exit 1
|
||||
fi
|
||||
strict="$saved_strict"
|
||||
echo "production inventory placeholder guard self-test: OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ((${#paths[@]} == 0)) && [[ -n "${DETMIR_PRODUCTION_CONFIG_PATHS:-}" ]]; then
|
||||
IFS=':' read -r -a paths <<<"$DETMIR_PRODUCTION_CONFIG_PATHS"
|
||||
fi
|
||||
|
||||
if ((${#paths[@]} == 0)); then
|
||||
if (( allow_missing == 1 && strict == 0 )); then
|
||||
echo "production inventory placeholder guard: skipped (no production paths)"
|
||||
exit 0
|
||||
fi
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
run_scan "${paths[@]}"
|
||||
echo "production inventory placeholder guard: OK"
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
"$SCRIPT_DIR/detmir-full-diagnostics.sh" --scope daily "$@"
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
"$SCRIPT_DIR/detmir-full-diagnostics.sh" --scope monthly "$@"
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
"$SCRIPT_DIR/detmir-full-diagnostics.sh" --scope weekly "$@"
|
||||
@@ -0,0 +1,20 @@
|
||||
## Базовые параметры для расширенной диагностики контуров DetMir
|
||||
DETMIR_FULL_DIAGNOSTICS_SCOPE=daily
|
||||
DETMIR_FULL_DIAGNOSTICS_OUTPUT_DIR=/var/log/detmir-full-diagnostics
|
||||
|
||||
# Опционально указывайте явные пути.
|
||||
# Если переменная не задана, используются пути относительно этой папки:
|
||||
# - aw-contour-diag.sh: ./aw-contour-diag.sh
|
||||
# - detmir-support-run.sh: ../detmir-support-run.sh
|
||||
# - detmir-support.env: ../detmir-support.env
|
||||
DETMIR_FULL_DIAGNOSTICS_AW_DIAG_SCRIPT=
|
||||
DETMIR_FULL_DIAGNOSTICS_AW_DIAG_QUICK=0
|
||||
DETMIR_FULL_DIAGNOSTICS_AW_DIAG_SKIP_WINDOWS=0
|
||||
DETMIR_FULL_DIAGNOSTICS_SUPPORT_RUN=
|
||||
DETMIR_FULL_DIAGNOSTICS_SUPPORT_ENV=
|
||||
|
||||
# Проверка placeholder-значений в production-конфиге (по желанию)
|
||||
DETMIR_FULL_DIAGNOSTICS_PLACEHOLDER_GUARD=1
|
||||
DETMIR_FULL_DIAGNOSTICS_PLACEHOLDER_SCRIPT=
|
||||
# Если путь пустой — проверка placeholder-guard будет пропущена.
|
||||
DETMIR_FULL_DIAGNOSTICS_PLACEHOLDER_PATHS=
|
||||
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
ENV_FILE="${DETMIR_FULL_DIAGNOSTICS_ENV_FILE:-$SCRIPT_DIR/detmir-full-diagnostics.env}"
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
fi
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage:
|
||||
scripts/detmir-full-diagnostics/detmir-full-diagnostics.sh [options]
|
||||
|
||||
Options:
|
||||
--scope daily|weekly|monthly Scope of operational checks (default: daily)
|
||||
--output-dir PATH Root output directory for all checks
|
||||
--quick Pass --quick to aw-contour-diag
|
||||
--skip-windows Skip WinRM checks in aw-contour-diag
|
||||
--no-support Skip detmir-support-run checks
|
||||
--no-aw-diagnostic Skip aw-contour-diag
|
||||
--no-placeholder-check Skip production placeholder guard check
|
||||
--help Show this help
|
||||
|
||||
Examples:
|
||||
scripts/detmir-full-diagnostics/detmir-full-diagnostics.sh --scope daily
|
||||
scripts/detmir-full-diagnostics/detmir-full-diagnostics.sh --scope weekly --quick
|
||||
scripts/detmir-full-diagnostics/detmir-full-diagnostics-daily.sh
|
||||
USAGE
|
||||
}
|
||||
|
||||
SCOPE="${DETMIR_FULL_DIAGNOSTICS_SCOPE:-daily}"
|
||||
OUTPUT_DIR="${DETMIR_FULL_DIAGNOSTICS_OUTPUT_DIR:-$REPO_ROOT/output/detmir-full-diagnostics}"
|
||||
RUN_AW_DIAG=1
|
||||
RUN_SUPPORT=1
|
||||
RUN_PLACEHOLDER_GUARD=1
|
||||
AW_QUICK="${DETMIR_FULL_DIAGNOSTICS_AW_DIAG_QUICK:-0}"
|
||||
AW_SKIP_WINDOWS="${DETMIR_FULL_DIAGNOSTICS_AW_DIAG_SKIP_WINDOWS:-0}"
|
||||
|
||||
AW_DIAG_SCRIPT="${DETMIR_FULL_DIAGNOSTICS_AW_DIAG_SCRIPT:-$SCRIPT_DIR/aw-contour-diag.sh}"
|
||||
SUPPORT_RUN_SCRIPT="${DETMIR_FULL_DIAGNOSTICS_SUPPORT_RUN:-$SCRIPT_DIR/../detmir-support-run.sh}"
|
||||
SUPPORT_ENV_FILE="${DETMIR_FULL_DIAGNOSTICS_SUPPORT_ENV:-$SCRIPT_DIR/../detmir-support.env}"
|
||||
PLACEHOLDER_SCRIPT="${DETMIR_FULL_DIAGNOSTICS_PLACEHOLDER_SCRIPT:-$SCRIPT_DIR/../check_production_inventory_placeholders.sh}"
|
||||
PLACEHOLDER_GUARD="${DETMIR_FULL_DIAGNOSTICS_PLACEHOLDER_GUARD:-1}"
|
||||
PLACEHOLDER_PATHS="${DETMIR_FULL_DIAGNOSTICS_PLACEHOLDER_PATHS:-$REPO_ROOT/ansible:$REPO_ROOT/private-config}"
|
||||
|
||||
while (( $# > 0 )); do
|
||||
case "$1" in
|
||||
--scope)
|
||||
if (( $# < 2 )); then
|
||||
usage
|
||||
echo "error: --scope requires daily|weekly|monthly" >&2
|
||||
exit 2
|
||||
fi
|
||||
SCOPE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--output-dir)
|
||||
if (( $# < 2 )); then
|
||||
usage
|
||||
echo "error: --output-dir requires path" >&2
|
||||
exit 2
|
||||
fi
|
||||
OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--quick)
|
||||
AW_QUICK=1
|
||||
shift
|
||||
;;
|
||||
--skip-windows)
|
||||
AW_SKIP_WINDOWS=1
|
||||
shift
|
||||
;;
|
||||
--no-support)
|
||||
RUN_SUPPORT=0
|
||||
shift
|
||||
;;
|
||||
--no-aw-diagnostic)
|
||||
RUN_AW_DIAG=0
|
||||
shift
|
||||
;;
|
||||
--no-placeholder-check)
|
||||
RUN_PLACEHOLDER_GUARD=0
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
echo "error: unknown arg '$1'" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$SCOPE" != "daily" && "$SCOPE" != "weekly" && "$SCOPE" != "monthly" ]]; then
|
||||
echo "error: invalid scope '$SCOPE', expected daily|weekly|monthly" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ "$RUN_PLACEHOLDER_GUARD" == "1" ]]; then
|
||||
RUN_PLACEHOLDER_GUARD="$PLACEHOLDER_GUARD"
|
||||
fi
|
||||
|
||||
RUN_DIR="$OUTPUT_DIR/$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
mkdir -p "$RUN_DIR"
|
||||
LOG_FILE="$RUN_DIR/full-diagnostics.log"
|
||||
|
||||
exec > >(tee -a "$LOG_FILE") 2>&1
|
||||
|
||||
TOTAL=0
|
||||
OK_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
|
||||
log() {
|
||||
printf '%s %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*"
|
||||
}
|
||||
|
||||
run_step() {
|
||||
local step="$1"
|
||||
shift
|
||||
|
||||
TOTAL=$((TOTAL + 1))
|
||||
log "----"
|
||||
log "STEP [$TOTAL]: $step"
|
||||
|
||||
if "$@"; then
|
||||
OK_COUNT=$((OK_COUNT + 1))
|
||||
log "RESULT: OK"
|
||||
return 0
|
||||
fi
|
||||
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
log "RESULT: FAIL"
|
||||
return 1
|
||||
}
|
||||
|
||||
log "DetMir full diagnostics started"
|
||||
log "Repository: $REPO_ROOT"
|
||||
log "Scope: $SCOPE"
|
||||
log "Output: $RUN_DIR"
|
||||
log "Log: $LOG_FILE"
|
||||
|
||||
if [[ ! -x "$AW_DIAG_SCRIPT" ]] && (( RUN_AW_DIAG == 1 )); then
|
||||
log "WARN: aw-contour-diag script not found or not executable: $AW_DIAG_SCRIPT"
|
||||
log " Диагностика AW-контуром будет пропущена."
|
||||
RUN_AW_DIAG=0
|
||||
fi
|
||||
|
||||
if [[ ! -x "$SUPPORT_RUN_SCRIPT" ]] && (( RUN_SUPPORT == 1 )); then
|
||||
log "ERROR: support script not found or not executable: $SUPPORT_RUN_SCRIPT"
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -f "$SUPPORT_ENV_FILE" ]] && (( RUN_SUPPORT == 1 )); then
|
||||
log "WARN: support env file not found, defaults will be used from detmir-support-run defaults: $SUPPORT_ENV_FILE"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$PLACEHOLDER_SCRIPT" ]] && (( RUN_PLACEHOLDER_GUARD == 1 )); then
|
||||
log "WARN: placeholder guard script not found: $PLACEHOLDER_SCRIPT"
|
||||
RUN_PLACEHOLDER_GUARD=0
|
||||
fi
|
||||
|
||||
if (( RUN_AW_DIAG == 1 )); then
|
||||
if [[ -x "$AW_DIAG_SCRIPT" ]]; then
|
||||
AW_ARGS=()
|
||||
if [[ "$AW_QUICK" == "1" ]]; then
|
||||
AW_ARGS+=(--quick)
|
||||
fi
|
||||
if [[ "$AW_SKIP_WINDOWS" == "1" ]]; then
|
||||
AW_ARGS+=(--skip-windows)
|
||||
fi
|
||||
run_step "aw-contour-diag (scope=$SCOPE)" "$AW_DIAG_SCRIPT" "${AW_ARGS[@]}" || true
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( RUN_SUPPORT == 1 )); then
|
||||
run_step "detmir-support-run --scope $SCOPE" \
|
||||
env \
|
||||
DETMIR_SUPPORT_ENV_FILE="$SUPPORT_ENV_FILE" \
|
||||
DETMIR_SUPPORT_OUTPUT_DIR="$RUN_DIR/support" \
|
||||
"$SUPPORT_RUN_SCRIPT" --scope "$SCOPE" --output-dir "$RUN_DIR/support" || true
|
||||
fi
|
||||
|
||||
if [[ "$RUN_PLACEHOLDER_GUARD" == "1" ]]; then
|
||||
if [[ -z "${PLACEHOLDER_PATHS// }" ]]; then
|
||||
log "WARN: DETMIR_FULL_DIAGNOSTICS_PLACEHOLDER_PATHS is empty. Placeholder guard skipped."
|
||||
else
|
||||
run_step "check production placeholders" \
|
||||
env DETMIR_PRODUCTION_CONFIG_PATHS="$PLACEHOLDER_PATHS" \
|
||||
"$PLACEHOLDER_SCRIPT" --allow-missing || true
|
||||
fi
|
||||
fi
|
||||
|
||||
SUMMARY_FILE="$RUN_DIR/summary.txt"
|
||||
{
|
||||
printf "Scope: %s\n" "$SCOPE"
|
||||
printf "Total steps: %s\n" "$TOTAL"
|
||||
printf "OK: %s\n" "$OK_COUNT"
|
||||
printf "FAIL: %s\n" "$FAIL_COUNT"
|
||||
if (( FAIL_COUNT == 0 )); then
|
||||
printf "Result: OK\n"
|
||||
else
|
||||
printf "Result: FAIL\n"
|
||||
fi
|
||||
} > "$SUMMARY_FILE"
|
||||
|
||||
log "Summary: $SUMMARY_FILE"
|
||||
log "DetMir full diagnostics completed"
|
||||
|
||||
if (( FAIL_COUNT > 0 )); then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
"$SCRIPT_DIR/detmir-support-run.sh" --scope daily "$@"
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
"$SCRIPT_DIR/detmir-support-run.sh" --scope monthly "$@"
|
||||
@@ -0,0 +1,694 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
DEFAULT_ENV_FILE="$SCRIPT_DIR/detmir-support.env"
|
||||
ENV_FILE="${DETMIR_SUPPORT_ENV_FILE:-$DEFAULT_ENV_FILE}"
|
||||
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
fi
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage:
|
||||
scripts/detmir-support-run.sh --scope daily|weekly|monthly [--output-dir PATH]
|
||||
scripts/detmir-support-daily.sh
|
||||
scripts/detmir-support-weekly.sh
|
||||
scripts/detmir-support-monthly.sh
|
||||
|
||||
Arguments:
|
||||
--scope daily|weekly|monthly (default: daily)
|
||||
--output-dir directory for logs/reports (default: output/detmir-support)
|
||||
--help show help
|
||||
USAGE
|
||||
}
|
||||
|
||||
SCOPE="daily"
|
||||
OUTPUT_DIR="${DETMIR_SUPPORT_OUTPUT_DIR:-$REPO_ROOT/output/detmir-support}"
|
||||
|
||||
while (( $# > 0 )); do
|
||||
case "$1" in
|
||||
--scope)
|
||||
if (( $# < 2 )); then
|
||||
usage
|
||||
echo "error: --scope requires daily|weekly|monthly" >&2
|
||||
exit 2
|
||||
fi
|
||||
SCOPE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--output-dir)
|
||||
if (( $# < 2 )); then
|
||||
usage
|
||||
echo "error: --output-dir requires directory path" >&2
|
||||
exit 2
|
||||
fi
|
||||
OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
echo "error: unknown argument '$1'" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$SCOPE" != "daily" && "$SCOPE" != "weekly" && "$SCOPE" != "monthly" ]]; then
|
||||
echo "error: invalid scope '$SCOPE', expected daily|weekly|monthly" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
DETMIR_SUPPORT_PVE_HOST="${DETMIR_SUPPORT_PVE_HOST:-10.10.10.2}"
|
||||
DETMIR_SUPPORT_AW_HOST="${DETMIR_SUPPORT_AW_HOST:-10.10.10.13}"
|
||||
DETMIR_SUPPORT_WEB_HOST="${DETMIR_SUPPORT_WEB_HOST:-10.10.10.11}"
|
||||
DETMIR_SUPPORT_WINDOWS_HOST="${DETMIR_SUPPORT_WINDOWS_HOST:-192.168.100.18}"
|
||||
DETMIR_SUPPORT_PFSENSE_HOST="${DETMIR_SUPPORT_PFSENSE_HOST:-}"
|
||||
DETMIR_SUPPORT_PFSENSE_URL="${DETMIR_SUPPORT_PFSENSE_URL:-}"
|
||||
DETMIR_SUPPORT_OPENVPN_HOST="${DETMIR_SUPPORT_OPENVPN_HOST:-}"
|
||||
DETMIR_SUPPORT_OPENVPN_WEB_URL="${DETMIR_SUPPORT_OPENVPN_WEB_URL:-}"
|
||||
DETMIR_SUPPORT_SURICATA_HOST="${DETMIR_SUPPORT_SURICATA_HOST:-$DETMIR_SUPPORT_PVE_HOST}"
|
||||
DETMIR_SUPPORT_WEB_TLS_HOST="${DETMIR_SUPPORT_WEB_TLS_HOST:-$DETMIR_SUPPORT_WEB_HOST}"
|
||||
|
||||
DETMIR_SUPPORT_SSH_USER="${DETMIR_SUPPORT_SSH_USER:-root}"
|
||||
DETMIR_SUPPORT_SSH_IDENTITY="${DETMIR_SUPPORT_SSH_IDENTITY:-}"
|
||||
DETMIR_SUPPORT_SKIP_REMOTE="${DETMIR_SUPPORT_SKIP_REMOTE:-0}"
|
||||
DETMIR_SUPPORT_CONNECT_TIMEOUT="${DETMIR_SUPPORT_CONNECT_TIMEOUT:-8}"
|
||||
|
||||
DETMIR_SUPPORT_PVE_SERVICES="${DETMIR_SUPPORT_PVE_SERVICES:-pve-cluster pvedaemon pvestatd pveproxy pvedaemon.service pvestatd.service}"
|
||||
DETMIR_SUPPORT_AW_SERVICES="${DETMIR_SUPPORT_AW_SERVICES:-activitywatch-server aw-server-rust}"
|
||||
DETMIR_SUPPORT_WEB_SERVICES="${DETMIR_SUPPORT_WEB_SERVICES:-nginx apache2}"
|
||||
DETMIR_SUPPORT_SURICATA_SERVICES="${DETMIR_SUPPORT_SURICATA_SERVICES:-suricata}"
|
||||
DETMIR_SUPPORT_PVE_VM_IDS="${DETMIR_SUPPORT_PVE_VM_IDS:-}"
|
||||
DETMIR_SUPPORT_PVE_BACKUP_DIRS="${DETMIR_SUPPORT_PVE_BACKUP_DIRS:-/var/lib/vz/dump}"
|
||||
DETMIR_SUPPORT_BACKUP_MAX_AGE_DAYS="${DETMIR_SUPPORT_BACKUP_MAX_AGE_DAYS:-14}"
|
||||
|
||||
DETMIR_SUPPORT_DISK_WARN_PERCENT="${DETMIR_SUPPORT_DISK_WARN_PERCENT:-85}"
|
||||
DETMIR_SUPPORT_DISK_CRIT_PERCENT="${DETMIR_SUPPORT_DISK_CRIT_PERCENT:-93}"
|
||||
DETMIR_SUPPORT_LOG_WARNING_LIMIT="${DETMIR_SUPPORT_LOG_WARNING_LIMIT:-5}"
|
||||
DETMIR_SUPPORT_LOG_CRIT_LIMIT="${DETMIR_SUPPORT_LOG_CRIT_LIMIT:-20}"
|
||||
DETMIR_SUPPORT_TLS_WARN_DAYS="${DETMIR_SUPPORT_TLS_WARN_DAYS:-30}"
|
||||
DETMIR_SUPPORT_TLS_CRIT_DAYS="${DETMIR_SUPPORT_TLS_CRIT_DAYS:-14}"
|
||||
DETMIR_SUPPORT_HTTP_WARNING_LATENCY_MS="${DETMIR_SUPPORT_HTTP_WARNING_LATENCY_MS:-2000}"
|
||||
|
||||
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
RUN_DIR="$OUTPUT_DIR/$(date -u +%F)"
|
||||
mkdir -p "$RUN_DIR"
|
||||
|
||||
LOG_FILE="$RUN_DIR/support-$SCOPE-$RUN_ID.log"
|
||||
CSV_FILE="$RUN_DIR/support-$SCOPE-$RUN_ID.csv"
|
||||
MD_FILE="$RUN_DIR/support-$SCOPE-$RUN_ID.md"
|
||||
|
||||
TOTAL=0
|
||||
OK_COUNT=0
|
||||
WARN_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
SKIP_COUNT=0
|
||||
REMOTE_OUTPUT=""
|
||||
|
||||
printf '%s\n' "timestamp;scope;section;status;check;result;action" > "$CSV_FILE"
|
||||
|
||||
log() {
|
||||
printf '%s %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
sanitize() {
|
||||
local value="$1"
|
||||
value="${value//$'\n'/ }"
|
||||
value="${value//$'\r'/ }"
|
||||
value="${value//;/ }"
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
record() {
|
||||
local section="$1"
|
||||
local status="$2"
|
||||
local check_name="$3"
|
||||
local result="$4"
|
||||
local action="${5:-}"
|
||||
|
||||
section="$(sanitize "$section")"
|
||||
check_name="$(sanitize "$check_name")"
|
||||
status="$(sanitize "$status")"
|
||||
result="$(sanitize "$result")"
|
||||
action="$(sanitize "$action")"
|
||||
|
||||
TOTAL=$((TOTAL + 1))
|
||||
case "$status" in
|
||||
OK) OK_COUNT=$((OK_COUNT + 1)) ;;
|
||||
WARN) WARN_COUNT=$((WARN_COUNT + 1)) ;;
|
||||
FAIL) FAIL_COUNT=$((FAIL_COUNT + 1)) ;;
|
||||
SKIP) SKIP_COUNT=$((SKIP_COUNT + 1)) ;;
|
||||
esac
|
||||
|
||||
printf '%s;%s;%s;%s;%s;%s;%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$SCOPE" "$section" "$status" "$check_name" "$result" "$action" >> "$CSV_FILE"
|
||||
case "$status" in
|
||||
OK) log "[OK] ${section} :: ${check_name} -- ${result}" ;;
|
||||
WARN) log "[WARN] ${section} :: ${check_name} -- ${result}" ;;
|
||||
FAIL) log "[FAIL] ${section} :: ${check_name} -- ${result}" ;;
|
||||
SKIP) log "[SKIP] ${section} :: ${check_name} -- ${result}" ;;
|
||||
esac
|
||||
if [[ -n "$action" ]]; then
|
||||
log " action: $action"
|
||||
fi
|
||||
}
|
||||
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
record "Подготовка" FAIL "$1" "not found" "Install command and re-run"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_remote() {
|
||||
local host="$1"
|
||||
shift
|
||||
local remote_cmd="$*"
|
||||
|
||||
if [[ "$DETMIR_SUPPORT_SKIP_REMOTE" == "1" ]]; then
|
||||
return 125
|
||||
fi
|
||||
|
||||
local -a ssh_opts=(
|
||||
-o BatchMode=yes
|
||||
-o ConnectTimeout="$DETMIR_SUPPORT_CONNECT_TIMEOUT"
|
||||
-o StrictHostKeyChecking=no
|
||||
-o UserKnownHostsFile=/dev/null
|
||||
)
|
||||
if [[ -n "$DETMIR_SUPPORT_SSH_IDENTITY" ]]; then
|
||||
ssh_opts=("-i" "$DETMIR_SUPPORT_SSH_IDENTITY" "${ssh_opts[@]}")
|
||||
fi
|
||||
|
||||
if REMOTE_OUTPUT="$(printf '%s\n' "$remote_cmd" | ssh "${ssh_opts[@]}" "${DETMIR_SUPPORT_SSH_USER}@${host}" bash -s 2>&1)"; then
|
||||
return 0
|
||||
fi
|
||||
return "$?"
|
||||
}
|
||||
|
||||
check_tcp() {
|
||||
local section="$1" host="$2" port="$3" name="$4"
|
||||
if timeout 8 bash -c ": >/dev/tcp/$host/$port" >/dev/null 2>&1; then
|
||||
record "$section" OK "$name" "${host}:${port} reachable"
|
||||
else
|
||||
record "$section" FAIL "$name" "${host}:${port} unreachable"
|
||||
fi
|
||||
}
|
||||
|
||||
check_http() {
|
||||
local section="$1" url="$2" expected_code="$3" name="$4"
|
||||
local tmp_file code latency_ms
|
||||
tmp_file="$(mktemp)"
|
||||
code="$(curl -ksS --connect-timeout 5 --max-time 20 -o "$tmp_file" -w '%{http_code};%{time_total}' "$url" 2>/dev/null || true)"
|
||||
latency_ms="${code#*;}"
|
||||
code="${code%;*}"
|
||||
latency_ms="$(awk "BEGIN { printf \"%.0f\", ${latency_ms:-0} * 1000 }")"
|
||||
if [[ -z "$code" ]]; then
|
||||
record "$section" WARN "$name" "No HTTP response" "Check service, DNS and auth"
|
||||
rm -f "$tmp_file"
|
||||
return
|
||||
fi
|
||||
|
||||
if echo "$code" | grep -Eq "$expected_code"; then
|
||||
if (( latency_ms > DETMIR_SUPPORT_HTTP_WARNING_LATENCY_MS )); then
|
||||
record "$section" WARN "$name" "$url -> HTTP ${code}, latency ${latency_ms}ms" "Check service latency/perf"
|
||||
else
|
||||
record "$section" OK "$name" "$url -> HTTP ${code}, latency ${latency_ms}ms"
|
||||
fi
|
||||
else
|
||||
record "$section" WARN "$name" "$url -> HTTP ${code}" "Verify endpoint auth/cert/rewrite"
|
||||
fi
|
||||
rm -f "$tmp_file"
|
||||
}
|
||||
|
||||
check_tls() {
|
||||
local section="$1" host="$2" port="$3" name="$4"
|
||||
if ! command -v openssl >/dev/null 2>&1; then
|
||||
record "$section" SKIP "$name" "openssl unavailable" "Install openssl to validate cert"
|
||||
return
|
||||
fi
|
||||
|
||||
local end_line now_ts end_ts days_left
|
||||
end_line="$(timeout 8 sh -c "openssl s_client -connect '$host:$port' -servername '$host' </dev/null 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | sed -n 's/^notAfter=//p' || true")"
|
||||
if [[ -z "$end_line" ]]; then
|
||||
record "$section" WARN "$name" "Unable to read certificate" "Check TLS termination"
|
||||
return
|
||||
fi
|
||||
|
||||
now_ts="$(date -u +%s)"
|
||||
end_ts="$(date -d "$end_line" +%s 2>/dev/null || true)"
|
||||
if [[ -z "$end_ts" ]]; then
|
||||
record "$section" WARN "$name" "Certificate expiry parse failed" "Update openssl/timezone settings"
|
||||
return
|
||||
fi
|
||||
|
||||
days_left=$(( (end_ts - now_ts) / 86400 ))
|
||||
if (( days_left < 0 )); then
|
||||
record "$section" FAIL "$name" "Certificate already expired (${days_left#-} days ago)" "Renew certificate immediately"
|
||||
elif (( days_left <= DETMIR_SUPPORT_TLS_CRIT_DAYS )); then
|
||||
record "$section" FAIL "$name" "Expires in ${days_left} days" "Renew certificate before expiry"
|
||||
elif (( days_left <= DETMIR_SUPPORT_TLS_WARN_DAYS )); then
|
||||
record "$section" WARN "$name" "Expires in ${days_left} days" "Plan renewal window"
|
||||
else
|
||||
record "$section" OK "$name" "Expires in ${days_left} days" ""
|
||||
fi
|
||||
}
|
||||
|
||||
check_remote_service_status() {
|
||||
local section="$1" host="$2" services="$3"
|
||||
if [[ -z "$services" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local service
|
||||
for service in $services; do
|
||||
if run_remote "$host" "systemctl is-active --quiet \"$service\""; then
|
||||
record "$section" OK "$service" "$host active" ""
|
||||
else
|
||||
local rc=$?
|
||||
if (( rc == 125 )); then
|
||||
record "$section" SKIP "$service" "SSH skipped" "Provide SSH access or run locally"
|
||||
else
|
||||
record "$section" WARN "$service" "$host inactive or unit missing" "Check unit status"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
check_node_load() {
|
||||
local section="$1" host="$2"
|
||||
if run_remote "$host" "awk '{print \$1 \" \" \$2 \" \" \$3}' /proc/loadavg"; then
|
||||
local load_line="$REMOTE_OUTPUT"
|
||||
if [[ -z "$load_line" ]]; then
|
||||
record "$section" SKIP "Node load" "Empty loadavg output" "Check /proc on remote"
|
||||
else
|
||||
local avg1 avg5 avg15
|
||||
avg1="${load_line%% *}"
|
||||
avg5="$(echo "$load_line" | awk '{print $2}')"
|
||||
avg15="$(echo "$load_line" | awk '{print $3}')"
|
||||
record "$section" OK "Node load" "1m=$avg1 5m=$avg5 15m=$avg15" ""
|
||||
fi
|
||||
else
|
||||
local rc=$?
|
||||
if (( rc == 125 )); then
|
||||
record "$section" SKIP "Node load" "SSH skipped" "Provide SSH access or run locally"
|
||||
else
|
||||
record "$section" WARN "Node load" "Cannot read /proc/loadavg" "Check remote shell access"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_disk() {
|
||||
local section="$1" host="$2" mount="$3"
|
||||
if run_remote "$host" "df -P '$mount' | awk 'NR==2 {print \$5}'"; then
|
||||
local used=0
|
||||
local raw_total
|
||||
raw_total="$(echo "$REMOTE_OUTPUT" | tr '\\t' ' ')"
|
||||
used="${raw_total%%\%*}"
|
||||
if ! [[ "$used" =~ ^[0-9]+$ ]]; then
|
||||
record "$section" SKIP "disk $mount" "Unable to parse df output ($raw_total)" "Check mount and fs output"
|
||||
return
|
||||
fi
|
||||
if (( used >= DETMIR_SUPPORT_DISK_CRIT_PERCENT )); then
|
||||
record "$section" FAIL "disk $mount" "Used ${used}% on ${host}:${mount}" "Free space now"
|
||||
elif (( used >= DETMIR_SUPPORT_DISK_WARN_PERCENT )); then
|
||||
record "$section" WARN "disk $mount" "Used ${used}% on ${host}:${mount}" "Plan cleanup/rebalance"
|
||||
else
|
||||
record "$section" OK "disk $mount" "Used ${used}% on ${host}:${mount}" ""
|
||||
fi
|
||||
else
|
||||
local rc=$?
|
||||
if (( rc == 125 )); then
|
||||
record "$section" SKIP "disk $mount" "SSH skipped" "Provide SSH access"
|
||||
else
|
||||
record "$section" FAIL "disk $mount" "df failed" "Check permissions and mount"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_backups() {
|
||||
local section="$1" host="$2" path="$3"
|
||||
local remote_cmd
|
||||
# shellcheck disable=SC2016
|
||||
remote_cmd=$(cat <<EOF
|
||||
if [ ! -d '$path' ]; then
|
||||
echo NO_PATH
|
||||
exit 0
|
||||
fi
|
||||
find '$path' -type f \( -name 'vzdump-*' -o -name '*.vma*' -o -name '*.tar*' -o -name '*.zip' -o -name '*.bak' \) -printf '%T@ %p\n' 2>/dev/null | sort -nr | head -1 | awk '{print \$1}'
|
||||
EOF
|
||||
)
|
||||
if run_remote "$host" "$remote_cmd"; then
|
||||
if [[ "$REMOTE_OUTPUT" == "NO_PATH" ]]; then
|
||||
record "$section" SKIP "backup" "Path not found: ${path}" "Fix backup path in env"
|
||||
return
|
||||
fi
|
||||
if [[ -z "$REMOTE_OUTPUT" ]]; then
|
||||
record "$section" WARN "backup" "No backup files found in ${path}" "Check backup schedule"
|
||||
return
|
||||
fi
|
||||
local epoch_now age_days
|
||||
epoch_now="$(date -u +%s)"
|
||||
age_days=$(( (epoch_now - ${REMOTE_OUTPUT%.*}) / 86400 ))
|
||||
if (( age_days > DETMIR_SUPPORT_BACKUP_MAX_AGE_DAYS )); then
|
||||
record "$section" WARN "backup" "Last backup ${age_days} day(s) ago" "Check scheduler and retention"
|
||||
else
|
||||
record "$section" OK "backup" "Last backup ${age_days} day(s) ago" ""
|
||||
fi
|
||||
else
|
||||
local rc=$?
|
||||
if (( rc == 125 )); then
|
||||
record "$section" SKIP "backup" "SSH skipped" "Provide SSH access"
|
||||
else
|
||||
record "$section" FAIL "backup" "Check failed" "Inspect permissions/path"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_vms() {
|
||||
local section="$1" host="$2" vm_ids="$3"
|
||||
if [[ -z "$vm_ids" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if run_remote "$host" "if command -v qm >/dev/null 2>&1; then qm list; else echo NO_QM; fi"; then
|
||||
if [[ "$REMOTE_OUTPUT" == "NO_QM" ]]; then
|
||||
record "$section" SKIP "VM/CT state" "qm command not found" "Run checks only on Proxmox host"
|
||||
return
|
||||
fi
|
||||
local vm_id state
|
||||
for vm_id in $vm_ids; do
|
||||
state="$(echo "$REMOTE_OUTPUT" | awk -v id="$vm_id" '$1==id {print $3}')"
|
||||
if [[ -z "$state" ]]; then
|
||||
record "$section" WARN "VM/CT $vm_id" "Not found in qm list" "Verify ID list"
|
||||
elif [[ "$state" == "running" ]]; then
|
||||
record "$section" OK "VM/CT $vm_id" "running" ""
|
||||
else
|
||||
record "$section" WARN "VM/CT $vm_id" "state=$state" "Inspect console before maintenance"
|
||||
fi
|
||||
done
|
||||
else
|
||||
local rc=$?
|
||||
if (( rc == 125 )); then
|
||||
record "$section" SKIP "VM/CT state" "SSH skipped" "Provide SSH access"
|
||||
else
|
||||
record "$section" FAIL "VM/CT state" "Cannot run qm list" "Check Proxmox shell"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_task_log() {
|
||||
local section="$1" host="$2"
|
||||
if run_remote "$host" "journalctl -p 3 --no-pager -n 50 2>/dev/null | grep -Eci '(error|fail|critical|timeout)'"; then
|
||||
local errors="${REMOTE_OUTPUT//[$'\n']/ }"
|
||||
if ! [[ "$errors" =~ ^[0-9]+$ ]]; then
|
||||
record "$section" SKIP "task log" "Cannot parse journal count" "Check journald"
|
||||
return
|
||||
fi
|
||||
if (( errors > 10 )); then
|
||||
record "$section" WARN "task log" "Found ${errors} critical/error lines" "Review pve/task.log scope"
|
||||
else
|
||||
record "$section" OK "task log" "Critical/error lines: ${errors}" ""
|
||||
fi
|
||||
else
|
||||
local rc=$?
|
||||
if (( rc == 125 )); then
|
||||
record "$section" SKIP "task log" "SSH skipped" "Provide SSH access"
|
||||
else
|
||||
record "$section" WARN "task log" "Cannot read task log" "Check journald config"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_log_growth() {
|
||||
local section="$1" host="$2"
|
||||
local dir total dir_count
|
||||
total=0
|
||||
|
||||
for dir in /var/log /var/log/pve /var/log/nginx; do
|
||||
if run_remote "$host" "[ -d '$dir' ] && find '$dir' -type f -size +100M 2>/dev/null | wc -l"; then
|
||||
dir_count="${REMOTE_OUTPUT//[$'\n']/ }"
|
||||
dir_count="${dir_count// /}"
|
||||
if ! [[ "$dir_count" =~ ^[0-9]+$ ]]; then
|
||||
dir_count=0
|
||||
fi
|
||||
total=$((total + dir_count))
|
||||
else
|
||||
local rc=$?
|
||||
if (( rc == 125 )); then
|
||||
record "$section" SKIP "log growth" "SSH skipped" "Provide SSH access"
|
||||
else
|
||||
record "$section" FAIL "log growth" "scan failed" "Check permissions"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
done
|
||||
|
||||
local large_files="${total}"
|
||||
if (( large_files >= DETMIR_SUPPORT_LOG_CRIT_LIMIT )); then
|
||||
record "$section" WARN "log growth" "${large_files} files >100M" "Run cleanup/logrotate"
|
||||
elif (( large_files >= DETMIR_SUPPORT_LOG_WARNING_LIMIT )); then
|
||||
record "$section" WARN "log growth" "${large_files} files >100M" "Monitor growth"
|
||||
else
|
||||
record "$section" OK "log growth" "${large_files} files >100M" ""
|
||||
fi
|
||||
}
|
||||
|
||||
check_pfsense() {
|
||||
local section="$1"
|
||||
if [[ -n "$DETMIR_SUPPORT_PFSENSE_URL" ]]; then
|
||||
check_http "$section" "$DETMIR_SUPPORT_PFSENSE_URL" '^2[0-9][0-9]$' "pfSense Web"
|
||||
fi
|
||||
if [[ -n "$DETMIR_SUPPORT_PFSENSE_HOST" ]]; then
|
||||
check_tcp "$section" "$DETMIR_SUPPORT_PFSENSE_HOST" 443 "pfSense HTTPS"
|
||||
fi
|
||||
}
|
||||
|
||||
check_openvpn() {
|
||||
local section="$1"
|
||||
if [[ -n "$DETMIR_SUPPORT_OPENVPN_WEB_URL" ]]; then
|
||||
check_http "$section" "$DETMIR_SUPPORT_OPENVPN_WEB_URL" '^2[0-9][0-9]$' "OpenVPN Web"
|
||||
fi
|
||||
if [[ -n "$DETMIR_SUPPORT_OPENVPN_HOST" ]]; then
|
||||
check_tcp "$section" "$DETMIR_SUPPORT_OPENVPN_HOST" 1194 "OpenVPN UDP/TCP"
|
||||
fi
|
||||
}
|
||||
|
||||
check_suricata_service() {
|
||||
local section="$1" host="$2"
|
||||
check_remote_service_status "$section" "$host" "$DETMIR_SUPPORT_SURICATA_SERVICES"
|
||||
|
||||
if run_remote "$host" "command -v suricata >/dev/null 2>&1 && pgrep -af suricata >/dev/null"; then
|
||||
record "$section" OK "Suricata process" "running"
|
||||
else
|
||||
local rc=$?
|
||||
if (( rc == 125 )); then
|
||||
record "$section" SKIP "Suricata process" "SSH skipped" "Provide SSH access"
|
||||
else
|
||||
record "$section" WARN "Suricata process" "No running process or suricata missing" "Verify IDS/IPS host"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
run_daily() {
|
||||
check_tcp "Доступность" "$DETMIR_SUPPORT_PVE_HOST" 22 "Proxmox SSH"
|
||||
check_tcp "Доступность" "$DETMIR_SUPPORT_PVE_HOST" 8006 "Proxmox WEB"
|
||||
check_tcp "Доступность" "$DETMIR_SUPPORT_AW_HOST" 5600 "AW API"
|
||||
check_tcp "Доступность" "$DETMIR_SUPPORT_WEB_HOST" 80 "Web HTTP"
|
||||
check_tcp "Доступность" "$DETMIR_SUPPORT_WEB_HOST" 443 "Web HTTPS"
|
||||
[[ -n "$DETMIR_SUPPORT_WINDOWS_HOST" ]] && check_tcp "Удаленный доступ" "$DETMIR_SUPPORT_WINDOWS_HOST" 3389 "Windows RDP"
|
||||
|
||||
check_http "Ключевые сервисы" "https://$DETMIR_SUPPORT_AW_HOST:5600" '^2[0-9][0-9]$' "AW API health"
|
||||
check_http "Web" "https://$DETMIR_SUPPORT_WEB_TLS_HOST/" '^2[0-9][0-9]$' "Public HTTPS"
|
||||
check_tls "TLS" "$DETMIR_SUPPORT_WEB_TLS_HOST" 443 "Web certificate"
|
||||
|
||||
check_pfsense "Сеть"
|
||||
check_openvpn "Сеть"
|
||||
|
||||
check_remote_service_status "Proxmox" "$DETMIR_SUPPORT_PVE_HOST" "$DETMIR_SUPPORT_PVE_SERVICES"
|
||||
check_remote_service_status "AW" "$DETMIR_SUPPORT_AW_HOST" "$DETMIR_SUPPORT_AW_SERVICES"
|
||||
check_remote_service_status "Web" "$DETMIR_SUPPORT_WEB_HOST" "$DETMIR_SUPPORT_WEB_SERVICES"
|
||||
|
||||
check_node_load "Виртуализация" "$DETMIR_SUPPORT_PVE_HOST"
|
||||
check_disk "Диски" "$DETMIR_SUPPORT_PVE_HOST" "/"
|
||||
check_disk "Диски" "$DETMIR_SUPPORT_AW_HOST" "/"
|
||||
check_disk "Диски" "$DETMIR_SUPPORT_WEB_HOST" "/"
|
||||
|
||||
check_backups "Резервные копии" "$DETMIR_SUPPORT_PVE_HOST" "$DETMIR_SUPPORT_PVE_BACKUP_DIRS"
|
||||
check_vms "Виртуализация" "$DETMIR_SUPPORT_PVE_HOST" "$DETMIR_SUPPORT_PVE_VM_IDS"
|
||||
check_task_log "Системные журналы" "$DETMIR_SUPPORT_PVE_HOST"
|
||||
check_suricata_service "Периметр" "$DETMIR_SUPPORT_SURICATA_HOST"
|
||||
}
|
||||
|
||||
run_weekly() {
|
||||
run_daily
|
||||
|
||||
check_log_growth "Логи" "$DETMIR_SUPPORT_PVE_HOST"
|
||||
check_log_growth "Логи" "$DETMIR_SUPPORT_AW_HOST"
|
||||
check_log_growth "Логи" "$DETMIR_SUPPORT_WEB_HOST"
|
||||
|
||||
if run_remote "$DETMIR_SUPPORT_AW_HOST" "journalctl -p 3 --no-pager -n 40"; then
|
||||
local lines
|
||||
lines="$(printf '%s\n' "$REMOTE_OUTPUT" | wc -l)"
|
||||
if (( lines > 20 )); then
|
||||
record "Логи" "journalctl critical" WARN "Found ${lines} critical lines in recent entries" "Review and file ticket if recurring"
|
||||
else
|
||||
record "Логи" "journalctl critical" OK "Critical entries in normal range" ""
|
||||
fi
|
||||
else
|
||||
local rc=$?
|
||||
if (( rc == 125 )); then
|
||||
record "Логи" "journalctl critical" SKIP "SSH skipped" "Provide SSH access"
|
||||
else
|
||||
record "Логи" "journalctl critical" WARN "Unable to read journal" "Inspect journald"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
run_monthly() {
|
||||
run_weekly
|
||||
|
||||
local update_cmd
|
||||
# shellcheck disable=SC2016
|
||||
update_cmd=$(
|
||||
cat <<'EOF'
|
||||
if command -v apt >/dev/null 2>&1; then
|
||||
apt list --upgradable 2>/dev/null | tail -n +2 | wc -l
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
yum check-update -q >/tmp/yumcheck 2>&1
|
||||
rc=$?
|
||||
if [ "$rc" -eq 100 ]; then
|
||||
wc -l < /tmp/yumcheck
|
||||
elif [ "$rc" -eq 0 ]; then
|
||||
echo 0
|
||||
else
|
||||
echo FAILED:$rc
|
||||
fi
|
||||
else
|
||||
echo 0
|
||||
fi
|
||||
EOF
|
||||
)
|
||||
|
||||
if run_remote "$DETMIR_SUPPORT_AW_HOST" "$update_cmd"; then
|
||||
if [[ "$REMOTE_OUTPUT" =~ ^[0-9]+$ ]]; then
|
||||
if (( REMOTE_OUTPUT > 15 )); then
|
||||
record "Обновления" "Update count" "${REMOTE_OUTPUT} updates available" "Согласуйте окно обслуживания"
|
||||
else
|
||||
record "Обновления" "Update count" "${REMOTE_OUTPUT} updates available" ""
|
||||
fi
|
||||
else
|
||||
record "Обновления" "Update check" "Unable parse update count" "Inspect package manager"
|
||||
fi
|
||||
else
|
||||
local rc=$?
|
||||
if (( rc == 125 )); then
|
||||
record "Обновления" "Update check" "SSH skipped" "Provide SSH access"
|
||||
else
|
||||
record "Обновления" "Update check" "Failed" "Inspect package manager/permissions"
|
||||
fi
|
||||
fi
|
||||
|
||||
local pve_version
|
||||
if run_remote "$DETMIR_SUPPORT_PVE_HOST" "pveversion -v | head -n 1"; then
|
||||
pve_version="$REMOTE_OUTPUT"
|
||||
record "Контур" "Proxmox version" "${pve_version}" ""
|
||||
else
|
||||
local rc=$?
|
||||
if (( rc == 125 )); then
|
||||
record "Контур" "Proxmox version" "SSH skipped" "Provide SSH access"
|
||||
else
|
||||
record "Контур" "Proxmox version" "Unable to read" "Check pve command"
|
||||
fi
|
||||
fi
|
||||
|
||||
record "Документация" OK "Access matrix" "Owner-visible matrix and contacts are required to be current" "Create/update support ownership map and include in evidence"
|
||||
record "DR" OK "Recovery drill" "Plan restore drill in maintenance window" "Schedule controlled restore drill on non-production clone"
|
||||
}
|
||||
|
||||
generate_report() {
|
||||
local exit_code="$1"
|
||||
local status_text="OK"
|
||||
if (( exit_code != 0 )); then
|
||||
status_text="ATTENTION"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "# Отчет по выполнению задач поддержки DetMir"
|
||||
echo
|
||||
echo "- **Дата:** $(date -u '+%Y-%m-%d %H:%M:%SZ')"
|
||||
echo "- **Режим:** $SCOPE"
|
||||
echo "- **Run-ID:** $RUN_ID"
|
||||
echo "- **Лог:** $LOG_FILE"
|
||||
echo "- **Итоговый статус:** $status_text"
|
||||
echo
|
||||
echo "## Итого"
|
||||
echo
|
||||
echo "- OK: $OK_COUNT"
|
||||
echo "- WARN: $WARN_COUNT"
|
||||
echo "- FAIL: $FAIL_COUNT"
|
||||
echo "- SKIP: $SKIP_COUNT"
|
||||
echo "- TOTAL: $TOTAL"
|
||||
echo
|
||||
echo "## Детали"
|
||||
echo
|
||||
echo "|Раздел|Проверка|Статус|Результат|Действие|"
|
||||
echo "|---|---|---|---|---|"
|
||||
tail -n +2 "$CSV_FILE" | while IFS=';' read -r _ _ section status check result action; do
|
||||
section="${section//|/\\|}"
|
||||
check="${check//|/\\|}"
|
||||
status="${status//|/\\|}"
|
||||
result="${result//|/\\|}"
|
||||
action="${action//|/\\|}"
|
||||
echo "|$section|$check|$status|$result|$action|"
|
||||
done
|
||||
echo
|
||||
} > "$MD_FILE"
|
||||
|
||||
cat <<EOF_SUMMARY
|
||||
|
||||
==> Отчет сохранен:
|
||||
- CSV: $CSV_FILE
|
||||
- Markdown: $MD_FILE
|
||||
- Log: $LOG_FILE
|
||||
EOF_SUMMARY
|
||||
}
|
||||
|
||||
main() {
|
||||
require_command bash || exit 1
|
||||
require_command awk || exit 1
|
||||
require_command date || exit 1
|
||||
require_command timeout || exit 1
|
||||
require_command curl || exit 1
|
||||
|
||||
log "Start DetMir support run, scope=$SCOPE"
|
||||
log "Run directory: $RUN_DIR"
|
||||
|
||||
case "$SCOPE" in
|
||||
daily) run_daily ;;
|
||||
weekly) run_weekly ;;
|
||||
monthly) run_monthly ;;
|
||||
esac
|
||||
|
||||
local exit_code=0
|
||||
if (( FAIL_COUNT > 0 )); then
|
||||
exit_code=2
|
||||
elif (( WARN_COUNT > 0 )); then
|
||||
exit_code=1
|
||||
fi
|
||||
|
||||
generate_report "$exit_code"
|
||||
log "Finish DetMir support run, status=$exit_code"
|
||||
return "$exit_code"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
"$SCRIPT_DIR/detmir-support-run.sh" --scope weekly "$@"
|
||||
@@ -0,0 +1,42 @@
|
||||
## Базовые узлы
|
||||
DETMIR_SUPPORT_PVE_HOST=10.10.10.2
|
||||
DETMIR_SUPPORT_AW_HOST=10.10.10.13
|
||||
DETMIR_SUPPORT_WEB_HOST=10.10.10.11
|
||||
DETMIR_SUPPORT_WINDOWS_HOST=192.168.100.18
|
||||
|
||||
## Сетевые компоненты
|
||||
DETMIR_SUPPORT_PFSENSE_HOST=10.0.0.1
|
||||
DETMIR_SUPPORT_PFSENSE_URL=https://10.0.0.1
|
||||
DETMIR_SUPPORT_OPENVPN_HOST=10.0.0.1
|
||||
DETMIR_SUPPORT_OPENVPN_WEB_URL=https://10.0.0.1:943
|
||||
DETMIR_SUPPORT_SURICATA_HOST=${DETMIR_SUPPORT_PVE_HOST}
|
||||
DETMIR_SUPPORT_WEB_TLS_HOST=${DETMIR_SUPPORT_WEB_HOST}
|
||||
|
||||
## SSH/доступ
|
||||
DETMIR_SUPPORT_SSH_USER=root
|
||||
#DETMIR_SUPPORT_SSH_IDENTITY=/path/to/key
|
||||
DETMIR_SUPPORT_SKIP_REMOTE=0
|
||||
DETMIR_SUPPORT_CONNECT_TIMEOUT=8
|
||||
|
||||
## Сервисы и параметры
|
||||
DETMIR_SUPPORT_PVE_SERVICES=pve-cluster pvedaemon pvestatd pveproxy
|
||||
DETMIR_SUPPORT_AW_SERVICES=activitywatch-server aw-server-rust
|
||||
DETMIR_SUPPORT_WEB_SERVICES=nginx apache2
|
||||
DETMIR_SUPPORT_SURICATA_SERVICES=suricata
|
||||
|
||||
## VM и резервные копии
|
||||
DETMIR_SUPPORT_PVE_VM_IDS="101 102 103"
|
||||
DETMIR_SUPPORT_PVE_BACKUP_DIRS=/var/lib/vz/dump
|
||||
DETMIR_SUPPORT_BACKUP_MAX_AGE_DAYS=14
|
||||
|
||||
## Пороги
|
||||
DETMIR_SUPPORT_DISK_WARN_PERCENT=85
|
||||
DETMIR_SUPPORT_DISK_CRIT_PERCENT=93
|
||||
DETMIR_SUPPORT_LOG_WARNING_LIMIT=5
|
||||
DETMIR_SUPPORT_LOG_CRIT_LIMIT=20
|
||||
DETMIR_SUPPORT_TLS_WARN_DAYS=30
|
||||
DETMIR_SUPPORT_TLS_CRIT_DAYS=14
|
||||
DETMIR_SUPPORT_HTTP_WARNING_LATENCY_MS=2000
|
||||
|
||||
## Вывод
|
||||
DETMIR_SUPPORT_OUTPUT_DIR=/var/log/detmir-support
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
@@ -175,27 +176,37 @@ function checkRequiredContent() {
|
||||
return findings;
|
||||
}
|
||||
|
||||
function checkSensitiveStrings(files) {
|
||||
const patterns = [
|
||||
{ name: "private_network_10", re: /10\.10\.10\./ },
|
||||
{ name: "private_network_192", re: /192\.168\./ },
|
||||
{ name: "private_network_172", re: /172\.(1[6-9]|2\d|3[0-1])\./ },
|
||||
{ name: "private_operator_domain", re: /dm\.iri/i },
|
||||
{ name: "customer_codename", re: new RegExp(`${["Det", "Mir"].join("")}|${["SHARKON", "2025"].join("")}`, "i") },
|
||||
{ name: "local_operator_path", re: /\/home\/igor/i },
|
||||
{ name: "mts_phishing_context", re: /\b(lk|l)\.mts\.ru/i },
|
||||
];
|
||||
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 });
|
||||
}
|
||||
}
|
||||
function checkDemoSafety() {
|
||||
const result = spawnSync("bash", ["scripts/check_demo_safety.sh", "--json"], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (result.error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: result.error.message,
|
||||
findings: [],
|
||||
};
|
||||
}
|
||||
return findings;
|
||||
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 {
|
||||
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 = {}) {
|
||||
@@ -213,12 +224,6 @@ function main() {
|
||||
...roadmapDocs,
|
||||
...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, {
|
||||
missing: checkFiles(validationDocs),
|
||||
});
|
||||
@@ -253,9 +258,13 @@ function main() {
|
||||
findings: linkFindings,
|
||||
});
|
||||
|
||||
const sensitiveFindings = checkSensitiveStrings(sensitiveScanFiles);
|
||||
pass(checks, "sensitive_string_scan", sensitiveFindings.length === 0, {
|
||||
findings: sensitiveFindings,
|
||||
const demoSafety = checkDemoSafety();
|
||||
pass(checks, "demo_safety_scan", demoSafety.ok, {
|
||||
findings: demoSafety.findings,
|
||||
scope_files: demoSafety.scope_files,
|
||||
status: demoSafety.status,
|
||||
stderr: demoSafety.stderr,
|
||||
error: demoSafety.error,
|
||||
});
|
||||
|
||||
const ok = checks.every((check) => check.ok);
|
||||
|
||||
@@ -21,6 +21,9 @@ rust_candidates=()
|
||||
if [[ -n "$RUST_BIN" ]]; then
|
||||
rust_candidates+=("$RUST_BIN")
|
||||
fi
|
||||
if [[ -n "${AW_RUS_CARGO_TARGET_DIR:-}" ]]; then
|
||||
rust_candidates+=("$AW_RUS_CARGO_TARGET_DIR/release/quality-gate")
|
||||
fi
|
||||
rust_candidates+=(
|
||||
"$TARGET_ROOT/release/quality-gate"
|
||||
"$ROOT_DIR/adk-rust/target/release/quality-gate"
|
||||
@@ -93,7 +96,7 @@ fi
|
||||
violations=()
|
||||
for path in "${tracked_py[@]}"; do
|
||||
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
|
||||
;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user