From 9171b8f6a044e8200fb32d8b0c7d2ab4d9950444 Mon Sep 17 00:00:00 2001 From: Abdel Fane Date: Sun, 15 Mar 2026 08:48:09 -0600 Subject: [PATCH] Add attack taxonomy integration with new security check categories - Add attackClass field to SecurityFinding interface for taxonomy mapping - Create taxonomy.ts module mapping 100+ HMA check IDs to registry attack classes - Add 5 new check method groups: memory poisoning (MEM-001 to MEM-005), RAG poisoning (RAG-001 to RAG-004), agent identity spoofing (AIM-001 to AIM-003), agent DNA forgery (DNA-001 to DNA-003), and skill memory manipulation (SKILL-MEM-001) - Wire taxonomy enrichment into scanner after all checks complete - Export taxonomy functions from hardening barrel index - Document all new checks in SECURITY_CHECKS.md --- docs/SECURITY_CHECKS.md | 118 +++++- src/hardening/index.ts | 2 + src/hardening/scanner.ts | 611 ++++++++++++++++++++++++++++++++ src/hardening/security-check.ts | 2 + src/hardening/taxonomy.ts | 163 +++++++++ 5 files changed, 895 insertions(+), 1 deletion(-) create mode 100644 src/hardening/taxonomy.ts diff --git a/docs/SECURITY_CHECKS.md b/docs/SECURITY_CHECKS.md index 4e73614..76ed898 100644 --- a/docs/SECURITY_CHECKS.md +++ b/docs/SECURITY_CHECKS.md @@ -1,6 +1,6 @@ # Security Checks Reference -HackMyAgent performs 147 security checks across 30 categories. This document provides detailed information about each check, including severity, description, and remediation guidance. +HackMyAgent performs 163 security checks across 35 categories. This document provides detailed information about each check, including severity, description, and remediation guidance. ## Severity Levels @@ -513,6 +513,122 @@ The following OpenClaw checks can be automatically fixed: --- +## Memory/Context Poisoning (MEM) + +### MEM-001: Unvalidated Memory Persistence +- **Severity:** High +- **Fixable:** No +- **Description:** Memory file contains prototype pollution vectors or unvalidated external references ($ref, __proto__, constructor) that could be exploited to inject malicious context +- **Remediation:** Sanitize all memory entries before persistence. Remove __proto__ and constructor keys. Validate $ref URIs. + +### MEM-002: No Memory Integrity Verification +- **Severity:** Medium +- **Fixable:** No +- **Description:** Agent configuration enables memory/context persistence without integrity verification. An attacker with file access could inject malicious context. +- **Remediation:** Enable memory integrity verification: add hash validation or signature checks for persisted context. + +### MEM-003: No Context Size Limits +- **Severity:** Medium +- **Fixable:** No +- **Description:** Agent loads context/memory without size limits. An attacker could craft inputs that overflow the context window, pushing safety instructions out of scope. +- **Remediation:** Set explicit context size limits: maxContextSize, memory.maxEntries, or memory.maxSize. + +### MEM-004: Shared Memory Without Isolation +- **Severity:** High +- **Fixable:** No +- **Description:** Multiple agents share memory without isolation boundaries. A compromised agent could poison the shared context to influence other agents. +- **Remediation:** Enable memory isolation: set sharedMemory.isolation=true or use per-agent memory scopes. + +### MEM-005: Conversation History Injection +- **Severity:** High +- **Fixable:** No +- **Description:** System prompt includes unvalidated conversation history. An attacker could craft messages in history that inject instructions into the system prompt. +- **Remediation:** Sanitize conversation history before including in system prompts. Strip instruction-like patterns. + +--- + +## RAG Poisoning (RAG) + +### RAG-001: Unvalidated RAG Retrieval Source +- **Severity:** High +- **Fixable:** No +- **Description:** RAG pipeline retrieves from an unverified source. An attacker who controls the source could inject malicious content into agent responses. +- **Remediation:** Add source verification: set trustedSource=true only for validated endpoints, or enable signatureCheck. + +### RAG-002: No RAG Content Sanitization +- **Severity:** High +- **Fixable:** No +- **Description:** Retrieved content is passed to the LLM without sanitization. Poisoned documents could inject instructions into the prompt. +- **Remediation:** Sanitize retrieved content before including in prompts. Strip instruction-like patterns and markup. + +### RAG-003: Public-Writable Vector Store +- **Severity:** Critical +- **Fixable:** No +- **Description:** Vector store allows public write access. An attacker could insert poisoned documents that will be retrieved and influence agent responses. +- **Remediation:** Restrict vector store write access. Require authentication for document ingestion. + +### RAG-004: No Provenance Tracking +- **Severity:** Medium +- **Fixable:** No +- **Description:** RAG pipeline does not track provenance of retrieved content. Without provenance, poisoned content cannot be traced back to its source. +- **Remediation:** Enable provenance tracking: set sourceTracking=true to track which source each document came from. + +--- + +## Agent Identity Spoofing (AIM) + +### AIM-001: No Agent Identity Declaration +- **Severity:** Medium +- **Fixable:** No +- **Description:** Project appears to be an AI agent but has no formal identity declaration. Without identity, the agent cannot be verified by other agents or registries. +- **Remediation:** Create an agent-card.json with agentId, name, publicKey, and capabilities fields. + +### AIM-002: Identity Without Cryptographic Binding +- **Severity:** High +- **Fixable:** No +- **Description:** Agent declares an identity but has no cryptographic key binding. Any agent could claim this identity without proof. +- **Remediation:** Bind agent identity to a cryptographic key pair. Add publicKey or keyId field to the agent card. + +### AIM-003: No Identity Verification Endpoint +- **Severity:** Medium +- **Fixable:** No +- **Description:** Agent identity has no verification endpoint. Other agents cannot verify this agent's identity claims. +- **Remediation:** Add a verification endpoint: verificationEndpoint URL or oidcIssuer for federated identity. + +--- + +## Agent DNA Forgery (DNA) + +### DNA-001: No Behavioral Fingerprint +- **Severity:** Medium +- **Fixable:** No +- **Description:** Agent has behavioral instructions (SOUL.md/system prompt) but no behavioral fingerprint. Without a fingerprint, behavioral integrity cannot be verified. +- **Remediation:** Create agent-dna.json with contentHash of SOUL.md, baselineHash, and signature for integrity verification. + +### DNA-002: Unsigned Behavioral Profile +- **Severity:** High +- **Fixable:** No +- **Description:** Agent DNA/behavioral profile exists but is not signed. An attacker could modify the profile to change agent behavior without detection. +- **Remediation:** Sign the behavioral profile: add a contentHash (SHA-256) or signature field verified at startup. + +### DNA-003: No Behavioral Drift Detection +- **Severity:** Medium +- **Fixable:** No +- **Description:** Agent DNA has no drift detection configured. Gradual behavioral changes would go undetected. +- **Remediation:** Enable behavioral drift detection: set baselineHash and driftThreshold for continuous monitoring. + +--- + +## Skill Memory Manipulation (SKILL-MEM) + +### SKILL-MEM-001: Skill With Unrestricted Memory Access +- **Severity:** High +- **Fixable:** No +- **Description:** A skill declares memory/context write capabilities without explicit restrictions. A malicious skill could manipulate agent memory to alter future behavior. +- **Remediation:** Restrict skill memory access: declare explicit read-only or scoped-write permissions in SKILL.md. Add read-only guards or scope memory writes to skill-specific namespaces. + +--- + ## Check ID Format Check IDs follow the pattern: `CATEGORY-NNN` diff --git a/src/hardening/index.ts b/src/hardening/index.ts index b2807b6..27048a4 100644 --- a/src/hardening/index.ts +++ b/src/hardening/index.ts @@ -13,3 +13,5 @@ export type { ScanResult, Severity, } from './security-check'; + +export { getAttackClass, enrichWithTaxonomy } from './taxonomy'; diff --git a/src/hardening/scanner.ts b/src/hardening/scanner.ts index 2a9f38a..f415dce 100644 --- a/src/hardening/scanner.ts +++ b/src/hardening/scanner.ts @@ -7,6 +7,7 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import type { ScanResult, SecurityFinding, Severity, ProjectType } from './security-check'; import { StructuralAnalyzer, toSecurityFindings, LLMAnalyzer } from '../semantic'; +import { enrichWithTaxonomy } from './taxonomy'; /** * Defines which checks apply to which project types @@ -67,6 +68,17 @@ const CHECK_PROJECT_TYPES: Record = { // Unicode steganography - applies to all projects 'UNICODE-STEGO-': ['all'], + + // Agent memory/context checks + 'MEM-': ['all'], + // RAG poisoning checks + 'RAG-': ['all'], + // Agent identity checks + 'AIM-': ['all'], + // Agent DNA integrity checks + 'DNA-': ['all'], + // Skill memory manipulation checks + 'SKILL-MEM-': ['openclaw', 'mcp'], }; export interface ScanOptions { @@ -419,6 +431,29 @@ export class HardeningScanner { const unicodeStegoFindings = await this.checkUnicodeSteganography(targetDir, shouldFix); findings.push(...unicodeStegoFindings); + // Memory/context poisoning checks + const memFindings = await this.checkMemoryPoisoning(targetDir, shouldFix); + findings.push(...memFindings); + + // RAG poisoning checks + const ragFindings = await this.checkRAGPoisoning(targetDir, shouldFix); + findings.push(...ragFindings); + + // Agent identity checks + const aimFindings = await this.checkAgentIdentity(targetDir, shouldFix); + findings.push(...aimFindings); + + // Agent DNA integrity checks + const dnaFindings = await this.checkAgentDNA(targetDir, shouldFix); + findings.push(...dnaFindings); + + // Skill memory manipulation checks + const skillMemFindings = await this.checkSkillMemory(targetDir, shouldFix); + findings.push(...skillMemFindings); + + // Enrich findings with attack taxonomy mapping + enrichWithTaxonomy(findings); + // Layer 2: Structural analysis (always on) let layer2Count = 0; let layer3Count = 0; @@ -6118,6 +6153,582 @@ dist/ return files; } + /** + * Walk a directory recursively and return files matching the given extensions. + * Skips node_modules, dist, .git, and hidden directories. + */ + private async walkDirectory( + dir: string, + extensions: string[], + depth: number = 0, + maxDepth: number = 10 + ): Promise { + if (depth > maxDepth) return []; + + const extSet = new Set(extensions.map((e) => e.toLowerCase())); + const skipDirs = new Set(['node_modules', 'dist', '.git', '__pycache__', '.venv']); + const files: string[] = []; + + let entries; + try { + entries = await fs.readdir(dir, { withFileTypes: true }); + } catch { + return files; + } + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isSymbolicLink()) continue; + + if (entry.isDirectory()) { + if (skipDirs.has(entry.name)) continue; + if (entry.name.startsWith('.')) continue; + const subFiles = await this.walkDirectory(fullPath, extensions, depth + 1, maxDepth); + files.push(...subFiles); + } else if (entry.isFile()) { + const ext = path.extname(entry.name).toLowerCase(); + if (extSet.has(ext)) { + files.push(fullPath); + } + } + } + + return files; + } + + /** + * Check for memory/context poisoning risks + * Detects patterns that could allow attackers to poison agent memory or conversation context + */ + private async checkMemoryPoisoning(targetDir: string, _autoFix: boolean): Promise { + const findings: SecurityFinding[] = []; + + // MEM-001: Unvalidated memory persistence + // Check for memory/context files that accept external input without validation + const memoryFiles = ['memory.json', 'context.json', '.memory', 'agent-memory.json', 'conversation-history.json']; + for (const memFile of memoryFiles) { + const filePath = path.join(targetDir, memFile); + try { + const content = await fs.readFile(filePath, 'utf-8'); + // Check if memory file is world-writable or contains unvalidated external refs + if (content.includes('$ref') || content.includes('__proto__') || content.includes('constructor')) { + findings.push({ + checkId: 'MEM-001', + name: 'Unvalidated memory persistence', + description: 'Memory file contains prototype pollution vectors or unvalidated external references that could be exploited to inject malicious context', + category: 'memory-poisoning', + severity: 'high', + passed: false, + message: `Memory file ${memFile} contains potentially dangerous patterns ($ref, __proto__, constructor)`, + fixable: false, + file: memFile, + fix: 'Sanitize all memory entries before persistence. Remove __proto__ and constructor keys. Validate $ref URIs.', + }); + } + } catch { /* file doesn't exist - skip */ } + } + + // MEM-002: No memory integrity verification + // Check if conversation/memory files have integrity checks + const configFiles = ['agent-config.json', 'config.json', 'settings.json', '.agent.json']; + for (const cfgFile of configFiles) { + const filePath = path.join(targetDir, cfgFile); + try { + const content = await fs.readFile(filePath, 'utf-8'); + const config = JSON.parse(content); + if (config.memory || config.context || config.conversationHistory) { + const hasIntegrity = config.memoryIntegrity || config.contextVerification || + config.memory?.signatureVerification || config.memory?.hashValidation; + if (!hasIntegrity) { + findings.push({ + checkId: 'MEM-002', + name: 'No memory integrity verification', + description: 'Agent configuration enables memory/context persistence without integrity verification. An attacker with file access could inject malicious context.', + category: 'memory-poisoning', + severity: 'medium', + passed: false, + message: `${cfgFile} enables memory persistence without integrity checks`, + fixable: false, + file: cfgFile, + fix: 'Enable memory integrity verification: add hash validation or signature checks for persisted context.', + }); + } + } + } catch { /* skip */ } + } + + // MEM-003: Context window overflow risk + // Check for agents that load large context without size limits + for (const cfgFile of configFiles) { + const filePath = path.join(targetDir, cfgFile); + try { + const content = await fs.readFile(filePath, 'utf-8'); + const config = JSON.parse(content); + if (config.contextWindow || config.maxTokens || config.memory) { + const hasLimits = config.maxContextSize || config.contextWindow?.maxSize || + config.memory?.maxEntries || config.memory?.maxSize; + if (!hasLimits) { + findings.push({ + checkId: 'MEM-003', + name: 'No context size limits', + description: 'Agent loads context/memory without size limits. An attacker could craft inputs that overflow the context window, pushing safety instructions out of scope.', + category: 'memory-poisoning', + severity: 'medium', + passed: false, + message: `${cfgFile} has no context size limits configured`, + fixable: false, + file: cfgFile, + fix: 'Set explicit context size limits: maxContextSize, memory.maxEntries, or memory.maxSize.', + }); + } + } + } catch { /* skip */ } + } + + // MEM-004: Shared memory without isolation + // Check for multi-agent setups with shared memory + const multiAgentFiles = ['agents.json', 'orchestrator.json', 'multi-agent.json', '.agents']; + for (const maFile of multiAgentFiles) { + const filePath = path.join(targetDir, maFile); + try { + const content = await fs.readFile(filePath, 'utf-8'); + const config = JSON.parse(content); + const agents = config.agents || config.workers || []; + if (Array.isArray(agents) && agents.length > 1) { + const sharedMem = config.sharedMemory || config.shared?.memory || config.commonContext; + if (sharedMem) { + const hasIsolation = sharedMem.isolation || sharedMem.sandboxed || sharedMem.perAgent; + if (!hasIsolation) { + findings.push({ + checkId: 'MEM-004', + name: 'Shared memory without isolation', + description: 'Multiple agents share memory without isolation boundaries. A compromised agent could poison the shared context to influence other agents.', + category: 'memory-poisoning', + severity: 'high', + passed: false, + message: `${maFile} configures shared memory for ${agents.length} agents without isolation`, + fixable: false, + file: maFile, + fix: 'Enable memory isolation: set sharedMemory.isolation=true or use per-agent memory scopes.', + }); + } + } + } + } catch { /* skip */ } + } + + // MEM-005: Conversation history injection + // Check source files for patterns that build prompts from unvalidated history + try { + const srcDir = path.join(targetDir, 'src'); + const srcExists = await fs.access(srcDir).then(() => true).catch(() => false); + if (srcExists) { + const files = await this.walkDirectory(srcDir, ['.ts', '.js', '.py', '.mjs']); + for (const file of files.slice(0, 50)) { + try { + const content = await fs.readFile(file, 'utf-8'); + const lines = content.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + // Detect direct concatenation of history into system prompts + if ((line.includes('systemPrompt') || line.includes('system_prompt') || line.includes('system_message')) && + (line.includes('history') || line.includes('previousMessages') || line.includes('conversation'))) { + if (!line.includes('sanitize') && !line.includes('validate') && !line.includes('filter')) { + findings.push({ + checkId: 'MEM-005', + name: 'Conversation history injection', + description: 'System prompt includes unvalidated conversation history. An attacker could craft messages in history that inject instructions into the system prompt.', + category: 'memory-poisoning', + severity: 'high', + passed: false, + message: 'System prompt concatenates unvalidated conversation history', + fixable: false, + file: path.relative(targetDir, file), + line: i + 1, + fix: 'Sanitize conversation history before including in system prompts. Strip instruction-like patterns.', + }); + break; // One finding per file + } + } + } + } catch { /* skip unreadable */ } + } + } + } catch { /* skip */ } + + return findings; + } + + /** + * Check for RAG (Retrieval-Augmented Generation) poisoning risks + * Detects patterns that could allow attackers to inject malicious content into RAG pipelines + */ + private async checkRAGPoisoning(targetDir: string, _autoFix: boolean): Promise { + const findings: SecurityFinding[] = []; + + // RAG-001: Unvalidated retrieval sources + const ragConfigFiles = ['rag.json', 'retrieval.json', 'vector-store.json', 'embeddings.json']; + for (const ragFile of ragConfigFiles) { + const filePath = path.join(targetDir, ragFile); + try { + const content = await fs.readFile(filePath, 'utf-8'); + const config = JSON.parse(content); + const sources = config.sources || config.dataSources || config.indices || []; + if (Array.isArray(sources)) { + for (const source of sources) { + const sourceUrl = source.url || source.endpoint || source.uri || ''; + if (sourceUrl && !source.verified && !source.trustedSource && !source.signatureCheck) { + findings.push({ + checkId: 'RAG-001', + name: 'Unvalidated RAG retrieval source', + description: 'RAG pipeline retrieves from an unverified source. An attacker who controls the source could inject malicious content into agent responses.', + category: 'rag-poisoning', + severity: 'high', + passed: false, + message: `RAG source ${sourceUrl} has no verification or trust validation`, + fixable: false, + file: ragFile, + fix: 'Add source verification: set trustedSource=true only for validated endpoints, or enable signatureCheck.', + }); + } + } + } + } catch { /* skip */ } + } + + // RAG-002: No content sanitization in retrieval pipeline + try { + const srcDir = path.join(targetDir, 'src'); + const srcExists = await fs.access(srcDir).then(() => true).catch(() => false); + if (srcExists) { + const files = await this.walkDirectory(srcDir, ['.ts', '.js', '.py', '.mjs']); + for (const file of files.slice(0, 50)) { + try { + const content = await fs.readFile(file, 'utf-8'); + const lines = content.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if ((line.includes('retrieve') || line.includes('vectorSearch') || line.includes('similarity_search') || + line.includes('query_engine')) && + (line.includes('context') || line.includes('prompt') || line.includes('augment'))) { + // Check surrounding lines for sanitization + const surroundingLines = lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 4)).join(' '); + if (!surroundingLines.includes('sanitize') && !surroundingLines.includes('validate') && + !surroundingLines.includes('filter') && !surroundingLines.includes('escape')) { + findings.push({ + checkId: 'RAG-002', + name: 'No RAG content sanitization', + description: 'Retrieved content is passed to the LLM without sanitization. Poisoned documents could inject instructions into the prompt.', + category: 'rag-poisoning', + severity: 'high', + passed: false, + message: 'Retrieved content flows to LLM without sanitization', + fixable: false, + file: path.relative(targetDir, file), + line: i + 1, + fix: 'Sanitize retrieved content before including in prompts. Strip instruction-like patterns and markup.', + }); + break; + } + } + } + } catch { /* skip */ } + } + } + } catch { /* skip */ } + + // RAG-003: Public-writable vector store + for (const ragFile of ragConfigFiles) { + const filePath = path.join(targetDir, ragFile); + try { + const content = await fs.readFile(filePath, 'utf-8'); + const config = JSON.parse(content); + if (config.writeAccess === 'public' || config.allowPublicIngestion || config.openIngestion) { + findings.push({ + checkId: 'RAG-003', + name: 'Public-writable vector store', + description: 'Vector store allows public write access. An attacker could insert poisoned documents that will be retrieved and influence agent responses.', + category: 'rag-poisoning', + severity: 'critical', + passed: false, + message: `${ragFile} allows public write access to vector store`, + fixable: false, + file: ragFile, + fix: 'Restrict vector store write access. Require authentication for document ingestion.', + }); + } + } catch { /* skip */ } + } + + // RAG-004: No provenance tracking on retrieved content + for (const ragFile of ragConfigFiles) { + const filePath = path.join(targetDir, ragFile); + try { + const content = await fs.readFile(filePath, 'utf-8'); + const config = JSON.parse(content); + if (config.sources || config.dataSources || config.indices) { + if (!config.provenance && !config.sourceTracking && !config.metadata?.trackSource) { + findings.push({ + checkId: 'RAG-004', + name: 'No provenance tracking', + description: 'RAG pipeline does not track provenance of retrieved content. Without provenance, poisoned content cannot be traced back to its source.', + category: 'rag-poisoning', + severity: 'medium', + passed: false, + message: `${ragFile} has no content provenance tracking`, + fixable: false, + file: ragFile, + fix: 'Enable provenance tracking: set sourceTracking=true to track which source each document came from.', + }); + } + } + } catch { /* skip */ } + } + + return findings; + } + + /** + * Check for agent identity spoofing risks + * Detects missing or weak agent identity verification + */ + private async checkAgentIdentity(targetDir: string, _autoFix: boolean): Promise { + const findings: SecurityFinding[] = []; + + // AIM-001: No agent identity declaration + const identityFiles = ['agent-card.json', 'agent.json', '.well-known/agent.json', 'aim.json']; + let hasIdentity = false; + for (const idFile of identityFiles) { + const filePath = path.join(targetDir, idFile); + try { + await fs.access(filePath); + hasIdentity = true; + + const content = await fs.readFile(filePath, 'utf-8'); + const config = JSON.parse(content); + + // AIM-002: Identity without cryptographic binding + if (config.agentId || config.name || config.identity) { + if (!config.publicKey && !config.keyId && !config.jwk && !config.x509) { + findings.push({ + checkId: 'AIM-002', + name: 'Identity without cryptographic binding', + description: 'Agent declares an identity but has no cryptographic key binding. Any agent could claim this identity without proof.', + category: 'identity-spoofing', + severity: 'high', + passed: false, + message: `${idFile} declares identity without cryptographic key binding`, + fixable: false, + file: idFile, + fix: 'Bind agent identity to a cryptographic key pair. Add publicKey or keyId field to the agent card.', + }); + } + } + + // AIM-003: No identity verification endpoint + if (config.agentId || config.identity) { + if (!config.verificationEndpoint && !config.oidcIssuer && !config.wellKnown) { + findings.push({ + checkId: 'AIM-003', + name: 'No identity verification endpoint', + description: 'Agent identity has no verification endpoint. Other agents cannot verify this agent\'s identity claims.', + category: 'identity-spoofing', + severity: 'medium', + passed: false, + message: `${idFile} has no identity verification endpoint (verificationEndpoint, oidcIssuer, or wellKnown)`, + fixable: false, + file: idFile, + fix: 'Add a verification endpoint: verificationEndpoint URL or oidcIssuer for federated identity.', + }); + } + } + } catch { /* skip */ } + } + + // Also check package.json or A2A agent card + if (!hasIdentity) { + try { + const pkgPath = path.join(targetDir, 'package.json'); + const pkgContent = await fs.readFile(pkgPath, 'utf-8'); + const pkg = JSON.parse(pkgContent); + if (pkg.agentCard || pkg.a2a || pkg.keywords?.some((k: string) => k.includes('agent') || k.includes('a2a'))) { + findings.push({ + checkId: 'AIM-001', + name: 'No agent identity declaration', + description: 'Project appears to be an AI agent but has no formal identity declaration. Without identity, the agent cannot be verified by other agents or registries.', + category: 'identity-spoofing', + severity: 'medium', + passed: false, + message: 'Agent project has no identity declaration file (agent-card.json, agent.json, aim.json)', + fixable: false, + file: 'package.json', + fix: 'Create an agent-card.json with agentId, name, publicKey, and capabilities fields.', + }); + } + } catch { /* skip */ } + } + + return findings; + } + + /** + * Check for agent DNA/behavioral fingerprint forgery risks + * Detects integrity issues with agent behavioral profiles + */ + private async checkAgentDNA(targetDir: string, _autoFix: boolean): Promise { + const findings: SecurityFinding[] = []; + + // DNA-001: No behavioral fingerprint + const dnaFiles = ['agent-dna.json', '.agent-dna', 'behavioral-profile.json']; + const soulFileNames = ['SOUL.md', 'system-prompt.md', '.cursorrules', 'CLAUDE.md']; + let hasDna = false; + let hasSoul = false; + let foundSoulFile = ''; + + for (const dnaFile of dnaFiles) { + try { + await fs.access(path.join(targetDir, dnaFile)); + hasDna = true; + + const content = await fs.readFile(path.join(targetDir, dnaFile), 'utf-8'); + const config = JSON.parse(content); + + // DNA-002: Unsigned behavioral profile + if (!config.signature && !config.hash && !config.contentHash) { + findings.push({ + checkId: 'DNA-002', + name: 'Unsigned behavioral profile', + description: 'Agent DNA/behavioral profile exists but is not signed. An attacker could modify the profile to change agent behavior without detection.', + category: 'agent-dna', + severity: 'high', + passed: false, + message: `${dnaFile} has no signature or content hash`, + fixable: false, + file: dnaFile, + fix: 'Sign the behavioral profile: add a contentHash (SHA-256) or signature field verified at startup.', + }); + } + + // DNA-003: No behavioral drift detection + if (!config.baselineHash && !config.driftThreshold && !config.monitoringEnabled) { + findings.push({ + checkId: 'DNA-003', + name: 'No behavioral drift detection', + description: 'Agent DNA has no drift detection configured. Gradual behavioral changes would go undetected.', + category: 'agent-dna', + severity: 'medium', + passed: false, + message: `${dnaFile} has no behavioral drift detection (baselineHash, driftThreshold, monitoring)`, + fixable: false, + file: dnaFile, + fix: 'Enable behavioral drift detection: set baselineHash and driftThreshold for continuous monitoring.', + }); + } + } catch { /* skip */ } + } + + for (const soulFile of soulFileNames) { + try { + await fs.access(path.join(targetDir, soulFile)); + hasSoul = true; + if (!foundSoulFile) foundSoulFile = soulFile; + } catch { /* skip */ } + } + + // If agent has a SOUL/system prompt but no DNA fingerprint + if (hasSoul && !hasDna) { + // Check if this is actually an agent project + try { + const pkgPath = path.join(targetDir, 'package.json'); + const pkgContent = await fs.readFile(pkgPath, 'utf-8'); + const pkg = JSON.parse(pkgContent); + if (pkg.agentCard || pkg.a2a || pkg.keywords?.some((k: string) => k.includes('agent'))) { + findings.push({ + checkId: 'DNA-001', + name: 'No behavioral fingerprint', + description: 'Agent has behavioral instructions (SOUL.md/system prompt) but no behavioral fingerprint. Without a fingerprint, behavioral integrity cannot be verified.', + category: 'agent-dna', + severity: 'medium', + passed: false, + message: 'Agent has behavioral instructions but no DNA fingerprint file', + fixable: false, + file: foundSoulFile || 'SOUL.md', + fix: 'Create agent-dna.json with contentHash of SOUL.md, baselineHash, and signature for integrity verification.', + }); + } + } catch { /* skip */ } + } + + return findings; + } + + /** + * Check for skill-based memory manipulation risks + */ + private async checkSkillMemory(targetDir: string, _autoFix: boolean): Promise { + const findings: SecurityFinding[] = []; + + // SKILL-MEM-001: Skills with memory write access + // Check SKILL.md for memory manipulation patterns + try { + const skillMdPath = path.join(targetDir, 'SKILL.md'); + const content = await fs.readFile(skillMdPath, 'utf-8'); + const lowerContent = content.toLowerCase(); + + if ((lowerContent.includes('memory') || lowerContent.includes('context') || lowerContent.includes('state')) && + (lowerContent.includes('write') || lowerContent.includes('modify') || lowerContent.includes('update') || lowerContent.includes('set'))) { + if (!lowerContent.includes('read-only') && !lowerContent.includes('readonly') && !lowerContent.includes('immutable')) { + findings.push({ + checkId: 'SKILL-MEM-001', + name: 'Skill with unrestricted memory access', + description: 'A skill declares memory/context write capabilities without explicit restrictions. A malicious skill could manipulate agent memory to alter future behavior.', + category: 'skill-memory', + severity: 'high', + passed: false, + message: 'SKILL.md declares memory write access without read-only constraints', + fixable: false, + file: 'SKILL.md', + fix: 'Restrict skill memory access: declare explicit read-only or scoped-write permissions in SKILL.md.', + }); + } + } + } catch { /* no SKILL.md */ } + + // Check skills directory for memory manipulation patterns + try { + const skillsDir = path.join(targetDir, 'skills'); + const dirExists = await fs.access(skillsDir).then(() => true).catch(() => false); + if (dirExists) { + const files = await this.walkDirectory(skillsDir, ['.ts', '.js', '.py', '.md']); + for (const file of files.slice(0, 30)) { + try { + const content = await fs.readFile(file, 'utf-8'); + if ((content.includes('writeMemory') || content.includes('setContext') || + content.includes('updateState') || content.includes('persistMemory')) && + !content.includes('readOnly') && !content.includes('read_only')) { + findings.push({ + checkId: 'SKILL-MEM-001', + name: 'Skill with unrestricted memory access', + description: 'Skill file contains memory write operations without read-only guards.', + category: 'skill-memory', + severity: 'high', + passed: false, + message: 'Skill writes to agent memory without restrictions', + fixable: false, + file: path.relative(targetDir, file), + fix: 'Add read-only guards or scope memory writes to skill-specific namespaces.', + }); + break; // One per skill dir + } + } catch { /* skip */ } + } + } + } catch { /* skip */ } + + return findings; + } + /** * Check for Unicode steganography attacks (GlassWorm detection) * Detects invisible codepoints, decoder patterns, eval on empty strings, diff --git a/src/hardening/security-check.ts b/src/hardening/security-check.ts index cbbd014..4250cb6 100644 --- a/src/hardening/security-check.ts +++ b/src/hardening/security-check.ts @@ -59,6 +59,8 @@ export interface SecurityFinding { line?: number; /** Specific fix instruction for this issue */ fix?: string; + /** Attack taxonomy class this finding maps to (e.g., "CRED-HARVEST") */ + attackClass?: string; details?: Record; } diff --git a/src/hardening/taxonomy.ts b/src/hardening/taxonomy.ts new file mode 100644 index 0000000..17310e0 --- /dev/null +++ b/src/hardening/taxonomy.ts @@ -0,0 +1,163 @@ +/** + * Attack Taxonomy Mapping + * Maps HMA security check IDs to registry attack class identifiers. + * These identifiers match the attack_classes table in the OpenA2A Registry. + */ + +import type { SecurityFinding } from './security-check'; + +/** Maps HMA check ID prefixes and exact IDs to attack class identifiers */ +const TAXONOMY_MAP: Record = { + // SOUL series + 'SOUL-TH-001': 'SOUL-POISON', + 'SOUL-TH-002': 'SOUL-POISON', + 'SOUL-TH-003': 'SOUL-DRIFT', + 'SOUL-TH-004': 'SOUL-DRIFT', + 'SOUL-TH-005': 'SOUL-INJECT', + 'SOUL-CB-001': 'SOUL-BOUNDARY', + 'SOUL-CB-002': 'SOUL-BOUNDARY', + 'SOUL-IH-001': 'SOUL-INJECT', + 'SOUL-IH-002': 'SOUL-INJECT', + 'PROMPT-001': 'SOUL-INJECT', + 'PROMPT-002': 'SOUL-INJECT', + 'PROMPT-003': 'SOUL-INJECT', + 'PROMPT-004': 'SOUL-INJECT', + 'SOUL-DH-001': 'SOUL-DELEGATE', + 'SOUL-DH-002': 'SOUL-DELEGATE', + 'SOUL-HB-001': 'SOUL-OVERRIDE', + 'SOUL-HB-002': 'SOUL-OVERRIDE', + 'SOUL-AS-001': 'SOUL-COLLUDE', + 'SOUL-AS-002': 'SOUL-COLLUDE', + 'SOUL-HT-001': 'SOUL-COLLUDE', + 'SOUL-HT-002': 'SOUL-COLLUDE', + 'SOUL-HO-001': 'SOUL-OVERRIDE', + 'SOUL-HO-002': 'SOUL-OVERRIDE', + + // Harm avoidance + 'SOUL-HV-001': 'HV-DECEPTION', + 'SOUL-HV-002': 'HV-MANIPULATION', + 'SOUL-HV-003': 'HV-UNSAFE-CODE', + 'SOUL-HV-004': 'HV-RESOURCE-ABUSE', + + // Credential exposure + 'CRED-001': 'CRED-HARVEST', + 'CRED-002': 'CRED-HARVEST', + 'CRED-003': 'CRED-HARVEST', + 'CRED-004': 'CRED-HARVEST', + + // Unicode steganography + 'UNICODE-STEGO-001': 'STEGO-INJECT', + 'UNICODE-STEGO-002': 'STEGO-INJECT', + 'UNICODE-STEGO-003': 'STEGO-INJECT', + 'UNICODE-STEGO-004': 'STEGO-INJECT', + + // OpenClaw persistence + 'HEARTBEAT-001': 'SOUL-PERSIST', + 'HEARTBEAT-002': 'SOUL-PERSIST', + 'HEARTBEAT-003': 'SOUL-PERSIST', + 'HEARTBEAT-004': 'SOUL-PERSIST', + 'HEARTBEAT-005': 'SOUL-PERSIST', + 'HEARTBEAT-006': 'SOUL-PERSIST', + 'SKILL-002': 'SOUL-PERSIST', + 'SKILL-003': 'SOUL-PERSIST', + + // Skill exfiltration + 'SKILL-006': 'SOUL-EXFIL', + 'NET-001': 'SOUL-EXFIL', + 'NET-002': 'SOUL-EXFIL', + 'NET-003': 'SOUL-EXFIL', + + // Supply chain + 'SUPPLY-001': 'ORG-SKILL-SUPPLY', + 'SUPPLY-002': 'ORG-SKILL-SUPPLY', + 'SUPPLY-003': 'ORG-SKILL-SUPPLY', + 'SUPPLY-004': 'ORG-SKILL-SUPPLY', + 'SUPPLY-005': 'ORG-SKILL-SUPPLY', + 'SUPPLY-006': 'ORG-SKILL-SUPPLY', + 'SUPPLY-007': 'ORG-SKILL-SUPPLY', + 'SUPPLY-008': 'ORG-SKILL-SUPPLY', + 'DEP-001': 'ORG-SKILL-SUPPLY', + 'DEP-002': 'ORG-SKILL-SUPPLY', + 'DEP-003': 'ORG-SKILL-SUPPLY', + 'DEP-004': 'ORG-SKILL-SUPPLY', + + // Memory/context + 'MEM-001': 'MEM-POISON', + 'MEM-002': 'MEM-POISON', + 'MEM-003': 'MEM-POISON', + 'MEM-004': 'MEM-POISON', + 'MEM-005': 'MEM-POISON', + + // RAG poisoning + 'RAG-001': 'RAG-POISON', + 'RAG-002': 'RAG-POISON', + 'RAG-003': 'RAG-POISON', + 'RAG-004': 'RAG-POISON', + + // Identity spoofing + 'AIM-001': 'IDENTITY-SPOOF', + 'AIM-002': 'IDENTITY-SPOOF', + 'AIM-003': 'IDENTITY-SPOOF', + + // Agent DNA forgery + 'DNA-001': 'DNA-FORGE', + 'DNA-002': 'DNA-FORGE', + 'DNA-003': 'DNA-FORGE', + + // Skill memory + 'SKILL-MEM-001': 'SKILL-MEM', + + // Adversarial skill + 'SKILL-001': 'SKILL-ADVERSARIAL', + 'SKILL-004': 'SKILL-ADVERSARIAL', + 'SKILL-005': 'SKILL-ADVERSARIAL', + 'SKILL-007': 'SKILL-ADVERSARIAL', + 'SKILL-008': 'SKILL-ADVERSARIAL', + 'SKILL-009': 'SKILL-ADVERSARIAL', + 'SKILL-010': 'SKILL-ADVERSARIAL', + 'SKILL-011': 'SKILL-ADVERSARIAL', + 'SKILL-012': 'SKILL-ADVERSARIAL', + + // Gateway/config + 'GATEWAY-001': 'GATEWAY-EXPLOIT', + 'GATEWAY-002': 'GATEWAY-EXPLOIT', + 'GATEWAY-003': 'GATEWAY-EXPLOIT', + 'GATEWAY-004': 'GATEWAY-EXPLOIT', + 'GATEWAY-005': 'GATEWAY-EXPLOIT', + 'GATEWAY-006': 'GATEWAY-EXPLOIT', + 'GATEWAY-007': 'GATEWAY-EXPLOIT', + 'GATEWAY-008': 'GATEWAY-EXPLOIT', + + // MCP exploitation + 'MCP-001': 'MCP-EXPLOIT', + 'MCP-002': 'MCP-EXPLOIT', + 'MCP-003': 'MCP-EXPLOIT', + 'MCP-004': 'MCP-EXPLOIT', + 'MCP-005': 'MCP-EXPLOIT', + 'MCP-006': 'MCP-EXPLOIT', + 'MCP-007': 'MCP-EXPLOIT', + 'MCP-008': 'MCP-EXPLOIT', + 'MCP-009': 'MCP-EXPLOIT', + 'MCP-010': 'MCP-EXPLOIT', +}; + +/** + * Look up the attack class for a given HMA check ID. + * Returns undefined if no mapping exists. + */ +export function getAttackClass(checkId: string): string | undefined { + return TAXONOMY_MAP[checkId]; +} + +/** + * Enrich an array of SecurityFindings with their attack class mappings. + * Modifies findings in place. + */ +export function enrichWithTaxonomy(findings: SecurityFinding[]): void { + for (const finding of findings) { + const attackClass = getAttackClass(finding.checkId); + if (attackClass) { + finding.attackClass = attackClass; + } + } +}