Compare commits

...

11 Commits
dev ... main

Author SHA1 Message Date
a7f2a4ead0 add: package2rpminfo
Signed-off-by: Jia Chao <jiachao2130@126.com>
2024-09-18 10:05:40 +08:00
bb88e5f0b8 update: _dnf_summary
Signed-off-by: Jia Chao <jiachao2130@126.com>
2024-09-12 10:52:27 +08:00
e5b9f185c5 dnf 支持,添加骨架
Signed-off-by: Jia Chao <jiachao2130@126.com>
2024-08-26 19:37:12 +08:00
fcd1a4dc7d 添加可配置支持
Signed-off-by: Jia Chao <jiachao2130@126.com>
2024-08-14 14:17:33 +08:00
6c2b47d4f4 初步完成 --repoter
Signed-off-by: Jia Chao <jiachao2130@126.com>
2024-08-13 14:54:20 +08:00
adf2180d11 增加 Reporter 结构体,用于存储所有更新统计
Signed-off-by: Jia Chao <jiachao2130@126.com>
2024-08-12 16:31:29 +08:00
b3f2204a88 完成:--sas 查找并列出并查看已修复但尚未更新的 sa 安全公告信息
Signed-off-by: Jia Chao <jiachao2130@126.com>
2024-08-09 11:37:10 +08:00
4510e3500d 完成:--cves 查找并列出并查看已修复但尚未更新的 cve 漏洞信息
Signed-off-by: Jia Chao <jiachao2130@126.com>
2024-08-08 14:56:59 +08:00
3b9396c500 完成:列出所有已发布但未修复的安全公告
Signed-off-by: Jia Chao <jiachao2130@126.com>
2024-08-06 15:10:43 +08:00
853be957ee cargo fmt
Signed-off-by: Jia Chao <jiachao2130@126.com>
2024-08-06 11:33:08 +08:00
f82ff6ac72 完成扫描整体结果展示
Signed-off-by: Jia Chao <jiachao2130@126.com>
2024-08-06 11:32:38 +08:00
7 changed files with 43833 additions and 52 deletions

View File

@ -3,11 +3,17 @@ name = "cuvat-rs"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "cuvat"
path = "src/main.rs"
[dependencies]
clap = { version = "4.0", features = ["derive"] }
serde = { version = "1", features = ["serde_derive"] }
serde_json = { version = "1.0" }
toml = { version = "0.8" }
updateinfo-xmlparser = { git = "https://git.zhgsun.com:8089/jiachao2130/updateinfo-xmlparser.git", version = "0.1.0" }
updateinfo-xmlparser = { git = "https://git.zhgsun.com:8089/jiachao2130/updateinfo-xmlparser.git", version = "0.1.1" }
cvrf-xmlparser = { git = "https://git.zhgsun.com:8089/jiachao2130/cvrf-xmlparser.git", version = "0.1.0" }
rpm-rs = { git = "https://git.zhgsun.com:8089/jiachao2130/rpm-rs.git", version = "0.1.0" }
lazy_static = { version = "1.5" }
ccutils = { git = "https://git.zhgsun.com:8089/jiachao2130/ccutils.git", version = "0.1.0" }

View File

