feat: support grafana sec notice (#23)
Some checks failed
ci / build-rust (ubuntu-latest) (push) Has been cancelled
ci / build-image (push) Has been cancelled

This commit is contained in:
fan-tastic-z
2025-10-23 18:05:24 +08:00
committed by GitHub
parent a51cadaccd
commit 7ed9cec8cb
4 changed files with 249 additions and 13 deletions

View File

@@ -17,17 +17,18 @@ VulnFeed 是一个用于收集和推送高价值漏洞和补丁公告信息的
当抓取安全补丁公告的站点数据:
| 名称 | 地址 | 推送策略 |
| --------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
| 用友安全中心 | <https://security.yonyou.com/#/home> | 近三条数据 |
| 泛微ECOLOGY安全补丁包 | <https://www.weaver.com.cn/cs/securityDownload.html?src=cn> | "EC9.0全量补丁", "EC8.0全量补丁", "EC10.0安全补丁" |
| Smartbi安全补丁包 | <https://www.smartbi.com.cn/patchinfo> | 近三条数据 |
| 帆软安全漏洞声明 | <https://help.fanruan.com/finereport/doc-view-4833.html> | 近三条数据 |
| 致远安全补丁 | <https://service.seeyon.com/patchtools/tp.html#/patchList?type=%E5%AE%89%E5%85%A8%E8%A1%A5%E4%B8%81> | V5的近三条数据 |
| Vmware安全公告 | <https://support.broadcom.com/web/ecx/security-advisory?> | 近十条数据 |
| Oracle安全公告 | <https://www.oracle.com/cn/security-alerts/> | 近三条数据 |
| Firefox安全公告 | <https://www.mozilla.org/en-US/security/known-vulnerabilities/firefox/> | 近三条数据 |
| Apple安全公告 | <https://support.apple.com/zh-cn/100100> | 近十条数,不包含没有链接的据 |
| 名称 | 地址 | 推送策略 |
| --------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| 用友安全中心 | <https://security.yonyou.com/#/home> | 近三条数据 |
| 泛微ECOLOGY安全补丁包 | <https://www.weaver.com.cn/cs/securityDownload.html?src=cn> | "EC9.0全量补丁", "EC8.0全量补丁", "EC10.0安全补丁" |
| Smartbi安全补丁包 | <https://www.smartbi.com.cn/patchinfo> | 近三条数据 |
| 帆软安全漏洞声明 | <https://help.fanruan.com/finereport/doc-view-4833.html> | 近三条数据 |
| 致远安全补丁 | <https://service.seeyon.com/patchtools/tp.html#/patchList?type=%E5%AE%89%E5%85%A8%E8%A1%A5%E4%B8%81> | V5的近三条数据 |
| Vmware安全公告 | <https://support.broadcom.com/web/ecx/security-advisory?> | 近十条数据 |
| Oracle安全公告 | <https://www.oracle.com/cn/security-alerts/> | 近三条数据 |
| Firefox安全公告 | <https://www.mozilla.org/en-US/security/known-vulnerabilities/firefox/> | 近三条数据 |
| Apple安全公告 | <https://support.apple.com/zh-cn/100100> | 近十条数,不包含没有链接的据 |
| Grafana安全公告 | <https://grafana.com/tags/security/> | 第一页数据中以"Grafana security release" 或 "Grafana security update" 开头的公告 |
![app](./images/app.jpg)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 KiB

After

Width:  |  Height:  |  Size: 142 KiB

View 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 &notices {
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());
}
}
}

View File

@@ -1,6 +1,7 @@
pub mod apple;
pub mod fanruan;
pub mod firefox;
pub mod grafana;
pub mod oracle;
pub mod seeyon;
pub mod smartbi;
@@ -20,8 +21,9 @@ use crate::{
domain::models::security_notice::CreateSecurityNotice,
output::plugins::sec_notice::{
apple::AppleNoticePlugin, fanruan::FanRuanNoticePlugin, firefox::FirefoxNoticePlugin,
oracle::OracleNoticePlugin, seeyon::SeeyonNoticePlugin, smartbi::SmartbiNoticePlugin,
vmware::VmwareNoticePlugin, weaver::WeaverNoticePlugin, yongyou::YongYouNoticePlugin,
grafana::GrafanaNoticePlugin, oracle::OracleNoticePlugin, seeyon::SeeyonNoticePlugin,
smartbi::SmartbiNoticePlugin, vmware::VmwareNoticePlugin, weaver::WeaverNoticePlugin,
yongyou::YongYouNoticePlugin,
},
};
@@ -39,6 +41,7 @@ pub fn init(sender: UnboundedSender<CreateSecurityNotice>) -> AppResult<()> {
OracleNoticePlugin::try_new(sender.clone())?;
FirefoxNoticePlugin::try_new(sender.clone())?;
AppleNoticePlugin::try_new(sender.clone())?;
GrafanaNoticePlugin::try_new(sender.clone())?;
Ok(())
}