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...
) : (
<>
-
+
+ Recent security alerts
+
+
+
+
+
+
+
+
+
|
-
+ Select
|
Severity |
Type |
@@ -323,56 +344,78 @@ const AlertPanel: React.FC = () => {
|
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
- )}
-
-
-
- ))}
+ }}>Quarantine
+ )}
+ {isQuarantined && (
+ handleRelease(container.id)}>Release
+ )}
+
+
+ );
+ })}
)}
{/* 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 (
-
-
+
+
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 (
-