@skillsmith/core 0.11.7 → 0.12.1
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/CHANGELOG.md +210 -0
- package/README.md +7 -8
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/api/client.types.d.ts +9 -0
- package/dist/src/api/client.types.d.ts.map +1 -1
- package/dist/src/api/schemas.d.ts +6 -0
- package/dist/src/api/schemas.d.ts.map +1 -1
- package/dist/src/api/schemas.js +5 -0
- package/dist/src/api/schemas.js.map +1 -1
- package/dist/src/api/security-summary.d.ts +10 -1
- package/dist/src/api/security-summary.d.ts.map +1 -1
- package/dist/src/api/security-summary.js +9 -0
- package/dist/src/api/security-summary.js.map +1 -1
- package/dist/src/api/security-summary.test.js +79 -1
- package/dist/src/api/security-summary.test.js.map +1 -1
- package/dist/src/api/types.d.ts +9 -0
- package/dist/src/api/types.d.ts.map +1 -1
- package/dist/src/config/token-credentials.d.ts +20 -0
- package/dist/src/config/token-credentials.d.ts.map +1 -1
- package/dist/src/config/token-credentials.js +59 -0
- package/dist/src/config/token-credentials.js.map +1 -1
- package/dist/src/config/token-credentials.test.d.ts +3 -0
- package/dist/src/config/token-credentials.test.d.ts.map +1 -1
- package/dist/src/config/token-credentials.test.js +68 -0
- package/dist/src/config/token-credentials.test.js.map +1 -1
- package/dist/src/exports/services.d.ts +1 -1
- package/dist/src/exports/services.d.ts.map +1 -1
- package/dist/src/exports/services.js +3 -1
- package/dist/src/exports/services.js.map +1 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/install/agent-config-merge.json-array.d.ts +8 -0
- package/dist/src/install/agent-config-merge.json-array.d.ts.map +1 -1
- package/dist/src/install/agent-config-merge.json-array.js +22 -2
- package/dist/src/install/agent-config-merge.json-array.js.map +1 -1
- package/dist/src/install/agent-pack-installer.cursor-hooks.d.ts.map +1 -1
- package/dist/src/install/agent-pack-installer.cursor-hooks.js +122 -3
- package/dist/src/install/agent-pack-installer.cursor-hooks.js.map +1 -1
- package/dist/src/install/agent-pack-installer.cursor-hooks.test.js +146 -1
- package/dist/src/install/agent-pack-installer.cursor-hooks.test.js.map +1 -1
- package/dist/src/install/agent-pack-installer.harness.d.ts +8 -0
- package/dist/src/install/agent-pack-installer.harness.d.ts.map +1 -1
- package/dist/src/install/agent-pack-installer.harness.js +8 -1
- package/dist/src/install/agent-pack-installer.harness.js.map +1 -1
- package/dist/src/scripts/__tests__/scan-imported-skills.test.js +25 -0
- package/dist/src/scripts/__tests__/scan-imported-skills.test.js.map +1 -1
- package/dist/src/scripts/skill-scanner/trust-scorer.js +1 -1
- package/dist/src/scripts/skill-scanner/trust-scorer.js.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.archive.d.ts +34 -0
- package/dist/src/security/scanner/SecurityScanner.archive.d.ts.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.archive.js +218 -0
- package/dist/src/security/scanner/SecurityScanner.archive.js.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.compound.d.ts +1 -0
- package/dist/src/security/scanner/SecurityScanner.compound.d.ts.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.compound.js +117 -38
- package/dist/src/security/scanner/SecurityScanner.compound.js.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.d.ts +41 -4
- package/dist/src/security/scanner/SecurityScanner.d.ts.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.decoy.d.ts +72 -0
- package/dist/src/security/scanner/SecurityScanner.decoy.d.ts.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.decoy.js +265 -0
- package/dist/src/security/scanner/SecurityScanner.decoy.js.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.encoding.d.ts +56 -0
- package/dist/src/security/scanner/SecurityScanner.encoding.d.ts.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.encoding.js +270 -0
- package/dist/src/security/scanner/SecurityScanner.encoding.js.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.exec.d.ts +23 -20
- package/dist/src/security/scanner/SecurityScanner.exec.d.ts.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.exec.js +207 -52
- package/dist/src/security/scanner/SecurityScanner.exec.js.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.fetch-correlation.d.ts +115 -0
- package/dist/src/security/scanner/SecurityScanner.fetch-correlation.d.ts.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.fetch-correlation.js +181 -0
- package/dist/src/security/scanner/SecurityScanner.fetch-correlation.js.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.helpers.d.ts +14 -11
- package/dist/src/security/scanner/SecurityScanner.helpers.d.ts.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.helpers.js +26 -124
- package/dist/src/security/scanner/SecurityScanner.helpers.js.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.js +114 -36
- package/dist/src/security/scanner/SecurityScanner.js.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.paste-host.d.ts +69 -0
- package/dist/src/security/scanner/SecurityScanner.paste-host.d.ts.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.paste-host.js +342 -0
- package/dist/src/security/scanner/SecurityScanner.paste-host.js.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.risk-score.d.ts +27 -0
- package/dist/src/security/scanner/SecurityScanner.risk-score.d.ts.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.risk-score.js +178 -0
- package/dist/src/security/scanner/SecurityScanner.risk-score.js.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.urls.d.ts +20 -0
- package/dist/src/security/scanner/SecurityScanner.urls.d.ts.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.urls.js +26 -0
- package/dist/src/security/scanner/SecurityScanner.urls.js.map +1 -0
- package/dist/src/security/scanner/index.d.ts +2 -2
- package/dist/src/security/scanner/index.d.ts.map +1 -1
- package/dist/src/security/scanner/index.js +5 -1
- package/dist/src/security/scanner/index.js.map +1 -1
- package/dist/src/security/scanner/multiline-category-closure.test.js +6 -0
- package/dist/src/security/scanner/multiline-category-closure.test.js.map +1 -1
- package/dist/src/security/scanner/patterns.d.ts +41 -21
- package/dist/src/security/scanner/patterns.d.ts.map +1 -1
- package/dist/src/security/scanner/patterns.exec.d.ts +89 -0
- package/dist/src/security/scanner/patterns.exec.d.ts.map +1 -0
- package/dist/src/security/scanner/patterns.exec.js +115 -0
- package/dist/src/security/scanner/patterns.exec.js.map +1 -0
- package/dist/src/security/scanner/patterns.js +78 -41
- package/dist/src/security/scanner/patterns.js.map +1 -1
- package/dist/src/security/scanner/types.d.ts +15 -1
- package/dist/src/security/scanner/types.d.ts.map +1 -1
- package/dist/src/security/scanner/typosquat.d.ts +10 -0
- package/dist/src/security/scanner/typosquat.d.ts.map +1 -1
- package/dist/src/security/scanner/typosquat.js +5 -1
- package/dist/src/security/scanner/typosquat.js.map +1 -1
- package/dist/src/security/scanner/weights.d.ts +12 -3
- package/dist/src/security/scanner/weights.d.ts.map +1 -1
- package/dist/src/security/scanner/weights.js +50 -1
- package/dist/src/security/scanner/weights.js.map +1 -1
- package/dist/src/services/agent-pack/prompt-source.js +1 -1
- package/dist/src/services/agent-pack/prompt-source.js.map +1 -1
- package/dist/src/services/bundled-sibling-scan.d.ts +87 -13
- package/dist/src/services/bundled-sibling-scan.d.ts.map +1 -1
- package/dist/src/services/bundled-sibling-scan.js +177 -33
- package/dist/src/services/bundled-sibling-scan.js.map +1 -1
- package/dist/src/services/recommend-guard.d.ts.map +1 -1
- package/dist/src/services/recommend-guard.js +8 -1
- package/dist/src/services/recommend-guard.js.map +1 -1
- package/dist/src/services/recommend-guard.test.js +6 -2
- package/dist/src/services/recommend-guard.test.js.map +1 -1
- package/dist/src/sync/index.d.ts +1 -0
- package/dist/src/sync/index.d.ts.map +1 -1
- package/dist/src/sync/index.js +2 -0
- package/dist/src/sync/index.js.map +1 -1
- package/dist/src/sync/license-status-client.d.ts +56 -0
- package/dist/src/sync/license-status-client.d.ts.map +1 -0
- package/dist/src/sync/license-status-client.js +118 -0
- package/dist/src/sync/license-status-client.js.map +1 -0
- package/dist/src/sync/license-status-client.test.d.ts +14 -0
- package/dist/src/sync/license-status-client.test.d.ts.map +1 -0
- package/dist/src/sync/license-status-client.test.js +136 -0
- package/dist/src/sync/license-status-client.test.js.map +1 -0
- package/dist/src/types.d.ts +13 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/version-check.d.ts +58 -5
- package/dist/src/utils/version-check.d.ts.map +1 -1
- package/dist/src/utils/version-check.js +73 -2
- package/dist/src/utils/version-check.js.map +1 -1
- package/dist/src/utils/version-check.test.js +49 -10
- package/dist/src/utils/version-check.test.js.map +1 -1
- package/dist/tests/SecurityScanner.scoring.test.js +123 -0
- package/dist/tests/SecurityScanner.scoring.test.js.map +1 -1
- package/dist/tests/security/advisory-severity-guard.test.d.ts +20 -0
- package/dist/tests/security/advisory-severity-guard.test.d.ts.map +1 -0
- package/dist/tests/security/advisory-severity-guard.test.js +70 -0
- package/dist/tests/security/advisory-severity-guard.test.js.map +1 -0
- package/dist/tests/security/archive-evasion.test.d.ts +2 -0
- package/dist/tests/security/archive-evasion.test.d.ts.map +1 -0
- package/dist/tests/security/archive-evasion.test.js +126 -0
- package/dist/tests/security/archive-evasion.test.js.map +1 -0
- package/dist/tests/security/clawhavoc-e2e.fixtures.d.ts +71 -0
- package/dist/tests/security/clawhavoc-e2e.fixtures.d.ts.map +1 -0
- package/dist/tests/security/clawhavoc-e2e.fixtures.js +135 -0
- package/dist/tests/security/clawhavoc-e2e.fixtures.js.map +1 -0
- package/dist/tests/security/co-signal-escalation.test.d.ts +30 -0
- package/dist/tests/security/co-signal-escalation.test.d.ts.map +1 -0
- package/dist/tests/security/co-signal-escalation.test.js +494 -0
- package/dist/tests/security/co-signal-escalation.test.js.map +1 -0
- package/dist/tests/security/decoy-misdirection.test.d.ts +2 -0
- package/dist/tests/security/decoy-misdirection.test.d.ts.map +1 -0
- package/dist/tests/security/decoy-misdirection.test.js +130 -0
- package/dist/tests/security/decoy-misdirection.test.js.map +1 -0
- package/dist/tests/security/encoded-payload.test.d.ts +2 -0
- package/dist/tests/security/encoded-payload.test.d.ts.map +1 -0
- package/dist/tests/security/encoded-payload.test.js +199 -0
- package/dist/tests/security/encoded-payload.test.js.map +1 -0
- package/dist/tests/security/gatekeeper-bypass.test.d.ts +2 -0
- package/dist/tests/security/gatekeeper-bypass.test.d.ts.map +1 -0
- package/dist/tests/security/gatekeeper-bypass.test.js +167 -0
- package/dist/tests/security/gatekeeper-bypass.test.js.map +1 -0
- package/dist/tests/security/paste-host-fetch.test.d.ts +2 -0
- package/dist/tests/security/paste-host-fetch.test.d.ts.map +1 -0
- package/dist/tests/security/paste-host-fetch.test.js +199 -0
- package/dist/tests/security/paste-host-fetch.test.js.map +1 -0
- package/dist/tests/security/scanner-regression-guard.test.js +5 -0
- package/dist/tests/security/scanner-regression-guard.test.js.map +1 -1
- package/dist/tests/services/aidefence-feedback.test.js +5 -0
- package/dist/tests/services/aidefence-feedback.test.js.map +1 -1
- package/dist/tests/services/bundled-sibling-scan.test.js +171 -10
- package/dist/tests/services/bundled-sibling-scan.test.js.map +1 -1
- package/dist/tests/skill-scanner/allowlist.test.js +12 -1
- package/dist/tests/skill-scanner/allowlist.test.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Scanner — decoy/misdirection URL-target heuristic
|
|
3
|
+
* @module @skillsmith/core/security/scanner/SecurityScanner.decoy
|
|
4
|
+
*
|
|
5
|
+
* SMI-6033 Wave 4 (Gap 6): reuses the typosquat detector's brand data —
|
|
6
|
+
* `BRAND_ALIASES` (typosquat.ts) and `AUTHORITY_CLAIMING_AFFIXES`
|
|
7
|
+
* (typosquat.ts, exported for exactly this reuse as part of Wave 1's Gap 6
|
|
8
|
+
* prerequisite) — to catch a fetch/exec instruction whose surrounding prose
|
|
9
|
+
* claims a specific vendor (a brand token, optionally reinforced by an
|
|
10
|
+
* authority-claiming affix like "official"/"verified") while the fetch
|
|
11
|
+
* target's actual domain does not belong to that vendor and is not in the
|
|
12
|
+
* general `DEFAULT_ALLOWED_DOMAINS` allowlist.
|
|
13
|
+
*
|
|
14
|
+
* Per the plan's §9 reconciliation table: `decoy_misdirection` is "N/A —
|
|
15
|
+
* never standalone... Approximate NL heuristic by construction; co-signal
|
|
16
|
+
* required (medium tier), unchanged." This detector therefore NEVER emits
|
|
17
|
+
* `high`/`critical` — only `medium`, or nothing (see the doc-context
|
|
18
|
+
* downgrade below, which uses `low`, not a third severity choice for the
|
|
19
|
+
* non-doc case). The co-signal mechanism that would let this medium finding
|
|
20
|
+
* contribute toward escalating a separate weak `code_execution` finding is a
|
|
21
|
+
* SEPARATE dispatch's job — `SecurityScanner.exec.ts`'s
|
|
22
|
+
* `CO_SIGNAL_MIN_SEVERITY` replacement — and is NOT implemented in this file.
|
|
23
|
+
*
|
|
24
|
+
* Design note, NOT explicit in the plan text and flagged here for review:
|
|
25
|
+
* the plan says to compare "the claimed vendor's canonical domain" against
|
|
26
|
+
* the fetch target's domain, but `BRAND_ALIASES`'s values are GitHub OWNER
|
|
27
|
+
* SLUGS (`anthropics`, `google-gemini`, ...) — they answer "whose GitHub org
|
|
28
|
+
* publishes this," not "what is this vendor's real website." A small
|
|
29
|
+
* curated `BRAND_CANONICAL_DOMAINS` map below, scoped to the exact same six
|
|
30
|
+
* brand tokens `BRAND_ALIASES` already curates, fills that gap; a brand
|
|
31
|
+
* token with no entry here is not currently possible (the two maps share the
|
|
32
|
+
* same key set by construction) but the lookup fails safe (empty domain
|
|
33
|
+
* list -> never matches -> never suppresses) if that ever drifts.
|
|
34
|
+
*
|
|
35
|
+
* Second design note: the plan text reads "a brand token OR an
|
|
36
|
+
* authority-claiming phrase implying a specific vendor." An authority-
|
|
37
|
+
* claiming affix ALONE ("the official installer") never names a specific
|
|
38
|
+
* vendor and so cannot resolve a canonical domain to compare against — this
|
|
39
|
+
* detector therefore requires a brand-token match to identify "the claimed
|
|
40
|
+
* vendor" at all (necessary for the domain comparison to mean anything), and
|
|
41
|
+
* treats a co-occurring authority-claiming affix as a confidence booster
|
|
42
|
+
* (medium -> high), not an alternate, independently-sufficient trigger. This
|
|
43
|
+
* mirrors typosquat.ts's own rule 3 (`hasBrandToken && hasAuthorityAffix`),
|
|
44
|
+
* which also requires both signals together.
|
|
45
|
+
*
|
|
46
|
+
* Two fixes from adversarial review (2026-08-16 — see
|
|
47
|
+
* docs/internal/code_review/2026-08-15-smi6033-wave4-escalation-model.md
|
|
48
|
+
* for the full account):
|
|
49
|
+
*
|
|
50
|
+
* 1. Fetch-target correlation. The original version treated ANY URL on a
|
|
51
|
+
* line that ALSO matched the generic FETCH_COMMAND_PATTERN as the fetch
|
|
52
|
+
* target — including a URL that was merely mentioned in prose alongside
|
|
53
|
+
* an unrelated fetch-verb usage on the same line (e.g.
|
|
54
|
+
* "curl --version; see mirror documentation at <url>", where curl is
|
|
55
|
+
* checking its own version, not fetching that URL). Replaced with
|
|
56
|
+
* `isActualFetchTarget`, which requires the fetch verb to be
|
|
57
|
+
* IMMEDIATELY followed (only flag-like tokens and whitespace, no
|
|
58
|
+
* command separator, no prose) by the URL — i.e. the URL must actually
|
|
59
|
+
* be the verb's argument.
|
|
60
|
+
*
|
|
61
|
+
* 2. Authority-affix proximity. `hasAuthorityAffix` used to scan the ENTIRE
|
|
62
|
+
* ±5-line window independently of where the brand token itself was
|
|
63
|
+
* found, so an unrelated authority phrase elsewhere in the window (e.g.
|
|
64
|
+
* "for official documentation on Python packaging, see PEP 517") could
|
|
65
|
+
* wrongly boost confidence to 'high' for a brand claim it has nothing to
|
|
66
|
+
* do with. Now scoped to a tight window around the brand token's OWN
|
|
67
|
+
* line (`DECOY_AUTHORITY_AFFIX_PROXIMITY_LINES`).
|
|
68
|
+
*/
|
|
69
|
+
import { analyzeMarkdownContext, isDocumentationContext } from './SecurityScanner.helpers.js';
|
|
70
|
+
import { extractUrls } from './SecurityScanner.urls.js';
|
|
71
|
+
import { DEFAULT_ALLOWED_DOMAINS } from './patterns.js';
|
|
72
|
+
import { BRAND_ALIASES, AUTHORITY_CLAIMING_AFFIXES } from './typosquat.js';
|
|
73
|
+
/** Bounded prose window (±N lines) around a fetch/exec instruction, per the plan's Gap 6 text. */
|
|
74
|
+
const DECOY_WINDOW_LINES = 5;
|
|
75
|
+
/**
|
|
76
|
+
* How close an authority-claiming affix ("official", "verified", ...) must
|
|
77
|
+
* be to the brand token's OWN line for it to count as a confidence booster.
|
|
78
|
+
* Same-line-only (adversarial-review fix, 2026-08-16) — the affix and the
|
|
79
|
+
* brand claim must plausibly be part of the SAME sentence/claim, not merely
|
|
80
|
+
* co-located somewhere in the wider fetch-correlation window OR on an
|
|
81
|
+
* adjacent line carrying an unrelated sentence (a real adversarial-review
|
|
82
|
+
* example: "For official documentation on Python packaging, see PEP 517."
|
|
83
|
+
* on the line right after a genuine, affix-free "Claude API" brand mention —
|
|
84
|
+
* a ±1-line window still wrongly pairs the two).
|
|
85
|
+
*/
|
|
86
|
+
const DECOY_AUTHORITY_AFFIX_PROXIMITY_LINES = 0;
|
|
87
|
+
const FETCH_VERBS = new Set(['curl', 'wget', 'npx']);
|
|
88
|
+
/** A bare flag token, e.g. `-o`, `-fsSL`, `--output`, `--data=x`. */
|
|
89
|
+
const FLAG_TOKEN = /^-{1,2}[A-Za-z][\w-]*$/;
|
|
90
|
+
/**
|
|
91
|
+
* Common curl/wget/npx flags that take a SEPARATE value token (e.g.
|
|
92
|
+
* `-o setup.sh`, `-X POST`) — that value token must be consumed as part of
|
|
93
|
+
* the flag, not mistaken for prose.
|
|
94
|
+
*/
|
|
95
|
+
const VALUE_TAKING_FLAGS = new Set([
|
|
96
|
+
'-o',
|
|
97
|
+
'-x',
|
|
98
|
+
'-h',
|
|
99
|
+
'-d',
|
|
100
|
+
'-a',
|
|
101
|
+
'-e',
|
|
102
|
+
'-u',
|
|
103
|
+
'-b',
|
|
104
|
+
'--output',
|
|
105
|
+
'--request',
|
|
106
|
+
'--header',
|
|
107
|
+
'--data',
|
|
108
|
+
'--data-raw',
|
|
109
|
+
'--data-binary',
|
|
110
|
+
'--data-urlencode',
|
|
111
|
+
'--user-agent',
|
|
112
|
+
'--referer',
|
|
113
|
+
'--proxy',
|
|
114
|
+
'--cookie',
|
|
115
|
+
]);
|
|
116
|
+
/**
|
|
117
|
+
* True when `url` (verbatim substring of `lineContent`) is actually the
|
|
118
|
+
* argument to a fetch verb (curl/wget/npx, or `git clone`) on that line —
|
|
119
|
+
* not merely co-located with one. Tokenizes the prefix before the URL:
|
|
120
|
+
* rejects outright on any command separator (`;`, `|`, `&`), requires the
|
|
121
|
+
* prefix to start with the fetch verb, and requires every remaining token to
|
|
122
|
+
* be either a flag or the value argument of a value-taking flag (e.g.
|
|
123
|
+
* `curl -o setup.sh <url>`) — any other token (prose) fails the match.
|
|
124
|
+
* Rejects "curl --version; see mirror documentation at <url>" (the URL is
|
|
125
|
+
* not curl's argument, and the `;` alone already disqualifies it); accepts
|
|
126
|
+
* "curl -fsSL <url>", "curl -o setup.sh <url>", "wget <url>",
|
|
127
|
+
* "git clone <url>", "npx --yes <url>".
|
|
128
|
+
*/
|
|
129
|
+
function isActualFetchTarget(lineContent, url) {
|
|
130
|
+
const urlIndex = lineContent.indexOf(url);
|
|
131
|
+
if (urlIndex < 0)
|
|
132
|
+
return false;
|
|
133
|
+
const prefix = lineContent.slice(0, urlIndex);
|
|
134
|
+
if (/[;|&]/.test(prefix))
|
|
135
|
+
return false;
|
|
136
|
+
const tokens = prefix
|
|
137
|
+
.trim()
|
|
138
|
+
.split(/\s+/)
|
|
139
|
+
.filter((t) => t.length > 0);
|
|
140
|
+
if (tokens.length === 0)
|
|
141
|
+
return false;
|
|
142
|
+
let i;
|
|
143
|
+
if (tokens[0]?.toLowerCase() === 'git' && tokens[1]?.toLowerCase() === 'clone') {
|
|
144
|
+
i = 2;
|
|
145
|
+
}
|
|
146
|
+
else if (FETCH_VERBS.has(tokens[0]?.toLowerCase() ?? '')) {
|
|
147
|
+
i = 1;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
for (; i < tokens.length; i++) {
|
|
153
|
+
const token = tokens[i];
|
|
154
|
+
if (!FLAG_TOKEN.test(token))
|
|
155
|
+
return false;
|
|
156
|
+
if (VALUE_TAKING_FLAGS.has(token.toLowerCase()) && i + 1 < tokens.length) {
|
|
157
|
+
i++; // consume the flag's own value token too
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Curated canonical domain(s) per `BRAND_ALIASES` brand token — see this
|
|
164
|
+
* file's own header for why this cannot simply reuse `BRAND_ALIASES`'
|
|
165
|
+
* values directly (those are GitHub owner slugs, not DNS domains).
|
|
166
|
+
*/
|
|
167
|
+
const BRAND_CANONICAL_DOMAINS = {
|
|
168
|
+
anthropic: ['anthropic.com', 'claude.ai'],
|
|
169
|
+
claude: ['anthropic.com', 'claude.ai'],
|
|
170
|
+
gemini: ['google.com', 'ai.google.dev', 'deepmind.google'],
|
|
171
|
+
copilot: ['github.com', 'microsoft.com'],
|
|
172
|
+
vercel: ['vercel.com'],
|
|
173
|
+
salesforce: ['salesforce.com'],
|
|
174
|
+
};
|
|
175
|
+
/** Split a line into lowercase alphanumeric tokens on any separator (mirrors typosquat.ts's own `tokenize`). */
|
|
176
|
+
function tokenize(raw) {
|
|
177
|
+
return raw
|
|
178
|
+
.toLowerCase()
|
|
179
|
+
.split(/[^a-z0-9]+/i)
|
|
180
|
+
.filter((t) => t.length > 0);
|
|
181
|
+
}
|
|
182
|
+
/** Exact-or-subdomain match against a domain set (mirrors `isAllowedDomain`'s own logic in SecurityScanner.ts). */
|
|
183
|
+
function matchesDomainSet(hostname, domains) {
|
|
184
|
+
return domains.some((d) => hostname === d || hostname.endsWith('.' + d));
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Scan a bounded ±`DECOY_WINDOW_LINES` window around `lineIndex` for a brand
|
|
188
|
+
* token (required). Returns `null` when no brand token is present — nothing
|
|
189
|
+
* to compare a fetch target's domain against. `hasAuthorityAffix` is then
|
|
190
|
+
* resolved separately, scoped tightly to the brand token's OWN line (see
|
|
191
|
+
* `DECOY_AUTHORITY_AFFIX_PROXIMITY_LINES` and this file's header note on the
|
|
192
|
+
* adversarial-review fix) — NOT the wider fetch-correlation window, so an
|
|
193
|
+
* authority phrase unrelated to this specific brand claim can't wrongly
|
|
194
|
+
* boost confidence.
|
|
195
|
+
*/
|
|
196
|
+
function findVendorClaimInWindow(lines, lineIndex) {
|
|
197
|
+
const start = Math.max(0, lineIndex - DECOY_WINDOW_LINES);
|
|
198
|
+
const end = Math.min(lines.length - 1, lineIndex + DECOY_WINDOW_LINES);
|
|
199
|
+
let brandToken = null;
|
|
200
|
+
let brandLineIndex = -1;
|
|
201
|
+
for (let i = start; i <= end && !brandToken; i++) {
|
|
202
|
+
for (const token of tokenize(lines[i] ?? '')) {
|
|
203
|
+
if (Object.prototype.hasOwnProperty.call(BRAND_ALIASES, token)) {
|
|
204
|
+
brandToken = token;
|
|
205
|
+
brandLineIndex = i;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (!brandToken)
|
|
211
|
+
return null;
|
|
212
|
+
const affixStart = Math.max(0, brandLineIndex - DECOY_AUTHORITY_AFFIX_PROXIMITY_LINES);
|
|
213
|
+
const affixEnd = Math.min(lines.length - 1, brandLineIndex + DECOY_AUTHORITY_AFFIX_PROXIMITY_LINES);
|
|
214
|
+
const hasAuthorityAffix = tokenize(lines.slice(affixStart, affixEnd + 1).join(' ')).some((t) => AUTHORITY_CLAIMING_AFFIXES.has(t));
|
|
215
|
+
return { brandToken, hasAuthorityAffix };
|
|
216
|
+
}
|
|
217
|
+
export function scanDecoyMisdirection(content, lineContexts) {
|
|
218
|
+
const findings = [];
|
|
219
|
+
const lines = content.split('\n');
|
|
220
|
+
const contexts = lineContexts ?? analyzeMarkdownContext(content);
|
|
221
|
+
const urls = extractUrls(content);
|
|
222
|
+
for (const { url, line } of urls) {
|
|
223
|
+
const lineIndex = line - 1;
|
|
224
|
+
const lineContent = lines[lineIndex] ?? '';
|
|
225
|
+
// Only a URL that is ACTUALLY the argument to a fetch verb is in scope —
|
|
226
|
+
// a bare linked URL ("see https://docs.example.com") is not, and neither
|
|
227
|
+
// is a URL merely co-located on a line with an unrelated fetch-verb
|
|
228
|
+
// usage (adversarial-review fix, 2026-08-16 — see isActualFetchTarget's
|
|
229
|
+
// own doc comment).
|
|
230
|
+
if (!isActualFetchTarget(lineContent, url))
|
|
231
|
+
continue;
|
|
232
|
+
let hostname;
|
|
233
|
+
try {
|
|
234
|
+
hostname = new URL(url).hostname.toLowerCase();
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const claim = findVendorClaimInWindow(lines, lineIndex);
|
|
240
|
+
if (!claim)
|
|
241
|
+
continue; // no vendor claim nearby -> nothing to compare against; not a general brand-mention detector
|
|
242
|
+
const canonicalDomains = BRAND_CANONICAL_DOMAINS[claim.brandToken] ?? [];
|
|
243
|
+
if (matchesDomainSet(hostname, canonicalDomains))
|
|
244
|
+
continue; // fetch target IS the claimed vendor's own domain
|
|
245
|
+
if (matchesDomainSet(hostname, DEFAULT_ALLOWED_DOMAINS))
|
|
246
|
+
continue; // generally-trusted host, excluded per the plan text
|
|
247
|
+
const ctx = contexts[lineIndex];
|
|
248
|
+
const inDocContext = ctx ? isDocumentationContext(ctx) : false;
|
|
249
|
+
findings.push({
|
|
250
|
+
type: 'decoy_misdirection',
|
|
251
|
+
// Never high/critical — an approximate NL heuristic by construction
|
|
252
|
+
// (plan §9 reconciliation table); the escalation-into-code_execution
|
|
253
|
+
// co-signal mechanism lives in a separate dispatch.
|
|
254
|
+
severity: inDocContext ? 'low' : 'medium',
|
|
255
|
+
message: `Fetch target domain ("${hostname}") does not match the "${claim.brandToken}" vendor claimed nearby in the skill's own prose: ${url}`,
|
|
256
|
+
location: lineContent.trim().slice(0, 100),
|
|
257
|
+
lineNumber: line,
|
|
258
|
+
category: 'decoy_misdirection',
|
|
259
|
+
inDocumentationContext: inDocContext,
|
|
260
|
+
confidence: inDocContext ? 'low' : claim.hasAuthorityAffix ? 'high' : 'medium',
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return findings;
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=SecurityScanner.decoy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SecurityScanner.decoy.js","sourceRoot":"","sources":["../../../../src/security/scanner/SecurityScanner.decoy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Scanner — encoded (base64) payload detect-decode-recursively-rescan
|
|
3
|
+
* @module @skillsmith/core/security/scanner/SecurityScanner.encoding
|
|
4
|
+
*
|
|
5
|
+
* SMI-6033 Wave 2 (Gap 2): before this file, `base64 -d | sh` (the DECODE
|
|
6
|
+
* INVOCATION syntax) was already caught by CODE_EXECUTION_PATTERNS, but an
|
|
7
|
+
* inert base64 blob the agent is merely instructed to decode and run itself
|
|
8
|
+
* scored zero.
|
|
9
|
+
*
|
|
10
|
+
* Rather than a parallel "this looks suspicious" heuristic (high FP risk —
|
|
11
|
+
* base64 blobs are extremely common in legitimate skills: image data URIs,
|
|
12
|
+
* git hashes, JWTs), this detector DECODES the candidate and recursively runs
|
|
13
|
+
* the caller's full detector suite against the decoded text, reusing the
|
|
14
|
+
* whole pattern arsenal instead of duplicating it. The escalation this gap
|
|
15
|
+
* achieves is free: a decoded `curl|bash` natively trips `code_execution` at
|
|
16
|
+
* ITS OWN severity, exactly as if the attacker had shipped it undecoded.
|
|
17
|
+
*
|
|
18
|
+
* Depth-1-only, STRUCTURALLY (not by convention). This module never scans
|
|
19
|
+
* itself — `rescan`, the callback the caller supplies, is expected to be the
|
|
20
|
+
* caller's OWN "run every detector" entry point with the encoded-payload
|
|
21
|
+
* detector specifically disabled for that inner call (see
|
|
22
|
+
* `SecurityScanner.ts`'s `runDetectors(content, lineContexts,
|
|
23
|
+
* skipEncodedPayload)`, where the recursive callback always passes `true`).
|
|
24
|
+
* The inner call therefore cannot reach this detector again no matter what
|
|
25
|
+
* the decoded content contains — a base64 blob discovered INSIDE decoded
|
|
26
|
+
* content is never itself decoded.
|
|
27
|
+
*
|
|
28
|
+
* Candidate detection: a contiguous base64-alphabet run,
|
|
29
|
+
* `[A-Za-z0-9+/]{120,}={0,2}`. Deliberately EXCLUDES `-`/`_` (base64url), so
|
|
30
|
+
* a JWT never becomes a candidate at all — that is the intentional mechanism
|
|
31
|
+
* keeping JWTs out, not a separate check. Bounded/ReDoS-safe: a single
|
|
32
|
+
* character-class quantifier has no catastrophic-backtracking surface, so
|
|
33
|
+
* this intentionally does NOT route through `safeRegexTest`'s 10,000-char
|
|
34
|
+
* truncation — a legitimate base64 blob routinely runs past that on one long
|
|
35
|
+
* line, and truncating it would silently defeat detection.
|
|
36
|
+
*
|
|
37
|
+
* The wrapper finding (`encoded_payload`) is advisory-tier ONLY (weight 1.2 /
|
|
38
|
+
* coefficient 0.04 — the sensitive_path/typosquat tier, NOT the 2.0/0.40 tier
|
|
39
|
+
* the other three Wave 2 detectors use, see weights.ts): this detector is
|
|
40
|
+
* pure observability/provenance-marking, not itself a strong signal. Every
|
|
41
|
+
* finding folded in from the decoded content carries a NEW `decodedFrom`
|
|
42
|
+
* field (types.ts) set to the OUTER document line the blob was found on —
|
|
43
|
+
* the same provenance-marker role `filePath` already plays for a
|
|
44
|
+
* sibling-file finding, just for a decoded-blob origin instead of a
|
|
45
|
+
* different file.
|
|
46
|
+
*/
|
|
47
|
+
import type { SecurityFinding } from './types.js';
|
|
48
|
+
import type { LineContext } from './SecurityScanner.helpers.js';
|
|
49
|
+
/**
|
|
50
|
+
* Callback the caller (`SecurityScanner`) supplies to run its OWN full
|
|
51
|
+
* detector suite against decoded text, with the encoded-payload detector
|
|
52
|
+
* itself disabled — the structural depth-1 guarantee (see module header).
|
|
53
|
+
*/
|
|
54
|
+
export type EncodedPayloadRescanner = (decodedContent: string) => SecurityFinding[];
|
|
55
|
+
export declare function scanEncodedPayload(content: string, lineContexts: LineContext[] | undefined, rescan: EncodedPayloadRescanner): SecurityFinding[];
|
|
56
|
+
//# sourceMappingURL=SecurityScanner.encoding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SecurityScanner.encoding.d.ts","sourceRoot":"","sources":["../../../../src/security/scanner/SecurityScanner.encoding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAA;AA2I/D;;;;GAIG;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,cAAc,EAAE,MAAM,KAAK,eAAe,EAAE,CAAA;AAEnF,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,WAAW,EAAE,GAAG,SAAS,EACvC,MAAM,EAAE,uBAAuB,GAC9B,eAAe,EAAE,CAsGnB"}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Scanner — encoded (base64) payload detect-decode-recursively-rescan
|
|
3
|
+
* @module @skillsmith/core/security/scanner/SecurityScanner.encoding
|
|
4
|
+
*
|
|
5
|
+
* SMI-6033 Wave 2 (Gap 2): before this file, `base64 -d | sh` (the DECODE
|
|
6
|
+
* INVOCATION syntax) was already caught by CODE_EXECUTION_PATTERNS, but an
|
|
7
|
+
* inert base64 blob the agent is merely instructed to decode and run itself
|
|
8
|
+
* scored zero.
|
|
9
|
+
*
|
|
10
|
+
* Rather than a parallel "this looks suspicious" heuristic (high FP risk —
|
|
11
|
+
* base64 blobs are extremely common in legitimate skills: image data URIs,
|
|
12
|
+
* git hashes, JWTs), this detector DECODES the candidate and recursively runs
|
|
13
|
+
* the caller's full detector suite against the decoded text, reusing the
|
|
14
|
+
* whole pattern arsenal instead of duplicating it. The escalation this gap
|
|
15
|
+
* achieves is free: a decoded `curl|bash` natively trips `code_execution` at
|
|
16
|
+
* ITS OWN severity, exactly as if the attacker had shipped it undecoded.
|
|
17
|
+
*
|
|
18
|
+
* Depth-1-only, STRUCTURALLY (not by convention). This module never scans
|
|
19
|
+
* itself — `rescan`, the callback the caller supplies, is expected to be the
|
|
20
|
+
* caller's OWN "run every detector" entry point with the encoded-payload
|
|
21
|
+
* detector specifically disabled for that inner call (see
|
|
22
|
+
* `SecurityScanner.ts`'s `runDetectors(content, lineContexts,
|
|
23
|
+
* skipEncodedPayload)`, where the recursive callback always passes `true`).
|
|
24
|
+
* The inner call therefore cannot reach this detector again no matter what
|
|
25
|
+
* the decoded content contains — a base64 blob discovered INSIDE decoded
|
|
26
|
+
* content is never itself decoded.
|
|
27
|
+
*
|
|
28
|
+
* Candidate detection: a contiguous base64-alphabet run,
|
|
29
|
+
* `[A-Za-z0-9+/]{120,}={0,2}`. Deliberately EXCLUDES `-`/`_` (base64url), so
|
|
30
|
+
* a JWT never becomes a candidate at all — that is the intentional mechanism
|
|
31
|
+
* keeping JWTs out, not a separate check. Bounded/ReDoS-safe: a single
|
|
32
|
+
* character-class quantifier has no catastrophic-backtracking surface, so
|
|
33
|
+
* this intentionally does NOT route through `safeRegexTest`'s 10,000-char
|
|
34
|
+
* truncation — a legitimate base64 blob routinely runs past that on one long
|
|
35
|
+
* line, and truncating it would silently defeat detection.
|
|
36
|
+
*
|
|
37
|
+
* The wrapper finding (`encoded_payload`) is advisory-tier ONLY (weight 1.2 /
|
|
38
|
+
* coefficient 0.04 — the sensitive_path/typosquat tier, NOT the 2.0/0.40 tier
|
|
39
|
+
* the other three Wave 2 detectors use, see weights.ts): this detector is
|
|
40
|
+
* pure observability/provenance-marking, not itself a strong signal. Every
|
|
41
|
+
* finding folded in from the decoded content carries a NEW `decodedFrom`
|
|
42
|
+
* field (types.ts) set to the OUTER document line the blob was found on —
|
|
43
|
+
* the same provenance-marker role `filePath` already plays for a
|
|
44
|
+
* sibling-file finding, just for a decoded-blob origin instead of a
|
|
45
|
+
* different file.
|
|
46
|
+
*/
|
|
47
|
+
import { analyzeMarkdownContext, isDocumentationContext, isWithinInlineCode, } from './SecurityScanner.helpers.js';
|
|
48
|
+
/**
|
|
49
|
+
* Per-candidate size cap. Mirrors `MAX_SIBLING_CONTENT_BYTES`'s rationale
|
|
50
|
+
* (`scripts/indexer/skill-processor.security.ts`, 256_000) — bound the work a
|
|
51
|
+
* single decode-and-recursively-rescan pass can do so an attacker can't
|
|
52
|
+
* weaponize an oversized base64 blob into a decode/rescan cost blowup. Same
|
|
53
|
+
* order of magnitude; not required to match exactly.
|
|
54
|
+
*
|
|
55
|
+
* SMI-6033 Wave 3 (adversarial-review fix): exceeding this cap used to
|
|
56
|
+
* `continue` with ZERO trace — no decode, no finding, nothing — which handed
|
|
57
|
+
* an attacker a one-line bypass of the entire detector: pad the malicious
|
|
58
|
+
* blob past 200 KB with base64-valid filler and it becomes invisible. The cap
|
|
59
|
+
* itself is a deliberate, correct resource bound and is UNCHANGED (an
|
|
60
|
+
* oversized candidate is still never decoded or rescanned — decoding it is
|
|
61
|
+
* exactly what the cap exists to prevent); what changed is that its
|
|
62
|
+
* existence is now SURFACED as a low/low advisory finding instead of being
|
|
63
|
+
* silently dropped. Same "caps surfaced, not silent" principle as Gap 8's
|
|
64
|
+
* `scan_coverage_incomplete` flag.
|
|
65
|
+
*/
|
|
66
|
+
const MAX_ENCODED_CANDIDATE_BYTES = 200_000;
|
|
67
|
+
/**
|
|
68
|
+
* Ceiling on how many oversized-candidate advisories one document can emit
|
|
69
|
+
* (SMI-6033 Wave 3). Deliberately a SEPARATE counter from
|
|
70
|
+
* `MAX_BASE64_CANDIDATES` below, not a share of that budget: an oversized
|
|
71
|
+
* candidate costs no decode work, so charging it to the decode budget would
|
|
72
|
+
* let an attacker spend the real budget on cheap oversized filler and
|
|
73
|
+
* suppress decoding of the genuine candidates that follow. Beyond this
|
|
74
|
+
* ceiling further oversized candidates are dropped, but — unlike before the
|
|
75
|
+
* fix — the pattern is no longer invisible: the first `MAX_OVERSIZED_ADVISORIES`
|
|
76
|
+
* already surface it. Reset fresh on every `scanEncodedPayload` call.
|
|
77
|
+
*/
|
|
78
|
+
const MAX_OVERSIZED_ADVISORIES = 8;
|
|
79
|
+
/**
|
|
80
|
+
* Document-wide candidate-COUNT cap (SMI-6033 Gap 2 resource-bound
|
|
81
|
+
* follow-up). Without a ceiling here, an attacker could pad a skill with
|
|
82
|
+
* dozens/hundreds of individually-qualifying base64 blobs to force many
|
|
83
|
+
* expensive decode-and-recursively-rescan passes — a cost/availability
|
|
84
|
+
* concern, not a false-positive concern (each candidate under
|
|
85
|
+
* `MAX_ENCODED_CANDIDATE_BYTES` is still legitimate-shaped on its own).
|
|
86
|
+
* Checked in REGEX-MATCH order, before the decode attempt itself, so the
|
|
87
|
+
* bound applies even to attempts that end up failing (invalid base64,
|
|
88
|
+
* binary noise) — not just to successful decode-and-rescan passes. Once 8
|
|
89
|
+
* candidates in one document have been attempted, every further candidate is
|
|
90
|
+
* silently skipped: no decode attempt, no finding — the same "not processed
|
|
91
|
+
* is silent" convention `tryDecodeBase64ToPlausibleText`'s own failure path
|
|
92
|
+
* already follows. Reset fresh on every `scanEncodedPayload` call; never
|
|
93
|
+
* persisted across calls or across a scan session.
|
|
94
|
+
*/
|
|
95
|
+
const MAX_BASE64_CANDIDATES = 8;
|
|
96
|
+
/**
|
|
97
|
+
* Aggregate DECODED-byte cap across ALL candidates in one document (SMI-6033
|
|
98
|
+
* Gap 2 resource-bound follow-up). Mirrors `MAX_SIBLING_CONTENT_BYTES`'s
|
|
99
|
+
* 256_000 convention (`scripts/indexer/skill-processor.security.ts`) — this
|
|
100
|
+
* detector's document-wide analogue of that per-fetch byte budget. IN
|
|
101
|
+
* ADDITION TO the per-candidate `MAX_ENCODED_CANDIDATE_BYTES` cap above: that
|
|
102
|
+
* one bounds a single blob's ENCODED size; this one bounds the running total
|
|
103
|
+
* of DECODED bytes across every candidate successfully decoded so far in the
|
|
104
|
+
* same document scan, so a run of many just-under-the-per-candidate-cap
|
|
105
|
+
* blobs can't sum to an unbounded decode/rescan cost. Once exceeded, every
|
|
106
|
+
* remaining candidate in the document is skipped — including the one whose
|
|
107
|
+
* own decode pushed the running total over the limit — same silent-skip
|
|
108
|
+
* convention as `MAX_BASE64_CANDIDATES` above. Reset fresh on every
|
|
109
|
+
* `scanEncodedPayload` call; never persisted across calls or across a scan
|
|
110
|
+
* session.
|
|
111
|
+
*/
|
|
112
|
+
const MAX_DECODED_TOTAL_BYTES = 256_000;
|
|
113
|
+
/** Contiguous base64-alphabet run, >=120 chars, optional 0-2 padding chars. */
|
|
114
|
+
const BASE64_CANDIDATE = /[A-Za-z0-9+/]{120,}={0,2}/g;
|
|
115
|
+
/**
|
|
116
|
+
* How far back (chars) to look, from a candidate's start, for a
|
|
117
|
+
* `data:image/`, `data:font/`, or `data:audio/` prefix. A real data URI's
|
|
118
|
+
* MIME type + `;base64,` separator is well within this window
|
|
119
|
+
* (`data:image/svg+xml;base64,` is 28 chars).
|
|
120
|
+
*/
|
|
121
|
+
const DATA_URI_LOOKBACK = 60;
|
|
122
|
+
const DATA_URI_PREFIX = /data:(?:image|font|audio)\//i;
|
|
123
|
+
/** Non-printable / control characters (excluding \t\r\n) — the printable-ratio gate. */
|
|
124
|
+
// eslint-disable-next-line no-control-regex -- Intentional: detecting binary/control-byte noise in decoded content.
|
|
125
|
+
const CONTROL_CHAR = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/;
|
|
126
|
+
/** Minimum fraction of non-control characters required to treat decoded bytes as plausible text. */
|
|
127
|
+
const MIN_PRINTABLE_RATIO = 0.9;
|
|
128
|
+
/**
|
|
129
|
+
* Attempt exactly one base64 decode of `candidate`. Returns the decoded text
|
|
130
|
+
* when it is valid UTF-8 with a plausible-text printable-character ratio;
|
|
131
|
+
* `null` on ANY failure (invalid base64 syntax, decoded bytes that are not
|
|
132
|
+
* valid UTF-8, or valid UTF-8 that is mostly control/binary noise) — decode
|
|
133
|
+
* failure is deliberately not itself a finding.
|
|
134
|
+
*/
|
|
135
|
+
function tryDecodeBase64ToPlausibleText(candidate) {
|
|
136
|
+
let binary;
|
|
137
|
+
try {
|
|
138
|
+
// atob (Web/Deno/Node-global — no Buffer dependency, keeps this logic
|
|
139
|
+
// parallel to the edge/Deno twin) throws on invalid base64 syntax.
|
|
140
|
+
binary = atob(candidate);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
if (binary.length === 0)
|
|
146
|
+
return null;
|
|
147
|
+
// Round-trip check: atob is lenient about some malformed/non-canonical
|
|
148
|
+
// input (e.g. missing padding) — re-encoding and comparing (modulo
|
|
149
|
+
// padding) rejects a decode atob accepted but that doesn't reproduce the
|
|
150
|
+
// original candidate, rather than silently scanning a garbled subset.
|
|
151
|
+
if (btoa(binary).replace(/=+$/, '') !== candidate.replace(/=+$/, ''))
|
|
152
|
+
return null;
|
|
153
|
+
const bytes = new Uint8Array(binary.length);
|
|
154
|
+
for (let i = 0; i < binary.length; i++)
|
|
155
|
+
bytes[i] = binary.charCodeAt(i);
|
|
156
|
+
let text;
|
|
157
|
+
try {
|
|
158
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return null; // not valid UTF-8 -> binary blob, not a text payload
|
|
162
|
+
}
|
|
163
|
+
if (text.length === 0)
|
|
164
|
+
return null;
|
|
165
|
+
let controlCount = 0;
|
|
166
|
+
for (const ch of text) {
|
|
167
|
+
if (CONTROL_CHAR.test(ch))
|
|
168
|
+
controlCount++;
|
|
169
|
+
}
|
|
170
|
+
const printableRatio = (text.length - controlCount) / text.length;
|
|
171
|
+
if (printableRatio < MIN_PRINTABLE_RATIO)
|
|
172
|
+
return null;
|
|
173
|
+
return text;
|
|
174
|
+
}
|
|
175
|
+
export function scanEncodedPayload(content, lineContexts, rescan) {
|
|
176
|
+
const findings = [];
|
|
177
|
+
const lines = content.split('\n');
|
|
178
|
+
const contexts = lineContexts ?? analyzeMarkdownContext(content);
|
|
179
|
+
// Per-document resource bounds (see MAX_BASE64_CANDIDATES /
|
|
180
|
+
// MAX_DECODED_TOTAL_BYTES doc comments above) — fresh per call, never
|
|
181
|
+
// shared across calls or across a scan session.
|
|
182
|
+
let candidatesProcessed = 0;
|
|
183
|
+
let decodedTotalBytes = 0;
|
|
184
|
+
let aggregateBudgetExhausted = false;
|
|
185
|
+
let oversizedAdvisories = 0;
|
|
186
|
+
lines.forEach((line, index) => {
|
|
187
|
+
const lineNumber = index + 1;
|
|
188
|
+
for (const match of line.matchAll(BASE64_CANDIDATE)) {
|
|
189
|
+
const candidate = match[0];
|
|
190
|
+
const start = match.index ?? 0;
|
|
191
|
+
// Data-URI exclusion runs FIRST (SMI-6033 Wave 3 — it used to run after
|
|
192
|
+
// the size check). An embedded image/font/audio data URI routinely runs
|
|
193
|
+
// past MAX_ENCODED_CANDIDATE_BYTES, and it is a known-benign shape, so
|
|
194
|
+
// it must be excluded BEFORE the oversized advisory below can fire on
|
|
195
|
+
// it. Behaviorally identical for every candidate under the cap.
|
|
196
|
+
const before = line.slice(Math.max(0, start - DATA_URI_LOOKBACK), start);
|
|
197
|
+
if (DATA_URI_PREFIX.test(before))
|
|
198
|
+
continue;
|
|
199
|
+
// Oversized candidate: never decoded/rescanned (that is precisely what
|
|
200
|
+
// the cap buys), but no longer silently dropped either — see
|
|
201
|
+
// MAX_ENCODED_CANDIDATE_BYTES's doc comment for the evasion this
|
|
202
|
+
// closes. Emitted at low/low: it is a coverage caveat, not evidence.
|
|
203
|
+
if (candidate.length > MAX_ENCODED_CANDIDATE_BYTES) {
|
|
204
|
+
if (oversizedAdvisories >= MAX_OVERSIZED_ADVISORIES)
|
|
205
|
+
continue;
|
|
206
|
+
oversizedAdvisories++;
|
|
207
|
+
const oversizeCtx = contexts[index];
|
|
208
|
+
const oversizeInInlineCode = oversizeCtx?.isInlineCode && isWithinInlineCode(line, start);
|
|
209
|
+
const oversizeInDocContext = oversizeCtx
|
|
210
|
+
? isDocumentationContext(oversizeCtx) || oversizeInInlineCode
|
|
211
|
+
: false;
|
|
212
|
+
findings.push({
|
|
213
|
+
type: 'encoded_payload',
|
|
214
|
+
severity: 'low',
|
|
215
|
+
message: `Base64-encoded payload exceeds the ${MAX_ENCODED_CANDIDATE_BYTES}-byte per-candidate cap — oversized candidate, not decoded/rescanned (${candidate.length} chars)`,
|
|
216
|
+
location: line.trim().slice(0, 100),
|
|
217
|
+
lineNumber,
|
|
218
|
+
category: 'encoded_payload',
|
|
219
|
+
inDocumentationContext: oversizeInDocContext,
|
|
220
|
+
confidence: 'low',
|
|
221
|
+
});
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
// Document-wide candidate-count cap — checked before the decode
|
|
225
|
+
// attempt itself so the bound applies even to attempts that end up
|
|
226
|
+
// failing, not just successful ones. Once the aggregate decoded-byte
|
|
227
|
+
// budget below is exhausted, every remaining candidate is skipped too.
|
|
228
|
+
if (candidatesProcessed >= MAX_BASE64_CANDIDATES || aggregateBudgetExhausted)
|
|
229
|
+
continue;
|
|
230
|
+
candidatesProcessed++;
|
|
231
|
+
const decoded = tryDecodeBase64ToPlausibleText(candidate);
|
|
232
|
+
if (decoded === null)
|
|
233
|
+
continue; // decode failure / binary noise -> not itself a finding
|
|
234
|
+
// Aggregate decoded-byte cap: if THIS candidate would push the
|
|
235
|
+
// document-wide running total over MAX_DECODED_TOTAL_BYTES, skip it
|
|
236
|
+
// (and, via aggregateBudgetExhausted, every candidate after it) rather
|
|
237
|
+
// than folding in its finding/rescan results.
|
|
238
|
+
const decodedByteLength = new TextEncoder().encode(decoded).length;
|
|
239
|
+
if (decodedTotalBytes + decodedByteLength > MAX_DECODED_TOTAL_BYTES) {
|
|
240
|
+
aggregateBudgetExhausted = true;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
decodedTotalBytes += decodedByteLength;
|
|
244
|
+
const ctx = contexts[index];
|
|
245
|
+
const inInlineCode = ctx?.isInlineCode && isWithinInlineCode(line, start);
|
|
246
|
+
const inDocContext = ctx ? isDocumentationContext(ctx) || inInlineCode : false;
|
|
247
|
+
findings.push({
|
|
248
|
+
type: 'encoded_payload',
|
|
249
|
+
// Advisory-tier only (weights.ts: 1.2 / 0.04, the sensitive_path/
|
|
250
|
+
// typosquat tier) — see module header for why this is deliberately
|
|
251
|
+
// NOT the 2.0/0.40 tier the other three Wave 2 detectors use.
|
|
252
|
+
severity: inDocContext ? 'low' : 'medium',
|
|
253
|
+
message: `Base64-encoded payload decoded and rescanned (${candidate.length} chars)`,
|
|
254
|
+
location: line.trim().slice(0, 100),
|
|
255
|
+
lineNumber,
|
|
256
|
+
category: 'encoded_payload',
|
|
257
|
+
inDocumentationContext: inDocContext,
|
|
258
|
+
confidence: inDocContext ? 'low' : 'high',
|
|
259
|
+
});
|
|
260
|
+
// Fold in the decoded content's OWN findings, each tagged with
|
|
261
|
+
// decodedFrom so it stays traceable back to the outer blob that
|
|
262
|
+
// produced it (see module header — the filePath-provenance analogue).
|
|
263
|
+
for (const inner of rescan(decoded)) {
|
|
264
|
+
findings.push({ ...inner, decodedFrom: lineNumber });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
return findings;
|
|
269
|
+
}
|
|
270
|
+
//# sourceMappingURL=SecurityScanner.encoding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SecurityScanner.encoding.js","sourceRoot":"","sources":["../../../../src/security/scanner/SecurityScanner.encoding.ts"],"names":[],"mappings":"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"}
|