diff --git a/web/src/App.css b/web/src/App.css index d28a34c..0556df5 100644 --- a/web/src/App.css +++ b/web/src/App.css @@ -1,8 +1,70 @@ .app-layout { + --sd-bg: #f4f6f9; + --sd-border: #dde3eb; + --sd-text: #172b45; + --sd-muted: #526379; + --sd-blue: #2457c5; display: flex; - align-items: flex-start; + align-items: stretch; + min-height: 100vh; + background: var(--sd-bg); + color: var(--sd-text); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-size: 0.875rem; + line-height: 1.5; + -webkit-font-smoothing: antialiased; } .app-layout .dashboard { flex: 1; + min-width: 0; + width: 100%; + max-width: 1680px; + padding: 32px; +} + +.app-layout [id] { + scroll-margin-top: 24px; +} + +.app-layout :focus-visible { + outline: 3px solid #2457c5; + outline-offset: 3px; +} + +.app-layout ::selection { + background: #dbe7ff; + color: #172b45; +} + +.app-layout .form-label { + margin-bottom: 6px; + color: var(--sd-muted); + font-size: 0.75rem; + font-weight: 600; +} + +.app-layout .form-select, +.app-layout .form-control { + min-height: 40px; + border-color: #c4cedb; + border-radius: 6px; + color: var(--sd-text); + font-size: 0.875rem; +} + +@media (max-width: 991.98px) { + .app-layout { + flex-direction: column; + } + + .app-layout .dashboard { + padding: 24px; + } +} + +@media (max-width: 575.98px) { + .app-layout .dashboard { + padding: 20px 12px; + } } diff --git a/web/src/components/AlertPanel.css b/web/src/components/AlertPanel.css index 518cb82..563bfbf 100644 --- a/web/src/components/AlertPanel.css +++ b/web/src/components/AlertPanel.css @@ -1,103 +1,377 @@ .alert-panel { - border: none; + container: alerts / inline-size; + min-width: 0; + border: 1px solid #dde3eb; border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background: #fff; height: 100%; + color: #172b45; } .alert-panel .card-header { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - border-radius: 12px 12px 0 0 !important; - padding: 1rem 1.5rem; + background: #fff; + border-bottom: 1px solid #dde3eb; + border-radius: 12px 12px 0 0; + padding: 20px 24px; } .alert-panel .card-title { margin: 0; - font-size: 1.25rem; - font-weight: 600; + font-size: 1rem; + font-weight: 650; +} + +.alert-panel .card-body { + padding: 24px; +} + +.alert-panel .alert-toolbar { + display: flex; + flex-wrap: wrap; + align-items: end; + gap: 24px; + margin-bottom: 24px; } -.alert-stats { +.alert-panel .alert-stats { display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 1rem; - padding: 1rem; - background: #f8f9fa; - border-radius: 8px; + flex: 1 1 360px; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px; } -.stat-item { +.alert-panel .stat-item { display: flex; flex-direction: column; - align-items: center; + gap: 4px; } -.stat-label { - font-size: 0.75rem; - color: #6c757d; +.alert-panel .stat-label { + font-size: 0.6875rem; + color: #526379; text-transform: uppercase; - letter-spacing: 0.5px; + letter-spacing: 0.04em; + overflow-wrap: anywhere; } -.stat-value { +.alert-panel .stat-value { font-size: 1.5rem; - font-weight: 700; - color: #2c3e50; + line-height: 1.2; + font-weight: 650; + color: #172b45; + font-variant-numeric: tabular-nums; } -.stat-value.new { - color: #007bff; +.alert-panel .stat-value.new { + color: #2457c5; } -.stat-value.acknowledged { - color: #ffc107; +.alert-panel .stat-value.acknowledged { + color: #92400e; } -.stat-value.resolved { - color: #28a745; +.alert-panel .stat-value.resolved { + color: #047857; } -.alert-filters { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1rem; +.alert-panel .alert-filters { + display: flex; + flex: 1 1 300px; + gap: 12px; } -.filter-group { - margin-bottom: 0 !important; +.alert-panel .filter-group { + flex: 1; + min-width: 0; } -.bulk-actions { - padding: 0.75rem; - background: #e7f3ff; - border-radius: 8px; - border-left: 4px solid #007bff; +.alert-panel .alert-selection { + margin-bottom: 12px; + color: #526379; + font-size: 0.75rem; } -.table { - margin-bottom: 0; +.alert-panel .bulk-actions { + padding: 12px; + background: #eff5ff; + border-radius: 6px; } -.table th { - font-weight: 600; - color: #495057; - border-top: none; - padding: 0.75rem; +.alert-panel .table-responsive { + overflow: visible; +} + +.alert-panel .table { + table-layout: fixed; + width: 100%; + margin: 0; + font-size: 0.8125rem; + --bs-table-color: #33465e; + --bs-table-border-color: #e5eaf0; + --bs-table-hover-bg: #f8fafc; } -.table td { +.alert-panel .select-column { + width: 32px; +} + +.alert-panel .severity-column { + width: 76px; +} + +.alert-panel .type-column { + width: 102px; +} + +.alert-panel .status-column { + width: 116px; +} + +.alert-panel .time-column { + width: 110px; +} + +.alert-panel .actions-column { + width: 280px; +} + +.alert-panel .table th { + padding: 12px 8px; + background: #f4f6f9; + color: #526379; + font-size: 0.6875rem; + font-weight: 650; vertical-align: middle; - padding: 0.75rem; } -/* Responsive adjustments */ -@media (max-width: 768px) { - .alert-stats { - grid-template-columns: repeat(2, 1fr); +.alert-panel .table td { + padding: 16px 8px; + vertical-align: middle; + overflow-wrap: anywhere; +} + +.alert-panel .alert-type { + font-size: 0.75rem; +} + +.alert-panel .alert-message { + white-space: pre-wrap; + word-break: break-word; +} + +.alert-panel .alert-hash { + padding: 1px 3px; + border-radius: 3px; + background: #edf1f6; + color: #33465e; + font-size: 0.75rem; + word-break: break-all; +} + +.alert-panel .alert-time span { + display: block; + white-space: nowrap; + font-variant-numeric: tabular-nums; + font-size: 0.75rem; +} + +.alert-panel .alert-time span + span { + color: #526379; +} + +.alert-panel .alert-actions { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.alert-panel .alert-actions .btn { + padding: 5px 8px; + font-size: 0.75rem; + font-weight: 600; + white-space: nowrap; +} + +.alert-panel .btn-outline-warning { + --bs-btn-color: #92400e; + --bs-btn-bg: #fef3c7; + --bs-btn-border-color: #f5d97d; + --bs-btn-hover-color: #78350f; + --bs-btn-hover-bg: #fde68a; + --bs-btn-hover-border-color: #d9ad31; + --bs-btn-active-color: #78350f; + --bs-btn-active-bg: #fde68a; +} + +.alert-panel .btn-outline-success { + --bs-btn-color: #065f46; + --bs-btn-bg: #ecfdf5; + --bs-btn-border-color: #a7d8c5; + --bs-btn-hover-bg: #065f46; + --bs-btn-hover-border-color: #065f46; +} + +.alert-panel .btn-outline-primary { + --bs-btn-color: #2457c5; + --bs-btn-bg: #eff5ff; + --bs-btn-border-color: #c9d9fa; + --bs-btn-hover-bg: #2457c5; + --bs-btn-hover-border-color: #2457c5; +} + +.alert-panel .badge { + font-size: 0.6875rem; + font-weight: 600; + padding: 5px 7px; + white-space: normal; +} + +.alert-panel .badge.bg-warning, + +.alert-panel .badge.bg-info { + color: #172b45; +} + +.alert-panel .alert-footer { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + gap: 12px; + padding: 16px 24px; + background: #fff; + border-color: #dde3eb; + border-radius: 0 0 12px 12px; +} + +.alert-panel .alert-page-summary, +.alert-panel .alert-empty { + color: #526379; + font-size: 0.75rem; +} + +.alert-panel .alert-empty { + padding: 24px 0; + margin: 0; +} + +.alert-panel .pagination { + flex-wrap: wrap; + gap: 4px; + margin: 0; +} + +.alert-panel .page-link { + border-radius: 4px; + color: #2457c5; + min-width: 30px; + text-align: center; +} + +.alert-panel .page-item.active .page-link { + background: #2457c5; + border-color: #2457c5; + color: #fff; +} + +.alert-panel .page-item.disabled .page-link { + color: #637083; + background: #f4f6f9; +} + +@container alerts (max-width: 850px) { + + .alert-panel .card-body { + padding: 16px; + } + + .alert-panel .alert-stats { + flex-basis: 100%; + } + + .alert-panel .alert-filters { + flex-basis: 100%; + } + + .alert-panel .alert-footer { + padding: 16px; + } + + .alert-panel .alerts-table, + + .alert-panel .alerts-table tbody { + display: block; + } + + .alert-panel .alerts-table colgroup { + display: none; + } + + .alert-panel .alerts-table thead { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + } + + .alert-panel .alerts-table tbody tr { + position: relative; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + padding: 16px; + border: 1px solid #dde3eb; + border-radius: 8px; + margin-bottom: 12px; + } + + .alert-panel .alerts-table td { + display: block; + padding: 0; + border: 0; + box-shadow: none; + background: transparent; + } + + .alert-panel .alerts-table td::before { + content: attr(data-label); + display: block; + margin-bottom: 4px; + color: #526379; + font-size: 0.6875rem; + } + + .alert-panel .alerts-table td:first-child { + position: absolute; + top: 16px; + right: 16px; + } + + .alert-panel .alerts-table .alert-type { + padding-right: 24px; + } + + .alert-panel .alerts-table .alert-message, + + .alert-panel .alerts-table .alert-action-cell { + grid-column: 1 / -1; + } + + .alert-panel .alert-actions .btn { + min-height: 36px; + padding: 6px 10px; + font-size: 0.75rem; + } +} + +@container alerts (max-width: 400px) { + + .alert-panel .alert-stats { + grid-template-columns: repeat(2, minmax(0, 1fr)); } - .alert-filters { - grid-template-columns: 1fr; + .alert-panel .alert-filters { + flex-direction: column; } } diff --git a/web/src/components/AlertPanel.tsx b/web/src/components/AlertPanel.tsx index 005927d..0df52eb 100644 --- a/web/src/components/AlertPanel.tsx +++ b/web/src/components/AlertPanel.tsx @@ -4,6 +4,7 @@ import apiService from '../services/api'; import webSocketService from '../services/websocket'; import { Alert, AlertSeverity, AlertStatus, AlertFilter, AlertStats } from '../types/alerts'; import './AlertPanel.css'; +import './DetailsModal.css'; const ITEMS_PER_PAGE = 10; @@ -232,59 +233,72 @@ const AlertPanel: React.FC = () => { )} - {/* Statistics */} - {stats && ( -
-
- Total - {stats.totalCount} -
-
- New - {stats.newCount} -
-
- Acknowledged - {stats.acknowledgedCount} -
-
- Resolved - {stats.resolvedCount} +
+ {/* Statistics */} + {stats && ( +
+
+ Total + {stats.totalCount.toLocaleString()} +
+
+ New + {stats.newCount.toLocaleString()} +
+
+ Acknowledged + {stats.acknowledgedCount.toLocaleString()} +
+
+ Resolved + {stats.resolvedCount.toLocaleString()} +
+ )} + + {/* Filters */} +
+ + Filter by severity + setFilter({ ...filter, severity: e.target.value ? [e.target.value as AlertSeverity] : undefined })} + > + + + + + + + + + + + Filter by status + setFilter({ ...filter, status: e.target.value ? [e.target.value as AlertStatus] : undefined })} + > + + + + + +
- )} - {/* Filters */} -
- - Filter by severity - setFilter({ ...filter, severity: e.target.value ? [e.target.value as AlertSeverity] : undefined })} - > - - - - - - - - - - - Filter by status - setFilter({ ...filter, status: e.target.value ? [e.target.value as AlertStatus] : undefined })} - > - - - - - -
+ {!loading && alerts.length > 0 && ( + + )} + {/* Bulk Actions */} {selectedAlerts.size > 0 && (
@@ -297,18 +311,25 @@ const AlertPanel: React.FC = () => { {/* Alerts Table */} {loading ? (
- + Loading alerts...
) : ( <> - +
+ + + + + + + + + + @@ -323,56 +344,78 @@ const AlertPanel: React.FC = () => { - - - - + + - - + ))}
Recent security alerts
- + Select Severity Type
handleSelectAlert(alert.id)} /> + {alert.severity} {alert.alertType}{alert.message} + {alert.alertType.replace(/([a-z])([A-Z])/g, '$1 $2')} + {alert.message.split(/((?:sha256:)?[a-f0-9]{64})/gi).map((part, index) => + /^(?:sha256:)?[a-f0-9]{64}$/i.test(part) + ? {part} + : part + )} + {alert.status} {new Date(alert.timestamp).toLocaleString()} - {alert.status === 'New' && ( - - )} - {alert.status !== 'Resolved' && ( + + + +
+ {alert.status === 'New' && ( + + )} + {alert.status !== 'Resolved' && ( + + )} - )} - +
- {/* Pagination */} - {totalPages > 1 && ( - + {!error && alerts.length === 0 &&

No alerts match the selected filters.

} + + )} + + {!loading && alerts.length > 0 && ( + + + {(currentPage - 1) * ITEMS_PER_PAGE + 1}–{Math.min(currentPage * ITEMS_PER_PAGE, alerts.length)} of {alerts.length.toLocaleString()} alerts + + + {totalPages > 1 && ( + + )} + + )} {/* Alert Detail Modal */} - + - Alert Details + Alert Details {selectedAlert && ( -
-

Type: {selectedAlert.alertType}

-

Severity: {selectedAlert.severity}

-

Status: {selectedAlert.status}

-

Message: {selectedAlert.message}

-

Time: {new Date(selectedAlert.timestamp).toLocaleString()}

+ <> +
+
ID
{selectedAlert.id}
+
Type
{selectedAlert.alertType}
+
Severity
{selectedAlert.severity}
+
Status
{selectedAlert.status}
+
Time
+
Message
{selectedAlert.message}
+
{selectedAlert.metadata && ( -
- Metadata: +
+

Metadata

{JSON.stringify(selectedAlert.metadata, null, 2)}
-
+ )} -
+ )}
diff --git a/web/src/components/ContainerList.css b/web/src/components/ContainerList.css index 1439178..c91951c 100644 --- a/web/src/components/ContainerList.css +++ b/web/src/components/ContainerList.css @@ -1,94 +1,196 @@ .container-list { - border: none; + min-width: 0; + border: 1px solid #dde3eb; border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - height: 100%; + background: #fff; + color: #172b45; } .container-list .card-header { - background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); - color: white; - border-radius: 12px 12px 0 0 !important; - padding: 1rem 1.5rem; + background: #fff; + border-bottom: 1px solid #dde3eb; + border-radius: 12px 12px 0 0; + padding: 20px 24px; } .container-list .card-title { margin: 0; - font-size: 1.25rem; - font-weight: 600; + font-size: 1rem; + font-weight: 650; } -.container-items { - display: flex; - flex-direction: column; - gap: 1rem; - max-height: 600px; - overflow-y: auto; +.container-list .card-body { + padding: 24px; } -.container-item { - border: 1px solid #e9ecef; - border-radius: 8px; - padding: 1rem; - transition: transform 0.2s, box-shadow 0.2s; +.container-list .container-filter { + max-width: 240px; +} + +.container-list .container-items { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); + gap: 20px; } -.container-item:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +.container-list .container-item { + display: flex; + flex-direction: column; + min-width: 0; + border: 1px solid #dde3eb; + border-radius: 8px; + padding: 20px; } -.container-header { +.container-list .container-header { display: flex; justify-content: space-between; - align-items: center; - margin-bottom: 0.75rem; + align-items: start; + gap: 12px; + margin-bottom: 16px; } -.container-header h5 { +.container-list .container-header h5 { margin: 0; - font-size: 1.1rem; - font-weight: 600; - color: #2c3e50; + font-size: 0.9375rem; + font-weight: 650; + overflow-wrap: anywhere; +} + +.container-list .container-details { + margin-bottom: 20px; +} + +.container-list .container-image { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 16px; +} + +.container-list .container-image span, + +.container-list .container-security > div > span:first-child { + display: block; + color: #526379; + font-size: 0.6875rem; + margin-bottom: 4px; +} + +.container-list .container-image code { + color: #33465e; + font-size: 0.75rem; + overflow-wrap: anywhere; } -.container-details { - margin-bottom: 1rem; +.container-list .container-security { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + margin-bottom: 16px; } -.container-details p { - margin: 0.25rem 0; - font-size: 0.9rem; - color: #495057; +.container-list .container-security strong { + font-variant-numeric: tabular-nums; + font-size: 1rem; } -.network-activity { +.container-list .container-security-badges { display: flex; + flex-wrap: wrap; align-items: center; - gap: 0.5rem; - padding: 0.5rem; - background: #f8f9fa; - border-radius: 4px; - font-size: 0.85rem; + gap: 6px; } -.container-actions { +.container-list .network-activity { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + margin: 0 0 12px; + padding: 12px 0; + border-top: 1px solid #e5eaf0; + border-bottom: 1px solid #e5eaf0; +} + +.container-list .network-activity dt { + color: #526379; + font-size: 0.6875rem; + font-weight: 500; +} + +.container-list .network-activity dd { + margin: 4px 0 0; + color: #33465e; + font-size: 0.75rem; + font-variant-numeric: tabular-nums; + overflow-wrap: anywhere; +} + +.container-list .container-actions { display: flex; - gap: 0.5rem; flex-wrap: wrap; + gap: 8px; + margin-top: auto; +} + +.container-list .container-actions .btn { + min-height: 36px; + font-size: 0.75rem; + font-weight: 600; + padding: 6px 12px; +} + +.container-list .badge { + font-size: 0.6875rem; + font-weight: 600; + padding: 5px 8px; + white-space: normal; } -/* Responsive adjustments */ -@media (max-width: 768px) { - .container-items { - max-height: none; +.container-list .badge.bg-warning { + color: #172b45; +} + +.container-list .container-empty { + margin: 0; + color: #526379; +} + +.container-list .btn-outline-primary { + --bs-btn-color: #2457c5; + --bs-btn-bg: #eff5ff; + --bs-btn-border-color: #c9d9fa; + --bs-btn-hover-bg: #2457c5; + --bs-btn-hover-border-color: #2457c5; +} + +.container-list .btn-outline-danger { + --bs-btn-color: #b42318; + --bs-btn-bg: #fff1f2; + --bs-btn-border-color: #f5c1c6; + --bs-btn-hover-bg: #b42318; + --bs-btn-hover-border-color: #b42318; +} + +.container-list .btn-outline-success { + --bs-btn-color: #065f46; + --bs-btn-bg: #ecfdf5; + --bs-btn-border-color: #a7d8c5; + --bs-btn-hover-bg: #065f46; + --bs-btn-hover-border-color: #065f46; +} + +@media (max-width: 575.98px) { + + .container-list .card-body { + padding: 16px; } - .container-actions { - flex-direction: column; + .container-list .container-filter { + max-width: none; } - .container-actions button { - width: 100%; + .container-list .container-item { + padding: 16px; } } diff --git a/web/src/components/ContainerList.tsx b/web/src/components/ContainerList.tsx index 42ff229..60f4d20 100644 --- a/web/src/components/ContainerList.tsx +++ b/web/src/components/ContainerList.tsx @@ -1,8 +1,9 @@ import React, { useEffect, useState } from 'react'; -import { Card, Button, Form, Badge, Modal, Spinner, Alert as BootstrapAlert } from 'react-bootstrap'; +import { Card, Button, Form, Badge, Modal, Spinner } from 'react-bootstrap'; import apiService from '../services/api'; import { Container, ContainerStatus } from '../types/containers'; import './ContainerList.css'; +import './DetailsModal.css'; const ContainerList: React.FC = () => { const [containers, setContainers] = useState([]); @@ -21,7 +22,7 @@ const ContainerList: React.FC = () => { try { setLoading(true); const data = await apiService.getContainers(); - setContainers(filterStatus ? data.filter((c: Container) => c.status === filterStatus) : data); + setContainers(filterStatus ? data.filter((c: Container) => (c.securityStatus.state === 'Quarantined' ? 'Quarantined' : c.status) === filterStatus) : data); } catch (err) { console.error('Error loading containers:', err); } finally { @@ -73,9 +74,9 @@ const ContainerList: React.FC = () => { }; const getRiskColor = (score: number) => { - if (score < 30) return '#27ae60'; - if (score < 60) return '#f39c12'; - return '#e74c3c'; + if (score < 30) return '#047857'; + if (score < 60) return '#92400e'; + return '#b42318'; }; const formatCount = (value: number | null) => (value === null ? 'n/a' : value.toLocaleString()); @@ -96,7 +97,7 @@ const ContainerList: React.FC = () => { Containers - + Filter by status { {loading ? (
- + Loading containers...
) : (
- {containers.map((container) => ( -
- {(() => { - const isQuarantined = - container.status === 'Quarantined' || container.securityStatus.state === 'Quarantined'; - - return ( - <> -
-
{container.name}
- - {isQuarantined ? 'Quarantined' : container.status} - -
-
-

Image: {container.image}

-

- Security:{' '} - - {container.securityStatus.state} + {containers.length === 0 &&

No containers match the selected status.

} + {containers.map((container) => { + const isQuarantined = container.status === 'Quarantined' || container.securityStatus.state === 'Quarantined'; + + return ( +
+
+
{container.name}
+ + {isQuarantined ? 'Quarantined' : container.status} -

-

- Risk Score:{' '} - - {container.riskScore} - -

-
- - ⬇ {formatCount(container.networkActivity.receivedPackets)} pkts | - ⬆ {formatCount(container.networkActivity.transmittedPackets)} pkts | - 🚫 {formatCount(container.networkActivity.blockedConnections)} - - {container.networkActivity.suspiciousActivity && ( - Suspicious - )}
-
-
- - {!isQuarantined && container.status === 'Running' && ( - + {!isQuarantined && container.status === 'Running' && ( + - )} - {isQuarantined && ( - - )} -
- - ); - })()} -
- ))} + }}>Quarantine + )} + {isQuarantined && ( + + )} +
+ + ); + })}
)}
{/* Detail Modal */} - setShowModal(false)}> + setShowModal(false)}> - Container Details + Container Details {selectedContainer && ( -
-

Name: {selectedContainer.name}

-

ID: {selectedContainer.id}

-

Image: {selectedContainer.image}

-

Status: {selectedContainer.status}

-

Security: {selectedContainer.securityStatus.state}

-

Risk Score: {selectedContainer.riskScore}

-

Threats: {selectedContainer.securityStatus.threats}

-

Vulnerabilities: {selectedContainer.securityStatus.vulnerabilities ?? 'Unavailable'}

-

Last Scan: {formatDateTime(selectedContainer.securityStatus.lastScan)}

-

RX Traffic: {formatBytes(selectedContainer.networkActivity.receivedBytes)}

-

TX Traffic: {formatBytes(selectedContainer.networkActivity.transmittedBytes)}

-

RX Packets: {formatCount(selectedContainer.networkActivity.receivedPackets)}

-

TX Packets: {formatCount(selectedContainer.networkActivity.transmittedPackets)}

-

Blocked Connections: {formatCount(selectedContainer.networkActivity.blockedConnections)}

-
+
+
Name
{selectedContainer.name}
+
ID
{selectedContainer.id}
+
Image
{selectedContainer.image}
+
Status
{selectedContainer.securityStatus.state === 'Quarantined' ? 'Quarantined' : selectedContainer.status}
+
Security
{selectedContainer.securityStatus.state}
+
Risk Score
{selectedContainer.riskScore}
+
Threats
{selectedContainer.securityStatus.threats}
+
Vulnerabilities
{selectedContainer.securityStatus.vulnerabilities ?? 'Unavailable'}
+
Created
+
Last Scan
{formatDateTime(selectedContainer.securityStatus.lastScan)}
+
RX Traffic
{formatBytes(selectedContainer.networkActivity.receivedBytes)}
+
TX Traffic
{formatBytes(selectedContainer.networkActivity.transmittedBytes)}
+
RX Packets
{formatCount(selectedContainer.networkActivity.receivedPackets)}
+
TX Packets
{formatCount(selectedContainer.networkActivity.transmittedPackets)}
+
Blocked Connections
{formatCount(selectedContainer.networkActivity.blockedConnections)}
+
)}
@@ -229,13 +202,13 @@ const ContainerList: React.FC = () => {
{/* Quarantine Modal */} - setShowQuarantineModal(false)}> + setShowQuarantineModal(false)}> - Confirm Quarantine + Confirm Quarantine

