shieldcortex 4.3.0 → 4.4.0

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/dist/index.js CHANGED
@@ -772,6 +772,12 @@ ${bold}DOCS${reset}
772
772
  await handleXRayCommand(process.argv.slice(3));
773
773
  process.exit(0);
774
774
  }
775
+ // Handle "xray-preinstall" subcommand — lightweight pre-install safety check
776
+ if (process.argv[2] === 'xray-preinstall') {
777
+ const { handlePreinstallCheck } = await import('./xray/preinstall.js');
778
+ await handlePreinstallCheck();
779
+ process.exit(0);
780
+ }
775
781
  // Handle "cortex" subcommand (Pro tier — mistake learning)
776
782
  if (process.argv[2] === 'cortex') {
777
783
  const { handleCortexCommand } = await import('./cortex/cli.js');
@@ -783,7 +789,7 @@ ${bold}DOCS${reset}
783
789
  'doctor', 'quickstart', 'setup', 'install', 'migrate', 'uninstall', 'hook',
784
790
  'openclaw', 'clawdbot', 'copilot', 'codex', 'service', 'config', 'status',
785
791
  'graph', 'license', 'licence', 'audit', 'iron-dome', 'scan', 'cloud',
786
- 'scan-skill', 'scan-skills', 'dashboard', 'api', 'worker', 'stats', 'cortex', 'consolidate', 'xray',
792
+ 'scan-skill', 'scan-skills', 'dashboard', 'api', 'worker', 'stats', 'cortex', 'consolidate', 'xray', 'xray-preinstall',
787
793
  ]);
788
794
  const arg = process.argv[2];
