Skip to main content

anodizer_core/config/
sbom.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Deserializer, Serialize};
3
4use super::{StringOrBool, deserialize_string_or_bool_opt};
5
6// ---------------------------------------------------------------------------
7// SbomConfig
8// ---------------------------------------------------------------------------
9
10#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
11#[serde(default, deny_unknown_fields)]
12pub struct SbomConfig {
13    /// Unique identifier for this SBOM config (default: "default").
14    pub id: Option<String>,
15    /// Command to run for SBOM generation (default: "syft").
16    pub cmd: Option<String>,
17    /// Environment variables to pass to the command, as `KEY=VALUE` strings.
18    /// Order is preserved. Values are template-rendered before being set.
19    #[serde(default)]
20    pub env: Option<Vec<String>>,
21    /// Command-line arguments (supports templates and $artifact, $document vars).
22    pub args: Option<Vec<String>>,
23    /// Output document path templates (supports templates).
24    pub documents: Option<Vec<String>>,
25    /// Which artifacts to catalog: "source", "archive", "binary", "package", "diskimage", "installer", "any" (default: "archive").
26    pub artifacts: Option<String>,
27    /// Filter by artifact IDs (ignored if artifacts="source").
28    pub ids: Option<Vec<String>>,
29    /// Skip this SBOM config. Accepts bool or template string.
30    /// Accepts the legacy `disable:` spelling via serde alias for back-compat.
31    #[serde(
32        alias = "disable",
33        deserialize_with = "deserialize_string_or_bool_opt",
34        default
35    )]
36    pub skip: Option<StringOrBool>,
37}
38
39impl SbomConfig {
40    /// Default `id` when an SBOM config has none (`"default"`).
41    pub const DEFAULT_ID: &'static str = "default";
42
43    /// Default SBOM-generation command
44    /// (`cfg.Cmd = "syft"`).
45    pub const DEFAULT_CMD: &'static str = "syft";
46
47    /// Default `artifacts` filter
48    /// (`cfg.Artifacts = "archive"`).
49    pub const DEFAULT_ARTIFACTS: &'static str = "archive";
50
51    /// Default document-path template when `artifacts: binary`. Includes
52    /// per-target Os/Arch suffix so per-arch SBOMs don't collide.
53    /// Default value.
54    pub const DEFAULT_DOCUMENT_BINARY: &'static str =
55        "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.sbom.json";
56
57    /// Default document-path template for any non-binary, non-any
58    /// `artifacts:` filter.
59    pub const DEFAULT_DOCUMENT_OTHER: &'static str = "{{ .ArtifactName }}.sbom.json";
60
61    /// Default `args` for the syft command, using shell-style `$artifact` /
62    /// `$document` placeholders verbatim — the arg-renderer rewrites
63    /// these to per-artifact values at execution time.
64    pub const DEFAULT_SYFT_ARGS: &[&'static str] = &[
65        "$artifact",
66        "--output",
67        "spdx-json=$document",
68        "--enrich",
69        "all",
70    ];
71
72    /// Env entry that syft requires to emit file paths in the SBOM
73    /// when cataloging archives or source.
74    pub const DEFAULT_SYFT_ENV_KEY: &'static str = "SYFT_FILE_METADATA_CATALOGER_ENABLED";
75    pub const DEFAULT_SYFT_ENV_VAL: &'static str = "true";
76
77    /// Resolve the SBOM-config id, falling back to `"default"`.
78    pub fn resolved_id(&self) -> &str {
79        self.id.as_deref().unwrap_or(Self::DEFAULT_ID)
80    }
81
82    /// Resolve the SBOM command, falling back to `"syft"`.
83    pub fn resolved_cmd(&self) -> &str {
84        self.cmd.as_deref().unwrap_or(Self::DEFAULT_CMD)
85    }
86
87    /// Resolve the `artifacts:` filter, falling back to `"archive"`.
88    pub fn resolved_artifacts(&self) -> &str {
89        self.artifacts.as_deref().unwrap_or(Self::DEFAULT_ARTIFACTS)
90    }
91
92    /// Resolve `documents`, falling back to the artifact-type-specific
93    /// default when unset. Caller should pass the result of
94    /// [`Self::resolved_artifacts`] for `artifacts`.
95    pub fn resolved_documents(&self, artifacts: &str) -> Vec<String> {
96        self.documents.clone().unwrap_or_else(|| match artifacts {
97            "binary" => vec![Self::DEFAULT_DOCUMENT_BINARY.to_string()],
98            "any" => vec![],
99            _ => vec![Self::DEFAULT_DOCUMENT_OTHER.to_string()],
100        })
101    }
102
103    /// Resolve `args`, falling back to [`Self::DEFAULT_SYFT_ARGS`] when
104    /// `cmd` is `"syft"`; empty vec otherwise (args are only initialized
105    /// when cmd is syft, and left
106    /// args empty for other cmds).
107    pub fn resolved_args(&self, cmd: &str) -> Vec<String> {
108        self.args.clone().unwrap_or_else(|| {
109            if cmd == Self::DEFAULT_CMD {
110                Self::DEFAULT_SYFT_ARGS
111                    .iter()
112                    .map(|s| (*s).to_string())
113                    .collect()
114            } else {
115                Vec::new()
116            }
117        })
118    }
119
120    /// Default env additions for the syft sub-process. Empty unless cmd
121    /// is syft AND artifacts is source/archive — in which case syft
122    /// needs the file-metadata cataloger enabled to produce file paths
123    /// in the SBOM.
124    pub fn default_syft_env_for(cmd: &str, artifacts: &str) -> Vec<(String, String)> {
125        if cmd == Self::DEFAULT_CMD && matches!(artifacts, "source" | "archive") {
126            vec![(
127                Self::DEFAULT_SYFT_ENV_KEY.to_string(),
128                Self::DEFAULT_SYFT_ENV_VAL.to_string(),
129            )]
130        } else {
131            Vec::new()
132        }
133    }
134}
135
136/// Custom deserializer for the `sboms` / `sbom` field.
137/// Accepts:
138///   - null/missing → empty vec (via serde default)
139///   - a single object → vec of one SbomConfig
140///   - an array → vec of SbomConfig
141pub(super) fn deserialize_sboms<'de, D>(deserializer: D) -> Result<Vec<SbomConfig>, D::Error>
142where
143    D: Deserializer<'de>,
144{
145    use serde::de::{self, Visitor};
146
147    struct SbomsVisitor;
148
149    impl<'de> Visitor<'de> for SbomsVisitor {
150        type Value = Vec<SbomConfig>;
151
152        fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153            f.write_str("an SBOM config object or an array of SBOM config objects")
154        }
155
156        fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
157            let mut configs = Vec::new();
158            while let Some(item) = seq.next_element::<SbomConfig>()? {
159                configs.push(item);
160            }
161            Ok(configs)
162        }
163
164        fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
165            let config = SbomConfig::deserialize(de::value::MapAccessDeserializer::new(map))?;
166            Ok(vec![config])
167        }
168
169        fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
170            Ok(Vec::new())
171        }
172
173        fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
174            Ok(Vec::new())
175        }
176    }
177
178    deserializer.deserialize_any(SbomsVisitor)
179}