@i4ctime/q-ring 0.9.8 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/{chunk-ZV7LRKBA.js → chunk-E46HG5NM.js} +364 -58
- package/dist/chunk-E46HG5NM.js.map +1 -0
- package/dist/{chunk-JV4JFWV6.js → chunk-LLMO64FE.js} +365 -59
- package/dist/chunk-LLMO64FE.js.map +1 -0
- package/dist/{dashboard-4H474M2N.js → dashboard-73QYM5FU.js} +2 -2
- package/dist/{dashboard-5L4ILERJ.js → dashboard-CMXRH4AQ.js} +2 -2
- package/dist/index.js +2811 -2221
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +1841 -1576
- 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.map → dashboard-73QYM5FU.js.map} +0 -0
- /package/dist/{dashboard-5L4ILERJ.js.map → dashboard-CMXRH4AQ.js.map} +0 -0
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/core/envelope.ts
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
var EntanglementLinkSchema = z.object({
|
|
6
|
+
service: z.string(),
|
|
7
|
+
key: z.string()
|
|
8
|
+
});
|
|
9
|
+
var SecretMetadataSchema = z.object({
|
|
10
|
+
createdAt: z.string(),
|
|
11
|
+
updatedAt: z.string(),
|
|
12
|
+
expiresAt: z.string().optional(),
|
|
13
|
+
ttlSeconds: z.number().optional(),
|
|
14
|
+
description: z.string().optional(),
|
|
15
|
+
tags: z.array(z.string()).optional(),
|
|
16
|
+
entangled: z.array(EntanglementLinkSchema).optional(),
|
|
17
|
+
accessCount: z.number(),
|
|
18
|
+
lastAccessedAt: z.string().optional(),
|
|
19
|
+
ephemeral: z.boolean().optional(),
|
|
20
|
+
rotationFormat: z.string().optional(),
|
|
21
|
+
rotationPrefix: z.string().optional(),
|
|
22
|
+
provider: z.string().optional(),
|
|
23
|
+
validationUrl: z.string().optional(),
|
|
24
|
+
requiresApproval: z.boolean().optional(),
|
|
25
|
+
jitProvider: z.string().optional(),
|
|
26
|
+
jitExpiresAt: z.string().optional()
|
|
27
|
+
});
|
|
28
|
+
var QuantumEnvelopeSchema = z.object({
|
|
29
|
+
v: z.literal(1),
|
|
30
|
+
value: z.string().optional(),
|
|
31
|
+
states: z.record(z.string(), z.string()).optional(),
|
|
32
|
+
defaultEnv: z.string().optional(),
|
|
33
|
+
meta: SecretMetadataSchema
|
|
34
|
+
});
|
|
4
35
|
function createEnvelope(value, opts) {
|
|
5
36
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6
37
|
let expiresAt = opts?.expiresAt;
|
|
@@ -32,8 +63,9 @@ function createEnvelope(value, opts) {
|
|
|
32
63
|
function parseEnvelope(raw) {
|
|
33
64
|
try {
|
|
34
65
|
const parsed = JSON.parse(raw);
|
|
35
|
-
|
|
36
|
-
|
|
66
|
+
const r = QuantumEnvelopeSchema.safeParse(parsed);
|
|
67
|
+
if (r.success) {
|
|
68
|
+
return r.data;
|
|
37
69
|
}
|
|
38
70
|
} catch {
|
|
39
71
|
}
|
|
@@ -84,6 +116,15 @@ function checkDecay(envelope) {
|
|
|
84
116
|
const now = Date.now();
|
|
85
117
|
const expires = new Date(envelope.meta.expiresAt).getTime();
|
|
86
118
|
const created = new Date(envelope.meta.createdAt).getTime();
|
|
119
|
+
if (!Number.isFinite(expires) || !Number.isFinite(created)) {
|
|
120
|
+
return {
|
|
121
|
+
isExpired: true,
|
|
122
|
+
isStale: true,
|
|
123
|
+
lifetimePercent: 100,
|
|
124
|
+
secondsRemaining: null,
|
|
125
|
+
timeRemaining: "invalid date"
|
|
126
|
+
};
|
|
127
|
+
}
|
|
87
128
|
const totalLifetime = expires - created;
|
|
88
129
|
const elapsed = now - created;
|
|
89
130
|
const remaining = expires - now;
|
|
@@ -188,12 +229,15 @@ function collapseEnvironment(ctx = {}) {
|
|
|
188
229
|
}
|
|
189
230
|
return null;
|
|
190
231
|
}
|
|
232
|
+
var MAX_BRANCH_GLOB_PATTERN_LEN = 200;
|
|
233
|
+
var MAX_BRANCH_GLOB_REGEX_SOURCE = 400;
|
|
191
234
|
function matchGlob(branchMap, branch) {
|
|
192
235
|
for (const [pattern, env] of Object.entries(branchMap)) {
|
|
193
236
|
if (!pattern.includes("*")) continue;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
);
|
|
237
|
+
if (pattern.length > MAX_BRANCH_GLOB_PATTERN_LEN) continue;
|
|
238
|
+
const source = "^" + pattern.replace(/\*/g, ".*") + "$";
|
|
239
|
+
if (source.length > MAX_BRANCH_GLOB_REGEX_SOURCE) continue;
|
|
240
|
+
const regex = new RegExp(source);
|
|
197
241
|
if (regex.test(branch)) return env;
|
|
198
242
|
}
|
|
199
243
|
return void 0;
|
|
@@ -206,11 +250,27 @@ function mapEnvName(raw) {
|
|
|
206
250
|
}
|
|
207
251
|
|
|
208
252
|
// src/core/observer.ts
|
|
209
|
-
import {
|
|
253
|
+
import {
|
|
254
|
+
existsSync as existsSync2,
|
|
255
|
+
mkdirSync,
|
|
256
|
+
appendFileSync,
|
|
257
|
+
readFileSync as readFileSync2,
|
|
258
|
+
openSync,
|
|
259
|
+
fstatSync,
|
|
260
|
+
readSync,
|
|
261
|
+
closeSync,
|
|
262
|
+
statSync
|
|
263
|
+
} from "fs";
|
|
210
264
|
import { join as join2 } from "path";
|
|
211
265
|
import { homedir } from "os";
|
|
212
266
|
import { createHash } from "crypto";
|
|
213
267
|
function getAuditDir() {
|
|
268
|
+
if (process.env.QRING_AUDIT_DIR) {
|
|
269
|
+
if (!existsSync2(process.env.QRING_AUDIT_DIR)) {
|
|
270
|
+
mkdirSync(process.env.QRING_AUDIT_DIR, { recursive: true });
|
|
271
|
+
}
|
|
272
|
+
return process.env.QRING_AUDIT_DIR;
|
|
273
|
+
}
|
|
214
274
|
const dir = join2(homedir(), ".config", "q-ring");
|
|
215
275
|
if (!existsSync2(dir)) {
|
|
216
276
|
mkdirSync(dir, { recursive: true });
|
|
@@ -256,11 +316,24 @@ function logAudit(event) {
|
|
|
256
316
|
} catch {
|
|
257
317
|
}
|
|
258
318
|
}
|
|
319
|
+
var MAX_AUDIT_BYTES = 12 * 1024 * 1024;
|
|
259
320
|
function queryAudit(query = {}) {
|
|
260
321
|
const path = getAuditPath();
|
|
261
322
|
if (!existsSync2(path)) return [];
|
|
262
323
|
try {
|
|
263
|
-
const
|
|
324
|
+
const st = statSync(path);
|
|
325
|
+
const readStart = st.size > MAX_AUDIT_BYTES ? st.size - MAX_AUDIT_BYTES : 0;
|
|
326
|
+
const readLen = st.size > MAX_AUDIT_BYTES ? MAX_AUDIT_BYTES : st.size;
|
|
327
|
+
const buf = Buffer.alloc(readLen);
|
|
328
|
+
const fd = openSync(path, "r");
|
|
329
|
+
readSync(fd, buf, 0, readLen, readStart);
|
|
330
|
+
closeSync(fd);
|
|
331
|
+
let text = buf.toString("utf8");
|
|
332
|
+
if (readStart > 0) {
|
|
333
|
+
const firstNl = text.indexOf("\n");
|
|
334
|
+
if (firstNl !== -1) text = text.slice(firstNl + 1);
|
|
335
|
+
}
|
|
336
|
+
const lines = text.split("\n").filter((l) => l.trim());
|
|
264
337
|
let events = lines.map((line) => {
|
|
265
338
|
try {
|
|
266
339
|
return JSON.parse(line);
|
|
@@ -393,7 +466,7 @@ function detectAnomalies(key) {
|
|
|
393
466
|
}
|
|
394
467
|
|
|
395
468
|
// src/core/entanglement.ts
|
|
396
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync
|
|
469
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
397
470
|
import { join as join3 } from "path";
|
|
398
471
|
import { homedir as homedir2 } from "os";
|
|
399
472
|
function getRegistryPath() {
|
|
@@ -415,7 +488,9 @@ function loadRegistry() {
|
|
|
415
488
|
}
|
|
416
489
|
}
|
|
417
490
|
function saveRegistry(registry2) {
|
|
418
|
-
|
|
491
|
+
writeFileSync(getRegistryPath(), JSON.stringify(registry2, null, 2), {
|
|
492
|
+
mode: 384
|
|
493
|
+
});
|
|
419
494
|
}
|
|
420
495
|
function entangle(source, target) {
|
|
421
496
|
const registry2 = loadRegistry();
|
|
@@ -454,6 +529,9 @@ function listEntanglements() {
|
|
|
454
529
|
}
|
|
455
530
|
|
|
456
531
|
// src/core/keyring.ts
|
|
532
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, unlinkSync } from "fs";
|
|
533
|
+
import { homedir as homedir5 } from "os";
|
|
534
|
+
import { join as join6 } from "path";
|
|
457
535
|
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
458
536
|
|
|
459
537
|
// src/utils/hash.ts
|
|
@@ -509,18 +587,18 @@ function resolveScope(opts) {
|
|
|
509
587
|
}
|
|
510
588
|
|
|
511
589
|
// src/core/hooks.ts
|
|
512
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as
|
|
590
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
513
591
|
import { join as join4 } from "path";
|
|
514
592
|
import { homedir as homedir3 } from "os";
|
|
515
|
-
import {
|
|
593
|
+
import { execFile, spawn } from "child_process";
|
|
516
594
|
import { randomUUID } from "crypto";
|
|
517
595
|
|
|
518
596
|
// src/utils/http-request.ts
|
|
519
597
|
import { request as httpsRequest } from "https";
|
|
520
|
-
import { request as
|
|
598
|
+
import { request as httpRequestPlain } from "http";
|
|
521
599
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
522
600
|
var DEFAULT_MAX_RESPONSE_BYTES = 65536;
|
|
523
|
-
function
|
|
601
|
+
function httpRequest(opts) {
|
|
524
602
|
const {
|
|
525
603
|
url,
|
|
526
604
|
method = "GET",
|
|
@@ -535,7 +613,7 @@ function httpRequest_(opts) {
|
|
|
535
613
|
reject(new Error(`Unsupported URL protocol: ${parsed.protocol}`));
|
|
536
614
|
return;
|
|
537
615
|
}
|
|
538
|
-
const reqFn = parsed.protocol === "https:" ? httpsRequest :
|
|
616
|
+
const reqFn = parsed.protocol === "https:" ? httpsRequest : httpRequestPlain;
|
|
539
617
|
const reqHeaders = { ...headers };
|
|
540
618
|
if (body && !reqHeaders["Content-Length"]) {
|
|
541
619
|
reqHeaders["Content-Length"] = Buffer.byteLength(body);
|
|
@@ -598,6 +676,19 @@ function httpRequest_(opts) {
|
|
|
598
676
|
|
|
599
677
|
// src/core/ssrf.ts
|
|
600
678
|
import { lookup } from "dns/promises";
|
|
679
|
+
import * as dns from "dns";
|
|
680
|
+
import { isIPv4, isIPv6 } from "net";
|
|
681
|
+
function lookupAddressesSync(hostname) {
|
|
682
|
+
const lookupSync2 = dns.lookupSync;
|
|
683
|
+
return lookupSync2(hostname, { all: true });
|
|
684
|
+
}
|
|
685
|
+
function isHostnameIpLiteral(hostname) {
|
|
686
|
+
if (isIPv4(hostname)) return true;
|
|
687
|
+
if (hostname.startsWith("[") && hostname.endsWith("]")) {
|
|
688
|
+
return isIPv6(hostname.slice(1, -1));
|
|
689
|
+
}
|
|
690
|
+
return isIPv6(hostname);
|
|
691
|
+
}
|
|
601
692
|
function isPrivateIP(ip) {
|
|
602
693
|
const octet = "(?:25[0-5]|2[0-4]\\d|1?\\d{1,2})";
|
|
603
694
|
const ipv4Re = new RegExp(`^::ffff:(${octet}\\.${octet}\\.${octet}\\.${octet})$`, "i");
|
|
@@ -632,15 +723,39 @@ async function checkSSRF(url) {
|
|
|
632
723
|
}
|
|
633
724
|
return null;
|
|
634
725
|
}
|
|
635
|
-
function
|
|
726
|
+
function checkJitHttpProvisionUrl(url) {
|
|
636
727
|
if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") return null;
|
|
637
728
|
try {
|
|
638
729
|
const parsed = new URL(url);
|
|
730
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
731
|
+
return "Blocked: JIT HTTP provider only allows http: or https: URLs.";
|
|
732
|
+
}
|
|
639
733
|
const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
734
|
+
if (!hostname) {
|
|
735
|
+
return "Blocked: empty hostname in JIT URL.";
|
|
736
|
+
}
|
|
640
737
|
if (isPrivateIP(hostname)) {
|
|
641
738
|
return `Blocked: URL resolves to private address (${hostname}). Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
642
739
|
}
|
|
740
|
+
if (isHostnameIpLiteral(hostname)) {
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
let results;
|
|
744
|
+
try {
|
|
745
|
+
results = lookupAddressesSync(hostname);
|
|
746
|
+
} catch {
|
|
747
|
+
return `Blocked: DNS resolution failed for "${hostname}" (JIT HTTP provisioning fails closed).`;
|
|
748
|
+
}
|
|
749
|
+
if (!results.length) {
|
|
750
|
+
return `Blocked: DNS returned no addresses for "${hostname}".`;
|
|
751
|
+
}
|
|
752
|
+
for (const { address } of results) {
|
|
753
|
+
if (isPrivateIP(address)) {
|
|
754
|
+
return `Blocked: URL "${hostname}" resolves to private address ${address}. Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
643
757
|
} catch {
|
|
758
|
+
return "Blocked: malformed JIT HTTP URL.";
|
|
644
759
|
}
|
|
645
760
|
return null;
|
|
646
761
|
}
|
|
@@ -665,7 +780,9 @@ function loadRegistry2() {
|
|
|
665
780
|
}
|
|
666
781
|
}
|
|
667
782
|
function saveRegistry2(registry2) {
|
|
668
|
-
|
|
783
|
+
writeFileSync2(getRegistryPath2(), JSON.stringify(registry2, null, 2), {
|
|
784
|
+
mode: 384
|
|
785
|
+
});
|
|
669
786
|
}
|
|
670
787
|
function registerHook(entry) {
|
|
671
788
|
const registry2 = loadRegistry2();
|
|
@@ -705,6 +822,14 @@ function matchesHook(hook, payload, tags) {
|
|
|
705
822
|
if (m.scope && m.scope !== payload.scope) return false;
|
|
706
823
|
return true;
|
|
707
824
|
}
|
|
825
|
+
var SAFE_PGREP_NAME = /^[a-zA-Z0-9._:/-]{1,256}$/;
|
|
826
|
+
function shellRunner() {
|
|
827
|
+
if (process.platform === "win32") {
|
|
828
|
+
const comspec = process.env.ComSpec ?? "cmd.exe";
|
|
829
|
+
return { file: comspec, args: ["/d", "/s", "/c"] };
|
|
830
|
+
}
|
|
831
|
+
return { file: "/bin/sh", args: ["-c"] };
|
|
832
|
+
}
|
|
708
833
|
function executeShell(command, payload) {
|
|
709
834
|
return new Promise((resolve) => {
|
|
710
835
|
const env = {
|
|
@@ -713,13 +838,33 @@ function executeShell(command, payload) {
|
|
|
713
838
|
QRING_HOOK_ACTION: payload.action,
|
|
714
839
|
QRING_HOOK_SCOPE: payload.scope
|
|
715
840
|
};
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
841
|
+
const { file, args } = shellRunner();
|
|
842
|
+
execFile(
|
|
843
|
+
file,
|
|
844
|
+
[...args, command],
|
|
845
|
+
{
|
|
846
|
+
timeout: 3e4,
|
|
847
|
+
env,
|
|
848
|
+
windowsHide: true,
|
|
849
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
850
|
+
encoding: "utf8"
|
|
851
|
+
},
|
|
852
|
+
(err, stdout) => {
|
|
853
|
+
if (err) {
|
|
854
|
+
resolve({
|
|
855
|
+
hookId: "",
|
|
856
|
+
success: false,
|
|
857
|
+
message: `Shell error: ${err.message}`
|
|
858
|
+
});
|
|
859
|
+
} else {
|
|
860
|
+
resolve({
|
|
861
|
+
hookId: "",
|
|
862
|
+
success: true,
|
|
863
|
+
message: (stdout ?? "").trim() || "OK"
|
|
864
|
+
});
|
|
865
|
+
}
|
|
721
866
|
}
|
|
722
|
-
|
|
867
|
+
);
|
|
723
868
|
});
|
|
724
869
|
}
|
|
725
870
|
async function executeHttp(url, payload) {
|
|
@@ -736,7 +881,7 @@ async function executeHttp(url, payload) {
|
|
|
736
881
|
}
|
|
737
882
|
try {
|
|
738
883
|
const body = JSON.stringify(payload);
|
|
739
|
-
const res = await
|
|
884
|
+
const res = await httpRequest({
|
|
740
885
|
url,
|
|
741
886
|
method: "POST",
|
|
742
887
|
headers: {
|
|
@@ -761,24 +906,48 @@ async function executeHttp(url, payload) {
|
|
|
761
906
|
}
|
|
762
907
|
function executeSignal(target, signal = "SIGHUP") {
|
|
763
908
|
return new Promise((resolve) => {
|
|
764
|
-
const
|
|
765
|
-
if (
|
|
909
|
+
const trimmed = target.trim();
|
|
910
|
+
if (/^\d+$/.test(trimmed)) {
|
|
911
|
+
const pid = parseInt(trimmed, 10);
|
|
766
912
|
try {
|
|
767
913
|
process.kill(pid, signal);
|
|
768
|
-
resolve({
|
|
914
|
+
resolve({
|
|
915
|
+
hookId: "",
|
|
916
|
+
success: true,
|
|
917
|
+
message: `Signal ${signal} sent to PID ${pid}`
|
|
918
|
+
});
|
|
769
919
|
} catch (err) {
|
|
770
|
-
resolve({
|
|
920
|
+
resolve({
|
|
921
|
+
hookId: "",
|
|
922
|
+
success: false,
|
|
923
|
+
message: `Signal error: ${err instanceof Error ? err.message : String(err)}`
|
|
924
|
+
});
|
|
771
925
|
}
|
|
772
926
|
return;
|
|
773
927
|
}
|
|
774
|
-
|
|
928
|
+
if (!SAFE_PGREP_NAME.test(trimmed)) {
|
|
929
|
+
resolve({
|
|
930
|
+
hookId: "",
|
|
931
|
+
success: false,
|
|
932
|
+
message: "Signal target must be a numeric PID, or a process name matching /^[a-zA-Z0-9._:/-]{1,256}$/ (no spaces or shell metacharacters)."
|
|
933
|
+
});
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
const child = spawn("pgrep", ["-f", trimmed], {
|
|
937
|
+
timeout: 5e3,
|
|
938
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
939
|
+
});
|
|
775
940
|
let stdout = "";
|
|
776
941
|
child.stdout.on("data", (d) => {
|
|
777
942
|
stdout += d.toString();
|
|
778
943
|
});
|
|
779
944
|
child.on("close", (code) => {
|
|
780
945
|
if (code !== 0 || !stdout.trim()) {
|
|
781
|
-
resolve({
|
|
946
|
+
resolve({
|
|
947
|
+
hookId: "",
|
|
948
|
+
success: false,
|
|
949
|
+
message: `Process "${trimmed}" not found`
|
|
950
|
+
});
|
|
782
951
|
return;
|
|
783
952
|
}
|
|
784
953
|
const pids = stdout.trim().split("\n").map((p) => parseInt(p.trim(), 10)).filter((p) => !isNaN(p));
|
|
@@ -790,10 +959,18 @@ function executeSignal(target, signal = "SIGHUP") {
|
|
|
790
959
|
} catch {
|
|
791
960
|
}
|
|
792
961
|
}
|
|
793
|
-
resolve({
|
|
962
|
+
resolve({
|
|
963
|
+
hookId: "",
|
|
964
|
+
success: sent > 0,
|
|
965
|
+
message: `Signal ${signal} sent to ${sent} process(es)`
|
|
966
|
+
});
|
|
794
967
|
});
|
|
795
968
|
child.on("error", () => {
|
|
796
|
-
resolve({
|
|
969
|
+
resolve({
|
|
970
|
+
hookId: "",
|
|
971
|
+
success: false,
|
|
972
|
+
message: `Process "${trimmed}" not found`
|
|
973
|
+
});
|
|
797
974
|
});
|
|
798
975
|
});
|
|
799
976
|
}
|
|
@@ -850,33 +1027,50 @@ async function fireHooks(payload, tags) {
|
|
|
850
1027
|
}
|
|
851
1028
|
|
|
852
1029
|
// src/core/approval.ts
|
|
853
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as
|
|
1030
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
854
1031
|
import { join as join5 } from "path";
|
|
855
1032
|
import { homedir as homedir4 } from "os";
|
|
856
|
-
import { createHmac, randomBytes } from "crypto";
|
|
1033
|
+
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
857
1034
|
function getHmacSecret() {
|
|
858
|
-
const secretPath = join5(homedir4(), ".config", "q-ring", ".approval-key");
|
|
859
1035
|
const dir = join5(homedir4(), ".config", "q-ring");
|
|
860
|
-
|
|
1036
|
+
const secretPath = join5(dir, ".approval-key");
|
|
1037
|
+
if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
861
1038
|
if (existsSync5(secretPath)) {
|
|
862
1039
|
return readFileSync5(secretPath, "utf8").trim();
|
|
863
1040
|
}
|
|
864
1041
|
const secret = randomBytes(32).toString("hex");
|
|
865
|
-
|
|
1042
|
+
writeFileSync3(secretPath, secret, { mode: 384 });
|
|
866
1043
|
return secret;
|
|
867
1044
|
}
|
|
868
1045
|
function computeHmac(entry) {
|
|
869
|
-
const payload =
|
|
1046
|
+
const payload = [
|
|
1047
|
+
entry.id,
|
|
1048
|
+
entry.key,
|
|
1049
|
+
entry.scope,
|
|
1050
|
+
entry.reason,
|
|
1051
|
+
entry.grantedBy,
|
|
1052
|
+
entry.grantedAt,
|
|
1053
|
+
entry.expiresAt,
|
|
1054
|
+
entry.workspace ?? "",
|
|
1055
|
+
entry.sessionId ?? ""
|
|
1056
|
+
].join("|");
|
|
870
1057
|
return createHmac("sha256", getHmacSecret()).update(payload).digest("hex");
|
|
871
1058
|
}
|
|
872
1059
|
function verifyHmac(entry) {
|
|
873
1060
|
const expected = computeHmac(entry);
|
|
874
|
-
|
|
1061
|
+
const a = Buffer.from(expected, "utf8");
|
|
1062
|
+
const b = Buffer.from(entry.hmac, "utf8");
|
|
1063
|
+
if (a.length !== b.length) return false;
|
|
1064
|
+
try {
|
|
1065
|
+
return timingSafeEqual(a, b);
|
|
1066
|
+
} catch {
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
875
1069
|
}
|
|
876
1070
|
function getRegistryPath3() {
|
|
877
1071
|
const dir = join5(homedir4(), ".config", "q-ring");
|
|
878
1072
|
if (!existsSync5(dir)) {
|
|
879
|
-
mkdirSync4(dir, { recursive: true });
|
|
1073
|
+
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
880
1074
|
}
|
|
881
1075
|
return join5(dir, "approvals.json");
|
|
882
1076
|
}
|
|
@@ -904,6 +1098,20 @@ function hasApproval(key, scope) {
|
|
|
904
1098
|
|
|
905
1099
|
// src/core/provision.ts
|
|
906
1100
|
import { execFileSync, spawnSync } from "child_process";
|
|
1101
|
+
import { z as z2 } from "zod";
|
|
1102
|
+
var AwsStsConfigSchema = z2.object({
|
|
1103
|
+
roleArn: z2.string(),
|
|
1104
|
+
sessionName: z2.string().optional(),
|
|
1105
|
+
durationSeconds: z2.number().optional()
|
|
1106
|
+
});
|
|
1107
|
+
var HttpJitConfigSchema = z2.object({
|
|
1108
|
+
url: z2.string(),
|
|
1109
|
+
method: z2.string().optional(),
|
|
1110
|
+
valuePath: z2.string().optional(),
|
|
1111
|
+
expiresInSeconds: z2.number().optional(),
|
|
1112
|
+
headers: z2.record(z2.string(), z2.string()).optional(),
|
|
1113
|
+
body: z2.unknown().optional()
|
|
1114
|
+
});
|
|
907
1115
|
var ProvisionRegistry = class {
|
|
908
1116
|
providers = /* @__PURE__ */ new Map();
|
|
909
1117
|
register(provider) {
|
|
@@ -920,16 +1128,20 @@ var awsStsProvider = {
|
|
|
920
1128
|
name: "aws-sts",
|
|
921
1129
|
description: "AWS STS AssumeRole (requires existing local AWS CLI credentials)",
|
|
922
1130
|
provision(configRaw) {
|
|
923
|
-
let
|
|
1131
|
+
let raw;
|
|
924
1132
|
try {
|
|
925
|
-
|
|
1133
|
+
raw = JSON.parse(configRaw);
|
|
926
1134
|
} catch {
|
|
927
1135
|
throw new Error('aws-sts requires valid JSON config (e.g. {"roleArn":"arn:aws:..."})');
|
|
928
1136
|
}
|
|
1137
|
+
const parsed = AwsStsConfigSchema.safeParse(raw);
|
|
1138
|
+
if (!parsed.success) {
|
|
1139
|
+
throw new Error(`aws-sts invalid config: ${parsed.error.message}`);
|
|
1140
|
+
}
|
|
1141
|
+
const config = parsed.data;
|
|
929
1142
|
const roleArn = config.roleArn;
|
|
930
1143
|
const sessionName = config.sessionName || "q-ring-agent";
|
|
931
1144
|
const duration = config.durationSeconds || 3600;
|
|
932
|
-
if (!roleArn) throw new Error("aws-sts requires roleArn in config");
|
|
933
1145
|
try {
|
|
934
1146
|
const output = execFileSync("aws", [
|
|
935
1147
|
"sts",
|
|
@@ -943,8 +1155,8 @@ var awsStsProvider = {
|
|
|
943
1155
|
"--output",
|
|
944
1156
|
"json"
|
|
945
1157
|
], { encoding: "utf8" });
|
|
946
|
-
const
|
|
947
|
-
const creds =
|
|
1158
|
+
const parsed2 = JSON.parse(output);
|
|
1159
|
+
const creds = parsed2.Credentials;
|
|
948
1160
|
const value = JSON.stringify({
|
|
949
1161
|
AWS_ACCESS_KEY_ID: creds.AccessKeyId,
|
|
950
1162
|
AWS_SECRET_ACCESS_KEY: creds.SecretAccessKey,
|
|
@@ -963,18 +1175,23 @@ var httpProvider = {
|
|
|
963
1175
|
name: "http",
|
|
964
1176
|
description: "Generic HTTP token endpoint using Node.js http/https",
|
|
965
1177
|
provision(configRaw) {
|
|
966
|
-
let
|
|
1178
|
+
let raw;
|
|
967
1179
|
try {
|
|
968
|
-
|
|
1180
|
+
raw = JSON.parse(configRaw);
|
|
969
1181
|
} catch {
|
|
970
1182
|
throw new Error("http provider requires valid JSON config");
|
|
971
1183
|
}
|
|
1184
|
+
const parsed = HttpJitConfigSchema.safeParse(raw);
|
|
1185
|
+
if (!parsed.success) {
|
|
1186
|
+
throw new Error(`http provider invalid config: ${parsed.error.message}`);
|
|
1187
|
+
}
|
|
1188
|
+
const config = parsed.data;
|
|
972
1189
|
const url = config.url;
|
|
973
1190
|
const method = config.method || "POST";
|
|
974
1191
|
const valuePath = config.valuePath || "token";
|
|
975
1192
|
const expiresInSeconds = config.expiresInSeconds || 3600;
|
|
976
1193
|
if (!url) throw new Error("http provider requires url in config");
|
|
977
|
-
const ssrfBlock =
|
|
1194
|
+
const ssrfBlock = checkJitHttpProvisionUrl(url);
|
|
978
1195
|
if (ssrfBlock) throw new Error(`SSRF blocked: ${ssrfBlock}`);
|
|
979
1196
|
const headers = {
|
|
980
1197
|
"User-Agent": "q-ring-jit/1.0",
|
|
@@ -1008,8 +1225,8 @@ req.end();
|
|
|
1008
1225
|
if (result.status !== 0) {
|
|
1009
1226
|
throw new Error(result.stderr || "HTTP request failed");
|
|
1010
1227
|
}
|
|
1011
|
-
const
|
|
1012
|
-
let val =
|
|
1228
|
+
const parsed2 = JSON.parse(result.stdout);
|
|
1229
|
+
let val = parsed2;
|
|
1013
1230
|
for (const key of valuePath.split(".")) {
|
|
1014
1231
|
val = val[key];
|
|
1015
1232
|
}
|
|
@@ -1057,6 +1274,39 @@ function checkToolPolicy(toolName, projectPath) {
|
|
|
1057
1274
|
}
|
|
1058
1275
|
return { allowed: true, policySource: ".q-ring.json" };
|
|
1059
1276
|
}
|
|
1277
|
+
function checkSecretLifecyclePolicy(input, projectPath) {
|
|
1278
|
+
const policy = loadPolicy(projectPath);
|
|
1279
|
+
if (!policy.secrets) return { allowed: true, policySource: "no-policy" };
|
|
1280
|
+
const s = policy.secrets;
|
|
1281
|
+
if (s.maxTtlSeconds != null && input.ttlSeconds != null && input.ttlSeconds > s.maxTtlSeconds) {
|
|
1282
|
+
return {
|
|
1283
|
+
allowed: false,
|
|
1284
|
+
reason: `TTL ${input.ttlSeconds}s exceeds policy maximum ${s.maxTtlSeconds}s`,
|
|
1285
|
+
policySource: ".q-ring.json policy.secrets.maxTtlSeconds"
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
if (s.requireApprovalForTags?.length && input.tags?.length) {
|
|
1289
|
+
const hit = input.tags.find((t) => s.requireApprovalForTags.includes(t));
|
|
1290
|
+
if (hit && !input.requiresApproval) {
|
|
1291
|
+
return {
|
|
1292
|
+
allowed: false,
|
|
1293
|
+
reason: `Tag "${hit}" requires explicit approval metadata (set requiresApproval / --requires-approval)`,
|
|
1294
|
+
policySource: ".q-ring.json policy.secrets.requireApprovalForTags"
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (s.requireRotationFormatForTags?.length && input.tags?.length) {
|
|
1299
|
+
const hit = input.tags.find((t) => s.requireRotationFormatForTags.includes(t));
|
|
1300
|
+
if (hit && !input.rotationFormat) {
|
|
1301
|
+
return {
|
|
1302
|
+
allowed: false,
|
|
1303
|
+
reason: `Tag "${hit}" requires a rotationFormat to be set`,
|
|
1304
|
+
policySource: ".q-ring.json policy.secrets.requireRotationFormatForTags"
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
return { allowed: true, policySource: ".q-ring.json" };
|
|
1309
|
+
}
|
|
1060
1310
|
function checkKeyReadPolicy(key, tags, projectPath) {
|
|
1061
1311
|
const policy = loadPolicy(projectPath);
|
|
1062
1312
|
if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
|
|
@@ -1125,6 +1375,32 @@ function getPolicySummary(projectPath) {
|
|
|
1125
1375
|
}
|
|
1126
1376
|
|
|
1127
1377
|
// src/core/keyring.ts
|
|
1378
|
+
function withJitEnvelopeLock(service, key, fn) {
|
|
1379
|
+
const dir = join6(homedir5(), ".config", "q-ring", "jit-locks");
|
|
1380
|
+
mkdirSync5(dir, { recursive: true });
|
|
1381
|
+
const safe = Buffer.from(`${service}\0${key}`, "utf8").toString("base64url");
|
|
1382
|
+
const lockPath = join6(dir, `${safe}.lock`);
|
|
1383
|
+
const deadline = Date.now() + 8e3;
|
|
1384
|
+
while (Date.now() < deadline) {
|
|
1385
|
+
try {
|
|
1386
|
+
writeFileSync4(lockPath, `${process.pid}
|
|
1387
|
+
`, { flag: "wx", mode: 384 });
|
|
1388
|
+
try {
|
|
1389
|
+
return fn();
|
|
1390
|
+
} finally {
|
|
1391
|
+
try {
|
|
1392
|
+
unlinkSync(lockPath);
|
|
1393
|
+
} catch {
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
} catch {
|
|
1397
|
+
const start = Date.now();
|
|
1398
|
+
while (Date.now() - start < 15) {
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
throw new Error("Could not acquire JIT envelope lock (timeout)");
|
|
1403
|
+
}
|
|
1128
1404
|
function readEnvelope(service, key) {
|
|
1129
1405
|
const entry = new Entry(service, key);
|
|
1130
1406
|
const raw = entry.getPassword();
|
|
@@ -1143,7 +1419,7 @@ function resolveEnv(opts) {
|
|
|
1143
1419
|
}
|
|
1144
1420
|
function resolveTemplates(value, opts, seen) {
|
|
1145
1421
|
if (!value.includes("{{") || !value.includes("}}")) return value;
|
|
1146
|
-
return value.replace(/\{\{([^}]+)\}\}/g, (
|
|
1422
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (_match, refKeyRaw) => {
|
|
1147
1423
|
const refKey = refKeyRaw.trim();
|
|
1148
1424
|
const refValue = getSecret(refKey, { ...opts, _seen: seen });
|
|
1149
1425
|
if (refValue === null) {
|
|
@@ -1154,7 +1430,7 @@ function resolveTemplates(value, opts, seen) {
|
|
|
1154
1430
|
}
|
|
1155
1431
|
function resolveTemplatesOffline(value, rawValues, seen) {
|
|
1156
1432
|
if (!value.includes("{{") || !value.includes("}}")) return value;
|
|
1157
|
-
return value.replace(/\{\{([^}]+)\}\}/g, (
|
|
1433
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (_match, refKeyRaw) => {
|
|
1158
1434
|
const refKey = refKeyRaw.trim();
|
|
1159
1435
|
if (seen.has(refKey)) {
|
|
1160
1436
|
throw new Error(`Circular dependency detected: ${[...seen].join(" -> ")} -> ${refKey}`);
|
|
@@ -1224,11 +1500,27 @@ function getSecret(key, opts = {}) {
|
|
|
1224
1500
|
isExpired = new Date(envelope.meta.jitExpiresAt).getTime() <= Date.now();
|
|
1225
1501
|
}
|
|
1226
1502
|
if (isExpired) {
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1503
|
+
const jitConfigSource = value;
|
|
1504
|
+
withJitEnvelopeLock(service, key, () => {
|
|
1505
|
+
const latest = readEnvelope(service, key);
|
|
1506
|
+
if (!latest?.meta.jitProvider) return;
|
|
1507
|
+
let innerExpired = true;
|
|
1508
|
+
if (latest.states && latest.states["jit"] && latest.meta.jitExpiresAt) {
|
|
1509
|
+
innerExpired = new Date(latest.meta.jitExpiresAt).getTime() <= Date.now();
|
|
1510
|
+
}
|
|
1511
|
+
if (!innerExpired) {
|
|
1512
|
+
envelope.states = latest.states;
|
|
1513
|
+
envelope.meta.jitExpiresAt = latest.meta.jitExpiresAt;
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
const result = provider.provision(jitConfigSource);
|
|
1517
|
+
latest.states = latest.states ?? {};
|
|
1518
|
+
latest.states["jit"] = result.value;
|
|
1519
|
+
latest.meta.jitExpiresAt = result.expiresAt;
|
|
1520
|
+
writeEnvelope(service, key, latest);
|
|
1521
|
+
envelope.states = latest.states;
|
|
1522
|
+
envelope.meta.jitExpiresAt = latest.meta.jitExpiresAt;
|
|
1523
|
+
});
|
|
1232
1524
|
}
|
|
1233
1525
|
value = envelope.states["jit"];
|
|
1234
1526
|
}
|
|
@@ -1270,6 +1562,20 @@ function setSecret(key, value, opts = {}) {
|
|
|
1270
1562
|
const prov = opts.provider ?? existing?.meta.provider;
|
|
1271
1563
|
const reqApp = opts.requiresApproval ?? existing?.meta.requiresApproval;
|
|
1272
1564
|
const jitProv = opts.jitProvider ?? existing?.meta.jitProvider;
|
|
1565
|
+
const mergedTags = opts.tags ?? existing?.meta.tags;
|
|
1566
|
+
const ttlForPolicy = opts.ttlSeconds ?? existing?.meta.ttlSeconds;
|
|
1567
|
+
const life = checkSecretLifecyclePolicy(
|
|
1568
|
+
{
|
|
1569
|
+
tags: mergedTags,
|
|
1570
|
+
ttlSeconds: ttlForPolicy,
|
|
1571
|
+
rotationFormat: rotFmt,
|
|
1572
|
+
requiresApproval: !!reqApp
|
|
1573
|
+
},
|
|
1574
|
+
opts.projectPath
|
|
1575
|
+
);
|
|
1576
|
+
if (!life.allowed) {
|
|
1577
|
+
throw new Error(life.reason ?? "Secret lifecycle policy denied");
|
|
1578
|
+
}
|
|
1273
1579
|
if (opts.states) {
|
|
1274
1580
|
envelope = createEnvelope("", {
|
|
1275
1581
|
states: opts.states,
|
|
@@ -1596,7 +1902,7 @@ export {
|
|
|
1596
1902
|
exportAudit,
|
|
1597
1903
|
detectAnomalies,
|
|
1598
1904
|
listEntanglements,
|
|
1599
|
-
|
|
1905
|
+
httpRequest,
|
|
1600
1906
|
checkSSRF,
|
|
1601
1907
|
registerHook,
|
|
1602
1908
|
removeHook,
|
|
@@ -1622,4 +1928,4 @@ export {
|
|
|
1622
1928
|
tunnelDestroy,
|
|
1623
1929
|
tunnelList
|
|
1624
1930
|
};
|
|
1625
|
-
//# sourceMappingURL=chunk-
|
|
1931
|
+
//# sourceMappingURL=chunk-E46HG5NM.js.map
|