Skip to content

fix(ci): fix LLM review crash and deliver override alerts to Slack - #169

Merged
Alexanderamiri merged 2 commits into
mainfrom
fix/review-alerts
Oct 29, 2026
Merged

Alexanderamiri merged 2 commits into
mainfrom
fix/review-alerts

Conversation

@Alexanderamiri

@Alexanderamiri Alexanderamiri commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Why

  • The main apply for feat(platform): app-to-app calls with private networking and scopes #156 was blocked with risk FAILED, not HIGH: LLM review failed: string indices must be integers, not 'str'. Bedrock returned findings as a JSON string, and format_review indexed it.
  • Override alerts weren't reaching Slack. notify-high-risk.sh and notify-slack.sh built JSON by string interpolation, posted with curl -s > /dev/null, and always printed "sent".
    • Checked live: the webhook is valid, since a test message returns ok.
    • Block payloads with long HIGH findings break Slack's 3000-char section limit and come back invalid_blocks.
    • Quotes in messages, e.g. an override reason, produced invalid JSON.

What

  • IAM: javabin-ci-override-approver can now ssm:GetParameter the override-alerts webhook. The "Risk Override Approved" step had always logged No webhook URL available (seen in every approve-override run), so approvals never reached Slack.
  • review-plan.py: normalize_review() parses stringified arrays and objects, keeps plain-string findings, drops unusable entries, and rejects risks outside LOW/MEDIUM/HIGH. The plan-review Lambda bundles this file, so app reviews are fixed too.
  • New scripts/slack_post.py, used by both notify scripts:
    • builds escaped Block Kit cut to Slack's limits (header 150, section 3000);
    • checks Slack's reply;
    • re-sends as plain text if the blocks are rejected;
    • emits a ::error:: annotation if delivery still fails. The job doesn't fail.
  • A FAILED review alert says "Plan Review Failed" instead of claiming HIGH.
  • Docs: CLAUDE.md script table and docs/ci-workflow.md.

How verified

  • python3 -m unittest discover -s scripts/tests: 126 tests, OK. New tests cover stringified findings, long findings, special characters, the fallback and the double failure.
  • python3 -m unittest discover -s terraform/lambda-src/tests: 194 tests, OK.
  • Live: the webhook replies ok, and the old FAILED-case payload is accepted.

Follow-ups

🤖 Generated with Claude Code

… override alerts

The review of #156 crashed with "string indices must be integers": Bedrock
returned `findings` as a JSON string, format_review indexed it, and the apply
was blocked with risk FAILED. review_plan now normalises the tool answer
(parses stringified arrays/objects, drops unusable entries, rejects unknown
risk). The plan-review Lambda bundles the same script, so app reviews get it.

Override alerts were reported as "sent" without checking Slack's reply.
Payloads were hand-built JSON strings with no size limits, so long HIGH
findings (>3000 chars per section) came back invalid_blocks and nobody saw
them. Both notify scripts now go through slack_post.py: JSON-escaped,
size-limited blocks, Slack's reply checked, plain-text fallback on rejection,
CI error annotation if still undelivered. A FAILED review is labelled as such
instead of "HIGH".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Alexanderamiri
Alexanderamiri requested a review from a team as a code owner October 29, 2026 20:49
@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 12 to add, 8 to change, 0 to destroy.

Plan output
Acquiring state lock. This may take a few moments...
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place

