diff --git a/.ash/.ash.yaml b/.ash/.ash.yaml
deleted file mode 100644
index 791bba4..0000000
--- a/.ash/.ash.yaml
+++ /dev/null
@@ -1,235 +0,0 @@
-# yaml-language-server: $schema=https://raw.githubusercontent.com/awslabs/automated-security-helper/refs/heads/main/automated_security_helper/schemas/AshConfig.json
-project_name: sample-aiml-security-assessment
-global_settings:
- severity_threshold: MEDIUM
- ignore_paths:
- # SAM build cache — transitively scanned pydantic/bs4/botocore deps
- # produce thousands of non-actionable Bandit findings in third-party code.
- # CI scans only changed files, so it never touches .aws-sam/ either.
- - path: "aiml-security-assessment/.aws-sam/**"
- reason: "SAM build cache; contains third-party deps, not project source"
- - path: "**/.aws-sam/**"
- reason: "SAM build cache at any depth (deps/, build/, etc.)"
- # Pre-existing static HTML sample reports; the embedded base64 SVG logos
- # are flagged as high-entropy strings by detect-secrets.
- - path: "sample-reports/**"
- reason: "Static HTML sample outputs with embedded base64 SVG icons (upstream)"
- # ASH's own output directory.
- - path: ".ash/ash_output/**"
- reason: "ASH output directory; scanning it creates recursion"
- suppressions:
- # The 4 Checkov rules below flag every assessment Lambda in the upstream
- # repo for: no VPC (CKV_AWS_117), no DLQ (CKV_AWS_116), no per-function
- # concurrency cap (CKV_AWS_115), and no KMS-encrypted env vars
- # (CKV_AWS_173). This matches the architecture choices made for all 6
- # pre-existing Lambdas (BedrockSecurity, SagemakerSecurity,
- # AgentCoreSecurity, GenerateConsolidatedReport, IAMPermissionCaching,
- # CleanupBucket) and applied consistently to the new
- # FinServSecurityAssessmentFunction. The assessment framework runs on an
- # internal schedule with short-lived invocations and reads config from a
- # non-sensitive bucket-name env var; hardening these four items for every
- # Lambda in the stack is a framework-wide change that should be proposed
- # in a separate PR.
- - rule_id: "CKV_AWS_117"
- path: "aiml-security-assessment/template.yaml"
- reason: "Upstream convention: assessment Lambdas are non-VPC (applies to all 7 Lambdas)"
- - rule_id: "CKV_AWS_117"
- path: "aiml-security-assessment/template-multi-account.yaml"
- reason: "Upstream convention: assessment Lambdas are non-VPC (applies to all 7 Lambdas)"
- - rule_id: "CKV_AWS_116"
- path: "aiml-security-assessment/template.yaml"
- reason: "Upstream convention: assessment Lambdas have no DLQ (applies to all 7 Lambdas)"
- - rule_id: "CKV_AWS_116"
- path: "aiml-security-assessment/template-multi-account.yaml"
- reason: "Upstream convention: assessment Lambdas have no DLQ (applies to all 7 Lambdas)"
- - rule_id: "CKV_AWS_115"
- path: "aiml-security-assessment/template.yaml"
- reason: "Upstream convention: no per-function concurrency cap (applies to all 7 Lambdas)"
- - rule_id: "CKV_AWS_115"
- path: "aiml-security-assessment/template-multi-account.yaml"
- reason: "Upstream convention: no per-function concurrency cap (applies to all 7 Lambdas)"
- - rule_id: "CKV_AWS_173"
- path: "aiml-security-assessment/template.yaml"
- reason: "Upstream convention: env var is a non-sensitive bucket-name reference"
- - rule_id: "CKV_AWS_173"
- path: "aiml-security-assessment/template-multi-account.yaml"
- reason: "Upstream convention: env var is a non-sensitive bucket-name reference"
-
- # cdk-nag AwsSolutions-IAM5 flags any IAM statement with Resource: "*".
- # The FinServ IAM statement I added to 1-aiml-security-member-roles.yaml
- # uses the same pattern as every pre-existing Sid in that policy. The
- # wildcard is required for list/describe operations that are not
- # ARN-scopable (e.g., ListBuckets, ListTrails, ListRules, ListPolicies).
- # If upstream tightens IAM across all assessment statements, the FinServ
- # Sid should be tightened in the same PR.
- - rule_id: "AwsSolutions-IAM5"
- path: "deployment/1-aiml-security-member-roles.yaml"
- reason: "Read-only list/describe actions that cannot be ARN-scoped (upstream convention)"
- - rule_id: "AwsSolutions-IAM5"
- path: "deployment/aiml-security-single-account.yaml"
- reason: "Read-only list/describe actions that cannot be ARN-scoped (upstream convention)"
- - rule_id: "CKV_AWS_173"
- path: "aiml-security-assessment/template-multi-account.yaml"
- reason: "Upstream convention: env var is a bucket name reference, not sensitive (same as BR/SM/AC)"
-fail_on_findings: true
-ash_plugin_modules: []
-external_reports_to_include: []
-converters:
- archive:
- enabled: true
- options: {}
- jupyter:
- enabled: true
- options:
- tool_version: '>=7.16.0,<8.0.0'
- install_timeout: 300
-scanners:
- bandit:
- enabled: true
- options:
- severity_threshold: null
- config_file: null
- confidence_level: all
- ignore_nosec: false
- excluded_paths: []
- additional_formats: []
- tool_version: '>=1.7.0,<2.0.0'
- install_timeout: 300
- cdk-nag:
- enabled: true
- options:
- severity_threshold: null
- nag_packs:
- AwsSolutionsChecks: true
- HIPAASecurityChecks: false
- NIST80053R4Checks: false
- NIST80053R5Checks: false
- PCIDSS321Checks: false
- cfn-nag:
- enabled: true
- options:
- severity_threshold: null
- checkov:
- enabled: true
- options:
- severity_threshold: null
- config_file: null
- skip_path: []
- additional_formats:
- - cyclonedx_json
- offline: false
- frameworks:
- - all
- skip_frameworks: []
- tool_version: null
- install_timeout: 300
- detect-secrets:
- enabled: true
- options:
- severity_threshold: null
- baseline_file: null
- scan_settings:
- version: null
- generated_at: null
- plugins_used: []
- filters_used: []
- results: {}
- grype:
- enabled: true
- options:
- severity_threshold: null
- config_file: null
- offline: false
- npm-audit:
- enabled: true
- options:
- severity_threshold: null
- offline: false
- opengrep:
- enabled: true
- options:
- severity_threshold: null
- config: auto
- exclude:
- - '*-converted.py'
- - '*_report_result.txt'
- exclude_rule: []
- severity: []
- metrics: auto
- offline: false
- patterns: []
- version: v1.15.1
- semgrep:
- enabled: true
- options:
- severity_threshold: null
- config: auto
- exclude:
- - '*-converted.py'
- - '*_report_result.txt'
- exclude_rule: []
- severity: []
- metrics: auto
- offline: false
- tool_version: null
- install_timeout: 300
- syft:
- enabled: true
- options:
- severity_threshold: null
- config_file: null
- exclude: []
- additional_outputs:
- - syft-table
-reporters:
- csv:
- enabled: true
- options: {}
- cyclonedx:
- enabled: true
- options: {}
- html:
- enabled: true
- options: {}
- flat-json:
- enabled: true
- options:
- include_scanner_metrics: true
- include_summary_metrics: true
- include_metadata: true
- gitlab-sast:
- enabled: true
- options: {}
- junitxml:
- enabled: true
- options:
- respect_severity_threshold: true
- markdown:
- enabled: true
- options:
- include_summary: true
- include_findings_table: false
- include_detailed_findings: true
- max_detailed_findings: 20
- top_hotspots_limit: 10
- use_collapsible_details: true
- ocsf:
- enabled: true
- options: {}
- sarif:
- enabled: true
- options: {}
- spdx:
- enabled: false
- options: {}
- text:
- enabled: true
- options:
- include_summary: true
- include_findings_table: false
- include_detailed_findings: false
- max_detailed_findings: 20
- top_hotspots_limit: 20
- yaml:
- enabled: false
- options: {}
diff --git a/.ash/.gitignore b/.ash/.gitignore
deleted file mode 100644
index d831134..0000000
--- a/.ash/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# ASH default output directory (and variants)
-ash_output*
diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml
index d0d65f4..b5288fc 100644
--- a/.github/workflows/python-tests.yml
+++ b/.github/workflows/python-tests.yml
@@ -7,7 +7,7 @@ on:
- "aiml-security-assessment/functions/security/**"
- "tests/**"
- "consolidate_html_reports.py"
- - "test_consolidate_finserv.py"
+ - "tests/test_consolidate_finserv.py"
- ".github/workflows/python-tests.yml"
pull_request:
branches: [main]
@@ -15,7 +15,7 @@ on:
- "aiml-security-assessment/functions/security/**"
- "tests/**"
- "consolidate_html_reports.py"
- - "test_consolidate_finserv.py"
+ - "tests/test_consolidate_finserv.py"
- ".github/workflows/python-tests.yml"
jobs:
@@ -81,7 +81,7 @@ jobs:
AWS_ACCESS_KEY_ID: testing
AWS_SECRET_ACCESS_KEY: testing # pragma: allowlist secret
run: |
- python -m pytest test_consolidate_finserv.py -v --tb=short
+ python -m pytest tests/test_consolidate_finserv.py -v --tb=short
cd aiml-security-assessment/functions/security/generate_consolidated_report
python -m pytest test_generate_report.py -v --tb=short
diff --git a/FOLLOWUPS.md b/FOLLOWUPS.md
deleted file mode 100644
index 537ab51..0000000
--- a/FOLLOWUPS.md
+++ /dev/null
@@ -1,196 +0,0 @@
-# Follow-up items (deferred from PR #23 Round 3)
-
-> GitHub Issues are disabled on the fork `mehtadman87/sample-aiml-security-assessment`, so
-> deferred work is tracked here in-repo (task T1b.7). Promote to an upstream issue/PR when ready.
-
-## FU-1 — Evaluate adopting a tool-wide `CRITICAL` severity tier
-
-**Status:** Deferred (intentionally out of scope for Round 3).
-
-**Background.** The FinServ severity methodology
-([`docs/SECURITY_CHECKS_FINSERV_SEVERITY_METHODOLOGY.md`](./SECURITY_CHECKS_FINSERV_SEVERITY_METHODOLOGY.md))
-defines a Likelihood × Impact matrix mapped to the AWS Security Hub ASFF label set, which includes a
-**CRITICAL** band (the Impact=High × Likelihood=High cell). Round 3 **capped that cell at High** and
-did **not** introduce `Critical`, to keep the FinServ checks consistent with the upstream
-Bedrock/SageMaker/AgentCore checks (which have no Critical tier). The drift-guard
-(`tests/test_severity_register.py`) asserts no `Critical` is currently emitted.
-
-**Why it is a separate item.** Adopting `Critical` is a **tool-wide** change. Doing it for FinServ
-alone would make FinServ inconsistent with the other three services. A tool-wide rollout touches:
-- the shared `SeverityEnum` (each package's `schema.py`);
-- all four assessment Lambdas (re-score the I=High × L=High controls);
-- the report template severity filters/colors (`generate_consolidated_report/report_template.py`
- and `consolidate_html_reports.py`);
-- every service's unit tests plus the FinServ drift-guard.
-
-**Decision needed.**
-1. Do we want a `CRITICAL` tier across the whole assessment?
-2. If yes, which controls qualify (e.g., full guardrail bypass enabling a regulatory breach;
- unauthorized high-value autonomous financial action)?
-
-**References:** methodology §2 (label scale) and §6 (the deferred decision); ASFF severity —
-https://docs.aws.amazon.com/securityhub/1.0/APIReference/API_Severity.html
-
----
-
-## FU-2 — Report UI design for FinServ / OWASP (REQ-8)
-
-Broader report UI re-design (top-page FinServ summary box placement, OWASP grouping, Risk
-Distribution treatment) is deferred to a separate PR per the reviewer's suggestion. Round 3 delivers
-FinServ as a functionally first-class service (REQ-1, Wave 2); the UI-design discussion is tracked
-here for a follow-up PR.
-
----
----
----
-
-## FU-3 — Shared-inventory refactor for the FinServ assessment Lambda (REQ-13 C3)
-
-**Status:** Deferred from Round 3.1 (Wave 5.5 task T5h.9). Classified **Should-Fix** (performance /
-scalability optimization), not a correctness blocker. Tracked here for a focused follow-up PR.
-
-### Background
-
-The FinServ Lambda (`finserv_assessments/app.py`) runs all 64 checks **sequentially in a single
-invocation** via `build_finserv_checks()`. There is no cross-check caching, so several checks
-independently re-enumerate the **same** account-wide inventories, and many then issue an N+1
-per-resource call on top. The Round-3.1 pre-release audit (the per-check AWS API inventory)
-identified the following duplicate full-account sweeps within one run:
-
-| Inventory (API) | Times enumerated | Checks performing the enumeration | Per-resource N+1 follow-up |
-| --- | --- | --- | --- |
-| `lambda:ListFunctions` | ~6× | FS-09, FS-52, FS-55, FS-58, FS-67, FS-69 | FS-09 `GetFunctionConcurrency` per function |
-| `bedrock:ListGuardrails` | ~9× | FS-27a, FS-28, FS-36, FS-38, FS-45, FS-47, FS-50, FS-51, FS-59 | `GetGuardrail` per guardrail in each check |
-| `bedrock:ListKnowledgeBases` | ~6× | FS-24, FS-31, FS-33, FS-48, FS-61, FS-65 | FS-31/33/65 `ListDataSources`/`GetDataSource` per KB |
-| `s3:ListBuckets` (full account) | ~3–4× | FS-21, FS-46 (full sweep); FS-33, FS-65 (KB data-source buckets) | `GetBucketVersioning`/`GetBucketTagging`/`GetBucketNotification` per bucket |
-| `wafv2:ListWebACLs` | 4× | FS-01, FS-53, FS-56, FS-68 | `GetWebACL` per ACL in each check |
-
-In small/empty accounts this is harmless (it is why the Wave-5 test account ran in ~3 minutes). In
-**large enterprise accounts** — thousands of Lambda functions, hundreds of S3 buckets, many
-guardrails/KBs/ACLs — the repeated full sweeps plus N+1 calls multiply API volume, drive adaptive
-retries/throttling (`Config(retries=adaptive)`), and inflate wall-clock time. This is the most
-likely driver of a slow run, and previously of a `States.Timeout`.
-
-### Why it was deferred (and why that is safe)
-
-1. **It is a large, regression-prone refactor.** It touches ~20 check functions and the way they
- obtain AWS data, plus every corresponding mocked unit test (the per-check tests patch
- `app.boto3.client`). Bundling that into a correctness-focused PR that "must be 100% sure"
- (the reviewer's bar) trades correctness risk for a performance gain.
-2. **The blast-radius risk it is associated with (audit C2) is already mitigated in Round 3.1.**
- A `Catch [States.ALL] → "FinServ Assessment Incomplete"` was added to the FinServ task in
- `statemachine/assessments.asl.json`, and the FinServ Lambda `Timeout` was raised 600 → 900 s.
- So even if the FinServ Lambda is slow or times out in a very large account, the consolidated
- report is still generated with the other services' findings and a visible incomplete marker —
- the failure no longer sinks the whole assessment.
-3. **It is purely an optimization** (Should-Fix). The disposition logic and severities are unchanged
- by this work; nothing about the *correctness* of any finding depends on it.
-
-### Who is affected
-
-Customers running the FinServ assessment (`EnableFinServAssessment=true`) against accounts with large
-resource estates — primarily large enterprises. Symptoms: long FinServ Lambda duration, CloudWatch
-throttling/retry noise, and (pre-mitigation) the risk of a 900 s timeout. Small accounts are
-unaffected.
-
-### Proposed design
-
-Collect each shared inventory **once** at the start of `lambda_handler`, into a read-only context
-object, and pass it to the checks that need it — mirroring the existing `permission_cache` pattern
-(which is already injected via `functools.partial` in `build_finserv_checks()`).
-
-1. **Add a `ResourceInventory` collected once per invocation**, e.g.:
-
- ```python
- @dataclass
- class ResourceInventory:
- lambda_functions: list # lambda:ListFunctions (+ concurrency map)
- guardrails: list # bedrock:ListGuardrails + GetGuardrail detail, keyed by id
- knowledge_bases: list # bedrock:ListKnowledgeBases (+ data sources per KB)
- buckets: list # s3:ListBuckets
- web_acls: list # wafv2:ListWebACLs + GetWebACL detail (REGIONAL)
- ```
-
- Each field is populated by one paginated enumeration (reuse `_paginate`). Per-resource detail
- (e.g., `GetGuardrail`, `GetWebACL`) is fetched once and stored alongside, eliminating the N+1
- repetition across checks.
-
-2. **Inject it like `permission_cache`.** In `build_finserv_checks(permission_cache, inventory)`,
- bind the inventory to the relevant checks with `functools.partial`, so every registry entry
- stays uniformly zero-arg and the handler loop is unchanged.
-
-3. **Make collection resilient.** A failure (e.g., `AccessDenied`) collecting one inventory must not
- abort the others or the whole run. Per-inventory collection should be wrapped so a failed
- inventory yields an explicit "unavailable" sentinel; checks that depend on an unavailable
- inventory then emit `COULD_NOT_ASSESS` (consistent with `_is_access_error` handling today) rather
- than a false `Failed`/`Passed`. Preserve the existing per-check `try/except` safety net.
-
-4. **Keep region/pagination semantics identical.** Same default-region clients, same `_paginate`
- token handling; this is a call-site consolidation, not a behavior change.
-
-### Test strategy
-
-- Update the affected per-check unit tests to pass a constructed `ResourceInventory` (or a partial)
- instead of patching `boto3.client` for the enumeration calls. Checks that still make non-inventory
- calls keep their existing mocks.
-- Add tests for the collector itself: one enumeration per inventory; pagination; and the
- per-inventory failure path producing the "unavailable" sentinel (→ `COULD_NOT_ASSESS` downstream).
-- **Behavior-preserving guarantee:** every existing disposition test (Passed/Failed/N/A per check)
- and the severity drift-guard (`tests/test_severity_register.py`) must remain green with **no**
- disposition or severity changes. That equivalence is the acceptance bar.
-- Optionally add a counter/assertion (in a unit harness) proving each inventory API is called at
- most once per handler invocation.
-
-### Acceptance criteria
-
-- Each shared inventory (`ListFunctions`, `ListGuardrails`+`GetGuardrail`, `ListKnowledgeBases`,
- `ListBuckets`, `ListWebACLs`+`GetWebACL`) is enumerated **at most once** per FinServ run.
-- No change to any finding's status or severity (all existing tests + the drift-guard pass
- unchanged).
-- A per-inventory collection failure degrades only the dependent checks (to `COULD_NOT_ASSESS`), not
- the whole run.
-- Workspace `finserv_assessments/` stays byte-identical to the fork copy after sync.
-
-### Risk / considerations
-
-- **Test isolation** if any memoization is module-level: prefer an explicit per-invocation object
- passed as an argument over a module-global cache, to avoid state leaking across unit tests.
-- **Partial-inventory correctness:** ensure a check that needs two inventories handles one being
- unavailable independently.
-- **Memory:** holding full inventories (e.g., all guardrail details) in memory is bounded and far
- smaller than the permission cache already loaded; no concern at 1024 MB.
-
-### Effort estimate
-
-Roughly 1–2 focused days: ~0.5 day for the collector + injection, ~0.5–1 day updating the ~20
-affected checks and their tests, ~0.5 day validation (full suite + a large-account timing check).
-
-### References
-
-- Round-3.1 requirement: **REQ-13 (audit finding C3)** and design section "REQ-13 — Enterprise-scale
- resilience & scope (audit C)" in `.kiro/specs/pr-review-round3-fixes/design.md`.
-- Deferred task: **T5h.9** in `.kiro/specs/pr-review-round3-fixes/tasks.md`.
-- Related mitigation already shipped in Round 3.1: **T5h.8** (ASL `Catch` on the FinServ task +
- Lambda `Timeout` 600 → 900 s) addressing audit finding C2.
-- Existing pattern to mirror: the `permission_cache` injection in `get_permissions_cache()` /
- `build_finserv_checks()` in `finserv_assessments/app.py`.
-
-## FU-4 — Migrate upstream schemas from Pydantic V1 `@validator` to V2 `@field_validator`
-
-**Priority:** Low (tech-debt) — not a blocker.
-
-The upstream `schema.py` files for the Bedrock, SageMaker, AgentCore, consolidated-report,
-and IAM-permission-caching Lambdas still use the deprecated Pydantic V1 `@validator` decorator,
-which emits `PydanticDeprecatedSince20` warnings and will break when Pydantic V3 removes V1-style
-validators. The FinServ `schema.py` already uses the V2 `@field_validator` form.
-
-**Why deferred:** this PR is scoped to `feature/finserv-risk-checks`. The affected files are
-upstream/shared components; migrating them here would exceed the PR's scope and risk merge
-conflicts with upstream. Best handled as a dedicated upstream change.
-
-**Scope:** swap `@validator("X")` → `@field_validator("X")` + `@classmethod`, adjust signatures,
-and re-run each module's tests.
-
-### References
-
-- Identified in the pre-Wave-6 verification pass (`.kiro/specs/pr-review-round3-fixes/tasks.md`).
diff --git a/README.md b/README.md
index 7ff75a7..3508f66 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,9 @@
[](https://opensource.org/licenses/MIT-0) [](https://www.python.org/downloads/) [](https://aws.amazon.com/serverless/sam/) [](https://aws.amazon.com/serverless/)
-**Open-source automated security scanner for Amazon Bedrock, Amazon SageMaker AI, Amazon Bedrock AgentCore, and Financial Services GenAI Risk** — Built on [AWS Well-Architected Framework (Generative AI Lens)](https://docs.aws.amazon.com/wellarchitected/latest/generative-ai-lens/generative-ai-lens.html)
+**Open-source automated security scanner for Amazon Bedrock, Amazon SageMaker AI, Amazon Bedrock AgentCore, and Financial Services GenAI Risk** — Built on the [AWS Well-Architected Framework (Generative AI Lens)](https://docs.aws.amazon.com/wellarchitected/latest/generative-ai-lens/generative-ai-lens.html) and optional [AWS User Guide to Governance, Risk, and Compliance for Responsible AI Adoption within Financial Services Industries](https://d1.awsstatic.com/onedam/marketing-channels/website/aws/en_US/whitepapers/compliance/AWS-User-Guide-Governance-Risk-Compliance-for-Responsible-AI-Adoption-Financial-Services.pdf) guidance.
-Cloud security automation with **[116 security checks](docs/SECURITY_CHECKS.md)** for your generative AI and machine learning workloads. Identify IAM misconfigurations, encryption gaps, network isolation issues, and compliance violations with interactive HTML reports and actionable remediation guidance.
+Cloud security automation with **[116 security checks](docs/SECURITY_CHECKS.md)** for your generative AI and machine learning workloads. Identify IAM misconfigurations, encryption gaps, network isolation issues, and potential governance or compliance gaps with interactive HTML reports and actionable remediation guidance.
---
@@ -38,7 +38,8 @@ The framework generates professional, interactive security assessment reports wi
- **Executive Summary** with severity counts and service breakdown
- **Priority Recommendations** highlighting critical issues requiring immediate attention
- **[116 Security Checks](docs/SECURITY_CHECKS.md)** across Amazon Bedrock, Amazon SageMaker AI, Amazon Bedrock AgentCore, and Financial Services GenAI Risk
-- **Interactive Filtering** by account, service, severity, and status
+- **Multi-Region Support** for core Bedrock, SageMaker, and AgentCore checks, with per-region risk breakdown
+- **Interactive Filtering** by account, region, service, severity, and status
- **Light/Dark Mode Toggle** with persistent user preference
- **Text Search** across all findings with real-time results
- **Direct AWS Documentation Links** for each finding with remediation guidance
@@ -73,14 +74,14 @@ The framework generates professional, interactive security assessment reports wi
This serverless assessment framework automatically evaluates your AI/ML workloads against AWS security best practices. It uses AWS serverless services to gather data from the control plane and generate reports containing the status of various security checks, severity levels, and recommended actions.
-Designed for workloads using [Amazon Bedrock](https://aws.amazon.com/bedrock/), [Amazon Bedrock AgentCore](https://aws.github.io/bedrock-agentcore-starter-toolkit/), or [Amazon SageMaker AI](https://aws.amazon.com/sagemaker/ai/).
+Designed for workloads using [Amazon Bedrock](https://aws.amazon.com/bedrock/), [Amazon Bedrock AgentCore](https://aws.github.io/bedrock-agentcore-starter-toolkit/), [Amazon SageMaker AI](https://aws.amazon.com/sagemaker/ai/), or the optional Financial Services GenAI risk assessment.
### Why Use This Framework?
| Challenge | How This Framework Helps |
|-----------|-------------------------|
| **Manual security audits are time-consuming** | Fully automated scanning with one-click CloudFormation deployment |
-| **Inconsistent security checks across teams** | Standardized 116-check assessment based on AWS Well-Architected best practices and AWS FinServ GenAI Risk guidance |
+| **Inconsistent security checks across teams** | Standardized 116-check assessment based on AWS Well-Architected Generative AI Lens best practices and AWS Responsible AI governance, risk, and compliance guidance for financial services |
| **Difficulty tracking AI/ML security posture** | Interactive HTML dashboards with severity breakdown and per-account visibility |
| **Multi-account complexity** | Consolidated reporting across AWS Organizations with cross-account role assumption |
| **Compliance and audit support** | Exportable reports to supplement your compliance program, with remediation guidance linked to AWS documentation |
@@ -90,7 +91,7 @@ Designed for workloads using [Amazon Bedrock](https://aws.amazon.com/bedrock/),
- **[Amazon Bedrock](docs/SECURITY_CHECKS.md#amazon-bedrock-security-checks-14)** (14 checks) - Guardrails, encryption, Amazon VPC endpoints, AWS IAM permissions, model invocation logging
- **[Amazon SageMaker AI](docs/SECURITY_CHECKS.md#amazon-sagemaker-ai-security-checks-25)** (25 checks) - AWS Security Hub controls (SageMaker.1-5), encryption, network isolation, AWS IAM, MLOps
- **[Amazon Bedrock AgentCore](docs/SECURITY_CHECKS.md#amazon-bedrock-agentcore-security-checks-13)** (13 checks) - Amazon VPC configuration, encryption, observability, resource policies
-- **[Financial Services GenAI Risk](docs/SECURITY_CHECKS.md#financial-services-genai-risk-checks-64-additional-5-upstream-extensions)** (64 checks) - Unbounded consumption, excessive agency, supply chain, training data poisoning, hallucination, prompt injection, PII disclosure, and 8 more FinServ-specific risk categories derived from the [AWS FinServ GenAI Risk Guide](https://d1.awsstatic.com/onedam/marketing-channels/website/public/global-FinServ-ComplianceGuide-GenAIRisks-public.pdf)
+- **[Financial Services GenAI Risk](docs/SECURITY_CHECKS.md#financial-services-genai-risk-checks-64-additional-5-upstream-extensions)** (64 checks) - Unbounded consumption, excessive agency, supply chain, training data poisoning, hallucination, prompt injection, PII disclosure, and 8 more FinServ-specific risk categories derived from the [AWS User Guide to Governance, Risk, and Compliance for Responsible AI Adoption within Financial Services Industries](https://d1.awsstatic.com/onedam/marketing-channels/website/aws/en_US/whitepapers/compliance/AWS-User-Guide-Governance-Risk-Compliance-for-Responsible-AI-Adoption-Financial-Services.pdf). See the [AWS Security Blog announcement](https://aws.amazon.com/blogs/security/introducing-the-updated-aws-user-guide-to-governance-risk-and-compliance-for-responsible-ai-adoption/) for context on the updated guide.
**Deployment Options:**
- **Single-Account**: Assess security in one AWS account
@@ -112,7 +113,7 @@ This tool operates within the [AWS Shared Responsibility Model](https://aws.amaz
**No guarantee of security or compliance.** This framework identifies common misconfigurations based on AWS best practices and the AWS Well-Architected Framework. It does not cover all possible security risks, does not replace formal compliance audits (SOC 2, HIPAA, and similar), and does not guarantee that your workloads are secure. Use the results as one input into your broader security program.
-**52 checks across three services.** The assessment covers Amazon Bedrock, Amazon SageMaker AI, and Amazon Bedrock AgentCore. Other AI/ML services (Amazon Comprehend, Amazon Rekognition, Amazon Textract, and others) are not currently assessed.
+**116 checks across four domains.** The assessment covers Amazon Bedrock, Amazon SageMaker AI, Amazon Bedrock AgentCore, and optional Financial Services GenAI risk checks. Other AI/ML services (Amazon Comprehend, Amazon Rekognition, Amazon Textract, and others) are not currently assessed.
---
@@ -127,136 +128,83 @@ This tool operates within the [AWS Shared Responsibility Model](https://aws.amaz
## Prerequisites
-- Python 3.12+ - [Install Python](https://www.python.org/downloads/)
-- AWS SAM CLI - [Install the AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)
-- Docker (optional) - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) - Only required for local development and testing, not for AWS deployment
+- Python 3.12+ — [Install Python](https://www.python.org/downloads/)
+- AWS SAM CLI — [Install the AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)
+- Docker (optional) — [Install Docker](https://hub.docker.com/search/?type=edition&offering=community) — Only required for local development
+
+---
## Single-Account Deployment
-1. Download the [aiml-security-single-account.yaml](deployment/aiml-security-single-account.yaml) AWS CloudFormation template.
+1. Download the [aiml-security-single-account.yaml](deployment/aiml-security-single-account.yaml) CloudFormation template.
2. **[Deploy to AWS CloudFormation](https://console.aws.amazon.com/cloudformation/home#/stacks/create/template?stackName=aiml-security-single-account)**
-3. Upload the AWS CloudFormation template from step 1.
-4. Provide a stack name and optionally specify your email address to receive notifications.
-5. Leave all other parameters at their default values.
-6. Navigate to the next page, read and acknowledge the notice, and click **Next**.
-7. Review the information and click **Submit**.
-8. Wait for the AWS CloudFormation stack to complete.
-9. Once complete, AWS CodeBuild automatically deploys the assessment stack and runs the assessment.
-10. To view results:
- - Navigate to the AWS CloudFormation console
- - Open the stack you deployed (for example, `aiml-security-single-account` or your custom name)
- - Go to the **Outputs** tab
- - Copy the `AssessmentBucket` value
- - Navigate to that Amazon S3 bucket and open the `{account_id}/security_assessment_*.html` file
-
-### Understanding Stack Names
-
-> **Important**: The deployment creates **TWO** AWS CloudFormation stacks. Only one contains your results!
+3. Upload the template and provide a stack name.
+4. Optionally specify your email address to receive notifications.
+5. **(Optional) Multi-Region**: Set `TargetRegions` to scan multiple regions:
+ - Leave empty to scan only the deployment region (default)
+ - Comma- or space-separated list (for example, `us-east-1,us-west-2,eu-west-1` or `us-east-1 us-west-2 eu-west-1`)
+ - `all` to scan all regions where the services are available
+6. Acknowledge IAM capabilities and click **Submit**.
+7. Once complete, CodeBuild automatically runs the assessment.
+8. View results: go to the stack **Outputs** tab → copy `AssessmentBucket` → open the report under the `/{account_id}/` prefix in that S3 bucket.
+
+> **Tip**: The deployment creates two stacks. Your results are in the stack *you named*, not the auto-generated `aiml-sec-*` stack. See [Troubleshooting](docs/TROUBLESHOOTING.md#8-confused-by-multiple-cloudformation-stacks) for details.
-
-
-Stack Type
-How to Identify
-What It Contains
-What to Do
-
-
-Infrastructure Stack (This is the one you need)
-
-The name you chose
-Examples:
- - my-aiml-assessment
- - aiml-security-prod
- - aiml-security-single-account
-
-
-AWS CodeBuild project
-Amazon S3 bucket for results
-AWS IAM roles
-The "AssessmentBucket" output
-
-
-Use this stack to view results!
-1. Open this stack in console
-2. Go to Outputs tab
-3. Copy AssessmentBucket value
-
-
-
-Assessment Stack (Auto-generated - ignore this)
-
-Auto-generated name:
-Single-account: aiml-sec-{account_id}
-Multi-account: aiml-security-{account_id} per member account, plus aiml-security-mgmt for the management account
-Examples:
-aiml-sec-123456789012 (single)
-aiml-security-123456789012 (multi)
-
-
-AWS Lambda functions
-AWS Step Functions
-Internal resources
-No outputs you need
-
-
-Don't use this stack!
-It's for internal operations only.
-Created automatically by AWS CodeBuild.
-
-
-
-
-**Quick Check**: If you see a stack name starting with `aiml-sec-` or `aiml-security-` followed by numbers (or `aiml-security-mgmt`), that's an **auto-generated assessment stack**. Look for the stack name you originally chose during deployment.
+---
## Multi-Account Deployment
-### Prerequisites
+### Step 1: Deploy Member Roles
-- AWS Organizations setup with management account access or delegated administrator privileges.
+Deploy [1-aiml-security-member-roles.yaml](deployment/1-aiml-security-member-roles.yaml) to all target accounts using CloudFormation StackSets with service-managed permissions.
-The deployment follows a two-step approach:
+1. Navigate to **CloudFormation** > **StackSets** in the AWS Organizations management account or delegated administrator account
+2. Upload the template and set `ManagementAccountID` to the account ID where the central multi-account CodeBuild project runs
+3. Select **Service-managed permissions** and target your OUs
+4. Select your target region and submit
-### Step 1: Deploy Member Roles (AWS CloudFormation StackSets)
+### Step 2: Deploy Central Infrastructure
-Deploy [1-aiml-security-member-roles.yaml](deployment/1-aiml-security-member-roles.yaml) to all target accounts using AWS CloudFormation StackSets with service-managed permissions.
+Deploy [2-aiml-security-codebuild.yaml](deployment/2-aiml-security-codebuild.yaml) in your central assessment account. This can be your AWS Organizations management account or a delegated administrator/central tooling account.
-#### AWS Console Deployment
+1. Upload the template and set `MultiAccountScan` to `true`
+2. Optionally set `TargetRegions` for multi-region scanning
+3. Optionally provide an email address for notifications
+4. Acknowledge IAM capabilities and submit
+5. Stack creation automatically triggers the assessment across all accounts
-1. Navigate to **AWS CloudFormation** > **StackSets** in the management account
-2. Click **Create StackSet**
-3. Select **Upload a template file** and upload [1-aiml-security-member-roles.yaml](deployment/1-aiml-security-member-roles.yaml)
-4. Enter a StackSet name (for example, `aiml-security-member-roles`)
-5. Set the `ManagementAccountID` parameter to your management account ID
-6. Under **Permissions**, select **Service-managed permissions**
-7. Under **Deployment targets**, select the Organizational Units (OUs) containing your target accounts
-8. Select **us-east-1** (or your target region) under **Specify regions**
-9. Review and click **Submit**
+---
-This uses AWS Organizations to deploy the member role to all accounts in the selected OUs. New accounts added to those OUs will automatically receive the role.
+## Multi-Region Scanning
-### Step 2: Deploy Central Infrastructure
+Both deployment modes support scanning multiple AWS regions in parallel via the `TargetRegions` parameter:
+
+| Value | Behavior |
+|-------|----------|
+| Empty (default) | Scans deployment region only — fully backward compatible |
+| Comma- or space-separated (for example, `us-east-1,us-west-2` or `us-east-1 us-west-2`) | Scans those regions in parallel |
+| `all` | Discovers and scans all regions where assessed services are available |
-Deploy [2-aiml-security-codebuild.yaml](deployment/2-aiml-security-codebuild.yaml) in your central management account or delegated administrator member account.
+Scanning uses a Step Functions Map state, so multiple regions execute in parallel with no additional time cost. Services unavailable in a region produce an informational N/A finding.
-#### AWS Console Deployment
+The HTML report includes a Region column, filter dropdown, and "Risk by Region" summary.
-1. Navigate to [AWS CloudFormation](https://console.aws.amazon.com/cloudformation/home#/stacks/create/template?stackName=aiml-security-multi-account)
-2. Select **Upload a template file** and upload the [2-aiml-security-codebuild.yaml](deployment/2-aiml-security-codebuild.yaml) file.
-3. Set the `MultiAccountScan` parameter to `true`.
-4. Optionally, provide your email address in the `EmailAddress` parameter for completion notifications.
-5. Optionally, set `EnableFinServAssessment` to `true` to run the Financial Services GenAI risk checks (FS-01..FS-69). It defaults to `false`; enable it only if you must adhere to FinServ compliance, as it adds a dedicated FinServ section to the report. See [How finding severities are determined](#how-finding-severities-are-determined) and the [FinServ check references](docs/SECURITY_CHECKS_FINSERV_COMMON.md).
-6. Leave the remaining parameters at their default values.
-6. Navigate to the next page, read and acknowledge the notice, and click **Next**.
-7. Review the information and click **Submit**.
-8. Stack creation automatically triggers AWS CodeBuild, which deploys the assessment to each account and runs it.
+> **Upgrading an existing deployment?** See [Troubleshooting](docs/TROUBLESHOOTING.md#9-upgrading-an-existing-deployment-to-multi-region) — it's a simple stack parameter update with no teardown.
+
+---
## How It Works
+1. **Deploy** — CloudFormation creates CodeBuild, S3, IAM roles, and a Lambda trigger
+2. **CodeBuild runs** — builds and deploys the SAM assessment stack (per account in multi-account mode)
+3. **Step Functions execute** — orchestrates: S3 cleanup → IAM permission caching → resolve regions → Map state fans out per-region assessments (Bedrock, SageMaker, AgentCore in parallel) → optionally run FinServ checks → generate consolidated report
+4. **Results** — HTML and CSV reports are stored in your S3 bucket
+
### Optional: Financial Services GenAI Risk Checks (`EnableFinServAssessment`)
The 64 Financial Services (FS-XX) GenAI risk checks are **opt-in** and default to `false`. Set the
-`EnableFinServAssessment` deployment parameter to `true` only if you must adhere to FinServ
-compliance. When enabled, the FinServ assessment Lambda runs and its findings appear in a dedicated
+`EnableFinServAssessment` deployment parameter to `true` when you want the additional Financial
+Services GenAI risk assessment. When enabled, the FinServ assessment Lambda runs and its findings appear in a dedicated
**Financial Services** section of the HTML report. When left `false`, no FinServ findings are
produced and the report omits the FinServ section entirely. The toggle is threaded into the Step
Functions execution input (`enableFinServ`); the FinServ Lambda is always deployed but is invoked
@@ -266,23 +214,20 @@ only when the flag is `true`.
#### Scope and limitations
-- **Single Region per run.** The assessment evaluates resources in the deployment Region only (the assessment Lambdas use their own Region). Region-scoped controls — WAF, API Gateway, Bedrock guardrails and Knowledge Bases, OpenSearch Serverless, Lambda, and SageMaker monitoring — are not evaluated in other Regions. For multi-Region GenAI workloads, deploy and run the assessment in each Region.
+- **FinServ Region scope.** Core Bedrock, SageMaker, AgentCore, and optional FinServ checks use the resolved `TargetRegions` from the deployment parameters. FinServ findings are emitted with Region values so they appear alongside the same regional filter and per-region report views as the core service checks.
- **Heuristic and advisory checks.** Some controls cannot be verified through an API (application-layer controls, dataset contents, resource associations); these are reported as `ADVISORY`/`N/A` and require manual review. See [How finding severities are determined](#how-finding-severities-are-determined).
- **Permissions.** A check that lacks an IAM permission is reported as `COULD NOT ASSESS` (not a failure). Re-deploy the member role after any IAM template change so newer actions take effect.
-### Single-Account Mode (`MultiAccountScan=false`)
+For detailed architecture, execution flow, and extension guidance, see the [Developer Guide](docs/DEVELOPER_GUIDE.md).
-- Creates a local `AIMLSecurityMemberRole`
-- Runs the assessment in the same account
-- Uses a local Amazon S3 bucket for results
+---
-### Multi-Account Mode (`MultiAccountScan=true`)
+## Viewing Results
-- Lists all active accounts in AWS Organizations
-- Assumes the `AIMLSecurityMemberRole` in each target account
-- Deploys selected assessment modules in each account with a shared Amazon S3 bucket
-- Executes AWS Step Functions for each deployed module in each account
-- Consolidates results by assessment type in a central Amazon S3 bucket
+1. Open your **infrastructure stack** in CloudFormation → **Outputs** tab → copy `AssessmentBucket`
+2. Navigate to that S3 bucket
+3. For single-account, open `{account_id}/security_assessment_single_account_*.html`
+4. For multi-account, open `consolidated-reports/security_assessment_multi_account_*.html`
### Assessment Execution Process
@@ -308,24 +253,6 @@ only when the flag is `true`.
7. **Reporting**: Generates multi-account HTML and CSV reports
8. **Notification**: Sends completion notification through Amazon SNS (if configured)
-## Permissions Required
-
-### Central Account Role (`MultiAccountCodeBuildRole`)
-
-- Assumes roles in member accounts
-- Lists AWS Organizations accounts
-- Deploys AWS CloudFormation/AWS SAM applications
-- Executes AWS Step Functions
-- Writes to the Amazon S3 bucket
-
-### Member Account Role (`AIMLSecurityMemberRole`)
-
-- Read-only access to AI/ML services (Amazon Bedrock, Amazon SageMaker AI, Amazon Bedrock AgentCore, and FinServ-specific services: AWS WAF, AWS Shield, Amazon Macie, AWS Organizations, Amazon OpenSearch Serverless)
-- AWS IAM read permissions for security assessment
-- AWS CloudTrail, Amazon GuardDuty, and AWS Lambda read permissions
-- Amazon VPC and Amazon EC2 read permissions
-- Amazon ECR, Amazon CloudWatch Logs, and AWS X-Ray read permissions (for Amazon Bedrock AgentCore)
-
## Monitoring and Results
- **Amazon S3 Bucket**: Central storage for all assessment results
@@ -333,8 +260,6 @@ only when the flag is `true`.
- **Amazon SNS Notifications**: Email alerts on completion/failure
- **Amazon EventBridge Rules**: Automated workflow triggers
-## Viewing Assessment Results
-
You can check the AWS CodeBuild console to confirm the assessment completed successfully before accessing the results.
### Accessing Results
@@ -351,7 +276,7 @@ You can check the AWS CodeBuild console to confirm the assessment completed succ
2. **Navigate to the Amazon S3 Bucket**:
- Go to **Amazon S3** in the AWS Console
- Search for and open your assessment bucket
- - For single-account deployments, open the `security_assessment_XXXXX.html` report
+ - For single-account deployments, open the `{account_id}/` folder and then open the `security_assessment_single_account_YYYYMMDD_HHMMSS.html` report
- For multi-account deployments, follow the [Report Structure](#report-structure) guidance below
### Report Structure
@@ -360,13 +285,13 @@ You can check the AWS CodeBuild console to confirm the assessment completed succ
- **Location**: `consolidated-reports/` folder in the bucket
- **Content**: Multi-account HTML report combining all account assessments
-- **File Format**: `multi_account_report_YYYYMMDD_HHMMSS.html`
+- **File Format**: `security_assessment_multi_account_YYYYMMDD_HHMMSS.html`
- **Features**:
- Executive summary with metrics (Total, High, Medium, Low severity counts)
- Service breakdown (Amazon Bedrock, Amazon SageMaker AI, Amazon Bedrock AgentCore, Financial Services GenAI Risk)
- Priority recommendations
- Light/dark mode toggle (persists through localStorage)
- - Dropdown filters for Account ID, Severity, Status
+ - Dropdown filters for Account ID, Region, Service, Severity, Status
- Text search filter for findings
- "View Docs" buttons for reference links
@@ -381,23 +306,24 @@ You can check the AWS CodeBuild console to confirm the assessment completed succ
- `finserv_security_report_{execution_id}.csv` - Financial Services GenAI risk assessment results (64 FS-XX checks)
- `permissions_cache_{execution_id}.json` - IAM permissions cache
- - `security_assessment_{timestamp}_{execution_id}.html` - Consolidated HTML report (same features as multi-account report)
+ - `security_assessment_single_account_{timestamp}.html` - Consolidated HTML report (same features as multi-account report)
### Understanding Results
-| Severity | Description |
-|----------|-------------|
-| **High** | Critical security issues requiring immediate attention |
-| **Medium** | Important security improvements recommended |
-| **Low** | Minor optimizations suggested |
-| **Informational** | Advisory information, no action required |
-| **N/A** | Check not applicable (no resources to assess) |
-
-| Status | Description |
-|--------|-------------|
-| **Failed** | Security issue identified that requires remediation |
-| **Passed** | Checked resources met the assessed best practice at time of scan |
-| **N/A** | No resources exist to check (for example, no notebooks, no guardrails configured) |
+| Severity | Meaning |
+|----------|---------|
+| **High** | Critical — immediate action required |
+| **Medium** | Important — should be addressed |
+| **Low** | Minor — best practice optimization |
+| **Informational** | Advisory — no action required |
+
+| Status | Meaning |
+|--------|---------|
+| **Failed** | Security issue identified |
+| **Passed** | Resource meets best practice |
+| **N/A** | No resources to assess or service not available in region |
+
+---
### How finding severities are determined
@@ -427,119 +353,30 @@ preliminary — validate with your MRM/Legal/Compliance teams before relying on
## Customization
-### Adding New Accounts
+| Task | How |
+|------|-----|
+| Add new accounts | Add to StackSet deployment targets |
+| Modify permissions scope | Edit `1-aiml-security-member-roles.yaml` |
+| Adjust concurrency | Change `ConcurrentAccountScans` parameter |
+| Add new service checks | See [Developer Guide](docs/DEVELOPER_GUIDE.md#adding-new-aiml-service-assessments) |
-#### Option A: AWS Console
-
-1. Navigate to **AWS CloudFormation** > **StackSets**
-2. Select `aiml-security-member-roles` AWS CloudFormation StackSet
-3. Click **Add stacks to StackSet**
-4. Choose deployment targets:
- - **Deploy to accounts**: Enter specific account IDs
- - **Regions**: Select target regions
-5. Review and click **Submit**
-
-### Modifying Assessment Scope
-
-To add or remove service permissions, edit the member role permissions in `1-aiml-security-member-roles.yaml`.
-
-### Concurrent Scanning
-
-Adjust the `ConcurrentAccountScans` parameter based on your organization size and cost considerations.
-
-## Cleanup
-
-### Single-Account Cleanup
-
-To remove all resources deployed for single-account assessment:
-
-1. **Delete the AWS SAM-deployed assessment stack**:
- - Navigate to **AWS CloudFormation** > **Stacks**
- - Select the `aiml-sec-{account_id}` stack (for example, `aiml-sec-123456789012`)
- - Click **Delete**
- - Wait for stack deletion to complete
-
-2. **Delete the AWS CodeBuild infrastructure stack**:
- - Select the `aiml-security-single-account` stack (or your custom stack name)
- - Click **Delete**
- - Wait for stack deletion to complete
-
-3. **Clean up Amazon S3 buckets** (if stack deletion fails due to non-empty buckets):
- ```bash
- # Empty the assessment bucket
- aws s3 rm s3:// --recursive
-
- # If versioning is enabled, delete version markers
- aws s3api delete-objects --bucket --delete \
- "$(aws s3api list-object-versions --bucket \
- --query '{Objects: Versions[].{Key:Key,VersionId:VersionId}}')"
-
- # Delete the bucket
- aws s3 rb s3://
- ```
-
-### Multi-Account Cleanup
-
-To remove all resources deployed for multi-account assessment:
-
-1. **Delete AWS SAM-deployed stacks in each member account**:
- - For each account that was scanned, navigate to **AWS CloudFormation** > **Stacks**
- - Select the `aiml-security-{account_id}` stack (for example, `aiml-security-123456789012`)
- - For the management account, select `aiml-security-mgmt`
- - Click **Delete**
- - Alternatively, use the AWS CLI to delete across accounts:
- ```bash
- # Assume role in member account and delete stack
- aws cloudformation delete-stack --stack-name aiml-security- \
- --region
- ```
-
-2. **Delete the central AWS CodeBuild infrastructure stack**:
- - In the management account, navigate to **AWS CloudFormation** > **Stacks**
- - Select the `aiml-security-multi-account` stack
- - Click **Delete**
- - Wait for stack deletion to complete
-
-3. **Delete the AWS CloudFormation StackSet member roles**:
- - Navigate to **AWS CloudFormation** > **StackSets**
- - Select the `aiml-security-member-roles` AWS CloudFormation StackSet
- - Click **Actions** > **Delete stacks from StackSet**
- - Select all deployment targets (OUs or accounts)
- - Wait for stack instances to be deleted
- - Once all stack instances are removed, delete the AWS CloudFormation StackSet itself
-
-4. **Clean up Amazon S3 buckets** (if stack deletion fails due to non-empty buckets):
- ```bash
- # List and identify assessment buckets
- aws s3 ls | grep aiml-security
-
- # Empty each bucket
- aws s3 rm s3:// --recursive
-
- # Delete version markers if versioning was enabled
- aws s3api delete-objects --bucket --delete \
- "$(aws s3api list-object-versions --bucket \
- --query '{Objects: Versions[].{Key:Key,VersionId:VersionId}}')"
-
- # Delete the bucket
- aws s3 rb s3://
- ```
-
-### Cleanup Order
+---
-For a clean removal, delete resources in this order:
+## Permissions Required
-1. **Assessment stacks** (auto-created by SAM):
- - Single-account: `aiml-sec-{account_id}` (for example, `aiml-sec-123456789012`)
- - Multi-account: `aiml-security-{account_id}` per member account, plus `aiml-security-mgmt` for management account
+The deployment uses multiple IAM roles with different trust and permission boundaries. They are not all read-only.
-2. **Infrastructure stack** (the stack you deployed manually):
- - Single-account: Your chosen stack name (for example, `my-aiml-assessment`)
- - Multi-account: `aiml-security-multi-account` or your chosen name
+- **`CodeBuildRole` / `MultiAccountCodeBuildRole`**: orchestration roles used by the infrastructure stack to clone the repo, build SAM, deploy/update the assessment stack, and start Step Functions executions. These roles require infrastructure-management permissions such as CloudFormation, Lambda, IAM, Step Functions, and S3 actions.
+- **`AIMLSecurityMemberRole`**: role assumed in the target account during single-account and multi-account runs. In the multi-account flow this role is also **not read-only**. It needs both service-read permissions for the checks and deployment permissions so CodeBuild can create or update the per-account SAM assessment stack.
+- **SAM-created Lambda execution roles**: runtime roles for the assessment functions. These are the closest thing to read-only assessment roles. They primarily use `List*`, `Describe*`, and `Get*` access against Bedrock, SageMaker, AgentCore, IAM analysis APIs, and supporting read APIs, plus S3 access to write reports and read the cached IAM permissions file.
-3. AWS CloudFormation StackSet member roles (multi-account only)
+If you need to reduce scope, review the role policies in:
-4. Any remaining Amazon S3 buckets manually
+- [deployment/aiml-security-single-account.yaml](deployment/aiml-security-single-account.yaml)
+- [deployment/1-aiml-security-member-roles.yaml](deployment/1-aiml-security-member-roles.yaml)
+- [deployment/2-aiml-security-codebuild.yaml](deployment/2-aiml-security-codebuild.yaml)
+- [aiml-security-assessment/template.yaml](aiml-security-assessment/template.yaml)
+- [aiml-security-assessment/template-multi-account.yaml](aiml-security-assessment/template-multi-account.yaml)
---
@@ -548,35 +385,34 @@ For a clean removal, delete resources in this order:
| Document | Description |
|----------|-------------|
| [Security Checks Reference](docs/SECURITY_CHECKS.md) | Complete reference for all 116 security checks with severity levels |
-| [FinServ GenAI Risk Checks — Common](docs/SECURITY_CHECKS_FINSERV_COMMON.md) | Shared introduction, severity rubric, upstream-overlap table, and compliance framework mapping for FS-01..69 |
-| [FinServ Part 1 — Infrastructure Controls](docs/SECURITY_CHECKS_FINSERV_PART1_INFRA_CONTROLS.md) | FS-01..26: Unbounded consumption, excessive agency, supply chain, training data poisoning, vector & embedding weaknesses |
-| [FinServ Part 2 — Guardrails & Content Safety](docs/SECURITY_CHECKS_FINSERV_PART2_GUARDRAILS_CONTENT_SAFETY.md) | FS-27..46: Non-compliant output, misinformation, abusive/harmful output, biased output, PII disclosure |
-| [FinServ Part 3 — App Layer & Gaps](docs/SECURITY_CHECKS_FINSERV_PART3_APP_LAYER_AND_GAPS.md) | FS-47..69: Hallucination, prompt injection, improper output handling, off-topic output, out-of-date training data, cross-category gap checks |
+| [FinServ GenAI Risk Checks](docs/SECURITY_CHECKS_FINSERV.md) | Complete FS-01..69 reference: shared introduction, severity rubric, upstream-overlap table, compliance framework mapping, and all check definitions (Part 1 infrastructure controls, Part 2 guardrails & content safety, Part 3 app-layer controls & gaps) |
| [FinServ Severity Methodology](docs/SECURITY_CHECKS_FINSERV_SEVERITY_METHODOLOGY.md) | Likelihood × Impact → ASFF severity model, disposition rules, and research basis for FS check severities |
| [FinServ Severity Register](docs/SECURITY_CHECKS_FINSERV_SEVERITY_REGISTER.md) | Authoritative per-finding severity assignments (the single source of truth enforced by the drift-guard test) |
-| [FinServ Compliance Mappings](docs/AIMLSecurityAssessment-MappingsTable.csv) | Machine-readable mapping of FS checks to SR 11-7, FFIEC CAT, NYDFS 500.06, PCI-DSS, DORA, MAS TRM, ISO 27001, OWASP LLM Top 10 |
-| [Troubleshooting Guide](docs/TROUBLESHOOTING.md) | Common issues, debugging tips, and FAQ |
+| [FinServ Compliance Mappings](docs/SECURITY_CHECKS_FINSERV.md#compliance-framework-mapping) | Preliminary mapping of FS checks to SR 11-7, FFIEC CAT, NYDFS 500, PCI-DSS, DORA, MAS TRM, ISO 27001, ECOA, and OWASP LLM Top 10 |
+| [Troubleshooting Guide](docs/TROUBLESHOOTING.md) | Common issues, stack identification, upgrade guide, debugging |
| [Developer Guide](docs/DEVELOPER_GUIDE.md) | Architecture details, adding custom checks, and contributing |
+| [Cleanup Guide](docs/CLEANUP.md) | Step-by-step resource removal instructions |
---
## CI/CD
-GitHub Actions workflows run automatically on pull requests and pushes to `main`:
+GitHub Actions workflows run automatically on pull requests and selected pushes:
| Workflow | Trigger | What It Checks |
|----------|---------|----------------|
-| **Python Code Quality** | PR | Runs `ruff check` and `ruff format --check` on changed Python files |
+| **Python Code Quality** | PR | `ruff check` and `ruff format --check` on changed Python files |
+| **AI/ML Security Assessment Tests** | PR, push to `main`/`develop` | Runs the `pytest` suite (assessment functions and report pipeline) on Python 3.11 and 3.12 |
| **CloudFormation Lint** | PR | Validates deployment and SAM templates with `cfn-lint` |
-| **SAM Validate & Build** | PR | Runs `sam validate --lint` and `sam build` on SAM templates |
-| **ASH Security Scan** | PR | Scans changed files for secrets, dependency vulnerabilities, and IaC misconfigurations |
-| **ASH Full Repository Scan** | Push to main, monthly | Full repository security scan with results uploaded as artifacts |
+| **SAM Validate & Build** | PR | `sam validate --lint` and `sam build` on SAM templates |
+| **ASH Security Scan** | PR | Scans for secrets, dependency vulnerabilities, and IaC misconfigurations |
+| **ASH Full Repository Scan** | Push to main, monthly | Full repository security scan |
---
## Contributing
-We welcome community contributions! Please see [Developer Guide](docs/DEVELOPER_GUIDE.md) for guidelines.
+We welcome community contributions! See the [Developer Guide](docs/DEVELOPER_GUIDE.md) for guidelines.
## Security
diff --git a/aiml-security-assessment/functions/security/agentcore_assessments/app.py b/aiml-security-assessment/functions/security/agentcore_assessments/app.py
index cd72a65..595f9be 100644
--- a/aiml-security-assessment/functions/security/agentcore_assessments/app.py
+++ b/aiml-security-assessment/functions/security/agentcore_assessments/app.py
@@ -15,7 +15,7 @@
from datetime import datetime, timezone
from typing import Dict, List, Any
from botocore.config import Config
-from botocore.exceptions import ClientError
+from botocore.exceptions import ClientError, EndpointConnectionError
from schema import create_finding, SeverityEnum, StatusEnum
@@ -26,26 +26,37 @@
# Configure boto3 with adaptive retry mode
boto3_config = Config(retries=dict(max_attempts=10, mode="adaptive"))
-# Initialize AWS clients
+# Initialize S3 client (always uses Lambda's region for bucket operations)
s3_client = boto3.client("s3", config=boto3_config)
-iam_client = boto3.client("iam", config=boto3_config)
-ec2_client = boto3.client("ec2", config=boto3_config)
-ecr_client = boto3.client("ecr", config=boto3_config)
-logs_client = boto3.client("logs", config=boto3_config)
-xray_client = boto3.client("xray", config=boto3_config)
-cloudwatch_client = boto3.client("cloudwatch", config=boto3_config)
-
-# Initialize AgentCore client
-try:
- agentcore_client = boto3.client("bedrock-agentcore-control", config=boto3_config)
- logger.info("Successfully initialized bedrock-agentcore-control client")
-except Exception as e:
- logger.warning(f"Failed to initialize bedrock-agentcore-control client: {e}")
- agentcore_client = None
+
+# Regional clients — initialized in lambda_handler with target region
+iam_client = None
+ec2_client = None
+ecr_client = None
+logs_client = None
+xray_client = None
+cloudwatch_client = None
+agentcore_client = None
# Environment variables
BUCKET_NAME = os.environ.get("AIML_ASSESSMENT_BUCKET_NAME")
+# IAM is a global service. Findings derived purely from the IAM permission cache
+# (e.g. AC-02, AC-03) are identical across regions, so they are produced only on
+# the primary region (Map index 0) and tagged with this region label to avoid
+# duplicate findings when scanning multiple regions.
+GLOBAL_REGION_LABEL = "Global"
+
+# Error codes returned when a region exists but is not enabled/usable for the
+# account (opt-in regions, disabled regions). The availability probe treats
+# these the same as an endpoint connection failure.
+REGION_UNAVAILABLE_ERROR_CODES = {
+ "UnrecognizedClientException",
+ "InvalidClientTokenId",
+ "AuthFailure",
+ "OptInRequired",
+}
+
# Execution tracking
start_time = None
@@ -129,6 +140,12 @@ def _agentcore_list_all(
kwargs["nextToken"] = next_token
response = list_method(**kwargs)
+ if not isinstance(response, dict):
+ logger.warning(
+ f"{list_method_name} returned unexpected response type: "
+ f"{type(response).__name__}"
+ )
+ break
for result_key in result_keys:
page_items = response.get(result_key)
@@ -137,6 +154,12 @@ def _agentcore_list_all(
break
next_token = response.get("nextToken")
+ if next_token is not None and not isinstance(next_token, str):
+ logger.warning(
+ f"{list_method_name} returned non-string nextToken: "
+ f"{type(next_token).__name__}"
+ )
+ break
if not next_token:
break
@@ -201,6 +224,7 @@ def generate_csv_report(findings: List[Dict[str, Any]]) -> str:
"Reference",
"Severity",
"Status",
+ "Region",
],
)
writer.writeheader()
@@ -217,6 +241,7 @@ def generate_csv_report(findings: List[Dict[str, Any]]) -> str:
"Reference",
"Severity",
"Status",
+ "Region",
],
)
writer.writeheader()
@@ -230,7 +255,9 @@ def generate_csv_report(findings: List[Dict[str, Any]]) -> str:
return csv_content
-def write_to_s3(execution_id: str, csv_content: str, bucket_name: str) -> str:
+def write_to_s3(
+ execution_id: str, csv_content: str, bucket_name: str, region: str = ""
+) -> str:
"""
Upload CSV report to S3.
@@ -238,6 +265,7 @@ def write_to_s3(execution_id: str, csv_content: str, bucket_name: str) -> str:
execution_id: Unique execution identifier
csv_content: CSV content to upload
bucket_name: S3 bucket name
+ region: AWS region identifier for the report filename
Returns:
S3 URL of uploaded file
@@ -246,7 +274,10 @@ def write_to_s3(execution_id: str, csv_content: str, bucket_name: str) -> str:
Exception: If upload fails
"""
try:
- key = f"agentcore_security_report_{execution_id}.csv"
+ if region:
+ key = f"agentcore_security_report_{execution_id}_{region}.csv"
+ else:
+ key = f"agentcore_security_report_{execution_id}.csv"
s3_client.put_object(
Bucket=bucket_name,
@@ -1505,9 +1536,40 @@ def check_agentcore_vpc_endpoints() -> List[Dict[str, Any]]:
"""
findings = []
+ if agentcore_client is None:
+ findings.append(
+ create_finding(
+ check_id="AC-08",
+ finding_name="AgentCore VPC Endpoints Check",
+ finding_details="AgentCore client not available in this region",
+ resolution="Deploy in a region where Amazon Bedrock AgentCore is available",
+ reference="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/vpc.html",
+ severity=SeverityEnum.INFORMATIONAL,
+ status=StatusEnum.NA,
+ )
+ )
+ return findings
+
try:
logger.info("Checking for AgentCore VPC endpoints")
+ runtimes_response = agentcore_client.list_agent_runtimes()
+ runtimes = runtimes_response.get("agentRuntimes", [])
+
+ if not runtimes:
+ findings.append(
+ create_finding(
+ check_id="AC-08",
+ finding_name="AgentCore VPC Endpoints Check",
+ finding_details="No AgentCore resources found",
+ resolution="No action required",
+ reference="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/vpc.html",
+ severity=SeverityEnum.INFORMATIONAL,
+ status=StatusEnum.NA,
+ )
+ )
+ return findings
+
# Get all VPCs
vpcs_response = ec2_client.describe_vpcs()
vpcs = vpcs_response.get("Vpcs", [])
@@ -1806,7 +1868,9 @@ def check_agentcore_resource_based_policies() -> List[Dict[str, Any]]:
gateway_name = gateway.get("name", gateway_id)
try:
- gateway_details = agentcore_client.get_gateway(gatewayId=gateway_id)
+ gateway_details = agentcore_client.get_gateway(
+ gatewayIdentifier=gateway_id
+ )
gateway_arn = gateway_details.get("gatewayArn")
if not gateway_arn:
@@ -2171,7 +2235,9 @@ def check_agentcore_gateway_encryption() -> List[Dict[str, Any]]:
gateway_name = gateway.get("name", gateway_id)
try:
- gateway_details = agentcore_client.get_gateway(gatewayId=gateway_id)
+ gateway_details = agentcore_client.get_gateway(
+ gatewayIdentifier=gateway_id
+ )
# Check for customer-managed KMS key
encryption_key_arn = gateway_details.get(
@@ -2388,47 +2454,181 @@ def lambda_handler(event, context):
Lambda handler for AgentCore security assessment.
Args:
- event: Lambda event containing execution_id
+ event: Lambda event containing execution_id and Region
context: Lambda context
Returns:
Response with status and S3 URL
"""
- global start_time
+ global start_time, iam_client, ec2_client, ecr_client, logs_client
+ global xray_client, cloudwatch_client, agentcore_client
start_time = time.time()
try:
- # Extract execution ID
+ # Extract target region from Step Functions Map state
+ region = event.get("Region", os.environ.get("AWS_REGION", "us-east-1"))
+ # IAM is global: only the primary region (Map index 0) runs IAM-only checks.
+ is_primary_region = int(event.get("RegionIndex", 0)) == 0
+ logger.info(f"Scanning region: {region} (primary={is_primary_region})")
+
execution_id = event.get("Execution", {}).get("Name", "unknown")
- logger.info(
- f"Starting AgentCore security assessment for execution: {execution_id}"
+
+ # Initialize regional clients (iam is global, the rest are region-scoped)
+ iam_client = boto3.client("iam", config=boto3_config)
+ ec2_client = boto3.client("ec2", config=boto3_config, region_name=region)
+ ecr_client = boto3.client("ecr", config=boto3_config, region_name=region)
+ logs_client = boto3.client("logs", config=boto3_config, region_name=region)
+ xray_client = boto3.client("xray", config=boto3_config, region_name=region)
+ cloudwatch_client = boto3.client(
+ "cloudwatch", config=boto3_config, region_name=region
)
- # Retrieve permission cache
+ # Collect all findings
+ all_findings = []
+
+ # Retrieve permission cache (shared/global IAM data)
try:
permission_cache = get_permissions_cache(execution_id)
except Exception as e:
logger.warning(f"Failed to retrieve permission cache: {e}")
permission_cache = {"role_permissions": [], "user_permissions": []}
- # Collect all findings
- all_findings = []
+ # Run global IAM-only checks once (on the primary region) so the same role
+ # violations are not reported once per scanned region. These run before the
+ # regional availability gate so they are still emitted even if AgentCore is
+ # not available in the primary region.
+ if is_primary_region:
+ global_checks = [
+ (
+ "IAM Full Access",
+ lambda: check_agentcore_full_access_roles(permission_cache),
+ ),
+ (
+ "Stale Access",
+ lambda: check_stale_agentcore_access(permission_cache),
+ ),
+ # AC-09 inspects a global IAM service-linked role, so it is also
+ # run once on the primary region rather than per scanned region.
+ ("Service-Linked Role", check_agentcore_service_linked_role),
+ ]
+ for check_name, check_func in global_checks:
+ try:
+ logger.info(f"Running global check: {check_name}")
+ global_findings = check_func()
+ for finding in global_findings:
+ finding["Region"] = GLOBAL_REGION_LABEL
+ all_findings.extend(global_findings)
+ except Exception as e:
+ logger.error(f"Error in global check '{check_name}': {e}")
+ all_findings.append(
+ create_finding(
+ check_id="AC-00",
+ finding_name=f"AgentCore {check_name} Check Error",
+ finding_details=f"Error during {check_name} check: {str(e)}",
+ resolution="Investigate error and retry assessment",
+ reference="https://aws.github.io/bedrock-agentcore-starter-toolkit/",
+ severity=SeverityEnum.HIGH,
+ status=StatusEnum.FAILED,
+ region=GLOBAL_REGION_LABEL,
+ )
+ )
+
+ # Reset per-invocation so a warm container cannot leak a previous
+ # region's client if creation below fails.
+ agentcore_client = None
+ try:
+ agentcore_client = boto3.client(
+ "bedrock-agentcore-control", config=boto3_config, region_name=region
+ )
+ except Exception as e:
+ # The client could not even be constructed (e.g. the SDK in this
+ # runtime does not know the service). This is the one case where the
+ # region genuinely cannot be assessed.
+ logger.warning(
+ f"Failed to initialize bedrock-agentcore-control client: {e}"
+ )
+ agentcore_client = None
- # Execute all assessment checks
+ if agentcore_client is not None:
+ # Test service availability with a lightweight call
+ try:
+ agentcore_client.list_agent_runtimes(maxResults=1)
+ logger.info("Successfully initialized bedrock-agentcore-control client")
+ except EndpointConnectionError:
+ logger.info(
+ f"AgentCore service not available in region {region}, skipping"
+ )
+ agentcore_client = None
+ except ClientError as e:
+ error_code = e.response.get("Error", {}).get("Code", "")
+ if error_code in REGION_UNAVAILABLE_ERROR_CODES:
+ logger.info(
+ f"AgentCore not accessible in region {region} ({error_code}), skipping"
+ )
+ agentcore_client = None
+ else:
+ # Service is reachable but returned another API error (e.g. access
+ # denied) — proceed; individual checks handle their own errors.
+ logger.info(
+ f"AgentCore client initialized (probe returned {error_code})"
+ )
+ except Exception as e:
+ # An unexpected probe failure (e.g. a boto3/botocore SDK param or
+ # operation mismatch such as ParamValidationError/AttributeError)
+ # says nothing about regional availability. Treating it as "not
+ # available" would silently skip every AgentCore check and emit a
+ # false N/A report, so keep the client and let the individual
+ # checks surface their own errors instead.
+ logger.warning(
+ f"AgentCore availability probe raised an unexpected error, "
+ f"proceeding with checks: {e}"
+ )
+
+ # If AgentCore not available, produce an N/A report (plus any global IAM
+ # findings already collected on the primary region) and exit early
+ if agentcore_client is None:
+ all_findings.append(
+ create_finding(
+ check_id="AC-00",
+ finding_name="AgentCore Service Availability",
+ finding_details=f"Amazon Bedrock AgentCore is not available in region {region}. No checks performed.",
+ resolution="No action required. AgentCore is not deployed in this region.",
+ reference="https://aws.github.io/bedrock-agentcore-starter-toolkit/",
+ severity=SeverityEnum.INFORMATIONAL,
+ status=StatusEnum.NA,
+ region=region,
+ )
+ )
+ for finding in all_findings:
+ if not finding.get("Region"):
+ finding["Region"] = region
+ csv_content = generate_csv_report(all_findings)
+ s3_url = write_to_s3(execution_id, csv_content, BUCKET_NAME, region=region)
+ return {
+ "statusCode": 200,
+ "body": json.dumps(
+ {
+ "message": f"AgentCore not available in {region}",
+ "s3_url": s3_url,
+ }
+ ),
+ }
+
+ logger.info(
+ f"Starting AgentCore security assessment for execution: {execution_id}"
+ )
+
+ # Execute regional assessment checks (IAM-only checks AC-02/AC-03 and the
+ # global service-linked role check AC-09 are run separately, once, on the
+ # primary region above)
checks = [
("VPC Configuration", check_agentcore_vpc_configuration),
- (
- "IAM Full Access",
- lambda: check_agentcore_full_access_roles(permission_cache),
- ),
- ("Stale Access", lambda: check_stale_agentcore_access(permission_cache)),
("Observability", check_agentcore_observability),
("Encryption", check_agentcore_encryption),
("Browser Tool Recording", check_browser_tool_recording),
("Memory Configuration", check_agentcore_memory_configuration),
("Gateway Configuration", check_agentcore_gateway_configuration),
("VPC Endpoints", check_agentcore_vpc_endpoints),
- ("Service-Linked Role", check_agentcore_service_linked_role),
("Resource-Based Policies", check_agentcore_resource_based_policies),
("Policy Engine Encryption", check_agentcore_policy_engine_encryption),
("Gateway Encryption", check_agentcore_gateway_encryption),
@@ -2455,7 +2655,6 @@ def lambda_handler(event, context):
except Exception as e:
logger.error(f"Error in check '{check_name}': {e}")
- # Add error finding
all_findings.append(
create_finding(
check_id="AC-00",
@@ -2465,15 +2664,21 @@ def lambda_handler(event, context):
reference="https://aws.github.io/bedrock-agentcore-starter-toolkit/",
severity=SeverityEnum.HIGH,
status=StatusEnum.FAILED,
+ region=region,
)
)
+ # Inject region into all findings that don't have it set
+ for finding in all_findings:
+ if not finding.get("Region"):
+ finding["Region"] = region
+
# Generate CSV report
logger.info(f"Generating CSV report with {len(all_findings)} total findings")
csv_content = generate_csv_report(all_findings)
# Upload to S3
- s3_url = write_to_s3(execution_id, csv_content, BUCKET_NAME)
+ s3_url = write_to_s3(execution_id, csv_content, BUCKET_NAME, region=region)
# Calculate execution metrics
total_duration = time.time() - start_time
diff --git a/aiml-security-assessment/functions/security/agentcore_assessments/schema.py b/aiml-security-assessment/functions/security/agentcore_assessments/schema.py
index 6c5a9f5..16a66d9 100644
--- a/aiml-security-assessment/functions/security/agentcore_assessments/schema.py
+++ b/aiml-security-assessment/functions/security/agentcore_assessments/schema.py
@@ -1,6 +1,6 @@
from enum import Enum
-from typing import Dict, Any
-from pydantic import BaseModel, Field, validator
+from typing import Any, Dict
+from pydantic import BaseModel, Field, field_validator
import re
@@ -35,8 +35,12 @@ class Finding(BaseModel):
Reference: str = Field(..., description="Documentation reference URL")
Severity: SeverityEnum = Field(..., description="Severity level of the finding")
Status: StatusEnum = Field(..., description="Current status of the finding")
+ Region: str = Field(
+ default="", description="AWS region where the finding was identified"
+ )
- @validator("Check_ID")
+ @field_validator("Check_ID")
+ @classmethod
def validate_check_id(cls, v):
"""Validate that Check_ID follows the pattern XX-NN (e.g., SM-01, BR-14, AC-05)"""
pattern = r"^[A-Z]{2,3}-\d{2}$"
@@ -46,21 +50,24 @@ def validate_check_id(cls, v):
)
return v
- @validator("Reference")
+ @field_validator("Reference")
+ @classmethod
def validate_reference_url(cls, v):
"""Validate that reference URL starts with https://"""
if not str(v).startswith("https://"):
raise ValueError("Reference URL must start with https://")
return v
- @validator("Severity")
+ @field_validator("Severity")
+ @classmethod
def validate_severity(cls, v):
"""Validate that severity is one of the allowed values"""
if v not in SeverityEnum.__members__.values():
raise ValueError("Severity must be one of the allowed values")
return v
- @validator("Status")
+ @field_validator("Status")
+ @classmethod
def validate_status(cls, v):
"""Validate that status is one of the allowed values"""
if v not in StatusEnum.__members__.values():
@@ -76,6 +83,7 @@ def create_finding(
reference: str,
severity: SeverityEnum,
status: StatusEnum,
+ region: str = "",
) -> Dict[str, Any]:
"""
Create a validated finding object
@@ -88,6 +96,7 @@ def create_finding(
reference: Documentation URL
severity: Severity level
status: Current status
+ region: AWS region where the finding was identified
Returns:
Dict[str, Any]: Validated finding as dictionary
@@ -103,5 +112,6 @@ def create_finding(
Reference=reference,
Severity=severity,
Status=status,
+ Region=region,
)
return dict(finding.model_dump()) # Convert to regular dictionary
diff --git a/aiml-security-assessment/functions/security/bedrock_assessments/app.py b/aiml-security-assessment/functions/security/bedrock_assessments/app.py
index be4ecd5..264dd42 100644
--- a/aiml-security-assessment/functions/security/bedrock_assessments/app.py
+++ b/aiml-security-assessment/functions/security/bedrock_assessments/app.py
@@ -7,7 +7,7 @@
from typing import Dict, List, Any, Optional
from io import StringIO
from botocore.config import Config
-from botocore.exceptions import ClientError
+from botocore.exceptions import ClientError, EndpointConnectionError
import random
import json
from schema import create_finding
@@ -25,6 +25,171 @@
logger = logging.getLogger()
logger.setLevel(logging.ERROR)
+# IAM is a global service. Findings derived purely from the IAM permission cache
+# (e.g. BR-01, BR-03) are identical across regions, so they are produced only on
+# the primary region (Map index 0) and tagged with this region label to avoid
+# duplicate findings when scanning multiple regions.
+GLOBAL_REGION_LABEL = "Global"
+
+# Error codes returned when a region exists but is not enabled/usable for the
+# account (opt-in regions, disabled regions). The availability probe treats
+# these the same as an endpoint connection failure.
+REGION_UNAVAILABLE_ERROR_CODES = {
+ "UnrecognizedClientException",
+ "InvalidClientTokenId",
+ "AuthFailure",
+ "OptInRequired",
+}
+
+ACCESS_DENIED_ERROR_CODES = {
+ "AccessDenied",
+ "AccessDeniedException",
+ "UnauthorizedOperation",
+}
+
+
+def describe_api_error(error: Exception, api_label: str, region: str = "") -> str:
+ """
+ Build a report-friendly description for an API error raised by a regional
+ check.
+
+ Some regions don't support a given Bedrock API. boto3 surfaces this as
+ "Unknown operation ..." (ValidationException) or UnknownOperationException.
+ For those, return a clean " not available in " message
+ instead of leaking the raw boto3 exception text into the report. Any other
+ error keeps its raw text so genuine problems (e.g. permissions) stay
+ diagnosable.
+ """
+ error_text = str(error)
+ if "UnknownOperation" in error_text or "Unknown operation" in error_text:
+ location = region if region else "this region"
+ return f"{api_label} not available in {location}"
+ return f"Unable to check {api_label}: {error_text}"
+
+
+def _probe_bedrock_resource_list(probe_label: str, probe_func) -> Optional[bool]:
+ """
+ Probe a Bedrock list API and report whether it found any regional resources.
+
+ Returns:
+ True if the API found at least one resource
+ False if the API was successfully queried and returned no resources
+ None if the result is inconclusive (for example, AccessDenied)
+ """
+ try:
+ return bool(probe_func())
+ except EndpointConnectionError:
+ raise
+ except ClientError as e:
+ code = e.response.get("Error", {}).get("Code", "")
+ error_text = str(e)
+
+ if code in REGION_UNAVAILABLE_ERROR_CODES:
+ raise
+
+ if code in ACCESS_DENIED_ERROR_CODES:
+ logger.warning(
+ f"Unable to determine regional Bedrock footprint from {probe_label}: {code}"
+ )
+ return None
+
+ if (
+ code == "ValidationException"
+ or "UnknownOperation" in error_text
+ or "Unknown operation" in error_text
+ ):
+ logger.info(f"{probe_label} API not available in this region")
+ return False
+
+ logger.warning(
+ f"Unexpected error probing {probe_label} for regional Bedrock footprint: {error_text}"
+ )
+ return None
+ except Exception as e:
+ error_text = str(e)
+ if "UnknownOperation" in error_text or "Unknown operation" in error_text:
+ logger.info(f"{probe_label} API not available in this region")
+ return False
+
+ logger.warning(
+ f"Unexpected error probing {probe_label} for regional Bedrock footprint: {error_text}"
+ )
+ return None
+
+
+def _first_page_items(paginator, result_key: str) -> List[Dict[str, Any]]:
+ """Return at most the first page of items from a paginator-based Bedrock list API."""
+ for page in paginator.paginate(PaginationConfig={"MaxItems": 1, "PageSize": 1}):
+ return page.get(result_key, [])
+ return []
+
+
+def detect_bedrock_regional_footprint(region: str = "") -> Optional[bool]:
+ """
+ Detect whether a region has Bedrock-managed resources that justify regional findings.
+
+ Returns:
+ True if Bedrock-managed resources exist in the region
+ False if supported APIs were probed and no resources were found
+ None if the footprint could not be determined confidently
+ """
+ bedrock_client = boto3.client("bedrock", config=boto3_config, region_name=region)
+ bedrock_agent_client = boto3.client(
+ "bedrock-agent", config=boto3_config, region_name=region
+ )
+
+ probes = [
+ (
+ "Bedrock Guardrails",
+ lambda: bedrock_client.list_guardrails().get("guardrails", []),
+ ),
+ (
+ "Bedrock Prompts",
+ lambda: bedrock_agent_client.list_prompts().get("promptSummaries", []),
+ ),
+ (
+ "Bedrock Agents",
+ lambda: bedrock_agent_client.list_agents().get("agents", []),
+ ),
+ (
+ "Bedrock Knowledge Bases",
+ lambda: _first_page_items(
+ bedrock_agent_client.get_paginator("list_knowledge_bases"),
+ "knowledgeBaseSummaries",
+ ),
+ ),
+ (
+ "Bedrock Flows",
+ lambda: _first_page_items(
+ bedrock_agent_client.get_paginator("list_flows"),
+ "flowSummaries",
+ ),
+ ),
+ (
+ "Bedrock Custom Models",
+ lambda: _first_page_items(
+ bedrock_client.get_paginator("list_custom_models"),
+ "modelSummaries",
+ ),
+ ),
+ ]
+
+ indeterminate = False
+ successful_empty_probe = False
+ for probe_label, probe_func in probes:
+ probe_result = _probe_bedrock_resource_list(probe_label, probe_func)
+ if probe_result is True:
+ return True
+ if probe_result is False:
+ successful_empty_probe = True
+ if probe_result is None:
+ indeterminate = True
+
+ if successful_empty_probe:
+ return False
+
+ return None if indeterminate else False
+
def _extract_s3_bucket_name(s3_config: Optional[Dict[str, Any]]) -> Optional[str]:
"""Support both the documented field name and the legacy test fixture key."""
@@ -96,7 +261,9 @@ def get_permissions_cache(execution_id: str) -> Optional[Dict[str, Any]]:
return None
-def check_marketplace_subscription_access(permission_cache) -> Dict[str, Any]:
+def check_marketplace_subscription_access(
+ permission_cache, region: str = ""
+) -> Dict[str, Any]:
logger.debug("Starting check for overly permissive Marketplace subscription access")
try:
findings = {
@@ -182,6 +349,7 @@ def check_policy_for_subscription_access(policy_doc: Any) -> bool:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam-awsmanpol.html#security-iam-awsmanpol-bedrock-marketplace",
severity="High",
status="Failed",
+ region=region,
)
)
else:
@@ -197,6 +365,7 @@ def check_policy_for_subscription_access(policy_doc: Any) -> bool:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam-awsmanpol.html#security-iam-awsmanpol-bedrock-marketplace",
severity="Medium",
status="Passed",
+ region=region,
)
)
@@ -219,6 +388,7 @@ def check_policy_for_subscription_access(policy_doc: Any) -> bool:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/security.html",
severity="High",
status="Failed",
+ region=region,
)
],
}
@@ -282,7 +452,14 @@ def has_bedrock_access(iam_client, principal_name: str, principal_type: str) ->
return False
-def check_stale_bedrock_access(permission_cache) -> Dict[str, Any]:
+def check_stale_bedrock_access(permission_cache, region: str = "") -> Dict[str, Any]:
+ """
+ Check for stale Bedrock access using IAM service-last-accessed data.
+
+ This check is derived purely from IAM (a global service) and the cached
+ permissions, so it produces identical results in every region. The handler
+ runs it once, on the primary region, tagged with GLOBAL_REGION_LABEL.
+ """
logger.debug("Starting check for stale Bedrock access")
try:
findings = {
@@ -322,6 +499,7 @@ def check_stale_bedrock_access(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_last-accessed.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
return findings
@@ -408,6 +586,7 @@ def check_stale_bedrock_access(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_last-accessed.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
@@ -435,6 +614,7 @@ def check_stale_bedrock_access(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_last-accessed.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
@@ -455,12 +635,15 @@ def check_stale_bedrock_access(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/security.html",
severity="High",
status="Failed",
+ region=region,
)
],
}
-def check_bedrock_full_access_roles(permission_cache) -> Dict[str, Any]:
+def check_bedrock_full_access_roles(
+ permission_cache, region: str = ""
+) -> Dict[str, Any]:
"""
Check for roles with AmazonBedrockFullAccess policy using cached permissions
"""
@@ -495,6 +678,7 @@ def check_bedrock_full_access_roles(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/security_iam_id-based-policy-examples-agent.html#iam-agents-ex-all\nhttps://docs.aws.amazon.com/bedrock/latest/userguide/security_iam_id-based-policy-examples-br-studio.html",
severity="High",
status="Failed",
+ region=region,
)
)
else:
@@ -508,6 +692,7 @@ def check_bedrock_full_access_roles(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/security_iam_id-based-policy-examples-agent.html#iam-agents-ex-all\nhttps://docs.aws.amazon.com/bedrock/latest/userguide/security_iam_id-based-policy-examples-br-studio.html",
severity="High",
status="Passed",
+ region=region,
)
)
@@ -552,13 +737,13 @@ def get_role_usage(role_name: str) -> str:
return result
-def check_bedrock_vpc_endpoints() -> Dict[str, bool]:
+def check_bedrock_vpc_endpoints(region: str = "") -> Dict[str, bool]:
"""
Check if any VPC has Bedrock VPC endpoints
"""
logger.debug("Checking for Bedrock VPC endpoints")
try:
- ec2_client = boto3.client("ec2", config=boto3_config)
+ ec2_client = boto3.client("ec2", config=boto3_config, region_name=region)
bedrock_endpoints = [
"com.amazonaws.region.bedrock",
@@ -568,8 +753,7 @@ def check_bedrock_vpc_endpoints() -> Dict[str, bool]:
]
# Get current region
- session = boto3.session.Session()
- current_region = session.region_name
+ current_region = region
logger.debug(f"Current region: {current_region}")
# Get list of all VPCs
@@ -678,7 +862,9 @@ def handle_aws_throttling(func, *args, **kwargs):
raise
-def check_bedrock_access_and_vpc_endpoints(permission_cache) -> Dict[str, Any]:
+def check_bedrock_access_and_vpc_endpoints(
+ permission_cache, region: str = ""
+) -> Dict[str, Any]:
logger.debug("Starting check for Bedrock access and VPC endpoints")
try:
findings = {
@@ -703,7 +889,25 @@ def check_bedrock_access_and_vpc_endpoints(permission_cache) -> Dict[str, Any]:
break
if bedrock_access_found:
- vpc_endpoint_check = check_bedrock_vpc_endpoints()
+ bedrock_footprint_found = detect_bedrock_regional_footprint(region=region)
+
+ if bedrock_footprint_found is False:
+ findings["details"] = "No regional Bedrock resources found"
+ findings["csv_data"].append(
+ create_finding(
+ check_id="BR-02",
+ finding_name="Amazon Bedrock private connectivity check",
+ finding_details="No regional Bedrock resources found to assess private connectivity",
+ resolution="No action required",
+ reference="https://docs.aws.amazon.com/bedrock/latest/userguide/vpc-interface-endpoints.html",
+ severity="Informational",
+ status="N/A",
+ region=region,
+ )
+ )
+ return findings
+
+ vpc_endpoint_check = check_bedrock_vpc_endpoints(region=region)
if not vpc_endpoint_check["has_endpoints"]:
findings["status"] = "WARN"
@@ -725,6 +929,7 @@ def check_bedrock_access_and_vpc_endpoints(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/vpc-interface-endpoints.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
else:
@@ -745,6 +950,7 @@ def check_bedrock_access_and_vpc_endpoints(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/vpc-interface-endpoints.html",
severity="High",
status="Passed",
+ region=region,
)
)
else:
@@ -769,12 +975,13 @@ def check_bedrock_access_and_vpc_endpoints(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/security.html",
severity="High",
status="Failed",
+ region=region,
)
],
}
-def check_bedrock_guardrails() -> Dict[str, Any]:
+def check_bedrock_guardrails(region: str = "") -> Dict[str, Any]:
"""
Check if Amazon Bedrock Guardrails are configured and being used
"""
@@ -787,7 +994,9 @@ def check_bedrock_guardrails() -> Dict[str, Any]:
"csv_data": [],
}
- bedrock_client = boto3.client("bedrock", config=boto3_config)
+ bedrock_client = boto3.client(
+ "bedrock", config=boto3_config, region_name=region
+ )
try:
# List all guardrails
@@ -809,23 +1018,44 @@ def check_bedrock_guardrails() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html",
severity="High",
status="Passed",
+ region=region,
)
)
else:
- findings["status"] = "WARN"
- findings["details"] = "No Bedrock guardrails configured"
- findings["csv_data"].append(
- create_finding(
- check_id="BR-05",
- finding_name="Bedrock Guardrails Check",
- finding_details="No Amazon Bedrock Guardrails are configured. This may expose your application to potential risks such as harmful content, sensitive information disclosure, or hallucinations.",
- resolution="Configure Bedrock Guardrails to implement safeguards such as:\n- Content filters to block harmful content\n- Denied topics to prevent undesirable discussions\n- Sensitive information filters to protect PII\n- Contextual grounding checks to prevent hallucinations",
- reference="https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html",
- severity="Medium",
- status="Failed",
- )
+ bedrock_footprint_found = detect_bedrock_regional_footprint(
+ region=region
)
+ if bedrock_footprint_found is False:
+ findings["details"] = "No regional Bedrock resources found"
+ findings["csv_data"].append(
+ create_finding(
+ check_id="BR-05",
+ finding_name="Bedrock Guardrails Check",
+ finding_details="No regional Bedrock resources found to protect with guardrails",
+ resolution="No action required",
+ reference="https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html",
+ severity="Informational",
+ status="N/A",
+ region=region,
+ )
+ )
+ else:
+ findings["status"] = "WARN"
+ findings["details"] = "No Bedrock guardrails configured"
+ findings["csv_data"].append(
+ create_finding(
+ check_id="BR-05",
+ finding_name="Bedrock Guardrails Check",
+ finding_details="No Amazon Bedrock Guardrails are configured. This may expose your application to potential risks such as harmful content, sensitive information disclosure, or hallucinations.",
+ resolution="Configure Bedrock Guardrails to implement safeguards such as:\n- Content filters to block harmful content\n- Denied topics to prevent undesirable discussions\n- Sensitive information filters to protect PII\n- Contextual grounding checks to prevent hallucinations",
+ reference="https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html",
+ severity="Medium",
+ status="Failed",
+ region=region,
+ )
+ )
+
except bedrock_client.exceptions.ValidationException as e:
findings["status"] = "ERROR"
findings["details"] = f"Error validating guardrails configuration: {str(e)}"
@@ -838,6 +1068,7 @@ def check_bedrock_guardrails() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html",
severity="High",
status="Failed",
+ region=region,
)
)
@@ -858,12 +1089,13 @@ def check_bedrock_guardrails() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/security.html",
severity="High",
status="Failed",
+ region=region,
)
],
}
-def check_bedrock_logging_configuration() -> Dict[str, Any]:
+def check_bedrock_logging_configuration(region: str = "") -> Dict[str, Any]:
"""
Check if model invocation logging is enabled for Amazon Bedrock
"""
@@ -871,7 +1103,7 @@ def check_bedrock_logging_configuration() -> Dict[str, Any]:
# logging is enabled, the FinServ guide (PDF §1.2.1, §1.2.6, §1.2.7)
# expects the log output to include guardrailTrace with action,
# inputAssessments, and outputAssessments to support SR 11-7 audit trails
- # and NYDFS 500.06 retention. See docs/SECURITY_CHECKS_FINSERV_PART3_APP_LAYER_AND_GAPS.md
+ # and NYDFS 500.06 retention. See docs/SECURITY_CHECKS_FINSERV.md
# (FS-64 → BR-04 extension note) for the detection / remediation language.
logger.debug("Starting check for Bedrock model invocation logging configuration")
try:
@@ -882,7 +1114,26 @@ def check_bedrock_logging_configuration() -> Dict[str, Any]:
"csv_data": [],
}
- bedrock_client = boto3.client("bedrock", config=boto3_config)
+ bedrock_footprint_found = detect_bedrock_regional_footprint(region=region)
+ if bedrock_footprint_found is False:
+ findings["details"] = "No regional Bedrock resources found"
+ findings["csv_data"].append(
+ create_finding(
+ check_id="BR-04",
+ finding_name="Bedrock Model Invocation Logging Check",
+ finding_details="No regional Bedrock resources found to monitor with invocation logging",
+ resolution="No action required",
+ reference="https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html",
+ severity="Informational",
+ status="N/A",
+ region=region,
+ )
+ )
+ return findings
+
+ bedrock_client = boto3.client(
+ "bedrock", config=boto3_config, region_name=region
+ )
try:
# Get current logging configuration
@@ -918,6 +1169,7 @@ def check_bedrock_logging_configuration() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
else:
@@ -932,6 +1184,7 @@ def check_bedrock_logging_configuration() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
@@ -947,6 +1200,7 @@ def check_bedrock_logging_configuration() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
@@ -969,12 +1223,13 @@ def check_bedrock_logging_configuration() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/security.html",
severity="High",
status="Failed",
+ region=region,
)
],
}
-def check_bedrock_cloudtrail_logging() -> Dict[str, Any]:
+def check_bedrock_cloudtrail_logging(region: str = "") -> Dict[str, Any]:
"""
Check if CloudTrail is configured to log Amazon Bedrock API calls
"""
@@ -982,7 +1237,7 @@ def check_bedrock_cloudtrail_logging() -> Dict[str, Any]:
# Bedrock API calls, the FinServ guide (PDF §1.2.15) expects an advanced
# event selector for AWS::Bedrock::KnowledgeBase so Retrieve and
# RetrieveAndGenerate data events are captured (NOT logged by default).
- # See docs/SECURITY_CHECKS_FINSERV_PART1_INFRA_CONTROLS.md (FS-23 → BR-06
+ # See docs/SECURITY_CHECKS_FINSERV.md (FS-23 → BR-06
# extension note) for the detection / remediation language.
logger.debug("Starting check for Bedrock CloudTrail logging configuration")
try:
@@ -993,7 +1248,26 @@ def check_bedrock_cloudtrail_logging() -> Dict[str, Any]:
"csv_data": [],
}
- cloudtrail_client = boto3.client("cloudtrail", config=boto3_config)
+ bedrock_footprint_found = detect_bedrock_regional_footprint(region=region)
+ if bedrock_footprint_found is False:
+ findings["details"] = "No regional Bedrock resources found"
+ findings["csv_data"].append(
+ create_finding(
+ check_id="BR-06",
+ finding_name="Bedrock CloudTrail Logging Check",
+ finding_details="No regional Bedrock resources found to audit with Bedrock-specific CloudTrail coverage",
+ resolution="No action required",
+ reference="https://docs.aws.amazon.com/bedrock/latest/userguide/logging-using-cloudtrail.html",
+ severity="Informational",
+ status="N/A",
+ region=region,
+ )
+ )
+ return findings
+
+ cloudtrail_client = boto3.client(
+ "cloudtrail", config=boto3_config, region_name=region
+ )
try:
# Get all trails
@@ -1063,6 +1337,7 @@ def check_bedrock_cloudtrail_logging() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/logging-using-cloudtrail.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
else:
@@ -1081,6 +1356,7 @@ def check_bedrock_cloudtrail_logging() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/logging-using-cloudtrail.html",
severity="High",
status="Failed",
+ region=region,
)
)
@@ -1096,6 +1372,7 @@ def check_bedrock_cloudtrail_logging() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/logging-using-cloudtrail.html",
severity="High",
status="Failed",
+ region=region,
)
)
@@ -1118,12 +1395,13 @@ def check_bedrock_cloudtrail_logging() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/security.html",
severity="High",
status="Failed",
+ region=region,
)
],
}
-def check_bedrock_prompt_management() -> Dict[str, Any]:
+def check_bedrock_prompt_management(region: str = "") -> Dict[str, Any]:
"""
Check if Amazon Bedrock Prompt Management feature is being used
"""
@@ -1136,7 +1414,9 @@ def check_bedrock_prompt_management() -> Dict[str, Any]:
"csv_data": [],
}
- bedrock_client = boto3.client("bedrock-agent", config=boto3_config)
+ bedrock_client = boto3.client(
+ "bedrock-agent", config=boto3_config, region_name=region
+ )
try:
# List all prompts
@@ -1156,6 +1436,7 @@ def check_bedrock_prompt_management() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-management.html",
severity="Low",
status="Passed",
+ region=region,
)
)
@@ -1193,6 +1474,7 @@ def check_bedrock_prompt_management() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-management.html",
severity="Low",
status="Failed",
+ region=region,
)
)
else:
@@ -1211,21 +1493,27 @@ def check_bedrock_prompt_management() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-management.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
- except bedrock_client.exceptions.ValidationException as e:
- findings["status"] = "ERROR"
- findings["details"] = f"Error checking Prompt Management: {str(e)}"
+ except Exception as e:
+ # An API error (e.g. InternalServerErrorException after retries,
+ # throttling, or a permissions issue) is not a security failure.
+ # Surface it as N/A rather than Failed, matching the BR-11 pattern.
+ logger.warning(f"Error listing prompts: {str(e)}")
findings["csv_data"].append(
create_finding(
check_id="BR-07",
finding_name="Bedrock Prompt Management Check",
- finding_details=f"Error checking Bedrock Prompt Management configuration: {str(e)}",
- resolution="Verify your AWS credentials and permissions to access Bedrock Prompt Management.",
+ finding_details=describe_api_error(
+ e, "Bedrock Prompt Management API", region
+ ),
+ resolution="Verify your AWS credentials and permissions to access Bedrock Prompt Management, then retry the assessment.",
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-management.html",
- severity="High",
- status="Failed",
+ severity="Low",
+ status="N/A",
+ region=region,
)
)
@@ -1248,12 +1536,13 @@ def check_bedrock_prompt_management() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/security.html",
severity="High",
status="Failed",
+ region=region,
)
],
}
-def check_bedrock_knowledge_base_encryption() -> Dict[str, Any]:
+def check_bedrock_knowledge_base_encryption(region: str = "") -> Dict[str, Any]:
"""
Check if Amazon Bedrock Knowledge Bases have proper encryption configured
including customer-managed KMS keys for data at rest
@@ -1267,7 +1556,9 @@ def check_bedrock_knowledge_base_encryption() -> Dict[str, Any]:
"csv_data": [],
}
- bedrock_agent_client = boto3.client("bedrock-agent", config=boto3_config)
+ bedrock_agent_client = boto3.client(
+ "bedrock-agent", config=boto3_config, region_name=region
+ )
try:
# List all knowledge bases
@@ -1287,6 +1578,7 @@ def check_bedrock_knowledge_base_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/encryption-kb.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
return findings
@@ -1347,6 +1639,7 @@ def check_bedrock_knowledge_base_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/encryption-kb.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
@@ -1360,6 +1653,7 @@ def check_bedrock_knowledge_base_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/encryption-kb.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
else:
@@ -1372,6 +1666,7 @@ def check_bedrock_knowledge_base_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/encryption-kb.html",
severity="High",
status="Passed",
+ region=region,
)
)
@@ -1390,6 +1685,7 @@ def check_bedrock_knowledge_base_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/encryption-kb.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
else:
@@ -1408,6 +1704,7 @@ def check_bedrock_knowledge_base_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/encryption-kb.html",
severity="High",
status="Failed",
+ region=region,
)
)
else:
@@ -1432,12 +1729,15 @@ def check_bedrock_knowledge_base_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/security.html",
severity="High",
status="Failed",
+ region=region,
)
],
}
-def check_bedrock_guardrail_iam_enforcement(permission_cache) -> Dict[str, Any]:
+def check_bedrock_guardrail_iam_enforcement(
+ permission_cache, region: str = ""
+) -> Dict[str, Any]:
"""
Check if IAM policies enforce the use of specific guardrails via
the bedrock:GuardrailIdentifier condition key
@@ -1451,7 +1751,9 @@ def check_bedrock_guardrail_iam_enforcement(permission_cache) -> Dict[str, Any]:
"csv_data": [],
}
- bedrock_client = boto3.client("bedrock", config=boto3_config)
+ bedrock_client = boto3.client(
+ "bedrock", config=boto3_config, region_name=region
+ )
# First check if any guardrails exist
try:
@@ -1468,6 +1770,7 @@ def check_bedrock_guardrail_iam_enforcement(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-permissions-id.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
return findings
@@ -1567,6 +1870,7 @@ def check_bedrock_guardrail_iam_enforcement(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-permissions-id.html",
severity="High",
status="Failed",
+ region=region,
)
)
else:
@@ -1581,6 +1885,7 @@ def check_bedrock_guardrail_iam_enforcement(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-permissions-id.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
else:
@@ -1594,6 +1899,7 @@ def check_bedrock_guardrail_iam_enforcement(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-permissions-id.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
@@ -1616,12 +1922,13 @@ def check_bedrock_guardrail_iam_enforcement(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/security.html",
severity="High",
status="Failed",
+ region=region,
)
],
}
-def check_bedrock_custom_model_encryption() -> Dict[str, Any]:
+def check_bedrock_custom_model_encryption(region: str = "") -> Dict[str, Any]:
"""
Check if custom/fine-tuned Bedrock models have proper encryption configured
"""
@@ -1634,7 +1941,9 @@ def check_bedrock_custom_model_encryption() -> Dict[str, Any]:
"csv_data": [],
}
- bedrock_client = boto3.client("bedrock", config=boto3_config)
+ bedrock_client = boto3.client(
+ "bedrock", config=boto3_config, region_name=region
+ )
try:
# List custom models
@@ -1654,6 +1963,7 @@ def check_bedrock_custom_model_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/encryption-custom-job.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
return findings
@@ -1721,6 +2031,7 @@ def check_bedrock_custom_model_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/encryption-custom-job.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
else:
@@ -1733,6 +2044,7 @@ def check_bedrock_custom_model_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/encryption-custom-job.html",
severity="High",
status="Passed",
+ region=region,
)
)
@@ -1742,11 +2054,12 @@ def check_bedrock_custom_model_encryption() -> Dict[str, Any]:
create_finding(
check_id="BR-11",
finding_name="Bedrock Custom Model Encryption Check",
- finding_details=f"Unable to list custom models: {str(e)}",
+ finding_details=describe_api_error(e, "Custom model API", region),
resolution="Verify permissions to access Bedrock custom models",
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/model-customization-iam-role.html",
severity="Low",
status="N/A",
+ region=region,
)
)
@@ -1769,12 +2082,13 @@ def check_bedrock_custom_model_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/security.html",
severity="High",
status="Failed",
+ region=region,
)
],
}
-def check_bedrock_invocation_log_encryption() -> Dict[str, Any]:
+def check_bedrock_invocation_log_encryption(region: str = "") -> Dict[str, Any]:
"""
Check if S3 buckets used for model invocation logging have proper encryption
"""
@@ -1787,8 +2101,10 @@ def check_bedrock_invocation_log_encryption() -> Dict[str, Any]:
"csv_data": [],
}
- bedrock_client = boto3.client("bedrock", config=boto3_config)
- s3_client = boto3.client("s3", config=boto3_config)
+ bedrock_client = boto3.client(
+ "bedrock", config=boto3_config, region_name=region
+ )
+ s3_client = boto3.client("s3", config=boto3_config, region_name=region)
try:
# Get logging configuration
@@ -1809,6 +2125,7 @@ def check_bedrock_invocation_log_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
return findings
@@ -1850,6 +2167,7 @@ def check_bedrock_invocation_log_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
else:
@@ -1863,6 +2181,7 @@ def check_bedrock_invocation_log_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
@@ -1881,6 +2200,7 @@ def check_bedrock_invocation_log_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html",
severity="High",
status="Failed",
+ region=region,
)
)
elif _is_access_denied_client_error(e):
@@ -1897,6 +2217,7 @@ def check_bedrock_invocation_log_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
else:
@@ -1912,6 +2233,7 @@ def check_bedrock_invocation_log_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
@@ -1934,12 +2256,13 @@ def check_bedrock_invocation_log_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/security.html",
severity="High",
status="Failed",
+ region=region,
)
],
}
-def check_bedrock_flows_guardrails() -> Dict[str, Any]:
+def check_bedrock_flows_guardrails(region: str = "") -> Dict[str, Any]:
"""
Check if Bedrock Flows have guardrails configured on prompt and knowledge base nodes
"""
@@ -1952,7 +2275,9 @@ def check_bedrock_flows_guardrails() -> Dict[str, Any]:
"csv_data": [],
}
- bedrock_agent_client = boto3.client("bedrock-agent", config=boto3_config)
+ bedrock_agent_client = boto3.client(
+ "bedrock-agent", config=boto3_config, region_name=region
+ )
try:
# List all flows
@@ -1972,6 +2297,7 @@ def check_bedrock_flows_guardrails() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/flows-guardrails.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
return findings
@@ -2056,6 +2382,7 @@ def check_bedrock_flows_guardrails() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/flows-guardrails.html",
severity="High",
status="Failed",
+ region=region,
)
)
else:
@@ -2070,6 +2397,7 @@ def check_bedrock_flows_guardrails() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/flows-guardrails.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
else:
@@ -2083,6 +2411,7 @@ def check_bedrock_flows_guardrails() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/flows-guardrails.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
@@ -2092,11 +2421,12 @@ def check_bedrock_flows_guardrails() -> Dict[str, Any]:
create_finding(
check_id="BR-13",
finding_name="Bedrock Flows Guardrails Check",
- finding_details=f"Unable to check flows: {str(e)}",
+ finding_details=describe_api_error(e, "Bedrock Flows API", region),
resolution="Verify permissions to access Bedrock Flows",
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/flows-guardrails.html",
severity="Low",
status="N/A",
+ region=region,
)
)
@@ -2119,12 +2449,13 @@ def check_bedrock_flows_guardrails() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/security.html",
severity="High",
status="Failed",
+ region=region,
)
],
}
-def check_bedrock_agent_roles(permission_cache) -> Dict[str, Any]:
+def check_bedrock_agent_roles(permission_cache, region: str = "") -> Dict[str, Any]:
"""
Check IAM roles associated with Bedrock agents for least privilege access
"""
@@ -2137,7 +2468,9 @@ def check_bedrock_agent_roles(permission_cache) -> Dict[str, Any]:
"csv_data": [],
}
- bedrock_client = boto3.client("bedrock-agent", config=boto3_config)
+ bedrock_client = boto3.client(
+ "bedrock-agent", config=boto3_config, region_name=region
+ )
try:
# Get all Bedrock agents
@@ -2157,6 +2490,7 @@ def check_bedrock_agent_roles(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/security_iam_service-with-iam.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
return findings
@@ -2264,6 +2598,7 @@ def check_bedrock_agent_roles(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/wellarchitected/latest/generative-ai-lens/gensec05-bp01.html",
severity="High",
status="Failed",
+ region=region,
)
)
else:
@@ -2279,6 +2614,7 @@ def check_bedrock_agent_roles(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/wellarchitected/latest/generative-ai-lens/gensec05-bp01.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
@@ -2294,6 +2630,7 @@ def check_bedrock_agent_roles(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/wellarchitected/latest/generative-ai-lens/gensec05-bp01.html",
severity="High",
status="Failed",
+ region=region,
)
)
@@ -2314,6 +2651,7 @@ def check_bedrock_agent_roles(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/bedrock/latest/userguide/security.html",
severity="High",
status="Failed",
+ region=region,
)
],
}
@@ -2333,6 +2671,7 @@ def generate_csv_report(findings: List[Dict[str, Any]]) -> str:
"Reference",
"Severity",
"Status",
+ "Region",
]
writer = csv.DictWriter(csv_buffer, fieldnames=fieldnames)
@@ -2349,14 +2688,19 @@ def get_current_utc_date():
return datetime.now(timezone.utc).strftime("%Y/%m/%d")
-def write_to_s3(execution_id, csv_content: str, bucket_name: str) -> str:
+def write_to_s3(
+ execution_id, csv_content: str, bucket_name: str, region: str = ""
+) -> str:
"""
Write CSV report to S3 bucket
"""
logger.debug(f"Writing CSV report to S3 bucket: {bucket_name}")
try:
s3_client = boto3.client("s3", config=boto3_config)
- file_name = f"bedrock_security_report_{execution_id}.csv"
+ if region:
+ file_name = f"bedrock_security_report_{execution_id}_{region}.csv"
+ else:
+ file_name = f"bedrock_security_report_{execution_id}.csv"
s3_client.put_object(
Bucket=bucket_name, Key=file_name, Body=csv_content, ContentType="text/csv"
@@ -2378,9 +2722,16 @@ def lambda_handler(event, context):
all_findings = []
try:
- # Initialize permission cache
- logger.info("Initializing IAM permission cache")
+ # Extract target region from Step Functions Map state
+ region = event.get("Region", os.environ.get("AWS_REGION", "us-east-1"))
+ # IAM is global: only the primary region (Map index 0) runs IAM-only checks.
+ is_primary_region = int(event.get("RegionIndex", 0)) == 0
+ logger.info(f"Scanning region: {region} (primary={is_primary_region})")
+
execution_id = event["Execution"]["Name"]
+
+ # Initialize permission cache (shared/global IAM data)
+ logger.info("Initializing IAM permission cache")
permission_cache = get_permissions_cache(execution_id)
if not permission_cache:
@@ -2389,67 +2740,143 @@ def lambda_handler(event, context):
)
permission_cache = {"role_permissions": {}, "user_permissions": {}}
- # Run all checks using the cached permissions
- logger.info("Running AmazonBedrockFullAccess check")
- bedrock_full_access_findings = check_bedrock_full_access_roles(permission_cache)
- all_findings.append(bedrock_full_access_findings)
+ # Run global IAM-only checks once (on the primary region) so the same role
+ # violations are not reported once per scanned region. These run before the
+ # regional availability gate so they are still emitted even if Bedrock is
+ # not available in the primary region.
+ if is_primary_region:
+ logger.info("Running global AmazonBedrockFullAccess check (BR-01)")
+ all_findings.append(
+ check_bedrock_full_access_roles(
+ permission_cache, region=GLOBAL_REGION_LABEL
+ )
+ )
+
+ logger.info("Running global marketplace subscription access check (BR-03)")
+ all_findings.append(
+ check_marketplace_subscription_access(
+ permission_cache, region=GLOBAL_REGION_LABEL
+ )
+ )
+
+ # logger.info("Running global stale Bedrock access check (BR-14)")
+ # all_findings.append(
+ # check_stale_bedrock_access(permission_cache, region=GLOBAL_REGION_LABEL)
+ # )
+
+ # Verify Bedrock is available in this region. A ValidationException here
+ # (logging simply not configured) still means the service is reachable,
+ # so only an endpoint connection failure or a region-not-enabled error
+ # should short-circuit the assessment.
+ bedrock_unavailable = False
+ unavailable_detail = ""
+ try:
+ test_client = boto3.client(
+ "bedrock", config=boto3_config, region_name=region
+ )
+ test_client.get_model_invocation_logging_configuration()
+ except EndpointConnectionError:
+ bedrock_unavailable = True
+ unavailable_detail = f"Amazon Bedrock is not available in region {region}. No checks performed."
+ except ClientError as e:
+ error_code = e.response.get("Error", {}).get("Code", "")
+ if error_code in REGION_UNAVAILABLE_ERROR_CODES:
+ bedrock_unavailable = True
+ unavailable_detail = f"Amazon Bedrock is not available or not enabled in region {region} ({error_code}). No checks performed."
+ else:
+ # Service is reachable (e.g. ValidationException, AccessDenied) —
+ # proceed; individual checks handle their own errors.
+ logger.info(
+ f"Bedrock availability probe returned {error_code}; proceeding with checks"
+ )
+ if bedrock_unavailable:
+ logger.info(f"Bedrock service not available in region {region}, skipping")
+ all_findings.append(
+ {
+ "check_name": "Bedrock Service Availability",
+ "status": "N/A",
+ "details": f"Bedrock is not available in region {region}",
+ "csv_data": [
+ create_finding(
+ check_id="BR-00",
+ finding_name="Bedrock Service Availability",
+ finding_details=unavailable_detail,
+ resolution="No action required. Bedrock is not deployed in this region.",
+ reference="https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-regions.html",
+ severity="Informational",
+ status="N/A",
+ region=region,
+ )
+ ],
+ }
+ )
+ csv_content = generate_csv_report(all_findings)
+ bucket_name = os.environ.get("AIML_ASSESSMENT_BUCKET_NAME")
+ s3_url = write_to_s3(execution_id, csv_content, bucket_name, region=region)
+ return {
+ "statusCode": 200,
+ "body": {
+ "message": f"Bedrock not available in {region}",
+ "report_url": s3_url,
+ },
+ }
+
+ # Run regional checks using the cached permissions
logger.info("Running Bedrock access and VPC endpoints check")
bedrock_access_vpc_findings = check_bedrock_access_and_vpc_endpoints(
- permission_cache
+ permission_cache, region=region
)
all_findings.append(bedrock_access_vpc_findings)
- # logger.info("Running stale access check")
- # stale_access_findings = check_stale_bedrock_access(permission_cache)
- # all_findings.append(stale_access_findings)
-
- logger.info("Running marketplace subscription access check")
- marketplace_access_findings = check_marketplace_subscription_access(
- permission_cache
- )
- all_findings.append(marketplace_access_findings)
-
logger.info("Running Bedrock logging findings check")
- bedrock_logging_findings = check_bedrock_logging_configuration()
+ bedrock_logging_findings = check_bedrock_logging_configuration(region=region)
all_findings.append(bedrock_logging_findings)
logger.info("Running Bedrock Guardrails check")
- bedrock_guardrails_findings = check_bedrock_guardrails()
+ bedrock_guardrails_findings = check_bedrock_guardrails(region=region)
all_findings.append(bedrock_guardrails_findings)
logger.info("Running Bedrock CloudTrail logging check")
- bedrock_cloudtrail_findings = check_bedrock_cloudtrail_logging()
+ bedrock_cloudtrail_findings = check_bedrock_cloudtrail_logging(region=region)
all_findings.append(bedrock_cloudtrail_findings)
logger.info("Running Bedrock Prompt Management check")
- bedrock_prompt_management_findings = check_bedrock_prompt_management()
+ bedrock_prompt_management_findings = check_bedrock_prompt_management(
+ region=region
+ )
all_findings.append(bedrock_prompt_management_findings)
logger.info("Running Bedrock agent IAM roles check")
- bedrock_agent_roles_findings = check_bedrock_agent_roles(permission_cache)
+ bedrock_agent_roles_findings = check_bedrock_agent_roles(
+ permission_cache, region=region
+ )
all_findings.append(bedrock_agent_roles_findings)
logger.info("Running Bedrock Knowledge Base encryption check")
- kb_encryption_findings = check_bedrock_knowledge_base_encryption()
+ kb_encryption_findings = check_bedrock_knowledge_base_encryption(region=region)
all_findings.append(kb_encryption_findings)
logger.info("Running Bedrock Guardrail IAM enforcement check")
guardrail_iam_findings = check_bedrock_guardrail_iam_enforcement(
- permission_cache
+ permission_cache, region=region
)
all_findings.append(guardrail_iam_findings)
logger.info("Running Bedrock custom model encryption check")
- custom_model_encryption_findings = check_bedrock_custom_model_encryption()
+ custom_model_encryption_findings = check_bedrock_custom_model_encryption(
+ region=region
+ )
all_findings.append(custom_model_encryption_findings)
logger.info("Running Bedrock invocation log encryption check")
- invocation_log_encryption_findings = check_bedrock_invocation_log_encryption()
+ invocation_log_encryption_findings = check_bedrock_invocation_log_encryption(
+ region=region
+ )
all_findings.append(invocation_log_encryption_findings)
logger.info("Running Bedrock Flows guardrails check")
- flows_guardrails_findings = check_bedrock_flows_guardrails()
+ flows_guardrails_findings = check_bedrock_flows_guardrails(region=region)
all_findings.append(flows_guardrails_findings)
# Generate and upload report
@@ -2463,7 +2890,7 @@ def lambda_handler(event, context):
)
logger.info("Writing report to S3")
- s3_url = write_to_s3(execution_id, csv_content, bucket_name)
+ s3_url = write_to_s3(execution_id, csv_content, bucket_name, region=region)
return {
"statusCode": 200,
diff --git a/aiml-security-assessment/functions/security/bedrock_assessments/schema.py b/aiml-security-assessment/functions/security/bedrock_assessments/schema.py
index 6c5a9f5..16a66d9 100644
--- a/aiml-security-assessment/functions/security/bedrock_assessments/schema.py
+++ b/aiml-security-assessment/functions/security/bedrock_assessments/schema.py
@@ -1,6 +1,6 @@
from enum import Enum
-from typing import Dict, Any
-from pydantic import BaseModel, Field, validator
+from typing import Any, Dict
+from pydantic import BaseModel, Field, field_validator
import re
@@ -35,8 +35,12 @@ class Finding(BaseModel):
Reference: str = Field(..., description="Documentation reference URL")
Severity: SeverityEnum = Field(..., description="Severity level of the finding")
Status: StatusEnum = Field(..., description="Current status of the finding")
+ Region: str = Field(
+ default="", description="AWS region where the finding was identified"
+ )
- @validator("Check_ID")
+ @field_validator("Check_ID")
+ @classmethod
def validate_check_id(cls, v):
"""Validate that Check_ID follows the pattern XX-NN (e.g., SM-01, BR-14, AC-05)"""
pattern = r"^[A-Z]{2,3}-\d{2}$"
@@ -46,21 +50,24 @@ def validate_check_id(cls, v):
)
return v
- @validator("Reference")
+ @field_validator("Reference")
+ @classmethod
def validate_reference_url(cls, v):
"""Validate that reference URL starts with https://"""
if not str(v).startswith("https://"):
raise ValueError("Reference URL must start with https://")
return v
- @validator("Severity")
+ @field_validator("Severity")
+ @classmethod
def validate_severity(cls, v):
"""Validate that severity is one of the allowed values"""
if v not in SeverityEnum.__members__.values():
raise ValueError("Severity must be one of the allowed values")
return v
- @validator("Status")
+ @field_validator("Status")
+ @classmethod
def validate_status(cls, v):
"""Validate that status is one of the allowed values"""
if v not in StatusEnum.__members__.values():
@@ -76,6 +83,7 @@ def create_finding(
reference: str,
severity: SeverityEnum,
status: StatusEnum,
+ region: str = "",
) -> Dict[str, Any]:
"""
Create a validated finding object
@@ -88,6 +96,7 @@ def create_finding(
reference: Documentation URL
severity: Severity level
status: Current status
+ region: AWS region where the finding was identified
Returns:
Dict[str, Any]: Validated finding as dictionary
@@ -103,5 +112,6 @@ def create_finding(
Reference=reference,
Severity=severity,
Status=status,
+ Region=region,
)
return dict(finding.model_dump()) # Convert to regular dictionary
diff --git a/aiml-security-assessment/functions/security/finserv_assessments/app.py b/aiml-security-assessment/functions/security/finserv_assessments/app.py
index 936795a..abfcd39 100644
--- a/aiml-security-assessment/functions/security/finserv_assessments/app.py
+++ b/aiml-security-assessment/functions/security/finserv_assessments/app.py
@@ -57,13 +57,18 @@
import json
import logging
import os
+import re
from dataclasses import dataclass
from datetime import datetime, timezone
from io import StringIO
from typing import Any, Dict, List, Optional
from botocore.config import Config
-from botocore.exceptions import ClientError, ParamValidationError
+from botocore.exceptions import (
+ ClientError,
+ EndpointConnectionError,
+ ParamValidationError,
+)
from schema import create_finding
@@ -243,6 +248,10 @@ def _is_missing_bucket_error(err: "ClientError") -> bool:
# could not run (e.g., missing IAM permission). They are visible in the report
# (Status="N/A") so a failed/permission-denied check does not silently vanish.
COULD_NOT_ASSESS_PREFIX = "COULD NOT ASSESS: "
+FINSERV_GUIDE_URL = (
+ "https://d1.awsstatic.com/onedam/marketing-channels/website/public/"
+ "global-FinServ-ComplianceGuide-GenAIRisks-public.pdf"
+)
# ---------------------------------------------------------------------------
# ResourceInventory data model and helpers (REQ-3, REQ-4.1, REQ-6.4)
@@ -735,6 +744,23 @@ def _could_not_assess_row(check_id: str, check_name: str, err: Any) -> Dict[str,
)
+def _no_regional_genai_resources_row(region: str) -> Dict[str, Any]:
+ """Visible N/A row used when a target region has no GenAI resource footprint."""
+ return create_finding(
+ check_id="FS-00",
+ finding_name="FinServ Regional Scope Not Applicable",
+ finding_details=(
+ f"No regional Bedrock, AgentCore, or SageMaker resources were found in {region}; "
+ "FinServ GenAI risk checks were not applied to this region."
+ ),
+ resolution="No action required unless GenAI workloads are expected in this region.",
+ reference=FINSERV_GUIDE_URL,
+ severity="Informational",
+ status="N/A",
+ region=region,
+ )
+
+
# ===========================================================================
# CATEGORY 1: UNBOUNDED CONSUMPTION (FS-01 to FS-06)
# Risk: GenAI workloads can be exploited to exhaust compute/cost budgets
@@ -2101,7 +2127,7 @@ def check_ecr_image_scanning() -> Dict[str, Any]:
#
# NOTE: FS-17 (Model Monitor Data Quality → SM-07), FS-18 (Model Drift Detection → SM-23),
# and FS-19 (Model Registry Approval → SM-22) are merged into upstream checks.
-# See extension notes in SECURITY_CHECKS_FINSERV_PART1_INFRA_CONTROLS.md.
+# See extension notes in SECURITY_CHECKS_FINSERV.md.
# ===========================================================================
@@ -5395,7 +5421,7 @@ def check_foundation_model_lifecycle_policy() -> Dict[str, Any]:
# Mitigations explicitly in the AWS FinServ Guide not covered by FS-01..63
# or the existing BR/SM/AC checks.
# NOTE: FS-64 (Guardrail Trace Logging) is merged into upstream BR-04.
-# See extension note in SECURITY_CHECKS_FINSERV_PART3_APP_LAYER_AND_GAPS.md.
+# See extension note in SECURITY_CHECKS_FINSERV.md.
# ===========================================================================
@@ -6066,6 +6092,7 @@ def generate_csv_report(findings: List[Dict[str, Any]]) -> str:
"Reference",
"Severity",
"Status",
+ "Region",
"Compliance_Frameworks",
]
writer = csv.DictWriter(csv_buffer, fieldnames=fieldnames)
@@ -6076,6 +6103,216 @@ def generate_csv_report(findings: List[Dict[str, Any]]) -> str:
return csv_buffer.getvalue()
+def _normalized_target_regions(value: str) -> List[str]:
+ """Parse the CloudFormation TargetRegions parameter value."""
+ value = (value or "").strip()
+ if not value or value.lower() == "all":
+ return []
+ return [region.strip() for region in re.split(r"[,\s]+", value) if region.strip()]
+
+
+def _get_region_scopes(event: Dict[str, Any]) -> List[str]:
+ """Return resolved target regions without assuming a fixed deployment region."""
+ target_regions = event.get("TargetRegions")
+ if isinstance(target_regions, list):
+ regions = [
+ str(region).strip() for region in target_regions if str(region).strip()
+ ]
+ if regions:
+ return regions
+
+ regions = _normalized_target_regions(os.environ.get("TARGET_REGIONS", ""))
+ if regions:
+ return regions
+
+ fallback_region = event.get("Region") or ""
+ return [fallback_region] if fallback_region else []
+
+
+def _probe_regional_resource_list(probe_label: str, probe_func) -> Optional[bool]:
+ """Return True for resources, False for a successful empty list, None if unknown."""
+ try:
+ result = probe_func()
+ return bool(result) if isinstance(result, list) else None
+ except (EndpointConnectionError, ParamValidationError):
+ logger.info("%s API is not available in this region", probe_label)
+ return False
+ except ClientError as e:
+ error_code = e.response.get("Error", {}).get("Code", "")
+ if _is_access_error(e):
+ logger.warning(
+ "Unable to determine FinServ regional footprint from %s: %s",
+ probe_label,
+ error_code,
+ )
+ return None
+ if error_code in {
+ "UnknownOperationException",
+ "ValidationException",
+ "ResourceNotFoundException",
+ "OptInRequired",
+ "UnauthorizedOperation",
+ }:
+ logger.info(
+ "%s API is not available in this region: %s", probe_label, error_code
+ )
+ return False
+ logger.warning(
+ "Unexpected error probing %s for FinServ regional footprint: %s",
+ probe_label,
+ error_code or str(e),
+ )
+ return None
+ except Exception as e:
+ error_text = str(e)
+ if "Unknown operation" in error_text or "UnknownOperation" in error_text:
+ logger.info("%s API is not available in this region", probe_label)
+ return False
+ logger.warning(
+ "Unexpected error probing %s for FinServ regional footprint: %s",
+ probe_label,
+ error_text,
+ )
+ return None
+
+
+def detect_finserv_regional_footprint(region: str) -> Optional[bool]:
+ """
+ Detect whether a target region has GenAI resources that justify regional
+ FinServ findings.
+
+ Returns:
+ True when Bedrock, AgentCore, or SageMaker resources exist
+ False when supported probes succeed and return no resources
+ None when permissions/API errors prevent a confident decision
+ """
+ bedrock = boto3.client("bedrock", config=boto3_config, region_name=region)
+ bedrock_agent = boto3.client(
+ "bedrock-agent", config=boto3_config, region_name=region
+ )
+ agentcore = boto3.client(
+ "bedrock-agentcore-control", config=boto3_config, region_name=region
+ )
+ sagemaker = boto3.client("sagemaker", config=boto3_config, region_name=region)
+
+ probes = [
+ ("Bedrock Guardrails", lambda: bedrock.list_guardrails().get("guardrails", [])),
+ (
+ "Bedrock Agents",
+ lambda: bedrock_agent.list_agents().get("agentSummaries", []),
+ ),
+ (
+ "Bedrock Knowledge Bases",
+ lambda: bedrock_agent.list_knowledge_bases().get(
+ "knowledgeBaseSummaries", []
+ ),
+ ),
+ (
+ "AgentCore Runtimes",
+ lambda: agentcore.list_agent_runtimes().get("agentRuntimes", []),
+ ),
+ (
+ "SageMaker Endpoints",
+ lambda: sagemaker.list_endpoints(MaxResults=1).get("Endpoints", []),
+ ),
+ (
+ "SageMaker Models",
+ lambda: sagemaker.list_models(MaxResults=1).get("Models", []),
+ ),
+ (
+ "SageMaker Feature Groups",
+ lambda: sagemaker.list_feature_groups(MaxResults=1).get(
+ "FeatureGroupSummaries", []
+ ),
+ ),
+ ]
+
+ indeterminate = False
+ successful_empty_probe = False
+ for probe_label, probe_func in probes:
+ probe_result = _probe_regional_resource_list(probe_label, probe_func)
+ if probe_result is True:
+ return True
+ if probe_result is False:
+ successful_empty_probe = True
+ if probe_result is None:
+ indeterminate = True
+
+ if successful_empty_probe:
+ return False
+ return None if indeterminate else False
+
+
+def _partition_regions_by_finserv_footprint(
+ regions: List[str],
+) -> "tuple[List[str], List[str]]":
+ """Split target regions into regions to assess and regions that are N/A."""
+ assessable_regions = []
+ empty_regions = []
+ for region in regions:
+ footprint_found = detect_finserv_regional_footprint(region)
+ if footprint_found is False:
+ empty_regions.append(region)
+ else:
+ # Unknown footprint keeps the region in scope so access/API problems do
+ # not hide potentially real risks.
+ assessable_regions.append(region)
+ return assessable_regions, empty_regions
+
+
+def _stamp_regions(findings: List[Dict[str, Any]], regions: List[str]) -> None:
+ """Expand missing CSV Region values so each target region is filterable."""
+ regions = [region for region in regions if region]
+ if not regions:
+ return
+
+ for finding in findings:
+ expanded_rows = []
+ for row in finding.get("csv_data", []):
+ if row.get("Region"):
+ expanded_rows.append(row)
+ continue
+ for region in regions:
+ regional_row = dict(row)
+ regional_row["Region"] = region
+ expanded_rows.append(regional_row)
+ finding["csv_data"] = expanded_rows
+
+
+def _append_no_resource_region_findings(
+ findings: List[Dict[str, Any]], regions: List[str]
+) -> None:
+ """Append one informational N/A finding for each region without GenAI resources."""
+ if not regions:
+ return
+ findings.append(
+ {
+ "check_name": "FinServ Regional Resource Scope",
+ "status": "PASS",
+ "details": "No regional GenAI resources found",
+ "csv_data": [
+ _no_regional_genai_resources_row(region) for region in regions if region
+ ],
+ }
+ )
+
+
+def _apply_region_scope(findings: List[Dict[str, Any]], regions: List[str]) -> None:
+ """Scope FinServ rows to target regions with resources and emit N/A rows for empty regions."""
+ if not regions:
+ return
+
+ assessable_regions, empty_regions = _partition_regions_by_finserv_footprint(regions)
+ if assessable_regions:
+ _stamp_regions(findings, assessable_regions)
+ else:
+ for finding in findings:
+ finding["csv_data"] = [
+ row for row in finding.get("csv_data", []) if row.get("Region")
+ ]
+ _append_no_resource_region_findings(findings, empty_regions)
+
+
def write_to_s3(execution_id: str, csv_content: str, bucket_name: str) -> str:
"""Write CSV report to S3 bucket."""
s3_client = boto3.client("s3", config=boto3_config)
@@ -6402,6 +6639,7 @@ def lambda_handler(event, context):
"""
logger.info("Starting FinServ GenAI security assessment")
all_findings = []
+ region_scopes = _get_region_scopes(event)
execution_id = event.get("Execution", {}).get("Name", "local-test")
permission_cache = get_permissions_cache(execution_id) or {
@@ -6432,7 +6670,10 @@ def lambda_handler(event, context):
)
all_findings.append(result)
- # Generate and upload report
+ # Generate and upload report. Only duplicate regional FinServ rows into
+ # target regions where a GenAI footprint is present; empty regions receive a
+ # visible N/A row instead of false failed controls.
+ _apply_region_scope(all_findings, region_scopes)
csv_content = generate_csv_report(all_findings)
bucket_name = os.environ.get("AIML_ASSESSMENT_BUCKET_NAME")
if not bucket_name:
diff --git a/aiml-security-assessment/functions/security/finserv_assessments/schema.py b/aiml-security-assessment/functions/security/finserv_assessments/schema.py
index 657ad66..22a4ed7 100644
--- a/aiml-security-assessment/functions/security/finserv_assessments/schema.py
+++ b/aiml-security-assessment/functions/security/finserv_assessments/schema.py
@@ -40,6 +40,9 @@ class Finding(BaseModel):
Reference: str = Field(..., description="Documentation reference URL")
Severity: SeverityEnum = Field(..., description="Severity level of the finding")
Status: StatusEnum = Field(..., description="Current status of the finding")
+ Region: str = Field(
+ default="", description="AWS region where the finding was identified"
+ )
Compliance_Frameworks: str = Field(
default="",
description=(
@@ -76,6 +79,7 @@ def create_finding(
reference: str,
severity: SeverityEnum,
status: StatusEnum,
+ region: str = "",
compliance_frameworks: Optional[str] = "",
) -> Dict[str, Any]:
"""Create a validated finding dict.
@@ -94,6 +98,7 @@ def create_finding(
Reference=reference,
Severity=severity,
Status=status,
+ Region=region,
Compliance_Frameworks=compliance_frameworks or "",
)
return dict(finding.model_dump())
diff --git a/aiml-security-assessment/functions/security/finserv_tests/test_checks.py b/aiml-security-assessment/functions/security/finserv_tests/test_checks.py
index a5a7f92..bd5fb37 100644
--- a/aiml-security-assessment/functions/security/finserv_tests/test_checks.py
+++ b/aiml-security-assessment/functions/security/finserv_tests/test_checks.py
@@ -3497,6 +3497,101 @@ def test_no_exact_pins_on_aws_sdk(self):
)
+class TestFinservRegionalFootprint:
+ """Regional footprint gating used to avoid false FinServ failures."""
+
+ @staticmethod
+ def _client_factory(clients):
+ def factory(service_name, **kwargs):
+ return clients[service_name]
+
+ return factory
+
+ @patch("finserv_app.boto3.client")
+ def test_detect_returns_true_when_any_genai_resource_exists(self, mock_client):
+ bedrock = MagicMock()
+ bedrock.list_guardrails.return_value = {"guardrails": [{"id": "gr-1"}]}
+ clients = {
+ "bedrock": bedrock,
+ "bedrock-agent": MagicMock(),
+ "bedrock-agentcore-control": MagicMock(),
+ "sagemaker": MagicMock(),
+ }
+ mock_client.side_effect = self._client_factory(clients)
+
+ assert app.detect_finserv_regional_footprint("us-east-1") is True
+ mock_client.assert_any_call(
+ "bedrock", config=app.boto3_config, region_name="us-east-1"
+ )
+
+ @patch("finserv_app.boto3.client")
+ def test_detect_returns_false_when_all_supported_probes_are_empty(
+ self, mock_client
+ ):
+ bedrock = MagicMock()
+ bedrock.list_guardrails.return_value = {"guardrails": []}
+ bedrock_agent = MagicMock()
+ bedrock_agent.list_agents.return_value = {"agentSummaries": []}
+ bedrock_agent.list_knowledge_bases.return_value = {"knowledgeBaseSummaries": []}
+ agentcore = MagicMock()
+ agentcore.list_agent_runtimes.return_value = {"agentRuntimes": []}
+ sagemaker = MagicMock()
+ sagemaker.list_endpoints.return_value = {"Endpoints": []}
+ sagemaker.list_models.return_value = {"Models": []}
+ sagemaker.list_feature_groups.return_value = {"FeatureGroupSummaries": []}
+ mock_client.side_effect = self._client_factory(
+ {
+ "bedrock": bedrock,
+ "bedrock-agent": bedrock_agent,
+ "bedrock-agentcore-control": agentcore,
+ "sagemaker": sagemaker,
+ }
+ )
+
+ assert app.detect_finserv_regional_footprint("us-west-2") is False
+
+ @patch("finserv_app.boto3.client")
+ def test_detect_returns_none_when_footprint_is_indeterminate(self, mock_client):
+ bedrock = MagicMock()
+ bedrock.list_guardrails.side_effect = _client_error("AccessDeniedException")
+ bedrock_agent = MagicMock()
+ bedrock_agent.list_agents.side_effect = _client_error("AccessDeniedException")
+ bedrock_agent.list_knowledge_bases.side_effect = _client_error(
+ "AccessDeniedException"
+ )
+ agentcore = MagicMock()
+ agentcore.list_agent_runtimes.side_effect = _client_error(
+ "AccessDeniedException"
+ )
+ sagemaker = MagicMock()
+ sagemaker.list_endpoints.side_effect = _client_error("AccessDeniedException")
+ sagemaker.list_models.side_effect = _client_error("AccessDeniedException")
+ sagemaker.list_feature_groups.side_effect = _client_error(
+ "AccessDeniedException"
+ )
+ mock_client.side_effect = self._client_factory(
+ {
+ "bedrock": bedrock,
+ "bedrock-agent": bedrock_agent,
+ "bedrock-agentcore-control": agentcore,
+ "sagemaker": sagemaker,
+ }
+ )
+
+ assert app.detect_finserv_regional_footprint("eu-west-1") is None
+
+ @patch("finserv_app.detect_finserv_regional_footprint")
+ def test_partition_keeps_indeterminate_regions_in_scope(self, mock_detect):
+ mock_detect.side_effect = [None, False, True]
+
+ assessable, empty = app._partition_regions_by_finserv_footprint(
+ ["unknown-region", "empty-region", "active-region"]
+ )
+
+ assert assessable == ["unknown-region", "active-region"]
+ assert empty == ["empty-region"]
+
+
class TestGenerateCsvReport:
"""Test CSV report generation."""
@@ -3505,6 +3600,7 @@ def test_empty_findings_produces_header_only(self):
lines = csv_content.strip().split("\n")
assert len(lines) == 1 # header only
assert "Check_ID" in lines[0]
+ assert "Region" in lines[0]
def test_findings_produce_csv_rows(self):
findings = [
@@ -3529,6 +3625,131 @@ def test_findings_produce_csv_rows(self):
assert len(lines) == 2 # header + 1 data row
assert "FS-01" in lines[1]
+ def test_region_scopes_use_configured_target_regions_from_event(self):
+ event = {"Region": "fallback-region", "TargetRegions": ["region-a", "region-b"]}
+
+ assert app._get_region_scopes(event) == ["region-a", "region-b"]
+
+ def test_region_scopes_use_target_regions_env_when_event_list_absent(
+ self, monkeypatch
+ ):
+ monkeypatch.setenv("TARGET_REGIONS", "region-a,region-b")
+
+ assert app._get_region_scopes({"Region": "fallback-region"}) == [
+ "region-a",
+ "region-b",
+ ]
+
+ def test_region_scopes_accept_whitespace_delimited_target_regions(
+ self, monkeypatch
+ ):
+ monkeypatch.setenv("TARGET_REGIONS", "region-a region-b")
+
+ assert app._get_region_scopes({"Region": "fallback-region"}) == [
+ "region-a",
+ "region-b",
+ ]
+
+ def test_stamp_regions_expands_missing_csv_regions(self):
+ findings = [
+ {
+ "check_name": "Test",
+ "status": "PASS",
+ "csv_data": [
+ {
+ "Check_ID": "FS-01",
+ "Finding": "Test Finding",
+ "Finding_Details": "Details",
+ "Resolution": "Fix",
+ "Reference": "https://example.com",
+ "Severity": "High",
+ "Status": "Passed",
+ },
+ {
+ "Check_ID": "FS-02",
+ "Finding": "Already Scoped",
+ "Finding_Details": "Details",
+ "Resolution": "Fix",
+ "Reference": "https://example.com",
+ "Severity": "Medium",
+ "Status": "Failed",
+ "Region": "Global",
+ },
+ ],
+ }
+ ]
+
+ app._stamp_regions(findings, ["region-a", "region-b"])
+
+ regions = [row["Region"] for row in findings[0]["csv_data"]]
+ assert regions == ["region-a", "region-b", "Global"]
+
+ def test_apply_region_scope_does_not_copy_failures_to_empty_regions(self):
+ findings = [
+ {
+ "check_name": "Test",
+ "status": "WARN",
+ "csv_data": [
+ {
+ "Check_ID": "FS-01",
+ "Finding": "Test Failed Finding",
+ "Finding_Details": "Details",
+ "Resolution": "Fix",
+ "Reference": "https://example.com",
+ "Severity": "High",
+ "Status": "Failed",
+ }
+ ],
+ }
+ ]
+
+ with patch(
+ "finserv_app._partition_regions_by_finserv_footprint",
+ return_value=(["region-with-resources"], ["region-without-resources"]),
+ ):
+ app._apply_region_scope(
+ findings, ["region-with-resources", "region-without-resources"]
+ )
+
+ rows = [row for finding in findings for row in finding["csv_data"]]
+ failed_rows = [row for row in rows if row["Status"] == "Failed"]
+ na_rows = [row for row in rows if row["Status"] == "N/A"]
+
+ assert [row["Region"] for row in failed_rows] == ["region-with-resources"]
+ assert [row["Region"] for row in na_rows] == ["region-without-resources"]
+ assert na_rows[0]["Check_ID"] == "FS-00"
+
+ def test_apply_region_scope_suppresses_unscoped_rows_when_all_regions_empty(self):
+ findings = [
+ {
+ "check_name": "Test",
+ "status": "WARN",
+ "csv_data": [
+ {
+ "Check_ID": "FS-01",
+ "Finding": "Test Failed Finding",
+ "Finding_Details": "Details",
+ "Resolution": "Fix",
+ "Reference": "https://example.com",
+ "Severity": "High",
+ "Status": "Failed",
+ }
+ ],
+ }
+ ]
+
+ with patch(
+ "finserv_app._partition_regions_by_finserv_footprint",
+ return_value=([], ["region-a", "region-b"]),
+ ):
+ app._apply_region_scope(findings, ["region-a", "region-b"])
+
+ rows = [row for finding in findings for row in finding["csv_data"]]
+
+ assert {row["Region"] for row in rows} == {"region-a", "region-b"}
+ assert {row["Status"] for row in rows} == {"N/A"}
+ assert {row["Check_ID"] for row in rows} == {"FS-00"}
+
def test_multiple_findings_multiple_rows(self):
findings = [
{
diff --git a/aiml-security-assessment/functions/security/finserv_tests/test_lambda_handler.py b/aiml-security-assessment/functions/security/finserv_tests/test_lambda_handler.py
index bf4558a..264904e 100644
--- a/aiml-security-assessment/functions/security/finserv_tests/test_lambda_handler.py
+++ b/aiml-security-assessment/functions/security/finserv_tests/test_lambda_handler.py
@@ -274,6 +274,56 @@ def test_handler_passes_inventory_to_build_finserv_checks(
"lambda_handler must never pass None as the inventory argument"
)
+ @patch("finserv_app.write_to_s3")
+ @patch("finserv_app.get_permissions_cache")
+ @patch("finserv_app._apply_region_scope")
+ @patch("finserv_app.build_finserv_checks")
+ @patch("finserv_app.collect_resource_inventory")
+ def test_handler_scopes_findings_with_target_regions_from_event(
+ self,
+ mock_collect,
+ mock_build,
+ mock_apply_scope,
+ mock_cache,
+ mock_s3,
+ ):
+ """lambda_handler uses the state-machine TargetRegions list for report scoping."""
+ mock_collect.return_value = object()
+ mock_build.return_value = [
+ (
+ "FS-01",
+ lambda: {
+ "check_name": "Scoped Check",
+ "status": "WARN",
+ "csv_data": [
+ {
+ "Check_ID": "FS-01",
+ "Finding": "Scoped Finding",
+ "Finding_Details": "Details",
+ "Resolution": "Fix",
+ "Reference": "https://example.com",
+ "Severity": "High",
+ "Status": "Failed",
+ }
+ ],
+ },
+ )
+ ]
+ mock_cache.return_value = {"role_permissions": {}, "user_permissions": {}}
+ mock_s3.return_value = "https://bucket.s3.amazonaws.com/report.csv"
+ event = {
+ "Execution": {"Name": "exec-target-regions"},
+ "Region": "fallback-region",
+ "TargetRegions": ["region-a", "region-b"],
+ }
+
+ app.lambda_handler(event, None)
+
+ mock_apply_scope.assert_called_once()
+ findings_arg, regions_arg = mock_apply_scope.call_args.args
+ assert regions_arg == ["region-a", "region-b"]
+ assert findings_arg[0]["check_name"] == "Scoped Check"
+
class TestWriteToS3:
"""Test the write_to_s3 helper."""
diff --git a/aiml-security-assessment/functions/security/finserv_tests/test_schema.py b/aiml-security-assessment/functions/security/finserv_tests/test_schema.py
index 7464790..6aacbea 100644
--- a/aiml-security-assessment/functions/security/finserv_tests/test_schema.py
+++ b/aiml-security-assessment/functions/security/finserv_tests/test_schema.py
@@ -98,10 +98,23 @@ def test_output_has_all_csv_fields(self):
"Reference",
"Severity",
"Status",
+ "Region",
"Compliance_Frameworks",
}
assert set(result.keys()) == expected_keys
+ def test_region_defaults_to_empty_string(self):
+ result = create_finding(
+ check_id="FS-42",
+ finding_name="Name",
+ finding_details="Details",
+ resolution="Resolution",
+ reference="https://example.com",
+ severity="High",
+ status="Failed",
+ )
+ assert result["Region"] == ""
+
@pytest.mark.parametrize(
"check_id",
["FS-01", "FS-69", "BR-14", "SM-07", "AC-05"],
diff --git a/aiml-security-assessment/functions/security/generate_consolidated_report/app.py b/aiml-security-assessment/functions/security/generate_consolidated_report/app.py
index ea707b9..f20fc9c 100644
--- a/aiml-security-assessment/functions/security/generate_consolidated_report/app.py
+++ b/aiml-security-assessment/functions/security/generate_consolidated_report/app.py
@@ -10,6 +10,13 @@
from report_template import generate_html_report as generate_report_from_template
+# Sentinel region label used by the per-service assessments to tag findings that
+# are derived purely from global (IAM) data and run once per execution rather
+# than per region (e.g. BR-01, SM-02, AC-09). It is NOT a real AWS region, so it
+# must be excluded when counting scanned regions for the report's multi-region
+# UI (region filter, "Risk by Region", region count).
+GLOBAL_REGION_LABEL = "Global"
+
boto3_config = Config(
retries=dict(
max_attempts=10, # Maximum number of retries
@@ -56,37 +63,26 @@ def get_assessment_results(execution_id: str, account_id: str = None) -> Dict[st
try:
s3_client = boto3.client("s3", config=boto3_config)
- # List all CSV files with execution ID in filename (bucket root)
+ # List all CSV files with execution ID in filename (bucket root).
+ # Use a paginator: a multi-region scan produces one file per service per
+ # region, so a single list_objects_v2 call (capped at 1000 keys) could
+ # silently truncate and drop regions for large scans.
s3_bucket = os.environ.get("AIML_ASSESSMENT_BUCKET_NAME")
- response = s3_client.list_objects_v2(
- Bucket=s3_bucket, Prefix=f"bedrock_security_report_{execution_id}"
- )
-
- # Also check for SageMaker reports
- sagemaker_response = s3_client.list_objects_v2(
- Bucket=s3_bucket, Prefix=f"sagemaker_security_report_{execution_id}"
- )
-
- # Also check for AgentCore reports
- agentcore_response = s3_client.list_objects_v2(
- Bucket=s3_bucket, Prefix=f"agentcore_security_report_{execution_id}"
- )
+ paginator = s3_client.get_paginator("list_objects_v2")
- # Also check for FinServ reports
- finserv_response = s3_client.list_objects_v2(
- Bucket=s3_bucket, Prefix=f"finserv_security_report_{execution_id}"
- )
+ # One prefix per service; each matches every region's report file.
+ prefixes = [
+ f"bedrock_security_report_{execution_id}",
+ f"sagemaker_security_report_{execution_id}",
+ f"agentcore_security_report_{execution_id}",
+ f"finserv_security_report_{execution_id}",
+ ]
- # Combine all responses
all_objects = []
- if "Contents" in response:
- all_objects.extend(response["Contents"])
- if "Contents" in sagemaker_response:
- all_objects.extend(sagemaker_response["Contents"])
- if "Contents" in agentcore_response:
- all_objects.extend(agentcore_response["Contents"])
- if "Contents" in finserv_response:
- all_objects.extend(finserv_response["Contents"])
+ for prefix in prefixes:
+ for page in paginator.paginate(Bucket=s3_bucket, Prefix=prefix):
+ all_objects.extend(page.get("Contents", []))
+
if not all_objects:
logger.warning(f"No assessment files found for execution {execution_id}")
return {}
@@ -211,11 +207,32 @@ def generate_html_report(assessment_results: Dict[str, Any]) -> str:
"finserv": {"passed": 0, "failed": 0, "na": 0},
}
service_findings = {"bedrock": [], "sagemaker": [], "agentcore": [], "finserv": []}
+ regions = set()
+
+ # Global/IAM findings (Region == "Global", e.g. BR-01, SM-02, AC-09) are
+ # produced once per run by the primary-region Lambda and should land in a
+ # single CSV. Dedup defensively here so the totals and per-region tiles stay
+ # correct even if the same finding ever appears in more than one region's
+ # file (e.g. RegionIndex missing from the event, or a future per-region
+ # write of a global check). The key uniquely identifies a finding within an
+ # account; account is included so a future multi-account merge is unaffected.
+ seen_findings = set()
for service in ["bedrock", "sagemaker", "agentcore", "finserv"]:
if service in assessment_results:
for report_type, findings in assessment_results[service].items():
for finding in findings:
+ dedup_key = (
+ finding.get("Account_ID", ""),
+ service,
+ finding.get("Check_ID", ""),
+ finding.get("Region", ""),
+ finding.get("Finding_Details", ""),
+ )
+ if dedup_key in seen_findings:
+ continue
+ seen_findings.add(dedup_key)
+
finding["_service"] = service
all_findings.append(finding)
service_findings[service].append(finding)
@@ -226,6 +243,11 @@ def generate_html_report(assessment_results: Dict[str, Any]) -> str:
service_stats[service]["failed"] += 1
elif status == "n/a":
service_stats[service]["na"] += 1
+ region = finding.get("Region", "")
+ # "Global" tags IAM-only findings; it is not a scanned region
+ # and must not inflate the region count / multi-region UI.
+ if region and region != GLOBAL_REGION_LABEL and "," not in region:
+ regions.add(region)
account_id = assessment_results.get("account_id", "Unknown")
timestamp = assessment_results.get(
@@ -240,6 +262,7 @@ def generate_html_report(assessment_results: Dict[str, Any]) -> str:
mode="single",
account_id=account_id,
timestamp=timestamp,
+ regions=sorted(regions) if regions else None,
)
except Exception as e:
logger.error(f"Error generating HTML report: {str(e)}", exc_info=True)
@@ -250,6 +273,11 @@ def get_current_utc_date():
return datetime.now(timezone.utc).strftime("%Y/%m/%d")
+def build_single_account_report_key(timestamp: str) -> str:
+ """Build the single-account HTML report object key."""
+ return f"security_assessment_single_account_{timestamp}.html"
+
+
def write_html_to_s3(
html_content: str, s3_bucket: str, execution_id: str, account_id: str = None
) -> Optional[str]:
@@ -269,7 +297,7 @@ def write_html_to_s3(
# Generate the S3 key for local bucket (no account folder needed)
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
- s3_key = f"security_assessment_{timestamp}_{execution_id}.html"
+ s3_key = build_single_account_report_key(timestamp)
# Upload the HTML file
s3_client.put_object(
diff --git a/aiml-security-assessment/functions/security/generate_consolidated_report/report_template.py b/aiml-security-assessment/functions/security/generate_consolidated_report/report_template.py
index 7cb6d41..27cf527 100644
--- a/aiml-security-assessment/functions/security/generate_consolidated_report/report_template.py
+++ b/aiml-security-assessment/functions/security/generate_consolidated_report/report_template.py
@@ -23,6 +23,14 @@
' '
)
+GENAI_LENS_URL = (
+ "https://docs.aws.amazon.com/wellarchitected/latest/generative-ai-lens/"
+ "generative-ai-lens.html"
+)
+FINSERV_GUIDE_URL = (
+ "https://aws.amazon.com/blogs/security/"
+ "introducing-the-updated-aws-user-guide-to-governance-risk-and-compliance-for-responsible-ai-adoption/"
+)
def generate_table_rows(findings: List[Dict], include_data_attrs: bool = True) -> str:
@@ -48,6 +56,7 @@ def generate_table_rows(findings: List[Dict], include_data_attrs: bool = True) -
)
service = finding.get("_service", "bedrock")
account_id = finding.get("account_id", finding.get("Account_ID", ""))
+ region = finding.get("region", finding.get("Region", ""))
check_id = finding.get("check_id", finding.get("Check_ID", ""))
finding_name = finding.get("finding", finding.get("Finding", ""))
details = finding.get("details", finding.get("Finding_Details", ""))
@@ -60,7 +69,7 @@ def generate_table_rows(findings: List[Dict], include_data_attrs: bool = True) -
ref_html = '- '
data_attrs = (
- f'data-service="{service}" data-severity="{severity}" data-status="{status}" data-account="{account_id}"'
+ f'data-service="{service}" data-severity="{severity}" data-status="{status}" data-account="{account_id}" data-region="{region}"'
if include_data_attrs
else ""
)
@@ -72,6 +81,7 @@ def generate_table_rows(findings: List[Dict], include_data_attrs: bool = True) -
row = f"""
{account_id}
+ {region}
{check_id}
{finding_name}
{details}
@@ -85,7 +95,7 @@ def generate_table_rows(findings: List[Dict], include_data_attrs: bool = True) -
return (
"\n".join(rows)
if rows
- else ' No findings to display '
+ else 'No findings to display '
)
@@ -163,6 +173,11 @@ def get_html_template() -> str:
.section-title .service-icon svg {{ border-radius: 8px; }}
.nav-item .count {{ margin-left: auto; font-size: 12px; font-weight: 600; background: var(--surface-2); padding: 2px 8px; border-radius: 10px; }}
.nav-item.active .count {{ background: var(--accent); color: #fff; }}
+ .nav-section.industry-nav {{ border-top: 1px solid var(--border); padding-top: 16px; margin-top: -8px; }}
+ .industry-nav .nav-item {{ background: var(--accent-soft); color: var(--text); box-shadow: inset 3px 0 0 var(--accent); }}
+ .industry-nav .nav-item:hover {{ background: var(--accent-soft); color: var(--accent); }}
+ .industry-nav .nav-item.active {{ background: var(--accent-soft); color: var(--accent); }}
+ .industry-nav .nav-item .count {{ background: var(--accent); color: #fff; }}
.sidebar-footer {{ margin-top: auto; padding: 16px 20px; border-top: 1px solid var(--border); font-size: 12px; color: var(--text-3); }}
.sidebar-footer a {{ color: var(--accent); text-decoration: none; }}
.main {{ padding: 32px 40px; max-width: 1400px; }}
@@ -179,6 +194,11 @@ def get_html_template() -> str:
.metric.highlight .metric-value {{ color: var(--success); }}
.metric.danger .metric-value {{ color: var(--danger); }}
.metric.warning .metric-value {{ color: var(--warning); }}
+ .scope-industry {{ margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); }}
+ .scope-industry-label {{ font-size: 11px; font-weight: 600; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }}
+ .scope-chip-row {{ display: flex; gap: 12px; flex-wrap: wrap; }}
+ .scope-chip {{ display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--surface-2); border-radius: 6px; }}
+ .scope-chip.industry-chip {{ background: var(--accent-soft); border: 1px solid var(--accent); }}
.card {{ background: var(--surface); border: 2px solid var(--border); border-radius: 12px; margin-bottom: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }}
.card-header {{ padding: 16px 20px; border-bottom: 2px solid var(--border); display: flex; justify-content: space-between; align-items: center; background: var(--surface-2); }}
.card-header h3 {{ font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 10px; }}
@@ -195,15 +215,16 @@ def get_html_template() -> str:
.alert-domain {{ font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }}
.alert-category {{ font-size: 12px; color: var(--text-2); margin-top: 2px; }}
.table-wrap {{ overflow-x: auto; max-height: 900px; overflow-y: auto; }}
- table {{ width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; min-width: 900px; }}
- table th:nth-child(1) {{ width: 11%; }}
- table th:nth-child(2) {{ width: 7%; }}
- table th:nth-child(3) {{ width: 13%; }}
- table th:nth-child(4) {{ width: 20%; }}
- table th:nth-child(5) {{ width: 20%; }}
- table th:nth-child(6) {{ width: 7%; }}
- table th:nth-child(7) {{ width: 10%; }}
- table th:nth-child(8) {{ width: 10%; }}
+ table {{ width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; min-width: 1000px; }}
+ table th:nth-child(1) {{ width: 10%; }}
+ table th:nth-child(2) {{ width: 9%; }}
+ table th:nth-child(3) {{ width: 6%; }}
+ table th:nth-child(4) {{ width: 12%; }}
+ table th:nth-child(5) {{ width: 18%; }}
+ table th:nth-child(6) {{ width: 18%; }}
+ table th:nth-child(7) {{ width: 6%; }}
+ table th:nth-child(8) {{ width: 9%; }}
+ table th:nth-child(9) {{ width: 9%; }}
th {{ text-align: left; padding: 14px 16px; font-weight: 700; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text); background: var(--surface-2); border-bottom: 3px solid var(--accent); white-space: nowrap; position: sticky; top: 0; }}
th.sortable {{ cursor: pointer; user-select: none; transition: background 0.15s; }}
th.sortable:hover {{ background: var(--border); }}
@@ -293,14 +314,14 @@ def get_html_template() -> str:
- AgentCore
- {agentcore_total}
-
- {finserv_nav}
-
-
-
Security Checks
{security_checks}
Evaluated per account
+
Security Checks
{security_checks}
{security_checks_sub}
Total Findings
{total_findings}
{findings_sub}
Actionable Findings
{actionable_findings}
High, Medium, and Low severity
High Severity
{high_passed}/{high_count}
{high_pass_rate}% passed · Immediate action required
@@ -342,12 +363,13 @@ def get_html_template() -> str:
Search
{account_filter}
-
Service All Services Bedrock SageMaker AgentCore {finserv_filter_option}
+ {region_filter}
+
Assessment Area All Assessment Areas Bedrock SageMaker AgentCore {finserv_filter_option}
Severity All Severities High Medium Low Informational
Status All Statuses Failed Passed N/A
Reset
-
Account ID Check ID Finding Details Resolution Reference Severity Status {all_rows}
+
Account ID Region Check ID Finding Details Resolution Reference Severity Status {all_rows}
@@ -359,6 +381,7 @@ def get_html_template() -> str:
Overall
{pass_rate}%
{passed_count} of {actionable_findings} actionable checks
{account_risk_section}
+ {region_risk_section}
Findings by Service
{bedrock_total}
{bedrock_failed} Failed · {bedrock_passed} Passed
@@ -372,33 +395,36 @@ def get_html_template() -> str:
Search
{bedrock_account_filter}
+ {bedrock_region_filter}
Severity All Severities High Medium Low Informational
Status All Statuses Failed Passed N/A
Reset
-
Account ID Check ID Finding Details Resolution Reference Severity Status {bedrock_rows}
+
Account ID Region Check ID Finding Details Resolution Reference Severity Status {bedrock_rows}
Amazon SageMaker Findings
Search
{sagemaker_account_filter}
+ {sagemaker_region_filter}
Severity All Severities High Medium Low Informational
Status All Statuses Failed Passed N/A
Reset
- Account ID Check ID Finding Details Resolution Reference Severity Status {sagemaker_rows}
+ Account ID Region Check ID Finding Details Resolution Reference Severity Status {sagemaker_rows}
Amazon Bedrock AgentCore Findings
Search
{agentcore_account_filter}
+ {agentcore_region_filter}
Severity All Severities High Medium Low Informational
Status All Statuses Failed Passed N/A
Reset
- Account ID Check ID Finding Details Resolution Reference Severity Status {agentcore_rows}
+ Account ID Region Check ID Finding Details Resolution Reference Severity Status {agentcore_rows}
{finserv_section}
@@ -435,6 +461,7 @@ def get_html_template() -> str:
function applyFilters() {{
const searchText = document.getElementById('searchInput').value.toLowerCase();
const accountFilter = document.getElementById('accountFilter')?.value.toLowerCase() || '';
+ const regionFilter = document.getElementById('regionFilter')?.value.toLowerCase() || '';
const serviceFilter = document.getElementById('serviceFilter').value.toLowerCase();
const severityFilter = document.getElementById('severityFilter').value.toLowerCase();
const statusFilter = document.getElementById('statusFilter').value.toLowerCase();
@@ -442,12 +469,14 @@ def get_html_template() -> str:
rows.forEach(row => {{
const rowText = row.textContent.toLowerCase();
const rowAccount = row.dataset.account || '';
+ const rowRegion = row.dataset.region || '';
const rowService = row.dataset.service || '';
const rowSeverity = row.dataset.severity || '';
const rowStatus = row.dataset.status || '';
let show = true;
if (searchText && !rowText.includes(searchText)) show = false;
if (accountFilter && rowAccount !== accountFilter) show = false;
+ if (regionFilter && rowRegion.toLowerCase() !== regionFilter) show = false;
if (serviceFilter && rowService !== serviceFilter) show = false;
if (severityFilter && rowSeverity !== severityFilter) show = false;
if (statusFilter && rowStatus !== statusFilter) show = false;
@@ -457,6 +486,7 @@ def get_html_template() -> str:
document.getElementById('resetFilters').addEventListener('click', function() {{
document.getElementById('searchInput').value = '';
if (document.getElementById('accountFilter')) document.getElementById('accountFilter').value = '';
+ if (document.getElementById('regionFilter')) document.getElementById('regionFilter').value = '';
document.getElementById('serviceFilter').value = '';
document.getElementById('severityFilter').value = '';
document.getElementById('statusFilter').value = '';
@@ -464,6 +494,7 @@ def get_html_template() -> str:
}});
document.getElementById('searchInput').addEventListener('input', applyFilters);
if (document.getElementById('accountFilter')) document.getElementById('accountFilter').addEventListener('change', applyFilters);
+ if (document.getElementById('regionFilter')) document.getElementById('regionFilter').addEventListener('change', applyFilters);
document.getElementById('serviceFilter').addEventListener('change', applyFilters);
document.getElementById('severityFilter').addEventListener('change', applyFilters);
document.getElementById('statusFilter').addEventListener('change', applyFilters);
@@ -504,9 +535,13 @@ def get_html_template() -> str:
aVal = a.dataset.account || '';
bVal = b.dataset.account || '';
break;
+ case 'region':
+ aVal = a.dataset.region || '';
+ bVal = b.dataset.region || '';
+ break;
case 'checkId':
- aVal = a.querySelector('td:nth-child(2) code')?.textContent || '';
- bVal = b.querySelector('td:nth-child(2) code')?.textContent || '';
+ aVal = a.querySelector('td:nth-child(3) code')?.textContent || '';
+ bVal = b.querySelector('td:nth-child(3) code')?.textContent || '';
break;
case 'finding':
aVal = a.querySelector('.col-domain')?.textContent.toLowerCase() || '';
@@ -529,28 +564,32 @@ def get_html_template() -> str:
}});
}});
// Service-specific filter functions
- function createServiceFilter(tableId, searchId, accountId, severityId, statusId, resetId) {{
+ function createServiceFilter(tableId, searchId, accountId, regionId, severityId, statusId, resetId) {{
const table = document.getElementById(tableId);
if (!table) return;
const searchInput = document.getElementById(searchId);
const accountFilter = document.getElementById(accountId);
+ const regionFilter = document.getElementById(regionId);
const severityFilter = document.getElementById(severityId);
const statusFilter = document.getElementById(statusId);
const resetBtn = document.getElementById(resetId);
function applyServiceFilters() {{
const searchText = searchInput?.value.toLowerCase() || '';
const accountValue = accountFilter?.value.toLowerCase() || '';
+ const regionValue = regionFilter?.value.toLowerCase() || '';
const severityValue = severityFilter?.value.toLowerCase() || '';
const statusValue = statusFilter?.value.toLowerCase() || '';
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {{
const rowText = row.textContent.toLowerCase();
const rowAccount = row.dataset.account || '';
+ const rowRegion = row.dataset.region || '';
const rowSeverity = row.dataset.severity || '';
const rowStatus = row.dataset.status || '';
let show = true;
if (searchText && !rowText.includes(searchText)) show = false;
if (accountValue && rowAccount !== accountValue) show = false;
+ if (regionValue && rowRegion.toLowerCase() !== regionValue) show = false;
if (severityValue && rowSeverity !== severityValue) show = false;
if (statusValue && rowStatus !== statusValue) show = false;
row.style.display = show ? '' : 'none';
@@ -558,20 +597,22 @@ def get_html_template() -> str:
}}
searchInput?.addEventListener('input', applyServiceFilters);
accountFilter?.addEventListener('change', applyServiceFilters);
+ regionFilter?.addEventListener('change', applyServiceFilters);
severityFilter?.addEventListener('change', applyServiceFilters);
statusFilter?.addEventListener('change', applyServiceFilters);
resetBtn?.addEventListener('click', function() {{
if (searchInput) searchInput.value = '';
if (accountFilter) accountFilter.value = '';
+ if (regionFilter) regionFilter.value = '';
if (severityFilter) severityFilter.value = '';
if (statusFilter) statusFilter.value = '';
applyServiceFilters();
}});
}}
- createServiceFilter('bedrockTable', 'bedrockSearchInput', 'bedrockAccountFilter', 'bedrockSeverityFilter', 'bedrockStatusFilter', 'bedrockResetFilters');
- createServiceFilter('sagemakerTable', 'sagemakerSearchInput', 'sagemakerAccountFilter', 'sagemakerSeverityFilter', 'sagemakerStatusFilter', 'sagemakerResetFilters');
- createServiceFilter('agentcoreTable', 'agentcoreSearchInput', 'agentcoreAccountFilter', 'agentcoreSeverityFilter', 'agentcoreStatusFilter', 'agentcoreResetFilters');
- createServiceFilter('finservTable', 'finservSearchInput', 'finservAccountFilter', 'finservSeverityFilter', 'finservStatusFilter', 'finservResetFilters');
+ createServiceFilter('bedrockTable', 'bedrockSearchInput', 'bedrockAccountFilter', 'bedrockRegionFilter', 'bedrockSeverityFilter', 'bedrockStatusFilter', 'bedrockResetFilters');
+ createServiceFilter('sagemakerTable', 'sagemakerSearchInput', 'sagemakerAccountFilter', 'sagemakerRegionFilter', 'sagemakerSeverityFilter', 'sagemakerStatusFilter', 'sagemakerResetFilters');
+ createServiceFilter('agentcoreTable', 'agentcoreSearchInput', 'agentcoreAccountFilter', 'agentcoreRegionFilter', 'agentcoreSeverityFilter', 'agentcoreStatusFilter', 'agentcoreResetFilters');
+ createServiceFilter('finservTable', 'finservSearchInput', 'finservAccountFilter', 'finservRegionFilter', 'finservSeverityFilter', 'finservStatusFilter', 'finservResetFilters');
// Apply initial filters for main table
applyFilters();
@@ -588,6 +629,7 @@ def generate_html_report(
account_id: Optional[str] = None,
account_ids: Optional[List[str]] = None,
timestamp: Optional[str] = None,
+ regions: list = None,
) -> str:
"""
Generate HTML report from findings data.
@@ -600,6 +642,7 @@ def generate_html_report(
account_id: Account ID (for single-account mode)
account_ids: List of account IDs (for multi-account mode)
timestamp: Optional timestamp string
+ regions: Optional list of region strings for multi-region filtering
Returns:
Complete HTML report string
@@ -742,14 +785,46 @@ def generate_html_report(
service_findings.get("finserv", []), include_data_attrs=True
)
+ # Build region filter HTML (shared across modes, only shown when multiple regions).
+ # "Global" tags IAM-only findings and is intentionally excluded from `regions`
+ # (it must not inflate the region count / tiles), but those findings still appear
+ # in the tables, so surface a "Global" filter option when any are present.
+ has_global = any(
+ (f.get("region") or f.get("Region")) == "Global" for f in all_findings
+ )
+ # Show the filter when there is more than one distinct value to choose from:
+ # multiple scanned regions, or a single scanned region alongside Global findings.
+ num_real_regions = len(regions) if regions else 0
+ if num_real_regions + (1 if has_global else 0) > 1:
+ region_options = "".join(
+ [f'{r} ' for r in sorted(regions or [])]
+ )
+ if has_global:
+ region_options += 'Global '
+ region_filter = f'Region All Regions {region_options}
'
+ bedrock_region_filter = f'Region All Regions {region_options}
'
+ sagemaker_region_filter = f'Region All Regions {region_options}
'
+ agentcore_region_filter = f'Region All Regions {region_options}
'
+ else:
+ region_filter = ""
+ bedrock_region_filter = ""
+ sagemaker_region_filter = ""
+ agentcore_region_filter = ""
+
# Mode-specific content
num_accounts = len(account_ids) if account_ids else 1
+ num_regions = len(regions) if regions else 1
if mode == "multi":
title = "Multi-Account AI/ML Security Assessment Report"
sidebar_subtitle = "Multi-Account Assessment"
account_info = f"Accounts: {num_accounts}"
header_account_info = f"{num_accounts} Accounts"
- findings_sub = f"Across {num_accounts} accounts"
+ if num_regions > 1:
+ findings_sub = f"Across {num_accounts} accounts · {num_regions} regions"
+ security_checks_sub = f"Evaluated across {num_regions} regions"
+ else:
+ findings_sub = f"Across {num_accounts} accounts"
+ security_checks_sub = "Evaluated per account"
account_options = "".join(
[
f'{acc} '
@@ -809,8 +884,16 @@ def generate_html_report(
title = "AI/ML Security Assessment Report"
sidebar_subtitle = "Assessment Report"
account_info = f"Account: {account_id or 'Unknown'}"
- header_account_info = f"Account: {account_id or 'Unknown'}"
- findings_sub = "Across 1 account"
+ if num_regions > 1:
+ header_account_info = (
+ f"Account: {account_id or 'Unknown'} · {num_regions} Regions"
+ )
+ findings_sub = f"Across {num_regions} regions"
+ security_checks_sub = f"Evaluated across {num_regions} regions"
+ else:
+ header_account_info = f"Account: {account_id or 'Unknown'}"
+ findings_sub = "Across 1 account"
+ security_checks_sub = "Evaluated per account"
account_filter = ""
bedrock_account_filter = ""
sagemaker_account_filter = ""
@@ -818,7 +901,51 @@ def generate_html_report(
finserv_account_filter = ""
account_risk_section = ""
- # FinServ (FS-*) — first-class service, rendered only when findings exist
+ # Build region risk section (shown when multiple regions)
+ if regions and len(regions) > 1:
+ region_metrics_html = ""
+ for reg in sorted(regions):
+ reg_findings = [
+ f for f in all_findings if f.get("region", f.get("Region", "")) == reg
+ ]
+ reg_high = sum(
+ 1
+ for f in reg_findings
+ if f.get("severity", f.get("Severity", "")).lower() == "high"
+ and f.get("status", f.get("Status", "")).lower() == "failed"
+ )
+ reg_medium = sum(
+ 1
+ for f in reg_findings
+ if f.get("severity", f.get("Severity", "")).lower() == "medium"
+ and f.get("status", f.get("Status", "")).lower() == "failed"
+ )
+ reg_low = sum(
+ 1
+ for f in reg_findings
+ if f.get("severity", f.get("Severity", "")).lower() == "low"
+ and f.get("status", f.get("Status", "")).lower() == "failed"
+ )
+ reg_total_failed = reg_high + reg_medium + reg_low
+
+ if reg_high > 0:
+ risk_class = "danger"
+ border_color = "var(--danger)"
+ elif reg_medium > 0:
+ risk_class = "warning"
+ border_color = "var(--warning)"
+ else:
+ risk_class = ""
+ border_color = "var(--success)"
+
+ region_metrics_html += f"""{reg}
{reg_total_failed}
{reg_high} High · {reg_medium} Med · {reg_low} Low
"""
+
+ region_risk_section = f"""Risk by Region
+ {region_metrics_html}
"""
+ else:
+ region_risk_section = ""
+
+ # FinServ (FS-*) — first-class industry assessment, rendered only when findings exist
# (so non-FinServ accounts and EnableFinServAssessment=false deploys stay clean).
finserv_total = (
service_stats.get("finserv", {}).get("passed", 0)
@@ -828,55 +955,96 @@ def generate_html_report(
finserv_failed = service_stats.get("finserv", {}).get("failed", 0)
finserv_passed = service_stats.get("finserv", {}).get("passed", 0)
if finserv_total > 0:
+ finserv_regions = sorted(
+ {
+ f.get("region", f.get("Region", ""))
+ for f in service_findings.get("finserv", [])
+ if f.get("region", f.get("Region", ""))
+ }
+ )
+ finserv_region_options = "".join(
+ [f'{r} ' for r in finserv_regions]
+ )
+ finserv_region_filter = (
+ 'Region '
+ ''
+ f'All Regions {finserv_region_options}
'
+ if finserv_regions
+ else ""
+ )
finserv_nav = (
- ''
+ ' '
+ FINSERV_ICON
+ " Financial Services"
+ f'{finserv_total} '
)
+ industry_nav = (
+ 'By Industry '
+ + finserv_nav
+ + " "
+ )
finserv_filter_option = 'Financial Services '
finserv_service_card = (
''
+ FINSERV_ICON_SMALL
- + f' FinServ
{finserv_total}
'
+ + f' Financial Services Risk
{finserv_total}
'
+ f'{finserv_failed} Failed \u00b7 {finserv_passed} Passed
'
)
+ finserv_scope_industry_block = (
+ ''
+ '
Industry
'
+ '
'
+ + FINSERV_ICON_SMALL
+ + 'Financial Services GenAI Risk
'
+ )
+ finserv_scope_source = (
+ f" Financial Services GenAI Risk checks are based on "
+ f'the AWS User Guide to Governance, Risk, and Compliance for Responsible AI Adoption .'
+ )
finserv_section = (
''
''
+ FINSERV_ICON
+ "Financial Services GenAI Risk Findings
"
- 'Scope: this assessment evaluates the deployment Region only — run it per Region for multi-Region GenAI workloads. Severities follow a documented Likelihood × Impact methodology (see docs).
'
+ '"
''
'
Search
'
+ finserv_account_filter
+ + finserv_region_filter
+ '
Severity All Severities High Medium Low Informational
'
'
Status All Statuses Failed Passed N/A
'
'
Reset'
"
"
- 'Account ID Check ID Finding Details Resolution Reference Severity Status '
+ 'Account ID Region Check ID Finding Details Resolution Reference Severity Status '
+ finserv_rows
+ "
"
""
)
else:
finserv_nav = ""
+ industry_nav = ""
finserv_filter_option = ""
finserv_service_card = ""
+ finserv_scope_industry_block = ""
+ finserv_scope_source = ""
finserv_section = ""
# Fill template
html_template = get_html_template()
- return html_template.format(
+ rendered_html = html_template.format(
title=title,
sidebar_subtitle=sidebar_subtitle,
account_info=account_info,
header_account_info=header_account_info,
account_filter=account_filter,
+ region_filter=region_filter,
timestamp=timestamp,
date_display=date_display,
security_checks=security_checks,
+ security_checks_sub=security_checks_sub,
total_findings=total_findings,
findings_sub=findings_sub,
actionable_findings=actionable_findings,
@@ -915,9 +1083,37 @@ def generate_html_report(
bedrock_account_filter=bedrock_account_filter,
sagemaker_account_filter=sagemaker_account_filter,
agentcore_account_filter=agentcore_account_filter,
- finserv_nav=finserv_nav,
+ bedrock_region_filter=bedrock_region_filter,
+ sagemaker_region_filter=sagemaker_region_filter,
+ agentcore_region_filter=agentcore_region_filter,
+ industry_nav=industry_nav,
finserv_filter_option=finserv_filter_option,
finserv_service_card=finserv_service_card,
finserv_section=finserv_section,
account_risk_section=account_risk_section,
+ region_risk_section=region_risk_section,
+ )
+ base_scope_source = (
+ f"Bedrock, SageMaker, and AgentCore checks are based on the "
+ f'AWS Well-Architected Framework Generative AI Lens .'
)
+ rendered_html = rendered_html.replace(
+ "Based on AWS Well-Architected Framework (Generative AI Lens) and service-specific security documentation.",
+ base_scope_source,
+ 1,
+ )
+ if finserv_scope_industry_block:
+ rendered_html = rendered_html.replace(
+ 'Amazon Bedrock AgentCore Amazon Bedrock AgentCore'
+ + ""
+ + finserv_scope_industry_block
+ + "
Finding:
+ region: str = "",
+) -> dict:
"""
Create a validated finding object
@@ -70,9 +73,10 @@ def create_finding(
reference: Documentation URL
severity: Severity level
status: Current status
+ region: AWS region where the finding was identified
Returns:
- Finding: Validated finding object
+ Dict: Validated finding as dictionary
Raises:
ValidationError: If any field fails validation
@@ -84,5 +88,6 @@ def create_finding(
Reference=reference,
Severity=severity,
Status=status,
+ Region=region,
)
return dict(finding.model_dump()) # Convert to regular dictionary
diff --git a/aiml-security-assessment/functions/security/generate_consolidated_report/test_generate_report.py b/aiml-security-assessment/functions/security/generate_consolidated_report/test_generate_report.py
index b92a6ea..4958bb0 100644
--- a/aiml-security-assessment/functions/security/generate_consolidated_report/test_generate_report.py
+++ b/aiml-security-assessment/functions/security/generate_consolidated_report/test_generate_report.py
@@ -171,6 +171,8 @@ def test_generate_viewable_report(self):
self.assertIn("sidebar", content)
self.assertIn("service-icon", content)
self.assertIn("theme-toggle", content)
+ self.assertIn("Assessment Area", content)
+ self.assertIn("All Assessment Areas", content)
# Verify new features from consolidation
self.assertIn("Methodology", content)
@@ -214,16 +216,29 @@ def test_generate_multi_account_report(self):
"status": "Passed",
"_service": "agentcore",
},
+ {
+ "account_id": "444455556666",
+ "check_id": "FS-01",
+ "finding": "FinServ Regional Scope Not Applicable",
+ "details": "No regional AI/ML resources found.",
+ "resolution": "No action required.",
+ "reference": "https://example.com",
+ "severity": "Informational",
+ "status": "N/A",
+ "_service": "finserv",
+ },
]
service_findings = {
"bedrock": [all_findings[0]],
"sagemaker": [all_findings[1]],
"agentcore": [all_findings[2]],
+ "finserv": [all_findings[3]],
}
service_stats = {
"bedrock": {"passed": 0, "failed": 1},
"sagemaker": {"passed": 0, "failed": 1},
"agentcore": {"passed": 1, "failed": 0},
+ "finserv": {"passed": 0, "failed": 0, "na": 1},
}
html_content = generate_report_direct(
@@ -250,6 +265,14 @@ def test_generate_multi_account_report(self):
self.assertIn("accountFilter", content)
self.assertIn("111122223333", content)
self.assertIn("444455556666", content)
+ self.assertIn("
By Industry ", content)
+ self.assertIn('class="nav-section industry-nav"', content)
+ self.assertIn("Financial Services Risk", content)
+ self.assertIn('class="scope-industry"', content)
+ by_service_nav = content.split("By Service ", 1)[1].split(
+ "By Industry ", 1
+ )[0]
+ self.assertNotIn("Financial Services", by_service_nav)
def test_missing_data_fields(self):
"""Test handling of assessment results with missing fields"""
@@ -308,6 +331,7 @@ def test_finserv_renders_when_present(self):
"Reference": "https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html",
"Severity": "Medium",
"Status": "Failed",
+ "Region": "region-a",
},
{
"Account_ID": "123456789012",
@@ -318,6 +342,7 @@ def test_finserv_renders_when_present(self):
"Reference": "https://docs.aws.amazon.com/macie/latest/user/what-is-macie.html",
"Severity": "High",
"Status": "Passed",
+ "Region": "region-b",
},
]
}
@@ -327,12 +352,47 @@ def test_finserv_renders_when_present(self):
self.assertIn('', html)
self.assertIn("FS-01", html)
self.assertIn('data-service="finserv"', html)
+ self.assertIn('id="finservRegionFilter"', html)
+ self.assertIn(' region-a ', html)
+ self.assertIn('region-b ', html)
+ self.assertIn('data-scope-service="finserv"', html)
+ self.assertIn('class="scope-industry"', html)
+ self.assertIn('class="scope-chip industry-chip"', html)
+ self.assertIn('class="nav-section industry-nav"', html)
+ self.assertIn("Financial Services Risk", html)
+ self.assertIn("Assessment Area", html)
+ self.assertIn("All Assessment Areas", html)
+ self.assertIn(
+ "wellarchitected/latest/generative-ai-lens/generative-ai-lens.html", html
+ )
+ self.assertIn(
+ "introducing-the-updated-aws-user-guide-to-governance-risk-and-compliance-for-responsible-ai-adoption",
+ html,
+ )
+ self.assertNotIn("global-FinServ-ComplianceGuide-GenAIRisks-public.pdf", html)
+ self.assertIn("By Industry ", html)
+ by_service_nav = html.split("By Service ", 1)[1].split(
+ "By Industry ", 1
+ )[0]
+ self.assertNotIn("Financial Services", by_service_nav)
def test_finserv_omitted_when_absent(self):
"""REQ-1/REQ-7: with no FinServ data the FinServ section is omitted cleanly."""
html = generate_html_report(self.test_assessment_results)
self.assertNotIn('id="finserv"', html)
+ self.assertNotIn("By Industry ", html)
self.assertNotIn('', html)
+ self.assertNotIn('data-scope-service="finserv"', html)
+ self.assertNotIn('class="scope-industry"', html)
+ self.assertNotIn("Financial Services Risk", html)
+ self.assertIn(
+ "wellarchitected/latest/generative-ai-lens/generative-ai-lens.html", html
+ )
+ self.assertNotIn(
+ "introducing-the-updated-aws-user-guide-to-governance-risk-and-compliance-for-responsible-ai-adoption",
+ html,
+ )
+ self.assertNotIn("global-FinServ-ComplianceGuide-GenAIRisks-public.pdf", html)
# Other services still render (regression check).
self.assertIn('id="bedrock"', html)
diff --git a/aiml-security-assessment/functions/security/resolve_regions/app.py b/aiml-security-assessment/functions/security/resolve_regions/app.py
new file mode 100644
index 0000000..4dd9297
--- /dev/null
+++ b/aiml-security-assessment/functions/security/resolve_regions/app.py
@@ -0,0 +1,64 @@
+"""
+Resolve Target Regions Lambda Function
+
+Resolves the list of AWS regions to scan based on the TARGET_REGIONS
+environment variable. Returns a list for the Step Functions Map state
+to iterate over.
+"""
+
+import os
+import logging
+import re
+import boto3
+
+logger = logging.getLogger()
+logger.setLevel(logging.INFO)
+
+BEDROCK_SERVICE = "bedrock"
+SAGEMAKER_SERVICE = "sagemaker"
+AGENTCORE_SERVICE = "bedrock-agentcore-control"
+
+SERVICES = [BEDROCK_SERVICE, SAGEMAKER_SERVICE, AGENTCORE_SERVICE]
+
+
+def get_available_regions():
+ """Get the union of all regions where assessed services are available."""
+ session = boto3.Session()
+ all_regions = set()
+ for service in SERVICES:
+ try:
+ regions = session.get_available_regions(service)
+ all_regions.update(regions)
+ except Exception as e:
+ logger.warning(f"Could not get regions for {service}: {e}")
+ return sorted(all_regions)
+
+
+def resolve_regions():
+ """Resolve target regions from environment variable."""
+ target_regions = os.environ.get("TARGET_REGIONS", "").strip()
+ current_region = os.environ.get(
+ "AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
+ )
+
+ if not target_regions:
+ return [current_region]
+
+ if target_regions.lower() == "all":
+ regions = get_available_regions()
+ if not regions:
+ logger.warning("No regions discovered, falling back to current region")
+ return [current_region]
+ return regions
+
+ return [r.strip() for r in re.split(r"[,\s]+", target_regions) if r.strip()]
+
+
+def lambda_handler(event, context):
+ """Main Lambda handler. Returns region list for Map state."""
+ logger.info(f"Event: {event}")
+
+ regions = resolve_regions()
+ logger.info(f"Resolved {len(regions)} target regions: {regions}")
+
+ return {"regions": regions}
diff --git a/aiml-security-assessment/functions/security/sagemaker_assessments/app.py b/aiml-security-assessment/functions/security/sagemaker_assessments/app.py
index 71b8909..2946b20 100644
--- a/aiml-security-assessment/functions/security/sagemaker_assessments/app.py
+++ b/aiml-security-assessment/functions/security/sagemaker_assessments/app.py
@@ -7,7 +7,7 @@
from typing import Dict, List, Any, Optional
from io import StringIO
from botocore.config import Config
-from botocore.exceptions import ClientError
+from botocore.exceptions import ClientError, EndpointConnectionError
import random
import json
@@ -26,6 +26,23 @@
logger = logging.getLogger()
logger.setLevel(logging.ERROR)
+# IAM is a global service. Findings derived purely from the IAM permission cache
+# (e.g. the SM-02 full-access and stale-access checks) are identical across
+# regions, so they are produced only on the primary region (Map index 0) and
+# tagged with this region label to avoid duplicate findings when scanning
+# multiple regions.
+GLOBAL_REGION_LABEL = "Global"
+
+# Error codes returned when a region exists but is not enabled/usable for the
+# account (opt-in regions, disabled regions). The availability probe treats
+# these the same as an endpoint connection failure.
+REGION_UNAVAILABLE_ERROR_CODES = {
+ "UnrecognizedClientException",
+ "InvalidClientTokenId",
+ "AuthFailure",
+ "OptInRequired",
+}
+
def get_permissions_cache(execution_id: str) -> Optional[Dict[str, Any]]:
"""
@@ -80,7 +97,7 @@ def get_permissions_cache(execution_id: str) -> Optional[Dict[str, Any]]:
return None
-def check_sagemaker_internet_access() -> Dict[str, Any]:
+def check_sagemaker_internet_access(region: str = "") -> Dict[str, Any]:
"""
Check if SageMaker notebook instances and domains have direct internet access
"""
@@ -93,7 +110,9 @@ def check_sagemaker_internet_access() -> Dict[str, Any]:
total_resources_checked = 0
# Create SageMaker client
- sagemaker_client = boto3.client("sagemaker")
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
# Check Notebook Instances
try:
@@ -167,6 +186,7 @@ def check_sagemaker_internet_access() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/infrastructure-security.html",
severity="High",
status="Failed",
+ region=region,
)
)
@@ -181,6 +201,7 @@ def check_sagemaker_internet_access() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/infrastructure-security.html",
severity="High",
status="Failed",
+ region=region,
)
)
else:
@@ -197,6 +218,7 @@ def check_sagemaker_internet_access() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/infrastructure-security.html",
severity="High",
status="Passed",
+ region=region,
)
)
else:
@@ -209,6 +231,7 @@ def check_sagemaker_internet_access() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/infrastructure-security.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
@@ -228,12 +251,13 @@ def check_sagemaker_internet_access() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Failed",
+ region=region,
)
]
}
-def check_guardduty_enabled() -> Dict[str, Any]:
+def check_guardduty_enabled(region: str = "") -> Dict[str, Any]:
"""
Check if GuardDuty is enabled in the account to monitor SageMaker security issues
@@ -248,7 +272,9 @@ def check_guardduty_enabled() -> Dict[str, Any]:
}
try:
- guardduty_client = boto3.client("guardduty")
+ guardduty_client = boto3.client(
+ "guardduty", config=boto3_config, region_name=region
+ )
# Get list of detectors in the current region
detectors = guardduty_client.list_detectors()
@@ -263,6 +289,7 @@ def check_guardduty_enabled() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/guardduty/latest/ug/ai-protection.html",
severity="High",
status="Failed",
+ region=region,
)
)
else:
@@ -275,6 +302,7 @@ def check_guardduty_enabled() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/guardduty/latest/ug/ai-protection.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
except ClientError as e:
@@ -289,6 +317,7 @@ def check_guardduty_enabled() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/guardduty/latest/ug/security-iam.html",
severity="High",
status="Failed",
+ region=region,
)
)
except Exception as e:
@@ -301,15 +330,24 @@ def check_guardduty_enabled() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/guardduty/latest/ug/what-is-guardduty.html",
severity="High",
status="Failed",
+ region=region,
)
)
return findings
-def check_sagemaker_iam_permissions(permission_cache) -> Dict[str, Any]:
+def check_sagemaker_iam_permissions(
+ permission_cache, region: str = ""
+) -> Dict[str, Any]:
"""
- Check SageMaker IAM permissions, SSO configuration, and stale access
+ Check SageMaker IAM permissions and stale access.
+
+ These checks are derived purely from IAM (a global service) and the cached
+ permissions, so they produce identical results in every region. The handler
+ runs this check once, on the primary region, tagged with GLOBAL_REGION_LABEL.
+ Regional SSO/domain configuration is checked separately by
+ check_sagemaker_sso_configuration.
"""
logger.debug("Starting check for SageMaker IAM permissions")
try:
@@ -323,9 +361,10 @@ def check_sagemaker_iam_permissions(permission_cache) -> Dict[str, Any]:
roles_with_full_access.append(role_name)
break
- # Check for stale access
+ # Check for stale access. IAM is a global service, so the client is not
+ # region-scoped (region is used only for finding tags).
stale_users = []
- iam_client = boto3.client("iam")
+ iam_client = boto3.client("iam", config=boto3_config)
two_months_ago = datetime.now(timezone.utc) - timedelta(days=60)
# Check users' last access to SageMaker
@@ -370,66 +409,8 @@ def check_sagemaker_iam_permissions(permission_cache) -> Dict[str, Any]:
f"Error checking last access for user {user_name}: {str(e)}"
)
- # Check SSO configuration
- domains_without_sso = []
- try:
- sagemaker_client = boto3.client("sagemaker")
- paginator = sagemaker_client.get_paginator("list_domains")
-
- for page in paginator.paginate():
- for domain in page["Domains"]:
- domain_id = domain["DomainId"]
- try:
- domain_details = sagemaker_client.describe_domain(
- DomainId=domain_id
- )
-
- # Check authentication mode
- auth_mode = domain_details.get("AuthMode", "")
- if auth_mode != "SSO":
- domains_without_sso.append(
- {
- "domain_id": domain_id,
- "domain_name": domain_details.get(
- "DomainName", "N/A"
- ),
- "auth_mode": auth_mode,
- }
- )
-
- # Check if SSO is properly configured with Identity Center
- if auth_mode == "SSO":
- try:
- # Check Identity Center configuration
- identity_store_id = domain_details.get(
- "IdentityStoreId"
- )
-
- if not identity_store_id:
- domains_without_sso.append(
- {
- "domain_id": domain_id,
- "domain_name": domain_details.get(
- "DomainName", "N/A"
- ),
- "auth_mode": "SSO (Incomplete Configuration)",
- }
- )
- except Exception as sso_error:
- logger.error(
- f"Error checking SSO configuration for domain {domain_id}: {str(sso_error)}"
- )
-
- except Exception as domain_error:
- logger.error(
- f"Error checking domain {domain_id}: {str(domain_error)}"
- )
-
- except Exception as e:
- logger.error(f"Error checking SSO configuration: {str(e)}")
-
# Generate findings
- if roles_with_full_access or stale_users or domains_without_sso:
+ if roles_with_full_access or stale_users:
# Findings for full access roles
if roles_with_full_access:
for role_name in roles_with_full_access:
@@ -442,6 +423,7 @@ def check_sagemaker_iam_permissions(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker-unified-studio/latest/adminguide/security-iam.html",
severity="High",
status="Failed",
+ region=region,
)
)
@@ -457,27 +439,7 @@ def check_sagemaker_iam_permissions(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker-unified-studio/latest/adminguide/security-iam.html",
severity="Medium",
status="Failed",
- )
- )
-
- # Findings for SSO
- if domains_without_sso:
- for domain in domains_without_sso:
- findings["csv_data"].append(
- create_finding(
- check_id="SM-02",
- finding_name="SSO Not Properly Configured",
- finding_details=(
- f"SageMaker domain '{domain['domain_id']}' ({domain['domain_name']}) "
- f"is using authentication mode: {domain['auth_mode']}"
- ),
- resolution=(
- "Enable and properly configure AWS IAM Identity Center (successor to AWS SSO) "
- "for centralized access management. Ensure Identity Store ID is configured."
- ),
- reference="https://aws.amazon.com/blogs/machine-learning/team-and-user-management-with-amazon-sagemaker-and-aws-sso/",
- severity="Medium",
- status="Failed",
+ region=region,
)
)
else:
@@ -485,11 +447,12 @@ def check_sagemaker_iam_permissions(permission_cache) -> Dict[str, Any]:
create_finding(
check_id="SM-02",
finding_name="SageMaker IAM Permissions Check",
- finding_details="No issues found with IAM permissions, SSO is enabled, and no stale access detected",
+ finding_details="No issues found with IAM permissions and no stale access detected",
resolution="No action required",
reference="https://docs.aws.amazon.com/sagemaker-unified-studio/latest/adminguide/security-iam.html",
severity="High",
status="Passed",
+ region=region,
)
)
@@ -507,6 +470,111 @@ def check_sagemaker_iam_permissions(permission_cache) -> Dict[str, Any]:
}
+def check_sagemaker_sso_configuration(region: str = "") -> Dict[str, Any]:
+ """
+ Check SageMaker domain SSO / IAM Identity Center configuration.
+
+ SageMaker domains are regional resources, so this check runs once per
+ scanned region (unlike the IAM-global checks in
+ check_sagemaker_iam_permissions).
+ """
+ logger.debug("Starting check for SageMaker SSO configuration")
+ try:
+ findings = {"csv_data": []}
+
+ domains_without_sso = []
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
+ paginator = sagemaker_client.get_paginator("list_domains")
+
+ for page in paginator.paginate():
+ for domain in page["Domains"]:
+ domain_id = domain["DomainId"]
+ try:
+ domain_details = sagemaker_client.describe_domain(
+ DomainId=domain_id
+ )
+
+ # Check authentication mode
+ auth_mode = domain_details.get("AuthMode", "")
+ if auth_mode != "SSO":
+ domains_without_sso.append(
+ {
+ "domain_id": domain_id,
+ "domain_name": domain_details.get("DomainName", "N/A"),
+ "auth_mode": auth_mode,
+ }
+ )
+
+ # Check if SSO is properly configured with Identity Center
+ if auth_mode == "SSO":
+ identity_store_id = domain_details.get("IdentityStoreId")
+
+ if not identity_store_id:
+ domains_without_sso.append(
+ {
+ "domain_id": domain_id,
+ "domain_name": domain_details.get(
+ "DomainName", "N/A"
+ ),
+ "auth_mode": "SSO (Incomplete Configuration)",
+ }
+ )
+
+ except Exception as domain_error:
+ logger.error(
+ f"Error checking domain {domain_id}: {str(domain_error)}"
+ )
+
+ if domains_without_sso:
+ for domain in domains_without_sso:
+ findings["csv_data"].append(
+ create_finding(
+ check_id="SM-02",
+ finding_name="SSO Not Properly Configured",
+ finding_details=(
+ f"SageMaker domain '{domain['domain_id']}' ({domain['domain_name']}) "
+ f"is using authentication mode: {domain['auth_mode']}"
+ ),
+ resolution=(
+ "Enable and properly configure AWS IAM Identity Center (successor to AWS SSO) "
+ "for centralized access management. Ensure Identity Store ID is configured."
+ ),
+ reference="https://aws.amazon.com/blogs/machine-learning/team-and-user-management-with-amazon-sagemaker-and-aws-sso/",
+ severity="Medium",
+ status="Failed",
+ region=region,
+ )
+ )
+ else:
+ findings["csv_data"].append(
+ create_finding(
+ check_id="SM-02",
+ finding_name="SageMaker SSO Configuration Check",
+ finding_details="No SageMaker domains found, or all domains use SSO with IAM Identity Center configured",
+ resolution="No action required",
+ reference="https://aws.amazon.com/blogs/machine-learning/team-and-user-management-with-amazon-sagemaker-and-aws-sso/",
+ severity="Medium",
+ status="Passed",
+ region=region,
+ )
+ )
+
+ return findings
+
+ except Exception as e:
+ logger.error(
+ f"Error in check_sagemaker_sso_configuration: {str(e)}", exc_info=True
+ )
+ return {
+ "check_name": "SageMaker SSO Configuration Check",
+ "status": "ERROR",
+ "details": f"Error during check: {str(e)}",
+ "csv_data": [],
+ }
+
+
def has_sagemaker_permissions(policy_doc: Dict) -> bool:
"""
Check if a policy document contains SageMaker permissions
@@ -546,7 +614,7 @@ def get_account_id() -> str:
raise
-def check_sagemaker_data_protection() -> Dict[str, Any]:
+def check_sagemaker_data_protection(region: str = "") -> Dict[str, Any]:
"""
Check SageMaker data protection configurations including encryption at rest and in transit
"""
@@ -554,7 +622,9 @@ def check_sagemaker_data_protection() -> Dict[str, Any]:
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker")
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
# Track resources with encryption issues
resources_with_aws_managed_keys = []
@@ -705,6 +775,7 @@ def check_sagemaker_data_protection() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/key-management.html",
severity="High",
status="Failed",
+ region=region,
)
)
@@ -719,6 +790,7 @@ def check_sagemaker_data_protection() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/key-management.html",
severity="Low",
status="Failed",
+ region=region,
)
)
@@ -733,6 +805,7 @@ def check_sagemaker_data_protection() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/encryption-in-transit.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
@@ -747,6 +820,7 @@ def check_sagemaker_data_protection() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Passed",
+ region=region,
)
)
else:
@@ -759,6 +833,7 @@ def check_sagemaker_data_protection() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
@@ -778,12 +853,15 @@ def check_sagemaker_data_protection() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Failed",
+ region=region,
)
]
}
-def check_sagemaker_mlops_utilization(permission_cache) -> Dict[str, Any]:
+def check_sagemaker_mlops_utilization(
+ permission_cache, region: str = ""
+) -> Dict[str, Any]:
"""
Check if SageMaker MLOps features (Model Registry, Feature Store, and Pipelines)
are being utilized properly
@@ -792,7 +870,9 @@ def check_sagemaker_mlops_utilization(permission_cache) -> Dict[str, Any]:
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
issues_found = []
# Check Model Registry Usage
@@ -948,6 +1028,7 @@ def check_sagemaker_mlops_utilization(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/mlops.html",
severity=issue["severity"],
status=issue["status"],
+ region=region,
)
)
else:
@@ -961,6 +1042,7 @@ def check_sagemaker_mlops_utilization(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/mlops.html",
severity="Low",
status="Passed",
+ region=region,
)
)
@@ -1001,7 +1083,7 @@ def get_resolution_for_component(component: str) -> str:
)
-def check_sagemaker_clarify_usage(permission_cache) -> Dict[str, Any]:
+def check_sagemaker_clarify_usage(permission_cache, region: str = "") -> Dict[str, Any]:
"""
Check if SageMaker Clarify is being used for bias detection and model explainability
"""
@@ -1009,7 +1091,9 @@ def check_sagemaker_clarify_usage(permission_cache) -> Dict[str, Any]:
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
issues_found = []
try:
@@ -1075,6 +1159,7 @@ def check_sagemaker_clarify_usage(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/clarify-configure-processing-jobs.html",
severity=issue["severity"],
status=issue["status"],
+ region=region,
)
)
else:
@@ -1088,6 +1173,7 @@ def check_sagemaker_clarify_usage(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/clarify-configure-processing-jobs.html",
severity="Low",
status="Passed",
+ region=region,
)
)
@@ -1103,7 +1189,9 @@ def check_sagemaker_clarify_usage(permission_cache) -> Dict[str, Any]:
}
-def check_sagemaker_model_monitor_usage(permission_cache) -> Dict[str, Any]:
+def check_sagemaker_model_monitor_usage(
+ permission_cache, region: str = ""
+) -> Dict[str, Any]:
"""
Check if SageMaker Model Monitor is configured and actively monitoring models
"""
@@ -1111,13 +1199,15 @@ def check_sagemaker_model_monitor_usage(permission_cache) -> Dict[str, Any]:
# Monitor data-quality baselines to be refreshed on a regulator-aligned cadence
# (SR 11-7 ongoing monitoring) and for the baseline statistics to be emitted to
# CloudWatch under namespace /aws/sagemaker/Endpoints/data-metric with
- # emit_metrics=Enabled. See docs/SECURITY_CHECKS_FINSERV_PART1_INFRA_CONTROLS.md
+ # emit_metrics=Enabled. See docs/SECURITY_CHECKS_FINSERV.md
# (FS-17 → SM-07 extension note).
logger.debug("Starting check for SageMaker Model Monitor usage")
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
issues_found = []
try:
@@ -1176,6 +1266,7 @@ def check_sagemaker_model_monitor_usage(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor.html",
severity=issue["severity"],
status=issue["status"],
+ region=region,
)
)
else:
@@ -1188,6 +1279,7 @@ def check_sagemaker_model_monitor_usage(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
@@ -1205,7 +1297,7 @@ def check_sagemaker_model_monitor_usage(permission_cache) -> Dict[str, Any]:
}
-def check_sagemaker_notebook_root_access() -> Dict[str, Any]:
+def check_sagemaker_notebook_root_access(region: str = "") -> Dict[str, Any]:
"""
Check if SageMaker notebook instances have root access disabled.
Root access enables privilege escalation and should be disabled for security.
@@ -1215,7 +1307,9 @@ def check_sagemaker_notebook_root_access() -> Dict[str, Any]:
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
notebooks_with_root = []
notebooks_without_root = []
@@ -1258,6 +1352,7 @@ def check_sagemaker_notebook_root_access() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/nbi-root-access.html",
severity="High",
status="Failed",
+ region=region,
)
)
else:
@@ -1272,6 +1367,7 @@ def check_sagemaker_notebook_root_access() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/nbi-root-access.html",
severity="High",
status="Passed",
+ region=region,
)
)
else:
@@ -1285,6 +1381,7 @@ def check_sagemaker_notebook_root_access() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/nbi-root-access.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
@@ -1304,12 +1401,13 @@ def check_sagemaker_notebook_root_access() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Failed",
+ region=region,
)
]
}
-def check_sagemaker_notebook_vpc_deployment() -> Dict[str, Any]:
+def check_sagemaker_notebook_vpc_deployment(region: str = "") -> Dict[str, Any]:
"""
Check if SageMaker notebook instances are deployed within a custom VPC.
Notebooks outside VPC use shared infrastructure with less isolation.
@@ -1319,7 +1417,9 @@ def check_sagemaker_notebook_vpc_deployment() -> Dict[str, Any]:
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
notebooks_without_vpc = []
notebooks_with_vpc = []
@@ -1368,6 +1468,7 @@ def check_sagemaker_notebook_vpc_deployment() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/appendix-notebook-and-internet-access.html",
severity="High",
status="Failed",
+ region=region,
)
)
else:
@@ -1382,6 +1483,7 @@ def check_sagemaker_notebook_vpc_deployment() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/appendix-notebook-and-internet-access.html",
severity="High",
status="Passed",
+ region=region,
)
)
else:
@@ -1395,6 +1497,7 @@ def check_sagemaker_notebook_vpc_deployment() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/appendix-notebook-and-internet-access.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
@@ -1414,12 +1517,13 @@ def check_sagemaker_notebook_vpc_deployment() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Failed",
+ region=region,
)
]
}
-def check_sagemaker_model_network_isolation() -> Dict[str, Any]:
+def check_sagemaker_model_network_isolation(region: str = "") -> Dict[str, Any]:
"""
Check if SageMaker hosted models have network isolation enabled.
Without isolation, model containers can make outbound calls and exfiltrate data.
@@ -1429,7 +1533,9 @@ def check_sagemaker_model_network_isolation() -> Dict[str, Any]:
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
models_without_isolation = []
models_with_isolation = []
@@ -1481,6 +1587,7 @@ def check_sagemaker_model_network_isolation() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/mkt-algo-model-internet-free.html",
severity="High",
status="Failed",
+ region=region,
)
)
@@ -1494,6 +1601,7 @@ def check_sagemaker_model_network_isolation() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/mkt-algo-model-internet-free.html",
severity="High",
status="Failed",
+ region=region,
)
)
else:
@@ -1508,6 +1616,7 @@ def check_sagemaker_model_network_isolation() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/mkt-algo-model-internet-free.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
else:
@@ -1521,6 +1630,7 @@ def check_sagemaker_model_network_isolation() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/mkt-algo-model-internet-free.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
@@ -1540,12 +1650,13 @@ def check_sagemaker_model_network_isolation() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Failed",
+ region=region,
)
]
}
-def check_sagemaker_endpoint_instance_count() -> Dict[str, Any]:
+def check_sagemaker_endpoint_instance_count(region: str = "") -> Dict[str, Any]:
"""
Check if SageMaker endpoints have more than one instance for availability.
Single instance creates availability risk and single point of compromise.
@@ -1555,7 +1666,9 @@ def check_sagemaker_endpoint_instance_count() -> Dict[str, Any]:
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
endpoints_single_instance = []
endpoints_multi_instance = []
@@ -1619,6 +1732,7 @@ def check_sagemaker_endpoint_instance_count() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/endpoint-auto-scaling.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
else:
@@ -1633,6 +1747,7 @@ def check_sagemaker_endpoint_instance_count() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/endpoint-auto-scaling.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
else:
@@ -1646,6 +1761,7 @@ def check_sagemaker_endpoint_instance_count() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/endpoint-auto-scaling.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
@@ -1665,12 +1781,13 @@ def check_sagemaker_endpoint_instance_count() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Failed",
+ region=region,
)
]
}
-def check_sagemaker_monitoring_network_isolation() -> Dict[str, Any]:
+def check_sagemaker_monitoring_network_isolation(region: str = "") -> Dict[str, Any]:
"""
Check if SageMaker monitoring schedules have network isolation enabled.
Aligns with AWS Security Hub control SageMaker.14
@@ -1679,7 +1796,9 @@ def check_sagemaker_monitoring_network_isolation() -> Dict[str, Any]:
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
schedules_without_isolation = []
schedules_with_isolation = []
@@ -1736,6 +1855,7 @@ def check_sagemaker_monitoring_network_isolation() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor-network-isolation.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
else:
@@ -1750,6 +1870,7 @@ def check_sagemaker_monitoring_network_isolation() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor-network-isolation.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
else:
@@ -1763,6 +1884,7 @@ def check_sagemaker_monitoring_network_isolation() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor-network-isolation.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
@@ -1783,12 +1905,13 @@ def check_sagemaker_monitoring_network_isolation() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Failed",
+ region=region,
)
]
}
-def check_sagemaker_model_container_repository() -> Dict[str, Any]:
+def check_sagemaker_model_container_repository(region: str = "") -> Dict[str, Any]:
"""
Check if SageMaker models pull container images from private ECR in VPC.
Using Platform mode exposes supply chain risks.
@@ -1798,7 +1921,9 @@ def check_sagemaker_model_container_repository() -> Dict[str, Any]:
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
models_platform_mode = []
models_vpc_mode = []
@@ -1879,6 +2004,7 @@ def check_sagemaker_model_container_repository() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-container-repositories.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
@@ -1892,6 +2018,7 @@ def check_sagemaker_model_container_repository() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-container-repositories.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
else:
@@ -1906,6 +2033,7 @@ def check_sagemaker_model_container_repository() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-container-repositories.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
else:
@@ -1919,6 +2047,7 @@ def check_sagemaker_model_container_repository() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-container-repositories.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
@@ -1939,12 +2068,13 @@ def check_sagemaker_model_container_repository() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Failed",
+ region=region,
)
]
}
-def check_sagemaker_feature_store_encryption() -> Dict[str, Any]:
+def check_sagemaker_feature_store_encryption(region: str = "") -> Dict[str, Any]:
"""
Check if SageMaker Feature Store offline stores have KMS encryption.
Aligns with AWS Security Hub control SageMaker.17
@@ -1953,7 +2083,9 @@ def check_sagemaker_feature_store_encryption() -> Dict[str, Any]:
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
feature_groups_without_encryption = []
feature_groups_with_encryption = []
@@ -2010,6 +2142,7 @@ def check_sagemaker_feature_store_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/feature-store-security.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
else:
@@ -2023,6 +2156,7 @@ def check_sagemaker_feature_store_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/feature-store-security.html",
severity="High",
status="Passed",
+ region=region,
)
)
else:
@@ -2036,6 +2170,7 @@ def check_sagemaker_feature_store_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/feature-store-security.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
@@ -2056,12 +2191,13 @@ def check_sagemaker_feature_store_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Failed",
+ region=region,
)
]
}
-def check_sagemaker_data_quality_encryption() -> Dict[str, Any]:
+def check_sagemaker_data_quality_encryption(region: str = "") -> Dict[str, Any]:
"""
Check if SageMaker data quality job definitions have inter-container traffic encryption.
Aligns with AWS Security Hub control SageMaker.9
@@ -2070,7 +2206,9 @@ def check_sagemaker_data_quality_encryption() -> Dict[str, Any]:
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
jobs_without_encryption = []
jobs_with_encryption = []
@@ -2120,6 +2258,7 @@ def check_sagemaker_data_quality_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor-data-quality.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
else:
@@ -2134,6 +2273,7 @@ def check_sagemaker_data_quality_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor-data-quality.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
else:
@@ -2147,6 +2287,7 @@ def check_sagemaker_data_quality_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor-data-quality.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
@@ -2166,12 +2307,13 @@ def check_sagemaker_data_quality_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Failed",
+ region=region,
)
]
}
-def check_sagemaker_processing_job_encryption() -> Dict[str, Any]:
+def check_sagemaker_processing_job_encryption(region: str = "") -> Dict[str, Any]:
"""
Check if SageMaker processing jobs have volume encryption enabled.
Aligns with AWS Security Hub control SageMaker.10
@@ -2180,7 +2322,9 @@ def check_sagemaker_processing_job_encryption() -> Dict[str, Any]:
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
jobs_without_encryption = []
jobs_with_encryption = []
@@ -2232,6 +2376,7 @@ def check_sagemaker_processing_job_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/processing-job.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
@@ -2245,6 +2390,7 @@ def check_sagemaker_processing_job_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/processing-job.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
else:
@@ -2259,6 +2405,7 @@ def check_sagemaker_processing_job_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/processing-job.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
else:
@@ -2272,6 +2419,7 @@ def check_sagemaker_processing_job_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/processing-job.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
@@ -2292,12 +2440,13 @@ def check_sagemaker_processing_job_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Failed",
+ region=region,
)
]
}
-def check_sagemaker_transform_job_encryption() -> Dict[str, Any]:
+def check_sagemaker_transform_job_encryption(region: str = "") -> Dict[str, Any]:
"""
Check if SageMaker transform jobs have volume encryption enabled.
Aligns with AWS Security Hub control SageMaker.11
@@ -2306,7 +2455,9 @@ def check_sagemaker_transform_job_encryption() -> Dict[str, Any]:
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
jobs_without_encryption = []
jobs_with_encryption = []
@@ -2355,6 +2506,7 @@ def check_sagemaker_transform_job_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/batch-transform.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
@@ -2368,6 +2520,7 @@ def check_sagemaker_transform_job_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/batch-transform.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
else:
@@ -2382,6 +2535,7 @@ def check_sagemaker_transform_job_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/batch-transform.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
else:
@@ -2395,6 +2549,7 @@ def check_sagemaker_transform_job_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/batch-transform.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
@@ -2415,12 +2570,15 @@ def check_sagemaker_transform_job_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Failed",
+ region=region,
)
]
}
-def check_sagemaker_hyperparameter_tuning_encryption() -> Dict[str, Any]:
+def check_sagemaker_hyperparameter_tuning_encryption(
+ region: str = "",
+) -> Dict[str, Any]:
"""
Check if SageMaker hyperparameter tuning jobs have volume encryption enabled.
Aligns with AWS Security Hub control SageMaker.12
@@ -2429,7 +2587,9 @@ def check_sagemaker_hyperparameter_tuning_encryption() -> Dict[str, Any]:
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
jobs_without_encryption = []
jobs_with_encryption = []
@@ -2485,6 +2645,7 @@ def check_sagemaker_hyperparameter_tuning_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/automatic-model-tuning.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
@@ -2498,6 +2659,7 @@ def check_sagemaker_hyperparameter_tuning_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/automatic-model-tuning.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
else:
@@ -2512,6 +2674,7 @@ def check_sagemaker_hyperparameter_tuning_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/automatic-model-tuning.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
else:
@@ -2525,6 +2688,7 @@ def check_sagemaker_hyperparameter_tuning_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/automatic-model-tuning.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
@@ -2545,12 +2709,13 @@ def check_sagemaker_hyperparameter_tuning_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Failed",
+ region=region,
)
]
}
-def check_sagemaker_compilation_job_encryption() -> Dict[str, Any]:
+def check_sagemaker_compilation_job_encryption(region: str = "") -> Dict[str, Any]:
"""
Check if SageMaker compilation jobs have volume encryption enabled.
Aligns with AWS Security Hub control SageMaker.13
@@ -2559,7 +2724,9 @@ def check_sagemaker_compilation_job_encryption() -> Dict[str, Any]:
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
jobs_without_encryption = []
jobs_with_encryption = []
@@ -2606,6 +2773,7 @@ def check_sagemaker_compilation_job_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/neo.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
@@ -2619,6 +2787,7 @@ def check_sagemaker_compilation_job_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/neo.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
else:
@@ -2633,6 +2802,7 @@ def check_sagemaker_compilation_job_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/neo.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
else:
@@ -2646,6 +2816,7 @@ def check_sagemaker_compilation_job_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/neo.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
@@ -2666,12 +2837,13 @@ def check_sagemaker_compilation_job_encryption() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Failed",
+ region=region,
)
]
}
-def check_sagemaker_automl_network_isolation() -> Dict[str, Any]:
+def check_sagemaker_automl_network_isolation(region: str = "") -> Dict[str, Any]:
"""
Check if SageMaker AutoML (Autopilot) jobs have network isolation enabled.
Aligns with AWS Security Hub control SageMaker.15
@@ -2680,7 +2852,9 @@ def check_sagemaker_automl_network_isolation() -> Dict[str, Any]:
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
jobs_without_isolation = []
jobs_with_isolation = []
@@ -2731,6 +2905,7 @@ def check_sagemaker_automl_network_isolation() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/autopilot-security.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
@@ -2744,6 +2919,7 @@ def check_sagemaker_automl_network_isolation() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/autopilot-security.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
else:
@@ -2758,6 +2934,7 @@ def check_sagemaker_automl_network_isolation() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/autopilot-security.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
else:
@@ -2771,6 +2948,7 @@ def check_sagemaker_automl_network_isolation() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/autopilot-security.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
@@ -2791,6 +2969,7 @@ def check_sagemaker_automl_network_isolation() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Failed",
+ region=region,
)
]
}
@@ -2801,7 +2980,7 @@ def check_sagemaker_automl_network_isolation() -> Dict[str, Any]:
# ============================================================================
-def check_model_approval_workflow() -> Dict[str, Any]:
+def check_model_approval_workflow(region: str = "") -> Dict[str, Any]:
"""
Check if Model Registry has proper approval workflows configured.
Validates that models go through approval process before production deployment.
@@ -2809,13 +2988,15 @@ def check_model_approval_workflow() -> Dict[str, Any]:
# FinServ extension (FS-19): The FinServ guide (PDF §1.2.14) expects model
# package groups to enforce ModelApprovalStatus=PendingManualApproval by default
# and to flag model packages that are auto-approved as their latest version.
- # See docs/SECURITY_CHECKS_FINSERV_PART1_INFRA_CONTROLS.md (FS-19 → SM-22
+ # See docs/SECURITY_CHECKS_FINSERV.md (FS-19 → SM-22
# extension note) for the detection refinement.
logger.debug("Starting check for model approval workflow")
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
issues_found = []
groups_checked = 0
@@ -2900,6 +3081,7 @@ def check_model_approval_workflow() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-registry-approve.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
elif issues_found:
@@ -2913,6 +3095,7 @@ def check_model_approval_workflow() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-registry-approve.html",
severity=issue["severity"],
status="Failed",
+ region=region,
)
)
else:
@@ -2925,6 +3108,7 @@ def check_model_approval_workflow() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-registry-approve.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
@@ -2942,12 +3126,13 @@ def check_model_approval_workflow() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Failed",
+ region=region,
)
]
}
-def check_model_drift_detection() -> Dict[str, Any]:
+def check_model_drift_detection(region: str = "") -> Dict[str, Any]:
"""
Check if Model Monitor is configured for drift detection with proper baselines.
Validates that models have data quality and model quality monitoring configured.
@@ -2955,13 +3140,15 @@ def check_model_drift_detection() -> Dict[str, Any]:
# FinServ extension (FS-18): In addition to ModelQuality drift monitoring, the
# FinServ guide (PDF §1.2.14) calls out low-entropy classification monitoring
# as an early-warning indicator of training-data poisoning. See
- # docs/SECURITY_CHECKS_FINSERV_PART1_INFRA_CONTROLS.md (FS-18 → SM-23
+ # docs/SECURITY_CHECKS_FINSERV.md (FS-18 → SM-23
# extension note) for the remediation step to add.
logger.debug("Starting check for model drift detection")
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
endpoints_without_monitoring = []
endpoints_with_monitoring = []
@@ -3054,6 +3241,7 @@ def check_model_drift_detection() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
@@ -3067,6 +3255,7 @@ def check_model_drift_detection() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor.html",
severity="Medium",
status="Failed",
+ region=region,
)
)
@@ -3081,6 +3270,7 @@ def check_model_drift_detection() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor.html",
severity="Low",
status="Failed",
+ region=region,
)
)
@@ -3095,6 +3285,7 @@ def check_model_drift_detection() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
else:
@@ -3107,6 +3298,7 @@ def check_model_drift_detection() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
@@ -3124,12 +3316,13 @@ def check_model_drift_detection() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Failed",
+ region=region,
)
]
}
-def check_ab_testing_shadow_deployment() -> Dict[str, Any]:
+def check_ab_testing_shadow_deployment(region: str = "") -> Dict[str, Any]:
"""
Check if endpoints are configured with proper A/B testing or shadow deployment patterns.
Validates production variant configurations for safe model deployment.
@@ -3138,7 +3331,9 @@ def check_ab_testing_shadow_deployment() -> Dict[str, Any]:
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
single_variant_endpoints = []
multi_variant_endpoints = []
@@ -3216,6 +3411,7 @@ def check_ab_testing_shadow_deployment() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-ab-testing.html",
severity="Low",
status="Passed",
+ region=region,
)
)
else:
@@ -3230,6 +3426,7 @@ def check_ab_testing_shadow_deployment() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-shadow-deployment.html",
severity="Low",
status="Passed",
+ region=region,
)
)
@@ -3244,6 +3441,7 @@ def check_ab_testing_shadow_deployment() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-ab-testing.html",
severity="Low",
status="Passed",
+ region=region,
)
)
@@ -3258,6 +3456,7 @@ def check_ab_testing_shadow_deployment() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-ab-testing.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
elif not shadow_endpoints and not multi_variant_endpoints:
@@ -3270,6 +3469,7 @@ def check_ab_testing_shadow_deployment() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-ab-testing.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
else:
@@ -3282,6 +3482,7 @@ def check_ab_testing_shadow_deployment() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-ab-testing.html",
severity="Low",
status="Passed",
+ region=region,
)
)
@@ -3301,12 +3502,13 @@ def check_ab_testing_shadow_deployment() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Failed",
+ region=region,
)
]
}
-def check_ml_lineage_tracking() -> Dict[str, Any]:
+def check_ml_lineage_tracking(region: str = "") -> Dict[str, Any]:
"""
Check if ML Lineage Tracking is being used to track model artifacts and experiments.
Validates that experiments, trials, and artifact associations are configured.
@@ -3315,7 +3517,9 @@ def check_ml_lineage_tracking() -> Dict[str, Any]:
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
experiments_found = False
trials_found = False
@@ -3404,6 +3608,7 @@ def check_ml_lineage_tracking() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/experiments.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
elif not trials_found:
@@ -3416,6 +3621,7 @@ def check_ml_lineage_tracking() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/experiments.html",
severity="Low",
status="Failed",
+ region=region,
)
)
else:
@@ -3428,6 +3634,7 @@ def check_ml_lineage_tracking() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/experiments.html",
severity="Low",
status="Passed",
+ region=region,
)
)
@@ -3442,6 +3649,7 @@ def check_ml_lineage_tracking() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/lineage-tracking.html",
severity="Informational",
status="N/A",
+ region=region,
)
)
@@ -3459,12 +3667,13 @@ def check_ml_lineage_tracking() -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/security.html",
severity="High",
status="Failed",
+ region=region,
)
]
}
-def check_model_registry_usage(permission_cache) -> Dict[str, Any]:
+def check_model_registry_usage(permission_cache, region: str = "") -> Dict[str, Any]:
"""
Check if Amazon Model Registry is being used effectively for model management
"""
@@ -3472,7 +3681,9 @@ def check_model_registry_usage(permission_cache) -> Dict[str, Any]:
try:
findings = {"csv_data": []}
- sagemaker_client = boto3.client("sagemaker", config=boto3_config)
+ sagemaker_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
issues_found = []
try:
@@ -3562,6 +3773,7 @@ def check_model_registry_usage(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-registry.html",
severity=issue["severity"],
status=issue["status"],
+ region=region,
)
)
else:
@@ -3574,6 +3786,7 @@ def check_model_registry_usage(permission_cache) -> Dict[str, Any]:
reference="https://docs.aws.amazon.com/sagemaker/latest/dg/model-registry.html",
severity="Medium",
status="Passed",
+ region=region,
)
)
@@ -3662,6 +3875,7 @@ def generate_csv_report(findings: List[Dict[str, Any]]) -> str:
"Reference",
"Severity",
"Status",
+ "Region",
]
writer = csv.DictWriter(csv_buffer, fieldnames=fieldnames)
@@ -3678,7 +3892,9 @@ def get_current_utc_date():
return datetime.now(timezone.utc).strftime("%Y/%m/%d")
-def write_to_s3(execution_id, csv_content: str, bucket_name: str) -> Dict[str, str]:
+def write_to_s3(
+ execution_id, csv_content: str, bucket_name: str, region: str = ""
+) -> Dict[str, str]:
"""
Write CSV reports to S3 bucket
"""
@@ -3686,8 +3902,10 @@ def write_to_s3(execution_id, csv_content: str, bucket_name: str) -> Dict[str, s
try:
s3_client = boto3.client("s3", config=boto3_config)
- # Upload CSV file
- csv_file_name = f"sagemaker_security_report_{execution_id}.csv"
+ if region:
+ csv_file_name = f"sagemaker_security_report_{execution_id}_{region}.csv"
+ else:
+ csv_file_name = f"sagemaker_security_report_{execution_id}.csv"
s3_client.put_object(
Bucket=bucket_name,
Key=csv_file_name,
@@ -3711,9 +3929,16 @@ def lambda_handler(event, context):
all_findings = []
try:
- # Initialize permission cache
- logger.info("Initializing IAM permission cache")
+ # Extract target region from Step Functions Map state
+ region = event.get("Region", os.environ.get("AWS_REGION", "us-east-1"))
+ # IAM is global: only the primary region (Map index 0) runs IAM-only checks.
+ is_primary_region = int(event.get("RegionIndex", 0)) == 0
+ logger.info(f"Scanning region: {region} (primary={is_primary_region})")
+
execution_id = event["Execution"]["Name"]
+
+ # Initialize permission cache (shared/global IAM data)
+ logger.info("Initializing IAM permission cache")
permission_cache = get_permissions_cache(execution_id)
if not permission_cache:
@@ -3722,114 +3947,236 @@ def lambda_handler(event, context):
)
permission_cache = {"role_permissions": {}, "user_permissions": {}}
+ # Run global IAM-only checks once (on the primary region) so the same role
+ # and stale-access violations are not reported once per scanned region.
+ # These run before the regional availability gate so they are still emitted
+ # even if SageMaker is not available in the primary region.
+ if is_primary_region:
+ logger.info("Running global SageMaker IAM permissions check (SM-02)")
+ sagemaker_iam_findings = check_sagemaker_iam_permissions(
+ permission_cache, region=GLOBAL_REGION_LABEL
+ )
+ all_findings.append(sagemaker_iam_findings)
+
+ # Verify SageMaker is available in this region
+ try:
+ test_client = boto3.client(
+ "sagemaker", config=boto3_config, region_name=region
+ )
+ test_client.list_notebook_instances(MaxResults=1)
+ except EndpointConnectionError:
+ logger.info(f"SageMaker service not available in region {region}, skipping")
+ all_findings.append(
+ {
+ "check_name": "SageMaker Service Availability",
+ "status": "N/A",
+ "details": f"SageMaker is not available in region {region}",
+ "csv_data": [
+ create_finding(
+ check_id="SM-00",
+ finding_name="SageMaker Service Availability",
+ finding_details=f"Amazon SageMaker is not available in region {region}. No checks performed.",
+ resolution="No action required. SageMaker is not deployed in this region.",
+ reference="https://docs.aws.amazon.com/general/latest/gr/sagemaker.html",
+ severity="Informational",
+ status="N/A",
+ region=region,
+ )
+ ],
+ }
+ )
+ csv_content = generate_csv_report(all_findings)
+ bucket_name = os.environ.get("AIML_ASSESSMENT_BUCKET_NAME")
+ s3_url = write_to_s3(execution_id, csv_content, bucket_name, region=region)
+ return {
+ "statusCode": 200,
+ "body": {
+ "message": f"SageMaker not available in {region}",
+ "report_url": s3_url,
+ },
+ }
+ except ClientError as e:
+ # A region that exists but is not enabled for the account surfaces as
+ # an auth/opt-in error rather than a connection failure. Treat it the
+ # same as "not available" instead of running every check against it.
+ error_code = e.response.get("Error", {}).get("Code", "")
+ if error_code in REGION_UNAVAILABLE_ERROR_CODES:
+ logger.info(
+ f"SageMaker not accessible in region {region} ({error_code}), skipping"
+ )
+ all_findings.append(
+ {
+ "check_name": "SageMaker Service Availability",
+ "status": "N/A",
+ "details": f"SageMaker is not available in region {region}",
+ "csv_data": [
+ create_finding(
+ check_id="SM-00",
+ finding_name="SageMaker Service Availability",
+ finding_details=f"Amazon SageMaker is not available or not enabled in region {region} ({error_code}). No checks performed.",
+ resolution="No action required if the region is intentionally disabled. Otherwise enable the region for this account.",
+ reference="https://docs.aws.amazon.com/general/latest/gr/sagemaker.html",
+ severity="Informational",
+ status="N/A",
+ region=region,
+ )
+ ],
+ }
+ )
+ csv_content = generate_csv_report(all_findings)
+ bucket_name = os.environ.get("AIML_ASSESSMENT_BUCKET_NAME")
+ s3_url = write_to_s3(
+ execution_id, csv_content, bucket_name, region=region
+ )
+ return {
+ "statusCode": 200,
+ "body": {
+ "message": f"SageMaker not available in {region}",
+ "report_url": s3_url,
+ },
+ }
+ # Service is reachable but returned another API error (e.g. AccessDenied)
+ # — proceed; individual checks handle their own errors.
+ logger.info(
+ f"SageMaker availability probe returned {error_code}; proceeding with checks"
+ )
+
logger.info("Running SageMaker internet access check")
- sagemaker_internet_access_findings = check_sagemaker_internet_access()
+ sagemaker_internet_access_findings = check_sagemaker_internet_access(
+ region=region
+ )
all_findings.append(sagemaker_internet_access_findings)
- logger.info("Running SageMaker IAM permissions check")
- sagemaker_iam_findings = check_sagemaker_iam_permissions(permission_cache)
- all_findings.append(sagemaker_iam_findings)
+ logger.info("Running SageMaker SSO configuration check")
+ sagemaker_sso_findings = check_sagemaker_sso_configuration(region=region)
+ all_findings.append(sagemaker_sso_findings)
logger.info("Running SageMaker data protection check")
- sagemaker_data_protection_findings = check_sagemaker_data_protection()
+ sagemaker_data_protection_findings = check_sagemaker_data_protection(
+ region=region
+ )
all_findings.append(sagemaker_data_protection_findings)
logger.info("Running GuardDuty SageMaker monitoring check")
- guardduty_findings = check_guardduty_enabled()
+ guardduty_findings = check_guardduty_enabled(region=region)
all_findings.append(guardduty_findings)
logger.info("Running SageMaker MLOps features utilization check")
- mlops_findings = check_sagemaker_mlops_utilization(permission_cache)
+ mlops_findings = check_sagemaker_mlops_utilization(
+ permission_cache, region=region
+ )
all_findings.append(mlops_findings)
logger.info("Running SageMaker Clarify usage check")
- clarify_findings = check_sagemaker_clarify_usage(permission_cache)
+ clarify_findings = check_sagemaker_clarify_usage(
+ permission_cache, region=region
+ )
all_findings.append(clarify_findings)
logger.info("Running SageMaker Model Monitor usage check")
- monitor_findings = check_sagemaker_model_monitor_usage(permission_cache)
+ monitor_findings = check_sagemaker_model_monitor_usage(
+ permission_cache, region=region
+ )
all_findings.append(monitor_findings)
logger.info("Running Model Registry usage check")
- registry_findings = check_model_registry_usage(permission_cache)
+ registry_findings = check_model_registry_usage(permission_cache, region=region)
all_findings.append(registry_findings)
logger.info("Running SageMaker notebook root access check")
- notebook_root_findings = check_sagemaker_notebook_root_access()
+ notebook_root_findings = check_sagemaker_notebook_root_access(region=region)
all_findings.append(notebook_root_findings)
logger.info("Running SageMaker notebook VPC deployment check")
- notebook_vpc_findings = check_sagemaker_notebook_vpc_deployment()
+ notebook_vpc_findings = check_sagemaker_notebook_vpc_deployment(region=region)
all_findings.append(notebook_vpc_findings)
logger.info("Running SageMaker model network isolation check")
- model_isolation_findings = check_sagemaker_model_network_isolation()
+ model_isolation_findings = check_sagemaker_model_network_isolation(
+ region=region
+ )
all_findings.append(model_isolation_findings)
logger.info("Running SageMaker endpoint instance count check")
- endpoint_instance_findings = check_sagemaker_endpoint_instance_count()
+ endpoint_instance_findings = check_sagemaker_endpoint_instance_count(
+ region=region
+ )
all_findings.append(endpoint_instance_findings)
logger.info("Running SageMaker monitoring network isolation check")
- monitoring_isolation_findings = check_sagemaker_monitoring_network_isolation()
+ monitoring_isolation_findings = check_sagemaker_monitoring_network_isolation(
+ region=region
+ )
all_findings.append(monitoring_isolation_findings)
logger.info("Running SageMaker model container repository check")
- model_repository_findings = check_sagemaker_model_container_repository()
+ model_repository_findings = check_sagemaker_model_container_repository(
+ region=region
+ )
all_findings.append(model_repository_findings)
logger.info("Running SageMaker Feature Store encryption check")
- feature_store_encryption_findings = check_sagemaker_feature_store_encryption()
+ feature_store_encryption_findings = check_sagemaker_feature_store_encryption(
+ region=region
+ )
all_findings.append(feature_store_encryption_findings)
logger.info("Running SageMaker data quality job encryption check")
- data_quality_encryption_findings = check_sagemaker_data_quality_encryption()
+ data_quality_encryption_findings = check_sagemaker_data_quality_encryption(
+ region=region
+ )
all_findings.append(data_quality_encryption_findings)
# Additional AWS Security Hub Controls
logger.info("Running SageMaker processing job encryption check (SageMaker.10)")
- processing_job_encryption_findings = check_sagemaker_processing_job_encryption()
+ processing_job_encryption_findings = check_sagemaker_processing_job_encryption(
+ region=region
+ )
all_findings.append(processing_job_encryption_findings)
logger.info("Running SageMaker transform job encryption check (SageMaker.11)")
- transform_job_encryption_findings = check_sagemaker_transform_job_encryption()
+ transform_job_encryption_findings = check_sagemaker_transform_job_encryption(
+ region=region
+ )
all_findings.append(transform_job_encryption_findings)
logger.info(
"Running SageMaker hyperparameter tuning job encryption check (SageMaker.12)"
)
hyperparameter_tuning_encryption_findings = (
- check_sagemaker_hyperparameter_tuning_encryption()
+ check_sagemaker_hyperparameter_tuning_encryption(region=region)
)
all_findings.append(hyperparameter_tuning_encryption_findings)
logger.info("Running SageMaker compilation job encryption check (SageMaker.13)")
compilation_job_encryption_findings = (
- check_sagemaker_compilation_job_encryption()
+ check_sagemaker_compilation_job_encryption(region=region)
)
all_findings.append(compilation_job_encryption_findings)
logger.info(
"Running SageMaker AutoML job network isolation check (SageMaker.15)"
)
- automl_network_isolation_findings = check_sagemaker_automl_network_isolation()
+ automl_network_isolation_findings = check_sagemaker_automl_network_isolation(
+ region=region
+ )
all_findings.append(automl_network_isolation_findings)
# Model Governance Checks
logger.info("Running model approval workflow check")
- model_approval_workflow_findings = check_model_approval_workflow()
+ model_approval_workflow_findings = check_model_approval_workflow(region=region)
all_findings.append(model_approval_workflow_findings)
logger.info("Running model drift detection check")
- model_drift_detection_findings = check_model_drift_detection()
+ model_drift_detection_findings = check_model_drift_detection(region=region)
all_findings.append(model_drift_detection_findings)
logger.info("Running A/B testing and shadow deployment check")
- ab_testing_findings = check_ab_testing_shadow_deployment()
+ ab_testing_findings = check_ab_testing_shadow_deployment(region=region)
all_findings.append(ab_testing_findings)
logger.info("Running ML lineage tracking check")
- ml_lineage_tracking_findings = check_ml_lineage_tracking()
+ ml_lineage_tracking_findings = check_ml_lineage_tracking(region=region)
all_findings.append(ml_lineage_tracking_findings)
# Generate and upload report
@@ -3843,7 +4190,7 @@ def lambda_handler(event, context):
)
logger.info("Writing reports to S3")
- s3_url = write_to_s3(execution_id, csv_content, bucket_name)
+ s3_url = write_to_s3(execution_id, csv_content, bucket_name, region=region)
return {
"statusCode": 200,
diff --git a/aiml-security-assessment/functions/security/sagemaker_assessments/schema.py b/aiml-security-assessment/functions/security/sagemaker_assessments/schema.py
index 9d4e097..16a66d9 100644
--- a/aiml-security-assessment/functions/security/sagemaker_assessments/schema.py
+++ b/aiml-security-assessment/functions/security/sagemaker_assessments/schema.py
@@ -1,13 +1,9 @@
from enum import Enum
-from typing import Dict, Any
-from pydantic import BaseModel, Field, validator
+from typing import Any, Dict
+from pydantic import BaseModel, Field, field_validator
import re
-class Config:
- strict = True # Enables strict type checking
-
-
class SeverityEnum(str, Enum):
HIGH = "High"
MEDIUM = "Medium"
@@ -39,8 +35,12 @@ class Finding(BaseModel):
Reference: str = Field(..., description="Documentation reference URL")
Severity: SeverityEnum = Field(..., description="Severity level of the finding")
Status: StatusEnum = Field(..., description="Current status of the finding")
+ Region: str = Field(
+ default="", description="AWS region where the finding was identified"
+ )
- @validator("Check_ID")
+ @field_validator("Check_ID")
+ @classmethod
def validate_check_id(cls, v):
"""Validate that Check_ID follows the pattern XX-NN (e.g., SM-01, BR-14, AC-05)"""
pattern = r"^[A-Z]{2,3}-\d{2}$"
@@ -50,21 +50,24 @@ def validate_check_id(cls, v):
)
return v
- @validator("Reference")
+ @field_validator("Reference")
+ @classmethod
def validate_reference_url(cls, v):
"""Validate that reference URL starts with https://"""
if not str(v).startswith("https://"):
raise ValueError("Reference URL must start with https://")
return v
- @validator("Severity")
+ @field_validator("Severity")
+ @classmethod
def validate_severity(cls, v):
"""Validate that severity is one of the allowed values"""
if v not in SeverityEnum.__members__.values():
raise ValueError("Severity must be one of the allowed values")
return v
- @validator("Status")
+ @field_validator("Status")
+ @classmethod
def validate_status(cls, v):
"""Validate that status is one of the allowed values"""
if v not in StatusEnum.__members__.values():
@@ -80,6 +83,7 @@ def create_finding(
reference: str,
severity: SeverityEnum,
status: StatusEnum,
+ region: str = "",
) -> Dict[str, Any]:
"""
Create a validated finding object
@@ -92,6 +96,7 @@ def create_finding(
reference: Documentation URL
severity: Severity level
status: Current status
+ region: AWS region where the finding was identified
Returns:
Dict[str, Any]: Validated finding as dictionary
@@ -107,5 +112,6 @@ def create_finding(
Reference=reference,
Severity=severity,
Status=status,
+ Region=region,
)
return dict(finding.model_dump()) # Convert to regular dictionary
diff --git a/aiml-security-assessment/statemachine/assessments.asl.json b/aiml-security-assessment/statemachine/assessments.asl.json
index 582a6eb..849e6b2 100644
--- a/aiml-security-assessment/statemachine/assessments.asl.json
+++ b/aiml-security-assessment/statemachine/assessments.asl.json
@@ -1,325 +1,390 @@
{
- "Comment": "A state machine that performs AI/ML security assessments.",
- "StartAt": "Cleanup S3 Bucket",
- "States": {
- "Cleanup S3 Bucket": {
- "Type": "Task",
- "Resource": "arn:aws:states:::lambda:invoke",
- "Parameters": {
- "FunctionName": "${CleanupBucketFunction}",
- "Payload": {
- "Execution.$": "$$.Execution",
- "StateMachine.$": "$$.StateMachine"
- }
- },
- "Retry": [
- {
- "ErrorEquals": [
- "Lambda.ServiceException",
- "Lambda.AWSLambdaException",
- "Lambda.SdkClientException",
- "Lambda.TooManyRequestsException"
- ],
- "IntervalSeconds": 1,
- "MaxAttempts": 3,
- "BackoffRate": 2,
- "JitterStrategy": "FULL"
- }
- ],
- "ResultPath": null,
- "Next": "IAM Permission Caching"
- },
- "IAM Permission Caching": {
- "Type": "Task",
- "Resource": "arn:aws:states:::lambda:invoke",
- "Parameters": {
- "FunctionName": "${IAMPermissionCachingFunction}",
- "Payload": {
- "Execution.$": "$$.Execution",
- "StateMachine.$": "$$.StateMachine"
- }
- },
- "Retry": [
- {
- "ErrorEquals": [
- "Lambda.ServiceException",
- "Lambda.AWSLambdaException",
- "Lambda.SdkClientException",
- "Lambda.TooManyRequestsException"
- ],
- "IntervalSeconds": 1,
- "MaxAttempts": 3,
- "BackoffRate": 2,
- "JitterStrategy": "FULL"
- }
- ],
- "ResultPath": null,
- "Next": "Run Security Assessments"
- },
- "Run Security Assessments": {
- "Type": "Parallel",
- "Branches": [
- {
- "StartAt": "Bedrock Security Assessment",
- "States": {
- "Bedrock Security Assessment": {
- "Type": "Task",
- "Resource": "arn:aws:states:::lambda:invoke",
- "Parameters": {
- "FunctionName": "${BedrockSecurityAssessmentFunction}",
+ "Comment": "A state machine that performs AI/ML security assessments across multiple regions.",
+ "StartAt": "Cleanup S3 Bucket",
+ "States": {
+ "Cleanup S3 Bucket": {
+ "Type": "Task",
+ "Resource": "arn:aws:states:::lambda:invoke",
+ "Parameters": {
+ "FunctionName": "${CleanupBucketFunction}",
"Payload": {
- "Execution.$": "$$.Execution",
- "StateMachine.$": "$$.StateMachine"
- }
- },
- "Retry": [
- {
- "ErrorEquals": [
- "Lambda.ServiceException",
- "Lambda.AWSLambdaException",
- "Lambda.SdkClientException",
- "Lambda.TooManyRequestsException"
- ],
- "IntervalSeconds": 1,
- "MaxAttempts": 3,
- "BackoffRate": 2,
- "JitterStrategy": "FULL"
- }
- ],
- "Catch": [
- {
- "ErrorEquals": [
- "States.ALL"
- ],
- "ResultPath": "$.assessmentError",
- "Next": "Bedrock Assessment Incomplete"
+ "Execution.$": "$$.Execution",
+ "StateMachine.$": "$$.StateMachine"
}
- ],
- "End": true
},
- "Bedrock Assessment Incomplete": {
- "Type": "Pass",
- "Result": {
- "statusCode": 207,
- "incomplete": true,
- "body": {
- "findings": []
+ "Retry": [
+ {
+ "ErrorEquals": [
+ "Lambda.ServiceException",
+ "Lambda.AWSLambdaException",
+ "Lambda.SdkClientException",
+ "Lambda.TooManyRequestsException"
+ ],
+ "IntervalSeconds": 1,
+ "MaxAttempts": 3,
+ "BackoffRate": 2,
+ "JitterStrategy": "FULL"
}
- },
- "End": true
- }
- }
+ ],
+ "ResultPath": null,
+ "Next": "IAM Permission Caching"
},
- {
- "StartAt": "Sagemaker Security Assessment",
- "States": {
- "Sagemaker Security Assessment": {
- "Type": "Task",
- "Resource": "arn:aws:states:::lambda:invoke",
- "Parameters": {
- "FunctionName": "${SagemakerSecurityAssessmentFunction}",
+ "IAM Permission Caching": {
+ "Type": "Task",
+ "Resource": "arn:aws:states:::lambda:invoke",
+ "Parameters": {
+ "FunctionName": "${IAMPermissionCachingFunction}",
"Payload": {
- "Execution.$": "$$.Execution",
- "StateMachine.$": "$$.StateMachine"
- }
- },
- "Retry": [
- {
- "ErrorEquals": [
- "Lambda.ServiceException",
- "Lambda.AWSLambdaException",
- "Lambda.SdkClientException",
- "Lambda.TooManyRequestsException"
- ],
- "IntervalSeconds": 1,
- "MaxAttempts": 3,
- "BackoffRate": 2,
- "JitterStrategy": "FULL"
+ "Execution.$": "$$.Execution",
+ "StateMachine.$": "$$.StateMachine"
}
- ],
- "Catch": [
- {
- "ErrorEquals": [
- "States.ALL"
- ],
- "ResultPath": "$.assessmentError",
- "Next": "Sagemaker Assessment Incomplete"
- }
- ],
- "End": true
},
- "Sagemaker Assessment Incomplete": {
- "Type": "Pass",
- "Result": {
- "statusCode": 207,
- "incomplete": true,
- "body": {
- "findings": []
+ "Retry": [
+ {
+ "ErrorEquals": [
+ "Lambda.ServiceException",
+ "Lambda.AWSLambdaException",
+ "Lambda.SdkClientException",
+ "Lambda.TooManyRequestsException"
+ ],
+ "IntervalSeconds": 1,
+ "MaxAttempts": 3,
+ "BackoffRate": 2,
+ "JitterStrategy": "FULL"
}
- },
- "End": true
- }
- }
+ ],
+ "ResultPath": null,
+ "Next": "Resolve Target Regions"
},
- {
- "StartAt": "AgentCore Security Assessment",
- "States": {
- "AgentCore Security Assessment": {
- "Type": "Task",
- "Resource": "arn:aws:states:::lambda:invoke",
- "Parameters": {
- "FunctionName": "${AgentCoreSecurityAssessmentFunction}",
+ "Resolve Target Regions": {
+ "Type": "Task",
+ "Resource": "arn:aws:states:::lambda:invoke",
+ "Parameters": {
+ "FunctionName": "${ResolveRegionsFunction}",
"Payload": {
- "Execution.$": "$$.Execution",
- "StateMachine.$": "$$.StateMachine"
- }
- },
- "Retry": [
- {
- "ErrorEquals": [
- "Lambda.ServiceException",
- "Lambda.AWSLambdaException",
- "Lambda.SdkClientException",
- "Lambda.TooManyRequestsException"
- ],
- "IntervalSeconds": 1,
- "MaxAttempts": 3,
- "BackoffRate": 2,
- "JitterStrategy": "FULL"
+ "Execution.$": "$$.Execution",
+ "StateMachine.$": "$$.StateMachine"
}
- ],
- "Catch": [
+ },
+ "Retry": [
{
- "ErrorEquals": [
- "States.ALL"
- ],
- "ResultPath": "$.assessmentError",
- "Next": "AgentCore Assessment Incomplete"
+ "ErrorEquals": [
+ "Lambda.ServiceException",
+ "Lambda.AWSLambdaException",
+ "Lambda.SdkClientException",
+ "Lambda.TooManyRequestsException"
+ ],
+ "IntervalSeconds": 1,
+ "MaxAttempts": 3,
+ "BackoffRate": 2,
+ "JitterStrategy": "FULL"
}
- ],
- "End": true
+ ],
+ "ResultPath": "$.ResolvedRegions",
+ "ResultSelector": {
+ "regions.$": "$.Payload.regions"
},
- "AgentCore Assessment Incomplete": {
- "Type": "Pass",
- "Result": {
- "statusCode": 207,
- "incomplete": true,
- "body": {
- "findings": []
- }
- },
- "End": true
- }
- }
+ "Next": "Scan Regions"
},
- {
- "StartAt": "FinServ Enabled?",
- "States": {
- "FinServ Enabled?": {
- "Type": "Choice",
- "Choices": [
- {
- "And": [
- {
- "Variable": "$.enableFinServ",
- "IsPresent": true
- },
- {
- "Variable": "$.enableFinServ",
- "StringEquals": "true"
+ "Scan Regions": {
+ "Type": "Map",
+ "ItemsPath": "$.ResolvedRegions.regions",
+ "ItemSelector": {
+ "Region.$": "$$.Map.Item.Value",
+ "RegionIndex.$": "$$.Map.Item.Index",
+ "Execution.$": "$$.Execution",
+ "StateMachine.$": "$$.StateMachine",
+ "OriginalInput.$": "$"
+ },
+ "MaxConcurrency": ${MaxRegionConcurrency},
+ "ItemProcessor": {
+ "ProcessorConfig": {
+ "Mode": "INLINE"
+ },
+ "StartAt": "Run Security Assessments",
+ "States": {
+ "Run Security Assessments": {
+ "Type": "Parallel",
+ "Branches": [
+ {
+ "StartAt": "Bedrock Security Assessment",
+ "States": {
+ "Bedrock Security Assessment": {
+ "Type": "Task",
+ "Resource": "arn:aws:states:::lambda:invoke",
+ "Parameters": {
+ "FunctionName": "${BedrockSecurityAssessmentFunction}",
+ "Payload": {
+ "Execution.$": "$.Execution",
+ "StateMachine.$": "$.StateMachine",
+ "Region.$": "$.Region",
+ "RegionIndex.$": "$.RegionIndex"
+ }
+ },
+ "Retry": [
+ {
+ "ErrorEquals": [
+ "Lambda.ServiceException",
+ "Lambda.AWSLambdaException",
+ "Lambda.SdkClientException",
+ "Lambda.TooManyRequestsException"
+ ],
+ "IntervalSeconds": 1,
+ "MaxAttempts": 3,
+ "BackoffRate": 2,
+ "JitterStrategy": "FULL"
+ }
+ ],
+ "Catch": [
+ {
+ "ErrorEquals": [
+ "States.ALL"
+ ],
+ "ResultPath": "$.assessmentError",
+ "Next": "Bedrock Assessment Incomplete"
+ }
+ ],
+ "End": true
+ },
+ "Bedrock Assessment Incomplete": {
+ "Type": "Pass",
+ "Result": {
+ "statusCode": 207,
+ "incomplete": true,
+ "body": {
+ "findings": []
+ }
+ },
+ "End": true
+ }
+ }
+ },
+ {
+ "StartAt": "Sagemaker Security Assessment",
+ "States": {
+ "Sagemaker Security Assessment": {
+ "Type": "Task",
+ "Resource": "arn:aws:states:::lambda:invoke",
+ "Parameters": {
+ "FunctionName": "${SagemakerSecurityAssessmentFunction}",
+ "Payload": {
+ "Execution.$": "$.Execution",
+ "StateMachine.$": "$.StateMachine",
+ "Region.$": "$.Region",
+ "RegionIndex.$": "$.RegionIndex"
+ }
+ },
+ "Retry": [
+ {
+ "ErrorEquals": [
+ "Lambda.ServiceException",
+ "Lambda.AWSLambdaException",
+ "Lambda.SdkClientException",
+ "Lambda.TooManyRequestsException"
+ ],
+ "IntervalSeconds": 1,
+ "MaxAttempts": 3,
+ "BackoffRate": 2,
+ "JitterStrategy": "FULL"
+ }
+ ],
+ "Catch": [
+ {
+ "ErrorEquals": [
+ "States.ALL"
+ ],
+ "ResultPath": "$.assessmentError",
+ "Next": "Sagemaker Assessment Incomplete"
+ }
+ ],
+ "End": true
+ },
+ "Sagemaker Assessment Incomplete": {
+ "Type": "Pass",
+ "Result": {
+ "statusCode": 207,
+ "incomplete": true,
+ "body": {
+ "findings": []
+ }
+ },
+ "End": true
+ }
+ }
+ },
+ {
+ "StartAt": "AgentCore Security Assessment",
+ "States": {
+ "AgentCore Security Assessment": {
+ "Type": "Task",
+ "Resource": "arn:aws:states:::lambda:invoke",
+ "Parameters": {
+ "FunctionName": "${AgentCoreSecurityAssessmentFunction}",
+ "Payload": {
+ "Execution.$": "$.Execution",
+ "StateMachine.$": "$.StateMachine",
+ "Region.$": "$.Region",
+ "RegionIndex.$": "$.RegionIndex"
+ }
+ },
+ "Retry": [
+ {
+ "ErrorEquals": [
+ "Lambda.ServiceException",
+ "Lambda.AWSLambdaException",
+ "Lambda.SdkClientException",
+ "Lambda.TooManyRequestsException"
+ ],
+ "IntervalSeconds": 1,
+ "MaxAttempts": 3,
+ "BackoffRate": 2,
+ "JitterStrategy": "FULL"
+ }
+ ],
+ "Catch": [
+ {
+ "ErrorEquals": [
+ "States.ALL"
+ ],
+ "ResultPath": "$.assessmentError",
+ "Next": "AgentCore Assessment Incomplete"
+ }
+ ],
+ "End": true
+ },
+ "AgentCore Assessment Incomplete": {
+ "Type": "Pass",
+ "Result": {
+ "statusCode": 207,
+ "incomplete": true,
+ "body": {
+ "findings": []
+ }
+ },
+ "End": true
+ }
+ }
+ },
+ {
+ "StartAt": "FinServ Enabled?",
+ "States": {
+ "FinServ Enabled?": {
+ "Type": "Choice",
+ "Choices": [
+ {
+ "And": [
+ {
+ "Variable": "$.OriginalInput.enableFinServ",
+ "IsPresent": true
+ },
+ {
+ "Variable": "$.OriginalInput.enableFinServ",
+ "StringEquals": "true"
+ },
+ {
+ "Variable": "$.RegionIndex",
+ "NumericEquals": 0
+ }
+ ],
+ "Next": "FinServ Security Assessment"
+ }
+ ],
+ "Default": "FinServ Assessment Skipped"
+ },
+ "FinServ Security Assessment": {
+ "Type": "Task",
+ "Resource": "arn:aws:states:::lambda:invoke",
+ "Parameters": {
+ "FunctionName": "${FinServSecurityAssessmentFunction}",
+ "Payload": {
+ "Execution.$": "$.Execution",
+ "StateMachine.$": "$.StateMachine",
+ "Region.$": "$.Region",
+ "RegionIndex.$": "$.RegionIndex",
+ "TargetRegions.$": "$.OriginalInput.ResolvedRegions.regions"
+ }
+ },
+ "Retry": [
+ {
+ "ErrorEquals": [
+ "Lambda.ServiceException",
+ "Lambda.AWSLambdaException",
+ "Lambda.SdkClientException",
+ "Lambda.TooManyRequestsException"
+ ],
+ "IntervalSeconds": 1,
+ "MaxAttempts": 3,
+ "BackoffRate": 2,
+ "JitterStrategy": "FULL"
+ }
+ ],
+ "Catch": [
+ {
+ "ErrorEquals": [
+ "States.ALL"
+ ],
+ "ResultPath": "$.finservError",
+ "Next": "FinServ Assessment Incomplete"
+ }
+ ],
+ "End": true
+ },
+ "FinServ Assessment Incomplete": {
+ "Type": "Pass",
+ "Result": {
+ "statusCode": 207,
+ "incomplete": true,
+ "body": {
+ "findings": []
+ }
+ },
+ "End": true
+ },
+ "FinServ Assessment Skipped": {
+ "Type": "Pass",
+ "Result": {
+ "statusCode": 200,
+ "skipped": true,
+ "body": {
+ "findings": []
+ }
+ },
+ "End": true
+ }
+ }
+ }
+ ],
+ "End": true
}
- ],
- "Next": "FinServ Security Assessment"
}
- ],
- "Default": "FinServ Assessment Skipped"
},
- "FinServ Security Assessment": {
- "Type": "Task",
- "Resource": "arn:aws:states:::lambda:invoke",
- "Parameters": {
- "FunctionName": "${FinServSecurityAssessmentFunction}",
+ "ResultPath": null,
+ "Next": "Generate Consolidated Report"
+ },
+ "Generate Consolidated Report": {
+ "Type": "Task",
+ "Resource": "arn:aws:states:::lambda:invoke",
+ "Parameters": {
+ "FunctionName": "${GenerateConsolidatedReportFunction}",
"Payload": {
- "Execution.$": "$$.Execution",
- "StateMachine.$": "$$.StateMachine"
- }
- },
- "Retry": [
- {
- "ErrorEquals": [
- "Lambda.ServiceException",
- "Lambda.AWSLambdaException",
- "Lambda.SdkClientException",
- "Lambda.TooManyRequestsException"
- ],
- "IntervalSeconds": 1,
- "MaxAttempts": 3,
- "BackoffRate": 2,
- "JitterStrategy": "FULL"
+ "Execution.$": "$$.Execution"
}
- ],
- "Catch": [
- {
- "ErrorEquals": [
- "States.ALL"
- ],
- "ResultPath": "$.finservError",
- "Next": "FinServ Assessment Incomplete"
- }
- ],
- "End": true
- },
- "FinServ Assessment Incomplete": {
- "Type": "Pass",
- "Result": {
- "statusCode": 207,
- "incomplete": true,
- "body": {
- "findings": []
- }
- },
- "End": true
},
- "FinServ Assessment Skipped": {
- "Type": "Pass",
- "Result": {
- "statusCode": 200,
- "skipped": true,
- "body": {
- "findings": []
+ "Retry": [
+ {
+ "ErrorEquals": [
+ "Lambda.ServiceException",
+ "Lambda.AWSLambdaException",
+ "Lambda.SdkClientException",
+ "Lambda.TooManyRequestsException"
+ ],
+ "IntervalSeconds": 1,
+ "MaxAttempts": 3,
+ "BackoffRate": 2,
+ "JitterStrategy": "FULL"
}
- },
- "End": true
- }
- }
- }
- ],
- "Next": "Generate Consolidated Report"
- },
- "Generate Consolidated Report": {
- "Type": "Task",
- "Resource": "arn:aws:states:::lambda:invoke",
- "Parameters": {
- "FunctionName": "${GenerateConsolidatedReportFunction}",
- "Payload": {
- "Execution.$": "$$.Execution"
- }
- },
- "Retry": [
- {
- "ErrorEquals": [
- "Lambda.ServiceException",
- "Lambda.AWSLambdaException",
- "Lambda.SdkClientException",
- "Lambda.TooManyRequestsException"
- ],
- "IntervalSeconds": 1,
- "MaxAttempts": 3,
- "BackoffRate": 2,
- "JitterStrategy": "FULL"
+ ],
+ "End": true
}
- ],
- "End": true
}
- }
}
diff --git a/aiml-security-assessment/template-multi-account.yaml b/aiml-security-assessment/template-multi-account.yaml
index 8ecdefc..34b054e 100644
--- a/aiml-security-assessment/template-multi-account.yaml
+++ b/aiml-security-assessment/template-multi-account.yaml
@@ -5,6 +5,45 @@ Description: >
Multi-account SAM Template for aiml-security-assessment
+Metadata:
+ AWS::CloudFormation::Interface:
+ ParameterGroups:
+ - Label:
+ default: Assessment Options
+ Parameters:
+ - TargetRegions
+ - MaxRegionConcurrency
+ ParameterLabels:
+ TargetRegions:
+ default: AWS regions to scan
+ MaxRegionConcurrency:
+ default: Regional scan concurrency
+
+Parameters:
+ TargetRegions:
+ Type: String
+ Default: ""
+ AllowedPattern: "^$|^all$|^[a-z]{2}(-gov)?-[a-z]+-[0-9]([,\\s]+[a-z]{2}(-gov)?-[a-z]+-[0-9])*$"
+ ConstraintDescription: >
+ TargetRegions must be empty, 'all', or a comma- or space-separated list of AWS
+ region names (for example, us-east-1,us-west-2,eu-west-1 or
+ us-east-1 us-west-2 eu-west-1).
+ Description: >
+ Comma- or space-separated list of AWS regions to scan (e.g.,
+ us-east-1,us-west-2,eu-west-1 or us-east-1 us-west-2 eu-west-1).
+ Use 'all' to scan all regions where the service is available.
+ Leave empty to scan only the deployment region.
+ MaxRegionConcurrency:
+ Type: Number
+ Default: 5
+ MinValue: 1
+ MaxValue: 40
+ Description: >
+ Maximum number of regions scanned in parallel by the Map state. Lower
+ values reduce concurrent Lambda usage; higher values speed up large
+ multi-region scans. Set to 1 for fully sequential scanning.
+ ConstraintDescription: Must be between 1 and 40.
+
Resources:
AIMLAssessmentStateMachine:
Type: AWS::Serverless::StateMachine
@@ -13,19 +52,23 @@ Resources:
DefinitionSubstitutions:
CleanupBucketFunction: !GetAtt CleanupBucketFunction.Arn
IAMPermissionCachingFunction: !GetAtt IAMPermissionCachingFunction.Arn
+ ResolveRegionsFunction: !GetAtt ResolveRegionsFunction.Arn
BedrockSecurityAssessmentFunction: !GetAtt BedrockSecurityAssessmentFunction.Arn
SagemakerSecurityAssessmentFunction: !GetAtt SagemakerSecurityAssessmentFunction.Arn
AgentCoreSecurityAssessmentFunction: !GetAtt AgentCoreSecurityAssessmentFunction.Arn
FinServSecurityAssessmentFunction: !GetAtt FinServSecurityAssessmentFunction.Arn
GenerateConsolidatedReportFunction: !GetAtt GenerateConsolidatedReportFunction.Arn
AIMLAssessmentBucketName: !Ref AIMLAssessmentBucket
+ MaxRegionConcurrency: !Ref MaxRegionConcurrency
Policies:
- LambdaInvokePolicy:
FunctionName: !Ref CleanupBucketFunction
- - LambdaInvokePolicy:
- FunctionName: !Ref BedrockSecurityAssessmentFunction
- LambdaInvokePolicy:
FunctionName: !Ref IAMPermissionCachingFunction
+ - LambdaInvokePolicy:
+ FunctionName: !Ref ResolveRegionsFunction
+ - LambdaInvokePolicy:
+ FunctionName: !Ref BedrockSecurityAssessmentFunction
- LambdaInvokePolicy:
FunctionName: !Ref SagemakerSecurityAssessmentFunction
- LambdaInvokePolicy:
@@ -37,10 +80,25 @@ Resources:
- S3CrudPolicy:
BucketName: !Ref AIMLAssessmentBucket
+ ResolveRegionsFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ FunctionName: !Sub 'aiml-security-${AWS::StackName}-ResolveRegions'
+ CodeUri: functions/security/resolve_regions/
+ Handler: app.lambda_handler
+ Runtime: python3.12
+ Architectures:
+ - x86_64
+ Timeout: 60
+ MemorySize: 256
+ Environment:
+ Variables:
+ TARGET_REGIONS: !Ref TargetRegions
+
CleanupBucketFunction:
Type: AWS::Serverless::Function
Properties:
- FunctionName: !Sub 'aiml-security-CleanupBucket'
+ FunctionName: !Sub 'aiml-security-${AWS::StackName}-CleanupBucket'
CodeUri: functions/security/cleanup_bucket/
Handler: app.lambda_handler
Runtime: python3.12
@@ -58,7 +116,7 @@ Resources:
IAMPermissionCachingFunction:
Type: AWS::Serverless::Function
Properties:
- FunctionName: !Sub 'aiml-security-IAMPermissionCaching'
+ FunctionName: !Sub 'aiml-security-${AWS::StackName}-IAMPermissionCaching'
CodeUri: functions/security/iam_permission_caching/
Handler: app.lambda_handler
Runtime: python3.12
@@ -94,7 +152,7 @@ Resources:
GenerateConsolidatedReportFunction:
Type: AWS::Serverless::Function
Properties:
- FunctionName: !Sub 'aiml-security-GenerateReport'
+ FunctionName: !Sub 'aiml-security-${AWS::StackName}-GenerateReport'
CodeUri: functions/security/generate_consolidated_report/
Handler: app.lambda_handler
Runtime: python3.12
@@ -112,7 +170,7 @@ Resources:
BedrockSecurityAssessmentFunction:
Type: AWS::Serverless::Function
Properties:
- FunctionName: !Sub 'aiml-security-BedrockAssessment'
+ FunctionName: !Sub 'aiml-security-${AWS::StackName}-BedrockAssessment'
CodeUri: functions/security/bedrock_assessments/
Handler: app.lambda_handler
Runtime: python3.12
@@ -123,6 +181,7 @@ Resources:
Environment:
Variables:
AIML_ASSESSMENT_BUCKET_NAME: !Ref AIMLAssessmentBucket
+ TARGET_REGIONS: !Ref TargetRegions
Policies:
- S3CrudPolicy:
BucketName: !Ref AIMLAssessmentBucket
@@ -199,7 +258,7 @@ Resources:
SagemakerSecurityAssessmentFunction:
Type: AWS::Serverless::Function
Properties:
- FunctionName: !Sub 'aiml-security-SagemakerAssessment'
+ FunctionName: !Sub 'aiml-security-${AWS::StackName}-SagemakerAssessment'
CodeUri: functions/security/sagemaker_assessments/
Handler: app.lambda_handler
Runtime: python3.12
@@ -210,6 +269,7 @@ Resources:
Environment:
Variables:
AIML_ASSESSMENT_BUCKET_NAME: !Ref AIMLAssessmentBucket
+ TARGET_REGIONS: !Ref TargetRegions
Policies:
- S3CrudPolicy:
BucketName: !Ref AIMLAssessmentBucket
@@ -250,6 +310,7 @@ Resources:
- kms:ListAliases
- sagemaker:ListModelPackageGroups
- sagemaker:ListModelPackages
+ - sagemaker:DescribeModelPackage
- sagemaker:ListFeatureGroups
- sagemaker:DescribeFeatureGroup
- sagemaker:ListPipelines
@@ -273,7 +334,9 @@ Resources:
- sagemaker:ListAutoMLJobs
- sagemaker:DescribeAutoMLJob
- sagemaker:ListExperiments
+ - sagemaker:DescribeExperiment
- sagemaker:ListTrials
+ - sagemaker:DescribeTrial
- sagemaker:ListAssociations
Resource: '*'
- Statement:
@@ -303,7 +366,7 @@ Resources:
AgentCoreSecurityAssessmentFunction:
Type: AWS::Serverless::Function
Properties:
- FunctionName: !Sub 'aiml-security-AgentCoreAssessment'
+ FunctionName: !Sub 'aiml-security-${AWS::StackName}-AgentCoreAssessment'
CodeUri: functions/security/agentcore_assessments/
Handler: app.lambda_handler
Runtime: python3.12
@@ -314,6 +377,7 @@ Resources:
Environment:
Variables:
AIML_ASSESSMENT_BUCKET_NAME: !Ref AIMLAssessmentBucket
+ TARGET_REGIONS: !Ref TargetRegions
Policies:
- S3CrudPolicy:
BucketName: !Ref AIMLAssessmentBucket
diff --git a/aiml-security-assessment/template.yaml b/aiml-security-assessment/template.yaml
index 673d63d..c6bd63d 100644
--- a/aiml-security-assessment/template.yaml
+++ b/aiml-security-assessment/template.yaml
@@ -5,6 +5,45 @@ Description: >
SAM Template for aiml-security-assessment
+Metadata:
+ AWS::CloudFormation::Interface:
+ ParameterGroups:
+ - Label:
+ default: Assessment Options
+ Parameters:
+ - TargetRegions
+ - MaxRegionConcurrency
+ ParameterLabels:
+ TargetRegions:
+ default: AWS regions to scan
+ MaxRegionConcurrency:
+ default: Regional scan concurrency
+
+Parameters:
+ TargetRegions:
+ Type: String
+ Default: ""
+ AllowedPattern: "^$|^all$|^[a-z]{2}(-gov)?-[a-z]+-[0-9]([,\\s]+[a-z]{2}(-gov)?-[a-z]+-[0-9])*$"
+ ConstraintDescription: >
+ TargetRegions must be empty, 'all', or a comma- or space-separated list of AWS
+ region names (for example, us-east-1,us-west-2,eu-west-1 or
+ us-east-1 us-west-2 eu-west-1).
+ Description: >
+ Comma- or space-separated list of AWS regions to scan (e.g.,
+ us-east-1,us-west-2,eu-west-1 or us-east-1 us-west-2 eu-west-1).
+ Use 'all' to scan all regions where the service is available.
+ Leave empty to scan only the deployment region.
+ MaxRegionConcurrency:
+ Type: Number
+ Default: 5
+ MinValue: 1
+ MaxValue: 40
+ Description: >
+ Maximum number of regions scanned in parallel by the Map state. Lower
+ values reduce concurrent Lambda usage; higher values speed up large
+ multi-region scans. Set to 1 for fully sequential scanning.
+ ConstraintDescription: Must be between 1 and 40.
+
Resources:
AIMLAssessmentStateMachine:
Type: AWS::Serverless::StateMachine # More info about State Machine Resource: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-statemachine.html
@@ -13,20 +52,23 @@ Resources:
DefinitionSubstitutions:
CleanupBucketFunction: !GetAtt CleanupBucketFunction.Arn
IAMPermissionCachingFunction: !GetAtt IAMPermissionCachingFunction.Arn
+ ResolveRegionsFunction: !GetAtt ResolveRegionsFunction.Arn
BedrockSecurityAssessmentFunction: !GetAtt BedrockSecurityAssessmentFunction.Arn
SagemakerSecurityAssessmentFunction: !GetAtt SagemakerSecurityAssessmentFunction.Arn
AgentCoreSecurityAssessmentFunction: !GetAtt AgentCoreSecurityAssessmentFunction.Arn
FinServSecurityAssessmentFunction: !GetAtt FinServSecurityAssessmentFunction.Arn
GenerateConsolidatedReportFunction: !GetAtt GenerateConsolidatedReportFunction.Arn
AIMLAssessmentBucketName: !Ref AIMLAssessmentBucket
+ MaxRegionConcurrency: !Ref MaxRegionConcurrency
Policies:
- # Find out more about SAM policy templates: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-policy-templates.html
- LambdaInvokePolicy:
FunctionName: !Ref CleanupBucketFunction
- - LambdaInvokePolicy:
- FunctionName: !Ref BedrockSecurityAssessmentFunction
- LambdaInvokePolicy:
FunctionName: !Ref IAMPermissionCachingFunction
+ - LambdaInvokePolicy:
+ FunctionName: !Ref ResolveRegionsFunction
+ - LambdaInvokePolicy:
+ FunctionName: !Ref BedrockSecurityAssessmentFunction
- LambdaInvokePolicy:
FunctionName: !Ref SagemakerSecurityAssessmentFunction
- LambdaInvokePolicy:
@@ -35,10 +77,24 @@ Resources:
FunctionName: !Ref FinServSecurityAssessmentFunction
- LambdaInvokePolicy:
FunctionName: !Ref GenerateConsolidatedReportFunction
- #Add S3 Read Write policy
- S3CrudPolicy:
BucketName: !Ref AIMLAssessmentBucket
+ ResolveRegionsFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ FunctionName: !Sub 'aiml-security-${AWS::StackName}-ResolveRegions'
+ CodeUri: functions/security/resolve_regions/
+ Handler: app.lambda_handler
+ Runtime: python3.12
+ Architectures:
+ - x86_64
+ Timeout: 60
+ MemorySize: 256
+ Environment:
+ Variables:
+ TARGET_REGIONS: !Ref TargetRegions
+
CleanupBucketFunction:
Type: AWS::Serverless::Function
Properties:
@@ -106,6 +162,7 @@ Resources:
Environment:
Variables:
AIML_ASSESSMENT_BUCKET_NAME: !Ref AIMLAssessmentBucket
+ TARGET_REGIONS: !Ref TargetRegions
Policies:
- S3CrudPolicy:
BucketName: !Ref AIMLAssessmentBucket
@@ -118,12 +175,12 @@ Resources:
Runtime: python3.12
Architectures:
- x86_64
- # Add S3 Read Write policy
Timeout: 600 # 10 minutes
MemorySize: 1024
Environment:
Variables:
AIML_ASSESSMENT_BUCKET_NAME: !Ref AIMLAssessmentBucket
+ TARGET_REGIONS: !Ref TargetRegions
Policies:
- S3CrudPolicy:
BucketName: !Ref AIMLAssessmentBucket
@@ -204,12 +261,12 @@ Resources:
Runtime: python3.12
Architectures:
- x86_64
- # Add S3 Read Write policy
Timeout: 600 # 10 minutes
MemorySize: 1024
Environment:
Variables:
AIML_ASSESSMENT_BUCKET_NAME: !Ref AIMLAssessmentBucket
+ TARGET_REGIONS: !Ref TargetRegions
Policies:
- S3CrudPolicy:
BucketName: !Ref AIMLAssessmentBucket
@@ -250,6 +307,7 @@ Resources:
- kms:ListAliases
- sagemaker:ListModelPackageGroups
- sagemaker:ListModelPackages
+ - sagemaker:DescribeModelPackage
- sagemaker:ListFeatureGroups
- sagemaker:DescribeFeatureGroup
- sagemaker:ListPipelines
@@ -273,7 +331,9 @@ Resources:
- sagemaker:ListAutoMLJobs
- sagemaker:DescribeAutoMLJob
- sagemaker:ListExperiments
+ - sagemaker:DescribeExperiment
- sagemaker:ListTrials
+ - sagemaker:DescribeTrial
- sagemaker:ListAssociations
Resource: '*'
- Statement:
@@ -314,6 +374,7 @@ Resources:
Environment:
Variables:
AIML_ASSESSMENT_BUCKET_NAME: !Ref AIMLAssessmentBucket
+ TARGET_REGIONS: !Ref TargetRegions
Policies:
- S3CrudPolicy:
BucketName: !Ref AIMLAssessmentBucket
@@ -410,6 +471,7 @@ Resources:
Environment:
Variables:
AIML_ASSESSMENT_BUCKET_NAME: !Ref AIMLAssessmentBucket
+ TARGET_REGIONS: !Ref TargetRegions
Policies:
- S3CrudPolicy:
BucketName: !Ref AIMLAssessmentBucket
diff --git a/buildspec-modular-example.yml b/buildspec-modular-example.yml
deleted file mode 100644
index 2e64946..0000000
--- a/buildspec-modular-example.yml
+++ /dev/null
@@ -1,189 +0,0 @@
-version: 0.2
-# AIML Security Modular Assessment Buildspec Example
-# This shows how to configure selective deployment of assessment modules
-
-phases:
- install:
- runtime-versions:
- python: 3.12
- commands:
- - pip install -q --upgrade pip
- - pip install -q --upgrade aws-sam-cli
-
- build:
- commands:
- - echo "Starting AIML Security Modular Assessment"
- - echo "Multi-account scan is $MULTI_ACCOUNT_SCAN"
-
- # Assessment module configuration
- - export DEPLOY_AIML_ASSESSMENT=${DEPLOY_AIML_ASSESSMENT:-true}
- - export DEPLOY_SECURITY_ASSESSMENT=${DEPLOY_SECURITY_ASSESSMENT:-false}
- - export DEPLOY_RESILIENCE_ASSESSMENT=${DEPLOY_RESILIENCE_ASSESSMENT:-false}
- - export DEPLOY_COST_ASSESSMENT=${DEPLOY_COST_ASSESSMENT:-false}
-
- - echo "Assessment modules to deploy:"
- - echo " AI/ML: $DEPLOY_AIML_ASSESSMENT"
- - echo " Security: $DEPLOY_SECURITY_ASSESSMENT"
- - echo " Resilience: $DEPLOY_RESILIENCE_ASSESSMENT"
- - echo " Cost: $DEPLOY_COST_ASSESSMENT"
-
- # Get account list for multi-account deployment
- - |-
- if [[ $MULTI_ACCOUNT_SCAN = 'true' ]]; then
- echo "Getting list of accounts to scan"
- if [[ $MULTI_ACCOUNT_LIST_OVERRIDE != '' ]]; then
- account_list=$MULTI_ACCOUNT_LIST_OVERRIDE
- else
- account_list=$(aws organizations list-accounts --query 'Accounts[?Status==`ACTIVE`].Id' --output text)
- fi
- echo "Will scan accounts: $account_list"
-
- # Deploy to each member account
- for accountId in $account_list; do
- echo "Processing account $accountId"
- if [[ $accountId == $AWS_ACCOUNT_ID ]]; then
- echo "Skipping management account $accountId - will be handled separately"
- continue
- fi
-
- # Assume role in target account
- aws sts assume-role --role-arn arn:$AWS_PARTITION:iam::$accountId:role/service-role/$MEMBER_ROLE --role-session-name AIMLSecurityAssessment > /tmp/creds.json || continue
- export AWS_ACCESS_KEY_ID=$(cat /tmp/creds.json | jq -r '.Credentials.AccessKeyId')
- export AWS_SECRET_ACCESS_KEY=$(cat /tmp/creds.json | jq -r '.Credentials.SecretAccessKey')
- export AWS_SESSION_TOKEN=$(cat /tmp/creds.json | jq -r '.Credentials.SessionToken')
-
- echo "Deploying assessment modules to account $accountId"
-
- # Deploy AI/ML Assessment
- if [[ $DEPLOY_AIML_ASSESSMENT = 'true' ]]; then
- echo "Deploying AI/ML Assessment to $accountId"
- cd aiml-security-assessment
- sam build --use-container --template template.yaml
- sam deploy --template-file .aws-sam/build/template.yaml \
- --stack-name aiml-security-$accountId \
- --capabilities CAPABILITY_IAM \
- --no-confirm-changeset \
- --resolve-s3 \
- --parameter-overrides BucketName=$BUCKET_REPORT \
- --region $AWS_DEFAULT_REGION || echo "AI/ML deploy failed for $accountId"
-
- # Start AI/ML assessment
- STATE_MACHINE_ARN=$(aws cloudformation describe-stacks --stack-name aiml-security-$accountId --query 'Stacks[0].Outputs[?OutputKey==`AIMLAssessmentStateMachineArn`].OutputValue' --output text 2>/dev/null)
- if [[ $STATE_MACHINE_ARN != "" ]]; then
- aws stepfunctions start-execution --state-machine-arn $STATE_MACHINE_ARN --input "{\"accountId\":\"$accountId\"}" --query 'executionArn' --output text
- echo "Started AI/ML assessment for $accountId"
- fi
- cd ..
- fi
-
- # Deploy Security Assessment
- if [[ $DEPLOY_SECURITY_ASSESSMENT = 'true' ]]; then
- echo "Deploying Security Assessment to $accountId"
- cd security-assessment
- sam build --use-container --template template.yaml
- sam deploy --template-file .aws-sam/build/template.yaml \
- --stack-name security-$accountId \
- --capabilities CAPABILITY_IAM \
- --no-confirm-changeset \
- --resolve-s3 \
- --parameter-overrides BucketName=$BUCKET_REPORT \
- --region $AWS_DEFAULT_REGION || echo "Security deploy failed for $accountId"
-
- # Start Security assessment
- STATE_MACHINE_ARN=$(aws cloudformation describe-stacks --stack-name security-$accountId --query 'Stacks[0].Outputs[?OutputKey==`SecurityAssessmentStateMachineArn`].OutputValue' --output text 2>/dev/null)
- if [[ $STATE_MACHINE_ARN != "" ]]; then
- aws stepfunctions start-execution --state-machine-arn $STATE_MACHINE_ARN --input "{\"accountId\":\"$accountId\"}" --query 'executionArn' --output text
- echo "Started Security assessment for $accountId"
- fi
- cd ..
- fi
-
- # Deploy Resilience Assessment
- if [[ $DEPLOY_RESILIENCE_ASSESSMENT = 'true' ]]; then
- echo "Deploying Resilience Assessment to $accountId"
- cd resilience-assessment
- sam build --use-container --template template.yaml
- sam deploy --template-file .aws-sam/build/template.yaml \
- --stack-name resilience-$accountId \
- --capabilities CAPABILITY_IAM \
- --no-confirm-changeset \
- --resolve-s3 \
- --parameter-overrides BucketName=$BUCKET_REPORT \
- --region $AWS_DEFAULT_REGION || echo "Resilience deploy failed for $accountId"
-
- # Start Resilience assessment
- STATE_MACHINE_ARN=$(aws cloudformation describe-stacks --stack-name resilience-$accountId --query 'Stacks[0].Outputs[?OutputKey==`ResilienceAssessmentStateMachineArn`].OutputValue' --output text 2>/dev/null)
- if [[ $STATE_MACHINE_ARN != "" ]]; then
- aws stepfunctions start-execution --state-machine-arn $STATE_MACHINE_ARN --input "{\"accountId\":\"$accountId\"}" --query 'executionArn' --output text
- echo "Started Resilience assessment for $accountId"
- fi
- cd ..
- fi
-
- # Deploy Cost Assessment
- if [[ $DEPLOY_COST_ASSESSMENT = 'true' ]]; then
- echo "Deploying Cost Assessment to $accountId"
- cd cost-assessment
- sam build --use-container --template template.yaml
- sam deploy --template-file .aws-sam/build/template.yaml \
- --stack-name cost-$accountId \
- --capabilities CAPABILITY_IAM \
- --no-confirm-changeset \
- --resolve-s3 \
- --parameter-overrides BucketName=$BUCKET_REPORT \
- --region $AWS_DEFAULT_REGION || echo "Cost deploy failed for $accountId"
-
- # Start Cost assessment
- STATE_MACHINE_ARN=$(aws cloudformation describe-stacks --stack-name cost-$accountId --query 'Stacks[0].Outputs[?OutputKey==`CostAssessmentStateMachineArn`].OutputValue' --output text 2>/dev/null)
- if [[ $STATE_MACHINE_ARN != "" ]]; then
- aws stepfunctions start-execution --state-machine-arn $STATE_MACHINE_ARN --input "{\"accountId\":\"$accountId\"}" --query 'executionArn' --output text
- echo "Started Cost assessment for $accountId"
- fi
- cd ..
- fi
-
- # Clear credentials
- unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
- done
-
- # Deploy to management account
- echo "Deploying to management account $AWS_ACCOUNT_ID"
- # Similar deployment logic for management account...
-
- else
- echo "Single account deployment"
- # Deploy selected modules to current account
- if [[ $DEPLOY_AIML_ASSESSMENT = 'true' ]]; then
- cd aiml-security-assessment
- sam build --use-container
- sam deploy --template-file .aws-sam/build/template.yaml \
- --stack-name aiml-security \
- --capabilities CAPABILITY_IAM \
- --no-confirm-changeset \
- --s3-bucket $BUCKET_REPORT \
- --parameter-overrides BucketName=$BUCKET_REPORT
- cd ..
- fi
- # Add similar blocks for other assessment types...
- fi
-
- post_build:
- commands:
- - echo "Assessment completed. Results in S3:$BUCKET_REPORT"
- - echo "Generating consolidated multi-module report"
-
- # Consolidate results from all deployed modules
- - |-
- if [[ $MULTI_ACCOUNT_SCAN = 'true' ]]; then
- pip3 install -q beautifulsoup4
-
- # Wait for all assessments to complete and collect results
- # This would include logic to:
- # 1. Wait for Step Functions to complete
- # 2. Collect results from each module type
- # 3. Generate consolidated report across all assessment types
- # 4. Upload to central S3 bucket organized by assessment type
-
- python3 consolidate_multi_module_reports.py
- fi
- - echo "Multi-module assessment and consolidation completed"
diff --git a/buildspec.yml b/buildspec.yml
index 0dbf302..5c7eb46 100644
--- a/buildspec.yml
+++ b/buildspec.yml
@@ -45,6 +45,44 @@ phases:
echo "ERROR: Missing required environment variables: ${missing_vars[*]}"
exit 1
fi
+ - |
+ normalize_delimited_list() {
+ local value="$1"
+ value=$(printf '%s' "$value" | sed -E 's/[[:space:]]*,[[:space:]]*/,/g; s/^[[:space:]]+//; s/[[:space:]]+$//; s/[[:space:]]+/,/g; s/,+/,/g; s/^,//; s/,$//')
+ printf '%s' "$value"
+ }
+
+ # Accept comma- or whitespace-delimited input from CloudFormation, then
+ # normalize to comma-delimited values for SAM and Lambda configuration.
+ TARGET_REGIONS=$(normalize_delimited_list "${TARGET_REGIONS:-}")
+ if [[ "$(printf '%s' "$TARGET_REGIONS" | tr '[:upper:]' '[:lower:]')" == "all" ]]; then
+ TARGET_REGIONS="all"
+ fi
+ export TARGET_REGIONS
+
+ MULTI_ACCOUNT_LIST_OVERRIDE=$(normalize_delimited_list "${MULTI_ACCOUNT_LIST_OVERRIDE:-}")
+ export MULTI_ACCOUNT_LIST_OVERRIDE
+
+ # Validate the normalized value matches the CloudFormation AllowedPattern.
+ region_pattern='^all$|^[a-z]{2}(-gov)?-[a-z]+-[0-9](,[a-z]{2}(-gov)?-[a-z]+-[0-9])*$'
+ if [[ -n "$TARGET_REGIONS" && ! "$TARGET_REGIONS" =~ $region_pattern ]]; then
+ echo "ERROR: TARGET_REGIONS contains invalid characters: '$TARGET_REGIONS'"
+ echo "Expected a comma- or space-separated list of region names (e.g., us-east-1,us-west-2), 'all', or empty."
+ exit 1
+ fi
+ echo "Using TARGET_REGIONS: '${TARGET_REGIONS:-}'"
+
+ account_list_pattern='^[0-9]{12}(,[0-9]{12})*$'
+ if [[ -n "$MULTI_ACCOUNT_LIST_OVERRIDE" && ! "$MULTI_ACCOUNT_LIST_OVERRIDE" =~ $account_list_pattern ]]; then
+ echo "ERROR: MULTI_ACCOUNT_LIST_OVERRIDE contains invalid account IDs: '$MULTI_ACCOUNT_LIST_OVERRIDE'"
+ echo "Expected a comma- or space-separated list of 12-digit AWS account IDs, or empty."
+ exit 1
+ fi
+
+ # Use the long-form SAM parameter syntax so an intentionally empty
+ # TargetRegions value is accepted and clears any previous stack value.
+ SAM_TARGET_REGIONS_PARAMETER="ParameterKey=TargetRegions,ParameterValue=${TARGET_REGIONS}"
+ export SAM_TARGET_REGIONS_PARAMETER
- echo "Multi-account scan is $MULTI_ACCOUNT_SCAN"
- cd aiml-security-assessment
- ls -la
@@ -69,7 +107,7 @@ phases:
echo "Getting list of accounts to scan"
if [[ $MULTI_ACCOUNT_LIST_OVERRIDE != '' ]]; then
echo "Using account overrides."
- account_list=$MULTI_ACCOUNT_LIST_OVERRIDE
+ account_list=${MULTI_ACCOUNT_LIST_OVERRIDE//,/ }
else
echo "Using accounts from AWS Organizations."
if ! account_list=$(aws organizations list-accounts --query 'Accounts[?Status==`ACTIVE`].Id' --output text); then
@@ -120,7 +158,7 @@ phases:
fi
echo "Deploying to account $accountId"
- if ! sam deploy --template-file .aws-sam/build/template.yaml --stack-name aiml-security-$accountId --capabilities CAPABILITY_IAM --no-confirm-changeset --no-fail-on-empty-changeset --resolve-s3 --region $AWS_DEFAULT_REGION; then
+ if ! sam deploy --template-file .aws-sam/build/template.yaml --stack-name "aiml-security-$accountId" --capabilities CAPABILITY_IAM --no-confirm-changeset --no-fail-on-empty-changeset --resolve-s3 --region "$AWS_DEFAULT_REGION" --parameter-overrides "$SAM_TARGET_REGIONS_PARAMETER"; then
echo "ERROR: Deploy failed for account $accountId"
failed_accounts+=($accountId)
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
@@ -151,7 +189,7 @@ phases:
aws cloudformation delete-stack --stack-name aws-sam-cli-managed-default
aws cloudformation wait stack-delete-complete --stack-name aws-sam-cli-managed-default 2>/dev/null || true
fi
- if ! sam deploy --template-file .aws-sam/build/template.yaml --stack-name aiml-security-mgmt --capabilities CAPABILITY_IAM --no-confirm-changeset --no-fail-on-empty-changeset --resolve-s3 --region $AWS_DEFAULT_REGION; then
+ if ! sam deploy --template-file .aws-sam/build/template.yaml --stack-name aiml-security-mgmt --capabilities CAPABILITY_IAM --no-confirm-changeset --no-fail-on-empty-changeset --resolve-s3 --region "$AWS_DEFAULT_REGION" --parameter-overrides "$SAM_TARGET_REGIONS_PARAMETER"; then
echo "ERROR: Deploy failed for management account"
failed_accounts+=($AWS_ACCOUNT_ID)
else
@@ -176,7 +214,7 @@ phases:
else
echo "Single account deployment"
STACK_NAME="aiml-sec-${AWS_ACCOUNT_ID}"
- if ! sam deploy --template-file .aws-sam/build/template.yaml --stack-name $STACK_NAME --capabilities CAPABILITY_IAM --no-confirm-changeset --no-fail-on-empty-changeset --resolve-s3; then
+ if ! sam deploy --template-file .aws-sam/build/template.yaml --stack-name "$STACK_NAME" --capabilities CAPABILITY_IAM --no-confirm-changeset --no-fail-on-empty-changeset --resolve-s3 --region "$AWS_DEFAULT_REGION" --parameter-overrides "$SAM_TARGET_REGIONS_PARAMETER"; then
echo "ERROR: SAM deploy failed for single account"
exit 1
fi
@@ -206,10 +244,27 @@ phases:
echo "Assessment completed. Results in S3:$BUCKET_REPORT"
+ # Derive how long to wait for Step Function completion from the overall
+ # CodeBuild timeout, reserving a buffer for the build/deploy work that runs
+ # before this loop and the report processing that runs after it. An explicit
+ # SF_POLL_TIMEOUT override always wins; a floor guards against tiny/negative
+ # values when BUILD_TIMEOUT_MINUTES is small.
+ SF_POLL_BUFFER_SECONDS=${SF_POLL_BUFFER_SECONDS:-600}
+ SF_POLL_FLOOR_SECONDS=${SF_POLL_FLOOR_SECONDS:-300}
+ if [[ -n "$SF_POLL_TIMEOUT" ]]; then
+ sf_poll_timeout=$SF_POLL_TIMEOUT
+ else
+ sf_poll_timeout=$(( ${BUILD_TIMEOUT_MINUTES:-60} * 60 - SF_POLL_BUFFER_SECONDS ))
+ if [[ $sf_poll_timeout -lt $SF_POLL_FLOOR_SECONDS ]]; then
+ sf_poll_timeout=$SF_POLL_FLOOR_SECONDS
+ fi
+ fi
+ echo "Step Function poll timeout set to ${sf_poll_timeout}s (CodeBuild timeout: ${BUILD_TIMEOUT_MINUTES:-60}m, buffer: ${SF_POLL_BUFFER_SECONDS}s, floor: ${SF_POLL_FLOOR_SECONDS}s)"
+
if [[ $MULTI_ACCOUNT_SCAN = 'true' ]]; then
echo "Getting list of accounts for post-build processing"
if [[ $MULTI_ACCOUNT_LIST_OVERRIDE != '' ]]; then
- account_list=$MULTI_ACCOUNT_LIST_OVERRIDE
+ account_list=${MULTI_ACCOUNT_LIST_OVERRIDE//,/ }
else
if ! account_list=$(aws organizations list-accounts --query 'Accounts[?Status==`ACTIVE`].Id' --output text); then
echo "ERROR: Failed to retrieve account list from AWS Organizations"
@@ -220,6 +275,7 @@ phases:
# Track failures
upload_failed=false
+ sf_failed=false
failed_accounts=()
# Clean up any existing account-files directory to ensure fresh start
@@ -262,14 +318,15 @@ phases:
EXECUTION_ARN=$(cat /tmp/execution_arns/$accountId.txt)
echo "Waiting for Step Function execution in account $accountId: $EXECUTION_ARN"
if [[ $EXECUTION_ARN != "" && $EXECUTION_ARN != "None" ]]; then
- timeout=${SF_POLL_TIMEOUT:-600}
+ timeout=$sf_poll_timeout
elapsed=0
while [[ $elapsed -lt $timeout ]]; do
STATUS=$(aws stepfunctions describe-execution --execution-arn $EXECUTION_ARN --query 'status' --output text 2>/dev/null)
if [[ $STATUS == "SUCCEEDED" || $STATUS == "FAILED" || $STATUS == "TIMED_OUT" || $STATUS == "ABORTED" ]]; then
echo "Step Function for account $accountId completed with status: $STATUS"
if [[ $STATUS == "FAILED" || $STATUS == "TIMED_OUT" || $STATUS == "ABORTED" ]]; then
- echo "WARNING: Step Function did not succeed for account $accountId"
+ echo "ERROR: Step Function did not succeed for account $accountId (status: $STATUS)"
+ sf_failed=true
fi
break
fi
@@ -354,6 +411,11 @@ phases:
exit 1
fi
+ if [[ $sf_failed == true ]]; then
+ echo "ERROR: One or more Step Function executions failed. Check Step Functions console for details."
+ exit 1
+ fi
+
echo "==========================================="
echo "Multi-account assessment completed successfully!"
echo "==========================================="
@@ -366,20 +428,22 @@ phases:
# Single account post-build: wait for Step Function and report status
echo "Single account post-build processing"
STACK_NAME="aiml-sec-${AWS_ACCOUNT_ID}"
+ sf_failed=false
# Read the execution ARN saved from build phase
if [[ -f /tmp/single_account_execution_arn.txt ]]; then
EXECUTION_ARN=$(cat /tmp/single_account_execution_arn.txt)
echo "Waiting for Step Function execution: $EXECUTION_ARN"
if [[ $EXECUTION_ARN != "" && $EXECUTION_ARN != "None" ]]; then
- timeout=${SF_POLL_TIMEOUT:-600}
+ timeout=$sf_poll_timeout
elapsed=0
while [[ $elapsed -lt $timeout ]]; do
STATUS=$(aws stepfunctions describe-execution --execution-arn $EXECUTION_ARN --query 'status' --output text 2>/dev/null)
if [[ $STATUS == "SUCCEEDED" || $STATUS == "FAILED" || $STATUS == "TIMED_OUT" || $STATUS == "ABORTED" ]]; then
echo "Step Function completed with status: $STATUS"
if [[ $STATUS != "SUCCEEDED" ]]; then
- echo "WARNING: Step Function did not succeed"
+ echo "ERROR: Step Function execution failed with status: $STATUS"
+ sf_failed=true
fi
break
fi
@@ -388,11 +452,13 @@ phases:
elapsed=$((elapsed + 30))
done
if [[ $elapsed -ge $timeout ]]; then
- echo "WARNING: Timeout waiting for Step Function completion"
+ echo "ERROR: Timeout waiting for Step Function completion"
+ sf_failed=true
fi
fi
else
- echo "WARNING: No execution ARN file found from build phase"
+ echo "ERROR: No execution ARN file found from build phase"
+ sf_failed=true
fi
# Get assessment bucket and sync results to BUCKET_REPORT for consistency
@@ -416,6 +482,11 @@ phases:
else
echo "WARNING: Could not determine assessment bucket from stack outputs"
fi
+
+ if [[ $sf_failed == true ]]; then
+ echo "ERROR: Assessment failed. Check Step Functions execution logs for details."
+ exit 1
+ fi
fi
- |
echo "==========================================="
diff --git a/consolidate_html_reports.py b/consolidate_html_reports.py
index 072f903..e0037bf 100644
--- a/consolidate_html_reports.py
+++ b/consolidate_html_reports.py
@@ -33,6 +33,16 @@
from report_template import generate_html_report
+# Sentinel region label used by the per-service assessments to tag IAM-only
+# findings that run once per execution rather than per region. It is not a real
+# AWS region and must be excluded when counting scanned regions.
+GLOBAL_REGION_LABEL = "Global"
+
+
+def build_multi_account_report_key(timestamp: str) -> str:
+ """Build the consolidated multi-account HTML report object key."""
+ return f"consolidated-reports/security_assessment_multi_account_{timestamp}.html"
+
def _account_files_dir():
"""Base directory holding per-account CSV files.
@@ -69,6 +79,7 @@ def consolidate_html_reports():
all_findings = []
account_ids = set()
+ regions = set()
service_stats = {
"bedrock": {"passed": 0, "failed": 0, "na": 0},
"sagemaker": {"passed": 0, "failed": 0, "na": 0},
@@ -97,6 +108,14 @@ def consolidate_html_reports():
reader = csv.DictReader(f)
for row in reader:
# Map CSV columns to finding structure
+ region = row.get("Region", "")
+ # "Global" tags IAM-only findings; not a scanned region.
+ if (
+ region
+ and region != GLOBAL_REGION_LABEL
+ and "," not in region
+ ):
+ regions.add(region)
finding = {
"account_id": account_id,
"check_id": row.get("Check_ID", ""),
@@ -106,6 +125,7 @@ def consolidate_html_reports():
"reference": row.get("Reference", ""),
"severity": row.get("Severity", "N/A"),
"status": row.get("Status", ""),
+ "region": region,
}
# Determine service from Check_ID prefix
@@ -167,10 +187,11 @@ def consolidate_html_reports():
mode="multi",
account_ids=list(account_ids),
timestamp=timestamp_display,
+ regions=sorted(regions) if regions else None,
)
timestamp_file = datetime.now().strftime("%Y%m%d_%H%M%S")
- s3_key = f"consolidated-reports/multi_account_report_{timestamp_file}.html"
+ s3_key = build_multi_account_report_key(timestamp_file)
try:
s3.put_object(
diff --git a/deployment/1-aiml-security-member-roles.yaml b/deployment/1-aiml-security-member-roles.yaml
index cfa70e1..66b8516 100644
--- a/deployment/1-aiml-security-member-roles.yaml
+++ b/deployment/1-aiml-security-member-roles.yaml
@@ -1,13 +1,26 @@
AWSTemplateFormatVersion: 2010-09-09
Description: This template deploys the roles needed for the AIML Security Assessment to run in each member account.
+Metadata:
+ AWS::CloudFormation::Interface:
+ ParameterGroups:
+ - Label:
+ default: Trust Configuration
+ Parameters:
+ - ManagementAccountID
+ ParameterLabels:
+ ManagementAccountID:
+ default: Central assessment account ID
+
Parameters:
ManagementAccountID:
- Description: "Specifies the account ID where AIML Security Assessment will run from."
+ Description: >
+ The 12-digit AWS account ID where the multi-account AIML Security
+ Assessment CodeBuild project runs. This can be the AWS Organizations
+ management account or a delegated administrator/central tooling account.
Type: String
- Default: "012345678910"
- AllowedPattern: \d{12}
- ConstraintDescription: Enter the 12 digit account ID with no spaces.
+ AllowedPattern: "^\\d{12}$"
+ ConstraintDescription: Enter a 12-digit AWS account ID with no spaces.
Resources:
MemberRole:
diff --git a/deployment/2-aiml-security-codebuild.yaml b/deployment/2-aiml-security-codebuild.yaml
index bf06f70..f4a1ee8 100644
--- a/deployment/2-aiml-security-codebuild.yaml
+++ b/deployment/2-aiml-security-codebuild.yaml
@@ -9,6 +9,8 @@ Metadata:
Parameters:
- MultiAccountScan
- MultiAccountListOverride
+ - TargetRegions
+ - EnableFinServAssessment
- EmailAddress
- Label:
default: Advanced Options
@@ -18,6 +20,27 @@ Metadata:
- MemberRoleName
- GitHubRepoUrl
- GitHubBranch
+ ParameterLabels:
+ MultiAccountScan:
+ default: Scan all AWS Organizations accounts
+ MultiAccountListOverride:
+ default: Account IDs to scan
+ TargetRegions:
+ default: AWS regions to scan
+ EnableFinServAssessment:
+ default: Financial Services GenAI risk checks
+ EmailAddress:
+ default: Notification email address
+ ConcurrentAccountScans:
+ default: Concurrent account scans
+ CodeBuildTimeout:
+ default: CodeBuild timeout in minutes
+ MemberRoleName:
+ default: Member account role name
+ GitHubRepoUrl:
+ default: GitHub repository URL
+ GitHubBranch:
+ default: GitHub branch
Parameters:
GitHubRepoUrl:
@@ -30,13 +53,19 @@ Parameters:
GitHubBranch:
Type: String
- Description: GitHub branch to build from
+ Description: GitHub branch, tag, or commit to build from.
Default: main
+ AllowedPattern: "^\\S{1,255}$"
+ ConstraintDescription: Must be 1-255 characters with no whitespace.
MemberRoleName:
- Description: "The role that the assessment should assume to perform the assessment."
+ Description: >
+ Name of the IAM role the assessment assumes in each member account. Provide
+ only the role name; the role must exist under the /service-role/ path.
Type: String
Default: AIMLSecurityMemberRole
+ AllowedPattern: "^[\\w+=,.@-]{1,64}$"
+ ConstraintDescription: Must be a valid IAM role name, not an ARN or path.
MultiAccountScan:
Description: "Set this to true if you want to scan all accounts in your organization."
@@ -47,10 +76,14 @@ Parameters:
Default: "false"
MultiAccountListOverride:
- Description: "Specify a space delimited list of accounts to scan. Leaving this
- blank will scan all accounts in your organization."
+ Description: >
+ Optional comma- or space-separated list of 12-digit AWS account IDs to scan. Leave
+ blank to scan all active accounts in your organization when
+ MultiAccountScan is true.
Type: String
Default: ""
+ AllowedPattern: "^$|^\\d{12}([,\\s]+\\d{12})*$"
+ ConstraintDescription: Must be empty or a comma- or space-separated list of 12-digit AWS account IDs.
EmailAddress:
Description: "Specify an address if you want to receive an email when the
@@ -69,17 +102,39 @@ Parameters:
- "Six"
- "Twelve"
Default: "Three"
+ ConstraintDescription: Must be one of Three, Six, or Twelve.
+
+ TargetRegions:
+ Type: String
+ AllowedPattern: "^$|^all$|^[a-z]{2}(-gov)?-[a-z]+-[0-9]([,\\s]+[a-z]{2}(-gov)?-[a-z]+-[0-9])*$"
+ ConstraintDescription: >
+ TargetRegions must be empty, 'all', or a comma- or space-separated list of AWS
+ region names (for example, us-east-1,us-west-2,eu-west-1 or
+ us-east-1 us-west-2 eu-west-1).
+ Description: >
+ Comma- or space-separated list of AWS regions to scan (e.g.,
+ us-east-1,us-west-2,eu-west-1 or us-east-1 us-west-2 eu-west-1).
+ Use 'all' to scan all regions where the service is available.
+ Leave empty to scan only the deployment region.
+ Default: ""
CodeBuildTimeout:
- Description: "Set the timeout for the CodeBuild job. The default is 300 minutes
- (5 hours)."
+ Description: >
+ Timeout for the CodeBuild assessment job, in minutes. The default is 300
+ minutes (5 hours); increase this value for large organizations or
+ all-region scans.
Type: Number
MinValue: 5
MaxValue: 2160
Default: 300
+ ConstraintDescription: Must be between 5 and 2160 minutes.
EnableFinServAssessment:
- Description: "Run the Financial Services GenAI risk checks (FS-01..FS-69) in every scanned account. Enable only if you must adhere to FinServ compliance; the FinServ findings add a dedicated report section and increase report size."
+ Description: >
+ Run the Financial Services GenAI risk checks (FS-01 through FS-69) in every
+ scanned account. Enable this only when you need the additional industry
+ risk assessment; the findings add a dedicated report section and increase
+ report size.
Type: String
AllowedValues: ["true", "false"]
Default: "false"
@@ -518,6 +573,12 @@ Resources:
"ParallelAccounts",
]
Type: PLAINTEXT
+ - Name: TARGET_REGIONS
+ Value: !Ref TargetRegions
+ Type: PLAINTEXT
+ - Name: BUILD_TIMEOUT_MINUTES
+ Value: !Ref CodeBuildTimeout
+ Type: PLAINTEXT
- Name: ENABLE_FINSERV
Value: !Ref EnableFinServAssessment
Type: PLAINTEXT
diff --git a/deployment/aiml-security-single-account.yaml b/deployment/aiml-security-single-account.yaml
index 0b962f8..0997c56 100644
--- a/deployment/aiml-security-single-account.yaml
+++ b/deployment/aiml-security-single-account.yaml
@@ -8,12 +8,27 @@ Metadata:
default: Assessment Options
Parameters:
- EmailAddress
+ - TargetRegions
+ - EnableFinServAssessment
- Label:
default: Advanced Options
Parameters:
- CodeBuildTimeout
- GitHubRepoUrl
- GitHubBranch
+ ParameterLabels:
+ EmailAddress:
+ default: Notification email address
+ TargetRegions:
+ default: AWS regions to scan
+ EnableFinServAssessment:
+ default: Financial Services GenAI risk checks
+ CodeBuildTimeout:
+ default: CodeBuild timeout in minutes
+ GitHubRepoUrl:
+ default: GitHub repository URL
+ GitHubBranch:
+ default: GitHub branch
Parameters:
GitHubRepoUrl:
@@ -25,8 +40,10 @@ Parameters:
GitHubBranch:
Type: String
- Description: GitHub branch to build from
+ Description: GitHub branch, tag, or commit to build from.
Default: main
+ AllowedPattern: "^\\S{1,255}$"
+ ConstraintDescription: Must be 1-255 characters with no whitespace.
EmailAddress:
Description: "Email address to receive assessment completion notifications (optional)"
@@ -35,15 +52,35 @@ Parameters:
AllowedPattern: ^$|^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
ConstraintDescription: Must be a valid email address or leave empty
+ TargetRegions:
+ Type: String
+ AllowedPattern: "^$|^all$|^[a-z]{2}(-gov)?-[a-z]+-[0-9]([,\\s]+[a-z]{2}(-gov)?-[a-z]+-[0-9])*$"
+ ConstraintDescription: >
+ TargetRegions must be empty, 'all', or a comma- or space-separated list of AWS
+ region names (for example, us-east-1,us-west-2,eu-west-1 or
+ us-east-1 us-west-2 eu-west-1).
+ Description: >
+ Comma- or space-separated list of AWS regions to scan (e.g.,
+ us-east-1,us-west-2,eu-west-1 or us-east-1 us-west-2 eu-west-1).
+ Use 'all' to scan all regions where the service is available.
+ Leave empty to scan only the deployment region.
+ Default: ""
+
CodeBuildTimeout:
- Description: "Timeout for the CodeBuild job in minutes"
+ Description: >
+ Timeout for the CodeBuild assessment job, in minutes. Increase this value
+ for large multi-region scans.
Type: Number
MinValue: 5
MaxValue: 480
Default: 60
+ ConstraintDescription: Must be between 5 and 480 minutes.
EnableFinServAssessment:
- Description: "Run the Financial Services GenAI risk checks (FS-01..FS-69). Enable only if you must adhere to FinServ compliance; the FinServ findings add a dedicated report section."
+ Description: >
+ Run the Financial Services GenAI risk checks (FS-01 through FS-69). Enable
+ this only when you need the additional industry risk assessment; the
+ findings add a dedicated report section.
Type: String
AllowedValues: ["true", "false"]
Default: "false"
@@ -509,6 +546,12 @@ Resources:
- Name: AWS_ACCOUNT_ID
Value: !Sub ${AWS::AccountId}
Type: PLAINTEXT
+ - Name: TARGET_REGIONS
+ Value: !Ref TargetRegions
+ Type: PLAINTEXT
+ - Name: BUILD_TIMEOUT_MINUTES
+ Value: !Ref CodeBuildTimeout
+ Type: PLAINTEXT
- Name: ENABLE_FINSERV
Value: !Ref EnableFinServAssessment
Type: PLAINTEXT
diff --git a/docs/AIMLSecurityAssessment-MappingsTable.csv b/docs/AIMLSecurityAssessment-MappingsTable.csv
deleted file mode 100644
index 9f16695..0000000
--- a/docs/AIMLSecurityAssessment-MappingsTable.csv
+++ /dev/null
@@ -1,16 +0,0 @@
-Risk Category,Gap Count,Key Missing Focus Areas
-Unbounded Consumption,7,"WAF/Shield, rate limiting, token quotas, cost monitoring, input body-size limits"
-Excessive Agency,6,"Action boundaries, AgentCore Policy, transaction limits, rate alarms, financial transaction value thresholds, end-user identity propagation"
-Supply Chain Vulnerabilities,5,"SCPs, model inventory, onboarding governance, adversarial evaluation, TPRM monitoring"
-Training Data & Model Poisoning,5,"Model Monitor, data drift, Model Registry, Feature Store rollback, training data audit trail"
-Vector & Embedding Weaknesses,6,"KB least privilege, CloudTrail logging, metadata filtering, multi-tenancy, S3 event notifications"
-Non-Compliant Output,5,"Automated Reasoning, RAG controls, human-in-the-loop, compliance-driven guardrails, guardrail trace logging"
-Misinformation,4,"KB data quality, source attribution, integrity monitoring with event notifications, sync cadence"
-Abusive or Harmful Output,4,"FMEval, user reporting, allowlists, AI Service Cards"
-Biased Output,4,"SageMaker Clarify, Bedrock Evaluations with cadence, bias datasets, AI Service Cards"
-Sensitive Information Disclosure,5,"CloudWatch log masking, Macie, PII pre-processing pipeline, data classification, end-user identity propagation"
-Hallucination,4,"Contextual grounding, Automated Reasoning, disclaimers, RAG"
-Prompt Injection,5,"Input sanitization, prompt input validation function, parameterized queries, pen testing, SDK currency"
-Improper Output Handling,4,"Output validation, sanitization, encoding, XSS prevention"
-Off-Topic & Inappropriate Output,2,"Contextual grounding, topic allowlists"
-Out-of-Date Training Data,3,"KB sync cadence, data currency disclaimers, FM version updates and TPRM"
diff --git a/docs/CLEANUP.md b/docs/CLEANUP.md
new file mode 100644
index 0000000..4fb1c49
--- /dev/null
+++ b/docs/CLEANUP.md
@@ -0,0 +1,166 @@
+# Cleanup Guide
+
+This guide provides step-by-step instructions for removing resources deployed by the AI/ML Security Assessment framework.
+
+Before deleting any stacks, record the S3 bucket names from the stack outputs. After a stack is deleted, its outputs are no longer available.
+
+The deployment creates two kinds of buckets:
+
+- **Infrastructure stack bucket**: The `AssessmentBucket` output from the stack you deployed manually, such as `aiml-security-single-account` or `aiml-security-multi-account`.
+- **Assessment stack buckets**: The `AssessmentBucketName` output from the auto-created SAM stacks, such as `aiml-sec-{account_id}`, `aiml-security-{account_id}`, or `aiml-security-mgmt`. These buckets use `DeletionPolicy: Retain`, so they remain after the SAM assessment stack is deleted and must be deleted manually if you want a full cleanup.
+
+## Cleanup Order
+
+For a clean removal, delete resources in this order:
+
+1. Record S3 bucket names from stack outputs
+2. **Assessment stacks** (auto-created by SAM)
+3. Manually empty and delete retained assessment buckets
+4. **Infrastructure stack** (the stack you deployed manually)
+5. Manually empty and delete the infrastructure stack bucket if stack deletion fails
+6. AWS CloudFormation StackSet member roles (multi-account only)
+7. Optional: CloudWatch log groups created during assessment runs
+
+---
+
+## Emptying and Deleting Versioned S3 Buckets
+
+The buckets created by this framework are versioned. A recursive `aws s3 rm` removes current objects, but versioned buckets can still contain noncurrent versions and delete markers. Use the following helper to remove current objects, noncurrent versions, delete markers, and then the bucket.
+
+This command requires `jq`.
+
+```bash
+BUCKET_NAME=""
+
+aws s3 rm "s3://${BUCKET_NAME}" --recursive
+
+while true; do
+ delete_payload=$(aws s3api list-object-versions \
+ --bucket "${BUCKET_NAME}" \
+ --output json \
+ | jq '{Objects: (((.Versions // []) + (.DeleteMarkers // [])) | map({Key, VersionId}) | .[0:1000])}')
+
+ object_count=$(echo "${delete_payload}" | jq '.Objects | length')
+ if [ "${object_count}" -eq 0 ]; then
+ break
+ fi
+
+ aws s3api delete-objects \
+ --bucket "${BUCKET_NAME}" \
+ --delete "${delete_payload}"
+done
+
+aws s3 rb "s3://${BUCKET_NAME}"
+```
+
+Repeat this for each infrastructure and assessment bucket you want to remove.
+
+---
+
+## Single-Account Cleanup
+
+To remove all resources deployed for single-account assessment:
+
+1. **Delete the AWS SAM-deployed assessment stack**:
+ - Navigate to **AWS CloudFormation** > **Stacks**
+ - Select the `aiml-sec-{account_id}` stack (for example, `aiml-sec-123456789012`)
+ - Before deleting it, open **Outputs** and record the `AssessmentBucketName` value
+ - Click **Delete**
+ - Wait for stack deletion to complete
+ - The assessment bucket is retained by design, so delete it manually using the S3 cleanup helper above if you no longer need the report artifacts
+
+2. **Delete the AWS CodeBuild infrastructure stack**:
+ - Select the `aiml-security-single-account` stack (or your custom stack name)
+ - Before deleting it, open **Outputs** and record the `AssessmentBucket` value
+ - Click **Delete**
+ - Wait for stack deletion to complete
+
+3. **Clean up Amazon S3 buckets**:
+ - Delete the retained `AssessmentBucketName` bucket from the SAM assessment stack.
+ - If the infrastructure stack deletion fails because its `AssessmentBucket` is not empty, empty and delete that bucket, then retry stack deletion.
+
+---
+
+## Multi-Account Cleanup
+
+To remove all resources deployed for multi-account assessment:
+
+1. **Delete AWS SAM-deployed stacks in each member account**:
+ - In the deployment region, for each account that was scanned, navigate to **AWS CloudFormation** > **Stacks**
+ - Select the `aiml-security-{account_id}` stack (for example, `aiml-security-123456789012`)
+ - For the management account, select `aiml-security-mgmt`
+ - Before deleting each stack, open **Outputs** and record the `AssessmentBucketName` value
+ - Click **Delete**
+ - Alternatively, use the AWS CLI to delete across accounts:
+ ```bash
+ # Assume role in member account and delete stack
+ aws cloudformation delete-stack --stack-name aiml-security- \
+ --region
+ ```
+ - The assessment buckets are retained by design, so delete them manually using the S3 cleanup helper above if you no longer need the report artifacts
+
+2. **Delete the central AWS CodeBuild infrastructure stack**:
+ - In the management account, navigate to **AWS CloudFormation** > **Stacks**
+ - Select the `aiml-security-multi-account` stack, or the custom stack name you chose
+ - Before deleting it, open **Outputs** and record the `AssessmentBucket` value
+ - Click **Delete**
+ - Wait for stack deletion to complete
+
+3. **Delete the AWS CloudFormation StackSet member roles**:
+ - Navigate to **AWS CloudFormation** > **StackSets**
+ - Select the StackSet created from `deployment/1-aiml-security-member-roles.yaml` (for example, `aiml-security-member-roles`, or your custom StackSet name)
+ - Click **Actions** > **Delete stacks from StackSet**
+ - Select all deployment targets (OUs or accounts)
+ - Wait for stack instances to be deleted
+ - Once all stack instances are removed, delete the AWS CloudFormation StackSet itself
+
+4. **Clean up Amazon S3 buckets**:
+ - Delete each retained `AssessmentBucketName` bucket from the per-account SAM assessment stacks.
+ - If the central infrastructure stack deletion fails because its `AssessmentBucket` is not empty, empty and delete that bucket, then retry stack deletion.
+
+ To find likely assessment buckets:
+
+ ```bash
+ aws s3 ls | grep aiml-security
+ ```
+
+---
+
+## Identifying Stack Types
+
+The deployment creates multiple AWS CloudFormation stacks. Here's how to identify them:
+
+| Stack Type | How to Identify | Action |
+|------------|-----------------|--------|
+| **Infrastructure Stack** (yours) | The name you chose (for example, `aiml-security-single-account`) | Delete after assessment stacks |
+| **Assessment Stack** (auto-generated) | `aiml-sec-{account_id}` (single) or `aiml-security-{account_id}` (multi) | Delete before the infrastructure stack |
+
+**Quick Check**: If you see a stack name starting with `aiml-sec-` or `aiml-security-` followed by numbers (or `aiml-security-mgmt`), that's an auto-generated assessment stack.
+
+---
+
+## Optional CloudWatch Logs Cleanup
+
+AWS Lambda and AWS CodeBuild create Amazon CloudWatch log groups during assessment runs. These log groups can remain after stack deletion unless you delete them or configure retention.
+
+Common log group name patterns include:
+
+- `/aws/lambda/aiml-security-*`
+- `/aws/codebuild/AIMLSecurityCodeBuild`
+- `/aws/codebuild/AIMLSecurityMultiAccountCodeBuild`
+
+To list likely log groups:
+
+```bash
+aws logs describe-log-groups \
+ --log-group-name-prefix /aws/lambda/aiml-security-
+
+aws logs describe-log-groups \
+ --log-group-name-prefix /aws/codebuild/AIMLSecurity
+```
+
+To delete a log group:
+
+```bash
+aws logs delete-log-group --log-group-name ""
+```
diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md
index 1dab9ef..010be26 100644
--- a/docs/DEVELOPER_GUIDE.md
+++ b/docs/DEVELOPER_GUIDE.md
@@ -44,13 +44,14 @@
## Architecture Overview
-The AI/ML Security Assessment Framework is a serverless, multi-account security assessment solution for AWS AI/ML workloads. It performs 52 security checks across Amazon Bedrock, Amazon SageMaker AI, and Amazon Bedrock AgentCore, generating interactive HTML reports with findings and remediation guidance.
+The AI/ML Security Assessment Framework is a serverless, multi-account security assessment solution for AWS AI/ML workloads. It performs 52 core security checks across Amazon Bedrock, Amazon SageMaker AI, and Amazon Bedrock AgentCore, with an optional 64-check Financial Services GenAI risk assessment, generating interactive HTML reports with findings and remediation guidance.
### Security Design Principles
-- All roles follow the principle of least privilege
-- Cross-account trust is limited to the specific AWS CodeBuild role
-- Amazon S3 bucket enforces SSL-only access
+- Runtime assessment Lambda roles are read-oriented and scoped to the APIs needed by each assessment
+- AWS CodeBuild and member-account roles require deployment permissions because they create or update the SAM assessment stacks before running checks
+- Cross-account trust is limited to the specific AWS CodeBuild role in the central assessment account
+- Amazon S3 buckets enforce SSL-only access
- Assessment data is encrypted in transit and at rest
- No persistent credentials are stored in AWS CodeBuild
@@ -71,8 +72,8 @@ The AI/ML Security Assessment Framework is a serverless, multi-account security
#### Step 1: Member Account Roles (`1-aiml-security-member-roles.yaml`)
- **AWS CloudFormation StackSets Deployment**: Deploys `AIMLSecurityMemberRole` to all target accounts
-- **Cross-Account Trust**: Establishes trust relationship with central management account
-- **Assessment Permissions**: Grants read-only access to AI/ML services (Amazon Bedrock, Amazon SageMaker AI, Amazon Bedrock AgentCore) for security assessment
+- **Cross-Account Trust**: Establishes trust relationship with the central assessment account
+- **Assessment and Deployment Permissions**: Grants read-oriented service permissions for assessment checks and deployment permissions needed for CodeBuild to create or update per-account SAM stacks
#### Step 2: Central Infrastructure (`2-aiml-security-codebuild.yaml`)
- **AWS CodeBuild Project**: Orchestrates multi-account deployments and assessments
@@ -85,11 +86,11 @@ The AI/ML Security Assessment Framework is a serverless, multi-account security
### Phase 2: Assessment Execution (AWS CodeBuild Orchestration)
#### AWS CodeBuild Execution Flow
-1. **Account Discovery**: Lists active accounts from AWS Organizations
-2. **Role Assumption**: Assumes `AIMLSecurityMemberRole` in each target account
-3. **AWS SAM Deployment**: Deploys the AI/ML assessment stack through AWS SAM
-4. **Assessment Execution**: Triggers AWS Step Functions workflow in each account
-5. **Results Consolidation**: Collects and consolidates results from all accounts
+1. **Account Discovery**: In multi-account mode, lists active accounts from AWS Organizations or uses `MultiAccountListOverride`
+2. **Role Assumption**: In multi-account mode, assumes `AIMLSecurityMemberRole` in each target account
+3. **AWS SAM Deployment**: Deploys or updates the AI/ML assessment stack through AWS SAM
+4. **Assessment Execution**: Triggers AWS Step Functions workflow in each account, passing `enableFinServ` from the deployment parameter
+5. **Results Consolidation**: Syncs per-account reports to the infrastructure bucket and creates a consolidated report for multi-account runs
#### Project Structure
```
@@ -99,8 +100,11 @@ sample-aiml-security-assessment/
│ │ ├── bedrock_assessments/ # Bedrock security checks (14)
│ │ ├── sagemaker_assessments/ # SageMaker security checks (25)
│ │ ├── agentcore_assessments/ # AgentCore security checks (13)
+│ │ ├── finserv_assessments/ # Optional Financial Services GenAI risk checks (64)
+│ │ ├── finserv_tests/ # FinServ-specific unit and coverage tests
│ │ ├── iam_permission_caching/ # AWS IAM permissions cache
│ │ ├── cleanup_bucket/ # Amazon S3 cleanup
+│ │ ├── resolve_regions/ # Multi-region resolution Lambda
│ │ └── generate_consolidated_report/ # HTML/CSV report generation
│ ├── statemachine/ # AWS Step Functions definition
│ ├── images/ # SAM application images
@@ -120,15 +124,17 @@ sample-aiml-security-assessment/
│ ├── scripts/ # Screenshot capture scripts
│ ├── *.html # Sample HTML reports
│ └── *.png # Report screenshots
+├── tests/ # Unit tests for assessment functions
+│ └── requirements.txt # Test dependencies
+├── .github/workflows/ # PR lint, test, SAM validate, and security scans
├── buildspec.yml # AWS CodeBuild orchestration
-├── buildspec-modular-example.yml # Modular buildspec example
└── consolidate_html_reports.py # Multi-account report consolidation
```
#### Member Account Resources (Deployed by AWS SAM)
- **AWS SAM Application**: AI/ML security assessment stack
- **AWS Step Functions**: Single workflow orchestrating all assessments
-- **AWS Lambda Functions**: One per service (Amazon Bedrock, Amazon SageMaker AI, Amazon Bedrock AgentCore) plus utilities
+- **AWS Lambda Functions**: One per core service (Amazon Bedrock, Amazon SageMaker AI, Amazon Bedrock AgentCore), one FinServ assessment Lambda invoked only when enabled, plus utilities
- **Local Amazon S3 Bucket**: Storage for account-specific results
### Assessment Execution Workflow
@@ -148,30 +154,55 @@ sample-aiml-security-assessment/
```json
{
"Comment": "AI/ML Assessment Module",
- "StartAt": "Cleanup Amazon S3 Bucket",
+ "StartAt": "Cleanup S3 Bucket",
"States": {
- "Cleanup Amazon S3 Bucket": {
+ "Cleanup S3 Bucket": {
"Type": "Task",
- "Resource": "arn:aws:states:::lambda:invoke",
- "Next": "AWS IAM Permission Caching"
+ "Next": "IAM Permission Caching"
},
- "AWS IAM Permission Caching": {
+ "IAM Permission Caching": {
"Type": "Task",
- "Resource": "arn:aws:states:::lambda:invoke",
- "Next": "Parallel Service Assessments"
+ "Next": "Resolve Target Regions"
},
- "Parallel Service Assessments": {
- "Type": "Parallel",
- "Branches": [
- {"StartAt": "Amazon Bedrock Assessment", "States": {...}},
- {"StartAt": "Amazon SageMaker AI Assessment", "States": {...}},
- {"StartAt": "Amazon Bedrock AgentCore Assessment", "States": {...}}
- ],
+ "Resolve Target Regions": {
+ "Type": "Task",
+ "Comment": "Resolves target regions from TARGET_REGIONS env var",
+ "Next": "Scan Regions"
+ },
+ "Scan Regions": {
+ "Type": "Map",
+ "ItemsPath": "$.ResolvedRegions.regions",
+ "MaxConcurrency": "${MaxRegionConcurrency}",
+ "ItemProcessor": {
+ "ProcessorConfig": {"Mode": "INLINE"},
+ "StartAt": "Run Security Assessments",
+ "States": {
+ "Run Security Assessments": {
+ "Type": "Parallel",
+ "Branches": [
+ {"StartAt": "Bedrock Security Assessment", "States": {...}},
+ {"StartAt": "Sagemaker Security Assessment", "States": {...}},
+ {"StartAt": "AgentCore Security Assessment", "States": {...}},
+ {
+ "StartAt": "FinServ Enabled?",
+ "States": {
+ "FinServ Enabled?": {
+ "Type": "Choice",
+ "Comment": "Runs FinServ only when enableFinServ is true and RegionIndex is 0"
+ },
+ "FinServ Security Assessment": {"Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "End": true},
+ "FinServ Assessment Skipped": {"Type": "Pass", "End": true}
+ }
+ }
+ ],
+ "End": true
+ }
+ }
+ },
"Next": "Generate Consolidated Report"
},
"Generate Consolidated Report": {
"Type": "Task",
- "Resource": "arn:aws:states:::lambda:invoke",
"End": true
}
}
@@ -180,22 +211,27 @@ sample-aiml-security-assessment/
## Assessment Structure
-The framework includes **52 security checks** across three AI/ML services. For the complete list of checks with descriptions, see the [Security Checks Reference](SECURITY_CHECKS.md).
+The framework includes **52 core security checks** across three AI/ML services, plus **64 optional Financial Services GenAI risk checks** when `EnableFinServAssessment` is enabled. For the complete list of checks with descriptions, see the [Security Checks Reference](SECURITY_CHECKS.md).
### AWS Lambda Functions
-Each assessment AWS Lambda function:
-1. Receives execution context from AWS Step Functions
-2. Reads cached AWS IAM permissions from Amazon S3
-3. Performs security checks against AWS APIs
-4. Generates CSV report with findings
-5. Uploads results to Amazon S3
-6. Returns findings summary to AWS Step Functions
+Each core service assessment AWS Lambda function:
+1. Receives execution context and target region from AWS Step Functions (via the Map state)
+2. Verifies the service is available in the target region (returns N/A finding if not)
+3. Reads cached AWS IAM permissions from Amazon S3
+4. Creates regional boto3 clients with explicit `region_name` parameter
+5. Performs security checks against AWS APIs in the target region
+6. Generates CSV report with findings (includes `Region` column)
+7. Uploads results to Amazon S3 with region-suffixed filename
+8. Returns findings summary to AWS Step Functions
+
+The Financial Services assessment Lambda is different. It is deployed in both SAM templates, but Step Functions invokes it only when the execution input includes `"enableFinServ": "true"` and only from the first region iteration (`RegionIndex == 0`). It receives the full `TargetRegions` list and emits FinServ findings with Region values so the report can display the same regional filters as the core services.
**Additional Functions:**
-- **AWS IAM Permission Caching**: Pre-fetches AWS IAM policies to optimize assessment
+- **AWS IAM Permission Caching**: Pre-fetches AWS IAM policies to optimize assessment (global, runs once)
- **Cleanup Bucket**: Removes old assessment data
-- **Generate Consolidated Report**: Creates HTML report from CSV findings
+- **Resolve Regions**: Resolves target regions from `TargetRegions` parameter for the Map state
+- **Generate Consolidated Report**: Creates HTML report from CSV findings with region filtering
## Adding New AI/ML Service Assessments
@@ -214,26 +250,44 @@ cd aiml-security-assessment/functions/security/comprehend_assessments
```python
# app.py
import boto3
+import os
import json
+from botocore.config import Config
+from botocore.exceptions import ClientError, EndpointConnectionError
from schema import create_finding
+boto3_config = Config(retries=dict(max_attempts=10, mode="adaptive"))
+
def lambda_handler(event, context):
"""Main assessment handler for new service"""
all_findings = []
+ # Extract target region from Step Functions Map state
+ region = event.get("Region", os.environ.get("AWS_REGION", "us-east-1"))
+
+ # Verify service availability in this region
+ try:
+ test_client = boto3.client("comprehend", config=boto3_config, region_name=region)
+ test_client.list_endpoints(MaxResults=1)
+ except (EndpointConnectionError, Exception) as e:
+ if "Could not connect to the endpoint URL" in str(e):
+ # Service not available — return N/A finding
+ ...
+ return {"statusCode": 200, "body": {"message": f"Service not available in {region}"}}
+
# Get cached permissions
execution_id = event["Execution"]["Name"]
permission_cache = get_permissions_cache(execution_id)
- # Run assessment checks
- findings = check_new_service_security(permission_cache)
+ # Run assessment checks (pass region to each)
+ findings = check_new_service_security(permission_cache, region=region)
all_findings.append(findings)
- # Generate and upload report
+ # Generate and upload report (include region in S3 key)
csv_content = generate_csv_report(all_findings)
bucket_name = os.environ.get("AIML_ASSESSMENT_BUCKET_NAME")
- s3_url = write_to_s3(execution_id, csv_content, bucket_name)
+ s3_url = write_to_s3(execution_id, csv_content, bucket_name, region=region)
return {
"statusCode": 200,
@@ -245,7 +299,7 @@ def lambda_handler(event, context):
}
-def check_new_service_security(permission_cache):
+def check_new_service_security(permission_cache, region: str = ""):
"""Implement your security checks here"""
findings = {
"check_name": "New Service Security Check",
@@ -254,9 +308,11 @@ def check_new_service_security(permission_cache):
"csv_data": [],
}
+ # Create regional client
+ client = boto3.client("comprehend", config=boto3_config, region_name=region)
+
# Your assessment logic here
- # Use permission_cache to check IAM permissions
- # Use AWS SDK to check service configurations
+ # Pass region= to all create_finding() calls
return findings
```
@@ -279,7 +335,6 @@ class SeverityEnum(str, Enum):
MEDIUM = "Medium"
LOW = "Low"
INFORMATIONAL = "Informational"
- NA = "N/A"
class StatusEnum(str, Enum):
@@ -289,7 +344,7 @@ class StatusEnum(str, Enum):
def create_finding(
- check_id, finding_name, finding_details, resolution, reference, severity, status
+ check_id, finding_name, finding_details, resolution, reference, severity, status, region=""
):
"""Create standardized finding format
@@ -301,6 +356,7 @@ def create_finding(
reference: Documentation URL
severity: SeverityEnum value
status: StatusEnum value (Failed, Passed, or N/A)
+ region: AWS region where the finding was identified
"""
return {
"Check_ID": check_id,
@@ -310,18 +366,22 @@ def create_finding(
"Reference": reference,
"Severity": severity,
"Status": status,
+ "Region": region,
}
```
### Step 2: Update AWS SAM Template
-Add your new function to `aiml-security-assessment/template.yaml`:
+Add your new function to both SAM templates:
+
+- `aiml-security-assessment/template.yaml`
+- `aiml-security-assessment/template-multi-account.yaml`
```yaml
ComprehendSecurityAssessmentFunction:
Type: AWS::Serverless::Function
Properties:
- FunctionName: !Sub 'ComprehendSecurityAssessment-${AWS::AccountId}'
+ FunctionName: !Sub 'aiml-security-${AWS::StackName}-ComprehendAssessment'
CodeUri: functions/security/comprehend_assessments/
Handler: app.lambda_handler
Runtime: python3.12
@@ -330,6 +390,7 @@ Add your new function to `aiml-security-assessment/template.yaml`:
Environment:
Variables:
AIML_ASSESSMENT_BUCKET_NAME: !Ref AIMLAssessmentBucket
+ TARGET_REGIONS: !Ref TargetRegions
Policies:
- S3CrudPolicy:
BucketName: !Ref AIMLAssessmentBucket
@@ -345,7 +406,7 @@ Add your new function to `aiml-security-assessment/template.yaml`:
### Step 3: Update AWS Step Functions Definition
-Add new service to the parallel execution in `aiml-security-assessment/statemachine/assessments.asl.json`:
+Add the new service to the `Run Security Assessments` parallel branch inside the `Scan Regions` Map state in `aiml-security-assessment/statemachine/assessments.asl.json`. Also add the function ARN substitution and `LambdaInvokePolicy` for the new function in both SAM templates.
```json
{
@@ -375,7 +436,7 @@ Add new service to the parallel execution in `aiml-security-assessment/statemach
### Step 4: Update AWS IAM Permissions
-Add required permissions to member role template:
+Add required permissions to every role that may deploy or run the new service assessment:
**In `deployment/1-aiml-security-member-roles.yaml`**:
```yaml
@@ -394,13 +455,22 @@ Add required permissions to member role template:
- comprehend:Get*
```
+**In `deployment/2-aiml-security-codebuild.yaml`** (for management-account multi-account mode):
+```yaml
+- comprehend:List*
+- comprehend:Describe*
+- comprehend:Get*
+```
+
+Also add runtime permissions to the new Lambda role statements in both SAM templates if the new service function needs service-specific access at execution time.
+
### Step 5: Test Locally
Test your new assessment function locally:
```bash
cd aiml-security-assessment
-sam build
+sam build --template template.yaml
sam local invoke ComprehendSecurityAssessmentFunction --event testfile.json
```
@@ -411,7 +481,7 @@ sam local invoke ComprehendSecurityAssessmentFunction --event testfile.json
- **Handle Exceptions**: Implement proper error handling and logging
- **Follow Least Privilege**: Only request necessary permissions
- **Standardize Findings**: Use the `create_finding()` function for consistent output
-- **Check ID Convention**: Use service prefixes for check IDs (BR-XX for Amazon Bedrock, SM-XX for Amazon SageMaker AI, AC-XX for Amazon Bedrock AgentCore)
+- **Check ID Convention**: Use service prefixes for check IDs (BR-XX for Amazon Bedrock, SM-XX for Amazon SageMaker AI, AC-XX for Amazon Bedrock AgentCore, FS-XX for Financial Services GenAI risk checks)
- **Status Semantics**: Use correct status values:
- `Passed`: Resources were checked and met the assessed best practice
- `Failed`: Resources were checked and found non-compliant
@@ -460,9 +530,9 @@ except Exception as e:
### 1. Local Testing
```bash
-# Test individual function
+# Test an individual SAM function
cd aiml-security-assessment
-sam build
+sam build --template template.yaml
sam local invoke NewServiceSecurityAssessmentFunction --event test-event.json
```
@@ -474,7 +544,7 @@ sam deploy --stack-name aiml-security-test --capabilities CAPABILITY_IAM
# Execute AWS Step Functions
aws stepfunctions start-execution \
--state-machine-arn arn:aws:states:region:account:stateMachine:TestStateMachine \
- --input '{"accountId":"123456789012"}'
+ --input '{"accountId":"123456789012","enableFinServ":"false"}'
```
### 3. Multi-Account Testing
@@ -490,7 +560,7 @@ For detailed troubleshooting guidance, common issues, and debugging tips, see th
## Development Roadmap
### Current Status
-- **AI/ML Assessment**: 52 security checks across three services (see [Security Checks Reference](SECURITY_CHECKS.md))
+- **AI/ML Assessment**: 52 core checks across three services, plus 64 optional Financial Services GenAI risk checks (see [Security Checks Reference](SECURITY_CHECKS.md))
### Potential Additions
- **Amazon Comprehend**: Data privacy, access controls, entity recognition security
@@ -501,6 +571,8 @@ For detailed troubleshooting guidance, common issues, and debugging tips, see th
### Development Pattern
- Each AWS AI/ML service gets its own dedicated AWS Lambda function
- AWS Step Functions orchestrates parallel execution of service assessments
+- Multi-region scans use a Step Functions Map state with configurable `MaxRegionConcurrency`
+- FinServ checks are opt-in through `EnableFinServAssessment`; the Lambda is deployed by default but invoked only when enabled
- Results are consolidated into a single HTML/CSV report
- AWS CodeBuild orchestrates deployment and execution across multiple accounts
@@ -533,7 +605,7 @@ Both call `generate_html_report()` from `report_template.py` with different para
To update report styling, layout, or features:
1. Edit `report_template.py` only - changes apply to both single and multi-account reports
-2. Run tests: `python test_generate_report.py`
+2. Run the report generator tests from the report package directory: `python -m pytest test_generate_report.py -v`
3. Key functions:
- `get_html_template()` - HTML/CSS/JS structure
- `generate_table_rows()` - Finding row generation
@@ -547,16 +619,16 @@ When you modify the report template or add new features, update the sample repor
#### 1. Generate New Sample Reports
-After making changes to `report_template.py`, regenerate sample reports:
+After making changes to `report_template.py`, regenerate sample reports from a fresh assessment run or from the local report test fixtures. The existing `test_generate_report.py` file is a pytest/unittest test module, not a standalone `--mode/--output` CLI.
```bash
-# Single-account mode
-python test_generate_report.py --mode single --output sample-reports/security_assessment_single_account.html
-
-# Multi-account mode
-python test_generate_report.py --mode multi --output sample-reports/security_assessment_multi_account.html
+# Generate local viewable reports from fixtures
+cd aiml-security-assessment/functions/security/generate_consolidated_report
+python -m pytest test_generate_report.py -k "generate_viewable_report or generate_multi_account_report" -s
```
+The fixture reports are written under `aiml-security-assessment/functions/security/generate_consolidated_report/test_reports/`. Use them to validate report rendering before refreshing the canonical files in `sample-reports/`.
+
#### 2. Capture Screenshots
The repository includes an automated screenshot capture tool:
@@ -667,6 +739,7 @@ GitHub Actions workflows run automatically to validate code quality and security
| Workflow | File | What It Checks |
|----------|------|----------------|
| **Python Code Quality** | `.github/workflows/python-lint.yml` | `ruff check` (lint) and `ruff format --check` (formatting) on changed `.py` files |
+| **Python Tests** | `.github/workflows/python-tests.yml` | Runs upstream tests, FinServ tests, and report-pipeline tests in separate pytest sessions |
| **CloudFormation Lint** | `.github/workflows/cfn-lint.yml` | Validates deployment and SAM templates with `cfn-lint` |
| **SAM Validate & Build** | `.github/workflows/sam-validate.yml` | Runs `sam validate --lint` and `sam build` on SAM templates |
| **ASH Security Scan** | `.github/workflows/ash-security-scan.yml` | Scans changed files for secrets, dependency vulnerabilities, and IaC misconfigurations |
@@ -686,14 +759,28 @@ Before pushing, run these checks locally to catch issues early:
```bash
# Install tools (first time only)
-pip install ruff cfn-lint pytest boto3 pydantic
+pip install ruff cfn-lint
+pip install -r tests/requirements.txt
+pip install "pydantic>=2.0.0"
# Python lint and format
ruff check aiml-security-assessment/functions/security/
ruff format --check aiml-security-assessment/functions/security/
-# Unit tests (213 tests, ~5 seconds, no AWS credentials needed)
-python -m pytest tests/ -v
+# Unit tests. Run these as separate pytest sessions because multiple
+# assessment packages use top-level app.py imports.
+export AIML_ASSESSMENT_BUCKET_NAME=test-assessment-bucket
+export AWS_DEFAULT_REGION=us-east-1
+export AWS_ACCESS_KEY_ID=testing
+export AWS_SECRET_ACCESS_KEY=testing
+
+python -m pytest tests/ -v --tb=short
+python -m pytest aiml-security-assessment/functions/security/finserv_tests/ -v --tb=short
+python -m pytest tests/test_consolidate_finserv.py -v --tb=short
+
+cd aiml-security-assessment/functions/security/generate_consolidated_report
+python -m pytest test_generate_report.py -v --tb=short
+cd -
# CloudFormation lint
cfn-lint deployment/*.yaml
@@ -703,7 +790,9 @@ cfn-lint aiml-security-assessment/template-multi-account.yaml
# SAM validate and build
cd aiml-security-assessment
sam validate --template template.yaml --lint
+sam validate --template template-multi-account.yaml --lint
sam build --template template.yaml
+sam build --template template-multi-account.yaml
```
## Support and Resources
@@ -715,4 +804,4 @@ sam build --template template.yaml
---
-This developer guide provides the foundation for extending the AI/ML Security Assessment Framework. As you add new AI/ML services and security checks, please update this documentation to help future contributors understand and build upon your work.
\ No newline at end of file
+This developer guide provides the foundation for extending the AI/ML Security Assessment Framework. As you add new AI/ML services and security checks, please update this documentation to help future contributors understand and build upon your work.
diff --git a/docs/SECURITY_CHECKS.md b/docs/SECURITY_CHECKS.md
index 9196176..fc732bf 100644
--- a/docs/SECURITY_CHECKS.md
+++ b/docs/SECURITY_CHECKS.md
@@ -22,7 +22,7 @@ The framework evaluates your AI/ML workloads against AWS security best practices
| Service | Number of Checks | Focus Areas |
|---------|------------------|-------------|
| Amazon SageMaker AI | 25 | Security Hub controls, encryption, network isolation, IAM, MLOps |
-| Amazon Bedrock | 13 | Guardrails, encryption, VPC endpoints, IAM permissions, logging |
+| Amazon Bedrock | 14 | Guardrails, encryption, VPC endpoints, IAM permissions, logging |
| Amazon Bedrock AgentCore | 13 | VPC configuration, encryption, observability, resource policies |
| Financial Services GenAI Risk | 64 | Unbounded consumption, excessive agency, supply chain, training data poisoning, vector weaknesses, non-compliant output, misinformation, harmful output, biased output, PII disclosure, hallucination, prompt injection, improper output handling, off-topic output, out-of-date training data |
@@ -35,7 +35,7 @@ Each security check has a unique identifier with a service prefix:
| Prefix | Service | Example |
|--------|---------|---------|
| **SM-XX** | Amazon SageMaker | SM-01, SM-25 |
-| **BR-XX** | Amazon Bedrock | BR-01, BR-13 |
+| **BR-XX** | Amazon Bedrock | BR-01, BR-14 |
| **AC-XX** | Amazon Bedrock AgentCore | AC-01, AC-13 |
| **FS-XX** | Financial Services GenAI Risk | FS-01, FS-69 |
@@ -265,6 +265,11 @@ Each security check has a unique identifier with a service prefix:
- **Severity:** Medium
- **Description:** Validates Bedrock Flows have guardrails attached.
+### BR-14: Stale Bedrock Access
+
+- **Severity:** Medium
+- **Description:** Detects principals with Bedrock permissions that have not used the service recently, using IAM service-last-accessed data. As an IAM-global check, it runs once per execution and is tagged with the `Global` region in multi-region scans.
+
---
## Amazon Bedrock AgentCore Security Checks (13)
@@ -348,26 +353,25 @@ Each security check has a unique identifier with a service prefix:
## Financial Services GenAI Risk Checks (64 additional, 5 upstream extensions)
These 64 standalone checks (FS-XX) extend the framework with Financial Services
-regulatory controls derived from the
-[AWS guide for Financial Services risk management of the use of Generative AI (March 2026)](https://d1.awsstatic.com/onedam/marketing-channels/website/public/global-FinServ-ComplianceGuide-GenAIRisks-public.pdf).
+risk-management controls derived from the
+[AWS User Guide to Governance, Risk, and Compliance for Responsible AI Adoption](https://aws.amazon.com/blogs/security/introducing-the-updated-aws-user-guide-to-governance-risk-and-compliance-for-responsible-ai-adoption/).
An additional 5 FS checks are contributed as extensions to existing SM-07,
SM-22, SM-23, BR-04, and BR-06 (see in-file extension notes).
-The full catalog is split across three companion files for readability:
+The full catalog is in **[`SECURITY_CHECKS_FINSERV.md`](./SECURITY_CHECKS_FINSERV.md)**,
+organized into three parts:
-- **[`SECURITY_CHECKS_FINSERV_COMMON.md`](./SECURITY_CHECKS_FINSERV_COMMON.md)** — shared
- intro, severity rubric, validation note, upstream-overlap table, compliance framework
- mapping.
-- **[`SECURITY_CHECKS_FINSERV_PART1_INFRA_CONTROLS.md`](./SECURITY_CHECKS_FINSERV_PART1_INFRA_CONTROLS.md)** — FS-01 to FS-26
+- **Part 1 — Infrastructure & Resource Controls** — FS-01 to FS-26
(Unbounded Consumption, Excessive Agency, Supply Chain, Training Poisoning, Vector
Weaknesses).
-- **[`SECURITY_CHECKS_FINSERV_PART2_GUARDRAILS_CONTENT_SAFETY.md`](./SECURITY_CHECKS_FINSERV_PART2_GUARDRAILS_CONTENT_SAFETY.md)** — FS-27 to FS-46
+- **Part 2 — Guardrails & Content Safety** — FS-27 to FS-46
(Non-Compliant Output, Misinformation, Abusive/Harmful Output, Biased Output,
Sensitive Information Disclosure).
-- **[`SECURITY_CHECKS_FINSERV_PART3_APP_LAYER_AND_GAPS.md`](./SECURITY_CHECKS_FINSERV_PART3_APP_LAYER_AND_GAPS.md)** — FS-47 to FS-69
+- **Part 3 — Application-Layer Controls & Material Gaps** — FS-47 to FS-69
(Hallucination, Prompt Injection, Improper Output Handling, Off-Topic Output,
Out-of-Date Training Data, and 6 cross-category material gap checks).
-Compliance framework mapping table is in `SECURITY_CHECKS_FINSERV_COMMON.md`
+The same document includes the shared intro, severity rubric, validation note,
+upstream-overlap table, and the compliance framework mapping table
(SR 11-7, FFIEC CAT, NYDFS 500.06, PCI-DSS 12.3.2, DORA Art.6, MAS TRM 9,
ISO 27001 A.12, ECOA, OWASP LLM Top 10).
diff --git a/docs/SECURITY_CHECKS_FINSERV.md b/docs/SECURITY_CHECKS_FINSERV.md
new file mode 100644
index 0000000..0227eea
--- /dev/null
+++ b/docs/SECURITY_CHECKS_FINSERV.md
@@ -0,0 +1,1213 @@
+# FinServ GenAI Risk Checks (FS-01 to FS-69)
+
+This document is the complete reference for the Financial Services (FS-XX) GenAI security
+checks derived from the [AWS User Guide to Governance, Risk, and Compliance for Responsible AI
+Adoption](https://aws.amazon.com/blogs/security/introducing-the-updated-aws-user-guide-to-governance-risk-and-compliance-for-responsible-ai-adoption/)
+(referred to throughout as "the Responsible AI GRC guide"). It combines the shared reference material
+(severity rubric, guide traceability, upstream-overlap table, compliance mapping) with the full
+set of check definitions, organised into three parts:
+
+- **Part 1 — Infrastructure & Resource Controls (FS-01 to FS-26):** unbounded consumption,
+ excessive agency, supply chain, training-data poisoning, vector & embedding weaknesses.
+- **Part 2 — Guardrails & Content Safety (FS-27 to FS-46):** non-compliant output,
+ misinformation, abusive/harmful output, biased output, sensitive information disclosure.
+- **Part 3 — Application-Layer Controls & Material Gaps (FS-47 to FS-69):** hallucination,
+ prompt injection, improper output handling, off-topic output, out-of-date training data,
+ cross-category gap checks.
+
+Of the 69 FS numbers, **64 ship as standalone checks**; 5 (FS-17, FS-18, FS-19, FS-23, FS-64)
+are merged into upstream SM/BR checks and appear here as upstream-extension notes. See
+[Relationship to upstream SM/BR/AC checks](#relationship-to-upstream-smbrac-checks) for the
+consolidation table.
+
+Each check includes how it is **detected** (the AWS API calls or configuration inspected) and
+how a failure is **remediated** (the specific AWS actions to take). Severities follow a
+documented Likelihood × Impact methodology — see the
+[FinServ Severity Methodology](./SECURITY_CHECKS_FINSERV_SEVERITY_METHODOLOGY.md), with
+authoritative per-finding assignments in the
+[FinServ Severity Register](./SECURITY_CHECKS_FINSERV_SEVERITY_REGISTER.md).
+
+## Table of Contents
+
+**Shared reference**
+
+- [About the source](#about-the-source)
+- [Guide traceability](#guide-traceability)
+- [Severity rubric](#severity-rubric)
+- [Validation note](#validation-note)
+- [Contribution workflow](#contribution-workflow)
+- [Relationship to upstream SM/BR/AC checks](#relationship-to-upstream-smbrac-checks)
+- [Compliance Framework Mapping](#compliance-framework-mapping)
+
+**Checks**
+
+- [Part 1 — Infrastructure & Resource Controls (FS-01 to FS-26)](#part-1--infrastructure--resource-controls-fs-01-to-fs-26)
+- [Part 2 — Guardrails & Content Safety (FS-27 to FS-46)](#part-2--guardrails--content-safety-fs-27-to-fs-46)
+- [Part 3 — Application-Layer Controls & Material Gaps (FS-47 to FS-69)](#part-3--application-layer-controls--material-gaps-fs-47-to-fs-69)
+
+---
+
+## About the source
+
+The 69 FS checks are derived from the [AWS User Guide to Governance, Risk, and Compliance for
+Responsible AI Adoption](https://aws.amazon.com/blogs/security/introducing-the-updated-aws-user-guide-to-governance-risk-and-compliance-for-responsible-ai-adoption/)
+(referred to throughout as "the Responsible AI GRC guide").
+
+Each check includes how it is **detected** (the AWS API calls or configuration inspected)
+and how a failure is **remediated** (the specific AWS actions to take).
+
+## Guide traceability
+
+The Responsible AI GRC guide organizes AI-specific risks into **15 categories** (§1.2.1 through
+§1.2.15). Every check below is tagged with one of:
+
+- **[Guide §x.y.z]** — mitigation is explicitly listed in that guide section's "Mitigations or controls"
+ table or "Practical guidance" callout.
+- **[Guide §x.y.z, extension]** — mitigation is consistent with the guide's risk description but is
+ not verbatim in the guide; included because it is a widely-accepted AWS best practice for the
+ same risk. These are labelled so reviewers know the provenance.
+
+## Severity rubric
+
+Severities follow a documented **Likelihood × Impact** methodology mapped to the AWS Security Hub
+ASFF label set (`Informational | Low | Medium | High`; `Critical` is reserved, not used this
+round). The full methodology, the 3×3 scoring matrix, the N/A **disposition rules**, and the
+authoritative per-finding assignments are in
+[`SECURITY_CHECKS_FINSERV_SEVERITY_METHODOLOGY.md`](./SECURITY_CHECKS_FINSERV_SEVERITY_METHODOLOGY.md)
+and [`SECURITY_CHECKS_FINSERV_SEVERITY_REGISTER.md`](./SECURITY_CHECKS_FINSERV_SEVERITY_REGISTER.md).
+
+| Severity | Criteria (ASFF-aligned) |
+|---|---|
+| **High** | Control whose absence can lead to direct regulatory breach, data exposure, large-scale financial loss, or full bypass of safety guardrails. |
+| **Medium** | Control whose absence materially increases the likelihood or impact of a risk category but does not by itself produce a breach. |
+| **Low** | Control that reduces residual risk or supports audit/observability but has alternative or compensating controls. |
+| **Informational** | No actionable issue is asserted. Used for three dispositions: (1) **NOT_APPLICABLE** — the control's resource type is absent (e.g., no Knowledge Bases, no guardrails); (2) **ADVISORY** — the control cannot be verified via AWS APIs and requires human review (finding name prefixed `ADVISORY:`); (3) checks awaiting manual verification. |
+
+> **Disposition rules (how a finding's severity is set):** severity is a property of the *control*
+> (its Likelihood × Impact), applied to that control's `Passed` and `Failed` rows alike. The `N/A`
+> family is fixed by disposition: **NOT_APPLICABLE → Informational**, **ADVISORY → Informational**,
+> **COULD_NOT_ASSESS** (access denied / unsupported region) **→ Low**. The legacy "Advisory" tier in
+> earlier revisions of this document is reconciled to the **Informational** label + `N/A` status +
+> `ADVISORY:` name prefix.
+
+## Validation note
+
+Detection and remediation guidance in this document was systematically validated against the
+Responsible AI GRC guide, current AWS documentation, API references, and AWS announcements as
+of April 2026. IAM action names were verified against the AWS Service Authorization Reference
+for [Amazon Bedrock](https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonbedrock.html),
+[Amazon Bedrock AgentCore](https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonbedrockagentcore.html),
+and [Amazon OpenSearch Serverless](https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonopensearchserverless.html)
+(note: the OpenSearch Serverless IAM prefix is `aoss:`, not `opensearchserverless:` — the latter
+is the boto3 client name).
+CloudWatch metric namespaces were verified against the service-specific monitoring docs (Bedrock,
+Bedrock Agents, Bedrock Guardrails, SageMaker Model Monitor, SageMaker Clarify). CloudTrail
+event-type classification (management vs data) for Bedrock API operations was verified against the
+[Bedrock CloudTrail integration guide](https://docs.aws.amazon.com/bedrock/latest/userguide/logging-using-cloudtrail.html).
+Cost Anomaly Detection monitor-type values were verified against the
+[AnomalyMonitor API reference](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_AnomalyMonitor.html).
+Where AWS does not prescribe a specific value (e.g., grounding thresholds), this is explicitly
+called out as an assessment recommendation rather than an AWS requirement. AWS regional
+availability of new features (Automated Reasoning, AgentCore Policy, AWS Security Agent,
+cross-account guardrails) evolves rapidly — region lists in Parts 1-3 reflect the state at the
+cited announcement date and should be re-verified before audit reliance.
+
+## Contribution workflow
+
+The FS checks are contributed via a pull request from a personal GitHub fork of
+`aws-samples/sample-aiml-security-assessment`. For the contribution process — feature-request
+GitHub issue, fork + feature branch, Conventional Commits, PR, and reviewer assignment — see
+[`CONTRIBUTING.md`](../CONTRIBUTING.md) and the [Developer Guide](./DEVELOPER_GUIDE.md).
+
+Key quality gates before opening the PR:
+
+1. `ruff check` and `ruff format --check` pass on `functions/security/finserv_assessments/`.
+2. `cfn-lint` and `sam validate --lint` pass on the SAM templates.
+3. [ASH v3](https://awslabs.github.io/automated-security-helper/) scan
+ (`ash --source-dir . --fail-on-findings --config-overrides
+ 'global_settings.severity_threshold=MEDIUM'`) reports zero Critical / High findings,
+ or suppressions are documented in the ASH configuration used for the scan.
+4. Amazon Code Defender (`git defender scan`) reports no secrets in the staged diff.
+
+Because `aws-samples` is an OSPO-managed organization, pushes to your personal fork of
+`aws-samples/*` are auto-allowed by Code Defender — a Git Defender exception ticket is
+**not expected** for this contribution.
+
+## Relationship to upstream SM/BR/AC checks
+
+The upstream [sample-aiml-security-assessment](https://github.com/aws-samples/sample-aiml-security-assessment)
+framework already provides 52 core security checks (SM-01 to SM-25, BR-01 to BR-14, AC-01 to AC-13).
+The 69 FS checks in this document are **additive**: they enhance the upstream with FinServ-specific
+detection and remediation guidance drawn from the Responsible AI GRC guide. A few FS
+checks overlap with upstream checks — in those cases, the FS check adds FinServ-specific depth
+(e.g., protected-attribute facets, regulatory cadence requirements, denied-topic content for
+financial advice). The table below surfaces each overlap with a systematic recommendation based
+on five factors: (1) whether the detection target is the same AWS resource/configuration, (2)
+whether the FS check adds FinServ-specific regulatory specificity, (3) severity differentiation,
+(4) whether a customer would remediate them differently, and (5) guide-traceability value.
+
+**Recommendation values:**
+
+- **Extend upstream** — merge FS detection/remediation detail into the upstream check; do not ship FS as a standalone entry in the final report. Best when both checks target the same resource and the FS content is an enhancement.
+- **Keep separate** — ship as a standalone FS check alongside the upstream check. Best when the FS check targets a different AWS resource, has materially different severity, or encodes a FinServ-specific regulatory requirement that would be diluted by merging.
+
+| FS check | Upstream check | Overlap analysis | Recommendation |
+|---|---|---|---|
+| FS-17 (Model Monitor Data Quality) | SM-07 (Model Monitor) | Same resource (`sagemaker:ListMonitoringSchedules`); FS-17 adds training-data-drift-specific guidance, exact CloudWatch namespace (`/aws/sagemaker/Endpoints/data-metric`), and `emit_metrics` requirement. | **Extend SM-07** — add FS-17's detection detail (namespace, `emit_metrics`) as a refinement of the existing check |
+| FS-18 (Model Drift Detection) | SM-23 (Model Drift Detection) | Same name, same resource, same detection logic (`MonitoringType=ModelQuality`). FS-18 adds Guide §1.2.14 low-entropy classification monitoring as an early-warning poisoning indicator. | **Extend SM-23** — add low-entropy monitoring as a new remediation step on SM-23; do not ship FS-18 separately |
+| FS-19 (Model Registry Approval) | SM-08 (Model Registry) / SM-22 (Model Approval Workflow) | SM-22 is conceptually identical. FS-19 specifies exact `ModelApprovalStatus=PendingManualApproval` default and flags auto-approved latest versions. | **Extend SM-22** — add FS-19's detection specificity (flag auto-approved latest versions) to SM-22; do not ship FS-19 separately |
+| FS-20 (Feature Store Rollback) | SM-15 (Feature Store Encryption) | Different security properties on the same resource: SM-15 checks encryption; FS-20 checks `OfflineStoreConfig` presence for point-in-time rollback. | **Keep separate** — different security property; no true overlap |
+| FS-39 (SageMaker Clarify Bias) | SM-06 (Clarify Usage) | Same resource family but SM-06 is Severity Low and generic ("validates Clarify for bias detection"); FS-39 is Severity High with specific `MonitoringType=ModelBias`, protected-attribute facets (age/gender/race/geography), and specific bias metrics (DPL, DI, DPPL) for FinServ decision models. | **Keep separate** — severity, detection specificity, and FinServ regulatory context (ECOA/Fair Housing) warrant a standalone check |
+| FS-41 (SageMaker Clarify Explainability) | SM-06 (Clarify Usage) | Same as FS-39 but for `MonitoringType=ModelExplainability`. FS-41 is Severity High with SHAP analysis for adverse-action-notice use cases. | **Keep separate** — severity and adverse-action-notice regulatory context justify a standalone check |
+| FS-22 (KB IAM Least Privilege) | BR-01 (IAM Least Privilege) | BR-01 detects the managed policy `AmazonBedrockFullAccess` on any role. FS-22 inspects role policy documents for wildcard `bedrock:*` affecting KB actions and requires ARN-scoped resource restrictions. | **Keep separate** — different detection logic (managed-policy attachment vs policy-document statement analysis); FS-22 fills a detection gap BR-01 does not cover |
+| FS-23 (KB CloudTrail Logging) | BR-06 (CloudTrail Logging) | BR-06 verifies CloudTrail is logging Bedrock API calls generally. FS-23 specifically requires an advanced event selector for `AWS::Bedrock::KnowledgeBase` to capture `Retrieve`/`RetrieveAndGenerate` data events (NOT logged by default). | **Extend BR-06** — add FS-23's data-event-selector requirement as a refinement of the same CloudTrail check |
+| FS-25 (OpenSearch Serverless Encryption) | BR-09 (Knowledge Base Encryption) | Different AWS resources: BR-09 checks the Bedrock KB's `kmsKeyArn`; FS-25 checks the underlying AOSS collection's encryption policy (`aoss:ListSecurityPolicies(type=encryption)`). A KB can be CMK-encrypted while its vector store is not. | **Keep separate** — different AWS resources with independent encryption configurations; both needed for defense-in-depth |
+| FS-26 (KB VPC Access) | BR-02 (VPC Endpoint Configuration) | BR-02 checks Bedrock VPC endpoints exist. FS-26 checks the AOSS collection's network policy for `AllowFromPublic=true` (whether the vector store itself is internet-reachable). | **Keep separate** — orthogonal controls: Bedrock VPC endpoint vs vector-store network policy |
+| FS-27 (Automated Reasoning / Contextual Grounding) | BR-05 (Guardrail Configuration) | BR-05 verifies a guardrail exists and is enforced. FS-27 checks for `automatedReasoningPolicy` or `contextualGroundingPolicy` with specific threshold (≥ 0.7). | **Keep separate** — policy-level guardrail content BR-05 does not evaluate |
+| FS-28 (Financial Denied Topics) | BR-05 | BR-05 is existence; FS-28 inspects `topicPolicy.topics` for FinServ-specific denied topics (investment advice, tax advice, guaranteed returns). | **Keep separate** — FinServ denied-topic content is a regulatory-specific requirement not representable as a generic extension |
+| FS-36 (Guardrail Content Filters) | BR-05 | FS-36 inspects `contentPolicy.filters` for HATE/VIOLENCE/SEXUAL/INSULTS/MISCONDUCT/PROMPT_ATTACK with strength ≥ MEDIUM. | **Keep separate** — policy-level detection BR-05 does not cover |
+| FS-38 (Word Filters and Allowlists) | BR-05 | FS-38 inspects `wordPolicy.words` and `managedWordLists` for FinServ business-term allowlist guidance. | **Keep separate** — advisory business-term allowlist has no upstream equivalent |
+| FS-45 (Guardrail PII Filters) | BR-05 | FS-45 inspects `sensitiveInformationPolicy.piiEntities` for 12 specific PII types critical to FinServ (SSN, bank account, SWIFT code, etc.) with `inputAction=BLOCK`/`outputAction=ANONYMIZE`. | **Keep separate** — FinServ-specific PII entity list is a distinct regulatory requirement |
+| FS-47 (Grounding Threshold) | BR-05 | FS-47 checks `contextualGroundingPolicy.filters` for `GROUNDING` filter with threshold ≥ 0.7. | **Keep separate** — threshold-value check BR-05 does not perform |
+| FS-50 (Relevance Grounding Filters) | BR-05 | Same as FS-47 but for `RELEVANCE` filter type. | **Keep separate** — distinct filter type |
+| FS-51 (Prompt Attack Filters) | BR-05 | FS-51 checks `PROMPT_ATTACK` filter in Standard tier with input-tagging requirement and `inputStrength=HIGH`. | **Keep separate** — Standard-tier cross-region-inference opt-in and input-tagging nuance warrant standalone guidance |
+| FS-59 (Guardrail Topic Allowlist) | BR-05 | FS-59 checks `topicPolicy.topics` exist to block off-topic conversations (politics, entertainment, medical advice). | **Keep separate** — off-topic content restrictions are distinct from FS-28's regulated-advice restrictions; different guide section (§1.2.2 vs §1.2.1) |
+| FS-64 (Guardrail Trace Logging) | BR-04 (Model Invocation Logging) | BR-04 verifies invocation logging is enabled. FS-64 additionally verifies the log output captures `guardrailTrace` with `action`/`inputAssessments`/`outputAssessments` and adds NYDFS/SR 11-7 retention guidance. | **Extend BR-04** — add guardrail-trace verification as a refinement of the same invocation-logging check; retention guidance can be a remediation note |
+
+### Summary of consolidation recommendations
+
+- **Extend upstream (5 FS checks merged into 5 upstream checks):** FS-17 → SM-07; FS-18 → SM-23; FS-19 → SM-22; FS-23 → BR-06; FS-64 → BR-04. These checks are replaced by upstream-extension notes in Parts 1 and 3 and are removed from `finserv_assessments/app.py`.
+- **Keep separate (64 FS checks):** All other FS checks ship as standalone entries. This includes FS-20, FS-22, FS-25, FS-26, FS-39, FS-41, all Guardrail-policy-level checks (FS-27, FS-28, FS-36, FS-38, FS-45, FS-47, FS-50, FS-51, FS-59), and all FS checks that have no upstream overlap at all.
+
+After consolidation the combined framework contains **52 upstream + 64 FS = 116 distinct checks** (down from 52 + 69 = 121 before merging). The consolidation reduces duplication without losing FinServ-specific regulatory depth.
+
+
+---
+
+## Compliance Framework Mapping
+
+> **Disclaimer:** The mappings below are **preliminary and illustrative**, provided by the
+> authors of this assessment to help FSI teams start conversations with their MRM/compliance
+> colleagues. They are **not** authoritative AWS compliance guidance and they have **not** been
+> reviewed by AWS Security Assurance Services, external auditors, or the regulators whose
+> frameworks are named. Each firm should have its own MRM, Legal, and Compliance teams
+> validate these mappings against the firm's specific interpretation of each framework before
+> relying on them as audit evidence.
+
+Each FS check maps to one or more FinServ regulatory frameworks (preliminary mapping):
+
+| Framework | Description | Relevant Checks |
+|-----------|-------------|-----------------|
+| SR 11-7 | Federal Reserve Model Risk Management Guidance | FS-07, FS-10, FS-12 to FS-16, FS-20, FS-21, FS-27 to FS-33, FS-34, FS-39 to FS-42, FS-66, FS-67 |
+| FFIEC CAT | Cybersecurity Assessment Tool | All FS checks |
+| NYDFS 500 | NY Cybersecurity Regulation | FS-22, FS-43 to FS-46, FS-51 to FS-54, FS-66 |
+| PCI-DSS | Payment Card Industry Data Security Standard | FS-22, FS-25, FS-26, FS-43 to FS-46, FS-53, FS-56, FS-67, FS-68 |
+| DORA | EU Digital Operational Resilience Act | FS-01 to FS-06, FS-08, FS-11, FS-54, FS-65, FS-68 |
+| MAS TRM 9 | Monetary Authority of Singapore Technology Risk Management | FS-07 to FS-11, FS-15, FS-27 to FS-30, FS-32, FS-37, FS-39 to FS-42, FS-66, FS-67 |
+| ISO 27001 | Information Security Management | FS-13, FS-14, FS-16, FS-21, FS-33, FS-46, FS-52, FS-63, FS-65 |
+| ECOA/Fair Housing | Equal Credit Opportunity Act (US) | FS-39 to FS-42 (advisory — applicability depends on whether the model is used for ECOA-covered credit decisions; confirm with your compliance team) |
+| OWASP LLM Top 10 | OWASP LLM Application Security | FS-51 to FS-58, FS-68, FS-69 |
+
+> **FS-34 note:** FS-34 (TPRM for FM Providers) is listed above under SR 11-7. Although the
+> check appears in the Misinformation section of Part 2 for numbering continuity, its
+> primary guide source is §1.2.12 Supply Chain, which is the lens MRM and TPRM teams will
+> evaluate it through.
+
+---
+
+## Part 1 — Infrastructure & Resource Controls (FS-01 to FS-26)
+
+> **Guide risk categories:** Unbounded Consumption (FS-01..06, §1.2.11), Excessive Agency (FS-07..11, §1.2.9), Supply Chain Vulnerabilities (FS-12..16, §1.2.12), Training Data & Model Poisoning (FS-17..21, §1.2.14), Vector & Embedding Weaknesses (FS-22..26, §1.2.15). FS-17, FS-18, FS-19, and FS-23 are merged into upstream checks — see the extension notes in each section.
+
+### Unbounded Consumption (FS-01 to FS-06)
+
+> **Guide source:** §1.2.11 Unbounded consumption. Guide-listed mitigations: (a) AWS WAF and Shield
+> Advanced for LLM APIs; (b) maximum input length limits; (c) rate limits/quotas on APIs
+> accessing LLMs; (d) cost-and-usage tracking for generative AI. Practical guidance in the guide
+> also calls out `max_tokens` optimisation and CloudWatch metrics for token usage.
+
+#### FS-01 — WAF and Shield Protection
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium (WAF) / Low (Shield Advanced) |
+| Guide ref | [Guide §1.2.11] — "Protect your LLM APIs and Amazon Bedrock-hosted LLMs by using AWS WAF and AWS Shield Advanced." Also covers: "To protect your API endpoints, set maximum length limits for input requests when you use large language models (LLMs) directly or through Amazon Bedrock." |
+| Description | Verifies AWS WAF Web ACLs and Shield Advanced protect GenAI API endpoints, and verifies the Web ACL enforces both rate-based limits and body-size (input-length) constraints. |
+| Detection | Calls `shield:DescribeSubscription` to check Shield Advanced is active. Calls `wafv2:ListWebACLs(Scope=REGIONAL)` in each region where GenAI API endpoints run to verify at least one regional Web ACL exists (covers API Gateway, ALB, AppSync). **Additionally** calls `wafv2:ListWebACLs(Scope=CLOUDFRONT)` in `us-east-1` to detect Web ACLs protecting CloudFront distributions fronting GenAI workloads — CLOUDFRONT-scope Web ACLs must be created and queried in `us-east-1` per the [WAF resources documentation](https://docs.aws.amazon.com/waf/latest/developerguide/how-aws-waf-works-resources.html). For each Web ACL found, calls `wafv2:GetWebACL` and inspects the `Rules` array for: (a) at least one `RateBasedStatement` (rate limiting) and (b) at least one `SizeConstraintStatement` with `FieldToMatch=Body` or `FieldToMatch=JsonBody` (input-size limit — this implements Guide §1.2.11 mitigation "set maximum length limits for input requests when you use large language models (LLMs) directly or through Amazon Bedrock"). Flags accounts with no Web ACL in either scope, a Web ACL with no rate-based rule, a Web ACL with no body size-constraint rule, or where Shield Advanced is inactive. |
+| Remediation | 1. Subscribe to AWS Shield Advanced via the Shield console. 2. Create a WAF Web ACL with both (a) a rate-based rule (e.g., 1 000 req / 5 min per IP) and (b) a `SizeConstraintStatement` that blocks requests where `FieldToMatch=Body` (or `JsonBody` for JSON APIs) exceeds your LLM's expected maximum input size — for example, `ComparisonOperator=GT, Size=100000` (100 KB) — use `Scope=REGIONAL` for API Gateway/ALB/AppSync resources, or `Scope=CLOUDFRONT` (created in `us-east-1`) for CloudFront distributions fronting Bedrock. The body size-constraint rule directly implements the Guide §1.2.11 mitigation "set maximum length limits for input requests when you use large language models (LLMs) directly or through Amazon Bedrock" and prevents large-prompt token-exhaustion attacks before they reach Bedrock. 3. Associate the ACL with the fronting resource (API Gateway stage, ALB, or CloudFront distribution). 4. Add AWS Managed Rules (e.g., `AWSManagedRulesCommonRuleSet`, which includes additional size checks). 5. For CloudFront-fronted workloads, register the distribution with Shield Advanced via `shield:CreateProtection` to unlock automatic application-layer DDoS mitigation. 6. For API Gateway REST APIs, also note the service's own payload-size quota: the default is 10 MB per request (see [API Gateway quotas](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-execution-service-limits-table.html)); use a request validator or Lambda authorizer for sub-10 MB limits where WAF size constraints are unsuitable. |
+| Reference | [Shield Advanced](https://docs.aws.amazon.com/waf/latest/developerguide/shield-chapter.html), [WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html), [WAF Size Constraint Rule](https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-type-size-constraint-match.html), [API Gateway Quotas](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-execution-service-limits-table.html) |
+
+#### FS-02 — API Gateway Rate Limiting
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.11] — "protect your API endpoints by implementing rate limits and quotas for APIs that access large language models (LLMs)". |
+| Description | Checks API Gateway usage plans enforce throttling on GenAI endpoints. |
+| Detection | Calls `apigateway:GetUsagePlans` and inspects each plan's `throttle.rateLimit` and `throttle.burstLimit`. Flags plans where either is zero or absent. |
+| Remediation | 1. Create or update usage plans with `rateLimit` and `burstLimit` values appropriate for your traffic. 2. Associate plans with API stages serving Bedrock. 3. Issue per-consumer API keys with individual quotas. |
+| Reference | [API Gateway Throttling](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html) |
+
+#### FS-03 — Bedrock Token Quota Review
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.11, extension] — guide practical guidance notes "Bedrock has default quota on model inference based on token usage" and recommends optimising `max_tokens`. Quota review as an operational control is an extension aligned with this guidance. |
+| Description | Verifies Bedrock TPM/RPM quotas have been reviewed and set appropriately. |
+| Detection | Calls `service-quotas:ListServiceQuotas(ServiceCode=bedrock)` for applied quotas and `ListAWSDefaultServiceQuotas` for defaults, then compares each adjustable quota's `Value` against the default `Value`. Flags accounts where every quota equals the service default (indicating no quota review or increase has been requested). |
+| Remediation | 1. Review current quotas in the Service Quotas console. 2. Request increases aligned with expected peak load via `service-quotas:RequestServiceQuotaIncrease`. 3. Implement client-side token counting and pre-flight quota checks. 4. Use Bedrock cross-region inference profiles to distribute load — note that cross-region inference routes requests across destination regions automatically with no additional cost, but requires the invoked model to be available in the destination regions defined in the inference profile. |
+| Reference | [Bedrock Quotas](https://docs.aws.amazon.com/bedrock/latest/userguide/quotas.html) |
+
+#### FS-04 — Cost Anomaly Detection
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.11] — "Track, allocate, and manage your costs and usage for generative AI." |
+| Description | Checks AWS Cost Anomaly Detection monitors cover Bedrock/SageMaker. |
+| Detection | Calls `ce:GetAnomalyMonitors` and inspects each monitor. AWS Cost Anomaly Detection supports exactly two `MonitorType` values per the [AnomalyMonitor API](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_AnomalyMonitor.html): `DIMENSIONAL` (AWS-managed, where `MonitorDimension` is one of `SERVICE`, `LINKED_ACCOUNT`, `TAG`, or `COST_CATEGORY`) and `CUSTOM` (customer-managed, scoped via `MonitorSpecification` to specific values). For `DIMENSIONAL` monitors, checks `MonitorDimension=SERVICE` (the AWS-managed "AWS services" monitor that automatically covers all services including Bedrock and SageMaker — the recommended default). For `CUSTOM` monitors, inspects `MonitorSpecification` for references to Bedrock or SageMaker. Flags accounts with no monitors, or with only narrowly-scoped monitors that would not detect Bedrock cost anomalies (e.g., `DIMENSIONAL` with `MonitorDimension=LINKED_ACCOUNT` only). |
+| Remediation | 1. Create an AWS-managed `DIMENSIONAL` monitor with `MonitorDimension=SERVICE` for comprehensive coverage across all AWS services (the recommended default — in the console this appears as "AWS services" under "Managed by AWS"). For narrower scope, add a `CUSTOM` monitor using `MonitorSpecification` with a `Dimensions` expression scoped to specific service values (e.g., `{"Dimensions": {"Key": "SERVICE", "Values": ["Amazon Bedrock", "Amazon SageMaker"]}}`) — note that for `CUSTOM` monitors you use `MonitorSpecification`, not `MonitorDimension`. 2. Configure alert subscriptions (SNS/email) for anomalies above threshold. 3. Set daily spend budgets with AWS Budgets as a secondary control. 4. Enable Bedrock IAM principal cost allocation: tag IAM users/roles with team or cost-center attributes, activate them as cost allocation tags in the Billing and Cost Management console, and include caller identity data in CUR 2.0 exports for per-user/per-team Bedrock spend attribution. |
+| Reference | [Cost Anomaly Detection](https://docs.aws.amazon.com/cost-management/latest/userguide/getting-started-ad.html), [Bedrock IAM Cost Allocation](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/iam-principal-cost-allocation.html) |
+
+#### FS-05 — CloudWatch Token Usage Alarms
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.11] — guide practical guidance cites CloudWatch metrics for token usage; alarms operationalise that guidance. |
+| Description | Verifies CloudWatch alarms exist for Bedrock throttling and token metrics. |
+| Detection | Paginates `cloudwatch:DescribeAlarms(AlarmTypes=MetricAlarm)` and filters for alarms in the `AWS/Bedrock` namespace or with "bedrock" in the alarm name. Separately counts throttle-specific alarms. |
+| Remediation | 1. Create alarms for `AWS/Bedrock InvocationThrottles` (threshold > 0). 2. Create alarms for `AWS/Bedrock EstimatedTPMQuotaUsage` to track approach to token quota limits, and separately on `InputTokenCount` + `OutputTokenCount` (sum via CloudWatch metric math) for absolute token consumption. Note: `TokensProcessed` is not a valid Bedrock metric — the correct runtime metrics are `InputTokenCount`, `OutputTokenCount`, `InvocationThrottles`, `EstimatedTPMQuotaUsage`, `Invocations`, `InvocationLatency`, `TimeToFirstToken`. 3. Publish custom application-level token counters via Embedded Metric Format (EMF) if you need per-tenant or per-feature attribution. 4. Attach SNS actions to all alarms. |
+| Reference | [Bedrock CloudWatch Metrics](https://docs.aws.amazon.com/bedrock/latest/userguide/monitoring.html) |
+
+#### FS-06 — AWS Budgets AI/ML Spend
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.11] — "Track, allocate, and manage your costs and usage for generative AI." |
+| Description | Checks AWS Budgets are configured with alerts for AI/ML service spend. |
+| Detection | Calls `budgets:DescribeBudgets` and inspects each budget's `FilterExpression` (the current field) and `CostFilters` (deprecated but may still be populated on older budgets) for references to "bedrock" or "sagemaker". Note: `CostFilters` is marked deprecated in the AWS Budgets API — new budgets use `FilterExpression` with an `Expression` object; the detection should check both fields to cover both old and new budgets. |
+| Remediation | 1. Create cost budgets for Bedrock and SageMaker with 80 %/100 % alert thresholds. 2. Add SNS notifications to on-call channels. 3. Consider budget actions to apply IAM deny policies when thresholds are breached. 4. Enable Bedrock IAM principal cost allocation to attribute inference costs to specific IAM users/roles via Cost Explorer and CUR 2.0 — tag IAM principals with team or cost-center attributes and activate them as cost allocation tags. |
+| Reference | [AWS Budgets](https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-managing-costs.html), [Bedrock IAM Cost Allocation](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/iam-principal-cost-allocation.html) |
+
+### Excessive Agency (FS-07 to FS-11)
+
+> **Guide source:** §1.2.9 Excessive agency. Guide-listed mitigations: (a) Amazon Bedrock AgentCore
+> for managing complex tasks; (b) least-privilege permissions on plugins; (c) human-in-the-loop
+> output validation; (d) explicit action boundaries in agent configuration (AgentCore Policy);
+> (e) audit logging of agent actions with reasoning chain (AgentCore Observability);
+> (f) transaction-value thresholds on agent tool calls; (g) monitoring agent call rates with
+> alarms (AgentCore Evaluations). Mitigation (e) is covered by the expanded FS-08 check, which
+> now verifies both AgentCore Policy Engine and AgentCore Observability are configured.
+
+#### FS-07 — Agent Action Boundaries
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.9] — "grant only the minimum permissions required"; "Define and enforce explicit action boundaries in the agent configuration". |
+| Description | Verifies Bedrock agent execution roles have no wildcard sensitive actions (iam:\*, s3:\*, ec2:\*, lambda:\*, \*). |
+| Detection | Calls `ListAgents` and `GetAgent` (via the `bedrock-agent` boto3 client; IAM actions are `bedrock:ListAgents` and `bedrock:GetAgent`) to retrieve each agent's `agentResourceRoleArn`. Resolves the role name and inspects attached and inline policy documents from the permissions cache for wildcard Allow statements. |
+| Remediation | 1. Replace wildcard actions with the specific actions the agent needs. 2. Apply IAM permission boundaries to agent execution roles. 3. Use resource-level conditions to restrict to specific ARNs. 4. Implement human-in-the-loop approval for high-impact actions. 5. For agents deployed in a VPC, use **AWS Network Firewall** with domain-based filtering to control which external domains agents can reach — this provides a network-layer boundary that limits agent tool access to approved endpoints regardless of IAM permissions. |
+| Reference | [Bedrock Agent Permissions](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-permissions.html), [Control Agent Domain Access](https://aws.amazon.com/blogs/machine-learning/control-which-domains-your-ai-agents-can-access/) |
+
+#### FS-08 — AgentCore Policy Engine and Observability
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.9] — "Use Amazon Bedrock AgentCore to manage complex tasks and connect securely"; "Define and enforce explicit action boundaries"; **"Implement audit logging of all actions taken by AI agents, including the reasoning chain that led to each action."** (The audit-logging mitigation's guide reference is "Observe your agent applications on Amazon Bedrock AgentCore Observability.") |
+| Description | Checks AgentCore Gateways have a Policy Engine attached to authorize agent-to-tool interactions, verifies AgentCore Runtimes have an inbound authorizer configured, and verifies AgentCore Observability is enabled so agent reasoning chains and tool calls are auditable. |
+| Detection | (a) Calls `ListGateways` and `GetGateway` (via the `bedrock-agentcore-control` boto3 client; IAM actions are `bedrock-agentcore:ListGateways` and `bedrock-agentcore:GetGateway`); inspects `policyEngineConfiguration.arn` and `policyEngineConfiguration.mode` (must be `ENFORCE` for production). (b) Calls `ListAgentRuntimes` (IAM action `bedrock-agentcore:ListAgentRuntimes`) and inspects each runtime's `authorizerConfiguration.customJWTAuthorizer` for inbound auth. (c) Verifies **AgentCore Observability** is enabled by (i) checking that CloudWatch Transaction Search is on via `xray:GetTraceSegmentDestination` (destination should be `CloudWatchLogs`) and that the X-Ray → CloudWatch Logs resource policy is in place via `logs:GetResourcePolicy`, and (ii) calling `logs:DescribeDeliveries` / `logs:DescribeDeliverySources` for AgentCore resource sources (runtime, memory, gateway, built-in tools, identity) — flags runtimes/gateways with no log delivery configured. For memory resources, additionally checks that tracing was enabled at memory creation time. Flags gateways without a Policy Engine in `ENFORCE` mode, runtimes without an authorizer, or accounts where Transaction Search is not enabled or no delivery exists for AgentCore resources. |
+| Remediation | 1. Configure a Policy Engine: create via `CreatePolicyEngine` (IAM action `bedrock-agentcore:CreatePolicyEngine`), then author Cedar policies using one of three methods: (a) write Cedar directly for fine-grained control via `CreatePolicy` (IAM action `bedrock-agentcore:CreatePolicy`), (b) use the form-based console UI, or (c) generate Cedar from natural language descriptions (natural-language-to-Cedar is a documented capability in the GA announcement; verify the exact IAM action name against the current [AgentCore Service Authorization Reference](https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonbedrockagentcore.html) before writing IAM policies for it). Policy in AgentCore went GA on March 3, 2026 in thirteen AWS regions (US East N. Virginia, US East Ohio, US West Oregon, Asia Pacific Mumbai/Seoul/Singapore/Sydney/Tokyo, Europe Frankfurt/Ireland/London/Paris/Stockholm) — verify current regional availability on the [launch announcement](https://aws.amazon.com/about-aws/whats-new/2026/03/policy-amazon-bedrock-agentcore-generally-available/) before audit reliance. 2. Attach the Policy Engine to each Gateway by specifying the Policy Engine ARN in the `policyEngineConfiguration` field during `CreateGateway`, or attach later via `UpdateGateway`. 3. Start in `LOG_ONLY` mode — the policy engine evaluates actions and logs whether they would be allowed or denied without enforcing the decision — then switch to `ENFORCE` mode once confident. 4. Configure a JWT inbound authorizer on each Runtime with discovery URL, allowed audiences, and allowed clients. 5. **Enable AgentCore Observability** so agent reasoning chains are captured (directly addresses the Guide §1.2.9 audit-logging mitigation): (a) one-time enable CloudWatch Transaction Search — console path **CloudWatch → Application Signals (APM) → Transaction search → Enable Transaction Search**, or CLI: `aws xray update-trace-segment-destination --destination CloudWatchLogs` plus a `logs:PutResourcePolicy` granting `xray.amazonaws.com` permission to `logs:PutLogEvents` on `aws/spans:*` and `/aws/application-signals/data:*`; (b) configure log delivery for AgentCore runtime, memory, gateway, built-in tools, and identity resources via `logs:PutDeliverySource` + `logs:PutDeliveryDestination` + `logs:CreateDelivery` (CloudWatch Logs / S3 / Firehose destinations supported; note the write APIs use `Put*` for source and destination but `Create*` for the delivery pairing); (c) enable tracing at memory creation. For traditional Bedrock Agents (non-AgentCore), set `enableTrace=true` on `InvokeAgent` calls to receive the reasoning-chain trace in the response. |
+| Reference | [Policy in AgentCore](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy.html), [Inbound JWT Authorizer](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/inbound-jwt-authorizer.html), [AgentCore Observability Configuration](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability-configure.html), [Bedrock Agent Trace View](https://docs.aws.amazon.com/bedrock/latest/userguide/trace-view.html) |
+
+#### FS-09 — Agent Transaction Limits
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.9, extension] — Lambda reserved concurrency is not named in the guide, but it directly implements the guide mitigation "Monitor agent call rates and alarm upon exceeding defined thresholds" by capping execution parallelism. |
+| Description | Verifies agent Lambda functions have reserved concurrency limits to cap execution parallelism. |
+| Detection | Calls `lambda:ListFunctions` and filters for functions with agent-related naming patterns. For each, calls `lambda:GetFunctionConcurrency` and flags functions with no reserved concurrency set. |
+| Remediation | 1. Set reserved concurrency on each agent action-group Lambda (e.g., 10–50 depending on expected load). 2. Add CloudWatch alarms for `Throttles` metric on these functions. 3. Consider Step Functions execution limits as an additional control. |
+| Reference | [Lambda Reserved Concurrency](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html) |
+
+#### FS-10 — Human-in-the-Loop Approval
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.9, §1.2.1, §1.2.2, §1.2.3, §1.2.7, §1.2.10] — "For internal AI systems, validate outputs with human review before business use (human-in-the-loop)." HITL is referenced in six separate guide risk sections. |
+| Description | Checks Step Functions workflows have human approval steps for high-risk agent actions. |
+| Detection | Calls `stepfunctions:ListStateMachines` and filters for agent/GenAI-related names. Retrieves each definition via `stepfunctions:DescribeStateMachine` and parses the ASL JSON for task states with `.waitForTaskToken` or callback patterns indicating human approval gates. |
+| Remediation | 1. Add a callback-pattern task state in your Step Functions workflow before any high-risk action (financial transactions, data modifications, external communications). 2. Route the approval token to a human reviewer via SNS/SQS/Slack. 3. Set a `HeartbeatSeconds` timeout so stale approvals expire. 4. Enable **user confirmation on Bedrock Agent action groups** for inline approval — when configured, the agent returns a confirmation prompt in the `returnControl.invocationInputs` field of the `InvokeAgent` response (alongside `invocationType` and a unique `invocationId`); the client displays the prompt, collects confirm/deny, and returns the user's decision via `sessionState.returnControlInvocationResults` (with `confirmationState` on each `apiResult`/`functionResult`) in the next `InvokeAgent` request (there is no standalone `GetUserConfirmation` API). |
+| Reference | [Step Functions Callback Pattern](https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#connect-wait-token), [Bedrock Agent User Confirmation](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-userconfirmation.html) |
+
+#### FS-11 — Agent Rate Alarms
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.9] — "Monitor agent call rates and alarm upon exceeding defined thresholds." |
+| Description | Verifies CloudWatch alarms exist for agent invocation rates. |
+| Detection | Paginates `cloudwatch:DescribeAlarms` and filters for alarms referencing "agent" in the alarm name or targeting `AWS/Bedrock/Agents` agent-related metrics (such as `InvocationCount` or `InvocationThrottles` with the `Operation, AgentAliasArn, ModelId` dimension combination). |
+| Remediation | 1. Create CloudWatch alarms on the `AWS/Bedrock/Agents` namespace for `InvocationCount` and `InvocationThrottles`. Per AWS docs, the available dimensions are: `Operation` alone; `Operation, ModelId`; or `Operation, AgentAliasArn, ModelId` — use the `Operation, AgentAliasArn, ModelId` combination to scope alarms to a specific agent alias. 2. Set thresholds based on expected peak agent call rates, established via CloudWatch metric math on historical `InvocationCount` data. 3. Attach SNS actions for on-call notification. 4. Use **AgentCore Evaluations** (GA March 2026, available in 9 AWS regions — verify current regional availability on the [GA announcement](https://aws.amazon.com/about-aws/whats-new/2026/03/agentcore-evaluations-generally-available/)) to monitor agent *quality* alongside rate-based alarms: online evaluation continuously scores production traffic against 13 built-in evaluators (response quality, safety, task completion, tool usage), and on-demand evaluation supports regression testing. |
+| Reference | [Bedrock Agents CloudWatch Metrics](https://docs.aws.amazon.com/bedrock/latest/userguide/monitoring-agents-cw-metrics.html), [AgentCore Evaluation Types](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/evaluations-types.html) |
+
+### Supply Chain Vulnerabilities (FS-12 to FS-16)
+
+> **Guide source:** §1.2.12 Supply chain vulnerabilities. Guide-listed mitigations:
+> (a) control access to serverless and marketplace models (IAM policies, SCPs);
+> (b) model onboarding process — EULA review, procurement, security/compliance review,
+> MRM assessment, documentation, stakeholder approvals;
+> (c) update TPRM to continuously monitor model providers — vendor security advisories,
+> deprecation notices, T&C changes;
+> (d) maintain a model inventory recording provenance, version, license terms, and risk
+> assessment status;
+> (e) use Bedrock Evaluations against attack test cases (practical guidance);
+> (f) allow-list approved models via SCP (practical guidance).
+
+#### FS-12 — SCP Model Access Restrictions
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.12 — Practical guidance] — "Implement an allow-list of models using a Service Control Policy (SCP) for your AWS organization." |
+| Description | Checks SCPs restrict Bedrock model access to approved models only. |
+| Detection | Calls `organizations:ListPolicies(Filter=SERVICE_CONTROL_POLICY)` and inspects each SCP document for Deny statements on `bedrock:InvokeModel*` with `StringNotEquals` conditions on `bedrock:ModelId`. Flags if no SCP restricts model access. |
+| Remediation | 1. Create an SCP that denies `bedrock:InvokeModel*` except for an explicit allowlist of approved model ARNs. 2. Attach the SCP to the OU containing GenAI workload accounts. 3. For multi-account guardrail enforcement, use the Bedrock cross-account safeguards feature (GA April 3, 2026, available in all AWS commercial and GovCloud regions where Bedrock Guardrails is supported): enable the Amazon Bedrock policy type in AWS Organizations, create a guardrail in the management account, create a versioned guardrail, optionally attach a resource-based policy granting `bedrock:ApplyGuardrail` to member accounts for cross-account access, then create and attach an AWS Organizations Bedrock policy referencing the guardrail ARN and version to the target OUs or accounts. This automatically enforces content filters, denied topics, word filters, sensitive information filters, and contextual grounding checks across all member accounts for every model invocation — no application code changes required. **Important limitation:** Automated Reasoning checks are **not supported** with cross-account safeguards — omit Automated Reasoning policies from any guardrail used for org-level enforcement. If you rely on AR (see FS-27), you must configure AR guardrails separately at the application or account level. 4. Test with both allowed and denied model IDs. |
+| Reference | [Managing Access in AWS Organizations](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps.html), [Bedrock Cross-Account Guardrails](https://aws.amazon.com/blogs/aws/amazon-bedrock-guardrails-supports-cross-account-safeguards-with-centralized-control-and-management/) |
+
+#### FS-13 — Model Inventory Tagging
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.12] — "Maintain a model inventory that records the provenance, version, license terms, and risk assessment status of all models in use across the organization." |
+| Description | Verifies models are tagged with provenance metadata (source, version, approval-date). |
+| Detection | Calls `bedrock:ListFoundationModels` and `bedrock:ListCustomModels`. For custom models, calls `bedrock:ListTagsForResource` and checks for required tag keys: `model-source`, `model-version`, `approval-date`, `risk-tier`. |
+| Remediation | 1. Define a mandatory tagging policy for all AI/ML models. 2. Tag each custom model with provenance metadata. 3. Create an AWS Config rule (`required-tags`) to enforce the tagging policy. 4. For foundation models, maintain an external inventory spreadsheet or CMDB entry. |
+| Reference | [Bedrock Tagging](https://docs.aws.amazon.com/bedrock/latest/userguide/tagging.html) |
+
+#### FS-14 — Model Onboarding Governance
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.12] — "To onboard a model, follow these steps: Review EULA, Complete procurement, Follow security and compliance procedures, Assess MRM requirements, Document findings, Get necessary approvals from stakeholders." |
+| Description | Checks AWS Config rules enforce model onboarding governance (EULA review, MRM assessment, stakeholder approval). |
+| Detection | Calls `config:DescribeConfigRules` and searches for rules targeting `AWS::Bedrock::*` resources or custom rules with "model" or "onboarding" in the name. |
+| Remediation | 1. Create a custom AWS Config rule that checks new Bedrock custom models have required tags (approval-date, risk-tier, eula-reviewed). 2. Document the model onboarding process: EULA review → procurement → security/compliance review → MRM assessment → stakeholder sign-off. 3. Store approval artifacts in a versioned S3 bucket. |
+| Reference | [AWS Config Custom Rules](https://docs.aws.amazon.com/config/latest/developerguide/evaluate-config_develop-rules.html) |
+
+#### FS-15 — Adversarial Model Evaluation
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.12 — Practical guidance] — "Amazon Bedrock Evaluations can help to evaluate models against specific types of attacks by automating your test cases, scoring, reporting and to enable comparison of different models." |
+| Description | Verifies Bedrock evaluation jobs include adversarial test datasets. |
+| Detection | Calls `bedrock:ListEvaluationJobs` and inspects each job's configuration for evaluation datasets. Flags if no evaluation jobs exist or if none reference adversarial/red-team test data. |
+| Remediation | 1. Create a Bedrock model evaluation job with adversarial prompt datasets (prompt injection attempts, jailbreak sequences, harmful content probes). 2. Include both automated metrics and human evaluation. 3. Run evaluations before production deployment and after model updates. 4. Store results for audit. |
+| Reference | [Bedrock Model Evaluation](https://docs.aws.amazon.com/bedrock/latest/userguide/evaluation.html) |
+
+#### FS-16 — ECR Image Scanning
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.12, extension] — ECR image scanning is not named in the guide, but directly mitigates the guide's listed risk "Third-party package vulnerabilities" in LLM supply chains. Included for completeness of the supply-chain risk category. |
+| Description | Checks ECR repositories have scan-on-push enabled for supply chain security of model containers. |
+| Detection | Calls `ecr:DescribeRepositories` and for each repository checks `imageScanningConfiguration.scanOnPush`. Also checks whether Amazon Inspector ECR scanning is enabled via `inspector2:BatchGetAccountStatus`. Flags repositories relying solely on basic scanning or with no scanning configured. |
+| Remediation | 1. Enable **enhanced scanning** via Amazon Inspector (the current best practice) — Inspector provides continuous vulnerability monitoring, re-scanning images automatically when new CVEs are published, and covers both OS and programming language package vulnerabilities. This requires two steps: (a) enable Inspector ECR scanning at the account level — `aws inspector2 enable --account-ids --resource-types ECR`; (b) set the ECR registry scanning configuration to enhanced mode — `aws ecr put-registry-scanning-configuration --scan-type ENHANCED --rules '[{"scanFrequency":"CONTINUOUS_SCAN","repositoryFilters":[{"filter":"*","filterType":"WILDCARD"}]}]'`. **Important limitations:** (i) When enhanced scanning is first enabled, Amazon Inspector only discovers images pushed within the **last 14 days** — older images receive `SCAN_ELIGIBILITY_EXPIRED` status and must be re-pushed to be scanned. (ii) After the initial scan, scan duration is controlled by the ECR re-scan duration setting in the Amazon Inspector console (defaults to `LIFETIME`); if you shorten this duration, images whose last scan exceeds the new window also move to `SCAN_ELIGIBILITY_EXPIRED`. (iii) Enhanced scanning incurs Amazon Inspector charges (no additional ECR cost). (iv) Repositories not matching a scan filter will have `Off` scan frequency and won't be scanned. 2. If enhanced scanning is not available in your region, enable basic scan-on-push as a fallback: `aws ecr put-image-scanning-configuration --repository-name --image-scanning-configuration scanOnPush=true`. 3. Create EventBridge rules to alert on CRITICAL/HIGH findings from Inspector. 4. Integrate findings into your vulnerability management workflow. |
+| Reference | [ECR Enhanced Scanning](https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning-enhanced.html), [Amazon Inspector ECR Scanning](https://docs.aws.amazon.com/inspector/latest/user/scanning-ecr.html) |
+
+### Training Data & Model Poisoning (FS-17 to FS-21)
+
+> **Guide source:** §1.2.14 Training data and model poisoning. Guide-listed mitigations:
+> (a) protect training datasets via data protection best practices;
+> (b) use trusted data sources with audit controls tracking changes (who/when);
+> (c) monitor training data for pattern/distribution changes (data drift);
+> (d) compare retrained model performance against baseline before production;
+> (e) rollback plan using versioned training data and models (Feature Store);
+> (f) monitor low-entropy classification with thresholds and alerts;
+> (g) AI Service Cards for evaluating third-party model testing procedures.
+
+#### FS-17 — Model Monitor Data Quality → *Merged into upstream SM-07*
+
+> **Upstream extension note (do not ship as a standalone check):** The detection and remediation
+> content from FS-17 should be added as a refinement of the existing **SM-07 (Model Monitor)**
+> check in the upstream `aws-samples/sample-aiml-security-assessment` repo.
+>
+> **What to add to SM-07:**
+>
+> - Filter `ListMonitoringSchedules` results for `MonitoringType=DataQuality` (not just any schedule). Note the format difference: `ListMonitoringSchedules`/`MonitoringScheduleSummary` returns `MonitoringType` in PascalCase (`DataQuality`, `ModelQuality`, `ModelBias`, `ModelExplainability`); `DescribeMonitoringSchedule` returns the same type in SCREAMING_SNAKE_CASE (`DATA_QUALITY`, `MODEL_QUALITY`, `MODEL_BIAS`, `MODEL_EXPLAINABILITY`) — the detection should normalise both forms.
+> - Require `emit_metrics` to be enabled on the monitoring schedule.
+> - Verify CloudWatch alarms exist on the `feature_baseline_drift_` metrics published
+> to namespace `/aws/sagemaker/Endpoints/data-metric` (real-time endpoints, dimensions
+> `EndpointName` + `ScheduleName`) or `/aws/sagemaker/ModelMonitoring/data-metric` (batch
+> transform, dimension `MonitoringSchedule`).
+> - Guide traceability: [Guide §1.2.14] — "Monitor your training data for pattern and distribution
+> changes to detect data drift"; "Amazon SageMaker Model Monitor – Data quality."
+>
+> **Reference:** [SageMaker Model Monitor Data Quality](https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor-data-quality.html)
+
+#### FS-18 — Model Drift Detection → *Merged into upstream SM-23*
+
+> **Upstream extension note (do not ship as a standalone check):** The detection and remediation
+> content from FS-18 should be added as a refinement of the existing **SM-23 (Model Drift
+> Detection)** check in the upstream repo.
+>
+> **What to add to SM-23:**
+> - Filter `ListMonitoringSchedules` results for `MonitoringType=ModelQuality`.
+> - Add a new remediation step for **low-entropy classification monitoring** (Guide §1.2.14
+> mitigation): publish custom CloudWatch metrics tracking prediction confidence distributions,
+> set threshold boundaries for unexpected low-confidence/high-confidence clusters, and alert
+> when the retrained model produces unexpected classification patterns — this can indicate
+> training data poisoning before accuracy metrics degrade.
+> - Guide traceability: [Guide §1.2.14] — "Before deploying to production, compare your retrained
+> model's performance against previous iterations using historical test data as a baseline."
+>
+> **Reference:** [SageMaker Model Monitor Model Quality](https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor-model-quality.html)
+
+#### FS-19 — Model Registry Approval → *Merged into upstream SM-22*
+
+> **Upstream extension note (do not ship as a standalone check):** The detection and remediation
+> content from FS-19 should be added as a refinement of the existing **SM-22 (Model Approval
+> Workflow)** check in the upstream repo.
+>
+> **What to add to SM-22:**
+> - Explicitly check that `ModelApprovalStatus=PendingManualApproval` is the default for new
+> model package versions (not `Approved`).
+> - Flag any model package group where the latest version has `ModelApprovalStatus=Approved`
+> without evidence of a manual approval step (i.e., auto-approved at creation time).
+> - Guide traceability: [Guide §1.2.14] — cites "Amazon SageMaker AI – Model Registration and
+> Deployment with Model Registry" as a reference for staged deployment with rollback.
+>
+> **Reference:** [SageMaker Model Registry](https://docs.aws.amazon.com/sagemaker/latest/dg/model-registry.html)
+
+#### FS-20 — Feature Store Rollback
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.14] — "Create a rollback plan by using versioned training data and models. This ensures that you can revert to a stable, working model if failures occur." References "Amazon SageMaker AI Feature Store". |
+| Description | Checks SageMaker Feature Store has offline store for rollback capability. |
+| Detection | Calls `sagemaker:ListFeatureGroups` to enumerate all groups, then `sagemaker:DescribeFeatureGroup` for each to inspect `OfflineStoreConfig`. Flags feature groups where `OfflineStoreConfig` is absent (online-only groups with no offline store for rollback). |
+| Remediation | 1. Enable the offline store on each feature group: specify an S3 URI and data catalog in `OfflineStoreConfig`. 2. The offline store provides a versioned, immutable history of feature values for point-in-time rollback. 3. Test rollback by querying the offline store with a historical timestamp. |
+| Reference | [SageMaker Feature Store](https://docs.aws.amazon.com/sagemaker/latest/dg/feature-store.html) |
+
+#### FS-21 — Training Data S3 Versioning and Audit Trail
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.14] — "Use trusted data sources for your training data. Implement audit controls that let you track and review changes, including who made them and when they occurred." |
+| Description | Verifies S3 buckets used for training data have versioning enabled so poisoned datasets can be rolled back. Recommends CloudTrail data-event logging as remediation to record who modified training data and when. |
+| Detection | Identifies training-data S3 buckets by naming convention (`train`/`dataset`/`model`/`sagemaker`/`bedrock`). Calls `s3:GetBucketVersioning` to verify `Status=Enabled`. (CloudTrail data-event logging is recommended in remediation but is not asserted by this check — verifying it is covered by the upstream BR-06 CloudTrail control and the FS-23 extension.) |
+| Remediation | 1. Enable versioning: `aws s3api put-bucket-versioning --bucket --versioning-configuration Status=Enabled`. 2. Enable CloudTrail S3 data events for the training-data buckets to capture PutObject/DeleteObject with caller identity. 3. Enable MFA Delete for critical training datasets. 4. Apply S3 Object Lock for immutable baselines. |
+| Reference | [S3 Versioning](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html), [CloudTrail Data Events](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/logging-data-events-with-cloudtrail.html) |
+
+### Vector & Embedding Weaknesses (FS-22 to FS-26)
+
+> **Guide source:** §1.2.15 Vector and embedding weaknesses. Guide-listed mitigations:
+> (a) apply least privilege to vector and embedding database access;
+> (b) validate knowledge base data sources;
+> (c) add data only from trusted sources to knowledge bases;
+> (d) monitor and log all activities in knowledge base control plane (CloudTrail);
+> (e) enable encryption at rest and in transit for vector and embedding databases;
+> (f) implement document/record-level access controls via KB metadata filtering for
+> multi-tenancy.
+
+#### FS-22 — Knowledge Base IAM Least Privilege
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.15] — "Apply the principle of least privilege to control access to your vector and embedding database. Only grant users and services the minimum permissions they need to perform their tasks." |
+| Description | Checks IAM roles accessing Knowledge Bases have no wildcard `bedrock:*` permissions covering KB actions. |
+| Detection | Inspects the permissions cache for all IAM roles. Flags any role with an Allow statement granting `bedrock:*` without resource-level restrictions, or broad `bedrock:` actions covering KB operations without a specific knowledge-base ARN. Note: Bedrock agent and KB operations use the single IAM service prefix `bedrock:` (not `bedrock-agent:`) — the `bedrock-agent` token refers to the boto3 SDK client name, not the IAM action prefix. |
+| Remediation | 1. Replace wildcard `bedrock:*` with specific KB actions: `bedrock:Retrieve`, `bedrock:RetrieveAndGenerate`, `bedrock:GetKnowledgeBase` (these are the actual IAM action names — verify via the AWS Service Authorization Reference for Amazon Bedrock). 2. Scope the resource ARN to specific Knowledge Base IDs (e.g., `arn:aws:bedrock:::knowledge-base/`). 3. Apply IAM permission boundaries to limit blast radius. |
+| Reference | [Bedrock Knowledge Base Permissions](https://docs.aws.amazon.com/bedrock/latest/userguide/kb-permissions.html) |
+
+#### FS-23 — Knowledge Base CloudTrail Logging → *Merged into upstream BR-06*
+
+> **Upstream extension note (do not ship as a standalone check):** The detection and remediation
+> content from FS-23 should be added as a refinement of the existing **BR-06 (CloudTrail
+> Logging)** check in the upstream repo.
+>
+> **What to add to BR-06:**
+> - After verifying that a CloudTrail trail is active and logging Bedrock management events,
+> additionally check for an **advanced event selector** with
+> `resources.type = AWS::Bedrock::KnowledgeBase` to capture `Retrieve` and
+> `RetrieveAndGenerate` data events (these are NOT logged by default — they require an
+> explicit advanced event selector).
+> - Note: `InvokeAgent` / `InvokeInlineAgent` are also data events requiring
+> `resources.type = AWS::Bedrock::AgentAlias` or `AWS::Bedrock::InlineAgent` respectively.
+> Data events incur additional CloudTrail charges and can produce high volumes under load.
+> - Guide traceability: [Guide §1.2.15] — "Monitor and log all activities in knowledge base
+> control plane" with reference "Monitor Amazon Bedrock API calls using CloudTrail."
+>
+> **Reference:** [CloudTrail Bedrock Logging](https://docs.aws.amazon.com/bedrock/latest/userguide/logging-using-cloudtrail.html)
+
+#### FS-24 — Knowledge Base Metadata Filtering
+
+| Field | Detail |
+|-------|--------|
+| Severity | Informational |
+| Guide ref | [Guide §1.2.15] — "Implement access controls at the document or record level within knowledge bases where different users or applications should only have access to specific subsets of data. Use Amazon Bedrock Knowledge Bases metadata filtering to enforce data segmentation." |
+| Description | Advisory: verifies KB metadata fields support tenant-level filtering for multi-tenancy. |
+| Detection | Calls `ListKnowledgeBases` and `GetKnowledgeBase` (via the `bedrock-agent` boto3 client; IAM actions are `bedrock:ListKnowledgeBases` and `bedrock:GetKnowledgeBase`). Inspects the storage configuration for metadata field definitions. Flags KBs with no metadata fields defined (no tenant isolation possible). |
+| Remediation | 1. Define metadata fields on your KB data sources (e.g., `tenant_id`, `department`, `classification`). 2. Populate metadata during document ingestion. 3. Use the `filter` parameter in Retrieve/RetrieveAndGenerate API calls to enforce tenant-scoped queries. 4. Test that cross-tenant data leakage is prevented. |
+| Reference | [Bedrock KB Metadata Filtering](https://docs.aws.amazon.com/bedrock/latest/userguide/kb-test-config.html) |
+
+#### FS-25 — OpenSearch Serverless Encryption
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.15] — "Enable encryption at rest and in transit for vector and embedding databases." |
+| Description | Checks OpenSearch Serverless collections used by KBs have CMK encryption policies. |
+| Detection | Calls `opensearchserverless:ListCollections` (IAM action `aoss:ListCollections`) and for each calls `opensearchserverless:ListSecurityPolicies(type=encryption)` (IAM action `aoss:ListSecurityPolicies`). Inspects each encryption policy's document for `AWSOwnedKey=true` or missing `KmsARN`. Note: the encryption **policy JSON document** uses PascalCase field names — `AWSOwnedKey` and `KmsARN` — while the direct API `EncryptionConfig` struct uses camelCase (`aWSOwnedKey`, `kmsKeyArn`); detection should inspect the policy document form returned by `GetSecurityPolicy`/`ListSecurityPolicies`. Flags collections using AWS-owned keys instead of customer-managed KMS keys. Note: the boto3 client name is `opensearchserverless`, but IAM actions use the service prefix `aoss:` (not `opensearchserverless:`). Note also: encryption in transit is automatic (TLS 1.2, AES-256) for all OpenSearch Serverless traffic and is not configurable — this check focuses on encryption at rest. |
+| Remediation | 1. Create an encryption security policy specifying a customer-managed KMS key: set `AWSOwnedKey=false` and provide `KmsARN` with the ARN of your KMS key. 2. Apply the policy to the collection by matching the collection name or prefix pattern in the policy `Rules`. 3. Ensure the KMS key policy grants the OpenSearch Serverless service principal `kms:Decrypt` and `kms:GenerateDataKey`. Note: if you provide a KMS key directly in the `CreateCollection` request, it takes precedence over any matching security policies. |
+| Reference | [OpenSearch Serverless Encryption](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/serverless-encryption.html) |
+
+#### FS-26 — Knowledge Base VPC Access
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.15, extension] — network isolation is not verbatim in the guide but directly implements "Apply the principle of least privilege to control access to your vector and embedding database" at the network layer. |
+| Description | Verifies OpenSearch Serverless collections have VPC-only network policies (no public access). |
+| Detection | Calls `opensearchserverless:ListSecurityPolicies(type=network)` (IAM action `aoss:ListSecurityPolicies` — the service prefix for OpenSearch Serverless is `aoss`, not `opensearchserverless`) and inspects each policy rule for `AllowFromPublic=true`. Flags collections accessible from the public internet. Note: a policy with `AllowFromPublic=false` may still grant private access to Bedrock via `SourceServices: ["bedrock.amazonaws.com"]` or to specific VPC endpoints via `SourceVPCEs` — these are the recommended private-access patterns and are not flagged. |
+| Remediation | 1. Create a network security policy that restricts access to specific VPC endpoints only via `SourceVPCEs`, or grants private AWS service access (e.g., Bedrock) via `SourceServices: ["bedrock.amazonaws.com"]`. Per AWS docs, private access to AWS services applies only to the collection's OpenSearch endpoint, not to the OpenSearch Dashboards endpoint. 2. Create an OpenSearch Serverless VPC endpoint in your VPC if VPC-private access is required. 3. Remove any policy rules with `AllowFromPublic=true`. 4. Test connectivity from within the VPC. |
+| Reference | [OpenSearch Serverless Network Access](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/serverless-network.html) |
+
+---
+
+## Part 2 — Guardrails & Content Safety (FS-27 to FS-46)
+
+> **Guide risk categories:** Non-Compliant Output (FS-27..30, §1.2.1), Misinformation (FS-31..34, §1.2.3; FS-34 sources from §1.2.12 — see note), Abusive or Harmful Output (FS-35..38, §1.2.4), Biased Output (FS-39..42, §1.2.5), Sensitive Information Disclosure (FS-43..46, §1.2.6).
+
+### Non-Compliant Output (FS-27 to FS-30)
+
+> **Guide source:** §1.2.1 Non-compliant output. Guide-listed mitigations:
+> (a) prompt engineering to guide the model and prevent unwanted responses;
+> (b) content filters and denied topics in Bedrock Guardrails;
+> (c) RAG with Bedrock Knowledge Bases;
+> (d) Automated Reasoning checks in Bedrock Guardrails;
+> (e) human-in-the-loop validation for internal AI systems;
+> (f) audit logs of AI-generated outputs and guardrails applied for regulatory reporting.
+
+#### FS-27 — Automated Reasoning Checks
+
+| Field | Detail |
+|-------|--------|
+| Severity | High (contextual grounding) / Medium (Automated Reasoning) |
+| Guide ref | [Guide §1.2.1, §1.2.7] — "Automated Reasoning checks in Amazon Bedrock Guardrails uses automated reasoning to verify that natural language content complies with your defined policies. This mathematical verification helps ensure that your content strictly follows your guardrails." |
+| Description | Verifies Bedrock Guardrails have Automated Reasoning checks or contextual grounding enabled. |
+| Detection | Calls `bedrock:ListGuardrails` and `bedrock:GetGuardrail` for each. Inspects the response fields `contextualGroundingPolicy` and `automatedReasoningPolicy`. Flags guardrails with neither enabled. |
+| Remediation | 1. Enable contextual grounding filters (type `GROUNDING`) with a threshold ≥ 0.7 — these filters CAN block content that fails grounding checks. Note: valid threshold values are 0 to 0.99; a threshold of 1.0 is invalid and will block all content. **Important use-case limitation:** Contextual grounding checks support summarization, paraphrasing, and question answering use cases only — **Conversational QA / Chatbot use cases are not supported**. If your FinServ application is a conversational chatbot, contextual grounding cannot be used for hallucination detection; use Automated Reasoning checks or human-in-the-loop validation instead. 2. If available in your region, additionally enable Automated Reasoning checks by creating an Automated Reasoning policy and attaching it to the guardrail. **Cross-Region inference is REQUIRED for AR:** Guardrails that use Automated Reasoning checks require a cross-Region inference profile — set `crossRegionConfig.guardrailProfileIdentifier` to a profile matching your Region (for example, `us.guardrail.v1:0` for US Regions or `eu.guardrail.v1:0` for EU Regions). Omitting this parameter returns `ValidationException`. As of April 2026, AR is generally available in US East (N. Virginia), US East (Ohio), US West (Oregon), EU (Frankfurt), EU (Ireland), and EU (Paris) — verify current regional availability on the [AR documentation page](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-automated-reasoning-checks.html) before audit reliance, as AWS regularly expands coverage. Attach the **versioned** policy ARN (for example, `...:1`) — the unversioned ARN returns an error. You can attach a maximum of 2 AR policies per guardrail. Important: Automated Reasoning operates in **detect mode only** — it returns findings and feedback but does NOT block content. AR finding types (per the AWS user guide) are: `VALID` (response is consistent with policy), `INVALID` (response contradicts policy rules), `SATISFIABLE` (response could be true or false depending on unstated conditions), `IMPOSSIBLE` (premises are contradictory), `TRANSLATION_AMBIGUOUS` (natural language could not be reliably translated to formal logic), `TOO_COMPLEX` (policy complexity exceeded processing limits), and `NO_TRANSLATIONS` (some or all input was not translated into logic due to irrelevance or lack of matching policy variables). Note: in the `AutomatedReasoningCheckFinding` runtime response, these appear as a **union** with lowercase camelCase keys (`valid`, `invalid`, `satisfiable`, `impossible`, `translationAmbiguous`, `tooComplex`, `noTranslations`) — exactly one key is present per finding. Per AWS docs, AR also **does not protect against prompt injection attacks**, **cannot detect off-topic responses**, **does not support streaming APIs**, and **supports English (US) only** — use content filters, topic policies, and other guardrail components alongside AR. **Critical limitation for cross-account enforcement:** AR policies are NOT supported with Bedrock Guardrails cross-account safeguards (org-level or account-level enforcement) — including an AR policy in a guardrail used for enforcement will cause runtime failures. If you rely on AR, configure it at the application or account level separately. Your application must inspect the AR findings via the `ApplyGuardrail` (or `Converse` / `InvokeModel` / `InvokeAgent` / `RetrieveAndGenerate`) API response and decide whether to serve the response, rewrite it using AR feedback, ask the user for clarification, or fall back to a default behavior. 3. For `INVALID` responses, implement an iterative rewriting loop that feeds AR feedback (contradicting rules) back to the LLM to self-correct. 4. Build an audit trail of all AR validation iterations — log `supportingRules` and `claimsTrueScenario` for `VALID` findings as mathematically verifiable compliance evidence. |
+| Reference | [Automated Reasoning in Bedrock Guardrails](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-automated-reasoning-checks.html), [AR Checks Concepts (Validation Results Reference)](https://docs.aws.amazon.com/bedrock/latest/userguide/automated-reasoning-checks-concepts.html), [Integrate AR Checks in Your Application](https://docs.aws.amazon.com/bedrock/latest/userguide/integrate-automated-reasoning-checks.html), [Deploy Automated Reasoning Policy](https://docs.aws.amazon.com/bedrock/latest/userguide/deploy-automated-reasoning-policy.html) |
+
+#### FS-28 — Financial Denied Topics
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.1] — "Configure content filters and guardrails to restrict model responses to approved topics" with reference "Amazon Bedrock User Guide – Guardrails – Denied topics". |
+| Description | Checks guardrails have denied topics for regulated financial advice. |
+| Detection | Calls `bedrock:GetGuardrail` and inspects `topicPolicy.topics` for entries with `type=DENY`. Flags guardrails with no denied topics or with no topics related to financial advice, investment recommendations, or tax guidance. |
+| Remediation | 1. Add denied topics to the guardrail following the AWS best-practice golden rules: (a) **Be crisp and precise** — e.g., "Investment advice is inquiries, guidance, or recommendations about the management or allocation of funds or assets with the goal of generating returns or achieving specific financial objectives" rather than vague "Investment advice". (b) **Define, don't instruct** — write "All content associated with specific investment recommendations" not "Block all investment advice". (c) **Stay positive** — never define topics negatively (e.g., avoid "All content except general financial education"). (d) **Focus on themes, not words** — denied topics capture subjects contextually; use word filters for specific names or entities. (e) **Provide sample phrases** — add up to 5 representative inputs per topic (each up to 100 characters). 2. **Quantity and character limits:** A guardrail can contain a maximum of **30 denied topics**. In Classic tier, topic definitions are limited to 200 characters; in Standard tier, up to 1,000 characters — use Standard tier for complex financial topic definitions. 3. Recommended denied topics for FinServ: "specific investment recommendations", "tax advice", "specific financial product recommendations", "guaranteed returns or performance claims". 4. For multi-account enforcement, use Bedrock cross-account safeguards to apply denied topics from a management-account guardrail across all member accounts automatically. When configuring account-level or org-level enforcement, set **both** `selectiveContentGuarding.messages` AND `selectiveContentGuarding.system` to `COMPREHENSIVE` to ensure guardrails evaluate all user messages AND system prompts regardless of input tags — use `SELECTIVE` only when you trust callers to correctly tag content. Setting only `messages` to COMPREHENSIVE leaves system prompts potentially unguarded. 5. Enforce guardrails via IAM policy conditions (`bedrock:GuardrailIdentifier`) to prevent any Bedrock inference call without a guardrail attached. 6. Test with prompts that attempt to elicit regulated financial advice. |
+| Reference | [Bedrock Guardrails Denied Topics](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-denied-topics.html), [Safeguard Tiers for Guardrails](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-tiers.html), [Cross-Account Safeguards with Enforcements](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-enforcements.html), [Guardrails Best Practices](https://aws.amazon.com/blogs/machine-learning/build-safe-generative-ai-applications-like-a-pro-best-practices-with-amazon-bedrock-guardrails/) |
+
+#### FS-29 — Compliance Disclaimer
+
+| Field | Detail |
+|-------|--------|
+| Severity | Informational |
+| Guide ref | [Guide §1.2.1, extension] — disclaimers are not verbatim in §1.2.1 but the guide references "Implement response disclaimers in customer-facing applications" under §1.2.7 Hallucination, which is conceptually the same control applied here for non-compliant financial-advice output. |
+| Description | Advisory: verifies application adds required regulatory disclaimers to AI-generated outputs. |
+| Detection | Advisory check — cannot be fully automated. Inspects application Lambda function environment variables or configuration for disclaimer-related settings (e.g., `DISCLAIMER_ENABLED`, `COMPLIANCE_FOOTER`). |
+| Remediation | 1. Add a standard regulatory disclaimer to all customer-facing AI-generated responses (e.g., "This information is generated by AI and does not constitute financial advice. Please consult a qualified financial advisor."). 2. Make the disclaimer text configurable via environment variable or parameter store. 3. Ensure disclaimers are not removable by prompt manipulation. |
+| Reference | [AWS Well-Architected GenAI Lens — Guardrails](https://docs.aws.amazon.com/wellarchitected/latest/generative-ai-lens/gensec02-bp01.html) |
+
+#### FS-30 — Compliance Evaluation Datasets
+
+| Field | Detail |
+|-------|--------|
+| Severity | Informational |
+| Guide ref | [Guide §1.2.1, extension] — the Guide §1.2.12 practical guidance mentions "Amazon Bedrock Evaluations can help to evaluate models against specific types of attacks"; this check extends that concept to compliance-specific evaluation for FS-regulated outputs. |
+| Description | Checks Bedrock evaluation jobs use compliance-specific test datasets. |
+| Detection | Calls `bedrock:ListEvaluationJobs` to enumerate existing jobs, then calls `bedrock:GetEvaluationJob` for each to inspect the full `evaluationConfig` including dataset configuration. Flags if no evaluation jobs exist or if none reference compliance/regulatory test data. Note: `ListEvaluationJobs` returns only job summaries — dataset configuration details require `GetEvaluationJob`. |
+| Remediation | 1. Create a compliance-specific evaluation dataset containing: prompts requesting regulated financial advice, prompts testing disclaimer presence, prompts testing denied-topic enforcement. 2. Run Bedrock evaluation jobs with this dataset before each production deployment. 3. Set pass/fail thresholds and gate deployments on results. |
+| Reference | [Bedrock Model Evaluation](https://docs.aws.amazon.com/bedrock/latest/userguide/evaluation.html) |
+
+### Misinformation (FS-31 to FS-33)
+
+> **Guide source:** §1.2.3 Misinformation through inadvertent or malicious action. Guide-listed mitigations:
+> (a) prompt engineering;
+> (b) verify knowledge base data sources are up-to-date, accurate, reliable, and complete;
+> (c) human-in-the-loop validation for internal AI systems;
+> (d) source attribution in RAG responses for end users to verify provenance;
+> (e) integrity monitoring on knowledge base data sources — e.g., S3 event notifications to
+> track document changes.
+
+#### FS-31 — Knowledge Base Data Source Sync
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.3, §1.2.10] — "Verify that your knowledge base data sources are up-to-date, accurate, reliable, and complete"; "Sync your data with your Amazon Bedrock knowledge base". |
+| Description | Verifies KB data sources have been synced within 7 days. |
+| Detection | Calls `ListDataSources` then `ListIngestionJobs` for each data source (via the `bedrock-agent` boto3 client; IAM actions are `bedrock:ListDataSources` and `bedrock:ListIngestionJobs`). Checks the most recent successful ingestion job's `updatedAt` timestamp. Flags data sources not synced within 7 days. |
+| Remediation | 1. Create an EventBridge scheduled rule to trigger KB data source sync at least weekly. 2. Use `StartIngestionJob` (IAM action `bedrock:StartIngestionJob`) as the rule target. 3. Add CloudWatch alarms for failed ingestion jobs. 4. For rapidly changing data, increase sync frequency. |
+| Reference | [Bedrock KB Data Source Sync](https://docs.aws.amazon.com/bedrock/latest/userguide/kb-data-source-sync-ingest.html) |
+
+#### FS-32 — Source Attribution
+
+| Field | Detail |
+|-------|--------|
+| Severity | Informational |
+| Guide ref | [Guide §1.2.3, §1.2.10] — "Use source attribution in RAG-based response for end users to verify provenance of information" (§1.2.3); "Use source attribution in RAG-based response for end users to verify currency of information" (§1.2.10). |
+| Description | Advisory: verifies application implements source citations in RAG responses. |
+| Detection | Advisory check — inspects application code or configuration for use of the `citations` field in `RetrieveAndGenerate` API responses. Checks Lambda environment variables for attribution-related settings. |
+| Remediation | 1. Use the `RetrieveAndGenerate` API (IAM action `bedrock:RetrieveAndGenerate`) which returns `citations` with source document references. Each citation contains `retrievedReferences` — an array where each reference has a `content` object (the cited text), a `location` object (data source type and URI — for S3 sources, `location.type=S3` and `location.s3Location.uri` contains the S3 URI), and optional `metadata` (a string-to-JSON map with any custom metadata attributes stored on the chunk, which can hold document title and other fields). Note: there is no fixed `title` field in the API — if you need to display document titles to end users, store them as a metadata attribute during KB ingestion and retrieve them via `retrievedReferences[].metadata`. 2. Display source citations to end users alongside AI-generated responses. 3. Include the data source location (URI or other location identifier depending on source type: S3, Web, Confluence, SharePoint, Salesforce, Kendra, SQL, or Custom) and the cited text excerpt (from `content`). 4. If document titles are required, ensure they are populated in KB metadata and propagated to your UI. 5. Allow users to click through to the original source document where possible. |
+| Reference | [Bedrock RetrieveAndGenerate API](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent-runtime_RetrieveAndGenerate.html) |
+
+#### FS-33 — Knowledge Base Integrity Monitoring
+
+| Field | Detail |
+|-------|--------|
+| Severity | High (deleted bucket) / Medium (versioning) |
+| Guide ref | [Guide §1.2.3] — "Use integrity monitoring on knowledge base data sources to detect unauthorized modifications. Track changes to documents used in knowledge bases." References "For example on S3 data sources use Amazon S3 event notification to track changes to documents." |
+| Description | Checks KB data source S3 buckets have versioning enabled and S3 event notifications (EventBridge or SNS) configured to detect unauthorized document modifications in real time. |
+| Detection | Identifies KB data-source S3 buckets via `GetDataSource` (via the `bedrock-agent` boto3 client; IAM action `bedrock:GetDataSource`). Calls `s3:GetBucketVersioning` to verify `Status=Enabled`. Calls `s3:GetBucketNotificationConfiguration` and checks for `EventBridgeConfiguration`, `TopicConfigurations`, `QueueConfigurations`, or `LambdaFunctionConfigurations`. Flags buckets missing either control. |
+| Remediation | 1. Enable versioning: `aws s3api put-bucket-versioning --bucket --versioning-configuration Status=Enabled`. 2. Enable EventBridge notifications on the bucket: `aws s3api put-bucket-notification-configuration --bucket --notification-configuration '{"EventBridgeConfiguration":{}}'`. Once enabled, S3 automatically sends **all** bucket events to EventBridge — you do not select specific event types at the bucket level. 3. Create an EventBridge rule that matches S3 events for this bucket — use the `detail-type` field values `Object Created` and `Object Deleted` (these are the EventBridge event type names; note: `s3:ObjectCreated:*` and `s3:ObjectRemoved:*` are the legacy SNS/SQS/Lambda notification event type names and are NOT used in EventBridge rules). Route matched events to an SNS topic or Lambda function for alerting. 4. Integrate alerts into your security incident response workflow. |
+| Reference | [S3 EventBridge Integration](https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventBridge.html) |
+
+> **Note:** FS-34 (Third-Party Risk Management for FM Providers) is kept adjacent to Misinformation
+> in this file for continuity with the prior draft numbering, but its guide source is §1.2.12
+> Supply Chain Vulnerabilities. Treat FS-34 as a Supply Chain check for compliance-framework
+> mapping purposes.
+
+#### FS-34 — Third-Party Risk Management (TPRM) for Foundation Model Providers
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.12] — *"Update existing third-party risk management processes to continuously monitor model providers and third-party dependencies, including tracking vendor security advisories, model deprecation notices, and change to terms and conditions."* (Note: moved from the Misinformation section in the prior draft; the guide places TPRM under Supply Chain.) |
+| Description | Verifies a documented third-party risk management (TPRM) process exists to monitor FM providers for security advisories, model deprecation notices, and T&C changes; also flags legacy FMs currently in use. |
+| Detection | Calls `bedrock:ListFoundationModels`, then `bedrock:GetFoundationModel` for each in-use model; inspects `modelLifecycle.status` and flags models with status `LEGACY`. Note: the `FoundationModelLifecycle.status` API field has only **two** valid values — `ACTIVE` and `LEGACY`. There is no `EOL` status value in the API; models that have passed their EOL date are removed from the service entirely and API calls referencing them will fail. The user-facing lifecycle page describes three conceptual states (Active, Legacy, EOL) but the API only exposes two. Advisory component checks for evidence of a TPRM process — e.g., an AWS Config rule or a tag on Bedrock resources indicating periodic review (`tprm-last-reviewed=`). |
+| Remediation | 1. Establish a documented TPRM process: at least quarterly review of each in-use FM provider's security advisories, model lifecycle announcements, and T&C changes. 2. Assign an owner for the TPRM process and record review evidence in your MRM system. 3. Subscribe to AWS Bedrock model lifecycle notifications. 4. Migrate workloads from `LEGACY` models to active versions before their published EOL date — note that for models with EOL dates after February 1, 2026, there is a "public extended access" period where Legacy models remain usable but at higher pricing set by the model provider. 5. For third-party models procured via AWS Marketplace or consumed directly, evaluate the provider's own testing procedures — AWS AI Service Cards provide this transparency for Amazon-trained models. |
+| Reference | [Bedrock Model Lifecycle](https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html), [Access Amazon Bedrock foundation models](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) |
+
+### Abusive or Harmful Output (FS-35 to FS-38)
+
+> **Guide source:** §1.2.4 Model output is abusive or harmful. Guide-listed mitigations:
+> (a) AWS AI Service Cards to understand how Amazon addresses toxicity per model;
+> (b) Amazon Bedrock Guardrails to detect and filter harmful content;
+> (c) FMEval to evaluate for inappropriate content (sexual, profanity, hate, aggression,
+> insults, flirtation, identity attacks, threats);
+> (d) user reporting mechanism so end users can flag abusive outputs, reviewed within a
+> defined process;
+> (e) Practical guidance: create allowlists for approved business terminology to reduce
+> false positives on brand, product, industry, and technical vocabulary.
+
+#### FS-35 — FMEval Harmful Content
+
+| Field | Detail |
+|-------|--------|
+| Severity | Informational |
+| Guide ref | [Guide §1.2.4] — "Foundation Model Evaluations (FMEval) evaluates your model to detect inappropriate content, including sexual references, profanity, hate speech, aggression, insults, flirtation, identity-based attacks, and threats." |
+| Description | Checks Bedrock evaluation jobs test for harmful/toxic content. |
+| Detection | Calls `bedrock:ListEvaluationJobs` to enumerate existing jobs, then calls `bedrock:GetEvaluationJob` for each to inspect the full `evaluationConfig`. The correct metric name depends on the evaluation job type: (a) For **automated model evaluation jobs** (pre-computed metrics), the toxicity metric is `"Builtin.Toxicity"` — the only valid harmful-content metric for this job type. (b) For **judge-based model evaluation jobs** (LLM-as-judge), the harmful content metrics are `"Builtin.Harmfulness"` and `"Builtin.Stereotyping"`. (c) For **knowledge base (RAG) evaluation jobs**, `"Builtin.Harmfulness"` and `"Builtin.Stereotyping"` are also valid. Flags if no evaluation jobs exist or none include a harmful-content metric (`Builtin.Toxicity` for automated, `Builtin.Harmfulness` for judge-based/RAG). Note: `ListEvaluationJobs` returns only job summaries — dataset configuration details require `GetEvaluationJob`. |
+| Remediation | 1. For **automated model evaluation** (fastest, no judge model required): create a Bedrock evaluation job with `"Builtin.Toxicity"` in the `metricNames` array. Valid task types are `Summarization`, `Classification`, `QuestionAndAnswer`, `Generation`, and `Custom`. 2. For **judge-based model evaluation** (more nuanced, requires a judge model): create a Bedrock evaluation job with `"Builtin.Harmfulness"` and/or `"Builtin.Stereotyping"` in the `metricNames` array — these metrics are only valid for judge-based and RAG evaluation jobs, not automated model evaluation jobs. 3. Include test prompts designed to elicit harmful content. 4. Set pass/fail thresholds based on the scores returned. 5. Run evaluations before production deployment and after model updates. 6. For more granular toxicity scoring (the 7-category UnitaryAI Detoxify-unbiased scores: `toxicity`, `severe_toxicity`, `obscene`, `threat`, `insult`, `sexual_explicit`, `identity_attack` — or the Toxigen-roberta binary classifier), use SageMaker FMEval via SageMaker Studio or the `fmeval` Python library as a complementary evaluation path. |
+| Reference | [Bedrock Model Evaluation Metrics](https://docs.aws.amazon.com/bedrock/latest/userguide/model-evaluation-metrics.html), [SageMaker FMEval Toxicity](https://docs.aws.amazon.com/sagemaker/latest/dg/clarify-toxicity-evaluation.html) |
+
+#### FS-36 — Guardrail Content Filters
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.4] — "Use Amazon Bedrock's guardrails to detect and filter harmful content." |
+| Description | Verifies guardrails have content filters for hate, violence, sexual, and other harmful content. |
+| Detection | Calls `bedrock:GetGuardrail` and inspects `contentPolicy.filters`. Flags guardrails missing filters for HATE, VIOLENCE, SEXUAL, INSULTS, or MISCONDUCT categories. Also checks that `inputStrength` and `outputStrength` are at least `MEDIUM`. |
+| Remediation | 1. Update the guardrail to include content filters for all harmful categories: HATE, VIOLENCE, SEXUAL, INSULTS, MISCONDUCT. 2. Select the **Standard tier** (not Classic) for content filters — it offers better accuracy, broader language support (extensive multilingual support vs. English/French/Spanish only in Classic), prompt leakage detection, and extends protection to harmful content within code elements. Standard tier requires cross-Region inference to be enabled on the guardrail (configurable at creation or by modifying an existing guardrail). 3. Start with **HIGH** filter strength for customer-facing applications; evaluate false-positive rates on representative sample traffic and lower to MEDIUM only if necessary. 4. Apply filters to both INPUT and OUTPUT. 5. Before enabling blocking in production, use **detect mode** (`action=NONE`) to test guardrail behavior on live traffic — review trace output to validate decisions, then switch to `action=BLOCK` once confident. 6. Enforce guardrails organization-wide via IAM policy-based enforcement: add an IAM condition key (`bedrock:GuardrailIdentifier`) to deny any `InvokeModel`/`Converse` call that does not include a guardrail. For account-level or org-level enforcement configurations, set **both** `selectiveContentGuarding.messages` AND `selectiveContentGuarding.system` to `COMPREHENSIVE` to ensure guardrails evaluate all user messages AND system prompts regardless of input tags (use `SELECTIVE` only when you trust callers to correctly tag content). Setting only `messages` to COMPREHENSIVE leaves system prompts potentially unguarded. |
+| Reference | [Bedrock Guardrails Content Filters](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-content-filters.html), [Safeguard Tiers for Guardrails](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-tiers.html), [Cross-Account Safeguards with Enforcements](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-enforcements.html), [Guardrails Best Practices](https://aws.amazon.com/blogs/machine-learning/build-safe-generative-ai-applications-like-a-pro-best-practices-with-amazon-bedrock-guardrails/), [IAM Guardrail Enforcement](https://aws.amazon.com/blogs/machine-learning/amazon-bedrock-guardrails-announces-iam-policy-based-enforcement-to-deliver-safe-ai-interactions/) |
+
+#### FS-37 — User Feedback Mechanism
+
+| Field | Detail |
+|-------|--------|
+| Severity | Informational |
+| Guide ref | [Guide §1.2.4] — "Implement a user reporting mechanism that allows end users to flag abusive or harmful outputs. Reported incidents [are] reviewed within a defined process to refine content filters." |
+| Description | Advisory: verifies application has a user reporting mechanism for harmful outputs. |
+| Detection | Advisory check — inspects application configuration for feedback-related settings (e.g., `FEEDBACK_ENABLED`, `REPORT_ABUSE_ENDPOINT`). Checks for Lambda functions with "feedback" or "report" in the name. |
+| Remediation | 1. Implement a "Report this response" button in the application UI. 2. Route reported responses to an SQS queue or DynamoDB table for review. 3. Define an SLA for reviewing reported content (e.g., 24 hours). 4. Use reported incidents to refine guardrail content filters and word lists. 5. Log all reports with Bedrock invocation logging correlation IDs. |
+| Reference | [Bedrock Model Invocation Logging](https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html) |
+
+#### FS-38 — Guardrail Word Filters and Business Term Allowlists
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.4 — Practical guidance] — "Create allowlists for business terms that include approved terminology for: brand names, product names, industry terms, and technical vocabulary. Also test filter settings to verify that your content filters allow necessary business communications and generate accurate alerts. Monitor and adjust regularly your filtering system to reduce false positives." |
+| Description | Checks guardrails have word/phrase block filters configured and that approved business terminology allowlists are defined to prevent false positives on legitimate financial services vocabulary. |
+| Detection | Calls `bedrock:GetGuardrail` and inspects `wordPolicy`. Flags guardrails with no custom `words` array (blocked phrases). Also checks `managedWordLists` for the AWS-managed `PROFANITY` list. Note: a guardrail with only the profanity filter and no custom FinServ-specific blocked terms should still be flagged as incomplete for financial services use cases. |
+| Remediation | 1. Add blocked words/phrases to the guardrail word filter (profanity, slurs, competitor names if applicable). Each custom word/phrase entry has a maximum length of **100 characters** per the API (`GuardrailWordConfig.text`); the console UI additionally limits entries to **up to three words** per phrase. You can add up to **10,000 items** to the custom word filter. 2. Enable the AWS-managed profanity filter (`managedWordListsConfig` with `type=PROFANITY`) as a baseline. 3. Create an allowlist of approved business terminology: brand names, product names, industry terms, technical vocabulary — document this separately as the guardrail word filter only blocks, it does not allowlist. Test filter settings to verify legitimate business communications are not blocked. 4. Monitor and adjust regularly to reduce false positives. |
+| Reference | [Bedrock Guardrails Word Filters](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-word-filters.html) |
+
+### Biased Output (FS-39 to FS-42)
+
+> **Guide source:** §1.2.5 Model output is biased. Guide-listed mitigations:
+> (a) AWS AI Service Cards to understand how providers address fairness/bias per model;
+> (b) prompt engineering;
+> (c) Amazon Bedrock Guardrails;
+> (d) Bedrock Evaluations to measure bias;
+> (e) Amazon SageMaker Clarify for bias detection, transparency, and prediction explanation
+> on fine-tuned and self-trained models;
+> (f) develop and maintain a bias testing dataset with representative cases across
+> demographic groups, geographic regions, and sensitive attributes — run periodically and
+> after each model update.
+
+#### FS-39 — SageMaker Clarify Bias
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.5] — "Use Amazon SageMaker Clarify to detect bias, increase transparency, and explain predictions for your fine-tuned and self-trained AI models." |
+| Description | Verifies Clarify model bias monitoring is configured for financial decision models. |
+| Detection | Calls `sagemaker:ListMonitoringSchedules` with the `MonitoringTypeEquals=ModelBias` filter parameter (the `MonitoringType` field on the `MonitoringScheduleSummary` response has one of four values: `DataQuality`, `ModelQuality`, `ModelBias`, `ModelExplainability`). Flags if no bias monitoring schedules exist. Cross-references with endpoints tagged `use-case=financial-decision` or similar. Clarify bias monitoring publishes metrics to the `aws/sagemaker/Endpoints/bias-metrics` namespace for real-time endpoints (and `aws/sagemaker/ModelMonitoring/bias-metrics` for batch transform jobs) with `Endpoint`, `MonitoringSchedule`, `BiasStage`, `Label`, `LabelValue`, `Facet`, and `FacetValue` dimensions. |
+| Remediation | 1. Create a SageMaker Clarify bias monitoring schedule for each financial decision model endpoint. 2. Specify facets (protected attributes: age, gender, race, geography) and bias metrics (DPL, DI, DPPL). 3. Provide a baseline bias report from training data. 4. Configure CloudWatch alarms on bias metric violations on the `aws/sagemaker/Endpoints/bias-metrics` namespace. Note: `publish_cloudwatch_metrics` is enabled by default — do NOT set it to `Disabled` in the model bias job definition's `Environment` map, as that would stop metrics from being published to CloudWatch. |
+| Reference | [SageMaker Clarify Bias Detection](https://docs.aws.amazon.com/sagemaker/latest/dg/clarify-detect-post-training-bias.html) |
+
+#### FS-40 — Bedrock Bias Evaluation Datasets and Cadence
+
+| Field | Detail |
+|-------|--------|
+| Severity | Informational |
+| Guide ref | [Guide §1.2.5] — "Develop and maintain a bias testing dataset that includes representative test cases across demographic groups, geographic regions, and other sensitive attributes relevant to your use case. Run these test cases periodically and after model updates." |
+| Description | Checks evaluation jobs include demographic fairness test cases across protected groups and verifies evaluations are run on a defined periodic schedule and after each model update. |
+| Detection | Calls `bedrock:ListEvaluationJobs` to enumerate existing jobs, then calls `bedrock:GetEvaluationJob` for each to inspect the full `evaluationConfig` including dataset configuration for demographic diversity test cases. Checks the `creationTime` of the most recent evaluation job and flags if it is older than 90 days or if no evaluation was run after the most recent model deployment. Note: `ListEvaluationJobs` returns only job summaries — dataset configuration details require `GetEvaluationJob`. |
+| Remediation | 1. Create a bias evaluation dataset with representative test cases across demographic groups, geographic regions, and other sensitive attributes. 2. Schedule evaluation jobs to run at least quarterly via EventBridge. 3. Trigger an evaluation job automatically after each model update in your CI/CD pipeline. 4. Store results for audit and trend analysis. |
+| Reference | [Bedrock Model Evaluation](https://docs.aws.amazon.com/bedrock/latest/userguide/evaluation.html) |
+
+#### FS-41 — SageMaker Clarify Explainability
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.5, extension] — Guide §1.2.5 recommends "Amazon SageMaker Clarify to detect bias, increase transparency, and explain predictions". ECOA/Fair Housing adverse-action-notice use case is an FS-specific extension of Clarify explainability not named verbatim in the guide. |
+| Description | Verifies Clarify explainability monitoring for adverse action notices (commonly cited under ECOA for credit decisions; this is an FS industry-practice extension, not a guide-prescribed control). |
+| Detection | Calls `sagemaker:ListMonitoringSchedules` with the `MonitoringTypeEquals=ModelExplainability` filter parameter. Flags if no explainability monitoring schedules exist for financial decision model endpoints. Clarify explainability monitoring publishes metrics to the `aws/sagemaker/Endpoints/explainability-metrics` namespace for real-time endpoints (and `aws/sagemaker/ModelMonitoring/explainability-metrics` for batch transform jobs) with `Endpoint`, `MonitoringSchedule`, `ExplainabilityMethod` (value: `KernelShap`), `Label`, and `ValueType` (values: `GlobalShapValues` or `ExpectedValue`) dimensions. |
+| Remediation | 1. Create a SageMaker Clarify explainability monitoring schedule using SHAP analysis. 2. Configure feature attribution baselines. 3. Use explainability outputs to generate adverse action notices (top contributing factors for negative decisions) where your firm's use case and regulatory interpretation require them. 4. Retain explainability reports for regulatory audit. |
+| Reference | [SageMaker Clarify Explainability](https://docs.aws.amazon.com/sagemaker/latest/dg/clarify-model-explainability.html) |
+
+#### FS-42 — AI Service Cards
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.4, §1.2.5, §1.2.14] — "Amazon provides AI Service Cards for models that are pre-trained for AWS services like Amazon Bedrock and Amazon Q. These cards help you understand how Amazon addresses toxicity in each model." Referenced in three separate guide risk sections. |
+| Description | Checks SageMaker Model Cards document intended use and bias evaluations. |
+| Detection | Calls `sagemaker:ListModelCards`. For each card, calls `sagemaker:DescribeModelCard` and inspects the content JSON for `intended_uses`, `business_details`, and `evaluation_details` sections. Flags cards missing these sections. |
+| Remediation | 1. Create a SageMaker Model Card for each production model. 2. Document: intended use cases, out-of-scope uses, training data description, bias evaluation results, performance metrics. 3. Review and update cards after each model retrain. 4. For Bedrock foundation models, reference the AWS AI Service Cards published by Amazon. |
+| Reference | [SageMaker Model Cards](https://docs.aws.amazon.com/sagemaker/latest/dg/model-cards.html), [AWS AI Service Cards](https://aws.amazon.com/ai/responsible-ai/resources/) |
+
+### Sensitive Information Disclosure (FS-43 to FS-46)
+
+> **Guide source:** §1.2.6 Sensitive information disclosure. Guide-listed mitigations:
+> (a) Bedrock Guardrails sensitive information filters for PII, PHI;
+> (b) data classification scanning and access controls on AI data sources;
+> (c) strict IAM access controls for Bedrock API;
+> (d) mask sensitive information in CloudWatch Logs and custom application logging;
+> (e) protect training and fine-tuning data via data protection best practices;
+> (f) monitor PII in training/fine-tuning/RAG data with Amazon Macie;
+> (g) remove, mask, or tokenize PII before use in training, fine-tuning, or RAG;
+> (h) Practical guidance: least privilege for agent identities; user-authorized communications
+> to tool services; propagate end-user identities so tool services can validate them without
+> revealing them to unauthorized third parties.
+
+#### FS-43 — CloudWatch Log PII Masking
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.6] — "If you implement model invocation logging for the LLM or custom logging logic in your application, make sure to mask sensitive information in your log data." References "Amazon CloudWatch – Help protect sensitive log data with masking". |
+| Description | Checks CloudWatch Logs data protection policies mask PII in Bedrock invocation logs. |
+| Detection | Identifies CloudWatch log groups used by Bedrock invocation logging (from `bedrock:GetModelInvocationLoggingConfiguration`). Calls `logs:GetDataProtectionPolicy` for each log group. Flags log groups with no data protection policy or policies missing PII identifiers. Note: model invocation logging only captures calls made through the `bedrock-runtime` endpoint (`Converse`, `ConverseStream`, `InvokeModel`, `InvokeModelWithResponseStream`); calls through other endpoints such as the Responses API (`bedrock-mantle` endpoint) are not captured. |
+| Remediation | 1. Create a CloudWatch Logs data protection policy on each Bedrock log group. 2. Include managed data identifiers using their exact ARN-based IDs — country-code suffixes are **required** in the ARN for most identifiers (the data-types table uses the short name such as `Ssn`, but the ARN must include the country code): `Ssn-US` (US Social Security Number; `Ssn-ES` for Spain — there is no bare `Ssn` ARN), `CreditCardNumber` (no suffix), `CreditCardSecurityCode` (no suffix), `EmailAddress` (no suffix), `Address` (no suffix), `PhoneNumber-US`, `BankAccountNumber-US`, `DriversLicense-US`, `PassportNumber-US`, `IndividualTaxIdentificationNumber-US`. 3. Add a `Deidentify` operation statement (no hyphen — this is the exact JSON key required in the policy document, even though AWS prose documentation uses "De-identify") to mask sensitive data, and a separate `Audit` statement to emit findings to CloudWatch. The `Deidentify` operation must contain an empty `"MaskConfig": {}` object. 4. **Retroactive masking scope:** A **log group-level** data protection policy only masks data ingested **after** the policy is applied — historical log events are not retroactively masked. However, an **account-level** data protection policy applies to both existing log groups and log groups created in the future. For maximum coverage, consider creating an account-level policy in addition to log group-level policies. Apply policies at log group creation time or as early as possible. 5. Test by sending a log entry containing sample PII and verifying it is masked in subsequent reads. |
+| Reference | [CloudWatch Logs Data Protection](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/mask-sensitive-log-data.html), [PII Data Identifier ARNs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/protect-sensitive-log-data-types-pii.html), [Financial Data Identifier ARNs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/protect-sensitive-log-data-types-financial.html) |
+
+#### FS-44 — Amazon Macie PII Scanning and Pre-Processing
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.6] — "Monitor personally identifiable information (PII) in your data when you train models, fine-tune them, or use retrieval-augmented generation (RAG)" and "Remove, mask, or tokenize personally identifiable information (PII) or sensitive data before you use it for training, fine-tuning, or retrieval-augmented generation (RAG)." |
+| Description | Verifies Macie is enabled and scanning AI/ML data buckets, and checks that a PII pre-processing step (tokenization, masking, or removal) exists in training and RAG ingestion pipelines before data reaches the model. |
+| Detection | Calls `macie2:GetMacieSession` to verify Macie is enabled. Calls `macie2:GetAutomatedDiscoveryConfiguration` to check whether automated sensitive data discovery is enabled (preferred over manual classification jobs — automated discovery evaluates S3 buckets daily without explicit job creation). Also calls `macie2:ListClassificationJobs` to check for any additional targeted jobs covering S3 buckets tagged for AI/ML use. Additionally inspects SageMaker Processing jobs or Glue jobs for PII-related naming patterns indicating a pre-processing pipeline. |
+| Remediation | 1. Enable Amazon Macie in the account. 2. **Preferred:** Enable Macie **Automated Sensitive Data Discovery** (via `macie2:UpdateAutomatedDiscoveryConfiguration` set to `ENABLED`) — this continuously evaluates ALL S3 buckets in the account or organization daily, selects representative objects, and produces sensitive-data findings without requiring manual job creation. 3. For higher-priority AI/ML buckets where you need full-depth scans, supplement with targeted classification jobs (`macie2:CreateClassificationJob`) scheduled at least weekly. 4. Implement a PII pre-processing step in your data pipeline (SageMaker Processing job, Glue job, or Lambda) that tokenizes, masks, or removes PII before data is used for training or RAG ingestion. 5. Use Amazon Comprehend `DetectPiiEntities` or Macie findings to identify PII locations and feed them into the pre-processing step. 6. Route Macie findings to EventBridge and then to your SIEM or ticketing system for timely investigation. |
+| Reference | [Amazon Macie](https://docs.aws.amazon.com/macie/latest/user/what-is-macie.html), [Macie Automated Sensitive Data Discovery](https://docs.aws.amazon.com/macie/latest/user/discovery-asdd.html), [Amazon Comprehend PII Detection](https://docs.aws.amazon.com/comprehend/latest/dg/pii.html) |
+
+#### FS-45 — Guardrail PII Filters
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.6] — "Use Amazon Bedrock Guardrails to detect and filter structured sensitive information in model inputs and outputs, such as personally identifiable information (PII), protected health information (PHI)." |
+| Description | Checks guardrails have PII entity filters for SSN, credit card, and account numbers. |
+| Detection | Calls `bedrock:GetGuardrail` and inspects `sensitiveInformationPolicy.piiEntities`. Flags guardrails missing filters for critical PII types: `US_SOCIAL_SECURITY_NUMBER`, `CREDIT_DEBIT_CARD_NUMBER`, `CREDIT_DEBIT_CARD_CVV`, `CREDIT_DEBIT_CARD_EXPIRY`, `US_BANK_ACCOUNT_NUMBER`, `US_BANK_ROUTING_NUMBER`, `PIN`, `SWIFT_CODE`, `INTERNATIONAL_BANK_ACCOUNT_NUMBER`, `US_INDIVIDUAL_TAX_IDENTIFICATION_NUMBER`, `EMAIL`, `PHONE`. |
+| Remediation | 1. Update the guardrail to add PII entity filters for all relevant types. 2. Configure separate input and output actions using the `inputAction` and `outputAction` fields: set `outputAction=ANONYMIZE` (replace with placeholder such as `{US_SOCIAL_SECURITY_NUMBER}`) so PII in model responses is masked before reaching the user; set `inputAction=BLOCK` for PII types that should never be submitted (e.g., SSN, credit card numbers). 3. Use `inputEnabled` and `outputEnabled` to selectively enable evaluation per direction — disable evaluation on a direction you don't need to reduce cost and latency. 4. **PHI coverage nuance:** The Bedrock Guardrails sensitive information filter has only limited built-in PHI entities — specifically `CA_HEALTH_NUMBER` (Canada) and `UK_NATIONAL_HEALTH_SERVICE_NUMBER` (UK). For US HIPAA PHI (for example, Medical Record Numbers, Health Plan Beneficiary Numbers, Medicare Beneficiary Identifiers), there is no built-in entity type — use `regexesConfig` (custom regex patterns) on the guardrail to detect these patterns, complemented by downstream CloudWatch Logs data protection policies (see FS-43) which have PHI identifiers under the HIPAA category. 5. **Critical limitation — tool_use outputs:** The sensitive information filter does NOT detect PII when models respond with `tool_use` (function call) output parameters via supported APIs. For FinServ agentic applications where models invoke tools and return structured function-call responses, implement application-layer PII scanning on tool outputs before they are processed or displayed. 6. **Critical limitation — invocation logs:** Guardrail PII masking applies only to content sent to and returned from the inference model. It does NOT apply to model invocation logs — the `input` field in CloudWatch Logs always contains the original, unmasked request regardless of guardrail intervention. Use CloudWatch Logs data protection policies (see FS-43) to mask PII in logs separately. Similarly, the `match` field in guardrail trace output contains the original PII value, not the masked output. 7. Test with sample inputs containing each PII type and verify both input blocking and output anonymization work as expected. |
+| Reference | [Bedrock Guardrails Sensitive Information Filters](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-sensitive-filters.html) |
+
+#### FS-46 — Data Classification Tagging
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.6] — "Implement data classification scanning and access controls on the data sources connected to your AI system to prevent disclosure of company-confidential or proprietary information." |
+| Description | Verifies AI/ML S3 buckets are tagged with data classification labels. |
+| Detection | Lists S3 buckets and filters for AI/ML-related names or tags. Calls `s3:GetBucketTagging` for each and checks for a `data-classification` tag with values like `public`, `internal`, `confidential`, `restricted`. Flags buckets missing the tag. |
+| Remediation | 1. Define a data classification taxonomy (e.g., Public, Internal, Confidential, Restricted). 2. Tag all AI/ML S3 buckets with `data-classification=`. 3. **Detective enforcement:** Create an AWS Config managed rule (`required-tags`, checks up to six tag keys at a time) to identify buckets missing the tag and trigger remediation via a custom SSM automation document (note: the AWS-managed `AWS-SetRequiredTags` automation document does NOT work as a remediation with this rule — you must author a custom Systems Manager automation document). 4. **Preventive enforcement:** Use AWS Organizations **Tag Policies** to require the `data-classification` tag key with allowed values (Public, Internal, Confidential, Restricted) across accounts — Tag Policies are preventive and complement the detective Config rule. 5. Use tag-based IAM policies (via condition keys `aws:ResourceTag/data-classification`) to restrict S3 access based on classification level. 6. Pair with Macie classification jobs (see FS-44) so that buckets automatically classified as containing sensitive data are flagged if their `data-classification` tag is missing or inconsistent with the Macie findings. |
+| Reference | [AWS Tagging Best Practices](https://docs.aws.amazon.com/tag-editor/latest/userguide/tagging.html), [AWS Config required-tags Rule](https://docs.aws.amazon.com/config/latest/developerguide/required-tags.html), [AWS Organizations Tag Policies](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_tag-policies.html) |
+
+---
+
+## Part 3 — Application-Layer Controls & Material Gaps (FS-47 to FS-69)
+
+> **Guide risk categories:** Hallucination (FS-47..50, §1.2.7), Prompt Injection (FS-51..54, §1.2.8), Improper Output Handling (FS-55..58, §1.2.13), Off-Topic & Inappropriate Output (FS-59..60, §1.2.2), Out-of-Date Training Data (FS-61..63, §1.2.10), Additional Controls — Material Gaps (FS-64..69). FS-64 is merged into upstream BR-04 — see the extension note in the Material Gaps section.
+
+### Hallucination (FS-47 to FS-50)
+
+> **Guide source:** §1.2.7 Hallucination. Guide-listed mitigations:
+> (a) prompt engineering;
+> (b) RAG with Bedrock Knowledge Bases;
+> (c) detect hallucinations in RAG and agent-based systems;
+> (d) HITL validation for internal AI systems;
+> (e) Automated Reasoning checks in Bedrock Guardrails;
+> (f) Bedrock Guardrails contextual grounding checks with reference source and query;
+> (g) response disclaimers in customer-facing applications informing users that AI responses
+> should be verified for critical decisions.
+
+#### FS-47 — Guardrail Grounding Threshold
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.7] — "You can use Amazon Bedrock Guardrails to detect and filter hallucinations in model responses by performing contextual grounding checks when you provide a reference source and query." |
+| Description | Verifies guardrail grounding thresholds are set appropriately for financial use cases (this assessment recommends ≥ 0.7; AWS does not prescribe a specific minimum, but the valid range is 0 to 0.99). Note: contextual grounding checks are not supported for conversational chatbot use cases — only for summarization, paraphrasing, and Q&A. |
+| Detection | Calls `bedrock:GetGuardrail` and inspects `contextualGroundingPolicy.filters` for the `GROUNDING` filter type. Checks that the `threshold` value is ≥ 0.7. Flags guardrails with lower thresholds or no grounding filter. |
+| Remediation | 1. Update the guardrail to set the grounding filter threshold to at least 0.7 (this assessment recommends 0.8 for financial services to reduce hallucination risk — note: AWS does not prescribe a specific minimum, but the valid range is **0 to 0.99**; a value of 1.0 is explicitly invalid and will block all content per AWS documentation). 2. Enable the grounding filter for both the `GROUNDING` and `RELEVANCE` types. 3. Test with prompts that should and should not be grounded in the reference source — tune the threshold based on your false-positive/false-negative tolerance. 4. Monitor grounding filter invocation rates via CloudWatch using the `AWS/Bedrock/Guardrails` namespace. **Important limitation:** Contextual grounding checks support only summarization, paraphrasing, and question-answering use cases — **Conversational QA / Chatbot use cases are explicitly not supported** per AWS documentation. For FinServ chatbot deployments, use denied topics and content filters (FS-28, FS-36, FS-59) as the primary hallucination-mitigation controls instead. |
+| Reference | [Bedrock Guardrails Contextual Grounding](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-contextual-grounding-check.html) |
+
+#### FS-48 — RAG Knowledge Base
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.1, §1.2.7, §1.2.10] — "Use Retrieval-Augmented Generation (RAG) to enhance your model responses with information from trusted knowledge bases." Referenced in three separate guide risk sections. |
+| Description | Checks active Knowledge Bases are configured for RAG grounding. |
+| Detection | Calls `ListKnowledgeBases` (via the `bedrock-agent` boto3 client; IAM action `bedrock:ListKnowledgeBases`) and checks that at least one KB exists with `status=ACTIVE`. Flags accounts with no active KBs when Bedrock models are in use (indicating responses are ungrounded). |
+| Remediation | 1. Create a Bedrock Knowledge Base with your authoritative data sources. 2. Configure the KB with an appropriate embedding model and vector store. 3. Use `RetrieveAndGenerate` API instead of direct `InvokeModel` for customer-facing use cases. 4. Sync data sources on a regular schedule. |
+| Reference | [Bedrock Knowledge Bases](https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base.html) |
+
+#### FS-49 — Hallucination Disclaimer
+
+| Field | Detail |
+|-------|--------|
+| Severity | Informational |
+| Guide ref | [Guide §1.2.7] — "Implement response disclaimers in customer-facing applications, to inform end users that AI-generated responses should be verified for critical decisions." References "AWS Well-Architected Framework Generative AI Lens - Implement guardrails to mitigate harmful or incorrect model responses". |
+| Description | Advisory: verifies application adds hallucination disclaimers to AI-generated outputs. |
+| Detection | Advisory check — inspects application Lambda environment variables for disclaimer-related settings. Checks for post-processing Lambda functions that append disclaimers. |
+| Remediation | 1. Add a standard disclaimer to all AI-generated responses: "This response is generated by AI and may contain inaccuracies. Please verify critical information independently." 2. Make the disclaimer configurable and non-removable by prompt manipulation. 3. For financial decisions, add: "This does not constitute financial advice." |
+| Reference | [AWS Well-Architected GenAI Lens](https://docs.aws.amazon.com/wellarchitected/latest/generative-ai-lens/gensec02-bp01.html) |
+
+#### FS-50 — Relevance Grounding Filters
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.2, §1.2.7] — "Use Amazon Bedrock Guardrails to detect and filter hallucinations in model responses by performing contextual grounding checks." Contextual grounding covers both `GROUNDING` and `RELEVANCE` filter sub-types. |
+| Description | Checks guardrails have relevance grounding filters to prevent off-topic responses. |
+| Detection | Calls `bedrock:GetGuardrail` and inspects `contextualGroundingPolicy.filters` for the `RELEVANCE` filter type. Flags guardrails with no relevance filter configured. |
+| Remediation | 1. Update the guardrail to enable the `RELEVANCE` contextual grounding filter. 2. Set the threshold to at least 0.7 (valid range is **0 to 0.99**; a value of 1.0 is explicitly invalid per AWS documentation). 3. This ensures responses are relevant to the user's query and the provided reference source, filtering out off-topic hallucinations. **Important limitation:** Contextual grounding checks (both `GROUNDING` and `RELEVANCE`) support only summarization, paraphrasing, and question-answering use cases — **Conversational QA / Chatbot use cases are explicitly not supported** per AWS documentation. For FinServ chatbot deployments, use denied topics (FS-59) as the primary off-topic control. |
+| Reference | [Bedrock Guardrails Contextual Grounding](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-contextual-grounding-check.html) |
+
+### Prompt Injection (FS-51 to FS-54)
+
+> **Guide source:** §1.2.8 Prompt injection. Guide-listed mitigations:
+> (a) prompt engineering best practices to avoid prompt injection;
+> (b) input validation — sanitize user input, remove special characters or use escape sequences,
+> match expected format;
+> (c) secure coding practices — parameterized queries, avoid string concatenation, minimal
+> privileges;
+> (d) security testing — regular testing for prompt injection and vulnerabilities, pentest,
+> static code analysis, DAST;
+> (e) stay updated — keep Bedrock SDK, libraries, and dependencies current;
+> (f) Bedrock Guardrails to detect and block user inputs attempting to override system
+> instructions through prompt attacks.
+
+#### FS-51 — Prompt Attack Filters
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.8] — "Use Amazon Bedrock Guardrails to detect and block user inputs that attempt to override system instructions through prompt attacks." |
+| Description | Verifies guardrails have PROMPT_ATTACK content filters enabled and are configured correctly for the Standard tier. |
+| Detection | Calls `bedrock:GetGuardrail` and inspects `contentPolicy.filters` for a filter with `type=PROMPT_ATTACK`. Flags guardrails where this filter is absent, has `inputStrength` set to `NONE` or `LOW` (note: PROMPT_ATTACK only applies to inputs — there is no `outputStrength` for this filter type), or where `contentPolicy.tier.tierName=CLASSIC` (the PROMPT_ATTACK filter in Classic tier detects jailbreaks and prompt injection; in Standard tier it additionally detects **prompt leakage** — attempts to extract system prompts or developer instructions). |
+| Remediation | 1. Ensure the guardrail is configured with the **Standard** content filters tier — prompt leakage detection (extracting system prompts/developer instructions) is available only in Standard tier; jailbreak and prompt injection detection are available in both tiers. Standard tier requires cross-Region inference to be enabled on the guardrail. You can configure Standard tier on a **new or existing guardrail**: for an existing guardrail, modify it via `UpdateGuardrail` (set `tierConfig.tierName=STANDARD` in `contentPolicyConfig` and add a `crossRegionConfig.guardrailProfileIdentifier`), or use the console by editing the guardrail and selecting Standard tier with cross-Region inference. 2. Add a `PROMPT_ATTACK` content filter with `inputStrength=HIGH`. 3. **Wrap user input in guardrail input tags when using `InvokeModel` or `InvokeModelResponseStream`** — for these APIs, PROMPT_ATTACK only evaluates content enclosed in input tags (e.g., `user text ` — the reserved prefix is `amazon-bedrock-guardrails-guardContent` and the suffix should be a unique random string per request to prevent an attacker from closing the tag and appending malicious content). Untagged content is not evaluated for PROMPT_ATTACK when using these APIs. **Note:** When using the `Converse` API, use the `guardContent` field (`GuardrailConverseContentBlock`) in user messages to scope PROMPT_ATTACK evaluation to specific content — this is the Converse API equivalent of input tags. Without `guardContent`, the guardrail evaluates ALL message content (the entire messages array). Using `guardContent` in user messages ensures only user-provided content is evaluated for prompt attacks, while system prompts and conversation history are excluded. If no `guardContent` blocks are present in messages, the guardrail evaluates everything in the messages array. 4. Test with known prompt injection patterns (role-play attacks, instruction override, delimiter injection). 5. Monitor filter invocation rates via CloudWatch guardrail metrics (`InvocationsIntervened` in the `AWS/Bedrock/Guardrails` namespace, filtered by `GuardrailPolicyType=ContentPolicy`) for trending attack patterns. |
+| Reference | [Bedrock Guardrails Prompt Attack](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-prompt-attack.html), [Safeguard tiers for guardrails](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-tiers.html), [Securing Amazon Bedrock Agents against indirect prompt injections](https://aws.amazon.com/blogs/machine-learning/securing-amazon-bedrock-agents-a-guide-to-safeguarding-against-indirect-prompt-injections/) |
+
+#### FS-52 — Bedrock SDK Version Currency
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.8] — "Stay Updated – Keep your Amazon Bedrock SDK, libraries, and dependencies current to receive the latest security patches and updates." |
+| Description | Checks Bedrock Lambda functions use current (non-deprecated) runtimes and SDK versions. |
+| Detection | Calls `lambda:ListFunctions` and filters for functions with Bedrock-related names or environment variables referencing Bedrock. Checks each function's `Runtime` against the list of deprecated Lambda runtimes. |
+| Remediation | 1. Update Lambda functions to use a currently supported runtime — as of April 2026, recommended runtimes are `python3.13` or `python3.14` for Python (both deprecation date June 30, 2029; `python3.12` remains supported through Oct 31, 2028), and `nodejs22.x` or `nodejs24.x` for Node.js (`nodejs20.x` reaches deprecation on April 30, 2026 and should not be used for new deployments). 2. Update the Bedrock SDK (boto3/botocore) to the latest version in your requirements.txt or package.json. 3. Test after upgrading to verify no breaking changes. 4. Subscribe to AWS Lambda runtime deprecation notifications via EventBridge or SNS (Lambda also surfaces runtime deprecation notices via AWS Health Dashboard and Trusted Advisor). |
+| Reference | [Lambda Runtime Deprecation Policy](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html) |
+
+#### FS-53 — WAF Injection Protection Rules
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.8, extension] — WAF SQLi and known-bad-inputs rule groups are not named in the guide, but implement the guide mitigation "Secure Coding Practices – use parameterized queries, avoid string concatenation for input, grant minimal access privileges" at the network edge for web-facing GenAI endpoints. |
+| Description | Verifies WAF ACLs include SQL injection (`AWSManagedRulesSQLiRuleSet`) and known-bad-inputs (`AWSManagedRulesKnownBadInputsRuleSet`) managed rule groups for GenAI endpoints. |
+| Detection | Calls `wafv2:ListWebACLs(Scope=REGIONAL)` and for each calls `wafv2:GetWebACL`. Inspects the rules list for `AWSManagedRulesSQLiRuleSet` and `AWSManagedRulesKnownBadInputsRuleSet`. Flags ACLs missing either rule group. |
+| Remediation | 1. Add `AWSManagedRulesSQLiRuleSet` to your WAF Web ACL (contains SQLi detection rules for body, URI path, cookie, and query-string components). 2. Add `AWSManagedRulesKnownBadInputsRuleSet` for known Remote Command Execution (RCE) and vulnerability-discovery patterns (e.g., Log4j, Spring Core deserialization, path traversal) — note this rule group does NOT cover XSS; XSS is in `AWSManagedRulesCommonRuleSet` (see FS-56). 3. Set both rule groups to COUNT mode initially, review logs for false positives, then switch to BLOCK. 4. Create custom rules for GenAI-specific injection patterns if needed. |
+| Reference | [AWS WAF Managed Rules](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html) |
+
+#### FS-54 — Penetration Testing Evidence
+
+| Field | Detail |
+|-------|--------|
+| Severity | Informational |
+| Guide ref | [Guide §1.2.8] — "Security Testing – Test your applications regularly for prompt injection and other security vulnerabilities. Use penetration testing, static code analysis, and dynamic application security testing (DAST)." |
+| Description | Advisory: verifies GenAI applications have been penetration tested for prompt injection and other AI-specific vulnerabilities. |
+| Detection | Advisory check — inspects resource tags for `last-pentest-date` or checks for a documented penetration testing schedule. Cannot be fully automated. |
+| Remediation | 1. Conduct penetration testing of your GenAI application at least annually and before major releases. 2. Include AI-specific test cases: prompt injection, jailbreak attempts, data extraction, system prompt leakage. 3. Use tools like Garak, PyRIT, manual red-teaming, or the **AWS Security Agent**. As of the March 2026 GA announcement, Security Agent runs from 6 AWS regions (N. Virginia, Oregon, Ireland, Frankfurt, Sydney, Tokyo) but can test targets across AWS, Azure, GCP, and on-premises environments. For multi-account FinServ deployments, Security Agent supports penetration testing on VPC resources **shared across AWS accounts in the same AWS Organization** via AWS Resource Access Manager (RAM) — enable this by launching Security Agent from a central security account and sharing VPC resources from sub-accounts via RAM. **Verify current region coverage on the [AWS Security Agent page](https://aws.amazon.com/security-agent/) before citing**, as AWS has been expanding regional availability and feature set rapidly. 4. Document findings and track remediation. 5. Tag resources with `last-pentest-date` for audit trail. |
+| Reference | [AWS Penetration Testing Policy](https://aws.amazon.com/security/penetration-testing/), [AWS Security Agent GA](https://aws.amazon.com/about-aws/whats-new/2026/03/aws-security-agent-ondemand-penetration/) |
+
+### Improper Output Handling (FS-55 to FS-58)
+
+> **Guide source:** §1.2.13 Improper output handling. Guide-listed mitigations:
+> (a) implement output validation rules against expected response format (e.g., JSON schema,
+> SQL schema);
+> (b) apply context-specific output sanitization — HTML encoding for web apps, SQL
+> parameterization for database queries, command escaping for system integrations;
+> (c) Practical guidance: treat model output as untrusted user input; use Bedrock Agents
+> action-group Lambda to implement output encoding so output text is non-executable by
+> JavaScript or Markdown.
+
+#### FS-55 — Output Validation Lambda
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.13] — "Implement output validation rules specific to the expected response format. For example, if the AI system is expected to return structured data (JSON, SQL), validate the output against the expected schema before processing." |
+| Description | Checks for Lambda functions implementing output validation/sanitization before AI responses reach downstream consumers. |
+| Detection | Calls `lambda:ListFunctions` and searches for functions with naming patterns indicating output validation (e.g., "output-valid", "sanitiz", "post-process", "response-filter"). Flags if no such functions exist. |
+| Remediation | 1. Implement a post-processing Lambda that validates AI model output before it reaches the end user or downstream system. 2. Validate output against expected schema (JSON schema validation for structured responses). 3. Strip or escape any executable content (HTML tags, JavaScript, SQL fragments). 4. Log rejected outputs for security monitoring. |
+| Reference | [AWS Well-Architected Security Pillar — Application Security](https://docs.aws.amazon.com/wellarchitected/latest/security-pillar/application-security.html), [Bedrock Prompt Injection Security](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-injection.html), [Well-Architected FSI Lens — FSISEC14 Monitor AI system outputs for security issues](https://docs.aws.amazon.com/wellarchitected/latest/financial-services-industry-lens/fsisec14.html) |
+
+#### FS-56 — XSS Prevention WAF
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.13, extension] — WAF XSS rule groups are not named in the guide, but implement the guide mitigation "Apply context-specific output sanitization ... apply HTML encoding for web applications" at the network edge. |
+| Description | Verifies WAF ACLs include XSS prevention rules to protect against AI-generated outputs containing malicious scripts. |
+| Detection | Calls `wafv2:GetWebACL` for each regional ACL and inspects rules for `AWSManagedRulesCommonRuleSet` (which includes the four `CrossSiteScripting_*` rules covering request body, query arguments, cookies, and URI path) or custom rules using `XssMatchStatement` on request components. Flags ACLs missing XSS protection. |
+| Remediation | 1. Add `AWSManagedRulesCommonRuleSet` to your WAF Web ACL (includes `CrossSiteScripting_COOKIE`, `CrossSiteScripting_QUERYARGUMENTS`, `CrossSiteScripting_BODY`, and `CrossSiteScripting_URIPATH` rules — all four inspect **inbound request** components). 2. `XssMatchStatement` and the CRS XSS rules inspect **request** components only (body, query string, URI path, cookies, headers). WAF does NOT inspect arbitrary response bodies for XSS — response inspection (`ResponseInspection`) is available only in `AWSManagedRulesATPRuleSet`/`AWSManagedRulesACFPRuleSet` for CloudFront-protected ACLs and only scans for configured success/failure strings. 3. To protect against XSS in **AI-generated output**, enforce output encoding at the application layer (see FS-57) — rendering raw model output in a browser without encoding is the root cause that WAF cannot mitigate after the fact. 4. Apply output encoding in your application layer as defense-in-depth. |
+| Reference | [AWS WAF XSS Protection](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html) |
+
+#### FS-57 — Output Encoding
+
+| Field | Detail |
+|-------|--------|
+| Severity | Informational |
+| Guide ref | [Guide §1.2.13] — "Apply context-specific output sanitization based on the downstream consumer. For example, apply HTML encoding for web applications, SQL parameterization for database queries, and command escaping for system integrations." Practical guidance: "Use Amazon Bedrock Agents to securely integrate with AWS native and third-party services and implement output encoding in the action group Lambda function under an Amazon Bedrock Agent. Encoding all output text presented to end-users makes it automatically non-executable by JavaScript or Markdown." |
+| Description | Advisory: verifies application encodes GenAI outputs appropriately for the rendering context (HTML, JSON, SQL). |
+| Detection | Advisory check — inspects application Lambda functions for encoding libraries or patterns (e.g., `html.escape`, `json.dumps`, `markupsafe`). Checks environment variables for encoding-related configuration. |
+| Remediation | 1. Treat all model output as untrusted user input. 2. Apply context-specific encoding: HTML encoding for web display, SQL parameterization for database queries, command escaping for system integrations. 3. Use Bedrock Agents action-group Lambda functions to implement output encoding — encoding all output text makes it non-executable by JavaScript or Markdown renderers. 4. Never render raw model output in a web page without encoding. |
+| Reference | [OWASP Output Encoding](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html) |
+
+#### FS-58 — Output Schema Validation
+
+| Field | Detail |
+|-------|--------|
+| Severity | Informational |
+| Guide ref | [Guide §1.2.13] — "Implement output validation rules specific to the expected response format. For example, if the AI system is expected to return structured data (JSON, SQL), validate the output against the expected schema before processing." |
+| Description | Checks for structured output validation in GenAI pipelines (JSON schema, XML schema, or custom validators). |
+| Detection | Inspects Step Functions state machine definitions for states that perform schema validation (e.g., `Choice` states with JSON path conditions, Lambda states with "schema" or "validate" in the name). Does not rely on API Gateway response models as a validation signal because those are used for SDK generation, not runtime validation. |
+| Remediation | 1. Define a JSON schema for expected AI output format. 2. Add a validation step in your pipeline (Lambda function or Step Functions Choice state) that rejects non-conforming outputs **before** returning the response to clients — this is the runtime enforcement point. 3. Note: API Gateway *response models* in REST APIs are used for SDK generation (user-defined data types) and documentation — they do NOT perform runtime validation of response payloads. API Gateway *request validators* only validate inbound requests against request models. To validate AI output at runtime, implement the check in Lambda/Step Functions before the response reaches API Gateway. 4. Return a safe fallback response when validation fails. 5. Log rejected outputs (without leaking sensitive content) for security monitoring. |
+| Reference | [API Gateway Request and Response Validation](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-request-validation.html) |
+
+### Off-Topic & Inappropriate Output (FS-59 to FS-60)
+
+> **Guide source:** §1.2.2 Off-topic and inappropriate output. Guide-listed mitigations:
+> (a) prompt engineering with an allowlist of approved topics aligned with business purpose;
+> (b) content filters and denied topics in Bedrock Guardrails;
+> (c) Bedrock Guardrails contextual grounding check with reference source and query;
+> (d) HITL validation for internal AI systems.
+
+#### FS-59 — Guardrail Topic Allowlist
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.2] — "Configure content filters and guardrails to restrict model responses to approved topics." The check name uses "allowlist" loosely — implementation uses denied-topic lists to block out-of-scope content. |
+| Description | Verifies guardrails restrict GenAI to on-topic financial services responses via denied topics. |
+| Detection | Calls `bedrock:GetGuardrail` and inspects `topicPolicy.topics`. Checks that denied topics exist to block off-topic conversations (e.g., politics, entertainment, medical advice). Flags guardrails with no topic restrictions. |
+| Remediation | 1. Define denied topics that are outside your business scope (e.g., "medical advice", "legal advice", "political opinions", "entertainment recommendations"). 2. Add these as denied topics in the guardrail with clear descriptions and sample phrases. 3. Test with off-topic prompts to verify they are blocked. 4. Use the system prompt to positively scope the assistant's role. |
+| Reference | [Bedrock Guardrails Topic Policies](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-denied-topics.html) |
+
+#### FS-60 — Contextual Grounding for Off-Topic
+
+| Field | Detail |
+|-------|--------|
+| Severity | Informational |
+| Guide ref | [Guide §1.2.2] — "Use prompt engineering techniques to guide the model toward appropriate topics and prevent unwanted responses. Include an allowlist of approved topics aligned with the business purpose." Use of Bedrock Prompt Management for system prompt versioning is an implementation choice. |
+| Description | Advisory: verifies system prompts explicitly scope the assistant's role to prevent off-topic responses. |
+| Detection | Advisory check — inspects Bedrock Prompt Management templates (via `ListPrompts` on the `bedrock-agent` boto3 client; IAM action `bedrock:ListPrompts`) for system prompt content that defines the assistant's role, scope, and boundaries. Flags if no prompt templates exist. |
+| Remediation | 1. Define a clear system prompt that states: the assistant's role, allowed topics, prohibited topics, and response format. 2. Use Bedrock Prompt Management to version and manage system prompts. 3. Include explicit instructions like "You are a financial services assistant. Only answer questions related to [specific topics]. Decline all other requests politely." 4. Test with boundary-case prompts. |
+| Reference | [Bedrock Prompt Management](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-management.html) |
+
+### Out-of-Date Training Data (FS-61 to FS-63)
+
+> **Guide source:** §1.2.10 Out-of-date training data. Guide-listed mitigations:
+> (a) RAG with Bedrock Knowledge Bases;
+> (b) keep knowledge bases up to date (sync data sources);
+> (c) HITL validation for internal AI systems;
+> (d) data currency disclaimers in AI system responses; source attribution via
+> RetrieveAndGenerate API for users to verify currency.
+
+#### FS-61 — Knowledge Base Sync Schedule
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.10] — "Keep your knowledge bases up to date." Automated scheduling via EventBridge operationalises this mitigation. |
+| Description | Checks EventBridge Scheduler or EventBridge rules automate KB data source sync on a regular schedule. |
+| Detection | Calls `events:ListRules` and searches for rules with targets that invoke `StartIngestionJob` (IAM action `bedrock:StartIngestionJob`) or Lambda functions that trigger KB sync. Also checks AWS Scheduler (`scheduler:ListSchedules`) for schedules targeting KB sync. Flags if no scheduled sync mechanism exists. |
+| Remediation | 1. Use **EventBridge Scheduler** (the current recommended approach — EventBridge scheduled rules are a legacy feature) to create a recurring schedule that triggers KB data source sync: create a schedule with a rate expression (e.g., `rate(1 day)`) or cron expression (e.g., `cron(0 2 * * ? *)`) targeting a Lambda function. 2. The Lambda function calls `StartIngestionJob` (IAM action `bedrock:StartIngestionJob`) for each data source. 3. Add error handling and CloudWatch alarms for failed syncs. |
+| Reference | [EventBridge Scheduler](https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html), [EventBridge Scheduled Rules (legacy)](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html) |
+
+#### FS-62 — Data Currency Disclaimer
+
+| Field | Detail |
+|-------|--------|
+| Severity | Informational |
+| Guide ref | [Guide §1.2.10] — "Include data currency disclaimers in AI system responses where appropriate. Use source attribution in RAG-based response for end users to verify currency of information." |
+| Description | Advisory: verifies application adds data currency disclaimers to AI-generated outputs. |
+| Detection | Advisory check — inspects application configuration for data-currency disclaimer settings. Checks system prompts for instructions to include data freshness information. |
+| Remediation | 1. Add a data currency disclaimer to responses: "This information is based on data available as of [date]. It may not reflect the most recent changes." 2. Use the `RetrieveAndGenerate` API's source attribution to display document dates. 3. Configure the system prompt to instruct the model to caveat time-sensitive information. |
+| Reference | [Bedrock RetrieveAndGenerate API](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent-runtime_RetrieveAndGenerate.html) |
+
+#### FS-63 — Foundation Model Lifecycle Policy
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.10, extension] — FM currency is conceptually related to "out-of-date training data" but the specific Bedrock lifecycle-status check is not named in the guide. The guide's "1.1.6 Monitor and improve" general guidance says "Update your foundation models when new versions become available" — this FS check operationalises that guidance. See also FS-34 (TPRM) which the guide places under §1.2.12. |
+| Description | Checks for a model lifecycle management process and Config rules to ensure models are updated when new versions are available. |
+| Detection | Calls `config:DescribeConfigRules` and searches for rules targeting Bedrock resources. Calls `bedrock:GetFoundationModel` for each model in use and inspects `modelLifecycle.status`. Flags models with status `LEGACY` (note: the Bedrock API exposes only two lifecycle status values — `ACTIVE` and `LEGACY`; models past their `endOfLifeTime` are removed from the service entirely and return a ResourceNotFound error, so any model still reachable via the API that is not `ACTIVE` will be `LEGACY`). |
+| Remediation | 1. Create an AWS Config custom rule that flags Bedrock models with `modelLifecycle.status=LEGACY`. 2. Establish a model lifecycle policy: evaluate new model versions within 30 days of release, test in staging, migrate production within 90 days (and before the `endOfLifeTime` published in the Bedrock model lifecycle page). 3. Subscribe to AWS Bedrock model lifecycle notifications. 4. Document the policy and assign an owner. 5. **Budget planning for FinServ:** For models with EOL dates after February 1, 2026, after a minimum of 3 months in Legacy state a model enters a **public extended access period** during which the model provider may set higher pricing. The `publicExtendedAccessTime` timestamp in the `FoundationModelLifecycle` response indicates when this phase begins. Include this phase in contract-and-budget review so FinServ cost governance teams are aware of potential price changes before migrating off Legacy models. |
+| Reference | [Bedrock Model Lifecycle](https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html) |
+
+### Additional Controls — Material Gaps (FS-64 to FS-69)
+
+These checks address mitigations explicitly called out in the Responsible AI GRC guide that were
+not covered by the original checks in the upstream AIML Security Assessment (BR/SM/AC).
+FS-64 is merged into upstream BR-04 (see extension note below); FS-65 to FS-69 ship as
+standalone checks.
+
+#### FS-64 — Guardrail Trace Logging → *Merged into upstream BR-04*
+
+> **Upstream extension note (do not ship as a standalone check):** The detection and remediation
+> content from FS-64 should be added as a refinement of the existing **BR-04 (Model Invocation
+> Logging)** check in the upstream repo.
+>
+> **What to add to BR-04:**
+> - After verifying that `bedrock:GetModelInvocationLoggingConfiguration` shows logging is
+> enabled, additionally verify the log output captures **guardrail trace data**: when
+> guardrails are applied during inference, the invocation log contains a `guardrailTrace`
+> object with `action` (values: `INTERVENED` or `NONE`), `inputAssessments`, and
+> `outputAssessments` arrays detailing which policies were evaluated and their results.
+> - **Important logging coverage gap:** Model invocation logging only captures calls made through the `bedrock-runtime` endpoint (`Converse`, `ConverseStream`, `InvokeModel`, `InvokeModelWithResponseStream`). Calls made through the `bedrock-mantle` endpoint (e.g., the Responses API) are **not currently captured** by invocation logging. If your application uses the Responses API, implement application-level logging as a compensating control.
+> - Add a remediation note on **retention requirements**: NYDFS 23 NYCRR 500.06 explicitly
+> requires cybersecurity records for ≥ 5 years; SR 11-7 does not prescribe a specific period
+> but requires documentation be maintained for the duration of model use plus a reasonable
+> period thereafter (commonly met with 5–7 year retention per firm policy). Consult your
+> compliance and records-management team for exact requirements.
+> - Suggest creating CloudWatch Metrics filters to track guardrail intervention rates (filter
+> on `guardrailTrace.action = INTERVENED`) and applying CloudWatch Logs data protection
+> policies to mask PII in traces.
+> - Guide traceability: [Guide §1.2.1] — "Maintain audit logs of AI-generated outputs and the
+> guardrails applied to support regulatory reporting and post-incident analysis." Also
+> §1.2.9 — "Implement audit logging of all actions taken by AI agents."
+>
+> **Reference:** [Bedrock Model Invocation Logging](https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html)
+
+#### FS-65 — KB Data Source S3 Event Notifications
+
+| Field | Detail |
+|-------|--------|
+| Severity | High (deleted bucket) / Medium (notifications) |
+| Guide ref | [Guide §1.2.3] — "Use integrity monitoring on knowledge base data sources to detect unauthorized modifications... For example on S3 data sources use Amazon S3 event notification to track changes to documents." **Note:** This check overlaps with FS-33; FS-33 verifies notifications are *enabled* on the bucket, while FS-65 verifies that notifications are *routed to an alerting destination* (SNS/Lambda/EventBridge rule with a target). In the final PR to aws-samples these two checks may be consolidated into a single check at the reviewer's discretion. |
+| Description | Checks that S3 event notifications on KB data-source buckets are routed to an alerting destination (EventBridge rule with SNS/Lambda target, or direct SNS/SQS/Lambda notification) — not just enabled with no consumer. |
+| Detection | Identifies KB data-source S3 buckets via `ListDataSources` and `GetDataSource` (via the `bedrock-agent` boto3 client; IAM actions `bedrock:ListDataSources` and `bedrock:GetDataSource`). For each bucket, calls `s3:GetBucketNotificationConfiguration` and checks for the presence of `EventBridgeConfiguration`, `TopicConfigurations`, `QueueConfigurations`, or `LambdaFunctionConfigurations`. Flags buckets with no notifications configured. |
+| Remediation | 1. Enable EventBridge notifications on each KB data-source bucket: `aws s3api put-bucket-notification-configuration --bucket --notification-configuration '{"EventBridgeConfiguration":{}}'`. 2. Create an EventBridge rule matching S3 event detail types `"Object Created"` and `"Object Deleted"` for the bucket (note: when S3 sends events to **EventBridge**, the event detail types are `Object Created`/`Object Deleted`; the `s3:ObjectCreated:*` and `s3:ObjectRemoved:*` wildcard names are used only for **direct** SNS/SQS/Lambda notification configurations, not for EventBridge rule patterns). 3. Route events to an SNS topic or Lambda function for alerting. 4. Integrate alerts into your security incident response workflow. |
+| Reference | [S3 EventBridge Integration](https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventBridge.html) |
+
+#### FS-66 — AgentCore End-User Identity Propagation
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.6 — Practical guidance] — "1. Implement least privilege for identities associated with agents and tool services. 2. Where supported by the tool service ensure that communications to tool services or agents are authorized by the end user. 3. Customers building their own tool services should consider propagating end-user identities separately; ensuring these identities can be validated and are not revealed to unauthorized third parties." |
+| Description | Verifies AgentCore runtimes are configured to propagate end-user identities to downstream tool services, ensuring tool calls are authorized by the originating user and not solely by the agent execution role. |
+| Detection | Calls `ListAgentRuntimes` (via the `bedrock-agentcore-control` boto3 client; IAM action `bedrock-agentcore:ListAgentRuntimes`) and inspects each runtime's `authorizerConfiguration.customJWTAuthorizer` for a `discoveryUrl` and allowed audiences/clients/scopes. Flags runtimes with no JWT authorizer (meaning inbound calls carry no verifiable end-user identity), and advises configuring outbound OAuth for downstream tool services. |
+| Remediation | 1. Configure a custom JWT inbound authorizer on each AgentCore runtime: specify `discoveryUrl`, `allowedAudience`, `allowedClients`, and optional required custom claims. 2. Propagate the end-user's identity via the `X-Amzn-Bedrock-AgentCore-Runtime-User-Id` header and JWT token in the `Authorization` header when calling downstream tool services. **Important:** Invoking `InvokeAgentRuntime` with the `X-Amzn-Bedrock-AgentCore-Runtime-User-Id` header requires the distinct IAM action `bedrock-agentcore:InvokeAgentRuntimeForUser` in addition to `bedrock-agentcore:InvokeAgentRuntime`. Only trusted principals should hold this permission — scope it to specific runtime resources with IAM resource conditions, never via wildcard. For runtimes that do not need user-id delegation, explicitly **deny** `bedrock-agentcore:InvokeAgentRuntimeForUser` to prevent the header from being accepted. Additionally, derive the user-id from the authenticated principal's context (IAM caller identity or JWT claims) rather than from arbitrary client-supplied values to prevent user impersonation, and log the relationship between the authenticated IAM principal (via CloudTrail's SigV4 context) and the `user-id` value passed. 3. Configure outbound OAuth 2.0 for agents accessing third-party resources on behalf of the user. 4. Ensure tool services validate the propagated JWT before executing actions. 5. Implement agent identity segregation: assign distinct identities to each sub-agent in multi-agent workflows so actions are separately attributable. 6. Apply a maker-checker pattern for critical financial actions — require a second agent or human to verify before execution. 7. Do not log or expose propagated identity tokens to unauthorized third parties. |
+| Reference | [Configure Inbound JWT Authorizer](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/inbound-jwt-authorizer.html), [Inbound and Outbound Auth](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-oauth.html) |
+
+#### FS-67 — Agent Financial Transaction Value Thresholds
+
+| Field | Detail |
+|-------|--------|
+| Severity | High |
+| Guide ref | [Guide §1.2.9] — "Enforce transaction value thresholds and action boundaries on agent tool calls (for example to cap financial transaction amounts)." |
+| Description | Checks AgentCore Policy Engine (attached to Gateways) or action-group Lambda functions enforce maximum transaction-value limits (e.g., cap on financial amounts an agent can initiate) to prevent runaway or unauthorized high-value transactions. |
+| Detection | (a) Calls `ListGateways` (via the `bedrock-agentcore-control` boto3 client; IAM action `bedrock-agentcore:ListGateways`) and for each inspects attached Policy Engine Cedar policies for transaction-value constraints (policies referencing amount, limit, or threshold context attributes). (b) Calls `lambda:ListFunctions` and filters for agent action-group Lambda functions. Inspects each function's environment variables for threshold-related keys (e.g., `MAX_TRANSACTION_AMOUNT`, `TRANSACTION_LIMIT`). Flags gateways and functions with no threshold configuration. |
+| Remediation | 1. Add transaction-value threshold environment variables to each agent action-group Lambda (e.g., `MAX_TRANSACTION_AMOUNT=10000`). 2. Implement threshold enforcement logic in the Lambda handler that rejects or escalates transactions exceeding the limit. 3. Author Cedar policies in the AgentCore Policy Engine that evaluate tool-call context attributes (amount, currency, tool) and deny calls exceeding defined limits. 4. Route transactions exceeding thresholds to a human-in-the-loop approval step via Step Functions callback pattern. |
+| Reference | [Policy in AgentCore](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy.html), [AgentCore Example Policies](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/example-policies.html) |
+
+#### FS-68 — API Gateway Request Body Size Limits
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.11] — "To protect your API endpoints, set maximum length limits for input requests when you use large language models (LLMs) directly or through Amazon Bedrock." |
+| Description | Verifies API Gateway REST/HTTP APIs fronting GenAI endpoints have WAF `SizeConstraintStatement` rules enforcing a maximum request body size, optionally paired with an API Gateway request-body JSON schema that bounds individual field lengths — to prevent token-exhaustion attacks via oversized prompts. |
+| Detection | Calls `apigateway:GetRestApis` and for each calls `apigateway:GetRequestValidators` to check for validators (validators enforce parameter-existence and request-body JSON schema conformance — not total body size). Calls `wafv2:GetWebACL` for associated ACLs and inspects rules for `SizeConstraintStatement` targeting the request body. Flags APIs with no WAF `SizeConstraintStatement` on body, since that is the only AWS-native mechanism that enforces a custom maximum body size in front of API Gateway. |
+| Remediation | 1. **Primary control — WAF `SizeConstraintStatement`:** Add a WAF `SizeConstraintStatement` rule on your regional Web ACL that blocks requests whose body size exceeds your maximum allowed prompt length (e.g., 32 KB). Verify that the Web ACL's `AssociationConfig.RequestBody.DefaultSizeInspectionLimit` is set high enough (16 KB default; can be increased to 32/48/64 KB) so WAF can actually inspect bodies at the size you are enforcing against — if the inspection limit is lower than the `SizeConstraintStatement` threshold, oversized requests fall through to oversize handling instead of the rule. This is the only AWS-native way to enforce a custom maximum body size before requests reach API Gateway. 2. **Secondary control — API Gateway request validation:** Add an API Gateway request validator with a request-body model (JSON schema). Request validators do **not** enforce total body size, but a JSON schema can constrain individual string fields with `maxLength` and arrays with `maxItems`, which indirectly bounds payload content. Note API Gateway REST APIs also enforce a service-level hard limit of 10 MB per request (6 MB when integrated with Lambda) that you cannot lower. 3. Set the `max_tokens` parameter in Bedrock API calls to cap output length. 4. Implement client-side token counting before submitting requests. |
+| Reference | [WAF Size Constraint](https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-type-size-constraint-match.html), [WAF Body Inspection Size Limit](https://docs.aws.amazon.com/waf/latest/developerguide/web-acl-setting-body-inspection-limit.html), [API Gateway Request Validation](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-request-validation.html) |
+
+#### FS-69 — Prompt Input Validation Function
+
+| Field | Detail |
+|-------|--------|
+| Severity | Medium |
+| Guide ref | [Guide §1.2.8] — "Input Validation – Before you send user input to Amazon Bedrock or the tokenizer, validate and sanitize it by removing special characters or using escape sequences. Make sure the input matches your expected format." |
+| Description | Checks for a Lambda function or API Gateway request validator that sanitizes user prompt input (strips special characters, enforces expected format, rejects oversized inputs) before forwarding to Bedrock, complementing WAF-level controls. |
+| Detection | Calls `lambda:ListFunctions` and searches for functions with input-validation naming patterns (e.g., "sanitiz", "validat", "input-filter", "prompt-guard", "preprocess"). Flags if no such functions exist. |
+| Remediation | 1. Implement a Lambda authorizer or pre-processing function that: strips or escapes special characters from user input; validates input against an expected format (e.g., regex allowlist); rejects inputs exceeding maximum token/character limits; logs rejected inputs for security monitoring. 2. Use parameterized prompt templates (Bedrock Prompt Management) instead of string concatenation. 3. Apply Bedrock Guardrails PROMPT_ATTACK filter as a complementary control. 4. Integrate the validation function as an API Gateway Lambda authorizer or Step Functions pre-processing step. 5. Implement schema validation for all tool interactions — validate both inputs to and outputs from tools against defined JSON schemas per AWS Prescriptive Guidance for tool integration security. 6. Enforce TLS for all remote tool communications. |
+| Reference | [Bedrock Prompt Injection Security](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-injection.html), [Security Best Practices for Tool Integration](https://docs.aws.amazon.com/prescriptive-guidance/latest/agentic-ai-frameworks/security-best-practices-for-tool-integration.html) |
diff --git a/docs/SECURITY_CHECKS_FINSERV_COMMON.md b/docs/SECURITY_CHECKS_FINSERV_COMMON.md
deleted file mode 100644
index 79c1bb7..0000000
--- a/docs/SECURITY_CHECKS_FINSERV_COMMON.md
+++ /dev/null
@@ -1,179 +0,0 @@
-# FinServ GenAI Risk Checks — Common Reference
-
-This file contains shared reference material used by all three parts of the FinServ GenAI
-security checks. It is not a checks file itself — the 64 standalone checks are split across Parts 1-3
-(5 additional FS checks are merged into upstream SM/BR checks — see the consolidation table below):
-
-- `SECURITY_CHECKS_FINSERV_PART1_INFRA_CONTROLS.md` — FS-01 to FS-26 (Unbounded, Excessive Agency, Supply Chain, Training Poisoning, Vector Weaknesses); FS-17, FS-18, FS-19, FS-23 merged into upstream
-- `SECURITY_CHECKS_FINSERV_PART2_GUARDRAILS_CONTENT_SAFETY.md` — FS-27 to FS-46 (Non-Compliant, Misinformation, Abusive, Biased, Sensitive Info)
-- `SECURITY_CHECKS_FINSERV_PART3_APP_LAYER_AND_GAPS.md` — FS-47 to FS-69 (Hallucination, Prompt Injection, Output Handling, Off-Topic, Stale Data, Material Gaps); FS-64 merged into upstream
-
-Together, Parts 1-3 plus this common file replace the earlier single-file `SECURITY_CHECKS_FINSERV_ADDITION.md`.
-
-## About the source
-
-The 69 FS checks are derived from the [AWS guide for Financial Services risk management of the
-use of Generative AI (March 2026)](https://d1.awsstatic.com/onedam/marketing-channels/website/public/global-FinServ-ComplianceGuide-GenAIRisks-public.pdf)
-(referred to throughout as "the FinServ Guide").
-
-Each check includes how it is **detected** (the AWS API calls or configuration inspected)
-and how a failure is **remediated** (the specific AWS actions to take).
-
-## PDF traceability
-
-The FinServ Guide organises AI-specific risks into **15 categories** (§1.2.1 through §1.2.15
-in the PDF). Every check below is tagged with one of:
-
-- **[PDF §x.y.z]** — mitigation is explicitly listed in that PDF section's "Mitigations or controls"
- table or "Practical guidance" callout.
-- **[PDF §x.y.z, extension]** — mitigation is consistent with the PDF's risk description but is
- not verbatim in the PDF; included because it is a widely-accepted AWS best practice for the
- same risk. These are labelled so reviewers know the provenance.
-
-## Severity rubric
-
-Severities follow a documented **Likelihood × Impact** methodology mapped to the AWS Security Hub
-ASFF label set (`Informational | Low | Medium | High`; `Critical` is reserved, not used this
-round). The full methodology, the 3×3 scoring matrix, the N/A **disposition rules**, and the
-authoritative per-finding assignments are in
-[`SECURITY_CHECKS_FINSERV_SEVERITY_METHODOLOGY.md`](./SECURITY_CHECKS_FINSERV_SEVERITY_METHODOLOGY.md)
-and [`SECURITY_CHECKS_FINSERV_SEVERITY_REGISTER.md`](./SECURITY_CHECKS_FINSERV_SEVERITY_REGISTER.md).
-
-| Severity | Criteria (ASFF-aligned) |
-|---|---|
-| **High** | Control whose absence can lead to direct regulatory breach, data exposure, large-scale financial loss, or full bypass of safety guardrails. |
-| **Medium** | Control whose absence materially increases the likelihood or impact of a risk category but does not by itself produce a breach. |
-| **Low** | Control that reduces residual risk or supports audit/observability but has alternative or compensating controls. |
-| **Informational** | No actionable issue is asserted. Used for three dispositions: (1) **NOT_APPLICABLE** — the control's resource type is absent (e.g., no Knowledge Bases, no guardrails); (2) **ADVISORY** — the control cannot be verified via AWS APIs and requires human review (finding name prefixed `ADVISORY:`); (3) checks awaiting manual verification. |
-
-> **Disposition rules (how a finding's severity is set):** severity is a property of the *control*
-> (its Likelihood × Impact), applied to that control's `Passed` and `Failed` rows alike. The `N/A`
-> family is fixed by disposition: **NOT_APPLICABLE → Informational**, **ADVISORY → Informational**,
-> **COULD_NOT_ASSESS** (access denied / unsupported region) **→ Low**. The legacy "Advisory" tier in
-> earlier revisions of this document is reconciled to the **Informational** label + `N/A` status +
-> `ADVISORY:` name prefix.
-
-## Validation note
-
-Detection and remediation guidance in this document was systematically validated against the
-FinServ Guide (March 2026), current AWS documentation, API references, and AWS announcements as
-of April 2026. IAM action names were verified against the AWS Service Authorization Reference
-for [Amazon Bedrock](https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonbedrock.html),
-[Amazon Bedrock AgentCore](https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonbedrockagentcore.html),
-and [Amazon OpenSearch Serverless](https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonopensearchserverless.html)
-(note: the OpenSearch Serverless IAM prefix is `aoss:`, not `opensearchserverless:` — the latter
-is the boto3 client name).
-CloudWatch metric namespaces were verified against the service-specific monitoring docs (Bedrock,
-Bedrock Agents, Bedrock Guardrails, SageMaker Model Monitor, SageMaker Clarify). CloudTrail
-event-type classification (management vs data) for Bedrock API operations was verified against the
-[Bedrock CloudTrail integration guide](https://docs.aws.amazon.com/bedrock/latest/userguide/logging-using-cloudtrail.html).
-Cost Anomaly Detection monitor-type values were verified against the
-[AnomalyMonitor API reference](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_AnomalyMonitor.html).
-Where AWS does not prescribe a specific value (e.g., grounding thresholds), this is explicitly
-called out as an assessment recommendation rather than an AWS requirement. AWS regional
-availability of new features (Automated Reasoning, AgentCore Policy, AWS Security Agent,
-cross-account guardrails) evolves rapidly — region lists in Parts 1-3 reflect the state at the
-cited announcement date and should be re-verified before audit reliance.
-
-## Contribution workflow
-
-The FS checks are contributed upstream as a single pull request via a personal GitHub fork
-of `aws-samples/sample-aiml-security-assessment`. The full 9-step process — feature-request
-GitHub issue, fork + feature branch, local ASH security scan, Conventional Commits, PR,
-GitHub Actions verification, reviewer assignment, and (only if needed) Git Defender
-exception — lives in [`GIT_WORKFLOW.md`](./GIT_WORKFLOW.md) at the repository root. A
-condensed version is in the `Git Workflow` section of [`IMPLEMENTATION_PLAN.md`](./IMPLEMENTATION_PLAN.md).
-
-Key quality gates before opening the PR (see `GIT_WORKFLOW.md` Step 5):
-
-1. `ruff check` and `ruff format --check` pass on `functions/security/finserv_assessments/`.
-2. `cfn-lint` and `sam validate --lint` pass on the SAM templates.
-3. [ASH v3](https://awslabs.github.io/automated-security-helper/) scan
- (`ash --source-dir . --fail-on-findings --config-overrides
- 'global_settings.severity_threshold=MEDIUM'`) reports zero Critical / High findings,
- or suppressions are documented in `.ash/.ash.yaml`.
-4. Amazon Code Defender (`git defender scan`) reports no secrets in the staged diff.
-
-Because `aws-samples` is an OSPO-managed organization, pushes to your personal fork of
-`aws-samples/*` are auto-allowed by Code Defender — a Git Defender exception ticket is
-**not expected** for this contribution.
-
-## Relationship to upstream SM/BR/AC checks
-
-The upstream [sample-aiml-security-assessment](https://github.com/aws-samples/sample-aiml-security-assessment)
-framework already provides 52 security checks (SM-01 to SM-25, BR-01 to BR-14, AC-01 to AC-13).
-The 69 FS checks in this document are **additive**: they enhance the upstream with FinServ-specific
-detection and remediation guidance drawn from the AWS FinServ GenAI Guide (March 2026). A few FS
-checks overlap with upstream checks — in those cases, the FS check adds FinServ-specific depth
-(e.g., protected-attribute facets, regulatory cadence requirements, denied-topic content for
-financial advice). The table below surfaces each overlap with a systematic recommendation based
-on five factors: (1) whether the detection target is the same AWS resource/configuration, (2)
-whether the FS check adds FinServ-specific regulatory specificity, (3) severity differentiation,
-(4) whether a customer would remediate them differently, and (5) PDF-traceability value.
-
-**Recommendation values:**
-
-- **Extend upstream** — merge FS detection/remediation detail into the upstream check; do not ship FS as a standalone entry in the final report. Best when both checks target the same resource and the FS content is an enhancement.
-- **Keep separate** — ship as a standalone FS check alongside the upstream check. Best when the FS check targets a different AWS resource, has materially different severity, or encodes a FinServ-specific regulatory requirement that would be diluted by merging.
-
-| FS check | Upstream check | Overlap analysis | Recommendation |
-|---|---|---|---|
-| FS-17 (Model Monitor Data Quality) | SM-07 (Model Monitor) | Same resource (`sagemaker:ListMonitoringSchedules`); FS-17 adds training-data-drift-specific guidance, exact CloudWatch namespace (`/aws/sagemaker/Endpoints/data-metric`), and `emit_metrics` requirement. | **Extend SM-07** — add FS-17's detection detail (namespace, `emit_metrics`) as a refinement of the existing check |
-| FS-18 (Model Drift Detection) | SM-23 (Model Drift Detection) | Same name, same resource, same detection logic (`MonitoringType=ModelQuality`). FS-18 adds PDF §1.2.14 low-entropy classification monitoring as an early-warning poisoning indicator. | **Extend SM-23** — add low-entropy monitoring as a new remediation step on SM-23; do not ship FS-18 separately |
-| FS-19 (Model Registry Approval) | SM-08 (Model Registry) / SM-22 (Model Approval Workflow) | SM-22 is conceptually identical. FS-19 specifies exact `ModelApprovalStatus=PendingManualApproval` default and flags auto-approved latest versions. | **Extend SM-22** — add FS-19's detection specificity (flag auto-approved latest versions) to SM-22; do not ship FS-19 separately |
-| FS-20 (Feature Store Rollback) | SM-15 (Feature Store Encryption) | Different security properties on the same resource: SM-15 checks encryption; FS-20 checks `OfflineStoreConfig` presence for point-in-time rollback. | **Keep separate** — different security property; no true overlap |
-| FS-39 (SageMaker Clarify Bias) | SM-06 (Clarify Usage) | Same resource family but SM-06 is Severity Low and generic ("validates Clarify for bias detection"); FS-39 is Severity High with specific `MonitoringType=ModelBias`, protected-attribute facets (age/gender/race/geography), and specific bias metrics (DPL, DI, DPPL) for FinServ decision models. | **Keep separate** — severity, detection specificity, and FinServ regulatory context (ECOA/Fair Housing) warrant a standalone check |
-| FS-41 (SageMaker Clarify Explainability) | SM-06 (Clarify Usage) | Same as FS-39 but for `MonitoringType=ModelExplainability`. FS-41 is Severity High with SHAP analysis for adverse-action-notice use cases. | **Keep separate** — severity and adverse-action-notice regulatory context justify a standalone check |
-| FS-22 (KB IAM Least Privilege) | BR-01 (IAM Least Privilege) | BR-01 detects the managed policy `AmazonBedrockFullAccess` on any role. FS-22 inspects role policy documents for wildcard `bedrock:*` affecting KB actions and requires ARN-scoped resource restrictions. | **Keep separate** — different detection logic (managed-policy attachment vs policy-document statement analysis); FS-22 fills a detection gap BR-01 does not cover |
-| FS-23 (KB CloudTrail Logging) | BR-06 (CloudTrail Logging) | BR-06 verifies CloudTrail is logging Bedrock API calls generally. FS-23 specifically requires an advanced event selector for `AWS::Bedrock::KnowledgeBase` to capture `Retrieve`/`RetrieveAndGenerate` data events (NOT logged by default). | **Extend BR-06** — add FS-23's data-event-selector requirement as a refinement of the same CloudTrail check |
-| FS-25 (OpenSearch Serverless Encryption) | BR-09 (Knowledge Base Encryption) | Different AWS resources: BR-09 checks the Bedrock KB's `kmsKeyArn`; FS-25 checks the underlying AOSS collection's encryption policy (`aoss:ListSecurityPolicies(type=encryption)`). A KB can be CMK-encrypted while its vector store is not. | **Keep separate** — different AWS resources with independent encryption configurations; both needed for defense-in-depth |
-| FS-26 (KB VPC Access) | BR-02 (VPC Endpoint Configuration) | BR-02 checks Bedrock VPC endpoints exist. FS-26 checks the AOSS collection's network policy for `AllowFromPublic=true` (whether the vector store itself is internet-reachable). | **Keep separate** — orthogonal controls: Bedrock VPC endpoint vs vector-store network policy |
-| FS-27 (Automated Reasoning / Contextual Grounding) | BR-05 (Guardrail Configuration) | BR-05 verifies a guardrail exists and is enforced. FS-27 checks for `automatedReasoningPolicy` or `contextualGroundingPolicy` with specific threshold (≥ 0.7). | **Keep separate** — policy-level guardrail content BR-05 does not evaluate |
-| FS-28 (Financial Denied Topics) | BR-05 | BR-05 is existence; FS-28 inspects `topicPolicy.topics` for FinServ-specific denied topics (investment advice, tax advice, guaranteed returns). | **Keep separate** — FinServ denied-topic content is a regulatory-specific requirement not representable as a generic extension |
-| FS-36 (Guardrail Content Filters) | BR-05 | FS-36 inspects `contentPolicy.filters` for HATE/VIOLENCE/SEXUAL/INSULTS/MISCONDUCT/PROMPT_ATTACK with strength ≥ MEDIUM. | **Keep separate** — policy-level detection BR-05 does not cover |
-| FS-38 (Word Filters and Allowlists) | BR-05 | FS-38 inspects `wordPolicy.words` and `managedWordLists` for FinServ business-term allowlist guidance. | **Keep separate** — advisory business-term allowlist has no upstream equivalent |
-| FS-45 (Guardrail PII Filters) | BR-05 | FS-45 inspects `sensitiveInformationPolicy.piiEntities` for 12 specific PII types critical to FinServ (SSN, bank account, SWIFT code, etc.) with `inputAction=BLOCK`/`outputAction=ANONYMIZE`. | **Keep separate** — FinServ-specific PII entity list is a distinct regulatory requirement |
-| FS-47 (Grounding Threshold) | BR-05 | FS-47 checks `contextualGroundingPolicy.filters` for `GROUNDING` filter with threshold ≥ 0.7. | **Keep separate** — threshold-value check BR-05 does not perform |
-| FS-50 (Relevance Grounding Filters) | BR-05 | Same as FS-47 but for `RELEVANCE` filter type. | **Keep separate** — distinct filter type |
-| FS-51 (Prompt Attack Filters) | BR-05 | FS-51 checks `PROMPT_ATTACK` filter in Standard tier with input-tagging requirement and `inputStrength=HIGH`. | **Keep separate** — Standard-tier cross-region-inference opt-in and input-tagging nuance warrant standalone guidance |
-| FS-59 (Guardrail Topic Allowlist) | BR-05 | FS-59 checks `topicPolicy.topics` exist to block off-topic conversations (politics, entertainment, medical advice). | **Keep separate** — off-topic content restrictions are distinct from FS-28's regulated-advice restrictions; different PDF section (§1.2.2 vs §1.2.1) |
-| FS-64 (Guardrail Trace Logging) | BR-04 (Model Invocation Logging) | BR-04 verifies invocation logging is enabled. FS-64 additionally verifies the log output captures `guardrailTrace` with `action`/`inputAssessments`/`outputAssessments` and adds NYDFS/SR 11-7 retention guidance. | **Extend BR-04** — add guardrail-trace verification as a refinement of the same invocation-logging check; retention guidance can be a remediation note |
-
-### Summary of consolidation recommendations
-
-- **Extend upstream (5 FS checks merged into 5 upstream checks):** FS-17 → SM-07; FS-18 → SM-23; FS-19 → SM-22; FS-23 → BR-06; FS-64 → BR-04. These checks are replaced by upstream-extension notes in Parts 1 and 3 and are removed from `finserv_assessments/app.py`.
-- **Keep separate (64 FS checks):** All other FS checks ship as standalone entries. This includes FS-20, FS-22, FS-25, FS-26, FS-39, FS-41, all Guardrail-policy-level checks (FS-27, FS-28, FS-36, FS-38, FS-45, FS-47, FS-50, FS-51, FS-59), and all FS checks that have no upstream overlap at all.
-
-After consolidation the combined framework contains **52 upstream + 64 FS = 116 distinct checks** (down from 52 + 69 = 121 before merging). The consolidation reduces duplication without losing FinServ-specific regulatory depth.
-
-
----
-
-## Compliance Framework Mapping
-
-> **Disclaimer:** The mappings below are **preliminary and illustrative**, provided by the
-> authors of this assessment to help FSI teams start conversations with their MRM/compliance
-> colleagues. They are **not** authoritative AWS compliance guidance and they have **not** been
-> reviewed by AWS Security Assurance Services, external auditors, or the regulators whose
-> frameworks are named. Each firm should have its own MRM, Legal, and Compliance teams
-> validate these mappings against the firm's specific interpretation of each framework before
-> relying on them as audit evidence.
-
-Each FS check maps to one or more FinServ regulatory frameworks (preliminary mapping):
-
-| Framework | Description | Relevant Checks |
-|-----------|-------------|-----------------|
-| SR 11-7 | Federal Reserve Model Risk Management Guidance | FS-07, FS-10, FS-12 to FS-16, FS-20, FS-21, FS-27 to FS-33, FS-34, FS-39 to FS-42, FS-66, FS-67 |
-| FFIEC CAT | Cybersecurity Assessment Tool | All FS checks |
-| NYDFS 500 | NY Cybersecurity Regulation | FS-22, FS-43 to FS-46, FS-51 to FS-54, FS-66 |
-| PCI-DSS | Payment Card Industry Data Security Standard | FS-22, FS-25, FS-26, FS-43 to FS-46, FS-53, FS-56, FS-67, FS-68 |
-| DORA | EU Digital Operational Resilience Act | FS-01 to FS-06, FS-08, FS-11, FS-54, FS-65, FS-68 |
-| MAS TRM 9 | Monetary Authority of Singapore Technology Risk Management | FS-07 to FS-11, FS-15, FS-27 to FS-30, FS-32, FS-37, FS-39 to FS-42, FS-66, FS-67 |
-| ISO 27001 | Information Security Management | FS-13, FS-14, FS-16, FS-21, FS-33, FS-46, FS-52, FS-63, FS-65 |
-| ECOA/Fair Housing | Equal Credit Opportunity Act (US) | FS-39 to FS-42 (advisory — applicability depends on whether the model is used for ECOA-covered credit decisions; confirm with your compliance team) |
-| OWASP LLM Top 10 | OWASP LLM Application Security | FS-51 to FS-58, FS-68, FS-69 |
-
-> **FS-34 note:** FS-34 (TPRM for FM Providers) is listed above under SR 11-7. Although the
-> check appears in the Misinformation section of Part 2 for numbering continuity, its
-> primary PDF source is §1.2.12 Supply Chain, which is the lens MRM and TPRM teams will
-> evaluate it through.
diff --git a/docs/SECURITY_CHECKS_FINSERV_PART1_INFRA_CONTROLS.md b/docs/SECURITY_CHECKS_FINSERV_PART1_INFRA_CONTROLS.md
deleted file mode 100644
index 9ff362e..0000000
--- a/docs/SECURITY_CHECKS_FINSERV_PART1_INFRA_CONTROLS.md
+++ /dev/null
@@ -1,401 +0,0 @@
-# FinServ GenAI Risk Checks — Part 1: Infrastructure & Resource Controls (FS-01 to FS-26)
-
-This is **Part 1 of 3** of the FinServ GenAI security checks derived from the
-[AWS guide for Financial Services risk management of the use of Generative AI (March 2026)](https://d1.awsstatic.com/onedam/marketing-channels/website/public/global-FinServ-ComplianceGuide-GenAIRisks-public.pdf)
-(referred to throughout as "the FinServ Guide").
-
-This part covers **22 standalone checks** across 5 PDF risk categories (FS-17, FS-18, FS-19, and FS-23 are merged into upstream checks — see extension notes in each section):
-
-- **Unbounded Consumption** (FS-01 to FS-06) — §1.2.11
-- **Excessive Agency** (FS-07 to FS-11) — §1.2.9
-- **Supply Chain Vulnerabilities** (FS-12 to FS-16) — §1.2.12
-- **Training Data & Model Poisoning** (FS-17 to FS-21) — §1.2.14 — *FS-17, FS-18, FS-19 merged into upstream*
-- **Vector & Embedding Weaknesses** (FS-22 to FS-26) — §1.2.15 — *FS-23 merged into upstream*
-
-**Companion files:**
-
-- `SECURITY_CHECKS_FINSERV_PART2_GUARDRAILS_CONTENT_SAFETY.md` — FS-27 to FS-46 (Non-Compliant, Misinformation, Abusive, Biased, Sensitive Info)
-- `SECURITY_CHECKS_FINSERV_PART3_APP_LAYER_AND_GAPS.md` — FS-47 to FS-69 (Hallucination, Prompt Injection, Output Handling, Off-Topic, Stale Data, Material Gaps)
-- `SECURITY_CHECKS_FINSERV_COMMON.md` — shared intro, severity rubric, validation note, upstream-overlap table
-
-Each check includes how it is **detected** (the AWS API calls or configuration inspected)
-and how a failure is **remediated** (the specific AWS actions to take).
-
-See `SECURITY_CHECKS_FINSERV_COMMON.md` for:
-
-- PDF traceability conventions (`[PDF §x.y.z]` vs `[PDF §x.y.z, extension]`)
-- Severity rubric (High / Medium / Low / Informational) — see SECURITY_CHECKS_FINSERV_SEVERITY_METHODOLOGY.md
-- Validation note and AWS service authorization references
-- Relationship to upstream SM/BR/AC checks and consolidation recommendations
-
----
-
-## FinServ GenAI Risk Checks — Part 1 content
-
-### Unbounded Consumption (FS-01 to FS-06)
-
-> **PDF source:** §1.2.11 Unbounded consumption. PDF-listed mitigations: (a) AWS WAF and Shield
-> Advanced for LLM APIs; (b) maximum input length limits; (c) rate limits/quotas on APIs
-> accessing LLMs; (d) cost-and-usage tracking for generative AI. Practical guidance in the PDF
-> also calls out `max_tokens` optimisation and CloudWatch metrics for token usage.
-
-#### FS-01 — WAF and Shield Protection
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium (WAF) / Low (Shield Advanced) |
-| PDF ref | [PDF §1.2.11] — "Protect your LLM APIs and Amazon Bedrock-hosted LLMs by using AWS WAF and AWS Shield Advanced." Also covers: "To protect your API endpoints, set maximum length limits for input requests when you use large language models (LLMs) directly or through Amazon Bedrock." |
-| Description | Verifies AWS WAF Web ACLs and Shield Advanced protect GenAI API endpoints, and verifies the Web ACL enforces both rate-based limits and body-size (input-length) constraints. |
-| Detection | Calls `shield:DescribeSubscription` to check Shield Advanced is active. Calls `wafv2:ListWebACLs(Scope=REGIONAL)` in each region where GenAI API endpoints run to verify at least one regional Web ACL exists (covers API Gateway, ALB, AppSync). **Additionally** calls `wafv2:ListWebACLs(Scope=CLOUDFRONT)` in `us-east-1` to detect Web ACLs protecting CloudFront distributions fronting GenAI workloads — CLOUDFRONT-scope Web ACLs must be created and queried in `us-east-1` per the [WAF resources documentation](https://docs.aws.amazon.com/waf/latest/developerguide/how-aws-waf-works-resources.html). For each Web ACL found, calls `wafv2:GetWebACL` and inspects the `Rules` array for: (a) at least one `RateBasedStatement` (rate limiting) and (b) at least one `SizeConstraintStatement` with `FieldToMatch=Body` or `FieldToMatch=JsonBody` (input-size limit — this implements PDF §1.2.11 mitigation "set maximum length limits for input requests when you use large language models (LLMs) directly or through Amazon Bedrock"). Flags accounts with no Web ACL in either scope, a Web ACL with no rate-based rule, a Web ACL with no body size-constraint rule, or where Shield Advanced is inactive. |
-| Remediation | 1. Subscribe to AWS Shield Advanced via the Shield console. 2. Create a WAF Web ACL with both (a) a rate-based rule (e.g., 1 000 req / 5 min per IP) and (b) a `SizeConstraintStatement` that blocks requests where `FieldToMatch=Body` (or `JsonBody` for JSON APIs) exceeds your LLM's expected maximum input size — for example, `ComparisonOperator=GT, Size=100000` (100 KB) — use `Scope=REGIONAL` for API Gateway/ALB/AppSync resources, or `Scope=CLOUDFRONT` (created in `us-east-1`) for CloudFront distributions fronting Bedrock. The body size-constraint rule directly implements the PDF §1.2.11 mitigation "set maximum length limits for input requests when you use large language models (LLMs) directly or through Amazon Bedrock" and prevents large-prompt token-exhaustion attacks before they reach Bedrock. 3. Associate the ACL with the fronting resource (API Gateway stage, ALB, or CloudFront distribution). 4. Add AWS Managed Rules (e.g., `AWSManagedRulesCommonRuleSet`, which includes additional size checks). 5. For CloudFront-fronted workloads, register the distribution with Shield Advanced via `shield:CreateProtection` to unlock automatic application-layer DDoS mitigation. 6. For API Gateway REST APIs, also note the service's own payload-size quota: the default is 10 MB per request (see [API Gateway quotas](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-execution-service-limits-table.html)); use a request validator or Lambda authorizer for sub-10 MB limits where WAF size constraints are unsuitable. |
-| Reference | [Shield Advanced](https://docs.aws.amazon.com/waf/latest/developerguide/shield-chapter.html), [WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html), [WAF Size Constraint Rule](https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-type-size-constraint-match.html), [API Gateway Quotas](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-execution-service-limits-table.html) |
-
-#### FS-02 — API Gateway Rate Limiting
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.11] — "protect your API endpoints by implementing rate limits and quotas for APIs that access large language models (LLMs)". |
-| Description | Checks API Gateway usage plans enforce throttling on GenAI endpoints. |
-| Detection | Calls `apigateway:GetUsagePlans` and inspects each plan's `throttle.rateLimit` and `throttle.burstLimit`. Flags plans where either is zero or absent. |
-| Remediation | 1. Create or update usage plans with `rateLimit` and `burstLimit` values appropriate for your traffic. 2. Associate plans with API stages serving Bedrock. 3. Issue per-consumer API keys with individual quotas. |
-| Reference | [API Gateway Throttling](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html) |
-
-#### FS-03 — Bedrock Token Quota Review
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.11, extension] — PDF practical guidance notes "Bedrock has default quota on model inference based on token usage" and recommends optimising `max_tokens`. Quota review as an operational control is an extension aligned with this guidance. |
-| Description | Verifies Bedrock TPM/RPM quotas have been reviewed and set appropriately. |
-| Detection | Calls `service-quotas:ListServiceQuotas(ServiceCode=bedrock)` for applied quotas and `ListAWSDefaultServiceQuotas` for defaults, then compares each adjustable quota's `Value` against the default `Value`. Flags accounts where every quota equals the service default (indicating no quota review or increase has been requested). |
-| Remediation | 1. Review current quotas in the Service Quotas console. 2. Request increases aligned with expected peak load via `service-quotas:RequestServiceQuotaIncrease`. 3. Implement client-side token counting and pre-flight quota checks. 4. Use Bedrock cross-region inference profiles to distribute load — note that cross-region inference routes requests across destination regions automatically with no additional cost, but requires the invoked model to be available in the destination regions defined in the inference profile. |
-| Reference | [Bedrock Quotas](https://docs.aws.amazon.com/bedrock/latest/userguide/quotas.html) |
-
-#### FS-04 — Cost Anomaly Detection
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.11] — "Track, allocate, and manage your costs and usage for generative AI." |
-| Description | Checks AWS Cost Anomaly Detection monitors cover Bedrock/SageMaker. |
-| Detection | Calls `ce:GetAnomalyMonitors` and inspects each monitor. AWS Cost Anomaly Detection supports exactly two `MonitorType` values per the [AnomalyMonitor API](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_AnomalyMonitor.html): `DIMENSIONAL` (AWS-managed, where `MonitorDimension` is one of `SERVICE`, `LINKED_ACCOUNT`, `TAG`, or `COST_CATEGORY`) and `CUSTOM` (customer-managed, scoped via `MonitorSpecification` to specific values). For `DIMENSIONAL` monitors, checks `MonitorDimension=SERVICE` (the AWS-managed "AWS services" monitor that automatically covers all services including Bedrock and SageMaker — the recommended default). For `CUSTOM` monitors, inspects `MonitorSpecification` for references to Bedrock or SageMaker. Flags accounts with no monitors, or with only narrowly-scoped monitors that would not detect Bedrock cost anomalies (e.g., `DIMENSIONAL` with `MonitorDimension=LINKED_ACCOUNT` only). |
-| Remediation | 1. Create an AWS-managed `DIMENSIONAL` monitor with `MonitorDimension=SERVICE` for comprehensive coverage across all AWS services (the recommended default — in the console this appears as "AWS services" under "Managed by AWS"). For narrower scope, add a `CUSTOM` monitor using `MonitorSpecification` with a `Dimensions` expression scoped to specific service values (e.g., `{"Dimensions": {"Key": "SERVICE", "Values": ["Amazon Bedrock", "Amazon SageMaker"]}}`) — note that for `CUSTOM` monitors you use `MonitorSpecification`, not `MonitorDimension`. 2. Configure alert subscriptions (SNS/email) for anomalies above threshold. 3. Set daily spend budgets with AWS Budgets as a secondary control. 4. Enable Bedrock IAM principal cost allocation: tag IAM users/roles with team or cost-center attributes, activate them as cost allocation tags in the Billing and Cost Management console, and include caller identity data in CUR 2.0 exports for per-user/per-team Bedrock spend attribution. |
-| Reference | [Cost Anomaly Detection](https://docs.aws.amazon.com/cost-management/latest/userguide/getting-started-ad.html), [Bedrock IAM Cost Allocation](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/iam-principal-cost-allocation.html) |
-
-#### FS-05 — CloudWatch Token Usage Alarms
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.11] — PDF practical guidance cites CloudWatch metrics for token usage; alarms operationalise that guidance. |
-| Description | Verifies CloudWatch alarms exist for Bedrock throttling and token metrics. |
-| Detection | Paginates `cloudwatch:DescribeAlarms(AlarmTypes=MetricAlarm)` and filters for alarms in the `AWS/Bedrock` namespace or with "bedrock" in the alarm name. Separately counts throttle-specific alarms. |
-| Remediation | 1. Create alarms for `AWS/Bedrock InvocationThrottles` (threshold > 0). 2. Create alarms for `AWS/Bedrock EstimatedTPMQuotaUsage` to track approach to token quota limits, and separately on `InputTokenCount` + `OutputTokenCount` (sum via CloudWatch metric math) for absolute token consumption. Note: `TokensProcessed` is not a valid Bedrock metric — the correct runtime metrics are `InputTokenCount`, `OutputTokenCount`, `InvocationThrottles`, `EstimatedTPMQuotaUsage`, `Invocations`, `InvocationLatency`, `TimeToFirstToken`. 3. Publish custom application-level token counters via Embedded Metric Format (EMF) if you need per-tenant or per-feature attribution. 4. Attach SNS actions to all alarms. |
-| Reference | [Bedrock CloudWatch Metrics](https://docs.aws.amazon.com/bedrock/latest/userguide/monitoring.html) |
-
-#### FS-06 — AWS Budgets AI/ML Spend
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.11] — "Track, allocate, and manage your costs and usage for generative AI." |
-| Description | Checks AWS Budgets are configured with alerts for AI/ML service spend. |
-| Detection | Calls `budgets:DescribeBudgets` and inspects each budget's `FilterExpression` (the current field) and `CostFilters` (deprecated but may still be populated on older budgets) for references to "bedrock" or "sagemaker". Note: `CostFilters` is marked deprecated in the AWS Budgets API — new budgets use `FilterExpression` with an `Expression` object; the detection should check both fields to cover both old and new budgets. |
-| Remediation | 1. Create cost budgets for Bedrock and SageMaker with 80 %/100 % alert thresholds. 2. Add SNS notifications to on-call channels. 3. Consider budget actions to apply IAM deny policies when thresholds are breached. 4. Enable Bedrock IAM principal cost allocation to attribute inference costs to specific IAM users/roles via Cost Explorer and CUR 2.0 — tag IAM principals with team or cost-center attributes and activate them as cost allocation tags. |
-| Reference | [AWS Budgets](https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-managing-costs.html), [Bedrock IAM Cost Allocation](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/iam-principal-cost-allocation.html) |
-
-### Excessive Agency (FS-07 to FS-11)
-
-> **PDF source:** §1.2.9 Excessive agency. PDF-listed mitigations: (a) Amazon Bedrock AgentCore
-> for managing complex tasks; (b) least-privilege permissions on plugins; (c) human-in-the-loop
-> output validation; (d) explicit action boundaries in agent configuration (AgentCore Policy);
-> (e) audit logging of agent actions with reasoning chain (AgentCore Observability);
-> (f) transaction-value thresholds on agent tool calls; (g) monitoring agent call rates with
-> alarms (AgentCore Evaluations). Mitigation (e) is covered by the expanded FS-08 check, which
-> now verifies both AgentCore Policy Engine and AgentCore Observability are configured.
-
-#### FS-07 — Agent Action Boundaries
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.9] — "grant only the minimum permissions required"; "Define and enforce explicit action boundaries in the agent configuration". |
-| Description | Verifies Bedrock agent execution roles have no wildcard sensitive actions (iam:\*, s3:\*, ec2:\*, lambda:\*, \*). |
-| Detection | Calls `ListAgents` and `GetAgent` (via the `bedrock-agent` boto3 client; IAM actions are `bedrock:ListAgents` and `bedrock:GetAgent`) to retrieve each agent's `agentResourceRoleArn`. Resolves the role name and inspects attached and inline policy documents from the permissions cache for wildcard Allow statements. |
-| Remediation | 1. Replace wildcard actions with the specific actions the agent needs. 2. Apply IAM permission boundaries to agent execution roles. 3. Use resource-level conditions to restrict to specific ARNs. 4. Implement human-in-the-loop approval for high-impact actions. 5. For agents deployed in a VPC, use **AWS Network Firewall** with domain-based filtering to control which external domains agents can reach — this provides a network-layer boundary that limits agent tool access to approved endpoints regardless of IAM permissions. |
-| Reference | [Bedrock Agent Permissions](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-permissions.html), [Control Agent Domain Access](https://aws.amazon.com/blogs/machine-learning/control-which-domains-your-ai-agents-can-access/) |
-
-#### FS-08 — AgentCore Policy Engine and Observability
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.9] — "Use Amazon Bedrock AgentCore to manage complex tasks and connect securely"; "Define and enforce explicit action boundaries"; **"Implement audit logging of all actions taken by AI agents, including the reasoning chain that led to each action."** (The audit-logging mitigation's PDF reference is "Observe your agent applications on Amazon Bedrock AgentCore Observability.") |
-| Description | Checks AgentCore Gateways have a Policy Engine attached to authorize agent-to-tool interactions, verifies AgentCore Runtimes have an inbound authorizer configured, and verifies AgentCore Observability is enabled so agent reasoning chains and tool calls are auditable. |
-| Detection | (a) Calls `ListGateways` and `GetGateway` (via the `bedrock-agentcore-control` boto3 client; IAM actions are `bedrock-agentcore:ListGateways` and `bedrock-agentcore:GetGateway`); inspects `policyEngineConfiguration.arn` and `policyEngineConfiguration.mode` (must be `ENFORCE` for production). (b) Calls `ListAgentRuntimes` (IAM action `bedrock-agentcore:ListAgentRuntimes`) and inspects each runtime's `authorizerConfiguration.customJWTAuthorizer` for inbound auth. (c) Verifies **AgentCore Observability** is enabled by (i) checking that CloudWatch Transaction Search is on via `xray:GetTraceSegmentDestination` (destination should be `CloudWatchLogs`) and that the X-Ray → CloudWatch Logs resource policy is in place via `logs:GetResourcePolicy`, and (ii) calling `logs:DescribeDeliveries` / `logs:DescribeDeliverySources` for AgentCore resource sources (runtime, memory, gateway, built-in tools, identity) — flags runtimes/gateways with no log delivery configured. For memory resources, additionally checks that tracing was enabled at memory creation time. Flags gateways without a Policy Engine in `ENFORCE` mode, runtimes without an authorizer, or accounts where Transaction Search is not enabled or no delivery exists for AgentCore resources. |
-| Remediation | 1. Configure a Policy Engine: create via `CreatePolicyEngine` (IAM action `bedrock-agentcore:CreatePolicyEngine`), then author Cedar policies using one of three methods: (a) write Cedar directly for fine-grained control via `CreatePolicy` (IAM action `bedrock-agentcore:CreatePolicy`), (b) use the form-based console UI, or (c) generate Cedar from natural language descriptions (natural-language-to-Cedar is a documented capability in the GA announcement; verify the exact IAM action name against the current [AgentCore Service Authorization Reference](https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonbedrockagentcore.html) before writing IAM policies for it). Policy in AgentCore went GA on March 3, 2026 in thirteen AWS regions (US East N. Virginia, US East Ohio, US West Oregon, Asia Pacific Mumbai/Seoul/Singapore/Sydney/Tokyo, Europe Frankfurt/Ireland/London/Paris/Stockholm) — verify current regional availability on the [launch announcement](https://aws.amazon.com/about-aws/whats-new/2026/03/policy-amazon-bedrock-agentcore-generally-available/) before audit reliance. 2. Attach the Policy Engine to each Gateway by specifying the Policy Engine ARN in the `policyEngineConfiguration` field during `CreateGateway`, or attach later via `UpdateGateway`. 3. Start in `LOG_ONLY` mode — the policy engine evaluates actions and logs whether they would be allowed or denied without enforcing the decision — then switch to `ENFORCE` mode once confident. 4. Configure a JWT inbound authorizer on each Runtime with discovery URL, allowed audiences, and allowed clients. 5. **Enable AgentCore Observability** so agent reasoning chains are captured (directly addresses the PDF §1.2.9 audit-logging mitigation): (a) one-time enable CloudWatch Transaction Search — console path **CloudWatch → Application Signals (APM) → Transaction search → Enable Transaction Search**, or CLI: `aws xray update-trace-segment-destination --destination CloudWatchLogs` plus a `logs:PutResourcePolicy` granting `xray.amazonaws.com` permission to `logs:PutLogEvents` on `aws/spans:*` and `/aws/application-signals/data:*`; (b) configure log delivery for AgentCore runtime, memory, gateway, built-in tools, and identity resources via `logs:PutDeliverySource` + `logs:PutDeliveryDestination` + `logs:CreateDelivery` (CloudWatch Logs / S3 / Firehose destinations supported; note the write APIs use `Put*` for source and destination but `Create*` for the delivery pairing); (c) enable tracing at memory creation. For traditional Bedrock Agents (non-AgentCore), set `enableTrace=true` on `InvokeAgent` calls to receive the reasoning-chain trace in the response. |
-| Reference | [Policy in AgentCore](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy.html), [Inbound JWT Authorizer](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/inbound-jwt-authorizer.html), [AgentCore Observability Configuration](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability-configure.html), [Bedrock Agent Trace View](https://docs.aws.amazon.com/bedrock/latest/userguide/trace-view.html) |
-
-#### FS-09 — Agent Transaction Limits
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.9, extension] — Lambda reserved concurrency is not named in the PDF, but it directly implements the PDF mitigation "Monitor agent call rates and alarm upon exceeding defined thresholds" by capping execution parallelism. |
-| Description | Verifies agent Lambda functions have reserved concurrency limits to cap execution parallelism. |
-| Detection | Calls `lambda:ListFunctions` and filters for functions with agent-related naming patterns. For each, calls `lambda:GetFunctionConcurrency` and flags functions with no reserved concurrency set. |
-| Remediation | 1. Set reserved concurrency on each agent action-group Lambda (e.g., 10–50 depending on expected load). 2. Add CloudWatch alarms for `Throttles` metric on these functions. 3. Consider Step Functions execution limits as an additional control. |
-| Reference | [Lambda Reserved Concurrency](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html) |
-
-#### FS-10 — Human-in-the-Loop Approval
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.9, §1.2.1, §1.2.2, §1.2.3, §1.2.7, §1.2.10] — "For internal AI systems, validate outputs with human review before business use (human-in-the-loop)." HITL is referenced in six separate PDF risk sections. |
-| Description | Checks Step Functions workflows have human approval steps for high-risk agent actions. |
-| Detection | Calls `stepfunctions:ListStateMachines` and filters for agent/GenAI-related names. Retrieves each definition via `stepfunctions:DescribeStateMachine` and parses the ASL JSON for task states with `.waitForTaskToken` or callback patterns indicating human approval gates. |
-| Remediation | 1. Add a callback-pattern task state in your Step Functions workflow before any high-risk action (financial transactions, data modifications, external communications). 2. Route the approval token to a human reviewer via SNS/SQS/Slack. 3. Set a `HeartbeatSeconds` timeout so stale approvals expire. 4. Enable **user confirmation on Bedrock Agent action groups** for inline approval — when configured, the agent returns a confirmation prompt in the `returnControl.invocationInputs` field of the `InvokeAgent` response (alongside `invocationType` and a unique `invocationId`); the client displays the prompt, collects confirm/deny, and returns the user's decision via `sessionState.returnControlInvocationResults` (with `confirmationState` on each `apiResult`/`functionResult`) in the next `InvokeAgent` request (there is no standalone `GetUserConfirmation` API). |
-| Reference | [Step Functions Callback Pattern](https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#connect-wait-token), [Bedrock Agent User Confirmation](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-userconfirmation.html) |
-
-#### FS-11 — Agent Rate Alarms
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.9] — "Monitor agent call rates and alarm upon exceeding defined thresholds." |
-| Description | Verifies CloudWatch alarms exist for agent invocation rates. |
-| Detection | Paginates `cloudwatch:DescribeAlarms` and filters for alarms referencing "agent" in the alarm name or targeting `AWS/Bedrock/Agents` agent-related metrics (such as `InvocationCount` or `InvocationThrottles` with the `Operation, AgentAliasArn, ModelId` dimension combination). |
-| Remediation | 1. Create CloudWatch alarms on the `AWS/Bedrock/Agents` namespace for `InvocationCount` and `InvocationThrottles`. Per AWS docs, the available dimensions are: `Operation` alone; `Operation, ModelId`; or `Operation, AgentAliasArn, ModelId` — use the `Operation, AgentAliasArn, ModelId` combination to scope alarms to a specific agent alias. 2. Set thresholds based on expected peak agent call rates, established via CloudWatch metric math on historical `InvocationCount` data. 3. Attach SNS actions for on-call notification. 4. Use **AgentCore Evaluations** (GA March 2026, available in 9 AWS regions — verify current regional availability on the [GA announcement](https://aws.amazon.com/about-aws/whats-new/2026/03/agentcore-evaluations-generally-available/)) to monitor agent *quality* alongside rate-based alarms: online evaluation continuously scores production traffic against 13 built-in evaluators (response quality, safety, task completion, tool usage), and on-demand evaluation supports regression testing. |
-| Reference | [Bedrock Agents CloudWatch Metrics](https://docs.aws.amazon.com/bedrock/latest/userguide/monitoring-agents-cw-metrics.html), [AgentCore Evaluation Types](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/evaluations-types.html) |
-
-### Supply Chain Vulnerabilities (FS-12 to FS-16)
-
-> **PDF source:** §1.2.12 Supply chain vulnerabilities. PDF-listed mitigations:
-> (a) control access to serverless and marketplace models (IAM policies, SCPs);
-> (b) model onboarding process — EULA review, procurement, security/compliance review,
-> MRM assessment, documentation, stakeholder approvals;
-> (c) update TPRM to continuously monitor model providers — vendor security advisories,
-> deprecation notices, T&C changes;
-> (d) maintain a model inventory recording provenance, version, license terms, and risk
-> assessment status;
-> (e) use Bedrock Evaluations against attack test cases (practical guidance);
-> (f) allow-list approved models via SCP (practical guidance).
-
-#### FS-12 — SCP Model Access Restrictions
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.12 — Practical guidance] — "Implement an allow-list of models using a Service Control Policy (SCP) for your AWS organization." |
-| Description | Checks SCPs restrict Bedrock model access to approved models only. |
-| Detection | Calls `organizations:ListPolicies(Filter=SERVICE_CONTROL_POLICY)` and inspects each SCP document for Deny statements on `bedrock:InvokeModel*` with `StringNotEquals` conditions on `bedrock:ModelId`. Flags if no SCP restricts model access. |
-| Remediation | 1. Create an SCP that denies `bedrock:InvokeModel*` except for an explicit allowlist of approved model ARNs. 2. Attach the SCP to the OU containing GenAI workload accounts. 3. For multi-account guardrail enforcement, use the Bedrock cross-account safeguards feature (GA April 3, 2026, available in all AWS commercial and GovCloud regions where Bedrock Guardrails is supported): enable the Amazon Bedrock policy type in AWS Organizations, create a guardrail in the management account, create a versioned guardrail, optionally attach a resource-based policy granting `bedrock:ApplyGuardrail` to member accounts for cross-account access, then create and attach an AWS Organizations Bedrock policy referencing the guardrail ARN and version to the target OUs or accounts. This automatically enforces content filters, denied topics, word filters, sensitive information filters, and contextual grounding checks across all member accounts for every model invocation — no application code changes required. **Important limitation:** Automated Reasoning checks are **not supported** with cross-account safeguards — omit Automated Reasoning policies from any guardrail used for org-level enforcement. If you rely on AR (see FS-27), you must configure AR guardrails separately at the application or account level. 4. Test with both allowed and denied model IDs. |
-| Reference | [Managing Access in AWS Organizations](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps.html), [Bedrock Cross-Account Guardrails](https://aws.amazon.com/blogs/aws/amazon-bedrock-guardrails-supports-cross-account-safeguards-with-centralized-control-and-management/) |
-
-#### FS-13 — Model Inventory Tagging
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.12] — "Maintain a model inventory that records the provenance, version, license terms, and risk assessment status of all models in use across the organization." |
-| Description | Verifies models are tagged with provenance metadata (source, version, approval-date). |
-| Detection | Calls `bedrock:ListFoundationModels` and `bedrock:ListCustomModels`. For custom models, calls `bedrock:ListTagsForResource` and checks for required tag keys: `model-source`, `model-version`, `approval-date`, `risk-tier`. |
-| Remediation | 1. Define a mandatory tagging policy for all AI/ML models. 2. Tag each custom model with provenance metadata. 3. Create an AWS Config rule (`required-tags`) to enforce the tagging policy. 4. For foundation models, maintain an external inventory spreadsheet or CMDB entry. |
-| Reference | [Bedrock Tagging](https://docs.aws.amazon.com/bedrock/latest/userguide/tagging.html) |
-
-#### FS-14 — Model Onboarding Governance
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.12] — "To onboard a model, follow these steps: Review EULA, Complete procurement, Follow security and compliance procedures, Assess MRM requirements, Document findings, Get necessary approvals from stakeholders." |
-| Description | Checks AWS Config rules enforce model onboarding governance (EULA review, MRM assessment, stakeholder approval). |
-| Detection | Calls `config:DescribeConfigRules` and searches for rules targeting `AWS::Bedrock::*` resources or custom rules with "model" or "onboarding" in the name. |
-| Remediation | 1. Create a custom AWS Config rule that checks new Bedrock custom models have required tags (approval-date, risk-tier, eula-reviewed). 2. Document the model onboarding process: EULA review → procurement → security/compliance review → MRM assessment → stakeholder sign-off. 3. Store approval artifacts in a versioned S3 bucket. |
-| Reference | [AWS Config Custom Rules](https://docs.aws.amazon.com/config/latest/developerguide/evaluate-config_develop-rules.html) |
-
-#### FS-15 — Adversarial Model Evaluation
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.12 — Practical guidance] — "Amazon Bedrock Evaluations can help to evaluate models against specific types of attacks by automating your test cases, scoring, reporting and to enable comparison of different models." |
-| Description | Verifies Bedrock evaluation jobs include adversarial test datasets. |
-| Detection | Calls `bedrock:ListEvaluationJobs` and inspects each job's configuration for evaluation datasets. Flags if no evaluation jobs exist or if none reference adversarial/red-team test data. |
-| Remediation | 1. Create a Bedrock model evaluation job with adversarial prompt datasets (prompt injection attempts, jailbreak sequences, harmful content probes). 2. Include both automated metrics and human evaluation. 3. Run evaluations before production deployment and after model updates. 4. Store results for audit. |
-| Reference | [Bedrock Model Evaluation](https://docs.aws.amazon.com/bedrock/latest/userguide/evaluation.html) |
-
-#### FS-16 — ECR Image Scanning
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.12, extension] — ECR image scanning is not named in the PDF, but directly mitigates the PDF's listed risk "Third-party package vulnerabilities" in LLM supply chains. Included for completeness of the supply-chain risk category. |
-| Description | Checks ECR repositories have scan-on-push enabled for supply chain security of model containers. |
-| Detection | Calls `ecr:DescribeRepositories` and for each repository checks `imageScanningConfiguration.scanOnPush`. Also checks whether Amazon Inspector ECR scanning is enabled via `inspector2:BatchGetAccountStatus`. Flags repositories relying solely on basic scanning or with no scanning configured. |
-| Remediation | 1. Enable **enhanced scanning** via Amazon Inspector (the current best practice) — Inspector provides continuous vulnerability monitoring, re-scanning images automatically when new CVEs are published, and covers both OS and programming language package vulnerabilities. This requires two steps: (a) enable Inspector ECR scanning at the account level — `aws inspector2 enable --account-ids --resource-types ECR`; (b) set the ECR registry scanning configuration to enhanced mode — `aws ecr put-registry-scanning-configuration --scan-type ENHANCED --rules '[{"scanFrequency":"CONTINUOUS_SCAN","repositoryFilters":[{"filter":"*","filterType":"WILDCARD"}]}]'`. **Important limitations:** (i) When enhanced scanning is first enabled, Amazon Inspector only discovers images pushed within the **last 14 days** — older images receive `SCAN_ELIGIBILITY_EXPIRED` status and must be re-pushed to be scanned. (ii) After the initial scan, scan duration is controlled by the ECR re-scan duration setting in the Amazon Inspector console (defaults to `LIFETIME`); if you shorten this duration, images whose last scan exceeds the new window also move to `SCAN_ELIGIBILITY_EXPIRED`. (iii) Enhanced scanning incurs Amazon Inspector charges (no additional ECR cost). (iv) Repositories not matching a scan filter will have `Off` scan frequency and won't be scanned. 2. If enhanced scanning is not available in your region, enable basic scan-on-push as a fallback: `aws ecr put-image-scanning-configuration --repository-name --image-scanning-configuration scanOnPush=true`. 3. Create EventBridge rules to alert on CRITICAL/HIGH findings from Inspector. 4. Integrate findings into your vulnerability management workflow. |
-| Reference | [ECR Enhanced Scanning](https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning-enhanced.html), [Amazon Inspector ECR Scanning](https://docs.aws.amazon.com/inspector/latest/user/scanning-ecr.html) |
-
-### Training Data & Model Poisoning (FS-17 to FS-21)
-
-> **PDF source:** §1.2.14 Training data and model poisoning. PDF-listed mitigations:
-> (a) protect training datasets via data protection best practices;
-> (b) use trusted data sources with audit controls tracking changes (who/when);
-> (c) monitor training data for pattern/distribution changes (data drift);
-> (d) compare retrained model performance against baseline before production;
-> (e) rollback plan using versioned training data and models (Feature Store);
-> (f) monitor low-entropy classification with thresholds and alerts;
-> (g) AI Service Cards for evaluating third-party model testing procedures.
-
-#### FS-17 — Model Monitor Data Quality → *Merged into upstream SM-07*
-
-> **Upstream extension note (do not ship as a standalone check):** The detection and remediation
-> content from FS-17 should be added as a refinement of the existing **SM-07 (Model Monitor)**
-> check in the upstream `aws-samples/sample-aiml-security-assessment` repo.
->
-> **What to add to SM-07:**
->
-> - Filter `ListMonitoringSchedules` results for `MonitoringType=DataQuality` (not just any schedule). Note the format difference: `ListMonitoringSchedules`/`MonitoringScheduleSummary` returns `MonitoringType` in PascalCase (`DataQuality`, `ModelQuality`, `ModelBias`, `ModelExplainability`); `DescribeMonitoringSchedule` returns the same type in SCREAMING_SNAKE_CASE (`DATA_QUALITY`, `MODEL_QUALITY`, `MODEL_BIAS`, `MODEL_EXPLAINABILITY`) — the detection should normalise both forms.
-> - Require `emit_metrics` to be enabled on the monitoring schedule.
-> - Verify CloudWatch alarms exist on the `feature_baseline_drift_` metrics published
-> to namespace `/aws/sagemaker/Endpoints/data-metric` (real-time endpoints, dimensions
-> `EndpointName` + `ScheduleName`) or `/aws/sagemaker/ModelMonitoring/data-metric` (batch
-> transform, dimension `MonitoringSchedule`).
-> - PDF traceability: [PDF §1.2.14] — "Monitor your training data for pattern and distribution
-> changes to detect data drift"; "Amazon SageMaker Model Monitor – Data quality."
->
-> **Reference:** [SageMaker Model Monitor Data Quality](https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor-data-quality.html)
-
-#### FS-18 — Model Drift Detection → *Merged into upstream SM-23*
-
-> **Upstream extension note (do not ship as a standalone check):** The detection and remediation
-> content from FS-18 should be added as a refinement of the existing **SM-23 (Model Drift
-> Detection)** check in the upstream repo.
->
-> **What to add to SM-23:**
-> - Filter `ListMonitoringSchedules` results for `MonitoringType=ModelQuality`.
-> - Add a new remediation step for **low-entropy classification monitoring** (PDF §1.2.14
-> mitigation): publish custom CloudWatch metrics tracking prediction confidence distributions,
-> set threshold boundaries for unexpected low-confidence/high-confidence clusters, and alert
-> when the retrained model produces unexpected classification patterns — this can indicate
-> training data poisoning before accuracy metrics degrade.
-> - PDF traceability: [PDF §1.2.14] — "Before deploying to production, compare your retrained
-> model's performance against previous iterations using historical test data as a baseline."
->
-> **Reference:** [SageMaker Model Monitor Model Quality](https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor-model-quality.html)
-
-#### FS-19 — Model Registry Approval → *Merged into upstream SM-22*
-
-> **Upstream extension note (do not ship as a standalone check):** The detection and remediation
-> content from FS-19 should be added as a refinement of the existing **SM-22 (Model Approval
-> Workflow)** check in the upstream repo.
->
-> **What to add to SM-22:**
-> - Explicitly check that `ModelApprovalStatus=PendingManualApproval` is the default for new
-> model package versions (not `Approved`).
-> - Flag any model package group where the latest version has `ModelApprovalStatus=Approved`
-> without evidence of a manual approval step (i.e., auto-approved at creation time).
-> - PDF traceability: [PDF §1.2.14] — cites "Amazon SageMaker AI – Model Registration and
-> Deployment with Model Registry" as a reference for staged deployment with rollback.
->
-> **Reference:** [SageMaker Model Registry](https://docs.aws.amazon.com/sagemaker/latest/dg/model-registry.html)
-
-#### FS-20 — Feature Store Rollback
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.14] — "Create a rollback plan by using versioned training data and models. This ensures that you can revert to a stable, working model if failures occur." References "Amazon SageMaker AI Feature Store". |
-| Description | Checks SageMaker Feature Store has offline store for rollback capability. |
-| Detection | Calls `sagemaker:ListFeatureGroups` to enumerate all groups, then `sagemaker:DescribeFeatureGroup` for each to inspect `OfflineStoreConfig`. Flags feature groups where `OfflineStoreConfig` is absent (online-only groups with no offline store for rollback). |
-| Remediation | 1. Enable the offline store on each feature group: specify an S3 URI and data catalog in `OfflineStoreConfig`. 2. The offline store provides a versioned, immutable history of feature values for point-in-time rollback. 3. Test rollback by querying the offline store with a historical timestamp. |
-| Reference | [SageMaker Feature Store](https://docs.aws.amazon.com/sagemaker/latest/dg/feature-store.html) |
-
-#### FS-21 — Training Data S3 Versioning and Audit Trail
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.14] — "Use trusted data sources for your training data. Implement audit controls that let you track and review changes, including who made them and when they occurred." |
-| Description | Verifies S3 buckets used for training data have versioning enabled so poisoned datasets can be rolled back. Recommends CloudTrail data-event logging as remediation to record who modified training data and when. |
-| Detection | Identifies training-data S3 buckets by naming convention (`train`/`dataset`/`model`/`sagemaker`/`bedrock`). Calls `s3:GetBucketVersioning` to verify `Status=Enabled`. (CloudTrail data-event logging is recommended in remediation but is not asserted by this check — verifying it is covered by the upstream BR-06 CloudTrail control and the FS-23 extension.) |
-| Remediation | 1. Enable versioning: `aws s3api put-bucket-versioning --bucket --versioning-configuration Status=Enabled`. 2. Enable CloudTrail S3 data events for the training-data buckets to capture PutObject/DeleteObject with caller identity. 3. Enable MFA Delete for critical training datasets. 4. Apply S3 Object Lock for immutable baselines. |
-| Reference | [S3 Versioning](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html), [CloudTrail Data Events](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/logging-data-events-with-cloudtrail.html) |
-
-### Vector & Embedding Weaknesses (FS-22 to FS-26)
-
-> **PDF source:** §1.2.15 Vector and embedding weaknesses. PDF-listed mitigations:
-> (a) apply least privilege to vector and embedding database access;
-> (b) validate knowledge base data sources;
-> (c) add data only from trusted sources to knowledge bases;
-> (d) monitor and log all activities in knowledge base control plane (CloudTrail);
-> (e) enable encryption at rest and in transit for vector and embedding databases;
-> (f) implement document/record-level access controls via KB metadata filtering for
-> multi-tenancy.
-
-#### FS-22 — Knowledge Base IAM Least Privilege
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.15] — "Apply the principle of least privilege to control access to your vector and embedding database. Only grant users and services the minimum permissions they need to perform their tasks." |
-| Description | Checks IAM roles accessing Knowledge Bases have no wildcard `bedrock:*` permissions covering KB actions. |
-| Detection | Inspects the permissions cache for all IAM roles. Flags any role with an Allow statement granting `bedrock:*` without resource-level restrictions, or broad `bedrock:` actions covering KB operations without a specific knowledge-base ARN. Note: Bedrock agent and KB operations use the single IAM service prefix `bedrock:` (not `bedrock-agent:`) — the `bedrock-agent` token refers to the boto3 SDK client name, not the IAM action prefix. |
-| Remediation | 1. Replace wildcard `bedrock:*` with specific KB actions: `bedrock:Retrieve`, `bedrock:RetrieveAndGenerate`, `bedrock:GetKnowledgeBase` (these are the actual IAM action names — verify via the AWS Service Authorization Reference for Amazon Bedrock). 2. Scope the resource ARN to specific Knowledge Base IDs (e.g., `arn:aws:bedrock:::knowledge-base/`). 3. Apply IAM permission boundaries to limit blast radius. |
-| Reference | [Bedrock Knowledge Base Permissions](https://docs.aws.amazon.com/bedrock/latest/userguide/kb-permissions.html) |
-
-#### FS-23 — Knowledge Base CloudTrail Logging → *Merged into upstream BR-06*
-
-> **Upstream extension note (do not ship as a standalone check):** The detection and remediation
-> content from FS-23 should be added as a refinement of the existing **BR-06 (CloudTrail
-> Logging)** check in the upstream repo.
->
-> **What to add to BR-06:**
-> - After verifying that a CloudTrail trail is active and logging Bedrock management events,
-> additionally check for an **advanced event selector** with
-> `resources.type = AWS::Bedrock::KnowledgeBase` to capture `Retrieve` and
-> `RetrieveAndGenerate` data events (these are NOT logged by default — they require an
-> explicit advanced event selector).
-> - Note: `InvokeAgent` / `InvokeInlineAgent` are also data events requiring
-> `resources.type = AWS::Bedrock::AgentAlias` or `AWS::Bedrock::InlineAgent` respectively.
-> Data events incur additional CloudTrail charges and can produce high volumes under load.
-> - PDF traceability: [PDF §1.2.15] — "Monitor and log all activities in knowledge base
-> control plane" with reference "Monitor Amazon Bedrock API calls using CloudTrail."
->
-> **Reference:** [CloudTrail Bedrock Logging](https://docs.aws.amazon.com/bedrock/latest/userguide/logging-using-cloudtrail.html)
-
-#### FS-24 — Knowledge Base Metadata Filtering
-
-| Field | Detail |
-|-------|--------|
-| Severity | Informational |
-| PDF ref | [PDF §1.2.15] — "Implement access controls at the document or record level within knowledge bases where different users or applications should only have access to specific subsets of data. Use Amazon Bedrock Knowledge Bases metadata filtering to enforce data segmentation." |
-| Description | Advisory: verifies KB metadata fields support tenant-level filtering for multi-tenancy. |
-| Detection | Calls `ListKnowledgeBases` and `GetKnowledgeBase` (via the `bedrock-agent` boto3 client; IAM actions are `bedrock:ListKnowledgeBases` and `bedrock:GetKnowledgeBase`). Inspects the storage configuration for metadata field definitions. Flags KBs with no metadata fields defined (no tenant isolation possible). |
-| Remediation | 1. Define metadata fields on your KB data sources (e.g., `tenant_id`, `department`, `classification`). 2. Populate metadata during document ingestion. 3. Use the `filter` parameter in Retrieve/RetrieveAndGenerate API calls to enforce tenant-scoped queries. 4. Test that cross-tenant data leakage is prevented. |
-| Reference | [Bedrock KB Metadata Filtering](https://docs.aws.amazon.com/bedrock/latest/userguide/kb-test-config.html) |
-
-#### FS-25 — OpenSearch Serverless Encryption
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.15] — "Enable encryption at rest and in transit for vector and embedding databases." |
-| Description | Checks OpenSearch Serverless collections used by KBs have CMK encryption policies. |
-| Detection | Calls `opensearchserverless:ListCollections` (IAM action `aoss:ListCollections`) and for each calls `opensearchserverless:ListSecurityPolicies(type=encryption)` (IAM action `aoss:ListSecurityPolicies`). Inspects each encryption policy's document for `AWSOwnedKey=true` or missing `KmsARN`. Note: the encryption **policy JSON document** uses PascalCase field names — `AWSOwnedKey` and `KmsARN` — while the direct API `EncryptionConfig` struct uses camelCase (`aWSOwnedKey`, `kmsKeyArn`); detection should inspect the policy document form returned by `GetSecurityPolicy`/`ListSecurityPolicies`. Flags collections using AWS-owned keys instead of customer-managed KMS keys. Note: the boto3 client name is `opensearchserverless`, but IAM actions use the service prefix `aoss:` (not `opensearchserverless:`). Note also: encryption in transit is automatic (TLS 1.2, AES-256) for all OpenSearch Serverless traffic and is not configurable — this check focuses on encryption at rest. |
-| Remediation | 1. Create an encryption security policy specifying a customer-managed KMS key: set `AWSOwnedKey=false` and provide `KmsARN` with the ARN of your KMS key. 2. Apply the policy to the collection by matching the collection name or prefix pattern in the policy `Rules`. 3. Ensure the KMS key policy grants the OpenSearch Serverless service principal `kms:Decrypt` and `kms:GenerateDataKey`. Note: if you provide a KMS key directly in the `CreateCollection` request, it takes precedence over any matching security policies. |
-| Reference | [OpenSearch Serverless Encryption](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/serverless-encryption.html) |
-
-#### FS-26 — Knowledge Base VPC Access
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.15, extension] — network isolation is not verbatim in the PDF but directly implements "Apply the principle of least privilege to control access to your vector and embedding database" at the network layer. |
-| Description | Verifies OpenSearch Serverless collections have VPC-only network policies (no public access). |
-| Detection | Calls `opensearchserverless:ListSecurityPolicies(type=network)` (IAM action `aoss:ListSecurityPolicies` — the service prefix for OpenSearch Serverless is `aoss`, not `opensearchserverless`) and inspects each policy rule for `AllowFromPublic=true`. Flags collections accessible from the public internet. Note: a policy with `AllowFromPublic=false` may still grant private access to Bedrock via `SourceServices: ["bedrock.amazonaws.com"]` or to specific VPC endpoints via `SourceVPCEs` — these are the recommended private-access patterns and are not flagged. |
-| Remediation | 1. Create a network security policy that restricts access to specific VPC endpoints only via `SourceVPCEs`, or grants private AWS service access (e.g., Bedrock) via `SourceServices: ["bedrock.amazonaws.com"]`. Per AWS docs, private access to AWS services applies only to the collection's OpenSearch endpoint, not to the OpenSearch Dashboards endpoint. 2. Create an OpenSearch Serverless VPC endpoint in your VPC if VPC-private access is required. 3. Remove any policy rules with `AllowFromPublic=true`. 4. Test connectivity from within the VPC. |
-| Reference | [OpenSearch Serverless Network Access](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/serverless-network.html) |
-
diff --git a/docs/SECURITY_CHECKS_FINSERV_PART2_GUARDRAILS_CONTENT_SAFETY.md b/docs/SECURITY_CHECKS_FINSERV_PART2_GUARDRAILS_CONTENT_SAFETY.md
deleted file mode 100644
index ef9cd7b..0000000
--- a/docs/SECURITY_CHECKS_FINSERV_PART2_GUARDRAILS_CONTENT_SAFETY.md
+++ /dev/null
@@ -1,318 +0,0 @@
-# FinServ GenAI Risk Checks — Part 2: Guardrails & Content Safety (FS-27 to FS-46)
-
-This is **Part 2 of 3** of the FinServ GenAI security checks derived from the
-[AWS guide for Financial Services risk management of the use of Generative AI (March 2026)](https://d1.awsstatic.com/onedam/marketing-channels/website/public/global-FinServ-ComplianceGuide-GenAIRisks-public.pdf)
-(referred to throughout as "the FinServ Guide").
-
-This part covers **20 checks** across 5 PDF risk categories:
-
-- **Non-Compliant Output** (FS-27 to FS-30) — §1.2.1
-- **Misinformation** (FS-31 to FS-34) — §1.2.3 (FS-34 sources from §1.2.12 — see note)
-- **Abusive or Harmful Output** (FS-35 to FS-38) — §1.2.4
-- **Biased Output** (FS-39 to FS-42) — §1.2.5
-- **Sensitive Information Disclosure** (FS-43 to FS-46) — §1.2.6
-
-**Companion files:**
-
-- `SECURITY_CHECKS_FINSERV_PART1_INFRA_CONTROLS.md` — FS-01 to FS-26 (Unbounded, Excessive Agency, Supply Chain, Training Poisoning, Vector Weaknesses)
-- `SECURITY_CHECKS_FINSERV_PART3_APP_LAYER_AND_GAPS.md` — FS-47 to FS-69 (Hallucination, Prompt Injection, Output Handling, Off-Topic, Stale Data, Material Gaps)
-- `SECURITY_CHECKS_FINSERV_COMMON.md` — shared intro, severity rubric, validation note, upstream-overlap table
-
-Each check includes how it is **detected** (the AWS API calls or configuration inspected)
-and how a failure is **remediated** (the specific AWS actions to take).
-
-See `SECURITY_CHECKS_FINSERV_COMMON.md` for:
-
-- PDF traceability conventions (`[PDF §x.y.z]` vs `[PDF §x.y.z, extension]`)
-- Severity rubric (High / Medium / Low / Informational) — see SECURITY_CHECKS_FINSERV_SEVERITY_METHODOLOGY.md
-- Validation note and AWS service authorization references
-- Relationship to upstream SM/BR/AC checks and consolidation recommendations
-
----
-
-## FinServ GenAI Risk Checks — Part 2 content
-
-### Non-Compliant Output (FS-27 to FS-30)
-
-> **PDF source:** §1.2.1 Non-compliant output. PDF-listed mitigations:
-> (a) prompt engineering to guide the model and prevent unwanted responses;
-> (b) content filters and denied topics in Bedrock Guardrails;
-> (c) RAG with Bedrock Knowledge Bases;
-> (d) Automated Reasoning checks in Bedrock Guardrails;
-> (e) human-in-the-loop validation for internal AI systems;
-> (f) audit logs of AI-generated outputs and guardrails applied for regulatory reporting.
-
-#### FS-27 — Automated Reasoning Checks
-
-| Field | Detail |
-|-------|--------|
-| Severity | High (contextual grounding) / Medium (Automated Reasoning) |
-| PDF ref | [PDF §1.2.1, §1.2.7] — "Automated Reasoning checks in Amazon Bedrock Guardrails uses automated reasoning to verify that natural language content complies with your defined policies. This mathematical verification helps ensure that your content strictly follows your guardrails." |
-| Description | Verifies Bedrock Guardrails have Automated Reasoning checks or contextual grounding enabled. |
-| Detection | Calls `bedrock:ListGuardrails` and `bedrock:GetGuardrail` for each. Inspects the response fields `contextualGroundingPolicy` and `automatedReasoningPolicy`. Flags guardrails with neither enabled. |
-| Remediation | 1. Enable contextual grounding filters (type `GROUNDING`) with a threshold ≥ 0.7 — these filters CAN block content that fails grounding checks. Note: valid threshold values are 0 to 0.99; a threshold of 1.0 is invalid and will block all content. **Important use-case limitation:** Contextual grounding checks support summarization, paraphrasing, and question answering use cases only — **Conversational QA / Chatbot use cases are not supported**. If your FinServ application is a conversational chatbot, contextual grounding cannot be used for hallucination detection; use Automated Reasoning checks or human-in-the-loop validation instead. 2. If available in your region, additionally enable Automated Reasoning checks by creating an Automated Reasoning policy and attaching it to the guardrail. **Cross-Region inference is REQUIRED for AR:** Guardrails that use Automated Reasoning checks require a cross-Region inference profile — set `crossRegionConfig.guardrailProfileIdentifier` to a profile matching your Region (for example, `us.guardrail.v1:0` for US Regions or `eu.guardrail.v1:0` for EU Regions). Omitting this parameter returns `ValidationException`. As of April 2026, AR is generally available in US East (N. Virginia), US East (Ohio), US West (Oregon), EU (Frankfurt), EU (Ireland), and EU (Paris) — verify current regional availability on the [AR documentation page](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-automated-reasoning-checks.html) before audit reliance, as AWS regularly expands coverage. Attach the **versioned** policy ARN (for example, `...:1`) — the unversioned ARN returns an error. You can attach a maximum of 2 AR policies per guardrail. Important: Automated Reasoning operates in **detect mode only** — it returns findings and feedback but does NOT block content. AR finding types (per the user guide) are: `VALID` (response is consistent with policy), `INVALID` (response contradicts policy rules), `SATISFIABLE` (response could be true or false depending on unstated conditions), `IMPOSSIBLE` (premises are contradictory), `TRANSLATION_AMBIGUOUS` (natural language could not be reliably translated to formal logic), `TOO_COMPLEX` (policy complexity exceeded processing limits), and `NO_TRANSLATIONS` (some or all input was not translated into logic due to irrelevance or lack of matching policy variables). Note: in the `AutomatedReasoningCheckFinding` runtime response, these appear as a **union** with lowercase camelCase keys (`valid`, `invalid`, `satisfiable`, `impossible`, `translationAmbiguous`, `tooComplex`, `noTranslations`) — exactly one key is present per finding. Per AWS docs, AR also **does not protect against prompt injection attacks**, **cannot detect off-topic responses**, **does not support streaming APIs**, and **supports English (US) only** — use content filters, topic policies, and other guardrail components alongside AR. **Critical limitation for cross-account enforcement:** AR policies are NOT supported with Bedrock Guardrails cross-account safeguards (org-level or account-level enforcement) — including an AR policy in a guardrail used for enforcement will cause runtime failures. If you rely on AR, configure it at the application or account level separately. Your application must inspect the AR findings via the `ApplyGuardrail` (or `Converse` / `InvokeModel` / `InvokeAgent` / `RetrieveAndGenerate`) API response and decide whether to serve the response, rewrite it using AR feedback, ask the user for clarification, or fall back to a default behavior. 3. For `INVALID` responses, implement an iterative rewriting loop that feeds AR feedback (contradicting rules) back to the LLM to self-correct. 4. Build an audit trail of all AR validation iterations — log `supportingRules` and `claimsTrueScenario` for `VALID` findings as mathematically verifiable compliance evidence. |
-| Reference | [Automated Reasoning in Bedrock Guardrails](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-automated-reasoning-checks.html), [AR Checks Concepts (Validation Results Reference)](https://docs.aws.amazon.com/bedrock/latest/userguide/automated-reasoning-checks-concepts.html), [Integrate AR Checks in Your Application](https://docs.aws.amazon.com/bedrock/latest/userguide/integrate-automated-reasoning-checks.html), [Deploy Automated Reasoning Policy](https://docs.aws.amazon.com/bedrock/latest/userguide/deploy-automated-reasoning-policy.html) |
-
-#### FS-28 — Financial Denied Topics
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.1] — "Configure content filters and guardrails to restrict model responses to approved topics" with reference "Amazon Bedrock User Guide – Guardrails – Denied topics". |
-| Description | Checks guardrails have denied topics for regulated financial advice. |
-| Detection | Calls `bedrock:GetGuardrail` and inspects `topicPolicy.topics` for entries with `type=DENY`. Flags guardrails with no denied topics or with no topics related to financial advice, investment recommendations, or tax guidance. |
-| Remediation | 1. Add denied topics to the guardrail following the AWS best-practice golden rules: (a) **Be crisp and precise** — e.g., "Investment advice is inquiries, guidance, or recommendations about the management or allocation of funds or assets with the goal of generating returns or achieving specific financial objectives" rather than vague "Investment advice". (b) **Define, don't instruct** — write "All content associated with specific investment recommendations" not "Block all investment advice". (c) **Stay positive** — never define topics negatively (e.g., avoid "All content except general financial education"). (d) **Focus on themes, not words** — denied topics capture subjects contextually; use word filters for specific names or entities. (e) **Provide sample phrases** — add up to 5 representative inputs per topic (each up to 100 characters). 2. **Quantity and character limits:** A guardrail can contain a maximum of **30 denied topics**. In Classic tier, topic definitions are limited to 200 characters; in Standard tier, up to 1,000 characters — use Standard tier for complex financial topic definitions. 3. Recommended denied topics for FinServ: "specific investment recommendations", "tax advice", "specific financial product recommendations", "guaranteed returns or performance claims". 4. For multi-account enforcement, use Bedrock cross-account safeguards to apply denied topics from a management-account guardrail across all member accounts automatically. When configuring account-level or org-level enforcement, set **both** `selectiveContentGuarding.messages` AND `selectiveContentGuarding.system` to `COMPREHENSIVE` to ensure guardrails evaluate all user messages AND system prompts regardless of input tags — use `SELECTIVE` only when you trust callers to correctly tag content. Setting only `messages` to COMPREHENSIVE leaves system prompts potentially unguarded. 5. Enforce guardrails via IAM policy conditions (`bedrock:GuardrailIdentifier`) to prevent any Bedrock inference call without a guardrail attached. 6. Test with prompts that attempt to elicit regulated financial advice. |
-| Reference | [Bedrock Guardrails Denied Topics](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-denied-topics.html), [Safeguard Tiers for Guardrails](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-tiers.html), [Cross-Account Safeguards with Enforcements](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-enforcements.html), [Guardrails Best Practices](https://aws.amazon.com/blogs/machine-learning/build-safe-generative-ai-applications-like-a-pro-best-practices-with-amazon-bedrock-guardrails/) |
-
-#### FS-29 — Compliance Disclaimer
-
-| Field | Detail |
-|-------|--------|
-| Severity | Informational |
-| PDF ref | [PDF §1.2.1, extension] — disclaimers are not verbatim in §1.2.1 but the PDF references "Implement response disclaimers in customer-facing applications" under §1.2.7 Hallucination, which is conceptually the same control applied here for non-compliant financial-advice output. |
-| Description | Advisory: verifies application adds required regulatory disclaimers to AI-generated outputs. |
-| Detection | Advisory check — cannot be fully automated. Inspects application Lambda function environment variables or configuration for disclaimer-related settings (e.g., `DISCLAIMER_ENABLED`, `COMPLIANCE_FOOTER`). |
-| Remediation | 1. Add a standard regulatory disclaimer to all customer-facing AI-generated responses (e.g., "This information is generated by AI and does not constitute financial advice. Please consult a qualified financial advisor."). 2. Make the disclaimer text configurable via environment variable or parameter store. 3. Ensure disclaimers are not removable by prompt manipulation. |
-| Reference | [AWS Well-Architected GenAI Lens — Guardrails](https://docs.aws.amazon.com/wellarchitected/latest/generative-ai-lens/gensec02-bp01.html) |
-
-#### FS-30 — Compliance Evaluation Datasets
-
-| Field | Detail |
-|-------|--------|
-| Severity | Informational |
-| PDF ref | [PDF §1.2.1, extension] — the PDF §1.2.12 practical guidance mentions "Amazon Bedrock Evaluations can help to evaluate models against specific types of attacks"; this check extends that concept to compliance-specific evaluation for FS-regulated outputs. |
-| Description | Checks Bedrock evaluation jobs use compliance-specific test datasets. |
-| Detection | Calls `bedrock:ListEvaluationJobs` to enumerate existing jobs, then calls `bedrock:GetEvaluationJob` for each to inspect the full `evaluationConfig` including dataset configuration. Flags if no evaluation jobs exist or if none reference compliance/regulatory test data. Note: `ListEvaluationJobs` returns only job summaries — dataset configuration details require `GetEvaluationJob`. |
-| Remediation | 1. Create a compliance-specific evaluation dataset containing: prompts requesting regulated financial advice, prompts testing disclaimer presence, prompts testing denied-topic enforcement. 2. Run Bedrock evaluation jobs with this dataset before each production deployment. 3. Set pass/fail thresholds and gate deployments on results. |
-| Reference | [Bedrock Model Evaluation](https://docs.aws.amazon.com/bedrock/latest/userguide/evaluation.html) |
-
-### Misinformation (FS-31 to FS-33)
-
-> **PDF source:** §1.2.3 Misinformation through inadvertent or malicious action. PDF-listed mitigations:
-> (a) prompt engineering;
-> (b) verify knowledge base data sources are up-to-date, accurate, reliable, and complete;
-> (c) human-in-the-loop validation for internal AI systems;
-> (d) source attribution in RAG responses for end users to verify provenance;
-> (e) integrity monitoring on knowledge base data sources — e.g., S3 event notifications to
-> track document changes.
-
-#### FS-31 — Knowledge Base Data Source Sync
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.3, §1.2.10] — "Verify that your knowledge base data sources are up-to-date, accurate, reliable, and complete"; "Sync your data with your Amazon Bedrock knowledge base". |
-| Description | Verifies KB data sources have been synced within 7 days. |
-| Detection | Calls `ListDataSources` then `ListIngestionJobs` for each data source (via the `bedrock-agent` boto3 client; IAM actions are `bedrock:ListDataSources` and `bedrock:ListIngestionJobs`). Checks the most recent successful ingestion job's `updatedAt` timestamp. Flags data sources not synced within 7 days. |
-| Remediation | 1. Create an EventBridge scheduled rule to trigger KB data source sync at least weekly. 2. Use `StartIngestionJob` (IAM action `bedrock:StartIngestionJob`) as the rule target. 3. Add CloudWatch alarms for failed ingestion jobs. 4. For rapidly changing data, increase sync frequency. |
-| Reference | [Bedrock KB Data Source Sync](https://docs.aws.amazon.com/bedrock/latest/userguide/kb-data-source-sync-ingest.html) |
-
-#### FS-32 — Source Attribution
-
-| Field | Detail |
-|-------|--------|
-| Severity | Informational |
-| PDF ref | [PDF §1.2.3, §1.2.10] — "Use source attribution in RAG-based response for end users to verify provenance of information" (§1.2.3); "Use source attribution in RAG-based response for end users to verify currency of information" (§1.2.10). |
-| Description | Advisory: verifies application implements source citations in RAG responses. |
-| Detection | Advisory check — inspects application code or configuration for use of the `citations` field in `RetrieveAndGenerate` API responses. Checks Lambda environment variables for attribution-related settings. |
-| Remediation | 1. Use the `RetrieveAndGenerate` API (IAM action `bedrock:RetrieveAndGenerate`) which returns `citations` with source document references. Each citation contains `retrievedReferences` — an array where each reference has a `content` object (the cited text), a `location` object (data source type and URI — for S3 sources, `location.type=S3` and `location.s3Location.uri` contains the S3 URI), and optional `metadata` (a string-to-JSON map with any custom metadata attributes stored on the chunk, which can hold document title and other fields). Note: there is no fixed `title` field in the API — if you need to display document titles to end users, store them as a metadata attribute during KB ingestion and retrieve them via `retrievedReferences[].metadata`. 2. Display source citations to end users alongside AI-generated responses. 3. Include the data source location (URI or other location identifier depending on source type: S3, Web, Confluence, SharePoint, Salesforce, Kendra, SQL, or Custom) and the cited text excerpt (from `content`). 4. If document titles are required, ensure they are populated in KB metadata and propagated to your UI. 5. Allow users to click through to the original source document where possible. |
-| Reference | [Bedrock RetrieveAndGenerate API](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent-runtime_RetrieveAndGenerate.html) |
-
-#### FS-33 — Knowledge Base Integrity Monitoring
-
-| Field | Detail |
-|-------|--------|
-| Severity | High (deleted bucket) / Medium (versioning) |
-| PDF ref | [PDF §1.2.3] — "Use integrity monitoring on knowledge base data sources to detect unauthorized modifications. Track changes to documents used in knowledge bases." References "For example on S3 data sources use Amazon S3 event notification to track changes to documents." |
-| Description | Checks KB data source S3 buckets have versioning enabled and S3 event notifications (EventBridge or SNS) configured to detect unauthorized document modifications in real time. |
-| Detection | Identifies KB data-source S3 buckets via `GetDataSource` (via the `bedrock-agent` boto3 client; IAM action `bedrock:GetDataSource`). Calls `s3:GetBucketVersioning` to verify `Status=Enabled`. Calls `s3:GetBucketNotificationConfiguration` and checks for `EventBridgeConfiguration`, `TopicConfigurations`, `QueueConfigurations`, or `LambdaFunctionConfigurations`. Flags buckets missing either control. |
-| Remediation | 1. Enable versioning: `aws s3api put-bucket-versioning --bucket --versioning-configuration Status=Enabled`. 2. Enable EventBridge notifications on the bucket: `aws s3api put-bucket-notification-configuration --bucket --notification-configuration '{"EventBridgeConfiguration":{}}'`. Once enabled, S3 automatically sends **all** bucket events to EventBridge — you do not select specific event types at the bucket level. 3. Create an EventBridge rule that matches S3 events for this bucket — use the `detail-type` field values `Object Created` and `Object Deleted` (these are the EventBridge event type names; note: `s3:ObjectCreated:*` and `s3:ObjectRemoved:*` are the legacy SNS/SQS/Lambda notification event type names and are NOT used in EventBridge rules). Route matched events to an SNS topic or Lambda function for alerting. 4. Integrate alerts into your security incident response workflow. |
-| Reference | [S3 EventBridge Integration](https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventBridge.html) |
-
-> **Note:** FS-34 (Third-Party Risk Management for FM Providers) is kept adjacent to Misinformation
-> in this file for continuity with the prior draft numbering, but its PDF source is §1.2.12
-> Supply Chain Vulnerabilities. Treat FS-34 as a Supply Chain check for compliance-framework
-> mapping purposes.
-
-#### FS-34 — Third-Party Risk Management (TPRM) for Foundation Model Providers
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.12] — *"Update existing third-party risk management processes to continuously monitor model providers and third-party dependencies, including tracking vendor security advisories, model deprecation notices, and change to terms and conditions."* (Note: moved from the Misinformation section in the prior draft; the PDF places TPRM under Supply Chain.) |
-| Description | Verifies a documented third-party risk management (TPRM) process exists to monitor FM providers for security advisories, model deprecation notices, and T&C changes; also flags legacy FMs currently in use. |
-| Detection | Calls `bedrock:ListFoundationModels`, then `bedrock:GetFoundationModel` for each in-use model; inspects `modelLifecycle.status` and flags models with status `LEGACY`. Note: the `FoundationModelLifecycle.status` API field has only **two** valid values — `ACTIVE` and `LEGACY`. There is no `EOL` status value in the API; models that have passed their EOL date are removed from the service entirely and API calls referencing them will fail. The user-facing lifecycle page describes three conceptual states (Active, Legacy, EOL) but the API only exposes two. Advisory component checks for evidence of a TPRM process — e.g., an AWS Config rule or a tag on Bedrock resources indicating periodic review (`tprm-last-reviewed=`). |
-| Remediation | 1. Establish a documented TPRM process: at least quarterly review of each in-use FM provider's security advisories, model lifecycle announcements, and T&C changes. 2. Assign an owner for the TPRM process and record review evidence in your MRM system. 3. Subscribe to AWS Bedrock model lifecycle notifications. 4. Migrate workloads from `LEGACY` models to active versions before their published EOL date — note that for models with EOL dates after February 1, 2026, there is a "public extended access" period where Legacy models remain usable but at higher pricing set by the model provider. 5. For third-party models procured via AWS Marketplace or consumed directly, evaluate the provider's own testing procedures — AWS AI Service Cards provide this transparency for Amazon-trained models. |
-| Reference | [Bedrock Model Lifecycle](https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html), [Access Amazon Bedrock foundation models](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) |
-
-### Abusive or Harmful Output (FS-35 to FS-38)
-
-> **PDF source:** §1.2.4 Model output is abusive or harmful. PDF-listed mitigations:
-> (a) AWS AI Service Cards to understand how Amazon addresses toxicity per model;
-> (b) Amazon Bedrock Guardrails to detect and filter harmful content;
-> (c) FMEval to evaluate for inappropriate content (sexual, profanity, hate, aggression,
-> insults, flirtation, identity attacks, threats);
-> (d) user reporting mechanism so end users can flag abusive outputs, reviewed within a
-> defined process;
-> (e) Practical guidance: create allowlists for approved business terminology to reduce
-> false positives on brand, product, industry, and technical vocabulary.
-
-#### FS-35 — FMEval Harmful Content
-
-| Field | Detail |
-|-------|--------|
-| Severity | Informational |
-| PDF ref | [PDF §1.2.4] — "Foundation Model Evaluations (FMEval) evaluates your model to detect inappropriate content, including sexual references, profanity, hate speech, aggression, insults, flirtation, identity-based attacks, and threats." |
-| Description | Checks Bedrock evaluation jobs test for harmful/toxic content. |
-| Detection | Calls `bedrock:ListEvaluationJobs` to enumerate existing jobs, then calls `bedrock:GetEvaluationJob` for each to inspect the full `evaluationConfig`. The correct metric name depends on the evaluation job type: (a) For **automated model evaluation jobs** (pre-computed metrics), the toxicity metric is `"Builtin.Toxicity"` — the only valid harmful-content metric for this job type. (b) For **judge-based model evaluation jobs** (LLM-as-judge), the harmful content metrics are `"Builtin.Harmfulness"` and `"Builtin.Stereotyping"`. (c) For **knowledge base (RAG) evaluation jobs**, `"Builtin.Harmfulness"` and `"Builtin.Stereotyping"` are also valid. Flags if no evaluation jobs exist or none include a harmful-content metric (`Builtin.Toxicity` for automated, `Builtin.Harmfulness` for judge-based/RAG). Note: `ListEvaluationJobs` returns only job summaries — dataset configuration details require `GetEvaluationJob`. |
-| Remediation | 1. For **automated model evaluation** (fastest, no judge model required): create a Bedrock evaluation job with `"Builtin.Toxicity"` in the `metricNames` array. Valid task types are `Summarization`, `Classification`, `QuestionAndAnswer`, `Generation`, and `Custom`. 2. For **judge-based model evaluation** (more nuanced, requires a judge model): create a Bedrock evaluation job with `"Builtin.Harmfulness"` and/or `"Builtin.Stereotyping"` in the `metricNames` array — these metrics are only valid for judge-based and RAG evaluation jobs, not automated model evaluation jobs. 3. Include test prompts designed to elicit harmful content. 4. Set pass/fail thresholds based on the scores returned. 5. Run evaluations before production deployment and after model updates. 6. For more granular toxicity scoring (the 7-category UnitaryAI Detoxify-unbiased scores: `toxicity`, `severe_toxicity`, `obscene`, `threat`, `insult`, `sexual_explicit`, `identity_attack` — or the Toxigen-roberta binary classifier), use SageMaker FMEval via SageMaker Studio or the `fmeval` Python library as a complementary evaluation path. |
-| Reference | [Bedrock Model Evaluation Metrics](https://docs.aws.amazon.com/bedrock/latest/userguide/model-evaluation-metrics.html), [SageMaker FMEval Toxicity](https://docs.aws.amazon.com/sagemaker/latest/dg/clarify-toxicity-evaluation.html) |
-
-#### FS-36 — Guardrail Content Filters
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.4] — "Use Amazon Bedrock's guardrails to detect and filter harmful content." |
-| Description | Verifies guardrails have content filters for hate, violence, sexual, and other harmful content. |
-| Detection | Calls `bedrock:GetGuardrail` and inspects `contentPolicy.filters`. Flags guardrails missing filters for HATE, VIOLENCE, SEXUAL, INSULTS, or MISCONDUCT categories. Also checks that `inputStrength` and `outputStrength` are at least `MEDIUM`. |
-| Remediation | 1. Update the guardrail to include content filters for all harmful categories: HATE, VIOLENCE, SEXUAL, INSULTS, MISCONDUCT. 2. Select the **Standard tier** (not Classic) for content filters — it offers better accuracy, broader language support (extensive multilingual support vs. English/French/Spanish only in Classic), prompt leakage detection, and extends protection to harmful content within code elements. Standard tier requires cross-Region inference to be enabled on the guardrail (configurable at creation or by modifying an existing guardrail). 3. Start with **HIGH** filter strength for customer-facing applications; evaluate false-positive rates on representative sample traffic and lower to MEDIUM only if necessary. 4. Apply filters to both INPUT and OUTPUT. 5. Before enabling blocking in production, use **detect mode** (`action=NONE`) to test guardrail behavior on live traffic — review trace output to validate decisions, then switch to `action=BLOCK` once confident. 6. Enforce guardrails organization-wide via IAM policy-based enforcement: add an IAM condition key (`bedrock:GuardrailIdentifier`) to deny any `InvokeModel`/`Converse` call that does not include a guardrail. For account-level or org-level enforcement configurations, set **both** `selectiveContentGuarding.messages` AND `selectiveContentGuarding.system` to `COMPREHENSIVE` to ensure guardrails evaluate all user messages AND system prompts regardless of input tags (use `SELECTIVE` only when you trust callers to correctly tag content). Setting only `messages` to COMPREHENSIVE leaves system prompts potentially unguarded. |
-| Reference | [Bedrock Guardrails Content Filters](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-content-filters.html), [Safeguard Tiers for Guardrails](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-tiers.html), [Cross-Account Safeguards with Enforcements](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-enforcements.html), [Guardrails Best Practices](https://aws.amazon.com/blogs/machine-learning/build-safe-generative-ai-applications-like-a-pro-best-practices-with-amazon-bedrock-guardrails/), [IAM Guardrail Enforcement](https://aws.amazon.com/blogs/machine-learning/amazon-bedrock-guardrails-announces-iam-policy-based-enforcement-to-deliver-safe-ai-interactions/) |
-
-#### FS-37 — User Feedback Mechanism
-
-| Field | Detail |
-|-------|--------|
-| Severity | Informational |
-| PDF ref | [PDF §1.2.4] — "Implement a user reporting mechanism that allows end users to flag abusive or harmful outputs. Reported incidents [are] reviewed within a defined process to refine content filters." |
-| Description | Advisory: verifies application has a user reporting mechanism for harmful outputs. |
-| Detection | Advisory check — inspects application configuration for feedback-related settings (e.g., `FEEDBACK_ENABLED`, `REPORT_ABUSE_ENDPOINT`). Checks for Lambda functions with "feedback" or "report" in the name. |
-| Remediation | 1. Implement a "Report this response" button in the application UI. 2. Route reported responses to an SQS queue or DynamoDB table for review. 3. Define an SLA for reviewing reported content (e.g., 24 hours). 4. Use reported incidents to refine guardrail content filters and word lists. 5. Log all reports with Bedrock invocation logging correlation IDs. |
-| Reference | [Bedrock Model Invocation Logging](https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html) |
-
-#### FS-38 — Guardrail Word Filters and Business Term Allowlists
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.4 — Practical guidance] — "Create allowlists for business terms that include approved terminology for: brand names, product names, industry terms, and technical vocabulary. Also test filter settings to verify that your content filters allow necessary business communications and generate accurate alerts. Monitor and adjust regularly your filtering system to reduce false positives." |
-| Description | Checks guardrails have word/phrase block filters configured and that approved business terminology allowlists are defined to prevent false positives on legitimate financial services vocabulary. |
-| Detection | Calls `bedrock:GetGuardrail` and inspects `wordPolicy`. Flags guardrails with no custom `words` array (blocked phrases). Also checks `managedWordLists` for the AWS-managed `PROFANITY` list. Note: a guardrail with only the profanity filter and no custom FinServ-specific blocked terms should still be flagged as incomplete for financial services use cases. |
-| Remediation | 1. Add blocked words/phrases to the guardrail word filter (profanity, slurs, competitor names if applicable). Each custom word/phrase entry has a maximum length of **100 characters** per the API (`GuardrailWordConfig.text`); the console UI additionally limits entries to **up to three words** per phrase. You can add up to **10,000 items** to the custom word filter. 2. Enable the AWS-managed profanity filter (`managedWordListsConfig` with `type=PROFANITY`) as a baseline. 3. Create an allowlist of approved business terminology: brand names, product names, industry terms, technical vocabulary — document this separately as the guardrail word filter only blocks, it does not allowlist. Test filter settings to verify legitimate business communications are not blocked. 4. Monitor and adjust regularly to reduce false positives. |
-| Reference | [Bedrock Guardrails Word Filters](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-word-filters.html) |
-
-### Biased Output (FS-39 to FS-42)
-
-> **PDF source:** §1.2.5 Model output is biased. PDF-listed mitigations:
-> (a) AWS AI Service Cards to understand how providers address fairness/bias per model;
-> (b) prompt engineering;
-> (c) Amazon Bedrock Guardrails;
-> (d) Bedrock Evaluations to measure bias;
-> (e) Amazon SageMaker Clarify for bias detection, transparency, and prediction explanation
-> on fine-tuned and self-trained models;
-> (f) develop and maintain a bias testing dataset with representative cases across
-> demographic groups, geographic regions, and sensitive attributes — run periodically and
-> after each model update.
-
-#### FS-39 — SageMaker Clarify Bias
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.5] — "Use Amazon SageMaker Clarify to detect bias, increase transparency, and explain predictions for your fine-tuned and self-trained AI models." |
-| Description | Verifies Clarify model bias monitoring is configured for financial decision models. |
-| Detection | Calls `sagemaker:ListMonitoringSchedules` with the `MonitoringTypeEquals=ModelBias` filter parameter (the `MonitoringType` field on the `MonitoringScheduleSummary` response has one of four values: `DataQuality`, `ModelQuality`, `ModelBias`, `ModelExplainability`). Flags if no bias monitoring schedules exist. Cross-references with endpoints tagged `use-case=financial-decision` or similar. Clarify bias monitoring publishes metrics to the `aws/sagemaker/Endpoints/bias-metrics` namespace for real-time endpoints (and `aws/sagemaker/ModelMonitoring/bias-metrics` for batch transform jobs) with `Endpoint`, `MonitoringSchedule`, `BiasStage`, `Label`, `LabelValue`, `Facet`, and `FacetValue` dimensions. |
-| Remediation | 1. Create a SageMaker Clarify bias monitoring schedule for each financial decision model endpoint. 2. Specify facets (protected attributes: age, gender, race, geography) and bias metrics (DPL, DI, DPPL). 3. Provide a baseline bias report from training data. 4. Configure CloudWatch alarms on bias metric violations on the `aws/sagemaker/Endpoints/bias-metrics` namespace. Note: `publish_cloudwatch_metrics` is enabled by default — do NOT set it to `Disabled` in the model bias job definition's `Environment` map, as that would stop metrics from being published to CloudWatch. |
-| Reference | [SageMaker Clarify Bias Detection](https://docs.aws.amazon.com/sagemaker/latest/dg/clarify-detect-post-training-bias.html) |
-
-#### FS-40 — Bedrock Bias Evaluation Datasets and Cadence
-
-| Field | Detail |
-|-------|--------|
-| Severity | Informational |
-| PDF ref | [PDF §1.2.5] — "Develop and maintain a bias testing dataset that includes representative test cases across demographic groups, geographic regions, and other sensitive attributes relevant to your use case. Run these test cases periodically and after model updates." |
-| Description | Checks evaluation jobs include demographic fairness test cases across protected groups and verifies evaluations are run on a defined periodic schedule and after each model update. |
-| Detection | Calls `bedrock:ListEvaluationJobs` to enumerate existing jobs, then calls `bedrock:GetEvaluationJob` for each to inspect the full `evaluationConfig` including dataset configuration for demographic diversity test cases. Checks the `creationTime` of the most recent evaluation job and flags if it is older than 90 days or if no evaluation was run after the most recent model deployment. Note: `ListEvaluationJobs` returns only job summaries — dataset configuration details require `GetEvaluationJob`. |
-| Remediation | 1. Create a bias evaluation dataset with representative test cases across demographic groups, geographic regions, and other sensitive attributes. 2. Schedule evaluation jobs to run at least quarterly via EventBridge. 3. Trigger an evaluation job automatically after each model update in your CI/CD pipeline. 4. Store results for audit and trend analysis. |
-| Reference | [Bedrock Model Evaluation](https://docs.aws.amazon.com/bedrock/latest/userguide/evaluation.html) |
-
-#### FS-41 — SageMaker Clarify Explainability
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.5, extension] — PDF §1.2.5 recommends "Amazon SageMaker Clarify to detect bias, increase transparency, and explain predictions". ECOA/Fair Housing adverse-action-notice use case is an FS-specific extension of Clarify explainability not named verbatim in the PDF. |
-| Description | Verifies Clarify explainability monitoring for adverse action notices (commonly cited under ECOA for credit decisions; this is an FS industry-practice extension, not a PDF-prescribed control). |
-| Detection | Calls `sagemaker:ListMonitoringSchedules` with the `MonitoringTypeEquals=ModelExplainability` filter parameter. Flags if no explainability monitoring schedules exist for financial decision model endpoints. Clarify explainability monitoring publishes metrics to the `aws/sagemaker/Endpoints/explainability-metrics` namespace for real-time endpoints (and `aws/sagemaker/ModelMonitoring/explainability-metrics` for batch transform jobs) with `Endpoint`, `MonitoringSchedule`, `ExplainabilityMethod` (value: `KernelShap`), `Label`, and `ValueType` (values: `GlobalShapValues` or `ExpectedValue`) dimensions. |
-| Remediation | 1. Create a SageMaker Clarify explainability monitoring schedule using SHAP analysis. 2. Configure feature attribution baselines. 3. Use explainability outputs to generate adverse action notices (top contributing factors for negative decisions) where your firm's use case and regulatory interpretation require them. 4. Retain explainability reports for regulatory audit. |
-| Reference | [SageMaker Clarify Explainability](https://docs.aws.amazon.com/sagemaker/latest/dg/clarify-model-explainability.html) |
-
-#### FS-42 — AI Service Cards
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.4, §1.2.5, §1.2.14] — "Amazon provides AI Service Cards for models that are pre-trained for AWS services like Amazon Bedrock and Amazon Q. These cards help you understand how Amazon addresses toxicity in each model." Referenced in three separate PDF risk sections. |
-| Description | Checks SageMaker Model Cards document intended use and bias evaluations. |
-| Detection | Calls `sagemaker:ListModelCards`. For each card, calls `sagemaker:DescribeModelCard` and inspects the content JSON for `intended_uses`, `business_details`, and `evaluation_details` sections. Flags cards missing these sections. |
-| Remediation | 1. Create a SageMaker Model Card for each production model. 2. Document: intended use cases, out-of-scope uses, training data description, bias evaluation results, performance metrics. 3. Review and update cards after each model retrain. 4. For Bedrock foundation models, reference the AWS AI Service Cards published by Amazon. |
-| Reference | [SageMaker Model Cards](https://docs.aws.amazon.com/sagemaker/latest/dg/model-cards.html), [AWS AI Service Cards](https://aws.amazon.com/ai/responsible-ai/resources/) |
-
-### Sensitive Information Disclosure (FS-43 to FS-46)
-
-> **PDF source:** §1.2.6 Sensitive information disclosure. PDF-listed mitigations:
-> (a) Bedrock Guardrails sensitive information filters for PII, PHI;
-> (b) data classification scanning and access controls on AI data sources;
-> (c) strict IAM access controls for Bedrock API;
-> (d) mask sensitive information in CloudWatch Logs and custom application logging;
-> (e) protect training and fine-tuning data via data protection best practices;
-> (f) monitor PII in training/fine-tuning/RAG data with Amazon Macie;
-> (g) remove, mask, or tokenize PII before use in training, fine-tuning, or RAG;
-> (h) Practical guidance: least privilege for agent identities; user-authorized communications
-> to tool services; propagate end-user identities so tool services can validate them without
-> revealing them to unauthorized third parties.
-
-#### FS-43 — CloudWatch Log PII Masking
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.6] — "If you implement model invocation logging for the LLM or custom logging logic in your application, make sure to mask sensitive information in your log data." References "Amazon CloudWatch – Help protect sensitive log data with masking". |
-| Description | Checks CloudWatch Logs data protection policies mask PII in Bedrock invocation logs. |
-| Detection | Identifies CloudWatch log groups used by Bedrock invocation logging (from `bedrock:GetModelInvocationLoggingConfiguration`). Calls `logs:GetDataProtectionPolicy` for each log group. Flags log groups with no data protection policy or policies missing PII identifiers. Note: model invocation logging only captures calls made through the `bedrock-runtime` endpoint (`Converse`, `ConverseStream`, `InvokeModel`, `InvokeModelWithResponseStream`); calls through other endpoints such as the Responses API (`bedrock-mantle` endpoint) are not captured. |
-| Remediation | 1. Create a CloudWatch Logs data protection policy on each Bedrock log group. 2. Include managed data identifiers using their exact ARN-based IDs — country-code suffixes are **required** in the ARN for most identifiers (the data-types table uses the short name such as `Ssn`, but the ARN must include the country code): `Ssn-US` (US Social Security Number; `Ssn-ES` for Spain — there is no bare `Ssn` ARN), `CreditCardNumber` (no suffix), `CreditCardSecurityCode` (no suffix), `EmailAddress` (no suffix), `Address` (no suffix), `PhoneNumber-US`, `BankAccountNumber-US`, `DriversLicense-US`, `PassportNumber-US`, `IndividualTaxIdentificationNumber-US`. 3. Add a `Deidentify` operation statement (no hyphen — this is the exact JSON key required in the policy document, even though AWS prose documentation uses "De-identify") to mask sensitive data, and a separate `Audit` statement to emit findings to CloudWatch. The `Deidentify` operation must contain an empty `"MaskConfig": {}` object. 4. **Retroactive masking scope:** A **log group-level** data protection policy only masks data ingested **after** the policy is applied — historical log events are not retroactively masked. However, an **account-level** data protection policy applies to both existing log groups and log groups created in the future. For maximum coverage, consider creating an account-level policy in addition to log group-level policies. Apply policies at log group creation time or as early as possible. 5. Test by sending a log entry containing sample PII and verifying it is masked in subsequent reads. |
-| Reference | [CloudWatch Logs Data Protection](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/mask-sensitive-log-data.html), [PII Data Identifier ARNs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/protect-sensitive-log-data-types-pii.html), [Financial Data Identifier ARNs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/protect-sensitive-log-data-types-financial.html) |
-
-#### FS-44 — Amazon Macie PII Scanning and Pre-Processing
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.6] — "Monitor personally identifiable information (PII) in your data when you train models, fine-tune them, or use retrieval-augmented generation (RAG)" and "Remove, mask, or tokenize personally identifiable information (PII) or sensitive data before you use it for training, fine-tuning, or retrieval-augmented generation (RAG)." |
-| Description | Verifies Macie is enabled and scanning AI/ML data buckets, and checks that a PII pre-processing step (tokenization, masking, or removal) exists in training and RAG ingestion pipelines before data reaches the model. |
-| Detection | Calls `macie2:GetMacieSession` to verify Macie is enabled. Calls `macie2:GetAutomatedDiscoveryConfiguration` to check whether automated sensitive data discovery is enabled (preferred over manual classification jobs — automated discovery evaluates S3 buckets daily without explicit job creation). Also calls `macie2:ListClassificationJobs` to check for any additional targeted jobs covering S3 buckets tagged for AI/ML use. Additionally inspects SageMaker Processing jobs or Glue jobs for PII-related naming patterns indicating a pre-processing pipeline. |
-| Remediation | 1. Enable Amazon Macie in the account. 2. **Preferred:** Enable Macie **Automated Sensitive Data Discovery** (via `macie2:UpdateAutomatedDiscoveryConfiguration` set to `ENABLED`) — this continuously evaluates ALL S3 buckets in the account or organization daily, selects representative objects, and produces sensitive-data findings without requiring manual job creation. 3. For higher-priority AI/ML buckets where you need full-depth scans, supplement with targeted classification jobs (`macie2:CreateClassificationJob`) scheduled at least weekly. 4. Implement a PII pre-processing step in your data pipeline (SageMaker Processing job, Glue job, or Lambda) that tokenizes, masks, or removes PII before data is used for training or RAG ingestion. 5. Use Amazon Comprehend `DetectPiiEntities` or Macie findings to identify PII locations and feed them into the pre-processing step. 6. Route Macie findings to EventBridge and then to your SIEM or ticketing system for timely investigation. |
-| Reference | [Amazon Macie](https://docs.aws.amazon.com/macie/latest/user/what-is-macie.html), [Macie Automated Sensitive Data Discovery](https://docs.aws.amazon.com/macie/latest/user/discovery-asdd.html), [Amazon Comprehend PII Detection](https://docs.aws.amazon.com/comprehend/latest/dg/pii.html) |
-
-#### FS-45 — Guardrail PII Filters
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.6] — "Use Amazon Bedrock Guardrails to detect and filter structured sensitive information in model inputs and outputs, such as personally identifiable information (PII), protected health information (PHI)." |
-| Description | Checks guardrails have PII entity filters for SSN, credit card, and account numbers. |
-| Detection | Calls `bedrock:GetGuardrail` and inspects `sensitiveInformationPolicy.piiEntities`. Flags guardrails missing filters for critical PII types: `US_SOCIAL_SECURITY_NUMBER`, `CREDIT_DEBIT_CARD_NUMBER`, `CREDIT_DEBIT_CARD_CVV`, `CREDIT_DEBIT_CARD_EXPIRY`, `US_BANK_ACCOUNT_NUMBER`, `US_BANK_ROUTING_NUMBER`, `PIN`, `SWIFT_CODE`, `INTERNATIONAL_BANK_ACCOUNT_NUMBER`, `US_INDIVIDUAL_TAX_IDENTIFICATION_NUMBER`, `EMAIL`, `PHONE`. |
-| Remediation | 1. Update the guardrail to add PII entity filters for all relevant types. 2. Configure separate input and output actions using the `inputAction` and `outputAction` fields: set `outputAction=ANONYMIZE` (replace with placeholder such as `{US_SOCIAL_SECURITY_NUMBER}`) so PII in model responses is masked before reaching the user; set `inputAction=BLOCK` for PII types that should never be submitted (e.g., SSN, credit card numbers). 3. Use `inputEnabled` and `outputEnabled` to selectively enable evaluation per direction — disable evaluation on a direction you don't need to reduce cost and latency. 4. **PHI coverage nuance:** The Bedrock Guardrails sensitive information filter has only limited built-in PHI entities — specifically `CA_HEALTH_NUMBER` (Canada) and `UK_NATIONAL_HEALTH_SERVICE_NUMBER` (UK). For US HIPAA PHI (for example, Medical Record Numbers, Health Plan Beneficiary Numbers, Medicare Beneficiary Identifiers), there is no built-in entity type — use `regexesConfig` (custom regex patterns) on the guardrail to detect these patterns, complemented by downstream CloudWatch Logs data protection policies (see FS-43) which have PHI identifiers under the HIPAA category. 5. **Critical limitation — tool_use outputs:** The sensitive information filter does NOT detect PII when models respond with `tool_use` (function call) output parameters via supported APIs. For FinServ agentic applications where models invoke tools and return structured function-call responses, implement application-layer PII scanning on tool outputs before they are processed or displayed. 6. **Critical limitation — invocation logs:** Guardrail PII masking applies only to content sent to and returned from the inference model. It does NOT apply to model invocation logs — the `input` field in CloudWatch Logs always contains the original, unmasked request regardless of guardrail intervention. Use CloudWatch Logs data protection policies (see FS-43) to mask PII in logs separately. Similarly, the `match` field in guardrail trace output contains the original PII value, not the masked output. 7. Test with sample inputs containing each PII type and verify both input blocking and output anonymization work as expected. |
-| Reference | [Bedrock Guardrails Sensitive Information Filters](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-sensitive-filters.html) |
-
-#### FS-46 — Data Classification Tagging
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.6] — "Implement data classification scanning and access controls on the data sources connected to your AI system to prevent disclosure of company-confidential or proprietary information." |
-| Description | Verifies AI/ML S3 buckets are tagged with data classification labels. |
-| Detection | Lists S3 buckets and filters for AI/ML-related names or tags. Calls `s3:GetBucketTagging` for each and checks for a `data-classification` tag with values like `public`, `internal`, `confidential`, `restricted`. Flags buckets missing the tag. |
-| Remediation | 1. Define a data classification taxonomy (e.g., Public, Internal, Confidential, Restricted). 2. Tag all AI/ML S3 buckets with `data-classification=`. 3. **Detective enforcement:** Create an AWS Config managed rule (`required-tags`, checks up to six tag keys at a time) to identify buckets missing the tag and trigger remediation via a custom SSM automation document (note: the AWS-managed `AWS-SetRequiredTags` automation document does NOT work as a remediation with this rule — you must author a custom Systems Manager automation document). 4. **Preventive enforcement:** Use AWS Organizations **Tag Policies** to require the `data-classification` tag key with allowed values (Public, Internal, Confidential, Restricted) across accounts — Tag Policies are preventive and complement the detective Config rule. 5. Use tag-based IAM policies (via condition keys `aws:ResourceTag/data-classification`) to restrict S3 access based on classification level. 6. Pair with Macie classification jobs (see FS-44) so that buckets automatically classified as containing sensitive data are flagged if their `data-classification` tag is missing or inconsistent with the Macie findings. |
-| Reference | [AWS Tagging Best Practices](https://docs.aws.amazon.com/tag-editor/latest/userguide/tagging.html), [AWS Config required-tags Rule](https://docs.aws.amazon.com/config/latest/developerguide/required-tags.html), [AWS Organizations Tag Policies](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_tag-policies.html) |
-
diff --git a/docs/SECURITY_CHECKS_FINSERV_PART3_APP_LAYER_AND_GAPS.md b/docs/SECURITY_CHECKS_FINSERV_PART3_APP_LAYER_AND_GAPS.md
deleted file mode 100644
index a0e2a44..0000000
--- a/docs/SECURITY_CHECKS_FINSERV_PART3_APP_LAYER_AND_GAPS.md
+++ /dev/null
@@ -1,368 +0,0 @@
-# FinServ GenAI Risk Checks — Part 3: Application-Layer Controls & Material Gaps (FS-47 to FS-69)
-
-This is **Part 3 of 3** of the FinServ GenAI security checks derived from the
-[AWS guide for Financial Services risk management of the use of Generative AI (March 2026)](https://d1.awsstatic.com/onedam/marketing-channels/website/public/global-FinServ-ComplianceGuide-GenAIRisks-public.pdf)
-(referred to throughout as "the FinServ Guide").
-
-This part covers **22 standalone checks** across 6 PDF risk categories (FS-64 is merged into upstream BR-04 — see extension note in the Material Gaps section):
-
-- **Hallucination** (FS-47 to FS-50) — §1.2.7
-- **Prompt Injection** (FS-51 to FS-54) — §1.2.8
-- **Improper Output Handling** (FS-55 to FS-58) — §1.2.13
-- **Off-Topic & Inappropriate Output** (FS-59 to FS-60) — §1.2.2
-- **Out-of-Date Training Data** (FS-61 to FS-63) — §1.2.10
-- **Additional Controls — Material Gaps** (FS-64 to FS-69) — cross-cutting checks addressing PDF-listed mitigations not covered elsewhere; *FS-64 merged into upstream*
-
-**Companion files:**
-
-- `SECURITY_CHECKS_FINSERV_PART1_INFRA_CONTROLS.md` — FS-01 to FS-26 (Unbounded, Excessive Agency, Supply Chain, Training Poisoning, Vector Weaknesses)
-- `SECURITY_CHECKS_FINSERV_PART2_GUARDRAILS_CONTENT_SAFETY.md` — FS-27 to FS-46 (Non-Compliant, Misinformation, Abusive, Biased, Sensitive Info)
-- `SECURITY_CHECKS_FINSERV_COMMON.md` — shared intro, severity rubric, validation note, upstream-overlap table
-
-Each check includes how it is **detected** (the AWS API calls or configuration inspected)
-and how a failure is **remediated** (the specific AWS actions to take).
-
-See `SECURITY_CHECKS_FINSERV_COMMON.md` for:
-
-- PDF traceability conventions (`[PDF §x.y.z]` vs `[PDF §x.y.z, extension]`)
-- Severity rubric (High / Medium / Low / Informational) — see SECURITY_CHECKS_FINSERV_SEVERITY_METHODOLOGY.md
-- Validation note and AWS service authorization references
-- Relationship to upstream SM/BR/AC checks and consolidation recommendations
-
----
-
-## FinServ GenAI Risk Checks — Part 3 content
-
-### Hallucination (FS-47 to FS-50)
-
-> **PDF source:** §1.2.7 Hallucination. PDF-listed mitigations:
-> (a) prompt engineering;
-> (b) RAG with Bedrock Knowledge Bases;
-> (c) detect hallucinations in RAG and agent-based systems;
-> (d) HITL validation for internal AI systems;
-> (e) Automated Reasoning checks in Bedrock Guardrails;
-> (f) Bedrock Guardrails contextual grounding checks with reference source and query;
-> (g) response disclaimers in customer-facing applications informing users that AI responses
-> should be verified for critical decisions.
-
-#### FS-47 — Guardrail Grounding Threshold
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.7] — "You can use Amazon Bedrock Guardrails to detect and filter hallucinations in model responses by performing contextual grounding checks when you provide a reference source and query." |
-| Description | Verifies guardrail grounding thresholds are set appropriately for financial use cases (this assessment recommends ≥ 0.7; AWS does not prescribe a specific minimum, but the valid range is 0 to 0.99). Note: contextual grounding checks are not supported for conversational chatbot use cases — only for summarization, paraphrasing, and Q&A. |
-| Detection | Calls `bedrock:GetGuardrail` and inspects `contextualGroundingPolicy.filters` for the `GROUNDING` filter type. Checks that the `threshold` value is ≥ 0.7. Flags guardrails with lower thresholds or no grounding filter. |
-| Remediation | 1. Update the guardrail to set the grounding filter threshold to at least 0.7 (this assessment recommends 0.8 for financial services to reduce hallucination risk — note: AWS does not prescribe a specific minimum, but the valid range is **0 to 0.99**; a value of 1.0 is explicitly invalid and will block all content per AWS documentation). 2. Enable the grounding filter for both the `GROUNDING` and `RELEVANCE` types. 3. Test with prompts that should and should not be grounded in the reference source — tune the threshold based on your false-positive/false-negative tolerance. 4. Monitor grounding filter invocation rates via CloudWatch using the `AWS/Bedrock/Guardrails` namespace. **Important limitation:** Contextual grounding checks support only summarization, paraphrasing, and question-answering use cases — **Conversational QA / Chatbot use cases are explicitly not supported** per AWS documentation. For FinServ chatbot deployments, use denied topics and content filters (FS-28, FS-36, FS-59) as the primary hallucination-mitigation controls instead. |
-| Reference | [Bedrock Guardrails Contextual Grounding](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-contextual-grounding-check.html) |
-
-#### FS-48 — RAG Knowledge Base
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.1, §1.2.7, §1.2.10] — "Use Retrieval-Augmented Generation (RAG) to enhance your model responses with information from trusted knowledge bases." Referenced in three separate PDF risk sections. |
-| Description | Checks active Knowledge Bases are configured for RAG grounding. |
-| Detection | Calls `ListKnowledgeBases` (via the `bedrock-agent` boto3 client; IAM action `bedrock:ListKnowledgeBases`) and checks that at least one KB exists with `status=ACTIVE`. Flags accounts with no active KBs when Bedrock models are in use (indicating responses are ungrounded). |
-| Remediation | 1. Create a Bedrock Knowledge Base with your authoritative data sources. 2. Configure the KB with an appropriate embedding model and vector store. 3. Use `RetrieveAndGenerate` API instead of direct `InvokeModel` for customer-facing use cases. 4. Sync data sources on a regular schedule. |
-| Reference | [Bedrock Knowledge Bases](https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base.html) |
-
-#### FS-49 — Hallucination Disclaimer
-
-| Field | Detail |
-|-------|--------|
-| Severity | Informational |
-| PDF ref | [PDF §1.2.7] — "Implement response disclaimers in customer-facing applications, to inform end users that AI-generated responses should be verified for critical decisions." References "AWS Well-Architected Framework Generative AI Lens - Implement guardrails to mitigate harmful or incorrect model responses". |
-| Description | Advisory: verifies application adds hallucination disclaimers to AI-generated outputs. |
-| Detection | Advisory check — inspects application Lambda environment variables for disclaimer-related settings. Checks for post-processing Lambda functions that append disclaimers. |
-| Remediation | 1. Add a standard disclaimer to all AI-generated responses: "This response is generated by AI and may contain inaccuracies. Please verify critical information independently." 2. Make the disclaimer configurable and non-removable by prompt manipulation. 3. For financial decisions, add: "This does not constitute financial advice." |
-| Reference | [AWS Well-Architected GenAI Lens](https://docs.aws.amazon.com/wellarchitected/latest/generative-ai-lens/gensec02-bp01.html) |
-
-#### FS-50 — Relevance Grounding Filters
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.2, §1.2.7] — "Use Amazon Bedrock Guardrails to detect and filter hallucinations in model responses by performing contextual grounding checks." Contextual grounding covers both `GROUNDING` and `RELEVANCE` filter sub-types. |
-| Description | Checks guardrails have relevance grounding filters to prevent off-topic responses. |
-| Detection | Calls `bedrock:GetGuardrail` and inspects `contextualGroundingPolicy.filters` for the `RELEVANCE` filter type. Flags guardrails with no relevance filter configured. |
-| Remediation | 1. Update the guardrail to enable the `RELEVANCE` contextual grounding filter. 2. Set the threshold to at least 0.7 (valid range is **0 to 0.99**; a value of 1.0 is explicitly invalid per AWS documentation). 3. This ensures responses are relevant to the user's query and the provided reference source, filtering out off-topic hallucinations. **Important limitation:** Contextual grounding checks (both `GROUNDING` and `RELEVANCE`) support only summarization, paraphrasing, and question-answering use cases — **Conversational QA / Chatbot use cases are explicitly not supported** per AWS documentation. For FinServ chatbot deployments, use denied topics (FS-59) as the primary off-topic control. |
-| Reference | [Bedrock Guardrails Contextual Grounding](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-contextual-grounding-check.html) |
-
-### Prompt Injection (FS-51 to FS-54)
-
-> **PDF source:** §1.2.8 Prompt injection. PDF-listed mitigations:
-> (a) prompt engineering best practices to avoid prompt injection;
-> (b) input validation — sanitize user input, remove special characters or use escape sequences,
-> match expected format;
-> (c) secure coding practices — parameterized queries, avoid string concatenation, minimal
-> privileges;
-> (d) security testing — regular testing for prompt injection and vulnerabilities, pentest,
-> static code analysis, DAST;
-> (e) stay updated — keep Bedrock SDK, libraries, and dependencies current;
-> (f) Bedrock Guardrails to detect and block user inputs attempting to override system
-> instructions through prompt attacks.
-
-#### FS-51 — Prompt Attack Filters
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.8] — "Use Amazon Bedrock Guardrails to detect and block user inputs that attempt to override system instructions through prompt attacks." |
-| Description | Verifies guardrails have PROMPT_ATTACK content filters enabled and are configured correctly for the Standard tier. |
-| Detection | Calls `bedrock:GetGuardrail` and inspects `contentPolicy.filters` for a filter with `type=PROMPT_ATTACK`. Flags guardrails where this filter is absent, has `inputStrength` set to `NONE` or `LOW` (note: PROMPT_ATTACK only applies to inputs — there is no `outputStrength` for this filter type), or where `contentPolicy.tier.tierName=CLASSIC` (the PROMPT_ATTACK filter in Classic tier detects jailbreaks and prompt injection; in Standard tier it additionally detects **prompt leakage** — attempts to extract system prompts or developer instructions). |
-| Remediation | 1. Ensure the guardrail is configured with the **Standard** content filters tier — prompt leakage detection (extracting system prompts/developer instructions) is available only in Standard tier; jailbreak and prompt injection detection are available in both tiers. Standard tier requires cross-Region inference to be enabled on the guardrail. You can configure Standard tier on a **new or existing guardrail**: for an existing guardrail, modify it via `UpdateGuardrail` (set `tierConfig.tierName=STANDARD` in `contentPolicyConfig` and add a `crossRegionConfig.guardrailProfileIdentifier`), or use the console by editing the guardrail and selecting Standard tier with cross-Region inference. 2. Add a `PROMPT_ATTACK` content filter with `inputStrength=HIGH`. 3. **Wrap user input in guardrail input tags when using `InvokeModel` or `InvokeModelResponseStream`** — for these APIs, PROMPT_ATTACK only evaluates content enclosed in input tags (e.g., `user text ` — the reserved prefix is `amazon-bedrock-guardrails-guardContent` and the suffix should be a unique random string per request to prevent an attacker from closing the tag and appending malicious content). Untagged content is not evaluated for PROMPT_ATTACK when using these APIs. **Note:** When using the `Converse` API, use the `guardContent` field (`GuardrailConverseContentBlock`) in user messages to scope PROMPT_ATTACK evaluation to specific content — this is the Converse API equivalent of input tags. Without `guardContent`, the guardrail evaluates ALL message content (the entire messages array). Using `guardContent` in user messages ensures only user-provided content is evaluated for prompt attacks, while system prompts and conversation history are excluded. If no `guardContent` blocks are present in messages, the guardrail evaluates everything in the messages array. 4. Test with known prompt injection patterns (role-play attacks, instruction override, delimiter injection). 5. Monitor filter invocation rates via CloudWatch guardrail metrics (`InvocationsIntervened` in the `AWS/Bedrock/Guardrails` namespace, filtered by `GuardrailPolicyType=ContentPolicy`) for trending attack patterns. |
-| Reference | [Bedrock Guardrails Prompt Attack](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-prompt-attack.html), [Safeguard tiers for guardrails](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-tiers.html), [Securing Amazon Bedrock Agents against indirect prompt injections](https://aws.amazon.com/blogs/machine-learning/securing-amazon-bedrock-agents-a-guide-to-safeguarding-against-indirect-prompt-injections/) |
-
-#### FS-52 — Bedrock SDK Version Currency
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.8] — "Stay Updated – Keep your Amazon Bedrock SDK, libraries, and dependencies current to receive the latest security patches and updates." |
-| Description | Checks Bedrock Lambda functions use current (non-deprecated) runtimes and SDK versions. |
-| Detection | Calls `lambda:ListFunctions` and filters for functions with Bedrock-related names or environment variables referencing Bedrock. Checks each function's `Runtime` against the list of deprecated Lambda runtimes. |
-| Remediation | 1. Update Lambda functions to use a currently supported runtime — as of April 2026, recommended runtimes are `python3.13` or `python3.14` for Python (both deprecation date June 30, 2029; `python3.12` remains supported through Oct 31, 2028), and `nodejs22.x` or `nodejs24.x` for Node.js (`nodejs20.x` reaches deprecation on April 30, 2026 and should not be used for new deployments). 2. Update the Bedrock SDK (boto3/botocore) to the latest version in your requirements.txt or package.json. 3. Test after upgrading to verify no breaking changes. 4. Subscribe to AWS Lambda runtime deprecation notifications via EventBridge or SNS (Lambda also surfaces runtime deprecation notices via AWS Health Dashboard and Trusted Advisor). |
-| Reference | [Lambda Runtime Deprecation Policy](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html) |
-
-#### FS-53 — WAF Injection Protection Rules
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.8, extension] — WAF SQLi and known-bad-inputs rule groups are not named in the PDF, but implement the PDF mitigation "Secure Coding Practices – use parameterized queries, avoid string concatenation for input, grant minimal access privileges" at the network edge for web-facing GenAI endpoints. |
-| Description | Verifies WAF ACLs include SQL injection (`AWSManagedRulesSQLiRuleSet`) and known-bad-inputs (`AWSManagedRulesKnownBadInputsRuleSet`) managed rule groups for GenAI endpoints. |
-| Detection | Calls `wafv2:ListWebACLs(Scope=REGIONAL)` and for each calls `wafv2:GetWebACL`. Inspects the rules list for `AWSManagedRulesSQLiRuleSet` and `AWSManagedRulesKnownBadInputsRuleSet`. Flags ACLs missing either rule group. |
-| Remediation | 1. Add `AWSManagedRulesSQLiRuleSet` to your WAF Web ACL (contains SQLi detection rules for body, URI path, cookie, and query-string components). 2. Add `AWSManagedRulesKnownBadInputsRuleSet` for known Remote Command Execution (RCE) and vulnerability-discovery patterns (e.g., Log4j, Spring Core deserialization, path traversal) — note this rule group does NOT cover XSS; XSS is in `AWSManagedRulesCommonRuleSet` (see FS-56). 3. Set both rule groups to COUNT mode initially, review logs for false positives, then switch to BLOCK. 4. Create custom rules for GenAI-specific injection patterns if needed. |
-| Reference | [AWS WAF Managed Rules](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html) |
-
-#### FS-54 — Penetration Testing Evidence
-
-| Field | Detail |
-|-------|--------|
-| Severity | Informational |
-| PDF ref | [PDF §1.2.8] — "Security Testing – Test your applications regularly for prompt injection and other security vulnerabilities. Use penetration testing, static code analysis, and dynamic application security testing (DAST)." |
-| Description | Advisory: verifies GenAI applications have been penetration tested for prompt injection and other AI-specific vulnerabilities. |
-| Detection | Advisory check — inspects resource tags for `last-pentest-date` or checks for a documented penetration testing schedule. Cannot be fully automated. |
-| Remediation | 1. Conduct penetration testing of your GenAI application at least annually and before major releases. 2. Include AI-specific test cases: prompt injection, jailbreak attempts, data extraction, system prompt leakage. 3. Use tools like Garak, PyRIT, manual red-teaming, or the **AWS Security Agent**. As of the March 2026 GA announcement, Security Agent runs from 6 AWS regions (N. Virginia, Oregon, Ireland, Frankfurt, Sydney, Tokyo) but can test targets across AWS, Azure, GCP, and on-premises environments. For multi-account FinServ deployments, Security Agent supports penetration testing on VPC resources **shared across AWS accounts in the same AWS Organization** via AWS Resource Access Manager (RAM) — enable this by launching Security Agent from a central security account and sharing VPC resources from sub-accounts via RAM. **Verify current region coverage on the [AWS Security Agent page](https://aws.amazon.com/security-agent/) before citing**, as AWS has been expanding regional availability and feature set rapidly. 4. Document findings and track remediation. 5. Tag resources with `last-pentest-date` for audit trail. |
-| Reference | [AWS Penetration Testing Policy](https://aws.amazon.com/security/penetration-testing/), [AWS Security Agent GA](https://aws.amazon.com/about-aws/whats-new/2026/03/aws-security-agent-ondemand-penetration/) |
-
-### Improper Output Handling (FS-55 to FS-58)
-
-> **PDF source:** §1.2.13 Improper output handling. PDF-listed mitigations:
-> (a) implement output validation rules against expected response format (e.g., JSON schema,
-> SQL schema);
-> (b) apply context-specific output sanitization — HTML encoding for web apps, SQL
-> parameterization for database queries, command escaping for system integrations;
-> (c) Practical guidance: treat model output as untrusted user input; use Bedrock Agents
-> action-group Lambda to implement output encoding so output text is non-executable by
-> JavaScript or Markdown.
-
-#### FS-55 — Output Validation Lambda
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.13] — "Implement output validation rules specific to the expected response format. For example, if the AI system is expected to return structured data (JSON, SQL), validate the output against the expected schema before processing." |
-| Description | Checks for Lambda functions implementing output validation/sanitization before AI responses reach downstream consumers. |
-| Detection | Calls `lambda:ListFunctions` and searches for functions with naming patterns indicating output validation (e.g., "output-valid", "sanitiz", "post-process", "response-filter"). Flags if no such functions exist. |
-| Remediation | 1. Implement a post-processing Lambda that validates AI model output before it reaches the end user or downstream system. 2. Validate output against expected schema (JSON schema validation for structured responses). 3. Strip or escape any executable content (HTML tags, JavaScript, SQL fragments). 4. Log rejected outputs for security monitoring. |
-| Reference | [AWS Well-Architected Security Pillar — Application Security](https://docs.aws.amazon.com/wellarchitected/latest/security-pillar/application-security.html), [Bedrock Prompt Injection Security](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-injection.html), [Well-Architected FSI Lens — FSISEC14 Monitor AI system outputs for security issues](https://docs.aws.amazon.com/wellarchitected/latest/financial-services-industry-lens/fsisec14.html) |
-
-#### FS-56 — XSS Prevention WAF
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.13, extension] — WAF XSS rule groups are not named in the PDF, but implement the PDF mitigation "Apply context-specific output sanitization ... apply HTML encoding for web applications" at the network edge. |
-| Description | Verifies WAF ACLs include XSS prevention rules to protect against AI-generated outputs containing malicious scripts. |
-| Detection | Calls `wafv2:GetWebACL` for each regional ACL and inspects rules for `AWSManagedRulesCommonRuleSet` (which includes the four `CrossSiteScripting_*` rules covering request body, query arguments, cookies, and URI path) or custom rules using `XssMatchStatement` on request components. Flags ACLs missing XSS protection. |
-| Remediation | 1. Add `AWSManagedRulesCommonRuleSet` to your WAF Web ACL (includes `CrossSiteScripting_COOKIE`, `CrossSiteScripting_QUERYARGUMENTS`, `CrossSiteScripting_BODY`, and `CrossSiteScripting_URIPATH` rules — all four inspect **inbound request** components). 2. `XssMatchStatement` and the CRS XSS rules inspect **request** components only (body, query string, URI path, cookies, headers). WAF does NOT inspect arbitrary response bodies for XSS — response inspection (`ResponseInspection`) is available only in `AWSManagedRulesATPRuleSet`/`AWSManagedRulesACFPRuleSet` for CloudFront-protected ACLs and only scans for configured success/failure strings. 3. To protect against XSS in **AI-generated output**, enforce output encoding at the application layer (see FS-57) — rendering raw model output in a browser without encoding is the root cause that WAF cannot mitigate after the fact. 4. Apply output encoding in your application layer as defense-in-depth. |
-| Reference | [AWS WAF XSS Protection](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html) |
-
-#### FS-57 — Output Encoding
-
-| Field | Detail |
-|-------|--------|
-| Severity | Informational |
-| PDF ref | [PDF §1.2.13] — "Apply context-specific output sanitization based on the downstream consumer. For example, apply HTML encoding for web applications, SQL parameterization for database queries, and command escaping for system integrations." Practical guidance: "Use Amazon Bedrock Agents to securely integrate with AWS native and third-party services and implement output encoding in the action group Lambda function under an Amazon Bedrock Agent. Encoding all output text presented to end-users makes it automatically non-executable by JavaScript or Markdown." |
-| Description | Advisory: verifies application encodes GenAI outputs appropriately for the rendering context (HTML, JSON, SQL). |
-| Detection | Advisory check — inspects application Lambda functions for encoding libraries or patterns (e.g., `html.escape`, `json.dumps`, `markupsafe`). Checks environment variables for encoding-related configuration. |
-| Remediation | 1. Treat all model output as untrusted user input. 2. Apply context-specific encoding: HTML encoding for web display, SQL parameterization for database queries, command escaping for system integrations. 3. Use Bedrock Agents action-group Lambda functions to implement output encoding — encoding all output text makes it non-executable by JavaScript or Markdown renderers. 4. Never render raw model output in a web page without encoding. |
-| Reference | [OWASP Output Encoding](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html) |
-
-#### FS-58 — Output Schema Validation
-
-| Field | Detail |
-|-------|--------|
-| Severity | Informational |
-| PDF ref | [PDF §1.2.13] — "Implement output validation rules specific to the expected response format. For example, if the AI system is expected to return structured data (JSON, SQL), validate the output against the expected schema before processing." |
-| Description | Checks for structured output validation in GenAI pipelines (JSON schema, XML schema, or custom validators). |
-| Detection | Inspects Step Functions state machine definitions for states that perform schema validation (e.g., `Choice` states with JSON path conditions, Lambda states with "schema" or "validate" in the name). Does not rely on API Gateway response models as a validation signal because those are used for SDK generation, not runtime validation. |
-| Remediation | 1. Define a JSON schema for expected AI output format. 2. Add a validation step in your pipeline (Lambda function or Step Functions Choice state) that rejects non-conforming outputs **before** returning the response to clients — this is the runtime enforcement point. 3. Note: API Gateway *response models* in REST APIs are used for SDK generation (user-defined data types) and documentation — they do NOT perform runtime validation of response payloads. API Gateway *request validators* only validate inbound requests against request models. To validate AI output at runtime, implement the check in Lambda/Step Functions before the response reaches API Gateway. 4. Return a safe fallback response when validation fails. 5. Log rejected outputs (without leaking sensitive content) for security monitoring. |
-| Reference | [API Gateway Request and Response Validation](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-request-validation.html) |
-
-### Off-Topic & Inappropriate Output (FS-59 to FS-60)
-
-> **PDF source:** §1.2.2 Off-topic and inappropriate output. PDF-listed mitigations:
-> (a) prompt engineering with an allowlist of approved topics aligned with business purpose;
-> (b) content filters and denied topics in Bedrock Guardrails;
-> (c) Bedrock Guardrails contextual grounding check with reference source and query;
-> (d) HITL validation for internal AI systems.
-
-#### FS-59 — Guardrail Topic Allowlist
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.2] — "Configure content filters and guardrails to restrict model responses to approved topics." The check name uses "allowlist" loosely — implementation uses denied-topic lists to block out-of-scope content. |
-| Description | Verifies guardrails restrict GenAI to on-topic financial services responses via denied topics. |
-| Detection | Calls `bedrock:GetGuardrail` and inspects `topicPolicy.topics`. Checks that denied topics exist to block off-topic conversations (e.g., politics, entertainment, medical advice). Flags guardrails with no topic restrictions. |
-| Remediation | 1. Define denied topics that are outside your business scope (e.g., "medical advice", "legal advice", "political opinions", "entertainment recommendations"). 2. Add these as denied topics in the guardrail with clear descriptions and sample phrases. 3. Test with off-topic prompts to verify they are blocked. 4. Use the system prompt to positively scope the assistant's role. |
-| Reference | [Bedrock Guardrails Topic Policies](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-denied-topics.html) |
-
-#### FS-60 — Contextual Grounding for Off-Topic
-
-| Field | Detail |
-|-------|--------|
-| Severity | Informational |
-| PDF ref | [PDF §1.2.2] — "Use prompt engineering techniques to guide the model toward appropriate topics and prevent unwanted responses. Include an allowlist of approved topics aligned with the business purpose." Use of Bedrock Prompt Management for system prompt versioning is an implementation choice. |
-| Description | Advisory: verifies system prompts explicitly scope the assistant's role to prevent off-topic responses. |
-| Detection | Advisory check — inspects Bedrock Prompt Management templates (via `ListPrompts` on the `bedrock-agent` boto3 client; IAM action `bedrock:ListPrompts`) for system prompt content that defines the assistant's role, scope, and boundaries. Flags if no prompt templates exist. |
-| Remediation | 1. Define a clear system prompt that states: the assistant's role, allowed topics, prohibited topics, and response format. 2. Use Bedrock Prompt Management to version and manage system prompts. 3. Include explicit instructions like "You are a financial services assistant. Only answer questions related to [specific topics]. Decline all other requests politely." 4. Test with boundary-case prompts. |
-| Reference | [Bedrock Prompt Management](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-management.html) |
-
-### Out-of-Date Training Data (FS-61 to FS-63)
-
-> **PDF source:** §1.2.10 Out-of-date training data. PDF-listed mitigations:
-> (a) RAG with Bedrock Knowledge Bases;
-> (b) keep knowledge bases up to date (sync data sources);
-> (c) HITL validation for internal AI systems;
-> (d) data currency disclaimers in AI system responses; source attribution via
-> RetrieveAndGenerate API for users to verify currency.
-
-#### FS-61 — Knowledge Base Sync Schedule
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.10] — "Keep your knowledge bases up to date." Automated scheduling via EventBridge operationalises this mitigation. |
-| Description | Checks EventBridge Scheduler or EventBridge rules automate KB data source sync on a regular schedule. |
-| Detection | Calls `events:ListRules` and searches for rules with targets that invoke `StartIngestionJob` (IAM action `bedrock:StartIngestionJob`) or Lambda functions that trigger KB sync. Also checks AWS Scheduler (`scheduler:ListSchedules`) for schedules targeting KB sync. Flags if no scheduled sync mechanism exists. |
-| Remediation | 1. Use **EventBridge Scheduler** (the current recommended approach — EventBridge scheduled rules are a legacy feature) to create a recurring schedule that triggers KB data source sync: create a schedule with a rate expression (e.g., `rate(1 day)`) or cron expression (e.g., `cron(0 2 * * ? *)`) targeting a Lambda function. 2. The Lambda function calls `StartIngestionJob` (IAM action `bedrock:StartIngestionJob`) for each data source. 3. Add error handling and CloudWatch alarms for failed syncs. |
-| Reference | [EventBridge Scheduler](https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html), [EventBridge Scheduled Rules (legacy)](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html) |
-
-#### FS-62 — Data Currency Disclaimer
-
-| Field | Detail |
-|-------|--------|
-| Severity | Informational |
-| PDF ref | [PDF §1.2.10] — "Include data currency disclaimers in AI system responses where appropriate. Use source attribution in RAG-based response for end users to verify currency of information." |
-| Description | Advisory: verifies application adds data currency disclaimers to AI-generated outputs. |
-| Detection | Advisory check — inspects application configuration for data-currency disclaimer settings. Checks system prompts for instructions to include data freshness information. |
-| Remediation | 1. Add a data currency disclaimer to responses: "This information is based on data available as of [date]. It may not reflect the most recent changes." 2. Use the `RetrieveAndGenerate` API's source attribution to display document dates. 3. Configure the system prompt to instruct the model to caveat time-sensitive information. |
-| Reference | [Bedrock RetrieveAndGenerate API](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent-runtime_RetrieveAndGenerate.html) |
-
-#### FS-63 — Foundation Model Lifecycle Policy
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.10, extension] — FM currency is conceptually related to "out-of-date training data" but the specific Bedrock lifecycle-status check is not named in the PDF. The PDF's "1.1.6 Monitor and improve" general guidance says "Update your foundation models when new versions become available" — this FS check operationalises that guidance. See also FS-34 (TPRM) which the PDF places under §1.2.12. |
-| Description | Checks for a model lifecycle management process and Config rules to ensure models are updated when new versions are available. |
-| Detection | Calls `config:DescribeConfigRules` and searches for rules targeting Bedrock resources. Calls `bedrock:GetFoundationModel` for each model in use and inspects `modelLifecycle.status`. Flags models with status `LEGACY` (note: the Bedrock API exposes only two lifecycle status values — `ACTIVE` and `LEGACY`; models past their `endOfLifeTime` are removed from the service entirely and return a ResourceNotFound error, so any model still reachable via the API that is not `ACTIVE` will be `LEGACY`). |
-| Remediation | 1. Create an AWS Config custom rule that flags Bedrock models with `modelLifecycle.status=LEGACY`. 2. Establish a model lifecycle policy: evaluate new model versions within 30 days of release, test in staging, migrate production within 90 days (and before the `endOfLifeTime` published in the Bedrock model lifecycle page). 3. Subscribe to AWS Bedrock model lifecycle notifications. 4. Document the policy and assign an owner. 5. **Budget planning for FinServ:** For models with EOL dates after February 1, 2026, after a minimum of 3 months in Legacy state a model enters a **public extended access period** during which the model provider may set higher pricing. The `publicExtendedAccessTime` timestamp in the `FoundationModelLifecycle` response indicates when this phase begins. Include this phase in contract-and-budget review so FinServ cost governance teams are aware of potential price changes before migrating off Legacy models. |
-| Reference | [Bedrock Model Lifecycle](https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html) |
-
-### Additional Controls — Material Gaps (FS-64 to FS-69)
-
-These checks address mitigations explicitly called out in the AWS FinServ Guide that were
-not covered by the original checks in the upstream AIML Security Assessment (BR/SM/AC).
-FS-64 is merged into upstream BR-04 (see extension note below); FS-65 to FS-69 ship as
-standalone checks.
-
-#### FS-64 — Guardrail Trace Logging → *Merged into upstream BR-04*
-
-> **Upstream extension note (do not ship as a standalone check):** The detection and remediation
-> content from FS-64 should be added as a refinement of the existing **BR-04 (Model Invocation
-> Logging)** check in the upstream repo.
->
-> **What to add to BR-04:**
-> - After verifying that `bedrock:GetModelInvocationLoggingConfiguration` shows logging is
-> enabled, additionally verify the log output captures **guardrail trace data**: when
-> guardrails are applied during inference, the invocation log contains a `guardrailTrace`
-> object with `action` (values: `INTERVENED` or `NONE`), `inputAssessments`, and
-> `outputAssessments` arrays detailing which policies were evaluated and their results.
-> - **Important logging coverage gap:** Model invocation logging only captures calls made through the `bedrock-runtime` endpoint (`Converse`, `ConverseStream`, `InvokeModel`, `InvokeModelWithResponseStream`). Calls made through the `bedrock-mantle` endpoint (e.g., the Responses API) are **not currently captured** by invocation logging. If your application uses the Responses API, implement application-level logging as a compensating control.
-> - Add a remediation note on **retention requirements**: NYDFS 23 NYCRR 500.06 explicitly
-> requires cybersecurity records for ≥ 5 years; SR 11-7 does not prescribe a specific period
-> but requires documentation be maintained for the duration of model use plus a reasonable
-> period thereafter (commonly met with 5–7 year retention per firm policy). Consult your
-> compliance and records-management team for exact requirements.
-> - Suggest creating CloudWatch Metrics filters to track guardrail intervention rates (filter
-> on `guardrailTrace.action = INTERVENED`) and applying CloudWatch Logs data protection
-> policies to mask PII in traces.
-> - PDF traceability: [PDF §1.2.1] — "Maintain audit logs of AI-generated outputs and the
-> guardrails applied to support regulatory reporting and post-incident analysis." Also
-> §1.2.9 — "Implement audit logging of all actions taken by AI agents."
->
-> **Reference:** [Bedrock Model Invocation Logging](https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html)
-
-#### FS-65 — KB Data Source S3 Event Notifications
-
-| Field | Detail |
-|-------|--------|
-| Severity | High (deleted bucket) / Medium (notifications) |
-| PDF ref | [PDF §1.2.3] — "Use integrity monitoring on knowledge base data sources to detect unauthorized modifications... For example on S3 data sources use Amazon S3 event notification to track changes to documents." **Note:** This check overlaps with FS-33; FS-33 verifies notifications are *enabled* on the bucket, while FS-65 verifies that notifications are *routed to an alerting destination* (SNS/Lambda/EventBridge rule with a target). In the final PR to aws-samples these two checks may be consolidated into a single check at the reviewer's discretion. |
-| Description | Checks that S3 event notifications on KB data-source buckets are routed to an alerting destination (EventBridge rule with SNS/Lambda target, or direct SNS/SQS/Lambda notification) — not just enabled with no consumer. |
-| Detection | Identifies KB data-source S3 buckets via `ListDataSources` and `GetDataSource` (via the `bedrock-agent` boto3 client; IAM actions `bedrock:ListDataSources` and `bedrock:GetDataSource`). For each bucket, calls `s3:GetBucketNotificationConfiguration` and checks for the presence of `EventBridgeConfiguration`, `TopicConfigurations`, `QueueConfigurations`, or `LambdaFunctionConfigurations`. Flags buckets with no notifications configured. |
-| Remediation | 1. Enable EventBridge notifications on each KB data-source bucket: `aws s3api put-bucket-notification-configuration --bucket --notification-configuration '{"EventBridgeConfiguration":{}}'`. 2. Create an EventBridge rule matching S3 event detail types `"Object Created"` and `"Object Deleted"` for the bucket (note: when S3 sends events to **EventBridge**, the event detail types are `Object Created`/`Object Deleted`; the `s3:ObjectCreated:*` and `s3:ObjectRemoved:*` wildcard names are used only for **direct** SNS/SQS/Lambda notification configurations, not for EventBridge rule patterns). 3. Route events to an SNS topic or Lambda function for alerting. 4. Integrate alerts into your security incident response workflow. |
-| Reference | [S3 EventBridge Integration](https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventBridge.html) |
-
-#### FS-66 — AgentCore End-User Identity Propagation
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.6 — Practical guidance] — "1. Implement least privilege for identities associated with agents and tool services. 2. Where supported by the tool service ensure that communications to tool services or agents are authorized by the end user. 3. Customers building their own tool services should consider propagating end-user identities separately; ensuring these identities can be validated and are not revealed to unauthorized third parties." |
-| Description | Verifies AgentCore runtimes are configured to propagate end-user identities to downstream tool services, ensuring tool calls are authorized by the originating user and not solely by the agent execution role. |
-| Detection | Calls `ListAgentRuntimes` (via the `bedrock-agentcore-control` boto3 client; IAM action `bedrock-agentcore:ListAgentRuntimes`) and inspects each runtime's `authorizerConfiguration.customJWTAuthorizer` for a `discoveryUrl` and allowed audiences/clients/scopes. Flags runtimes with no JWT authorizer (meaning inbound calls carry no verifiable end-user identity), and advises configuring outbound OAuth for downstream tool services. |
-| Remediation | 1. Configure a custom JWT inbound authorizer on each AgentCore runtime: specify `discoveryUrl`, `allowedAudience`, `allowedClients`, and optional required custom claims. 2. Propagate the end-user's identity via the `X-Amzn-Bedrock-AgentCore-Runtime-User-Id` header and JWT token in the `Authorization` header when calling downstream tool services. **Important:** Invoking `InvokeAgentRuntime` with the `X-Amzn-Bedrock-AgentCore-Runtime-User-Id` header requires the distinct IAM action `bedrock-agentcore:InvokeAgentRuntimeForUser` in addition to `bedrock-agentcore:InvokeAgentRuntime`. Only trusted principals should hold this permission — scope it to specific runtime resources with IAM resource conditions, never via wildcard. For runtimes that do not need user-id delegation, explicitly **deny** `bedrock-agentcore:InvokeAgentRuntimeForUser` to prevent the header from being accepted. Additionally, derive the user-id from the authenticated principal's context (IAM caller identity or JWT claims) rather than from arbitrary client-supplied values to prevent user impersonation, and log the relationship between the authenticated IAM principal (via CloudTrail's SigV4 context) and the `user-id` value passed. 3. Configure outbound OAuth 2.0 for agents accessing third-party resources on behalf of the user. 4. Ensure tool services validate the propagated JWT before executing actions. 5. Implement agent identity segregation: assign distinct identities to each sub-agent in multi-agent workflows so actions are separately attributable. 6. Apply a maker-checker pattern for critical financial actions — require a second agent or human to verify before execution. 7. Do not log or expose propagated identity tokens to unauthorized third parties. |
-| Reference | [Configure Inbound JWT Authorizer](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/inbound-jwt-authorizer.html), [Inbound and Outbound Auth](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-oauth.html) |
-
-#### FS-67 — Agent Financial Transaction Value Thresholds
-
-| Field | Detail |
-|-------|--------|
-| Severity | High |
-| PDF ref | [PDF §1.2.9] — "Enforce transaction value thresholds and action boundaries on agent tool calls (for example to cap financial transaction amounts)." |
-| Description | Checks AgentCore Policy Engine (attached to Gateways) or action-group Lambda functions enforce maximum transaction-value limits (e.g., cap on financial amounts an agent can initiate) to prevent runaway or unauthorized high-value transactions. |
-| Detection | (a) Calls `ListGateways` (via the `bedrock-agentcore-control` boto3 client; IAM action `bedrock-agentcore:ListGateways`) and for each inspects attached Policy Engine Cedar policies for transaction-value constraints (policies referencing amount, limit, or threshold context attributes). (b) Calls `lambda:ListFunctions` and filters for agent action-group Lambda functions. Inspects each function's environment variables for threshold-related keys (e.g., `MAX_TRANSACTION_AMOUNT`, `TRANSACTION_LIMIT`). Flags gateways and functions with no threshold configuration. |
-| Remediation | 1. Add transaction-value threshold environment variables to each agent action-group Lambda (e.g., `MAX_TRANSACTION_AMOUNT=10000`). 2. Implement threshold enforcement logic in the Lambda handler that rejects or escalates transactions exceeding the limit. 3. Author Cedar policies in the AgentCore Policy Engine that evaluate tool-call context attributes (amount, currency, tool) and deny calls exceeding defined limits. 4. Route transactions exceeding thresholds to a human-in-the-loop approval step via Step Functions callback pattern. |
-| Reference | [Policy in AgentCore](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy.html), [AgentCore Example Policies](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/example-policies.html) |
-
-#### FS-68 — API Gateway Request Body Size Limits
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.11] — "To protect your API endpoints, set maximum length limits for input requests when you use large language models (LLMs) directly or through Amazon Bedrock." |
-| Description | Verifies API Gateway REST/HTTP APIs fronting GenAI endpoints have WAF `SizeConstraintStatement` rules enforcing a maximum request body size, optionally paired with an API Gateway request-body JSON schema that bounds individual field lengths — to prevent token-exhaustion attacks via oversized prompts. |
-| Detection | Calls `apigateway:GetRestApis` and for each calls `apigateway:GetRequestValidators` to check for validators (validators enforce parameter-existence and request-body JSON schema conformance — not total body size). Calls `wafv2:GetWebACL` for associated ACLs and inspects rules for `SizeConstraintStatement` targeting the request body. Flags APIs with no WAF `SizeConstraintStatement` on body, since that is the only AWS-native mechanism that enforces a custom maximum body size in front of API Gateway. |
-| Remediation | 1. **Primary control — WAF `SizeConstraintStatement`:** Add a WAF `SizeConstraintStatement` rule on your regional Web ACL that blocks requests whose body size exceeds your maximum allowed prompt length (e.g., 32 KB). Verify that the Web ACL's `AssociationConfig.RequestBody.DefaultSizeInspectionLimit` is set high enough (16 KB default; can be increased to 32/48/64 KB) so WAF can actually inspect bodies at the size you are enforcing against — if the inspection limit is lower than the `SizeConstraintStatement` threshold, oversized requests fall through to oversize handling instead of the rule. This is the only AWS-native way to enforce a custom maximum body size before requests reach API Gateway. 2. **Secondary control — API Gateway request validation:** Add an API Gateway request validator with a request-body model (JSON schema). Request validators do **not** enforce total body size, but a JSON schema can constrain individual string fields with `maxLength` and arrays with `maxItems`, which indirectly bounds payload content. Note API Gateway REST APIs also enforce a service-level hard limit of 10 MB per request (6 MB when integrated with Lambda) that you cannot lower. 3. Set the `max_tokens` parameter in Bedrock API calls to cap output length. 4. Implement client-side token counting before submitting requests. |
-| Reference | [WAF Size Constraint](https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-type-size-constraint-match.html), [WAF Body Inspection Size Limit](https://docs.aws.amazon.com/waf/latest/developerguide/web-acl-setting-body-inspection-limit.html), [API Gateway Request Validation](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-request-validation.html) |
-
-#### FS-69 — Prompt Input Validation Function
-
-| Field | Detail |
-|-------|--------|
-| Severity | Medium |
-| PDF ref | [PDF §1.2.8] — "Input Validation – Before you send user input to Amazon Bedrock or the tokenizer, validate and sanitize it by removing special characters or using escape sequences. Make sure the input matches your expected format." |
-| Description | Checks for a Lambda function or API Gateway request validator that sanitizes user prompt input (strips special characters, enforces expected format, rejects oversized inputs) before forwarding to Bedrock, complementing WAF-level controls. |
-| Detection | Calls `lambda:ListFunctions` and searches for functions with input-validation naming patterns (e.g., "sanitiz", "validat", "input-filter", "prompt-guard", "preprocess"). Flags if no such functions exist. |
-| Remediation | 1. Implement a Lambda authorizer or pre-processing function that: strips or escapes special characters from user input; validates input against an expected format (e.g., regex allowlist); rejects inputs exceeding maximum token/character limits; logs rejected inputs for security monitoring. 2. Use parameterized prompt templates (Bedrock Prompt Management) instead of string concatenation. 3. Apply Bedrock Guardrails PROMPT_ATTACK filter as a complementary control. 4. Integrate the validation function as an API Gateway Lambda authorizer or Step Functions pre-processing step. 5. Implement schema validation for all tool interactions — validate both inputs to and outputs from tools against defined JSON schemas per AWS Prescriptive Guidance for tool integration security. 6. Enforce TLS for all remote tool communications. |
-| Reference | [Bedrock Prompt Injection Security](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-injection.html), [Security Best Practices for Tool Integration](https://docs.aws.amazon.com/prescriptive-guidance/latest/agentic-ai-frameworks/security-best-practices-for-tool-integration.html) |
-
----
-
-*See `SECURITY_CHECKS_FINSERV_COMMON.md` for the Compliance Framework Mapping table that applies to all 69 FS checks.*
diff --git a/docs/SECURITY_CHECKS_FINSERV_SEVERITY_METHODOLOGY.md b/docs/SECURITY_CHECKS_FINSERV_SEVERITY_METHODOLOGY.md
index c28fc8e..6787733 100644
--- a/docs/SECURITY_CHECKS_FINSERV_SEVERITY_METHODOLOGY.md
+++ b/docs/SECURITY_CHECKS_FINSERV_SEVERITY_METHODOLOGY.md
@@ -3,7 +3,7 @@
**Status:** Proposed (Round 3). This document is the authoritative reference for how every
FinServ (`FS-`) check is assigned a severity. It answers reviewer Finding 6 ("How are the priorities of the findings determined?") with a concrete reproducible, industry- and AWS-aligned formula — not per-check intuition.
-> Scope note: today the FinServ checks and the upstream Bedrock/SageMaker/AgentCore checks all use ad-hoc severities with **no documente methodology** (verified by inspection — the upstream `app.py` files hardcode `severity="High"|"Medium"|...` with no rationale and no rubric doc). This methodology is introduced for the FinServ checks first and is written so it can later be adopted tool-wide.
+> Scope note: today the FinServ checks and the upstream Bedrock/SageMaker/AgentCore checks all use ad-hoc severities with **no documented methodology** (verified by inspection — the upstream `app.py` files hardcode `severity="High"|"Medium"|...` with no rationale and no rubric doc). This methodology is introduced for the FinServ checks first and is written so it can later be adopted tool-wide.
---
@@ -146,8 +146,8 @@ The authoritative per-finding assignments are in
1. **Per-finding severity register.** A machine-checkable register (`severity-register.csv` or a `SEVERITY_REGISTER` dict in `app.py`) lists every `FS-` finding-name with its `I`, `L`, resulting label, and a one-line justification. This is the single source of truth.
*(FS-01 emits four finding-names under one Check_ID; the register is keyed by finding-name so Shield=Low and WAF=Medium can coexist under FS-01.)*
2. **Code matches register.** Every `create_finding(... severity=...)` must equal the register's label for that finding. A unit test enforces this (prevents future drift) — a strong guard for a public tool.
-3. **Docs match register.** The severity columns in `SECURITY_CHECKS_FINSERV_PART1/2/3.md` and the
- `Severity rubric` section of `SECURITY_CHECKS_FINSERV_COMMON.md` are regenerated/checked against
+3. **Docs match register.** The per-check severity columns and the `Severity rubric` section in
+ `SECURITY_CHECKS_FINSERV.md` are regenerated/checked against
the register. The "Advisory" tier in the existing rubric is reconciled (Advisory = the Informational disposition for non-verifiable controls).
4. **Methodology surfaced to users.** A condensed version of §2–§3 is added to the README and
linked from the FinServ report section so the methodology travels with the artifact (directly answering the reviewer).
diff --git a/docs/SECURITY_CHECKS_FINSERV_SEVERITY_REGISTER.md b/docs/SECURITY_CHECKS_FINSERV_SEVERITY_REGISTER.md
index c1d9374..50d49f6 100644
--- a/docs/SECURITY_CHECKS_FINSERV_SEVERITY_REGISTER.md
+++ b/docs/SECURITY_CHECKS_FINSERV_SEVERITY_REGISTER.md
@@ -1,7 +1,7 @@
# FinServ Severity Register (authoritative)
This register is the **single source of truth** for the severity of every FinServ finding. It is
-derived by applying [`severity-methodology.md`](./severity-methodology.md) (Likelihood × Impact →
+derived by applying [`SECURITY_CHECKS_FINSERV_SEVERITY_METHODOLOGY.md`](./SECURITY_CHECKS_FINSERV_SEVERITY_METHODOLOGY.md) (Likelihood × Impact →
ASFF label; §3.4 disposition rules; §3.5 family bands) to the **164 `create_finding` rows / 65
check IDs** extracted from `finserv_assessments/app.py`.
diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md
index 02251d5..09e29bb 100644
--- a/docs/TROUBLESHOOTING.md
+++ b/docs/TROUBLESHOOTING.md
@@ -23,7 +23,7 @@ This guide covers common issues, debugging tips, and frequently asked questions
**Solutions:**
- Check that service-linked roles exist for AWS CloudFormation StackSets
-- Verify the management account has AWS Organizations permissions
+- Verify the account running the central CodeBuild project has AWS Organizations permissions, if it is discovering accounts automatically
- Verify target OUs contain active accounts
- Review the StackSet operation history for specific error messages
@@ -34,7 +34,7 @@ This guide covers common issues, debugging tips, and frequently asked questions
**Solutions:**
- Verify the `AIMLSecurityMemberRole` exists in target accounts
- Check the trust relationship allows the central CodeBuild role
-- Confirm the `ManagementAccountID` parameter matches your management account
+- Confirm the `ManagementAccountID` parameter matches the account where the central `MultiAccountCodeBuildRole` runs
- Verify the StackSet deployment completed successfully in all accounts
### 3. AWS SAM Deployment Failures
@@ -43,9 +43,11 @@ This guide covers common issues, debugging tips, and frequently asked questions
**Solutions:**
- Check CodeBuild logs in CloudWatch for specific errors
-- Verify the S3 bucket for SAM artifacts exists and is accessible
+- Verify the GitHub repository URL and `GitHubBranch` parameter point to a branch, tag, or commit that CodeBuild can clone
+- Verify the S3 bucket for SAM artifacts exists and is accessible. If the `aws-sam-cli-managed-default` stack is stuck in `ROLLBACK_COMPLETE` or `DELETE_FAILED`, delete it and re-run CodeBuild
- Look for IAM permission errors in the logs
- Check if a previous deployment left orphaned resources
+- Check whether `TARGET_REGIONS` failed validation. It must be empty, `all`, or a comma- or space-separated list such as `us-east-1,us-west-2` or `us-east-1 us-west-2`
### 4. AWS Step Functions Execution Failures
@@ -54,8 +56,9 @@ This guide covers common issues, debugging tips, and frequently asked questions
**Solutions:**
- Monitor state machine executions in each account
- Check Lambda function logs for errors
-- Verify Lambda has sufficient timeout (default 10 minutes)
+- Verify Lambda has sufficient timeout. Most assessment Lambdas default to 10 minutes; FinServ has its own timeout in the SAM templates
- Verify AWS IAM permissions allow Lambda to access required services
+- In multi-region scans, review each region's Map state iteration. A single service branch can be marked incomplete while the state machine still generates a report for the remaining services and regions
### 5. EarlyValidation::ResourceExistenceCheck Error
@@ -64,25 +67,72 @@ This guide covers common issues, debugging tips, and frequently asked questions
**Cause:** A resource with the same physical name already exists outside of CloudFormation management, typically from a failed deployment.
**Solution:**
+The versioned bucket cleanup command requires `jq`.
+
```bash
-# Find the orphaned bucket
+# Find likely orphaned buckets
aws s3 ls | grep aiml-security
-# Empty the bucket
-aws s3 rm s3:// --recursive
+BUCKET_NAME=""
+
+aws s3 rm "s3://${BUCKET_NAME}" --recursive
+
+while true; do
+ delete_payload=$(aws s3api list-object-versions \
+ --bucket "${BUCKET_NAME}" \
+ --output json \
+ | jq '{Objects: (((.Versions // []) + (.DeleteMarkers // [])) | map({Key, VersionId}) | .[0:1000])}')
+
+ object_count=$(echo "${delete_payload}" | jq '.Objects | length')
+ if [ "${object_count}" -eq 0 ]; then
+ break
+ fi
-# Delete version markers if versioned
-aws s3api delete-objects --bucket --delete \
- "$(aws s3api list-object-versions --bucket \
- --query '{Objects: Versions[].{Key:Key,VersionId:VersionId}}')"
+ aws s3api delete-objects \
+ --bucket "${BUCKET_NAME}" \
+ --delete "${delete_payload}"
+done
-# Delete the bucket
-aws s3 rb s3://
+aws s3 rb "s3://${BUCKET_NAME}"
# Re-run the CodeBuild project
```
-### 6. CodeBuild Timeout or Out-of-Memory with Many Accounts
+For full bucket cleanup guidance, see [Cleanup Guide](CLEANUP.md#emptying-and-deleting-versioned-s3-buckets).
+
+### 6. Financial Services Checks Do Not Appear in the Report
+
+**Symptoms:** The report does not include the **Financial Services** section or any `FS-` findings.
+
+**Solutions:**
+- Confirm the infrastructure stack parameter `EnableFinServAssessment` is set to `true`
+- Confirm CodeBuild logs show `FinServ assessment enabled is true`
+- If you updated an existing deployment, re-run the CodeBuild project after the CloudFormation update completes. The parameter is passed to the Step Functions execution input when CodeBuild starts the assessment
+- Use the CodeBuild-based deployment templates for normal operation. If you deploy the SAM template directly and start Step Functions manually, include `"enableFinServ": "true"` in the `StartExecution` input
+- Check the Step Functions execution for the `FinServ Enabled?` choice state and `FinServ Security Assessment` task
+
+### 7. TargetRegions Validation or Unexpected Region Coverage
+
+**Symptoms:** CodeBuild fails with a `TARGET_REGIONS` validation error, or the report scans fewer or different regions than expected.
+
+**Solutions:**
+- Leave `TargetRegions` empty to scan only the deployment region
+- Use `all` to scan the union of regions returned by boto3 for Amazon Bedrock, Amazon SageMaker AI, and Amazon Bedrock AgentCore
+- Use a comma- or space-separated list, such as `us-east-1,us-west-2,eu-west-1` or `us-east-1 us-west-2 eu-west-1`. The deployment normalizes the value before passing it to SAM
+- Confirm the services being assessed are available in each target region. If a service is unavailable or has no resources in a region, the report can show `N/A` or no resource-specific findings for that service and region
+- Confirm the account is opted in to any opt-in regions you include
+
+### 8. CodeBuild Source or GitHub Branch Failures
+
+**Symptoms:** CodeBuild fails before SAM build, or the logs show repository clone/source errors.
+
+**Solutions:**
+- Confirm `GitHubRepoUrl` is reachable by CodeBuild
+- Confirm `GitHubBranch` is a valid branch, tag, or commit
+- If you use a fork or feature branch, make sure the infrastructure stack points at that branch before starting CodeBuild
+- For private repositories, configure CodeBuild source credentials before deployment or use a source location CodeBuild can access
+
+### 9. CodeBuild Timeout or Out-of-Memory with Many Accounts
**Symptoms:** CodeBuild job times out or runs slowly when scanning a large number of accounts concurrently.
@@ -98,17 +148,51 @@ aws s3 rb s3://
- For organizations with fewer than 10 accounts, the default "Three" is usually sufficient
- If builds timeout, increase `ConcurrentAccountScans` to process more accounts in parallel -- but be aware this also increases the per-minute CodeBuild cost
- If builds timeout even at "Twelve," increase the `CodeBuildTimeout` parameter (default is 300 minutes for multi-account)
-- For very large organizations (100+ accounts), consider using `MultiAccountListOverride` to split assessments into batches
+- For very large organizations (100+ accounts), consider using `MultiAccountListOverride` to split assessments into batches. It accepts comma- or space-separated account IDs
-### 7. No Reports in S3 Bucket
+### 10. No Reports in S3 Bucket
**Symptoms:** Assessment completes but no HTML/CSV files appear.
**Solutions:**
1. **Wrong bucket**: Use the bucket from the **Infrastructure Stack** outputs, not the assessment stack
-2. **Still running**: Check CodeBuild console - assessment typically takes 5-10 minutes
-3. **Wrong prefix**: Look under `{account_id}/` for single-account, `consolidated-reports/` for multi-account
-4. **Permissions**: Check CloudWatch Logs for Lambda execution errors
+2. **Still running**: Check CodeBuild console. Multi-region, multi-account, or FinServ-enabled assessments can take longer than a single-region run
+3. **Wrong prefix**: Look under `{account_id}/` for per-account reports and `consolidated-reports/` for the multi-account consolidated HTML report
+4. **Post-build copy failed**: In multi-account mode, CodeBuild copies CSV/HTML files from each account's SAM assessment bucket into the central infrastructure bucket. Search CodeBuild logs for `Copying files from`, `Failed to list bucket contents`, or `No files to copy`
+5. **Permissions**: Check CloudWatch Logs for Lambda execution errors and CodeBuild logs for S3 sync errors
+
+### 11. Confused by Multiple CloudFormation Stacks
+
+**Symptoms:** You see multiple stacks and aren't sure which one has your results.
+
+**Explanation:** The deployment creates an infrastructure stack and one or more SAM assessment stacks. The infrastructure stack is the user-facing stack for report access.
+
+| Stack Type | How to Identify | What to Do |
+|------------|-----------------|------------|
+| **Infrastructure Stack** (yours) | The name you chose (for example, `aiml-security-single-account`) | Use this — go to Outputs tab, copy `AssessmentBucket` |
+| **Assessment Stack** (auto-generated) | `aiml-sec-{account_id}` (single), `aiml-security-{account_id}` (multi), or `aiml-security-mgmt` | Internal execution stack. Its `AssessmentBucketName` output is useful for debugging raw per-account reports and retained buckets |
+
+**Quick Check**: If a stack name starts with `aiml-sec-` or `aiml-security-` followed by numbers (or `aiml-security-mgmt`), it's auto-generated. Look for the name you chose during deployment.
+
+### 12. Upgrading an Existing Deployment to Multi-Region
+
+**Symptoms:** You have an existing single-region deployment and want to enable multi-region scanning.
+
+**Solution:** Update your existing CloudFormation stack — no teardown required.
+
+1. Navigate to **AWS CloudFormation** > **Stacks**
+2. Select your infrastructure stack (for example, `aiml-security-single-account` or `aiml-security-multi-account`)
+3. Click **Update** > **Use current template**
+4. Set the `TargetRegions` parameter (for example, `us-east-1,us-west-2,eu-west-1`, `us-east-1 us-west-2 eu-west-1`, or `all`)
+5. Click through to **Submit**
+6. The next assessment run will scan the specified regions in parallel
+
+**What happens during the upgrade:**
+- CloudFormation updates the `TARGET_REGIONS` environment variable on the CodeBuild project in the infrastructure stack
+- On the next CodeBuild run, SAM deploy updates the assessment stack and passes the new `TargetRegions` value into the assessment Lambdas and state machine
+- The Step Functions `Resolve Target Regions` state resolves the region list, then the Map state scans those regions in parallel
+- No data is lost — the S3 bucket retains all previous reports
+- Fully backward compatible — leaving `TargetRegions` empty preserves single-region behavior
---
@@ -133,12 +217,12 @@ The trust policy should allow the central CodeBuild role:
{
"Effect": "Allow",
"Principal": {
- "AWS": "arn:aws:iam:::root"
+ "AWS": "arn:aws:iam:::root"
},
"Action": "sts:AssumeRole",
"Condition": {
"ArnEquals": {
- "aws:PrincipalArn": "arn:aws:iam:::role/service-role/MultiAccountCodeBuildRole"
+ "aws:PrincipalArn": "arn:aws:iam:::role/service-role/MultiAccountCodeBuildRole"
}
}
}
@@ -146,12 +230,16 @@ The trust policy should allow the central CodeBuild role:
### Check S3 Bucket Permissions
-Verify the bucket policy allows cross-account writes for multi-account deployments:
+The central infrastructure bucket is the user-facing report bucket. In multi-account mode, member accounts do not write directly to this bucket. CodeBuild assumes into each account, copies report files from that account's SAM assessment bucket, then uploads them to the central infrastructure bucket.
+
+Verify the central bucket policy and CodeBuild S3 permissions if report upload fails:
```bash
-aws s3api get-bucket-policy --bucket
+aws s3api get-bucket-policy --bucket
```
+If per-account reports are missing, also check the SAM assessment stack's `AssessmentBucketName` output for that account and confirm the files were created there.
+
### Monitor AWS Step Functions Executions
1. Navigate to **AWS Step Functions** in the target account
@@ -167,7 +255,9 @@ aws s3api get-bucket-policy --bucket
**Q: Does this assessment make any changes to my AWS resources?**
-A: No. All security checks are **read-only**. The framework only queries your resources to evaluate their configurations. It does not create, modify, or delete any of your AI/ML workloads or data.
+A: The security checks do not modify your AI/ML workloads or data. They query resource configuration and write assessment artifacts to framework-owned S3 buckets.
+
+The framework itself does create and manage its own deployment resources, including CloudFormation stacks, IAM roles, Lambda functions, Step Functions state machines, CodeBuild projects, S3 buckets, EventBridge rules, and optional SNS notifications. At the start of each assessment run, it also cleans old objects from its own SAM assessment bucket before writing the new report artifacts.
**Q: How long does an assessment take to run?**
@@ -190,7 +280,7 @@ You can automate regular assessments using Amazon EventBridge scheduled rules.
**Q: What AWS regions are supported?**
-A: The framework supports all standard AWS commercial regions where Amazon Bedrock, Amazon SageMaker AI, or Amazon Bedrock AgentCore are available. AWS GovCloud and AWS China regions may require template modifications.
+A: The framework is designed for standard AWS commercial regions where Amazon Bedrock, Amazon SageMaker AI, or Amazon Bedrock AgentCore are available. Leave `TargetRegions` empty for the deployment region, set it to `all` to resolve the union of assessed-service regions, or provide an explicit comma- or space-separated list. AWS GovCloud and AWS China regions may require template modifications.
**Q: Does this work if I don't have any AI/ML resources deployed yet?**
@@ -233,7 +323,7 @@ A: Minimal ongoing costs:
**Q: Can I customize which security checks are included?**
-A: Currently, all 52 checks run by default to provide comprehensive coverage. You can filter results in the generated HTML reports by severity, status, or service. Future versions may support selective check execution.
+A: Currently, all 52 core checks run by default to provide comprehensive coverage. If `EnableFinServAssessment` is enabled, the 64 optional Financial Services GenAI risk checks also run. You can filter results in the generated HTML reports by severity, status, service, industry, or region. Future versions may support selective check execution.
**Q: Can I add custom security checks?**
@@ -259,9 +349,15 @@ aws events put-rule \
aws events put-targets \
--rule "WeeklyAIMLAssessment" \
- --targets "Id"="1","Arn"="arn:aws:codebuild:region:account:project/your-project"
+ --targets '[{
+ "Id": "1",
+ "Arn": "arn:aws:codebuild:::project/",
+ "RoleArn": "arn:aws:iam:::role/"
+ }]'
```
+The target role must trust `events.amazonaws.com` and allow `codebuild:StartBuild` on the assessment CodeBuild project. For new schedules, Amazon EventBridge Scheduler is also a good option.
+
---
### Troubleshooting Questions
@@ -270,9 +366,10 @@ aws events put-targets \
A: Common causes:
1. **Wrong bucket**: Verify you're looking at the bucket from the **Infrastructure Stack** outputs (not the assessment stack)
-2. **Still running**: Check AWS CodeBuild console - the assessment may still be in progress (typically takes 5-10 minutes)
-3. **Permissions issue**: Check AWS CloudWatch Logs for AWS Lambda execution errors
-4. **Wrong prefix**: Look under `{account_id}/` prefix for single-account, `consolidated-reports/` for multi-account
+2. **Still running**: Check AWS CodeBuild console. Multi-region, multi-account, or FinServ-enabled assessments can take longer than a single-region run
+3. **Wrong prefix**: Look under `{account_id}/` for per-account reports and `consolidated-reports/` for the multi-account consolidated HTML report
+4. **Post-build copy failed**: Search CodeBuild logs for `Copying files from`, `Failed to list bucket contents`, `No files to copy`, or S3 sync errors
+5. **Permissions issue**: Check AWS CloudWatch Logs for Lambda errors and CodeBuild logs for S3 access errors
**Q: I see "Access Denied" errors in the AWS CodeBuild logs.**
@@ -289,8 +386,10 @@ A: Performance factors:
- **Number of resources**: Accounts with hundreds of Amazon SageMaker notebooks or Amazon Bedrock models take longer
- **API throttling**: AWS API rate limits may slow down assessments in large environments
- **Concurrent executions**: Multi-account assessments run in parallel (configurable through the `ConcurrentAccountScans` parameter)
+- **Region scope**: Multi-region scans multiply the amount of service inventory collected
+- **Financial Services checks**: Enabling `EnableFinServAssessment` adds the optional `FS-` checks and can increase run time
-If assessments consistently timeout, increase the AWS Lambda timeout in the AWS SAM template or reduce concurrent account scans.
+If assessments consistently timeout, increase `CodeBuildTimeout`, reduce `TargetRegions`, reduce the account batch size with `MultiAccountListOverride`, or lower concurrency if throttling is the bottleneck. Lambda timeout changes require editing the SAM templates.
---
@@ -303,14 +402,14 @@ A: All assessment data remains **entirely within your AWS account**:
- Logs in **your Amazon CloudWatch Logs** (configurable retention)
- No data is sent to external services or third parties
-**Q: What IAM permissions does the assessment role need?**
+**Q: What IAM permissions does the framework need?**
-A: The framework uses **read-only permissions** only:
-- AI/ML services: `List*`, `Describe*`, `Get*` actions
-- AWS IAM: Read permissions for policy analysis
-- Supporting services: AWS CloudTrail, Amazon GuardDuty, Amazon VPC (read-only)
+A: The framework uses multiple roles, and only the Lambda runtime roles are close to read-only:
+- **CodeBuild orchestration roles** (`CodeBuildRole`, `MultiAccountCodeBuildRole`) need deployment permissions to build SAM, create or update stacks, and start Step Functions executions.
+- **`AIMLSecurityMemberRole`** in the target account is also not read-only in the multi-account flow, because it must allow the central CodeBuild project to deploy or update the per-account SAM stack before the assessment runs.
+- **Assessment Lambda execution roles** are primarily read-oriented. They use AI/ML service `List*`, `Describe*`, and `Get*` APIs plus supporting read APIs, and S3 access to read the IAM cache and write reports.
-See the main [README](../README.md#permissions-required) for the complete permission list.
+See [README - Permissions Required](../README.md#permissions-required) for the role breakdown and the template files that define each policy.
**Q: Is this assessment sufficient for compliance requirements (SOC 2, HIPAA, and similar)?**
diff --git a/sample-reports/dashboard-overview-dark.png b/sample-reports/dashboard-overview-dark.png
index bf06768..f868852 100644
Binary files a/sample-reports/dashboard-overview-dark.png and b/sample-reports/dashboard-overview-dark.png differ
diff --git a/sample-reports/dashboard-overview-light.png b/sample-reports/dashboard-overview-light.png
index 028b824..f55f455 100644
Binary files a/sample-reports/dashboard-overview-light.png and b/sample-reports/dashboard-overview-light.png differ
diff --git a/sample-reports/findings-table.png b/sample-reports/findings-table.png
index 21f2ed3..28abffe 100644
Binary files a/sample-reports/findings-table.png and b/sample-reports/findings-table.png differ
diff --git a/sample-reports/finserv_security_report_phase3.csv b/sample-reports/finserv_security_report_phase3.csv
deleted file mode 100644
index bfa3882..0000000
--- a/sample-reports/finserv_security_report_phase3.csv
+++ /dev/null
@@ -1,190 +0,0 @@
-Check_ID,Finding,Finding_Details,Resolution,Reference,Severity,Status,Compliance_Frameworks
-FS-01,AWS Shield Advanced Not Enabled,AWS Shield Advanced is not subscribed. GenAI API endpoints are vulnerable to volumetric DDoS attacks that can exhaust token quotas and inflate costs.,"1. Subscribe to AWS Shield Advanced for DDoS protection.
-2. After subscribing, explicitly add resource protections in the Shield Advanced console for each Bedrock-facing resource (API Gateway stages, ALBs, CloudFront distributions, Route 53 hosted zones). Shield Advanced subscription alone does NOT automatically protect resources — each resource must be individually added to receive protection.
-3. Enable Shield Response Team (SRT) access and configure proactive engagement.
-4. Alternatively, use AWS Firewall Manager with a Shield Advanced policy to automate resource protection based on tags or resource types.",https://docs.aws.amazon.com/waf/latest/developerguide/shield-chapter.html,High,Failed,FFIEC CAT | DORA Art.6
-FS-01,Regional WAF Web ACLs Present,Found 1 regional WAF Web ACL(s).,Verify ACLs are associated with Bedrock-facing endpoints.,https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html,Informational,Passed,FFIEC CAT | DORA Art.6
-FS-02,API Gateway Rate Limiting Configured,All 1 usage plan(s) have throttle settings.,No action required.,https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html,Informational,Passed,FFIEC CAT | DORA Art.6 | PCI-DSS 12.3.2
-FS-03,Bedrock Token Quotas At Default,"All 224 Bedrock token-based quota(s) are at their AWS default values — no quota increase has been applied. Running at default is a legitimate posture, but it should be a reviewed decision aligned with expected peak load rather than an oversight.","1. Review current Bedrock TPM/TPD quotas in the Service Quotas console.
-2. Request increases aligned with expected peak load, or document a deliberate decision to remain at default after review.
-3. Implement client-side token counting and pre-flight quota checks.
-4. Use Bedrock cross-region inference profiles to distribute load.",https://docs.aws.amazon.com/bedrock/latest/userguide/quotas.html,Medium,N/A,FFIEC CAT | SR 11-7
-FS-04,No Cost Anomaly Detection Monitors,"No AWS Cost Anomaly Detection monitors found. Unexpected spikes in Bedrock/SageMaker usage (e.g., from prompt injection loops) will go undetected.","1. Create a Cost Anomaly Detection monitor scoped to AWS/Bedrock and AWS/SageMaker.
-2. Configure alert subscriptions (SNS/email) for anomalies above threshold.
-3. Set daily spend budgets with AWS Budgets as a secondary control.",https://docs.aws.amazon.com/cost-management/latest/userguide/getting-started-ad.html,Medium,Failed,FFIEC CAT | SR 11-7
-FS-05,No Bedrock CloudWatch Alarms Found,No CloudWatch alarms found for Bedrock metrics. Token exhaustion and throttling events will not trigger operational alerts.,"Create CloudWatch alarms for:
-- AWS/Bedrock InvocationThrottles (threshold > 0)
-- AWS/Bedrock TokensProcessed (threshold based on quota)
-- Custom application-level token counters via EMF",https://docs.aws.amazon.com/bedrock/latest/userguide/monitoring-cw.html,Medium,Failed,FFIEC CAT | DORA Art.6
-FS-06,No AI/ML Service Budgets Configured,No AWS Budgets found scoped to Bedrock or SageMaker. Unbounded GenAI spend can go undetected until the monthly bill.,"1. Create cost budgets for AWS Bedrock and SageMaker with 80%/100% alert thresholds.
-2. Add SNS notifications to on-call channels.
-3. Consider budget actions to apply IAM deny policies when thresholds are breached.",https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-managing-costs.html,Medium,Failed,FFIEC CAT | SR 11-7
-FS-07,Agent Action Boundaries Look Appropriate,Reviewed 4 agent(s); no wildcard sensitive actions found.,No action required.,https://docs.aws.amazon.com/bedrock/latest/userguide/agents-permissions.html,Informational,Passed,SR 11-7 | FFIEC CAT
-FS-08,AgentCore Runtimes Missing Policy Engine,"Runtimes without authorizer configuration: vendoriq_vendoriq, test_minimal, strands_claude_short_timeout, strands_claude_getting_started, proposal_drafter, harness_harness_ctuux, executive_summary_writer, document_generation, datetimemcp_Agent. Without a policy engine, agents can invoke any registered tool without authorization checks.",Configure an authorizer (Lambda or Cedar policy store) on each AgentCore runtime to enforce fine-grained tool-call authorization.,https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/security-authorization.html,High,Failed,SR 11-7 | MAS TRM 9.1
-FS-09,Agent Lambda Functions Without Concurrency Limits,"Agent-related Lambda functions without reserved concurrency: Dev-DashboardStack-DashboardApiLayerAgentSessionsF-EnQLRGDeLRkn, fedpoint-kms-poc-create-bedrock-index, Dev-DashboardStack-DashboardApiLayerAgentInvokeFn3-r3fQdrJuboTX. Unlimited concurrency allows runaway agent loops to exhaust account limits.","1. Set reserved concurrency on agent Lambda functions.
-2. Implement maximum iteration counts in agent orchestration logic.
-3. Use Step Functions with MaxConcurrency and timeout states.
-4. Add circuit-breaker patterns to agent tool invocations.",https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html,Medium,Failed,FFIEC CAT | SR 11-7
-FS-10,Human-in-the-Loop Check — No Agent Workflows Found,"No Step Functions state machines with agent/approval naming found. Verify that high-risk agent actions (e.g., fund transfers, account changes) have human approval gates.",Implement Step Functions .waitForTaskToken patterns for high-risk agent actions. Route approval requests to human reviewers via SNS/SES/Slack.,https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#connect-wait-token,Medium,N/A,SR 11-7 | FFIEC CAT | MAS TRM 9.2
-FS-11,No Agent Rate Alarms Found,No CloudWatch alarms found for agent invocation rates. Looping or runaway agents will not trigger operational alerts.,"Create CloudWatch alarms on:
-- Bedrock agent invocation counts (threshold based on expected max)
-- Lambda invocation errors for agent functions
-- Step Functions execution failures and timeouts",https://docs.aws.amazon.com/bedrock/latest/userguide/monitoring-cw.html,Medium,Failed,FFIEC CAT | DORA Art.6
-FS-12,Bedrock SCPs Found,SCPs referencing Bedrock: InnovationSandboxAwsNukeSupportedServicesScp.,Verify SCPs use bedrock:ModelId conditions to allowlist approved models.,https://docs.aws.amazon.com/bedrock/latest/userguide/security_iam_id-based-policy-examples.html,Informational,Passed,SR 11-7 | FFIEC CAT | ISO 27001 A.15.2
-FS-13,Model Provenance Tags Present,All reviewed models have required provenance tags.,No action required.,https://docs.aws.amazon.com/bedrock/latest/userguide/tagging.html,Informational,Passed,SR 11-7 | ISO 27001 A.12.5 | FFIEC CAT
-FS-14,No Model Governance Config Rules Found,No AWS Config rules found for Bedrock model governance. Unapproved models may be deployed without detection.,"1. Create custom AWS Config rules to detect use of non-approved Bedrock models.
-2. Use AWS Service Catalog to publish approved model configurations.
-3. Implement a model risk management (MRM) process per SR 11-7.",https://docs.aws.amazon.com/config/latest/developerguide/evaluate-config.html,Medium,Failed,SR 11-7 | FFIEC CAT | ISO 27001 A.15.1
-FS-15,No Bedrock Evaluation Jobs Found,No Bedrock Model Evaluation jobs found. Models have not been evaluated for adversarial robustness.,"1. Run Bedrock Model Evaluation with adversarial/red-team datasets.
-2. Use FMEval library for automated robustness testing.
-3. Schedule periodic re-evaluation after model updates.",https://docs.aws.amazon.com/bedrock/latest/userguide/model-evaluation.html,Medium,N/A,SR 11-7 | FFIEC CAT | MAS TRM 9.3
-FS-16,ECR Repositories Without Image Scanning,"5 ECR repo(s) without scan-on-push: bedrock-agentcore-strands_claude_getting_started, bedrock-agentcore-strands_claude_short_timeout, cdk-hnb659fds-container-assets-123456789012-us-east-1, bracket-predictor, vendor-iq-dashboard.",Enable scan-on-push for all ECR repositories containing model containers. Consider enabling Enhanced Scanning (Inspector) for CVE detection.,https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning.html,High,Failed,ISO 27001 A.12.6 | FFIEC CAT | DORA Art.6
-FS-20,No SageMaker Feature Groups Found,No SageMaker Feature Store groups found.,No action required.,https://docs.aws.amazon.com/sagemaker/latest/dg/feature-store.html,Informational,N/A,SR 11-7 | FFIEC CAT
-FS-21,Training Data Buckets Without Versioning,"4 training data bucket(s) without versioning: bedrock-agentcore-codebuild-sources-123456789012-us-east-1, bedrock-agentcore-runtime-123456789012-us-east-1-2h09526czr, sagemaker-studio-123456789012-vs7m9hvz7np, sagemaker-us-east-1-123456789012.",Enable S3 versioning on all training data buckets. Consider enabling MFA Delete for additional protection against poisoning.,https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html,High,Failed,SR 11-7 | ISO 27001 A.12.3 | FFIEC CAT
-FS-22,Knowledge Base IAM Permissions Look Appropriate,No wildcard KB permissions found in reviewed roles.,No action required.,https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam-awsmanpol.html,Informational,Passed,NYDFS 500 | FFIEC CAT | PCI-DSS 12.3.2
-FS-24,Knowledge Base Metadata Filtering — Manual Review Required,"Found 2 Knowledge Base(s). Verify that metadata attributes (e.g., tenantId, classification) are indexed and that Retrieve calls include RetrievalFilter conditions for tenant isolation.","1. Add metadata fields (tenantId, dataClassification) to KB data sources.
-2. Pass RetrievalFilter in all Retrieve/RetrieveAndGenerate calls.
-3. Validate filters in integration tests to prevent cross-tenant data leakage.",https://docs.aws.amazon.com/bedrock/latest/userguide/kb-test-config.html,Medium,Passed,NYDFS 500 | FFIEC CAT | PCI-DSS 12.3.2
-FS-25,OpenSearch Serverless Encryption Policies Present,Found 5 encryption policy(ies); 5 use a customer-managed KMS key.,Verify all vector store collections use customer-managed KMS keys.,https://docs.aws.amazon.com/opensearch-service/latest/developerguide/serverless-encryption.html,Informational,Passed,NYDFS 500 | PCI-DSS 3.5 | FFIEC CAT
-FS-26,OpenSearch Serverless Collections Not VPC-Restricted,Found 5 network policy(ies) but none restrict to VPC. Vector stores may be accessible from the public internet.,Update network policies to allow access only from VPC endpoints. Create an OpenSearch Serverless VPC endpoint in your VPC.,https://docs.aws.amazon.com/opensearch-service/latest/developerguide/serverless-network.html,High,Failed,NYDFS 500 | FFIEC CAT | PCI-DSS 1.3
-FS-27,No Guardrails — Contextual Grounding Not Applicable,No Bedrock Guardrails configured. Configure guardrails first (see BR-05).,Configure Bedrock Guardrails with contextual grounding checks (grounding threshold ≥0.7 and relevance threshold ≥0.7 for FinServ use cases).,https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-grounding.html,Medium,N/A,SR 11-7 | FFIEC CAT | MAS TRM 9.2
-FS-27,No Automated Reasoning Policies Found,"No Bedrock Automated Reasoning policies have been created. ARC (GA August 2025) uses formal verification to guarantee that GenAI outputs comply with authored business rules — e.g., loan criteria, regulatory thresholds, policy constraints. Without ARC policies, factual accuracy of outputs is not formally verified, only heuristically filtered by contextual grounding thresholds.","1. In the Amazon Bedrock console → Guardrails → Automated Reasoning, create a policy document encoding your FinServ business rules (e.g., eligibility criteria, rate limits, regulatory thresholds).
-2. Associate the ARC policy with your guardrail (automatedReasoningPolicy.policies field in CreateGuardrail/UpdateGuardrail).
-3. Set confidenceThreshold on the policy to control strictness.
-4. ARC requires cross-Region inference — ensure your guardrail has a guardrailProfileArn configured (crossRegionDetails in GetGuardrail response).
-5. Reference: AWS Announcement — Automated Reasoning checks GA (August 2025).",https://docs.aws.amazon.com/bedrock/latest/userguide/automated-reasoning.html,Medium,Failed,SR 11-7 | FFIEC CAT | MAS TRM 9.2
-FS-28,No Guardrails — Denied Topics Not Applicable,No Bedrock Guardrails configured.,Configure guardrails with denied topics for regulated financial content.,https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-components.html,Medium,N/A,SR 11-7 | FFIEC CAT | NYDFS 500 | MAS TRM 9.2
-FS-29,ADVISORY: Compliance Disclaimer — Manual Review Required,Application-level compliance disclaimers cannot be verified via AWS APIs. Manual review required to confirm GenAI outputs include required regulatory disclosures.,"1. Implement post-processing to append required disclaimers to GenAI outputs.
-2. Use Bedrock Guardrails word filters to block outputs that omit required disclosures.
-3. Document disclaimer requirements in the AI use case register.
-4. Test disclaimer presence in QA/UAT before production deployment.",https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-components.html,Informational,N/A,SR 11-7 | FFIEC CAT | NYDFS 500 | MAS TRM 9.2
-FS-30,No Bedrock Evaluation Jobs — Compliance Datasets Not Verified,No Bedrock Model Evaluation jobs found.,"Run Bedrock Model Evaluation with compliance-specific datasets:
-- Fair lending test cases (ECOA, Fair Housing Act)
-- UDAP/UDAAP unfair/deceptive practice scenarios
-- AML/KYC edge cases",https://docs.aws.amazon.com/bedrock/latest/userguide/model-evaluation.html,Medium,N/A,SR 11-7 | FFIEC CAT | NYDFS 500
-FS-31,Knowledge Base Data Sources Past Review Threshold,"1 data source(s) not synced in >7 days (a configurable review threshold, NOT an AWS-mandated limit):
-- KB 'fedpoint-kms-poc-knowledge-base' source 'fedpoint-kms-poc-content-data-source' last synced 194 days ago
-Confirm this age is acceptable for each data source's currency requirement — slow-changing reference data may legitimately sync infrequently.","1. Define the maximum acceptable data age per use case (e.g., intraday for market data, daily for product terms, weekly/monthly for regulatory guidance) and adjust the review threshold to match.
-2. Configure automated sync (EventBridge Scheduler → StartIngestionJob) at that cadence — see FS-61.
-3. Set CloudWatch alarms on sync job failures.",https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base-ingest.html,Medium,Failed,SR 11-7 | FFIEC CAT
-FS-32,ADVISORY: Source Attribution — Manual Review Required,Source attribution in GenAI responses cannot be verified via AWS APIs. Manual review required to confirm responses include citations.,"1. Use Bedrock RetrieveAndGenerate with citations enabled.
-2. Include source document references in response post-processing.
-3. Test citation accuracy in QA before production deployment.
-4. Consider Bedrock Guardrails grounding checks to validate response accuracy.",https://docs.aws.amazon.com/bedrock/latest/userguide/kb-test-config.html,Informational,N/A,SR 11-7 | FFIEC CAT | MAS TRM 9.2
-FS-33,KB Data Source References a Deleted S3 Bucket,"One or more Knowledge Base data sources point to S3 buckets that no longer exist (NoSuchBucket). Retrieval will silently return no results for these sources, and the integrity of the KB's grounding data cannot be verified:
-- fedpoint-kms-poc-content-123456789012 (KB 'fedpoint-kms-poc-knowledge-base', source 'fedpoint-kms-poc-content-data-source')","1. Investigate why the data-source bucket was deleted (accidental deletion, environment teardown, or a stale KB configuration).
-2. Recreate/restore the bucket with versioning enabled, or remove the orphaned data source from the Knowledge Base.
-3. Re-run a KB ingestion job after restoring the data source.
-4. Enable S3 versioning and MFA Delete on KB data-source buckets to reduce the risk of unrecoverable deletion.",https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html,High,Failed,SR 11-7 | FFIEC CAT | ISO 27001 A.12
-FS-34,Legacy Foundation Models Available in Region,"Legacy/deprecated foundation models are available in this account/region: anthropic.claude-sonnet-4-20250514-v1:0, amazon.titan-image-generator-v2:0, amazon.nova-premier-v1:0:8k, amazon.nova-premier-v1:0:20k, amazon.nova-premier-v1:0:1000k, amazon.nova-premier-v1:0:mm, amazon.nova-premier-v1:0, amazon.nova-canvas-v1:0, amazon.nova-reel-v1:0, amazon.nova-reel-v1:1. This API reports model *availability*, not actual usage — it cannot determine which models your applications invoke. Legacy models have older training-data cutoffs and may produce outdated information if used. Review whether any are in active use.","1. Identify which (if any) of these legacy models your applications invoke (e.g., via CloudTrail InvokeModel events or application config).
-2. Migrate active usage to current model versions.
-3. Document training-data cutoff dates for all models in use.
-4. Add data-currency disclaimers to outputs from models with old cutoffs.",https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html,Medium,N/A,SR 11-7 | FFIEC CAT
-FS-35,No Evaluation Jobs — Harmful Content Testing Not Verified,No Bedrock Model Evaluation jobs found.,"Run Bedrock Model Evaluation or FMEval with harmful content datasets:
-- Toxicity detection
-- Hate speech classification
-- Violence/self-harm content",https://docs.aws.amazon.com/bedrock/latest/userguide/model-evaluation.html,Medium,N/A,SR 11-7 | FFIEC CAT
-FS-36,No Guardrails — Content Filters Not Applicable,No Bedrock Guardrails configured.,Configure guardrails with content filters.,https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-filters.html,High,N/A,SR 11-7 | FFIEC CAT
-FS-37,ADVISORY: User Feedback Mechanism — Manual Review Required,User feedback mechanisms for harmful outputs cannot be verified via AWS APIs. Manual review required.,"1. Implement thumbs-up/down or flag-for-review UI in GenAI applications.
-2. Route flagged outputs to human reviewers via SQS/SNS.
-3. Log feedback to DynamoDB/S3 for model improvement.
-4. Define SLAs for reviewing flagged content.",https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html,Informational,N/A,SR 11-7 | FFIEC CAT | MAS TRM 9.2
-FS-38,No Guardrails — Word Filters Not Applicable,No Bedrock Guardrails configured.,Configure guardrails with word filters.,https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-components.html,Medium,N/A,SR 11-7 | FFIEC CAT
-FS-39,No SageMaker Clarify Bias Monitoring,"No SageMaker Clarify model bias monitoring schedules found. Models making financial decisions (credit, insurance) may exhibit discriminatory bias without detection.","1. Configure SageMaker Clarify bias detection for all models making credit, insurance, or employment decisions.
-2. Define protected attributes (age, gender, race proxies).
-3. Set bias metric thresholds and alert on violations.
-4. Document bias testing results for regulatory examination.",https://docs.aws.amazon.com/sagemaker/latest/dg/clarify-model-monitor-bias-drift.html,High,Failed,SR 11-7 | FFIEC CAT | ECOA/Fair Housing
-FS-40,No Evaluation Jobs — Bias Datasets Not Verified,No Bedrock Model Evaluation jobs found.,"Run Bedrock Model Evaluation with bias test datasets:
-- Demographic parity test cases
-- Equal opportunity scenarios
-- Counterfactual fairness tests",https://docs.aws.amazon.com/bedrock/latest/userguide/model-evaluation.html,Medium,N/A,SR 11-7 | FFIEC CAT | ECOA/Fair Housing
-FS-41,No SageMaker Clarify Explainability Monitoring,No SageMaker Clarify explainability monitoring found. Models making adverse financial decisions may not provide required explanations (ECOA adverse action notices).,"1. Configure SageMaker Clarify explainability for credit/lending models.
-2. Generate SHAP values for feature importance.
-3. Map top features to human-readable adverse action reason codes.
-4. Store explanations for regulatory examination.",https://docs.aws.amazon.com/sagemaker/latest/dg/clarify-model-explainability.html,High,Failed,SR 11-7 | FFIEC CAT
-FS-42,No SageMaker Model Cards Found,"No SageMaker Model Cards found. Production AI models lack documented intended use, limitations, and bias evaluations.","1. Create SageMaker Model Cards for all production models.
-2. Document: intended use, out-of-scope uses, training data, bias evaluations.
-3. Include regulatory compliance attestations.
-4. Review and update cards at each model version release.",https://docs.aws.amazon.com/sagemaker/latest/dg/model-cards.html,Medium,Failed,SR 11-7 | FFIEC CAT
-FS-43,No CloudWatch Logs Data Protection Policies,"No CloudWatch Logs data protection policies found. PII (SSN, account numbers, credit card numbers) in Bedrock invocation logs may be stored in plaintext.","1. Create CloudWatch Logs data protection policies to mask PII.
-2. Enable masking for: SSN, credit card numbers, bank account numbers, email.
-3. Apply policies to Bedrock invocation log groups.
-4. Test masking with synthetic PII before production deployment.",https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/mask-sensitive-log-data.html,High,Failed,NYDFS 500 | FFIEC CAT | PCI-DSS
-FS-44,Amazon Macie Not Enabled,Amazon Macie is not enabled. S3 buckets containing training data and KB data sources are not being scanned for PII/sensitive data.,"1. Enable Amazon Macie in all regions where AI/ML data is stored.
-2. Create Macie classification jobs for training data and KB buckets.
-3. Configure Macie findings to route to Security Hub and SNS.
-4. Remediate PII findings before using data for model training.",https://docs.aws.amazon.com/macie/latest/user/what-is-macie.html,High,Failed,NYDFS 500 | FFIEC CAT | PCI-DSS
-FS-45,No Guardrails — PII Filters Not Applicable,No Bedrock Guardrails configured.,Configure guardrails with PII/sensitive information filters.,https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-sensitive-filters.html,High,N/A,NYDFS 500 | FFIEC CAT | PCI-DSS
-FS-46,AI/ML Buckets Without Data Classification Tags,"12 AI/ML bucket(s) without data-classification tags: bedrock-agentcore-codebuild-sources-123456789012-us-east-1, bedrock-agentcore-runtime-123456789012-us-east-1-2h09526czr, bedrock-usage-intel-failed-records-123456789012-us-east-1, bedrock-usage-intel-processed-data-123456789012-us-east-1, bedrock-usage-intel-raw-logs-123456789012-us-east-1, dev-vendorcommandcenterst-knowledgebasekbbucket1bf-bfoajiinsr4n, dev-vendorcommandcenterst-knowledgebasekbbucket1bf-dttwp5qrbqyq, dev-vendorcommandcenterst-knowledgebasekbbucket1bf-mure4ntcjmmf, dev-vendorcommandcenterst-knowledgebasekbbucket1bf-pivy5zfs0whp, fedpoint-quick-kb.","Tag all AI/ML data buckets with 'data-classification' key. Values: Public, Internal, Confidential, Restricted. Enforce via SCP or AWS Config rule.",https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-tagging.html,Medium,Failed,NYDFS 500 | FFIEC CAT | ISO 27001 A.12 | PCI-DSS
-FS-47,No Guardrails — Grounding Threshold Not Applicable,No Bedrock Guardrails configured.,Configure guardrails with contextual grounding checks.,https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-grounding.html,High,N/A,SR 11-7 | FFIEC CAT
-FS-48,Active Knowledge Bases for RAG Present,Found 2 active Knowledge Base(s) for RAG grounding.,No action required.,https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base.html,Informational,Passed,SR 11-7 | FFIEC CAT
-FS-49,ADVISORY: Hallucination Disclaimer — Manual Review Required,Application-level hallucination disclaimers cannot be verified via AWS APIs. Manual review required.,"1. Add disclaimers to GenAI outputs: 'AI-generated content may contain errors. Verify with authoritative sources before acting.'
-2. Implement post-processing to append disclaimers.
-3. Test disclaimer presence in QA before production.",https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html,Informational,N/A,SR 11-7 | FFIEC CAT | MAS TRM 9.2
-FS-50,No Guardrails With Relevance Grounding Filters,"No guardrails have RELEVANCE contextual grounding filters. Without relevance filters, responses that are off-topic or unrelated to the user query will not be blocked, increasing hallucination risk in RAG-based FinServ applications.",Enable the RELEVANCE contextual grounding filter in Bedrock Guardrails with a threshold of ≥0.7 to block responses that are not relevant to the user query. Also enable the GROUNDING filter (≥0.7) to block responses not supported by the retrieved source context.,https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-grounding.html,Medium,Failed,SR 11-7 | FFIEC CAT
-FS-51,No Guardrails — Prompt Attack Filters Not Applicable,No Bedrock Guardrails configured.,Configure guardrails with prompt attack filters.,https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-components.html,High,N/A,NYDFS 500 | FFIEC CAT | OWASP LLM Top 10
-FS-52,Bedrock Lambda Functions on Current Runtimes,All 3 Bedrock Lambda function(s) use current runtimes.,No action required.,https://docs.aws.amazon.com/lambda/latest/dg/runtimes-update.html,Informational,Passed,NYDFS 500 | FFIEC CAT | ISO 27001 A.12 | OWASP LLM Top 10
-FS-53,WAF Injection Protection Rules Present,All 1 WAF ACL(s) have injection protection rules.,No action required.,https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html,Informational,Passed,NYDFS 500 | PCI-DSS | FFIEC CAT | OWASP LLM Top 10
-FS-54,ADVISORY: Penetration Testing — Manual Review Required,Penetration testing evidence cannot be verified via AWS APIs. Manual review required to confirm GenAI applications have been tested.,"1. Conduct penetration testing of GenAI applications at least annually and before major releases.
-2. Include AI-specific test cases: prompt injection, jailbreak, indirect (cross-domain) injection, system-prompt leakage, and data-extraction attempts.
-3. Consider AWS Security Agent for on-demand, AI-driven penetration testing (GA March 2026; available in US East N. Virginia, US West Oregon, Europe Ireland, Europe Frankfurt, Asia Pacific Sydney, Asia Pacific Tokyo, with cross-account shared-VPC testing via AWS RAM). Open-source tools such as Garak or PyRIT and manual red-teaming are complementary options. Verify current regional availability on the AWS Security Agent page before relying on it.
-4. Document findings and remediation for regulatory examination, and tag tested resources with a last-pentest-date for audit trail.
-5. For DORA compliance, include GenAI in TLPT (Threat-Led Penetration Testing) scope.",https://aws.amazon.com/security/penetration-testing/,Informational,N/A,NYDFS 500 | FFIEC CAT | OWASP LLM Top 10
-FS-55,No Output Validation Functions Found,No Lambda functions with output validation/sanitization naming found. GenAI outputs may be passed directly to downstream systems without validation.,"1. Implement output validation Lambda functions in GenAI pipelines.
-2. Validate output schema, length, and content before downstream use.
-3. Sanitize outputs before rendering in web UIs (XSS prevention).
-4. Encode outputs appropriately for the target context (HTML, SQL, JSON).",https://genai.owasp.org/llm-top-10/,Medium,Failed,FFIEC CAT | OWASP LLM Top 10
-FS-56,XSS Prevention — Review WAF Common Rule Set,Found 1 WAF ACL(s). Verify AWSManagedRulesCommonRuleSet is enabled for XSS prevention (see FS-53).,"Ensure AWSManagedRulesCommonRuleSet is enabled on all WAF ACLs protecting GenAI web applications. Additionally, implement Content Security Policy (CSP) headers.",https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html,Medium,Passed,NYDFS 500 | PCI-DSS | OWASP LLM Top 10
-FS-57,ADVISORY: Output Encoding — Manual Review Required,Output encoding practices cannot be verified via AWS APIs. Manual code review required.,"1. HTML-encode GenAI outputs before rendering in web UIs.
-2. Use parameterized queries when GenAI output is used in database operations.
-3. JSON-encode outputs before embedding in JavaScript contexts.
-4. Validate output length and format before passing to downstream APIs.",https://genai.owasp.org/llm-top-10/,Informational,N/A,NYDFS 500.06 | FFIEC CAT | OWASP LLM Top 10
-FS-58,Output Schema Validation — Review Required,Found 1 potential schema validation function(s). Verify structured output validation is implemented for all GenAI responses.,"1. Use Bedrock structured output (response schemas) where supported.
-2. Implement JSON schema validation on Lambda output processors.
-3. Reject malformed outputs and return safe error responses.
-4. Log schema validation failures to CloudWatch for monitoring.",https://docs.aws.amazon.com/bedrock/latest/userguide/inference-parameters.html,Medium,Passed,FFIEC CAT | OWASP LLM Top 10
-FS-59,No Guardrails — Topic Allowlist Not Applicable,No Bedrock Guardrails configured.,Configure guardrails with topic policies to restrict off-topic responses.,https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-components.html,Medium,N/A,SR 11-7 | FFIEC CAT
-FS-60,ADVISORY: Contextual Grounding for Off-Topic Prevention,Contextual grounding for off-topic prevention is covered by guardrail grounding checks (FS-47) and RAG configuration (FS-48). Additionally verify system prompts explicitly scope the assistant's role.,"1. Include explicit scope instructions in system prompts.
-2. Use Bedrock Guardrails relevance grounding filter.
-3. Test with off-topic prompts in QA to verify rejection behavior.",https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-grounding.html,Informational,N/A,SR 11-7 | FFIEC CAT
-FS-61,No Automated KB Sync Schedules Detected,"Found 2 Knowledge Base(s) but no EventBridge Scheduler schedules or EventBridge rules with 'bedrock'/'knowledge' naming were found. Note: this check uses a name/target heuristic — sync automation with other naming conventions, AWS Step Functions-based orchestration, or native Bedrock API-triggered syncs (StartIngestionJob called directly) will not be detected. Verify sync automation manually if applicable.","1. Use EventBridge Scheduler (the AWS-recommended approach) to create a recurring schedule (e.g., rate(1 day) or a cron expression) that triggers a Lambda function calling the Bedrock StartIngestionJob API for each data source. Classic EventBridge scheduled rules also work but are a legacy feature.
-2. As of December 2024, Bedrock Knowledge Bases supports custom connectors and streaming data ingestion — use direct document ingestion (KnowledgeBaseDocuments API) for real-time updates without a full S3 sync.
-3. Set sync frequency based on data currency requirements (e.g., hourly for market data, daily for regulatory guidance).
-4. Configure CloudWatch alarms or SNS notifications on IngestionJob FAILED status for sync failure alerting.",https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html,Medium,Failed,SR 11-7 | FFIEC CAT
-FS-62,ADVISORY: Data Currency Disclaimer — Manual Review Required,Data currency disclaimers cannot be verified via AWS APIs. Manual review required.,"1. Add data currency disclaimers to GenAI outputs: 'Information based on data current as of [KB last sync date].'
-2. Expose KB last sync timestamp in application responses.
-3. Alert users when KB data is older than defined threshold.",https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base-ingest.html,Informational,N/A,SR 11-7 | FFIEC CAT | MAS TRM 9.2
-FS-63,Legacy Models Without Lifecycle Management,"Legacy foundation models available: anthropic.claude-sonnet-4-20250514-v1:0, amazon.titan-image-generator-v2:0, amazon.nova-premier-v1:0:8k, amazon.nova-premier-v1:0:20k, amazon.nova-premier-v1:0:1000k. No Config rules found for model lifecycle management.","1. Create a model lifecycle management process.
-2. Subscribe to AWS Bedrock model deprecation notifications.
-3. Test and migrate to new model versions before deprecation dates.
-4. Document training data cutoff dates in model inventory.",https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html,Medium,Failed,SR 11-7 | FFIEC CAT | ISO 27001 A.12
-FS-65,KB Data Source References a Deleted S3 Bucket,"One or more Knowledge Base data sources point to S3 buckets that no longer exist (NoSuchBucket). Document-change monitoring cannot be assessed and KB grounding data is missing for these sources:
-- fedpoint-kms-poc-content-123456789012 (KB 'fedpoint-kms-poc-knowledge-base', source 'fedpoint-kms-poc-content-data-source')","1. Investigate why the data-source bucket was deleted (accidental deletion, environment teardown, or stale KB configuration).
-2. Recreate/restore the bucket (with event notifications and versioning enabled), or remove the orphaned data source from the Knowledge Base.
-3. Re-run a KB ingestion job after restoring the data source.",https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventBridge.html,High,Failed,FFIEC CAT | DORA Art.6 | ISO 27001 A.12
-FS-66,AgentCore Runtimes Missing End-User Identity Propagation,"The following runtimes have no JWT or IAM authorizer configured for end-user identity propagation. Tool calls are authorized only by the agent execution role, not the originating user:
-- vendoriq_vendoriq
-- test_minimal
-- strands_claude_short_timeout
-- strands_claude_getting_started
-- proposal_drafter
-- harness_harness_ctuux
-- executive_summary_writer
-- document_generation
-- datetimemcp_Agent","1. Configure a custom JWT authorizer or IAM authorizer on each AgentCore runtime.
-2. Propagate the end-user's identity token to downstream tool services.
-3. Ensure tool services validate the propagated identity before executing actions.
-4. Do not expose propagated identity tokens to unauthorized third parties.",https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/security-authorization.html,High,Failed,NYDFS 500 | SR 11-7 | MAS TRM 9 | PCI-DSS
-FS-67,Agent Action-Group Lambdas May Lack Transaction Thresholds,"The following agent action-group Lambda functions have no environment variables whose names suggest transaction-value threshold configuration (this is a best-effort heuristic — a threshold enforced in code or in an AgentCore Policy Engine rule would not be detected here, so treat this as a prompt for manual verification rather than a definitive gap). Without explicit limits, agents could initiate unbounded financial transactions:
-- Dev-DashboardStack-DashboardApiLayerAgentSessionsF-EnQLRGDeLRkn
-- fedpoint-kms-poc-create-bedrock-index
-- Dev-DashboardStack-DashboardApiLayerAgentInvokeFn3-r3fQdrJuboTX","1. Add transaction-value threshold environment variables (e.g., MAX_TRANSACTION_AMOUNT) to each agent action-group Lambda.
-2. Implement threshold enforcement logic in the Lambda handler.
-3. Configure AgentCore Policy Engine rules to cap financial transaction amounts.
-4. Route transactions exceeding thresholds to a human-in-the-loop approval step.",https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-engine.html,High,Failed,SR 11-7 | FFIEC CAT | MAS TRM 9 | PCI-DSS
-FS-68,API Gateway Request Body Size Limits Not Enforced,"Input payload size limits are not fully enforced on GenAI API endpoints. Oversized prompts can exhaust Bedrock token quotas and inflate costs:
-- REST APIs without request validators: VendorIQ MuleSoft API, VendorIQ Dashboard API, IsbRestApi","1. Add API Gateway request validators to enforce maximum body size on all Bedrock-facing REST API methods.
-2. Add a WAF SizeConstraintStatement rule to block requests with body size exceeding your maximum prompt length (e.g., 32 KB).
-3. Set the max_tokens parameter in Bedrock API calls to cap output length.
-4. Implement client-side token counting before submitting requests.",https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-type-size-constraint.html,Medium,Failed,DORA Art.6 | FFIEC CAT | PCI-DSS | OWASP LLM Top 10
-FS-69,Prompt Input Validation Functions Present,"Found 3 Lambda function(s) with input validation/sanitization naming patterns: ISB-InitializeCleanupLambda-myisb, fedpoint-kms-poc-user-api-clean, fedpoint-compliance-validation.","Review these functions to confirm they cover: special-character stripping, format validation, size limits, and injection-sequence detection.",https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-injection.html,Informational,Passed,NYDFS 500 | FFIEC CAT | OWASP LLM Top 10
diff --git a/sample-reports/multi-account-summary.png b/sample-reports/multi-account-summary.png
index bcc9866..639c175 100644
Binary files a/sample-reports/multi-account-summary.png and b/sample-reports/multi-account-summary.png differ
diff --git a/sample-reports/security_assessment_multi_account.html b/sample-reports/security_assessment_multi_account.html
index f8bff1b..ab1ba76 100644
--- a/sample-reports/security_assessment_multi_account.html
+++ b/sample-reports/security_assessment_multi_account.html
@@ -1,192 +1,10 @@
-
-
-
+
+
+
Multi-Account AI/ML Security Assessment Report
-
+
-
+
-
-
+
+
Dark Mode
Navigation
-
+
Overview
-
+
Security Findings
- 345
+ 723
-
+
Risk Distribution
-
+
Methodology
By Service
-
+
Bedrock
- 79
+ 209
-
+
SageMaker
- 163
+ 252
-
- AgentCore
- 103
-
-
-
@@ -7612,6 +16853,7 @@ {
const rowText = row.textContent.toLowerCase();
const rowAccount = row.dataset.account || '';
+ const rowRegion = row.dataset.region || '';
const rowService = row.dataset.service || '';
const rowSeverity = row.dataset.severity || '';
const rowStatus = row.dataset.status || '';
let show = true;
if (searchText && !rowText.includes(searchText)) show = false;
if (accountFilter && rowAccount !== accountFilter) show = false;
+ if (regionFilter && rowRegion.toLowerCase() !== regionFilter) show = false;
if (serviceFilter && rowService !== serviceFilter) show = false;
if (severityFilter && rowSeverity !== severityFilter) show = false;
if (statusFilter && rowStatus !== statusFilter) show = false;
@@ -7634,6 +16878,7 @@ {
const rowText = row.textContent.toLowerCase();
const rowAccount = row.dataset.account || '';
+ const rowRegion = row.dataset.region || '';
const rowSeverity = row.dataset.severity || '';
const rowStatus = row.dataset.status || '';
let show = true;
if (searchText && !rowText.includes(searchText)) show = false;
if (accountValue && rowAccount !== accountValue) show = false;
+ if (regionValue && rowRegion.toLowerCase() !== regionValue) show = false;
if (severityValue && rowSeverity !== severityValue) show = false;
if (statusValue && rowStatus !== statusValue) show = false;
row.style.display = show ? '' : 'none';
@@ -7735,22 +16989,25 @@ x
\ No newline at end of file
+
+