feat(detmir): focus security incidents portal card
This commit is contained in:
@@ -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
|
failed units stayed `0`. Local Playwright smoke against a temporary
|
||||||
portal instance verified `Инциденты ИБ`, `В работу`, persisted UI state,
|
portal instance verified `Инциденты ИБ`, `В работу`, persisted UI state,
|
||||||
disabled ack button, and zero JS errors.
|
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
|
&& first_number
|
||||||
.map(|value| value > cli.max_freshness_minutes)
|
.map(|value| value > cli.max_freshness_minutes)
|
||||||
.unwrap_or(true);
|
.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
|
Status::Fail
|
||||||
|
} else if empty_optional_panel {
|
||||||
|
Status::Warn
|
||||||
} else {
|
} else {
|
||||||
Status::Ok
|
Status::Ok
|
||||||
};
|
};
|
||||||
@@ -572,6 +578,11 @@ fn is_freshness_panel(title: &str) -> bool {
|
|||||||
title.to_lowercase().contains("свеж")
|
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 {
|
fn frame_row_count(value: &Value) -> usize {
|
||||||
value
|
value
|
||||||
.get("frames")
|
.get("frames")
|
||||||
@@ -708,4 +719,18 @@ mod tests {
|
|||||||
assert_eq!(targets[0].panel_id, 5);
|
assert_eq!(targets[0].panel_id, 5);
|
||||||
assert_eq!(targets[0].datasource_uid, "influxdb_aw");
|
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("Сегодня: активное время"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,6 +182,9 @@ struct PortalLinks {
|
|||||||
portal: String,
|
portal: String,
|
||||||
grafana_dashboards: String,
|
grafana_dashboards: String,
|
||||||
detmir_activitywatch: String,
|
detmir_activitywatch: String,
|
||||||
|
dlp_security_dashboard: String,
|
||||||
|
dlp_management_dashboard: String,
|
||||||
|
dlp_overview_dashboard: String,
|
||||||
aw_ui: String,
|
aw_ui: String,
|
||||||
worktime_report: String,
|
worktime_report: String,
|
||||||
file1c_brief: 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
|
incidents
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -923,6 +942,12 @@ fn links() -> PortalLinks {
|
|||||||
detmir_activitywatch:
|
detmir_activitywatch:
|
||||||
"/d/detmir-aw-main/detmir-activitywatch?orgId=1&from=now-48h&to=now&timezone=browser&var-host=SHARKON2025&refresh=5m"
|
"/d/detmir-aw-main/detmir-activitywatch?orgId=1&from=now-48h&to=now&timezone=browser&var-host=SHARKON2025&refresh=5m"
|
||||||
.to_string(),
|
.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(),
|
aw_ui: "/r/aw/".to_string(),
|
||||||
worktime_report: "/reports/worktime/management?format=html&host=SHARKON2025".to_string(),
|
worktime_report: "/reports/worktime/management?format=html&host=SHARKON2025".to_string(),
|
||||||
file1c_brief: "/r/file1c/brief".to_string(),
|
file1c_brief: "/r/file1c/brief".to_string(),
|
||||||
|
|||||||
@@ -73,6 +73,16 @@ function renderLinks(links) {
|
|||||||
</div>`;
|
</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) {
|
function renderSourceList(data) {
|
||||||
const sources = [
|
const sources = [
|
||||||
["DetMir", data.detmir_status],
|
["DetMir", data.detmir_status],
|
||||||
@@ -181,14 +191,36 @@ function renderIncidentsList(items) {
|
|||||||
`).join("")}</div>`;
|
`).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) {
|
function renderIncidents(data) {
|
||||||
|
const links = state.links || {};
|
||||||
return `
|
return `
|
||||||
<h2 class="section-title">Инциденты ИБ</h2>
|
<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() {
|
async function refresh() {
|
||||||
|
if (!state.links) state.links = await loadJson("/links");
|
||||||
const summary = await loadJson("/summary");
|
const summary = await loadJson("/summary");
|
||||||
renderSummary(summary);
|
renderSummary(summary);
|
||||||
const content = document.getElementById("content");
|
const content = document.getElementById("content");
|
||||||
|
|||||||
Reference in New Issue
Block a user