// ============================================
// Network Security Analyzer
// Interactive Tool for IoT Security Education
// ============================================
{
// IEEE Color palette
const colors = {
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
lightGray: "#ECF0F1",
white: "#FFFFFF",
red: "#E74C3C",
green: "#27AE60",
purple: "#9B59B6",
yellow: "#F1C40F",
darkRed: "#C0392B",
darkTeal: "#1ABC9C",
lightBlue: "#3498DB",
darkBlue: "#1A5276",
lightYellow: "#FEF9E7",
lightRed: "#FDEDEC",
lightGreen: "#E8F8F5"
};
// Risk levels
const riskLevels = {
critical: { label: "CRITICAL", color: colors.darkRed, score: 10 },
high: { label: "HIGH", color: colors.red, score: 7 },
medium: { label: "MEDIUM", color: colors.orange, score: 4 },
low: { label: "LOW", color: colors.yellow, score: 2 },
info: { label: "INFO", color: colors.lightBlue, score: 1 },
secure: { label: "SECURE", color: colors.green, score: 0 }
};
// STRIDE threat categories
const strideCategories = {
S: { name: "Spoofing", description: "Impersonating something or someone", icon: "mask" },
T: { name: "Tampering", description: "Modifying data or code", icon: "edit" },
R: { name: "Repudiation", description: "Denying performed actions", icon: "eye-off" },
I: { name: "Information Disclosure", description: "Exposing information", icon: "unlock" },
D: { name: "Denial of Service", description: "Denying or degrading service", icon: "x-circle" },
E: { name: "Elevation of Privilege", description: "Gaining unauthorized capabilities", icon: "arrow-up" }
};
// Device types
const deviceTypes = {
sensor: { name: "IoT Sensor", icon: "S", color: colors.orange, defaultPorts: [1883, 5683] },
gateway: { name: "Gateway", icon: "G", color: colors.purple, defaultPorts: [1883, 8883, 443, 80] },
cloud: { name: "Cloud Service", icon: "C", color: colors.lightBlue, defaultPorts: [443, 8883, 5672] },
mobile: { name: "Mobile App", icon: "M", color: colors.teal, defaultPorts: [443] },
server: { name: "Local Server", icon: "Sv", color: colors.darkTeal, defaultPorts: [443, 80, 22, 3306] },
camera: { name: "IP Camera", icon: "Cam", color: colors.orange, defaultPorts: [80, 554, 8080] },
actuator: { name: "Actuator", icon: "A", color: colors.yellow, defaultPorts: [502, 1883] },
database: { name: "Database", icon: "DB", color: colors.darkBlue, defaultPorts: [3306, 5432, 27017] }
};
// Protocol definitions with security properties
const protocols = {
mqtt: { name: "MQTT", port: 1883, encrypted: false, authRequired: false },
mqtts: { name: "MQTT/TLS", port: 8883, encrypted: true, authRequired: true },
coap: { name: "CoAP", port: 5683, encrypted: false, authRequired: false },
coaps: { name: "CoAP/DTLS", port: 5684, encrypted: true, authRequired: true },
http: { name: "HTTP", port: 80, encrypted: false, authRequired: false },
https: { name: "HTTPS", port: 443, encrypted: true, authRequired: true },
amqp: { name: "AMQP", port: 5672, encrypted: false, authRequired: true },
amqps: { name: "AMQP/TLS", port: 5671, encrypted: true, authRequired: true },
modbus: { name: "Modbus TCP", port: 502, encrypted: false, authRequired: false },
ssh: { name: "SSH", port: 22, encrypted: true, authRequired: true },
rtsp: { name: "RTSP", port: 554, encrypted: false, authRequired: false },
custom: { name: "Custom", port: 0, encrypted: false, authRequired: false }
};
// Preset scenarios
const presetScenarios = {
insecure: {
name: "Insecure Smart Home",
description: "Common vulnerabilities in consumer IoT deployments",
devices: [
{ id: "sensor1", type: "sensor", name: "Temp Sensor", x: 100, y: 150, authentication: false, encryption: false, firmwareUpdates: false, exposedPorts: [80, 1883] },
{ id: "camera1", type: "camera", name: "IP Camera", x: 100, y: 250, authentication: false, encryption: false, firmwareUpdates: false, exposedPorts: [80, 554, 8080] },
{ id: "gateway1", type: "gateway", name: "Home Hub", x: 300, y: 200, authentication: true, encryption: false, firmwareUpdates: false, exposedPorts: [80, 1883, 22] },
{ id: "cloud1", type: "cloud", name: "Cloud API", x: 500, y: 200, authentication: true, encryption: true, firmwareUpdates: true, exposedPorts: [443] },
{ id: "mobile1", type: "mobile", name: "Phone App", x: 500, y: 350, authentication: true, encryption: true, firmwareUpdates: true, exposedPorts: [] }
],
connections: [
{ from: "sensor1", to: "gateway1", protocol: "mqtt" },
{ from: "camera1", to: "gateway1", protocol: "http" },
{ from: "gateway1", to: "cloud1", protocol: "mqtt" },
{ from: "mobile1", to: "cloud1", protocol: "https" }
]
},
partial: {
name: "Partially Secured Industrial",
description: "Industrial IoT with mixed security posture",
devices: [
{ id: "sensor1", type: "sensor", name: "PLC Sensor", x: 80, y: 120, authentication: false, encryption: false, firmwareUpdates: false, exposedPorts: [502] },
{ id: "sensor2", type: "sensor", name: "Flow Meter", x: 80, y: 220, authentication: true, encryption: false, firmwareUpdates: true, exposedPorts: [1883] },
{ id: "actuator1", type: "actuator", name: "Valve Control", x: 80, y: 320, authentication: false, encryption: false, firmwareUpdates: false, exposedPorts: [502] },
{ id: "gateway1", type: "gateway", name: "Edge Gateway", x: 280, y: 200, authentication: true, encryption: true, firmwareUpdates: true, exposedPorts: [8883, 443] },
{ id: "server1", type: "server", name: "SCADA Server", x: 480, y: 120, authentication: true, encryption: false, firmwareUpdates: true, exposedPorts: [80, 502, 22] },
{ id: "cloud1", type: "cloud", name: "Analytics Cloud", x: 480, y: 300, authentication: true, encryption: true, firmwareUpdates: true, exposedPorts: [443] }
],
connections: [
{ from: "sensor1", to: "gateway1", protocol: "modbus" },
{ from: "sensor2", to: "gateway1", protocol: "mqtts" },
{ from: "actuator1", to: "gateway1", protocol: "modbus" },
{ from: "gateway1", to: "server1", protocol: "http" },
{ from: "gateway1", to: "cloud1", protocol: "https" }
]
},
secure: {
name: "Best Practice Implementation",
description: "Security best practices applied throughout",
devices: [
{ id: "sensor1", type: "sensor", name: "Secure Sensor 1", x: 80, y: 120, authentication: true, encryption: true, firmwareUpdates: true, exposedPorts: [] },
{ id: "sensor2", type: "sensor", name: "Secure Sensor 2", x: 80, y: 220, authentication: true, encryption: true, firmwareUpdates: true, exposedPorts: [] },
{ id: "camera1", type: "camera", name: "Secure Camera", x: 80, y: 320, authentication: true, encryption: true, firmwareUpdates: true, exposedPorts: [] },
{ id: "gateway1", type: "gateway", name: "Secure Gateway", x: 280, y: 200, authentication: true, encryption: true, firmwareUpdates: true, exposedPorts: [8883] },
{ id: "cloud1", type: "cloud", name: "Secure Cloud", x: 480, y: 150, authentication: true, encryption: true, firmwareUpdates: true, exposedPorts: [443] },
{ id: "mobile1", type: "mobile", name: "Secure App", x: 480, y: 300, authentication: true, encryption: true, firmwareUpdates: true, exposedPorts: [] }
],
connections: [
{ from: "sensor1", to: "gateway1", protocol: "mqtts" },
{ from: "sensor2", to: "gateway1", protocol: "coaps" },
{ from: "camera1", to: "gateway1", protocol: "https" },
{ from: "gateway1", to: "cloud1", protocol: "mqtts" },
{ from: "mobile1", to: "cloud1", protocol: "https" }
]
}
};
// State management
let devices = [];
let connections = [];
let analysisResults = null;
let selectedDevice = null;
let selectedConnection = null;
let deviceIdCounter = 1;
// Layout
const width = 900;
const height = 420;
// Create main container
const container = d3.create("div")
.style("font-family", "system-ui, -apple-system, sans-serif")
.style("max-width", "1000px")
.style("margin", "0 auto");
// Title
container.append("div")
.style("text-align", "center")
.style("margin-bottom", "15px")
.append("h3")
.style("color", colors.navy)
.style("margin", "0")
.text("Network Security Analyzer");
// Preset scenario selector
const presetRow = container.append("div")
.style("display", "flex")
.style("justify-content", "center")
.style("gap", "10px")
.style("margin-bottom", "15px")
.style("flex-wrap", "wrap");
presetRow.append("span")
.style("font-size", "12px")
.style("color", colors.gray)
.style("align-self", "center")
.text("Load Preset:");
Object.entries(presetScenarios).forEach(([key, scenario]) => {
presetRow.append("button")
.text(scenario.name)
.style("padding", "6px 12px")
.style("background", colors.lightGray)
.style("color", colors.navy)
.style("border", `1px solid ${colors.gray}`)
.style("border-radius", "4px")
.style("cursor", "pointer")
.style("font-size", "11px")
.on("click", () => loadPreset(key));
});
// Main layout: 3 columns
const mainLayout = container.append("div")
.style("display", "grid")
.style("grid-template-columns", "200px 1fr 250px")
.style("gap", "15px")
.style("margin-bottom", "15px");
// === LEFT PANEL: Device Builder ===
const leftPanel = mainLayout.append("div")
.style("display", "flex")
.style("flex-direction", "column")
.style("gap", "10px");
// Device palette
const devicePalette = leftPanel.append("div")
.style("background", colors.lightGray)
.style("padding", "12px")
.style("border-radius", "8px");
devicePalette.append("div")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "10px")
.style("font-size", "13px")
.text("Add Device");
const deviceTypeSelect = devicePalette.append("select")
.style("width", "100%")
.style("padding", "6px")
.style("border-radius", "4px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "11px")
.style("margin-bottom", "8px");
Object.entries(deviceTypes).forEach(([key, dt]) => {
deviceTypeSelect.append("option").attr("value", key).text(dt.name);
});
const deviceNameInput = devicePalette.append("input")
.attr("type", "text")
.attr("placeholder", "Device name")
.style("width", "100%")
.style("padding", "6px")
.style("border-radius", "4px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "11px")
.style("margin-bottom", "8px")
.style("box-sizing", "border-box");
// Security properties checkboxes
const securityProps = devicePalette.append("div")
.style("font-size", "10px")
.style("margin-bottom", "8px");
securityProps.append("div")
.style("margin-bottom", "4px")
.style("color", colors.gray)
.text("Security Properties:");
const authCheck = securityProps.append("label")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "4px")
.style("margin-bottom", "3px");
authCheck.append("input").attr("type", "checkbox").attr("id", "authCheck");
authCheck.append("span").text("Authentication enabled");
const encCheck = securityProps.append("label")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "4px")
.style("margin-bottom", "3px");
encCheck.append("input").attr("type", "checkbox").attr("id", "encCheck");
encCheck.append("span").text("Encryption enabled");
const fwCheck = securityProps.append("label")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "4px");
fwCheck.append("input").attr("type", "checkbox").attr("id", "fwCheck");
fwCheck.append("span").text("Firmware updates");
const addDeviceBtn = devicePalette.append("button")
.text("Add Device")
.style("width", "100%")
.style("padding", "8px")
.style("background", colors.teal)
.style("color", colors.white)
.style("border", "none")
.style("border-radius", "4px")
.style("cursor", "pointer")
.style("font-weight", "bold")
.style("font-size", "12px");
// Connection builder
const connectionBuilder = leftPanel.append("div")
.style("background", colors.lightGray)
.style("padding", "12px")
.style("border-radius", "8px");
connectionBuilder.append("div")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "10px")
.style("font-size", "13px")
.text("Add Connection");
connectionBuilder.append("label")
.style("font-size", "10px")
.style("color", colors.gray)
.text("From Device:");
const fromDeviceSelect = connectionBuilder.append("select")
.attr("class", "from-device-select")
.style("width", "100%")
.style("padding", "5px")
.style("border-radius", "4px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "11px")
.style("margin-bottom", "6px");
connectionBuilder.append("label")
.style("font-size", "10px")
.style("color", colors.gray)
.text("To Device:");
const toDeviceSelect = connectionBuilder.append("select")
.attr("class", "to-device-select")
.style("width", "100%")
.style("padding", "5px")
.style("border-radius", "4px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "11px")
.style("margin-bottom", "6px");
connectionBuilder.append("label")
.style("font-size", "10px")
.style("color", colors.gray)
.text("Protocol:");
const protocolSelect = connectionBuilder.append("select")
.style("width", "100%")
.style("padding", "5px")
.style("border-radius", "4px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "11px")
.style("margin-bottom", "8px");
Object.entries(protocols).forEach(([key, p]) => {
const label = p.encrypted ? `${p.name} (Encrypted)` : p.name;
protocolSelect.append("option").attr("value", key).text(label);
});
const addConnectionBtn = connectionBuilder.append("button")
.text("Add Connection")
.style("width", "100%")
.style("padding", "8px")
.style("background", colors.purple)
.style("color", colors.white)
.style("border", "none")
.style("border-radius", "4px")
.style("cursor", "pointer")
.style("font-weight", "bold")
.style("font-size", "12px");
// === CENTER PANEL: Network Diagram ===
const centerPanel = mainLayout.append("div");
const svg = centerPanel.append("svg")
.attr("viewBox", `0 0 ${width - 450} ${height}`)
.attr("width", "100%")
.style("background", colors.white)
.style("border", `2px solid ${colors.gray}`)
.style("border-radius", "8px");
// SVG definitions
const defs = svg.append("defs");
// Arrow markers for connections
["secure", "insecure", "warning"].forEach(type => {
const markerColor = type === "secure" ? colors.green : type === "insecure" ? colors.red : colors.orange;
defs.append("marker")
.attr("id", `arrow-${type}`)
.attr("viewBox", "0 0 10 10")
.attr("refX", 9)
.attr("refY", 5)
.attr("markerWidth", 5)
.attr("markerHeight", 5)
.attr("orient", "auto")
.append("path")
.attr("d", "M 0 0 L 10 5 L 0 10 z")
.attr("fill", markerColor);
});
// Glow filters
["red", "orange", "green"].forEach(color => {
const filterColor = color === "red" ? colors.red : color === "orange" ? colors.orange : colors.green;
const filter = defs.append("filter")
.attr("id", `glow-${color}`)
.attr("x", "-50%")
.attr("y", "-50%")
.attr("width", "200%")
.attr("height", "200%");
filter.append("feDropShadow")
.attr("dx", "0")
.attr("dy", "0")
.attr("stdDeviation", "4")
.attr("flood-color", filterColor)
.attr("flood-opacity", "0.6");
});
// Network diagram groups
const connectionsGroup = svg.append("g").attr("class", "connections");
const devicesGroup = svg.append("g").attr("class", "devices");
const labelsGroup = svg.append("g").attr("class", "labels");
const threatPathsGroup = svg.append("g").attr("class", "threat-paths");
// Instruction text when empty
const instructionText = svg.append("text")
.attr("x", (width - 450) / 2)
.attr("y", height / 2)
.attr("text-anchor", "middle")
.attr("fill", colors.gray)
.attr("font-size", "14px")
.text("Add devices and connections, or load a preset scenario");
// === RIGHT PANEL: Analysis Results ===
const rightPanel = mainLayout.append("div")
.style("display", "flex")
.style("flex-direction", "column")
.style("gap", "10px");
// Analysis controls
const analysisControls = rightPanel.append("div")
.style("background", colors.navy)
.style("padding", "12px")
.style("border-radius", "8px")
.style("color", colors.white);
analysisControls.append("div")
.style("font-weight", "bold")
.style("margin-bottom", "10px")
.style("font-size", "13px")
.text("Security Analysis");
const analyzeBtn = analysisControls.append("button")
.text("Analyze Security")
.style("width", "100%")
.style("padding", "10px")
.style("background", colors.orange)
.style("color", colors.white)
.style("border", "none")
.style("border-radius", "4px")
.style("cursor", "pointer")
.style("font-weight", "bold")
.style("font-size", "13px")
.style("margin-bottom", "8px");
const clearBtn = analysisControls.append("button")
.text("Clear Network")
.style("width", "100%")
.style("padding", "8px")
.style("background", colors.gray)
.style("color", colors.white)
.style("border", "none")
.style("border-radius", "4px")
.style("cursor", "pointer")
.style("font-size", "11px");
// Risk score display
const riskScoreBox = rightPanel.append("div")
.style("background", colors.lightGray)
.style("padding", "12px")
.style("border-radius", "8px")
.style("text-align", "center");
riskScoreBox.append("div")
.style("font-size", "11px")
.style("color", colors.gray)
.style("margin-bottom", "5px")
.text("Overall Risk Score");
const riskScoreValue = riskScoreBox.append("div")
.attr("class", "risk-score-value")
.style("font-size", "36px")
.style("font-weight", "bold")
.style("color", colors.gray)
.text("--");
const riskScoreLabel = riskScoreBox.append("div")
.attr("class", "risk-score-label")
.style("font-size", "12px")
.style("color", colors.gray)
.text("Not analyzed");
// Findings summary
const findingsBox = rightPanel.append("div")
.style("background", colors.white)
.style("border", `1px solid ${colors.gray}`)
.style("padding", "12px")
.style("border-radius", "8px")
.style("max-height", "180px")
.style("overflow-y", "auto");
findingsBox.append("div")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "8px")
.style("font-size", "12px")
.text("Findings Summary");
const findingsContainer = findingsBox.append("div")
.attr("class", "findings-container")
.style("font-size", "10px");
// === BOTTOM SECTION: Detailed Results ===
const bottomSection = container.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "15px")
.style("margin-bottom", "15px");
// STRIDE Threat Model
const strideBox = bottomSection.append("div")
.style("background", colors.lightGray)
.style("padding", "15px")
.style("border-radius", "8px");
strideBox.append("div")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "10px")
.style("font-size", "13px")
.text("STRIDE Threat Model");
const strideContainer = strideBox.append("div")
.attr("class", "stride-container")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "8px");
// Initialize STRIDE categories display
Object.entries(strideCategories).forEach(([key, cat]) => {
const item = strideContainer.append("div")
.attr("class", `stride-${key}`)
.style("background", colors.white)
.style("padding", "8px")
.style("border-radius", "4px")
.style("border-left", `3px solid ${colors.gray}`);
item.append("div")
.style("font-weight", "bold")
.style("font-size", "11px")
.style("color", colors.navy)
.text(`${key}: ${cat.name}`);
item.append("div")
.attr("class", "threat-count")
.style("font-size", "10px")
.style("color", colors.gray)
.text("0 threats");
});
// Recommendations
const recsBox = bottomSection.append("div")
.style("background", colors.white)
.style("border", `1px solid ${colors.gray}`)
.style("padding", "15px")
.style("border-radius", "8px");
recsBox.append("div")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "10px")
.style("font-size", "13px")
.text("Recommendations");
const recsContainer = recsBox.append("div")
.attr("class", "recs-container")
.style("max-height", "200px")
.style("overflow-y", "auto")
.style("font-size", "11px");
// Security Checklist
const checklistBox = container.append("div")
.style("background", colors.lightGray)
.style("padding", "15px")
.style("border-radius", "8px")
.style("margin-bottom", "15px");
checklistBox.append("div")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "10px")
.style("font-size", "13px")
.text("Security Checklist");
const checklistContainer = checklistBox.append("div")
.attr("class", "checklist-container")
.style("display", "grid")
.style("grid-template-columns", "repeat(5, 1fr)")
.style("gap", "10px");
// Initialize checklist items
const checklistItems = [
{ id: "auth", name: "Device Authentication", icon: "key" },
{ id: "encrypt", name: "Data Encryption", icon: "lock" },
{ id: "access", name: "Access Control", icon: "shield" },
{ id: "firmware", name: "Firmware Updates", icon: "refresh" },
{ id: "segment", name: "Network Segmentation", icon: "layers" }
];
checklistItems.forEach(item => {
const checkItem = checklistContainer.append("div")
.attr("class", `checklist-${item.id}`)
.style("background", colors.white)
.style("padding", "10px")
.style("border-radius", "4px")
.style("text-align", "center");
checkItem.append("div")
.attr("class", "check-status")
.style("font-size", "20px")
.style("margin-bottom", "5px")
.text("?");
checkItem.append("div")
.style("font-size", "10px")
.style("color", colors.navy)
.text(item.name);
});
// === HELPER FUNCTIONS ===
function updateDeviceSelects() {
fromDeviceSelect.selectAll("option").remove();
toDeviceSelect.selectAll("option").remove();
fromDeviceSelect.append("option").attr("value", "").text("Select device...");
toDeviceSelect.append("option").attr("value", "").text("Select device...");
devices.forEach(d => {
fromDeviceSelect.append("option").attr("value", d.id).text(d.name);
toDeviceSelect.append("option").attr("value", d.id).text(d.name);
});
}
function addDevice() {
const type = deviceTypeSelect.node().value;
const name = deviceNameInput.node().value || `${deviceTypes[type].name} ${deviceIdCounter}`;
const auth = container.select("#authCheck").node().checked;
const enc = container.select("#encCheck").node().checked;
const fw = container.select("#fwCheck").node().checked;
const device = {
id: `device${deviceIdCounter}`,
type,
name,
x: 50 + Math.random() * 300,
y: 50 + Math.random() * 300,
authentication: auth,
encryption: enc,
firmwareUpdates: fw,
exposedPorts: auth ? [] : deviceTypes[type].defaultPorts
};
devices.push(device);
deviceIdCounter++;
deviceNameInput.node().value = "";
container.select("#authCheck").node().checked = false;
container.select("#encCheck").node().checked = false;
container.select("#fwCheck").node().checked = false;
updateDeviceSelects();
renderNetwork();
}
function addConnection() {
const fromId = fromDeviceSelect.node().value;
const toId = toDeviceSelect.node().value;
const protocol = protocolSelect.node().value;
if (!fromId || !toId) {
alert("Please select both devices for the connection.");
return;
}
if (fromId === toId) {
alert("Cannot connect a device to itself.");
return;
}
// Check for existing connection
const exists = connections.some(c =>
(c.from === fromId && c.to === toId) || (c.from === toId && c.to === fromId)
);
if (exists) {
alert("Connection already exists between these devices.");
return;
}
connections.push({ from: fromId, to: toId, protocol });
fromDeviceSelect.node().value = "";
toDeviceSelect.node().value = "";
renderNetwork();
}
function loadPreset(key) {
const scenario = presetScenarios[key];
devices = JSON.parse(JSON.stringify(scenario.devices));
connections = JSON.parse(JSON.stringify(scenario.connections));
deviceIdCounter = devices.length + 1;
analysisResults = null;
updateDeviceSelects();
renderNetwork();
resetAnalysisDisplay();
}
function clearNetwork() {
devices = [];
connections = [];
deviceIdCounter = 1;
analysisResults = null;
updateDeviceSelects();
renderNetwork();
resetAnalysisDisplay();
}
function resetAnalysisDisplay() {
riskScoreValue.text("--").style("color", colors.gray);
riskScoreLabel.text("Not analyzed");
findingsContainer.selectAll("*").remove();
findingsContainer.append("div")
.style("color", colors.gray)
.style("font-style", "italic")
.text("Run analysis to see findings");
Object.keys(strideCategories).forEach(key => {
strideContainer.select(`.stride-${key}`)
.style("border-left-color", colors.gray)
.select(".threat-count")
.text("0 threats");
});
recsContainer.selectAll("*").remove();
recsContainer.append("div")
.style("color", colors.gray)
.style("font-style", "italic")
.text("Run analysis to see recommendations");
checklistItems.forEach(item => {
checklistContainer.select(`.checklist-${item.id}`)
.style("background", colors.white)
.select(".check-status")
.style("color", colors.gray)
.text("?");
});
threatPathsGroup.selectAll("*").remove();
}
function renderNetwork() {
connectionsGroup.selectAll("*").remove();
devicesGroup.selectAll("*").remove();
labelsGroup.selectAll("*").remove();
// Show/hide instruction text
instructionText.style("display", devices.length === 0 ? "block" : "none");
// Draw connections
connections.forEach(conn => {
const fromDevice = devices.find(d => d.id === conn.from);
const toDevice = devices.find(d => d.id === conn.to);
if (!fromDevice || !toDevice) return;
const protocol = protocols[conn.protocol];
const isSecure = protocol.encrypted;
const lineColor = isSecure ? colors.green : colors.red;
const markerType = isSecure ? "secure" : "insecure";
// Calculate connection line with offset for device radius
const dx = toDevice.x - fromDevice.x;
const dy = toDevice.y - fromDevice.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const offsetX = (dx / dist) * 25;
const offsetY = (dy / dist) * 25;
connectionsGroup.append("line")
.attr("x1", fromDevice.x + offsetX)
.attr("y1", fromDevice.y + offsetY)
.attr("x2", toDevice.x - offsetX)
.attr("y2", toDevice.y - offsetY)
.attr("stroke", lineColor)
.attr("stroke-width", 2)
.attr("stroke-dasharray", isSecure ? "none" : "5,3")
.attr("marker-end", `url(#arrow-${markerType})`)
.attr("class", `connection-${conn.from}-${conn.to}`);
// Protocol label
const midX = (fromDevice.x + toDevice.x) / 2;
const midY = (fromDevice.y + toDevice.y) / 2;
labelsGroup.append("rect")
.attr("x", midX - 25)
.attr("y", midY - 8)
.attr("width", 50)
.attr("height", 16)
.attr("fill", colors.white)
.attr("stroke", lineColor)
.attr("stroke-width", 1)
.attr("rx", 3);
labelsGroup.append("text")
.attr("x", midX)
.attr("y", midY + 4)
.attr("text-anchor", "middle")
.attr("font-size", "8px")
.attr("fill", lineColor)
.attr("font-weight", "bold")
.text(protocol.name);
});
// Draw devices
devices.forEach(device => {
const dt = deviceTypes[device.type];
const g = devicesGroup.append("g")
.attr("class", `device-${device.id}`)
.attr("transform", `translate(${device.x}, ${device.y})`)
.style("cursor", "pointer");
// Device circle
const securityScore = (device.authentication ? 1 : 0) + (device.encryption ? 1 : 0) + (device.firmwareUpdates ? 1 : 0);
let deviceColor = dt.color;
let glowFilter = null;
if (analysisResults) {
const deviceVulns = analysisResults.vulnerabilities.filter(v => v.deviceId === device.id);
if (deviceVulns.some(v => v.risk === "critical" || v.risk === "high")) {
glowFilter = "url(#glow-red)";
} else if (deviceVulns.some(v => v.risk === "medium")) {
glowFilter = "url(#glow-orange)";
} else if (securityScore === 3) {
glowFilter = "url(#glow-green)";
}
}
g.append("circle")
.attr("r", 22)
.attr("fill", deviceColor)
.attr("stroke", colors.navy)
.attr("stroke-width", 2)
.attr("filter", glowFilter);
// Device icon
g.append("text")
.attr("text-anchor", "middle")
.attr("dy", "0.35em")
.attr("font-size", "10px")
.attr("font-weight", "bold")
.attr("fill", colors.white)
.text(dt.icon);
// Device name
g.append("text")
.attr("y", 35)
.attr("text-anchor", "middle")
.attr("font-size", "9px")
.attr("fill", colors.navy)
.text(device.name);
// Security indicators
const indicators = g.append("g")
.attr("transform", "translate(-18, -35)");
if (!device.authentication) {
indicators.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", 5)
.attr("fill", colors.red)
.append("title")
.text("No authentication");
}
if (!device.encryption) {
indicators.append("circle")
.attr("cx", 12)
.attr("cy", 0)
.attr("r", 5)
.attr("fill", colors.orange)
.append("title")
.text("No encryption");
}
if (!device.firmwareUpdates) {
indicators.append("circle")
.attr("cx", 24)
.attr("cy", 0)
.attr("r", 5)
.attr("fill", colors.yellow)
.append("title")
.text("No firmware updates");
}
// Click handler to remove device
g.on("click", () => {
if (confirm(`Remove ${device.name}?`)) {
devices = devices.filter(d => d.id !== device.id);
connections = connections.filter(c => c.from !== device.id && c.to !== device.id);
updateDeviceSelects();
renderNetwork();
resetAnalysisDisplay();
}
});
});
}
function analyzeNetwork() {
if (devices.length === 0) {
alert("Add devices to analyze the network.");
return;
}
const vulnerabilities = [];
const threats = { S: [], T: [], R: [], I: [], D: [], E: [] };
const recommendations = [];
let totalRiskScore = 0;
// Analyze devices
devices.forEach(device => {
// Check authentication
if (!device.authentication) {
vulnerabilities.push({
deviceId: device.id,
deviceName: device.name,
type: "missing-auth",
description: "Missing device authentication",
risk: "critical",
details: "Device accepts connections without verifying identity"
});
threats.S.push(`${device.name}: Device spoofing possible`);
threats.E.push(`${device.name}: Unauthorized access possible`);
totalRiskScore += 10;
recommendations.push({
priority: 1,
difficulty: "medium",
action: `Enable authentication on ${device.name}`,
details: "Implement certificate-based or pre-shared key authentication"
});
}
// Check encryption
if (!device.encryption) {
vulnerabilities.push({
deviceId: device.id,
deviceName: device.name,
type: "missing-encryption",
description: "Data not encrypted",
risk: "high",
details: "Data transmitted in cleartext can be intercepted"
});
threats.I.push(`${device.name}: Data interception possible`);
threats.T.push(`${device.name}: Man-in-the-middle attacks possible`);
totalRiskScore += 7;
recommendations.push({
priority: 2,
difficulty: "medium",
action: `Enable TLS/DTLS encryption on ${device.name}`,
details: "Use secure protocol variants (MQTTS, HTTPS, CoAPS)"
});
}
// Check firmware updates
if (!device.firmwareUpdates) {
vulnerabilities.push({
deviceId: device.id,
deviceName: device.name,
type: "no-updates",
description: "No firmware update mechanism",
risk: "medium",
details: "Cannot patch security vulnerabilities"
});
threats.T.push(`${device.name}: Unpatched vulnerabilities exploitable`);
totalRiskScore += 4;
recommendations.push({
priority: 3,
difficulty: "high",
action: `Implement OTA updates for ${device.name}`,
details: "Set up secure firmware update infrastructure"
});
}
// Check exposed ports
if (device.exposedPorts && device.exposedPorts.length > 0) {
const dangerousPorts = device.exposedPorts.filter(p => [22, 23, 80, 502, 554, 8080].includes(p));
if (dangerousPorts.length > 0) {
vulnerabilities.push({
deviceId: device.id,
deviceName: device.name,
type: "exposed-ports",
description: `Potentially dangerous ports exposed: ${dangerousPorts.join(", ")}`,
risk: "high",
details: "Open ports increase attack surface"
});
threats.D.push(`${device.name}: DoS attacks via exposed services`);
threats.E.push(`${device.name}: Service exploitation possible`);
totalRiskScore += 6;
recommendations.push({
priority: 2,
difficulty: "easy",
action: `Close unnecessary ports on ${device.name}`,
details: `Review and restrict access to ports: ${dangerousPorts.join(", ")}`
});
}
}
});
// Analyze connections
connections.forEach(conn => {
const protocol = protocols[conn.protocol];
const fromDevice = devices.find(d => d.id === conn.from);
const toDevice = devices.find(d => d.id === conn.to);
if (!protocol.encrypted) {
vulnerabilities.push({
connectionId: `${conn.from}-${conn.to}`,
type: "unencrypted-connection",
description: `Unencrypted ${protocol.name} connection`,
risk: "high",
details: `Connection between ${fromDevice?.name} and ${toDevice?.name} is not encrypted`
});
threats.I.push(`${fromDevice?.name} to ${toDevice?.name}: Traffic interception possible`);
totalRiskScore += 7;
const secureAlt = conn.protocol === "mqtt" ? "MQTTS" :
conn.protocol === "http" ? "HTTPS" :
conn.protocol === "coap" ? "CoAPS" : "TLS-secured variant";
recommendations.push({
priority: 1,
difficulty: "easy",
action: `Upgrade ${protocol.name} to ${secureAlt}`,
details: `Secure the connection between ${fromDevice?.name} and ${toDevice?.name}`
});
}
if (!protocol.authRequired && !fromDevice?.authentication) {
vulnerabilities.push({
connectionId: `${conn.from}-${conn.to}`,
type: "no-connection-auth",
description: `No authentication on ${protocol.name} connection`,
risk: "high",
details: `Connection allows unauthenticated access`
});
threats.S.push(`${fromDevice?.name}: Anonymous access enabled`);
threats.R.push(`${fromDevice?.name}: Actions cannot be attributed`);
totalRiskScore += 5;
}
// Check for weak protocols
if (conn.protocol === "modbus" || conn.protocol === "rtsp") {
vulnerabilities.push({
connectionId: `${conn.from}-${conn.to}`,
type: "weak-protocol",
description: `Inherently insecure protocol: ${protocol.name}`,
risk: "critical",
details: `${protocol.name} has no built-in security features`
});
threats.T.push(`${fromDevice?.name}: Protocol manipulation possible`);
threats.D.push(`${toDevice?.name}: Service disruption via protocol abuse`);
totalRiskScore += 8;
recommendations.push({
priority: 1,
difficulty: "high",
action: `Wrap ${protocol.name} in VPN or TLS tunnel`,
details: "Legacy protocols require network-level security"
});
}
});
// Check network segmentation
const deviceTypeCounts = {};
devices.forEach(d => {
deviceTypeCounts[d.type] = (deviceTypeCounts[d.type] || 0) + 1;
});
const hasMultipleTypes = Object.keys(deviceTypeCounts).length > 1;
const allDirectlyConnected = connections.length >= devices.length - 1;
if (hasMultipleTypes && !devices.some(d => d.type === "gateway")) {
vulnerabilities.push({
type: "no-segmentation",
description: "No network segmentation detected",
risk: "medium",
details: "All devices appear to be on the same network segment"
});
threats.E.push("Network: Lateral movement easy after breach");
totalRiskScore += 4;
recommendations.push({
priority: 3,
difficulty: "medium",
action: "Implement network segmentation",
details: "Use VLANs or separate subnets for IoT devices"
});
}
// Calculate overall risk
const maxPossibleScore = devices.length * 30 + connections.length * 20;
const normalizedScore = Math.min(100, Math.round((totalRiskScore / Math.max(maxPossibleScore, 1)) * 100));
analysisResults = {
vulnerabilities,
threats,
recommendations: recommendations.sort((a, b) => a.priority - b.priority),
riskScore: normalizedScore,
totalIssues: vulnerabilities.length
};
displayResults();
renderNetwork();
renderThreatPaths();
}
function displayResults() {
if (!analysisResults) return;
// Update risk score
const score = analysisResults.riskScore;
let riskLevel, riskColor;
if (score >= 70) {
riskLevel = "CRITICAL";
riskColor = colors.darkRed;
} else if (score >= 50) {
riskLevel = "HIGH";
riskColor = colors.red;
} else if (score >= 30) {
riskLevel = "MEDIUM";
riskColor = colors.orange;
} else if (score >= 10) {
riskLevel = "LOW";
riskColor = colors.yellow;
} else {
riskLevel = "SECURE";
riskColor = colors.green;
}
riskScoreValue.text(score).style("color", riskColor);
riskScoreLabel.text(riskLevel).style("color", riskColor).style("font-weight", "bold");
// Update findings
findingsContainer.selectAll("*").remove();
if (analysisResults.vulnerabilities.length === 0) {
findingsContainer.append("div")
.style("color", colors.green)
.style("font-weight", "bold")
.text("No vulnerabilities found!");
} else {
const grouped = {};
analysisResults.vulnerabilities.forEach(v => {
if (!grouped[v.risk]) grouped[v.risk] = [];
grouped[v.risk].push(v);
});
["critical", "high", "medium", "low", "info"].forEach(risk => {
if (grouped[risk]) {
findingsContainer.append("div")
.style("font-weight", "bold")
.style("color", riskLevels[risk].color)
.style("margin-top", "8px")
.style("margin-bottom", "4px")
.text(`${riskLevels[risk].label} (${grouped[risk].length})`);
grouped[risk].forEach(v => {
findingsContainer.append("div")
.style("padding-left", "10px")
.style("margin-bottom", "3px")
.style("border-left", `2px solid ${riskLevels[risk].color}`)
.text(v.description);
});
}
});
}
// Update STRIDE threats
Object.entries(analysisResults.threats).forEach(([key, threatList]) => {
const threatColor = threatList.length > 2 ? colors.red :
threatList.length > 0 ? colors.orange : colors.green;
strideContainer.select(`.stride-${key}`)
.style("border-left-color", threatColor)
.select(".threat-count")
.style("color", threatColor)
.text(`${threatList.length} threat${threatList.length !== 1 ? "s" : ""}`);
});
// Update recommendations
recsContainer.selectAll("*").remove();
if (analysisResults.recommendations.length === 0) {
recsContainer.append("div")
.style("color", colors.green)
.text("No recommendations - security looks good!");
} else {
analysisResults.recommendations.forEach((rec, i) => {
const item = recsContainer.append("div")
.style("padding", "8px")
.style("margin-bottom", "6px")
.style("background", colors.lightGray)
.style("border-radius", "4px")
.style("border-left", `3px solid ${rec.priority === 1 ? colors.red : rec.priority === 2 ? colors.orange : colors.yellow}`);
const header = item.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("margin-bottom", "4px");
header.append("span")
.style("font-weight", "bold")
.text(`${i + 1}. ${rec.action}`);
const difficultyColor = rec.difficulty === "easy" ? colors.green :
rec.difficulty === "medium" ? colors.orange : colors.red;
header.append("span")
.style("font-size", "9px")
.style("background", difficultyColor)
.style("color", colors.white)
.style("padding", "2px 6px")
.style("border-radius", "3px")
.text(rec.difficulty.toUpperCase());
item.append("div")
.style("font-size", "10px")
.style("color", colors.gray)
.text(rec.details);
});
}
// Update security checklist
const allDevicesAuth = devices.every(d => d.authentication);
const allDevicesEnc = devices.every(d => d.encryption);
const allDevicesFw = devices.every(d => d.firmwareUpdates);
const allConnsSecure = connections.every(c => protocols[c.protocol].encrypted);
const hasSegmentation = devices.some(d => d.type === "gateway");
const checkResults = {
auth: { passed: allDevicesAuth, partial: devices.some(d => d.authentication) },
encrypt: { passed: allDevicesEnc && allConnsSecure, partial: devices.some(d => d.encryption) || connections.some(c => protocols[c.protocol].encrypted) },
access: { passed: allDevicesAuth, partial: devices.some(d => d.authentication) },
firmware: { passed: allDevicesFw, partial: devices.some(d => d.firmwareUpdates) },
segment: { passed: hasSegmentation, partial: false }
};
checklistItems.forEach(item => {
const result = checkResults[item.id];
let statusSymbol, statusColor, bgColor;
if (result.passed) {
statusSymbol = "✓";
statusColor = colors.green;
bgColor = colors.lightGreen;
} else if (result.partial) {
statusSymbol = "~";
statusColor = colors.orange;
bgColor = colors.lightYellow;
} else {
statusSymbol = "✗";
statusColor = colors.red;
bgColor = colors.lightRed;
}
checklistContainer.select(`.checklist-${item.id}`)
.style("background", bgColor)
.select(".check-status")
.style("color", statusColor)
.text(statusSymbol);
});
}
function renderThreatPaths() {
threatPathsGroup.selectAll("*").remove();
if (!analysisResults) return;
// Highlight vulnerable connections
analysisResults.vulnerabilities
.filter(v => v.connectionId)
.forEach(v => {
const [fromId, toId] = v.connectionId.split("-");
const fromDevice = devices.find(d => d.id === fromId);
const toDevice = devices.find(d => d.id === toId);
if (!fromDevice || !toDevice) return;
// Draw threat indicator on connection
const midX = (fromDevice.x + toDevice.x) / 2;
const midY = (fromDevice.y + toDevice.y) / 2;
threatPathsGroup.append("circle")
.attr("cx", midX)
.attr("cy", midY - 20)
.attr("r", 8)
.attr("fill", riskLevels[v.risk]?.color || colors.red)
.attr("stroke", colors.white)
.attr("stroke-width", 1);
threatPathsGroup.append("text")
.attr("x", midX)
.attr("y", midY - 17)
.attr("text-anchor", "middle")
.attr("font-size", "10px")
.attr("font-weight", "bold")
.attr("fill", colors.white)
.text("!");
});
}
// === EVENT HANDLERS ===
addDeviceBtn.on("click", addDevice);
addConnectionBtn.on("click", addConnection);
analyzeBtn.on("click", analyzeNetwork);
clearBtn.on("click", clearNetwork);
// === INITIAL RENDER ===
updateDeviceSelects();
renderNetwork();
resetAnalysisDisplay();
// Legend
const legend = container.append("div")
.style("display", "flex")
.style("justify-content", "center")
.style("gap", "20px")
.style("margin-top", "15px")
.style("flex-wrap", "wrap")
.style("font-size", "11px");
const legendItems = [
{ color: colors.green, label: "Secure Connection", dash: false },
{ color: colors.red, label: "Insecure Connection", dash: true },
{ color: colors.red, circle: true, label: "No Auth" },
{ color: colors.orange, circle: true, label: "No Encryption" },
{ color: colors.yellow, circle: true, label: "No Updates" }
];
legendItems.forEach(item => {
const legendItem = legend.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "5px");
if (item.circle) {
legendItem.append("div")
.style("width", "10px")
.style("height", "10px")
.style("background", item.color)
.style("border-radius", "50%");
} else if (item.dash) {
legendItem.append("div")
.style("width", "20px")
.style("height", "2px")
.style("background", `repeating-linear-gradient(90deg, ${item.color} 0px, ${item.color} 4px, transparent 4px, transparent 7px)`);
} else {
legendItem.append("div")
.style("width", "20px")
.style("height", "2px")
.style("background", item.color);
}
legendItem.append("span")
.style("color", colors.navy)
.text(item.label);
});
return container.node();
}