shieldcortex 4.2.4 → 4.3.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.
@@ -36,6 +36,22 @@ function formatBytes(bytes) {
36
36
  function getShieldCortexDir() {
37
37
  return path.join(os.homedir(), '.shieldcortex');
38
38
  }
39
+ function detectEnvironment() {
40
+ const home = os.homedir();
41
+ const hasClaude = fs.existsSync(path.join(home, '.claude')) || fs.existsSync(path.join(home, '.claude.json'));
42
+ const hasOpenClaw = fs.existsSync(path.join(home, '.openclaw'));
43
+ const hasCodex = fs.existsSync(path.join(home, '.codex'));
44
+ const platform = process.platform;
45
+ const vscodeDirs = platform === 'darwin'
46
+ ? [path.join(home, 'Library', 'Application Support', 'Code', 'User')]
47
+ : platform === 'win32'
48
+ ? [path.join(process.env.APPDATA ?? path.join(home, 'AppData', 'Roaming'), 'Code', 'User')]
49
+ : [path.join(home, '.config', 'Code', 'User')];
50
+ const hasVSCode = vscodeDirs.some(d => fs.existsSync(d));
51
+ // Headless = no display environment (SSH server, container, etc.)
52
+ const isHeadless = !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY && platform !== 'darwin' && platform !== 'win32';
53
+ return { hasClaude, hasOpenClaw, hasVSCode, hasCodex, isHeadless };
54
+ }
39
55
  function getDbPath() {
40
56
  const newPath = path.join(getShieldCortexDir(), 'memories.db');
41
57
  const legacyPath = path.join(os.homedir(), '.claude-memory', 'memories.db');
@@ -49,11 +65,15 @@ function getDbPath() {
49
65
  async function checkDatabase() {
50
66
  const dbPath = getDbPath();
51
67
  if (!fs.existsSync(dbPath)) {
68
+ const env = detectEnvironment();
69
+ const fix = env.hasOpenClaw && !env.hasClaude
70
+ ? 'Run `shieldcortex scan "test"` to initialise the database (OpenClaw-only setup detected)'
71
+ : 'Start the MCP server or run `shieldcortex quickstart` to initialise the database';
52
72
  return {
53
73
  label: 'Database',
54
74
  status: 'fail',
55
75
  message: 'not found',
56
- fix: 'Start the MCP server or run `shieldcortex setup` to initialise the database',
76
+ fix,
57
77
  };
58
78
  }
59
79
  try {
@@ -225,6 +245,9 @@ async function checkHooks() {
225
245
  // ── Check 5: Process check ────────────────────────────────
226
246
  async function checkProcesses() {
227
247
  const results = [];
248
+ const env = detectEnvironment();
249
+ // On headless/OpenClaw-only setups, API/Dashboard are optional
250
+ const isOptional = env.isHeadless || (env.hasOpenClaw && !env.hasClaude && !env.hasVSCode);
228
251
  // Check API server on port 3001
229
252
  try {
230
253
  const controller = new AbortController();
@@ -244,12 +267,21 @@ async function checkProcesses() {
244
267
  }
245
268
  }
246
269
  catch {
247
- results.push({
248
- label: 'API server',
249
- status: 'warn',
250
- message: 'not running',
251
- fix: 'Run `shieldcortex dashboard` to start the API server',
252
- });
270
+ if (isOptional) {
271
+ results.push({
272
+ label: 'API server',
273
+ status: 'info',
274
+ message: 'not running (optional on headless/OpenClaw-only setups)',
275
+ });
276
+ }
277
+ else {
278
+ results.push({
279
+ label: 'API server',
280
+ status: 'warn',
281
+ message: 'not running',
282
+ fix: 'Run `shieldcortex dashboard` to start the API server',
283
+ });
284
+ }
253
285
  }
254
286
  // Check dashboard on port 3030
255
287
  try {
@@ -270,12 +302,21 @@ async function checkProcesses() {
270
302
  }
271
303
  }
272
304
  catch {
273
- results.push({
274
- label: 'Dashboard',
275
- status: 'warn',
276
- message: 'not running',
277
- fix: 'Run `shieldcortex dashboard` to start the dashboard',
278
- });
305
+ if (isOptional) {
306
+ results.push({
307
+ label: 'Dashboard',
308
+ status: 'info',
309
+ message: 'not running (optional on headless/OpenClaw-only setups)',
310
+ });
311
+ }
312
+ else {
313
+ results.push({
314
+ label: 'Dashboard',
315
+ status: 'warn',
316
+ message: 'not running',
317
+ fix: 'Run `shieldcortex dashboard` to start the dashboard',
318
+ });
319
+ }
279
320
  }
280
321
  return results;
281
322
  }
package/dist/index.d.ts CHANGED
@@ -43,6 +43,9 @@
43
43
  * shieldcortex codex install # Configure MCP server for Codex CLI + VS Code extension
44
44
  * shieldcortex codex uninstall # Remove Codex MCP configuration
45
45
  * shieldcortex codex status # Check Codex MCP configuration
46
+ * shieldcortex xray <target> # Inspect package/file/plugin for hidden risk
47
+ * shieldcortex xray <path> --deep # Deep scan (Pro) with full analysis
48
+ * shieldcortex xray <package> --json # JSON output
46
49
  * shieldcortex cortex capture # Log a mistake with rule extraction
47
50
  * shieldcortex cortex preflight # Pre-flight check before a task
48
51
  * shieldcortex cortex review # Weekly pattern review
package/dist/index.js CHANGED
@@ -43,6 +43,9 @@
43
43
  * shieldcortex codex install # Configure MCP server for Codex CLI + VS Code extension
44
44
  * shieldcortex codex uninstall # Remove Codex MCP configuration
45
45
  * shieldcortex codex status # Check Codex MCP configuration
46
+ * shieldcortex xray <target> # Inspect package/file/plugin for hidden risk
47
+ * shieldcortex xray <path> --deep # Deep scan (Pro) with full analysis
48
+ * shieldcortex xray <package> --json # JSON output
46
49
  * shieldcortex cortex capture # Log a mistake with rule extraction
47
50
  * shieldcortex cortex preflight # Pre-flight check before a task
48
51
  * shieldcortex cortex review # Weekly pattern review
@@ -763,6 +766,12 @@ ${bold}DOCS${reset}
763
766
  console.log(` Total processed: ${result.totalProcessed}`);
764
767
  process.exit(0);
765
768
  }
769
+ // Handle "xray" subcommand — package/file/plugin risk inspector
770
+ if (process.argv[2] === 'xray') {
771
+ const { handleXRayCommand } = await import('./xray/index.js');
772
+ await handleXRayCommand(process.argv.slice(3));
773
+ process.exit(0);
774
+ }
766
775
  // Handle "cortex" subcommand (Pro tier — mistake learning)
767
776
  if (process.argv[2] === 'cortex') {
768
777
  const { handleCortexCommand } = await import('./cortex/cli.js');
@@ -774,7 +783,7 @@ ${bold}DOCS${reset}
774
783
  'doctor', 'quickstart', 'setup', 'install', 'migrate', 'uninstall', 'hook',
775
784
  'openclaw', 'clawdbot', 'copilot', 'codex', 'service', 'config', 'status',
776
785
  'graph', 'license', 'licence', 'audit', 'iron-dome', 'scan', 'cloud',
777
- 'scan-skill', 'scan-skills', 'dashboard', 'api', 'worker', 'stats', 'cortex', 'consolidate',
786
+ 'scan-skill', 'scan-skills', 'dashboard', 'api', 'worker', 'stats', 'cortex', 'consolidate', 'xray',
778
787
  ]);
779
788
  const arg = process.argv[2];
780
789
  if (arg && !arg.startsWith('-') && !knownCommands.has(arg)) {
@@ -6,7 +6,7 @@
6
6
  * isFeatureEnabled('cloud_sync'); // returns boolean (soft check)
7
7
  */
8
8
  import { type LicenseTier } from './keys.js';
9
- export type GatedFeature = 'custom_injection_patterns' | 'custom_iron_dome_policies' | 'custom_firewall_rules' | 'audit_export' | 'skill_scanner_deep' | 'cloud_sync' | 'team_management' | 'shared_patterns' | 'cortex_learning' | 'deps_quarantine' | 'deps_clean' | 'deps_auto_protect' | 'deps_global_scan' | 'memory_types' | 'memory_scopes' | 'dream_mode' | 'llm_reranking' | 'positive_feedback';
9
+ export type GatedFeature = 'custom_injection_patterns' | 'custom_iron_dome_policies' | 'custom_firewall_rules' | 'audit_export' | 'skill_scanner_deep' | 'cloud_sync' | 'team_management' | 'shared_patterns' | 'cortex_learning' | 'deps_quarantine' | 'deps_clean' | 'deps_auto_protect' | 'deps_global_scan' | 'memory_types' | 'memory_scopes' | 'dream_mode' | 'llm_reranking' | 'positive_feedback' | 'xray_deep';
10
10
  export declare class FeatureGatedError extends Error {
11
11
  feature: GatedFeature;
12
12
  requiredTier: LicenseTier;
@@ -26,6 +26,7 @@ const FEATURE_TIERS = {
26
26
  dream_mode: 'pro',
27
27
  llm_reranking: 'pro',
28
28
  positive_feedback: 'pro',
29
+ xray_deep: 'pro',
29
30
  };
30
31
  const FEATURE_DESCRIPTIONS = {
31
32
  custom_injection_patterns: 'Define up to 50 custom regex patterns for detecting domain-specific threats.',
@@ -46,6 +47,7 @@ const FEATURE_DESCRIPTIONS = {
46
47
  dream_mode: 'Background memory consolidation — merge duplicates, archive stale, detect contradictions.',
47
48
  llm_reranking: 'LLM-powered memory reranking for precision recall.',
48
49
  positive_feedback: 'Capture what worked, not just what failed — learn from success.',
50
+ xray_deep: 'Deep X-Ray scanning: npm registry analysis, binary file inspection, dependency graph risk, and AI-directive detection in metadata and filenames.',
49
51
  };
50
52
  // ── Error class ──────────────────────────────────────────
51
53
  export class FeatureGatedError extends Error {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * X-Ray Directory Scanner
3
+ *
4
+ * Walks a directory tree, scans each file, checks package.json if present,
5
+ * and aggregates findings into a single XRayResult.
6
+ */
7
+ import type { XRayResult } from './types.js';
8
+ /**
9
+ * Scan an entire directory for X-Ray findings.
10
+ *
11
+ * @param dirPath - Path to the directory to scan
12
+ * @param deep - If true, performs deeper analysis (Pro feature)
13
+ */
14
+ export declare function scanDirectory(dirPath: string, deep: boolean): Promise<XRayResult>;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * X-Ray Directory Scanner
3
+ *
4
+ * Walks a directory tree, scans each file, checks package.json if present,
5
+ * and aggregates findings into a single XRayResult.
6
+ */
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { scanFile } from './file-scanner.js';
10
+ import { calculateTrustScore } from './trust-score.js';
11
+ // ── Constants ───────────────────────────────────────────────
12
+ /** Directories to skip when walking. */
13
+ const SKIP_DIRS = new Set([
14
+ 'node_modules', '.git', '.svn', '.hg', 'dist', 'build', '.next',
15
+ '__pycache__', '.tox', '.venv', 'venv', '.cache', 'coverage',
16
+ ]);
17
+ /** Maximum number of files to scan per directory. */
18
+ const MAX_FILES = 5000;
19
+ // ── Directory walker ────────────────────────────────────────
20
+ function walkDir(dirPath, files, depth = 0) {
21
+ if (depth > 20 || files.length >= MAX_FILES)
22
+ return;
23
+ let entries;
24
+ try {
25
+ entries = fs.readdirSync(dirPath, { withFileTypes: true });
26
+ }
27
+ catch {
28
+ return;
29
+ }
30
+ for (const entry of entries) {
31
+ if (files.length >= MAX_FILES)
32
+ break;
33
+ if (entry.isDirectory()) {
34
+ if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
35
+ walkDir(path.join(dirPath, entry.name), files, depth + 1);
36
+ }
37
+ }
38
+ else if (entry.isFile()) {
39
+ files.push(path.join(dirPath, entry.name));
40
+ }
41
+ }
42
+ }
43
+ // ── Public API ──────────────────────────────────────────────
44
+ /**
45
+ * Scan an entire directory for X-Ray findings.
46
+ *
47
+ * @param dirPath - Path to the directory to scan
48
+ * @param deep - If true, performs deeper analysis (Pro feature)
49
+ */
50
+ export async function scanDirectory(dirPath, deep) {
51
+ const startTime = Date.now();
52
+ const allFindings = [];
53
+ // Collect files
54
+ const files = [];
55
+ walkDir(dirPath, files);
56
+ // Scan each file
57
+ for (const file of files) {
58
+ const findings = await scanFile(file, deep);
59
+ allFindings.push(...findings);
60
+ }
61
+ // Check for package.json at root
62
+ const pkgJsonPath = path.join(dirPath, 'package.json');
63
+ if (fs.existsSync(pkgJsonPath) && !files.includes(pkgJsonPath)) {
64
+ const findings = await scanFile(pkgJsonPath, deep);
65
+ allFindings.push(...findings);
66
+ }
67
+ const { score, riskLevel } = calculateTrustScore(allFindings);
68
+ return {
69
+ target: dirPath,
70
+ trustScore: score,
71
+ riskLevel,
72
+ findings: allFindings,
73
+ filesScanned: files.length,
74
+ scannedAt: new Date(),
75
+ deepScan: deep,
76
+ };
77
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * X-Ray File Scanner
3
+ *
4
+ * Scans individual files for hidden risk patterns.
5
+ * Handles code files (.js/.ts), JSON, and binary/image files.
6
+ * Uses only Node.js built-ins — no new dependencies.
7
+ */
8
+ import type { XRayFinding } from './types.js';
9
+ /**
10
+ * Scan a single file for X-Ray findings.
11
+ *
12
+ * @param filePath - Absolute or relative path to the file
13
+ * @param deep - If true, performs deeper analysis (Pro feature)
14
+ */
15
+ export declare function scanFile(filePath: string, deep: boolean): Promise<XRayFinding[]>;
@@ -0,0 +1,296 @@
1
+ /**
2
+ * X-Ray File Scanner
3
+ *
4
+ * Scans individual files for hidden risk patterns.
5
+ * Handles code files (.js/.ts), JSON, and binary/image files.
6
+ * Uses only Node.js built-ins — no new dependencies.
7
+ */
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { detectPatterns, detectFilenameDirectives } from './patterns.js';
11
+ // ── Constants ───────────────────────────────────────────────
12
+ /** Extensions treated as code files. */
13
+ const CODE_EXTENSIONS = new Set(['.js', '.ts', '.mjs', '.cjs', '.mts', '.cts', '.jsx', '.tsx']);
14
+ /** Extensions treated as JSON. */
15
+ const JSON_EXTENSIONS = new Set(['.json']);
16
+ /** Extensions treated as images. */
17
+ const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']);
18
+ /** Maximum file size to scan (10 MB). */
19
+ const MAX_FILE_SIZE = 10 * 1024 * 1024;
20
+ // ── Image EOI markers ───────────────────────────────────────
21
+ /** JPEG End-of-Image marker. */
22
+ const JPEG_EOI = Buffer.from([0xff, 0xd9]);
23
+ /** PNG IEND chunk signature. */
24
+ const PNG_IEND = Buffer.from([0x49, 0x45, 0x4e, 0x44]);
25
+ /** GIF trailer byte. */
26
+ const GIF_TRAILER = 0x3b;
27
+ // ── Helpers ─────────────────────────────────────────────────
28
+ function hasZeroWidthUnicode(content) {
29
+ const zwMatch = content.match(/[\u200b\u200c\u200d\ufeff\u2060]{2,}/);
30
+ if (!zwMatch)
31
+ return null;
32
+ const before = content.slice(0, zwMatch.index);
33
+ const line = (before.match(/\n/g) || []).length + 1;
34
+ return {
35
+ severity: 'medium',
36
+ category: 'unicode-trick',
37
+ title: 'Zero-width Unicode characters detected',
38
+ description: 'File contains clusters of zero-width characters that may hide invisible content.',
39
+ line,
40
+ evidence: `${zwMatch[0].length} zero-width chars at line ${line}`,
41
+ };
42
+ }
43
+ function checkPolyglot(buf) {
44
+ // Check for multiple magic byte signatures in a single file
45
+ const signatures = [
46
+ { name: 'PDF', magic: Buffer.from('%PDF') },
47
+ { name: 'ZIP/JAR', magic: Buffer.from([0x50, 0x4b, 0x03, 0x04]) },
48
+ { name: 'ELF', magic: Buffer.from([0x7f, 0x45, 0x4c, 0x46]) },
49
+ { name: 'PE/EXE', magic: Buffer.from([0x4d, 0x5a]) },
50
+ { name: 'GZIP', magic: Buffer.from([0x1f, 0x8b]) },
51
+ ];
52
+ const matched = [];
53
+ for (const sig of signatures) {
54
+ // Check at start and also embedded
55
+ if (buf.includes(sig.magic)) {
56
+ matched.push(sig.name);
57
+ }
58
+ }
59
+ if (matched.length >= 2) {
60
+ return {
61
+ severity: 'high',
62
+ category: 'steganography',
63
+ title: 'Polyglot file detected',
64
+ description: `File contains multiple format signatures (${matched.join(', ')}), indicating a polyglot that may hide payloads.`,
65
+ evidence: matched.join(', '),
66
+ };
67
+ }
68
+ return null;
69
+ }
70
+ // ── Image scanner ───────────────────────────────────────────
71
+ function scanImage(filePath, buf) {
72
+ const findings = [];
73
+ const ext = path.extname(filePath).toLowerCase();
74
+ // Check for appended data after EOI marker
75
+ if (ext === '.jpg' || ext === '.jpeg') {
76
+ const eoiIndex = buf.lastIndexOf(JPEG_EOI);
77
+ if (eoiIndex >= 0 && eoiIndex < buf.length - 2) {
78
+ const trailing = buf.length - eoiIndex - 2;
79
+ if (trailing > 16) {
80
+ findings.push({
81
+ severity: 'high',
82
+ category: 'steganography',
83
+ title: 'Data appended after JPEG EOI marker',
84
+ description: `${trailing} bytes found after the JPEG End-of-Image marker — may contain hidden payloads.`,
85
+ file: filePath,
86
+ evidence: `${trailing} trailing bytes after EOI at offset ${eoiIndex}`,
87
+ });
88
+ }
89
+ }
90
+ }
91
+ if (ext === '.png') {
92
+ const iendIndex = buf.lastIndexOf(PNG_IEND);
93
+ if (iendIndex >= 0) {
94
+ // IEND chunk: 4-byte length + 4-byte type + 4-byte CRC = the IEND text is at +4 from chunk start
95
+ // After IEND chunk: iendIndex + 4 (IEND) + 4 (CRC) = iendIndex + 8
96
+ const afterIend = iendIndex + 8;
97
+ if (afterIend < buf.length) {
98
+ const trailing = buf.length - afterIend;
99
+ if (trailing > 16) {
100
+ findings.push({
101
+ severity: 'high',
102
+ category: 'steganography',
103
+ title: 'Data appended after PNG IEND chunk',
104
+ description: `${trailing} bytes found after the PNG IEND marker — may contain hidden payloads.`,
105
+ file: filePath,
106
+ evidence: `${trailing} trailing bytes after IEND at offset ${iendIndex}`,
107
+ });
108
+ }
109
+ }
110
+ }
111
+ }
112
+ if (ext === '.gif') {
113
+ const lastByte = buf[buf.length - 1];
114
+ if (lastByte !== GIF_TRAILER) {
115
+ // Find the trailer
116
+ const trailerIndex = buf.lastIndexOf(GIF_TRAILER);
117
+ if (trailerIndex >= 0 && trailerIndex < buf.length - 1) {
118
+ const trailing = buf.length - trailerIndex - 1;
119
+ if (trailing > 16) {
120
+ findings.push({
121
+ severity: 'high',
122
+ category: 'steganography',
123
+ title: 'Data appended after GIF trailer',
124
+ description: `${trailing} bytes found after the GIF trailer byte — may contain hidden payloads.`,
125
+ file: filePath,
126
+ evidence: `${trailing} trailing bytes after GIF trailer`,
127
+ });
128
+ }
129
+ }
130
+ }
131
+ }
132
+ // Check EXIF/metadata chunks for text with AI directives
133
+ // Look for readable text in the binary that matches AI directive patterns
134
+ const textChunks = extractTextFromBinary(buf);
135
+ if (textChunks) {
136
+ const metaFindings = detectPatterns(textChunks, filePath);
137
+ for (const f of metaFindings) {
138
+ if (f.category === 'ai-directive' || f.category === 'metadata-exploit') {
139
+ f.description = `Image metadata contains: ${f.description}`;
140
+ findings.push(f);
141
+ }
142
+ }
143
+ }
144
+ return findings;
145
+ }
146
+ /**
147
+ * Extract readable ASCII/UTF-8 text sequences from a binary buffer.
148
+ * Returns concatenated text chunks >= 8 chars for pattern scanning.
149
+ */
150
+ function extractTextFromBinary(buf) {
151
+ const chunks = [];
152
+ let current = '';
153
+ for (let i = 0; i < buf.length && i < 1024 * 1024; i++) {
154
+ const byte = buf[i];
155
+ // Printable ASCII range
156
+ if (byte >= 0x20 && byte <= 0x7e) {
157
+ current += String.fromCharCode(byte);
158
+ }
159
+ else {
160
+ if (current.length >= 8) {
161
+ chunks.push(current);
162
+ }
163
+ current = '';
164
+ }
165
+ }
166
+ if (current.length >= 8) {
167
+ chunks.push(current);
168
+ }
169
+ return chunks.length > 0 ? chunks.join('\n') : null;
170
+ }
171
+ // ── JSON scanner ────────────────────────────────────────────
172
+ function scanJson(filePath, content) {
173
+ const findings = [];
174
+ // Run pattern detection for metadata injection, postinstall hooks, etc.
175
+ findings.push(...detectPatterns(content, filePath));
176
+ // Check for suspicious scripts in package.json
177
+ const basename = path.basename(filePath);
178
+ if (basename === 'package.json') {
179
+ try {
180
+ const pkg = JSON.parse(content);
181
+ const scripts = pkg.scripts || {};
182
+ for (const hook of ['preinstall', 'install', 'postinstall', 'preuninstall', 'postuninstall']) {
183
+ if (scripts[hook]) {
184
+ const val = scripts[hook];
185
+ if (/curl|wget|node\s+-e|bash\s+-c|powershell|https?:\/\/|eval|exec/i.test(val)) {
186
+ findings.push({
187
+ severity: 'high',
188
+ category: 'persistence-hook',
189
+ title: `Suspicious ${hook} script`,
190
+ description: `package.json "${hook}" script executes potentially dangerous commands.`,
191
+ file: filePath,
192
+ evidence: val.slice(0, 120),
193
+ });
194
+ }
195
+ }
196
+ }
197
+ }
198
+ catch {
199
+ // Not valid JSON — just rely on pattern detection
200
+ }
201
+ }
202
+ return findings;
203
+ }
204
+ // ── Public API ──────────────────────────────────────────────
205
+ /**
206
+ * Scan a single file for X-Ray findings.
207
+ *
208
+ * @param filePath - Absolute or relative path to the file
209
+ * @param deep - If true, performs deeper analysis (Pro feature)
210
+ */
211
+ export async function scanFile(filePath, deep) {
212
+ const findings = [];
213
+ const ext = path.extname(filePath).toLowerCase();
214
+ const basename = path.basename(filePath);
215
+ // Check filename for AI directives
216
+ findings.push(...detectFilenameDirectives(basename));
217
+ // Check file exists and size
218
+ let stat;
219
+ try {
220
+ stat = fs.statSync(filePath);
221
+ }
222
+ catch {
223
+ return findings;
224
+ }
225
+ if (!stat.isFile() || stat.size > MAX_FILE_SIZE) {
226
+ return findings;
227
+ }
228
+ // Route by extension
229
+ if (IMAGE_EXTENSIONS.has(ext)) {
230
+ const buf = fs.readFileSync(filePath);
231
+ findings.push(...scanImage(filePath, buf));
232
+ // Polyglot check
233
+ const polyglot = checkPolyglot(buf);
234
+ if (polyglot) {
235
+ polyglot.file = filePath;
236
+ findings.push(polyglot);
237
+ }
238
+ }
239
+ else if (JSON_EXTENSIONS.has(ext)) {
240
+ const content = fs.readFileSync(filePath, 'utf-8');
241
+ findings.push(...scanJson(filePath, content));
242
+ // Zero-width unicode check
243
+ const zw = hasZeroWidthUnicode(content);
244
+ if (zw) {
245
+ zw.file = filePath;
246
+ findings.push(zw);
247
+ }
248
+ }
249
+ else if (CODE_EXTENSIONS.has(ext)) {
250
+ const content = fs.readFileSync(filePath, 'utf-8');
251
+ findings.push(...detectPatterns(content, filePath));
252
+ // Zero-width unicode check
253
+ const zw = hasZeroWidthUnicode(content);
254
+ if (zw) {
255
+ zw.file = filePath;
256
+ findings.push(zw);
257
+ }
258
+ }
259
+ else {
260
+ // For any other text-like file, try reading as text
261
+ try {
262
+ const buf = fs.readFileSync(filePath);
263
+ // Check if it's mostly text
264
+ let nonPrintable = 0;
265
+ const sampleSize = Math.min(buf.length, 8192);
266
+ for (let i = 0; i < sampleSize; i++) {
267
+ const b = buf[i];
268
+ if (b < 0x09 || (b > 0x0d && b < 0x20 && b !== 0x1b)) {
269
+ nonPrintable++;
270
+ }
271
+ }
272
+ if (nonPrintable / sampleSize < 0.1) {
273
+ // Treat as text
274
+ const content = buf.toString('utf-8');
275
+ findings.push(...detectPatterns(content, filePath));
276
+ const zw = hasZeroWidthUnicode(content);
277
+ if (zw) {
278
+ zw.file = filePath;
279
+ findings.push(zw);
280
+ }
281
+ }
282
+ else {
283
+ // Binary file — polyglot check
284
+ const polyglot = checkPolyglot(buf);
285
+ if (polyglot) {
286
+ polyglot.file = filePath;
287
+ findings.push(polyglot);
288
+ }
289
+ }
290
+ }
291
+ catch {
292
+ // Can't read — skip
293
+ }
294
+ }
295
+ return findings;
296
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * X-Ray — Package, File & Plugin Risk Inspector
3
+ *
4
+ * Main entry point for the `shieldcortex xray` CLI command.
5
+ * Inspects npm packages, local files, and directories for hidden risk.
6
+ *
7
+ * Free tier: local scans only (no npm registry), max 5 scans/day.
8
+ * Pro tier: npm registry analysis, deep scanning, unlimited scans.
9
+ */
10
+ export type { XRayTarget, XRayFinding, XRayCategory, XRayResult } from './types.js';
11
+ export { calculateTrustScore } from './trust-score.js';
12
+ export { detectPatterns, detectFilenameDirectives } from './patterns.js';
13
+ export { scanFile } from './file-scanner.js';
14
+ export { scanDirectory } from './dir-scanner.js';
15
+ export { inspectNpmPackage } from './npm-inspector.js';
16
+ export { formatXRayReport, formatXRayMarkdown } from './report.js';
17
+ /**
18
+ * Handle the `shieldcortex xray` CLI command.
19
+ */
20
+ export declare function handleXRayCommand(args: string[]): Promise<void>;