Add ThreatHunt agent backend/frontend scaffolding

This commit is contained in:
2025-12-29 10:22:57 -05:00
parent dc2dcd02c1
commit d0c9f88268
35 changed files with 21929 additions and 42 deletions

17164
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
frontend/package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "threathunt-frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"typescript": "^4.9.5"
},
"proxy": "http://localhost:8000"
}

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#1976d2" />
<meta
name="description"
content="ThreatHunt - Analyst-assist threat hunting platform with agent guidance"
/>
<title>ThreatHunt - Threat Hunting with Agent Assistance</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

224
frontend/src/App.css Normal file
View File

@@ -0,0 +1,224 @@
/**
* Main application styles.
*/
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, sans-serif;
color: #333;
background: #f5f5f5;
}
.app {
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* Header */
.app-header {
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
color: white;
padding: 24px 32px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.app-header h1 {
font-size: 28px;
margin-bottom: 8px;
font-weight: 600;
}
.subtitle {
font-size: 14px;
opacity: 0.9;
margin: 0;
}
/* Main content */
.app-main {
flex: 1;
padding: 24px 32px;
max-width: 1400px;
width: 100%;
margin: 0 auto;
}
.app-content {
display: grid;
grid-template-columns: 1fr 420px;
gap: 24px;
}
.main-panel {
background: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.main-panel h2 {
font-size: 20px;
margin-bottom: 16px;
color: #333;
}
.placeholder-text {
color: #999;
font-size: 14px;
margin-bottom: 16px;
}
.data-view {
overflow-x: auto;
}
.sample-data {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.sample-data thead {
background: #f0f0f0;
}
.sample-data th {
padding: 12px;
text-align: left;
font-weight: 600;
color: #333;
border-bottom: 2px solid #e0e0e0;
}
.sample-data td {
padding: 12px;
border-bottom: 1px solid #e0e0e0;
font-family: monospace;
font-size: 13px;
color: #555;
}
.sample-data tbody tr:hover {
background: #fafafa;
}
/* Agent sidebar */
.agent-sidebar {
display: flex;
flex-direction: column;
}
/* Footer */
.app-footer {
background: #f9f9f9;
border-top: 1px solid #e0e0e0;
padding: 32px;
margin-top: 32px;
}
.footer-content {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 32px;
max-width: 1400px;
margin: 0 auto 32px;
}
.footer-section h4 {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
color: #333;
}
.footer-section p {
font-size: 13px;
color: #666;
line-height: 1.5;
margin-bottom: 8px;
}
.footer-section ul {
list-style: none;
margin: 0;
padding: 0;
}
.footer-section li {
font-size: 13px;
color: #666;
padding: 4px 0;
}
.footer-section li:before {
content: "• ";
color: #1976d2;
margin-right: 6px;
}
.footer-bottom {
text-align: center;
padding-top: 24px;
border-top: 1px solid #e0e0e0;
max-width: 1400px;
margin: 0 auto;
}
.footer-bottom p {
font-size: 12px;
color: #999;
}
/* Responsive */
@media (max-width: 1024px) {
.app-content {
grid-template-columns: 1fr;
}
.app-main {
padding: 16px;
}
.app-header {
padding: 16px;
}
}
@media (max-width: 640px) {
.app-header h1 {
font-size: 20px;
}
.subtitle {
font-size: 12px;
}
.app-main {
padding: 12px;
}
.main-panel {
padding: 16px;
}
.footer-content {
grid-template-columns: 1fr;
gap: 16px;
}
.sample-data {
font-size: 12px;
}
.sample-data th,
.sample-data td {
padding: 8px;
}
}

124
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,124 @@
/**
* Main ThreatHunt application entry point.
*/
import React, { useState } from "react";
import "./App.css";
import AgentPanel from "./components/AgentPanel";
function App() {
// Sample state for demonstration
const [currentDataset] = useState("FileList-2025-12-26");
const [currentHost] = useState("DESKTOP-ABC123");
const [currentArtifact] = useState("FileList");
const [dataDescription] = useState(
"File listing from system scan showing recent modifications"
);
const handleAnalysisAction = (action: string) => {
console.log("Analysis action triggered:", action);
// In a real app, this would update the analysis view or apply filters
};
return (
<div className="app">
<header className="app-header">
<h1>ThreatHunt - Analyst-Assist Platform</h1>
<p className="subtitle">
Powered by agent guidance for faster threat hunting
</p>
</header>
<main className="app-main">
<div className="app-content">
<section className="main-panel">
<h2>Analysis Dashboard</h2>
<p className="placeholder-text">
[Main analysis interface would display here]
</p>
<div className="data-view">
<table className="sample-data">
<thead>
<tr>
<th>File</th>
<th>Modified</th>
<th>Size</th>
<th>Hash</th>
</tr>
</thead>
<tbody>
<tr>
<td>System32\drivers\etc\hosts</td>
<td>2025-12-20 14:32</td>
<td>456 B</td>
<td>d41d8cd98f00b204...</td>
</tr>
<tr>
<td>Windows\Temp\cache.bin</td>
<td>2025-12-26 09:15</td>
<td>2.3 MB</td>
<td>5d41402abc4b2a76...</td>
</tr>
<tr>
<td>Users\Admin\AppData\Roaming\config.xml</td>
<td>2025-12-25 16:45</td>
<td>12.4 KB</td>
<td>e99a18c428cb38d5...</td>
</tr>
</tbody>
</table>
</div>
</section>
<aside className="agent-sidebar">
<AgentPanel
dataset_name={currentDataset}
artifact_type={currentArtifact}
host_identifier={currentHost}
data_summary={dataDescription}
onAnalysisAction={handleAnalysisAction}
/>
</aside>
</div>
</main>
<footer className="app-footer">
<div className="footer-content">
<div className="footer-section">
<h4>About Analyst-Assist Agent</h4>
<p>
The agent provides advisory guidance on artifact data, analytical
pivots, and hypotheses. All decisions remain with the analyst.
</p>
</div>
<div className="footer-section">
<h4>Capabilities</h4>
<ul>
<li>Interpret CSV artifact data</li>
<li>Suggest analytical directions</li>
<li>Highlight anomalies</li>
<li>Propose investigative steps</li>
</ul>
</div>
<div className="footer-section">
<h4>Governance</h4>
<ul>
<li>Read-only guidance</li>
<li>No tool execution</li>
<li>No autonomous actions</li>
<li>Analyst controls decisions</li>
</ul>
</div>
</div>
<div className="footer-bottom">
<p>
&copy; 2025 ThreatHunt. Agent guidance is advisory only. All
analytical decisions remain with the analyst.
</p>
</div>
</footer>
</div>
);
}
export default App;

View File

@@ -0,0 +1,373 @@
/**
* Styles for analyst-assist agent chat panel.
*/
.agent-panel {
display: flex;
flex-direction: column;
height: 100%;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.agent-panel-header {
padding: 16px;
border-bottom: 1px solid #e0e0e0;
background: #f9f9f9;
}
.agent-panel-header h3 {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.agent-context {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.context-badge {
display: inline-block;
padding: 4px 8px;
background: #e3f2fd;
border: 1px solid #90caf9;
border-radius: 4px;
font-size: 12px;
color: #1976d2;
font-family: monospace;
}
.agent-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.agent-welcome {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: #666;
padding: 24px;
}
.welcome-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.welcome-text {
margin-bottom: 12px;
font-size: 14px;
}
.agent-welcome ul {
list-style: none;
padding: 0;
margin: 12px 0;
text-align: left;
}
.agent-welcome li {
padding: 4px 0;
font-size: 14px;
color: #555;
}
.agent-welcome li:before {
content: "✓ ";
color: #4caf50;
font-weight: bold;
margin-right: 6px;
}
.welcome-note {
font-size: 13px;
color: #999;
margin-top: 16px;
font-style: italic;
}
.message {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
border-radius: 6px;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.user {
background: #e3f2fd;
border-left: 4px solid #1976d2;
margin-left: 24px;
}
.message.agent {
background: #f5f5f5;
border-left: 4px solid #757575;
margin-right: 24px;
}
.message.loading {
align-items: center;
justify-content: center;
}
.message.error {
background: #ffebee;
border-left: 4px solid #d32f2f;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #999;
}
.message-role {
font-weight: 600;
color: #333;
}
.message-content {
font-size: 14px;
line-height: 1.5;
color: #333;
}
.message-details {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid rgba(0, 0, 0, 0.1);
font-size: 13px;
}
.detail-section {
margin-bottom: 8px;
}
.detail-section h5 {
margin: 0 0 6px 0;
font-weight: 600;
color: #333;
font-size: 13px;
}
.detail-section ul {
list-style: none;
padding: 0;
margin: 0;
}
.detail-section li {
padding: 4px 0;
margin-left: 8px;
border-left: 2px solid #ccc;
padding-left: 8px;
}
.pivot-button {
background: none;
border: none;
color: #1976d2;
cursor: pointer;
padding: 0;
font-size: 13px;
text-decoration: underline;
transition: color 0.2s;
}
.pivot-button:hover {
color: #1565c0;
}
.detail-section code {
background: #f0f0f0;
border: 1px solid #ddd;
padding: 2px 4px;
border-radius: 3px;
font-family: monospace;
font-size: 12px;
color: #555;
}
.detail-section.caveats {
background: #fffde7;
padding: 8px;
border-radius: 4px;
border-left: 3px solid #fbc02d;
}
.detail-section.caveats p {
margin: 0;
font-size: 13px;
color: #666;
}
.confidence {
display: inline-block;
padding: 4px 8px;
background: #f0f0f0;
border-radius: 3px;
font-size: 12px;
color: #666;
}
.error-text {
color: #d32f2f;
margin: 0;
font-size: 14px;
}
/* Loading animation */
.loading-indicator {
display: flex;
gap: 4px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #999;
animation: bounce 1.4s infinite;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes bounce {
0%,
80%,
100% {
opacity: 0.3;
}
40% {
opacity: 1;
}
}
/* Input form */
.agent-input-form {
display: flex;
gap: 8px;
padding: 12px;
border-top: 1px solid #e0e0e0;
background: #fafafa;
}
.agent-input {
flex: 1;
padding: 10px 12px;
border: 1px solid #d0d0d0;
border-radius: 4px;
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
transition: border-color 0.2s;
}
.agent-input:focus {
outline: none;
border-color: #1976d2;
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.1);
}
.agent-input:disabled {
background: #f0f0f0;
color: #999;
}
.agent-input-form button {
padding: 10px 20px;
background: #1976d2;
color: white;
border: none;
border-radius: 4px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.agent-input-form button:hover:not(:disabled) {
background: #1565c0;
}
.agent-input-form button:disabled {
background: #ccc;
cursor: not-allowed;
}
/* Footer */
.agent-footer {
padding: 8px 12px;
border-top: 1px solid #e0e0e0;
background: #f9f9f9;
text-align: center;
}
.footer-note {
margin: 0;
font-size: 12px;
color: #999;
}
/* Responsive */
@media (max-width: 768px) {
.message {
margin-left: 12px;
margin-right: 12px;
}
.message.user {
margin-left: 12px;
}
.message.agent {
margin-right: 12px;
}
.agent-panel-header {
padding: 12px;
}
.agent-messages {
padding: 12px;
}
.agent-input-form {
padding: 10px;
gap: 6px;
}
}

View File

@@ -0,0 +1,264 @@
/**
* Analyst-assist agent chat panel component.
* Provides context-aware guidance on artifact data and analysis.
*/
import React, { useState, useRef, useEffect } from "react";
import "./AgentPanel.css";
import {
requestAgentAssistance,
AssistResponse,
AssistRequest,
} from "../utils/agentApi";
export interface AgentPanelProps {
/** Name of the current dataset */
dataset_name?: string;
/** Type of artifact (e.g., FileList, ProcessList) */
artifact_type?: string;
/** Host name, IP, or identifier */
host_identifier?: string;
/** Summary of the uploaded data */
data_summary?: string;
/** Callback when user needs to execute analysis based on suggestions */
onAnalysisAction?: (action: string) => void;
}
interface Message {
role: "user" | "agent";
content: string;
response?: AssistResponse;
timestamp: Date;
}
export const AgentPanel: React.FC<AgentPanelProps> = ({
dataset_name,
artifact_type,
host_identifier,
data_summary,
onAnalysisAction,
}) => {
const [messages, setMessages] = useState<Message[]>([]);
const [query, setQuery] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!query.trim()) {
return;
}
// Add user message
const userMessage: Message = {
role: "user",
content: query,
timestamp: new Date(),
};
setMessages((prev) => [...prev, userMessage]);
setQuery("");
setLoading(true);
setError(null);
try {
// Build conversation history for context
const conversation_history = messages.map((msg) => ({
role: msg.role,
content: msg.content,
}));
// Request guidance from agent
const response = await requestAgentAssistance({
query: query,
dataset_name,
artifact_type,
host_identifier,
data_summary,
conversation_history,
});
// Add agent response
const agentMessage: Message = {
role: "agent",
content: response.guidance,
response,
timestamp: new Date(),
};
setMessages((prev) => [...prev, agentMessage]);
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : "Failed to get guidance";
setError(errorMessage);
// Add error message
const errorMsg: Message = {
role: "agent",
content: `Error: ${errorMessage}. The agent service may be unavailable.`,
timestamp: new Date(),
};
setMessages((prev) => [...prev, errorMsg]);
} finally {
setLoading(false);
}
};
return (
<div className="agent-panel">
<div className="agent-panel-header">
<h3>Analyst Assist Agent</h3>
<div className="agent-context">
{host_identifier && (
<span className="context-badge">Host: {host_identifier}</span>
)}
{artifact_type && (
<span className="context-badge">Artifact: {artifact_type}</span>
)}
{dataset_name && (
<span className="context-badge">Dataset: {dataset_name}</span>
)}
</div>
</div>
<div className="agent-messages">
{messages.length === 0 ? (
<div className="agent-welcome">
<p className="welcome-title">Welcome to Analyst Assist</p>
<p className="welcome-text">
Ask questions about your artifact data. I can help you:
</p>
<ul>
<li>Interpret and explain data patterns</li>
<li>Suggest analytical pivots and filters</li>
<li>Help form and test hypotheses</li>
<li>Highlight anomalies and points of interest</li>
</ul>
<p className="welcome-note">
💡 This agent provides guidance only. All analytical decisions
remain with you.
</p>
</div>
) : (
messages.map((msg, idx) => (
<div key={idx} className={`message ${msg.role}`}>
<div className="message-header">
<span className="message-role">
{msg.role === "user" ? "You" : "Agent"}
</span>
<span className="message-time">
{msg.timestamp.toLocaleTimeString()}
</span>
</div>
<div className="message-content">{msg.content}</div>
{msg.response && (
<div className="message-details">
{msg.response.suggested_pivots.length > 0 && (
<div className="detail-section">
<h5>Suggested Pivots:</h5>
<ul>
{msg.response.suggested_pivots.map((pivot, i) => (
<li key={i}>
<button
className="pivot-button"
onClick={() =>
onAnalysisAction && onAnalysisAction(pivot)
}
>
{pivot}
</button>
</li>
))}
</ul>
</div>
)}
{msg.response.suggested_filters.length > 0 && (
<div className="detail-section">
<h5>Suggested Filters:</h5>
<ul>
{msg.response.suggested_filters.map((filter, i) => (
<li key={i}>
<code>{filter}</code>
</li>
))}
</ul>
</div>
)}
{msg.response.caveats && (
<div className="detail-section caveats">
<h5> Caveats:</h5>
<p>{msg.response.caveats}</p>
</div>
)}
{msg.response.confidence && (
<div className="detail-section">
<span className="confidence">
Confidence: {(msg.response.confidence * 100).toFixed(0)}%
</span>
</div>
)}
</div>
)}
</div>
))
)}
{loading && (
<div className="message agent loading">
<div className="loading-indicator">
<span className="dot"></span>
<span className="dot"></span>
<span className="dot"></span>
</div>
</div>
)}
{error && (
<div className="message agent error">
<p className="error-text"> {error}</p>
</div>
)}
<div ref={messagesEndRef} />
</div>
<form onSubmit={handleSubmit} className="agent-input-form">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Ask about your data, patterns, or next steps..."
disabled={loading}
className="agent-input"
/>
<button type="submit" disabled={loading || !query.trim()}>
{loading ? "Thinking..." : "Ask"}
</button>
</form>
<div className="agent-footer">
<p className="footer-note">
Agent provides guidance only. All decisions remain with the analyst.
</p>
</div>
</div>
);
};
export default AgentPanel;

29
frontend/src/index.css Normal file
View File

@@ -0,0 +1,29 @@
/**
* CSS Modules and style definitions for components.
*/
/* Ensure consistent styling across all components */
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
#root {
width: 100%;
min-height: 100vh;
}

13
frontend/src/index.tsx Normal file
View File

@@ -0,0 +1,13 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -0,0 +1,65 @@
/**
* API utility functions for agent communication.
*/
export interface AssistRequest {
query: string;
dataset_name?: string;
artifact_type?: string;
host_identifier?: string;
data_summary?: string;
conversation_history?: Array<{ role: string; content: string }>;
}
export interface AssistResponse {
guidance: string;
confidence: number;
suggested_pivots: string[];
suggested_filters: string[];
caveats?: string;
reasoning?: string;
}
const API_BASE_URL = process.env.REACT_APP_API_URL || "http://localhost:8000";
/**
* Request guidance from the analyst-assist agent.
*/
export async function requestAgentAssistance(
request: AssistRequest
): Promise<AssistResponse> {
const response = await fetch(`${API_BASE_URL}/api/agent/assist`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(
`Agent request failed: ${response.status} ${response.statusText}`
);
}
return response.json();
}
/**
* Check if agent is available.
*/
export async function checkAgentHealth(): Promise<{
status: string;
provider?: string;
configured_providers?: Record<string, boolean>;
}> {
try {
const response = await fetch(`${API_BASE_URL}/api/agent/health`);
if (!response.ok) {
return { status: "error" };
}
return response.json();
} catch {
return { status: "offline" };
}
}

22
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,22 @@
/**
* TypeScript configuration.
*/
{
"compilerOptions": {
"target": "es2020",
"lib": ["es2020", "dom", "dom.iterable"],
"jsx": "react-jsx",
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowJs": true,
"noEmit": true
},
"include": ["src"],
"exclude": ["node_modules"]
}