csaf-models 0.3.2

CSAF 2.0/2.1 data models, SQLite management, and user models
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Pierre Gronau, ndaal in Cologne

//! CSAF provider metadata model.

use serde::{Deserialize, Serialize};

/// CSAF provider metadata document.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProviderMetadata {
    /// Canonical URL for this metadata file.
    pub canonical_url: String,

    /// Distribution endpoints.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub distributions: Vec<ProviderDistribution>,

    /// Last updated timestamp (ISO 8601).
    pub last_updated: String,

    /// Whether to list on CSAF aggregators.
    #[serde(default, rename = "list_on_CSAF_aggregators")]
    pub list_on_csaf_aggregators: bool,

    /// Metadata schema version.
    pub metadata_version: String,

    /// Whether to allow mirroring on CSAF aggregators.
    #[serde(default, rename = "mirror_on_CSAF_aggregators")]
    pub mirror_on_csaf_aggregators: bool,

    /// Public OpenPGP keys for signature verification.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub public_openpgp_keys: Vec<OpenPgpKey>,

    /// Publisher identity.
    pub publisher: ProviderPublisher,

    /// Publisher role (e.g. `csaf_publisher`, `csaf_provider`, `csaf_trusted_provider`).
    pub role: String,
}

/// A distribution endpoint for CSAF documents.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProviderDistribution {
    /// Directory URL where CSAF documents are published.
    pub directory_url: String,
}

/// An OpenPGP public key reference.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OpenPgpKey {
    /// Key fingerprint (40 hex characters).
    pub fingerprint: String,

    /// URL to download the public key.
    pub url: String,
}

/// Publisher identity within provider metadata.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProviderPublisher {
    /// Publisher category.
    pub category: String,

    /// Contact email or details.
    pub contact_details: String,

    /// Publisher name.
    pub name: String,

    /// Publisher namespace URI.
    pub namespace: String,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_deserialize_provider_metadata() {
        let json = include_str!("../../../test/csaf/provider-metadata.json");
        let meta: ProviderMetadata =
            serde_json::from_str(json).expect("Failed to deserialize provider metadata");

        // The in-tree provider-metadata.json fixture advertises CSAF 2.1;
        // keep this assertion aligned with the fixture to avoid silent drift.
        assert_eq!(meta.metadata_version, "2.1");
        assert_eq!(meta.role, "csaf_publisher");
        assert_eq!(meta.publisher.category, "vendor");
        assert!(meta.list_on_csaf_aggregators);
        assert!(meta.mirror_on_csaf_aggregators);
        assert_eq!(meta.public_openpgp_keys.len(), 1);
        assert_eq!(
            meta.public_openpgp_keys[0].fingerprint,
            "D1DE14AA6D1980BD3FB67BF5C706DFBCAC5EFA64"
        );
    }

    #[test]
    fn test_roundtrip_provider_metadata() {
        let json = include_str!("../../../test/csaf/provider-metadata.json");
        let meta: ProviderMetadata = serde_json::from_str(json).expect("parse error");
        let serialized = serde_json::to_string_pretty(&meta).expect("serialize error");
        let meta2: ProviderMetadata = serde_json::from_str(&serialized).expect("re-parse error");
        assert_eq!(meta, meta2);
    }
}