@ -1,8 +1,8 @@
use std::collections::HashMap;
use cvrf_xmlparser::{SaInfo, CVE};
use cvrf_xmlparser::{CUSA, CVE};
use serde::{Deserialize, Serialize};
use updateinfo_xmlparser::{UpdateInfoDb, RpmInfo};
use updateinfo_xmlparser::{RpmInfo, UpdateInfoDb};
// PackageDb 从 updateinfo 中获取
// 以包名为键,值为一个 Vector ,里面包含更新的不同版本的 rpm 包信息
@ -13,9 +13,7 @@ pub struct PackgeDb {
impl PackgeDb {
pub fn new() -> Self {
PackgeDb {
db: HashMap::new(),
}
PackgeDb { db: HashMap::new() }
}
/// 从已有的 updateinfo 仓库文件中,获取所有与安全更新相关的软件包
@ -43,24 +41,26 @@ impl PackgeDb {
// 其键为 SA id值为详情
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SaDb {
db: HashMap<String, SaInfo>,
db: HashMap<String, CUSA>,
}
impl SaDb {
pub fn new() -> Self {
SaDb {
db: HashMap::new(),
}
SaDb { db: HashMap::new() }
}
/// 一般来自对 cvrf 文件解析并转换为 SaInfo 的文本数据文件
pub fn db(&self) -> &HashMap<String, CUSA> {
&self.db
}
/// 一般来自对 cvrf 文件解析并转换为 CUSA 的文本数据文件
pub fn load_from_file(&mut self, file: &str) -> crate::Result<()> {
let data = std::fs::read_to_string(file)?;
self.db = toml::from_str(&data)?;
self.db = serde_json::from_str(&data)?;
Ok(())
}
/// 从 SaInfo 中提取出所有的 CVE 源
/// 从 CUSA 中提取出所有的 CVE 源
pub fn get_cvedb(&self) -> CveDb {
let mut cvedb = CveDb::new();
@ -84,9 +84,7 @@ pub struct CveDb {
impl CveDb {
#[allow(dead_code)]
pub fn new() -> Self {
CveDb {
db: HashMap::new(),
}
CveDb { db: HashMap::new() }
}
}

View File

@ -1,43 +1,104 @@
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::process::Command;
use std::str::FromStr;
use ccutils::cmd::IsOk;
use cvrf_xmlparser::{Severity, CUSA};
use lazy_static::lazy_static;
use rpm_rs::rpm::{
get_installed_packages,
Package,
};
use rpm_rs::rpm::{get_installed_packages, Package};
use rpm_rs::rpmio::rpmvercmp;
use updateinfo_xmlparser::{UpdateInfoDb, RpmInfo};
use updateinfo_xmlparser::{RpmInfo, UpdateInfoDb};
use crate::analyzer::db::{CveDb, PackgeDb, SaDb};
use crate::cli::Cli;
use crate::analyzer::db::PackgeDb;
use crate::CONFIG;
lazy_static! {
pub static ref update_pkgs: PackgeDb= {
pub static ref update_pkgs: PackgeDb = {
let mut updatedb = UpdateInfoDb::new();
updatedb.load_xml("test/updateinfo.xml").unwrap();
updatedb.load_xml(&CONFIG.updateinfo()).unwrap();
let mut pkgdb = PackgeDb::new();
pkgdb.load_from_updateinfodb(&updatedb);
pkgdb
};
pub static ref sadb: SaDb = {
let mut _sadb = SaDb::new();
_sadb.load_from_file(&CONFIG.sainfo()).unwrap();
_sadb
};
pub static ref cvedb: CveDb = sadb.get_cvedb();
}
pub mod db;
struct Reporter {
// 已修复的安全更新
fixed: HashMap<CUSA, HashSet<RpmInfo>>,
// 可用的安全更新
avaliable: HashMap<CUSA, HashSet<RpmInfo>>,
}
impl Reporter {
// 新建一个空的 Reporter
pub fn new() -> Self {
let fixed = HashMap::new();
let avaliable = HashMap::new();
Self { fixed, avaliable }
}
// get fixed, 不可写
pub fn get_fixed(&self) -> &HashMap<CUSA, HashSet<RpmInfo>> {
&self.fixed
}
// get avaliable, 只读
pub fn get_avaliable(&self) -> &HashMap<CUSA, HashSet<RpmInfo>> {
&self.avaliable
}
// 添加包到已修复
pub fn add_to_fixed(&mut self, cusa: &CUSA, rpm: RpmInfo) {
// 可以找到则直接 insert
if let Some(rpms) = self.fixed.get_mut(cusa) {
rpms.insert(rpm);
} else {
let mut rpms = HashSet::new();
rpms.insert(rpm);
self.fixed.insert(cusa.clone(), rpms);
}
}
// 添加包到可用更新
pub fn add_to_avaliable(&mut self, cusa: &CUSA, rpm: RpmInfo) {
// 可以找到则直接 insert
if let Some(rpms) = self.avaliable.get_mut(cusa) {
rpms.insert(rpm);
} else {
let mut rpms = HashSet::new();
rpms.insert(rpm);
self.avaliable.insert(cusa.clone(), rpms);
}
}
}
pub fn cuvat_run(cli: &Cli) -> crate::Result<()> {
// 只报告 cve 相关
if cli.cves {
return list_cves(cli)
return list_cves(cli);
}
// 只报告 sa 相关
if cli.sas {
return list_sas(cli)
return list_sas(cli);
}
// 生成报告
if cli.report {
return repoter(cli)
return repoter(cli);
}
// 默认报告格式
@ -45,71 +106,338 @@ pub fn cuvat_run(cli: &Cli) -> crate::Result<()> {
}
fn list_cves(cli: &Cli) -> crate::Result<()> {
if cli.dnf {
return _dnf_list_cves(cli);
}
let reporter = _reporter()?;
let avaliable = reporter.get_avaliable();
let severity = Severity::from_str(&cli.severity)?;
let mut _cves = HashSet::new();
cli.sources.iter().for_each(|id| {
_cves.insert(id);
});
let mut cves = HashSet::new();
let mut msg = String::new();
// 仅针对可用更新
for (cusa, _) in avaliable {
// 过滤
if cusa.severity() < &severity {
continue;
}
cusa.cves().iter().for_each(|cve| {
if let Some(_) = _cves.get(&cve.id) {
let _ = cves.insert(cve.clone());
_cves.remove(&cve.id);
}
});
}
for cve in &cves {
if cli.info {
msg = format!("{msg}\n\n{cve:#?}");
} else {
msg = format!("{msg}\n{}", cve.id);
}
}
if !_cves.is_empty() {
msg = format!("{msg}\n\nUnaffected CVEs:\n{_cves:#?}")
}
println!("{msg}");
Ok(())
}
fn _dnf_list_cves(_cli: &Cli) -> crate::Result<()> {
Ok(())
}
fn list_sas(cli: &Cli) -> crate::Result<()> {
if cli.dnf {
return _dnf_list_sas(cli);
}
let reporter = _reporter()?;
let avaliable = reporter.get_avaliable();
let severity = Severity::from_str(&cli.severity)?;
let mut _sas = HashSet::new();
cli.sources.iter().for_each(|id| {
_sas.insert(id);
});
let mut sas = HashSet::new();
let mut msg = String::new();
// 仅针对可用更新
for (cusa, _) in avaliable {
// 过滤
if cusa.severity() < &severity {
continue;
}
if let Some(_) = _sas.get(&cusa.id) {
let _ = sas.insert(cusa.clone());
_sas.remove(&cusa.id);
}
}
for sa in &sas {
if cli.info {
msg = format!("{msg}\n\n{sa:#?}");
} else {
msg = format!("{msg}\n{}", sa.id);
}
}
if !_sas.is_empty() {
msg = format!("{msg}\n\nUnaffected SAs:\n{_sas:#?}")
}
println!("{msg}");
Ok(())
}
fn repoter(cli: &Cli) -> crate::Result<()> {
fn _dnf_list_sas(_cli: &Cli) -> crate::Result<()> {
Ok(())
}
// 最为详尽的报告,包括当前系统中软件包所涉及到,已修复和可用但未修复
// 的所有软件包的列表。
// 其格式如下:
// XXXXXX 报告
// FIXED:
// SA ID
// CVES
// effected rpms list
// ...
//
// ====================
// AVALIABLE:
// SA ID
// CVES
// effected rpms list
// ...
//
fn repoter(cli: &Cli) -> crate::Result<()> {
if cli.dnf {
println!("暂不支持此功能,请联系开发者");
return Ok(());
}
let reporter = _reporter()?;
let fixed = reporter.get_fixed();
let avaliable = reporter.get_avaliable();
let mut msg = String::new();
msg = format!("{msg}系统漏洞信息检测报告\n\n");
// fixed
msg = format!("{msg}已修复的安全公告\n");
for (cusa, rpms) in fixed {
// sa cves 信息
msg = format!("{msg}{} (fixed):\n", cusa.id());
let mut cves = vec![];
cusa.cves().iter().for_each(|cve| {
cves.push(&cve.id);
});
msg = format!("{msg}{:2}涉及 CVE 列表:{:?}\n", "", cves);
for rpm in rpms {
msg = format!("{msg}{:4}{}\n", "", rpm.nevra());
}
// 空
msg = format!("{msg}\n");
}
// "==============="
let mut seprator = String::new();
for _ in 0..80 {
seprator.push('=');
}
msg = format!("{msg}\n{seprator}\n\n\n");
// avaliable updates
msg = format!("{msg}可用更新\n");
for (cusa, rpms) in avaliable {
// sa cves 信息
msg = format!("{msg}{} (avaliable):\n", cusa.id());
let mut cves = vec![];
cusa.cves().iter().for_each(|cve| {
cves.push(&cve.id);
});
msg = format!("{msg}{:2}涉及 CVE 列表:{:?}\n", "", cves);
for rpm in rpms {
msg = format!("{msg}{:4}{}\n", "", rpm.nevra());
}
// 空
msg = format!("{msg}\n");
}
println!("{msg}");
// summary 一下
summary(cli)
}
fn summary(cli: &Cli) -> crate::Result<()> {
let mut sa_ids: HashSet<String> = HashSet::new();
if cli.dnf {
return _dnf_summary(cli);
}
let reporter = _reporter()?;
let avaliable = reporter.get_avaliable();
let severity = Severity::from_str(&cli.severity)?;
let mut total = 0;
let mut res = vec![0; 5];
let mut lists = vec![vec![]; 5];
let mut msg = String::new();
for (cusa, _) in avaliable {
// 过滤
if cusa.severity() < &severity {
continue;
}
let pos: usize = cusa.severity().into();
total += 1;
res[pos] += 1;
lists[pos].push(cusa);
}
// 如果需要列出详细的 sa
if cli.list {
msg = format!("当前系统未修复的公告列表(共 {total} 个):\n");
for sas in &lists {
for sa in sas {
msg = format!("{msg}\n{:>4}{}: {:?}", "", sa.id(), sa.severity());
}
}
//msg = format!("{msg}\n\n");
} else {
msg = format!(
"{msg}
{}
{:>3}
{:>3}
{:>3}
{:>3}
",
total, res[4], res[3], res[2], res[1]
);
}
println!("{msg}");
Ok(())
}
fn _dnf_summary(_cli: &Cli) -> crate::Result<()> {
let cmd = Command::new("dnf")
.arg("updateinfo")
.output()
.expect("执行 `dnf udpateinfo` 命令失败");
match cmd.result() {
Ok(res) => println!("{res}"),
Err(err) => eprintln!("{err}"),
}
Ok(())
}
fn _reporter() -> crate::Result<Reporter> {
let mut reporter = Reporter::new();
// 当前系统所有已安装的 rpm 包
let installed = get_installed_packages();
// installed 已被消费掉
// latest_installed 是所有最新版本软件包的 Vec
let latest_installed: Vec<Package> = installed.into_iter().map(|(_, pkgs)| {
let mut latest = pkgs[0].clone();
for pkg in pkgs.into_iter() {
latest = rpmdb_package_vercmp(latest, pkg);
}
latest
}).collect();
let latest_installed: Vec<Package> = installed
.into_iter()
.map(|(_, pkgs)| {
let mut latest = pkgs[0].clone();
for pkg in pkgs.into_iter() {
latest = rpmdb_package_vercmp(latest, pkg);
}
latest
})
.collect();
// 获取 sa 更新列表
for pkg in latest_installed {
let name = pkg.name();
if let Some(updates) = update_pkgs.db().get(name) {
for update in updates {
// 因 updateinfo 里包含所有架构,故于此做个判断
// 是否考虑架构信息在不同的版本有所不同?
if update.arch() != pkg.arch().unwrap() {
continue;
}
let sainfo = sadb.db().get(update.sa()).unwrap();
// epoch 判断
match (update.epoch(), pkg.epoch()) {
(Some(_), None) => {
let _ = sa_ids.insert(update.sa().into());
},
(None, Some(_)) => continue,
reporter.add_to_avaliable(&sainfo, update.clone());
}
(None, Some(_)) => {
reporter.add_to_fixed(&sainfo, update.clone());
}
_ => {}
}
// evr 对比
if rpmvercmp(&update.evr(), &pkg.evr()) > 0 {
sa_ids.insert(update.sa().into());
reporter.add_to_avaliable(&sainfo, update.clone());
} else {
reporter.add_to_fixed(&sainfo, update.clone());
}
}
}
}
println!("{:#?}", sa_ids.len());
// TODO: sa -> { rpms, cves }
Ok(())
Ok(reporter)
}
// 对比两个 rpm Package 的版本,返回最新的一个
// TODO: 使用 RpmInfo 作为对比对象
fn rpmdb_package_vercmp(pa: Package, pb: Package) -> Package {
// 首先进行 epoch 的比较
match (pa.epoch(), pb.epoch()) {
(Some(_), None) => return pa,
(None, Some(_)) => return pb,
_ => {}, // 继续往下对比
_ => {} // 继续往下对比
}
if rpmvercmp(&pa.evr(), &pb.evr()) > 0 {
return pa
return pa;
} else {
return pb
return pb;
}
}
// 将 RPMDB 里的 Package 转换为 RpmInfo方便统一进行版本对比和处理。
fn package2rpminfo(pkg: Package) -> RpmInfo {
let mut rpminfo = RpmInfo::new();
// 目前只需要将 name/epoch/version/release/arch 几项填充即可
rpminfo.set("name", pkg.name().to_string());
// Option<i32> => String
if let Some(epoch) = pkg.epoch() {
rpminfo.set("epoch", epoch.to_string());
}
rpminfo.set("version", pkg.version().to_string());
rpminfo.set("release", pkg.release().to_string());
// Option<&str> => String
if let Some(arch) = pkg.arch() {
rpminfo.set("arch", arch.to_string());
}
rpminfo
}

View File

@ -14,11 +14,11 @@ pub struct Cli {
#[arg(short, long, default_value_t = false)]
pub list: bool,
/// 列出并查看已修复但尚未更新的 cve 漏洞信息
/// 查找并列出并查看已修复但尚未更新的 cve 漏洞信息
#[arg(long, default_value_t = false)]
pub cves: bool,
/// 列出并查看已修复但尚未更新的 sa 安全公告信息
/// 查找并列出并查看已修复但尚未更新的 sa 安全公告信息
#[arg(long, default_value_t = false)]
pub sas: bool,
@ -30,8 +30,8 @@ pub struct Cli {
#[arg(action = clap::ArgAction::Append)]
pub sources: Vec<String>,
/// 设置过滤级别,由高到低为:[Critical, Important, Moderate, Low]
#[arg(long, default_value_t = String::from("Low"))]
/// 设置过滤级别,由高到低为:[Critical, High, Medium, Low, None]
#[arg(long, default_value_t = String::from("None"))]
pub severity: String,
/// 生成漏洞扫描报告

44
src/config.rs Normal file
View File

@ -0,0 +1,44 @@
use serde::{Deserialize, Serialize};
// 可编辑的配置,一般位于 /etc/cuavrs 下
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Config {
// 从此处更新 updateinfo, 以及 cusa 数据库
remote: String,
// 本地数据存储位置
// /var/cache/cuvars
local: String,
}
impl Config {
// 从文件中读取配置信息
pub fn from(path: &str) -> crate::Result<Self> {
let data = std::fs::read_to_string(path)?;
Ok(serde_json::from_str::<Self>(&data)?)
}
// 从此地址获取更新
pub fn remote(&self) -> &str {
&self.remote
}
// 本地数据存储位置
pub fn local(&self) -> &str {
&self.local
}
pub fn update(&self) -> crate::Result<()> {
// TODO
Ok(())
}
pub fn updateinfo(&self) -> String {
format!("{}/updateinfo.xml", &self.local)
}
pub fn sainfo(&self) -> String {
format!("{}/sainfos.json", &self.local)
}
}

View File

@ -1,4 +1,10 @@
use std::path::Path;
use lazy_static::lazy_static;
pub mod cli;
pub mod config;
use config::Config;
mod analyzer;
@ -9,6 +15,19 @@ pub type Error = Box<dyn std::error::Error + Send + Sync>;
/// 定义 crate::Result
pub type Result<T> = std::result::Result<T, Error>;
// 首先需要读取并载入配置信息
// 默认为 /etc/cuavrs 下
lazy_static! {
pub static ref CONFIG: Config = {
let default = Path::new("/etc/cuavrs/cuvat.json");
if default.is_file() {
Config::from(default.to_str().unwrap()).unwrap()
} else {
Config::from("cuvat.json").unwrap()
}
};
}
pub fn cumain() -> Result<()> {
let cli = cli::parse();
// 初始化使用 rpm 默认配置

43386
test/cvrf_sainfos.json Normal file

File diff suppressed because one or more lines are too long