- ui/monitoring_tab.py (1546 dong) tach thanh presentation/monitoring/** (container + 7 tab/card + shared helper), ui/monitoring_tab.py con lai re-export shim de app.py khong doi. - infrastructure/telemetry/audit_logger.py: CanonicalAuditLogger, core/audit_log.py thanh wrapper mong, tuong thich nguoc 100% voi schema .jsonl cu. - application/monitoring/monitoring_query_service.py: MonitoringQueryService read-only, filter/sort/pagination, khong import PySide6. - Go circular import model_pricing<->usage_tracker va agent_security<-> agent_security_alert (core/agent_security_types.py moi). - infrastructure/sandbox/sandbox_capabilities.py: SandboxCapabilityMatrix theo OS (Windows/Linux/macOS), chua dau noi vao core/sandbox_manager.py. - conftest.py: sua loi checkout khong ten cowork_local khien pytest import nham thu muc khac. - 77 test moi, 167/167 pass. QA da xac nhan UI/business logic khong doi (xem evidence/report/unified_report.html). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
190 lines
7.7 KiB
Python
190 lines
7.7 KiB
Python
"""Right-hand "Chi tiet su kien" detail panel shared by the Security Events /
|
|
MCP Call History / Action Logs tabs — the full record behind whichever row is
|
|
selected in an :class:`~.event_table.EventTable`. Extracted verbatim from
|
|
``ui/monitoring_tab.py``.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Dict, List, Tuple
|
|
|
|
from PySide6.QtCore import Qt, QTimer, Signal
|
|
from PySide6.QtGui import QGuiApplication
|
|
from PySide6.QtWidgets import (
|
|
QHBoxLayout, QLabel, QPushButton, QScrollArea, QVBoxLayout, QWidget,
|
|
)
|
|
|
|
from ....core import agent_roles
|
|
from ....i18n import tr
|
|
from ....ui.icons import icon
|
|
from .badges import agent_badge_name, action_label, apply_badge, severity_info, status_info
|
|
from .formatters import event_id, fmt_event_time_full
|
|
from .layout_helpers import kv_row
|
|
|
|
# Cosmetic label only — no real policy-versioning system exists yet.
|
|
_STATIC_POLICY_LABEL = "security_policy_v2"
|
|
|
|
|
|
class EventDetailPanel(QWidget):
|
|
"""Laid out as three labelled sections, a terminal-style block quote for
|
|
the detail text, and a metadata footer, closed by the header close
|
|
button, the footer button, Esc, or a click outside the table/panel (see
|
|
:class:`~.event_table.ClickOutsideCloser`)."""
|
|
|
|
closed = Signal()
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.setObjectName("monSection")
|
|
self._detail_text = ""
|
|
outer = QVBoxLayout(self)
|
|
|
|
hdr = QHBoxLayout()
|
|
self._title_lbl = QLabel()
|
|
self._title_lbl.setStyleSheet("font-weight:700;")
|
|
hdr.addWidget(self._title_lbl, 1)
|
|
self._close_btn = QPushButton()
|
|
self._close_btn.setIcon(icon("close"))
|
|
self._close_btn.setFlat(True)
|
|
self._close_btn.setFixedWidth(28)
|
|
self._close_btn.setCursor(Qt.PointingHandCursor)
|
|
self._close_btn.clicked.connect(self.closed.emit)
|
|
hdr.addWidget(self._close_btn)
|
|
outer.addLayout(hdr)
|
|
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
scroll.setFrameShape(QScrollArea.NoFrame)
|
|
body = QWidget()
|
|
self._body_lay = QVBoxLayout(body)
|
|
self._body_lay.setContentsMargins(0, 0, 4, 0)
|
|
scroll.setWidget(body)
|
|
outer.addWidget(scroll, 1)
|
|
|
|
self._section_hdrs: List[Tuple[str, QLabel]] = []
|
|
self._rows: Dict[str, Tuple[QLabel, QLabel]] = {}
|
|
|
|
def _section(key: str) -> None:
|
|
lbl = QLabel()
|
|
lbl.setObjectName("detailSectionHdr")
|
|
self._body_lay.addWidget(lbl)
|
|
self._section_hdrs.append((key, lbl))
|
|
|
|
def _field(key: str) -> QLabel:
|
|
lbl, val = kv_row(self._body_lay)
|
|
self._rows[key] = (lbl, val)
|
|
return val
|
|
|
|
_section("general")
|
|
_field("time")
|
|
self._agent_val = _field("agent")
|
|
_field("account")
|
|
self._machine_val = _field("machine")
|
|
self._machine_val.setObjectName("monoChip")
|
|
|
|
_section("action")
|
|
self._type_val = _field("type")
|
|
self._type_val.setObjectName("neutralTag")
|
|
self._status_val = _field("status")
|
|
|
|
_section("block")
|
|
code_box = QWidget()
|
|
code_box.setObjectName("detailCodeBlock")
|
|
code_lay = QHBoxLayout(code_box)
|
|
code_lay.setContentsMargins(8, 6, 8, 6)
|
|
self._code_text = QLabel()
|
|
self._code_text.setObjectName("detailCodeText")
|
|
self._code_text.setWordWrap(True)
|
|
self._code_text.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
|
code_lay.addWidget(self._code_text, 1)
|
|
self._copy_btn = QPushButton()
|
|
self._copy_btn.setObjectName("detailCopyBtn")
|
|
self._copy_btn.setCursor(Qt.PointingHandCursor)
|
|
self._copy_btn.clicked.connect(self._copy_detail)
|
|
code_lay.addWidget(self._copy_btn, 0, Qt.AlignVCenter)
|
|
self._body_lay.addWidget(code_box)
|
|
|
|
_section("metadata")
|
|
self._event_id_val = _field("event_id")
|
|
self._event_id_val.setObjectName("monoChip")
|
|
self._policy_val = _field("policy")
|
|
self._severity_val = _field("severity")
|
|
|
|
self._body_lay.addStretch(1)
|
|
|
|
footer = QHBoxLayout()
|
|
footer.setContentsMargins(0, 6, 0, 0)
|
|
self._footer_close_btn = QPushButton()
|
|
self._footer_close_btn.setObjectName("primary")
|
|
self._footer_close_btn.setCursor(Qt.PointingHandCursor)
|
|
self._footer_close_btn.clicked.connect(self.closed.emit)
|
|
footer.addWidget(self._footer_close_btn, 1)
|
|
outer.addLayout(footer)
|
|
|
|
def retranslate(self) -> None:
|
|
self._title_lbl.setText(tr("monitoring.security_detail_title"))
|
|
self._close_btn.setToolTip(tr("monitoring.security_detail_close"))
|
|
self._footer_close_btn.setText(tr("monitoring.security_detail_close"))
|
|
self._footer_close_btn.setIcon(icon("close"))
|
|
section_keys = {
|
|
"general": "monitoring.detail_section_general",
|
|
"action": "monitoring.detail_section_action",
|
|
"block": "monitoring.col_detail_block",
|
|
"metadata": "monitoring.detail_section_metadata",
|
|
}
|
|
for key, lbl in self._section_hdrs:
|
|
lbl.setText(tr(section_keys[key]).upper())
|
|
self._rows["time"][0].setText(tr("monitoring.col_time"))
|
|
self._rows["agent"][0].setText(tr("monitoring.col_agent"))
|
|
self._rows["account"][0].setText(tr("monitoring.col_account"))
|
|
self._rows["machine"][0].setText(tr("monitoring.col_machine"))
|
|
self._rows["type"][0].setText(tr("monitoring.detail_type"))
|
|
self._rows["status"][0].setText(tr("monitoring.detail_status"))
|
|
self._rows["event_id"][0].setText(tr("monitoring.detail_event_id"))
|
|
self._rows["policy"][0].setText(tr("monitoring.detail_policy"))
|
|
self._rows["severity"][0].setText(tr("monitoring.detail_severity"))
|
|
if not self._copy_btn.text() or self._copy_btn.text() != tr("monitoring.detail_copied"):
|
|
self._reset_copy_btn()
|
|
|
|
def show_event(self, ev: dict, row: int) -> None:
|
|
na = "—"
|
|
self._rows["time"][1].setText(fmt_event_time_full(ev.get("ts", "")) or na)
|
|
|
|
agent_label = agent_roles.label_for(ev.get("agent_role", "")) or na
|
|
self._agent_val.setText(agent_label)
|
|
apply_badge(self._agent_val,
|
|
agent_badge_name(agent_label) if agent_label != na else "badge")
|
|
|
|
self._rows["account"][1].setText(ev.get("account", "") or na)
|
|
self._machine_val.setText(ev.get("machine", "") or na)
|
|
|
|
name = ev.get("name", "")
|
|
kind = ev.get("kind", "security_block")
|
|
ok = ev.get("ok", False)
|
|
self._type_val.setText(action_label(name) or na)
|
|
status_badge, status_key = status_info(name, kind, ok)
|
|
self._status_val.setText(tr(status_key))
|
|
apply_badge(self._status_val, status_badge)
|
|
|
|
self._detail_text = ev.get("detail", "") or na
|
|
self._code_text.setText(self._detail_text)
|
|
self._reset_copy_btn()
|
|
|
|
self._event_id_val.setText(event_id(ev.get("ts", ""), row))
|
|
# The static policy label names a real enforcement ruleset — only
|
|
# meaningful for a Security Events row; MCP calls/generic actions
|
|
# were never evaluated against it.
|
|
self._policy_val.setText(_STATIC_POLICY_LABEL if kind == "security_block" else na)
|
|
severity_badge, severity_key = severity_info(name, kind, ok)
|
|
self._severity_val.setText(tr(severity_key))
|
|
apply_badge(self._severity_val, severity_badge)
|
|
|
|
def _copy_detail(self) -> None:
|
|
QGuiApplication.clipboard().setText(self._detail_text)
|
|
self._copy_btn.setText(tr("monitoring.detail_copied"))
|
|
self._copy_btn.setIcon(icon("check"))
|
|
QTimer.singleShot(1500, self._reset_copy_btn)
|
|
|
|
def _reset_copy_btn(self) -> None:
|
|
self._copy_btn.setText(tr("monitoring.detail_copy"))
|
|
self._copy_btn.setIcon(icon("document"))
|