feat(scan): add OWASP AST05 agent identity integrity check#77
feat(scan): add OWASP AST05 agent identity integrity check#77rhiannalitchfield wants to merge 2 commits into
Conversation
Add AGENT_IDENTITY_INTEGRITY scan check that detects unauthorized modifications to agent identity files (SOUL.md, MEMORY.md, IDENTITY.md). On first scan, SHA-256 hashes of any found identity files are saved to ~/.openclaw/reins/agent-identity-hashes.json as a baseline. Subsequent scans compare current hashes against the baseline and FAIL if any file has changed or a new identity file has appeared unexpectedly. Also fix pre-existing package.json syntax error where a winston dependency key was accidentally nested inside the keywords array. Closes pegasi-ai#60
| const tampered: string[] = []; | ||
| for (const [filePath, currentHash] of Object.entries(currentHashes)) { | ||
| const baselineHash = baseline[filePath]; | ||
| if (!baselineHash) { | ||
| tampered.push(`${path.basename(filePath)} (new)`); | ||
| } else if (baselineHash !== currentHash) { | ||
| tampered.push(path.basename(filePath)); | ||
| } | ||
| } | ||
|
|
||
| if (tampered.length > 0) { | ||
| return this.fail( | ||
| 'AGENT_IDENTITY_INTEGRITY', | ||
| `unexpected modification to identity file(s): ${tampered.join(', ')}`, | ||
| REMEDIATIONS.agentIdentity | ||
| ); | ||
| } |
There was a problem hiding this comment.
Deleted identity files are never flagged as tampered
The tamper loop iterates Object.entries(currentHashes), which only contains files that currently exist on disk. A file that was tracked in the baseline but has since been deleted (or had its permissions changed so readFile throws) will simply be absent from currentHashes and never compared. Concretely: if SOUL.md is in the baseline and is then deleted, foundFiles will be empty and the method returns PASS ("no agent identity files found") — the check silently reports clean even though a tracked identity file has disappeared. To close this gap the loop should also walk Object.keys(baseline) and flag any key not present in currentHashes.
| await fs.ensureDir(path.dirname(this.identityHashesPath)); | ||
| await fs.writeJson(getPreferredDataPath('agent-identity-hashes.json'), currentHashes, { spaces: 2 }); |
There was a problem hiding this comment.
Baseline write uses a different path expression than the read
ensureDir is called on path.dirname(this.identityHashesPath) but writeJson targets getPreferredDataPath(...) directly. These resolve to the same path when the file doesn't yet exist (the only moment this branch runs), but it's fragile: the directory is created for one path and the file is written to a separately-computed one. Using this.identityHashesPath for the write makes the intent explicit and removes the implicit coupling.
| await fs.ensureDir(path.dirname(this.identityHashesPath)); | |
| await fs.writeJson(getPreferredDataPath('agent-identity-hashes.json'), currentHashes, { spaces: 2 }); | |
| await fs.ensureDir(path.dirname(this.identityHashesPath)); | |
| await fs.writeJson(this.identityHashesPath, currentHashes, { spaces: 2 }); |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Two issues flagged in code review: - Deleted identity files were silently ignored because the tamper loop only iterated currently-present files. Add a second loop over baseline keys to catch files that existed at baseline time but are now absent, flagging them as (deleted). - Baseline was written via getPreferredDataPath() directly instead of this.identityHashesPath. Use the instance field consistently. - Remove unused getPreferredDataPath import.
Purpose
Implements the check requested in #60. Agent identity files (SOUL.md, MEMORY.md, IDENTITY.md) are a known vector for memory poisoning attacks (OWASP AST05). This adds a scan check that detects unauthorized modifications to those files.
What Changed
AGENT_IDENTITY_INTEGRITYcheck toSecurityScanner.tsfollowing the existing check pattern~/.openclaw/reins/agent-identity-hashes.jsonas a baseline; returns WARN~/.openclaw/and the current working directorypackage.jsonsyntax error where awinstonkey was accidentally nested inside thekeywordsarray, preventingnpm installfrom runningTesting
SOUL.mdorMEMORY.mdin~/.openclaw/reins scan- should see WARN with "baseline created"reins scanagain - should see FAIL with the filename flaggedreins scanwith no identity files present - should see PASSCloses #60