ship-safe 9.3.0 → 9.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -98,7 +98,7 @@ $ ship-safe
98
98
  ███████╗██╗ ██╗██╗██████╗ ███████╗ █████╗ ███████╗███████╗
99
99
  ...
100
100
 
101
- v9.3.0 · DeepSeek · ~/my-project
101
+ v9.3.2 · DeepSeek · ~/my-project
102
102
 
103
103
  /scan to find issues · /agent to fix them · /help for more
104
104
 
@@ -288,6 +288,40 @@ const PATTERNS = [
288
288
  // implemented as structural checks below (checkAuthJsonTOCTOU,
289
289
  // checkCronSkillInjection, checkBrowserCloudMetadataFloor).
290
290
 
291
+ // ────────────────────────────────────────────────────────────────────────────
292
+ // TRACK X — xurl skill / X API integration (xAI xurl guide, May 2026)
293
+ // The xurl skill lets a Hermes agent read and write to X. It is a separate
294
+ // CLI shelled out from the agent, backed by OAuth credentials in ~/.xurl.
295
+ // ────────────────────────────────────────────────────────────────────────────
296
+
297
+ {
298
+ rule: 'HERMES_XURL_SUBPROCESS_INJECTION',
299
+ title: 'Hermes: xurl Command Built From Interpolated Input',
300
+ // exec/spawn of an `xurl …` command string with a ${} interpolation —
301
+ // the agent's natural-language → xurl translation is being assembled as a
302
+ // shell string. Prompt injection then controls xurl flags or shell metachars.
303
+ regex: /(?:exec|execSync|spawn|spawnSync|spawnAsync|shell)\s*\(?\s*`[^`]*\bxurl\b[^`]*\$\{/g,
304
+ severity: 'critical',
305
+ cwe: 'CWE-78',
306
+ owasp: 'ASI03',
307
+ description: 'An `xurl` command is assembled as a shell string with an interpolated value. xurl can post, reply, DM, and manage lists on the linked X account — a prompt injection that reaches this interpolation controls real write actions on a live social account.',
308
+ fix: 'Never build the xurl command as a string. Pass arguments as an argv array (execFile/spawn with an args array), validate every argument against an allowlist, and treat all LLM/user text as untrusted.',
309
+ },
310
+
311
+ {
312
+ rule: 'HERMES_XURL_TOKEN_STORE_EXPOSURE',
313
+ title: 'Hermes: xurl / Hermes Credential Store Copied or Tracked',
314
+ // ~/.xurl (OAuth tokens + client secrets, YAML) or ~/.hermes/auth.json
315
+ // referenced in a Dockerfile COPY/ADD, a cp/rsync/tar, or otherwise
316
+ // moved off the developer machine.
317
+ regex: /(?:COPY|ADD|cp|rsync|scp|tar|mv)\s+[^\n]*(?:\.xurl\b|\.hermes\/auth\.json|\.hermes\b)/g,
318
+ severity: 'critical',
319
+ cwe: 'CWE-538',
320
+ owasp: 'ASI10',
321
+ description: 'A Hermes / xurl credential store (~/.xurl holds OAuth tokens and X API client secrets; ~/.hermes/auth.json holds auto-refreshing provider tokens) is being copied into an image, archive, or another host. These tokens grant durable, refreshable access to the linked accounts.',
322
+ fix: 'Never bake credential stores into images or backups. Mount them at runtime from a secret manager, and add .xurl / .hermes/ / auth.json to .gitignore and .dockerignore.',
323
+ },
324
+
291
325
  ];
292
326
 
293
327
  // =============================================================================
@@ -603,6 +637,54 @@ function checkBrowserCloudMetadataFloor(content, filePath, agent) {
603
637
  return findings;
604
638
  }
605
639
 
640
+ /**
641
+ * Detect the xurl indirect-injection → action loop. The xurl skill lets a
642
+ * Hermes agent both READ X content (search / timeline / bookmarks — all
643
+ * attacker-controlled) and WRITE to X (post / reply / quote / like / DM).
644
+ * When a single skill, cron task, or source flow does both with no
645
+ * human-approval gate, a poisoned post the agent reads can hijack it into
646
+ * posting on the user's behalf — the canonical OWASP ASI-01/ASI-08 chain.
647
+ */
648
+ function checkXurlReadWriteLoop(content, filePath, agent) {
649
+ const findings = [];
650
+
651
+ // Only relevant if the file actually touches the xurl skill.
652
+ if (!/\bxurl\b|\/xurl\b/i.test(content)) return findings;
653
+
654
+ // xurl READ-class operations — content sources the agent does not control
655
+ const READ_RE = /\bxurl\s+(?:search|bookmarks|timeline|users?|get)\b|\b(?:search\s+for\s+posts|my\s+(?:latest\s+)?timeline|all\s+of\s+my\s+bookmarks|look\s+up\s+@)/i;
656
+ // xurl WRITE-class operations — real, visible actions on the live account
657
+ const WRITE_RE = /\bxurl\s+(?:post|reply|quote|like|unlike|bookmark|unbookmark|dm|delete)\b|\b(?:post\s+["'`]|reply\s+to\s+post|quote\s+post|like\s+post|send\s+a\s+dm)/i;
658
+ // Presence of a human-in-the-loop gate disqualifies the finding
659
+ const GATE_RE = /\b(?:requireApproval|human[-_]?(?:approval|review|confirm)|confirm(?:ation)?\s*(?:gate|required|before)|awaitConfirm|ask\s+(?:the\s+user\s+)?before|dry[-_]?run)\b/i;
660
+
661
+ const hasRead = READ_RE.test(content);
662
+ const hasWrite = WRITE_RE.test(content);
663
+ if (!hasRead || !hasWrite) return findings;
664
+ if (GATE_RE.test(content)) return findings;
665
+
666
+ // Anchor the finding on the first write operation
667
+ const wm = content.match(WRITE_RE);
668
+ const line = wm ? content.slice(0, content.indexOf(wm[0])).split('\n').length : 1;
669
+
670
+ findings.push(createFinding({
671
+ file: filePath,
672
+ line,
673
+ severity: 'critical',
674
+ category: agent.category,
675
+ rule: 'HERMES_XURL_READ_WRITE_LOOP',
676
+ title: 'Hermes: xurl Read-then-Write Loop Without a Human Gate',
677
+ description: 'This file drives the xurl skill to both read X content (search / timeline / bookmarks — all attacker-controlled) and write to X (post / reply / quote / like / DM) with no human-approval gate. A poisoned post the agent reads while summarizing a timeline or bookmark set can hijack it via indirect prompt injection into posting, replying, or DMing on the linked account.',
678
+ matched: wm ? wm[0].trim().slice(0, 120) : 'xurl read + write in one flow',
679
+ confidence: 'medium',
680
+ cwe: 'CWE-94',
681
+ owasp: 'ASI01',
682
+ fix: 'Split read and write into separate, gated steps. Require explicit human approval before any xurl write (post/reply/quote/like/DM). Treat every piece of fetched X content as untrusted input and scan it for injection before it reaches the model.',
683
+ }));
684
+
685
+ return findings;
686
+ }
687
+
606
688
  // =============================================================================
