Skip to content

Add Meridian Risk blueprint: governed multi-agent KYC on AgentCore - #1950

Open
vivibui wants to merge 1 commit into
awslabs:mainfrom
vivibui:add-meridian-risk-blueprint
Open

Add Meridian Risk blueprint: governed multi-agent KYC on AgentCore#1950
vivibui wants to merge 1 commit into
awslabs:mainfrom
vivibui:add-meridian-risk-blueprint

Conversation

@vivibui

@vivibui vivibui commented Aug 13, 2026

Copy link
Copy Markdown

Summary

Adds Meridian Risk — a governed multi-agent KYC onboarding desk on Amazon Bedrock AgentCore — as a new full-stack blueprint under 05-blueprints/meridian-risk/.

A Credit Analyst and a Compliance Officer run concurrently against the Gateway's KYC tools, then a supervisor synthesizes one auditable APPROVE / REJECT / ESCALATE decision with a 0–100 risk score. The emphasis is platform governance — proving an agent was only ever allowed to call the tools it was scoped to.

What this blueprint demonstrates

Six AgentCore services working together:

  • Runtime — a Strands multi-agent workflow (two specialists in parallel, then synthesis)
  • Gateway — a single governed ingress fronting both the five KYC tools (MCP target) and Bedrock models (inference target — the "LLM-gateway" pattern)
  • Policy — Cedar authorization enforced server-side on every request in ENFORCE mode
  • Memory — per-customer assessment history
  • Agent Registry — governed catalog with a DRAFT → APPROVED workflow
  • Harness — the same assistant expressed as configuration (declarative counterpart to the code-defined Runtime)

Also includes Bedrock Guardrails and ADOT/OpenTelemetry observability (traces and spans in CloudWatch Transaction Search).

Stack & deploy

  • Terraform IaC, React console (light/dark), FastAPI console API on Lambda, ARM64 agent container
  • One-command deploy (./scripts/deploy.sh) with a harness skill smoke test
  • Synthetic customer data only

Notes

  • Adds a new directory under 05-blueprints/ — no other blueprints modified
  • No secrets, state, or hardcoded deployment-specific identifiers (Amplify domains are Terraform interpolations; account IDs are AWS-doc placeholders / EXAMPLE mock values only)

A full-stack, deployment-ready blueprint under 05-blueprints/meridian-risk: a
governed multi-agent KYC onboarding desk on Amazon Bedrock AgentCore. A Credit
Analyst and a Compliance Officer run concurrently against the Gateway's KYC
tools, then a supervisor synthesizes one auditable APPROVE / REJECT / ESCALATE
decision with a 0-100 risk score.

Uses six AgentCore services together: Runtime (Strands multi-agent workflow),
Gateway as a single governed ingress fronting both the KYC tools (MCP target)
and Bedrock models (inference target — the LLM-gateway pattern), Policy (Cedar
authorization enforced on every request), Memory (per-customer history), Agent
Registry (governed catalog), and a managed Harness (the same assistant as
config). Adds Bedrock Guardrails and ADOT/OpenTelemetry observability. Terraform
IaC, React console, one-command deploy. Synthetic data only; no secrets or
deployment-specific identifiers.
@BharathiSrini
BharathiSrini self-requested a review August 18, 2026 15:51


def log(message: str) -> None:
print(f"[oauth-provider] {message}", flush=True)
yield _sse(event)