Terraform will perform the following actions:

  # module.iam.aws_iam_policy.developer_boundary will be updated in-place
  ~ resource "aws_iam_policy" "developer_boundary" {
        id               = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
        name             = "javabin-developer-boundary"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (5 unchanged elements hidden)
                    {
                        Action   = [
                            "organizations:*",
                            "sso:*",
                            "sso-directory:*",
                            "identitystore:*",
                            "account:*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenyOrgSSO"
                    },
                  ~ {
                      ~ Action   = [
                            # (4 unchanged elements hidden)
                            "ec2:DeleteSubnet",
                          - "ec2:CreateInternetGateway",
                          - "ec2:DeleteInternetGateway",
                          - "ec2:AttachInternetGateway",
                          - "ec2:DetachInternetGateway",
                          - "ec2:CreateNatGateway",
                          - "ec2:DeleteNatGateway",
                          + "ec2:*InternetGateway",
                          + "ec2:*NatGateway",
                            "ec2:CreateRouteTable",
                            "ec2:DeleteRouteTable",
                          + "route53:DeleteHostedZone",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Resource = [
                          - "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                          - "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-*",
                          + "arn:aws:ecs:*:*:cluster/javabin-platform",
                          + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/javabin-*",
                            "arn:aws:s3:::javabin-terraform-*",
                            "arn:aws:s3:::javabin-ci-*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-*",
                          + "arn:aws:dynamodb:*:*:table/javabin-terraform-*",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "guardduty:*",
                            "securityhub:*",
                            "config:*",
                            "cloudtrail:*",
                            "bedrock:Invoke*",
                            "bedrock:Converse*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenySecSvcs"
                    },
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "${aws:PrincipalTag/team}",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (13 unchanged elements hidden)
                            "iam:DetachRolePolicy",
                          - "route53:DeleteHostedZone",
                            "sns:DeleteTopic",
                            # (2 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:TagResource",
                          + "servicediscovery:UntagResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (4 unchanged elements hidden)
                    {
                        Action      = [
                            "s3:CreateBucket",
                            "dynamodb:CreateTable",
                            "sqs:CreateQueue",
                            "sns:CreateTopic",
                            "rds:CreateDBInstance",
                            "rds:CreateDBSubnetGroup",
                            "ecs:CreateService",
                            "ecs:RegisterTaskDefinition",
                            "ecr:CreateRepository",
                            "logs:CreateLogGroup",
                            "ssm:PutParameter",
                            "lambda:CreateFunction",
                            "events:PutRule",
                            "states:CreateStateMachine",
                        ]
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::${aws:PrincipalTag/team}-*",
                            "arn:aws:dynamodb:*:*:table/${aws:PrincipalTag/team}-*",
                            "arn:aws:sqs:*:*:${aws:PrincipalTag/team}-*",
                            "arn:aws:sns:*:*:${aws:PrincipalTag/team}-*",
                            "arn:aws:rds:*:*:db:${aws:PrincipalTag/team}-*",
                            "arn:aws:rds:*:*:subgrp:${aws:PrincipalTag/team}-*",
                            "arn:aws:ecs:*:*:service/*/${aws:PrincipalTag/team}-*",
                            "arn:aws:ecs:*:*:task-definition/${aws:PrincipalTag/team}-*:*",
                            "arn:aws:ecr:*:*:repository/${aws:PrincipalTag/team}-*",
                            "arn:aws:logs:*:*:log-group:/ecs/${aws:PrincipalTag/team}/*",
                            "arn:aws:ssm:*:*:parameter/javabin/apps/${aws:PrincipalTag/team}/*",
                            "arn:aws:lambda:*:*:function:${aws:PrincipalTag/team}-*",
                            "arn:aws:events:*:*:rule/${aws:PrincipalTag/team}-*",
                            "arn:aws:states:*:*:stateMachine:${aws:PrincipalTag/team}-*",
                        ]
                        Sid         = "DenyBadName"
                    },
                  + {
                      + Action    = "route53:ChangeResourceRecordSets"
                      + Condition = {
                          + "ForAnyValue:StringLike" = {
                              + "route53:ChangeResourceRecordSetsNormalizedRecordNames" = "*javabin.internal"
                            }
                        }
                      + Effect    = "Deny"
                      + Resource  = "*"
                      + Sid       = "DenyAppDNS"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name" = "javabin-developer-boundary"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_policy.team_guardrails["cupcake"] will be updated in-place
  ~ resource "aws_iam_policy" "team_guardrails" {
        id               = "arn:aws:iam::553637109631:policy/javabin-team-cupcake-guardrails"
        name             = "javabin-team-cupcake-guardrails"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (5 unchanged elements hidden)
                    {
                        Action   = [
                            "organizations:*",
                            "sso:*",
                            "sso-directory:*",
                            "identitystore:*",
                            "account:*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenyOrgSSO"
                    },
                  ~ {
                      ~ Action   = [
                            # (4 unchanged elements hidden)
                            "ec2:DeleteSubnet",
                          - "ec2:CreateInternetGateway",
                          - "ec2:DeleteInternetGateway",
                          - "ec2:AttachInternetGateway",
                          - "ec2:DetachInternetGateway",
                          - "ec2:CreateNatGateway",
                          - "ec2:DeleteNatGateway",
                          + "ec2:*InternetGateway",
                          + "ec2:*NatGateway",
                            "ec2:CreateRouteTable",
                            "ec2:DeleteRouteTable",
                          + "route53:DeleteHostedZone",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Resource = [
                          - "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                          - "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-*",
                          + "arn:aws:ecs:*:*:cluster/javabin-platform",
                          + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/javabin-*",
                            "arn:aws:s3:::javabin-terraform-*",
                            "arn:aws:s3:::javabin-ci-*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-*",
                          + "arn:aws:dynamodb:*:*:table/javabin-terraform-*",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "guardduty:*",
                            "securityhub:*",
                            "config:*",
                            "cloudtrail:*",
                            "bedrock:Invoke*",
                            "bedrock:Converse*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenySecSvcs"
                    },
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "cupcake",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (13 unchanged elements hidden)
                            "iam:DetachRolePolicy",
                          - "route53:DeleteHostedZone",
                            "sns:DeleteTopic",
                            # (2 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:TagResource",
                          + "servicediscovery:UntagResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (4 unchanged elements hidden)
                    {
                        Action      = [
                            "s3:CreateBucket",
                            "dynamodb:CreateTable",
                            "sqs:CreateQueue",
                            "sns:CreateTopic",
                            "rds:CreateDBInstance",
                            "rds:CreateDBSubnetGroup",
                            "ecs:CreateService",
                            "ecs:RegisterTaskDefinition",
                            "ecr:CreateRepository",
                            "logs:CreateLogGroup",
                            "ssm:PutParameter",
                            "lambda:CreateFunction",
                            "events:PutRule",
                            "states:CreateStateMachine",
                        ]
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::cupcake-*",
                            "arn:aws:dynamodb:*:*:table/cupcake-*",
                            "arn:aws:sqs:*:*:cupcake-*",
                            "arn:aws:sns:*:*:cupcake-*",
                            "arn:aws:rds:*:*:db:cupcake-*",
                            "arn:aws:rds:*:*:subgrp:cupcake-*",
                            "arn:aws:ecs:*:*:service/*/cupcake-*",
                            "arn:aws:ecs:*:*:task-definition/cupcake-*:*",
                            "arn:aws:ecr:*:*:repository/cupcake-*",
                            "arn:aws:logs:*:*:log-group:/ecs/cupcake/*",
                            "arn:aws:ssm:*:*:parameter/javabin/apps/cupcake/*",
                            "arn:aws:lambda:*:*:function:cupcake-*",
                            "arn:aws:events:*:*:rule/cupcake-*",
                            "arn:aws:states:*:*:stateMachine:cupcake-*",
                        ]
                        Sid         = "DenyBadName"
                    },
                  + {
                      + Action    = "route53:ChangeResourceRecordSets"
                      + Condition = {
                          + "ForAnyValue:StringLike" = {
                              + "route53:ChangeResourceRecordSetsNormalizedRecordNames" = "*javabin.internal"
                            }
                        }
                      + Effect    = "Deny"
                      + Resource  = "*"
                      + Sid       = "DenyAppDNS"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name"     = "javabin-team-cupcake-guardrails"
            "for-team" = "cupcake"
            "team"     = "shared"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_policy.team_guardrails["javazoneweb"] will be updated in-place
  ~ resource "aws_iam_policy" "team_guardrails" {
        id               = "arn:aws:iam::553637109631:policy/javabin-team-javazoneweb-guardrails"
        name             = "javabin-team-javazoneweb-guardrails"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (5 unchanged elements hidden)
                    {
                        Action   = [
                            "organizations:*",
                            "sso:*",
                            "sso-directory:*",
                            "identitystore:*",
                            "account:*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenyOrgSSO"
                    },
                  ~ {
                      ~ Action   = [
                            # (4 unchanged elements hidden)
                            "ec2:DeleteSubnet",
                          - "ec2:CreateInternetGateway",
                          - "ec2:DeleteInternetGateway",
                          - "ec2:AttachInternetGateway",
                          - "ec2:DetachInternetGateway",
                          - "ec2:CreateNatGateway",
                          - "ec2:DeleteNatGateway",
                          + "ec2:*InternetGateway",
                          + "ec2:*NatGateway",
                            "ec2:CreateRouteTable",
                            "ec2:DeleteRouteTable",
                          + "route53:DeleteHostedZone",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Resource = [
                          - "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                          - "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-*",
                          + "arn:aws:ecs:*:*:cluster/javabin-platform",
                          + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/javabin-*",
                            "arn:aws:s3:::javabin-terraform-*",
                            "arn:aws:s3:::javabin-ci-*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-*",
                          + "arn:aws:dynamodb:*:*:table/javabin-terraform-*",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "guardduty:*",
                            "securityhub:*",
                            "config:*",
                            "cloudtrail:*",
                            "bedrock:Invoke*",
                            "bedrock:Converse*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenySecSvcs"
                    },
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "javazoneweb",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (13 unchanged elements hidden)
                            "iam:DetachRolePolicy",
                          - "route53:DeleteHostedZone",
                            "sns:DeleteTopic",
                            # (2 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:TagResource",
                          + "servicediscovery:UntagResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (4 unchanged elements hidden)
                    {
                        Action      = [
                            "s3:CreateBucket",
                            "dynamodb:CreateTable",
                            "sqs:CreateQueue",
                            "sns:CreateTopic",
                            "rds:CreateDBInstance",
                            "rds:CreateDBSubnetGroup",
                            "ecs:CreateService",
                            "ecs:RegisterTaskDefinition",
                            "ecr:CreateRepository",
                            "logs:CreateLogGroup",
                            "ssm:PutParameter",
                            "lambda:CreateFunction",
                            "events:PutRule",
                            "states:CreateStateMachine",
                        ]
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javazoneweb-*",
                            "arn:aws:dynamodb:*:*:table/javazoneweb-*",
                            "arn:aws:sqs:*:*:javazoneweb-*",
                            "arn:aws:sns:*:*:javazoneweb-*",
                            "arn:aws:rds:*:*:db:javazoneweb-*",
                            "arn:aws:rds:*:*:subgrp:javazoneweb-*",
                            "arn:aws:ecs:*:*:service/*/javazoneweb-*",
                            "arn:aws:ecs:*:*:task-definition/javazoneweb-*:*",
                            "arn:aws:ecr:*:*:repository/javazoneweb-*",
                            "arn:aws:logs:*:*:log-group:/ecs/javazoneweb/*",
                            "arn:aws:ssm:*:*:parameter/javabin/apps/javazoneweb/*",
                            "arn:aws:lambda:*:*:function:javazoneweb-*",
                            "arn:aws:events:*:*:rule/javazoneweb-*",
                            "arn:aws:states:*:*:stateMachine:javazoneweb-*",
                        ]
                        Sid         = "DenyBadName"
                    },
                  + {
                      + Action    = "route53:ChangeResourceRecordSets"
                      + Condition = {
                          + "ForAnyValue:StringLike" = {
                              + "route53:ChangeResourceRecordSetsNormalizedRecordNames" = "*javabin.internal"
                            }
                        }
                      + Effect    = "Deny"
                      + Resource  = "*"
                      + Sid       = "DenyAppDNS"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name"     = "javabin-team-javazoneweb-guardrails"
            "for-team" = "javazoneweb"
            "team"     = "shared"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_policy.team_guardrails["jzwatch"] will be updated in-place
  ~ resource "aws_iam_policy" "team_guardrails" {
        id               = "arn:aws:iam::553637109631:policy/javabin-team-jzwatch-guardrails"
        name             = "javabin-team-jzwatch-guardrails"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (5 unchanged elements hidden)
                    {
                        Action   = [
                            "organizations:*",
                            "sso:*",
                            "sso-directory:*",
                            "identitystore:*",
                            "account:*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenyOrgSSO"
                    },
                  ~ {
                      ~ Action   = [
                            # (4 unchanged elements hidden)
                            "ec2:DeleteSubnet",
                          - "ec2:CreateInternetGateway",
                          - "ec2:DeleteInternetGateway",
                          - "ec2:AttachInternetGateway",
                          - "ec2:DetachInternetGateway",
                          - "ec2:CreateNatGateway",
                          - "ec2:DeleteNatGateway",
                          + "ec2:*InternetGateway",
                          + "ec2:*NatGateway",
                            "ec2:CreateRouteTable",
                            "ec2:DeleteRouteTable",
                          + "route53:DeleteHostedZone",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Resource = [
                          - "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                          - "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-*",
                          + "arn:aws:ecs:*:*:cluster/javabin-platform",
                          + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/javabin-*",
                            "arn:aws:s3:::javabin-terraform-*",
                            "arn:aws:s3:::javabin-ci-*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-*",
                          + "arn:aws:dynamodb:*:*:table/javabin-terraform-*",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "guardduty:*",
                            "securityhub:*",
                            "config:*",
                            "cloudtrail:*",
                            "bedrock:Invoke*",
                            "bedrock:Converse*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenySecSvcs"
                    },
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "jzwatch",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (13 unchanged elements hidden)
                            "iam:DetachRolePolicy",
                          - "route53:DeleteHostedZone",
                            "sns:DeleteTopic",
                            # (2 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:TagResource",
                          + "servicediscovery:UntagResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (4 unchanged elements hidden)
                    {
                        Action      = [
                            "s3:CreateBucket",
                            "dynamodb:CreateTable",
                            "sqs:CreateQueue",
                            "sns:CreateTopic",
                            "rds:CreateDBInstance",
                            "rds:CreateDBSubnetGroup",
                            "ecs:CreateService",
                            "ecs:RegisterTaskDefinition",
                            "ecr:CreateRepository",
                            "logs:CreateLogGroup",
                            "ssm:PutParameter",
                            "lambda:CreateFunction",
                            "events:PutRule",
                            "states:CreateStateMachine",
                        ]
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::jzwatch-*",
                            "arn:aws:dynamodb:*:*:table/jzwatch-*",
                            "arn:aws:sqs:*:*:jzwatch-*",
                            "arn:aws:sns:*:*:jzwatch-*",
                            "arn:aws:rds:*:*:db:jzwatch-*",
                            "arn:aws:rds:*:*:subgrp:jzwatch-*",
                            "arn:aws:ecs:*:*:service/*/jzwatch-*",
                            "arn:aws:ecs:*:*:task-definition/jzwatch-*:*",
                            "arn:aws:ecr:*:*:repository/jzwatch-*",
                            "arn:aws:logs:*:*:log-group:/ecs/jzwatch/*",
                            "arn:aws:ssm:*:*:parameter/javabin/apps/jzwatch/*",
                            "arn:aws:lambda:*:*:function:jzwatch-*",
                            "arn:aws:events:*:*:rule/jzwatch-*",
                            "arn:aws:states:*:*:stateMachine:jzwatch-*",
                        ]
                        Sid         = "DenyBadName"
                    },
                  + {
                      + Action    = "route53:ChangeResourceRecordSets"
                      + Condition = {
                          + "ForAnyValue:StringLike" = {
                              + "route53:ChangeResourceRecordSetsNormalizedRecordNames" = "*javabin.internal"
                            }
                        }
                      + Effect    = "Deny"
                      + Resource  = "*"
                      + Sid       = "DenyAppDNS"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name"     = "javabin-team-jzwatch-guardrails"
            "for-team" = "jzwatch"
            "team"     = "shared"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_policy.team_guardrails["testteam"] will be updated in-place
  ~ resource "aws_iam_policy" "team_guardrails" {
        id               = "arn:aws:iam::553637109631:policy/javabin-team-testteam-guardrails"
        name             = "javabin-team-testteam-guardrails"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (5 unchanged elements hidden)
                    {
                        Action   = [
                            "organizations:*",
                            "sso:*",
                            "sso-directory:*",
                            "identitystore:*",
                            "account:*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenyOrgSSO"
                    },
                  ~ {
                      ~ Action   = [
                            # (4 unchanged elements hidden)
                            "ec2:DeleteSubnet",
                          - "ec2:CreateInternetGateway",
                          - "ec2:DeleteInternetGateway",
                          - "ec2:AttachInternetGateway",
                          - "ec2:DetachInternetGateway",
                          - "ec2:CreateNatGateway",
                          - "ec2:DeleteNatGateway",
                          + "ec2:*InternetGateway",
                          + "ec2:*NatGateway",
                            "ec2:CreateRouteTable",
                            "ec2:DeleteRouteTable",
                          + "route53:DeleteHostedZone",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Resource = [
                          - "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                          - "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-*",
                          + "arn:aws:ecs:*:*:cluster/javabin-platform",
                          + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/javabin-*",
                            "arn:aws:s3:::javabin-terraform-*",
                            "arn:aws:s3:::javabin-ci-*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-*",
                          + "arn:aws:dynamodb:*:*:table/javabin-terraform-*",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "guardduty:*",
                            "securityhub:*",
                            "config:*",
                            "cloudtrail:*",
                            "bedrock:Invoke*",
                            "bedrock:Converse*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenySecSvcs"
                    },
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "testteam",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (13 unchanged elements hidden)
                            "iam:DetachRolePolicy",
                          - "route53:DeleteHostedZone",
                            "sns:DeleteTopic",
                            # (2 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:TagResource",
                          + "servicediscovery:UntagResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (4 unchanged elements hidden)
                    {
                        Action      = [
                            "s3:CreateBucket",
                            "dynamodb:CreateTable",
                            "sqs:CreateQueue",
                            "sns:CreateTopic",
                            "rds:CreateDBInstance",
                            "rds:CreateDBSubnetGroup",
                            "ecs:CreateService",
                            "ecs:RegisterTaskDefinition",
                            "ecr:CreateRepository",
                            "logs:CreateLogGroup",
                            "ssm:PutParameter",
                            "lambda:CreateFunction",
                            "events:PutRule",
                            "states:CreateStateMachine",
                        ]
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::testteam-*",
                            "arn:aws:dynamodb:*:*:table/testteam-*",
                            "arn:aws:sqs:*:*:testteam-*",
                            "arn:aws:sns:*:*:testteam-*",
                            "arn:aws:rds:*:*:db:testteam-*",
                            "arn:aws:rds:*:*:subgrp:testteam-*",
                            "arn:aws:ecs:*:*:service/*/testteam-*",
                            "arn:aws:ecs:*:*:task-definition/testteam-*:*",
                            "arn:aws:ecr:*:*:repository/testteam-*",
                            "arn:aws:logs:*:*:log-group:/ecs/testteam/*",
                            "arn:aws:ssm:*:*:parameter/javabin/apps/testteam/*",
                            "arn:aws:lambda:*:*:function:testteam-*",
                            "arn:aws:events:*:*:rule/testteam-*",
                            "arn:aws:states:*:*:stateMachine:testteam-*",
                        ]
                        Sid         = "DenyBadName"
                    },
                  + {
                      + Action    = "route53:ChangeResourceRecordSets"
                      + Condition = {
                          + "ForAnyValue:StringLike" = {
                              + "route53:ChangeResourceRecordSetsNormalizedRecordNames" = "*javabin.internal"
                            }
                        }
                      + Effect    = "Deny"
                      + Resource  = "*"
                      + Sid       = "DenyAppDNS"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name"     = "javabin-team-testteam-guardrails"
            "for-team" = "testteam"
            "team"     = "shared"
        }
        # (6 unchanged attributes hidden)
    }

  # module.identity.aws_cognito_identity_provider.internal_google[0] will be updated in-place
  ~ resource "aws_cognito_identity_provider" "internal_google" {
        id                = "eu-central-1_Icikv3dtD:Google"
      ~ provider_details  = {
          ~ "authorize_url"                 = "https://accounts.google.com/o/oauth2/v2/auth" -> "https://accounts.google.com/o/oauth2/v2/auth?hd=java.no&prompt=select_account"
            # (8 unchanged elements hidden)
        }
        # (5 unchanged attributes hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.app_peers_sync_reconcile will be created
  + resource "aws_cloudwatch_event_rule" "app_peers_sync_reconcile" {
      + arn                 = (known after apply)
      + description         = "Daily reconcile and audit of app-to-app calls"
      + event_bus_name      = "default"
      + force_destroy       = false
      + id                  = (known after apply)
      + name                = "javabin-app-peers-sync-reconcile"
      + name_prefix         = (known after apply)
      + schedule_expression = "cron(45 3 * * ? *)"
      + tags_all            = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_event_rule.app_peers_sync_trigger will be created
  + resource "aws_cloudwatch_event_rule" "app_peers_sync_trigger" {
      + arn            = (known after apply)
      + description    = "app.yaml calls/provides request created, changed or deleted"
      + event_bus_name = "default"
      + event_pattern  = jsonencode(
            {
              + detail      = {
                  + name      = [
                      + {
                          + wildcard = "/javabin/apps/*/*/network"
                        },
                    ]
                  + operation = [
                      + "Create",
                      + "Update",
                      + "Delete",
                    ]
                }
              + detail-type = [
                  + "Parameter Store Change",
                ]
              + source      = [
                  + "aws.ssm",
                ]
            }
        )
      + force_destroy  = false
      + id             = (known after apply)
      + name           = "javabin-app-peers-sync-trigger"
      + name_prefix    = (known after apply)
      + tags_all       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_event_target.app_peers_sync will be created
  + resource "aws_cloudwatch_event_target" "app_peers_sync" {
      + arn            = (known after apply)
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + rule           = "javabin-app-peers-sync-trigger"
      + target_id      = "invoke-app-peers-sync"
    }

  # module.lambdas.aws_cloudwatch_event_target.app_peers_sync_reconcile will be created
  + resource "aws_cloudwatch_event_target" "app_peers_sync_reconcile" {
      + arn            = (known after apply)
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + input          = jsonencode(
            {
              + action = "reconcile"
            }
        )
      + rule           = "javabin-app-peers-sync-reconcile"
      + target_id      = "reconcile-app-peers-sync"
    }

  # module.lambdas.aws_cloudwatch_log_group.lambda["app-peers-sync"] will be created
  + resource "aws_cloudwatch_log_group" "lambda" {
      + arn               = (known after apply)
      + id                = (known after apply)
      + log_group_class   = (known after apply)
      + name              = "/aws/lambda/javabin-app-peers-sync"
      + name_prefix       = (known after apply)
      + retention_in_days = 365
      + skip_destroy      = false
      + tags_all          = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_iam_role.app_peers_sync will be created
  + resource "aws_iam_role" "app_peers_sync" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "lambda.amazonaws.com"
                        }
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "javabin-app-peers-sync"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags_all              = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + unique_id             = (known after apply)
    }

  # module.lambdas.aws_iam_role_policy.app_peers_sync will be created
  + resource "aws_iam_role_policy" "app_peers_sync" {
      + id          = (known after apply)
      + name        = "javabin-app-peers-sync"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = [
                          + "ssm:GetParameter",
                          + "ssm:GetParametersByPath",
                          + "ssm:ListTagsForResource",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*",
                        ]
                      + Sid      = "ReadAppRequests"
                    },
                  + {
                      + Action   = [
                          + "ssm:PutParameter",
                          + "ssm:DeleteParameter",
                          + "ssm:DeleteParameters",
                          + "ssm:AddTagsToResource",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*/*/peer-*",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*/*/m2m-client-*",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*/*/peers-status",
                        ]
                      + Sid      = "WritePeerParameters"
                    },
                  + {
                      + Action   = "ssm:GetParameter"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/slack/platform-resource-alerts-webhook"
                      + Sid      = "ReadSlackWebhook"
                    },
                  + {
                      + Action    = [
                          + "ec2:AuthorizeSecurityGroupIngress",
                          + "ec2:RevokeSecurityGroupIngress",
                        ]
                      + Condition = {
                          + ArnEquals    = {
                              + "ec2:Vpc" = "arn:aws:ec2:eu-central-1:553637109631:vpc/vpc-0cd3502de2527a310"
                            }
                          + StringEquals = {
                              + "aws:ResourceTag/javabin-network" = "app"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:ec2:eu-central-1:553637109631:security-group/*"
                      + Sid       = "EditAppSecurityGroups"
                    },
                  + {
                      + Action   = [
                          + "ec2:AuthorizeSecurityGroupIngress",
                          + "ec2:RevokeSecurityGroupIngress",
                        ]
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ec2:eu-central-1:553637109631:security-group-rule/*"
                      + Sid      = "EditPeerRules"
                    },
                  + {
                      + Action    = "ec2:CreateTags"
                      + Condition = {
                          + StringEquals = {
                              + "ec2:CreateAction" = "AuthorizeSecurityGroupIngress"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:ec2:eu-central-1:553637109631:security-group-rule/*"
                      + Sid       = "TagPeerRulesOnCreate"
                    },
                  + {
                      + Action   = [
                          + "ec2:DescribeSecurityGroups",
                          + "ec2:DescribeSecurityGroupRules",
                          + "ec2:DescribeNetworkInterfaces",
                          + "servicediscovery:ListServices",
                          + "servicediscovery:ListInstances",
                          + "servicediscovery:ListTagsForResource",
                          + "ecs:ListClusters",
                          + "ecs:ListTasks",
                        ]
                      + Effect   = "Allow"
                      + Resource = "*"
                      + Sid      = "Describe"
                    },
                  + {
                      + Action   = "route53:ListResourceRecordSets"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:route53:::hostedzone/*"
                      + Sid      = "AuditPrivateZone"
                    },
                  + {
                      + Action   = "ecs:DescribeTasks"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ecs:eu-central-1:553637109631:task/*"
                      + Sid      = "DescribeAppTasks"
                    },
                  + {
                      + Action   = [
                          + "cognito-idp:CreateResourceServer",
                          + "cognito-idp:UpdateResourceServer",
                          + "cognito-idp:DeleteResourceServer",
                          + "cognito-idp:DescribeResourceServer",
                          + "cognito-idp:ListResourceServers",
                          + "cognito-idp:CreateUserPoolClient",
                          + "cognito-idp:UpdateUserPoolClient",
                          + "cognito-idp:DeleteUserPoolClient",
                          + "cognito-idp:DescribeUserPoolClient",
                          + "cognito-idp:ListUserPoolClients",
                        ]
                      + Effect   = "Allow"
                      + Resource = "arn:aws:cognito-idp:eu-central-1:553637109631:userpool/eu-central-1_Icikv3dtD"
                      + Sid      = "ManageM2M"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + role        = (known after apply)
    }

  # module.lambdas.aws_iam_role_policy_attachment.app_peers_sync_logs will be created
  + resource "aws_iam_role_policy_attachment" "app_peers_sync_logs" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      + role       = "javabin-app-peers-sync"
    }

  # module.lambdas.aws_lambda_function.app_peers_sync will be created
  + resource "aws_lambda_function" "app_peers_sync" {
      + architectures                  = (known after apply)
      + arn                            = (known after apply)
      + code_sha256                    = (known after apply)
      + filename                       = "lambdas/builds/app_peers_sync.zip"
      + function_name                  = "javabin-app-peers-sync"
      + handler                        = "handler.handler"
      + id                             = (known after apply)
      + invoke_arn                     = (known after apply)
      + last_modified                  = (known after apply)
      + memory_size                    = 128
      + package_type                   = "Zip"
      + publish                        = false
      + qualified_arn                  = (known after apply)
      + qualified_invoke_arn           = (known after apply)
      + reserved_concurrent_executions = 1
      + role                           = (known after apply)
      + runtime                        = "python3.12"
      + signing_job_arn                = (known after apply)
      + signing_profile_version_arn    = (known after apply)
      + skip_destroy                   = false
      + source_code_hash               = "Ul5YRq3XRu7ocIrOuEdlXddQjeDdp7Iqe7XCHxLAjpY="
      + source_code_size               = (known after apply)
      + tags_all                       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + timeout                        = 120
      + version                        = (known after apply)

      + environment {
          + variables = (known after apply)
        }
    }

  # module.lambdas.aws_lambda_function.app_secret_rollout will be updated in-place
  ~ resource "aws_lambda_function" "app_secret_rollout" {
        id                             = "javabin-app-secret-rollout"
      ~ last_modified                  = "2026-10-24T22:51:01.000+0000" -> (known after apply)
      ~ source_code_hash               = "IIeAoHH+LEqFBOPwakogwUQGHBkHIBEugTSRdS+r/EI=" -> "/3zU8NzvBOE12Mku6DTfgI889lUaC8PjPgzOly0UShE="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.plan_review will be updated in-place
  ~ resource "aws_lambda_function" "plan_review" {
        id                             = "javabin-plan-review"
      ~ last_modified                  = "2026-10-24T10:17:54.000+0000" -> (known after apply)
      ~ source_code_hash               = "e9X15ZWURfd4volxoEzZ3Pio4Ye7IRZdyO+wqk0tJQk=" -> "pssbwXoZg3c98wMsTKW//8ZpVy84IiWLjExZQClIFvY="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_permission.app_peers_sync_eventbridge will be created
  + resource "aws_lambda_permission" "app_peers_sync_eventbridge" {
      + action              = "lambda:InvokeFunction"
      + function_name       = "javabin-app-peers-sync"
      + id                  = (known after apply)
      + principal           = "events.amazonaws.com"
      + source_arn          = (known after apply)
      + statement_id        = "AllowEventBridge"
      + statement_id_prefix = (known after apply)
    }

  # module.lambdas.aws_lambda_permission.app_peers_sync_reconcile will be created
  + resource "aws_lambda_permission" "app_peers_sync_reconcile" {
      + action              = "lambda:InvokeFunction"
      + function_name       = "javabin-app-peers-sync"
      + id                  = (known after apply)
      + principal           = "events.amazonaws.com"
      + source_arn          = (known after apply)
      + statement_id        = "AllowEventBridgeReconcile"
      + statement_id_prefix = (k

✂️ Plan output above is cut at 60000 of 61274 characters (GitHub comment size limit). The full plan is in the workflow log; the LLM review below is not affected by this cut.


LLM Review

Risk: 🔴 HIGH

Plan creates 12 resources (app-peers-sync Lambda with EventBridge rules, IAM role, service discovery namespace) and updates 8 in place, mainly IAM guardrail and developer boundary policies, with 0 destroys; the boundary changes remove the ability to manage IGWs/NAT gateways and Route53 hosted zone deletion and add a DenyAppDNS statement, and the Lambda role gets security group editing rights.

  • 🔒 [security] The developer boundary and all four team_guardrails policies are changed so the Deny/Allow semantics shift. Removed from the ec2 list are Create/Delete/Attach/Detach InternetGateway and Create/Delete NatGateway, replaced by wildcards 'ec2:*InternetGateway' and 'ec2:*NatGateway'. Removed route53:DeleteHostedZone is re-added to a Deny list, and a new route53:ChangeResourceRecordSets Deny (DenyAppDNS) on *javabin.internal is added. Because the Action and Effect lines of unchanged statements are hidden, verify these are intended Deny-side changes and not broader grants. The wildcard ARN changes in the ECS and ELB resource lists also widen the account scope from eu-central-1 to any region.
  • 🔒 [security] New Lambda role javabin-app-peers-sync has broad rights: ec2:AuthorizeSecurityGroupIngress/RevokeSecurityGroupIngress on any security group in the VPC tagged javabin-network=app, cognito-idp Create/Delete UserPoolClient and ResourceServer on the production user pool, and ssm Put/Delete on javabin/apps///peer-*. It also has a permissions boundary of javabin-org-boundary. Review whether these are minimal, since a bug or compromise could open app security groups or alter Cognito clients.
  • 🔒 [security] Cognito Google identity provider authorize_url changes to add hd=java.no&prompt=select_account. This restricts Google sign-in to the java.no domain (hd is a hint, not a hard enforcement) and forces account selection. Low risk, but confirm it is intended.
  • 💥 [destruction] 0 resources are destroyed. The Lambda functions app_secret_rollout and plan_review only have code hash updates in place. Both are routine code deployments.
  • 💰 [cost] New resources are low cost: Lambda (128 MB, reserved concurrency 1), two EventBridge rules, CloudWatch log group with 365-day retention, and a Cloud Map private DNS namespace (javabin.internal, which carries a small hosted zone charge). No NAT gateways, RDS or load balancers are created.

approve-override.yml's "Risk Override Approved" Slack step has always logged
"No webhook URL available": javabin-ci-override-approver could only invoke
the apply gate. Grant ssm:GetParameter on that one parameter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Alexanderamiri Alexanderamiri changed the title fix(ci): stop LLM review crashing on stringified findings and deliver override alerts fix(ci): fix LLM review crash and deliver override alerts to Slack Oct 29, 2026
@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 0 to add, 3 to change, 0 to destroy.

Plan output
Acquiring state lock. This may take a few moments...
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.iam.aws_iam_role_policy.ci_override_approver will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_override_approver" {
        id     = "javabin-ci-override-approver:invoke-apply-gate"
        name   = "invoke-apply-gate"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    {
                        Action   = "lambda:InvokeFunction"
                        Effect   = "Allow"
                        Resource = "arn:aws:lambda:eu-central-1:553637109631:function:javabin-apply-gate"
                    },
                  + {
                      + Action   = "ssm:GetParameter"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/slack/platform-override-alerts-webhook"
                      + Sid      = "ReadOverrideAlertsWebhook"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.identity.aws_cognito_identity_provider.internal_google[0] will be updated in-place
  ~ resource "aws_cognito_identity_provider" "internal_google" {
        id                = "eu-central-1_Icikv3dtD:Google"
      ~ provider_details  = {
          ~ "authorize_url"                 = "https://accounts.google.com/o/oauth2/v2/auth" -> "https://accounts.google.com/o/oauth2/v2/auth?hd=java.no&prompt=select_account"
            # (8 unchanged elements hidden)
        }
        # (5 unchanged attributes hidden)
    }

  # module.lambdas.aws_lambda_function.plan_review will be updated in-place
  ~ resource "aws_lambda_function" "plan_review" {
        id                             = "javabin-plan-review"
      ~ last_modified                  = "2026-10-24T10:17:54.000+0000" -> (known after apply)
      ~ source_code_hash               = "e9X15ZWURfd4volxoEzZ3Pio4Ye7IRZdyO+wqk0tJQk=" -> "pssbwXoZg3c98wMsTKW//8ZpVy84IiWLjExZQClIFvY="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

Plan: 0 to add, 3 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

LLM Review

Risk: 🟡 MEDIUM

Plan updates 3 existing resources in place (0 add, 0 destroy): adds an SSM read permission for a Slack webhook parameter to the CI override approver role, restricts Google identity provider login to the java.no hosted domain, and redeploys the plan-review Lambda code.

  • 🔒 [security] The ci_override_approver inline policy gains a new statement with Effect = Allow for ssm:GetParameter on the Slack webhook parameter javabin/slack/platform-override-alerts-webhook (ReadOverrideAlertsWebhook). This is a new grant of read access to a secret-like value, scoped to a single parameter ARN. Confirm the role genuinely needs it and that the parameter is SecureString-encrypted; the plan does not show KMS permissions for decryption.
  • 🔒 [security] The Google identity provider authorize_url adds hd=java.no and prompt=select_account. This restricts the Google hosted domain hint to java.no, which tightens who can sign in via the Google IdP. Note that hd is a UI hint only, not an enforced server-side check, so the enforcement should be verified in the app's token validation (e.g. checking the hd claim).
  • ✅ [routine] The plan-review Lambda source_code_hash changes (new deployment package); no configuration, role, or environment change is shown. This is a routine code update.
  • 💥 [destruction] No resources are created or destroyed (Plan: 0 to add, 0 to destroy). No data-loss or force-replacement risk is present.
  • 💰 [cost] No new billable resources are introduced; the cost impact is negligible.

@Alexanderamiri
Alexanderamiri merged commit b44efa6 into main Oct 29, 2026
4 checks passed
@Alexanderamiri
Alexanderamiri deleted the fix/review-alerts branch October 29, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant