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, 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    out.push_str(&format!(
63        "  \"status\": \"{}\",\n",
64        if failed {
65            ARTIFACT_STATUS_FAILED
66        } else {
67            ARTIFACT_STATUS_PASSED
68        }
69    ));
70    out.push_str(&format!("  \"failed\": {},\n", bool_json(failed)));
71}
72
73pub(crate) fn push_json_source_context_properties(
74    out: &mut String,
75    inventory: InventoryContext<'_>,
76    indent: &str,
77) {
78    out.push_str(&format!("{indent}\"inventory\": "));
79    out.push_str(&render_inventory_json(inventory, indent));
80    out.push_str(",\n");
81    out.push_str(&format!(
82        "{indent}\"claim_boundary\": {},\n",
83        render_claim_boundary_json()
84    ));
85    out.push_str(&format!(
86        "{indent}\"scanner_limitations\": {}\n",
87        render_scanner_limitations_json()
88    ));
89}
90
91pub(crate) fn option_json(value: Option<&str>) -> String {
92    value
93        .map(|v| format!("\"{}\"", json_escape(v)))
94        .unwrap_or_else(|| "null".to_string())
95}
96
97pub(crate) fn bool_json(value: bool) -> &'static str {
98    if value { "true" } else { "false" }
99}
100
101pub(crate) fn option_u32_json(value: Option<u32>) -> String {
102    value
103        .map(|value| value.to_string())
104        .unwrap_or_else(|| "null".to_string())
105}
106
107pub(crate) fn option_usize_json(value: Option<usize>) -> String {
108    value
109        .map(|value| value.to_string())
110        .unwrap_or_else(|| "null".to_string())
111}
112
113pub(crate) fn render_match_outcome_json(outcome: &MatchOutcome, indent: &str) -> String {
114    let fields = MatchOutcomeJsonFields::new(outcome);
115    format!(
116        "{indent}  {{\n{indent}    \"status\": \"{}\",\n{indent}    \"allow_id\": {},\n{indent}    \"finding_index\": {},\n{indent}    \"score\": {},\n{indent}    \"message\": \"{}\"\n{indent}  }}",
117        fields.status, fields.allow_id, fields.finding_index, fields.score, fields.message
118    )
119}
120
121pub(crate) fn render_match_outcome_json_compact(outcome: &MatchOutcome) -> String {
122    let fields = MatchOutcomeJsonFields::new(outcome);
123    format!(
124        "{{\"status\": \"{}\", \"allow_id\": {}, \"finding_index\": {}, \"score\": {}, \"message\": \"{}\"}}",
125        fields.status, fields.allow_id, fields.finding_index, fields.score, fields.message
126    )
127}
128
129struct MatchOutcomeJsonFields {
130    status: &'static str,
131    allow_id: String,
132    finding_index: String,
133    score: u32,
134    message: String,
135}
136
137impl MatchOutcomeJsonFields {
138    fn new(outcome: &MatchOutcome) -> Self {
139        Self {
140            status: outcome.status.as_str(),
141            allow_id: option_json(outcome.allow_id.as_deref()),
142            finding_index: option_usize_json(outcome.finding_index),
143            score: outcome.score,
144            message: json_escape(&outcome.message),
145        }
146    }
147}
148
149pub(crate) fn json_string_array<T: AsRef<str>>(values: &[T]) -> String {
150    format!(
151        "[{}]",
152        values
153            .iter()
154            .map(|value| format!("\"{}\"", json_escape(value.as_ref())))
155            .collect::<Vec<_>>()
156            .join(", ")
157    )
158}
159
160pub fn render_claim_boundary_json() -> String {
161    json_string_array(CLAIM_BOUNDARY)
162}
163
164pub fn render_scanner_limitations_json() -> String {
165    json_string_array(SCANNER_LIMITATIONS)
166}
167
168pub fn render_inventory_json(context: InventoryContext<'_>, indent: &str) -> String {
169    let mut out = String::new();
170    out.push_str("{\n");
171    out.push_str(&format!(
172        "{indent}  \"scope\": \"{}\",\n",
173        json_escape(context.scope)
174    ));
175    out.push_str(&format!(
176        "{indent}  \"scanner\": \"{}\",\n",
177        json_escape(context.scanner)
178    ));
179    out.push_str(&format!(
180        "{indent}  \"source\": \"{}\"",
181        json_escape(context.source)
182    ));
183    if let Some(root) = context.root {
184        out.push_str(",\n");
185        out.push_str(&format!("{indent}  \"root\": \"{}\"", json_escape(root)));
186    }
187    if let Some(files) = context.files_scanned {
188        out.push_str(",\n");
189        out.push_str(&format!("{indent}  \"files_scanned\": {files}"));
190    }
191    out.push('\n');
192    out.push_str(&format!("{indent}}}"));
193    out
194}