feat: update web and vuln list support filter

This commit is contained in:
fan-tastic-z
2025-08-27 11:52:29 +08:00
parent fd1f644c41
commit f72f5bd688
10 changed files with 231 additions and 57 deletions

View File

@@ -48,12 +48,15 @@ export const login = (username, password) => {
}
// 获取漏洞列表
export const getVulnerabilities = (pageNo, pageSize, searchTerm = '') => {
export const getVulnerabilities = (params) => {
return api.get('/vulns', {
params: {
page_no: pageNo,
page_size: pageSize,
search: searchTerm,
page_no: params.pageNo,
page_size: params.pageSize,
cve: params.cve,
title: params.title,
pushed: params.pushed,
source: params.source,
},
})
}

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { getVulnerabilities } from '../../lib/api'
import { getVulnerabilities, getPlugins } from '../../lib/api'
const VulnerabilityListPage = () => {
const [vulnerabilities, setVulnerabilities] = useState([])
@@ -9,18 +9,50 @@ const VulnerabilityListPage = () => {
const [pageNo, setPageNo] = useState(1)
const [pageSize] = useState(10)
const [totalCount, setTotalCount] = useState(0)
const [searchTerm, setSearchTerm] = useState('')
const [plugins, setPlugins] = useState([])
// 新的筛选条件状态
const [filters, setFilters] = useState({
cve: '',
title: '',
pushed: '',
source: '' // 存储插件的link字段
})
useEffect(() => {
fetchPlugins()
}, [])
useEffect(() => {
fetchVulnerabilities()
}, [pageNo, searchTerm])
}, [pageNo, filters])
const fetchPlugins = async () => {
try {
const response = await getPlugins()
setPlugins(response.data.data)
} catch (err) {
console.error('获取插件列表失败:', err)
}
}
const fetchVulnerabilities = async () => {
setLoading(true)
setError('')
try {
const response = await getVulnerabilities(pageNo, pageSize, searchTerm)
const params = {
pageNo,
pageSize,
...filters
}
// 清理空值参数
Object.keys(params).forEach(key => {
if (params[key] === '' || params[key] === undefined) {
delete params[key]
}
})
const response = await getVulnerabilities(params)
const { data, total_count } = response.data.data
setVulnerabilities(data)
setTotalCount(total_count)
@@ -35,9 +67,34 @@ const VulnerabilityListPage = () => {
const handlePageChange = (newPageNo) => {
setPageNo(newPageNo)
}
const handleFilterChange = (e) => {
const { name, value } = e.target
setFilters(prev => ({
...prev,
[name]: value
}))
// 重置页码到第一页
setPageNo(1)
}
const handleSearch = (e) => {
e.preventDefault()
const handleSourceFilterChange = (e) => {
const selectedLink = e.target.value
setFilters(prev => ({
...prev,
source: selectedLink
}))
// 重置页码到第一页
setPageNo(1)
}
const handleResetFilters = () => {
setFilters({
cve: '',
title: '',
pushed: '',
source: ''
})
// 重置页码到第一页
setPageNo(1)
}
@@ -55,27 +112,81 @@ const VulnerabilityListPage = () => {
</p>
</div>
{/* 搜索框 */}
{/* 筛选控件 */}
<div className="mb-6">
<form onSubmit={handleSearch} className="max-w-md mx-auto">
<div className="relative">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
<div>
<label htmlFor="cve" className="block text-sm font-medium text-gray-700">
CVE编号
</label>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="搜索漏洞标题或描述..."
name="cve"
id="cve"
value={filters.cve}
onChange={handleFilterChange}
placeholder="搜索CVE编号..."
className="block w-full py-2 pl-4 pr-12 leading-5 placeholder-gray-500 bg-white border border-gray-300 rounded-md focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
<button
type="submit"
className="absolute inset-y-0 right-0 flex items-center pr-3"
>
<svg className="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clipRule="evenodd" />
</svg>
</button>
</div>
</form>
<div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700">
漏洞标题
</label>
<input
type="text"
name="title"
id="title"
value={filters.title}
onChange={handleFilterChange}
placeholder="搜索漏洞标题..."
className="block w-full py-2 pl-4 pr-12 leading-5 placeholder-gray-500 bg-white border border-gray-300 rounded-md focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</div>
<div>
<label htmlFor="pushed" className="block text-sm font-medium text-gray-700">
推送状态
</label>
<select
name="pushed"
id="pushed"
value={filters.pushed}
onChange={handleFilterChange}
className="block w-full py-2 pl-4 pr-12 leading-5 placeholder-gray-500 bg-white border border-gray-300 rounded-md focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
>
<option value="">全部</option>
<option value="true">已推送</option>
<option value="false">未推送</option>
</select>
</div>
<div>
<label htmlFor="source" className="block text-sm font-medium text-gray-700">
来源
</label>
<select
name="source"
id="source"
value={filters.source}
onChange={handleSourceFilterChange}
className="block w-full py-2 pl-4 pr-12 leading-5 placeholder-gray-500 bg-white border border-gray-300 rounded-md focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
>
<option value="">全部</option>
{plugins.map((plugin) => (
<option key={plugin.link} value={plugin.link}>
{plugin.display_name}
</option>
))}
</select>
</div>
</div>
<div className="flex justify-end mt-4">
<button
onClick={handleResetFilters}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
重置筛选
</button>
</div>
</div>
{error && (

View File

@@ -19,4 +19,4 @@ dir = "logs"
max_files = 64
[telemetry.logs.stderr]
filter = "INFO"
filter = "DEBUG"

View File

@@ -60,14 +60,45 @@ impl fmt::Display for Severity {
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ListVulnInformationRequest {
pub page_filter: PageFilter,
pub search: Option<String>,
pub search_params: SearchParams,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct SearchParams {
pub cve: Option<String>,
pub title: Option<String>,
pub pushed: Option<bool>,
pub source: Option<String>,
}
impl SearchParams {
pub fn new() -> Self {
SearchParams::default()
}
pub fn with_cve(mut self, cve: Option<String>) -> Self {
self.cve = cve;
self
}
pub fn with_title(mut self, title: Option<String>) -> Self {
self.title = title;
self
}
pub fn with_pushed(mut self, pushed: Option<bool>) -> Self {
self.pushed = pushed;
self
}
pub fn with_source(mut self, source: Option<String>) -> Self {
self.source = source;
self
}
}
impl ListVulnInformationRequest {
pub fn new(page_filter: PageFilter, search: Option<String>) -> Self {
pub fn new(page_filter: PageFilter, search_params: SearchParams) -> Self {
ListVulnInformationRequest {
page_filter,
search,
search_params,
}
}
}

View File

@@ -45,7 +45,7 @@ pub trait VulnRepository: Clone + Send + Sync + 'static {
&self,
) -> impl Future<Output = Result<Option<SyncDataTask>, Error>> + Send;
fn list_vulnfusion_information(
fn list_vuln_information(
&self,
req: ListVulnInformationRequest,
) -> impl Future<Output = Result<ListVulnInformationResponseData, Error>> + Send;

View File

@@ -54,7 +54,7 @@ where
&self,
req: ListVulnInformationRequest,
) -> Result<ListVulnInformationResponseData, Error> {
let ret = self.repo.list_vulnfusion_information(req).await?; // Implement the logic here
let ret = self.repo.list_vuln_information(req).await?; // Implement the logic here
Ok(ret)
}

View File

@@ -14,7 +14,7 @@ use crate::{
page_utils::{PageFilter, PageNo, PageNoError, PageSize, PageSizeError},
vuln_information::{
GetVulnInformationRequest, ListVulnInformationRequest,
ListVulnInformationResponseData, VulnInformation,
ListVulnInformationResponseData, SearchParams, VulnInformation,
},
},
ports::VulnService,
@@ -26,7 +26,10 @@ use crate::{
pub struct ListVulnInformationRequestBody {
pub page_no: i32,
pub page_size: i32,
pub search: Option<String>,
pub cve: Option<String>,
pub title: Option<String>,
pub pushed: Option<bool>,
pub source: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
@@ -41,7 +44,12 @@ impl ListVulnInformationRequestBody {
let page_no = PageNo::try_new(self.page_no)?;
let page_size = PageSize::try_new(self.page_size)?;
let page_filter = PageFilter::new(page_no, page_size);
Ok(ListVulnInformationRequest::new(page_filter, self.search))
let search_params = SearchParams::new()
.with_cve(self.cve)
.with_pushed(self.pushed)
.with_source(self.source)
.with_title(self.title);
Ok(ListVulnInformationRequest::new(page_filter, search_params))
}
}

View File

@@ -266,6 +266,13 @@ impl<D: Dao> DaoQueryBuilder<D> {
self
}
pub fn and_where_bool(mut self, column: &str, value: bool) -> Self {
let condition = Expr::col(Alias::new(column)).eq(value);
self.query.and_where(condition.clone());
self.conditions.push(Condition::all().add(condition));
self
}
pub fn order_by_desc(mut self, column: &str) -> Self {
self.query
.order_by(Alias::new(column), sea_query::Order::Desc);

View File

@@ -63,7 +63,7 @@ impl VulnRepository for Pg {
Ok(sync_data_task)
}
async fn list_vulnfusion_information(
async fn list_vuln_information(
&self,
req: ListVulnInformationRequest,
) -> Result<ListVulnInformationResponseData, Error> {
@@ -74,11 +74,11 @@ impl VulnRepository for Pg {
let vuln_informations = VulnInformationDao::filter_vulnfusion_information(
&mut tx,
&req.page_filter,
req.search.as_deref(),
&req.search_params,
)
.await?;
let count =
VulnInformationDao::filter_vulnfusion_information_count(&mut tx, req.search.as_deref())
VulnInformationDao::filter_vulnfusion_information_count(&mut tx, &req.search_params)
.await?;
tx.commit()

View File

@@ -4,7 +4,7 @@ use sqlx::{Postgres, Transaction};
use crate::{
domain::models::{
page_utils::PageFilter,
vuln_information::{CreateVulnInformation, VulnInformation},
vuln_information::{CreateVulnInformation, SearchParams, VulnInformation},
},
errors::Error,
output::db::base::{
@@ -106,22 +106,29 @@ impl VulnInformationDao {
pub async fn filter_vulnfusion_information(
tx: &mut Transaction<'_, Postgres>,
page_filter: &PageFilter,
search: Option<&str>,
search_params: &SearchParams,
) -> Result<Vec<VulnInformation>, Error> {
let mut query_builder = DaoQueryBuilder::<Self>::new();
if let Some(title) = &search_params.title {
query_builder = query_builder.and_where_like("title", title);
}
if let Some(source) = &search_params.source {
query_builder = query_builder.and_where_like("source", source);
}
if let Some(pushed) = &search_params.pushed {
query_builder = query_builder.and_where_bool("pushed", *pushed);
}
if let Some(cve) = &search_params.cve {
query_builder = query_builder.and_where_like("cve", cve);
}
let page_no = *page_filter.page_no().as_ref();
let page_size = *page_filter.page_size().as_ref();
let offset = (page_no - 1) * page_size;
// 添加搜索条件
if let Some(search_term) = search
&& !search_term.is_empty()
{
query_builder = query_builder
.and_where_like("title", search_term)
.and_where_like("description", search_term);
}
query_builder
.order_by_desc("updated_at")
.limit_offset(page_size as i64, offset as i64)
@@ -131,17 +138,24 @@ impl VulnInformationDao {
pub async fn filter_vulnfusion_information_count(
tx: &mut Transaction<'_, Postgres>,
search: Option<&str>,
search_params: &SearchParams,
) -> Result<i64, Error> {
let mut query_builder = DaoQueryBuilder::<Self>::new();
// 添加搜索条件
if let Some(search_term) = search
&& !search_term.is_empty()
{
query_builder = query_builder
.and_where_like("title", search_term)
.and_where_like("description", search_term);
if let Some(title) = &search_params.title {
query_builder = query_builder.and_where_like("title", title);
}
if let Some(source) = &search_params.source {
query_builder = query_builder.and_where_like("source", source);
}
if let Some(pushed) = &search_params.pushed {
query_builder = query_builder.and_where_bool("pushed", *pushed);
}
if let Some(cve) = &search_params.cve {
query_builder = query_builder.and_where_like("cve", cve);
}
query_builder.count(tx).await