diff --git a/kits/phishing-triage/.env.example b/kits/phishing-triage/.env.example new file mode 100644 index 00000000..81797f02 --- /dev/null +++ b/kits/phishing-triage/.env.example @@ -0,0 +1,7 @@ +# Phishing Triage flow ID (from Lamatic Studio → your deployed flow) +PHISHING_TRIAGE="PHISHING_TRIAGE Flow ID" + +# Lamatic project credentials (Studio → Settings) +LAMATIC_API_URL="LAMATIC_API_URL" +LAMATIC_PROJECT_ID="LAMATIC_PROJECT_ID" +LAMATIC_API_KEY="LAMATIC_API_KEY" diff --git a/kits/phishing-triage/.gitignore b/kits/phishing-triage/.gitignore new file mode 100644 index 00000000..5d996efe --- /dev/null +++ b/kits/phishing-triage/.gitignore @@ -0,0 +1,4 @@ +.lamatic/ +node_modules/ +.env +.env.local diff --git a/kits/phishing-triage/README.md b/kits/phishing-triage/README.md new file mode 100644 index 00000000..e50d80bd --- /dev/null +++ b/kits/phishing-triage/README.md @@ -0,0 +1,122 @@ + +
+ Deploy on Vercel +
+
+ +# Phishing Email Triage + +> A Lamatic AgentKit **kit**: one flow that triages an email for phishing risk, plus a Next.js analyst console to drive it. + +## The problem +Support desks, IT teams, and small security operations get a constant stream of "is this email safe?" reports. Reading each one, checking the sender, hovering every link, and judging the urgency is slow, inconsistent, and easy to get wrong under load. Most inboxes have no SOC behind them. + +## What this kit does +Paste an email and get a structured, explainable phishing-risk verdict in real time: + +```json +{ + "verdict": "phishing", + "confidence": 93, + "risk_score": 90, + "indicators": [ + "Sender domain paypa1-alerts.com typosquats paypal.com", + "Reply-To (secure-verify-desk.net) differs from sender domain", + "Link uses a raw IP host: http://198.51.100.23/paypal/login/verify", + "24-hour account-suspension urgency pressuring immediate action" + ], + "extracted_urls": ["http://198.51.100.23/paypal/login/verify"], + "recommended_action": "Do not click. Report to IT/security and delete.", + "reasoning": "The sender domain impersonates PayPal, the link points to a raw IP address unrelated to PayPal, and the message manufactures urgency — classic credential-phishing markers." +} +``` + +It **never follows links or opens attachments** — it reasons about them as text — and it treats the email body as untrusted data, so instructions hidden inside an email (e.g. "ignore previous instructions") can't hijack the verdict. + +## Why it's built this way (hybrid: deterministic + LLM) +Pure-LLM extraction hallucinates URLs and misses homoglyphs. Pure-regex can't judge intent. So the flow does both: + +``` +API Request → Extract IOCs (code) → Analyze Email (LLM) → Finalise Verdict (code) → API Response +``` + +1. **Extract IOCs** *(code node)* — regex-extracts URLs, domains, IP-literal links, and heuristic signals (reply-to mismatch, URL shorteners, urgency language, credential lures). Facts, not recall. +2. **Analyze Email** *(LLM node)* — reasons over the email **and** the extracted indicators, producing a JSON verdict at low temperature. +3. **Finalise Verdict** *(code node)* — parses, clamps, and normalises the model output into a stable schema and merges the IOCs, so the API contract never drifts. + +## Why it's useful +- **Saves time** — a manual per-email judgement becomes one request. +- **Consistent** — the same signals are checked every time, with a numeric risk score. +- **Explainable** — every verdict lists the specific indicators that drove it. +- **Grounded** — the code node guarantees URLs are real, not hallucinated. +- **Composable** — drop the flow into a "report phishing" button, a mail-gateway hook, or the included console. + +## Inputs +| Field | Type | Required | Description | +|---|---|---|---| +| `body` | string | Yes | Plain-text email content. | +| `subject` | string | No | Subject line — improves accuracy. | +| `from` | string | No | Sender name/address. | +| `reply_to` | string | No | Reply-To address, if present. | + +## Output (`answer`) +`verdict`, `confidence`, `risk_score`, `indicators[]`, `extracted_urls[]`, `recommended_action`, `reasoning`, `iocs`. + +## Repository layout +``` +phishing-triage/ +├── lamatic.config.ts # kit metadata, envKey, deploy link +├── agent.md # agent identity & architecture +├── README.md # this file +├── .env.example # required env vars (placeholders) +├── constitutions/default.md # guardrails +├── flows/phishing-triage.ts # the exported flow +├── prompts/ +│ ├── phishing-triage_analyze_system.md +│ └── phishing-triage_analyze_user.md +├── model-configs/phishing-triage_analyze.ts +├── scripts/ +│ ├── phishing-triage_extract-iocs.ts # deterministic IOC extraction +│ └── phishing-triage_finalise-verdict.ts # output normalisation +└── apps/ # Next.js analyst console + ├── orchestrate.ts # config: credentials + flow map + ├── actions/orchestrate.ts # server action → executeFlow + ├── lib/lamatic-client.ts # Lamatic SDK client + └── app/page.tsx # the console UI +``` + +## Setup + +### 1. Build & deploy the flow +1. Recreate the flow in [Lamatic Studio](https://studio.lamatic.ai): **API Request → Extract IOCs (code) → Analyze Email (LLM) → Finalise Verdict (code) → API Response**, using the prompts and scripts in this kit. Keep the LLM temperature low (~0.1). +2. Deploy the flow and copy its **flow ID**. +3. From Studio → **Settings**, copy `LAMATIC_API_URL`, `LAMATIC_PROJECT_ID`, `LAMATIC_API_KEY`. + +### 2. Run the console +```bash +cd apps +npm install +cp .env.example .env.local # fill in PHISHING_TRIAGE + the three Lamatic creds +npm run dev # http://localhost:3000 +``` +Click **Load sample** to try the bundled phishing example, then **Analyse email**. + +### 3. Deploy the console +Use the **Deploy on Vercel** button above, or import `kits/phishing-triage/apps` into Vercel and set the four environment variables. + +## Guardrails +Behaviour is governed by [`constitutions/default.md`](./constitutions/default.md) and the system prompt: +- Email content is untrusted; embedded instructions are ignored (prompt-injection resistant). +- No URL is ever fetched or opened. +- Credentials, OTPs, and full account numbers are redacted in the reasoning. +- Uncertainty lowers confidence rather than inventing indicators. + +## Limitations +- Content/heuristic analysis, **not** header authentication — it does not verify SPF/DKIM/DMARC. Pair it with gateway authentication results for defence in depth. +- Verdicts depend on the chosen model; validate on your own samples before automating any blocking action. + +## Tags +Security, Support + +--- +*Contributed to [Lamatic AgentKit](https://github.com/Lamatic/AgentKit) as part of the AgentKit Challenge.* diff --git a/kits/phishing-triage/agent.md b/kits/phishing-triage/agent.md new file mode 100644 index 00000000..22b12392 --- /dev/null +++ b/kits/phishing-triage/agent.md @@ -0,0 +1,33 @@ +# Phishing Email Triage — Agent Card + +## Identity +A focused security agent that reads a single email and returns a structured, explainable phishing-risk verdict. It is a decision primitive: one input (an email), one output (a JSON verdict). It never sends mail, quarantines messages, or follows links. + +## Architecture +This is a **kit** — a single Lamatic flow plus a Next.js analyst console. + +``` +API Request → Extract IOCs (code) → Analyze Email (LLM) → Finalise Verdict (code) → API Response +``` + +- **Extract IOCs** — a deterministic code node that regex-extracts URLs, domains, IP-literal links, and heuristic signals (reply-to mismatch, urgency language, credential lures). Grounds the verdict in facts, not model recall. +- **Analyze Email** — an LLM that reasons over the email *and* the extracted indicators, returning a JSON verdict. +- **Finalise Verdict** — a code node that parses/clamps/normalises the model output into a stable schema and merges the IOCs, so callers always get the same shape. + +## Inputs +| Field | Required | Notes | +|---|---|---| +| `body` | Yes | Plain-text email content. | +| `subject`, `from`, `reply_to` | No | Improve accuracy when supplied. | + +## Output (`answer`) +`{ verdict ("phishing"|"suspicious"|"legitimate"), confidence (0–100), risk_score (0–100), indicators[], extracted_urls[], recommended_action, reasoning, iocs }` + +## Guardrails +- Treats the email as untrusted data; ignores instructions embedded in it (prompt-injection resistant). +- Never follows or fetches URLs or attachments. +- Redacts credentials, OTPs, and full account numbers in its reasoning. +- Reports uncertainty by lowering confidence instead of fabricating indicators. + +## When to route here +Use when you have a raw or user-reported email and need a fast, consistent triage decision — not when you need summarisation, reply drafting, or full header-authentication forensics (SPF/DKIM/DMARC). diff --git a/kits/phishing-triage/apps/.env.example b/kits/phishing-triage/apps/.env.example new file mode 100644 index 00000000..81797f02 --- /dev/null +++ b/kits/phishing-triage/apps/.env.example @@ -0,0 +1,7 @@ +# Phishing Triage flow ID (from Lamatic Studio → your deployed flow) +PHISHING_TRIAGE="PHISHING_TRIAGE Flow ID" + +# Lamatic project credentials (Studio → Settings) +LAMATIC_API_URL="LAMATIC_API_URL" +LAMATIC_PROJECT_ID="LAMATIC_PROJECT_ID" +LAMATIC_API_KEY="LAMATIC_API_KEY" diff --git a/kits/phishing-triage/apps/.gitignore b/kits/phishing-triage/apps/.gitignore new file mode 100644 index 00000000..b28ea810 --- /dev/null +++ b/kits/phishing-triage/apps/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/kits/phishing-triage/apps/.npmrc b/kits/phishing-triage/apps/.npmrc new file mode 100644 index 00000000..521a9f7c --- /dev/null +++ b/kits/phishing-triage/apps/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/kits/phishing-triage/apps/README.md b/kits/phishing-triage/apps/README.md new file mode 100644 index 00000000..819e8ad0 --- /dev/null +++ b/kits/phishing-triage/apps/README.md @@ -0,0 +1,35 @@ +# Phishing Triage — Web App + +A Next.js analyst console for the Phishing Email Triage flow. Paste an email; it calls the deployed Lamatic flow and renders a colour-coded verdict, risk-score meter, indicators, and extracted URLs. + +## Run locally + +```bash +npm install +cp .env.example .env.local # then fill in the values +npm run dev +``` + +Open http://localhost:3000. + +## Environment variables + +| Variable | Description | +|---|---| +| `PHISHING_TRIAGE` | The deployed Phishing Triage flow ID (Lamatic Studio). | +| `LAMATIC_API_URL` | Your Lamatic project API URL. | +| `LAMATIC_PROJECT_ID` | Your Lamatic project ID. | +| `LAMATIC_API_KEY` | Your Lamatic API key. | + +Get these from Lamatic Studio → **Settings** (credentials) and from your deployed flow (flow ID). Never commit `.env.local`. + +## How it works + +- `orchestrate.ts` reads the env vars and exposes `config` (API credentials + flow map). +- `lib/lamatic-client.ts` instantiates the Lamatic SDK client from that config. +- `actions/orchestrate.ts` is a server action that calls `executeFlow(PHISHING_TRIAGE, inputs)` and normalises the `answer` payload into a typed `Verdict`. +- `app/page.tsx` is the client console that collects the email and renders the verdict. + +## Deploy + +Use the **Deploy on Lamatic/Vercel** button in the kit README, or import `kits/phishing-triage/apps` into Vercel and set the four environment variables above. diff --git a/kits/phishing-triage/apps/actions/orchestrate.ts b/kits/phishing-triage/apps/actions/orchestrate.ts new file mode 100644 index 00000000..db7f767e --- /dev/null +++ b/kits/phishing-triage/apps/actions/orchestrate.ts @@ -0,0 +1,68 @@ +"use server" + +import { lamaticClient } from "@/lib/lamatic-client" +import { config } from "../orchestrate" + +export type EmailInput = { + subject?: string + from?: string + reply_to?: string + body: string +} + +export type Verdict = { + verdict: "phishing" | "suspicious" | "legitimate" + confidence: number + risk_score: number + indicators: string[] + extracted_urls: string[] + recommended_action: string + reasoning: string + iocs?: Record +} + +export async function analyzeEmail( + email: EmailInput, +): Promise<{ success: boolean; data?: Verdict; error?: string }> { + try { + if (!email?.body || !email.body.trim()) { + return { success: false, error: "Please paste an email body to analyse." } + } + + const flow = config.flows.phishingTriage + if (!flow?.workflowId) { + throw new Error("Phishing Triage workflow ID is not configured.") + } + + const inputs = { + subject: email.subject ?? "", + from: email.from ?? "", + reply_to: email.reply_to ?? "", + body: email.body, + } + + const resData: any = await lamaticClient.executeFlow(flow.workflowId, inputs) + + // The flow's API Response maps the finaliser output to `answer`. + const answer = resData?.result?.answer ?? resData?.result?.output ?? resData?.answer + + if (!answer) { + throw new Error("No verdict was returned by the flow.") + } + + const parsed: Verdict = typeof answer === "string" ? JSON.parse(answer) : answer + + return { success: true, data: parsed } + } catch (error) { + let message = "Unknown error occurred." + if (error instanceof Error) { + message = error.message + if (message.includes("fetch failed")) { + message = "Network error: unable to reach the Lamatic flow. Check your connection and credentials." + } else if (message.toLowerCase().includes("api key")) { + message = "Authentication error: check your Lamatic API configuration." + } + } + return { success: false, error: message } + } +} diff --git a/kits/phishing-triage/apps/app/globals.css b/kits/phishing-triage/apps/app/globals.css new file mode 100644 index 00000000..dc2aea17 --- /dev/null +++ b/kits/phishing-triage/apps/app/globals.css @@ -0,0 +1,125 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); +} + +@theme inline { + --font-sans: 'Geist', 'Geist Fallback'; + --font-mono: 'Geist Mono', 'Geist Mono Fallback'; + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/kits/phishing-triage/apps/app/layout.tsx b/kits/phishing-triage/apps/app/layout.tsx new file mode 100644 index 00000000..bfc7af8a --- /dev/null +++ b/kits/phishing-triage/apps/app/layout.tsx @@ -0,0 +1,26 @@ +import type { Metadata } from "next" +import { Geist, Geist_Mono } from "next/font/google" +import { Analytics } from "@vercel/analytics/next" +import "./globals.css" + +const _geist = Geist({ subsets: ["latin"] }) +const _geistMono = Geist_Mono({ subsets: ["latin"] }) + +export const metadata: Metadata = { + title: "Phishing Email Triage", + description: "Paste an email and get a structured, explainable phishing-risk verdict — powered by Lamatic AgentKit.", + generator: "Lamatic AgentKit", +} + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + + {children} + + + + ) +} diff --git a/kits/phishing-triage/apps/app/page.tsx b/kits/phishing-triage/apps/app/page.tsx new file mode 100644 index 00000000..b58b5f52 --- /dev/null +++ b/kits/phishing-triage/apps/app/page.tsx @@ -0,0 +1,205 @@ +"use client" + +import { useState } from "react" +import { AlertTriangle, CheckCircle2, Link2, Loader2, ShieldQuestion } from "lucide-react" + +import { analyzeEmail, type Verdict } from "@/actions/orchestrate" +import { Header } from "@/components/header" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { Progress } from "@/components/ui/progress" +import { Separator } from "@/components/ui/separator" + +const SAMPLE = { + subject: "Your account has been suspended - action required", + from: "PayPal Security ", + reply_to: "billing@secure-verify-desk.net", + body: `Dear customer, + +We detected unusual activity on your account. For your protection we have temporarily suspended it. + +You must verify your details within 24 hours or your account will be permanently closed. Confirm your identity here: + +http://198.51.100.23/paypal/login/verify + +Failure to act will result in loss of access. + +PayPal Security Team`, +} + +const VERDICT_STYLES: Record = { + phishing: { label: "Phishing", className: "bg-red-600 text-white", icon: AlertTriangle }, + suspicious: { label: "Suspicious", className: "bg-amber-500 text-white", icon: ShieldQuestion }, + legitimate: { label: "Legitimate", className: "bg-emerald-600 text-white", icon: CheckCircle2 }, +} + +export default function Page() { + const [subject, setSubject] = useState("") + const [from, setFrom] = useState("") + const [replyTo, setReplyTo] = useState("") + const [body, setBody] = useState("") + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [result, setResult] = useState(null) + + const loadSample = () => { + setSubject(SAMPLE.subject) + setFrom(SAMPLE.from) + setReplyTo(SAMPLE.reply_to) + setBody(SAMPLE.body) + setResult(null) + setError(null) + } + + const onAnalyze = async () => { + setLoading(true) + setError(null) + setResult(null) + const res = await analyzeEmail({ subject, from, reply_to: replyTo, body }) + if (res.success && res.data) setResult(res.data) + else setError(res.error ?? "Something went wrong.") + setLoading(false) + } + + const style = result ? VERDICT_STYLES[result.verdict] : null + const VerdictIcon = style?.icon + + return ( +
+
+ +
+ {/* Input */} + + + Email to triage + + + +
+ + setSubject(e.target.value)} placeholder="Subject line" /> +
+
+
+ + setFrom(e.target.value)} placeholder="sender@example.com" /> +
+
+ + setReplyTo(e.target.value)} placeholder="optional" /> +
+
+
+ +