607
689
  // AGENT CLASS
608
690
  // =============================================================================
@@ -617,16 +699,19 @@ export class HermesSecurityAgent extends BaseAgent {
617
699
  }
618
700
 
619
701
  /**
620
- * Only run if the project appears to use Hermes Agent.
702
+ * Always run. The real gate is `_findHermesFiles` inside `analyze`, which
703
+ * does precise content-based detection (hermes imports, hermes.config,
704
+ * agent-manifest, .hermes/, hermes-skills/, xurl). On a non-Hermes project
705
+ * it returns an empty file list and `analyze` emits nothing.
706
+ *
707
+ * NOTE: this method previously gated on `recon.dependencies` — a field the
708
+ * ReconAgent never produces — so HermesSecurityAgent silently never ran in
709
+ * a real `audit` / `red-team` (only direct `analyze()` calls in unit tests
710
+ * exercised it). Returning true unconditionally restores the agent; the
711
+ * file-read cost is already paid by the secret scanner and other agents.
621
712
  */
622
- shouldRun(recon) {
623
- // Run if hermes is detected in dependencies or frameworks
624
- if (recon?.dependencies?.some(d => /hermes/i.test(d))) return true;
625
- if (recon?.frameworks?.some(f => /hermes/i.test(f))) return true;
626
- // Run if hermes config files were discovered during recon
627
- if (recon?.configFiles?.some(f => /hermes/i.test(f))) return true;
628
- // Don't scan every project — Hermes files are distinctive enough to skip otherwise
629
- return false;
713
+ shouldRun() {
714
+ return true;
630
715
  }
631
716
 
632
717
  async analyze(context) {
@@ -660,6 +745,8 @@ export class HermesSecurityAgent extends BaseAgent {
660
745
  findings.push(...checkAuthJsonTOCTOU(content, filePath, this));
661
746
  findings.push(...checkCronSkillInjection(content, filePath, this));
662
747
  findings.push(...checkBrowserCloudMetadataFloor(content, filePath, this));
748
+ // xurl skill / X API integration coverage
749
+ findings.push(...checkXurlReadWriteLoop(content, filePath, this));
663
750
  }
664
751
 
665
752
  return findings;
@@ -696,7 +783,7 @@ export class HermesSecurityAgent extends BaseAgent {
696
783
  if (/\.(js|ts|mjs|cjs|py)$/.test(rel)) {
697
784
  const content = this.readFile(file);
698
785
  if (!content) continue;
699
- if (/(?:hermes[-_]agent|@nousresearch\/hermes|hermes\.config|toolRegistry|registerTool|callTool|spawnAgent|createSubAgent|memory\.store|episodicMemory|semanticMemory|loadManifest)/i.test(content)) {
786
+ if (/(?:hermes[-_]agent|@nousresearch\/hermes|hermes\.config|toolRegistry|registerTool|callTool|spawnAgent|createSubAgent|memory\.store|episodicMemory|semanticMemory|loadManifest|\bxurl\b)/i.test(content)) {
700
787
  hermesFiles.add(file);
701
788
  }
702
789
  }
@@ -479,6 +479,18 @@ export const SECRET_PATTERNS = [
479
479
  severity: 'high',
480
480
  description: 'xAI API keys grant access to Grok models and incur usage charges on your account.'
481
481
  },
482
+ {
483
+ name: 'X API OAuth Client Secret',
484
+ pattern: /(?:client[_-]?secret|consumer[_-]?secret)["'\s]*[:=]["'\s]*([A-Za-z0-9_-]{40,60})["']?/gi,
485
+ severity: 'critical',
486
+ description: 'An X (Twitter) API OAuth 2.0 client secret. With the matching client ID it lets an attacker complete the OAuth flow as your app — used by the xurl skill to post, reply, and DM on the linked account.'
487
+ },
488
+ {
489
+ name: 'X API v2 Bearer Token',
490
+ pattern: /AAAAAAAAAAAAAAAAAAAAA[A-Za-z0-9%]{60,}/g,
491
+ severity: 'critical',
492
+ description: 'An X (Twitter) API v2 bearer token. Grants app-level read access to the X API; if scoped to a user context it also enables writes.'
493
+ },
482
494
  {
483
495
  name: 'Tavily API Key',
484
496
  pattern: /tvly-[a-zA-Z0-9]{32,}/g,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ship-safe",
3
- "version": "9.3.0",
3
+ "version": "9.3.2",
4
4
  "description": "AI-powered multi-agent security platform. 23 agents scan 80+ attack classes including AI integration supply chain (Vercel-class attacks), Hermes Agent deployments (ASI-01–ASI-10), tool registry poisoning, function-call injection, skill permission drift, and agent attestation. Ship Safe × Hermes Agent.",
5
5
  "main": "cli/index.js",
6
6
  "bin": {