{"id":23617,"date":"2026-09-01T02:12:13","date_gmt":"2026-09-01T02:12:13","guid":{"rendered":"https:\/\/scannn.com\/how-we-built-an-ai-security-analyst-on-aws-for-under-500-month\/"},"modified":"2026-09-01T02:12:13","modified_gmt":"2026-09-01T02:12:13","slug":"how-we-built-an-ai-security-analyst-on-aws-for-under-500-month","status":"publish","type":"post","link":"https:\/\/scannn.com\/lv\/how-we-built-an-ai-security-analyst-on-aws-for-under-500-month\/","title":{"rendered":"How we built an AI security analyst on AWS for under $500\/month"},"content":{"rendered":"\n<div data-framer-name=\"Information\" data-framer-component-type=\"RichTextContainer\" style=\"transform:none\">\n<p dir=\"auto\" class=\"framer-text framer-styles-preset-21ogod\"><em class=\"framer-text\">This article was written by <\/em><!--$--><a class=\"framer-text framer-styles-preset-1ff8vye\" href=\"https:\/\/www.linkedin.com\/in\/pablo-vidal-60064528\/\" target=\"_blank\" rel=\"\"><em class=\"framer-text\">Pablo Vidal<\/em><\/a><!--\/$--><em class=\"framer-text\">.<\/em><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">As a healthcare platform, we require proactive security coverage across every system that touches sensitive data. We run a suite of automated threat hunts every day across AWS, Okta, Google Workspace, GitHub, CrowdStrike, Slack, Stripe, and a dozen other services.\u00a0<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">The whole thing costs about $500\/month. It runs on ECS Fargate, a few Lambda functions, and Claude.<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">This post covers how we built it, what went well, and what we&#8217;d change.<\/p>\n<h2 dir=\"ltr\" class=\"framer-text framer-styles-preset-3gqujr\">Extending threat detection with LLMs<\/h2>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">Fifteen log sources flow into our Snowflake warehouse: CloudTrail, Okta, Google Workspace, GitHub, CrowdStrike, Auth0, GCP, Slack, Stripe, Looker, HubSpot, Retool, and a couple of internal services. Separately, Datadog holds about 14 days of searchable logs.<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">SQL threat detections handle deterministic patterns, like <code class=\"framer-text framer-styles-preset-8ji2eo\">WHERE event_name=\"ConsoleLogin\" AND source_ip_country IN ('RU', 'CN', 'KP').<\/code> In our setup, Snowflake tasks run every 15 minutes. Each detection is a YAML file with a SQL query, a lookback window, deduplication rules, and a severity; this constitutes the majority of our alert volume.<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">But then there\u2019s everything you can\u2019t write a rule for, or may not know how to write a rule for because you\u2019re not an expert in that threat surface. (We know AWS and Okta well, but HubSpot audit logs?)<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">There\u2019s a growing body of work showing that LLMs are well-suited to run this kind of proactive threat detection. Anthropic runs their own Claude-powered security operations center (SOC) internally. Slack\u2019s security engineering team published a great post in December 2025 (<!--$--><a class=\"framer-text framer-styles-preset-1ff8vye\" href=\"https:\/\/slack.engineering\/streamlining-security-investigations-with-agents\/\" rel=\"\">Streamlining Security Investigations with Agents<\/a><!--\/$-->) describing a multi-agent system where a Director, domain Experts, and a Critic collaborate on alert investigations.\u00a0<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">We learned from what these teams shared, and built our own version on top of infrastructure we already had.<\/p>\n<h2 dir=\"ltr\" class=\"framer-text framer-styles-preset-3gqujr\">How it runs on AWS<\/h2>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">Three decisions shaped the architecture:<\/p>\n<p><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><strong class=\"framer-text\">Reuse existing data infrastructure.<\/strong> The security logs already exist in Datadog and Snowflake. The security team reads from the same APIs and tables that engineering uses. We didn&#8217;t build a separate ingestion pipeline or stand up a security-specific data store. This is why the Datadog line item in the cost table is $0: we&#8217;re piggybacking on infrastructure the company already pays for.<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><strong class=\"framer-text\">No long-running services.<\/strong> Each hunt is a Docker container that runs as an ECS Fargate task (1 vCPU, 2GB of memory). It starts, runs the investigation (usually 15\u201345 minutes), writes findings to Snowflake, and stops. EventBridge triggers hunts on a cron schedule, and a small Lambda handles the ECS task launch.<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><strong class=\"framer-text\">One alert table for everything.<\/strong> Both SQL detections and AI hunts write to the same Snowflake ALERTS table. The downstream pipeline (routing to Slack, creating Jira tickets, tracking state) doesn&#8217;t need to know where an alert came from, and we can add new detection types without changing anything downstream. A Lambda polls a change data capture (CDC) stream on that table every 5 minutes, consuming new rows inside a transaction. The stream offset only advances on commit, so if anything fails mid-way, it rolls back and retries next cycle. A second Lambda handles Slack button clicks and Jira webhooks. A third processes actions asynchronously off SQS.<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">State lives in DynamoDB (90-day TTLs) for short-term tracking and in Snowflake for anything persistent. The only non-obvious piece of infrastructure: a VPC with a NAT Gateway, because Snowflake requires a static IP for network allowlisting. Credentials live in Secrets Manager. Everything is Terraform.<\/p>\n<figure class=\"framer-text framer-table-wrapper\">\n<table class=\"framer-text framer-styles-preset-1x98hda\">\n<tbody class=\"framer-text\">\n<tr class=\"framer-text\">\n<th class=\"framer-text\">\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><strong class=\"framer-text\">Component<\/strong><\/p>\n<\/th>\n<th class=\"framer-text\">\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><strong class=\"framer-text\">Monthly cost<\/strong><\/p>\n<\/th>\n<\/tr>\n<tr class=\"framer-text\">\n<td class=\"framer-text\">\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">Claude API (Sonnet + Opus)<\/p>\n<\/td>\n<td class=\"framer-text\">\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">$250\u2013400<\/p>\n<\/td>\n<\/tr>\n<tr class=\"framer-text\">\n<td class=\"framer-text\">\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">Snowflake compute<\/p>\n<\/td>\n<td class=\"framer-text\">\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">$60\u201390<\/p>\n<\/td>\n<\/tr>\n<tr class=\"framer-text\">\n<td class=\"framer-text\">\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">ECS Fargate<\/p>\n<\/td>\n<td class=\"framer-text\">\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">$10\u201320<\/p>\n<\/td>\n<\/tr>\n<tr class=\"framer-text\">\n<td class=\"framer-text\">\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">Lambda, DynamoDB, NAT Gateway<\/p>\n<\/td>\n<td class=\"framer-text\">\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">$5\u201310<\/p>\n<\/td>\n<\/tr>\n<tr class=\"framer-text\">\n<td class=\"framer-text\">\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">Additional Datadog ingestion<\/p>\n<\/td>\n<td class=\"framer-text\">\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">$0 (already paying for observability)<\/p>\n<\/td>\n<\/tr>\n<tr class=\"framer-text\">\n<td class=\"framer-text\">\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><strong class=\"framer-text\">Total<\/strong><\/p>\n<\/td>\n<td class=\"framer-text\">\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><strong class=\"framer-text\">$320\u2013510<\/strong><\/p>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">We track token usage per hunt in DynamoDB and generate cost reports weekly. Every hunt logs its input tokens, output tokens, cache hits, and computed cost.<\/p>\n<h2 dir=\"ltr\" class=\"framer-text framer-styles-preset-3gqujr\">Defining a hunt in YAML<\/h2>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">The AI hunts are where the interesting design decisions live. We don&#8217;t write these by hand. The repo carries an AGENTS.md with threat hunt conventions and ~20 existing hunts as working examples. That&#8217;s enough context for Claude to take a one-paragraph description like \u201chunt Snowflake for privilege escalation, off-hours bulk extraction, and queries against sensitive tables by non-clinical roles\u201d.<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">Here&#8217;s a cut-down version of our CloudTrail hunt. The key sections are the <code class=\"framer-text framer-styles-preset-8ji2eo\">objective<\/code> (what to investigate, in plain English), <code class=\"framer-text framer-styles-preset-8ji2eo\">severity_guidelines<\/code> (how to calibrate judgment), and <code class=\"framer-text framer-styles-preset-8ji2eo\">false_positives<\/code> (patterns we already know are benign):<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">id: aws_cloudtrail<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">name: AWS CloudTrail Security<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">status: production<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">enable_aws_tools: true<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">enable_snowflake_tools: true<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">enable_github_tools: true<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">schedule:<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0day_of_week: daily<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0lookback_hours: 24<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0timeout_minutes: 45<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">objective: |<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0Investigate AWS CloudTrail audit logs for security anomalies.<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0HIGH PRIORITY:<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0- IAM privilege escalation<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0- Security group opened to 0.0.0.0\/0 on sensitive ports<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0- Root account usage<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0- CloudTrail\/GuardDuty\/Config disabling<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0Filter out:<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0- Known CI\/CD service accounts<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0- Terraform\/CloudFormation automation<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">severity_guidelines:<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0critical: |<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0\u00a0\u00a0- Root account API usage or console login<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0\u00a0\u00a0- CloudTrail logging disabled<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0\u00a0\u00a0- Evidence of credential compromise<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0high: |<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0\u00a0\u00a0- New IAM user with admin privileges<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0\u00a0\u00a0- MFA disabled on privileged account<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">indicators:<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0- name: \"Root account usage\"<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0\u00a0\u00a0severity_boost: critical<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0- name: \"IAM privilege escalation\"<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0\u00a0\u00a0severity_boost: high<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">false_positives:<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0- pattern: \"CI\/CD automation\"<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><code class=\"framer-text framer-styles-preset-8ji2eo\">\u00a0\u00a0\u00a0\u00a0mitigation: \"Filter by known automation role ARNs\"<\/code><\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">The objective is plain English. The severity guidelines tell the model how to calibrate its judgment. The false positives encode things we&#8217;ve already learned aren&#8217;t worth alerting on. The <code class=\"framer-text framer-styles-preset-8ji2eo\">enable_*_tools<\/code> flags control which APIs the model can call during the hunt, since you don&#8217;t want the CloudTrail hunt wasting time querying CrowdStrike if it&#8217;s not relevant for the investigation.<\/p>\n<h2 dir=\"ltr\" class=\"framer-text framer-styles-preset-3gqujr\">Multi-phase analysis: Why single-shot doesn&#8217;t work<\/h2>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">The first version of this was simple. Essentially, we told Claude the objective, gave it the tools, and tried to one-shot it. The results were bad, full of vague findings and surface-level observations. The model would make three or four queries, see something mildly unusual, and call it a day.<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">During autonomous operations, Claude can overstate findings and occasionally invent evidence it didn&#8217;t actually find. That said, this is a moving target. As model security capabilities improve, especially around evidence handling and autonomous investigation, we expect this phase to get better, not disappear. For now, we still treat explicit validation as part of the system design.<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">The fix was to structure the investigation the way a human would. A real analyst doesn&#8217;t glance at one dashboard and file a report. They dig through data, build a picture of normal behavior, compare what they&#8217;re seeing against that baseline, pull in additional context, and only then decide whether something is worth escalating. So the analysis runs in five phases:<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><strong class=\"framer-text\">Phase 1: Data gathering.<\/strong> Uses Sonnet, the faster and cheaper model. It makes up to 100 tool calls: querying Datadog logs, running Snowflake SQL, calling the CloudTrail API, looking up Okta users, etc. This is pure legwork. Sonnet costs a fifth of what Opus does, and speed matters more than reasoning depth when you&#8217;re just pulling data. (We tried Opus with extended thinking for every phase early on. It&#8217;s better for reasoning but irrelevant for data gathering. Switching Phase 1 to Sonnet alone cut costs by about 60%.)<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><strong class=\"framer-text\">Phase 2: Baseline comparison.<\/strong> Switches to Opus with extended thinking.. It compares the data against 1-day and 7-day baselines. It looks for shifts in volume, rarity, and sequence: whether an API call is new for that actor, whether the same behavior appeared in the prior day or week, whether peers in the same role do similar things, and whether the activity clusters around other suspicious events.<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><strong class=\"framer-text\">Phase 3: Context enrichment and analysis.<\/strong> Still Opus. For any actor, resource, IP, service account, or behavior pattern that looks off, it pulls full context: activity timelines, peer behavior, privilege level, ownership metadata, historical alerts, and whether the pattern has appeared before.\u00a0<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><strong class=\"framer-text\">Phase 4: Confidence scoring.<\/strong> Still Opus with extended thinking, the model assigns a confidence level to each potential finding. We\u2019ve tuned this to reduce noise for our team and ensure we\u2019re prioritizing investigations appropriately.\u00a0<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><strong class=\"framer-text\">Phase 5: Adversarial validation.<\/strong> This is the phase we spent the most time on and it wasn&#8217;t in the original design. The first version only had four phases, and about a third of confirmed findings were things a human would have dismissed with 30 seconds of context. Adding this step cut the noise substantially. Each surviving finding gets one more pass where the model is explicitly instructed to argue against itself. It has to:<\/p>\n<ol dir=\"auto\" class=\"framer-text\" style=\"--max-list-digits:1\">\n<li data-preset-tag=\"p\" class=\"framer-text framer-styles-preset-21ogod\">\n<p class=\"framer-text framer-styles-preset-21ogod\">Call <code class=\"framer-text framer-styles-preset-8ji2eo\">get_finding_pattern_history<\/code> to check if this type of finding has historically been a false positive (specifically: &gt;80% FP rate with at least 3 prior occurrences).<\/p>\n<\/li>\n<li data-preset-tag=\"p\" class=\"framer-text framer-styles-preset-21ogod\">\n<p class=\"framer-text framer-styles-preset-21ogod\">Call <code class=\"framer-text framer-styles-preset-8ji2eo\">get_actor_finding_history<\/code> to see whether this actor has been flagged before and what happened. If the actor has repeatedly triggered the same finding and analysts marked it false positive, that lowers confidence; if prior findings were confirmed or unresolved, it increases the reason to keep the alert.<\/p>\n<\/li>\n<li data-preset-tag=\"p\" class=\"framer-text framer-styles-preset-21ogod\">\n<p class=\"framer-text framer-styles-preset-21ogod\">Actively search for counter-evidence. Is there a legitimate business reason for this behavior? Is the &#8220;suspicious&#8221; IP actually a corporate VPN? Is the &#8220;anomalous&#8221; API call coming from a known automation account?<\/p>\n<\/li>\n<\/ol>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">The model can only reject a finding if it produces specific counter-evidence. It avoids claims like &#8220;this seems normal&#8221;, instead it adds concrete proof like a matching allowlist entry, a documented service account, or a finding pattern with an established FP history. If the counter-evidence is vague or absent, the default is KEEP.<\/p>\n<h2 dir=\"ltr\" class=\"framer-text framer-styles-preset-3gqujr\">Handling noise: False positives &amp; deduplication<\/h2>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">To reduce noisy alerts, we need to handle false positives and duplicates (where the same real finding appears more than once, due to suspicious activity persisting across multiple hunts).\u00a0<\/p>\n<h3 dir=\"ltr\" class=\"framer-text framer-styles-preset-lib3m2\">False positives<\/h3>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">When we write a hunt, we include patterns we already know are benign in the YAML. For the CloudTrail hunt, that&#8217;s CI\/CD automation from Terraform, AWS service-linked role activity, SSO federation events, and auto-scaling. The model gets these before it starts, so it doesn&#8217;t waste time investigating them (or worse, flagging them).<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">We also create a feedback loop where if an analyst clicks \u201cfalse positive\u201d on a Slack alert, that action flows to Jira and syncs back to Snowflake. Before each hunt runs, a SQL query checks the last 90 days of resolved alerts, and any finding with 3+ occurrences and an 80%+ false positive rate gets injected into the system prompt as a known FP (surfacing during <code class=\"framer-text framer-styles-preset-8ji2eo\">get_finding_pattern_history<\/code> in Phase 5). This frequency-based filter is simple, it works, and it\u2019s easy to debug if something goes wrong.<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">Finally, Phase 5 (adversarial validation) made a huge difference in production quality. Without this phase where the model argues against itself, about 40% of findings were noise. With it, most of what surfaces is worth looking at.<\/p>\n<h3 dir=\"ltr\" class=\"framer-text framer-styles-preset-lib3m2\">Deduplication<\/h3>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">Each finding gets a fingerprint: a SHA-256 hash of the hunt ID, title, affected actor, and severity. Before insertion, the system checks the ALERTS table for anything with a matching fingerprint or exact title match in the last 48 hours. If there&#8217;s a potential match, a separate Claude call (Sonnet, single-shot, max 50 tokens) compares the new finding with the existing one and decides: DUPLICATE or NEW.<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">This prevents the same finding from generating a new Slack alert every day. If the underlying activity is still happening, the existing ticket stays open. If something meaningfully changed (different actor, different severity, different evidence) it comes through as a new alert.<\/p>\n<h2 dir=\"ltr\" class=\"framer-text framer-styles-preset-3gqujr\">Lessons from building this<\/h2>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">A few things we&#8217;d tell ourselves if we were starting over:<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><strong class=\"framer-text\">The data pipeline matters more than the AI.<\/strong> Get your logs into a queryable store like Snowflake or BigQuery. If your logs aren&#8217;t centralized it\u2019ll be harder to orchestrate a detection system that scales.\u00a0<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><strong class=\"framer-text\">Existing observability spend is a cheat code.<\/strong> If you already have Datadog, you already have 14 days of searchable logs, metrics, and traces. The Datadog bill remains the same regardless of use. The AI reads from the same APIs your engineers use for debugging with its own dedicated security role.\u00a0<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><strong class=\"framer-text\">Build the feedback loop before deploying anything.<\/strong> The system that lets analysts mark false positives, stores those resolutions, and feeds them back into future hunts needs to exist from day one. Without it, you generate noise, people tune out, and you spend weeks rebuilding trust.<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><strong class=\"framer-text\">Do signal-to-noise tuning in staging mode.<\/strong> Every new hunt should run against production data but write to a staging table, meaning it doesn\u2019t generate any alerts until we\u2019ve found the right signal-to-noise ratio. We started with two hunts on our highest-risk data source and expanded from there. Our first CloudTrail hunt generated 15+ findings per run without tuning it in staging. After staging, it settled to 2\u20133.<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><strong class=\"framer-text\">Start narrow on tools.<\/strong> The system has 40+ tools across 7 categories now, but we didn&#8217;t need all of them from day one. The first five hunts used Datadog and Snowflake only. AWS, Okta, GitHub, CrowdStrike, and web tools got added over time as specific hunts needed them. Having a large set of tools available to the LLM might bloat up the LLM\u2019s context window and lower the signal to noise ratio.<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><strong class=\"framer-text\">Cost tracking is not optional.<\/strong> We didn&#8217;t add it until a month in and had to reconstruct token usage from CloudWatch logs. Now every hunt logs its token counts and computed cost to DynamoDB on completion. Build that into the pipeline before you deploy.<\/p>\n<h2 dir=\"ltr\" class=\"framer-text framer-styles-preset-3gqujr\">Where it stands today<\/h2>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">Twenty hunts run daily. SQL detections run every 15 minutes. Fifteen log sources are monitored for ingestion health (a separate health check system verifies whether expected log volumes are arriving).<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">The weekly automated report posts to Slack every Friday: how many hunts ran, how many findings, how many were marked as FPs, token usage, and cost. The team reviews it to decide whether any hunts need tuning.<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\">For a healthcare company, this is about the trust that therapists and clients place in the platform when they share sensitive information. Proactive coverage across every system, every day, is part of how we take that seriously and building it on infrastructure we already had made it possible at a scale that wouldn&#8217;t have been realistic otherwise.<\/p>\n<p dir=\"ltr\" class=\"framer-text framer-styles-preset-21ogod\"><em class=\"framer-text\">Grow Therapy is building technology to make therapy more accessible. We&#8217;re hiring across engineering.\u00a0 <\/em><!--$--><a class=\"framer-text framer-styles-preset-1ff8vye\" href=\"http:\/\/link\/\" rel=\"\"><em class=\"framer-text\">See open roles<\/em><\/a><!--\/$--><em class=\"framer-text\">.<\/em><\/p>\n<\/div>\n<p><a href=\"https:\/\/engineering.growtherapy.com\/post\/threat-hunt-ai-how-we-built-an-ai-security-analyst-on-aws-for-under-500-month?utm_source=tldrinfosec\">Source link <\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This article was written by Pablo Vidal. As a healthcare platform, we require proactive security coverage across every system that touches sensitive data. We run a suite of automated threat hunts every day across AWS, Okta, Google Workspace, GitHub, CrowdStrike, Slack, Stripe, and a dozen other services.\u00a0 The whole thing costs about $500\/month. It runs [&hellip;]<\/p>\n","protected":false},"author":16,"featured_media":23618,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[143],"tags":[],"class_list":["post-23617","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai"],"_links":{"self":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts\/23617","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/users\/16"}],"replies":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/comments?post=23617"}],"version-history":[{"count":0,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts\/23617\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/media\/23618"}],"wp:attachment":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/media?parent=23617"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/categories?post=23617"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/tags?post=23617"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}