From fba7f7c5087c2fe083c099c577d2680f43dde8d5 Mon Sep 17 00:00:00 2001 From: yyhuni Date: Sun, 28 Dec 2025 19:55:57 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0ui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/scripts/generate_test_data_sql.py | 848 +++++++++++++++--- frontend/app/tools/nuclei/page.tsx | 5 +- frontend/components/app-sidebar.tsx | 28 +- .../directories/directories-columns.tsx | 12 +- .../directories/directories-data-table.tsx | 14 +- .../endpoints/endpoints-columns.tsx | 84 +- .../endpoints/endpoints-data-table.tsx | 15 +- .../ehole-fingerprint-columns.tsx | 65 +- .../fingerprints/goby-fingerprint-columns.tsx | 36 +- .../wappalyzer-fingerprint-columns.tsx | 189 ++-- .../ip-addresses/ip-addresses-columns.tsx | 9 +- .../organization/organization-columns.tsx | 19 +- .../components/scan/engine/engine-columns.tsx | 9 + .../scan/history/scan-history-columns.tsx | 7 +- .../subdomains/subdomains-columns.tsx | 16 +- .../subdomains/subdomains-data-table.tsx | 20 +- .../components/target/all-targets-columns.tsx | 54 +- .../components/target/targets-data-table.tsx | 14 +- .../tools/commands/commands-columns.tsx | 31 +- .../ui/data-table/expandable-cell.tsx | 280 ++++++ .../ui/data-table/unified-data-table.tsx | 16 +- frontend/components/ui/truncated-cell.tsx | 83 -- .../vulnerabilities-columns.tsx | 16 +- .../components/websites/websites-columns.tsx | 67 +- .../websites/websites-data-table.tsx | 15 +- frontend/types/data-table.types.ts | 6 + 26 files changed, 1335 insertions(+), 623 deletions(-) create mode 100644 frontend/components/ui/data-table/expandable-cell.tsx delete mode 100644 frontend/components/ui/truncated-cell.tsx diff --git a/backend/scripts/generate_test_data_sql.py b/backend/scripts/generate_test_data_sql.py index 51ef0ab8..1b890499 100644 --- a/backend/scripts/generate_test_data_sql.py +++ b/backend/scripts/generate_test_data_sql.py @@ -33,6 +33,117 @@ import psycopg2 from psycopg2.extras import execute_values +def generate_fixed_length_url(target_name: str, length: int = 245, path_hint: str = '') -> str: + """ + 生成固定长度的 URL + + Args: + target_name: 目标域名 + length: 目标URL长度,默认245 + path_hint: 可选的路径提示,用于区分不同类型的URL + + Returns: + 固定长度的URL字符串 + """ + base = f'https://{target_name}' + + # 基础路径 + paths = [ + '/api/v3/enterprise/security-assessment/vulnerability-management', + '/admin/dashboard/system-configuration/advanced-settings', + '/portal/user-authentication/multi-factor/verification', + '/services/cloud-infrastructure/monitoring/metrics', + '/internal/system-administration/audit-logging/events', + ] + + path = random.choice(paths) if not path_hint else f'/{path_hint}' + url = f'{base}{path}' + + # 添加查询参数 + param_idx = 0 + while len(url) < length - 20: + param_idx += 1 + param = f'p{param_idx}={random.randint(10000000, 99999999)}' + separator = '?' if '?' not in url else '&' + url = f'{url}{separator}{param}' + + # 精确调整到目标长度 + if len(url) < length: + # 添加填充参数 + padding_needed = length - len(url) - 1 # -1 for '&' or '?' + if padding_needed > 0: + separator = '?' if '?' not in url else '&' + # 创建精确长度的填充 + padding = 'x' * padding_needed + url = f'{url}{separator}{padding}' + + # 截断到精确长度 + if len(url) > length: + url = url[:length] + + return url + + +def generate_fixed_length_text(length: int = 300, text_type: str = 'description') -> str: + """ + 生成固定长度的文本内容 + + Args: + length: 目标文本长度,默认300 + text_type: 文本类型,用于选择不同的内容模板 + + Returns: + 固定长度的文本字符串 + """ + # 基础文本模板 + templates = { + 'description': [ + 'A critical security vulnerability was discovered in the application authentication module. This vulnerability allows attackers to bypass security controls and gain unauthorized access to sensitive system resources. The issue stems from improper input validation and insufficient access control mechanisms. Exploitation could lead to complete system compromise, data exfiltration, and service disruption. Immediate remediation is strongly recommended including implementing proper input sanitization, strengthening authentication mechanisms, and deploying additional security monitoring. The vulnerability affects multiple components including user authentication, session management, API endpoints, and data processing pipelines. Risk assessment indicates high severity with potential for significant business impact.', + 'Server-side request forgery (SSRF) vulnerability detected in the API gateway service. An attacker can manipulate server-side requests to access internal network resources, potentially exposing sensitive configuration data, internal services, and cloud metadata endpoints. The vulnerability exists due to insufficient URL validation in the proxy functionality. This could allow attackers to scan internal networks, access cloud instance metadata, retrieve sensitive credentials, and pivot to other internal systems. Recommended mitigations include implementing strict URL allowlisting, blocking requests to internal IP ranges, and adding network segmentation controls. The vulnerability has been assigned a high severity rating due to potential for lateral movement.', + 'Remote code execution vulnerability identified in the file upload processing module. Insufficient file type validation allows attackers to upload malicious executable files that can be triggered to execute arbitrary code on the server. The vulnerability bypasses existing security controls through specially crafted file headers and extension manipulation. Successful exploitation grants attackers full control over the affected server, enabling data theft, malware deployment, and establishment of persistent backdoor access. Critical remediation steps include implementing strict file type validation, sandboxed file processing, content inspection, and removal of execution permissions from upload directories. This vulnerability requires immediate attention.', + 'Cross-site scripting (XSS) vulnerability found in the user profile management interface. User-supplied input is rendered without proper encoding, allowing injection of malicious JavaScript code. Attackers can exploit this to steal session tokens, perform actions on behalf of authenticated users, redirect victims to phishing sites, and exfiltrate sensitive personal information. The vulnerability affects multiple input fields including display name, bio, and custom URL parameters. Remediation requires implementing context-aware output encoding, Content Security Policy headers, and input validation. Additionally, consider implementing HTTP-only and Secure flags on session cookies to limit the impact of successful XSS attacks.', + 'SQL injection vulnerability discovered in the advanced search functionality. The application constructs database queries using unsanitized user input, enabling attackers to manipulate query logic, extract sensitive data, modify database contents, or execute administrative operations. The vulnerability affects the product search, user lookup, and reporting modules. Exploitation could result in complete database compromise, unauthorized data access, data manipulation, and potential privilege escalation. Immediate remediation includes implementing parameterized queries, stored procedures, input validation, and principle of least privilege for database accounts. Consider deploying a web application firewall as an additional defense layer.', + ], + 'organization': [ + 'A leading global technology corporation specializing in enterprise software solutions, cloud computing infrastructure, cybersecurity services, and digital transformation consulting. The organization operates across multiple continents with regional headquarters in North America, Europe, and Asia-Pacific. Core business units include enterprise resource planning systems, customer relationship management platforms, supply chain optimization tools, and advanced analytics solutions. The company maintains strategic partnerships with major cloud providers and technology vendors. Annual revenue exceeds several billion dollars with consistent year-over-year growth. The organization employs thousands of professionals including software engineers, security researchers, and business consultants.', + 'An innovative financial technology company providing comprehensive digital banking services, payment processing solutions, and investment management platforms. The organization serves millions of customers globally through mobile applications, web portals, and API integrations. Key offerings include real-time payment processing, cryptocurrency trading, automated investment advisory, and small business lending. The company maintains regulatory compliance across multiple jurisdictions and holds various financial services licenses. Security infrastructure includes advanced fraud detection, multi-factor authentication, and end-to-end encryption. The organization has received multiple industry awards for innovation and customer satisfaction.', + 'A healthcare technology enterprise focused on electronic health records, telemedicine platforms, medical device integration, and healthcare analytics. The organization partners with hospitals, clinics, and healthcare systems worldwide to improve patient outcomes and operational efficiency. Core products include comprehensive EHR systems, patient engagement portals, clinical decision support tools, and population health management platforms. The company maintains strict compliance with healthcare regulations including HIPAA, GDPR, and regional data protection requirements. Research and development investments focus on artificial intelligence applications in diagnostics, treatment optimization, and predictive health analytics.', + ], + 'title': [ + 'Enterprise Resource Planning System - Comprehensive Business Management Dashboard with Real-time Analytics, Workflow Automation, and Multi-department Integration Capabilities for Global Operations Management and Strategic Decision Support', + 'Advanced Security Operations Center - Unified Threat Detection and Response Platform featuring Machine Learning-powered Anomaly Detection, Automated Incident Response, and Comprehensive Security Posture Management', + 'Customer Experience Management Platform - Omnichannel Engagement Solution with AI-driven Personalization, Journey Orchestration, Sentiment Analysis, and Predictive Customer Behavior Modeling Capabilities', + 'Cloud Infrastructure Management Console - Multi-cloud Orchestration Platform supporting AWS, Azure, and GCP with Automated Provisioning, Cost Optimization, Compliance Monitoring, and Performance Analytics', + 'Data Analytics and Business Intelligence Suite - Self-service Analytics Platform with Advanced Visualization, Predictive Modeling, Natural Language Query Processing, and Automated Report Generation', + ], + } + + # 选择模板 + template_list = templates.get(text_type, templates['description']) + base_text = random.choice(template_list) + + # 调整到目标长度 + if len(base_text) < length: + # 需要扩展文本 + padding_words = [ + 'Additionally', 'Furthermore', 'Moreover', 'Consequently', 'Subsequently', + 'comprehensive', 'implementation', 'infrastructure', 'configuration', 'authentication', + 'vulnerability', 'exploitation', 'remediation', 'mitigation', 'assessment', + ] + while len(base_text) < length - 20: + base_text += f' {random.choice(padding_words)}' + # 精确填充 + if len(base_text) < length: + padding_needed = length - len(base_text) + base_text += ' ' + 'x' * (padding_needed - 1) + + # 截断到精确长度 + if len(base_text) > length: + base_text = base_text[:length] + + return base_text + + def load_env_file(env_path: str) -> dict: """从 .env 文件加载环境变量""" env_vars = {} @@ -107,6 +218,11 @@ class TestDataGenerator: self.create_host_port_mapping_snapshots(scan_ids) self.create_vulnerability_snapshots(scan_ids) + # 生成指纹数据 + self.create_ehole_fingerprints() + self.create_goby_fingerprints() + self.create_wappalyzer_fingerprints() + self.conn.commit() print("\n✅ 测试数据生成完成!") except Exception as e: @@ -120,6 +236,8 @@ class TestDataGenerator: """清除所有测试数据""" cur = self.conn.cursor() tables = [ + # 指纹表 + 'ehole_fingerprint', 'goby_fingerprint', 'wappalyzer_fingerprint', # 快照表(先删除,因为有外键依赖 scan) 'vulnerability_snapshot', 'host_port_mapping_snapshot', 'directory_snapshot', 'endpoint_snapshot', 'website_snapshot', 'subdomain_snapshot', @@ -264,9 +382,11 @@ class TestDataGenerator: selected = random.sample(org_templates, min(num_orgs, len(org_templates))) ids = [] - for name_base, desc in selected: + for name_base, _ in selected: division = random.choice(divisions) name = f'{name_base} - {division} ({suffix})' + # 生成固定 300 长度的描述 + desc = generate_fixed_length_text(length=300, text_type='organization') cur.execute(""" INSERT INTO organization (name, description, created_at, deleted_at) VALUES (%s, %s, NOW() - INTERVAL '%s days', NULL) @@ -335,13 +455,27 @@ class TestDataGenerator: if row: ids.append(row[0]) # 随机关联到组织 - 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])) + if org_ids: + # 20% 概率关联多个组织(3-5个),50% 概率关联1个组织,30% 不关联 + rand_val = random.random() + if rand_val < 0.2: + # 关联多个组织 (3-5个) + num_orgs = min(random.randint(3, 5), len(org_ids)) + selected_orgs = random.sample(org_ids, num_orgs) + for org_id in selected_orgs: + cur.execute(""" + INSERT INTO organization_targets (organization_id, target_id) + VALUES (%s, %s) + ON CONFLICT DO NOTHING + """, (org_id, row[0])) + elif rand_val < 0.7: + # 关联1个组织 + 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])) # 随机生成 50-80 个 IP 目标 num_ips = random.randint(50, 80) @@ -702,17 +836,8 @@ class TestDataGenerator: batch_data = [] for target_id, target_name in domain_targets: for i in range(random.randint(15, 30)): - protocol = random.choice(['https', 'http']) - port = random.choice([80, 443, 8080, 8443, 3000]) - - if port in [80, 443]: - url = f'{protocol}://{target_name}/' - else: - url = f'{protocol}://{target_name}:{port}/' - - if i > 0: - path = random.choice(['admin/', 'api/', 'portal/', 'dashboard/']) - url = f'{protocol}://{target_name}:{port}/{path}' + # 生成固定 245 长度的 URL + url = generate_fixed_length_url(target_name, length=245, path_hint=f'website/{i:04d}') batch_data.append(( url, target_id, target_name, random.choice(titles), @@ -778,8 +903,62 @@ class TestDataGenerator: '/internal/secrets/vault/kv/applications/credentials/rotation', ] - gf_patterns = [['debug', 'config'], ['api', 'json'], ['upload', 'file'], ['admin'], ['auth'], - ['secrets', 'credentials'], ['backup', 'archive'], ['debug', 'trace'], []] + gf_patterns = [ + ['debug', 'config', 'api', 'json', 'upload', 'file', 'admin', 'auth', 'secrets', 'credentials'], + ['backup', 'archive', 'debug', 'trace', 'log', 'error', 'exception', 'stack', 'dump', 'memory'], + ['api', 'rest', 'graphql', 'websocket', 'grpc', 'soap', 'xml', 'json', 'yaml', 'protobuf'], + ['auth', 'login', 'logout', 'session', 'token', 'jwt', 'oauth', 'saml', 'sso', 'mfa', 'otp', '2fa'], + ['upload', 'download', 'file', 'attachment', 'document', 'image', 'video', 'audio', 'media', 'asset'], + ['admin', 'dashboard', 'panel', 'console', 'management', 'settings', 'config', 'system', 'control'], + ['database', 'sql', 'query', 'table', 'schema', 'migration', 'backup', 'restore', 'dump', 'export'], + ['cache', 'redis', 'memcached', 'session', 'storage', 'temp', 'buffer', 'queue', 'message', 'event'], + ['security', 'vulnerability', 'exploit', 'injection', 'xss', 'csrf', 'ssrf', 'rce', 'lfi', 'sqli'], + ['payment', 'billing', 'invoice', 'subscription', 'checkout', 'cart', 'order', 'transaction', 'refund'], + ['user', 'profile', 'account', 'password', 'email', 'phone', 'address', 'preference', 'notification'], + ['api-key', 'secret-key', 'access-token', 'refresh-token', 'private-key', 'public-key', 'certificate'], + ['debug', 'trace', 'log', 'error', 'warning', 'info', 'verbose', 'metric', 'monitor', 'health'], + ['internal', 'private', 'restricted', 'confidential', 'sensitive', 'protected', 'secure', 'encrypted'], + ['test', 'staging', 'development', 'production', 'sandbox', 'demo', 'preview', 'beta', 'alpha'], + [], # 空的情况 + ] + + # 100字符长度的标题 + titles = [ + 'Enterprise API Gateway - RESTful Service Documentation with OpenAPI 3.0 Specification and Interactive', + 'User Authentication Service - OAuth 2.0 and SAML 2.0 Single Sign-On Integration Platform Dashboard', + 'Payment Processing Gateway - PCI-DSS Compliant Transaction Management System Administration Panel', + 'Content Delivery Network - Global Edge Cache Management and Real-time Analytics Dashboard Interface', + 'Database Administration Console - PostgreSQL Cluster Management with Automated Backup and Recovery', + 'Kubernetes Container Orchestration - Pod Deployment and Service Mesh Configuration Control Panel', + 'Message Queue Management - RabbitMQ Exchange and Binding Configuration with Dead Letter Handling', + 'Search Engine Administration - Elasticsearch Index Management and Query Performance Optimization', + 'Monitoring and Alerting System - Prometheus Metrics Collection with Grafana Dashboard Integration', + 'Security Operations Center - Vulnerability Assessment and Incident Response Management Platform', + 'API Rate Limiting Service - Request Throttling and Quota Management with Real-time Usage Analytics', + 'File Storage Management - S3-Compatible Object Storage with Lifecycle Policy and Access Control', + 'Email Notification Service - SMTP Gateway with Template Management and Delivery Status Tracking', + 'Webhook Integration Platform - Event-Driven Architecture with Retry Logic and Failure Handling', + 'GraphQL API Playground - Interactive Query Builder with Schema Introspection and Documentation', + ] + + # 扩展的技术栈列表(用于生成10-20个技术) + all_techs = [ + 'React 18.2.0', 'Vue.js 3.4', 'Angular 17.1', 'Next.js 14.0', 'Nuxt 3.9', 'Svelte 4.2', + 'Node.js 20.10', 'Express 4.18', 'NestJS 10.3', 'Fastify 4.25', 'Koa 2.15', + 'Python 3.12', 'Django 5.0', 'FastAPI 0.109', 'Flask 3.0', 'Tornado 6.4', + 'Go 1.21', 'Gin 1.9', 'Echo 4.11', 'Fiber 2.52', 'Chi 5.0', + 'Java 21', 'Spring Boot 3.2', 'Quarkus 3.6', 'Micronaut 4.2', + 'PostgreSQL 16.1', 'MySQL 8.2', 'MongoDB 7.0', 'Redis 7.2', 'Elasticsearch 8.11', + 'Kubernetes 1.28', 'Docker 24.0', 'Nginx 1.25', 'Apache 2.4', 'Traefik 3.0', + 'GraphQL 16.8', 'gRPC 1.60', 'WebSocket', 'REST API', 'OpenAPI 3.0', + 'JWT', 'OAuth 2.0', 'SAML 2.0', 'OIDC', 'Passport.js', + 'Webpack 5.89', 'Vite 5.0', 'esbuild 0.19', 'Rollup 4.9', 'Parcel 2.11', + 'TypeScript 5.3', 'Tailwind CSS 3.4', 'Bootstrap 5.3', 'Material UI 5.15', + 'Jest 29.7', 'Vitest 1.1', 'Cypress 13.6', 'Playwright 1.40', + 'Prometheus', 'Grafana 10.2', 'Jaeger', 'Zipkin', 'OpenTelemetry', + 'RabbitMQ 3.12', 'Kafka 3.6', 'NATS 2.10', 'Redis Streams', + 'AWS Lambda', 'Azure Functions', 'Google Cloud Functions', 'Cloudflare Workers', + ] # 真实的 API 响应 body preview body_previews = [ @@ -820,19 +999,28 @@ class TestDataGenerator: num = random.randint(50, 100) selected = random.sample(paths, min(num, len(paths))) - for path in selected: - protocol = random.choice(['https', 'http']) - port = random.choice([443, 8443, 3000, 8080]) - url = f'{protocol}://{target_name}:{port}{path}' if port != 443 else f'{protocol}://{target_name}{path}' + for idx, path in enumerate(selected): + # 生成固定 245 长度的 URL + url = generate_fixed_length_url(target_name, length=245, path_hint=f'endpoint/{idx:04d}') + + # 生成 100 字符的标题 + title = random.choice(titles) + + # 生成 10-20 个技术 + num_techs = random.randint(10, 20) + tech_list = random.sample(all_techs, min(num_techs, len(all_techs))) + + # 生成 10-20 个 tags (gf_patterns) + tags = random.choice(gf_patterns) batch_data.append(( - url, target_id, target_name, 'API Documentation - Swagger UI', + url, target_id, target_name, title, random.choice(['nginx/1.24.0', 'gunicorn/21.2.0']), random.choice([200, 201, 301, 400, 401, 403, 404, 500]), random.randint(100, 50000), 'application/json', - random.choice([['Node.js', 'Express'], ['Python', 'FastAPI'], ['Go', 'Gin']]), + tech_list, '', random.choice(body_previews), - random.choice([True, False, None]), random.choice(gf_patterns) + random.choice([True, False, None]), tags )) count += 1 @@ -855,10 +1043,6 @@ class TestDataGenerator: print("📁 创建目录...") cur = self.conn.cursor() - if not website_ids: - print(" ⚠ 没有网站,跳过\n") - return - dir_paths = [ '/admin/', '/administrator/', '/wp-admin/', '/wp-content/', '/backup/', '/backups/', '/old/', '/archive/', '/temp/', '/test/', '/dev/', '/staging/', '/config/', @@ -890,18 +1074,23 @@ class TestDataGenerator: content_types = ['text/html; charset=utf-8', 'application/json', 'text/plain', 'text/css', 'application/xml', 'application/javascript', 'text/xml'] - # 获取网站信息(用于生成目录 URL) - cur.execute("SELECT id, url, target_id FROM website LIMIT 100") - websites = cur.fetchall() + # 直接获取域名目标来生成目录数据 + cur.execute("SELECT id, name FROM target WHERE type = 'domain' AND deleted_at IS NULL LIMIT 100") + domain_targets = cur.fetchall() + + if not domain_targets: + print(" ⚠ 没有域名目标,跳过\n") + return count = 0 batch_data = [] - for website_id, website_url, target_id in websites: + for target_id, target_name in domain_targets: num = random.randint(60, 100) selected = random.sample(dir_paths, min(num, len(dir_paths))) - for path in selected: - url = website_url.rstrip('/') + path + for idx, path in enumerate(selected): + # 生成固定 245 长度的 URL + url = generate_fixed_length_url(target_name, length=245, path_hint=f'directory/{idx:04d}') batch_data.append(( url, target_id, random.choice([200, 301, 302, 403, 404, 500]), @@ -997,45 +1186,62 @@ class TestDataGenerator: cur = self.conn.cursor() vuln_types = [ - 'sql-injection', 'cross-site-scripting-xss', 'cross-site-request-forgery-csrf', - 'server-side-request-forgery-ssrf', 'xml-external-entity-xxe', 'remote-code-execution-rce', - 'local-file-inclusion-lfi', 'directory-traversal', 'authentication-bypass', - 'insecure-direct-object-reference-idor', 'sensitive-data-exposure', 'security-misconfiguration', - 'broken-access-control', 'cors-misconfiguration', 'subdomain-takeover', - 'exposed-admin-panel', 'default-credentials', 'information-disclosure', - # 扩展漏洞类型 - 'command-injection', 'ldap-injection', 'xpath-injection', 'nosql-injection', - 'template-injection-ssti', 'deserialization-vulnerability', 'jwt-vulnerability', - 'open-redirect', 'http-request-smuggling', 'host-header-injection', - 'clickjacking', 'session-fixation', 'session-hijacking', 'privilege-escalation', - 'path-traversal', 'arbitrary-file-upload', 'arbitrary-file-download', - 'buffer-overflow', 'integer-overflow', 'race-condition', 'time-based-attack', - 'blind-sql-injection', 'stored-xss', 'dom-based-xss', 'reflected-xss', - 'crlf-injection', 'http-response-splitting', 'cache-poisoning', 'dns-rebinding', - 'prototype-pollution', 'mass-assignment', 'graphql-introspection-enabled', - 'api-key-exposure', 'hardcoded-credentials', 'weak-password-policy', - 'missing-rate-limiting', 'missing-security-headers', 'insecure-cookie-configuration', - 'tls-ssl-vulnerability', 'weak-cipher-suite', 'certificate-validation-bypass', + 'sql-injection-authentication-bypass-vulnerability-', # 50 chars + 'cross-site-scripting-xss-stored-persistent-attack-', # 50 chars + 'cross-site-request-forgery-csrf-token-validation--', # 50 chars + 'server-side-request-forgery-ssrf-internal-access--', # 50 chars + 'xml-external-entity-xxe-injection-vulnerability---', # 50 chars + 'remote-code-execution-rce-command-injection-flaw--', # 50 chars + 'local-file-inclusion-lfi-path-traversal-exploit---', # 50 chars + 'directory-traversal-arbitrary-file-read-access----', # 50 chars + 'authentication-bypass-session-management-flaw-----', # 50 chars + 'insecure-direct-object-reference-idor-access-ctrl-', # 50 chars + 'sensitive-data-exposure-information-disclosure----', # 50 chars + 'security-misconfiguration-default-credentials-----', # 50 chars + 'broken-access-control-privilege-escalation-vuln---', # 50 chars + 'cors-misconfiguration-cross-origin-data-leakage---', # 50 chars + 'subdomain-takeover-dns-misconfiguration-exploit---', # 50 chars + 'exposed-admin-panel-unauthorized-access-control---', # 50 chars + 'default-credentials-weak-authentication-bypass----', # 50 chars + 'information-disclosure-sensitive-data-exposure----', # 50 chars + 'command-injection-os-command-execution-exploit----', # 50 chars + 'ldap-injection-directory-service-manipulation-----', # 50 chars + 'xpath-injection-xml-query-manipulation-attack-----', # 50 chars + 'nosql-injection-mongodb-query-manipulation--------', # 50 chars + 'template-injection-ssti-server-side-execution-----', # 50 chars + 'deserialization-vulnerability-object-injection----', # 50 chars + 'jwt-vulnerability-token-forgery-authentication----', # 50 chars + 'open-redirect-url-redirection-phishing-attack-----', # 50 chars + 'http-request-smuggling-cache-poisoning-attack-----', # 50 chars + 'host-header-injection-password-reset-poisoning----', # 50 chars + 'clickjacking-ui-redressing-frame-injection--------', # 50 chars + 'session-fixation-authentication-session-attack----', # 50 chars ] - sources = ['nuclei', 'dalfox', 'sqlmap', 'crlfuzz', 'httpx', 'manual-testing', - 'burp-suite', 'zap', 'nmap', 'nikto', 'wpscan', 'dirsearch', 'ffuf', - 'amass', 'subfinder', 'masscan', 'nessus', 'qualys', 'acunetix'] + sources = [ + 'nuclei-vulnerability-scanner--', # 30 chars + 'dalfox-xss-parameter-analysis-', # 30 chars + 'sqlmap-sql-injection-testing--', # 30 chars + 'crlfuzz-crlf-injection-finder-', # 30 chars + 'httpx-web-probe-fingerprint---', # 30 chars + 'manual-penetration-testing----', # 30 chars + 'burp-suite-professional-scan--', # 30 chars + 'owasp-zap-security-scanner----', # 30 chars + 'nmap-network-service-scanner--', # 30 chars + 'nikto-web-server-scanner------', # 30 chars + 'wpscan-wordpress-vuln-scan----', # 30 chars + 'dirsearch-directory-brute-----', # 30 chars + 'ffuf-web-fuzzer-content-disc--', # 30 chars + 'amass-subdomain-enumeration---', # 30 chars + 'subfinder-passive-subdomain---', # 30 chars + 'masscan-port-scanner-fast-----', # 30 chars + 'nessus-vulnerability-assess---', # 30 chars + 'qualys-cloud-security-scan----', # 30 chars + 'acunetix-web-vuln-scanner-----', # 30 chars + 'semgrep-static-code-analysis--', # 30 chars + ] severities = ['unknown', 'info', 'low', 'medium', 'high', 'critical'] - descriptions = [ - 'A critical SQL injection vulnerability was discovered in the login form authentication module. An attacker can inject malicious SQL queries through the username parameter to bypass authentication or extract sensitive data from the database. The vulnerability exists due to improper input validation and lack of parameterized queries in the authentication module. This vulnerability affects all database operations including user authentication, session management, and data retrieval. Exploitation could lead to complete database compromise, unauthorized access to all user accounts, and potential data exfiltration. Recommended remediation includes implementing parameterized queries, input validation, and web application firewall rules.', - 'A reflected cross-site scripting (XSS) vulnerability was found in the search functionality of the web application. User input is not properly sanitized before being rendered in the response, allowing attackers to execute arbitrary JavaScript code in the context of the victims browser session, potentially stealing session cookies or performing actions on behalf of the user. This vulnerability can be exploited to hijack user sessions, deface the website, redirect users to malicious sites, or steal sensitive information. The attack vector includes crafted URLs that can be distributed via phishing emails or social engineering. Immediate patching is recommended along with implementation of Content Security Policy headers.', - 'Server-Side Request Forgery (SSRF) vulnerability detected in the URL preview feature of the application. An attacker can manipulate the server to make requests to internal services, potentially accessing sensitive internal resources such as cloud metadata endpoints (169.254.169.254), internal APIs, administrative interfaces, or other services that are not directly accessible from the internet. This vulnerability can be chained with other vulnerabilities to achieve remote code execution or access sensitive cloud credentials. The application should implement strict URL validation, whitelist allowed domains, and block requests to internal IP ranges.', - 'Remote Code Execution (RCE) vulnerability found in the file upload functionality of the content management system. Insufficient validation of uploaded files allows attackers to upload malicious scripts and execute arbitrary code on the server. This could lead to complete server compromise, data exfiltration, cryptocurrency mining, ransomware deployment, or lateral movement within the network infrastructure. The vulnerability bypasses file type validation by manipulating Content-Type headers or using double extensions. Recommended fixes include implementing strict file type validation, storing uploads outside the web root, and using antivirus scanning.', - 'Authentication bypass vulnerability discovered in the password reset mechanism of the user management system. Attackers can reset any users password without proper verification by manipulating the reset token or user identifier in the password reset request. This vulnerability allows unauthorized access to any user account including administrative accounts with elevated privileges. The flaw exists in the token validation logic which does not properly verify token ownership. Organizations should implement secure token generation, add rate limiting, and require additional verification steps for password resets.', - 'Insecure Direct Object Reference (IDOR) vulnerability found in the user profile API endpoints. By manipulating the user ID parameter in API requests, attackers can access, modify, or delete other users data without proper authorization checks. This affects all user-related endpoints including profile information, payment details, personal documents, and account settings. The vulnerability stems from missing access control checks at the API layer. Remediation requires implementing proper authorization checks, using indirect object references, and adding audit logging for sensitive operations.', - ] - - paths = ['/api/v1/users/login', '/api/v2/search', '/admin/dashboard', '/portal/upload', '/graphql', '/oauth/authorize', - '/api/v1/users/profile', '/api/v2/orders', '/admin/settings', '/portal/documents', '/webhook/callback', - '/api/v3/analytics', '/admin/users/export', '/portal/payments', '/api/internal/debug', '/system/config'] - # 获取域名目标 cur.execute("SELECT id, name FROM target WHERE type = 'domain' AND deleted_at IS NULL LIMIT 80") domain_targets = cur.fetchall() @@ -1045,7 +1251,7 @@ class TestDataGenerator: for target_id, target_name in domain_targets: num = random.randint(30, 80) - for _ in range(num): + for idx in range(num): severity = random.choice(severities) cvss_ranges = { 'critical': (9.0, 10.0), 'high': (7.0, 8.9), 'medium': (4.0, 6.9), @@ -1054,8 +1260,11 @@ class TestDataGenerator: cvss_range = cvss_ranges.get(severity, (0.0, 10.0)) cvss_score = round(random.uniform(*cvss_range), 1) - path = random.choice(paths) - url = f'https://{target_name}{path}?param=test&id={random.randint(1, 1000)}' + # 生成固定 245 长度的 URL + url = generate_fixed_length_url(target_name, length=245, path_hint=f'vuln/{idx:04d}') + + # 生成固定 300 长度的描述 + description = generate_fixed_length_text(length=300, text_type='description') raw_output = json.dumps({ 'template': f'CVE-2024-{random.randint(10000, 99999)}', @@ -1067,7 +1276,7 @@ class TestDataGenerator: batch_data.append(( target_id, url, random.choice(vuln_types), severity, - random.choice(sources), cvss_score, random.choice(descriptions), raw_output + random.choice(sources), cvss_score, description, raw_output )) count += 1 @@ -1098,11 +1307,23 @@ class TestDataGenerator: 'monitoring', 'metrics', 'grafana', 'prometheus', 'kibana', 'logs', 'jenkins', 'ci', 'cd', 'gitlab', 'jira', 'confluence', 'kubernetes', 'www', 'www2', 'ns1', 'ns2', 'mx', 'mx1', 'autodiscover', 'webmail', + 'api-v1', 'api-v2', 'api-v3', 'internal', 'external', 'public', 'private', + 'gateway', 'proxy', 'cache', 'redis', 'mongo', 'mysql', 'postgres', + 'elastic', 'search', 'analytics', 'reporting', 'billing', 'payment', + 'checkout', 'cart', 'shop', 'store', 'catalog', 'inventory', 'orders', + 'users', 'customers', 'partners', 'vendors', 'suppliers', 'merchants', + 'docs', 'help', 'support', 'faq', 'kb', 'wiki', 'blog', 'news', + 'status', 'health', 'ping', 'heartbeat', 'uptime', 'monitor', + 'backup', 'archive', 'storage', 'files', 'uploads', 'downloads', + 'assets', 'images', 'media', 'video', 'audio', 'fonts', 'icons', + 'api-gateway', 'load-balancer', 'reverse-proxy', 'edge', 'origin', + 'primary', 'secondary', 'failover', 'replica', 'master', 'slave', + 'prod', 'stage', 'preprod', 'sandbox', 'demo', 'preview', 'canary', ] count = 0 batch_data = [] - for scan_id in scan_ids[:100]: # 为前100个扫描创建快照 + for scan_id in scan_ids: # 为所有扫描创建快照 # 获取扫描对应的目标域名 cur.execute(""" SELECT t.name FROM scan s @@ -1114,7 +1335,7 @@ class TestDataGenerator: continue target_name = row[0] - num = random.randint(40, 80) + num = random.randint(60, 100) selected = random.sample(prefixes, min(num, len(prefixes))) for prefix in selected: @@ -1161,7 +1382,7 @@ class TestDataGenerator: count = 0 batch_data = [] - for scan_id in scan_ids[:100]: + for scan_id in scan_ids: # 为所有扫描创建快照 cur.execute(""" SELECT t.name FROM scan s JOIN target t ON s.target_id = t.id @@ -1172,10 +1393,9 @@ class TestDataGenerator: continue target_name = row[0] - for i in range(random.randint(15, 30)): - protocol = random.choice(['https', 'http']) - port = random.choice([80, 443, 8080]) - url = f'{protocol}://{target_name}:{port}/' if port not in [80, 443] else f'{protocol}://{target_name}/' + for i in range(random.randint(30, 60)): + # 生成固定 245 长度的 URL + url = generate_fixed_length_url(target_name, length=245, path_hint=f'website-snap/{i:04d}') batch_data.append(( scan_id, url, target_name, random.choice(titles), @@ -1222,9 +1442,33 @@ class TestDataGenerator: '/swagger/v1/api-docs/openapi.json', ] + # 100字符长度的标题 + titles = [ + 'Enterprise API Gateway - RESTful Service Documentation with OpenAPI 3.0 Specification and Interactive', + 'User Authentication Service - OAuth 2.0 and SAML 2.0 Single Sign-On Integration Platform Dashboard', + 'Payment Processing Gateway - PCI-DSS Compliant Transaction Management System Administration Panel', + 'Content Delivery Network - Global Edge Cache Management and Real-time Analytics Dashboard Interface', + 'Database Administration Console - PostgreSQL Cluster Management with Automated Backup and Recovery', + ] + + # 扩展的技术栈列表 + all_techs = [ + 'React 18.2.0', 'Vue.js 3.4', 'Angular 17.1', 'Next.js 14.0', 'Node.js 20.10', + 'Express 4.18', 'Python 3.12', 'Django 5.0', 'FastAPI 0.109', 'Go 1.21', + 'PostgreSQL 16.1', 'MySQL 8.2', 'MongoDB 7.0', 'Redis 7.2', 'Elasticsearch 8.11', + 'Kubernetes 1.28', 'Docker 24.0', 'Nginx 1.25', 'GraphQL 16.8', 'JWT', + ] + + # 扩展的 tags + all_tags = [ + 'debug', 'config', 'api', 'json', 'upload', 'file', 'admin', 'auth', + 'secrets', 'credentials', 'backup', 'archive', 'trace', 'log', 'error', + 'security', 'vulnerability', 'payment', 'user', 'internal', 'private', + ] + count = 0 batch_data = [] - for scan_id in scan_ids[:100]: + for scan_id in scan_ids: # 为所有扫描创建快照 cur.execute(""" SELECT t.name FROM scan s JOIN target t ON s.target_id = t.id @@ -1235,17 +1479,30 @@ class TestDataGenerator: continue target_name = row[0] - for path in random.sample(paths, min(random.randint(20, 40), len(paths))): - url = f'https://{target_name}{path}' + for idx, path in enumerate(random.sample(paths, min(random.randint(40, 80), len(paths)))): + # 生成固定 245 长度的 URL + url = generate_fixed_length_url(target_name, length=245, path_hint=f'endpoint-snap/{idx:04d}') + + # 生成 100 字符的标题 + title = random.choice(titles) + + # 生成 10-20 个技术 + num_techs = random.randint(10, 20) + tech_list = random.sample(all_techs, min(num_techs, len(all_techs))) + + # 生成 10-20 个 tags + num_tags = random.randint(10, 20) + tags = random.sample(all_tags, min(num_tags, len(all_tags))) + batch_data.append(( - scan_id, url, target_name, 'API Endpoint', + scan_id, url, target_name, title, random.choice([200, 201, 401, 403, 404]), random.randint(100, 5000), '', # location 'nginx/1.24.0', - 'application/json', ['REST', 'JSON'], + 'application/json', tech_list, '{"status":"ok","data":{}}', - [] # matched_gf_patterns + tags )) count += 1 @@ -1289,7 +1546,7 @@ class TestDataGenerator: count = 0 batch_data = [] - for scan_id in scan_ids[:100]: + for scan_id in scan_ids: # 为所有扫描创建快照 cur.execute(""" SELECT t.name FROM scan s JOIN target t ON s.target_id = t.id @@ -1300,8 +1557,9 @@ class TestDataGenerator: continue target_name = row[0] - for d in random.sample(dirs, min(random.randint(30, 50), len(dirs))): - url = f'https://{target_name}{d}' + for idx, d in enumerate(random.sample(dirs, min(random.randint(50, 80), len(dirs)))): + # 生成固定 245 长度的 URL + url = generate_fixed_length_url(target_name, length=245, path_hint=f'dir-snap/{idx:04d}') batch_data.append(( scan_id, url, random.choice([200, 301, 403]), random.randint(500, 10000), random.randint(50, 500), @@ -1338,7 +1596,7 @@ class TestDataGenerator: count = 0 batch_data = [] - for scan_id in scan_ids[:100]: + for scan_id in scan_ids: # 为所有扫描创建快照 cur.execute(""" SELECT t.name FROM scan s JOIN target t ON s.target_id = t.id @@ -1350,10 +1608,10 @@ class TestDataGenerator: target_name = row[0] # 生成多个随机 IP - for _ in range(random.randint(8, 15)): + for _ in range(random.randint(10, 20)): ip = f'192.168.{random.randint(1, 254)}.{random.randint(1, 254)}' - for port in random.sample(common_ports, min(random.randint(15, 30), len(common_ports))): + for port in random.sample(common_ports, min(random.randint(20, 35), len(common_ports))): batch_data.append((scan_id, target_name, ip, port)) count += 1 @@ -1377,16 +1635,34 @@ class TestDataGenerator: print(" ⚠ 缺少扫描任务,跳过\n") return - vuln_types = ['xss', 'sqli', 'ssrf', 'lfi', 'rce', 'xxe', 'csrf', - 'idor', 'auth-bypass', 'info-disclosure', 'cors-misconfig', - 'open-redirect', 'command-injection', 'deserialization', - 'jwt-vulnerability', 'path-traversal', 'file-upload'] + vuln_types = [ + 'sql-injection-authentication-bypass-vulnerability-', + 'cross-site-scripting-xss-stored-persistent-attack-', + 'server-side-request-forgery-ssrf-internal-access--', + 'remote-code-execution-rce-command-injection-flaw--', + 'insecure-direct-object-reference-idor-access-ctrl-', + 'authentication-bypass-session-management-flaw-----', + 'cors-misconfiguration-cross-origin-data-leakage---', + 'command-injection-os-command-execution-exploit----', + 'deserialization-vulnerability-object-injection----', + 'jwt-vulnerability-token-forgery-authentication----', + 'open-redirect-url-redirection-phishing-attack-----', + 'path-traversal-arbitrary-file-read-access-vuln----', + ] severities = ['critical', 'high', 'medium', 'low', 'info'] - sources = ['nuclei', 'dalfox', 'sqlmap', 'burp-suite', 'zap', 'nmap', 'nikto'] + sources = [ + 'nuclei-vulnerability-scanner--', + 'dalfox-xss-parameter-analysis-', + 'sqlmap-sql-injection-testing--', + 'burp-suite-professional-scan--', + 'owasp-zap-security-scanner----', + 'nmap-network-service-scanner--', + 'nikto-web-server-scanner------', + ] count = 0 batch_data = [] - for scan_id in scan_ids[:100]: + for scan_id in scan_ids: # 为所有扫描创建快照 cur.execute(""" SELECT t.name FROM scan s JOIN target t ON s.target_id = t.id @@ -1397,7 +1673,7 @@ class TestDataGenerator: continue target_name = row[0] - for _ in range(random.randint(15, 40)): + for idx in range(random.randint(30, 60)): severity = random.choice(severities) cvss_ranges = { 'critical': (9.0, 10.0), 'high': (7.0, 8.9), 'medium': (4.0, 6.9), @@ -1406,12 +1682,16 @@ class TestDataGenerator: cvss_range = cvss_ranges.get(severity, (0.0, 10.0)) cvss_score = round(random.uniform(*cvss_range), 1) - url = f'https://{target_name}/api/v1/users?id={random.randint(1, 100)}' + # 生成固定 245 长度的 URL + url = generate_fixed_length_url(target_name, length=245, path_hint=f'vuln-snap/{idx:04d}') + + # 生成固定 300 长度的描述 + description = generate_fixed_length_text(length=300, text_type='description') batch_data.append(( scan_id, url, random.choice(vuln_types), severity, random.choice(sources), cvss_score, - f'Detected {severity} severity vulnerability', + description, json.dumps({'template': f'CVE-2024-{random.randint(10000, 99999)}'}) )) count += 1 @@ -1427,6 +1707,289 @@ class TestDataGenerator: print(f" ✓ 创建了 {count} 个漏洞快照\n") + def create_ehole_fingerprints(self): + """创建 EHole 指纹数据""" + print("🔍 创建 EHole 指纹...") + cur = self.conn.cursor() + + # CMS/产品名称模板(长名称) + cms_templates = [ + 'WordPress-Enterprise-Content-Management-System-Professional-Edition', + 'Drupal-Open-Source-CMS-Platform-Community-Extended-Version', + 'Joomla-Web-Content-Management-Framework-Business-Suite', + 'Magento-E-Commerce-Platform-Enterprise-Cloud-Edition', + 'Shopify-Online-Store-Builder-Professional-Business-Plan', + 'PrestaShop-E-Commerce-Solution-Multi-Store-Edition', + 'OpenCart-Shopping-Cart-System-Enterprise-Features', + 'WooCommerce-WordPress-E-Commerce-Plugin-Extended', + 'Laravel-PHP-Framework-Application-Boilerplate', + 'Django-Python-Web-Framework-Admin-Dashboard', + 'Spring-Boot-Java-Microservices-Framework-Starter', + 'Express-Node-JS-Web-Application-Framework-API', + 'Ruby-on-Rails-MVC-Framework-Application-Template', + 'ASP-NET-Core-Microsoft-Web-Framework-Enterprise', + 'Flask-Python-Micro-Framework-REST-API-Template', + 'FastAPI-Python-Modern-Web-Framework-OpenAPI', + 'Next-JS-React-Framework-Server-Side-Rendering', + 'Nuxt-JS-Vue-Framework-Universal-Application', + 'Angular-Universal-Server-Side-Rendering-Platform', + 'Svelte-Kit-Web-Application-Framework-Compiler', + 'Apache-Tomcat-Java-Servlet-Container-Server', + 'Nginx-Web-Server-Reverse-Proxy-Load-Balancer', + 'Microsoft-IIS-Internet-Information-Services-Server', + 'Apache-HTTP-Server-Web-Server-Platform', + 'Caddy-Web-Server-Automatic-HTTPS-Configuration', + 'LiteSpeed-Web-Server-High-Performance-HTTP', + 'Oracle-WebLogic-Server-Java-EE-Application', + 'IBM-WebSphere-Application-Server-Enterprise', + 'JBoss-EAP-Enterprise-Application-Platform-RedHat', + 'GlassFish-Server-Open-Source-Java-EE-Reference', + ] + + methods = ['keyword', 'faviconhash', 'regula'] + locations = ['body', 'header', 'title', 'server', 'cookie', 'cert'] + types = ['CMS', 'Framework', 'Server', 'Database', 'Cache', 'CDN', 'WAF', 'Load-Balancer', 'Container', 'Cloud'] + + # 关键词模板(多个长关键词) + keyword_templates = [ + ['wp-content/themes/', 'wp-includes/js/', 'wp-admin/css/', 'wordpress-hash-', 'wp-json/wp/v2/'], + ['sites/all/modules/', 'misc/drupal.js', 'drupal-settings-json', 'X-Drupal-Cache', 'X-Generator: Drupal'], + ['media/jui/js/', 'administrator/index.php', 'Joomla!', 'com_content', 'mod_custom'], + ['skin/frontend/', 'Mage.Cookies', 'MAGENTO_CACHE', 'varien/js.js', 'mage/cookies.js'], + ['cdn.shopify.com', 'Shopify.theme', 'shopify-section', 'shopify-payment-button', 'myshopify.com'], + ['prestashop', 'PrestaShop', 'ps_versions_compliancy', 'prestashop-page', 'id_product'], + ['catalog/view/theme/', 'index.php?route=', 'OpenCart', 'text_home', 'common/home'], + ['woocommerce', 'WooCommerce', 'wc-ajax', 'woocommerce-page', 'add_to_cart_button'], + ['laravel_session', 'XSRF-TOKEN', 'Laravel', 'laravel-livewire', 'laravel_token'], + ['csrfmiddlewaretoken', 'django.contrib', 'Django', '__admin_media_prefix__', 'django-debug-toolbar'], + ['X-Application-Context', 'spring-boot', 'Spring', 'actuator/health', 'spring-security'], + ['X-Powered-By: Express', 'express-session', 'connect.sid', 'express.static', 'express-validator'], + ['X-Powered-By: Phusion', 'Rails', 'csrf-token', 'action_controller', 'rails-ujs'], + ['X-AspNet-Version', 'ASP.NET', '__VIEWSTATE', '__EVENTVALIDATION', 'aspnetcore-'], + ['Werkzeug', 'Flask', 'flask-login', 'flask-wtf', 'flask-session'], + ] + + count = 0 + batch_data = [] + + for i in range(200): # 生成 200 条 EHole 指纹 + cms = f'{random.choice(cms_templates)}-{random.randint(1000, 9999)}' + method = random.choice(methods) + location = random.choice(locations) + keywords = random.choice(keyword_templates) + [f'custom-keyword-{random.randint(10000, 99999)}' for _ in range(random.randint(3, 8))] + is_important = random.choice([True, False]) + fp_type = random.choice(types) + + batch_data.append(( + cms, method, location, json.dumps(keywords), is_important, fp_type + )) + count += 1 + + if batch_data: + execute_values(cur, """ + INSERT INTO ehole_fingerprint (cms, method, location, keyword, is_important, type, created_at) + VALUES %s + ON CONFLICT DO NOTHING + """, batch_data, template="(%s, %s, %s, %s, %s, %s, NOW())") + + print(f" ✓ 创建了 {count} 个 EHole 指纹\n") + + def create_goby_fingerprints(self): + """创建 Goby 指纹数据""" + print("🔍 创建 Goby 指纹...") + cur = self.conn.cursor() + + # 产品名称模板(长名称) + name_templates = [ + 'Apache-Tomcat-Java-Servlet-Container-Application-Server-Enterprise', + 'Nginx-High-Performance-Web-Server-Reverse-Proxy-Load-Balancer', + 'Microsoft-Exchange-Server-Email-Collaboration-Platform-Enterprise', + 'VMware-vCenter-Server-Virtual-Infrastructure-Management-Platform', + 'Cisco-Adaptive-Security-Appliance-Firewall-VPN-Concentrator', + 'Fortinet-FortiGate-Next-Generation-Firewall-Security-Platform', + 'Palo-Alto-Networks-Firewall-Threat-Prevention-Platform', + 'F5-BIG-IP-Application-Delivery-Controller-Load-Balancer', + 'Citrix-NetScaler-Application-Delivery-Controller-Gateway', + 'Juniper-Networks-SRX-Series-Services-Gateway-Firewall', + 'Oracle-WebLogic-Server-Java-Enterprise-Application-Platform', + 'IBM-WebSphere-Application-Server-Java-EE-Enterprise-Edition', + 'SAP-NetWeaver-Application-Server-Business-Suite-Platform', + 'Adobe-Experience-Manager-Content-Management-System-Enterprise', + 'Atlassian-Confluence-Team-Collaboration-Wiki-Platform-Server', + 'Atlassian-Jira-Project-Issue-Tracking-Software-Server-Edition', + 'GitLab-DevOps-Platform-Source-Code-Management-CI-CD-Pipeline', + 'Jenkins-Automation-Server-Continuous-Integration-Deployment', + 'SonarQube-Code-Quality-Security-Analysis-Platform-Enterprise', + 'Elasticsearch-Distributed-Search-Analytics-Engine-Cluster', + 'Kibana-Data-Visualization-Dashboard-Elasticsearch-Frontend', + 'Grafana-Observability-Platform-Metrics-Logs-Traces-Dashboard', + 'Prometheus-Monitoring-System-Time-Series-Database-Alerting', + 'Zabbix-Enterprise-Monitoring-Solution-Network-Server-Cloud', + 'Nagios-Infrastructure-Monitoring-Alerting-System-Enterprise', + 'Redis-In-Memory-Data-Structure-Store-Cache-Message-Broker', + 'MongoDB-Document-Database-NoSQL-Distributed-Cluster-Platform', + 'PostgreSQL-Advanced-Open-Source-Relational-Database-System', + 'MySQL-Enterprise-Relational-Database-Management-System-Server', + 'Microsoft-SQL-Server-Relational-Database-Management-Platform', + ] + + # 逻辑表达式模板 + logic_templates = [ + '(a&&b)||c', 'a||(b&&c)', '(a||b)&&(c||d)', 'a&&b&&c', 'a||b||c', + '((a&&b)||c)&&d', '(a||(b&&c))&&(d||e)', 'a&&(b||c)&&d', + '(a&&b&&c)||(d&&e)', '((a||b)&&c)||(d&&e&&f)', + ] + + # 规则模板 + rule_labels = ['body', 'header', 'title', 'server', 'cert', 'banner', 'protocol', 'port'] + + count = 0 + batch_data = [] + + for i in range(200): # 生成 200 条 Goby 指纹 + name = f'{random.choice(name_templates)}-{random.randint(1000, 9999)}' + logic = random.choice(logic_templates) + + # 生成 5-15 条规则 + num_rules = random.randint(5, 15) + rules = [] + for j in range(num_rules): + rule = { + 'label': random.choice(rule_labels), + 'feature': f'feature-pattern-{random.randint(10000, 99999)}-{random.choice(["regex", "keyword", "hash"])}', + 'is_equal': random.choice([True, False]) + } + rules.append(rule) + + batch_data.append((name, logic, json.dumps(rules))) + count += 1 + + if batch_data: + execute_values(cur, """ + INSERT INTO goby_fingerprint (name, logic, rule, created_at) + VALUES %s + ON CONFLICT DO NOTHING + """, batch_data, template="(%s, %s, %s, NOW())") + + print(f" ✓ 创建了 {count} 个 Goby 指纹\n") + + def create_wappalyzer_fingerprints(self): + """创建 Wappalyzer 指纹数据""" + print("🔍 创建 Wappalyzer 指纹...") + cur = self.conn.cursor() + + # 应用名称模板(长名称) + name_templates = [ + 'WordPress-Content-Management-System-Open-Source-Blogging-Platform', + 'React-JavaScript-Library-User-Interface-Components-Facebook', + 'Vue-JS-Progressive-JavaScript-Framework-Reactive-Components', + 'Angular-Platform-Web-Application-Framework-Google-TypeScript', + 'jQuery-JavaScript-Library-DOM-Manipulation-Event-Handling', + 'Bootstrap-CSS-Framework-Responsive-Design-Mobile-First', + 'Tailwind-CSS-Utility-First-Framework-Rapid-UI-Development', + 'Node-JS-JavaScript-Runtime-Server-Side-V8-Engine-Platform', + 'Express-JS-Web-Application-Framework-Node-JS-Middleware', + 'Django-Python-Web-Framework-Batteries-Included-MTV-Pattern', + 'Flask-Python-Micro-Framework-Lightweight-WSGI-Application', + 'Ruby-on-Rails-MVC-Framework-Convention-Over-Configuration', + 'Laravel-PHP-Framework-Elegant-Syntax-Expressive-Beautiful', + 'Spring-Framework-Java-Enterprise-Application-Development', + 'ASP-NET-Core-Cross-Platform-Web-Framework-Microsoft-Open', + 'Nginx-Web-Server-Reverse-Proxy-Load-Balancer-HTTP-Cache', + 'Apache-HTTP-Server-Web-Server-Cross-Platform-Open-Source', + 'Cloudflare-CDN-DDoS-Protection-Web-Application-Firewall', + 'Amazon-Web-Services-Cloud-Computing-Platform-Infrastructure', + 'Google-Cloud-Platform-Cloud-Computing-Services-Infrastructure', + 'Microsoft-Azure-Cloud-Computing-Service-Platform-Enterprise', + 'Docker-Container-Platform-Application-Deployment-Orchestration', + 'Kubernetes-Container-Orchestration-Platform-Cloud-Native', + 'Elasticsearch-Search-Analytics-Engine-Distributed-RESTful', + 'Redis-In-Memory-Data-Store-Cache-Message-Broker-Database', + 'MongoDB-Document-Database-NoSQL-Scalable-High-Performance', + 'PostgreSQL-Object-Relational-Database-System-Open-Source', + 'MySQL-Relational-Database-Management-System-Oracle-Open', + 'GraphQL-Query-Language-API-Runtime-Data-Fetching-Facebook', + 'Webpack-Module-Bundler-JavaScript-Asset-Pipeline-Build-Tool', + ] + + # 分类 ID + cats_options = [ + [1, 2, 3], [4, 5], [6, 7, 8, 9], [10, 11, 12], [13, 14, 15, 16], + [17, 18], [19, 20, 21], [22, 23, 24, 25], [26, 27], [28, 29, 30], + ] + + # 描述模板 + descriptions = [ + 'A powerful and flexible content management system designed for enterprise-level web applications with extensive plugin ecosystem and community support.', + 'Modern JavaScript framework for building interactive user interfaces with component-based architecture and virtual DOM for optimal performance.', + 'High-performance web server and reverse proxy with advanced load balancing, caching, and security features for production deployments.', + 'Comprehensive cloud computing platform providing infrastructure as a service, platform as a service, and software as a service solutions.', + 'Enterprise-grade database management system with ACID compliance, advanced security features, and horizontal scaling capabilities.', + 'Container orchestration platform for automating deployment, scaling, and management of containerized applications across clusters.', + 'Full-stack web application framework with built-in ORM, authentication, and admin interface for rapid development.', + 'Lightweight and modular CSS framework with utility classes for building responsive and customizable user interfaces.', + 'Real-time search and analytics engine with distributed architecture for handling large-scale data processing workloads.', + 'In-memory data structure store supporting various data types with persistence options and pub/sub messaging capabilities.', + ] + + count = 0 + batch_data = [] + + for i in range(200): # 生成 200 条 Wappalyzer 指纹 + name = f'{random.choice(name_templates)}-{random.randint(1000, 9999)}' + cats = random.choice(cats_options) + + # 生成 cookies 规则 + cookies = {} + for j in range(random.randint(2, 5)): + cookies[f'cookie_name_{j}'] = f'regex_pattern_{random.randint(1000, 9999)}' + + # 生成 headers 规则 + headers = {} + header_names = ['X-Powered-By', 'Server', 'X-Generator', 'X-Framework', 'X-Application'] + for h in random.sample(header_names, random.randint(2, 4)): + headers[h] = f'pattern_{random.randint(1000, 9999)}' + + # 生成 script_src 规则 + script_src = [f'/js/lib/framework-{random.randint(100, 999)}.min.js' for _ in range(random.randint(3, 8))] + + # 生成 js 变量规则 + js_vars = [f'window.Framework{random.randint(100, 999)}' for _ in range(random.randint(2, 6))] + + # 生成 implies 依赖 + implies = [f'Dependency-{random.randint(100, 999)}' for _ in range(random.randint(1, 4))] + + # 生成 meta 规则 + meta = {} + meta_names = ['generator', 'framework', 'application-name', 'author', 'description'] + for m in random.sample(meta_names, random.randint(2, 4)): + meta[m] = f'meta_pattern_{random.randint(1000, 9999)}' + + # 生成 html 规则 + html = [f'
' for _ in range(random.randint(3, 7))] + + description = random.choice(descriptions) + website = f'https://www.example-framework-{random.randint(1000, 9999)}.com' + cpe = f'cpe:/a:vendor:product:{random.randint(1, 10)}.{random.randint(0, 9)}.{random.randint(0, 9)}' + + batch_data.append(( + name, json.dumps(cats), json.dumps(cookies), json.dumps(headers), + json.dumps(script_src), json.dumps(js_vars), json.dumps(implies), + json.dumps(meta), json.dumps(html), description, website, cpe + )) + count += 1 + + if batch_data: + execute_values(cur, """ + INSERT INTO wappalyzer_fingerprint ( + name, cats, cookies, headers, script_src, js, implies, + meta, html, description, website, cpe, created_at + ) VALUES %s + ON CONFLICT DO NOTHING + """, batch_data, template="(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW())") + + print(f" ✓ 创建了 {count} 个 Wappalyzer 指纹\n") + class MillionDataGenerator: """ @@ -1597,9 +2160,8 @@ class MillionDataGenerator: for i in range(per_target): if count >= target_count: break - protocol = random.choice(['https', 'http']) - port = random.choice([80, 443, 8080, 8443, 3000]) - url = f'{protocol}://{target_name}:{port}/path-{i:04d}/' + # 生成固定 245 长度的 URL + url = generate_fixed_length_url(target_name, length=245, path_hint=f'million-website/{i:06d}') batch_data.append(( url, target_id, target_name, f'Website Title {count}', @@ -1638,6 +2200,30 @@ class MillionDataGenerator: paths = ['/api/v1/', '/api/v2/', '/admin/', '/portal/', '/graphql/', '/health/', '/metrics/'] + # 100字符长度的标题 + titles = [ + 'Enterprise API Gateway - RESTful Service Documentation with OpenAPI 3.0 Specification and Interactive', + 'User Authentication Service - OAuth 2.0 and SAML 2.0 Single Sign-On Integration Platform Dashboard', + 'Payment Processing Gateway - PCI-DSS Compliant Transaction Management System Administration Panel', + 'Content Delivery Network - Global Edge Cache Management and Real-time Analytics Dashboard Interface', + 'Database Administration Console - PostgreSQL Cluster Management with Automated Backup and Recovery', + ] + + # 扩展的技术栈列表 + all_techs = [ + 'React 18.2.0', 'Vue.js 3.4', 'Angular 17.1', 'Next.js 14.0', 'Node.js 20.10', + 'Express 4.18', 'Python 3.12', 'Django 5.0', 'FastAPI 0.109', 'Go 1.21', + 'PostgreSQL 16.1', 'MySQL 8.2', 'MongoDB 7.0', 'Redis 7.2', 'Elasticsearch 8.11', + 'Kubernetes 1.28', 'Docker 24.0', 'Nginx 1.25', 'GraphQL 16.8', 'JWT', + ] + + # 扩展的 tags + all_tags = [ + 'debug', 'config', 'api', 'json', 'upload', 'file', 'admin', 'auth', + 'secrets', 'credentials', 'backup', 'archive', 'trace', 'log', 'error', + 'security', 'vulnerability', 'payment', 'user', 'internal', 'private', + ] + cur.execute("SELECT id, name FROM target WHERE type = 'domain' AND deleted_at IS NULL") domain_targets = cur.fetchall() @@ -1651,14 +2237,25 @@ class MillionDataGenerator: for i in range(per_target): if count >= target_count: break - path = random.choice(paths) - url = f'https://{target_name}{path}endpoint-{i:04d}' + # 生成固定 245 长度的 URL + url = generate_fixed_length_url(target_name, length=245, path_hint=f'million-endpoint/{i:06d}') + + # 生成 100 字符的标题 + title = random.choice(titles) + + # 生成 10-20 个技术 + num_techs = random.randint(10, 20) + tech_list = random.sample(all_techs, min(num_techs, len(all_techs))) + + # 生成 10-20 个 tags + num_tags = random.randint(10, 20) + tags = random.sample(all_tags, min(num_tags, len(all_tags))) batch_data.append(( - url, target_id, target_name, 'API Endpoint', + url, target_id, target_name, title, 'nginx/1.24.0', random.choice([200, 201, 401, 403]), random.randint(100, 5000), 'application/json', - ['Node.js', 'Express'], '', '{"status":"ok"}', None, [] + tech_list, '', '{"status":"ok"}', None, tags )) count += 1 @@ -1737,8 +2334,23 @@ class MillionDataGenerator: print("🐛 创建漏洞 (200,000 个)...") cur = self.conn.cursor() - vuln_types = ['sql-injection', 'xss', 'ssrf', 'rce', 'lfi', 'xxe', 'csrf', 'idor'] - sources = ['nuclei', 'dalfox', 'sqlmap', 'burp-suite', 'zap'] + vuln_types = [ + 'sql-injection-authentication-bypass-vulnerability-', + 'cross-site-scripting-xss-stored-persistent-attack-', + 'server-side-request-forgery-ssrf-internal-access--', + 'remote-code-execution-rce-command-injection-flaw--', + 'local-file-inclusion-lfi-path-traversal-exploit---', + 'xml-external-entity-xxe-injection-vulnerability---', + 'cross-site-request-forgery-csrf-token-validation--', + 'insecure-direct-object-reference-idor-access-ctrl-', + ] + sources = [ + 'nuclei-vulnerability-scanner--', + 'dalfox-xss-parameter-analysis-', + 'sqlmap-sql-injection-testing--', + 'burp-suite-professional-scan--', + 'owasp-zap-security-scanner----', + ] # 按严重程度分配数量 severity_counts = { @@ -1773,12 +2385,16 @@ class MillionDataGenerator: break cvss_score = round(random.uniform(*cvss_range), 1) - url = f'https://{target_name}/api/v1/vuln-{severity_count:06d}' + # 生成固定 245 长度的 URL + url = generate_fixed_length_url(target_name, length=245, path_hint=f'million-vuln/{severity_count:06d}') + + # 生成固定 300 长度的描述 + description = generate_fixed_length_text(length=300, text_type='description') batch_data.append(( target_id, url, random.choice(vuln_types), severity, random.choice(sources), cvss_score, - f'{severity.upper()} vulnerability detected', + description, json.dumps({'template': f'CVE-2024-{random.randint(10000, 99999)}'}) )) severity_count += 1 diff --git a/frontend/app/tools/nuclei/page.tsx b/frontend/app/tools/nuclei/page.tsx index 729b0ae0..c17a3d7b 100644 --- a/frontend/app/tools/nuclei/page.tsx +++ b/frontend/app/tools/nuclei/page.tsx @@ -2,7 +2,7 @@ import Link from "next/link" import { useState, useMemo, type FormEvent } from "react" -import { GitBranch, Search, RefreshCw, Settings, Trash2, FolderOpen } from "lucide-react" +import { GitBranch, Search, RefreshCw, Settings, Trash2, FolderOpen, Plus } from "lucide-react" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Input } from "@/components/ui/input" @@ -173,7 +173,8 @@ export default function NucleiReposPage() {
diff --git a/frontend/components/app-sidebar.tsx b/frontend/components/app-sidebar.tsx index e0030e31..8964de81 100644 --- a/frontend/components/app-sidebar.tsx +++ b/frontend/components/app-sidebar.tsx @@ -229,18 +229,22 @@ export function AppSidebar({ ...props }: React.ComponentProps) { - {item.items?.map((subItem) => ( - - - - {subItem.title} - - - - ))} + {item.items?.map((subItem) => { + const subUrl = normalize(subItem.url) + const isSubActive = current === subUrl || current.startsWith(subUrl + "/") + return ( + + + + {subItem.title} + + + + ) + })} diff --git a/frontend/components/directories/directories-columns.tsx b/frontend/components/directories/directories-columns.tsx index 152c1902..dac7b978 100644 --- a/frontend/components/directories/directories-columns.tsx +++ b/frontend/components/directories/directories-columns.tsx @@ -5,7 +5,7 @@ import { ColumnDef } from "@tanstack/react-table" import { Checkbox } from "@/components/ui/checkbox" import { Badge } from "@/components/ui/badge" import { DataTableColumnHeader } from "@/components/ui/data-table/column-header" -import { TruncatedUrlCell } from "@/components/ui/truncated-cell" +import { ExpandableCell } from "@/components/ui/data-table/expandable-cell" import type { Directory } from "@/types/directory.types" /** @@ -88,10 +88,9 @@ export function createDirectoryColumns({ header: ({ column }) => ( ), - cell: ({ row }) => { - const url = row.getValue("url") as string - return - }, + cell: ({ row }) => ( + + ), }, // Status 列 { @@ -99,6 +98,7 @@ export function createDirectoryColumns({ size: 80, minSize: 60, maxSize: 120, + enableResizing: false, meta: { title: "Status" }, header: ({ column }) => ( @@ -156,6 +156,7 @@ export function createDirectoryColumns({ size: 120, minSize: 80, maxSize: 200, + enableResizing: false, meta: { title: "Content Type" }, header: ({ column }) => ( @@ -190,6 +191,7 @@ export function createDirectoryColumns({ size: 150, minSize: 120, maxSize: 200, + enableResizing: false, meta: { title: "Created At" }, header: ({ column }) => ( diff --git a/frontend/components/directories/directories-data-table.tsx b/frontend/components/directories/directories-data-table.tsx index 6d98514f..ed85d6e6 100644 --- a/frontend/components/directories/directories-data-table.tsx +++ b/frontend/components/directories/directories-data-table.tsx @@ -2,8 +2,6 @@ import * as React from "react" import type { ColumnDef } from "@tanstack/react-table" -import { IconPlus } from "@tabler/icons-react" -import { Button } from "@/components/ui/button" import { UnifiedDataTable } from "@/components/ui/data-table" import type { FilterField } from "@/components/common/smart-filter-input" import type { Directory } from "@/types/directory.types" @@ -114,19 +112,13 @@ export function DirectoriesDataTable({ onBulkDelete={onBulkDelete} bulkDeleteLabel="Delete" showAddButton={false} + // 批量添加按钮 + onBulkAdd={onBulkAdd} + bulkAddLabel="批量添加" // 下载 downloadOptions={downloadOptions.length > 0 ? downloadOptions : undefined} // 空状态 emptyMessage="暂无数据" - // 自定义工具栏按钮 - toolbarRight={ - onBulkAdd ? ( - - ) : undefined - } /> ) } diff --git a/frontend/components/endpoints/endpoints-columns.tsx b/frontend/components/endpoints/endpoints-columns.tsx index 5ce0f81a..166e657f 100644 --- a/frontend/components/endpoints/endpoints-columns.tsx +++ b/frontend/components/endpoints/endpoints-columns.tsx @@ -2,12 +2,11 @@ import React from "react" import { ColumnDef } from "@tanstack/react-table" -import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Checkbox } from "@/components/ui/checkbox" import { DataTableColumnHeader } from "@/components/ui/data-table/column-header" import type { Endpoint } from "@/types/endpoint.types" -import { TruncatedCell, TruncatedUrlCell } from "@/components/ui/truncated-cell" +import { ExpandableCell, ExpandableTagList } from "@/components/ui/data-table/expandable-cell" interface CreateColumnsProps { formatDate: (dateString: string) => string @@ -45,37 +44,6 @@ function HttpStatusBadge({ statusCode }: { statusCode: number | null | undefined ) } -/** - * Body Preview 单元格组件 - 最多显示3行,超出折叠,点击展开查看完整内容 - */ -function BodyPreviewCell({ value }: { value: string | null | undefined }) { - const [expanded, setExpanded] = React.useState(false) - - if (!value) { - return - - } - - return ( -
-
setExpanded(!expanded)} - title={expanded ? "点击收起" : "点击展开"} - > - {value} -
- {value.length > 100 && ( - - )} -
- ) -} - export function createEndpointColumns({ formatDate, }: CreateColumnsProps): ColumnDef[] { @@ -115,10 +83,9 @@ export function createEndpointColumns({ size: 400, minSize: 200, maxSize: 700, - cell: ({ row }) => { - const url = row.getValue("url") as string - return - }, + cell: ({ row }) => ( + + ), }, { accessorKey: "title", @@ -130,7 +97,7 @@ export function createEndpointColumns({ minSize: 100, maxSize: 300, cell: ({ row }) => ( - + ), }, { @@ -174,7 +141,7 @@ export function createEndpointColumns({ minSize: 100, maxSize: 300, cell: ({ row }) => ( - + ), }, { @@ -187,7 +154,7 @@ export function createEndpointColumns({ minSize: 80, maxSize: 200, cell: ({ row }) => ( - + ), }, { @@ -200,7 +167,7 @@ export function createEndpointColumns({ minSize: 80, maxSize: 200, cell: ({ row }) => ( - + ), }, { @@ -213,16 +180,12 @@ export function createEndpointColumns({ minSize: 150, cell: ({ row }) => { const tech = (row.getValue("tech") as string[] | null | undefined) || [] - if (!tech.length) return - - return ( -
- {tech.map((t, index) => ( - - {t} - - ))} -
+ ) }, }, @@ -235,7 +198,7 @@ export function createEndpointColumns({ size: 350, minSize: 250, cell: ({ row }) => ( - + ), }, { @@ -264,21 +227,12 @@ export function createEndpointColumns({ maxSize: 250, cell: ({ row }) => { const tags = (row.getValue("tags") as string[] | null | undefined) || [] - if (!tags.length) { - return - - } return ( -
- {tags.map((tag, idx) => ( - - {tag} - - ))} -
+ ) }, enableSorting: false, diff --git a/frontend/components/endpoints/endpoints-data-table.tsx b/frontend/components/endpoints/endpoints-data-table.tsx index e6896400..cc5a7612 100644 --- a/frontend/components/endpoints/endpoints-data-table.tsx +++ b/frontend/components/endpoints/endpoints-data-table.tsx @@ -2,8 +2,6 @@ import * as React from "react" import type { ColumnDef } from "@tanstack/react-table" -import { IconPlus } from "@tabler/icons-react" -import { Button } from "@/components/ui/button" import { UnifiedDataTable } from "@/components/ui/data-table" import type { FilterField } from "@/components/common/smart-filter-input" import type { DownloadOption, PaginationState } from "@/types/data-table.types" @@ -109,14 +107,6 @@ export function EndpointsDataTable - - 批量添加 - - ) : undefined - return ( 0 ? downloadOptions : undefined} // 空状态 emptyMessage="No results" - // 自定义工具栏按钮 - toolbarRight={toolbarRightContent} /> ) } diff --git a/frontend/components/fingerprints/ehole-fingerprint-columns.tsx b/frontend/components/fingerprints/ehole-fingerprint-columns.tsx index a7c96220..d04a202c 100644 --- a/frontend/components/fingerprints/ehole-fingerprint-columns.tsx +++ b/frontend/components/fingerprints/ehole-fingerprint-columns.tsx @@ -1,5 +1,6 @@ "use client" +import React from "react" import { ColumnDef } from "@tanstack/react-table" import { Checkbox } from "@/components/ui/checkbox" import { Badge } from "@/components/ui/badge" @@ -10,6 +11,38 @@ interface ColumnOptions { formatDate: (date: string) => string } +/** + * 关键词列表单元格 - 默认显示3个,超出可展开 + */ +function KeywordListCell({ keywords }: { keywords: string[] }) { + const [expanded, setExpanded] = React.useState(false) + + if (!keywords || keywords.length === 0) return - + + const displayKeywords = expanded ? keywords : keywords.slice(0, 3) + const hasMore = keywords.length > 3 + + return ( +
+
+ {displayKeywords.map((kw, idx) => ( +
+ {kw} +
+ ))} +
+ {hasMore && ( + + )} +
+ ) +} + /** * 创建 EHole 指纹表格列定义 */ @@ -70,8 +103,8 @@ export function createEholeFingerprintColumns({ ) }, - enableResizing: true, - size: 100, + enableResizing: false, + size: 120, }, // 匹配位置 { @@ -88,7 +121,7 @@ export function createEholeFingerprintColumns({ ) }, - enableResizing: true, + enableResizing: false, size: 100, }, // 关键词 @@ -98,17 +131,7 @@ export function createEholeFingerprintColumns({ header: ({ column }) => ( ), - cell: ({ row }) => { - const keywords = row.getValue("keyword") as string[] - if (!keywords || keywords.length === 0) return "-" - return ( -
- {keywords.map((kw, idx) => ( -
{kw}
- ))} -
- ) - }, + cell: ({ row }) => , enableResizing: true, size: 300, }, @@ -124,7 +147,7 @@ export function createEholeFingerprintColumns({ if (!type || type === "-") return "-" return {type} }, - enableResizing: true, + enableResizing: false, size: 100, }, // 重点资产 @@ -135,13 +158,11 @@ export function createEholeFingerprintColumns({ ), cell: ({ row }) => { - const isImportant = row.getValue("isImportant") as boolean - return isImportant ? ( - 重点 - ) : null + const isImportant = row.getValue("isImportant") + return {String(isImportant)} }, - enableResizing: true, - size: 80, + enableResizing: false, + size: 100, }, // 创建时间 { @@ -158,7 +179,7 @@ export function createEholeFingerprintColumns({ ) }, - enableResizing: true, + enableResizing: false, size: 160, }, ] diff --git a/frontend/components/fingerprints/goby-fingerprint-columns.tsx b/frontend/components/fingerprints/goby-fingerprint-columns.tsx index fbc27bd8..87690c87 100644 --- a/frontend/components/fingerprints/goby-fingerprint-columns.tsx +++ b/frontend/components/fingerprints/goby-fingerprint-columns.tsx @@ -12,26 +12,22 @@ interface ColumnOptions { } /** - * 规则详情单元格组件 - 默认显示3条,超出可展开 + * 规则详情单元格组件 - 显示原始 JSON 数据 */ function RuleDetailsCell({ rules }: { rules: any[] }) { const [expanded, setExpanded] = React.useState(false) if (!rules || rules.length === 0) return - - const displayRules = expanded ? rules : rules.slice(0, 3) - const hasMore = rules.length > 3 + const displayRules = expanded ? rules : rules.slice(0, 2) + const hasMore = rules.length > 2 return (
-
hasMore && setExpanded(!expanded)} - title={hasMore ? (expanded ? "点击收起" : "点击展开") : undefined} - > +
{displayRules.map((r, idx) => (
- {r.label}: {r.feature} + {JSON.stringify(r)}
))}
@@ -101,14 +97,10 @@ export function createGobyFingerprintColumns({ ), cell: ({ row }) => { const logic = row.getValue("logic") as string - return ( - - {logic} - - ) + return {logic} }, - enableResizing: true, - size: 120, + enableResizing: false, + size: 100, }, // 规则数量 { @@ -119,14 +111,10 @@ export function createGobyFingerprintColumns({ ), cell: ({ row }) => { const rules = row.getValue("rule") as any[] - return ( - - {rules?.length || 0} 条规则 - - ) + return {rules?.length || 0} }, - enableResizing: true, - size: 100, + enableResizing: false, + size: 80, }, // 规则详情 { @@ -154,7 +142,7 @@ export function createGobyFingerprintColumns({
) }, - enableResizing: true, + enableResizing: false, size: 160, }, ] diff --git a/frontend/components/fingerprints/wappalyzer-fingerprint-columns.tsx b/frontend/components/fingerprints/wappalyzer-fingerprint-columns.tsx index eb45ecbf..dc8a88ee 100644 --- a/frontend/components/fingerprints/wappalyzer-fingerprint-columns.tsx +++ b/frontend/components/fingerprints/wappalyzer-fingerprint-columns.tsx @@ -1,10 +1,10 @@ "use client" -import { useState } from "react" +import React from "react" import { ColumnDef } from "@tanstack/react-table" import { Checkbox } from "@/components/ui/checkbox" -import { Badge } from "@/components/ui/badge" import { DataTableColumnHeader } from "@/components/ui/data-table/column-header" +import { ExpandableCell } from "@/components/ui/data-table/expandable-cell" import type { WappalyzerFingerprint } from "@/types/fingerprint.types" interface ColumnOptions { @@ -12,25 +12,24 @@ interface ColumnOptions { } /** - * 可展开文本单元格组件 + * JSON 对象展示单元格 - 显示原始 JSON */ -function ExpandableTextCell({ value, maxLength = 80 }: { value: string | null | undefined; maxLength?: number }) { - const [expanded, setExpanded] = useState(false) +function JsonCell({ data }: { data: any }) { + const [expanded, setExpanded] = React.useState(false) - if (!value) return - + if (!data || (typeof data === 'object' && Object.keys(data).length === 0)) { + return - + } - const needsExpand = value.length > maxLength + const jsonStr = JSON.stringify(data) + const isLong = jsonStr.length > 50 return ( -
-
needsExpand && setExpanded(!expanded)} - title={needsExpand ? (expanded ? "点击收起" : "点击展开") : undefined} - > - {value} +
+
+ {expanded ? JSON.stringify(data, null, 2) : jsonStr}
- {needsExpand && ( + {isLong && (
) }, - enableResizing: true, + enableResizing: false, size: 160, }, ] diff --git a/frontend/components/ip-addresses/ip-addresses-columns.tsx b/frontend/components/ip-addresses/ip-addresses-columns.tsx index df7d2d56..61c84a86 100644 --- a/frontend/components/ip-addresses/ip-addresses-columns.tsx +++ b/frontend/components/ip-addresses/ip-addresses-columns.tsx @@ -11,7 +11,7 @@ import { } from "@/components/ui/popover" import { DataTableColumnHeader } from "@/components/ui/data-table/column-header" import type { IPAddress } from "@/types/ip-address.types" -import { TruncatedCell } from "@/components/ui/truncated-cell" +import { ExpandableCell } from "@/components/ui/data-table/expandable-cell" export function createIPAddressColumns(params: { formatDate: (value: string) => string @@ -57,7 +57,7 @@ export function createIPAddressColumns(params: { ), cell: ({ row }) => ( - + ), }, // host 列 @@ -83,7 +83,7 @@ export function createIPAddressColumns(params: { return (
{displayHosts.map((host, index) => ( - + ))} {hasMore && ( @@ -97,7 +97,7 @@ export function createIPAddressColumns(params: {

All Hosts ({hosts.length})

{hosts.map((host, index) => ( - + {host} ))} @@ -116,6 +116,7 @@ export function createIPAddressColumns(params: { size: 150, minSize: 120, maxSize: 200, + enableResizing: false, meta: { title: "Created At" }, header: ({ column }) => ( diff --git a/frontend/components/organization/organization-columns.tsx b/frontend/components/organization/organization-columns.tsx index d7493a8d..e21b56bb 100644 --- a/frontend/components/organization/organization-columns.tsx +++ b/frontend/components/organization/organization-columns.tsx @@ -15,6 +15,7 @@ import { // 导入图标组件 import { MoreHorizontal, Play, Calendar, Edit, Trash2, Eye } from "lucide-react" import { DataTableColumnHeader } from "@/components/ui/data-table/column-header" +import { ExpandableCell } from "@/components/ui/data-table/expandable-cell" import { Tooltip, TooltipContent, @@ -161,21 +162,9 @@ export const createOrganizationColumns = ({ header: ({ column }) => ( ), - cell: ({ row }) => { - const description = row.getValue("description") as string - - if (!description) { - return - - } - - return ( -
- - {description} - -
- ) - }, + cell: ({ row }) => ( + + ), }, // Total Targets 列 diff --git a/frontend/components/scan/engine/engine-columns.tsx b/frontend/components/scan/engine/engine-columns.tsx index ac80bede..1f8b1efe 100644 --- a/frontend/components/scan/engine/engine-columns.tsx +++ b/frontend/components/scan/engine/engine-columns.tsx @@ -172,6 +172,7 @@ export const createEngineColumns = ({ size: 80, minSize: 60, maxSize: 100, + enableResizing: false, cell: ({ row }) => { const features = parseEngineFeatures(row.original) return @@ -189,6 +190,7 @@ export const createEngineColumns = ({ size: 80, minSize: 60, maxSize: 100, + enableResizing: false, cell: ({ row }) => { const features = parseEngineFeatures(row.original) return @@ -206,6 +208,7 @@ export const createEngineColumns = ({ size: 80, minSize: 60, maxSize: 100, + enableResizing: false, cell: ({ row }) => { const features = parseEngineFeatures(row.original) return @@ -223,6 +226,7 @@ export const createEngineColumns = ({ size: 80, minSize: 60, maxSize: 100, + enableResizing: false, cell: ({ row }) => { const features = parseEngineFeatures(row.original) return @@ -240,6 +244,7 @@ export const createEngineColumns = ({ size: 80, minSize: 60, maxSize: 100, + enableResizing: false, cell: ({ row }) => { const features = parseEngineFeatures(row.original) return @@ -257,6 +262,7 @@ export const createEngineColumns = ({ size: 80, minSize: 60, maxSize: 100, + enableResizing: false, cell: ({ row }) => { const features = parseEngineFeatures(row.original) return @@ -274,6 +280,7 @@ export const createEngineColumns = ({ size: 80, minSize: 60, maxSize: 100, + enableResizing: false, cell: ({ row }) => { const features = parseEngineFeatures(row.original) return @@ -291,6 +298,7 @@ export const createEngineColumns = ({ size: 80, minSize: 60, maxSize: 100, + enableResizing: false, cell: ({ row }) => { const features = parseEngineFeatures(row.original) return @@ -308,6 +316,7 @@ export const createEngineColumns = ({ size: 80, minSize: 60, maxSize: 100, + enableResizing: false, cell: ({ row }) => { const features = parseEngineFeatures(row.original) return diff --git a/frontend/components/scan/history/scan-history-columns.tsx b/frontend/components/scan/history/scan-history-columns.tsx index 4f6b5965..d0837452 100644 --- a/frontend/components/scan/history/scan-history-columns.tsx +++ b/frontend/components/scan/history/scan-history-columns.tsx @@ -370,6 +370,7 @@ export const createScanHistoryColumns = ({ size: 120, minSize: 80, maxSize: 180, + enableResizing: false, meta: { title: "Engine Name" }, header: ({ column }) => ( @@ -390,6 +391,7 @@ export const createScanHistoryColumns = ({ size: 150, minSize: 120, maxSize: 200, + enableResizing: false, meta: { title: "Created At" }, header: ({ column }) => ( @@ -407,9 +409,10 @@ export const createScanHistoryColumns = ({ // Status 列 { accessorKey: "status", - size: 100, - minSize: 80, + size: 110, + minSize: 90, maxSize: 130, + enableResizing: false, meta: { title: "Status" }, header: ({ column }) => ( diff --git a/frontend/components/subdomains/subdomains-columns.tsx b/frontend/components/subdomains/subdomains-columns.tsx index 861b5e79..810eb97c 100644 --- a/frontend/components/subdomains/subdomains-columns.tsx +++ b/frontend/components/subdomains/subdomains-columns.tsx @@ -1,9 +1,9 @@ "use client" -import React from "react" import { ColumnDef } from "@tanstack/react-table" import { Checkbox } from "@/components/ui/checkbox" import { DataTableColumnHeader } from "@/components/ui/data-table/column-header" +import { ExpandableCell } from "@/components/ui/data-table/expandable-cell" import type { Subdomain } from "@/types/subdomain.types" // 列创建函数的参数类型 @@ -54,16 +54,9 @@ export const createSubdomainColumns = ({ header: ({ column }) => ( ), - cell: ({ row }) => { - const name = row.getValue("name") as string - return ( -
- - {name} - -
- ) - }, + cell: ({ row }) => ( + + ), }, // 创建时间列 @@ -72,6 +65,7 @@ export const createSubdomainColumns = ({ size: 150, minSize: 120, maxSize: 200, + enableResizing: false, meta: { title: "Created At" }, header: ({ column }) => ( diff --git a/frontend/components/subdomains/subdomains-data-table.tsx b/frontend/components/subdomains/subdomains-data-table.tsx index 52fbdbb4..c5b36c3e 100644 --- a/frontend/components/subdomains/subdomains-data-table.tsx +++ b/frontend/components/subdomains/subdomains-data-table.tsx @@ -2,8 +2,6 @@ import * as React from "react" import type { ColumnDef } from "@tanstack/react-table" -import { IconPlus } from "@tabler/icons-react" -import { Button } from "@/components/ui/button" import { UnifiedDataTable } from "@/components/ui/data-table" import type { FilterField } from "@/components/common/smart-filter-input" import type { Subdomain } from "@/types/subdomain.types" @@ -101,18 +99,6 @@ export function SubdomainsDataTable({ }) } - // 自定义工具栏右侧按钮 - const toolbarRightContent = ( - <> - {onBulkAdd && ( - - )} - - ) - return ( 0 ? downloadOptions : undefined} // 空状态 emptyMessage="No results" - // 自定义工具栏按钮 - toolbarRight={onBulkAdd ? toolbarRightContent : undefined} /> ) } diff --git a/frontend/components/target/all-targets-columns.tsx b/frontend/components/target/all-targets-columns.tsx index ed95471a..49628981 100644 --- a/frontend/components/target/all-targets-columns.tsx +++ b/frontend/components/target/all-targets-columns.tsx @@ -18,9 +18,9 @@ import { TooltipTrigger, } from "@/components/ui/tooltip" import { MoreHorizontal, Eye, Trash2, Play, Calendar, Copy, Check } from "lucide-react" -import { Badge } from "@/components/ui/badge" import { toast } from "sonner" import { DataTableColumnHeader } from "@/components/ui/data-table/column-header" +import { ExpandableBadgeList } from "@/components/ui/data-table/expandable-cell" import type { Target } from "@/types/target.types" /** @@ -278,54 +278,12 @@ export const createAllTargetsColumns = ({ ), cell: ({ row }) => { const organizations = row.getValue("organizations") as Array<{ id: number; name: string }> | undefined - if (!organizations || organizations.length === 0) { - return - - } - - const displayOrgs = organizations.slice(0, 2) - const remainingCount = organizations.length - 2 - return ( -
- {displayOrgs.map((org) => ( - - {org.name} - - ))} - {remainingCount > 0 && ( - - - - - +{remainingCount} - - - -
- {organizations.slice(2).map((org) => ( -
- {org.name} -
- ))} -
-
-
-
- )} -
+ ) }, enableSorting: false, diff --git a/frontend/components/target/targets-data-table.tsx b/frontend/components/target/targets-data-table.tsx index 5c260184..1243ed1a 100644 --- a/frontend/components/target/targets-data-table.tsx +++ b/frontend/components/target/targets-data-table.tsx @@ -93,13 +93,6 @@ export function TargetsDataTable({ pageSize: pagination.pageSize, } : undefined - // 自定义添加按钮(支持 onAddHover) - const addButton = onAddNew ? ( - - ) : undefined - return (
} - toolbarRight={addButton} /> ) } diff --git a/frontend/components/tools/commands/commands-columns.tsx b/frontend/components/tools/commands/commands-columns.tsx index 64313892..60fb6656 100644 --- a/frontend/components/tools/commands/commands-columns.tsx +++ b/frontend/components/tools/commands/commands-columns.tsx @@ -14,6 +14,7 @@ import { } from "@/components/ui/dropdown-menu" import { MoreHorizontal, Eye, Trash2, Copy } from "lucide-react" import { DataTableColumnHeader } from "@/components/ui/data-table/column-header" +import { ExpandableCell } from "@/components/ui/data-table/expandable-cell" import { Badge } from "@/components/ui/badge" import { formatDate } from "@/lib/utils" import { toast } from "sonner" @@ -110,18 +111,9 @@ export const commandColumns: ColumnDef[] = [ header: ({ column }) => ( ), - cell: ({ row }) => { - const template = row.getValue("commandTemplate") as string - if (!template) return - - - return ( -
- - {template} - -
- ) - }, + cell: ({ row }) => ( + + ), }, // 描述列 @@ -133,18 +125,9 @@ export const commandColumns: ColumnDef[] = [ header: ({ column }) => ( ), - cell: ({ row }) => { - const description = row.getValue("description") as string - if (!description) return - - - return ( -
- - {description} - -
- ) - }, + cell: ({ row }) => ( + + ), }, // 更新时间列 diff --git a/frontend/components/ui/data-table/expandable-cell.tsx b/frontend/components/ui/data-table/expandable-cell.tsx new file mode 100644 index 00000000..cbac6ae6 --- /dev/null +++ b/frontend/components/ui/data-table/expandable-cell.tsx @@ -0,0 +1,280 @@ +"use client" + +import * as React from "react" +import { cn } from "@/lib/utils" +import { Badge } from "@/components/ui/badge" +import { ChevronDown, ChevronUp } from "lucide-react" + +export interface ExpandableCellProps { + /** 要显示的值 */ + value: string | null | undefined + /** 显示变体 */ + variant?: "text" | "url" | "mono" | "muted" + /** 最大显示行数,默认 3 */ + maxLines?: number + /** 额外的 CSS 类名 */ + className?: string + /** 空值时显示的占位符 */ + placeholder?: string + /** 展开按钮文本 */ + expandLabel?: string + /** 收起按钮文本 */ + collapseLabel?: string +} + +/** + * 统一的可展开单元格组件 + * + * 特性: + * - 默认显示最多 3 行(可配置) + * - 自动检测内容是否溢出 + * - 只在内容超出时显示展开/收起按钮 + * - 支持 text、url、mono、muted 四种变体 + */ +export function ExpandableCell({ + value, + variant = "text", + maxLines = 3, + className, + placeholder = "-", + expandLabel = "展开", + collapseLabel = "收起", +}: ExpandableCellProps) { + const [expanded, setExpanded] = React.useState(false) + const [isOverflowing, setIsOverflowing] = React.useState(false) + const contentRef = React.useRef(null) + + // 检测内容是否溢出 + React.useEffect(() => { + const el = contentRef.current + if (!el) return + + const checkOverflow = () => { + // 比较 scrollHeight 和 clientHeight 来判断是否溢出 + setIsOverflowing(el.scrollHeight > el.clientHeight + 1) + } + + checkOverflow() + + // 监听窗口大小变化 + const resizeObserver = new ResizeObserver(checkOverflow) + resizeObserver.observe(el) + + return () => resizeObserver.disconnect() + }, [value, expanded]) + + if (!value) { + return {placeholder} + } + + const lineClampClass = { + 1: "line-clamp-1", + 2: "line-clamp-2", + 3: "line-clamp-3", + 4: "line-clamp-4", + 5: "line-clamp-5", + 6: "line-clamp-6", + }[maxLines] || "line-clamp-3" + + return ( +
+
+ {value} +
+ {(isOverflowing || expanded) && ( + + )} +
+ ) +} + +/** + * URL 专用的可展开单元格 + */ +export function ExpandableUrlCell(props: Omit) { + return +} + +/** + * 代码/等宽字体的可展开单元格 + */ +export function ExpandableMonoCell(props: Omit) { + return +} + +// ============================================================================ +// Badge 列表相关组件 +// ============================================================================ + +export interface BadgeItem { + id: number | string + name: string +} + +export interface ExpandableBadgeListProps { + /** Badge 项目列表 */ + items: BadgeItem[] | null | undefined + /** 默认显示的数量,默认 2 */ + maxVisible?: number + /** Badge 变体 */ + variant?: "default" | "secondary" | "outline" | "destructive" + /** 空值时显示的占位符 */ + placeholder?: string + /** 额外的 CSS 类名 */ + className?: string + /** 点击 Badge 时的回调 */ + onItemClick?: (item: BadgeItem) => void +} + +/** + * 可展开的 Badge 列表组件 + * + * 特性: + * - 默认显示前 N 个 Badge(可配置) + * - 超过数量时显示展开按钮 + * - 点击展开按钮显示所有 Badge + * - 展开后显示收起按钮 + */ +export function ExpandableBadgeList({ + items, + maxVisible = 2, + variant = "secondary", + placeholder = "-", + className, + onItemClick, +}: ExpandableBadgeListProps) { + const [expanded, setExpanded] = React.useState(false) + + if (!items || items.length === 0) { + return {placeholder} + } + + const hasMore = items.length > maxVisible + const displayItems = expanded ? items : items.slice(0, maxVisible) + const remainingCount = items.length - maxVisible + + return ( +
+ {displayItems.map((item) => ( + onItemClick(item) : undefined} + > + {item.name} + + ))} + {hasMore && ( + + )} +
+ ) +} + +// ============================================================================ +// 字符串列表相关组件 +// ============================================================================ + +export interface ExpandableTagListProps { + /** 标签列表 */ + items: string[] | null | undefined + /** 默认显示的数量,默认 3 */ + maxVisible?: number + /** Badge 变体 */ + variant?: "default" | "secondary" | "outline" | "destructive" + /** 空值时显示的占位符 */ + placeholder?: string + /** 额外的 CSS 类名 */ + className?: string +} + +/** + * 可展开的标签列表组件(用于字符串数组) + * + * 适用于 tech 列表、tags 列表等场景 + */ +export function ExpandableTagList({ + items, + maxVisible = 3, + variant = "outline", + placeholder = "-", + className, +}: ExpandableTagListProps) { + const [expanded, setExpanded] = React.useState(false) + + if (!items || items.length === 0) { + return {placeholder} + } + + const hasMore = items.length > maxVisible + const displayItems = expanded ? items : items.slice(0, maxVisible) + const remainingCount = items.length - maxVisible + + return ( +
+ {displayItems.map((item, index) => ( + + {item} + + ))} + {hasMore && ( + + )} +
+ ) +} diff --git a/frontend/components/ui/data-table/unified-data-table.tsx b/frontend/components/ui/data-table/unified-data-table.tsx index 33550e63..46ec8429 100644 --- a/frontend/components/ui/data-table/unified-data-table.tsx +++ b/frontend/components/ui/data-table/unified-data-table.tsx @@ -113,9 +113,15 @@ export function UnifiedDataTable({ // 添加操作 onAddNew, + onAddHover, addButtonLabel = "Add", showAddButton = true, + // 批量添加操作 + onBulkAdd, + bulkAddLabel = "批量添加", + showBulkAdd = true, + // 下载操作 downloadOptions, @@ -436,11 +442,19 @@ export function UnifiedDataTable({ {/* 添加按钮 */} {showAddButton && onAddNew && ( - )} + + {/* 批量添加按钮 */} + {showBulkAdd && onBulkAdd && ( + + )} )} diff --git a/frontend/components/ui/truncated-cell.tsx b/frontend/components/ui/truncated-cell.tsx deleted file mode 100644 index 793723f2..00000000 --- a/frontend/components/ui/truncated-cell.tsx +++ /dev/null @@ -1,83 +0,0 @@ -"use client" - -import { cn } from "@/lib/utils" - -/** - * 预设的截断长度配置(保留用于兼容) - */ -export const TRUNCATE_LENGTHS = { - url: 50, - title: 25, - location: 20, - webServer: 20, - contentType: 20, - bodyPreview: 25, - subdomain: 35, - ip: 35, - host: 30, - default: 30, -} as const - -export type TruncateLengthKey = keyof typeof TRUNCATE_LENGTHS - -interface TruncatedCellProps { - /** 要显示的值 */ - value: string | null | undefined - /** 最大显示长度(已废弃,不再使用) */ - maxLength?: number | TruncateLengthKey - /** 额外的 CSS 类名 */ - className?: string - /** 是否使用等宽字体 */ - mono?: boolean - /** 空值时显示的占位符 */ - placeholder?: string -} - -/** - * 单元格组件 - 多行显示 - */ -export function TruncatedCell({ - value, - className, - mono = false, - placeholder = "-", -}: TruncatedCellProps) { - if (!value) { - return {placeholder} - } - - return ( -
- {value} -
- ) -} - -/** - * URL 专用的单元格 - 多行显示 - */ -export function TruncatedUrlCell({ - value, - className, -}: Omit) { - if (!value) { - return - - } - - return ( -
- {value} -
- ) -} diff --git a/frontend/components/vulnerabilities/vulnerabilities-columns.tsx b/frontend/components/vulnerabilities/vulnerabilities-columns.tsx index 40bd6868..73582165 100644 --- a/frontend/components/vulnerabilities/vulnerabilities-columns.tsx +++ b/frontend/components/vulnerabilities/vulnerabilities-columns.tsx @@ -6,7 +6,7 @@ import { Checkbox } from "@/components/ui/checkbox" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" -import { TruncatedUrlCell, TruncatedCell } from "@/components/ui/truncated-cell" +import { ExpandableUrlCell } from "@/components/ui/data-table/expandable-cell" import { DataTableColumnHeader } from "@/components/ui/data-table/column-header" import type { Vulnerability, VulnerabilitySeverity } from "@/types/vulnerability.types" @@ -62,9 +62,10 @@ export function createVulnerabilityColumns({ header: ({ column }) => ( ), - size: 80, - minSize: 60, + size: 100, + minSize: 80, maxSize: 120, + enableResizing: false, cell: ({ row }) => { const severity = row.getValue("severity") as VulnerabilitySeverity const config = severityConfig[severity] @@ -84,6 +85,7 @@ export function createVulnerabilityColumns({ size: 100, minSize: 80, maxSize: 150, + enableResizing: false, cell: ({ row }) => { const source = row.getValue("source") as string return ( @@ -129,10 +131,9 @@ export function createVulnerabilityColumns({ size: 500, minSize: 300, maxSize: 700, - cell: ({ row }) => { - const url = row.original.url - return - }, + cell: ({ row }) => ( + + ), }, { accessorKey: "createdAt", @@ -143,6 +144,7 @@ export function createVulnerabilityColumns({ size: 150, minSize: 120, maxSize: 200, + enableResizing: false, cell: ({ row }) => { const createdAt = row.getValue("createdAt") as string return ( diff --git a/frontend/components/websites/websites-columns.tsx b/frontend/components/websites/websites-columns.tsx index 38ffdbec..adb09bff 100644 --- a/frontend/components/websites/websites-columns.tsx +++ b/frontend/components/websites/websites-columns.tsx @@ -6,38 +6,7 @@ import { Checkbox } from "@/components/ui/checkbox" import { Badge } from "@/components/ui/badge" import { DataTableColumnHeader } from "@/components/ui/data-table/column-header" import type { WebSite } from "@/types/website.types" -import { TruncatedCell, TruncatedUrlCell } from "@/components/ui/truncated-cell" - -/** - * Body Preview 单元格组件 - 最多显示3行,超出折叠,点击展开查看完整内容 - */ -function BodyPreviewCell({ value }: { value: string | null | undefined }) { - const [expanded, setExpanded] = React.useState(false) - - if (!value) { - return - - } - - return ( -
-
setExpanded(!expanded)} - title={expanded ? "点击收起" : "点击展开"} - > - {value} -
- {value.length > 100 && ( - - )} -
- ) -} +import { ExpandableCell, ExpandableTagList } from "@/components/ui/data-table/expandable-cell" interface CreateWebSiteColumnsProps { formatDate: (dateString: string) => string @@ -82,10 +51,9 @@ export function createWebSiteColumns({ size: 400, minSize: 200, maxSize: 700, - cell: ({ row }) => { - const url = row.getValue("url") as string - return - }, + cell: ({ row }) => ( + + ), }, { accessorKey: "host", @@ -97,7 +65,7 @@ export function createWebSiteColumns({ minSize: 100, maxSize: 250, cell: ({ row }) => ( - + ), }, { @@ -110,7 +78,7 @@ export function createWebSiteColumns({ minSize: 100, maxSize: 300, cell: ({ row }) => ( - + ), }, { @@ -122,6 +90,7 @@ export function createWebSiteColumns({ size: 80, minSize: 60, maxSize: 100, + enableResizing: false, cell: ({ row }) => { const statusCode = row.getValue("statusCode") as number if (!statusCode) return "-" @@ -148,17 +117,7 @@ export function createWebSiteColumns({ minSize: 150, cell: ({ row }) => { const tech = row.getValue("tech") as string[] - if (!tech || tech.length === 0) return - - - return ( -
- {tech.map((technology, index) => ( - - {technology} - - ))} -
- ) + return }, }, { @@ -186,7 +145,7 @@ export function createWebSiteColumns({ minSize: 100, maxSize: 300, cell: ({ row }) => ( - + ), }, { @@ -199,7 +158,7 @@ export function createWebSiteColumns({ minSize: 80, maxSize: 200, cell: ({ row }) => ( - + ), }, { @@ -212,7 +171,7 @@ export function createWebSiteColumns({ minSize: 80, maxSize: 200, cell: ({ row }) => ( - + ), }, { @@ -224,7 +183,7 @@ export function createWebSiteColumns({ size: 350, minSize: 250, cell: ({ row }) => ( - + ), }, { @@ -236,6 +195,7 @@ export function createWebSiteColumns({ size: 80, minSize: 60, maxSize: 100, + enableResizing: false, cell: ({ row }) => { const vhost = row.getValue("vhost") as boolean | null if (vhost === null) return "-" @@ -255,6 +215,7 @@ export function createWebSiteColumns({ size: 150, minSize: 120, maxSize: 200, + enableResizing: false, cell: ({ row }) => { const createdAt = row.getValue("createdAt") as string return
{createdAt ? formatDate(createdAt) : "-"}
diff --git a/frontend/components/websites/websites-data-table.tsx b/frontend/components/websites/websites-data-table.tsx index a30a26f2..c2a16b83 100644 --- a/frontend/components/websites/websites-data-table.tsx +++ b/frontend/components/websites/websites-data-table.tsx @@ -2,8 +2,6 @@ import * as React from "react" import type { ColumnDef } from "@tanstack/react-table" -import { IconPlus } from "@tabler/icons-react" -import { Button } from "@/components/ui/button" import { UnifiedDataTable } from "@/components/ui/data-table" import type { FilterField } from "@/components/common/smart-filter-input" import type { WebSite } from "@/types/website.types" @@ -84,14 +82,6 @@ export function WebSitesDataTable({ }) } - // 自定义工具栏右侧按钮 - const toolbarRightContent = onBulkAdd ? ( - - ) : undefined - return ( 0 ? downloadOptions : undefined} // 空状态 emptyMessage="暂无数据" - // 自定义工具栏按钮 - toolbarRight={toolbarRightContent} /> ) } diff --git a/frontend/types/data-table.types.ts b/frontend/types/data-table.types.ts index e8ab0ef8..44b1e09c 100644 --- a/frontend/types/data-table.types.ts +++ b/frontend/types/data-table.types.ts @@ -105,9 +105,15 @@ export interface UnifiedDataTableProps { // 添加操作 onAddNew?: () => void + onAddHover?: () => void addButtonLabel?: string showAddButton?: boolean + // 批量添加操作 + onBulkAdd?: () => void + bulkAddLabel?: string + showBulkAdd?: boolean + // 下载操作 downloadOptions?: DownloadOption[]