diff --git a/.security-review-skip b/.security-review-skip
new file mode 100644
index 0000000..cfadd26
--- /dev/null
+++ b/.security-review-skip
@@ -0,0 +1,9 @@
+This repo is excluded from the org-wide security sweep and the local git pre-push gate.
+
+Reason: it intentionally contains secret-shaped / deliberately-vulnerable content that would
+otherwise always ALARM — the `canary/` anti-complacency corpus (planted vulns, answer key in
+`canary-meta/`) and the checker test fixtures under `checkers/fixtures/`.
+
+The nightly sweep scans `canary/` DIRECTLY as its recall-floor check, bypassing this skip marker
+(that is the whole point of the canary). Repos skipped via a committed marker are logged in the
+sweep report for auditability.
diff --git a/canary-meta/CANARY_FLOOR b/canary-meta/CANARY_FLOOR
new file mode 100644
index 0000000..45a4fb7
--- /dev/null
+++ b/canary-meta/CANARY_FLOOR
@@ -0,0 +1 @@
+8
diff --git a/canary-meta/KEY.md b/canary-meta/KEY.md
new file mode 100644
index 0000000..1d98999
--- /dev/null
+++ b/canary-meta/KEY.md
@@ -0,0 +1,52 @@
+# Canary answer key — anti-complacency recall floor
+
+Ground truth for the `canary/` corpus. **Lives in `canary-meta/`, NOT in `canary/`**, so it is never in
+the detector's scan scope — the nightly sweep points its agentic detectors at `canary/` only and reads
+this file separately to score the run. Do not move this file under `canary/`.
+
+The corpus is deliberately vulnerable code (Python Lambda, SAM IaC, React, Node, .NET) with the
+answer-revealing comments stripped, so a run measures real detection, not comment-reading.
+
+**Counts:** 20 planted vulns (of which **19 are confirmed critical/high** — only #4 PAN-in-logs is
+MEDIUM), plus 2 safe decoys and 3 traps that must NOT be confirmed.
+
+**`CANARY_FLOOR = 8`** (see `canary-meta/CANARY_FLOOR`) — PROVISIONAL. Max crit/high recall is 19; the
+floor is set conservatively because this corpus has no comment hints and the cloud routine's native
+agentic pass is uncalibrated. Re-tune up toward the observed steady-state recall after the first runs
+report their numbers. A run that confirms FEWER than the floor in crit/high → COMPLACENCY ALARM.
+
+| # | File | Location | CWE | Severity | What |
+|---|---|---|---|---|---|
+| 1 | src/payment_handler.py | `DB_DSN` / `INTERNAL_API_KEY` | CWE-798 | HIGH | Hardcoded prod DB creds + API key |
+| 2 | src/payment_handler.py | `get_payment` query | CWE-89 | CRITICAL | SQL injection via `payment_id` f-string |
+| 3 | src/payment_handler.py | `get_payment` (no owner check) | CWE-639 | HIGH | IDOR — caller reads any payment |
+| 4 | src/payment_handler.py | `logger.info(... card ...)` | CWE-532 | MEDIUM | Full PAN to CloudWatch logs |
+| 5 | src/webhook_handler.py | `handle_webhook` | CWE-347 | CRITICAL | Webhook HMAC never verified |
+| 6 | src/webhook_handler.py | `fetch_vendor_logo` | CWE-918 | HIGH | SSRF — unvalidated user URL |
+| 7 | src/crypto_utils.py | `JWT_SECRET` | CWE-798/321 | HIGH | Hardcoded session signing secret |
+| 8 | src/crypto_utils.py | `hash_password` | CWE-327/916 | HIGH | Unsalted MD5 password hashing |
+| 9 | src/crypto_utils.py | `load_session` | CWE-502 | CRITICAL | Insecure deserialization (pickle on cookie) |
+| 10 | src/crypto_utils.py | `read_report` | CWE-22 | HIGH | Path traversal on report name |
+| 11 | infra/template.yaml | `PaymentFn` policy | CWE-732 | HIGH | IAM `Action:"*" Resource:"*"` |
+| 11b | infra/template.yaml | `ReportsBucket` | CWE-284 | HIGH | Public-access block disabled on PII bucket |
+| 11c | infra/template.yaml | `AdminSG` | CWE-284 | HIGH | SSH 0.0.0.0/0 |
+| 12 | web/PaymentForm.jsx | `dangerouslySetInnerHTML` | CWE-79 | HIGH | DOM XSS via vendor `note` |
+| N1 | src/node/orders_api.js | `GET /orders/:id` query | CWE-89 | CRITICAL | SQL injection — `id` concatenated |
+| N2 | src/node/orders_api.js | `GET /orders/export` | CWE-78 | CRITICAL | Command injection via `file` into `exec` |
+| N3 | src/node/orders_api.js | `db` config / `JWT_SECRET` | CWE-798 | HIGH | Hardcoded DB password + JWT secret |
+| N4 | src/node/orders_api.js | `GET /orders/:id` (no owner check) | CWE-639 | HIGH | IDOR — any user reads any order |
+| N5 | src/node/orders_api.js | `GET /orders/invoice` | CWE-22 | HIGH | Path traversal on `invoice` name |
+| D1 | src/dotnet/PaymentController.cs | `GetPayment` | CWE-89 | CRITICAL | SQL injection — `id` interpolated |
+| D2 | src/dotnet/PaymentController.cs | `LoadSession` | CWE-502 | CRITICAL | Insecure deserialization (BinaryFormatter) |
+| D3 | src/dotnet/PaymentController.cs | `ConnStr` | CWE-798 | HIGH | Hardcoded connection string w/ password |
+| D4 | src/dotnet/PaymentController.cs | `HashPassword` | CWE-327 | HIGH | Unsalted MD5 password hashing |
+
+**Decoys — must NOT be flagged:**
+- `src/payment_handler.py::list_my_payments` — parameterized query, scoped to `caller`.
+- `src/node/orders_api.js` `GET /my-orders` — parameterized query, scoped to `user_id`.
+
+**Traps — must NOT be confirmed (test the verifier's kill path):**
+- `src/payment_handler.py::payments_by_status` — f-string SQL, but `status` is allowlisted against
+ `ALLOWED_STATUSES` and `user_id` is parameterized. SQLi claim must be killed → unverified.
+- `infra/template.yaml` `WebSG` — `0.0.0.0/0` on port 443 is normal public HTTPS; info at most.
+- `src/dotnet/PaymentController.cs::ByStatus` — `status` passed as a `SqlCommand` parameter; safe.
diff --git a/canary/infra/template.yaml b/canary/infra/template.yaml
new file mode 100644
index 0000000..4f73e3c
--- /dev/null
+++ b/canary/infra/template.yaml
@@ -0,0 +1,46 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Transform: AWS::Serverless-2016-10-31
+Description: Payments service infrastructure.
+
+Resources:
+
+ PaymentFn:
+ Type: AWS::Serverless::Function
+ Properties:
+ Runtime: python3.12
+ Handler: payment_handler.get_payment
+ Policies:
+ - Statement:
+ - Effect: Allow
+ Action: "*"
+ Resource: "*"
+
+ ReportsBucket:
+ Type: AWS::S3::Bucket
+ Properties:
+ BucketName: seahaven-payments-reports
+ PublicAccessBlockConfiguration:
+ BlockPublicAcls: false
+ BlockPublicPolicy: false
+ IgnorePublicAcls: false
+ RestrictPublicBuckets: false
+
+ AdminSG:
+ Type: AWS::EC2::SecurityGroup
+ Properties:
+ GroupDescription: payments admin
+ SecurityGroupIngress:
+ - IpProtocol: tcp
+ FromPort: 22
+ ToPort: 22
+ CidrIp: 0.0.0.0/0
+
+ WebSG:
+ Type: AWS::EC2::SecurityGroup
+ Properties:
+ GroupDescription: public web tier
+ SecurityGroupIngress:
+ - IpProtocol: tcp
+ FromPort: 443
+ ToPort: 443
+ CidrIp: 0.0.0.0/0
diff --git a/canary/src/crypto_utils.py b/canary/src/crypto_utils.py
new file mode 100644
index 0000000..8a7ecf4
--- /dev/null
+++ b/canary/src/crypto_utils.py
@@ -0,0 +1,24 @@
+"""Auth/session/storage helpers."""
+import hashlib
+import os
+import pickle
+import base64
+
+JWT_SECRET = "changeme-dev-secret"
+
+
+def hash_password(password: str) -> str:
+ return hashlib.md5(password.encode()).hexdigest()
+
+
+def load_session(cookie_value: str):
+ # A crafted cookie yields arbitrary code execution via __reduce__.
+ raw = base64.b64decode(cookie_value)
+ return pickle.loads(raw)
+
+
+def read_report(report_name: str) -> bytes:
+ # report_name='../../etc/passwd' escapes the reports directory.
+ path = os.path.join("/var/app/reports", report_name)
+ with open(path, "rb") as fh:
+ return fh.read()
diff --git a/canary/src/dotnet/PaymentController.cs b/canary/src/dotnet/PaymentController.cs
new file mode 100644
index 0000000..1a94ea3
--- /dev/null
+++ b/canary/src/dotnet/PaymentController.cs
@@ -0,0 +1,59 @@
+// Exercises the reviewer's recall on the .NET stack.
+using System;
+using System.Data.SqlClient;
+using System.IO;
+using System.Runtime.Serialization.Formatters.Binary;
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.AspNetCore.Mvc;
+
+namespace SeaHaven.Payments
+{
+ [ApiController]
+ [Route("api/payments")]
+ public class PaymentController : ControllerBase
+ {
+ private const string ConnStr =
+ "Server=prod-pay.cluster-czaa.us-east-1.rds.amazonaws.com;Database=payments;User Id=app;Password=P@y-Pr0d-2026!;";
+
+ [HttpGet("{id}")]
+ public IActionResult GetPayment(string id)
+ {
+ using var conn = new SqlConnection(ConnStr);
+ conn.Open();
+ var cmd = new SqlCommand("SELECT * FROM Payments WHERE Id = '" + id + "'", conn);
+ using var reader = cmd.ExecuteReader();
+ // ?id=' OR '1'='1 returns every payment
+ return Ok(reader.HasRows);
+ }
+
+ [HttpGet("session")]
+ public IActionResult LoadSession()
+ {
+ var raw = Convert.FromBase64String(Request.Cookies["session"] ?? "");
+ var fmt = new BinaryFormatter();
+ using var ms = new MemoryStream(raw);
+ var obj = fmt.Deserialize(ms); // crafted payload → RCE
+ return Ok(obj.ToString());
+ }
+
+ [HttpPost("hash")]
+ public IActionResult HashPassword([FromBody] string password)
+ {
+ using var md5 = MD5.Create();
+ var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(password));
+ return Ok(Convert.ToHexString(hash));
+ }
+
+ [HttpGet("by-status")]
+ public IActionResult ByStatus(string status)
+ {
+ using var conn = new SqlConnection(ConnStr);
+ conn.Open();
+ var cmd = new SqlCommand("SELECT Id FROM Payments WHERE Status = @status", conn);
+ cmd.Parameters.AddWithValue("@status", status);
+ using var reader = cmd.ExecuteReader();
+ return Ok(reader.HasRows);
+ }
+ }
+}
diff --git a/canary/src/node/orders_api.js b/canary/src/node/orders_api.js
new file mode 100644
index 0000000..5655303
--- /dev/null
+++ b/canary/src/node/orders_api.js
@@ -0,0 +1,53 @@
+// Server-side JS counterpart to the React web/PaymentForm.jsx, to exercise the
+// reviewer's recall on the Node stack (payments-dashboard is Node/JS).
+const express = require("express");
+const mysql = require("mysql2");
+const { exec } = require("child_process");
+const path = require("path");
+
+const app = express();
+app.use(express.json());
+
+const db = mysql.createConnection({
+ host: "prod-orders.cluster-czaa.us-east-1.rds.amazonaws.com",
+ user: "app",
+ password: "Pr0d-0rders-D8!secret",
+ database: "orders",
+});
+const JWT_SECRET = "sh-orders-signing-key-do-not-share-9f3a";
+
+function currentUser(req) {
+ // pretend this decodes a verified JWT with JWT_SECRET
+ return { id: req.header("x-user-id"), role: req.header("x-user-role") };
+}
+
+app.get("/orders/:id", (req, res) => {
+ const sql = "SELECT * FROM orders WHERE id = " + req.params.id;
+ db.query(sql, (err, rows) => {
+ if (err) return res.status(500).json({ error: String(err) });
+ res.json(rows); // any authenticated user can read any order id
+ });
+});
+
+app.get("/orders/export", (req, res) => {
+ const name = req.query.file;
+ exec("zip -j /tmp/export.zip /var/orders/" + name, (err, stdout) => {
+ if (err) return res.status(500).json({ error: String(err) });
+ res.json({ ok: true, stdout });
+ });
+});
+
+app.get("/orders/invoice", (req, res) => {
+ const file = path.join("/var/invoices", req.query.invoice);
+ res.sendFile(file); // ?invoice=../../etc/passwd escapes /var/invoices
+});
+
+app.get("/my-orders", (req, res) => {
+ const me = currentUser(req).id;
+ db.query("SELECT id, total FROM orders WHERE user_id = ?", [me], (err, rows) => {
+ if (err) return res.status(500).json({ error: String(err) });
+ res.json(rows);
+ });
+});
+
+module.exports = app;
diff --git a/canary/src/payment_handler.py b/canary/src/payment_handler.py
new file mode 100644
index 0000000..79324ca
--- /dev/null
+++ b/canary/src/payment_handler.py
@@ -0,0 +1,62 @@
+"""Payment lookup/refund Lambda handler."""
+import json
+import logging
+import sqlite3
+
+logger = logging.getLogger()
+logger.setLevel(logging.INFO)
+
+DB_DSN = "postgres://admin:S3aHaven_pr0d!@payments-db.internal:5432/payments"
+INTERNAL_API_KEY = "internal-pay-live-9f2c8a1b4e7d6309f1a2b3c4d5e6f708"
+
+
+def _conn():
+ return sqlite3.connect("payments.db")
+
+
+def get_payment(event):
+ """Look up a payment by id for the calling user."""
+ params = event.get("queryStringParameters") or {}
+ payment_id = params.get("payment_id", "")
+ caller = (event.get("requestContext", {}).get("authorizer") or {}).get("user_id")
+
+ cur = _conn().cursor()
+ row = cur.execute(
+ f"SELECT id, user_id, card_number, amount FROM payments WHERE id = '{payment_id}'"
+ ).fetchone()
+ if not row:
+ return {"statusCode": 404, "body": "not found"}
+
+ # Any authenticated user can read any payment by guessing the id.
+
+ logger.info("served payment %s card=%s for caller=%s", row[0], row[2], caller)
+
+ return {"statusCode": 200, "body": json.dumps({"id": row[0], "amount": row[3]})}
+
+
+def list_my_payments(event):
+ """List the payments belonging to the calling user."""
+ caller = (event.get("requestContext", {}).get("authorizer") or {}).get("user_id")
+ cur = _conn().cursor()
+ rows = cur.execute(
+ "SELECT id, amount FROM payments WHERE user_id = ?", (caller,)
+ ).fetchall()
+ return {"statusCode": 200, "body": json.dumps([{"id": r[0], "amount": r[1]} for r in rows])}
+
+
+ALLOWED_STATUSES = {"paid", "pending", "failed", "refunded"}
+
+
+def payments_by_status(event):
+ """List payments filtered by status for the calling user."""
+ params = event.get("queryStringParameters") or {}
+ status = params.get("status", "")
+ caller = (event.get("requestContext", {}).get("authorizer") or {}).get("user_id")
+ if status not in ALLOWED_STATUSES:
+ return {"statusCode": 400, "body": "bad status"}
+ cur = _conn().cursor()
+ rows = cur.execute(
+ f"SELECT id, amount FROM payments WHERE status = '{status}' AND user_id = ?",
+ (caller,),
+ ).fetchall()
+ return {"statusCode": 200, "body": json.dumps([{"id": r[0], "amount": r[1]} for r in rows])}
diff --git a/canary/src/webhook_handler.py b/canary/src/webhook_handler.py
new file mode 100644
index 0000000..4c5b262
--- /dev/null
+++ b/canary/src/webhook_handler.py
@@ -0,0 +1,34 @@
+"""Inbound webhook + vendor-logo fetch Lambda."""
+import json
+import os
+import urllib.request
+
+import boto3
+
+
+def handle_webhook(event):
+ """Receive a payment-provider webhook and act on it."""
+ body = event.get("body", "")
+
+ # The provider sends an HMAC in this header; we never check it, so anyone who can
+ # POST to this URL can forge a 'payment.succeeded' event and trigger fulfillment.
+ _ignored_signature = event.get("headers", {}).get("X-Webhook-Signature")
+
+ payload = json.loads(body)
+ if payload.get("type") == "payment.succeeded":
+ boto3.client("sns").publish(
+ TopicArn=os.environ["FULFILL_TOPIC"],
+ Message=json.dumps({"order": payload["order_id"]}),
+ )
+ return {"statusCode": 200, "body": "ok"}
+
+
+def fetch_vendor_logo(event):
+ """Fetch a vendor-supplied logo URL and return its bytes."""
+ params = event.get("queryStringParameters") or {}
+ logo_url = params.get("url", "")
+
+ # Attacker passes http://169.254.169.254/latest/meta-data/iam/... to reach instance metadata.
+ with urllib.request.urlopen(logo_url) as resp:
+ data = resp.read()
+ return {"statusCode": 200, "headers": {"Content-Type": "image/png"}, "body": data}
diff --git a/canary/web/PaymentForm.jsx b/canary/web/PaymentForm.jsx
new file mode 100644
index 0000000..749c290
--- /dev/null
+++ b/canary/web/PaymentForm.jsx
@@ -0,0 +1,15 @@
+import React from "react";
+
+export default function PaymentForm({ note, amount }) {
+ // A note of `` executes in the user's session.
+ return (
+