How to build an AI agent that Investigates Vulnerability Alerts A developer published a tutorial for a bounded TypeScript vulnerability-research agent that uses Valyu Search, Vercel AI SDK 7, an OpenAI model, Zod and npm's semver package to investigate an existing CVE alert. The agent retrieves public advisory evidence, compares an exact installed package version against published ranges, checks the CISA Known Exploited Vulnerabilities catalog, and produces a cited brief for human review, demonstrated on the historical Next.js CVE-2025-29927 middleware authorization bypass. The author stresses the agent does not scan applications, prove exploitability, detect compromise or apply patches, and that version-range matches and deployment conditions must be verified separately by a human. Day 19 of 30 Days of Search. A vulnerability research agent investigates an alert that you already have. It uses Valyu to retrieve public source evidence, compares your installed package version with a structured advisory, and prepares a cited brief for human review. It does not scan your application, prove exploitability, detect compromise, or apply patches. In this tutorial, we build a bounded TypeScript agent around a historical Next.js advisory, CVE-2025-29927 . Application code owns the version comparison, a CISA Known Exploited Vulnerabilities catalog lookup, and the rendered source passages. The schema has no dedicated deployment-verdict or quotation field. Free-form model-suggested questions can still contain unsupported assertions and require human review. The implementation uses Valyu Search, Vercel AI SDK 7, an OpenAI model, Zod, and npm's semver package. The complete script is included below. Both Valyu and the model provider can incur charges. Suppose a dependency alert identifies a CVE against a Next.js application. The alert gives you an identifier and a severity label. You still need to establish which versions the advisory covers, whether your deployment has the relevant conditions, and what the vendor recommends. Opening a browser is reasonable. The repetitive part is finding the original advisory, distinguishing it from copies, retaining the passages that matter, and recording what you have not checked. That is the job for this agent. A developer supplies an exact package version. The agent gathers evidence. The developer then checks the deployed artefact and its authorisation path before deciding how to remediate. For the worked example, we supply next@14.2.24 as text. We do not install or run that vulnerable package. The script only installs its own research dependencies. The reviewed GitHub advisory https://github.com/advisories/GHSA-f82v-jwr5-mffw lists the Next.js 14 affected range as = 14.0.0, < 14.2.25 . It describes an authorisation bypass when the authorisation check occurs in middleware. Those are two different observations: a version-range match and a condition to investigate. The vendor's postmortem https://vercel.com/blog/postmortem-on-next-js-middleware-bypass explains why deployment details matter. It identifies affected self-hosted configurations and discusses hosting environments that were not affected by this specific issue. Our script does not inspect or verify your hosting configuration. This is a historical case study, not an instruction to upgrade a current application to 14.2.25 . That release is the advisory's historical first patch for this branch. Choose a currently supported release after reviewing later advisories and compatibility requirements. The agent turns one known alert into an evidence packet and a set of review questions. Known CVE + exact installed version + limited deployment context | Public GitHub advisory + CISA KEV catalog | Code compares the published version ranges | Model chooses Valyu searches and follow-up questions | Code retains allowed sources and passage choices | Model selects passage IDs; code renders source text | Cited brief + unresolved deployment questions The division of work is deliberate: | Responsibility | Owner | What it establishes | |---|---|---| | Find a candidate dependency alert | Existing dependency tooling or a developer | An issue worth investigating | | Validate the advisory identity and package | Application code | This is the expected CVE, GHSA and npm package | | Compare the exact version with advisory ranges | Application code using semver | A match against the retrieved published ranges | | Look up the CVE in the retrieved KEV catalog | Application code | Listed, not listed, or unavailable in that catalog snapshot | | Choose research queries and relevant passages | Model with a Valyu Search tool | A source-backed reading path | | Verify deployment conditions and approve remediation | Developer or security team | The operational decision | Valyu is the retrieval component. It is not probing a private server or inspecting a repository in this example. Because a fluent answer can merge facts that need to stay separate. An otherwise valid citation can accompany an unsupported deployment inference. A prompt asking for caution is not a factual validation boundary. The implementation therefore has no dedicated model-authored factual-summary or deployment-verdict field. It exposes source passages with application-assigned IDs such as E1.P1 . The model selects those IDs and suggests review questions. Code retrieves the corresponding source text for the report; it does not validate the factual meaning of the free-form questions. Version ranges, historical patch fields and KEV lookup results are rendered from parsed data. Unknown passage IDs and ASCII digits 0-9 in generated questions cause validation to fail. The digit check does not block numbers written as words or all Unicode numerals, and it does not detect unsupported assertions. The application-owned runtime exposure and compromise fields always remain NOT ASSESSED . This does not prove the sources are correct or that the selected passage is relevant. It does prevent the model from rewriting a quotation into a different claim. The example uses public sources. It does not depend on a specialised cybersecurity dataset subscription. UNKNOWN . The Valyu Search reference https://docs.valyu.ai/api-reference/endpoint/search documents the REST fields used below. We use fetch for the Search request so the same abort signal can cover public-source fetches, model calls and tool requests. There is no arbitrary URL-fetch tool available to the model. Search filters are not a guarantee of complete coverage. A returned record must contain the CVE within the portion retained by the application. A rejected or missing result is a retrieval gap, not evidence that the issue does not exist. Use Node.js 24.2 or later. The example was checked on Node.js 24.12.0. Native TypeScript execution requires the erasable syntax used in this script. mkdir vulnerability-alert-agent cd vulnerability-alert-agent npm init -y npm pkg set type=module npm install ai@7.0.143 @ai-sdk/openai@4.0.96 zod@4.6.7 semver@7.8.5 Get a Valyu key from platform.valyu.ai https://platform.valyu.ai/ and an OpenAI key with access to the configured model. Store both server-side in a gitignored .env.local file: VALYU API KEY=your-valyu-key OPENAI API KEY=your-openai-key Save this as vulnerability-agent.ts . This is the same source file used for the checks accompanying this article. js import { generateText, isStepCount, Output, tool } from "ai"; import { createOpenAI } from "@ai-sdk/openai"; import { z } from "zod"; import semver from "semver"; import { writeFile } from "node:fs/promises"; export const CVE = "CVE-2025-29927"; export const GHSA = "GHSA-f82v-jwr5-mffw"; export const ADVISORY URL = https://github.com/advisories/${GHSA} ; export const KEV URL = "https://www.cisa.gov/sites/default/files/feeds/known exploited vulnerabilities.json"; export const DOMAINS = "github.com", "nextjs.org", "vercel.com", "nvd.nist.gov", "www.cisa.gov" ; export const Context = z.object { installedVersion: z.string .min 1 .max 80 .refine value = Boolean semver.valid value , "Provide an exact npm version." , hosting: z.enum "self-hosted", "vercel", "other", "unknown" , middlewareAuthorisation: z.enum "yes", "no", "unknown" , } ; export type Context = z.infer