@i4ctime/q-ring 0.4.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +341 -10
- package/dist/{chunk-IGNU622R.js → chunk-5JBU7TWN.js} +715 -124
- package/dist/chunk-5JBU7TWN.js.map +1 -0
- package/dist/chunk-WG4ZKN7Q.js +1632 -0
- package/dist/chunk-WG4ZKN7Q.js.map +1 -0
- package/dist/{dashboard-32PCZF7D.js → dashboard-JT5ZNLT5.js} +41 -16
- package/dist/dashboard-JT5ZNLT5.js.map +1 -0
- package/dist/{dashboard-HVIQO6NT.js → dashboard-Q5OQRQCX.js} +41 -16
- package/dist/dashboard-Q5OQRQCX.js.map +1 -0
- package/dist/index.js +1213 -39
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +1066 -48
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-6IQ5SFLI.js +0 -967
- package/dist/chunk-6IQ5SFLI.js.map +0 -1
- package/dist/chunk-IGNU622R.js.map +0 -1
- package/dist/dashboard-32PCZF7D.js.map +0 -1
- package/dist/dashboard-HVIQO6NT.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
checkDecay,
|
|
4
|
+
checkExecPolicy,
|
|
4
5
|
collapseEnvironment,
|
|
5
6
|
deleteSecret,
|
|
6
7
|
detectAnomalies,
|
|
@@ -8,24 +9,33 @@ import {
|
|
|
8
9
|
disentangleSecrets,
|
|
9
10
|
enableHook,
|
|
10
11
|
entangleSecrets,
|
|
12
|
+
exportAudit,
|
|
11
13
|
exportSecrets,
|
|
12
14
|
fireHooks,
|
|
13
15
|
getEnvelope,
|
|
16
|
+
getExecMaxRuntime,
|
|
17
|
+
getPolicySummary,
|
|
14
18
|
getSecret,
|
|
19
|
+
grantApproval,
|
|
15
20
|
hasSecret,
|
|
21
|
+
httpRequest_,
|
|
22
|
+
listApprovals,
|
|
16
23
|
listHooks,
|
|
17
24
|
listSecrets,
|
|
18
25
|
logAudit,
|
|
19
26
|
queryAudit,
|
|
20
27
|
readProjectConfig,
|
|
21
28
|
registerHook,
|
|
29
|
+
registry,
|
|
22
30
|
removeHook,
|
|
31
|
+
revokeApproval,
|
|
23
32
|
setSecret,
|
|
24
33
|
tunnelCreate,
|
|
25
34
|
tunnelDestroy,
|
|
26
35
|
tunnelList,
|
|
27
|
-
tunnelRead
|
|
28
|
-
|
|
36
|
+
tunnelRead,
|
|
37
|
+
verifyAuditChain
|
|
38
|
+
} from "./chunk-WG4ZKN7Q.js";
|
|
29
39
|
|
|
30
40
|
// src/cli/commands.ts
|
|
31
41
|
import { Command } from "commander";
|
|
@@ -432,33 +442,443 @@ function importDotenv(filePathOrContent, options = {}) {
|
|
|
432
442
|
return result;
|
|
433
443
|
}
|
|
434
444
|
|
|
435
|
-
// src/core/
|
|
436
|
-
import {
|
|
437
|
-
import {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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);
|
|
452
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))
|
|
453
558
|
);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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);
|
|
458
640
|
});
|
|
459
|
-
req.end();
|
|
460
641
|
});
|
|
461
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
|
+
}
|
|
462
882
|
var ProviderRegistry = class {
|
|
463
883
|
providers = /* @__PURE__ */ new Map();
|
|
464
884
|
register(provider) {
|
|
@@ -605,14 +1025,14 @@ var httpProvider = {
|
|
|
605
1025
|
}
|
|
606
1026
|
}
|
|
607
1027
|
};
|
|
608
|
-
var
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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);
|
|
614
1034
|
async function validateSecret(value, opts) {
|
|
615
|
-
const provider = opts?.provider ?
|
|
1035
|
+
const provider = opts?.provider ? registry2.get(opts.provider) : registry2.detectProvider(value);
|
|
616
1036
|
if (!provider) {
|
|
617
1037
|
return {
|
|
618
1038
|
valid: false,
|
|
@@ -627,9 +1047,271 @@ async function validateSecret(value, opts) {
|
|
|
627
1047
|
}
|
|
628
1048
|
return provider.validate(value);
|
|
629
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
|
+
}
|
|
630
1312
|
|
|
631
1313
|
// src/cli/commands.ts
|
|
632
|
-
import { writeFileSync } from "fs";
|
|
1314
|
+
import { writeFileSync as writeFileSync4, readFileSync as readFileSync6, existsSync as existsSync4 } from "fs";
|
|
633
1315
|
|
|
634
1316
|
// src/utils/prompt.ts
|
|
635
1317
|
import { createInterface } from "readline";
|
|
@@ -683,6 +1365,8 @@ function buildOpts(cmd) {
|
|
|
683
1365
|
let scope;
|
|
684
1366
|
if (cmd.global) scope = "global";
|
|
685
1367
|
else if (cmd.project) scope = "project";
|
|
1368
|
+
else if (cmd.team) scope = "team";
|
|
1369
|
+
else if (cmd.org) scope = "org";
|
|
686
1370
|
const projectPath = cmd.projectPath ?? (cmd.project ? process.cwd() : void 0);
|
|
687
1371
|
if (scope === "project" && !projectPath) {
|
|
688
1372
|
throw new Error("Project path is required for project scope");
|
|
@@ -690,6 +1374,8 @@ function buildOpts(cmd) {
|
|
|
690
1374
|
return {
|
|
691
1375
|
scope,
|
|
692
1376
|
projectPath: projectPath ?? process.cwd(),
|
|
1377
|
+
teamId: cmd.team,
|
|
1378
|
+
orgId: cmd.org,
|
|
693
1379
|
env: cmd.env,
|
|
694
1380
|
source: "cli"
|
|
695
1381
|
};
|
|
@@ -698,7 +1384,7 @@ function createProgram() {
|
|
|
698
1384
|
const program2 = new Command().name("qring").description(
|
|
699
1385
|
`${c.bold("q-ring")} ${c.dim("\u2014 quantum keyring for AI coding tools")}`
|
|
700
1386
|
).version("0.4.0");
|
|
701
|
-
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").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-)").action(async (key, value, cmd) => {
|
|
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) => {
|
|
702
1388
|
const opts = buildOpts(cmd);
|
|
703
1389
|
if (!value) {
|
|
704
1390
|
value = await promptSecret(`${SYMBOLS.key} Enter value for ${c.bold(key)}: `);
|
|
@@ -714,7 +1400,9 @@ function createProgram() {
|
|
|
714
1400
|
description: cmd.description,
|
|
715
1401
|
tags: cmd.tags?.split(",").map((t) => t.trim()),
|
|
716
1402
|
rotationFormat: cmd.rotationFormat,
|
|
717
|
-
rotationPrefix: cmd.rotationPrefix
|
|
1403
|
+
rotationPrefix: cmd.rotationPrefix,
|
|
1404
|
+
requiresApproval: cmd.requiresApproval,
|
|
1405
|
+
jitProvider: cmd.jitProvider
|
|
718
1406
|
};
|
|
719
1407
|
if (cmd.env) {
|
|
720
1408
|
const existing = getEnvelope(key, opts);
|
|
@@ -739,7 +1427,7 @@ function createProgram() {
|
|
|
739
1427
|
);
|
|
740
1428
|
}
|
|
741
1429
|
});
|
|
742
|
-
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) => {
|
|
743
1431
|
const opts = buildOpts(cmd);
|
|
744
1432
|
const value = getSecret(key, opts);
|
|
745
1433
|
if (value === null) {
|
|
@@ -758,7 +1446,7 @@ function createProgram() {
|
|
|
758
1446
|
process.exit(1);
|
|
759
1447
|
}
|
|
760
1448
|
});
|
|
761
|
-
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").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) => {
|
|
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) => {
|
|
762
1450
|
const opts = buildOpts(cmd);
|
|
763
1451
|
let entries = listSecrets(opts);
|
|
764
1452
|
if (cmd.tag) {
|
|
@@ -1020,7 +1708,7 @@ function createProgram() {
|
|
|
1020
1708
|
console.log(c.bold(`
|
|
1021
1709
|
${SYMBOLS.shield} Available validation providers
|
|
1022
1710
|
`));
|
|
1023
|
-
for (const p of
|
|
1711
|
+
for (const p of registry2.listProviders()) {
|
|
1024
1712
|
const prefixes = p.prefixes?.length ? c.dim(` (${p.prefixes.join(", ")})`) : "";
|
|
1025
1713
|
console.log(` ${c.cyan(p.name.padEnd(10))} ${p.description}${prefixes}`);
|
|
1026
1714
|
}
|
|
@@ -1087,6 +1775,372 @@ function createProgram() {
|
|
|
1087
1775
|
}
|
|
1088
1776
|
console.log();
|
|
1089
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
|
+
});
|
|
1090
2144
|
program2.command("env").description("Show detected environment (wavefunction collapse context)").option("--project-path <path>", "Project path for detection").action((cmd) => {
|
|
1091
2145
|
const result = collapseEnvironment({
|
|
1092
2146
|
projectPath: cmd.projectPath ?? process.cwd()
|
|
@@ -1343,6 +2397,36 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
|
|
|
1343
2397
|
}
|
|
1344
2398
|
console.log();
|
|
1345
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
|
+
});
|
|
1346
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) => {
|
|
1347
2431
|
const opts = buildOpts(cmd);
|
|
1348
2432
|
const entries = listSecrets(opts);
|
|
@@ -1534,7 +2618,7 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
|
|
|
1534
2618
|
}
|
|
1535
2619
|
const output = lines.join("\n") + "\n";
|
|
1536
2620
|
if (cmd.output) {
|
|
1537
|
-
|
|
2621
|
+
writeFileSync4(cmd.output, output);
|
|
1538
2622
|
console.log(
|
|
1539
2623
|
`${SYMBOLS.check} ${c.green("generated")} ${c.bold(cmd.output)} (${Object.keys(config.secrets).length} keys)`
|
|
1540
2624
|
);
|
|
@@ -1550,7 +2634,7 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
|
|
|
1550
2634
|
}
|
|
1551
2635
|
});
|
|
1552
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) => {
|
|
1553
|
-
const { startDashboardServer } = await import("./dashboard-
|
|
2637
|
+
const { startDashboardServer } = await import("./dashboard-JT5ZNLT5.js");
|
|
1554
2638
|
const { exec } = await import("child_process");
|
|
1555
2639
|
const { platform } = await import("os");
|
|
1556
2640
|
const port = Number(cmd.port);
|
|
@@ -1602,6 +2686,96 @@ ${c.dim(" dashboard stopped")}`);
|
|
|
1602
2686
|
verbose: cmd.verbose
|
|
1603
2687
|
});
|
|
1604
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
|
+
});
|
|
1605
2779
|
return program2;
|
|
1606
2780
|
}
|
|
1607
2781
|
|