diff --git a/src/cel-engine/src/rules/resources_extra.rs b/src/cel-engine/src/rules/resources_extra.rs index e94d27d4..51cc56dc 100644 --- a/src/cel-engine/src/rules/resources_extra.rs +++ b/src/cel-engine/src/rules/resources_extra.rs @@ -22,9 +22,7 @@ use template_model::message::{primary_identifier_conflict_message, render_str_li use template_model::resolver::{RefKind, ResolvedValue}; use template_model::route_table::duplicate_subnet_associations; use template_model::vpc_cidr::subnets_outside_vpc; -use template_model::{ - CAA_RECORD_PATTERN, IAM_ROLE_ARN_RULE_PATTERN, MARKER_DYNAMIC, MARKER_PARAM_TYPE, MX_RECORD_PATTERN, SourceSpan, -}; +use template_model::{CAA_RECORD_PATTERN, MARKER_DYNAMIC, MARKER_PARAM_TYPE, MX_RECORD_PATTERN, SourceSpan}; use template_model::{ListMemberScenario, SemanticModel}; use template_model::{hardcoded_az, region_enums, schedule_expression_errors}; use validation_engine::{make_resource_diagnostic, make_resource_diagnostic_at_source}; @@ -34,9 +32,6 @@ static DOMAIN_RE: LazyLock = LazyLock::new(|| { .expect("Invalid DOMAIN_RE pattern") }); -static ARN_RE: LazyLock = - LazyLock::new(|| regex::Regex::new(IAM_ROLE_ARN_RULE_PATTERN).expect("Invalid ARN_RE pattern")); - static W1030_VPC_RE: LazyLock = LazyLock::new(|| { regex::Regex::new(r"^vpc-(([0-9A-Fa-f]{8})|([0-9A-Fa-f]{17}))$").expect("Invalid W1030_VPC_RE pattern") }); @@ -2115,24 +2110,6 @@ pub fn eval_extra_resources(ctx: &EvalContext) -> Vec { out.extend(snapstart_without_published_version(m, name)); } - for rpp in &ctx.cached_data.rule_tables.iam_role_arn_property_paths { - let path = full_property_path(rpp); - for name in m.resources_of_type(&rpp.resource_type) { - if let Some(serde_json::Value::String(val)) = resolve_concrete(m, name, &path) - && !ARN_RE.is_match(&val) - { - out.push(make_resource_diagnostic( - "E3511", - &format!("IAM Role ARN '{}' does not match expected pattern", val), - m, - name, - &path, - None, - )); - } - } - } - for (name, res) in &m.resources { for (prop, val) in &res.properties { if !prop.ends_with("Arn") { diff --git a/src/cfn-validate/tests/github_issues.rs b/src/cfn-validate/tests/github_issues.rs index 97c37b70..ea4439a2 100644 --- a/src/cfn-validate/tests/github_issues.rs +++ b/src/cfn-validate/tests/github_issues.rs @@ -1297,32 +1297,22 @@ fn w9007_still_fires_when_two_entries_read_one_deploy_time_value() { } // --------------------------------------------------------------------------- -// Issue #36 - the IAM-role-ARN checks use a future-proof `arn:aws[a-zA-Z-]*` -// partition prefix, so ADC-partition ARNs no longer false-positive and the two -// engines agree. The committed fixture uses `arn:aws-iso:` (snapshot-compatible); -// the previously-divergent E3511 path is exercised here with inline bytes using -// `arn:aws-isob:`, which both engines must now accept identically. +// Issue #36 - IAM role ARN formats accept `aws`-prefixed partitions without a fixed allowlist. // --------------------------------------------------------------------------- -/// Issue #36: the schema-validator's `AWS::IAM::Role.Arn` format check (E1156) -/// must not fire on an `arn:aws-iso:` ADC-partition ARN in either engine - the -/// partition list is no longer hardcoded. +/// An `arn:aws-iso:` role ARN must be accepted without a partition allowlist. /// https://github.com/aws-cloudformation/cloudformation-validate/issues/36 #[test] -fn issue_36_no_e1156_on_iso_partition_arn() { - let diags = validate_all("issue-36.yaml"); - assert_absent(&diags, "E1156"); - assert_count(&diags, "E1156", 0); +fn issue_36_no_role_arn_format_error_on_iso_partition() { + let diagnostics = validate_all("issue-36.yaml"); + assert_absent(&diagnostics, "E1156"); + assert_count(&diagnostics, "E1156", 0); } -/// Issue #36 (continued): the engine ARN rule (E3511) and the schema-validator -/// format check (E1156) both accept an `arn:aws-isob:` ARN, and the two engines -/// agree - the prior rego/cel divergence (CEL hardcoded the partition list) is -/// gone. A genuinely malformed ARN still fires E3511 in both engines, so the -/// future-proof prefix did not become a catch-all. +/// An AWS partition outside a fixed allowlist is accepted while a malformed role ARN remains rejected at engine parity. /// https://github.com/aws-cloudformation/cloudformation-validate/issues/36 #[test] -fn issue_36_isob_partition_arn_accepted_at_parity() { +fn issue_36_future_partition_arn_accepted_at_parity() { const VALID: &[u8] = br#" AWSTemplateFormatVersion: "2010-09-09" Resources: @@ -1331,18 +1321,18 @@ Resources: Properties: ExecutionRoleArn: arn:aws-isob:iam::123456789012:role/my-task-role "#; - let sv = SchemaValidator::default(); - let rego = validate_bytes(&*REGO, &sv, VALID, debug_config()).unwrap().diagnostics; - let cel = validate_bytes(&*CEL, &sv, VALID, debug_config()).unwrap().diagnostics; - let composite = validate_bytes(&*COMPOSITE, &sv, VALID, debug_config()).unwrap().diagnostics; + let schema_validator = SchemaValidator::default(); + let rego = validate_bytes(&*REGO, &schema_validator, VALID, debug_config()).unwrap().diagnostics; + let cel = validate_bytes(&*CEL, &schema_validator, VALID, debug_config()).unwrap().diagnostics; + let composite = validate_bytes(&*COMPOSITE, &schema_validator, VALID, debug_config()).unwrap().diagnostics; - for (name, d) in [("rego", ®o), ("cel", &cel), ("composite", &composite)] { - assert!(!d.iter().any(|x| x.rule_id == "E1156"), "[{name}] E1156 must not fire on an aws-isob ARN"); - assert!(!d.iter().any(|x| x.rule_id == "E3511"), "[{name}] E3511 must not fire on an aws-isob ARN"); + for (engine_name, diagnostics) in [("rego", ®o), ("cel", &cel), ("composite", &composite)] { + assert!( + !diagnostics.iter().any(|diagnostic| diagnostic.rule_id == "E1156"), + "[{engine_name}] role ARN format must accept an aws-isob ARN" + ); } - // A malformed ARN (no partition at all) must still be rejected by E3511 in - // both engines - the future-proof prefix is not a catch-all. const MALFORMED: &[u8] = br#" AWSTemplateFormatVersion: "2010-09-09" Resources: @@ -1351,12 +1341,15 @@ Resources: Properties: ExecutionRoleArn: not-an-arn "#; - let rego_bad = validate_bytes(&*REGO, &sv, MALFORMED, debug_config()).unwrap().diagnostics; - let cel_bad = validate_bytes(&*CEL, &sv, MALFORMED, debug_config()).unwrap().diagnostics; - let composite_bad = validate_bytes(&*COMPOSITE, &sv, MALFORMED, debug_config()).unwrap().diagnostics; - assert!(rego_bad.iter().any(|d| d.rule_id == "E3511"), "rego must still fire E3511 on a malformed ARN"); - assert!(cel_bad.iter().any(|d| d.rule_id == "E3511"), "cel must still fire E3511 on a malformed ARN"); - assert!(composite_bad.iter().any(|d| d.rule_id == "E3511"), "composite must still fire E3511 on a malformed ARN"); + let rego_bad = validate_bytes(&*REGO, &schema_validator, MALFORMED, debug_config()).unwrap().diagnostics; + let cel_bad = validate_bytes(&*CEL, &schema_validator, MALFORMED, debug_config()).unwrap().diagnostics; + let composite_bad = validate_bytes(&*COMPOSITE, &schema_validator, MALFORMED, debug_config()).unwrap().diagnostics; + for (engine_name, diagnostics) in [("rego", ®o_bad), ("cel", &cel_bad), ("composite", &composite_bad)] { + assert!( + diagnostics.iter().any(|diagnostic| diagnostic.rule_id == "E1156"), + "[{engine_name}] malformed role ARN must be rejected" + ); + } } // --------------------------------------------------------------------------- diff --git a/src/data-source/scripts/sync_cfnlint_data.py b/src/data-source/scripts/sync_cfnlint_data.py index 3e1e65ea..817218a4 100644 --- a/src/data-source/scripts/sync_cfnlint_data.py +++ b/src/data-source/scripts/sync_cfnlint_data.py @@ -279,7 +279,6 @@ def extract_rule_tables(cfn_lint_root: Path) -> dict: from cfnlint.rules.resources.apigateway.RestApiMixingDefinitions import RestApiMixingDefinitions from cfnlint.rules.resources.ectwo.EbsIopsIgnored import EbsIopsIgnored, _no_iops_volume_types from cfnlint.rules.resources.iam.ResourcePolicy import ResourcePolicy - from cfnlint.rules.resources.iam.RoleArnPattern import RoleArnPattern from cfnlint.rules.resources.lmbd.SnapStartEnabled import SnapStartEnabled from cfnlint.rules.resources.lmbd.SnapStartSupported import SnapStartSupported from cfnlint.rules.resources.lmbd.ZipPackageRequiredProperties import ZipPackageRequiredProperties @@ -302,7 +301,6 @@ def extract_rule_tables(cfn_lint_root: Path) -> dict: "api_gateway_mixing_resource_types": api_gateway_rule._mix_types, "ebs_iops_ignored_volume_types": sorted(_no_iops_volume_types), "ebs_iops_property_paths": EbsIopsIgnored().keywords, - "iam_role_arn_property_paths": RoleArnPattern().keywords, "image_id_parameter_types": extract_enum_list(image_id_rule.validate), "image_id_property_paths": image_id_rule.keywords, "lambda_zip_required_properties": extract_local_list( diff --git a/src/data-source/src/rule_data.rs b/src/data-source/src/rule_data.rs index dd59cd0d..eef7cb06 100644 --- a/src/data-source/src/rule_data.rs +++ b/src/data-source/src/rule_data.rs @@ -184,7 +184,7 @@ pub struct NormalizedRuleTablesDocument { pub rule_tables: RuleTablesRaw, } -/// Raw deserialized rule tables with all 22 fields exactly as stored in JSON. +/// Raw deserialized rule tables as stored in JSON. /// /// Every field uses `deny_unknown_fields` on the outer struct to reject unknown keys. /// Path fields are stored as raw strings here; [`RuleTables`] holds the parsed forms. @@ -194,7 +194,6 @@ pub struct RuleTablesRaw { pub api_gateway_mixing_resource_types: Vec, pub ebs_iops_ignored_volume_types: Vec, pub ebs_iops_property_paths: Vec, - pub iam_role_arn_property_paths: Vec, pub image_id_parameter_types: Vec, pub image_id_property_paths: Vec, pub lambda_zip_required_properties: Vec, @@ -225,7 +224,6 @@ pub struct RuleTables { pub api_gateway_mixing_resource_types: Vec, pub ebs_iops_ignored_volume_types: Vec, pub ebs_iops_property_paths: Vec, - pub iam_role_arn_property_paths: Vec, pub image_id_parameter_types: Vec, pub image_id_property_paths: Vec, pub lambda_zip_required_properties: Vec, @@ -260,7 +258,6 @@ impl RuleTables { validate_nonempty_unique(&raw.api_gateway_mixing_resource_types, "api_gateway_mixing_resource_types")?; validate_nonempty_unique(&raw.ebs_iops_ignored_volume_types, "ebs_iops_ignored_volume_types")?; validate_nonempty_unique(&raw.ebs_iops_property_paths, "ebs_iops_property_paths")?; - validate_nonempty_unique(&raw.iam_role_arn_property_paths, "iam_role_arn_property_paths")?; validate_nonempty_unique(&raw.image_id_parameter_types, "image_id_parameter_types")?; validate_nonempty_unique(&raw.image_id_property_paths, "image_id_property_paths")?; validate_nonempty_unique(&raw.lambda_zip_required_properties, "lambda_zip_required_properties")?; @@ -300,7 +297,6 @@ impl RuleTables { let ebs_iops_property_paths = parse_paths(&raw.ebs_iops_property_paths, "ebs_iops_property_paths")?; validate_ebs_iops_shape(&ebs_iops_property_paths)?; - let iam_role_arn_property_paths = parse_paths(&raw.iam_role_arn_property_paths, "iam_role_arn_property_paths")?; let image_id_property_paths = parse_paths(&raw.image_id_property_paths, "image_id_property_paths")?; let package_property_paths = parse_paths(&raw.package_property_paths, "package_property_paths")?; let previous_generation_instance_property_paths = parse_paths( @@ -315,7 +311,6 @@ impl RuleTables { api_gateway_mixing_resource_types: raw.api_gateway_mixing_resource_types, ebs_iops_ignored_volume_types: raw.ebs_iops_ignored_volume_types, ebs_iops_property_paths, - iam_role_arn_property_paths, image_id_parameter_types: raw.image_id_parameter_types, image_id_property_paths, lambda_zip_required_properties: raw.lambda_zip_required_properties, @@ -740,9 +735,6 @@ mod tests { api_gateway_mixing_resource_types: vec!["AWS::ApiGateway::Method".into()], ebs_iops_ignored_volume_types: vec!["gp2".into()], ebs_iops_property_paths: vec!["Resources/AWS::EC2::Instance/Properties/BlockDeviceMappings/*/Ebs".into()], - iam_role_arn_property_paths: vec![ - "Resources/AWS::Backup::BackupSelection/Properties/BackupSelection/IamRoleArn".into(), - ], image_id_parameter_types: vec!["AWS::EC2::Image::Id".into()], image_id_property_paths: vec!["Resources/AWS::AutoScaling::LaunchConfiguration/Properties/ImageId".into()], lambda_zip_required_properties: vec!["Handler".into()], diff --git a/src/rego-engine/handwritten/rego/resources/iam/role_arn_pattern.rego b/src/rego-engine/handwritten/rego/resources/iam/role_arn_pattern.rego deleted file mode 100644 index d33a70e5..00000000 --- a/src/rego-engine/handwritten/rego/resources/iam/role_arn_pattern.rego +++ /dev/null @@ -1,28 +0,0 @@ -package resources - -import rego.v1 - -# E3511: IAM Role ARN must match expected pattern -_iam_role_arn_checks contains {"type": rtype, "path": prop_path} if { - some raw_path in data.rule_tables.iam_role_arn_property_paths - parts := split(raw_path, "/") - count(parts) > 2 - parts[0] == "Resources" - rtype := parts[1] - prop_parts := array.slice(parts, 2, count(parts)) - prop_path := concat(".", prop_parts) -} - -violation contains make_diag_full("E3511", "ERROR", name, - prop.path, - sprintf("IAM Role ARN '%s' does not match expected pattern", [val]), - "Use format: arn:aws:iam::123456789012:role/role-name", - "") if { - cfn_rule_active("E3511") - some prop in _iam_role_arn_checks - some name in resources_of_type(prop.type) - val := resolve(name, prop.path) - is_string(val) - not is_dynamic(name, prop.path) - not regex.match(`^arn:(aws[a-zA-Z-]*)?:iam::\d{12}:role/[a-zA-Z_0-9+=,.@\-_/]+$`, val) -} diff --git a/src/resources/templates/gh-issues/issue-36.yaml b/src/resources/templates/gh-issues/issue-36.yaml index 8304a84f..ddee9370 100644 --- a/src/resources/templates/gh-issues/issue-36.yaml +++ b/src/resources/templates/gh-issues/issue-36.yaml @@ -1,9 +1,9 @@ AWSTemplateFormatVersion: "2010-09-09" Resources: - # ExecutionRoleArn uses the `aws-iso` ADC partition. The schema-validator's - # AWS::IAM::Role.Arn format pattern hardcodes the partition list and omits the - # ADC partitions, so it raises a false-positive E1156 (in BOTH engines) even - # though this is a deployable ARN. + # ExecutionRoleArn uses the `aws-iso` partition. + # The role ARN format accepts every `aws`-prefixed partition rather than a + # hardcoded subset, so this deployable partition must not raise a + # format diagnostic in any engine. # See https://github.com/aws-cloudformation/cloudformation-validate/issues/36 TaskDef: Type: AWS::ECS::TaskDefinition diff --git a/src/rules/src/helpers.rs b/src/rules/src/helpers.rs index d436dcb6..439f4177 100644 --- a/src/rules/src/helpers.rs +++ b/src/rules/src/helpers.rs @@ -87,6 +87,7 @@ pub fn format_rule_for_format(format: &str) -> Option<&'static str> { "AWS::EC2::Subnet.Id" => Some("E1154"), "AWS::Logs::LogGroup.Name" => Some("E1155"), "AWS::IAM::Role.Arn" => Some("E1156"), + "AWS::IAM::Role.Name" => Some("E1165"), _ => None, } } @@ -231,6 +232,7 @@ mod tests { assert_eq!(format_rule_for_format("AWS::EC2::Subnet.Id"), Some("E1154")); assert_eq!(format_rule_for_format("AWS::Logs::LogGroup.Name"), Some("E1155")); assert_eq!(format_rule_for_format("AWS::IAM::Role.Arn"), Some("E1156")); + assert_eq!(format_rule_for_format("AWS::IAM::Role.Name"), Some("E1165")); } #[test] diff --git a/src/rules/src/registry.rs b/src/rules/src/registry.rs index 77448d5d..c67025b7 100644 --- a/src/rules/src/registry.rs +++ b/src/rules/src/registry.rs @@ -716,6 +716,12 @@ pub const RULE_REGISTRY: &[RuleDefinition] = &[ description: "Validate IAM role ARN format", origin: RuleOrigin::CfnLint, }, + RuleDefinition { + id: "E1165", + category: Category::Intrinsic, + description: "Validate IAM role name format", + origin: RuleOrigin::CfnLint, + }, RuleDefinition { id: "I1022", category: Category::Intrinsic, @@ -1028,12 +1034,6 @@ pub const RULE_REGISTRY: &[RuleDefinition] = &[ description: "Validate identity based IAM policies", origin: RuleOrigin::CfnLint, }, - RuleDefinition { - id: "E3511", - category: Category::Resource, - description: "Validate IAM role arn pattern", - origin: RuleOrigin::CfnLint, - }, RuleDefinition { id: "E3512", category: Category::Resource, diff --git a/src/schema-validator/src/validate.rs b/src/schema-validator/src/validate.rs index bb6a3072..5f940792 100644 --- a/src/schema-validator/src/validate.rs +++ b/src/schema-validator/src/validate.rs @@ -25,8 +25,8 @@ use template_model::resolved_value::{ }; use template_model::resolver::{RefKind, ResolvedValue}; use template_model::{ - BudgetKind, CompiledPattern, IAM_ROLE_ARN_PATTERN, PSEUDO_PARAMETERS, SECURITY_GROUP_NAME_PATTERN, SemanticModel, - compile_pattern, is_custom_resource_type, resolved_value_to_json, + BudgetKind, CompiledPattern, IAM_ROLE_ARN_PATTERN, IAM_ROLE_NAME_PATTERN, PSEUDO_PARAMETERS, + SECURITY_GROUP_NAME_PATTERN, SemanticModel, compile_pattern, is_custom_resource_type, resolved_value_to_json, }; /// Properties that accept a string value when used with `aws cloudformation package`. @@ -4912,12 +4912,13 @@ pub(crate) fn prewarm_statics() { } static FORMAT_PATTERNS: LazyLock>> = LazyLock::new(|| { - let sources: [(&str, &str); 13] = [ + let sources: [(&str, &str); 14] = [ ("AWS::EC2::VPC.Id", r"^vpc-[a-f0-9]{8,17}$"), ("AWS::EC2::Subnet.Id", r"^subnet-[a-f0-9]{8,17}$"), ("AWS::EC2::SecurityGroup.Id", r"^sg-[a-f0-9]{8,17}$"), (EC2_IMAGE_ID_FORMAT, r"^ami-([0-9a-z]{8}|[0-9a-z]{17})$"), ("AWS::IAM::Role.Arn", IAM_ROLE_ARN_PATTERN), + ("AWS::IAM::Role.Name", IAM_ROLE_NAME_PATTERN), ("AWS::Logs::LogGroup.Name", r"^[\.\-_/#A-Za-z0-9]{1,512}$"), ("AWS::EC2::SecurityGroup.Name", SECURITY_GROUP_NAME_PATTERN), ("AWS::EC2::KeyPair.KeyName", r"^[\x20-\x7E]{1,255}$"), @@ -5075,6 +5076,7 @@ fn validate_prop_composition( Some(val), condition_map(conds), )); + validate_failed_any_of_formats(out, m, rid, prop_path, schema, defs, val, conds); } } } @@ -5145,6 +5147,32 @@ fn validate_prop_composition( } } +fn validate_failed_any_of_formats( + out: &mut Vec, + m: &Arc, + rid: &str, + prop_path: &str, + schema: &PropSchema, + defs: &HashMap, + value: &serde_json::Value, + conditions: &HashMap, +) { + let mut reported_formats = HashSet::new(); + if let Some(format) = &schema.format { + reported_formats.insert(format.clone()); + } + + for branch in &schema.any_of { + let resolved_branch = branch.resolve(defs); + let Some(format) = resolved_branch.format.as_deref() else { + continue; + }; + if reported_formats.insert(format.to_string()) { + validate_format_scenario(out, m, rid, prop_path, format, value, conditions, false); + } + } +} + fn validate_format( out: &mut Vec, m: &Arc, @@ -5154,43 +5182,55 @@ fn validate_format( scenarios: &[(serde_json::Value, HashMap)], schema_pattern_failures: &[(serde_json::Value, HashMap)], ) { - let Some(re) = FORMAT_PATTERNS.get(format) else { + for (value, conditions) in scenarios { + let schema_pattern_failed = schema_pattern_failures + .iter() + .any(|(pattern_value, pattern_conditions)| pattern_value == value && pattern_conditions == conditions); + validate_format_scenario(out, m, rid, prop_path, format, value, conditions, schema_pattern_failed); + } +} + +#[allow(clippy::too_many_arguments)] +fn validate_format_scenario( + out: &mut Vec, + m: &Arc, + rid: &str, + prop_path: &str, + format: &str, + value: &serde_json::Value, + conditions: &HashMap, + schema_pattern_failed: bool, +) { + let Some(pattern) = FORMAT_PATTERNS.get(format) else { + return; + }; + if !is_satisfiable(m, conditions) + || value.is_null() + || m.is_from_intrinsic(rid, prop_path) + || defer_value_constraints(m, rid, prop_path, value, conditions) + { + return; + } + let Some(string_value) = coerce_to_string(value) else { return; }; + if ec2_resolves_ssm_image_alias(m, rid, prop_path, format, &string_value) || pattern.is_match(&string_value) { + return; + } - for (val, conds) in scenarios { - if !is_satisfiable(m, conds) - || val.is_null() - || m.is_from_intrinsic(rid, prop_path) - || defer_value_constraints(m, rid, prop_path, val, conds) - { - continue; - } - if let Some(s) = coerce_to_string(val) { - if ec2_resolves_ssm_image_alias(m, rid, prop_path, format, &s) { - continue; - } - if !re.is_match(&s) { - let rule_id = format_rule_for_format(format).unwrap_or("E1103"); - if rule_id == "E1155" - && schema_pattern_failures - .iter() - .any(|(pattern_value, pattern_conditions)| pattern_value == val && pattern_conditions == conds) - { - continue; - } - out.push(build_diagnostic_conditional( - rule_id, - &format!("{} does not match format '{}'", format_value(val), format), - m, - rid, - prop_path, - None, - condition_map(conds), - )); - } - } + let rule_id = format_rule_for_format(format).unwrap_or("E1103"); + if rule_id == "E1155" && schema_pattern_failed { + return; } + out.push(build_diagnostic_conditional( + rule_id, + &format!("{} does not match format '{}'", format_value(value), format), + m, + rid, + prop_path, + None, + condition_map(conditions), + )); } fn validate_lifecycle(out: &mut Vec, store: &CompiledSchemaStore, model: &Arc) { diff --git a/src/template-model/src/lib.rs b/src/template-model/src/lib.rs index ef618936..4ebb1bda 100644 --- a/src/template-model/src/lib.rs +++ b/src/template-model/src/lib.rs @@ -67,7 +67,7 @@ pub use serialization::resolved_value_to_json; pub use span::{SourceSpan, SpanProvider, UNKNOWN_SPAN, span_to_option}; pub use template_section::{EntityType, TopLevelSection, entity_identity}; pub use value_patterns::{ - AMI_ID_PATTERN, AVAILABILITY_ZONE_PATTERN, CAA_RECORD_PATTERN, IAM_ROLE_ARN_PATTERN, IAM_ROLE_ARN_RULE_PATTERN, + AMI_ID_PATTERN, AVAILABILITY_ZONE_PATTERN, CAA_RECORD_PATTERN, IAM_ROLE_ARN_PATTERN, IAM_ROLE_NAME_PATTERN, MX_RECORD_PATTERN, SECURITY_GROUP_NAME_PATTERN, }; diff --git a/src/template-model/src/value_patterns.rs b/src/template-model/src/value_patterns.rs index a8b577fb..af00fcef 100644 --- a/src/template-model/src/value_patterns.rs +++ b/src/template-model/src/value_patterns.rs @@ -6,11 +6,9 @@ /// other legal-but-unusual character is accepted here. pub const IAM_ROLE_ARN_PATTERN: &str = r"^arn:aws[a-zA-Z-]*:iam::\d{12}:role/.+$"; -/// Regex for the resource-property IAM role-ARN *rule* check: the partition group is -/// optional and the role name is constrained to the IAM role-name character class. This is -/// intentionally stricter than [`IAM_ROLE_ARN_PATTERN`] - the two correspond to two distinct checks -/// and must not be conflated. -pub const IAM_ROLE_ARN_RULE_PATTERN: &str = r"^arn:(aws[a-zA-Z-]*)?:iam::\d{12}:role/[a-zA-Z_0-9+=,.@\-_/]+$"; +/// Regex for an IAM role name with an optional path. Paths use printable ASCII and the role name +/// uses the IAM role-name character set with its 64-character limit. +pub const IAM_ROLE_NAME_PATTERN: &str = r"^(/[!-~]+/)?[a-zA-Z0-9_+=,.@-]{1,64}$"; /// Regex for a valid EC2 Security Group name. The class mirrors the character set the service /// accepts; the `+` requires at least one character. diff --git a/src/validation-engine/src/engine.rs b/src/validation-engine/src/engine.rs index 6ddcfa2a..1d86dd4b 100644 --- a/src/validation-engine/src/engine.rs +++ b/src/validation-engine/src/engine.rs @@ -1065,7 +1065,7 @@ pub(crate) fn build_context( } } "F3030" | "W3030" | "E3030" | "F3031" | "E3031" | "F3034" | "E3034" | "F3037" | "W3045" | "E1103" | "E1150" - | "E1151" | "E1152" | "E1153" | "E1154" | "E1155" | "E1156" => { + | "E1151" | "E1152" | "E1153" | "E1154" | "E1155" | "E1156" | "E1165" => { actual_value = resolve_val(property_path).map(Into::into); } "F3033" | "W9006" => {