use crate::eval_context::{current_model, current_region, is_builtin_rule_suppressed};
use data_source::types::{ArtifactCountEntry, CodepipelineArtifactCounts, GetattData, SchemaMetadataCatalog};
use regex::Regex;
use regorus::Value;
use std::cell::RefCell;
use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
use std::sync::Arc;
use template_model::SemanticModel;
use template_model::coercion::{
coerce_port_to_string, coerce_to_bool, coerce_to_integer, coerce_to_number, coerce_to_string, type_compatible,
};
use template_model::consts::{
FIELD_CONDITION, FIELD_DEPENDS_ON, FIELD_KIND, FIELD_PROPERTIES, FIELD_RESOURCE_TYPE, FIELD_SOURCE,
FIELD_SOURCE_PATH, FIELD_TARGET, FN_IF, effective_deployed_resource_type,
};
use template_model::dynamodb::analyze_dynamodb_table_scenarios;
use template_model::fargate::{cpu_is_offered, task_size_is_offered};
use template_model::iam_policy::{
inline_identity_policy_document_paths, policy_has_allow_not_action_scenarios, validate_identity_policy_scenarios,
};
use template_model::region_enums;
use template_model::resolved_value::json_contains_markers;
use template_model::resolver::{MapEntry, RefKind, ResolvedValue};
use template_model::{MARKER_DYNAMIC, MARKER_PARAM_TYPE, MARKER_REF};
use template_model::{SourceSpan, UNKNOWN_SPAN, primary_identifier_conflict_message, render_value, render_value_list};
use validation_engine::DIAGNOSTIC_SOURCE_PATH_FIELD;
pub(crate) fn serde_json_to_rego_value(v: &serde_json::Value) -> Value {
json_to_value(v)
}
pub(crate) fn register_all(
rego: &mut regorus::Engine,
schema_metadata: Arc<SchemaMetadataCatalog>,
getatt: Arc<GetattData>,
) -> anyhow::Result<()> {
register_cfn_rule_active(rego)?;
register_regex_match(rego)?;
register_resolve(rego);
register_resolve_preserving_conditionals(rego);
register_resolve_all(rego);
register_is_dynamic(rego);
register_is_from_parameter(rego);
register_is_from_intrinsic(rego);
register_lifecycle_attribute_status(rego);
register_lifecycle_policy_scenarios(rego);
register_value_identity(rego);
register_referenced_resource_or_value_identity(rego);
register_follow_ref(rego);
register_authored_form(rego);
register_resources_of_type(rego);
register_effective_resource_type(rego);
register_duplicate_subnet_associations(rego);
register_subnets_outside_vpc_findings(rego);
register_overlapping_subnet_findings(rego);
register_hardcoded_azs(rego);
register_ref_targets(rego);
register_ref_sources(rego);
register_depends_on(rego);
register_conditions_compatible(rego);
register_condition_implies(rego);
register_conjunction_implies(rego);
register_resource_condition(rego);
register_primary_identifier_conflicts(rego);
register_has_property(rego);
register_property_can_be_absent(rego);
register_param_allowed_values(rego);
register_param_type(rego);
register_mapping_value(rego);
register_has_transform(rego);
register_make_diag(rego);
register_make_diag_at(rego);
register_make_diag_at_source(rego);
register_make_diag_full(rego);
register_make_diag_related(rego);
register_make_diag_conditional(rego);
register_resolve_scenarios(rego);
register_has_unresolved_scenario(rego);
register_scenario_source_path(rego);
register_properties_scenarios(rego);
register_dynamodb_scenario_analysis(rego);
register_is_satisfiable(rego);
register_get_resource(rego);
register_resolve_ref_target(rego);
register_matching_property_paths(rego);
register_flatten_list(rego);
register_pipeline_artifacts(rego);
register_pipeline_artifact_count_issues(rego)?;
register_resolve_type(rego);
register_schema_properties(rego, schema_metadata.clone());
register_schema_required(rego, schema_metadata.clone());
register_schema_type(rego, schema_metadata.clone());
register_schema_enum(rego, schema_metadata.clone());
register_attribute_type(rego, schema_metadata.clone());
register_getatt_return_type(rego, getatt);
register_edges_from(rego);
register_edges_to(rego);
register_arn_matches(rego);
register_arn_matches_format(rego);
register_ip_overlaps(rego);
register_ip_subnet_of(rego);
register_is_valid_cidr_strict(rego);
register_ensure_list(rego);
register_input_region(rego);
register_is_valid_region(rego);
register_region_flat_invalid(rego);
register_region_conditional_invalid(rego);
register_render_list(rego);
register_render_value(rego);
register_coerce_to_number(rego);
register_coerce_to_integer(rego);
register_coerce_to_string(rego);
register_coerce_port_to_string(rego);
register_coerce_to_bool(rego);
register_fargate_cpu_is_offered(rego);
register_fargate_task_size_is_offered(rego);
register_cfn_type_compatible(rego);
register_estimated_string_length_bounds(rego);
register_schema_string_length(rego, schema_metadata.clone());
register_schema_requires_unique_items(rego, schema_metadata);
register_unreachable_if_branches(rego);
register_iam_identity_policy_findings(rego);
register_iam_inline_policy_document_paths(rego);
register_iam_policy_has_allow_not_action(rego);
Ok(())
}
const REGEX_CACHE_CAPACITY: usize = 256;
thread_local! {
static REGEX_CACHE: RefCell<RegexCache> = RefCell::new(RegexCache::new());
}
struct RegexCache {
compiled: HashMap<Box<str>, Option<Regex>>,
insertion_order: VecDeque<Box<str>>,
}
impl RegexCache {
fn new() -> Self {
Self { compiled: HashMap::new(), insertion_order: VecDeque::new() }
}
fn is_match(&mut self, pattern: &str, haystack: &str) -> Option<bool> {
if let Some(cached) = self.compiled.get(pattern) {
return cached.as_ref().map(|regex| regex.is_match(haystack));
}
let compiled = Regex::new(pattern).ok();
let outcome = compiled.as_ref().map(|regex| regex.is_match(haystack));
self.remember(pattern, compiled);
outcome
}
fn remember(&mut self, pattern: &str, compiled: Option<Regex>) {
if self.insertion_order.len() >= REGEX_CACHE_CAPACITY
&& let Some(evicted) = self.insertion_order.pop_front()
{
self.compiled.remove(&evicted);
}
let key: Box<str> = Box::from(pattern);
self.insertion_order.push_back(key.clone());
self.compiled.insert(key, compiled);
}
}
fn register_regex_match(rego: &mut regorus::Engine) -> anyhow::Result<()> {
rego.add_extension(
"regex.match".into(),
2,
Box::new(|params: Vec<Value>| {
let (Ok(pattern), Ok(value)) = (params[0].as_string(), params[1].as_string()) else {
return Ok(Value::Undefined);
};
let matched = REGEX_CACHE.with(|cache| cache.borrow_mut().is_match(pattern.as_ref(), value.as_ref()));
Ok(matched.map_or(Value::Undefined, Value::from))
}),
)
}
fn register_cfn_rule_active(rego: &mut regorus::Engine) -> anyhow::Result<()> {
rego.add_extension(
"cfn_rule_active".into(),
1,
Box::new(|params: Vec<Value>| {
let Ok(rule_id) = params[0].as_string() else {
return Ok(Value::Bool(true));
};
Ok(Value::Bool(!is_builtin_rule_suppressed(rule_id.as_ref())))
}),
)
}
fn resolved_to_rego(rv: &ResolvedValue) -> Value {
match rv {
ResolvedValue::Concrete { value: v } => json_to_value(v),
ResolvedValue::List { items } => {
let vals: Vec<Value> = items.iter().map(resolved_to_rego).collect();
Value::from(vals)
}
ResolvedValue::Map { entries } => {
let mut map = serde_json::Map::new();
for MapEntry { key: k, value: v } in entries {
map.insert(k.clone(), resolved_value_to_json_static(v));
}
json_to_value(&serde_json::Value::Object(map))
}
ResolvedValue::Enum { variants: vals } => {
for v in vals {
if let ResolvedValue::Concrete { value: c } = v {
return json_to_value(c);
}
}
Value::Undefined
}
ResolvedValue::Conditional { if_true: t, .. } => resolved_to_rego(t),
ResolvedValue::Reference { target, .. } => json_to_value(&serde_json::json!({MARKER_REF: target})),
ResolvedValue::Dynamic { .. } | ResolvedValue::TypedDynamic { .. } => Value::Undefined,
}
}
fn resolved_all_to_rego(rv: &ResolvedValue) -> Vec<Value> {
match rv {
ResolvedValue::Concrete { value: v } => vec![json_to_value(v)],
ResolvedValue::List { items } => {
let vals: Vec<Value> = items.iter().map(resolved_to_rego).collect();
vec![Value::from(vals)]
}
ResolvedValue::Map { entries } => {
let mut map = serde_json::Map::new();
for MapEntry { key: k, value: v } in entries {
map.insert(k.clone(), resolved_value_to_json_static(v));
}
vec![json_to_value(&serde_json::Value::Object(map))]
}
ResolvedValue::Enum { variants: vals } => vals.iter().flat_map(resolved_all_to_rego).collect(),
ResolvedValue::Conditional { if_true: t, if_false: f, .. } => {
let mut r = resolved_all_to_rego(t);
r.extend(resolved_all_to_rego(f));
r
}
ResolvedValue::Reference { .. } | ResolvedValue::Dynamic { .. } | ResolvedValue::TypedDynamic { .. } => vec![],
}
}
fn json_to_value(v: &serde_json::Value) -> Value {
match v {
serde_json::Value::Null => Value::Null,
serde_json::Value::Bool(b) => Value::from(*b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Value::from(i)
} else if let Some(f) = n.as_f64() {
Value::from(f)
} else {
Value::Undefined
}
}
serde_json::Value::String(s) => Value::from(s.as_str()),
serde_json::Value::Array(arr) => Value::from(arr.iter().map(json_to_value).collect::<Vec<_>>()),
serde_json::Value::Object(map) => {
let mut obj = regorus::Value::new_object();
for (k, v) in map {
obj.as_object_mut().unwrap().insert(Value::from(k.as_str()), json_to_value(v));
}
obj
}
}
}
fn resolved_value_to_json_static(val: &ResolvedValue) -> serde_json::Value {
match val {
ResolvedValue::Concrete { value: v } => v.0.clone(),
ResolvedValue::List { items } => {
serde_json::Value::Array(items.iter().map(resolved_value_to_json_static).collect())
}
ResolvedValue::Map { entries } => {
let mut map = serde_json::Map::new();
for MapEntry { key: k, value: v } in entries {
map.insert(k.clone(), resolved_value_to_json_static(v));
}
serde_json::Value::Object(map)
}
ResolvedValue::Enum { variants: vals } => {
for v in vals {
if let ResolvedValue::Concrete { value: c } = v {
return c.0.clone();
}
}
serde_json::Value::Null
}
ResolvedValue::Conditional { if_true: t, .. } => resolved_value_to_json_static(t),
ResolvedValue::Reference { target, .. } => {
serde_json::json!({MARKER_REF: target})
}
ResolvedValue::Dynamic { reason } => {
serde_json::json!({MARKER_DYNAMIC: reason})
}
ResolvedValue::TypedDynamic { reason, param_type } => {
serde_json::json!({MARKER_DYNAMIC: reason, MARKER_PARAM_TYPE: param_type})
}
}
}
fn resolved_to_json_preserving_conditionals(val: &ResolvedValue) -> serde_json::Value {
match val {
ResolvedValue::List { items } => {
serde_json::Value::Array(items.iter().map(resolved_to_json_preserving_conditionals).collect())
}
ResolvedValue::Map { entries } => {
let mut map = serde_json::Map::new();
for MapEntry { key: k, value: v } in entries {
map.insert(k.clone(), resolved_to_json_preserving_conditionals(v));
}
serde_json::Value::Object(map)
}
ResolvedValue::Conditional { condition, if_true, if_false } => serde_json::json!({
FN_IF: [
condition.clone(),
resolved_to_json_preserving_conditionals(if_true),
resolved_to_json_preserving_conditionals(if_false),
]
}),
other => resolved_value_to_json_static(other),
}
}
fn contains_dynamic(rv: &ResolvedValue) -> bool {
match rv {
ResolvedValue::Dynamic { .. } | ResolvedValue::TypedDynamic { .. } => true,
ResolvedValue::List { items } => items.iter().any(contains_dynamic),
ResolvedValue::Map { entries } => entries.iter().any(|e| contains_dynamic(&e.value)),
ResolvedValue::Enum { variants: vals } => vals.iter().any(contains_dynamic),
ResolvedValue::Conditional { if_true: t, if_false: f, .. } => contains_dynamic(t) || contains_dynamic(f),
ResolvedValue::Reference { .. } => true,
ResolvedValue::Concrete { value: v } => json_contains_markers(v),
}
}
fn register_resolve(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"resolve".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let path = params[1].as_string()?;
if let Some(val) = model.resolve_deep(rid, path) {
return Ok(resolved_to_rego(&val));
}
if let Some(val) = model.resolve(rid, path) {
return Ok(resolved_to_rego(val));
}
let scenarios = model.resolve_scenarios_json_shared(rid, path);
if let Some((first, _)) = scenarios.first() {
return Ok(serde_json_to_rego_value(first));
}
Ok(Value::Undefined)
}),
);
}
fn register_resolve_preserving_conditionals(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"resolve_preserving_conditionals".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let path = params[1].as_string()?;
let resolved = model.resolve_deep(rid, path).or_else(|| model.resolve(rid, path).cloned());
if let Some(val) = resolved {
return Ok(serde_json_to_rego_value(&resolved_to_json_preserving_conditionals(&val)));
}
let scenarios = model.resolve_scenarios_json_shared(rid, path);
if let Some((first, _)) = scenarios.first() {
return Ok(serde_json_to_rego_value(first));
}
Ok(Value::Undefined)
}),
);
}
fn register_resolve_all(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"resolve_all".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let path = params[1].as_string()?;
if let Some(val) = model.resolve_deep(rid, path) {
return Ok(Value::from(resolved_all_to_rego(&val)));
}
if let Some(val) = model.resolve(rid, path) {
return Ok(Value::from(resolved_all_to_rego(val)));
}
let scenarios = model.resolve_scenarios_json_shared(rid, path);
if scenarios.is_empty() {
return Ok(Value::from(Vec::<Value>::new()));
}
let vals: Vec<Value> = scenarios.iter().map(|(v, _)| serde_json_to_rego_value(v)).collect();
Ok(Value::from(vals))
}),
);
}
fn register_is_dynamic(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"is_dynamic".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let path = params[1].as_string()?;
if let Some(val) = model.resolve_deep(rid, path) {
return Ok(Value::from(contains_dynamic(&val)));
}
if let Some(val) = model.resolve(rid, path) {
return Ok(Value::from(contains_dynamic(val)));
}
Ok(Value::from(false))
}),
);
}
fn register_is_from_parameter(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"is_from_parameter".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let path = params[1].as_string()?;
Ok(Value::from(model.is_from_parameter(rid, path)))
}),
);
}
fn register_is_from_intrinsic(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"is_from_intrinsic".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let path = params[1].as_string()?;
Ok(Value::from(model.is_from_intrinsic(rid, path)))
}),
);
}
fn register_lifecycle_attribute_status(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"lifecycle_attribute_status".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let resource_id = params[0].as_string()?;
let attribute = params[1].as_string()?;
let status = model.lifecycle_attribute_status(resource_id, attribute);
Ok(json_to_value(&serde_json::json!({
"mayBePresent": status.may_be_present,
"invalidValue": status.invalid_value.unwrap_or_default(),
})))
}),
);
}
fn register_lifecycle_policy_scenarios(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"lifecycle_policy_scenarios".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let resource_id = params[0].as_string()?;
let attribute = params[1].as_string()?;
let scenarios = model.lifecycle_policy_scenarios(resource_id, attribute);
let json_array: Vec<serde_json::Value> = scenarios.into_iter().map(|(val, _conds)| val).collect();
Ok(json_to_value(&serde_json::json!(json_array)))
}),
);
}
fn register_value_identity(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"value_identity".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let path = params[1].as_string()?;
match model.value_identity(rid, path) {
Some(identity) => Ok(Value::from(identity)),
None => Ok(Value::Undefined),
}
}),
);
}
fn register_referenced_resource_or_value_identity(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"referenced_resource_or_value_identity".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let path = params[1].as_string()?;
match model.referenced_resource_or_value_identity(rid, path) {
Some(identity) => Ok(Value::from(identity)),
None => Ok(Value::Undefined),
}
}),
);
}
fn register_resolve_scenarios(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"resolve_scenarios".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let path = params[1].as_string()?;
if path.contains("{}") {
let arr_path = path.split(".{}").next().unwrap_or(path);
let suffix = path.split_once("{}").map(|x| x.1).unwrap_or("");
let arr_len = match model.resolve_deep(rid, arr_path) {
Some(ResolvedValue::List { items }) => items.len(),
Some(ResolvedValue::Concrete { value: v }) if v.as_array().is_some() => v.as_array().unwrap().len(),
_ => 0,
};
if arr_len > 0 {
let mut results: Vec<Value> = Vec::new();
for i in 0..arr_len {
let idx_path = format!("{}.{}{}", arr_path, i, suffix);
let scenarios = model.resolve_scenarios_json_shared(rid, &idx_path);
for (v_json, conds) in scenarios.iter() {
let mut conds_map = serde_json::Map::new();
for (k, b) in conds {
conds_map.insert(k.clone(), serde_json::Value::Bool(*b));
}
if let Ok(v) = Value::from_json_str(
&serde_json::json!({"value": v_json, "conditions": conds_map, "path": idx_path})
.to_string(),
) {
results.push(v);
}
}
}
return Ok(Value::from(results));
}
}
let scenarios = model.resolve_scenarios_json_shared(rid, path);
let results: Vec<Value> = scenarios
.iter()
.filter_map(|(v_json, conds)| {
let mut conds_map = serde_json::Map::new();
for (k, b) in conds {
conds_map.insert(k.clone(), serde_json::Value::Bool(*b));
}
Value::from_json_str(&serde_json::json!({"value": v_json, "conditions": conds_map}).to_string())
.ok()
})
.collect();
Ok(Value::from(results))
}),
);
}
fn register_has_unresolved_scenario(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"has_unresolved_scenario".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::from(true));
};
let resource_id = params[0].as_string()?;
let path = params[1].as_string()?;
Ok(Value::from(model.has_unresolved_scenario(resource_id.as_ref(), path.as_ref())))
}),
);
}
fn condition_assumptions(value: &Value) -> Vec<(String, bool)> {
let Ok(entries) = value.as_object() else {
return Vec::new();
};
entries
.iter()
.filter_map(|(name, truth)| match (name.as_string(), truth.as_bool()) {
(Ok(name), Ok(truth)) => Some((name.to_string(), *truth)),
_ => None,
})
.collect()
}
fn register_scenario_source_path(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"scenario_source_path".into(),
3,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let resource_id = params[0].as_string()?;
let effective_path = params[1].as_string()?;
let conditions: HashMap<String, bool> = condition_assumptions(¶ms[2]).into_iter().collect();
let source_path = model
.scenario_source_path(resource_id.as_ref(), effective_path.as_ref(), &conditions)
.unwrap_or_else(|| effective_path.to_string());
Ok(Value::from(source_path))
}),
);
}
fn project_selected_properties(
properties: &ResolvedValue,
selected_fields: &HashSet<String>,
) -> serde_json::Map<String, serde_json::Value> {
let mut projected = serde_json::Map::new();
match properties {
ResolvedValue::Concrete { value } => {
let Some(properties) = value.as_object() else {
return projected;
};
for (name, value) in properties {
if selected_fields.contains(name) && !value.is_null() {
projected.insert(name.clone(), value.clone());
}
}
}
ResolvedValue::Map { entries } => {
for MapEntry { key, value } in entries {
if selected_fields.contains(key) {
let value = resolved_value_to_json_static(value);
if !value.is_null() {
projected.insert(key.clone(), value);
}
}
}
}
_ => {}
}
projected
}
fn register_properties_scenarios(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"properties_scenarios".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let requested_fields = params[1].as_array()?;
let mut selected_fields = HashSet::with_capacity(requested_fields.len());
for field in requested_fields.iter() {
selected_fields.insert(field.as_string()?.to_string());
}
let results: Vec<Value> = model
.resolve_properties_scenarios_shared(rid.as_ref())
.iter()
.map(|(properties, conditions)| {
let properties = project_selected_properties(properties, &selected_fields);
json_to_value(&serde_json::json!({
"properties": properties,
"conditions": conditions,
}))
})
.collect();
Ok(Value::from(results))
}),
);
}
fn register_dynamodb_scenario_analysis(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"dynamodb_scenario_analysis".into(),
1,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let resource_id = params[0].as_string()?;
let analysis = analyze_dynamodb_table_scenarios(&model, resource_id.as_ref());
let attribute_mismatches: Vec<_> = analysis
.attribute_mismatches
.into_iter()
.map(|mismatch| {
serde_json::json!({
"missing": mismatch.missing,
"unused": mismatch.unused,
})
})
.collect();
Ok(json_to_value(&serde_json::json!({
"attribute_mismatches": attribute_mismatches,
"explicit_provisioned_missing_throughput": analysis.explicit_provisioned_missing_throughput,
"default_provisioned_missing_throughput": analysis.default_provisioned_missing_throughput,
})))
}),
);
}
fn register_is_satisfiable(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"is_satisfiable".into(),
1,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let assumptions = condition_assumptions(¶ms[0]);
if assumptions.is_empty() {
return Ok(Value::from(true));
}
Ok(Value::from(model.conditions.is_satisfiable(&assumptions)))
}),
);
}
fn register_follow_ref(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"follow_ref".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let path = params[1].as_string()?;
Ok(model.follow_ref(rid, path).map(Value::from).unwrap_or(Value::Undefined))
}),
);
}
fn register_authored_form(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"authored_form".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let path = params[1].as_string()?;
let form = match model.resolve(rid.as_ref(), path.as_ref()) {
Some(ResolvedValue::Reference { target, kind }) => authored_ref_form(target, kind),
Some(ResolvedValue::Concrete { value: v }) => Some(v.0.clone()),
_ => {
let prop_path = path.strip_prefix("Properties.").unwrap_or(path.as_ref());
let qualified = format!("Properties.{}", prop_path);
model
.graph
.outgoing(rid.as_ref())
.into_iter()
.find(|e| {
e.source_path == path.as_ref() || e.source_path == prop_path || e.source_path == qualified
})
.and_then(|e| authored_ref_form(&e.target, &e.kind))
}
};
match form {
Some(v) => Ok(serde_json_to_rego_value(&v)),
None => Ok(Value::Undefined),
}
}),
);
}
fn authored_ref_form(target: &str, kind: &RefKind) -> Option<serde_json::Value> {
match kind {
RefKind::Ref => Some(serde_json::json!({ "Ref": target })),
RefKind::GetAtt { attr } => Some(serde_json::json!({ "Fn::GetAtt": [target, attr] })),
_ => None,
}
}
fn register_effective_resource_type(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"effective_resource_type".into(),
1,
Box::new(|params: Vec<Value>| {
let resource_type = params[0].as_string()?;
Ok(Value::from(effective_deployed_resource_type(resource_type.as_ref())))
}),
);
}
fn register_duplicate_subnet_associations(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"duplicate_subnet_route_table_associations".into(),
0,
Box::new(move |_params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::from(Vec::<Value>::new()));
};
let findings: Vec<Value> = template_model::route_table::duplicate_subnet_associations(&model)
.into_iter()
.map(|finding| {
json_to_value(&serde_json::json!({
"resourceId": finding.resource_id,
"message": finding.message,
}))
})
.collect();
Ok(Value::from(findings))
}),
);
}
fn register_subnets_outside_vpc_findings(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"subnets_outside_vpc_findings".into(),
0,
Box::new(move |_params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::from(Vec::<Value>::new()));
};
let findings: Vec<Value> = template_model::vpc_cidr::subnets_outside_vpc(&model)
.into_iter()
.map(|finding| {
json_to_value(&serde_json::json!({
"subnetId": finding.subnet_id,
"message": finding.message,
}))
})
.collect();
Ok(Value::from(findings))
}),
);
}
fn register_overlapping_subnet_findings(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"overlapping_subnet_findings".into(),
0,
Box::new(move |_params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::from(Vec::<Value>::new()));
};
let findings: Vec<Value> = template_model::vpc_cidr::overlapping_subnets(&model)
.into_iter()
.map(|finding| {
json_to_value(&serde_json::json!({
"subnetId": finding.subnet_id,
"message": finding.message,
"earlierSubnetId": finding.earlier_subnet_id,
"earlierSubnetMessage": finding.earlier_subnet_message,
}))
})
.collect();
Ok(Value::from(findings))
}),
);
}
fn register_resources_of_type(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"resources_of_type".into(),
1,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let type_name = params[0].as_string()?;
let ids = model.resources_of_type(type_name);
let set: Vec<Value> = ids.iter().map(|s: &String| Value::from(s.as_str())).collect();
Ok(Value::from(set))
}),
);
}
fn register_hardcoded_azs(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"hardcoded_azs".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let resource_id = params[0].as_string()?;
let resource_type = params[1].as_string()?;
let found: Vec<Value> =
template_model::hardcoded_az::find(&model, resource_id.as_ref(), resource_type.as_ref())
.into_iter()
.map(|az| {
Value::from(std::collections::BTreeMap::from([
(Value::from("path"), Value::from(az.path)),
(Value::from("zone"), Value::from(az.zone)),
]))
})
.collect();
Ok(Value::from(found))
}),
);
}
fn register_ref_targets(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"ref_targets".into(),
1,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let targets: Vec<Value> = model.graph.ref_targets(rid).into_iter().map(Value::from).collect();
Ok(Value::from(targets))
}),
);
}
fn register_ref_sources(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"ref_sources".into(),
1,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let sources: Vec<Value> = model.graph.ref_sources(rid).into_iter().map(Value::from).collect();
Ok(Value::from(sources))
}),
);
}
fn register_depends_on(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"depends_on".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let source_id = params[0].as_string()?;
let target_id = params[1].as_string()?;
Ok(Value::from(model.graph.depends_on(source_id, target_id)))
}),
);
}
fn register_conditions_compatible(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"conditions_compatible".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let resource_a = params[0].as_string()?;
let resource_b = params[1].as_string()?;
let cond_a = model.resources.get(resource_a.as_ref()).and_then(|r| r.condition.as_deref());
let cond_b = model.resources.get(resource_b.as_ref()).and_then(|r| r.condition.as_deref());
Ok(Value::from(model.conditions.resources_compatible(cond_a, cond_b)))
}),
);
}
fn register_condition_implies(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"condition_implies".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
if params[0] == Value::Null {
return Ok(Value::from(false));
}
if params[1] == Value::Null {
return Ok(Value::from(true));
}
let antecedent = params[0].as_string()?;
let consequent = params[1].as_string()?;
Ok(Value::from(model.conditions.condition_implies(antecedent, consequent)))
}),
);
}
fn register_conjunction_implies(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"conjunction_implies".into(),
3,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
if params[2] == Value::Null {
return Ok(Value::from(true));
}
let target = params[2].as_string()?;
let mut assumptions: Vec<(String, bool)> = vec![(target.to_string(), false)];
if params[0] != Value::Null {
let g1 = params[0].as_string()?;
assumptions.push((g1.to_string(), true));
}
if params[1] != Value::Null {
let g2 = params[1].as_string()?;
assumptions.push((g2.to_string(), true));
}
Ok(Value::from(!model.conditions.is_satisfiable(&assumptions)))
}),
);
}
fn register_resource_condition(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"resource_condition".into(),
1,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
Ok(model
.resources
.get(rid.as_ref())
.and_then(|r| r.condition.as_deref())
.map(Value::from)
.unwrap_or(Value::Null))
}),
);
}
fn register_primary_identifier_conflicts(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"primary_identifier_conflicts".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::from(Vec::<Value>::new()));
};
let resource_type = params[0].as_string()?;
let identifier_properties = params[1]
.as_array()?
.iter()
.map(|property| property.as_string().map(|property| property.to_string()))
.collect::<Result<Vec<_>, _>>()?;
let conflicts = model
.primary_identifier_conflicts(resource_type.as_ref(), &identifier_properties)
.into_iter()
.map(|(tuple, resources)| {
let message = primary_identifier_conflict_message(
resource_type.as_ref(),
&identifier_properties,
&tuple,
&resources,
);
json_to_value(&serde_json::json!({
"tuple": tuple,
"resources": resources,
"message": message,
}))
})
.collect::<Vec<_>>();
Ok(Value::from(conflicts))
}),
);
}
fn register_has_property(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"has_property".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let prop = params[1].as_string()?;
Ok(Value::from(model.has_property(rid.as_ref(), prop.as_ref())))
}),
);
}
fn register_param_allowed_values(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"param_allowed_values".into(),
1,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let name = params[0].as_string()?;
match model.parameters.get(name.as_ref()).and_then(|p| p.allowed_values.as_ref()) {
Some(vals) => {
let v: Vec<Value> = vals.iter().map(|s: &String| Value::from(s.as_str())).collect();
Ok(Value::from(v))
}
None => Ok(Value::from(Vec::<Value>::new())),
}
}),
);
}
fn register_param_type(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"param_type".into(),
1,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let name = params[0].as_string()?;
Ok(model
.parameters
.get(name.as_ref())
.map(|p| Value::from(p.param_type.as_str()))
.unwrap_or(Value::Undefined))
}),
);
}
fn register_mapping_value(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"mapping_value".into(),
3,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let map_name = params[0].as_string()?;
let k1 = params[1].as_string()?;
let k2 = params[2].as_string()?;
let result = model
.mappings
.get(map_name.as_ref())
.and_then(|l1| l1.get(k1.as_ref()))
.and_then(|l2| l2.get(k2.as_ref()));
Ok(result.map(json_to_value).unwrap_or(Value::Undefined))
}),
);
}
fn register_has_transform(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"has_transform".into(),
1,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let name = params[0].as_string()?;
Ok(Value::from(model.transforms.iter().any(|t| t.as_str() == name.as_ref())))
}),
);
}
fn register_get_resource(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"get_resource".into(),
1,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let res = match model.resources.get(rid.as_ref()) {
Some(r) => r,
None => return Ok(Value::Undefined),
};
let mut props = serde_json::Map::new();
for (k, v) in &res.properties {
props.insert(k.clone(), resolved_value_to_json_static(v));
}
Value::from_json_str(
&serde_json::json!({
(FIELD_RESOURCE_TYPE): res.resource_type, (FIELD_CONDITION): res.condition,
(FIELD_DEPENDS_ON): res.depends_on, (FIELD_PROPERTIES): props,
})
.to_string(),
)
}),
);
}
fn register_resolve_ref_target(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"resolve_ref_target".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let path = params[1].as_string()?;
let target_id = match model.follow_ref(rid, path) {
Some(t) => t.to_string(),
None => return Ok(Value::Undefined),
};
let res = match model.resources.get(&target_id) {
Some(r) => r,
None => return Ok(Value::Undefined),
};
let mut props = serde_json::Map::new();
for (k, v) in &res.properties {
props.insert(k.clone(), resolved_value_to_json_static(v));
}
Value::from_json_str(
&serde_json::json!({
(FIELD_RESOURCE_TYPE): res.resource_type, (FIELD_CONDITION): res.condition, (FIELD_PROPERTIES): props,
})
.to_string(),
)
}),
);
}
fn matching_property_paths(properties: &serde_json::Value, property_names: &HashSet<String>) -> Vec<serde_json::Value> {
fn visit(
value: &serde_json::Value,
property_names: &HashSet<String>,
segments: &mut Vec<String>,
matches: &mut Vec<serde_json::Value>,
) {
match value {
serde_json::Value::Object(map) => {
for (property, child) in map {
segments.push(property.clone());
if property_names.contains(property) {
matches.push(serde_json::json!({
"path": format!("Properties.{}", segments.join(".")),
"property": property,
"value": child,
}));
}
visit(child, property_names, segments, matches);
segments.pop();
}
}
serde_json::Value::Array(items) => {
for (index, child) in items.iter().enumerate() {
segments.push(index.to_string());
visit(child, property_names, segments, matches);
segments.pop();
}
}
_ => {}
}
}
let mut matches = Vec::new();
visit(properties, property_names, &mut Vec::new(), &mut matches);
matches
}
fn register_matching_property_paths(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"matching_property_paths".into(),
2,
Box::new(|params: Vec<Value>| {
let properties: serde_json::Value = serde_json::from_str(¶ms[0].to_json_str()?)?;
let names = params[1].as_array()?;
let mut property_names = HashSet::with_capacity(names.len());
for name in names.iter() {
property_names.insert(name.as_string()?.to_string());
}
Ok(json_to_value(&serde_json::Value::Array(matching_property_paths(&properties, &property_names))))
}),
);
}
fn register_flatten_list(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"flatten_list".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let path = params[1].as_string()?;
let val = model.resolve_deep(rid, path).or_else(|| model.resolve(rid, path).cloned());
let Some(resolved) = val else {
return Ok(Value::from(Vec::<Value>::new()));
};
let items = flatten_resolved_list(&resolved);
let results: Vec<Value> = items
.into_iter()
.enumerate()
.map(|(i, v)| {
let jv = resolved_value_to_json_static(&v);
Value::from_json_str(&serde_json::json!({"value": jv, "index": i}).to_string())
.unwrap_or(Value::Undefined)
})
.collect();
Ok(Value::from(results))
}),
);
}
fn flatten_resolved_list(rv: &ResolvedValue) -> Vec<ResolvedValue> {
match rv {
ResolvedValue::Concrete { value: v } if v.is_array() => {
v.as_array().unwrap().iter().map(|v| ResolvedValue::Concrete { value: v.clone().into() }).collect()
}
ResolvedValue::List { items } => items.clone(),
ResolvedValue::Conditional { if_true: t, if_false: f, .. } => {
let mut items = flatten_resolved_list(t);
items.extend(flatten_resolved_list(f));
items
}
ResolvedValue::Enum { variants: vals } => vals.iter().flat_map(flatten_resolved_list).collect(),
_ => vec![rv.clone()],
}
}
fn register_arn_matches(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"arn_matches".into(),
2,
Box::new(|params: Vec<Value>| {
let arn = params[0].as_string()?;
let pattern = params[1].as_string()?;
let arn_parts: Vec<&str> = arn.split(':').collect();
let pat_parts: Vec<&str> = pattern.split(':').collect();
if arn_parts.len() < 6 || pat_parts.len() < 6 {
return Ok(Value::from(false));
}
for (a, p) in arn_parts.iter().zip(pat_parts.iter()) {
if *p != "*" && *p != *a {
return Ok(Value::from(false));
}
}
Ok(Value::from(true))
}),
);
}
fn register_arn_matches_format(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"arn_matches_format".into(),
2,
Box::new(|params: Vec<Value>| {
let resource_arn = params[0].as_string()?;
let format_arn = params[1].as_string()?;
Ok(Value::from(arn_matches_format(resource_arn.as_ref(), format_arn.as_ref())))
}),
);
}
fn arn_matches_format(resource_arn: &str, format_arn: &str) -> bool {
let pad_to_six = |arn: &str| -> Vec<String> {
let mut parts: Vec<String> = arn.splitn(6, ':').map(str::to_string).collect();
while parts.len() < 6 {
parts.push("*".to_string());
}
parts
};
let r_parts = pad_to_six(resource_arn);
let f_parts = pad_to_six(format_arn);
for i in 0..5 {
if r_parts[i] == "*" {
continue;
}
if matches!(f_parts[i].as_str(), "${Partition}" | "${Region}" | "${Account}") {
continue;
}
if r_parts[i] != f_parts[i] {
return false;
}
}
if r_parts[5] == "*" {
return true;
}
let delimiter = if f_parts[5].contains(':') {
':'
} else if f_parts[5].contains('/') {
'/'
} else {
return true;
};
for (r_seg, f_seg) in r_parts[5].split(delimiter).zip(f_parts[5].split(delimiter)) {
if r_seg == f_seg {
continue;
}
if r_seg == "*" || r_seg.starts_with('*') || f_seg.is_empty() || f_seg == ".*" {
return true;
}
if r_seg.starts_with(f_seg) && r_seg.contains('*') {
return true;
}
return false;
}
true
}
fn register_ip_overlaps(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"ip_overlaps".into(),
2,
Box::new(|params: Vec<Value>| {
let a_str = params[0].as_string()?;
let b_str = params[1].as_string()?;
let net_a: ipnetwork::IpNetwork =
a_str.parse().map_err(|e| anyhow::anyhow!("Invalid CIDR '{}': {}", a_str, e))?;
let net_b: ipnetwork::IpNetwork =
b_str.parse().map_err(|e| anyhow::anyhow!("Invalid CIDR '{}': {}", b_str, e))?;
let overlaps = net_a.contains(net_b.network()) || net_b.contains(net_a.network());
Ok(Value::from(overlaps))
}),
);
}
fn register_ip_subnet_of(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"ip_subnet_of".into(),
2,
Box::new(|params: Vec<Value>| {
let sub_str = params[0].as_string()?;
let vpc_str = params[1].as_string()?;
let sub_net: ipnetwork::IpNetwork =
sub_str.parse().map_err(|e| anyhow::anyhow!("Invalid CIDR '{}': {}", sub_str, e))?;
let vpc_net: ipnetwork::IpNetwork =
vpc_str.parse().map_err(|e| anyhow::anyhow!("Invalid CIDR '{}': {}", vpc_str, e))?;
let is_sub = match (sub_net, vpc_net) {
(ipnetwork::IpNetwork::V4(s), ipnetwork::IpNetwork::V4(v)) => s.is_subnet_of(v),
(ipnetwork::IpNetwork::V6(s), ipnetwork::IpNetwork::V6(v)) => s.is_subnet_of(v),
_ => false,
};
Ok(Value::from(is_sub))
}),
);
}
fn register_is_valid_cidr_strict(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"is_valid_cidr_strict".into(),
1,
Box::new(|params: Vec<Value>| {
let cidr_str = params[0].as_string()?;
let valid = cidr_str
.parse::<ipnetwork::IpNetwork>()
.map(|net| match net {
ipnetwork::IpNetwork::V4(n) => {
let ip: u32 = n.ip().into();
let host_bits = 32u32.saturating_sub(n.prefix() as u32);
let mask = if host_bits >= 32 { 0u32 } else { !0u32 << host_bits };
ip & !mask == 0
}
ipnetwork::IpNetwork::V6(_) => true,
})
.unwrap_or(false);
Ok(Value::from(valid))
}),
);
}
fn register_ensure_list(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"ensure_list".into(),
1,
Box::new(|params: Vec<Value>| match ¶ms[0] {
Value::Array(_) => Ok(params[0].clone()),
other => Ok(Value::from(vec![other.clone()])),
}),
);
}
fn register_render_list(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"render_list".into(),
1,
Box::new(|params: Vec<Value>| {
let items = match rego_to_json(¶ms[0]) {
serde_json::Value::Array(items) => items,
other => vec![other],
};
Ok(Value::from(render_value_list(&items)))
}),
);
}
fn register_render_value(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"render_value".into(),
1,
Box::new(|params: Vec<Value>| Ok(Value::from(render_value(®o_to_json(¶ms[0]))))),
);
}
fn register_property_can_be_absent(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"property_can_be_absent".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::from(false));
};
let rid = params[0].as_string()?;
let path = params[1].as_string()?;
let prop = path.strip_prefix("Properties.").unwrap_or(path.as_ref());
let key_present =
model.resources.get(rid.as_ref()).map(|r| r.properties.contains_key(prop)).unwrap_or(false);
if !key_present {
return Ok(Value::from(true));
}
let scenarios = model.resolve_scenarios_json_shared(rid.as_ref(), path.as_ref());
let absent = scenarios.is_empty() || scenarios.iter().any(|(v, _)| v.is_null());
Ok(Value::from(absent))
}),
);
}
fn register_is_valid_region(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"is_valid_region".into(),
1,
Box::new(|params: Vec<Value>| {
let region = params[0].as_string()?;
Ok(Value::from(template_model::is_known_region(region.as_ref())))
}),
);
}
fn register_region_flat_invalid(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"region_flat_invalid".into(),
2,
Box::new(move |params: Vec<Value>| {
let region = current_region();
let region_map = rego_to_json(¶ms[0]);
let value = params[1].as_string()?;
let Some(map) = region_map.as_object() else {
return Ok(Value::Undefined);
};
match region_enums::flat_allowed_value_set(map, region.as_deref()) {
Some(allowed) if !allowed.contains(value.as_ref()) => {
Ok(Value::from(region_enums::flat_invalid_message(value.as_ref(), region.as_deref())))
}
_ => Ok(Value::Undefined),
}
}),
);
}
fn register_region_conditional_invalid(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"region_conditional_invalid".into(),
5,
Box::new(move |params: Vec<Value>| {
let region = current_region();
let region_map = rego_to_json(¶ms[0]);
let target_prop = params[1].as_string()?;
let normalize_engine_case = matches!(¶ms[2], Value::Bool(b) if *b);
let value = params[3].as_string()?;
let props = rego_to_json(¶ms[4]);
let Some(map) = region_map.as_object() else {
return Ok(Value::Undefined);
};
match region_enums::conditional_enum_mismatch(
map,
region.as_deref(),
target_prop.as_ref(),
normalize_engine_case,
value.as_ref(),
|prop| props.get(prop).and_then(|v| v.as_str()).map(String::from),
) {
Some(mismatch) => Ok(Value::from(region_enums::conditional_mismatch_message(
value.as_ref(),
&mismatch,
region.as_deref(),
))),
None => Ok(Value::Undefined),
}
}),
);
}
fn rego_to_json(v: &Value) -> serde_json::Value {
match v.to_json_str() {
Ok(s) => serde_json::from_str(&s).unwrap_or(serde_json::Value::Null),
Err(_) => serde_json::Value::Null,
}
}
fn register_coerce_to_number(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"coerce_to_number".into(),
1,
Box::new(|params: Vec<Value>| {
let jv = rego_to_json(¶ms[0]);
match coerce_to_number(&jv) {
Some(n) => {
if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 {
Ok(Value::from(n as i64))
} else {
Ok(Value::from(n))
}
}
None => Ok(Value::Undefined),
}
}),
);
}
fn register_coerce_to_integer(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"coerce_to_integer".into(),
1,
Box::new(|params: Vec<Value>| {
let jv = rego_to_json(¶ms[0]);
match coerce_to_integer(&jv) {
Some(i) => Ok(Value::from(i)),
None => Ok(Value::Undefined),
}
}),
);
}
fn register_coerce_to_string(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"coerce_to_string".into(),
1,
Box::new(|params: Vec<Value>| {
let jv = rego_to_json(¶ms[0]);
match coerce_to_string(&jv) {
Some(s) => Ok(Value::from(s.as_str())),
None => Ok(Value::Undefined),
}
}),
);
}
fn register_coerce_port_to_string(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"coerce_port_to_string".into(),
1,
Box::new(|params: Vec<Value>| {
let jv = rego_to_json(¶ms[0]);
match coerce_port_to_string(&jv) {
Some(s) => Ok(Value::from(s.as_str())),
None => Ok(Value::Undefined),
}
}),
);
}
fn register_coerce_to_bool(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"coerce_to_bool".into(),
1,
Box::new(|params: Vec<Value>| {
let jv = rego_to_json(¶ms[0]);
match coerce_to_bool(&jv) {
Some(b) => Ok(Value::from(b)),
None => Ok(Value::Undefined),
}
}),
);
}
fn register_fargate_cpu_is_offered(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"fargate_cpu_is_offered".into(),
1,
Box::new(|params: Vec<Value>| match cpu_is_offered(®o_to_json(¶ms[0])) {
Some(is_offered) => Ok(Value::from(is_offered)),
None => Ok(Value::Undefined),
}),
);
}
fn register_fargate_task_size_is_offered(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"fargate_task_size_is_offered".into(),
2,
Box::new(|params: Vec<Value>| {
match task_size_is_offered(®o_to_json(¶ms[0]), ®o_to_json(¶ms[1])) {
Some(is_offered) => Ok(Value::from(is_offered)),
None => Ok(Value::Undefined),
}
}),
);
}
fn register_cfn_type_compatible(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"cfn_type_compatible".into(),
2,
Box::new(|params: Vec<Value>| {
let jv = rego_to_json(¶ms[0]);
let expected = params[1].as_string()?;
Ok(Value::from(type_compatible(&jv, expected)))
}),
);
}
fn register_input_region(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"input_region".into(),
0,
Box::new(|_: Vec<Value>| match current_region() {
Some(r) => Ok(Value::from(r.as_str())),
None => Ok(Value::Null),
}),
);
}
fn register_pipeline_artifacts(rego: &mut regorus::Engine) {
let _ = rego.add_extension("pipeline_artifacts".into(), 1, Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else { return Ok(Value::Undefined); };
let rid = params[0].as_string()?;
let resource = match model.resources.get(rid.as_ref()) {
Some(r) => r,
None => return Ok(Value::Undefined),
};
let stages_val = match resource.properties.get("Stages") {
Some(ResolvedValue::Concrete { value: v }) => v.0.clone(),
_ => return Value::from_json_str(&serde_json::json!({"issues": []}).to_string()),
};
let stages = match stages_val.as_array() {
Some(a) => a,
None => return Value::from_json_str(&serde_json::json!({"issues": []}).to_string()),
};
let mut seen_outputs = HashSet::new();
let mut issues: Vec<serde_json::Value> = Vec::new();
for (stage_idx, stage) in stages.iter().enumerate() {
let stage_name = stage.get("Name").and_then(|v| v.as_str()).unwrap_or("unknown");
let actions = match stage.get("Actions").and_then(|v| v.as_array()) {
Some(a) => a,
None => continue,
};
for action in actions {
let action_name = action.get("Name").and_then(|v| v.as_str()).unwrap_or("unknown");
if let Some(outputs) = action.get("OutputArtifacts").and_then(|v| v.as_array()) {
for out in outputs {
if let Some(name) = out.get("Name").and_then(|v| v.as_str())
&& !seen_outputs.insert(name.to_string()) {
issues.push(serde_json::json!({"message": format!("Duplicate OutputArtifact name '{}' in stage '{}' action '{}'", name, stage_name, action_name)}));
}
}
}
if stage_idx > 0
&& let Some(inputs) = action.get("InputArtifacts").and_then(|v| v.as_array()) {
for inp in inputs {
if let Some(name) = inp.get("Name").and_then(|v| v.as_str())
&& !seen_outputs.contains(name) {
issues.push(serde_json::json!({"message": format!("InputArtifact '{}' in stage '{}' action '{}' does not reference a previously defined OutputArtifact", name, stage_name, action_name)}));
}
}
}
}
}
Value::from_json_str(&serde_json::json!({"issues": issues}).to_string())
}));
}
fn rego_artifact_count_scenarios(value: Option<&serde_json::Value>) -> Vec<usize> {
fn walk(value: Option<&serde_json::Value>, out: &mut BTreeSet<usize>) {
match value {
Some(serde_json::Value::Array(a)) => {
out.insert(a.len());
}
Some(serde_json::Value::Object(o)) if o.len() == 1 && o.contains_key(FN_IF) => {
if let Some(branches) = o.get(FN_IF).and_then(|v| v.as_array()) {
walk(branches.get(1), out);
walk(branches.get(2), out);
}
}
_ => {
out.insert(0);
}
}
}
let mut counts = BTreeSet::new();
walk(value, &mut counts);
counts.into_iter().collect()
}
fn register_pipeline_artifact_count_issues(rego: &mut regorus::Engine) -> anyhow::Result<()> {
let document: CodepipelineArtifactCounts =
serde_json::from_slice(&data_source::embedded::CODEPIPELINE_ACTION_ARTIFACT_COUNTS_BYTES).map_err(|error| {
anyhow::anyhow!("Failed to parse embedded codepipeline_action_artifact_counts data: {}", error)
})?;
let counts = document.codepipeline_action_artifact_counts;
anyhow::ensure!(!counts.is_empty(), "Embedded codepipeline_action_artifact_counts data must not be empty");
let _ = rego.add_extension(
"pipeline_artifact_count_issues".into(),
1,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let stages_json = model
.resolve_deep(rid.as_ref(), "Properties.Stages")
.map(|rv| resolved_to_json_preserving_conditionals(&rv));
let issues = pipeline_artifact_count_issues(stages_json.as_ref(), &counts);
Value::from_json_str(&serde_json::json!({ "issues": issues }).to_string())
}),
);
Ok(())
}
fn pipeline_artifact_count_issues(
stages_json: Option<&serde_json::Value>,
counts: &HashMap<String, ArtifactCountEntry>,
) -> Vec<serde_json::Value> {
let mut issues = Vec::new();
let Some(stages) = stages_json.and_then(|v| v.as_array()) else {
return issues;
};
for stage in stages {
let Some(actions) = stage.get("Actions").and_then(|a| a.as_array()) else {
continue;
};
for action in actions {
let action_type_id = action.get("ActionTypeId");
let (Some(owner), Some(category), Some(provider)) = (
action_type_id.and_then(|a| a.get("Owner")).and_then(|c| c.as_str()),
action_type_id.and_then(|a| a.get("Category")).and_then(|c| c.as_str()),
action_type_id.and_then(|a| a.get("Provider")).and_then(|c| c.as_str()),
) else {
continue;
};
let aname = action.get("Name").and_then(|n| n.as_str()).unwrap_or("unknown");
let key = format!("{owner}/{category}/{provider}");
let Some(bounds) = counts.get(&key) else { continue };
for n in rego_artifact_count_scenarios(action.get("InputArtifacts")) {
if n < bounds.min_input {
issues.push(serde_json::json!({"message": format!(
"Action '{}' ({}) has {} input artifacts, expected at least {}",
aname, key, n, bounds.min_input
)}));
}
if n > bounds.max_input {
issues.push(serde_json::json!({"message": format!(
"Action '{}' ({}) has {} input artifacts, expected at most {}",
aname, key, n, bounds.max_input
)}));
}
}
for n in rego_artifact_count_scenarios(action.get("OutputArtifacts")) {
if n < bounds.min_output {
issues.push(serde_json::json!({"message": format!(
"Action '{}' ({}) has {} output artifacts, expected at least {}",
aname, key, n, bounds.min_output
)}));
}
if n > bounds.max_output {
issues.push(serde_json::json!({"message": format!(
"Action '{}' ({}) has {} output artifacts, expected at most {}",
aname, key, n, bounds.max_output
)}));
}
}
}
}
issues
}
fn register_resolve_type(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"resolve_type".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let path = params[1].as_string()?;
let val = model.resolve_deep(rid, path).or_else(|| model.resolve(rid, path).cloned());
let type_str = match val {
Some(ResolvedValue::Concrete { value: v }) if v.is_string() => "string",
Some(ResolvedValue::Concrete { value: v }) if v.is_number() => "number",
Some(ResolvedValue::Concrete { value: v }) if v.is_boolean() => "boolean",
Some(ResolvedValue::Concrete { value: v }) if v.is_array() => "array",
Some(ResolvedValue::List { .. }) => "array",
Some(ResolvedValue::Concrete { value: v }) if v.is_object() => "object",
Some(ResolvedValue::Map { .. }) => "object",
Some(ResolvedValue::Concrete { value: v }) if v.is_null() => "null",
Some(ResolvedValue::Conditional { .. }) => "conditional",
Some(ResolvedValue::Dynamic { .. }) => "dynamic",
Some(ResolvedValue::TypedDynamic { .. }) => "dynamic",
Some(ResolvedValue::Reference { .. }) => "reference",
Some(ResolvedValue::Enum { .. }) => "enum",
Some(ResolvedValue::Concrete { .. }) => "null",
None => return Ok(Value::Undefined),
};
Ok(Value::from(type_str))
}),
);
}
fn register_schema_properties(rego: &mut regorus::Engine, catalog: Arc<SchemaMetadataCatalog>) {
let _ = rego.add_extension(
"schema_properties".into(),
1,
Box::new(move |params: Vec<Value>| {
let rtype = params[0].as_string()?;
match catalog.get(rtype.as_ref()) {
Some(entry) => {
let vals: Vec<Value> = entry.properties.iter().map(|s| Value::from(s.as_str())).collect();
Ok(Value::from(vals))
}
None => Ok(Value::from(Vec::<Value>::new())),
}
}),
);
}
fn register_schema_required(rego: &mut regorus::Engine, catalog: Arc<SchemaMetadataCatalog>) {
let _ = rego.add_extension(
"schema_required".into(),
1,
Box::new(move |params: Vec<Value>| {
let rtype = params[0].as_string()?;
match catalog.get(rtype.as_ref()) {
Some(entry) => {
let vals: Vec<Value> = entry.required.iter().map(|s| Value::from(s.as_str())).collect();
Ok(Value::from(vals))
}
None => Ok(Value::from(Vec::<Value>::new())),
}
}),
);
}
fn register_schema_type(rego: &mut regorus::Engine, catalog: Arc<SchemaMetadataCatalog>) {
let _ = rego.add_extension(
"schema_type".into(),
2,
Box::new(move |params: Vec<Value>| {
let rtype = params[0].as_string()?;
let prop = params[1].as_string()?;
match catalog.get(rtype.as_ref()).and_then(|entry| entry.property_types.get(prop.as_ref())) {
Some(s) => Ok(Value::from(s.as_str())),
None => Ok(Value::Undefined),
}
}),
);
}
fn register_schema_enum(rego: &mut regorus::Engine, catalog: Arc<SchemaMetadataCatalog>) {
let _ = rego.add_extension(
"schema_enum".into(),
2,
Box::new(move |params: Vec<Value>| {
let rtype = params[0].as_string()?;
let prop = params[1].as_string()?;
match catalog.get(rtype.as_ref()).and_then(|entry| entry.property_enums.get(prop.as_ref())) {
Some(vals) => {
let v: Vec<Value> = vals.iter().map(json_to_value).collect();
Ok(Value::from(v))
}
None => Ok(Value::from(Vec::<Value>::new())),
}
}),
);
}
fn register_attribute_type(rego: &mut regorus::Engine, catalog: Arc<SchemaMetadataCatalog>) {
let _ = rego.add_extension(
"attribute_type".into(),
2,
Box::new(move |params: Vec<Value>| {
let rtype = params[0].as_string()?;
let attr = params[1].as_string()?;
match catalog.get(rtype.as_ref()).and_then(|entry| entry.property_types.get(attr.as_ref())) {
Some(s) => Ok(Value::from(s.as_str())),
None => Ok(Value::Undefined),
}
}),
);
}
fn register_getatt_return_type(rego: &mut regorus::Engine, getatt: Arc<GetattData>) {
let _ = rego.add_extension(
"getatt_return_type".into(),
2,
Box::new(move |params: Vec<Value>| {
let rtype = params[0].as_string()?;
let attr = params[1].as_string()?;
match getatt.getatt_attribute_types.get(rtype.as_ref()).and_then(|m| m.get(attr.as_ref())) {
Some(s) => Ok(Value::from(s.as_str())),
None => Ok(Value::from("string")),
}
}),
);
}
fn register_edges_from(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"edges_from".into(),
1,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let edges: Vec<Value> = model
.graph
.outgoing(rid)
.into_iter()
.filter_map(|e| {
Value::from_json_str(
&serde_json::json!({
(FIELD_TARGET): e.target, (FIELD_KIND): format!("{:?}", e.kind),
(FIELD_SOURCE_PATH): e.source_path,
})
.to_string(),
)
.ok()
})
.collect();
Ok(Value::from(edges))
}),
);
}
fn register_edges_to(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"edges_to".into(),
1,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let edges: Vec<Value> = model
.graph
.incoming(rid)
.into_iter()
.filter_map(|e| {
Value::from_json_str(
&serde_json::json!({
(FIELD_SOURCE): e.source_resource, (FIELD_KIND): format!("{:?}", e.kind),
(FIELD_SOURCE_PATH): e.source_path,
})
.to_string(),
)
.ok()
})
.collect();
Ok(Value::from(edges))
}),
);
}
fn register_make_diag(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"make_diag".into(),
4,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rule_id = params[0].as_string()?;
let severity = params[1].as_string()?;
let resource_id = params[2].as_string()?;
let message = params[3].as_string()?;
let span = if resource_id.is_empty() { UNKNOWN_SPAN } else { model.resource_span(resource_id, "") };
let mut obj = serde_json::json!({
"rule_id": rule_id.as_ref(), "severity": severity.as_ref(),
"message": message.as_ref(), "resource_id": resource_id.as_ref(),
"resource_path": "",
});
if span != UNKNOWN_SPAN {
let m = obj.as_object_mut().unwrap();
m.insert("start_line".into(), span.start_line.into());
m.insert("start_column".into(), span.start_column.into());
m.insert("end_line".into(), span.end_line.into());
m.insert("end_column".into(), span.end_column.into());
}
Ok(json_to_value(&obj))
}),
);
}
fn register_make_diag_at(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"make_diag_at".into(),
5,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rule_id = params[0].as_string()?;
let severity = params[1].as_string()?;
let resource_id = params[2].as_string()?;
let prop_path = params[3].as_string()?;
let message = params[4].as_string()?;
let span = resolve_span(&model, resource_id, prop_path);
let mut obj = serde_json::json!({
"rule_id": rule_id.as_ref(), "severity": severity.as_ref(),
"message": message.as_ref(), "resource_id": resource_id.as_ref(),
"resource_path": prop_path.as_ref(),
});
if span != UNKNOWN_SPAN {
let m = obj.as_object_mut().unwrap();
m.insert("start_line".into(), span.start_line.into());
m.insert("start_column".into(), span.start_column.into());
m.insert("end_line".into(), span.end_line.into());
m.insert("end_column".into(), span.end_column.into());
}
Ok(json_to_value(&obj))
}),
);
}
fn resolve_span(model: &SemanticModel, resource_id: &str, prop_path: &str) -> SourceSpan {
model.resource_span(resource_id, prop_path)
}
fn register_make_diag_at_source(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"make_diag_at_source".into(),
6,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rule_id = params[0].as_string()?;
let severity = params[1].as_string()?;
let resource_id = params[2].as_string()?;
let prop_path = params[3].as_string()?;
let source_path = params[4].as_string()?;
let message = params[5].as_string()?;
let span = resolve_span(&model, resource_id, source_path);
let mut obj = serde_json::json!({
"rule_id": rule_id.as_ref(), "severity": severity.as_ref(),
"message": message.as_ref(), "resource_id": resource_id.as_ref(),
"resource_path": prop_path.as_ref(),
(DIAGNOSTIC_SOURCE_PATH_FIELD): source_path.as_ref(),
});
if span != UNKNOWN_SPAN {
let fields = obj.as_object_mut().unwrap();
fields.insert("start_line".into(), span.start_line.into());
fields.insert("start_column".into(), span.start_column.into());
fields.insert("end_line".into(), span.end_line.into());
fields.insert("end_column".into(), span.end_column.into());
}
Ok(json_to_value(&obj))
}),
);
}
fn register_make_diag_full(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"make_diag_full".into(),
7,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rule_id = params[0].as_string()?;
let severity = params[1].as_string()?;
let resource_id = params[2].as_string()?;
let prop_path = params[3].as_string()?;
let message = params[4].as_string()?;
let fix = params[5].as_string()?;
let doc_url = params[6].as_string()?;
let span = resolve_span(&model, resource_id, prop_path);
let fix_val =
if fix.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(fix.to_string()) };
let doc_val = if doc_url.is_empty() {
serde_json::Value::Null
} else {
serde_json::Value::String(doc_url.to_string())
};
let mut obj = serde_json::json!({
"rule_id": rule_id.as_ref(), "severity": severity.as_ref(),
"message": message.as_ref(), "resource_id": resource_id.as_ref(),
"resource_path": prop_path.as_ref(),
"suggested_fix": fix_val, "documentation_url": doc_val,
});
if span != UNKNOWN_SPAN {
let m = obj.as_object_mut().unwrap();
m.insert("start_line".into(), span.start_line.into());
m.insert("start_column".into(), span.start_column.into());
m.insert("end_line".into(), span.end_line.into());
m.insert("end_column".into(), span.end_column.into());
}
Ok(json_to_value(&obj))
}),
);
}
fn register_make_diag_related(rego: &mut regorus::Engine) {
let _ = rego.add_extension("make_diag_related".into(), 6, Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else { return Ok(Value::Undefined); };
let rule_id = params[0].as_string()?;
let severity = params[1].as_string()?;
let resource_id = params[2].as_string()?;
let prop_path = params[3].as_string()?;
let message = params[4].as_string()?;
let related_str = params[5].to_json_str()?;
let span = resolve_span(&model, resource_id, prop_path);
let related_arr: Vec<serde_json::Value> = serde_json::from_str(&related_str).unwrap_or_default();
let related: Vec<serde_json::Value> = related_arr.iter().filter_map(|r| {
let rr = r.get("resource")?.as_str()?;
let rp = r.get("path").and_then(|v| v.as_str()).unwrap_or("");
let rm = r.get("message").and_then(|v| v.as_str()).unwrap_or("");
let rspan = resolve_span(&model, rr, rp);
let rtype = model.resources.get(rr).map(|res| res.resource_type.as_str()).unwrap_or("");
Some(serde_json::json!({"start_line": rspan.start_line, "start_column": rspan.start_column, "end_line": rspan.end_line, "end_column": rspan.end_column, "message": rm, "resource_id": rr, "resource_type": rtype}))
}).collect();
let mut obj = serde_json::json!({
"rule_id": rule_id.as_ref(), "severity": severity.as_ref(),
"message": message.as_ref(), "resource_id": resource_id.as_ref(),
"resource_path": prop_path.as_ref(),
"related_locations": related,
});
if span != UNKNOWN_SPAN {
let m = obj.as_object_mut().unwrap();
m.insert("start_line".into(), span.start_line.into());
m.insert("start_column".into(), span.start_column.into());
m.insert("end_line".into(), span.end_line.into());
m.insert("end_column".into(), span.end_column.into());
}
Ok(json_to_value(&obj))
}));
}
fn register_make_diag_conditional(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"make_diag_conditional".into(),
6,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rule_id = params[0].as_string()?;
let severity = params[1].as_string()?;
let resource_id = params[2].as_string()?;
let prop_path = params[3].as_string()?;
let message = params[4].as_string()?;
let conds_str = params[5].to_json_str()?;
let span = resolve_span(&model, resource_id, prop_path);
let conds_val: serde_json::Value = serde_json::from_str(&conds_str).unwrap_or(serde_json::Value::Null);
let mut obj = serde_json::json!({
"rule_id": rule_id.as_ref(), "severity": severity.as_ref(),
"message": message.as_ref(), "resource_id": resource_id.as_ref(),
"resource_path": prop_path.as_ref(),
"condition_scenario": conds_val,
});
if span != UNKNOWN_SPAN {
let m = obj.as_object_mut().unwrap();
m.insert("start_line".into(), span.start_line.into());
m.insert("start_column".into(), span.start_column.into());
m.insert("end_line".into(), span.end_line.into());
m.insert("end_column".into(), span.end_column.into());
}
Ok(json_to_value(&obj))
}),
);
}
fn register_estimated_string_length_bounds(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"estimated_string_length_bounds".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::Undefined);
};
let rid = params[0].as_string()?;
let path = params[1].as_string()?;
match model.estimated_string_length_bounds(rid, path) {
Some((shortest, longest)) => {
Ok(json_to_value(&serde_json::json!({"shortest": shortest, "longest": longest})))
}
None => Ok(Value::Undefined),
}
}),
);
}
fn register_schema_string_length(rego: &mut regorus::Engine, catalog: Arc<SchemaMetadataCatalog>) {
let _ = rego.add_extension(
"schema_string_length".into(),
2,
Box::new(move |params: Vec<Value>| {
let rtype = params[0].as_string()?;
let prop = params[1].as_string()?;
let entry = match catalog.get(rtype.as_ref()) {
Some(entry) => entry,
None => return Ok(Value::Undefined),
};
let constraints = match entry.property_constraints.get(prop.as_ref()) {
Some(constraints) => constraints,
None => return Ok(Value::Undefined),
};
let is_string = entry.property_types.get(prop.as_ref()).map(|t| t == "string").unwrap_or(false);
let mut map = serde_json::Map::new();
if let Some(v) = constraints.min_length {
map.insert("minLength".into(), serde_json::json!(v));
} else if is_string && let Some(v) = &constraints.minimum {
map.insert("minLength".into(), serde_json::Value::Number(v.clone()));
}
if let Some(v) = constraints.max_length {
map.insert("maxLength".into(), serde_json::json!(v));
} else if is_string && let Some(v) = &constraints.maximum {
map.insert("maxLength".into(), serde_json::Value::Number(v.clone()));
}
if map.is_empty() {
return Ok(Value::Undefined);
}
Ok(json_to_value(&serde_json::Value::Object(map)))
}),
);
}
fn register_schema_requires_unique_items(rego: &mut regorus::Engine, catalog: Arc<SchemaMetadataCatalog>) {
let _ = rego.add_extension(
"schema_requires_unique_items".into(),
2,
Box::new(move |params: Vec<Value>| {
let rtype = params[0].as_string()?;
let prop = params[1].as_string()?;
let requires_unique = catalog
.get(rtype.as_ref())
.and_then(|entry| entry.property_constraints.get(prop.as_ref()))
.and_then(|constraints| constraints.unique_items)
.unwrap_or(false);
Ok(Value::from(requires_unique))
}),
);
}
fn register_unreachable_if_branches(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"unreachable_if_branches".into(),
1,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::from(Vec::<Value>::new()));
};
let rid = params[0].as_string()?;
let rid_str: &str = rid.as_ref();
if let Some(output_name) = rid_str.strip_prefix("__output__") {
if let Some(output) = model.outputs.get(output_name) {
let base_assumptions: Vec<(String, bool)> = match &output.condition {
Some(cond) => vec![(cond.clone(), true)],
None => vec![],
};
let mut results = Vec::new();
let path_prefix = format!("Outputs/{}/Value", output_name);
collect_unreachable_branches(
&model,
"",
&output.value,
&path_prefix,
&base_assumptions,
&mut results,
);
return Ok(Value::from(results));
}
return Ok(Value::from(Vec::<Value>::new()));
}
let res = match model.resources.get(rid_str) {
Some(r) => r,
None => return Ok(Value::from(Vec::<Value>::new())),
};
let base_assumptions: Vec<(String, bool)> = match &res.condition {
Some(cond) => vec![(cond.clone(), true)],
None => vec![],
};
let mut results = Vec::new();
for (prop_key, prop_val) in &res.properties {
let path_prefix = format!("Properties.{}", prop_key);
collect_unreachable_branches(&model, rid, prop_val, &path_prefix, &base_assumptions, &mut results);
}
Ok(Value::from(results))
}),
);
}
fn collect_unreachable_branches(
model: &Arc<SemanticModel>,
resource_id: &str,
value: &ResolvedValue,
path: &str,
assumptions: &[(String, bool)],
results: &mut Vec<Value>,
) {
match value {
ResolvedValue::Conditional { condition: cond, if_true, if_false } => {
if !model.condition_is_valid_for_reachability(cond) {
collect_unreachable_branches(
model,
resource_id,
if_true,
&format!("{}.{}.1", path, FN_IF),
assumptions,
results,
);
collect_unreachable_branches(
model,
resource_id,
if_false,
&format!("{}.{}.2", path, FN_IF),
assumptions,
results,
);
return;
}
let mut true_assumptions = assumptions.to_vec();
true_assumptions.push((cond.clone(), true));
let true_unreachable = !model.conditions.is_satisfiable(&true_assumptions)
&& model.conditions.is_satisfiable(&[(cond.clone(), true)]);
if true_unreachable {
let mut map = serde_json::Map::new();
map.insert("resourceId".into(), serde_json::Value::String(resource_id.to_string()));
map.insert("path".into(), serde_json::Value::String(format!("{}.{}.1", path, FN_IF)));
map.insert(
"message".into(),
serde_json::Value::String(format!(
"['Fn::If', 1] is not reachable. When setting condition '{}' to True",
cond
)),
);
results.push(json_to_value(&serde_json::Value::Object(map)));
}
let true_recurse_assumptions: &[(String, bool)] =
if true_unreachable { assumptions } else { &true_assumptions };
collect_unreachable_branches(
model,
resource_id,
if_true,
&format!("{}.{}.1", path, FN_IF),
true_recurse_assumptions,
results,
);
let mut false_assumptions = assumptions.to_vec();
false_assumptions.push((cond.clone(), false));
let false_unreachable = !model.conditions.is_satisfiable(&false_assumptions)
&& model.conditions.is_satisfiable(&[(cond.clone(), false)]);
if false_unreachable {
let existing: Vec<String> = assumptions
.iter()
.filter(|(name, _)| name != cond)
.map(|(name, val)| format!("condition '{}' is {}", name, if *val { "True" } else { "False" }))
.collect();
let explanation = if existing.is_empty() {
format!("When setting condition '{}' to False from current status True", cond)
} else {
format!(
"When setting condition '{}' to False. Where existing status for {}",
cond,
existing.join(" and ")
)
};
let mut map = serde_json::Map::new();
map.insert("resourceId".into(), serde_json::Value::String(resource_id.to_string()));
map.insert("path".into(), serde_json::Value::String(format!("{}.{}.2", path, FN_IF)));
map.insert(
"message".into(),
serde_json::Value::String(format!("['Fn::If', 2] is not reachable. {}", explanation)),
);
results.push(json_to_value(&serde_json::Value::Object(map)));
}
let false_recurse_assumptions: &[(String, bool)] =
if false_unreachable { assumptions } else { &false_assumptions };
collect_unreachable_branches(
model,
resource_id,
if_false,
&format!("{}.{}.2", path, FN_IF),
false_recurse_assumptions,
results,
);
}
ResolvedValue::Map { entries } => {
for MapEntry { key, value: val } in entries {
collect_unreachable_branches(
model,
resource_id,
val,
&format!("{}.{}", path, key),
assumptions,
results,
);
}
}
ResolvedValue::List { items } => {
for (i, val) in items.iter().enumerate() {
collect_unreachable_branches(model, resource_id, val, &format!("{}.{}", path, i), assumptions, results);
}
}
_ => {}
}
}
fn register_iam_identity_policy_findings(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"iam_identity_policy_findings".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::from(Vec::<Value>::new()));
};
let resource_id = params[0].as_string()?;
let document_path = params[1].as_string()?;
let findings = validate_identity_policy_scenarios(&model, resource_id.as_ref(), document_path.as_ref())
.into_iter()
.map(|finding| {
let effective_path = if finding.path.is_empty() {
document_path.to_string()
} else {
format!("{}.{}", document_path, finding.path)
};
let source_path =
if finding.source_path.is_empty() { effective_path.clone() } else { finding.source_path };
json_to_value(&serde_json::json!({
"effective_path": effective_path,
"source_path": source_path,
"message": finding.message
}))
})
.collect::<Vec<_>>();
Ok(Value::from(findings))
}),
);
}
fn register_iam_inline_policy_document_paths(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"iam_inline_policy_document_paths".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::from(Vec::<Value>::new()));
};
let resource_id = params[0].as_string()?;
let policies_path = params[1].as_string()?;
let paths = inline_identity_policy_document_paths(&model, resource_id.as_ref(), policies_path.as_ref());
Ok(Value::from(paths.into_iter().map(Value::from).collect::<Vec<_>>()))
}),
);
}
fn register_iam_policy_has_allow_not_action(rego: &mut regorus::Engine) {
let _ = rego.add_extension(
"iam_policy_has_allow_not_action".into(),
2,
Box::new(move |params: Vec<Value>| {
let Some(model) = current_model() else {
return Ok(Value::from(false));
};
let resource_id = params[0].as_string()?;
let document_path = params[1].as_string()?;
let result = policy_has_allow_not_action_scenarios(&model, resource_id.as_ref(), document_path.as_ref());
Ok(Value::from(result))
}),
);
}
#[cfg(test)]
mod tests {
use super::*;
use schema_validator::shared_base_getatt_data;
use template_model::resolver::{MapEntry, RefKind, ResolvedValue};
use template_model::{MARKER_DYNAMIC, MARKER_PARAM_TYPE, MARKER_REF};
#[test]
fn matching_property_paths_finds_nested_objects_and_arrays() {
let properties = serde_json::json!({
"Config": {"AccountPassword": "first"},
"Items": [{"Password": "second"}, {"Other": "value"}],
});
let names = HashSet::from(["AccountPassword".to_string(), "Password".to_string()]);
assert_eq!(
matching_property_paths(&properties, &names),
vec![
serde_json::json!({
"path": "Properties.Config.AccountPassword",
"property": "AccountPassword",
"value": "first",
}),
serde_json::json!({
"path": "Properties.Items.0.Password",
"property": "Password",
"value": "second",
}),
]
);
}
#[test]
fn json_to_value_preserves_nested_structure() {
let v = json_to_value(&serde_json::json!({"a": [1, "two", true], "b": {"c": null}}));
let obj = v.as_object().expect("should be object");
let a = obj.get(&Value::from("a")).expect("should have key 'a'");
let arr = a.as_array().expect("'a' should be array");
assert_eq!(arr.len(), 3);
}
#[test]
fn json_to_value_empty_collections() {
let arr = json_to_value(&serde_json::json!([]));
assert!(arr.as_array().expect("should be array").is_empty());
let obj = json_to_value(&serde_json::json!({}));
obj.as_object().expect("should be a valid object");
}
#[test]
fn condition_assumptions_reads_string_keyed_booleans() {
let value = json_to_value(&serde_json::json!({"IsProd": true, "IsDev": false}));
let mut pairs = condition_assumptions(&value);
pairs.sort();
assert_eq!(pairs, vec![("IsDev".to_string(), false), ("IsProd".to_string(), true)]);
}
#[test]
fn condition_assumptions_is_empty_for_non_object() {
assert!(condition_assumptions(&Value::from("not an object")).is_empty());
assert!(condition_assumptions(&Value::Null).is_empty());
}
#[test]
fn condition_assumptions_skips_non_boolean_values() {
let value = json_to_value(&serde_json::json!({"IsProd": true, "Count": 3, "Name": "x"}));
assert_eq!(condition_assumptions(&value), vec![("IsProd".to_string(), true)]);
}
#[test]
fn condition_assumptions_skips_non_string_keys() {
let mut value = Value::new_object();
let object = value.as_object_mut().expect("a fresh object");
object.insert(Value::from("IsProd"), Value::from(true));
object.insert(Value::from(7i64), Value::from(false));
assert_eq!(condition_assumptions(&value), vec![("IsProd".to_string(), true)]);
}
#[test]
fn json_to_value_preserves_diagnostic_object_numbers() {
let obj = serde_json::json!({
"rule_id": "E9999",
"severity": "error",
"message": "m",
"resource_id": "R",
"resource_path": "Properties.X",
"start_line": 12,
"start_column": 3,
"end_line": 12,
"end_column": 9,
});
let round_tripped = serde_json::to_value(json_to_value(&obj)).expect("regorus value serializes");
assert_eq!(round_tripped["start_line"].as_u64(), Some(12));
assert_eq!(round_tripped["end_column"].as_u64(), Some(9));
assert_eq!(round_tripped["rule_id"], serde_json::json!("E9999"));
}
#[test]
fn rego_to_json_round_trips_primitives() {
assert_eq!(rego_to_json(&Value::from("hello")), serde_json::json!("hello"));
assert_eq!(rego_to_json(&Value::from(42i64)), serde_json::json!(42));
assert_eq!(rego_to_json(&Value::from(true)), serde_json::json!(true));
assert_eq!(rego_to_json(&Value::Null), serde_json::Value::Null);
}
#[test]
fn resolved_to_rego_concrete_string() {
let rv = ResolvedValue::Concrete { value: serde_json::json!("test").into() };
assert_eq!(resolved_to_rego(&rv), Value::from("test"));
}
#[test]
fn resolved_to_rego_concrete_number() {
let rv = ResolvedValue::Concrete { value: serde_json::json!(99).into() };
assert_eq!(resolved_to_rego(&rv), Value::from(99i64));
}
#[test]
fn resolved_to_rego_list() {
let rv = ResolvedValue::List {
items: vec![
ResolvedValue::Concrete { value: serde_json::json!(1).into() },
ResolvedValue::Concrete { value: serde_json::json!(2).into() },
],
};
let v = resolved_to_rego(&rv);
let arr = v.as_array().expect("should be array");
assert_eq!(arr.len(), 2);
}
#[test]
fn resolved_to_rego_map() {
let rv = ResolvedValue::Map {
entries: vec![MapEntry {
key: "key".to_string(),
value: ResolvedValue::Concrete { value: serde_json::json!("val").into() },
}],
};
let v = resolved_to_rego(&rv);
v.as_object().expect("resolved_to_rego should produce a valid object");
}
#[test]
fn resolved_to_rego_enum_picks_first_concrete() {
let rv = ResolvedValue::Enum {
variants: vec![
ResolvedValue::Concrete { value: serde_json::json!("first").into() },
ResolvedValue::Concrete { value: serde_json::json!("second").into() },
],
};
assert_eq!(resolved_to_rego(&rv), Value::from("first"));
}
#[test]
fn resolved_to_rego_enum_empty_returns_undefined() {
let rv = ResolvedValue::Enum { variants: vec![] };
assert_eq!(resolved_to_rego(&rv), Value::Undefined);
}
#[test]
fn resolved_to_rego_conditional_returns_true_branch() {
let rv = ResolvedValue::Conditional {
condition: "cond".to_string(),
if_true: Box::new(ResolvedValue::Concrete { value: serde_json::json!("yes").into() }),
if_false: Box::new(ResolvedValue::Concrete { value: serde_json::json!("no").into() }),
};
assert_eq!(resolved_to_rego(&rv), Value::from("yes"));
}
#[test]
fn resolved_to_rego_reference_is_a_marker_object_not_the_target_string() {
let rv = ResolvedValue::Reference { target: "MyBucket".to_string(), kind: RefKind::Ref };
let rendered = resolved_to_rego(&rv);
assert_ne!(rendered, Value::from("MyBucket"), "a logical ID must never masquerade as a literal string");
assert_eq!(rendered, json_to_value(&serde_json::json!({MARKER_REF: "MyBucket"})));
}
#[test]
fn resolved_to_rego_dynamic_returns_undefined() {
let rv = ResolvedValue::Dynamic { reason: "param".to_string() };
assert_eq!(resolved_to_rego(&rv), Value::Undefined);
}
#[test]
fn resolved_to_rego_typed_dynamic_returns_undefined() {
let rv = ResolvedValue::TypedDynamic { reason: "param".to_string(), param_type: "String".to_string() };
assert_eq!(resolved_to_rego(&rv), Value::Undefined);
}
#[test]
fn resolved_all_concrete_returns_single() {
let rv = ResolvedValue::Concrete { value: serde_json::json!("x").into() };
let vals = resolved_all_to_rego(&rv);
assert_eq!(vals.len(), 1);
assert_eq!(vals[0], Value::from("x"));
}
#[test]
fn resolved_all_enum_expands_all() {
let rv = ResolvedValue::Enum {
variants: vec![
ResolvedValue::Concrete { value: serde_json::json!("a").into() },
ResolvedValue::Concrete { value: serde_json::json!("b").into() },
],
};
let vals = resolved_all_to_rego(&rv);
assert_eq!(vals.len(), 2);
}
#[test]
fn resolved_all_conditional_expands_both_branches() {
let rv = ResolvedValue::Conditional {
condition: "c".to_string(),
if_true: Box::new(ResolvedValue::Concrete { value: serde_json::json!("t").into() }),
if_false: Box::new(ResolvedValue::Concrete { value: serde_json::json!("f").into() }),
};
let vals = resolved_all_to_rego(&rv);
assert_eq!(vals.len(), 2);
}
#[test]
fn resolved_all_dynamic_returns_empty() {
let rv = ResolvedValue::Dynamic { reason: "x".to_string() };
assert!(resolved_all_to_rego(&rv).is_empty());
}
#[test]
fn contains_dynamic_concrete_false() {
let rv = ResolvedValue::Concrete { value: serde_json::json!("static").into() };
assert!(!contains_dynamic(&rv));
}
#[test]
fn contains_dynamic_dynamic_true() {
let rv = ResolvedValue::Dynamic { reason: "param".to_string() };
assert!(contains_dynamic(&rv));
}
#[test]
fn contains_dynamic_typed_dynamic_true() {
let rv = ResolvedValue::TypedDynamic { reason: "p".to_string(), param_type: "String".to_string() };
assert!(contains_dynamic(&rv));
}
#[test]
fn contains_dynamic_reference_true() {
let rv = ResolvedValue::Reference { target: "Ref".to_string(), kind: RefKind::Ref };
assert!(contains_dynamic(&rv));
}
#[test]
fn contains_dynamic_nested_list() {
let rv = ResolvedValue::List {
items: vec![
ResolvedValue::Concrete { value: serde_json::json!("ok").into() },
ResolvedValue::Dynamic { reason: "param".to_string() },
],
};
assert!(contains_dynamic(&rv));
}
#[test]
fn contains_dynamic_nested_map() {
let rv = ResolvedValue::Map {
entries: vec![
MapEntry {
key: "a".to_string(),
value: ResolvedValue::Concrete { value: serde_json::json!("ok").into() },
},
MapEntry { key: "b".to_string(), value: ResolvedValue::Dynamic { reason: "p".to_string() } },
],
};
assert!(contains_dynamic(&rv));
}
#[test]
fn contains_dynamic_conditional_true_branch() {
let rv = ResolvedValue::Conditional {
condition: "c".to_string(),
if_true: Box::new(ResolvedValue::Dynamic { reason: "p".to_string() }),
if_false: Box::new(ResolvedValue::Concrete { value: serde_json::json!("ok").into() }),
};
assert!(contains_dynamic(&rv));
}
#[test]
fn resolved_value_to_json_static_concrete() {
let rv = ResolvedValue::Concrete { value: serde_json::json!(42).into() };
assert_eq!(resolved_value_to_json_static(&rv), serde_json::json!(42));
}
#[test]
fn resolved_value_to_json_static_list() {
let rv = ResolvedValue::List { items: vec![ResolvedValue::Concrete { value: serde_json::json!(1).into() }] };
assert_eq!(resolved_value_to_json_static(&rv), serde_json::json!([1]));
}
#[test]
fn resolved_value_to_json_static_map() {
let rv = ResolvedValue::Map {
entries: vec![MapEntry {
key: "k".to_string(),
value: ResolvedValue::Concrete { value: serde_json::json!("v").into() },
}],
};
let j = resolved_value_to_json_static(&rv);
assert_eq!(j["k"], serde_json::json!("v"));
}
#[test]
fn resolved_value_to_json_static_enum_picks_first_concrete() {
let rv = ResolvedValue::Enum {
variants: vec![
ResolvedValue::Dynamic { reason: "x".to_string() },
ResolvedValue::Concrete { value: serde_json::json!("found").into() },
],
};
assert_eq!(resolved_value_to_json_static(&rv), serde_json::json!("found"));
}
#[test]
fn resolved_value_to_json_static_enum_no_concrete_returns_null() {
let rv = ResolvedValue::Enum { variants: vec![ResolvedValue::Dynamic { reason: "x".to_string() }] };
assert_eq!(resolved_value_to_json_static(&rv), serde_json::Value::Null);
}
#[test]
fn resolved_value_to_json_static_conditional_returns_true_branch() {
let rv = ResolvedValue::Conditional {
condition: "c".to_string(),
if_true: Box::new(ResolvedValue::Concrete { value: serde_json::json!("yes").into() }),
if_false: Box::new(ResolvedValue::Concrete { value: serde_json::json!("no").into() }),
};
assert_eq!(resolved_value_to_json_static(&rv), serde_json::json!("yes"));
}
#[test]
fn resolved_value_to_json_static_reference_produces_marker() {
let rv = ResolvedValue::Reference { target: "MyBucket".to_string(), kind: RefKind::Ref };
let j = resolved_value_to_json_static(&rv);
assert_ne!(j.get(MARKER_REF), None, "expected MARKER_REF key");
}
#[test]
fn resolved_value_to_json_static_dynamic_produces_marker() {
let rv = ResolvedValue::Dynamic { reason: "param".to_string() };
let j = resolved_value_to_json_static(&rv);
assert_ne!(j.get(MARKER_DYNAMIC), None, "expected MARKER_DYNAMIC key");
}
#[test]
fn flatten_resolved_list_concrete_array() {
let rv = ResolvedValue::Concrete { value: serde_json::json!([1, 2, 3]).into() };
let items = flatten_resolved_list(&rv);
assert_eq!(items.len(), 3);
}
#[test]
fn flatten_resolved_list_ir_list() {
let rv = ResolvedValue::List {
items: vec![
ResolvedValue::Concrete { value: serde_json::json!("a").into() },
ResolvedValue::Concrete { value: serde_json::json!("b").into() },
],
};
let items = flatten_resolved_list(&rv);
assert_eq!(items.len(), 2);
}
#[test]
fn flatten_resolved_list_conditional_merges_branches() {
let rv = ResolvedValue::Conditional {
condition: "c".to_string(),
if_true: Box::new(ResolvedValue::List {
items: vec![ResolvedValue::Concrete { value: serde_json::json!("a").into() }],
}),
if_false: Box::new(ResolvedValue::List {
items: vec![ResolvedValue::Concrete { value: serde_json::json!("b").into() }],
}),
};
let items = flatten_resolved_list(&rv);
assert_eq!(items.len(), 2);
}
#[test]
fn flatten_resolved_list_scalar_wraps() {
let rv = ResolvedValue::Concrete { value: serde_json::json!("scalar").into() };
let items = flatten_resolved_list(&rv);
assert_eq!(items.len(), 1);
}
#[test]
fn flatten_resolved_list_empty_array() {
let rv = ResolvedValue::Concrete { value: serde_json::json!([]).into() };
assert!(flatten_resolved_list(&rv).is_empty());
}
fn eval_builtin(expr: &str) -> Value {
let mut rego = regorus::Engine::new();
rego.set_strict_builtin_errors(false);
register_all(&mut rego, Arc::new(SchemaMetadataCatalog::new()), shared_base_getatt_data().unwrap())
.expect("register builtins");
let policy = format!("package test\nimport rego.v1\nresult := {}", expr);
rego.add_policy("test.rego".into(), policy).unwrap();
rego.set_input(Value::new_object());
rego.eval_rule("data.test.result".into()).unwrap()
}
#[test]
fn arn_matches_exact() {
let v = eval_builtin(r#"arn_matches("arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket")"#);
assert_eq!(v, Value::from(true));
}
#[test]
fn arn_matches_wildcard_region() {
let v = eval_builtin(r#"arn_matches("arn:aws:s3:us-east-1:123:bucket", "arn:aws:s3:*:123:bucket")"#);
assert_eq!(v, Value::from(true));
}
#[test]
fn arn_matches_mismatch() {
let v = eval_builtin(r#"arn_matches("arn:aws:s3:::my-bucket", "arn:aws:ec2:::my-bucket")"#);
assert_eq!(v, Value::from(false));
}
#[test]
fn arn_matches_too_few_parts() {
let v = eval_builtin(r#"arn_matches("arn:aws", "arn:aws:s3:::bucket")"#);
assert_eq!(v, Value::from(false));
}
#[test]
fn arn_matches_all_wildcards() {
let v = eval_builtin(r#"arn_matches("arn:aws:s3:us-east-1:123:bucket", "arn:*:*:*:*:*")"#);
assert_eq!(v, Value::from(true));
}
#[test]
fn ip_overlaps_same_cidr() {
let v = eval_builtin(r#"ip_overlaps("10.0.0.0/24", "10.0.0.0/24")"#);
assert_eq!(v, Value::from(true));
}
#[test]
fn ip_overlaps_supernet_contains_subnet() {
let v = eval_builtin(r#"ip_overlaps("10.0.0.0/16", "10.0.1.0/24")"#);
assert_eq!(v, Value::from(true));
}
#[test]
fn ip_overlaps_disjoint() {
let v = eval_builtin(r#"ip_overlaps("10.0.0.0/24", "10.0.1.0/24")"#);
assert_eq!(v, Value::from(false));
}
#[test]
fn ip_subnet_of_true() {
let v = eval_builtin(r#"ip_subnet_of("10.0.1.0/24", "10.0.0.0/16")"#);
assert_eq!(v, Value::from(true));
}
#[test]
fn ip_subnet_of_false_disjoint() {
let v = eval_builtin(r#"ip_subnet_of("192.168.0.0/24", "10.0.0.0/16")"#);
assert_eq!(v, Value::from(false));
}
#[test]
fn ip_subnet_of_same_network() {
let v = eval_builtin(r#"ip_subnet_of("10.0.0.0/16", "10.0.0.0/16")"#);
assert_eq!(v, Value::from(true));
}
#[test]
fn is_valid_cidr_strict_valid_network() {
let v = eval_builtin(r#"is_valid_cidr_strict("10.0.0.0/24")"#);
assert_eq!(v, Value::from(true));
}
#[test]
fn is_valid_cidr_strict_host_bits_set() {
let v = eval_builtin(r#"is_valid_cidr_strict("10.0.0.1/24")"#);
assert_eq!(v, Value::from(false));
}
#[test]
fn is_valid_cidr_strict_slash_32() {
let v = eval_builtin(r#"is_valid_cidr_strict("10.0.0.1/32")"#);
assert_eq!(v, Value::from(true));
}
#[test]
fn is_valid_cidr_strict_slash_0() {
let v = eval_builtin(r#"is_valid_cidr_strict("0.0.0.0/0")"#);
assert_eq!(v, Value::from(true));
}
#[test]
fn is_valid_cidr_strict_slash_0_with_host_bits() {
let v = eval_builtin(r#"is_valid_cidr_strict("10.0.0.0/0")"#);
assert_eq!(v, Value::from(false));
}
#[test]
fn is_valid_cidr_strict_invalid_string() {
let v = eval_builtin(r#"is_valid_cidr_strict("not-a-cidr")"#);
assert_eq!(v, Value::from(false));
}
#[test]
fn ensure_list_scalar_wraps() {
let v = eval_builtin(r#"ensure_list("hello")"#);
let arr = v.as_array().expect("should be array");
assert_eq!(arr.len(), 1);
}
#[test]
fn ensure_list_array_passthrough() {
let v = eval_builtin(r#"ensure_list(["a", "b"])"#);
let arr = v.as_array().expect("should be array");
assert_eq!(arr.len(), 2);
}
#[test]
fn ensure_list_number_wraps() {
let v = eval_builtin(r#"ensure_list(42)"#);
let arr = v.as_array().expect("should be array");
assert_eq!(arr.len(), 1);
}
#[test]
fn coerce_to_number_from_string() {
let v = eval_builtin(r#"coerce_to_number("42")"#);
assert_eq!(v, Value::from(42i64));
}
#[test]
fn coerce_to_number_from_int() {
let v = eval_builtin(r#"coerce_to_number(7)"#);
assert_eq!(v, Value::from(7i64));
}
#[test]
fn coerce_to_number_from_bool_returns_undefined() {
let v = eval_builtin(r#"coerce_to_number(true)"#);
assert_eq!(v, Value::Undefined);
}
#[test]
fn coerce_to_number_from_non_numeric_string() {
let v = eval_builtin(r#"coerce_to_number("abc")"#);
assert_eq!(v, Value::Undefined);
}
#[test]
fn coerce_to_string_from_number() {
let v = eval_builtin(r#"coerce_to_string(42)"#);
assert_eq!(v, Value::from("42"));
}
#[test]
fn coerce_to_string_from_bool() {
let v = eval_builtin(r#"coerce_to_string(true)"#);
assert_eq!(v, Value::from("true"));
}
#[test]
fn coerce_to_string_from_string() {
let v = eval_builtin(r#"coerce_to_string("hello")"#);
assert_eq!(v, Value::from("hello"));
}
#[test]
fn cfn_type_compatible_string_to_string() {
let v = eval_builtin(r#"cfn_type_compatible("hello", "string")"#);
assert_eq!(v, Value::from(true));
}
#[test]
fn cfn_type_compatible_number_string_to_number() {
let v = eval_builtin(r#"cfn_type_compatible("42", "number")"#);
assert_eq!(v, Value::from(true));
}
#[test]
fn cfn_type_compatible_bool_to_string() {
let v = eval_builtin(r#"cfn_type_compatible(true, "string")"#);
assert_eq!(v, Value::from(false));
}
#[test]
fn input_region_returns_null_when_unset() {
let v = eval_builtin(r#"input_region()"#);
assert_eq!(v, Value::Null);
}
#[test]
fn input_region_returns_value_when_set() {
let mut rego = regorus::Engine::new();
rego.set_strict_builtin_errors(false);
register_all(&mut rego, Arc::new(SchemaMetadataCatalog::new()), shared_base_getatt_data().unwrap())
.expect("register builtins");
rego.add_policy("test.rego".into(), "package test\nimport rego.v1\nresult := input_region()".into()).unwrap();
rego.set_input(Value::new_object());
let model =
Arc::new(SemanticModel::from_bytes(b"AWSTemplateFormatVersion: '2010-09-09'\nResources: {}").unwrap());
let _scope = crate::eval_context::EvaluationScope::enter(crate::eval_context::EvaluationContext::new(
model,
Some("us-west-2".to_string()),
Arc::default(),
));
let v = rego.eval_rule("data.test.result".into()).unwrap();
assert_eq!(v, Value::from("us-west-2"));
}
#[test]
fn regex_match_override_matches_and_rejects_like_the_builtin() {
assert_eq!(eval_builtin(r#"regex.match("^abcde$", "abcde")"#), Value::from(true));
assert_eq!(eval_builtin(r#"regex.match("^abc$", "abcde")"#), Value::from(false));
}
#[test]
fn regex_match_override_is_unanchored() {
assert_eq!(eval_builtin(r#"regex.match("cd", "abcde")"#), Value::from(true));
assert_eq!(eval_builtin(r#"regex.match("^cd", "abcde")"#), Value::from(false));
}
#[test]
fn regex_match_override_returns_undefined_for_an_invalid_pattern() {
assert_eq!(eval_builtin(r#"regex.match("(unterminated", "anything")"#), Value::Undefined);
}
#[test]
fn regex_cache_reports_match_nonmatch_and_invalid() {
let mut cache = RegexCache::new();
assert_eq!(cache.is_match("^a.*z$", "abcz"), Some(true));
assert_eq!(cache.is_match("^a.*z$", "nope"), Some(false));
assert_eq!(cache.is_match("(unterminated", "x"), None);
assert_eq!(cache.is_match("(unterminated", "y"), None);
assert_eq!(cache.is_match("^a.*z$", "az"), Some(true));
}
#[test]
fn regex_cache_stays_bounded_under_many_distinct_patterns() {
let mut cache = RegexCache::new();
for i in 0..(REGEX_CACHE_CAPACITY + 50) {
let pattern = format!("^pattern{i}$");
assert_eq!(cache.is_match(&pattern, &format!("pattern{i}")), Some(true));
}
assert!(cache.compiled.len() <= REGEX_CACHE_CAPACITY, "compiled map must stay within capacity");
assert_eq!(cache.compiled.len(), cache.insertion_order.len(), "cache maps must stay in step");
}
#[test]
fn resolved_value_to_json_static_typed_dynamic_produces_both_markers() {
let rv = ResolvedValue::TypedDynamic {
reason: "param".to_string(),
param_type: "AWS::SSM::Parameter::Value<String>".to_string(),
};
let j = resolved_value_to_json_static(&rv);
assert_ne!(j.get(MARKER_DYNAMIC), None, "expected MARKER_DYNAMIC key");
assert_ne!(j.get(MARKER_PARAM_TYPE), None, "expected MARKER_PARAM_TYPE key");
assert_eq!(j[MARKER_PARAM_TYPE], "AWS::SSM::Parameter::Value<String>");
}
#[test]
fn resolved_all_list_returns_single_array() {
let rv = ResolvedValue::List {
items: vec![
ResolvedValue::Concrete { value: serde_json::json!(1).into() },
ResolvedValue::Concrete { value: serde_json::json!(2).into() },
],
};
let vals = resolved_all_to_rego(&rv);
assert_eq!(vals.len(), 1, "List wraps into a single array value");
vals[0].as_array().expect("first element should be an array");
}
#[test]
fn resolved_all_map_returns_single_object() {
let rv = ResolvedValue::Map {
entries: vec![MapEntry {
key: "k".to_string(),
value: ResolvedValue::Concrete { value: serde_json::json!("v").into() },
}],
};
let vals = resolved_all_to_rego(&rv);
assert_eq!(vals.len(), 1, "Map wraps into a single object value");
}
#[test]
fn resolved_all_reference_returns_empty() {
let rv = ResolvedValue::Reference { target: "Target".to_string(), kind: RefKind::Ref };
assert!(resolved_all_to_rego(&rv).is_empty());
}
#[test]
fn resolved_all_typed_dynamic_returns_empty() {
let rv = ResolvedValue::TypedDynamic { reason: "p".to_string(), param_type: "String".to_string() };
assert!(resolved_all_to_rego(&rv).is_empty());
}
#[test]
fn flatten_resolved_list_enum_flattens_all_variants() {
let rv = ResolvedValue::Enum {
variants: vec![
ResolvedValue::Concrete { value: serde_json::json!([1, 2]).into() },
ResolvedValue::Concrete { value: serde_json::json!([3]).into() },
],
};
let items = flatten_resolved_list(&rv);
assert_eq!(items.len(), 3, "Enum should flatten all array variants");
}
#[test]
fn contains_dynamic_enum_all_static_false() {
let rv = ResolvedValue::Enum {
variants: vec![
ResolvedValue::Concrete { value: serde_json::json!("a").into() },
ResolvedValue::Concrete { value: serde_json::json!("b").into() },
],
};
assert!(!contains_dynamic(&rv));
}
#[test]
fn contains_dynamic_conditional_false_branch_only() {
let rv = ResolvedValue::Conditional {
condition: "c".to_string(),
if_true: Box::new(ResolvedValue::Concrete { value: serde_json::json!("ok").into() }),
if_false: Box::new(ResolvedValue::Dynamic { reason: "p".to_string() }),
};
assert!(contains_dynamic(&rv));
}
#[test]
fn ip_subnet_of_mixed_v4_v6_returns_false() {
let v = eval_builtin(r#"ip_subnet_of("10.0.0.0/24", "::1/128")"#);
assert_eq!(v, Value::from(false));
}
#[test]
fn is_valid_cidr_strict_ipv6_valid() {
let v = eval_builtin(r#"is_valid_cidr_strict("2001:db8::/32")"#);
assert_eq!(v, Value::from(true));
}
#[test]
fn coerce_to_number_from_float_string() {
let v = eval_builtin(r#"coerce_to_number("3.14")"#);
assert_eq!(v, Value::from(3.14f64));
}
#[test]
fn coerce_to_string_from_null_returns_undefined() {
let v = eval_builtin(r#"coerce_to_string(null)"#);
assert_eq!(v, Value::Undefined);
}
#[test]
fn ensure_list_null_wraps() {
let v = eval_builtin(r#"ensure_list(null)"#);
let arr = v.as_array().expect("should be array");
assert_eq!(arr.len(), 1);
}
#[test]
fn ensure_list_bool_wraps() {
let v = eval_builtin(r#"ensure_list(true)"#);
let arr = v.as_array().expect("should be array");
assert_eq!(arr.len(), 1);
}
}