Are you sure you want to quarantine this container?

- + Reason { } return ( - - + +
-
- +

Overview

Real-time security monitoring for containers and Linux servers @@ -90,7 +89,7 @@ const Dashboard: React.FC = () => { {/* Security Score Card */} - + @@ -99,7 +98,7 @@ const Dashboard: React.FC = () => { Active Threats - {securityStatus?.activeThreats || 0} + {(securityStatus?.activeThreats || 0).toLocaleString()} @@ -109,7 +108,7 @@ const Dashboard: React.FC = () => { Quarantined - {securityStatus?.quarantinedContainers || 0} + {(securityStatus?.quarantinedContainers || 0).toLocaleString()} @@ -119,7 +118,7 @@ const Dashboard: React.FC = () => { New Alerts - {securityStatus?.alertsNew || 0} + {(securityStatus?.alertsNew || 0).toLocaleString()} @@ -127,18 +126,18 @@ const Dashboard: React.FC = () => { {/* Threat Map */} - - + + {/* Alerts and Containers */} - - + + - + diff --git a/web/src/components/DetailsModal.css b/web/src/components/DetailsModal.css new file mode 100644 index 0000000..dd538bc --- /dev/null +++ b/web/src/components/DetailsModal.css @@ -0,0 +1,122 @@ +.security-details-modal { + color: #172b45; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-size: 0.875rem; +} + +.security-details-modal .modal-content { + border: 1px solid #dde3eb; + border-radius: 12px; +} + +.security-details-modal .modal-header, + +.security-details-modal .modal-footer { + padding: 20px 24px; + border-color: #e5eaf0; +} + +.security-details-modal .modal-title { + font-size: 1.125rem; + font-weight: 650; +} + +.security-details-modal .modal-body { + padding: 24px; +} + +.security-details-modal .detail-grid { + display: grid; + grid-template-columns: 160px minmax(0, 1fr); + gap: 16px 24px; + margin: 0; +} + +.security-details-modal .detail-grid dt { + color: #526379; + font-size: 0.75rem; + font-weight: 500; +} + +.security-details-modal .detail-grid dd { + min-width: 0; + margin: 0; + overflow-wrap: anywhere; + font-variant-numeric: tabular-nums; +} + +.security-details-modal .detail-grid code { + color: #33465e; + font-size: 0.8125rem; + word-break: break-all; +} + +.security-details-modal .detail-message { + white-space: pre-wrap; +} + +.security-details-modal .detail-metadata { + margin-top: 24px; +} + +.security-details-modal .detail-metadata h3 { + color: #526379; + font-size: 0.75rem; + margin-bottom: 12px; +} + +.security-details-modal .detail-metadata pre { + padding: 16px; + margin: 0; + border-radius: 6px; + background: #f4f6f9; + color: #33465e; + font-size: 0.75rem; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.security-details-modal .badge.bg-warning, + +.security-details-modal .badge.bg-info { + color: #172b45; +} + +.security-details-modal .btn { + min-height: 40px; + font-size: 0.8125rem; +} + +.security-details-modal .form-label { + color: #526379; + font-size: 0.75rem; + font-weight: 600; +} + +.security-details-modal .form-control { + border-color: #c4cedb; + font-size: 0.875rem; +} + +.security-details-modal :focus-visible { + outline: 3px solid #2457c5; + outline-offset: 3px; +} + +@media (max-width: 575.98px) { + .security-details-modal .modal-body, + .security-details-modal .modal-header, + + .security-details-modal .modal-footer { + padding: 16px; + } + + .security-details-modal .detail-grid { + grid-template-columns: minmax(0, 1fr); + gap: 4px; + } + + .security-details-modal .detail-grid dd { + margin-bottom: 16px; + } +} diff --git a/web/src/components/SecurityScore.css b/web/src/components/SecurityScore.css index 1dcc1b8..c28c96c 100644 --- a/web/src/components/SecurityScore.css +++ b/web/src/components/SecurityScore.css @@ -1,85 +1,77 @@ .security-score-card { - border: none; + border: 1px solid #dde3eb; border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background: #fff; height: 100%; + color: #172b45; +} + +.security-score-card .card-body { + padding: 24px; } .security-score-card .card-title { - font-size: 0.875rem; - color: #7f8c8d; - margin-bottom: 1rem; + font-size: 0.8125rem; + font-weight: 500; + color: #526379; + margin-bottom: 16px; } -.gauge-container { +.security-score-card .gauge-container { display: flex; justify-content: center; - align-items: center; - padding: 1rem 0; + padding: 4px 0; } -.gauge { +.security-score-card .gauge { position: relative; width: 150px; height: 75px; overflow: hidden; } -.gauge-background { +.security-score-card .gauge-background { position: absolute; width: 150px; height: 150px; border-radius: 50%; - background: conic-gradient( - from -90deg, - #e74c3c 0deg, - #e67e22 60deg, - #f39c12 120deg, - #27ae60 180deg, - #ecf0f1 180deg - ); - transform: rotate(-90deg); + background: conic-gradient(from 270deg, #b42318 0deg, #c2410c 60deg, #b7790b 120deg, #047857 180deg, transparent 180deg); } -.gauge-fill { +.security-score-card .gauge-fill { position: absolute; - width: 150px; - height: 150px; - border-radius: 50%; - background: conic-gradient( - from -90deg, - #27ae60 0deg, - #27ae60 var(--rotation, 0deg), - transparent var(--rotation, 0deg) - ); - transform-origin: center; - transition: transform 0.5s ease-out; + left: 73px; + bottom: 75px; + width: 4px; + height: 74px; + border-radius: 2px; + background: #172b45; + transform-origin: 50% 100%; } -.gauge-cover { +.security-score-card .gauge-cover { position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 120px; - height: 120px; + top: 18px; + left: 18px; + width: 114px; + height: 114px; border-radius: 50%; - background: white; - display: flex; - justify-content: center; - align-items: center; + background: #fff; + padding-top: 14px; + text-align: center; } -.gauge-value { - font-size: 2.5rem; - font-weight: 700; - color: #2c3e50; +.security-score-card .gauge-value { + font-size: 2.25rem; + line-height: 1.2; + font-weight: 650; + font-variant-numeric: tabular-nums; + color: #172b45; } -.score-label { +.security-score-card .score-label { text-align: center; - font-size: 1.25rem; - font-weight: 600; - margin-top: 0.5rem; - transition: color 0.3s; + font-size: 0.8125rem; + font-weight: 650; + margin-top: 8px; } diff --git a/web/src/components/SecurityScore.tsx b/web/src/components/SecurityScore.tsx index eb7d029..5bfc4f6 100644 --- a/web/src/components/SecurityScore.tsx +++ b/web/src/components/SecurityScore.tsx @@ -8,10 +8,10 @@ interface SecurityScoreProps { const SecurityScore: React.FC = ({ score }) => { const getScoreColor = (score: number): string => { - if (score >= 80) return '#27ae60'; // Green - if (score >= 60) return '#f39c12'; // Orange - if (score >= 40) return '#e67e22'; // Dark Orange - return '#e74c3c'; // Red + if (score >= 80) return '#047857'; // Green + if (score >= 60) return '#92400e'; // Orange + if (score >= 40) return '#c2410c'; // Dark Orange + return '#b42318'; // Red }; const getScoreLabel = (score: number): string => { @@ -28,7 +28,7 @@ const SecurityScore: React.FC = ({ score }) => { Security Score

-
+
diff --git a/web/src/components/Sidebar.css b/web/src/components/Sidebar.css index 18e6382..46ba8f1 100644 --- a/web/src/components/Sidebar.css +++ b/web/src/components/Sidebar.css @@ -1,51 +1,160 @@ .sidebar { + display: flex; + flex-direction: column; + flex: 0 0 220px; width: 220px; - min-height: 100vh; - background: #1f2937; - color: #f9fafb; - padding: 20px 16px; + height: 100vh; position: sticky; top: 0; + align-self: flex-start; + overflow-y: auto; + background: #182536; + color: #f8fafc; + padding: 28px 16px; } -.sidebar-brand { +.sidebar .sidebar-brand { display: flex; align-items: center; - gap: 10px; - font-size: 1.1rem; + gap: 12px; + margin: 0 8px 36px; + font-size: 1.25rem; font-weight: 700; - margin-bottom: 20px; + letter-spacing: -0.025em; } -.sidebar-logo { - width: 39px; - height: 39px; +.sidebar .sidebar-logo { + width: 36px; + height: 36px; object-fit: contain; flex-shrink: 0; /* The artwork is a black silhouette; the sidebar is dark, so draw it white. */ filter: brightness(0) invert(1); } -.sidebar-nav { +.sidebar .sidebar-nav { display: flex; flex-direction: column; - gap: 10px; + gap: 6px; } -.sidebar-nav a { - color: #d1d5db; +.sidebar .sidebar-nav a { + display: flex; + align-items: center; + gap: 12px; + min-height: 44px; + padding: 10px 12px; + border-radius: 6px; + color: #becbdc; text-decoration: none; - padding: 8px 10px; + font-size: 0.875rem; + font-weight: 500; +} + +.sidebar .sidebar-nav svg { + flex-shrink: 0; +} + +.sidebar .sidebar-nav a:hover { + background: #24364d; + color: #fff; +} + +.sidebar .sidebar-nav a[aria-current='location'] { + background: #2b4566; + color: #fff; +} + +.sidebar .sidebar-nav a:focus-visible { + outline: 2px solid #a8caff; + outline-offset: 2px; +} + +.sidebar .sidebar-toggle { + display: flex; + align-items: center; + gap: 12px; + min-height: 44px; + margin-top: auto; + padding: 10px 12px; + border: 1px solid #45566d; border-radius: 6px; + background: transparent; + color: #becbdc; + font-size: 0.8125rem; + text-align: left; +} + +.sidebar .sidebar-toggle svg { + flex-shrink: 0; } -.sidebar-nav a:hover { - background: #374151; +.sidebar .sidebar-toggle:hover { + background: #24364d; color: #fff; } -@media (max-width: 992px) { +.sidebar .sidebar-toggle:focus-visible { + outline: 2px solid #a8caff; + outline-offset: 2px; +} + +@media (min-width: 992px) { + .sidebar.sidebar-collapsed { + flex-basis: 68px; + width: 68px; + padding: 28px 10px; + } + + .sidebar.sidebar-collapsed .sidebar-brand { + justify-content: center; + margin-inline: 0; + } + + .sidebar.sidebar-collapsed .sidebar-brand-title, + .sidebar.sidebar-collapsed .sidebar-link-label { + display: none; + } + + .sidebar.sidebar-collapsed .sidebar-nav a, + .sidebar.sidebar-collapsed .sidebar-toggle { + justify-content: center; + gap: 0; + padding-inline: 8px; + } +} + +@media (max-width: 991.98px) { .sidebar { + position: static; + flex: none; + width: 100%; + height: auto; + padding: 16px 24px; + } + + .sidebar .sidebar-brand { + margin: 0 0 16px; + } + + .sidebar .sidebar-nav { + flex-direction: row; + flex-wrap: wrap; + gap: 4px; + } + + .sidebar .sidebar-toggle { display: none; } } + +@media (max-width: 575.98px) { + .sidebar { + padding: 16px 12px; + } + + .sidebar .sidebar-nav { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} diff --git a/web/src/components/Sidebar.tsx b/web/src/components/Sidebar.tsx index e81a03d..bdddd69 100644 --- a/web/src/components/Sidebar.tsx +++ b/web/src/components/Sidebar.tsx @@ -1,11 +1,71 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import logoUrl from '../assets/logo.png'; import './Sidebar.css'; +const links = [ + { id: 'overview', label: 'Overview', path: 'M3 3h7v7H3z M14 3h7v7h-7z M3 14h7v7H3z M14 14h7v7h-7z' }, + { id: 'threat-map', label: 'Threat Map', path: 'M12 3 3 7v5c0 5 9 9 9 9s9-4 9-9V7l-9-4Z M7 12h3l2-4 2 8 2-4h2' }, + { id: 'alerts', label: 'Alerts', path: 'M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9 M10 21h4' }, + { id: 'containers', label: 'Containers', path: 'm12 3 9 5v9l-9 5-9-5V8l9-5Z M3 8l9 5 9-5 M12 13v9 M7.5 5.5l9 5' }, +]; + +const sectionFromHash = () => { + const hash = window.location.hash.split('/')[0] || '#overview'; + return hash === '#threats' ? '#threat-map' : hash; +}; + const Sidebar: React.FC = () => { + const [activeSection, setActiveSection] = useState(sectionFromHash); + const [collapsed, setCollapsed] = useState(false); + + useEffect(() => { + const updateFromScroll = () => { + const sections = links.map(({ id }) => document.getElementById(id)) + .filter((section): section is HTMLElement => section !== null); + if (!sections.length) return; + + const atBottom = window.scrollY > 0 && + Math.ceil(window.scrollY + window.innerHeight) >= document.documentElement.scrollHeight; + const section = atBottom + ? sections[sections.length - 1] + : [...sections].reverse().find(item => item.getBoundingClientRect().top <= 96) || sections[0]; + setActiveSection(`#${section.id}`); + }; + const updateFromHash = () => setActiveSection(sectionFromHash()); + // Dashboard sections arrive after the API response; their heights also + // change when alerts and containers finish loading or filters are applied. + const observer = new ResizeObserver(updateFromScroll); + observer.observe(document.body); + window.addEventListener('scroll', updateFromScroll, { passive: true }); + window.addEventListener('resize', updateFromScroll); + window.addEventListener('hashchange', updateFromHash); + updateFromScroll(); + + return () => { + observer.disconnect(); + window.removeEventListener('scroll', updateFromScroll); + window.removeEventListener('resize', updateFromScroll); + window.removeEventListener('hashchange', updateFromHash); + }; + }, []); + + const navigateToSection = (event: React.MouseEvent, id: string) => { + if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; + const section = document.getElementById(id); + if (!section) return; + event.preventDefault(); + const hash = `#${id}`; + if (window.location.hash !== hash) window.history.pushState(null, '', hash); + setActiveSection(hash); + section.scrollIntoView({ + behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth', + block: 'start', + }); + }; + return ( -