Complete local deployment setup with Cloudflare tunnel

- setup_local_hosting.sh: Configures Nginx reverse proxy, installs cloudflared
- SETUP_CLOUDFLARE_TUNNEL.sh: Interactive tunnel setup script (5 min)
- check_status.sh: Real-time status dashboard for all services
- LOCAL_ACCESS_GUIDE.md: Complete local access instructions
- FINAL_DEPLOYMENT_README.md: Comprehensive deployment guide

Current Status:
   Nginx reverse proxy running (port 80)
   Backend API healthy (port 8000)
   Frontend running (port 3000, redirecting unauthenticated to login)
   PostgreSQL database connected with demo data
   All services accessible at http://10.30.20.38
   Cloudflare tunnel installed and ready

Access:
  - Local: http://10.30.20.38
  - With Cloudflare: https://your-domain.com (after tunnel setup)
  - Demo credentials included and working

Next Steps:
  1. Visit http://10.30.20.38 and login
  2. Run SETUP_CLOUDFLARE_TUNNEL.sh for global access
  3. Share HTTPS URL with anyone

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
drjones
2026-07-07 14:06:47 +00:00
parent c16272b8b5
commit d92a7c057d
12 changed files with 1592 additions and 38 deletions

View File

@@ -0,0 +1,252 @@
"use client";
import { useRef, useEffect, useState } from "react";
export interface AttackNode {
id: string;
label: string;
type: "attacker" | "entry_point" | "pivot" | "target";
risk_level: "none" | "low" | "medium" | "high" | "critical";
}
export interface AttackEdge {
source: string;
target: string;
}
interface AttackPathVisualizerProps {
nodes: AttackNode[];
edges: AttackEdge[];
narrative?: string;
}
const getNodeColors = (type: string) => {
const colors: Record<string, { bg: string; border: string; text: string }> = {
attacker: {
bg: "bg-red-950/60",
border: "border-red-700/60",
text: "text-red-300",
},
entry_point: {
bg: "bg-orange-950/60",
border: "border-orange-700/60",
text: "text-orange-300",
},
pivot: {
bg: "bg-amber-950/60",
border: "border-amber-700/60",
text: "text-amber-300",
},
target: {
bg: "bg-blue-950/60",
border: "border-blue-700/60",
text: "text-blue-300",
},
};
return colors[type] || { bg: "bg-vault-dark", border: "border-vault-border", text: "text-vault-muted" };
};
const getRiskColor = (level: string) => {
const colors: Record<string, string> = {
none: "#6b7280",
low: "#3b82f6",
medium: "#f59e0b",
high: "#ef4444",
critical: "#dc2626",
};
return colors[level] || "#6b7280";
};
export default function AttackPathVisualizer({
nodes,
edges,
narrative,
}: AttackPathVisualizerProps) {
const svgRef = useRef<SVGSVGElement>(null);
const [hoveredNode, setHoveredNode] = useState<string | null>(null);
if (!nodes || nodes.length === 0) {
return (
<div className="text-vault-muted text-sm">
No attack path data available
</div>
);
}
// Calculate positions for linear layout (left to right)
const padding = 40;
const nodeWidth = 120;
const nodeHeight = 80;
const svgWidth = nodes.length * (nodeWidth + 60) + padding * 2;
const svgHeight = 200;
const nodePositions: Record<string, { x: number; y: number }> = {};
nodes.forEach((node, index) => {
nodePositions[node.id] = {
x: padding + index * (nodeWidth + 60),
y: svgHeight / 2 - nodeHeight / 2,
};
});
return (
<div className="w-full">
{narrative && (
<div className="bg-vault-dark/40 border border-vault-sapphireDim/30 rounded-lg p-3 mb-4">
<p className="text-xs text-vault-sapphireLight font-medium mb-1">
Attack Narrative
</p>
<p className="text-vault-subtle text-sm leading-relaxed">{narrative}</p>
</div>
)}
<div className="bg-vault-dark/20 border border-vault-border/20 rounded-lg p-2 overflow-x-auto">
<svg
ref={svgRef}
width={svgWidth}
height={svgHeight}
className="min-w-full"
style={{ display: "block" }}
>
{/* Edges/Arrows */}
<defs>
<marker
id="arrowhead"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto"
>
<polygon points="0 0, 10 3, 0 6" fill="#3b82d4" />
</marker>
</defs>
{edges.map((edge, idx) => {
const source = nodePositions[edge.source];
const target = nodePositions[edge.target];
if (!source || !target) return null;
const x1 = source.x + nodeWidth / 2;
const y1 = source.y + nodeHeight / 2;
const x2 = target.x - nodeWidth / 2;
const y2 = target.y + nodeHeight / 2;
return (
<line
key={`edge-${idx}`}
x1={x1}
y1={y1}
x2={x2}
y2={y2}
stroke="#3b82d4"
strokeWidth="2"
markerEnd="url(#arrowhead)"
opacity={hoveredNode ? 0.3 : 0.6}
style={{ transition: "opacity 0.2s" }}
/>
);
})}
{/* Nodes */}
{nodes.map((node) => {
const pos = nodePositions[node.id];
if (!pos) return null;
const colors = getNodeColors(node.type);
const isHovered = hoveredNode === node.id;
return (
<g key={node.id}>
{/* Node background */}
<rect
x={pos.x - nodeWidth / 2}
y={pos.y - nodeHeight / 2}
width={nodeWidth}
height={nodeHeight}
rx="6"
className={`${colors.bg} ${colors.border}`}
stroke={getRiskColor(node.risk_level)}
strokeWidth={isHovered ? "2" : "1"}
opacity={hoveredNode && !isHovered ? 0.4 : 1}
style={{
cursor: "pointer",
transition: "opacity 0.2s, stroke-width 0.2s",
}}
onMouseEnter={() => setHoveredNode(node.id)}
onMouseLeave={() => setHoveredNode(null)}
/>
{/* Node label */}
<text
x={pos.x}
y={pos.y - 12}
textAnchor="middle"
className={colors.text}
fontSize="12"
fontWeight="600"
pointerEvents="none"
>
{node.label}
</text>
{/* Node type */}
<text
x={pos.x}
y={pos.y + 8}
textAnchor="middle"
fill="#94a3b8"
fontSize="10"
pointerEvents="none"
fontStyle="italic"
>
{node.type.replace(/_/g, " ")}
</text>
{/* Risk level indicator */}
<circle
cx={pos.x + nodeWidth / 2 - 8}
cy={pos.y - nodeHeight / 2 + 8}
r="5"
fill={getRiskColor(node.risk_level)}
opacity="0.8"
pointerEvents="none"
/>
</g>
);
})}
</svg>
</div>
{/* Legend */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-4 text-xs">
{["attacker", "entry_point", "pivot", "target"].map((type) => {
const colors = getNodeColors(type);
return (
<div key={type} className="flex items-center gap-2">
<div
className={`w-3 h-3 rounded ${colors.bg} border ${colors.border}`}
/>
<span className="text-vault-muted">{type.replace(/_/g, " ")}</span>
</div>
);
})}
</div>
{/* Risk level legend */}
<div className="mt-3 pt-3 border-t border-vault-border/20">
<p className="text-xs text-vault-muted font-medium mb-2">Risk Level</p>
<div className="flex flex-wrap gap-3">
{["none", "low", "medium", "high", "critical"].map((level) => (
<div key={level} className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: getRiskColor(level) }}
/>
<span className="text-vault-muted text-xs capitalize">{level}</span>
</div>
))}
</div>
</div>
</div>
);
}