1use crate::eval_context::{EvaluationContext, EvaluationScope, ReferenceRendering, ReferenceRenderingScope};
2use crate::policies;
3use data_source::embedded;
4use data_source::types::KnownResourceTypes;
5use diagnostics::{Diagnostic, PhaseMetric, phase_metric};
6use log::{debug, info};
7use rules::{RuleInfo, RuleMetadataEntry, RuleOrigin, build_rule_metadata_map};
8use schema_validator::{
9 OverlayCatalog, SchemaMetadataCatalog, SchemaValidator, getatt_data_with_overlays,
10 schema_metadata_catalog_with_overlays,
11};
12use std::collections::{HashMap, HashSet};
13use std::str::from_utf8;
14use std::sync::{Arc, LazyLock, Mutex};
15use template_model::SemanticModel;
16use validation_engine::{
17 EngineConfig, GuardRuleSet, ValidateConfig, ValidationEngine, ValidationError, build_rule_list,
18 extract_diagnostics_from_value, semantic_model_to_input_json,
19};
20
21static REGORUS_DATA: LazyLock<Vec<(&str, &[u8])>> = LazyLock::new(|| {
22 vec![
23 (KNOWN_RESOURCE_TYPES_PATH, &*embedded::KNOWN_RESOURCE_TYPES_BYTES),
24 (PRIMARY_IDENTIFIERS_PATH, &*embedded::PRIMARY_IDENTIFIERS_BYTES),
25 ("data/iam_action_resource_patterns", &*embedded::IAM_ACTION_RESOURCE_PATTERNS_BYTES),
26 ("data/stateful_resource_types", &*embedded::STATEFUL_RESOURCE_TYPES_BYTES),
27 ("data/aws_rds_dbinstance_dbinstanceclass_enum", &*embedded::AWS_RDS_DBINSTANCE_DBINSTANCECLASS_ENUM_BYTES),
28 ("data/aws_ec2_instance_instancetype_enum", &*embedded::AWS_EC2_INSTANCE_INSTANCETYPE_ENUM_BYTES),
29 (
30 "data/aws_emr_cluster_instancetypeconfig_instancetype_enum",
31 &*embedded::AWS_EMR_CLUSTER_INSTANCETYPECONFIG_INSTANCETYPE_ENUM_BYTES,
32 ),
33 ("data/aws_gamelift_fleet_ec2instancetype_enum", &*embedded::AWS_GAMELIFT_FLEET_EC2INSTANCETYPE_ENUM_BYTES),
34 ("data/aws_appstream_fleet_instancetype_enum", &*embedded::AWS_APPSTREAM_FLEET_INSTANCETYPE_ENUM_BYTES),
35 ("data/aws_dax_cluster_nodetype_enum", &*embedded::AWS_DAX_CLUSTER_NODETYPE_ENUM_BYTES),
36 ("data/aws_docdb_dbinstance_dbinstanceclass_enum", &*embedded::AWS_DOCDB_DBINSTANCE_DBINSTANCECLASS_ENUM_BYTES),
37 (
38 "data/aws_elasticache_cachecluster_cachenodetype_enum",
39 &*embedded::AWS_ELASTICACHE_CACHECLUSTER_CACHENODETYPE_ENUM_BYTES,
40 ),
41 (
42 "data/aws_managedblockchain_node_nodeconfiguration_instancetype_enum",
43 &*embedded::AWS_MANAGEDBLOCKCHAIN_NODE_NODECONFIGURATION_INSTANCETYPE_ENUM_BYTES,
44 ),
45 (
46 "data/aws_neptune_dbinstance_dbinstanceclass_enum",
47 &*embedded::AWS_NEPTUNE_DBINSTANCE_DBINSTANCECLASS_ENUM_BYTES,
48 ),
49 (
50 "data/aws_rds_dbcluster_dbclusterinstanceclass_enum",
51 &*embedded::AWS_RDS_DBCLUSTER_DBCLUSTERINSTANCECLASS_ENUM_BYTES,
52 ),
53 ("data/aws_rds_dbinstance_db_instance_class", &*embedded::AWS_RDS_DBINSTANCE_DB_INSTANCE_CLASS_BYTES),
54 ("data/aws_redshift_cluster_nodetype_enum", &*embedded::AWS_REDSHIFT_CLUSTER_NODETYPE_ENUM_BYTES),
55 ("data/aws_amazonmq_broker_instancetype_enum", &*embedded::AWS_AMAZONMQ_BROKER_INSTANCETYPE_ENUM_BYTES),
56 (
57 "data/aws_sagemaker_processing_instancetype_enum",
58 &*embedded::AWS_SAGEMAKER_PROCESSING_INSTANCETYPE_ENUM_BYTES,
59 ),
60 ("data/aws_sagemaker_hosting_instancetype_enum", &*embedded::AWS_SAGEMAKER_HOSTING_INSTANCETYPE_ENUM_BYTES),
61 ("data/aws_sagemaker_transform_instancetype_enum", &*embedded::AWS_SAGEMAKER_TRANSFORM_INSTANCETYPE_ENUM_BYTES),
62 ("data/aws_sagemaker_cluster_instancetype_enum", &*embedded::AWS_SAGEMAKER_CLUSTER_INSTANCETYPE_ENUM_BYTES),
63 (
64 "data/aws_elasticsearch_domain_elasticsearchclusterconfig_instancetype_enum",
65 &*embedded::AWS_ELASTICSEARCH_DOMAIN_ELASTICSEARCHCLUSTERCONFIG_INSTANCETYPE_ENUM_BYTES,
66 ),
67 (
68 "data/aws_opensearchservice_domain_clusterconfig_instancetype_enum",
69 &*embedded::AWS_OPENSEARCHSERVICE_DOMAIN_CLUSTERCONFIG_INSTANCETYPE_ENUM_BYTES,
70 ),
71 (GETATT_ATTRIBUTES_PATH, &*embedded::GETATT_ATTRIBUTES_BYTES),
72 ("data/codepipeline_action_artifact_counts", &*embedded::CODEPIPELINE_ACTION_ARTIFACT_COUNTS_BYTES),
73 ("data/deprecated_resource_types", &*embedded::DEPRECATED_RESOURCE_TYPES_BYTES),
74 ("data/retention_period_requirements", &*embedded::RETENTION_PERIOD_REQUIREMENTS_BYTES),
75 ("data/sensitive_ports", &*embedded::SENSITIVE_PORTS_BYTES),
76 ("data/secretsmanager_arn_fields", &*embedded::SECRETSMANAGER_ARN_FIELDS_BYTES),
77 ("data/rule_data", &*embedded::RULE_DATA_BYTES),
78 ("data/rule_tables", &*embedded::RULE_TABLES_BYTES),
79 ]
80});
81
82const CORE_PACKAGES: &[&str] = &[
85 "data.structure.violation",
86 "data.intrinsics.violation",
87 "data.references.violation",
88 "data.best_practices.violation",
89 "data.resources.violation",
90];
91
92const KNOWN_RESOURCE_TYPES_PATH: &str = "data/known_resource_types";
95
96const GETATT_ATTRIBUTES_PATH: &str = "data/getatt_attributes";
98
99const PRIMARY_IDENTIFIERS_PATH: &str = "data/primary_identifiers";
101
102fn extend_known_resource_types(extra_types: &[String]) -> anyhow::Result<Option<String>> {
106 if extra_types.is_empty() {
107 return Ok(None);
108 }
109 let mut catalog: KnownResourceTypes = serde_json::from_slice(&embedded::KNOWN_RESOURCE_TYPES_BYTES)
110 .map_err(|e| anyhow::anyhow!("Failed to parse the embedded known_resource_types data: {e}"))?;
111 anyhow::ensure!(!catalog.known_resource_types.is_empty(), "Embedded known_resource_types data must not be empty");
112 for type_name in extra_types {
113 if !catalog.known_resource_types.contains(type_name) {
114 catalog.known_resource_types.push(type_name.clone());
115 }
116 }
117 Ok(Some(serde_json::to_string(&catalog)?))
118}
119
120fn extend_getatt_data(catalog: &OverlayCatalog) -> anyhow::Result<Option<String>> {
123 if catalog.getatt_attributes.is_empty() && catalog.getatt_attribute_types.is_empty() {
124 return Ok(None);
125 }
126 let mut data: serde_json::Value = serde_json::from_slice(&embedded::GETATT_ATTRIBUTES_BYTES)
127 .map_err(|e| anyhow::anyhow!("Failed to parse the embedded getatt_attributes data: {e}"))?;
128
129 if let Some(attrs_obj) = data.get_mut("getatt_attributes").and_then(|v| v.as_object_mut()) {
131 for (type_name, attrs) in &catalog.getatt_attributes {
132 let entry = attrs_obj.entry(type_name.clone()).or_insert_with(|| serde_json::Value::Array(Vec::new()));
133 if let Some(arr) = entry.as_array_mut() {
134 for attr in attrs {
135 let val = serde_json::Value::String(attr.clone());
136 if !arr.contains(&val) {
137 arr.push(val);
138 }
139 }
140 arr.sort_by(|a, b| a.as_str().unwrap_or("").cmp(b.as_str().unwrap_or("")));
141 arr.dedup();
142 }
143 }
144 }
145
146 if let Some(types_obj) = data.get_mut("getatt_attribute_types").and_then(|v| v.as_object_mut()) {
148 for (type_name, attr_types) in &catalog.getatt_attribute_types {
149 let entry =
150 types_obj.entry(type_name.clone()).or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
151 if let Some(obj) = entry.as_object_mut() {
152 for (attr, atype) in attr_types {
153 obj.insert(attr.clone(), serde_json::Value::String(atype.clone()));
154 }
155 }
156 }
157 }
158
159 Ok(Some(serde_json::to_string(&data)?))
160}
161
162fn extend_primary_identifiers_data(catalog: &OverlayCatalog) -> anyhow::Result<Option<String>> {
165 if catalog.primary_identifiers.is_empty() {
166 return Ok(None);
167 }
168 let mut data: serde_json::Value = serde_json::from_slice(&embedded::PRIMARY_IDENTIFIERS_BYTES)
169 .map_err(|e| anyhow::anyhow!("Failed to parse the embedded primary_identifiers data: {e}"))?;
170
171 if let Some(pids_obj) = data.get_mut("primary_identifiers").and_then(|v| v.as_object_mut()) {
172 for (type_name, pids) in &catalog.primary_identifiers {
173 pids_obj.insert(type_name.clone(), serde_json::json!(pids));
174 }
175 }
176
177 Ok(Some(serde_json::to_string(&data)?))
178}
179
180#[derive(Clone, Copy, PartialEq, Eq)]
188enum BuiltinRuleMode {
189 Enabled,
190 ExternalOnly,
191}
192
193#[derive(Clone, Copy)]
197enum PolicyPackageKind {
198 BuiltIn,
200 Custom,
202}
203
204impl PolicyPackageKind {
205 fn source_label(self) -> &'static str {
206 match self {
207 Self::BuiltIn => "Core",
208 Self::Custom => "Custom",
209 }
210 }
211
212 fn rule_origin_override(self) -> Option<&'static RuleOrigin> {
215 match self {
216 Self::BuiltIn => None,
217 Self::Custom => Some(&RuleOrigin::Custom),
218 }
219 }
220
221 fn reference_rendering(self) -> ReferenceRendering {
225 match self {
226 Self::BuiltIn => ReferenceRendering::Marker,
227 Self::Custom => ReferenceRendering::TargetId,
228 }
229 }
230}
231
232pub struct RegoEngine {
233 base_rego: regorus::Engine,
234 builtin_mode: BuiltinRuleMode,
237 registry_metadata: HashMap<String, RuleMetadataEntry>,
239 discovered_custom_metadata: Mutex<HashMap<String, RuleMetadataEntry>>,
243 custom_packages: Vec<String>,
244 guard_rules: Option<GuardRuleSet>,
247 init_metric: PhaseMetric,
248}
249
250impl RegoEngine {
251 pub fn new(config: EngineConfig) -> anyhow::Result<Self> {
252 let overlay_catalog =
253 config.build_overlay_catalog().map_err(|e| anyhow::anyhow!("Failed to build overlay catalog: {e}"))?;
254 let start = web_time::Instant::now();
255 let schema_metadata = schema_metadata_catalog_with_overlays(&overlay_catalog)?;
256 Self::new_from_parts(config, &overlay_catalog, schema_metadata, BuiltinRuleMode::Enabled, start)
257 }
258
259 #[doc(hidden)]
270 pub fn new_with_schema_validator(config: EngineConfig, validator: &SchemaValidator) -> anyhow::Result<Self> {
271 let start = web_time::Instant::now();
272 let schema_metadata = validator.schema_metadata_catalog()?;
273 Self::new_from_parts(config, validator.overlay_catalog(), schema_metadata, BuiltinRuleMode::Enabled, start)
274 }
275
276 pub fn new_external_only(config: EngineConfig) -> anyhow::Result<Self> {
284 let overlay_catalog =
285 config.build_overlay_catalog().map_err(|e| anyhow::anyhow!("Failed to build overlay catalog: {e}"))?;
286 let start = web_time::Instant::now();
287 let schema_metadata = schema_metadata_catalog_with_overlays(&overlay_catalog)?;
288 Self::new_from_parts(config, &overlay_catalog, schema_metadata, BuiltinRuleMode::ExternalOnly, start)
289 }
290
291 #[doc(hidden)]
295 pub fn new_external_only_with_schema_validator(
296 config: EngineConfig,
297 validator: &SchemaValidator,
298 ) -> anyhow::Result<Self> {
299 let start = web_time::Instant::now();
300 let schema_metadata = validator.schema_metadata_catalog()?;
301 Self::new_from_parts(config, validator.overlay_catalog(), schema_metadata, BuiltinRuleMode::ExternalOnly, start)
302 }
303
304 fn new_from_parts(
307 config: EngineConfig,
308 overlay_catalog: &OverlayCatalog,
309 schema_metadata: Arc<SchemaMetadataCatalog>,
310 builtin_mode: BuiltinRuleMode,
311 start: web_time::Instant,
312 ) -> anyhow::Result<Self> {
313 let mut rego = regorus::Engine::new();
314 rego.set_strict_builtin_errors(false);
315
316 {
318 let overlay_types: Vec<String> = overlay_catalog.type_names.clone();
322 let extended_known_types = extend_known_resource_types(&overlay_types)?;
323
324 let extended_getatt = extend_getatt_data(overlay_catalog)?;
326 let extended_primary_ids = extend_primary_identifiers_data(overlay_catalog)?;
328
329 let merged_capacity: usize = REGORUS_DATA.iter().map(|(_, json_bytes)| json_bytes.len()).sum();
332 let mut merged = String::with_capacity(merged_capacity);
333 merged.push('{');
334 for (i, (path, json_bytes)) in REGORUS_DATA.iter().enumerate() {
335 let json_str = match (
336 *path,
337 extended_known_types.as_deref(),
338 extended_getatt.as_deref(),
339 extended_primary_ids.as_deref(),
340 ) {
341 (KNOWN_RESOURCE_TYPES_PATH, Some(extended), _, _) => extended,
342 (GETATT_ATTRIBUTES_PATH, _, Some(extended), _) => extended,
343 (PRIMARY_IDENTIFIERS_PATH, _, _, Some(extended)) => extended,
344 _ => from_utf8(json_bytes).map_err(|error| {
345 anyhow::anyhow!("Embedded JSON data '{}' is not valid UTF-8: {}", path, error)
346 })?,
347 };
348 let inner = json_str
349 .trim()
350 .strip_prefix('{')
351 .and_then(|value| value.strip_suffix('}'))
352 .ok_or_else(|| anyhow::anyhow!("Embedded JSON data '{}' must be a top-level object", path))?;
353 anyhow::ensure!(!inner.trim().is_empty(), "Embedded JSON data '{}' must not be empty", path);
354 if i > 0 {
355 merged.push(',');
356 }
357 merged.push_str(inner);
358 }
359 merged.push('}');
360 rego.add_data(regorus::Value::from_json_str(&merged)?)?;
361 }
362
363 let handwritten_rule_count = match builtin_mode {
368 BuiltinRuleMode::Enabled => {
369 for (path, source) in policies::HANDWRITTEN_REGO_POLICIES {
370 rego.add_policy(path.to_string(), source.to_string())?;
371 }
372 policies::HANDWRITTEN_REGO_POLICIES.len()
373 }
374 BuiltinRuleMode::ExternalOnly => 0,
375 };
376 debug!("Loaded {} data files, {} handwritten rules", REGORUS_DATA.len(), handwritten_rule_count);
377
378 let guard_rules = GuardRuleSet::compile(&config.guard_rules).map_err(|error| anyhow::anyhow!(error))?;
379
380 for entry in &config.custom_rules {
381 rego.add_policy(entry.name.clone(), entry.content.clone())?;
382 }
383 let mut custom_packages = Vec::new();
384 for entry in &config.custom_rules {
385 for line in entry.content.lines() {
386 let trimmed = line.trim();
387 if let Some(pkg) = trimmed.strip_prefix("package ") {
388 let eval_path = format!("data.{}.violation", pkg.trim());
389 if !custom_packages.contains(&eval_path) {
390 custom_packages.push(eval_path);
391 }
392 }
393 }
394 }
395
396 let getatt = getatt_data_with_overlays(overlay_catalog)?;
400 crate::builtins::register_all(&mut rego, Arc::clone(&schema_metadata), getatt)?;
401
402 let registry_metadata = match builtin_mode {
405 BuiltinRuleMode::Enabled => build_rule_metadata_map(),
406 BuiltinRuleMode::ExternalOnly => HashMap::new(),
407 };
408
409 let _ = rego.eval_query("true".to_string(), false);
417
418 info!(
419 "RegoEngine initialized: {} handwritten rules, {} data files, {} registry + {} Guard metadata entries",
420 handwritten_rule_count,
421 REGORUS_DATA.len(),
422 registry_metadata.len(),
423 guard_rules.as_ref().map_or(0, |guard| guard.rule_metadata().len())
424 );
425 let init_metric = phase_metric(start);
426 Ok(RegoEngine {
427 base_rego: rego,
428 builtin_mode,
429 registry_metadata,
430 discovered_custom_metadata: Mutex::new(HashMap::new()),
431 custom_packages,
432 guard_rules,
433 init_metric,
434 })
435 }
436
437 fn eval_package_into(
448 &self,
449 rego: &mut regorus::Engine,
450 package: &str,
451 kind: PolicyPackageKind,
452 model: &SemanticModel,
453 out: &mut Vec<Diagnostic>,
454 ) -> Result<(), ValidationError> {
455 let source_label = kind.source_label();
456 let _rendering = ReferenceRenderingScope::enter(kind.reference_rendering());
457 let value = rego.eval_rule(package.to_string()).map_err(|e| {
458 ValidationError::Engine(format!("{source_label} rule package '{package}' failed to evaluate: {e}"))
459 })?;
460 let diagnostics_json = serde_json::to_value(&value).map_err(|e| {
461 ValidationError::Engine(format!(
462 "{source_label} rule package '{package}' produced a result that could not be \
463 serialized to JSON: {e}"
464 ))
465 })?;
466 extract_diagnostics_from_value(&diagnostics_json, model, out, kind.rule_origin_override())
467 .map_err(ValidationError::from)
468 }
469
470 fn globally_suppressed_builtin_rules(&self, config: &ValidateConfig) -> Arc<HashSet<String>> {
475 let suppressed: HashSet<String> = self
476 .registry_metadata
477 .iter()
478 .filter(|(rule_id, entry)| {
479 config.filters.globally_suppresses_builtin_rule(
480 rule_id,
481 entry.category.as_deref(),
482 entry.severity,
483 config.strict,
484 config.severity_level,
485 )
486 })
487 .map(|(rule_id, _)| rule_id.clone())
488 .collect();
489 Arc::new(suppressed)
490 }
491}
492
493impl ValidationEngine for RegoEngine {
494 fn engine_name(&self) -> &str {
495 "rego"
496 }
497
498 fn evaluate_rules(
499 &self,
500 model: &Arc<SemanticModel>,
501 config: &ValidateConfig,
502 ) -> Result<Vec<Diagnostic>, ValidationError> {
503 let context = EvaluationContext::new(
504 model.clone(),
505 config.pseudo_parameter_overrides.region.clone(),
506 self.globally_suppressed_builtin_rules(config),
507 );
508 let _scope = EvaluationScope::enter(context);
509
510 let mut rego = self.base_rego.clone();
511
512 let input_json = semantic_model_to_input_json(model)?;
513 let input_value = crate::builtins::serde_json_to_rego_value(&input_json);
514 rego.set_input(input_value);
515
516 let mut diagnostics = Vec::new();
517
518 let evaluate_builtins = matches!(self.builtin_mode, BuiltinRuleMode::Enabled) && !config.disable_builtin_rules;
522 if evaluate_builtins {
523 for package in CORE_PACKAGES {
533 self.eval_package_into(&mut rego, package, PolicyPackageKind::BuiltIn, model, &mut diagnostics)?;
534 }
535 }
536
537 for pkg in &self.custom_packages {
538 self.eval_package_into(&mut rego, pkg, PolicyPackageKind::Custom, model, &mut diagnostics)?;
539 }
540
541 if let Some(guard_rules) = &self.guard_rules {
542 diagnostics.extend(guard_rules.evaluate(model)?);
543 }
544
545 if !self.custom_packages.is_empty() {
546 let mut discovered = self.discovered_custom_metadata.lock().unwrap_or_else(|e| e.into_inner());
547 for d in &diagnostics {
548 if d.source == RuleOrigin::Custom {
549 discovered.entry(d.rule_id.clone()).or_insert_with(|| RuleMetadataEntry {
550 category: d.category.clone(),
551 description: d.message.clone(),
552 severity: d.severity,
553 origin: RuleOrigin::Custom,
554 });
555 }
556 }
557 }
558
559 Ok(diagnostics)
560 }
561
562 fn list_rules(&self) -> Vec<RuleInfo> {
563 build_rule_list(&self.registry_metadata, &self.external_rule_metadata())
564 }
565
566 fn rule_metadata(&self) -> &HashMap<String, RuleMetadataEntry> {
567 &self.registry_metadata
568 }
569
570 fn external_rule_metadata(&self) -> HashMap<String, RuleMetadataEntry> {
575 let mut merged = self.guard_rules.as_ref().map(|guard| guard.rule_metadata().clone()).unwrap_or_default();
576 if !self.custom_packages.is_empty() {
577 let discovered = self.discovered_custom_metadata.lock().unwrap_or_else(|e| e.into_inner());
578 merged.extend(discovered.iter().map(|(k, v)| (k.clone(), v.clone())));
579 }
580 merged
581 }
582
583 fn init_metric(&self) -> &PhaseMetric {
584 &self.init_metric
585 }
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591 use diagnostics::DetailLevel;
592 use rules::{Category, FilterConfig, RuleFilterConfig, Severity, lookup_rule};
593 use std::sync::Barrier;
594 use std::thread;
595 use template_model::SemanticModel;
596 use validation_engine::{EngineConfig, ExternalRuleSource, ValidateConfig, ValidationEngine, validate_bytes};
597
598 fn make_engine() -> RegoEngine {
599 RegoEngine::new(EngineConfig::default()).unwrap()
600 }
601
602 fn make_model_from_yaml(yaml: &str) -> Arc<SemanticModel> {
603 let model = SemanticModel::from_bytes(yaml.as_bytes()).unwrap();
604 Arc::new(model)
605 }
606
607 #[test]
608 fn new_default_config_succeeds() {
609 let engine = make_engine();
610 assert_eq!(engine.engine_name(), "rego");
611 }
612
613 #[test]
614 fn list_rules_returns_nonempty() {
615 let engine = make_engine();
616 let rules = engine.list_rules();
617 assert!(!rules.is_empty(), "should have built-in rules");
618 }
619
620 #[test]
621 fn rule_metadata_contains_entries() {
622 let engine = make_engine();
623 assert!(!engine.rule_metadata().is_empty());
624 }
625
626 #[test]
627 fn init_metric_has_positive_duration() {
628 let engine = make_engine();
629 let metric = engine.init_metric();
630 assert!(metric.duration_ms > 0.0, "init duration should be > 0, got {}", metric.duration_ms);
631 }
632
633 #[test]
634 fn evaluate_minimal_template_no_errors() {
635 let engine = make_engine();
636 let model = make_model_from_yaml(
637 r#"
638AWSTemplateFormatVersion: "2010-09-09"
639Resources:
640 MyBucket:
641 Type: AWS::S3::Bucket
642"#,
643 );
644 let diags = engine.evaluate_rules(&model, &ValidateConfig::default()).unwrap();
645 assert!(
646 diags.iter().all(|d| d.severity != Severity::Fatal),
647 "rego engine should not produce Fatal diagnostics"
648 );
649 }
650
651 #[test]
652 fn evaluate_empty_resources_does_not_panic() {
653 let engine = make_engine();
654 let model = make_model_from_yaml(
655 r#"
656AWSTemplateFormatVersion: "2010-09-09"
657Resources: {}
658"#,
659 );
660 let diags = engine.evaluate_rules(&model, &ValidateConfig::default()).unwrap();
662 assert!(
663 diags.iter().any(|d| d.rule_id == "F0001"),
664 "expected F0001 Fatal for empty Resources section, got: {:?}",
665 diags.iter().map(|d| &d.rule_id).collect::<Vec<_>>(),
666 );
667 }
668
669 #[test]
674 fn evaluate_with_excluded_categories() {
675 let engine = make_engine();
676 let model = make_model_from_yaml(
677 r#"
678AWSTemplateFormatVersion: "2010-09-09"
679Resources:
680 Permission:
681 Type: AWS::Lambda::Permission
682 Properties:
683 Action: lambda:InvokeFunction
684 FunctionName: my-function
685 Principal: sns.amazonaws.com
686 SourceArn: arn:aws:sns:us-east-1:123456789012:my-topic
687"#,
688 );
689 fn rule_ids(diags: &[Diagnostic]) -> HashSet<&str> {
690 diags.iter().map(|d| d.rule_id.as_str()).collect()
691 }
692 let diags_all = engine.evaluate_rules(&model, &ValidateConfig::default()).unwrap();
693 let all_ids = rule_ids(&diags_all);
694 assert!(all_ids.contains("W9002"), "sanity: the best-practice hardcoded-ARN rule fires: {all_ids:?}");
695 assert!(all_ids.contains("W9013"), "sanity: the security hardcoded-account-ID rule fires: {all_ids:?}");
696
697 let excluded = Category::BestPractice;
698 let config = ValidateConfig {
699 filters: FilterConfig::new(
700 RuleFilterConfig::default(),
701 RuleFilterConfig { categories: vec![excluded.as_str().to_string()], ..Default::default() },
702 ),
703 ..Default::default()
704 };
705 let diags_filtered = engine.evaluate_rules(&model, &config).unwrap();
706 let filtered_ids = rule_ids(&diags_filtered);
707
708 let expected_ids: HashSet<&str> =
709 all_ids.iter().copied().filter(|id| lookup_rule(id).is_none_or(|rule| rule.category != excluded)).collect();
710 assert_eq!(filtered_ids, expected_ids, "only rules the registry files under {excluded:?} may disappear");
711 assert!(!filtered_ids.contains("W9002"), "the excluded best-practice rule is gone");
712 assert!(filtered_ids.contains("W9013"), "the security rule hosted in the same package survives");
713 }
714
715 #[test]
716 fn custom_rego_rule_produces_diagnostics() {
717 let custom_rego = r#"
718package custom_test
719import rego.v1
720
721violation contains v if {
722 some name, res in input.resources
723 res.resourceType == "AWS::S3::Bucket"
724 v := {"rule_id": "CUSTOM001", "severity": "error", "message": "Custom rule fired", "resource_id": name}
725}
726"#;
727 let config = EngineConfig {
728 custom_rules: vec![ExternalRuleSource { name: "custom_test.rego".into(), content: custom_rego.into() }],
729 guard_rules: vec![],
730 ..Default::default()
731 };
732 let engine = RegoEngine::new(config).unwrap();
733 let model = make_model_from_yaml(
734 r#"
735AWSTemplateFormatVersion: "2010-09-09"
736Resources:
737 MyBucket:
738 Type: AWS::S3::Bucket
739"#,
740 );
741 let diags = engine.evaluate_rules(&model, &ValidateConfig::default()).unwrap();
742 let custom = diags.iter().find(|d| d.rule_id == "CUSTOM001");
743 assert!(custom.is_some(), "custom rule should fire");
744 }
745
746 #[test]
747 fn guard_rule_produces_diagnostics() {
748 let guard_source = r#"
749rule check_bucket_name {
750 AWS::S3::Bucket {
751 Properties.BucketName EXISTS
752 <<BucketName must be specified>>
753 }
754}
755"#;
756 let config = EngineConfig {
757 custom_rules: vec![],
758 guard_rules: vec![ExternalRuleSource { name: "test.guard".into(), content: guard_source.into() }],
759 ..Default::default()
760 };
761 let engine = RegoEngine::new(config).unwrap();
762 let model = make_model_from_yaml(
763 r#"
764AWSTemplateFormatVersion: "2010-09-09"
765Resources:
766 MyBucket:
767 Type: AWS::S3::Bucket
768"#,
769 );
770 let diags = engine.evaluate_rules(&model, &ValidateConfig::default()).unwrap();
771 let guard_diag = diags.iter().find(|d| d.rule_id == "check_bucket_name").expect("guard rule fires");
772 assert_eq!(guard_diag.source, RuleOrigin::Guard);
773 assert_eq!(guard_diag.category.as_deref(), Some("guard:test"));
774 assert_eq!(guard_diag.message, "BucketName must be specified");
775 assert_eq!(guard_diag.resource_logical_id(), Some("MyBucket"));
776 }
777
778 #[test]
779 fn guard_rule_exists_check_does_not_fire_when_property_is_present() {
780 let guard_source = r#"
781rule check_bucket_name {
782 AWS::S3::Bucket {
783 Properties.BucketName EXISTS
784 <<BucketName must be specified>>
785 }
786}
787"#;
788 let config = EngineConfig {
789 custom_rules: vec![],
790 guard_rules: vec![ExternalRuleSource { name: "test.guard".into(), content: guard_source.into() }],
791 ..Default::default()
792 };
793 let engine = RegoEngine::new(config).unwrap();
794 let model = make_model_from_yaml(
795 r#"
796AWSTemplateFormatVersion: "2010-09-09"
797Resources:
798 MyBucket:
799 Type: AWS::S3::Bucket
800 Properties:
801 BucketName: my-bucket
802"#,
803 );
804 let diags = engine.evaluate_rules(&model, &ValidateConfig::default()).unwrap();
805 let guard_diag = diags.iter().find(|d| d.rule_id == "check_bucket_name");
806 assert!(
807 guard_diag.is_none(),
808 "guard EXISTS must be satisfied when BucketName is present, so the rule must not fire; got: {:?}",
809 diags.iter().map(|d| &d.rule_id).collect::<Vec<_>>()
810 );
811 }
812
813 #[test]
814 fn external_only_engine_evaluates_external_rules_but_skips_builtins() {
815 let custom_rego = r#"
816package external_only_test
817import rego.v1
818
819violation contains v if {
820 v := {"rule_id": "EXT_ONLY", "severity": "error", "message": "external rule fired", "resource_id": ""}
821}
822"#;
823 let config = EngineConfig {
824 custom_rules: vec![ExternalRuleSource {
825 name: "external_only_test.rego".into(),
826 content: custom_rego.into(),
827 }],
828 guard_rules: vec![],
829 ..Default::default()
830 };
831 let engine = RegoEngine::new_external_only(config).unwrap();
832 assert!(engine.rule_metadata().is_empty(), "an external-only engine must advertise no built-in registry rules");
833
834 let model = make_model_from_yaml(
838 r#"
839AWSTemplateFormatVersion: "2010-09-09"
840Resources: {}
841"#,
842 );
843 let normal_diags = RegoEngine::new(EngineConfig::default())
844 .unwrap()
845 .evaluate_rules(&model, &ValidateConfig::default())
846 .unwrap();
847 assert!(
848 normal_diags.iter().any(|d| d.rule_id == "F0001"),
849 "a normal engine must emit the built-in F0001 for empty Resources"
850 );
851
852 let diags = engine.evaluate_rules(&model, &ValidateConfig::default()).unwrap();
853 let external = diags.iter().find(|d| d.rule_id == "EXT_ONLY").expect("the external rule must still evaluate");
854 assert_eq!(external.severity, Severity::Error);
855 assert_eq!(external.source, RuleOrigin::Custom);
856 assert!(
857 diags.iter().all(|d| d.rule_id == "EXT_ONLY"),
858 "external-only evaluation must produce no built-in diagnostics, got: {:?}",
859 diags.iter().map(|d| &d.rule_id).collect::<Vec<_>>()
860 );
861 assert!(
862 !engine.list_rules().iter().any(|r| r.id == "F0001"),
863 "external-only list_rules must not include built-in rule IDs"
864 );
865 }
866
867 #[test]
868 fn external_only_engine_registers_custom_builtin_helpers() {
869 let custom_rego = r#"
870package external_builtin_test
871import rego.v1
872
873violation contains make_diag("EXT_BUILTINS", "error", name, "custom builtins available") if {
874 some name in resources_of_type("AWS::S3::Bucket")
875 has_property(name, "BucketName")
876 resolve(name, "Properties.BucketName") == "custom-builtins-bucket"
877 "BucketName" in schema_properties("AWS::S3::Bucket")
878}
879"#;
880 let engine = RegoEngine::new_external_only(EngineConfig {
881 custom_rules: vec![ExternalRuleSource {
882 name: "external_builtin_test.rego".into(),
883 content: custom_rego.into(),
884 }],
885 ..Default::default()
886 })
887 .expect("external-only engine must initialize with custom builtins");
888 let model = make_model_from_yaml(
889 r#"
890Resources:
891 Bucket:
892 Type: AWS::S3::Bucket
893 Properties:
894 BucketName: custom-builtins-bucket
895"#,
896 );
897
898 let diagnostics = engine.evaluate_rules(&model, &ValidateConfig::default()).expect("custom rule must evaluate");
899 let finding = diagnostics
900 .iter()
901 .find(|diagnostic| diagnostic.rule_id == "EXT_BUILTINS")
902 .expect("custom rule using resource, resolution, schema, and diagnostic helpers must fire");
903 assert_eq!(finding.source, RuleOrigin::Custom);
904 assert_eq!(finding.resource_logical_id(), Some("Bucket"));
905 }
906
907 #[test]
908 fn evaluate_rules_can_be_called_repeatedly() {
909 let engine = make_engine();
910 let model = make_model_from_yaml(
911 r#"
912AWSTemplateFormatVersion: "2010-09-09"
913Resources:
914 A:
915 Type: AWS::S3::Bucket
916"#,
917 );
918 let _ = engine.evaluate_rules(&model, &ValidateConfig::default()).unwrap();
921 let diags = engine.evaluate_rules(&model, &ValidateConfig::default()).unwrap();
922 assert!(
923 diags.iter().all(|d| d.severity != Severity::Fatal),
924 "rego engine should not produce Fatal diagnostics"
925 );
926 }
927
928 #[test]
929 fn build_rule_metadata_map_returns_entries() {
930 let index = build_rule_metadata_map();
931 assert!(!index.is_empty());
932 }
933
934 const BUILTIN_TEST_TEMPLATE: &str = r#"
935AWSTemplateFormatVersion: "2010-09-09"
936Parameters:
937 Env:
938 Type: String
939 AllowedValues:
940 - prod
941 - dev
942 Port:
943 Type: Number
944Conditions:
945 IsProd:
946 Fn::Equals:
947 - !Ref Env
948 - prod
949Mappings:
950 RegionMap:
951 us-east-1:
952 AMI: ami-12345678
953 us-west-2:
954 AMI: ami-abcdefgh
955Resources:
956 MyBucket:
957 Type: AWS::S3::Bucket
958 Properties:
959 BucketName: my-bucket
960 Tags:
961 - Key: Env
962 Value: !If [IsProd, "production", "development"]
963 MyFunc:
964 Type: AWS::Lambda::Function
965 DependsOn: MyBucket
966 Condition: IsProd
967 Properties:
968 FunctionName: my-func
969 Runtime: python3.12
970 Handler: index.handler
971 Code:
972 S3Bucket: !Ref MyBucket
973 S3Key: code.zip
974 VpcConfig:
975 SecurityGroupIds:
976 - sg-0123456789abcdef0
977 SubnetIds:
978 - !Ref MySubnet
979 - subnet-0123456789abcdef0
980 MyVpc:
981 Type: AWS::EC2::VPC
982 Properties:
983 CidrBlock: 10.0.0.0/16
984 MySubnet:
985 Type: AWS::EC2::Subnet
986 Properties:
987 VpcId: !Ref MyVpc
988 CidrBlock: 10.0.0.0/24
989 MyQueue:
990 Type: AWS::SQS::Queue
991 Properties:
992 QueueName: my-queue
993 MessageRetentionPeriod: !Ref Port
994Transform: AWS::Serverless-2016-10-31
995"#;
996
997 fn eval_builtin_policy_result(rego_source: &str) -> Result<Vec<Diagnostic>, validation_engine::ValidationError> {
998 let config = EngineConfig {
999 custom_rules: vec![ExternalRuleSource { name: "builtin_test.rego".into(), content: rego_source.into() }],
1000 guard_rules: vec![],
1001 ..Default::default()
1002 };
1003 let engine = RegoEngine::new(config).unwrap();
1004 let model = make_model_from_yaml(BUILTIN_TEST_TEMPLATE);
1005 engine.evaluate_rules(&model, &ValidateConfig::default())
1006 }
1007
1008 fn eval_builtin_policy(rego_source: &str, rule_id: &str) -> Vec<Diagnostic> {
1009 eval_builtin_policy_result(rego_source).unwrap().into_iter().filter(|d| d.rule_id == rule_id).collect()
1010 }
1011
1012 #[test]
1013 fn builtin_resolve_returns_property_value() {
1014 let diags = eval_builtin_policy(
1015 r#"
1016package builtin_test
1017import rego.v1
1018violation contains v if {
1019 val := resolve("MyBucket", "BucketName")
1020 val == "my-bucket"
1021 v := {"rule_id": "B_RESOLVE", "severity": "error", "message": "ok", "resource_id": "MyBucket"}
1022}
1023"#,
1024 "B_RESOLVE",
1025 );
1026 assert_eq!(diags.len(), 1, "resolve should return 'my-bucket'");
1027 }
1028
1029 #[test]
1030 fn builtin_resolve_all_returns_conditional_values() {
1031 let diags = eval_builtin_policy(
1032 r#"
1033package builtin_test
1034import rego.v1
1035violation contains v if {
1036 vals := resolve_all("MyBucket", "Tags.0.Value")
1037 count(vals) >= 1
1038 v := {"rule_id": "B_RESOLVE_ALL", "severity": "error", "message": "ok", "resource_id": "MyBucket"}
1039}
1040"#,
1041 "B_RESOLVE_ALL",
1042 );
1043 assert_eq!(diags.len(), 1, "resolve_all should return at least one value");
1044 }
1045
1046 const LEGACY_REFERENCE_CUSTOM_RULE: &str = r#"
1050package legacy_reference_test
1051import rego.v1
1052
1053violation contains make_diag("LEGACY_LOOKUP", "error", name, sprintf("code bucket is %s", [target])) if {
1054 some name in resources_of_type("AWS::Lambda::Function")
1055 target := resolve(name, "Properties.Code.S3Bucket")
1056 is_string(target)
1057 input.resources[target].resourceType == "AWS::S3::Bucket"
1058}
1059
1060violation contains make_diag("LEGACY_EQUALS", "error", name, "code lives in MyBucket") if {
1061 some name in resources_of_type("AWS::Lambda::Function")
1062 resolve(name, "Properties.Code.S3Bucket") == "MyBucket"
1063}
1064
1065violation contains make_diag("LEGACY_MARKER_SEEN", "error", name, "resolve returned the marker object") if {
1066 some name in resources_of_type("AWS::Lambda::Function")
1067 is_object(resolve(name, "Properties.Code.S3Bucket"))
1068}
1069"#;
1070
1071 fn assert_legacy_reference_contract(diags: &[Diagnostic]) {
1072 let lookup = diags
1073 .iter()
1074 .find(|d| d.rule_id == "LEGACY_LOOKUP")
1075 .expect("resolve() must hand back a logical ID a rule can look up in input.resources");
1076 assert_eq!(lookup.message, "code bucket is MyBucket");
1077 assert_eq!(lookup.resource_logical_id(), Some("MyFunc"));
1078 assert!(
1079 diags.iter().any(|d| d.rule_id == "LEGACY_EQUALS"),
1080 "resolve() must compare equal to the target's logical ID"
1081 );
1082 assert!(
1083 !diags.iter().any(|d| d.rule_id == "LEGACY_MARKER_SEEN"),
1084 "a custom rule must never see the marker object from resolve()"
1085 );
1086 }
1087
1088 #[test]
1089 fn custom_rule_resolve_keeps_the_legacy_target_id_string_for_a_reference() {
1090 let engine = RegoEngine::new(EngineConfig {
1091 custom_rules: vec![ExternalRuleSource {
1092 name: "legacy_reference_test.rego".into(),
1093 content: LEGACY_REFERENCE_CUSTOM_RULE.into(),
1094 }],
1095 ..Default::default()
1096 })
1097 .unwrap();
1098 let model = make_model_from_yaml(BUILTIN_TEST_TEMPLATE);
1099
1100 let diags = engine.evaluate_rules(&model, &ValidateConfig::default()).unwrap();
1101
1102 assert_legacy_reference_contract(&diags);
1103 }
1104
1105 #[test]
1106 fn external_only_engine_keeps_the_legacy_target_id_string_for_a_reference() {
1107 let engine = RegoEngine::new_external_only(EngineConfig {
1108 custom_rules: vec![ExternalRuleSource {
1109 name: "legacy_reference_test.rego".into(),
1110 content: LEGACY_REFERENCE_CUSTOM_RULE.into(),
1111 }],
1112 ..Default::default()
1113 })
1114 .unwrap();
1115 let model = make_model_from_yaml(BUILTIN_TEST_TEMPLATE);
1116
1117 let diags = engine.evaluate_rules(&model, &ValidateConfig::default()).unwrap();
1118
1119 assert_legacy_reference_contract(&diags);
1120 }
1121
1122 #[test]
1123 fn custom_rule_resolve_all_list_items_keep_the_legacy_target_id_string() {
1124 let diags = eval_builtin_policy(
1125 r#"
1126package builtin_test
1127import rego.v1
1128violation contains make_diag("LEGACY_LIST_ITEM", "error", "MyFunc", sprintf("subnets: %v", [subnets])) if {
1129 some subnets in resolve_all("MyFunc", "Properties.VpcConfig.SubnetIds")
1130 subnets == ["MySubnet", "subnet-0123456789abcdef0"]
1131}
1132"#,
1133 "LEGACY_LIST_ITEM",
1134 );
1135 assert_eq!(diags.len(), 1, "a referenced list item must come back as the target's logical ID");
1136 }
1137
1138 #[test]
1143 fn builtin_policies_keep_the_marker_rendering_while_a_custom_rule_gets_the_legacy_string() {
1144 let engine = RegoEngine::new(EngineConfig {
1145 custom_rules: vec![ExternalRuleSource {
1146 name: "legacy_reference_test.rego".into(),
1147 content: LEGACY_REFERENCE_CUSTOM_RULE.into(),
1148 }],
1149 ..Default::default()
1150 })
1151 .unwrap();
1152 let model = make_model_from_yaml(
1153 r#"
1154AWSTemplateFormatVersion: "2010-09-09"
1155Resources:
1156 Store:
1157 Type: AWS::SSM::Parameter
1158 Properties:
1159 Type: String
1160 Value: placeholder
1161 MyBucket:
1162 Type: AWS::S3::Bucket
1163 Distribution:
1164 Type: AWS::CloudFront::Distribution
1165 Properties:
1166 DistributionConfig:
1167 Enabled: true
1168 Aliases:
1169 - !GetAtt Store.Value
1170 DefaultCacheBehavior:
1171 TargetOriginId: primary
1172 ViewerProtocolPolicy: redirect-to-https
1173 ForwardedValues:
1174 QueryString: false
1175 Origins:
1176 - Id: primary
1177 DomainName: origin.example.com
1178 CustomOriginConfig:
1179 OriginProtocolPolicy: https-only
1180 MyFunc:
1181 Type: AWS::Lambda::Function
1182 Properties:
1183 Runtime: python3.12
1184 Handler: index.handler
1185 Role: arn:aws:iam::123456789012:role/lambda-role
1186 Code:
1187 S3Bucket: !Ref MyBucket
1188 S3Key: code.zip
1189"#,
1190 );
1191
1192 let diags = engine.evaluate_rules(&model, &ValidateConfig::default()).unwrap();
1193
1194 assert!(
1195 !diags.iter().any(|d| d.rule_id == "E3013"),
1196 "the built-in alias check must not judge the logical ID 'Store' as a domain name: {:?}",
1197 diags.iter().map(|d| (&d.rule_id, &d.message)).collect::<Vec<_>>()
1198 );
1199 assert_legacy_reference_contract(&diags);
1200 }
1201
1202 #[test]
1203 fn builtin_is_dynamic_on_conditional_property() {
1204 let diags = eval_builtin_policy(
1205 r#"
1206package builtin_test
1207import rego.v1
1208violation contains v if {
1209 is_dynamic("MyFunc", "Code.S3Bucket")
1210 v := {"rule_id": "B_IS_DYN", "severity": "error", "message": "ok", "resource_id": "MyFunc"}
1211}
1212"#,
1213 "B_IS_DYN",
1214 );
1215 assert_eq!(diags.len(), 1, "Code.S3Bucket is a Ref → dynamic");
1216 }
1217
1218 #[test]
1219 fn builtin_is_dynamic_false_for_static() {
1220 let diags = eval_builtin_policy(
1221 r#"
1222package builtin_test
1223import rego.v1
1224violation contains v if {
1225 not is_dynamic("MyBucket", "BucketName")
1226 v := {"rule_id": "B_NOT_DYN", "severity": "error", "message": "ok", "resource_id": "MyBucket"}
1227}
1228"#,
1229 "B_NOT_DYN",
1230 );
1231 assert_eq!(diags.len(), 1, "BucketName is static");
1232 }
1233
1234 #[test]
1235 fn builtin_property_can_be_absent_uses_known_absence() {
1236 let diags = eval_builtin_policy(
1237 r#"
1238package builtin_test
1239import rego.v1
1240violation contains v if {
1241 property_can_be_absent("MyQueue", "Properties.RedrivePolicy")
1242 not property_can_be_absent("MyBucket", "Properties.BucketName")
1243 not property_can_be_absent("MyQueue", "Properties.MessageRetentionPeriod")
1244 property_can_be_absent("MissingQueue", "Properties.MessageRetentionPeriod")
1245 v := {"rule_id": "B_PROPERTY_ABSENCE", "severity": "error", "message": "ok", "resource_id": "MyQueue"}
1246}
1247"#,
1248 "B_PROPERTY_ABSENCE",
1249 );
1250 assert_eq!(diags.len(), 1, "only known property absence should satisfy the builtin");
1251 }
1252
1253 #[test]
1254 fn builtin_resources_of_type() {
1255 let diags = eval_builtin_policy(
1256 r#"
1257package builtin_test
1258import rego.v1
1259violation contains v if {
1260 ids := resources_of_type("AWS::S3::Bucket")
1261 count(ids) == 1
1262 v := {"rule_id": "B_ROT", "severity": "error", "message": "ok", "resource_id": ""}
1263}
1264"#,
1265 "B_ROT",
1266 );
1267 assert_eq!(diags.len(), 1, "should find 1 S3 bucket");
1268 }
1269
1270 #[test]
1271 fn builtin_has_property_true() {
1272 let diags = eval_builtin_policy(
1273 r#"
1274package builtin_test
1275import rego.v1
1276violation contains v if {
1277 has_property("MyBucket", "BucketName")
1278 v := {"rule_id": "B_HAS_P", "severity": "error", "message": "ok", "resource_id": "MyBucket"}
1279}
1280"#,
1281 "B_HAS_P",
1282 );
1283 assert_eq!(diags.len(), 1, "MyBucket has BucketName");
1284 }
1285
1286 #[test]
1287 fn builtin_has_property_false() {
1288 let diags = eval_builtin_policy(
1289 r#"
1290package builtin_test
1291import rego.v1
1292violation contains v if {
1293 not has_property("MyBucket", "NonExistent")
1294 v := {"rule_id": "B_NO_P", "severity": "error", "message": "ok", "resource_id": "MyBucket"}
1295}
1296"#,
1297 "B_NO_P",
1298 );
1299 assert_eq!(diags.len(), 1, "MyBucket does not have NonExistent");
1300 }
1301
1302 #[test]
1303 fn builtin_has_transform() {
1304 let diags = eval_builtin_policy(
1305 r#"
1306package builtin_test
1307import rego.v1
1308violation contains v if {
1309 has_transform("AWS::Serverless-2016-10-31")
1310 v := {"rule_id": "B_HAS_T", "severity": "error", "message": "ok", "resource_id": ""}
1311}
1312"#,
1313 "B_HAS_T",
1314 );
1315 assert_eq!(diags.len(), 1, "template has Serverless transform");
1316 }
1317
1318 #[test]
1319 fn builtin_param_allowed_values() {
1320 let diags = eval_builtin_policy(
1321 r#"
1322package builtin_test
1323import rego.v1
1324violation contains v if {
1325 vals := param_allowed_values("Env")
1326 count(vals) == 2
1327 v := {"rule_id": "B_PAV", "severity": "error", "message": "ok", "resource_id": ""}
1328}
1329"#,
1330 "B_PAV",
1331 );
1332 assert_eq!(diags.len(), 1, "Env has 2 allowed values");
1333 }
1334
1335 #[test]
1336 fn builtin_param_type() {
1337 let diags = eval_builtin_policy(
1338 r#"
1339package builtin_test
1340import rego.v1
1341violation contains v if {
1342 t := param_type("Port")
1343 t == "Number"
1344 v := {"rule_id": "B_PT", "severity": "error", "message": "ok", "resource_id": ""}
1345}
1346"#,
1347 "B_PT",
1348 );
1349 assert_eq!(diags.len(), 1, "Port param type is Number");
1350 }
1351
1352 #[test]
1353 fn builtin_mapping_value() {
1354 let diags = eval_builtin_policy(
1355 r#"
1356package builtin_test
1357import rego.v1
1358violation contains v if {
1359 val := mapping_value("RegionMap", "us-east-1", "AMI")
1360 val == "ami-12345678"
1361 v := {"rule_id": "B_MAP", "severity": "error", "message": "ok", "resource_id": ""}
1362}
1363"#,
1364 "B_MAP",
1365 );
1366 assert_eq!(diags.len(), 1, "mapping lookup should return ami-12345678");
1367 }
1368
1369 #[test]
1370 fn builtin_resource_condition() {
1371 let diags = eval_builtin_policy(
1372 r#"
1373package builtin_test
1374import rego.v1
1375violation contains v if {
1376 c := resource_condition("MyFunc")
1377 c == "IsProd"
1378 v := {"rule_id": "B_RC", "severity": "error", "message": "ok", "resource_id": "MyFunc"}
1379}
1380"#,
1381 "B_RC",
1382 );
1383 assert_eq!(diags.len(), 1, "MyFunc has condition IsProd");
1384 }
1385
1386 #[test]
1387 fn builtin_resource_condition_null_when_none() {
1388 let diags = eval_builtin_policy(
1389 r#"
1390package builtin_test
1391import rego.v1
1392violation contains v if {
1393 c := resource_condition("MyBucket")
1394 is_null(c)
1395 v := {"rule_id": "B_RC_NULL", "severity": "error", "message": "ok", "resource_id": "MyBucket"}
1396}
1397"#,
1398 "B_RC_NULL",
1399 );
1400 assert_eq!(diags.len(), 1, "MyBucket has no condition");
1401 }
1402
1403 #[test]
1404 fn builtin_get_resource() {
1405 let diags = eval_builtin_policy(
1406 r#"
1407package builtin_test
1408import rego.v1
1409violation contains v if {
1410 res := get_resource("MyBucket")
1411 res.resourceType == "AWS::S3::Bucket"
1412 v := {"rule_id": "B_GR", "severity": "error", "message": "ok", "resource_id": "MyBucket"}
1413}
1414"#,
1415 "B_GR",
1416 );
1417 assert_eq!(diags.len(), 1, "get_resource should return bucket data");
1418 }
1419
1420 #[test]
1421 fn builtin_depends_on() {
1422 let diags = eval_builtin_policy(
1423 r#"
1424package builtin_test
1425import rego.v1
1426violation contains v if {
1427 depends_on("MyFunc", "MyBucket")
1428 v := {"rule_id": "B_DEP", "severity": "error", "message": "ok", "resource_id": "MyFunc"}
1429}
1430"#,
1431 "B_DEP",
1432 );
1433 assert_eq!(diags.len(), 1, "MyFunc depends on MyBucket");
1434 }
1435
1436 #[test]
1437 fn builtin_ref_targets() {
1438 let diags = eval_builtin_policy(
1439 r#"
1440package builtin_test
1441import rego.v1
1442violation contains v if {
1443 targets := ref_targets("MyFunc")
1444 count(targets) > 0
1445 v := {"rule_id": "B_RT", "severity": "error", "message": "ok", "resource_id": "MyFunc"}
1446}
1447"#,
1448 "B_RT",
1449 );
1450 assert_eq!(diags.len(), 1, "MyFunc should have ref targets (MyBucket)");
1451 }
1452
1453 #[test]
1454 fn builtin_ref_sources() {
1455 let diags = eval_builtin_policy(
1456 r#"
1457package builtin_test
1458import rego.v1
1459violation contains v if {
1460 sources := ref_sources("MyBucket")
1461 count(sources) > 0
1462 v := {"rule_id": "B_RS", "severity": "error", "message": "ok", "resource_id": "MyBucket"}
1463}
1464"#,
1465 "B_RS",
1466 );
1467 assert_eq!(diags.len(), 1, "MyBucket should have ref sources (MyFunc)");
1468 }
1469
1470 #[test]
1471 fn builtin_edges_from() {
1472 let diags = eval_builtin_policy(
1473 r#"
1474package builtin_test
1475import rego.v1
1476violation contains v if {
1477 edges := edges_from("MyFunc")
1478 count(edges) > 0
1479 v := {"rule_id": "B_EF", "severity": "error", "message": "ok", "resource_id": "MyFunc"}
1480}
1481"#,
1482 "B_EF",
1483 );
1484 assert_eq!(diags.len(), 1, "MyFunc should have outgoing edges");
1485 }
1486
1487 #[test]
1488 fn builtin_edges_to() {
1489 let diags = eval_builtin_policy(
1490 r#"
1491package builtin_test
1492import rego.v1
1493violation contains v if {
1494 edges := edges_to("MyBucket")
1495 count(edges) > 0
1496 v := {"rule_id": "B_ET", "severity": "error", "message": "ok", "resource_id": "MyBucket"}
1497}
1498"#,
1499 "B_ET",
1500 );
1501 assert_eq!(diags.len(), 1, "MyBucket should have incoming edges");
1502 }
1503
1504 #[test]
1505 fn builtin_make_diag_at() {
1506 let diags = eval_builtin_policy(
1507 r#"
1508package builtin_test
1509import rego.v1
1510violation contains v if {
1511 v := make_diag_at("B_MDA", "error", "MyBucket", "BucketName", "test diag at")
1512}
1513"#,
1514 "B_MDA",
1515 );
1516 assert_eq!(diags.len(), 1);
1517 assert_eq!(diags[0].message, "test diag at");
1518 }
1519
1520 #[test]
1521 fn builtin_make_diag_full() {
1522 let diags = eval_builtin_policy(
1523 r#"
1524package builtin_test
1525import rego.v1
1526violation contains v if {
1527 v := make_diag_full("B_MDF", "error", "MyBucket", "BucketName", "full diag", "fix it", "https://example.com")
1528}
1529"#,
1530 "B_MDF",
1531 );
1532 assert_eq!(diags.len(), 1);
1533 assert_eq!(diags[0].message, "full diag");
1534 }
1535
1536 #[test]
1537 fn builtin_make_diag_at_source_conditional_rejects_non_object_conditions() {
1538 let evaluation = eval_builtin_policy_result(
1539 r#"
1540package builtin_test
1541import rego.v1
1542violation contains make_diag_at_source_conditional(
1543 "B_BAD_CONDITIONS", "error", "MyBucket", "BucketName", "BucketName",
1544 "bad conditions", "", ["not-an-object"]
1545) if {
1546 true
1547}
1548"#,
1549 );
1550
1551 let error = evaluation.expect_err("a non-object condition scenario must fail evaluation");
1552 assert!(error.to_string().contains("condition scenario must be an object"));
1553 }
1554
1555 #[test]
1556 fn builtin_schema_properties_known_type() {
1557 let diags = eval_builtin_policy(
1558 r#"
1559package builtin_test
1560import rego.v1
1561violation contains v if {
1562 props := schema_properties("AWS::S3::Bucket")
1563 # Just verify the builtin is callable and returns an array
1564 is_array(props)
1565 v := {"rule_id": "B_SP", "severity": "error", "message": "ok", "resource_id": ""}
1566}
1567"#,
1568 "B_SP",
1569 );
1570 assert_eq!(diags.len(), 1, "schema_properties should be callable and return array");
1571 }
1572
1573 #[test]
1574 fn builtin_schema_required_known_type() {
1575 let diags = eval_builtin_policy(
1576 r#"
1577package builtin_test
1578import rego.v1
1579violation contains v if {
1580 req := schema_required("AWS::Lambda::Function")
1581 is_array(req)
1582 v := {"rule_id": "B_SR", "severity": "error", "message": "ok", "resource_id": ""}
1583}
1584"#,
1585 "B_SR",
1586 );
1587 assert_eq!(diags.len(), 1, "schema_required should be callable and return array");
1588 }
1589
1590 #[test]
1591 fn builtin_conditions_compatible() {
1592 let diags = eval_builtin_policy(
1593 r#"
1594package builtin_test
1595import rego.v1
1596violation contains v if {
1597 conditions_compatible("MyBucket", "MyQueue")
1598 v := {"rule_id": "B_CC", "severity": "error", "message": "ok", "resource_id": ""}
1599}
1600"#,
1601 "B_CC",
1602 );
1603 assert_eq!(diags.len(), 1, "unconditional resources are always compatible");
1604 }
1605
1606 #[test]
1607 fn builtin_resolve_type() {
1608 let diags = eval_builtin_policy(
1609 r#"
1610package builtin_test
1611import rego.v1
1612violation contains v if {
1613 t := resolve_type("MyBucket", "BucketName")
1614 t == "string"
1615 v := {"rule_id": "B_RTYPE", "severity": "error", "message": "ok", "resource_id": "MyBucket"}
1616}
1617"#,
1618 "B_RTYPE",
1619 );
1620 assert_eq!(diags.len(), 1, "BucketName should resolve to string type");
1621 }
1622
1623 #[test]
1624 fn region_override_available_to_builtins() {
1625 let custom_rego = r#"
1626package region_test
1627import rego.v1
1628violation contains v if {
1629 r := input_region()
1630 r == "eu-west-1"
1631 v := {"rule_id": "B_REGION", "severity": "error", "message": "ok", "resource_id": ""}
1632}
1633"#;
1634 let config = EngineConfig {
1635 custom_rules: vec![ExternalRuleSource { name: "region_test.rego".into(), content: custom_rego.into() }],
1636 guard_rules: vec![],
1637 ..Default::default()
1638 };
1639 let engine = RegoEngine::new(config).unwrap();
1640 let model = make_model_from_yaml(BUILTIN_TEST_TEMPLATE);
1641 let mut vc = ValidateConfig::default();
1642 vc.pseudo_parameter_overrides.region = Some("eu-west-1".to_string());
1643 let diags = engine.evaluate_rules(&model, &vc).unwrap();
1644 let found = diags.iter().find(|d| d.rule_id == "B_REGION");
1645 assert!(found.is_some(), "input_region() should return eu-west-1");
1646 }
1647
1648 #[test]
1649 fn multiple_custom_packages_all_evaluated() {
1650 let pkg_a = r#"
1651package custom_a
1652import rego.v1
1653violation contains v if {
1654 v := {"rule_id": "PKG_A", "severity": "error", "message": "from A", "resource_id": ""}
1655}
1656"#;
1657 let pkg_b = r#"
1658package custom_b
1659import rego.v1
1660violation contains v if {
1661 v := {"rule_id": "PKG_B", "severity": "error", "message": "from B", "resource_id": ""}
1662}
1663"#;
1664 let config = EngineConfig {
1665 custom_rules: vec![
1666 ExternalRuleSource { name: "a.rego".into(), content: pkg_a.into() },
1667 ExternalRuleSource { name: "b.rego".into(), content: pkg_b.into() },
1668 ],
1669 guard_rules: vec![],
1670 ..Default::default()
1671 };
1672 let engine = RegoEngine::new(config).unwrap();
1673 let model = make_model_from_yaml(BUILTIN_TEST_TEMPLATE);
1674 let diags = engine.evaluate_rules(&model, &ValidateConfig::default()).unwrap();
1675 assert!(diags.iter().any(|d| d.rule_id == "PKG_A"), "package A should fire");
1676 assert!(diags.iter().any(|d| d.rule_id == "PKG_B"), "package B should fire");
1677 }
1678
1679 #[test]
1680 fn duplicate_custom_package_deduplicated() {
1681 let source = r#"
1682package custom_dedup
1683import rego.v1
1684violation contains v if {
1685 v := {"rule_id": "DEDUP1", "severity": "error", "message": "first", "resource_id": ""}
1686}
1687"#;
1688 let config = EngineConfig {
1689 custom_rules: vec![
1690 ExternalRuleSource { name: "a.rego".into(), content: source.into() },
1691 ExternalRuleSource { name: "b.rego".into(), content: source.into() },
1692 ],
1693 guard_rules: vec![],
1694 ..Default::default()
1695 };
1696 let engine = RegoEngine::new(config).unwrap();
1697 assert_eq!(
1698 engine.custom_packages.iter().filter(|p| p.contains("custom_dedup")).count(),
1699 1,
1700 "duplicate package should be deduplicated"
1701 );
1702 }
1703
1704 #[test]
1705 fn builtin_follow_ref_returns_target_resource() {
1706 let diags = eval_builtin_policy(
1707 r#"
1708package builtin_test
1709import rego.v1
1710violation contains v if {
1711 target := follow_ref("MyFunc", "Code.S3Bucket")
1712 target == "MyBucket"
1713 v := {"rule_id": "B_FREF", "severity": "error", "message": "ok", "resource_id": "MyFunc"}
1714}
1715"#,
1716 "B_FREF",
1717 );
1718 assert_eq!(diags.len(), 1, "follow_ref should resolve Ref to MyBucket");
1719 }
1720
1721 #[test]
1722 fn builtin_is_from_parameter_true_for_ref_param() {
1723 let diags = eval_builtin_policy(
1724 r#"
1725package builtin_test
1726import rego.v1
1727violation contains v if {
1728 is_from_parameter("MyBucket", "Tags.0.Value")
1729 v := {"rule_id": "B_IFP", "severity": "error", "message": "ok", "resource_id": "MyBucket"}
1730}
1731"#,
1732 "B_IFP",
1733 );
1734 assert!(diags.len() <= 1, "expected at most 1 diagnostic, got {}", diags.len());
1736 }
1737
1738 #[test]
1739 fn builtin_flatten_list_returns_items() {
1740 let diags = eval_builtin_policy(
1741 r#"
1742package builtin_test
1743import rego.v1
1744violation contains v if {
1745 items := flatten_list("MyBucket", "Tags")
1746 count(items) >= 1
1747 v := {"rule_id": "B_FL", "severity": "error", "message": "ok", "resource_id": "MyBucket"}
1748}
1749"#,
1750 "B_FL",
1751 );
1752 assert_eq!(diags.len(), 1, "flatten_list should return tag items");
1753 }
1754
1755 #[test]
1756 fn builtin_resolve_scenarios_returns_values() {
1757 let diags = eval_builtin_policy(
1758 r#"
1759package builtin_test
1760import rego.v1
1761violation contains v if {
1762 scenarios := resolve_scenarios("MyBucket", "Tags.0.Value")
1763 count(scenarios) >= 1
1764 v := {"rule_id": "B_RS2", "severity": "error", "message": "ok", "resource_id": "MyBucket"}
1765}
1766"#,
1767 "B_RS2",
1768 );
1769 assert_eq!(diags.len(), 1, "resolve_scenarios should return at least one scenario");
1770 }
1771
1772 #[test]
1773 fn builtin_is_satisfiable_with_compatible_assumptions() {
1774 let diags = eval_builtin_policy(
1775 r#"
1776package builtin_test
1777import rego.v1
1778violation contains v if {
1779 is_satisfiable({"IsProd": true})
1780 v := {"rule_id": "B_SAT", "severity": "error", "message": "ok", "resource_id": ""}
1781}
1782"#,
1783 "B_SAT",
1784 );
1785 assert_eq!(diags.len(), 1, "IsProd=true should be satisfiable");
1786 }
1787
1788 #[test]
1789 fn builtin_make_diag_produces_diagnostic() {
1790 let diags = eval_builtin_policy(
1791 r#"
1792package builtin_test
1793import rego.v1
1794violation contains v if {
1795 v := make_diag("B_MD", "error", "MyBucket", "test message")
1796}
1797"#,
1798 "B_MD",
1799 );
1800 assert_eq!(diags.len(), 1);
1801 assert_eq!(diags[0].message, "test message");
1802 }
1803
1804 #[test]
1805 fn builtin_estimated_string_length_bounds_returns_both_bounds() {
1806 let diags = eval_builtin_policy(
1809 r#"
1810package builtin_test
1811import rego.v1
1812violation contains v if {
1813 bounds := estimated_string_length_bounds("MyBucket", "Tags.0.Value")
1814 bounds.shortest == 10
1815 bounds.longest == 11
1816 v := {"rule_id": "B_ESL", "severity": "error", "message": "ok", "resource_id": "MyBucket"}
1817}
1818"#,
1819 "B_ESL",
1820 );
1821 assert_eq!(diags.len(), 1, "a value chosen between two known strings must report both bounds");
1822 }
1823
1824 #[test]
1825 fn builtin_estimated_string_length_bounds_is_undefined_for_a_literal() {
1826 let diags = eval_builtin_policy(
1829 r#"
1830package builtin_test
1831import rego.v1
1832violation contains v if {
1833 bounds := estimated_string_length_bounds("MyBucket", "BucketName")
1834 v := {"rule_id": "B_ESL_LITERAL", "severity": "error", "message": "ok", "resource_id": "MyBucket"}
1835}
1836"#,
1837 "B_ESL_LITERAL",
1838 );
1839 assert!(diags.is_empty(), "a literal string must yield no bounds, got {diags:?}");
1840 }
1841
1842 #[test]
1843 fn regoengine_is_send_and_sync() {
1844 fn assert_send_sync<T: Send + Sync>() {}
1845 assert_send_sync::<RegoEngine>();
1846 }
1847
1848 #[test]
1849 fn evaluate_rules_isolates_model_and_region_across_threads() {
1850 let probe = r#"
1854package concurrency_probe
1855import rego.v1
1856violation contains v if {
1857 r := input_region()
1858 v := {"rule_id": "REGIONPROBE", "severity": "info", "message": r, "resource_id": ""}
1859}
1860violation contains v if {
1861 some name in resources_of_type("AWS::S3::Bucket")
1862 v := {"rule_id": "MODELPROBE", "severity": "info", "message": name, "resource_id": name}
1863}
1864violation contains v if {
1865 some name, _ in input.resources
1866 v := {"rule_id": "INPUTPROBE", "severity": "info", "message": name, "resource_id": name}
1867}
1868"#;
1869 let config = EngineConfig {
1870 custom_rules: vec![ExternalRuleSource { name: "concurrency_probe.rego".into(), content: probe.into() }],
1871 guard_rules: vec![],
1872 ..Default::default()
1873 };
1874 let engine = Arc::new(RegoEngine::new(config).unwrap());
1875
1876 const THREAD_COUNT: usize = 8;
1877 const ITERATIONS_PER_THREAD: usize = 128;
1878 let barrier = Arc::new(Barrier::new(THREAD_COUNT));
1879 let handles: Vec<_> = (0..THREAD_COUNT)
1880 .map(|i| {
1881 let engine = Arc::clone(&engine);
1882 let barrier = Arc::clone(&barrier);
1883 thread::spawn(move || {
1884 let region = format!("region-{i}");
1885 let bucket = format!("Bucket{i}");
1886 let model = make_model_from_yaml(&format!(
1887 "AWSTemplateFormatVersion: \"2010-09-09\"\nResources:\n {bucket}:\n Type: AWS::S3::Bucket\n"
1888 ));
1889 let mut config = ValidateConfig::default();
1890 config.pseudo_parameter_overrides.region = Some(region.clone());
1891 barrier.wait();
1893 let mut diagnostics = Vec::new();
1894 for _ in 0..ITERATIONS_PER_THREAD {
1895 diagnostics = engine.evaluate_rules(&model, &config).unwrap();
1896 let observed_regions: Vec<&str> = diagnostics
1897 .iter()
1898 .filter(|diagnostic| diagnostic.rule_id == "REGIONPROBE")
1899 .map(|diagnostic| diagnostic.message.as_str())
1900 .collect();
1901 assert_eq!(observed_regions, vec![region.as_str()]);
1902 let observed_resources: Vec<&str> = diagnostics
1903 .iter()
1904 .filter(|diagnostic| diagnostic.rule_id == "MODELPROBE")
1905 .map(|diagnostic| diagnostic.message.as_str())
1906 .collect();
1907 assert_eq!(observed_resources, vec![bucket.as_str()]);
1908 let observed_input_resources: Vec<&str> = diagnostics
1909 .iter()
1910 .filter(|diagnostic| diagnostic.rule_id == "INPUTPROBE")
1911 .map(|diagnostic| diagnostic.message.as_str())
1912 .collect();
1913 assert_eq!(observed_input_resources, vec![bucket.as_str()]);
1914 }
1915 (region, bucket, diagnostics)
1916 })
1917 })
1918 .collect();
1919
1920 for handle in handles {
1921 let (region, bucket, diags) = handle.join().unwrap();
1922 let regions: Vec<&str> =
1923 diags.iter().filter(|d| d.rule_id == "REGIONPROBE").map(|d| d.message.as_str()).collect();
1924 assert_eq!(regions, vec![region.as_str()], "a thread must observe only its own region");
1925 let resources: Vec<&str> =
1926 diags.iter().filter(|d| d.rule_id == "MODELPROBE").map(|d| d.message.as_str()).collect();
1927 assert_eq!(resources, vec![bucket.as_str()], "a thread must observe only its own model");
1928 let input_resources: Vec<&str> =
1929 diags.iter().filter(|d| d.rule_id == "INPUTPROBE").map(|d| d.message.as_str()).collect();
1930 assert_eq!(input_resources, vec![bucket.as_str()], "a thread must observe only its own Rego input");
1931 }
1932 }
1933
1934 #[test]
1935 fn shared_engine_keeps_thousand_sequential_reports_template_local() {
1936 const CUSTOM_RULE: &str = r#"
1937package sequential_isolation
1938import rego.v1
1939
1940violation contains v if {
1941 pattern := input.template.description
1942 some name, resource in input.resources
1943 regex.match(pattern, resource.properties.Value)
1944 region := input_region()
1945 message := sprintf("%s/%s", [name, region])
1946 v := {
1947 "rule_id": "SEQUENTIALPROBE",
1948 "severity": "info",
1949 "category": "isolation",
1950 "message": message,
1951 "resource_id": name,
1952 }
1953}
1954"#;
1955 let engine = RegoEngine::new(EngineConfig {
1956 custom_rules: vec![ExternalRuleSource {
1957 name: "sequential_isolation.rego".into(),
1958 content: CUSTOM_RULE.into(),
1959 }],
1960 ..Default::default()
1961 })
1962 .unwrap();
1963 let schema_validator = SchemaValidator::default();
1964
1965 for iteration in 0..1_000 {
1966 let (resource_name, region, pattern, value) = if iteration % 2 == 0 {
1967 ("AlphaResource", "alpha-region", "^alpha+$", "alpha")
1968 } else {
1969 ("BetaResource", "beta-region", "^beta+$", "beta")
1970 };
1971 let template = format!(
1972 r#"{{
1973 "AWSTemplateFormatVersion": "2010-09-09",
1974 "Description": "{pattern}",
1975 "Resources": {{
1976 "{resource_name}": {{
1977 "Type": "Custom::Probe",
1978 "Properties": {{"Value": "{value}"}}
1979 }}
1980 }}
1981 }}"#
1982 );
1983 let mut config = ValidateConfig {
1984 detail_level: DetailLevel::Detailed,
1985 severity_level: Severity::Debug,
1986 disable_builtin_rules: true,
1987 ..Default::default()
1988 };
1989 config.pseudo_parameter_overrides.region = Some(region.to_string());
1990
1991 let report = validate_bytes(&engine, &schema_validator, template.as_bytes(), config).unwrap();
1992 let probes: Vec<&Diagnostic> =
1993 report.diagnostics.iter().filter(|diagnostic| diagnostic.rule_id == "SEQUENTIALPROBE").collect();
1994 assert_eq!(probes.len(), 1, "iteration {iteration} must emit exactly one probe diagnostic");
1995 let expected_message = format!("{resource_name}/{region}");
1996 assert_eq!(probes[0].message, expected_message, "the current template supplies the diagnostic message");
1997 assert_eq!(
1998 probes[0].rule_description.as_deref(),
1999 Some(expected_message.as_str()),
2000 "the current template supplies its own detailed rule description"
2001 );
2002 assert_eq!(probes[0].category.as_deref(), Some("isolation"));
2003 }
2004 }
2005
2006 #[test]
2007 fn failed_evaluation_does_not_contaminate_the_next_call() {
2008 const CUSTOM_RULE: &str = r#"
2009package error_cleanup
2010import rego.v1
2011
2012violation contains v if {
2013 input.template.description == "error"
2014 ip_overlaps("not-a-cidr", "10.0.0.0/8")
2015 v := {"rule_id": "UNREACHABLE", "severity": "error", "message": "unreachable"}
2016}
2017
2018violation contains v if {
2019 input.template.description == "ok"
2020 some name in resources_of_type("Custom::Probe")
2021 region := input_region()
2022 v := {
2023 "rule_id": "RECOVERYPROBE",
2024 "severity": "info",
2025 "message": sprintf("%s/%s", [name, region]),
2026 "resource_id": name,
2027 }
2028}
2029"#;
2030 let engine = RegoEngine::new(EngineConfig {
2031 custom_rules: vec![ExternalRuleSource { name: "error_cleanup.rego".into(), content: CUSTOM_RULE.into() }],
2032 ..Default::default()
2033 })
2034 .unwrap();
2035 let error_model =
2036 make_model_from_yaml("Description: error\nResources:\n ErrorResource:\n Type: Custom::Probe\n");
2037 let valid_model =
2038 make_model_from_yaml("Description: ok\nResources:\n ValidResource:\n Type: Custom::Probe\n");
2039 let mut error_config = ValidateConfig { disable_builtin_rules: true, ..Default::default() };
2040 error_config.pseudo_parameter_overrides.region = Some("error-region".to_string());
2041 assert!(engine.evaluate_rules(&error_model, &error_config).is_err(), "the first evaluation must fail");
2042
2043 let mut valid_config = ValidateConfig { disable_builtin_rules: true, ..Default::default() };
2044 valid_config.pseudo_parameter_overrides.region = Some("valid-region".to_string());
2045 let diagnostics = engine.evaluate_rules(&valid_model, &valid_config).unwrap();
2046 let recovery: Vec<&Diagnostic> =
2047 diagnostics.iter().filter(|diagnostic| diagnostic.rule_id == "RECOVERYPROBE").collect();
2048 assert_eq!(recovery.len(), 1, "the next evaluation must recover after the prior error");
2049 assert_eq!(recovery[0].message, "ValidResource/valid-region");
2050 }
2051
2052 const SUPPRESSION_TEMPLATE: &str = r#"
2053AWSTemplateFormatVersion: "2010-09-09"
2054Resources:
2055 MyInstance:
2056 Type: AWS::EC2::Instance
2057 Properties:
2058 ImageId: ami-12345678
2059 InstanceType: !Sub "t3.micro"
2060"#;
2061
2062 fn exclude_ids(ids: &[&str]) -> ValidateConfig {
2063 ValidateConfig {
2064 filters: FilterConfig::new(
2065 RuleFilterConfig::default(),
2066 RuleFilterConfig { ids: ids.iter().map(|s| s.to_string()).collect(), ..Default::default() },
2067 ),
2068 ..Default::default()
2069 }
2070 }
2071
2072 #[test]
2073 fn exclude_id_suppresses_matching_builtin_and_preserves_other_findings() {
2074 let engine = make_engine();
2075 let model = make_model_from_yaml(SUPPRESSION_TEMPLATE);
2076
2077 let unfiltered = engine.evaluate_rules(&model, &ValidateConfig::default()).unwrap();
2078 assert!(unfiltered.iter().any(|d| d.rule_id == "W9010"), "hardcoded AMI should trigger W9010");
2079 let mut other_findings: Vec<(String, String)> = unfiltered
2080 .iter()
2081 .filter(|d| d.rule_id != "W9010")
2082 .map(|d| (d.rule_id.clone(), d.message.clone()))
2083 .collect();
2084 assert!(!other_findings.is_empty(), "template must also produce a non-W9010 finding worth preserving");
2085 other_findings.sort();
2086
2087 let filtered = engine.evaluate_rules(&model, &exclude_ids(&["W9010"])).unwrap();
2088 assert!(filtered.iter().all(|d| d.rule_id != "W9010"), "the excluded rule must not be emitted");
2089 let mut remaining: Vec<(String, String)> =
2090 filtered.iter().map(|d| (d.rule_id.clone(), d.message.clone())).collect();
2091 remaining.sort();
2092 assert_eq!(remaining, other_findings, "excluding one rule must not alter any other diagnostic");
2093
2094 let unfiltered_again = engine.evaluate_rules(&model, &ValidateConfig::default()).unwrap();
2095 assert!(
2096 unfiltered_again.iter().any(|diagnostic| diagnostic.rule_id == "W9010"),
2097 "a later unfiltered call must not inherit the previous call's suppressed-rule set"
2098 );
2099 }
2100
2101 #[test]
2102 fn include_id_globally_suppresses_every_other_builtin() {
2103 let engine = make_engine();
2104 let model = make_model_from_yaml(SUPPRESSION_TEMPLATE);
2105
2106 let config = ValidateConfig {
2107 filters: FilterConfig::new(
2108 RuleFilterConfig { ids: vec!["W9010".to_string()], ..Default::default() },
2109 RuleFilterConfig::default(),
2110 ),
2111 ..Default::default()
2112 };
2113 let filtered = engine.evaluate_rules(&model, &config).unwrap();
2114 assert!(filtered.iter().any(|d| d.rule_id == "W9010"), "the included rule must still fire");
2115 assert!(
2116 filtered.iter().all(|d| d.rule_id == "W9010"),
2117 "a non-empty include filter must globally suppress every rule it can never admit"
2118 );
2119 }
2120
2121 #[test]
2122 fn severity_level_prunes_lower_severity_builtins() {
2123 let engine = make_engine();
2124 let model = make_model_from_yaml(SUPPRESSION_TEMPLATE);
2125
2126 let default_run = engine.evaluate_rules(&model, &ValidateConfig::default()).unwrap();
2127 assert!(default_run.iter().any(|d| d.rule_id == "W9010"), "W9010 fires at the default Info level");
2128
2129 let config = ValidateConfig { severity_level: Severity::Error, ..Default::default() };
2130 let error_only = engine.evaluate_rules(&model, &config).unwrap();
2131 assert!(
2132 error_only.iter().all(|d| d.severity >= Severity::Error),
2133 "no sub-Error diagnostics should be produced when the level is Error"
2134 );
2135 assert!(
2136 error_only.iter().all(|d| d.rule_id != "W9010"),
2137 "the Warn rule W9010 must be pruned at the Error level"
2138 );
2139 }
2140
2141 #[test]
2142 fn cfn_rule_active_guard_short_circuits_a_suppressed_rule_body() {
2143 let probe = r#"
2148package guard_probe
2149import rego.v1
2150violation contains v if {
2151 cfn_rule_active("W9010")
2152 not regex.match("(unterminated", "anything")
2153 v := {"rule_id": "GUARDPROBE", "severity": "error", "message": "guarded body ran", "resource_id": ""}
2154}
2155"#;
2156 let config = EngineConfig {
2157 custom_rules: vec![ExternalRuleSource { name: "guard_probe.rego".into(), content: probe.into() }],
2158 guard_rules: vec![],
2159 ..Default::default()
2160 };
2161 let engine = RegoEngine::new(config).unwrap();
2162 let model = make_model_from_yaml(
2163 r#"
2164AWSTemplateFormatVersion: "2010-09-09"
2165Resources:
2166 MyBucket:
2167 Type: AWS::S3::Bucket
2168"#,
2169 );
2170
2171 let active = engine.evaluate_rules(&model, &ValidateConfig::default()).unwrap();
2172 assert!(active.iter().any(|d| d.rule_id == "GUARDPROBE"), "an active rule's guarded body must run");
2173
2174 let suppressed = engine.evaluate_rules(&model, &exclude_ids(&["W9010"])).unwrap();
2175 assert!(
2176 suppressed.iter().all(|d| d.rule_id != "GUARDPROBE"),
2177 "a suppressed rule's guarded body - including its invalid regex - must not run"
2178 );
2179 }
2180}