new file: .gitignore
modified: README.md new file: app/dashboard.py new file: app/main.py new file: docker-compose.yml new file: snort/local.rules new file: snort/snort-logs/soc_actions.log new file: snort/snort.lua new file: snort/snort3-community.rules
This commit is contained in:
218
app/dashboard.py
Normal file
218
app/dashboard.py
Normal file
@@ -0,0 +1,218 @@
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
from fastapi import FastAPI, Form, HTTPException
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
|
||||
app = FastAPI(title="Duct-Tape SOC Dashboard")
|
||||
|
||||
LOG_FILE = "/var/log/snort/alert_json.txt"
|
||||
RULES_FILE = "/etc/snort/rules/local.rules"
|
||||
ACTION_LOG = "/var/log/snort/soc_actions.log"
|
||||
|
||||
HTML_TEMPLATE = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Duct-Tape SOC Dashboard v3</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="bg-gray-900 text-gray-100 font-sans min-h-screen p-6">
|
||||
<div class="max-w-7xl mx-auto space-y-6">
|
||||
<header class="border-b border-gray-800 pb-4 flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-indigo-400">Duct-Tape SOC</h1>
|
||||
<p class="text-sm text-gray-400">Advanced Snort 3 & LLM Firewall Automation Simulator</p>
|
||||
</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
<div class="text-right mr-4 text-sm text-gray-400">
|
||||
Active Rules: <span id="ruleCount" class="font-bold text-green-400">0</span><br>
|
||||
Alerts Logged: <span id="alertCount" class="font-bold text-blue-400">0</span>
|
||||
</div>
|
||||
<button onclick="clearRules()" class="bg-red-600 hover:bg-red-700 text-white font-medium py-2 px-4 rounded transition">
|
||||
Wipe Active Rules
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div class="bg-gray-800 p-6 rounded-lg border border-gray-700 space-y-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-indigo-300 mb-4">Quick Scenarios</h2>
|
||||
<select id="scenarioSelect" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white mb-3">
|
||||
<option value="ssh_attack">External SSH Brute Force</option>
|
||||
<option value="nmap_scan">External Nmap Scan</option>
|
||||
<option value="ssdp_noise">Internal SSDP Noise</option>
|
||||
<option value="mdns_noise">Internal mDNS Noise</option>
|
||||
</select>
|
||||
<button onclick="injectStandard()" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 rounded transition">
|
||||
Fire Quick Payload
|
||||
</button>
|
||||
</div>
|
||||
<hr class="border-gray-700">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-orange-400 mb-4">Chaos Mode</h2>
|
||||
<button onclick="injectRandom()" class="w-full bg-orange-600 hover:bg-orange-700 text-white font-medium py-2 rounded transition">
|
||||
Generate Random Attack
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-800 p-6 rounded-lg border border-gray-700 flex flex-col h-[350px]">
|
||||
<h2 class="text-xl font-semibold text-pink-400 mb-4">Python & LLM Action Stream</h2>
|
||||
<pre id="actionBox" class="bg-gray-950 p-4 rounded border border-gray-800 overflow-y-auto text-xs font-mono flex-1 text-gray-300 whitespace-pre-wrap">Awaiting actions...</pre>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-800 p-6 rounded-lg border border-gray-700 flex flex-col h-[350px]">
|
||||
<h2 class="text-xl font-semibold text-indigo-300 mb-4">Active local.rules</h2>
|
||||
<pre id="rulesBox" class="bg-gray-950 p-4 rounded border border-gray-800 overflow-y-auto text-xs font-mono flex-1 text-green-400">Loading rules...</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-800 p-6 rounded-lg border border-gray-700">
|
||||
<h2 class="text-xl font-semibold text-indigo-300 mb-4">Live Snort JSON Tail</h2>
|
||||
<div id="alertsContainer" class="space-y-2 max-h-[300px] overflow-y-auto">
|
||||
<p class="text-sm text-gray-500 italic">Streaming logs...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function updateDashboardData() {
|
||||
try {
|
||||
const response = await fetch('/api/data');
|
||||
const data = await response.json();
|
||||
|
||||
document.getElementById('rulesBox').textContent = data.rules || "No rules active.";
|
||||
document.getElementById('ruleCount').textContent = data.rule_count;
|
||||
document.getElementById('alertCount').textContent = data.total_alerts;
|
||||
|
||||
// Update new Action Log Box
|
||||
const actionBox = document.getElementById('actionBox');
|
||||
actionBox.textContent = data.actions || "No actions logged.";
|
||||
|
||||
const container = document.getElementById('alertsContainer');
|
||||
if (data.alerts && data.alerts.length > 0) {
|
||||
container.innerHTML = data.alerts.map(alert =>
|
||||
`<pre class="bg-gray-950 p-3 rounded border border-gray-800 text-xs font-mono text-gray-300 overflow-x-auto mb-2">${JSON.stringify(alert, null, 2)}</pre>`
|
||||
).join('');
|
||||
} else {
|
||||
container.innerHTML = '<p class="text-sm text-gray-500 italic">No alerts seen yet.</p>';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Dashboard poll failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function injectStandard() {
|
||||
const formData = new FormData();
|
||||
formData.append('scenario', document.getElementById('scenarioSelect').value);
|
||||
await fetch('/inject-standard', { method: 'POST', body: formData });
|
||||
updateDashboardData();
|
||||
}
|
||||
|
||||
async function injectRandom() {
|
||||
await fetch('/inject-random', { method: 'POST' });
|
||||
updateDashboardData();
|
||||
}
|
||||
|
||||
async function clearRules() {
|
||||
await fetch('/clear-rules', { method: 'POST' });
|
||||
updateDashboardData();
|
||||
}
|
||||
|
||||
setInterval(updateDashboardData, 2000);
|
||||
window.onload = updateDashboardData;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
def write_alert(payload):
|
||||
try:
|
||||
with open(LOG_FILE, "a") as f:
|
||||
f.write(json.dumps(payload) + "\n")
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to write to log: {e}")
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
def index():
|
||||
return HTML_TEMPLATE
|
||||
|
||||
@app.get("/api/data")
|
||||
def get_data():
|
||||
rules_content = "File empty or missing."
|
||||
rule_count = 0
|
||||
if os.path.exists(RULES_FILE):
|
||||
with open(RULES_FILE, "r") as f:
|
||||
rules_content = f.read().strip()
|
||||
rule_count = rules_content.count("drop ")
|
||||
|
||||
actions_content = ""
|
||||
if os.path.exists(ACTION_LOG):
|
||||
with open(ACTION_LOG, "r") as f:
|
||||
# Grab the last 25 lines of the action stream
|
||||
actions_content = "".join(f.readlines()[-25:])
|
||||
|
||||
parsed_alerts = []
|
||||
total_alerts = 0
|
||||
if os.path.exists(LOG_FILE):
|
||||
with open(LOG_FILE, "r") as f:
|
||||
lines = f.readlines()
|
||||
total_alerts = len(lines)
|
||||
for line in reversed(lines[-10:]):
|
||||
try:
|
||||
parsed_alerts.append(json.loads(line))
|
||||
except:
|
||||
continue
|
||||
|
||||
return JSONResponse(content={
|
||||
"rules": rules_content,
|
||||
"alerts": parsed_alerts,
|
||||
"actions": actions_content.strip(),
|
||||
"rule_count": rule_count,
|
||||
"total_alerts": total_alerts
|
||||
})
|
||||
|
||||
@app.post("/inject-standard")
|
||||
def inject_standard(scenario: str = Form(...)):
|
||||
scenarios = {
|
||||
"ssh_attack": {"proto": "TCP", "src_ap": "185.220.101.5:43210", "dst_ap": "192.168.1.50:22", "rule": "1:2000123:1", "msg": "SSH Brute Force Attempt"},
|
||||
"nmap_scan": {"proto": "TCP", "src_ap": "45.33.32.156:59832", "dst_ap": "192.168.1.50:80", "rule": "1:2000456:1", "msg": "Nmap Port Scan"},
|
||||
"ssdp_noise": {"proto": "UDP", "src_ap": "192.168.1.121:1900", "dst_ap": "239.255.255.250:1900", "rule": "116:6:1", "msg": "SSDP Broadcast"},
|
||||
"mdns_noise": {"proto": "UDP", "src_ap": "192.168.1.83:5353", "dst_ap": "224.0.0.251:5353", "rule": "116:6:1", "msg": "mDNS Multicast"}
|
||||
}
|
||||
if scenario not in scenarios:
|
||||
raise HTTPException(status_code=400, detail="Invalid scenario")
|
||||
write_alert(scenarios[scenario])
|
||||
return {"status": "success"}
|
||||
|
||||
@app.post("/inject-random")
|
||||
def inject_random():
|
||||
src_ip = f"{random.randint(1, 223)}.{random.randint(0, 255)}.{random.randint(0, 255)}.{random.randint(1, 254)}"
|
||||
src_port = random.randint(1024, 65535)
|
||||
dst_ip = f"192.168.1.{random.randint(2, 254)}"
|
||||
dst_port = random.choice([22, 80, 443, 3389, 8080])
|
||||
|
||||
payload = {
|
||||
"proto": random.choice(["TCP", "UDP"]),
|
||||
"src_ap": f"{src_ip}:{src_port}",
|
||||
"dst_ap": f"{dst_ip}:{dst_port}",
|
||||
"rule": f"1:{random.randint(10000, 99999)}:1",
|
||||
"msg": "Simulated Random Attack"
|
||||
}
|
||||
write_alert(payload)
|
||||
return {"status": "success"}
|
||||
|
||||
@app.post("/clear-rules")
|
||||
def clear_rules():
|
||||
try:
|
||||
with open(RULES_FILE, "w") as f:
|
||||
f.write("")
|
||||
return {"status": "success"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to clear rules: {e}")
|
||||
Reference in New Issue
Block a user