@synkro-sh/cli 1.4.79 → 1.4.81
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/dist/bootstrap.js +272 -214
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -2136,7 +2136,115 @@ import {
|
|
|
2136
2136
|
localGradeCwe, parseVerdict, reconstructContent, readStdin, log,
|
|
2137
2137
|
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, filePathFromToolInput, dispatchFinding, dispatchCapture, GATEWAY_URL,
|
|
2138
2138
|
} from './_synkro-common.ts';
|
|
2139
|
-
import { basename, extname } from 'node:path';
|
|
2139
|
+
import { basename, extname, resolve, join, dirname } from 'node:path';
|
|
2140
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
2141
|
+
|
|
2142
|
+
|
|
2143
|
+
interface PackageCapability {
|
|
2144
|
+
name: string;
|
|
2145
|
+
description: string;
|
|
2146
|
+
capabilities: string[];
|
|
2147
|
+
sourceExcerpt: string;
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
const JS_DANGEROUS_MODULES = new Set([
|
|
2151
|
+
'child_process', 'net', 'dgram', 'http', 'https', 'fs', 'vm',
|
|
2152
|
+
'worker_threads', 'cluster', 'dns', 'tls', 'crypto',
|
|
2153
|
+
]);
|
|
2154
|
+
|
|
2155
|
+
const PY_DANGEROUS_MODULES = new Set([
|
|
2156
|
+
'subprocess', 'os', 'socket', 'requests', 'urllib', 'ctypes',
|
|
2157
|
+
'importlib', 'shutil', 'tempfile', 'webbrowser',
|
|
2158
|
+
]);
|
|
2159
|
+
|
|
2160
|
+
function detectNewImports(toolName: string, toolInput: any, fileExt: string): string[] {
|
|
2161
|
+
const newCode = toolName === 'Edit' || toolName === 'edit_file' || toolName === 'reapply'
|
|
2162
|
+
? (toolInput.new_string || '')
|
|
2163
|
+
: toolName === 'ApplyPatch' || toolName === 'apply_patch'
|
|
2164
|
+
? (toolInput.patch || toolInput.content || toolInput.code_edit || '')
|
|
2165
|
+
: toolName === 'MultiEdit'
|
|
2166
|
+
? (Array.isArray(toolInput.edits) ? toolInput.edits.map((e: any) => e?.new_string || '').join('\n') : '')
|
|
2167
|
+
: (toolInput.content || toolInput.code_edit || '');
|
|
2168
|
+
|
|
2169
|
+
if (!newCode) return [];
|
|
2170
|
+
const imports: string[] = [];
|
|
2171
|
+
|
|
2172
|
+
if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(fileExt)) {
|
|
2173
|
+
const jsMatches = newCode.matchAll(/(?:from\s+['"]|require\s*\(\s*['"])([^./'"@][^'"]*|@[^'"]+)['"]/g);
|
|
2174
|
+
for (const m of jsMatches) {
|
|
2175
|
+
const pkg = m[1].startsWith('@') ? m[1].split('/').slice(0, 2).join('/') : m[1].split('/')[0];
|
|
2176
|
+
if (pkg && !imports.includes(pkg)) imports.push(pkg);
|
|
2177
|
+
}
|
|
2178
|
+
} else if (['.py'].includes(fileExt)) {
|
|
2179
|
+
const pyMatches = newCode.matchAll(/(?:^import\s+|^from\s+)([a-zA-Z_]\w*)/gm);
|
|
2180
|
+
for (const m of pyMatches) {
|
|
2181
|
+
if (m[1] && !imports.includes(m[1])) imports.push(m[1]);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
return imports;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
function scanPackageCapabilities(pkgName: string, cwd: string): PackageCapability | null {
|
|
2189
|
+
const pkgDir = resolve(cwd, 'node_modules', pkgName);
|
|
2190
|
+
if (!existsSync(pkgDir)) return null;
|
|
2191
|
+
|
|
2192
|
+
try {
|
|
2193
|
+
const pkgJsonPath = join(pkgDir, 'package.json');
|
|
2194
|
+
if (!existsSync(pkgJsonPath)) return null;
|
|
2195
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
2196
|
+
|
|
2197
|
+
const capabilities: string[] = [];
|
|
2198
|
+
const dangerousFiles: string[] = [];
|
|
2199
|
+
|
|
2200
|
+
function scanDir(dir: string, depth: number) {
|
|
2201
|
+
if (depth > 3 || dangerousFiles.length >= 3) return;
|
|
2202
|
+
let entries: string[];
|
|
2203
|
+
try { entries = readdirSync(dir); } catch { return; }
|
|
2204
|
+
for (const entry of entries) {
|
|
2205
|
+
if (entry === 'node_modules' || entry.startsWith('.')) continue;
|
|
2206
|
+
const full = join(dir, entry);
|
|
2207
|
+
if (entry.endsWith('.js') || entry.endsWith('.mjs') || entry.endsWith('.cjs') || entry.endsWith('.ts')) {
|
|
2208
|
+
try {
|
|
2209
|
+
const src = readFileSync(full, 'utf-8').slice(0, 5000);
|
|
2210
|
+
for (const mod of JS_DANGEROUS_MODULES) {
|
|
2211
|
+
if (src.includes("'" + mod + "'") || src.includes('"' + mod + '"')) {
|
|
2212
|
+
if (!capabilities.includes(mod)) capabilities.push(mod);
|
|
2213
|
+
if (dangerousFiles.length < 3) {
|
|
2214
|
+
const short = full.replace(pkgDir + '/', '');
|
|
2215
|
+
const lines = src.split('\n').filter(l =>
|
|
2216
|
+
JS_DANGEROUS_MODULES.has(l.match(/require\s*\(\s*['"]([^'"]+)['"]\)/)?.[1] || '') ||
|
|
2217
|
+
JS_DANGEROUS_MODULES.has(l.match(/from\s+['"]([^'"]+)['"]/)?.[1] || '')
|
|
2218
|
+
).slice(0, 5).join('\n');
|
|
2219
|
+
if (lines) dangerousFiles.push('--- ' + short + ' ---\n' + lines);
|
|
2220
|
+
}
|
|
2221
|
+
break;
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
} catch {}
|
|
2225
|
+
} else {
|
|
2226
|
+
try {
|
|
2227
|
+
const stat = require('node:fs').statSync(full);
|
|
2228
|
+
if (stat.isDirectory()) scanDir(full, depth + 1);
|
|
2229
|
+
} catch {}
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
scanDir(pkgDir, 0);
|
|
2235
|
+
|
|
2236
|
+
if (capabilities.length === 0) return null;
|
|
2237
|
+
|
|
2238
|
+
return {
|
|
2239
|
+
name: pkgName,
|
|
2240
|
+
description: pkgJson.description || '',
|
|
2241
|
+
capabilities,
|
|
2242
|
+
sourceExcerpt: dangerousFiles.join('\n'),
|
|
2243
|
+
};
|
|
2244
|
+
} catch {
|
|
2245
|
+
return null;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2140
2248
|
|
|
2141
2249
|
async function main() {
|
|
2142
2250
|
setupCursorHookSignals();
|
|
@@ -2222,6 +2330,18 @@ async function main() {
|
|
|
2222
2330
|
return;
|
|
2223
2331
|
}
|
|
2224
2332
|
|
|
2333
|
+
let localPkgContext = '';
|
|
2334
|
+
if (cwd) {
|
|
2335
|
+
const newImports = detectNewImports(toolName, toolInput, fileExt);
|
|
2336
|
+
const caps = newImports.slice(0, 5)
|
|
2337
|
+
.map(pkg => scanPackageCapabilities(pkg, cwd))
|
|
2338
|
+
.filter((c): c is PackageCapability => c !== null);
|
|
2339
|
+
if (caps.length > 0) {
|
|
2340
|
+
localPkgContext = '\n\nImported packages with notable capabilities:\n' +
|
|
2341
|
+
caps.map(c => '- ' + c.name + ' (claims: "' + c.description + '")\n Capabilities: ' + c.capabilities.join(', ') + '\n' + c.sourceExcerpt).join('\n');
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2225
2345
|
const graderPrompt = [
|
|
2226
2346
|
'File: ' + filePath,
|
|
2227
2347
|
'Content:',
|
|
@@ -2229,7 +2349,7 @@ async function main() {
|
|
|
2229
2349
|
'',
|
|
2230
2350
|
'CWE rules to check against:',
|
|
2231
2351
|
JSON.stringify(cweRules),
|
|
2232
|
-
].join('\n');
|
|
2352
|
+
].join('\n') + localPkgContext;
|
|
2233
2353
|
|
|
2234
2354
|
let gradeResp: string;
|
|
2235
2355
|
try {
|
|
@@ -2314,18 +2434,104 @@ async function main() {
|
|
|
2314
2434
|
status: 'resolved',
|
|
2315
2435
|
}, config.captureDepth);
|
|
2316
2436
|
|
|
2317
|
-
|
|
2437
|
+
const cleanMsg = cweTag + ' ' + fileShort + ' \u2192 clean' + (verdict.reason ? ' (' + verdict.reason + ')' : '');
|
|
2438
|
+
outputJson({ systemMessage: cleanMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: cleanMsg } });
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
// Cloud path \u2014 thin client, all grading logic server-side
|
|
2443
|
+
// Detect imports and scan capabilities for cloud grading
|
|
2444
|
+
let packageContext: PackageCapability[] | undefined;
|
|
2445
|
+
if (cwd) {
|
|
2446
|
+
const newImports = detectNewImports(toolName, toolInput, fileExt);
|
|
2447
|
+
if (newImports.length > 0) {
|
|
2448
|
+
const caps = newImports
|
|
2449
|
+
.slice(0, 5)
|
|
2450
|
+
.map(pkg => scanPackageCapabilities(pkg, cwd))
|
|
2451
|
+
.filter((c): c is PackageCapability => c !== null);
|
|
2452
|
+
if (caps.length > 0) packageContext = caps;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
const scanBody: any = { file_path: filePath, content: cweContent };
|
|
2456
|
+
if (packageContext) scanBody.package_context = packageContext.map(c => ({
|
|
2457
|
+
name: c.name, description: c.description, capabilities: c.capabilities, source_excerpt: c.sourceExcerpt,
|
|
2458
|
+
}));
|
|
2459
|
+
let cweResp: any;
|
|
2460
|
+
try {
|
|
2461
|
+
const resp = await fetch(GATEWAY_URL + '/api/v1/cwe-scan', {
|
|
2462
|
+
method: 'POST',
|
|
2463
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
2464
|
+
body: JSON.stringify(scanBody),
|
|
2465
|
+
signal: AbortSignal.timeout(12000),
|
|
2466
|
+
});
|
|
2467
|
+
cweResp = await resp.json();
|
|
2468
|
+
} catch {
|
|
2469
|
+
outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \u2192 cloud grader timeout, skipped' });
|
|
2470
|
+
return;
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
const findings = Array.isArray(cweResp?.findings) ? cweResp.findings : [];
|
|
2474
|
+
if (cweResp?.action === 'deny' && findings.length > 0) {
|
|
2475
|
+
const activeCweIds = findings
|
|
2476
|
+
.filter((f: any) => f.mode === 'blocking')
|
|
2477
|
+
.map((f: any) => f.cwe)
|
|
2478
|
+
.filter((id: string) => !exemptedCwes.has(id.toUpperCase()));
|
|
2479
|
+
|
|
2480
|
+
if (activeCweIds.length === 0) {
|
|
2481
|
+
outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \u2192 clean (exempted)' });
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
const displayIds = activeCweIds.slice(0, 3).join(', ');
|
|
2486
|
+
const count = activeCweIds.length;
|
|
2487
|
+
const label = count === 1 ? 'match' : 'matches';
|
|
2488
|
+
const cweMsg = cweTag + ' ' + fileShort + ' \u2192 ' + count + ' CWE ' + label + ' (' + displayIds + ')';
|
|
2489
|
+
|
|
2490
|
+
const fixLines = findings
|
|
2491
|
+
.filter((f: any) => activeCweIds.includes(f.cwe) && f.suggested_fix)
|
|
2492
|
+
.map((f: any) => '[' + f.cwe + '] Fix: ' + f.suggested_fix);
|
|
2493
|
+
const fixHint = fixLines.length > 0 ? '\n' + fixLines.join('\n') : '';
|
|
2494
|
+
const denyDetail = '[' + displayIds + '] ' + (findings[0]?.reason || 'code weakness detected');
|
|
2495
|
+
const ctx = 'CWE: ' + denyDetail + fixHint + '\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the weakness in code yourself.';
|
|
2496
|
+
|
|
2497
|
+
for (const cweId of activeCweIds) {
|
|
2498
|
+
const f = findings.find((x: any) => x.cwe === cweId);
|
|
2499
|
+
dispatchFinding(jwt, {
|
|
2500
|
+
session_id: sessionId,
|
|
2501
|
+
file_path: filePath,
|
|
2502
|
+
finding_type: 'cwe',
|
|
2503
|
+
finding_id: cweId,
|
|
2504
|
+
severity: f?.severity || 'high',
|
|
2505
|
+
status: 'open',
|
|
2506
|
+
detail: f?.reason || 'code weakness detected',
|
|
2507
|
+
cwe_name: f?.name || undefined,
|
|
2508
|
+
}, config.captureDepth);
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
dispatchCapture(jwt, 'cwe', 'block', findings[0]?.severity || 'high', 'security',
|
|
2318
2512
|
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
2319
2513
|
command: 'edit ' + filePath,
|
|
2320
|
-
reasoning:
|
|
2514
|
+
reasoning: denyDetail,
|
|
2515
|
+
violatedRules: activeCweIds,
|
|
2321
2516
|
});
|
|
2322
2517
|
|
|
2323
|
-
|
|
2324
|
-
|
|
2518
|
+
outputJson({
|
|
2519
|
+
systemMessage: cweMsg,
|
|
2520
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
|
|
2521
|
+
});
|
|
2325
2522
|
return;
|
|
2326
2523
|
}
|
|
2327
2524
|
|
|
2328
|
-
|
|
2525
|
+
dispatchFinding(jwt, {
|
|
2526
|
+
session_id: sessionId,
|
|
2527
|
+
file_path: filePath,
|
|
2528
|
+
finding_type: 'cwe',
|
|
2529
|
+
finding_id: 'pass',
|
|
2530
|
+
status: 'resolved',
|
|
2531
|
+
}, config.captureDepth);
|
|
2532
|
+
|
|
2533
|
+
const cleanMsg = cweTag + ' ' + fileShort + ' \u2192 clean' + (cweResp?.summary ? ' (cloud)' : '');
|
|
2534
|
+
outputJson({ systemMessage: cleanMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: cleanMsg } });
|
|
2329
2535
|
} catch (err) {
|
|
2330
2536
|
process.stderr.write('[synkro] cweGuard error: ' + String(err) + '\n');
|
|
2331
2537
|
outputEmpty();
|
|
@@ -2501,77 +2707,6 @@ import {
|
|
|
2501
2707
|
type HookConfig, type Rule,
|
|
2502
2708
|
} from './_synkro-common.ts';
|
|
2503
2709
|
|
|
2504
|
-
const TOP_NPM_PKGS = new Set([
|
|
2505
|
-
'express','react','lodash','chalk','commander','debug','dotenv','webpack',
|
|
2506
|
-
'typescript','moment','uuid','cors','body-parser','mongoose','jsonwebtoken','bcrypt',
|
|
2507
|
-
'nodemon','eslint','prettier','jest','mocha','chai','sinon','supertest','request',
|
|
2508
|
-
'async','bluebird','underscore','ramda','rxjs','socket.io','redis','pg','mysql',
|
|
2509
|
-
'sequelize','knex','prisma','next','nuxt','vue','svelte','angular','ember',
|
|
2510
|
-
'react-dom','react-router','react-redux','redux','mobx','formik','yup','zod',
|
|
2511
|
-
'ajv','joi','helmet','morgan','passport','cookie-parser','express-session',
|
|
2512
|
-
'multer','sharp','jimp','puppeteer','playwright','cheerio','got','node-fetch',
|
|
2513
|
-
'superagent','inquirer','ora','yargs','minimist','glob','rimraf','mkdirp',
|
|
2514
|
-
'fs-extra','chokidar','ws','graphql','apollo-server','fastify','koa','hapi',
|
|
2515
|
-
'nest','drizzle-orm','typeorm','mikro-orm','bull','bullmq','ioredis','kafkajs',
|
|
2516
|
-
'amqplib','nodemailer','handlebars','ejs','pug','marked','highlight.js',
|
|
2517
|
-
'dayjs','date-fns','luxon','nanoid','cuid','short-uuid','colors','picocolors',
|
|
2518
|
-
'winston','pino','bunyan','semver','tar','archiver','unzipper','crypto-js',
|
|
2519
|
-
'bcryptjs','argon2','jose','openai','anthropic','langchain','tensorflow',
|
|
2520
|
-
'onnxruntime-node','sharp','canvas','three','d3','chart.js','echarts',
|
|
2521
|
-
'tailwindcss','postcss','autoprefixer','sass','less','styled-components',
|
|
2522
|
-
'emotion','framer-motion','gsap','lottie-web','swiper','i18next',
|
|
2523
|
-
]);
|
|
2524
|
-
|
|
2525
|
-
const TOP_PYPI_PKGS = new Set([
|
|
2526
|
-
'requests','flask','django','numpy','pandas','scipy','matplotlib','scikit-learn',
|
|
2527
|
-
'tensorflow','torch','pytorch','keras','fastapi','uvicorn','gunicorn','celery',
|
|
2528
|
-
'redis','sqlalchemy','alembic','pydantic','httpx','aiohttp','beautifulsoup4',
|
|
2529
|
-
'scrapy','selenium','playwright','pillow','opencv-python','boto3','awscli',
|
|
2530
|
-
'google-cloud-storage','azure-storage-blob','psycopg2','pymongo','motor',
|
|
2531
|
-
'pytest','unittest2','mock','coverage','tox','black','flake8','mypy','ruff',
|
|
2532
|
-
'isort','pylint','bandit','cryptography','paramiko','fabric','click','typer',
|
|
2533
|
-
'rich','colorama','tqdm','loguru','python-dotenv','pyyaml','toml','orjson',
|
|
2534
|
-
'ujson','marshmallow','attrs','dataclasses-json','jinja2','mako','arrow',
|
|
2535
|
-
'pendulum','dateutil','pytz','regex','chardet','charset-normalizer',
|
|
2536
|
-
'langchain','openai','anthropic','transformers','huggingface-hub','tokenizers',
|
|
2537
|
-
'gradio','streamlit','dash','plotly','seaborn','bokeh','altair',
|
|
2538
|
-
]);
|
|
2539
|
-
|
|
2540
|
-
function levenshtein(a: string, b: string): number {
|
|
2541
|
-
const m = a.length, n = b.length;
|
|
2542
|
-
if (Math.abs(m - n) > 2) return 3;
|
|
2543
|
-
const dp: number[][] = Array.from({ length: m + 1 }, (_, i) => {
|
|
2544
|
-
const row = new Array(n + 1).fill(0);
|
|
2545
|
-
row[0] = i;
|
|
2546
|
-
return row;
|
|
2547
|
-
});
|
|
2548
|
-
for (let j = 1; j <= n; j++) dp[0][j] = j;
|
|
2549
|
-
for (let i = 1; i <= m; i++) {
|
|
2550
|
-
for (let j = 1; j <= n; j++) {
|
|
2551
|
-
dp[i][j] = a[i - 1] === b[j - 1]
|
|
2552
|
-
? dp[i - 1][j - 1]
|
|
2553
|
-
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
2554
|
-
}
|
|
2555
|
-
}
|
|
2556
|
-
return dp[m][n];
|
|
2557
|
-
}
|
|
2558
|
-
|
|
2559
|
-
function checkTyposquat(pkg: string, isPip: boolean): string | null {
|
|
2560
|
-
const topPkgs = isPip ? TOP_PYPI_PKGS : TOP_NPM_PKGS;
|
|
2561
|
-
if (topPkgs.has(pkg)) return null;
|
|
2562
|
-
const pkgLower = pkg.toLowerCase();
|
|
2563
|
-
for (const known of topPkgs) {
|
|
2564
|
-
const dist = levenshtein(pkgLower, known);
|
|
2565
|
-
if (dist > 0 && dist <= 2) return known;
|
|
2566
|
-
}
|
|
2567
|
-
return null;
|
|
2568
|
-
}
|
|
2569
|
-
|
|
2570
|
-
interface PkgMeta {
|
|
2571
|
-
deprecated?: string;
|
|
2572
|
-
weeklyDownloads?: number;
|
|
2573
|
-
}
|
|
2574
|
-
|
|
2575
2710
|
async function main() {
|
|
2576
2711
|
setupCursorHookSignals();
|
|
2577
2712
|
try {
|
|
@@ -2615,21 +2750,17 @@ async function main() {
|
|
|
2615
2750
|
if (!jwt) { outputEmpty(); return; }
|
|
2616
2751
|
jwt = await ensureFreshJwt(jwt);
|
|
2617
2752
|
|
|
2618
|
-
// \u2500\u2500\u2500 Install protection: CVE + typosquat +
|
|
2753
|
+
// \u2500\u2500\u2500 Install protection: server-side pkg-scan (CVE + typosquat + tarball + reputation) \u2500\u2500\u2500
|
|
2619
2754
|
let installScanMsg = '';
|
|
2620
2755
|
if (toolName === 'Bash') {
|
|
2621
2756
|
const pkgInstallMatch = command.match(
|
|
2622
|
-
|
|
2757
|
+
/^(?:.*&&s*|.*;s*)?(?:npms+(?:install|i|add)|pnpms+(?:add|install|i)|yarns+add|buns+(?:add|install|i)|(?:uvs+)?pip3?s+install|gos+get|cargos+add|gems+install|composers+require)s+([^|;&><]+)/
|
|
2623
2758
|
);
|
|
2624
|
-
const isPip = /(?:
|
|
2625
|
-
const isGo = command.match(/^go\\s+get/);
|
|
2626
|
-
const isCargo = command.match(/^cargo\\s+add/);
|
|
2627
|
-
const isGem = command.match(/^gem\\s+install/);
|
|
2628
|
-
const isComposer = command.match(/^composer\\s+require/);
|
|
2759
|
+
const isPip = /(?:uvs+)?pip3?s+install/.test(command);
|
|
2629
2760
|
if (pkgInstallMatch) {
|
|
2630
2761
|
const rawArgs = pkgInstallMatch[1];
|
|
2631
|
-
const
|
|
2632
|
-
const tokens = rawArgs.split(
|
|
2762
|
+
const packages: Array<{ name: string; version: string; ecosystem: string }> = [];
|
|
2763
|
+
const tokens = rawArgs.split(/s+/);
|
|
2633
2764
|
let skipNext = false;
|
|
2634
2765
|
for (const token of tokens) {
|
|
2635
2766
|
if (skipNext) { skipNext = false; continue; }
|
|
@@ -2638,164 +2769,88 @@ async function main() {
|
|
|
2638
2769
|
if (/^--(python|target|prefix|root|constraint|requirement|index-url|extra-index-url|find-links|build|src|cache-dir|filter|workspace)$/.test(token)) skipNext = true;
|
|
2639
2770
|
continue;
|
|
2640
2771
|
}
|
|
2772
|
+
const ecosystem = isPip ? 'PyPI' : 'npm';
|
|
2641
2773
|
if (isPip) {
|
|
2642
2774
|
const pipMatch = token.match(/^([a-zA-Z0-9_.-]+)(?:[=~!<>]=?(.+))?$/);
|
|
2643
2775
|
if (pipMatch) {
|
|
2644
|
-
|
|
2776
|
+
packages.push({ name: pipMatch[1], version: pipMatch[2]?.replace(/^=/, '') || '*', ecosystem });
|
|
2645
2777
|
continue;
|
|
2646
2778
|
}
|
|
2647
2779
|
}
|
|
2648
2780
|
const atIdx = token.lastIndexOf('@');
|
|
2649
2781
|
if (atIdx > 0) {
|
|
2650
|
-
|
|
2782
|
+
packages.push({ name: token.slice(0, atIdx), version: token.slice(atIdx + 1), ecosystem });
|
|
2651
2783
|
} else {
|
|
2652
|
-
|
|
2784
|
+
packages.push({ name: token, version: '*', ecosystem });
|
|
2653
2785
|
}
|
|
2654
2786
|
}
|
|
2655
2787
|
|
|
2656
|
-
if (
|
|
2657
|
-
const warnings: string[] = [];
|
|
2658
|
-
const pkgMeta: Record<string, PkgMeta> = {};
|
|
2659
|
-
|
|
2660
|
-
const metaLookups = Object.keys(deps).map(async (pkg) => {
|
|
2661
|
-
try {
|
|
2662
|
-
if (isPip) {
|
|
2663
|
-
const r = await fetch('https://pypi.org/pypi/' + encodeURIComponent(pkg) + '/json', { signal: AbortSignal.timeout(4000) });
|
|
2664
|
-
if (r.ok) {
|
|
2665
|
-
const d = await r.json() as any;
|
|
2666
|
-
if (deps[pkg] === '*' && d?.info?.version) deps[pkg] = d.info.version;
|
|
2667
|
-
const classifiers: string[] = d?.info?.classifiers || [];
|
|
2668
|
-
const isInactive = classifiers.some((c: string) => /Development Status :: [67]/.test(c));
|
|
2669
|
-
if (isInactive) pkgMeta[pkg] = { ...pkgMeta[pkg], deprecated: 'package marked as inactive/obsolete' };
|
|
2670
|
-
if (d?.info?.yanked) pkgMeta[pkg] = { ...pkgMeta[pkg], deprecated: d.info.yanked_reason || 'yanked from PyPI' };
|
|
2671
|
-
} else if (r.status === 404) {
|
|
2672
|
-
warnings.push('\\u26a0 ' + pkg + ': package not found on PyPI \\u2014 may not exist');
|
|
2673
|
-
}
|
|
2674
|
-
} else {
|
|
2675
|
-
const verSlug = deps[pkg] !== '*' ? deps[pkg] : 'latest';
|
|
2676
|
-
const [metaResp, dlResp] = await Promise.all([
|
|
2677
|
-
fetch('https://registry.npmjs.org/' + encodeURIComponent(pkg) + '/' + verSlug, { signal: AbortSignal.timeout(4000) }),
|
|
2678
|
-
fetch('https://api.npmjs.org/downloads/point/last-week/' + encodeURIComponent(pkg), { signal: AbortSignal.timeout(4000) }),
|
|
2679
|
-
]);
|
|
2680
|
-
if (metaResp.ok) {
|
|
2681
|
-
const d = await metaResp.json() as any;
|
|
2682
|
-
if (deps[pkg] === '*' && d?.version) deps[pkg] = d.version;
|
|
2683
|
-
if (d?.deprecated) pkgMeta[pkg] = { ...pkgMeta[pkg], deprecated: d.deprecated };
|
|
2684
|
-
} else if (metaResp.status === 404) {
|
|
2685
|
-
warnings.push('\\u26a0 ' + pkg + ': package not found on npm \\u2014 may not exist');
|
|
2686
|
-
}
|
|
2687
|
-
if (dlResp.ok) {
|
|
2688
|
-
const d = await dlResp.json() as any;
|
|
2689
|
-
if (typeof d?.downloads === 'number') pkgMeta[pkg] = { ...pkgMeta[pkg], weeklyDownloads: d.downloads };
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
} catch {}
|
|
2693
|
-
});
|
|
2694
|
-
await Promise.all(metaLookups);
|
|
2695
|
-
|
|
2696
|
-
for (const pkg of Object.keys(deps)) {
|
|
2697
|
-
const similar = checkTyposquat(pkg, isPip);
|
|
2698
|
-
if (similar) {
|
|
2699
|
-
const dl = pkgMeta[pkg]?.weeklyDownloads;
|
|
2700
|
-
if (dl === undefined || dl < 1000) {
|
|
2701
|
-
warnings.push('\\u26a0 ' + pkg + ': possible typosquat of "' + similar + '"' + (dl !== undefined ? ' (' + dl + ' weekly downloads)' : '') + ' \\u2014 verify package name');
|
|
2702
|
-
}
|
|
2703
|
-
}
|
|
2704
|
-
}
|
|
2705
|
-
|
|
2706
|
-
for (const [pkg, meta] of Object.entries(pkgMeta)) {
|
|
2707
|
-
if (meta.deprecated) {
|
|
2708
|
-
warnings.push('\\u26a0 ' + pkg + ': deprecated \\u2014 ' + meta.deprecated);
|
|
2709
|
-
}
|
|
2710
|
-
}
|
|
2711
|
-
|
|
2712
|
-
if (!isPip) {
|
|
2713
|
-
for (const [pkg, meta] of Object.entries(pkgMeta)) {
|
|
2714
|
-
if (meta.weeklyDownloads !== undefined && meta.weeklyDownloads < 50 && !warnings.some(w => w.includes(pkg))) {
|
|
2715
|
-
warnings.push('\\u26a0 ' + pkg + ': very low adoption (' + meta.weeklyDownloads + ' weekly downloads) \\u2014 consider a more established alternative');
|
|
2716
|
-
}
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
2719
|
-
|
|
2720
|
-
const manifestFile = isPip ? 'requirements.txt'
|
|
2721
|
-
: isGo ? 'go.mod'
|
|
2722
|
-
: isCargo ? 'Cargo.toml'
|
|
2723
|
-
: isGem ? 'Gemfile'
|
|
2724
|
-
: isComposer ? 'composer.json'
|
|
2725
|
-
: 'package.json';
|
|
2726
|
-
const manifestContent = isPip
|
|
2727
|
-
? Object.entries(deps).map(([k, v]) => v === '*' ? k : k + '==' + v).join('\\n')
|
|
2728
|
-
: JSON.stringify({ dependencies: deps });
|
|
2729
|
-
|
|
2788
|
+
if (packages.length > 0) {
|
|
2730
2789
|
try {
|
|
2731
|
-
const
|
|
2732
|
-
const cveResp = await fetch(GATEWAY_URL + '/api/v1/cve-scan', {
|
|
2790
|
+
const scanResp = await fetch(GATEWAY_URL + '/api/v1/pkg-scan', {
|
|
2733
2791
|
method: 'POST',
|
|
2734
2792
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
2735
|
-
body: JSON.stringify(
|
|
2736
|
-
signal: AbortSignal.timeout(
|
|
2793
|
+
body: JSON.stringify({ packages, command }),
|
|
2794
|
+
signal: AbortSignal.timeout(15000),
|
|
2737
2795
|
}).then(r => r.json()) as any;
|
|
2738
2796
|
|
|
2739
|
-
const
|
|
2740
|
-
const
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
const
|
|
2751
|
-
|
|
2752
|
-
const cveMsg = '[synkro:installScan] ' + cmdShort + ' \\u2192 ' + count + ' ' + label;
|
|
2753
|
-
const ctx = 'CVE: ' + top3 + '\\nDo NOT install packages with known vulnerabilities. Use a patched version or a different package.'
|
|
2754
|
-
+ (warnings.length > 0 ? '\\n' + warnings.join('\\n') : '');
|
|
2797
|
+
const action = scanResp?.action || 'allow';
|
|
2798
|
+
const pkgResults = Array.isArray(scanResp?.packages) ? scanResp.packages : [];
|
|
2799
|
+
const summary = scanResp?.summary || '';
|
|
2800
|
+
|
|
2801
|
+
if (action === 'block') {
|
|
2802
|
+
const blockSignals = pkgResults
|
|
2803
|
+
.flatMap((p: any) => (p.signals || []).filter((s: any) => s.severity === 'critical' || s.severity === 'high'))
|
|
2804
|
+
.slice(0, 5);
|
|
2805
|
+
const scanMsg = '[synkro:installScan] ' + cmdShort + ' \u2192 blocked';
|
|
2806
|
+
const details = blockSignals.map((s: any) => s.detail).join('
|
|
2807
|
+
');
|
|
2808
|
+
const ctx = details + '
|
|
2809
|
+
Do NOT install packages with security risks. Use a patched version or a different package.';
|
|
2755
2810
|
|
|
2756
2811
|
const config = await loadConfig(jwt);
|
|
2757
|
-
for (const
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
}
|
|
2812
|
+
for (const p of pkgResults) {
|
|
2813
|
+
for (const s of (p.signals || [])) {
|
|
2814
|
+
if (s.severity === 'critical' || s.severity === 'high') {
|
|
2815
|
+
dispatchFinding(jwt, {
|
|
2816
|
+
session_id: sessionId,
|
|
2817
|
+
file_path: command,
|
|
2818
|
+
finding_type: s.type === 'cve' ? 'cve' : 'pkg',
|
|
2819
|
+
finding_id: s.type + ':' + p.name,
|
|
2820
|
+
severity: s.severity,
|
|
2821
|
+
status: 'open',
|
|
2822
|
+
detail: s.detail,
|
|
2823
|
+
package_name: p.name,
|
|
2824
|
+
package_version: p.version,
|
|
2825
|
+
}, config.captureDepth);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2773
2828
|
}
|
|
2774
2829
|
|
|
2775
|
-
const
|
|
2776
|
-
dispatchCapture(jwt, '
|
|
2830
|
+
const violatedIds = blockSignals.map((s: any) => s.type + ':' + s.detail.slice(0, 40));
|
|
2831
|
+
dispatchCapture(jwt, 'pkg', 'block', 'critical', 'security',
|
|
2777
2832
|
'Bash', gitRepo, sessionId, config.captureDepth, {
|
|
2778
2833
|
command,
|
|
2779
|
-
reasoning:
|
|
2780
|
-
violatedRules:
|
|
2834
|
+
reasoning: details.slice(0, 200),
|
|
2835
|
+
violatedRules: violatedIds,
|
|
2781
2836
|
ccModel: transcript.ccModel,
|
|
2782
2837
|
});
|
|
2783
2838
|
|
|
2784
2839
|
outputJson({
|
|
2785
|
-
systemMessage:
|
|
2840
|
+
systemMessage: scanMsg,
|
|
2786
2841
|
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
|
|
2787
2842
|
});
|
|
2788
2843
|
return;
|
|
2789
2844
|
}
|
|
2790
2845
|
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
if (warnings.length > 0) {
|
|
2797
|
-
installScanMsg = '[synkro:installScan] ' + warnings.join('\\n');
|
|
2846
|
+
if (action === 'warn') {
|
|
2847
|
+
installScanMsg = '[synkro:installScan] ' + summary;
|
|
2848
|
+
} else {
|
|
2849
|
+
const scannedPkgs = packages.map(p => p.name + '@' + p.version).join(', ');
|
|
2850
|
+
installScanMsg = '[synkro:installScan] ' + scannedPkgs + ' \u2192 clean';
|
|
2798
2851
|
}
|
|
2852
|
+
} catch (e) {
|
|
2853
|
+
log('bashGuard pkg-scan failed: ' + String(e));
|
|
2799
2854
|
}
|
|
2800
2855
|
}
|
|
2801
2856
|
}
|
|
@@ -6342,7 +6397,7 @@ function writeConfigEnv(opts) {
|
|
|
6342
6397
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
6343
6398
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
6344
6399
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
6345
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.4.
|
|
6400
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.4.81")}`
|
|
6346
6401
|
];
|
|
6347
6402
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
6348
6403
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -6598,10 +6653,13 @@ async function startLocalMcpServer() {
|
|
|
6598
6653
|
console.log(" Restarting local MCP server to pick up new credentials...");
|
|
6599
6654
|
try {
|
|
6600
6655
|
const { execSync: execSync7 } = await import("child_process");
|
|
6656
|
+
const selfPid = process.pid;
|
|
6601
6657
|
const pids = execSync7(`lsof -ti :${MCP_LOCAL_PORT}`, { encoding: "utf-8" }).trim();
|
|
6602
6658
|
for (const pid of pids.split("\n").filter(Boolean)) {
|
|
6659
|
+
const n = parseInt(pid, 10);
|
|
6660
|
+
if (n === selfPid) continue;
|
|
6603
6661
|
try {
|
|
6604
|
-
process.kill(
|
|
6662
|
+
process.kill(n, "SIGTERM");
|
|
6605
6663
|
} catch {
|
|
6606
6664
|
}
|
|
6607
6665
|
}
|