@skillsmith/core 0.5.2 → 0.5.4
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 +14 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/activation/ActivationManager.d.ts +7 -0
- package/dist/src/activation/ActivationManager.d.ts.map +1 -1
- package/dist/src/activation/ActivationManager.js +13 -4
- package/dist/src/activation/ActivationManager.js.map +1 -1
- package/dist/src/analysis/adapters/python.d.ts +16 -11
- package/dist/src/analysis/adapters/python.d.ts.map +1 -1
- package/dist/src/analysis/adapters/python.js +46 -61
- package/dist/src/analysis/adapters/python.js.map +1 -1
- package/dist/src/analysis/router.test.d.ts +2 -0
- package/dist/src/analysis/router.test.d.ts.map +1 -0
- package/dist/src/analysis/router.test.js +411 -0
- package/dist/src/analysis/router.test.js.map +1 -0
- package/dist/src/analysis/tree-sitter/manager.d.ts.map +1 -1
- package/dist/src/analysis/tree-sitter/manager.js +12 -5
- package/dist/src/analysis/tree-sitter/manager.js.map +1 -1
- package/dist/src/analysis/tree-sitter/pythonExtractor.d.ts +45 -0
- package/dist/src/analysis/tree-sitter/pythonExtractor.d.ts.map +1 -0
- package/dist/src/analysis/tree-sitter/pythonExtractor.js +264 -0
- package/dist/src/analysis/tree-sitter/pythonExtractor.js.map +1 -0
- package/dist/src/analysis/tree-sitter/pythonExtractor.test.d.ts +12 -0
- package/dist/src/analysis/tree-sitter/pythonExtractor.test.d.ts.map +1 -0
- package/dist/src/analysis/tree-sitter/pythonExtractor.test.js +74 -0
- package/dist/src/analysis/tree-sitter/pythonExtractor.test.js.map +1 -0
- package/dist/src/analysis/tree-sitter/pythonIncremental.d.ts +93 -0
- package/dist/src/analysis/tree-sitter/pythonIncremental.d.ts.map +1 -0
- package/dist/src/analysis/tree-sitter/pythonIncremental.hardening.test.d.ts +22 -0
- package/dist/src/analysis/tree-sitter/pythonIncremental.hardening.test.d.ts.map +1 -0
- package/dist/src/analysis/tree-sitter/pythonIncremental.hardening.test.js +229 -0
- package/dist/src/analysis/tree-sitter/pythonIncremental.hardening.test.js.map +1 -0
- package/dist/src/analysis/tree-sitter/pythonIncremental.js +287 -0
- package/dist/src/analysis/tree-sitter/pythonIncremental.js.map +1 -0
- package/dist/src/analysis/tree-sitter/pythonIncremental.test.d.ts +17 -0
- package/dist/src/analysis/tree-sitter/pythonIncremental.test.d.ts.map +1 -0
- package/dist/src/analysis/tree-sitter/pythonIncremental.test.js +142 -0
- package/dist/src/analysis/tree-sitter/pythonIncremental.test.js.map +1 -0
- package/dist/src/analysis/tree-sitter/queries/python.d.ts +43 -0
- package/dist/src/analysis/tree-sitter/queries/python.d.ts.map +1 -0
- package/dist/src/analysis/tree-sitter/queries/python.js +88 -0
- package/dist/src/analysis/tree-sitter/queries/python.js.map +1 -0
- package/dist/src/analysis/tree-sitter/queryExtractionMatchesOrExceedsRegex.test.d.ts +13 -0
- package/dist/src/analysis/tree-sitter/queryExtractionMatchesOrExceedsRegex.test.d.ts.map +1 -0
- package/dist/src/analysis/tree-sitter/queryExtractionMatchesOrExceedsRegex.test.js +174 -0
- package/dist/src/analysis/tree-sitter/queryExtractionMatchesOrExceedsRegex.test.js.map +1 -0
- package/dist/src/analytics/ROIDashboardService.csv.d.ts +11 -0
- package/dist/src/analytics/ROIDashboardService.csv.d.ts.map +1 -0
- package/dist/src/analytics/ROIDashboardService.csv.js +43 -0
- package/dist/src/analytics/ROIDashboardService.csv.js.map +1 -0
- package/dist/src/analytics/ROIDashboardService.d.ts +64 -3
- package/dist/src/analytics/ROIDashboardService.d.ts.map +1 -1
- package/dist/src/analytics/ROIDashboardService.js +116 -45
- package/dist/src/analytics/ROIDashboardService.js.map +1 -1
- package/dist/src/api/schemas.d.ts +19 -4
- package/dist/src/api/schemas.d.ts.map +1 -1
- package/dist/src/api/schemas.js +8 -0
- package/dist/src/api/schemas.js.map +1 -1
- package/dist/src/benchmarks/incrementalParseBenchmark.d.ts +18 -0
- package/dist/src/benchmarks/incrementalParseBenchmark.d.ts.map +1 -0
- package/dist/src/benchmarks/incrementalParseBenchmark.js +121 -0
- package/dist/src/benchmarks/incrementalParseBenchmark.js.map +1 -0
- package/dist/src/billing/GDPRComplianceService.test.d.ts +2 -0
- package/dist/src/billing/GDPRComplianceService.test.d.ts.map +1 -0
- package/dist/src/billing/GDPRComplianceService.test.js +405 -0
- package/dist/src/billing/GDPRComplianceService.test.js.map +1 -0
- package/dist/src/index.d.ts +4 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/indexer/SkillParser.coverage.test.d.ts +10 -0
- package/dist/src/indexer/SkillParser.coverage.test.d.ts.map +1 -0
- package/dist/src/indexer/SkillParser.coverage.test.js +76 -0
- package/dist/src/indexer/SkillParser.coverage.test.js.map +1 -0
- package/dist/src/indexer/SkillParser.test.d.ts +2 -0
- package/dist/src/indexer/SkillParser.test.d.ts.map +1 -0
- package/dist/src/indexer/SkillParser.test.js +375 -0
- package/dist/src/indexer/SkillParser.test.js.map +1 -0
- package/dist/src/sources/LocalFilesystemAdapter.d.ts +104 -10
- package/dist/src/sources/LocalFilesystemAdapter.d.ts.map +1 -1
- package/dist/src/sources/LocalFilesystemAdapter.helpers.d.ts +92 -0
- package/dist/src/sources/LocalFilesystemAdapter.helpers.d.ts.map +1 -0
- package/dist/src/sources/LocalFilesystemAdapter.helpers.js +157 -0
- package/dist/src/sources/LocalFilesystemAdapter.helpers.js.map +1 -0
- package/dist/src/sources/LocalFilesystemAdapter.js +218 -159
- package/dist/src/sources/LocalFilesystemAdapter.js.map +1 -1
- package/dist/src/sources/LocalFilesystemAdapter.scan.d.ts +78 -0
- package/dist/src/sources/LocalFilesystemAdapter.scan.d.ts.map +1 -0
- package/dist/src/sources/LocalFilesystemAdapter.scan.js +118 -0
- package/dist/src/sources/LocalFilesystemAdapter.scan.js.map +1 -0
- package/dist/src/sources/index.d.ts +1 -1
- package/dist/src/sources/index.d.ts.map +1 -1
- package/dist/src/sources/index.js.map +1 -1
- package/dist/src/sources/types.d.ts +28 -0
- package/dist/src/sources/types.d.ts.map +1 -1
- package/dist/src/telemetry/tracer-imports.d.ts +13 -0
- package/dist/src/telemetry/tracer-imports.d.ts.map +1 -0
- package/dist/src/telemetry/tracer-imports.js +26 -0
- package/dist/src/telemetry/tracer-imports.js.map +1 -0
- package/dist/src/telemetry/tracer.d.ts.map +1 -1
- package/dist/src/telemetry/tracer.js +18 -21
- package/dist/src/telemetry/tracer.js.map +1 -1
- package/dist/src/utils/rate-limit.d.ts +39 -0
- package/dist/src/utils/rate-limit.d.ts.map +1 -0
- package/dist/src/utils/rate-limit.js +48 -0
- package/dist/src/utils/rate-limit.js.map +1 -0
- package/dist/src/utils/rate-limit.test.d.ts +11 -0
- package/dist/src/utils/rate-limit.test.d.ts.map +1 -0
- package/dist/src/utils/rate-limit.test.js +86 -0
- package/dist/src/utils/rate-limit.test.js.map +1 -0
- package/dist/src/webhooks/WebhookDeadLetterRepository.d.ts +178 -0
- package/dist/src/webhooks/WebhookDeadLetterRepository.d.ts.map +1 -0
- package/dist/src/webhooks/WebhookDeadLetterRepository.js +196 -0
- package/dist/src/webhooks/WebhookDeadLetterRepository.js.map +1 -0
- package/dist/src/webhooks/WebhookQueue.d.ts +1 -0
- package/dist/src/webhooks/WebhookQueue.d.ts.map +1 -1
- package/dist/src/webhooks/WebhookQueue.js +19 -0
- package/dist/src/webhooks/WebhookQueue.js.map +1 -1
- package/dist/src/webhooks/WebhookQueue.types.d.ts +11 -0
- package/dist/src/webhooks/WebhookQueue.types.d.ts.map +1 -1
- package/dist/src/webhooks/index.d.ts +1 -0
- package/dist/src/webhooks/index.d.ts.map +1 -1
- package/dist/src/webhooks/index.js +2 -0
- package/dist/src/webhooks/index.js.map +1 -1
- package/dist/tests/ActivationManager.test.d.ts +13 -0
- package/dist/tests/ActivationManager.test.d.ts.map +1 -0
- package/dist/tests/ActivationManager.test.js +218 -0
- package/dist/tests/ActivationManager.test.js.map +1 -0
- package/dist/tests/LocalFilesystemAdapter.coverage.test.d.ts +13 -0
- package/dist/tests/LocalFilesystemAdapter.coverage.test.d.ts.map +1 -0
- package/dist/tests/LocalFilesystemAdapter.coverage.test.js +314 -0
- package/dist/tests/LocalFilesystemAdapter.coverage.test.js.map +1 -0
- package/dist/tests/LocalFilesystemAdapter.security.test.d.ts +18 -0
- package/dist/tests/LocalFilesystemAdapter.security.test.d.ts.map +1 -0
- package/dist/tests/LocalFilesystemAdapter.security.test.js +344 -0
- package/dist/tests/LocalFilesystemAdapter.security.test.js.map +1 -0
- package/dist/tests/LocalFilesystemAdapter.test.d.ts +12 -0
- package/dist/tests/LocalFilesystemAdapter.test.d.ts.map +1 -0
- package/dist/tests/LocalFilesystemAdapter.test.js +301 -0
- package/dist/tests/LocalFilesystemAdapter.test.js.map +1 -0
- package/dist/tests/ROIDashboardService.coverage.test.d.ts +9 -0
- package/dist/tests/ROIDashboardService.coverage.test.d.ts.map +1 -0
- package/dist/tests/ROIDashboardService.coverage.test.js +118 -0
- package/dist/tests/ROIDashboardService.coverage.test.js.map +1 -0
- package/dist/tests/ROIDashboardService.test.js +87 -0
- package/dist/tests/ROIDashboardService.test.js.map +1 -1
- package/dist/tests/ScraperAdapters.gitlab-coverage.test.d.ts +14 -0
- package/dist/tests/ScraperAdapters.gitlab-coverage.test.d.ts.map +1 -0
- package/dist/tests/ScraperAdapters.gitlab-coverage.test.js +169 -0
- package/dist/tests/ScraperAdapters.gitlab-coverage.test.js.map +1 -0
- package/dist/tests/ScraperAdapters.test.d.ts +5 -1
- package/dist/tests/ScraperAdapters.test.d.ts.map +1 -1
- package/dist/tests/ScraperAdapters.test.js +6 -336
- package/dist/tests/ScraperAdapters.test.js.map +1 -1
- package/dist/tests/WebhookDeadLetterRepository.test.d.ts +2 -0
- package/dist/tests/WebhookDeadLetterRepository.test.d.ts.map +1 -0
- package/dist/tests/WebhookDeadLetterRepository.test.js +333 -0
- package/dist/tests/WebhookDeadLetterRepository.test.js.map +1 -0
- package/dist/tests/WebhookHandler.test.js +93 -1
- package/dist/tests/WebhookHandler.test.js.map +1 -1
- package/dist/tests/WebhookQueue.coverage.test.d.ts +19 -0
- package/dist/tests/WebhookQueue.coverage.test.d.ts.map +1 -0
- package/dist/tests/WebhookQueue.coverage.test.js +190 -0
- package/dist/tests/WebhookQueue.coverage.test.js.map +1 -0
- package/dist/tests/api/client.validation.test.js +37 -0
- package/dist/tests/api/client.validation.test.js.map +1 -1
- package/dist/tests/billing/GDPRCompliance.test.d.ts +2 -2
- package/dist/tests/billing/GDPRCompliance.test.js +221 -36
- package/dist/tests/billing/GDPRCompliance.test.js.map +1 -1
- package/dist/tests/telemetry.test.js +126 -0
- package/dist/tests/telemetry.test.js.map +1 -1
- package/dist/tests/webhooks/WebhookDeadLetterRepository.test.d.ts +10 -0
- package/dist/tests/webhooks/WebhookDeadLetterRepository.test.d.ts.map +1 -0
- package/dist/tests/webhooks/WebhookDeadLetterRepository.test.js +109 -0
- package/dist/tests/webhooks/WebhookDeadLetterRepository.test.js.map +1 -0
- package/package.json +9 -5
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalFilesystemAdapter helpers (SMI-4287, SMI-4319, SMI-4320)
|
|
3
|
+
*
|
|
4
|
+
* Typed filesystem wrappers and symlink containment helpers used by
|
|
5
|
+
* LocalFilesystemAdapter. These surface `AdapterError` return values instead
|
|
6
|
+
* of throwing, so the adapter can continue scanning past individual failures
|
|
7
|
+
* (permission, symlink escape, loop) and aggregate them into
|
|
8
|
+
* `SourceSearchResult.warnings`.
|
|
9
|
+
*
|
|
10
|
+
* SMI-4320: drops platform-based `normaliseForFs` in favour of byte-wise
|
|
11
|
+
* `startsWith(root + sep)` on realpath outputs. The FS itself canonicalises
|
|
12
|
+
* case via `realpath` — platform heuristics miscategorise case-sensitive
|
|
13
|
+
* volumes (HFS+ case-sensitive macOS volumes, ext4 case-folded dirs).
|
|
14
|
+
* `resolveSafeRealpath` now accepts an `allowSymlinksOutsideRoot` opt-in
|
|
15
|
+
* (see SMI-4287) so direct-access callers can inherit the same containment
|
|
16
|
+
* policy as the scan loop.
|
|
17
|
+
*/
|
|
18
|
+
import { type Dirent, type Stats } from 'fs';
|
|
19
|
+
import type { AdapterError } from './types.js';
|
|
20
|
+
/**
|
|
21
|
+
* Success / failure result envelope used by `safeFs` helpers.
|
|
22
|
+
*
|
|
23
|
+
* @typeParam T - Type of the successful value
|
|
24
|
+
*/
|
|
25
|
+
export type FsResult<T> = {
|
|
26
|
+
ok: true;
|
|
27
|
+
value: T;
|
|
28
|
+
} | {
|
|
29
|
+
ok: false;
|
|
30
|
+
error: AdapterError;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Safe filesystem wrappers that return `FsResult<T>` instead of throwing.
|
|
34
|
+
*/
|
|
35
|
+
export declare const safeFs: {
|
|
36
|
+
readonly readdir: (path: string) => Promise<FsResult<Dirent[]>>;
|
|
37
|
+
readonly stat: (path: string) => Promise<FsResult<Stats>>;
|
|
38
|
+
readonly readFile: (path: string, encoding?: BufferEncoding) => Promise<FsResult<string>>;
|
|
39
|
+
readonly realpath: (path: string) => Promise<FsResult<string>>;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Byte-wise containment check on two realpath outputs (SMI-4320).
|
|
43
|
+
*
|
|
44
|
+
* Compares raw realpath bytes: `candidateReal === rootReal` or
|
|
45
|
+
* `candidateReal.startsWith(rootReal + sep)`. No platform lowercasing — the
|
|
46
|
+
* filesystem is authoritative. Case-insensitive volumes (APFS default, NTFS)
|
|
47
|
+
* already canonicalise case through `fs.realpath`; case-sensitive volumes
|
|
48
|
+
* (HFS+ case-sensitive, ext4 case-folded) keep distinct paths distinct.
|
|
49
|
+
*
|
|
50
|
+
* The trailing-separator guard is load-bearing: without `+ sep`,
|
|
51
|
+
* `rootDir = /a/root` would accept `/a/rootfoo` as contained.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isRealpathContained(candidateReal: string, rootReal: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Options accepted by `resolveSafeRealpath`.
|
|
56
|
+
*
|
|
57
|
+
* - `allowSymlinksOutsideRoot` (SMI-4287): when `true`, skip the containment
|
|
58
|
+
* re-check and return the realpath unconditionally. Callers that opt in
|
|
59
|
+
* accept the security tradeoff — used by dev-install tooling that scans
|
|
60
|
+
* linked sibling packages.
|
|
61
|
+
*/
|
|
62
|
+
export interface ResolveSafeRealpathOptions {
|
|
63
|
+
allowSymlinksOutsideRoot?: boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve `candidate` to a realpath and verify it remains within `root`.
|
|
67
|
+
*
|
|
68
|
+
* Runs `fs.realpath` on both the candidate and the root, then performs a
|
|
69
|
+
* byte-wise `startsWith(rootReal + sep)` check (SMI-4320). On case-insensitive
|
|
70
|
+
* volumes the FS canonicalises case inside `realpath`; on case-sensitive
|
|
71
|
+
* volumes distinct cases remain distinct — both outcomes are correct.
|
|
72
|
+
*
|
|
73
|
+
* Returns `{ ok: true, value: resolvedRealpath }` on success. On failure
|
|
74
|
+
* returns an `AdapterError` with:
|
|
75
|
+
* - `symlink-escape` if the target resolves outside the root
|
|
76
|
+
* - `loop` on `ELOOP` (circular symlinks)
|
|
77
|
+
* - `permission` / `not-found` / `io` for other filesystem errors
|
|
78
|
+
*
|
|
79
|
+
* Does NOT throw for containment violations (caller drives the warning list).
|
|
80
|
+
*
|
|
81
|
+
* SMI-4287 opt-in: when `opts.allowSymlinksOutsideRoot === true`, containment
|
|
82
|
+
* is skipped entirely. The loop-detection + other realpath errors still apply.
|
|
83
|
+
*
|
|
84
|
+
* TOCTOU caveat: this is a check-then-use pattern. Between the realpath
|
|
85
|
+
* check and a subsequent `fs.readFile`, a malicious actor with write access
|
|
86
|
+
* inside `rootDir` could swap the symlink target. True atomicity requires
|
|
87
|
+
* fd-based I/O (`fs.open` + fstat-by-fd + read-by-fd) and is out of scope
|
|
88
|
+
* here — this helper closes the 99% case where the attack window is
|
|
89
|
+
* scan-to-fetch (minutes to hours). See plan doc for the residual risk.
|
|
90
|
+
*/
|
|
91
|
+
export declare function resolveSafeRealpath(candidate: string, root: string, opts?: ResolveSafeRealpathOptions): Promise<FsResult<string>>;
|
|
92
|
+
//# sourceMappingURL=LocalFilesystemAdapter.helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalFilesystemAdapter.helpers.d.ts","sourceRoot":"","sources":["../../../src/sources/LocalFilesystemAdapter.helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAkB,KAAK,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,IAAI,CAAA;AAE5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C;;;;GAIG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,YAAY,CAAA;CAAE,CAAA;AA0DrF;;GAEG;AACH,eAAO,MAAM,MAAM;6BACH,MAAM,KAAG,OAAO,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;0BAGvC,MAAM,KAAG,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;8BAG7B,MAAM,aAAY,cAAc,KAAa,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;8BAGtE,MAAM,KAAG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;CAGzC,CAAA;AAEV;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEpF;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,0BAA0B;IACzC,wBAAwB,CAAC,EAAE,OAAO,CAAA;CACnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,0BAA+B,GACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CA0B3B"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalFilesystemAdapter helpers (SMI-4287, SMI-4319, SMI-4320)
|
|
3
|
+
*
|
|
4
|
+
* Typed filesystem wrappers and symlink containment helpers used by
|
|
5
|
+
* LocalFilesystemAdapter. These surface `AdapterError` return values instead
|
|
6
|
+
* of throwing, so the adapter can continue scanning past individual failures
|
|
7
|
+
* (permission, symlink escape, loop) and aggregate them into
|
|
8
|
+
* `SourceSearchResult.warnings`.
|
|
9
|
+
*
|
|
10
|
+
* SMI-4320: drops platform-based `normaliseForFs` in favour of byte-wise
|
|
11
|
+
* `startsWith(root + sep)` on realpath outputs. The FS itself canonicalises
|
|
12
|
+
* case via `realpath` — platform heuristics miscategorise case-sensitive
|
|
13
|
+
* volumes (HFS+ case-sensitive macOS volumes, ext4 case-folded dirs).
|
|
14
|
+
* `resolveSafeRealpath` now accepts an `allowSymlinksOutsideRoot` opt-in
|
|
15
|
+
* (see SMI-4287) so direct-access callers can inherit the same containment
|
|
16
|
+
* policy as the scan loop.
|
|
17
|
+
*/
|
|
18
|
+
import { promises as fs } from 'fs';
|
|
19
|
+
import { sep } from 'path';
|
|
20
|
+
/**
|
|
21
|
+
* Map a Node filesystem error to an `AdapterError.code`.
|
|
22
|
+
*/
|
|
23
|
+
function mapErrnoToCode(err) {
|
|
24
|
+
const code = err?.code;
|
|
25
|
+
switch (code) {
|
|
26
|
+
case 'EACCES':
|
|
27
|
+
case 'EPERM':
|
|
28
|
+
return 'permission';
|
|
29
|
+
case 'ENOENT':
|
|
30
|
+
return 'not-found';
|
|
31
|
+
case 'ELOOP':
|
|
32
|
+
return 'loop';
|
|
33
|
+
default:
|
|
34
|
+
return 'io';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build a human-friendly message for an AdapterError.
|
|
39
|
+
*/
|
|
40
|
+
function describe(code, path) {
|
|
41
|
+
switch (code) {
|
|
42
|
+
case 'permission':
|
|
43
|
+
return `Cannot read: ${path}`;
|
|
44
|
+
case 'not-found':
|
|
45
|
+
return `Not found: ${path}`;
|
|
46
|
+
case 'loop':
|
|
47
|
+
return `Symlink loop detected: ${path}`;
|
|
48
|
+
case 'symlink-escape':
|
|
49
|
+
return `Symlink outside root, skipped: ${path}`;
|
|
50
|
+
case 'io':
|
|
51
|
+
return `Filesystem error: ${path}`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Wrap a throwing `fs` call into an `FsResult<T>`.
|
|
56
|
+
*/
|
|
57
|
+
async function wrap(path, op) {
|
|
58
|
+
try {
|
|
59
|
+
return { ok: true, value: await op() };
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const code = mapErrnoToCode(error);
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
error: {
|
|
66
|
+
code,
|
|
67
|
+
path,
|
|
68
|
+
message: describe(code, path),
|
|
69
|
+
cause: error,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Safe filesystem wrappers that return `FsResult<T>` instead of throwing.
|
|
76
|
+
*/
|
|
77
|
+
export const safeFs = {
|
|
78
|
+
readdir(path) {
|
|
79
|
+
return wrap(path, () => fs.readdir(path, { withFileTypes: true }));
|
|
80
|
+
},
|
|
81
|
+
stat(path) {
|
|
82
|
+
return wrap(path, () => fs.stat(path));
|
|
83
|
+
},
|
|
84
|
+
readFile(path, encoding = 'utf-8') {
|
|
85
|
+
return wrap(path, () => fs.readFile(path, encoding));
|
|
86
|
+
},
|
|
87
|
+
realpath(path) {
|
|
88
|
+
return wrap(path, () => fs.realpath(path));
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Byte-wise containment check on two realpath outputs (SMI-4320).
|
|
93
|
+
*
|
|
94
|
+
* Compares raw realpath bytes: `candidateReal === rootReal` or
|
|
95
|
+
* `candidateReal.startsWith(rootReal + sep)`. No platform lowercasing — the
|
|
96
|
+
* filesystem is authoritative. Case-insensitive volumes (APFS default, NTFS)
|
|
97
|
+
* already canonicalise case through `fs.realpath`; case-sensitive volumes
|
|
98
|
+
* (HFS+ case-sensitive, ext4 case-folded) keep distinct paths distinct.
|
|
99
|
+
*
|
|
100
|
+
* The trailing-separator guard is load-bearing: without `+ sep`,
|
|
101
|
+
* `rootDir = /a/root` would accept `/a/rootfoo` as contained.
|
|
102
|
+
*/
|
|
103
|
+
export function isRealpathContained(candidateReal, rootReal) {
|
|
104
|
+
return candidateReal === rootReal || candidateReal.startsWith(rootReal + sep);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Resolve `candidate` to a realpath and verify it remains within `root`.
|
|
108
|
+
*
|
|
109
|
+
* Runs `fs.realpath` on both the candidate and the root, then performs a
|
|
110
|
+
* byte-wise `startsWith(rootReal + sep)` check (SMI-4320). On case-insensitive
|
|
111
|
+
* volumes the FS canonicalises case inside `realpath`; on case-sensitive
|
|
112
|
+
* volumes distinct cases remain distinct — both outcomes are correct.
|
|
113
|
+
*
|
|
114
|
+
* Returns `{ ok: true, value: resolvedRealpath }` on success. On failure
|
|
115
|
+
* returns an `AdapterError` with:
|
|
116
|
+
* - `symlink-escape` if the target resolves outside the root
|
|
117
|
+
* - `loop` on `ELOOP` (circular symlinks)
|
|
118
|
+
* - `permission` / `not-found` / `io` for other filesystem errors
|
|
119
|
+
*
|
|
120
|
+
* Does NOT throw for containment violations (caller drives the warning list).
|
|
121
|
+
*
|
|
122
|
+
* SMI-4287 opt-in: when `opts.allowSymlinksOutsideRoot === true`, containment
|
|
123
|
+
* is skipped entirely. The loop-detection + other realpath errors still apply.
|
|
124
|
+
*
|
|
125
|
+
* TOCTOU caveat: this is a check-then-use pattern. Between the realpath
|
|
126
|
+
* check and a subsequent `fs.readFile`, a malicious actor with write access
|
|
127
|
+
* inside `rootDir` could swap the symlink target. True atomicity requires
|
|
128
|
+
* fd-based I/O (`fs.open` + fstat-by-fd + read-by-fd) and is out of scope
|
|
129
|
+
* here — this helper closes the 99% case where the attack window is
|
|
130
|
+
* scan-to-fetch (minutes to hours). See plan doc for the residual risk.
|
|
131
|
+
*/
|
|
132
|
+
export async function resolveSafeRealpath(candidate, root, opts = {}) {
|
|
133
|
+
const candidateResult = await safeFs.realpath(candidate);
|
|
134
|
+
if (!candidateResult.ok)
|
|
135
|
+
return candidateResult;
|
|
136
|
+
if (opts.allowSymlinksOutsideRoot === true) {
|
|
137
|
+
return candidateResult;
|
|
138
|
+
}
|
|
139
|
+
const rootResult = await safeFs.realpath(root);
|
|
140
|
+
if (!rootResult.ok)
|
|
141
|
+
return rootResult;
|
|
142
|
+
if (!isRealpathContained(candidateResult.value, rootResult.value)) {
|
|
143
|
+
// Report the symlink path the user can identify (not the opaque
|
|
144
|
+
// realpath target). Including the target would leak the external
|
|
145
|
+
// location, which is exactly what the guard is protecting against.
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
error: {
|
|
149
|
+
code: 'symlink-escape',
|
|
150
|
+
path: candidate,
|
|
151
|
+
message: describe('symlink-escape', candidate),
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return { ok: true, value: candidateResult.value };
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=LocalFilesystemAdapter.helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalFilesystemAdapter.helpers.js","sourceRoot":"","sources":["../../../src/sources/LocalFilesystemAdapter.helpers.ts"],"names":[],"mappings":"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"}
|