feat(portal): prioritize risk narrative
This commit is contained in:
@@ -517,11 +517,13 @@ struct ExecutiveCandidateSummary {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
struct ExecutiveDashboardSummary {
|
struct ExecutiveDashboardSummary {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
risk_narrative_status: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
main_risk_cause: Option<String>,
|
||||||
main_risk: String,
|
main_risk: String,
|
||||||
main_improvement: String,
|
main_improvement: String,
|
||||||
main_data_gap: String,
|
main_data_gap: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
main_risk_cause: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
@@ -2378,7 +2380,24 @@ fn build_reports(
|
|||||||
} else {
|
} else {
|
||||||
"Контур DetMir требует технической проверки"
|
"Контур DetMir требует технической проверки"
|
||||||
};
|
};
|
||||||
let mut executive_points = vec![
|
let mut executive_points = Vec::new();
|
||||||
|
executive_points.push(format!(
|
||||||
|
"Главный управленческий вывод: {}",
|
||||||
|
executive_dashboard
|
||||||
|
.summary
|
||||||
|
.main_risk_cause
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("связанный риск не выражен")
|
||||||
|
));
|
||||||
|
executive_points.push(format!(
|
||||||
|
"Статус связанной картины риска: {}",
|
||||||
|
executive_dashboard
|
||||||
|
.summary
|
||||||
|
.risk_narrative_status
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("NORMAL")
|
||||||
|
));
|
||||||
|
executive_points.extend([
|
||||||
format!("Сбор данных: {}. {}", collection.status, collection.text),
|
format!("Сбор данных: {}. {}", collection.status, collection.text),
|
||||||
format!(
|
format!(
|
||||||
"Достоверность данных агента: {}. {}",
|
"Достоверность данных агента: {}. {}",
|
||||||
@@ -2401,7 +2420,7 @@ fn build_reports(
|
|||||||
"Открытые вопросы: {}, в работе: {}",
|
"Открытые вопросы: {}, в работе: {}",
|
||||||
metrics.open_incidents, metrics.acknowledged_incidents
|
metrics.open_incidents, metrics.acknowledged_incidents
|
||||||
),
|
),
|
||||||
];
|
]);
|
||||||
if agent_quality_history_summary.ok_days < 5 {
|
if agent_quality_history_summary.ok_days < 5 {
|
||||||
executive_points.push("KPI требует валидации: нестабильный сбор данных агента".to_string());
|
executive_points.push("KPI требует валидации: нестабильный сбор данных агента".to_string());
|
||||||
}
|
}
|
||||||
@@ -2421,46 +2440,11 @@ fn build_reports(
|
|||||||
),
|
),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
for item in business_risk.iter().take(3) {
|
|
||||||
if item.risk_level != "LOW" {
|
|
||||||
let reason = item
|
|
||||||
.reasons
|
|
||||||
.first()
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| "требуется управленческая проверка".to_string());
|
|
||||||
executive_points.push(format!(
|
|
||||||
"Подразделение {}: {} — причина: {}",
|
|
||||||
item.department, item.risk_level, reason
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for department in stable_high_risk_departments(&business_risk_history, 3) {
|
for department in stable_high_risk_departments(&business_risk_history, 3) {
|
||||||
executive_points.push(format!(
|
executive_points.push(format!(
|
||||||
"Подразделение {department} сохраняет высокий риск несколько дней подряд."
|
"Подразделение {department} сохраняет высокий риск несколько дней подряд."
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
for candidate in risk_incident_candidates.iter().take(3) {
|
|
||||||
executive_points.push(format!(
|
|
||||||
"Кандидат в инцидент {}: {} — {}",
|
|
||||||
candidate.id,
|
|
||||||
candidate.risk_level.as_deref().unwrap_or("UNKNOWN"),
|
|
||||||
candidate.reason.as_deref().unwrap_or("требуется проверка")
|
|
||||||
));
|
|
||||||
}
|
|
||||||
for item in risk_heatmap
|
|
||||||
.iter()
|
|
||||||
.filter(|item| !matches!(item.heat_level.as_str(), "LOW" | "UNKNOWN"))
|
|
||||||
.take(3)
|
|
||||||
{
|
|
||||||
executive_points.push(format!(
|
|
||||||
"Связанная картина риска: {}",
|
|
||||||
linked_risk_statement(item)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
executive_points.push(format!(
|
|
||||||
"Главный риск: {}",
|
|
||||||
executive_dashboard.summary.main_risk
|
|
||||||
));
|
|
||||||
executive_points.push(format!(
|
executive_points.push(format!(
|
||||||
"Главное улучшение: {}",
|
"Главное улучшение: {}",
|
||||||
executive_dashboard.summary.main_improvement
|
executive_dashboard.summary.main_improvement
|
||||||
@@ -3060,6 +3044,7 @@ fn build_risk_heatmap(
|
|||||||
critical_candidates,
|
critical_candidates,
|
||||||
);
|
);
|
||||||
let links = risk_narrative_links(
|
let links = risk_narrative_links(
|
||||||
|
&heat_level,
|
||||||
trust_kpi_score,
|
trust_kpi_score,
|
||||||
activity_score,
|
activity_score,
|
||||||
agent_coverage_pct,
|
agent_coverage_pct,
|
||||||
@@ -3068,6 +3053,7 @@ fn build_risk_heatmap(
|
|||||||
critical_candidates,
|
critical_candidates,
|
||||||
);
|
);
|
||||||
let summary = risk_heatmap_summary(
|
let summary = risk_heatmap_summary(
|
||||||
|
&heat_level,
|
||||||
trust_kpi_score,
|
trust_kpi_score,
|
||||||
activity_score,
|
activity_score,
|
||||||
agent_coverage_pct,
|
agent_coverage_pct,
|
||||||
@@ -3256,6 +3242,7 @@ fn heatmap_rank(level: &str) -> u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn risk_narrative_links(
|
fn risk_narrative_links(
|
||||||
|
heat_level: &str,
|
||||||
trust_kpi_score: Option<u8>,
|
trust_kpi_score: Option<u8>,
|
||||||
activity_score: Option<u8>,
|
activity_score: Option<u8>,
|
||||||
agent_coverage_pct: Option<u8>,
|
agent_coverage_pct: Option<u8>,
|
||||||
@@ -3278,6 +3265,16 @@ fn risk_narrative_links(
|
|||||||
label: "Business Risk".to_string(),
|
label: "Business Risk".to_string(),
|
||||||
summary: format!("уровень {}", business_risk_level.unwrap_or("UNKNOWN")),
|
summary: format!("уровень {}", business_risk_level.unwrap_or("UNKNOWN")),
|
||||||
},
|
},
|
||||||
|
RiskNarrativeLink {
|
||||||
|
target: "risk_heatmap".to_string(),
|
||||||
|
label: "Карта риска".to_string(),
|
||||||
|
summary: format!("итоговый статус карты: {heat_level}"),
|
||||||
|
},
|
||||||
|
RiskNarrativeLink {
|
||||||
|
target: "security_correlation".to_string(),
|
||||||
|
label: "Корреляция".to_string(),
|
||||||
|
summary: "связь Workforce и Security по подразделению".to_string(),
|
||||||
|
},
|
||||||
RiskNarrativeLink {
|
RiskNarrativeLink {
|
||||||
target: "incident_candidates".to_string(),
|
target: "incident_candidates".to_string(),
|
||||||
label: "Кандидаты".to_string(),
|
label: "Кандидаты".to_string(),
|
||||||
@@ -3297,6 +3294,7 @@ fn risk_narrative_links(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn risk_heatmap_summary(
|
fn risk_heatmap_summary(
|
||||||
|
heat_level: &str,
|
||||||
trust_kpi_score: Option<u8>,
|
trust_kpi_score: Option<u8>,
|
||||||
activity_score: Option<u8>,
|
activity_score: Option<u8>,
|
||||||
agent_coverage_pct: Option<u8>,
|
agent_coverage_pct: Option<u8>,
|
||||||
@@ -3305,10 +3303,11 @@ fn risk_heatmap_summary(
|
|||||||
critical_candidates: usize,
|
critical_candidates: usize,
|
||||||
) -> String {
|
) -> String {
|
||||||
format!(
|
format!(
|
||||||
"Trust {} → Coverage {} → Business Risk {} → Candidates {} → Cases {} → {}",
|
"Trust {} → Coverage {} → Business Risk {} → Risk Heatmap {} → Security Correlation → Candidates {} → Cases {} → {}",
|
||||||
optional_score_text(trust_kpi_score),
|
optional_score_text(trust_kpi_score),
|
||||||
optional_score_text(agent_coverage_pct),
|
optional_score_text(agent_coverage_pct),
|
||||||
business_risk_level.unwrap_or("UNKNOWN"),
|
business_risk_level.unwrap_or("UNKNOWN"),
|
||||||
|
heat_level,
|
||||||
critical_candidates,
|
critical_candidates,
|
||||||
open_cases,
|
open_cases,
|
||||||
risk_narrative_conclusion(
|
risk_narrative_conclusion(
|
||||||
@@ -3359,21 +3358,6 @@ fn risk_narrative_conclusion(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn linked_risk_statement(item: &RiskHeatmapItem) -> String {
|
|
||||||
format!(
|
|
||||||
"В подразделении {} {}.",
|
|
||||||
item.department,
|
|
||||||
risk_narrative_conclusion(
|
|
||||||
item.trust_kpi_score,
|
|
||||||
item.activity_score,
|
|
||||||
item.agent_coverage_pct,
|
|
||||||
item.business_risk_level.as_deref(),
|
|
||||||
item.open_cases.unwrap_or(0),
|
|
||||||
item.critical_candidates.unwrap_or(0),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_security_correlation(heatmap: &[RiskHeatmapItem]) -> Vec<SecurityCorrelationItem> {
|
fn build_security_correlation(heatmap: &[RiskHeatmapItem]) -> Vec<SecurityCorrelationItem> {
|
||||||
let mut items = heatmap
|
let mut items = heatmap
|
||||||
.iter()
|
.iter()
|
||||||
@@ -3526,6 +3510,16 @@ fn build_executive_dashboard(
|
|||||||
let resolved_cases_30d = resolved_cases_30d(inputs.cases);
|
let resolved_cases_30d = resolved_cases_30d(inputs.cases);
|
||||||
let forensics_readiness = forensics_readiness(snapshot, inputs.candidates, inputs.evidence);
|
let forensics_readiness = forensics_readiness(snapshot, inputs.candidates, inputs.evidence);
|
||||||
let summary = ExecutiveDashboardSummary {
|
let summary = ExecutiveDashboardSummary {
|
||||||
|
risk_narrative_status: Some(risk_narrative_status(
|
||||||
|
inputs.risk_heatmap,
|
||||||
|
inputs.security_correlation,
|
||||||
|
&forensics_readiness,
|
||||||
|
)),
|
||||||
|
main_risk_cause: executive_main_risk_cause(
|
||||||
|
inputs.risk_heatmap,
|
||||||
|
inputs.security_correlation,
|
||||||
|
&forensics_readiness,
|
||||||
|
),
|
||||||
main_risk: executive_main_risk(
|
main_risk: executive_main_risk(
|
||||||
&high_risk_departments,
|
&high_risk_departments,
|
||||||
&critical_candidates,
|
&critical_candidates,
|
||||||
@@ -3537,11 +3531,6 @@ fn build_executive_dashboard(
|
|||||||
&critical_candidates,
|
&critical_candidates,
|
||||||
),
|
),
|
||||||
main_data_gap: executive_main_data_gap(snapshot, inputs.agent_quality_explain),
|
main_data_gap: executive_main_data_gap(snapshot, inputs.agent_quality_explain),
|
||||||
main_risk_cause: executive_main_risk_cause(
|
|
||||||
inputs.risk_heatmap,
|
|
||||||
inputs.security_correlation,
|
|
||||||
&forensics_readiness,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
ExecutiveDashboard {
|
ExecutiveDashboard {
|
||||||
trust_kpi_score,
|
trust_kpi_score,
|
||||||
@@ -3673,6 +3662,35 @@ fn executive_main_data_gap(
|
|||||||
"критичных пробелов в данных не выявлено".to_string()
|
"критичных пробелов в данных не выявлено".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn risk_narrative_status(
|
||||||
|
risk_heatmap: &[RiskHeatmapItem],
|
||||||
|
security_correlation: &[SecurityCorrelationItem],
|
||||||
|
forensics_readiness: &str,
|
||||||
|
) -> String {
|
||||||
|
let worst_heat_rank = risk_heatmap
|
||||||
|
.iter()
|
||||||
|
.map(|item| heatmap_rank(item.heat_level.as_str()))
|
||||||
|
.max()
|
||||||
|
.unwrap_or_else(|| heatmap_rank("UNKNOWN"));
|
||||||
|
let max_correlation = security_correlation
|
||||||
|
.iter()
|
||||||
|
.map(|item| item.correlation_score)
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0);
|
||||||
|
if worst_heat_rank >= heatmap_rank("CRITICAL") || max_correlation >= 85 {
|
||||||
|
"CRITICAL".to_string()
|
||||||
|
} else if worst_heat_rank >= heatmap_rank("HIGH") || max_correlation >= 60 {
|
||||||
|
"HIGH_RISK".to_string()
|
||||||
|
} else if worst_heat_rank >= heatmap_rank("MEDIUM")
|
||||||
|
|| max_correlation > 0
|
||||||
|
|| !matches!(forensics_readiness, "READY" | "OBSERVE")
|
||||||
|
{
|
||||||
|
"ATTENTION".to_string()
|
||||||
|
} else {
|
||||||
|
"NORMAL".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn executive_main_risk_cause(
|
fn executive_main_risk_cause(
|
||||||
risk_heatmap: &[RiskHeatmapItem],
|
risk_heatmap: &[RiskHeatmapItem],
|
||||||
security_correlation: &[SecurityCorrelationItem],
|
security_correlation: &[SecurityCorrelationItem],
|
||||||
@@ -3684,13 +3702,7 @@ fn executive_main_risk_cause(
|
|||||||
let correlation = security_correlation
|
let correlation = security_correlation
|
||||||
.iter()
|
.iter()
|
||||||
.find(|item| item.department == top.department);
|
.find(|item| item.department == top.department);
|
||||||
let mut statement = linked_risk_statement(top);
|
let mut statement = risk_narrative_statement(top, correlation);
|
||||||
if let Some(item) = correlation.filter(|item| item.correlation_score > 0) {
|
|
||||||
statement.push_str(&format!(
|
|
||||||
" Корреляция Workforce/Security: {}/100, {}.",
|
|
||||||
item.correlation_score, item.correlation_reason
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if !matches!(forensics_readiness, "READY") {
|
if !matches!(forensics_readiness, "READY") {
|
||||||
statement.push_str(&format!(
|
statement.push_str(&format!(
|
||||||
" Готовность Forensics: {forensics_readiness}, доказательная база требует проверки."
|
" Готовность Forensics: {forensics_readiness}, доказательная база требует проверки."
|
||||||
@@ -3699,6 +3711,38 @@ fn executive_main_risk_cause(
|
|||||||
Some(statement)
|
Some(statement)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn risk_narrative_statement(
|
||||||
|
item: &RiskHeatmapItem,
|
||||||
|
correlation: Option<&SecurityCorrelationItem>,
|
||||||
|
) -> String {
|
||||||
|
let correlation_score = correlation
|
||||||
|
.map(|value| format!("{}/100", value.correlation_score))
|
||||||
|
.unwrap_or_else(|| "0/100".to_string());
|
||||||
|
let correlation_reason = correlation
|
||||||
|
.map(|value| format!(" Причина корреляции: {}.", value.correlation_reason))
|
||||||
|
.unwrap_or_default();
|
||||||
|
format!(
|
||||||
|
"В подразделении {} связаны слои: Trust KPI {}, Agent Coverage {}, Business Risk {}, Risk Heatmap {}, Security Correlation {}, Incident Candidates {}, Cases {}. {}.{}",
|
||||||
|
item.department,
|
||||||
|
optional_score_text(item.trust_kpi_score),
|
||||||
|
optional_score_text(item.agent_coverage_pct),
|
||||||
|
item.business_risk_level.as_deref().unwrap_or("UNKNOWN"),
|
||||||
|
item.heat_level,
|
||||||
|
correlation_score,
|
||||||
|
item.critical_candidates.unwrap_or(0),
|
||||||
|
item.open_cases.unwrap_or(0),
|
||||||
|
risk_narrative_conclusion(
|
||||||
|
item.trust_kpi_score,
|
||||||
|
item.activity_score,
|
||||||
|
item.agent_coverage_pct,
|
||||||
|
item.business_risk_level.as_deref(),
|
||||||
|
item.open_cases.unwrap_or(0),
|
||||||
|
item.critical_candidates.unwrap_or(0),
|
||||||
|
),
|
||||||
|
correlation_reason
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn build_investigation_pack(
|
fn build_investigation_pack(
|
||||||
snapshot: &Snapshot,
|
snapshot: &Snapshot,
|
||||||
candidate_id: &str,
|
candidate_id: &str,
|
||||||
@@ -5652,6 +5696,11 @@ fn render_report_markdown(
|
|||||||
text.push_str(&format!("- {item}\n"));
|
text.push_str(&format!("- {item}\n"));
|
||||||
}
|
}
|
||||||
append_executive_dashboard_markdown(&mut text, context.executive_dashboard);
|
append_executive_dashboard_markdown(&mut text, context.executive_dashboard);
|
||||||
|
append_linked_risk_narrative_markdown(
|
||||||
|
&mut text,
|
||||||
|
context.risk_heatmap,
|
||||||
|
context.security_correlation,
|
||||||
|
);
|
||||||
append_agent_quality_markdown(&mut text, &snapshot.agent_quality);
|
append_agent_quality_markdown(&mut text, &snapshot.agent_quality);
|
||||||
append_agent_quality_history_markdown(
|
append_agent_quality_history_markdown(
|
||||||
&mut text,
|
&mut text,
|
||||||
@@ -5672,11 +5721,6 @@ fn render_report_markdown(
|
|||||||
);
|
);
|
||||||
append_risk_heatmap_markdown(&mut text, context.risk_heatmap);
|
append_risk_heatmap_markdown(&mut text, context.risk_heatmap);
|
||||||
append_security_correlation_markdown(&mut text, context.security_correlation);
|
append_security_correlation_markdown(&mut text, context.security_correlation);
|
||||||
append_linked_risk_narrative_markdown(
|
|
||||||
&mut text,
|
|
||||||
context.risk_heatmap,
|
|
||||||
context.security_correlation,
|
|
||||||
);
|
|
||||||
append_risk_incident_candidates_markdown(&mut text, context.risk_incident_candidates);
|
append_risk_incident_candidates_markdown(&mut text, context.risk_incident_candidates);
|
||||||
append_incident_review_markdown(&mut text, context.risk_incident_candidates);
|
append_incident_review_markdown(&mut text, context.risk_incident_candidates);
|
||||||
append_incident_review_audit_markdown(
|
append_incident_review_audit_markdown(
|
||||||
@@ -5692,6 +5736,23 @@ fn render_report_markdown(
|
|||||||
|
|
||||||
fn append_executive_dashboard_markdown(text: &mut String, dashboard: &ExecutiveDashboard) {
|
fn append_executive_dashboard_markdown(text: &mut String, dashboard: &ExecutiveDashboard) {
|
||||||
text.push_str("\n## Сводка руководителя\n\n");
|
text.push_str("\n## Сводка руководителя\n\n");
|
||||||
|
text.push_str(&format!(
|
||||||
|
"- Статус связанной картины риска: {}\n",
|
||||||
|
dashboard
|
||||||
|
.summary
|
||||||
|
.risk_narrative_status
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("NORMAL")
|
||||||
|
));
|
||||||
|
text.push_str(&format!(
|
||||||
|
"- Главная причина риска: {}\n",
|
||||||
|
dashboard
|
||||||
|
.summary
|
||||||
|
.main_risk_cause
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("связанный риск не выражен")
|
||||||
|
));
|
||||||
|
text.push_str("- Подтверждающие слои: Trust KPI, Agent Coverage, Business Risk, Risk Heatmap, Security Correlation, Incident Candidates, Cases\n");
|
||||||
text.push_str(&format!(
|
text.push_str(&format!(
|
||||||
"- Trust KPI: {}\n",
|
"- Trust KPI: {}\n",
|
||||||
dashboard
|
dashboard
|
||||||
@@ -5737,14 +5798,6 @@ fn append_executive_dashboard_markdown(text: &mut String, dashboard: &ExecutiveD
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.unwrap_or("UNKNOWN")
|
.unwrap_or("UNKNOWN")
|
||||||
));
|
));
|
||||||
text.push_str(&format!(
|
|
||||||
"- Главная причина риска: {}\n",
|
|
||||||
dashboard
|
|
||||||
.summary
|
|
||||||
.main_risk_cause
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or("связанный риск не выражен")
|
|
||||||
));
|
|
||||||
text.push_str(&format!(
|
text.push_str(&format!(
|
||||||
"- Главный риск: {}\n",
|
"- Главный риск: {}\n",
|
||||||
dashboard.summary.main_risk
|
dashboard.summary.main_risk
|
||||||
@@ -6043,14 +6096,15 @@ fn append_linked_risk_narrative_markdown(
|
|||||||
.map(|value| format!("{}/100", value.correlation_score))
|
.map(|value| format!("{}/100", value.correlation_score))
|
||||||
.unwrap_or_else(|| "0/100".to_string());
|
.unwrap_or_else(|| "0/100".to_string());
|
||||||
text.push_str(&format!(
|
text.push_str(&format!(
|
||||||
"- {} → Trust {} → Coverage {} → Business Risk {} → Candidates {} → Cases {} → Correlation {} → {}\n",
|
"- {} → Trust KPI {} → Agent Coverage {} → Business Risk {} → Risk Heatmap {} → Security Correlation {} → Incident Candidates {} → Cases {} → {}\n",
|
||||||
item.department,
|
item.department,
|
||||||
optional_score_text(item.trust_kpi_score),
|
optional_score_text(item.trust_kpi_score),
|
||||||
optional_score_text(item.agent_coverage_pct),
|
optional_score_text(item.agent_coverage_pct),
|
||||||
item.business_risk_level.as_deref().unwrap_or("UNKNOWN"),
|
item.business_risk_level.as_deref().unwrap_or("UNKNOWN"),
|
||||||
|
item.heat_level,
|
||||||
|
correlation_text,
|
||||||
item.critical_candidates.unwrap_or(0),
|
item.critical_candidates.unwrap_or(0),
|
||||||
item.open_cases.unwrap_or(0),
|
item.open_cases.unwrap_or(0),
|
||||||
correlation_text,
|
|
||||||
item.summary.as_deref().unwrap_or("вывод не сформирован")
|
item.summary.as_deref().unwrap_or("вывод не сформирован")
|
||||||
));
|
));
|
||||||
if let Some(value) = correlation.and_then(|value| value.explanation.as_deref()) {
|
if let Some(value) = correlation.and_then(|value| value.explanation.as_deref()) {
|
||||||
@@ -9755,6 +9809,27 @@ mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.contains("В подразделении")
|
.contains("В подразделении")
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
report["executive_dashboard"]["summary"]["risk_narrative_status"],
|
||||||
|
"HIGH_RISK"
|
||||||
|
);
|
||||||
|
let main_risk_cause = report["executive_dashboard"]["summary"]["main_risk_cause"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap();
|
||||||
|
for layer in [
|
||||||
|
"Trust KPI",
|
||||||
|
"Agent Coverage",
|
||||||
|
"Risk Heatmap",
|
||||||
|
"Business Risk",
|
||||||
|
"Security Correlation",
|
||||||
|
"Incident Candidates",
|
||||||
|
"Cases",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
main_risk_cause.contains(layer),
|
||||||
|
"main_risk_cause must mention {layer}"
|
||||||
|
);
|
||||||
|
}
|
||||||
assert_eq!(report["business_risk"].as_array().unwrap().len(), 1);
|
assert_eq!(report["business_risk"].as_array().unwrap().len(), 1);
|
||||||
assert_eq!(report["business_risk"][0]["department"], "Бухгалтерия");
|
assert_eq!(report["business_risk"][0]["department"], "Бухгалтерия");
|
||||||
assert_eq!(report["business_risk"][0]["risk_level"], "MEDIUM");
|
assert_eq!(report["business_risk"][0]["risk_level"], "MEDIUM");
|
||||||
@@ -9787,11 +9862,31 @@ mod tests {
|
|||||||
assert_eq!(report["risk_heatmap"][0]["trust_kpi_score"], 50);
|
assert_eq!(report["risk_heatmap"][0]["trust_kpi_score"], 50);
|
||||||
assert_eq!(report["risk_heatmap"][0]["activity_score"], 50);
|
assert_eq!(report["risk_heatmap"][0]["activity_score"], 50);
|
||||||
assert!(report["risk_heatmap"][0]["links"].is_array());
|
assert!(report["risk_heatmap"][0]["links"].is_array());
|
||||||
|
let heatmap_link_targets = report["risk_heatmap"][0]["links"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|item| item["target"].as_str())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for target in [
|
||||||
|
"risk_heatmap",
|
||||||
|
"trust_kpi",
|
||||||
|
"agent_coverage",
|
||||||
|
"business_risk",
|
||||||
|
"security_correlation",
|
||||||
|
"incident_candidates",
|
||||||
|
"cases",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
heatmap_link_targets.contains(&target),
|
||||||
|
"risk heatmap links must include {target}"
|
||||||
|
);
|
||||||
|
}
|
||||||
assert!(
|
assert!(
|
||||||
report["risk_heatmap"][0]["summary"]
|
report["risk_heatmap"][0]["summary"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.contains("Trust")
|
.contains("Security Correlation")
|
||||||
);
|
);
|
||||||
assert!(report["security_correlation"].is_array());
|
assert!(report["security_correlation"].is_array());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -9967,21 +10062,21 @@ mod tests {
|
|||||||
.as_array()
|
.as_array()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|item| item.as_str().unwrap().contains("причина:"))
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
|
.unwrap()
|
||||||
|
.contains("Главный управленческий вывод")
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
report["executive_points"]
|
report["executive_points"]
|
||||||
.as_array()
|
.as_array()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|item| item.as_str().unwrap().contains("Связанная картина риска"))
|
.any(|item| item
|
||||||
);
|
.as_str()
|
||||||
assert!(
|
|
||||||
report["executive_points"]
|
|
||||||
.as_array()
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.contains("Статус связанной картины риска"))
|
||||||
.any(|item| item.as_str().unwrap().contains("Кандидат в инцидент"))
|
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
report["markdown"]
|
report["markdown"]
|
||||||
@@ -10007,6 +10102,11 @@ mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.contains("## Связанная картина риска")
|
.contains("## Связанная картина риска")
|
||||||
);
|
);
|
||||||
|
let markdown = report["markdown"].as_str().unwrap();
|
||||||
|
assert!(
|
||||||
|
markdown.find("## Связанная картина риска").unwrap()
|
||||||
|
< markdown.find("## Достоверность данных").unwrap()
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
report["markdown"]
|
report["markdown"]
|
||||||
.as_str()
|
.as_str()
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ async function postJson(path, payload) {
|
|||||||
|
|
||||||
function statusClass(status) {
|
function statusClass(status) {
|
||||||
const s = String(status || "UNKNOWN").toLowerCase();
|
const s = String(status || "UNKNOWN").toLowerCase();
|
||||||
if (s === "ok" || s === "true" || s === "low" || s === "false_positive" || s === "resolved") return "status-ok";
|
if (s === "ok" || s === "true" || s === "normal" || s === "low" || s === "false_positive" || s === "resolved") return "status-ok";
|
||||||
if (s === "warn" || s === "warning" || s === "fallback" || s === "stale" || s === "medium" || s === "in_review" || s === "postponed" || s === "open" || s === "in_progress") return "status-warn";
|
if (s === "warn" || s === "warning" || s === "attention" || s === "fallback" || s === "stale" || s === "medium" || s === "in_review" || s === "postponed" || s === "open" || s === "in_progress") return "status-warn";
|
||||||
if (s === "degraded" || s === "high" || s === "confirmed" || s === "rejected" || s === "archived") return "status-degraded";
|
if (s === "degraded" || s === "high" || s === "high_risk" || s === "confirmed" || s === "rejected" || s === "archived") return "status-degraded";
|
||||||
if (s === "fail" || s === "false" || s === "error" || s === "critical" || s === "missing") return "status-fail";
|
if (s === "fail" || s === "false" || s === "error" || s === "critical" || s === "missing") return "status-fail";
|
||||||
return "status-unknown";
|
return "status-unknown";
|
||||||
}
|
}
|
||||||
@@ -319,6 +319,7 @@ function renderExecutiveDashboard(report) {
|
|||||||
const highRisk = Array.isArray(dashboard.high_risk_departments) ? dashboard.high_risk_departments : [];
|
const highRisk = Array.isArray(dashboard.high_risk_departments) ? dashboard.high_risk_departments : [];
|
||||||
const candidates = Array.isArray(dashboard.critical_candidates) ? dashboard.critical_candidates : [];
|
const candidates = Array.isArray(dashboard.critical_candidates) ? dashboard.critical_candidates : [];
|
||||||
const summary = dashboard.summary || {};
|
const summary = dashboard.summary || {};
|
||||||
|
const narrativeStatus = summary.risk_narrative_status || executiveDashboardStatus(dashboard);
|
||||||
return `
|
return `
|
||||||
<section class="card executive-dashboard-card">
|
<section class="card executive-dashboard-card">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
@@ -326,7 +327,11 @@ function renderExecutiveDashboard(report) {
|
|||||||
<h3>Сводка руководителя</h3>
|
<h3>Сводка руководителя</h3>
|
||||||
<p class="muted">Что происходит в организации прямо сейчас: доверие к KPI, покрытие, риски, дела и готовность расследований.</p>
|
<p class="muted">Что происходит в организации прямо сейчас: доверие к KPI, покрытие, риски, дела и готовность расследований.</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge ${statusClass(executiveDashboardStatus(dashboard))}">${ui(dashboard.forensics_readiness || "UNKNOWN")}</span>
|
<span class="badge ${statusClass(narrativeStatus)}">${ui(narrativeStatus)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list compact-list executive-summary-list">
|
||||||
|
<div class="row compact-row"><strong>Главная причина риска</strong><span class="muted">${ui(summary.main_risk_cause || "связанный риск не выражен")}</span><span class="badge ${statusClass(narrativeStatus)}">${ui(narrativeStatus)}</span></div>
|
||||||
|
<div class="row compact-row"><strong>Подтверждающие слои</strong><span class="muted">Trust KPI · Agent Coverage · Business Risk · Risk Heatmap · Security Correlation · Incident Candidates · Cases</span><span></span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="quality-grid">
|
<div class="quality-grid">
|
||||||
<div><span class="muted">Trust KPI</span><strong>${ui(optionalPercent(dashboard.trust_kpi_score))}</strong></div>
|
<div><span class="muted">Trust KPI</span><strong>${ui(optionalPercent(dashboard.trust_kpi_score))}</strong></div>
|
||||||
@@ -337,13 +342,10 @@ function renderExecutiveDashboard(report) {
|
|||||||
<div><span class="muted">Закрыто за 30 дней</span><strong>${ui(dashboard.resolved_cases_30d ?? 0)}</strong></div>
|
<div><span class="muted">Закрыто за 30 дней</span><strong>${ui(dashboard.resolved_cases_30d ?? 0)}</strong></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="list compact-list executive-summary-list">
|
<div class="list compact-list executive-summary-list">
|
||||||
<div class="row compact-row"><strong>Главная причина риска</strong><span class="muted">${ui(summary.main_risk_cause || "связанный риск не выражен")}</span><span></span></div>
|
|
||||||
<div class="row compact-row"><strong>Главный риск</strong><span class="muted">${ui(summary.main_risk || "нет данных")}</span><span></span></div>
|
<div class="row compact-row"><strong>Главный риск</strong><span class="muted">${ui(summary.main_risk || "нет данных")}</span><span></span></div>
|
||||||
<div class="row compact-row"><strong>Главное улучшение</strong><span class="muted">${ui(summary.main_improvement || "нет данных")}</span><span></span></div>
|
<div class="row compact-row"><strong>Главное улучшение</strong><span class="muted">${ui(summary.main_improvement || "нет данных")}</span><span></span></div>
|
||||||
<div class="row compact-row"><strong>Пробел в данных</strong><span class="muted">${ui(summary.main_data_gap || "нет данных")}</span><span></span></div>
|
<div class="row compact-row"><strong>Пробел в данных</strong><span class="muted">${ui(summary.main_data_gap || "нет данных")}</span><span></span></div>
|
||||||
</div>
|
</div>
|
||||||
${highRisk.length ? `<p class="muted small">Риск-подразделения: ${ui(highRisk.slice(0, 5).map(item => `${item.department} (${item.risk_level})`).join("; "))}</p>` : ""}
|
|
||||||
${candidates.length ? `<p class="muted small">Кандидаты для проверки: ${ui(candidates.slice(0, 5).map(item => `${item.id} (${item.risk_level})`).join("; "))}</p>` : ""}
|
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -1838,7 +1840,9 @@ function renderRiskLayerLinks(links) {
|
|||||||
|
|
||||||
function riskLayerTarget(target) {
|
function riskLayerTarget(target) {
|
||||||
const value = String(target || "");
|
const value = String(target || "");
|
||||||
|
if (value === "risk_heatmap") return { tab: "operator", selector: ".risk-heatmap-card" };
|
||||||
if (value === "business_risk") return { tab: "operator", selector: "#business-risk-section" };
|
if (value === "business_risk") return { tab: "operator", selector: "#business-risk-section" };
|
||||||
|
if (value === "security_correlation") return { tab: "operator", selector: ".security-correlation-card" };
|
||||||
if (value === "incident_candidates") return { tab: "operator", selector: "#risk-candidates-section" };
|
if (value === "incident_candidates") return { tab: "operator", selector: "#risk-candidates-section" };
|
||||||
if (value === "cases") return { tab: "incidents", selector: "#cases-section" };
|
if (value === "cases") return { tab: "incidents", selector: "#cases-section" };
|
||||||
if (value === "agent_coverage") return { tab: "operator", selector: "#agent-coverage-section" };
|
if (value === "agent_coverage") return { tab: "operator", selector: "#agent-coverage-section" };
|
||||||
|
|||||||
+35
-14
@@ -1,8 +1,13 @@
|
|||||||
# Business Risk
|
# Business Risk
|
||||||
|
|
||||||
`Business Risk` - управленческий слой AWatch-rus / DetMir, который показывает
|
`Business Risk` - расчетный слой AWatch-rus / DetMir, который показывает не
|
||||||
не просто активность сотрудников, а зоны организационного риска по
|
просто активность сотрудников, а зоны организационного риска по подразделениям.
|
||||||
подразделениям.
|
|
||||||
|
Над ним расположен основной управленческий слой `Risk Narrative`: единая
|
||||||
|
связанная картина риска для руководителя. Он не создает новых сущностей и не
|
||||||
|
добавляет workflow, а связывает уже рассчитанные слои в один вывод:
|
||||||
|
`Trust KPI → Agent Coverage → Business Risk → Risk Heatmap → Security
|
||||||
|
Correlation → Incident Candidates → Cases`.
|
||||||
|
|
||||||
## Назначение
|
## Назначение
|
||||||
|
|
||||||
@@ -14,8 +19,9 @@
|
|||||||
- где есть падение тренда;
|
- где есть падение тренда;
|
||||||
- где проблемные рабочие места портят доверие к отчету.
|
- где проблемные рабочие места портят доверие к отчету.
|
||||||
|
|
||||||
Business Risk не является автоматическим обвинением сотрудников или
|
Business Risk и Risk Narrative не являются автоматическим обвинением
|
||||||
подразделений. Это read-only приоритизация управленческой проверки.
|
сотрудников или подразделений. Это read-only приоритизация управленческой
|
||||||
|
проверки.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
@@ -62,9 +68,11 @@ Business Risk не является автоматическим обвинен
|
|||||||
- `summary.main_risk` - главный риск текущего среза;
|
- `summary.main_risk` - главный риск текущего среза;
|
||||||
- `summary.main_improvement` - главное подтвержденное улучшение;
|
- `summary.main_improvement` - главное подтвержденное улучшение;
|
||||||
- `summary.main_data_gap` - главный пробел в данных.
|
- `summary.main_data_gap` - главный пробел в данных.
|
||||||
|
- `summary.risk_narrative_status` - optional единый статус связанной картины
|
||||||
|
риска: `NORMAL`, `ATTENTION`, `HIGH_RISK` или `CRITICAL`;
|
||||||
- `summary.main_risk_cause` - optional связанная причина риска:
|
- `summary.main_risk_cause` - optional связанная причина риска:
|
||||||
Trust KPI, coverage, Business Risk, candidates, cases и готовность
|
Trust KPI, Agent Coverage, Risk Heatmap, Business Risk, Security
|
||||||
Forensics.
|
Correlation, Incident Candidates, Cases и готовность Forensics.
|
||||||
|
|
||||||
`risk_heatmap`:
|
`risk_heatmap`:
|
||||||
|
|
||||||
@@ -79,9 +87,11 @@ Business Risk не является автоматическим обвинен
|
|||||||
- `heat_level` - итоговая зона карты: `LOW`, `MEDIUM`, `HIGH`,
|
- `heat_level` - итоговая зона карты: `LOW`, `MEDIUM`, `HIGH`,
|
||||||
`CRITICAL` или `UNKNOWN`.
|
`CRITICAL` или `UNKNOWN`.
|
||||||
- `links` - optional read-only переходы к уже существующим слоям портала:
|
- `links` - optional read-only переходы к уже существующим слоям портала:
|
||||||
Trust KPI, Business Risk, кандидаты, дела и покрытие агентов.
|
Trust KPI, Agent Coverage, Business Risk, Risk Heatmap, Security
|
||||||
|
Correlation, Incident Candidates и Cases.
|
||||||
- `summary` - optional связанная строка
|
- `summary` - optional связанная строка
|
||||||
`Trust → Coverage → Business Risk → Candidates → Cases → вывод`.
|
`Trust → Coverage → Business Risk → Risk Heatmap → Security Correlation →
|
||||||
|
Candidates → Cases → вывод`.
|
||||||
|
|
||||||
`security_correlation`:
|
`security_correlation`:
|
||||||
|
|
||||||
@@ -336,13 +346,19 @@ POST /api/cases/{case_id}/status
|
|||||||
|
|
||||||
## Markdown
|
## Markdown
|
||||||
|
|
||||||
Оперативный отчет содержит раздел:
|
Оперативный отчет содержит раздел `Связанная картина риска` сразу после
|
||||||
|
`Сводка руководителя`, до технических деталей. Это главный управленческий
|
||||||
|
вывод: что происходит, почему это риск и какие слои это подтверждают.
|
||||||
|
|
||||||
|
Оперативный отчет содержит разделы:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
## Сводка руководителя
|
## Сводка руководителя
|
||||||
|
## Связанная картина риска
|
||||||
|
## Достоверность данных
|
||||||
|
## SLA покрытия агентов
|
||||||
## Карта рисков подразделений
|
## Карта рисков подразделений
|
||||||
## Корреляция Workforce ↔ Security
|
## Корреляция Workforce ↔ Security
|
||||||
## Связанная картина риска
|
|
||||||
## Риски подразделений
|
## Риски подразделений
|
||||||
## Динамика бизнес-рисков
|
## Динамика бизнес-рисков
|
||||||
## Кандидаты в инциденты
|
## Кандидаты в инциденты
|
||||||
@@ -354,13 +370,18 @@ POST /api/cases/{case_id}/status
|
|||||||
|
|
||||||
## Executive Summary
|
## Executive Summary
|
||||||
|
|
||||||
Executive summary показывает до 3 наиболее рискованных подразделений, если
|
Executive summary сначала показывает связанный управленческий вывод по Risk
|
||||||
их уровень риска выше `LOW`.
|
Narrative, а затем только поддерживающие технические пункты. Отдельные строки
|
||||||
|
по Business Risk и кандидатам не дублируют главный вывод, если эти причины уже
|
||||||
|
вошли в narrative.
|
||||||
|
|
||||||
Формат:
|
Формат:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Подразделение Бухгалтерия: HIGH — причина: низкий Trust KPI
|
Главный управленческий вывод: В подразделении Бухгалтерия связаны слои:
|
||||||
|
Trust KPI 50%, Agent Coverage 80%, Risk Heatmap HIGH, Business Risk MEDIUM,
|
||||||
|
Security Correlation 40/100, Incident Candidates 1, Cases 0. риск связан из-за
|
||||||
|
слабого Trust KPI, падения активности, слабого покрытия агентов.
|
||||||
```
|
```
|
||||||
|
|
||||||
Если подразделение 3+ дня подряд находится в `HIGH` или `CRITICAL`,
|
Если подразделение 3+ дня подряд находится в `HIGH` или `CRITICAL`,
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ async function main() {
|
|||||||
"Сотрудников в работе",
|
"Сотрудников в работе",
|
||||||
"Средний индекс активности",
|
"Средний индекс активности",
|
||||||
"Главная причина риска",
|
"Главная причина риска",
|
||||||
|
"Подтверждающие слои",
|
||||||
"Достоверность данных агента",
|
"Достоверность данных агента",
|
||||||
"Стабильность агента за 7 дней",
|
"Стабильность агента за 7 дней",
|
||||||
"Качество данных по рабочим местам",
|
"Качество данных по рабочим местам",
|
||||||
|
|||||||
Reference in New Issue
Block a user