@omnidev-ai/core 0.11.0 → 0.12.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.d.ts +190 -7
- package/dist/index.js +773 -91
- package/package.json +1 -1
- package/src/capability/loader.ts +32 -4
- package/src/capability/sources.ts +303 -56
- package/src/index.ts +3 -0
- package/src/mcp-json/manager.ts +0 -7
- package/src/security/index.ts +11 -0
- package/src/security/scanner.ts +563 -0
- package/src/security/types.ts +108 -0
- package/src/state/index.ts +1 -0
- package/src/state/security-allows.ts +178 -0
- package/src/sync.ts +65 -45
- package/src/types/index.ts +52 -1
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security scanner implementation
|
|
3
|
+
*
|
|
4
|
+
* Scans capability directories for potential supply-chain issues:
|
|
5
|
+
* - Suspicious Unicode characters (bidi overrides, zero-width, control chars)
|
|
6
|
+
* - Symlinks that could escape the capability directory
|
|
7
|
+
* - Suspicious script patterns
|
|
8
|
+
* - Binary files in content directories
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { lstat, readdir, readFile, readlink, realpath } from "node:fs/promises";
|
|
13
|
+
import { join, relative, resolve } from "node:path";
|
|
14
|
+
import type {
|
|
15
|
+
FindingSeverity,
|
|
16
|
+
FindingType,
|
|
17
|
+
ScanResult,
|
|
18
|
+
ScanSettings,
|
|
19
|
+
ScanSummary,
|
|
20
|
+
SecurityConfig,
|
|
21
|
+
SecurityFinding,
|
|
22
|
+
} from "./types.js";
|
|
23
|
+
import { DEFAULT_SCAN_SETTINGS } from "./types.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Suspicious Unicode codepoint ranges
|
|
27
|
+
*/
|
|
28
|
+
const UNICODE_PATTERNS = {
|
|
29
|
+
// Bidirectional text override characters (can hide malicious code)
|
|
30
|
+
bidi: [
|
|
31
|
+
0x202a, // LEFT-TO-RIGHT EMBEDDING
|
|
32
|
+
0x202b, // RIGHT-TO-LEFT EMBEDDING
|
|
33
|
+
0x202c, // POP DIRECTIONAL FORMATTING
|
|
34
|
+
0x202d, // LEFT-TO-RIGHT OVERRIDE
|
|
35
|
+
0x202e, // RIGHT-TO-LEFT OVERRIDE
|
|
36
|
+
0x2066, // LEFT-TO-RIGHT ISOLATE
|
|
37
|
+
0x2067, // RIGHT-TO-LEFT ISOLATE
|
|
38
|
+
0x2068, // FIRST STRONG ISOLATE
|
|
39
|
+
0x2069, // POP DIRECTIONAL ISOLATE
|
|
40
|
+
],
|
|
41
|
+
// Zero-width characters (can hide content)
|
|
42
|
+
zeroWidth: [
|
|
43
|
+
0x200b, // ZERO WIDTH SPACE
|
|
44
|
+
0x200c, // ZERO WIDTH NON-JOINER
|
|
45
|
+
0x200d, // ZERO WIDTH JOINER
|
|
46
|
+
0x2060, // WORD JOINER
|
|
47
|
+
0xfeff, // ZERO WIDTH NO-BREAK SPACE (BOM when not at start)
|
|
48
|
+
],
|
|
49
|
+
// Control characters (excluding common ones like \n, \r, \t)
|
|
50
|
+
control: [
|
|
51
|
+
0x0000, // NUL
|
|
52
|
+
0x0001, // SOH
|
|
53
|
+
0x0002, // STX
|
|
54
|
+
0x0003, // ETX
|
|
55
|
+
0x0004, // EOT
|
|
56
|
+
0x0005, // ENQ
|
|
57
|
+
0x0006, // ACK
|
|
58
|
+
0x0007, // BEL
|
|
59
|
+
0x0008, // BS
|
|
60
|
+
// 0x0009 TAB - allowed
|
|
61
|
+
// 0x000A LF - allowed
|
|
62
|
+
0x000b, // VT
|
|
63
|
+
0x000c, // FF
|
|
64
|
+
// 0x000D CR - allowed
|
|
65
|
+
0x000e, // SO
|
|
66
|
+
0x000f, // SI
|
|
67
|
+
0x0010, // DLE
|
|
68
|
+
0x0011, // DC1
|
|
69
|
+
0x0012, // DC2
|
|
70
|
+
0x0013, // DC3
|
|
71
|
+
0x0014, // DC4
|
|
72
|
+
0x0015, // NAK
|
|
73
|
+
0x0016, // SYN
|
|
74
|
+
0x0017, // ETB
|
|
75
|
+
0x0018, // CAN
|
|
76
|
+
0x0019, // EM
|
|
77
|
+
0x001a, // SUB
|
|
78
|
+
0x001b, // ESC
|
|
79
|
+
0x001c, // FS
|
|
80
|
+
0x001d, // GS
|
|
81
|
+
0x001e, // RS
|
|
82
|
+
0x001f, // US
|
|
83
|
+
0x007f, // DEL
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Suspicious script patterns (regex patterns and descriptions)
|
|
89
|
+
*/
|
|
90
|
+
const SUSPICIOUS_SCRIPT_PATTERNS: Array<{
|
|
91
|
+
pattern: RegExp;
|
|
92
|
+
message: string;
|
|
93
|
+
severity: FindingSeverity;
|
|
94
|
+
}> = [
|
|
95
|
+
{
|
|
96
|
+
pattern: /curl\s+.*\|\s*(ba)?sh/i,
|
|
97
|
+
message: "Piping curl to shell can execute arbitrary remote code",
|
|
98
|
+
severity: "high",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
pattern: /wget\s+.*\|\s*(ba)?sh/i,
|
|
102
|
+
message: "Piping wget to shell can execute arbitrary remote code",
|
|
103
|
+
severity: "high",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
pattern: /eval\s*\(\s*\$\(/,
|
|
107
|
+
message: "eval with command substitution can be dangerous",
|
|
108
|
+
severity: "medium",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
pattern: /rm\s+-rf\s+\/($|\s)|rm\s+-rf\s+~($|\s)/,
|
|
112
|
+
message: "Recursive deletion from root or home directory",
|
|
113
|
+
severity: "critical",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
pattern: /chmod\s+777/,
|
|
117
|
+
message: "Setting world-writable permissions",
|
|
118
|
+
severity: "medium",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
pattern: /\bsudo\b.*>/,
|
|
122
|
+
message: "Using sudo with output redirection",
|
|
123
|
+
severity: "medium",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
pattern: /base64\s+-d.*\|\s*(ba)?sh/i,
|
|
127
|
+
message: "Decoding and executing base64 content",
|
|
128
|
+
severity: "high",
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* File extensions that are considered binary (not content files)
|
|
134
|
+
*/
|
|
135
|
+
const BINARY_EXTENSIONS = new Set([
|
|
136
|
+
".exe",
|
|
137
|
+
".dll",
|
|
138
|
+
".so",
|
|
139
|
+
".dylib",
|
|
140
|
+
".bin",
|
|
141
|
+
".o",
|
|
142
|
+
".a",
|
|
143
|
+
".lib",
|
|
144
|
+
".pyc",
|
|
145
|
+
".pyo",
|
|
146
|
+
".class",
|
|
147
|
+
".jar",
|
|
148
|
+
".war",
|
|
149
|
+
".ear",
|
|
150
|
+
".wasm",
|
|
151
|
+
".node",
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* File extensions that should be scanned for content
|
|
156
|
+
*/
|
|
157
|
+
const TEXT_EXTENSIONS = new Set([
|
|
158
|
+
".md",
|
|
159
|
+
".txt",
|
|
160
|
+
".toml",
|
|
161
|
+
".yaml",
|
|
162
|
+
".yml",
|
|
163
|
+
".json",
|
|
164
|
+
".js",
|
|
165
|
+
".ts",
|
|
166
|
+
".sh",
|
|
167
|
+
".bash",
|
|
168
|
+
".zsh",
|
|
169
|
+
".fish",
|
|
170
|
+
".py",
|
|
171
|
+
".rb",
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Scan a file for suspicious Unicode characters
|
|
176
|
+
*/
|
|
177
|
+
async function scanFileForUnicode(
|
|
178
|
+
filePath: string,
|
|
179
|
+
relativePath: string,
|
|
180
|
+
): Promise<SecurityFinding[]> {
|
|
181
|
+
const findings: SecurityFinding[] = [];
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const content = await readFile(filePath, "utf-8");
|
|
185
|
+
const lines = content.split("\n");
|
|
186
|
+
|
|
187
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
188
|
+
const line = lines[lineNum];
|
|
189
|
+
if (!line) continue;
|
|
190
|
+
|
|
191
|
+
for (let col = 0; col < line.length; col++) {
|
|
192
|
+
const codePoint = line.codePointAt(col);
|
|
193
|
+
if (codePoint === undefined) continue;
|
|
194
|
+
|
|
195
|
+
// Check bidi characters
|
|
196
|
+
if (UNICODE_PATTERNS.bidi.includes(codePoint)) {
|
|
197
|
+
findings.push({
|
|
198
|
+
type: "unicode_bidi",
|
|
199
|
+
severity: "high",
|
|
200
|
+
file: relativePath,
|
|
201
|
+
line: lineNum + 1,
|
|
202
|
+
column: col + 1,
|
|
203
|
+
message: "Bidirectional text override character detected",
|
|
204
|
+
details: `Codepoint U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check zero-width characters (skip BOM at start of file)
|
|
209
|
+
if (UNICODE_PATTERNS.zeroWidth.includes(codePoint)) {
|
|
210
|
+
// Skip BOM at the very start of file
|
|
211
|
+
if (codePoint === 0xfeff && lineNum === 0 && col === 0) continue;
|
|
212
|
+
|
|
213
|
+
findings.push({
|
|
214
|
+
type: "unicode_zero_width",
|
|
215
|
+
severity: "medium",
|
|
216
|
+
file: relativePath,
|
|
217
|
+
line: lineNum + 1,
|
|
218
|
+
column: col + 1,
|
|
219
|
+
message: "Zero-width character detected",
|
|
220
|
+
details: `Codepoint U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}`,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check control characters
|
|
225
|
+
if (UNICODE_PATTERNS.control.includes(codePoint)) {
|
|
226
|
+
findings.push({
|
|
227
|
+
type: "unicode_control",
|
|
228
|
+
severity: "medium",
|
|
229
|
+
file: relativePath,
|
|
230
|
+
line: lineNum + 1,
|
|
231
|
+
column: col + 1,
|
|
232
|
+
message: "Suspicious control character detected",
|
|
233
|
+
details: `Codepoint U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}`,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
// File might be binary or unreadable, skip
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return findings;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Scan a file for suspicious script patterns
|
|
247
|
+
*/
|
|
248
|
+
async function scanFileForScripts(
|
|
249
|
+
filePath: string,
|
|
250
|
+
relativePath: string,
|
|
251
|
+
): Promise<SecurityFinding[]> {
|
|
252
|
+
const findings: SecurityFinding[] = [];
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const content = await readFile(filePath, "utf-8");
|
|
256
|
+
const lines = content.split("\n");
|
|
257
|
+
|
|
258
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
259
|
+
const line = lines[lineNum];
|
|
260
|
+
if (!line) continue;
|
|
261
|
+
|
|
262
|
+
for (const { pattern, message, severity } of SUSPICIOUS_SCRIPT_PATTERNS) {
|
|
263
|
+
if (pattern.test(line)) {
|
|
264
|
+
findings.push({
|
|
265
|
+
type: "suspicious_script",
|
|
266
|
+
severity,
|
|
267
|
+
file: relativePath,
|
|
268
|
+
line: lineNum + 1,
|
|
269
|
+
message,
|
|
270
|
+
details: line.trim().substring(0, 100),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
// File might be binary or unreadable, skip
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return findings;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Check if a symlink escapes the capability directory
|
|
284
|
+
*/
|
|
285
|
+
async function checkSymlink(
|
|
286
|
+
symlinkPath: string,
|
|
287
|
+
relativePath: string,
|
|
288
|
+
capabilityRoot: string,
|
|
289
|
+
): Promise<SecurityFinding | null> {
|
|
290
|
+
try {
|
|
291
|
+
const linkTarget = await readlink(symlinkPath);
|
|
292
|
+
const resolvedTarget = resolve(join(symlinkPath, "..", linkTarget));
|
|
293
|
+
const normalizedRoot = await realpath(capabilityRoot);
|
|
294
|
+
|
|
295
|
+
// Check if it's an absolute path
|
|
296
|
+
if (linkTarget.startsWith("/")) {
|
|
297
|
+
return {
|
|
298
|
+
type: "symlink_absolute",
|
|
299
|
+
severity: "high",
|
|
300
|
+
file: relativePath,
|
|
301
|
+
message: "Symlink points to an absolute path",
|
|
302
|
+
details: `Target: ${linkTarget}`,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Check if resolved path escapes capability root
|
|
307
|
+
const relativeToRoot = relative(normalizedRoot, resolvedTarget);
|
|
308
|
+
if (relativeToRoot.startsWith("..") || relativeToRoot.startsWith("/")) {
|
|
309
|
+
return {
|
|
310
|
+
type: "symlink_escape",
|
|
311
|
+
severity: "critical",
|
|
312
|
+
file: relativePath,
|
|
313
|
+
message: "Symlink escapes capability directory",
|
|
314
|
+
details: `Resolves to: ${resolvedTarget}`,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
} catch {
|
|
318
|
+
// Broken symlink or permission issue
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Check if a file is a binary
|
|
326
|
+
*/
|
|
327
|
+
function isBinaryFile(filePath: string): boolean {
|
|
328
|
+
const ext = filePath.toLowerCase().substring(filePath.lastIndexOf("."));
|
|
329
|
+
return BINARY_EXTENSIONS.has(ext);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Check if a file should be scanned for text content
|
|
334
|
+
*/
|
|
335
|
+
function isTextFile(filePath: string): boolean {
|
|
336
|
+
const ext = filePath.toLowerCase().substring(filePath.lastIndexOf("."));
|
|
337
|
+
return TEXT_EXTENSIONS.has(ext);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Scan a single capability directory
|
|
342
|
+
*/
|
|
343
|
+
export async function scanCapability(
|
|
344
|
+
capabilityId: string,
|
|
345
|
+
capabilityPath: string,
|
|
346
|
+
settings: ScanSettings = DEFAULT_SCAN_SETTINGS,
|
|
347
|
+
): Promise<ScanResult> {
|
|
348
|
+
const startTime = Date.now();
|
|
349
|
+
const findings: SecurityFinding[] = [];
|
|
350
|
+
|
|
351
|
+
if (!existsSync(capabilityPath)) {
|
|
352
|
+
return {
|
|
353
|
+
capabilityId,
|
|
354
|
+
path: capabilityPath,
|
|
355
|
+
findings: [],
|
|
356
|
+
passed: true,
|
|
357
|
+
duration: Date.now() - startTime,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Recursively scan directory
|
|
362
|
+
async function scanDirectory(dirPath: string): Promise<void> {
|
|
363
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
364
|
+
|
|
365
|
+
for (const entry of entries) {
|
|
366
|
+
const fullPath = join(dirPath, entry.name);
|
|
367
|
+
const relativePath = relative(capabilityPath, fullPath);
|
|
368
|
+
|
|
369
|
+
// Skip hidden files and common non-content directories
|
|
370
|
+
if (
|
|
371
|
+
entry.name.startsWith(".") ||
|
|
372
|
+
entry.name === "node_modules" ||
|
|
373
|
+
entry.name === "__pycache__"
|
|
374
|
+
) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const stats = await lstat(fullPath);
|
|
379
|
+
|
|
380
|
+
// Check symlinks
|
|
381
|
+
if (stats.isSymbolicLink() && settings.symlinks) {
|
|
382
|
+
const finding = await checkSymlink(fullPath, relativePath, capabilityPath);
|
|
383
|
+
if (finding) {
|
|
384
|
+
findings.push(finding);
|
|
385
|
+
}
|
|
386
|
+
continue; // Don't follow symlinks
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Handle directories
|
|
390
|
+
if (entry.isDirectory()) {
|
|
391
|
+
await scanDirectory(fullPath);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Handle files
|
|
396
|
+
if (entry.isFile()) {
|
|
397
|
+
// Check for binary files
|
|
398
|
+
if (settings.binaries && isBinaryFile(fullPath)) {
|
|
399
|
+
findings.push({
|
|
400
|
+
type: "binary_file",
|
|
401
|
+
severity: "low",
|
|
402
|
+
file: relativePath,
|
|
403
|
+
message: "Binary file detected in capability",
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Only scan text files for content issues
|
|
408
|
+
if (isTextFile(fullPath)) {
|
|
409
|
+
// Scan for Unicode issues
|
|
410
|
+
if (settings.unicode) {
|
|
411
|
+
const unicodeFindings = await scanFileForUnicode(fullPath, relativePath);
|
|
412
|
+
findings.push(...unicodeFindings);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Scan for script issues (only for shell/script files)
|
|
416
|
+
if (settings.scripts) {
|
|
417
|
+
const ext = fullPath.toLowerCase().substring(fullPath.lastIndexOf("."));
|
|
418
|
+
if ([".sh", ".bash", ".zsh", ".fish", ".py", ".rb", ".js", ".ts"].includes(ext)) {
|
|
419
|
+
const scriptFindings = await scanFileForScripts(fullPath, relativePath);
|
|
420
|
+
findings.push(...scriptFindings);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
await scanDirectory(capabilityPath);
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
capabilityId,
|
|
432
|
+
path: capabilityPath,
|
|
433
|
+
findings,
|
|
434
|
+
passed: findings.length === 0 || findings.every((f) => f.severity === "low"),
|
|
435
|
+
duration: Date.now() - startTime,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Scan multiple capabilities and produce a summary
|
|
441
|
+
*/
|
|
442
|
+
export async function scanCapabilities(
|
|
443
|
+
capabilities: Array<{ id: string; path: string }>,
|
|
444
|
+
config: SecurityConfig = {},
|
|
445
|
+
): Promise<ScanSummary> {
|
|
446
|
+
const settings = {
|
|
447
|
+
...DEFAULT_SCAN_SETTINGS,
|
|
448
|
+
...config.scan,
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const results: ScanResult[] = [];
|
|
452
|
+
|
|
453
|
+
for (const cap of capabilities) {
|
|
454
|
+
// Check if source is trusted
|
|
455
|
+
if (config.trusted_sources && config.trusted_sources.length > 0) {
|
|
456
|
+
// For now, skip trusted source checking (would need source info)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const result = await scanCapability(cap.id, cap.path, settings);
|
|
460
|
+
results.push(result);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Build summary
|
|
464
|
+
const findingsByType: Record<FindingType, number> = {
|
|
465
|
+
unicode_bidi: 0,
|
|
466
|
+
unicode_zero_width: 0,
|
|
467
|
+
unicode_control: 0,
|
|
468
|
+
symlink_escape: 0,
|
|
469
|
+
symlink_absolute: 0,
|
|
470
|
+
suspicious_script: 0,
|
|
471
|
+
binary_file: 0,
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const findingsBySeverity: Record<FindingSeverity, number> = {
|
|
475
|
+
low: 0,
|
|
476
|
+
medium: 0,
|
|
477
|
+
high: 0,
|
|
478
|
+
critical: 0,
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
let totalFindings = 0;
|
|
482
|
+
let capabilitiesWithFindings = 0;
|
|
483
|
+
|
|
484
|
+
for (const result of results) {
|
|
485
|
+
if (result.findings.length > 0) {
|
|
486
|
+
capabilitiesWithFindings++;
|
|
487
|
+
}
|
|
488
|
+
for (const finding of result.findings) {
|
|
489
|
+
totalFindings++;
|
|
490
|
+
findingsByType[finding.type]++;
|
|
491
|
+
findingsBySeverity[finding.severity]++;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
totalCapabilities: capabilities.length,
|
|
497
|
+
capabilitiesWithFindings,
|
|
498
|
+
totalFindings,
|
|
499
|
+
findingsByType,
|
|
500
|
+
findingsBySeverity,
|
|
501
|
+
results,
|
|
502
|
+
allPassed: results.every((r) => r.passed),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Format scan results for console output
|
|
508
|
+
*/
|
|
509
|
+
export function formatScanResults(summary: ScanSummary, verbose = false): string {
|
|
510
|
+
const lines: string[] = [];
|
|
511
|
+
|
|
512
|
+
lines.push("Security Scan Results");
|
|
513
|
+
lines.push("=====================");
|
|
514
|
+
lines.push("");
|
|
515
|
+
|
|
516
|
+
if (summary.totalFindings === 0) {
|
|
517
|
+
lines.push("✓ No security issues found");
|
|
518
|
+
return lines.join("\n");
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
lines.push(
|
|
522
|
+
`Found ${summary.totalFindings} issue(s) in ${summary.capabilitiesWithFindings} capability(ies)`,
|
|
523
|
+
);
|
|
524
|
+
lines.push("");
|
|
525
|
+
|
|
526
|
+
// Show by severity
|
|
527
|
+
if (summary.findingsBySeverity.critical > 0) {
|
|
528
|
+
lines.push(` ✗ Critical: ${summary.findingsBySeverity.critical}`);
|
|
529
|
+
}
|
|
530
|
+
if (summary.findingsBySeverity.high > 0) {
|
|
531
|
+
lines.push(` ✗ High: ${summary.findingsBySeverity.high}`);
|
|
532
|
+
}
|
|
533
|
+
if (summary.findingsBySeverity.medium > 0) {
|
|
534
|
+
lines.push(` ! Medium: ${summary.findingsBySeverity.medium}`);
|
|
535
|
+
}
|
|
536
|
+
if (summary.findingsBySeverity.low > 0) {
|
|
537
|
+
lines.push(` · Low: ${summary.findingsBySeverity.low}`);
|
|
538
|
+
}
|
|
539
|
+
lines.push("");
|
|
540
|
+
|
|
541
|
+
// Show detailed findings if verbose
|
|
542
|
+
if (verbose) {
|
|
543
|
+
for (const result of summary.results) {
|
|
544
|
+
if (result.findings.length === 0) continue;
|
|
545
|
+
|
|
546
|
+
lines.push(`${result.capabilityId}:`);
|
|
547
|
+
for (const finding of result.findings) {
|
|
548
|
+
const location = finding.line
|
|
549
|
+
? `:${finding.line}${finding.column ? `:${finding.column}` : ""}`
|
|
550
|
+
: "";
|
|
551
|
+
const severity = finding.severity.toUpperCase().padEnd(8);
|
|
552
|
+
lines.push(` [${severity}] ${finding.file}${location}`);
|
|
553
|
+
lines.push(` ${finding.message}`);
|
|
554
|
+
if (finding.details) {
|
|
555
|
+
lines.push(` ${finding.details}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
lines.push("");
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return lines.join("\n");
|
|
563
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security scanning types
|
|
3
|
+
*
|
|
4
|
+
* Base types (SecurityMode, ScanSettings, SecurityConfig) are defined in ../types/index.ts
|
|
5
|
+
* and used in OmniConfig. This file contains additional types for the scanner implementation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SecurityConfig, ScanSettings } from "../types/index.js";
|
|
9
|
+
|
|
10
|
+
// Re-export the base types from the main types file
|
|
11
|
+
export type { SecurityMode, ScanSettings, SecurityConfig } from "../types/index.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Severity level for security findings
|
|
15
|
+
*/
|
|
16
|
+
export type FindingSeverity = "low" | "medium" | "high" | "critical";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Types of security findings
|
|
20
|
+
*/
|
|
21
|
+
export type FindingType =
|
|
22
|
+
| "unicode_bidi"
|
|
23
|
+
| "unicode_zero_width"
|
|
24
|
+
| "unicode_control"
|
|
25
|
+
| "symlink_escape"
|
|
26
|
+
| "symlink_absolute"
|
|
27
|
+
| "suspicious_script"
|
|
28
|
+
| "binary_file";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A security finding (potential issue)
|
|
32
|
+
*/
|
|
33
|
+
export interface SecurityFinding {
|
|
34
|
+
/** Type of finding */
|
|
35
|
+
type: FindingType;
|
|
36
|
+
/** Severity level */
|
|
37
|
+
severity: FindingSeverity;
|
|
38
|
+
/** File path relative to capability root */
|
|
39
|
+
file: string;
|
|
40
|
+
/** Line number (if applicable) */
|
|
41
|
+
line?: number;
|
|
42
|
+
/** Column number (if applicable) */
|
|
43
|
+
column?: number;
|
|
44
|
+
/** Human-readable description */
|
|
45
|
+
message: string;
|
|
46
|
+
/** Additional details (e.g., specific codepoints found) */
|
|
47
|
+
details?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Result of scanning a capability
|
|
52
|
+
*/
|
|
53
|
+
export interface ScanResult {
|
|
54
|
+
/** Capability ID */
|
|
55
|
+
capabilityId: string;
|
|
56
|
+
/** Capability path */
|
|
57
|
+
path: string;
|
|
58
|
+
/** List of findings */
|
|
59
|
+
findings: SecurityFinding[];
|
|
60
|
+
/** Whether the scan passed (no findings or mode=warn) */
|
|
61
|
+
passed: boolean;
|
|
62
|
+
/** Scan duration in milliseconds */
|
|
63
|
+
duration: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Overall scan summary
|
|
68
|
+
*/
|
|
69
|
+
export interface ScanSummary {
|
|
70
|
+
/** Total capabilities scanned */
|
|
71
|
+
totalCapabilities: number;
|
|
72
|
+
/** Capabilities with findings */
|
|
73
|
+
capabilitiesWithFindings: number;
|
|
74
|
+
/** Total findings */
|
|
75
|
+
totalFindings: number;
|
|
76
|
+
/** Findings by type */
|
|
77
|
+
findingsByType: Record<FindingType, number>;
|
|
78
|
+
/** Findings by severity */
|
|
79
|
+
findingsBySeverity: Record<FindingSeverity, number>;
|
|
80
|
+
/** Individual scan results */
|
|
81
|
+
results: ScanResult[];
|
|
82
|
+
/** Whether all scans passed */
|
|
83
|
+
allPassed: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Default security configuration
|
|
88
|
+
*/
|
|
89
|
+
export const DEFAULT_SECURITY_CONFIG: Required<SecurityConfig> = {
|
|
90
|
+
mode: "off",
|
|
91
|
+
trusted_sources: [],
|
|
92
|
+
scan: {
|
|
93
|
+
unicode: true,
|
|
94
|
+
symlinks: true,
|
|
95
|
+
scripts: true,
|
|
96
|
+
binaries: false,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Default scan settings
|
|
102
|
+
*/
|
|
103
|
+
export const DEFAULT_SCAN_SETTINGS: Required<ScanSettings> = {
|
|
104
|
+
unicode: true,
|
|
105
|
+
symlinks: true,
|
|
106
|
+
scripts: true,
|
|
107
|
+
binaries: false,
|
|
108
|
+
};
|
package/src/state/index.ts
CHANGED