789
795
  if (arg && !arg.startsWith('-') && !knownCommands.has(arg)) {
@@ -14,6 +14,10 @@ export { scanFile } from './file-scanner.js';
14
14
  export { scanDirectory } from './dir-scanner.js';
15
15
  export { inspectNpmPackage } from './npm-inspector.js';
16
16
  export { formatXRayReport, formatXRayMarkdown } from './report.js';
17
+ export { watchDirectory } from './watch.js';
18
+ export { handlePreinstallCheck } from './preinstall.js';
19
+ export { xrayMemoryContent } from './memory-guard.js';
20
+ export type { MemoryGuardResult } from './memory-guard.js';
17
21
  /**
18
22
  * Handle the `shieldcortex xray` CLI command.
19
23
  */
@@ -16,12 +16,16 @@ import { scanDirectory } from './dir-scanner.js';
16
16
  import { inspectNpmPackage } from './npm-inspector.js';
17
17
  import { calculateTrustScore } from './trust-score.js';
18
18
  import { formatXRayReport, formatXRayMarkdown } from './report.js';
19
+ import { watchDirectory } from './watch.js';
19
20
  export { calculateTrustScore } from './trust-score.js';
20
21
  export { detectPatterns, detectFilenameDirectives } from './patterns.js';
21
22
  export { scanFile } from './file-scanner.js';
22
23
  export { scanDirectory } from './dir-scanner.js';
23
24
  export { inspectNpmPackage } from './npm-inspector.js';
24
25
  export { formatXRayReport, formatXRayMarkdown } from './report.js';
26
+ export { watchDirectory } from './watch.js';
27
+ export { handlePreinstallCheck } from './preinstall.js';
28
+ export { xrayMemoryContent } from './memory-guard.js';
25
29
  // ── Usage tracking ──────────────────────────────────────────
26
30
  const USAGE_FILE = path.join(os.homedir(), '.shieldcortex', 'xray-usage.json');
27
31
  const FREE_DAILY_LIMIT = 5;
@@ -79,22 +83,44 @@ export async function handleXRayCommand(args) {
79
83
  const deep = flags.has('--deep');
80
84
  const jsonOutput = flags.has('--json');
81
85
  const markdownOutput = flags.has('--markdown');
86
+ const ciMode = flags.has('--ci');
87
+ const watchMode = flags.has('--watch');
88
+ const ciThreshold = (() => {
89
+ const t = args.find(a => a.startsWith('--threshold='));
90
+ if (t)
91
+ return t.split('=')[1]?.toUpperCase() || 'HIGH';
92
+ return 'HIGH';
93
+ })();
82
94
  // Show usage if no target
83
95
  if (positional.length === 0) {
84
- console.error('Usage: shieldcortex xray <target> [--deep] [--json] [--markdown]');
96
+ console.error('Usage: shieldcortex xray <target> [--deep] [--json] [--markdown] [--watch]');
85
97
  console.error('');
86
98
  console.error(' target: npm package name, local file path, or directory path');
87
99
  console.error(' --deep Deep scan with full analysis (Pro)');
88
100
  console.error(' --json Output JSON result');
89
101
  console.error(' --markdown Output markdown report');
102
+ console.error(' --ci CI/CD mode: exit code 1 if risk >= threshold');
103
+ console.error(' --threshold=LEVEL Risk threshold for --ci (CRITICAL|HIGH|MEDIUM|LOW, default: HIGH)');
104
+ console.error(' --watch Watch directory for changes and scan incrementally');
90
105
  console.error('');
91
106
  console.error('Examples:');
92
107
  console.error(' shieldcortex xray ./src/');
93
108
  console.error(' shieldcortex xray package.json');
94
109
  console.error(' shieldcortex xray lodash --deep');
110
+ console.error(' shieldcortex xray ./src --watch');
95
111
  process.exit(1);
96
112
  }
97
113
  const target = positional[0];
114
+ // Watch mode — delegate to watchDirectory
115
+ if (watchMode) {
116
+ const resolved = path.resolve(target);
117
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
118
+ console.error(`--watch requires a directory target: ${resolved}`);
119
+ process.exit(1);
120
+ }
121
+ await watchDirectory(resolved, deep, { json: jsonOutput });
122
+ return;
123
+ }
98
124
  // Deep scan requires Pro
99
125
  if (deep) {
100
126
  try {
@@ -163,4 +189,14 @@ export async function handleXRayCommand(args) {
163
189
  else {
164
190
  console.log(formatXRayReport(result));
165
191
  }
192
+ // CI/CD gate: exit 1 if risk level meets or exceeds threshold
193
+ if (ciMode) {
194
+ const levels = { SAFE: 0, LOW: 1, MEDIUM: 2, HIGH: 3, CRITICAL: 4 };
195
+ const resultLevel = levels[result.riskLevel] ?? 0;
196
+ const thresholdLevel = levels[ciThreshold] ?? 3;
197
+ if (resultLevel >= thresholdLevel) {
198
+ process.exit(1);
199
+ }
200
+ process.exit(0);
201
+ }
166
202
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * X-Ray Memory Guard
3
+ *
4
+ * Synchronous, fast content scanner for memory pipeline integration.
5
+ * Checks content destined for memory storage against X-Ray pattern
6
+ * detection, returning an allow/deny decision with findings.
7
+ *
8
+ * No file I/O, no network — pure in-memory analysis.
9
+ */
10
+ import type { XRayFinding } from './types.js';
11
+ export interface MemoryGuardResult {
12
+ allowed: boolean;
13
+ findings: XRayFinding[];
14
+ riskLevel: string;
15
+ }
16
+ /**
17
+ * Scan content destined for memory storage.
18
+ *
19
+ * @param content - The memory content to scan
20
+ * @param title - Optional title/name for the memory entry
21
+ * @returns { allowed, findings, riskLevel }
22
+ * - allowed: true if trust score >= 40 (MEDIUM or better)
23
+ * - findings: all detected X-Ray findings
24
+ * - riskLevel: SAFE | LOW | MEDIUM | HIGH | CRITICAL
25
+ */
26
+ export declare function xrayMemoryContent(content: string, title?: string): MemoryGuardResult;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * X-Ray Memory Guard
3
+ *
4
+ * Synchronous, fast content scanner for memory pipeline integration.
5
+ * Checks content destined for memory storage against X-Ray pattern
6
+ * detection, returning an allow/deny decision with findings.
7
+ *
8
+ * No file I/O, no network — pure in-memory analysis.
9
+ */
10
+ import { detectPatterns, detectFilenameDirectives } from './patterns.js';
11
+ import { calculateTrustScore } from './trust-score.js';
12
+ /**
13
+ * Scan content destined for memory storage.
14
+ *
15
+ * @param content - The memory content to scan
16
+ * @param title - Optional title/name for the memory entry
17
+ * @returns { allowed, findings, riskLevel }
18
+ * - allowed: true if trust score >= 40 (MEDIUM or better)
19
+ * - findings: all detected X-Ray findings
20
+ * - riskLevel: SAFE | LOW | MEDIUM | HIGH | CRITICAL
21
+ */
22
+ export function xrayMemoryContent(content, title) {
23
+ const findings = [];
24
+ // Scan the content body
25
+ findings.push(...detectPatterns(content));
26
+ // Scan the title for filename-style directives
27
+ if (title) {
28
+ findings.push(...detectFilenameDirectives(title));
29
+ }
30
+ const { score, riskLevel } = calculateTrustScore(findings);
31
+ return {
32
+ allowed: score >= 40,
33
+ findings,
34
+ riskLevel,
35
+ };
36
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * X-Ray Pre-install Hook
3
+ *
4
+ * Lightweight pre-install check that scans package.json scripts
5
+ * for suspicious patterns. Designed to run as an npm lifecycle script:
6
+ * "preinstall": "shieldcortex xray-preinstall"
7
+ *
8
+ * Exit 1 blocks install (HIGH+ findings), exit 0 allows.
9
+ */
10
+ /**
11
+ * Run a lightweight pre-install check on package.json scripts.
12
+ */
13
+ export declare function handlePreinstallCheck(): Promise<void>;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * X-Ray Pre-install Hook
3
+ *
4
+ * Lightweight pre-install check that scans package.json scripts
5
+ * for suspicious patterns. Designed to run as an npm lifecycle script:
6
+ * "preinstall": "shieldcortex xray-preinstall"
7
+ *
8
+ * Exit 1 blocks install (HIGH+ findings), exit 0 allows.
9
+ */
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import { detectPatterns } from './patterns.js';
13
+ import { calculateTrustScore } from './trust-score.js';
14
+ // ── Constants ───────────────────────────────────────────────
15
+ const SEVERITY_RANK = {
16
+ critical: 4,
17
+ high: 3,
18
+ medium: 2,
19
+ low: 1,
20
+ info: 0,
21
+ };
22
+ // ── Public API ──────────────────────────────────────────────
23
+ /**
24
+ * Run a lightweight pre-install check on package.json scripts.
25
+ */
26
+ export async function handlePreinstallCheck() {
27
+ const pkgName = process.env.npm_package_name;
28
+ const pkgVersion = process.env.npm_package_version;
29
+ const cwd = process.cwd();
30
+ const pkgJsonPath = path.join(cwd, 'package.json');
31
+ if (!fs.existsSync(pkgJsonPath)) {
32
+ console.log('ShieldCortex X-Ray pre-install check: PASS (no package.json found)');
33
+ process.exit(0);
34
+ }
35
+ let pkgContent;
36
+ try {
37
+ pkgContent = fs.readFileSync(pkgJsonPath, 'utf-8');
38
+ }
39
+ catch {
40
+ console.log('ShieldCortex X-Ray pre-install check: PASS (could not read package.json)');
41
+ process.exit(0);
42
+ }
43
+ let pkg;
44
+ try {
45
+ pkg = JSON.parse(pkgContent);
46
+ }
47
+ catch {
48
+ console.log('ShieldCortex X-Ray pre-install check: PASS (invalid package.json)');
49
+ process.exit(0);
50
+ }
51
+ // Scan scripts section only (lightweight, fast)
52
+ const scripts = pkg.scripts;
53
+ const allFindings = [];
54
+ if (scripts && typeof scripts === 'object') {
55
+ const scriptsJson = JSON.stringify({ scripts });
56
+ const findings = detectPatterns(scriptsJson, 'package.json (scripts)');
57
+ allFindings.push(...findings);
58
+ }
59
+ // Also scan the full package.json for metadata injection
60
+ const metaFindings = detectPatterns(pkgContent, 'package.json');
61
+ for (const f of metaFindings) {
62
+ if (f.category === 'metadata-exploit' || f.category === 'ai-directive') {
63
+ allFindings.push(f);
64
+ }
65
+ }
66
+ const { score, riskLevel } = calculateTrustScore(allFindings);
67
+ const maxSeverity = allFindings.reduce((max, f) => Math.max(max, SEVERITY_RANK[f.severity] ?? 0), 0);
68
+ const label = pkgName
69
+ ? `${pkgName}@${pkgVersion || 'unknown'}`
70
+ : path.basename(cwd);
71
+ if (allFindings.length === 0) {
72
+ console.log(`ShieldCortex X-Ray pre-install check: PASS — ${label} (score: ${score})`);
73
+ process.exit(0);
74
+ }
75
+ // Print findings
76
+ console.log('');
77
+ console.log(`ShieldCortex X-Ray pre-install scan: ${label}`);
78
+ console.log(` Trust Score: ${score}/100 Risk: ${riskLevel}`);
79
+ console.log('');
80
+ for (const f of allFindings) {
81
+ console.log(` [${f.severity.toUpperCase()}] [${f.category}] ${f.title}`);
82
+ if (f.evidence) {
83
+ console.log(` Evidence: ${f.evidence}`);
84
+ }
85
+ }
86
+ console.log('');
87
+ // HIGH or CRITICAL → block
88
+ if (maxSeverity >= SEVERITY_RANK.high) {
89
+ console.log('ShieldCortex X-Ray pre-install check: FAIL — blocking install due to HIGH+ risk findings');
90
+ process.exit(1);
91
+ }
92
+ // MEDIUM or below → warn but allow
93
+ console.log('ShieldCortex X-Ray pre-install check: PASS (warnings found, but below blocking threshold)');
94
+ process.exit(0);
95
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * X-Ray Watch Mode
3
+ *
4
+ * Watches a directory for file changes and incrementally re-scans
5
+ * only the changed files, printing new findings as they appear.
6
+ */
7
+ /**
8
+ * Watch a directory for changes and incrementally scan changed files.
9
+ */
10
+ export declare function watchDirectory(dirPath: string, deep: boolean, options?: {
11
+ json?: boolean;
12
+ }): Promise<void>;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * X-Ray Watch Mode
3
+ *
4
+ * Watches a directory for file changes and incrementally re-scans
5
+ * only the changed files, printing new findings as they appear.
6
+ */
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import crypto from 'crypto';
10
+ import { scanFile } from './file-scanner.js';
11
+ import { calculateTrustScore } from './trust-score.js';
12
+ // ── Constants ───────────────────────────────────────────────
13
+ const DEBOUNCE_MS = 500;
14
+ const MAX_FILE_SIZE = 10 * 1024 * 1024;
15
+ const IGNORE_DIRS = new Set(['node_modules', '.git', 'dist', 'build']);
16
+ // ── Helpers ─────────────────────────────────────────────────
17
+ function shouldIgnore(filePath) {
18
+ const parts = filePath.split(path.sep);
19
+ return parts.some(p => IGNORE_DIRS.has(p));
20
+ }
21
+ function findingHash(f) {
22
+ const key = `${f.file || ''}|${f.category}|${f.title}`;
23
+ return crypto.createHash('sha256').update(key).digest('hex');
24
+ }
25
+ // ── ANSI colours ────────────────────────────────────────────
26
+ const c = {
27
+ reset: '\x1b[0m',
28
+ bold: '\x1b[1m',
29
+ dim: '\x1b[2m',
30
+ red: '\x1b[31m',
31
+ green: '\x1b[32m',
32
+ yellow: '\x1b[33m',
33
+ cyan: '\x1b[36m',
34
+ brightRed: '\x1b[91m',
35
+ };
36
+ function severityColour(severity) {
37
+ switch (severity) {
38
+ case 'critical': return c.red;
39
+ case 'high': return c.brightRed;
40
+ case 'medium': return c.yellow;
41
+ case 'low': return c.cyan;
42
+ default: return c.dim;
43
+ }
44
+ }
45
+ // ── Public API ──────────────────────────────────────────────
46
+ /**
47
+ * Watch a directory for changes and incrementally scan changed files.
48
+ */
49
+ export async function watchDirectory(dirPath, deep, options) {
50
+ const resolved = path.resolve(dirPath);
51
+ const seenHashes = new Set();
52
+ const json = options?.json ?? false;
53
+ // Debounce map: filePath → timeout
54
+ const pending = new Map();
55
+ console.log(`${c.cyan}${c.bold}Watching ${resolved} for changes... (Ctrl+C to stop)${c.reset}`);
56
+ async function handleChange(filePath) {
57
+ const abs = path.resolve(resolved, filePath);
58
+ if (shouldIgnore(abs))
59
+ return;
60
+ // Check exists & size
61
+ let stat;
62
+ try {
63
+ stat = fs.statSync(abs);
64
+ }
65
+ catch {
66
+ return;
67
+ }
68
+ if (!stat.isFile() || stat.size > MAX_FILE_SIZE)
69
+ return;
70
+ const findings = await scanFile(abs, deep);
71
+ const newFindings = [];
72
+ for (const f of findings) {
73
+ const h = findingHash(f);
74
+ if (!seenHashes.has(h)) {
75
+ seenHashes.add(h);
76
+ newFindings.push(f);
77
+ }
78
+ }
79
+ if (newFindings.length === 0)
80
+ return;
81
+ if (json) {
82
+ const { score, riskLevel } = calculateTrustScore(newFindings);
83
+ console.log(JSON.stringify({
84
+ file: abs,
85
+ trustScore: score,
86
+ riskLevel,
87
+ findings: newFindings,
88
+ detectedAt: new Date().toISOString(),
89
+ }));
90
+ }
91
+ else {
92
+ const { score, riskLevel } = calculateTrustScore(newFindings);
93
+ console.log('');
94
+ console.log(`${c.bold}[${new Date().toLocaleTimeString()}]${c.reset} ${c.cyan}${abs}${c.reset} (score: ${score}, risk: ${riskLevel})`);
95
+ for (const f of newFindings) {
96
+ const sc = severityColour(f.severity);
97
+ console.log(` ${sc}[${f.severity.toUpperCase()}]${c.reset} [${f.category}] ${f.title}`);
98
+ if (f.evidence) {
99
+ console.log(` ${c.dim}${f.evidence}${c.reset}`);
100
+ }
101
+ }
102
+ }
103
+ }
104
+ const watcher = fs.watch(resolved, { recursive: true }, (_event, filename) => {
105
+ if (!filename)
106
+ return;
107
+ const filePath = filename.toString();
108
+ // Debounce
109
+ const existing = pending.get(filePath);
110
+ if (existing)
111
+ clearTimeout(existing);
112
+ pending.set(filePath, setTimeout(() => {
113
+ pending.delete(filePath);
114
+ handleChange(filePath).catch(() => { });
115
+ }, DEBOUNCE_MS));
116
+ });
117
+ // Keep the process alive until Ctrl+C
118
+ return new Promise((resolve) => {
119
+ process.on('SIGINT', () => {
120
+ watcher.close();
121
+ console.log(`\n${c.dim}Watch stopped.${c.reset}`);
122
+ resolve();
123
+ });
124
+ });
125
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shieldcortex",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "Trustworthy memory and security for AI agents. Recall debugging, review queue, OpenClaw session capture, and memory poisoning defence for Claude Code, Codex, OpenClaw, LangChain, and MCP agents.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -141,6 +141,49 @@ function writeAuditEntry(entry) {
141
141
  // Best-effort — never block on audit failure
142
142
  }
143
143
  }
144
+ // --- X-Ray Inline Guard ---
145
+ // Lightweight inline version of xrayMemoryContent for the plugin build boundary.
146
+ // Detects AI directive injection patterns in memory content.
147
+ const XRAY_AI_DIRECTIVE_PATTERNS = [
148
+ /ignore\s+(all\s+)?(previous|prior|above)\s+(instructions?|prompts?|context)/i,
149
+ /disregard\s+(all\s+)?(previous|prior|above)\s+(instructions?|rules?)/i,
150
+ /override\s+(previous|prior|all)\s+(instructions?|rules?|constraints?)/i,
151
+ /you\s+are\s+now\s+(?:in\s+)?(?:developer|god|admin|root|unrestricted)\s+mode/i,
152
+ /enter\s+(?:developer|god|admin|DAN|jailbreak)\s+mode/i,
153
+ /(?:system|hidden|secret)\s*(?:prompt|instruction|directive)\s*:/i,
154
+ /\[SYSTEM\]\s*:/i,
155
+ /\[INST\]/i,
156
+ /<\|(?:system|user|assistant|im_start|im_end)\|>/i,
157
+ /(?:decode|execute|follow)\s+(?:the\s+)?hidden\s+(?:instructions?|payload|message)/i,
158
+ /(?:hidden|embedded|encoded)\s+(?:instructions?|directive|command)\s+(?:in|within|inside)/i,
159
+ ];
160
+ const XRAY_FILENAME_PATTERNS = [
161
+ /ignore_previous/i, /decode_hidden/i, /execute_instructions/i,
162
+ /override_previous/i, /developer_mode/i, /system_prompt/i,
163
+ /jailbreak/i, /\[SYSTEM\]/i, /\[INST\]/i,
164
+ ];
165
+ function xrayMemoryGuard(content, title) {
166
+ const findings = [];
167
+ const text = content.length > 50000 ? content.slice(0, 50000) : content;
168
+ for (const pattern of XRAY_AI_DIRECTIVE_PATTERNS) {
169
+ if (pattern.test(text)) {
170
+ findings.push({ category: 'ai-directive', title: 'AI directive injection detected', severity: 'critical' });
171
+ break;
172
+ }
173
+ }
174
+ if (title) {
175
+ for (const pattern of XRAY_FILENAME_PATTERNS) {
176
+ if (pattern.test(title)) {
177
+ findings.push({ category: 'ai-directive', title: 'AI directive in title', severity: 'critical' });
178
+ break;
179
+ }
180
+ }
181
+ }
182
+ // Score: 100 - 25 per critical finding
183
+ const score = Math.max(0, 100 - findings.length * 25);
184
+ const riskLevel = score >= 80 ? 'SAFE' : score >= 60 ? 'LOW' : score >= 40 ? 'MEDIUM' : score >= 20 ? 'HIGH' : 'CRITICAL';
185
+ return { allowed: score >= 40, findings, riskLevel };
186
+ }
144
187
  export function createInterceptor(config, pipeline, options) {
145
188
  const denyCache = new DenyCache();
146
189
  const rateLimiter = new RateLimiter(options?.maxPromptsPerMinute ?? 5);
@@ -157,6 +200,18 @@ export function createInterceptor(config, pipeline, options) {
157
200
  const fullContent = [title, content].filter(Boolean).join(' ');
158
201
  if (!fullContent.trim())
159
202
  return;
203
+ // X-Ray content scan — fast, synchronous, no I/O
204
+ const xrayResult = xrayMemoryGuard(content, title || undefined);
205
+ if (!xrayResult.allowed) {
206
+ const xrayEntry = {
207
+ type: 'intercept', tool: context.toolName, severity: 'critical',
208
+ firewallResult: 'BLOCK', threats: xrayResult.findings.map(f => f.category),
209
+ anomalyScore: 1, action: 'auto_deny', outcome: 'auto_denied',
210
+ preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
211
+ };
212
+ emitAudit(xrayEntry);
213
+ throw new Error(`ShieldCortex: tool call blocked by X-Ray memory guard (risk: ${xrayResult.riskLevel}, findings: ${xrayResult.findings.length})`);
214
+ }
160
215
  let severity;
161
216
  let firewallResult;
162
217
  let threats;