vskill 1.2.6 → 1.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/agents.json +1 -1
- package/dist/audit/fix-suggestions.js +6 -0
- package/dist/audit/fix-suggestions.js.map +1 -1
- package/dist/commands/add.d.ts +5 -0
- package/dist/commands/add.js +10 -4
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/scan.js +1 -1
- package/dist/commands/scan.js.map +1 -1
- package/dist/scanner/hidden-content.d.ts +47 -0
- package/dist/scanner/hidden-content.js +904 -0
- package/dist/scanner/hidden-content.js.map +1 -0
- package/dist/scanner/installer-hosts.d.ts +26 -0
- package/dist/scanner/installer-hosts.js +92 -0
- package/dist/scanner/installer-hosts.js.map +1 -0
- package/dist/scanner/judge-guard.d.ts +119 -0
- package/dist/scanner/judge-guard.js +481 -0
- package/dist/scanner/judge-guard.js.map +1 -0
- package/dist/scanner/patterns.d.ts +12 -0
- package/dist/scanner/patterns.js +353 -113
- package/dist/scanner/patterns.js.map +1 -1
- package/dist/scanner/repo-scanner.d.ts +4 -0
- package/dist/scanner/repo-scanner.js +4 -1
- package/dist/scanner/repo-scanner.js.map +1 -1
- package/dist/scanner/tier1.js +37 -7
- package/dist/scanner/tier1.js.map +1 -1
- package/dist/scanner/tier2-llm.d.ts +22 -4
- package/dist/scanner/tier2-llm.js +163 -23
- package/dist/scanner/tier2-llm.js.map +1 -1
- package/dist/scanner/types.d.ts +8 -0
- package/package.json +1 -1
package/dist/scanner/patterns.js
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// Tier 1 Security Scan Patterns
|
|
3
|
-
//
|
|
3
|
+
// 58 regex-based patterns for static analysis of skill content, plus the
|
|
4
|
+
// hidden-content checks (HC-*) in hidden-content.ts.
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
|
-
|
|
6
|
+
import { HIDDEN_CONTENT_RULES, scanHiddenContent } from "./hidden-content.js";
|
|
7
|
+
import { DOT_SEGMENT_URL_SOURCE, OFFICIAL_INSTALLER_URL_SOURCE } from "./installer-hosts.js";
|
|
8
|
+
// ---- Shared fragments ------------------------------------------------------
|
|
9
|
+
/**
|
|
10
|
+
* netcat in command position: at the start of a line or string or after a
|
|
11
|
+
* list marker or shell prompt there, after a shell separator, colon, quote,
|
|
12
|
+
* backtick or `$(`, or after a wrapper or verb such as sudo, nohup,
|
|
13
|
+
* timeout N, run or Run; an absolute path is allowed. No fragment repeats and
|
|
14
|
+
* no whitespace run crosses a newline, so a long line or a run of blank lines
|
|
15
|
+
* is scanned in one pass.
|
|
16
|
+
*/
|
|
17
|
+
const NETCAT_COMMAND = String.raw `(?:^|^[^\S\n]*(?:[-*+>$#]|\d+[.)])(?=\s)|[\n|;&(\x60'":]|\$\(|\b(?:sudo|doas|busybox|nohup|exec|env|command|time|then|do|[Rr]un|[Ee]xecute)(?=\s)|\btimeout[^\S\n]+[\w.]+(?=\s))[^\S\n]*(?:(?:\/[\w.-]+)*\/)?(?:nc|ncat|netcat)\b`;
|
|
18
|
+
/**
|
|
19
|
+
* An official installer URL start with no dot segment in reach: a dot
|
|
20
|
+
* segment can climb out of an allowlisted owner's path on
|
|
21
|
+
* raw.githubusercontent.com, so such a URL is not official.
|
|
22
|
+
*/
|
|
23
|
+
const OFFICIAL_INSTALLER_URL_SOURCE_NO_DOT_SEGMENTS = `(?!${DOT_SEGMENT_URL_SOURCE})(?:${OFFICIAL_INSTALLER_URL_SOURCE})`;
|
|
24
|
+
/** A filesystem API call opening its argument list (FS-003). */
|
|
25
|
+
const FS_CALL = String.raw `(?:readFile|writeFile|appendFile|createReadStream|createWriteStream|open|unlink|stat|lstat|fstat|access|rename|copyFile|chmod|chown|truncate)(?:Sync)?\s*\(`;
|
|
26
|
+
// ---- Patterns (58 total) --------------------------------------------------
|
|
6
27
|
export const SCAN_PATTERNS = [
|
|
7
28
|
// --- Command Injection (1-7) ---------------------------------------------
|
|
8
29
|
{
|
|
@@ -50,7 +71,7 @@ export const SCAN_PATTERNS = [
|
|
|
50
71
|
name: "Shell pipe operator",
|
|
51
72
|
severity: "medium",
|
|
52
73
|
description: "Detects shell piping within exec-like calls",
|
|
53
|
-
pattern: /exec\s*\([^)]*\|[^)]*\)/g,
|
|
74
|
+
pattern: /exec\s*\((?:(?!exec\s*\()[^)|\n])*\|(?:(?!exec\s*\()[^)\n])*\)/g,
|
|
54
75
|
category: "command-injection",
|
|
55
76
|
},
|
|
56
77
|
{
|
|
@@ -66,7 +87,38 @@ export const SCAN_PATTERNS = [
|
|
|
66
87
|
name: "Pipe-to-shell execution",
|
|
67
88
|
severity: "critical",
|
|
68
89
|
description: "Detects curl/wget output piped directly to a shell interpreter (download-and-execute)",
|
|
69
|
-
|
|
90
|
+
// Each scan stops at the next curl/wget, so a pipe-free run costs one
|
|
91
|
+
// pass however many times the command repeats, and padding cannot hide the pipe.
|
|
92
|
+
pattern: /\b(?:curl|wget)\b(?:(?!\b(?:curl|wget)\b)[^|\n])*\|\s*(?:(?:\/\w+)*\/)?(?:env\s+)?(?:ba|z|da|k)?sh\b/g,
|
|
93
|
+
category: "command-injection",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "CI-009",
|
|
97
|
+
name: "Python shell execution",
|
|
98
|
+
severity: "high",
|
|
99
|
+
description: "Detects os.system/os.popen and subprocess calls with shell=True",
|
|
100
|
+
// os.system( is already CI-003 ("system("); only the spaced form is added.
|
|
101
|
+
pattern: /\bos\.popen\s*\(|\bos\.system\s+\(|\bsubprocess\.(?:run|call|Popen|check_output|check_call)\s*\([^)]{0,400}?\bshell\s*=\s*True/g,
|
|
102
|
+
category: "command-injection",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "CI-010",
|
|
106
|
+
name: "PowerShell dynamic execution",
|
|
107
|
+
severity: "high",
|
|
108
|
+
description: "Detects Invoke-Expression/iex, -EncodedCommand payloads and base64 decoding in PowerShell",
|
|
109
|
+
pattern: /\bInvoke-Expression\b|\|\s*iex\b|\biex\s*\(\s*(?:\(|\$|\[|['"]|New-Object\b|Invoke-|i(?:wr|rm)\b|Get-Content\b)|\s-(?:EncodedCommand|enc|ec)\s+[A-Za-z0-9+\/=]{20,}|\[(?:System\.)?Convert\]::FromBase64String/gi,
|
|
110
|
+
category: "command-injection",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
// CLI-only: the PowerShell twin of CI-008. A CI-010 match inside this
|
|
114
|
+
// span is reported as CI-011 instead, so installer docs in fenced code
|
|
115
|
+
// (irm bun.sh/install.ps1 | iex) get the same downgrade as curl | sh.
|
|
116
|
+
// The download target must be literal: a $variable URL stays CI-010.
|
|
117
|
+
id: "CI-011",
|
|
118
|
+
name: "PowerShell download-and-execute",
|
|
119
|
+
severity: "critical",
|
|
120
|
+
description: "Detects irm/iwr output piped to iex, or iex of a downloaded script (PowerShell installer one-liners)",
|
|
121
|
+
pattern: /\b(?:irm|iwr|Invoke-RestMethod|Invoke-WebRequest)\b[^|\n$]{1,400}\|\s*(?:iex|Invoke-Expression)\b|\b(?:iex|Invoke-Expression)\s*\(?\s*\(\s*(?:(?:irm|iwr|Invoke-RestMethod|Invoke-WebRequest)\b[^()\n$]{1,400}|New-Object\s+(?:System\.)?Net\.WebClient\s*\)\s*\.DownloadString\s*\(\s*['"]https?:\/\/[^()\n$'"]{1,400}['"]\s*)\)/gi,
|
|
70
122
|
category: "command-injection",
|
|
71
123
|
},
|
|
72
124
|
// --- Data Exfiltration (8-12) --------------------------------------------
|
|
@@ -99,7 +151,10 @@ export const SCAN_PATTERNS = [
|
|
|
99
151
|
name: "DNS exfiltration pattern",
|
|
100
152
|
severity: "medium",
|
|
101
153
|
description: "Detects encoding data into DNS lookups",
|
|
102
|
-
|
|
154
|
+
// Scans like this one stop at the next call of the same function, so a
|
|
155
|
+
// repeated prefix cannot make a long line quadratic, and no length bound
|
|
156
|
+
// lets padding hide the rest of the call.
|
|
157
|
+
pattern: /dns\.(?:resolve|lookup)\s*\((?:(?!dns\.(?:resolve|lookup)\b)[^\n])*?(?:\+|concat|\$\{)/g,
|
|
103
158
|
category: "data-exfiltration",
|
|
104
159
|
},
|
|
105
160
|
{
|
|
@@ -107,7 +162,7 @@ export const SCAN_PATTERNS = [
|
|
|
107
162
|
name: "Base64 encode and send",
|
|
108
163
|
severity: "medium",
|
|
109
164
|
description: "Detects base64 encoding followed by network send (exfil pattern)",
|
|
110
|
-
pattern: /btoa\s*\(|Buffer\.from\([^)]*\)\.toString\s*\(\s*['"]base64['"]/g,
|
|
165
|
+
pattern: /btoa\s*\(|Buffer\.from\((?:(?!Buffer\.from\()[^)\n])*\)\.toString\s*\(\s*['"]base64['"]/g,
|
|
111
166
|
category: "data-exfiltration",
|
|
112
167
|
},
|
|
113
168
|
// --- Privilege Escalation (13-17) ----------------------------------------
|
|
@@ -157,7 +212,7 @@ export const SCAN_PATTERNS = [
|
|
|
157
212
|
name: "Read .env file",
|
|
158
213
|
severity: "critical",
|
|
159
214
|
description: "Detects reading .env files which contain secrets",
|
|
160
|
-
pattern: /readFile(?:Sync)?\s*\([^)]
|
|
215
|
+
pattern: /readFile(?:Sync)?\s*\((?:(?!readFile)[^)\n])*?\.env\b/g,
|
|
161
216
|
category: "credential-theft",
|
|
162
217
|
},
|
|
163
218
|
{
|
|
@@ -189,7 +244,7 @@ export const SCAN_PATTERNS = [
|
|
|
189
244
|
name: "Secrets in environment",
|
|
190
245
|
severity: "high",
|
|
191
246
|
description: "Detects broad process.env access patterns",
|
|
192
|
-
pattern: /process\.env\[(?:[^'"\]]*\+|[a-zA-Z_]\w*\])/g,
|
|
247
|
+
pattern: /process\.env\[(?:(?:(?!process\.env\[)[^'"\]\n])*\+|[a-zA-Z_]\w*\])/g,
|
|
193
248
|
category: "credential-theft",
|
|
194
249
|
},
|
|
195
250
|
{
|
|
@@ -247,15 +302,15 @@ export const SCAN_PATTERNS = [
|
|
|
247
302
|
name: "Write to system paths",
|
|
248
303
|
severity: "critical",
|
|
249
304
|
description: "Detects writing to system-critical paths",
|
|
250
|
-
pattern: /writeFile(?:Sync)?\s*\([^)]
|
|
305
|
+
pattern: /writeFile(?:Sync)?\s*\((?:(?!writeFile)[^)\n])*?(?:\/etc\/|\/usr\/|\/var\/|\/System\/|C:\\Windows)/g,
|
|
251
306
|
category: "filesystem-access",
|
|
252
307
|
},
|
|
253
308
|
{
|
|
254
309
|
id: "FS-003",
|
|
255
310
|
name: "Path traversal",
|
|
256
311
|
severity: "high",
|
|
257
|
-
description: "Detects directory traversal
|
|
258
|
-
pattern:
|
|
312
|
+
description: "Detects directory traversal in filesystem API call arguments",
|
|
313
|
+
pattern: new RegExp(String.raw `${FS_CALL}(?:(?!${FS_CALL})[^)\n])*\.\.[/\\]\.\.[/\\]`, "g"),
|
|
259
314
|
category: "filesystem-access",
|
|
260
315
|
},
|
|
261
316
|
{
|
|
@@ -280,7 +335,10 @@ export const SCAN_PATTERNS = [
|
|
|
280
335
|
name: "Reverse shell pattern",
|
|
281
336
|
severity: "critical",
|
|
282
337
|
description: "Detects reverse shell connection patterns",
|
|
283
|
-
|
|
338
|
+
// A flag or shell pipe after the host only counts when netcat is the
|
|
339
|
+
// command itself, and the argument run stops at the first | ; or &, so
|
|
340
|
+
// `nc -z db 5432 && psql -c ...` and prose about `wc -c` stay clean.
|
|
341
|
+
pattern: new RegExp(String.raw `(?:\/dev\/tcp\/|\b(?:nc|netcat)\s+-[a-z]*e|\bncat\b\s+-|${NETCAT_COMMAND}[^\n|;&]{0,160}?\s(?:-e|-c|--exec|--sh-exec)\b|${NETCAT_COMMAND}[^\n|;&]{0,160}\|\s*(?:(?:\/\w+)*\/)?(?:ba|z|da|k)?sh\b|bash\s+-i\s+>&|mkfifo\s+\S|0<&\d+)`, "g"),
|
|
284
342
|
category: "network-access",
|
|
285
343
|
},
|
|
286
344
|
{
|
|
@@ -291,6 +349,23 @@ export const SCAN_PATTERNS = [
|
|
|
291
349
|
pattern: /(?:http|https):\/\/\$\{|(?:http|https):\/\/['"]\s*\+/g,
|
|
292
350
|
category: "network-access",
|
|
293
351
|
},
|
|
352
|
+
{
|
|
353
|
+
id: "NA-004",
|
|
354
|
+
name: "Fake prerequisite download",
|
|
355
|
+
severity: "high",
|
|
356
|
+
description: "Detects instructions to download and run an external binary or script as a setup step (ClawHavoc-style)",
|
|
357
|
+
pattern: new RegExp(String.raw `\b(?:before (?:using|running|installing)|prerequisites?|required?|first)\b[^.\n]{0,120}?\b(?:download|install|run|execute)\b[^\n]{0,160}?(?:https?:\/\/(?!${OFFICIAL_INSTALLER_URL_SOURCE_NO_DOT_SEGMENTS})[^\/\s)'"\x60]{1,253}\/[^\s)'"\x60]{0,400}?\.(?:zip|exe|dmg|pkg|msi|sh|ps1|bat|cmd|command|app|scr)\b|\b(?:glot\.io|pastebin\.com|rentry\.co|paste\.ee|transfer\.sh|hastebin\.com)\/)`, "gi"),
|
|
358
|
+
category: "network-access",
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
id: "NA-005",
|
|
362
|
+
name: "Password-protected archive",
|
|
363
|
+
severity: "high",
|
|
364
|
+
description: "Detects password-protected archives, a common way to hide payloads from scanners",
|
|
365
|
+
// No /i flag: unzip -P is the password but -p pipes to stdout.
|
|
366
|
+
pattern: /\b(?:[Zz][Ii][Pp]|[Aa][Rr][Cc][Hh][Ii][Vv][Ee]|[Rr][Aa][Rr]|7[Zz])\b[^\n]{0,80}?\b[Pp][Aa][Ss][Ss](?:[Ww][Oo][Rr][Dd]|[Cc][Oo][Dd][Ee])\s*(?:[Ii][Ss]\s*)?[:=]\s*\S+|\bunzip\b[^|&;\n]{0,60}?\s-[a-zA-Z]*P\s*\S+|\b(?:7z|7za|7zz|unrar)\s+[ex]\b[^|&;\n]{0,60}?\s-[pP]\S+/g,
|
|
367
|
+
category: "network-access",
|
|
368
|
+
},
|
|
294
369
|
// --- Code Execution (35-37) ----------------------------------------------
|
|
295
370
|
{
|
|
296
371
|
id: "CE-001",
|
|
@@ -316,15 +391,21 @@ export const SCAN_PATTERNS = [
|
|
|
316
391
|
pattern: /import\s*\(\s*(?:`[^`]*\$\{|[a-zA-Z_]\w*\s*[\+)])/g,
|
|
317
392
|
category: "code-execution",
|
|
318
393
|
},
|
|
394
|
+
{
|
|
395
|
+
id: "CE-004",
|
|
396
|
+
name: "Python dynamic execution or deserialization",
|
|
397
|
+
severity: "high",
|
|
398
|
+
description: "Detects pickle/marshal loads and exec() of decoded or compiled code",
|
|
399
|
+
pattern: /\b(?:pickle|cPickle|_pickle|marshal|dill)\.loads?\s*\(|\bexec\s*\(\s*(?:compile|base64|codecs|zlib|bytes\.fromhex|marshal|__import__)\b|\b__import__\s*\(\s*['"](?:os|subprocess|socket|pty)['"]\s*\)/g,
|
|
400
|
+
category: "code-execution",
|
|
401
|
+
},
|
|
319
402
|
// --- DCI Block Abuse (38-51) -----------------------------------------------
|
|
320
|
-
// DCI blocks are shell commands in SKILL.md executed via ! prefix.
|
|
321
|
-
// These patterns detect malicious use within DCI contexts.
|
|
322
403
|
{
|
|
323
404
|
id: "DCI-001",
|
|
324
405
|
name: "DCI credential file read",
|
|
325
406
|
severity: "critical",
|
|
326
407
|
description: "DCI block reads credential files (~/.ssh/, ~/.aws/, .env)",
|
|
327
|
-
pattern:
|
|
408
|
+
pattern: /^[^\S\n]*![^\S\n]*`[^`\n]*(?:~\/\.ssh\/|~\/\.aws\/|\.env\b|\.gnupg\/)/gm,
|
|
328
409
|
category: "dci-abuse",
|
|
329
410
|
},
|
|
330
411
|
{
|
|
@@ -332,7 +413,7 @@ export const SCAN_PATTERNS = [
|
|
|
332
413
|
name: "DCI network exfiltration",
|
|
333
414
|
severity: "critical",
|
|
334
415
|
description: "DCI block uses curl/wget for network access",
|
|
335
|
-
pattern:
|
|
416
|
+
pattern: /^[^\S\n]*![^\S\n]*`[^`\n]*\b(?:curl|wget)\b/gm,
|
|
336
417
|
category: "dci-abuse",
|
|
337
418
|
},
|
|
338
419
|
{
|
|
@@ -340,7 +421,9 @@ export const SCAN_PATTERNS = [
|
|
|
340
421
|
name: "DCI fetch/nc network call",
|
|
341
422
|
severity: "critical",
|
|
342
423
|
description: "DCI block uses fetch or netcat for network access",
|
|
343
|
-
|
|
424
|
+
// `git fetch` and other package-manager fetch subcommands are not network
|
|
425
|
+
// calls the skill makes. The lookbehind runs only where `fetch` matched.
|
|
426
|
+
pattern: /^[^\S\n]*![^\S\n]*`[^`\n]*(?:\bfetch\b(?<!\b(?:git|npm|pnpm|yarn)\s+fetch)|\b(?:nc|ncat|netcat)\b)/gm,
|
|
344
427
|
category: "dci-abuse",
|
|
345
428
|
},
|
|
346
429
|
{
|
|
@@ -348,7 +431,9 @@ export const SCAN_PATTERNS = [
|
|
|
348
431
|
name: "DCI agent config write",
|
|
349
432
|
severity: "critical",
|
|
350
433
|
description: "DCI block writes to agent config files (CLAUDE.md, AGENTS.md, .claude/)",
|
|
351
|
-
|
|
434
|
+
// The prefix cannot pass a `>`, so only the first redirect is tried: a
|
|
435
|
+
// later target also follows it, and the line is scanned once.
|
|
436
|
+
pattern: /^[^\S\n]*![^\S\n]*`[^`>\n]*>[^`\n]*(?:CLAUDE\.md|AGENTS\.md|\.claude\/|\.specweave\/)/gm,
|
|
352
437
|
category: "dci-abuse",
|
|
353
438
|
},
|
|
354
439
|
{
|
|
@@ -356,7 +441,9 @@ export const SCAN_PATTERNS = [
|
|
|
356
441
|
name: "DCI agent config modify",
|
|
357
442
|
severity: "critical",
|
|
358
443
|
description: "DCI block modifies agent config via tee/sed/echo append",
|
|
359
|
-
|
|
444
|
+
// Each tee/sed -i scan stops at the next one, each echo scan at the next
|
|
445
|
+
// echo, and each `>>` scan at the next `>>`: linear with no length bound.
|
|
446
|
+
pattern: /^[^\S\n]*![^\S\n]*`[^`\n]*?(?:(?:\btee\b|\bsed\s+-i)(?:(?!\btee\b|\bsed\s+-i)[^`\n])*|\becho\s(?:(?!\becho\s)[^`\n])*?>>(?:(?!>>)[^`\n])*)(?:CLAUDE\.md|AGENTS\.md|\.claude\/)/gm,
|
|
360
447
|
category: "dci-abuse",
|
|
361
448
|
},
|
|
362
449
|
{
|
|
@@ -364,7 +451,7 @@ export const SCAN_PATTERNS = [
|
|
|
364
451
|
name: "DCI base64 decode",
|
|
365
452
|
severity: "critical",
|
|
366
453
|
description: "DCI block contains base64 decoding (obfuscation)",
|
|
367
|
-
pattern:
|
|
454
|
+
pattern: /^[^\S\n]*![^\S\n]*`[^`\n]*\b(?:base64\s+(?:-[dD]|--decode)|atob\s*\()/gm,
|
|
368
455
|
category: "dci-abuse",
|
|
369
456
|
},
|
|
370
457
|
{
|
|
@@ -372,7 +459,7 @@ export const SCAN_PATTERNS = [
|
|
|
372
459
|
name: "DCI hex escape obfuscation",
|
|
373
460
|
severity: "critical",
|
|
374
461
|
description: "DCI block contains hex escape sequences (obfuscation)",
|
|
375
|
-
pattern:
|
|
462
|
+
pattern: /^[^\S\n]*![^\S\n]*`[^`\n]*\\x[0-9a-fA-F]{2}(?:\\x[0-9a-fA-F]{2}){3,}/gm,
|
|
376
463
|
category: "dci-abuse",
|
|
377
464
|
},
|
|
378
465
|
{
|
|
@@ -380,7 +467,7 @@ export const SCAN_PATTERNS = [
|
|
|
380
467
|
name: "DCI eval execution",
|
|
381
468
|
severity: "critical",
|
|
382
469
|
description: "DCI block uses eval for code execution",
|
|
383
|
-
pattern:
|
|
470
|
+
pattern: /^[^\S\n]*![^\S\n]*`[^`\n]*\beval\b/gm,
|
|
384
471
|
category: "dci-abuse",
|
|
385
472
|
},
|
|
386
473
|
{
|
|
@@ -388,7 +475,9 @@ export const SCAN_PATTERNS = [
|
|
|
388
475
|
name: "DCI download and execute",
|
|
389
476
|
severity: "critical",
|
|
390
477
|
description: "DCI block pipes downloaded content to shell (download-and-execute)",
|
|
391
|
-
|
|
478
|
+
// The prefix stops at the first curl/wget; a later shell pipe also
|
|
479
|
+
// follows it, so one pass decides the line.
|
|
480
|
+
pattern: /^[^\S\n]*![^\S\n]*`(?:(?!\b(?:curl|wget)\b)[^`\n])*\b(?:curl|wget)\b[^`\n]*\|\s*(?:ba|z|da|k)?sh\b/gm,
|
|
392
481
|
category: "dci-abuse",
|
|
393
482
|
},
|
|
394
483
|
{
|
|
@@ -396,7 +485,7 @@ export const SCAN_PATTERNS = [
|
|
|
396
485
|
name: "DCI reverse shell",
|
|
397
486
|
severity: "critical",
|
|
398
487
|
description: "DCI block establishes a reverse shell connection",
|
|
399
|
-
pattern:
|
|
488
|
+
pattern: /^[^\S\n]*![^\S\n]*`[^`\n]*(?:\/dev\/tcp\/|bash\s+-i\s+>&|mkfifo|nc\s+-[a-z]*e)/gm,
|
|
400
489
|
category: "dci-abuse",
|
|
401
490
|
},
|
|
402
491
|
{
|
|
@@ -404,7 +493,7 @@ export const SCAN_PATTERNS = [
|
|
|
404
493
|
name: "DCI sudo escalation",
|
|
405
494
|
severity: "critical",
|
|
406
495
|
description: "DCI block uses sudo for privilege escalation",
|
|
407
|
-
pattern:
|
|
496
|
+
pattern: /^[^\S\n]*![^\S\n]*`[^`\n]*\bsudo\b/gm,
|
|
408
497
|
category: "dci-abuse",
|
|
409
498
|
},
|
|
410
499
|
{
|
|
@@ -412,7 +501,7 @@ export const SCAN_PATTERNS = [
|
|
|
412
501
|
name: "DCI rm destructive command",
|
|
413
502
|
severity: "critical",
|
|
414
503
|
description: "DCI block executes destructive rm -rf command",
|
|
415
|
-
pattern:
|
|
504
|
+
pattern: /^[^\S\n]*![^\S\n]*`[^`\n]*\brm\s+-[a-zA-Z]*r[a-zA-Z]*f/gm,
|
|
416
505
|
category: "dci-abuse",
|
|
417
506
|
},
|
|
418
507
|
{
|
|
@@ -420,7 +509,9 @@ export const SCAN_PATTERNS = [
|
|
|
420
509
|
name: "DCI home dir exfiltration",
|
|
421
510
|
severity: "critical",
|
|
422
511
|
description: "DCI block reads from home directory sensitive paths",
|
|
423
|
-
|
|
512
|
+
// Independent lookaheads with no length bound, so padding cannot push the
|
|
513
|
+
// path out of reach; each /home/ scan stops at the next /home/.
|
|
514
|
+
pattern: /^[^\S\n]*![^\S\n]*`(?=[^`\n]*\b(?:cat|less|head|tail|strings)\s)(?=[^`\n]*(?:~\/\.|\/home\/(?:(?!\/home\/)[^`\s])*\.))/gm,
|
|
424
515
|
category: "dci-abuse",
|
|
425
516
|
},
|
|
426
517
|
{
|
|
@@ -428,7 +519,7 @@ export const SCAN_PATTERNS = [
|
|
|
428
519
|
name: "DCI data pipe to network",
|
|
429
520
|
severity: "critical",
|
|
430
521
|
description: "DCI block pipes local data to a network command",
|
|
431
|
-
pattern:
|
|
522
|
+
pattern: /^[^\S\n]*![^\S\n]*`(?=[^`\n]*\b(?:cat|tar|zip)\s)(?=[^`\n]*\|\s*(?:curl|wget|nc)\b)/gm,
|
|
432
523
|
category: "dci-abuse",
|
|
433
524
|
},
|
|
434
525
|
];
|
|
@@ -437,7 +528,7 @@ export const SCAN_PATTERNS = [
|
|
|
437
528
|
// Suppress DCI-abuse findings when the line matches this pattern AND
|
|
438
529
|
// no malicious DCI pattern also matches (prevents appended-command bypass).
|
|
439
530
|
const SAFE_DCI_PATTERNS = [
|
|
440
|
-
|
|
531
|
+
/^[^\S\n]*![^\S\n]*`for\s+d\s+in\s+\.specweave\/skill-memories/,
|
|
441
532
|
];
|
|
442
533
|
/** All malicious DCI patterns extracted for the two-pass safe check */
|
|
443
534
|
const MALICIOUS_DCI_PATTERNS = SCAN_PATTERNS
|
|
@@ -547,71 +638,46 @@ function computeFencedCodeBlockLines(lines) {
|
|
|
547
638
|
}
|
|
548
639
|
return fenced;
|
|
549
640
|
}
|
|
550
|
-
// ---- HTML comment line detection -------------------------------------------
|
|
551
|
-
// Pre-compute which lines are inside HTML comments (<!-- ... -->).
|
|
552
|
-
// Findings inside HTML comments are suppressed entirely.
|
|
553
|
-
function computeHtmlCommentLines(lines) {
|
|
554
|
-
const commented = new Set();
|
|
555
|
-
let inComment = false;
|
|
556
|
-
for (let i = 0; i < lines.length; i++) {
|
|
557
|
-
const line = lines[i];
|
|
558
|
-
if (inComment) {
|
|
559
|
-
if (line.includes("-->")) {
|
|
560
|
-
// Closing line — don't mark it (content may follow -->)
|
|
561
|
-
inComment = false;
|
|
562
|
-
}
|
|
563
|
-
else {
|
|
564
|
-
// Interior line — fully inside comment
|
|
565
|
-
commented.add(i);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
else if (line.includes("<!--") && !line.includes("-->")) {
|
|
569
|
-
// Multi-line comment starts — don't mark opening line
|
|
570
|
-
// (content may precede <!-- or follow on same line)
|
|
571
|
-
inComment = true;
|
|
572
|
-
}
|
|
573
|
-
// Single-line <!-- ... --> comments are NOT marked to prevent
|
|
574
|
-
// bypass via: <!-- --> eval("malicious")
|
|
575
|
-
}
|
|
576
|
-
return commented;
|
|
577
|
-
}
|
|
578
641
|
// ---- Documentation-safe patterns -------------------------------------------
|
|
579
642
|
// These patterns commonly fire on installation instructions and code examples
|
|
580
643
|
// inside fenced code blocks. Downgrade to "info" when in documentation context.
|
|
581
|
-
|
|
582
|
-
//
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (
|
|
595
|
-
return
|
|
596
|
-
|
|
597
|
-
|
|
644
|
+
// NA-004/NA-005 are deliberately absent: fake-prerequisite and password-archive
|
|
645
|
+
// lures are written as prose, so documentation context must not hide them.
|
|
646
|
+
const DOCUMENTATION_SAFE_PATTERNS = new Set([
|
|
647
|
+
"PE-001", "PE-002", "PE-003", "FS-001", "CI-008", "CI-009", "CI-010", "CI-011",
|
|
648
|
+
"CE-004", "CT-002", "CT-004", "NA-001", "PI-003",
|
|
649
|
+
]);
|
|
650
|
+
// CI-010 forms that run hidden or computed code keep their severity even in a
|
|
651
|
+
// fenced example: an -EncodedCommand payload, base64 decoded on a line that
|
|
652
|
+
// also executes, and Invoke-Expression/iex of a variable or a download.
|
|
653
|
+
const CI010_ENCODED_RE = /^\s-(?:EncodedCommand|enc|ec)\s/i;
|
|
654
|
+
const CI010_EXEC_SINK_RE = /\b(?:iex|Invoke-Expression|Invoke-Command|Start-Process|powershell|pwsh)\b|\[ScriptBlock\]::Create|\.Invoke\s*\(|&\s*\(/i;
|
|
655
|
+
const CI010_COMPUTED_RE = /\$[\w{(:]|DownloadString|FromBase64String|GetString|\b(?:irm|iwr|Invoke-RestMethod|Invoke-WebRequest)\b/i;
|
|
656
|
+
function ci010KeepsSeverity(match, line) {
|
|
657
|
+
if (CI010_ENCODED_RE.test(match))
|
|
658
|
+
return true;
|
|
659
|
+
if (/FromBase64String/i.test(match))
|
|
660
|
+
return CI010_EXEC_SINK_RE.test(line);
|
|
661
|
+
return CI010_COMPUTED_RE.test(line);
|
|
598
662
|
}
|
|
599
663
|
// ---- Markdown inline code detection ----------------------------------------
|
|
600
664
|
// Detect if a regex match falls inside markdown inline code (backticks).
|
|
601
665
|
// Inline code in SKILL.md is referential — it names functions/commands, not
|
|
602
666
|
// invokes them. Example: `eval()` in "Watch for: `eval()`, `exec()`".
|
|
603
667
|
/**
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
668
|
+
* The [contentStart, contentEnd) ranges of markdown inline code spans (single
|
|
669
|
+
* or double backtick delimiters) on a line, in order. Fenced code block
|
|
670
|
+
* markers (```) are ignored — handled separately. Computed once per line so
|
|
671
|
+
* many matches on one long line stay linear.
|
|
607
672
|
*/
|
|
608
|
-
function
|
|
673
|
+
function inlineCodeSpans(line) {
|
|
674
|
+
const spans = [];
|
|
609
675
|
let i = 0;
|
|
610
676
|
while (i < line.length) {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
677
|
+
const tick = line.indexOf("`", i);
|
|
678
|
+
if (tick < 0)
|
|
679
|
+
break;
|
|
680
|
+
i = tick;
|
|
615
681
|
// Count opening backticks
|
|
616
682
|
let openLen = 0;
|
|
617
683
|
while (i + openLen < line.length && line[i + openLen] === "`")
|
|
@@ -629,12 +695,91 @@ function isInsideInlineCode(line, matchStart, matchEnd) {
|
|
|
629
695
|
i = contentStart;
|
|
630
696
|
continue;
|
|
631
697
|
}
|
|
632
|
-
|
|
633
|
-
if (matchStart >= contentStart && matchEnd <= closeIdx)
|
|
634
|
-
return true;
|
|
698
|
+
spans.push([contentStart, closeIdx]);
|
|
635
699
|
i = closeIdx + openLen;
|
|
636
700
|
}
|
|
637
|
-
return
|
|
701
|
+
return spans;
|
|
702
|
+
}
|
|
703
|
+
// ---- Finding context ---------------------------------------------------------
|
|
704
|
+
// Each context line is cut to a window around the match column, so a match on
|
|
705
|
+
// a huge line costs O(window), not O(line).
|
|
706
|
+
const CONTEXT_WINDOW = 300;
|
|
707
|
+
function contextWindow(text, column) {
|
|
708
|
+
if (text.length <= CONTEXT_WINDOW)
|
|
709
|
+
return text;
|
|
710
|
+
let start = Math.max(0, Math.min(column - CONTEXT_WINDOW / 2, text.length - CONTEXT_WINDOW));
|
|
711
|
+
let end = start + CONTEXT_WINDOW;
|
|
712
|
+
// Never split a surrogate pair.
|
|
713
|
+
const high = (c) => c >= 0xd800 && c <= 0xdbff;
|
|
714
|
+
if (start > 0 && high(text.charCodeAt(start - 1)))
|
|
715
|
+
start++;
|
|
716
|
+
if (end < text.length && high(text.charCodeAt(end - 1)))
|
|
717
|
+
end--;
|
|
718
|
+
return text.slice(start, end);
|
|
719
|
+
}
|
|
720
|
+
// ---- CI-010 vs CI-011 -------------------------------------------------------
|
|
721
|
+
// CI-011 (PowerShell download-and-execute) is a more specific form of CI-010.
|
|
722
|
+
// A CI-010 match inside a CI-011 span is dropped so the installer one-liner is
|
|
723
|
+
// reported once, under the id that gets the documentation-context downgrade.
|
|
724
|
+
/** Longest rescanned-payload match kept on a finding. */
|
|
725
|
+
const MAX_PAYLOAD_MATCH = 200;
|
|
726
|
+
const INSTALLER_SOURCE = SCAN_PATTERNS.find((p) => p.id === "CI-011").pattern;
|
|
727
|
+
function installerSpans(text) {
|
|
728
|
+
const spans = [];
|
|
729
|
+
const re = new RegExp(INSTALLER_SOURCE.source, INSTALLER_SOURCE.flags);
|
|
730
|
+
let m;
|
|
731
|
+
while ((m = re.exec(text)) !== null) {
|
|
732
|
+
if (m[0].length === 0) {
|
|
733
|
+
re.lastIndex++;
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
spans.push([m.index, m.index + m[0].length]);
|
|
737
|
+
}
|
|
738
|
+
return spans;
|
|
739
|
+
}
|
|
740
|
+
/** True if [start, end) lies inside one of the sorted, non-overlapping spans. */
|
|
741
|
+
function insideSpan(spans, start, end) {
|
|
742
|
+
let lo = 0;
|
|
743
|
+
let hi = spans.length - 1;
|
|
744
|
+
while (lo <= hi) {
|
|
745
|
+
const mid = (lo + hi) >> 1;
|
|
746
|
+
if (spans[mid][0] <= start)
|
|
747
|
+
lo = mid + 1;
|
|
748
|
+
else
|
|
749
|
+
hi = mid - 1;
|
|
750
|
+
}
|
|
751
|
+
return hi >= 0 && end <= spans[hi][1];
|
|
752
|
+
}
|
|
753
|
+
// ---- Finding caps -------------------------------------------------------------
|
|
754
|
+
// A 2MB file of one repeated match must not become hundreds of thousands of
|
|
755
|
+
// findings. Past the caps a finding folds into the latest one of the same
|
|
756
|
+
// pattern and severity; one is still listed when no such finding exists, so
|
|
757
|
+
// a new severity is never lost.
|
|
758
|
+
const MAX_FINDINGS_PER_PATTERN = 100;
|
|
759
|
+
const MAX_FINDINGS = 2000;
|
|
760
|
+
// Decoded payloads are rescanned up to these limits; the rest are reported
|
|
761
|
+
// as one HC-010 marker so the judge knows they were not rescanned.
|
|
762
|
+
const MAX_RESCAN_PAYLOADS = 50;
|
|
763
|
+
const MAX_RESCAN_CHARS = 64 * 1024;
|
|
764
|
+
const UNSCANNED_RULE = HIDDEN_CONTENT_RULES.find((r) => r.id === "HC-010");
|
|
765
|
+
/** Adds findings to `out` under the caps; `add` returns the finding that holds the new one. */
|
|
766
|
+
function cappedFindings(out) {
|
|
767
|
+
const listed = new Map();
|
|
768
|
+
const latest = new Map();
|
|
769
|
+
return (finding) => {
|
|
770
|
+
const key = `${finding.patternId}:${finding.severity}`;
|
|
771
|
+
const count = listed.get(finding.patternId) ?? 0;
|
|
772
|
+
const holder = latest.get(key);
|
|
773
|
+
if (holder && (count >= MAX_FINDINGS_PER_PATTERN || out.length >= MAX_FINDINGS)) {
|
|
774
|
+
holder.occurrences = (holder.occurrences ?? 1) + (finding.occurrences ?? 1);
|
|
775
|
+
holder.foldedFindings = (holder.foldedFindings ?? 0) + 1 + (finding.foldedFindings ?? 0);
|
|
776
|
+
return holder;
|
|
777
|
+
}
|
|
778
|
+
out.push(finding);
|
|
779
|
+
listed.set(finding.patternId, count + 1);
|
|
780
|
+
latest.set(key, finding);
|
|
781
|
+
return finding;
|
|
782
|
+
};
|
|
638
783
|
}
|
|
639
784
|
// ---- Scanner function -----------------------------------------------------
|
|
640
785
|
/**
|
|
@@ -643,67 +788,162 @@ function isInsideInlineCode(line, matchStart, matchEnd) {
|
|
|
643
788
|
export function scanContent(content) {
|
|
644
789
|
const lines = content.split("\n");
|
|
645
790
|
const fencedLines = computeFencedCodeBlockLines(lines);
|
|
646
|
-
const commentLines = computeHtmlCommentLines(lines);
|
|
647
791
|
const findings = [];
|
|
792
|
+
const add = cappedFindings(findings);
|
|
793
|
+
const installerSpanCache = new Map();
|
|
794
|
+
const inlineCodeCache = new Map();
|
|
795
|
+
const safeDciCache = new Map();
|
|
796
|
+
const rmSeverityCache = new Map();
|
|
648
797
|
for (const pattern of SCAN_PATTERNS) {
|
|
649
798
|
// Reset the regex lastIndex for each pattern (they use /g flag)
|
|
650
799
|
const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags);
|
|
651
800
|
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
652
801
|
const line = lines[lineIdx];
|
|
653
802
|
let match;
|
|
803
|
+
// One finding per severity for this pattern on this line; further
|
|
804
|
+
// matches only raise its occurrence count.
|
|
805
|
+
let folded = null;
|
|
654
806
|
while ((match = regex.exec(line)) !== null) {
|
|
655
|
-
//
|
|
656
|
-
if (
|
|
807
|
+
// Guard against zero-length matches looping forever on a /g regex.
|
|
808
|
+
if (match[0].length === 0) {
|
|
809
|
+
regex.lastIndex++;
|
|
657
810
|
continue;
|
|
658
811
|
}
|
|
659
|
-
// Suppress
|
|
660
|
-
if (
|
|
661
|
-
|
|
812
|
+
// Suppress DCI-abuse findings for known-safe DCI patterns
|
|
813
|
+
if (pattern.category === "dci-abuse") {
|
|
814
|
+
let safe = safeDciCache.get(lineIdx);
|
|
815
|
+
if (safe === undefined)
|
|
816
|
+
safeDciCache.set(lineIdx, (safe = isSafeDciBlock(line)));
|
|
817
|
+
if (safe)
|
|
818
|
+
continue;
|
|
662
819
|
}
|
|
663
|
-
//
|
|
664
|
-
|
|
665
|
-
|
|
820
|
+
// HTML comments are NOT suppressed: they render invisibly to a human
|
|
821
|
+
// reviewer but an agent reads them verbatim, so they are where hidden
|
|
822
|
+
// instructions go. hidden-content.ts adds HC-005 for directive bodies.
|
|
823
|
+
if (pattern.id === "CI-010") {
|
|
824
|
+
let spans = installerSpanCache.get(lineIdx);
|
|
825
|
+
if (!spans)
|
|
826
|
+
installerSpanCache.set(lineIdx, (spans = installerSpans(line)));
|
|
827
|
+
if (insideSpan(spans, match.index, match.index + match[0].length))
|
|
828
|
+
continue;
|
|
666
829
|
}
|
|
667
|
-
// Build context: up to 1 line before and after
|
|
668
|
-
const contextLines = [];
|
|
669
|
-
if (lineIdx > 0)
|
|
670
|
-
contextLines.push(lines[lineIdx - 1]);
|
|
671
|
-
contextLines.push(line);
|
|
672
|
-
if (lineIdx < lines.length - 1)
|
|
673
|
-
contextLines.push(lines[lineIdx + 1]);
|
|
674
830
|
// Downgrade FS-001 severity based on context:
|
|
675
831
|
// safe targets → info, system paths → keep high, other → low
|
|
676
832
|
// Only for `rm` commands — rimraf() keeps base severity ("high")
|
|
677
833
|
let severity = pattern.severity;
|
|
678
|
-
if (pattern.id === "FS-001"
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
834
|
+
if (pattern.id === "FS-001") {
|
|
835
|
+
let rmSeverity = rmSeverityCache.get(lineIdx);
|
|
836
|
+
if (rmSeverity === undefined) {
|
|
837
|
+
rmSeverity = !/\brm\s+-/.test(line)
|
|
838
|
+
? null
|
|
839
|
+
: isRmTargetingSafePaths(line)
|
|
840
|
+
? "info"
|
|
841
|
+
: isRmTargetingSystemPaths(line)
|
|
842
|
+
? null
|
|
843
|
+
: "low";
|
|
844
|
+
rmSeverityCache.set(lineIdx, rmSeverity);
|
|
684
845
|
}
|
|
846
|
+
if (rmSeverity)
|
|
847
|
+
severity = rmSeverity;
|
|
685
848
|
}
|
|
686
849
|
// Downgrade documentation-safe patterns inside fenced code blocks
|
|
687
|
-
if (DOCUMENTATION_SAFE_PATTERNS.has(pattern.id) &&
|
|
850
|
+
if (DOCUMENTATION_SAFE_PATTERNS.has(pattern.id) &&
|
|
851
|
+
fencedLines.has(lineIdx) &&
|
|
852
|
+
!(pattern.id === "CI-010" && ci010KeepsSeverity(match[0], line))) {
|
|
688
853
|
severity = "info";
|
|
689
854
|
}
|
|
690
855
|
// Downgrade non-DCI patterns inside markdown inline code to info.
|
|
691
856
|
// Inline code references (e.g., `eval()`) are educational, not invocations.
|
|
692
|
-
if (pattern.category !== "dci-abuse"
|
|
693
|
-
|
|
857
|
+
if (pattern.category !== "dci-abuse") {
|
|
858
|
+
let spans = inlineCodeCache.get(lineIdx);
|
|
859
|
+
if (!spans)
|
|
860
|
+
inlineCodeCache.set(lineIdx, (spans = inlineCodeSpans(line)));
|
|
861
|
+
if (insideSpan(spans, match.index, match.index + match[0].length))
|
|
862
|
+
severity = "info";
|
|
694
863
|
}
|
|
695
|
-
|
|
864
|
+
const prior = folded?.get(severity);
|
|
865
|
+
if (prior) {
|
|
866
|
+
prior.occurrences = (prior.occurrences ?? 1) + 1;
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
// Build context: up to 1 line before and after, each windowed
|
|
870
|
+
// around the match column.
|
|
871
|
+
const contextLines = [];
|
|
872
|
+
if (lineIdx > 0)
|
|
873
|
+
contextLines.push(contextWindow(lines[lineIdx - 1], match.index));
|
|
874
|
+
contextLines.push(contextWindow(line, match.index));
|
|
875
|
+
if (lineIdx < lines.length - 1)
|
|
876
|
+
contextLines.push(contextWindow(lines[lineIdx + 1], match.index));
|
|
877
|
+
const finding = {
|
|
696
878
|
patternId: pattern.id,
|
|
697
879
|
patternName: pattern.name,
|
|
698
880
|
severity,
|
|
881
|
+
...(severity !== pattern.severity ? { downgradedFrom: pattern.severity } : {}),
|
|
699
882
|
category: pattern.category,
|
|
700
883
|
match: match[0],
|
|
701
884
|
lineNumber: lineIdx + 1,
|
|
702
885
|
context: contextLines.join("\n"),
|
|
703
|
-
}
|
|
886
|
+
};
|
|
887
|
+
// Later matches on this line go to whichever finding now holds it.
|
|
888
|
+
(folded ??= new Map()).set(severity, add(finding));
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
// Hidden channels: invisible Unicode, comment directives, encoded text.
|
|
893
|
+
// Decoded payloads are rescanned at full severity: hidden text gets no
|
|
894
|
+
// documentation-context downgrade.
|
|
895
|
+
const hidden = scanHiddenContent(content);
|
|
896
|
+
const extra = [...hidden.findings];
|
|
897
|
+
let budget = MAX_RESCAN_CHARS;
|
|
898
|
+
for (const [index, payload] of hidden.payloads.entries()) {
|
|
899
|
+
if (index >= MAX_RESCAN_PAYLOADS || budget <= 0) {
|
|
900
|
+
const skipped = hidden.payloads.length - index;
|
|
901
|
+
extra.push({
|
|
902
|
+
patternId: UNSCANNED_RULE.id,
|
|
903
|
+
patternName: UNSCANNED_RULE.name,
|
|
904
|
+
severity: UNSCANNED_RULE.severity,
|
|
905
|
+
category: "prompt-injection",
|
|
906
|
+
match: `[vskill: ${skipped} decoded payload${skipped === 1 ? "" : "s"} not rescanned]`,
|
|
907
|
+
lineNumber: payload.lineNumber,
|
|
908
|
+
context: "",
|
|
909
|
+
});
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
// The whole decoded text is rescanned (within the budget), not the
|
|
913
|
+
// display copy.
|
|
914
|
+
const text = payload.text.length > budget ? payload.text.slice(0, budget) : payload.text;
|
|
915
|
+
budget -= text.length;
|
|
916
|
+
const spans = installerSpans(text);
|
|
917
|
+
for (const pattern of SCAN_PATTERNS) {
|
|
918
|
+
const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags);
|
|
919
|
+
let match;
|
|
920
|
+
let first = null;
|
|
921
|
+
while ((match = regex.exec(text)) !== null) {
|
|
922
|
+
if (match[0].length === 0) {
|
|
923
|
+
regex.lastIndex++;
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
if (pattern.id === "CI-010" && insideSpan(spans, match.index, match.index + match[0].length))
|
|
927
|
+
continue;
|
|
928
|
+
if (first) {
|
|
929
|
+
first.occurrences = (first.occurrences ?? 1) + 1;
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
first = {
|
|
933
|
+
patternId: pattern.id,
|
|
934
|
+
patternName: pattern.name,
|
|
935
|
+
severity: pattern.severity,
|
|
936
|
+
category: pattern.category,
|
|
937
|
+
match: `[hidden ${payload.kind}] ${match[0].slice(0, MAX_PAYLOAD_MATCH)}`,
|
|
938
|
+
lineNumber: payload.lineNumber,
|
|
939
|
+
context: contextWindow(text, match.index),
|
|
940
|
+
};
|
|
941
|
+
extra.push(first);
|
|
704
942
|
}
|
|
705
943
|
}
|
|
706
944
|
}
|
|
945
|
+
for (const finding of extra)
|
|
946
|
+
add(finding);
|
|
707
947
|
return findings;
|
|
708
948
|
}
|
|
709
949
|
//# sourceMappingURL=patterns.js.map
|