diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts new file mode 100644 index 0000000..7eb5d38 --- /dev/null +++ b/app/api/chat/route.ts @@ -0,0 +1,235 @@ +import { NextRequest, NextResponse } from "next/server"; +import { Ollama } from "ollama"; +import { z } from "zod"; + +// Initialize Ollama client +const ollama = new Ollama({ + host: "http://localhost:11434", +}); + +// Define response schemas for structured output +const CommandResponseSchema = { + type: "object", + properties: { + type: { + type: "string", + enum: ["command", "explanation", "error"], + }, + content: { + type: "string", + }, + commands: { + type: "array", + items: { + type: "object", + properties: { + action: { type: "string" }, + target: { type: "string" }, + parameters: { + type: "object", + additionalProperties: true, + }, + description: { type: "string" }, + }, + required: ["action", "description"], + }, + }, + networkChanges: { + type: "object", + properties: { + nodes: { + type: "array", + items: { type: "object" }, + }, + connections: { + type: "array", + items: { type: "object" }, + }, + rules: { + type: "array", + items: { type: "string" }, + }, + }, + }, + }, + required: ["type", "content"], +}; + +const TextResponseSchema = { + type: "object", + properties: { + type: { type: "string", enum: ["text"] }, + content: { type: "string" }, + }, + required: ["type", "content"], +}; + +export async function POST(request: NextRequest) { + try { + const { message, context } = await request.json(); + + // Determine if this is a command request or general query + const commandKeywords = [ + "command", + "execute", + "run", + "apply", + "block", + "allow", + "rule", + "iptables", + "add", + "remove", + "delete", + "create", + "configure", + "set", + "enable", + "disable", + "drop", + "accept", + "reject", + "forward", + "input", + "output", + "chain", + "table", + "flush", + "policy", + "insert", + "append", + "firewall", + "route", + "redirect", + "nat", + "masquerade", + "port", + "protocol", + "tcp", + "udp", + "icmp", + "ssh", + "http", + "https", + "connect", + "disconnect", + "link", + "unlink", + "bridge", + "subnet", + "vlan", + "interface", + ]; + + const isCommandRequest = commandKeywords.some((keyword) => + message.toLowerCase().includes(keyword) + ); + + if (isCommandRequest) { + // Generate structured response for commands + const prompt = `You are an expert network security assistant for LLMGuard. + +Current network context: ${JSON.stringify(context)} + +Analyze the request and provide a structured response with specific commands that can be executed on the network topology. Focus on iptables rules, network configuration, or topology changes. +After generating the request, generate a quick summary of the changes being made. + +For iptables commands, provide the exact command syntax. +For network changes, specify node IDs and connection modifications. +Always include a clear description of what each command does. + +Respond in JSON format with: +- type: "command" +- content: A brief explanation of what you're doing +- commands: Array of command objects with action, target, parameters, and description + +Example response format: +{ + "type": "command", + "content": "I'll create iptables rules to block traffic between the specified nodes.", + "commands": [ + { + "action": "iptables", + "target": "fw-1", + "parameters": { + "rule": "iptables -A FORWARD -s 172.20.3.10 -d 172.20.3.11 -j DROP" + }, + "description": "Block traffic from user-1 to user-2" + } + ] +}`; + + const response = await ollama.generate({ + model: "qwen3:4b", + prompt: prompt, + system: message, + format: CommandResponseSchema, + options: { + temperature: 0.1, + }, + }); + + try { + const parsedResponse = JSON.parse(response.response); + + return NextResponse.json({ + success: true, + data: parsedResponse, + type: "structured", + }); + } catch (parseError) { + // Fallback to text response if JSON parsing fails + return NextResponse.json({ + success: true, + data: { + type: "explanation", + content: response.response, + }, + type: "text", + }); + } + } else { + // Use regular text generation for explanations + const prompt = `You are an expert network security assistant for LLMGuard. + + Current network context: ${JSON.stringify(context)} + +Provide a helpful, detailed explanation about network security, firewall rules, or topology analysis. Be specific and technical when appropriate. Keep responses concise but informative. +Be friendly but still professional. +`; + + const response = await ollama.chat({ + model: "qwen3:4b", + messages: [ + { role: "system", content: prompt }, + { role: "user", content: message }, + ], + options: { + temperature: 0.3, + }, + format: TextResponseSchema, + }); + + return NextResponse.json({ + success: true, + data: { + type: "explanation", + content: JSON.parse(response.message.content).content, + }, + type: "text", + }); + } + } catch (error) { + console.error("LLM API Error:", error); + return NextResponse.json( + { + success: false, + error: + error instanceof Error + ? error.message + : "Failed to process request", + }, + { status: 500 } + ); + } +} diff --git a/app/api/execute-command/route.ts b/app/api/execute-command/route.ts new file mode 100644 index 0000000..7916e38 --- /dev/null +++ b/app/api/execute-command/route.ts @@ -0,0 +1,129 @@ +import { NextRequest, NextResponse } from "next/server"; +import { exec } from "child_process"; +import { promisify } from "util"; + +const execAsync = promisify(exec); + +export async function POST(request: NextRequest) { + try { + const { commands, nodeId } = await request.json(); + + if (!commands || !Array.isArray(commands)) { + return NextResponse.json( + { + success: false, + error: "Invalid commands format", + }, + { status: 400 } + ); + } + + const results = []; + + for (const command of commands) { + try { + let dockerCommand = ""; + + if (command.action === "iptables") { + // Execute iptables command in specific container + const targetNode = nodeId || command.target; + if (!targetNode) { + results.push({ + command: command.action, + success: false, + error: "No target node specified for iptables command", + }); + continue; + } + + dockerCommand = `docker exec ${targetNode} ${command.parameters.rule}`; + } else if (command.action === "network_config") { + // Execute network configuration + const targetNode = nodeId || command.target; + if (!targetNode) { + results.push({ + command: command.action, + success: false, + error: "No target node specified for network config", + }); + continue; + } + + dockerCommand = `docker exec ${targetNode} ${command.parameters.command}`; + } else if (command.action === "ping_test") { + // Execute ping test + const targetNode = nodeId || command.target; + if (!targetNode) { + results.push({ + command: command.action, + success: false, + error: "No target node specified for ping test", + }); + continue; + } + + dockerCommand = `docker exec ${targetNode} ping -c 3 ${command.parameters.destination}`; + } else if (command.action === "topology_change") { + // Handle topology changes (would require updating state.json and reinitializing) + results.push({ + command: command.action, + success: false, + message: + "Topology changes require manual approval and system restart", + }); + continue; + } else { + // Generic docker exec command + const targetNode = nodeId || command.target; + if (!targetNode) { + results.push({ + command: command.action, + success: false, + error: "No target node specified", + }); + continue; + } + + dockerCommand = `docker exec ${targetNode} ${ + command.parameters?.command || command.action + }`; + } + + if (dockerCommand) { + const { stdout, stderr } = await execAsync(dockerCommand); + results.push({ + command: command.action, + description: command.description, + target: command.target, + success: true, + output: stdout.trim(), + error: stderr.trim() || null, + }); + } + } catch (error: any) { + console.error(`Command execution failed:`, error); + results.push({ + command: command.action, + description: command.description, + target: command.target, + success: false, + error: error.message || "Unknown error occurred", + }); + } + } + + return NextResponse.json({ + success: true, + results, + }); + } catch (error) { + console.error("Execute command API error:", error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }, + { status: 500 } + ); + } +} diff --git a/app/api/network-state/route.ts b/app/api/network-state/route.ts new file mode 100644 index 0000000..2375a95 --- /dev/null +++ b/app/api/network-state/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from "next/server"; +import { readFile } from "fs/promises"; +import path from "path"; + +export async function GET(request: NextRequest) { + try { + // Read the state.json file from the public directory + const statePath = path.join( + process.cwd(), + "public", + "simplified_state.json" + ); + const stateContent = await readFile(statePath, "utf-8"); + const networkState = JSON.parse(stateContent); + + return NextResponse.json({ + success: true, + data: networkState, + }); + } catch (error) { + console.error("Error reading network state:", error); + + // Return a default/empty state if file doesn't exist + return NextResponse.json({ + success: false, + error: "Failed to load network state", + data: { + network: { + name: "Default Network", + nodes: [], + connections: [], + }, + }, + }); + } +} diff --git a/app/components/ChatInterface.tsx b/app/components/ChatInterface.tsx index c178995..3cc142d 100644 --- a/app/components/ChatInterface.tsx +++ b/app/components/ChatInterface.tsx @@ -1,34 +1,40 @@ "use client"; import { AnimatePresence, motion } from "framer-motion"; -import { Bot, Loader2, Send, Trash2, User } from "lucide-react"; +import { Bot, Loader2, Play, Send, Terminal, Trash2, User } from "lucide-react"; import React, { useRef, useState } from "react"; +import ReactMarkdown from "react-markdown"; interface Message { id: string; type: "user" | "assistant"; content: string; timestamp: Date; + data?: any; // For structured responses + commandResults?: any[]; // For command execution results } interface ChatInterfaceProps { onMessageSend?: (message: string) => void; + networkContext?: any; // Current network state } export const ChatInterface: React.FC = ({ onMessageSend, + networkContext, }) => { const [messages, setMessages] = useState([ { id: "1", type: "assistant", content: - "Welcome to LLM Guard! I can help you analyze network security, explain firewall rules, and provide insights about your network topology. What would you like to know?", + "Welcome to LLM Guard! I'll be your personal assistant. What would you like to know?", timestamp: new Date(), }, ]); const [inputValue, setInputValue] = useState(""); const [isLoading, setIsLoading] = useState(false); + const [isExecuting, setIsExecuting] = useState(false); const messagesEndRef = useRef(null); const scrollToBottom = () => { @@ -50,23 +56,51 @@ export const ChatInterface: React.FC = ({ }; setMessages((prev) => [...prev, userMessage]); + const currentInput = inputValue; setInputValue(""); setIsLoading(true); - // Call the callback if provided - onMessageSend?.(inputValue); + onMessageSend?.(currentInput); - // Simulate LLM response (replace with actual LLM integration later) - setTimeout(() => { - const assistantMessage: Message = { + try { + // Send to LLM API + const response = await fetch("/api/chat", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + message: currentInput, + context: networkContext, + }), + }); + + const result = await response.json(); + + if (result.success) { + const assistantMessage: Message = { + id: (Date.now() + 1).toString(), + type: "assistant", + content: result.data.content, + timestamp: new Date(), + data: result.data, + }; + + setMessages((prev) => [...prev, assistantMessage]); + } else { + throw new Error(result.error || "Failed to get response"); + } + } catch (error: any) { + const errorMessage: Message = { id: (Date.now() + 1).toString(), type: "assistant", - content: `I received your query: "${userMessage.content}". This is a simulated response. In the actual implementation, this would be processed by the LLM and provide real network security insights.`, + content: `Error: ${error.message}`, timestamp: new Date(), }; - setMessages((prev) => [...prev, assistantMessage]); + setMessages((prev) => [...prev, errorMessage]); + } finally { setIsLoading(false); - }, 1500); + } }; const handleKeyPress = (e: React.KeyboardEvent) => { @@ -76,6 +110,37 @@ export const ChatInterface: React.FC = ({ } }; + const executeCommands = async (messageId: string, commands: any[]) => { + setIsExecuting(true); + + try { + const response = await fetch("/api/execute-command", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + commands: commands, + }), + }); + + const result = await response.json(); + + // Update the message with execution results + setMessages((prev) => + prev.map((msg) => + msg.id === messageId + ? { ...msg, commandResults: result.results } + : msg + ) + ); + } catch (error) { + console.error("Command execution failed:", error); + } finally { + setIsExecuting(false); + } + }; + const clearChat = () => { setMessages([ { @@ -88,6 +153,147 @@ export const ChatInterface: React.FC = ({ ]); }; + const renderMessage = (message: Message) => { + const hasCommands = + message.data?.commands && message.data.commands.length > 0; + + return ( + + {message.type === "assistant" && ( +
+ +
+ )} + +
+
+ + {message.content + .replace(/[\s\S]*?<\/think>/g, "") + .trim()} + +
+ + {/* Command display and execution */} + {hasCommands && ( +
+
+ + + Commands Found + + +
+ + {message.data.commands.map( + (cmd: any, idx: number) => ( +
+
+
+ {cmd.description} +
+
+ {cmd.action} + {cmd.target + ? ` (${cmd.target})` + : ""} +
+ {cmd.parameters?.rule && ( +
+ {cmd.parameters.rule} +
+ )} +
+
+ ) + )} +
+ )} + + {/* Command results display */} + {message.commandResults && ( +
+ + Execution Results: + + {message.commandResults.map( + (result: any, idx: number) => ( +
+
+ {result.command}:{" "} + {result.success + ? "Success" + : "Failed"} + {result.output && ( +
+ {result.output} +
+ )} + {result.error && ( +
+ {result.error} +
+ )} +
+
+ ) + )} +
+ )} + + + {message.timestamp.toLocaleTimeString()} + +
+ + {message.type === "user" && ( +
+ +
+ )} +
+ ); + }; + return (
{/* Header */} @@ -109,49 +315,7 @@ export const ChatInterface: React.FC = ({ {/* Messages */}
- - {messages.map((message) => ( - - {message.type === "assistant" && ( -
- -
- )} - -
-

- {message.content} -

- - {message.timestamp.toLocaleTimeString()} - -
- - {message.type === "user" && ( -
- -
- )} -
- ))} -
+ {messages.map(renderMessage)} {/* Loading indicator */} {isLoading && ( @@ -184,7 +348,7 @@ export const ChatInterface: React.FC = ({ value={inputValue} onChange={(e) => setInputValue(e.target.value)} onKeyPress={handleKeyPress} - placeholder="Ask about your network or firewall" + placeholder="Prompt here!" className="flex-1 resize-none border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent min-h-[40px] max-h-[120px] text-black" rows={1} disabled={isLoading} @@ -198,13 +362,11 @@ export const ChatInterface: React.FC = ({
- {/* Quick suggestions */} + {/* Updated suggestions for command-oriented prompts */}
{[ - "Analyze current threats", - "Show firewall rules", - "Network performance", - "Security recommendations", + "Show me current iptables rules", + "Analyze network security posture", ].map((suggestion) => (