initial commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
vendor/
|
||||
.env
|
||||
mysql/
|
||||
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../config/database.php';
|
||||
|
||||
$db = getDbConnection();
|
||||
$requestUri = $_SERVER['REQUEST_URI'];
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
$path = parse_url($requestUri, PHP_URL_PATH);
|
||||
$path = str_replace('/api/', '', $path);
|
||||
$segments = explode('/', trim($path, '/'));
|
||||
|
||||
$resource = $segments[0] ?? '';
|
||||
$id = $segments[1] ?? null;
|
||||
|
||||
try {
|
||||
switch ($resource) {
|
||||
case 'teams':
|
||||
handleTeams($method, $id, $db);
|
||||
break;
|
||||
case 'events':
|
||||
handleEvents($method, $id, $db);
|
||||
break;
|
||||
case 'comments':
|
||||
handleComments($method, $id, $db);
|
||||
break;
|
||||
case 'nodes':
|
||||
handleNodes($method, $id, $db);
|
||||
break;
|
||||
case 'links':
|
||||
handleLinks($method, $id, $db);
|
||||
break;
|
||||
default:
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Not found']);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
function handleTeams($method, $id, $db) {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
if ($id) {
|
||||
$stmt = $db->prepare("SELECT * FROM teams WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
echo json_encode($stmt->fetch(PDO::FETCH_ASSOC));
|
||||
} else {
|
||||
echo json_encode($db->query("SELECT * FROM teams ORDER BY name")->fetchAll(PDO::FETCH_ASSOC));
|
||||
}
|
||||
break;
|
||||
case 'POST':
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$stmt = $db->prepare("INSERT INTO teams (name, color) VALUES (?, ?)");
|
||||
$stmt->execute([$data['name'], $data['color'] ?? '#0d6efd']);
|
||||
echo json_encode(['id' => $db->lastInsertId()]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEvents($method, $id, $db) {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
if ($id) {
|
||||
$stmt = $db->prepare("
|
||||
SELECT e.*, t.name AS team_name, t.color AS team_color
|
||||
FROM events e JOIN teams t ON e.team_id = t.id
|
||||
WHERE e.id = ?
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$event = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($event) {
|
||||
$cstmt = $db->prepare("SELECT * FROM comments WHERE event_id = ? ORDER BY created_at ASC");
|
||||
$cstmt->execute([$id]);
|
||||
$event['comments'] = $cstmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
echo json_encode($event);
|
||||
} else {
|
||||
$teamFilter = $_GET['team_id'] ?? null;
|
||||
$sql = "
|
||||
SELECT e.*, t.name AS team_name, t.color AS team_color
|
||||
FROM events e JOIN teams t ON e.team_id = t.id
|
||||
";
|
||||
$params = [];
|
||||
if ($teamFilter) {
|
||||
$sql .= " WHERE e.team_id = ?";
|
||||
$params[] = $teamFilter;
|
||||
}
|
||||
$sql .= " ORDER BY e.occurred_at DESC";
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
}
|
||||
break;
|
||||
case 'POST':
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO events (team_id, title, description, severity, event_type, occurred_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([
|
||||
$data['team_id'],
|
||||
$data['title'],
|
||||
$data['description'] ?? '',
|
||||
$data['severity'] ?? 'info',
|
||||
$data['event_type'] ?? 'general',
|
||||
$data['occurred_at'] ?? date('Y-m-d H:i:s')
|
||||
]);
|
||||
echo json_encode(['id' => $db->lastInsertId()]);
|
||||
break;
|
||||
case 'DELETE':
|
||||
if ($id) {
|
||||
$stmt = $db->prepare("DELETE FROM events WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
echo json_encode(['deleted' => true]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleComments($method, $id, $db) {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$eventId = $_GET['event_id'] ?? null;
|
||||
if ($eventId) {
|
||||
$stmt = $db->prepare("SELECT * FROM comments WHERE event_id = ? ORDER BY created_at ASC");
|
||||
$stmt->execute([$eventId]);
|
||||
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
}
|
||||
break;
|
||||
case 'POST':
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$stmt = $db->prepare("INSERT INTO comments (event_id, author, body) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$data['event_id'], $data['author'], $data['body']]);
|
||||
echo json_encode(['id' => $db->lastInsertId()]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleNodes($method, $id, $db) {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
echo json_encode($db->query("SELECT * FROM network_nodes ORDER BY group_name, label")->fetchAll(PDO::FETCH_ASSOC));
|
||||
break;
|
||||
case 'POST':
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO network_nodes (label, ip_address, node_type, status, group_name, pos_x, pos_y)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([
|
||||
$data['label'],
|
||||
$data['ip_address'] ?? '',
|
||||
$data['node_type'] ?? 'host',
|
||||
$data['status'] ?? 'unknown',
|
||||
$data['group_name'] ?? 'default',
|
||||
$data['pos_x'] ?? 0,
|
||||
$data['pos_y'] ?? 0
|
||||
]);
|
||||
echo json_encode(['id' => $db->lastInsertId()]);
|
||||
break;
|
||||
case 'PUT':
|
||||
if ($id) {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$fields = [];
|
||||
$params = [];
|
||||
foreach (['label','ip_address','node_type','status','group_name','pos_x','pos_y'] as $f) {
|
||||
if (isset($data[$f])) {
|
||||
$fields[] = "$f = ?";
|
||||
$params[] = $data[$f];
|
||||
}
|
||||
}
|
||||
if ($fields) {
|
||||
$params[] = $id;
|
||||
$stmt = $db->prepare("UPDATE network_nodes SET " . implode(', ', $fields) . " WHERE id = ?");
|
||||
$stmt->execute($params);
|
||||
}
|
||||
echo json_encode(['updated' => true]);
|
||||
}
|
||||
break;
|
||||
case 'DELETE':
|
||||
if ($id) {
|
||||
$db->prepare("DELETE FROM network_nodes WHERE id = ?")->execute([$id]);
|
||||
echo json_encode(['deleted' => true]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleLinks($method, $id, $db) {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
echo json_encode($db->query("
|
||||
SELECT l.*, s.label AS source_label, t.label AS target_label
|
||||
FROM network_links l
|
||||
JOIN network_nodes s ON l.source_id = s.id
|
||||
JOIN network_nodes t ON l.target_id = t.id
|
||||
")->fetchAll(PDO::FETCH_ASSOC));
|
||||
break;
|
||||
case 'POST':
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO network_links (source_id, target_id, link_type, label)
|
||||
VALUES (?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([
|
||||
$data['source_id'],
|
||||
$data['target_id'],
|
||||
$data['link_type'] ?? 'direct',
|
||||
$data['label'] ?? ''
|
||||
]);
|
||||
echo json_encode(['id' => $db->lastInsertId()]);
|
||||
break;
|
||||
case 'DELETE':
|
||||
if ($id) {
|
||||
$db->prepare("DELETE FROM network_links WHERE id = ?")->execute([$id]);
|
||||
echo json_encode(['deleted' => true]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
function getDbConnection() {
|
||||
static $db = null;
|
||||
if ($db === null) {
|
||||
$host = getenv('DB_HOST') ?: 'mysql';
|
||||
$dbname = getenv('DB_NAME') ?: 'neptune';
|
||||
$user = getenv('DB_USER') ?: 'neptune';
|
||||
$pass = getenv('DB_PASS') ?: 'neptune_pass';
|
||||
$db = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $user, $pass, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
|
||||
]);
|
||||
}
|
||||
return $db;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- ./frontend:/var/www/html
|
||||
- ./docker/nginx.conf:/etc/nginx/conf.d/default.conf
|
||||
depends_on:
|
||||
- php
|
||||
networks:
|
||||
- neptune
|
||||
|
||||
php:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile.php
|
||||
volumes:
|
||||
- ./backend:/var/www/backend
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- neptune
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: neptune_root_pass
|
||||
MYSQL_DATABASE: neptune
|
||||
MYSQL_USER: neptune
|
||||
MYSQL_PASSWORD: neptune_pass
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
- ./docker/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
networks:
|
||||
- neptune
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
|
||||
networks:
|
||||
neptune:
|
||||
@@ -0,0 +1,5 @@
|
||||
FROM php:8.2-fpm
|
||||
|
||||
RUN docker-php-ext-install pdo pdo_mysql
|
||||
|
||||
WORKDIR /var/www/backend
|
||||
@@ -0,0 +1,56 @@
|
||||
CREATE TABLE IF NOT EXISTS teams (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL UNIQUE,
|
||||
color VARCHAR(7) DEFAULT '#0d6efd',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
team_id INT NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
severity ENUM('info','low','medium','high','critical') DEFAULT 'info',
|
||||
event_type VARCHAR(50) DEFAULT 'general',
|
||||
occurred_at DATETIME NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS comments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
event_id INT NOT NULL,
|
||||
author VARCHAR(100) NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS network_nodes (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
label VARCHAR(255) NOT NULL,
|
||||
ip_address VARCHAR(45),
|
||||
node_type ENUM('host','server','router','firewall','switch','cloud','endpoint','other') DEFAULT 'host',
|
||||
status ENUM('online','offline','unknown','compromised','monitoring') DEFAULT 'unknown',
|
||||
group_name VARCHAR(100) DEFAULT 'default',
|
||||
pos_x FLOAT DEFAULT 0,
|
||||
pos_y FLOAT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS network_links (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
source_id INT NOT NULL,
|
||||
target_id INT NOT NULL,
|
||||
link_type ENUM('direct','vpn','wireless','monitored') DEFAULT 'direct',
|
||||
label VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (source_id) REFERENCES network_nodes(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (target_id) REFERENCES network_nodes(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT IGNORE INTO teams (name, color) VALUES
|
||||
('Blue Team', '#0d6efd'),
|
||||
('Red Team', '#dc3545'),
|
||||
('SOC', '#ffc107'),
|
||||
('Threat Intel', '#198754');
|
||||
@@ -0,0 +1,20 @@
|
||||
server {
|
||||
listen 80;
|
||||
root /var/www/html;
|
||||
index index.html;
|
||||
|
||||
location /api/ {
|
||||
fastcgi_pass php:9000;
|
||||
fastcgi_param SCRIPT_FILENAME /var/www/backend/api/index.php;
|
||||
fastcgi_param REQUEST_URI $request_uri;
|
||||
fastcgi_param QUERY_STRING $query_string;
|
||||
fastcgi_param REQUEST_METHOD $request_method;
|
||||
fastcgi_param CONTENT_TYPE $content_type;
|
||||
fastcgi_param CONTENT_LENGTH $content_length;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
:root {
|
||||
--neptune-bg: #0a0e1a;
|
||||
--neptune-card: #131a2b;
|
||||
--neptune-border: #1e2a45;
|
||||
--neptune-accent: #3b82f6;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--neptune-bg);
|
||||
color: #e2e8f0;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: linear-gradient(135deg, #0a0e1a 0%, #121828 100%) !important;
|
||||
}
|
||||
|
||||
.card, .modal-content, .list-group-item {
|
||||
background-color: var(--neptune-card);
|
||||
border-color: var(--neptune-border);
|
||||
}
|
||||
|
||||
.form-control, .form-select {
|
||||
background-color: #0d1117;
|
||||
border-color: var(--neptune-border);
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.form-control:focus, .form-select:focus {
|
||||
background-color: #0d1117;
|
||||
border-color: var(--neptune-accent);
|
||||
color: #e2e8f0;
|
||||
box-shadow: 0 0 0 .2rem rgba(59,130,246,.15);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--neptune-accent);
|
||||
border-color: var(--neptune-accent);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #2563eb;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
/* Timeline */
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
padding-left: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.timeline-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: .5rem;
|
||||
top: 0;
|
||||
bottom: -1.5rem;
|
||||
width: 2px;
|
||||
background: var(--neptune-border);
|
||||
}
|
||||
|
||||
.timeline-item:last-child::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
position: absolute;
|
||||
left: .25rem;
|
||||
top: .35rem;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid;
|
||||
background: var(--neptune-card);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.severity-critical { border-color: #ef4444; color: #ef4444; }
|
||||
.severity-high { border-color: #f97316; color: #f97316; }
|
||||
.severity-medium { border-color: #eab308; color: #eab308; }
|
||||
.severity-low { border-color: #22c55e; color: #22c55e; }
|
||||
.severity-info { border-color: #3b82f6; color: #3b82f6; }
|
||||
|
||||
.timeline-card {
|
||||
border-left: 3px solid var(--neptune-accent);
|
||||
transition: border-color .2s;
|
||||
}
|
||||
|
||||
.timeline-card:hover {
|
||||
border-left-color: #60a5fa;
|
||||
}
|
||||
|
||||
.severity-badge {
|
||||
font-size: .65rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .05em;
|
||||
}
|
||||
|
||||
.comment-box {
|
||||
background: #0d1117;
|
||||
border-radius: .375rem;
|
||||
padding: .5rem .75rem;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.comment-box .comment-author {
|
||||
color: var(--neptune-accent);
|
||||
font-weight: 600;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
.comment-box .comment-body {
|
||||
font-size: .85rem;
|
||||
margin-top: .15rem;
|
||||
}
|
||||
|
||||
/* Network Map */
|
||||
.node-tooltip {
|
||||
position: absolute;
|
||||
background: #131a2b;
|
||||
border: 1px solid var(--neptune-border);
|
||||
border-radius: .375rem;
|
||||
padding: .5rem .75rem;
|
||||
font-size: .8rem;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#networkCanvas {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
#networkCanvas:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar { width: 6px; }
|
||||
::-webkit-scrollbar-track { background: var(--neptune-bg); }
|
||||
::-webkit-scrollbar-thumb { background: var(--neptune-border); border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #2a3a60; }
|
||||
|
||||
/* Nav tabs in navbar */
|
||||
.nav-tabs .nav-link {
|
||||
color: #94a3b8;
|
||||
border: none;
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
border-bottom: 2px solid var(--neptune-accent);
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link:hover {
|
||||
color: #fff;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
/* Node list items */
|
||||
.node-list-item {
|
||||
cursor: pointer;
|
||||
transition: background .15s;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.node-list-item:hover {
|
||||
background: rgba(59,130,246,.1);
|
||||
}
|
||||
|
||||
.node-list-item.active {
|
||||
border-left-color: var(--neptune-accent);
|
||||
background: rgba(59,130,246,.15);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: .4rem;
|
||||
}
|
||||
|
||||
.status-online { background: #22c55e; }
|
||||
.status-offline { background: #6b7280; }
|
||||
.status-unknown { background: #9ca3af; }
|
||||
.status-compromised { background: #ef4444; animation: pulse 1.5s infinite; }
|
||||
.status-monitoring { background: #eab308; }
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: .4; }
|
||||
}
|
||||
|
||||
.event-meta {
|
||||
font-size: .75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.event-title {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
.comment-input-group .btn {
|
||||
border-color: var(--neptune-border);
|
||||
color: var(--neptune-accent);
|
||||
}
|
||||
|
||||
.comment-input-group .btn:hover {
|
||||
background: var(--neptune-accent);
|
||||
color: #fff;
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
const API = '/api/';
|
||||
let teams = [];
|
||||
let events = [];
|
||||
let nodes = [];
|
||||
let links = [];
|
||||
let selectedNodeId = null;
|
||||
|
||||
// Network canvas state
|
||||
let canvas, ctx;
|
||||
let canvasNodes = [];
|
||||
let canvasLinks = [];
|
||||
let isDragging = false;
|
||||
let dragNode = null;
|
||||
let offsetX, offsetY;
|
||||
let panX = 0, panY = 0;
|
||||
let isPanning = false;
|
||||
let panStartX, panStartY;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
canvas = document.getElementById('networkCanvas');
|
||||
ctx = canvas.getContext('2d');
|
||||
resizeCanvas();
|
||||
|
||||
loadTeams().then(() => {
|
||||
loadEvents();
|
||||
});
|
||||
loadNetworkData();
|
||||
|
||||
document.getElementById('saveEvent').addEventListener('click', saveEvent);
|
||||
document.getElementById('saveNode').addEventListener('click', saveNode);
|
||||
document.getElementById('saveLink').addEventListener('click', saveLink);
|
||||
document.getElementById('teamFilter').addEventListener('change', renderTimeline);
|
||||
document.getElementById('searchEvents').addEventListener('input', renderTimeline);
|
||||
|
||||
canvas.addEventListener('mousedown', onCanvasMouseDown);
|
||||
canvas.addEventListener('mousemove', onCanvasMouseMove);
|
||||
canvas.addEventListener('mouseup', onCanvasMouseUp);
|
||||
canvas.addEventListener('dblclick', onCanvasDblClick);
|
||||
window.addEventListener('resize', () => { resizeCanvas(); renderNetwork(); });
|
||||
|
||||
document.querySelectorAll('[data-bs-toggle="tab"]').forEach(tab => {
|
||||
tab.addEventListener('shown.bs.tab', () => {
|
||||
if (tab.id === 'network-tab') {
|
||||
resizeCanvas();
|
||||
renderNetwork();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Bootstrap dark mode default
|
||||
document.documentElement.setAttribute('data-bs-theme', 'dark');
|
||||
});
|
||||
|
||||
function resizeCanvas() {
|
||||
const wrapper = document.getElementById('networkCanvasWrapper');
|
||||
canvas.width = wrapper.clientWidth;
|
||||
canvas.height = wrapper.clientHeight;
|
||||
}
|
||||
|
||||
// ==================== API HELPERS ====================
|
||||
async function apiFetch(path, options = {}) {
|
||||
const res = await fetch(API + path, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
...options
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// ==================== TEAMS ====================
|
||||
async function loadTeams() {
|
||||
teams = await apiFetch('teams');
|
||||
const selTeam = document.getElementById('eventTeam');
|
||||
const filter = document.getElementById('teamFilter');
|
||||
selTeam.innerHTML = '';
|
||||
filter.innerHTML = '<option value="">All Teams</option>';
|
||||
teams.forEach(t => {
|
||||
selTeam.innerHTML += `<option value="${t.id}">${t.name}</option>`;
|
||||
filter.innerHTML += `<option value="${t.id}">${t.name}</option>`;
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== EVENTS / TIMELINE ====================
|
||||
async function loadEvents() {
|
||||
events = await apiFetch('events');
|
||||
renderTimeline();
|
||||
}
|
||||
|
||||
function renderTimeline() {
|
||||
const container = document.getElementById('timelineContainer');
|
||||
const teamFilter = document.getElementById('teamFilter').value;
|
||||
const search = document.getElementById('searchEvents').value.toLowerCase();
|
||||
|
||||
let filtered = events;
|
||||
if (teamFilter) filtered = filtered.filter(e => e.team_id == teamFilter);
|
||||
if (search) filtered = filtered.filter(e =>
|
||||
e.title.toLowerCase().includes(search) ||
|
||||
(e.description && e.description.toLowerCase().includes(search))
|
||||
);
|
||||
|
||||
if (!filtered.length) {
|
||||
container.innerHTML = `<div class="text-center text-secondary py-5"><i class="bi bi-journal-text fs-1"></i><p class="mt-2">No events yet. Create your first incident entry!</p></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = filtered.map(e => {
|
||||
const sevClass = 'severity-' + e.severity;
|
||||
const date = new Date(e.occurred_at).toLocaleString();
|
||||
return `
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-dot ${sevClass}"></div>
|
||||
<div class="card timeline-card bg-dark border-secondary" style="border-left-color: ${e.team_color}">
|
||||
<div class="card-body py-2 px-3">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<span class="badge severity-badge me-1" style="background:${e.team_color}20;color:${e.team_color}">${e.team_name}</span>
|
||||
<span class="badge bg-${e.severity === 'critical' ? 'danger' : e.severity === 'high' ? 'warning' : e.severity === 'medium' ? 'warning' : e.severity === 'low' ? 'success' : 'info'} severity-badge">${e.severity}</span>
|
||||
<span class="badge bg-secondary severity-badge ms-1">${e.event_type}</span>
|
||||
</div>
|
||||
<small class="event-meta">${date}</small>
|
||||
</div>
|
||||
<h6 class="event-title mt-1 mb-1">${esc(e.title)}</h6>
|
||||
${e.description ? `<p class="mb-1 small text-secondary">${esc(e.description)}</p>` : ''}
|
||||
<div class="mt-2" id="comments-${e.id}">
|
||||
${renderComments(e)}
|
||||
<div class="input-group input-group-sm comment-input-group mt-1">
|
||||
<input type="text" class="form-control form-control-sm comment-input" placeholder="Add comment..." data-event-id="${e.id}">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="addComment(${e.id}, this)"><i class="bi bi-send"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderComments(event) {
|
||||
if (!event.comments || !event.comments.length) return '';
|
||||
return event.comments.map(c => `
|
||||
<div class="comment-box">
|
||||
<div class="comment-author"><i class="bi bi-person-circle me-1"></i>${esc(c.author)} <span class="text-secondary fw-normal">· ${new Date(c.created_at).toLocaleString()}</span></div>
|
||||
<div class="comment-body">${esc(c.body)}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function addComment(eventId, btn) {
|
||||
const input = btn.parentElement.querySelector('.comment-input');
|
||||
const body = input.value.trim();
|
||||
if (!body) return;
|
||||
const author = prompt('Your name:') || 'Anonymous';
|
||||
await apiFetch('comments', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ event_id: eventId, author, body })
|
||||
});
|
||||
input.value = '';
|
||||
loadEvents();
|
||||
}
|
||||
|
||||
async function saveEvent() {
|
||||
const data = {
|
||||
team_id: document.getElementById('eventTeam').value,
|
||||
title: document.getElementById('eventTitle').value,
|
||||
description: document.getElementById('eventDescription').value,
|
||||
severity: document.getElementById('eventSeverity').value,
|
||||
event_type: document.getElementById('eventType').value,
|
||||
occurred_at: document.getElementById('eventTime').value || new Date().toISOString().slice(0, 16)
|
||||
};
|
||||
if (!data.title) return alert('Title required');
|
||||
await apiFetch('events', { method: 'POST', body: JSON.stringify(data) });
|
||||
bootstrap.Modal.getInstance(document.getElementById('eventModal')).hide();
|
||||
document.getElementById('eventForm').reset();
|
||||
loadEvents();
|
||||
}
|
||||
|
||||
// ==================== NETWORK MAP ====================
|
||||
async function loadNetworkData() {
|
||||
nodes = await apiFetch('nodes');
|
||||
links = await apiFetch('links');
|
||||
populateNodeSelects();
|
||||
renderNodeList();
|
||||
renderNetwork();
|
||||
}
|
||||
|
||||
function populateNodeSelects() {
|
||||
const html = nodes.map(n => `<option value="${n.id}">${esc(n.label)} (${n.ip_address || 'no IP'})</option>`).join('');
|
||||
document.getElementById('linkSource').innerHTML = html;
|
||||
document.getElementById('linkTarget').innerHTML = html;
|
||||
}
|
||||
|
||||
function renderNodeList() {
|
||||
const list = document.getElementById('nodeList');
|
||||
list.innerHTML = nodes.map(n => `
|
||||
<div class="list-group-item bg-dark border-secondary node-list-item py-2 ${selectedNodeId == n.id ? 'active' : ''}" data-node-id="${n.id}" onclick="selectNode(${n.id})">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="status-dot status-${n.status}"></span>
|
||||
<div>
|
||||
<strong class="small">${esc(n.label)}</strong>
|
||||
<div class="text-secondary" style="font-size:.7rem;">${n.ip_address || '—'} · ${n.node_type} · ${n.group_name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function selectNode(id) {
|
||||
selectedNodeId = id;
|
||||
const n = nodes.find(x => x.id == id);
|
||||
if (n) {
|
||||
document.getElementById('nodeDetails').innerHTML = `
|
||||
<div class="small">
|
||||
<div class="d-flex align-items-center mb-1"><span class="status-dot status-${n.status} me-2"></span><strong>${esc(n.label)}</strong></div>
|
||||
<div><span class="text-secondary">IP:</span> ${n.ip_address || '—'}</div>
|
||||
<div><span class="text-secondary">Type:</span> ${n.node_type}</div>
|
||||
<div><span class="text-secondary">Status:</span> ${n.status}</div>
|
||||
<div><span class="text-secondary">Group:</span> ${n.group_name}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
renderNodeList();
|
||||
renderNetwork();
|
||||
}
|
||||
|
||||
async function saveNode() {
|
||||
const data = {
|
||||
label: document.getElementById('nodeLabel').value,
|
||||
ip_address: document.getElementById('nodeIp').value,
|
||||
node_type: document.getElementById('nodeType').value,
|
||||
status: document.getElementById('nodeStatus').value,
|
||||
group_name: document.getElementById('nodeGroup').value || 'default',
|
||||
pos_x: Math.random() * canvas.width * 0.6 + canvas.width * 0.2,
|
||||
pos_y: Math.random() * canvas.height * 0.6 + canvas.height * 0.2
|
||||
};
|
||||
if (!data.label) return alert('Label required');
|
||||
await apiFetch('nodes', { method: 'POST', body: JSON.stringify(data) });
|
||||
bootstrap.Modal.getInstance(document.getElementById('nodeModal')).hide();
|
||||
document.getElementById('nodeForm').reset();
|
||||
loadNetworkData();
|
||||
}
|
||||
|
||||
async function saveLink() {
|
||||
const data = {
|
||||
source_id: document.getElementById('linkSource').value,
|
||||
target_id: document.getElementById('linkTarget').value,
|
||||
link_type: document.getElementById('linkType').value,
|
||||
label: document.getElementById('linkLabel').value
|
||||
};
|
||||
if (data.source_id === data.target_id) return alert('Source and target must differ');
|
||||
await apiFetch('links', { method: 'POST', body: JSON.stringify(data) });
|
||||
bootstrap.Modal.getInstance(document.getElementById('linkModal')).hide();
|
||||
document.getElementById('linkForm').reset();
|
||||
loadNetworkData();
|
||||
}
|
||||
|
||||
// ==================== CANVAS RENDERING ====================
|
||||
function buildCanvasGraph() {
|
||||
canvasNodes = nodes.map(n => ({
|
||||
id: n.id,
|
||||
label: n.label,
|
||||
ip: n.ip_address,
|
||||
type: n.node_type,
|
||||
status: n.status,
|
||||
group: n.group_name,
|
||||
x: parseFloat(n.pos_x) || (Math.random() * canvas.width * 0.6 + canvas.width * 0.2),
|
||||
y: parseFloat(n.pos_y) || (Math.random() * canvas.height * 0.6 + canvas.height * 0.2),
|
||||
radius: n.node_type === 'router' || n.node_type === 'firewall' ? 22 : n.node_type === 'server' ? 28 : 18
|
||||
}));
|
||||
|
||||
canvasLinks = links.map(l => ({
|
||||
source: canvasNodes.find(n => n.id == l.source_id),
|
||||
target: canvasNodes.find(n => n.id == l.target_id),
|
||||
type: l.link_type,
|
||||
label: l.label
|
||||
})).filter(l => l.source && l.target);
|
||||
}
|
||||
|
||||
function getNodeColor(node) {
|
||||
const colors = {
|
||||
host: '#3b82f6', server: '#8b5cf6', router: '#f59e0b',
|
||||
firewall: '#ef4444', switch: '#06b6d4', cloud: '#22c55e',
|
||||
endpoint: '#ec4899', other: '#6b7280'
|
||||
};
|
||||
return colors[node.type] || '#6b7280';
|
||||
}
|
||||
|
||||
function renderNetwork() {
|
||||
buildCanvasGraph();
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.save();
|
||||
ctx.translate(panX, panY);
|
||||
|
||||
// Draw links
|
||||
canvasLinks.forEach(l => {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(l.source.x, l.source.y);
|
||||
ctx.lineTo(l.target.x, l.target.y);
|
||||
|
||||
const colors = { direct: '#334155', vpn: '#eab308', wireless: '#22c55e', monitored: '#3b82f6' };
|
||||
ctx.strokeStyle = colors[l.type] || '#334155';
|
||||
ctx.lineWidth = l.type === 'vpn' ? 2.5 : l.type === 'monitored' ? 1.5 : 1.5;
|
||||
|
||||
if (l.type === 'vpn' || l.type === 'wireless') {
|
||||
ctx.setLineDash([6, 4]);
|
||||
} else {
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
|
||||
// Link label
|
||||
if (l.label) {
|
||||
const mx = (l.source.x + l.target.x) / 2;
|
||||
const my = (l.source.y + l.target.y) / 2;
|
||||
ctx.fillStyle = '#94a3b8';
|
||||
ctx.font = '10px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(l.label, mx, my - 6);
|
||||
}
|
||||
});
|
||||
|
||||
// Draw nodes
|
||||
canvasNodes.forEach(n => {
|
||||
const isSelected = selectedNodeId == n.id;
|
||||
const color = getNodeColor(n);
|
||||
|
||||
// Glow for selected
|
||||
if (isSelected) {
|
||||
ctx.shadowColor = color;
|
||||
ctx.shadowBlur = 20;
|
||||
}
|
||||
|
||||
// Shape by type
|
||||
ctx.beginPath();
|
||||
if (n.type === 'router' || n.type === 'firewall') {
|
||||
// Diamond
|
||||
ctx.moveTo(n.x, n.y - n.radius);
|
||||
ctx.lineTo(n.x + n.radius, n.y);
|
||||
ctx.lineTo(n.x, n.y + n.radius);
|
||||
ctx.lineTo(n.x - n.radius, n.y);
|
||||
ctx.closePath();
|
||||
} else if (n.type === 'cloud') {
|
||||
// Cloud-like
|
||||
ctx.arc(n.x, n.y, n.radius, 0, Math.PI * 2);
|
||||
} else if (n.type === 'server') {
|
||||
// Square
|
||||
ctx.rect(n.x - n.radius * 0.8, n.y - n.radius * 0.8, n.radius * 1.6, n.radius * 1.6);
|
||||
} else {
|
||||
// Circle (host, endpoint, etc)
|
||||
ctx.arc(n.x, n.y, n.radius, 0, Math.PI * 2);
|
||||
}
|
||||
|
||||
ctx.fillStyle = color + '30';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = isSelected ? '#fff' : color;
|
||||
ctx.lineWidth = isSelected ? 3 : 2;
|
||||
ctx.stroke();
|
||||
|
||||
// Status indicator
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.beginPath();
|
||||
ctx.arc(n.x + n.radius * 0.6, n.y - n.radius * 0.6, 5, 0, Math.PI * 2);
|
||||
const statusColors = { online: '#22c55e', offline: '#6b7280', unknown: '#9ca3af', compromised: '#ef4444', monitoring: '#eab308' };
|
||||
ctx.fillStyle = statusColors[n.status] || '#9ca3af';
|
||||
ctx.fill();
|
||||
|
||||
// Label
|
||||
ctx.fillStyle = '#e2e8f0';
|
||||
ctx.font = isSelected ? 'bold 11px sans-serif' : '10px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(n.label, n.x, n.y + n.radius + 14);
|
||||
if (n.ip) {
|
||||
ctx.fillStyle = '#64748b';
|
||||
ctx.font = '9px sans-serif';
|
||||
ctx.fillText(n.ip, n.x, n.y + n.radius + 26);
|
||||
}
|
||||
});
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// ==================== CANVAS EVENTS ====================
|
||||
function getCanvasNode(mx, my) {
|
||||
return canvasNodes.find(n => {
|
||||
const dx = mx - n.x, dy = my - n.y;
|
||||
return Math.sqrt(dx * dx + dy * dy) < n.radius + 8;
|
||||
});
|
||||
}
|
||||
|
||||
function onCanvasMouseDown(e) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mx = e.clientX - rect.left - panX;
|
||||
const my = e.clientY - rect.top - panY;
|
||||
|
||||
const node = getCanvasNode(mx, my);
|
||||
if (node) {
|
||||
isDragging = true;
|
||||
dragNode = node;
|
||||
offsetX = mx - node.x;
|
||||
offsetY = my - node.y;
|
||||
canvas.style.cursor = 'grabbing';
|
||||
} else {
|
||||
isPanning = true;
|
||||
panStartX = e.clientX - panX;
|
||||
panStartY = e.clientY - panY;
|
||||
canvas.style.cursor = 'grabbing';
|
||||
}
|
||||
}
|
||||
|
||||
function onCanvasMouseMove(e) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
if (isDragging && dragNode) {
|
||||
dragNode.x = e.clientX - rect.left - panX - offsetX;
|
||||
dragNode.y = e.clientY - rect.top - panY - offsetY;
|
||||
renderNetwork();
|
||||
} else if (isPanning) {
|
||||
panX = e.clientX - panStartX;
|
||||
panY = e.clientY - panStartY;
|
||||
renderNetwork();
|
||||
} else {
|
||||
const mx = e.clientX - rect.left - panX;
|
||||
const my = e.clientY - rect.top - panY;
|
||||
canvas.style.cursor = getCanvasNode(mx, my) ? 'pointer' : 'grab';
|
||||
}
|
||||
}
|
||||
|
||||
function onCanvasMouseUp(e) {
|
||||
if (isDragging && dragNode) {
|
||||
// Save position
|
||||
apiFetch(`nodes/${dragNode.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ pos_x: dragNode.x, pos_y: dragNode.y })
|
||||
});
|
||||
}
|
||||
isDragging = false;
|
||||
dragNode = null;
|
||||
isPanning = false;
|
||||
canvas.style.cursor = 'grab';
|
||||
}
|
||||
|
||||
function onCanvasDblClick(e) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mx = e.clientX - rect.left - panX;
|
||||
const my = e.clientY - rect.top - panY;
|
||||
const node = getCanvasNode(mx, my);
|
||||
if (node) {
|
||||
selectNode(node.id);
|
||||
}
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
if (!s) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = s;
|
||||
return div.innerHTML;
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Neptune - Cybersecurity Incident Journal</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="assets/css/style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark border-bottom border-secondary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand d-flex align-items-center" href="#">
|
||||
<i class="bi bi-globe2 me-2 text-primary"></i>
|
||||
<span class="fw-bold">Neptune</span>
|
||||
<span class="badge bg-secondary ms-2 small">Cybersecurity Journal</span>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="nav nav-tabs border-0 ms-3" id="mainTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="timeline-tab" data-bs-toggle="tab" data-bs-target="#timeline" type="button" role="tab">
|
||||
<i class="bi bi-clock-history me-1"></i>Timeline
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="network-tab" data-bs-toggle="tab" data-bs-target="#network" type="button" role="tab">
|
||||
<i class="bi bi-diagram-3 me-1"></i>Network Map
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
<div class="tab-content" id="mainTabContent">
|
||||
|
||||
<!-- ==================== TIMELINE TAB ==================== -->
|
||||
<div class="tab-pane fade show active" id="timeline" role="tabpanel">
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<h4><i class="bi bi-clock-history text-primary"></i> Incident Timeline</h4>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#eventModal">
|
||||
<i class="bi bi-plus-lg"></i> New Event
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<select class="form-select form-select-sm" id="teamFilter">
|
||||
<option value="">All Teams</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control form-control-sm" id="searchEvents" placeholder="Search events...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="timelineContainer">
|
||||
<div class="text-center text-secondary py-5">
|
||||
<div class="spinner-border" role="status"></div>
|
||||
<p class="mt-2">Loading events...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== NETWORK MAP TAB ==================== -->
|
||||
<div class="tab-pane fade" id="network" role="tabpanel">
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<h4><i class="bi bi-diagram-3 text-primary"></i> Network Map</h4>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#nodeModal">
|
||||
<i class="bi bi-plus-lg"></i> Add Node
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#linkModal">
|
||||
<i class="bi bi-link-45deg"></i> Add Link
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-body p-0" id="networkCanvasWrapper" style="height: 70vh; position: relative; overflow: hidden;">
|
||||
<canvas id="networkCanvas" style="width: 100%; height: 100%;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-dark border-secondary mb-2">
|
||||
<div class="card-header py-2">
|
||||
<small class="fw-bold">Node Details</small>
|
||||
</div>
|
||||
<div class="card-body py-2" id="nodeDetails">
|
||||
<small class="text-secondary">Click a node to see details</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group list-group-flush bg-dark border-secondary rounded" id="nodeList" style="max-height: 50vh; overflow-y: auto;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== EVENT MODAL ==================== -->
|
||||
<div class="modal fade" id="eventModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content bg-dark">
|
||||
<div class="modal-header border-secondary">
|
||||
<h5 class="modal-title"><i class="bi bi-plus-circle text-primary"></i> New Event</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="eventForm">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Team</label>
|
||||
<select class="form-select form-select-sm" id="eventTeam" required></select>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Title</label>
|
||||
<input type="text" class="form-control form-control-sm" id="eventTitle" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Description</label>
|
||||
<textarea class="form-control form-control-sm" id="eventDescription" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col">
|
||||
<label class="form-label small">Severity</label>
|
||||
<select class="form-select form-select-sm" id="eventSeverity">
|
||||
<option value="info">Info</option>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
<option value="critical">Critical</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label small">Type</label>
|
||||
<select class="form-select form-select-sm" id="eventType">
|
||||
<option value="general">General</option>
|
||||
<option value="detection">Detection</option>
|
||||
<option value="incident">Incident</option>
|
||||
<option value="remediation">Remediation</option>
|
||||
<option value="intel">Threat Intel</option>
|
||||
<option value="exercise">Exercise</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Occurred At</label>
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="eventTime">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer border-secondary">
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" id="saveEvent"><i class="bi bi-save"></i> Save Event</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== NODE MODAL ==================== -->
|
||||
<div class="modal fade" id="nodeModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content bg-dark">
|
||||
<div class="modal-header border-secondary">
|
||||
<h5 class="modal-title"><i class="bi bi-plus-circle text-primary"></i> Add Network Node</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="nodeForm">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Label</label>
|
||||
<input type="text" class="form-control form-control-sm" id="nodeLabel" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">IP Address</label>
|
||||
<input type="text" class="form-control form-control-sm" id="nodeIp">
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col">
|
||||
<label class="form-label small">Type</label>
|
||||
<select class="form-select form-select-sm" id="nodeType">
|
||||
<option value="host">Host</option>
|
||||
<option value="server">Server</option>
|
||||
<option value="router">Router</option>
|
||||
<option value="firewall">Firewall</option>
|
||||
<option value="switch">Switch</option>
|
||||
<option value="cloud">Cloud</option>
|
||||
<option value="endpoint">Endpoint</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label small">Status</label>
|
||||
<select class="form-select form-select-sm" id="nodeStatus">
|
||||
<option value="unknown">Unknown</option>
|
||||
<option value="online">Online</option>
|
||||
<option value="offline">Offline</option>
|
||||
<option value="monitoring">Monitoring</option>
|
||||
<option value="compromised">Compromised</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Group</label>
|
||||
<input type="text" class="form-control form-control-sm" id="nodeGroup" placeholder="e.g. DMZ, Internal, Cloud">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer border-secondary">
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" id="saveNode"><i class="bi bi-save"></i> Add Node</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== LINK MODAL ==================== -->
|
||||
<div class="modal fade" id="linkModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content bg-dark">
|
||||
<div class="modal-header border-secondary">
|
||||
<h5 class="modal-title"><i class="bi bi-link-45deg text-primary"></i> Add Connection</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="linkForm">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Source Node</label>
|
||||
<select class="form-select form-select-sm" id="linkSource" required></select>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Target Node</label>
|
||||
<select class="form-select form-select-sm" id="linkTarget" required></select>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Connection Type</label>
|
||||
<select class="form-select form-select-sm" id="linkType">
|
||||
<option value="direct">Direct</option>
|
||||
<option value="vpn">VPN</option>
|
||||
<option value="wireless">Wireless</option>
|
||||
<option value="monitored">Monitored</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Label (optional)</label>
|
||||
<input type="text" class="form-control form-control-sm" id="linkLabel" placeholder="e.g. 1 Gbps, SSH Tunnel">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer border-secondary">
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" id="saveLink"><i class="bi bi-save"></i> Add Link</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user