use std::collections::HashSet;
use super::table;
pub(super) fn normalize_for_pypi(name: &str) -> String {
name.to_lowercase().replace('_', "-")
}
pub(super) fn package_to_pypi_link(name: &str) -> String {
let normalized = normalize_for_pypi(name);
format!(
"[{}](https://pypi.org/project/{}/)",
table::escape_markdown_table_cell(name),
normalized
)
}
pub(super) fn vulnerability_id_to_link(id: &str) -> String {
let url = if id.starts_with("CVE-") {
format!("https://nvd.nist.gov/vuln/detail/{}", id)
} else if id.starts_with("GHSA-") {
format!("https://github.com/advisories/{}", id)
} else {
format!("https://osv.dev/vulnerability/{}", id)
};
format!("[{}]({})", table::escape_markdown_table_cell(id), url)
}
pub(super) fn format_package_name(
name: &str,
verified_packages: Option<&HashSet<String>>,
) -> String {
match verified_packages {
None => package_to_pypi_link(name),
Some(verified) => {
if verified.contains(name) {
package_to_pypi_link(name)
} else {
table::escape_markdown_table_cell(name)
}
}
}
}
#[cfg(test)]
mod tests {
use super::super::MarkdownFormatter;
use super::*;
use crate::application::read_models::{
ComponentView, DependencyView, LicenseView, SbomMetadataView, SbomReadModel, SeverityView,
VulnerabilityReportView, VulnerabilitySummary, VulnerabilityView,
};
use crate::i18n::Locale;
use crate::ports::outbound::SbomFormatter;
use std::collections::HashMap;
fn create_test_read_model() -> SbomReadModel {
SbomReadModel {
metadata: SbomMetadataView {
timestamp: "2024-01-01T00:00:00Z".to_string(),
tool_name: "uv-sbom".to_string(),
tool_version: "1.0.0".to_string(),
serial_number: "urn:uuid:test-123".to_string(),
component: None,
},
components: vec![
ComponentView {
bom_ref: "pkg:pypi/requests@2.31.0".to_string(),
name: "requests".to_string(),
version: "2.31.0".to_string(),
purl: "pkg:pypi/requests@2.31.0".to_string(),
license: Some(LicenseView {
spdx_id: Some("Apache-2.0".to_string()),
name: "Apache License 2.0".to_string(),
url: None,
}),
description: Some("HTTP library".to_string()),
sha256_hash: None,
is_direct_dependency: true,
},
ComponentView {
bom_ref: "pkg:pypi/urllib3@1.26.0".to_string(),
name: "urllib3".to_string(),
version: "1.26.0".to_string(),
purl: "pkg:pypi/urllib3@1.26.0".to_string(),
license: Some(LicenseView {
spdx_id: Some("MIT".to_string()),
name: "MIT License".to_string(),
url: None,
}),
description: None,
sha256_hash: None,
is_direct_dependency: false,
},
],
dependencies: None,
vulnerabilities: None,
license_compliance: None,
resolution_guide: None,
upgrade_recommendations: None,
}
}
#[test]
fn test_normalize_for_pypi_underscore() {
assert_eq!(normalize_for_pypi("typing_extensions"), "typing-extensions");
}
#[test]
fn test_normalize_for_pypi_uppercase() {
assert_eq!(normalize_for_pypi("Flask"), "flask");
}
#[test]
fn test_normalize_for_pypi_already_normalized() {
assert_eq!(normalize_for_pypi("ruamel-yaml"), "ruamel-yaml");
}
#[test]
fn test_package_to_pypi_link_simple() {
assert_eq!(
package_to_pypi_link("requests"),
"[requests](https://pypi.org/project/requests/)"
);
}
#[test]
fn test_package_to_pypi_link_with_underscore() {
assert_eq!(
package_to_pypi_link("typing_extensions"),
"[typing_extensions](https://pypi.org/project/typing-extensions/)"
);
}
#[test]
fn test_package_to_pypi_link_with_uppercase() {
assert_eq!(
package_to_pypi_link("Flask"),
"[Flask](https://pypi.org/project/flask/)"
);
}
#[test]
fn test_format_basic_contains_pypi_links() {
let model = create_test_read_model();
let formatter = MarkdownFormatter::new(Locale::En);
let markdown = formatter.format(&model).unwrap();
assert!(markdown.contains("[requests](https://pypi.org/project/requests/)"));
assert!(markdown.contains("[urllib3](https://pypi.org/project/urllib3/)"));
}
#[test]
fn test_format_with_dependencies_contains_pypi_links() {
let mut model = create_test_read_model();
let mut transitive = HashMap::new();
transitive.insert(
"pkg:pypi/requests@2.31.0".to_string(),
vec!["pkg:pypi/urllib3@1.26.0".to_string()],
);
model.dependencies = Some(DependencyView {
direct: vec!["pkg:pypi/requests@2.31.0".to_string()],
transitive,
});
let formatter = MarkdownFormatter::new(Locale::En);
let markdown = formatter.format(&model).unwrap();
assert!(markdown.contains("[requests](https://pypi.org/project/requests/)"));
assert!(markdown.contains("[urllib3](https://pypi.org/project/urllib3/)"));
}
#[test]
fn test_vulnerability_table_contains_pypi_links() {
let mut model = create_test_read_model();
model.vulnerabilities = Some(VulnerabilityReportView {
actionable: vec![VulnerabilityView {
bom_ref: "vuln-001".to_string(),
id: "CVE-2024-1234".to_string(),
affected_component: "pkg:pypi/requests@2.31.0".to_string(),
affected_component_name: "requests".to_string(),
affected_version: "2.31.0".to_string(),
cvss_score: Some(9.8),
cvss_vector: None,
severity: SeverityView::Critical,
fixed_version: Some("2.32.0".to_string()),
description: None,
source_url: None,
}],
informational: vec![],
threshold_exceeded: true,
summary: VulnerabilitySummary {
total_count: 1,
actionable_count: 1,
informational_count: 0,
affected_package_count: 1,
},
});
let formatter = MarkdownFormatter::new(Locale::En);
let markdown = formatter.format(&model).unwrap();
assert!(markdown.contains("[requests](https://pypi.org/project/requests/)"));
}
#[test]
fn test_format_with_verified_packages_only_verified_get_links() {
let model = create_test_read_model();
let mut verified = HashSet::new();
verified.insert("requests".to_string());
let formatter = MarkdownFormatter::with_verified_packages(verified, Locale::En);
let markdown = formatter.format(&model).unwrap();
assert!(markdown.contains("[requests](https://pypi.org/project/requests/)"));
assert!(!markdown.contains("[urllib3](https://pypi.org/project/urllib3/)"));
assert!(markdown.contains("| urllib3 |"));
}
#[test]
fn test_format_without_verified_packages_all_get_links() {
let model = create_test_read_model();
let formatter = MarkdownFormatter::new(Locale::En);
let markdown = formatter.format(&model).unwrap();
assert!(markdown.contains("[requests](https://pypi.org/project/requests/)"));
assert!(markdown.contains("[urllib3](https://pypi.org/project/urllib3/)"));
}
#[test]
fn test_format_with_empty_verified_set_no_links() {
let model = create_test_read_model();
let verified = HashSet::new();
let formatter = MarkdownFormatter::with_verified_packages(verified, Locale::En);
let markdown = formatter.format(&model).unwrap();
assert!(!markdown.contains("[requests](https://pypi.org/project/requests/)"));
assert!(!markdown.contains("[urllib3](https://pypi.org/project/urllib3/)"));
assert!(markdown.contains("| requests |"));
assert!(markdown.contains("| urllib3 |"));
}
#[test]
fn test_format_vulnerability_with_verified_packages() {
let mut model = create_test_read_model();
model.vulnerabilities = Some(VulnerabilityReportView {
actionable: vec![VulnerabilityView {
bom_ref: "vuln-001".to_string(),
id: "CVE-2024-1234".to_string(),
affected_component: "pkg:pypi/requests@2.31.0".to_string(),
affected_component_name: "requests".to_string(),
affected_version: "2.31.0".to_string(),
cvss_score: Some(9.8),
cvss_vector: None,
severity: SeverityView::Critical,
fixed_version: Some("2.32.0".to_string()),
description: None,
source_url: None,
}],
informational: vec![],
threshold_exceeded: true,
summary: VulnerabilitySummary {
total_count: 1,
actionable_count: 1,
informational_count: 0,
affected_package_count: 1,
},
});
let verified = HashSet::new();
let formatter = MarkdownFormatter::with_verified_packages(verified, Locale::En);
let markdown = formatter.format(&model).unwrap();
assert!(!markdown.contains("[requests](https://pypi.org/project/requests/)"));
assert!(markdown.contains("| requests |"));
}
#[test]
fn test_format_package_name_with_none_verified() {
let result = format_package_name("requests", None);
assert_eq!(result, "[requests](https://pypi.org/project/requests/)");
}
#[test]
fn test_format_package_name_with_verified_present() {
let mut verified = HashSet::new();
verified.insert("requests".to_string());
let result = format_package_name("requests", Some(&verified));
assert_eq!(result, "[requests](https://pypi.org/project/requests/)");
}
#[test]
fn test_format_package_name_with_verified_absent() {
let verified = HashSet::new();
let result = format_package_name("nonexistent-pkg", Some(&verified));
assert_eq!(result, "nonexistent-pkg");
}
#[test]
fn test_vulnerability_id_to_link_cve() {
assert_eq!(
vulnerability_id_to_link("CVE-2024-1234"),
"[CVE-2024-1234](https://nvd.nist.gov/vuln/detail/CVE-2024-1234)"
);
}
#[test]
fn test_vulnerability_id_to_link_ghsa() {
assert_eq!(
vulnerability_id_to_link("GHSA-abcd-efgh-ijkl"),
"[GHSA-abcd-efgh-ijkl](https://github.com/advisories/GHSA-abcd-efgh-ijkl)"
);
}
#[test]
fn test_vulnerability_id_to_link_pysec() {
assert_eq!(
vulnerability_id_to_link("PYSEC-2021-108"),
"[PYSEC-2021-108](https://osv.dev/vulnerability/PYSEC-2021-108)"
);
}
#[test]
fn test_vulnerability_id_to_link_rustsec() {
assert_eq!(
vulnerability_id_to_link("RUSTSEC-2023-0001"),
"[RUSTSEC-2023-0001](https://osv.dev/vulnerability/RUSTSEC-2023-0001)"
);
}
}