@i4ctime/q-ring 0.9.8 → 0.11.0
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 +24 -3
- package/dist/{chunk-JV4JFWV6.js → chunk-A4RZVP3P.js} +669 -223
- package/dist/chunk-A4RZVP3P.js.map +1 -0
- package/dist/{chunk-ZV7LRKBA.js → chunk-MQM4DLPI.js} +693 -241
- package/dist/chunk-MQM4DLPI.js.map +1 -0
- package/dist/dashboard-FSCJDLJX.js +1313 -0
- package/dist/dashboard-FSCJDLJX.js.map +1 -0
- package/dist/dashboard-OXUD62RV.js +1313 -0
- package/dist/dashboard-OXUD62RV.js.map +1 -0
- package/dist/index.js +2455 -1993
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +1798 -1661
- package/dist/mcp.js.map +1 -1
- package/package.json +15 -3
- package/dist/chunk-JV4JFWV6.js.map +0 -1
- package/dist/chunk-ZV7LRKBA.js.map +0 -1
- package/dist/dashboard-4H474M2N.js +0 -557
- package/dist/dashboard-4H474M2N.js.map +0 -1
- package/dist/dashboard-5L4ILERJ.js +0 -557
- package/dist/dashboard-5L4ILERJ.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
PACKAGE_VERSION,
|
|
3
4
|
checkDecay,
|
|
4
5
|
checkExecPolicy,
|
|
5
6
|
checkSSRF,
|
|
7
|
+
clearMemory,
|
|
6
8
|
collapseEnvironment,
|
|
7
9
|
deleteSecret,
|
|
8
10
|
detectAnomalies,
|
|
@@ -13,21 +15,25 @@ import {
|
|
|
13
15
|
exportAudit,
|
|
14
16
|
exportSecrets,
|
|
15
17
|
fireHooks,
|
|
18
|
+
forget,
|
|
16
19
|
getEnvelope,
|
|
17
20
|
getExecMaxRuntime,
|
|
18
21
|
getPolicySummary,
|
|
19
22
|
getSecret,
|
|
20
23
|
grantApproval,
|
|
21
24
|
hasSecret,
|
|
22
|
-
|
|
25
|
+
httpRequest,
|
|
23
26
|
listApprovals,
|
|
24
27
|
listHooks,
|
|
28
|
+
listMemory,
|
|
25
29
|
listSecrets,
|
|
26
30
|
logAudit,
|
|
27
31
|
queryAudit,
|
|
28
32
|
readProjectConfig,
|
|
33
|
+
recall,
|
|
29
34
|
registerHook,
|
|
30
35
|
registry,
|
|
36
|
+
remember,
|
|
31
37
|
removeHook,
|
|
32
38
|
revokeApproval,
|
|
33
39
|
setSecret,
|
|
@@ -36,93 +42,10 @@ import {
|
|
|
36
42
|
tunnelList,
|
|
37
43
|
tunnelRead,
|
|
38
44
|
verifyAuditChain
|
|
39
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-A4RZVP3P.js";
|
|
40
46
|
|
|
41
47
|
// src/cli/commands.ts
|
|
42
|
-
import { Command } from "commander";
|
|
43
|
-
|
|
44
|
-
// src/core/noise.ts
|
|
45
|
-
import { randomBytes, randomInt } from "crypto";
|
|
46
|
-
var ALPHA_NUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
47
|
-
var PASSWORD_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,.<>?";
|
|
48
|
-
function randomString(charset, length) {
|
|
49
|
-
let result = "";
|
|
50
|
-
for (let i = 0; i < length; i++) {
|
|
51
|
-
result += charset[randomInt(charset.length)];
|
|
52
|
-
}
|
|
53
|
-
return result;
|
|
54
|
-
}
|
|
55
|
-
function generateSecret(opts = {}) {
|
|
56
|
-
const format = opts.format ?? "api-key";
|
|
57
|
-
switch (format) {
|
|
58
|
-
case "hex": {
|
|
59
|
-
const len = opts.length ?? 32;
|
|
60
|
-
return randomBytes(len).toString("hex");
|
|
61
|
-
}
|
|
62
|
-
case "base64": {
|
|
63
|
-
const len = opts.length ?? 32;
|
|
64
|
-
return randomBytes(len).toString("base64url");
|
|
65
|
-
}
|
|
66
|
-
case "alphanumeric": {
|
|
67
|
-
const len = opts.length ?? 32;
|
|
68
|
-
return randomString(ALPHA_NUM, len);
|
|
69
|
-
}
|
|
70
|
-
case "uuid": {
|
|
71
|
-
const bytes = randomBytes(16);
|
|
72
|
-
bytes[6] = bytes[6] & 15 | 64;
|
|
73
|
-
bytes[8] = bytes[8] & 63 | 128;
|
|
74
|
-
const hex = bytes.toString("hex");
|
|
75
|
-
return [
|
|
76
|
-
hex.slice(0, 8),
|
|
77
|
-
hex.slice(8, 12),
|
|
78
|
-
hex.slice(12, 16),
|
|
79
|
-
hex.slice(16, 20),
|
|
80
|
-
hex.slice(20, 32)
|
|
81
|
-
].join("-");
|
|
82
|
-
}
|
|
83
|
-
case "api-key": {
|
|
84
|
-
const prefix = opts.prefix ?? "qr_";
|
|
85
|
-
const len = opts.length ?? 48;
|
|
86
|
-
return prefix + randomString(ALPHA_NUM, len);
|
|
87
|
-
}
|
|
88
|
-
case "token": {
|
|
89
|
-
const prefix = opts.prefix ?? "";
|
|
90
|
-
const len = opts.length ?? 64;
|
|
91
|
-
return prefix + randomBytes(len).toString("base64url");
|
|
92
|
-
}
|
|
93
|
-
case "password": {
|
|
94
|
-
const len = opts.length ?? 24;
|
|
95
|
-
let pw = randomString(PASSWORD_CHARS, len);
|
|
96
|
-
const hasUpper = /[A-Z]/.test(pw);
|
|
97
|
-
const hasLower = /[a-z]/.test(pw);
|
|
98
|
-
const hasDigit = /[0-9]/.test(pw);
|
|
99
|
-
const hasSpecial = /[^A-Za-z0-9]/.test(pw);
|
|
100
|
-
if (!hasUpper) pw = replaceAt(pw, randomInt(len), randomString("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1));
|
|
101
|
-
if (!hasLower) pw = replaceAt(pw, randomInt(len), randomString("abcdefghijklmnopqrstuvwxyz", 1));
|
|
102
|
-
if (!hasDigit) pw = replaceAt(pw, randomInt(len), randomString("0123456789", 1));
|
|
103
|
-
if (!hasSpecial) pw = replaceAt(pw, randomInt(len), randomString("!@#$%^&*()-_=+", 1));
|
|
104
|
-
return pw;
|
|
105
|
-
}
|
|
106
|
-
default:
|
|
107
|
-
return randomBytes(32).toString("hex");
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
function replaceAt(str, index, char) {
|
|
111
|
-
return str.slice(0, index) + char + str.slice(index + 1);
|
|
112
|
-
}
|
|
113
|
-
function estimateEntropy(secret) {
|
|
114
|
-
const charsets = [
|
|
115
|
-
{ regex: /[a-z]/, size: 26 },
|
|
116
|
-
{ regex: /[A-Z]/, size: 26 },
|
|
117
|
-
{ regex: /[0-9]/, size: 10 },
|
|
118
|
-
{ regex: /[^A-Za-z0-9]/, size: 32 }
|
|
119
|
-
];
|
|
120
|
-
let poolSize = 0;
|
|
121
|
-
for (const { regex, size } of charsets) {
|
|
122
|
-
if (regex.test(secret)) poolSize += size;
|
|
123
|
-
}
|
|
124
|
-
return poolSize > 0 ? Math.floor(Math.log2(poolSize) * secret.length) : 0;
|
|
125
|
-
}
|
|
48
|
+
import { Command, Help } from "commander";
|
|
126
49
|
|
|
127
50
|
// src/utils/colors.ts
|
|
128
51
|
var enabled = process.stdout.isTTY !== false && !process.env.NO_COLOR;
|
|
@@ -191,783 +114,600 @@ var SYMBOLS = {
|
|
|
191
114
|
sparkle: enabled ? "\u2728" : "[*]"
|
|
192
115
|
};
|
|
193
116
|
|
|
194
|
-
// src/core/
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
healthy: 0,
|
|
209
|
-
stale: 0,
|
|
210
|
-
expired: 0,
|
|
211
|
-
anomalies: 0,
|
|
212
|
-
rotated: [],
|
|
213
|
-
warnings: []
|
|
214
|
-
};
|
|
215
|
-
const globalEntries = listSecrets({ scope: "global", source: "agent" });
|
|
216
|
-
const projectEntries = cfg.projectPaths.flatMap(
|
|
217
|
-
(pp) => listSecrets({ scope: "project", projectPath: pp, source: "agent" })
|
|
218
|
-
);
|
|
219
|
-
const allEntries = [...globalEntries, ...projectEntries];
|
|
220
|
-
report.totalSecrets = allEntries.length;
|
|
221
|
-
for (const entry of allEntries) {
|
|
222
|
-
if (!entry.envelope) continue;
|
|
223
|
-
const decay = checkDecay(entry.envelope);
|
|
224
|
-
if (decay.isExpired) {
|
|
225
|
-
report.expired++;
|
|
226
|
-
report.warnings.push(
|
|
227
|
-
`EXPIRED: ${entry.key} [${entry.scope}] \u2014 expired ${decay.timeRemaining}`
|
|
228
|
-
);
|
|
229
|
-
if (cfg.autoRotate) {
|
|
230
|
-
const fmt = entry.envelope?.meta.rotationFormat ?? "api-key";
|
|
231
|
-
const prefix = entry.envelope?.meta.rotationPrefix;
|
|
232
|
-
const newValue = generateSecret({ format: fmt, prefix });
|
|
233
|
-
setSecret(entry.key, newValue, {
|
|
234
|
-
scope: entry.scope,
|
|
235
|
-
projectPath: cfg.projectPaths[0],
|
|
236
|
-
source: "agent"
|
|
237
|
-
});
|
|
238
|
-
report.rotated.push(entry.key);
|
|
239
|
-
logAudit({
|
|
240
|
-
action: "write",
|
|
241
|
-
key: entry.key,
|
|
242
|
-
scope: entry.scope,
|
|
243
|
-
source: "agent",
|
|
244
|
-
detail: "auto-rotated by agent (expired)"
|
|
245
|
-
});
|
|
246
|
-
fireHooks({
|
|
247
|
-
action: "rotate",
|
|
248
|
-
key: entry.key,
|
|
249
|
-
scope: entry.scope,
|
|
250
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
251
|
-
source: "agent"
|
|
252
|
-
}, entry.envelope?.meta.tags).catch(() => {
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
} else if (decay.isStale) {
|
|
256
|
-
report.stale++;
|
|
257
|
-
report.warnings.push(
|
|
258
|
-
`STALE: ${entry.key} [${entry.scope}] \u2014 ${decay.lifetimePercent}% lifetime, ${decay.timeRemaining} remaining`
|
|
259
|
-
);
|
|
260
|
-
} else {
|
|
261
|
-
report.healthy++;
|
|
117
|
+
// src/core/import.ts
|
|
118
|
+
import { readFileSync } from "fs";
|
|
119
|
+
function parseDotenv(content) {
|
|
120
|
+
const result = /* @__PURE__ */ new Map();
|
|
121
|
+
const lines = content.split(/\r?\n/);
|
|
122
|
+
for (let i = 0; i < lines.length; i++) {
|
|
123
|
+
const line = lines[i].trim();
|
|
124
|
+
if (!line || line.startsWith("#")) continue;
|
|
125
|
+
const eqIdx = line.indexOf("=");
|
|
126
|
+
if (eqIdx === -1) continue;
|
|
127
|
+
const key = line.slice(0, eqIdx).trim();
|
|
128
|
+
let value = line.slice(eqIdx + 1).trim();
|
|
129
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
130
|
+
value = value.slice(1, -1);
|
|
262
131
|
}
|
|
132
|
+
const escapeMap = {
|
|
133
|
+
n: "\n",
|
|
134
|
+
r: "\r",
|
|
135
|
+
t: " ",
|
|
136
|
+
"\\": "\\",
|
|
137
|
+
'"': '"'
|
|
138
|
+
};
|
|
139
|
+
value = value.replace(/\\([nrt"\\])/g, (_, ch) => escapeMap[ch] ?? ch);
|
|
140
|
+
if (value.includes("#") && !line.includes('"') && !line.includes("'")) {
|
|
141
|
+
value = value.split("#")[0].trim();
|
|
142
|
+
}
|
|
143
|
+
if (key) result.set(key, value);
|
|
263
144
|
}
|
|
264
|
-
|
|
265
|
-
report.anomalies = anomalies.length;
|
|
266
|
-
for (const a of anomalies) {
|
|
267
|
-
report.warnings.push(`ANOMALY [${a.type}]: ${a.description}`);
|
|
268
|
-
}
|
|
269
|
-
return report;
|
|
145
|
+
return result;
|
|
270
146
|
}
|
|
271
|
-
function
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
` ${c.dim("secrets:")} ${report.totalSecrets} ${c.green(`${SYMBOLS.check} ${report.healthy}`)} ${c.yellow(`${SYMBOLS.warning} ${report.stale}`)} ${c.red(`${SYMBOLS.cross} ${report.expired}`)} ${c.dim(`anomalies: ${report.anomalies}`)}`
|
|
278
|
-
);
|
|
279
|
-
if (report.rotated.length > 0) {
|
|
280
|
-
lines.push(
|
|
281
|
-
` ${c.cyan(`${SYMBOLS.zap} auto-rotated:`)} ${report.rotated.join(", ")}`
|
|
282
|
-
);
|
|
147
|
+
function importDotenv(filePathOrContent, options = {}) {
|
|
148
|
+
let content;
|
|
149
|
+
try {
|
|
150
|
+
content = readFileSync(filePathOrContent, "utf8");
|
|
151
|
+
} catch {
|
|
152
|
+
content = filePathOrContent;
|
|
283
153
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
154
|
+
const pairs = parseDotenv(content);
|
|
155
|
+
const result = {
|
|
156
|
+
imported: [],
|
|
157
|
+
skipped: [],
|
|
158
|
+
total: pairs.size
|
|
159
|
+
};
|
|
160
|
+
for (const [key, value] of pairs) {
|
|
161
|
+
if (options.skipExisting && hasSecret(key, {
|
|
162
|
+
scope: options.scope,
|
|
163
|
+
projectPath: options.projectPath,
|
|
164
|
+
source: options.source ?? "cli"
|
|
165
|
+
})) {
|
|
166
|
+
result.skipped.push(key);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (options.dryRun) {
|
|
170
|
+
result.imported.push(key);
|
|
171
|
+
continue;
|
|
291
172
|
}
|
|
173
|
+
const setOpts = {
|
|
174
|
+
scope: options.scope ?? "global",
|
|
175
|
+
projectPath: options.projectPath ?? process.cwd(),
|
|
176
|
+
source: options.source ?? "cli"
|
|
177
|
+
};
|
|
178
|
+
setSecret(key, value, setOpts);
|
|
179
|
+
result.imported.push(key);
|
|
292
180
|
}
|
|
293
|
-
return
|
|
181
|
+
return result;
|
|
294
182
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
183
|
+
|
|
184
|
+
// src/utils/prompt.ts
|
|
185
|
+
import { createInterface } from "readline";
|
|
186
|
+
async function promptSecret(message) {
|
|
187
|
+
const rl = createInterface({
|
|
188
|
+
input: process.stdin,
|
|
189
|
+
output: process.stderr,
|
|
190
|
+
terminal: true
|
|
191
|
+
});
|
|
192
|
+
return new Promise((resolve) => {
|
|
193
|
+
process.stderr.write(message);
|
|
194
|
+
const stdin = process.stdin;
|
|
195
|
+
const wasRaw = stdin.isRaw;
|
|
196
|
+
if (stdin.isTTY && stdin.setRawMode) {
|
|
197
|
+
stdin.setRawMode(true);
|
|
198
|
+
}
|
|
199
|
+
let input = "";
|
|
200
|
+
const onData = (chunk) => {
|
|
201
|
+
const char = chunk.toString("utf8");
|
|
202
|
+
if (char === "\n" || char === "\r") {
|
|
203
|
+
stdin.removeListener("data", onData);
|
|
204
|
+
if (stdin.isTTY && stdin.setRawMode) {
|
|
205
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
206
|
+
}
|
|
207
|
+
process.stderr.write("\n");
|
|
208
|
+
rl.close();
|
|
209
|
+
resolve(input);
|
|
210
|
+
} else if (char === "") {
|
|
211
|
+
process.exit(1);
|
|
212
|
+
} else if (char === "\x7F" || char === "\b") {
|
|
213
|
+
input = input.slice(0, -1);
|
|
214
|
+
} else {
|
|
215
|
+
input += char;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
stdin.on("data", onData);
|
|
322
219
|
});
|
|
323
220
|
}
|
|
324
221
|
|
|
325
|
-
// src/
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
} from "crypto";
|
|
332
|
-
var ALGORITHM = "aes-256-gcm";
|
|
333
|
-
var KEY_LENGTH = 32;
|
|
334
|
-
var IV_LENGTH = 16;
|
|
335
|
-
var SALT_LENGTH = 32;
|
|
336
|
-
var PBKDF2_ITERATIONS = 1e5;
|
|
337
|
-
function deriveKey(passphrase, salt) {
|
|
338
|
-
return pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
|
|
222
|
+
// src/cli/helpers.ts
|
|
223
|
+
function wantsJsonOutput(program2, cmd) {
|
|
224
|
+
const c2 = cmd;
|
|
225
|
+
if (c2.json) return true;
|
|
226
|
+
const g = program2.opts();
|
|
227
|
+
return !!g.json;
|
|
339
228
|
}
|
|
340
|
-
function
|
|
341
|
-
|
|
342
|
-
secrets,
|
|
343
|
-
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
344
|
-
};
|
|
345
|
-
const plaintext = JSON.stringify(payload);
|
|
346
|
-
const salt = randomBytes2(SALT_LENGTH);
|
|
347
|
-
const iv = randomBytes2(IV_LENGTH);
|
|
348
|
-
const key = deriveKey(passphrase, salt);
|
|
349
|
-
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
350
|
-
const encrypted = Buffer.concat([
|
|
351
|
-
cipher.update(plaintext, "utf8"),
|
|
352
|
-
cipher.final()
|
|
353
|
-
]);
|
|
354
|
-
const tag = cipher.getAuthTag();
|
|
355
|
-
const bundle = {
|
|
356
|
-
v: 1,
|
|
357
|
-
data: encrypted.toString("base64"),
|
|
358
|
-
salt: salt.toString("base64"),
|
|
359
|
-
iv: iv.toString("base64"),
|
|
360
|
-
tag: tag.toString("base64"),
|
|
361
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
362
|
-
count: secrets.length
|
|
363
|
-
};
|
|
364
|
-
return Buffer.from(JSON.stringify(bundle)).toString("base64");
|
|
229
|
+
function safeStr(s) {
|
|
230
|
+
return s == null ? "" : `${s}`;
|
|
365
231
|
}
|
|
366
|
-
function
|
|
367
|
-
|
|
368
|
-
const bundle = JSON.parse(bundleJson);
|
|
369
|
-
if (bundle.v !== 1) {
|
|
370
|
-
throw new Error(`Unsupported teleport bundle version: ${bundle.v}`);
|
|
371
|
-
}
|
|
372
|
-
const salt = Buffer.from(bundle.salt, "base64");
|
|
373
|
-
const iv = Buffer.from(bundle.iv, "base64");
|
|
374
|
-
const tag = Buffer.from(bundle.tag, "base64");
|
|
375
|
-
const encrypted = Buffer.from(bundle.data, "base64");
|
|
376
|
-
const key = deriveKey(passphrase, salt);
|
|
377
|
-
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
378
|
-
decipher.setAuthTag(tag);
|
|
379
|
-
const decrypted = Buffer.concat([
|
|
380
|
-
decipher.update(encrypted),
|
|
381
|
-
decipher.final()
|
|
382
|
-
]);
|
|
383
|
-
return JSON.parse(decrypted.toString("utf8"));
|
|
232
|
+
function safeNum(n) {
|
|
233
|
+
return n == null ? 0 : Number(n);
|
|
384
234
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
import { readFileSync } from "fs";
|
|
388
|
-
function parseDotenv(content) {
|
|
389
|
-
const result = /* @__PURE__ */ new Map();
|
|
390
|
-
const lines = content.split(/\r?\n/);
|
|
391
|
-
for (let i = 0; i < lines.length; i++) {
|
|
392
|
-
const line = lines[i].trim();
|
|
393
|
-
if (!line || line.startsWith("#")) continue;
|
|
394
|
-
const eqIdx = line.indexOf("=");
|
|
395
|
-
if (eqIdx === -1) continue;
|
|
396
|
-
const key = line.slice(0, eqIdx).trim();
|
|
397
|
-
let value = line.slice(eqIdx + 1).trim();
|
|
398
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
399
|
-
value = value.slice(1, -1);
|
|
400
|
-
}
|
|
401
|
-
const escapeMap = {
|
|
402
|
-
n: "\n",
|
|
403
|
-
r: "\r",
|
|
404
|
-
t: " ",
|
|
405
|
-
"\\": "\\",
|
|
406
|
-
'"': '"'
|
|
407
|
-
};
|
|
408
|
-
value = value.replace(/\\([nrt"\\])/g, (_, ch) => escapeMap[ch] ?? ch);
|
|
409
|
-
if (value.includes("#") && !line.includes('"') && !line.includes("'")) {
|
|
410
|
-
value = value.split("#")[0].trim();
|
|
411
|
-
}
|
|
412
|
-
if (key) result.set(key, value);
|
|
413
|
-
}
|
|
414
|
-
return result;
|
|
235
|
+
function safeArr(arr) {
|
|
236
|
+
return arr ? arr.map((x) => typeof x === "string" ? safeStr(x) : x) : [];
|
|
415
237
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
238
|
+
|
|
239
|
+
// src/cli/options.ts
|
|
240
|
+
function buildOpts(cmd) {
|
|
241
|
+
let scope;
|
|
242
|
+
if (cmd.global) scope = "global";
|
|
243
|
+
else if (cmd.project) scope = "project";
|
|
244
|
+
else if (cmd.team) scope = "team";
|
|
245
|
+
else if (cmd.org) scope = "org";
|
|
246
|
+
const projectPath = cmd.projectPath ?? (cmd.project ? process.cwd() : void 0);
|
|
247
|
+
if (scope === "project" && !projectPath) {
|
|
248
|
+
throw new Error("Project path is required for project scope");
|
|
422
249
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
250
|
+
return {
|
|
251
|
+
scope,
|
|
252
|
+
projectPath: projectPath ?? process.cwd(),
|
|
253
|
+
teamId: cmd.team,
|
|
254
|
+
orgId: cmd.org,
|
|
255
|
+
env: cmd.env,
|
|
256
|
+
source: "cli"
|
|
428
257
|
};
|
|
429
|
-
for (const [key, value] of pairs) {
|
|
430
|
-
if (options.skipExisting && hasSecret(key, {
|
|
431
|
-
scope: options.scope,
|
|
432
|
-
projectPath: options.projectPath,
|
|
433
|
-
source: options.source ?? "cli"
|
|
434
|
-
})) {
|
|
435
|
-
result.skipped.push(key);
|
|
436
|
-
continue;
|
|
437
|
-
}
|
|
438
|
-
if (options.dryRun) {
|
|
439
|
-
result.imported.push(key);
|
|
440
|
-
continue;
|
|
441
|
-
}
|
|
442
|
-
const setOpts = {
|
|
443
|
-
scope: options.scope ?? "global",
|
|
444
|
-
projectPath: options.projectPath ?? process.cwd(),
|
|
445
|
-
source: options.source ?? "cli"
|
|
446
|
-
};
|
|
447
|
-
setSecret(key, value, setOpts);
|
|
448
|
-
result.imported.push(key);
|
|
449
|
-
}
|
|
450
|
-
return result;
|
|
451
258
|
}
|
|
452
259
|
|
|
453
|
-
// src/
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
allowNetwork: false,
|
|
463
|
-
stripEnvVars: ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"]
|
|
464
|
-
},
|
|
465
|
-
ci: {
|
|
466
|
-
name: "ci",
|
|
467
|
-
maxRuntimeSeconds: 300,
|
|
468
|
-
allowNetwork: true,
|
|
469
|
-
denyCommands: ["rm -rf /", "mkfs", "dd if="]
|
|
260
|
+
// src/services/list-secrets-filter.ts
|
|
261
|
+
function globKeyPatternToRegexSource(pattern) {
|
|
262
|
+
let out = "";
|
|
263
|
+
for (const c2 of pattern) {
|
|
264
|
+
if (c2 === "*") out += ".*";
|
|
265
|
+
else if (c2 === "?") out += ".";
|
|
266
|
+
else if ("\\^$+{}()|[]".includes(c2)) out += "\\" + c2;
|
|
267
|
+
else if (c2 === ".") out += "\\.";
|
|
268
|
+
else out += c2;
|
|
470
269
|
}
|
|
471
|
-
|
|
472
|
-
function getProfile(name) {
|
|
473
|
-
if (!name) return BUILTIN_PROFILES.unrestricted;
|
|
474
|
-
return BUILTIN_PROFILES[name] ?? { name };
|
|
270
|
+
return out;
|
|
475
271
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
if (
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
this.push(output);
|
|
510
|
-
callback();
|
|
511
|
-
}
|
|
512
|
-
_flush(callback) {
|
|
513
|
-
if (this.tail) {
|
|
514
|
-
let final = this.tail;
|
|
515
|
-
for (const { value, replacement } of this.patterns) {
|
|
516
|
-
final = final.split(value).join(replacement);
|
|
272
|
+
function filterSecretsByKeyGlob(entries, filter) {
|
|
273
|
+
if (!filter?.trim()) return entries;
|
|
274
|
+
const regex = new RegExp("^" + globKeyPatternToRegexSource(filter) + "$", "i");
|
|
275
|
+
return entries.filter((e) => regex.test(e.key));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/cli/commands/secrets.ts
|
|
279
|
+
function registerSecretsCommands(program2) {
|
|
280
|
+
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(
|
|
281
|
+
"-e, --env <env>",
|
|
282
|
+
"Set value for a specific environment (superposition)"
|
|
283
|
+
).option(
|
|
284
|
+
"--ttl <seconds>",
|
|
285
|
+
"Time-to-live in seconds (quantum decay)",
|
|
286
|
+
parseInt
|
|
287
|
+
).option("--expires <iso>", "Expiry timestamp (ISO 8601)").option("--description <desc>", "Human-readable description").option("--tags <tags>", "Comma-separated tags").option(
|
|
288
|
+
"--rotation-format <format>",
|
|
289
|
+
"Format for auto-rotation (api-key, password, uuid, hex, base64, alphanumeric, token)"
|
|
290
|
+
).option("--rotation-prefix <prefix>", "Prefix for auto-rotation (e.g. sk-)").option(
|
|
291
|
+
"--requires-approval",
|
|
292
|
+
"Require explicit user approval for MCP agents to read"
|
|
293
|
+
).option(
|
|
294
|
+
"--jit-provider <provider>",
|
|
295
|
+
"Use a Just-In-Time provider to dynamically generate this secret"
|
|
296
|
+
).action(async (key, value, cmd) => {
|
|
297
|
+
const opts = buildOpts(cmd);
|
|
298
|
+
if (!value) {
|
|
299
|
+
value = await promptSecret(
|
|
300
|
+
`${SYMBOLS.key} Enter value for ${c.bold(key)}: `
|
|
301
|
+
);
|
|
302
|
+
if (!value) {
|
|
303
|
+
console.error(c.red(`${SYMBOLS.cross} No value provided, aborting.`));
|
|
304
|
+
process.exit(1);
|
|
517
305
|
}
|
|
518
|
-
this.push(final);
|
|
519
306
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
307
|
+
const setOpts = {
|
|
308
|
+
...opts,
|
|
309
|
+
ttlSeconds: cmd.ttl,
|
|
310
|
+
expiresAt: cmd.expires,
|
|
311
|
+
description: cmd.description,
|
|
312
|
+
tags: cmd.tags?.split(",").map((t) => t.trim()),
|
|
313
|
+
rotationFormat: cmd.rotationFormat,
|
|
314
|
+
rotationPrefix: cmd.rotationPrefix,
|
|
315
|
+
requiresApproval: cmd.requiresApproval,
|
|
316
|
+
jitProvider: cmd.jitProvider
|
|
317
|
+
};
|
|
318
|
+
if (cmd.env) {
|
|
319
|
+
const existing = getEnvelope(key, opts);
|
|
320
|
+
const states = existing?.envelope?.states ?? {};
|
|
321
|
+
states[cmd.env] = value;
|
|
322
|
+
if (existing?.envelope?.value && !states["default"]) {
|
|
323
|
+
states["default"] = existing.envelope.value;
|
|
324
|
+
}
|
|
325
|
+
setOpts.states = states;
|
|
326
|
+
setOpts.defaultEnv = existing?.envelope?.defaultEnv ?? cmd.env;
|
|
327
|
+
setSecret(key, "", setOpts);
|
|
328
|
+
console.log(
|
|
329
|
+
`${SYMBOLS.check} ${c.green("saved")} ${c.bold(key)} ${envBadge(cmd.env)} ${c.dim(`[${scopeColor(opts.scope ?? "global")}]`)}`
|
|
330
|
+
);
|
|
331
|
+
} else {
|
|
332
|
+
setSecret(key, value, setOpts);
|
|
333
|
+
const extras = [];
|
|
334
|
+
if (cmd.ttl) extras.push(`${SYMBOLS.clock} ttl=${cmd.ttl}s`);
|
|
335
|
+
if (cmd.description) extras.push(c.dim(cmd.description));
|
|
336
|
+
console.log(
|
|
337
|
+
`${SYMBOLS.check} ${c.green("saved")} ${c.bold(key)} ${c.dim(`[${scopeColor(opts.scope ?? "global")}]`)} ${extras.join(" ")}`
|
|
338
|
+
);
|
|
537
339
|
}
|
|
538
|
-
}
|
|
539
|
-
if (
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
340
|
+
});
|
|
341
|
+
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").option("--raw", "Print the secret value only (for scripts and piping)").action((key, cmd) => {
|
|
342
|
+
const opts = buildOpts(cmd);
|
|
343
|
+
const value = getSecret(key, opts);
|
|
344
|
+
if (value === null) {
|
|
345
|
+
console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
|
|
346
|
+
process.exit(1);
|
|
543
347
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (v !== void 0) envMap[k] = v;
|
|
548
|
-
}
|
|
549
|
-
if (profile.stripEnvVars) {
|
|
550
|
-
for (const key of profile.stripEnvVars) {
|
|
551
|
-
delete envMap[key];
|
|
348
|
+
if (cmd.raw) {
|
|
349
|
+
process.stdout.write(value);
|
|
350
|
+
return;
|
|
552
351
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
let entries = listSecrets({
|
|
556
|
-
scope: opts.scope,
|
|
557
|
-
projectPath: opts.projectPath,
|
|
558
|
-
source: opts.source ?? "cli",
|
|
559
|
-
silent: true
|
|
560
|
-
// list silently
|
|
352
|
+
const payload = wantsJsonOutput(program2, cmd) ? { ok: true, data: { key, value } } : { key, value };
|
|
353
|
+
process.stdout.write(JSON.stringify(payload) + "\n");
|
|
561
354
|
});
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
(
|
|
569
|
-
|
|
570
|
-
}
|
|
571
|
-
for (const entry of entries) {
|
|
572
|
-
if (entry.envelope) {
|
|
573
|
-
const decay = checkDecay(entry.envelope);
|
|
574
|
-
if (decay.isExpired) continue;
|
|
355
|
+
program2.command("delete <key>").alias("rm").description("Remove a secret from the keyring").option("-g, --global", "Delete from global scope only").option("-p, --project", "Delete from project scope only").option("--project-path <path>", "Explicit project path").action((key, cmd) => {
|
|
356
|
+
const opts = buildOpts(cmd);
|
|
357
|
+
const deleted = deleteSecret(key, opts);
|
|
358
|
+
if (deleted) {
|
|
359
|
+
console.log(`${SYMBOLS.check} ${c.green("deleted")} ${c.bold(key)}`);
|
|
360
|
+
} else {
|
|
361
|
+
console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
|
|
362
|
+
process.exit(1);
|
|
575
363
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
if (val !== null) {
|
|
585
|
-
envMap[entry.key] = val;
|
|
586
|
-
if (val.length > 5) {
|
|
587
|
-
secretsToRedact.add(val);
|
|
588
|
-
}
|
|
364
|
+
});
|
|
365
|
+
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) => {
|
|
366
|
+
const opts = buildOpts(cmd);
|
|
367
|
+
let entries = listSecrets(opts);
|
|
368
|
+
if (cmd.tag) {
|
|
369
|
+
entries = entries.filter(
|
|
370
|
+
(e) => e.envelope?.meta.tags?.includes(cmd.tag)
|
|
371
|
+
);
|
|
589
372
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
return new Promise((resolve, reject) => {
|
|
593
|
-
const networkTools = /* @__PURE__ */ new Set([
|
|
594
|
-
"curl",
|
|
595
|
-
"wget",
|
|
596
|
-
"ping",
|
|
597
|
-
"nc",
|
|
598
|
-
"netcat",
|
|
599
|
-
"ssh",
|
|
600
|
-
"telnet",
|
|
601
|
-
"ftp",
|
|
602
|
-
"dig",
|
|
603
|
-
"nslookup"
|
|
604
|
-
]);
|
|
605
|
-
if (profile.allowNetwork === false && networkTools.has(opts.command)) {
|
|
606
|
-
const msg = `[QRING] Execution blocked: network access is disabled for profile "${profile.name}", command "${opts.command}" is considered network-related`;
|
|
607
|
-
if (opts.captureOutput) {
|
|
608
|
-
return resolve({ code: 126, stdout: "", stderr: msg });
|
|
609
|
-
}
|
|
610
|
-
process.stderr.write(msg + "\n");
|
|
611
|
-
return resolve({ code: 126, stdout: "", stderr: "" });
|
|
373
|
+
if (cmd.expired) {
|
|
374
|
+
entries = entries.filter((e) => e.decay?.isExpired);
|
|
612
375
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
});
|
|
618
|
-
let timedOut = false;
|
|
619
|
-
let timer;
|
|
620
|
-
if (maxRuntime) {
|
|
621
|
-
timer = setTimeout(() => {
|
|
622
|
-
timedOut = true;
|
|
623
|
-
child.kill("SIGKILL");
|
|
624
|
-
}, maxRuntime * 1e3);
|
|
376
|
+
if (cmd.stale) {
|
|
377
|
+
entries = entries.filter(
|
|
378
|
+
(e) => e.decay?.isStale && !e.decay?.isExpired
|
|
379
|
+
);
|
|
625
380
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
if (child.stdout) child.stdout.pipe(stdoutRedact);
|
|
629
|
-
if (child.stderr) child.stderr.pipe(stderrRedact);
|
|
630
|
-
let stdoutStr = "";
|
|
631
|
-
let stderrStr = "";
|
|
632
|
-
if (opts.captureOutput) {
|
|
633
|
-
stdoutRedact.on("data", (d) => stdoutStr += d.toString());
|
|
634
|
-
stderrRedact.on("data", (d) => stderrStr += d.toString());
|
|
635
|
-
} else {
|
|
636
|
-
stdoutRedact.pipe(process.stdout);
|
|
637
|
-
stderrRedact.pipe(process.stderr);
|
|
381
|
+
if (cmd.filter) {
|
|
382
|
+
entries = filterSecretsByKeyGlob(entries, cmd.filter);
|
|
638
383
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
if (timedOut) {
|
|
642
|
-
resolve({ code: 124, stdout: stdoutStr, stderr: stderrStr + `
|
|
643
|
-
[QRING] Process killed: exceeded ${maxRuntime}s runtime limit` });
|
|
644
|
-
} else {
|
|
645
|
-
resolve({ code: code ?? 0, stdout: stdoutStr, stderr: stderrStr });
|
|
646
|
-
}
|
|
647
|
-
});
|
|
648
|
-
child.on("error", (err) => {
|
|
649
|
-
if (timer) clearTimeout(timer);
|
|
650
|
-
reject(err);
|
|
651
|
-
});
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
// src/core/scan.ts
|
|
656
|
-
import { readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
657
|
-
import { join } from "path";
|
|
658
|
-
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
659
|
-
"node_modules",
|
|
660
|
-
".git",
|
|
661
|
-
".next",
|
|
662
|
-
"dist",
|
|
663
|
-
"build",
|
|
664
|
-
"coverage",
|
|
665
|
-
".cursor",
|
|
666
|
-
"venv",
|
|
667
|
-
"__pycache__"
|
|
668
|
-
]);
|
|
669
|
-
var IGNORE_EXTS = /* @__PURE__ */ new Set([
|
|
670
|
-
".png",
|
|
671
|
-
".jpg",
|
|
672
|
-
".jpeg",
|
|
673
|
-
".gif",
|
|
674
|
-
".ico",
|
|
675
|
-
".svg",
|
|
676
|
-
".webp",
|
|
677
|
-
".mp4",
|
|
678
|
-
".mp3",
|
|
679
|
-
".wav",
|
|
680
|
-
".ogg",
|
|
681
|
-
".pdf",
|
|
682
|
-
".zip",
|
|
683
|
-
".tar",
|
|
684
|
-
".gz",
|
|
685
|
-
".xz",
|
|
686
|
-
".ttf",
|
|
687
|
-
".woff",
|
|
688
|
-
".woff2",
|
|
689
|
-
".eot",
|
|
690
|
-
".exe",
|
|
691
|
-
".dll",
|
|
692
|
-
".so",
|
|
693
|
-
".dylib",
|
|
694
|
-
".lock"
|
|
695
|
-
]);
|
|
696
|
-
var SECRET_KEYWORDS = /((?:api_?key|secret|token|password|auth|credential|access_?key)[a-z0-9_]*)\s*[:=]\s*(['"])([^'"]+)\2/i;
|
|
697
|
-
function calculateEntropy(str) {
|
|
698
|
-
if (!str) return 0;
|
|
699
|
-
const len = str.length;
|
|
700
|
-
const frequencies = /* @__PURE__ */ new Map();
|
|
701
|
-
for (let i = 0; i < len; i++) {
|
|
702
|
-
const char = str[i];
|
|
703
|
-
frequencies.set(char, (frequencies.get(char) || 0) + 1);
|
|
704
|
-
}
|
|
705
|
-
let entropy = 0;
|
|
706
|
-
for (const count of frequencies.values()) {
|
|
707
|
-
const p = count / len;
|
|
708
|
-
entropy -= p * Math.log2(p);
|
|
709
|
-
}
|
|
710
|
-
return entropy;
|
|
711
|
-
}
|
|
712
|
-
function scanCodebase(dir) {
|
|
713
|
-
const results = [];
|
|
714
|
-
function walk(currentDir) {
|
|
715
|
-
let entries;
|
|
716
|
-
try {
|
|
717
|
-
entries = readdirSync(currentDir);
|
|
718
|
-
} catch {
|
|
384
|
+
if (entries.length === 0) {
|
|
385
|
+
console.log(c.dim("No secrets found"));
|
|
719
386
|
return;
|
|
720
387
|
}
|
|
388
|
+
console.log(
|
|
389
|
+
c.bold(`
|
|
390
|
+
${SYMBOLS.key} q-ring secrets (${entries.length})
|
|
391
|
+
`)
|
|
392
|
+
);
|
|
393
|
+
const maxKeyLen = Math.max(...entries.map((e) => e.key.length));
|
|
721
394
|
for (const entry of entries) {
|
|
722
|
-
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
395
|
+
const parts = [];
|
|
396
|
+
const key = safeStr(entry.key);
|
|
397
|
+
const scope = safeStr(entry.scope);
|
|
398
|
+
const envs = entry.envelope?.states ? Object.keys(entry.envelope.states).map(safeStr) : null;
|
|
399
|
+
const entangledCount = safeNum(entry.envelope?.meta.entangled?.length);
|
|
400
|
+
const accessCount = safeNum(entry.envelope?.meta.accessCount);
|
|
401
|
+
const tags = safeArr(entry.envelope?.meta.tags);
|
|
402
|
+
const decayPct = safeNum(entry.decay?.lifetimePercent);
|
|
403
|
+
const expired = !!entry.decay?.isExpired;
|
|
404
|
+
const timeLeft = safeStr(entry.decay?.timeRemaining);
|
|
405
|
+
parts.push(c.dim("[") + scopeColor(scope) + c.dim("]"));
|
|
406
|
+
parts.push(c.bold(key.padEnd(maxKeyLen)));
|
|
407
|
+
if (envs) {
|
|
408
|
+
parts.push(c.magenta(`[${envs.join("|")}]`));
|
|
729
409
|
}
|
|
730
|
-
if (
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
if (IGNORE_EXTS.has(ext) || entry.endsWith(".lock")) continue;
|
|
735
|
-
let content;
|
|
736
|
-
try {
|
|
737
|
-
content = readFileSync2(fullPath, "utf8");
|
|
738
|
-
} catch {
|
|
739
|
-
continue;
|
|
740
|
-
}
|
|
741
|
-
if (content.includes("\0")) continue;
|
|
742
|
-
const lines = content.split(/\r?\n/);
|
|
743
|
-
for (let i = 0; i < lines.length; i++) {
|
|
744
|
-
const line = lines[i];
|
|
745
|
-
if (line.length > 500) continue;
|
|
746
|
-
const match = line.match(SECRET_KEYWORDS);
|
|
747
|
-
if (match) {
|
|
748
|
-
const varName = match[1];
|
|
749
|
-
const value = match[3];
|
|
750
|
-
if (value.length < 8) continue;
|
|
751
|
-
const lowerValue = value.toLowerCase();
|
|
752
|
-
if (lowerValue.includes("example") || lowerValue.includes("your_") || lowerValue.includes("placeholder") || lowerValue.includes("replace_me")) {
|
|
753
|
-
continue;
|
|
754
|
-
}
|
|
755
|
-
const entropy = calculateEntropy(value);
|
|
756
|
-
if (entropy > 3.5 || value.startsWith("sk-") || value.startsWith("ghp_")) {
|
|
757
|
-
const relPath = fullPath.startsWith(dir) ? fullPath.slice(dir.length).replace(/^[/\\]+/, "") : fullPath;
|
|
758
|
-
results.push({
|
|
759
|
-
file: relPath || fullPath,
|
|
760
|
-
line: i + 1,
|
|
761
|
-
keyName: varName,
|
|
762
|
-
match: value,
|
|
763
|
-
context: line.trim(),
|
|
764
|
-
entropy: parseFloat(entropy.toFixed(2))
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
}
|
|
410
|
+
if (entry.decay && (decayPct > 0 || expired)) {
|
|
411
|
+
parts.push(decayIndicator(decayPct, expired));
|
|
412
|
+
if (timeLeft && !expired) {
|
|
413
|
+
parts.push(c.dim(timeLeft));
|
|
768
414
|
}
|
|
769
415
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
walk(dir);
|
|
773
|
-
return results;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
// src/core/linter.ts
|
|
777
|
-
import { readFileSync as readFileSync3, writeFileSync, existsSync } from "fs";
|
|
778
|
-
import { basename, extname } from "path";
|
|
779
|
-
var ENV_REF_BY_EXT = {
|
|
780
|
-
".ts": (k) => `process.env.${k}`,
|
|
781
|
-
".tsx": (k) => `process.env.${k}`,
|
|
782
|
-
".js": (k) => `process.env.${k}`,
|
|
783
|
-
".jsx": (k) => `process.env.${k}`,
|
|
784
|
-
".mjs": (k) => `process.env.${k}`,
|
|
785
|
-
".cjs": (k) => `process.env.${k}`,
|
|
786
|
-
".py": (k) => `os.environ["${k}"]`,
|
|
787
|
-
".rb": (k) => `ENV["${k}"]`,
|
|
788
|
-
".go": (k) => `os.Getenv("${k}")`,
|
|
789
|
-
".rs": (k) => `std::env::var("${k}")`,
|
|
790
|
-
".java": (k) => `System.getenv("${k}")`,
|
|
791
|
-
".kt": (k) => `System.getenv("${k}")`,
|
|
792
|
-
".cs": (k) => `Environment.GetEnvironmentVariable("${k}")`,
|
|
793
|
-
".php": (k) => `getenv('${k}')`,
|
|
794
|
-
".sh": (k) => `\${${k}}`,
|
|
795
|
-
".bash": (k) => `\${${k}}`
|
|
796
|
-
};
|
|
797
|
-
function getEnvRef(filePath, keyName) {
|
|
798
|
-
const ext = extname(filePath).toLowerCase();
|
|
799
|
-
const formatter = ENV_REF_BY_EXT[ext];
|
|
800
|
-
return formatter ? formatter(keyName) : `process.env.${keyName}`;
|
|
801
|
-
}
|
|
802
|
-
function lintFiles(files, opts = {}) {
|
|
803
|
-
const results = [];
|
|
804
|
-
for (const file of files) {
|
|
805
|
-
if (!existsSync(file)) continue;
|
|
806
|
-
let content;
|
|
807
|
-
try {
|
|
808
|
-
content = readFileSync3(file, "utf8");
|
|
809
|
-
} catch {
|
|
810
|
-
continue;
|
|
811
|
-
}
|
|
812
|
-
if (content.includes("\0")) continue;
|
|
813
|
-
const SECRET_KEYWORDS2 = /((?:api_?key|secret|token|password|auth|credential|access_?key)[a-z0-9_]*)\s*[:=]\s*(['"])([^'"]+)\2/gi;
|
|
814
|
-
const lines = content.split(/\r?\n/);
|
|
815
|
-
const fixes = [];
|
|
816
|
-
for (let i = 0; i < lines.length; i++) {
|
|
817
|
-
const line = lines[i];
|
|
818
|
-
if (line.length > 500) continue;
|
|
819
|
-
let match;
|
|
820
|
-
SECRET_KEYWORDS2.lastIndex = 0;
|
|
821
|
-
while ((match = SECRET_KEYWORDS2.exec(line)) !== null) {
|
|
822
|
-
const varName = match[1].toUpperCase();
|
|
823
|
-
const quote = match[2];
|
|
824
|
-
const value = match[3];
|
|
825
|
-
if (value.length < 8) continue;
|
|
826
|
-
const lv = value.toLowerCase();
|
|
827
|
-
if (lv.includes("example") || lv.includes("your_") || lv.includes("placeholder") || lv.includes("replace_me") || lv.includes("xxx")) continue;
|
|
828
|
-
const entropy = calculateEntropy2(value);
|
|
829
|
-
if (entropy <= 3.5 && !value.startsWith("sk-") && !value.startsWith("ghp_")) continue;
|
|
830
|
-
const shouldFix = opts.fix === true;
|
|
831
|
-
if (shouldFix) {
|
|
832
|
-
const envRef = getEnvRef(file, varName);
|
|
833
|
-
fixes.push({
|
|
834
|
-
line: i,
|
|
835
|
-
original: `${quote}${value}${quote}`,
|
|
836
|
-
replacement: envRef,
|
|
837
|
-
keyName: varName,
|
|
838
|
-
value
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
results.push({
|
|
842
|
-
file,
|
|
843
|
-
line: i + 1,
|
|
844
|
-
keyName: varName,
|
|
845
|
-
match: value,
|
|
846
|
-
context: line.trim(),
|
|
847
|
-
entropy: parseFloat(entropy.toFixed(2)),
|
|
848
|
-
fixed: shouldFix
|
|
849
|
-
});
|
|
416
|
+
if (entangledCount > 0) {
|
|
417
|
+
parts.push(c.cyan(`${SYMBOLS.link} ${entangledCount}`));
|
|
850
418
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
if (lineIdx >= 0 && lineIdx < fixLines.length) {
|
|
857
|
-
fixLines[lineIdx] = fixLines[lineIdx].replace(fix.original, fix.replacement);
|
|
858
|
-
}
|
|
859
|
-
if (!hasSecret(fix.keyName, { scope: opts.scope, projectPath: opts.projectPath })) {
|
|
860
|
-
setSecret(fix.keyName, fix.value, {
|
|
861
|
-
scope: opts.scope ?? "global",
|
|
862
|
-
projectPath: opts.projectPath,
|
|
863
|
-
source: "cli",
|
|
864
|
-
description: `Auto-imported from ${basename(file)}:${fix.line + 1}`
|
|
865
|
-
});
|
|
866
|
-
}
|
|
419
|
+
if (accessCount > 0) {
|
|
420
|
+
parts.push(c.dim(`${SYMBOLS.eye} ${accessCount}`));
|
|
421
|
+
}
|
|
422
|
+
if (tags.length > 0) {
|
|
423
|
+
parts.push(c.dim(tags.map((t) => `#${t}`).join(" ")));
|
|
867
424
|
}
|
|
868
|
-
|
|
425
|
+
console.log(` ${parts.join(" ")}`);
|
|
869
426
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
const ch = str[i];
|
|
879
|
-
frequencies.set(ch, (frequencies.get(ch) || 0) + 1);
|
|
880
|
-
}
|
|
881
|
-
let entropy = 0;
|
|
882
|
-
for (const count of frequencies.values()) {
|
|
883
|
-
const p = count / len;
|
|
884
|
-
entropy -= p * Math.log2(p);
|
|
885
|
-
}
|
|
886
|
-
return entropy;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// src/core/validate.ts
|
|
890
|
-
function makeRequest(url, headers, timeoutMs = 1e4) {
|
|
891
|
-
return httpRequest_({ url, method: "GET", headers, timeoutMs });
|
|
892
|
-
}
|
|
893
|
-
var ProviderRegistry = class {
|
|
894
|
-
providers = /* @__PURE__ */ new Map();
|
|
895
|
-
register(provider) {
|
|
896
|
-
this.providers.set(provider.name, provider);
|
|
897
|
-
}
|
|
898
|
-
get(name) {
|
|
899
|
-
return this.providers.get(name);
|
|
900
|
-
}
|
|
901
|
-
detectProvider(value, hints) {
|
|
902
|
-
if (hints?.provider) {
|
|
903
|
-
return this.providers.get(hints.provider);
|
|
427
|
+
console.log();
|
|
428
|
+
});
|
|
429
|
+
program2.command("inspect <key>").description("Show full quantum state of a secret").option("-g, --global", "Inspect global scope only").option("-p, --project", "Inspect project scope only").option("--project-path <path>", "Explicit project path").action((key, cmd) => {
|
|
430
|
+
const opts = buildOpts(cmd);
|
|
431
|
+
const result = getEnvelope(key, opts);
|
|
432
|
+
if (!result) {
|
|
433
|
+
console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
|
|
434
|
+
process.exit(1);
|
|
904
435
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
436
|
+
const { envelope, scope } = result;
|
|
437
|
+
const decay = checkDecay(envelope);
|
|
438
|
+
const safeScope = safeStr(scope);
|
|
439
|
+
const createdAt = safeStr(envelope.meta.createdAt);
|
|
440
|
+
const updatedAt = safeStr(envelope.meta.updatedAt);
|
|
441
|
+
const accessCount = safeNum(envelope.meta.accessCount);
|
|
442
|
+
const lastAccess = safeStr(envelope.meta.lastAccessedAt);
|
|
443
|
+
const desc = safeStr(envelope.meta.description);
|
|
444
|
+
const tags = safeArr(envelope.meta.tags);
|
|
445
|
+
const entangled = (envelope.meta.entangled ?? []).map((l) => ({
|
|
446
|
+
service: safeStr(l.service),
|
|
447
|
+
key: safeStr(l.key)
|
|
448
|
+
}));
|
|
449
|
+
const stateEnvs = envelope.states ? Object.keys(envelope.states).map(safeStr) : null;
|
|
450
|
+
const defaultEnv = safeStr(envelope.defaultEnv);
|
|
451
|
+
const decayTime = safeStr(decay.timeRemaining);
|
|
452
|
+
const decayPct = safeNum(decay.lifetimePercent);
|
|
453
|
+
const expired = !!decay.isExpired;
|
|
454
|
+
console.log(`
|
|
455
|
+
${c.bold(SYMBOLS.key + " " + key)}`);
|
|
456
|
+
console.log(` ${c.dim("scope:")} ${scopeColor(safeScope)}`);
|
|
457
|
+
if (stateEnvs) {
|
|
458
|
+
console.log(` ${c.dim("type:")} ${c.magenta("superposition")}`);
|
|
459
|
+
console.log(` ${c.dim("states:")}`);
|
|
460
|
+
for (const env of stateEnvs) {
|
|
461
|
+
const isDefault = env === defaultEnv;
|
|
462
|
+
console.log(
|
|
463
|
+
` ${envBadge(env)} ${isDefault ? c.dim("(default)") : ""}`
|
|
464
|
+
);
|
|
910
465
|
}
|
|
466
|
+
} else {
|
|
467
|
+
console.log(` ${c.dim("type:")} ${c.green("collapsed")}`);
|
|
911
468
|
}
|
|
912
|
-
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
}
|
|
917
|
-
};
|
|
918
|
-
var openaiProvider = {
|
|
919
|
-
name: "openai",
|
|
920
|
-
description: "OpenAI API key validation",
|
|
921
|
-
prefixes: ["sk-"],
|
|
922
|
-
async validate(value) {
|
|
923
|
-
const start = Date.now();
|
|
924
|
-
try {
|
|
925
|
-
const { statusCode } = await makeRequest(
|
|
926
|
-
"https://api.openai.com/v1/models?limit=1",
|
|
927
|
-
{
|
|
928
|
-
Authorization: `Bearer ${value}`,
|
|
929
|
-
"User-Agent": "q-ring-validator/1.0"
|
|
930
|
-
}
|
|
931
|
-
);
|
|
932
|
-
const latencyMs = Date.now() - start;
|
|
933
|
-
if (statusCode === 200)
|
|
934
|
-
return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "openai" };
|
|
935
|
-
if (statusCode === 401)
|
|
936
|
-
return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "openai" };
|
|
937
|
-
if (statusCode === 429)
|
|
938
|
-
return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "openai" };
|
|
939
|
-
return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "openai" };
|
|
940
|
-
} catch (err) {
|
|
941
|
-
return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "openai" };
|
|
469
|
+
console.log(` ${c.dim("created:")} ${createdAt}`);
|
|
470
|
+
console.log(` ${c.dim("updated:")} ${updatedAt}`);
|
|
471
|
+
console.log(` ${c.dim("accessed:")} ${accessCount} times`);
|
|
472
|
+
if (lastAccess) {
|
|
473
|
+
console.log(` ${c.dim("last read:")} ${lastAccess}`);
|
|
942
474
|
}
|
|
943
|
-
|
|
944
|
-
};
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
async validate(value) {
|
|
950
|
-
const start = Date.now();
|
|
951
|
-
try {
|
|
952
|
-
const { statusCode } = await makeRequest(
|
|
953
|
-
"https://api.stripe.com/v1/balance",
|
|
954
|
-
{
|
|
955
|
-
Authorization: `Bearer ${value}`,
|
|
956
|
-
"User-Agent": "q-ring-validator/1.0"
|
|
957
|
-
}
|
|
475
|
+
if (desc) {
|
|
476
|
+
console.log(` ${c.dim("desc:")} ${desc}`);
|
|
477
|
+
}
|
|
478
|
+
if (tags.length > 0) {
|
|
479
|
+
console.log(
|
|
480
|
+
` ${c.dim("tags:")} ${tags.map((t) => c.cyan(`#${t}`)).join(" ")}`
|
|
958
481
|
);
|
|
959
|
-
const latencyMs = Date.now() - start;
|
|
960
|
-
if (statusCode === 200)
|
|
961
|
-
return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "stripe" };
|
|
962
|
-
if (statusCode === 401)
|
|
963
|
-
return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "stripe" };
|
|
964
|
-
if (statusCode === 429)
|
|
965
|
-
return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "stripe" };
|
|
966
|
-
return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "stripe" };
|
|
967
|
-
} catch (err) {
|
|
968
|
-
return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "stripe" };
|
|
969
482
|
}
|
|
970
|
-
|
|
483
|
+
if (decayTime) {
|
|
484
|
+
console.log(
|
|
485
|
+
` ${c.dim("decay:")} ${decayIndicator(decayPct, expired)} ${decayTime}`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
if (entangled.length > 0) {
|
|
489
|
+
console.log(` ${c.dim("entangled:")}`);
|
|
490
|
+
for (const link of entangled) {
|
|
491
|
+
console.log(` ${SYMBOLS.link} ${link.service}/${link.key}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
console.log();
|
|
495
|
+
});
|
|
496
|
+
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) => {
|
|
497
|
+
const opts = buildOpts(cmd);
|
|
498
|
+
const output = exportSecrets({
|
|
499
|
+
...opts,
|
|
500
|
+
format: cmd.format,
|
|
501
|
+
keys: cmd.keys?.split(",").map((k) => k.trim()),
|
|
502
|
+
tags: cmd.tags?.split(",").map((t) => t.trim())
|
|
503
|
+
});
|
|
504
|
+
process.stdout.write(output + "\n");
|
|
505
|
+
});
|
|
506
|
+
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) => {
|
|
507
|
+
const opts = buildOpts(cmd);
|
|
508
|
+
const result = importDotenv(file, {
|
|
509
|
+
scope: opts.scope,
|
|
510
|
+
projectPath: opts.projectPath,
|
|
511
|
+
source: "cli",
|
|
512
|
+
skipExisting: cmd.skipExisting,
|
|
513
|
+
dryRun: cmd.dryRun
|
|
514
|
+
});
|
|
515
|
+
if (cmd.dryRun) {
|
|
516
|
+
console.log(
|
|
517
|
+
`
|
|
518
|
+
${SYMBOLS.package} ${c.bold("Dry run")} \u2014 would import ${result.imported.length} of ${result.total} secrets:
|
|
519
|
+
`
|
|
520
|
+
);
|
|
521
|
+
for (const key of result.imported) {
|
|
522
|
+
console.log(` ${SYMBOLS.key} ${c.bold(key)}`);
|
|
523
|
+
}
|
|
524
|
+
if (result.skipped.length > 0) {
|
|
525
|
+
console.log(
|
|
526
|
+
`
|
|
527
|
+
${c.dim(`Skipped (existing): ${result.skipped.join(", ")}`)}`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
} else {
|
|
531
|
+
console.log(
|
|
532
|
+
`${SYMBOLS.check} ${c.green("imported")} ${result.imported.length} secret(s) from ${c.bold(file)}`
|
|
533
|
+
);
|
|
534
|
+
if (result.skipped.length > 0) {
|
|
535
|
+
console.log(
|
|
536
|
+
c.dim(
|
|
537
|
+
` skipped ${result.skipped.length} existing: ${result.skipped.join(", ")}`
|
|
538
|
+
)
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
console.log();
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/core/noise.ts
|
|
547
|
+
import { randomBytes, randomInt } from "crypto";
|
|
548
|
+
var ALPHA_NUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
549
|
+
var PASSWORD_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,.<>?";
|
|
550
|
+
function randomString(charset, length) {
|
|
551
|
+
let result = "";
|
|
552
|
+
for (let i = 0; i < length; i++) {
|
|
553
|
+
result += charset[randomInt(charset.length)];
|
|
554
|
+
}
|
|
555
|
+
return result;
|
|
556
|
+
}
|
|
557
|
+
function generateSecret(opts = {}) {
|
|
558
|
+
const format = opts.format ?? "api-key";
|
|
559
|
+
switch (format) {
|
|
560
|
+
case "hex": {
|
|
561
|
+
const len = opts.length ?? 32;
|
|
562
|
+
return randomBytes(len).toString("hex");
|
|
563
|
+
}
|
|
564
|
+
case "base64": {
|
|
565
|
+
const len = opts.length ?? 32;
|
|
566
|
+
return randomBytes(len).toString("base64url");
|
|
567
|
+
}
|
|
568
|
+
case "alphanumeric": {
|
|
569
|
+
const len = opts.length ?? 32;
|
|
570
|
+
return randomString(ALPHA_NUM, len);
|
|
571
|
+
}
|
|
572
|
+
case "uuid": {
|
|
573
|
+
const bytes = randomBytes(16);
|
|
574
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
575
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
576
|
+
const hex = bytes.toString("hex");
|
|
577
|
+
return [
|
|
578
|
+
hex.slice(0, 8),
|
|
579
|
+
hex.slice(8, 12),
|
|
580
|
+
hex.slice(12, 16),
|
|
581
|
+
hex.slice(16, 20),
|
|
582
|
+
hex.slice(20, 32)
|
|
583
|
+
].join("-");
|
|
584
|
+
}
|
|
585
|
+
case "api-key": {
|
|
586
|
+
const prefix = opts.prefix ?? "qr_";
|
|
587
|
+
const len = opts.length ?? 48;
|
|
588
|
+
return prefix + randomString(ALPHA_NUM, len);
|
|
589
|
+
}
|
|
590
|
+
case "token": {
|
|
591
|
+
const prefix = opts.prefix ?? "";
|
|
592
|
+
const len = opts.length ?? 64;
|
|
593
|
+
return prefix + randomBytes(len).toString("base64url");
|
|
594
|
+
}
|
|
595
|
+
case "password": {
|
|
596
|
+
const len = opts.length ?? 24;
|
|
597
|
+
let pw = randomString(PASSWORD_CHARS, len);
|
|
598
|
+
const hasUpper = /[A-Z]/.test(pw);
|
|
599
|
+
const hasLower = /[a-z]/.test(pw);
|
|
600
|
+
const hasDigit = /[0-9]/.test(pw);
|
|
601
|
+
const hasSpecial = /[^A-Za-z0-9]/.test(pw);
|
|
602
|
+
if (!hasUpper) pw = replaceAt(pw, randomInt(len), randomString("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1));
|
|
603
|
+
if (!hasLower) pw = replaceAt(pw, randomInt(len), randomString("abcdefghijklmnopqrstuvwxyz", 1));
|
|
604
|
+
if (!hasDigit) pw = replaceAt(pw, randomInt(len), randomString("0123456789", 1));
|
|
605
|
+
if (!hasSpecial) pw = replaceAt(pw, randomInt(len), randomString("!@#$%^&*()-_=+", 1));
|
|
606
|
+
return pw;
|
|
607
|
+
}
|
|
608
|
+
default:
|
|
609
|
+
return randomBytes(32).toString("hex");
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function replaceAt(str, index, char) {
|
|
613
|
+
return str.slice(0, index) + char + str.slice(index + 1);
|
|
614
|
+
}
|
|
615
|
+
function estimateEntropy(secret) {
|
|
616
|
+
const charsets = [
|
|
617
|
+
{ regex: /[a-z]/, size: 26 },
|
|
618
|
+
{ regex: /[A-Z]/, size: 26 },
|
|
619
|
+
{ regex: /[0-9]/, size: 10 },
|
|
620
|
+
{ regex: /[^A-Za-z0-9]/, size: 32 }
|
|
621
|
+
];
|
|
622
|
+
let poolSize = 0;
|
|
623
|
+
for (const { regex, size } of charsets) {
|
|
624
|
+
if (regex.test(secret)) poolSize += size;
|
|
625
|
+
}
|
|
626
|
+
return poolSize > 0 ? Math.floor(Math.log2(poolSize) * secret.length) : 0;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/core/validate.ts
|
|
630
|
+
function makeRequest(url, headers, timeoutMs = 1e4) {
|
|
631
|
+
return httpRequest({ url, method: "GET", headers, timeoutMs });
|
|
632
|
+
}
|
|
633
|
+
var ProviderRegistry = class {
|
|
634
|
+
providers = /* @__PURE__ */ new Map();
|
|
635
|
+
register(provider) {
|
|
636
|
+
this.providers.set(provider.name, provider);
|
|
637
|
+
}
|
|
638
|
+
get(name) {
|
|
639
|
+
return this.providers.get(name);
|
|
640
|
+
}
|
|
641
|
+
detectProvider(value, hints) {
|
|
642
|
+
if (hints?.provider) {
|
|
643
|
+
return this.providers.get(hints.provider);
|
|
644
|
+
}
|
|
645
|
+
for (const provider of this.providers.values()) {
|
|
646
|
+
if (provider.prefixes) {
|
|
647
|
+
for (const pfx of provider.prefixes) {
|
|
648
|
+
if (value.startsWith(pfx)) return provider;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return void 0;
|
|
653
|
+
}
|
|
654
|
+
listProviders() {
|
|
655
|
+
return [...this.providers.values()];
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
var openaiProvider = {
|
|
659
|
+
name: "openai",
|
|
660
|
+
description: "OpenAI API key validation",
|
|
661
|
+
prefixes: ["sk-"],
|
|
662
|
+
async validate(value) {
|
|
663
|
+
const start = Date.now();
|
|
664
|
+
try {
|
|
665
|
+
const { statusCode } = await makeRequest(
|
|
666
|
+
"https://api.openai.com/v1/models?limit=1",
|
|
667
|
+
{
|
|
668
|
+
Authorization: `Bearer ${value}`,
|
|
669
|
+
"User-Agent": "q-ring-validator/1.0"
|
|
670
|
+
}
|
|
671
|
+
);
|
|
672
|
+
const latencyMs = Date.now() - start;
|
|
673
|
+
if (statusCode === 200)
|
|
674
|
+
return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "openai" };
|
|
675
|
+
if (statusCode === 401)
|
|
676
|
+
return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "openai" };
|
|
677
|
+
if (statusCode === 429)
|
|
678
|
+
return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "openai" };
|
|
679
|
+
return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "openai" };
|
|
680
|
+
} catch (err) {
|
|
681
|
+
return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "openai" };
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
var stripeProvider = {
|
|
686
|
+
name: "stripe",
|
|
687
|
+
description: "Stripe API key validation",
|
|
688
|
+
prefixes: ["sk_live_", "sk_test_", "rk_live_", "rk_test_", "pk_live_", "pk_test_"],
|
|
689
|
+
async validate(value) {
|
|
690
|
+
const start = Date.now();
|
|
691
|
+
try {
|
|
692
|
+
const { statusCode } = await makeRequest(
|
|
693
|
+
"https://api.stripe.com/v1/balance",
|
|
694
|
+
{
|
|
695
|
+
Authorization: `Bearer ${value}`,
|
|
696
|
+
"User-Agent": "q-ring-validator/1.0"
|
|
697
|
+
}
|
|
698
|
+
);
|
|
699
|
+
const latencyMs = Date.now() - start;
|
|
700
|
+
if (statusCode === 200)
|
|
701
|
+
return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "stripe" };
|
|
702
|
+
if (statusCode === 401)
|
|
703
|
+
return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "stripe" };
|
|
704
|
+
if (statusCode === 429)
|
|
705
|
+
return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "stripe" };
|
|
706
|
+
return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "stripe" };
|
|
707
|
+
} catch (err) {
|
|
708
|
+
return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "stripe" };
|
|
709
|
+
}
|
|
710
|
+
}
|
|
971
711
|
};
|
|
972
712
|
var githubProvider = {
|
|
973
713
|
name: "github",
|
|
@@ -1163,522 +903,93 @@ function getProjectContext(opts = {}) {
|
|
|
1163
903
|
};
|
|
1164
904
|
}
|
|
1165
905
|
|
|
1166
|
-
// src/
|
|
1167
|
-
import {
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
var MEMORY_FILE = "agent-memory.enc";
|
|
1173
|
-
var KEYRING_SERVICE = "qring-memory-key";
|
|
1174
|
-
var KEYRING_ACCOUNT = "encryption-key";
|
|
1175
|
-
function getMemoryDir() {
|
|
1176
|
-
const dir = join2(homedir(), ".config", "q-ring");
|
|
1177
|
-
if (!existsSync2(dir)) {
|
|
1178
|
-
mkdirSync(dir, { recursive: true });
|
|
1179
|
-
}
|
|
1180
|
-
return dir;
|
|
1181
|
-
}
|
|
1182
|
-
function getMemoryPath() {
|
|
1183
|
-
return join2(getMemoryDir(), MEMORY_FILE);
|
|
1184
|
-
}
|
|
1185
|
-
function deriveLegacyKey() {
|
|
1186
|
-
const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
|
|
1187
|
-
return createHash("sha256").update(fingerprint).digest();
|
|
1188
|
-
}
|
|
1189
|
-
function getOrCreateKey() {
|
|
1190
|
-
try {
|
|
1191
|
-
const entry = new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT);
|
|
1192
|
-
const stored = entry.getPassword();
|
|
1193
|
-
if (stored) return Buffer.from(stored, "base64");
|
|
1194
|
-
const key = randomBytes3(32);
|
|
1195
|
-
entry.setPassword(key.toString("base64"));
|
|
1196
|
-
return key;
|
|
1197
|
-
} catch {
|
|
1198
|
-
console.warn("[q-ring] OS keyring unavailable for memory key \u2014 falling back to machine-derived key");
|
|
1199
|
-
return deriveLegacyKey();
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
function encryptWith(data, key) {
|
|
1203
|
-
const iv = randomBytes3(12);
|
|
1204
|
-
const cipher = createCipheriv2("aes-256-gcm", key, iv);
|
|
1205
|
-
const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
|
|
1206
|
-
const tag = cipher.getAuthTag();
|
|
1207
|
-
return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
|
|
1208
|
-
}
|
|
1209
|
-
function decryptWith(blob, key) {
|
|
1210
|
-
const parts = blob.split(":");
|
|
1211
|
-
if (parts.length !== 3) throw new Error("Invalid encrypted format");
|
|
1212
|
-
const iv = Buffer.from(parts[0], "base64");
|
|
1213
|
-
const tag = Buffer.from(parts[1], "base64");
|
|
1214
|
-
const encrypted = Buffer.from(parts[2], "base64");
|
|
1215
|
-
const decipher = createDecipheriv2("aes-256-gcm", key, iv);
|
|
1216
|
-
decipher.setAuthTag(tag);
|
|
1217
|
-
return decipher.update(encrypted) + decipher.final("utf8");
|
|
1218
|
-
}
|
|
1219
|
-
function encrypt(data) {
|
|
1220
|
-
return encryptWith(data, getOrCreateKey());
|
|
1221
|
-
}
|
|
1222
|
-
function decrypt(blob) {
|
|
1223
|
-
const key = getOrCreateKey();
|
|
1224
|
-
try {
|
|
1225
|
-
return decryptWith(blob, key);
|
|
1226
|
-
} catch {
|
|
1227
|
-
const legacy = deriveLegacyKey();
|
|
1228
|
-
const plain = decryptWith(blob, legacy);
|
|
1229
|
-
writeFileSync2(getMemoryPath(), encryptWith(plain, key), "utf8");
|
|
1230
|
-
return plain;
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
function loadStore() {
|
|
1234
|
-
const path = getMemoryPath();
|
|
1235
|
-
if (!existsSync2(path)) {
|
|
1236
|
-
return { entries: {} };
|
|
1237
|
-
}
|
|
1238
|
-
try {
|
|
1239
|
-
const raw = readFileSync4(path, "utf8");
|
|
1240
|
-
const decrypted = decrypt(raw);
|
|
1241
|
-
return JSON.parse(decrypted);
|
|
1242
|
-
} catch {
|
|
1243
|
-
return { entries: {} };
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
function saveStore(store) {
|
|
1247
|
-
const json = JSON.stringify(store);
|
|
1248
|
-
const encrypted = encrypt(json);
|
|
1249
|
-
writeFileSync2(getMemoryPath(), encrypted, "utf8");
|
|
1250
|
-
}
|
|
1251
|
-
function remember(key, value) {
|
|
1252
|
-
const store = loadStore();
|
|
1253
|
-
store.entries[key] = {
|
|
1254
|
-
value,
|
|
1255
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1256
|
-
};
|
|
1257
|
-
saveStore(store);
|
|
1258
|
-
}
|
|
1259
|
-
function recall(key) {
|
|
1260
|
-
const store = loadStore();
|
|
1261
|
-
return store.entries[key]?.value ?? null;
|
|
1262
|
-
}
|
|
1263
|
-
function listMemory() {
|
|
1264
|
-
const store = loadStore();
|
|
1265
|
-
return Object.entries(store.entries).map(([key, entry]) => ({
|
|
1266
|
-
key,
|
|
1267
|
-
updatedAt: entry.updatedAt
|
|
1268
|
-
}));
|
|
1269
|
-
}
|
|
1270
|
-
function forget(key) {
|
|
1271
|
-
const store = loadStore();
|
|
1272
|
-
if (key in store.entries) {
|
|
1273
|
-
delete store.entries[key];
|
|
1274
|
-
saveStore(store);
|
|
1275
|
-
return true;
|
|
1276
|
-
}
|
|
1277
|
-
return false;
|
|
1278
|
-
}
|
|
1279
|
-
function clearMemory() {
|
|
1280
|
-
saveStore({ entries: {} });
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
// src/hooks/precommit.ts
|
|
1284
|
-
import { execSync } from "child_process";
|
|
1285
|
-
import { existsSync as existsSync3, writeFileSync as writeFileSync3, chmodSync, readFileSync as readFileSync5, unlinkSync } from "fs";
|
|
1286
|
-
import { join as join3 } from "path";
|
|
1287
|
-
function getStagedFiles() {
|
|
1288
|
-
try {
|
|
1289
|
-
const output = execSync("git diff --cached --name-only --diff-filter=ACM", {
|
|
1290
|
-
encoding: "utf8",
|
|
1291
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1292
|
-
});
|
|
1293
|
-
return output.split("\n").map((f) => f.trim()).filter((f) => f.length > 0);
|
|
1294
|
-
} catch {
|
|
1295
|
-
return [];
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
function runPreCommitScan() {
|
|
1299
|
-
const staged = getStagedFiles();
|
|
1300
|
-
if (staged.length === 0) return 0;
|
|
1301
|
-
const results = lintFiles(staged);
|
|
1302
|
-
if (results.length === 0) return 0;
|
|
1303
|
-
console.error("\n[q-ring] Pre-commit scan found hardcoded secrets:\n");
|
|
1304
|
-
for (const r of results) {
|
|
1305
|
-
console.error(` ${r.file}:${r.line} ${r.keyName} (entropy: ${r.entropy})`);
|
|
1306
|
-
}
|
|
1307
|
-
console.error(
|
|
1308
|
-
`
|
|
1309
|
-
[q-ring] Commit blocked. Use "qring scan --fix" to auto-migrate, or "git commit --no-verify" to bypass.
|
|
1310
|
-
`
|
|
1311
|
-
);
|
|
1312
|
-
return 1;
|
|
1313
|
-
}
|
|
1314
|
-
var HOOK_SCRIPT = `#!/bin/sh
|
|
1315
|
-
# q-ring pre-commit hook \u2014 scans staged files for hardcoded secrets
|
|
1316
|
-
npx qring hook:run 2>&1
|
|
1317
|
-
exit $?
|
|
1318
|
-
`;
|
|
1319
|
-
function installPreCommitHook(repoPath) {
|
|
1320
|
-
const root = repoPath ?? process.cwd();
|
|
1321
|
-
const hooksDir = join3(root, ".git", "hooks");
|
|
1322
|
-
if (!existsSync3(join3(root, ".git"))) {
|
|
1323
|
-
return { installed: false, path: "", message: "Not a git repository" };
|
|
1324
|
-
}
|
|
1325
|
-
const hookPath = join3(hooksDir, "pre-commit");
|
|
1326
|
-
if (existsSync3(hookPath)) {
|
|
1327
|
-
const existing = readFileSync5(hookPath, "utf8");
|
|
1328
|
-
if (existing.includes("q-ring")) {
|
|
1329
|
-
return { installed: true, path: hookPath, message: "Hook already installed" };
|
|
1330
|
-
}
|
|
1331
|
-
writeFileSync3(hookPath, HOOK_SCRIPT + "\n" + existing, "utf8");
|
|
1332
|
-
} else {
|
|
1333
|
-
writeFileSync3(hookPath, HOOK_SCRIPT, "utf8");
|
|
1334
|
-
}
|
|
1335
|
-
chmodSync(hookPath, 493);
|
|
1336
|
-
return { installed: true, path: hookPath, message: "Pre-commit hook installed" };
|
|
1337
|
-
}
|
|
1338
|
-
function uninstallPreCommitHook(repoPath) {
|
|
1339
|
-
const root = repoPath ?? process.cwd();
|
|
1340
|
-
const hookPath = join3(root, ".git", "hooks", "pre-commit");
|
|
1341
|
-
if (!existsSync3(hookPath)) return false;
|
|
1342
|
-
const content = readFileSync5(hookPath, "utf8");
|
|
1343
|
-
if (!content.includes("q-ring")) return false;
|
|
1344
|
-
const lines = content.split("\n");
|
|
1345
|
-
const cleaned = lines.filter(
|
|
1346
|
-
(l) => !l.includes("q-ring") && !l.includes("npx qring hook:run")
|
|
1347
|
-
);
|
|
1348
|
-
if (cleaned.filter((l) => l.trim() && !l.startsWith("#!")).length === 0) {
|
|
1349
|
-
unlinkSync(hookPath);
|
|
1350
|
-
} else {
|
|
1351
|
-
writeFileSync3(hookPath, cleaned.join("\n"), "utf8");
|
|
1352
|
-
}
|
|
1353
|
-
return true;
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
// src/cli/commands.ts
|
|
1357
|
-
import { writeFileSync as writeFileSync4, readFileSync as readFileSync6, existsSync as existsSync4 } from "fs";
|
|
1358
|
-
|
|
1359
|
-
// src/utils/prompt.ts
|
|
1360
|
-
import { createInterface } from "readline";
|
|
1361
|
-
async function promptSecret(message) {
|
|
1362
|
-
const rl = createInterface({
|
|
1363
|
-
input: process.stdin,
|
|
1364
|
-
output: process.stderr,
|
|
1365
|
-
terminal: true
|
|
1366
|
-
});
|
|
1367
|
-
return new Promise((resolve) => {
|
|
1368
|
-
process.stderr.write(message);
|
|
1369
|
-
const stdin = process.stdin;
|
|
1370
|
-
const wasRaw = stdin.isRaw;
|
|
1371
|
-
if (stdin.isTTY && stdin.setRawMode) {
|
|
1372
|
-
stdin.setRawMode(true);
|
|
1373
|
-
}
|
|
1374
|
-
let input = "";
|
|
1375
|
-
const onData = (chunk) => {
|
|
1376
|
-
const char = chunk.toString("utf8");
|
|
1377
|
-
if (char === "\n" || char === "\r") {
|
|
1378
|
-
stdin.removeListener("data", onData);
|
|
1379
|
-
if (stdin.isTTY && stdin.setRawMode) {
|
|
1380
|
-
stdin.setRawMode(wasRaw ?? false);
|
|
1381
|
-
}
|
|
1382
|
-
process.stderr.write("\n");
|
|
1383
|
-
rl.close();
|
|
1384
|
-
resolve(input);
|
|
1385
|
-
} else if (char === "") {
|
|
1386
|
-
process.exit(1);
|
|
1387
|
-
} else if (char === "\x7F" || char === "\b") {
|
|
1388
|
-
input = input.slice(0, -1);
|
|
1389
|
-
} else {
|
|
1390
|
-
input += char;
|
|
1391
|
-
}
|
|
1392
|
-
};
|
|
1393
|
-
stdin.on("data", onData);
|
|
1394
|
-
});
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
// src/cli/commands.ts
|
|
1398
|
-
function safeStr(s) {
|
|
1399
|
-
return s == null ? "" : `${s}`;
|
|
1400
|
-
}
|
|
1401
|
-
function safeNum(n) {
|
|
1402
|
-
return n == null ? 0 : Number(n);
|
|
1403
|
-
}
|
|
1404
|
-
function safeArr(arr) {
|
|
1405
|
-
return arr ? arr.map((x) => typeof x === "string" ? safeStr(x) : x) : [];
|
|
1406
|
-
}
|
|
1407
|
-
function buildOpts(cmd) {
|
|
1408
|
-
let scope;
|
|
1409
|
-
if (cmd.global) scope = "global";
|
|
1410
|
-
else if (cmd.project) scope = "project";
|
|
1411
|
-
else if (cmd.team) scope = "team";
|
|
1412
|
-
else if (cmd.org) scope = "org";
|
|
1413
|
-
const projectPath = cmd.projectPath ?? (cmd.project ? process.cwd() : void 0);
|
|
1414
|
-
if (scope === "project" && !projectPath) {
|
|
1415
|
-
throw new Error("Project path is required for project scope");
|
|
1416
|
-
}
|
|
1417
|
-
return {
|
|
1418
|
-
scope,
|
|
1419
|
-
projectPath: projectPath ?? process.cwd(),
|
|
1420
|
-
teamId: cmd.team,
|
|
1421
|
-
orgId: cmd.org,
|
|
1422
|
-
env: cmd.env,
|
|
1423
|
-
source: "cli"
|
|
1424
|
-
};
|
|
1425
|
-
}
|
|
1426
|
-
function createProgram() {
|
|
1427
|
-
const program2 = new Command().name("qring").description(
|
|
1428
|
-
`${c.bold("q-ring")} ${c.dim("\u2014 quantum keyring for AI coding tools")}`
|
|
1429
|
-
).version("0.4.0");
|
|
1430
|
-
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) => {
|
|
906
|
+
// src/cli/commands/project.ts
|
|
907
|
+
import { writeFileSync, readFileSync as readFileSync2, existsSync } from "fs";
|
|
908
|
+
function registerProjectCommands(program2) {
|
|
909
|
+
program2.command("context").alias("describe").description(
|
|
910
|
+
"Show safe, redacted project context for AI agents (no secret values exposed)"
|
|
911
|
+
).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) => {
|
|
1431
912
|
const opts = buildOpts(cmd);
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
process.exit(1);
|
|
1437
|
-
}
|
|
913
|
+
const context = getProjectContext(opts);
|
|
914
|
+
if (wantsJsonOutput(program2, cmd)) {
|
|
915
|
+
console.log(JSON.stringify(context, null, 2));
|
|
916
|
+
return;
|
|
1438
917
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
description: cmd.description,
|
|
1444
|
-
tags: cmd.tags?.split(",").map((t) => t.trim()),
|
|
1445
|
-
rotationFormat: cmd.rotationFormat,
|
|
1446
|
-
rotationPrefix: cmd.rotationPrefix,
|
|
1447
|
-
requiresApproval: cmd.requiresApproval,
|
|
1448
|
-
jitProvider: cmd.jitProvider
|
|
1449
|
-
};
|
|
1450
|
-
if (cmd.env) {
|
|
1451
|
-
const existing = getEnvelope(key, opts);
|
|
1452
|
-
const states = existing?.envelope?.states ?? {};
|
|
1453
|
-
states[cmd.env] = value;
|
|
1454
|
-
if (existing?.envelope?.value && !states["default"]) {
|
|
1455
|
-
states["default"] = existing.envelope.value;
|
|
1456
|
-
}
|
|
1457
|
-
setOpts.states = states;
|
|
1458
|
-
setOpts.defaultEnv = existing?.envelope?.defaultEnv ?? cmd.env;
|
|
1459
|
-
setSecret(key, "", setOpts);
|
|
918
|
+
console.log(`
|
|
919
|
+
${SYMBOLS.zap} ${c.bold("Project Context for AI Assistant")}`);
|
|
920
|
+
console.log(` Project: ${c.cyan(context.projectPath)}`);
|
|
921
|
+
if (context.environment) {
|
|
1460
922
|
console.log(
|
|
1461
|
-
|
|
923
|
+
` Environment: ${envBadge(context.environment.env)} ${c.dim(`(${context.environment.source})`)}`
|
|
1462
924
|
);
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
925
|
+
}
|
|
926
|
+
console.log(
|
|
927
|
+
`
|
|
928
|
+
${c.bold(" Secrets:")} ${context.totalSecrets} total ${c.dim(`(${context.expiredCount} expired, ${context.staleCount} stale, ${context.protectedCount} protected)`)}`
|
|
929
|
+
);
|
|
930
|
+
for (const s of context.secrets) {
|
|
931
|
+
const tags = s.tags?.length ? c.dim(` [${s.tags.join(",")}]`) : "";
|
|
932
|
+
const flags = [];
|
|
933
|
+
if (s.requiresApproval) flags.push(c.yellow("locked"));
|
|
934
|
+
if (s.jitProvider) flags.push(c.magenta("jit"));
|
|
935
|
+
if (s.hasStates) flags.push(c.blue("superposition"));
|
|
936
|
+
if (s.isExpired) flags.push(c.red("expired"));
|
|
937
|
+
else if (s.isStale) flags.push(c.yellow("stale"));
|
|
938
|
+
const flagStr = flags.length ? ` ${flags.join(" ")}` : "";
|
|
1468
939
|
console.log(
|
|
1469
|
-
|
|
940
|
+
` ${c.bold(s.key)} ${scopeColor(s.scope)}${tags}${flagStr}`
|
|
1470
941
|
);
|
|
1471
942
|
}
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
if (value === null) {
|
|
1477
|
-
console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
|
|
1478
|
-
process.exit(1);
|
|
1479
|
-
}
|
|
1480
|
-
process.stdout.write(value);
|
|
1481
|
-
});
|
|
1482
|
-
program2.command("delete <key>").alias("rm").description("Remove a secret from the keyring").option("-g, --global", "Delete from global scope only").option("-p, --project", "Delete from project scope only").option("--project-path <path>", "Explicit project path").action((key, cmd) => {
|
|
1483
|
-
const opts = buildOpts(cmd);
|
|
1484
|
-
const deleted = deleteSecret(key, opts);
|
|
1485
|
-
if (deleted) {
|
|
1486
|
-
console.log(`${SYMBOLS.check} ${c.green("deleted")} ${c.bold(key)}`);
|
|
1487
|
-
} else {
|
|
1488
|
-
console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
|
|
1489
|
-
process.exit(1);
|
|
1490
|
-
}
|
|
1491
|
-
});
|
|
1492
|
-
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) => {
|
|
1493
|
-
const opts = buildOpts(cmd);
|
|
1494
|
-
let entries = listSecrets(opts);
|
|
1495
|
-
if (cmd.tag) {
|
|
1496
|
-
entries = entries.filter(
|
|
1497
|
-
(e) => e.envelope?.meta.tags?.includes(cmd.tag)
|
|
943
|
+
if (context.manifest) {
|
|
944
|
+
console.log(
|
|
945
|
+
`
|
|
946
|
+
${c.bold(" Manifest:")} ${context.manifest.declared} declared`
|
|
1498
947
|
);
|
|
948
|
+
if (context.manifest.missing.length > 0) {
|
|
949
|
+
console.log(
|
|
950
|
+
` ${c.red("Missing:")} ${context.manifest.missing.join(", ")}`
|
|
951
|
+
);
|
|
952
|
+
} else {
|
|
953
|
+
console.log(
|
|
954
|
+
` ${c.green(SYMBOLS.check)} All manifest secrets present`
|
|
955
|
+
);
|
|
956
|
+
}
|
|
1499
957
|
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
958
|
+
console.log(
|
|
959
|
+
`
|
|
960
|
+
${c.bold(" Providers:")} ${context.validationProviders.join(", ") || "none"}`
|
|
961
|
+
);
|
|
962
|
+
console.log(
|
|
963
|
+
`${c.bold(" JIT Providers:")} ${context.jitProviders.join(", ") || "none"}`
|
|
964
|
+
);
|
|
965
|
+
console.log(`${c.bold(" Hooks:")} ${context.hooksCount} registered`);
|
|
966
|
+
if (context.recentActions.length > 0) {
|
|
967
|
+
console.log(
|
|
968
|
+
`
|
|
969
|
+
${c.bold(" Recent Activity:")} (last ${context.recentActions.length})`
|
|
1506
970
|
);
|
|
971
|
+
for (const a of context.recentActions.slice(0, 8)) {
|
|
972
|
+
const ts = new Date(a.timestamp).toLocaleTimeString();
|
|
973
|
+
console.log(
|
|
974
|
+
` ${c.dim(ts)} ${a.action}${a.key ? ` ${c.bold(a.key)}` : ""} ${c.dim(`(${a.source})`)}`
|
|
975
|
+
);
|
|
976
|
+
}
|
|
1507
977
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
978
|
+
console.log();
|
|
979
|
+
});
|
|
980
|
+
program2.command("check").description("Validate project secrets against .q-ring.json manifest").option("--project-path <path>", "Project path (defaults to cwd)").action((cmd) => {
|
|
981
|
+
const projectPath = cmd.projectPath ?? process.cwd();
|
|
982
|
+
const config = readProjectConfig(projectPath);
|
|
983
|
+
if (!config?.secrets || Object.keys(config.secrets).length === 0) {
|
|
984
|
+
console.error(
|
|
985
|
+
c.red(`${SYMBOLS.cross} No secrets manifest found in .q-ring.json`)
|
|
986
|
+
);
|
|
987
|
+
console.log(
|
|
988
|
+
c.dim(
|
|
989
|
+
' Add a "secrets" field to your .q-ring.json to define required secrets.'
|
|
990
|
+
)
|
|
1512
991
|
);
|
|
1513
|
-
|
|
1514
|
-
}
|
|
1515
|
-
if (entries.length === 0) {
|
|
1516
|
-
console.log(c.dim("No secrets found"));
|
|
1517
|
-
return;
|
|
1518
|
-
}
|
|
1519
|
-
console.log(
|
|
1520
|
-
c.bold(`
|
|
1521
|
-
${SYMBOLS.key} q-ring secrets (${entries.length})
|
|
1522
|
-
`)
|
|
1523
|
-
);
|
|
1524
|
-
const maxKeyLen = Math.max(...entries.map((e) => e.key.length));
|
|
1525
|
-
for (const entry of entries) {
|
|
1526
|
-
const parts = [];
|
|
1527
|
-
const key = safeStr(entry.key);
|
|
1528
|
-
const scope = safeStr(entry.scope);
|
|
1529
|
-
const envs = entry.envelope?.states ? Object.keys(entry.envelope.states).map(safeStr) : null;
|
|
1530
|
-
const entangledCount = safeNum(entry.envelope?.meta.entangled?.length);
|
|
1531
|
-
const accessCount = safeNum(entry.envelope?.meta.accessCount);
|
|
1532
|
-
const tags = safeArr(entry.envelope?.meta.tags);
|
|
1533
|
-
const decayPct = safeNum(entry.decay?.lifetimePercent);
|
|
1534
|
-
const expired = !!entry.decay?.isExpired;
|
|
1535
|
-
const timeLeft = safeStr(entry.decay?.timeRemaining);
|
|
1536
|
-
parts.push(c.dim("[") + scopeColor(scope) + c.dim("]"));
|
|
1537
|
-
parts.push(c.bold(key.padEnd(maxKeyLen)));
|
|
1538
|
-
if (envs) {
|
|
1539
|
-
parts.push(c.magenta(`[${envs.join("|")}]`));
|
|
1540
|
-
}
|
|
1541
|
-
if (entry.decay && (decayPct > 0 || expired)) {
|
|
1542
|
-
parts.push(decayIndicator(decayPct, expired));
|
|
1543
|
-
if (timeLeft && !expired) {
|
|
1544
|
-
parts.push(c.dim(timeLeft));
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
if (entangledCount > 0) {
|
|
1548
|
-
parts.push(c.cyan(`${SYMBOLS.link} ${entangledCount}`));
|
|
1549
|
-
}
|
|
1550
|
-
if (accessCount > 0) {
|
|
1551
|
-
parts.push(c.dim(`${SYMBOLS.eye} ${accessCount}`));
|
|
1552
|
-
}
|
|
1553
|
-
if (tags.length > 0) {
|
|
1554
|
-
parts.push(c.dim(tags.map((t) => `#${t}`).join(" ")));
|
|
1555
|
-
}
|
|
1556
|
-
console.log(` ${parts.join(" ")}`);
|
|
1557
|
-
}
|
|
1558
|
-
console.log();
|
|
1559
|
-
});
|
|
1560
|
-
program2.command("inspect <key>").description("Show full quantum state of a secret").option("-g, --global", "Inspect global scope only").option("-p, --project", "Inspect project scope only").option("--project-path <path>", "Explicit project path").action((key, cmd) => {
|
|
1561
|
-
const opts = buildOpts(cmd);
|
|
1562
|
-
const result = getEnvelope(key, opts);
|
|
1563
|
-
if (!result) {
|
|
1564
|
-
console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
|
|
1565
|
-
process.exit(1);
|
|
1566
|
-
}
|
|
1567
|
-
const { envelope, scope } = result;
|
|
1568
|
-
const decay = checkDecay(envelope);
|
|
1569
|
-
const safeScope = safeStr(scope);
|
|
1570
|
-
const createdAt = safeStr(envelope.meta.createdAt);
|
|
1571
|
-
const updatedAt = safeStr(envelope.meta.updatedAt);
|
|
1572
|
-
const accessCount = safeNum(envelope.meta.accessCount);
|
|
1573
|
-
const lastAccess = safeStr(envelope.meta.lastAccessedAt);
|
|
1574
|
-
const desc = safeStr(envelope.meta.description);
|
|
1575
|
-
const tags = safeArr(envelope.meta.tags);
|
|
1576
|
-
const entangled = (envelope.meta.entangled ?? []).map((l) => ({
|
|
1577
|
-
service: safeStr(l.service),
|
|
1578
|
-
key: safeStr(l.key)
|
|
1579
|
-
}));
|
|
1580
|
-
const stateEnvs = envelope.states ? Object.keys(envelope.states).map(safeStr) : null;
|
|
1581
|
-
const defaultEnv = safeStr(envelope.defaultEnv);
|
|
1582
|
-
const decayTime = safeStr(decay.timeRemaining);
|
|
1583
|
-
const decayPct = safeNum(decay.lifetimePercent);
|
|
1584
|
-
const expired = !!decay.isExpired;
|
|
1585
|
-
console.log(`
|
|
1586
|
-
${c.bold(SYMBOLS.key + " " + key)}`);
|
|
1587
|
-
console.log(` ${c.dim("scope:")} ${scopeColor(safeScope)}`);
|
|
1588
|
-
if (stateEnvs) {
|
|
1589
|
-
console.log(` ${c.dim("type:")} ${c.magenta("superposition")}`);
|
|
1590
|
-
console.log(` ${c.dim("states:")}`);
|
|
1591
|
-
for (const env of stateEnvs) {
|
|
1592
|
-
const isDefault = env === defaultEnv;
|
|
1593
|
-
console.log(
|
|
1594
|
-
` ${envBadge(env)} ${isDefault ? c.dim("(default)") : ""}`
|
|
1595
|
-
);
|
|
1596
|
-
}
|
|
1597
|
-
} else {
|
|
1598
|
-
console.log(` ${c.dim("type:")} ${c.green("collapsed")}`);
|
|
1599
|
-
}
|
|
1600
|
-
console.log(` ${c.dim("created:")} ${createdAt}`);
|
|
1601
|
-
console.log(` ${c.dim("updated:")} ${updatedAt}`);
|
|
1602
|
-
console.log(` ${c.dim("accessed:")} ${accessCount} times`);
|
|
1603
|
-
if (lastAccess) {
|
|
1604
|
-
console.log(` ${c.dim("last read:")} ${lastAccess}`);
|
|
1605
|
-
}
|
|
1606
|
-
if (desc) {
|
|
1607
|
-
console.log(` ${c.dim("desc:")} ${desc}`);
|
|
1608
|
-
}
|
|
1609
|
-
if (tags.length > 0) {
|
|
1610
|
-
console.log(
|
|
1611
|
-
` ${c.dim("tags:")} ${tags.map((t) => c.cyan(`#${t}`)).join(" ")}`
|
|
1612
|
-
);
|
|
1613
|
-
}
|
|
1614
|
-
if (decayTime) {
|
|
1615
|
-
console.log(
|
|
1616
|
-
` ${c.dim("decay:")} ${decayIndicator(decayPct, expired)} ${decayTime}`
|
|
1617
|
-
);
|
|
1618
|
-
}
|
|
1619
|
-
if (entangled.length > 0) {
|
|
1620
|
-
console.log(` ${c.dim("entangled:")}`);
|
|
1621
|
-
for (const link of entangled) {
|
|
1622
|
-
console.log(` ${SYMBOLS.link} ${link.service}/${link.key}`);
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
console.log();
|
|
1626
|
-
});
|
|
1627
|
-
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) => {
|
|
1628
|
-
const opts = buildOpts(cmd);
|
|
1629
|
-
const output = exportSecrets({
|
|
1630
|
-
...opts,
|
|
1631
|
-
format: cmd.format,
|
|
1632
|
-
keys: cmd.keys?.split(",").map((k) => k.trim()),
|
|
1633
|
-
tags: cmd.tags?.split(",").map((t) => t.trim())
|
|
1634
|
-
});
|
|
1635
|
-
process.stdout.write(output + "\n");
|
|
1636
|
-
});
|
|
1637
|
-
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) => {
|
|
1638
|
-
const opts = buildOpts(cmd);
|
|
1639
|
-
const result = importDotenv(file, {
|
|
1640
|
-
scope: opts.scope,
|
|
1641
|
-
projectPath: opts.projectPath,
|
|
1642
|
-
source: "cli",
|
|
1643
|
-
skipExisting: cmd.skipExisting,
|
|
1644
|
-
dryRun: cmd.dryRun
|
|
1645
|
-
});
|
|
1646
|
-
if (cmd.dryRun) {
|
|
1647
|
-
console.log(
|
|
1648
|
-
`
|
|
1649
|
-
${SYMBOLS.package} ${c.bold("Dry run")} \u2014 would import ${result.imported.length} of ${result.total} secrets:
|
|
1650
|
-
`
|
|
1651
|
-
);
|
|
1652
|
-
for (const key of result.imported) {
|
|
1653
|
-
console.log(` ${SYMBOLS.key} ${c.bold(key)}`);
|
|
1654
|
-
}
|
|
1655
|
-
if (result.skipped.length > 0) {
|
|
1656
|
-
console.log(`
|
|
1657
|
-
${c.dim(`Skipped (existing): ${result.skipped.join(", ")}`)}`);
|
|
1658
|
-
}
|
|
1659
|
-
} else {
|
|
1660
|
-
console.log(
|
|
1661
|
-
`${SYMBOLS.check} ${c.green("imported")} ${result.imported.length} secret(s) from ${c.bold(file)}`
|
|
1662
|
-
);
|
|
1663
|
-
if (result.skipped.length > 0) {
|
|
1664
|
-
console.log(
|
|
1665
|
-
c.dim(` skipped ${result.skipped.length} existing: ${result.skipped.join(", ")}`)
|
|
1666
|
-
);
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
console.log();
|
|
1670
|
-
});
|
|
1671
|
-
program2.command("check").description("Validate project secrets against .q-ring.json manifest").option("--project-path <path>", "Project path (defaults to cwd)").action((cmd) => {
|
|
1672
|
-
const projectPath = cmd.projectPath ?? process.cwd();
|
|
1673
|
-
const config = readProjectConfig(projectPath);
|
|
1674
|
-
if (!config?.secrets || Object.keys(config.secrets).length === 0) {
|
|
1675
|
-
console.error(
|
|
1676
|
-
c.red(`${SYMBOLS.cross} No secrets manifest found in .q-ring.json`)
|
|
1677
|
-
);
|
|
1678
|
-
console.log(
|
|
1679
|
-
c.dim(' Add a "secrets" field to your .q-ring.json to define required secrets.')
|
|
1680
|
-
);
|
|
1681
|
-
process.exit(1);
|
|
992
|
+
process.exit(1);
|
|
1682
993
|
}
|
|
1683
994
|
console.log(
|
|
1684
995
|
c.bold(`
|
|
@@ -1746,313 +1057,93 @@ function createProgram() {
|
|
|
1746
1057
|
console.log();
|
|
1747
1058
|
if (missing > 0) process.exit(1);
|
|
1748
1059
|
});
|
|
1749
|
-
program2.command("
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
console.log(
|
|
1759
|
-
|
|
1060
|
+
program2.command("env").description("Show detected environment (wavefunction collapse context)").option("--project-path <path>", "Project path for detection").action((cmd) => {
|
|
1061
|
+
const result = collapseEnvironment({
|
|
1062
|
+
projectPath: cmd.projectPath ?? process.cwd()
|
|
1063
|
+
});
|
|
1064
|
+
if (result) {
|
|
1065
|
+
console.log(
|
|
1066
|
+
`${SYMBOLS.zap} ${c.bold("Collapsed environment:")} ${envBadge(result.env)} ${c.dim(`(source: ${result.source})`)}`
|
|
1067
|
+
);
|
|
1068
|
+
} else {
|
|
1069
|
+
console.log(
|
|
1070
|
+
c.dim(
|
|
1071
|
+
"No environment detected. Set QRING_ENV, NODE_ENV, or create .q-ring.json"
|
|
1072
|
+
)
|
|
1073
|
+
);
|
|
1760
1074
|
}
|
|
1761
|
-
|
|
1762
|
-
|
|
1075
|
+
});
|
|
1076
|
+
program2.command("env:generate").description(
|
|
1077
|
+
"Generate a .env file from the project manifest (.q-ring.json)"
|
|
1078
|
+
).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) => {
|
|
1079
|
+
const projectPath = cmd.projectPath ?? process.cwd();
|
|
1080
|
+
const config = readProjectConfig(projectPath);
|
|
1081
|
+
if (!config?.secrets || Object.keys(config.secrets).length === 0) {
|
|
1082
|
+
console.error(
|
|
1083
|
+
c.red(`${SYMBOLS.cross} No secrets manifest found in .q-ring.json`)
|
|
1084
|
+
);
|
|
1763
1085
|
process.exit(1);
|
|
1764
1086
|
}
|
|
1765
1087
|
const opts = buildOpts(cmd);
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
if (
|
|
1772
|
-
|
|
1773
|
-
entries = entries.filter((e) => manifestKeys.has(e.key));
|
|
1088
|
+
const lines = [];
|
|
1089
|
+
const warnings = [];
|
|
1090
|
+
for (const [key, manifest] of Object.entries(config.secrets)) {
|
|
1091
|
+
const value = getSecret(key, { ...opts, projectPath, source: "cli" });
|
|
1092
|
+
if (value === null) {
|
|
1093
|
+
if (manifest.required !== false) {
|
|
1094
|
+
warnings.push(`MISSING (required): ${key}`);
|
|
1774
1095
|
}
|
|
1096
|
+
lines.push(
|
|
1097
|
+
`# ${key}= ${manifest.description ? `# ${manifest.description}` : ""}`
|
|
1098
|
+
);
|
|
1099
|
+
continue;
|
|
1775
1100
|
}
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
if (!value2) {
|
|
1784
|
-
skipped++;
|
|
1785
|
-
continue;
|
|
1786
|
-
}
|
|
1787
|
-
const provHint2 = entry.envelope?.meta.provider ?? cmd.provider;
|
|
1788
|
-
const result2 = await validateSecret(value2, { provider: provHint2 });
|
|
1789
|
-
if (result2.status === "unknown") {
|
|
1790
|
-
skipped++;
|
|
1791
|
-
continue;
|
|
1101
|
+
const result = getEnvelope(key, { projectPath, source: "cli" });
|
|
1102
|
+
if (result) {
|
|
1103
|
+
const decay = checkDecay(result.envelope);
|
|
1104
|
+
if (decay.isExpired) {
|
|
1105
|
+
warnings.push(`EXPIRED: ${key}`);
|
|
1106
|
+
} else if (decay.isStale) {
|
|
1107
|
+
warnings.push(`STALE (${decay.lifetimePercent}%): ${key}`);
|
|
1792
1108
|
}
|
|
1793
|
-
validated++;
|
|
1794
|
-
const icon2 = result2.status === "valid" ? c.green(SYMBOLS.check) : result2.status === "invalid" ? c.red(SYMBOLS.cross) : c.yellow(SYMBOLS.warning);
|
|
1795
|
-
const statusText = result2.status === "valid" ? c.green("valid") : result2.status === "invalid" ? c.red("invalid") : c.yellow("error");
|
|
1796
|
-
console.log(
|
|
1797
|
-
` ${icon2} ${c.bold(entry.key.padEnd(24))} ${statusText} ${c.dim(`(${result2.provider}, ${result2.latencyMs}ms)`)}${result2.status !== "valid" ? ` ${c.dim("\u2014 " + result2.message)}` : ""}`
|
|
1798
|
-
);
|
|
1799
1109
|
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
`);
|
|
1803
|
-
return;
|
|
1110
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
1111
|
+
lines.push(`${key}="${escaped}"`);
|
|
1804
1112
|
}
|
|
1805
|
-
const
|
|
1806
|
-
if (
|
|
1807
|
-
|
|
1808
|
-
|
|
1113
|
+
const output = lines.join("\n") + "\n";
|
|
1114
|
+
if (cmd.output) {
|
|
1115
|
+
writeFileSync(cmd.output, output);
|
|
1116
|
+
console.log(
|
|
1117
|
+
`${SYMBOLS.check} ${c.green("generated")} ${c.bold(cmd.output)} (${Object.keys(config.secrets).length} keys)`
|
|
1118
|
+
);
|
|
1119
|
+
} else {
|
|
1120
|
+
process.stdout.write(output);
|
|
1809
1121
|
}
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
console.log(`
|
|
1815
|
-
${icon} ${c.bold(key)} ${result.status} ${c.dim(`(${result.provider}, ${result.latencyMs}ms)`)}`);
|
|
1816
|
-
if (result.message && result.status !== "valid") {
|
|
1817
|
-
console.log(` ${c.dim(result.message)}`);
|
|
1818
|
-
}
|
|
1819
|
-
console.log();
|
|
1820
|
-
});
|
|
1821
|
-
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) => {
|
|
1822
|
-
const opts = buildOpts(cmd);
|
|
1823
|
-
const command = commandArgs[0];
|
|
1824
|
-
const args = commandArgs.slice(1);
|
|
1825
|
-
try {
|
|
1826
|
-
const { code } = await execCommand({
|
|
1827
|
-
...opts,
|
|
1828
|
-
command,
|
|
1829
|
-
args,
|
|
1830
|
-
keys: cmd.keys?.split(",").map((k) => k.trim()),
|
|
1831
|
-
tags: cmd.tags?.split(",").map((t) => t.trim()),
|
|
1832
|
-
profile: cmd.profile
|
|
1833
|
-
});
|
|
1834
|
-
process.exit(code);
|
|
1835
|
-
} catch (err) {
|
|
1836
|
-
console.error(c.red(`${SYMBOLS.cross} Exec failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
1837
|
-
process.exit(1);
|
|
1838
|
-
}
|
|
1839
|
-
});
|
|
1840
|
-
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) => {
|
|
1841
|
-
const targetDir = dir ?? process.cwd();
|
|
1842
|
-
const fixMode = cmd.fix === true;
|
|
1843
|
-
console.log(`
|
|
1844
|
-
${SYMBOLS.eye} Scanning ${c.bold(targetDir)} for secrets...${fixMode ? c.yellow(" [--fix mode]") : ""}
|
|
1845
|
-
`);
|
|
1846
|
-
const results = scanCodebase(targetDir);
|
|
1847
|
-
if (results.length === 0) {
|
|
1848
|
-
console.log(` ${c.green(SYMBOLS.check)} No hardcoded secrets found. Awesome!
|
|
1849
|
-
`);
|
|
1850
|
-
return;
|
|
1851
|
-
}
|
|
1852
|
-
if (fixMode) {
|
|
1853
|
-
const fileSet = new Set(results.map((r) => r.file.startsWith("/") ? r.file : `${targetDir}/${r.file}`));
|
|
1854
|
-
const opts = buildOpts(cmd);
|
|
1855
|
-
const lintResults = lintFiles([...fileSet], { fix: true, scope: opts.scope, projectPath: opts.projectPath });
|
|
1856
|
-
const fixedCount = lintResults.filter((r) => r.fixed).length;
|
|
1857
|
-
console.log(` ${c.green(SYMBOLS.check)} Fixed ${fixedCount} secrets \u2014 replaced with process.env references and stored in q-ring.
|
|
1858
|
-
`);
|
|
1859
|
-
return;
|
|
1860
|
-
}
|
|
1861
|
-
for (const res of results) {
|
|
1862
|
-
console.log(` ${c.red(SYMBOLS.cross)} ${c.bold(res.file)}:${res.line}`);
|
|
1863
|
-
console.log(` ${c.dim("Key:")} ${c.cyan(res.keyName)}`);
|
|
1864
|
-
console.log(` ${c.dim("Entropy:")} ${res.entropy > 4 ? c.red(res.entropy.toString()) : c.yellow(res.entropy.toString())}`);
|
|
1865
|
-
console.log(` ${c.dim("Context:")} ${res.context}`);
|
|
1866
|
-
console.log();
|
|
1867
|
-
}
|
|
1868
|
-
console.log(` ${c.red(`Found ${results.length} potential secrets.`)} Use ${c.bold("qring scan --fix")} to auto-migrate them.
|
|
1869
|
-
`);
|
|
1870
|
-
});
|
|
1871
|
-
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) => {
|
|
1872
|
-
const opts = buildOpts(cmd);
|
|
1873
|
-
const results = lintFiles(files, { fix: cmd.fix, scope: opts.scope, projectPath: opts.projectPath });
|
|
1874
|
-
if (results.length === 0) {
|
|
1875
|
-
console.log(`
|
|
1876
|
-
${c.green(SYMBOLS.check)} No hardcoded secrets found in ${files.length} file(s).
|
|
1877
|
-
`);
|
|
1878
|
-
return;
|
|
1879
|
-
}
|
|
1880
|
-
for (const res of results) {
|
|
1881
|
-
const status = res.fixed ? c.green("fixed") : c.red("found");
|
|
1882
|
-
console.log(` ${res.fixed ? c.green(SYMBOLS.check) : c.red(SYMBOLS.cross)} ${c.bold(res.file)}:${res.line} [${status}]`);
|
|
1883
|
-
console.log(` ${c.dim("Key:")} ${c.cyan(res.keyName)}`);
|
|
1884
|
-
console.log(` ${c.dim("Entropy:")} ${res.entropy > 4 ? c.red(res.entropy.toString()) : c.yellow(res.entropy.toString())}`);
|
|
1885
|
-
console.log();
|
|
1886
|
-
}
|
|
1887
|
-
const fixedCount = results.filter((r) => r.fixed).length;
|
|
1888
|
-
if (cmd.fix && fixedCount > 0) {
|
|
1889
|
-
console.log(` ${c.green(`Fixed ${fixedCount} secret(s)`)} \u2014 replaced with env references and stored in q-ring.
|
|
1890
|
-
`);
|
|
1891
|
-
} else {
|
|
1892
|
-
console.log(` ${c.red(`Found ${results.length} potential secret(s).`)} Use ${c.bold("--fix")} to auto-migrate.
|
|
1893
|
-
`);
|
|
1894
|
-
}
|
|
1895
|
-
});
|
|
1896
|
-
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) => {
|
|
1897
|
-
const opts = buildOpts(cmd);
|
|
1898
|
-
const context = getProjectContext(opts);
|
|
1899
|
-
if (cmd.json) {
|
|
1900
|
-
console.log(JSON.stringify(context, null, 2));
|
|
1901
|
-
return;
|
|
1902
|
-
}
|
|
1903
|
-
console.log(`
|
|
1904
|
-
${SYMBOLS.zap} ${c.bold("Project Context for AI Assistant")}`);
|
|
1905
|
-
console.log(` Project: ${c.cyan(context.projectPath)}`);
|
|
1906
|
-
if (context.environment) {
|
|
1907
|
-
console.log(` Environment: ${envBadge(context.environment.env)} ${c.dim(`(${context.environment.source})`)}`);
|
|
1908
|
-
}
|
|
1909
|
-
console.log(`
|
|
1910
|
-
${c.bold(" Secrets:")} ${context.totalSecrets} total ${c.dim(`(${context.expiredCount} expired, ${context.staleCount} stale, ${context.protectedCount} protected)`)}`);
|
|
1911
|
-
for (const s of context.secrets) {
|
|
1912
|
-
const tags = s.tags?.length ? c.dim(` [${s.tags.join(",")}]`) : "";
|
|
1913
|
-
const flags = [];
|
|
1914
|
-
if (s.requiresApproval) flags.push(c.yellow("locked"));
|
|
1915
|
-
if (s.jitProvider) flags.push(c.magenta("jit"));
|
|
1916
|
-
if (s.hasStates) flags.push(c.blue("superposition"));
|
|
1917
|
-
if (s.isExpired) flags.push(c.red("expired"));
|
|
1918
|
-
else if (s.isStale) flags.push(c.yellow("stale"));
|
|
1919
|
-
const flagStr = flags.length ? ` ${flags.join(" ")}` : "";
|
|
1920
|
-
console.log(` ${c.bold(s.key)} ${scopeColor(s.scope)}${tags}${flagStr}`);
|
|
1921
|
-
}
|
|
1922
|
-
if (context.manifest) {
|
|
1923
|
-
console.log(`
|
|
1924
|
-
${c.bold(" Manifest:")} ${context.manifest.declared} declared`);
|
|
1925
|
-
if (context.manifest.missing.length > 0) {
|
|
1926
|
-
console.log(` ${c.red("Missing:")} ${context.manifest.missing.join(", ")}`);
|
|
1927
|
-
} else {
|
|
1928
|
-
console.log(` ${c.green(SYMBOLS.check)} All manifest secrets present`);
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
|
-
console.log(`
|
|
1932
|
-
${c.bold(" Providers:")} ${context.validationProviders.join(", ") || "none"}`);
|
|
1933
|
-
console.log(`${c.bold(" JIT Providers:")} ${context.jitProviders.join(", ") || "none"}`);
|
|
1934
|
-
console.log(`${c.bold(" Hooks:")} ${context.hooksCount} registered`);
|
|
1935
|
-
if (context.recentActions.length > 0) {
|
|
1936
|
-
console.log(`
|
|
1937
|
-
${c.bold(" Recent Activity:")} (last ${context.recentActions.length})`);
|
|
1938
|
-
for (const a of context.recentActions.slice(0, 8)) {
|
|
1939
|
-
const ts = new Date(a.timestamp).toLocaleTimeString();
|
|
1940
|
-
console.log(` ${c.dim(ts)} ${a.action}${a.key ? ` ${c.bold(a.key)}` : ""} ${c.dim(`(${a.source})`)}`);
|
|
1941
|
-
}
|
|
1942
|
-
}
|
|
1943
|
-
console.log();
|
|
1944
|
-
});
|
|
1945
|
-
program2.command("remember <key> <value>").description("Store a key-value pair in encrypted agent memory (persists across sessions)").action((key, value) => {
|
|
1946
|
-
remember(key, value);
|
|
1947
|
-
console.log(`${SYMBOLS.check} ${c.green("remembered")} ${c.bold(key)}`);
|
|
1948
|
-
});
|
|
1949
|
-
program2.command("recall [key]").description("Retrieve a value from agent memory, or list all keys").action((key) => {
|
|
1950
|
-
if (!key) {
|
|
1951
|
-
const entries = listMemory();
|
|
1952
|
-
if (entries.length === 0) {
|
|
1953
|
-
console.log(c.dim("Agent memory is empty."));
|
|
1954
|
-
return;
|
|
1955
|
-
}
|
|
1956
|
-
console.log(`
|
|
1957
|
-
${SYMBOLS.zap} ${c.bold("Agent Memory")} (${entries.length} entries)
|
|
1958
|
-
`);
|
|
1959
|
-
for (const e of entries) {
|
|
1960
|
-
console.log(` ${c.bold(e.key)} ${c.dim(new Date(e.updatedAt).toLocaleString())}`);
|
|
1961
|
-
}
|
|
1962
|
-
console.log();
|
|
1963
|
-
return;
|
|
1964
|
-
}
|
|
1965
|
-
const value = recall(key);
|
|
1966
|
-
if (value === null) {
|
|
1967
|
-
console.log(c.dim(`No memory found for "${key}"`));
|
|
1968
|
-
} else {
|
|
1969
|
-
console.log(safeStr(value));
|
|
1970
|
-
}
|
|
1971
|
-
});
|
|
1972
|
-
program2.command("forget <key>").description("Delete a key from agent memory").option("--all", "Clear all agent memory").action((key, cmd) => {
|
|
1973
|
-
if (cmd.all) {
|
|
1974
|
-
clearMemory();
|
|
1975
|
-
console.log(`${SYMBOLS.check} ${c.yellow("cleared")} all agent memory`);
|
|
1976
|
-
return;
|
|
1977
|
-
}
|
|
1978
|
-
const removed = forget(key);
|
|
1979
|
-
if (removed) {
|
|
1980
|
-
console.log(`${SYMBOLS.check} ${c.yellow("forgot")} ${c.bold(key)}`);
|
|
1981
|
-
} else {
|
|
1982
|
-
console.log(c.dim(`No memory found for "${key}"`));
|
|
1983
|
-
}
|
|
1984
|
-
});
|
|
1985
|
-
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) => {
|
|
1986
|
-
const opts = buildOpts(cmd);
|
|
1987
|
-
const scope = opts.scope ?? "global";
|
|
1988
|
-
if (cmd.list) {
|
|
1989
|
-
const approvals = listApprovals();
|
|
1990
|
-
if (approvals.length === 0) {
|
|
1991
|
-
console.log(c.dim(" No active approvals"));
|
|
1992
|
-
return;
|
|
1993
|
-
}
|
|
1994
|
-
for (const a of approvals) {
|
|
1995
|
-
const status = a.tampered ? c.red("TAMPERED") : a.valid ? c.green("active") : c.dim("expired");
|
|
1996
|
-
const ttl = Math.max(0, Math.round((new Date(a.expiresAt).getTime() - Date.now()) / 1e3));
|
|
1997
|
-
console.log(` ${status} ${c.bold(a.key)} [${a.scope}] reason=${c.dim(a.reason)} ttl=${ttl}s granted-by=${a.grantedBy}`);
|
|
1998
|
-
}
|
|
1999
|
-
return;
|
|
2000
|
-
}
|
|
2001
|
-
if (cmd.revoke) {
|
|
2002
|
-
const revoked = revokeApproval(key, scope);
|
|
2003
|
-
if (revoked) {
|
|
2004
|
-
console.log(`${SYMBOLS.check} ${c.yellow("revoked")} approval for ${c.bold(key)}`);
|
|
2005
|
-
} else {
|
|
2006
|
-
console.log(c.dim(` No active approval found for ${key}`));
|
|
1122
|
+
if (warnings.length > 0 && process.stderr.isTTY) {
|
|
1123
|
+
console.error();
|
|
1124
|
+
for (const w of warnings) {
|
|
1125
|
+
console.error(` ${c.yellow(SYMBOLS.warning)} ${w}`);
|
|
2007
1126
|
}
|
|
2008
|
-
|
|
2009
|
-
}
|
|
2010
|
-
const entry = grantApproval(key, scope, cmd.for, {
|
|
2011
|
-
reason: cmd.reason ?? "manual approval"
|
|
2012
|
-
});
|
|
2013
|
-
console.log(`${SYMBOLS.check} ${c.green("approved")} ${c.bold(key)} for ${cmd.for}s`);
|
|
2014
|
-
console.log(c.dim(` id=${entry.id} reason="${entry.reason}" expires=${entry.expiresAt}`));
|
|
2015
|
-
});
|
|
2016
|
-
program2.command("approvals").description("List all approval tokens with verification status").action(() => {
|
|
2017
|
-
const approvals = listApprovals();
|
|
2018
|
-
if (approvals.length === 0) {
|
|
2019
|
-
console.log(c.dim(" No approvals found"));
|
|
2020
|
-
return;
|
|
2021
|
-
}
|
|
2022
|
-
console.log(c.bold("\n\u{1F510} Approval Tokens\n"));
|
|
2023
|
-
for (const a of approvals) {
|
|
2024
|
-
const status = a.tampered ? c.red(`${SYMBOLS.cross} TAMPERED`) : a.valid ? c.green(`${SYMBOLS.check} active`) : c.dim(`${SYMBOLS.warning} expired`);
|
|
2025
|
-
const ttl = Math.max(0, Math.round((new Date(a.expiresAt).getTime() - Date.now()) / 1e3));
|
|
2026
|
-
console.log(` ${status} ${c.bold(a.key)} [${a.scope}]`);
|
|
2027
|
-
console.log(c.dim(` id=${a.id} reason="${a.reason}" ttl=${ttl}s by=${a.grantedBy}`));
|
|
2028
|
-
if (a.workspace) console.log(c.dim(` workspace=${a.workspace}`));
|
|
2029
|
-
}
|
|
2030
|
-
console.log();
|
|
2031
|
-
});
|
|
2032
|
-
program2.command("hook:install").description("Install a git pre-commit hook that scans for hardcoded secrets").option("--project-path <path>", "Repository path").action((cmd) => {
|
|
2033
|
-
const result = installPreCommitHook(cmd.projectPath);
|
|
2034
|
-
if (result.installed) {
|
|
2035
|
-
console.log(`${SYMBOLS.check} ${c.green(result.message)} at ${c.dim(result.path)}`);
|
|
2036
|
-
} else {
|
|
2037
|
-
console.log(`${SYMBOLS.cross} ${c.red(result.message)}`);
|
|
2038
|
-
}
|
|
2039
|
-
});
|
|
2040
|
-
program2.command("hook:uninstall").description("Remove the q-ring pre-commit hook").option("--project-path <path>", "Repository path").action((cmd) => {
|
|
2041
|
-
const removed = uninstallPreCommitHook(cmd.projectPath);
|
|
2042
|
-
if (removed) {
|
|
2043
|
-
console.log(`${SYMBOLS.check} ${c.green("Pre-commit hook removed")}`);
|
|
2044
|
-
} else {
|
|
2045
|
-
console.log(c.dim("No q-ring pre-commit hook found"));
|
|
1127
|
+
console.error();
|
|
2046
1128
|
}
|
|
2047
1129
|
});
|
|
2048
|
-
program2.command("
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
1130
|
+
program2.command("wizard <name>").description(
|
|
1131
|
+
"Set up a new service integration with secrets, manifest, and hooks"
|
|
1132
|
+
).option(
|
|
1133
|
+
"--keys <keys>",
|
|
1134
|
+
"Comma-separated secret key names to create (e.g. API_KEY,API_SECRET)"
|
|
1135
|
+
).option(
|
|
1136
|
+
"--provider <provider>",
|
|
1137
|
+
"Validation provider (e.g. openai, stripe, github)"
|
|
1138
|
+
).option("--tags <tags>", "Comma-separated tags for all secrets").option(
|
|
1139
|
+
"--hook-exec <cmd>",
|
|
1140
|
+
"Shell command to run when any of these secrets change"
|
|
1141
|
+
).option("-g, --global", "Global scope").option("-p, --project", "Project scope").option("--project-path <path>", "Explicit project path").action(async (name, cmd) => {
|
|
2053
1142
|
const opts = buildOpts(cmd);
|
|
2054
1143
|
const prefix = name.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
2055
|
-
const tags = cmd.tags?.split(",").map((t) => t.trim()) ?? [
|
|
1144
|
+
const tags = cmd.tags?.split(",").map((t) => t.trim()) ?? [
|
|
1145
|
+
name.toLowerCase()
|
|
1146
|
+
];
|
|
2056
1147
|
const provider = cmd.provider;
|
|
2057
1148
|
let keyNames;
|
|
2058
1149
|
if (cmd.keys) {
|
|
@@ -2064,7 +1155,10 @@ ${SYMBOLS.zap} ${c.bold("Agent Memory")} (${entries.length} entries)
|
|
|
2064
1155
|
${SYMBOLS.zap} ${c.bold(`Setting up service: ${name}`)}
|
|
2065
1156
|
`);
|
|
2066
1157
|
for (const key of keyNames) {
|
|
2067
|
-
const value = generateSecret({
|
|
1158
|
+
const value = generateSecret({
|
|
1159
|
+
format: "api-key",
|
|
1160
|
+
prefix: `${prefix.toLowerCase()}_`
|
|
1161
|
+
});
|
|
2068
1162
|
setSecret(key, value, {
|
|
2069
1163
|
...opts,
|
|
2070
1164
|
tags,
|
|
@@ -2077,8 +1171,8 @@ ${SYMBOLS.zap} ${c.bold(`Setting up service: ${name}`)}
|
|
|
2077
1171
|
const manifestPath = `${projectPath}/.q-ring.json`;
|
|
2078
1172
|
let config = {};
|
|
2079
1173
|
try {
|
|
2080
|
-
if (
|
|
2081
|
-
config = JSON.parse(
|
|
1174
|
+
if (existsSync(manifestPath)) {
|
|
1175
|
+
config = JSON.parse(readFileSync2(manifestPath, "utf8"));
|
|
2082
1176
|
}
|
|
2083
1177
|
} catch {
|
|
2084
1178
|
}
|
|
@@ -2090,119 +1184,162 @@ ${SYMBOLS.zap} ${c.bold(`Setting up service: ${name}`)}
|
|
|
2090
1184
|
...provider ? { provider } : {}
|
|
2091
1185
|
};
|
|
2092
1186
|
}
|
|
2093
|
-
|
|
2094
|
-
console.log(
|
|
1187
|
+
writeFileSync(manifestPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
1188
|
+
console.log(
|
|
1189
|
+
` ${c.green(SYMBOLS.check)} Updated ${c.dim(".q-ring.json")} manifest`
|
|
1190
|
+
);
|
|
2095
1191
|
if (cmd.hookExec) {
|
|
2096
1192
|
for (const key of keyNames) {
|
|
2097
1193
|
registerHook({
|
|
2098
1194
|
type: "shell",
|
|
2099
|
-
match: {
|
|
1195
|
+
match: {
|
|
1196
|
+
key,
|
|
1197
|
+
action: ["write", "delete"]
|
|
1198
|
+
},
|
|
2100
1199
|
command: cmd.hookExec,
|
|
2101
1200
|
description: `${name} wizard hook`,
|
|
2102
1201
|
enabled: true
|
|
2103
1202
|
});
|
|
2104
1203
|
}
|
|
2105
|
-
console.log(
|
|
1204
|
+
console.log(
|
|
1205
|
+
` ${c.green(SYMBOLS.check)} Registered hook: ${c.dim(cmd.hookExec)}`
|
|
1206
|
+
);
|
|
2106
1207
|
}
|
|
2107
|
-
console.log(
|
|
1208
|
+
console.log(
|
|
1209
|
+
`
|
|
2108
1210
|
${c.green("Done!")} Service "${name}" is ready with ${keyNames.length} secrets.
|
|
2109
|
-
`
|
|
1211
|
+
`
|
|
1212
|
+
);
|
|
2110
1213
|
});
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// src/core/teleport.ts
|
|
1217
|
+
import {
|
|
1218
|
+
randomBytes as randomBytes2,
|
|
1219
|
+
createCipheriv,
|
|
1220
|
+
createDecipheriv,
|
|
1221
|
+
pbkdf2Sync
|
|
1222
|
+
} from "crypto";
|
|
1223
|
+
import { z } from "zod";
|
|
1224
|
+
var ALGORITHM = "aes-256-gcm";
|
|
1225
|
+
var KEY_LENGTH = 32;
|
|
1226
|
+
var IV_LENGTH = 12;
|
|
1227
|
+
var SALT_LENGTH = 32;
|
|
1228
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
1229
|
+
var TeleportBundleSchema = z.object({
|
|
1230
|
+
v: z.literal(1),
|
|
1231
|
+
data: z.string(),
|
|
1232
|
+
salt: z.string(),
|
|
1233
|
+
iv: z.string(),
|
|
1234
|
+
tag: z.string(),
|
|
1235
|
+
createdAt: z.string(),
|
|
1236
|
+
count: z.number()
|
|
1237
|
+
});
|
|
1238
|
+
var TeleportPayloadSchema = z.object({
|
|
1239
|
+
secrets: z.array(
|
|
1240
|
+
z.object({
|
|
1241
|
+
key: z.string(),
|
|
1242
|
+
value: z.string(),
|
|
1243
|
+
scope: z.string().optional()
|
|
1244
|
+
})
|
|
1245
|
+
),
|
|
1246
|
+
exportedAt: z.string(),
|
|
1247
|
+
exportedBy: z.string().optional()
|
|
1248
|
+
});
|
|
1249
|
+
function deriveKey(passphrase, salt) {
|
|
1250
|
+
return pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
|
|
1251
|
+
}
|
|
1252
|
+
function teleportPack(secrets, passphrase) {
|
|
1253
|
+
const payload = {
|
|
1254
|
+
secrets,
|
|
1255
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1256
|
+
};
|
|
1257
|
+
const plaintext = JSON.stringify(payload);
|
|
1258
|
+
const salt = randomBytes2(SALT_LENGTH);
|
|
1259
|
+
const iv = randomBytes2(IV_LENGTH);
|
|
1260
|
+
const key = deriveKey(passphrase, salt);
|
|
1261
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
1262
|
+
const encrypted = Buffer.concat([
|
|
1263
|
+
cipher.update(plaintext, "utf8"),
|
|
1264
|
+
cipher.final()
|
|
1265
|
+
]);
|
|
1266
|
+
const tag = cipher.getAuthTag();
|
|
1267
|
+
const bundle = {
|
|
1268
|
+
v: 1,
|
|
1269
|
+
data: encrypted.toString("base64"),
|
|
1270
|
+
salt: salt.toString("base64"),
|
|
1271
|
+
iv: iv.toString("base64"),
|
|
1272
|
+
tag: tag.toString("base64"),
|
|
1273
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1274
|
+
count: secrets.length
|
|
1275
|
+
};
|
|
1276
|
+
return Buffer.from(JSON.stringify(bundle)).toString("base64");
|
|
1277
|
+
}
|
|
1278
|
+
function teleportUnpack(encoded, passphrase) {
|
|
1279
|
+
let bundleJson;
|
|
1280
|
+
try {
|
|
1281
|
+
bundleJson = Buffer.from(encoded, "base64").toString("utf8");
|
|
1282
|
+
} catch {
|
|
1283
|
+
throw new Error("ERR_TELEPORT_CORRUPT: invalid base64 bundle");
|
|
1284
|
+
}
|
|
1285
|
+
let rawBundle;
|
|
1286
|
+
try {
|
|
1287
|
+
rawBundle = JSON.parse(bundleJson);
|
|
1288
|
+
} catch {
|
|
1289
|
+
throw new Error("ERR_TELEPORT_CORRUPT: bundle is not valid JSON");
|
|
1290
|
+
}
|
|
1291
|
+
const parsedBundle = TeleportBundleSchema.safeParse(rawBundle);
|
|
1292
|
+
if (!parsedBundle.success) {
|
|
1293
|
+
throw new Error(
|
|
1294
|
+
`ERR_TELEPORT_CORRUPT: invalid bundle shape (${parsedBundle.error.message})`
|
|
2163
1295
|
);
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
1296
|
+
}
|
|
1297
|
+
const bundle = parsedBundle.data;
|
|
1298
|
+
const salt = Buffer.from(bundle.salt, "base64");
|
|
1299
|
+
const iv = Buffer.from(bundle.iv, "base64");
|
|
1300
|
+
const tag = Buffer.from(bundle.tag, "base64");
|
|
1301
|
+
const encrypted = Buffer.from(bundle.data, "base64");
|
|
1302
|
+
const key = deriveKey(passphrase, salt);
|
|
1303
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
1304
|
+
decipher.setAuthTag(tag);
|
|
1305
|
+
let decrypted;
|
|
1306
|
+
try {
|
|
1307
|
+
decrypted = Buffer.concat([
|
|
1308
|
+
decipher.update(encrypted),
|
|
1309
|
+
decipher.final()
|
|
1310
|
+
]);
|
|
1311
|
+
} catch {
|
|
1312
|
+
throw new Error("ERR_TELEPORT_BAD_PASSPHRASE: decryption failed (wrong passphrase or corrupt data)");
|
|
1313
|
+
}
|
|
1314
|
+
let rawPayload;
|
|
1315
|
+
try {
|
|
1316
|
+
rawPayload = JSON.parse(decrypted.toString("utf8"));
|
|
1317
|
+
} catch {
|
|
1318
|
+
throw new Error("ERR_TELEPORT_CORRUPT: decrypted payload is not valid JSON");
|
|
1319
|
+
}
|
|
1320
|
+
const payload = TeleportPayloadSchema.safeParse(rawPayload);
|
|
1321
|
+
if (!payload.success) {
|
|
1322
|
+
throw new Error(
|
|
1323
|
+
`ERR_TELEPORT_CORRUPT: invalid payload (${payload.error.message})`
|
|
2171
1324
|
);
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
console.log(` ${c.bold("Summary:")}`);
|
|
2179
|
-
console.log(` Total secrets: ${entries.length}`);
|
|
2180
|
-
console.log(` Active: ${entries.length - expired.length}`);
|
|
2181
|
-
console.log(` Expired: ${expired.length}`);
|
|
2182
|
-
console.log(` Stale: ${stale.length}`);
|
|
2183
|
-
console.log(` Never accessed: ${neverAccessed.length}`);
|
|
2184
|
-
console.log(` With rotation config: ${entries.length - noRotation.length}`);
|
|
2185
|
-
console.log();
|
|
2186
|
-
});
|
|
2187
|
-
program2.command("env").description("Show detected environment (wavefunction collapse context)").option("--project-path <path>", "Project path for detection").action((cmd) => {
|
|
2188
|
-
const result = collapseEnvironment({
|
|
2189
|
-
projectPath: cmd.projectPath ?? process.cwd()
|
|
2190
|
-
});
|
|
2191
|
-
if (result) {
|
|
2192
|
-
console.log(
|
|
2193
|
-
`${SYMBOLS.zap} ${c.bold("Collapsed environment:")} ${envBadge(result.env)} ${c.dim(`(source: ${result.source})`)}`
|
|
2194
|
-
);
|
|
2195
|
-
} else {
|
|
2196
|
-
console.log(
|
|
2197
|
-
c.dim("No environment detected. Set QRING_ENV, NODE_ENV, or create .q-ring.json")
|
|
2198
|
-
);
|
|
2199
|
-
}
|
|
2200
|
-
});
|
|
1325
|
+
}
|
|
1326
|
+
return payload.data;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// src/cli/commands/quantum.ts
|
|
1330
|
+
function registerQuantumCommands(program2) {
|
|
2201
1331
|
program2.command("generate").alias("gen").description("Generate a cryptographic secret (quantum noise)").option(
|
|
2202
1332
|
"-f, --format <format>",
|
|
2203
1333
|
"Format: hex, base64, alphanumeric, uuid, api-key, token, password",
|
|
2204
1334
|
"api-key"
|
|
2205
|
-
).option(
|
|
1335
|
+
).option(
|
|
1336
|
+
"-l, --length <n>",
|
|
1337
|
+
"Length (bytes or chars depending on format)",
|
|
1338
|
+
parseInt
|
|
1339
|
+
).option("--prefix <prefix>", "Prefix for api-key/token format").option(
|
|
1340
|
+
"-s, --save <key>",
|
|
1341
|
+
"Save the generated secret to keyring with this key"
|
|
1342
|
+
).option("-g, --global", "Save to global scope").option("-p, --project", "Save to project scope").option("--project-path <path>", "Explicit project path").action((cmd) => {
|
|
2206
1343
|
const secret = generateSecret({
|
|
2207
1344
|
format: cmd.format,
|
|
2208
1345
|
length: cmd.length,
|
|
@@ -2225,42 +1362,38 @@ ${c.dim(`format: ${cmd.format} | entropy: ~${entropy} bits`)}`
|
|
|
2225
1362
|
}
|
|
2226
1363
|
}
|
|
2227
1364
|
});
|
|
2228
|
-
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(
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
`${SYMBOLS.link} ${c.yellow("disentangled")} ${c.bold(sourceKey)} ${SYMBOLS.arrow} ${c.bold(targetKey)}`
|
|
2261
|
-
);
|
|
2262
|
-
}
|
|
2263
|
-
);
|
|
1365
|
+
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((sourceKey, targetKey, cmd) => {
|
|
1366
|
+
const sourceOpts = {
|
|
1367
|
+
scope: cmd.sourceProject ? "project" : "global",
|
|
1368
|
+
projectPath: cmd.sourceProject ?? process.cwd(),
|
|
1369
|
+
source: "cli"
|
|
1370
|
+
};
|
|
1371
|
+
const targetOpts = {
|
|
1372
|
+
scope: cmd.targetProject ? "project" : "global",
|
|
1373
|
+
projectPath: cmd.targetProject ?? process.cwd(),
|
|
1374
|
+
source: "cli"
|
|
1375
|
+
};
|
|
1376
|
+
entangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts);
|
|
1377
|
+
console.log(
|
|
1378
|
+
`${SYMBOLS.link} ${c.cyan("entangled")} ${c.bold(sourceKey)} ${SYMBOLS.arrow} ${c.bold(targetKey)}`
|
|
1379
|
+
);
|
|
1380
|
+
});
|
|
1381
|
+
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((sourceKey, targetKey, cmd) => {
|
|
1382
|
+
const sourceOpts = {
|
|
1383
|
+
scope: cmd.sourceProject ? "project" : "global",
|
|
1384
|
+
projectPath: cmd.sourceProject ?? process.cwd(),
|
|
1385
|
+
source: "cli"
|
|
1386
|
+
};
|
|
1387
|
+
const targetOpts = {
|
|
1388
|
+
scope: cmd.targetProject ? "project" : "global",
|
|
1389
|
+
projectPath: cmd.targetProject ?? process.cwd(),
|
|
1390
|
+
source: "cli"
|
|
1391
|
+
};
|
|
1392
|
+
disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts);
|
|
1393
|
+
console.log(
|
|
1394
|
+
`${SYMBOLS.link} ${c.yellow("disentangled")} ${c.bold(sourceKey)} ${SYMBOLS.arrow} ${c.bold(targetKey)}`
|
|
1395
|
+
);
|
|
1396
|
+
});
|
|
2264
1397
|
const tunnel = program2.command("tunnel").description("Ephemeral in-memory secrets (quantum tunneling)");
|
|
2265
1398
|
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) => {
|
|
2266
1399
|
const id = tunnelCreate(value, {
|
|
@@ -2277,118 +1410,881 @@ ${c.dim(`format: ${cmd.format} | entropy: ~${entropy} bits`)}`
|
|
|
2277
1410
|
const value = tunnelRead(id);
|
|
2278
1411
|
if (value === null) {
|
|
2279
1412
|
console.error(
|
|
2280
|
-
c.red(`${SYMBOLS.cross} Tunnel "${id}" not found or expired`)
|
|
1413
|
+
c.red(`${SYMBOLS.cross} Tunnel "${id}" not found or expired`)
|
|
1414
|
+
);
|
|
1415
|
+
process.exit(1);
|
|
1416
|
+
}
|
|
1417
|
+
process.stdout.write(value);
|
|
1418
|
+
});
|
|
1419
|
+
tunnel.command("destroy <id>").description("Destroy a tunneled secret immediately").action((id) => {
|
|
1420
|
+
const destroyed = tunnelDestroy(id);
|
|
1421
|
+
if (destroyed) {
|
|
1422
|
+
console.log(`${SYMBOLS.check} ${c.green("destroyed")} ${id}`);
|
|
1423
|
+
} else {
|
|
1424
|
+
console.error(c.red(`${SYMBOLS.cross} Tunnel "${id}" not found`));
|
|
1425
|
+
process.exit(1);
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
1428
|
+
tunnel.command("list").alias("ls").description("List active tunnels").action(() => {
|
|
1429
|
+
const tunnels = tunnelList();
|
|
1430
|
+
if (tunnels.length === 0) {
|
|
1431
|
+
console.log(c.dim("No active tunnels"));
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
console.log(
|
|
1435
|
+
c.bold(`
|
|
1436
|
+
${SYMBOLS.ghost} Active tunnels (${tunnels.length})
|
|
1437
|
+
`)
|
|
1438
|
+
);
|
|
1439
|
+
for (const t of tunnels) {
|
|
1440
|
+
const parts = [c.bold(t.id)];
|
|
1441
|
+
parts.push(c.dim(`reads: ${t.accessCount}`));
|
|
1442
|
+
if (t.maxReads) parts.push(c.dim(`max: ${t.maxReads}`));
|
|
1443
|
+
if (t.expiresAt) {
|
|
1444
|
+
const remaining = Math.max(
|
|
1445
|
+
0,
|
|
1446
|
+
Math.floor((t.expiresAt - Date.now()) / 1e3)
|
|
1447
|
+
);
|
|
1448
|
+
parts.push(c.dim(`expires: ${remaining}s`));
|
|
1449
|
+
}
|
|
1450
|
+
console.log(` ${SYMBOLS.ghost} ${parts.join(" ")}`);
|
|
1451
|
+
}
|
|
1452
|
+
console.log();
|
|
1453
|
+
});
|
|
1454
|
+
const tp = program2.command("teleport").alias("tp").description("Encrypted secret sharing (quantum teleportation)");
|
|
1455
|
+
tp.command("pack").description("Pack secrets into an encrypted bundle").option("-k, --keys <keys>", "Comma-separated key names to pack").option("-g, --global", "Pack global scope").option("-p, --project", "Pack project scope").option("--project-path <path>", "Explicit project path").action(async (cmd) => {
|
|
1456
|
+
const opts = buildOpts(cmd);
|
|
1457
|
+
const passphrase = await promptSecret(
|
|
1458
|
+
`${SYMBOLS.lock} Enter passphrase for encryption: `
|
|
1459
|
+
);
|
|
1460
|
+
if (!passphrase) {
|
|
1461
|
+
console.error(c.red("Passphrase required"));
|
|
1462
|
+
process.exit(1);
|
|
1463
|
+
}
|
|
1464
|
+
const entries = listSecrets(opts);
|
|
1465
|
+
let keys;
|
|
1466
|
+
if (cmd.keys) {
|
|
1467
|
+
keys = cmd.keys.split(",").map((k) => k.trim());
|
|
1468
|
+
}
|
|
1469
|
+
const secrets = [];
|
|
1470
|
+
for (const entry of entries) {
|
|
1471
|
+
if (keys && !keys.includes(entry.key)) continue;
|
|
1472
|
+
const value = getSecret(entry.key, { ...opts, scope: entry.scope });
|
|
1473
|
+
if (value !== null) {
|
|
1474
|
+
secrets.push({ key: entry.key, value, scope: entry.scope });
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
if (secrets.length === 0) {
|
|
1478
|
+
console.error(c.red("No secrets to pack"));
|
|
1479
|
+
process.exit(1);
|
|
1480
|
+
}
|
|
1481
|
+
const bundle = teleportPack(secrets, passphrase);
|
|
1482
|
+
process.stdout.write(bundle);
|
|
1483
|
+
if (process.stdout.isTTY) {
|
|
1484
|
+
console.log(
|
|
1485
|
+
`
|
|
1486
|
+
${SYMBOLS.package} ${c.green("packed")} ${secrets.length} secret(s)`
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
tp.command("unpack [bundle]").description("Unpack and import secrets from an encrypted bundle").option("-g, --global", "Import to global scope").option("-p, --project", "Import to project scope").option("--project-path <path>", "Explicit project path").option("--dry-run", "Show what would be imported without saving").action(async (bundle, cmd) => {
|
|
1491
|
+
if (!bundle) {
|
|
1492
|
+
const chunks = [];
|
|
1493
|
+
for await (const chunk of process.stdin) {
|
|
1494
|
+
chunks.push(chunk);
|
|
1495
|
+
}
|
|
1496
|
+
bundle = Buffer.concat(chunks).toString("utf8").trim();
|
|
1497
|
+
}
|
|
1498
|
+
const passphrase = await promptSecret(
|
|
1499
|
+
`${SYMBOLS.lock} Enter passphrase for decryption: `
|
|
1500
|
+
);
|
|
1501
|
+
try {
|
|
1502
|
+
const payload = teleportUnpack(bundle, passphrase);
|
|
1503
|
+
if (cmd.dryRun) {
|
|
1504
|
+
console.log(
|
|
1505
|
+
`
|
|
1506
|
+
${SYMBOLS.package} ${c.bold("Would import")} (${payload.secrets.length} secrets):
|
|
1507
|
+
`
|
|
1508
|
+
);
|
|
1509
|
+
for (const s of payload.secrets) {
|
|
1510
|
+
console.log(
|
|
1511
|
+
` ${SYMBOLS.key} ${c.bold(s.key)} ${c.dim(`[${s.scope ?? "global"}]`)}`
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
const opts = buildOpts(cmd);
|
|
1517
|
+
for (const s of payload.secrets) {
|
|
1518
|
+
setSecret(s.key, s.value, opts);
|
|
1519
|
+
}
|
|
1520
|
+
console.log(
|
|
1521
|
+
`${SYMBOLS.check} ${c.green("imported")} ${payload.secrets.length} secret(s) from teleport bundle`
|
|
1522
|
+
);
|
|
1523
|
+
} catch {
|
|
1524
|
+
console.error(
|
|
1525
|
+
c.red(
|
|
1526
|
+
`${SYMBOLS.cross} Failed to unpack: wrong passphrase or corrupted bundle`
|
|
1527
|
+
)
|
|
1528
|
+
);
|
|
1529
|
+
process.exit(1);
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// src/cli/commands/validation.ts
|
|
1535
|
+
function registerValidationCommands(program2) {
|
|
1536
|
+
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(
|
|
1537
|
+
"--provider <name>",
|
|
1538
|
+
"Force a specific provider (openai, stripe, github, aws, http)"
|
|
1539
|
+
).option(
|
|
1540
|
+
"--all",
|
|
1541
|
+
"Validate all secrets that have a detectable provider"
|
|
1542
|
+
).option(
|
|
1543
|
+
"--manifest",
|
|
1544
|
+
"Only validate manifest-declared secrets (with --all)"
|
|
1545
|
+
).option("--list-providers", "List all available providers").action(async (key, cmd) => {
|
|
1546
|
+
if (cmd.listProviders) {
|
|
1547
|
+
console.log(
|
|
1548
|
+
c.bold(`
|
|
1549
|
+
${SYMBOLS.shield} Available validation providers
|
|
1550
|
+
`)
|
|
1551
|
+
);
|
|
1552
|
+
for (const p of registry2.listProviders()) {
|
|
1553
|
+
const prefixes = p.prefixes?.length ? c.dim(` (${p.prefixes.join(", ")})`) : "";
|
|
1554
|
+
console.log(
|
|
1555
|
+
` ${c.cyan(p.name.padEnd(10))} ${p.description}${prefixes}`
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
console.log();
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
if (!key && !cmd.all) {
|
|
1562
|
+
console.error(c.red(`${SYMBOLS.cross} Provide a key name or use --all`));
|
|
1563
|
+
process.exit(1);
|
|
1564
|
+
}
|
|
1565
|
+
const opts = buildOpts(cmd);
|
|
1566
|
+
if (cmd.all) {
|
|
1567
|
+
let entries = listSecrets(opts);
|
|
1568
|
+
const projectPath = cmd.projectPath ?? process.cwd();
|
|
1569
|
+
if (cmd.manifest) {
|
|
1570
|
+
const config = readProjectConfig(projectPath);
|
|
1571
|
+
if (config?.secrets) {
|
|
1572
|
+
const manifestKeys = new Set(Object.keys(config.secrets));
|
|
1573
|
+
entries = entries.filter((e) => manifestKeys.has(e.key));
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
console.log(c.bold(`
|
|
1577
|
+
${SYMBOLS.shield} Validating secrets
|
|
1578
|
+
`));
|
|
1579
|
+
let validated = 0;
|
|
1580
|
+
let skipped = 0;
|
|
1581
|
+
for (const entry of entries) {
|
|
1582
|
+
const value2 = getSecret(entry.key, { ...opts, scope: entry.scope });
|
|
1583
|
+
if (!value2) {
|
|
1584
|
+
skipped++;
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
const provHint2 = entry.envelope?.meta.provider ?? cmd.provider;
|
|
1588
|
+
const result2 = await validateSecret(value2, { provider: provHint2 });
|
|
1589
|
+
if (result2.status === "unknown") {
|
|
1590
|
+
skipped++;
|
|
1591
|
+
continue;
|
|
1592
|
+
}
|
|
1593
|
+
validated++;
|
|
1594
|
+
const icon2 = result2.status === "valid" ? c.green(SYMBOLS.check) : result2.status === "invalid" ? c.red(SYMBOLS.cross) : c.yellow(SYMBOLS.warning);
|
|
1595
|
+
const statusText = result2.status === "valid" ? c.green("valid") : result2.status === "invalid" ? c.red("invalid") : c.yellow("error");
|
|
1596
|
+
console.log(
|
|
1597
|
+
` ${icon2} ${c.bold(entry.key.padEnd(24))} ${statusText} ${c.dim(`(${result2.provider}, ${result2.latencyMs}ms)`)}${result2.status !== "valid" ? ` ${c.dim("\u2014 " + result2.message)}` : ""}`
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
console.log(
|
|
1601
|
+
`
|
|
1602
|
+
${c.dim(`${validated} validated, ${skipped} skipped (no provider)`)}
|
|
1603
|
+
`
|
|
1604
|
+
);
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
const value = getSecret(key, opts);
|
|
1608
|
+
if (!value) {
|
|
1609
|
+
console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
|
|
1610
|
+
process.exit(1);
|
|
1611
|
+
}
|
|
1612
|
+
const envelope = getEnvelope(key, opts);
|
|
1613
|
+
const provHint = envelope?.envelope.meta.provider ?? cmd.provider;
|
|
1614
|
+
const result = await validateSecret(value, { provider: provHint });
|
|
1615
|
+
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");
|
|
1616
|
+
console.log(
|
|
1617
|
+
`
|
|
1618
|
+
${icon} ${c.bold(key)} ${result.status} ${c.dim(`(${result.provider}, ${result.latencyMs}ms)`)}`
|
|
1619
|
+
);
|
|
1620
|
+
if (result.message && result.status !== "valid") {
|
|
1621
|
+
console.log(` ${c.dim(result.message)}`);
|
|
1622
|
+
}
|
|
1623
|
+
console.log();
|
|
1624
|
+
});
|
|
1625
|
+
program2.command("rotate <key>").description(
|
|
1626
|
+
"Attempt issuer-native rotation of a secret via its provider"
|
|
1627
|
+
).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) => {
|
|
1628
|
+
const opts = buildOpts(cmd);
|
|
1629
|
+
const value = getSecret(key, opts);
|
|
1630
|
+
if (!value) {
|
|
1631
|
+
console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
|
|
1632
|
+
process.exitCode = 1;
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
const result = await rotateWithProvider(value, cmd.provider);
|
|
1636
|
+
if (result.rotated && result.newValue) {
|
|
1637
|
+
setSecret(key, result.newValue, {
|
|
1638
|
+
...opts,
|
|
1639
|
+
scope: opts.scope ?? "global"
|
|
1640
|
+
});
|
|
1641
|
+
console.log(
|
|
1642
|
+
`${SYMBOLS.check} ${c.green("Rotated")} ${c.bold(key)} via ${result.provider}`
|
|
1643
|
+
);
|
|
1644
|
+
console.log(c.dim(` ${result.message}`));
|
|
1645
|
+
} else {
|
|
1646
|
+
console.log(c.yellow(`${SYMBOLS.warning} ${result.message}`));
|
|
1647
|
+
}
|
|
1648
|
+
});
|
|
1649
|
+
program2.command("ci:validate").description(
|
|
1650
|
+
"CI-oriented batch validation of all secrets (exit code 1 on failure)"
|
|
1651
|
+
).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) => {
|
|
1652
|
+
const opts = buildOpts(cmd);
|
|
1653
|
+
const entries = listSecrets(opts);
|
|
1654
|
+
const secrets = entries.map((e) => {
|
|
1655
|
+
const val = getSecret(e.key, {
|
|
1656
|
+
...opts,
|
|
1657
|
+
scope: e.scope,
|
|
1658
|
+
silent: true
|
|
1659
|
+
});
|
|
1660
|
+
if (!val) return null;
|
|
1661
|
+
const provider = e.envelope?.meta.provider;
|
|
1662
|
+
const validationUrl = e.envelope?.meta.validationUrl;
|
|
1663
|
+
return { key: e.key, value: val, provider, validationUrl };
|
|
1664
|
+
}).filter((s) => s !== null);
|
|
1665
|
+
if (secrets.length === 0) {
|
|
1666
|
+
console.log(c.dim("No secrets to validate"));
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
const report = await ciValidateBatch(secrets);
|
|
1670
|
+
if (wantsJsonOutput(program2, cmd)) {
|
|
1671
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1672
|
+
} else {
|
|
1673
|
+
console.log(
|
|
1674
|
+
c.bold(`
|
|
1675
|
+
CI Secret Validation: ${report.results.length} secrets
|
|
1676
|
+
`)
|
|
1677
|
+
);
|
|
1678
|
+
for (const r of report.results) {
|
|
1679
|
+
const icon = r.validation.valid ? SYMBOLS.check : SYMBOLS.cross;
|
|
1680
|
+
const color = r.validation.valid ? c.green : c.red;
|
|
1681
|
+
console.log(
|
|
1682
|
+
` ${icon} ${color(r.key)} [${r.validation.provider}] ${r.validation.message}`
|
|
1683
|
+
);
|
|
1684
|
+
if (r.requiresRotation) console.log(c.dim(` \u2192 rotation required`));
|
|
1685
|
+
}
|
|
1686
|
+
console.log();
|
|
1687
|
+
if (!report.allValid) {
|
|
1688
|
+
console.log(c.red(` ${report.failCount} secret(s) failed validation`));
|
|
1689
|
+
process.exitCode = 1;
|
|
1690
|
+
} else {
|
|
1691
|
+
console.log(c.green(` All secrets valid`));
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// src/core/exec.ts
|
|
1698
|
+
import { spawn } from "child_process";
|
|
1699
|
+
import { Transform } from "stream";
|
|
1700
|
+
var BUILTIN_PROFILES = {
|
|
1701
|
+
unrestricted: { name: "unrestricted" },
|
|
1702
|
+
restricted: {
|
|
1703
|
+
name: "restricted",
|
|
1704
|
+
denyCommands: ["curl", "wget", "ssh", "scp", "nc", "netcat", "ncat"],
|
|
1705
|
+
maxRuntimeSeconds: 30,
|
|
1706
|
+
allowNetwork: false,
|
|
1707
|
+
stripEnvVars: ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"]
|
|
1708
|
+
},
|
|
1709
|
+
ci: {
|
|
1710
|
+
name: "ci",
|
|
1711
|
+
maxRuntimeSeconds: 300,
|
|
1712
|
+
allowNetwork: true,
|
|
1713
|
+
denyCommands: ["rm -rf /", "mkfs", "dd if="]
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
function getProfile(name) {
|
|
1717
|
+
if (!name) return BUILTIN_PROFILES.unrestricted;
|
|
1718
|
+
return BUILTIN_PROFILES[name] ?? { name };
|
|
1719
|
+
}
|
|
1720
|
+
var RedactionTransform = class extends Transform {
|
|
1721
|
+
patterns = [];
|
|
1722
|
+
tail = "";
|
|
1723
|
+
maxLen = 0;
|
|
1724
|
+
constructor(secretsToRedact) {
|
|
1725
|
+
super();
|
|
1726
|
+
const validSecrets = secretsToRedact.filter((s) => s.length > 5);
|
|
1727
|
+
validSecrets.sort((a, b) => b.length - a.length);
|
|
1728
|
+
this.patterns = validSecrets.map((s) => ({
|
|
1729
|
+
value: s,
|
|
1730
|
+
replacement: "[QRING:REDACTED]"
|
|
1731
|
+
}));
|
|
1732
|
+
if (validSecrets.length > 0) {
|
|
1733
|
+
this.maxLen = validSecrets[0].length;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
_transform(chunk, _encoding, callback) {
|
|
1737
|
+
if (this.patterns.length === 0) {
|
|
1738
|
+
this.push(chunk);
|
|
1739
|
+
return callback();
|
|
1740
|
+
}
|
|
1741
|
+
const text = this.tail + chunk.toString();
|
|
1742
|
+
let redacted = text;
|
|
1743
|
+
for (const { value, replacement } of this.patterns) {
|
|
1744
|
+
redacted = redacted.split(value).join(replacement);
|
|
1745
|
+
}
|
|
1746
|
+
if (redacted.length < this.maxLen) {
|
|
1747
|
+
this.tail = redacted;
|
|
1748
|
+
return callback();
|
|
1749
|
+
}
|
|
1750
|
+
const outputLen = redacted.length - this.maxLen + 1;
|
|
1751
|
+
const output = redacted.slice(0, outputLen);
|
|
1752
|
+
this.tail = redacted.slice(outputLen);
|
|
1753
|
+
this.push(output);
|
|
1754
|
+
callback();
|
|
1755
|
+
}
|
|
1756
|
+
_flush(callback) {
|
|
1757
|
+
if (this.tail) {
|
|
1758
|
+
let final = this.tail;
|
|
1759
|
+
for (const { value, replacement } of this.patterns) {
|
|
1760
|
+
final = final.split(value).join(replacement);
|
|
1761
|
+
}
|
|
1762
|
+
this.push(final);
|
|
1763
|
+
}
|
|
1764
|
+
callback();
|
|
1765
|
+
}
|
|
1766
|
+
};
|
|
1767
|
+
async function execCommand(opts) {
|
|
1768
|
+
const profile = getProfile(opts.profile);
|
|
1769
|
+
const fullCommand = [opts.command, ...opts.args].join(" ");
|
|
1770
|
+
const policyDecision = checkExecPolicy(fullCommand, opts.projectPath);
|
|
1771
|
+
if (!policyDecision.allowed) {
|
|
1772
|
+
throw new Error(`Policy Denied: ${policyDecision.reason}`);
|
|
1773
|
+
}
|
|
1774
|
+
if (profile.denyCommands) {
|
|
1775
|
+
const denied = profile.denyCommands.find((d) => {
|
|
1776
|
+
const pattern = new RegExp(`(^|[\\s/])${d.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`, "i");
|
|
1777
|
+
return pattern.test(fullCommand);
|
|
1778
|
+
});
|
|
1779
|
+
if (denied) {
|
|
1780
|
+
throw new Error(`Exec profile "${profile.name}" denies command containing "${denied}"`);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
if (profile.allowCommands) {
|
|
1784
|
+
const allowed = profile.allowCommands.some((a) => fullCommand.startsWith(a));
|
|
1785
|
+
if (!allowed) {
|
|
1786
|
+
throw new Error(`Exec profile "${profile.name}" does not allow command "${opts.command}"`);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
const envMap = {};
|
|
1790
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
1791
|
+
if (v !== void 0) envMap[k] = v;
|
|
1792
|
+
}
|
|
1793
|
+
if (profile.stripEnvVars) {
|
|
1794
|
+
for (const key of profile.stripEnvVars) {
|
|
1795
|
+
delete envMap[key];
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
const secretsToRedact = /* @__PURE__ */ new Set();
|
|
1799
|
+
let entries = listSecrets({
|
|
1800
|
+
scope: opts.scope,
|
|
1801
|
+
projectPath: opts.projectPath,
|
|
1802
|
+
source: opts.source ?? "cli",
|
|
1803
|
+
silent: true
|
|
1804
|
+
// list silently
|
|
1805
|
+
});
|
|
1806
|
+
if (opts.keys?.length) {
|
|
1807
|
+
const keySet = new Set(opts.keys);
|
|
1808
|
+
entries = entries.filter((e) => keySet.has(e.key));
|
|
1809
|
+
}
|
|
1810
|
+
if (opts.tags?.length) {
|
|
1811
|
+
entries = entries.filter(
|
|
1812
|
+
(e) => opts.tags.some((t) => e.envelope?.meta.tags?.includes(t))
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
for (const entry of entries) {
|
|
1816
|
+
if (entry.envelope) {
|
|
1817
|
+
const decay = checkDecay(entry.envelope);
|
|
1818
|
+
if (decay.isExpired) continue;
|
|
1819
|
+
}
|
|
1820
|
+
const val = getSecret(entry.key, {
|
|
1821
|
+
scope: entry.scope,
|
|
1822
|
+
projectPath: opts.projectPath,
|
|
1823
|
+
env: opts.env,
|
|
1824
|
+
source: opts.source ?? "cli",
|
|
1825
|
+
silent: false
|
|
1826
|
+
// Log access for execution
|
|
1827
|
+
});
|
|
1828
|
+
if (val !== null) {
|
|
1829
|
+
envMap[entry.key] = val;
|
|
1830
|
+
if (val.length > 5) {
|
|
1831
|
+
secretsToRedact.add(val);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
const maxRuntime = profile.maxRuntimeSeconds ?? getExecMaxRuntime(opts.projectPath);
|
|
1836
|
+
return new Promise((resolve, reject) => {
|
|
1837
|
+
const networkTools = /* @__PURE__ */ new Set([
|
|
1838
|
+
"curl",
|
|
1839
|
+
"wget",
|
|
1840
|
+
"ping",
|
|
1841
|
+
"nc",
|
|
1842
|
+
"netcat",
|
|
1843
|
+
"ssh",
|
|
1844
|
+
"telnet",
|
|
1845
|
+
"ftp",
|
|
1846
|
+
"dig",
|
|
1847
|
+
"nslookup"
|
|
1848
|
+
]);
|
|
1849
|
+
if (profile.allowNetwork === false && networkTools.has(opts.command)) {
|
|
1850
|
+
const msg = `[QRING] Execution blocked: network access is disabled for profile "${profile.name}", command "${opts.command}" is considered network-related`;
|
|
1851
|
+
if (opts.captureOutput) {
|
|
1852
|
+
return resolve({ code: 126, stdout: "", stderr: msg });
|
|
1853
|
+
}
|
|
1854
|
+
process.stderr.write(msg + "\n");
|
|
1855
|
+
return resolve({ code: 126, stdout: "", stderr: "" });
|
|
1856
|
+
}
|
|
1857
|
+
const child = spawn(opts.command, opts.args, {
|
|
1858
|
+
env: envMap,
|
|
1859
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
1860
|
+
shell: false
|
|
1861
|
+
});
|
|
1862
|
+
let timedOut = false;
|
|
1863
|
+
let timer;
|
|
1864
|
+
if (maxRuntime) {
|
|
1865
|
+
timer = setTimeout(() => {
|
|
1866
|
+
timedOut = true;
|
|
1867
|
+
child.kill("SIGKILL");
|
|
1868
|
+
}, maxRuntime * 1e3);
|
|
1869
|
+
}
|
|
1870
|
+
const stdoutRedact = new RedactionTransform([...secretsToRedact]);
|
|
1871
|
+
const stderrRedact = new RedactionTransform([...secretsToRedact]);
|
|
1872
|
+
if (child.stdout) child.stdout.pipe(stdoutRedact);
|
|
1873
|
+
if (child.stderr) child.stderr.pipe(stderrRedact);
|
|
1874
|
+
let stdoutStr = "";
|
|
1875
|
+
let stderrStr = "";
|
|
1876
|
+
if (opts.captureOutput) {
|
|
1877
|
+
stdoutRedact.on("data", (d) => stdoutStr += d.toString());
|
|
1878
|
+
stderrRedact.on("data", (d) => stderrStr += d.toString());
|
|
1879
|
+
} else {
|
|
1880
|
+
stdoutRedact.pipe(process.stdout);
|
|
1881
|
+
stderrRedact.pipe(process.stderr);
|
|
1882
|
+
}
|
|
1883
|
+
child.on("close", (code) => {
|
|
1884
|
+
if (timer) clearTimeout(timer);
|
|
1885
|
+
if (timedOut) {
|
|
1886
|
+
resolve({ code: 124, stdout: stdoutStr, stderr: stderrStr + `
|
|
1887
|
+
[QRING] Process killed: exceeded ${maxRuntime}s runtime limit` });
|
|
1888
|
+
} else {
|
|
1889
|
+
resolve({ code: code ?? 0, stdout: stdoutStr, stderr: stderrStr });
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1892
|
+
child.on("error", (err) => {
|
|
1893
|
+
if (timer) clearTimeout(timer);
|
|
1894
|
+
reject(err);
|
|
1895
|
+
});
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// src/core/scan.ts
|
|
1900
|
+
import { readFileSync as readFileSync3, readdirSync, statSync } from "fs";
|
|
1901
|
+
import { join } from "path";
|
|
1902
|
+
|
|
1903
|
+
// src/core/secrets-detect.ts
|
|
1904
|
+
var SECRET_ASSIGNMENT_PATTERN = /((?:api_?key|secret|token|password|auth|credential|access_?key)[a-z0-9_]*)\s*[:=]\s*(['"])([^'"]+)\2/gi;
|
|
1905
|
+
function calculateEntropy(str) {
|
|
1906
|
+
if (!str) return 0;
|
|
1907
|
+
const len = str.length;
|
|
1908
|
+
const frequencies = /* @__PURE__ */ new Map();
|
|
1909
|
+
for (let i = 0; i < len; i++) {
|
|
1910
|
+
const char = str[i];
|
|
1911
|
+
frequencies.set(char, (frequencies.get(char) || 0) + 1);
|
|
1912
|
+
}
|
|
1913
|
+
let entropy = 0;
|
|
1914
|
+
for (const count of frequencies.values()) {
|
|
1915
|
+
const p = count / len;
|
|
1916
|
+
entropy -= p * Math.log2(p);
|
|
1917
|
+
}
|
|
1918
|
+
return entropy;
|
|
1919
|
+
}
|
|
1920
|
+
function isPlaceholderValue(value) {
|
|
1921
|
+
const lv = value.toLowerCase();
|
|
1922
|
+
return lv.includes("example") || lv.includes("your_") || lv.includes("placeholder") || lv.includes("replace_me") || lv.includes("xxx");
|
|
1923
|
+
}
|
|
1924
|
+
function passesSecretHeuristic(value, entropy) {
|
|
1925
|
+
return entropy > 3.5 || value.startsWith("sk-") || value.startsWith("ghp_");
|
|
1926
|
+
}
|
|
1927
|
+
function findSecretsInLine(line) {
|
|
1928
|
+
if (line.length > 500) return [];
|
|
1929
|
+
const out = [];
|
|
1930
|
+
SECRET_ASSIGNMENT_PATTERN.lastIndex = 0;
|
|
1931
|
+
let match;
|
|
1932
|
+
while ((match = SECRET_ASSIGNMENT_PATTERN.exec(line)) !== null) {
|
|
1933
|
+
const varName = match[1];
|
|
1934
|
+
const quote = match[2];
|
|
1935
|
+
const value = match[3];
|
|
1936
|
+
if (value.length < 8) continue;
|
|
1937
|
+
if (isPlaceholderValue(value)) continue;
|
|
1938
|
+
const entropy = calculateEntropy(value);
|
|
1939
|
+
if (!passesSecretHeuristic(value, entropy)) continue;
|
|
1940
|
+
out.push({ varName, value, quote });
|
|
1941
|
+
}
|
|
1942
|
+
return out;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// src/core/scan.ts
|
|
1946
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
1947
|
+
"node_modules",
|
|
1948
|
+
".git",
|
|
1949
|
+
".next",
|
|
1950
|
+
"dist",
|
|
1951
|
+
"build",
|
|
1952
|
+
"coverage",
|
|
1953
|
+
".cursor",
|
|
1954
|
+
"venv",
|
|
1955
|
+
"__pycache__"
|
|
1956
|
+
]);
|
|
1957
|
+
var IGNORE_EXTS = /* @__PURE__ */ new Set([
|
|
1958
|
+
".png",
|
|
1959
|
+
".jpg",
|
|
1960
|
+
".jpeg",
|
|
1961
|
+
".gif",
|
|
1962
|
+
".ico",
|
|
1963
|
+
".svg",
|
|
1964
|
+
".webp",
|
|
1965
|
+
".mp4",
|
|
1966
|
+
".mp3",
|
|
1967
|
+
".wav",
|
|
1968
|
+
".ogg",
|
|
1969
|
+
".pdf",
|
|
1970
|
+
".zip",
|
|
1971
|
+
".tar",
|
|
1972
|
+
".gz",
|
|
1973
|
+
".xz",
|
|
1974
|
+
".ttf",
|
|
1975
|
+
".woff",
|
|
1976
|
+
".woff2",
|
|
1977
|
+
".eot",
|
|
1978
|
+
".exe",
|
|
1979
|
+
".dll",
|
|
1980
|
+
".so",
|
|
1981
|
+
".dylib",
|
|
1982
|
+
".lock"
|
|
1983
|
+
]);
|
|
1984
|
+
function scanCodebase(dir) {
|
|
1985
|
+
const results = [];
|
|
1986
|
+
function walk(currentDir) {
|
|
1987
|
+
let entries;
|
|
1988
|
+
try {
|
|
1989
|
+
entries = readdirSync(currentDir);
|
|
1990
|
+
} catch {
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
for (const entry of entries) {
|
|
1994
|
+
if (IGNORE_DIRS.has(entry)) continue;
|
|
1995
|
+
const fullPath = join(currentDir, entry);
|
|
1996
|
+
let stat;
|
|
1997
|
+
try {
|
|
1998
|
+
stat = statSync(fullPath);
|
|
1999
|
+
} catch {
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
2002
|
+
if (stat.isDirectory()) {
|
|
2003
|
+
walk(fullPath);
|
|
2004
|
+
} else if (stat.isFile()) {
|
|
2005
|
+
const ext = fullPath.slice(fullPath.lastIndexOf(".")).toLowerCase();
|
|
2006
|
+
if (IGNORE_EXTS.has(ext) || entry.endsWith(".lock")) continue;
|
|
2007
|
+
let content;
|
|
2008
|
+
try {
|
|
2009
|
+
content = readFileSync3(fullPath, "utf8");
|
|
2010
|
+
} catch {
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
if (content.includes("\0")) continue;
|
|
2014
|
+
const lines = content.split(/\r?\n/);
|
|
2015
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2016
|
+
const line = lines[i];
|
|
2017
|
+
const matches = findSecretsInLine(line);
|
|
2018
|
+
for (const m of matches) {
|
|
2019
|
+
const entropy = calculateEntropy(m.value);
|
|
2020
|
+
const relPath = fullPath.startsWith(dir) ? fullPath.slice(dir.length).replace(/^[/\\]+/, "") : fullPath;
|
|
2021
|
+
results.push({
|
|
2022
|
+
file: relPath || fullPath,
|
|
2023
|
+
line: i + 1,
|
|
2024
|
+
keyName: m.varName,
|
|
2025
|
+
match: m.value,
|
|
2026
|
+
context: line.trim(),
|
|
2027
|
+
entropy: parseFloat(entropy.toFixed(2))
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
walk(dir);
|
|
2035
|
+
return results;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// src/core/linter.ts
|
|
2039
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
|
|
2040
|
+
import { basename, extname } from "path";
|
|
2041
|
+
var ENV_REF_BY_EXT = {
|
|
2042
|
+
".ts": (k) => `process.env.${k}`,
|
|
2043
|
+
".tsx": (k) => `process.env.${k}`,
|
|
2044
|
+
".js": (k) => `process.env.${k}`,
|
|
2045
|
+
".jsx": (k) => `process.env.${k}`,
|
|
2046
|
+
".mjs": (k) => `process.env.${k}`,
|
|
2047
|
+
".cjs": (k) => `process.env.${k}`,
|
|
2048
|
+
".py": (k) => `os.environ["${k}"]`,
|
|
2049
|
+
".rb": (k) => `ENV["${k}"]`,
|
|
2050
|
+
".go": (k) => `os.Getenv("${k}")`,
|
|
2051
|
+
".rs": (k) => `std::env::var("${k}")`,
|
|
2052
|
+
".java": (k) => `System.getenv("${k}")`,
|
|
2053
|
+
".kt": (k) => `System.getenv("${k}")`,
|
|
2054
|
+
".cs": (k) => `Environment.GetEnvironmentVariable("${k}")`,
|
|
2055
|
+
".php": (k) => `getenv('${k}')`,
|
|
2056
|
+
".sh": (k) => `\${${k}}`,
|
|
2057
|
+
".bash": (k) => `\${${k}}`
|
|
2058
|
+
};
|
|
2059
|
+
function getEnvRef(filePath, keyName) {
|
|
2060
|
+
const ext = extname(filePath).toLowerCase();
|
|
2061
|
+
const formatter = ENV_REF_BY_EXT[ext];
|
|
2062
|
+
return formatter ? formatter(keyName) : `process.env.${keyName}`;
|
|
2063
|
+
}
|
|
2064
|
+
function lintFiles(files, opts = {}) {
|
|
2065
|
+
const results = [];
|
|
2066
|
+
for (const file of files) {
|
|
2067
|
+
if (!existsSync2(file)) continue;
|
|
2068
|
+
let content;
|
|
2069
|
+
try {
|
|
2070
|
+
content = readFileSync4(file, "utf8");
|
|
2071
|
+
} catch {
|
|
2072
|
+
continue;
|
|
2073
|
+
}
|
|
2074
|
+
if (content.includes("\0")) continue;
|
|
2075
|
+
const lines = content.split(/\r?\n/);
|
|
2076
|
+
const fixes = [];
|
|
2077
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2078
|
+
const line = lines[i];
|
|
2079
|
+
const matches = findSecretsInLine(line);
|
|
2080
|
+
for (const m of matches) {
|
|
2081
|
+
const varNameUpper = m.varName.toUpperCase();
|
|
2082
|
+
const entropy = calculateEntropy(m.value);
|
|
2083
|
+
const shouldFix = opts.fix === true;
|
|
2084
|
+
if (shouldFix) {
|
|
2085
|
+
const envRef = getEnvRef(file, varNameUpper);
|
|
2086
|
+
fixes.push({
|
|
2087
|
+
line: i,
|
|
2088
|
+
original: `${m.quote}${m.value}${m.quote}`,
|
|
2089
|
+
replacement: envRef,
|
|
2090
|
+
keyName: varNameUpper,
|
|
2091
|
+
value: m.value
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
results.push({
|
|
2095
|
+
file,
|
|
2096
|
+
line: i + 1,
|
|
2097
|
+
keyName: varNameUpper,
|
|
2098
|
+
match: m.value,
|
|
2099
|
+
context: line.trim(),
|
|
2100
|
+
entropy: parseFloat(entropy.toFixed(2)),
|
|
2101
|
+
fixed: shouldFix
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
if (opts.fix && fixes.length > 0) {
|
|
2106
|
+
const fixLines = content.split(/\r?\n/);
|
|
2107
|
+
for (const fix of fixes.reverse()) {
|
|
2108
|
+
const lineIdx = fix.line;
|
|
2109
|
+
if (lineIdx >= 0 && lineIdx < fixLines.length) {
|
|
2110
|
+
fixLines[lineIdx] = fixLines[lineIdx].replace(fix.original, fix.replacement);
|
|
2111
|
+
}
|
|
2112
|
+
if (!hasSecret(fix.keyName, { scope: opts.scope, projectPath: opts.projectPath })) {
|
|
2113
|
+
setSecret(fix.keyName, fix.value, {
|
|
2114
|
+
scope: opts.scope ?? "global",
|
|
2115
|
+
projectPath: opts.projectPath,
|
|
2116
|
+
source: "cli",
|
|
2117
|
+
description: `Auto-imported from ${basename(file)}:${fix.line + 1}`
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
writeFileSync2(file, fixLines.join("\n"), "utf8");
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
return results;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// src/cli/commands/tooling.ts
|
|
2128
|
+
function registerToolingCommands(program2) {
|
|
2129
|
+
program2.command("exec <command...>").description(
|
|
2130
|
+
"Run a command with secrets injected into its environment (output auto-redacted)"
|
|
2131
|
+
).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(
|
|
2132
|
+
"--profile <name>",
|
|
2133
|
+
"Exec profile: unrestricted, restricted, ci"
|
|
2134
|
+
).action(async (commandArgs, cmd) => {
|
|
2135
|
+
const opts = buildOpts(cmd);
|
|
2136
|
+
const command = commandArgs[0];
|
|
2137
|
+
const args = commandArgs.slice(1);
|
|
2138
|
+
try {
|
|
2139
|
+
const { code } = await execCommand({
|
|
2140
|
+
...opts,
|
|
2141
|
+
command,
|
|
2142
|
+
args,
|
|
2143
|
+
keys: cmd.keys?.split(",").map((k) => k.trim()),
|
|
2144
|
+
tags: cmd.tags?.split(",").map((t) => t.trim()),
|
|
2145
|
+
profile: cmd.profile
|
|
2146
|
+
});
|
|
2147
|
+
process.exit(code);
|
|
2148
|
+
} catch (err) {
|
|
2149
|
+
console.error(
|
|
2150
|
+
c.red(
|
|
2151
|
+
`${SYMBOLS.cross} Exec failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2152
|
+
)
|
|
2281
2153
|
);
|
|
2282
2154
|
process.exit(1);
|
|
2283
2155
|
}
|
|
2284
|
-
process.stdout.write(value);
|
|
2285
2156
|
});
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2157
|
+
program2.command("scan [dir]").description("Scan a codebase for hardcoded secrets").option(
|
|
2158
|
+
"--fix",
|
|
2159
|
+
"Auto-replace hardcoded secrets with process.env references and store in q-ring"
|
|
2160
|
+
).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) => {
|
|
2161
|
+
const targetDir = dir ?? process.cwd();
|
|
2162
|
+
const fixMode = cmd.fix === true;
|
|
2163
|
+
console.log(
|
|
2164
|
+
`
|
|
2165
|
+
${SYMBOLS.eye} Scanning ${c.bold(targetDir)} for secrets...${fixMode ? c.yellow(" [--fix mode]") : ""}
|
|
2166
|
+
`
|
|
2167
|
+
);
|
|
2168
|
+
const results = scanCodebase(targetDir);
|
|
2169
|
+
if (results.length === 0) {
|
|
2170
|
+
console.log(
|
|
2171
|
+
` ${c.green(SYMBOLS.check)} No hardcoded secrets found. Awesome!
|
|
2172
|
+
`
|
|
2293
2173
|
);
|
|
2294
|
-
|
|
2174
|
+
return;
|
|
2295
2175
|
}
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2176
|
+
if (fixMode) {
|
|
2177
|
+
const fileSet = new Set(
|
|
2178
|
+
results.map(
|
|
2179
|
+
(r) => r.file.startsWith("/") ? r.file : `${targetDir}/${r.file}`
|
|
2180
|
+
)
|
|
2181
|
+
);
|
|
2182
|
+
const opts = buildOpts(cmd);
|
|
2183
|
+
const lintResults = lintFiles([...fileSet], {
|
|
2184
|
+
fix: true,
|
|
2185
|
+
scope: opts.scope,
|
|
2186
|
+
projectPath: opts.projectPath
|
|
2187
|
+
});
|
|
2188
|
+
const fixedCount = lintResults.filter((r) => r.fixed).length;
|
|
2189
|
+
console.log(
|
|
2190
|
+
` ${c.green(SYMBOLS.check)} Fixed ${fixedCount} secrets \u2014 replaced with process.env references and stored in q-ring.
|
|
2191
|
+
`
|
|
2192
|
+
);
|
|
2301
2193
|
return;
|
|
2302
2194
|
}
|
|
2303
|
-
|
|
2304
|
-
${SYMBOLS.
|
|
2305
|
-
`));
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
const remaining = Math.max(0, Math.floor((t.expiresAt - Date.now()) / 1e3));
|
|
2312
|
-
parts.push(c.dim(`expires: ${remaining}s`));
|
|
2313
|
-
}
|
|
2314
|
-
console.log(` ${SYMBOLS.ghost} ${parts.join(" ")}`);
|
|
2195
|
+
for (const res of results) {
|
|
2196
|
+
console.log(` ${c.red(SYMBOLS.cross)} ${c.bold(res.file)}:${res.line}`);
|
|
2197
|
+
console.log(` ${c.dim("Key:")} ${c.cyan(res.keyName)}`);
|
|
2198
|
+
console.log(
|
|
2199
|
+
` ${c.dim("Entropy:")} ${res.entropy > 4 ? c.red(res.entropy.toString()) : c.yellow(res.entropy.toString())}`
|
|
2200
|
+
);
|
|
2201
|
+
console.log(` ${c.dim("Context:")} ${res.context}`);
|
|
2202
|
+
console.log();
|
|
2315
2203
|
}
|
|
2316
|
-
console.log(
|
|
2204
|
+
console.log(
|
|
2205
|
+
` ${c.red(`Found ${results.length} potential secrets.`)} Use ${c.bold("qring scan --fix")} to auto-migrate them.
|
|
2206
|
+
`
|
|
2207
|
+
);
|
|
2317
2208
|
});
|
|
2318
|
-
|
|
2319
|
-
|
|
2209
|
+
program2.command("lint <files...>").description(
|
|
2210
|
+
"Lint specific files for hardcoded secrets (with optional auto-fix)"
|
|
2211
|
+
).option(
|
|
2212
|
+
"--fix",
|
|
2213
|
+
"Replace hardcoded secrets with process.env references and store in q-ring"
|
|
2214
|
+
).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) => {
|
|
2320
2215
|
const opts = buildOpts(cmd);
|
|
2321
|
-
const
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
}
|
|
2328
|
-
const entries = listSecrets(opts);
|
|
2329
|
-
let keys;
|
|
2330
|
-
if (cmd.keys) {
|
|
2331
|
-
keys = cmd.keys.split(",").map((k) => k.trim());
|
|
2332
|
-
}
|
|
2333
|
-
const secrets = [];
|
|
2334
|
-
for (const entry of entries) {
|
|
2335
|
-
if (keys && !keys.includes(entry.key)) continue;
|
|
2336
|
-
const value = getSecret(entry.key, { ...opts, scope: entry.scope });
|
|
2337
|
-
if (value !== null) {
|
|
2338
|
-
secrets.push({ key: entry.key, value, scope: entry.scope });
|
|
2339
|
-
}
|
|
2340
|
-
}
|
|
2341
|
-
if (secrets.length === 0) {
|
|
2342
|
-
console.error(c.red("No secrets to pack"));
|
|
2343
|
-
process.exit(1);
|
|
2344
|
-
}
|
|
2345
|
-
const bundle = teleportPack(secrets, passphrase);
|
|
2346
|
-
process.stdout.write(bundle);
|
|
2347
|
-
if (process.stdout.isTTY) {
|
|
2216
|
+
const results = lintFiles(files, {
|
|
2217
|
+
fix: cmd.fix,
|
|
2218
|
+
scope: opts.scope,
|
|
2219
|
+
projectPath: opts.projectPath
|
|
2220
|
+
});
|
|
2221
|
+
if (results.length === 0) {
|
|
2348
2222
|
console.log(
|
|
2349
2223
|
`
|
|
2350
|
-
${
|
|
2224
|
+
${c.green(SYMBOLS.check)} No hardcoded secrets found in ${files.length} file(s).
|
|
2225
|
+
`
|
|
2351
2226
|
);
|
|
2227
|
+
return;
|
|
2352
2228
|
}
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2229
|
+
for (const res of results) {
|
|
2230
|
+
const status = res.fixed ? c.green("fixed") : c.red("found");
|
|
2231
|
+
console.log(
|
|
2232
|
+
` ${res.fixed ? c.green(SYMBOLS.check) : c.red(SYMBOLS.cross)} ${c.bold(res.file)}:${res.line} [${status}]`
|
|
2233
|
+
);
|
|
2234
|
+
console.log(` ${c.dim("Key:")} ${c.cyan(res.keyName)}`);
|
|
2235
|
+
console.log(
|
|
2236
|
+
` ${c.dim("Entropy:")} ${res.entropy > 4 ? c.red(res.entropy.toString()) : c.yellow(res.entropy.toString())}`
|
|
2237
|
+
);
|
|
2238
|
+
console.log();
|
|
2361
2239
|
}
|
|
2362
|
-
const
|
|
2363
|
-
|
|
2364
|
-
);
|
|
2365
|
-
try {
|
|
2366
|
-
const payload = teleportUnpack(bundle, passphrase);
|
|
2367
|
-
if (cmd.dryRun) {
|
|
2368
|
-
console.log(
|
|
2369
|
-
`
|
|
2370
|
-
${SYMBOLS.package} ${c.bold("Would import")} (${payload.secrets.length} secrets):
|
|
2371
|
-
`
|
|
2372
|
-
);
|
|
2373
|
-
for (const s of payload.secrets) {
|
|
2374
|
-
console.log(` ${SYMBOLS.key} ${c.bold(s.key)} ${c.dim(`[${s.scope ?? "global"}]`)}`);
|
|
2375
|
-
}
|
|
2376
|
-
return;
|
|
2377
|
-
}
|
|
2378
|
-
const opts = buildOpts(cmd);
|
|
2379
|
-
for (const s of payload.secrets) {
|
|
2380
|
-
setSecret(s.key, s.value, opts);
|
|
2381
|
-
}
|
|
2240
|
+
const fixedCount = results.filter((r) => r.fixed).length;
|
|
2241
|
+
if (cmd.fix && fixedCount > 0) {
|
|
2382
2242
|
console.log(
|
|
2383
|
-
|
|
2243
|
+
` ${c.green(`Fixed ${fixedCount} secret(s)`)} \u2014 replaced with env references and stored in q-ring.
|
|
2244
|
+
`
|
|
2384
2245
|
);
|
|
2385
|
-
}
|
|
2386
|
-
console.
|
|
2387
|
-
c.red(
|
|
2246
|
+
} else {
|
|
2247
|
+
console.log(
|
|
2248
|
+
` ${c.red(`Found ${results.length} potential secret(s).`)} Use ${c.bold("--fix")} to auto-migrate.
|
|
2249
|
+
`
|
|
2388
2250
|
);
|
|
2389
|
-
process.exit(1);
|
|
2390
2251
|
}
|
|
2391
2252
|
});
|
|
2253
|
+
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) => {
|
|
2254
|
+
const { startDashboardServer } = await import("./dashboard-OXUD62RV.js");
|
|
2255
|
+
const { exec } = await import("child_process");
|
|
2256
|
+
const { platform } = await import("os");
|
|
2257
|
+
const port = Number(cmd.port);
|
|
2258
|
+
const { close } = startDashboardServer({ port });
|
|
2259
|
+
const url = `http://127.0.0.1:${port}`;
|
|
2260
|
+
console.log(
|
|
2261
|
+
`
|
|
2262
|
+
${SYMBOLS.zap} ${c.bold("q-ring quantum status dashboard")}
|
|
2263
|
+
`
|
|
2264
|
+
);
|
|
2265
|
+
console.log(` ${c.cyan(url)}
|
|
2266
|
+
`);
|
|
2267
|
+
console.log(c.dim(" Press Ctrl+C to stop\n"));
|
|
2268
|
+
if (cmd.open !== false) {
|
|
2269
|
+
const openCmd = platform() === "darwin" ? "open" : platform() === "win32" ? "start" : "xdg-open";
|
|
2270
|
+
exec(`${openCmd} ${url}`);
|
|
2271
|
+
}
|
|
2272
|
+
const shutdown = () => {
|
|
2273
|
+
console.log(`
|
|
2274
|
+
${c.dim(" dashboard stopped")}`);
|
|
2275
|
+
close();
|
|
2276
|
+
process.exit(0);
|
|
2277
|
+
};
|
|
2278
|
+
process.on("SIGINT", shutdown);
|
|
2279
|
+
process.on("SIGTERM", shutdown);
|
|
2280
|
+
await new Promise(() => {
|
|
2281
|
+
});
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
// src/cli/commands/audit.ts
|
|
2286
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
2287
|
+
function registerAuditCommands(program2) {
|
|
2392
2288
|
program2.command("audit").description("View the audit log (observer effect)").option("-k, --key <key>", "Filter by key").option(
|
|
2393
2289
|
"-a, --action <action>",
|
|
2394
2290
|
"Filter by action (read, write, delete, etc.)"
|
|
@@ -2396,9 +2292,7 @@ ${SYMBOLS.package} ${c.bold("Would import")} (${payload.secrets.length} secrets)
|
|
|
2396
2292
|
if (cmd.anomalies) {
|
|
2397
2293
|
const anomalies = detectAnomalies(cmd.key);
|
|
2398
2294
|
if (anomalies.length === 0) {
|
|
2399
|
-
console.log(
|
|
2400
|
-
`${SYMBOLS.shield} ${c.green("No anomalies detected")}`
|
|
2401
|
-
);
|
|
2295
|
+
console.log(`${SYMBOLS.shield} ${c.green("No anomalies detected")}`);
|
|
2402
2296
|
return;
|
|
2403
2297
|
}
|
|
2404
2298
|
console.log(
|
|
@@ -2447,12 +2341,24 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
|
|
|
2447
2341
|
return;
|
|
2448
2342
|
}
|
|
2449
2343
|
if (result.intact) {
|
|
2450
|
-
console.log(
|
|
2344
|
+
console.log(
|
|
2345
|
+
`${SYMBOLS.shield} ${c.green("Audit chain intact")} \u2014 ${result.totalEvents} events verified`
|
|
2346
|
+
);
|
|
2451
2347
|
} else {
|
|
2452
|
-
console.log(
|
|
2453
|
-
|
|
2348
|
+
console.log(
|
|
2349
|
+
`${SYMBOLS.cross} ${c.red("Audit chain BROKEN")} at event #${result.brokenAt}`
|
|
2350
|
+
);
|
|
2351
|
+
console.log(
|
|
2352
|
+
c.dim(
|
|
2353
|
+
` ${result.validEvents}/${result.totalEvents} events valid before break`
|
|
2354
|
+
)
|
|
2355
|
+
);
|
|
2454
2356
|
if (result.brokenEvent) {
|
|
2455
|
-
console.log(
|
|
2357
|
+
console.log(
|
|
2358
|
+
c.dim(
|
|
2359
|
+
` Broken event: ${result.brokenEvent.timestamp} ${result.brokenEvent.action} ${result.brokenEvent.key ?? ""}`
|
|
2360
|
+
)
|
|
2361
|
+
);
|
|
2456
2362
|
}
|
|
2457
2363
|
process.exitCode = 1;
|
|
2458
2364
|
}
|
|
@@ -2464,7 +2370,7 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
|
|
|
2464
2370
|
format: cmd.format
|
|
2465
2371
|
});
|
|
2466
2372
|
if (cmd.output) {
|
|
2467
|
-
|
|
2373
|
+
writeFileSync3(cmd.output, output);
|
|
2468
2374
|
console.log(`${SYMBOLS.check} Exported to ${cmd.output}`);
|
|
2469
2375
|
} else {
|
|
2470
2376
|
console.log(output);
|
|
@@ -2477,11 +2383,9 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
|
|
|
2477
2383
|
console.log(c.dim("No secrets to check"));
|
|
2478
2384
|
return;
|
|
2479
2385
|
}
|
|
2480
|
-
console.log(
|
|
2481
|
-
c.bold(`
|
|
2386
|
+
console.log(c.bold(`
|
|
2482
2387
|
${SYMBOLS.shield} Secret health report
|
|
2483
|
-
`)
|
|
2484
|
-
);
|
|
2388
|
+
`));
|
|
2485
2389
|
let healthy = 0;
|
|
2486
2390
|
let stale = 0;
|
|
2487
2391
|
let expired = 0;
|
|
@@ -2505,31 +2409,147 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
|
|
|
2505
2409
|
healthy++;
|
|
2506
2410
|
}
|
|
2507
2411
|
}
|
|
2508
|
-
console.log(
|
|
2509
|
-
`
|
|
2510
|
-
${c.green(`${SYMBOLS.check} ${healthy} healthy`)} ${c.yellow(`${SYMBOLS.warning} ${stale} stale`)} ${c.red(`${SYMBOLS.cross} ${expired} expired`)} ${c.dim(`${noDecay} no decay`)}`
|
|
2511
|
-
);
|
|
2512
|
-
const anomalies = detectAnomalies();
|
|
2513
|
-
if (anomalies.length > 0) {
|
|
2412
|
+
console.log(
|
|
2413
|
+
`
|
|
2414
|
+
${c.green(`${SYMBOLS.check} ${healthy} healthy`)} ${c.yellow(`${SYMBOLS.warning} ${stale} stale`)} ${c.red(`${SYMBOLS.cross} ${expired} expired`)} ${c.dim(`${noDecay} no decay`)}`
|
|
2415
|
+
);
|
|
2416
|
+
const anomalies = detectAnomalies();
|
|
2417
|
+
if (anomalies.length > 0) {
|
|
2418
|
+
console.log(
|
|
2419
|
+
`
|
|
2420
|
+
${c.yellow(`${SYMBOLS.warning} ${anomalies.length} access anomaly/anomalies detected`)} ${c.dim("(run: qring audit --anomalies)")}`
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
console.log();
|
|
2424
|
+
});
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// src/hooks/precommit.ts
|
|
2428
|
+
import { execSync } from "child_process";
|
|
2429
|
+
import { existsSync as existsSync3, writeFileSync as writeFileSync4, chmodSync, readFileSync as readFileSync5, unlinkSync } from "fs";
|
|
2430
|
+
import { join as join2 } from "path";
|
|
2431
|
+
function getStagedFiles() {
|
|
2432
|
+
try {
|
|
2433
|
+
const output = execSync("git diff --cached --name-only --diff-filter=ACM", {
|
|
2434
|
+
encoding: "utf8",
|
|
2435
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2436
|
+
});
|
|
2437
|
+
return output.split("\n").map((f) => f.trim()).filter((f) => f.length > 0);
|
|
2438
|
+
} catch {
|
|
2439
|
+
return [];
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
function runPreCommitScan() {
|
|
2443
|
+
const staged = getStagedFiles();
|
|
2444
|
+
if (staged.length === 0) return 0;
|
|
2445
|
+
const results = lintFiles(staged);
|
|
2446
|
+
if (results.length === 0) return 0;
|
|
2447
|
+
console.error("\n[q-ring] Pre-commit scan found hardcoded secrets:\n");
|
|
2448
|
+
for (const r of results) {
|
|
2449
|
+
console.error(` ${r.file}:${r.line} ${r.keyName} (entropy: ${r.entropy})`);
|
|
2450
|
+
}
|
|
2451
|
+
console.error(
|
|
2452
|
+
`
|
|
2453
|
+
[q-ring] Commit blocked. Use "qring scan --fix" to auto-migrate, or "git commit --no-verify" to bypass.
|
|
2454
|
+
`
|
|
2455
|
+
);
|
|
2456
|
+
return 1;
|
|
2457
|
+
}
|
|
2458
|
+
var HOOK_SCRIPT = `#!/bin/sh
|
|
2459
|
+
# q-ring pre-commit hook \u2014 scans staged files for hardcoded secrets
|
|
2460
|
+
npx qring hook:run 2>&1
|
|
2461
|
+
exit $?
|
|
2462
|
+
`;
|
|
2463
|
+
function installPreCommitHook(repoPath) {
|
|
2464
|
+
const root = repoPath ?? process.cwd();
|
|
2465
|
+
const hooksDir = join2(root, ".git", "hooks");
|
|
2466
|
+
if (!existsSync3(join2(root, ".git"))) {
|
|
2467
|
+
return { installed: false, path: "", message: "Not a git repository" };
|
|
2468
|
+
}
|
|
2469
|
+
const hookPath = join2(hooksDir, "pre-commit");
|
|
2470
|
+
if (existsSync3(hookPath)) {
|
|
2471
|
+
const existing = readFileSync5(hookPath, "utf8");
|
|
2472
|
+
if (existing.includes("q-ring")) {
|
|
2473
|
+
return { installed: true, path: hookPath, message: "Hook already installed" };
|
|
2474
|
+
}
|
|
2475
|
+
writeFileSync4(hookPath, HOOK_SCRIPT + "\n" + existing, "utf8");
|
|
2476
|
+
} else {
|
|
2477
|
+
writeFileSync4(hookPath, HOOK_SCRIPT, "utf8");
|
|
2478
|
+
}
|
|
2479
|
+
chmodSync(hookPath, 493);
|
|
2480
|
+
return { installed: true, path: hookPath, message: "Pre-commit hook installed" };
|
|
2481
|
+
}
|
|
2482
|
+
function uninstallPreCommitHook(repoPath) {
|
|
2483
|
+
const root = repoPath ?? process.cwd();
|
|
2484
|
+
const hookPath = join2(root, ".git", "hooks", "pre-commit");
|
|
2485
|
+
if (!existsSync3(hookPath)) return false;
|
|
2486
|
+
const content = readFileSync5(hookPath, "utf8");
|
|
2487
|
+
if (!content.includes("q-ring")) return false;
|
|
2488
|
+
const lines = content.split("\n");
|
|
2489
|
+
const cleaned = lines.filter(
|
|
2490
|
+
(l) => !l.includes("q-ring") && !l.includes("npx qring hook:run")
|
|
2491
|
+
);
|
|
2492
|
+
if (cleaned.filter((l) => l.trim() && !l.startsWith("#!")).length === 0) {
|
|
2493
|
+
unlinkSync(hookPath);
|
|
2494
|
+
} else {
|
|
2495
|
+
writeFileSync4(hookPath, cleaned.join("\n"), "utf8");
|
|
2496
|
+
}
|
|
2497
|
+
return true;
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
// src/cli/commands/hooks.ts
|
|
2501
|
+
function registerHookCommands(program2) {
|
|
2502
|
+
program2.command("hook:install").description(
|
|
2503
|
+
"Install a git pre-commit hook that scans for hardcoded secrets"
|
|
2504
|
+
).option("--project-path <path>", "Repository path").action((cmd) => {
|
|
2505
|
+
const result = installPreCommitHook(cmd.projectPath);
|
|
2506
|
+
if (result.installed) {
|
|
2514
2507
|
console.log(
|
|
2515
|
-
`
|
|
2516
|
-
${c.yellow(`${SYMBOLS.warning} ${anomalies.length} access anomaly/anomalies detected`)} ${c.dim("(run: qring audit --anomalies)")}`
|
|
2508
|
+
`${SYMBOLS.check} ${c.green(result.message)} at ${c.dim(result.path)}`
|
|
2517
2509
|
);
|
|
2510
|
+
} else {
|
|
2511
|
+
console.log(`${SYMBOLS.cross} ${c.red(result.message)}`);
|
|
2518
2512
|
}
|
|
2519
|
-
console.log();
|
|
2520
2513
|
});
|
|
2521
|
-
|
|
2522
|
-
|
|
2514
|
+
program2.command("hook:uninstall").description("Remove the q-ring pre-commit hook").option("--project-path <path>", "Repository path").action((cmd) => {
|
|
2515
|
+
const removed = uninstallPreCommitHook(cmd.projectPath);
|
|
2516
|
+
if (removed) {
|
|
2517
|
+
console.log(`${SYMBOLS.check} ${c.green("Pre-commit hook removed")}`);
|
|
2518
|
+
} else {
|
|
2519
|
+
console.log(c.dim("No q-ring pre-commit hook found"));
|
|
2520
|
+
}
|
|
2521
|
+
});
|
|
2522
|
+
program2.command("hook:run").description("Run the pre-commit secret scan (called by the git hook)").action(() => {
|
|
2523
|
+
const code = runPreCommitScan();
|
|
2524
|
+
process.exit(code);
|
|
2525
|
+
});
|
|
2526
|
+
const hook = program2.command("hook").description(
|
|
2527
|
+
"Manage secret change hooks (callbacks on write/delete/rotate)"
|
|
2528
|
+
);
|
|
2529
|
+
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(
|
|
2530
|
+
"--scope <scope>",
|
|
2531
|
+
"Trigger only for this scope (global or project)"
|
|
2532
|
+
).option(
|
|
2533
|
+
"--action <actions>",
|
|
2534
|
+
"Comma-separated actions: write,delete,rotate",
|
|
2535
|
+
"write,delete,rotate"
|
|
2536
|
+
).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) => {
|
|
2523
2537
|
let type;
|
|
2524
2538
|
if (cmd.exec) type = "shell";
|
|
2525
2539
|
else if (cmd.url) type = "http";
|
|
2526
2540
|
else if (cmd.signalTarget) type = "signal";
|
|
2527
2541
|
else {
|
|
2528
|
-
console.error(
|
|
2542
|
+
console.error(
|
|
2543
|
+
c.red(`${SYMBOLS.cross} Specify --exec, --url, or --signal-target`)
|
|
2544
|
+
);
|
|
2529
2545
|
process.exit(1);
|
|
2530
2546
|
}
|
|
2531
2547
|
if (!cmd.key && !cmd.keyPattern && !cmd.tag) {
|
|
2532
|
-
console.error(
|
|
2548
|
+
console.error(
|
|
2549
|
+
c.red(
|
|
2550
|
+
`${SYMBOLS.cross} Specify at least one match criterion: --key, --key-pattern, or --tag`
|
|
2551
|
+
)
|
|
2552
|
+
);
|
|
2533
2553
|
process.exit(1);
|
|
2534
2554
|
}
|
|
2535
2555
|
const actions = cmd.action.split(",").map((a) => a.trim());
|
|
@@ -2548,7 +2568,9 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
|
|
|
2548
2568
|
description: cmd.description,
|
|
2549
2569
|
enabled: true
|
|
2550
2570
|
});
|
|
2551
|
-
console.log(
|
|
2571
|
+
console.log(
|
|
2572
|
+
`${SYMBOLS.check} ${c.green("registered")} hook ${c.bold(entry.id)} (${type})`
|
|
2573
|
+
);
|
|
2552
2574
|
if (cmd.key) console.log(c.dim(` key: ${cmd.key}`));
|
|
2553
2575
|
if (cmd.keyPattern) console.log(c.dim(` pattern: ${cmd.keyPattern}`));
|
|
2554
2576
|
if (cmd.tag) console.log(c.dim(` tag: ${cmd.tag}`));
|
|
@@ -2559,9 +2581,11 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
|
|
|
2559
2581
|
console.log(c.dim("No hooks registered"));
|
|
2560
2582
|
return;
|
|
2561
2583
|
}
|
|
2562
|
-
console.log(
|
|
2584
|
+
console.log(
|
|
2585
|
+
c.bold(`
|
|
2563
2586
|
${SYMBOLS.zap} Registered hooks (${hooks.length})
|
|
2564
|
-
`)
|
|
2587
|
+
`)
|
|
2588
|
+
);
|
|
2565
2589
|
for (const h of hooks) {
|
|
2566
2590
|
const status = h.enabled ? c.green("on") : c.red("off");
|
|
2567
2591
|
const matchParts = [];
|
|
@@ -2569,10 +2593,15 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
|
|
|
2569
2593
|
if (h.match.keyPattern) matchParts.push(`pattern=${h.match.keyPattern}`);
|
|
2570
2594
|
if (h.match.tag) matchParts.push(`tag=${h.match.tag}`);
|
|
2571
2595
|
if (h.match.scope) matchParts.push(`scope=${h.match.scope}`);
|
|
2572
|
-
if (h.match.action?.length)
|
|
2596
|
+
if (h.match.action?.length)
|
|
2597
|
+
matchParts.push(`actions=${h.match.action.join(",")}`);
|
|
2573
2598
|
const target = h.type === "shell" ? h.command : h.type === "http" ? h.url : h.signal ? `${h.signal.target} (${h.signal.signal ?? "SIGHUP"})` : "?";
|
|
2574
|
-
console.log(
|
|
2575
|
-
|
|
2599
|
+
console.log(
|
|
2600
|
+
` ${c.bold(h.id)} [${status}] ${c.cyan(h.type)} ${c.dim(matchParts.join(" "))}`
|
|
2601
|
+
);
|
|
2602
|
+
console.log(
|
|
2603
|
+
` ${c.dim("\u2192")} ${target}${h.description ? ` ${c.dim(`\u2014 ${h.description}`)}` : ""}`
|
|
2604
|
+
);
|
|
2576
2605
|
}
|
|
2577
2606
|
console.log();
|
|
2578
2607
|
});
|
|
@@ -2594,7 +2623,9 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
|
|
|
2594
2623
|
});
|
|
2595
2624
|
hook.command("disable <id>").description("Disable a hook").action((id) => {
|
|
2596
2625
|
if (disableHook(id)) {
|
|
2597
|
-
console.log(
|
|
2626
|
+
console.log(
|
|
2627
|
+
`${SYMBOLS.check} ${c.yellow("disabled")} hook ${c.bold(id)}`
|
|
2628
|
+
);
|
|
2598
2629
|
} else {
|
|
2599
2630
|
console.error(c.red(`${SYMBOLS.cross} Hook "${id}" not found`));
|
|
2600
2631
|
process.exit(1);
|
|
@@ -2622,96 +2653,302 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
|
|
|
2622
2653
|
const icon = result.success ? c.green(SYMBOLS.check) : c.red(SYMBOLS.cross);
|
|
2623
2654
|
console.log(` ${icon} ${result.message}`);
|
|
2624
2655
|
} else {
|
|
2625
|
-
console.log(
|
|
2656
|
+
console.log(
|
|
2657
|
+
c.yellow(` ${SYMBOLS.warning} Hook did not match the test payload`)
|
|
2658
|
+
);
|
|
2626
2659
|
}
|
|
2627
2660
|
console.log();
|
|
2628
2661
|
});
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
// src/core/agent.ts
|
|
2665
|
+
function defaultConfig() {
|
|
2666
|
+
return {
|
|
2667
|
+
intervalSeconds: 60,
|
|
2668
|
+
autoRotate: false,
|
|
2669
|
+
projectPaths: [process.cwd()],
|
|
2670
|
+
verbose: false
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2673
|
+
function runHealthScan(config = {}) {
|
|
2674
|
+
const cfg = { ...defaultConfig(), ...config };
|
|
2675
|
+
const report = {
|
|
2676
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2677
|
+
totalSecrets: 0,
|
|
2678
|
+
healthy: 0,
|
|
2679
|
+
stale: 0,
|
|
2680
|
+
expired: 0,
|
|
2681
|
+
anomalies: 0,
|
|
2682
|
+
rotated: [],
|
|
2683
|
+
warnings: []
|
|
2684
|
+
};
|
|
2685
|
+
const globalEntries = listSecrets({ scope: "global", source: "agent" });
|
|
2686
|
+
const projectEntries = cfg.projectPaths.flatMap(
|
|
2687
|
+
(pp) => listSecrets({ scope: "project", projectPath: pp, source: "agent" })
|
|
2688
|
+
);
|
|
2689
|
+
const allEntries = [...globalEntries, ...projectEntries];
|
|
2690
|
+
report.totalSecrets = allEntries.length;
|
|
2691
|
+
for (const entry of allEntries) {
|
|
2692
|
+
if (!entry.envelope) continue;
|
|
2693
|
+
const decay = checkDecay(entry.envelope);
|
|
2694
|
+
if (decay.isExpired) {
|
|
2695
|
+
report.expired++;
|
|
2696
|
+
report.warnings.push(
|
|
2697
|
+
`EXPIRED: ${entry.key} [${entry.scope}] \u2014 expired ${decay.timeRemaining}`
|
|
2635
2698
|
);
|
|
2636
|
-
|
|
2699
|
+
if (cfg.autoRotate) {
|
|
2700
|
+
const fmt = entry.envelope?.meta.rotationFormat ?? "api-key";
|
|
2701
|
+
const prefix = entry.envelope?.meta.rotationPrefix;
|
|
2702
|
+
const newValue = generateSecret({ format: fmt, prefix });
|
|
2703
|
+
setSecret(entry.key, newValue, {
|
|
2704
|
+
scope: entry.scope,
|
|
2705
|
+
projectPath: cfg.projectPaths[0],
|
|
2706
|
+
source: "agent"
|
|
2707
|
+
});
|
|
2708
|
+
report.rotated.push(entry.key);
|
|
2709
|
+
logAudit({
|
|
2710
|
+
action: "write",
|
|
2711
|
+
key: entry.key,
|
|
2712
|
+
scope: entry.scope,
|
|
2713
|
+
source: "agent",
|
|
2714
|
+
detail: "auto-rotated by agent (expired)"
|
|
2715
|
+
});
|
|
2716
|
+
fireHooks({
|
|
2717
|
+
action: "rotate",
|
|
2718
|
+
key: entry.key,
|
|
2719
|
+
scope: entry.scope,
|
|
2720
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2721
|
+
source: "agent"
|
|
2722
|
+
}, entry.envelope?.meta.tags).catch(() => {
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
} else if (decay.isStale) {
|
|
2726
|
+
report.stale++;
|
|
2727
|
+
report.warnings.push(
|
|
2728
|
+
`STALE: ${entry.key} [${entry.scope}] \u2014 ${decay.lifetimePercent}% lifetime, ${decay.timeRemaining} remaining`
|
|
2729
|
+
);
|
|
2730
|
+
} else {
|
|
2731
|
+
report.healthy++;
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
const anomalies = detectAnomalies();
|
|
2735
|
+
report.anomalies = anomalies.length;
|
|
2736
|
+
for (const a of anomalies) {
|
|
2737
|
+
report.warnings.push(`ANOMALY [${a.type}]: ${a.description}`);
|
|
2738
|
+
}
|
|
2739
|
+
return report;
|
|
2740
|
+
}
|
|
2741
|
+
function formatReport(report, verbose) {
|
|
2742
|
+
const lines = [];
|
|
2743
|
+
lines.push(
|
|
2744
|
+
`${c.bold(`${SYMBOLS.shield} q-ring agent scan`)} ${c.dim(report.timestamp)}`
|
|
2745
|
+
);
|
|
2746
|
+
lines.push(
|
|
2747
|
+
` ${c.dim("secrets:")} ${report.totalSecrets} ${c.green(`${SYMBOLS.check} ${report.healthy}`)} ${c.yellow(`${SYMBOLS.warning} ${report.stale}`)} ${c.red(`${SYMBOLS.cross} ${report.expired}`)} ${c.dim(`anomalies: ${report.anomalies}`)}`
|
|
2748
|
+
);
|
|
2749
|
+
if (report.rotated.length > 0) {
|
|
2750
|
+
lines.push(
|
|
2751
|
+
` ${c.cyan(`${SYMBOLS.zap} auto-rotated:`)} ${report.rotated.join(", ")}`
|
|
2752
|
+
);
|
|
2753
|
+
}
|
|
2754
|
+
if (verbose && report.warnings.length > 0) {
|
|
2755
|
+
lines.push("");
|
|
2756
|
+
for (const w of report.warnings) {
|
|
2757
|
+
if (w.startsWith("EXPIRED")) lines.push(` ${c.red(w)}`);
|
|
2758
|
+
else if (w.startsWith("STALE")) lines.push(` ${c.yellow(w)}`);
|
|
2759
|
+
else if (w.startsWith("ANOMALY")) lines.push(` ${c.magenta(w)}`);
|
|
2760
|
+
else lines.push(` ${w}`);
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
return lines.join("\n");
|
|
2764
|
+
}
|
|
2765
|
+
async function startAgent(config = {}) {
|
|
2766
|
+
const cfg = { ...defaultConfig(), ...config };
|
|
2767
|
+
console.log(
|
|
2768
|
+
`${c.bold(`${SYMBOLS.zap} q-ring agent started`)} ${c.dim(`(interval: ${cfg.intervalSeconds}s, auto-rotate: ${cfg.autoRotate})`)}`
|
|
2769
|
+
);
|
|
2770
|
+
console.log(
|
|
2771
|
+
c.dim(` monitoring: global + ${cfg.projectPaths.length} project(s)`)
|
|
2772
|
+
);
|
|
2773
|
+
console.log();
|
|
2774
|
+
const scan = () => {
|
|
2775
|
+
const report = runHealthScan(cfg);
|
|
2776
|
+
console.log(formatReport(report, cfg.verbose));
|
|
2777
|
+
if (report.warnings.length > 0 || cfg.verbose) {
|
|
2778
|
+
console.log();
|
|
2779
|
+
}
|
|
2780
|
+
};
|
|
2781
|
+
scan();
|
|
2782
|
+
const interval = setInterval(scan, cfg.intervalSeconds * 1e3);
|
|
2783
|
+
const shutdown = () => {
|
|
2784
|
+
clearInterval(interval);
|
|
2785
|
+
console.log(`
|
|
2786
|
+
${c.dim("q-ring agent stopped")}`);
|
|
2787
|
+
process.exit(0);
|
|
2788
|
+
};
|
|
2789
|
+
process.on("SIGINT", shutdown);
|
|
2790
|
+
process.on("SIGTERM", shutdown);
|
|
2791
|
+
await new Promise(() => {
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
// src/cli/commands/agent.ts
|
|
2796
|
+
function registerAgentCommands(program2) {
|
|
2797
|
+
program2.command("remember <key> <value>").description(
|
|
2798
|
+
"Store a key-value pair in encrypted agent memory (persists across sessions)"
|
|
2799
|
+
).action((key, value) => {
|
|
2800
|
+
remember(key, value);
|
|
2801
|
+
console.log(`${SYMBOLS.check} ${c.green("remembered")} ${c.bold(key)}`);
|
|
2802
|
+
});
|
|
2803
|
+
program2.command("recall [key]").description("Retrieve a value from agent memory, or list all keys").action((key) => {
|
|
2804
|
+
if (!key) {
|
|
2805
|
+
const entries = listMemory();
|
|
2806
|
+
if (entries.length === 0) {
|
|
2807
|
+
console.log(c.dim("Agent memory is empty."));
|
|
2808
|
+
return;
|
|
2809
|
+
}
|
|
2810
|
+
console.log(
|
|
2811
|
+
`
|
|
2812
|
+
${SYMBOLS.zap} ${c.bold("Agent Memory")} (${entries.length} entries)
|
|
2813
|
+
`
|
|
2814
|
+
);
|
|
2815
|
+
for (const e of entries) {
|
|
2816
|
+
console.log(
|
|
2817
|
+
` ${c.bold(e.key)} ${c.dim(new Date(e.updatedAt).toLocaleString())}`
|
|
2818
|
+
);
|
|
2819
|
+
}
|
|
2820
|
+
console.log();
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
const value = recall(key);
|
|
2824
|
+
if (value === null) {
|
|
2825
|
+
console.log(c.dim(`No memory found for "${key}"`));
|
|
2826
|
+
} else {
|
|
2827
|
+
console.log(safeStr(value));
|
|
2828
|
+
}
|
|
2829
|
+
});
|
|
2830
|
+
program2.command("forget <key>").description("Delete a key from agent memory").option("--all", "Clear all agent memory").action((key, cmd) => {
|
|
2831
|
+
if (cmd.all) {
|
|
2832
|
+
clearMemory();
|
|
2833
|
+
console.log(`${SYMBOLS.check} ${c.yellow("cleared")} all agent memory`);
|
|
2834
|
+
return;
|
|
2835
|
+
}
|
|
2836
|
+
const removed = forget(key);
|
|
2837
|
+
if (removed) {
|
|
2838
|
+
console.log(`${SYMBOLS.check} ${c.yellow("forgot")} ${c.bold(key)}`);
|
|
2839
|
+
} else {
|
|
2840
|
+
console.log(c.dim(`No memory found for "${key}"`));
|
|
2637
2841
|
}
|
|
2842
|
+
});
|
|
2843
|
+
program2.command("analyze").description(
|
|
2844
|
+
"Analyze secret usage patterns and provide optimization suggestions"
|
|
2845
|
+
).option("-g, --global", "Global scope only").option("-p, --project", "Project scope only").option("--project-path <path>", "Explicit project path").action((cmd) => {
|
|
2638
2846
|
const opts = buildOpts(cmd);
|
|
2639
|
-
const
|
|
2640
|
-
const
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
continue;
|
|
2847
|
+
const entries = listSecrets({ ...opts, silent: true });
|
|
2848
|
+
const audit = queryAudit({ limit: 1e3 });
|
|
2849
|
+
console.log(`
|
|
2850
|
+
${SYMBOLS.zap} ${c.bold("Secret Usage Analysis")}
|
|
2851
|
+
`);
|
|
2852
|
+
const accessMap = /* @__PURE__ */ new Map();
|
|
2853
|
+
for (const e of audit) {
|
|
2854
|
+
if (e.action === "read" && e.key) {
|
|
2855
|
+
accessMap.set(e.key, (accessMap.get(e.key) || 0) + 1);
|
|
2649
2856
|
}
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
}
|
|
2656
|
-
warnings.push(`STALE (${decay.lifetimePercent}%): ${key}`);
|
|
2657
|
-
}
|
|
2857
|
+
}
|
|
2858
|
+
const sorted = [...accessMap.entries()].sort((a, b) => b[1] - a[1]);
|
|
2859
|
+
if (sorted.length > 0) {
|
|
2860
|
+
console.log(` ${c.bold("Most accessed:")}`);
|
|
2861
|
+
for (const [key, count] of sorted.slice(0, 5)) {
|
|
2862
|
+
console.log(` ${c.bold(key)} \u2014 ${c.cyan(count.toString())} reads`);
|
|
2658
2863
|
}
|
|
2659
|
-
|
|
2660
|
-
lines.push(`${key}="${escaped}"`);
|
|
2864
|
+
console.log();
|
|
2661
2865
|
}
|
|
2662
|
-
const
|
|
2663
|
-
|
|
2664
|
-
|
|
2866
|
+
const neverAccessed = entries.filter((e) => {
|
|
2867
|
+
const count = e.envelope?.meta.accessCount ?? 0;
|
|
2868
|
+
return count === 0;
|
|
2869
|
+
});
|
|
2870
|
+
if (neverAccessed.length > 0) {
|
|
2665
2871
|
console.log(
|
|
2666
|
-
|
|
2872
|
+
` ${c.bold("Never accessed:")} ${c.yellow(neverAccessed.length.toString())} secrets`
|
|
2667
2873
|
);
|
|
2668
|
-
|
|
2669
|
-
|
|
2874
|
+
for (const e of neverAccessed.slice(0, 8)) {
|
|
2875
|
+
const age = e.envelope?.meta.createdAt ? c.dim(
|
|
2876
|
+
`(created ${new Date(e.envelope.meta.createdAt).toLocaleDateString()})`
|
|
2877
|
+
) : "";
|
|
2878
|
+
console.log(` ${c.dim(SYMBOLS.cross)} ${e.key} ${age}`);
|
|
2879
|
+
}
|
|
2880
|
+
console.log();
|
|
2670
2881
|
}
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2882
|
+
const expired = entries.filter((e) => e.decay?.isExpired);
|
|
2883
|
+
const stale = entries.filter(
|
|
2884
|
+
(e) => e.decay?.isStale && !e.decay?.isExpired
|
|
2885
|
+
);
|
|
2886
|
+
if (expired.length > 0) {
|
|
2887
|
+
console.log(
|
|
2888
|
+
` ${c.red("Expired:")} ${expired.length} secrets need rotation or cleanup`
|
|
2889
|
+
);
|
|
2890
|
+
for (const e of expired.slice(0, 5)) {
|
|
2891
|
+
console.log(` ${c.red(SYMBOLS.cross)} ${e.key}`);
|
|
2675
2892
|
}
|
|
2676
|
-
console.
|
|
2893
|
+
console.log();
|
|
2677
2894
|
}
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2895
|
+
if (stale.length > 0) {
|
|
2896
|
+
console.log(
|
|
2897
|
+
` ${c.yellow("Stale (>75% lifetime):")} ${stale.length} secrets approaching expiry`
|
|
2898
|
+
);
|
|
2899
|
+
for (const e of stale.slice(0, 5)) {
|
|
2900
|
+
console.log(
|
|
2901
|
+
` ${c.yellow(SYMBOLS.warning)} ${e.key} ${c.dim(`(${e.decay?.timeRemaining} remaining)`)}`
|
|
2902
|
+
);
|
|
2903
|
+
}
|
|
2904
|
+
console.log();
|
|
2905
|
+
}
|
|
2906
|
+
const globalOnly = entries.filter((e) => e.scope === "global");
|
|
2907
|
+
const withProjectTags = globalOnly.filter(
|
|
2908
|
+
(e) => e.envelope?.meta.tags?.some(
|
|
2909
|
+
(t) => ["backend", "frontend", "db", "api"].includes(t)
|
|
2910
|
+
)
|
|
2690
2911
|
);
|
|
2691
|
-
|
|
2692
|
-
`);
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2912
|
+
if (withProjectTags.length > 0) {
|
|
2913
|
+
console.log(` ${c.bold("Scope suggestions:")}`);
|
|
2914
|
+
console.log(
|
|
2915
|
+
` ${withProjectTags.length} global secret(s) have project-specific tags \u2014 consider moving to project scope`
|
|
2916
|
+
);
|
|
2917
|
+
console.log();
|
|
2697
2918
|
}
|
|
2698
|
-
const
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
}
|
|
2919
|
+
const noRotation = entries.filter(
|
|
2920
|
+
(e) => !e.envelope?.meta.rotationFormat && !e.decay?.isExpired
|
|
2921
|
+
);
|
|
2922
|
+
if (noRotation.length > 0) {
|
|
2923
|
+
console.log(` ${c.bold("Rotation suggestions:")}`);
|
|
2924
|
+
console.log(
|
|
2925
|
+
` ${noRotation.length} secret(s) have no rotation format set`
|
|
2926
|
+
);
|
|
2927
|
+
console.log(
|
|
2928
|
+
` Use ${c.bold("qring set <key> <value> --rotation-format api-key")} to enable auto-rotation`
|
|
2929
|
+
);
|
|
2930
|
+
console.log();
|
|
2931
|
+
}
|
|
2932
|
+
console.log(` ${c.bold("Summary:")}`);
|
|
2933
|
+
console.log(` Total secrets: ${entries.length}`);
|
|
2934
|
+
console.log(` Active: ${entries.length - expired.length}`);
|
|
2935
|
+
console.log(` Expired: ${expired.length}`);
|
|
2936
|
+
console.log(` Stale: ${stale.length}`);
|
|
2937
|
+
console.log(` Never accessed: ${neverAccessed.length}`);
|
|
2938
|
+
console.log(
|
|
2939
|
+
` With rotation config: ${entries.length - noRotation.length}`
|
|
2940
|
+
);
|
|
2941
|
+
console.log();
|
|
2708
2942
|
});
|
|
2709
2943
|
program2.command("agent").description("Start the autonomous agent (background monitor)").option(
|
|
2710
2944
|
"-i, --interval <seconds>",
|
|
2711
2945
|
"Scan interval in seconds",
|
|
2712
2946
|
parseInt,
|
|
2713
2947
|
60
|
|
2714
|
-
).option("--auto-rotate", "Auto-rotate expired secrets").option("-v, --verbose", "Verbose output with all warnings").option(
|
|
2948
|
+
).option("--auto-rotate", "Auto-rotate expired secrets").option("-v, --verbose", "Verbose output with all warnings").option(
|
|
2949
|
+
"--project-path <paths>",
|
|
2950
|
+
"Comma-separated project paths to monitor"
|
|
2951
|
+
).option("--once", "Run a single scan and exit (no daemon)").action(async (cmd) => {
|
|
2715
2952
|
const projectPaths = cmd.projectPath ? cmd.projectPath.split(",").map((p) => p.trim()) : [process.cwd()];
|
|
2716
2953
|
if (cmd.once) {
|
|
2717
2954
|
const report = runHealthScan({
|
|
@@ -2729,96 +2966,321 @@ ${c.dim(" dashboard stopped")}`);
|
|
|
2729
2966
|
verbose: cmd.verbose
|
|
2730
2967
|
});
|
|
2731
2968
|
});
|
|
2732
|
-
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
// src/cli/commands/security.ts
|
|
2972
|
+
function registerSecurityCommands(program2) {
|
|
2973
|
+
program2.command("approve <key>").description(
|
|
2974
|
+
"Grant a scoped, reasoned, HMAC-verified approval token for MCP secret access"
|
|
2975
|
+
).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) => {
|
|
2733
2976
|
const opts = buildOpts(cmd);
|
|
2734
|
-
const
|
|
2735
|
-
if (
|
|
2736
|
-
|
|
2737
|
-
|
|
2977
|
+
const scope = opts.scope ?? "global";
|
|
2978
|
+
if (cmd.list) {
|
|
2979
|
+
const approvals = listApprovals();
|
|
2980
|
+
if (approvals.length === 0) {
|
|
2981
|
+
console.log(c.dim(" No active approvals"));
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2984
|
+
for (const a of approvals) {
|
|
2985
|
+
const status = a.tampered ? c.red("TAMPERED") : a.valid ? c.green("active") : c.dim("expired");
|
|
2986
|
+
const ttl = Math.max(
|
|
2987
|
+
0,
|
|
2988
|
+
Math.round(
|
|
2989
|
+
(new Date(a.expiresAt).getTime() - Date.now()) / 1e3
|
|
2990
|
+
)
|
|
2991
|
+
);
|
|
2992
|
+
console.log(
|
|
2993
|
+
` ${status} ${c.bold(a.key)} [${a.scope}] reason=${c.dim(a.reason)} ttl=${ttl}s granted-by=${a.grantedBy}`
|
|
2994
|
+
);
|
|
2995
|
+
}
|
|
2738
2996
|
return;
|
|
2739
2997
|
}
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2998
|
+
if (cmd.revoke) {
|
|
2999
|
+
const revoked = revokeApproval(key, scope);
|
|
3000
|
+
if (revoked) {
|
|
3001
|
+
console.log(
|
|
3002
|
+
`${SYMBOLS.check} ${c.yellow("revoked")} approval for ${c.bold(key)}`
|
|
3003
|
+
);
|
|
3004
|
+
} else {
|
|
3005
|
+
console.log(c.dim(` No active approval found for ${key}`));
|
|
3006
|
+
}
|
|
3007
|
+
return;
|
|
2747
3008
|
}
|
|
3009
|
+
const entry = grantApproval(key, scope, cmd.for, {
|
|
3010
|
+
reason: cmd.reason ?? "manual approval"
|
|
3011
|
+
});
|
|
3012
|
+
console.log(
|
|
3013
|
+
`${SYMBOLS.check} ${c.green("approved")} ${c.bold(key)} for ${cmd.for}s`
|
|
3014
|
+
);
|
|
3015
|
+
console.log(
|
|
3016
|
+
c.dim(
|
|
3017
|
+
` id=${entry.id} reason="${entry.reason}" expires=${entry.expiresAt}`
|
|
3018
|
+
)
|
|
3019
|
+
);
|
|
2748
3020
|
});
|
|
2749
|
-
program2.command("
|
|
2750
|
-
const
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
const val = getSecret(e.key, { ...opts, scope: e.scope, silent: true });
|
|
2754
|
-
if (!val) return null;
|
|
2755
|
-
const provider = e.envelope?.meta.provider;
|
|
2756
|
-
const validationUrl = e.envelope?.meta.validationUrl;
|
|
2757
|
-
return { key: e.key, value: val, provider, validationUrl };
|
|
2758
|
-
}).filter((s) => s !== null);
|
|
2759
|
-
if (secrets.length === 0) {
|
|
2760
|
-
console.log(c.dim("No secrets to validate"));
|
|
3021
|
+
program2.command("approvals").description("List all approval tokens with verification status").action(() => {
|
|
3022
|
+
const approvals = listApprovals();
|
|
3023
|
+
if (approvals.length === 0) {
|
|
3024
|
+
console.log(c.dim(" No approvals found"));
|
|
2761
3025
|
return;
|
|
2762
3026
|
}
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
console.log(JSON.stringify(report, null, 2));
|
|
2766
|
-
} else {
|
|
2767
|
-
console.log(c.bold(`
|
|
2768
|
-
CI Secret Validation: ${report.results.length} secrets
|
|
3027
|
+
console.log(c.bold(`
|
|
3028
|
+
${SYMBOLS.lock} Approval Tokens
|
|
2769
3029
|
`));
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
console.log();
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
console.log(c.green(` All secrets valid`));
|
|
2782
|
-
}
|
|
3030
|
+
for (const a of approvals) {
|
|
3031
|
+
const status = a.tampered ? c.red(`${SYMBOLS.cross} TAMPERED`) : a.valid ? c.green(`${SYMBOLS.check} active`) : c.dim(`${SYMBOLS.warning} expired`);
|
|
3032
|
+
const ttl = Math.max(
|
|
3033
|
+
0,
|
|
3034
|
+
Math.round((new Date(a.expiresAt).getTime() - Date.now()) / 1e3)
|
|
3035
|
+
);
|
|
3036
|
+
console.log(` ${status} ${c.bold(a.key)} [${a.scope}]`);
|
|
3037
|
+
console.log(
|
|
3038
|
+
c.dim(` id=${a.id} reason="${a.reason}" ttl=${ttl}s by=${a.grantedBy}`)
|
|
3039
|
+
);
|
|
3040
|
+
if (a.workspace) console.log(c.dim(` workspace=${a.workspace}`));
|
|
2783
3041
|
}
|
|
3042
|
+
console.log();
|
|
2784
3043
|
});
|
|
2785
3044
|
program2.command("policy").description("Show project governance policy summary").option("--json", "Output as JSON").action((cmd) => {
|
|
2786
3045
|
const summary = getPolicySummary();
|
|
2787
|
-
if (cmd
|
|
3046
|
+
if (wantsJsonOutput(program2, cmd)) {
|
|
2788
3047
|
console.log(JSON.stringify(summary, null, 2));
|
|
2789
3048
|
return;
|
|
2790
3049
|
}
|
|
2791
3050
|
console.log(c.bold("\n\u2696 Governance Policy\n"));
|
|
2792
3051
|
if (!summary.hasMcpPolicy && !summary.hasExecPolicy && !summary.hasSecretPolicy) {
|
|
2793
3052
|
console.log(c.dim(" No policy configured in .q-ring.json"));
|
|
2794
|
-
console.log(
|
|
3053
|
+
console.log(
|
|
3054
|
+
c.dim(' Add a "policy" section to enable governance controls.\n')
|
|
3055
|
+
);
|
|
2795
3056
|
return;
|
|
2796
3057
|
}
|
|
2797
3058
|
if (summary.hasMcpPolicy) {
|
|
2798
3059
|
console.log(c.cyan(" MCP Policy:"));
|
|
2799
3060
|
const m = summary.details.mcp;
|
|
2800
|
-
if (m.allowTools)
|
|
2801
|
-
|
|
2802
|
-
if (m.
|
|
2803
|
-
|
|
2804
|
-
if (m.
|
|
3061
|
+
if (m.allowTools)
|
|
3062
|
+
console.log(c.green(` Allow tools: ${m.allowTools.join(", ")}`));
|
|
3063
|
+
if (m.denyTools)
|
|
3064
|
+
console.log(c.red(` Deny tools: ${m.denyTools.join(", ")}`));
|
|
3065
|
+
if (m.readableKeys)
|
|
3066
|
+
console.log(
|
|
3067
|
+
c.green(` Readable keys: ${m.readableKeys.join(", ")}`)
|
|
3068
|
+
);
|
|
3069
|
+
if (m.deniedKeys)
|
|
3070
|
+
console.log(c.red(` Denied keys: ${m.deniedKeys.join(", ")}`));
|
|
3071
|
+
if (m.deniedTags)
|
|
3072
|
+
console.log(c.red(` Denied tags: ${m.deniedTags.join(", ")}`));
|
|
2805
3073
|
}
|
|
2806
3074
|
if (summary.hasExecPolicy) {
|
|
2807
3075
|
console.log(c.cyan(" Exec Policy:"));
|
|
2808
3076
|
const e = summary.details.exec;
|
|
2809
|
-
if (e.allowCommands)
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
3077
|
+
if (e.allowCommands)
|
|
3078
|
+
console.log(
|
|
3079
|
+
c.green(` Allow commands: ${e.allowCommands.join(", ")}`)
|
|
3080
|
+
);
|
|
3081
|
+
if (e.denyCommands)
|
|
3082
|
+
console.log(
|
|
3083
|
+
c.red(` Deny commands: ${e.denyCommands.join(", ")}`)
|
|
3084
|
+
);
|
|
3085
|
+
if (e.maxRuntimeSeconds)
|
|
3086
|
+
console.log(` Max runtime: ${e.maxRuntimeSeconds}s`);
|
|
3087
|
+
if (e.allowNetwork !== void 0)
|
|
3088
|
+
console.log(` Allow network: ${e.allowNetwork}`);
|
|
2813
3089
|
}
|
|
2814
3090
|
if (summary.hasSecretPolicy) {
|
|
2815
3091
|
console.log(c.cyan(" Secret Lifecycle Policy:"));
|
|
2816
3092
|
const s = summary.details.secrets;
|
|
2817
|
-
if (s.requireApprovalForTags)
|
|
3093
|
+
if (s.requireApprovalForTags)
|
|
3094
|
+
console.log(
|
|
3095
|
+
` Require approval for tags: ${s.requireApprovalForTags.join(", ")}`
|
|
3096
|
+
);
|
|
2818
3097
|
if (s.maxTtlSeconds) console.log(` Max TTL: ${s.maxTtlSeconds}s`);
|
|
2819
3098
|
}
|
|
2820
3099
|
console.log();
|
|
2821
3100
|
});
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
// src/cli/commands.ts
|
|
3104
|
+
var COMMAND_GROUPS = [
|
|
3105
|
+
{
|
|
3106
|
+
name: "Secrets",
|
|
3107
|
+
symbol: SYMBOLS.key,
|
|
3108
|
+
commands: [
|
|
3109
|
+
"set",
|
|
3110
|
+
"get",
|
|
3111
|
+
"delete",
|
|
3112
|
+
"list",
|
|
3113
|
+
"inspect",
|
|
3114
|
+
"export",
|
|
3115
|
+
"import"
|
|
3116
|
+
]
|
|
3117
|
+
},
|
|
3118
|
+
{
|
|
3119
|
+
name: "Project",
|
|
3120
|
+
symbol: SYMBOLS.package,
|
|
3121
|
+
commands: ["context", "check", "env", "env:generate", "wizard"]
|
|
3122
|
+
},
|
|
3123
|
+
{
|
|
3124
|
+
name: "Quantum",
|
|
3125
|
+
symbol: SYMBOLS.sparkle,
|
|
3126
|
+
commands: [
|
|
3127
|
+
"generate",
|
|
3128
|
+
"entangle",
|
|
3129
|
+
"disentangle",
|
|
3130
|
+
"tunnel",
|
|
3131
|
+
"teleport"
|
|
3132
|
+
]
|
|
3133
|
+
},
|
|
3134
|
+
{
|
|
3135
|
+
name: "Validation & Rotation",
|
|
3136
|
+
symbol: SYMBOLS.shield,
|
|
3137
|
+
commands: ["validate", "rotate", "ci:validate"]
|
|
3138
|
+
},
|
|
3139
|
+
{
|
|
3140
|
+
name: "Dev Tooling",
|
|
3141
|
+
symbol: SYMBOLS.zap,
|
|
3142
|
+
commands: ["exec", "scan", "lint", "status"]
|
|
3143
|
+
},
|
|
3144
|
+
{
|
|
3145
|
+
name: "Audit & Health",
|
|
3146
|
+
symbol: SYMBOLS.eye,
|
|
3147
|
+
commands: ["audit", "audit:verify", "audit:export", "health"]
|
|
3148
|
+
},
|
|
3149
|
+
{
|
|
3150
|
+
name: "Hooks",
|
|
3151
|
+
symbol: SYMBOLS.link,
|
|
3152
|
+
commands: [
|
|
3153
|
+
"hook:install",
|
|
3154
|
+
"hook:uninstall",
|
|
3155
|
+
"hook:run",
|
|
3156
|
+
"hook"
|
|
3157
|
+
]
|
|
3158
|
+
},
|
|
3159
|
+
{
|
|
3160
|
+
name: "Agent Memory",
|
|
3161
|
+
symbol: SYMBOLS.ghost,
|
|
3162
|
+
commands: ["remember", "recall", "forget", "analyze", "agent"]
|
|
3163
|
+
},
|
|
3164
|
+
{
|
|
3165
|
+
name: "Security & Governance",
|
|
3166
|
+
symbol: SYMBOLS.lock,
|
|
3167
|
+
commands: ["approve", "approvals", "policy"]
|
|
3168
|
+
}
|
|
3169
|
+
];
|
|
3170
|
+
var GroupedHelp = class extends Help {
|
|
3171
|
+
formatHelp(cmd, helper) {
|
|
3172
|
+
if (cmd.parent) return super.formatHelp(cmd, helper);
|
|
3173
|
+
const termWidth = helper.padWidth(cmd, helper);
|
|
3174
|
+
const helpWidth = helper.helpWidth ?? 80;
|
|
3175
|
+
const sections = [];
|
|
3176
|
+
sections.push(
|
|
3177
|
+
`${c.bold("qring")} ${c.dim(`v${PACKAGE_VERSION}`)} ${c.dim("\u2014")} ${c.dim(
|
|
3178
|
+
"quantum keyring for AI coding tools"
|
|
3179
|
+
)}`
|
|
3180
|
+
);
|
|
3181
|
+
sections.push("");
|
|
3182
|
+
sections.push(`${c.bold("Usage:")} qring [options] [command]`);
|
|
3183
|
+
const options = helper.visibleOptions(cmd);
|
|
3184
|
+
if (options.length > 0) {
|
|
3185
|
+
sections.push("");
|
|
3186
|
+
sections.push(c.bold("Options:"));
|
|
3187
|
+
for (const opt of options) {
|
|
3188
|
+
const term = helper.optionTerm(opt);
|
|
3189
|
+
const desc = helper.optionDescription(opt);
|
|
3190
|
+
sections.push(formatRow(term, desc, termWidth, helpWidth));
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
const commands = helper.visibleCommands(cmd);
|
|
3194
|
+
if (commands.length === 0) return sections.join("\n") + "\n";
|
|
3195
|
+
const byName = /* @__PURE__ */ new Map();
|
|
3196
|
+
for (const sub of commands) byName.set(sub.name(), sub);
|
|
3197
|
+
const used = /* @__PURE__ */ new Set();
|
|
3198
|
+
for (const group of COMMAND_GROUPS) {
|
|
3199
|
+
const members = group.commands.map((n) => byName.get(n)).filter((x) => Boolean(x));
|
|
3200
|
+
if (members.length === 0) continue;
|
|
3201
|
+
for (const m of members) used.add(m.name());
|
|
3202
|
+
sections.push("");
|
|
3203
|
+
sections.push(`${group.symbol} ${c.bold(group.name)}`);
|
|
3204
|
+
for (const sub of members) {
|
|
3205
|
+
sections.push(
|
|
3206
|
+
formatRow(
|
|
3207
|
+
helper.subcommandTerm(sub),
|
|
3208
|
+
helper.subcommandDescription(sub),
|
|
3209
|
+
termWidth,
|
|
3210
|
+
helpWidth
|
|
3211
|
+
)
|
|
3212
|
+
);
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
const other = commands.filter((sub) => !used.has(sub.name()));
|
|
3216
|
+
if (other.length > 0) {
|
|
3217
|
+
sections.push("");
|
|
3218
|
+
sections.push(c.bold("Other:"));
|
|
3219
|
+
for (const sub of other) {
|
|
3220
|
+
sections.push(
|
|
3221
|
+
formatRow(
|
|
3222
|
+
helper.subcommandTerm(sub),
|
|
3223
|
+
helper.subcommandDescription(sub),
|
|
3224
|
+
termWidth,
|
|
3225
|
+
helpWidth
|
|
3226
|
+
)
|
|
3227
|
+
);
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
sections.push("");
|
|
3231
|
+
sections.push(
|
|
3232
|
+
c.dim(
|
|
3233
|
+
`Run ${c.bold("qring <command> --help")} to see detailed flags for any command.`
|
|
3234
|
+
)
|
|
3235
|
+
);
|
|
3236
|
+
return sections.join("\n") + "\n";
|
|
3237
|
+
}
|
|
3238
|
+
};
|
|
3239
|
+
function formatRow(term, description, termWidth, helpWidth) {
|
|
3240
|
+
const indent = " ";
|
|
3241
|
+
const gap = " ";
|
|
3242
|
+
const padded = term.padEnd(termWidth);
|
|
3243
|
+
if (!description) return `${indent}${padded}`;
|
|
3244
|
+
const available = Math.max(helpWidth - indent.length - termWidth - gap.length, 20);
|
|
3245
|
+
const desc = wrapText(description, available).replace(
|
|
3246
|
+
/\n/g,
|
|
3247
|
+
`
|
|
3248
|
+
${indent}${" ".repeat(termWidth)}${gap}`
|
|
3249
|
+
);
|
|
3250
|
+
return `${indent}${padded}${gap}${c.dim(desc)}`;
|
|
3251
|
+
}
|
|
3252
|
+
function wrapText(text, width) {
|
|
3253
|
+
if (text.length <= width) return text;
|
|
3254
|
+
const lines = [];
|
|
3255
|
+
let current = "";
|
|
3256
|
+
for (const word of text.split(/\s+/)) {
|
|
3257
|
+
if (current.length + word.length + 1 > width) {
|
|
3258
|
+
lines.push(current);
|
|
3259
|
+
current = word;
|
|
3260
|
+
} else {
|
|
3261
|
+
current = current ? `${current} ${word}` : word;
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
if (current) lines.push(current);
|
|
3265
|
+
return lines.join("\n");
|
|
3266
|
+
}
|
|
3267
|
+
function createProgram() {
|
|
3268
|
+
const program2 = new Command().name("qring").description(
|
|
3269
|
+
`${c.bold("q-ring")} ${c.dim("\u2014 quantum keyring for AI coding tools")}`
|
|
3270
|
+
).version(PACKAGE_VERSION).option("--json", "Machine-readable JSON output for supported commands").configureHelp({
|
|
3271
|
+
sortSubcommands: false,
|
|
3272
|
+
sortOptions: false
|
|
3273
|
+
}).showHelpAfterError(c.dim("(run `qring --help` for usage)"));
|
|
3274
|
+
program2.createHelp = () => new GroupedHelp();
|
|
3275
|
+
registerSecretsCommands(program2);
|
|
3276
|
+
registerProjectCommands(program2);
|
|
3277
|
+
registerQuantumCommands(program2);
|
|
3278
|
+
registerValidationCommands(program2);
|
|
3279
|
+
registerToolingCommands(program2);
|
|
3280
|
+
registerAuditCommands(program2);
|
|
3281
|
+
registerHookCommands(program2);
|
|
3282
|
+
registerAgentCommands(program2);
|
|
3283
|
+
registerSecurityCommands(program2);
|
|
2822
3284
|
return program2;
|
|
2823
3285
|
}
|
|
2824
3286
|
|