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:
2026-05-29 21:22:34 -04:00
parent 03544b828d
commit 19c23117b9
9 changed files with 4923 additions and 2 deletions

218
app/dashboard.py Normal file
View 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}")