1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(clippy::pattern_type_mismatch)]
//! Modify and Append effect detail compilation.
//!
//! Modify effects contain an array of operations (`add`, `addOrReplace`,
//! `remove`) each targeting a specific field/alias. Append effects contain
//! a `{ "field", "value" }` pair or an array of such pairs.
//!
//! Values within operations may be template expressions (`[concat(…)]`)
//! which are compiled rather than stored as literals.
use alloc::format;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use anyhow::{bail, Result};
use crate::languages::azure_policy::compiler::utils::json_value_to_runtime;
use crate::languages::azure_policy::ast::{JsonValue, ObjectEntry};
use crate::rvm::instructions::ArrayCreateParams;
use crate::rvm::Instruction;
use super::core::Compiler;
use super::effects::build_object_from_keys;
use super::expressions::check_json_depth;
use crate::Value;
impl Compiler {
// -- Modify details -----------------------------------------------------
/// Compile Modify effect details:
/// `{ "effect": "modify", "details": { "roleDefinitionIds": […], "operations": […] } }`
pub(super) fn compile_modify_details(
&mut self,
effect_name_reg: u8,
details: Option<&JsonValue>,
span: &crate::lexer::Span,
) -> Result<u8> {
// When details is absent or not an object, return the bare effect.
// Azure Policy accepts this — the effect is reported for compliance
// evaluation even when remediation details are missing. Erroring here
// would reject policies that the real engine considers valid.
let Some(JsonValue::Object(_, entries)) = details else {
return self.wrap_effect_result(effect_name_reg, None, span);
};
// Extract roleDefinitionIds and operations from details entries.
let mut role_ids_value: Option<&JsonValue> = None;
let mut operations: Option<&Vec<JsonValue>> = None;
for ObjectEntry { key, value, .. } in entries {
match key.to_lowercase().as_str() {
"roledefinitionids" => role_ids_value = Some(value),
"operations" => {
if let JsonValue::Array(_, ops) = value {
operations = Some(ops);
} else {
bail!(value
.span()
.error("Modify effect 'operations' must be an array"));
}
}
_ => {} // existenceCondition, conflictEffect, etc. — skip
}
}
// roleDefinitionIds is required for Modify effects (must be an array
// or a template expression that evaluates to one).
let Some(role_json) = role_ids_value else {
bail!(span.error("Modify effect requires 'roleDefinitionIds' in details"));
};
match role_json {
JsonValue::Array(_, _) => {}
JsonValue::Str(_, s) if crate::languages::azure_policy::parser::is_template_expr(s) => {
}
_ => bail!(role_json.span().error(
"Modify effect 'roleDefinitionIds' must be an array or template expression",
)),
}
let mut detail_keys: Vec<(u16, u8)> = Vec::new();
// roleDefinitionIds — compile as expression (may be parameterized).
{
let role_reg = self.compile_json_value(role_json, role_json.span())?;
let key_idx = self.add_literal_u16(Value::from("roleDefinitionIds"))?;
detail_keys.push((key_idx, role_reg));
}
let Some(ops) = operations else {
bail!(span.error("Modify effect requires 'operations' in details"));
};
if ops.is_empty() {
bail!(span.error("Modify effect 'operations' must not be empty"));
}
// operations — compile each operation into an object.
{
let mut op_regs = Vec::new();
for op_json in ops {
let op_reg = self.compile_modify_operation(op_json, span)?;
op_regs.push(op_reg);
}
let ops_dest = self.alloc_register()?;
let ops_params = ArrayCreateParams {
dest: ops_dest,
elements: op_regs,
};
let ops_params_index = self
.program
.instruction_data
.add_array_create_params(ops_params);
self.emit(
Instruction::ArrayCreate {
params_index: ops_params_index,
},
span,
);
let key_idx = self.add_literal_u16(Value::from("operations"))?;
detail_keys.push((key_idx, ops_dest));
}
let details_dest = build_object_from_keys(self, detail_keys, span)?;
self.wrap_effect_result(effect_name_reg, Some(details_dest), span)
}
/// Compile a single Modify operation into an object register.
///
/// Expects `{ "operation": "…", "field": "…", "value": …, "condition": "…" }`.
/// The `"value"` field may contain template expressions.
pub(super) fn compile_modify_operation(
&mut self,
op_json: &JsonValue,
span: &crate::lexer::Span,
) -> Result<u8> {
let JsonValue::Object(_, entries) = op_json else {
bail!(op_json.span().error("modify operation must be an object"));
};
let mut op_keys: Vec<(u16, u8)> = Vec::new();
let mut operation_name: Option<String> = None;
let mut has_field = false;
let mut has_value = false;
for ObjectEntry { key, value, .. } in entries {
match key.to_lowercase().as_str() {
"operation" => {
let JsonValue::Str(_, op_str) = value else {
bail!(value
.span()
.error("modify operation 'operation' must be a string"));
};
let canonical_op = match op_str.to_lowercase().as_str() {
"add" => "add",
"addorreplace" => "addOrReplace",
"remove" => "remove",
other => bail!(value
.span()
.error(&format!("unsupported modify operation: {other}"))),
};
operation_name = Some(canonical_op.into());
let val = Value::from(canonical_op);
let reg = self.load_literal(val, value.span())?;
let key_idx = self.add_literal_u16(Value::from("operation"))?;
op_keys.push((key_idx, reg));
}
"field" => {
if let JsonValue::Str(_, field_path) = value {
self.check_modify_field_alias(field_path, value.span())?;
let val = Value::from(field_path.clone());
let reg = self.load_literal(val, value.span())?;
let key_idx = self.add_literal_u16(Value::from("field"))?;
op_keys.push((key_idx, reg));
has_field = true;
} else {
bail!(value
.span()
.error("modify operation 'field' must be a string"));
}
}
"value" => {
// Value may contain template expressions.
let reg = self.compile_value_or_expr_from_json(value, value.span())?;
let key_idx = self.add_literal_u16(Value::from("value"))?;
op_keys.push((key_idx, reg));
has_value = true;
}
"condition" => {
// The `condition` field is NOT evaluated during policy
// rule evaluation. It is a remediation instruction:
// when Azure's remediation engine applies the modify
// effect it evaluates this condition against the
// resource to decide whether to execute the specific
// operation. We preserve it verbatim (as a literal
// string) so the consumer receives the original
// expression, e.g. `"[equals(field('tags.env'), '')]"`.
check_json_depth(value, 0).map_err(|_| {
value
.span()
.error("JSON value nesting exceeds maximum depth")
})?;
let runtime_value = json_value_to_runtime(value)?;
let reg = self.load_literal(runtime_value, value.span())?;
let key_idx = self.add_literal_u16(Value::from("condition"))?;
op_keys.push((key_idx, reg));
}
_ => {} // Unknown fields — skip
}
}
let Some(op_name) = operation_name else {
bail!(op_json
.span()
.error("modify operation must include 'operation'"));
};
if !has_field {
bail!(op_json
.span()
.error("modify operation must include 'field'"));
}
// 'add' and 'addOrReplace' require a value; 'remove' does not.
if !has_value && op_name != "remove" {
bail!(op_json.span().error(&format!(
"modify operation '{op_name}' must include 'value'"
)));
}
build_object_from_keys(self, op_keys, span)
}
// -- Append details -----------------------------------------------------
/// Compile an Append effect's details.
///
/// Accepts both array form `[ { "field": …, "value": … }, … ]` and
/// single-object form `{ "field": …, "value": … }`.
pub(super) fn compile_append_details(
&mut self,
effect_name_reg: u8,
details: Option<&JsonValue>,
span: &crate::lexer::Span,
) -> Result<u8> {
let Some(details) = details else {
// When details is absent, return the bare effect. Same rationale
// as modify: Azure Policy accepts this for compliance evaluation.
return self.wrap_effect_result(effect_name_reg, None, span);
};
let item_regs = match details {
JsonValue::Array(_, arr) => {
if arr.is_empty() {
bail!(span.error("Append effect requires non-empty 'details' array"));
}
let mut regs = Vec::new();
for item in arr {
regs.push(self.compile_append_item(item, span)?);
}
regs
}
JsonValue::Object(_, _) => {
vec![self.compile_append_item(details, span)?]
}
_ => {
bail!(span.error("Append effect 'details' must be an array or object"));
}
};
// Create the details array.
let details_dest = self.alloc_register()?;
let params = ArrayCreateParams {
dest: details_dest,
elements: item_regs,
};
let params_index = self
.program
.instruction_data
.add_array_create_params(params);
self.emit(Instruction::ArrayCreate { params_index }, span);
self.wrap_effect_result(effect_name_reg, Some(details_dest), span)
}
/// Compile a single Append item `{ "field": "…", "value": … }` into an
/// object register.
pub(super) fn compile_append_item(
&mut self,
item_json: &JsonValue,
span: &crate::lexer::Span,
) -> Result<u8> {
let JsonValue::Object(_, entries) = item_json else {
bail!(item_json
.span()
.error("append details item must be an object"));
};
let mut field_reg: Option<u8> = None;
let mut value_reg: Option<u8> = None;
for ObjectEntry { key, value, .. } in entries {
match key.to_lowercase().as_str() {
"field" => {
let JsonValue::Str(_, field_path) = value else {
bail!(value
.span()
.error("append details item 'field' must be a string"));
};
let val = Value::from(field_path.clone());
field_reg = Some(self.load_literal(val, value.span())?);
}
"value" => {
value_reg = Some(self.compile_value_or_expr_from_json(value, value.span())?);
}
_ => {}
}
}
let Some(field_reg) = field_reg else {
bail!(item_json
.span()
.error("append details item must include 'field'"));
};
let Some(value_reg) = value_reg else {
bail!(item_json
.span()
.error("append details item must include 'value'"));
};
let item_keys = vec![
(self.add_literal_u16(Value::from("field"))?, field_reg),
(self.add_literal_u16(Value::from("value"))?, value_reg),
];
build_object_from_keys(self, item_keys, span)
}
}