{"slug": "building-a-stride-aware-fraud-pattern-review-agent-on-amazon-bedrock", "title": "Building a STRIDE-Aware Fraud-Pattern Review Agent on Amazon Bedrock", "summary": "Raj Mohamad Sabibullah, a senior solution architect, published a step-by-step build log for a STRIDE-aware fraud-pattern review agent on Amazon Bedrock that classifies flagged card transactions against a STRIDE-based fraud taxonomy, cites the relevant PCI DSS 4.0 control, and returns a risk rating without exposing unmasked card numbers. The build covers per-region model access for Claude 3.7 Sonnet and Titan Text Embeddings V2, an S3-backed knowledge base of STRIDE runbooks, and chunk-size tuning, which he calls the highest-leverage knob in the build.", "body_md": "A step-by-step build: from model access to a deployed agent, with every console screen along the way.\n\n**Raj Mohamad Sabibullah** · Senior Solution Architect\n\nI spend most of my week in the unglamorous middle layer of a digital bank build: a card management system on one side, a core banking system on the other, Kafka topics carrying every authorization and reversal in between, and a security team that wants a threat model for anything that so much as reads a transaction record. It is not the part of the stack that gets a conference talk. But it is exactly the part where agentic AI turned out to be more useful than I expected — not as a chatbot bolted onto a mobile app, but as a colleague sitting inside the fraud-ops workflow, reading runbooks faster than any analyst can, and never seeing a live PAN.\n\nThis post is the build log I wish I'd had going in: every step from requesting model access to a working CLI invocation, in order, with the console screen at each step. It's long on purpose — a previous, shorter version of this post got fair feedback that it skipped the \"how,\" so this one doesn't skip anything. Account IDs, transaction references and internal system names below are illustrative, not real; the steps, screens and configuration are the real sequence I used.\n\n**What we're building** An agent that takes a flagged transaction reference from a cards-ops analyst, classifies it against a STRIDE-based fraud taxonomy, cites the PCI DSS 4.0 control it relied on, and returns a risk rating — without ever seeing or returning an unmasked card number.\n\nOn this page\n\nBefore touching the console, make sure you have:\n\n`eu-central-1` for data-residency reasons — check what your own compliance scope requires before picking a region).\nWith that in place, the build runs in the order below — each step depends on the one before it.\n\nBedrock model access is granted per account, per region, and it is not on by default. This trips up almost everyone the first time — an agent creation will fail silently referencing a model you haven't been granted.\n\nGo to **Amazon Bedrock → Model access** and request access to the models you need. For this build that's Claude 3.7 Sonnet (the agent's reasoning model) and Titan Text Embeddings V2 (for the knowledge base).\n\nModel access is a one-time, per-region grant. Approval for Anthropic models is usually instant; some providers require a use-case form.\n\nAccess typically activates within a few minutes for Anthropic and Amazon models. Confirm the status shows **Access granted** before moving on — every later step assumes this is done.\n\nThe agent's domain knowledge — STRIDE runbooks, the PCI DSS control mapping, prior incident write-ups — needs to live somewhere Bedrock can ingest it from. Create a dedicated S3 bucket and upload the source documents.\n\nbash\n\n```\naws s3 mb s3://stride-runbooks-kb-src-eucentral1 --region eu-central-1\naws s3 cp ./runbooks/ s3://stride-runbooks-kb-src-eucentral1/ --recursive\n```\n\nOne file per STRIDE category kept retrieval clean later — a runbook that blends two categories produces a knowledge-base chunk that blends two answers.\n\nA few things that mattered here:\n\nThis is where the runbooks become something the agent can retrieve from. Go to **Amazon Bedrock → Knowledge bases → Create knowledge base** and work through the wizard.\n\nThe first two steps of the wizard (name/description, then pointing at the S3 data source) are straightforward. The step worth slowing down on is vector store configuration:\n\nChunk size was the single highest-leverage tuning knob in this whole build — more than model choice or prompt wording.\n\nConfiguration choices that mattered:\n\nAfter the wizard finishes, **sync the data source** (Knowledge base → Data source → Sync) so the documents in S3 are actually embedded and indexed — creating the knowledge base does not automatically ingest the files. Sync typically takes a few minutes for a handful of PDFs; watch the sync history for failures before assuming it worked.\n\nEvery Bedrock agent runs under an IAM role that Bedrock assumes to invoke the model, retrieve from the knowledge base, and call your action-group Lambdas. This is the control that turns \"an LLM agent\" into \"an LLM agent that provably cannot write to your core-banking system,\" and it's worth building before you build the agent itself.\n\nThe permissions boundary is the layer that matters most: even if the Lambda code changes next year to add a write path, the boundary denies it at the IAM layer regardless of what the code tries to do.\n\nTwo design decisions here, not just defaults:\n\n`lambda:InvokeFunction` on `*`. This is more setup work and it is the difference between \"the agent can call three specific, reviewed functions\" and \"the agent can call any Lambda in the account,\" which is not a sentence I wanted to say to our security team.\nThe trust relationship restricts `sts:AssumeRole` to the `bedrock.amazonaws.com` service principal, scoped further with an `aws:SourceAccount` condition — standard practice, worth double-checking it's actually there rather than assumed.\n\nThe agent doesn't touch any real system directly — every action it can take is a Lambda function behind an action group. We built three:\n\n| Function | Purpose | Access | \n|---|---|---|\n| `cms-read-proxy-fn` | Looks up a transaction by reference ID, returns a masked record | Read-only, scoped IAM role into the CMS read replica | \n| `stride-classifier-fn` | Scores the transaction against STRIDE categories using rules + recent MSK velocity features | Reads from MSK consumer group, no write access anywhere | \n| `sns-notify-fn` | Publishes an alert to the cards-ops on-call topic when risk is HIGH | `sns:Publish` on one topic only | \n\nA trimmed version of the transaction-lookup handler, showing the masking that happens before anything reaches the model:\n\npython · cms-read-proxy-fn\n\n``` python\nimport json\nimport boto3\n\ndef lambda_handler(event, context):\n    reference_id = event[\"parameters\"][0][\"value\"]\n    record = fetch_from_cms_readonly(reference_id)  # internal read-only client\n\n    # Mask before this ever leaves the function — the agent and the\n    # guardrail are a second line of defense, not the first.\n    masked_pan = \"•••• •••• •••• \" + record[\"pan\"][-4:]\n\n    response_body = {\n        \"referenceId\": reference_id,\n        \"amount\": record[\"amount\"],\n        \"currency\": record[\"currency\"],\n        \"maskedPan\": masked_pan,\n        \"deviceFingerprint\": record[\"device_fingerprint\"],\n        \"authCount90s\": record[\"velocity_90s\"],\n        \"merchantCategoryCode\": record[\"mcc\"],\n    }\n\n    return {\n        \"messageVersion\": \"1.0\",\n        \"response\": {\n            \"actionGroup\": event[\"actionGroup\"],\n            \"apiPath\": event[\"apiPath\"],\n            \"httpMethod\": event[\"httpMethod\"],\n            \"httpStatusCode\": 200,\n            \"responseBody\": {\"application/json\": {\"body\": json.dumps(response_body)}},\n        },\n    }\n```\n\nThe masking happening inside the Lambda — not left to the model or the guardrail to catch — is deliberate. Guardrails and instructions are the second and third layers; the function itself should never emit a raw PAN in the first place.\n\nDeploy these three functions however your team normally deploys Lambdas (console, SAM, CDK — we moved to CDK for this in step 11). Note each function's ARN; you'll need them for the action group and the IAM role above.\n\nWith the knowledge base synced, the execution role scoped, and the Lambdas deployed, go to **Amazon Bedrock → Agents → Create agent**.\n\nEverything the agent can reason with or act on attaches to this one resource: model, instructions, action groups, knowledge base, and guardrail.\n\nFill in, in order:\n\n`fraud-pattern-review-agent`.\nagent instructions\n\n```\nYou are a fraud-pattern review assistant for the XY Bank Cards Workstream.\nGiven a transaction reference, cross-check STRIDE threat categories against the\nknowledge base of card-fraud runbooks, summarize risk indicators in plain\nlanguage, cite the matching control from PCI DSS 4.0 / CBE regulation where\napplicable, and NEVER return raw PAN, CVV, or cardholder PII in your response.\nAlways call the transaction-lookup action before concluding a risk rating.\n```\n\nThree lessons baked into that wording:\n\n**State the negative constraint as its own capitalized sentence, early.** Buried as a passive aside, it gets ignored more often than as a standalone line.\n\n**Force the tool call explicitly.** Without \"always call the transaction-lookup action before concluding,\" the agent would occasionally answer from a prior turn's context instead of looking the transaction up fresh.\n\n**Name the citation format you want.** Asking for \"the matching control from PCI DSS 4.0 / CBE regulation where applicable\" produces consistent, checkable citations — paired with contextual grounding in the guardrail (step 8), which stops the model from inventing a plausible-sounding control number it didn't actually retrieve.\n\nSave the agent. It exists now, but it can't do anything yet — no action groups, no knowledge base link, no guardrail. Those are the next three steps.\n\nAn action group tells the agent what it can call and how — Bedrock uses an OpenAPI schema to generate the function-calling interface, and a Lambda function to actually execute the call.\n\nGo to the agent's **Action groups** tab and create one per Lambda (`transaction-lookup`, `stride-classifier`, `analyst-notify`). For each, select \"Define with API schemas,\" point it at the corresponding Lambda ARN, and provide the schema.\n\nThe OpenAPI schema is what lets the model decide, on its own, when to call this action and with what parameters — get the field descriptions right and the model's tool-use gets noticeably more reliable.\n\nThe schema for `transaction-lookup`:\n\nyaml · openapi schema\n\n```\nopenapi: 3.0.0\ninfo:\n  title: Transaction Lookup API\n  version: 1.0.0\npaths:\n  /transaction/{referenceId}:\n    get:\n      summary: Retrieve a masked transaction record by reference ID\n      operationId: getTransaction\n      parameters:\n        - name: referenceId\n          in: path\n          required: true\n          schema: { type: string }\n      responses:\n        '200':\n          description: Masked transaction record\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  referenceId: { type: string }\n                  amount: { type: number }\n                  currency: { type: string }\n                  maskedPan: { type: string, example: \"•••• •••• •••• 4471\" }\n                  deviceFingerprint: { type: string }\n                  authCount90s: { type: integer }\n                  merchantCategoryCode: { type: string }\n```\n\nTwo practical notes: the `summary` field on each operation matters more than you'd expect — it's what the model reads to decide *when* to call this tool, so write it as a clear instruction, not a terse label. And repeat this for `stride-classifier` and `analyst-notify` with their own schemas before moving on — an agent with only one of three action groups attached will happily answer without ever invoking the other two.\n\nThen, on the agent's main page, link the **knowledge base** created in step 3 under the Knowledge base section, and give it its own retrieval instructions (a short sentence telling the agent when to consult it — e.g., \"Use this knowledge base to look up STRIDE categories and their matching compliance controls\").\n\nThis is the step I'd tell anyone building something similar to budget the most time for — it took longer than the agent and action groups combined, mostly in review cycles with our security team, not console clicks.\n\nGo to **Amazon Bedrock → Guardrails → Create guardrail**.\n\nA guardrail is a separate, versioned resource from the agent — you can update the guardrail and ship a stricter policy without redeploying the agent itself.\n\nThree configuration sections did most of the work:\n\n`CREDIT_DEBIT_CARD_NUMBER` and `CREDIT_DEBIT_CARD_CVV` as built-in PII types, action set to Once created, attach the guardrail to the agent from the agent's main configuration page (visible in the step 6 screenshot above, bottom right). Guardrails are versioned independently of the agent, so tightening a filter later doesn't require touching the agent resource at all — useful when a security review asks for a stricter policy mid-project.\n\nWith instructions, action groups, knowledge base, and guardrail all attached, click **Prepare** on the agent (top right of the Agent builder screen) — this compiles the current draft configuration into something invocable. Then open the **Test** window to run it end to end before anything touches a real alias.\n\nA full round trip in the test window: two action-group calls, a STRIDE classification, a cited control, and a masked card number — all before this reaches a real alias.\n\nWhat to check here, beyond \"did it answer\":\n\nIterate on the instructions and chunking here — this loop is fast (no redeploy needed) and it's where most of the actual tuning happens.\n\nThe agent's `DRAFT` state is for testing only. To integrate it into anything real, create a **version** (an immutable snapshot of the prepared configuration) and point an **alias** at it — your application code calls the alias, not the draft, so you can promote a new version without changing any calling code.\n\nprod-v1 stays pinned to a reviewed, immutable version. Testing a new instruction wording happens against staging first.\n\nGo to **Aliases → Create alias**, name it (`prod-v1`), and either create a new version from the current draft or point it at an existing one. Keep a `staging` alias around pointed at whatever draft you're actively iterating on — it lets you validate a change against the same integration path production uses, without touching `prod-v1` until you're ready.\n\nOnce the console configuration behaves the way you want, capture it as infrastructure-as-code so it ships through the same pipeline as everything else in the workstream, with a clean audit trail for the security sign-off — no console drift between what was reviewed and what's running.\n\ntypescript · cdk stack\n\n``` js\nconst guardrail = new bedrock.CfnGuardrail(this, 'CardsPiiGuardrail', {\n  name: 'cards-pii-guardrail-v3',\n  blockedInputMessaging: 'This request cannot be processed under current policy.',\n  blockedOutputsMessaging: 'This response was withheld by the cards data guardrail.',\n  sensitiveInformationPolicyConfig: {\n    piiEntitiesConfig: [\n      { type: 'CREDIT_DEBIT_CARD_NUMBER', action: 'BLOCK' },\n      { type: 'CREDIT_DEBIT_CARD_CVV', action: 'BLOCK' },\n    ],\n  },\n  contextualGroundingPolicyConfig: {\n    filtersConfig: [\n      { type: 'GROUNDING', threshold: 0.75 },\n      { type: 'RELEVANCE', threshold: 0.70 },\n    ],\n  },\n});\n\nconst agent = new bedrock.CfnAgent(this, 'FraudPatternAgent', {\n  agentName: 'fraud-pattern-review-agent',\n  foundationModel: 'anthropic.claude-3-7-sonnet-20250219-v1:0',\n  instruction: fs.readFileSync('./agent-instructions.txt', 'utf-8'),\n  guardrailConfiguration: {\n    guardrailIdentifier: guardrail.attrGuardrailId,\n    guardrailVersion: guardrail.attrVersion,\n  },\n  agentResourceRoleArn: agentExecutionRole.roleArn,\n});\n\nconst alias = new bedrock.CfnAgentAlias(this, 'ProdAlias', {\n  agentId: agent.attrAgentId,\n  agentAliasName: 'prod-v1',\n});\n```\n\nBootstrap once per account/region if you haven't already, then deploy:\n\n```\ncdk bootstrap aws://104829xxxxxx/eu-central-1 --profile xybank-nonprod\ncdk deploy FraudPatternAgentStack --profile xybank-nonprod\n```\n\nSame agent, same configuration, now deployed through CDK — IAM role, Lambda action groups, guardrail, knowledge base and alias all in one stack.\n\nThe stack outputs (`AgentId`, `AgentAliasId`, `KnowledgeBaseId`) are what your calling application needs — capture them in your deployment pipeline's output rather than hardcoding them, since a version promotion can change the alias ID.\n\nWith an alias live, the agent is callable via the `bedrock-agent-runtime` API — from the CLI for a smoke test, or from your cards-ops console backend for the real integration.\n\nbash · smoke test\n\n```\naws bedrock-agent-runtime invoke-agent \\\n  --agent-id FRAUD7X2QK9 \\\n  --agent-alias-id TSTALIASID \\\n  --session-id test-001 \\\n  --input-text \"Review txn XYB-2026-88213910\" \\\n  --profile xybank-nonprod\n```\n\nFor the actual application integration, the same call goes through the AWS SDK from the Lambda orchestrator behind API Gateway (see the architecture diagram below) — the CLI call above is purely for verifying the deployed alias works before wiring up the real request path.\n\npython · orchestrator\n\n``` python\nimport boto3\n\nclient = boto3.client(\"bedrock-agent-runtime\", region_name=\"eu-central-1\")\n\nresponse = client.invoke_agent(\n    agentId=\"FRAUD7X2QK9\",\n    agentAliasId=\"PRODALIASID\",\n    sessionId=session_id,\n    inputText=analyst_query,\n)\n\nfor event in response[\"completion\"]:\n    if \"chunk\" in event:\n        print(event[\"chunk\"][\"bytes\"].decode())\n```\n\nThat's the full path — model access, knowledge base, IAM boundary, action groups, agent, guardrail, alias, IaC, invocation. Everything from here is tuning.\n\nZooming back out, here's how all the pieces built above fit together:\n\nThe agent orchestrates through Lambda action groups; nothing writes back to the core-banking system, and PII never leaves the guardrail boundary.\n\nAPI Gateway and the Lambda orchestrator in front of the agent (left side of the diagram) handle request-level auth against our existing identity-provider session before anything reaches Bedrock — that layer isn't part of the Bedrock build itself, but it's what makes \"who called this agent and were they authorized\" answerable, which matters as much to a security review as anything inside Bedrock does.\n\nSTRIDE was built for software threat modeling, not fraud triage, so we adapted rather than adopted it:\n\n| STRIDE category | What it looks like in a card transaction | Primary control we cite | \n|---|---|---|\n| **S** poofing | Card-not-present transaction from a new device fingerprint, no prior history with the payee | PCI DSS 4.0 Req 8.4.2 — MFA for CNP | \n| **T** ampering | Amount or MCC mismatch between authorization and clearing message | ISO 8583 field-level integrity checks | \n| **R** epudiation | Multiple rapid authorizations with no matching customer-initiated event in the channel logs | Non-repudiation logging, regulatory circular on transaction logging | \n| **I** nformation disclosure | Any flow where the agent itself would need to return unmasked PAN/CVV | Bedrock Guardrail sensitive-info filter (this is the one the guardrail exists to stop) | \n| **D** enial of service | Velocity spikes consistent with card-testing rather than genuine usage | Rate-limiting at API Gateway + card-testing runbook | \n| **E** levation of privilege | Any request pattern that resembles probing the core-banking proxy for scopes beyond transaction read | IAM least privilege + CloudWatch anomaly alarms | \n\nThe knowledge base holds one runbook section per row, which is why the chunking decision in step 3 mattered as much as it did — a chunk that blends \"spoofing\" and \"repudiation\" guidance produces exactly the kind of blended, half-right answer that erodes an analyst's trust in the tool after two or three uses.\n\nA few honest notes, since most write-ups skip this part:\n\n**The first version of the instructions let the agent answer from memory when knowledge base retrieval came back thin.** Turning on contextual grounding in the guardrail (step 8) fixed this more reliably than any amount of prompt tuning — treat grounding as a guardrail-layer control, not a prompt-layer one.\n\n**Chunk size was the single highest-leverage tuning knob**, above model choice, above temperature, above instruction wording. If your retrieval quality is inconsistent, look at chunking (step 3) before you look at the model.\n\n**IAM scoping (step 4) took longer than every other piece of this build combined.** Getting the security team comfortable with \"an LLM agent has a role that can invoke this Lambda\" required the permissions boundary, a CloudTrail-backed review process, and a tabletop walkthrough of what happens if the model is prompt-injected through a transaction memo field. Budget for that conversation; it's not a formality in a cards environment.\n\n**I'd add a human-in-the-loop confirmation step before `sns-notify-fn` fires in production**, even though it's a read-only-adjacent notification. An agent that pages someone at 3 a.m. on a false positive burns trust fast.\n\nNone of this is exotic. It's the same discipline any of us would apply to a human-facing fraud process — least privilege, grounded evidence, a clear audit trail — applied to an agent instead of a person. The interesting part isn't that Bedrock can do agentic orchestration; it's that the hard 80% of shipping it in a bank was identical to the hard 80% of shipping anything else in a bank.\n\nThe natural extension is multi-agent collaboration — a supervisor agent that routes between this fraud-pattern reviewer and a separate compliance-documentation agent for SAR (suspicious activity report) drafting, which Bedrock now supports natively. I haven't shipped that part yet, so I'll leave it for a follow-up post once it's been through a security review rather than write about it speculatively here.\n\nIf you're building something similar on a legacy cards or core-banking stack, I'd genuinely like to compare notes — particularly on how you scoped IAM for action groups touching a system of record, and how your security team handled the guardrail sign-off. Drop a comment or find me on LinkedIn.", "url": "https://wpnews.pro/news/building-a-stride-aware-fraud-pattern-review-agent-on-amazon-bedrock", "canonical_source": "https://dev.to/raj_mohamed_3de1e4c27eacc/-building-a-stride-aware-fraud-pattern-review-agent-on-amazon-bedrock-df5", "published_at": "2026-10-21 06:08:13+00:00", "updated_at": "2026-10-21 06:17:29.007135+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "ai-infrastructure", "large-language-models"], "entities": ["Amazon Bedrock", "Raj Mohamad Sabibullah", "Anthropic", "Claude 3.7 Sonnet", "Titan Text Embeddings V2", "Amazon S3", "PCI DSS"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/building-a-stride-aware-fraud-pattern-review-agent-on-amazon-bedrock", "markdown": "https://wpnews.pro/news/building-a-stride-aware-fraud-pattern-review-agent-on-amazon-bedrock.md", "text": "https://wpnews.pro/news/building-a-stride-aware-fraud-pattern-review-agent-on-amazon-bedrock.txt", "jsonld": "https://wpnews.pro/news/building-a-stride-aware-fraud-pattern-review-agent-on-amazon-bedrock.jsonld"}}