@i4ctime/q-ring 0.4.0 → 0.9.2
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 +341 -10
- package/dist/{chunk-IGNU622R.js → chunk-5JBU7TWN.js} +715 -124
- package/dist/chunk-5JBU7TWN.js.map +1 -0
- package/dist/chunk-WG4ZKN7Q.js +1632 -0
- package/dist/chunk-WG4ZKN7Q.js.map +1 -0
- package/dist/{dashboard-32PCZF7D.js → dashboard-JT5ZNLT5.js} +41 -16
- package/dist/dashboard-JT5ZNLT5.js.map +1 -0
- package/dist/{dashboard-HVIQO6NT.js → dashboard-Q5OQRQCX.js} +41 -16
- package/dist/dashboard-Q5OQRQCX.js.map +1 -0
- package/dist/index.js +1213 -39
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +1066 -48
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-6IQ5SFLI.js +0 -967
- package/dist/chunk-6IQ5SFLI.js.map +0 -1
- package/dist/chunk-IGNU622R.js.map +0 -1
- package/dist/dashboard-32PCZF7D.js.map +0 -1
- package/dist/dashboard-HVIQO6NT.js.map +0 -1
package/dist/mcp.js
CHANGED
|
@@ -1,29 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
checkDecay,
|
|
4
|
+
checkExecPolicy,
|
|
5
|
+
checkKeyReadPolicy,
|
|
6
|
+
checkToolPolicy,
|
|
4
7
|
collapseEnvironment,
|
|
5
8
|
deleteSecret,
|
|
6
9
|
detectAnomalies,
|
|
7
10
|
disentangleSecrets,
|
|
8
11
|
entangleSecrets,
|
|
12
|
+
exportAudit,
|
|
9
13
|
exportSecrets,
|
|
10
14
|
fireHooks,
|
|
11
15
|
getEnvelope,
|
|
16
|
+
getExecMaxRuntime,
|
|
17
|
+
getPolicySummary,
|
|
12
18
|
getSecret,
|
|
13
19
|
hasSecret,
|
|
20
|
+
httpRequest_,
|
|
14
21
|
listHooks,
|
|
15
22
|
listSecrets,
|
|
16
23
|
logAudit,
|
|
17
24
|
queryAudit,
|
|
18
25
|
readProjectConfig,
|
|
19
26
|
registerHook,
|
|
27
|
+
registry,
|
|
20
28
|
removeHook,
|
|
21
29
|
setSecret,
|
|
22
30
|
tunnelCreate,
|
|
23
31
|
tunnelDestroy,
|
|
24
32
|
tunnelList,
|
|
25
|
-
tunnelRead
|
|
26
|
-
|
|
33
|
+
tunnelRead,
|
|
34
|
+
verifyAuditChain
|
|
35
|
+
} from "./chunk-5JBU7TWN.js";
|
|
27
36
|
|
|
28
37
|
// src/mcp.ts
|
|
29
38
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -317,33 +326,443 @@ function importDotenv(filePathOrContent, options = {}) {
|
|
|
317
326
|
return result;
|
|
318
327
|
}
|
|
319
328
|
|
|
320
|
-
// src/core/
|
|
321
|
-
import {
|
|
322
|
-
import {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
329
|
+
// src/core/exec.ts
|
|
330
|
+
import { spawn } from "child_process";
|
|
331
|
+
import { Transform } from "stream";
|
|
332
|
+
var BUILTIN_PROFILES = {
|
|
333
|
+
unrestricted: { name: "unrestricted" },
|
|
334
|
+
restricted: {
|
|
335
|
+
name: "restricted",
|
|
336
|
+
denyCommands: ["curl", "wget", "ssh", "scp", "nc", "netcat", "ncat"],
|
|
337
|
+
maxRuntimeSeconds: 30,
|
|
338
|
+
allowNetwork: false,
|
|
339
|
+
stripEnvVars: ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"]
|
|
340
|
+
},
|
|
341
|
+
ci: {
|
|
342
|
+
name: "ci",
|
|
343
|
+
maxRuntimeSeconds: 300,
|
|
344
|
+
allowNetwork: true,
|
|
345
|
+
denyCommands: ["rm -rf /", "mkfs", "dd if="]
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
function getProfile(name) {
|
|
349
|
+
if (!name) return BUILTIN_PROFILES.unrestricted;
|
|
350
|
+
return BUILTIN_PROFILES[name] ?? { name };
|
|
351
|
+
}
|
|
352
|
+
var RedactionTransform = class extends Transform {
|
|
353
|
+
patterns = [];
|
|
354
|
+
tail = "";
|
|
355
|
+
maxLen = 0;
|
|
356
|
+
constructor(secretsToRedact) {
|
|
357
|
+
super();
|
|
358
|
+
const validSecrets = secretsToRedact.filter((s) => s.length > 5);
|
|
359
|
+
validSecrets.sort((a, b) => b.length - a.length);
|
|
360
|
+
this.patterns = validSecrets.map((s) => ({
|
|
361
|
+
value: s,
|
|
362
|
+
replacement: "[QRING:REDACTED]"
|
|
363
|
+
}));
|
|
364
|
+
if (validSecrets.length > 0) {
|
|
365
|
+
this.maxLen = validSecrets[0].length;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
_transform(chunk, encoding, callback) {
|
|
369
|
+
if (this.patterns.length === 0) {
|
|
370
|
+
this.push(chunk);
|
|
371
|
+
return callback();
|
|
372
|
+
}
|
|
373
|
+
const text2 = this.tail + chunk.toString();
|
|
374
|
+
let redacted = text2;
|
|
375
|
+
for (const { value, replacement } of this.patterns) {
|
|
376
|
+
redacted = redacted.split(value).join(replacement);
|
|
377
|
+
}
|
|
378
|
+
if (redacted.length < this.maxLen) {
|
|
379
|
+
this.tail = redacted;
|
|
380
|
+
return callback();
|
|
381
|
+
}
|
|
382
|
+
const outputLen = redacted.length - this.maxLen + 1;
|
|
383
|
+
const output = redacted.slice(0, outputLen);
|
|
384
|
+
this.tail = redacted.slice(outputLen);
|
|
385
|
+
this.push(output);
|
|
386
|
+
callback();
|
|
387
|
+
}
|
|
388
|
+
_flush(callback) {
|
|
389
|
+
if (this.tail) {
|
|
390
|
+
let final = this.tail;
|
|
391
|
+
for (const { value, replacement } of this.patterns) {
|
|
392
|
+
final = final.split(value).join(replacement);
|
|
337
393
|
}
|
|
394
|
+
this.push(final);
|
|
395
|
+
}
|
|
396
|
+
callback();
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
async function execCommand(opts2) {
|
|
400
|
+
const profile = getProfile(opts2.profile);
|
|
401
|
+
const fullCommand = [opts2.command, ...opts2.args].join(" ");
|
|
402
|
+
const policyDecision = checkExecPolicy(fullCommand, opts2.projectPath);
|
|
403
|
+
if (!policyDecision.allowed) {
|
|
404
|
+
throw new Error(`Policy Denied: ${policyDecision.reason}`);
|
|
405
|
+
}
|
|
406
|
+
if (profile.denyCommands) {
|
|
407
|
+
const denied = profile.denyCommands.find((d) => fullCommand.includes(d));
|
|
408
|
+
if (denied) {
|
|
409
|
+
throw new Error(`Exec profile "${profile.name}" denies command containing "${denied}"`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (profile.allowCommands) {
|
|
413
|
+
const allowed = profile.allowCommands.some((a) => fullCommand.startsWith(a));
|
|
414
|
+
if (!allowed) {
|
|
415
|
+
throw new Error(`Exec profile "${profile.name}" does not allow command "${opts2.command}"`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const envMap = {};
|
|
419
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
420
|
+
if (v !== void 0) envMap[k] = v;
|
|
421
|
+
}
|
|
422
|
+
if (profile.stripEnvVars) {
|
|
423
|
+
for (const key of profile.stripEnvVars) {
|
|
424
|
+
delete envMap[key];
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const secretsToRedact = /* @__PURE__ */ new Set();
|
|
428
|
+
let entries = listSecrets({
|
|
429
|
+
scope: opts2.scope,
|
|
430
|
+
projectPath: opts2.projectPath,
|
|
431
|
+
source: opts2.source ?? "cli",
|
|
432
|
+
silent: true
|
|
433
|
+
// list silently
|
|
434
|
+
});
|
|
435
|
+
if (opts2.keys?.length) {
|
|
436
|
+
const keySet = new Set(opts2.keys);
|
|
437
|
+
entries = entries.filter((e) => keySet.has(e.key));
|
|
438
|
+
}
|
|
439
|
+
if (opts2.tags?.length) {
|
|
440
|
+
entries = entries.filter(
|
|
441
|
+
(e) => opts2.tags.some((t) => e.envelope?.meta.tags?.includes(t))
|
|
338
442
|
);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
443
|
+
}
|
|
444
|
+
for (const entry of entries) {
|
|
445
|
+
if (entry.envelope) {
|
|
446
|
+
const decay = checkDecay(entry.envelope);
|
|
447
|
+
if (decay.isExpired) continue;
|
|
448
|
+
}
|
|
449
|
+
const val = getSecret(entry.key, {
|
|
450
|
+
scope: entry.scope,
|
|
451
|
+
projectPath: opts2.projectPath,
|
|
452
|
+
env: opts2.env,
|
|
453
|
+
source: opts2.source ?? "cli",
|
|
454
|
+
silent: false
|
|
455
|
+
// Log access for execution
|
|
456
|
+
});
|
|
457
|
+
if (val !== null) {
|
|
458
|
+
envMap[entry.key] = val;
|
|
459
|
+
if (val.length > 5) {
|
|
460
|
+
secretsToRedact.add(val);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const maxRuntime = profile.maxRuntimeSeconds ?? getExecMaxRuntime(opts2.projectPath);
|
|
465
|
+
return new Promise((resolve, reject) => {
|
|
466
|
+
const networkTools = /* @__PURE__ */ new Set([
|
|
467
|
+
"curl",
|
|
468
|
+
"wget",
|
|
469
|
+
"ping",
|
|
470
|
+
"nc",
|
|
471
|
+
"netcat",
|
|
472
|
+
"ssh",
|
|
473
|
+
"telnet",
|
|
474
|
+
"ftp",
|
|
475
|
+
"dig",
|
|
476
|
+
"nslookup"
|
|
477
|
+
]);
|
|
478
|
+
if (profile.allowNetwork === false && networkTools.has(opts2.command)) {
|
|
479
|
+
const msg = `[QRING] Execution blocked: network access is disabled for profile "${profile.name}", command "${opts2.command}" is considered network-related`;
|
|
480
|
+
if (opts2.captureOutput) {
|
|
481
|
+
return resolve({ code: 126, stdout: "", stderr: msg });
|
|
482
|
+
}
|
|
483
|
+
process.stderr.write(msg + "\n");
|
|
484
|
+
return resolve({ code: 126, stdout: "", stderr: "" });
|
|
485
|
+
}
|
|
486
|
+
const child = spawn(opts2.command, opts2.args, {
|
|
487
|
+
env: envMap,
|
|
488
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
489
|
+
shell: false
|
|
490
|
+
});
|
|
491
|
+
let timedOut = false;
|
|
492
|
+
let timer;
|
|
493
|
+
if (maxRuntime) {
|
|
494
|
+
timer = setTimeout(() => {
|
|
495
|
+
timedOut = true;
|
|
496
|
+
child.kill("SIGKILL");
|
|
497
|
+
}, maxRuntime * 1e3);
|
|
498
|
+
}
|
|
499
|
+
const stdoutRedact = new RedactionTransform([...secretsToRedact]);
|
|
500
|
+
const stderrRedact = new RedactionTransform([...secretsToRedact]);
|
|
501
|
+
if (child.stdout) child.stdout.pipe(stdoutRedact);
|
|
502
|
+
if (child.stderr) child.stderr.pipe(stderrRedact);
|
|
503
|
+
let stdoutStr = "";
|
|
504
|
+
let stderrStr = "";
|
|
505
|
+
if (opts2.captureOutput) {
|
|
506
|
+
stdoutRedact.on("data", (d) => stdoutStr += d.toString());
|
|
507
|
+
stderrRedact.on("data", (d) => stderrStr += d.toString());
|
|
508
|
+
} else {
|
|
509
|
+
stdoutRedact.pipe(process.stdout);
|
|
510
|
+
stderrRedact.pipe(process.stderr);
|
|
511
|
+
}
|
|
512
|
+
child.on("close", (code) => {
|
|
513
|
+
if (timer) clearTimeout(timer);
|
|
514
|
+
if (timedOut) {
|
|
515
|
+
resolve({ code: 124, stdout: stdoutStr, stderr: stderrStr + `
|
|
516
|
+
[QRING] Process killed: exceeded ${maxRuntime}s runtime limit` });
|
|
517
|
+
} else {
|
|
518
|
+
resolve({ code: code ?? 0, stdout: stdoutStr, stderr: stderrStr });
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
child.on("error", (err) => {
|
|
522
|
+
if (timer) clearTimeout(timer);
|
|
523
|
+
reject(err);
|
|
343
524
|
});
|
|
344
|
-
req.end();
|
|
345
525
|
});
|
|
346
526
|
}
|
|
527
|
+
|
|
528
|
+
// src/core/scan.ts
|
|
529
|
+
import { readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
530
|
+
import { join } from "path";
|
|
531
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
532
|
+
"node_modules",
|
|
533
|
+
".git",
|
|
534
|
+
".next",
|
|
535
|
+
"dist",
|
|
536
|
+
"build",
|
|
537
|
+
"coverage",
|
|
538
|
+
".cursor",
|
|
539
|
+
"venv",
|
|
540
|
+
"__pycache__"
|
|
541
|
+
]);
|
|
542
|
+
var IGNORE_EXTS = /* @__PURE__ */ new Set([
|
|
543
|
+
".png",
|
|
544
|
+
".jpg",
|
|
545
|
+
".jpeg",
|
|
546
|
+
".gif",
|
|
547
|
+
".ico",
|
|
548
|
+
".svg",
|
|
549
|
+
".webp",
|
|
550
|
+
".mp4",
|
|
551
|
+
".mp3",
|
|
552
|
+
".wav",
|
|
553
|
+
".ogg",
|
|
554
|
+
".pdf",
|
|
555
|
+
".zip",
|
|
556
|
+
".tar",
|
|
557
|
+
".gz",
|
|
558
|
+
".xz",
|
|
559
|
+
".ttf",
|
|
560
|
+
".woff",
|
|
561
|
+
".woff2",
|
|
562
|
+
".eot",
|
|
563
|
+
".exe",
|
|
564
|
+
".dll",
|
|
565
|
+
".so",
|
|
566
|
+
".dylib",
|
|
567
|
+
".lock"
|
|
568
|
+
]);
|
|
569
|
+
var SECRET_KEYWORDS = /((?:api_?key|secret|token|password|auth|credential|access_?key)[a-z0-9_]*)\s*[:=]\s*(['"])([^'"]+)\2/i;
|
|
570
|
+
function calculateEntropy(str) {
|
|
571
|
+
if (!str) return 0;
|
|
572
|
+
const len = str.length;
|
|
573
|
+
const frequencies = /* @__PURE__ */ new Map();
|
|
574
|
+
for (let i = 0; i < len; i++) {
|
|
575
|
+
const char = str[i];
|
|
576
|
+
frequencies.set(char, (frequencies.get(char) || 0) + 1);
|
|
577
|
+
}
|
|
578
|
+
let entropy = 0;
|
|
579
|
+
for (const count of frequencies.values()) {
|
|
580
|
+
const p = count / len;
|
|
581
|
+
entropy -= p * Math.log2(p);
|
|
582
|
+
}
|
|
583
|
+
return entropy;
|
|
584
|
+
}
|
|
585
|
+
function scanCodebase(dir) {
|
|
586
|
+
const results = [];
|
|
587
|
+
function walk(currentDir) {
|
|
588
|
+
let entries;
|
|
589
|
+
try {
|
|
590
|
+
entries = readdirSync(currentDir);
|
|
591
|
+
} catch {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
for (const entry of entries) {
|
|
595
|
+
if (IGNORE_DIRS.has(entry)) continue;
|
|
596
|
+
const fullPath = join(currentDir, entry);
|
|
597
|
+
let stat;
|
|
598
|
+
try {
|
|
599
|
+
stat = statSync(fullPath);
|
|
600
|
+
} catch {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (stat.isDirectory()) {
|
|
604
|
+
walk(fullPath);
|
|
605
|
+
} else if (stat.isFile()) {
|
|
606
|
+
const ext = fullPath.slice(fullPath.lastIndexOf(".")).toLowerCase();
|
|
607
|
+
if (IGNORE_EXTS.has(ext) || entry.endsWith(".lock")) continue;
|
|
608
|
+
let content;
|
|
609
|
+
try {
|
|
610
|
+
content = readFileSync2(fullPath, "utf8");
|
|
611
|
+
} catch {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
if (content.includes("\0")) continue;
|
|
615
|
+
const lines = content.split(/\r?\n/);
|
|
616
|
+
for (let i = 0; i < lines.length; i++) {
|
|
617
|
+
const line = lines[i];
|
|
618
|
+
if (line.length > 500) continue;
|
|
619
|
+
const match = line.match(SECRET_KEYWORDS);
|
|
620
|
+
if (match) {
|
|
621
|
+
const varName = match[1];
|
|
622
|
+
const value = match[3];
|
|
623
|
+
if (value.length < 8) continue;
|
|
624
|
+
const lowerValue = value.toLowerCase();
|
|
625
|
+
if (lowerValue.includes("example") || lowerValue.includes("your_") || lowerValue.includes("placeholder") || lowerValue.includes("replace_me")) {
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const entropy = calculateEntropy(value);
|
|
629
|
+
if (entropy > 3.5 || value.startsWith("sk-") || value.startsWith("ghp_")) {
|
|
630
|
+
const relPath = fullPath.startsWith(dir) ? fullPath.slice(dir.length).replace(/^[/\\]+/, "") : fullPath;
|
|
631
|
+
results.push({
|
|
632
|
+
file: relPath || fullPath,
|
|
633
|
+
line: i + 1,
|
|
634
|
+
keyName: varName,
|
|
635
|
+
match: value,
|
|
636
|
+
context: line.trim(),
|
|
637
|
+
entropy: parseFloat(entropy.toFixed(2))
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
walk(dir);
|
|
646
|
+
return results;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/core/linter.ts
|
|
650
|
+
import { readFileSync as readFileSync3, writeFileSync, existsSync } from "fs";
|
|
651
|
+
import { basename, extname } from "path";
|
|
652
|
+
var ENV_REF_BY_EXT = {
|
|
653
|
+
".ts": (k) => `process.env.${k}`,
|
|
654
|
+
".tsx": (k) => `process.env.${k}`,
|
|
655
|
+
".js": (k) => `process.env.${k}`,
|
|
656
|
+
".jsx": (k) => `process.env.${k}`,
|
|
657
|
+
".mjs": (k) => `process.env.${k}`,
|
|
658
|
+
".cjs": (k) => `process.env.${k}`,
|
|
659
|
+
".py": (k) => `os.environ["${k}"]`,
|
|
660
|
+
".rb": (k) => `ENV["${k}"]`,
|
|
661
|
+
".go": (k) => `os.Getenv("${k}")`,
|
|
662
|
+
".rs": (k) => `std::env::var("${k}")`,
|
|
663
|
+
".java": (k) => `System.getenv("${k}")`,
|
|
664
|
+
".kt": (k) => `System.getenv("${k}")`,
|
|
665
|
+
".cs": (k) => `Environment.GetEnvironmentVariable("${k}")`,
|
|
666
|
+
".php": (k) => `getenv('${k}')`,
|
|
667
|
+
".sh": (k) => `\${${k}}`,
|
|
668
|
+
".bash": (k) => `\${${k}}`
|
|
669
|
+
};
|
|
670
|
+
function getEnvRef(filePath, keyName) {
|
|
671
|
+
const ext = extname(filePath).toLowerCase();
|
|
672
|
+
const formatter = ENV_REF_BY_EXT[ext];
|
|
673
|
+
return formatter ? formatter(keyName) : `process.env.${keyName}`;
|
|
674
|
+
}
|
|
675
|
+
function lintFiles(files, opts2 = {}) {
|
|
676
|
+
const results = [];
|
|
677
|
+
for (const file of files) {
|
|
678
|
+
if (!existsSync(file)) continue;
|
|
679
|
+
let content;
|
|
680
|
+
try {
|
|
681
|
+
content = readFileSync3(file, "utf8");
|
|
682
|
+
} catch {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (content.includes("\0")) continue;
|
|
686
|
+
const SECRET_KEYWORDS2 = /((?:api_?key|secret|token|password|auth|credential|access_?key)[a-z0-9_]*)\s*[:=]\s*(['"])([^'"]+)\2/gi;
|
|
687
|
+
const lines = content.split(/\r?\n/);
|
|
688
|
+
const fixes = [];
|
|
689
|
+
for (let i = 0; i < lines.length; i++) {
|
|
690
|
+
const line = lines[i];
|
|
691
|
+
if (line.length > 500) continue;
|
|
692
|
+
let match;
|
|
693
|
+
SECRET_KEYWORDS2.lastIndex = 0;
|
|
694
|
+
while ((match = SECRET_KEYWORDS2.exec(line)) !== null) {
|
|
695
|
+
const varName = match[1].toUpperCase();
|
|
696
|
+
const quote = match[2];
|
|
697
|
+
const value = match[3];
|
|
698
|
+
if (value.length < 8) continue;
|
|
699
|
+
const lv = value.toLowerCase();
|
|
700
|
+
if (lv.includes("example") || lv.includes("your_") || lv.includes("placeholder") || lv.includes("replace_me") || lv.includes("xxx")) continue;
|
|
701
|
+
const entropy = calculateEntropy2(value);
|
|
702
|
+
if (entropy <= 3.5 && !value.startsWith("sk-") && !value.startsWith("ghp_")) continue;
|
|
703
|
+
const shouldFix = opts2.fix === true;
|
|
704
|
+
if (shouldFix) {
|
|
705
|
+
const envRef = getEnvRef(file, varName);
|
|
706
|
+
fixes.push({
|
|
707
|
+
line: i,
|
|
708
|
+
original: `${quote}${value}${quote}`,
|
|
709
|
+
replacement: envRef,
|
|
710
|
+
keyName: varName,
|
|
711
|
+
value
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
results.push({
|
|
715
|
+
file,
|
|
716
|
+
line: i + 1,
|
|
717
|
+
keyName: varName,
|
|
718
|
+
match: value,
|
|
719
|
+
context: line.trim(),
|
|
720
|
+
entropy: parseFloat(entropy.toFixed(2)),
|
|
721
|
+
fixed: shouldFix
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (opts2.fix && fixes.length > 0) {
|
|
726
|
+
const fixLines = content.split(/\r?\n/);
|
|
727
|
+
for (const fix of fixes.reverse()) {
|
|
728
|
+
const lineIdx = fix.line;
|
|
729
|
+
if (lineIdx >= 0 && lineIdx < fixLines.length) {
|
|
730
|
+
fixLines[lineIdx] = fixLines[lineIdx].replace(fix.original, fix.replacement);
|
|
731
|
+
}
|
|
732
|
+
if (!hasSecret(fix.keyName, { scope: opts2.scope, projectPath: opts2.projectPath })) {
|
|
733
|
+
setSecret(fix.keyName, fix.value, {
|
|
734
|
+
scope: opts2.scope ?? "global",
|
|
735
|
+
projectPath: opts2.projectPath,
|
|
736
|
+
source: "cli",
|
|
737
|
+
description: `Auto-imported from ${basename(file)}:${fix.line + 1}`
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
writeFileSync(file, fixLines.join("\n"), "utf8");
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return results;
|
|
745
|
+
}
|
|
746
|
+
function calculateEntropy2(str) {
|
|
747
|
+
if (!str) return 0;
|
|
748
|
+
const len = str.length;
|
|
749
|
+
const frequencies = /* @__PURE__ */ new Map();
|
|
750
|
+
for (let i = 0; i < len; i++) {
|
|
751
|
+
const ch = str[i];
|
|
752
|
+
frequencies.set(ch, (frequencies.get(ch) || 0) + 1);
|
|
753
|
+
}
|
|
754
|
+
let entropy = 0;
|
|
755
|
+
for (const count of frequencies.values()) {
|
|
756
|
+
const p = count / len;
|
|
757
|
+
entropy -= p * Math.log2(p);
|
|
758
|
+
}
|
|
759
|
+
return entropy;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// src/core/validate.ts
|
|
763
|
+
function makeRequest(url, headers, timeoutMs = 1e4) {
|
|
764
|
+
return httpRequest_({ url, method: "GET", headers, timeoutMs });
|
|
765
|
+
}
|
|
347
766
|
var ProviderRegistry = class {
|
|
348
767
|
providers = /* @__PURE__ */ new Map();
|
|
349
768
|
register(provider) {
|
|
@@ -490,14 +909,14 @@ var httpProvider = {
|
|
|
490
909
|
}
|
|
491
910
|
}
|
|
492
911
|
};
|
|
493
|
-
var
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
912
|
+
var registry2 = new ProviderRegistry();
|
|
913
|
+
registry2.register(openaiProvider);
|
|
914
|
+
registry2.register(stripeProvider);
|
|
915
|
+
registry2.register(githubProvider);
|
|
916
|
+
registry2.register(awsProvider);
|
|
917
|
+
registry2.register(httpProvider);
|
|
499
918
|
async function validateSecret(value, opts2) {
|
|
500
|
-
const provider = opts2?.provider ?
|
|
919
|
+
const provider = opts2?.provider ? registry2.get(opts2.provider) : registry2.detectProvider(value);
|
|
501
920
|
if (!provider) {
|
|
502
921
|
return {
|
|
503
922
|
valid: false,
|
|
@@ -512,6 +931,192 @@ async function validateSecret(value, opts2) {
|
|
|
512
931
|
}
|
|
513
932
|
return provider.validate(value);
|
|
514
933
|
}
|
|
934
|
+
async function rotateWithProvider(value, providerName) {
|
|
935
|
+
const provider = providerName ? registry2.get(providerName) : registry2.detectProvider(value);
|
|
936
|
+
if (!provider) {
|
|
937
|
+
return { rotated: false, provider: "none", message: "No provider detected for rotation" };
|
|
938
|
+
}
|
|
939
|
+
const rotatable = provider;
|
|
940
|
+
if (rotatable.supportsRotation && rotatable.rotate) {
|
|
941
|
+
return rotatable.rotate(value);
|
|
942
|
+
}
|
|
943
|
+
const format = "api-key";
|
|
944
|
+
const newValue = generateSecret({ format, length: 48 });
|
|
945
|
+
return {
|
|
946
|
+
rotated: true,
|
|
947
|
+
provider: provider.name,
|
|
948
|
+
message: `Provider "${provider.name}" does not support native rotation \u2014 generated new value locally`,
|
|
949
|
+
newValue
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
async function ciValidateBatch(secrets) {
|
|
953
|
+
const results = [];
|
|
954
|
+
for (const s of secrets) {
|
|
955
|
+
const validation = await validateSecret(s.value, {
|
|
956
|
+
provider: s.provider,
|
|
957
|
+
validationUrl: s.validationUrl
|
|
958
|
+
});
|
|
959
|
+
results.push({
|
|
960
|
+
key: s.key,
|
|
961
|
+
validation,
|
|
962
|
+
requiresRotation: validation.status === "invalid"
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
const failCount = results.filter((r) => !r.validation.valid).length;
|
|
966
|
+
return { results, allValid: failCount === 0, failCount };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/core/context.ts
|
|
970
|
+
function getProjectContext(opts2 = {}) {
|
|
971
|
+
const projectPath = opts2.projectPath ?? process.cwd();
|
|
972
|
+
const envResult = collapseEnvironment({ projectPath });
|
|
973
|
+
const secretsList = listSecrets({
|
|
974
|
+
...opts2,
|
|
975
|
+
projectPath,
|
|
976
|
+
silent: true
|
|
977
|
+
});
|
|
978
|
+
let expiredCount = 0;
|
|
979
|
+
let staleCount = 0;
|
|
980
|
+
let protectedCount = 0;
|
|
981
|
+
const secrets = secretsList.map((entry) => {
|
|
982
|
+
const meta = entry.envelope?.meta;
|
|
983
|
+
const decay = entry.decay;
|
|
984
|
+
if (decay?.isExpired) expiredCount++;
|
|
985
|
+
if (decay?.isStale) staleCount++;
|
|
986
|
+
if (meta?.requiresApproval) protectedCount++;
|
|
987
|
+
return {
|
|
988
|
+
key: entry.key,
|
|
989
|
+
scope: entry.scope,
|
|
990
|
+
tags: meta?.tags,
|
|
991
|
+
description: meta?.description,
|
|
992
|
+
provider: meta?.provider,
|
|
993
|
+
requiresApproval: meta?.requiresApproval,
|
|
994
|
+
jitProvider: meta?.jitProvider,
|
|
995
|
+
hasStates: !!(entry.envelope?.states && Object.keys(entry.envelope.states).length > 0),
|
|
996
|
+
isExpired: decay?.isExpired ?? false,
|
|
997
|
+
isStale: decay?.isStale ?? false,
|
|
998
|
+
timeRemaining: decay?.timeRemaining ?? null,
|
|
999
|
+
accessCount: meta?.accessCount ?? 0,
|
|
1000
|
+
lastAccessed: meta?.lastAccessedAt ?? null,
|
|
1001
|
+
rotationFormat: meta?.rotationFormat
|
|
1002
|
+
};
|
|
1003
|
+
});
|
|
1004
|
+
let manifest = null;
|
|
1005
|
+
const config = readProjectConfig(projectPath);
|
|
1006
|
+
if (config?.secrets) {
|
|
1007
|
+
const declaredKeys = Object.keys(config.secrets);
|
|
1008
|
+
const existingKeys = new Set(secrets.map((s) => s.key));
|
|
1009
|
+
const missing = declaredKeys.filter((k) => !existingKeys.has(k));
|
|
1010
|
+
manifest = { declared: declaredKeys.length, missing };
|
|
1011
|
+
}
|
|
1012
|
+
const recentEvents = queryAudit({ limit: 20 });
|
|
1013
|
+
const recentActions = recentEvents.map((e) => ({
|
|
1014
|
+
action: e.action,
|
|
1015
|
+
key: e.key,
|
|
1016
|
+
source: e.source,
|
|
1017
|
+
timestamp: e.timestamp
|
|
1018
|
+
}));
|
|
1019
|
+
return {
|
|
1020
|
+
projectPath,
|
|
1021
|
+
environment: envResult ? { env: envResult.env, source: envResult.source } : null,
|
|
1022
|
+
secrets,
|
|
1023
|
+
totalSecrets: secrets.length,
|
|
1024
|
+
expiredCount,
|
|
1025
|
+
staleCount,
|
|
1026
|
+
protectedCount,
|
|
1027
|
+
manifest,
|
|
1028
|
+
validationProviders: registry2.listProviders().map((p) => p.name),
|
|
1029
|
+
jitProviders: registry.listProviders().map((p) => p.name),
|
|
1030
|
+
hooksCount: listHooks().length,
|
|
1031
|
+
recentActions
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// src/core/memory.ts
|
|
1036
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync } from "fs";
|
|
1037
|
+
import { join as join2 } from "path";
|
|
1038
|
+
import { homedir, hostname, userInfo } from "os";
|
|
1039
|
+
import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, createHash, randomBytes as randomBytes3 } from "crypto";
|
|
1040
|
+
var MEMORY_FILE = "agent-memory.enc";
|
|
1041
|
+
function getMemoryDir() {
|
|
1042
|
+
const dir = join2(homedir(), ".config", "q-ring");
|
|
1043
|
+
if (!existsSync2(dir)) {
|
|
1044
|
+
mkdirSync(dir, { recursive: true });
|
|
1045
|
+
}
|
|
1046
|
+
return dir;
|
|
1047
|
+
}
|
|
1048
|
+
function getMemoryPath() {
|
|
1049
|
+
return join2(getMemoryDir(), MEMORY_FILE);
|
|
1050
|
+
}
|
|
1051
|
+
function deriveKey2() {
|
|
1052
|
+
const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
|
|
1053
|
+
return createHash("sha256").update(fingerprint).digest();
|
|
1054
|
+
}
|
|
1055
|
+
function encrypt(data) {
|
|
1056
|
+
const key = deriveKey2();
|
|
1057
|
+
const iv = randomBytes3(12);
|
|
1058
|
+
const cipher = createCipheriv2("aes-256-gcm", key, iv);
|
|
1059
|
+
const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
|
|
1060
|
+
const tag = cipher.getAuthTag();
|
|
1061
|
+
return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
|
|
1062
|
+
}
|
|
1063
|
+
function decrypt(blob) {
|
|
1064
|
+
const parts = blob.split(":");
|
|
1065
|
+
if (parts.length !== 3) throw new Error("Invalid encrypted format");
|
|
1066
|
+
const iv = Buffer.from(parts[0], "base64");
|
|
1067
|
+
const tag = Buffer.from(parts[1], "base64");
|
|
1068
|
+
const encrypted = Buffer.from(parts[2], "base64");
|
|
1069
|
+
const key = deriveKey2();
|
|
1070
|
+
const decipher = createDecipheriv2("aes-256-gcm", key, iv);
|
|
1071
|
+
decipher.setAuthTag(tag);
|
|
1072
|
+
return decipher.update(encrypted) + decipher.final("utf8");
|
|
1073
|
+
}
|
|
1074
|
+
function loadStore() {
|
|
1075
|
+
const path = getMemoryPath();
|
|
1076
|
+
if (!existsSync2(path)) {
|
|
1077
|
+
return { entries: {} };
|
|
1078
|
+
}
|
|
1079
|
+
try {
|
|
1080
|
+
const raw = readFileSync4(path, "utf8");
|
|
1081
|
+
const decrypted = decrypt(raw);
|
|
1082
|
+
return JSON.parse(decrypted);
|
|
1083
|
+
} catch {
|
|
1084
|
+
return { entries: {} };
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
function saveStore(store) {
|
|
1088
|
+
const json = JSON.stringify(store);
|
|
1089
|
+
const encrypted = encrypt(json);
|
|
1090
|
+
writeFileSync2(getMemoryPath(), encrypted, "utf8");
|
|
1091
|
+
}
|
|
1092
|
+
function remember(key, value) {
|
|
1093
|
+
const store = loadStore();
|
|
1094
|
+
store.entries[key] = {
|
|
1095
|
+
value,
|
|
1096
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1097
|
+
};
|
|
1098
|
+
saveStore(store);
|
|
1099
|
+
}
|
|
1100
|
+
function recall(key) {
|
|
1101
|
+
const store = loadStore();
|
|
1102
|
+
return store.entries[key]?.value ?? null;
|
|
1103
|
+
}
|
|
1104
|
+
function listMemory() {
|
|
1105
|
+
const store = loadStore();
|
|
1106
|
+
return Object.entries(store.entries).map(([key, entry]) => ({
|
|
1107
|
+
key,
|
|
1108
|
+
updatedAt: entry.updatedAt
|
|
1109
|
+
}));
|
|
1110
|
+
}
|
|
1111
|
+
function forget(key) {
|
|
1112
|
+
const store = loadStore();
|
|
1113
|
+
if (key in store.entries) {
|
|
1114
|
+
delete store.entries[key];
|
|
1115
|
+
saveStore(store);
|
|
1116
|
+
return true;
|
|
1117
|
+
}
|
|
1118
|
+
return false;
|
|
1119
|
+
}
|
|
515
1120
|
|
|
516
1121
|
// src/mcp/server.ts
|
|
517
1122
|
function text(t, isError = false) {
|
|
@@ -524,16 +1129,27 @@ function opts(params) {
|
|
|
524
1129
|
return {
|
|
525
1130
|
scope: params.scope,
|
|
526
1131
|
projectPath: params.projectPath ?? process.cwd(),
|
|
1132
|
+
teamId: params.teamId,
|
|
1133
|
+
orgId: params.orgId,
|
|
527
1134
|
env: params.env,
|
|
528
1135
|
source: "mcp"
|
|
529
1136
|
};
|
|
530
1137
|
}
|
|
1138
|
+
function enforceToolPolicy(toolName, projectPath) {
|
|
1139
|
+
const decision = checkToolPolicy(toolName, projectPath);
|
|
1140
|
+
if (!decision.allowed) {
|
|
1141
|
+
return text(`Policy Denied: ${decision.reason} (source: ${decision.policySource})`, true);
|
|
1142
|
+
}
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
531
1145
|
function createMcpServer() {
|
|
532
1146
|
const server2 = new McpServer({
|
|
533
1147
|
name: "q-ring",
|
|
534
1148
|
version: "0.2.0"
|
|
535
1149
|
});
|
|
536
|
-
const
|
|
1150
|
+
const teamIdSchema = z.string().optional().describe("Team identifier for team-scoped secrets");
|
|
1151
|
+
const orgIdSchema = z.string().optional().describe("Org identifier for org-scoped secrets");
|
|
1152
|
+
const scopeSchema = z.enum(["global", "project", "team", "org"]).optional().describe("Scope: global, project, team, or org");
|
|
537
1153
|
const projectPathSchema = z.string().optional().describe("Project root path for project-scoped secrets");
|
|
538
1154
|
const envSchema = z.string().optional().describe("Environment for superposition collapse (e.g., dev, staging, prod)");
|
|
539
1155
|
server2.tool(
|
|
@@ -543,12 +1159,24 @@ function createMcpServer() {
|
|
|
543
1159
|
key: z.string().describe("The secret key name"),
|
|
544
1160
|
scope: scopeSchema,
|
|
545
1161
|
projectPath: projectPathSchema,
|
|
546
|
-
env: envSchema
|
|
1162
|
+
env: envSchema,
|
|
1163
|
+
teamId: teamIdSchema,
|
|
1164
|
+
orgId: orgIdSchema
|
|
547
1165
|
},
|
|
548
1166
|
async (params) => {
|
|
549
|
-
const
|
|
550
|
-
if (
|
|
551
|
-
|
|
1167
|
+
const toolBlock = enforceToolPolicy("get_secret", params.projectPath);
|
|
1168
|
+
if (toolBlock) return toolBlock;
|
|
1169
|
+
try {
|
|
1170
|
+
const keyBlock = checkKeyReadPolicy(params.key, void 0, params.projectPath);
|
|
1171
|
+
if (!keyBlock.allowed) {
|
|
1172
|
+
return text(`Policy Denied: ${keyBlock.reason}`, true);
|
|
1173
|
+
}
|
|
1174
|
+
const value = getSecret(params.key, opts(params));
|
|
1175
|
+
if (value === null) return text(`Secret "${params.key}" not found`, true);
|
|
1176
|
+
return text(value);
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
return text(err instanceof Error ? err.message : String(err), true);
|
|
1179
|
+
}
|
|
552
1180
|
}
|
|
553
1181
|
);
|
|
554
1182
|
server2.tool(
|
|
@@ -560,9 +1188,13 @@ function createMcpServer() {
|
|
|
560
1188
|
tag: z.string().optional().describe("Filter by tag"),
|
|
561
1189
|
expired: z.boolean().optional().describe("Show only expired secrets"),
|
|
562
1190
|
stale: z.boolean().optional().describe("Show only stale secrets (75%+ decay)"),
|
|
563
|
-
filter: z.string().optional().describe("Glob pattern on key name (e.g., 'API_*')")
|
|
1191
|
+
filter: z.string().optional().describe("Glob pattern on key name (e.g., 'API_*')"),
|
|
1192
|
+
teamId: teamIdSchema,
|
|
1193
|
+
orgId: orgIdSchema
|
|
564
1194
|
},
|
|
565
1195
|
async (params) => {
|
|
1196
|
+
const toolBlock = enforceToolPolicy("list_secrets", params.projectPath);
|
|
1197
|
+
if (toolBlock) return toolBlock;
|
|
566
1198
|
let entries = listSecrets(opts(params));
|
|
567
1199
|
if (params.tag) {
|
|
568
1200
|
entries = entries.filter(
|
|
@@ -622,9 +1254,13 @@ function createMcpServer() {
|
|
|
622
1254
|
description: z.string().optional().describe("Human-readable description"),
|
|
623
1255
|
tags: z.array(z.string()).optional().describe("Tags for organization"),
|
|
624
1256
|
rotationFormat: z.enum(["hex", "base64", "alphanumeric", "uuid", "api-key", "token", "password"]).optional().describe("Format for auto-rotation when this secret expires"),
|
|
625
|
-
rotationPrefix: z.string().optional().describe("Prefix for auto-rotation (e.g. 'sk-')")
|
|
1257
|
+
rotationPrefix: z.string().optional().describe("Prefix for auto-rotation (e.g. 'sk-')"),
|
|
1258
|
+
teamId: teamIdSchema,
|
|
1259
|
+
orgId: orgIdSchema
|
|
626
1260
|
},
|
|
627
1261
|
async (params) => {
|
|
1262
|
+
const toolBlock = enforceToolPolicy("set_secret", params.projectPath);
|
|
1263
|
+
if (toolBlock) return toolBlock;
|
|
628
1264
|
const o = opts(params);
|
|
629
1265
|
if (params.env) {
|
|
630
1266
|
const existing = getEnvelope(params.key, o);
|
|
@@ -662,9 +1298,13 @@ function createMcpServer() {
|
|
|
662
1298
|
{
|
|
663
1299
|
key: z.string().describe("The secret key name"),
|
|
664
1300
|
scope: scopeSchema,
|
|
665
|
-
projectPath: projectPathSchema
|
|
1301
|
+
projectPath: projectPathSchema,
|
|
1302
|
+
teamId: teamIdSchema,
|
|
1303
|
+
orgId: orgIdSchema
|
|
666
1304
|
},
|
|
667
1305
|
async (params) => {
|
|
1306
|
+
const toolBlock = enforceToolPolicy("delete_secret", params.projectPath);
|
|
1307
|
+
if (toolBlock) return toolBlock;
|
|
668
1308
|
const deleted = deleteSecret(params.key, opts(params));
|
|
669
1309
|
return text(
|
|
670
1310
|
deleted ? `Deleted "${params.key}"` : `Secret "${params.key}" not found`,
|
|
@@ -678,9 +1318,13 @@ function createMcpServer() {
|
|
|
678
1318
|
{
|
|
679
1319
|
key: z.string().describe("The secret key name"),
|
|
680
1320
|
scope: scopeSchema,
|
|
681
|
-
projectPath: projectPathSchema
|
|
1321
|
+
projectPath: projectPathSchema,
|
|
1322
|
+
teamId: teamIdSchema,
|
|
1323
|
+
orgId: orgIdSchema
|
|
682
1324
|
},
|
|
683
1325
|
async (params) => {
|
|
1326
|
+
const toolBlock = enforceToolPolicy("has_secret", params.projectPath);
|
|
1327
|
+
if (toolBlock) return toolBlock;
|
|
684
1328
|
return text(hasSecret(params.key, opts(params)) ? "true" : "false");
|
|
685
1329
|
}
|
|
686
1330
|
);
|
|
@@ -693,9 +1337,13 @@ function createMcpServer() {
|
|
|
693
1337
|
tags: z.array(z.string()).optional().describe("Only export secrets with any of these tags"),
|
|
694
1338
|
scope: scopeSchema,
|
|
695
1339
|
projectPath: projectPathSchema,
|
|
696
|
-
env: envSchema
|
|
1340
|
+
env: envSchema,
|
|
1341
|
+
teamId: teamIdSchema,
|
|
1342
|
+
orgId: orgIdSchema
|
|
697
1343
|
},
|
|
698
1344
|
async (params) => {
|
|
1345
|
+
const toolBlock = enforceToolPolicy("export_secrets", params.projectPath);
|
|
1346
|
+
if (toolBlock) return toolBlock;
|
|
699
1347
|
const output = exportSecrets({
|
|
700
1348
|
...opts(params),
|
|
701
1349
|
format: params.format,
|
|
@@ -717,6 +1365,8 @@ function createMcpServer() {
|
|
|
717
1365
|
dryRun: z.boolean().optional().default(false).describe("Preview what would be imported without saving")
|
|
718
1366
|
},
|
|
719
1367
|
async (params) => {
|
|
1368
|
+
const toolBlock = enforceToolPolicy("import_dotenv", params.projectPath);
|
|
1369
|
+
if (toolBlock) return toolBlock;
|
|
720
1370
|
const result = importDotenv(params.content, {
|
|
721
1371
|
scope: params.scope,
|
|
722
1372
|
projectPath: params.projectPath ?? process.cwd(),
|
|
@@ -743,6 +1393,8 @@ function createMcpServer() {
|
|
|
743
1393
|
projectPath: projectPathSchema
|
|
744
1394
|
},
|
|
745
1395
|
async (params) => {
|
|
1396
|
+
const toolBlock = enforceToolPolicy("check_project", params.projectPath);
|
|
1397
|
+
if (toolBlock) return toolBlock;
|
|
746
1398
|
const projectPath = params.projectPath ?? process.cwd();
|
|
747
1399
|
const config = readProjectConfig(projectPath);
|
|
748
1400
|
if (!config?.secrets || Object.keys(config.secrets).length === 0) {
|
|
@@ -793,6 +1445,8 @@ function createMcpServer() {
|
|
|
793
1445
|
env: envSchema
|
|
794
1446
|
},
|
|
795
1447
|
async (params) => {
|
|
1448
|
+
const toolBlock = enforceToolPolicy("env_generate", params.projectPath);
|
|
1449
|
+
if (toolBlock) return toolBlock;
|
|
796
1450
|
const projectPath = params.projectPath ?? process.cwd();
|
|
797
1451
|
const config = readProjectConfig(projectPath);
|
|
798
1452
|
if (!config?.secrets || Object.keys(config.secrets).length === 0) {
|
|
@@ -836,9 +1490,13 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
|
|
|
836
1490
|
{
|
|
837
1491
|
key: z.string().describe("The secret key name"),
|
|
838
1492
|
scope: scopeSchema,
|
|
839
|
-
projectPath: projectPathSchema
|
|
1493
|
+
projectPath: projectPathSchema,
|
|
1494
|
+
teamId: teamIdSchema,
|
|
1495
|
+
orgId: orgIdSchema
|
|
840
1496
|
},
|
|
841
1497
|
async (params) => {
|
|
1498
|
+
const toolBlock = enforceToolPolicy("inspect_secret", params.projectPath);
|
|
1499
|
+
if (toolBlock) return toolBlock;
|
|
842
1500
|
const result = getEnvelope(params.key, opts(params));
|
|
843
1501
|
if (!result) return text(`Secret "${params.key}" not found`, true);
|
|
844
1502
|
const { envelope, scope } = result;
|
|
@@ -879,6 +1537,8 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
|
|
|
879
1537
|
projectPath: projectPathSchema
|
|
880
1538
|
},
|
|
881
1539
|
async (params) => {
|
|
1540
|
+
const toolBlock = enforceToolPolicy("detect_environment", params.projectPath);
|
|
1541
|
+
if (toolBlock) return toolBlock;
|
|
882
1542
|
const result = collapseEnvironment({
|
|
883
1543
|
projectPath: params.projectPath ?? process.cwd()
|
|
884
1544
|
});
|
|
@@ -899,9 +1559,13 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
|
|
|
899
1559
|
prefix: z.string().optional().describe("Prefix for api-key/token format"),
|
|
900
1560
|
saveAs: z.string().optional().describe("If provided, save the generated secret with this key name"),
|
|
901
1561
|
scope: scopeSchema.default("global"),
|
|
902
|
-
projectPath: projectPathSchema
|
|
1562
|
+
projectPath: projectPathSchema,
|
|
1563
|
+
teamId: teamIdSchema,
|
|
1564
|
+
orgId: orgIdSchema
|
|
903
1565
|
},
|
|
904
1566
|
async (params) => {
|
|
1567
|
+
const toolBlock = enforceToolPolicy("generate_secret", params.projectPath);
|
|
1568
|
+
if (toolBlock) return toolBlock;
|
|
905
1569
|
const secret = generateSecret({
|
|
906
1570
|
format: params.format,
|
|
907
1571
|
length: params.length,
|
|
@@ -932,6 +1596,8 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
|
|
|
932
1596
|
targetProjectPath: z.string().optional()
|
|
933
1597
|
},
|
|
934
1598
|
async (params) => {
|
|
1599
|
+
const toolBlock = enforceToolPolicy("entangle_secrets", params.sourceProjectPath);
|
|
1600
|
+
if (toolBlock) return toolBlock;
|
|
935
1601
|
entangleSecrets(
|
|
936
1602
|
params.sourceKey,
|
|
937
1603
|
{
|
|
@@ -961,6 +1627,8 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
|
|
|
961
1627
|
targetProjectPath: z.string().optional()
|
|
962
1628
|
},
|
|
963
1629
|
async (params) => {
|
|
1630
|
+
const toolBlock = enforceToolPolicy("disentangle_secrets", params.sourceProjectPath);
|
|
1631
|
+
if (toolBlock) return toolBlock;
|
|
964
1632
|
disentangleSecrets(
|
|
965
1633
|
params.sourceKey,
|
|
966
1634
|
{
|
|
@@ -987,6 +1655,8 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
|
|
|
987
1655
|
maxReads: z.number().optional().describe("Self-destruct after N reads")
|
|
988
1656
|
},
|
|
989
1657
|
async (params) => {
|
|
1658
|
+
const toolBlock = enforceToolPolicy("tunnel_create");
|
|
1659
|
+
if (toolBlock) return toolBlock;
|
|
990
1660
|
const id = tunnelCreate(params.value, {
|
|
991
1661
|
ttlSeconds: params.ttlSeconds,
|
|
992
1662
|
maxReads: params.maxReads
|
|
@@ -1001,6 +1671,8 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
|
|
|
1001
1671
|
id: z.string().describe("Tunnel ID")
|
|
1002
1672
|
},
|
|
1003
1673
|
async (params) => {
|
|
1674
|
+
const toolBlock = enforceToolPolicy("tunnel_read");
|
|
1675
|
+
if (toolBlock) return toolBlock;
|
|
1004
1676
|
const value = tunnelRead(params.id);
|
|
1005
1677
|
if (value === null) {
|
|
1006
1678
|
return text(`Tunnel "${params.id}" not found or expired`, true);
|
|
@@ -1013,6 +1685,8 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
|
|
|
1013
1685
|
"List active tunneled secrets (IDs and metadata only, never values).",
|
|
1014
1686
|
{},
|
|
1015
1687
|
async () => {
|
|
1688
|
+
const toolBlock = enforceToolPolicy("tunnel_list");
|
|
1689
|
+
if (toolBlock) return toolBlock;
|
|
1016
1690
|
const tunnels = tunnelList();
|
|
1017
1691
|
if (tunnels.length === 0) return text("No active tunnels");
|
|
1018
1692
|
const lines = tunnels.map((t) => {
|
|
@@ -1035,6 +1709,8 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
|
|
|
1035
1709
|
id: z.string().describe("Tunnel ID")
|
|
1036
1710
|
},
|
|
1037
1711
|
async (params) => {
|
|
1712
|
+
const toolBlock = enforceToolPolicy("tunnel_destroy");
|
|
1713
|
+
if (toolBlock) return toolBlock;
|
|
1038
1714
|
const destroyed = tunnelDestroy(params.id);
|
|
1039
1715
|
return text(
|
|
1040
1716
|
destroyed ? `Destroyed ${params.id}` : `Tunnel "${params.id}" not found`,
|
|
@@ -1049,9 +1725,13 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
|
|
|
1049
1725
|
keys: z.array(z.string()).optional().describe("Specific keys to pack (all if omitted)"),
|
|
1050
1726
|
passphrase: z.string().describe("Encryption passphrase"),
|
|
1051
1727
|
scope: scopeSchema,
|
|
1052
|
-
projectPath: projectPathSchema
|
|
1728
|
+
projectPath: projectPathSchema,
|
|
1729
|
+
teamId: teamIdSchema,
|
|
1730
|
+
orgId: orgIdSchema
|
|
1053
1731
|
},
|
|
1054
1732
|
async (params) => {
|
|
1733
|
+
const toolBlock = enforceToolPolicy("teleport_pack", params.projectPath);
|
|
1734
|
+
if (toolBlock) return toolBlock;
|
|
1055
1735
|
const o = opts(params);
|
|
1056
1736
|
const entries = listSecrets(o);
|
|
1057
1737
|
const secrets = [];
|
|
@@ -1075,9 +1755,13 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
|
|
|
1075
1755
|
passphrase: z.string().describe("Decryption passphrase"),
|
|
1076
1756
|
scope: scopeSchema.default("global"),
|
|
1077
1757
|
projectPath: projectPathSchema,
|
|
1758
|
+
teamId: teamIdSchema,
|
|
1759
|
+
orgId: orgIdSchema,
|
|
1078
1760
|
dryRun: z.boolean().optional().default(false).describe("Preview without importing")
|
|
1079
1761
|
},
|
|
1080
1762
|
async (params) => {
|
|
1763
|
+
const toolBlock = enforceToolPolicy("teleport_unpack", params.projectPath);
|
|
1764
|
+
if (toolBlock) return toolBlock;
|
|
1081
1765
|
try {
|
|
1082
1766
|
const payload = teleportUnpack(params.bundle, params.passphrase);
|
|
1083
1767
|
if (params.dryRun) {
|
|
@@ -1104,6 +1788,8 @@ ${preview}`);
|
|
|
1104
1788
|
limit: z.number().optional().default(20).describe("Max events to return")
|
|
1105
1789
|
},
|
|
1106
1790
|
async (params) => {
|
|
1791
|
+
const toolBlock = enforceToolPolicy("audit_log");
|
|
1792
|
+
if (toolBlock) return toolBlock;
|
|
1107
1793
|
const events = queryAudit({
|
|
1108
1794
|
key: params.key,
|
|
1109
1795
|
action: params.action,
|
|
@@ -1128,6 +1814,8 @@ ${preview}`);
|
|
|
1128
1814
|
key: z.string().optional().describe("Check anomalies for a specific key")
|
|
1129
1815
|
},
|
|
1130
1816
|
async (params) => {
|
|
1817
|
+
const toolBlock = enforceToolPolicy("detect_anomalies");
|
|
1818
|
+
if (toolBlock) return toolBlock;
|
|
1131
1819
|
const anomalies = detectAnomalies(params.key);
|
|
1132
1820
|
if (anomalies.length === 0) return text("No anomalies detected");
|
|
1133
1821
|
const lines = anomalies.map(
|
|
@@ -1141,9 +1829,13 @@ ${preview}`);
|
|
|
1141
1829
|
"Run a comprehensive health check on all secrets: decay status, staleness, anomalies, entropy assessment.",
|
|
1142
1830
|
{
|
|
1143
1831
|
scope: scopeSchema,
|
|
1144
|
-
projectPath: projectPathSchema
|
|
1832
|
+
projectPath: projectPathSchema,
|
|
1833
|
+
teamId: teamIdSchema,
|
|
1834
|
+
orgId: orgIdSchema
|
|
1145
1835
|
},
|
|
1146
1836
|
async (params) => {
|
|
1837
|
+
const toolBlock = enforceToolPolicy("health_check", params.projectPath);
|
|
1838
|
+
if (toolBlock) return toolBlock;
|
|
1147
1839
|
const entries = listSecrets(opts(params));
|
|
1148
1840
|
const anomalies = detectAnomalies();
|
|
1149
1841
|
let healthy = 0;
|
|
@@ -1193,9 +1885,13 @@ ${preview}`);
|
|
|
1193
1885
|
key: z.string().describe("The secret key name"),
|
|
1194
1886
|
provider: z.string().optional().describe("Force a specific provider (openai, stripe, github, aws, http)"),
|
|
1195
1887
|
scope: scopeSchema,
|
|
1196
|
-
projectPath: projectPathSchema
|
|
1888
|
+
projectPath: projectPathSchema,
|
|
1889
|
+
teamId: teamIdSchema,
|
|
1890
|
+
orgId: orgIdSchema
|
|
1197
1891
|
},
|
|
1198
1892
|
async (params) => {
|
|
1893
|
+
const toolBlock = enforceToolPolicy("validate_secret", params.projectPath);
|
|
1894
|
+
if (toolBlock) return toolBlock;
|
|
1199
1895
|
const value = getSecret(params.key, opts(params));
|
|
1200
1896
|
if (value === null) return text(`Secret "${params.key}" not found`, true);
|
|
1201
1897
|
const envelope = getEnvelope(params.key, opts(params));
|
|
@@ -1209,7 +1905,9 @@ ${preview}`);
|
|
|
1209
1905
|
"List all available validation providers for secret liveness testing.",
|
|
1210
1906
|
{},
|
|
1211
1907
|
async () => {
|
|
1212
|
-
const
|
|
1908
|
+
const toolBlock = enforceToolPolicy("list_providers");
|
|
1909
|
+
if (toolBlock) return toolBlock;
|
|
1910
|
+
const providers = registry2.listProviders().map((p) => ({
|
|
1213
1911
|
name: p.name,
|
|
1214
1912
|
description: p.description,
|
|
1215
1913
|
prefixes: p.prefixes ?? []
|
|
@@ -1234,6 +1932,8 @@ ${preview}`);
|
|
|
1234
1932
|
description: z.string().optional().describe("Human-readable description")
|
|
1235
1933
|
},
|
|
1236
1934
|
async (params) => {
|
|
1935
|
+
const toolBlock = enforceToolPolicy("register_hook");
|
|
1936
|
+
if (toolBlock) return toolBlock;
|
|
1237
1937
|
if (!params.key && !params.keyPattern && !params.tag) {
|
|
1238
1938
|
return text("At least one match criterion required: key, keyPattern, or tag", true);
|
|
1239
1939
|
}
|
|
@@ -1260,6 +1960,8 @@ ${preview}`);
|
|
|
1260
1960
|
"List all registered secret change hooks with their match criteria, type, and status.",
|
|
1261
1961
|
{},
|
|
1262
1962
|
async () => {
|
|
1963
|
+
const toolBlock = enforceToolPolicy("list_hooks");
|
|
1964
|
+
if (toolBlock) return toolBlock;
|
|
1263
1965
|
const hooks = listHooks();
|
|
1264
1966
|
if (hooks.length === 0) return text("No hooks registered");
|
|
1265
1967
|
return text(JSON.stringify(hooks, null, 2));
|
|
@@ -1272,6 +1974,8 @@ ${preview}`);
|
|
|
1272
1974
|
id: z.string().describe("Hook ID to remove")
|
|
1273
1975
|
},
|
|
1274
1976
|
async (params) => {
|
|
1977
|
+
const toolBlock = enforceToolPolicy("remove_hook");
|
|
1978
|
+
if (toolBlock) return toolBlock;
|
|
1275
1979
|
const removed = removeHook(params.id);
|
|
1276
1980
|
return text(
|
|
1277
1981
|
removed ? `Removed hook ${params.id}` : `Hook "${params.id}" not found`,
|
|
@@ -1279,6 +1983,194 @@ ${preview}`);
|
|
|
1279
1983
|
);
|
|
1280
1984
|
}
|
|
1281
1985
|
);
|
|
1986
|
+
server2.tool(
|
|
1987
|
+
"exec_with_secrets",
|
|
1988
|
+
"Run a shell command securely. Project secrets are injected into the environment, and any secret values in the output are automatically redacted to prevent leaking into transcripts.",
|
|
1989
|
+
{
|
|
1990
|
+
command: z.string().describe("Command to run"),
|
|
1991
|
+
args: z.array(z.string()).optional().describe("Command arguments"),
|
|
1992
|
+
keys: z.array(z.string()).optional().describe("Only inject these specific keys"),
|
|
1993
|
+
tags: z.array(z.string()).optional().describe("Only inject secrets with these tags"),
|
|
1994
|
+
profile: z.enum(["unrestricted", "restricted", "ci"]).optional().default("restricted").describe("Exec profile: unrestricted, restricted, or ci"),
|
|
1995
|
+
scope: scopeSchema,
|
|
1996
|
+
projectPath: projectPathSchema,
|
|
1997
|
+
teamId: teamIdSchema,
|
|
1998
|
+
orgId: orgIdSchema
|
|
1999
|
+
},
|
|
2000
|
+
async (params) => {
|
|
2001
|
+
const toolBlock = enforceToolPolicy("exec_with_secrets", params.projectPath);
|
|
2002
|
+
if (toolBlock) return toolBlock;
|
|
2003
|
+
const execBlock = checkExecPolicy(params.command, params.projectPath);
|
|
2004
|
+
if (!execBlock.allowed) {
|
|
2005
|
+
return text(`Policy Denied: ${execBlock.reason}`, true);
|
|
2006
|
+
}
|
|
2007
|
+
try {
|
|
2008
|
+
const result = await execCommand({
|
|
2009
|
+
command: params.command,
|
|
2010
|
+
args: params.args ?? [],
|
|
2011
|
+
keys: params.keys,
|
|
2012
|
+
tags: params.tags,
|
|
2013
|
+
profile: params.profile,
|
|
2014
|
+
scope: params.scope,
|
|
2015
|
+
projectPath: params.projectPath,
|
|
2016
|
+
source: "mcp",
|
|
2017
|
+
captureOutput: true
|
|
2018
|
+
});
|
|
2019
|
+
const output = [];
|
|
2020
|
+
output.push(`Exit code: ${result.code}`);
|
|
2021
|
+
if (result.stdout) output.push(`STDOUT:
|
|
2022
|
+
${result.stdout}`);
|
|
2023
|
+
if (result.stderr) output.push(`STDERR:
|
|
2024
|
+
${result.stderr}`);
|
|
2025
|
+
return text(output.join("\n\n"));
|
|
2026
|
+
} catch (err) {
|
|
2027
|
+
return text(`Execution failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
);
|
|
2031
|
+
server2.tool(
|
|
2032
|
+
"scan_codebase_for_secrets",
|
|
2033
|
+
"Scan a directory for hardcoded secrets using regex heuristics and Shannon entropy analysis. Returns file paths, line numbers, and the matched key/value to help migrate legacy codebases into q-ring.",
|
|
2034
|
+
{
|
|
2035
|
+
dirPath: z.string().describe("Absolute or relative path to the directory to scan")
|
|
2036
|
+
},
|
|
2037
|
+
async (params) => {
|
|
2038
|
+
const toolBlock = enforceToolPolicy("scan_codebase_for_secrets");
|
|
2039
|
+
if (toolBlock) return toolBlock;
|
|
2040
|
+
try {
|
|
2041
|
+
const results = scanCodebase(params.dirPath);
|
|
2042
|
+
if (results.length === 0) {
|
|
2043
|
+
return text("No hardcoded secrets found in the specified directory.");
|
|
2044
|
+
}
|
|
2045
|
+
return text(JSON.stringify(results, null, 2));
|
|
2046
|
+
} catch (err) {
|
|
2047
|
+
return text(`Scan failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
);
|
|
2051
|
+
server2.tool(
|
|
2052
|
+
"get_project_context",
|
|
2053
|
+
"Get a safe, redacted overview of the project's secrets, environment, manifest, providers, hooks, and recent audit activity. No secret values are ever exposed. Use this to understand what secrets exist before asking to read them.",
|
|
2054
|
+
{
|
|
2055
|
+
scope: scopeSchema,
|
|
2056
|
+
projectPath: projectPathSchema,
|
|
2057
|
+
teamId: teamIdSchema,
|
|
2058
|
+
orgId: orgIdSchema
|
|
2059
|
+
},
|
|
2060
|
+
async (params) => {
|
|
2061
|
+
const toolBlock = enforceToolPolicy("get_project_context", params.projectPath);
|
|
2062
|
+
if (toolBlock) return toolBlock;
|
|
2063
|
+
const context = getProjectContext(opts(params));
|
|
2064
|
+
return text(JSON.stringify(context, null, 2));
|
|
2065
|
+
}
|
|
2066
|
+
);
|
|
2067
|
+
server2.tool(
|
|
2068
|
+
"agent_remember",
|
|
2069
|
+
"Store a key-value pair in encrypted agent memory that persists across sessions. Use this to remember decisions, rotation history, or project-specific context.",
|
|
2070
|
+
{
|
|
2071
|
+
key: z.string().describe("Memory key"),
|
|
2072
|
+
value: z.string().describe("Value to store")
|
|
2073
|
+
},
|
|
2074
|
+
async (params) => {
|
|
2075
|
+
const toolBlock = enforceToolPolicy("agent_remember");
|
|
2076
|
+
if (toolBlock) return toolBlock;
|
|
2077
|
+
remember(params.key, params.value);
|
|
2078
|
+
return text(`Remembered "${params.key}"`);
|
|
2079
|
+
}
|
|
2080
|
+
);
|
|
2081
|
+
server2.tool(
|
|
2082
|
+
"agent_recall",
|
|
2083
|
+
"Retrieve a value from agent memory, or list all stored keys if no key is provided.",
|
|
2084
|
+
{
|
|
2085
|
+
key: z.string().optional().describe("Memory key to recall (omit to list all)")
|
|
2086
|
+
},
|
|
2087
|
+
async (params) => {
|
|
2088
|
+
const toolBlock = enforceToolPolicy("agent_recall");
|
|
2089
|
+
if (toolBlock) return toolBlock;
|
|
2090
|
+
if (!params.key) {
|
|
2091
|
+
const entries = listMemory();
|
|
2092
|
+
if (entries.length === 0) return text("Agent memory is empty");
|
|
2093
|
+
return text(JSON.stringify(entries, null, 2));
|
|
2094
|
+
}
|
|
2095
|
+
const value = recall(params.key);
|
|
2096
|
+
if (value === null) return text(`No memory found for "${params.key}"`, true);
|
|
2097
|
+
return text(value);
|
|
2098
|
+
}
|
|
2099
|
+
);
|
|
2100
|
+
server2.tool(
|
|
2101
|
+
"agent_forget",
|
|
2102
|
+
"Delete a key from agent memory.",
|
|
2103
|
+
{
|
|
2104
|
+
key: z.string().describe("Memory key to forget")
|
|
2105
|
+
},
|
|
2106
|
+
async (params) => {
|
|
2107
|
+
const toolBlock = enforceToolPolicy("agent_forget");
|
|
2108
|
+
if (toolBlock) return toolBlock;
|
|
2109
|
+
const removed = forget(params.key);
|
|
2110
|
+
return text(removed ? `Forgot "${params.key}"` : `No memory found for "${params.key}"`, !removed);
|
|
2111
|
+
}
|
|
2112
|
+
);
|
|
2113
|
+
server2.tool(
|
|
2114
|
+
"lint_files",
|
|
2115
|
+
"Scan specific files for hardcoded secrets. Optionally auto-fix by replacing them with process.env references and storing the values in q-ring.",
|
|
2116
|
+
{
|
|
2117
|
+
files: z.array(z.string()).describe("File paths to lint"),
|
|
2118
|
+
fix: z.boolean().optional().default(false).describe("Auto-replace and store secrets"),
|
|
2119
|
+
scope: scopeSchema,
|
|
2120
|
+
projectPath: projectPathSchema,
|
|
2121
|
+
teamId: teamIdSchema,
|
|
2122
|
+
orgId: orgIdSchema
|
|
2123
|
+
},
|
|
2124
|
+
async (params) => {
|
|
2125
|
+
const toolBlock = enforceToolPolicy("lint_files", params.projectPath);
|
|
2126
|
+
if (toolBlock) return toolBlock;
|
|
2127
|
+
try {
|
|
2128
|
+
const results = lintFiles(params.files, {
|
|
2129
|
+
fix: params.fix,
|
|
2130
|
+
scope: params.scope,
|
|
2131
|
+
projectPath: params.projectPath
|
|
2132
|
+
});
|
|
2133
|
+
if (results.length === 0) {
|
|
2134
|
+
return text("No hardcoded secrets found in the specified files.");
|
|
2135
|
+
}
|
|
2136
|
+
return text(JSON.stringify(results, null, 2));
|
|
2137
|
+
} catch (err) {
|
|
2138
|
+
return text(`Lint failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
);
|
|
2142
|
+
server2.tool(
|
|
2143
|
+
"analyze_secrets",
|
|
2144
|
+
"Analyze secret usage patterns and provide optimization suggestions including most accessed, stale, unused, and rotation recommendations.",
|
|
2145
|
+
{
|
|
2146
|
+
scope: scopeSchema,
|
|
2147
|
+
projectPath: projectPathSchema,
|
|
2148
|
+
teamId: teamIdSchema,
|
|
2149
|
+
orgId: orgIdSchema
|
|
2150
|
+
},
|
|
2151
|
+
async (params) => {
|
|
2152
|
+
const toolBlock = enforceToolPolicy("analyze_secrets", params.projectPath);
|
|
2153
|
+
if (toolBlock) return toolBlock;
|
|
2154
|
+
const o = opts(params);
|
|
2155
|
+
const entries = listSecrets({ ...o, silent: true });
|
|
2156
|
+
const audit = queryAudit({ limit: 500 });
|
|
2157
|
+
const accessMap = /* @__PURE__ */ new Map();
|
|
2158
|
+
for (const e of audit) {
|
|
2159
|
+
if (e.action === "read" && e.key) {
|
|
2160
|
+
accessMap.set(e.key, (accessMap.get(e.key) || 0) + 1);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
const analysis = {
|
|
2164
|
+
total: entries.length,
|
|
2165
|
+
expired: entries.filter((e) => e.decay?.isExpired).length,
|
|
2166
|
+
stale: entries.filter((e) => e.decay?.isStale && !e.decay?.isExpired).length,
|
|
2167
|
+
neverAccessed: entries.filter((e) => (e.envelope?.meta.accessCount ?? 0) === 0).map((e) => e.key),
|
|
2168
|
+
noRotationFormat: entries.filter((e) => !e.envelope?.meta.rotationFormat).map((e) => e.key),
|
|
2169
|
+
mostAccessed: [...accessMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([key, count]) => ({ key, reads: count }))
|
|
2170
|
+
};
|
|
2171
|
+
return text(JSON.stringify(analysis, null, 2));
|
|
2172
|
+
}
|
|
2173
|
+
);
|
|
1282
2174
|
let dashboardInstance = null;
|
|
1283
2175
|
server2.tool(
|
|
1284
2176
|
"status_dashboard",
|
|
@@ -1287,10 +2179,12 @@ ${preview}`);
|
|
|
1287
2179
|
port: z.number().optional().default(9876).describe("Port to serve on")
|
|
1288
2180
|
},
|
|
1289
2181
|
async (params) => {
|
|
2182
|
+
const toolBlock = enforceToolPolicy("status_dashboard");
|
|
2183
|
+
if (toolBlock) return toolBlock;
|
|
1290
2184
|
if (dashboardInstance) {
|
|
1291
2185
|
return text(`Dashboard already running at http://127.0.0.1:${dashboardInstance.port}`);
|
|
1292
2186
|
}
|
|
1293
|
-
const { startDashboardServer } = await import("./dashboard-
|
|
2187
|
+
const { startDashboardServer } = await import("./dashboard-Q5OQRQCX.js");
|
|
1294
2188
|
dashboardInstance = startDashboardServer({ port: params.port });
|
|
1295
2189
|
return text(`Dashboard started at http://127.0.0.1:${dashboardInstance.port}
|
|
1296
2190
|
Open this URL in a browser to see live quantum status.`);
|
|
@@ -1304,6 +2198,8 @@ Open this URL in a browser to see live quantum status.`);
|
|
|
1304
2198
|
projectPaths: z.array(z.string()).optional().describe("Project paths to monitor")
|
|
1305
2199
|
},
|
|
1306
2200
|
async (params) => {
|
|
2201
|
+
const toolBlock = enforceToolPolicy("agent_scan");
|
|
2202
|
+
if (toolBlock) return toolBlock;
|
|
1307
2203
|
const report = runHealthScan({
|
|
1308
2204
|
autoRotate: params.autoRotate,
|
|
1309
2205
|
projectPaths: params.projectPaths ?? [process.cwd()]
|
|
@@ -1311,6 +2207,128 @@ Open this URL in a browser to see live quantum status.`);
|
|
|
1311
2207
|
return text(JSON.stringify(report, null, 2));
|
|
1312
2208
|
}
|
|
1313
2209
|
);
|
|
2210
|
+
server2.tool(
|
|
2211
|
+
"verify_audit_chain",
|
|
2212
|
+
"Verify the tamper-evident hash chain of the audit log. Returns integrity status and the first break point if tampered.",
|
|
2213
|
+
{},
|
|
2214
|
+
async () => {
|
|
2215
|
+
const toolBlock = enforceToolPolicy("verify_audit_chain");
|
|
2216
|
+
if (toolBlock) return toolBlock;
|
|
2217
|
+
const result = verifyAuditChain();
|
|
2218
|
+
return text(JSON.stringify(result, null, 2));
|
|
2219
|
+
}
|
|
2220
|
+
);
|
|
2221
|
+
server2.tool(
|
|
2222
|
+
"export_audit",
|
|
2223
|
+
"Export audit events in a portable format (jsonl, json, or csv) with optional time range filtering.",
|
|
2224
|
+
{
|
|
2225
|
+
since: z.string().optional().describe("Start date (ISO 8601)"),
|
|
2226
|
+
until: z.string().optional().describe("End date (ISO 8601)"),
|
|
2227
|
+
format: z.enum(["jsonl", "json", "csv"]).optional().default("jsonl").describe("Output format")
|
|
2228
|
+
},
|
|
2229
|
+
async (params) => {
|
|
2230
|
+
const toolBlock = enforceToolPolicy("export_audit");
|
|
2231
|
+
if (toolBlock) return toolBlock;
|
|
2232
|
+
const output = exportAudit({
|
|
2233
|
+
since: params.since,
|
|
2234
|
+
until: params.until,
|
|
2235
|
+
format: params.format
|
|
2236
|
+
});
|
|
2237
|
+
return text(output);
|
|
2238
|
+
}
|
|
2239
|
+
);
|
|
2240
|
+
server2.tool(
|
|
2241
|
+
"rotate_secret",
|
|
2242
|
+
"Attempt issuer-native rotation of a secret via its detected or specified provider. Returns rotation result.",
|
|
2243
|
+
{
|
|
2244
|
+
key: z.string().describe("The secret key to rotate"),
|
|
2245
|
+
provider: z.string().optional().describe("Force a specific provider"),
|
|
2246
|
+
scope: scopeSchema,
|
|
2247
|
+
projectPath: projectPathSchema,
|
|
2248
|
+
teamId: teamIdSchema,
|
|
2249
|
+
orgId: orgIdSchema
|
|
2250
|
+
},
|
|
2251
|
+
async (params) => {
|
|
2252
|
+
const toolBlock = enforceToolPolicy("rotate_secret", params.projectPath);
|
|
2253
|
+
if (toolBlock) return toolBlock;
|
|
2254
|
+
const value = getSecret(params.key, opts(params));
|
|
2255
|
+
if (!value) return text(`Secret "${params.key}" not found`, true);
|
|
2256
|
+
const result = await rotateWithProvider(value, params.provider);
|
|
2257
|
+
if (result.rotated && result.newValue) {
|
|
2258
|
+
setSecret(params.key, result.newValue, {
|
|
2259
|
+
scope: params.scope ?? "global",
|
|
2260
|
+
projectPath: params.projectPath,
|
|
2261
|
+
source: "mcp"
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2264
|
+
return text(JSON.stringify(result, null, 2));
|
|
2265
|
+
}
|
|
2266
|
+
);
|
|
2267
|
+
server2.tool(
|
|
2268
|
+
"ci_validate_secrets",
|
|
2269
|
+
"CI-oriented batch validation: validates all accessible secrets against their providers and returns a structured pass/fail report.",
|
|
2270
|
+
{
|
|
2271
|
+
scope: scopeSchema,
|
|
2272
|
+
projectPath: projectPathSchema,
|
|
2273
|
+
teamId: teamIdSchema,
|
|
2274
|
+
orgId: orgIdSchema
|
|
2275
|
+
},
|
|
2276
|
+
async (params) => {
|
|
2277
|
+
const toolBlock = enforceToolPolicy("ci_validate_secrets", params.projectPath);
|
|
2278
|
+
if (toolBlock) return toolBlock;
|
|
2279
|
+
const entries = listSecrets(opts(params));
|
|
2280
|
+
const secrets = entries.map((e) => {
|
|
2281
|
+
const val = getSecret(e.key, { ...opts(params), scope: e.scope, silent: true });
|
|
2282
|
+
if (!val) return null;
|
|
2283
|
+
return {
|
|
2284
|
+
key: e.key,
|
|
2285
|
+
value: val,
|
|
2286
|
+
provider: e.envelope?.meta.provider,
|
|
2287
|
+
validationUrl: e.envelope?.meta.validationUrl
|
|
2288
|
+
};
|
|
2289
|
+
}).filter((s) => s !== null);
|
|
2290
|
+
if (secrets.length === 0) return text("No secrets to validate");
|
|
2291
|
+
const report = await ciValidateBatch(secrets);
|
|
2292
|
+
return text(JSON.stringify(report, null, 2));
|
|
2293
|
+
}
|
|
2294
|
+
);
|
|
2295
|
+
server2.tool(
|
|
2296
|
+
"check_policy",
|
|
2297
|
+
"Check if an action is allowed by the project's governance policy. Returns the policy decision and source.",
|
|
2298
|
+
{
|
|
2299
|
+
action: z.enum(["tool", "key_read", "exec"]).describe("Type of policy check"),
|
|
2300
|
+
toolName: z.string().optional().describe("Tool name to check (for action=tool)"),
|
|
2301
|
+
key: z.string().optional().describe("Secret key to check (for action=key_read)"),
|
|
2302
|
+
command: z.string().optional().describe("Command to check (for action=exec)"),
|
|
2303
|
+
projectPath: projectPathSchema
|
|
2304
|
+
},
|
|
2305
|
+
async (params) => {
|
|
2306
|
+
if (params.action === "tool" && params.toolName) {
|
|
2307
|
+
const d = checkToolPolicy(params.toolName, params.projectPath);
|
|
2308
|
+
return text(JSON.stringify(d, null, 2));
|
|
2309
|
+
}
|
|
2310
|
+
if (params.action === "key_read" && params.key) {
|
|
2311
|
+
const d = checkKeyReadPolicy(params.key, void 0, params.projectPath);
|
|
2312
|
+
return text(JSON.stringify(d, null, 2));
|
|
2313
|
+
}
|
|
2314
|
+
if (params.action === "exec" && params.command) {
|
|
2315
|
+
const d = checkExecPolicy(params.command, params.projectPath);
|
|
2316
|
+
return text(JSON.stringify(d, null, 2));
|
|
2317
|
+
}
|
|
2318
|
+
return text("Missing required parameter for the selected action type", true);
|
|
2319
|
+
}
|
|
2320
|
+
);
|
|
2321
|
+
server2.tool(
|
|
2322
|
+
"get_policy_summary",
|
|
2323
|
+
"Get a summary of the project's governance policy configuration.",
|
|
2324
|
+
{
|
|
2325
|
+
projectPath: projectPathSchema
|
|
2326
|
+
},
|
|
2327
|
+
async (params) => {
|
|
2328
|
+
const summary = getPolicySummary(params.projectPath);
|
|
2329
|
+
return text(JSON.stringify(summary, null, 2));
|
|
2330
|
+
}
|
|
2331
|
+
);
|
|
1314
2332
|
return server2;
|
|
1315
2333
|
}
|
|
1316
2334
|
|