mirror of
https://github.com/yyhuni/xingrin.git
synced 2026-01-31 11:46:16 +08:00
feat(search): add mock data infrastructure and vulnerability detail integration
- Add comprehensive mock data configuration for all major entities (dashboard, endpoints, organizations, scans, subdomains, targets, vulnerabilities, websites) - Implement mock service layer with centralized config for development and testing - Add vulnerability detail dialog integration to search results with lazy loading - Enhance search result card with vulnerability viewing capability - Update search materialized view migration to include vulnerability name field - Implement default host fuzzy search fallback for bare text queries without operators - Add vulnerability data formatting in search view for consistent API response structure - Configure Vercel deployment settings and update Next.js configuration - Update all service layers to support mock data injection for development environment - Extend search types with improved vulnerability data structure - Add internationalization strings for vulnerability loading errors - Enable rapid frontend development and testing without backend API dependency
This commit is contained in:
@@ -57,6 +57,7 @@ class Migration(migrations.Migration):
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'id', v.id,
|
||||
'name', v.vuln_type,
|
||||
'vuln_type', v.vuln_type,
|
||||
'severity', v.severity,
|
||||
'url', v.url
|
||||
|
||||
@@ -68,6 +68,11 @@ class SearchQueryParser:
|
||||
|
||||
query = query.strip()
|
||||
|
||||
# 检查是否包含操作符语法,如果不包含则作为 host 模糊搜索
|
||||
if not cls.CONDITION_PATTERN.search(query):
|
||||
# 裸文本,默认作为 host 模糊搜索
|
||||
return "host ILIKE %s", [f"%{query}%"]
|
||||
|
||||
# 按 || 分割为 OR 组
|
||||
or_groups = cls._split_by_or(query)
|
||||
|
||||
|
||||
@@ -125,6 +125,17 @@ class AssetSearchView(APIView):
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
vulnerabilities = []
|
||||
|
||||
# 格式化漏洞数据
|
||||
formatted_vulns = []
|
||||
for v in (vulnerabilities or []):
|
||||
formatted_vulns.append({
|
||||
'id': v.get('id'),
|
||||
'name': v.get('name', v.get('vuln_type', '')),
|
||||
'vulnType': v.get('vuln_type', ''),
|
||||
'severity': v.get('severity', 'info'),
|
||||
'url': v.get('url', ''),
|
||||
})
|
||||
|
||||
formatted_results.append({
|
||||
'url': result.get('url', ''),
|
||||
'host': result.get('host', ''),
|
||||
@@ -133,7 +144,7 @@ class AssetSearchView(APIView):
|
||||
'statusCode': result.get('status_code'),
|
||||
'responseHeaders': response_headers,
|
||||
'responseBody': result.get('response_body', ''),
|
||||
'vulnerabilities': vulnerabilities or [],
|
||||
'vulnerabilities': formatted_vulns,
|
||||
})
|
||||
|
||||
return success_response(data={
|
||||
|
||||
@@ -4,11 +4,15 @@ import { useState, useCallback } from "react"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
import { Search, AlertCircle } from "lucide-react"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { toast } from "sonner"
|
||||
import { SmartFilterInput, type FilterField } from "@/components/common/smart-filter-input"
|
||||
import { SearchResultCard } from "./search-result-card"
|
||||
import { SearchPagination } from "./search-pagination"
|
||||
import { useAssetSearch } from "@/hooks/use-search"
|
||||
import type { SearchParams, SearchState } from "@/types/search.types"
|
||||
import { VulnerabilityDetailDialog } from "@/components/vulnerabilities/vulnerability-detail-dialog"
|
||||
import { VulnerabilityService } from "@/services/vulnerability.service"
|
||||
import type { SearchParams, SearchState, Vulnerability as SearchVuln } from "@/types/search.types"
|
||||
import type { Vulnerability } from "@/types/vulnerability.types"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
|
||||
// 搜索示例 - 展示各种查询语法
|
||||
@@ -37,29 +41,6 @@ const SEARCH_FILTER_EXAMPLES = [
|
||||
'host="example" && status!="404" && tech="nginx"',
|
||||
]
|
||||
|
||||
// 验证搜索查询语法
|
||||
function validateSearchQuery(query: string): { valid: boolean; error?: string } {
|
||||
if (!query.trim()) {
|
||||
return { valid: false, error: 'Query cannot be empty' }
|
||||
}
|
||||
|
||||
// 检查是否有未闭合的引号
|
||||
const quoteCount = (query.match(/"/g) || []).length
|
||||
if (quoteCount % 2 !== 0) {
|
||||
return { valid: false, error: 'Unclosed quote detected' }
|
||||
}
|
||||
|
||||
// 检查基本语法:field="value" 或 field=="value" 或 field!="value"
|
||||
const conditionPattern = /(\w+)\s*(==|!=|=)\s*"([^"]*)"/g
|
||||
const conditions = query.match(conditionPattern)
|
||||
|
||||
if (!conditions || conditions.length === 0) {
|
||||
return { valid: false, error: 'Invalid syntax. Use: field="value", field=="value", or field!="value"' }
|
||||
}
|
||||
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
export function SearchPage() {
|
||||
const t = useTranslations('search')
|
||||
const [searchState, setSearchState] = useState<SearchState>("initial")
|
||||
@@ -67,6 +48,9 @@ export function SearchPage() {
|
||||
const [searchParams, setSearchParams] = useState<SearchParams>({})
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [selectedVuln, setSelectedVuln] = useState<Vulnerability | null>(null)
|
||||
const [vulnDialogOpen, setVulnDialogOpen] = useState(false)
|
||||
const [loadingVuln, setLoadingVuln] = useState(false)
|
||||
|
||||
// 搜索过滤字段配置
|
||||
const SEARCH_FILTER_FIELDS: FilterField[] = [
|
||||
@@ -88,15 +72,7 @@ export function SearchPage() {
|
||||
const handleSearch = useCallback((_filters: unknown, rawQuery: string) => {
|
||||
if (!rawQuery.trim()) return
|
||||
|
||||
// 验证语法
|
||||
const validation = validateSearchQuery(rawQuery)
|
||||
if (!validation.valid) {
|
||||
// 可以显示错误提示,这里简单处理
|
||||
console.warn('Search validation:', validation.error)
|
||||
}
|
||||
|
||||
setQuery(rawQuery)
|
||||
// 直接将原始查询发送给后端解析
|
||||
setSearchParams({ q: rawQuery })
|
||||
setPage(1)
|
||||
setSearchState("searching")
|
||||
@@ -116,6 +92,21 @@ export function SearchPage() {
|
||||
setPage(1)
|
||||
}, [])
|
||||
|
||||
const handleViewVulnerability = useCallback(async (vuln: SearchVuln) => {
|
||||
if (!vuln.id) return
|
||||
|
||||
setLoadingVuln(true)
|
||||
try {
|
||||
const fullVuln = await VulnerabilityService.getVulnerabilityById(vuln.id)
|
||||
setSelectedVuln(fullVuln)
|
||||
setVulnDialogOpen(true)
|
||||
} catch {
|
||||
toast.error(t('vulnLoadError'))
|
||||
} finally {
|
||||
setLoadingVuln(false)
|
||||
}
|
||||
}, [t])
|
||||
|
||||
return (
|
||||
<div className="flex-1 w-full flex flex-col">
|
||||
<AnimatePresence mode="wait">
|
||||
@@ -233,7 +224,10 @@ export function SearchPage() {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: index * 0.05 }}
|
||||
>
|
||||
<SearchResultCard result={result} />
|
||||
<SearchResultCard
|
||||
result={result}
|
||||
onViewVulnerability={handleViewVulnerability}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
@@ -257,6 +251,13 @@ export function SearchPage() {
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* 漏洞详情弹窗 - 复用现有组件 */}
|
||||
<VulnerabilityDetailDialog
|
||||
vulnerability={selectedVuln}
|
||||
open={vulnDialogOpen}
|
||||
onOpenChange={setVulnDialogOpen}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -190,10 +190,9 @@ export function SearchResultCard({ result, onViewVulnerability }: SearchResultCa
|
||||
<Table className="table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-xs w-[40%]">{t('vulnName')}</TableHead>
|
||||
<TableHead className="text-xs w-[15%]">{t('severity')}</TableHead>
|
||||
<TableHead className="text-xs w-[15%]">{t('source')}</TableHead>
|
||||
<TableHead className="text-xs w-[50%]">{t('vulnName')}</TableHead>
|
||||
<TableHead className="text-xs w-[20%]">{t('vulnType')}</TableHead>
|
||||
<TableHead className="text-xs w-[20%]">{t('severity')}</TableHead>
|
||||
<TableHead className="text-xs w-[10%]"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -212,19 +211,6 @@ export function SearchResultCard({ result, onViewVulnerability }: SearchResultCa
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-xs ${severityColors[vuln.severity] || severityColors.info}`}
|
||||
>
|
||||
{vuln.severity}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{vuln.source}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -237,6 +223,14 @@ export function SearchResultCard({ result, onViewVulnerability }: SearchResultCa
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-xs ${severityColors[vuln.severity] || severityColors.info}`}
|
||||
>
|
||||
{vuln.severity}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -317,13 +317,14 @@
|
||||
},
|
||||
"search": {
|
||||
"title": "Asset Search",
|
||||
"hint": "Click search box to view available fields and syntax",
|
||||
"hint": "Click search box to view available fields and syntax. Plain text defaults to hostname search",
|
||||
"searching": "Searching...",
|
||||
"loading": "Loading...",
|
||||
"resultsCount": "Found {count} results",
|
||||
"error": "Search failed, please try again later",
|
||||
"noResults": "No matching assets found",
|
||||
"noResultsHint": "Try adjusting your search criteria",
|
||||
"vulnLoadError": "Failed to load vulnerability details",
|
||||
"fields": {
|
||||
"host": "Hostname",
|
||||
"url": "URL address",
|
||||
@@ -342,6 +343,13 @@
|
||||
"severity": "Severity",
|
||||
"source": "Source",
|
||||
"vulnType": "Vuln Type"
|
||||
},
|
||||
"vulnDetail": {
|
||||
"title": "Vulnerability Details",
|
||||
"name": "Vulnerability Name",
|
||||
"source": "Source",
|
||||
"type": "Vulnerability Type",
|
||||
"url": "Vulnerability URL"
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
@@ -745,6 +753,7 @@
|
||||
"organizationMode": "Organization Scan",
|
||||
"organizationModeHint": "In organization scan mode, all targets under this organization will be dynamically fetched at execution",
|
||||
"noAvailableTarget": "No available targets",
|
||||
"noEngine": "No engines available",
|
||||
"selected": "Selected",
|
||||
"selectedEngines": "{count} engines selected"
|
||||
},
|
||||
|
||||
@@ -317,13 +317,14 @@
|
||||
},
|
||||
"search": {
|
||||
"title": "资产搜索",
|
||||
"hint": "点击搜索框查看可用字段和语法",
|
||||
"hint": "点击搜索框查看可用字段和语法,直接输入文本默认搜索主机名",
|
||||
"searching": "搜索中...",
|
||||
"loading": "加载中...",
|
||||
"resultsCount": "找到 {count} 条结果",
|
||||
"error": "搜索失败,请稍后重试",
|
||||
"noResults": "未找到匹配的资产",
|
||||
"noResultsHint": "请尝试调整搜索条件",
|
||||
"vulnLoadError": "加载漏洞详情失败",
|
||||
"fields": {
|
||||
"host": "主机名",
|
||||
"url": "URL 地址",
|
||||
@@ -342,6 +343,13 @@
|
||||
"severity": "严重程度",
|
||||
"source": "来源",
|
||||
"vulnType": "漏洞类型"
|
||||
},
|
||||
"vulnDetail": {
|
||||
"title": "漏洞详情",
|
||||
"name": "漏洞名称",
|
||||
"source": "来源",
|
||||
"type": "漏洞类型",
|
||||
"url": "漏洞 URL"
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
@@ -745,6 +753,7 @@
|
||||
"organizationMode": "组织扫描",
|
||||
"organizationModeHint": "组织扫描模式下,执行时将动态获取该组织下所有目标",
|
||||
"noAvailableTarget": "暂无可用目标",
|
||||
"noEngine": "暂无可用引擎",
|
||||
"selected": "已选择",
|
||||
"selectedEngines": "已选择 {count} 个引擎"
|
||||
},
|
||||
|
||||
23
frontend/mock/config.ts
Normal file
23
frontend/mock/config.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Mock 数据配置
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. 在 .env.local 中设置 NEXT_PUBLIC_USE_MOCK=true 启用 mock 数据
|
||||
* 2. 或者直接修改下面的 FORCE_MOCK 为 true
|
||||
*/
|
||||
|
||||
// 强制使用 mock 数据(一般保持 false,通过环境变量控制)
|
||||
const FORCE_MOCK = false
|
||||
|
||||
// 从环境变量读取 mock 配置
|
||||
export const USE_MOCK = FORCE_MOCK || process.env.NEXT_PUBLIC_USE_MOCK === 'true'
|
||||
|
||||
// Mock 数据延迟(模拟网络请求)
|
||||
export const MOCK_DELAY = 300 // ms
|
||||
|
||||
/**
|
||||
* 模拟网络延迟
|
||||
*/
|
||||
export function mockDelay(ms: number = MOCK_DELAY): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
71
frontend/mock/data/dashboard.ts
Normal file
71
frontend/mock/data/dashboard.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { AssetStatistics, StatisticsHistoryItem, DashboardStats } from '@/types/dashboard.types'
|
||||
|
||||
export const mockDashboardStats: DashboardStats = {
|
||||
totalTargets: 156,
|
||||
totalSubdomains: 4823,
|
||||
totalEndpoints: 12456,
|
||||
totalVulnerabilities: 89,
|
||||
}
|
||||
|
||||
export const mockAssetStatistics: AssetStatistics = {
|
||||
totalTargets: 156,
|
||||
totalSubdomains: 4823,
|
||||
totalIps: 892,
|
||||
totalEndpoints: 12456,
|
||||
totalWebsites: 3421,
|
||||
totalVulns: 89,
|
||||
totalAssets: 21638,
|
||||
runningScans: 3,
|
||||
updatedAt: new Date().toISOString(),
|
||||
// 变化值
|
||||
changeTargets: 12,
|
||||
changeSubdomains: 234,
|
||||
changeIps: 45,
|
||||
changeEndpoints: 567,
|
||||
changeWebsites: 89,
|
||||
changeVulns: -5,
|
||||
changeAssets: 942,
|
||||
// 漏洞严重程度分布
|
||||
vulnBySeverity: {
|
||||
critical: 3,
|
||||
high: 12,
|
||||
medium: 28,
|
||||
low: 34,
|
||||
info: 12,
|
||||
},
|
||||
}
|
||||
|
||||
// 生成过去 N 天的历史数据
|
||||
function generateHistoryData(days: number): StatisticsHistoryItem[] {
|
||||
const data: StatisticsHistoryItem[] = []
|
||||
const now = new Date()
|
||||
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = new Date(now)
|
||||
date.setDate(date.getDate() - i)
|
||||
|
||||
// 模拟逐渐增长的趋势
|
||||
const factor = 1 + (days - i) * 0.02
|
||||
|
||||
data.push({
|
||||
date: date.toISOString().split('T')[0],
|
||||
totalTargets: Math.floor(140 * factor),
|
||||
totalSubdomains: Math.floor(4200 * factor),
|
||||
totalIps: Math.floor(780 * factor),
|
||||
totalEndpoints: Math.floor(10800 * factor),
|
||||
totalWebsites: Math.floor(2980 * factor),
|
||||
totalVulns: Math.floor(75 * factor),
|
||||
totalAssets: Math.floor(18900 * factor),
|
||||
})
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export const mockStatisticsHistory7Days = generateHistoryData(7)
|
||||
export const mockStatisticsHistory30Days = generateHistoryData(30)
|
||||
|
||||
export function getMockStatisticsHistory(days: number): StatisticsHistoryItem[] {
|
||||
if (days <= 7) return mockStatisticsHistory7Days
|
||||
return generateHistoryData(days)
|
||||
}
|
||||
257
frontend/mock/data/endpoints.ts
Normal file
257
frontend/mock/data/endpoints.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import type { Endpoint, GetEndpointsResponse } from '@/types/endpoint.types'
|
||||
|
||||
export const mockEndpoints: Endpoint[] = [
|
||||
{
|
||||
id: 1,
|
||||
url: 'https://acme.com/',
|
||||
method: 'GET',
|
||||
statusCode: 200,
|
||||
title: 'Acme Corporation - Home',
|
||||
contentLength: 45678,
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
responseTime: 0.234,
|
||||
host: 'acme.com',
|
||||
webserver: 'nginx/1.24.0',
|
||||
tech: ['React', 'Next.js', 'Node.js'],
|
||||
createdAt: '2024-12-28T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
url: 'https://acme.com/login',
|
||||
method: 'GET',
|
||||
statusCode: 200,
|
||||
title: 'Login - Acme',
|
||||
contentLength: 12345,
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
responseTime: 0.156,
|
||||
host: 'acme.com',
|
||||
webserver: 'nginx/1.24.0',
|
||||
tech: ['React', 'Next.js'],
|
||||
createdAt: '2024-12-28T10:01:00Z',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
url: 'https://api.acme.com/v1/users',
|
||||
method: 'GET',
|
||||
statusCode: 200,
|
||||
title: '',
|
||||
contentLength: 8923,
|
||||
contentType: 'application/json',
|
||||
responseTime: 0.089,
|
||||
host: 'api.acme.com',
|
||||
webserver: 'nginx/1.24.0',
|
||||
tech: ['Django', 'Python'],
|
||||
gfPatterns: ['api', 'json'],
|
||||
createdAt: '2024-12-28T10:02:00Z',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
url: 'https://api.acme.com/v1/products',
|
||||
method: 'GET',
|
||||
statusCode: 200,
|
||||
title: '',
|
||||
contentLength: 23456,
|
||||
contentType: 'application/json',
|
||||
responseTime: 0.145,
|
||||
host: 'api.acme.com',
|
||||
webserver: 'nginx/1.24.0',
|
||||
tech: ['Django', 'Python'],
|
||||
gfPatterns: ['api', 'json'],
|
||||
createdAt: '2024-12-28T10:03:00Z',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
url: 'https://acme.io/docs',
|
||||
method: 'GET',
|
||||
statusCode: 200,
|
||||
title: 'Documentation - Acme.io',
|
||||
contentLength: 67890,
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
responseTime: 0.312,
|
||||
host: 'acme.io',
|
||||
webserver: 'cloudflare',
|
||||
tech: ['Vue.js', 'Vitepress'],
|
||||
createdAt: '2024-12-27T14:30:00Z',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
url: 'https://acme.io/api/config',
|
||||
method: 'GET',
|
||||
statusCode: 200,
|
||||
title: '',
|
||||
contentLength: 1234,
|
||||
contentType: 'application/json',
|
||||
responseTime: 0.067,
|
||||
host: 'acme.io',
|
||||
webserver: 'cloudflare',
|
||||
tech: ['Node.js', 'Express'],
|
||||
gfPatterns: ['config', 'json'],
|
||||
createdAt: '2024-12-27T14:31:00Z',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
url: 'https://techstart.io/',
|
||||
method: 'GET',
|
||||
statusCode: 200,
|
||||
title: 'TechStart - Innovation Hub',
|
||||
contentLength: 34567,
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
responseTime: 0.278,
|
||||
host: 'techstart.io',
|
||||
webserver: 'Apache/2.4.54',
|
||||
tech: ['WordPress', 'PHP'],
|
||||
createdAt: '2024-12-26T08:45:00Z',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
url: 'https://techstart.io/admin',
|
||||
method: 'GET',
|
||||
statusCode: 302,
|
||||
title: '',
|
||||
contentLength: 0,
|
||||
contentType: 'text/html',
|
||||
responseTime: 0.045,
|
||||
location: 'https://techstart.io/admin/login',
|
||||
host: 'techstart.io',
|
||||
webserver: 'Apache/2.4.54',
|
||||
tech: ['WordPress', 'PHP'],
|
||||
createdAt: '2024-12-26T08:46:00Z',
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
url: 'https://globalfinance.com/',
|
||||
method: 'GET',
|
||||
statusCode: 200,
|
||||
title: 'Global Finance - Your Financial Partner',
|
||||
contentLength: 56789,
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
responseTime: 0.456,
|
||||
host: 'globalfinance.com',
|
||||
webserver: 'Microsoft-IIS/10.0',
|
||||
tech: ['ASP.NET', 'C#', 'jQuery'],
|
||||
createdAt: '2024-12-25T16:20:00Z',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
url: 'https://globalfinance.com/.git/config',
|
||||
method: 'GET',
|
||||
statusCode: 200,
|
||||
title: '',
|
||||
contentLength: 456,
|
||||
contentType: 'text/plain',
|
||||
responseTime: 0.034,
|
||||
host: 'globalfinance.com',
|
||||
webserver: 'Microsoft-IIS/10.0',
|
||||
gfPatterns: ['git', 'config'],
|
||||
createdAt: '2024-12-25T16:21:00Z',
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
url: 'https://retailmax.com/',
|
||||
method: 'GET',
|
||||
statusCode: 200,
|
||||
title: 'RetailMax - Shop Everything',
|
||||
contentLength: 89012,
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
responseTime: 0.567,
|
||||
host: 'retailmax.com',
|
||||
webserver: 'nginx/1.22.0',
|
||||
tech: ['React', 'Redux', 'Node.js'],
|
||||
createdAt: '2024-12-21T10:45:00Z',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
url: 'https://retailmax.com/product?id=1',
|
||||
method: 'GET',
|
||||
statusCode: 200,
|
||||
title: 'Product Detail - RetailMax',
|
||||
contentLength: 23456,
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
responseTime: 0.234,
|
||||
host: 'retailmax.com',
|
||||
webserver: 'nginx/1.22.0',
|
||||
tech: ['React', 'Redux'],
|
||||
gfPatterns: ['param', 'id'],
|
||||
createdAt: '2024-12-21T10:46:00Z',
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
url: 'https://healthcareplus.com/',
|
||||
method: 'GET',
|
||||
statusCode: 200,
|
||||
title: 'HealthCare Plus - Digital Health',
|
||||
contentLength: 45678,
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
responseTime: 0.345,
|
||||
host: 'healthcareplus.com',
|
||||
webserver: 'nginx/1.24.0',
|
||||
tech: ['Angular', 'TypeScript'],
|
||||
createdAt: '2024-12-23T11:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
url: 'https://edutech.io/',
|
||||
method: 'GET',
|
||||
statusCode: 200,
|
||||
title: 'EduTech - Learn Anywhere',
|
||||
contentLength: 67890,
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
responseTime: 0.289,
|
||||
host: 'edutech.io',
|
||||
webserver: 'cloudflare',
|
||||
tech: ['Vue.js', 'Nuxt.js'],
|
||||
createdAt: '2024-12-22T13:30:00Z',
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
url: 'https://cloudnine.host/',
|
||||
method: 'GET',
|
||||
statusCode: 200,
|
||||
title: 'CloudNine Hosting',
|
||||
contentLength: 34567,
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
responseTime: 0.178,
|
||||
host: 'cloudnine.host',
|
||||
webserver: 'LiteSpeed',
|
||||
tech: ['PHP', 'Laravel'],
|
||||
createdAt: '2024-12-19T16:00:00Z',
|
||||
},
|
||||
]
|
||||
|
||||
export function getMockEndpoints(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
search?: string
|
||||
}): GetEndpointsResponse {
|
||||
const page = params?.page || 1
|
||||
const pageSize = params?.pageSize || 10
|
||||
const search = params?.search?.toLowerCase() || ''
|
||||
|
||||
let filtered = mockEndpoints
|
||||
|
||||
if (search) {
|
||||
filtered = mockEndpoints.filter(
|
||||
ep =>
|
||||
ep.url.toLowerCase().includes(search) ||
|
||||
ep.title.toLowerCase().includes(search) ||
|
||||
ep.host?.toLowerCase().includes(search)
|
||||
)
|
||||
}
|
||||
|
||||
const total = filtered.length
|
||||
const totalPages = Math.ceil(total / pageSize)
|
||||
const start = (page - 1) * pageSize
|
||||
const endpoints = filtered.slice(start, start + pageSize)
|
||||
|
||||
return {
|
||||
endpoints,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
}
|
||||
}
|
||||
|
||||
export function getMockEndpointById(id: number): Endpoint | undefined {
|
||||
return mockEndpoints.find(ep => ep.id === id)
|
||||
}
|
||||
145
frontend/mock/data/organizations.ts
Normal file
145
frontend/mock/data/organizations.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import type { Organization, OrganizationsResponse } from '@/types/organization.types'
|
||||
|
||||
export const mockOrganizations: Organization[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Acme Corporation',
|
||||
description: '全球领先的科技公司,专注于云计算和人工智能领域',
|
||||
createdAt: '2024-01-15T08:30:00Z',
|
||||
updatedAt: '2024-12-28T14:20:00Z',
|
||||
targetCount: 12,
|
||||
domainCount: 156,
|
||||
endpointCount: 2341,
|
||||
targets: [
|
||||
{ id: 1, name: 'acme.com' },
|
||||
{ id: 2, name: 'acme.io' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'TechStart Inc',
|
||||
description: '创新型初创企业,主营 SaaS 产品开发',
|
||||
createdAt: '2024-02-20T10:15:00Z',
|
||||
updatedAt: '2024-12-27T09:45:00Z',
|
||||
targetCount: 5,
|
||||
domainCount: 78,
|
||||
endpointCount: 892,
|
||||
targets: [
|
||||
{ id: 3, name: 'techstart.io' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Global Finance Ltd',
|
||||
description: '国际金融服务公司,提供银行和投资解决方案',
|
||||
createdAt: '2024-03-10T14:00:00Z',
|
||||
updatedAt: '2024-12-26T16:30:00Z',
|
||||
targetCount: 8,
|
||||
domainCount: 234,
|
||||
endpointCount: 1567,
|
||||
targets: [
|
||||
{ id: 4, name: 'globalfinance.com' },
|
||||
{ id: 5, name: 'gf-bank.net' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'HealthCare Plus',
|
||||
description: '医疗健康科技公司,专注于数字化医疗解决方案',
|
||||
createdAt: '2024-04-05T09:20:00Z',
|
||||
updatedAt: '2024-12-25T11:10:00Z',
|
||||
targetCount: 6,
|
||||
domainCount: 89,
|
||||
endpointCount: 723,
|
||||
targets: [
|
||||
{ id: 6, name: 'healthcareplus.com' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'EduTech Solutions',
|
||||
description: '在线教育平台,提供 K-12 和职业培训课程',
|
||||
createdAt: '2024-05-12T11:45:00Z',
|
||||
updatedAt: '2024-12-24T13:55:00Z',
|
||||
targetCount: 4,
|
||||
domainCount: 45,
|
||||
endpointCount: 456,
|
||||
targets: [
|
||||
{ id: 7, name: 'edutech.io' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'RetailMax',
|
||||
description: '电子商务零售平台,覆盖多品类商品销售',
|
||||
createdAt: '2024-06-08T16:30:00Z',
|
||||
updatedAt: '2024-12-23T10:20:00Z',
|
||||
targetCount: 15,
|
||||
domainCount: 312,
|
||||
endpointCount: 4521,
|
||||
targets: [
|
||||
{ id: 8, name: 'retailmax.com' },
|
||||
{ id: 9, name: 'retailmax.cn' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'CloudNine Hosting',
|
||||
description: '云托管服务提供商,提供 VPS 和专用服务器',
|
||||
createdAt: '2024-07-20T08:00:00Z',
|
||||
updatedAt: '2024-12-22T15:40:00Z',
|
||||
targetCount: 3,
|
||||
domainCount: 67,
|
||||
endpointCount: 389,
|
||||
targets: [
|
||||
{ id: 10, name: 'cloudnine.host' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'MediaStream Corp',
|
||||
description: '流媒体内容分发平台,提供视频和音频服务',
|
||||
createdAt: '2024-08-15T12:10:00Z',
|
||||
updatedAt: '2024-12-21T08:25:00Z',
|
||||
targetCount: 7,
|
||||
domainCount: 123,
|
||||
endpointCount: 1234,
|
||||
targets: [
|
||||
{ id: 11, name: 'mediastream.tv' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export function getMockOrganizations(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
search?: string
|
||||
}): OrganizationsResponse<Organization> {
|
||||
const page = params?.page || 1
|
||||
const pageSize = params?.pageSize || 10
|
||||
const search = params?.search?.toLowerCase() || ''
|
||||
|
||||
// 过滤搜索
|
||||
let filtered = mockOrganizations
|
||||
if (search) {
|
||||
filtered = mockOrganizations.filter(
|
||||
org =>
|
||||
org.name.toLowerCase().includes(search) ||
|
||||
org.description.toLowerCase().includes(search)
|
||||
)
|
||||
}
|
||||
|
||||
// 分页
|
||||
const total = filtered.length
|
||||
const totalPages = Math.ceil(total / pageSize)
|
||||
const start = (page - 1) * pageSize
|
||||
const results = filtered.slice(start, start + pageSize)
|
||||
|
||||
return {
|
||||
results,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
}
|
||||
}
|
||||
309
frontend/mock/data/scans.ts
Normal file
309
frontend/mock/data/scans.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import type { ScanRecord, GetScansResponse, ScanStatus } from '@/types/scan.types'
|
||||
import type { ScanStatistics } from '@/services/scan.service'
|
||||
|
||||
export const mockScans: ScanRecord[] = [
|
||||
{
|
||||
id: 1,
|
||||
target: 1,
|
||||
targetName: 'acme.com',
|
||||
workerName: 'worker-01',
|
||||
summary: {
|
||||
subdomains: 156,
|
||||
websites: 89,
|
||||
directories: 234,
|
||||
endpoints: 2341,
|
||||
ips: 45,
|
||||
vulnerabilities: {
|
||||
total: 23,
|
||||
critical: 1,
|
||||
high: 4,
|
||||
medium: 8,
|
||||
low: 10,
|
||||
},
|
||||
},
|
||||
engineIds: [1, 2, 3],
|
||||
engineNames: ['Subdomain Discovery', 'Web Crawling', 'Nuclei Scanner'],
|
||||
createdAt: '2024-12-28T10:00:00Z',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
target: 2,
|
||||
targetName: 'acme.io',
|
||||
workerName: 'worker-02',
|
||||
summary: {
|
||||
subdomains: 78,
|
||||
websites: 45,
|
||||
directories: 123,
|
||||
endpoints: 892,
|
||||
ips: 23,
|
||||
vulnerabilities: {
|
||||
total: 12,
|
||||
critical: 0,
|
||||
high: 2,
|
||||
medium: 5,
|
||||
low: 5,
|
||||
},
|
||||
},
|
||||
engineIds: [1, 2],
|
||||
engineNames: ['Subdomain Discovery', 'Web Crawling'],
|
||||
createdAt: '2024-12-27T14:30:00Z',
|
||||
status: 'running',
|
||||
progress: 65,
|
||||
currentStage: 'web_crawling',
|
||||
stageProgress: {
|
||||
subdomain_discovery: {
|
||||
status: 'completed',
|
||||
order: 0,
|
||||
startedAt: '2024-12-27T14:30:00Z',
|
||||
duration: 1200,
|
||||
detail: 'Found 78 subdomains',
|
||||
},
|
||||
web_crawling: {
|
||||
status: 'running',
|
||||
order: 1,
|
||||
startedAt: '2024-12-27T14:50:00Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
target: 3,
|
||||
targetName: 'techstart.io',
|
||||
workerName: 'worker-01',
|
||||
summary: {
|
||||
subdomains: 45,
|
||||
websites: 28,
|
||||
directories: 89,
|
||||
endpoints: 567,
|
||||
ips: 12,
|
||||
vulnerabilities: {
|
||||
total: 8,
|
||||
critical: 0,
|
||||
high: 1,
|
||||
medium: 3,
|
||||
low: 4,
|
||||
},
|
||||
},
|
||||
engineIds: [1, 2, 3],
|
||||
engineNames: ['Subdomain Discovery', 'Web Crawling', 'Nuclei Scanner'],
|
||||
createdAt: '2024-12-26T08:45:00Z',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
target: 4,
|
||||
targetName: 'globalfinance.com',
|
||||
workerName: 'worker-03',
|
||||
summary: {
|
||||
subdomains: 0,
|
||||
websites: 0,
|
||||
directories: 0,
|
||||
endpoints: 0,
|
||||
ips: 0,
|
||||
vulnerabilities: {
|
||||
total: 0,
|
||||
critical: 0,
|
||||
high: 0,
|
||||
medium: 0,
|
||||
low: 0,
|
||||
},
|
||||
},
|
||||
engineIds: [1],
|
||||
engineNames: ['Subdomain Discovery'],
|
||||
createdAt: '2024-12-25T16:20:00Z',
|
||||
status: 'failed',
|
||||
progress: 15,
|
||||
errorMessage: 'Connection timeout: Unable to reach target',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
target: 6,
|
||||
targetName: 'healthcareplus.com',
|
||||
workerName: 'worker-02',
|
||||
summary: {
|
||||
subdomains: 34,
|
||||
websites: 0,
|
||||
directories: 0,
|
||||
endpoints: 0,
|
||||
ips: 8,
|
||||
vulnerabilities: {
|
||||
total: 0,
|
||||
critical: 0,
|
||||
high: 0,
|
||||
medium: 0,
|
||||
low: 0,
|
||||
},
|
||||
},
|
||||
engineIds: [1, 2, 3],
|
||||
engineNames: ['Subdomain Discovery', 'Web Crawling', 'Nuclei Scanner'],
|
||||
createdAt: '2024-12-29T09:00:00Z',
|
||||
status: 'running',
|
||||
progress: 25,
|
||||
currentStage: 'subdomain_discovery',
|
||||
stageProgress: {
|
||||
subdomain_discovery: {
|
||||
status: 'running',
|
||||
order: 0,
|
||||
startedAt: '2024-12-29T09:00:00Z',
|
||||
},
|
||||
web_crawling: {
|
||||
status: 'pending',
|
||||
order: 1,
|
||||
},
|
||||
nuclei_scan: {
|
||||
status: 'pending',
|
||||
order: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
target: 7,
|
||||
targetName: 'edutech.io',
|
||||
workerName: null,
|
||||
summary: {
|
||||
subdomains: 0,
|
||||
websites: 0,
|
||||
directories: 0,
|
||||
endpoints: 0,
|
||||
ips: 0,
|
||||
vulnerabilities: {
|
||||
total: 0,
|
||||
critical: 0,
|
||||
high: 0,
|
||||
medium: 0,
|
||||
low: 0,
|
||||
},
|
||||
},
|
||||
engineIds: [1, 2],
|
||||
engineNames: ['Subdomain Discovery', 'Web Crawling'],
|
||||
createdAt: '2024-12-29T10:30:00Z',
|
||||
status: 'initiated',
|
||||
progress: 0,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
target: 8,
|
||||
targetName: 'retailmax.com',
|
||||
workerName: 'worker-01',
|
||||
summary: {
|
||||
subdomains: 89,
|
||||
websites: 56,
|
||||
directories: 178,
|
||||
endpoints: 1234,
|
||||
ips: 28,
|
||||
vulnerabilities: {
|
||||
total: 15,
|
||||
critical: 0,
|
||||
high: 3,
|
||||
medium: 6,
|
||||
low: 6,
|
||||
},
|
||||
},
|
||||
engineIds: [1, 2, 3],
|
||||
engineNames: ['Subdomain Discovery', 'Web Crawling', 'Nuclei Scanner'],
|
||||
createdAt: '2024-12-21T10:45:00Z',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
target: 11,
|
||||
targetName: 'mediastream.tv',
|
||||
workerName: 'worker-02',
|
||||
summary: {
|
||||
subdomains: 67,
|
||||
websites: 0,
|
||||
directories: 0,
|
||||
endpoints: 0,
|
||||
ips: 15,
|
||||
vulnerabilities: {
|
||||
total: 0,
|
||||
critical: 0,
|
||||
high: 0,
|
||||
medium: 0,
|
||||
low: 0,
|
||||
},
|
||||
},
|
||||
engineIds: [1, 2, 3],
|
||||
engineNames: ['Subdomain Discovery', 'Web Crawling', 'Nuclei Scanner'],
|
||||
createdAt: '2024-12-29T08:00:00Z',
|
||||
status: 'running',
|
||||
progress: 45,
|
||||
currentStage: 'web_crawling',
|
||||
stageProgress: {
|
||||
subdomain_discovery: {
|
||||
status: 'completed',
|
||||
order: 0,
|
||||
startedAt: '2024-12-29T08:00:00Z',
|
||||
duration: 900,
|
||||
detail: 'Found 67 subdomains',
|
||||
},
|
||||
web_crawling: {
|
||||
status: 'running',
|
||||
order: 1,
|
||||
startedAt: '2024-12-29T08:15:00Z',
|
||||
},
|
||||
nuclei_scan: {
|
||||
status: 'pending',
|
||||
order: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export const mockScanStatistics: ScanStatistics = {
|
||||
total: 156,
|
||||
running: 3,
|
||||
completed: 142,
|
||||
failed: 11,
|
||||
totalVulns: 89,
|
||||
totalSubdomains: 4823,
|
||||
totalEndpoints: 12456,
|
||||
totalWebsites: 3421,
|
||||
totalAssets: 21638,
|
||||
}
|
||||
|
||||
export function getMockScans(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
status?: ScanStatus
|
||||
search?: string
|
||||
}): GetScansResponse {
|
||||
const page = params?.page || 1
|
||||
const pageSize = params?.pageSize || 10
|
||||
const status = params?.status
|
||||
const search = params?.search?.toLowerCase() || ''
|
||||
|
||||
let filtered = mockScans
|
||||
|
||||
if (status) {
|
||||
filtered = filtered.filter(scan => scan.status === status)
|
||||
}
|
||||
|
||||
if (search) {
|
||||
filtered = filtered.filter(scan =>
|
||||
scan.targetName.toLowerCase().includes(search)
|
||||
)
|
||||
}
|
||||
|
||||
const total = filtered.length
|
||||
const totalPages = Math.ceil(total / pageSize)
|
||||
const start = (page - 1) * pageSize
|
||||
const results = filtered.slice(start, start + pageSize)
|
||||
|
||||
return {
|
||||
results,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
}
|
||||
}
|
||||
|
||||
export function getMockScanById(id: number): ScanRecord | undefined {
|
||||
return mockScans.find(scan => scan.id === id)
|
||||
}
|
||||
78
frontend/mock/data/subdomains.ts
Normal file
78
frontend/mock/data/subdomains.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { Subdomain, GetAllSubdomainsResponse } from '@/types/subdomain.types'
|
||||
|
||||
export const mockSubdomains: Subdomain[] = [
|
||||
{ id: 1, name: 'acme.com', createdAt: '2024-12-28T10:00:00Z' },
|
||||
{ id: 2, name: 'www.acme.com', createdAt: '2024-12-28T10:01:00Z' },
|
||||
{ id: 3, name: 'api.acme.com', createdAt: '2024-12-28T10:02:00Z' },
|
||||
{ id: 4, name: 'admin.acme.com', createdAt: '2024-12-28T10:03:00Z' },
|
||||
{ id: 5, name: 'mail.acme.com', createdAt: '2024-12-28T10:04:00Z' },
|
||||
{ id: 6, name: 'blog.acme.com', createdAt: '2024-12-28T10:05:00Z' },
|
||||
{ id: 7, name: 'shop.acme.com', createdAt: '2024-12-28T10:06:00Z' },
|
||||
{ id: 8, name: 'cdn.acme.com', createdAt: '2024-12-28T10:07:00Z' },
|
||||
{ id: 9, name: 'static.acme.com', createdAt: '2024-12-28T10:08:00Z' },
|
||||
{ id: 10, name: 'dev.acme.com', createdAt: '2024-12-28T10:09:00Z' },
|
||||
{ id: 11, name: 'staging.acme.com', createdAt: '2024-12-28T10:10:00Z' },
|
||||
{ id: 12, name: 'test.acme.com', createdAt: '2024-12-28T10:11:00Z' },
|
||||
{ id: 13, name: 'acme.io', createdAt: '2024-12-27T14:30:00Z' },
|
||||
{ id: 14, name: 'docs.acme.io', createdAt: '2024-12-27T14:31:00Z' },
|
||||
{ id: 15, name: 'api.acme.io', createdAt: '2024-12-27T14:32:00Z' },
|
||||
{ id: 16, name: 'status.acme.io', createdAt: '2024-12-27T14:33:00Z' },
|
||||
{ id: 17, name: 'techstart.io', createdAt: '2024-12-26T08:45:00Z' },
|
||||
{ id: 18, name: 'www.techstart.io', createdAt: '2024-12-26T08:46:00Z' },
|
||||
{ id: 19, name: 'app.techstart.io', createdAt: '2024-12-26T08:47:00Z' },
|
||||
{ id: 20, name: 'globalfinance.com', createdAt: '2024-12-25T16:20:00Z' },
|
||||
{ id: 21, name: 'www.globalfinance.com', createdAt: '2024-12-25T16:21:00Z' },
|
||||
{ id: 22, name: 'secure.globalfinance.com', createdAt: '2024-12-25T16:22:00Z' },
|
||||
{ id: 23, name: 'portal.globalfinance.com', createdAt: '2024-12-25T16:23:00Z' },
|
||||
{ id: 24, name: 'healthcareplus.com', createdAt: '2024-12-23T11:00:00Z' },
|
||||
{ id: 25, name: 'www.healthcareplus.com', createdAt: '2024-12-23T11:01:00Z' },
|
||||
{ id: 26, name: 'patient.healthcareplus.com', createdAt: '2024-12-23T11:02:00Z' },
|
||||
{ id: 27, name: 'edutech.io', createdAt: '2024-12-22T13:30:00Z' },
|
||||
{ id: 28, name: 'learn.edutech.io', createdAt: '2024-12-22T13:31:00Z' },
|
||||
{ id: 29, name: 'retailmax.com', createdAt: '2024-12-21T10:45:00Z' },
|
||||
{ id: 30, name: 'www.retailmax.com', createdAt: '2024-12-21T10:46:00Z' },
|
||||
{ id: 31, name: 'm.retailmax.com', createdAt: '2024-12-21T10:47:00Z' },
|
||||
{ id: 32, name: 'api.retailmax.com', createdAt: '2024-12-21T10:48:00Z' },
|
||||
{ id: 33, name: 'cloudnine.host', createdAt: '2024-12-19T16:00:00Z' },
|
||||
{ id: 34, name: 'panel.cloudnine.host', createdAt: '2024-12-19T16:01:00Z' },
|
||||
{ id: 35, name: 'mediastream.tv', createdAt: '2024-12-18T09:30:00Z' },
|
||||
{ id: 36, name: 'www.mediastream.tv', createdAt: '2024-12-18T09:31:00Z' },
|
||||
{ id: 37, name: 'cdn.mediastream.tv', createdAt: '2024-12-18T09:32:00Z' },
|
||||
{ id: 38, name: 'stream.mediastream.tv', createdAt: '2024-12-18T09:33:00Z' },
|
||||
]
|
||||
|
||||
export function getMockSubdomains(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
search?: string
|
||||
organizationId?: number
|
||||
}): GetAllSubdomainsResponse {
|
||||
const page = params?.page || 1
|
||||
const pageSize = params?.pageSize || 10
|
||||
const search = params?.search?.toLowerCase() || ''
|
||||
|
||||
let filtered = mockSubdomains
|
||||
|
||||
if (search) {
|
||||
filtered = mockSubdomains.filter(sub =>
|
||||
sub.name.toLowerCase().includes(search)
|
||||
)
|
||||
}
|
||||
|
||||
const total = filtered.length
|
||||
const totalPages = Math.ceil(total / pageSize)
|
||||
const start = (page - 1) * pageSize
|
||||
const domains = filtered.slice(start, start + pageSize)
|
||||
|
||||
return {
|
||||
domains,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
}
|
||||
}
|
||||
|
||||
export function getMockSubdomainById(id: number): Subdomain | undefined {
|
||||
return mockSubdomains.find(sub => sub.id === id)
|
||||
}
|
||||
205
frontend/mock/data/targets.ts
Normal file
205
frontend/mock/data/targets.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import type { Target, TargetsResponse, TargetDetail } from '@/types/target.types'
|
||||
|
||||
export const mockTargets: Target[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'acme.com',
|
||||
type: 'domain',
|
||||
description: 'Acme Corporation 主站',
|
||||
createdAt: '2024-01-15T08:30:00Z',
|
||||
lastScannedAt: '2024-12-28T10:00:00Z',
|
||||
organizations: [{ id: 1, name: 'Acme Corporation' }],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'acme.io',
|
||||
type: 'domain',
|
||||
description: 'Acme Corporation 开发者平台',
|
||||
createdAt: '2024-01-16T09:00:00Z',
|
||||
lastScannedAt: '2024-12-27T14:30:00Z',
|
||||
organizations: [{ id: 1, name: 'Acme Corporation' }],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'techstart.io',
|
||||
type: 'domain',
|
||||
description: 'TechStart 官网',
|
||||
createdAt: '2024-02-20T10:15:00Z',
|
||||
lastScannedAt: '2024-12-26T08:45:00Z',
|
||||
organizations: [{ id: 2, name: 'TechStart Inc' }],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'globalfinance.com',
|
||||
type: 'domain',
|
||||
description: 'Global Finance 主站',
|
||||
createdAt: '2024-03-10T14:00:00Z',
|
||||
lastScannedAt: '2024-12-25T16:20:00Z',
|
||||
organizations: [{ id: 3, name: 'Global Finance Ltd' }],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '192.168.1.0/24',
|
||||
type: 'cidr',
|
||||
description: '内网 IP 段',
|
||||
createdAt: '2024-03-15T11:30:00Z',
|
||||
lastScannedAt: '2024-12-24T09:15:00Z',
|
||||
organizations: [{ id: 3, name: 'Global Finance Ltd' }],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'healthcareplus.com',
|
||||
type: 'domain',
|
||||
description: 'HealthCare Plus 官网',
|
||||
createdAt: '2024-04-05T09:20:00Z',
|
||||
lastScannedAt: '2024-12-23T11:00:00Z',
|
||||
organizations: [{ id: 4, name: 'HealthCare Plus' }],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'edutech.io',
|
||||
type: 'domain',
|
||||
description: 'EduTech 在线教育平台',
|
||||
createdAt: '2024-05-12T11:45:00Z',
|
||||
lastScannedAt: '2024-12-22T13:30:00Z',
|
||||
organizations: [{ id: 5, name: 'EduTech Solutions' }],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'retailmax.com',
|
||||
type: 'domain',
|
||||
description: 'RetailMax 电商主站',
|
||||
createdAt: '2024-06-08T16:30:00Z',
|
||||
lastScannedAt: '2024-12-21T10:45:00Z',
|
||||
organizations: [{ id: 6, name: 'RetailMax' }],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: '10.0.0.1',
|
||||
type: 'ip',
|
||||
description: '核心服务器 IP',
|
||||
createdAt: '2024-07-01T08:00:00Z',
|
||||
lastScannedAt: '2024-12-20T14:20:00Z',
|
||||
organizations: [{ id: 7, name: 'CloudNine Hosting' }],
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'cloudnine.host',
|
||||
type: 'domain',
|
||||
description: 'CloudNine 托管服务',
|
||||
createdAt: '2024-07-20T08:00:00Z',
|
||||
lastScannedAt: '2024-12-19T16:00:00Z',
|
||||
organizations: [{ id: 7, name: 'CloudNine Hosting' }],
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: 'mediastream.tv',
|
||||
type: 'domain',
|
||||
description: 'MediaStream 流媒体平台',
|
||||
createdAt: '2024-08-15T12:10:00Z',
|
||||
lastScannedAt: '2024-12-18T09:30:00Z',
|
||||
organizations: [{ id: 8, name: 'MediaStream Corp' }],
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: 'api.acme.com',
|
||||
type: 'domain',
|
||||
description: 'Acme API 服务',
|
||||
createdAt: '2024-09-01T10:00:00Z',
|
||||
lastScannedAt: '2024-12-17T11:15:00Z',
|
||||
organizations: [{ id: 1, name: 'Acme Corporation' }],
|
||||
},
|
||||
]
|
||||
|
||||
export const mockTargetDetails: Record<number, TargetDetail> = {
|
||||
1: {
|
||||
...mockTargets[0],
|
||||
summary: {
|
||||
subdomains: 156,
|
||||
websites: 89,
|
||||
endpoints: 2341,
|
||||
ips: 45,
|
||||
vulnerabilities: {
|
||||
total: 23,
|
||||
critical: 1,
|
||||
high: 4,
|
||||
medium: 8,
|
||||
low: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
2: {
|
||||
...mockTargets[1],
|
||||
summary: {
|
||||
subdomains: 78,
|
||||
websites: 45,
|
||||
endpoints: 892,
|
||||
ips: 23,
|
||||
vulnerabilities: {
|
||||
total: 12,
|
||||
critical: 0,
|
||||
high: 2,
|
||||
medium: 5,
|
||||
low: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export function getMockTargets(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
search?: string
|
||||
}): TargetsResponse {
|
||||
const page = params?.page || 1
|
||||
const pageSize = params?.pageSize || 10
|
||||
const search = params?.search?.toLowerCase() || ''
|
||||
|
||||
let filtered = mockTargets
|
||||
if (search) {
|
||||
filtered = mockTargets.filter(
|
||||
target =>
|
||||
target.name.toLowerCase().includes(search) ||
|
||||
target.description?.toLowerCase().includes(search)
|
||||
)
|
||||
}
|
||||
|
||||
const total = filtered.length
|
||||
const totalPages = Math.ceil(total / pageSize)
|
||||
const start = (page - 1) * pageSize
|
||||
const results = filtered.slice(start, start + pageSize)
|
||||
|
||||
return {
|
||||
results,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
}
|
||||
}
|
||||
|
||||
export function getMockTargetById(id: number): TargetDetail | undefined {
|
||||
if (mockTargetDetails[id]) {
|
||||
return mockTargetDetails[id]
|
||||
}
|
||||
const target = mockTargets.find(t => t.id === id)
|
||||
if (target) {
|
||||
return {
|
||||
...target,
|
||||
summary: {
|
||||
subdomains: Math.floor(Math.random() * 100) + 10,
|
||||
websites: Math.floor(Math.random() * 50) + 5,
|
||||
endpoints: Math.floor(Math.random() * 1000) + 100,
|
||||
ips: Math.floor(Math.random() * 30) + 5,
|
||||
vulnerabilities: {
|
||||
total: Math.floor(Math.random() * 20) + 1,
|
||||
critical: Math.floor(Math.random() * 2),
|
||||
high: Math.floor(Math.random() * 5),
|
||||
medium: Math.floor(Math.random() * 8),
|
||||
low: Math.floor(Math.random() * 10),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
275
frontend/mock/data/vulnerabilities.ts
Normal file
275
frontend/mock/data/vulnerabilities.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import type { Vulnerability, GetVulnerabilitiesResponse, VulnerabilitySeverity } from '@/types/vulnerability.types'
|
||||
|
||||
export const mockVulnerabilities: Vulnerability[] = [
|
||||
{
|
||||
id: 1,
|
||||
target: 1,
|
||||
url: 'https://acme.com/search?q=test',
|
||||
vulnType: 'xss-reflected',
|
||||
severity: 'critical',
|
||||
source: 'dalfox',
|
||||
cvssScore: 9.1,
|
||||
description: 'Reflected XSS in search parameter',
|
||||
rawOutput: {
|
||||
type: 'R',
|
||||
inject_type: 'inHTML-URL',
|
||||
method: 'GET',
|
||||
data: 'https://acme.com/search?q=<script>alert(1)</script>',
|
||||
param: 'q',
|
||||
payload: '<script>alert(1)</script>',
|
||||
evidence: '<script>alert(1)</script>',
|
||||
cwe: 'CWE-79',
|
||||
},
|
||||
createdAt: '2024-12-28T10:30:00Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
target: 1,
|
||||
url: 'https://api.acme.com/v1/users',
|
||||
vulnType: 'CVE-2024-1234',
|
||||
severity: 'high',
|
||||
source: 'nuclei',
|
||||
cvssScore: 8.5,
|
||||
description: 'SQL Injection in user API endpoint',
|
||||
rawOutput: {
|
||||
'template-id': 'CVE-2024-1234',
|
||||
'matched-at': 'https://api.acme.com/v1/users',
|
||||
host: 'api.acme.com',
|
||||
info: {
|
||||
name: 'SQL Injection',
|
||||
description: 'SQL injection vulnerability in user endpoint',
|
||||
severity: 'high',
|
||||
tags: ['sqli', 'cve'],
|
||||
reference: ['https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-1234'],
|
||||
classification: {
|
||||
'cve-id': 'CVE-2024-1234',
|
||||
'cwe-id': ['CWE-89'],
|
||||
},
|
||||
},
|
||||
},
|
||||
createdAt: '2024-12-28T10:45:00Z',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
target: 1,
|
||||
url: 'https://acme.com/login',
|
||||
vulnType: 'xss-stored',
|
||||
severity: 'high',
|
||||
source: 'dalfox',
|
||||
cvssScore: 8.2,
|
||||
description: 'Stored XSS in user profile',
|
||||
rawOutput: {
|
||||
type: 'S',
|
||||
inject_type: 'inHTML-TAG',
|
||||
method: 'POST',
|
||||
param: 'bio',
|
||||
payload: '<img src=x onerror=alert(1)>',
|
||||
},
|
||||
createdAt: '2024-12-27T14:20:00Z',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
target: 2,
|
||||
url: 'https://acme.io/api/config',
|
||||
vulnType: 'information-disclosure',
|
||||
severity: 'medium',
|
||||
source: 'nuclei',
|
||||
cvssScore: 5.3,
|
||||
description: 'Exposed configuration file',
|
||||
rawOutput: {
|
||||
'template-id': 'exposed-config',
|
||||
'matched-at': 'https://acme.io/api/config',
|
||||
host: 'acme.io',
|
||||
info: {
|
||||
name: 'Exposed Configuration',
|
||||
description: 'Configuration file accessible without authentication',
|
||||
severity: 'medium',
|
||||
tags: ['exposure', 'config'],
|
||||
},
|
||||
},
|
||||
createdAt: '2024-12-27T15:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
target: 3,
|
||||
url: 'https://techstart.io/admin',
|
||||
vulnType: 'open-redirect',
|
||||
severity: 'medium',
|
||||
source: 'nuclei',
|
||||
cvssScore: 4.7,
|
||||
description: 'Open redirect vulnerability',
|
||||
rawOutput: {
|
||||
'template-id': 'open-redirect',
|
||||
'matched-at': 'https://techstart.io/admin?redirect=evil.com',
|
||||
host: 'techstart.io',
|
||||
info: {
|
||||
name: 'Open Redirect',
|
||||
description: 'URL redirect without validation',
|
||||
severity: 'medium',
|
||||
tags: ['redirect'],
|
||||
},
|
||||
},
|
||||
createdAt: '2024-12-26T09:30:00Z',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
target: 4,
|
||||
url: 'https://globalfinance.com/.git/config',
|
||||
vulnType: 'git-config-exposure',
|
||||
severity: 'high',
|
||||
source: 'nuclei',
|
||||
cvssScore: 7.5,
|
||||
description: 'Git configuration file exposed',
|
||||
rawOutput: {
|
||||
'template-id': 'git-config',
|
||||
'matched-at': 'https://globalfinance.com/.git/config',
|
||||
host: 'globalfinance.com',
|
||||
info: {
|
||||
name: 'Git Config Exposure',
|
||||
description: 'Git configuration file is publicly accessible',
|
||||
severity: 'high',
|
||||
tags: ['git', 'exposure'],
|
||||
},
|
||||
},
|
||||
createdAt: '2024-12-25T11:15:00Z',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
target: 8,
|
||||
url: 'https://retailmax.com/product?id=1',
|
||||
vulnType: 'sqli',
|
||||
severity: 'critical',
|
||||
source: 'nuclei',
|
||||
cvssScore: 9.8,
|
||||
description: 'SQL Injection in product parameter',
|
||||
rawOutput: {
|
||||
'template-id': 'generic-sqli',
|
||||
'matched-at': "https://retailmax.com/product?id=1'",
|
||||
host: 'retailmax.com',
|
||||
info: {
|
||||
name: 'SQL Injection',
|
||||
description: 'SQL injection in product ID parameter',
|
||||
severity: 'critical',
|
||||
tags: ['sqli'],
|
||||
classification: {
|
||||
'cwe-id': ['CWE-89'],
|
||||
},
|
||||
},
|
||||
},
|
||||
createdAt: '2024-12-21T12:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
target: 1,
|
||||
url: 'https://acme.com/robots.txt',
|
||||
vulnType: 'robots-txt-exposure',
|
||||
severity: 'info',
|
||||
source: 'nuclei',
|
||||
description: 'Robots.txt file found',
|
||||
rawOutput: {
|
||||
'template-id': 'robots-txt',
|
||||
'matched-at': 'https://acme.com/robots.txt',
|
||||
host: 'acme.com',
|
||||
info: {
|
||||
name: 'Robots.txt',
|
||||
description: 'Robots.txt file detected',
|
||||
severity: 'info',
|
||||
tags: ['misc'],
|
||||
},
|
||||
},
|
||||
createdAt: '2024-12-28T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
target: 2,
|
||||
url: 'https://acme.io/sitemap.xml',
|
||||
vulnType: 'sitemap-exposure',
|
||||
severity: 'info',
|
||||
source: 'nuclei',
|
||||
description: 'Sitemap.xml file found',
|
||||
rawOutput: {
|
||||
'template-id': 'sitemap-xml',
|
||||
'matched-at': 'https://acme.io/sitemap.xml',
|
||||
host: 'acme.io',
|
||||
info: {
|
||||
name: 'Sitemap.xml',
|
||||
description: 'Sitemap.xml file detected',
|
||||
severity: 'info',
|
||||
tags: ['misc'],
|
||||
},
|
||||
},
|
||||
createdAt: '2024-12-27T14:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
target: 3,
|
||||
url: 'https://techstart.io/api/v2/debug',
|
||||
vulnType: 'debug-endpoint',
|
||||
severity: 'low',
|
||||
source: 'nuclei',
|
||||
cvssScore: 3.1,
|
||||
description: 'Debug endpoint exposed',
|
||||
rawOutput: {
|
||||
'template-id': 'debug-endpoint',
|
||||
'matched-at': 'https://techstart.io/api/v2/debug',
|
||||
host: 'techstart.io',
|
||||
info: {
|
||||
name: 'Debug Endpoint',
|
||||
description: 'Debug endpoint accessible in production',
|
||||
severity: 'low',
|
||||
tags: ['debug', 'exposure'],
|
||||
},
|
||||
},
|
||||
createdAt: '2024-12-26T10:00:00Z',
|
||||
},
|
||||
]
|
||||
|
||||
export function getMockVulnerabilities(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
targetId?: number
|
||||
severity?: VulnerabilitySeverity
|
||||
search?: string
|
||||
}): GetVulnerabilitiesResponse {
|
||||
const page = params?.page || 1
|
||||
const pageSize = params?.pageSize || 10
|
||||
const targetId = params?.targetId
|
||||
const severity = params?.severity
|
||||
const search = params?.search?.toLowerCase() || ''
|
||||
|
||||
let filtered = mockVulnerabilities
|
||||
|
||||
if (targetId) {
|
||||
filtered = filtered.filter(v => v.target === targetId)
|
||||
}
|
||||
|
||||
if (severity) {
|
||||
filtered = filtered.filter(v => v.severity === severity)
|
||||
}
|
||||
|
||||
if (search) {
|
||||
filtered = filtered.filter(
|
||||
v =>
|
||||
v.url.toLowerCase().includes(search) ||
|
||||
v.vulnType.toLowerCase().includes(search) ||
|
||||
v.description?.toLowerCase().includes(search)
|
||||
)
|
||||
}
|
||||
|
||||
const total = filtered.length
|
||||
const totalPages = Math.ceil(total / pageSize)
|
||||
const start = (page - 1) * pageSize
|
||||
const vulnerabilities = filtered.slice(start, start + pageSize)
|
||||
|
||||
return {
|
||||
vulnerabilities,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
}
|
||||
}
|
||||
|
||||
export function getMockVulnerabilityById(id: number): Vulnerability | undefined {
|
||||
return mockVulnerabilities.find(v => v.id === id)
|
||||
}
|
||||
252
frontend/mock/data/websites.ts
Normal file
252
frontend/mock/data/websites.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import type { WebSite, WebSiteListResponse } from '@/types/website.types'
|
||||
|
||||
export const mockWebsites: WebSite[] = [
|
||||
{
|
||||
id: 1,
|
||||
target: 1,
|
||||
url: 'https://acme.com',
|
||||
host: 'acme.com',
|
||||
location: '',
|
||||
title: 'Acme Corporation - Home',
|
||||
webserver: 'nginx/1.24.0',
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
statusCode: 200,
|
||||
contentLength: 45678,
|
||||
responseBody: '<!DOCTYPE html>...',
|
||||
tech: ['React', 'Next.js', 'Node.js', 'Tailwind CSS'],
|
||||
vhost: false,
|
||||
subdomain: 'acme.com',
|
||||
createdAt: '2024-12-28T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
target: 1,
|
||||
url: 'https://www.acme.com',
|
||||
host: 'www.acme.com',
|
||||
location: 'https://acme.com',
|
||||
title: 'Acme Corporation - Home',
|
||||
webserver: 'nginx/1.24.0',
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
statusCode: 301,
|
||||
contentLength: 0,
|
||||
responseBody: '',
|
||||
tech: [],
|
||||
vhost: false,
|
||||
subdomain: 'www.acme.com',
|
||||
createdAt: '2024-12-28T10:01:00Z',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
target: 1,
|
||||
url: 'https://api.acme.com',
|
||||
host: 'api.acme.com',
|
||||
location: '',
|
||||
title: 'Acme API',
|
||||
webserver: 'nginx/1.24.0',
|
||||
contentType: 'application/json',
|
||||
statusCode: 200,
|
||||
contentLength: 234,
|
||||
responseBody: '{"status":"ok","version":"1.0"}',
|
||||
tech: ['Django', 'Python', 'PostgreSQL'],
|
||||
vhost: false,
|
||||
subdomain: 'api.acme.com',
|
||||
createdAt: '2024-12-28T10:02:00Z',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
target: 1,
|
||||
url: 'https://admin.acme.com',
|
||||
host: 'admin.acme.com',
|
||||
location: '',
|
||||
title: 'Admin Panel - Acme',
|
||||
webserver: 'nginx/1.24.0',
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
statusCode: 200,
|
||||
contentLength: 23456,
|
||||
responseBody: '<!DOCTYPE html>...',
|
||||
tech: ['React', 'Ant Design'],
|
||||
vhost: false,
|
||||
subdomain: 'admin.acme.com',
|
||||
createdAt: '2024-12-28T10:03:00Z',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
target: 2,
|
||||
url: 'https://acme.io',
|
||||
host: 'acme.io',
|
||||
location: '',
|
||||
title: 'Acme Developer Platform',
|
||||
webserver: 'cloudflare',
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
statusCode: 200,
|
||||
contentLength: 56789,
|
||||
responseBody: '<!DOCTYPE html>...',
|
||||
tech: ['Vue.js', 'Vitepress', 'CloudFlare'],
|
||||
vhost: false,
|
||||
subdomain: 'acme.io',
|
||||
createdAt: '2024-12-27T14:30:00Z',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
target: 2,
|
||||
url: 'https://docs.acme.io',
|
||||
host: 'docs.acme.io',
|
||||
location: '',
|
||||
title: 'Documentation - Acme.io',
|
||||
webserver: 'cloudflare',
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
statusCode: 200,
|
||||
contentLength: 67890,
|
||||
responseBody: '<!DOCTYPE html>...',
|
||||
tech: ['Vue.js', 'Vitepress'],
|
||||
vhost: false,
|
||||
subdomain: 'docs.acme.io',
|
||||
createdAt: '2024-12-27T14:31:00Z',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
target: 3,
|
||||
url: 'https://techstart.io',
|
||||
host: 'techstart.io',
|
||||
location: '',
|
||||
title: 'TechStart - Innovation Hub',
|
||||
webserver: 'Apache/2.4.54',
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
statusCode: 200,
|
||||
contentLength: 34567,
|
||||
responseBody: '<!DOCTYPE html>...',
|
||||
tech: ['WordPress', 'PHP', 'MySQL'],
|
||||
vhost: false,
|
||||
subdomain: 'techstart.io',
|
||||
createdAt: '2024-12-26T08:45:00Z',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
target: 4,
|
||||
url: 'https://globalfinance.com',
|
||||
host: 'globalfinance.com',
|
||||
location: '',
|
||||
title: 'Global Finance - Your Financial Partner',
|
||||
webserver: 'Microsoft-IIS/10.0',
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
statusCode: 200,
|
||||
contentLength: 56789,
|
||||
responseBody: '<!DOCTYPE html>...',
|
||||
tech: ['ASP.NET', 'C#', 'jQuery', 'SQL Server'],
|
||||
vhost: false,
|
||||
subdomain: 'globalfinance.com',
|
||||
createdAt: '2024-12-25T16:20:00Z',
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
target: 6,
|
||||
url: 'https://healthcareplus.com',
|
||||
host: 'healthcareplus.com',
|
||||
location: '',
|
||||
title: 'HealthCare Plus - Digital Health',
|
||||
webserver: 'nginx/1.24.0',
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
statusCode: 200,
|
||||
contentLength: 45678,
|
||||
responseBody: '<!DOCTYPE html>...',
|
||||
tech: ['Angular', 'TypeScript', 'Node.js'],
|
||||
vhost: false,
|
||||
subdomain: 'healthcareplus.com',
|
||||
createdAt: '2024-12-23T11:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
target: 7,
|
||||
url: 'https://edutech.io',
|
||||
host: 'edutech.io',
|
||||
location: '',
|
||||
title: 'EduTech - Learn Anywhere',
|
||||
webserver: 'cloudflare',
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
statusCode: 200,
|
||||
contentLength: 67890,
|
||||
responseBody: '<!DOCTYPE html>...',
|
||||
tech: ['Vue.js', 'Nuxt.js', 'PostgreSQL'],
|
||||
vhost: false,
|
||||
subdomain: 'edutech.io',
|
||||
createdAt: '2024-12-22T13:30:00Z',
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
target: 8,
|
||||
url: 'https://retailmax.com',
|
||||
host: 'retailmax.com',
|
||||
location: '',
|
||||
title: 'RetailMax - Shop Everything',
|
||||
webserver: 'nginx/1.22.0',
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
statusCode: 200,
|
||||
contentLength: 89012,
|
||||
responseBody: '<!DOCTYPE html>...',
|
||||
tech: ['React', 'Redux', 'Node.js', 'MongoDB'],
|
||||
vhost: false,
|
||||
subdomain: 'retailmax.com',
|
||||
createdAt: '2024-12-21T10:45:00Z',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
target: 10,
|
||||
url: 'https://cloudnine.host',
|
||||
host: 'cloudnine.host',
|
||||
location: '',
|
||||
title: 'CloudNine Hosting',
|
||||
webserver: 'LiteSpeed',
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
statusCode: 200,
|
||||
contentLength: 34567,
|
||||
responseBody: '<!DOCTYPE html>...',
|
||||
tech: ['PHP', 'Laravel', 'MySQL'],
|
||||
vhost: false,
|
||||
subdomain: 'cloudnine.host',
|
||||
createdAt: '2024-12-19T16:00:00Z',
|
||||
},
|
||||
]
|
||||
|
||||
export function getMockWebsites(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
search?: string
|
||||
targetId?: number
|
||||
}): WebSiteListResponse {
|
||||
const page = params?.page || 1
|
||||
const pageSize = params?.pageSize || 10
|
||||
const search = params?.search?.toLowerCase() || ''
|
||||
const targetId = params?.targetId
|
||||
|
||||
let filtered = mockWebsites
|
||||
|
||||
if (targetId) {
|
||||
filtered = filtered.filter(w => w.target === targetId)
|
||||
}
|
||||
|
||||
if (search) {
|
||||
filtered = filtered.filter(
|
||||
w =>
|
||||
w.url.toLowerCase().includes(search) ||
|
||||
w.title.toLowerCase().includes(search) ||
|
||||
w.host.toLowerCase().includes(search)
|
||||
)
|
||||
}
|
||||
|
||||
const total = filtered.length
|
||||
const totalPages = Math.ceil(total / pageSize)
|
||||
const start = (page - 1) * pageSize
|
||||
const results = filtered.slice(start, start + pageSize)
|
||||
|
||||
return {
|
||||
results,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
}
|
||||
}
|
||||
|
||||
export function getMockWebsiteById(id: number): WebSite | undefined {
|
||||
return mockWebsites.find(w => w.id === id)
|
||||
}
|
||||
71
frontend/mock/index.ts
Normal file
71
frontend/mock/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Mock 数据统一导出
|
||||
*
|
||||
* 使用方式:
|
||||
* import { USE_MOCK, mockData } from '@/mock'
|
||||
*
|
||||
* if (USE_MOCK) {
|
||||
* return mockData.dashboard.assetStatistics
|
||||
* }
|
||||
*/
|
||||
|
||||
export { USE_MOCK, MOCK_DELAY, mockDelay } from './config'
|
||||
|
||||
// Dashboard
|
||||
export {
|
||||
mockDashboardStats,
|
||||
mockAssetStatistics,
|
||||
mockStatisticsHistory7Days,
|
||||
mockStatisticsHistory30Days,
|
||||
getMockStatisticsHistory,
|
||||
} from './data/dashboard'
|
||||
|
||||
// Organizations
|
||||
export {
|
||||
mockOrganizations,
|
||||
getMockOrganizations,
|
||||
} from './data/organizations'
|
||||
|
||||
// Targets
|
||||
export {
|
||||
mockTargets,
|
||||
mockTargetDetails,
|
||||
getMockTargets,
|
||||
getMockTargetById,
|
||||
} from './data/targets'
|
||||
|
||||
// Scans
|
||||
export {
|
||||
mockScans,
|
||||
mockScanStatistics,
|
||||
getMockScans,
|
||||
getMockScanById,
|
||||
} from './data/scans'
|
||||
|
||||
// Vulnerabilities
|
||||
export {
|
||||
mockVulnerabilities,
|
||||
getMockVulnerabilities,
|
||||
getMockVulnerabilityById,
|
||||
} from './data/vulnerabilities'
|
||||
|
||||
// Endpoints
|
||||
export {
|
||||
mockEndpoints,
|
||||
getMockEndpoints,
|
||||
getMockEndpointById,
|
||||
} from './data/endpoints'
|
||||
|
||||
// Websites
|
||||
export {
|
||||
mockWebsites,
|
||||
getMockWebsites,
|
||||
getMockWebsiteById,
|
||||
} from './data/websites'
|
||||
|
||||
// Subdomains
|
||||
export {
|
||||
mockSubdomains,
|
||||
getMockSubdomains,
|
||||
getMockSubdomainById,
|
||||
} from './data/subdomains'
|
||||
@@ -3,9 +3,12 @@ import createNextIntlPlugin from 'next-intl/plugin';
|
||||
|
||||
const withNextIntl = createNextIntlPlugin('./i18n/request.ts');
|
||||
|
||||
// Check if running on Vercel
|
||||
const isVercel = process.env.VERCEL === '1';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
// Use standalone mode for Docker deployment
|
||||
output: 'standalone',
|
||||
// Use standalone mode for Docker deployment (not needed on Vercel)
|
||||
...(isVercel ? {} : { output: 'standalone' }),
|
||||
// Disable Next.js automatic add/remove trailing slash behavior
|
||||
// Let us manually control URL format
|
||||
skipTrailingSlashRedirect: true,
|
||||
@@ -17,6 +20,10 @@ const nextConfig: NextConfig = {
|
||||
allowedDevOrigins: ['192.168.*.*', '10.*.*.*', '172.16.*.*'],
|
||||
|
||||
async rewrites() {
|
||||
// Skip rewrites on Vercel when using mock data
|
||||
if (isVercel) {
|
||||
return [];
|
||||
}
|
||||
// Use server service name in Docker environment, localhost for local development
|
||||
const apiHost = process.env.API_HOST || 'localhost';
|
||||
return [
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"dev:mock": "NEXT_PUBLIC_USE_MOCK=true next dev --turbopack",
|
||||
"dev:noauth": "NEXT_PUBLIC_SKIP_AUTH=true next dev --turbopack",
|
||||
"build": "next build --turbopack",
|
||||
"start": "next start",
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { DashboardStats, AssetStatistics, StatisticsHistoryItem } from '@/types/dashboard.types'
|
||||
import { USE_MOCK, mockDelay, mockDashboardStats, mockAssetStatistics, getMockStatisticsHistory } from '@/mock'
|
||||
|
||||
export async function getDashboardStats(): Promise<DashboardStats> {
|
||||
if (USE_MOCK) {
|
||||
await mockDelay()
|
||||
return mockDashboardStats
|
||||
}
|
||||
const res = await api.get<DashboardStats>('/dashboard/stats/')
|
||||
return res.data
|
||||
}
|
||||
@@ -10,6 +15,10 @@ export async function getDashboardStats(): Promise<DashboardStats> {
|
||||
* Get asset statistics data (pre-aggregated)
|
||||
*/
|
||||
export async function getAssetStatistics(): Promise<AssetStatistics> {
|
||||
if (USE_MOCK) {
|
||||
await mockDelay()
|
||||
return mockAssetStatistics
|
||||
}
|
||||
const res = await api.get<AssetStatistics>('/assets/statistics/')
|
||||
return res.data
|
||||
}
|
||||
@@ -18,6 +27,10 @@ export async function getAssetStatistics(): Promise<AssetStatistics> {
|
||||
* Get statistics history data (for line charts)
|
||||
*/
|
||||
export async function getStatisticsHistory(days: number = 7): Promise<StatisticsHistoryItem[]> {
|
||||
if (USE_MOCK) {
|
||||
await mockDelay()
|
||||
return getMockStatisticsHistory(days)
|
||||
}
|
||||
const res = await api.get<StatisticsHistoryItem[]>('/assets/statistics/history/', {
|
||||
params: { days }
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
BatchDeleteEndpointsRequest,
|
||||
BatchDeleteEndpointsResponse
|
||||
} from "@/types/endpoint.types"
|
||||
import { USE_MOCK, mockDelay, getMockEndpoints, getMockEndpointById } from '@/mock'
|
||||
|
||||
// Bulk create endpoints response type
|
||||
export interface BulkCreateEndpointsResponse {
|
||||
@@ -38,6 +39,12 @@ export class EndpointService {
|
||||
* @returns Promise<Endpoint>
|
||||
*/
|
||||
static async getEndpointById(id: number): Promise<Endpoint> {
|
||||
if (USE_MOCK) {
|
||||
await mockDelay()
|
||||
const endpoint = getMockEndpointById(id)
|
||||
if (!endpoint) throw new Error('Endpoint not found')
|
||||
return endpoint
|
||||
}
|
||||
const response = await api.get<Endpoint>(`/endpoints/${id}/`)
|
||||
return response.data
|
||||
}
|
||||
@@ -48,6 +55,10 @@ export class EndpointService {
|
||||
* @returns Promise<GetEndpointsResponse>
|
||||
*/
|
||||
static async getEndpoints(params: GetEndpointsRequest): Promise<GetEndpointsResponse> {
|
||||
if (USE_MOCK) {
|
||||
await mockDelay()
|
||||
return getMockEndpoints(params)
|
||||
}
|
||||
// api-client.ts automatically converts camelCase params to snake_case
|
||||
const response = await api.get<GetEndpointsResponse>('/endpoints/', {
|
||||
params
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { api } from "@/lib/api-client"
|
||||
import type { Organization, OrganizationsResponse } from "@/types/organization.types"
|
||||
import { USE_MOCK, mockDelay, getMockOrganizations, mockOrganizations } from '@/mock'
|
||||
|
||||
|
||||
export class OrganizationService {
|
||||
@@ -18,6 +19,10 @@ export class OrganizationService {
|
||||
pageSize?: number
|
||||
search?: string
|
||||
}): Promise<OrganizationsResponse<Organization>> {
|
||||
if (USE_MOCK) {
|
||||
await mockDelay()
|
||||
return getMockOrganizations(params)
|
||||
}
|
||||
const response = await api.get<OrganizationsResponse<Organization>>(
|
||||
'/organizations/',
|
||||
{ params }
|
||||
@@ -31,6 +36,12 @@ export class OrganizationService {
|
||||
* @returns Promise<Organization>
|
||||
*/
|
||||
static async getOrganizationById(id: string | number): Promise<Organization> {
|
||||
if (USE_MOCK) {
|
||||
await mockDelay()
|
||||
const org = mockOrganizations.find(o => o.id === Number(id))
|
||||
if (!org) throw new Error('Organization not found')
|
||||
return org
|
||||
}
|
||||
const response = await api.get<Organization>(`/organizations/${id}/`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
@@ -8,11 +8,16 @@ import type {
|
||||
QuickScanResponse,
|
||||
ScanRecord
|
||||
} from '@/types/scan.types'
|
||||
import { USE_MOCK, mockDelay, getMockScans, getMockScanById, mockScanStatistics } from '@/mock'
|
||||
|
||||
/**
|
||||
* Get scan list
|
||||
*/
|
||||
export async function getScans(params?: GetScansParams): Promise<GetScansResponse> {
|
||||
if (USE_MOCK) {
|
||||
await mockDelay()
|
||||
return getMockScans(params)
|
||||
}
|
||||
const res = await api.get<GetScansResponse>('/scans/', { params })
|
||||
return res.data
|
||||
}
|
||||
@@ -23,6 +28,12 @@ export async function getScans(params?: GetScansParams): Promise<GetScansRespons
|
||||
* @returns Scan details
|
||||
*/
|
||||
export async function getScan(id: number): Promise<ScanRecord> {
|
||||
if (USE_MOCK) {
|
||||
await mockDelay()
|
||||
const scan = getMockScanById(id)
|
||||
if (!scan) throw new Error('Scan not found')
|
||||
return scan
|
||||
}
|
||||
const res = await api.get<ScanRecord>(`/scans/${id}/`)
|
||||
return res.data
|
||||
}
|
||||
@@ -95,6 +106,10 @@ export interface ScanStatistics {
|
||||
* @returns Statistics data
|
||||
*/
|
||||
export async function getScanStatistics(): Promise<ScanStatistics> {
|
||||
if (USE_MOCK) {
|
||||
await mockDelay()
|
||||
return mockScanStatistics
|
||||
}
|
||||
const res = await api.get<ScanStatistics>('/scans/statistics/')
|
||||
return res.data
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { api } from "@/lib/api-client"
|
||||
import type { Subdomain, GetSubdomainsParams, GetSubdomainsResponse, GetAllSubdomainsParams, GetAllSubdomainsResponse, GetSubdomainByIDResponse, BatchCreateSubdomainsResponse } from "@/types/subdomain.types"
|
||||
import { USE_MOCK, mockDelay, getMockSubdomains, getMockSubdomainById } from '@/mock'
|
||||
|
||||
// Bulk create subdomains response type
|
||||
export interface BulkCreateSubdomainsResponse {
|
||||
@@ -48,6 +49,12 @@ export class SubdomainService {
|
||||
* Get single subdomain details
|
||||
*/
|
||||
static async getSubdomainById(id: string | number): Promise<GetSubdomainByIDResponse> {
|
||||
if (USE_MOCK) {
|
||||
await mockDelay()
|
||||
const subdomain = getMockSubdomainById(Number(id))
|
||||
if (!subdomain) throw new Error('Subdomain not found')
|
||||
return subdomain
|
||||
}
|
||||
const response = await api.get<GetSubdomainByIDResponse>(`/domains/${id}/`)
|
||||
return response.data
|
||||
}
|
||||
@@ -164,6 +171,10 @@ export class SubdomainService {
|
||||
|
||||
/** Get all subdomains list (server-side pagination) */
|
||||
static async getAllSubdomains(params?: GetAllSubdomainsParams): Promise<GetAllSubdomainsResponse> {
|
||||
if (USE_MOCK) {
|
||||
await mockDelay()
|
||||
return getMockSubdomains(params)
|
||||
}
|
||||
const response = await api.get<GetAllSubdomainsResponse>('/domains/', {
|
||||
params: {
|
||||
page: params?.page || 1,
|
||||
|
||||
@@ -12,11 +12,16 @@ import type {
|
||||
BatchCreateTargetsRequest,
|
||||
BatchCreateTargetsResponse,
|
||||
} from '@/types/target.types'
|
||||
import { USE_MOCK, mockDelay, getMockTargets, getMockTargetById } from '@/mock'
|
||||
|
||||
/**
|
||||
* Get all targets list (paginated)
|
||||
*/
|
||||
export async function getTargets(page = 1, pageSize = 10, search?: string): Promise<TargetsResponse> {
|
||||
if (USE_MOCK) {
|
||||
await mockDelay()
|
||||
return getMockTargets({ page, pageSize, search })
|
||||
}
|
||||
const response = await api.get<TargetsResponse>('/targets/', {
|
||||
params: {
|
||||
page,
|
||||
@@ -31,6 +36,12 @@ export async function getTargets(page = 1, pageSize = 10, search?: string): Prom
|
||||
* Get single target details
|
||||
*/
|
||||
export async function getTargetById(id: number): Promise<Target> {
|
||||
if (USE_MOCK) {
|
||||
await mockDelay()
|
||||
const target = getMockTargetById(id)
|
||||
if (!target) throw new Error('Target not found')
|
||||
return target
|
||||
}
|
||||
const response = await api.get<Target>(`/targets/${id}/`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { api } from "@/lib/api-client"
|
||||
import type { GetVulnerabilitiesParams } from "@/types/vulnerability.types"
|
||||
import type { GetVulnerabilitiesParams, Vulnerability } from "@/types/vulnerability.types"
|
||||
import { USE_MOCK, mockDelay, getMockVulnerabilities } from '@/mock'
|
||||
|
||||
export class VulnerabilityService {
|
||||
/** Get all vulnerabilities list (used by global vulnerabilities page) */
|
||||
@@ -7,12 +8,22 @@ export class VulnerabilityService {
|
||||
params: GetVulnerabilitiesParams,
|
||||
filter?: string,
|
||||
): Promise<any> {
|
||||
if (USE_MOCK) {
|
||||
await mockDelay()
|
||||
return getMockVulnerabilities(params)
|
||||
}
|
||||
const response = await api.get(`/assets/vulnerabilities/`, {
|
||||
params: { ...params, filter },
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** Get single vulnerability by ID */
|
||||
static async getVulnerabilityById(id: number): Promise<Vulnerability> {
|
||||
const response = await api.get(`/assets/vulnerabilities/${id}/`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** Get vulnerability snapshot list by scan task (used by scan history page) */
|
||||
static async getVulnerabilitiesByScanId(
|
||||
scanId: number,
|
||||
|
||||
@@ -11,9 +11,9 @@ export interface SearchResult {
|
||||
}
|
||||
|
||||
export interface Vulnerability {
|
||||
id?: number
|
||||
name: string
|
||||
severity: 'critical' | 'high' | 'medium' | 'low' | 'info'
|
||||
source: string
|
||||
severity: 'critical' | 'high' | 'medium' | 'low' | 'info' | 'unknown'
|
||||
vulnType: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
9
frontend/vercel.json
Normal file
9
frontend/vercel.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"framework": "nextjs",
|
||||
"buildCommand": "pnpm build",
|
||||
"installCommand": "pnpm install",
|
||||
"env": {
|
||||
"NEXT_PUBLIC_USE_MOCK": "true"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user