@synkro-sh/cli 1.4.71 → 1.4.73

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 CHANGED
@@ -147,6 +147,11 @@ function installCCHooks(settingsPath, config) {
147
147
  command: config.editPrecheckScriptPath,
148
148
  timeout: 30
149
149
  },
150
+ {
151
+ type: "command",
152
+ command: config.cwePrecheckScriptPath,
153
+ timeout: 30
154
+ },
150
155
  {
151
156
  type: "command",
152
157
  command: config.cvePrecheckScriptPath,
@@ -6249,6 +6254,7 @@ function writeHookScripts() {
6249
6254
  const bashScriptPath = join11(HOOKS_DIR, "cc-bash-judge.ts");
6250
6255
  const bashFollowupScriptPath = join11(HOOKS_DIR, "cc-bash-followup.ts");
6251
6256
  const editPrecheckScriptPath = join11(HOOKS_DIR, "cc-edit-precheck.ts");
6257
+ const cwePrecheckScriptPath = join11(HOOKS_DIR, "cc-cwe-precheck.ts");
6252
6258
  const cvePrecheckScriptPath = join11(HOOKS_DIR, "cc-cve-precheck.ts");
6253
6259
  const planJudgeScriptPath = join11(HOOKS_DIR, "cc-plan-judge.ts");
6254
6260
  const agentJudgeScriptPath = join11(HOOKS_DIR, "cc-agent-judge.ts");
@@ -6264,6 +6270,7 @@ function writeHookScripts() {
6264
6270
  writeFileSync7(bashScriptPath, BASH_JUDGE_TS, "utf-8");
6265
6271
  writeFileSync7(bashFollowupScriptPath, BASH_FOLLOWUP_TS, "utf-8");
6266
6272
  writeFileSync7(editPrecheckScriptPath, EDIT_PRECHECK_TS, "utf-8");
6273
+ writeFileSync7(cwePrecheckScriptPath, CWE_PRECHECK_TS, "utf-8");
6267
6274
  writeFileSync7(cvePrecheckScriptPath, CVE_PRECHECK_TS, "utf-8");
6268
6275
  writeFileSync7(planJudgeScriptPath, PLAN_JUDGE_TS, "utf-8");
6269
6276
  writeFileSync7(agentJudgeScriptPath, AGENT_JUDGE_TS, "utf-8");
@@ -6279,7 +6286,7 @@ function writeHookScripts() {
6279
6286
  /**
6280
6287
  * Local MCP guardrails server \u2014 runs on port 8931, stores rules in ~/.synkro/rules.json.
6281
6288
  * JSON-RPC 2.0 over HTTP, same protocol as the cloud MCP server.
6282
- * No auth (localhost only), no embedding API, no Inngest.
6289
+ * Bearer token auth (file-based shared secret), localhost only, no embedding API, no Inngest.
6283
6290
  */
6284
6291
  import { existsSync, readFileSync, writeFileSync, renameSync, appendFileSync, mkdirSync } from 'node:fs';
6285
6292
  import { homedir } from 'node:os';
@@ -6296,15 +6303,29 @@ const TOKEN_PATH = join(HOME, '.synkro', '.mcp-local-token');
6296
6303
  // File-based shared secret \u2014 generated once, required on all POST requests.
6297
6304
  function getOrCreateToken(): string {
6298
6305
  try {
6299
- if (existsSync(TOKEN_PATH)) return readFileSync(TOKEN_PATH, 'utf-8').trim();
6306
+ return readFileSync(TOKEN_PATH, 'utf-8').trim();
6300
6307
  } catch {}
6301
6308
  const token = randomBytes(32).toString('hex');
6302
6309
  mkdirSync(join(HOME, '.synkro'), { recursive: true });
6303
- writeFileSync(TOKEN_PATH, token + '\\n', { mode: 0o600 });
6304
- return token;
6310
+ try {
6311
+ writeFileSync(TOKEN_PATH, token + '\\n', { mode: 0o600, flag: 'wx' });
6312
+ return token;
6313
+ } catch {
6314
+ return readFileSync(TOKEN_PATH, 'utf-8').trim();
6315
+ }
6305
6316
  }
6306
6317
 
6307
6318
  const SERVER_TOKEN = getOrCreateToken();
6319
+ const MAX_BODY_BYTES = 1_048_576;
6320
+
6321
+ let _writeLock: Promise<void> = Promise.resolve();
6322
+ function serialized<T>(fn: () => T | Promise<T>): Promise<T> {
6323
+ let release: () => void;
6324
+ const next = new Promise<void>(r => { release = r; });
6325
+ const prev = _writeLock;
6326
+ _writeLock = next;
6327
+ return prev.then(() => fn()).finally(() => release!());
6328
+ }
6308
6329
 
6309
6330
  // \u2500\u2500\u2500 Storage \u2500\u2500\u2500
6310
6331
 
@@ -6691,6 +6712,219 @@ function handleListExemptions(): any {
6691
6712
  return { exemptions: data.scanExemptions, total: data.scanExemptions.length };
6692
6713
  }
6693
6714
 
6715
+ // \u2500\u2500\u2500 Findings \u2500\u2500\u2500
6716
+
6717
+ const CONFIG_PATH = join(HOME, '.synkro', 'config.json');
6718
+ const JWT_PATH = join(HOME, '.synkro', '.jwt');
6719
+
6720
+ interface Finding {
6721
+ id: string;
6722
+ session_id: string;
6723
+ file_path: string;
6724
+ finding_type: string;
6725
+ finding_id: string;
6726
+ severity: string;
6727
+ status: string;
6728
+ detail?: string;
6729
+ package_name?: string;
6730
+ package_version?: string;
6731
+ fixed_version?: string;
6732
+ created_at: string;
6733
+ resolved_at?: string;
6734
+ }
6735
+
6736
+ function getCloudConfig(): { apiUrl: string; jwt: string } | null {
6737
+ try {
6738
+ const jwt = readFileSync(JWT_PATH, 'utf-8').trim();
6739
+ if (!jwt) return null;
6740
+ let apiUrl = process.env.SYNKRO_API_URL || '';
6741
+ if (!apiUrl) {
6742
+ try {
6743
+ const cfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
6744
+ apiUrl = cfg.api_url || cfg.apiUrl || '';
6745
+ } catch {}
6746
+ }
6747
+ if (!apiUrl) apiUrl = 'https://api.synkro.sh';
6748
+ return { apiUrl, jwt };
6749
+ } catch {
6750
+ return null;
6751
+ }
6752
+ }
6753
+
6754
+ function readLocalFindings(): Finding[] {
6755
+ if (!existsSync(TELEMETRY_PATH)) return [];
6756
+ let lines: string[];
6757
+ try {
6758
+ lines = readFileSync(TELEMETRY_PATH, 'utf-8').split('\\n').filter(Boolean);
6759
+ } catch { return []; }
6760
+ const findingMap = new Map<string, Finding>();
6761
+
6762
+ for (const line of lines) {
6763
+ try {
6764
+ const event = JSON.parse(line);
6765
+ if (event.capture_type !== 'scan_finding') continue;
6766
+ const key = \`\${event.file_path}:\${event.finding_id}\`;
6767
+ const existing = findingMap.get(key);
6768
+ const ts = event._ts || event.created_at || '';
6769
+
6770
+ findingMap.set(key, {
6771
+ id: event.id || existing?.id || \`sf_\${event.session_id}_\${event.finding_id}_\${Date.now()}\`,
6772
+ session_id: event.session_id || existing?.session_id || '',
6773
+ file_path: event.file_path,
6774
+ finding_type: event.finding_type,
6775
+ finding_id: event.finding_id,
6776
+ severity: event.severity || 'unknown',
6777
+ status: event.status || 'open',
6778
+ detail: event.detail || existing?.detail,
6779
+ package_name: event.package_name || existing?.package_name,
6780
+ package_version: event.package_version || existing?.package_version,
6781
+ fixed_version: event.fixed_version || existing?.fixed_version,
6782
+ created_at: existing?.created_at || ts,
6783
+ resolved_at: event.status === 'resolved' ? ts : existing?.resolved_at,
6784
+ });
6785
+ } catch {}
6786
+ }
6787
+
6788
+ return Array.from(findingMap.values());
6789
+ }
6790
+
6791
+ async function fetchCloudFindings(params: Record<string, string>): Promise<Finding[] | null> {
6792
+ const cloud = getCloudConfig();
6793
+ if (!cloud) return null;
6794
+ try {
6795
+ const qs = new URLSearchParams(params).toString();
6796
+ const resp = await fetch(\`\${cloud.apiUrl}/api/v1/scan-findings?\${qs}\`, {
6797
+ headers: { Authorization: \`Bearer \${cloud.jwt}\` },
6798
+ signal: AbortSignal.timeout(5000),
6799
+ });
6800
+ if (!resp.ok) return null;
6801
+ const data = await resp.json() as any;
6802
+ return data.findings || data.data || [];
6803
+ } catch {
6804
+ return null;
6805
+ }
6806
+ }
6807
+
6808
+ function dispatchResolution(finding: Finding): void {
6809
+ const cloud = getCloudConfig();
6810
+ if (!cloud) return;
6811
+ fetch(\`\${cloud.apiUrl}/api/v1/hook/finding\`, {
6812
+ method: 'POST',
6813
+ headers: { Authorization: \`Bearer \${cloud.jwt}\`, 'Content-Type': 'application/json' },
6814
+ body: JSON.stringify({
6815
+ session_id: finding.session_id,
6816
+ file_path: finding.file_path,
6817
+ finding_type: finding.finding_type,
6818
+ finding_id: finding.finding_id,
6819
+ severity: finding.severity,
6820
+ status: 'resolved',
6821
+ }),
6822
+ signal: AbortSignal.timeout(3000),
6823
+ }).catch(() => {});
6824
+ }
6825
+
6826
+ async function handleListFindings(args: any): Promise<any> {
6827
+ const cloudParams: Record<string, string> = {};
6828
+ if (args.status) cloudParams.status = args.status;
6829
+ if (args.finding_type) cloudParams.finding_type = args.finding_type;
6830
+ if (args.severity) cloudParams.severity = args.severity;
6831
+ if (args.file_path) cloudParams.file_path = args.file_path;
6832
+ if (args.limit) cloudParams.limit = String(args.limit);
6833
+
6834
+ const cloudFindings = await fetchCloudFindings(cloudParams);
6835
+
6836
+ let findings: Finding[];
6837
+ let source: string;
6838
+ if (cloudFindings) {
6839
+ findings = cloudFindings;
6840
+ source = 'cloud';
6841
+ } else {
6842
+ findings = readLocalFindings();
6843
+ source = 'local';
6844
+ if (args.status) findings = findings.filter(f => f.status === args.status);
6845
+ if (args.finding_type) findings = findings.filter(f => f.finding_type === args.finding_type);
6846
+ if (args.severity) findings = findings.filter(f => f.severity === args.severity);
6847
+ if (args.file_path) findings = findings.filter(f => f.file_path.includes(args.file_path));
6848
+ }
6849
+
6850
+ findings.sort((a, b) => (b.created_at || '').localeCompare(a.created_at || ''));
6851
+ const limit = Math.min(args.limit || 25, 50);
6852
+ return {
6853
+ source,
6854
+ findings: findings.slice(0, limit).map(f => ({
6855
+ id: f.id,
6856
+ finding_type: f.finding_type,
6857
+ finding_id: f.finding_id,
6858
+ file_path: f.file_path,
6859
+ severity: f.severity,
6860
+ status: f.status,
6861
+ package_name: f.package_name,
6862
+ package_version: f.package_version,
6863
+ created_at: f.created_at,
6864
+ })),
6865
+ total: findings.length,
6866
+ open: findings.filter(f => f.status === 'open').length,
6867
+ resolved: findings.filter(f => f.status === 'resolved').length,
6868
+ };
6869
+ }
6870
+
6871
+ async function handleGetFindingDetail(args: any): Promise<any> {
6872
+ const findings = readLocalFindings();
6873
+ const match = findings.find(f => {
6874
+ if (args.id && f.id === args.id) return true;
6875
+ if (args.file_path && args.finding_id) {
6876
+ return f.file_path.includes(args.file_path) && f.finding_id.toUpperCase() === (args.finding_id || '').toUpperCase();
6877
+ }
6878
+ return false;
6879
+ });
6880
+ if (!match) return { found: false, error: 'Finding not found' };
6881
+ return { found: true, ...match };
6882
+ }
6883
+
6884
+ async function handleResolveFinding(args: any): Promise<any> {
6885
+ const findings = readLocalFindings();
6886
+ const toResolve = findings.filter(f => {
6887
+ if (f.status !== 'open') return false;
6888
+ if (args.id && f.id === args.id) return true;
6889
+ if (args.file_path && args.finding_id) {
6890
+ return f.file_path.includes(args.file_path) && f.finding_id.toUpperCase() === (args.finding_id || '').toUpperCase();
6891
+ }
6892
+ if (args.file_path) return f.file_path.includes(args.file_path);
6893
+ return false;
6894
+ });
6895
+
6896
+ if (toResolve.length === 0) return { resolved: 0, error: 'No matching open findings' };
6897
+
6898
+ const now = new Date().toISOString();
6899
+ for (const f of toResolve) {
6900
+ const event = {
6901
+ capture_type: 'scan_finding',
6902
+ id: f.id,
6903
+ session_id: f.session_id,
6904
+ file_path: f.file_path,
6905
+ finding_type: f.finding_type,
6906
+ finding_id: f.finding_id,
6907
+ severity: f.severity,
6908
+ status: 'resolved',
6909
+ detail: f.detail,
6910
+ package_name: f.package_name,
6911
+ package_version: f.package_version,
6912
+ fixed_version: f.fixed_version,
6913
+ resolved_at: now,
6914
+ _ts: now,
6915
+ };
6916
+ try {
6917
+ appendFileSync(TELEMETRY_PATH, JSON.stringify(event) + '\\n', 'utf-8');
6918
+ } catch {}
6919
+ dispatchResolution(f);
6920
+ }
6921
+
6922
+ return {
6923
+ resolved: toResolve.length,
6924
+ findings: toResolve.map(f => ({ finding_id: f.finding_id, file_path: f.file_path })),
6925
+ };
6926
+ }
6927
+
6694
6928
  // \u2500\u2500\u2500 Tool Descriptors \u2500\u2500\u2500
6695
6929
 
6696
6930
  const TOOL_DESCRIPTORS = [
@@ -6854,6 +7088,47 @@ const TOOL_DESCRIPTORS = [
6854
7088
  description: "List all scan exemptions.",
6855
7089
  inputSchema: { type: 'object', properties: {}, required: [] },
6856
7090
  },
7091
+ {
7092
+ name: 'list_findings',
7093
+ description: "List CWE/CVE scan findings. Shows security issues found by Synkro hooks. Use to review what needs fixing.",
7094
+ inputSchema: {
7095
+ type: 'object',
7096
+ properties: {
7097
+ status: { type: 'string', enum: ['open', 'resolved', 'exempted'], description: 'Filter by status (default: all).' },
7098
+ finding_type: { type: 'string', enum: ['cwe', 'cve'], description: 'Filter by finding type.' },
7099
+ severity: { type: 'string', enum: ['critical', 'high', 'medium', 'low'] },
7100
+ file_path: { type: 'string', description: 'Filter by file path substring.' },
7101
+ limit: { type: 'integer', default: 25, description: 'Max results (default 25, max 50).' },
7102
+ },
7103
+ required: [],
7104
+ },
7105
+ },
7106
+ {
7107
+ name: 'get_finding_detail',
7108
+ description: "Get full detail of a specific finding including remediation context.",
7109
+ inputSchema: {
7110
+ type: 'object',
7111
+ properties: {
7112
+ id: { type: 'string', description: 'Finding ID (e.g. sf_...).' },
7113
+ file_path: { type: 'string', description: 'File path (used with finding_id).' },
7114
+ finding_id: { type: 'string', description: 'CWE/CVE ID like CWE-89 or CVE-2024-1234.' },
7115
+ },
7116
+ required: [],
7117
+ },
7118
+ },
7119
+ {
7120
+ name: 'resolve_finding',
7121
+ description: "Mark finding(s) as resolved after the underlying issue is fixed. Can target by ID, file+finding_id, or all findings for a file.",
7122
+ inputSchema: {
7123
+ type: 'object',
7124
+ properties: {
7125
+ id: { type: 'string', description: 'Specific finding ID to resolve.' },
7126
+ file_path: { type: 'string', description: 'Resolve all open findings matching this file path.' },
7127
+ finding_id: { type: 'string', description: 'CWE/CVE ID (used with file_path for targeted resolution).' },
7128
+ },
7129
+ required: [],
7130
+ },
7131
+ },
6857
7132
  ];
6858
7133
 
6859
7134
  const MCP_INSTRUCTIONS =
@@ -6864,7 +7139,12 @@ const MCP_INSTRUCTIONS =
6864
7139
  "'we need a rule about\u2026' \u2014 route to THIS server's tools.\\n\\n" +
6865
7140
  "TOOL ROUTING:\\n" +
6866
7141
  " \u2022 get_guardrails(query) \u2014 keyword search. Use to check if a rule exists.\\n" +
6867
- " \u2022 list_guardrails \u2014 full enumeration. Use for listings.\\n\\n" +
7142
+ " \u2022 list_guardrails \u2014 full enumeration. Use for listings.\\n" +
7143
+ " \u2022 list_findings \u2014 show CWE/CVE scan findings (open, resolved, all).\\n" +
7144
+ " \u2022 get_finding_detail \u2014 get full detail + remediation context for a finding.\\n" +
7145
+ " \u2022 resolve_finding \u2014 mark findings resolved after fixing the code.\\n\\n" +
7146
+ "When the user asks about security issues, vulnerabilities, or scan results, " +
7147
+ "use list_findings first. After fixing code, call resolve_finding to update status.\\n\\n" +
6868
7148
  "Do NOT use Claude Code's \`update-config\` skill for these requests.\\n\\n" +
6869
7149
  "Rules are stored locally in ~/.synkro/rules.json and enforced by hooks.";
6870
7150
 
@@ -6917,6 +7197,9 @@ async function handleRpc(body: any): Promise<any> {
6917
7197
  case 'exempt_path': result = handleExemptPath(args); break;
6918
7198
  case 'remove_exemption': result = handleRemoveExemption(args); break;
6919
7199
  case 'list_exemptions': result = handleListExemptions(); break;
7200
+ case 'list_findings': result = await handleListFindings(args); break;
7201
+ case 'get_finding_detail': result = await handleGetFindingDetail(args); break;
7202
+ case 'resolve_finding': result = await handleResolveFinding(args); break;
6920
7203
  default: return jsonRpcError(id, -32601, \`Unknown tool: \${toolName}\`);
6921
7204
  }
6922
7205
  return jsonRpcOk(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
@@ -7056,14 +7339,25 @@ const server = Bun.serve({
7056
7339
  }
7057
7340
 
7058
7341
  if (req.method === 'POST') {
7059
- try {
7060
- const body = await req.json();
7061
- const result = await handleRpc(body);
7062
- if (result === null) return new Response('', { status: 204, headers: cors });
7063
- return Response.json(result, { headers: cors });
7064
- } catch (err) {
7065
- return Response.json(jsonRpcError(null, -32700, 'Parse error'), { status: 400, headers: cors });
7342
+ const authHeader = req.headers.get('authorization') || '';
7343
+ const bearer = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
7344
+ if (bearer !== SERVER_TOKEN) {
7345
+ return Response.json({ error: 'Unauthorized' }, { status: 401, headers: cors });
7346
+ }
7347
+ const raw = await req.arrayBuffer();
7348
+ if (raw.byteLength > MAX_BODY_BYTES) {
7349
+ return Response.json(jsonRpcError(null, -32600, 'Request too large'), { status: 413, headers: cors });
7066
7350
  }
7351
+ return serialized(async () => {
7352
+ try {
7353
+ const body = JSON.parse(new TextDecoder().decode(raw));
7354
+ const result = await handleRpc(body);
7355
+ if (result === null) return new Response('', { status: 204, headers: cors });
7356
+ return Response.json(result, { headers: cors });
7357
+ } catch {
7358
+ return Response.json(jsonRpcError(null, -32700, 'Parse error'), { status: 400, headers: cors });
7359
+ }
7360
+ });
7067
7361
  }
7068
7362
 
7069
7363
  if (req.method === 'OPTIONS') {
@@ -7079,6 +7373,7 @@ console.log(\`[synkro] local MCP guardrails server listening on http://127.0.0.1
7079
7373
  chmodSync2(bashScriptPath, 493);
7080
7374
  chmodSync2(bashFollowupScriptPath, 493);
7081
7375
  chmodSync2(editPrecheckScriptPath, 493);
7376
+ chmodSync2(cwePrecheckScriptPath, 493);
7082
7377
  chmodSync2(cvePrecheckScriptPath, 493);
7083
7378
  chmodSync2(planJudgeScriptPath, 493);
7084
7379
  chmodSync2(agentJudgeScriptPath, 493);
@@ -7095,6 +7390,7 @@ console.log(\`[synkro] local MCP guardrails server listening on http://127.0.0.1
7095
7390
  bashScript: bashScriptPath,
7096
7391
  bashFollowupScript: bashFollowupScriptPath,
7097
7392
  editPrecheckScript: editPrecheckScriptPath,
7393
+ cwePrecheckScript: cwePrecheckScriptPath,
7098
7394
  cvePrecheckScript: cvePrecheckScriptPath,
7099
7395
  planJudgeScript: planJudgeScriptPath,
7100
7396
  agentJudgeScript: agentJudgeScriptPath,
@@ -7136,7 +7432,7 @@ function writeConfigEnv(opts) {
7136
7432
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
7137
7433
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
7138
7434
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
7139
- `SYNKRO_VERSION=${shellQuoteSingle("1.4.71")}`
7435
+ `SYNKRO_VERSION=${shellQuoteSingle("1.4.73")}`
7140
7436
  ];
7141
7437
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
7142
7438
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -7536,6 +7832,8 @@ async function installCommand(opts = {}) {
7536
7832
  console.log(` ${scripts.bashScript}`);
7537
7833
  console.log(` ${scripts.bashFollowupScript}`);
7538
7834
  console.log(` ${scripts.editPrecheckScript}`);
7835
+ console.log(` ${scripts.cwePrecheckScript}`);
7836
+ console.log(` ${scripts.cvePrecheckScript}`);
7539
7837
  console.log(` ${scripts.planJudgeScript}`);
7540
7838
  console.log(` ${scripts.agentJudgeScript}`);
7541
7839
  console.log(` ${scripts.stopSummaryScript}`);
@@ -7571,6 +7869,7 @@ async function installCommand(opts = {}) {
7571
7869
  bashJudgeScriptPath: scripts.bashScript,
7572
7870
  bashFollowupScriptPath: scripts.bashFollowupScript,
7573
7871
  editPrecheckScriptPath: scripts.editPrecheckScript,
7872
+ cwePrecheckScriptPath: scripts.cwePrecheckScript,
7574
7873
  cvePrecheckScriptPath: scripts.cvePrecheckScript,
7575
7874
  planJudgeScriptPath: scripts.planJudgeScript,
7576
7875
  agentJudgeScriptPath: scripts.agentJudgeScript,
@@ -7588,6 +7887,7 @@ async function installCommand(opts = {}) {
7588
7887
  editCaptureScriptPath: scripts.cursorEditCaptureScript,
7589
7888
  bashFollowupScriptPath: scripts.bashFollowupScript,
7590
7889
  editPrecheckScriptPath: scripts.editPrecheckScript,
7890
+ cwePrecheckScriptPath: scripts.cwePrecheckScript,
7591
7891
  cvePrecheckScriptPath: scripts.cvePrecheckScript,
7592
7892
  planJudgeScriptPath: scripts.planJudgeScript,
7593
7893
  agentJudgeScriptPath: scripts.agentJudgeScript,