grokcodecli 0.1.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/.claude/settings.local.json +32 -0
- package/README.md +1464 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +61 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/loader.d.ts +34 -0
- package/dist/commands/loader.d.ts.map +1 -0
- package/dist/commands/loader.js +192 -0
- package/dist/commands/loader.js.map +1 -0
- package/dist/config/manager.d.ts +21 -0
- package/dist/config/manager.d.ts.map +1 -0
- package/dist/config/manager.js +203 -0
- package/dist/config/manager.js.map +1 -0
- package/dist/conversation/chat.d.ts +50 -0
- package/dist/conversation/chat.d.ts.map +1 -0
- package/dist/conversation/chat.js +1145 -0
- package/dist/conversation/chat.js.map +1 -0
- package/dist/conversation/history.d.ts +24 -0
- package/dist/conversation/history.d.ts.map +1 -0
- package/dist/conversation/history.js +103 -0
- package/dist/conversation/history.js.map +1 -0
- package/dist/grok/client.d.ts +86 -0
- package/dist/grok/client.d.ts.map +1 -0
- package/dist/grok/client.js +106 -0
- package/dist/grok/client.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/permissions/manager.d.ts +26 -0
- package/dist/permissions/manager.d.ts.map +1 -0
- package/dist/permissions/manager.js +170 -0
- package/dist/permissions/manager.js.map +1 -0
- package/dist/tools/bash.d.ts +8 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +102 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/edit.d.ts +9 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +61 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/glob.d.ts +7 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/glob.js +38 -0
- package/dist/tools/glob.js.map +1 -0
- package/dist/tools/grep.d.ts +8 -0
- package/dist/tools/grep.d.ts.map +1 -0
- package/dist/tools/grep.js +78 -0
- package/dist/tools/grep.js.map +1 -0
- package/dist/tools/read.d.ts +8 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +96 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/registry.d.ts +42 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +230 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/webfetch.d.ts +10 -0
- package/dist/tools/webfetch.d.ts.map +1 -0
- package/dist/tools/webfetch.js +108 -0
- package/dist/tools/webfetch.js.map +1 -0
- package/dist/tools/websearch.d.ts +7 -0
- package/dist/tools/websearch.d.ts.map +1 -0
- package/dist/tools/websearch.js +180 -0
- package/dist/tools/websearch.js.map +1 -0
- package/dist/tools/write.d.ts +7 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +80 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/utils/security.d.ts +36 -0
- package/dist/utils/security.d.ts.map +1 -0
- package/dist/utils/security.js +227 -0
- package/dist/utils/security.js.map +1 -0
- package/dist/utils/ui.d.ts +49 -0
- package/dist/utils/ui.d.ts.map +1 -0
- package/dist/utils/ui.js +302 -0
- package/dist/utils/ui.js.map +1 -0
- package/package.json +45 -0
- package/src/cli.ts +68 -0
- package/src/commands/loader.ts +244 -0
- package/src/config/manager.ts +239 -0
- package/src/conversation/chat.ts +1294 -0
- package/src/conversation/history.ts +131 -0
- package/src/grok/client.ts +192 -0
- package/src/index.ts +8 -0
- package/src/permissions/manager.ts +208 -0
- package/src/tools/bash.ts +119 -0
- package/src/tools/edit.ts +73 -0
- package/src/tools/glob.ts +49 -0
- package/src/tools/grep.ts +96 -0
- package/src/tools/read.ts +116 -0
- package/src/tools/registry.ts +248 -0
- package/src/tools/webfetch.ts +127 -0
- package/src/tools/websearch.ts +219 -0
- package/src/tools/write.ts +94 -0
- package/src/utils/security.ts +259 -0
- package/src/utils/ui.ts +382 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { ToolResult } from './registry.js';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
export interface WebSearchToolParams {
|
|
5
|
+
query: string;
|
|
6
|
+
num_results?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface SearchResult {
|
|
10
|
+
title: string;
|
|
11
|
+
url: string;
|
|
12
|
+
snippet: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// DuckDuckGo HTML search (no API key needed)
|
|
16
|
+
async function searchDuckDuckGo(query: string, numResults: number = 10): Promise<SearchResult[]> {
|
|
17
|
+
const results: SearchResult[] = [];
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Use DuckDuckGo HTML version
|
|
21
|
+
const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
22
|
+
|
|
23
|
+
const response = await fetch(searchUrl, {
|
|
24
|
+
headers: {
|
|
25
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
|
26
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
27
|
+
'Accept-Language': 'en-US,en;q=0.5',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new Error(`Search failed: ${response.status} ${response.statusText}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const html = await response.text();
|
|
36
|
+
|
|
37
|
+
// Parse results from HTML
|
|
38
|
+
// DuckDuckGo HTML results are in <a class="result__a"> tags
|
|
39
|
+
const resultPattern = /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/gi;
|
|
40
|
+
const snippetPattern = /<a[^>]*class="result__snippet"[^>]*>([^<]*(?:<[^>]*>[^<]*)*)<\/a>/gi;
|
|
41
|
+
|
|
42
|
+
// Alternative: Parse result blocks
|
|
43
|
+
const resultBlocks = html.split('<div class="result results_links results_links_deep web-result');
|
|
44
|
+
|
|
45
|
+
for (let i = 1; i < resultBlocks.length && results.length < numResults; i++) {
|
|
46
|
+
const block = resultBlocks[i];
|
|
47
|
+
|
|
48
|
+
// Extract URL
|
|
49
|
+
const urlMatch = block.match(/href="([^"]+)"/);
|
|
50
|
+
// Extract title
|
|
51
|
+
const titleMatch = block.match(/class="result__a"[^>]*>([^<]+)</);
|
|
52
|
+
// Extract snippet
|
|
53
|
+
const snippetMatch = block.match(/class="result__snippet"[^>]*>([^<]+)/);
|
|
54
|
+
|
|
55
|
+
if (urlMatch && titleMatch) {
|
|
56
|
+
let url = urlMatch[1];
|
|
57
|
+
// DuckDuckGo uses redirect URLs, try to extract actual URL
|
|
58
|
+
if (url.includes('uddg=')) {
|
|
59
|
+
const actualUrl = url.match(/uddg=([^&]+)/);
|
|
60
|
+
if (actualUrl) {
|
|
61
|
+
url = decodeURIComponent(actualUrl[1]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
results.push({
|
|
66
|
+
title: decodeHtmlEntities(titleMatch[1].trim()),
|
|
67
|
+
url: url,
|
|
68
|
+
snippet: snippetMatch ? decodeHtmlEntities(snippetMatch[1].trim()) : '',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// If no results found with block parsing, try simpler approach
|
|
74
|
+
if (results.length === 0) {
|
|
75
|
+
const links = html.matchAll(/<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/gi);
|
|
76
|
+
for (const match of links) {
|
|
77
|
+
if (results.length >= numResults) break;
|
|
78
|
+
let url = match[1];
|
|
79
|
+
if (url.includes('uddg=')) {
|
|
80
|
+
const actualUrl = url.match(/uddg=([^&]+)/);
|
|
81
|
+
if (actualUrl) {
|
|
82
|
+
url = decodeURIComponent(actualUrl[1]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
results.push({
|
|
86
|
+
title: decodeHtmlEntities(match[2].trim()),
|
|
87
|
+
url: url,
|
|
88
|
+
snippet: '',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
} catch (error) {
|
|
94
|
+
// Fallback: try alternative search
|
|
95
|
+
return await searchWithAlternative(query, numResults);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return results;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Fallback using a simple web scraping approach
|
|
102
|
+
async function searchWithAlternative(query: string, numResults: number): Promise<SearchResult[]> {
|
|
103
|
+
const results: SearchResult[] = [];
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
// Try using DuckDuckGo Lite
|
|
107
|
+
const searchUrl = `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`;
|
|
108
|
+
|
|
109
|
+
const response = await fetch(searchUrl, {
|
|
110
|
+
headers: {
|
|
111
|
+
'User-Agent': 'Mozilla/5.0 (compatible; GrokCode/1.0)',
|
|
112
|
+
'Accept': 'text/html',
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
return results;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const html = await response.text();
|
|
121
|
+
|
|
122
|
+
// Parse lite version results
|
|
123
|
+
const linkMatches = html.matchAll(/<a[^>]*rel="nofollow"[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>/gi);
|
|
124
|
+
|
|
125
|
+
for (const match of linkMatches) {
|
|
126
|
+
if (results.length >= numResults) break;
|
|
127
|
+
const url = match[1];
|
|
128
|
+
const title = match[2];
|
|
129
|
+
|
|
130
|
+
// Skip DuckDuckGo internal links
|
|
131
|
+
if (url.startsWith('/') || url.includes('duckduckgo.com')) continue;
|
|
132
|
+
|
|
133
|
+
results.push({
|
|
134
|
+
title: decodeHtmlEntities(title.trim()),
|
|
135
|
+
url: url,
|
|
136
|
+
snippet: '',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// Return empty results if all methods fail
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return results;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function decodeHtmlEntities(text: string): string {
|
|
147
|
+
return text
|
|
148
|
+
.replace(/&/g, '&')
|
|
149
|
+
.replace(/</g, '<')
|
|
150
|
+
.replace(/>/g, '>')
|
|
151
|
+
.replace(/"/g, '"')
|
|
152
|
+
.replace(/'/g, "'")
|
|
153
|
+
.replace(/ /g, ' ')
|
|
154
|
+
.replace(/'/g, "'")
|
|
155
|
+
.replace(///g, '/')
|
|
156
|
+
.replace(/'/g, "'");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function webSearchTool(params: WebSearchToolParams): Promise<ToolResult> {
|
|
160
|
+
try {
|
|
161
|
+
const query = params.query.trim();
|
|
162
|
+
const numResults = Math.min(params.num_results ?? 10, 20);
|
|
163
|
+
|
|
164
|
+
if (!query) {
|
|
165
|
+
return {
|
|
166
|
+
success: false,
|
|
167
|
+
output: '',
|
|
168
|
+
error: 'Search query cannot be empty',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (query.length > 500) {
|
|
173
|
+
return {
|
|
174
|
+
success: false,
|
|
175
|
+
output: '',
|
|
176
|
+
error: 'Search query too long (maximum 500 characters)',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const results = await searchDuckDuckGo(query, numResults);
|
|
181
|
+
|
|
182
|
+
if (results.length === 0) {
|
|
183
|
+
return {
|
|
184
|
+
success: true,
|
|
185
|
+
output: `${chalk.yellow('No results found for:')} "${query}"\n\nTry:\n • Using different keywords\n • Checking spelling\n • Using fewer, more general terms`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let output = `${chalk.cyan('🔍 Search Results for:')} "${query}"\n`;
|
|
190
|
+
output += `${chalk.gray(`Found ${results.length} results`)}\n\n`;
|
|
191
|
+
|
|
192
|
+
results.forEach((result, index) => {
|
|
193
|
+
output += `${chalk.bold(`${index + 1}. ${result.title}`)}\n`;
|
|
194
|
+
output += ` ${chalk.blue(result.url)}\n`;
|
|
195
|
+
if (result.snippet) {
|
|
196
|
+
output += ` ${chalk.gray(result.snippet)}\n`;
|
|
197
|
+
}
|
|
198
|
+
output += '\n';
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Add sources section like Claude Code
|
|
202
|
+
output += `${chalk.cyan('Sources:')}\n`;
|
|
203
|
+
results.forEach((result) => {
|
|
204
|
+
output += ` • [${result.title}](${result.url})\n`;
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
success: true,
|
|
209
|
+
output,
|
|
210
|
+
};
|
|
211
|
+
} catch (error) {
|
|
212
|
+
const err = error as Error;
|
|
213
|
+
return {
|
|
214
|
+
success: false,
|
|
215
|
+
output: '',
|
|
216
|
+
error: `Search failed: ${err.message}`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { ToolResult } from './registry.js';
|
|
4
|
+
import { validatePath } from '../utils/security.js';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
|
|
7
|
+
export interface WriteToolParams {
|
|
8
|
+
file_path: string;
|
|
9
|
+
content: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Maximum file size to write (50MB)
|
|
13
|
+
const MAX_CONTENT_SIZE = 50 * 1024 * 1024;
|
|
14
|
+
|
|
15
|
+
export async function writeTool(params: WriteToolParams): Promise<ToolResult> {
|
|
16
|
+
try {
|
|
17
|
+
const filePath = path.resolve(params.file_path);
|
|
18
|
+
|
|
19
|
+
// Security validation
|
|
20
|
+
const security = validatePath(filePath);
|
|
21
|
+
if (!security.allowed) {
|
|
22
|
+
return {
|
|
23
|
+
success: false,
|
|
24
|
+
output: '',
|
|
25
|
+
error: `Security: ${security.reason}`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check content size
|
|
30
|
+
if (params.content.length > MAX_CONTENT_SIZE) {
|
|
31
|
+
return {
|
|
32
|
+
success: false,
|
|
33
|
+
output: '',
|
|
34
|
+
error: `Content too large (${(params.content.length / 1024 / 1024).toFixed(1)}MB). Maximum is ${MAX_CONTENT_SIZE / 1024 / 1024}MB.`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check if file exists (for backup/warning)
|
|
39
|
+
let wasExisting = false;
|
|
40
|
+
let previousSize = 0;
|
|
41
|
+
try {
|
|
42
|
+
const stats = await fs.stat(filePath);
|
|
43
|
+
wasExisting = true;
|
|
44
|
+
previousSize = stats.size;
|
|
45
|
+
} catch {
|
|
46
|
+
// File doesn't exist, that's fine
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Ensure directory exists
|
|
50
|
+
const dir = path.dirname(filePath);
|
|
51
|
+
await fs.mkdir(dir, { recursive: true });
|
|
52
|
+
|
|
53
|
+
// Write the file
|
|
54
|
+
await fs.writeFile(filePath, params.content, 'utf-8');
|
|
55
|
+
|
|
56
|
+
const lines = params.content.split('\n').length;
|
|
57
|
+
const size = params.content.length;
|
|
58
|
+
|
|
59
|
+
let output = `${chalk.green('✓')} File written: ${filePath}\n`;
|
|
60
|
+
output += ` ${chalk.gray('Lines:')} ${lines}\n`;
|
|
61
|
+
output += ` ${chalk.gray('Size:')} ${formatSize(size)}`;
|
|
62
|
+
|
|
63
|
+
if (wasExisting) {
|
|
64
|
+
const diff = size - previousSize;
|
|
65
|
+
const diffStr = diff >= 0 ? `+${formatSize(diff)}` : `-${formatSize(Math.abs(diff))}`;
|
|
66
|
+
output += ` (${diffStr} from previous)`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Security warning if applicable
|
|
70
|
+
if (security.severity === 'medium') {
|
|
71
|
+
output = chalk.yellow(`⚠️ ${security.suggestion}\n`) + output;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
success: true,
|
|
76
|
+
output,
|
|
77
|
+
};
|
|
78
|
+
} catch (error) {
|
|
79
|
+
const err = error as NodeJS.ErrnoException;
|
|
80
|
+
if (err.code === 'EACCES') {
|
|
81
|
+
return { success: false, output: '', error: `Permission denied: ${params.file_path}` };
|
|
82
|
+
}
|
|
83
|
+
if (err.code === 'ENOSPC') {
|
|
84
|
+
return { success: false, output: '', error: 'No space left on device' };
|
|
85
|
+
}
|
|
86
|
+
return { success: false, output: '', error: `Error writing file: ${err.message}` };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function formatSize(bytes: number): string {
|
|
91
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
92
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
93
|
+
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
94
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides input validation, path traversal prevention, and security checks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
|
|
10
|
+
// Dangerous patterns that should be flagged
|
|
11
|
+
const DANGEROUS_COMMANDS = [
|
|
12
|
+
/rm\s+-rf\s+[\/~]/i, // rm -rf with root or home
|
|
13
|
+
/rm\s+-rf\s+\*/, // rm -rf *
|
|
14
|
+
/>\s*\/dev\/sd/, // Writing to disk devices
|
|
15
|
+
/mkfs\./, // Formatting filesystems
|
|
16
|
+
/dd\s+if=.*of=\/dev/, // dd to device
|
|
17
|
+
/chmod\s+-R\s+777/, // Recursive 777
|
|
18
|
+
/curl.*\|\s*sh/, // Curl pipe to shell
|
|
19
|
+
/wget.*\|\s*sh/, // Wget pipe to shell
|
|
20
|
+
/curl.*\|\s*bash/,
|
|
21
|
+
/wget.*\|\s*bash/,
|
|
22
|
+
/:(){.*};:/, // Fork bomb pattern
|
|
23
|
+
/>\s*\/etc\//, // Writing to /etc
|
|
24
|
+
/>\s*\/boot\//, // Writing to /boot
|
|
25
|
+
/>\s*\/proc\//, // Writing to /proc
|
|
26
|
+
/>\s*\/sys\//, // Writing to /sys
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// Sensitive file patterns
|
|
30
|
+
const SENSITIVE_FILES = [
|
|
31
|
+
/\.env$/,
|
|
32
|
+
/\.env\./,
|
|
33
|
+
/credentials/i,
|
|
34
|
+
/secret/i,
|
|
35
|
+
/password/i,
|
|
36
|
+
/private_key/,
|
|
37
|
+
/\.pem$/,
|
|
38
|
+
/\.key$/,
|
|
39
|
+
/id_rsa/,
|
|
40
|
+
/id_ed25519/,
|
|
41
|
+
/\.ssh\/config/,
|
|
42
|
+
/\.netrc/,
|
|
43
|
+
/\.npmrc/,
|
|
44
|
+
/\.pypirc/,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// Blocked paths (never allow access)
|
|
48
|
+
const BLOCKED_PATHS = [
|
|
49
|
+
'/etc/passwd',
|
|
50
|
+
'/etc/shadow',
|
|
51
|
+
'/etc/sudoers',
|
|
52
|
+
'/etc/ssh/',
|
|
53
|
+
'/root/.ssh/',
|
|
54
|
+
'/proc/',
|
|
55
|
+
'/sys/',
|
|
56
|
+
'/dev/',
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
export interface SecurityCheckResult {
|
|
60
|
+
allowed: boolean;
|
|
61
|
+
reason?: string;
|
|
62
|
+
severity?: 'low' | 'medium' | 'high' | 'critical';
|
|
63
|
+
suggestion?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Validate a file path for security issues
|
|
68
|
+
*/
|
|
69
|
+
export function validatePath(filePath: string, allowedRoots?: string[]): SecurityCheckResult {
|
|
70
|
+
// Resolve to absolute path
|
|
71
|
+
const resolved = path.resolve(filePath);
|
|
72
|
+
const normalized = path.normalize(resolved);
|
|
73
|
+
|
|
74
|
+
// Check for path traversal attempts
|
|
75
|
+
if (filePath.includes('..')) {
|
|
76
|
+
// Allow if the resolved path is still within allowed roots
|
|
77
|
+
if (allowedRoots && allowedRoots.length > 0) {
|
|
78
|
+
const isWithinRoots = allowedRoots.some(root =>
|
|
79
|
+
normalized.startsWith(path.resolve(root))
|
|
80
|
+
);
|
|
81
|
+
if (!isWithinRoots) {
|
|
82
|
+
return {
|
|
83
|
+
allowed: false,
|
|
84
|
+
reason: 'Path traversal detected - path escapes allowed directories',
|
|
85
|
+
severity: 'high',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check blocked paths
|
|
92
|
+
for (const blocked of BLOCKED_PATHS) {
|
|
93
|
+
if (normalized.startsWith(blocked) || normalized === blocked.slice(0, -1)) {
|
|
94
|
+
return {
|
|
95
|
+
allowed: false,
|
|
96
|
+
reason: `Access to ${blocked} is blocked for security`,
|
|
97
|
+
severity: 'critical',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check for sensitive files (warning only)
|
|
103
|
+
for (const pattern of SENSITIVE_FILES) {
|
|
104
|
+
if (pattern.test(normalized)) {
|
|
105
|
+
return {
|
|
106
|
+
allowed: true, // Allow but warn
|
|
107
|
+
reason: 'This appears to be a sensitive file',
|
|
108
|
+
severity: 'medium',
|
|
109
|
+
suggestion: 'Be careful with files containing credentials or secrets',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { allowed: true };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Validate a bash command for dangerous patterns
|
|
119
|
+
*/
|
|
120
|
+
export function validateCommand(command: string): SecurityCheckResult {
|
|
121
|
+
// Check for dangerous patterns
|
|
122
|
+
for (const pattern of DANGEROUS_COMMANDS) {
|
|
123
|
+
if (pattern.test(command)) {
|
|
124
|
+
return {
|
|
125
|
+
allowed: false,
|
|
126
|
+
reason: 'Command matches a dangerous pattern',
|
|
127
|
+
severity: 'critical',
|
|
128
|
+
suggestion: 'This command could cause system damage. Please review carefully.',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check for commands that modify critical paths
|
|
134
|
+
if (/>\s*(\/etc|\/boot|\/root|\/var\/log)/i.test(command)) {
|
|
135
|
+
return {
|
|
136
|
+
allowed: false,
|
|
137
|
+
reason: 'Writing to system directories is blocked',
|
|
138
|
+
severity: 'high',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check for privilege escalation
|
|
143
|
+
if (/sudo\s+-s|su\s+-|sudo\s+su/.test(command)) {
|
|
144
|
+
return {
|
|
145
|
+
allowed: true,
|
|
146
|
+
reason: 'Command requires elevated privileges',
|
|
147
|
+
severity: 'medium',
|
|
148
|
+
suggestion: 'This command will prompt for sudo password',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { allowed: true };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Validate a URL for security issues
|
|
157
|
+
*/
|
|
158
|
+
export function validateUrl(url: string): SecurityCheckResult {
|
|
159
|
+
try {
|
|
160
|
+
const parsed = new URL(url);
|
|
161
|
+
|
|
162
|
+
// Only allow http/https
|
|
163
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
164
|
+
return {
|
|
165
|
+
allowed: false,
|
|
166
|
+
reason: `Protocol ${parsed.protocol} is not allowed`,
|
|
167
|
+
severity: 'medium',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Block localhost/internal IPs (SSRF prevention)
|
|
172
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
173
|
+
const blockedHosts = ['localhost', '127.0.0.1', '0.0.0.0', '::1'];
|
|
174
|
+
const internalPatterns = [
|
|
175
|
+
/^10\./,
|
|
176
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
|
177
|
+
/^192\.168\./,
|
|
178
|
+
/^169\.254\./,
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
if (blockedHosts.includes(hostname)) {
|
|
182
|
+
return {
|
|
183
|
+
allowed: false,
|
|
184
|
+
reason: 'Localhost access is blocked',
|
|
185
|
+
severity: 'high',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const pattern of internalPatterns) {
|
|
190
|
+
if (pattern.test(hostname)) {
|
|
191
|
+
return {
|
|
192
|
+
allowed: false,
|
|
193
|
+
reason: 'Internal network access is blocked',
|
|
194
|
+
severity: 'high',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { allowed: true };
|
|
200
|
+
} catch {
|
|
201
|
+
return {
|
|
202
|
+
allowed: false,
|
|
203
|
+
reason: 'Invalid URL format',
|
|
204
|
+
severity: 'low',
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Sanitize output for display (prevent terminal escape sequences)
|
|
211
|
+
*/
|
|
212
|
+
export function sanitizeOutput(output: string): string {
|
|
213
|
+
// Remove potentially dangerous escape sequences but keep basic formatting
|
|
214
|
+
return output
|
|
215
|
+
// Remove cursor manipulation
|
|
216
|
+
.replace(/\x1b\[\d*[ABCDJK]/g, '')
|
|
217
|
+
// Remove window title changes
|
|
218
|
+
.replace(/\x1b\]0;[^\x07]*\x07/g, '')
|
|
219
|
+
// Remove scrolling region changes
|
|
220
|
+
.replace(/\x1b\[\d*;\d*r/g, '')
|
|
221
|
+
// Keep basic colors (safe)
|
|
222
|
+
.replace(/\x1b\[(\d+;)*\d*[mK]/g, (match) => {
|
|
223
|
+
// Only allow color codes 0-107
|
|
224
|
+
const nums = match.slice(2, -1).split(';').map(Number);
|
|
225
|
+
if (nums.every(n => n >= 0 && n <= 107)) {
|
|
226
|
+
return match;
|
|
227
|
+
}
|
|
228
|
+
return '';
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get safe environment variables (filter out sensitive ones)
|
|
234
|
+
*/
|
|
235
|
+
export function getSafeEnv(): Record<string, string> {
|
|
236
|
+
const sensitive = [
|
|
237
|
+
'API_KEY', 'SECRET', 'TOKEN', 'PASSWORD', 'CREDENTIAL',
|
|
238
|
+
'AWS_', 'GITHUB_TOKEN', 'NPM_TOKEN', 'PRIVATE_KEY',
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
const result: Record<string, string> = {};
|
|
242
|
+
|
|
243
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
244
|
+
const isSensitive = sensitive.some(s => key.toUpperCase().includes(s));
|
|
245
|
+
if (!isSensitive && value !== undefined) {
|
|
246
|
+
result[key] = value;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Generate a safe temporary path
|
|
255
|
+
*/
|
|
256
|
+
export function getSafeTempPath(filename: string): string {
|
|
257
|
+
const sanitized = filename.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
258
|
+
return path.join(os.tmpdir(), 'grokcode', sanitized);
|
|
259
|
+
}
|