feat: support grafana sec notice (#23)
This commit is contained in:
@@ -18,7 +18,7 @@ VulnFeed 是一个用于收集和推送高价值漏洞和补丁公告信息的
|
|||||||
当抓取安全补丁公告的站点数据:
|
当抓取安全补丁公告的站点数据:
|
||||||
|
|
||||||
| 名称 | 地址 | 推送策略 |
|
| 名称 | 地址 | 推送策略 |
|
||||||
| --------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
|
| --------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||||
| 用友安全中心 | <https://security.yonyou.com/#/home> | 近三条数据 |
|
| 用友安全中心 | <https://security.yonyou.com/#/home> | 近三条数据 |
|
||||||
| 泛微ECOLOGY安全补丁包 | <https://www.weaver.com.cn/cs/securityDownload.html?src=cn> | "EC9.0全量补丁", "EC8.0全量补丁", "EC10.0安全补丁" |
|
| 泛微ECOLOGY安全补丁包 | <https://www.weaver.com.cn/cs/securityDownload.html?src=cn> | "EC9.0全量补丁", "EC8.0全量补丁", "EC10.0安全补丁" |
|
||||||
| Smartbi安全补丁包 | <https://www.smartbi.com.cn/patchinfo> | 近三条数据 |
|
| Smartbi安全补丁包 | <https://www.smartbi.com.cn/patchinfo> | 近三条数据 |
|
||||||
@@ -28,6 +28,7 @@ VulnFeed 是一个用于收集和推送高价值漏洞和补丁公告信息的
|
|||||||
| Oracle安全公告 | <https://www.oracle.com/cn/security-alerts/> | 近三条数据 |
|
| Oracle安全公告 | <https://www.oracle.com/cn/security-alerts/> | 近三条数据 |
|
||||||
| Firefox安全公告 | <https://www.mozilla.org/en-US/security/known-vulnerabilities/firefox/> | 近三条数据 |
|
| Firefox安全公告 | <https://www.mozilla.org/en-US/security/known-vulnerabilities/firefox/> | 近三条数据 |
|
||||||
| Apple安全公告 | <https://support.apple.com/zh-cn/100100> | 近十条数,不包含没有链接的据 |
|
| Apple安全公告 | <https://support.apple.com/zh-cn/100100> | 近十条数,不包含没有链接的据 |
|
||||||
|
| Grafana安全公告 | <https://grafana.com/tags/security/> | 第一页数据中以"Grafana security release" 或 "Grafana security update" 开头的公告 |
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 142 KiB |
232
src/output/plugins/sec_notice/grafana.rs
Normal file
232
src/output/plugins/sec_notice/grafana.rs
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use error_stack::ResultExt;
|
||||||
|
use mea::mpsc::UnboundedSender;
|
||||||
|
use scraper::{Html, Selector};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
AppResult,
|
||||||
|
domain::models::security_notice::{CreateSecurityNotice, RiskLevel},
|
||||||
|
errors::Error,
|
||||||
|
output::plugins::sec_notice::{SecNoticePlugin, register_sec_notice},
|
||||||
|
utils::http_client::HttpClient,
|
||||||
|
};
|
||||||
|
|
||||||
|
const GRAFANA_SECURITY_URL: &str = "https://grafana.com/tags/security/";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GrafanaNoticePlugin {
|
||||||
|
name: String,
|
||||||
|
display_name: String,
|
||||||
|
link: String,
|
||||||
|
http_client: HttpClient,
|
||||||
|
sender: UnboundedSender<CreateSecurityNotice>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl SecNoticePlugin for GrafanaNoticePlugin {
|
||||||
|
fn get_name(&self) -> String {
|
||||||
|
self.name.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_display_name(&self) -> String {
|
||||||
|
self.display_name.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_link(&self) -> String {
|
||||||
|
self.link.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(&self, _page_limit: i32) -> AppResult<()> {
|
||||||
|
let notices = self.parse_security_notices().await?;
|
||||||
|
|
||||||
|
// 只取前10条数据
|
||||||
|
for notice in notices.into_iter().take(10) {
|
||||||
|
let create_security_notice = CreateSecurityNotice {
|
||||||
|
key: notice.id,
|
||||||
|
title: notice.title,
|
||||||
|
product_name: "Grafana".to_string(),
|
||||||
|
risk_level: RiskLevel::Critical.to_string(), // Grafana安全公告通常为高风险
|
||||||
|
source: self.link.clone(),
|
||||||
|
source_name: self.get_name(),
|
||||||
|
is_zero_day: false,
|
||||||
|
publish_time: notice.published_date,
|
||||||
|
detail_link: notice.detail_link,
|
||||||
|
pushed: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.sender
|
||||||
|
.send(create_security_notice)
|
||||||
|
.change_context_lazy(|| {
|
||||||
|
Error::Message("Failed to send security notice to queue".to_string())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GrafanaNoticePlugin {
|
||||||
|
pub fn try_new(
|
||||||
|
sender: UnboundedSender<CreateSecurityNotice>,
|
||||||
|
) -> AppResult<GrafanaNoticePlugin> {
|
||||||
|
let http_client = HttpClient::try_new()?;
|
||||||
|
let grafana = GrafanaNoticePlugin {
|
||||||
|
name: "GrafanaPlugin".to_string(),
|
||||||
|
display_name: "Grafana安全公告".to_string(),
|
||||||
|
link: GRAFANA_SECURITY_URL.to_string(),
|
||||||
|
http_client,
|
||||||
|
sender,
|
||||||
|
};
|
||||||
|
register_sec_notice(grafana.name.clone(), Box::new(grafana.clone()));
|
||||||
|
Ok(grafana)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_document(&self, url: &str) -> AppResult<Html> {
|
||||||
|
let content = self.http_client.get_html_content(url).await?;
|
||||||
|
let document = Html::parse_document(&content);
|
||||||
|
Ok(document)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析安全公告列表
|
||||||
|
pub async fn parse_security_notices(&self) -> AppResult<Vec<GrafanaSecurityNotice>> {
|
||||||
|
let document = self.get_document(&self.link).await?;
|
||||||
|
|
||||||
|
// 选择 class="all-posts" 下的所有 article 元素
|
||||||
|
let articles_selector = Selector::parse(".all-posts article").map_err(|e| {
|
||||||
|
Error::Message(format!("Failed to parse CSS selector for articles: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let articles: Vec<_> = document.select(&articles_selector).collect();
|
||||||
|
|
||||||
|
let mut notices = Vec::new();
|
||||||
|
|
||||||
|
for article in articles.iter() {
|
||||||
|
if notices.len() >= 10 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取标题
|
||||||
|
let title_selector = Selector::parse("h3").map_err(|e| {
|
||||||
|
Error::Message(format!("Failed to parse CSS selector for title: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if let Some(title_element) = article.select(&title_selector).next() {
|
||||||
|
let title = title_element.text().collect::<String>().trim().to_string();
|
||||||
|
|
||||||
|
// 只处理以 "Grafana security release" 或 "Grafana security update" 开头的公告
|
||||||
|
if title.starts_with("Grafana security release")
|
||||||
|
|| title.starts_with("Grafana security update")
|
||||||
|
{
|
||||||
|
// 提取详情链接
|
||||||
|
let link_selector = Selector::parse("a").map_err(|e| {
|
||||||
|
Error::Message(format!("Failed to parse CSS selector for link: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let detail_link =
|
||||||
|
if let Some(link_element) = article.select(&link_selector).next() {
|
||||||
|
link_element
|
||||||
|
.value()
|
||||||
|
.attr("href")
|
||||||
|
.map(|href| {
|
||||||
|
if href.starts_with("http") {
|
||||||
|
href.to_string()
|
||||||
|
} else {
|
||||||
|
format!("https://grafana.com{}", href)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提取发布日期
|
||||||
|
let date_selector =
|
||||||
|
Selector::parse("p.blog-list-item__byline").map_err(|e| {
|
||||||
|
Error::Message(format!("Failed to parse CSS selector for date: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let published_date =
|
||||||
|
if let Some(date_element) = article.select(&date_selector).next() {
|
||||||
|
// 从 byline 中提取日期,格式类似 "Naima Alexander · 18 Sep 2025 · 5 min read"
|
||||||
|
let text = date_element.text().collect::<String>();
|
||||||
|
let parts: Vec<&str> = text.split("·").collect();
|
||||||
|
if parts.len() >= 2 {
|
||||||
|
parts[1].trim().to_string()
|
||||||
|
} else {
|
||||||
|
text.trim().to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从链接中提取ID
|
||||||
|
let id = detail_link
|
||||||
|
.split('/')
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.next_back()
|
||||||
|
.unwrap_or(&detail_link)
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
notices.push(GrafanaSecurityNotice {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
detail_link,
|
||||||
|
published_date,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(notices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GrafanaSecurityNotice {
|
||||||
|
pub id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub detail_link: String,
|
||||||
|
pub published_date: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use mea::mpsc::unbounded;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_parse_security_notices() {
|
||||||
|
let (sender, _receiver) = unbounded::<CreateSecurityNotice>();
|
||||||
|
let plugin = GrafanaNoticePlugin::try_new(sender).unwrap();
|
||||||
|
|
||||||
|
// 测试解析安全公告
|
||||||
|
let result = plugin.parse_security_notices().await;
|
||||||
|
if let Err(e) = &result {
|
||||||
|
println!("Error parsing security notices: {:?}", e);
|
||||||
|
}
|
||||||
|
let notices = result.unwrap();
|
||||||
|
|
||||||
|
for (i, notice) in notices.iter().enumerate() {
|
||||||
|
println!(
|
||||||
|
"Notice {}: title={}, id={}, link={}",
|
||||||
|
i, notice.title, notice.id, notice.detail_link
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证是否获取到了公告
|
||||||
|
// 验证是否获取到了公告
|
||||||
|
assert!(!notices.is_empty());
|
||||||
|
|
||||||
|
// 验证前10个公告
|
||||||
|
assert!(notices.len() <= 10);
|
||||||
|
|
||||||
|
// 验证每个公告的标题都以指定前缀开头
|
||||||
|
for notice in ¬ices {
|
||||||
|
assert!(
|
||||||
|
notice.title.starts_with("Grafana security release")
|
||||||
|
|| notice.title.starts_with("Grafana security update")
|
||||||
|
);
|
||||||
|
assert!(!notice.id.is_empty());
|
||||||
|
assert!(!notice.detail_link.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod apple;
|
pub mod apple;
|
||||||
pub mod fanruan;
|
pub mod fanruan;
|
||||||
pub mod firefox;
|
pub mod firefox;
|
||||||
|
pub mod grafana;
|
||||||
pub mod oracle;
|
pub mod oracle;
|
||||||
pub mod seeyon;
|
pub mod seeyon;
|
||||||
pub mod smartbi;
|
pub mod smartbi;
|
||||||
@@ -20,8 +21,9 @@ use crate::{
|
|||||||
domain::models::security_notice::CreateSecurityNotice,
|
domain::models::security_notice::CreateSecurityNotice,
|
||||||
output::plugins::sec_notice::{
|
output::plugins::sec_notice::{
|
||||||
apple::AppleNoticePlugin, fanruan::FanRuanNoticePlugin, firefox::FirefoxNoticePlugin,
|
apple::AppleNoticePlugin, fanruan::FanRuanNoticePlugin, firefox::FirefoxNoticePlugin,
|
||||||
oracle::OracleNoticePlugin, seeyon::SeeyonNoticePlugin, smartbi::SmartbiNoticePlugin,
|
grafana::GrafanaNoticePlugin, oracle::OracleNoticePlugin, seeyon::SeeyonNoticePlugin,
|
||||||
vmware::VmwareNoticePlugin, weaver::WeaverNoticePlugin, yongyou::YongYouNoticePlugin,
|
smartbi::SmartbiNoticePlugin, vmware::VmwareNoticePlugin, weaver::WeaverNoticePlugin,
|
||||||
|
yongyou::YongYouNoticePlugin,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,6 +41,7 @@ pub fn init(sender: UnboundedSender<CreateSecurityNotice>) -> AppResult<()> {
|
|||||||
OracleNoticePlugin::try_new(sender.clone())?;
|
OracleNoticePlugin::try_new(sender.clone())?;
|
||||||
FirefoxNoticePlugin::try_new(sender.clone())?;
|
FirefoxNoticePlugin::try_new(sender.clone())?;
|
||||||
AppleNoticePlugin::try_new(sender.clone())?;
|
AppleNoticePlugin::try_new(sender.clone())?;
|
||||||
|
GrafanaNoticePlugin::try_new(sender.clone())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user