{"record":{"id":"6699447678b33f74","repo":"floci-io/floci","slug":"validationexception-669944","errorCode":"ValidationException","errorMessage":"EventBusPolicy StatementId is required.","messagePattern":"EventBusPolicy StatementId is required\\.","errorType":"validation","errorClass":"AwsException","httpStatus":400,"severity":"error","filePath":"src/main/java/io/github/hectorvent/floci/services/cloudformation/CloudFormationResourceProvisioner.java","lineNumber":3296,"sourceCode":"        // A missing attribute means ownership was never tracked, not that it changed: stacks\n        // provisioned before this attribute existed are restored from cloudformation-stacks.json\n        // without it. Refusing there would leave every such stack permanently in DELETE_FAILED, so\n        // fall back to the pre-tracking behaviour of deleting what the stack recorded it created.\n        String expectedCreatedTime = resource.getAttributes().get(EVENT_BUS_CREATED_TIME_ATTR);\n        if (expectedCreatedTime != null && !expectedCreatedTime.equals(eventBusCreatedTime(bus))) {\n            throw new AwsException(\"ValidationError\",\n                    \"EventBus ownership changed; refusing to delete: \" + busName, 400);\n        }\n        deleteEventBusSafe(busName, region);\n    }\n\n\n    private void provisionEventBusPolicy(StackResource r, JsonNode props, CloudFormationTemplateEngine engine,\n                                         String region) {\n        String busName = resolveOrDefault(props, \"EventBusName\", engine, \"default\");\n        String statementId = resolveOptional(props, \"StatementId\", engine);\n        if (statementId == null || statementId.isBlank()) {\n            throw new AwsException(\"ValidationException\", \"EventBusPolicy StatementId is required.\", 400);\n        }\n\n        if (props != null && props.has(\"Statement\") && props.get(\"Statement\").isObject()) {\n            // Statement form: merge the full statement into the bus policy, keyed by Sid,\n            // so multiple EventBusPolicy resources on the same bus coexist.\n            try {\n                ObjectNode statement = (ObjectNode) engine.resolveNode(props.get(\"Statement\")).deepCopy();\n                statement.put(\"Sid\", statementId);\n\n                EventBus bus = eventBridgeService.describeEventBus(busName, region);\n                ObjectNode policy;\n                String current = bus.getPolicy();\n                if (current != null && !current.isBlank()) {\n                    policy = (ObjectNode) objectMapper.readTree(current);\n                } else {\n                    policy = objectMapper.createObjectNode();\n                    policy.put(\"Version\", \"2012-10-17\");\n                    policy.putArray(\"Statement\");","sourceCodeStart":3278,"sourceCodeEnd":3314,"githubUrl":"https://github.com/floci-io/floci/blob/62ff490619e7bd3554597c28c704081b4c15add5/src/main/java/io/github/hectorvent/floci/services/cloudformation/CloudFormationResourceProvisioner.java#L3278-L3314","documentation":"Provisioning an AWS::Events::EventBusPolicy resource whose StatementId property resolves to null or blank. StatementId (the policy Sid) is mandatory in both the Statement-form and individual-form code paths, so the provisioner validates it up front before touching the bus policy.","triggerScenarios":"CreateStack/UpdateStack with an AWS::Events::EventBusPolicy resource where StatementId is omitted, set to empty string, or resolves via intrinsic functions to blank (e.g. a Fn::Sub or Ref that yields \"\").","commonSituations":"Template authored with only Action+Principal (copying putPermission muscle memory where Sid is optional); StatementId supplied via a parameter defaulted to ''; short-form intrinsic shorthand miswritten so it resolves to null.","solutions":["Add a non-blank StatementId to the resource properties in the template","If StatementId comes from a parameter/Ref, verify the parameter has a real non-empty value at deploy time","Ensure intrinsic expressions (Fn::Sub, Ref) actually resolve to a string rather than null — test with the emulator's template engine or aws cloudformation validate-template first"],"exampleFix":"# before\nResources:\n  BusPolicy:\n    Type: AWS::Events::EventBusPolicy\n    Properties:\n      EventBusName: !Ref Bus\n      Action: events:PutEvents\n      Principal: \"123456789012\"\n# after\nResources:\n  BusPolicy:\n    Type: AWS::Events::EventBusPolicy\n    Properties:\n      EventBusName: !Ref Bus\n      StatementId: AllowCrossAccountPut\n      Action: events:PutEvents\n      Principal: \"123456789012\"","handlingStrategy":"validation","validationCode":"// Validate the template before deploy\nconst ok = resource.Properties?.StatementId != null\n  && String(resource.Properties.StatementId).trim().length > 0;\nif (!ok) throw new Error('EventBusPolicy requires a non-blank StatementId');","typeGuard":"function hasStatementId(p: unknown): p is { StatementId: string } {\n  return typeof (p as any)?.StatementId === 'string'\n    && (p as any).StatementId.trim().length > 0;\n}","tryCatchPattern":null,"preventionTips":["Run cfn-lint / aws cloudformation validate-template before CreateStack","Copy from a known-good EventBusPolicy example that includes StatementId","Treat StatementId as the policy's primary key — one unique Sid per policy resource per bus"],"tags":["cloudformation","eventbridge","event-bus-policy","validation","template"],"backgroundTag":null,"analyzedSha":"62ff490619e7bd3554597c28c704081b4c15add5","analyzedAt":"2026-08-14T14:25:23.764Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}