mirror of
https://github.com/yyhuni/xingrin.git
synced 2026-01-31 11:46:16 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
766f045904 | ||
|
|
8acfe1cc33 | ||
|
|
7aec3eabb2 | ||
|
|
b1f11c36a4 | ||
|
|
d97fb5245a | ||
|
|
ddf9a1f5a4 | ||
|
|
47f9f96a4b | ||
|
|
6f43e73162 | ||
|
|
9b7d496f3e | ||
|
|
6390849d52 | ||
|
|
7a6d2054f6 | ||
|
|
73ebaab232 | ||
|
|
11899b29c2 | ||
|
|
877d2a56d1 |
@@ -181,7 +181,7 @@ sudo ./install.sh
|
||||
|
||||
### 访问服务
|
||||
|
||||
- **Web 界面**: `https://localhost`
|
||||
- **Web 界面**: `https://ip:8083`
|
||||
|
||||
### 常用命令
|
||||
|
||||
|
||||
@@ -242,8 +242,9 @@ class WorkerDeployConsumer(AsyncWebsocketConsumer):
|
||||
return
|
||||
|
||||
# 远程 Worker 通过 nginx HTTPS 访问(nginx 反代到后端 8888)
|
||||
# 使用 https://{PUBLIC_HOST} 而不是直连 8888 端口
|
||||
heartbeat_api_url = f"https://{public_host}" # 基础 URL,agent 会加 /api/...
|
||||
# 使用 https://{PUBLIC_HOST}:{PUBLIC_PORT} 而不是直连 8888 端口
|
||||
public_port = getattr(settings, 'PUBLIC_PORT', '8083')
|
||||
heartbeat_api_url = f"https://{public_host}:{public_port}"
|
||||
|
||||
session_name = f'xingrin_deploy_{self.worker_id}'
|
||||
remote_script_path = '/tmp/xingrin_deploy.sh'
|
||||
|
||||
@@ -234,7 +234,7 @@ class TaskDistributor:
|
||||
else:
|
||||
# 远程:通过 Nginx 反向代理访问(HTTPS,不直连 8888 端口)
|
||||
network_arg = ""
|
||||
server_url = f"https://{settings.PUBLIC_HOST}"
|
||||
server_url = f"https://{settings.PUBLIC_HOST}:{settings.PUBLIC_PORT}"
|
||||
|
||||
# 挂载路径(所有节点统一使用固定路径)
|
||||
host_results_dir = settings.HOST_RESULTS_DIR # /opt/xingrin/results
|
||||
@@ -251,9 +251,7 @@ class TaskDistributor:
|
||||
"-e PREFECT_SERVER_EPHEMERAL_ENABLED=true", # 启用 ephemeral server(本地临时服务器)
|
||||
"-e PREFECT_SERVER_EPHEMERAL_STARTUP_TIMEOUT_SECONDS=120", # 增加启动超时时间
|
||||
"-e PREFECT_SERVER_DATABASE_CONNECTION_URL=sqlite+aiosqlite:////tmp/.prefect/prefect.db", # 使用 /tmp 下的 SQLite
|
||||
"-e PREFECT_LOGGING_LEVEL=DEBUG", # 启用 DEBUG 级别日志
|
||||
"-e PREFECT_LOGGING_SERVER_LEVEL=DEBUG", # Server 日志级别
|
||||
"-e PREFECT_DEBUG_MODE=true", # 启用调试模式
|
||||
"-e PREFECT_LOGGING_LEVEL=WARNING", # 日志级别(减少 DEBUG 噪音)
|
||||
]
|
||||
|
||||
# 挂载卷
|
||||
|
||||
@@ -238,7 +238,7 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
|
||||
docker run -d --pull=always \
|
||||
--name xingrin-agent \
|
||||
--restart always \
|
||||
-e HEARTBEAT_API_URL="https://{django_settings.PUBLIC_HOST}" \
|
||||
-e HEARTBEAT_API_URL="https://{django_settings.PUBLIC_HOST}:{getattr(django_settings, 'PUBLIC_PORT', '8083')}" \
|
||||
-e WORKER_ID="{worker_id}" \
|
||||
-e IMAGE_TAG="{target_version}" \
|
||||
-v /proc:/host/proc:ro \
|
||||
|
||||
@@ -483,13 +483,23 @@ def _run_scans_concurrently(
|
||||
logger.warning("没有有效的扫描任务")
|
||||
continue
|
||||
|
||||
# 使用 ThreadPoolTaskRunner 并发执行
|
||||
logger.info("开始并发提交 %d 个扫描任务...", len(scan_params_list))
|
||||
# ============================================================
|
||||
# 分批执行策略:控制实际并发的 ffuf 进程数
|
||||
# ============================================================
|
||||
total_tasks = len(scan_params_list)
|
||||
logger.info("开始分批执行 %d 个扫描任务(每批 %d 个)...", total_tasks, max_workers)
|
||||
|
||||
with ThreadPoolTaskRunner(max_workers=max_workers) as task_runner:
|
||||
# 提交所有任务
|
||||
batch_num = 0
|
||||
for batch_start in range(0, total_tasks, max_workers):
|
||||
batch_end = min(batch_start + max_workers, total_tasks)
|
||||
batch_params = scan_params_list[batch_start:batch_end]
|
||||
batch_num += 1
|
||||
|
||||
logger.info("执行第 %d 批任务(%d-%d/%d)...", batch_num, batch_start + 1, batch_end, total_tasks)
|
||||
|
||||
# 提交当前批次的任务(非阻塞,立即返回 future)
|
||||
futures = []
|
||||
for params in scan_params_list:
|
||||
for params in batch_params:
|
||||
future = run_and_stream_save_directories_task.submit(
|
||||
cmd=params['command'],
|
||||
tool_name=tool_name,
|
||||
@@ -504,12 +514,10 @@ def _run_scans_concurrently(
|
||||
)
|
||||
futures.append((params['idx'], params['site_url'], future))
|
||||
|
||||
logger.info("✓ 已提交 %d 个扫描任务,等待完成...", len(futures))
|
||||
|
||||
# 等待所有任务完成并聚合结果
|
||||
# 等待当前批次所有任务完成(阻塞,确保本批完成后再启动下一批)
|
||||
for idx, site_url, future in futures:
|
||||
try:
|
||||
result = future.result()
|
||||
result = future.result() # 阻塞等待单个任务完成
|
||||
directories_found = result.get('created_directories', 0)
|
||||
total_directories += directories_found
|
||||
processed_sites_count += 1
|
||||
@@ -521,7 +529,6 @@ def _run_scans_concurrently(
|
||||
|
||||
except Exception as exc:
|
||||
failed_sites.append(site_url)
|
||||
# 判断是否为超时异常
|
||||
if 'timeout' in str(exc).lower() or isinstance(exc, subprocess.TimeoutExpired):
|
||||
logger.warning(
|
||||
"⚠️ [%d/%d] 站点扫描超时: %s - 错误: %s",
|
||||
|
||||
@@ -83,7 +83,8 @@ def ensure_wordlist_local(wordlist_name: str) -> str:
|
||||
"无法确定 Django API 地址:请配置 SERVER_URL 或 PUBLIC_HOST 环境变量"
|
||||
)
|
||||
# 远程 Worker 通过 nginx HTTPS 访问,不再直连 8888
|
||||
api_base = f"https://{public_host}/api"
|
||||
public_port = getattr(settings, 'PUBLIC_PORT', '8083')
|
||||
api_base = f"https://{public_host}:{public_port}/api"
|
||||
query = urllib_parse.urlencode({'wordlist': wordlist_name})
|
||||
download_url = f"{api_base.rstrip('/')}/wordlists/download/?{query}"
|
||||
|
||||
|
||||
@@ -290,6 +290,7 @@ NUCLEI_TEMPLATES_REPO_URL = os.getenv('NUCLEI_TEMPLATES_REPO_URL', 'https://gith
|
||||
|
||||
# 对外访问主机与端口(供 Worker 访问 Django 使用)
|
||||
PUBLIC_HOST = os.getenv('PUBLIC_HOST', 'localhost').strip()
|
||||
PUBLIC_PORT = os.getenv('PUBLIC_PORT', '8083').strip() # 对外 HTTPS 端口
|
||||
SERVER_PORT = os.getenv('SERVER_PORT', '8888')
|
||||
|
||||
# ============================================
|
||||
|
||||
@@ -125,20 +125,31 @@ class TestDataGenerator:
|
||||
print("👷 创建 Worker 节点...")
|
||||
cur = self.conn.cursor()
|
||||
|
||||
# 生成随机后缀确保唯一性
|
||||
suffix = random.randint(1000, 9999)
|
||||
|
||||
regions = ['asia-singapore', 'asia-tokyo', 'asia-hongkong', 'europe-frankfurt', 'europe-london',
|
||||
'us-east-virginia', 'us-west-oregon', 'us-central-iowa', 'australia-sydney', 'brazil-saopaulo']
|
||||
statuses = ['online', 'offline', 'pending', 'deploying', 'maintenance']
|
||||
|
||||
workers = [
|
||||
('local-worker-primary-node-for-internal-scanning-tasks', '127.0.0.1', True, 'online'),
|
||||
('remote-worker-asia-pacific-region-singapore-datacenter-01', '192.168.1.100', False, 'online'),
|
||||
('remote-worker-europe-west-region-frankfurt-datacenter-02', '192.168.1.101', False, 'offline'),
|
||||
('remote-worker-north-america-east-region-virginia-datacenter-03', '192.168.1.102', False, 'pending'),
|
||||
('remote-worker-asia-pacific-region-tokyo-datacenter-04', '192.168.1.103', False, 'deploying'),
|
||||
(f'local-worker-primary-{suffix}', '127.0.0.1', True, 'online'),
|
||||
]
|
||||
|
||||
# 随机生成 4-8 个远程 worker
|
||||
num_remote = random.randint(4, 8)
|
||||
selected_regions = random.sample(regions, min(num_remote, len(regions)))
|
||||
for i, region in enumerate(selected_regions):
|
||||
ip = f'192.168.{random.randint(1, 254)}.{random.randint(1, 254)}'
|
||||
status = random.choice(statuses)
|
||||
workers.append((f'remote-worker-{region}-{suffix}-{i:02d}', ip, False, status))
|
||||
|
||||
ids = []
|
||||
for name, ip, is_local, status in workers:
|
||||
cur.execute("""
|
||||
INSERT INTO worker_node (name, ip_address, ssh_port, username, password, is_local, status, created_at, updated_at)
|
||||
VALUES (%s, %s, 22, 'root', '', %s, %s, NOW(), NOW())
|
||||
ON CONFLICT DO NOTHING
|
||||
ON CONFLICT (name) DO UPDATE SET updated_at = NOW()
|
||||
RETURNING id
|
||||
""", (name, ip, is_local, status))
|
||||
row = cur.fetchone()
|
||||
@@ -153,19 +164,37 @@ class TestDataGenerator:
|
||||
print("⚙️ 创建扫描引擎...")
|
||||
cur = self.conn.cursor()
|
||||
|
||||
engines = [
|
||||
('Full-Comprehensive-Security-Assessment-Engine-With-All-Modules-Enabled', 'subdomain_discovery:\n enabled: true\n tools: [subfinder, amass]\nvulnerability_scanning:\n enabled: true'),
|
||||
('Quick-Reconnaissance-Engine-For-Fast-Surface-Discovery-Only', 'subdomain_discovery:\n enabled: true\n tools: [subfinder]\n timeout: 600'),
|
||||
('Deep-Vulnerability-Assessment-Engine-With-Extended-Nuclei-Templates', 'vulnerability_scanning:\n enabled: true\n nuclei:\n severity: critical,high,medium,low,info'),
|
||||
('Passive-Information-Gathering-Engine-No-Active-Probing', 'subdomain_discovery:\n enabled: true\n passive_only: true'),
|
||||
suffix = random.randint(1000, 9999)
|
||||
|
||||
engine_templates = [
|
||||
('Full-Comprehensive-Security-Assessment', 'subdomain_discovery:\n enabled: true\n tools: [subfinder, amass]\nvulnerability_scanning:\n enabled: true\n nuclei:\n severity: critical,high,medium,low,info\n rate_limit: {rate}\n concurrency: {conc}'),
|
||||
('Quick-Reconnaissance-Fast-Discovery', 'subdomain_discovery:\n enabled: true\n tools: [subfinder]\n timeout: {timeout}\nport_scanning:\n enabled: true\n top_ports: {ports}'),
|
||||
('Deep-Vulnerability-Assessment-Extended', 'vulnerability_scanning:\n enabled: true\n nuclei:\n severity: critical,high,medium,low,info\n templates: [cves, vulnerabilities, exposures]\n rate_limit: {rate}'),
|
||||
('Passive-Information-Gathering-OSINT', 'subdomain_discovery:\n enabled: true\n passive_only: true\n sources: [crtsh, hackertarget, threatcrowd]\n timeout: {timeout}'),
|
||||
('Web-Application-Security-Scanner', 'web_discovery:\n enabled: true\n httpx:\n threads: {conc}\nvulnerability_scanning:\n enabled: true\n dalfox:\n enabled: true'),
|
||||
('API-Endpoint-Security-Audit', 'endpoint_discovery:\n enabled: true\n katana:\n depth: {depth}\n concurrency: {conc}\nvulnerability_scanning:\n enabled: true'),
|
||||
('Infrastructure-Port-Scanner', 'port_scanning:\n enabled: true\n naabu:\n top_ports: {ports}\n rate: {rate}\n service_detection: true'),
|
||||
('Directory-Bruteforce-Engine', 'directory_bruteforce:\n enabled: true\n ffuf:\n threads: {conc}\n wordlist: common.txt\n recursion_depth: {depth}'),
|
||||
]
|
||||
|
||||
# 随机选择 4-6 个引擎模板
|
||||
num_engines = random.randint(4, 6)
|
||||
selected = random.sample(engine_templates, min(num_engines, len(engine_templates)))
|
||||
|
||||
ids = []
|
||||
for name, config in engines:
|
||||
for name_base, config_template in selected:
|
||||
name = f'{name_base}-{suffix}'
|
||||
config = config_template.format(
|
||||
rate=random.choice([100, 150, 200, 300]),
|
||||
conc=random.choice([10, 20, 50, 100]),
|
||||
timeout=random.choice([300, 600, 900, 1200]),
|
||||
ports=random.choice([100, 1000, 'full']),
|
||||
depth=random.choice([2, 3, 4, 5])
|
||||
)
|
||||
cur.execute("""
|
||||
INSERT INTO scan_engine (name, configuration, created_at, updated_at)
|
||||
VALUES (%s, %s, NOW(), NOW())
|
||||
ON CONFLICT (name) DO NOTHING
|
||||
ON CONFLICT (name) DO UPDATE SET configuration = EXCLUDED.configuration, updated_at = NOW()
|
||||
RETURNING id
|
||||
""", (name, config))
|
||||
row = cur.fetchone()
|
||||
@@ -180,25 +209,39 @@ class TestDataGenerator:
|
||||
print("🏢 创建组织...")
|
||||
cur = self.conn.cursor()
|
||||
|
||||
orgs = [
|
||||
('Acme Corporation International Holdings Limited - Global Technology Division', '全球领先的技术解决方案提供商,专注于企业级软件开发、云计算服务和网络安全解决方案。'),
|
||||
('TechStart Innovation Labs - Research and Development Center', '专注于人工智能、机器学习和区块链技术研发的创新实验室。'),
|
||||
('Global Financial Services Group - Digital Banking Platform', '提供全方位数字银行服务的金融科技公司,包括移动支付、在线贷款、投资理财等服务。'),
|
||||
('HealthCare Plus Medical Systems - Electronic Health Records Division', '医疗信息化解决方案提供商,专注于电子病历系统、医院信息管理系统和远程医疗平台开发。'),
|
||||
('E-Commerce Mega Platform - Asia Pacific Regional Operations', '亚太地区最大的电子商务平台之一,提供 B2B、B2C 和 C2C 多种交易模式。'),
|
||||
('Smart City Infrastructure Solutions - IoT and Sensor Networks', '智慧城市基础设施解决方案提供商,专注于物联网传感器网络、智能交通系统。'),
|
||||
('Educational Technology Consortium - Online Learning Platform', '在线教育技术联盟,提供 K-12 和高等教育在线学习平台。'),
|
||||
('Green Energy Solutions - Renewable Power Management Systems', '可再生能源管理系统提供商,专注于太阳能、风能发电站的监控、调度和优化管理。'),
|
||||
suffix = random.randint(1000, 9999)
|
||||
|
||||
org_templates = [
|
||||
('Acme Corporation', '全球领先的技术解决方案提供商,专注于企业级软件开发、云计算服务和网络安全解决方案。'),
|
||||
('TechStart Innovation Labs', '专注于人工智能、机器学习和区块链技术研发的创新实验室。'),
|
||||
('Global Financial Services', '提供全方位数字银行服务的金融科技公司,包括移动支付、在线贷款、投资理财等服务。'),
|
||||
('HealthCare Plus Medical', '医疗信息化解决方案提供商,专注于电子病历系统、医院信息管理系统和远程医疗平台开发。'),
|
||||
('E-Commerce Mega Platform', '亚太地区最大的电子商务平台之一,提供 B2B、B2C 和 C2C 多种交易模式。'),
|
||||
('Smart City Infrastructure', '智慧城市基础设施解决方案提供商,专注于物联网传感器网络、智能交通系统。'),
|
||||
('Educational Technology', '在线教育技术联盟,提供 K-12 和高等教育在线学习平台。'),
|
||||
('Green Energy Solutions', '可再生能源管理系统提供商,专注于太阳能、风能发电站的监控、调度和优化管理。'),
|
||||
('CyberSec Defense Corp', '网络安全防御公司,提供渗透测试、漏洞评估和安全咨询服务。'),
|
||||
('CloudNative Systems', '云原生系统开发商,专注于 Kubernetes、微服务架构和 DevOps 工具链。'),
|
||||
('DataFlow Analytics', '大数据分析平台,提供实时数据处理、商业智能和预测分析服务。'),
|
||||
('MobileFirst Technologies', '移动优先技术公司,专注于 iOS/Android 应用开发和跨平台解决方案。'),
|
||||
]
|
||||
|
||||
divisions = ['Global Division', 'Asia Pacific', 'EMEA Region', 'Americas', 'R&D Center', 'Digital Platform', 'Cloud Services', 'Security Team']
|
||||
|
||||
# 随机选择 5-10 个组织
|
||||
num_orgs = random.randint(5, 10)
|
||||
selected = random.sample(org_templates, min(num_orgs, len(org_templates)))
|
||||
|
||||
ids = []
|
||||
for name, desc in orgs:
|
||||
for name_base, desc in selected:
|
||||
division = random.choice(divisions)
|
||||
name = f'{name_base} - {division} ({suffix})'
|
||||
cur.execute("""
|
||||
INSERT INTO organization (name, description, created_at, deleted_at)
|
||||
VALUES (%s, %s, NOW(), NULL)
|
||||
VALUES (%s, %s, NOW() - INTERVAL '%s days', NULL)
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING id
|
||||
""", (name, desc))
|
||||
""", (name, desc, random.randint(0, 365)))
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
ids.append(row[0])
|
||||
@@ -212,69 +255,84 @@ class TestDataGenerator:
|
||||
print("🎯 创建扫描目标...")
|
||||
cur = self.conn.cursor()
|
||||
|
||||
domains = [
|
||||
'api.acme-corporation-international-holdings.com',
|
||||
'portal.techstart-innovation-labs-research.io',
|
||||
'secure.global-financial-services-digital-banking.com',
|
||||
'ehr.healthcare-plus-medical-systems-platform.org',
|
||||
'shop.ecommerce-mega-platform-asia-pacific.com',
|
||||
'dashboard.smart-city-infrastructure-iot-sensors.net',
|
||||
'learn.educational-technology-consortium-online.edu',
|
||||
'monitor.green-energy-solutions-renewable-power.com',
|
||||
'admin.enterprise-resource-planning-system-v2.internal.corp',
|
||||
'staging.customer-relationship-management-platform.dev',
|
||||
'beta.supply-chain-management-logistics-tracking.io',
|
||||
'test.human-resources-information-system-portal.local',
|
||||
'dev.content-management-system-headless-api.example.com',
|
||||
'qa.business-intelligence-analytics-dashboard.staging',
|
||||
'uat.project-management-collaboration-tools.preview',
|
||||
]
|
||||
suffix = random.randint(1000, 9999)
|
||||
|
||||
ips = ['203.0.113.50', '198.51.100.100', '192.0.2.200', '203.0.113.150', '198.51.100.250']
|
||||
cidrs = ['10.0.0.0/24', '172.16.0.0/16', '192.168.100.0/24']
|
||||
# 域名前缀和后缀组合,增加随机性
|
||||
prefixes = ['api', 'portal', 'secure', 'admin', 'dashboard', 'app', 'mobile', 'staging', 'dev', 'test', 'qa', 'uat', 'beta', 'prod', 'internal', 'external', 'public', 'private']
|
||||
companies = ['acme', 'techstart', 'globalfinance', 'healthcare', 'ecommerce', 'smartcity', 'edutech', 'greenenergy', 'cybersec', 'cloudnative', 'dataflow', 'mobilefirst', 'secureops', 'devplatform']
|
||||
tlds = ['.com', '.io', '.net', '.org', '.dev', '.app', '.cloud', '.tech', '.systems']
|
||||
|
||||
ids = []
|
||||
|
||||
# 域名目标
|
||||
for i, domain in enumerate(domains):
|
||||
# 随机生成 10-20 个域名目标
|
||||
num_domains = random.randint(10, 20)
|
||||
used_domains = set()
|
||||
|
||||
for i in range(num_domains):
|
||||
prefix = random.choice(prefixes)
|
||||
company = random.choice(companies)
|
||||
tld = random.choice(tlds)
|
||||
domain = f'{prefix}.{company}-{suffix}{tld}'
|
||||
|
||||
if domain in used_domains:
|
||||
continue
|
||||
used_domains.add(domain)
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO target (name, type, created_at, last_scanned_at, deleted_at)
|
||||
VALUES (%s, 'domain', NOW(), NOW() - INTERVAL '%s days', NULL)
|
||||
VALUES (%s, 'domain', NOW() - INTERVAL '%s days', NOW() - INTERVAL '%s days', NULL)
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING id
|
||||
""", (domain, random.randint(0, 30)))
|
||||
""", (domain, random.randint(30, 365), random.randint(0, 30)))
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
ids.append(row[0])
|
||||
# 关联到组织
|
||||
if org_ids:
|
||||
org_id = org_ids[i % len(org_ids)]
|
||||
# 随机关联到组织
|
||||
if org_ids and random.random() > 0.3: # 70% 概率关联
|
||||
org_id = random.choice(org_ids)
|
||||
cur.execute("""
|
||||
INSERT INTO organization_targets (organization_id, target_id)
|
||||
VALUES (%s, %s)
|
||||
ON CONFLICT DO NOTHING
|
||||
""", (org_id, row[0]))
|
||||
|
||||
# IP 目标
|
||||
for ip in ips:
|
||||
# 随机生成 3-8 个 IP 目标
|
||||
num_ips = random.randint(3, 8)
|
||||
for _ in range(num_ips):
|
||||
# 使用文档保留的 IP 范围
|
||||
ip_ranges = [
|
||||
(203, 0, 113), # TEST-NET-3
|
||||
(198, 51, 100), # TEST-NET-2
|
||||
(192, 0, 2), # TEST-NET-1
|
||||
]
|
||||
base = random.choice(ip_ranges)
|
||||
ip = f'{base[0]}.{base[1]}.{base[2]}.{random.randint(1, 254)}'
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO target (name, type, created_at, last_scanned_at, deleted_at)
|
||||
VALUES (%s, 'ip', NOW(), NOW() - INTERVAL '%s days', NULL)
|
||||
VALUES (%s, 'ip', NOW() - INTERVAL '%s days', NOW() - INTERVAL '%s days', NULL)
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING id
|
||||
""", (ip, random.randint(0, 30)))
|
||||
""", (ip, random.randint(30, 365), random.randint(0, 30)))
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
ids.append(row[0])
|
||||
|
||||
# CIDR 目标
|
||||
for cidr in cidrs:
|
||||
# 随机生成 2-5 个 CIDR 目标
|
||||
num_cidrs = random.randint(2, 5)
|
||||
cidr_bases = ['10.0', '172.16', '172.17', '172.18', '192.168']
|
||||
for _ in range(num_cidrs):
|
||||
base = random.choice(cidr_bases)
|
||||
third_octet = random.randint(0, 255)
|
||||
mask = random.choice([24, 25, 26, 27, 28])
|
||||
cidr = f'{base}.{third_octet}.0/{mask}'
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO target (name, type, created_at, last_scanned_at, deleted_at)
|
||||
VALUES (%s, 'cidr', NOW(), NOW() - INTERVAL '%s days', NULL)
|
||||
VALUES (%s, 'cidr', NOW() - INTERVAL '%s days', NOW() - INTERVAL '%s days', NULL)
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING id
|
||||
""", (cidr, random.randint(0, 30)))
|
||||
""", (cidr, random.randint(30, 365), random.randint(0, 30)))
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
ids.append(row[0])
|
||||
@@ -292,18 +350,49 @@ class TestDataGenerator:
|
||||
return []
|
||||
|
||||
statuses = ['cancelled', 'completed', 'failed', 'initiated', 'running']
|
||||
stages = ['subdomain_discovery', 'port_scanning', 'web_discovery', 'vulnerability_scanning']
|
||||
status_weights = [0.05, 0.6, 0.1, 0.1, 0.15] # completed 占比最高
|
||||
stages = ['subdomain_discovery', 'port_scanning', 'web_discovery', 'vulnerability_scanning', 'directory_bruteforce', 'endpoint_discovery']
|
||||
|
||||
error_messages = [
|
||||
'Connection timeout while scanning target. Please check network connectivity.',
|
||||
'DNS resolution failed for target domain.',
|
||||
'Rate limit exceeded. Scan paused and will resume automatically.',
|
||||
'Worker node disconnected during scan execution.',
|
||||
'Insufficient disk space on worker node.',
|
||||
'Target returned too many errors, scan aborted.',
|
||||
'Authentication failed for protected resources.',
|
||||
]
|
||||
|
||||
ids = []
|
||||
for target_id in target_ids[:10]:
|
||||
for _ in range(random.randint(2, 5)):
|
||||
status = random.choice(statuses)
|
||||
# 随机选择目标数量
|
||||
num_targets = min(random.randint(8, 15), len(target_ids))
|
||||
selected_targets = random.sample(target_ids, num_targets)
|
||||
|
||||
for target_id in selected_targets:
|
||||
# 每个目标随机 1-6 个扫描任务
|
||||
num_scans = random.randint(1, 6)
|
||||
for _ in range(num_scans):
|
||||
status = random.choices(statuses, weights=status_weights)[0]
|
||||
engine_id = random.choice(engine_ids)
|
||||
worker_id = random.choice(worker_ids) if worker_ids else None
|
||||
|
||||
progress = random.randint(0, 100) if status == 'running' else (100 if status == 'completed' else 0)
|
||||
progress = random.randint(10, 95) if status == 'running' else (100 if status == 'completed' else random.randint(0, 50))
|
||||
stage = random.choice(stages) if status == 'running' else ''
|
||||
error_msg = 'Connection timeout while scanning target. Please check network connectivity.' if status == 'failed' else ''
|
||||
error_msg = random.choice(error_messages) if status == 'failed' else ''
|
||||
|
||||
# 随机生成更真实的统计数据
|
||||
subdomains = random.randint(5, 800)
|
||||
websites = random.randint(2, 150)
|
||||
endpoints = random.randint(20, 2000)
|
||||
ips = random.randint(3, 100)
|
||||
directories = random.randint(50, 3000)
|
||||
vulns_critical = random.randint(0, 8)
|
||||
vulns_high = random.randint(0, 20)
|
||||
vulns_medium = random.randint(0, 40)
|
||||
vulns_low = random.randint(0, 60)
|
||||
vulns_total = vulns_critical + vulns_high + vulns_medium + vulns_low + random.randint(0, 30) # info
|
||||
|
||||
days_ago = random.randint(0, 90)
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO scan (
|
||||
@@ -321,12 +410,11 @@ class TestDataGenerator:
|
||||
RETURNING id
|
||||
""", (
|
||||
target_id, engine_id, status, worker_id, progress, stage,
|
||||
f'/app/results/scan_{target_id}', error_msg, '{}', '{}',
|
||||
random.randint(10, 500), random.randint(5, 100), random.randint(50, 1000),
|
||||
random.randint(5, 50), random.randint(100, 2000), random.randint(0, 50),
|
||||
random.randint(0, 5), random.randint(0, 10), random.randint(0, 15), random.randint(0, 20),
|
||||
random.randint(0, 60),
|
||||
datetime.now() if status == 'completed' else None
|
||||
f'/app/results/scan_{target_id}_{random.randint(1000, 9999)}', error_msg, '{}', '{}',
|
||||
subdomains, websites, endpoints, ips, directories, vulns_total,
|
||||
vulns_critical, vulns_high, vulns_medium, vulns_low,
|
||||
days_ago,
|
||||
datetime.now() - timedelta(days=days_ago, hours=random.randint(0, 23)) if status in ['completed', 'failed', 'cancelled'] else None
|
||||
))
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
@@ -344,35 +432,72 @@ class TestDataGenerator:
|
||||
print(" ⚠ 缺少引擎,跳过\n")
|
||||
return
|
||||
|
||||
schedules = [
|
||||
('Daily-Full-Security-Assessment-Scan-For-Production-Environment-Critical-Assets', '0 2 * * *', True),
|
||||
('Weekly-Comprehensive-Vulnerability-Scan-For-All-External-Facing-Services', '0 3 * * 0', True),
|
||||
('Monthly-Deep-Penetration-Testing-Scan-For-Internal-Network-Infrastructure', '0 4 1 * *', True),
|
||||
('Hourly-Quick-Reconnaissance-Scan-For-New-Asset-Discovery-And-Monitoring', '0 * * * *', False),
|
||||
('Bi-Weekly-Compliance-Check-Scan-For-PCI-DSS-And-SOC2-Requirements', '0 5 1,15 * *', True),
|
||||
('Quarterly-Full-Infrastructure-Security-Audit-Scan-With-Extended-Templates', '0 6 1 1,4,7,10 *', True),
|
||||
('Daily-API-Endpoint-Security-Scan-For-REST-And-GraphQL-Services', '30 1 * * *', True),
|
||||
('Weekly-Web-Application-Vulnerability-Scan-For-Customer-Facing-Portals', '0 4 * * 1', False),
|
||||
suffix = random.randint(1000, 9999)
|
||||
|
||||
schedule_templates = [
|
||||
('Daily-Full-Security-Assessment', '0 {hour} * * *'),
|
||||
('Weekly-Vulnerability-Scan', '0 {hour} * * {dow}'),
|
||||
('Monthly-Penetration-Testing', '0 {hour} {dom} * *'),
|
||||
('Hourly-Quick-Reconnaissance', '{min} * * * *'),
|
||||
('Bi-Weekly-Compliance-Check', '0 {hour} 1,15 * *'),
|
||||
('Quarterly-Infrastructure-Audit', '0 {hour} 1 1,4,7,10 *'),
|
||||
('Daily-API-Security-Scan', '{min} {hour} * * *'),
|
||||
('Weekly-Web-Application-Scan', '0 {hour} * * {dow}'),
|
||||
('Nightly-Asset-Discovery', '0 {hour} * * *'),
|
||||
('Weekend-Deep-Scan', '0 {hour} * * 0,6'),
|
||||
('Business-Hours-Monitor', '0 9-17 * * 1-5'),
|
||||
('Off-Hours-Intensive-Scan', '0 {hour} * * *'),
|
||||
]
|
||||
|
||||
# 随机选择 6-12 个定时任务
|
||||
num_schedules = random.randint(6, 12)
|
||||
selected = random.sample(schedule_templates, min(num_schedules, len(schedule_templates)))
|
||||
|
||||
count = 0
|
||||
for name, cron, enabled in schedules:
|
||||
for name_base, cron_template in selected:
|
||||
name = f'{name_base}-{suffix}-{count:02d}'
|
||||
cron = cron_template.format(
|
||||
hour=random.randint(0, 23),
|
||||
min=random.randint(0, 59),
|
||||
dow=random.randint(0, 6),
|
||||
dom=random.randint(1, 28)
|
||||
)
|
||||
enabled = random.random() > 0.3 # 70% 启用
|
||||
|
||||
engine_id = random.choice(engine_ids)
|
||||
org_id = random.choice(org_ids) if org_ids and random.choice([True, False]) else None
|
||||
target_id = random.choice(target_ids) if target_ids and not org_id else None
|
||||
# 随机决定关联组织还是目标
|
||||
if org_ids and target_ids:
|
||||
if random.random() > 0.5:
|
||||
org_id = random.choice(org_ids)
|
||||
target_id = None
|
||||
else:
|
||||
org_id = None
|
||||
target_id = random.choice(target_ids)
|
||||
elif org_ids:
|
||||
org_id = random.choice(org_ids)
|
||||
target_id = None
|
||||
elif target_ids:
|
||||
org_id = None
|
||||
target_id = random.choice(target_ids)
|
||||
else:
|
||||
org_id = None
|
||||
target_id = None
|
||||
|
||||
run_count = random.randint(0, 200)
|
||||
has_run = random.random() > 0.2 # 80% 已运行过
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO scheduled_scan (
|
||||
name, engine_id, organization_id, target_id, cron_expression, is_enabled,
|
||||
run_count, last_run_time, next_run_time, created_at, updated_at
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, NOW() - INTERVAL '%s days', NOW())
|
||||
ON CONFLICT DO NOTHING
|
||||
""", (
|
||||
name, engine_id, org_id, target_id, cron, enabled,
|
||||
random.randint(0, 100),
|
||||
datetime.now() - timedelta(days=random.randint(0, 7)) if random.choice([True, False]) else None,
|
||||
datetime.now() + timedelta(hours=random.randint(1, 168))
|
||||
))
|
||||
run_count if has_run else 0,
|
||||
datetime.now() - timedelta(days=random.randint(0, 14), hours=random.randint(0, 23)) if has_run else None,
|
||||
datetime.now() + timedelta(hours=random.randint(1, 336)) # 最多 2 周后
|
||||
, random.randint(30, 180)))
|
||||
count += 1
|
||||
|
||||
print(f" ✓ 创建了 {count} 个定时扫描任务\n")
|
||||
@@ -392,24 +517,39 @@ class TestDataGenerator:
|
||||
'redis', 'mongo', 'elastic', 'vpn', 'remote', 'gateway', 'proxy',
|
||||
'monitoring', 'metrics', 'grafana', 'prometheus', 'kibana', 'logs',
|
||||
'jenkins', 'ci', 'cd', 'gitlab', 'jira', 'confluence', 'kubernetes', 'k8s',
|
||||
'www', 'www2', 'www3', 'ns1', 'ns2', 'mx', 'mx1', 'mx2', 'autodiscover',
|
||||
'webdisk', 'cpanel', 'whm', 'webmail2', 'email', 'smtp2', 'pop', 'pop3',
|
||||
'imap2', 'calendar', 'contacts', 'drive', 'docs', 'sheets', 'slides',
|
||||
'meet', 'chat', 'teams', 'slack', 'discord', 'zoom', 'video', 'stream',
|
||||
'blog', 'news', 'press', 'media2', 'images', 'img', 'photos', 'video2',
|
||||
'shop', 'store', 'cart', 'checkout', 'pay', 'payment', 'billing', 'invoice',
|
||||
'support', 'help', 'helpdesk', 'ticket', 'tickets', 'status', 'health',
|
||||
'api-v1', 'api-v2', 'api-v3', 'graphql', 'rest', 'soap', 'rpc', 'grpc',
|
||||
]
|
||||
|
||||
# 二级前缀,用于生成更复杂的子域名
|
||||
secondary_prefixes = ['', 'prod-', 'dev-', 'staging-', 'test-', 'int-', 'ext-', 'us-', 'eu-', 'ap-']
|
||||
|
||||
# 获取域名目标
|
||||
cur.execute("SELECT id, name FROM target WHERE type = 'domain' AND deleted_at IS NULL LIMIT 8")
|
||||
cur.execute("SELECT id, name FROM target WHERE type = 'domain' AND deleted_at IS NULL")
|
||||
domain_targets = cur.fetchall()
|
||||
|
||||
count = 0
|
||||
for target_id, target_name in domain_targets:
|
||||
num = random.randint(20, 40)
|
||||
# 每个目标随机 15-60 个子域名
|
||||
num = random.randint(15, 60)
|
||||
selected = random.sample(prefixes, min(num, len(prefixes)))
|
||||
|
||||
for prefix in selected:
|
||||
subdomain_name = f'{prefix}.{target_name}'
|
||||
# 随机添加二级前缀
|
||||
sec_prefix = random.choice(secondary_prefixes) if random.random() > 0.7 else ''
|
||||
subdomain_name = f'{sec_prefix}{prefix}.{target_name}'
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO subdomain (name, target_id, created_at)
|
||||
VALUES (%s, %s, NOW())
|
||||
VALUES (%s, %s, NOW() - INTERVAL '%s days')
|
||||
ON CONFLICT DO NOTHING
|
||||
""", (subdomain_name, target_id))
|
||||
""", (subdomain_name, target_id, random.randint(0, 90)))
|
||||
count += 1
|
||||
|
||||
print(f" ✓ 创建了 {count} 个子域名\n")
|
||||
|
||||
@@ -22,8 +22,10 @@ SERVER_PORT=8888
|
||||
# 供远程 Worker 访问主服务器的地址:
|
||||
# - 仅本地部署:server(Docker 内部服务名)
|
||||
# - 有远程 Worker:改为主服务器外网 IP 或域名(如 192.168.1.100 或 xingrin.example.com)
|
||||
# 注意:远程 Worker 会通过 https://{PUBLIC_HOST} 访问(nginx 反代到后端 8888)
|
||||
# 注意:远程 Worker 会通过 https://{PUBLIC_HOST}:{PUBLIC_PORT} 访问(nginx 反代到后端 8888)
|
||||
PUBLIC_HOST=server
|
||||
# 对外 HTTPS 端口
|
||||
PUBLIC_PORT=8083
|
||||
|
||||
# ==================== Django 核心配置 ====================
|
||||
# 生产环境务必更换为随机强密钥
|
||||
|
||||
@@ -97,8 +97,7 @@ services:
|
||||
frontend:
|
||||
condition: service_started
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "8083:8083"
|
||||
volumes:
|
||||
# SSL 证书挂载(方便更新)
|
||||
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||
|
||||
@@ -95,8 +95,7 @@ services:
|
||||
frontend:
|
||||
condition: service_started
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "8083:8083"
|
||||
volumes:
|
||||
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||
|
||||
|
||||
@@ -16,17 +16,9 @@ http {
|
||||
server frontend:3000;
|
||||
}
|
||||
|
||||
# HTTP 跳转到 HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
# HTTPS 反代(将证书放在 /docker/nginx/ssl 下映射到 /etc/nginx/ssl)
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen 8083 ssl http2;
|
||||
server_name _;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/fullchain.pem;
|
||||
@@ -36,6 +28,9 @@ http {
|
||||
|
||||
client_max_body_size 50m;
|
||||
|
||||
# HTTP 请求到 HTTPS 端口时自动跳转
|
||||
error_page 497 =301 https://$host:$server_port$request_uri;
|
||||
|
||||
# 指纹特征 - 用于 FOFA/Shodan 等搜索引擎识别
|
||||
add_header X-Powered-By "Xingrin ASM" always;
|
||||
|
||||
|
||||
@@ -12,11 +12,10 @@
|
||||
- **操作系统**: Ubuntu 18.04+ / Debian 10+
|
||||
- **权限**: sudo 管理员权限
|
||||
- **端口要求**: 需要开放以下端口
|
||||
- `80` - HTTP 访问(自动跳转到 HTTPS)
|
||||
- `443` - HTTPS 访问(主要访问端口)
|
||||
- `8083` - HTTPS 访问(主要访问端口)
|
||||
- `5432` - PostgreSQL 数据库(如使用本地数据库)
|
||||
- `6379` - Redis 缓存服务
|
||||
- 后端 API 仅容器内监听 8888,由 nginx 反代到 80/443,对公网无需放行 8888
|
||||
- 后端 API 仅容器内监听 8888,由 nginx 反代到 8083,对公网无需放行 8888
|
||||
|
||||
## 一键安装
|
||||
|
||||
@@ -45,7 +44,7 @@ sudo ./install.sh --no-frontend
|
||||
|
||||
### 3. 访问系统
|
||||
安装完成后,访问:
|
||||
- **Web 界面**: https://你的服务器IP/
|
||||
- **Web 界面**: https://你的服务器IP:8083/
|
||||
|
||||
**默认账号**:
|
||||
- 用户名: `admin`
|
||||
@@ -60,13 +59,10 @@ sudo ./install.sh --no-frontend
|
||||
|
||||
#### 必须放行的端口
|
||||
```
|
||||
80 - HTTP 访问
|
||||
443 - HTTPS 访问
|
||||
3000 - 前端服务(开发模式)
|
||||
8083 - HTTPS 访问(主要访问端口)
|
||||
5432 - PostgreSQL(如使用本地数据库)
|
||||
6379 - Redis 缓存
|
||||
```
|
||||
> 后端 API 默认仅在容器内 8888 监听,由 nginx 反代到 80/443,对公网无需放行 8888。
|
||||
|
||||
#### 推荐方案
|
||||
- **国外 VPS**:如 Vultr、DigitalOcean、Linode 等,默认开放所有端口,无需额外配置
|
||||
@@ -195,8 +191,7 @@ IMAGE_TAG=v1.0.0 # 镜像版本(自动设置)
|
||||
#### 1. 端口被占用
|
||||
```bash
|
||||
# 检查端口占用
|
||||
sudo netstat -tlnp | grep :80
|
||||
sudo netstat -tlnp | grep :443
|
||||
sudo netstat -tlnp | grep :8083
|
||||
|
||||
# 停止占用端口的服务
|
||||
sudo systemctl stop apache2 # 如果是 Apache
|
||||
|
||||
@@ -245,7 +245,7 @@ A: 更新字典内容后会重新计算 hash,Worker 下次使用时会检测
|
||||
|
||||
A: 检查:
|
||||
1. `PUBLIC_HOST` 是否配置为 Server 的外网 IP 或域名
|
||||
2. Nginx 443 (HTTPS) 是否可达(远程 Worker 通过 nginx 访问后端)
|
||||
2. Nginx 8083 (HTTPS) 是否可达(远程 Worker 通过 nginx 访问后端)
|
||||
3. Worker 到 Server 的网络是否通畅
|
||||
|
||||
### Q: 如何批量导入字典?
|
||||
|
||||
@@ -248,6 +248,31 @@
|
||||
|
||||
}
|
||||
|
||||
/* 登录页背景 - 使用主题色适配亮暗模式 */
|
||||
.login-bg {
|
||||
position: relative;
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
.login-bg::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: var(--primary);
|
||||
opacity: 0.04;
|
||||
mask-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23000' fill-opacity='1'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23000' fill-opacity='1'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||||
mask-size: 60px 60px;
|
||||
-webkit-mask-size: 60px 60px;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.login-bg > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 通知铃铛摇晃动画 */
|
||||
@keyframes wiggle {
|
||||
0%, 100% {
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function RootLayout({
|
||||
{/* ThemeProvider 提供主题切换功能,跟随系统自动切换亮暗色 */}
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
defaultTheme="dark"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
|
||||
@@ -41,13 +41,7 @@ export default function LoginPage() {
|
||||
// 加载中显示 spinner
|
||||
if (authLoading) {
|
||||
return (
|
||||
<div
|
||||
className="flex min-h-svh w-full flex-col items-center justify-center gap-4"
|
||||
style={{
|
||||
backgroundColor: '#DFDBE5',
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='180' height='180' viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M82.42 180h-1.415L0 98.995v-2.827L6.167 90 0 83.833V81.004L81.005 0h2.827L90 6.167 96.167 0H98.996L180 81.005v2.827L173.833 90 180 96.167V98.996L98.995 180h-2.827L90 173.833 83.833 180H82.42zm0-1.414L1.413 97.58 8.994 90l-7.58-7.58L82.42 1.413 90 8.994l7.58-7.58 81.006 81.005-7.58 7.58 7.58 7.58-81.005 81.006-7.58-7.58-7.58 7.58zM175.196 0h-25.832c1.033 2.924 2.616 5.59 4.625 7.868C152.145 9.682 151 12.208 151 15c0 5.523 4.477 10 10 10 1.657 0 3 1.343 3 3v4h16V0h-4.803c.51.883.803 1.907.803 3 0 3.314-2.686 6-6 6s-6-2.686-6-6c0-1.093.292-2.117.803-3h10.394-13.685C161.18.938 161 1.948 161 3v4c-4.418 0-8 3.582-8 8s3.582 8 8 8c2.76 0 5 2.24 5 5v2h4v-4h2v4h4v-4h2v4h2V0h-4.803zm-15.783 0c-.27.954-.414 1.96-.414 3v2.2c-1.25.254-2.414.74-3.447 1.412-1.716-1.93-3.098-4.164-4.054-6.612h7.914zM180 17h-3l2.143-10H180v10zm-30.635 163c-.884-2.502-1.365-5.195-1.365-8 0-13.255 10.748-24 23.99-24H180v32h-30.635zm12.147 0c.5-1.416 1.345-2.67 2.434-3.66l-1.345-1.48c-1.498 1.364-2.62 3.136-3.186 5.14H151.5c-.97-2.48-1.5-5.177-1.5-8 0-12.15 9.84-22 22-22h8v30h-18.488zm13.685 0c-1.037-1.793-2.976-3-5.197-3-2.22 0-4.16 1.207-5.197 3h10.394zM0 148h8.01C21.26 148 32 158.742 32 172c0 2.805-.48 5.498-1.366 8H0v-32zm0 2h8c12.15 0 22 9.847 22 22 0 2.822-.53 5.52-1.5 8h-7.914c-.567-2.004-1.688-3.776-3.187-5.14l-1.346 1.48c1.09.99 1.933 2.244 2.434 3.66H0v-30zm15.197 30c-1.037-1.793-2.976-3-5.197-3-2.22 0-4.16 1.207-5.197 3h10.394zM0 32h16v-4c0-1.657 1.343-3 3-3 5.523 0 10-4.477 10-10 0-2.794-1.145-5.32-2.992-7.134C28.018 5.586 29.6 2.924 30.634 0H0v32zm0-2h2v-4h2v4h4v-4h2v4h4v-2c0-2.76 2.24-5 5-5 4.418 0 8-3.582 8-8s-3.582-8-8-8V3c0-1.052-.18-2.062-.512-3H0v30zM28.5 0c-.954 2.448-2.335 4.683-4.05 6.613-1.035-.672-2.2-1.16-3.45-1.413V3c0-1.04-.144-2.046-.414-3H28.5zM0 17h3L.857 7H0v10zM15.197 0c.51.883.803 1.907.803 3 0 3.314-2.686 6-6 6S4 6.314 4 3c0-1.093.292-2.117.803-3h10.394zM109 115c-1.657 0-3 1.343-3 3v4H74v-4c0-1.657-1.343-3-3-3-5.523 0-10-4.477-10-10 0-2.793 1.145-5.318 2.99-7.132C60.262 93.638 58 88.084 58 82c0-13.255 10.748-24 23.99-24h16.02C111.26 58 122 68.742 122 82c0 6.082-2.263 11.636-5.992 15.866C117.855 99.68 119 102.206 119 105c0 5.523-4.477 10-10 10zm0-2c-2.76 0-5 2.24-5 5v2h-4v-4h-2v4h-4v-4h-2v4h-4v-4h-2v4h-4v-4h-2v4h-4v-2c0-2.76-2.24-5-5-5-4.418 0-8-3.582-8-8s3.582-8 8-8v-4c0-2.64 1.136-5.013 2.946-6.66L72.6 84.86C70.39 86.874 69 89.775 69 93v2.2c-1.25.254-2.414.74-3.447 1.412C62.098 92.727 60 87.61 60 82c0-12.15 9.84-22 22-22h16c12.15 0 22 9.847 22 22 0 5.61-2.097 10.728-5.55 14.613-1.035-.672-2.2-1.16-3.45-1.413V93c0-3.226-1.39-6.127-3.6-8.14l-1.346 1.48C107.864 87.987 109 90.36 109 93v4c4.418 0 8 3.582 8 8s-3.582 8-8 8zM90.857 97L93 107h-6l2.143-10h1.714zM80 99c3.314 0 6-2.686 6-6s-2.686-6-6-6-6 2.686-6 6 2.686 6 6 6zm20 0c3.314 0 6-2.686 6-6s-2.686-6-6-6-6 2.686-6 6 2.686 6 6 6z' fill='%239C92AC' fill-opacity='0.28' fill-rule='evenodd'/%3E%3C/svg%3E")`
|
||||
}}
|
||||
>
|
||||
<div className="flex min-h-svh w-full flex-col items-center justify-center gap-4 bg-background">
|
||||
<Spinner className="size-8 text-primary" />
|
||||
<p className="text-muted-foreground text-sm" suppressHydrationWarning>loading...</p>
|
||||
</div>
|
||||
@@ -60,13 +54,7 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex min-h-svh flex-col p-6 md:p-10"
|
||||
style={{
|
||||
backgroundColor: '#DFDBE5',
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='180' height='180' viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M82.42 180h-1.415L0 98.995v-2.827L6.167 90 0 83.833V81.004L81.005 0h2.827L90 6.167 96.167 0H98.996L180 81.005v2.827L173.833 90 180 96.167V98.996L98.995 180h-2.827L90 173.833 83.833 180H82.42zm0-1.414L1.413 97.58 8.994 90l-7.58-7.58L82.42 1.413 90 8.994l7.58-7.58 81.006 81.005-7.58 7.58 7.58 7.58-81.005 81.006-7.58-7.58-7.58 7.58zM175.196 0h-25.832c1.033 2.924 2.616 5.59 4.625 7.868C152.145 9.682 151 12.208 151 15c0 5.523 4.477 10 10 10 1.657 0 3 1.343 3 3v4h16V0h-4.803c.51.883.803 1.907.803 3 0 3.314-2.686 6-6 6s-6-2.686-6-6c0-1.093.292-2.117.803-3h10.394-13.685C161.18.938 161 1.948 161 3v4c-4.418 0-8 3.582-8 8s3.582 8 8 8c2.76 0 5 2.24 5 5v2h4v-4h2v4h4v-4h2v4h2V0h-4.803zm-15.783 0c-.27.954-.414 1.96-.414 3v2.2c-1.25.254-2.414.74-3.447 1.412-1.716-1.93-3.098-4.164-4.054-6.612h7.914zM180 17h-3l2.143-10H180v10zm-30.635 163c-.884-2.502-1.365-5.195-1.365-8 0-13.255 10.748-24 23.99-24H180v32h-30.635zm12.147 0c.5-1.416 1.345-2.67 2.434-3.66l-1.345-1.48c-1.498 1.364-2.62 3.136-3.186 5.14H151.5c-.97-2.48-1.5-5.177-1.5-8 0-12.15 9.84-22 22-22h8v30h-18.488zm13.685 0c-1.037-1.793-2.976-3-5.197-3-2.22 0-4.16 1.207-5.197 3h10.394zM0 148h8.01C21.26 148 32 158.742 32 172c0 2.805-.48 5.498-1.366 8H0v-32zm0 2h8c12.15 0 22 9.847 22 22 0 2.822-.53 5.52-1.5 8h-7.914c-.567-2.004-1.688-3.776-3.187-5.14l-1.346 1.48c1.09.99 1.933 2.244 2.434 3.66H0v-30zm15.197 30c-1.037-1.793-2.976-3-5.197-3-2.22 0-4.16 1.207-5.197 3h10.394zM0 32h16v-4c0-1.657 1.343-3 3-3 5.523 0 10-4.477 10-10 0-2.794-1.145-5.32-2.992-7.134C28.018 5.586 29.6 2.924 30.634 0H0v32zm0-2h2v-4h2v4h4v-4h2v4h4v-2c0-2.76 2.24-5 5-5 4.418 0 8-3.582 8-8s-3.582-8-8-8V3c0-1.052-.18-2.062-.512-3H0v30zM28.5 0c-.954 2.448-2.335 4.683-4.05 6.613-1.035-.672-2.2-1.16-3.45-1.413V3c0-1.04-.144-2.046-.414-3H28.5zM0 17h3L.857 7H0v10zM15.197 0c.51.883.803 1.907.803 3 0 3.314-2.686 6-6 6S4 6.314 4 3c0-1.093.292-2.117.803-3h10.394zM109 115c-1.657 0-3 1.343-3 3v4H74v-4c0-1.657-1.343-3-3-3-5.523 0-10-4.477-10-10 0-2.793 1.145-5.318 2.99-7.132C60.262 93.638 58 88.084 58 82c0-13.255 10.748-24 23.99-24h16.02C111.26 58 122 68.742 122 82c0 6.082-2.263 11.636-5.992 15.866C117.855 99.68 119 102.206 119 105c0 5.523-4.477 10-10 10zm0-2c-2.76 0-5 2.24-5 5v2h-4v-4h-2v4h-4v-4h-2v4h-4v-4h-2v4h-4v-4h-2v4h-4v-2c0-2.76-2.24-5-5-5-4.418 0-8-3.582-8-8s3.582-8 8-8v-4c0-2.64 1.136-5.013 2.946-6.66L72.6 84.86C70.39 86.874 69 89.775 69 93v2.2c-1.25.254-2.414.74-3.447 1.412C62.098 92.727 60 87.61 60 82c0-12.15 9.84-22 22-22h16c12.15 0 22 9.847 22 22 0 5.61-2.097 10.728-5.55 14.613-1.035-.672-2.2-1.16-3.45-1.413V93c0-3.226-1.39-6.127-3.6-8.14l-1.346 1.48C107.864 87.987 109 90.36 109 93v4c4.418 0 8 3.582 8 8s-3.582 8-8 8zM90.857 97L93 107h-6l2.143-10h1.714zM80 99c3.314 0 6-2.686 6-6s-2.686-6-6-6-6 2.686-6 6 2.686 6 6 6zm20 0c3.314 0 6-2.686 6-6s-2.686-6-6-6-6 2.686-6 6 2.686 6 6 6z' fill='%239C92AC' fill-opacity='0.28' fill-rule='evenodd'/%3E%3C/svg%3E")`
|
||||
}}
|
||||
>
|
||||
<div className="login-bg flex min-h-svh flex-col p-6 md:p-10">
|
||||
{/* 主要内容区域 */}
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="w-full max-w-sm md:max-w-4xl">
|
||||
|
||||
@@ -20,10 +20,10 @@ export function DashboardActivityTabs() {
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="vulnerabilities" className="mt-0">
|
||||
<VulnerabilitiesDetailView />
|
||||
<VulnerabilitiesDetailView hideToolbar />
|
||||
</TabsContent>
|
||||
<TabsContent value="scans" className="mt-0">
|
||||
<ScanHistoryList />
|
||||
<ScanHistoryList hideToolbar />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { IconLayoutColumns, IconBug, IconRadar, IconChevronLeft, IconChevronRight, IconChevronsLeft, IconChevronsRight, IconSearch, IconLoader2, IconChevronDown } from "@tabler/icons-react"
|
||||
import { IconLayoutColumns, IconBug, IconRadar, IconChevronLeft, IconChevronRight, IconChevronsLeft, IconChevronsRight, IconChevronDown } from "@tabler/icons-react"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import {
|
||||
Select,
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -106,41 +105,18 @@ export function DashboardDataTable() {
|
||||
const [vulnPagination, setVulnPagination] = React.useState({ pageIndex: 0, pageSize: 10 })
|
||||
const [scanPagination, setScanPagination] = React.useState({ pageIndex: 0, pageSize: 10 })
|
||||
|
||||
// 服务端搜索状态
|
||||
const [vulnSearchQuery, setVulnSearchQuery] = React.useState("")
|
||||
const [scanSearchQuery, setScanSearchQuery] = React.useState("")
|
||||
const [localVulnSearch, setLocalVulnSearch] = React.useState("")
|
||||
const [localScanSearch, setLocalScanSearch] = React.useState("")
|
||||
const [isVulnSearching, setIsVulnSearching] = React.useState(false)
|
||||
const [isScanSearching, setIsScanSearching] = React.useState(false)
|
||||
|
||||
// 获取漏洞数据
|
||||
const vulnQuery = useAllVulnerabilities({
|
||||
page: vulnPagination.pageIndex + 1,
|
||||
pageSize: vulnPagination.pageSize,
|
||||
search: vulnSearchQuery || undefined,
|
||||
})
|
||||
|
||||
// 获取扫描数据
|
||||
const scanQuery = useScans({
|
||||
page: scanPagination.pageIndex + 1,
|
||||
pageSize: scanPagination.pageSize,
|
||||
search: scanSearchQuery || undefined,
|
||||
})
|
||||
|
||||
// 当请求完成时重置搜索状态
|
||||
React.useEffect(() => {
|
||||
if (!vulnQuery.isFetching && isVulnSearching) {
|
||||
setIsVulnSearching(false)
|
||||
}
|
||||
}, [vulnQuery.isFetching, isVulnSearching])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!scanQuery.isFetching && isScanSearching) {
|
||||
setIsScanSearching(false)
|
||||
}
|
||||
}, [scanQuery.isFetching, isScanSearching])
|
||||
|
||||
// 删除扫描的 mutation
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: deleteScan,
|
||||
@@ -157,19 +133,6 @@ export function DashboardDataTable() {
|
||||
},
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleVulnSearch = () => {
|
||||
setIsVulnSearching(true)
|
||||
setVulnSearchQuery(localVulnSearch)
|
||||
setVulnPagination(prev => ({ ...prev, pageIndex: 0 }))
|
||||
}
|
||||
|
||||
const handleScanSearch = () => {
|
||||
setIsScanSearching(true)
|
||||
setScanSearchQuery(localScanSearch)
|
||||
setScanPagination(prev => ({ ...prev, pageIndex: 0 }))
|
||||
}
|
||||
|
||||
const vulnerabilities = vulnQuery.data?.vulnerabilities ?? []
|
||||
const scans = scanQuery.data?.results ?? []
|
||||
|
||||
@@ -302,10 +265,6 @@ export function DashboardDataTable() {
|
||||
})
|
||||
|
||||
const currentTable = activeTab === "vulnerabilities" ? vulnTable : scanTable
|
||||
const currentLocalSearch = activeTab === "vulnerabilities" ? localVulnSearch : localScanSearch
|
||||
const setCurrentLocalSearch = activeTab === "vulnerabilities" ? setLocalVulnSearch : setLocalScanSearch
|
||||
const handleCurrentSearch = activeTab === "vulnerabilities" ? handleVulnSearch : handleScanSearch
|
||||
const isCurrentSearching = activeTab === "vulnerabilities" ? isVulnSearching : isScanSearching
|
||||
const isLoading = activeTab === "vulnerabilities" ? vulnQuery.isLoading : scanQuery.isLoading
|
||||
const pagination = activeTab === "vulnerabilities" ? vulnPagination : scanPagination
|
||||
const setPagination = activeTab === "vulnerabilities" ? setVulnPagination : setScanPagination
|
||||
@@ -371,7 +330,7 @@ export function DashboardDataTable() {
|
||||
</AlertDialog>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
{/* Tab + 搜索框 + Columns 在同一行 */}
|
||||
{/* Tab + Columns 在同一行 */}
|
||||
<div className="flex items-center justify-between gap-4 mb-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="scans" className="gap-1.5">
|
||||
@@ -384,46 +343,30 @@ export function DashboardDataTable() {
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
placeholder={activeTab === "vulnerabilities" ? "搜索漏洞类型..." : "搜索目标名称..."}
|
||||
value={currentLocalSearch}
|
||||
onChange={(e) => setCurrentLocalSearch(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleCurrentSearch()}
|
||||
className="h-8 w-[200px]"
|
||||
/>
|
||||
<Button variant="outline" size="sm" onClick={handleCurrentSearch} disabled={isCurrentSearching} className="h-8">
|
||||
{isCurrentSearching ? (
|
||||
<IconLoader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<IconSearch className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<IconLayoutColumns />
|
||||
Columns
|
||||
<IconChevronDown />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{currentTable
|
||||
.getAllColumns()
|
||||
.filter((column) => column.getCanHide())
|
||||
.map((column) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
className="capitalize"
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||
>
|
||||
{column.id}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<IconLayoutColumns />
|
||||
Columns
|
||||
<IconChevronDown />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{currentTable
|
||||
.getAllColumns()
|
||||
.filter((column) => column.getCanHide())
|
||||
.map((column) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
className="capitalize"
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||
>
|
||||
{column.id}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* 表格内容 */}
|
||||
|
||||
@@ -11,29 +11,14 @@ import type { ColumnDef } from "@tanstack/react-table"
|
||||
|
||||
export function DashboardScanHistory() {
|
||||
const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize: 5 })
|
||||
const [searchQuery, setSearchQuery] = React.useState("")
|
||||
const [isSearching, setIsSearching] = React.useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
const handleSearchChange = (value: string) => {
|
||||
setIsSearching(true)
|
||||
setSearchQuery(value)
|
||||
setPagination((prev) => ({ ...prev, pageIndex: 0 }))
|
||||
}
|
||||
|
||||
const { data, isLoading, isFetching } = useScans({
|
||||
const { data, isLoading } = useScans({
|
||||
page: pagination.pageIndex + 1,
|
||||
pageSize: pagination.pageSize,
|
||||
status: 'running',
|
||||
search: searchQuery || undefined,
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isFetching && isSearching) {
|
||||
setIsSearching(false)
|
||||
}
|
||||
}, [isFetching, isSearching])
|
||||
|
||||
const formatDate = React.useCallback((dateString: string) => new Date(dateString).toLocaleString("zh-CN", { hour12: false }), [])
|
||||
const navigate = React.useCallback((path: string) => router.push(path), [router])
|
||||
const handleDelete = React.useCallback(() => {}, [])
|
||||
@@ -65,9 +50,7 @@ export function DashboardScanHistory() {
|
||||
<ScanHistoryDataTable
|
||||
data={data?.results ?? []}
|
||||
columns={columns}
|
||||
searchValue={searchQuery}
|
||||
onSearch={handleSearchChange}
|
||||
isSearching={isSearching}
|
||||
hideToolbar
|
||||
hidePagination
|
||||
pagination={pagination}
|
||||
setPagination={setPagination}
|
||||
|
||||
@@ -245,7 +245,7 @@ export const createScanHistoryColumns = ({
|
||||
{
|
||||
accessorKey: "summary",
|
||||
header: "Summary",
|
||||
size: 250,
|
||||
size: 420,
|
||||
minSize: 150,
|
||||
cell: ({ row }) => {
|
||||
const summary = (row.getValue("summary") as {
|
||||
|
||||
@@ -27,7 +27,11 @@ import { ScanProgressDialog, buildScanProgressData, type ScanProgressData } from
|
||||
* 扫描历史列表组件
|
||||
* 用于显示和管理扫描历史记录
|
||||
*/
|
||||
export function ScanHistoryList() {
|
||||
interface ScanHistoryListProps {
|
||||
hideToolbar?: boolean
|
||||
}
|
||||
|
||||
export function ScanHistoryList({ hideToolbar = false }: ScanHistoryListProps) {
|
||||
const queryClient = useQueryClient()
|
||||
const [selectedScans, setSelectedScans] = useState<ScanRecord[]>([])
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||
@@ -259,7 +263,7 @@ export function ScanHistoryList() {
|
||||
<ScanHistoryDataTable
|
||||
data={scans}
|
||||
columns={scanColumns as ColumnDef<ScanRecord>[]}
|
||||
onBulkDelete={handleBulkDelete}
|
||||
onBulkDelete={hideToolbar ? undefined : handleBulkDelete}
|
||||
onSelectionChange={setSelectedScans}
|
||||
searchPlaceholder="搜索目标名称..."
|
||||
searchColumn="targetName"
|
||||
@@ -275,6 +279,7 @@ export function ScanHistoryList() {
|
||||
totalPages: data?.totalPages || 1,
|
||||
}}
|
||||
onPaginationChange={handlePaginationChange}
|
||||
hideToolbar={hideToolbar}
|
||||
/>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
|
||||
@@ -87,6 +87,7 @@ interface VulnerabilitiesDataTableProps {
|
||||
onSelectionChange?: (selectedRows: Vulnerability[]) => void
|
||||
onDownloadAll?: () => void
|
||||
onDownloadSelected?: () => void
|
||||
hideToolbar?: boolean
|
||||
}
|
||||
|
||||
export function VulnerabilitiesDataTable({
|
||||
@@ -102,6 +103,7 @@ export function VulnerabilitiesDataTable({
|
||||
onSelectionChange,
|
||||
onDownloadAll,
|
||||
onDownloadSelected,
|
||||
hideToolbar = false,
|
||||
}: VulnerabilitiesDataTableProps) {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
|
||||
@@ -176,105 +178,107 @@ export function VulnerabilitiesDataTable({
|
||||
return (
|
||||
<div className="w-full space-y-4">
|
||||
{/* 工具栏 */}
|
||||
<div className="flex items-center justify-between">
|
||||
{/* 智能过滤输入框 */}
|
||||
<SmartFilterInput
|
||||
fields={VULNERABILITY_FILTER_FIELDS}
|
||||
examples={VULNERABILITY_FILTER_EXAMPLES}
|
||||
value={filterValue}
|
||||
onSearch={handleFilterSearch}
|
||||
className="flex-1 max-w-xl"
|
||||
/>
|
||||
{!hideToolbar && (
|
||||
<div className="flex items-center justify-between">
|
||||
{/* 智能过滤输入框 */}
|
||||
<SmartFilterInput
|
||||
fields={VULNERABILITY_FILTER_FIELDS}
|
||||
examples={VULNERABILITY_FILTER_EXAMPLES}
|
||||
value={filterValue}
|
||||
onSearch={handleFilterSearch}
|
||||
className="flex-1 max-w-xl"
|
||||
/>
|
||||
|
||||
{/* 右侧操作按钮 */}
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* 列显示控制 */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<IconLayoutColumns />
|
||||
Columns
|
||||
<IconChevronDown />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter(
|
||||
(column) =>
|
||||
typeof column.accessorFn !== "undefined" && column.getCanHide()
|
||||
)
|
||||
.map((column) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
className="capitalize"
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||
>
|
||||
{column.id === "select" && "Select"}
|
||||
{column.id === "title" && "Title"}
|
||||
{column.id === "severity" && "Severity"}
|
||||
{column.id === "status" && "Status"}
|
||||
{column.id === "url" && "URL"}
|
||||
{column.id === "createdAt" && "Created At"}
|
||||
{column.id === "actions" && "Actions"}
|
||||
{!["select", "title", "severity", "status", "url", "createdAt", "actions"].includes(column.id) && column.id}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* 下载按钮 */}
|
||||
{(onDownloadAll || onDownloadSelected) && (
|
||||
{/* 右侧操作按钮 */}
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* 列显示控制 */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<IconDownload />
|
||||
Download
|
||||
<IconLayoutColumns />
|
||||
Columns
|
||||
<IconChevronDown />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-64">
|
||||
<DropdownMenuLabel>Download Options</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{onDownloadAll && (
|
||||
<DropdownMenuItem onClick={onDownloadAll}>
|
||||
<IconDownload className="h-4 w-4" />
|
||||
Download All Vulnerabilities
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{onDownloadSelected && (
|
||||
<DropdownMenuItem
|
||||
onClick={onDownloadSelected}
|
||||
disabled={table.getFilteredSelectedRowModel().rows.length === 0}
|
||||
>
|
||||
<IconDownload className="h-4 w-4" />
|
||||
Download Selected Vulnerabilities
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuContent align="end">
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter(
|
||||
(column) =>
|
||||
typeof column.accessorFn !== "undefined" && column.getCanHide()
|
||||
)
|
||||
.map((column) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
className="capitalize"
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||
>
|
||||
{column.id === "select" && "Select"}
|
||||
{column.id === "title" && "Title"}
|
||||
{column.id === "severity" && "Severity"}
|
||||
{column.id === "status" && "Status"}
|
||||
{column.id === "url" && "URL"}
|
||||
{column.id === "createdAt" && "Created At"}
|
||||
{column.id === "actions" && "Actions"}
|
||||
{!["select", "title", "severity", "status", "url", "createdAt", "actions"].includes(column.id) && column.id}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
{/* 批量删除按钮 */}
|
||||
{onBulkDelete && (
|
||||
<Button
|
||||
onClick={onBulkDelete}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={table.getFilteredSelectedRowModel().rows.length === 0}
|
||||
className={
|
||||
table.getFilteredSelectedRowModel().rows.length === 0
|
||||
? "text-muted-foreground"
|
||||
: "text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
}
|
||||
>
|
||||
<IconTrash />
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
{/* 下载按钮 */}
|
||||
{(onDownloadAll || onDownloadSelected) && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<IconDownload />
|
||||
Download
|
||||
<IconChevronDown />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-64">
|
||||
<DropdownMenuLabel>Download Options</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{onDownloadAll && (
|
||||
<DropdownMenuItem onClick={onDownloadAll}>
|
||||
<IconDownload className="h-4 w-4" />
|
||||
Download All Vulnerabilities
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{onDownloadSelected && (
|
||||
<DropdownMenuItem
|
||||
onClick={onDownloadSelected}
|
||||
disabled={table.getFilteredSelectedRowModel().rows.length === 0}
|
||||
>
|
||||
<IconDownload className="h-4 w-4" />
|
||||
Download Selected Vulnerabilities
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
{/* 批量删除按钮 */}
|
||||
{onBulkDelete && (
|
||||
<Button
|
||||
onClick={onBulkDelete}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={table.getFilteredSelectedRowModel().rows.length === 0}
|
||||
className={
|
||||
table.getFilteredSelectedRowModel().rows.length === 0
|
||||
? "text-muted-foreground"
|
||||
: "text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
}
|
||||
>
|
||||
<IconTrash />
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-md border overflow-x-auto">
|
||||
<Table style={{ minWidth: table.getCenterTotalSize() }}>
|
||||
|
||||
@@ -23,11 +23,14 @@ interface VulnerabilitiesDetailViewProps {
|
||||
scanId?: number
|
||||
/** 目标详情页面使用:按 target 维度查看漏洞 */
|
||||
targetId?: number
|
||||
/** 隐藏工具栏(搜索、列控制等) */
|
||||
hideToolbar?: boolean
|
||||
}
|
||||
|
||||
export function VulnerabilitiesDetailView({
|
||||
scanId,
|
||||
targetId,
|
||||
hideToolbar = false,
|
||||
}: VulnerabilitiesDetailViewProps) {
|
||||
const [selectedVulnerabilities, setSelectedVulnerabilities] = useState<Vulnerability[]>([])
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||
@@ -212,6 +215,7 @@ export function VulnerabilitiesDetailView({
|
||||
}}
|
||||
onPaginationChange={handlePaginationChange}
|
||||
onSelectionChange={setSelectedVulnerabilities}
|
||||
hideToolbar={hideToolbar}
|
||||
/>
|
||||
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
|
||||
@@ -26,8 +26,8 @@ const STORAGE_KEY = 'color-theme'
|
||||
* 获取当前颜色主题
|
||||
*/
|
||||
function getStoredTheme(): ColorThemeId {
|
||||
if (typeof window === 'undefined') return 'vercel'
|
||||
return (localStorage.getItem(STORAGE_KEY) as ColorThemeId) || 'vercel'
|
||||
if (typeof window === 'undefined') return 'vercel-dark'
|
||||
return (localStorage.getItem(STORAGE_KEY) as ColorThemeId) || 'vercel-dark'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,7 +43,7 @@ function applyThemeAttribute(themeId: ColorThemeId) {
|
||||
* 颜色主题 hook
|
||||
*/
|
||||
export function useColorTheme() {
|
||||
const [theme, setThemeState] = useState<ColorThemeId>('vercel')
|
||||
const [theme, setThemeState] = useState<ColorThemeId>('vercel-dark')
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { setTheme: setNextTheme } = useTheme()
|
||||
|
||||
|
||||
@@ -212,8 +212,7 @@ show_summary() {
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}访问地址:${RESET}"
|
||||
printf " %-16s %s\n" "XingRin:" "https://${ACCESS_HOST}/"
|
||||
echo -e " ${YELLOW}(HTTP 会自动跳转到 HTTPS)${RESET}"
|
||||
printf " %-16s %s\n" "XingRin:" "https://${ACCESS_HOST}:8083/"
|
||||
echo
|
||||
|
||||
echo -e "${YELLOW}默认登录账号:${RESET}"
|
||||
@@ -233,7 +232,7 @@ show_summary() {
|
||||
|
||||
echo -e "${YELLOW}[!] 云服务器某些厂商默认开启了安全策略(阿里云/腾讯云/华为云等):${RESET}"
|
||||
echo -e " 端口未放行可能导致无法访问或无法扫描,强烈推荐用国外vps,或者在云控制台放行:"
|
||||
echo -e " ${RESET}80, 443, 5432, 6379"
|
||||
echo -e " ${RESET}8083, 5432, 6379"
|
||||
echo
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user