Feat support yonyou security notice (#9)
This commit is contained in:
@@ -6,6 +6,7 @@ import VulnerabilityDetailPage from './routes/vuln/detail'
|
||||
import SyncDataTaskPage from './routes/sync/task'
|
||||
import PluginListPage from './routes/plugins/list'
|
||||
import DingBotConfigPage from './routes/dingbot/config'
|
||||
import SecNoticeListPage from './routes/secnotice/list'
|
||||
import ProtectedRoute from './components/ProtectedRoute'
|
||||
import Layout from './components/Layout'
|
||||
|
||||
@@ -74,6 +75,16 @@ function App() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/secnotice"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Layout>
|
||||
<SecNoticeListPage />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -58,6 +58,16 @@ const Navigation = () => {
|
||||
>
|
||||
漏洞列表
|
||||
</Link>
|
||||
<Link
|
||||
to="/secnotice"
|
||||
className={`${
|
||||
isActive('/secnotice')
|
||||
? 'border-indigo-500 text-gray-900'
|
||||
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'
|
||||
} inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium`}
|
||||
>
|
||||
安全公告
|
||||
</Link>
|
||||
<Link
|
||||
to="/plugins"
|
||||
className={`${
|
||||
|
||||
@@ -91,4 +91,17 @@ export const createOrUpdateDingBotConfig = (data) => {
|
||||
return api.post('/ding_bot_config', data)
|
||||
}
|
||||
|
||||
// 获取安全公告列表
|
||||
export const getSecNotices = (params) => {
|
||||
return api.get('/sec_notice', {
|
||||
params: {
|
||||
page_no: params.pageNo,
|
||||
page_size: params.pageSize,
|
||||
title: params.title,
|
||||
pushed: params.pushed,
|
||||
source_name: params.source_name,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default api
|
||||
|
||||
290
assets/src/routes/secnotice/list.jsx
Normal file
290
assets/src/routes/secnotice/list.jsx
Normal file
@@ -0,0 +1,290 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { getSecNotices, getPlugins } from '../../lib/api'
|
||||
|
||||
const SecNoticeListPage = () => {
|
||||
const [secNotices, setSecNotices] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [pageNo, setPageNo] = useState(1)
|
||||
const [pageSize] = useState(10)
|
||||
const [totalCount, setTotalCount] = useState(0)
|
||||
const [plugins, setPlugins] = useState([])
|
||||
// 新的筛选条件状态
|
||||
const [filters, setFilters] = useState({
|
||||
title: '',
|
||||
pushed: '',
|
||||
source_name: '' // 存储插件的name字段
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
fetchPlugins()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchSecNotices()
|
||||
}, [pageNo, filters])
|
||||
|
||||
const fetchPlugins = async () => {
|
||||
try {
|
||||
const response = await getPlugins()
|
||||
setPlugins(response.data.data)
|
||||
} catch (err) {
|
||||
console.error('获取插件列表失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchSecNotices = async () => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const params = {
|
||||
pageNo,
|
||||
pageSize,
|
||||
...filters
|
||||
}
|
||||
// 清理空值参数
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] === '' || params[key] === undefined) {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
|
||||
const response = await getSecNotices(params)
|
||||
const { data, total_count } = response.data.data
|
||||
setSecNotices(data)
|
||||
setTotalCount(total_count)
|
||||
} catch (err) {
|
||||
setError('获取安全公告列表失败')
|
||||
console.error('Fetch sec notices error:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePageChange = (newPageNo) => {
|
||||
setPageNo(newPageNo)
|
||||
}
|
||||
|
||||
const handleFilterChange = (e) => {
|
||||
const { name, value } = e.target
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}))
|
||||
// 重置页码到第一页
|
||||
setPageNo(1)
|
||||
}
|
||||
|
||||
const handleSourceFilterChange = (e) => {
|
||||
const selectedName = e.target.value
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
source_name: selectedName
|
||||
}))
|
||||
// 重置页码到第一页
|
||||
setPageNo(1)
|
||||
}
|
||||
|
||||
const handleResetFilters = () => {
|
||||
setFilters({
|
||||
title: '',
|
||||
pushed: '',
|
||||
source_name: ''
|
||||
})
|
||||
// 重置页码到第一页
|
||||
setPageNo(1)
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(totalCount / pageSize)
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="mb-2 text-3xl font-extrabold text-gray-900">
|
||||
安全公告列表
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
当前共有 {totalCount} 个安全公告
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 筛选控件 */}
|
||||
<div className="mb-6">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<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 px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm appearance-none focus:outline-none 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 px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm appearance-none focus:outline-none 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 px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm appearance-none focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
>
|
||||
<option value="">全部</option>
|
||||
{plugins.map((plugin) => (
|
||||
<option key={plugin.name} value={plugin.name}>
|
||||
{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 && (
|
||||
<div className="p-4 mb-6 border border-red-200 rounded-md bg-red-50">
|
||||
<div className="text-sm text-red-800">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="w-12 h-12 border-b-2 border-indigo-600 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="overflow-hidden bg-white shadow sm:rounded-md">
|
||||
<ul className="divide-y divide-gray-200">
|
||||
{secNotices.map((notice) => (
|
||||
<li key={notice.id}>
|
||||
<div className="block hover:bg-gray-50">
|
||||
<div className="px-4 py-4 sm:px-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-medium text-indigo-600 truncate">{notice.title}</p>
|
||||
<div className="flex flex-shrink-0 ml-2">
|
||||
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
|
||||
notice.risk_level === 'Critical' ? 'bg-red-100 text-red-800' :
|
||||
notice.risk_level === 'High' ? 'bg-orange-100 text-orange-800' :
|
||||
notice.risk_level === 'Medium' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-green-100 text-green-800'
|
||||
}`}>
|
||||
{notice.risk_level}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 sm:flex sm:justify-between">
|
||||
<div className="sm:flex">
|
||||
<p className="flex items-center text-sm text-gray-500">
|
||||
产品: {notice.product_name || 'N/A'}
|
||||
</p>
|
||||
<p className="flex items-center mt-1 text-sm text-gray-500 sm:mt-0 sm:ml-6">
|
||||
来源: {notice.source}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center mt-2 text-sm text-gray-500 sm:mt-0">
|
||||
<svg className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<p>
|
||||
发布时间: <time dateTime={notice.publish_time}>{new Date(notice.publish_time).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}</time>
|
||||
</p>
|
||||
<span className={`ml-4 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
notice.pushed ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{notice.pushed ? '已推送' : '未推送'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500 line-clamp-2">
|
||||
{notice.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 分页组件 */}
|
||||
<div className="flex items-center justify-between mt-6">
|
||||
<div className="text-sm text-gray-700">
|
||||
显示第 {(pageNo - 1) * pageSize + 1} 到 {Math.min(pageNo * pageSize, totalCount)} 条记录,
|
||||
总共 {totalCount} 条记录
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => handlePageChange(pageNo - 1)}
|
||||
disabled={pageNo === 1}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-md ${
|
||||
pageNo === 1
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-white text-gray-700 hover:bg-gray-50 border border-gray-300'
|
||||
}`}
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span className="px-4 py-2 text-sm text-gray-700">
|
||||
第 {pageNo} 页,共 {totalPages} 页
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handlePageChange(pageNo + 1)}
|
||||
disabled={pageNo === totalPages}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-md ${
|
||||
pageNo === totalPages
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-white text-gray-700 hover:bg-gray-50 border border-gray-300'
|
||||
}`}
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SecNoticeListPage
|
||||
16
migrations/20250919085151_create_security_notice_table.sql
Normal file
16
migrations/20250919085151_create_security_notice_table.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- Add migration script here
|
||||
CREATE TABLE IF NOT EXISTS security_notice (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
key TEXT NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
product_name TEXT NOT NULL DEFAULT '',
|
||||
risk_level TEXT NOT NULL DEFAULT '',
|
||||
source TEXT NOT NULL DEFAULT '',
|
||||
source_name TEXT NOT NULL DEFAULT '',
|
||||
is_zero_day BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
publish_time TEXT NOT NULL DEFAULT '',
|
||||
detail_link TEXT NOT NULL DEFAULT '',
|
||||
pushed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
12
src/cli.rs
12
src/cli.rs
@@ -6,6 +6,7 @@ use crate::{
|
||||
domain::{
|
||||
models::{
|
||||
admin_user::{AdminUserPassword, AdminUsername, CreateAdminUserRequest},
|
||||
security_notice::CreateSecurityNotice,
|
||||
vuln_information::CreateVulnInformation,
|
||||
},
|
||||
ports::VulnService,
|
||||
@@ -15,8 +16,9 @@ use crate::{
|
||||
input::http::http_server::{self, make_acceptor_and_advertise_addr},
|
||||
output::{
|
||||
db::{admin_user::AdminUserDao, pg::Pg},
|
||||
plugins,
|
||||
plugins::{sec_notice, vuln},
|
||||
scheduler::Scheduler,
|
||||
sec_notcie_worker::SecNoticeWorker,
|
||||
worker::Worker,
|
||||
},
|
||||
utils::{
|
||||
@@ -84,11 +86,17 @@ async fn run_server(server_rt: &Runtime, config: Config) -> AppResult<()> {
|
||||
.change_context_lazy(make_error)?;
|
||||
|
||||
let (sender, receiver) = mea::mpsc::unbounded::<CreateVulnInformation>();
|
||||
plugins::init(sender).change_context_lazy(make_error)?;
|
||||
vuln::init(sender).change_context_lazy(make_error)?;
|
||||
|
||||
let mut worker = Worker::new(receiver, db.clone());
|
||||
server_rt.spawn(async move { worker.run().await });
|
||||
|
||||
let (sec_sender, sec_receiver) = mea::mpsc::unbounded::<CreateSecurityNotice>();
|
||||
sec_notice::init(sec_sender).change_context_lazy(make_error)?;
|
||||
|
||||
let mut sec_worker = SecNoticeWorker::new(sec_receiver, db.clone());
|
||||
server_rt.spawn(async move { sec_worker.run().await });
|
||||
|
||||
let sched = Scheduler::try_new(db.clone())
|
||||
.await
|
||||
.change_context_lazy(make_error)?;
|
||||
|
||||
@@ -3,5 +3,6 @@ pub mod auth;
|
||||
pub mod ding_bot;
|
||||
pub mod extension_data;
|
||||
pub mod page_utils;
|
||||
pub mod security_notice;
|
||||
pub mod sync_data_task;
|
||||
pub mod vuln_information;
|
||||
|
||||
106
src/domain/models/security_notice.rs
Normal file
106
src/domain/models/security_notice.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use std::fmt;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use modql::field::Fields;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::domain::models::page_utils::PageFilter;
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, sqlx::FromRow, Deserialize,
|
||||
)]
|
||||
pub struct SecuritNotice {
|
||||
pub id: i64,
|
||||
pub key: String,
|
||||
pub title: String,
|
||||
pub product_name: String,
|
||||
pub risk_level: String,
|
||||
pub source: String,
|
||||
pub source_name: String,
|
||||
pub is_zero_day: bool,
|
||||
pub publish_time: String,
|
||||
pub detail_link: String,
|
||||
pub pushed: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Fields, Serialize)]
|
||||
pub struct CreateSecurityNotice {
|
||||
pub key: String,
|
||||
pub title: String,
|
||||
pub product_name: String,
|
||||
pub risk_level: String,
|
||||
pub source: String,
|
||||
pub source_name: String,
|
||||
pub is_zero_day: bool,
|
||||
pub publish_time: String,
|
||||
pub detail_link: String,
|
||||
pub pushed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum RiskLevel {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
Critical,
|
||||
}
|
||||
impl fmt::Display for RiskLevel {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ListSecNoticeRequest {
|
||||
pub page_filter: PageFilter,
|
||||
pub search_params: SearchParams,
|
||||
}
|
||||
|
||||
impl ListSecNoticeRequest {
|
||||
pub fn new(page_filter: PageFilter, search_params: SearchParams) -> Self {
|
||||
ListSecNoticeRequest {
|
||||
page_filter,
|
||||
search_params,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||
pub struct SearchParams {
|
||||
pub title: Option<String>,
|
||||
pub pushed: Option<bool>,
|
||||
pub source_name: Option<String>,
|
||||
}
|
||||
|
||||
impl SearchParams {
|
||||
pub fn new() -> Self {
|
||||
SearchParams::default()
|
||||
}
|
||||
|
||||
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_name(mut self, source_name: Option<String>) -> Self {
|
||||
self.source_name = source_name;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ListSecNoticeResponseData {
|
||||
pub total: i64,
|
||||
pub data: Vec<SecuritNotice>,
|
||||
}
|
||||
|
||||
impl ListSecNoticeResponseData {
|
||||
pub fn new(total: i64, data: Vec<SecuritNotice>) -> Self {
|
||||
ListSecNoticeResponseData { total, data }
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ use crate::{
|
||||
admin_user::AdminUser,
|
||||
auth::LoginRequest,
|
||||
ding_bot::{CreateDingBotRequest, DingBotConfig},
|
||||
security_notice::{ListSecNoticeRequest, ListSecNoticeResponseData},
|
||||
sync_data_task::{CreateSyncDataTaskRequest, SyncDataTask},
|
||||
vuln_information::{
|
||||
GetVulnInformationRequest, ListVulnInformationRequest, ListVulnInformationResponseData,
|
||||
@@ -37,6 +38,11 @@ pub trait VulnService: Clone + Send + Sync + 'static {
|
||||
) -> impl Future<Output = AppResult<i64>> + Send;
|
||||
|
||||
fn get_ding_bot_config(&self) -> impl Future<Output = AppResult<Option<DingBotConfig>>> + Send;
|
||||
|
||||
fn list_sec_notice(
|
||||
&self,
|
||||
req: ListSecNoticeRequest,
|
||||
) -> impl Future<Output = AppResult<ListSecNoticeResponseData>> + Send;
|
||||
}
|
||||
|
||||
pub trait VulnRepository: Clone + Send + Sync + 'static {
|
||||
@@ -64,4 +70,9 @@ pub trait VulnRepository: Clone + Send + Sync + 'static {
|
||||
) -> impl Future<Output = AppResult<i64>> + Send;
|
||||
|
||||
fn get_ding_bot_config(&self) -> impl Future<Output = AppResult<Option<DingBotConfig>>> + Send;
|
||||
|
||||
fn list_sec_notice(
|
||||
&self,
|
||||
req: ListSecNoticeRequest,
|
||||
) -> impl Future<Output = AppResult<ListSecNoticeResponseData>> + Send;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::{
|
||||
admin_user::AdminUser,
|
||||
auth::LoginRequest,
|
||||
ding_bot::{CreateDingBotRequest, DingBotConfig},
|
||||
security_notice::{ListSecNoticeRequest, ListSecNoticeResponseData},
|
||||
sync_data_task::{CreateSyncDataTaskRequest, SyncDataTask},
|
||||
vuln_information::{
|
||||
GetVulnInformationRequest, ListVulnInformationRequest,
|
||||
@@ -75,4 +76,12 @@ where
|
||||
let ret = self.repo.create_ding_bot_config(req).await?;
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
async fn list_sec_notice(
|
||||
&self,
|
||||
req: ListSecNoticeRequest,
|
||||
) -> AppResult<ListSecNoticeResponseData> {
|
||||
let ret = self.repo.list_sec_notice(req).await?;
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod ding_bot_config;
|
||||
pub mod login;
|
||||
pub mod plugin;
|
||||
pub mod sec_notice;
|
||||
pub mod sync_data_task;
|
||||
pub mod vuln_information;
|
||||
|
||||
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
input::http::response::{ApiError, ApiSuccess},
|
||||
output::plugins::get_registry,
|
||||
output::plugins::vuln::get_registry,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
|
||||
129
src/input/http/handlers/sec_notice.rs
Normal file
129
src/input/http/handlers/sec_notice.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use poem::{
|
||||
handler,
|
||||
http::StatusCode,
|
||||
web::{Data, Query},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
cli::Ctx,
|
||||
domain::{
|
||||
models::{
|
||||
page_utils::{PageFilter, PageNo, PageNoError, PageSize, PageSizeError},
|
||||
security_notice::{ListSecNoticeRequest, SearchParams, SecuritNotice},
|
||||
},
|
||||
ports::VulnService,
|
||||
},
|
||||
input::http::response::{ApiError, ApiSuccess},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
pub struct ListSecNoticeRequestBody {
|
||||
pub page_no: i32,
|
||||
pub page_size: i32,
|
||||
pub title: Option<String>,
|
||||
pub pushed: Option<bool>,
|
||||
pub source_name: Option<String>,
|
||||
}
|
||||
|
||||
impl ListSecNoticeRequestBody {
|
||||
pub fn try_into_domain(
|
||||
self,
|
||||
) -> Result<ListSecNoticeRequest, ParseListSecNoticeRequestBodyError> {
|
||||
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);
|
||||
let search_params = SearchParams::new()
|
||||
.with_title(self.title)
|
||||
.with_pushed(self.pushed)
|
||||
.with_source_name(self.source_name);
|
||||
Ok(ListSecNoticeRequest::new(page_filter, search_params))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||
pub struct SecNoticeData {
|
||||
pub id: i64,
|
||||
pub key: String,
|
||||
pub title: String,
|
||||
pub product_name: String,
|
||||
pub risk_level: String,
|
||||
pub source: String,
|
||||
pub source_name: String,
|
||||
pub is_zero_day: bool,
|
||||
pub publish_time: String,
|
||||
pub detail_link: String,
|
||||
pub pushed: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl SecNoticeData {
|
||||
pub fn new(notice: SecuritNotice) -> Self {
|
||||
Self {
|
||||
id: notice.id,
|
||||
key: notice.key,
|
||||
title: notice.title,
|
||||
product_name: notice.product_name,
|
||||
risk_level: notice.risk_level,
|
||||
source: notice.source,
|
||||
source_name: notice.source_name,
|
||||
is_zero_day: notice.is_zero_day,
|
||||
publish_time: notice.publish_time,
|
||||
detail_link: notice.detail_link,
|
||||
pushed: notice.pushed,
|
||||
created_at: notice.created_at,
|
||||
updated_at: notice.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum ParseListSecNoticeRequestBodyError {
|
||||
#[error(transparent)]
|
||||
InvalidPageNo(#[from] PageNoError),
|
||||
#[error(transparent)]
|
||||
InvalidPageSize(#[from] PageSizeError),
|
||||
}
|
||||
|
||||
impl From<ParseListSecNoticeRequestBodyError> for ApiError {
|
||||
fn from(e: ParseListSecNoticeRequestBodyError) -> Self {
|
||||
let msg = match e {
|
||||
ParseListSecNoticeRequestBodyError::InvalidPageNo(e) => {
|
||||
format!("Invalid page number: {}", e)
|
||||
}
|
||||
ParseListSecNoticeRequestBodyError::InvalidPageSize(e) => {
|
||||
format!("Invalid page size: {}", e)
|
||||
}
|
||||
};
|
||||
ApiError::UnprocessableEntity(msg)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct ListSecNoticeHttpResponseData {
|
||||
pub data: Vec<SecNoticeData>,
|
||||
pub total_count: i64,
|
||||
}
|
||||
|
||||
#[handler]
|
||||
pub async fn list_sec_notice<S: VulnService + Send + Sync + 'static>(
|
||||
state: Data<&Ctx<S>>,
|
||||
Query(body): Query<ListSecNoticeRequestBody>,
|
||||
) -> Result<ApiSuccess<ListSecNoticeHttpResponseData>, ApiError> {
|
||||
let req = body.try_into_domain()?;
|
||||
state
|
||||
.vuln_service
|
||||
.list_sec_notice(req)
|
||||
.await
|
||||
.map_err(ApiError::from)
|
||||
.map(|data| {
|
||||
let response_data = ListSecNoticeHttpResponseData {
|
||||
data: data.data.into_iter().map(SecNoticeData::new).collect(),
|
||||
total_count: data.total,
|
||||
};
|
||||
ApiSuccess::new(StatusCode::OK, response_data)
|
||||
})
|
||||
}
|
||||
@@ -18,7 +18,7 @@ use crate::{
|
||||
cli::Ctx,
|
||||
domain::ports::VulnService,
|
||||
input::http::{
|
||||
handlers::{login, plugin, sync_data_task, vuln_information},
|
||||
handlers::{login, plugin, sec_notice, sync_data_task, vuln_information},
|
||||
middleware::auth::AuthMiddleware,
|
||||
},
|
||||
utils::runtime::{self, Runtime},
|
||||
@@ -156,6 +156,10 @@ fn api_routes<S: VulnService + Send + Sync + 'static>() -> impl Endpoint {
|
||||
.get(ding_bot_config::get_ding_bot_config::<S>::default()),
|
||||
),
|
||||
)
|
||||
.nest(
|
||||
"/sec_notice",
|
||||
Route::new().at("", get(sec_notice::list_sec_notice::<S>::default())),
|
||||
)
|
||||
.with(AuthMiddleware::<S>::default()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@ pub mod base;
|
||||
pub mod ding_bot_config;
|
||||
pub mod pg;
|
||||
pub mod repository_impl;
|
||||
pub mod security_notice;
|
||||
pub mod sync_data_task;
|
||||
pub mod vuln_information;
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::{
|
||||
admin_user::AdminUser,
|
||||
auth::LoginRequest,
|
||||
ding_bot::{CreateDingBotRequest, DingBotConfig},
|
||||
security_notice::{ListSecNoticeRequest, ListSecNoticeResponseData},
|
||||
sync_data_task::{CreateSyncDataTaskRequest, SyncDataTask},
|
||||
vuln_information::{
|
||||
GetVulnInformationRequest, ListVulnInformationRequest,
|
||||
@@ -18,7 +19,8 @@ use crate::{
|
||||
errors::Error,
|
||||
output::db::{
|
||||
admin_user::AdminUserDao, ding_bot_config::DingBotConfigDao, pg::Pg,
|
||||
sync_data_task::SyncDataTaskDao, vuln_information::VulnInformationDao,
|
||||
security_notice::SecurityNoticeDao, sync_data_task::SyncDataTaskDao,
|
||||
vuln_information::VulnInformationDao,
|
||||
},
|
||||
utils::password_hash::verify_password_hash,
|
||||
};
|
||||
@@ -127,4 +129,26 @@ impl VulnRepository for Pg {
|
||||
.change_context_lazy(|| Error::Message("failed to commit transaction".to_string()))?;
|
||||
Ok(ding_bot_config)
|
||||
}
|
||||
async fn list_sec_notice(
|
||||
&self,
|
||||
req: ListSecNoticeRequest,
|
||||
) -> AppResult<ListSecNoticeResponseData> {
|
||||
let mut tx =
|
||||
self.pool.begin().await.change_context_lazy(|| {
|
||||
Error::Message("failed to begin transaction".to_string())
|
||||
})?;
|
||||
let data = SecurityNoticeDao::filter_security_notices(
|
||||
&mut tx,
|
||||
&req.page_filter,
|
||||
&req.search_params,
|
||||
)
|
||||
.await?;
|
||||
let count =
|
||||
SecurityNoticeDao::filter_security_notices_count(&mut tx, &req.search_params).await?;
|
||||
|
||||
tx.commit()
|
||||
.await
|
||||
.change_context_lazy(|| Error::Message("failed to commit transaction".to_string()))?;
|
||||
Ok(ListSecNoticeResponseData { data, total: count })
|
||||
}
|
||||
}
|
||||
|
||||
102
src/output/db/security_notice.rs
Normal file
102
src/output/db/security_notice.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use sea_query::Value;
|
||||
use sqlx::{Postgres, Transaction};
|
||||
|
||||
use crate::{
|
||||
AppResult,
|
||||
domain::models::{
|
||||
page_utils::PageFilter,
|
||||
security_notice::{CreateSecurityNotice, SearchParams, SecuritNotice},
|
||||
},
|
||||
output::db::base::{
|
||||
Dao, DaoQueryBuilder, dao_create, dao_fetch_by_column, dao_fetch_by_id, dao_update_field,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct SecurityNoticeDao;
|
||||
|
||||
impl Dao for SecurityNoticeDao {
|
||||
const TABLE: &'static str = "security_notice";
|
||||
}
|
||||
|
||||
impl SecurityNoticeDao {
|
||||
pub async fn create(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
req: CreateSecurityNotice,
|
||||
) -> AppResult<i64> {
|
||||
let id = dao_create::<Self, _>(tx, req).await?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub async fn fetch_by_key(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
key: &str,
|
||||
) -> AppResult<Option<SecuritNotice>> {
|
||||
dao_fetch_by_column::<Self, SecuritNotice>(tx, "key", key).await
|
||||
}
|
||||
|
||||
pub async fn fetch_by_id(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
id: i64,
|
||||
) -> AppResult<Option<SecuritNotice>> {
|
||||
dao_fetch_by_id::<Self, SecuritNotice>(tx, id).await
|
||||
}
|
||||
|
||||
pub async fn update_pushed(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
id: i64,
|
||||
status: bool,
|
||||
) -> AppResult<u64> {
|
||||
let row = dao_update_field::<Self>(tx, id, "pushed", Value::Bool(Some(status))).await?;
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
pub async fn filter_security_notices(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
page_filter: &PageFilter,
|
||||
search_params: &SearchParams,
|
||||
) -> AppResult<Vec<SecuritNotice>> {
|
||||
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_name) = &search_params.source_name {
|
||||
query_builder = query_builder.and_where_like("source_name", source_name);
|
||||
}
|
||||
|
||||
if let Some(pushed) = &search_params.pushed {
|
||||
query_builder = query_builder.and_where_bool("pushed", *pushed);
|
||||
}
|
||||
|
||||
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;
|
||||
query_builder
|
||||
.order_by_desc("updated_at")
|
||||
.limit_offset(page_size as i64, offset as i64)
|
||||
.fetch_all(tx)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn filter_security_notices_count(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
search_params: &SearchParams,
|
||||
) -> AppResult<i64> {
|
||||
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_name) = &search_params.source_name {
|
||||
query_builder = query_builder.and_where_like("source_name", source_name);
|
||||
}
|
||||
|
||||
if let Some(pushed) = &search_params.pushed {
|
||||
query_builder = query_builder.and_where_bool("pushed", *pushed);
|
||||
}
|
||||
|
||||
query_builder.count(tx).await
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,5 @@ pub mod db;
|
||||
pub mod plugins;
|
||||
pub mod push;
|
||||
pub mod scheduler;
|
||||
pub mod sec_notcie_worker;
|
||||
pub mod worker;
|
||||
|
||||
@@ -1,55 +1,2 @@
|
||||
pub mod avd;
|
||||
pub mod kev;
|
||||
pub mod oscs;
|
||||
pub mod seekbug;
|
||||
pub mod threatbook;
|
||||
pub mod ti;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use dashmap::DashMap;
|
||||
use lazy_static::lazy_static;
|
||||
use mea::mpsc::UnboundedSender;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
AppResult,
|
||||
domain::models::vuln_information::CreateVulnInformation,
|
||||
output::plugins::{
|
||||
avd::AVDPlugin, kev::KevPlugin, oscs::OscsPlugin, seekbug::SeekBugPlugin,
|
||||
threatbook::ThreatBookPlugin, ti::TiPlugin,
|
||||
},
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
static ref PLUGINS: Arc<DashMap<String, Box<dyn VulnPlugin>>> = Arc::new(DashMap::new());
|
||||
}
|
||||
|
||||
pub fn init(sender: UnboundedSender<CreateVulnInformation>) -> AppResult<()> {
|
||||
KevPlugin::try_new(sender.clone())?;
|
||||
AVDPlugin::try_new(sender.clone())?;
|
||||
OscsPlugin::try_new(sender.clone())?;
|
||||
SeekBugPlugin::try_new(sender.clone())?;
|
||||
ThreatBookPlugin::try_new(sender.clone())?;
|
||||
TiPlugin::try_new(sender)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait VulnPlugin: Send + Sync + 'static {
|
||||
fn get_name(&self) -> String;
|
||||
fn get_display_name(&self) -> String;
|
||||
fn get_link(&self) -> String;
|
||||
async fn update(&self, page_limit: i32) -> AppResult<()>;
|
||||
}
|
||||
|
||||
pub fn register_plugin(name: String, plugin: Box<dyn VulnPlugin>) {
|
||||
PLUGINS.insert(name, plugin);
|
||||
}
|
||||
|
||||
pub fn get_registry() -> Arc<DashMap<String, Box<dyn VulnPlugin>>> {
|
||||
PLUGINS.clone()
|
||||
}
|
||||
|
||||
pub fn list_plugin_names() -> Vec<String> {
|
||||
PLUGINS.iter().map(|r| r.key().clone()).collect()
|
||||
}
|
||||
pub mod sec_notice;
|
||||
pub mod vuln;
|
||||
|
||||
40
src/output/plugins/sec_notice/mod.rs
Normal file
40
src/output/plugins/sec_notice/mod.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
pub mod yongyou;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use dashmap::DashMap;
|
||||
use lazy_static::lazy_static;
|
||||
use mea::mpsc::UnboundedSender;
|
||||
|
||||
use crate::{
|
||||
AppResult, domain::models::security_notice::CreateSecurityNotice,
|
||||
output::plugins::sec_notice::yongyou::YongYouNoticePlugin,
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
static ref NOTICE: Arc<DashMap<String, Box<dyn SecNoticePlugin>>> = Arc::new(DashMap::new());
|
||||
}
|
||||
|
||||
pub fn init(sender: UnboundedSender<CreateSecurityNotice>) -> AppResult<()> {
|
||||
YongYouNoticePlugin::try_new(sender.clone())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait SecNoticePlugin: Send + Sync + 'static {
|
||||
fn get_name(&self) -> String;
|
||||
async fn update(&self, page_limit: i32) -> AppResult<()>;
|
||||
}
|
||||
|
||||
pub fn register_sec_notice(name: String, sec_notice: Box<dyn SecNoticePlugin>) {
|
||||
NOTICE.insert(name, sec_notice);
|
||||
}
|
||||
|
||||
pub fn get_notice_registry() -> Arc<DashMap<String, Box<dyn SecNoticePlugin>>> {
|
||||
NOTICE.clone()
|
||||
}
|
||||
|
||||
pub fn list_sec_notice_names() -> Vec<String> {
|
||||
NOTICE.iter().map(|r| r.key().clone()).collect()
|
||||
}
|
||||
136
src/output/plugins/sec_notice/yongyou.rs
Normal file
136
src/output/plugins/sec_notice/yongyou.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use async_trait::async_trait;
|
||||
use error_stack::ResultExt;
|
||||
use mea::mpsc::UnboundedSender;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
AppResult,
|
||||
domain::models::security_notice::{CreateSecurityNotice, RiskLevel},
|
||||
errors::Error,
|
||||
output::plugins::sec_notice::{SecNoticePlugin, register_sec_notice},
|
||||
utils::http_client::HttpClient,
|
||||
};
|
||||
|
||||
const YONGYOU_NOTICE_URL: &str = "https://security.yonyou.com/web-api/web/notice/page";
|
||||
const YONGYOU_DEFAULT_PAGE_NO: i32 = 1;
|
||||
const YONGYOU_DEFAULT_PAGE_SIZE: i32 = 3;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct YongYouNoticePlugin {
|
||||
name: String,
|
||||
link: String,
|
||||
http_client: HttpClient,
|
||||
sender: UnboundedSender<CreateSecurityNotice>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SecNoticePlugin for YongYouNoticePlugin {
|
||||
fn get_name(&self) -> String {
|
||||
self.name.to_string()
|
||||
}
|
||||
|
||||
async fn update(&self, _page_limit: i32) -> AppResult<()> {
|
||||
let sec_notices = self
|
||||
.get_sec_notices(YONGYOU_DEFAULT_PAGE_NO, YONGYOU_DEFAULT_PAGE_SIZE)
|
||||
.await?;
|
||||
for sec_notice in sec_notices.data.list {
|
||||
let detail_link = format!(
|
||||
"https://security.yonyou.com/#/noticeInfo?id={}",
|
||||
sec_notice.id
|
||||
);
|
||||
let risk_level = self.get_risk_level(&sec_notice.risk_level).to_string();
|
||||
let create_security_notice = CreateSecurityNotice {
|
||||
key: sec_notice.identifier,
|
||||
title: sec_notice.notice,
|
||||
detail_link,
|
||||
source: self.link.clone(),
|
||||
source_name: self.get_name(),
|
||||
product_name: sec_notice.product_line_name,
|
||||
is_zero_day: self.is_zero_day(&sec_notice.is_zero_day),
|
||||
publish_time: sec_notice.publish_time,
|
||||
risk_level,
|
||||
pushed: false,
|
||||
};
|
||||
self.sender
|
||||
.send(create_security_notice)
|
||||
.change_context_lazy(|| {
|
||||
Error::Message("Failed to send security notice to queue".to_string())
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl YongYouNoticePlugin {
|
||||
pub fn try_new(
|
||||
sender: UnboundedSender<CreateSecurityNotice>,
|
||||
) -> AppResult<YongYouNoticePlugin> {
|
||||
let http_client = HttpClient::try_new()?;
|
||||
let yongyou = YongYouNoticePlugin {
|
||||
name: "YongYouPlugin".to_string(),
|
||||
link: "https://security.yonyou.com/#/home".to_string(),
|
||||
http_client,
|
||||
sender,
|
||||
};
|
||||
register_sec_notice(yongyou.name.clone(), Box::new(yongyou.clone()));
|
||||
Ok(yongyou)
|
||||
}
|
||||
|
||||
pub async fn get_sec_notices(
|
||||
&self,
|
||||
page_no: i32,
|
||||
page_size: i32,
|
||||
) -> AppResult<ListYongYouSecNotices> {
|
||||
let params = serde_json::json!({
|
||||
"pageNo": page_no,
|
||||
"pageSize": page_size,
|
||||
});
|
||||
let sec_notices: ListYongYouSecNotices = self
|
||||
.http_client
|
||||
.post_json(YONGYOU_NOTICE_URL, ¶ms)
|
||||
.await?
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| Error::Message(format!("yongyou get sec notices error: {}", e)))?;
|
||||
Ok(sec_notices)
|
||||
}
|
||||
|
||||
pub fn get_risk_level(&self, risk_level: &str) -> RiskLevel {
|
||||
match risk_level {
|
||||
"1" => RiskLevel::Critical,
|
||||
"2" => RiskLevel::High,
|
||||
"3" => RiskLevel::Medium,
|
||||
"4" => RiskLevel::Low,
|
||||
_ => RiskLevel::Low,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_zero_day(&self, zero_day: &str) -> bool {
|
||||
matches!(zero_day, "是")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListYongYouSecNotices {
|
||||
code: i32,
|
||||
data: YongYouNoticeData,
|
||||
msg: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct YongYouNoticeData {
|
||||
list: Vec<YongYouNotice>,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct YongYouNotice {
|
||||
pub id: i32,
|
||||
pub notice: String,
|
||||
pub identifier: String,
|
||||
pub product_line_name: String,
|
||||
pub publish_time: String,
|
||||
pub risk_level: String,
|
||||
pub is_zero_day: String,
|
||||
}
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
AppResult,
|
||||
domain::models::vuln_information::{CreateVulnInformation, Severity},
|
||||
errors::Error,
|
||||
output::plugins::{VulnPlugin, register_plugin},
|
||||
output::plugins::vuln::{VulnPlugin, register_plugin},
|
||||
utils::http_client::HttpClient,
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
AppResult,
|
||||
domain::models::vuln_information::{CreateVulnInformation, Severity},
|
||||
errors::Error,
|
||||
output::plugins::{VulnPlugin, register_plugin},
|
||||
output::plugins::vuln::{VulnPlugin, register_plugin},
|
||||
utils::http_client::HttpClient,
|
||||
};
|
||||
|
||||
55
src/output/plugins/vuln/mod.rs
Normal file
55
src/output/plugins/vuln/mod.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
pub mod avd;
|
||||
pub mod kev;
|
||||
pub mod oscs;
|
||||
pub mod seekbug;
|
||||
pub mod threatbook;
|
||||
pub mod ti;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use dashmap::DashMap;
|
||||
use lazy_static::lazy_static;
|
||||
use mea::mpsc::UnboundedSender;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
AppResult,
|
||||
domain::models::vuln_information::CreateVulnInformation,
|
||||
output::plugins::vuln::{
|
||||
avd::AVDPlugin, kev::KevPlugin, oscs::OscsPlugin, seekbug::SeekBugPlugin,
|
||||
threatbook::ThreatBookPlugin, ti::TiPlugin,
|
||||
},
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
static ref PLUGINS: Arc<DashMap<String, Box<dyn VulnPlugin>>> = Arc::new(DashMap::new());
|
||||
}
|
||||
|
||||
pub fn init(sender: UnboundedSender<CreateVulnInformation>) -> AppResult<()> {
|
||||
KevPlugin::try_new(sender.clone())?;
|
||||
AVDPlugin::try_new(sender.clone())?;
|
||||
OscsPlugin::try_new(sender.clone())?;
|
||||
SeekBugPlugin::try_new(sender.clone())?;
|
||||
ThreatBookPlugin::try_new(sender.clone())?;
|
||||
TiPlugin::try_new(sender)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait VulnPlugin: Send + Sync + 'static {
|
||||
fn get_name(&self) -> String;
|
||||
fn get_display_name(&self) -> String;
|
||||
fn get_link(&self) -> String;
|
||||
async fn update(&self, page_limit: i32) -> AppResult<()>;
|
||||
}
|
||||
|
||||
pub fn register_plugin(name: String, plugin: Box<dyn VulnPlugin>) {
|
||||
PLUGINS.insert(name, plugin);
|
||||
}
|
||||
|
||||
pub fn get_registry() -> Arc<DashMap<String, Box<dyn VulnPlugin>>> {
|
||||
PLUGINS.clone()
|
||||
}
|
||||
|
||||
pub fn list_plugin_names() -> Vec<String> {
|
||||
PLUGINS.iter().map(|r| r.key().clone()).collect()
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
AppResult,
|
||||
domain::models::vuln_information::{CreateVulnInformation, Severity},
|
||||
errors::Error,
|
||||
output::plugins::{VulnPlugin, register_plugin},
|
||||
output::plugins::vuln::{VulnPlugin, register_plugin},
|
||||
utils::{http_client::HttpClient, util::timestamp_to_date},
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::{
|
||||
AppResult,
|
||||
domain::models::vuln_information::{CreateVulnInformation, Severity},
|
||||
errors::Error,
|
||||
output::plugins::{VulnPlugin, register_plugin},
|
||||
output::plugins::vuln::{VulnPlugin, register_plugin},
|
||||
utils::http_client::HttpClient,
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
AppResult,
|
||||
domain::models::vuln_information::{CreateVulnInformation, Severity},
|
||||
errors::Error,
|
||||
output::plugins::{VulnPlugin, register_plugin},
|
||||
output::plugins::vuln::{VulnPlugin, register_plugin},
|
||||
utils::{http_client::HttpClient, util::check_over_two_month},
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
AppResult,
|
||||
domain::models::vuln_information::{CreateVulnInformation, Severity},
|
||||
errors::Error,
|
||||
output::plugins::{VulnPlugin, register_plugin},
|
||||
output::plugins::vuln::{VulnPlugin, register_plugin},
|
||||
utils::http_client::HttpClient,
|
||||
};
|
||||
|
||||
@@ -6,7 +6,9 @@ use lazy_static::lazy_static;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
AppResult, domain::models::vuln_information::VulnInformation, errors::Error,
|
||||
AppResult,
|
||||
domain::models::{security_notice::SecuritNotice, vuln_information::VulnInformation},
|
||||
errors::Error,
|
||||
utils::util::render_string,
|
||||
};
|
||||
|
||||
@@ -46,9 +48,19 @@ const VULN_INFO_MSG_TEMPLATE: &str = r####"
|
||||
{% if github_search | length > 0 %}{% for link in github_search %}{{ loop.index }}. {{ link }}
|
||||
{% endfor %}{% else %}暂未找到{% endif %}{% endif %}"####;
|
||||
|
||||
const SEC_NOTICE_MSG_TEMPLATE: &str = r####"
|
||||
# {{ title }}
|
||||
|
||||
- 危害定级: **{{ risk_level }}**
|
||||
- 披露日期: **{{ publish_time }}**
|
||||
- ZeroDay: **{{ is_zero_day }}**
|
||||
- 产品名称: {{ product_name }}
|
||||
- 详情链接: {{ detail_link }}
|
||||
"####;
|
||||
|
||||
const MAX_REFERENCE_LENGTH: usize = 8;
|
||||
|
||||
pub fn reader_vulninfo(mut vuln: VulnInformation) -> AppResult<String> {
|
||||
pub fn render_vulninfo(mut vuln: VulnInformation) -> AppResult<String> {
|
||||
if vuln.reference_links.len() > MAX_REFERENCE_LENGTH {
|
||||
vuln.reference_links = vuln.reference_links[..MAX_REFERENCE_LENGTH].to_vec();
|
||||
}
|
||||
@@ -58,6 +70,13 @@ pub fn reader_vulninfo(mut vuln: VulnInformation) -> AppResult<String> {
|
||||
Ok(markdown)
|
||||
}
|
||||
|
||||
pub fn render_sec_notice(sec_notice: SecuritNotice) -> AppResult<String> {
|
||||
let json_value: Value = serde_json::to_value(sec_notice)
|
||||
.map_err(|e| Error::Message(format!("Failed to serialize sec notice: {e}")))?;
|
||||
let markdown = render_string(SEC_NOTICE_MSG_TEMPLATE, &json_value)?;
|
||||
Ok(markdown)
|
||||
}
|
||||
|
||||
pub fn escape_markdown(input: String) -> String {
|
||||
input
|
||||
.replace('_', "\\_")
|
||||
|
||||
@@ -10,7 +10,10 @@ use crate::{
|
||||
errors::Error,
|
||||
output::{
|
||||
db::{pg::Pg, sync_data_task::SyncDataTaskDao},
|
||||
plugins::{get_registry, list_plugin_names},
|
||||
plugins::{
|
||||
sec_notice::{get_notice_registry, list_sec_notice_names},
|
||||
vuln::{get_registry, list_plugin_names},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -151,6 +154,21 @@ async fn execute_job(_uuid: Uuid) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let sec_notices = list_sec_notice_names();
|
||||
for name in sec_notices {
|
||||
job_set.spawn(async move {
|
||||
let notice = get_notice_registry();
|
||||
log::info!("Updating sec notice: {}", name);
|
||||
if let Some(notice) = notice.get::<str>(&name)
|
||||
&& let Err(e) = notice.update(1).await
|
||||
{
|
||||
log::error!("Sec notice update failed for {}: {}", name, e)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
job_set.join_all().await;
|
||||
|
||||
log::info!("Plugin syn finished elapsed {:?}", start.elapsed());
|
||||
}
|
||||
|
||||
105
src/output/sec_notcie_worker.rs
Normal file
105
src/output/sec_notcie_worker.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use error_stack::ResultExt;
|
||||
use mea::mpsc::UnboundedReceiver;
|
||||
|
||||
use crate::{
|
||||
AppResult,
|
||||
domain::models::security_notice::CreateSecurityNotice,
|
||||
errors::Error,
|
||||
output::{
|
||||
db::{ding_bot_config::DingBotConfigDao, pg::Pg, security_notice::SecurityNoticeDao},
|
||||
push::{MessageBot, ding_bot::DingBot, render_sec_notice},
|
||||
},
|
||||
};
|
||||
|
||||
pub struct SecNoticeWorker {
|
||||
pub receiver: UnboundedReceiver<CreateSecurityNotice>,
|
||||
pub pg: Arc<Pg>,
|
||||
}
|
||||
|
||||
impl SecNoticeWorker {
|
||||
pub fn new(receiver: UnboundedReceiver<CreateSecurityNotice>, pg: Pg) -> Self {
|
||||
SecNoticeWorker {
|
||||
receiver,
|
||||
pg: Arc::new(pg),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> AppResult<()> {
|
||||
while let Some(req) = self.receiver.recv().await {
|
||||
match self.store(req).await {
|
||||
Err(e) => {
|
||||
log::error!("Failed to store vuln information: {:?}", e);
|
||||
continue;
|
||||
}
|
||||
Ok((id, created)) => {
|
||||
if created {
|
||||
self.ding_bot_push(id).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn ding_bot_push(&self, id: i64) -> AppResult<()> {
|
||||
log::info!("ding bot push start! id: {}", id);
|
||||
let mut tx =
|
||||
self.pg.pool.begin().await.change_context_lazy(|| {
|
||||
Error::Message("failed to begin transaction".to_string())
|
||||
})?;
|
||||
let ding_bot_config = DingBotConfigDao::first(&mut tx).await?;
|
||||
if let Some(config) = ding_bot_config
|
||||
&& config.status
|
||||
{
|
||||
let sec_notice = SecurityNoticeDao::fetch_by_id(&mut tx, id).await?;
|
||||
if let Some(s) = sec_notice
|
||||
&& !s.pushed
|
||||
{
|
||||
let ding = DingBot::try_new(config.access_token, config.secret_token)?;
|
||||
let title = s.title.clone();
|
||||
let msg = match render_sec_notice(s) {
|
||||
Ok(msg) => msg,
|
||||
Err(e) => {
|
||||
log::error!("Failed to render sec notice: {:?}", e);
|
||||
return Err(Error::Message(format!(
|
||||
"Failed to render sec notice: {:?}",
|
||||
e
|
||||
))
|
||||
.into());
|
||||
}
|
||||
};
|
||||
ding.push_markdown(title, msg).await?;
|
||||
log::info!("ding bot push success! id: {}", id);
|
||||
SecurityNoticeDao::update_pushed(&mut tx, id, true).await?;
|
||||
}
|
||||
} else {
|
||||
log::info!("ding bot config not found or status is false");
|
||||
}
|
||||
tx.commit()
|
||||
.await
|
||||
.change_context_lazy(|| Error::Message("failed to commit transaction".to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn store(&self, req: CreateSecurityNotice) -> AppResult<(i64, bool)> {
|
||||
let mut tx =
|
||||
self.pg.pool.begin().await.change_context_lazy(|| {
|
||||
Error::Message("failed to begin transaction".to_string())
|
||||
})?;
|
||||
let res = SecurityNoticeDao::fetch_by_key(&mut tx, &req.key).await?;
|
||||
let (id, created) = match res {
|
||||
None => {
|
||||
let id = SecurityNoticeDao::create(&mut tx, req).await?;
|
||||
(id, true)
|
||||
}
|
||||
Some(sec_notice) => (sec_notice.id, false),
|
||||
};
|
||||
|
||||
tx.commit()
|
||||
.await
|
||||
.change_context_lazy(|| Error::Message("failed to commit transaction".to_string()))?;
|
||||
Ok((id, created))
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
errors::Error,
|
||||
output::{
|
||||
db::{ding_bot_config::DingBotConfigDao, pg::Pg, vuln_information::VulnInformationDao},
|
||||
push::{MessageBot, ding_bot::DingBot, reader_vulninfo},
|
||||
push::{MessageBot, ding_bot::DingBot, render_vulninfo},
|
||||
},
|
||||
utils::search::search_github_poc,
|
||||
};
|
||||
@@ -60,7 +60,7 @@ impl Worker {
|
||||
{
|
||||
let ding = DingBot::try_new(config.access_token, config.secret_token)?;
|
||||
let title = v.title.clone();
|
||||
let msg = match reader_vulninfo(v) {
|
||||
let msg = match render_vulninfo(v) {
|
||||
Ok(msg) => msg,
|
||||
Err(e) => {
|
||||
log::error!("Failed to read vuln info: {:?}", e);
|
||||
|
||||
Reference in New Issue
Block a user