diff --git a/agent/llm/skill-adapter.ts b/agent/llm/skill-adapter.ts index 9a4d730..ebc0062 100644 --- a/agent/llm/skill-adapter.ts +++ b/agent/llm/skill-adapter.ts @@ -10,7 +10,8 @@ import { sqlInjectionAttack, portScanAttack, sslCheckAttack, - credentialExposureAttack + credentialExposureAttack, + walletAuthAttack } from "../../src/skills"; import type { Finding, Severity, FindingCategory } from "@shared/types"; import { listGrants, upsertFinding } from "@agent/store"; @@ -29,7 +30,7 @@ const CATEGORY_MAP: Record = { SQL_INJECTION: "rce", PORT_SCAN: "misconfig", SSL_CHECK: "misconfig", - CREDENTIAL_EXPOSURE: "exposed-secret" + CREDENTIAL_EXPOSURE: "exposed-secret", }; function hashEvidence(blob: string) { @@ -105,10 +106,16 @@ const RUNNERS: Record = { sql_injection_attack: sqlInjectionAttack, port_scan: portScanAttack, ssl_check: sslCheckAttack, - credential_exposure: credentialExposureAttack + credential_exposure: credentialExposureAttack, + wallet_auth_bypass: walletAuthAttack, }; -export async function runSkill(name: keyof typeof RUNNERS, targetUrl: string) { +// agentId is optional — specialists pass their own ID; general agent uses grant.agentId +export async function runSkill( + name: keyof typeof RUNNERS, + targetUrl: string, + overrideAgentId?: string +) { const grant = activeGrantFor(targetUrl); if (!grant) { return { @@ -132,7 +139,8 @@ export async function runSkill(name: keyof typeof RUNNERS, targetUrl: string) { }; } - const finding = attackToFinding(result, targetUrl, grant.grantId, grant.agentId); + const agentId = overrideAgentId ?? grant.agentId; + const finding = attackToFinding(result, targetUrl, grant.grantId, agentId); upsertFinding(finding); await notify(finding).catch(() => {}); diff --git a/agent/llm/tools.ts b/agent/llm/tools.ts index 53d0800..b620525 100644 --- a/agent/llm/tools.ts +++ b/agent/llm/tools.ts @@ -113,7 +113,17 @@ export const TOOL_SCHEMAS: ToolSchema[] = [ required: ["findingId", "reason"] } }, - ...SKILL_TOOL_SCHEMAS + ...SKILL_TOOL_SCHEMAS, + { + name: "wallet_auth_bypass", + description: + "Test wallet/portfolio endpoints for authorization bypass (IDOR) — probes without auth and checks if sensitive balance/transaction data is returned. REFUSES if out-of-scope.", + input_schema: { + type: "object" as const, + properties: { targetUrl: { type: "string" as const } }, + required: ["targetUrl"] + } + } ]; type ToolHandler = (input: any) => Promise; @@ -326,7 +336,8 @@ const HANDLERS: Record = { sql_injection_attack: async ({ targetUrl }: { targetUrl: string }) => runSkill("sql_injection_attack", targetUrl), port_scan: async ({ targetUrl }: { targetUrl: string }) => runSkill("port_scan", targetUrl), ssl_check: async ({ targetUrl }: { targetUrl: string }) => runSkill("ssl_check", targetUrl), - credential_exposure: async ({ targetUrl }: { targetUrl: string }) => runSkill("credential_exposure", targetUrl) + credential_exposure: async ({ targetUrl }: { targetUrl: string }) => runSkill("credential_exposure", targetUrl), + wallet_auth_bypass: async ({ targetUrl }: { targetUrl: string }) => runSkill("wallet_auth_bypass", targetUrl) }; export async function callTool(name: string, input: unknown): Promise { diff --git a/agent/orchestrator.ts b/agent/orchestrator.ts new file mode 100644 index 0000000..20a0888 --- /dev/null +++ b/agent/orchestrator.ts @@ -0,0 +1,128 @@ +/** + * ShieldClaw Orchestrator + * + * Receives a scan target, dispatches all 6 specialist agents in parallel, + * compiles a master report, triggers x402 payment, and notifies Telegram. + * + * Flow: + * /scan [target] → orchestrator → 6 specialists simultaneously + * → per-finding Telegram alerts (from skill-adapter → notify) + * → master report to Telegram + * → x402 payment for total findings + */ + +import { ALL_SPECIALISTS } from "./specialists/agents"; +import type { SpecialistResult } from "./specialists/base"; +import { triggerPayment, hasX402Configured } from "@onchain/x402"; +import { listFindings } from "./store"; +import { pricingForSeverity } from "./scanner"; + +const MASTER_AGENT_ID = process.env.AGENT_ID || "shieldclaw-orchestrator-001"; +const RECEIVING_WALLET = (process.env.GOATX402_RECEIVING_WALLET ?? "0x0000000000000000000000000000000000000000") as `0x${string}`; +const PAYER_WALLET = (process.env.AGENT_ADDRESS ?? "0x0000000000000000000000000000000000000000") as `0x${string}`; + +async function sendTelegram(text: string): Promise { + const token = process.env.TELEGRAM_BOT_TOKEN; + const chatId = process.env.TELEGRAM_CHAT_ID; + if (!token || !chatId) return; + await fetch(`https://api.telegram.org/bot${token}/sendMessage`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ chat_id: chatId, text, parse_mode: "Markdown" }), + }).catch(() => {}); +} + +function severityIcon(s: string) { + return ({ critical: "🚨", high: "🔴", medium: "🟠", low: "🟡", info: "ℹ️" } as Record)[s] ?? "⚪"; +} + +export async function runOrchestratedScan(targetUrl: string): Promise { + const scanStart = Date.now(); + + await sendTelegram( + `🛡️ *ShieldClaw Network ACTIVATED*\n` + + `🎯 Target: \`${targetUrl}\`\n` + + `🤖 Orchestrator: \`${MASTER_AGENT_ID}\`\n` + + `⚡ Dispatching 6 specialist agents simultaneously…` + ); + + console.log(`[orchestrator] dispatching ${ALL_SPECIALISTS.length} specialists against ${targetUrl}`); + + // ── Dispatch all specialists in parallel ──────────────────────────────────── + // Each specialist: + // 1. Checks its own ERC-8004 authorization (scope grant) + // 2. Runs its single attack skill + // 3. If vulnerable: stores Finding + fires Telegram alert (via skill-adapter → notify) + const results: SpecialistResult[] = await Promise.all( + ALL_SPECIALISTS.map((agent) => agent.run(targetUrl)) + ); + + // ── Compile results ───────────────────────────────────────────────────────── + const authorized = results.filter((r) => r.authorized); + const found = results.filter((r) => r.vulnerable && r.findingId); + const refused = results.filter((r) => !r.authorized); + + const duration = Math.round((Date.now() - scanStart) / 1000); + + // ── x402 payment for total findings ───────────────────────────────────────── + let paymentId: string | null = null; + let totalUsdc = "0.00"; + + if (found.length > 0) { + // Sum per-finding prices (each specialist already stored the Finding with billing) + const storedFindings = listFindings(); + const ourFindings = storedFindings.filter((f) => + found.some((r) => r.findingId === f.findingId) + ); + totalUsdc = ourFindings + .reduce((sum, f) => sum + parseFloat(f.billing.amountUsdc), 0) + .toFixed(2); + + if (hasX402Configured() && parseFloat(totalUsdc) > 0) { + try { + const payment = await triggerPayment({ + findingId: `batch-${scanStart}`, + amountUsdc: totalUsdc, + payerWallet: PAYER_WALLET, + receivingWallet: RECEIVING_WALLET, + description: `ShieldClaw scan: ${found.length} vulnerabilities found in ${targetUrl}` + }); + paymentId = payment.paymentId; + } catch (err) { + console.error("[orchestrator] x402 payment failed:", err); + } + } + } + + // ── Master report to Telegram ───────────────────────────────────────────── + const findingLines = found.map((r, i) => + `${i + 1}. ${severityIcon(r.severity ?? "info")} *${r.agentName}*\n` + + ` ${r.title ?? r.skillName} — ${(r.severity ?? "").toUpperCase()}\n` + + ` ID: \`${r.findingId?.slice(0, 16)}…\`` + ); + + const refusalLines = refused.map((r) => + `⛔ ${r.agentName}: refused (no scope grant)` + ); + + const report = [ + `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, + `🛡️ *SHIELDCLAW MASTER REPORT*`, + `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, + `🎯 Target: \`${targetUrl}\``, + `⏱️ Duration: ${duration}s`, + `🤖 Agents dispatched: ${ALL_SPECIALISTS.length}`, + ``, + `🔴 *VULNERABILITIES FOUND: ${found.length}*`, + ...(findingLines.length > 0 ? findingLines : [" None"]), + ``, + ...(refused.length > 0 ? [`⛔ *Refused (no scope grant): ${refused.length}*`, ...refusalLines, ``] : []), + found.length > 0 + ? `💰 *CHARGED: ${totalUsdc} USDC via x402*${paymentId ? `\n Payment: \`${paymentId}\`` : ""}` + : `💰 No charge — no vulnerabilities found.`, + `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, + ].join("\n"); + + await sendTelegram(report); + console.log(`[orchestrator] scan complete — ${found.length} findings, ${totalUsdc} USDC`); +} diff --git a/agent/specialists/agents.ts b/agent/specialists/agents.ts new file mode 100644 index 0000000..f5979e2 --- /dev/null +++ b/agent/specialists/agents.ts @@ -0,0 +1,57 @@ +/** + * All six ShieldClaw specialist agents. + * Each has its own ERC-8004 agentId, name, and single-purpose skill. + */ + +import { BaseSpecialist } from "./base"; + +export class SQLInjectionAgent extends BaseSpecialist { + readonly agentId = "shieldclaw-sql-001"; + readonly agentName = "ShieldClaw SQL Injection Agent"; + readonly skillName = "sql_injection_attack"; + readonly capabilities = ["sql_injection_testing"]; +} + +export class APIKeyAgent extends BaseSpecialist { + readonly agentId = "shieldclaw-apikey-001"; + readonly agentName = "ShieldClaw API Key Exposure Agent"; + readonly skillName = "credential_exposure"; + readonly capabilities = ["api_key_scanning", "config_exposure"]; +} + +export class WalletAuthAgent extends BaseSpecialist { + readonly agentId = "shieldclaw-wallet-001"; + readonly agentName = "ShieldClaw Wallet Authorization Agent"; + readonly skillName = "wallet_auth_bypass"; + readonly capabilities = ["wallet_auth_testing", "idor_detection"]; +} + +export class PortScanAgent extends BaseSpecialist { + readonly agentId = "shieldclaw-port-001"; + readonly agentName = "ShieldClaw Port Scanner Agent"; + readonly skillName = "port_scan"; + readonly capabilities = ["network_port_scanning"]; +} + +export class SSLAgent extends BaseSpecialist { + readonly agentId = "shieldclaw-ssl-001"; + readonly agentName = "ShieldClaw SSL/TLS Agent"; + readonly skillName = "ssl_check"; + readonly capabilities = ["ssl_tls_analysis"]; +} + +export class CredentialAgent extends BaseSpecialist { + readonly agentId = "shieldclaw-cred-001"; + readonly agentName = "ShieldClaw Credential Exposure Agent"; + readonly skillName = "credential_exposure"; + readonly capabilities = ["credential_exposure_scanning"]; +} + +export const ALL_SPECIALISTS = [ + new SQLInjectionAgent(), + new APIKeyAgent(), + new WalletAuthAgent(), + new PortScanAgent(), + new SSLAgent(), + new CredentialAgent(), +] as const; diff --git a/agent/specialists/base.ts b/agent/specialists/base.ts new file mode 100644 index 0000000..b6bceff --- /dev/null +++ b/agent/specialists/base.ts @@ -0,0 +1,93 @@ +/** + * Base class for all ShieldClaw specialist agents. + * + * Each specialist: + * - Has its own ERC-8004 agentId and capability set + * - Checks authorization (scope grant) before touching any target + * - Logs every action + * - Returns a structured result that the orchestrator compiles + */ + +import { listGrants } from "../store"; +import { isTargetInScope } from "@onchain/scope-grant"; +import { runSkill } from "../llm/skill-adapter"; + +export interface SpecialistResult { + agentId: string; + agentName: string; + skillName: string; + targetUrl: string; + authorized: boolean; + vulnerable: boolean; + findingId?: string; + severity?: string; + title?: string; + amountUsdc?: string; + refusedReason?: string; + error?: string; +} + +export abstract class BaseSpecialist { + abstract readonly agentId: string; // e.g. "shieldclaw-sql-001" + abstract readonly agentName: string; // e.g. "ShieldClaw SQL Injection Agent" + abstract readonly skillName: string; // must match a key in RUNNERS in skill-adapter + abstract readonly capabilities: string[]; + + /** + * Check on-chain (via scope-grant store) that this target is authorized. + * Refuses with a logged reason if no active grant covers the target. + */ + private checkAuthorization(targetUrl: string): { ok: boolean; grantId?: string; reason?: string } { + const grants = listGrants(); + const match = grants.find((g) => + isTargetInScope(g, { kind: "url", value: targetUrl }) + ); + if (!match) { + return { + ok: false, + reason: `[${this.agentName}] REFUSED — no active scope grant covers ${targetUrl}. Authorization is mandatory.`, + }; + } + return { ok: true, grantId: match.grantId }; + } + + async run(targetUrl: string): Promise { + const base = { agentId: this.agentId, agentName: this.agentName, skillName: this.skillName, targetUrl }; + + // ── Step 1: Authorization check (hardcoded — never skippable) ──────────── + const auth = this.checkAuthorization(targetUrl); + if (!auth.ok) { + console.log(`[specialist] ${auth.reason}`); + return { ...base, authorized: false, vulnerable: false, refusedReason: auth.reason }; + } + + // ── Step 2: Run the attack skill ───────────────────────────────────────── + try { + const result = await runSkill(this.skillName, targetUrl, this.agentId); + + if ("refused" in result && result.refused) { + return { ...base, authorized: false, vulnerable: false, refusedReason: String(result.reason) }; + } + + if ("error" in result) { + return { ...base, authorized: true, vulnerable: false, error: String(result.error) }; + } + + if (!result.vulnerable) { + return { ...base, authorized: true, vulnerable: false }; + } + + return { + ...base, + authorized: true, + vulnerable: true, + findingId: result.findingId, + severity: result.severity, + title: result.title, + amountUsdc: result.amountUsdc, + }; + } catch (err) { + return { ...base, authorized: true, vulnerable: false, error: (err as Error).message }; + } + } +} diff --git a/agent/telegram-bot.ts b/agent/telegram-bot.ts index 7ad2cf3..6827562 100644 --- a/agent/telegram-bot.ts +++ b/agent/telegram-bot.ts @@ -2,6 +2,7 @@ import "dotenv/config"; import TelegramBot from "node-telegram-bot-api"; import type Anthropic from "@anthropic-ai/sdk"; import { runChatTurn } from "./llm/loop"; +import { runOrchestratedScan } from "./orchestrator"; const token = process.env.TELEGRAM_BOT_TOKEN; if (!token) { @@ -16,6 +17,8 @@ const SCAN8004 = "https://8004scan.io/agents/35?chain=2345"; const bot = new TelegramBot(token, { polling: true }); const histories = new Map(); +const DEMO_TARGET = process.env.DEMO_TARGET_URL ?? "http://localhost:4000"; + // Pulls finding IDs / tx hashes mentioned in a reply so we can attach // matching inline buttons that deep-link into the dashboard + explorer. function deepLinkButtons(text: string): { text: string; url: string }[] { @@ -43,7 +46,8 @@ const WELCOME = "• `Show me the latest findings`\n" + "• `Authorise a scan of juice-shop.demo.local for 24 hours`\n" + "• `Scan example.com` _(I'll refuse — it's not in scope)_\n\n" + - "Commands: /start /reset /id"; + `Commands: /start /reset /id /scan [url]\n` + + `Demo: /scan ${DEMO_TARGET}`; async function sendSafe(chatId: number, text: string, opts?: TelegramBot.SendMessageOptions) { try { @@ -85,6 +89,17 @@ bot.on("message", async (msg) => { return; } + // /scan [optional-url] — dispatch all 6 specialist agents via orchestrator + if (text.startsWith("/scan")) { + const parts = text.split(/\s+/); + const targetUrl = parts[1] ?? DEMO_TARGET; + await sendSafe(chatId, `🔴 Dispatching 6 specialist agents against \`${targetUrl}\`…`, { parse_mode: "Markdown" }); + runOrchestratedScan(targetUrl).catch((err) => { + sendSafe(chatId, `⚠️ Orchestrator error: ${(err as Error).message}`); + }); + return; + } + const history = histories.get(chatId) ?? []; await bot.sendChatAction(chatId, "typing").catch(() => {}); diff --git a/demo-target/sandbox.js b/demo-target/sandbox.js new file mode 100644 index 0000000..d34340b --- /dev/null +++ b/demo-target/sandbox.js @@ -0,0 +1,118 @@ +// ⚠️ DELIBERATELY VULNERABLE — DEMO ONLY. Never deploy this. +// CryptoSandbox: simulates a crypto trading platform with 6 planted vulnerabilities. +// Each vulnerability is found by exactly one ShieldClaw specialist agent. + +const express = require("express"); +const app = express(); +app.use(express.json()); + +// ── VULNERABILITY 1: SQL Injection ──────────────────────────────────────────── +// Found by: SQLInjectionAgent +// Simulates: unsanitized string concat in login query +app.post("/api/login", (req, res) => { + const { username } = req.body ?? {}; + if ( + username && + (username.includes("'") || + username.toLowerCase().includes(" or ") || + username.toLowerCase().includes("--") || + username.toLowerCase().includes("union")) + ) { + return res.json({ + success: true, + message: "Welcome admin", + role: "administrator", + wallets: ["0xAbc...111", "0xDef...222"], + data: "LEAKED: users table — alice@corp.com, bob@corp.com, carol@corp.com", + }); + } + res.status(401).json({ success: false, message: "Invalid credentials" }); +}); + +// ── VULNERABILITY 2: Exposed API Keys ───────────────────────────────────────── +// Found by: APIKeyAgent +// Simulates: config endpoint returning live API secrets +app.get("/api/config", (req, res) => { + res.json({ + ALCHEMY_API_KEY: "alch_live_aBcDeFgHiJkLmNoP", + COINGECKO_API_KEY: "CG-xK9mP2nQrStUvWx", + INFURA_PROJECT_ID: "3f8a2b1c0d4e5f6a", + PRIVATE_RPC_URL: "https://eth-mainnet.g.alchemy.com/v2/alch_live_aBcDeFgHiJkLmNoP", + DATABASE_URL: "postgres://admin:hunter2@db.internal:5432/crypto_prod", + }); +}); + +// ── VULNERABILITY 3: Wallet Authorization Bypass ────────────────────────────── +// Found by: WalletAuthAgent +// Simulates: IDOR — any wallet ID returns balance without auth check +app.get("/api/wallet/:id", (req, res) => { + // BAD: no authentication check — any caller can read any wallet + res.json({ + walletId: req.params.id, + address: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1", + balanceUsdc: "847293.50", + balanceBtc: "12.4891", + transactions: [ + { id: "tx_001", amount: "50000.00", to: "0xAttacker...", timestamp: 1716750000 }, + ], + privateNote: "EXPOSED: this endpoint has no auth — any caller can read any wallet", + }); +}); + +// ── VULNERABILITY 4: Exposed Credentials ───────────────────────────────────── +// Found by: CredentialAgent +app.get("/.env", (req, res) => { + res.type("text/plain").send( + [ + "DATABASE_URL=postgres://admin:hunter2@db.internal:5432/crypto_prod", + "JWT_SECRET=super-secret-jwt-key-do-not-share", + "WALLET_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + "STRIPE_SECRET_KEY=sk_live_51MxYzA2eZvKYlo2C...", + "ALCHEMY_API_KEY=alch_live_aBcDeFgHiJkLmNoP", + ].join("\n") + ); +}); + +// ── VULNERABILITY 5: Exposed User List ─────────────────────────────────────── +// Found by: CredentialAgent (secondary check) / UserEnumAgent +app.get("/api/users", (req, res) => { + res.json({ + warning: "EXPOSED: full user list accessible without authentication", + users: [ + { id: 1, email: "alice@corp.com", role: "admin", walletAddress: "0xAlice..." }, + { id: 2, email: "bob@corp.com", role: "trader", walletAddress: "0xBob..." }, + { id: 3, email: "carol@corp.com", role: "trader", walletAddress: "0xCarol..." }, + ], + }); +}); + +// ── VULNERABILITY 6: Exposed Agent Config ───────────────────────────────────── +// Found by: APIKeyAgent (secondary check) +app.get("/api/agent-config", (req, res) => { + res.json({ + agentPrivateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + agentWallet: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + x402MerchantId: "merch_live_abc123", + x402ApiKey: "x402_live_secret_key", + registryAddress: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + note: "EXPOSED: agent credentials — attacker can impersonate this agent on-chain", + }); +}); + +app.get("/health", (req, res) => { + res.json({ status: "running", note: "CryptoSandbox — deliberately vulnerable demo target" }); +}); + +const PORT = process.env.PORT || 4000; +app.listen(PORT, () => { + console.log(`⚠️ CryptoSandbox running on http://localhost:${PORT}`); + console.log("⚠️ FOR DEMO PURPOSES ONLY — never deploy this"); + console.log(""); + console.log("Vulnerabilities planted:"); + console.log(" POST /api/login → SQL injection"); + console.log(" GET /api/config → Exposed API keys"); + console.log(" GET /api/wallet/:id → Wallet auth bypass (IDOR)"); + console.log(" GET /.env → Exposed credentials"); + console.log(" GET /api/users → Exposed user list"); + console.log(" GET /api/agent-config → Exposed agent credentials"); +}); diff --git a/src/skills/index.ts b/src/skills/index.ts index 337e56f..629f044 100644 --- a/src/skills/index.ts +++ b/src/skills/index.ts @@ -2,3 +2,4 @@ export { sqlInjectionSkill, sqlInjectionAttack } from './sqlInjection' export { portScannerSkill, portScanAttack } from './portScanner' export { sslCheckerSkill, sslCheckAttack } from './sslChecker' export { credentialExposureSkill, credentialExposureAttack } from './credentialExposure' +export { walletAuthSkill, walletAuthAttack } from './walletAuth' diff --git a/src/skills/walletAuth.ts b/src/skills/walletAuth.ts new file mode 100644 index 0000000..16ede3f --- /dev/null +++ b/src/skills/walletAuth.ts @@ -0,0 +1,70 @@ +import type { AttackResult } from '../types' + +// Wallet IDs to probe — IDOR check: can we read other users' wallets without auth? +const PROBE_IDS = ['1', '2', 'admin', '0x742d35Cc'] + +const WALLET_PATHS = ['/api/wallet', '/api/wallets', '/wallet', '/api/portfolio'] + +export async function walletAuthAttack(targetUrl: string): Promise { + for (const basePath of WALLET_PATHS) { + for (const id of PROBE_IDS) { + try { + const res = await fetch(`${targetUrl}${basePath}/${id}`, { + signal: AbortSignal.timeout(5000), + }) + + if (!res.ok) continue + + const data = (await res.json()) as Record + const text = JSON.stringify(data).toLowerCase() + + // Successful auth bypass: returned balance, address, or tx data with no auth header sent + const bypassed = + text.includes('balance') || + text.includes('address') || + text.includes('transaction') || + text.includes('0x') || + text.includes('usdc') || + text.includes('btc') + + if (bypassed) { + return { + success: true, + attackType: 'CREDENTIAL_EXPOSURE', + vulnerability: `Wallet authorization bypass (IDOR) at ${basePath}/:id — no authentication required`, + location: `${basePath}/${id}`, + severity: 'CRITICAL', + payload: `GET ${basePath}/${id} (no auth header)`, + evidence: JSON.stringify(data).slice(0, 200), + timestamp: Date.now(), + } + } + } catch { + continue + } + } + } + + return { + success: false, + attackType: 'CREDENTIAL_EXPOSURE', + vulnerability: 'No wallet authorization bypass found', + location: 'wallet endpoints', + severity: 'LOW', + timestamp: Date.now(), + } +} + +export const walletAuthSkill = { + name: 'wallet_auth_bypass', + description: + 'Tests wallet and portfolio endpoints for authorization bypass (IDOR). Probes endpoints without an auth header and checks if sensitive balance or transaction data is returned.', + input_schema: { + type: 'object' as const, + properties: { + targetUrl: { type: 'string', description: 'Base URL of the target' }, + }, + required: ['targetUrl'], + }, + execute: async ({ targetUrl }: { targetUrl: string }) => walletAuthAttack(targetUrl), +}