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 +7 -1
- package/dist/xray/index.d.ts +4 -0
- package/dist/xray/index.js +37 -1
- package/dist/xray/memory-guard.d.ts +26 -0
- package/dist/xray/memory-guard.js +36 -0
- package/dist/xray/preinstall.d.ts +13 -0
- package/dist/xray/preinstall.js +95 -0
- package/dist/xray/watch.d.ts +12 -0
- package/dist/xray/watch.js +125 -0
- package/package.json +1 -1
- package/plugins/openclaw/dist/interceptor.js +55 -0
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)) {
|
package/dist/xray/index.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/xray/index.js
CHANGED
|
@@ -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
|
+
"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;
|