diff --git a/backend/apps/asset/migrations/0002_create_search_materialized_view.py b/backend/apps/asset/migrations/0002_create_search_materialized_view.py index 67c6295f..da46d345 100644 --- a/backend/apps/asset/migrations/0002_create_search_materialized_view.py +++ b/backend/apps/asset/migrations/0002_create_search_materialized_view.py @@ -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 diff --git a/backend/apps/asset/services/search_service.py b/backend/apps/asset/services/search_service.py index 396df971..3f4a694a 100644 --- a/backend/apps/asset/services/search_service.py +++ b/backend/apps/asset/services/search_service.py @@ -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) diff --git a/backend/apps/asset/views/search_views.py b/backend/apps/asset/views/search_views.py index a7aed07c..93b0a8cb 100644 --- a/backend/apps/asset/views/search_views.py +++ b/backend/apps/asset/views/search_views.py @@ -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={ diff --git a/frontend/components/search/search-page.tsx b/frontend/components/search/search-page.tsx index 4e49fcff..40443d8d 100644 --- a/frontend/components/search/search-page.tsx +++ b/frontend/components/search/search-page.tsx @@ -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("initial") @@ -67,6 +48,9 @@ export function SearchPage() { const [searchParams, setSearchParams] = useState({}) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(10) + const [selectedVuln, setSelectedVuln] = useState(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 (
@@ -233,7 +224,10 @@ export function SearchPage() { animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3, delay: index * 0.05 }} > - + ))}
@@ -257,6 +251,13 @@ export function SearchPage() { )} + + {/* 漏洞详情弹窗 - 复用现有组件 */} + ) } diff --git a/frontend/components/search/search-result-card.tsx b/frontend/components/search/search-result-card.tsx index 5a895ad9..d2c6f0db 100644 --- a/frontend/components/search/search-result-card.tsx +++ b/frontend/components/search/search-result-card.tsx @@ -190,10 +190,9 @@ export function SearchResultCard({ result, onViewVulnerability }: SearchResultCa - {t('vulnName')} - {t('severity')} - {t('source')} + {t('vulnName')} {t('vulnType')} + {t('severity')} @@ -212,19 +211,6 @@ export function SearchResultCard({ result, onViewVulnerability }: SearchResultCa - - - {vuln.severity} - - - - - {vuln.source} - - @@ -237,6 +223,14 @@ export function SearchResultCard({ result, onViewVulnerability }: SearchResultCa + + + {vuln.severity} + +