@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,85 @@
|
|
|
1
|
+
export type InitializationOptions = Record<string, unknown>;
|
|
2
|
+
export interface LanguageServerConfig {
|
|
3
|
+
command: string;
|
|
4
|
+
args?: string[];
|
|
5
|
+
workspaceRoot: string;
|
|
6
|
+
languageId?: string;
|
|
7
|
+
initializationOptions?: InitializationOptions;
|
|
8
|
+
env?: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
export interface UserLanguageServerConfig {
|
|
11
|
+
command: string;
|
|
12
|
+
args?: string[];
|
|
13
|
+
languageId: string;
|
|
14
|
+
initializationOptions?: InitializationOptions;
|
|
15
|
+
}
|
|
16
|
+
export interface LanguageServerCommand {
|
|
17
|
+
command: string;
|
|
18
|
+
args: string[];
|
|
19
|
+
languageId: string;
|
|
20
|
+
envVar: string;
|
|
21
|
+
packageName?: string;
|
|
22
|
+
binName?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface FuzzyPosition {
|
|
25
|
+
symbolName: string;
|
|
26
|
+
lineHint?: number;
|
|
27
|
+
orderHint?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface ExactPosition {
|
|
30
|
+
line: number;
|
|
31
|
+
character: number;
|
|
32
|
+
}
|
|
33
|
+
export interface LSPRange {
|
|
34
|
+
start: ExactPosition;
|
|
35
|
+
end: ExactPosition;
|
|
36
|
+
}
|
|
37
|
+
export interface CodeSnippet {
|
|
38
|
+
uri: string;
|
|
39
|
+
range: LSPRange;
|
|
40
|
+
content: string;
|
|
41
|
+
symbolKind?: SymbolKind;
|
|
42
|
+
displayRange?: {
|
|
43
|
+
startLine: number;
|
|
44
|
+
endLine: number;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export type SymbolKind = 'function' | 'method' | 'class' | 'interface' | 'type' | 'variable' | 'constant' | 'property' | 'enum' | 'module' | 'namespace' | 'unknown';
|
|
48
|
+
export interface ReferenceLocation extends CodeSnippet {
|
|
49
|
+
isDefinition?: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface ReferencesByFile {
|
|
52
|
+
uri: string;
|
|
53
|
+
count: number;
|
|
54
|
+
firstLine: number;
|
|
55
|
+
firstCharacter: number;
|
|
56
|
+
lines: number[];
|
|
57
|
+
hasDefinition?: boolean;
|
|
58
|
+
}
|
|
59
|
+
export interface CallHierarchyItem {
|
|
60
|
+
name: string;
|
|
61
|
+
kind: SymbolKind;
|
|
62
|
+
uri: string;
|
|
63
|
+
range: LSPRange;
|
|
64
|
+
selectionRange?: LSPRange;
|
|
65
|
+
content?: string;
|
|
66
|
+
displayRange?: {
|
|
67
|
+
startLine: number;
|
|
68
|
+
endLine: number;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export interface IncomingCall {
|
|
72
|
+
from: CallHierarchyItem;
|
|
73
|
+
fromRanges: LSPRange[];
|
|
74
|
+
}
|
|
75
|
+
export interface OutgoingCall {
|
|
76
|
+
to: CallHierarchyItem;
|
|
77
|
+
fromRanges: LSPRange[];
|
|
78
|
+
}
|
|
79
|
+
export interface LSPPaginationInfo {
|
|
80
|
+
currentPage: number;
|
|
81
|
+
totalPages: number;
|
|
82
|
+
totalResults: number;
|
|
83
|
+
hasMore: boolean;
|
|
84
|
+
resultsPerPage?: number;
|
|
85
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare class UnsafeUriError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export type SafeUriResult = {
|
|
5
|
+
isValid: true;
|
|
6
|
+
path: string;
|
|
7
|
+
} | {
|
|
8
|
+
isValid: false;
|
|
9
|
+
error: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function toUri(filePath: string): string;
|
|
12
|
+
export declare function fromUri(uri: string): string;
|
|
13
|
+
export declare function fromUriSafe(uri: string, options?: {
|
|
14
|
+
throwOnInvalid?: boolean;
|
|
15
|
+
}): SafeUriResult;
|
package/dist/lsp/uri.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { nativeBinding } from './native.js';
|
|
2
|
+
export class UnsafeUriError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = 'UnsafeUriError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function toUri(filePath) {
|
|
9
|
+
return nativeBinding.toUri(filePath);
|
|
10
|
+
}
|
|
11
|
+
export function fromUri(uri) {
|
|
12
|
+
return nativeBinding.fromUri(uri);
|
|
13
|
+
}
|
|
14
|
+
export function fromUriSafe(uri, options) {
|
|
15
|
+
try {
|
|
16
|
+
return { isValid: true, path: nativeBinding.fromUri(uri) };
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
if (options?.throwOnInvalid)
|
|
21
|
+
throw new UnsafeUriError(message);
|
|
22
|
+
return { isValid: false, error: message };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { nativeBinding } from './native.js';
|
|
2
|
+
export async function safeReadFile(filePath) {
|
|
3
|
+
try {
|
|
4
|
+
return nativeBinding.safeReadFile(filePath);
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function validateLSPServerPath(command, _workspaceRoot) {
|
|
11
|
+
try {
|
|
12
|
+
return {
|
|
13
|
+
isValid: true,
|
|
14
|
+
resolvedPath: nativeBinding.validateLspServerPath(command),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
return {
|
|
19
|
+
isValid: false,
|
|
20
|
+
error: error instanceof Error ? error.message : String(error),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { nativeBinding } from './native.js';
|
|
2
|
+
export async function resolveWorkspaceRootForFile(filePath, _workspaceRoot) {
|
|
3
|
+
return nativeBinding.resolveWorkspaceRootForFile(filePath);
|
|
4
|
+
}
|
|
5
|
+
export async function findWorkspaceRoot(filePath) {
|
|
6
|
+
return resolveWorkspaceRootForFile(filePath);
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function normalizeCommandName(command: string): string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function normalizeCommandName(command) {
|
|
2
|
+
if (!command || typeof command !== 'string')
|
|
3
|
+
return command;
|
|
4
|
+
const lastSep = Math.max(command.lastIndexOf('/'), command.lastIndexOf('\\'));
|
|
5
|
+
const base = lastSep >= 0 ? command.slice(lastSep + 1) : command;
|
|
6
|
+
return base.replace(/\.exe$/i, '');
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { normalizeCommandName } from './commandUtils.js';
|
|
2
|
+
export { normalizeCommandName };
|
|
3
|
+
interface CommandValidationResult {
|
|
4
|
+
isValid: boolean;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function validateCommand(command: string, args: string[]): CommandValidationResult;
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { ALLOWED_COMMANDS, DANGEROUS_PATTERNS, PATTERN_DANGEROUS_PATTERNS, } from './securityConstants.js';
|
|
2
|
+
import { securityRegistry } from './registry.js';
|
|
3
|
+
import { normalizeCommandName } from './commandUtils.js';
|
|
4
|
+
export { normalizeCommandName };
|
|
5
|
+
const RG_COMMAND_PATTERN = /^rg(-[a-z0-9-]+)?$/i;
|
|
6
|
+
function isRgCommand(command) {
|
|
7
|
+
return RG_COMMAND_PATTERN.test(command);
|
|
8
|
+
}
|
|
9
|
+
const RG_ALLOWED_FLAGS = new Set([
|
|
10
|
+
'-F',
|
|
11
|
+
'-P',
|
|
12
|
+
'-s',
|
|
13
|
+
'-i',
|
|
14
|
+
'-S',
|
|
15
|
+
'--no-unicode',
|
|
16
|
+
'-w',
|
|
17
|
+
'-v',
|
|
18
|
+
'-a',
|
|
19
|
+
'--binary',
|
|
20
|
+
'-L',
|
|
21
|
+
'-n',
|
|
22
|
+
'--line-number',
|
|
23
|
+
'--column',
|
|
24
|
+
'-l',
|
|
25
|
+
'--files-without-match',
|
|
26
|
+
'--count-matches',
|
|
27
|
+
'-c',
|
|
28
|
+
'--no-ignore',
|
|
29
|
+
'--no-config',
|
|
30
|
+
'--hidden',
|
|
31
|
+
'-U',
|
|
32
|
+
'--multiline-dotall',
|
|
33
|
+
'--json',
|
|
34
|
+
'--stats',
|
|
35
|
+
'--no-mmap',
|
|
36
|
+
'--no-messages',
|
|
37
|
+
'-x',
|
|
38
|
+
'--passthru',
|
|
39
|
+
'--debug',
|
|
40
|
+
]);
|
|
41
|
+
const RG_ALLOWED_SHORT_FLAGS = new Set([...RG_ALLOWED_FLAGS].filter(f => /^-[a-zA-Z]$/.test(f)).map(f => f[1]));
|
|
42
|
+
const RG_ALLOWED_FLAGS_WITH_VALUES = new Set([
|
|
43
|
+
'-g',
|
|
44
|
+
'--glob',
|
|
45
|
+
'--include',
|
|
46
|
+
'--exclude',
|
|
47
|
+
'--exclude-dir',
|
|
48
|
+
'-A',
|
|
49
|
+
'-B',
|
|
50
|
+
'-C',
|
|
51
|
+
'-m',
|
|
52
|
+
'-t',
|
|
53
|
+
'--type',
|
|
54
|
+
'-T',
|
|
55
|
+
'--type-not',
|
|
56
|
+
'--type-add',
|
|
57
|
+
'-j',
|
|
58
|
+
'--threads',
|
|
59
|
+
'--sort',
|
|
60
|
+
'--sortr',
|
|
61
|
+
'--max-filesize',
|
|
62
|
+
'-E',
|
|
63
|
+
'--encoding',
|
|
64
|
+
'--color',
|
|
65
|
+
]);
|
|
66
|
+
const FIND_DISALLOWED_OPERATORS = new Set([
|
|
67
|
+
'-delete',
|
|
68
|
+
'-exec',
|
|
69
|
+
'-execdir',
|
|
70
|
+
'-ok',
|
|
71
|
+
'-okdir',
|
|
72
|
+
'-printf',
|
|
73
|
+
'-fprintf',
|
|
74
|
+
'-fprint',
|
|
75
|
+
'-fprint0',
|
|
76
|
+
'-fls',
|
|
77
|
+
'-ls',
|
|
78
|
+
]);
|
|
79
|
+
const GIT_ALLOWED_SUBCOMMANDS = new Set(['clone', 'sparse-checkout']);
|
|
80
|
+
const GIT_CLONE_ALLOWED_FLAGS = new Set([
|
|
81
|
+
'--depth',
|
|
82
|
+
'--single-branch',
|
|
83
|
+
'--branch',
|
|
84
|
+
'--filter',
|
|
85
|
+
'--sparse',
|
|
86
|
+
'--no-checkout',
|
|
87
|
+
'--quiet',
|
|
88
|
+
'-q',
|
|
89
|
+
'-c',
|
|
90
|
+
'--',
|
|
91
|
+
]);
|
|
92
|
+
const GIT_SPARSE_CHECKOUT_ALLOWED_ACTIONS = new Set([
|
|
93
|
+
'init',
|
|
94
|
+
'set',
|
|
95
|
+
'add',
|
|
96
|
+
'list',
|
|
97
|
+
'disable',
|
|
98
|
+
]);
|
|
99
|
+
const GIT_SPARSE_CHECKOUT_ALLOWED_FLAGS = new Set([
|
|
100
|
+
'--cone',
|
|
101
|
+
'--no-cone',
|
|
102
|
+
'--',
|
|
103
|
+
]);
|
|
104
|
+
const GIT_SAFE_CONFIG_KEYS = new Set([
|
|
105
|
+
'advice.detachedHead',
|
|
106
|
+
'core.autocrlf',
|
|
107
|
+
'core.sparseCheckout',
|
|
108
|
+
'http.extraHeader',
|
|
109
|
+
'http.followRedirects',
|
|
110
|
+
'http.userAgent',
|
|
111
|
+
'http.version',
|
|
112
|
+
]);
|
|
113
|
+
const GIT_BLOCKED_URL_PROTOCOLS = ['file://', 'git://', 'http://'];
|
|
114
|
+
function validateGitConfigKeyValue(keyValue) {
|
|
115
|
+
const eqIndex = keyValue.indexOf('=');
|
|
116
|
+
const key = eqIndex >= 0 ? keyValue.substring(0, eqIndex) : keyValue;
|
|
117
|
+
if (!GIT_SAFE_CONFIG_KEYS.has(key)) {
|
|
118
|
+
return `git config key '${key}' is not allowed via -c`;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
function validateGitCloneUrl(url) {
|
|
123
|
+
for (const protocol of GIT_BLOCKED_URL_PROTOCOLS) {
|
|
124
|
+
if (url.startsWith(protocol)) {
|
|
125
|
+
return `git clone URL protocol '${protocol}' is not allowed`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const FIND_ALLOWED_TOKENS = new Set([
|
|
131
|
+
'-E',
|
|
132
|
+
'-O3',
|
|
133
|
+
'-empty',
|
|
134
|
+
'-executable',
|
|
135
|
+
'-readable',
|
|
136
|
+
'-writable',
|
|
137
|
+
'-prune',
|
|
138
|
+
'-print0',
|
|
139
|
+
'(',
|
|
140
|
+
')',
|
|
141
|
+
'-o',
|
|
142
|
+
]);
|
|
143
|
+
const FIND_ALLOWED_TOKENS_WITH_VALUES = new Set([
|
|
144
|
+
'-maxdepth',
|
|
145
|
+
'-mindepth',
|
|
146
|
+
'-type',
|
|
147
|
+
'-name',
|
|
148
|
+
'-iname',
|
|
149
|
+
'-path',
|
|
150
|
+
'-regex',
|
|
151
|
+
'-regextype',
|
|
152
|
+
'-size',
|
|
153
|
+
'-mtime',
|
|
154
|
+
'-mmin',
|
|
155
|
+
'-atime',
|
|
156
|
+
'-amin',
|
|
157
|
+
'-perm',
|
|
158
|
+
]);
|
|
159
|
+
export function validateCommand(command, args) {
|
|
160
|
+
if (!Array.isArray(args)) {
|
|
161
|
+
return {
|
|
162
|
+
isValid: false,
|
|
163
|
+
error: 'Arguments must be an array',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const normalized = normalizeCommandName(command);
|
|
167
|
+
const extraCmds = securityRegistry.extraAllowedCommands;
|
|
168
|
+
const isBuiltinAllowed = ALLOWED_COMMANDS.includes(normalized);
|
|
169
|
+
const isExtraAllowed = extraCmds.includes(normalized);
|
|
170
|
+
if (!isBuiltinAllowed && !isExtraAllowed) {
|
|
171
|
+
const all = [...ALLOWED_COMMANDS, ...extraCmds];
|
|
172
|
+
return {
|
|
173
|
+
isValid: false,
|
|
174
|
+
error: `Command '${command}' is not allowed. Allowed commands: ${all.join(', ')}`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return validateCommandArgs(normalized, args);
|
|
178
|
+
}
|
|
179
|
+
function validateCommandArgs(command, args) {
|
|
180
|
+
if (isRgCommand(command)) {
|
|
181
|
+
const disallowedFlag = findDisallowedRgFlag(args);
|
|
182
|
+
if (disallowedFlag) {
|
|
183
|
+
return {
|
|
184
|
+
isValid: false,
|
|
185
|
+
error: `rg option '${disallowedFlag}' is not allowed.`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else if (command === 'git') {
|
|
190
|
+
const gitError = validateGitArgs(args);
|
|
191
|
+
if (gitError) {
|
|
192
|
+
return { isValid: false, error: gitError };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else if (command === 'find') {
|
|
196
|
+
const invalidFindArg = findInvalidFindArg(args);
|
|
197
|
+
if (invalidFindArg) {
|
|
198
|
+
return {
|
|
199
|
+
isValid: false,
|
|
200
|
+
error: `find operator '${invalidFindArg}' is not allowed.`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const patternPositions = getPatternArgPositions(command, args);
|
|
205
|
+
for (let i = 0; i < args.length; i++) {
|
|
206
|
+
const arg = args[i];
|
|
207
|
+
const isPattern = patternPositions.has(i);
|
|
208
|
+
const dangerousPatterns = isPattern
|
|
209
|
+
? PATTERN_DANGEROUS_PATTERNS
|
|
210
|
+
: DANGEROUS_PATTERNS;
|
|
211
|
+
for (const dangerousPattern of dangerousPatterns) {
|
|
212
|
+
if (dangerousPattern.test(arg)) {
|
|
213
|
+
const argType = isPattern ? 'search pattern' : 'argument';
|
|
214
|
+
return {
|
|
215
|
+
isValid: false,
|
|
216
|
+
error: `Dangerous pattern detected in ${argType}: '${arg}'. This may be a command injection attempt.`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { isValid: true };
|
|
222
|
+
}
|
|
223
|
+
const RG_GLOB_FLAGS = new Set([
|
|
224
|
+
'-g',
|
|
225
|
+
'--glob',
|
|
226
|
+
'--include',
|
|
227
|
+
'--exclude',
|
|
228
|
+
'--exclude-dir',
|
|
229
|
+
]);
|
|
230
|
+
function getRgPatternPositions(args) {
|
|
231
|
+
const positions = new Set();
|
|
232
|
+
let foundPattern = false;
|
|
233
|
+
for (let i = 0; i < args.length; i++) {
|
|
234
|
+
const arg = args[i];
|
|
235
|
+
if (arg === '--') {
|
|
236
|
+
if (i + 1 < args.length)
|
|
237
|
+
positions.add(i + 1);
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
if (arg.startsWith('-')) {
|
|
241
|
+
if (RG_GLOB_FLAGS.has(arg)) {
|
|
242
|
+
i++;
|
|
243
|
+
positions.add(i);
|
|
244
|
+
}
|
|
245
|
+
else if (RG_ALLOWED_FLAGS_WITH_VALUES.has(arg)) {
|
|
246
|
+
i++;
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (!foundPattern) {
|
|
251
|
+
positions.add(i);
|
|
252
|
+
foundPattern = true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return positions;
|
|
256
|
+
}
|
|
257
|
+
function getGrepPatternPositions(args) {
|
|
258
|
+
const positions = new Set();
|
|
259
|
+
let foundPattern = false;
|
|
260
|
+
for (let i = 0; i < args.length; i++) {
|
|
261
|
+
const arg = args[i];
|
|
262
|
+
if (arg === '--') {
|
|
263
|
+
if (i + 1 < args.length)
|
|
264
|
+
positions.add(i + 1);
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
if (arg.startsWith('--include=') || arg.startsWith('--exclude=')) {
|
|
268
|
+
positions.add(i);
|
|
269
|
+
}
|
|
270
|
+
else if (!arg.startsWith('-') && !foundPattern) {
|
|
271
|
+
positions.add(i);
|
|
272
|
+
foundPattern = true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return positions;
|
|
276
|
+
}
|
|
277
|
+
const FIND_PATTERN_ARGS = new Set([
|
|
278
|
+
'-name',
|
|
279
|
+
'-iname',
|
|
280
|
+
'-path',
|
|
281
|
+
'-regex',
|
|
282
|
+
'-size',
|
|
283
|
+
'-perm',
|
|
284
|
+
]);
|
|
285
|
+
function getFindPatternPositions(args) {
|
|
286
|
+
const positions = new Set();
|
|
287
|
+
for (let i = 0; i < args.length; i++) {
|
|
288
|
+
const arg = args[i];
|
|
289
|
+
const prevArg = i > 0 ? args[i - 1] : '';
|
|
290
|
+
if (FIND_PATTERN_ARGS.has(prevArg))
|
|
291
|
+
positions.add(i);
|
|
292
|
+
if (arg === '(' || arg === ')' || arg === '-o')
|
|
293
|
+
positions.add(i);
|
|
294
|
+
}
|
|
295
|
+
return positions;
|
|
296
|
+
}
|
|
297
|
+
function getPatternArgPositions(command, args) {
|
|
298
|
+
switch (true) {
|
|
299
|
+
case isRgCommand(command):
|
|
300
|
+
return getRgPatternPositions(args);
|
|
301
|
+
case command === 'grep':
|
|
302
|
+
return getGrepPatternPositions(args);
|
|
303
|
+
case command === 'find':
|
|
304
|
+
return getFindPatternPositions(args);
|
|
305
|
+
default:
|
|
306
|
+
return new Set();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function findDisallowedRgFlag(args) {
|
|
310
|
+
for (let i = 0; i < args.length; i++) {
|
|
311
|
+
const arg = args[i];
|
|
312
|
+
if (arg === '--') {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
if (!arg.startsWith('-')) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
if (arg.startsWith('--pre') || arg.startsWith('--pre-glob')) {
|
|
319
|
+
return arg;
|
|
320
|
+
}
|
|
321
|
+
if (RG_ALLOWED_FLAGS_WITH_VALUES.has(arg)) {
|
|
322
|
+
i++;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (RG_ALLOWED_FLAGS.has(arg)) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (/^-[a-zA-Z]{2,}$/.test(arg)) {
|
|
329
|
+
const chars = arg.slice(1);
|
|
330
|
+
const allAllowed = [...chars].every(ch => RG_ALLOWED_SHORT_FLAGS.has(ch));
|
|
331
|
+
if (allAllowed) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
return arg;
|
|
335
|
+
}
|
|
336
|
+
return arg;
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
function validateGitArgs(args) {
|
|
341
|
+
if (args.length === 0) {
|
|
342
|
+
return 'git command requires a subcommand';
|
|
343
|
+
}
|
|
344
|
+
let subcommandIndex = 0;
|
|
345
|
+
while (subcommandIndex < args.length) {
|
|
346
|
+
const arg = args[subcommandIndex];
|
|
347
|
+
if (arg === '-c') {
|
|
348
|
+
const keyValue = args[subcommandIndex + 1];
|
|
349
|
+
if (keyValue !== undefined) {
|
|
350
|
+
const configError = validateGitConfigKeyValue(keyValue);
|
|
351
|
+
if (configError)
|
|
352
|
+
return configError;
|
|
353
|
+
}
|
|
354
|
+
subcommandIndex += 2;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (arg === '-C') {
|
|
358
|
+
subcommandIndex += 2;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
if (subcommandIndex >= args.length) {
|
|
364
|
+
return 'git command requires a subcommand';
|
|
365
|
+
}
|
|
366
|
+
const subcommand = args[subcommandIndex];
|
|
367
|
+
if (!GIT_ALLOWED_SUBCOMMANDS.has(subcommand)) {
|
|
368
|
+
return `git subcommand '${subcommand}' is not allowed. Allowed: ${[...GIT_ALLOWED_SUBCOMMANDS].join(', ')}`;
|
|
369
|
+
}
|
|
370
|
+
if (subcommand === 'clone') {
|
|
371
|
+
return validateGitCloneArgs(args, subcommandIndex);
|
|
372
|
+
}
|
|
373
|
+
else if (subcommand === 'sparse-checkout') {
|
|
374
|
+
return validateGitSparseCheckoutArgs(args, subcommandIndex);
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
const GIT_CLONE_FLAGS_WITH_VALUES = new Set([
|
|
379
|
+
'--depth',
|
|
380
|
+
'--branch',
|
|
381
|
+
'--filter',
|
|
382
|
+
]);
|
|
383
|
+
function processCloneFlag(args, i) {
|
|
384
|
+
const arg = args[i];
|
|
385
|
+
if (!GIT_CLONE_ALLOWED_FLAGS.has(arg)) {
|
|
386
|
+
return { skip: 0, error: `git clone flag '${arg}' is not allowed` };
|
|
387
|
+
}
|
|
388
|
+
if (GIT_CLONE_FLAGS_WITH_VALUES.has(arg))
|
|
389
|
+
return { skip: 1 };
|
|
390
|
+
if (arg === '-c' && i + 1 < args.length) {
|
|
391
|
+
const configError = validateGitConfigKeyValue(args[i + 1]);
|
|
392
|
+
if (configError)
|
|
393
|
+
return { skip: 1, error: configError };
|
|
394
|
+
return { skip: 1 };
|
|
395
|
+
}
|
|
396
|
+
return { skip: 0 };
|
|
397
|
+
}
|
|
398
|
+
function validateGitCloneArgs(args, subcommandIndex) {
|
|
399
|
+
let pastEndOfFlags = false;
|
|
400
|
+
let urlFound = false;
|
|
401
|
+
for (let i = subcommandIndex + 1; i < args.length; i++) {
|
|
402
|
+
const arg = args[i];
|
|
403
|
+
if (arg === '--') {
|
|
404
|
+
pastEndOfFlags = true;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (pastEndOfFlags || !arg.startsWith('-')) {
|
|
408
|
+
if (!urlFound) {
|
|
409
|
+
const urlError = validateGitCloneUrl(arg);
|
|
410
|
+
if (urlError)
|
|
411
|
+
return urlError;
|
|
412
|
+
urlFound = true;
|
|
413
|
+
}
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const { skip, error } = processCloneFlag(args, i);
|
|
417
|
+
if (error)
|
|
418
|
+
return error;
|
|
419
|
+
i += skip;
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
function validateGitSubcommandFlags(args, startIndex, allowedFlags, label) {
|
|
424
|
+
for (let i = startIndex; i < args.length; i++) {
|
|
425
|
+
const arg = args[i];
|
|
426
|
+
if (!arg.startsWith('-'))
|
|
427
|
+
continue;
|
|
428
|
+
if (!allowedFlags.has(arg)) {
|
|
429
|
+
return `git ${label} flag '${arg}' is not allowed`;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
function validateGitSparseCheckoutArgs(args, subcommandIndex) {
|
|
435
|
+
const actionIndex = subcommandIndex + 1;
|
|
436
|
+
if (actionIndex >= args.length) {
|
|
437
|
+
return 'git sparse-checkout requires an action (init, set, add, list, disable)';
|
|
438
|
+
}
|
|
439
|
+
const action = args[actionIndex];
|
|
440
|
+
if (!GIT_SPARSE_CHECKOUT_ALLOWED_ACTIONS.has(action)) {
|
|
441
|
+
return `git sparse-checkout action '${action}' is not allowed`;
|
|
442
|
+
}
|
|
443
|
+
return validateGitSubcommandFlags(args, actionIndex + 1, GIT_SPARSE_CHECKOUT_ALLOWED_FLAGS, 'sparse-checkout');
|
|
444
|
+
}
|
|
445
|
+
function findInvalidFindArg(args) {
|
|
446
|
+
let afterPathArgs = false;
|
|
447
|
+
for (let i = 0; i < args.length; i++) {
|
|
448
|
+
const arg = args[i];
|
|
449
|
+
if (arg === '--') {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (!afterPathArgs && !arg.startsWith('-') && arg !== '(' && arg !== ')') {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
afterPathArgs = true;
|
|
456
|
+
if (FIND_DISALLOWED_OPERATORS.has(arg)) {
|
|
457
|
+
return arg;
|
|
458
|
+
}
|
|
459
|
+
if (FIND_ALLOWED_TOKENS_WITH_VALUES.has(arg)) {
|
|
460
|
+
i++;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (FIND_ALLOWED_TOKENS.has(arg)) {
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (!arg.startsWith('-')) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
return arg;
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
472
|
+
}
|