```{ojs}
//| echo: false
//| label: device-provisioning-visualizer
//| fig-alt: "Interactive device provisioning flow visualizer showing different IoT onboarding methods with animated swimlane diagrams, security analysis, and method comparison"
// IEEE Color Palette
ieeeColors = ({
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
lightGray: "#ECF0F1",
darkGray: "#34495E",
green: "#27AE60",
red: "#E74C3C",
purple: "#9B59B6",
blue: "#3498DB",
yellow: "#F1C40F"
})
// Provisioning Methods Configuration
provisioningMethods = [
{
id: "ztp",
name: "Zero-Touch Provisioning (ZTP)",
shortName: "ZTP",
description: "Fully automated provisioning without user interaction",
complexity: 2,
security: 4,
friction: 1,
scalability: 5,
actors: ["Device", "DHCP/DNS", "Provisioning Server", "Cloud Platform"],
useCases: ["Enterprise deployments", "Large-scale rollouts", "Industrial IoT"],
standards: ["RFC 8572", "SZTP", "Cisco ZTP"],
steps: [
{
id: 1,
name: "Factory Configuration",
from: "manufacturer",
to: "device",
duration: "Offline",
description: "Device manufactured with bootstrap credentials and discovery URLs",
credentials: ["Bootstrap Certificate", "Vendor Root CA"],
secure: true,
attackSurface: "Supply chain compromise",
icon: "factory"
},
{
id: 2,
name: "Device Powers On",
from: "device",
to: "device",
duration: "< 1 sec",
description: "Device boots and enters discovery mode",
credentials: [],
secure: true,
attackSurface: "Physical tampering",
icon: "power"
},
{
id: 3,
name: "DHCP/DNS Discovery",
from: "device",
to: "network",
duration: "1-5 sec",
description: "Device queries DHCP option 143 or DNS SRV records for provisioning server",
credentials: [],
secure: false,
attackSurface: "DNS spoofing, DHCP hijacking",
icon: "search"
},
{
id: 4,
name: "TLS Connection",
from: "device",
to: "server",
duration: "2-5 sec",
description: "Device connects to provisioning server using bootstrap credentials",
credentials: ["Bootstrap Certificate", "Server Certificate"],
secure: true,
attackSurface: "Certificate validation bypass",
icon: "lock"
},
{
id: 5,
name: "Device Authentication",
from: "server",
to: "device",
duration: "1-3 sec",
description: "Server validates device identity using serial number and bootstrap cert",
credentials: ["Device Serial", "Bootstrap Certificate"],
secure: true,
attackSurface: "Credential theft",
icon: "verify"
},
{
id: 6,
name: "Configuration Download",
from: "server",
to: "device",
duration: "5-30 sec",
description: "Device receives operational certificates, configuration, and firmware",
credentials: ["Operational Certificate", "Cloud Credentials", "Config File"],
secure: true,
attackSurface: "Configuration injection",
icon: "download"
},
{
id: 7,
name: "Cloud Registration",
from: "device",
to: "cloud",
duration: "2-5 sec",
description: "Device registers with cloud platform using operational credentials",
credentials: ["Operational Certificate", "Device Token"],
secure: true,
attackSurface: "Registration hijacking",
icon: "cloud"
},
{
id: 8,
name: "Activation Complete",
from: "cloud",
to: "device",
duration: "< 1 sec",
description: "Device activated and begins normal operation",
credentials: ["Session Keys"],
secure: true,
attackSurface: "Session hijacking",
icon: "check"
}
]
},
{
id: "qrcode",
name: "QR Code / App-based",
shortName: "QR/App",
description: "User-assisted provisioning using mobile app and QR code",
complexity: 3,
security: 3,
friction: 3,
scalability: 3,
actors: ["Device", "Mobile App", "User", "Cloud Platform"],
useCases: ["Consumer IoT", "Smart home", "Retail products"],
standards: ["Matter", "HomeKit", "SmartThings"],
steps: [
{
id: 1,
name: "Device Setup Mode",
from: "device",
to: "device",
duration: "User action",
description: "User powers on device and initiates pairing mode (button press)",
credentials: [],
secure: true,
attackSurface: "Unauthorized pairing window",
icon: "power"
},
{
id: 2,
name: "QR Code Scan",
from: "user",
to: "app",
duration: "2-5 sec",
description: "User scans QR code on device containing setup payload",
credentials: ["Setup Code", "Device ID", "Vendor ID"],
secure: true,
attackSurface: "QR code cloning",
icon: "qrcode"
},
{
id: 3,
name: "BLE/Wi-Fi Discovery",
from: "app",
to: "device",
duration: "3-10 sec",
description: "App discovers device via BLE or soft-AP Wi-Fi",
credentials: ["Discriminator"],
secure: false,
attackSurface: "Nearby attacker interception",
icon: "wifi"
},
{
id: 4,
name: "PASE Session",
from: "app",
to: "device",
duration: "2-5 sec",
description: "Password-Authenticated Session Establishment using setup code",
credentials: ["Setup Code", "PASE Keys"],
secure: true,
attackSurface: "Brute force (mitigated by code entropy)",
icon: "key"
},
{
id: 5,
name: "Network Credentials",
from: "app",
to: "device",
duration: "1-3 sec",
description: "App sends Wi-Fi/Thread credentials to device",
credentials: ["Wi-Fi SSID/Password", "Thread Network Key"],
secure: true,
attackSurface: "Credential extraction from app",
icon: "network"
},
{
id: 6,
name: "Operational Certificate",
from: "app",
to: "device",
duration: "3-10 sec",
description: "App generates and installs operational certificate (NOC)",
credentials: ["Node Operational Certificate", "Fabric Credentials"],
secure: true,
attackSurface: "Certificate injection",
icon: "certificate"
},
{
id: 7,
name: "Cloud Registration",
from: "app",
to: "cloud",
duration: "2-5 sec",
description: "App registers device with cloud platform",
credentials: ["Device ID", "User Token"],
secure: true,
attackSurface: "Account takeover",
icon: "cloud"
},
{
id: 8,
name: "Setup Complete",
from: "cloud",
to: "device",
duration: "< 1 sec",
description: "Device paired and operational",
credentials: ["Session Keys"],
secure: true,
attackSurface: "None (secured)",
icon: "check"
}
]
},
{
id: "x509",
name: "Certificate-based (X.509)",
shortName: "X.509",
description: "Mutual TLS authentication using manufacturer certificates",
complexity: 4,
security: 5,
friction: 1,
scalability: 5,
actors: ["Device", "Manufacturer CA", "Cloud Platform", "Registration Authority"],
useCases: ["Critical infrastructure", "Healthcare", "Financial IoT"],
standards: ["X.509", "IEEE 802.1AR", "IDevID"],
steps: [
{
id: 1,
name: "Certificate Embedding",
from: "manufacturer",
to: "device",
duration: "Factory",
description: "Manufacturer embeds unique device certificate (IDevID) in secure element",
credentials: ["Device Certificate", "Private Key", "CA Chain"],
secure: true,
attackSurface: "Manufacturing compromise",
icon: "factory"
},
{
id: 2,
name: "Device Powers On",
from: "device",
to: "device",
duration: "< 1 sec",
description: "Device boots and loads certificate from secure element",
credentials: ["Device Certificate"],
secure: true,
attackSurface: "Secure element extraction",
icon: "power"
},
{
id: 3,
name: "DNS Resolution",
from: "device",
to: "network",
duration: "1-3 sec",
description: "Device resolves cloud endpoint from hardcoded or configured hostname",
credentials: [],
secure: false,
attackSurface: "DNS hijacking",
icon: "search"
},
{
id: 4,
name: "TLS Client Hello",
from: "device",
to: "cloud",
duration: "< 1 sec",
description: "Device initiates TLS handshake with client certificate",
credentials: ["Device Certificate"],
secure: true,
attackSurface: "Downgrade attack",
icon: "handshake"
},
{
id: 5,
name: "Server Certificate",
from: "cloud",
to: "device",
duration: "< 1 sec",
description: "Cloud presents server certificate, device validates chain",
credentials: ["Server Certificate", "Root CA"],
secure: true,
attackSurface: "Rogue server",
icon: "certificate"
},
{
id: 6,
name: "Client Certificate Validation",
from: "cloud",
to: "device",
duration: "1-5 sec",
description: "Cloud validates device certificate against manufacturer CA and CRL/OCSP",
credentials: ["Device Certificate", "Manufacturer CA", "CRL/OCSP"],
secure: true,
attackSurface: "Revocation check bypass",
icon: "verify"
},
{
id: 7,
name: "mTLS Established",
from: "device",
to: "cloud",
duration: "< 1 sec",
description: "Mutual TLS session established with perfect forward secrecy",
credentials: ["Session Keys", "ECDHE Parameters"],
secure: true,
attackSurface: "Side-channel attacks",
icon: "lock"
},
{
id: 8,
name: "Device Registration",
from: "device",
to: "cloud",
duration: "2-5 sec",
description: "Device registers identity and receives operational configuration",
credentials: ["Device Token", "Config"],
secure: true,
attackSurface: "Authorization bypass",
icon: "register"
}
]
},
{
id: "psk",
name: "Token-based (Pre-shared Keys)",
shortName: "PSK",
description: "Simple provisioning using pre-shared symmetric keys",
complexity: 1,
security: 2,
friction: 2,
scalability: 2,
actors: ["Device", "Admin Portal", "Cloud Platform"],
useCases: ["Prototypes", "Small deployments", "Cost-sensitive"],
standards: ["TLS-PSK", "DTLS-PSK", "CoAP"],
steps: [
{
id: 1,
name: "Key Generation",
from: "cloud",
to: "portal",
duration: "Admin action",
description: "Administrator generates device ID and pre-shared key in cloud portal",
credentials: ["Device ID", "Pre-shared Key"],
secure: true,
attackSurface: "Admin account compromise",
icon: "key"
},
{
id: 2,
name: "Key Programming",
from: "portal",
to: "device",
duration: "Manual",
description: "Key is programmed into device (serial, USB, or hardcoded)",
credentials: ["Device ID", "Pre-shared Key"],
secure: false,
attackSurface: "Key exposure during transfer",
icon: "program"
},
{
id: 3,
name: "Device Powers On",
from: "device",
to: "device",
duration: "< 1 sec",
description: "Device boots with pre-configured credentials",
credentials: ["Device ID", "Pre-shared Key"],
secure: true,
attackSurface: "Key extraction from device",
icon: "power"
},
{
id: 4,
name: "Connect to Cloud",
from: "device",
to: "cloud",
duration: "1-5 sec",
description: "Device connects to cloud endpoint",
credentials: [],
secure: false,
attackSurface: "Endpoint spoofing",
icon: "connect"
},
{
id: 5,
name: "PSK Authentication",
from: "device",
to: "cloud",
duration: "1-3 sec",
description: "TLS-PSK or DTLS-PSK handshake using pre-shared key",
credentials: ["Device ID", "Pre-shared Key"],
secure: true,
attackSurface: "Offline brute force",
icon: "lock"
},
{
id: 6,
name: "Session Established",
from: "cloud",
to: "device",
duration: "< 1 sec",
description: "Encrypted session established for data exchange",
credentials: ["Session Keys"],
secure: true,
attackSurface: "Key reuse across sessions",
icon: "check"
}
]
},
{
id: "manufacturer",
name: "Manufacturer Certificate",
shortName: "Mfr Cert",
description: "Vendor-managed certificate lifecycle with cloud integration",
complexity: 4,
security: 5,
friction: 1,
scalability: 5,
actors: ["Manufacturer", "Device", "Cloud Platform", "Certificate Authority"],
useCases: ["Branded ecosystems", "Vendor-controlled deployments"],
standards: ["AWS IoT", "Azure IoT Hub", "Google Cloud IoT"],
steps: [
{
id: 1,
name: "CA Registration",
from: "manufacturer",
to: "cloud",
duration: "One-time",
description: "Manufacturer registers CA certificate with cloud platform",
credentials: ["Manufacturer CA Certificate", "Verification Certificate"],
secure: true,
attackSurface: "CA private key compromise",
icon: "register"
},
{
id: 2,
name: "Device Manufacturing",
from: "manufacturer",
to: "device",
duration: "Factory",
description: "Each device receives unique certificate signed by manufacturer CA",
credentials: ["Device Certificate", "Private Key"],
secure: true,
attackSurface: "Manufacturing security",
icon: "factory"
},
{
id: 3,
name: "Device Provisioning Record",
from: "manufacturer",
to: "cloud",
duration: "Batch upload",
description: "Manufacturer uploads device serial/cert mapping to cloud",
credentials: ["Device Inventory", "Certificate Fingerprints"],
secure: true,
attackSurface: "Data tampering",
icon: "upload"
},
{
id: 4,
name: "Device First Boot",
from: "device",
to: "device",
duration: "< 1 sec",
description: "Device powers on with manufacturer certificate",
credentials: ["Device Certificate"],
secure: true,
attackSurface: "Physical tampering",
icon: "power"
},
{
id: 5,
name: "TLS Connection",
from: "device",
to: "cloud",
duration: "2-5 sec",
description: "Device connects with mutual TLS using manufacturer certificate",
credentials: ["Device Certificate", "Cloud Certificate"],
secure: true,
attackSurface: "Certificate validation",
icon: "lock"
},
{
id: 6,
name: "Certificate Chain Validation",
from: "cloud",
to: "device",
duration: "1-3 sec",
description: "Cloud validates certificate chain against registered manufacturer CA",
credentials: ["Certificate Chain", "CRL"],
secure: true,
attackSurface: "Revocation bypass",
icon: "verify"
},
{
id: 7,
name: "Auto-Registration",
from: "cloud",
to: "cloud",
duration: "1-5 sec",
description: "Cloud auto-creates device identity and applies policies",
credentials: ["Device Identity", "IoT Policies"],
secure: true,
attackSurface: "Policy misconfiguration",
icon: "register"
},
{
id: 8,
name: "Operational",
from: "cloud",
to: "device",
duration: "< 1 sec",
description: "Device fully operational with cloud platform",
credentials: ["Session Credentials"],
secure: true,
attackSurface: "None",
icon: "check"
}
]
},
{
id: "jitp",
name: "Just-in-Time Provisioning (JITP)",
shortName: "JITP",
description: "On-demand provisioning when device first connects",
complexity: 3,
security: 4,
friction: 1,
scalability: 4,
actors: ["Device", "Cloud Platform", "Provisioning Template", "Policy Engine"],
useCases: ["Dynamic fleets", "Multi-tenant platforms", "Flexible deployments"],
standards: ["AWS IoT JITP", "Azure DPS", "Cloud-native IoT"],
steps: [
{
id: 1,
name: "Template Configuration",
from: "admin",
to: "cloud",
duration: "One-time",
description: "Administrator configures provisioning template with device policies",
credentials: ["Template ID", "Policy Templates"],
secure: true,
attackSurface: "Template injection",
icon: "template"
},
{
id: 2,
name: "CA Registration",
from: "admin",
to: "cloud",
duration: "One-time",
description: "Register trusted CA for device certificate validation",
credentials: ["CA Certificate"],
secure: true,
attackSurface: "Unauthorized CA registration",
icon: "certificate"
},
{
id: 3,
name: "Device First Connection",
from: "device",
to: "cloud",
duration: "2-5 sec",
description: "Device connects for first time with bootstrap certificate",
credentials: ["Device Certificate"],
secure: true,
attackSurface: "Certificate theft",
icon: "connect"
},
{
id: 4,
name: "Certificate Validation",
from: "cloud",
to: "device",
duration: "1-3 sec",
description: "Cloud validates certificate against registered CA",
credentials: ["Certificate Chain"],
secure: true,
attackSurface: "Validation bypass",
icon: "verify"
},
{
id: 5,
name: "JITP Trigger",
from: "cloud",
to: "cloud",
duration: "< 1 sec",
description: "Unknown device triggers just-in-time provisioning flow",
credentials: ["Certificate CN/Subject"],
secure: true,
attackSurface: "Resource exhaustion",
icon: "trigger"
},
{
id: 6,
name: "Template Execution",
from: "cloud",
to: "cloud",
duration: "1-5 sec",
description: "Provisioning template creates Thing, certificates, and policies",
credentials: ["Thing Name", "Policy ARN"],
secure: true,
attackSurface: "Privilege escalation",
icon: "execute"
},
{
id: 7,
name: "Registration Complete",
from: "cloud",
to: "device",
duration: "< 1 sec",
description: "Device registered and authorized for operation",
credentials: ["Device Token", "MQTT Topics"],
secure: true,
attackSurface: "Overprivileged access",
icon: "register"
},
{
id: 8,
name: "Operational",
from: "device",
to: "cloud",
duration: "Immediate",
description: "Device begins normal operation with assigned identity",
credentials: ["Session Credentials"],
secure: true,
attackSurface: "None",
icon: "check"
}
]
}
]
// State Management
viewModel = ({
selectedMethod: provisioningMethods[0],
comparisonMethod: null,
currentStep: 0,
isPlaying: false,
animationSpeed: 1,
showDetails: true,
compareMode: false
})
// Selected method state
selectedMethodId = Inputs.select(
provisioningMethods.map(m => m.id),
{
label: "Provisioning Method",
format: id => provisioningMethods.find(m => m.id === id).name,
value: "ztp"
}
)
selectedMethod = provisioningMethods.find(m => m.id === selectedMethodId)
// Comparison method selector
comparisonMethodId = Inputs.select(
["none", ...provisioningMethods.filter(m => m.id !== selectedMethodId).map(m => m.id)],
{
label: "Compare With",
format: id => id === "none" ? "None (Single View)" : provisioningMethods.find(m => m.id === id)?.name || "None",
value: "none"
}
)
comparisonMethod = comparisonMethodId === "none" ? null : provisioningMethods.find(m => m.id === comparisonMethodId)
// Animation controls
animationSpeed = Inputs.range([0.5, 3], {
label: "Animation Speed",
value: 1,
step: 0.25
})
showTechnicalDetails = Inputs.toggle({
label: "Show Technical Details",
value: true
})
// Current step state
currentStepInput = Inputs.range([1, selectedMethod.steps.length], {
label: "Current Step",
value: 1,
step: 1
})
currentStep = currentStepInput - 1
// Animation player
isPlaying = Mutable(false)
playPauseButton = {
const button = html`<button type="button" aria-label="${isPlaying.value ? 'Pause animation' : 'Play animation'}" style="
padding: 10px 24px;
font-size: 14px;
font-weight: 600;
background: linear-gradient(135deg, ${ieeeColors.teal}, ${ieeeColors.navy});
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
">
<span style="font-size: 16px;">${isPlaying.value ? "βΈ" : "βΆ"}</span>
${isPlaying.value ? "Pause" : "Play Animation"}
</button>`;
button.onclick = () => {
isPlaying.value = !isPlaying.value;
};
return button;
}
// Step-through buttons
stepButtons = html`
<div style="display: flex; gap: 10px; align-items: center;">
<button type="button" aria-label="Previous step" onclick=${() => { if (currentStepInput > 1) currentStepInput--; }} style="
padding: 8px 16px;
background: ${ieeeColors.lightGray};
border: 1px solid ${ieeeColors.gray};
border-radius: 4px;
cursor: pointer;
font-weight: 500;
">β Previous</button>
<span style="font-weight: 600; color: ${ieeeColors.navy};">
Step ${currentStep + 1} of ${selectedMethod.steps.length}
</span>
<button type="button" aria-label="Next step" onclick=${() => { if (currentStepInput < selectedMethod.steps.length) currentStepInput++; }} style="
padding: 8px 16px;
background: ${ieeeColors.lightGray};
border: 1px solid ${ieeeColors.gray};
border-radius: 4px;
cursor: pointer;
font-weight: 500;
">Next β</button>
</div>
`
// Main visualization
mainVisualization = {
const width = 900;
const height = comparisonMethod ? 700 : 550;
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", "100%")
.attr("height", height)
.style("font-family", "system-ui, -apple-system, sans-serif")
.style("background", "white");
// Define arrow markers
const defs = svg.append("defs");
defs.append("marker")
.attr("id", "arrowhead-secure")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 8)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", ieeeColors.teal);
defs.append("marker")
.attr("id", "arrowhead-insecure")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 8)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", ieeeColors.orange);
// Gradient for secure channel
const gradient = defs.append("linearGradient")
.attr("id", "secure-gradient")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "100%")
.attr("y2", "0%");
gradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", ieeeColors.teal);
gradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", ieeeColors.navy);
// Draw single or comparison view
if (comparisonMethod) {
drawComparisonView(svg, width, height, selectedMethod, comparisonMethod, currentStep);
} else {
drawSingleView(svg, width, height, selectedMethod, currentStep);
}
return svg.node();
function drawSingleView(svg, width, height, method, currentStep) {
// Title
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.attr("font-size", "18px")
.attr("font-weight", "700")
.attr("fill", ieeeColors.navy)
.text(method.name);
// Swimlane setup
const actors = method.actors;
const laneWidth = (width - 100) / actors.length;
const laneStartY = 60;
const laneHeight = height - 100;
// Draw swimlanes
actors.forEach((actor, i) => {
const x = 50 + i * laneWidth;
// Lane background
svg.append("rect")
.attr("x", x)
.attr("y", laneStartY)
.attr("width", laneWidth)
.attr("height", laneHeight)
.attr("fill", i % 2 === 0 ? ieeeColors.lightGray : "white")
.attr("stroke", ieeeColors.gray)
.attr("stroke-width", 0.5);
// Actor header
svg.append("rect")
.attr("x", x)
.attr("y", laneStartY)
.attr("width", laneWidth)
.attr("height", 35)
.attr("fill", ieeeColors.navy);
svg.append("text")
.attr("x", x + laneWidth / 2)
.attr("y", laneStartY + 22)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("font-weight", "600")
.attr("fill", "white")
.text(actor);
});
// Draw steps
const stepHeight = (laneHeight - 60) / method.steps.length;
method.steps.forEach((step, i) => {
const y = laneStartY + 50 + i * stepHeight;
const isActive = i === currentStep;
const isPast = i < currentStep;
const isFuture = i > currentStep;
// Step number circle
const stepX = 25;
svg.append("circle")
.attr("cx", stepX)
.attr("cy", y + 15)
.attr("r", 12)
.attr("fill", isActive ? ieeeColors.teal : isPast ? ieeeColors.navy : ieeeColors.lightGray)
.attr("stroke", isActive ? ieeeColors.navy : "none")
.attr("stroke-width", 2);
svg.append("text")
.attr("x", stepX)
.attr("y", y + 19)
.attr("text-anchor", "middle")
.attr("font-size", "10px")
.attr("font-weight", "600")
.attr("fill", isFuture ? ieeeColors.gray : "white")
.text(step.id);
// Get actor positions
const fromActor = getActorIndex(step.from, actors);
const toActor = getActorIndex(step.to, actors);
const fromX = 50 + fromActor * laneWidth + laneWidth / 2;
const toX = 50 + toActor * laneWidth + laneWidth / 2;
// Draw message flow
if (fromActor !== toActor) {
const arrowOffset = fromX < toX ? -15 : 15;
svg.append("line")
.attr("x1", fromX + (fromX < toX ? 20 : -20))
.attr("y1", y + 15)
.attr("x2", toX + arrowOffset)
.attr("y2", y + 15)
.attr("stroke", step.secure ? ieeeColors.teal : ieeeColors.orange)
.attr("stroke-width", isActive ? 3 : isPast ? 2 : 1)
.attr("stroke-dasharray", step.secure ? "none" : "5,3")
.attr("marker-end", `url(#arrowhead-${step.secure ? "secure" : "insecure"})`)
.attr("opacity", isFuture ? 0.3 : 1);
// Channel indicator
if (isActive && showTechnicalDetails) {
svg.append("text")
.attr("x", (fromX + toX) / 2)
.attr("y", y + 5)
.attr("text-anchor", "middle")
.attr("font-size", "8px")
.attr("fill", step.secure ? ieeeColors.teal : ieeeColors.orange)
.text(step.secure ? "Secure" : "Insecure");
}
} else {
// Self-referencing action
svg.append("ellipse")
.attr("cx", fromX)
.attr("cy", y + 15)
.attr("rx", 25)
.attr("ry", 12)
.attr("fill", "none")
.attr("stroke", isActive ? ieeeColors.teal : isPast ? ieeeColors.navy : ieeeColors.gray)
.attr("stroke-width", isActive ? 2 : 1)
.attr("opacity", isFuture ? 0.3 : 1);
}
// Step icon/symbol based on type
const iconX = fromActor === toActor ? fromX : (fromX + toX) / 2;
const icons = {
factory: "F",
power: "P",
search: "S",
lock: "L",
verify: "V",
download: "D",
cloud: "C",
check: "OK",
qrcode: "QR",
wifi: "W",
key: "K",
network: "N",
certificate: "Ct",
handshake: "H",
register: "R",
program: "Pg",
connect: "Cn",
upload: "U",
template: "T",
trigger: "Tr",
execute: "E"
};
if (isActive && !isFuture) {
svg.append("text")
.attr("x", iconX)
.attr("y", y + 35)
.attr("text-anchor", "middle")
.attr("font-size", "10px")
.attr("font-weight", "bold")
.attr("fill", ieeeColors.navy)
.text(icons[step.icon] || "β");
}
});
// Current step details panel
const detailY = laneStartY + laneHeight - 80;
const currentStepData = method.steps[currentStep];
// Details background
svg.append("rect")
.attr("x", 50)
.attr("y", detailY)
.attr("width", width - 100)
.attr("height", 75)
.attr("rx", 8)
.attr("fill", "white")
.attr("stroke", ieeeColors.teal)
.attr("stroke-width", 2);
// Step name
svg.append("text")
.attr("x", 65)
.attr("y", detailY + 20)
.attr("font-size", "14px")
.attr("font-weight", "700")
.attr("fill", ieeeColors.navy)
.text(`Step ${currentStep + 1}: ${currentStepData.name}`);
// Duration
svg.append("text")
.attr("x", width - 65)
.attr("y", detailY + 20)
.attr("text-anchor", "end")
.attr("font-size", "12px")
.attr("fill", ieeeColors.gray)
.text(`Duration: ${currentStepData.duration}`);
// Description
svg.append("text")
.attr("x", 65)
.attr("y", detailY + 40)
.attr("font-size", "11px")
.attr("fill", ieeeColors.darkGray)
.text(currentStepData.description);
// Credentials
if (showTechnicalDetails && currentStepData.credentials.length > 0) {
svg.append("text")
.attr("x", 65)
.attr("y", detailY + 58)
.attr("font-size", "10px")
.attr("fill", ieeeColors.teal)
.text(`Credentials: ${currentStepData.credentials.join(", ")}`);
}
// Security indicator
svg.append("rect")
.attr("x", 65)
.attr("y", detailY + 62)
.attr("width", 8)
.attr("height", 8)
.attr("rx", 2)
.attr("fill", currentStepData.secure ? ieeeColors.green : ieeeColors.orange);
svg.append("text")
.attr("x", 78)
.attr("y", detailY + 70)
.attr("font-size", "9px")
.attr("fill", ieeeColors.gray)
.text(currentStepData.secure ? "Secure Channel" : "Insecure Channel");
}
function drawComparisonView(svg, width, height, method1, method2, currentStep) {
const halfWidth = width / 2 - 20;
// Method 1 (left)
const g1 = svg.append("g").attr("transform", "translate(0, 0)");
drawCompactView(g1, halfWidth, height, method1, currentStep, 10);
// Divider
svg.append("line")
.attr("x1", width / 2)
.attr("y1", 20)
.attr("x2", width / 2)
.attr("y2", height - 20)
.attr("stroke", ieeeColors.gray)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "5,5");
// Method 2 (right)
const g2 = svg.append("g").attr("transform", `translate(${width / 2 + 10}, 0)`);
drawCompactView(g2, halfWidth, height, method2, Math.min(currentStep, method2.steps.length - 1), 0);
}
function drawCompactView(g, width, height, method, currentStep, offsetX) {
// Title
g.append("text")
.attr("x", width / 2 + offsetX)
.attr("y", 25)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.attr("font-weight", "700")
.attr("fill", ieeeColors.navy)
.text(method.shortName);
// Steps list
const stepHeight = Math.min(50, (height - 100) / method.steps.length);
const startY = 50;
method.steps.forEach((step, i) => {
const y = startY + i * stepHeight;
const isActive = i === currentStep;
const isPast = i < currentStep;
// Step indicator
g.append("circle")
.attr("cx", 25 + offsetX)
.attr("cy", y + 12)
.attr("r", 8)
.attr("fill", isActive ? ieeeColors.teal : isPast ? ieeeColors.navy : ieeeColors.lightGray);
g.append("text")
.attr("x", 25 + offsetX)
.attr("y", y + 16)
.attr("text-anchor", "middle")
.attr("font-size", "9px")
.attr("font-weight", "600")
.attr("fill", "white")
.text(step.id);
// Step name
g.append("text")
.attr("x", 45 + offsetX)
.attr("y", y + 16)
.attr("font-size", "10px")
.attr("font-weight", isActive ? "600" : "400")
.attr("fill", isActive ? ieeeColors.navy : ieeeColors.darkGray)
.text(step.name.substring(0, 25) + (step.name.length > 25 ? "..." : ""));
// Security indicator
g.append("circle")
.attr("cx", width - 15 + offsetX)
.attr("cy", y + 12)
.attr("r", 5)
.attr("fill", step.secure ? ieeeColors.green : ieeeColors.orange);
// Duration
if (isActive) {
g.append("text")
.attr("x", width - 30 + offsetX)
.attr("y", y + 16)
.attr("text-anchor", "end")
.attr("font-size", "8px")
.attr("fill", ieeeColors.gray)
.text(step.duration);
}
});
// Current step detail
const detailY = height - 120;
const step = method.steps[currentStep];
g.append("rect")
.attr("x", 10 + offsetX)
.attr("y", detailY)
.attr("width", width - 20)
.attr("height", 100)
.attr("rx", 6)
.attr("fill", ieeeColors.lightGray)
.attr("stroke", ieeeColors.teal)
.attr("stroke-width", 1);
g.append("text")
.attr("x", 20 + offsetX)
.attr("y", detailY + 18)
.attr("font-size", "11px")
.attr("font-weight", "600")
.attr("fill", ieeeColors.navy)
.text(step.name);
// Wrap description
const words = step.description.split(" ");
let line = "";
let lineNum = 0;
const maxWidth = 35;
words.forEach(word => {
if ((line + " " + word).length > maxWidth) {
g.append("text")
.attr("x", 20 + offsetX)
.attr("y", detailY + 35 + lineNum * 12)
.attr("font-size", "9px")
.attr("fill", ieeeColors.darkGray)
.text(line);
line = word;
lineNum++;
} else {
line = line ? line + " " + word : word;
}
});
if (line) {
g.append("text")
.attr("x", 20 + offsetX)
.attr("y", detailY + 35 + lineNum * 12)
.attr("font-size", "9px")
.attr("fill", ieeeColors.darkGray)
.text(line);
}
// Attack surface
g.append("text")
.attr("x", 20 + offsetX)
.attr("y", detailY + 88)
.attr("font-size", "8px")
.attr("fill", ieeeColors.orange)
.text(`Attack: ${step.attackSurface}`);
}
function getActorIndex(actorName, actors) {
const mapping = {
"device": 0,
"manufacturer": 0,
"network": 1,
"server": 2,
"cloud": actors.length - 1,
"user": 2,
"app": 1,
"portal": 1,
"admin": 0
};
const normalizedName = actorName.toLowerCase();
if (mapping.hasOwnProperty(normalizedName)) {
return Math.min(mapping[normalizedName], actors.length - 1);
}
return 0;
}
}
// Flow Timeline View
flowTimeline = {
const method = selectedMethod;
const totalTime = method.steps.reduce((acc, step) => {
const time = parseTime(step.duration);
return acc + time;
}, 0);
function parseTime(duration) {
if (duration.includes("sec")) {
const match = duration.match(/(\d+)/);
return match ? parseInt(match[1]) : 3;
}
if (duration.includes("Offline") || duration.includes("Factory") || duration.includes("Manual")) {
return 0;
}
return 2;
}
return html`
<div style="
background: white;
border: 1px solid ${ieeeColors.gray};
border-radius: 12px;
padding: 20px;
margin-top: 20px;
">
<h3 style="margin: 0 0 15px 0; color: ${ieeeColors.navy}; font-size: 16px;">
Provisioning Timeline
</h3>
<div style="position: relative; padding: 20px 0;">
<!-- Timeline bar -->
<div style="
position: absolute;
top: 50%;
left: 40px;
right: 40px;
height: 4px;
background: ${ieeeColors.lightGray};
transform: translateY(-50%);
border-radius: 2px;
"></div>
<!-- Progress bar -->
<div style="
position: absolute;
top: 50%;
left: 40px;
width: ${((currentStep + 1) / method.steps.length) * (100 - 8)}%;
height: 4px;
background: linear-gradient(90deg, ${ieeeColors.teal}, ${ieeeColors.navy});
transform: translateY(-50%);
border-radius: 2px;
transition: width 0.3s ease;
"></div>
<!-- Step markers -->
<div style="display: flex; justify-content: space-between; padding: 0 20px;">
${method.steps.map((step, i) => html`
<div style="
display: flex;
flex-direction: column;
align-items: center;
z-index: 1;
">
<div style="
width: ${i === currentStep ? 24 : 16}px;
height: ${i === currentStep ? 24 : 16}px;
border-radius: 50%;
background: ${i <= currentStep ? ieeeColors.teal : ieeeColors.lightGray};
border: 3px solid ${i === currentStep ? ieeeColors.navy : "transparent"};
display: flex;
align-items: center;
justify-content: center;
font-size: ${i === currentStep ? 10 : 8}px;
font-weight: 600;
color: ${i <= currentStep ? "white" : ieeeColors.gray};
transition: all 0.3s ease;
box-shadow: ${i === currentStep ? `0 0 10px ${ieeeColors.teal}` : "none"};
">
${i + 1}
</div>
<div style="
margin-top: 8px;
font-size: 9px;
color: ${i === currentStep ? ieeeColors.navy : ieeeColors.gray};
font-weight: ${i === currentStep ? 600 : 400};
text-align: center;
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
">
${step.name.split(" ")[0]}
</div>
<div style="
font-size: 8px;
color: ${ieeeColors.gray};
margin-top: 2px;
">
${step.duration}
</div>
</div>
`)}
</div>
</div>
<!-- Estimated total time -->
<div style="
text-align: center;
margin-top: 10px;
font-size: 12px;
color: ${ieeeColors.darkGray};
">
Estimated Total Time: <strong style="color: ${ieeeColors.navy};">
${totalTime > 60 ? `${Math.round(totalTime / 60)} min` : `${totalTime} sec`}
</strong> (online steps only)
</div>
</div>
`;
}
```