feat(detmir): focus security incidents portal card

This commit is contained in:
igor04091968
2026-06-03 00:09:20 +03:00
parent 17e8e7b56a
commit 189885d73c
4 changed files with 100 additions and 2 deletions
+16
View File
@@ -1603,6 +1603,22 @@ systemctl is-active tsj-guardian-bot tsj-guardian-watchdog gost-tg
failed units stayed `0`. Local Playwright smoke against a temporary
portal instance verified `Инциденты ИБ`, `В работу`, persisted UI state,
disabled ack button, and zero JS errors.
- `Инциденты ИБ` tightened to a DLP-focused card: the tab now filters the
card to DLP/incident/case items, adds a synthetic DLP-count incident when
`dlp_counts.warn/fail` is non-zero, and shows only DLP/Grafana dashboard
links (`detmir-dlp-security`, `detmir-dlp-management`,
`awatch-dlp-overview`, Grafana catalog). Worktime, 1C, AW UI, and other
non-incident operational links are intentionally not shown in this card.
Local Playwright smoke verified DLP incident rendering, dashboard links,
no Worktime/1C text in the card, and zero JS errors.
- during this deploy, `detmir-grafana-check` was corrected so empty
detail-only panels for employees/applications are WARN, not FAIL. The
mandatory freshness/summary panels still fail the check when stale or
empty. This removed a false red state shortly after midnight when
`today` detail rows were legitimately empty. Production verification:
Grafana check `ok=true` with `fail=0`, `detmir-auto --no-heal` rc `0`,
portal health `true`, `detmir-status` `OK / ok_for_operator=true`, and
failed units `0`.
Отложить:
@@ -232,8 +232,14 @@ fn build_report(cli: &Cli) -> Result<Report> {
&& first_number
.map(|value| value > cli.max_freshness_minutes)
.unwrap_or(true);
let status = if rows < cli.min_panel_rows || freshness_failed {
let empty_optional_panel =
rows < cli.min_panel_rows && is_optional_empty_panel(&target.panel_title);
let status = if freshness_failed
|| (rows < cli.min_panel_rows && !empty_optional_panel)
{
Status::Fail
} else if empty_optional_panel {
Status::Warn
} else {
Status::Ok
};
@@ -572,6 +578,11 @@ fn is_freshness_panel(title: &str) -> bool {
title.to_lowercase().contains("свеж")
}
fn is_optional_empty_panel(title: &str) -> bool {
let title = title.to_lowercase();
title.contains("прилож") || title.contains("сотрудник")
}
fn frame_row_count(value: &Value) -> usize {
value
.get("frames")
@@ -708,4 +719,18 @@ mod tests {
assert_eq!(targets[0].panel_id, 5);
assert_eq!(targets[0].datasource_uid, "influxdb_aw");
}
#[test]
fn application_panels_are_optional_when_empty() {
assert!(is_optional_empty_panel(
"Сегодня: доказанная работа по приложениям"
));
assert!(is_optional_empty_panel(
"Сегодня: приложения и подтверждения"
));
assert!(is_optional_empty_panel(
"Сегодня: активность по сотрудникам"
));
assert!(!is_optional_empty_panel("Сегодня: активное время"));
}
}
+25
View File
@@ -182,6 +182,9 @@ struct PortalLinks {
portal: String,
grafana_dashboards: String,
detmir_activitywatch: String,
dlp_security_dashboard: String,
dlp_management_dashboard: String,
dlp_overview_dashboard: String,
aw_ui: String,
worktime_report: String,
file1c_brief: String,
@@ -689,6 +692,22 @@ fn build_incidents(snapshot: &Snapshot, state: &IncidentStateFile) -> Vec<Incide
}
}
}
if let Some(status) = snapshot.detmir_status.payload.as_ref() {
let counts = status.get("dlp_counts").unwrap_or(&Value::Null);
let warn = counts.get("warn").and_then(Value::as_u64).unwrap_or(0);
let fail = counts.get("fail").and_then(Value::as_u64).unwrap_or(0);
if warn > 0 || fail > 0 {
incidents.push(incident_item(
if fail > 0 { "FAIL" } else { "WARN" },
"dlp",
"dlp_counts",
&format!("DLP requires review: warn={warn}, fail={fail}"),
&snapshot.generated_at_utc,
"/portal/incidents",
state,
));
}
}
incidents
}
@@ -923,6 +942,12 @@ fn links() -> PortalLinks {
detmir_activitywatch:
"/d/detmir-aw-main/detmir-activitywatch?orgId=1&from=now-48h&to=now&timezone=browser&var-host=SHARKON2025&refresh=5m"
.to_string(),
dlp_security_dashboard:
"/d/detmir-dlp-security?orgId=1&from=now-30d&to=now&timezone=browser".to_string(),
dlp_management_dashboard:
"/d/detmir-dlp-management?orgId=1&from=now-30d&to=now&timezone=browser".to_string(),
dlp_overview_dashboard:
"/d/awatch-dlp-overview?orgId=1&from=now-30d&to=now&timezone=browser".to_string(),
aw_ui: "/r/aw/".to_string(),
worktime_report: "/reports/worktime/management?format=html&host=SHARKON2025".to_string(),
file1c_brief: "/r/file1c/brief".to_string(),
@@ -73,6 +73,16 @@ function renderLinks(links) {
</div>`;
}
function renderDlpLinks(links) {
const link = (text, href) => `<a class="button" href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(text)}</a>`;
return `<div class="links">
${link("ИБ дашборд", links.dlp_security_dashboard)}
${link("ИБ для руководства", links.dlp_management_dashboard)}
${link("DLP обзор", links.dlp_overview_dashboard)}
${link("Все Grafana dashboards", links.grafana_dashboards)}
</div>`;
}
function renderSourceList(data) {
const sources = [
["DetMir", data.detmir_status],
@@ -181,14 +191,36 @@ function renderIncidentsList(items) {
`).join("")}</div>`;
}
function isDlpIncident(item) {
const text = `${item?.kind || ""} ${item?.source || ""} ${item?.summary || ""}`.toLowerCase();
return text.includes("dlp") || text.includes("incident") || text.includes("case") || text.includes("иб");
}
function renderDlpIncidentsList(items) {
const dlpItems = (items || []).filter(isDlpIncident);
if (dlpItems.length === 0) return `<p class="muted">Активных DLP/ИБ-инцидентов нет.</p>`;
return renderIncidentsList(dlpItems);
}
function renderIncidents(data) {
const links = state.links || {};
return `
<h2 class="section-title">Инциденты ИБ</h2>
${renderIncidentsList(data)}
<div class="grid-2">
<section class="card">
<h3>DLP-инциденты</h3>
${renderDlpIncidentsList(data)}
</section>
<section class="card">
<h3>Графики и дашборды</h3>
${renderDlpLinks(links)}
</section>
</div>
`;
}
async function refresh() {
if (!state.links) state.links = await loadJson("/links");
const summary = await loadJson("/summary");
renderSummary(summary);
const content = document.getElementById("content");