use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
#[non_exhaustive]
pub enum PolicyCategory {
SecuritySqli,
SecurityAdmin,
PiiGlobal,
PiiUs,
PiiEu,
PiiIndia,
PiiSingapore,
PiiIndonesia,
CodeSecrets,
CodeUnsafe,
CodeCompliance,
SensitiveData,
MediaSafety,
MediaBiometric,
MediaDocument,
MediaPii,
DynamicRisk,
DynamicCompliance,
DynamicSecurity,
DynamicCost,
DynamicAccess,
SecurityDangerous,
ComplianceEuaiact,
DangerousQueries,
PiiDetection,
SqlInjection,
Unknown(String),
}
impl PolicyCategory {
pub const KNOWN_WIRE_VALUES: &'static [&'static str] = &[
"security-sqli",
"security-admin",
"pii-global",
"pii-us",
"pii-eu",
"pii-india",
"pii-singapore",
"pii-indonesia",
"code-secrets",
"code-unsafe",
"code-compliance",
"sensitive-data",
"media-safety",
"media-biometric",
"media-document",
"media-pii",
"dynamic-risk",
"dynamic-compliance",
"dynamic-security",
"dynamic-cost",
"dynamic-access",
"security-dangerous",
"compliance-euaiact",
"dangerous_queries",
"pii_detection",
"sql_injection",
];
pub fn as_str(&self) -> &str {
match self {
Self::SecuritySqli => "security-sqli",
Self::SecurityAdmin => "security-admin",
Self::PiiGlobal => "pii-global",
Self::PiiUs => "pii-us",
Self::PiiEu => "pii-eu",
Self::PiiIndia => "pii-india",
Self::PiiSingapore => "pii-singapore",
Self::PiiIndonesia => "pii-indonesia",
Self::CodeSecrets => "code-secrets",
Self::CodeUnsafe => "code-unsafe",
Self::CodeCompliance => "code-compliance",
Self::SensitiveData => "sensitive-data",
Self::MediaSafety => "media-safety",
Self::MediaBiometric => "media-biometric",
Self::MediaDocument => "media-document",
Self::MediaPii => "media-pii",
Self::DynamicRisk => "dynamic-risk",
Self::DynamicCompliance => "dynamic-compliance",
Self::DynamicSecurity => "dynamic-security",
Self::DynamicCost => "dynamic-cost",
Self::DynamicAccess => "dynamic-access",
Self::SecurityDangerous => "security-dangerous",
Self::ComplianceEuaiact => "compliance-euaiact",
Self::DangerousQueries => "dangerous_queries",
Self::PiiDetection => "pii_detection",
Self::SqlInjection => "sql_injection",
Self::Unknown(v) => v.as_str(),
}
}
pub fn is_known(&self) -> bool {
!matches!(self, Self::Unknown(_))
}
}
impl From<String> for PolicyCategory {
fn from(v: String) -> Self {
match v.as_str() {
"security-sqli" => Self::SecuritySqli,
"security-admin" => Self::SecurityAdmin,
"pii-global" => Self::PiiGlobal,
"pii-us" => Self::PiiUs,
"pii-eu" => Self::PiiEu,
"pii-india" => Self::PiiIndia,
"pii-singapore" => Self::PiiSingapore,
"pii-indonesia" => Self::PiiIndonesia,
"code-secrets" => Self::CodeSecrets,
"code-unsafe" => Self::CodeUnsafe,
"code-compliance" => Self::CodeCompliance,
"sensitive-data" => Self::SensitiveData,
"media-safety" => Self::MediaSafety,
"media-biometric" => Self::MediaBiometric,
"media-document" => Self::MediaDocument,
"media-pii" => Self::MediaPii,
"dynamic-risk" => Self::DynamicRisk,
"dynamic-compliance" => Self::DynamicCompliance,
"dynamic-security" => Self::DynamicSecurity,
"dynamic-cost" => Self::DynamicCost,
"dynamic-access" => Self::DynamicAccess,
"security-dangerous" => Self::SecurityDangerous,
"compliance-euaiact" => Self::ComplianceEuaiact,
"dangerous_queries" => Self::DangerousQueries,
"pii_detection" => Self::PiiDetection,
"sql_injection" => Self::SqlInjection,
_ => Self::Unknown(v),
}
}
}
impl From<PolicyCategory> for String {
fn from(v: PolicyCategory) -> Self {
match v {
PolicyCategory::Unknown(s) => s,
known => known.as_str().to_string(),
}
}
}
impl fmt::Display for PolicyCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
fn posture() -> Value {
serde_json::from_str(include_str!(
"../../testdata/shipped_posture_categories.json"
))
.expect("the vendored posture fixture")
}
#[test]
fn pii_indonesia_serializes_to_wire_name() {
let cat = PolicyCategory::PiiIndonesia;
let json = serde_json::to_string(&cat).unwrap();
assert_eq!(json, r#""pii-indonesia""#);
}
#[test]
fn pii_indonesia_round_trips() {
let cat = PolicyCategory::PiiIndonesia;
let json = serde_json::to_string(&cat).unwrap();
let back: PolicyCategory = serde_json::from_str(&json).unwrap();
assert_eq!(back, PolicyCategory::PiiIndonesia);
}
#[test]
fn all_categories_serialize_to_expected_wire_names() {
let cases = vec![
(PolicyCategory::SecuritySqli, "security-sqli"),
(PolicyCategory::SecurityAdmin, "security-admin"),
(PolicyCategory::PiiGlobal, "pii-global"),
(PolicyCategory::PiiUs, "pii-us"),
(PolicyCategory::PiiEu, "pii-eu"),
(PolicyCategory::PiiIndia, "pii-india"),
(PolicyCategory::PiiSingapore, "pii-singapore"),
(PolicyCategory::PiiIndonesia, "pii-indonesia"),
(PolicyCategory::CodeSecrets, "code-secrets"),
(PolicyCategory::CodeUnsafe, "code-unsafe"),
(PolicyCategory::CodeCompliance, "code-compliance"),
(PolicyCategory::SensitiveData, "sensitive-data"),
(PolicyCategory::MediaSafety, "media-safety"),
(PolicyCategory::MediaBiometric, "media-biometric"),
(PolicyCategory::MediaDocument, "media-document"),
(PolicyCategory::MediaPii, "media-pii"),
(PolicyCategory::DynamicRisk, "dynamic-risk"),
(PolicyCategory::DynamicCompliance, "dynamic-compliance"),
(PolicyCategory::DynamicSecurity, "dynamic-security"),
(PolicyCategory::DynamicCost, "dynamic-cost"),
(PolicyCategory::DynamicAccess, "dynamic-access"),
(PolicyCategory::SecurityDangerous, "security-dangerous"),
(PolicyCategory::ComplianceEuaiact, "compliance-euaiact"),
(PolicyCategory::DangerousQueries, "dangerous_queries"),
(PolicyCategory::PiiDetection, "pii_detection"),
(PolicyCategory::SqlInjection, "sql_injection"),
];
assert_eq!(cases.len(), PolicyCategory::KNOWN_WIRE_VALUES.len());
for (variant, expected) in cases {
let json = serde_json::to_string(&variant).unwrap();
assert_eq!(json, format!(r#""{}""#, expected), "variant {:?}", variant);
let back: PolicyCategory = serde_json::from_str(&json).unwrap();
assert_eq!(back, variant, "{expected} parses back to its variant");
}
}
#[test]
fn every_known_wire_value_parses_to_a_known_variant_and_back() {
for value in PolicyCategory::KNOWN_WIRE_VALUES {
let category: PolicyCategory =
serde_json::from_value(Value::String(value.to_string())).unwrap();
assert!(category.is_known(), "{value} must be known");
assert_eq!(category.as_str(), *value);
assert_eq!(String::from(category), *value);
}
}
#[test]
fn the_five_categories_the_platform_added_are_known() {
for (value, variant) in [
("security-dangerous", PolicyCategory::SecurityDangerous),
("compliance-euaiact", PolicyCategory::ComplianceEuaiact),
("dangerous_queries", PolicyCategory::DangerousQueries),
("pii_detection", PolicyCategory::PiiDetection),
("sql_injection", PolicyCategory::SqlInjection),
] {
let parsed: PolicyCategory = serde_json::from_str(&format!("\"{value}\"")).unwrap();
assert_eq!(parsed, variant, "{value}");
}
}
#[test]
fn an_unknown_category_is_kept_and_re_serializes_byte_identical() {
let wire = r#""a-category-from-a-later-platform""#;
let parsed: PolicyCategory = serde_json::from_str(wire).unwrap();
assert_eq!(
parsed,
PolicyCategory::Unknown("a-category-from-a-later-platform".into())
);
assert!(!parsed.is_known());
assert_eq!(parsed.as_str(), "a-category-from-a-later-platform");
assert_eq!(parsed.to_string(), "a-category-from-a-later-platform");
assert_eq!(serde_json::to_string(&parsed).unwrap(), wire);
}
#[test]
fn a_platform_object_with_an_unknown_category_deserializes() {
#[derive(Deserialize)]
struct Policy {
category: PolicyCategory,
}
let known: Policy = serde_json::from_str(r#"{"category":"security-dangerous"}"#).unwrap();
assert_eq!(known.category, PolicyCategory::SecurityDangerous);
let unknown: Policy = serde_json::from_str(r#"{"category":"brand-new"}"#).unwrap();
assert_eq!(
unknown.category,
PolicyCategory::Unknown("brand-new".into())
);
}
#[test]
fn every_category_the_shipped_posture_uses_is_known() {
let posture = posture();
let missing: Vec<&str> = posture["categories"]
.as_array()
.expect("categories")
.iter()
.map(|c| c.as_str().expect("a category string"))
.filter(|c| !PolicyCategory::KNOWN_WIRE_VALUES.contains(c))
.collect();
assert!(
missing.is_empty(),
"the platform's shipped posture at {} uses categories PolicyCategory lacks: \
{missing:?} (getaxonflow/axonflow-enterprise#4224)",
posture["platform_commit"]
);
}
#[test]
fn every_variant_is_in_the_known_set_and_parses_back() {
use PolicyCategory::*;
let all = [
SecuritySqli,
SecurityAdmin,
PiiGlobal,
PiiUs,
PiiEu,
PiiIndia,
PiiSingapore,
PiiIndonesia,
CodeSecrets,
CodeUnsafe,
CodeCompliance,
SensitiveData,
MediaSafety,
MediaBiometric,
MediaDocument,
MediaPii,
DynamicRisk,
DynamicCompliance,
DynamicSecurity,
DynamicCost,
DynamicAccess,
SecurityDangerous,
ComplianceEuaiact,
DangerousQueries,
PiiDetection,
SqlInjection,
];
for c in &all {
match c {
SecuritySqli | SecurityAdmin | PiiGlobal | PiiUs | PiiEu | PiiIndia
| PiiSingapore | PiiIndonesia | CodeSecrets | CodeUnsafe | CodeCompliance
| SensitiveData | MediaSafety | MediaBiometric | MediaDocument | MediaPii
| DynamicRisk | DynamicCompliance | DynamicSecurity | DynamicCost
| DynamicAccess | SecurityDangerous | ComplianceEuaiact | DangerousQueries
| PiiDetection | SqlInjection => {}
Unknown(_) => unreachable!("the list names known variants only"),
}
}
assert_eq!(all.len(), PolicyCategory::KNOWN_WIRE_VALUES.len());
for c in all {
let wire = c.as_str().to_string();
assert!(
PolicyCategory::KNOWN_WIRE_VALUES.contains(&wire.as_str()),
"{wire}"
);
assert_eq!(PolicyCategory::from(wire.clone()), c, "{wire}");
}
}
#[test]
fn the_posture_fixture_names_its_source() {
let posture = posture();
assert_eq!(
posture["source"],
"platform/decision/pdp/shipped_posture.json"
);
let commit = posture["platform_commit"].as_str().expect("commit");
assert!(
commit.len() == 40 && commit.bytes().all(|b| b.is_ascii_hexdigit()),
"{commit}"
);
let sha = posture["source_sha256"].as_str().expect("sha256");
assert!(
sha.len() == 64 && sha.bytes().all(|b| b.is_ascii_hexdigit()),
"{sha}"
);
let categories: Vec<&str> = posture["categories"]
.as_array()
.expect("categories")
.iter()
.map(|c| c.as_str().expect("a category string"))
.collect();
let mut canonical = categories.clone();
canonical.sort_unstable();
canonical.dedup();
assert_eq!(categories, canonical);
}
}