use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum VulnerabilityType {
#[serde(rename = "LINES")]
Lines,
#[serde(rename = "INPUTS")]
Inputs,
#[serde(rename = "PORTS")]
Ports,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Technique {
#[serde(rename = "SAST")]
Sast,
#[serde(rename = "SCA")]
Sca,
#[serde(rename = "DAST")]
Dast,
#[serde(rename = "CSPM")]
Cspm,
#[serde(rename = "MAST")]
Mast,
#[serde(rename = "SS")]
Ss,
#[serde(rename = "CS")]
Cs,
#[serde(rename = "AI_SAST")]
AiSast,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum VlaiLevel {
#[serde(rename = "Low")]
Low,
#[serde(rename = "Medium")]
Medium,
#[serde(rename = "High")]
High,
#[serde(rename = "Critical")]
Critical,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Level {
#[serde(rename = "none")]
None,
#[serde(rename = "note")]
Note,
#[serde(rename = "warning")]
Warning,
#[serde(rename = "error")]
Error,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SarifVersion {
#[serde(rename = "2.1.0")]
V210,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SecretStatus {
#[serde(rename = "active")]
Active,
#[serde(rename = "inactive")]
Inactive,
#[serde(rename = "unverifiable")]
Unverifiable,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Epss {
Float(f64),
Text(String),
}
fn in_range<E: serde::de::Error>(value: f64, max: f64) -> Result<f64, E> {
if (0.0..=max).contains(&value) {
Ok(value)
} else {
Err(E::custom(format!("{value} is outside [0, {max}]")))
}
}
fn probability<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<f64, D::Error> {
in_range(f64::deserialize(deserializer)?, 1.0)
}
fn severity_score<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<f64, D::Error> {
in_range(f64::deserialize(deserializer)?, 10.0)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ScaSeverityCves {
#[serde(skip_serializing_if = "Option::is_none")]
pub critical: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub high: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub medium: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub low: Option<Vec<String>>,
pub total: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ScaResidualCves {
#[serde(skip_serializing_if = "Option::is_none")]
pub remedied: Option<ScaSeverityCves>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maintained: Option<ScaSeverityCves>,
#[serde(skip_serializing_if = "Option::is_none")]
pub introduced: Option<ScaSeverityCves>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ScaFixMetadata {
pub fix_version: String,
pub upgrade_type: String,
pub breaking_change: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub residual_cves: Option<ScaResidualCves>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ScaFixMetadataFull {
#[serde(skip_serializing_if = "Option::is_none")]
pub closest_min_fix: Option<ScaFixMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_parents: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub closest_safe_fix: Option<ScaFixMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub closest_complete_fix: Option<ScaFixMetadata>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ScaProperties {
pub package: String,
pub vulnerable_version: String,
pub advisory_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub epss: Option<Epss>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(default)]
pub is_reachable: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub package_manager: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ecosystem: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dependency_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reachability_status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fix_metadata_new: Option<ScaFixMetadataFull>,
#[serde(default)]
pub kev_catalog: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VlaiConfidenceByLevel {
#[serde(deserialize_with = "probability")]
pub low: f64,
#[serde(deserialize_with = "probability")]
pub medium: f64,
#[serde(deserialize_with = "probability")]
pub high: f64,
#[serde(deserialize_with = "probability")]
pub critical: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VlaiProperties {
pub level: VlaiLevel,
#[serde(deserialize_with = "severity_score")]
pub score: f64,
#[serde(deserialize_with = "probability")]
pub confidence: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidence_by_level: Option<VlaiConfidenceByLevel>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FluidResultProperties {
#[serde(default)]
pub auto_approve: bool,
pub vulnerability_type: VulnerabilityType,
pub technique: Technique,
pub source_method: String,
#[serde(default)]
pub method_developer: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cwe_ids: Option<Vec<String>>,
pub cvss_v4: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cvss_v4_score: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cvss_v4_severity: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sca: Option<ScaProperties>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vlai: Option<VlaiProperties>,
#[serde(default)]
pub has_redirect: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub original_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub environment_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub missing_dependency: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub secret_status: Option<SecretStatus>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FluidRuleProperties {
#[serde(default)]
pub auto_approve: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FluidCspmErrorMethods {
#[serde(default)]
pub aws: Vec<String>,
#[serde(default)]
pub aws_detailed_errors: HashMap<String, HashMap<String, Vec<String>>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FluidWebResponseProperties {
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ssl_status: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_methods: Option<FluidCspmErrorMethods>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FluidNotificationProperties {
pub package_manager: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidRegionProperties {
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub column: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line_context: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub wrap: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub show_line_numbers: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub highlight_line_number: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidMessage {
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidArtifactContent {
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidArtifactLocation {
pub uri: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidRegion {
#[serde(rename = "startLine", skip_serializing_if = "Option::is_none")]
pub start_line: Option<i64>,
#[serde(rename = "startColumn", skip_serializing_if = "Option::is_none")]
pub start_column: Option<i64>,
#[serde(rename = "endLine", skip_serializing_if = "Option::is_none")]
pub end_line: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub snippet: Option<FluidArtifactContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<FluidMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<FluidRegionProperties>,
#[serde(rename = "sourceLanguage", skip_serializing_if = "Option::is_none")]
pub source_language: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidPhysicalLocation {
#[serde(rename = "artifactLocation")]
pub artifact_location: FluidArtifactLocation,
#[serde(skip_serializing_if = "Option::is_none")]
pub region: Option<FluidRegion>,
#[serde(rename = "contextRegion", skip_serializing_if = "Option::is_none")]
pub context_region: Option<FluidRegion>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidLocation {
#[serde(rename = "physicalLocation")]
pub physical_location: FluidPhysicalLocation,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidToolComponentRef {
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidTaxon {
pub id: String,
#[serde(rename = "toolComponent", skip_serializing_if = "Option::is_none")]
pub tool_component: Option<FluidToolComponentRef>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidResult {
#[serde(rename = "ruleId")]
pub rule_id: String,
pub level: Level,
pub message: FluidMessage,
pub locations: Vec<FluidLocation>,
pub fingerprints: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub taxa: Option<Vec<FluidTaxon>>,
pub properties: FluidResultProperties,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidMultiformatMessageString {
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub markdown: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidTaxonomyTaxon {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(rename = "shortDescription", skip_serializing_if = "Option::is_none")]
pub short_description: Option<FluidMultiformatMessageString>,
#[serde(rename = "fullDescription", skip_serializing_if = "Option::is_none")]
pub full_description: Option<FluidMultiformatMessageString>,
#[serde(rename = "helpUri", skip_serializing_if = "Option::is_none")]
pub help_uri: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidTaxonomyComponent {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(rename = "informationUri", skip_serializing_if = "Option::is_none")]
pub information_uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub organization: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub taxa: Option<Vec<FluidTaxonomyTaxon>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidRule {
pub id: String,
pub name: String,
#[serde(rename = "fullDescription", skip_serializing_if = "Option::is_none")]
pub full_description: Option<FluidMultiformatMessageString>,
#[serde(rename = "helpUri", skip_serializing_if = "Option::is_none")]
pub help_uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub help: Option<FluidMultiformatMessageString>,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<FluidRuleProperties>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidDriver {
pub name: String,
pub version: String,
#[serde(rename = "semanticVersion", skip_serializing_if = "Option::is_none")]
pub semantic_version: Option<String>,
#[serde(default)]
pub rules: Vec<FluidRule>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidTool {
pub driver: FluidDriver,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidVersionControlProvenance {
#[serde(rename = "repositoryUri")]
pub repository_uri: String,
#[serde(rename = "revisionId")]
pub revision_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidOriginalUriBaseId {
pub uri: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidWebResponse {
#[serde(rename = "statusCode", skip_serializing_if = "Option::is_none")]
pub status_code: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<FluidWebResponseProperties>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidNotification {
pub message: FluidMessage,
#[serde(skip_serializing_if = "Option::is_none")]
pub locations: Option<Vec<FluidLocation>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub level: Option<Level>,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<FluidNotificationProperties>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidInvocation {
#[serde(rename = "executionSuccessful")]
pub execution_successful: bool,
#[serde(
rename = "toolExecutionNotifications",
skip_serializing_if = "Option::is_none"
)]
pub tool_execution_notifications: Option<Vec<FluidNotification>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidRun {
pub tool: FluidTool,
#[serde(
rename = "versionControlProvenance",
skip_serializing_if = "Option::is_none"
)]
pub version_control_provenance: Option<Vec<FluidVersionControlProvenance>>,
#[serde(rename = "originalUriBaseIds", skip_serializing_if = "Option::is_none")]
pub original_uri_base_ids: Option<HashMap<String, FluidOriginalUriBaseId>>,
#[serde(default)]
pub results: Vec<FluidResult>,
#[serde(rename = "webResponses", skip_serializing_if = "Option::is_none")]
pub web_responses: Option<Vec<FluidWebResponse>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub taxonomies: Option<Vec<FluidTaxonomyComponent>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub invocations: Option<Vec<FluidInvocation>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidSarifReport {
#[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
pub schema_url: Option<String>,
pub version: SarifVersion,
pub runs: Vec<FluidRun>,
}
impl FluidSarifReport {
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic
)]
mod tests {
use super::*;
fn skims_report() -> FluidSarifReport {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../test/data/skims_results.sarif"
);
let json = std::fs::read_to_string(path).expect("skims fixture not found");
FluidSarifReport::from_json(&json).expect("skims parse failed")
}
fn sca_report() -> FluidSarifReport {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../test/data/sca_results.sarif"
);
let json = std::fs::read_to_string(path).expect("sca fixture not found");
FluidSarifReport::from_json(&json).expect("sca parse failed")
}
#[test]
fn test_skims_top_level() {
let r = skims_report();
assert_eq!(r.version, SarifVersion::V210);
assert!(r.schema_url.is_some());
assert_eq!(r.runs.len(), 1);
}
#[test]
fn test_sca_top_level() {
let r = sca_report();
assert_eq!(r.version, SarifVersion::V210);
assert!(r.schema_url.is_some());
assert_eq!(r.runs.len(), 1);
}
#[test]
fn test_skims_tool_driver() {
let driver = &skims_report().runs[0].tool.driver;
assert_eq!(driver.name, "Fluid Attacks SAST");
assert_eq!(driver.version, "1.0.0");
assert_eq!(driver.semantic_version.as_deref(), Some("1.0.0"));
}
#[test]
fn test_sca_tool_driver() {
let driver = &sca_report().runs[0].tool.driver;
assert_eq!(driver.name, "Fluid Attacks SCA");
assert_eq!(driver.version, "1.0.0");
assert_eq!(driver.semantic_version.as_deref(), Some("1.0.0"));
}
#[test]
fn test_skims_rules() {
let rules = &skims_report().runs[0].tool.driver.rules;
assert_eq!(rules.len(), 4);
let rule = rules
.iter()
.find(|r| r.id == "015")
.expect("rule 015 not found");
assert_eq!(rule.name, "Insecure authentication method - Basic");
assert!(rule.help_uri.is_some());
let desc = rule.full_description.as_ref().expect("no full_description");
assert!(!desc.text.is_empty());
let props = rule.properties.as_ref().expect("no properties");
assert!(props.auto_approve);
}
#[test]
fn test_sca_rules() {
let rules = &sca_report().runs[0].tool.driver.rules;
assert!(!rules.is_empty());
let rule = rules
.iter()
.find(|r| r.id == "120")
.expect("rule 120 not found");
assert_eq!(rule.name, "Improper dependency pinning");
assert!(rule.help_uri.is_some());
let props = rule.properties.as_ref().expect("no properties");
assert!(props.auto_approve);
}
#[test]
fn test_skims_results_count() {
assert_eq!(skims_report().runs[0].results.len(), 5);
}
#[test]
fn test_skims_result_fields() {
let result = &skims_report().runs[0].results[0];
assert_eq!(result.rule_id, "004");
assert_eq!(result.level, Level::Error);
assert!(result.message.text.is_some());
assert_eq!(result.locations.len(), 1);
}
#[test]
fn test_skims_result_location() {
let report = skims_report();
let phys = &report.runs[0].results[0].locations[0].physical_location;
assert_eq!(
phys.artifact_location.uri,
"test/data/sarif_test/js_sandbox_escape_vm2.js"
);
let region = phys.region.as_ref().expect("no region");
assert_eq!(region.start_line, Some(7));
let snippet = region.snippet.as_ref().expect("no snippet");
assert!(!snippet.text.is_empty());
}
#[test]
fn test_skims_result_fingerprints() {
let fingerprints = &skims_report().runs[0].results[0].fingerprints;
assert!(fingerprints.contains_key("guid"));
}
#[test]
fn test_skims_result_properties() {
let props = &skims_report().runs[0].results[0].properties;
assert_eq!(props.technique, Technique::Sast);
assert_eq!(props.vulnerability_type, VulnerabilityType::Lines);
assert!(props.cvss_v4.starts_with("CVSS:4.0/"));
assert_eq!(props.cvss_v4_score, Some(8.1));
assert_eq!(props.cvss_v4_severity.as_deref(), Some("High"));
assert_eq!(props.cwe_ids, Some(vec!["CWE-94".to_owned()]));
assert_eq!(props.source_method, "javascript.vm2.f004.cve_2023_32314");
assert!(props.auto_approve);
assert!(props.sca.is_none());
}
#[test]
fn test_sca_results_non_empty() {
assert!(!sca_report().runs[0].results.is_empty());
}
#[test]
fn test_sca_result_technique() {
let props = &sca_report().runs[0].results[0].properties;
assert_eq!(props.technique, Technique::Sca);
assert_eq!(props.vulnerability_type, VulnerabilityType::Lines);
assert!(props.sca.is_some());
}
#[test]
fn test_sca_result_sca_block() {
let sca = sca_report().runs[0].results[0]
.properties
.sca
.clone()
.expect("no sca block");
assert_eq!(sca.package, "vm2");
assert_eq!(sca.advisory_id, "CVE-2023-37903");
assert_eq!(sca.package_manager.as_deref(), Some("npm"));
assert_eq!(sca.ecosystem.as_deref(), Some("npm"));
assert_eq!(sca.dependency_type.as_deref(), Some("DIRECT"));
assert_eq!(sca.scope.as_deref(), Some("RUN"));
assert!(!sca.is_reachable);
assert!(!sca.kev_catalog);
}
#[test]
fn test_sca_result_fix_metadata() {
let sca = sca_report().runs[0].results[0]
.properties
.sca
.clone()
.expect("no sca block");
let fix = sca.fix_metadata_new.expect("no fix_metadata_new");
let min_fix = fix.closest_min_fix.expect("no closest_min_fix");
assert_eq!(min_fix.fix_version, "3.10.0");
assert_eq!(min_fix.upgrade_type, "minor");
assert!(!min_fix.breaking_change);
let residual = min_fix.residual_cves.expect("no residual_cves");
let remedied = residual.remedied.expect("no remedied");
let critical = remedied.critical.expect("no critical");
assert!(critical.contains(&"CVE-2023-37466".to_owned()));
assert!(critical.contains(&"CVE-2023-32314".to_owned()));
}
#[test]
fn test_secret_status_serde() {
let json = "\"active\"";
let parsed: SecretStatus = serde_json::from_str(json).expect("active parse failed");
assert_eq!(parsed, SecretStatus::Active);
let serialized = serde_json::to_string(&SecretStatus::Active).expect("serialize failed");
assert_eq!(serialized, json);
}
#[test]
fn test_fluid_result_properties_secret_status_omitted_when_none() {
let props = &skims_report().runs[0].results[0].properties;
assert!(props.secret_status.is_none());
let serialized = serde_json::to_string(props).expect("properties serialize failed");
assert!(!serialized.contains("secret_status"));
}
#[test]
fn test_technique_ai_sast_serde() {
let json = "\"AI_SAST\"";
let parsed: Technique = serde_json::from_str(json).expect("AI_SAST parse failed");
assert_eq!(parsed, Technique::AiSast);
let serialized = serde_json::to_string(&Technique::AiSast).expect("serialize failed");
assert_eq!(serialized, json);
}
#[test]
fn test_fluid_region_source_language_roundtrip() {
let json = r#"{"sourceLanguage":"python"}"#;
let region: FluidRegion = serde_json::from_str(json).expect("region parse failed");
assert_eq!(region.source_language.as_deref(), Some("python"));
let serialized = serde_json::to_string(®ion).expect("region serialize failed");
assert_eq!(serialized, json);
}
#[test]
fn test_fluid_region_source_language_omitted_when_none() {
let json = r"{}";
let region: FluidRegion = serde_json::from_str(json).expect("empty region parse failed");
assert!(region.source_language.is_none());
let serialized = serde_json::to_string(®ion).expect("empty region serialize failed");
assert_eq!(serialized, json);
}
#[test]
fn test_fluid_run_invocations_roundtrip() {
let json = r#"{"tool":{"driver":{"name":"x","version":"1.0.0","rules":[]}},"results":[],"invocations":[{"executionSuccessful":false,"toolExecutionNotifications":[{"message":{"text":"incomplete resolution"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"pom.xml"}}}],"properties":{"package_manager":"maven"}}]}]}"#;
let run: FluidRun = serde_json::from_str(json).expect("run parse failed");
let invocations = run.invocations.as_ref().expect("no invocations");
assert!(!invocations[0].execution_successful);
let notifications = invocations[0]
.tool_execution_notifications
.as_ref()
.expect("no notifications");
let props = notifications[0].properties.as_ref().expect("no properties");
assert_eq!(props.package_manager, "maven");
let serialized = serde_json::to_string(&run).expect("run serialize failed");
assert_eq!(serialized, json);
}
#[test]
fn test_fluid_run_invocations_omitted_when_none() {
let json = r#"{"tool":{"driver":{"name":"x","version":"1.0.0","rules":[]}},"results":[]}"#;
let run: FluidRun = serde_json::from_str(json).expect("empty run parse failed");
assert!(run.invocations.is_none());
let serialized = serde_json::to_string(&run).expect("empty run serialize failed");
assert_eq!(serialized, json);
}
fn result_properties_json(vlai: &str) -> String {
format!(
r#"{{"vulnerability_type":"LINES","technique":"AI_SAST","source_method":"sifts","cvss_v4":"CVSS:4.0/AV:N"{vlai}}}"#
)
}
#[test]
fn test_result_properties_vlai_roundtrip() {
let json = result_properties_json(
r#","vlai":{"level":"High","score":8.0,"confidence":0.91,"confidence_by_level":{"low":0.01,"medium":0.05,"high":0.91,"critical":0.03}}"#,
);
let props: FluidResultProperties = serde_json::from_str(&json).expect("vlai parse failed");
let vlai = props.vlai.as_ref().expect("no vlai block");
assert_eq!(vlai.level, VlaiLevel::High);
assert_eq!(props.vlai.as_ref().map(|block| block.score), Some(8.0));
assert_eq!(
vlai.confidence_by_level
.as_ref()
.map(|by_level| by_level.critical),
Some(0.03)
);
}
#[test]
fn test_result_properties_vlai_omitted_when_none() {
let props: FluidResultProperties =
serde_json::from_str(&result_properties_json("")).expect("parse failed");
assert!(props.vlai.is_none());
let serialized = serde_json::to_string(&props).expect("serialize failed");
assert!(!serialized.contains("vlai"));
}
#[test]
fn test_result_properties_vlai_rejects_malformed_payloads() {
for vlai in [
r#","vlai":{"level":"Severe","score":8.0,"confidence":0.9}"#,
r#","vlai":{"level":"high","score":8.0,"confidence":0.9}"#,
r#","vlai":{"level":"High","score":8.0,"confidence":0.9,"probs":{"high":0.9}}"#,
r#","vlai":{"level":"High","score":8.0}"#,
r#","vlai":{"level":"High","score":11.0,"confidence":0.9}"#,
r#","vlai":{"level":"High","score":8.0,"confidence":1.5}"#,
r#","vlai":{"level":"High","score":8.0,"confidence":0.9,"confidence_by_level":{"low":1.5,"medium":0.0,"high":0.0,"critical":0.0}}"#,
r#","vlai":{"level":"High","score":8.0,"confidence":0.9,"confidence_by_level":{"low":0.0,"medium":1.5,"high":0.0,"critical":0.0}}"#,
r#","vlai":{"level":"High","score":8.0,"confidence":0.9,"confidence_by_level":{"low":0.0,"medium":0.0,"high":1.5,"critical":0.0}}"#,
r#","vlai":{"level":"High","score":8.0,"confidence":0.9,"confidence_by_level":{"low":0.0,"medium":0.0,"high":0.0,"critical":1.5}}"#,
] {
let parsed: Result<FluidResultProperties, _> =
serde_json::from_str(&result_properties_json(vlai));
assert!(parsed.is_err(), "accepted malformed vlai: {vlai}");
}
}
#[test]
fn test_result_properties_vlai_accepts_inclusive_bounds() {
for vlai in [
r#","vlai":{"level":"High","score":0.0,"confidence":0.0,"confidence_by_level":{"low":0.0,"medium":1.0,"high":0.5,"critical":0.0}}"#,
r#","vlai":{"level":"High","score":10.0,"confidence":1.0,"confidence_by_level":{"low":1.0,"medium":0.0,"high":0.5,"critical":1.0}}"#,
] {
let parsed: Result<FluidResultProperties, _> =
serde_json::from_str(&result_properties_json(vlai));
assert!(parsed.is_ok(), "rejected valid vlai: {vlai}");
}
}
}