4 Commits

15 changed files with 82 additions and 570 deletions
+2 -2
View File
@@ -11,8 +11,8 @@
:ipd.orgn,目标IP组织名称,字符串,97.61%
:ips.city,源IP所在城市,字符串,99.26%
:ipd.city,目标IP所在城市,字符串,98.74%
:ips.anon,源IP匿名状态,"anon, hosting"或无,12.66%
:ipd.anon,目标IP匿名状态,"anon, hosting"或无,15.54%
:ips.anon,源IP匿名状态,"anon, hosting""anon, vpn"或"anon, server"或,12.66%
:ipd.anon,目标IP匿名状态,"anon, hosting""anon, vpn"或"anon, server"或,15.54%
:ips.doma,源IP域名,字符串,28.91%
:ipd.doma,目标IP域名,字符串,13.48%
:prs,源端口,整数,100%
Can't render this file because it contains an unexpected character in line 14 and column 43.
+1 -1
View File
@@ -1 +1 @@
v1.1.6
v2.0.0beta
+2 -2
View File
@@ -62,7 +62,7 @@ class Command(BaseCommand):
and schema[c].split('(')[0].strip() in numeric_types
][:10]
# ── Step 2: Clustering + Extraction + PCA (shared pipeline) ─────
# ── Step 2: Clustering + Extraction + UMAP (shared pipeline) ─────
_self.stdout.write(f'[2/3] 聚类 ({algo})')
_self.stdout.write(f' → 聚类特征: {feature_cols}')
run.total_flows = row_count
@@ -72,7 +72,7 @@ class Command(BaseCommand):
run=run, store=store, entity_ds_id=ds_id,
feature_columns=feature_cols,
algorithm=algo, min_cluster_size=5,
run_pca=True,
run_umap=True,
)
if run.status == 'failed':
_self.stderr.write(_self.style.ERROR(f' ✗ 分析失败: {run.error_message}'))
+2 -2
View File
@@ -94,8 +94,8 @@ class EntityProfile(models.Model):
related_name='entities')
feature_json = models.JSONField(default=dict, blank=True,
help_text="Aggregated entity features as dict")
embedding_x = models.FloatField(null=True, blank=True, help_text="PCA-2D X coordinate")
embedding_y = models.FloatField(null=True, blank=True, help_text="PCA-2D Y coordinate")
embedding_x = models.FloatField(null=True, blank=True, help_text="UMAP-2D X coordinate")
embedding_y = models.FloatField(null=True, blank=True, help_text="UMAP-2D Y coordinate")
class Meta:
unique_together = ['run', 'entity_value']
+1
View File
@@ -252,6 +252,7 @@ class SessionStore:
'parent_id': entry.get('parent_id'),
'row_count': meta.get('row_count'),
'file_count': meta.get('file_count'),
'svd_components': meta.get('svd_components', 0),
'columns': list(entry.get('schema', {}).keys()),
'column_count': len(entry.get('schema', {})),
})
+5 -5
View File
@@ -498,7 +498,7 @@ def get_tools_meta() -> list[Tool]:
Tool(
name="compute_scores",
description=(
"在实体数据上计算自适应代理/异常/威胁评分。使用PCA从数据自动学习"
"在实体数据上计算自适应代理/异常/威胁评分。从数据自动学习"
"特征权重,计算加权代理评分(proxy_score)、基于IQR的风险等级"
"normal/watch/suspicious/critical)和威胁评分(threat_score)。"
"返回评分摘要与阈值。"
@@ -539,7 +539,7 @@ def get_tools_meta() -> list[Tool]:
Tool(
name="visualize_anomalies",
description=(
"为高风险实体生成PCA-2D嵌入和聚类可视化数据。按风险等级和聚类着色。"
"为高风险实体生成UMAP-2D嵌入和聚类可视化数据。按风险等级和聚类着色。"
"返回用于前端渲染的散点图JSON数据。"
),
inputSchema={
@@ -608,7 +608,7 @@ def get_tools_meta() -> list[Tool]:
name="diagnose_clustering",
description=(
"诊断工具:当聚类产生太少/太多聚类、全部为噪声或轮廓系数低时——诊断原因。"
"检查:特征方差、相关矩阵、PCA解释方差、数据稀疏性,并建议参数调整"
"检查:特征方差、相关矩阵、特征降维分析、数据稀疏性,并建议参数调整"
"min_cluster_size、算法选择、特征选择)。"
),
inputSchema={
@@ -1598,7 +1598,7 @@ async def _handle_extract_features(
metric='euclidean')
coords = reducer.fit_transform(data_scaled)
non_numeric = [c for c in df.columns if c not in numeric_cols and not c.startswith('_')]
entity_col_pca = non_numeric[0] if non_numeric else df.columns[0]
entity_col_umap = non_numeric[0] if non_numeric else df.columns[0]
labels_list = cluster_entry.get('labels', [])
# Save: create EntityProfile records with coordinates
from analysis.models import EntityProfile as EP
@@ -1616,7 +1616,7 @@ async def _handle_extract_features(
cr_cache = {}
for i, row in enumerate(df.iter_rows(named=True)):
if i >= len(coords): break
ev = str(row.get(entity_col_pca, ''))
ev = str(row.get(entity_col_umap, ''))
if not ev: continue
lbl = labels_list[i] if i < len(labels_list) else -1
cache_key = f'{run_id_for_ep}_{lbl}'
+60 -15
View File
@@ -580,6 +580,50 @@ def _background_process(run_id, upload_dir):
logger.info('[BACKGROUND] Restored numeric types for %d columns: %s',
len(type_map), list(type_map.keys())[:10])
# ── SVD dimensionality reduction ──
svd_components = 0
# Only operate on columns with known numeric types in the schema
numeric_dtypes = {'Int64', 'Float64', 'Int32', 'Float32',
'Int8', 'Int16', 'UInt8', 'UInt16', 'UInt32', 'UInt64'}
numeric_cols = [c for c, dt in schema.items() if dt in numeric_dtypes]
n_numeric = len(numeric_cols)
if n_numeric >= 2:
n_components = min(50, n_numeric - 1)
try:
from sklearn.decomposition import TruncatedSVD # fmt: skip
import numpy as np # fmt: skip
# Collect numeric data for SVD (full materialise → transform → add back)
df_full = lf.select(numeric_cols).collect(streaming=True)
X = df_full.to_numpy()
X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0)
svd = TruncatedSVD(n_components=n_components, random_state=42)
X_svd = svd.fit_transform(X)
svd_components = n_components
# Build a new LazyFrame that includes _svd_* columns
# Strategy: collect full LazyFrame once, add SVD columns, re-wrap as lazy
df_all = lf.collect(streaming=True)
for i in range(n_components):
col_name = f'_svd_{i}'
schema[col_name] = 'Float64'
df_all = df_all.with_columns(
pl.Series(col_name, X_svd[:, i])
)
lf = df_all.lazy()
logger.info('[BACKGROUND] SVD: %d components from %d numeric columns '
'(explained variance ratio sum=%.4f)',
svd_components, n_numeric,
float(svd.explained_variance_ratio_.sum()))
except Exception as e:
logger.warning('[BACKGROUND] SVD failed (non-fatal): %s', e)
elif n_numeric == 1:
logger.info('[BACKGROUND] SVD skipped: only 1 numeric column (%s)',
numeric_cols[0])
# Get row count
df_count = lf.select(pl.len()).collect(streaming=True)
total_rows = df_count[0, 0] if df_count.height > 0 else 0
@@ -593,6 +637,7 @@ def _background_process(run_id, upload_dir):
'row_count': total_rows, 'file_count': total_files, 'upload_dir': str(upload_dir),
'csv_glob': str(upload_dir / '*.csv'),
'sqlite_table': table_name,
'svd_components': svd_components,
})
# ── Phase 3: Data ready for filtering/clustering via MCP tools ──
@@ -730,7 +775,7 @@ def _run_pipeline_worker(run_id, analysis_fn, **ctx):
# ── Shared clustering pipeline ─────────────────────────────────────────
def _run_clustering_pipeline(run, store, entity_ds_id, feature_columns, algorithm,
min_cluster_size, run_pca=True, head=None):
min_cluster_size, run_umap=True, head=None):
"""Unified clustering pipeline: clustering → feature extraction → UMAP → ORM save.
Args:
@@ -740,7 +785,7 @@ def _run_clustering_pipeline(run, store, entity_ds_id, feature_columns, algorith
feature_columns: list of feature column names (None = auto-detect).
algorithm: 'hdbscan' or 'kmeans'.
min_cluster_size: int for HDBSCAN.
run_pca: bool, whether to compute and save UMAP-2D embeddings.
run_umap: bool, whether to compute and save UMAP-2D embeddings.
head: optional int, downsample to at most this many rows (min 100).
"""
import warnings
@@ -851,7 +896,7 @@ def _run_clustering_pipeline(run, store, entity_ds_id, feature_columns, algorith
raise Exception(feat_result['error'])
# ── Step 3: UMAP-2D embedding (sampled training for large data) ─────────
if run_pca:
if run_umap:
try:
lf = entry['lazyframe']
# UMAP: sample max 10K for training, batch-transform rest in 1K batches
@@ -876,11 +921,11 @@ def _run_clustering_pipeline(run, store, entity_ds_id, feature_columns, algorith
run.progress_msg = f'UMAP 采样 {MAX_UMAP_TRAIN}/{n_total} 实体训练...'
run.save(update_fields=['progress_msg'])
df_sample = lf.sample(n=MAX_UMAP_TRAIN, seed=42).collect(streaming=True)
df_pca = lf.collect(streaming=True)
df_umap = lf.collect(streaming=True)
else:
df_pca = lf.collect(streaming=True)
df_umap = lf.collect(streaming=True)
if len(df_pca) > 2:
if len(df_umap) > 2:
if n_total > MAX_UMAP_TRAIN:
# Train UMAP on 10K sample, batch-transform full dataset
mat_sample = df_sample.select(num_cols).to_numpy()
@@ -892,23 +937,23 @@ def _run_clustering_pipeline(run, store, entity_ds_id, feature_columns, algorith
reducer.fit(mat_sample_scaled)
# Batch transform in 1K-row batches
coords_list = []
for start in range(0, len(df_pca), BATCH_SIZE):
end = min(start + BATCH_SIZE, len(df_pca))
for start in range(0, len(df_umap), BATCH_SIZE):
end = min(start + BATCH_SIZE, len(df_umap))
mat_batch = scaler.transform(
df_pca[start:end].select(num_cols).to_numpy())
df_umap[start:end].select(num_cols).to_numpy())
mat_batch = np.nan_to_num(mat_batch, nan=0.0)
coords_list.append(reducer.transform(mat_batch))
coords = np.vstack(coords_list)
else:
mat = np.nan_to_num(StandardScaler().fit_transform(
df_pca.select(num_cols).to_numpy()), nan=0.0)
df_umap.select(num_cols).to_numpy()), nan=0.0)
reducer = umap.UMAP(n_components=2, random_state=42,
n_neighbors=15, min_dist=0.1,
metric='euclidean')
coords = reducer.fit_transform(mat)
non_num = [c for c in df_pca.columns if c not in num_cols and not c.startswith('_')]
ent_col = non_num[0] if non_num else df_pca.columns[0]
non_num = [c for c in df_umap.columns if c not in num_cols and not c.startswith('_')]
ent_col = non_num[0] if non_num else df_umap.columns[0]
cluster_entry = store.get_cluster_result(cluster_id)
labels_list = cluster_entry.get('labels', []) if cluster_entry else []
@@ -918,7 +963,7 @@ def _run_clustering_pipeline(run, store, entity_ds_id, feature_columns, algorith
profiles = []
cr_cache = {}
for i, row in enumerate(df_pca.iter_rows(named=True)):
for i, row in enumerate(df_umap.iter_rows(named=True)):
ev = str(row.get(ent_col, ''))
if not ev:
continue
@@ -1019,7 +1064,7 @@ def manual_run_analysis(request):
run=run, store=store, entity_ds_id=ds_id,
feature_columns=feature_columns,
algorithm=algorithm, min_cluster_size=min_cluster_size,
run_pca=True, head=head,
run_umap=True, head=head,
)
except Exception:
tb = traceback.format_exc()
@@ -1253,7 +1298,7 @@ def run_llm_analysis_view(request):
feature_columns=None,
algorithm='hdbscan',
min_cluster_size=5,
run_pca=True,
run_umap=True,
head=ctx.get('head'),
)
else:
+2 -2
View File
@@ -28,7 +28,7 @@
</div>
<div class="card">
<h2>PCA-2D Embedding</h2>
<h2>UMAP-2D Embedding</h2>
<div class="chart-wrapper">
<div class="chart-container">
<canvas id="scatterChart" class="scatter-canvas"></canvas>
@@ -189,7 +189,7 @@ function drawScatter(canvasId, data, xLabel, yLabel) {
}
}
drawScatter('scatterChart', scatterData, 'PCA-1', 'PCA-2');
drawScatter('scatterChart', scatterData, 'UMAP-1', 'UMAP-2');
{% if geo_count > 0 %}
const geoData = {{ geo_data_json|safe }};
+1 -1
View File
@@ -37,7 +37,7 @@
{% if entity.embedding_x is not None and entity.embedding_y is not None %}
<div class="card">
<h2>PCA Position</h2>
<h2>UMAP Position</h2>
<p>X: {{ entity.embedding_x|floatformat:3 }}, Y: {{ entity.embedding_y|floatformat:3 }}</p>
<canvas id="entityScatter" class="scatter-canvas" height="300"></canvas>
</div>
+3 -1
View File
@@ -194,7 +194,7 @@ function buildDatasetTable(filter) {
return;
}
let html = '<table class="ds-table"><thead><tr><th>ID</th><th>状态</th><th>行数</th><th>文件数</th><th></th></tr></thead><tbody>';
let html = '<table class="ds-table"><thead><tr><th>ID</th><th>状态</th><th>行数</th><th>文件数</th><th>SVD</th><th></th></tr></thead><tbody>';
rows.forEach(ds => {
const label = ds.status_label || '待分析';
const badgeClass = (ds.run_status === 'completed') ? 'completed' :
@@ -203,11 +203,13 @@ function buildDatasetTable(filter) {
const dispId = ds.display_id || ds.dataset_id;
const rowCount = ds.row_count != null ? ds.row_count.toLocaleString() : '?';
const fileCount = ds.file_count != null ? ds.file_count : '?';
const svdInfo = ds.svd_components ? `SVD: ${ds.svd_components}维` : '-';
html += `<tr data-ds-id="${ds.dataset_id}" onclick="selectDataset('${ds.dataset_id}')">
<td style="font-weight:600;">${dispId}</td>
<td><span class="status-badge ${badgeClass}">${label}</span></td>
<td>${rowCount}</td>
<td>${fileCount}</td>
<td>${svdInfo}</td>
<td><button class="btn" onclick="event.stopPropagation();selectDataset('${ds.dataset_id}')" style="font-size:0.75rem;padding:0.2rem 0.5rem;">选择</button></td>
</tr>`;
});
+1 -1
View File
@@ -1,7 +1,7 @@
"""E2E batch upload test: upload 10 CSV files (2 batches of 5), cluster, verify."""
import subprocess, time, os, sys, glob, json
PROJ = r"C:\Users\25044\Desktop\Proj\天璇"
PROJ = r"E:\hjq\天璇"
PORT = 8765
BASE = f"http://127.0.0.1:{PORT}"
-229
View File
@@ -1,229 +0,0 @@
"""E2E test v1.1.4: upload 10 CSVs → cluster → verify globe.
Full pipeline WITHOUT manual server start/stop.
Port 8766 avoids conflicts with 8000.
Uses stdlib urllib (no pip install needed).
"""
import subprocess
import time
import sys
import os
import glob
import json
import uuid
import urllib.request
import urllib.error
PORT = 8766
BASE = f"http://127.0.0.1:{PORT}"
PROJ = r"C:\Users\25044\Desktop\Proj\天璇"
def http_get(path, timeout=30):
"""GET and return (status_code, body_bytes)."""
try:
r = urllib.request.urlopen(f"{BASE}{path}", timeout=timeout)
body = r.read()
code = r.status
r.close()
return code, body
except urllib.error.HTTPError as e:
return e.code, e.read()
def http_post_json(path, data_dict, timeout=30):
"""POST JSON and return (status_code, parsed_json)."""
body = json.dumps(data_dict).encode("utf-8")
req = urllib.request.Request(
f"{BASE}{path}", data=body, headers={"Content-Type": "application/json"}
)
try:
r = urllib.request.urlopen(req, timeout=timeout)
return r.status, json.loads(r.read())
except urllib.error.HTTPError as e:
return e.code, json.loads(e.read())
def http_post_files(path, file_paths, timeout=120):
"""POST multipart/form-data upload. Returns (status_code, parsed_json)."""
boundary = uuid.uuid4().hex
lines = []
for fp in file_paths:
fname = os.path.basename(fp)
with open(fp, "rb") as f:
filedata = f.read()
lines.append(f"--{boundary}".encode())
lines.append(
f'Content-Disposition: form-data; name="files"; filename="{fname}"'.encode()
)
lines.append(b"Content-Type: text/csv")
lines.append(b"")
lines.append(filedata)
lines.append(f"--{boundary}--".encode())
body = b"\r\n".join(lines)
req = urllib.request.Request(
f"{BASE}{path}",
data=body,
headers={"Content-Type": f"multipart/form-data; boundary={boundary}"},
)
try:
r = urllib.request.urlopen(req, timeout=timeout)
return r.status, json.loads(r.read())
except urllib.error.HTTPError as e:
return e.code, json.loads(e.read())
# ── 1. Start server ──
print("[1/6] Starting Django server...")
import tempfile as _tmp
_stderr_log = os.path.join(PROJ, "tests", "_server_stderr.log")
_stderr_fh = open(_stderr_log, "w")
server = subprocess.Popen(
[
os.path.join(PROJ, "runtime", "python", "python.exe"),
"manage.py",
"runserver",
f"127.0.0.1:{PORT}",
"--noreload",
],
cwd=PROJ,
stdout=_stderr_fh,
stderr=subprocess.STDOUT,
)
time.sleep(8)
# Verify server is up
for attempt in range(10):
try:
urllib.request.urlopen(f"{BASE}/", timeout=3)
print(" Server is up.")
break
except Exception:
if attempt == 9:
print(" FAIL: Server did not start within 30 seconds")
server.terminate()
server.wait()
sys.exit(1)
time.sleep(2)
# ── 2. Upload 10 test_*.csv files ──
csv_files = sorted(glob.glob(os.path.join(PROJ, "data", "multi_upload", "test_*.csv")))
print(f"[2/6] Found {len(csv_files)} CSV files. Uploading first 10...")
batch = csv_files[:10]
code, data = http_post_files("/upload/csv/", batch, timeout=120)
if code != 200:
print(f" FAIL: Upload returned HTTP {code}: {data}")
server.terminate()
server.wait()
sys.exit(1)
run_id = data["run_id"]
print(f" Uploaded 10 files → run_id={run_id}, status={data['status']}")
# ── 3. Poll until upload processing completes (status='ready') ──
print("[3/6] Waiting for background processing...")
s = {}
for i in range(180):
code, body = http_get(f"/runs/{run_id}/status/", timeout=10)
s = json.loads(body) if code == 200 else {}
if s.get("status") in ("ready", "failed"):
break
if i % 10 == 0:
print(f" Poll #{i}: {s.get('status')} ({s.get('progress_pct', 0)}%)")
time.sleep(2)
print(f" Status: {s.get('status')}, progress={s.get('progress_pct')}%")
if s.get("status") == "failed":
print(f" FAIL: {s.get('error_message', '')[:500]}")
server.terminate()
server.wait()
sys.exit(1)
if s.get("status") != "ready":
print(f" FAIL: Timed out waiting for 'ready', got '{s.get('status')}'")
server.terminate()
server.wait()
sys.exit(1)
# ── 4. Run clustering ──
print("[4/6] Starting clustering (HDBSCAN, min_cluster_size=5)...")
code, resp = http_post_json(
"/analyze/run/",
{"run_id": run_id, "algorithm": "hdbscan", "min_cluster_size": 5, "cluster_mode": "raw"},
timeout=30,
)
print(f" Response HTTP {code}: {resp.get('status')}")
if code != 200:
print(f" FAIL: Clustering start failed: {resp}")
server.terminate()
server.wait()
sys.exit(1)
# ── 5. Poll until clustering completes ──
print("[5/6] Waiting for clustering...")
for i in range(120):
code, body = http_get(f"/runs/{run_id}/status/", timeout=10)
s = json.loads(body) if code == 200 else {}
if s.get("status") in ("completed", "failed"):
break
if i % 5 == 0:
print(
f" Poll #{i}: {s.get('status')} ({s.get('progress_pct', 0)}%)"
f" - {s.get('progress_msg', '')}"
)
time.sleep(3)
print(
f" Final: status={s.get('status')}, entity_count={s.get('entity_count')},"
f" cluster_count={s.get('cluster_count')}"
)
# ── 6. Verify globe page ──
print("[6/6] Verifying globe page...")
code, body = http_get(f"/globe/?runs={run_id}", timeout=30)
globe_text = body.decode("utf-8", errors="replace")
has_arcs = "arcCount" in globe_text
print(f" Globe HTTP {code}, arcCount present: {has_arcs}")
# ── Cleanup ──
server.terminate()
server.wait()
_stderr_fh.close()
# Print last 30 lines of server output on failure
_need_server_log = False
# ── Report ──
errors = []
if s.get("status") != "completed":
errors.append(f"Expected status='completed', got '{s.get('status')}'")
errors.append(f"error: {s.get('error_message', '')[:500]}")
if s.get("cluster_count", 0) == 0:
errors.append("No clusters found (cluster_count=0)")
if not has_arcs:
errors.append("Globe page missing 'arcCount' element")
if errors:
print(f"\nFAIL ({len(errors)} errors):")
for e in errors:
print(f" - {e}")
# Show server stderr tail
if os.path.exists(_stderr_log):
with open(_stderr_log, "r", encoding="utf-8", errors="replace") as f:
lines = f.readlines()
tail = lines[-30:] if len(lines) > 30 else lines
print(f"\n--- Server stderr (last {len(tail)} lines) ---")
for line in tail:
print(f" {line.rstrip()}")
sys.exit(1)
print("\n*** E2E v1.1.4 PASSED ***")
print(
f" run_id={run_id}, clusters={s.get('cluster_count')},"
f" entities={s.get('entity_count')}, globe_arcs={'yes' if has_arcs else 'no'}"
)
-237
View File
@@ -1,237 +0,0 @@
"""E2E test v1.1.6: generate 50 CSVs → upload 10 → cluster → verify globe.
Self-contained: generates data, starts server, uploads, clusters, verifies globe, reports.
Port 18766. Uses requests for HTTP (installed automatically if missing).
"""
import subprocess
import time
import sys
import os
import glob
import json
import urllib.request
import urllib.error
PORT = 18766
BASE = f"http://127.0.0.1:{PORT}"
PROJ = r"C:\Users\25044\Desktop\Proj\天璇"
PYTHON = os.path.join(PROJ, "runtime", "python", "python.exe")
# ── Ensure requests is installed ──
try:
import requests
except ImportError:
print("[0] Installing requests...")
subprocess.run([PYTHON, "-m", "pip", "install", "requests", "-q"], cwd=PROJ, check=True, timeout=120)
import requests
def http_get(path, timeout=30):
"""GET and return (status_code, parsed_json or body_bytes)."""
try:
r = requests.get(f"{BASE}{path}", timeout=timeout)
try:
return r.status_code, r.json()
except Exception:
return r.status_code, r.text
except Exception as e:
return 0, str(e)
def http_post_json(path, data_dict, timeout=30):
"""POST JSON and return (status_code, parsed_json)."""
try:
r = requests.post(f"{BASE}{path}", json=data_dict, timeout=timeout)
try:
return r.status_code, r.json()
except Exception:
return r.status_code, r.text
except Exception as e:
return 0, str(e)
def http_post_files(path, file_paths, timeout=120):
"""POST multipart/form-data upload. Returns (status_code, parsed_json)."""
files = [("files", (os.path.basename(fp), open(fp, "rb"), "text/csv")) for fp in file_paths]
try:
r = requests.post(f"{BASE}{path}", files=files, timeout=timeout)
return r.status_code, r.json()
except Exception as e:
return 0, str(e)
def poll_status(run_id, target_status, max_wait_sec, description):
"""Poll /runs/<run_id>/status/ until target_status or timeout. Returns status dict."""
s = {}
for i in range(max_wait_sec // 2):
code, body = http_get(f"/runs/{run_id}/status/", timeout=10)
s = body if isinstance(body, dict) else {}
if s.get("status") in (target_status, "failed"):
break
if i % 10 == 0:
print(f" [{description}] Poll #{i}: {s.get('status')} ({s.get('progress_pct', 0)}%)")
time.sleep(2)
print(f" [{description}] Final: status={s.get('status')}, progress={s.get('progress_pct', 0)}%")
return s
# ═══════════════════════════════════════════════════════════════════════════
# MAIN
# ═══════════════════════════════════════════════════════════════════════════
errors = []
# ── 1. Generate 50 test CSV files ──
print("[1/6] Generating 50 test CSV files (100 rows each)...")
gen_result = subprocess.run(
[PYTHON, "scripts/gen_multi.py", "--files", "50", "--rows", "100"],
cwd=PROJ, capture_output=True, text=True, timeout=180,
)
if gen_result.returncode != 0:
print(f" FAIL: gen_multi.py failed.\nSTDERR:\n{gen_result.stderr}")
sys.exit(1)
print(gen_result.stdout.strip())
csv_files = sorted(glob.glob(os.path.join(PROJ, "data", "multi_upload", "test_*.csv")))
print(f" Generated {len(csv_files)} CSV files.")
# ── 2. Start Django server ──
print(f"[2/6] Starting Django server on port {PORT}...")
stderr_log = os.path.join(PROJ, "tests", "_e2e_server_v116.log")
stderr_fh = open(stderr_log, "w")
server = subprocess.Popen(
[PYTHON, "manage.py", "runserver", f"127.0.0.1:{PORT}", "--noreload"],
cwd=PROJ, stdout=stderr_fh, stderr=subprocess.STDOUT,
)
time.sleep(6)
# Verify server is up
server_up = False
for attempt in range(15):
try:
urllib.request.urlopen(f"{BASE}/", timeout=3)
server_up = True
print(" Server is up.")
break
except Exception:
if attempt == 14:
errors.append("Server did not start within 45 seconds")
time.sleep(2)
if not server_up:
server.terminate(); server.wait(); stderr_fh.close()
print("\nFAIL")
for e in errors: print(f" - {e}")
sys.exit(1)
# ── 3. Upload first 10 CSV files ──
print("[3/6] Uploading first 10 CSV files...")
batch = csv_files[:10]
code, data = http_post_files("/upload/csv/", batch, timeout=180)
if code != 200:
errors.append(f"Upload returned HTTP {code}: {str(data)[:300]}")
server.terminate(); server.wait(); stderr_fh.close()
print("\nFAIL")
for e in errors: print(f" - {e}")
sys.exit(1)
run_id = data["run_id"]
print(f" Uploaded {len(batch)} files → run_id={run_id}, status={data['status']}")
# ── 4. Poll until background processing completes (status='ready') ──
print("[4/6] Waiting for background upload processing (max 300s)...")
s = poll_status(run_id, "ready", 300, "Upload processing")
if s.get("status") == "failed":
errors.append(f"Upload processing failed: {s.get('error_message', '')[:500]}")
if s.get("status") not in ("ready",):
errors.append(f"Expected status='ready', got '{s.get('status')}'")
if errors:
server.terminate(); server.wait(); stderr_fh.close()
print("\nFAIL")
for e in errors: print(f" - {e}")
sys.exit(1)
# ── 5. Run clustering and poll ──
print("[5/6] Starting clustering (HDBSCAN, min_cluster_size=5)...")
code, resp = http_post_json(
"/analyze/run/",
{"run_id": run_id, "algorithm": "hdbscan", "min_cluster_size": 5, "cluster_mode": "raw"},
timeout=30,
)
print(f" Response HTTP {code}: {resp if isinstance(resp, dict) else str(resp)[:200]}")
if code != 200:
errors.append(f"Clustering start failed: {resp}")
server.terminate(); server.wait(); stderr_fh.close()
print("\nFAIL")
for e in errors: print(f" - {e}")
sys.exit(1)
s_cluster = poll_status(run_id, "completed", 600, "Clustering")
if s_cluster.get("status") == "failed":
errors.append(f"Clustering failed: {s_cluster.get('error_message', '')[:500]}")
if s_cluster.get("status") != "completed":
errors.append(f"Expected status='completed', got '{s_cluster.get('status')}'")
# ── 6. Verify globe page ──
print("[6/6] Verifying globe page...")
code, globe_body = http_get(f"/globe/?runs={run_id}", timeout=30)
globe_text = globe_body if isinstance(globe_body, str) else json.dumps(globe_body)
has_arcs = "arcCount" in globe_text if isinstance(globe_text, str) else False
print(f" Globe HTTP {code}, arcCount present: {has_arcs}")
# ── Cleanup ──
server.terminate()
server.wait()
stderr_fh.close()
# ── Collect metrics ──
total_flows = s_cluster.get("total_flows", "?")
entity_count = s_cluster.get("entity_count", 0)
cluster_count = s_cluster.get("cluster_count", 0)
status = s_cluster.get("status", "unknown")
# ── Validate required metrics ──
if not has_arcs:
errors.append("Globe page missing 'arcCount' element")
if entity_count == 0:
errors.append("entity_count=0 (no entities detected)")
if cluster_count == 0:
errors.append("cluster_count=0 (no clusters found)")
# ── Show server log tail on failure ──
if errors and os.path.exists(stderr_log):
with open(stderr_log, "r", encoding="utf-8", errors="replace") as f:
lines = f.readlines()
tail = lines[-40:] if len(lines) > 40 else lines
print(f"\n--- Server log tail ({len(tail)} lines) ---")
for line in tail:
print(f" {line.rstrip()}")
# ── Report ──
print()
print("=" * 60)
print(" E2E v1.1.6 RESULTS")
print("=" * 60)
print(f" run_id : {run_id}")
print(f" status : {status}")
print(f" total_flows : {total_flows}")
print(f" entity_count : {entity_count}")
print(f" cluster_count : {cluster_count}")
print(f" globe_arcs : {'PASS' if has_arcs else 'FAIL'}")
if errors:
print(f"\n FAIL ({len(errors)} errors):")
for e in errors:
print(f" - {e}")
sys.exit(1)
else:
print(f"\n *** E2E v1.1.6 PASSED ***")
print(f" run_id={run_id}, clusters={cluster_count}, entities={entity_count}, globe_arcs=yes")
sys.exit(0)
-70
View File
@@ -1,70 +0,0 @@
import subprocess, time, os, sys, glob, json
PORT = 17766
PROJ = r"C:\Users\25044\Desktop\Proj\天璇"
os.chdir(PROJ)
# 1. Start server
print("Starting server...")
server = subprocess.Popen(
[r"runtime\python\python.exe", "manage.py", "runserver", f"127.0.0.1:{PORT}", "--noreload"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
time.sleep(6)
import requests
# 2. Upload 50 files
print("Uploading 50 files...")
csvs = sorted(glob.glob("data/multi_upload/[0-9]*.csv"))[:50]
files = [("files", (os.path.basename(f), open(f, "rb"), "text/csv")) for f in csvs]
r = requests.post(f"http://127.0.0.1:{PORT}/upload/csv/", files=files, timeout=300)
data = r.json()
run_id = data["run_id"]
print(f"run_id={run_id}")
# 3. Poll until ready
for _ in range(180):
r = requests.get(f"http://127.0.0.1:{PORT}/runs/{run_id}/status/", timeout=10)
s = r.json()
st = s["status"]
if st in ("ready", "failed"):
break
time.sleep(2)
print(f"Upload status={st}, flows={s.get('total_flows')}, error={s.get('error_message','')[:300]}")
if st == "failed":
print(f"FAILED: {s.get('error_message','')}")
server.terminate(); sys.exit(1)
# 4. Run clustering
print("Running clustering...")
r = requests.post(f"http://127.0.0.1:{PORT}/analyze/run/",
json={"run_id": run_id, "algorithm": "hdbscan", "min_cluster_size": 5, "cluster_mode": "raw"},
timeout=30)
print(f"Cluster started: {r.json()}")
# 5. Poll until completed
for _ in range(120):
r = requests.get(f"http://127.0.0.1:{PORT}/runs/{run_id}/status/", timeout=10)
s = r.json()
st = s["status"]
if st in ("completed", "failed"):
break
time.sleep(5)
print(f"Cluster status={st}, entity_count={s.get('entity_count')}, cluster_count={s.get('cluster_count')}")
# 6. Verify globe
r = requests.get(f"http://127.0.0.1:{PORT}/globe/?runs={run_id}", timeout=10)
has_arcs = "arcCount" in r.text
print(f"Globe arcCount present: {has_arcs}")
# 7. Result
server.terminate()
server.wait()
if st == "completed" and s.get("cluster_count", 0) > 0:
print(f"E2E PASSED: {s['entity_count']} entities, {s['cluster_count']} clusters")
sys.exit(0)
else:
print(f"E2E FAILED")
sys.exit(1)
+2 -2
View File
@@ -19,7 +19,7 @@ TOOLS = [
"description": "按实体列(源IP、SNI等)分组原始流。生成每个实体的特征(流数量、TLS版本、端口)。需要已概要分析的数据。",
"parameters": {"type": "object", "properties": {"dataset_id": {"type": "string"}, "entity_column": {"type": "string"}, "entity_columns": {"type": "array", "items": {"type": "string"}}, "auto_detect": {"type": "boolean"}}, "required": ["dataset_id"]}}},
{"type": "function", "function": {"name": "compute_scores",
"description": "使用PCA权重学习在实体数据上计算自适应代理/异常/威胁评分。在build_entity_profiles之后调用。添加proxy_score/risk_level/threat_score列。",
"description": "使用自适应权重学习在实体数据上计算自适应代理/异常/威胁评分。在build_entity_profiles之后调用。添加proxy_score/risk_level/threat_score列。",
"parameters": {"type": "object", "properties": {"dataset_id": {"type": "string"}}, "required": ["dataset_id"]}}},
{"type": "function", "function": {"name": "run_clustering",
"description": "将实体数据聚类为行为组。需要实体数据集ID。探索性分析选择hdbscan(自动检测聚类数);固定簇数选择kmeans。",
@@ -31,7 +31,7 @@ TOOLS = [
"description": "运行孤立森林(Isolation Forest)检测异常实体。在compute_scores之后调用。返回异常数量和比率。",
"parameters": {"type": "object", "properties": {"dataset_id": {"type": "string"}, "contamination": {"type": "number", "description": "Expected anomaly rate 0.01-0.1 (default 0.05)"}}, "required": ["dataset_id"]}}},
{"type": "function", "function": {"name": "visualize_anomalies",
"description": "生成PCA-2D散点数据用于前端展示。按风险等级着色。在compute_scores或detect_anomalies之后调用。",
"description": "生成UMAP-2D散点数据用于前端展示。按风险等级着色。在compute_scores或detect_anomalies之后调用。",
"parameters": {"type": "object", "properties": {"dataset_id": {"type": "string"}}, "required": ["dataset_id"]}}},
# ── Diagnostic (call when pipeline fails or results look wrong) ──
{"type": "function", "function": {"name": "validate_data",