@octocodeai/octocode-engine 16.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +558 -0
- package/dist/lsp/client.d.ts +28 -0
- package/dist/lsp/client.js +98 -0
- package/dist/lsp/config.d.ts +3 -0
- package/dist/lsp/config.js +7 -0
- package/dist/lsp/evidence.d.ts +4 -0
- package/dist/lsp/evidence.js +28 -0
- package/dist/lsp/index.d.ts +7 -0
- package/dist/lsp/index.js +6 -0
- package/dist/lsp/initConstants.d.ts +4 -0
- package/dist/lsp/initConstants.js +27 -0
- package/dist/lsp/lspClientPool.d.ts +27 -0
- package/dist/lsp/lspClientPool.js +87 -0
- package/dist/lsp/lspErrorCodes.d.ts +12 -0
- package/dist/lsp/lspErrorCodes.js +12 -0
- package/dist/lsp/manager.d.ts +23 -0
- package/dist/lsp/manager.js +94 -0
- package/dist/lsp/native.d.ts +35 -0
- package/dist/lsp/native.js +3 -0
- package/dist/lsp/resolver.d.ts +26 -0
- package/dist/lsp/resolver.js +73 -0
- package/dist/lsp/schemas.d.ts +9 -0
- package/dist/lsp/schemas.js +30 -0
- package/dist/lsp/types.d.ts +85 -0
- package/dist/lsp/types.js +1 -0
- package/dist/lsp/uri.d.ts +15 -0
- package/dist/lsp/uri.js +24 -0
- package/dist/lsp/validation.d.ts +6 -0
- package/dist/lsp/validation.js +23 -0
- package/dist/lsp/workspaceRoot.d.ts +2 -0
- package/dist/lsp/workspaceRoot.js +7 -0
- package/dist/security/commandUtils.d.ts +1 -0
- package/dist/security/commandUtils.js +7 -0
- package/dist/security/commandValidator.d.ts +7 -0
- package/dist/security/commandValidator.js +472 -0
- package/dist/security/contentSanitizer.d.ts +2 -0
- package/dist/security/contentSanitizer.js +181 -0
- package/dist/security/filePatterns.d.ts +1 -0
- package/dist/security/filePatterns.js +209 -0
- package/dist/security/ignoredPathFilter.d.ts +3 -0
- package/dist/security/ignoredPathFilter.js +72 -0
- package/dist/security/index.d.ts +16 -0
- package/dist/security/index.js +13 -0
- package/dist/security/mask.d.ts +1 -0
- package/dist/security/mask.js +49 -0
- package/dist/security/maskUtils.d.ts +1 -0
- package/dist/security/maskUtils.js +7 -0
- package/dist/security/native.d.ts +9 -0
- package/dist/security/native.js +161 -0
- package/dist/security/paramExtractors.d.ts +7 -0
- package/dist/security/paramExtractors.js +75 -0
- package/dist/security/pathPatterns.d.ts +1 -0
- package/dist/security/pathPatterns.js +26 -0
- package/dist/security/pathUtils.d.ts +1 -0
- package/dist/security/pathUtils.js +37 -0
- package/dist/security/pathValidator.d.ts +22 -0
- package/dist/security/pathValidator.js +229 -0
- package/dist/security/regexes/ai-providers.d.ts +2 -0
- package/dist/security/regexes/ai-providers.js +177 -0
- package/dist/security/regexes/analytics.d.ts +2 -0
- package/dist/security/regexes/analytics.js +50 -0
- package/dist/security/regexes/auth-crypto.d.ts +6 -0
- package/dist/security/regexes/auth-crypto.js +255 -0
- package/dist/security/regexes/aws.d.ts +2 -0
- package/dist/security/regexes/aws.js +68 -0
- package/dist/security/regexes/cloudProviders.d.ts +2 -0
- package/dist/security/regexes/cloudProviders.js +328 -0
- package/dist/security/regexes/communications.d.ts +4 -0
- package/dist/security/regexes/communications.js +260 -0
- package/dist/security/regexes/databases.d.ts +2 -0
- package/dist/security/regexes/databases.js +135 -0
- package/dist/security/regexes/devTools.d.ts +2 -0
- package/dist/security/regexes/devTools.js +236 -0
- package/dist/security/regexes/index.d.ts +3 -0
- package/dist/security/regexes/index.js +31 -0
- package/dist/security/regexes/monitoring.d.ts +2 -0
- package/dist/security/regexes/monitoring.js +92 -0
- package/dist/security/regexes/payments-commerce.d.ts +3 -0
- package/dist/security/regexes/payments-commerce.js +197 -0
- package/dist/security/regexes/types.d.ts +2 -0
- package/dist/security/regexes/types.js +1 -0
- package/dist/security/regexes/vcs.d.ts +2 -0
- package/dist/security/regexes/vcs.js +105 -0
- package/dist/security/registry.d.ts +48 -0
- package/dist/security/registry.js +156 -0
- package/dist/security/securityConstants.d.ts +3 -0
- package/dist/security/securityConstants.js +12 -0
- package/dist/security/types.d.ts +35 -0
- package/dist/security/types.js +1 -0
- package/dist/security/withSecurityValidation.d.ts +21 -0
- package/dist/security/withSecurityValidation.js +107 -0
- package/index.cjs +97 -0
- package/index.d.ts +934 -0
- package/index.js +652 -0
- package/package.json +311 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { maskEveryOtherChar } from './maskUtils.js';
|
|
5
|
+
import { allRegexPatterns } from './regexes/index.js';
|
|
6
|
+
const _require = createRequire(import.meta.url);
|
|
7
|
+
const _dir = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const MAX_CONTENT_SIZE = 10_000_000;
|
|
9
|
+
const CONTENT_SIZE_EXCEEDED = 'content-size-exceeded';
|
|
10
|
+
const CONTENT_SIZE_PLACEHOLDER = '[CONTENT-REDACTED-SIZE-LIMIT]';
|
|
11
|
+
let nativeLoadState;
|
|
12
|
+
function envFlag(name) {
|
|
13
|
+
const value = process.env[name];
|
|
14
|
+
return value === '1' || value?.toLowerCase() === 'true';
|
|
15
|
+
}
|
|
16
|
+
// Secret detection is part of the single octocode-engine native binary. The
|
|
17
|
+
// engine root loader (index.cjs) already resolves the correct platform binary
|
|
18
|
+
// (incl. musl detection), so we simply require it — both dist/security/native.js
|
|
19
|
+
// and src/security/native.ts sit two levels below the package root.
|
|
20
|
+
function loadNative() {
|
|
21
|
+
const candidates = [];
|
|
22
|
+
if (process.env.OCTOCODE_SECURITY_NATIVE_PATH) {
|
|
23
|
+
candidates.push(process.env.OCTOCODE_SECURITY_NATIVE_PATH);
|
|
24
|
+
}
|
|
25
|
+
candidates.push(join(_dir, '..', '..', 'index.cjs'));
|
|
26
|
+
const errors = [];
|
|
27
|
+
for (const candidate of candidates) {
|
|
28
|
+
try {
|
|
29
|
+
return _require(candidate);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
33
|
+
errors.push(`${candidate}: ${message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`octocode-engine: secret-detection native binding unavailable for ` +
|
|
37
|
+
`${process.platform}-${process.arch}. Tried:\n${errors.join('\n')}`);
|
|
38
|
+
}
|
|
39
|
+
function getNativeModule() {
|
|
40
|
+
if (envFlag('OCTOCODE_SECURITY_FORCE_JS')) {
|
|
41
|
+
if (envFlag('OCTOCODE_SECURITY_REQUIRE_NATIVE')) {
|
|
42
|
+
throw new Error('OCTOCODE_SECURITY_REQUIRE_NATIVE=1 conflicts with OCTOCODE_SECURITY_FORCE_JS=1');
|
|
43
|
+
}
|
|
44
|
+
nativeLoadState = { kind: 'fallback' };
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (nativeLoadState) {
|
|
48
|
+
if (nativeLoadState.kind === 'native')
|
|
49
|
+
return nativeLoadState.module;
|
|
50
|
+
if (envFlag('OCTOCODE_SECURITY_REQUIRE_NATIVE')) {
|
|
51
|
+
throw nativeLoadState.error instanceof Error
|
|
52
|
+
? nativeLoadState.error
|
|
53
|
+
: new Error(String(nativeLoadState.error));
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const module = loadNative();
|
|
59
|
+
nativeLoadState = { kind: 'native', module };
|
|
60
|
+
return module;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (envFlag('OCTOCODE_SECURITY_REQUIRE_NATIVE')) {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
nativeLoadState = { kind: 'fallback', error };
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function cloneGlobalRegex(regex) {
|
|
71
|
+
const flags = regex.flags.includes('g') ? regex.flags : `${regex.flags}g`;
|
|
72
|
+
return new RegExp(regex.source, flags);
|
|
73
|
+
}
|
|
74
|
+
function shouldApplyPattern(pattern, filePath) {
|
|
75
|
+
if (!pattern.fileContext)
|
|
76
|
+
return true;
|
|
77
|
+
if (!filePath)
|
|
78
|
+
return false;
|
|
79
|
+
pattern.fileContext.lastIndex = 0;
|
|
80
|
+
const applies = pattern.fileContext.test(filePath);
|
|
81
|
+
pattern.fileContext.lastIndex = 0;
|
|
82
|
+
return applies;
|
|
83
|
+
}
|
|
84
|
+
function sanitizeWithJsFallback(content, filePath) {
|
|
85
|
+
// Byte length (UTF-8) to match the native path's `content.len()` — otherwise
|
|
86
|
+
// large multibyte content is redacted by native but not by this fallback.
|
|
87
|
+
if (Buffer.byteLength(content, 'utf8') > MAX_CONTENT_SIZE) {
|
|
88
|
+
return {
|
|
89
|
+
content: CONTENT_SIZE_PLACEHOLDER,
|
|
90
|
+
hasSecrets: true,
|
|
91
|
+
secretsDetected: [CONTENT_SIZE_EXCEEDED],
|
|
92
|
+
warnings: [
|
|
93
|
+
`Content exceeds ${MAX_CONTENT_SIZE} byte limit — redacted for safety`,
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
let sanitized = content;
|
|
98
|
+
const secretsDetected = [];
|
|
99
|
+
for (const pattern of allRegexPatterns) {
|
|
100
|
+
if (!shouldApplyPattern(pattern, filePath))
|
|
101
|
+
continue;
|
|
102
|
+
const regex = cloneGlobalRegex(pattern.regex);
|
|
103
|
+
regex.lastIndex = 0;
|
|
104
|
+
if (!regex.test(sanitized))
|
|
105
|
+
continue;
|
|
106
|
+
regex.lastIndex = 0;
|
|
107
|
+
const next = sanitized.replace(regex, `[REDACTED-${pattern.name.toUpperCase()}]`);
|
|
108
|
+
if (next !== sanitized) {
|
|
109
|
+
sanitized = next;
|
|
110
|
+
secretsDetected.push(pattern.name);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
content: sanitized,
|
|
115
|
+
hasSecrets: secretsDetected.length > 0,
|
|
116
|
+
secretsDetected,
|
|
117
|
+
warnings: secretsDetected.length > 0
|
|
118
|
+
? [`${secretsDetected.length} secret(s) redacted`]
|
|
119
|
+
: [],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function maskWithJsFallback(text) {
|
|
123
|
+
if (!text)
|
|
124
|
+
return text;
|
|
125
|
+
const matches = [];
|
|
126
|
+
for (const pattern of allRegexPatterns) {
|
|
127
|
+
if (pattern.fileContext)
|
|
128
|
+
continue;
|
|
129
|
+
const regex = cloneGlobalRegex(pattern.regex);
|
|
130
|
+
let match;
|
|
131
|
+
while ((match = regex.exec(text)) !== null) {
|
|
132
|
+
matches.push({ start: match.index, end: match.index + match[0].length });
|
|
133
|
+
if (match[0].length === 0)
|
|
134
|
+
regex.lastIndex++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (matches.length === 0)
|
|
138
|
+
return text;
|
|
139
|
+
matches.sort((left, right) => left.start - right.start);
|
|
140
|
+
const nonOverlapping = [];
|
|
141
|
+
let lastEnd = 0;
|
|
142
|
+
for (const match of matches) {
|
|
143
|
+
if (match.start >= lastEnd) {
|
|
144
|
+
nonOverlapping.push(match);
|
|
145
|
+
lastEnd = match.end;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
let result = '';
|
|
149
|
+
let position = 0;
|
|
150
|
+
for (const match of nonOverlapping) {
|
|
151
|
+
result += text.slice(position, match.start);
|
|
152
|
+
result += maskEveryOtherChar(text.slice(match.start, match.end));
|
|
153
|
+
position = match.end;
|
|
154
|
+
}
|
|
155
|
+
result += text.slice(position);
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
export const nativeSanitizeContent = (content, filePath) => getNativeModule()?.sanitizeContent(content, filePath) ??
|
|
159
|
+
sanitizeWithJsFallback(content, filePath);
|
|
160
|
+
export const nativeMaskSensitiveData = (text) => getNativeModule()?.maskSensitiveData(text) ?? maskWithJsFallback(text);
|
|
161
|
+
export const nativePatternCount = () => getNativeModule()?.patternCount() ?? allRegexPatterns.length;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface ResearchFields {
|
|
2
|
+
mainResearchGoal?: string;
|
|
3
|
+
researchGoal?: string;
|
|
4
|
+
reasoning?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function extractResearchFields(params: Record<string, unknown>): ResearchFields;
|
|
7
|
+
export declare function extractRepoOwnerFromParams(params: Record<string, unknown>): string[];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
function getQueriesArray(params) {
|
|
2
|
+
const queries = params.queries;
|
|
3
|
+
if (queries && Array.isArray(queries) && queries.length > 0) {
|
|
4
|
+
return queries;
|
|
5
|
+
}
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
function extractResearchFieldsFromQuery(query) {
|
|
9
|
+
const fields = {};
|
|
10
|
+
if (typeof query.mainResearchGoal === 'string' && query.mainResearchGoal) {
|
|
11
|
+
fields.mainResearchGoal = query.mainResearchGoal;
|
|
12
|
+
}
|
|
13
|
+
if (typeof query.researchGoal === 'string' && query.researchGoal) {
|
|
14
|
+
fields.researchGoal = query.researchGoal;
|
|
15
|
+
}
|
|
16
|
+
if (typeof query.reasoning === 'string' && query.reasoning) {
|
|
17
|
+
fields.reasoning = query.reasoning;
|
|
18
|
+
}
|
|
19
|
+
return fields;
|
|
20
|
+
}
|
|
21
|
+
export function extractResearchFields(params) {
|
|
22
|
+
const queries = getQueriesArray(params);
|
|
23
|
+
if (!queries) {
|
|
24
|
+
return extractResearchFieldsFromQuery(params);
|
|
25
|
+
}
|
|
26
|
+
const mainGoals = new Set();
|
|
27
|
+
const goals = new Set();
|
|
28
|
+
const reasonings = new Set();
|
|
29
|
+
for (const query of queries) {
|
|
30
|
+
const fields = extractResearchFieldsFromQuery(query);
|
|
31
|
+
if (fields.mainResearchGoal)
|
|
32
|
+
mainGoals.add(fields.mainResearchGoal);
|
|
33
|
+
if (fields.researchGoal)
|
|
34
|
+
goals.add(fields.researchGoal);
|
|
35
|
+
if (fields.reasoning)
|
|
36
|
+
reasonings.add(fields.reasoning);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
...(mainGoals.size > 0 && {
|
|
40
|
+
mainResearchGoal: Array.from(mainGoals).join('; '),
|
|
41
|
+
}),
|
|
42
|
+
...(goals.size > 0 && { researchGoal: Array.from(goals).join('; ') }),
|
|
43
|
+
...(reasonings.size > 0 && {
|
|
44
|
+
reasoning: Array.from(reasonings).join('; '),
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function extractRepoOwnerFromQuery(query) {
|
|
49
|
+
const repository = typeof query.repository === 'string' ? query.repository : undefined;
|
|
50
|
+
if (repository && repository.includes('/')) {
|
|
51
|
+
return [repository];
|
|
52
|
+
}
|
|
53
|
+
const repo = typeof query.repo === 'string' ? query.repo : undefined;
|
|
54
|
+
const owner = typeof query.owner === 'string' ? query.owner : undefined;
|
|
55
|
+
if (owner && repo) {
|
|
56
|
+
return [`${owner}/${repo}`];
|
|
57
|
+
}
|
|
58
|
+
if (owner) {
|
|
59
|
+
return [owner];
|
|
60
|
+
}
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
export function extractRepoOwnerFromParams(params) {
|
|
64
|
+
const queries = getQueriesArray(params);
|
|
65
|
+
if (!queries) {
|
|
66
|
+
return extractRepoOwnerFromQuery(params);
|
|
67
|
+
}
|
|
68
|
+
const repoSet = new Set();
|
|
69
|
+
for (const query of queries) {
|
|
70
|
+
for (const repo of extractRepoOwnerFromQuery(query)) {
|
|
71
|
+
repoSet.add(repo);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return Array.from(repoSet);
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const IGNORED_PATH_PATTERNS: RegExp[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const IGNORED_PATH_PATTERNS = [
|
|
2
|
+
/(?:^|\/)\.git(?:\/|$)/,
|
|
3
|
+
/(?:^|\/)\.ssh(?:\/|$)/,
|
|
4
|
+
/(?:^|\/)\.aws(?:\/|$)/,
|
|
5
|
+
/(?:^|\/)\.docker(?:\/|$)/,
|
|
6
|
+
/(?:^|\/)\.config\/gcloud(?:\/|$)/,
|
|
7
|
+
/(?:^|\/)\.azure(?:\/|$)/,
|
|
8
|
+
/(?:^|\/)\.kube(?:\/|$)/,
|
|
9
|
+
/(?:^|\/)\.terraform(?:\/|$)/,
|
|
10
|
+
/(?:^|\/)secrets(?:\/|$)/,
|
|
11
|
+
/(?:^|\/)private(?:\/|$)/,
|
|
12
|
+
/(?:^|\/)\.password-store(?:\/|$)/,
|
|
13
|
+
/\.mozilla\/firefox\//,
|
|
14
|
+
/\.config\/chromium\//,
|
|
15
|
+
/\.config\/google-chrome\//,
|
|
16
|
+
/Library\/Application Support\/Google\/Chrome\//,
|
|
17
|
+
/Library\/Application Support\/Firefox\//,
|
|
18
|
+
/Library\/Keychains\//,
|
|
19
|
+
/(?:^|\/)\.thunderbird(?:\/|$)/,
|
|
20
|
+
/(?:^|\/)\.evolution(?:\/|$)/,
|
|
21
|
+
/(?:^|\/)\.vagrant(?:\/|$)/,
|
|
22
|
+
/(?:^|\/)\.minikube(?:\/|$)/,
|
|
23
|
+
/(?:^|\/)\.bitcoin(?:\/|$)/,
|
|
24
|
+
/(?:^|\/)\.ethereum(?:\/|$)/,
|
|
25
|
+
/(?:^|\/)\.electrum(?:\/|$)/,
|
|
26
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function redactPath(absolutePath: string, workspaceRoot?: string): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
function normalizePath(p) {
|
|
4
|
+
if (!p)
|
|
5
|
+
return p;
|
|
6
|
+
const normalized = path.posix.normalize(p.replace(/\\/g, '/'));
|
|
7
|
+
return normalized.length > 1 && normalized.endsWith('/')
|
|
8
|
+
? normalized.slice(0, -1)
|
|
9
|
+
: normalized;
|
|
10
|
+
}
|
|
11
|
+
function getRelativeIfChild(child, parent) {
|
|
12
|
+
if (child === parent)
|
|
13
|
+
return '.';
|
|
14
|
+
const parentPrefix = parent + '/';
|
|
15
|
+
if (child.startsWith(parentPrefix)) {
|
|
16
|
+
return child.slice(parentPrefix.length);
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const HOME_DIR = normalizePath(os.homedir());
|
|
21
|
+
export function redactPath(absolutePath, workspaceRoot) {
|
|
22
|
+
if (!absolutePath)
|
|
23
|
+
return '';
|
|
24
|
+
const normalized = normalizePath(absolutePath);
|
|
25
|
+
const rootSource = workspaceRoot ?? process.cwd();
|
|
26
|
+
const root = normalizePath(rootSource);
|
|
27
|
+
const relative = getRelativeIfChild(normalized, root);
|
|
28
|
+
if (relative !== null)
|
|
29
|
+
return relative;
|
|
30
|
+
if (HOME_DIR) {
|
|
31
|
+
const homeRelative = getRelativeIfChild(normalized, HOME_DIR);
|
|
32
|
+
if (homeRelative !== null) {
|
|
33
|
+
return homeRelative === '.' ? '~' : '~/' + homeRelative;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return path.basename(normalized);
|
|
37
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PathValidationResult } from './types.js';
|
|
2
|
+
interface PathValidatorOptions {
|
|
3
|
+
workspaceRoot?: string;
|
|
4
|
+
additionalRoots?: string[];
|
|
5
|
+
includeHomeDir?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare class PathValidator {
|
|
8
|
+
private allowedRoots;
|
|
9
|
+
constructor(options?: PathValidatorOptions);
|
|
10
|
+
private expandTilde;
|
|
11
|
+
addAllowedRoot(root: string): void;
|
|
12
|
+
private isResolvedPathAllowed;
|
|
13
|
+
validate(inputPath: string): PathValidationResult;
|
|
14
|
+
private validateNonExistentPath;
|
|
15
|
+
exists(inputPath: string): Promise<boolean>;
|
|
16
|
+
getType(inputPath: string): Promise<'file' | 'directory' | 'symlink' | null>;
|
|
17
|
+
getAllowedRoots(): readonly string[];
|
|
18
|
+
replaceAllowedRoots(roots: readonly string[]): void;
|
|
19
|
+
}
|
|
20
|
+
export declare const pathValidator: PathValidator;
|
|
21
|
+
export declare function resetPathValidator(options?: PathValidatorOptions): PathValidator;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { shouldIgnore } from './ignoredPathFilter.js';
|
|
5
|
+
import { redactPath } from './pathUtils.js';
|
|
6
|
+
import { securityRegistry } from './registry.js';
|
|
7
|
+
export class PathValidator {
|
|
8
|
+
allowedRoots;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
const opts = options || {};
|
|
11
|
+
this.allowedRoots = [];
|
|
12
|
+
if (opts.workspaceRoot) {
|
|
13
|
+
this.addAllowedRoot(opts.workspaceRoot);
|
|
14
|
+
}
|
|
15
|
+
if (opts.includeHomeDir !== false) {
|
|
16
|
+
const homeDir = os.homedir();
|
|
17
|
+
if (homeDir && !this.allowedRoots.includes(homeDir)) {
|
|
18
|
+
this.allowedRoots.push(homeDir);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (opts.additionalRoots) {
|
|
22
|
+
for (const additionalRoot of opts.additionalRoots) {
|
|
23
|
+
this.addAllowedRoot(additionalRoot);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const envPaths = process.env.ALLOWED_PATHS;
|
|
27
|
+
if (envPaths) {
|
|
28
|
+
const paths = envPaths
|
|
29
|
+
.split(',')
|
|
30
|
+
.map(p => p.trim())
|
|
31
|
+
.filter(p => p.length > 0);
|
|
32
|
+
for (const envPath of paths) {
|
|
33
|
+
this.addAllowedRoot(envPath);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const root of securityRegistry.extraAllowedRoots) {
|
|
37
|
+
this.addAllowedRoot(root);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
expandTilde(inputPath) {
|
|
41
|
+
if (inputPath.startsWith('~')) {
|
|
42
|
+
return path.join(os.homedir(), inputPath.slice(1));
|
|
43
|
+
}
|
|
44
|
+
return inputPath;
|
|
45
|
+
}
|
|
46
|
+
addAllowedRoot(root) {
|
|
47
|
+
const expandedRoot = this.expandTilde(root);
|
|
48
|
+
const resolvedRoot = path.resolve(expandedRoot);
|
|
49
|
+
if (!this.allowedRoots.includes(resolvedRoot)) {
|
|
50
|
+
this.allowedRoots.push(resolvedRoot);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
isResolvedPathAllowed(absolutePath, resolvedPath) {
|
|
54
|
+
return this.allowedRoots.some(root => {
|
|
55
|
+
if (resolvedPath === root || resolvedPath.startsWith(root + path.sep)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const realRoot = fs.realpathSync(root);
|
|
60
|
+
if (absolutePath === root) {
|
|
61
|
+
return resolvedPath === realRoot;
|
|
62
|
+
}
|
|
63
|
+
return resolvedPath.startsWith(realRoot + path.sep);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
validate(inputPath) {
|
|
71
|
+
if (!inputPath || inputPath.trim() === '') {
|
|
72
|
+
return {
|
|
73
|
+
isValid: false,
|
|
74
|
+
error: 'Path cannot be empty',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const expandedPath = this.expandTilde(inputPath);
|
|
78
|
+
const absolutePath = path.resolve(expandedPath);
|
|
79
|
+
const isAllowed = this.allowedRoots.some(root => {
|
|
80
|
+
if (absolutePath === root) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return absolutePath.startsWith(root + path.sep);
|
|
84
|
+
});
|
|
85
|
+
if (!isAllowed) {
|
|
86
|
+
return {
|
|
87
|
+
isValid: false,
|
|
88
|
+
error: `Path '${redactPath(inputPath)}' is outside allowed directories`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (shouldIgnore(absolutePath)) {
|
|
92
|
+
return {
|
|
93
|
+
isValid: false,
|
|
94
|
+
error: `Path '${redactPath(inputPath)}' is in an ignored directory or matches an ignored pattern`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const realPath = fs.realpathSync(absolutePath);
|
|
99
|
+
const isRealPathAllowed = this.isResolvedPathAllowed(absolutePath, realPath);
|
|
100
|
+
if (!isRealPathAllowed) {
|
|
101
|
+
return {
|
|
102
|
+
isValid: false,
|
|
103
|
+
error: `Symlink target '${redactPath(realPath)}' is outside allowed directories`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (shouldIgnore(realPath)) {
|
|
107
|
+
return {
|
|
108
|
+
isValid: false,
|
|
109
|
+
error: `Symlink target '${redactPath(realPath)}' is in an ignored directory or matches an ignored pattern`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
isValid: true,
|
|
114
|
+
sanitizedPath: realPath,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error instanceof Error) {
|
|
119
|
+
const nodeError = error;
|
|
120
|
+
if (nodeError.code === 'ENOENT') {
|
|
121
|
+
return this.validateNonExistentPath(absolutePath, inputPath);
|
|
122
|
+
}
|
|
123
|
+
if (nodeError.code === 'EACCES') {
|
|
124
|
+
return {
|
|
125
|
+
isValid: false,
|
|
126
|
+
error: `Permission denied accessing path: ${redactPath(inputPath)}`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (nodeError.code === 'ELOOP') {
|
|
130
|
+
return {
|
|
131
|
+
isValid: false,
|
|
132
|
+
error: `Symlink loop detected at path: ${redactPath(inputPath)}`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (nodeError.code === 'ENAMETOOLONG') {
|
|
136
|
+
return {
|
|
137
|
+
isValid: false,
|
|
138
|
+
error: `Path name too long: ${redactPath(inputPath)}`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
isValid: false,
|
|
144
|
+
error: `Unexpected error validating path: ${redactPath(inputPath)}`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
validateNonExistentPath(absolutePath, inputPath) {
|
|
149
|
+
const { root } = path.parse(absolutePath);
|
|
150
|
+
let ancestor = path.dirname(absolutePath);
|
|
151
|
+
const remainder = [path.basename(absolutePath)];
|
|
152
|
+
while (ancestor !== root && !fs.existsSync(ancestor)) {
|
|
153
|
+
remainder.unshift(path.basename(ancestor));
|
|
154
|
+
ancestor = path.dirname(ancestor);
|
|
155
|
+
}
|
|
156
|
+
let resolvedAncestor;
|
|
157
|
+
try {
|
|
158
|
+
resolvedAncestor = fs.realpathSync(ancestor);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return {
|
|
162
|
+
isValid: false,
|
|
163
|
+
error: `Unexpected error validating path: ${redactPath(inputPath)}`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const resolvedPath = path.join(resolvedAncestor, ...remainder);
|
|
167
|
+
const isAllowed = this.isResolvedPathAllowed(absolutePath, resolvedPath);
|
|
168
|
+
if (!isAllowed) {
|
|
169
|
+
return {
|
|
170
|
+
isValid: false,
|
|
171
|
+
error: `Path '${redactPath(inputPath)}' is outside allowed directories`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (shouldIgnore(absolutePath)) {
|
|
175
|
+
return {
|
|
176
|
+
isValid: false,
|
|
177
|
+
error: `Path '${redactPath(inputPath)}' is in an ignored directory or matches an ignored pattern`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
isValid: true,
|
|
182
|
+
sanitizedPath: resolvedPath,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
async exists(inputPath) {
|
|
186
|
+
const validation = this.validate(inputPath);
|
|
187
|
+
if (!validation.isValid || !validation.sanitizedPath) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
await fs.promises.access(validation.sanitizedPath, fs.constants.R_OK);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async getType(inputPath) {
|
|
199
|
+
const validation = this.validate(inputPath);
|
|
200
|
+
if (!validation.isValid || !validation.sanitizedPath) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const stats = await fs.promises.lstat(validation.sanitizedPath);
|
|
205
|
+
if (stats.isFile())
|
|
206
|
+
return 'file';
|
|
207
|
+
if (stats.isDirectory())
|
|
208
|
+
return 'directory';
|
|
209
|
+
if (stats.isSymbolicLink())
|
|
210
|
+
return 'symlink';
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
getAllowedRoots() {
|
|
218
|
+
return [...this.allowedRoots];
|
|
219
|
+
}
|
|
220
|
+
replaceAllowedRoots(roots) {
|
|
221
|
+
this.allowedRoots = [...roots];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
export const pathValidator = new PathValidator();
|
|
225
|
+
export function resetPathValidator(options) {
|
|
226
|
+
const newValidator = new PathValidator(options);
|
|
227
|
+
pathValidator.replaceAllowedRoots(newValidator.getAllowedRoots());
|
|
228
|
+
return pathValidator;
|
|
229
|
+
}
|