Compare commits

...

4 Commits

Author SHA1 Message Date
yyhuni
7a6d2054f6 更新:ui 2025-12-25 11:50:21 +08:00
yyhuni
73ebaab232 更新:ui 2025-12-25 11:31:25 +08:00
github-actions[bot]
11899b29c2 chore: bump version to v1.1.10 2025-12-25 03:20:57 +00:00
github-actions[bot]
877d2a56d1 chore: bump version to v1.1.9 2025-12-25 03:13:58 +00:00
13 changed files with 399 additions and 307 deletions

View File

@@ -1 +1 @@
v1.1.8
v1.1.10

View File

@@ -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")

View File

@@ -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% {

View File

@@ -83,7 +83,7 @@ export default function RootLayout({
{/* ThemeProvider 提供主题切换功能,跟随系统自动切换亮暗色 */}
<ThemeProvider
attribute="class"
defaultTheme="system"
defaultTheme="dark"
enableSystem
disableTransitionOnChange
>

View File

@@ -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">

View File

@@ -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>
)

View File

@@ -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>
{/* 表格内容 */}

View File

@@ -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}

View File

@@ -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 {

View File

@@ -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}
/>
{/* 删除确认对话框 */}

View File

@@ -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() }}>

View File

@@ -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}>

View File

@@ -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()