Spaces:
Sleeping
Sleeping
Trae Assistant commited on
Commit ·
2d4937b
1
Parent(s): 573829f
feat: enhance features with chinese localization, file upload, and datasets management
Browse files- __pycache__/app.cpython-314.pyc +0 -0
- app.py +91 -24
- templates/index.html +396 -121
__pycache__/app.cpython-314.pyc
ADDED
|
Binary file (13.6 kB). View file
|
|
|
app.py
CHANGED
|
@@ -2,8 +2,12 @@ import os
|
|
| 2 |
import json
|
| 3 |
import sqlite3
|
| 4 |
import requests
|
|
|
|
|
|
|
| 5 |
from flask import Flask, render_template, request, jsonify, send_from_directory
|
| 6 |
from flask_cors import CORS
|
|
|
|
|
|
|
| 7 |
|
| 8 |
app = Flask(__name__, static_folder='static', template_folder='templates')
|
| 9 |
CORS(app)
|
|
@@ -12,9 +16,26 @@ CORS(app)
|
|
| 12 |
SILICONFLOW_API_KEY = "sk-vimuseiptfbomzegyuvmebjzooncsqbyjtlddrfodzcdskgi"
|
| 13 |
SILICONFLOW_API_URL = "https://api.siliconflow.cn/v1/chat/completions"
|
| 14 |
DB_PATH = os.path.join(app.instance_path, "cyber_sentry.db")
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
# Ensure instance
|
| 17 |
os.makedirs(app.instance_path, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
# Database Setup
|
| 20 |
def init_db():
|
|
@@ -32,9 +53,11 @@ def init_db():
|
|
| 32 |
c.execute('SELECT count(*) FROM playbooks')
|
| 33 |
if c.fetchone()[0] == 0:
|
| 34 |
c.execute("INSERT INTO playbooks (title, scenario, steps, severity) VALUES (?, ?, ?, ?)",
|
| 35 |
-
("Phishing Response", "
|
| 36 |
c.execute("INSERT INTO playbooks (title, scenario, steps, severity) VALUES (?, ?, ?, ?)",
|
| 37 |
-
("Ransomware Containment", "
|
|
|
|
|
|
|
| 38 |
conn.commit()
|
| 39 |
|
| 40 |
conn.close()
|
|
@@ -53,21 +76,43 @@ def index():
|
|
| 53 |
|
| 54 |
@app.route('/api/stats')
|
| 55 |
def get_stats():
|
| 56 |
-
# Mock stats for the dashboard
|
| 57 |
return jsonify({
|
| 58 |
-
"threat_level": "Elevated",
|
| 59 |
-
"active_incidents":
|
| 60 |
-
"blocked_attempts":
|
| 61 |
-
"system_health":
|
| 62 |
"radar_data": [
|
| 63 |
-
{"name": "Phishing", "value":
|
| 64 |
-
{"name": "Malware", "value":
|
| 65 |
-
{"name": "DDoS", "value":
|
| 66 |
-
{"name": "Insider", "value":
|
| 67 |
-
{"name": "Zero-day", "value":
|
| 68 |
]
|
| 69 |
})
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
@app.route('/api/analyze', methods=['POST'])
|
| 72 |
def analyze_log():
|
| 73 |
data = request.json
|
|
@@ -87,7 +132,7 @@ def analyze_log():
|
|
| 87 |
- severity: "Low", "Medium", "High", or "Critical"
|
| 88 |
- summary: A brief summary of what happened (max 2 sentences).
|
| 89 |
- indicators: List of suspicious IPs, filenames, or hashes found.
|
| 90 |
-
- recommendation: Actionable steps to remediate.
|
| 91 |
- mock_graph: A list of 5 integers representing traffic spike during this event (0-100).
|
| 92 |
|
| 93 |
Do not output markdown code blocks, just the raw JSON."""
|
|
@@ -103,7 +148,11 @@ def analyze_log():
|
|
| 103 |
}
|
| 104 |
|
| 105 |
try:
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
response.raise_for_status()
|
| 108 |
ai_data = response.json()
|
| 109 |
content = ai_data['choices'][0]['message']['content']
|
|
@@ -116,12 +165,13 @@ def analyze_log():
|
|
| 116 |
return jsonify(json.loads(content))
|
| 117 |
except Exception as e:
|
| 118 |
print(f"AI Error: {e}")
|
| 119 |
-
# Mock Fallback
|
|
|
|
| 120 |
return jsonify({
|
| 121 |
-
"severity": "High",
|
| 122 |
-
"summary": "AI
|
| 123 |
-
"indicators": ["192.168.1.105", "admin_user"],
|
| 124 |
-
"recommendation": "
|
| 125 |
"mock_graph": [10, 25, 60, 95, 40]
|
| 126 |
})
|
| 127 |
|
|
@@ -144,13 +194,30 @@ def handle_playbooks():
|
|
| 144 |
def simulate_attack():
|
| 145 |
# Simulate an attack scenario for training
|
| 146 |
scenarios = [
|
| 147 |
-
{"type": "SQL Injection", "log": "GET /products?id=1' OR '1'='1", "details": "
|
| 148 |
-
{"type": "XSS", "log": "<script>alert('pwned')</script>", "details": "
|
| 149 |
-
{"type": "Brute Force", "log": "Failed password for root from 10.0.0.5 port 22 ssh2", "details": "500
|
|
|
|
| 150 |
]
|
| 151 |
-
import random
|
| 152 |
selected = random.choice(scenarios)
|
| 153 |
return jsonify(selected)
|
| 154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
if __name__ == '__main__':
|
| 156 |
app.run(host='0.0.0.0', port=7860)
|
|
|
|
| 2 |
import json
|
| 3 |
import sqlite3
|
| 4 |
import requests
|
| 5 |
+
import random
|
| 6 |
+
import time
|
| 7 |
from flask import Flask, render_template, request, jsonify, send_from_directory
|
| 8 |
from flask_cors import CORS
|
| 9 |
+
from werkzeug.utils import secure_filename
|
| 10 |
+
from werkzeug.exceptions import RequestEntityTooLarge
|
| 11 |
|
| 12 |
app = Flask(__name__, static_folder='static', template_folder='templates')
|
| 13 |
CORS(app)
|
|
|
|
| 16 |
SILICONFLOW_API_KEY = "sk-vimuseiptfbomzegyuvmebjzooncsqbyjtlddrfodzcdskgi"
|
| 17 |
SILICONFLOW_API_URL = "https://api.siliconflow.cn/v1/chat/completions"
|
| 18 |
DB_PATH = os.path.join(app.instance_path, "cyber_sentry.db")
|
| 19 |
+
UPLOAD_FOLDER = os.path.join(app.instance_path, 'uploads')
|
| 20 |
+
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB limit
|
| 21 |
+
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
| 22 |
|
| 23 |
+
# Ensure instance and upload folders exist
|
| 24 |
os.makedirs(app.instance_path, exist_ok=True)
|
| 25 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 26 |
+
|
| 27 |
+
# Error Handlers
|
| 28 |
+
@app.errorhandler(404)
|
| 29 |
+
def not_found_error(error):
|
| 30 |
+
return jsonify({"error": "Resource not found"}), 404
|
| 31 |
+
|
| 32 |
+
@app.errorhandler(500)
|
| 33 |
+
def internal_error(error):
|
| 34 |
+
return jsonify({"error": "Internal server error"}), 500
|
| 35 |
+
|
| 36 |
+
@app.errorhandler(RequestEntityTooLarge)
|
| 37 |
+
def handle_file_too_large(e):
|
| 38 |
+
return jsonify({"error": "File is too large (Max 16MB)"}), 413
|
| 39 |
|
| 40 |
# Database Setup
|
| 41 |
def init_db():
|
|
|
|
| 53 |
c.execute('SELECT count(*) FROM playbooks')
|
| 54 |
if c.fetchone()[0] == 0:
|
| 55 |
c.execute("INSERT INTO playbooks (title, scenario, steps, severity) VALUES (?, ?, ?, ?)",
|
| 56 |
+
("钓鱼邮件响应流程 (Phishing Response)", "用户报告收到可疑邮件,疑似包含恶意链接或附件。", "1. 隔离终端设备。\n2. 分析邮件头信息。\n3. 重置用户凭据。\n4. 全盘扫描恶意软件。\n5. 全局搜索并删除同类邮件。", "Medium"))
|
| 57 |
c.execute("INSERT INTO playbooks (title, scenario, steps, severity) VALUES (?, ?, ?, ?)",
|
| 58 |
+
("勒索软件遏制 (Ransomware Containment)", "监测到文件被批量加密,出现勒索信。", "1. 立即断开网络连接(拔网线/禁用网卡)。\n2. 识别勒索病毒变种。\n3. 检查备份完整性。\n4. 通知法务/合规部门。\n5. 启动业务连续性计划 (BCP)。", "Critical"))
|
| 59 |
+
c.execute("INSERT INTO playbooks (title, scenario, steps, severity) VALUES (?, ?, ?, ?)",
|
| 60 |
+
("DDoS 攻击防御 (DDoS Defense)", "服务器流量异常激增,服务不可用。", "1. 确认攻击类型(流量型/应用层)。\n2. 启用流量清洗服务 (WAF/CDN)。\n3. 限制非必要 IP 段访问。\n4. 调整防火墙策略。\n5. 联系 ISP 协助溯源。", "High"))
|
| 61 |
conn.commit()
|
| 62 |
|
| 63 |
conn.close()
|
|
|
|
| 76 |
|
| 77 |
@app.route('/api/stats')
|
| 78 |
def get_stats():
|
| 79 |
+
# Mock stats for the dashboard (randomized for liveliness)
|
| 80 |
return jsonify({
|
| 81 |
+
"threat_level": random.choice(["Elevated", "High", "Critical", "Low", "Medium"]),
|
| 82 |
+
"active_incidents": random.randint(1, 15),
|
| 83 |
+
"blocked_attempts": random.randint(1000, 5000),
|
| 84 |
+
"system_health": random.randint(85, 99),
|
| 85 |
"radar_data": [
|
| 86 |
+
{"name": "钓鱼 (Phishing)", "value": random.randint(40, 90)},
|
| 87 |
+
{"name": "恶意软件 (Malware)", "value": random.randint(30, 80)},
|
| 88 |
+
{"name": "DDoS", "value": random.randint(20, 70)},
|
| 89 |
+
{"name": "内鬼 (Insider)", "value": random.randint(10, 50)},
|
| 90 |
+
{"name": "零日 (Zero-day)", "value": random.randint(5, 40)}
|
| 91 |
]
|
| 92 |
})
|
| 93 |
|
| 94 |
+
@app.route('/api/upload', methods=['POST'])
|
| 95 |
+
def upload_file():
|
| 96 |
+
if 'file' not in request.files:
|
| 97 |
+
return jsonify({"error": "No file part"}), 400
|
| 98 |
+
file = request.files['file']
|
| 99 |
+
if file.filename == '':
|
| 100 |
+
return jsonify({"error": "No selected file"}), 400
|
| 101 |
+
if file:
|
| 102 |
+
filename = secure_filename(file.filename)
|
| 103 |
+
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
| 104 |
+
file.save(filepath)
|
| 105 |
+
|
| 106 |
+
# Mock processing of the file
|
| 107 |
+
size_mb = os.path.getsize(filepath) / (1024 * 1024)
|
| 108 |
+
|
| 109 |
+
return jsonify({
|
| 110 |
+
"message": "File uploaded successfully",
|
| 111 |
+
"filename": filename,
|
| 112 |
+
"size_mb": f"{size_mb:.2f} MB",
|
| 113 |
+
"analysis": "File scanned. No immediate threats detected. Added to dataset."
|
| 114 |
+
})
|
| 115 |
+
|
| 116 |
@app.route('/api/analyze', methods=['POST'])
|
| 117 |
def analyze_log():
|
| 118 |
data = request.json
|
|
|
|
| 132 |
- severity: "Low", "Medium", "High", or "Critical"
|
| 133 |
- summary: A brief summary of what happened (max 2 sentences).
|
| 134 |
- indicators: List of suspicious IPs, filenames, or hashes found.
|
| 135 |
+
- recommendation: Actionable steps to remediate (can use markdown).
|
| 136 |
- mock_graph: A list of 5 integers representing traffic spike during this event (0-100).
|
| 137 |
|
| 138 |
Do not output markdown code blocks, just the raw JSON."""
|
|
|
|
| 148 |
}
|
| 149 |
|
| 150 |
try:
|
| 151 |
+
# Mock check: If key is invalid or request fails, use fallback
|
| 152 |
+
if "sk-" not in SILICONFLOW_API_KEY:
|
| 153 |
+
raise Exception("Invalid Key")
|
| 154 |
+
|
| 155 |
+
response = requests.post(SILICONFLOW_API_URL, json=payload, headers=headers, timeout=10)
|
| 156 |
response.raise_for_status()
|
| 157 |
ai_data = response.json()
|
| 158 |
content = ai_data['choices'][0]['message']['content']
|
|
|
|
| 165 |
return jsonify(json.loads(content))
|
| 166 |
except Exception as e:
|
| 167 |
print(f"AI Error: {e}")
|
| 168 |
+
# Mock Fallback with Richer Data
|
| 169 |
+
time.sleep(1) # Simulate processing
|
| 170 |
return jsonify({
|
| 171 |
+
"severity": random.choice(["High", "Critical"]),
|
| 172 |
+
"summary": "AI 服务暂不可用。模拟分析结果:检测到针对 SSH 端口的暴力破解攻击。",
|
| 173 |
+
"indicators": ["192.168.1.105", "admin_user", "id_rsa"],
|
| 174 |
+
"recommendation": "**建议措施:**\n1. 立即封禁来源 IP `192.168.1.105`。\n2. 强制重置 `admin` 用户密码。\n3. 检查 `/var/log/auth.log` 确认是否有成功登录记录。\n4. 启用多因素认证 (MFA)。",
|
| 175 |
"mock_graph": [10, 25, 60, 95, 40]
|
| 176 |
})
|
| 177 |
|
|
|
|
| 194 |
def simulate_attack():
|
| 195 |
# Simulate an attack scenario for training
|
| 196 |
scenarios = [
|
| 197 |
+
{"type": "SQL 注入 (SQL Injection)", "log": "GET /products?id=1' OR '1'='1", "details": "在产品详情页参数中检测到典型的 SQL 注入特征。"},
|
| 198 |
+
{"type": "跨站脚本 (XSS)", "log": "<script>alert('pwned')</script>", "details": "搜索框输入参数包含反射型 XSS 攻击载荷。"},
|
| 199 |
+
{"type": "暴力破解 (Brute Force)", "log": "Failed password for root from 10.0.0.5 port 22 ssh2", "details": "1分钟内检测到来自同一IP的 500 次 SSH 登录失败尝试。"},
|
| 200 |
+
{"type": "远程代码执行 (RCE)", "log": "cmd.exe /c powershell -w hidden -c (new-object System.Net.WebClient).DownloadFile...", "details": "检测到可疑的 PowerShell 命令执行,试图下载外部文件。"}
|
| 201 |
]
|
|
|
|
| 202 |
selected = random.choice(scenarios)
|
| 203 |
return jsonify(selected)
|
| 204 |
|
| 205 |
+
@app.route('/api/datasets', methods=['GET'])
|
| 206 |
+
def list_datasets():
|
| 207 |
+
# List files in the upload folder
|
| 208 |
+
try:
|
| 209 |
+
files = []
|
| 210 |
+
for f in os.listdir(app.config['UPLOAD_FOLDER']):
|
| 211 |
+
path = os.path.join(app.config['UPLOAD_FOLDER'], f)
|
| 212 |
+
if os.path.isfile(path):
|
| 213 |
+
files.append({
|
| 214 |
+
"name": f,
|
| 215 |
+
"size": f"{os.path.getsize(path) / 1024:.2f} KB",
|
| 216 |
+
"date": time.ctime(os.path.getmtime(path))
|
| 217 |
+
})
|
| 218 |
+
return jsonify(files)
|
| 219 |
+
except Exception as e:
|
| 220 |
+
return jsonify([])
|
| 221 |
+
|
| 222 |
if __name__ == '__main__':
|
| 223 |
app.run(host='0.0.0.0', port=7860)
|
templates/index.html
CHANGED
|
@@ -11,76 +11,134 @@
|
|
| 11 |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
| 12 |
<style>
|
| 13 |
[v-cloak] { display: none; }
|
| 14 |
-
body { background-color: #f8fafc; color: #1e293b; }
|
| 15 |
-
.fade-enter-active, .fade-leave-active { transition: opacity 0.
|
| 16 |
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
| 17 |
.markdown-body h1 { font-size: 1.5em; font-weight: bold; margin-bottom: 0.5em; }
|
| 18 |
.markdown-body h2 { font-size: 1.25em; font-weight: bold; margin-bottom: 0.5em; }
|
| 19 |
.markdown-body ul { list-style-type: disc; margin-left: 1.5em; margin-bottom: 1em; }
|
| 20 |
-
.markdown-body code { background-color: #f1f5f9; padding: 0.2em 0.4em; border-radius: 0.25em; font-family: monospace; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
</style>
|
| 22 |
</head>
|
| 23 |
<body>
|
| 24 |
<div id="app" v-cloak class="min-h-screen flex flex-col md:flex-row">
|
| 25 |
<!-- Sidebar -->
|
| 26 |
-
<aside class="w-full md:w-64 bg-white border-r border-slate-200 flex-shrink-0">
|
| 27 |
<div class="p-6 flex items-center space-x-3 border-b border-slate-100">
|
| 28 |
-
<
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
</div>
|
| 31 |
-
<nav class="p-4 space-y-
|
| 32 |
-
<button @click="currentTab = 'dashboard'" :class="{'bg-blue-50 text-blue-
|
| 33 |
-
<i class="fas fa-chart-pie w-6"></i> 态势感知 (Dashboard)
|
|
|
|
|
|
|
|
|
|
| 34 |
</button>
|
| 35 |
-
<button @click="currentTab = '
|
| 36 |
-
<i class="fas fa-
|
| 37 |
</button>
|
| 38 |
-
<button @click="currentTab = '
|
| 39 |
-
<i class="fas fa-
|
| 40 |
</button>
|
| 41 |
-
<button @click="currentTab = '
|
| 42 |
-
<i class="fas fa-
|
| 43 |
</button>
|
| 44 |
</nav>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
</aside>
|
| 46 |
|
| 47 |
<!-- Main Content -->
|
| 48 |
-
<main class="flex-1 overflow-y-auto
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
<!-- Dashboard -->
|
| 51 |
-
<div v-if="currentTab === 'dashboard'" class="space-y-6">
|
| 52 |
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
| 53 |
-
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
|
| 54 |
-
<div class="text-sm text-slate-500 mb-1">
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
</div>
|
| 57 |
-
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
|
| 58 |
-
<div class="text-sm text-slate-500 mb-1">
|
|
|
|
|
|
|
| 59 |
<div class="text-2xl font-bold text-red-600">${ stats.active_incidents }</div>
|
| 60 |
</div>
|
| 61 |
-
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
|
| 62 |
-
<div class="text-sm text-slate-500 mb-1">
|
|
|
|
|
|
|
| 63 |
<div class="text-2xl font-bold text-green-600">${ stats.blocked_attempts }</div>
|
| 64 |
</div>
|
| 65 |
-
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
|
| 66 |
-
<div class="text-sm text-slate-500 mb-1">
|
|
|
|
|
|
|
| 67 |
<div class="text-2xl font-bold text-blue-600">${ stats.system_health }%</div>
|
| 68 |
</div>
|
| 69 |
</div>
|
| 70 |
|
| 71 |
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
| 72 |
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
|
| 73 |
-
<h3 class="text-lg font-bold mb-4
|
|
|
|
|
|
|
| 74 |
<div id="radarChart" class="w-full h-80"></div>
|
| 75 |
</div>
|
| 76 |
-
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
|
| 77 |
-
<h3 class="text-lg font-bold mb-4">
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
</div>
|
| 82 |
-
<div class="p-3 bg-
|
| 83 |
-
<strong>
|
| 84 |
</div>
|
| 85 |
</div>
|
| 86 |
</div>
|
|
@@ -88,125 +146,241 @@
|
|
| 88 |
</div>
|
| 89 |
|
| 90 |
<!-- Analyzer -->
|
| 91 |
-
<div v-if="currentTab === 'analyzer'" class="space-y-6">
|
| 92 |
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
|
| 93 |
-
<h2 class="text-xl font-bold mb-4
|
|
|
|
|
|
|
| 94 |
<p class="text-slate-500 text-sm mb-4">粘贴系统日志,AI 将自动分析威胁等级、提取指标并生成修复建议。</p>
|
| 95 |
-
<textarea v-model="logInput" class="w-full h-40 p-4 bg-slate-50 border border-slate-200 rounded-lg font-mono text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none" placeholder="
|
| 96 |
-
<div class="mt-4 flex justify-
|
| 97 |
-
<
|
| 98 |
-
<i
|
| 99 |
-
|
| 100 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
</div>
|
| 102 |
</div>
|
| 103 |
|
| 104 |
-
<div v-if="analysisResult" class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 animate-fade-in">
|
| 105 |
<div class="flex items-center justify-between mb-6">
|
| 106 |
-
<h3 class="text-lg font-bold">
|
| 107 |
-
|
|
|
|
|
|
|
| 108 |
${ analysisResult.severity }
|
| 109 |
</span>
|
| 110 |
</div>
|
| 111 |
|
| 112 |
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-
|
| 113 |
<div>
|
| 114 |
-
<div class="mb-
|
| 115 |
-
<h4 class="font-bold text-slate-700 mb-2">摘要</h4>
|
| 116 |
-
<p class="text-slate-600 text-sm">${ analysisResult.summary }</p>
|
| 117 |
</div>
|
| 118 |
-
<div class="mb-
|
| 119 |
-
<h4 class="font-bold text-slate-700 mb-2">发现指标 (IOCs)</h4>
|
| 120 |
<div class="flex flex-wrap gap-2">
|
| 121 |
-
<span v-for="ioc in analysisResult.indicators" class="bg-slate-100 text-slate-600 px-2 py-1 rounded text-xs font-mono border border-slate-200">${ ioc }</span>
|
|
|
|
| 122 |
</div>
|
| 123 |
</div>
|
| 124 |
<div>
|
| 125 |
-
<h4 class="font-bold text-slate-700 mb-2">修复建议</h4>
|
| 126 |
-
<div class="p-
|
| 127 |
</div>
|
| 128 |
</div>
|
| 129 |
<div>
|
| 130 |
-
<h4 class="font-bold text-slate-700 mb-2">流量/活动峰值</h4>
|
| 131 |
-
<div id="mockGraph" class="w-full h-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
</div>
|
| 133 |
</div>
|
| 134 |
</div>
|
| 135 |
</div>
|
| 136 |
|
| 137 |
<!-- Playbooks -->
|
| 138 |
-
<div v-if="currentTab === 'playbooks'" class="space-y-6">
|
| 139 |
-
<div class="flex justify-between items-center">
|
| 140 |
-
<
|
| 141 |
-
|
| 142 |
-
<
|
|
|
|
|
|
|
|
|
|
| 143 |
</button>
|
| 144 |
</div>
|
| 145 |
|
| 146 |
<!-- Add Modal -->
|
| 147 |
-
<div v-if="showAddPlaybook" class="fixed inset-0 bg-black
|
| 148 |
-
<div class="bg-white rounded-xl max-w-lg w-full p-6">
|
| 149 |
-
<h3 class="text-lg font-bold mb-4">
|
| 150 |
<div class="space-y-4">
|
| 151 |
-
<
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
<
|
| 157 |
-
<
|
| 158 |
-
</
|
| 159 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
</div>
|
| 161 |
<div class="mt-6 flex justify-end space-x-3">
|
| 162 |
-
<button @click="showAddPlaybook = false" class="text-slate-500 hover:text-slate-700">
|
| 163 |
-
<button @click="addPlaybook" class="bg-blue-600 text-white px-
|
| 164 |
</div>
|
| 165 |
</div>
|
| 166 |
</div>
|
| 167 |
|
| 168 |
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 169 |
-
<div v-for="pb in playbooks" :key="pb.id" class="bg-white rounded-xl shadow-sm border border-slate-100 hover:shadow-
|
| 170 |
-
<div class="p-5">
|
| 171 |
<div class="flex justify-between items-start mb-3">
|
| 172 |
-
<h3 class="font-bold text-lg">${ pb.title }</h3>
|
| 173 |
-
<span class="text-xs px-2 py-1 rounded-full border" :class="{'bg-red-50 text-red-600 border-red-100': pb.severity==='Critical', 'bg-orange-50 text-orange-600 border-orange-100': pb.severity==='High'}">
|
| 174 |
${ pb.severity }
|
| 175 |
</span>
|
| 176 |
</div>
|
| 177 |
-
<p class="text-sm text-slate-500 mb-4 h-10 overflow-hidden text-ellipsis">${ pb.scenario }</p>
|
| 178 |
-
<div class="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
</div>
|
| 180 |
</div>
|
| 181 |
</div>
|
| 182 |
</div>
|
| 183 |
|
| 184 |
<!-- Simulation -->
|
| 185 |
-
<div v-if="currentTab === 'simulation'" class="space-y-6">
|
| 186 |
-
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 text-center py-
|
| 187 |
-
<div class="w-
|
| 188 |
-
|
|
|
|
| 189 |
</div>
|
| 190 |
-
<h2 class="text-2xl font-bold mb-2">红蓝对抗演练 (Breach Simulator)</h2>
|
| 191 |
-
<p class="text-slate-500 mb-8 max-w-md mx-auto">
|
| 192 |
-
<button @click="runSimulation" :disabled="simulating" class="bg-red-600 text-white px-8 py-3 rounded-xl hover:bg-red-700 shadow-
|
| 193 |
-
|
|
|
|
| 194 |
</button>
|
| 195 |
</div>
|
| 196 |
|
| 197 |
-
<div v-if="simulationResult" class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 border-l-4 border-red-500 animate-
|
| 198 |
-
<
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
<
|
| 202 |
-
<
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
</div>
|
| 205 |
</div>
|
| 206 |
-
<div class="mt-
|
| 207 |
-
<button @click="copyToAnalyzer(simulationResult.log)" class="text-blue-600 hover:
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
</div>
|
| 211 |
</div>
|
| 212 |
</div>
|
|
@@ -215,7 +389,7 @@
|
|
| 215 |
</div>
|
| 216 |
|
| 217 |
<script>
|
| 218 |
-
const { createApp, ref, onMounted, nextTick } = Vue;
|
| 219 |
|
| 220 |
createApp({
|
| 221 |
delimiters: ['${', '}'],
|
|
@@ -230,13 +404,42 @@
|
|
| 230 |
const newPlaybook = ref({ title: '', scenario: '', steps: '', severity: 'Medium' });
|
| 231 |
const simulating = ref(false);
|
| 232 |
const simulationResult = ref(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
let radarChart = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
|
| 235 |
const fetchStats = async () => {
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
};
|
| 241 |
|
| 242 |
const initRadar = (data) => {
|
|
@@ -245,6 +448,7 @@
|
|
| 245 |
radarChart = echarts.init(document.getElementById('radarChart'));
|
| 246 |
radarChart.setOption({
|
| 247 |
color: ['#3b82f6'],
|
|
|
|
| 248 |
radar: {
|
| 249 |
indicator: data.map(d => ({ name: d.name, max: 100 })),
|
| 250 |
shape: 'circle',
|
|
@@ -254,11 +458,15 @@
|
|
| 254 |
type: 'radar',
|
| 255 |
data: [{
|
| 256 |
value: data.map(d => d.value),
|
| 257 |
-
name: '
|
| 258 |
-
areaStyle: { opacity: 0.2 }
|
|
|
|
| 259 |
}]
|
| 260 |
}]
|
| 261 |
});
|
|
|
|
|
|
|
|
|
|
| 262 |
};
|
| 263 |
|
| 264 |
const analyzeLog = async () => {
|
|
@@ -276,7 +484,7 @@
|
|
| 276 |
if (analysisResult.value.mock_graph) initLineChart(analysisResult.value.mock_graph);
|
| 277 |
});
|
| 278 |
} catch (e) {
|
| 279 |
-
alert('Analysis failed');
|
| 280 |
} finally {
|
| 281 |
analyzing.value = false;
|
| 282 |
}
|
|
@@ -285,17 +493,20 @@
|
|
| 285 |
const initLineChart = (data) => {
|
| 286 |
const el = document.getElementById('mockGraph');
|
| 287 |
if (!el) return;
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
|
|
|
|
|
|
| 293 |
series: [{
|
| 294 |
data: data,
|
| 295 |
type: 'line',
|
| 296 |
smooth: true,
|
| 297 |
-
areaStyle: { opacity: 0.2 },
|
| 298 |
-
itemStyle: { color: '#ef4444' }
|
|
|
|
| 299 |
}]
|
| 300 |
});
|
| 301 |
};
|
|
@@ -306,6 +517,7 @@
|
|
| 306 |
};
|
| 307 |
|
| 308 |
const addPlaybook = async () => {
|
|
|
|
| 309 |
await fetch('/api/playbooks', {
|
| 310 |
method: 'POST',
|
| 311 |
headers: {'Content-Type': 'application/json'},
|
|
@@ -319,8 +531,8 @@
|
|
| 319 |
const runSimulation = async () => {
|
| 320 |
simulating.value = true;
|
| 321 |
simulationResult.value = null;
|
| 322 |
-
// Mock delay
|
| 323 |
-
await new Promise(r => setTimeout(r,
|
| 324 |
const res = await fetch('/api/simulate', { method: 'POST' });
|
| 325 |
simulationResult.value = await res.json();
|
| 326 |
simulating.value = false;
|
|
@@ -329,22 +541,85 @@
|
|
| 329 |
const copyToAnalyzer = (log) => {
|
| 330 |
currentTab.value = 'analyzer';
|
| 331 |
logInput.value = log;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
};
|
| 333 |
|
| 334 |
const renderMarkdown = (text) => {
|
| 335 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
};
|
| 337 |
|
| 338 |
onMounted(() => {
|
| 339 |
fetchStats();
|
| 340 |
fetchPlaybooks();
|
| 341 |
-
|
|
|
|
|
|
|
|
|
|
| 342 |
});
|
| 343 |
|
| 344 |
return {
|
| 345 |
-
currentTab,
|
| 346 |
-
|
| 347 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
};
|
| 349 |
}
|
| 350 |
}).mount('#app');
|
|
|
|
| 11 |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
| 12 |
<style>
|
| 13 |
[v-cloak] { display: none; }
|
| 14 |
+
body { background-color: #f8fafc; color: #1e293b; font-family: 'Inter', system-ui, sans-serif; }
|
| 15 |
+
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
|
| 16 |
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
| 17 |
.markdown-body h1 { font-size: 1.5em; font-weight: bold; margin-bottom: 0.5em; }
|
| 18 |
.markdown-body h2 { font-size: 1.25em; font-weight: bold; margin-bottom: 0.5em; }
|
| 19 |
.markdown-body ul { list-style-type: disc; margin-left: 1.5em; margin-bottom: 1em; }
|
| 20 |
+
.markdown-body code { background-color: #f1f5f9; padding: 0.2em 0.4em; border-radius: 0.25em; font-family: monospace; color: #ef4444; }
|
| 21 |
+
.markdown-body strong { color: #0f172a; }
|
| 22 |
+
/* Custom Scrollbar */
|
| 23 |
+
::-webkit-scrollbar { width: 8px; height: 8px; }
|
| 24 |
+
::-webkit-scrollbar-track { background: #f1f5f9; }
|
| 25 |
+
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
|
| 26 |
+
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
|
| 27 |
</style>
|
| 28 |
</head>
|
| 29 |
<body>
|
| 30 |
<div id="app" v-cloak class="min-h-screen flex flex-col md:flex-row">
|
| 31 |
<!-- Sidebar -->
|
| 32 |
+
<aside class="w-full md:w-64 bg-white border-r border-slate-200 flex-shrink-0 flex flex-col shadow-sm z-10">
|
| 33 |
<div class="p-6 flex items-center space-x-3 border-b border-slate-100">
|
| 34 |
+
<div class="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center text-white shadow-lg shadow-blue-200">
|
| 35 |
+
<i class="fas fa-shield-alt text-xl"></i>
|
| 36 |
+
</div>
|
| 37 |
+
<div>
|
| 38 |
+
<h1 class="text-lg font-bold text-slate-800 leading-tight">Cyber Sentry</h1>
|
| 39 |
+
<p class="text-xs text-slate-500">智能安全中心</p>
|
| 40 |
+
</div>
|
| 41 |
</div>
|
| 42 |
+
<nav class="p-4 space-y-1 flex-1 overflow-y-auto">
|
| 43 |
+
<button @click="currentTab = 'dashboard'" :class="{'bg-blue-50 text-blue-700 font-semibold': currentTab === 'dashboard', 'text-slate-600 hover:bg-slate-50': currentTab !== 'dashboard'}" class="w-full text-left px-4 py-3 rounded-lg transition-all flex items-center group">
|
| 44 |
+
<i class="fas fa-chart-pie w-6 transition-transform group-hover:scale-110"></i> 态势感知 (Dashboard)
|
| 45 |
+
</button>
|
| 46 |
+
<button @click="currentTab = 'analyzer'" :class="{'bg-blue-50 text-blue-700 font-semibold': currentTab === 'analyzer', 'text-slate-600 hover:bg-slate-50': currentTab !== 'analyzer'}" class="w-full text-left px-4 py-3 rounded-lg transition-all flex items-center group">
|
| 47 |
+
<i class="fas fa-search w-6 transition-transform group-hover:scale-110"></i> 日志分析 (Analyzer)
|
| 48 |
</button>
|
| 49 |
+
<button @click="currentTab = 'playbooks'" :class="{'bg-blue-50 text-blue-700 font-semibold': currentTab === 'playbooks', 'text-slate-600 hover:bg-slate-50': currentTab !== 'playbooks'}" class="w-full text-left px-4 py-3 rounded-lg transition-all flex items-center group">
|
| 50 |
+
<i class="fas fa-book w-6 transition-transform group-hover:scale-110"></i> 剧本库 (Playbooks)
|
| 51 |
</button>
|
| 52 |
+
<button @click="currentTab = 'simulation'" :class="{'bg-blue-50 text-blue-700 font-semibold': currentTab === 'simulation', 'text-slate-600 hover:bg-slate-50': currentTab !== 'simulation'}" class="w-full text-left px-4 py-3 rounded-lg transition-all flex items-center group">
|
| 53 |
+
<i class="fas fa-bug w-6 transition-transform group-hover:scale-110"></i> 攻防演练 (Sim)
|
| 54 |
</button>
|
| 55 |
+
<button @click="currentTab = 'datasets'" :class="{'bg-blue-50 text-blue-700 font-semibold': currentTab === 'datasets', 'text-slate-600 hover:bg-slate-50': currentTab !== 'datasets'}" class="w-full text-left px-4 py-3 rounded-lg transition-all flex items-center group">
|
| 56 |
+
<i class="fas fa-database w-6 transition-transform group-hover:scale-110"></i> 数据集 (Datasets)
|
| 57 |
</button>
|
| 58 |
</nav>
|
| 59 |
+
<div class="p-4 border-t border-slate-100">
|
| 60 |
+
<div class="bg-slate-50 rounded-lg p-3 text-xs text-slate-500 flex items-center justify-between">
|
| 61 |
+
<span><i class="fas fa-circle text-green-500 mr-1"></i> System Online</span>
|
| 62 |
+
<span>v2.1.0</span>
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
</aside>
|
| 66 |
|
| 67 |
<!-- Main Content -->
|
| 68 |
+
<main class="flex-1 overflow-y-auto bg-slate-50/50">
|
| 69 |
+
<header class="bg-white border-b border-slate-200 px-8 py-4 flex justify-between items-center sticky top-0 z-20 shadow-sm">
|
| 70 |
+
<h2 class="text-xl font-bold text-slate-800 capitalize">${ currentTabName }</h2>
|
| 71 |
+
<div class="flex items-center space-x-4">
|
| 72 |
+
<button class="w-8 h-8 rounded-full bg-slate-100 hover:bg-slate-200 flex items-center justify-center text-slate-600 transition-colors">
|
| 73 |
+
<i class="fas fa-bell"></i>
|
| 74 |
+
</button>
|
| 75 |
+
<div class="flex items-center space-x-2">
|
| 76 |
+
<div class="w-8 h-8 rounded-full bg-gradient-to-tr from-blue-500 to-indigo-600 flex items-center justify-center text-white text-xs font-bold shadow-md">
|
| 77 |
+
AD
|
| 78 |
+
</div>
|
| 79 |
+
<span class="text-sm font-medium text-slate-700">Admin</span>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
</header>
|
| 83 |
+
|
| 84 |
+
<div class="p-4 md:p-8 max-w-7xl mx-auto min-h-[calc(100vh-80px)]">
|
| 85 |
+
|
| 86 |
|
| 87 |
<!-- Dashboard -->
|
| 88 |
+
<div v-if="currentTab === 'dashboard'" class="space-y-6 animate-fade-in">
|
| 89 |
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
| 90 |
+
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
|
| 91 |
+
<div class="text-sm text-slate-500 mb-1 flex justify-between items-center">
|
| 92 |
+
当前威胁等级 <i class="fas fa-temperature-high text-orange-200"></i>
|
| 93 |
+
</div>
|
| 94 |
+
<div class="text-2xl font-bold text-orange-500 flex items-center gap-2">
|
| 95 |
+
${ stats.threat_level }
|
| 96 |
+
<span v-if="stats.threat_level === 'Critical'" class="flex h-3 w-3 relative">
|
| 97 |
+
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
|
| 98 |
+
<span class="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
|
| 99 |
+
</span>
|
| 100 |
+
</div>
|
| 101 |
</div>
|
| 102 |
+
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
|
| 103 |
+
<div class="text-sm text-slate-500 mb-1 flex justify-between items-center">
|
| 104 |
+
活跃事件 <i class="fas fa-bolt text-red-200"></i>
|
| 105 |
+
</div>
|
| 106 |
<div class="text-2xl font-bold text-red-600">${ stats.active_incidents }</div>
|
| 107 |
</div>
|
| 108 |
+
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
|
| 109 |
+
<div class="text-sm text-slate-500 mb-1 flex justify-between items-center">
|
| 110 |
+
已拦截攻击 <i class="fas fa-shield-virus text-green-200"></i>
|
| 111 |
+
</div>
|
| 112 |
<div class="text-2xl font-bold text-green-600">${ stats.blocked_attempts }</div>
|
| 113 |
</div>
|
| 114 |
+
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
|
| 115 |
+
<div class="text-sm text-slate-500 mb-1 flex justify-between items-center">
|
| 116 |
+
系统健康度 <i class="fas fa-heartbeat text-blue-200"></i>
|
| 117 |
+
</div>
|
| 118 |
<div class="text-2xl font-bold text-blue-600">${ stats.system_health }%</div>
|
| 119 |
</div>
|
| 120 |
</div>
|
| 121 |
|
| 122 |
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
| 123 |
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
|
| 124 |
+
<h3 class="text-lg font-bold mb-4 flex items-center gap-2">
|
| 125 |
+
<i class="fas fa-radar text-blue-500"></i> 威胁分布雷达
|
| 126 |
+
</h3>
|
| 127 |
<div id="radarChart" class="w-full h-80"></div>
|
| 128 |
</div>
|
| 129 |
+
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 flex flex-col">
|
| 130 |
+
<h3 class="text-lg font-bold mb-4 flex items-center gap-2">
|
| 131 |
+
<i class="fas fa-bullhorn text-yellow-500"></i> 系统公告
|
| 132 |
+
</h3>
|
| 133 |
+
<div class="space-y-3 flex-1 overflow-y-auto">
|
| 134 |
+
<div class="p-3 bg-blue-50 rounded-lg text-sm text-blue-800 border-l-4 border-blue-500 shadow-sm">
|
| 135 |
+
<strong>Updated:</strong> Log4j 漏洞补丁已应用到所有集群节点。
|
| 136 |
+
</div>
|
| 137 |
+
<div class="p-3 bg-yellow-50 rounded-lg text-sm text-yellow-800 border-l-4 border-yellow-500 shadow-sm">
|
| 138 |
+
<strong>Warning:</strong> 检测到针对金融部门的钓鱼活动激增,请加强防范。
|
| 139 |
</div>
|
| 140 |
+
<div class="p-3 bg-green-50 rounded-lg text-sm text-green-800 border-l-4 border-green-500 shadow-sm">
|
| 141 |
+
<strong>System:</strong> 数据库自动备份完成 (Size: 2.4GB)。
|
| 142 |
</div>
|
| 143 |
</div>
|
| 144 |
</div>
|
|
|
|
| 146 |
</div>
|
| 147 |
|
| 148 |
<!-- Analyzer -->
|
| 149 |
+
<div v-if="currentTab === 'analyzer'" class="space-y-6 animate-fade-in">
|
| 150 |
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
|
| 151 |
+
<h2 class="text-xl font-bold mb-4 flex items-center gap-2">
|
| 152 |
+
<i class="fas fa-robot text-purple-500"></i> 智能日志分析 (AI Log Analyzer)
|
| 153 |
+
</h2>
|
| 154 |
<p class="text-slate-500 text-sm mb-4">粘贴系统日志,AI 将自动分析威胁等级、提取指标并生成修复建议。</p>
|
| 155 |
+
<textarea v-model="logInput" class="w-full h-40 p-4 bg-slate-50 border border-slate-200 rounded-lg font-mono text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none transition-all" placeholder="在此粘贴日志 (例如: /var/log/auth.log) ..."></textarea>
|
| 156 |
+
<div class="mt-4 flex justify-between items-center">
|
| 157 |
+
<div class="text-xs text-slate-400">
|
| 158 |
+
<i class="fas fa-info-circle mr-1"></i> 支持 Syslog, Apache, Nginx, Linux Auth Logs
|
| 159 |
+
</div>
|
| 160 |
+
<div class="space-x-2">
|
| 161 |
+
<button @click="logInput = ''" class="text-slate-500 hover:text-slate-700 px-4 py-2 text-sm">
|
| 162 |
+
清空
|
| 163 |
+
</button>
|
| 164 |
+
<button @click="analyzeLog" :disabled="analyzing" class="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center shadow-lg shadow-blue-200">
|
| 165 |
+
<i v-if="analyzing" class="fas fa-spinner fa-spin mr-2"></i>
|
| 166 |
+
${ analyzing ? '分析中...' : '开始分析' }
|
| 167 |
+
</button>
|
| 168 |
+
</div>
|
| 169 |
</div>
|
| 170 |
</div>
|
| 171 |
|
| 172 |
+
<div v-if="analysisResult" class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 animate-fade-in border-t-4" :class="{'border-t-green-500': analysisResult.severity === 'Low', 'border-t-yellow-500': analysisResult.severity === 'Medium', 'border-t-orange-500': analysisResult.severity === 'High', 'border-t-red-600': analysisResult.severity === 'Critical'}">
|
| 173 |
<div class="flex items-center justify-between mb-6">
|
| 174 |
+
<h3 class="text-lg font-bold flex items-center gap-2">
|
| 175 |
+
<i class="fas fa-file-medical-alt"></i> 分析报告
|
| 176 |
+
</h3>
|
| 177 |
+
<span :class="{'bg-green-100 text-green-800': analysisResult.severity === 'Low', 'bg-yellow-100 text-yellow-800': analysisResult.severity === 'Medium', 'bg-orange-100 text-orange-800': analysisResult.severity === 'High', 'bg-red-100 text-red-800': analysisResult.severity === 'Critical'}" class="px-3 py-1 rounded-full text-sm font-bold uppercase tracking-wide">
|
| 178 |
${ analysisResult.severity }
|
| 179 |
</span>
|
| 180 |
</div>
|
| 181 |
|
| 182 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
| 183 |
<div>
|
| 184 |
+
<div class="mb-6">
|
| 185 |
+
<h4 class="font-bold text-slate-700 mb-2 flex items-center gap-2"><i class="fas fa-align-left text-slate-400"></i> 摘要</h4>
|
| 186 |
+
<p class="text-slate-600 text-sm leading-relaxed bg-slate-50 p-3 rounded-lg border border-slate-100">${ analysisResult.summary }</p>
|
| 187 |
</div>
|
| 188 |
+
<div class="mb-6">
|
| 189 |
+
<h4 class="font-bold text-slate-700 mb-2 flex items-center gap-2"><i class="fas fa-fingerprint text-slate-400"></i> 发现指标 (IOCs)</h4>
|
| 190 |
<div class="flex flex-wrap gap-2">
|
| 191 |
+
<span v-for="ioc in analysisResult.indicators" class="bg-slate-100 text-slate-600 px-2 py-1 rounded text-xs font-mono border border-slate-200 select-all hover:bg-slate-200 transition-colors cursor-pointer" title="Click to copy">${ ioc }</span>
|
| 192 |
+
<span v-if="!analysisResult.indicators || analysisResult.indicators.length === 0" class="text-slate-400 text-xs italic">无 IOC</span>
|
| 193 |
</div>
|
| 194 |
</div>
|
| 195 |
<div>
|
| 196 |
+
<h4 class="font-bold text-slate-700 mb-2 flex items-center gap-2"><i class="fas fa-clipboard-check text-slate-400"></i> 修复建议</h4>
|
| 197 |
+
<div class="p-4 bg-green-50 text-green-800 rounded-lg text-sm markdown-body border border-green-100 shadow-inner" v-html="renderMarkdown(analysisResult.recommendation)"></div>
|
| 198 |
</div>
|
| 199 |
</div>
|
| 200 |
<div>
|
| 201 |
+
<h4 class="font-bold text-slate-700 mb-2 flex items-center gap-2"><i class="fas fa-chart-line text-slate-400"></i> 流量/活动峰值</h4>
|
| 202 |
+
<div id="mockGraph" class="w-full h-64 bg-slate-50 rounded-lg border border-slate-100"></div>
|
| 203 |
+
|
| 204 |
+
<div class="mt-6">
|
| 205 |
+
<h4 class="font-bold text-slate-700 mb-2 flex items-center gap-2"><i class="fas fa-tools text-slate-400"></i> 快速操作</h4>
|
| 206 |
+
<div class="grid grid-cols-2 gap-3">
|
| 207 |
+
<button class="bg-slate-100 hover:bg-slate-200 text-slate-700 px-4 py-2 rounded-lg text-sm transition-colors flex items-center justify-center gap-2">
|
| 208 |
+
<i class="fas fa-ban"></i> 封禁 IP
|
| 209 |
+
</button>
|
| 210 |
+
<button class="bg-slate-100 hover:bg-slate-200 text-slate-700 px-4 py-2 rounded-lg text-sm transition-colors flex items-center justify-center gap-2">
|
| 211 |
+
<i class="fas fa-envelope"></i> 发送报告
|
| 212 |
+
</button>
|
| 213 |
+
</div>
|
| 214 |
+
</div>
|
| 215 |
</div>
|
| 216 |
</div>
|
| 217 |
</div>
|
| 218 |
</div>
|
| 219 |
|
| 220 |
<!-- Playbooks -->
|
| 221 |
+
<div v-if="currentTab === 'playbooks'" class="space-y-6 animate-fade-in">
|
| 222 |
+
<div class="flex justify-between items-center bg-white p-4 rounded-xl shadow-sm border border-slate-100">
|
| 223 |
+
<div>
|
| 224 |
+
<h2 class="text-xl font-bold text-slate-800">安全剧本库 (SOP Vault)</h2>
|
| 225 |
+
<p class="text-slate-500 text-xs">标准操作程序与应急响应指南</p>
|
| 226 |
+
</div>
|
| 227 |
+
<button @click="showAddPlaybook = true" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 text-sm shadow-md shadow-blue-200 transition-transform active:scale-95 flex items-center gap-2">
|
| 228 |
+
<i class="fas fa-plus"></i> 新增剧本
|
| 229 |
</button>
|
| 230 |
</div>
|
| 231 |
|
| 232 |
<!-- Add Modal -->
|
| 233 |
+
<div v-if="showAddPlaybook" class="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50 transition-all">
|
| 234 |
+
<div class="bg-white rounded-xl max-w-lg w-full p-6 shadow-2xl transform transition-all scale-100">
|
| 235 |
+
<h3 class="text-lg font-bold mb-4 flex items-center gap-2"><i class="fas fa-edit text-blue-500"></i> 新增响应剧本</h3>
|
| 236 |
<div class="space-y-4">
|
| 237 |
+
<div>
|
| 238 |
+
<label class="block text-xs font-bold text-slate-500 mb-1">剧本标题</label>
|
| 239 |
+
<input v-model="newPlaybook.title" placeholder="例如: 数据库泄露响应" class="w-full p-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none">
|
| 240 |
+
</div>
|
| 241 |
+
<div>
|
| 242 |
+
<label class="block text-xs font-bold text-slate-500 mb-1">触发场景</label>
|
| 243 |
+
<input v-model="newPlaybook.scenario" placeholder="例如: 检测到敏感数据外发" class="w-full p-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none">
|
| 244 |
+
</div>
|
| 245 |
+
<div>
|
| 246 |
+
<label class="block text-xs font-bold text-slate-500 mb-1">严重等级</label>
|
| 247 |
+
<select v-model="newPlaybook.severity" class="w-full p-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none bg-white">
|
| 248 |
+
<option>Low</option>
|
| 249 |
+
<option>Medium</option>
|
| 250 |
+
<option>High</option>
|
| 251 |
+
<option>Critical</option>
|
| 252 |
+
</select>
|
| 253 |
+
</div>
|
| 254 |
+
<div>
|
| 255 |
+
<label class="block text-xs font-bold text-slate-500 mb-1">响应步骤 (Markdown)</label>
|
| 256 |
+
<textarea v-model="newPlaybook.steps" placeholder="1. 步骤一..." class="w-full p-2 border border-slate-200 rounded-lg h-32 focus:ring-2 focus:ring-blue-500 focus:outline-none font-mono text-sm"></textarea>
|
| 257 |
+
</div>
|
| 258 |
</div>
|
| 259 |
<div class="mt-6 flex justify-end space-x-3">
|
| 260 |
+
<button @click="showAddPlaybook = false" class="text-slate-500 hover:text-slate-700 px-4 py-2 rounded-lg hover:bg-slate-100 transition-colors">取消</button>
|
| 261 |
+
<button @click="addPlaybook" class="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 shadow-lg shadow-blue-200 transition-colors">保存剧本</button>
|
| 262 |
</div>
|
| 263 |
</div>
|
| 264 |
</div>
|
| 265 |
|
| 266 |
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 267 |
+
<div v-for="pb in playbooks" :key="pb.id" class="bg-white rounded-xl shadow-sm border border-slate-100 hover:shadow-lg transition-all overflow-hidden group">
|
| 268 |
+
<div class="p-5 flex flex-col h-full">
|
| 269 |
<div class="flex justify-between items-start mb-3">
|
| 270 |
+
<h3 class="font-bold text-lg text-slate-800 group-hover:text-blue-600 transition-colors">${ pb.title }</h3>
|
| 271 |
+
<span class="text-xs px-2 py-1 rounded-full border font-bold" :class="{'bg-red-50 text-red-600 border-red-100': pb.severity==='Critical', 'bg-orange-50 text-orange-600 border-orange-100': pb.severity==='High', 'bg-yellow-50 text-yellow-600 border-yellow-100': pb.severity==='Medium', 'bg-green-50 text-green-600 border-green-100': pb.severity==='Low'}">
|
| 272 |
${ pb.severity }
|
| 273 |
</span>
|
| 274 |
</div>
|
| 275 |
+
<p class="text-sm text-slate-500 mb-4 h-10 overflow-hidden text-ellipsis line-clamp-2" title="${ pb.scenario }">${ pb.scenario }</p>
|
| 276 |
+
<div class="flex-1 bg-slate-50 p-3 rounded-lg border border-slate-100 font-mono text-xs text-slate-600 h-32 overflow-y-auto custom-scrollbar leading-relaxed whitespace-pre-wrap">${ pb.steps }</div>
|
| 277 |
+
<div class="mt-4 flex justify-end opacity-0 group-hover:opacity-100 transition-opacity">
|
| 278 |
+
<button class="text-slate-400 hover:text-blue-600 text-xs flex items-center gap-1">
|
| 279 |
+
<i class="fas fa-external-link-alt"></i> 查看详情
|
| 280 |
+
</button>
|
| 281 |
+
</div>
|
| 282 |
</div>
|
| 283 |
</div>
|
| 284 |
</div>
|
| 285 |
</div>
|
| 286 |
|
| 287 |
<!-- Simulation -->
|
| 288 |
+
<div v-if="currentTab === 'simulation'" class="space-y-6 animate-fade-in">
|
| 289 |
+
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 text-center py-16 relative overflow-hidden">
|
| 290 |
+
<div class="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-red-500 via-orange-500 to-yellow-500"></div>
|
| 291 |
+
<div class="w-24 h-24 bg-red-50 rounded-full flex items-center justify-center mx-auto mb-6 shadow-inner">
|
| 292 |
+
<i class="fas fa-biohazard text-red-500 text-4xl animate-pulse"></i>
|
| 293 |
</div>
|
| 294 |
+
<h2 class="text-2xl font-bold mb-2 text-slate-800">红蓝对抗演练 (Breach Simulator)</h2>
|
| 295 |
+
<p class="text-slate-500 mb-8 max-w-md mx-auto">生成逼真的攻击场景,测试您的应急响应能力和团队协作水平。</p>
|
| 296 |
+
<button @click="runSimulation" :disabled="simulating" class="bg-red-600 text-white px-8 py-3 rounded-xl hover:bg-red-700 shadow-xl shadow-red-200 transition-all transform hover:scale-105 disabled:opacity-50 disabled:scale-100 flex items-center mx-auto gap-2">
|
| 297 |
+
<i class="fas fa-rocket" :class="{'animate-bounce': simulating}"></i>
|
| 298 |
+
${ simulating ? '正在生成攻击场景...' : '发起模拟攻击' }
|
| 299 |
</button>
|
| 300 |
</div>
|
| 301 |
|
| 302 |
+
<div v-if="simulationResult" class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 border-l-4 border-red-500 animate-fade-in-up">
|
| 303 |
+
<div class="flex items-center justify-between mb-4">
|
| 304 |
+
<h3 class="text-red-600 font-bold text-lg flex items-center gap-2">
|
| 305 |
+
<i class="fas fa-exclamation-triangle"></i> 新威胁告警
|
| 306 |
+
</h3>
|
| 307 |
+
<span class="text-xs text-slate-400">Just now</span>
|
| 308 |
+
</div>
|
| 309 |
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 text-sm">
|
| 310 |
+
<div class="bg-slate-50 p-4 rounded-lg border border-slate-100">
|
| 311 |
+
<span class="text-slate-500 block text-xs uppercase tracking-wider mb-1">攻击类型</span>
|
| 312 |
+
<span class="font-bold text-lg text-slate-800">${ simulationResult.type }</span>
|
| 313 |
+
</div>
|
| 314 |
+
<div class="md:col-span-2 bg-slate-50 p-4 rounded-lg border border-slate-100">
|
| 315 |
+
<span class="text-slate-500 block text-xs uppercase tracking-wider mb-1">详情描述</span>
|
| 316 |
+
<span class="text-slate-700">${ simulationResult.details }</span>
|
| 317 |
+
</div>
|
| 318 |
+
<div class="md:col-span-3">
|
| 319 |
+
<span class="text-slate-500 block text-xs uppercase tracking-wider mb-1">原始日志</span>
|
| 320 |
+
<div class="font-mono bg-slate-900 text-green-400 p-4 rounded-lg text-xs overflow-x-auto shadow-inner relative group">
|
| 321 |
+
${ simulationResult.log }
|
| 322 |
+
<button @click="copyToClipboard(simulationResult.log)" class="absolute top-2 right-2 text-slate-500 hover:text-white opacity-0 group-hover:opacity-100 transition-opacity">
|
| 323 |
+
<i class="fas fa-copy"></i>
|
| 324 |
+
</button>
|
| 325 |
+
</div>
|
| 326 |
</div>
|
| 327 |
</div>
|
| 328 |
+
<div class="mt-6 flex justify-end">
|
| 329 |
+
<button @click="copyToAnalyzer(simulationResult.log)" class="bg-blue-50 text-blue-600 px-4 py-2 rounded-lg hover:bg-blue-100 transition-colors text-sm font-bold flex items-center gap-2">
|
| 330 |
+
发送至分析器 <i class="fas fa-arrow-right"></i>
|
| 331 |
+
</button>
|
| 332 |
+
</div>
|
| 333 |
+
</div>
|
| 334 |
+
</div>
|
| 335 |
+
|
| 336 |
+
<!-- Datasets (New) -->
|
| 337 |
+
<div v-if="currentTab === 'datasets'" class="space-y-6 animate-fade-in">
|
| 338 |
+
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
|
| 339 |
+
<div class="flex justify-between items-center mb-6">
|
| 340 |
+
<div>
|
| 341 |
+
<h2 class="text-xl font-bold text-slate-800 flex items-center gap-2">
|
| 342 |
+
<i class="fas fa-database text-indigo-500"></i> 数据集管理
|
| 343 |
+
</h2>
|
| 344 |
+
<p class="text-slate-500 text-sm">管理上传的日志文件和数据集,用于微调模型或历史回溯。</p>
|
| 345 |
+
</div>
|
| 346 |
+
<button @click="triggerUpload" class="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 text-sm shadow-md shadow-indigo-200 transition-all flex items-center gap-2">
|
| 347 |
+
<i class="fas fa-cloud-upload-alt"></i> 上传数据
|
| 348 |
</button>
|
| 349 |
+
<input type="file" ref="fileInput" @change="handleFileUpload" class="hidden" accept=".log,.txt,.csv,.json">
|
| 350 |
+
</div>
|
| 351 |
+
|
| 352 |
+
<div v-if="uploading" class="w-full bg-slate-100 rounded-full h-2.5 mb-6 overflow-hidden">
|
| 353 |
+
<div class="bg-indigo-600 h-2.5 rounded-full animate-pulse" style="width: 100%"></div>
|
| 354 |
+
</div>
|
| 355 |
+
|
| 356 |
+
<div class="overflow-x-auto">
|
| 357 |
+
<table class="w-full text-sm text-left text-slate-500">
|
| 358 |
+
<thead class="text-xs text-slate-700 uppercase bg-slate-50">
|
| 359 |
+
<tr>
|
| 360 |
+
<th scope="col" class="px-6 py-3 rounded-l-lg">文件名</th>
|
| 361 |
+
<th scope="col" class="px-6 py-3">大小</th>
|
| 362 |
+
<th scope="col" class="px-6 py-3">上传日期</th>
|
| 363 |
+
<th scope="col" class="px-6 py-3 rounded-r-lg text-right">操作</th>
|
| 364 |
+
</tr>
|
| 365 |
+
</thead>
|
| 366 |
+
<tbody>
|
| 367 |
+
<tr v-for="file in datasets" :key="file.name" class="bg-white border-b hover:bg-slate-50 transition-colors">
|
| 368 |
+
<td class="px-6 py-4 font-medium text-slate-900 flex items-center gap-2">
|
| 369 |
+
<i class="far fa-file-alt text-slate-400"></i> ${ file.name }
|
| 370 |
+
</td>
|
| 371 |
+
<td class="px-6 py-4">${ file.size }</td>
|
| 372 |
+
<td class="px-6 py-4">${ file.date }</td>
|
| 373 |
+
<td class="px-6 py-4 text-right">
|
| 374 |
+
<button class="text-blue-600 hover:text-blue-900 font-medium text-xs">下载</button>
|
| 375 |
+
</td>
|
| 376 |
+
</tr>
|
| 377 |
+
<tr v-if="datasets.length === 0">
|
| 378 |
+
<td colspan="4" class="px-6 py-8 text-center text-slate-400 italic">
|
| 379 |
+
暂无数据,请上传文件。
|
| 380 |
+
</td>
|
| 381 |
+
</tr>
|
| 382 |
+
</tbody>
|
| 383 |
+
</table>
|
| 384 |
</div>
|
| 385 |
</div>
|
| 386 |
</div>
|
|
|
|
| 389 |
</div>
|
| 390 |
|
| 391 |
<script>
|
| 392 |
+
const { createApp, ref, onMounted, nextTick, computed } = Vue;
|
| 393 |
|
| 394 |
createApp({
|
| 395 |
delimiters: ['${', '}'],
|
|
|
|
| 404 |
const newPlaybook = ref({ title: '', scenario: '', steps: '', severity: 'Medium' });
|
| 405 |
const simulating = ref(false);
|
| 406 |
const simulationResult = ref(null);
|
| 407 |
+
const datasets = ref([]);
|
| 408 |
+
const uploading = ref(false);
|
| 409 |
+
const fileInput = ref(null);
|
| 410 |
+
|
| 411 |
let radarChart = null;
|
| 412 |
+
let trafficChart = null;
|
| 413 |
+
|
| 414 |
+
const currentTabName = computed(() => {
|
| 415 |
+
const names = {
|
| 416 |
+
'dashboard': '态势感知',
|
| 417 |
+
'analyzer': '日志分析',
|
| 418 |
+
'playbooks': '剧本库',
|
| 419 |
+
'simulation': '攻防演练',
|
| 420 |
+
'datasets': '数据集管理'
|
| 421 |
+
};
|
| 422 |
+
return names[currentTab.value] || 'Dashboard';
|
| 423 |
+
});
|
| 424 |
|
| 425 |
const fetchStats = async () => {
|
| 426 |
+
try {
|
| 427 |
+
const res = await fetch('/api/stats');
|
| 428 |
+
const data = await res.json();
|
| 429 |
+
stats.value = data;
|
| 430 |
+
initRadar(data.radar_data);
|
| 431 |
+
} catch (e) {
|
| 432 |
+
console.error("Failed to fetch stats", e);
|
| 433 |
+
}
|
| 434 |
+
};
|
| 435 |
+
|
| 436 |
+
const fetchDatasets = async () => {
|
| 437 |
+
try {
|
| 438 |
+
const res = await fetch('/api/datasets');
|
| 439 |
+
datasets.value = await res.json();
|
| 440 |
+
} catch (e) {
|
| 441 |
+
console.error("Failed to fetch datasets", e);
|
| 442 |
+
}
|
| 443 |
};
|
| 444 |
|
| 445 |
const initRadar = (data) => {
|
|
|
|
| 448 |
radarChart = echarts.init(document.getElementById('radarChart'));
|
| 449 |
radarChart.setOption({
|
| 450 |
color: ['#3b82f6'],
|
| 451 |
+
tooltip: {},
|
| 452 |
radar: {
|
| 453 |
indicator: data.map(d => ({ name: d.name, max: 100 })),
|
| 454 |
shape: 'circle',
|
|
|
|
| 458 |
type: 'radar',
|
| 459 |
data: [{
|
| 460 |
value: data.map(d => d.value),
|
| 461 |
+
name: '当前威胁分布',
|
| 462 |
+
areaStyle: { opacity: 0.2, color: '#3b82f6' },
|
| 463 |
+
lineStyle: { width: 2 }
|
| 464 |
}]
|
| 465 |
}]
|
| 466 |
});
|
| 467 |
+
|
| 468 |
+
// Responsive resize
|
| 469 |
+
window.addEventListener('resize', () => radarChart && radarChart.resize());
|
| 470 |
};
|
| 471 |
|
| 472 |
const analyzeLog = async () => {
|
|
|
|
| 484 |
if (analysisResult.value.mock_graph) initLineChart(analysisResult.value.mock_graph);
|
| 485 |
});
|
| 486 |
} catch (e) {
|
| 487 |
+
alert('Analysis failed: ' + e);
|
| 488 |
} finally {
|
| 489 |
analyzing.value = false;
|
| 490 |
}
|
|
|
|
| 493 |
const initLineChart = (data) => {
|
| 494 |
const el = document.getElementById('mockGraph');
|
| 495 |
if (!el) return;
|
| 496 |
+
if (trafficChart) trafficChart.dispose();
|
| 497 |
+
trafficChart = echarts.init(el);
|
| 498 |
+
trafficChart.setOption({
|
| 499 |
+
grid: { top: 20, bottom: 20, left: 40, right: 20 },
|
| 500 |
+
tooltip: { trigger: 'axis' },
|
| 501 |
+
xAxis: { type: 'category', data: ['T-5', 'T-4', 'T-3', 'T-2', 'Now'] },
|
| 502 |
+
yAxis: { type: 'value', splitLine: { lineStyle: { type: 'dashed' } } },
|
| 503 |
series: [{
|
| 504 |
data: data,
|
| 505 |
type: 'line',
|
| 506 |
smooth: true,
|
| 507 |
+
areaStyle: { opacity: 0.2, color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{offset: 0, color: '#ef4444'}, {offset: 1, color: '#fee2e2'}]) },
|
| 508 |
+
itemStyle: { color: '#ef4444' },
|
| 509 |
+
lineStyle: { width: 3 }
|
| 510 |
}]
|
| 511 |
});
|
| 512 |
};
|
|
|
|
| 517 |
};
|
| 518 |
|
| 519 |
const addPlaybook = async () => {
|
| 520 |
+
if(!newPlaybook.value.title) return;
|
| 521 |
await fetch('/api/playbooks', {
|
| 522 |
method: 'POST',
|
| 523 |
headers: {'Content-Type': 'application/json'},
|
|
|
|
| 531 |
const runSimulation = async () => {
|
| 532 |
simulating.value = true;
|
| 533 |
simulationResult.value = null;
|
| 534 |
+
// Mock delay for realism
|
| 535 |
+
await new Promise(r => setTimeout(r, 2000));
|
| 536 |
const res = await fetch('/api/simulate', { method: 'POST' });
|
| 537 |
simulationResult.value = await res.json();
|
| 538 |
simulating.value = false;
|
|
|
|
| 541 |
const copyToAnalyzer = (log) => {
|
| 542 |
currentTab.value = 'analyzer';
|
| 543 |
logInput.value = log;
|
| 544 |
+
// Auto analyze after switch
|
| 545 |
+
setTimeout(() => analyzeLog(), 500);
|
| 546 |
+
};
|
| 547 |
+
|
| 548 |
+
const copyToClipboard = (text) => {
|
| 549 |
+
navigator.clipboard.writeText(text);
|
| 550 |
+
// Could add a toast here
|
| 551 |
};
|
| 552 |
|
| 553 |
const renderMarkdown = (text) => {
|
| 554 |
+
if (!text) return '';
|
| 555 |
+
return marked.parse(text);
|
| 556 |
+
};
|
| 557 |
+
|
| 558 |
+
const triggerUpload = () => {
|
| 559 |
+
fileInput.value.click();
|
| 560 |
+
};
|
| 561 |
+
|
| 562 |
+
const handleFileUpload = async (event) => {
|
| 563 |
+
const file = event.target.files[0];
|
| 564 |
+
if (!file) return;
|
| 565 |
+
|
| 566 |
+
uploading.value = true;
|
| 567 |
+
const formData = new FormData();
|
| 568 |
+
formData.append('file', file);
|
| 569 |
+
|
| 570 |
+
try {
|
| 571 |
+
const res = await fetch('/api/upload', {
|
| 572 |
+
method: 'POST',
|
| 573 |
+
body: formData
|
| 574 |
+
});
|
| 575 |
+
const data = await res.json();
|
| 576 |
+
if (res.ok) {
|
| 577 |
+
alert(`Upload Success: ${data.message}\nAnalysis: ${data.analysis}`);
|
| 578 |
+
fetchDatasets(); // Refresh list
|
| 579 |
+
} else {
|
| 580 |
+
alert('Upload Failed: ' + data.error);
|
| 581 |
+
}
|
| 582 |
+
} catch (e) {
|
| 583 |
+
alert('Upload Error: ' + e);
|
| 584 |
+
} finally {
|
| 585 |
+
uploading.value = false;
|
| 586 |
+
// Reset input
|
| 587 |
+
event.target.value = '';
|
| 588 |
+
}
|
| 589 |
};
|
| 590 |
|
| 591 |
onMounted(() => {
|
| 592 |
fetchStats();
|
| 593 |
fetchPlaybooks();
|
| 594 |
+
fetchDatasets();
|
| 595 |
+
|
| 596 |
+
// Poll stats every 10s for liveliness
|
| 597 |
+
setInterval(fetchStats, 10000);
|
| 598 |
});
|
| 599 |
|
| 600 |
return {
|
| 601 |
+
currentTab,
|
| 602 |
+
currentTabName,
|
| 603 |
+
stats,
|
| 604 |
+
logInput,
|
| 605 |
+
analyzing,
|
| 606 |
+
analysisResult,
|
| 607 |
+
playbooks,
|
| 608 |
+
showAddPlaybook,
|
| 609 |
+
newPlaybook,
|
| 610 |
+
simulating,
|
| 611 |
+
simulationResult,
|
| 612 |
+
datasets,
|
| 613 |
+
uploading,
|
| 614 |
+
fileInput,
|
| 615 |
+
analyzeLog,
|
| 616 |
+
addPlaybook,
|
| 617 |
+
runSimulation,
|
| 618 |
+
copyToAnalyzer,
|
| 619 |
+
renderMarkdown,
|
| 620 |
+
triggerUpload,
|
| 621 |
+
handleFileUpload,
|
| 622 |
+
copyToClipboard
|
| 623 |
};
|
| 624 |
}
|
| 625 |
}).mount('#app');
|