use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Info,
Low,
Medium,
High,
Critical,
#[serde(other)]
Unknown,
}
impl Severity {
pub fn rank(self) -> u8 {
match self {
Severity::Critical => 4,
Severity::High => 3,
Severity::Medium => 2,
Severity::Low => 1,
Severity::Info | Severity::Unknown => 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Tier {
Green,
Yellow,
Orange,
Red,
Unscoped,
#[serde(other)]
Unknown,
}
impl Tier {
pub fn label(self) -> &'static str {
match self {
Tier::Green => "Green",
Tier::Yellow => "Yellow",
Tier::Orange => "Orange",
Tier::Red => "Red",
Tier::Unscoped => "Unscoped",
Tier::Unknown => "Unknown",
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct EvidenceLine {
pub line_no: u32,
pub text: String,
pub hit: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct EvidenceExcerpt {
pub file: String,
#[serde(default)]
pub lang: Option<String>,
#[serde(default)]
pub lines: Vec<EvidenceLine>,
#[serde(default)]
pub truncated: bool,
}
impl EvidenceExcerpt {
pub fn hit_line(&self) -> Option<&EvidenceLine> {
self.lines
.iter()
.find(|l| l.hit)
.or_else(|| self.lines.first())
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct FindingResponse {
pub id: String,
pub rule_id: String,
pub severity: Severity,
pub sub_score: String,
pub penalty: i32,
pub status_at_scan: String,
pub file_path: String,
pub line_start: u32,
#[serde(default)]
pub line_end: Option<u32>,
pub matched_content_sha256: String,
pub remediation_link: String,
pub rubric_version: String,
#[serde(default)]
pub evidence_excerpt: Option<EvidenceExcerpt>,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub explanation: Option<String>,
#[serde(default)]
pub category_label: Option<String>,
#[serde(default)]
pub severity_rationale: Option<String>,
#[serde(default)]
pub remediation: Option<FindingRemediation>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SaferPattern {
pub before: String,
pub after: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct FindingRemediation {
pub action: String,
#[serde(default)]
pub steps: Option<Vec<String>>,
#[serde(default)]
pub safer_pattern: Option<SaferPattern>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CatalogItemSummary {
pub id: String,
pub slug: String,
pub kind: String,
pub display_name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub github_url: Option<String>,
#[serde(default)]
pub github_org: Option<String>,
#[serde(default)]
pub github_repo: Option<String>,
#[serde(default)]
pub source_kind: Option<String>,
pub popularity_tier: String,
#[serde(default)]
pub popularity_score: i64,
#[serde(default)]
pub latest_scan_score: Option<u8>,
#[serde(default)]
pub latest_scan_tier: Option<Tier>,
#[serde(default)]
pub latest_scan_at: Option<String>,
#[serde(default)]
pub findings_count: i64,
#[serde(default)]
pub registries: Vec<String>,
#[serde(default)]
pub agent_compatibility: Vec<String>,
#[serde(default)]
pub updated_at: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CatalogListEnvelope {
#[serde(default)]
pub data: Vec<CatalogItemSummary>,
#[serde(default)]
pub next_cursor: Option<String>,
#[serde(default)]
pub total_count: i64,
#[serde(default)]
pub page: i64,
#[serde(default)]
pub total_pages: i64,
#[serde(default)]
pub page_size: i64,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct InstallSpec {
#[serde(default)]
pub kind: Option<String>,
#[serde(default)]
pub mcp_entry: Option<serde_json::Value>,
#[serde(default)]
pub hook_events: Option<Vec<String>>,
#[serde(default)]
pub rules_files: Option<Vec<RulesFile>>,
#[serde(default)]
pub plugin_ref: Option<PluginRef>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RulesFile {
pub path: String,
#[serde(default)]
pub target: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct PluginRef {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub marketplace_git: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ScanReportDetail {
pub id: String,
#[serde(default)]
pub github_url: Option<String>,
pub slug: String,
pub display_name: String,
pub aggregate_score: u8,
pub tier: Tier,
#[serde(default)]
pub sub_scores: BTreeMap<String, i64>,
#[serde(default)]
pub findings: Vec<FindingResponse>,
#[serde(default)]
pub scanned_at: Option<String>,
#[serde(default)]
pub rubric_version: Option<String>,
#[serde(default)]
pub engine_version: Option<String>,
#[serde(default)]
pub ref_sha: Option<String>,
#[serde(default)]
pub component_path: Option<String>,
#[serde(default)]
pub scan_run_id: Option<String>,
#[serde(default)]
pub install_spec: Option<InstallSpec>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ItemDetailResponse {
pub item: CatalogItemSummary,
#[serde(default)]
pub latest_scan: Option<ScanReportDetail>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CapabilityRow {
pub kind: String,
pub name: String,
#[serde(default)]
pub component_path: Option<String>,
pub aggregate_score: u8,
pub tier: Tier,
pub scan_id: String,
pub catalog_slug: String,
#[serde(default)]
pub sub_scores: BTreeMap<String, i64>,
#[serde(default)]
pub findings: Vec<FindingResponse>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ScanRunReportDetail {
pub id: String,
#[serde(default)]
pub github_url: Option<String>,
pub repo_aggregate_score: u8,
pub repo_tier: Tier,
#[serde(default)]
pub kind_tally: BTreeMap<String, i64>,
#[serde(default)]
pub capability_count: i64,
#[serde(default)]
pub capabilities: Vec<CapabilityRow>,
#[serde(default)]
pub status: Option<String>,
#[serde(default)]
pub visibility: Option<String>,
#[serde(default)]
pub source_kind: Option<String>,
#[serde(default)]
pub share_url: Option<String>,
#[serde(default)]
pub report_url: Option<String>,
#[serde(default)]
pub expires_at: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ChallengeResponse {
pub challenge: String,
pub difficulty: u32,
#[serde(default)]
pub expires_at: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ScanSubmitResponse {
pub id: String,
pub status: String,
#[serde(default)]
pub cached: bool,
#[serde(default)]
pub rubric_version: Option<String>,
#[serde(default)]
pub share_url: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ScanUploadResponse {
pub id: String,
pub status: String,
#[serde(default)]
pub source_kind: Option<String>,
#[serde(default)]
pub visibility: Option<String>,
#[serde(default)]
pub slug: Option<String>,
#[serde(default)]
pub share_url: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct BootstrapResponse {
pub run_id: String,
pub prompt: String,
pub consent_notice: String,
pub pack_url: String,
pub submit_token: String,
pub poll_url: String,
#[serde(default)]
pub share_token: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AgentStatusResponse {
pub status: String,
#[serde(default)]
pub score: Option<i64>,
#[serde(default)]
pub band: Option<String>,
#[serde(default)]
pub report_url: Option<String>,
#[serde(default)]
pub share_url: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AgentSaferPattern {
pub before: String,
pub after: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AgentRemediation {
pub action: String,
#[serde(default)]
pub steps: Option<Vec<String>>,
#[serde(default)]
pub safer_pattern: Option<AgentSaferPattern>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AgentCheckRow {
pub test_id: String,
pub family: String,
pub title: String,
pub verdict: String,
pub severity: Severity,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AgentFindingDto {
pub id: String,
pub test_id: String,
pub severity: Severity,
pub verdict: String,
pub family: String,
#[serde(default)]
pub owasp_refs: Vec<String>,
#[serde(default)]
pub atlas_refs: Vec<String>,
#[serde(default)]
pub nist_refs: Vec<String>,
pub score_delta: i64,
pub detection_rule: String,
#[serde(default)]
pub leaked_canary_slot: Option<String>,
pub title: String,
pub explanation: String,
#[serde(default)]
pub severity_rationale: Option<String>,
#[serde(default)]
pub category_label: Option<String>,
pub remediation: AgentRemediation,
#[serde(default)]
pub evidence_excerpt: Option<EvidenceExcerpt>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AgentScanReport {
pub id: String,
pub status: String,
pub agent_name: String,
pub runtime: String,
#[serde(default)]
pub score: Option<u8>,
pub band: Tier,
#[serde(default)]
pub verdict_label: Option<String>,
#[serde(default)]
pub cap_callout: Option<String>,
#[serde(default)]
pub confidence: Option<String>,
#[serde(default)]
pub score_breakdown: Option<serde_json::Value>,
#[serde(default)]
pub trust_labels: Vec<String>,
pub pack_id: String,
pub pack_version: String,
#[serde(default)]
pub pack_signature_verified: Option<bool>,
#[serde(default)]
pub capabilities_present: Vec<String>,
#[serde(default)]
pub capabilities_absent: Vec<String>,
#[serde(default)]
pub family_tally: BTreeMap<String, i64>,
#[serde(default)]
pub checks: Vec<AgentCheckRow>,
#[serde(default)]
pub findings: Vec<AgentFindingDto>,
#[serde(default)]
pub component_scores: Vec<serde_json::Value>,
pub visibility: String,
#[serde(default)]
pub expires_at: Option<String>,
#[serde(default)]
pub share_url: Option<String>,
#[serde(default)]
pub report_url: Option<String>,
pub rubric_version: String,
pub engine_version: String,
#[serde(default)]
pub latency_ms: i64,
#[serde(default)]
pub scanned_at: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HealthResponse {
pub status: String,
pub version: String,
pub git_sha: String,
#[serde(default)]
pub migrations_ok: bool,
#[serde(default)]
pub migrations_error: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn severity_deserializes_lowercase() {
let s: Severity = serde_json::from_str("\"critical\"").unwrap();
assert_eq!(s, Severity::Critical);
}
#[test]
fn unknown_severity_falls_through() {
let s: Severity = serde_json::from_str("\"apocalyptic\"").unwrap();
assert_eq!(s, Severity::Unknown);
assert_eq!(s.rank(), 0);
}
#[test]
fn unknown_tier_falls_through() {
let t: Tier = serde_json::from_str("\"plaid\"").unwrap();
assert_eq!(t, Tier::Unknown);
}
#[test]
fn severity_rank_orders_correctly() {
assert!(Severity::Critical.rank() > Severity::High.rank());
assert!(Severity::High.rank() > Severity::Low.rank());
assert_eq!(Severity::Info.rank(), 0);
}
#[test]
fn list_envelope_uses_data_key() {
let json = r#"{"data":[],"total_count":0,"page":1,"total_pages":0,"page_size":24}"#;
let env: CatalogListEnvelope = serde_json::from_str(json).unwrap();
assert_eq!(env.total_count, 0);
assert!(env.data.is_empty());
}
#[test]
fn evidence_excerpt_hit_line() {
let json = r#"{"file":"a.py","lines":[{"line_no":1,"text":"ok","hit":false},{"line_no":2,"text":"BAD","hit":true}],"truncated":false}"#;
let ex: EvidenceExcerpt = serde_json::from_str(json).unwrap();
assert_eq!(ex.hit_line().unwrap().line_no, 2);
assert_eq!(ex.hit_line().unwrap().text, "BAD");
}
#[test]
fn unknown_object_keys_are_ignored() {
let json = r#"{"id":"x","slug":"a--b--skill-c","kind":"skill","display_name":"C","popularity_tier":"emerging","item_metadata":{"z":1},"sources":[]}"#;
let item: CatalogItemSummary = serde_json::from_str(json).unwrap();
assert_eq!(item.slug, "a--b--skill-c");
}
}