Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions agent/llm/skill-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -29,7 +30,7 @@ const CATEGORY_MAP: Record<AttackResult["attackType"], FindingCategory> = {
SQL_INJECTION: "rce",
PORT_SCAN: "misconfig",
SSL_CHECK: "misconfig",
CREDENTIAL_EXPOSURE: "exposed-secret"
CREDENTIAL_EXPOSURE: "exposed-secret",
};

function hashEvidence(blob: string) {
Expand Down Expand Up @@ -105,10 +106,16 @@ const RUNNERS: Record<string, SkillRunner> = {
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 {
Expand All @@ -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(() => {});

Expand Down
15 changes: 13 additions & 2 deletions agent/llm/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>;
Expand Down Expand Up @@ -326,7 +336,8 @@ const HANDLERS: Record<string, ToolHandler> = {
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<string> {
Expand Down
128 changes: 128 additions & 0 deletions agent/orchestrator.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<string, string>)[s] ?? "⚪";
}

export async function runOrchestratedScan(targetUrl: string): Promise<void> {
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`);
}
57 changes: 57 additions & 0 deletions agent/specialists/agents.ts
Original file line number Diff line number Diff line change
@@ -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;
93 changes: 93 additions & 0 deletions agent/specialists/base.ts
Original file line number Diff line number Diff line change
@@ -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<SpecialistResult> {
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 };
}
}
}
Loading
Loading