return StreamingResponse(
stream(),
@github-actions

Copy link
Copy Markdown

Latest scan for commit: 213c9cb | Updated: 2026-08-18 15:57:58 UTC

Security Scan Results

Scan Metadata

  • Project: ASH
  • Scan executed: 2026-08-18T15:57:40+00:00
  • ASH version: 3.0.0

Summary

Scanner Results

The table below shows findings by scanner, with status based on severity thresholds and dependencies:

Column Explanations:

Severity Levels (S/C/H/M/L/I):

  • Suppressed (S): Security findings that have been explicitly suppressed/ignored and don't affect the scanner's pass/fail status
  • Critical (C): The most severe security vulnerabilities requiring immediate remediation (e.g., SQL injection, remote code execution)
  • High (H): Serious security vulnerabilities that should be addressed promptly (e.g., authentication bypasses, privilege escalation)
  • Medium (M): Moderate security risks that should be addressed in normal development cycles (e.g., weak encryption, input validation issues)
  • Low (L): Minor security concerns with limited impact (e.g., information disclosure, weak recommendations)
  • Info (I): Informational findings for awareness with minimal security risk (e.g., code quality suggestions, best practice recommendations)

Other Columns:

  • Time: Duration taken by each scanner to complete its analysis
  • Action: Total number of actionable findings at or above the configured severity threshold that require attention

Scanner Results:

  • PASSED: Scanner found no security issues at or above the configured severity threshold - code is clean for this scanner
  • FAILED: Scanner found security vulnerabilities at or above the threshold that require attention and remediation
  • MISSING: Scanner could not run because required dependencies/tools are not installed or available
  • SKIPPED: Scanner was intentionally disabled or excluded from this scan
  • ERROR: Scanner encountered an execution error and could not complete successfully

Severity Thresholds (Thresh Column):

  • CRITICAL: Only Critical severity findings cause scanner to fail
  • HIGH: High and Critical severity findings cause scanner to fail
  • MEDIUM (MED): Medium, High, and Critical severity findings cause scanner to fail
  • LOW: Low, Medium, High, and Critical severity findings cause scanner to fail
  • ALL: Any finding of any severity level causes scanner to fail

Threshold Source: Values in parentheses indicate where the threshold is configured:

  • (g) = global: Set in the global_settings section of ASH configuration
  • (c) = config: Set in the individual scanner configuration section
  • (s) = scanner: Default threshold built into the scanner itself

Statistics calculation:

  • All statistics are calculated from the final aggregated SARIF report
  • Suppressed findings are counted separately and do not contribute to actionable findings
  • Scanner status is determined by comparing actionable findings to the threshold
Scanner S C H M L I Time Action Result Thresh
bandit 0 4 0 0 12 0 1.8s 4 FAILED MED (g)
cdk-nag 0 0 0 0 0 0 9.4s 0 PASSED MED (g)
cfn-nag 0 0 0 0 0 0 423ms 0 PASSED MED (g)
checkov 0 28 0 0 0 0 11.2s 28 FAILED MED (g)
detect-secre… 0 2 0 0 0 0 2.2s 2 FAILED MED (g)
grype 0 2 0 0 0 0 1m 1s 2 FAILED MED (g)
npm-audit 0 0 0 0 0 0 1.3s 0 PASSED MED (g)
opengrep 0 0 0 0 0 0 <1ms 0 SKIPPED MED (g)
semgrep 0 0 0 0 0 0 <1ms 0 MISSING MED (g)
syft 0 0 0 0 0 0 3.3s 0 PASSED MED (g)

Detailed Findings

Show 36 actionable findings

Finding 1: B310

  • Severity: HIGH
  • Scanner: bandit
  • Rule ID: B310
  • Location: 05-blueprints/meridian-risk/backend/api/auth.py:65-67

Description:
Audit url open for permitted schemes. Allowing use of file:/ or custom schemes is often unexpected.

Code Snippet:

# nosec B310 — the URL is built from our own pool ID, not user input.
    with urlopen(url, timeout=5) as response:  # noqa: S310
        import json

Finding 2: B310

  • Severity: HIGH
  • Scanner: bandit
  • Rule ID: B310
  • Location: 05-blueprints/meridian-risk/scripts/deploy_frontend.py:103-105

Description:
Audit url open for permitted schemes. Allowing use of file:/ or custom schemes is often unexpected.

Code Snippet:

# nosec B310 — the URL is an AWS-issued presigned S3 URL.
    with urllib.request.urlopen(request, timeout=300) as response:  # noqa: S310
        if response.status not in (200, 204):

Finding 3: B310

  • Severity: HIGH
  • Scanner: bandit
  • Rule ID: B310
  • Location: 05-blueprints/meridian-risk/scripts/discover_and_invoke_via_oauth.py:121-123

Description:
Audit url open for permitted schemes. Allowing use of file:/ or custom schemes is often unexpected.

Code Snippet:

try:
        with urllib.request.urlopen(request, timeout=120) as resp:  # noqa: S310
            return resp.status, resp.read().decode()

Finding 4: B310

  • Severity: HIGH
  • Scanner: bandit
  • Rule ID: B310
  • Location: 05-blueprints/meridian-risk/scripts/discover_via_oauth_registry.py:73-75

Description:
Audit url open for permitted schemes. Allowing use of file:/ or custom schemes is often unexpected.

Code Snippet:

try:
        with urllib.request.urlopen(request, timeout=60) as resp:  # noqa: S310
            return resp.status, json.loads(resp.read().decode())

Finding 5: SECRET-SECRET-KEYWORD

  • Severity: HIGH
  • Scanner: detect-secrets
  • Rule ID: SECRET-SECRET-KEYWORD
  • Location: 05-blueprints/meridian-risk/backend/agent/lib/inference.py:192

Description:
Secret of type 'Secret Keyword' detected in file '05-blueprints/meridian-risk/backend/agent/lib/inference.py' at line 192

Code Snippet:

Secret of type Secret Keyword detected

Finding 6: SECRET-SECRET-KEYWORD

  • Severity: HIGH
  • Scanner: detect-secrets
  • Rule ID: SECRET-SECRET-KEYWORD
  • Location: 05-blueprints/meridian-risk/scripts/manage_oauth_provider.py:50

Description:
Secret of type 'Secret Keyword' detected in file '05-blueprints/meridian-risk/scripts/manage_oauth_provider.py' at line 50

Code Snippet:

Secret of type Secret Keyword detected

Finding 7: CKV_AWS_158

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV_AWS_158
  • Location: 05-blueprints/meridian-risk/infra/console_api.tf:57-60

Description:
Ensure that CloudWatch Log Group is encrypted by KMS

Code Snippet:

resource "aws_cloudwatch_log_group" "console_api" {
  name              = "/aws/lambda/${var.stack_name}-console-api"
  retention_in_days = var.log_retention_days
}

Finding 8: CKV_AWS_338

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV_AWS_338
  • Location: 05-blueprints/meridian-risk/infra/console_api.tf:57-60

Description:
Ensure CloudWatch log groups retains logs for at least 1 year

Code Snippet:

resource "aws_cloudwatch_log_group" "console_api" {
  name              = "/aws/lambda/${var.stack_name}-console-api"
  retention_in_days = var.log_retention_days
}

Finding 9: CKV_AWS_173

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV_AWS_173
  • Location: 05-blueprints/meridian-risk/infra/console_api.tf:149-200

Description:
Check encryption settings for Lambda environmental variable

Code Snippet:

resource "aws_lambda_function" "console_api" {
  function_name = "${var.stack_name}-console-api"
  role          = aws_iam_role.console_api.arn
  package_type  = "Image"
  image_uri     = module.console_api_image.image_uri
  architectures = ["arm64"]

  # Must exceed the longest assessment; the Runtime call itself is capped at
  # 600s by the boto3 read timeout in main.py.
  timeout     = 300
  memory_size = 1024

  environment {
    variables = {
      RUNTIME_ARN         = aws_bedrockagentcore_agent_runtime.kyc.agent_runtime_arn
      REGISTRY_ID         = aws_bedrockagentcore_registry.fsi.registry_id
      GATEWAY_ID          = aws_bedrockagentcore_gateway.kyc.gateway_id
      GATEWAY_URL         = aws_bedrockagentcore_gateway.kyc.gateway_url
      MEMORY_ID           = aws_bedrockagentcore_memory.kyc.id
      KYC_TOOLS_LAMBDA    = aws_lambda_function.kyc_tools.function_name
      USER_POOL_ID        = aws_cognito_user_pool.console.id
      USER_POOL_CLIENT_ID = aws_cognito_user_pool_client.console.id
      CONSOLE_ORIGIN      = "https://${aws_amplify_branch.console.branch_name}.${aws_amplify_app.console.id}.amplifyapp.com"

      # Model-plane facts, mirrored from the runtime's own environment so
      # /api/config can report which route and guardrail the deployment is
      # configured for. The console API does not invoke models itself; these
      # are descriptive only, which is why the same values are set in two
      # places rather than read back from the runtime at request time.
      INFERENCE_ROUTE   = var.inference_route
      GUARDRAIL_ID      = aws_bedrock_guardrail.kyc.guardrail_id
      GUARDRAIL_VERSION = aws_bedrock_guardrail_version.kyc.version
      POLICY_ENGINE_ID  = aws_bedrockagentcore_policy_engine.kyc.policy_engine_id
      POLICY_MODE       = var.policy_engine_mode

      # AgentCore Harness — the managed agent loop, when deployed. one(...)
      # yields null (→ empty env var) when enable_harness = false.
      HARNESS_ID = one(aws_bedrockagentcore_harness.kyc[*].harness_id)
    }
  }

  # No replace_triggered_by: the image is content-addressed, so image_uri
  # changes on a code edit and Terraform issues an in-place
  # update-function-code. Replacing the function instead would tear down the
  # Function URL (whose hostname is regenerated) and its resource policy — the
  # exact churn that broke the deployed frontend during the refactor.
  depends_on = [
    aws_iam_role_policy.console_api,
    aws_cloudwatch_log_group.console_api,
    module.console_api_image,
  ]
}

Finding 10: CKV_AWS_116

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV_AWS_116
  • Location: 05-blueprints/meridian-risk/infra/console_api.tf:149-200

Description:
Ensure that AWS Lambda function is configured for a Dead Letter Queue(DLQ)

Code Snippet:

resource "aws_lambda_function" "console_api" {
  function_name = "${var.stack_name}-console-api"
  role          = aws_iam_role.console_api.arn
  package_type  = "Image"
  image_uri     = module.console_api_image.image_uri
  architectures = ["arm64"]

  # Must exceed the longest assessment; the Runtime call itself is capped at
  # 600s by the boto3 read timeout in main.py.
  timeout     = 300
  memory_size = 1024

  environment {
    variables = {
      RUNTIME_ARN         = aws_bedrockagentcore_agent_runtime.kyc.agent_runtime_arn
      REGISTRY_ID         = aws_bedrockagentcore_registry.fsi.registry_id
      GATEWAY_ID          = aws_bedrockagentcore_gateway.kyc.gateway_id
      GATEWAY_URL         = aws_bedrockagentcore_gateway.kyc.gateway_url
      MEMORY_ID           = aws_bedrockagentcore_memory.kyc.id
      KYC_TOOLS_LAMBDA    = aws_lambda_function.kyc_tools.function_name
      USER_POOL_ID        = aws_cognito_user_pool.console.id
      USER_POOL_CLIENT_ID = aws_cognito_user_pool_client.console.id
      CONSOLE_ORIGIN      = "https://${aws_amplify_branch.console.branch_name}.${aws_amplify_app.console.id}.amplifyapp.com"

      # Model-plane facts, mirrored from the runtime's own environment so
      # /api/config can report which route and guardrail the deployment is
      # configured for. The console API does not invoke models itself; these
      # are descriptive only, which is why the same values are set in two
      # places rather than read back from the runtime at request time.
      INFERENCE_ROUTE   = var.inference_route
      GUARDRAIL_ID      = aws_bedrock_guardrail.kyc.guardrail_id
      GUARDRAIL_VERSION = aws_bedrock_guardrail_version.kyc.version
      POLICY_ENGINE_ID  = aws_bedrockagentcore_policy_engine.kyc.policy_engine_id
      POLICY_MODE       = var.policy_engine_mode

      # AgentCore Harness — the managed agent loop, when deployed. one(...)
      # yields null (→ empty env var) when enable_harness = false.
      HARNESS_ID = one(aws_bedrockagentcore_harness.kyc[*].harness_id)
    }
  }

  # No replace_triggered_by: the image is content-addressed, so image_uri
  # changes on a code edit and Terraform issues an in-place
  # update-function-code. Replacing the function instead would tear down the
  # Function URL (whose hostname is regenerated) and its resource policy — the
  # exact churn that broke the deployed frontend during the refactor.
  depends_on = [
    aws_iam_role_policy.console_api,
    aws_cloudwatch_log_group.console_api,
    module.console_api_image,
  ]
}

Finding 11: CKV_AWS_272

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV_AWS_272
  • Location: 05-blueprints/meridian-risk/infra/console_api.tf:149-200

Description:
Ensure AWS Lambda function is configured to validate code-signing

Code Snippet:

resource "aws_lambda_function" "console_api" {
  function_name = "${var.stack_name}-console-api"
  role          = aws_iam_role.console_api.arn
  package_type  = "Image"
  image_uri     = module.console_api_image.image_uri
  architectures = ["arm64"]

  # Must exceed the longest assessment; the Runtime call itself is capped at
  # 600s by the boto3 read timeout in main.py.
  timeout     = 300
  memory_size = 1024

  environment {
    variables = {
      RUNTIME_ARN         = aws_bedrockagentcore_agent_runtime.kyc.agent_runtime_arn
      REGISTRY_ID         = aws_bedrockagentcore_registry.fsi.registry_id
      GATEWAY_ID          = aws_bedrockagentcore_gateway.kyc.gateway_id
      GATEWAY_URL         = aws_bedrockagentcore_gateway.kyc.gateway_url
      MEMORY_ID           = aws_bedrockagentcore_memory.kyc.id
      KYC_TOOLS_LAMBDA    = aws_lambda_function.kyc_tools.function_name
      USER_POOL_ID        = aws_cognito_user_pool.console.id
      USER_POOL_CLIENT_ID = aws_cognito_user_pool_client.console.id
      CONSOLE_ORIGIN      = "https://${aws_amplify_branch.console.branch_name}.${aws_amplify_app.console.id}.amplifyapp.com"

      # Model-plane facts, mirrored from the runtime's own environment so
      # /api/config can report which route and guardrail the deployment is
      # configured for. The console API does not invoke models itself; these
      # are descriptive only, which is why the same values are set in two
      # places rather than read back from the runtime at request time.
      INFERENCE_ROUTE   = var.inference_route
      GUARDRAIL_ID      = aws_bedrock_guardrail.kyc.guardrail_id
      GUARDRAIL_VERSION = aws_bedrock_guardrail_version.kyc.version
      POLICY_ENGINE_ID  = aws_bedrockagentcore_policy_engine.kyc.policy_engine_id
      POLICY_MODE       = var.policy_engine_mode

      # AgentCore Harness — the managed agent loop, when deployed. one(...)
      # yields null (→ empty env var) when enable_harness = false.
      HARNESS_ID = one(aws_bedrockagentcore_harness.kyc[*].harness_id)
    }
  }

  # No replace_triggered_by: the image is content-addressed, so image_uri
  # changes on a code edit and Terraform issues an in-place
  # update-function-code. Replacing the function instead would tear down the
  # Function URL (whose hostname is regenerated) and its resource policy — the
  # exact churn that broke the deployed frontend during the refactor.
  depends_on = [
    aws_iam_role_policy.console_api,
    aws_cloudwatch_log_group.console_api,
    module.console_api_image,
  ]
}

Finding 12: CKV_AWS_115

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV_AWS_115
  • Location: 05-blueprints/meridian-risk/infra/console_api.tf:149-200

Description:
Ensure that AWS Lambda function is configured for function-level concurrent execution limit

Code Snippet:

resource "aws_lambda_function" "console_api" {
  function_name = "${var.stack_name}-console-api"
  role          = aws_iam_role.console_api.arn
  package_type  = "Image"
  image_uri     = module.console_api_image.image_uri
  architectures = ["arm64"]

  # Must exceed the longest assessment; the Runtime call itself is capped at
  # 600s by the boto3 read timeout in main.py.
  timeout     = 300
  memory_size = 1024

  environment {
    variables = {
      RUNTIME_ARN         = aws_bedrockagentcore_agent_runtime.kyc.agent_runtime_arn
      REGISTRY_ID         = aws_bedrockagentcore_registry.fsi.registry_id
      GATEWAY_ID          = aws_bedrockagentcore_gateway.kyc.gateway_id
      GATEWAY_URL         = aws_bedrockagentcore_gateway.kyc.gateway_url
      MEMORY_ID           = aws_bedrockagentcore_memory.kyc.id
      KYC_TOOLS_LAMBDA    = aws_lambda_function.kyc_tools.function_name
      USER_POOL_ID        = aws_cognito_user_pool.console.id
      USER_POOL_CLIENT_ID = aws_cognito_user_pool_client.console.id
      CONSOLE_ORIGIN      = "https://${aws_amplify_branch.console.branch_name}.${aws_amplify_app.console.id}.amplifyapp.com"

      # Model-plane facts, mirrored from the runtime's own environment so
      # /api/config can report which route and guardrail the deployment is
      # configured for. The console API does not invoke models itself; these
      # are descriptive only, which is why the same values are set in two
      # places rather than read back from the runtime at request time.
      INFERENCE_ROUTE   = var.inference_route
      GUARDRAIL_ID      = aws_bedrock_guardrail.kyc.guardrail_id
      GUARDRAIL_VERSION = aws_bedrock_guardrail_version.kyc.version
      POLICY_ENGINE_ID  = aws_bedrockagentcore_policy_engine.kyc.policy_engine_id
      POLICY_MODE       = var.policy_engine_mode

      # AgentCore Harness — the managed agent loop, when deployed. one(...)
      # yields null (→ empty env var) when enable_harness = false.
      HARNESS_ID = one(aws_bedrockagentcore_harness.kyc[*].harness_id)
    }
  }

  # No replace_triggered_by: the image is content-addressed, so image_uri
  # changes on a code edit and Terraform issues an in-place
  # update-function-code. Replacing the function instead would tear down the
  # Function URL (whose hostname is regenerated) and its resource policy — the
  # exact churn that broke the deployed frontend during the refactor.
  depends_on = [
    aws_iam_role_policy.console_api,
    aws_cloudwatch_log_group.console_api,
    module.console_api_image,
  ]
}

Finding 13: CKV_AWS_117

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV_AWS_117
  • Location: 05-blueprints/meridian-risk/infra/console_api.tf:149-200

Description:
Ensure that AWS Lambda function is configured inside a VPC

Code Snippet:

resource "aws_lambda_function" "console_api" {
  function_name = "${var.stack_name}-console-api"
  role          = aws_iam_role.console_api.arn
  package_type  = "Image"
  image_uri     = module.console_api_image.image_uri
  architectures = ["arm64"]

  # Must exceed the longest assessment; the Runtime call itself is capped at
  # 600s by the boto3 read timeout in main.py.
  timeout     = 300
  memory_size = 1024

  environment {
    variables = {
      RUNTIME_ARN         = aws_bedrockagentcore_agent_runtime.kyc.agent_runtime_arn
      REGISTRY_ID         = aws_bedrockagentcore_registry.fsi.registry_id
      GATEWAY_ID          = aws_bedrockagentcore_gateway.kyc.gateway_id
      GATEWAY_URL         = aws_bedrockagentcore_gateway.kyc.gateway_url
      MEMORY_ID           = aws_bedrockagentcore_memory.kyc.id
      KYC_TOOLS_LAMBDA    = aws_lambda_function.kyc_tools.function_name
      USER_POOL_ID        = aws_cognito_user_pool.console.id
      USER_POOL_CLIENT_ID = aws_cognito_user_pool_client.console.id
      CONSOLE_ORIGIN      = "https://${aws_amplify_branch.console.branch_name}.${aws_amplify_app.console.id}.amplifyapp.com"

      # Model-plane facts, mirrored from the runtime's own environment so
      # /api/config can report which route and guardrail the deployment is
      # configured for. The console API does not invoke models itself; these
      # are descriptive only, which is why the same values are set in two
      # places rather than read back from the runtime at request time.
      INFERENCE_ROUTE   = var.inference_route
      GUARDRAIL_ID      = aws_bedrock_guardrail.kyc.guardrail_id
      GUARDRAIL_VERSION = aws_bedrock_guardrail_version.kyc.version
      POLICY_ENGINE_ID  = aws_bedrockagentcore_policy_engine.kyc.policy_engine_id
      POLICY_MODE       = var.policy_engine_mode

      # AgentCore Harness — the managed agent loop, when deployed. one(...)
      # yields null (→ empty env var) when enable_harness = false.
      HARNESS_ID = one(aws_bedrockagentcore_harness.kyc[*].harness_id)
    }
  }

  # No replace_triggered_by: the image is content-addressed, so image_uri
  # changes on a code edit and Terraform issues an in-place
  # update-function-code. Replacing the function instead would tear down the
  # Function URL (whose hostname is regenerated) and its resource policy — the
  # exact churn that broke the deployed frontend during the refactor.
  depends_on = [
    aws_iam_role_policy.console_api,
    aws_cloudwatch_log_group.console_api,
    module.console_api_image,
  ]
}

Finding 14: CKV_AWS_50

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV_AWS_50
  • Location: 05-blueprints/meridian-risk/infra/console_api.tf:149-200

Description:
X-Ray tracing is enabled for Lambda

Code Snippet:

resource "aws_lambda_function" "console_api" {
  function_name = "${var.stack_name}-console-api"
  role          = aws_iam_role.console_api.arn
  package_type  = "Image"
  image_uri     = module.console_api_image.image_uri
  architectures = ["arm64"]

  # Must exceed the longest assessment; the Runtime call itself is capped at
  # 600s by the boto3 read timeout in main.py.
  timeout     = 300
  memory_size = 1024

  environment {
    variables = {
      RUNTIME_ARN         = aws_bedrockagentcore_agent_runtime.kyc.agent_runtime_arn
      REGISTRY_ID         = aws_bedrockagentcore_registry.fsi.registry_id
      GATEWAY_ID          = aws_bedrockagentcore_gateway.kyc.gateway_id
      GATEWAY_URL         = aws_bedrockagentcore_gateway.kyc.gateway_url
      MEMORY_ID           = aws_bedrockagentcore_memory.kyc.id
      KYC_TOOLS_LAMBDA    = aws_lambda_function.kyc_tools.function_name
      USER_POOL_ID        = aws_cognito_user_pool.console.id
      USER_POOL_CLIENT_ID = aws_cognito_user_pool_client.console.id
      CONSOLE_ORIGIN      = "https://${aws_amplify_branch.console.branch_name}.${aws_amplify_app.console.id}.amplifyapp.com"

      # Model-plane facts, mirrored from the runtime's own environment so
      # /api/config can report which route and guardrail the deployment is
      # configured for. The console API does not invoke models itself; these
      # are descriptive only, which is why the same values are set in two
      # places rather than read back from the runtime at request time.
      INFERENCE_ROUTE   = var.inference_route
      GUARDRAIL_ID      = aws_bedrock_guardrail.kyc.guardrail_id
      GUARDRAIL_VERSION = aws_bedrock_guardrail_version.kyc.version
      POLICY_ENGINE_ID  = aws_bedrockagentcore_policy_engine.kyc.policy_engine_id
      POLICY_MODE       = var.policy_engine_mode

      # AgentCore Harness — the managed agent loop, when deployed. one(...)
      # yields null (→ empty env var) when enable_harness = false.
      HARNESS_ID = one(aws_bedrockagentcore_harness.kyc[*].harness_id)
    }
  }

  # No replace_triggered_by: the image is content-addressed, so image_uri
  # changes on a code edit and Terraform issues an in-place
  # update-function-code. Replacing the function instead would tear down the
  # Function URL (whose hostname is regenerated) and its resource policy — the
  # exact churn that broke the deployed frontend during the refactor.
  depends_on = [
    aws_iam_role_policy.console_api,
    aws_cloudwatch_log_group.console_api,
    module.console_api_image,
  ]
}

Finding 15: CKV_AWS_158

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV_AWS_158
  • Location: 05-blueprints/meridian-risk/infra/gateway.tf:16-19

Description:
Ensure that CloudWatch Log Group is encrypted by KMS

Code Snippet:

resource "aws_cloudwatch_log_group" "kyc_tools" {
  name              = "/aws/lambda/${var.stack_name}-kyc-tools"
  retention_in_days = var.log_retention_days
}

Finding 16: CKV_AWS_338

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV_AWS_338
  • Location: 05-blueprints/meridian-risk/infra/gateway.tf:16-19

Description:
Ensure CloudWatch log groups retains logs for at least 1 year

Code Snippet:

resource "aws_cloudwatch_log_group" "kyc_tools" {
  name              = "/aws/lambda/${var.stack_name}-kyc-tools"
  retention_in_days = var.log_retention_days
}

Finding 17: CKV_AWS_116

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV_AWS_116
  • Location: 05-blueprints/meridian-risk/infra/gateway.tf:75-91

Description:
Ensure that AWS Lambda function is configured for a Dead Letter Queue(DLQ)

Code Snippet:

resource "aws_lambda_function" "kyc_tools" {
  function_name = "${var.stack_name}-kyc-tools"
  role          = aws_iam_role.kyc_tools_lambda.arn
  handler       = "kyc_tools_lambda.handler"
  runtime       = "python3.13"
  architectures = ["arm64"]
  timeout       = 30
  memory_size   = 256

  filename         = data.archive_file.kyc_tools.output_path
  source_code_hash = data.archive_file.kyc_tools.output_base64sha256

  depends_on = [
    aws_cloudwatch_log_group.kyc_tools,
    aws_iam_role_policy.kyc_tools_lambda,
  ]
}

Finding 18: CKV_AWS_272

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV_AWS_272
  • Location: 05-blueprints/meridian-risk/infra/gateway.tf:75-91

Description:
Ensure AWS Lambda function is configured to validate code-signing

Code Snippet:

resource "aws_lambda_function" "kyc_tools" {
  function_name = "${var.stack_name}-kyc-tools"
  role          = aws_iam_role.kyc_tools_lambda.arn
  handler       = "kyc_tools_lambda.handler"
  runtime       = "python3.13"
  architectures = ["arm64"]
  timeout       = 30
  memory_size   = 256

  filename         = data.archive_file.kyc_tools.output_path
  source_code_hash = data.archive_file.kyc_tools.output_base64sha256

  depends_on = [
    aws_cloudwatch_log_group.kyc_tools,
    aws_iam_role_policy.kyc_tools_lambda,
  ]
}

Finding 19: CKV_AWS_115

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV_AWS_115
  • Location: 05-blueprints/meridian-risk/infra/gateway.tf:75-91

Description:
Ensure that AWS Lambda function is configured for function-level concurrent execution limit

Code Snippet:

resource "aws_lambda_function" "kyc_tools" {
  function_name = "${var.stack_name}-kyc-tools"
  role          = aws_iam_role.kyc_tools_lambda.arn
  handler       = "kyc_tools_lambda.handler"
  runtime       = "python3.13"
  architectures = ["arm64"]
  timeout       = 30
  memory_size   = 256

  filename         = data.archive_file.kyc_tools.output_path
  source_code_hash = data.archive_file.kyc_tools.output_base64sha256

  depends_on = [
    aws_cloudwatch_log_group.kyc_tools,
    aws_iam_role_policy.kyc_tools_lambda,
  ]
}

Finding 20: CKV_AWS_117

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV_AWS_117
  • Location: 05-blueprints/meridian-risk/infra/gateway.tf:75-91

Description:
Ensure that AWS Lambda function is configured inside a VPC

Code Snippet:

resource "aws_lambda_function" "kyc_tools" {
  function_name = "${var.stack_name}-kyc-tools"
  role          = aws_iam_role.kyc_tools_lambda.arn
  handler       = "kyc_tools_lambda.handler"
  runtime       = "python3.13"
  architectures = ["arm64"]
  timeout       = 30
  memory_size   = 256

  filename         = data.archive_file.kyc_tools.output_path
  source_code_hash = data.archive_file.kyc_tools.output_base64sha256

  depends_on = [
    aws_cloudwatch_log_group.kyc_tools,
    aws_iam_role_policy.kyc_tools_lambda,
  ]
}

Finding 21: CKV_AWS_50

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV_AWS_50
  • Location: 05-blueprints/meridian-risk/infra/gateway.tf:75-91

Description:
X-Ray tracing is enabled for Lambda

Code Snippet:

resource "aws_lambda_function" "kyc_tools" {
  function_name = "${var.stack_name}-kyc-tools"
  role          = aws_iam_role.kyc_tools_lambda.arn
  handler       = "kyc_tools_lambda.handler"
  runtime       = "python3.13"
  architectures = ["arm64"]
  timeout       = 30
  memory_size   = 256

  filename         = data.archive_file.kyc_tools.output_path
  source_code_hash = data.archive_file.kyc_tools.output_base64sha256

  depends_on = [
    aws_cloudwatch_log_group.kyc_tools,
    aws_iam_role_policy.kyc_tools_lambda,
  ]
}

Finding 22: CKV_AWS_107

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV_AWS_107
  • Location: 05-blueprints/meridian-risk/infra/harness.tf:123-260

Description:
Ensure IAM policies does not allow credentials exposure

Code Snippet:

data "aws_iam_policy_document" "harness" {
  count = var.enable_harness ? 1 : 0

  # Model invocation — the harness calls Bedrock for the agent loop.
  statement {
    sid    = "BedrockModelInvocation"
    effect = "Allow"
    actions = [
      "bedrock:InvokeModel",
      "bedrock:InvokeModelWithResponseStream",
    ]
    resources = [
      "arn:aws:bedrock:*::foundation-model/*",
      "arn:aws:bedrock:${local.region}:${local.account_id}:inference-profile/*",
    ]
  }

  # The managed harness image is pulled from ECR Public on the public network.
  statement {
    sid       = "EcrPublicTokenAccess"
    effect    = "Allow"
    actions   = ["ecr-public:GetAuthorizationToken"]
    resources = ["*"]
  }
  statement {
    sid       = "StsForEcrPublicPull"
    effect    = "Allow"
    actions   = ["sts:GetServiceBearerToken"]
    resources = ["*"]
  }

  # Observability — traces to X-Ray, logs and metrics to CloudWatch. Same shape
  # as the Runtime role so both agents land in the unified trace view.
  statement {
    sid    = "Observability"
    effect = "Allow"
    actions = [
      "xray:PutTraceSegments",
      "xray:PutTelemetryRecords",
      "xray:GetSamplingRules",
      "xray:GetSamplingTargets",
    ]
    resources = ["*"]
  }
  statement {
    sid    = "CloudWatchLogs"
    effect = "Allow"
    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents",
      "logs:DescribeLogStreams",
    ]
    resources = ["arn:aws:logs:${local.region}:${local.account_id}:log-group:/aws/bedrock-agentcore/runtimes/*"]
  }
  statement {
    sid       = "CloudWatchLogsDescribeGroups"
    effect    = "Allow"
    actions   = ["logs:DescribeLogGroups"]
    resources = ["arn:aws:logs:${local.region}:${local.account_id}:log-group:*"]
  }
  statement {
    sid       = "CloudWatchLogsResourcePolicy"
    effect    = "Allow"
    actions   = ["logs:PutResourcePolicy"]
    resources = ["*"]
  }
  statement {
    sid       = "CloudWatchMetrics"
    effect    = "Allow"
    actions   = ["cloudwatch:PutMetricData"]
    resources = ["*"]
    condition {
      test     = "StringEquals"
      variable = "cloudwatch:namespace"
      values   = ["bedrock-agentcore"]
    }
  }

  # Workload identity for the managed loop.
  statement {
    sid    = "WorkloadIdentity"
    effect = "Allow"
    actions = [
      "bedrock-agentcore:GetWorkloadAccessToken",
      "bedrock-agentcore:GetWorkloadAccessTokenForJWT",
    ]
    resources = [
      "arn:aws:bedrock-agentcore:${local.region}:${local.account_id}:workload-identity-directory/default",
      "arn:aws:bedrock-agentcore:${local.region}:${local.account_id}:workload-identity-directory/default/workload-identity/*",
    ]
  }

  # This harness's wiring: reach the shared Gateway (its KYC tools) under the
  # same ARN the Runtime role grants.
  statement {
    sid       = "InvokeGateway"
    effect    = "Allow"
    actions   = ["bedrock-agentcore:InvokeGateway"]
    resources = [aws_bedrockagentcore_gateway.kyc.gateway_arn]
  }

  # Fetch the S3 skill bundle at session start. The docs require both GetObject
  # (read the SKILL.md) and ListBucket (enumerate the skill prefix).
  statement {
    sid       = "SkillBucketRead"
    effect    = "Allow"
    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.harness_skills[0].arn}/*"]
  }
  statement {
    sid       = "SkillBucketList"
    effect    = "Allow"
    actions   = ["s3:ListBucket"]
    resources = [aws_s3_bucket.harness_skills[0].arn]
  }
  # The harness provisions its OWN managed short/long-term memory (the
  # managedMemoryConfiguration the service creates by default) — a resource
  # distinct from the Runtime's shared KYC memory, and named after the harness
  # (`<harness_name>-<service-assigned-id>`). Its id is not known at plan time,
  # so scope to the harness-name prefix. Without this the loop fails mid-run on
  # `ListEvents` against that managed memory.
  statement {
    sid    = "ManagedMemoryAccess"
    effect = "Allow"
    actions = [
      "bedrock-agentcore:CreateEvent",
      "bedrock-agentcore:GetEvent",
      "bedrock-agentcore:ListEvents",
      "bedrock-agentcore:ListSessions",
      "bedrock-agentcore:RetrieveMemoryRecords",
      "bedrock-agentcore:ListMemoryRecords",
    ]
    resources = [
      "arn:aws:bedrock-agentcore:${local.region}:${local.account_id}:memory/${local.harness_name}-*",
    ]
  }
}

Finding 23: CKV_AWS_136

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV_AWS_136
  • Location: 05-blueprints/meridian-risk/infra/modules/ecr-image/main.tf:30-43

Description:
Ensure that ECR repositories are encrypted using KMS

Code Snippet:

resource "aws_ecr_repository" "this" {
  name                 = var.repository_name
  image_tag_mutability = "MUTABLE"
  # Demo stack: allow `terraform destroy` to remove the repo with images in it.
  force_delete = true

  image_scanning_configuration {
    scan_on_push = true
  }

  encryption_configuration {
    encryption_type = "AES256"
  }
}

Finding 24: CKV_AWS_51

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV_AWS_51
  • Location: 05-blueprints/meridian-risk/infra/modules/ecr-image/main.tf:30-43

Description:
Ensure ECR Image Tags are immutable

Code Snippet:

resource "aws_ecr_repository" "this" {
  name                 = var.repository_name
  image_tag_mutability = "MUTABLE"
  # Demo stack: allow `terraform destroy` to remove the repo with images in it.
  force_delete = true

  image_scanning_configuration {
    scan_on_push = true
  }

  encryption_configuration {
    encryption_type = "AES256"
  }
}

Finding 25: CKV_AWS_136

  • Severity: HIGH
  • Scanner: checkov
  • Rule ID: CKV_AWS_136
  • Location: 05-blueprints/meridian-risk/infra/modules/ecr-image/main.tf:30-43

Description:
Ensure that ECR repositories are encrypted using KMS

Code Snippet:

resource "aws_ecr_repository" "this" {
  name                 = var.repository_name
  image_tag_mutability = "MUTABLE"
  # Demo stack: allow `terraform destroy` to remove the repo with images in it.
  force_delete = true

  image_scanning_configuration {


<!-- ASH-SECURITY-SCAN-COMMENT -->

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.

2 participants