1#[derive(Debug, Clone, serde::Serialize)]
5#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
6pub struct GuardReport {
7 pub files: Vec<GuardFileReport>,
9}
10
11#[derive(Debug, Clone, serde::Serialize)]
13#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
14pub struct GuardFileReport {
15 pub path: String,
17 pub exists: bool,
19 pub zone: Option<GuardZone>,
21 pub boundary: GuardBoundary,
23 pub policy_rules: Vec<GuardPolicyRule>,
25 pub severities: GuardSeverities,
27 pub notes: Vec<String>,
29}
30
31#[derive(Debug, Clone, serde::Serialize)]
33#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
34pub struct GuardZone {
35 pub name: String,
37 pub patterns: Vec<String>,
39}
40
41#[derive(Debug, Clone, serde::Serialize)]
43#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
44pub struct GuardBoundary {
45 pub configured: bool,
47 pub unrestricted: bool,
49 pub allowed_zones: Vec<String>,
51 pub allowed_type_only_zones: Vec<String>,
53 pub forbidden_calls: Vec<String>,
55 pub coverage_required: bool,
57}
58
59#[derive(Debug, Clone, serde::Serialize)]
61#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
62pub struct GuardPolicyRule {
63 pub pack: String,
65 pub rule_id: String,
67 pub kind: String,
69 pub patterns: Vec<String>,
71 #[serde(default, skip_serializing_if = "Vec::is_empty")]
73 pub allowed_files: Vec<String>,
74 #[serde(default, skip_serializing_if = "Vec::is_empty")]
76 pub proof_kinds: Vec<String>,
77 pub message: Option<String>,
79 pub severity: String,
81 pub suppress_token: String,
83}
84
85#[derive(Debug, Clone, serde::Serialize)]
87#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
88pub struct GuardSeverities {
89 pub boundary_violation: String,
91 pub policy_violation: String,
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn guard_file_report_serializes_expected_wire_shape() {
101 let report = GuardReport {
102 files: vec![GuardFileReport {
103 path: "src/domain/user.ts".to_string(),
104 exists: false,
105 zone: None,
106 boundary: GuardBoundary {
107 configured: true,
108 unrestricted: true,
109 allowed_zones: vec![],
110 allowed_type_only_zones: vec![],
111 forbidden_calls: vec!["child_process.*".to_string()],
112 coverage_required: true,
113 },
114 policy_rules: vec![GuardPolicyRule {
115 pack: "team-policy".to_string(),
116 rule_id: "pure-domain".to_string(),
117 kind: "banned-effect".to_string(),
118 patterns: vec!["network".to_string()],
119 allowed_files: vec![],
120 proof_kinds: vec![],
121 message: Some("Inject effects via ports.".to_string()),
122 severity: "warn".to_string(),
123 suppress_token: "policy-violation:team-policy/pure-domain".to_string(),
124 }],
125 severities: GuardSeverities {
126 boundary_violation: "error".to_string(),
127 policy_violation: "warn".to_string(),
128 },
129 notes: vec!["Files outside every zone are unrestricted.".to_string()],
130 }],
131 };
132
133 let json = serde_json::to_value(report).unwrap();
134 let file = &json["files"][0];
135 assert_eq!(file["path"], "src/domain/user.ts");
136 assert_eq!(file["exists"], false);
137 assert!(file["zone"].is_null());
138 assert_eq!(file["boundary"]["allowed_zones"], serde_json::json!([]));
139 assert_eq!(
140 file["boundary"]["allowed_type_only_zones"],
141 serde_json::json!([])
142 );
143 assert_eq!(file["boundary"]["coverage_required"], true);
144 assert_eq!(file["policy_rules"][0]["rule_id"], "pure-domain");
145 assert_eq!(
146 file["policy_rules"][0]["suppress_token"],
147 "policy-violation:team-policy/pure-domain"
148 );
149 assert_eq!(file["severities"]["boundary_violation"], "error");
150 }
151}