aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/mozart-core/src/advisory.rs332
-rw-r--r--crates/mozart-core/src/advisory/audit_config.rs296
-rw-r--r--crates/mozart-core/src/advisory/auditor.rs77
-rw-r--r--crates/mozart-core/src/advisory/ignored_security_advisory.rs1
-rw-r--r--crates/mozart-core/src/advisory/partial_security_advisory.rs1
-rw-r--r--crates/mozart-core/src/advisory/security_advisory.rs1
-rw-r--r--crates/mozart/src/commands/audit.rs2
7 files changed, 387 insertions, 323 deletions
diff --git a/crates/mozart-core/src/advisory.rs b/crates/mozart-core/src/advisory.rs
index f20405e..bcfbe93 100644
--- a/crates/mozart-core/src/advisory.rs
+++ b/crates/mozart-core/src/advisory.rs
@@ -1,323 +1,11 @@
-use indexmap::IndexMap;
+pub mod audit_config;
+pub mod auditor;
+pub mod ignored_security_advisory;
+pub mod partial_security_advisory;
+pub mod security_advisory;
-use crate::config::Config;
-
-pub const FORMAT_TABLE: &str = "table";
-pub const FORMAT_PLAIN: &str = "plain";
-pub const FORMAT_JSON: &str = "json";
-pub const FORMAT_SUMMARY: &str = "summary";
-pub const FORMATS: [&str; 4] = [FORMAT_TABLE, FORMAT_PLAIN, FORMAT_JSON, FORMAT_SUMMARY];
-
-pub const ABANDONED_IGNORE: &str = "ignore";
-pub const ABANDONED_REPORT: &str = "report";
-pub const ABANDONED_FAIL: &str = "fail";
-pub const ABANDONEDS: [&str; 3] = [ABANDONED_IGNORE, ABANDONED_REPORT, ABANDONED_FAIL];
-
-/// Mirrors `Auditor::FORMAT_*` constants.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub enum AuditFormat {
- #[default]
- Table,
- Plain,
- Json,
- Summary,
-}
-
-impl AuditFormat {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::Table => FORMAT_TABLE,
- Self::Plain => FORMAT_PLAIN,
- Self::Json => FORMAT_JSON,
- Self::Summary => FORMAT_SUMMARY,
- }
- }
-}
-
-impl std::str::FromStr for AuditFormat {
- type Err = ();
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- match s {
- FORMAT_TABLE => Ok(Self::Table),
- FORMAT_PLAIN => Ok(Self::Plain),
- FORMAT_JSON => Ok(Self::Json),
- FORMAT_SUMMARY => Ok(Self::Summary),
- _ => Err(()),
- }
- }
-}
-
-/// Mirrors `Auditor::ABANDONED_*` constants.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub enum AbandonedHandling {
- Ignore,
- Report,
- #[default]
- Fail,
-}
-
-impl AbandonedHandling {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::Ignore => ABANDONED_IGNORE,
- Self::Report => ABANDONED_REPORT,
- Self::Fail => ABANDONED_FAIL,
- }
- }
-}
-
-impl std::str::FromStr for AbandonedHandling {
- type Err = ();
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- match s {
- ABANDONED_IGNORE => Ok(Self::Ignore),
- ABANDONED_REPORT => Ok(Self::Report),
- ABANDONED_FAIL => Ok(Self::Fail),
- _ => Err(()),
- }
- }
-}
-
-/// Mirrors `Composer\Advisory\AuditConfig`.
-#[derive(Debug, Clone)]
-pub struct AuditConfig {
- pub audit: bool,
- pub audit_format: AuditFormat,
- pub audit_abandoned: AbandonedHandling,
- pub block_insecure: bool,
- pub block_abandoned: bool,
- pub ignore_unreachable: bool,
- pub ignore_list_for_audit: IndexMap<String, Option<String>>,
- pub ignore_list_for_blocking: IndexMap<String, Option<String>>,
- pub ignore_severity_for_audit: IndexMap<String, Option<String>>,
- pub ignore_severity_for_blocking: IndexMap<String, Option<String>>,
- pub ignore_abandoned_for_audit: IndexMap<String, Option<String>>,
- pub ignore_abandoned_for_blocking: IndexMap<String, Option<String>>,
-}
-
-struct ParsedIgnore {
- audit: IndexMap<String, Option<String>>,
- block: IndexMap<String, Option<String>>,
-}
-
-/// Mirrors `AuditConfig::parseIgnoreWithApply()`.
-///
-/// Supports these JSON shapes:
-/// - `["CVE-1"]` — simple list, apply=all, reason=null
-/// - `{"CVE-1": "reason"}` — with reason, apply=all
-/// - `{"CVE-1": null}` — null reason, apply=all
-/// - `{"CVE-1": {"apply": "audit|block|all", "reason": "..."}}` — detailed
-fn parse_ignore_with_apply(config: &serde_json::Value) -> ParsedIgnore {
- let mut for_audit: IndexMap<String, Option<String>> = IndexMap::new();
- let mut for_block: IndexMap<String, Option<String>> = IndexMap::new();
-
- match config {
- serde_json::Value::Array(arr) => {
- for item in arr {
- if let Some(s) = item.as_str() {
- for_audit.insert(s.to_string(), None);
- for_block.insert(s.to_string(), None);
- }
- }
- }
- serde_json::Value::Object(obj) => {
- for (key, value) in obj {
- let (apply, reason) = match value {
- serde_json::Value::String(r) => ("all", Some(r.clone())),
- serde_json::Value::Null => ("all", None),
- serde_json::Value::Object(detail) => {
- let apply = detail
- .get("apply")
- .and_then(|v| v.as_str())
- .unwrap_or("all");
- if !matches!(apply, "audit" | "block" | "all") {
- continue;
- }
- let reason = detail
- .get("reason")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
- (apply, reason)
- }
- _ => continue,
- };
-
- if apply == "audit" || apply == "all" {
- for_audit.insert(key.clone(), reason.clone());
- }
- if apply == "block" || apply == "all" {
- for_block.insert(key.clone(), reason);
- }
- }
- }
- _ => {}
- }
-
- ParsedIgnore {
- audit: for_audit,
- block: for_block,
- }
-}
-
-impl AuditConfig {
- /// Mirrors `AuditConfig::fromConfig()`.
- pub fn from_config(config: &Config, audit: bool, audit_format: AuditFormat) -> Self {
- let empty_arr = serde_json::Value::Array(vec![]);
- let audit_val = config
- .get("audit")
- .unwrap_or_else(|| serde_json::Value::Object(Default::default()));
-
- let ignore_list_val = audit_val
- .get("ignore")
- .cloned()
- .unwrap_or_else(|| empty_arr.clone());
- let ignore_list_parsed = parse_ignore_with_apply(&ignore_list_val);
-
- let ignore_abandoned_val = audit_val
- .get("ignore-abandoned")
- .cloned()
- .unwrap_or_else(|| empty_arr.clone());
- let ignore_abandoned_parsed = parse_ignore_with_apply(&ignore_abandoned_val);
-
- let ignore_severity_val = audit_val
- .get("ignore-severity")
- .cloned()
- .unwrap_or_else(|| empty_arr.clone());
- let ignore_severity_parsed = parse_ignore_with_apply(&ignore_severity_val);
-
- let audit_abandoned = audit_val
- .get("abandoned")
- .and_then(|v| v.as_str())
- .and_then(|s| s.parse::<AbandonedHandling>().ok())
- .unwrap_or_default();
-
- let block_insecure = audit_val
- .get("block-insecure")
- .and_then(|v| v.as_bool())
- .unwrap_or(true);
-
- let block_abandoned = audit_val
- .get("block-abandoned")
- .and_then(|v| v.as_bool())
- .unwrap_or(false);
-
- let ignore_unreachable = audit_val
- .get("ignore-unreachable")
- .and_then(|v| v.as_bool())
- .unwrap_or(false);
-
- Self {
- audit,
- audit_format,
- audit_abandoned,
- block_insecure,
- block_abandoned,
- ignore_unreachable,
- ignore_list_for_audit: ignore_list_parsed.audit,
- ignore_list_for_blocking: ignore_list_parsed.block,
- ignore_severity_for_audit: ignore_severity_parsed.audit,
- ignore_severity_for_blocking: ignore_severity_parsed.block,
- ignore_abandoned_for_audit: ignore_abandoned_parsed.audit,
- ignore_abandoned_for_blocking: ignore_abandoned_parsed.block,
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_parse_ignore_simple_array() {
- let val = serde_json::json!(["CVE-2024-1234", "PKSA-0001"]);
- let parsed = parse_ignore_with_apply(&val);
- assert_eq!(parsed.audit.len(), 2);
- assert_eq!(parsed.block.len(), 2);
- assert_eq!(parsed.audit.get("CVE-2024-1234"), Some(&None));
- }
-
- #[test]
- fn test_parse_ignore_object_with_reasons() {
- let val = serde_json::json!({
- "CVE-2024-1234": "manually patched",
- "PKSA-0001": null
- });
- let parsed = parse_ignore_with_apply(&val);
- assert_eq!(
- parsed.audit.get("CVE-2024-1234"),
- Some(&Some("manually patched".to_string()))
- );
- assert_eq!(parsed.audit.get("PKSA-0001"), Some(&None));
- }
-
- #[test]
- fn test_parse_ignore_detailed_apply_audit_only() {
- let val = serde_json::json!({
- "CVE-2024-1234": { "apply": "audit", "reason": "test" }
- });
- let parsed = parse_ignore_with_apply(&val);
- assert_eq!(parsed.audit.len(), 1);
- assert_eq!(parsed.block.len(), 0);
- }
-
- #[test]
- fn test_parse_ignore_detailed_apply_block_only() {
- let val = serde_json::json!({
- "CVE-2024-1234": { "apply": "block", "reason": "test" }
- });
- let parsed = parse_ignore_with_apply(&val);
- assert_eq!(parsed.audit.len(), 0);
- assert_eq!(parsed.block.len(), 1);
- }
-
- #[test]
- fn test_parse_ignore_detailed_apply_all() {
- let val = serde_json::json!({
- "CVE-2024-1234": { "apply": "all", "reason": "test" }
- });
- let parsed = parse_ignore_with_apply(&val);
- assert_eq!(parsed.audit.len(), 1);
- assert_eq!(parsed.block.len(), 1);
- }
-
- #[test]
- fn test_audit_config_defaults() {
- let config = Config::default();
- let audit_config = AuditConfig::from_config(&config, true, AuditFormat::Table);
- assert!(audit_config.audit);
- assert_eq!(audit_config.audit_format, AuditFormat::Table);
- assert_eq!(audit_config.audit_abandoned, AbandonedHandling::Fail);
- assert!(audit_config.block_insecure);
- assert!(!audit_config.block_abandoned);
- assert!(!audit_config.ignore_unreachable);
- assert!(audit_config.ignore_list_for_audit.is_empty());
- assert!(audit_config.ignore_severity_for_audit.is_empty());
- }
-
- #[test]
- fn test_audit_config_from_config_with_audit_section() {
- use std::collections::BTreeMap;
- let mut config = Config::default();
- config
- .merge(&BTreeMap::from([(
- "audit".to_string(),
- serde_json::json!({
- "ignore": ["CVE-2024-1234"],
- "ignore-severity": {"low": "low severity not critical"},
- "abandoned": "report",
- "block-insecure": false,
- "ignore-unreachable": true
- }),
- )]))
- .unwrap();
-
- let audit_config = AuditConfig::from_config(&config, true, AuditFormat::Summary);
- assert_eq!(audit_config.audit_abandoned, AbandonedHandling::Report);
- assert!(!audit_config.block_insecure);
- assert!(audit_config.ignore_unreachable);
- assert_eq!(audit_config.ignore_list_for_audit.len(), 1);
- assert_eq!(audit_config.ignore_severity_for_audit.len(), 1);
- }
-}
+pub use audit_config::*;
+pub use auditor::*;
+pub use ignored_security_advisory::*;
+pub use partial_security_advisory::*;
+pub use security_advisory::*;
diff --git a/crates/mozart-core/src/advisory/audit_config.rs b/crates/mozart-core/src/advisory/audit_config.rs
new file mode 100644
index 0000000..00f800f
--- /dev/null
+++ b/crates/mozart-core/src/advisory/audit_config.rs
@@ -0,0 +1,296 @@
+use crate::advisory::{AbandonedHandling, AuditFormat};
+use crate::config::Config;
+
+/// ref: \Composer\Advisory\AuditConfig
+#[derive(Debug, Clone)]
+pub struct AuditConfig {
+ /// Whether to run audit
+ pub audit: bool,
+ pub audit_format: AuditFormat,
+ pub audit_abandoned: AbandonedHandling,
+ /// Should insecure versions be blocked during a composer update/required command
+ pub block_insecure: bool,
+ /// Should abandoned packages be blocked during a composer update/required command
+ pub block_abandoned: bool,
+ /// Should repositories that are unreachable or return a non-200 status code be ignored.
+ pub ignore_unreachable: bool,
+ /// List of advisory IDs to ignore during auditing => reason for ignoring
+ pub ignore_list_for_audit: indexmap::IndexMap<String, Option<String>>,
+ /// List of advisory IDs to ignore during blocking
+ pub ignore_list_for_blocking: indexmap::IndexMap<String, Option<String>>,
+ /// List of severities to ignore during auditing
+ pub ignore_severity_for_audit: indexmap::IndexMap<String, Option<String>>,
+ /// List of severities to ignore during blocking
+ pub ignore_severity_for_blocking: indexmap::IndexMap<String, Option<String>>,
+ /// List of abandoned packages to ignore during auditing
+ pub ignore_abandoned_for_audit: indexmap::IndexMap<String, Option<String>>,
+ /// List of abandoned packages to ignore during blocking
+ pub ignore_abandoned_for_blocking: indexmap::IndexMap<String, Option<String>>,
+}
+
+impl AuditConfig {
+ #[allow(clippy::too_many_arguments)]
+ pub fn new(
+ audit: bool,
+ audit_format: AuditFormat,
+ audit_abandoned: AbandonedHandling,
+ block_insecure: bool,
+ block_abandoned: bool,
+ ignore_unreachable: bool,
+ ignore_list_for_audit: indexmap::IndexMap<String, Option<String>>,
+ ignore_list_for_blocking: indexmap::IndexMap<String, Option<String>>,
+ ignore_severity_for_audit: indexmap::IndexMap<String, Option<String>>,
+ ignore_severity_for_blocking: indexmap::IndexMap<String, Option<String>>,
+ ignore_abandoned_for_audit: indexmap::IndexMap<String, Option<String>>,
+ ignore_abandoned_for_blocking: indexmap::IndexMap<String, Option<String>>,
+ ) -> Self {
+ Self {
+ audit,
+ audit_format,
+ audit_abandoned,
+ block_insecure,
+ block_abandoned,
+ ignore_unreachable,
+ ignore_list_for_audit,
+ ignore_list_for_blocking,
+ ignore_severity_for_audit,
+ ignore_severity_for_blocking,
+ ignore_abandoned_for_audit,
+ ignore_abandoned_for_blocking,
+ }
+ }
+
+ pub fn from_config(
+ config: &Config,
+ audit: bool,
+ audit_format: AuditFormat,
+ ) -> anyhow::Result<Self> {
+ let empty_arr = serde_json::Value::Array(vec![]);
+ let audit_val = config
+ .get("audit")
+ .unwrap_or_else(|| serde_json::Value::Object(Default::default()));
+
+ let ignore_list_val = audit_val
+ .get("ignore")
+ .cloned()
+ .unwrap_or_else(|| empty_arr.clone());
+ let ignore_list_parsed = Self::parse_ignore_with_apply(&ignore_list_val)?;
+
+ let ignore_abandoned_val = audit_val
+ .get("ignore-abandoned")
+ .cloned()
+ .unwrap_or_else(|| empty_arr.clone());
+ let ignore_abandoned_parsed = Self::parse_ignore_with_apply(&ignore_abandoned_val)?;
+
+ let ignore_severity_val = audit_val
+ .get("ignore-severity")
+ .cloned()
+ .unwrap_or_else(|| empty_arr.clone());
+ let ignore_severity_parsed = Self::parse_ignore_with_apply(&ignore_severity_val)?;
+
+ let audit_abandoned = audit_val
+ .get("abandoned")
+ .and_then(|v| v.as_str())
+ .and_then(|s| s.parse::<AbandonedHandling>().ok())
+ .unwrap_or_default();
+
+ let block_insecure = audit_val
+ .get("block-insecure")
+ .and_then(|v| v.as_bool())
+ .unwrap_or(true);
+
+ let block_abandoned = audit_val
+ .get("block-abandoned")
+ .and_then(|v| v.as_bool())
+ .unwrap_or(false);
+
+ let ignore_unreachable = audit_val
+ .get("ignore-unreachable")
+ .and_then(|v| v.as_bool())
+ .unwrap_or(false);
+
+ Ok(Self {
+ audit,
+ audit_format,
+ audit_abandoned,
+ block_insecure,
+ block_abandoned,
+ ignore_unreachable,
+ ignore_list_for_audit: ignore_list_parsed.audit,
+ ignore_list_for_blocking: ignore_list_parsed.block,
+ ignore_severity_for_audit: ignore_severity_parsed.audit,
+ ignore_severity_for_blocking: ignore_severity_parsed.block,
+ ignore_abandoned_for_audit: ignore_abandoned_parsed.audit,
+ ignore_abandoned_for_blocking: ignore_abandoned_parsed.block,
+ })
+ }
+
+ /// Parse ignore configuration supporting both simple and detailed formats with apply scopes
+ ///
+ /// Simple format: ['CVE-123', 'CVE-456'] or ['CVE-123' => 'reason']
+ /// Detailed format: ['CVE-123' => ['apply' => 'audit|block|all', 'reason' => '...']]
+ fn parse_ignore_with_apply(config: &serde_json::Value) -> anyhow::Result<ParsedIgnore> {
+ let mut for_audit = indexmap::IndexMap::new();
+ let mut for_block = indexmap::IndexMap::new();
+
+ match config {
+ serde_json::Value::Array(arr) => {
+ // Simple format: ['CVE-123']
+ for item in arr {
+ if let Some(s) = item.as_str() {
+ for_audit.insert(s.to_string(), None);
+ for_block.insert(s.to_string(), None);
+ }
+ }
+ }
+ serde_json::Value::Object(obj) => {
+ for (key, value) in obj {
+ let (apply, reason) = match value {
+ serde_json::Value::String(r) => {
+ // Simple format with reason: ['CVE-123' => 'reason']
+ ("all", Some(r.clone()))
+ }
+ serde_json::Value::Null => {
+ // Simple format with null: ['CVE-123' => null]
+ ("all", None)
+ }
+ serde_json::Value::Object(detail) => {
+ // Detailed format: ['CVE-123' => ['apply' => '...', 'reason' => '...']]
+ let apply = detail
+ .get("apply")
+ .and_then(|v| v.as_str())
+ .unwrap_or("all");
+ let reason = detail
+ .get("reason")
+ .and_then(|v| v.as_str())
+ .map(|s| s.to_string());
+
+ if !matches!(apply, "audit" | "block" | "all") {
+ anyhow::bail!(
+ "Invalid 'apply' value for '{key}': {apply}. Expected 'audit', 'block', or 'all'."
+ );
+ }
+ (apply, reason)
+ }
+ _ => continue,
+ };
+
+ if apply == "audit" || apply == "all" {
+ for_audit.insert(key.clone(), reason.clone());
+ }
+ if apply == "block" || apply == "all" {
+ for_block.insert(key.clone(), reason);
+ }
+ }
+ }
+ _ => {}
+ }
+
+ Ok(ParsedIgnore {
+ audit: for_audit,
+ block: for_block,
+ })
+ }
+}
+
+struct ParsedIgnore {
+ audit: indexmap::IndexMap<String, Option<String>>,
+ block: indexmap::IndexMap<String, Option<String>>,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_ignore_simple_array() {
+ let val = serde_json::json!(["CVE-2024-1234", "PKSA-0001"]);
+ let parsed = AuditConfig::parse_ignore_with_apply(&val).unwrap();
+ assert_eq!(parsed.audit.len(), 2);
+ assert_eq!(parsed.block.len(), 2);
+ assert_eq!(parsed.audit.get("CVE-2024-1234"), Some(&None));
+ }
+
+ #[test]
+ fn test_parse_ignore_object_with_reasons() {
+ let val = serde_json::json!({
+ "CVE-2024-1234": "manually patched",
+ "PKSA-0001": null
+ });
+ let parsed = AuditConfig::parse_ignore_with_apply(&val).unwrap();
+ assert_eq!(
+ parsed.audit.get("CVE-2024-1234"),
+ Some(&Some("manually patched".to_string()))
+ );
+ assert_eq!(parsed.audit.get("PKSA-0001"), Some(&None));
+ }
+
+ #[test]
+ fn test_parse_ignore_detailed_apply_audit_only() {
+ let val = serde_json::json!({
+ "CVE-2024-1234": { "apply": "audit", "reason": "test" }
+ });
+ let parsed = AuditConfig::parse_ignore_with_apply(&val).unwrap();
+ assert_eq!(parsed.audit.len(), 1);
+ assert_eq!(parsed.block.len(), 0);
+ }
+
+ #[test]
+ fn test_parse_ignore_detailed_apply_block_only() {
+ let val = serde_json::json!({
+ "CVE-2024-1234": { "apply": "block", "reason": "test" }
+ });
+ let parsed = AuditConfig::parse_ignore_with_apply(&val).unwrap();
+ assert_eq!(parsed.audit.len(), 0);
+ assert_eq!(parsed.block.len(), 1);
+ }
+
+ #[test]
+ fn test_parse_ignore_detailed_apply_all() {
+ let val = serde_json::json!({
+ "CVE-2024-1234": { "apply": "all", "reason": "test" }
+ });
+ let parsed = AuditConfig::parse_ignore_with_apply(&val).unwrap();
+ assert_eq!(parsed.audit.len(), 1);
+ assert_eq!(parsed.block.len(), 1);
+ }
+
+ #[test]
+ fn test_audit_config_defaults() {
+ let config = Config::default();
+ let audit_config = AuditConfig::from_config(&config, true, AuditFormat::Table).unwrap();
+ assert!(audit_config.audit);
+ assert_eq!(audit_config.audit_format, AuditFormat::Table);
+ assert_eq!(audit_config.audit_abandoned, AbandonedHandling::Fail);
+ assert!(audit_config.block_insecure);
+ assert!(!audit_config.block_abandoned);
+ assert!(!audit_config.ignore_unreachable);
+ assert!(audit_config.ignore_list_for_audit.is_empty());
+ assert!(audit_config.ignore_severity_for_audit.is_empty());
+ }
+
+ #[test]
+ fn test_audit_config_from_config_with_audit_section() {
+ use std::collections::BTreeMap;
+ let mut config = Config::default();
+ config
+ .merge(&BTreeMap::from([(
+ "audit".to_string(),
+ serde_json::json!({
+ "ignore": ["CVE-2024-1234"],
+ "ignore-severity": {"low": "low severity not critical"},
+ "abandoned": "report",
+ "block-insecure": false,
+ "ignore-unreachable": true
+ }),
+ )]))
+ .unwrap();
+
+ let audit_config = AuditConfig::from_config(&config, true, AuditFormat::Summary).unwrap();
+ assert_eq!(audit_config.audit_abandoned, AbandonedHandling::Report);
+ assert!(!audit_config.block_insecure);
+ assert!(audit_config.ignore_unreachable);
+ assert_eq!(audit_config.ignore_list_for_audit.len(), 1);
+ assert_eq!(audit_config.ignore_severity_for_audit.len(), 1);
+ }
+}
diff --git a/crates/mozart-core/src/advisory/auditor.rs b/crates/mozart-core/src/advisory/auditor.rs
new file mode 100644
index 0000000..14c1693
--- /dev/null
+++ b/crates/mozart-core/src/advisory/auditor.rs
@@ -0,0 +1,77 @@
+pub const FORMAT_TABLE: &str = "table";
+pub const FORMAT_PLAIN: &str = "plain";
+pub const FORMAT_JSON: &str = "json";
+pub const FORMAT_SUMMARY: &str = "summary";
+pub const FORMATS: [&str; 4] = [FORMAT_TABLE, FORMAT_PLAIN, FORMAT_JSON, FORMAT_SUMMARY];
+
+pub const ABANDONED_IGNORE: &str = "ignore";
+pub const ABANDONED_REPORT: &str = "report";
+pub const ABANDONED_FAIL: &str = "fail";
+pub const ABANDONEDS: [&str; 3] = [ABANDONED_IGNORE, ABANDONED_REPORT, ABANDONED_FAIL];
+
+/// Mirrors `Auditor::FORMAT_*` constants.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub enum AuditFormat {
+ #[default]
+ Table,
+ Plain,
+ Json,
+ Summary,
+}
+
+impl AuditFormat {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Table => FORMAT_TABLE,
+ Self::Plain => FORMAT_PLAIN,
+ Self::Json => FORMAT_JSON,
+ Self::Summary => FORMAT_SUMMARY,
+ }
+ }
+}
+
+impl std::str::FromStr for AuditFormat {
+ type Err = ();
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ FORMAT_TABLE => Ok(Self::Table),
+ FORMAT_PLAIN => Ok(Self::Plain),
+ FORMAT_JSON => Ok(Self::Json),
+ FORMAT_SUMMARY => Ok(Self::Summary),
+ _ => Err(()),
+ }
+ }
+}
+
+/// Mirrors `Auditor::ABANDONED_*` constants.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub enum AbandonedHandling {
+ Ignore,
+ Report,
+ #[default]
+ Fail,
+}
+
+impl AbandonedHandling {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Ignore => ABANDONED_IGNORE,
+ Self::Report => ABANDONED_REPORT,
+ Self::Fail => ABANDONED_FAIL,
+ }
+ }
+}
+
+impl std::str::FromStr for AbandonedHandling {
+ type Err = ();
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ ABANDONED_IGNORE => Ok(Self::Ignore),
+ ABANDONED_REPORT => Ok(Self::Report),
+ ABANDONED_FAIL => Ok(Self::Fail),
+ _ => Err(()),
+ }
+ }
+}
diff --git a/crates/mozart-core/src/advisory/ignored_security_advisory.rs b/crates/mozart-core/src/advisory/ignored_security_advisory.rs
new file mode 100644
index 0000000..e3e81f7
--- /dev/null
+++ b/crates/mozart-core/src/advisory/ignored_security_advisory.rs
@@ -0,0 +1 @@
+pub struct IgnoredSecurityAdvisory;
diff --git a/crates/mozart-core/src/advisory/partial_security_advisory.rs b/crates/mozart-core/src/advisory/partial_security_advisory.rs
new file mode 100644
index 0000000..56dfc25
--- /dev/null
+++ b/crates/mozart-core/src/advisory/partial_security_advisory.rs
@@ -0,0 +1 @@
+pub struct PartialSecurityAdvisory;
diff --git a/crates/mozart-core/src/advisory/security_advisory.rs b/crates/mozart-core/src/advisory/security_advisory.rs
new file mode 100644
index 0000000..6780cdf
--- /dev/null
+++ b/crates/mozart-core/src/advisory/security_advisory.rs
@@ -0,0 +1 @@
+pub struct SecurityAdvisory;
diff --git a/crates/mozart/src/commands/audit.rs b/crates/mozart/src/commands/audit.rs
index 72d4408..eafcba4 100644
--- a/crates/mozart/src/commands/audit.rs
+++ b/crates/mozart/src/commands/audit.rs
@@ -46,7 +46,7 @@ pub async fn execute(
let composer = Composer::require(&working_dir)?;
// Parse audit config from composer.json's config.audit section
- let audit_config = AuditConfig::from_config(composer.config(), true, AuditFormat::Table);
+ let audit_config = AuditConfig::from_config(composer.config(), true, AuditFormat::Table)?;
// Resolve format: CLI arg > config default (table)
let format = match args.format.as_deref() {