@i4ctime/q-ring 0.3.2 → 0.9.1
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 +519 -10
- package/dist/chunk-5JBU7TWN.js +1576 -0
- 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-X3ONQFLV.js → dashboard-JT5ZNLT5.js} +41 -16
- package/dist/dashboard-JT5ZNLT5.js.map +1 -0
- package/dist/{dashboard-QQWKOOI5.js → dashboard-Q5OQRQCX.js} +41 -16
- package/dist/dashboard-Q5OQRQCX.js.map +1 -0
- package/dist/index.js +1878 -39
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +1610 -21
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-3WTTWJYU.js +0 -653
- package/dist/chunk-3WTTWJYU.js.map +0 -1
- package/dist/chunk-F4SPZ774.js +0 -675
- package/dist/chunk-F4SPZ774.js.map +0 -1
- package/dist/dashboard-QQWKOOI5.js.map +0 -1
- package/dist/dashboard-X3ONQFLV.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,22 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
checkDecay,
|
|
4
|
+
checkExecPolicy,
|
|
4
5
|
collapseEnvironment,
|
|
5
6
|
deleteSecret,
|
|
6
7
|
detectAnomalies,
|
|
8
|
+
disableHook,
|
|
9
|
+
disentangleSecrets,
|
|
10
|
+
enableHook,
|
|
7
11
|
entangleSecrets,
|
|
12
|
+
exportAudit,
|
|
8
13
|
exportSecrets,
|
|
14
|
+
fireHooks,
|
|
9
15
|
getEnvelope,
|
|
16
|
+
getExecMaxRuntime,
|
|
17
|
+
getPolicySummary,
|
|
10
18
|
getSecret,
|
|
19
|
+
grantApproval,
|
|
20
|
+
hasSecret,
|
|
21
|
+
httpRequest_,
|
|
22
|
+
listApprovals,
|
|
23
|
+
listHooks,
|
|
11
24
|
listSecrets,
|
|
12
25
|
logAudit,
|
|
13
26
|
queryAudit,
|
|
27
|
+
readProjectConfig,
|
|
28
|
+
registerHook,
|
|
29
|
+
registry,
|
|
30
|
+
removeHook,
|
|
31
|
+
revokeApproval,
|
|
14
32
|
setSecret,
|
|
15
33
|
tunnelCreate,
|
|
16
34
|
tunnelDestroy,
|
|
17
35
|
tunnelList,
|
|
18
|
-
tunnelRead
|
|
19
|
-
|
|
36
|
+
tunnelRead,
|
|
37
|
+
verifyAuditChain
|
|
38
|
+
} from "./chunk-WG4ZKN7Q.js";
|
|
20
39
|
|
|
21
40
|
// src/cli/commands.ts
|
|
22
41
|
import { Command } from "commander";
|
|
@@ -207,7 +226,9 @@ function runHealthScan(config = {}) {
|
|
|
207
226
|
`EXPIRED: ${entry.key} [${entry.scope}] \u2014 expired ${decay.timeRemaining}`
|
|
208
227
|
);
|
|
209
228
|
if (cfg.autoRotate) {
|
|
210
|
-
const
|
|
229
|
+
const fmt = entry.envelope?.meta.rotationFormat ?? "api-key";
|
|
230
|
+
const prefix = entry.envelope?.meta.rotationPrefix;
|
|
231
|
+
const newValue = generateSecret({ format: fmt, prefix });
|
|
211
232
|
setSecret(entry.key, newValue, {
|
|
212
233
|
scope: entry.scope,
|
|
213
234
|
projectPath: cfg.projectPaths[0],
|
|
@@ -221,6 +242,14 @@ function runHealthScan(config = {}) {
|
|
|
221
242
|
source: "agent",
|
|
222
243
|
detail: "auto-rotated by agent (expired)"
|
|
223
244
|
});
|
|
245
|
+
fireHooks({
|
|
246
|
+
action: "rotate",
|
|
247
|
+
key: entry.key,
|
|
248
|
+
scope: entry.scope,
|
|
249
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
250
|
+
source: "agent"
|
|
251
|
+
}, entry.envelope?.meta.tags).catch(() => {
|
|
252
|
+
});
|
|
224
253
|
}
|
|
225
254
|
} else if (decay.isStale) {
|
|
226
255
|
report.stale++;
|
|
@@ -353,6 +382,937 @@ function teleportUnpack(encoded, passphrase) {
|
|
|
353
382
|
return JSON.parse(decrypted.toString("utf8"));
|
|
354
383
|
}
|
|
355
384
|
|
|
385
|
+
// src/core/import.ts
|
|
386
|
+
import { readFileSync } from "fs";
|
|
387
|
+
function parseDotenv(content) {
|
|
388
|
+
const result = /* @__PURE__ */ new Map();
|
|
389
|
+
const lines = content.split(/\r?\n/);
|
|
390
|
+
for (let i = 0; i < lines.length; i++) {
|
|
391
|
+
const line = lines[i].trim();
|
|
392
|
+
if (!line || line.startsWith("#")) continue;
|
|
393
|
+
const eqIdx = line.indexOf("=");
|
|
394
|
+
if (eqIdx === -1) continue;
|
|
395
|
+
const key = line.slice(0, eqIdx).trim();
|
|
396
|
+
let value = line.slice(eqIdx + 1).trim();
|
|
397
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
398
|
+
value = value.slice(1, -1);
|
|
399
|
+
}
|
|
400
|
+
value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\\\/g, "\\").replace(/\\"/g, '"');
|
|
401
|
+
if (value.includes("#") && !line.includes('"') && !line.includes("'")) {
|
|
402
|
+
value = value.split("#")[0].trim();
|
|
403
|
+
}
|
|
404
|
+
if (key) result.set(key, value);
|
|
405
|
+
}
|
|
406
|
+
return result;
|
|
407
|
+
}
|
|
408
|
+
function importDotenv(filePathOrContent, options = {}) {
|
|
409
|
+
let content;
|
|
410
|
+
try {
|
|
411
|
+
content = readFileSync(filePathOrContent, "utf8");
|
|
412
|
+
} catch {
|
|
413
|
+
content = filePathOrContent;
|
|
414
|
+
}
|
|
415
|
+
const pairs = parseDotenv(content);
|
|
416
|
+
const result = {
|
|
417
|
+
imported: [],
|
|
418
|
+
skipped: [],
|
|
419
|
+
total: pairs.size
|
|
420
|
+
};
|
|
421
|
+
for (const [key, value] of pairs) {
|
|
422
|
+
if (options.skipExisting && hasSecret(key, {
|
|
423
|
+
scope: options.scope,
|
|
424
|
+
projectPath: options.projectPath,
|
|
425
|
+
source: options.source ?? "cli"
|
|
426
|
+
})) {
|
|
427
|
+
result.skipped.push(key);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (options.dryRun) {
|
|
431
|
+
result.imported.push(key);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const setOpts = {
|
|
435
|
+
scope: options.scope ?? "global",
|
|
436
|
+
projectPath: options.projectPath ?? process.cwd(),
|
|
437
|
+
source: options.source ?? "cli"
|
|
438
|
+
};
|
|
439
|
+
setSecret(key, value, setOpts);
|
|
440
|
+
result.imported.push(key);
|
|
441
|
+
}
|
|
442
|
+
return result;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/core/exec.ts
|
|
446
|
+
import { spawn } from "child_process";
|
|
447
|
+
import { Transform } from "stream";
|
|
448
|
+
var BUILTIN_PROFILES = {
|
|
449
|
+
unrestricted: { name: "unrestricted" },
|
|
450
|
+
restricted: {
|
|
451
|
+
name: "restricted",
|
|
452
|
+
denyCommands: ["curl", "wget", "ssh", "scp", "nc", "netcat", "ncat"],
|
|
453
|
+
maxRuntimeSeconds: 30,
|
|
454
|
+
allowNetwork: false,
|
|
455
|
+
stripEnvVars: ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"]
|
|
456
|
+
},
|
|
457
|
+
ci: {
|
|
458
|
+
name: "ci",
|
|
459
|
+
maxRuntimeSeconds: 300,
|
|
460
|
+
allowNetwork: true,
|
|
461
|
+
denyCommands: ["rm -rf /", "mkfs", "dd if="]
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
function getProfile(name) {
|
|
465
|
+
if (!name) return BUILTIN_PROFILES.unrestricted;
|
|
466
|
+
return BUILTIN_PROFILES[name] ?? { name };
|
|
467
|
+
}
|
|
468
|
+
var RedactionTransform = class extends Transform {
|
|
469
|
+
patterns = [];
|
|
470
|
+
tail = "";
|
|
471
|
+
maxLen = 0;
|
|
472
|
+
constructor(secretsToRedact) {
|
|
473
|
+
super();
|
|
474
|
+
const validSecrets = secretsToRedact.filter((s) => s.length > 5);
|
|
475
|
+
validSecrets.sort((a, b) => b.length - a.length);
|
|
476
|
+
this.patterns = validSecrets.map((s) => ({
|
|
477
|
+
value: s,
|
|
478
|
+
replacement: "[QRING:REDACTED]"
|
|
479
|
+
}));
|
|
480
|
+
if (validSecrets.length > 0) {
|
|
481
|
+
this.maxLen = validSecrets[0].length;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
_transform(chunk, encoding, callback) {
|
|
485
|
+
if (this.patterns.length === 0) {
|
|
486
|
+
this.push(chunk);
|
|
487
|
+
return callback();
|
|
488
|
+
}
|
|
489
|
+
const text = this.tail + chunk.toString();
|
|
490
|
+
let redacted = text;
|
|
491
|
+
for (const { value, replacement } of this.patterns) {
|
|
492
|
+
redacted = redacted.split(value).join(replacement);
|
|
493
|
+
}
|
|
494
|
+
if (redacted.length < this.maxLen) {
|
|
495
|
+
this.tail = redacted;
|
|
496
|
+
return callback();
|
|
497
|
+
}
|
|
498
|
+
const outputLen = redacted.length - this.maxLen + 1;
|
|
499
|
+
const output = redacted.slice(0, outputLen);
|
|
500
|
+
this.tail = redacted.slice(outputLen);
|
|
501
|
+
this.push(output);
|
|
502
|
+
callback();
|
|
503
|
+
}
|
|
504
|
+
_flush(callback) {
|
|
505
|
+
if (this.tail) {
|
|
506
|
+
let final = this.tail;
|
|
507
|
+
for (const { value, replacement } of this.patterns) {
|
|
508
|
+
final = final.split(value).join(replacement);
|
|
509
|
+
}
|
|
510
|
+
this.push(final);
|
|
511
|
+
}
|
|
512
|
+
callback();
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
async function execCommand(opts) {
|
|
516
|
+
const profile = getProfile(opts.profile);
|
|
517
|
+
const fullCommand = [opts.command, ...opts.args].join(" ");
|
|
518
|
+
const policyDecision = checkExecPolicy(fullCommand, opts.projectPath);
|
|
519
|
+
if (!policyDecision.allowed) {
|
|
520
|
+
throw new Error(`Policy Denied: ${policyDecision.reason}`);
|
|
521
|
+
}
|
|
522
|
+
if (profile.denyCommands) {
|
|
523
|
+
const denied = profile.denyCommands.find((d) => fullCommand.includes(d));
|
|
524
|
+
if (denied) {
|
|
525
|
+
throw new Error(`Exec profile "${profile.name}" denies command containing "${denied}"`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (profile.allowCommands) {
|
|
529
|
+
const allowed = profile.allowCommands.some((a) => fullCommand.startsWith(a));
|
|
530
|
+
if (!allowed) {
|
|
531
|
+
throw new Error(`Exec profile "${profile.name}" does not allow command "${opts.command}"`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const envMap = {};
|
|
535
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
536
|
+
if (v !== void 0) envMap[k] = v;
|
|
537
|
+
}
|
|
538
|
+
if (profile.stripEnvVars) {
|
|
539
|
+
for (const key of profile.stripEnvVars) {
|
|
540
|
+
delete envMap[key];
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const secretsToRedact = /* @__PURE__ */ new Set();
|
|
544
|
+
let entries = listSecrets({
|
|
545
|
+
scope: opts.scope,
|
|
546
|
+
projectPath: opts.projectPath,
|
|
547
|
+
source: opts.source ?? "cli",
|
|
548
|
+
silent: true
|
|
549
|
+
// list silently
|
|
550
|
+
});
|
|
551
|
+
if (opts.keys?.length) {
|
|
552
|
+
const keySet = new Set(opts.keys);
|
|
553
|
+
entries = entries.filter((e) => keySet.has(e.key));
|
|
554
|
+
}
|
|
555
|
+
if (opts.tags?.length) {
|
|
556
|
+
entries = entries.filter(
|
|
557
|
+
(e) => opts.tags.some((t) => e.envelope?.meta.tags?.includes(t))
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
for (const entry of entries) {
|
|
561
|
+
if (entry.envelope) {
|
|
562
|
+
const decay = checkDecay(entry.envelope);
|
|
563
|
+
if (decay.isExpired) continue;
|
|
564
|
+
}
|
|
565
|
+
const val = getSecret(entry.key, {
|
|
566
|
+
scope: entry.scope,
|
|
567
|
+
projectPath: opts.projectPath,
|
|
568
|
+
env: opts.env,
|
|
569
|
+
source: opts.source ?? "cli",
|
|
570
|
+
silent: false
|
|
571
|
+
// Log access for execution
|
|
572
|
+
});
|
|
573
|
+
if (val !== null) {
|
|
574
|
+
envMap[entry.key] = val;
|
|
575
|
+
if (val.length > 5) {
|
|
576
|
+
secretsToRedact.add(val);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const maxRuntime = profile.maxRuntimeSeconds ?? getExecMaxRuntime(opts.projectPath);
|
|
581
|
+
return new Promise((resolve, reject) => {
|
|
582
|
+
const networkTools = /* @__PURE__ */ new Set([
|
|
583
|
+
"curl",
|
|
584
|
+
"wget",
|
|
585
|
+
"ping",
|
|
586
|
+
"nc",
|
|
587
|
+
"netcat",
|
|
588
|
+
"ssh",
|
|
589
|
+
"telnet",
|
|
590
|
+
"ftp",
|
|
591
|
+
"dig",
|
|
592
|
+
"nslookup"
|
|
593
|
+
]);
|
|
594
|
+
if (profile.allowNetwork === false && networkTools.has(opts.command)) {
|
|
595
|
+
const msg = `[QRING] Execution blocked: network access is disabled for profile "${profile.name}", command "${opts.command}" is considered network-related`;
|
|
596
|
+
if (opts.captureOutput) {
|
|
597
|
+
return resolve({ code: 126, stdout: "", stderr: msg });
|
|
598
|
+
}
|
|
599
|
+
process.stderr.write(msg + "\n");
|
|
600
|
+
return resolve({ code: 126, stdout: "", stderr: "" });
|
|
601
|
+
}
|
|
602
|
+
const child = spawn(opts.command, opts.args, {
|
|
603
|
+
env: envMap,
|
|
604
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
605
|
+
shell: false
|
|
606
|
+
});
|
|
607
|
+
let timedOut = false;
|
|
608
|
+
let timer;
|
|
609
|
+
if (maxRuntime) {
|
|
610
|
+
timer = setTimeout(() => {
|
|
611
|
+
timedOut = true;
|
|
612
|
+
child.kill("SIGKILL");
|
|
613
|
+
}, maxRuntime * 1e3);
|
|
614
|
+
}
|
|
615
|
+
const stdoutRedact = new RedactionTransform([...secretsToRedact]);
|
|
616
|
+
const stderrRedact = new RedactionTransform([...secretsToRedact]);
|
|
617
|
+
if (child.stdout) child.stdout.pipe(stdoutRedact);
|
|
618
|
+
if (child.stderr) child.stderr.pipe(stderrRedact);
|
|
619
|
+
let stdoutStr = "";
|
|
620
|
+
let stderrStr = "";
|
|
621
|
+
if (opts.captureOutput) {
|
|
622
|
+
stdoutRedact.on("data", (d) => stdoutStr += d.toString());
|
|
623
|
+
stderrRedact.on("data", (d) => stderrStr += d.toString());
|
|
624
|
+
} else {
|
|
625
|
+
stdoutRedact.pipe(process.stdout);
|
|
626
|
+
stderrRedact.pipe(process.stderr);
|
|
627
|
+
}
|
|
628
|
+
child.on("close", (code) => {
|
|
629
|
+
if (timer) clearTimeout(timer);
|
|
630
|
+
if (timedOut) {
|
|
631
|
+
resolve({ code: 124, stdout: stdoutStr, stderr: stderrStr + `
|
|
632
|
+
[QRING] Process killed: exceeded ${maxRuntime}s runtime limit` });
|
|
633
|
+
} else {
|
|
634
|
+
resolve({ code: code ?? 0, stdout: stdoutStr, stderr: stderrStr });
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
child.on("error", (err) => {
|
|
638
|
+
if (timer) clearTimeout(timer);
|
|
639
|
+
reject(err);
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// src/core/scan.ts
|
|
645
|
+
import { readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
646
|
+
import { join } from "path";
|
|
647
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
648
|
+
"node_modules",
|
|
649
|
+
".git",
|
|
650
|
+
".next",
|
|
651
|
+
"dist",
|
|
652
|
+
"build",
|
|
653
|
+
"coverage",
|
|
654
|
+
".cursor",
|
|
655
|
+
"venv",
|
|
656
|
+
"__pycache__"
|
|
657
|
+
]);
|
|
658
|
+
var IGNORE_EXTS = /* @__PURE__ */ new Set([
|
|
659
|
+
".png",
|
|
660
|
+
".jpg",
|
|
661
|
+
".jpeg",
|
|
662
|
+
".gif",
|
|
663
|
+
".ico",
|
|
664
|
+
".svg",
|
|
665
|
+
".webp",
|
|
666
|
+
".mp4",
|
|
667
|
+
".mp3",
|
|
668
|
+
".wav",
|
|
669
|
+
".ogg",
|
|
670
|
+
".pdf",
|
|
671
|
+
".zip",
|
|
672
|
+
".tar",
|
|
673
|
+
".gz",
|
|
674
|
+
".xz",
|
|
675
|
+
".ttf",
|
|
676
|
+
".woff",
|
|
677
|
+
".woff2",
|
|
678
|
+
".eot",
|
|
679
|
+
".exe",
|
|
680
|
+
".dll",
|
|
681
|
+
".so",
|
|
682
|
+
".dylib",
|
|
683
|
+
".lock"
|
|
684
|
+
]);
|
|
685
|
+
var SECRET_KEYWORDS = /((?:api_?key|secret|token|password|auth|credential|access_?key)[a-z0-9_]*)\s*[:=]\s*(['"])([^'"]+)\2/i;
|
|
686
|
+
function calculateEntropy(str) {
|
|
687
|
+
if (!str) return 0;
|
|
688
|
+
const len = str.length;
|
|
689
|
+
const frequencies = /* @__PURE__ */ new Map();
|
|
690
|
+
for (let i = 0; i < len; i++) {
|
|
691
|
+
const char = str[i];
|
|
692
|
+
frequencies.set(char, (frequencies.get(char) || 0) + 1);
|
|
693
|
+
}
|
|
694
|
+
let entropy = 0;
|
|
695
|
+
for (const count of frequencies.values()) {
|
|
696
|
+
const p = count / len;
|
|
697
|
+
entropy -= p * Math.log2(p);
|
|
698
|
+
}
|
|
699
|
+
return entropy;
|
|
700
|
+
}
|
|
701
|
+
function scanCodebase(dir) {
|
|
702
|
+
const results = [];
|
|
703
|
+
function walk(currentDir) {
|
|
704
|
+
let entries;
|
|
705
|
+
try {
|
|
706
|
+
entries = readdirSync(currentDir);
|
|
707
|
+
} catch {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
for (const entry of entries) {
|
|
711
|
+
if (IGNORE_DIRS.has(entry)) continue;
|
|
712
|
+
const fullPath = join(currentDir, entry);
|
|
713
|
+
let stat;
|
|
714
|
+
try {
|
|
715
|
+
stat = statSync(fullPath);
|
|
716
|
+
} catch {
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
if (stat.isDirectory()) {
|
|
720
|
+
walk(fullPath);
|
|
721
|
+
} else if (stat.isFile()) {
|
|
722
|
+
const ext = fullPath.slice(fullPath.lastIndexOf(".")).toLowerCase();
|
|
723
|
+
if (IGNORE_EXTS.has(ext) || entry.endsWith(".lock")) continue;
|
|
724
|
+
let content;
|
|
725
|
+
try {
|
|
726
|
+
content = readFileSync2(fullPath, "utf8");
|
|
727
|
+
} catch {
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (content.includes("\0")) continue;
|
|
731
|
+
const lines = content.split(/\r?\n/);
|
|
732
|
+
for (let i = 0; i < lines.length; i++) {
|
|
733
|
+
const line = lines[i];
|
|
734
|
+
if (line.length > 500) continue;
|
|
735
|
+
const match = line.match(SECRET_KEYWORDS);
|
|
736
|
+
if (match) {
|
|
737
|
+
const varName = match[1];
|
|
738
|
+
const value = match[3];
|
|
739
|
+
if (value.length < 8) continue;
|
|
740
|
+
const lowerValue = value.toLowerCase();
|
|
741
|
+
if (lowerValue.includes("example") || lowerValue.includes("your_") || lowerValue.includes("placeholder") || lowerValue.includes("replace_me")) {
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
const entropy = calculateEntropy(value);
|
|
745
|
+
if (entropy > 3.5 || value.startsWith("sk-") || value.startsWith("ghp_")) {
|
|
746
|
+
const relPath = fullPath.startsWith(dir) ? fullPath.slice(dir.length).replace(/^[/\\]+/, "") : fullPath;
|
|
747
|
+
results.push({
|
|
748
|
+
file: relPath || fullPath,
|
|
749
|
+
line: i + 1,
|
|
750
|
+
keyName: varName,
|
|
751
|
+
match: value,
|
|
752
|
+
context: line.trim(),
|
|
753
|
+
entropy: parseFloat(entropy.toFixed(2))
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
walk(dir);
|
|
762
|
+
return results;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/core/linter.ts
|
|
766
|
+
import { readFileSync as readFileSync3, writeFileSync, existsSync } from "fs";
|
|
767
|
+
import { basename, extname } from "path";
|
|
768
|
+
var ENV_REF_BY_EXT = {
|
|
769
|
+
".ts": (k) => `process.env.${k}`,
|
|
770
|
+
".tsx": (k) => `process.env.${k}`,
|
|
771
|
+
".js": (k) => `process.env.${k}`,
|
|
772
|
+
".jsx": (k) => `process.env.${k}`,
|
|
773
|
+
".mjs": (k) => `process.env.${k}`,
|
|
774
|
+
".cjs": (k) => `process.env.${k}`,
|
|
775
|
+
".py": (k) => `os.environ["${k}"]`,
|
|
776
|
+
".rb": (k) => `ENV["${k}"]`,
|
|
777
|
+
".go": (k) => `os.Getenv("${k}")`,
|
|
778
|
+
".rs": (k) => `std::env::var("${k}")`,
|
|
779
|
+
".java": (k) => `System.getenv("${k}")`,
|
|
780
|
+
".kt": (k) => `System.getenv("${k}")`,
|
|
781
|
+
".cs": (k) => `Environment.GetEnvironmentVariable("${k}")`,
|
|
782
|
+
".php": (k) => `getenv('${k}')`,
|
|
783
|
+
".sh": (k) => `\${${k}}`,
|
|
784
|
+
".bash": (k) => `\${${k}}`
|
|
785
|
+
};
|
|
786
|
+
function getEnvRef(filePath, keyName) {
|
|
787
|
+
const ext = extname(filePath).toLowerCase();
|
|
788
|
+
const formatter = ENV_REF_BY_EXT[ext];
|
|
789
|
+
return formatter ? formatter(keyName) : `process.env.${keyName}`;
|
|
790
|
+
}
|
|
791
|
+
function lintFiles(files, opts = {}) {
|
|
792
|
+
const results = [];
|
|
793
|
+
for (const file of files) {
|
|
794
|
+
if (!existsSync(file)) continue;
|
|
795
|
+
let content;
|
|
796
|
+
try {
|
|
797
|
+
content = readFileSync3(file, "utf8");
|
|
798
|
+
} catch {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
if (content.includes("\0")) continue;
|
|
802
|
+
const SECRET_KEYWORDS2 = /((?:api_?key|secret|token|password|auth|credential|access_?key)[a-z0-9_]*)\s*[:=]\s*(['"])([^'"]+)\2/gi;
|
|
803
|
+
const lines = content.split(/\r?\n/);
|
|
804
|
+
const fixes = [];
|
|
805
|
+
for (let i = 0; i < lines.length; i++) {
|
|
806
|
+
const line = lines[i];
|
|
807
|
+
if (line.length > 500) continue;
|
|
808
|
+
let match;
|
|
809
|
+
SECRET_KEYWORDS2.lastIndex = 0;
|
|
810
|
+
while ((match = SECRET_KEYWORDS2.exec(line)) !== null) {
|
|
811
|
+
const varName = match[1].toUpperCase();
|
|
812
|
+
const quote = match[2];
|
|
813
|
+
const value = match[3];
|
|
814
|
+
if (value.length < 8) continue;
|
|
815
|
+
const lv = value.toLowerCase();
|
|
816
|
+
if (lv.includes("example") || lv.includes("your_") || lv.includes("placeholder") || lv.includes("replace_me") || lv.includes("xxx")) continue;
|
|
817
|
+
const entropy = calculateEntropy2(value);
|
|
818
|
+
if (entropy <= 3.5 && !value.startsWith("sk-") && !value.startsWith("ghp_")) continue;
|
|
819
|
+
const shouldFix = opts.fix === true;
|
|
820
|
+
if (shouldFix) {
|
|
821
|
+
const envRef = getEnvRef(file, varName);
|
|
822
|
+
fixes.push({
|
|
823
|
+
line: i,
|
|
824
|
+
original: `${quote}${value}${quote}`,
|
|
825
|
+
replacement: envRef,
|
|
826
|
+
keyName: varName,
|
|
827
|
+
value
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
results.push({
|
|
831
|
+
file,
|
|
832
|
+
line: i + 1,
|
|
833
|
+
keyName: varName,
|
|
834
|
+
match: value,
|
|
835
|
+
context: line.trim(),
|
|
836
|
+
entropy: parseFloat(entropy.toFixed(2)),
|
|
837
|
+
fixed: shouldFix
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (opts.fix && fixes.length > 0) {
|
|
842
|
+
const fixLines = content.split(/\r?\n/);
|
|
843
|
+
for (const fix of fixes.reverse()) {
|
|
844
|
+
const lineIdx = fix.line;
|
|
845
|
+
if (lineIdx >= 0 && lineIdx < fixLines.length) {
|
|
846
|
+
fixLines[lineIdx] = fixLines[lineIdx].replace(fix.original, fix.replacement);
|
|
847
|
+
}
|
|
848
|
+
if (!hasSecret(fix.keyName, { scope: opts.scope, projectPath: opts.projectPath })) {
|
|
849
|
+
setSecret(fix.keyName, fix.value, {
|
|
850
|
+
scope: opts.scope ?? "global",
|
|
851
|
+
projectPath: opts.projectPath,
|
|
852
|
+
source: "cli",
|
|
853
|
+
description: `Auto-imported from ${basename(file)}:${fix.line + 1}`
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
writeFileSync(file, fixLines.join("\n"), "utf8");
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return results;
|
|
861
|
+
}
|
|
862
|
+
function calculateEntropy2(str) {
|
|
863
|
+
if (!str) return 0;
|
|
864
|
+
const len = str.length;
|
|
865
|
+
const frequencies = /* @__PURE__ */ new Map();
|
|
866
|
+
for (let i = 0; i < len; i++) {
|
|
867
|
+
const ch = str[i];
|
|
868
|
+
frequencies.set(ch, (frequencies.get(ch) || 0) + 1);
|
|
869
|
+
}
|
|
870
|
+
let entropy = 0;
|
|
871
|
+
for (const count of frequencies.values()) {
|
|
872
|
+
const p = count / len;
|
|
873
|
+
entropy -= p * Math.log2(p);
|
|
874
|
+
}
|
|
875
|
+
return entropy;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/core/validate.ts
|
|
879
|
+
function makeRequest(url, headers, timeoutMs = 1e4) {
|
|
880
|
+
return httpRequest_({ url, method: "GET", headers, timeoutMs });
|
|
881
|
+
}
|
|
882
|
+
var ProviderRegistry = class {
|
|
883
|
+
providers = /* @__PURE__ */ new Map();
|
|
884
|
+
register(provider) {
|
|
885
|
+
this.providers.set(provider.name, provider);
|
|
886
|
+
}
|
|
887
|
+
get(name) {
|
|
888
|
+
return this.providers.get(name);
|
|
889
|
+
}
|
|
890
|
+
detectProvider(value, hints) {
|
|
891
|
+
if (hints?.provider) {
|
|
892
|
+
return this.providers.get(hints.provider);
|
|
893
|
+
}
|
|
894
|
+
for (const provider of this.providers.values()) {
|
|
895
|
+
if (provider.prefixes) {
|
|
896
|
+
for (const pfx of provider.prefixes) {
|
|
897
|
+
if (value.startsWith(pfx)) return provider;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return void 0;
|
|
902
|
+
}
|
|
903
|
+
listProviders() {
|
|
904
|
+
return [...this.providers.values()];
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
var openaiProvider = {
|
|
908
|
+
name: "openai",
|
|
909
|
+
description: "OpenAI API key validation",
|
|
910
|
+
prefixes: ["sk-"],
|
|
911
|
+
async validate(value) {
|
|
912
|
+
const start = Date.now();
|
|
913
|
+
try {
|
|
914
|
+
const { statusCode } = await makeRequest(
|
|
915
|
+
"https://api.openai.com/v1/models?limit=1",
|
|
916
|
+
{
|
|
917
|
+
Authorization: `Bearer ${value}`,
|
|
918
|
+
"User-Agent": "q-ring-validator/1.0"
|
|
919
|
+
}
|
|
920
|
+
);
|
|
921
|
+
const latencyMs = Date.now() - start;
|
|
922
|
+
if (statusCode === 200)
|
|
923
|
+
return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "openai" };
|
|
924
|
+
if (statusCode === 401)
|
|
925
|
+
return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "openai" };
|
|
926
|
+
if (statusCode === 429)
|
|
927
|
+
return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "openai" };
|
|
928
|
+
return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "openai" };
|
|
929
|
+
} catch (err) {
|
|
930
|
+
return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "openai" };
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
var stripeProvider = {
|
|
935
|
+
name: "stripe",
|
|
936
|
+
description: "Stripe API key validation",
|
|
937
|
+
prefixes: ["sk_live_", "sk_test_", "rk_live_", "rk_test_", "pk_live_", "pk_test_"],
|
|
938
|
+
async validate(value) {
|
|
939
|
+
const start = Date.now();
|
|
940
|
+
try {
|
|
941
|
+
const { statusCode } = await makeRequest(
|
|
942
|
+
"https://api.stripe.com/v1/balance",
|
|
943
|
+
{
|
|
944
|
+
Authorization: `Bearer ${value}`,
|
|
945
|
+
"User-Agent": "q-ring-validator/1.0"
|
|
946
|
+
}
|
|
947
|
+
);
|
|
948
|
+
const latencyMs = Date.now() - start;
|
|
949
|
+
if (statusCode === 200)
|
|
950
|
+
return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "stripe" };
|
|
951
|
+
if (statusCode === 401)
|
|
952
|
+
return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "stripe" };
|
|
953
|
+
if (statusCode === 429)
|
|
954
|
+
return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "stripe" };
|
|
955
|
+
return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "stripe" };
|
|
956
|
+
} catch (err) {
|
|
957
|
+
return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "stripe" };
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
var githubProvider = {
|
|
962
|
+
name: "github",
|
|
963
|
+
description: "GitHub token validation",
|
|
964
|
+
prefixes: ["ghp_", "gho_", "ghu_", "ghs_", "ghr_", "github_pat_"],
|
|
965
|
+
async validate(value) {
|
|
966
|
+
const start = Date.now();
|
|
967
|
+
try {
|
|
968
|
+
const { statusCode } = await makeRequest(
|
|
969
|
+
"https://api.github.com/user",
|
|
970
|
+
{
|
|
971
|
+
Authorization: `token ${value}`,
|
|
972
|
+
"User-Agent": "q-ring-validator/1.0",
|
|
973
|
+
Accept: "application/vnd.github+json"
|
|
974
|
+
}
|
|
975
|
+
);
|
|
976
|
+
const latencyMs = Date.now() - start;
|
|
977
|
+
if (statusCode === 200)
|
|
978
|
+
return { valid: true, status: "valid", message: "Token is valid", latencyMs, provider: "github" };
|
|
979
|
+
if (statusCode === 401)
|
|
980
|
+
return { valid: false, status: "invalid", message: "Invalid or expired token", latencyMs, provider: "github" };
|
|
981
|
+
if (statusCode === 403)
|
|
982
|
+
return { valid: false, status: "invalid", message: "Token lacks required permissions", latencyMs, provider: "github" };
|
|
983
|
+
if (statusCode === 429)
|
|
984
|
+
return { valid: true, status: "error", message: "Rate limited \u2014 token may be valid", latencyMs, provider: "github" };
|
|
985
|
+
return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "github" };
|
|
986
|
+
} catch (err) {
|
|
987
|
+
return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "github" };
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
var awsProvider = {
|
|
992
|
+
name: "aws",
|
|
993
|
+
description: "AWS access key validation (checks key format only \u2014 full STS validation requires secret key + region)",
|
|
994
|
+
prefixes: ["AKIA", "ASIA"],
|
|
995
|
+
async validate(value) {
|
|
996
|
+
const start = Date.now();
|
|
997
|
+
const latencyMs = Date.now() - start;
|
|
998
|
+
if (/^(AKIA|ASIA)[A-Z0-9]{16}$/.test(value)) {
|
|
999
|
+
return { valid: true, status: "unknown", message: "Valid AWS access key format (STS validation requires secret key)", latencyMs, provider: "aws" };
|
|
1000
|
+
}
|
|
1001
|
+
return { valid: false, status: "invalid", message: "Invalid AWS access key format", latencyMs, provider: "aws" };
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
var httpProvider = {
|
|
1005
|
+
name: "http",
|
|
1006
|
+
description: "Generic HTTP endpoint validation",
|
|
1007
|
+
async validate(value, url) {
|
|
1008
|
+
const start = Date.now();
|
|
1009
|
+
if (!url) {
|
|
1010
|
+
return { valid: false, status: "unknown", message: "No validation URL configured", latencyMs: 0, provider: "http" };
|
|
1011
|
+
}
|
|
1012
|
+
try {
|
|
1013
|
+
const { statusCode } = await makeRequest(url, {
|
|
1014
|
+
Authorization: `Bearer ${value}`,
|
|
1015
|
+
"User-Agent": "q-ring-validator/1.0"
|
|
1016
|
+
});
|
|
1017
|
+
const latencyMs = Date.now() - start;
|
|
1018
|
+
if (statusCode >= 200 && statusCode < 300)
|
|
1019
|
+
return { valid: true, status: "valid", message: `Endpoint returned ${statusCode}`, latencyMs, provider: "http" };
|
|
1020
|
+
if (statusCode === 401 || statusCode === 403)
|
|
1021
|
+
return { valid: false, status: "invalid", message: `Authentication failed (${statusCode})`, latencyMs, provider: "http" };
|
|
1022
|
+
return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "http" };
|
|
1023
|
+
} catch (err) {
|
|
1024
|
+
return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "http" };
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
var registry2 = new ProviderRegistry();
|
|
1029
|
+
registry2.register(openaiProvider);
|
|
1030
|
+
registry2.register(stripeProvider);
|
|
1031
|
+
registry2.register(githubProvider);
|
|
1032
|
+
registry2.register(awsProvider);
|
|
1033
|
+
registry2.register(httpProvider);
|
|
1034
|
+
async function validateSecret(value, opts) {
|
|
1035
|
+
const provider = opts?.provider ? registry2.get(opts.provider) : registry2.detectProvider(value);
|
|
1036
|
+
if (!provider) {
|
|
1037
|
+
return {
|
|
1038
|
+
valid: false,
|
|
1039
|
+
status: "unknown",
|
|
1040
|
+
message: "No provider detected \u2014 set a provider in the manifest or secret metadata",
|
|
1041
|
+
latencyMs: 0,
|
|
1042
|
+
provider: "none"
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
if (provider.name === "http" && opts?.validationUrl) {
|
|
1046
|
+
return provider.validate(value, opts.validationUrl);
|
|
1047
|
+
}
|
|
1048
|
+
return provider.validate(value);
|
|
1049
|
+
}
|
|
1050
|
+
async function rotateWithProvider(value, providerName) {
|
|
1051
|
+
const provider = providerName ? registry2.get(providerName) : registry2.detectProvider(value);
|
|
1052
|
+
if (!provider) {
|
|
1053
|
+
return { rotated: false, provider: "none", message: "No provider detected for rotation" };
|
|
1054
|
+
}
|
|
1055
|
+
const rotatable = provider;
|
|
1056
|
+
if (rotatable.supportsRotation && rotatable.rotate) {
|
|
1057
|
+
return rotatable.rotate(value);
|
|
1058
|
+
}
|
|
1059
|
+
const format = "api-key";
|
|
1060
|
+
const newValue = generateSecret({ format, length: 48 });
|
|
1061
|
+
return {
|
|
1062
|
+
rotated: true,
|
|
1063
|
+
provider: provider.name,
|
|
1064
|
+
message: `Provider "${provider.name}" does not support native rotation \u2014 generated new value locally`,
|
|
1065
|
+
newValue
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
async function ciValidateBatch(secrets) {
|
|
1069
|
+
const results = [];
|
|
1070
|
+
for (const s of secrets) {
|
|
1071
|
+
const validation = await validateSecret(s.value, {
|
|
1072
|
+
provider: s.provider,
|
|
1073
|
+
validationUrl: s.validationUrl
|
|
1074
|
+
});
|
|
1075
|
+
results.push({
|
|
1076
|
+
key: s.key,
|
|
1077
|
+
validation,
|
|
1078
|
+
requiresRotation: validation.status === "invalid"
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
const failCount = results.filter((r) => !r.validation.valid).length;
|
|
1082
|
+
return { results, allValid: failCount === 0, failCount };
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// src/core/context.ts
|
|
1086
|
+
function getProjectContext(opts = {}) {
|
|
1087
|
+
const projectPath = opts.projectPath ?? process.cwd();
|
|
1088
|
+
const envResult = collapseEnvironment({ projectPath });
|
|
1089
|
+
const secretsList = listSecrets({
|
|
1090
|
+
...opts,
|
|
1091
|
+
projectPath,
|
|
1092
|
+
silent: true
|
|
1093
|
+
});
|
|
1094
|
+
let expiredCount = 0;
|
|
1095
|
+
let staleCount = 0;
|
|
1096
|
+
let protectedCount = 0;
|
|
1097
|
+
const secrets = secretsList.map((entry) => {
|
|
1098
|
+
const meta = entry.envelope?.meta;
|
|
1099
|
+
const decay = entry.decay;
|
|
1100
|
+
if (decay?.isExpired) expiredCount++;
|
|
1101
|
+
if (decay?.isStale) staleCount++;
|
|
1102
|
+
if (meta?.requiresApproval) protectedCount++;
|
|
1103
|
+
return {
|
|
1104
|
+
key: entry.key,
|
|
1105
|
+
scope: entry.scope,
|
|
1106
|
+
tags: meta?.tags,
|
|
1107
|
+
description: meta?.description,
|
|
1108
|
+
provider: meta?.provider,
|
|
1109
|
+
requiresApproval: meta?.requiresApproval,
|
|
1110
|
+
jitProvider: meta?.jitProvider,
|
|
1111
|
+
hasStates: !!(entry.envelope?.states && Object.keys(entry.envelope.states).length > 0),
|
|
1112
|
+
isExpired: decay?.isExpired ?? false,
|
|
1113
|
+
isStale: decay?.isStale ?? false,
|
|
1114
|
+
timeRemaining: decay?.timeRemaining ?? null,
|
|
1115
|
+
accessCount: meta?.accessCount ?? 0,
|
|
1116
|
+
lastAccessed: meta?.lastAccessedAt ?? null,
|
|
1117
|
+
rotationFormat: meta?.rotationFormat
|
|
1118
|
+
};
|
|
1119
|
+
});
|
|
1120
|
+
let manifest = null;
|
|
1121
|
+
const config = readProjectConfig(projectPath);
|
|
1122
|
+
if (config?.secrets) {
|
|
1123
|
+
const declaredKeys = Object.keys(config.secrets);
|
|
1124
|
+
const existingKeys = new Set(secrets.map((s) => s.key));
|
|
1125
|
+
const missing = declaredKeys.filter((k) => !existingKeys.has(k));
|
|
1126
|
+
manifest = { declared: declaredKeys.length, missing };
|
|
1127
|
+
}
|
|
1128
|
+
const recentEvents = queryAudit({ limit: 20 });
|
|
1129
|
+
const recentActions = recentEvents.map((e) => ({
|
|
1130
|
+
action: e.action,
|
|
1131
|
+
key: e.key,
|
|
1132
|
+
source: e.source,
|
|
1133
|
+
timestamp: e.timestamp
|
|
1134
|
+
}));
|
|
1135
|
+
return {
|
|
1136
|
+
projectPath,
|
|
1137
|
+
environment: envResult ? { env: envResult.env, source: envResult.source } : null,
|
|
1138
|
+
secrets,
|
|
1139
|
+
totalSecrets: secrets.length,
|
|
1140
|
+
expiredCount,
|
|
1141
|
+
staleCount,
|
|
1142
|
+
protectedCount,
|
|
1143
|
+
manifest,
|
|
1144
|
+
validationProviders: registry2.listProviders().map((p) => p.name),
|
|
1145
|
+
jitProviders: registry.listProviders().map((p) => p.name),
|
|
1146
|
+
hooksCount: listHooks().length,
|
|
1147
|
+
recentActions
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/core/memory.ts
|
|
1152
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync } from "fs";
|
|
1153
|
+
import { join as join2 } from "path";
|
|
1154
|
+
import { homedir, hostname, userInfo } from "os";
|
|
1155
|
+
import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, createHash, randomBytes as randomBytes3 } from "crypto";
|
|
1156
|
+
var MEMORY_FILE = "agent-memory.enc";
|
|
1157
|
+
function getMemoryDir() {
|
|
1158
|
+
const dir = join2(homedir(), ".config", "q-ring");
|
|
1159
|
+
if (!existsSync2(dir)) {
|
|
1160
|
+
mkdirSync(dir, { recursive: true });
|
|
1161
|
+
}
|
|
1162
|
+
return dir;
|
|
1163
|
+
}
|
|
1164
|
+
function getMemoryPath() {
|
|
1165
|
+
return join2(getMemoryDir(), MEMORY_FILE);
|
|
1166
|
+
}
|
|
1167
|
+
function deriveKey2() {
|
|
1168
|
+
const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
|
|
1169
|
+
return createHash("sha256").update(fingerprint).digest();
|
|
1170
|
+
}
|
|
1171
|
+
function encrypt(data) {
|
|
1172
|
+
const key = deriveKey2();
|
|
1173
|
+
const iv = randomBytes3(12);
|
|
1174
|
+
const cipher = createCipheriv2("aes-256-gcm", key, iv);
|
|
1175
|
+
const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
|
|
1176
|
+
const tag = cipher.getAuthTag();
|
|
1177
|
+
return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
|
|
1178
|
+
}
|
|
1179
|
+
function decrypt(blob) {
|
|
1180
|
+
const parts = blob.split(":");
|
|
1181
|
+
if (parts.length !== 3) throw new Error("Invalid encrypted format");
|
|
1182
|
+
const iv = Buffer.from(parts[0], "base64");
|
|
1183
|
+
const tag = Buffer.from(parts[1], "base64");
|
|
1184
|
+
const encrypted = Buffer.from(parts[2], "base64");
|
|
1185
|
+
const key = deriveKey2();
|
|
1186
|
+
const decipher = createDecipheriv2("aes-256-gcm", key, iv);
|
|
1187
|
+
decipher.setAuthTag(tag);
|
|
1188
|
+
return decipher.update(encrypted) + decipher.final("utf8");
|
|
1189
|
+
}
|
|
1190
|
+
function loadStore() {
|
|
1191
|
+
const path = getMemoryPath();
|
|
1192
|
+
if (!existsSync2(path)) {
|
|
1193
|
+
return { entries: {} };
|
|
1194
|
+
}
|
|
1195
|
+
try {
|
|
1196
|
+
const raw = readFileSync4(path, "utf8");
|
|
1197
|
+
const decrypted = decrypt(raw);
|
|
1198
|
+
return JSON.parse(decrypted);
|
|
1199
|
+
} catch {
|
|
1200
|
+
return { entries: {} };
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
function saveStore(store) {
|
|
1204
|
+
const json = JSON.stringify(store);
|
|
1205
|
+
const encrypted = encrypt(json);
|
|
1206
|
+
writeFileSync2(getMemoryPath(), encrypted, "utf8");
|
|
1207
|
+
}
|
|
1208
|
+
function remember(key, value) {
|
|
1209
|
+
const store = loadStore();
|
|
1210
|
+
store.entries[key] = {
|
|
1211
|
+
value,
|
|
1212
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1213
|
+
};
|
|
1214
|
+
saveStore(store);
|
|
1215
|
+
}
|
|
1216
|
+
function recall(key) {
|
|
1217
|
+
const store = loadStore();
|
|
1218
|
+
return store.entries[key]?.value ?? null;
|
|
1219
|
+
}
|
|
1220
|
+
function listMemory() {
|
|
1221
|
+
const store = loadStore();
|
|
1222
|
+
return Object.entries(store.entries).map(([key, entry]) => ({
|
|
1223
|
+
key,
|
|
1224
|
+
updatedAt: entry.updatedAt
|
|
1225
|
+
}));
|
|
1226
|
+
}
|
|
1227
|
+
function forget(key) {
|
|
1228
|
+
const store = loadStore();
|
|
1229
|
+
if (key in store.entries) {
|
|
1230
|
+
delete store.entries[key];
|
|
1231
|
+
saveStore(store);
|
|
1232
|
+
return true;
|
|
1233
|
+
}
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
function clearMemory() {
|
|
1237
|
+
saveStore({ entries: {} });
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// src/hooks/precommit.ts
|
|
1241
|
+
import { execSync } from "child_process";
|
|
1242
|
+
import { existsSync as existsSync3, writeFileSync as writeFileSync3, chmodSync, readFileSync as readFileSync5, unlinkSync } from "fs";
|
|
1243
|
+
import { join as join3 } from "path";
|
|
1244
|
+
function getStagedFiles() {
|
|
1245
|
+
try {
|
|
1246
|
+
const output = execSync("git diff --cached --name-only --diff-filter=ACM", {
|
|
1247
|
+
encoding: "utf8",
|
|
1248
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1249
|
+
});
|
|
1250
|
+
return output.split("\n").map((f) => f.trim()).filter((f) => f.length > 0);
|
|
1251
|
+
} catch {
|
|
1252
|
+
return [];
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
function runPreCommitScan() {
|
|
1256
|
+
const staged = getStagedFiles();
|
|
1257
|
+
if (staged.length === 0) return 0;
|
|
1258
|
+
const results = lintFiles(staged);
|
|
1259
|
+
if (results.length === 0) return 0;
|
|
1260
|
+
console.error("\n[q-ring] Pre-commit scan found hardcoded secrets:\n");
|
|
1261
|
+
for (const r of results) {
|
|
1262
|
+
console.error(` ${r.file}:${r.line} ${r.keyName} (entropy: ${r.entropy})`);
|
|
1263
|
+
}
|
|
1264
|
+
console.error(
|
|
1265
|
+
`
|
|
1266
|
+
[q-ring] Commit blocked. Use "qring scan --fix" to auto-migrate, or "git commit --no-verify" to bypass.
|
|
1267
|
+
`
|
|
1268
|
+
);
|
|
1269
|
+
return 1;
|
|
1270
|
+
}
|
|
1271
|
+
var HOOK_SCRIPT = `#!/bin/sh
|
|
1272
|
+
# q-ring pre-commit hook \u2014 scans staged files for hardcoded secrets
|
|
1273
|
+
npx qring hook:run 2>&1
|
|
1274
|
+
exit $?
|
|
1275
|
+
`;
|
|
1276
|
+
function installPreCommitHook(repoPath) {
|
|
1277
|
+
const root = repoPath ?? process.cwd();
|
|
1278
|
+
const hooksDir = join3(root, ".git", "hooks");
|
|
1279
|
+
if (!existsSync3(join3(root, ".git"))) {
|
|
1280
|
+
return { installed: false, path: "", message: "Not a git repository" };
|
|
1281
|
+
}
|
|
1282
|
+
const hookPath = join3(hooksDir, "pre-commit");
|
|
1283
|
+
if (existsSync3(hookPath)) {
|
|
1284
|
+
const existing = readFileSync5(hookPath, "utf8");
|
|
1285
|
+
if (existing.includes("q-ring")) {
|
|
1286
|
+
return { installed: true, path: hookPath, message: "Hook already installed" };
|
|
1287
|
+
}
|
|
1288
|
+
writeFileSync3(hookPath, HOOK_SCRIPT + "\n" + existing, "utf8");
|
|
1289
|
+
} else {
|
|
1290
|
+
writeFileSync3(hookPath, HOOK_SCRIPT, "utf8");
|
|
1291
|
+
}
|
|
1292
|
+
chmodSync(hookPath, 493);
|
|
1293
|
+
return { installed: true, path: hookPath, message: "Pre-commit hook installed" };
|
|
1294
|
+
}
|
|
1295
|
+
function uninstallPreCommitHook(repoPath) {
|
|
1296
|
+
const root = repoPath ?? process.cwd();
|
|
1297
|
+
const hookPath = join3(root, ".git", "hooks", "pre-commit");
|
|
1298
|
+
if (!existsSync3(hookPath)) return false;
|
|
1299
|
+
const content = readFileSync5(hookPath, "utf8");
|
|
1300
|
+
if (!content.includes("q-ring")) return false;
|
|
1301
|
+
const lines = content.split("\n");
|
|
1302
|
+
const cleaned = lines.filter(
|
|
1303
|
+
(l) => !l.includes("q-ring") && !l.includes("npx qring hook:run")
|
|
1304
|
+
);
|
|
1305
|
+
if (cleaned.filter((l) => l.trim() && !l.startsWith("#!")).length === 0) {
|
|
1306
|
+
unlinkSync(hookPath);
|
|
1307
|
+
} else {
|
|
1308
|
+
writeFileSync3(hookPath, cleaned.join("\n"), "utf8");
|
|
1309
|
+
}
|
|
1310
|
+
return true;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// src/cli/commands.ts
|
|
1314
|
+
import { writeFileSync as writeFileSync4, readFileSync as readFileSync6, existsSync as existsSync4 } from "fs";
|
|
1315
|
+
|
|
356
1316
|
// src/utils/prompt.ts
|
|
357
1317
|
import { createInterface } from "readline";
|
|
358
1318
|
async function promptSecret(message) {
|
|
@@ -405,6 +1365,8 @@ function buildOpts(cmd) {
|
|
|
405
1365
|
let scope;
|
|
406
1366
|
if (cmd.global) scope = "global";
|
|
407
1367
|
else if (cmd.project) scope = "project";
|
|
1368
|
+
else if (cmd.team) scope = "team";
|
|
1369
|
+
else if (cmd.org) scope = "org";
|
|
408
1370
|
const projectPath = cmd.projectPath ?? (cmd.project ? process.cwd() : void 0);
|
|
409
1371
|
if (scope === "project" && !projectPath) {
|
|
410
1372
|
throw new Error("Project path is required for project scope");
|
|
@@ -412,6 +1374,8 @@ function buildOpts(cmd) {
|
|
|
412
1374
|
return {
|
|
413
1375
|
scope,
|
|
414
1376
|
projectPath: projectPath ?? process.cwd(),
|
|
1377
|
+
teamId: cmd.team,
|
|
1378
|
+
orgId: cmd.org,
|
|
415
1379
|
env: cmd.env,
|
|
416
1380
|
source: "cli"
|
|
417
1381
|
};
|
|
@@ -419,8 +1383,8 @@ function buildOpts(cmd) {
|
|
|
419
1383
|
function createProgram() {
|
|
420
1384
|
const program2 = new Command().name("qring").description(
|
|
421
1385
|
`${c.bold("q-ring")} ${c.dim("\u2014 quantum keyring for AI coding tools")}`
|
|
422
|
-
).version("0.
|
|
423
|
-
program2.command("set <key> [value]").description("Store a secret (with optional quantum metadata)").option("-g, --global", "Store in global scope").option("-p, --project", "Store in project scope (uses cwd)").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Set value for a specific environment (superposition)").option("--ttl <seconds>", "Time-to-live in seconds (quantum decay)", parseInt).option("--expires <iso>", "Expiry timestamp (ISO 8601)").option("--description <desc>", "Human-readable description").option("--tags <tags>", "Comma-separated tags").action(async (key, value, cmd) => {
|
|
1386
|
+
).version("0.4.0");
|
|
1387
|
+
program2.command("set <key> [value]").description("Store a secret (with optional quantum metadata)").option("-g, --global", "Store in global scope").option("-p, --project", "Store in project scope (uses cwd)").option("--team <id>", "Store in team scope").option("--org <id>", "Store in org scope").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Set value for a specific environment (superposition)").option("--ttl <seconds>", "Time-to-live in seconds (quantum decay)", parseInt).option("--expires <iso>", "Expiry timestamp (ISO 8601)").option("--description <desc>", "Human-readable description").option("--tags <tags>", "Comma-separated tags").option("--rotation-format <format>", "Format for auto-rotation (api-key, password, uuid, hex, base64, alphanumeric, token)").option("--rotation-prefix <prefix>", "Prefix for auto-rotation (e.g. sk-)").option("--requires-approval", "Require explicit user approval for MCP agents to read").option("--jit-provider <provider>", "Use a Just-In-Time provider to dynamically generate this secret").action(async (key, value, cmd) => {
|
|
424
1388
|
const opts = buildOpts(cmd);
|
|
425
1389
|
if (!value) {
|
|
426
1390
|
value = await promptSecret(`${SYMBOLS.key} Enter value for ${c.bold(key)}: `);
|
|
@@ -434,7 +1398,11 @@ function createProgram() {
|
|
|
434
1398
|
ttlSeconds: cmd.ttl,
|
|
435
1399
|
expiresAt: cmd.expires,
|
|
436
1400
|
description: cmd.description,
|
|
437
|
-
tags: cmd.tags?.split(",").map((t) => t.trim())
|
|
1401
|
+
tags: cmd.tags?.split(",").map((t) => t.trim()),
|
|
1402
|
+
rotationFormat: cmd.rotationFormat,
|
|
1403
|
+
rotationPrefix: cmd.rotationPrefix,
|
|
1404
|
+
requiresApproval: cmd.requiresApproval,
|
|
1405
|
+
jitProvider: cmd.jitProvider
|
|
438
1406
|
};
|
|
439
1407
|
if (cmd.env) {
|
|
440
1408
|
const existing = getEnvelope(key, opts);
|
|
@@ -459,7 +1427,7 @@ function createProgram() {
|
|
|
459
1427
|
);
|
|
460
1428
|
}
|
|
461
1429
|
});
|
|
462
|
-
program2.command("get <key>").description("Retrieve a secret (collapses superposition if needed)").option("-g, --global", "Look only in global scope").option("-p, --project", "Look only in project scope").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Force a specific environment").action((key, cmd) => {
|
|
1430
|
+
program2.command("get <key>").description("Retrieve a secret (collapses superposition if needed)").option("-g, --global", "Look only in global scope").option("-p, --project", "Look only in project scope").option("--team <id>", "Look only in team scope").option("--org <id>", "Look only in org scope").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Force a specific environment").action((key, cmd) => {
|
|
463
1431
|
const opts = buildOpts(cmd);
|
|
464
1432
|
const value = getSecret(key, opts);
|
|
465
1433
|
if (value === null) {
|
|
@@ -478,9 +1446,29 @@ function createProgram() {
|
|
|
478
1446
|
process.exit(1);
|
|
479
1447
|
}
|
|
480
1448
|
});
|
|
481
|
-
program2.command("list").alias("ls").description("List all secrets with quantum status indicators").option("-g, --global", "List global scope only").option("-p, --project", "List project scope only").option("--project-path <path>", "Explicit project path").option("--show-decay", "Show decay/expiry status").action((cmd) => {
|
|
1449
|
+
program2.command("list").alias("ls").description("List all secrets with quantum status indicators").option("-g, --global", "List global scope only").option("-p, --project", "List project scope only").option("--team <id>", "List team scope only").option("--org <id>", "List org scope only").option("--project-path <path>", "Explicit project path").option("--show-decay", "Show decay/expiry status").option("-t, --tag <tag>", "Filter by tag").option("--expired", "Show only expired secrets").option("--stale", "Show only stale secrets (75%+ decay)").option("-f, --filter <pattern>", "Glob pattern on key name").action((cmd) => {
|
|
482
1450
|
const opts = buildOpts(cmd);
|
|
483
|
-
|
|
1451
|
+
let entries = listSecrets(opts);
|
|
1452
|
+
if (cmd.tag) {
|
|
1453
|
+
entries = entries.filter(
|
|
1454
|
+
(e) => e.envelope?.meta.tags?.includes(cmd.tag)
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
if (cmd.expired) {
|
|
1458
|
+
entries = entries.filter((e) => e.decay?.isExpired);
|
|
1459
|
+
}
|
|
1460
|
+
if (cmd.stale) {
|
|
1461
|
+
entries = entries.filter(
|
|
1462
|
+
(e) => e.decay?.isStale && !e.decay?.isExpired
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
if (cmd.filter) {
|
|
1466
|
+
const regex = new RegExp(
|
|
1467
|
+
"^" + cmd.filter.replace(/\*/g, ".*") + "$",
|
|
1468
|
+
"i"
|
|
1469
|
+
);
|
|
1470
|
+
entries = entries.filter((e) => regex.test(e.key));
|
|
1471
|
+
}
|
|
484
1472
|
if (entries.length === 0) {
|
|
485
1473
|
console.log(c.dim("No secrets found"));
|
|
486
1474
|
return;
|
|
@@ -593,50 +1581,605 @@ function createProgram() {
|
|
|
593
1581
|
}
|
|
594
1582
|
console.log();
|
|
595
1583
|
});
|
|
596
|
-
program2.command("export").description("Export secrets as .env or JSON (collapses superposition)").option("-f, --format <format>", "Output format: env or json", "env").option("-g, --global", "Export global scope only").option("-p, --project", "Export project scope only").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Force environment for collapse").action((cmd) => {
|
|
1584
|
+
program2.command("export").description("Export secrets as .env or JSON (collapses superposition)").option("-f, --format <format>", "Output format: env or json", "env").option("-g, --global", "Export global scope only").option("-p, --project", "Export project scope only").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Force environment for collapse").option("-k, --keys <keys>", "Comma-separated key names to export").option("-t, --tags <tags>", "Comma-separated tags to filter by").action((cmd) => {
|
|
597
1585
|
const opts = buildOpts(cmd);
|
|
598
|
-
const output = exportSecrets({
|
|
1586
|
+
const output = exportSecrets({
|
|
1587
|
+
...opts,
|
|
1588
|
+
format: cmd.format,
|
|
1589
|
+
keys: cmd.keys?.split(",").map((k) => k.trim()),
|
|
1590
|
+
tags: cmd.tags?.split(",").map((t) => t.trim())
|
|
1591
|
+
});
|
|
599
1592
|
process.stdout.write(output + "\n");
|
|
600
1593
|
});
|
|
601
|
-
program2.command("
|
|
602
|
-
const
|
|
603
|
-
|
|
1594
|
+
program2.command("import <file>").description("Import secrets from a .env file").option("-g, --global", "Import to global scope").option("-p, --project", "Import to project scope").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Environment context").option("--skip-existing", "Skip keys that already exist").option("--dry-run", "Preview what would be imported without saving").action((file, cmd) => {
|
|
1595
|
+
const opts = buildOpts(cmd);
|
|
1596
|
+
const result = importDotenv(file, {
|
|
1597
|
+
scope: opts.scope,
|
|
1598
|
+
projectPath: opts.projectPath,
|
|
1599
|
+
source: "cli",
|
|
1600
|
+
skipExisting: cmd.skipExisting,
|
|
1601
|
+
dryRun: cmd.dryRun
|
|
604
1602
|
});
|
|
605
|
-
if (
|
|
1603
|
+
if (cmd.dryRun) {
|
|
606
1604
|
console.log(
|
|
607
|
-
|
|
1605
|
+
`
|
|
1606
|
+
${SYMBOLS.package} ${c.bold("Dry run")} \u2014 would import ${result.imported.length} of ${result.total} secrets:
|
|
1607
|
+
`
|
|
608
1608
|
);
|
|
1609
|
+
for (const key of result.imported) {
|
|
1610
|
+
console.log(` ${SYMBOLS.key} ${c.bold(key)}`);
|
|
1611
|
+
}
|
|
1612
|
+
if (result.skipped.length > 0) {
|
|
1613
|
+
console.log(`
|
|
1614
|
+
${c.dim(`Skipped (existing): ${result.skipped.join(", ")}`)}`);
|
|
1615
|
+
}
|
|
609
1616
|
} else {
|
|
610
1617
|
console.log(
|
|
611
|
-
c.
|
|
1618
|
+
`${SYMBOLS.check} ${c.green("imported")} ${result.imported.length} secret(s) from ${c.bold(file)}`
|
|
612
1619
|
);
|
|
1620
|
+
if (result.skipped.length > 0) {
|
|
1621
|
+
console.log(
|
|
1622
|
+
c.dim(` skipped ${result.skipped.length} existing: ${result.skipped.join(", ")}`)
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
613
1625
|
}
|
|
1626
|
+
console.log();
|
|
614
1627
|
});
|
|
615
|
-
program2.command("
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
length: cmd.length,
|
|
623
|
-
prefix: cmd.prefix
|
|
624
|
-
});
|
|
625
|
-
const entropy = estimateEntropy(secret);
|
|
626
|
-
if (cmd.save) {
|
|
627
|
-
const opts = buildOpts(cmd);
|
|
628
|
-
setSecret(cmd.save, secret, opts);
|
|
1628
|
+
program2.command("check").description("Validate project secrets against .q-ring.json manifest").option("--project-path <path>", "Project path (defaults to cwd)").action((cmd) => {
|
|
1629
|
+
const projectPath = cmd.projectPath ?? process.cwd();
|
|
1630
|
+
const config = readProjectConfig(projectPath);
|
|
1631
|
+
if (!config?.secrets || Object.keys(config.secrets).length === 0) {
|
|
1632
|
+
console.error(
|
|
1633
|
+
c.red(`${SYMBOLS.cross} No secrets manifest found in .q-ring.json`)
|
|
1634
|
+
);
|
|
629
1635
|
console.log(
|
|
630
|
-
|
|
1636
|
+
c.dim(' Add a "secrets" field to your .q-ring.json to define required secrets.')
|
|
631
1637
|
);
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
1638
|
+
process.exit(1);
|
|
1639
|
+
}
|
|
1640
|
+
console.log(
|
|
1641
|
+
c.bold(`
|
|
1642
|
+
${SYMBOLS.shield} Project secret manifest check
|
|
1643
|
+
`)
|
|
1644
|
+
);
|
|
1645
|
+
let present = 0;
|
|
1646
|
+
let missing = 0;
|
|
1647
|
+
let expiredCount = 0;
|
|
1648
|
+
let staleCount = 0;
|
|
1649
|
+
for (const [key, manifest] of Object.entries(config.secrets)) {
|
|
1650
|
+
const result = getEnvelope(key, { projectPath, source: "cli" });
|
|
1651
|
+
if (!result) {
|
|
1652
|
+
if (manifest.required !== false) {
|
|
1653
|
+
missing++;
|
|
1654
|
+
console.log(
|
|
1655
|
+
` ${c.red(SYMBOLS.cross)} ${c.bold(key)} ${c.red("MISSING")} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
|
|
1656
|
+
);
|
|
1657
|
+
} else {
|
|
1658
|
+
console.log(
|
|
1659
|
+
` ${c.dim(SYMBOLS.cross)} ${c.bold(key)} ${c.dim("optional, not set")} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
const decay = checkDecay(result.envelope);
|
|
1665
|
+
if (decay.isExpired) {
|
|
1666
|
+
expiredCount++;
|
|
635
1667
|
console.log(
|
|
636
|
-
`
|
|
637
|
-
${c.dim(`format: ${cmd.format} | entropy: ~${entropy} bits`)}`
|
|
1668
|
+
` ${c.red(SYMBOLS.warning)} ${c.bold(key)} ${c.bgRed(c.white(" EXPIRED "))} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
|
|
638
1669
|
);
|
|
639
|
-
}
|
|
1670
|
+
} else if (decay.isStale) {
|
|
1671
|
+
staleCount++;
|
|
1672
|
+
console.log(
|
|
1673
|
+
` ${c.yellow(SYMBOLS.warning)} ${c.bold(key)} ${c.yellow(`stale (${decay.lifetimePercent}%)`)} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
|
|
1674
|
+
);
|
|
1675
|
+
} else {
|
|
1676
|
+
present++;
|
|
1677
|
+
console.log(
|
|
1678
|
+
` ${c.green(SYMBOLS.check)} ${c.bold(key)} ${c.green("OK")} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
const total = Object.keys(config.secrets).length;
|
|
1683
|
+
console.log(
|
|
1684
|
+
`
|
|
1685
|
+
${c.bold(`${total} declared`)} ${c.green(`${present} present`)} ${c.yellow(`${staleCount} stale`)} ${c.red(`${expiredCount} expired`)} ${c.red(`${missing} missing`)}`
|
|
1686
|
+
);
|
|
1687
|
+
if (missing > 0) {
|
|
1688
|
+
console.log(
|
|
1689
|
+
`
|
|
1690
|
+
${c.red("Project is NOT ready \u2014 missing required secrets.")}`
|
|
1691
|
+
);
|
|
1692
|
+
} else if (expiredCount > 0) {
|
|
1693
|
+
console.log(
|
|
1694
|
+
`
|
|
1695
|
+
${c.yellow("Project has expired secrets that need rotation.")}`
|
|
1696
|
+
);
|
|
1697
|
+
} else {
|
|
1698
|
+
console.log(
|
|
1699
|
+
`
|
|
1700
|
+
${c.green(`${SYMBOLS.check} Project is ready \u2014 all required secrets present.`)}`
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
console.log();
|
|
1704
|
+
if (missing > 0) process.exit(1);
|
|
1705
|
+
});
|
|
1706
|
+
program2.command("validate [key]").description("Test if a secret is actually valid with its target service").option("-g, --global", "Global scope only").option("-p, --project", "Project scope only").option("--project-path <path>", "Explicit project path").option("--provider <name>", "Force a specific provider (openai, stripe, github, aws, http)").option("--all", "Validate all secrets that have a detectable provider").option("--manifest", "Only validate manifest-declared secrets (with --all)").option("--list-providers", "List all available providers").action(async (key, cmd) => {
|
|
1707
|
+
if (cmd.listProviders) {
|
|
1708
|
+
console.log(c.bold(`
|
|
1709
|
+
${SYMBOLS.shield} Available validation providers
|
|
1710
|
+
`));
|
|
1711
|
+
for (const p of registry2.listProviders()) {
|
|
1712
|
+
const prefixes = p.prefixes?.length ? c.dim(` (${p.prefixes.join(", ")})`) : "";
|
|
1713
|
+
console.log(` ${c.cyan(p.name.padEnd(10))} ${p.description}${prefixes}`);
|
|
1714
|
+
}
|
|
1715
|
+
console.log();
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
if (!key && !cmd.all) {
|
|
1719
|
+
console.error(c.red(`${SYMBOLS.cross} Provide a key name or use --all`));
|
|
1720
|
+
process.exit(1);
|
|
1721
|
+
}
|
|
1722
|
+
const opts = buildOpts(cmd);
|
|
1723
|
+
if (cmd.all) {
|
|
1724
|
+
let entries = listSecrets(opts);
|
|
1725
|
+
const projectPath = cmd.projectPath ?? process.cwd();
|
|
1726
|
+
if (cmd.manifest) {
|
|
1727
|
+
const config = readProjectConfig(projectPath);
|
|
1728
|
+
if (config?.secrets) {
|
|
1729
|
+
const manifestKeys = new Set(Object.keys(config.secrets));
|
|
1730
|
+
entries = entries.filter((e) => manifestKeys.has(e.key));
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
console.log(c.bold(`
|
|
1734
|
+
${SYMBOLS.shield} Validating secrets
|
|
1735
|
+
`));
|
|
1736
|
+
let validated = 0;
|
|
1737
|
+
let skipped = 0;
|
|
1738
|
+
for (const entry of entries) {
|
|
1739
|
+
const value2 = getSecret(entry.key, { ...opts, scope: entry.scope });
|
|
1740
|
+
if (!value2) {
|
|
1741
|
+
skipped++;
|
|
1742
|
+
continue;
|
|
1743
|
+
}
|
|
1744
|
+
const provHint2 = entry.envelope?.meta.provider ?? cmd.provider;
|
|
1745
|
+
const result2 = await validateSecret(value2, { provider: provHint2 });
|
|
1746
|
+
if (result2.status === "unknown") {
|
|
1747
|
+
skipped++;
|
|
1748
|
+
continue;
|
|
1749
|
+
}
|
|
1750
|
+
validated++;
|
|
1751
|
+
const icon2 = result2.status === "valid" ? c.green(SYMBOLS.check) : result2.status === "invalid" ? c.red(SYMBOLS.cross) : c.yellow(SYMBOLS.warning);
|
|
1752
|
+
const statusText = result2.status === "valid" ? c.green("valid") : result2.status === "invalid" ? c.red("invalid") : c.yellow("error");
|
|
1753
|
+
console.log(
|
|
1754
|
+
` ${icon2} ${c.bold(entry.key.padEnd(24))} ${statusText} ${c.dim(`(${result2.provider}, ${result2.latencyMs}ms)`)}${result2.status !== "valid" ? ` ${c.dim("\u2014 " + result2.message)}` : ""}`
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
console.log(`
|
|
1758
|
+
${c.dim(`${validated} validated, ${skipped} skipped (no provider)`)}
|
|
1759
|
+
`);
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
const value = getSecret(key, opts);
|
|
1763
|
+
if (!value) {
|
|
1764
|
+
console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
|
|
1765
|
+
process.exit(1);
|
|
1766
|
+
}
|
|
1767
|
+
const envelope = getEnvelope(key, opts);
|
|
1768
|
+
const provHint = envelope?.envelope.meta.provider ?? cmd.provider;
|
|
1769
|
+
const result = await validateSecret(value, { provider: provHint });
|
|
1770
|
+
const icon = result.status === "valid" ? c.green(SYMBOLS.check) : result.status === "invalid" ? c.red(SYMBOLS.cross) : result.status === "error" ? c.yellow(SYMBOLS.warning) : c.dim("\u25CB");
|
|
1771
|
+
console.log(`
|
|
1772
|
+
${icon} ${c.bold(key)} ${result.status} ${c.dim(`(${result.provider}, ${result.latencyMs}ms)`)}`);
|
|
1773
|
+
if (result.message && result.status !== "valid") {
|
|
1774
|
+
console.log(` ${c.dim(result.message)}`);
|
|
1775
|
+
}
|
|
1776
|
+
console.log();
|
|
1777
|
+
});
|
|
1778
|
+
program2.command("exec <command...>").description("Run a command with secrets injected into its environment (output auto-redacted)").option("-g, --global", "Inject global scope secrets only").option("-p, --project", "Inject project scope secrets only").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Environment context").option("-k, --keys <keys>", "Comma-separated key names to inject").option("-t, --tags <tags>", "Comma-separated tags to filter by").option("--profile <name>", "Exec profile: unrestricted, restricted, ci").action(async (commandArgs, cmd) => {
|
|
1779
|
+
const opts = buildOpts(cmd);
|
|
1780
|
+
const command = commandArgs[0];
|
|
1781
|
+
const args = commandArgs.slice(1);
|
|
1782
|
+
try {
|
|
1783
|
+
const { code } = await execCommand({
|
|
1784
|
+
...opts,
|
|
1785
|
+
command,
|
|
1786
|
+
args,
|
|
1787
|
+
keys: cmd.keys?.split(",").map((k) => k.trim()),
|
|
1788
|
+
tags: cmd.tags?.split(",").map((t) => t.trim()),
|
|
1789
|
+
profile: cmd.profile
|
|
1790
|
+
});
|
|
1791
|
+
process.exit(code);
|
|
1792
|
+
} catch (err) {
|
|
1793
|
+
console.error(c.red(`${SYMBOLS.cross} Exec failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
1794
|
+
process.exit(1);
|
|
1795
|
+
}
|
|
1796
|
+
});
|
|
1797
|
+
program2.command("scan [dir]").description("Scan a codebase for hardcoded secrets").option("--fix", "Auto-replace hardcoded secrets with process.env references and store in q-ring").option("-g, --global", "Store fixed secrets in global scope").option("-p, --project", "Store fixed secrets in project scope").option("--project-path <path>", "Explicit project path").action((dir, cmd) => {
|
|
1798
|
+
const targetDir = dir ?? process.cwd();
|
|
1799
|
+
const fixMode = cmd.fix === true;
|
|
1800
|
+
console.log(`
|
|
1801
|
+
${SYMBOLS.eye} Scanning ${c.bold(targetDir)} for secrets...${fixMode ? c.yellow(" [--fix mode]") : ""}
|
|
1802
|
+
`);
|
|
1803
|
+
const results = scanCodebase(targetDir);
|
|
1804
|
+
if (results.length === 0) {
|
|
1805
|
+
console.log(` ${c.green(SYMBOLS.check)} No hardcoded secrets found. Awesome!
|
|
1806
|
+
`);
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
if (fixMode) {
|
|
1810
|
+
const fileSet = new Set(results.map((r) => r.file.startsWith("/") ? r.file : `${targetDir}/${r.file}`));
|
|
1811
|
+
const opts = buildOpts(cmd);
|
|
1812
|
+
const lintResults = lintFiles([...fileSet], { fix: true, scope: opts.scope, projectPath: opts.projectPath });
|
|
1813
|
+
const fixedCount = lintResults.filter((r) => r.fixed).length;
|
|
1814
|
+
console.log(` ${c.green(SYMBOLS.check)} Fixed ${fixedCount} secrets \u2014 replaced with process.env references and stored in q-ring.
|
|
1815
|
+
`);
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
for (const res of results) {
|
|
1819
|
+
console.log(` ${c.red(SYMBOLS.cross)} ${c.bold(res.file)}:${res.line}`);
|
|
1820
|
+
console.log(` ${c.dim("Key:")} ${c.cyan(res.keyName)}`);
|
|
1821
|
+
console.log(` ${c.dim("Entropy:")} ${res.entropy > 4 ? c.red(res.entropy.toString()) : c.yellow(res.entropy.toString())}`);
|
|
1822
|
+
console.log(` ${c.dim("Context:")} ${res.context}`);
|
|
1823
|
+
console.log();
|
|
1824
|
+
}
|
|
1825
|
+
console.log(` ${c.red(`Found ${results.length} potential secrets.`)} Use ${c.bold("qring scan --fix")} to auto-migrate them.
|
|
1826
|
+
`);
|
|
1827
|
+
});
|
|
1828
|
+
program2.command("lint <files...>").description("Lint specific files for hardcoded secrets (with optional auto-fix)").option("--fix", "Replace hardcoded secrets with process.env references and store in q-ring").option("-g, --global", "Store fixed secrets in global scope").option("-p, --project", "Store fixed secrets in project scope").option("--project-path <path>", "Explicit project path").action((files, cmd) => {
|
|
1829
|
+
const opts = buildOpts(cmd);
|
|
1830
|
+
const results = lintFiles(files, { fix: cmd.fix, scope: opts.scope, projectPath: opts.projectPath });
|
|
1831
|
+
if (results.length === 0) {
|
|
1832
|
+
console.log(`
|
|
1833
|
+
${c.green(SYMBOLS.check)} No hardcoded secrets found in ${files.length} file(s).
|
|
1834
|
+
`);
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
for (const res of results) {
|
|
1838
|
+
const status = res.fixed ? c.green("fixed") : c.red("found");
|
|
1839
|
+
console.log(` ${res.fixed ? c.green(SYMBOLS.check) : c.red(SYMBOLS.cross)} ${c.bold(res.file)}:${res.line} [${status}]`);
|
|
1840
|
+
console.log(` ${c.dim("Key:")} ${c.cyan(res.keyName)}`);
|
|
1841
|
+
console.log(` ${c.dim("Entropy:")} ${res.entropy > 4 ? c.red(res.entropy.toString()) : c.yellow(res.entropy.toString())}`);
|
|
1842
|
+
console.log();
|
|
1843
|
+
}
|
|
1844
|
+
const fixedCount = results.filter((r) => r.fixed).length;
|
|
1845
|
+
if (cmd.fix && fixedCount > 0) {
|
|
1846
|
+
console.log(` ${c.green(`Fixed ${fixedCount} secret(s)`)} \u2014 replaced with env references and stored in q-ring.
|
|
1847
|
+
`);
|
|
1848
|
+
} else {
|
|
1849
|
+
console.log(` ${c.red(`Found ${results.length} potential secret(s).`)} Use ${c.bold("--fix")} to auto-migrate.
|
|
1850
|
+
`);
|
|
1851
|
+
}
|
|
1852
|
+
});
|
|
1853
|
+
program2.command("context").alias("describe").description("Show safe, redacted project context for AI agents (no secret values exposed)").option("-g, --global", "Global scope only").option("-p, --project", "Project scope only").option("--project-path <path>", "Explicit project path").option("--json", "Output as JSON (for MCP / programmatic use)").action((cmd) => {
|
|
1854
|
+
const opts = buildOpts(cmd);
|
|
1855
|
+
const context = getProjectContext(opts);
|
|
1856
|
+
if (cmd.json) {
|
|
1857
|
+
console.log(JSON.stringify(context, null, 2));
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
console.log(`
|
|
1861
|
+
${SYMBOLS.zap} ${c.bold("Project Context for AI Assistant")}`);
|
|
1862
|
+
console.log(` Project: ${c.cyan(context.projectPath)}`);
|
|
1863
|
+
if (context.environment) {
|
|
1864
|
+
console.log(` Environment: ${envBadge(context.environment.env)} ${c.dim(`(${context.environment.source})`)}`);
|
|
1865
|
+
}
|
|
1866
|
+
console.log(`
|
|
1867
|
+
${c.bold(" Secrets:")} ${context.totalSecrets} total ${c.dim(`(${context.expiredCount} expired, ${context.staleCount} stale, ${context.protectedCount} protected)`)}`);
|
|
1868
|
+
for (const s of context.secrets) {
|
|
1869
|
+
const tags = s.tags?.length ? c.dim(` [${s.tags.join(",")}]`) : "";
|
|
1870
|
+
const flags = [];
|
|
1871
|
+
if (s.requiresApproval) flags.push(c.yellow("locked"));
|
|
1872
|
+
if (s.jitProvider) flags.push(c.magenta("jit"));
|
|
1873
|
+
if (s.hasStates) flags.push(c.blue("superposition"));
|
|
1874
|
+
if (s.isExpired) flags.push(c.red("expired"));
|
|
1875
|
+
else if (s.isStale) flags.push(c.yellow("stale"));
|
|
1876
|
+
const flagStr = flags.length ? ` ${flags.join(" ")}` : "";
|
|
1877
|
+
console.log(` ${c.bold(s.key)} ${scopeColor(s.scope)}${tags}${flagStr}`);
|
|
1878
|
+
}
|
|
1879
|
+
if (context.manifest) {
|
|
1880
|
+
console.log(`
|
|
1881
|
+
${c.bold(" Manifest:")} ${context.manifest.declared} declared`);
|
|
1882
|
+
if (context.manifest.missing.length > 0) {
|
|
1883
|
+
console.log(` ${c.red("Missing:")} ${context.manifest.missing.join(", ")}`);
|
|
1884
|
+
} else {
|
|
1885
|
+
console.log(` ${c.green(SYMBOLS.check)} All manifest secrets present`);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
console.log(`
|
|
1889
|
+
${c.bold(" Providers:")} ${context.validationProviders.join(", ") || "none"}`);
|
|
1890
|
+
console.log(`${c.bold(" JIT Providers:")} ${context.jitProviders.join(", ") || "none"}`);
|
|
1891
|
+
console.log(`${c.bold(" Hooks:")} ${context.hooksCount} registered`);
|
|
1892
|
+
if (context.recentActions.length > 0) {
|
|
1893
|
+
console.log(`
|
|
1894
|
+
${c.bold(" Recent Activity:")} (last ${context.recentActions.length})`);
|
|
1895
|
+
for (const a of context.recentActions.slice(0, 8)) {
|
|
1896
|
+
const ts = new Date(a.timestamp).toLocaleTimeString();
|
|
1897
|
+
console.log(` ${c.dim(ts)} ${a.action}${a.key ? ` ${c.bold(a.key)}` : ""} ${c.dim(`(${a.source})`)}`);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
console.log();
|
|
1901
|
+
});
|
|
1902
|
+
program2.command("remember <key> <value>").description("Store a key-value pair in encrypted agent memory (persists across sessions)").action((key, value) => {
|
|
1903
|
+
remember(key, value);
|
|
1904
|
+
console.log(`${SYMBOLS.check} ${c.green("remembered")} ${c.bold(key)}`);
|
|
1905
|
+
});
|
|
1906
|
+
program2.command("recall [key]").description("Retrieve a value from agent memory, or list all keys").action((key) => {
|
|
1907
|
+
if (!key) {
|
|
1908
|
+
const entries = listMemory();
|
|
1909
|
+
if (entries.length === 0) {
|
|
1910
|
+
console.log(c.dim("Agent memory is empty."));
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
console.log(`
|
|
1914
|
+
${SYMBOLS.zap} ${c.bold("Agent Memory")} (${entries.length} entries)
|
|
1915
|
+
`);
|
|
1916
|
+
for (const e of entries) {
|
|
1917
|
+
console.log(` ${c.bold(e.key)} ${c.dim(new Date(e.updatedAt).toLocaleString())}`);
|
|
1918
|
+
}
|
|
1919
|
+
console.log();
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
const value = recall(key);
|
|
1923
|
+
if (value === null) {
|
|
1924
|
+
console.log(c.dim(`No memory found for "${key}"`));
|
|
1925
|
+
} else {
|
|
1926
|
+
console.log(safeStr(value));
|
|
1927
|
+
}
|
|
1928
|
+
});
|
|
1929
|
+
program2.command("forget <key>").description("Delete a key from agent memory").option("--all", "Clear all agent memory").action((key, cmd) => {
|
|
1930
|
+
if (cmd.all) {
|
|
1931
|
+
clearMemory();
|
|
1932
|
+
console.log(`${SYMBOLS.check} ${c.yellow("cleared")} all agent memory`);
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
const removed = forget(key);
|
|
1936
|
+
if (removed) {
|
|
1937
|
+
console.log(`${SYMBOLS.check} ${c.yellow("forgot")} ${c.bold(key)}`);
|
|
1938
|
+
} else {
|
|
1939
|
+
console.log(c.dim(`No memory found for "${key}"`));
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
program2.command("approve <key>").description("Grant a scoped, reasoned, HMAC-verified approval token for MCP secret access").option("-g, --global", "Global scope").option("-p, --project", "Project scope").option("--project-path <path>", "Explicit project path").option("--for <seconds>", "Duration of approval in seconds", parseInt, 3600).option("--reason <text>", "Reason for granting approval").option("--revoke", "Revoke an existing approval").option("--list", "List all approvals").action((key, cmd) => {
|
|
1943
|
+
const opts = buildOpts(cmd);
|
|
1944
|
+
const scope = opts.scope ?? "global";
|
|
1945
|
+
if (cmd.list) {
|
|
1946
|
+
const approvals = listApprovals();
|
|
1947
|
+
if (approvals.length === 0) {
|
|
1948
|
+
console.log(c.dim(" No active approvals"));
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
for (const a of approvals) {
|
|
1952
|
+
const status = a.tampered ? c.red("TAMPERED") : a.valid ? c.green("active") : c.dim("expired");
|
|
1953
|
+
const ttl = Math.max(0, Math.round((new Date(a.expiresAt).getTime() - Date.now()) / 1e3));
|
|
1954
|
+
console.log(` ${status} ${c.bold(a.key)} [${a.scope}] reason=${c.dim(a.reason)} ttl=${ttl}s granted-by=${a.grantedBy}`);
|
|
1955
|
+
}
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
if (cmd.revoke) {
|
|
1959
|
+
const revoked = revokeApproval(key, scope);
|
|
1960
|
+
if (revoked) {
|
|
1961
|
+
console.log(`${SYMBOLS.check} ${c.yellow("revoked")} approval for ${c.bold(key)}`);
|
|
1962
|
+
} else {
|
|
1963
|
+
console.log(c.dim(` No active approval found for ${key}`));
|
|
1964
|
+
}
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
const entry = grantApproval(key, scope, cmd.for, {
|
|
1968
|
+
reason: cmd.reason ?? "manual approval"
|
|
1969
|
+
});
|
|
1970
|
+
console.log(`${SYMBOLS.check} ${c.green("approved")} ${c.bold(key)} for ${cmd.for}s`);
|
|
1971
|
+
console.log(c.dim(` id=${entry.id} reason="${entry.reason}" expires=${entry.expiresAt}`));
|
|
1972
|
+
});
|
|
1973
|
+
program2.command("approvals").description("List all approval tokens with verification status").action(() => {
|
|
1974
|
+
const approvals = listApprovals();
|
|
1975
|
+
if (approvals.length === 0) {
|
|
1976
|
+
console.log(c.dim(" No approvals found"));
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
console.log(c.bold("\n\u{1F510} Approval Tokens\n"));
|
|
1980
|
+
for (const a of approvals) {
|
|
1981
|
+
const status = a.tampered ? c.red(`${SYMBOLS.cross} TAMPERED`) : a.valid ? c.green(`${SYMBOLS.check} active`) : c.dim(`${SYMBOLS.warning} expired`);
|
|
1982
|
+
const ttl = Math.max(0, Math.round((new Date(a.expiresAt).getTime() - Date.now()) / 1e3));
|
|
1983
|
+
console.log(` ${status} ${c.bold(a.key)} [${a.scope}]`);
|
|
1984
|
+
console.log(c.dim(` id=${a.id} reason="${a.reason}" ttl=${ttl}s by=${a.grantedBy}`));
|
|
1985
|
+
if (a.workspace) console.log(c.dim(` workspace=${a.workspace}`));
|
|
1986
|
+
}
|
|
1987
|
+
console.log();
|
|
1988
|
+
});
|
|
1989
|
+
program2.command("hook:install").description("Install a git pre-commit hook that scans for hardcoded secrets").option("--project-path <path>", "Repository path").action((cmd) => {
|
|
1990
|
+
const result = installPreCommitHook(cmd.projectPath);
|
|
1991
|
+
if (result.installed) {
|
|
1992
|
+
console.log(`${SYMBOLS.check} ${c.green(result.message)} at ${c.dim(result.path)}`);
|
|
1993
|
+
} else {
|
|
1994
|
+
console.log(`${SYMBOLS.cross} ${c.red(result.message)}`);
|
|
1995
|
+
}
|
|
1996
|
+
});
|
|
1997
|
+
program2.command("hook:uninstall").description("Remove the q-ring pre-commit hook").option("--project-path <path>", "Repository path").action((cmd) => {
|
|
1998
|
+
const removed = uninstallPreCommitHook(cmd.projectPath);
|
|
1999
|
+
if (removed) {
|
|
2000
|
+
console.log(`${SYMBOLS.check} ${c.green("Pre-commit hook removed")}`);
|
|
2001
|
+
} else {
|
|
2002
|
+
console.log(c.dim("No q-ring pre-commit hook found"));
|
|
2003
|
+
}
|
|
2004
|
+
});
|
|
2005
|
+
program2.command("hook:run").description("Run the pre-commit secret scan (called by the git hook)").action(() => {
|
|
2006
|
+
const code = runPreCommitScan();
|
|
2007
|
+
process.exit(code);
|
|
2008
|
+
});
|
|
2009
|
+
program2.command("wizard <name>").description("Set up a new service integration with secrets, manifest, and hooks").option("--keys <keys>", "Comma-separated secret key names to create (e.g. API_KEY,API_SECRET)").option("--provider <provider>", "Validation provider (e.g. openai, stripe, github)").option("--tags <tags>", "Comma-separated tags for all secrets").option("--hook-exec <cmd>", "Shell command to run when any of these secrets change").option("-g, --global", "Global scope").option("-p, --project", "Project scope").option("--project-path <path>", "Explicit project path").action(async (name, cmd) => {
|
|
2010
|
+
const opts = buildOpts(cmd);
|
|
2011
|
+
const prefix = name.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
2012
|
+
const tags = cmd.tags?.split(",").map((t) => t.trim()) ?? [name.toLowerCase()];
|
|
2013
|
+
const provider = cmd.provider;
|
|
2014
|
+
let keyNames;
|
|
2015
|
+
if (cmd.keys) {
|
|
2016
|
+
keyNames = cmd.keys.split(",").map((k) => k.trim());
|
|
2017
|
+
} else {
|
|
2018
|
+
keyNames = [`${prefix}_API_KEY`, `${prefix}_API_SECRET`];
|
|
2019
|
+
}
|
|
2020
|
+
console.log(`
|
|
2021
|
+
${SYMBOLS.zap} ${c.bold(`Setting up service: ${name}`)}
|
|
2022
|
+
`);
|
|
2023
|
+
for (const key of keyNames) {
|
|
2024
|
+
const value = generateSecret({ format: "api-key", prefix: `${prefix.toLowerCase()}_` });
|
|
2025
|
+
setSecret(key, value, {
|
|
2026
|
+
...opts,
|
|
2027
|
+
tags,
|
|
2028
|
+
provider,
|
|
2029
|
+
description: `Auto-generated by wizard for ${name}`
|
|
2030
|
+
});
|
|
2031
|
+
console.log(` ${c.green(SYMBOLS.check)} Created ${c.bold(key)}`);
|
|
2032
|
+
}
|
|
2033
|
+
const projectPath = opts.projectPath ?? process.cwd();
|
|
2034
|
+
const manifestPath = `${projectPath}/.q-ring.json`;
|
|
2035
|
+
let config = {};
|
|
2036
|
+
try {
|
|
2037
|
+
if (existsSync4(manifestPath)) {
|
|
2038
|
+
config = JSON.parse(readFileSync6(manifestPath, "utf8"));
|
|
2039
|
+
}
|
|
2040
|
+
} catch {
|
|
2041
|
+
}
|
|
2042
|
+
if (!config.secrets) config.secrets = {};
|
|
2043
|
+
for (const key of keyNames) {
|
|
2044
|
+
config.secrets[key] = {
|
|
2045
|
+
required: true,
|
|
2046
|
+
description: `${name} integration`,
|
|
2047
|
+
...provider ? { provider } : {}
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
writeFileSync4(manifestPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
2051
|
+
console.log(` ${c.green(SYMBOLS.check)} Updated ${c.dim(".q-ring.json")} manifest`);
|
|
2052
|
+
if (cmd.hookExec) {
|
|
2053
|
+
for (const key of keyNames) {
|
|
2054
|
+
registerHook({
|
|
2055
|
+
type: "shell",
|
|
2056
|
+
match: { key, action: ["write", "delete"] },
|
|
2057
|
+
command: cmd.hookExec,
|
|
2058
|
+
description: `${name} wizard hook`,
|
|
2059
|
+
enabled: true
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
console.log(` ${c.green(SYMBOLS.check)} Registered hook: ${c.dim(cmd.hookExec)}`);
|
|
2063
|
+
}
|
|
2064
|
+
console.log(`
|
|
2065
|
+
${c.green("Done!")} Service "${name}" is ready with ${keyNames.length} secrets.
|
|
2066
|
+
`);
|
|
2067
|
+
});
|
|
2068
|
+
program2.command("analyze").description("Analyze secret usage patterns and provide optimization suggestions").option("-g, --global", "Global scope only").option("-p, --project", "Project scope only").option("--project-path <path>", "Explicit project path").action((cmd) => {
|
|
2069
|
+
const opts = buildOpts(cmd);
|
|
2070
|
+
const entries = listSecrets({ ...opts, silent: true });
|
|
2071
|
+
const audit = queryAudit({ limit: 1e3 });
|
|
2072
|
+
console.log(`
|
|
2073
|
+
${SYMBOLS.zap} ${c.bold("Secret Usage Analysis")}
|
|
2074
|
+
`);
|
|
2075
|
+
const accessMap = /* @__PURE__ */ new Map();
|
|
2076
|
+
for (const e of audit) {
|
|
2077
|
+
if (e.action === "read" && e.key) {
|
|
2078
|
+
accessMap.set(e.key, (accessMap.get(e.key) || 0) + 1);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
const sorted = [...accessMap.entries()].sort((a, b) => b[1] - a[1]);
|
|
2082
|
+
if (sorted.length > 0) {
|
|
2083
|
+
console.log(` ${c.bold("Most accessed:")}`);
|
|
2084
|
+
for (const [key, count] of sorted.slice(0, 5)) {
|
|
2085
|
+
console.log(` ${c.bold(key)} \u2014 ${c.cyan(count.toString())} reads`);
|
|
2086
|
+
}
|
|
2087
|
+
console.log();
|
|
2088
|
+
}
|
|
2089
|
+
const neverAccessed = entries.filter((e) => {
|
|
2090
|
+
const count = e.envelope?.meta.accessCount ?? 0;
|
|
2091
|
+
return count === 0;
|
|
2092
|
+
});
|
|
2093
|
+
if (neverAccessed.length > 0) {
|
|
2094
|
+
console.log(` ${c.bold("Never accessed:")} ${c.yellow(neverAccessed.length.toString())} secrets`);
|
|
2095
|
+
for (const e of neverAccessed.slice(0, 8)) {
|
|
2096
|
+
const age = e.envelope?.meta.createdAt ? c.dim(`(created ${new Date(e.envelope.meta.createdAt).toLocaleDateString()})`) : "";
|
|
2097
|
+
console.log(` ${c.dim(SYMBOLS.cross)} ${e.key} ${age}`);
|
|
2098
|
+
}
|
|
2099
|
+
console.log();
|
|
2100
|
+
}
|
|
2101
|
+
const expired = entries.filter((e) => e.decay?.isExpired);
|
|
2102
|
+
const stale = entries.filter((e) => e.decay?.isStale && !e.decay?.isExpired);
|
|
2103
|
+
if (expired.length > 0) {
|
|
2104
|
+
console.log(` ${c.red("Expired:")} ${expired.length} secrets need rotation or cleanup`);
|
|
2105
|
+
for (const e of expired.slice(0, 5)) {
|
|
2106
|
+
console.log(` ${c.red(SYMBOLS.cross)} ${e.key}`);
|
|
2107
|
+
}
|
|
2108
|
+
console.log();
|
|
2109
|
+
}
|
|
2110
|
+
if (stale.length > 0) {
|
|
2111
|
+
console.log(` ${c.yellow("Stale (>75% lifetime):")} ${stale.length} secrets approaching expiry`);
|
|
2112
|
+
for (const e of stale.slice(0, 5)) {
|
|
2113
|
+
console.log(` ${c.yellow(SYMBOLS.warning)} ${e.key} ${c.dim(`(${e.decay?.timeRemaining} remaining)`)}`);
|
|
2114
|
+
}
|
|
2115
|
+
console.log();
|
|
2116
|
+
}
|
|
2117
|
+
const globalOnly = entries.filter((e) => e.scope === "global");
|
|
2118
|
+
const withProjectTags = globalOnly.filter(
|
|
2119
|
+
(e) => e.envelope?.meta.tags?.some((t) => ["backend", "frontend", "db", "api"].includes(t))
|
|
2120
|
+
);
|
|
2121
|
+
if (withProjectTags.length > 0) {
|
|
2122
|
+
console.log(` ${c.bold("Scope suggestions:")}`);
|
|
2123
|
+
console.log(` ${withProjectTags.length} global secret(s) have project-specific tags \u2014 consider moving to project scope`);
|
|
2124
|
+
console.log();
|
|
2125
|
+
}
|
|
2126
|
+
const noRotation = entries.filter(
|
|
2127
|
+
(e) => !e.envelope?.meta.rotationFormat && !e.decay?.isExpired
|
|
2128
|
+
);
|
|
2129
|
+
if (noRotation.length > 0) {
|
|
2130
|
+
console.log(` ${c.bold("Rotation suggestions:")}`);
|
|
2131
|
+
console.log(` ${noRotation.length} secret(s) have no rotation format set`);
|
|
2132
|
+
console.log(` Use ${c.bold("qring set <key> <value> --rotation-format api-key")} to enable auto-rotation`);
|
|
2133
|
+
console.log();
|
|
2134
|
+
}
|
|
2135
|
+
console.log(` ${c.bold("Summary:")}`);
|
|
2136
|
+
console.log(` Total secrets: ${entries.length}`);
|
|
2137
|
+
console.log(` Active: ${entries.length - expired.length}`);
|
|
2138
|
+
console.log(` Expired: ${expired.length}`);
|
|
2139
|
+
console.log(` Stale: ${stale.length}`);
|
|
2140
|
+
console.log(` Never accessed: ${neverAccessed.length}`);
|
|
2141
|
+
console.log(` With rotation config: ${entries.length - noRotation.length}`);
|
|
2142
|
+
console.log();
|
|
2143
|
+
});
|
|
2144
|
+
program2.command("env").description("Show detected environment (wavefunction collapse context)").option("--project-path <path>", "Project path for detection").action((cmd) => {
|
|
2145
|
+
const result = collapseEnvironment({
|
|
2146
|
+
projectPath: cmd.projectPath ?? process.cwd()
|
|
2147
|
+
});
|
|
2148
|
+
if (result) {
|
|
2149
|
+
console.log(
|
|
2150
|
+
`${SYMBOLS.zap} ${c.bold("Collapsed environment:")} ${envBadge(result.env)} ${c.dim(`(source: ${result.source})`)}`
|
|
2151
|
+
);
|
|
2152
|
+
} else {
|
|
2153
|
+
console.log(
|
|
2154
|
+
c.dim("No environment detected. Set QRING_ENV, NODE_ENV, or create .q-ring.json")
|
|
2155
|
+
);
|
|
2156
|
+
}
|
|
2157
|
+
});
|
|
2158
|
+
program2.command("generate").alias("gen").description("Generate a cryptographic secret (quantum noise)").option(
|
|
2159
|
+
"-f, --format <format>",
|
|
2160
|
+
"Format: hex, base64, alphanumeric, uuid, api-key, token, password",
|
|
2161
|
+
"api-key"
|
|
2162
|
+
).option("-l, --length <n>", "Length (bytes or chars depending on format)", parseInt).option("--prefix <prefix>", "Prefix for api-key/token format").option("-s, --save <key>", "Save the generated secret to keyring with this key").option("-g, --global", "Save to global scope").option("-p, --project", "Save to project scope").option("--project-path <path>", "Explicit project path").action((cmd) => {
|
|
2163
|
+
const secret = generateSecret({
|
|
2164
|
+
format: cmd.format,
|
|
2165
|
+
length: cmd.length,
|
|
2166
|
+
prefix: cmd.prefix
|
|
2167
|
+
});
|
|
2168
|
+
const entropy = estimateEntropy(secret);
|
|
2169
|
+
if (cmd.save) {
|
|
2170
|
+
const opts = buildOpts(cmd);
|
|
2171
|
+
setSecret(cmd.save, secret, opts);
|
|
2172
|
+
console.log(
|
|
2173
|
+
`${SYMBOLS.sparkle} ${c.green("generated & saved")} ${c.bold(cmd.save)} ${c.dim(`(${cmd.format}, ${entropy} bits entropy)`)}`
|
|
2174
|
+
);
|
|
2175
|
+
} else {
|
|
2176
|
+
process.stdout.write(secret);
|
|
2177
|
+
if (process.stdout.isTTY) {
|
|
2178
|
+
console.log(
|
|
2179
|
+
`
|
|
2180
|
+
${c.dim(`format: ${cmd.format} | entropy: ~${entropy} bits`)}`
|
|
2181
|
+
);
|
|
2182
|
+
}
|
|
640
2183
|
}
|
|
641
2184
|
});
|
|
642
2185
|
program2.command("entangle <sourceKey> <targetKey>").description("Link two secrets \u2014 rotating one updates the other").option("-g, --global", "Both in global scope").option("--source-project <path>", "Source project path").option("--target-project <path>", "Target project path").action(
|
|
@@ -657,6 +2200,24 @@ ${c.dim(`format: ${cmd.format} | entropy: ~${entropy} bits`)}`
|
|
|
657
2200
|
);
|
|
658
2201
|
}
|
|
659
2202
|
);
|
|
2203
|
+
program2.command("disentangle <sourceKey> <targetKey>").description("Unlink two entangled secrets").option("-g, --global", "Both in global scope").option("--source-project <path>", "Source project path").option("--target-project <path>", "Target project path").action(
|
|
2204
|
+
(sourceKey, targetKey, cmd) => {
|
|
2205
|
+
const sourceOpts = {
|
|
2206
|
+
scope: cmd.sourceProject ? "project" : "global",
|
|
2207
|
+
projectPath: cmd.sourceProject ?? process.cwd(),
|
|
2208
|
+
source: "cli"
|
|
2209
|
+
};
|
|
2210
|
+
const targetOpts = {
|
|
2211
|
+
scope: cmd.targetProject ? "project" : "global",
|
|
2212
|
+
projectPath: cmd.targetProject ?? process.cwd(),
|
|
2213
|
+
source: "cli"
|
|
2214
|
+
};
|
|
2215
|
+
disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts);
|
|
2216
|
+
console.log(
|
|
2217
|
+
`${SYMBOLS.link} ${c.yellow("disentangled")} ${c.bold(sourceKey)} ${SYMBOLS.arrow} ${c.bold(targetKey)}`
|
|
2218
|
+
);
|
|
2219
|
+
}
|
|
2220
|
+
);
|
|
660
2221
|
const tunnel = program2.command("tunnel").description("Ephemeral in-memory secrets (quantum tunneling)");
|
|
661
2222
|
tunnel.command("create <value>").description("Create a tunneled secret (returns tunnel ID)").option("--ttl <seconds>", "Auto-expire after N seconds", parseInt).option("--max-reads <n>", "Self-destruct after N reads", parseInt).action((value, cmd) => {
|
|
662
2223
|
const id = tunnelCreate(value, {
|
|
@@ -836,6 +2397,36 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
|
|
|
836
2397
|
}
|
|
837
2398
|
console.log();
|
|
838
2399
|
});
|
|
2400
|
+
program2.command("audit:verify").description("Verify the integrity of the audit hash chain").action(() => {
|
|
2401
|
+
const result = verifyAuditChain();
|
|
2402
|
+
if (result.totalEvents === 0) {
|
|
2403
|
+
console.log(c.dim(" No audit events to verify"));
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
if (result.intact) {
|
|
2407
|
+
console.log(`${SYMBOLS.shield} ${c.green("Audit chain intact")} \u2014 ${result.totalEvents} events verified`);
|
|
2408
|
+
} else {
|
|
2409
|
+
console.log(`${SYMBOLS.cross} ${c.red("Audit chain BROKEN")} at event #${result.brokenAt}`);
|
|
2410
|
+
console.log(c.dim(` ${result.validEvents}/${result.totalEvents} events valid before break`));
|
|
2411
|
+
if (result.brokenEvent) {
|
|
2412
|
+
console.log(c.dim(` Broken event: ${result.brokenEvent.timestamp} ${result.brokenEvent.action} ${result.brokenEvent.key ?? ""}`));
|
|
2413
|
+
}
|
|
2414
|
+
process.exitCode = 1;
|
|
2415
|
+
}
|
|
2416
|
+
});
|
|
2417
|
+
program2.command("audit:export").description("Export audit events in a portable format").option("--since <date>", "Start date (ISO 8601)").option("--until <date>", "End date (ISO 8601)").option("--format <fmt>", "Output format: jsonl, json, csv", "jsonl").option("-o, --output <file>", "Write to file instead of stdout").action((cmd) => {
|
|
2418
|
+
const output = exportAudit({
|
|
2419
|
+
since: cmd.since,
|
|
2420
|
+
until: cmd.until,
|
|
2421
|
+
format: cmd.format
|
|
2422
|
+
});
|
|
2423
|
+
if (cmd.output) {
|
|
2424
|
+
writeFileSync4(cmd.output, output);
|
|
2425
|
+
console.log(`${SYMBOLS.check} Exported to ${cmd.output}`);
|
|
2426
|
+
} else {
|
|
2427
|
+
console.log(output);
|
|
2428
|
+
}
|
|
2429
|
+
});
|
|
839
2430
|
program2.command("health").description("Check the health of all secrets").option("-g, --global", "Check global scope only").option("-p, --project", "Check project scope only").option("--project-path <path>", "Explicit project path").action((cmd) => {
|
|
840
2431
|
const opts = buildOpts(cmd);
|
|
841
2432
|
const entries = listSecrets(opts);
|
|
@@ -884,8 +2475,166 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
|
|
|
884
2475
|
}
|
|
885
2476
|
console.log();
|
|
886
2477
|
});
|
|
2478
|
+
const hook = program2.command("hook").description("Manage secret change hooks (callbacks on write/delete/rotate)");
|
|
2479
|
+
hook.command("add").description("Register a new hook").option("--key <key>", "Trigger on exact key match").option("--key-pattern <pattern>", "Trigger on key glob pattern (e.g. DB_*)").option("--tag <tag>", "Trigger on secrets with this tag").option("--scope <scope>", "Trigger only for this scope (global or project)").option("--action <actions>", "Comma-separated actions: write,delete,rotate", "write,delete,rotate").option("--exec <command>", "Shell command to execute").option("--url <url>", "HTTP URL to POST to").option("--signal-target <target>", "Process name or PID to signal").option("--signal-name <signal>", "Signal to send (default: SIGHUP)", "SIGHUP").option("--description <desc>", "Human-readable description").action((cmd) => {
|
|
2480
|
+
let type;
|
|
2481
|
+
if (cmd.exec) type = "shell";
|
|
2482
|
+
else if (cmd.url) type = "http";
|
|
2483
|
+
else if (cmd.signalTarget) type = "signal";
|
|
2484
|
+
else {
|
|
2485
|
+
console.error(c.red(`${SYMBOLS.cross} Specify --exec, --url, or --signal-target`));
|
|
2486
|
+
process.exit(1);
|
|
2487
|
+
}
|
|
2488
|
+
if (!cmd.key && !cmd.keyPattern && !cmd.tag) {
|
|
2489
|
+
console.error(c.red(`${SYMBOLS.cross} Specify at least one match criterion: --key, --key-pattern, or --tag`));
|
|
2490
|
+
process.exit(1);
|
|
2491
|
+
}
|
|
2492
|
+
const actions = cmd.action.split(",").map((a) => a.trim());
|
|
2493
|
+
const entry = registerHook({
|
|
2494
|
+
type,
|
|
2495
|
+
match: {
|
|
2496
|
+
key: cmd.key,
|
|
2497
|
+
keyPattern: cmd.keyPattern,
|
|
2498
|
+
tag: cmd.tag,
|
|
2499
|
+
scope: cmd.scope,
|
|
2500
|
+
action: actions
|
|
2501
|
+
},
|
|
2502
|
+
command: cmd.exec,
|
|
2503
|
+
url: cmd.url,
|
|
2504
|
+
signal: cmd.signalTarget ? { target: cmd.signalTarget, signal: cmd.signalName } : void 0,
|
|
2505
|
+
description: cmd.description,
|
|
2506
|
+
enabled: true
|
|
2507
|
+
});
|
|
2508
|
+
console.log(`${SYMBOLS.check} ${c.green("registered")} hook ${c.bold(entry.id)} (${type})`);
|
|
2509
|
+
if (cmd.key) console.log(c.dim(` key: ${cmd.key}`));
|
|
2510
|
+
if (cmd.keyPattern) console.log(c.dim(` pattern: ${cmd.keyPattern}`));
|
|
2511
|
+
if (cmd.tag) console.log(c.dim(` tag: ${cmd.tag}`));
|
|
2512
|
+
});
|
|
2513
|
+
hook.command("list").alias("ls").description("List all registered hooks").action(() => {
|
|
2514
|
+
const hooks = listHooks();
|
|
2515
|
+
if (hooks.length === 0) {
|
|
2516
|
+
console.log(c.dim("No hooks registered"));
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
console.log(c.bold(`
|
|
2520
|
+
${SYMBOLS.zap} Registered hooks (${hooks.length})
|
|
2521
|
+
`));
|
|
2522
|
+
for (const h of hooks) {
|
|
2523
|
+
const status = h.enabled ? c.green("on") : c.red("off");
|
|
2524
|
+
const matchParts = [];
|
|
2525
|
+
if (h.match.key) matchParts.push(`key=${h.match.key}`);
|
|
2526
|
+
if (h.match.keyPattern) matchParts.push(`pattern=${h.match.keyPattern}`);
|
|
2527
|
+
if (h.match.tag) matchParts.push(`tag=${h.match.tag}`);
|
|
2528
|
+
if (h.match.scope) matchParts.push(`scope=${h.match.scope}`);
|
|
2529
|
+
if (h.match.action?.length) matchParts.push(`actions=${h.match.action.join(",")}`);
|
|
2530
|
+
const target = h.type === "shell" ? h.command : h.type === "http" ? h.url : h.signal ? `${h.signal.target} (${h.signal.signal ?? "SIGHUP"})` : "?";
|
|
2531
|
+
console.log(` ${c.bold(h.id)} [${status}] ${c.cyan(h.type)} ${c.dim(matchParts.join(" "))}`);
|
|
2532
|
+
console.log(` ${c.dim("\u2192")} ${target}${h.description ? ` ${c.dim(`\u2014 ${h.description}`)}` : ""}`);
|
|
2533
|
+
}
|
|
2534
|
+
console.log();
|
|
2535
|
+
});
|
|
2536
|
+
hook.command("remove <id>").alias("rm").description("Remove a hook by ID").action((id) => {
|
|
2537
|
+
if (removeHook(id)) {
|
|
2538
|
+
console.log(`${SYMBOLS.check} ${c.green("removed")} hook ${c.bold(id)}`);
|
|
2539
|
+
} else {
|
|
2540
|
+
console.error(c.red(`${SYMBOLS.cross} Hook "${id}" not found`));
|
|
2541
|
+
process.exit(1);
|
|
2542
|
+
}
|
|
2543
|
+
});
|
|
2544
|
+
hook.command("enable <id>").description("Enable a hook").action((id) => {
|
|
2545
|
+
if (enableHook(id)) {
|
|
2546
|
+
console.log(`${SYMBOLS.check} ${c.green("enabled")} hook ${c.bold(id)}`);
|
|
2547
|
+
} else {
|
|
2548
|
+
console.error(c.red(`${SYMBOLS.cross} Hook "${id}" not found`));
|
|
2549
|
+
process.exit(1);
|
|
2550
|
+
}
|
|
2551
|
+
});
|
|
2552
|
+
hook.command("disable <id>").description("Disable a hook").action((id) => {
|
|
2553
|
+
if (disableHook(id)) {
|
|
2554
|
+
console.log(`${SYMBOLS.check} ${c.yellow("disabled")} hook ${c.bold(id)}`);
|
|
2555
|
+
} else {
|
|
2556
|
+
console.error(c.red(`${SYMBOLS.cross} Hook "${id}" not found`));
|
|
2557
|
+
process.exit(1);
|
|
2558
|
+
}
|
|
2559
|
+
});
|
|
2560
|
+
hook.command("test <id>").description("Dry-run a hook with a mock payload").action(async (id) => {
|
|
2561
|
+
const hooks = listHooks();
|
|
2562
|
+
const h = hooks.find((hook2) => hook2.id === id);
|
|
2563
|
+
if (!h) {
|
|
2564
|
+
console.error(c.red(`${SYMBOLS.cross} Hook "${id}" not found`));
|
|
2565
|
+
process.exit(1);
|
|
2566
|
+
}
|
|
2567
|
+
console.log(c.dim(`Testing hook ${id} (${h.type})...
|
|
2568
|
+
`));
|
|
2569
|
+
const payload = {
|
|
2570
|
+
action: "write",
|
|
2571
|
+
key: h.match.key ?? "TEST_KEY",
|
|
2572
|
+
scope: h.match.scope ?? "global",
|
|
2573
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2574
|
+
source: "cli"
|
|
2575
|
+
};
|
|
2576
|
+
const results = await fireHooks(payload);
|
|
2577
|
+
const result = results.find((r) => r.hookId === id);
|
|
2578
|
+
if (result) {
|
|
2579
|
+
const icon = result.success ? c.green(SYMBOLS.check) : c.red(SYMBOLS.cross);
|
|
2580
|
+
console.log(` ${icon} ${result.message}`);
|
|
2581
|
+
} else {
|
|
2582
|
+
console.log(c.yellow(` ${SYMBOLS.warning} Hook did not match the test payload`));
|
|
2583
|
+
}
|
|
2584
|
+
console.log();
|
|
2585
|
+
});
|
|
2586
|
+
program2.command("env:generate").description("Generate a .env file from the project manifest (.q-ring.json)").option("--project-path <path>", "Project path (defaults to cwd)").option("-o, --output <file>", "Output file path (defaults to stdout)").option("-e, --env <env>", "Force environment for superposition collapse").action((cmd) => {
|
|
2587
|
+
const projectPath = cmd.projectPath ?? process.cwd();
|
|
2588
|
+
const config = readProjectConfig(projectPath);
|
|
2589
|
+
if (!config?.secrets || Object.keys(config.secrets).length === 0) {
|
|
2590
|
+
console.error(
|
|
2591
|
+
c.red(`${SYMBOLS.cross} No secrets manifest found in .q-ring.json`)
|
|
2592
|
+
);
|
|
2593
|
+
process.exit(1);
|
|
2594
|
+
}
|
|
2595
|
+
const opts = buildOpts(cmd);
|
|
2596
|
+
const lines = [];
|
|
2597
|
+
const warnings = [];
|
|
2598
|
+
for (const [key, manifest] of Object.entries(config.secrets)) {
|
|
2599
|
+
const value = getSecret(key, { ...opts, projectPath, source: "cli" });
|
|
2600
|
+
if (value === null) {
|
|
2601
|
+
if (manifest.required !== false) {
|
|
2602
|
+
warnings.push(`MISSING (required): ${key}`);
|
|
2603
|
+
}
|
|
2604
|
+
lines.push(`# ${key}= ${manifest.description ? `# ${manifest.description}` : ""}`);
|
|
2605
|
+
continue;
|
|
2606
|
+
}
|
|
2607
|
+
const result = getEnvelope(key, { projectPath, source: "cli" });
|
|
2608
|
+
if (result) {
|
|
2609
|
+
const decay = checkDecay(result.envelope);
|
|
2610
|
+
if (decay.isExpired) {
|
|
2611
|
+
warnings.push(`EXPIRED: ${key}`);
|
|
2612
|
+
} else if (decay.isStale) {
|
|
2613
|
+
warnings.push(`STALE (${decay.lifetimePercent}%): ${key}`);
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
2617
|
+
lines.push(`${key}="${escaped}"`);
|
|
2618
|
+
}
|
|
2619
|
+
const output = lines.join("\n") + "\n";
|
|
2620
|
+
if (cmd.output) {
|
|
2621
|
+
writeFileSync4(cmd.output, output);
|
|
2622
|
+
console.log(
|
|
2623
|
+
`${SYMBOLS.check} ${c.green("generated")} ${c.bold(cmd.output)} (${Object.keys(config.secrets).length} keys)`
|
|
2624
|
+
);
|
|
2625
|
+
} else {
|
|
2626
|
+
process.stdout.write(output);
|
|
2627
|
+
}
|
|
2628
|
+
if (warnings.length > 0 && process.stderr.isTTY) {
|
|
2629
|
+
console.error();
|
|
2630
|
+
for (const w of warnings) {
|
|
2631
|
+
console.error(` ${c.yellow(SYMBOLS.warning)} ${w}`);
|
|
2632
|
+
}
|
|
2633
|
+
console.error();
|
|
2634
|
+
}
|
|
2635
|
+
});
|
|
887
2636
|
program2.command("status").description("Launch the quantum status dashboard in your browser").option("--port <port>", "Port to serve on", "9876").option("--no-open", "Don't auto-open the browser").action(async (cmd) => {
|
|
888
|
-
const { startDashboardServer } = await import("./dashboard-
|
|
2637
|
+
const { startDashboardServer } = await import("./dashboard-JT5ZNLT5.js");
|
|
889
2638
|
const { exec } = await import("child_process");
|
|
890
2639
|
const { platform } = await import("os");
|
|
891
2640
|
const port = Number(cmd.port);
|
|
@@ -937,6 +2686,96 @@ ${c.dim(" dashboard stopped")}`);
|
|
|
937
2686
|
verbose: cmd.verbose
|
|
938
2687
|
});
|
|
939
2688
|
});
|
|
2689
|
+
program2.command("rotate <key>").description("Attempt issuer-native rotation of a secret via its provider").option("-g, --global", "Global scope").option("-p, --project", "Project scope").option("--project-path <path>", "Explicit project path").option("--provider <name>", "Force a specific provider").action(async (key, cmd) => {
|
|
2690
|
+
const opts = buildOpts(cmd);
|
|
2691
|
+
const value = getSecret(key, opts);
|
|
2692
|
+
if (!value) {
|
|
2693
|
+
console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
|
|
2694
|
+
process.exitCode = 1;
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
const result = await rotateWithProvider(value, cmd.provider);
|
|
2698
|
+
if (result.rotated && result.newValue) {
|
|
2699
|
+
setSecret(key, result.newValue, { ...opts, scope: opts.scope ?? "global" });
|
|
2700
|
+
console.log(`${SYMBOLS.check} ${c.green("Rotated")} ${c.bold(key)} via ${result.provider}`);
|
|
2701
|
+
console.log(c.dim(` ${result.message}`));
|
|
2702
|
+
} else {
|
|
2703
|
+
console.log(c.yellow(`${SYMBOLS.warning} ${result.message}`));
|
|
2704
|
+
}
|
|
2705
|
+
});
|
|
2706
|
+
program2.command("ci:validate").description("CI-oriented batch validation of all secrets (exit code 1 on failure)").option("-g, --global", "Global scope").option("-p, --project", "Project scope").option("--project-path <path>", "Explicit project path").option("--json", "Output as JSON").action(async (cmd) => {
|
|
2707
|
+
const opts = buildOpts(cmd);
|
|
2708
|
+
const entries = listSecrets(opts);
|
|
2709
|
+
const secrets = entries.map((e) => {
|
|
2710
|
+
const val = getSecret(e.key, { ...opts, scope: e.scope, silent: true });
|
|
2711
|
+
if (!val) return null;
|
|
2712
|
+
const provider = e.envelope?.meta.provider;
|
|
2713
|
+
const validationUrl = e.envelope?.meta.validationUrl;
|
|
2714
|
+
return { key: e.key, value: val, provider, validationUrl };
|
|
2715
|
+
}).filter((s) => s !== null);
|
|
2716
|
+
if (secrets.length === 0) {
|
|
2717
|
+
console.log(c.dim("No secrets to validate"));
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
const report = await ciValidateBatch(secrets);
|
|
2721
|
+
if (cmd.json) {
|
|
2722
|
+
console.log(JSON.stringify(report, null, 2));
|
|
2723
|
+
} else {
|
|
2724
|
+
console.log(c.bold(`
|
|
2725
|
+
CI Secret Validation: ${report.results.length} secrets
|
|
2726
|
+
`));
|
|
2727
|
+
for (const r of report.results) {
|
|
2728
|
+
const icon = r.validation.valid ? SYMBOLS.check : SYMBOLS.cross;
|
|
2729
|
+
const color = r.validation.valid ? c.green : c.red;
|
|
2730
|
+
console.log(` ${icon} ${color(r.key)} [${r.validation.provider}] ${r.validation.message}`);
|
|
2731
|
+
if (r.requiresRotation) console.log(c.dim(` \u2192 rotation required`));
|
|
2732
|
+
}
|
|
2733
|
+
console.log();
|
|
2734
|
+
if (!report.allValid) {
|
|
2735
|
+
console.log(c.red(` ${report.failCount} secret(s) failed validation`));
|
|
2736
|
+
process.exitCode = 1;
|
|
2737
|
+
} else {
|
|
2738
|
+
console.log(c.green(` All secrets valid`));
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
});
|
|
2742
|
+
program2.command("policy").description("Show project governance policy summary").option("--json", "Output as JSON").action((cmd) => {
|
|
2743
|
+
const summary = getPolicySummary();
|
|
2744
|
+
if (cmd.json) {
|
|
2745
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
console.log(c.bold("\n\u2696 Governance Policy\n"));
|
|
2749
|
+
if (!summary.hasMcpPolicy && !summary.hasExecPolicy && !summary.hasSecretPolicy) {
|
|
2750
|
+
console.log(c.dim(" No policy configured in .q-ring.json"));
|
|
2751
|
+
console.log(c.dim(' Add a "policy" section to enable governance controls.\n'));
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
if (summary.hasMcpPolicy) {
|
|
2755
|
+
console.log(c.cyan(" MCP Policy:"));
|
|
2756
|
+
const m = summary.details.mcp;
|
|
2757
|
+
if (m.allowTools) console.log(c.green(` Allow tools: ${m.allowTools.join(", ")}`));
|
|
2758
|
+
if (m.denyTools) console.log(c.red(` Deny tools: ${m.denyTools.join(", ")}`));
|
|
2759
|
+
if (m.readableKeys) console.log(c.green(` Readable keys: ${m.readableKeys.join(", ")}`));
|
|
2760
|
+
if (m.deniedKeys) console.log(c.red(` Denied keys: ${m.deniedKeys.join(", ")}`));
|
|
2761
|
+
if (m.deniedTags) console.log(c.red(` Denied tags: ${m.deniedTags.join(", ")}`));
|
|
2762
|
+
}
|
|
2763
|
+
if (summary.hasExecPolicy) {
|
|
2764
|
+
console.log(c.cyan(" Exec Policy:"));
|
|
2765
|
+
const e = summary.details.exec;
|
|
2766
|
+
if (e.allowCommands) console.log(c.green(` Allow commands: ${e.allowCommands.join(", ")}`));
|
|
2767
|
+
if (e.denyCommands) console.log(c.red(` Deny commands: ${e.denyCommands.join(", ")}`));
|
|
2768
|
+
if (e.maxRuntimeSeconds) console.log(` Max runtime: ${e.maxRuntimeSeconds}s`);
|
|
2769
|
+
if (e.allowNetwork !== void 0) console.log(` Allow network: ${e.allowNetwork}`);
|
|
2770
|
+
}
|
|
2771
|
+
if (summary.hasSecretPolicy) {
|
|
2772
|
+
console.log(c.cyan(" Secret Lifecycle Policy:"));
|
|
2773
|
+
const s = summary.details.secrets;
|
|
2774
|
+
if (s.requireApprovalForTags) console.log(` Require approval for tags: ${s.requireApprovalForTags.join(", ")}`);
|
|
2775
|
+
if (s.maxTtlSeconds) console.log(` Max TTL: ${s.maxTtlSeconds}s`);
|
|
2776
|
+
}
|
|
2777
|
+
console.log();
|
|
2778
|
+
});
|
|
940
2779
|
return program2;
|
|
941
2780
|
}
|
|
942
2781
|
|