+
e.stopPropagation()}>
+
+
+
{process.name}
+
PID: {process.pid}
+
+
+
+
+
+
+
Overview
+
+
+
+ {process.pid}
+
+
+
+ {process.ppid}
+
+
+
+ {process.user}
+
+
+
+ {process.parent_name}
+
+
+
+ {process.exe || 'N/A'}
+
+
+
+ {process.cwd || 'N/A'}
+
+
+
+ {process.sha256 || 'N/A'}
+
+
+
+
+
+
Threat Scores
+
+
+
+
+ {process.total_score.toFixed(2)}
+
+
+
+
+ {process.heuristic_score.toFixed(2)}
+
+
+
+ {process.ml_score.toFixed(2)}
+
+
+
+
+
+
Resource Usage
+
+
+
+
+ {process.cpu_percent.toFixed(1)}%
+
+
+
80 ? '#ef4444' : process.cpu_percent > 50 ? '#eab308' : '#22c55e'
+ }}
+ />
+
+
+
+
+
+ {process.conns_outbound ?? 0}
+
+
+
+
+
+
+
+
+
+
Detection Reasons
+
+ {process.reasons.length > 0 ? (
+ process.reasons.map((reason, index) => (
+
+ = 3 ? 'rgba(239, 68, 68, 0.2)' :
+ reason.score >= 2 ? 'rgba(234, 179, 8, 0.2)' :
+ 'rgba(34, 197, 94, 0.2)',
+ color: reason.score >= 3 ? '#ef4444' :
+ reason.score >= 2 ? '#eab308' :
+ '#22c55e'
+ }}>
+ {reason.score.toFixed(1)}
+
+ {reason.reason}
+
+ ))
+ ) : (
+
No specific reasons detected
+ )}
+
+
+
+
+
Command Line
+
+
+ {Array.isArray(process.cmdline)
+ ? process.cmdline.join(' ')
+ : (process.cmdline || 'N/A')}
+
+
+
+
+
+
+ )
+}
+
+export default ProcessDetails
diff --git a/frontend/src/components/ProcessTable.css b/frontend/src/components/ProcessTable.css
new file mode 100644
index 0000000..5ff824d
--- /dev/null
+++ b/frontend/src/components/ProcessTable.css
@@ -0,0 +1,165 @@
+.process-table-container {
+ background: rgba(30, 41, 59, 0.6);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(148, 163, 184, 0.1);
+ border-radius: 12px;
+ overflow: hidden;
+}
+
+.table-header {
+ padding: 20px 24px;
+ border-bottom: 1px solid rgba(148, 163, 184, 0.1);
+}
+
+.table-header h2 {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: #f1f5f9;
+}
+
+.table-wrapper {
+ overflow-x: auto;
+}
+
+.process-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.process-table thead {
+ background: rgba(15, 23, 42, 0.4);
+}
+
+.process-table th {
+ padding: 16px;
+ text-align: left;
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: #cbd5e1;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ cursor: pointer;
+ user-select: none;
+ white-space: nowrap;
+}
+
+.process-table th:hover {
+ background: rgba(15, 23, 42, 0.6);
+}
+
+.process-table tbody tr {
+ border-bottom: 1px solid rgba(148, 163, 184, 0.05);
+ transition: all 0.2s;
+}
+
+.process-table tbody tr.clickable {
+ cursor: pointer;
+}
+
+.process-table tbody tr:hover {
+ background: rgba(59, 130, 246, 0.1);
+}
+
+.process-table td {
+ padding: 16px;
+ color: #e2e8f0;
+ font-size: 0.9rem;
+}
+
+.mono {
+ font-family: 'Courier New', monospace;
+ color: #94a3b8;
+}
+
+.process-name {
+ font-weight: 600;
+ color: #60a5fa;
+}
+
+.parent-name {
+ color: #94a3b8;
+ font-size: 0.85rem;
+}
+
+.score-badge {
+ display: inline-block;
+ padding: 4px 12px;
+ border-radius: 12px;
+ font-weight: 700;
+ font-size: 0.9rem;
+}
+
+.score-critical {
+ background-color: rgba(239, 68, 68, 0.2);
+ color: #ef4444;
+ border: 1px solid rgba(239, 68, 68, 0.4);
+}
+
+.score-warning {
+ background-color: rgba(234, 179, 8, 0.2);
+ color: #eab308;
+ border: 1px solid rgba(234, 179, 8, 0.4);
+}
+
+.score-normal {
+ background-color: rgba(34, 197, 94, 0.2);
+ color: #22c55e;
+ border: 1px solid rgba(34, 197, 94, 0.4);
+}
+
+.reasons-cell {
+ max-width: 400px;
+}
+
+.reasons-preview {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: #cbd5e1;
+ font-size: 0.85rem;
+}
+
+.more-badge {
+ background: rgba(59, 130, 246, 0.2);
+ color: #60a5fa;
+ padding: 2px 8px;
+ border-radius: 8px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ white-space: nowrap;
+}
+
+.loading-state, .empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 64px 24px;
+ color: #94a3b8;
+}
+
+.spinner {
+ width: 48px;
+ height: 48px;
+ border: 4px solid rgba(59, 130, 246, 0.2);
+ border-top-color: #3b82f6;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin-bottom: 16px;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+.empty-state h3 {
+ margin: 16px 0 8px 0;
+ color: #22c55e;
+ font-size: 1.5rem;
+}
+
+.empty-state p {
+ margin: 0;
+ font-size: 1rem;
+}
diff --git a/frontend/src/components/ProcessTable.tsx b/frontend/src/components/ProcessTable.tsx
new file mode 100644
index 0000000..30cc008
--- /dev/null
+++ b/frontend/src/components/ProcessTable.tsx
@@ -0,0 +1,165 @@
+import React, { useState } from 'react'
+import type { ProcessData } from './Dashboard'
+import ProcessDetails from './ProcessDetails'
+import './ProcessTable.css'
+
+interface ProcessTableProps {
+ processes: ProcessData[]
+ loading: boolean
+}
+
+const ProcessTable: React.FC
= ({ processes, loading }) => {
+ const [selectedProcess, setSelectedProcess] = useState(null)
+ const [sortField, setSortField] = useState('total_score')
+ const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc')
+
+ const handleSort = (field: keyof ProcessData) => {
+ if (sortField === field) {
+ setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
+ } else {
+ setSortField(field)
+ setSortOrder('desc')
+ }
+ }
+
+ const sortedProcesses = [...processes].sort((a, b) => {
+ const aVal = a[sortField]
+ const bVal = b[sortField]
+
+ if (typeof aVal === 'number' && typeof bVal === 'number') {
+ return sortOrder === 'asc' ? aVal - bVal : bVal - aVal
+ }
+
+ if (typeof aVal === 'string' && typeof bVal === 'string') {
+ return sortOrder === 'asc'
+ ? aVal.localeCompare(bVal)
+ : bVal.localeCompare(aVal)
+ }
+
+ return 0
+ })
+
+ const getStatusClass = (status: string) => {
+ switch (status) {
+ case 'critical': return 'badge-critical'
+ case 'warning': return 'badge-warning'
+ default: return 'badge-normal'
+ }
+ }
+
+ const getScoreClass = (score: number) => {
+ if (score >= 8) return 'score-critical'
+ if (score >= 5) return 'score-warning'
+ return 'score-normal'
+ }
+
+ if (loading && processes.length === 0) {
+ return (
+
+
+
+
Loading processes...
+
+
+ )
+ }
+
+ if (processes.length === 0) {
+ return (
+
+
+
+
All Clear!
+
No suspicious processes detected
+
+
+ )
+ }
+
+ return (
+ <>
+
+
+
Suspicious Processes ({processes.length})
+
+
+
+
+
+
+ | handleSort('total_score')}>
+ Score {sortField === 'total_score' && (sortOrder === 'asc' ? 'β' : 'β')}
+ |
+ handleSort('pid')}>
+ PID {sortField === 'pid' && (sortOrder === 'asc' ? 'β' : 'β')}
+ |
+ handleSort('name')}>
+ Name {sortField === 'name' && (sortOrder === 'asc' ? 'β' : 'β')}
+ |
+ handleSort('user')}>
+ User {sortField === 'user' && (sortOrder === 'asc' ? 'β' : 'β')}
+ |
+ Parent |
+ handleSort('cpu_percent')}>
+ CPU% {sortField === 'cpu_percent' && (sortOrder === 'asc' ? 'β' : 'β')}
+ |
+ handleSort('conns_outbound')}>
+ Connections {sortField === 'conns_outbound' && (sortOrder === 'asc' ? 'β' : 'β')}
+ |
+ Status |
+ Reasons |
+
+
+
+ {sortedProcesses.map((proc) => (
+ setSelectedProcess(proc)}
+ className="clickable"
+ >
+ |
+
+ {proc.total_score.toFixed(1)}
+
+ |
+ {proc.pid} |
+ {proc.name} |
+ {proc.user} |
+ {proc.parent_name} |
+ {proc.cpu_percent.toFixed(1)}% |
+ {proc.conns_outbound ?? 0} |
+
+
+ {proc.status.toUpperCase()}
+
+ |
+
+ {proc.reasons.length > 0 && (
+
+ {proc.reasons[0].reason}
+ {proc.reasons.length > 1 && (
+ +{proc.reasons.length - 1}
+ )}
+
+ )}
+ |
+
+ ))}
+
+
+
+
+
+ {selectedProcess && (
+ setSelectedProcess(null)}
+ />
+ )}
+ >
+ )
+}
+
+export default ProcessTable
diff --git a/frontend/src/components/StatsCards.css b/frontend/src/components/StatsCards.css
new file mode 100644
index 0000000..ede282c
--- /dev/null
+++ b/frontend/src/components/StatsCards.css
@@ -0,0 +1,94 @@
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 20px;
+ margin-bottom: 32px;
+}
+
+.stat-card {
+ background: rgba(30, 41, 59, 0.6);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(148, 163, 184, 0.1);
+ border-radius: 12px;
+ padding: 24px;
+ transition: all 0.3s;
+ position: relative;
+ overflow: hidden;
+}
+
+.stat-card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3);
+}
+
+.stat-card.loading {
+ animation: pulse 1.5s ease-in-out infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+}
+
+.stat-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 16px;
+}
+
+.stat-icon {
+ font-size: 1.5rem;
+}
+
+.stat-title {
+ color: #cbd5e1;
+ font-size: 0.95rem;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.stat-value {
+ font-size: 2.5rem;
+ font-weight: 700;
+ line-height: 1;
+ margin-bottom: 8px;
+}
+
+.stat-alert {
+ color: #ef4444;
+ font-size: 0.85rem;
+ font-weight: 500;
+ margin-top: 8px;
+ animation: blink 2s ease-in-out infinite;
+}
+
+@keyframes blink {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+}
+
+.skeleton-text {
+ height: 16px;
+ background: linear-gradient(90deg, #334155 25%, #475569 50%, #334155 75%);
+ background-size: 200% 100%;
+ animation: loading 1.5s infinite;
+ border-radius: 4px;
+ margin-bottom: 16px;
+ width: 60%;
+}
+
+.skeleton-number {
+ height: 40px;
+ background: linear-gradient(90deg, #334155 25%, #475569 50%, #334155 75%);
+ background-size: 200% 100%;
+ animation: loading 1.5s infinite;
+ border-radius: 8px;
+ width: 80%;
+}
+
+@keyframes loading {
+ 0% { background-position: 200% 0; }
+ 100% { background-position: -200% 0; }
+}
diff --git a/frontend/src/components/StatsCards.tsx b/frontend/src/components/StatsCards.tsx
new file mode 100644
index 0000000..ab09bc7
--- /dev/null
+++ b/frontend/src/components/StatsCards.tsx
@@ -0,0 +1,83 @@
+import React from 'react'
+import type { StatsData } from './Dashboard'
+import './StatsCards.css'
+
+interface StatsCardsProps {
+ stats: StatsData | null
+}
+
+const StatsCards: React.FC = ({ stats }) => {
+ if (!stats) {
+ return (
+
+ {[...Array(4)].map((_, i) => (
+
+ ))}
+
+ )
+ }
+
+ const cards = [
+ {
+ title: 'Total Processes',
+ value: stats.total_processes,
+ icon: 'π',
+ color: '#3b82f6',
+ bgColor: 'rgba(59, 130, 246, 0.1)'
+ },
+ {
+ title: 'Normal',
+ value: stats.normal,
+ icon: 'β
',
+ color: '#22c55e',
+ bgColor: 'rgba(34, 197, 94, 0.1)'
+ },
+ {
+ title: 'Warnings',
+ value: stats.warning,
+ icon: 'β οΈ',
+ color: '#eab308',
+ bgColor: 'rgba(234, 179, 8, 0.1)'
+ },
+ {
+ title: 'Critical',
+ value: stats.critical,
+ icon: 'π¨',
+ color: '#ef4444',
+ bgColor: 'rgba(239, 68, 68, 0.1)'
+ }
+ ]
+
+ return (
+
+ {cards.map((card, index) => (
+
+
+ {card.icon}
+ {card.title}
+
+
+ {card.value.toLocaleString()}
+
+ {card.title === 'Critical' && card.value > 0 && (
+
+ Requires attention
+
+ )}
+
+ ))}
+
+ )
+}
+
+export default StatsCards
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..3707448
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,59 @@
+:root {
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #0f172a;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ min-height: 100vh;
+}
+
+#root {
+ min-height: 100vh;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ font-size: 1em;
+ font-family: inherit;
+ cursor: pointer;
+ transition: all 0.25s;
+}
+
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+input {
+ font-family: inherit;
+}
+
+code {
+ font-family: 'Courier New', Courier, monospace;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+::selection {
+ background-color: rgba(59, 130, 246, 0.3);
+ color: #fff;
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 0000000..bef5202
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json
new file mode 100644
index 0000000..a9b5a59
--- /dev/null
+++ b/frontend/tsconfig.app.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json
new file mode 100644
index 0000000..8a67f62
--- /dev/null
+++ b/frontend/tsconfig.node.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..8b0f57b
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+})
diff --git a/procwatch/cli.py b/procwatch/cli.py
index daa02b2..d5ba546 100644
--- a/procwatch/cli.py
+++ b/procwatch/cli.py
@@ -194,10 +194,6 @@ def scan_once():
scan_once()
-def cmd_api(args: argparse.Namespace) -> None:
- from .api import run_api_server
- run_api_server(args.host, args.port, args.config, args.model)
-
def build_parser() -> argparse.ArgumentParser:
ap = argparse.ArgumentParser(description="procwatch v3 - modular + ML")
sub = ap.add_subparsers(dest="cmd", required=True)
@@ -219,13 +215,6 @@ def build_parser() -> argparse.ArgumentParser:
p_scan.add_argument("--dump", type=str, help="Dump artifacts directory")
p_scan.set_defaults(func=cmd_scan)
- p_api = sub.add_parser("api", help="Run REST API server")
- p_api.add_argument("--host", type=str, default="0.0.0.0", help="Host to bind to")
- p_api.add_argument("--port", type=int, default=8080, help="Port to bind to")
- p_api.add_argument("--config", type=str, help="Config YAML")
- p_api.add_argument("--model", type=str, help="Path to load model")
- p_api.set_defaults(func=cmd_api)
-
return ap
def main(argv: list[str] | None = None) -> None:
diff --git a/start_webui.sh b/start_webui.sh
new file mode 100644
index 0000000..9ac3239
--- /dev/null
+++ b/start_webui.sh
@@ -0,0 +1,72 @@
+#!/bin/bash
+
+echo "π Starting ProcSentinel Web UI..."
+echo ""
+
+# Check if dependencies are installed
+echo "π¦ Checking dependencies..."
+
+if ! command -v python3 &> /dev/null; then
+ echo "β Python3 not found. Please install Python 3."
+ exit 1
+fi
+
+if ! command -v npm &> /dev/null; then
+ echo "β npm not found. Please install Node.js and npm."
+ exit 1
+fi
+
+# Check Python dependencies
+echo "π Checking Python dependencies..."
+python3 -c "import flask" 2>/dev/null || {
+ echo "β οΈ Flask not found. Installing..."
+ pip3 install flask flask-cors pyjwt
+}
+
+# Check frontend dependencies
+if [ ! -d "frontend/node_modules" ]; then
+ echo "π¦ Installing frontend dependencies..."
+ cd frontend && npm install && cd ..
+fi
+
+echo ""
+echo "β
All dependencies ready!"
+echo ""
+echo "π§ Starting services..."
+echo ""
+
+# Start backend in background
+echo "Starting backend API on http://localhost:5000..."
+python3 api_server.py &
+BACKEND_PID=$!
+
+# Wait for backend to start
+sleep 3
+
+# Start frontend in background
+echo "Starting frontend on http://localhost:5173..."
+cd frontend
+npm run dev &
+FRONTEND_PID=$!
+cd ..
+
+echo ""
+echo "ββββββββββββββββββββββββββββββββββββββββ"
+echo "β¨ ProcSentinel Web UI is ready!"
+echo "ββββββββββββββββββββββββββββββββββββββββ"
+echo ""
+echo "π Frontend: http://localhost:5173"
+echo "π Backend: http://localhost:5000"
+echo ""
+echo "π€ Login with:"
+echo " Username: admin"
+echo " Password: admin123"
+echo ""
+echo "Press Ctrl+C to stop all services"
+echo "ββββββββββββββββββββββββββββββββββββββββ"
+echo ""
+
+# Wait for Ctrl+C
+trap "echo ''; echo 'π Stopping services...'; kill $BACKEND_PID $FRONTEND_PID 2>/dev/null; echo 'β
Stopped'; exit 0" SIGINT SIGTERM
+
+wait