Skip to main content

allow_report/
json.rs

1use allow_core::{MatchOutcome, json_escape};
2
3use crate::{
4    ARTIFACT_STATUS_FAILED, ARTIFACT_STATUS_PASSED, ArtifactContract, CLAIM_BOUNDARY,
5    InventoryContext, ReportContext, SCANNER_LIMITATIONS,
6};
7
8pub(crate) fn push_json_artifact_header(
9    out: &mut String,
10    contract: ArtifactContract,
11    command: &str,
12) {
13    if let Some(fixed_command) = contract.fixed_command {
14        debug_assert_eq!(fixed_command, command);
15    }
16    let schema_version = contract.schema_version;
17    out.push_str(&format!("  \"schema_version\": {schema_version},\n"));
18    out.push_str(&format!(
19        "  \"schema_id\": \"{}\",\n",
20        json_escape(contract.schema_id)
21    ));
22    out.push_str("  \"tool\": \"cargo-allow\",\n");
23    out.push_str(&format!("  \"command\": \"{}\",\n", json_escape(command)));
24}
25
26pub(crate) fn push_json_artifact_preamble(
27    out: &mut String,
28    contract: ArtifactContract,
29    command: &str,
30    inventory: InventoryContext<'_>,
31) {
32    push_json_artifact_header(out, contract, command);
33    push_json_artifact_source_context(out, inventory);
34}
35
36pub(crate) fn push_json_fixed_artifact_preamble(
37    out: &mut String,
38    contract: ArtifactContract,
39    inventory: InventoryContext<'_>,
40) {
41    let Some(command) = contract.fixed_command else {
42        std::panic::panic_any("fixed artifact preamble requires a fixed-command artifact contract");
43    };
44    push_json_artifact_preamble(out, contract, command, inventory);
45}
46
47pub(crate) fn push_json_artifact_source_context(out: &mut String, inventory: InventoryContext<'_>) {
48    out.push_str(&format!(
49        "  \"claim_boundary\": {},\n",
50        render_claim_boundary_json()
51    ));
52    out.push_str(&format!(
53        "  \"scanner_limitations\": {},\n",
54        render_scanner_limitations_json()
55    ));
56    out.push_str("  \"inventory\": ");
57    out.push_str(&render_inventory_json(inventory, "  "));
58    out.push_str(",\n");
59}
60
61pub(crate) fn push_json_status_fields(out: &mut String, failed: bool) {
62    push_json_status_fields_with_status(
63        out,
64        if failed {
65            ARTIFACT_STATUS_FAILED
66        } else {
67            ARTIFACT_STATUS_PASSED
68        },
69        failed,
70    );
71}
72
73pub(crate) fn push_json_status_fields_with_status(out: &mut String, status: &str, failed: bool) {
74    out.push_str(&format!("  \"status\": \"{}\",\n", json_escape(status)));
75    out.push_str(&format!("  \"failed\": {},\n", bool_json(failed)));
76}
77
78pub(crate) fn push_json_receipt_run_metadata(out: &mut String, context: ReportContext<'_>) {
79    if let Some(mode) = context.mode {
80        out.push_str(&format!("  \"mode\": \"{}\",\n", json_escape(mode)));
81    }
82    if let Some(enforcement) = context.enforcement {
83        out.push_str(&format!(
84            "  \"enforcement\": \"{}\",\n",
85            json_escape(enforcement)
86        ));
87    }
88    if let Some(policy_config) = context.policy_config {
89        out.push_str(&format!(
90            "  \"policy_config\": \"{}\",\n",
91            json_escape(policy_config)
92        ));
93    }
94    if let Some(tool_version) = context.tool_version {
95        out.push_str(&format!(
96            "  \"tool_version\": \"{}\",\n",
97            json_escape(tool_version)
98        ));
99    }
100}
101
102pub(crate) fn push_json_source_context_properties(
103    out: &mut String,
104    inventory: InventoryContext<'_>,
105    indent: &str,
106) {
107    out.push_str(&format!("{indent}\"inventory\": "));
108    out.push_str(&render_inventory_json(inventory, indent));
109    out.push_str(",\n");
110    out.push_str(&format!(
111        "{indent}\"claim_boundary\": {},\n",
112        render_claim_boundary_json()
113    ));
114    out.push_str(&format!(
115        "{indent}\"scanner_limitations\": {}\n",
116        render_scanner_limitations_json()
117    ));
118}
119
120pub(crate) fn option_json(value: Option<&str>) -> String {
121    value
122        .map(|v| format!("\"{}\"", json_escape(v)))
123        .unwrap_or_else(|| "null".to_string())
124}
125
126pub(crate) fn bool_json(value: bool) -> &'static str {
127    if value { "true" } else { "false" }
128}
129
130pub(crate) fn option_u32_json(value: Option<u32>) -> String {
131    value
132        .map(|value| value.to_string())
133        .unwrap_or_else(|| "null".to_string())
134}
135
136pub(crate) fn option_usize_json(value: Option<usize>) -> String {
137    value
138        .map(|value| value.to_string())
139        .unwrap_or_else(|| "null".to_string())
140}
141
142pub(crate) fn render_match_outcome_json(outcome: &MatchOutcome, indent: &str) -> String {
143    let fields = MatchOutcomeJsonFields::new(outcome);
144    format!(
145        "{indent}  {{\n{indent}    \"status\": \"{}\",\n{indent}    \"allow_id\": {},\n{indent}    \"finding_index\": {},\n{indent}    \"score\": {},\n{indent}    \"message\": \"{}\"\n{indent}  }}",
146        fields.status, fields.allow_id, fields.finding_index, fields.score, fields.message
147    )
148}
149
150pub(crate) fn render_match_outcome_json_compact(outcome: &MatchOutcome) -> String {
151    let fields = MatchOutcomeJsonFields::new(outcome);
152    format!(
153        "{{\"status\": \"{}\", \"allow_id\": {}, \"finding_index\": {}, \"score\": {}, \"message\": \"{}\"}}",
154        fields.status, fields.allow_id, fields.finding_index, fields.score, fields.message
155    )
156}
157
158struct MatchOutcomeJsonFields {
159    status: &'static str,
160    allow_id: String,
161    finding_index: String,
162    score: u32,
163    message: String,
164}
165
166impl MatchOutcomeJsonFields {
167    fn new(outcome: &MatchOutcome) -> Self {
168        Self {
169            status: outcome.status.as_str(),
170            allow_id: option_json(outcome.allow_id.as_deref()),
171            finding_index: option_usize_json(outcome.finding_index),
172            score: outcome.score,
173            message: json_escape(&outcome.message),
174        }
175    }
176}
177
178pub(crate) fn json_string_array<T: AsRef<str>>(values: &[T]) -> String {
179    format!(
180        "[{}]",
181        values
182            .iter()
183            .map(|value| format!("\"{}\"", json_escape(value.as_ref())))
184            .collect::<Vec<_>>()
185            .join(", ")
186    )
187}
188
189pub fn render_claim_boundary_json() -> String {
190    json_string_array(CLAIM_BOUNDARY)
191}
192
193pub fn render_scanner_limitations_json() -> String {
194    json_string_array(SCANNER_LIMITATIONS)
195}
196
197pub fn render_inventory_json(context: InventoryContext<'_>, indent: &str) -> String {
198    let mut out = String::new();
199    out.push_str("{\n");
200    out.push_str(&format!(
201        "{indent}  \"scope\": \"{}\",\n",
202        json_escape(context.scope)
203    ));
204    out.push_str(&format!(
205        "{indent}  \"scanner\": \"{}\",\n",
206        json_escape(context.scanner)
207    ));
208    out.push_str(&format!(
209        "{indent}  \"source\": \"{}\"",
210        json_escape(context.source)
211    ));
212    if let Some(root) = context.root {
213        out.push_str(",\n");
214        out.push_str(&format!("{indent}  \"root\": \"{}\"", json_escape(root)));
215    }
216    if let Some(files) = context.files_scanned {
217        out.push_str(",\n");
218        out.push_str(&format!("{indent}  \"files_scanned\": {files}"));
219    }
220    out.push('\n');
221    out.push_str(&format!("{indent}}}"));
222    out
223}