use std::collections::HashMap;
use std::path::Path;
use anyhow::{Context, Result};
use tracing::{debug, info};
use crate::osv::{OsvResult, OsvVuln};
use crate::vex::{VexStatement, VexStatus};
const JUSTIFICATION: &str = "vulnerable_code_not_present";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConfigValue {
BuiltIn,
Module,
Disabled,
NotSet,
}
fn known_config_mappings() -> HashMap<&'static str, Vec<&'static str>> {
HashMap::from([
("bluez5", vec!["BT", "BLUETOOTH"]),
("wpa-supplicant", vec!["CFG80211", "WLAN", "MAC80211"]),
("openssh", vec!["CRYPTO", "ASYMMETRIC_KEY_TYPE"]),
("openssl", vec!["CRYPTO", "TLS"]),
("iptables", vec!["NETFILTER", "IP_NF_IPTABLES"]),
("systemd", vec!["CGROUPS", "FANOTIFY"]),
("mesa", vec!["DRM"]),
("imx-gpu-viv", vec!["DRM", "MXC_GPU_VIV"]),
("linux-imx", vec!["ARCH_MXC"]),
("u-boot-imx", vec![]),
])
}
fn is_userspace_package(name: &str) -> bool {
let prefixes = [
"glibc",
"bash",
"coreutils",
"dbus",
"systemd",
"python",
"ruby",
"perl",
"php",
"node",
"npm",
"vim",
"nano",
"gcc",
"binutils",
"make",
"cmake",
"autoconf",
"busybox",
"shadow",
"util-linux",
"curl",
"wget",
"openssl",
"openssh",
"zlib",
"libpng",
"libxml2",
"sqlite",
"icu",
"mesa",
"wayland",
"gstreamer",
"opkg",
"dnsmasq",
"avahi",
"networkmanager",
"bluez",
"wpa-supplicant",
"iptables",
];
let lower = name.to_lowercase();
prefixes.iter().any(|p| lower.starts_with(p))
}
pub fn parse_kernel_config(path: &Path) -> Result<HashMap<String, ConfigValue>> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read kernel config: {}", path.display()))?;
let mut config = HashMap::new();
for line in content.lines() {
let line = line.trim();
if line.starts_with("CONFIG_") {
if let Some(rest) = line.strip_prefix("CONFIG_")
&& let Some((key, value)) = rest.split_once('=')
{
let val = match value {
"y" => ConfigValue::BuiltIn,
"m" => ConfigValue::Module,
"n" => ConfigValue::Disabled,
_ => continue,
};
config.insert(key.to_string(), val);
}
} else if line.starts_with("# CONFIG_")
&& line.ends_with("is not set")
&& let Some(key_part) = line.strip_prefix("# CONFIG_")
&& let Some(key) = key_part.strip_suffix(" is not set")
{
config.insert(key.to_string(), ConfigValue::NotSet);
}
}
info!("Parsed {} kernel config entries", config.len());
Ok(config)
}
fn extract_config_keys_from_package(pkg_name: &str) -> Vec<String> {
let known = known_config_mappings();
if let Some(keys) = known.get(pkg_name) {
return keys.iter().map(|k| k.to_string()).collect();
}
if is_userspace_package(pkg_name) {
return vec![];
}
let base = pkg_name.to_uppercase().replace('-', "_");
vec![
base.clone(),
format!("{}_DRIVER", base),
format!("{}_MODULE", base),
]
}
pub fn filter_by_kernel_config(
results: &[OsvResult],
config: &HashMap<String, ConfigValue>,
) -> (Vec<VexStatement>, Vec<usize>) {
let mut statements = Vec::new();
let mut filtered_indices = Vec::new();
for (i, result) in results.iter().enumerate() {
let config_keys = extract_config_keys_from_package(&result.package.name);
if config_keys.is_empty() {
continue;
}
let has_existing_disabled = config_keys.iter().any(|key| {
matches!(
config.get(key),
Some(ConfigValue::Disabled) | Some(ConfigValue::NotSet)
)
});
let existing_keys: Vec<_> = config_keys
.iter()
.filter(|key| config.contains_key(*key))
.collect();
let all_existing_disabled = !existing_keys.is_empty()
&& existing_keys.iter().all(|key| {
matches!(
config.get(*key),
Some(ConfigValue::Disabled) | Some(ConfigValue::NotSet)
)
});
if has_existing_disabled || all_existing_disabled {
filtered_indices.push(i);
for vuln in &result.vulns {
statements.push(build_statement(vuln, result));
}
debug!(
"Filtered package '{}' (not active in kernel config)",
result.package.name
);
}
}
(statements, filtered_indices)
}
fn build_statement(vuln: &OsvVuln, result: &OsvResult) -> VexStatement {
let purl = result
.package
.purl
.clone()
.unwrap_or_else(|| format!("pkg:generic/{}", result.package.name));
VexStatement {
vulnerability_name: vuln.id.clone(),
product_purl: purl,
status: VexStatus::NotAffected,
justification: Some(JUSTIFICATION.to_string()),
impact_statement: Some(format!(
"The driver '{}' is not enabled in the kernel configuration (.config).",
result.package.name
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn test_parse_kernel_config() {
let mut file = tempfile::NamedTempFile::new().unwrap();
writeln!(file, "CONFIG_USB_STORAGE=y").unwrap();
writeln!(file, "CONFIG_EXT4_FS=m").unwrap();
writeln!(file, "# CONFIG_BT is not set").unwrap();
writeln!(file, "# CONFIG_NFS_FS is not set").unwrap();
writeln!(file, "CONFIG_NET=y").unwrap();
let config = parse_kernel_config(file.path()).unwrap();
assert_eq!(config.get("USB_STORAGE"), Some(&ConfigValue::BuiltIn));
assert_eq!(config.get("EXT4_FS"), Some(&ConfigValue::Module));
assert_eq!(config.get("BT"), Some(&ConfigValue::NotSet));
assert_eq!(config.get("NFS_FS"), Some(&ConfigValue::NotSet));
assert_eq!(config.get("NET"), Some(&ConfigValue::BuiltIn));
assert!(config.get("NONEXISTENT").is_none());
}
#[test]
fn test_known_mappings() {
let keys = extract_config_keys_from_package("bluez5");
assert!(keys.contains(&"BT".to_string()));
assert!(keys.contains(&"BLUETOOTH".to_string()));
let keys = extract_config_keys_from_package("openssl");
assert!(keys.contains(&"CRYPTO".to_string()));
}
#[test]
fn test_userspace_packages_skipped() {
let keys = extract_config_keys_from_package("glibc");
assert!(keys.is_empty());
let keys = extract_config_keys_from_package("bash");
assert!(keys.is_empty());
let keys = extract_config_keys_from_package("python3");
assert!(keys.is_empty());
}
#[test]
fn test_config_string_values_ignored() {
let mut file = tempfile::NamedTempFile::new().unwrap();
std::io::Write::write_all(
&mut file,
b"CONFIG_CMDLINE=\"console=ttyAMA0\"\nCONFIG_VALUE=\"\"\nCONFIG_NORMAL=y\n",
)
.unwrap();
let config = parse_kernel_config(file.path()).unwrap();
assert_eq!(config.get("NORMAL"), Some(&ConfigValue::BuiltIn));
assert!(config.get("CMDLINE").is_none());
assert!(config.get("VALUE").is_none());
}
#[test]
fn test_config_explicit_n() {
let mut file = tempfile::NamedTempFile::new().unwrap();
std::io::Write::write_all(&mut file, b"CONFIG_DISABLED=n\n").unwrap();
let config = parse_kernel_config(file.path()).unwrap();
assert_eq!(config.get("DISABLED"), Some(&ConfigValue::Disabled));
}
#[test]
fn test_filter_with_disabled_config() {
let mut config = std::collections::HashMap::new();
config.insert("BT".to_string(), ConfigValue::NotSet);
config.insert("USB_STORAGE".to_string(), ConfigValue::BuiltIn);
let results = vec![OsvResult {
package: crate::sbom::SbomPackage {
_spdx_id: "SPDXRef-1".into(),
name: "bluez5".into(),
version: Some("5.68".into()),
purl: Some("pkg:generic/bluez5@5.68".into()),
},
vulns: vec![OsvVuln {
id: "CVE-2024-0001".into(),
_modified: "2024-01-01T00:00:00Z".into(),
aliases: vec![],
}],
}];
let (statements, indices) = filter_by_kernel_config(&results, &config);
assert_eq!(indices.len(), 1);
assert_eq!(statements.len(), 1);
assert_eq!(statements[0].status, VexStatus::NotAffected);
}
}