@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();
|
|
@@ -721,6 +838,14 @@ function matchesHook(hook, payload, tags) {
|
|
|
721
838
|
if (m.scope && m.scope !== payload.scope) return false;
|
|
722
839
|
return true;
|
|
723
840
|
}
|
|
841
|
+
var SAFE_PGREP_NAME = /^[a-zA-Z0-9._:/-]{1,256}$/;
|
|
842
|
+
function shellRunner() {
|
|
843
|
+
if (process.platform === "win32") {
|
|
844
|
+
const comspec = process.env.ComSpec ?? "cmd.exe";
|
|
845
|
+
return { file: comspec, args: ["/d", "/s", "/c"] };
|
|
846
|
+
}
|
|
847
|
+
return { file: "/bin/sh", args: ["-c"] };
|
|
848
|
+
}
|
|
724
849
|
function executeShell(command, payload) {
|
|
725
850
|
return new Promise((resolve) => {
|
|
726
851
|
const env = {
|
|
@@ -729,13 +854,33 @@ function executeShell(command, payload) {
|
|
|
729
854
|
QRING_HOOK_ACTION: payload.action,
|
|
730
855
|
QRING_HOOK_SCOPE: payload.scope
|
|
731
856
|
};
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
857
|
+
const { file, args } = shellRunner();
|
|
858
|
+
execFile(
|
|
859
|
+
file,
|
|
860
|
+
[...args, command],
|
|
861
|
+
{
|
|
862
|
+
timeout: 3e4,
|
|
863
|
+
env,
|
|
864
|
+
windowsHide: true,
|
|
865
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
866
|
+
encoding: "utf8"
|
|
867
|
+
},
|
|
868
|
+
(err, stdout) => {
|
|
869
|
+
if (err) {
|
|
870
|
+
resolve({
|
|
871
|
+
hookId: "",
|
|
872
|
+
success: false,
|
|
873
|
+
message: `Shell error: ${err.message}`
|
|
874
|
+
});
|
|
875
|
+
} else {
|
|
876
|
+
resolve({
|
|
877
|
+
hookId: "",
|
|
878
|
+
success: true,
|
|
879
|
+
message: (stdout ?? "").trim() || "OK"
|
|
880
|
+
});
|
|
881
|
+
}
|
|
737
882
|
}
|
|
738
|
-
|
|
883
|
+
);
|
|
739
884
|
});
|
|
740
885
|
}
|
|
741
886
|
async function executeHttp(url, payload) {
|
|
@@ -752,7 +897,7 @@ async function executeHttp(url, payload) {
|
|
|
752
897
|
}
|
|
753
898
|
try {
|
|
754
899
|
const body = JSON.stringify(payload);
|
|
755
|
-
const res = await
|
|
900
|
+
const res = await httpRequest({
|
|
756
901
|
url,
|
|
757
902
|
method: "POST",
|
|
758
903
|
headers: {
|
|
@@ -777,24 +922,48 @@ async function executeHttp(url, payload) {
|
|
|
777
922
|
}
|
|
778
923
|
function executeSignal(target, signal = "SIGHUP") {
|
|
779
924
|
return new Promise((resolve) => {
|
|
780
|
-
const
|
|
781
|
-
if (
|
|
925
|
+
const trimmed = target.trim();
|
|
926
|
+
if (/^\d+$/.test(trimmed)) {
|
|
927
|
+
const pid = parseInt(trimmed, 10);
|
|
782
928
|
try {
|
|
783
929
|
process.kill(pid, signal);
|
|
784
|
-
resolve({
|
|
930
|
+
resolve({
|
|
931
|
+
hookId: "",
|
|
932
|
+
success: true,
|
|
933
|
+
message: `Signal ${signal} sent to PID ${pid}`
|
|
934
|
+
});
|
|
785
935
|
} catch (err) {
|
|
786
|
-
resolve({
|
|
936
|
+
resolve({
|
|
937
|
+
hookId: "",
|
|
938
|
+
success: false,
|
|
939
|
+
message: `Signal error: ${err instanceof Error ? err.message : String(err)}`
|
|
940
|
+
});
|
|
787
941
|
}
|
|
788
942
|
return;
|
|
789
943
|
}
|
|
790
|
-
|
|
944
|
+
if (!SAFE_PGREP_NAME.test(trimmed)) {
|
|
945
|
+
resolve({
|
|
946
|
+
hookId: "",
|
|
947
|
+
success: false,
|
|
948
|
+
message: "Signal target must be a numeric PID, or a process name matching /^[a-zA-Z0-9._:/-]{1,256}$/ (no spaces or shell metacharacters)."
|
|
949
|
+
});
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const child = spawn("pgrep", ["-f", trimmed], {
|
|
953
|
+
timeout: 5e3,
|
|
954
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
955
|
+
});
|
|
791
956
|
let stdout = "";
|
|
792
957
|
child.stdout.on("data", (d) => {
|
|
793
958
|
stdout += d.toString();
|
|
794
959
|
});
|
|
795
960
|
child.on("close", (code) => {
|
|
796
961
|
if (code !== 0 || !stdout.trim()) {
|
|
797
|
-
resolve({
|
|
962
|
+
resolve({
|
|
963
|
+
hookId: "",
|
|
964
|
+
success: false,
|
|
965
|
+
message: `Process "${trimmed}" not found`
|
|
966
|
+
});
|
|
798
967
|
return;
|
|
799
968
|
}
|
|
800
969
|
const pids = stdout.trim().split("\n").map((p) => parseInt(p.trim(), 10)).filter((p) => !isNaN(p));
|
|
@@ -806,10 +975,18 @@ function executeSignal(target, signal = "SIGHUP") {
|
|
|
806
975
|
} catch {
|
|
807
976
|
}
|
|
808
977
|
}
|
|
809
|
-
resolve({
|
|
978
|
+
resolve({
|
|
979
|
+
hookId: "",
|
|
980
|
+
success: sent > 0,
|
|
981
|
+
message: `Signal ${signal} sent to ${sent} process(es)`
|
|
982
|
+
});
|
|
810
983
|
});
|
|
811
984
|
child.on("error", () => {
|
|
812
|
-
resolve({
|
|
985
|
+
resolve({
|
|
986
|
+
hookId: "",
|
|
987
|
+
success: false,
|
|
988
|
+
message: `Process "${trimmed}" not found`
|
|
989
|
+
});
|
|
813
990
|
});
|
|
814
991
|
});
|
|
815
992
|
}
|
|
@@ -866,33 +1043,50 @@ async function fireHooks(payload, tags) {
|
|
|
866
1043
|
}
|
|
867
1044
|
|
|
868
1045
|
// src/core/approval.ts
|
|
869
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as
|
|
1046
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
870
1047
|
import { join as join5 } from "path";
|
|
871
1048
|
import { homedir as homedir4 } from "os";
|
|
872
|
-
import { createHmac, randomBytes } from "crypto";
|
|
1049
|
+
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
873
1050
|
function getHmacSecret() {
|
|
874
|
-
const secretPath = join5(homedir4(), ".config", "q-ring", ".approval-key");
|
|
875
1051
|
const dir = join5(homedir4(), ".config", "q-ring");
|
|
876
|
-
|
|
1052
|
+
const secretPath = join5(dir, ".approval-key");
|
|
1053
|
+
if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
877
1054
|
if (existsSync5(secretPath)) {
|
|
878
1055
|
return readFileSync5(secretPath, "utf8").trim();
|
|
879
1056
|
}
|
|
880
1057
|
const secret = randomBytes(32).toString("hex");
|
|
881
|
-
|
|
1058
|
+
writeFileSync3(secretPath, secret, { mode: 384 });
|
|
882
1059
|
return secret;
|
|
883
1060
|
}
|
|
884
1061
|
function computeHmac(entry) {
|
|
885
|
-
const payload =
|
|
1062
|
+
const payload = [
|
|
1063
|
+
entry.id,
|
|
1064
|
+
entry.key,
|
|
1065
|
+
entry.scope,
|
|
1066
|
+
entry.reason,
|
|
1067
|
+
entry.grantedBy,
|
|
1068
|
+
entry.grantedAt,
|
|
1069
|
+
entry.expiresAt,
|
|
1070
|
+
entry.workspace ?? "",
|
|
1071
|
+
entry.sessionId ?? ""
|
|
1072
|
+
].join("|");
|
|
886
1073
|
return createHmac("sha256", getHmacSecret()).update(payload).digest("hex");
|
|
887
1074
|
}
|
|
888
1075
|
function verifyHmac(entry) {
|
|
889
1076
|
const expected = computeHmac(entry);
|
|
890
|
-
|
|
1077
|
+
const a = Buffer.from(expected, "utf8");
|
|
1078
|
+
const b = Buffer.from(entry.hmac, "utf8");
|
|
1079
|
+
if (a.length !== b.length) return false;
|
|
1080
|
+
try {
|
|
1081
|
+
return timingSafeEqual(a, b);
|
|
1082
|
+
} catch {
|
|
1083
|
+
return false;
|
|
1084
|
+
}
|
|
891
1085
|
}
|
|
892
1086
|
function getRegistryPath3() {
|
|
893
1087
|
const dir = join5(homedir4(), ".config", "q-ring");
|
|
894
1088
|
if (!existsSync5(dir)) {
|
|
895
|
-
mkdirSync4(dir, { recursive: true });
|
|
1089
|
+
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
896
1090
|
}
|
|
897
1091
|
return join5(dir, "approvals.json");
|
|
898
1092
|
}
|
|
@@ -908,7 +1102,7 @@ function loadRegistry3() {
|
|
|
908
1102
|
}
|
|
909
1103
|
}
|
|
910
1104
|
function saveRegistry3(registry2) {
|
|
911
|
-
|
|
1105
|
+
writeFileSync3(getRegistryPath3(), JSON.stringify(registry2, null, 2), { mode: 384 });
|
|
912
1106
|
}
|
|
913
1107
|
function cleanup(registry2) {
|
|
914
1108
|
const now = Date.now();
|
|
@@ -976,6 +1170,20 @@ function listApprovals() {
|
|
|
976
1170
|
|
|
977
1171
|
// src/core/provision.ts
|
|
978
1172
|
import { execFileSync, spawnSync } from "child_process";
|
|
1173
|
+
import { z as z2 } from "zod";
|
|
1174
|
+
var AwsStsConfigSchema = z2.object({
|
|
1175
|
+
roleArn: z2.string(),
|
|
1176
|
+
sessionName: z2.string().optional(),
|
|
1177
|
+
durationSeconds: z2.number().optional()
|
|
1178
|
+
});
|
|
1179
|
+
var HttpJitConfigSchema = z2.object({
|
|
1180
|
+
url: z2.string(),
|
|
1181
|
+
method: z2.string().optional(),
|
|
1182
|
+
valuePath: z2.string().optional(),
|
|
1183
|
+
expiresInSeconds: z2.number().optional(),
|
|
1184
|
+
headers: z2.record(z2.string(), z2.string()).optional(),
|
|
1185
|
+
body: z2.unknown().optional()
|
|
1186
|
+
});
|
|
979
1187
|
var ProvisionRegistry = class {
|
|
980
1188
|
providers = /* @__PURE__ */ new Map();
|
|
981
1189
|
register(provider) {
|
|
@@ -992,16 +1200,20 @@ var awsStsProvider = {
|
|
|
992
1200
|
name: "aws-sts",
|
|
993
1201
|
description: "AWS STS AssumeRole (requires existing local AWS CLI credentials)",
|
|
994
1202
|
provision(configRaw) {
|
|
995
|
-
let
|
|
1203
|
+
let raw;
|
|
996
1204
|
try {
|
|
997
|
-
|
|
1205
|
+
raw = JSON.parse(configRaw);
|
|
998
1206
|
} catch {
|
|
999
1207
|
throw new Error('aws-sts requires valid JSON config (e.g. {"roleArn":"arn:aws:..."})');
|
|
1000
1208
|
}
|
|
1209
|
+
const parsed = AwsStsConfigSchema.safeParse(raw);
|
|
1210
|
+
if (!parsed.success) {
|
|
1211
|
+
throw new Error(`aws-sts invalid config: ${parsed.error.message}`);
|
|
1212
|
+
}
|
|
1213
|
+
const config = parsed.data;
|
|
1001
1214
|
const roleArn = config.roleArn;
|
|
1002
1215
|
const sessionName = config.sessionName || "q-ring-agent";
|
|
1003
1216
|
const duration = config.durationSeconds || 3600;
|
|
1004
|
-
if (!roleArn) throw new Error("aws-sts requires roleArn in config");
|
|
1005
1217
|
try {
|
|
1006
1218
|
const output = execFileSync("aws", [
|
|
1007
1219
|
"sts",
|
|
@@ -1015,8 +1227,8 @@ var awsStsProvider = {
|
|
|
1015
1227
|
"--output",
|
|
1016
1228
|
"json"
|
|
1017
1229
|
], { encoding: "utf8" });
|
|
1018
|
-
const
|
|
1019
|
-
const creds =
|
|
1230
|
+
const parsed2 = JSON.parse(output);
|
|
1231
|
+
const creds = parsed2.Credentials;
|
|
1020
1232
|
const value = JSON.stringify({
|
|
1021
1233
|
AWS_ACCESS_KEY_ID: creds.AccessKeyId,
|
|
1022
1234
|
AWS_SECRET_ACCESS_KEY: creds.SecretAccessKey,
|
|
@@ -1035,18 +1247,23 @@ var httpProvider = {
|
|
|
1035
1247
|
name: "http",
|
|
1036
1248
|
description: "Generic HTTP token endpoint using Node.js http/https",
|
|
1037
1249
|
provision(configRaw) {
|
|
1038
|
-
let
|
|
1250
|
+
let raw;
|
|
1039
1251
|
try {
|
|
1040
|
-
|
|
1252
|
+
raw = JSON.parse(configRaw);
|
|
1041
1253
|
} catch {
|
|
1042
1254
|
throw new Error("http provider requires valid JSON config");
|
|
1043
1255
|
}
|
|
1256
|
+
const parsed = HttpJitConfigSchema.safeParse(raw);
|
|
1257
|
+
if (!parsed.success) {
|
|
1258
|
+
throw new Error(`http provider invalid config: ${parsed.error.message}`);
|
|
1259
|
+
}
|
|
1260
|
+
const config = parsed.data;
|
|
1044
1261
|
const url = config.url;
|
|
1045
1262
|
const method = config.method || "POST";
|
|
1046
1263
|
const valuePath = config.valuePath || "token";
|
|
1047
1264
|
const expiresInSeconds = config.expiresInSeconds || 3600;
|
|
1048
1265
|
if (!url) throw new Error("http provider requires url in config");
|
|
1049
|
-
const ssrfBlock =
|
|
1266
|
+
const ssrfBlock = checkJitHttpProvisionUrl(url);
|
|
1050
1267
|
if (ssrfBlock) throw new Error(`SSRF blocked: ${ssrfBlock}`);
|
|
1051
1268
|
const headers = {
|
|
1052
1269
|
"User-Agent": "q-ring-jit/1.0",
|
|
@@ -1080,8 +1297,8 @@ req.end();
|
|
|
1080
1297
|
if (result.status !== 0) {
|
|
1081
1298
|
throw new Error(result.stderr || "HTTP request failed");
|
|
1082
1299
|
}
|
|
1083
|
-
const
|
|
1084
|
-
let val =
|
|
1300
|
+
const parsed2 = JSON.parse(result.stdout);
|
|
1301
|
+
let val = parsed2;
|
|
1085
1302
|
for (const key of valuePath.split(".")) {
|
|
1086
1303
|
val = val[key];
|
|
1087
1304
|
}
|
|
@@ -1110,6 +1327,39 @@ function loadPolicy(projectPath) {
|
|
|
1110
1327
|
cachedPolicy = { path: pp, policy };
|
|
1111
1328
|
return policy;
|
|
1112
1329
|
}
|
|
1330
|
+
function checkSecretLifecyclePolicy(input, projectPath) {
|
|
1331
|
+
const policy = loadPolicy(projectPath);
|
|
1332
|
+
if (!policy.secrets) return { allowed: true, policySource: "no-policy" };
|
|
1333
|
+
const s = policy.secrets;
|
|
1334
|
+
if (s.maxTtlSeconds != null && input.ttlSeconds != null && input.ttlSeconds > s.maxTtlSeconds) {
|
|
1335
|
+
return {
|
|
1336
|
+
allowed: false,
|
|
1337
|
+
reason: `TTL ${input.ttlSeconds}s exceeds policy maximum ${s.maxTtlSeconds}s`,
|
|
1338
|
+
policySource: ".q-ring.json policy.secrets.maxTtlSeconds"
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
if (s.requireApprovalForTags?.length && input.tags?.length) {
|
|
1342
|
+
const hit = input.tags.find((t) => s.requireApprovalForTags.includes(t));
|
|
1343
|
+
if (hit && !input.requiresApproval) {
|
|
1344
|
+
return {
|
|
1345
|
+
allowed: false,
|
|
1346
|
+
reason: `Tag "${hit}" requires explicit approval metadata (set requiresApproval / --requires-approval)`,
|
|
1347
|
+
policySource: ".q-ring.json policy.secrets.requireApprovalForTags"
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
if (s.requireRotationFormatForTags?.length && input.tags?.length) {
|
|
1352
|
+
const hit = input.tags.find((t) => s.requireRotationFormatForTags.includes(t));
|
|
1353
|
+
if (hit && !input.rotationFormat) {
|
|
1354
|
+
return {
|
|
1355
|
+
allowed: false,
|
|
1356
|
+
reason: `Tag "${hit}" requires a rotationFormat to be set`,
|
|
1357
|
+
policySource: ".q-ring.json policy.secrets.requireRotationFormatForTags"
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
return { allowed: true, policySource: ".q-ring.json" };
|
|
1362
|
+
}
|
|
1113
1363
|
function checkKeyReadPolicy(key, tags, projectPath) {
|
|
1114
1364
|
const policy = loadPolicy(projectPath);
|
|
1115
1365
|
if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
|
|
@@ -1178,6 +1428,32 @@ function getPolicySummary(projectPath) {
|
|
|
1178
1428
|
}
|
|
1179
1429
|
|
|
1180
1430
|
// src/core/keyring.ts
|
|
1431
|
+
function withJitEnvelopeLock(service, key, fn) {
|
|
1432
|
+
const dir = join6(homedir5(), ".config", "q-ring", "jit-locks");
|
|
1433
|
+
mkdirSync5(dir, { recursive: true });
|
|
1434
|
+
const safe = Buffer.from(`${service}\0${key}`, "utf8").toString("base64url");
|
|
1435
|
+
const lockPath = join6(dir, `${safe}.lock`);
|
|
1436
|
+
const deadline = Date.now() + 8e3;
|
|
1437
|
+
while (Date.now() < deadline) {
|
|
1438
|
+
try {
|
|
1439
|
+
writeFileSync4(lockPath, `${process.pid}
|
|
1440
|
+
`, { flag: "wx", mode: 384 });
|
|
1441
|
+
try {
|
|
1442
|
+
return fn();
|
|
1443
|
+
} finally {
|
|
1444
|
+
try {
|
|
1445
|
+
unlinkSync(lockPath);
|
|
1446
|
+
} catch {
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
} catch {
|
|
1450
|
+
const start = Date.now();
|
|
1451
|
+
while (Date.now() - start < 15) {
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
throw new Error("Could not acquire JIT envelope lock (timeout)");
|
|
1456
|
+
}
|
|
1181
1457
|
function readEnvelope(service, key) {
|
|
1182
1458
|
const entry = new Entry(service, key);
|
|
1183
1459
|
const raw = entry.getPassword();
|
|
@@ -1196,7 +1472,7 @@ function resolveEnv(opts) {
|
|
|
1196
1472
|
}
|
|
1197
1473
|
function resolveTemplates(value, opts, seen) {
|
|
1198
1474
|
if (!value.includes("{{") || !value.includes("}}")) return value;
|
|
1199
|
-
return value.replace(/\{\{([^}]+)\}\}/g, (
|
|
1475
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (_match, refKeyRaw) => {
|
|
1200
1476
|
const refKey = refKeyRaw.trim();
|
|
1201
1477
|
const refValue = getSecret(refKey, { ...opts, _seen: seen });
|
|
1202
1478
|
if (refValue === null) {
|
|
@@ -1207,7 +1483,7 @@ function resolveTemplates(value, opts, seen) {
|
|
|
1207
1483
|
}
|
|
1208
1484
|
function resolveTemplatesOffline(value, rawValues, seen) {
|
|
1209
1485
|
if (!value.includes("{{") || !value.includes("}}")) return value;
|
|
1210
|
-
return value.replace(/\{\{([^}]+)\}\}/g, (
|
|
1486
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (_match, refKeyRaw) => {
|
|
1211
1487
|
const refKey = refKeyRaw.trim();
|
|
1212
1488
|
if (seen.has(refKey)) {
|
|
1213
1489
|
throw new Error(`Circular dependency detected: ${[...seen].join(" -> ")} -> ${refKey}`);
|
|
@@ -1277,11 +1553,27 @@ function getSecret(key, opts = {}) {
|
|
|
1277
1553
|
isExpired = new Date(envelope.meta.jitExpiresAt).getTime() <= Date.now();
|
|
1278
1554
|
}
|
|
1279
1555
|
if (isExpired) {
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1556
|
+
const jitConfigSource = value;
|
|
1557
|
+
withJitEnvelopeLock(service, key, () => {
|
|
1558
|
+
const latest = readEnvelope(service, key);
|
|
1559
|
+
if (!latest?.meta.jitProvider) return;
|
|
1560
|
+
let innerExpired = true;
|
|
1561
|
+
if (latest.states && latest.states["jit"] && latest.meta.jitExpiresAt) {
|
|
1562
|
+
innerExpired = new Date(latest.meta.jitExpiresAt).getTime() <= Date.now();
|
|
1563
|
+
}
|
|
1564
|
+
if (!innerExpired) {
|
|
1565
|
+
envelope.states = latest.states;
|
|
1566
|
+
envelope.meta.jitExpiresAt = latest.meta.jitExpiresAt;
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
const result = provider.provision(jitConfigSource);
|
|
1570
|
+
latest.states = latest.states ?? {};
|
|
1571
|
+
latest.states["jit"] = result.value;
|
|
1572
|
+
latest.meta.jitExpiresAt = result.expiresAt;
|
|
1573
|
+
writeEnvelope(service, key, latest);
|
|
1574
|
+
envelope.states = latest.states;
|
|
1575
|
+
envelope.meta.jitExpiresAt = latest.meta.jitExpiresAt;
|
|
1576
|
+
});
|
|
1285
1577
|
}
|
|
1286
1578
|
value = envelope.states["jit"];
|
|
1287
1579
|
}
|
|
@@ -1323,6 +1615,20 @@ function setSecret(key, value, opts = {}) {
|
|
|
1323
1615
|
const prov = opts.provider ?? existing?.meta.provider;
|
|
1324
1616
|
const reqApp = opts.requiresApproval ?? existing?.meta.requiresApproval;
|
|
1325
1617
|
const jitProv = opts.jitProvider ?? existing?.meta.jitProvider;
|
|
1618
|
+
const mergedTags = opts.tags ?? existing?.meta.tags;
|
|
1619
|
+
const ttlForPolicy = opts.ttlSeconds ?? existing?.meta.ttlSeconds;
|
|
1620
|
+
const life = checkSecretLifecyclePolicy(
|
|
1621
|
+
{
|
|
1622
|
+
tags: mergedTags,
|
|
1623
|
+
ttlSeconds: ttlForPolicy,
|
|
1624
|
+
rotationFormat: rotFmt,
|
|
1625
|
+
requiresApproval: !!reqApp
|
|
1626
|
+
},
|
|
1627
|
+
opts.projectPath
|
|
1628
|
+
);
|
|
1629
|
+
if (!life.allowed) {
|
|
1630
|
+
throw new Error(life.reason ?? "Secret lifecycle policy denied");
|
|
1631
|
+
}
|
|
1326
1632
|
if (opts.states) {
|
|
1327
1633
|
envelope = createEnvelope("", {
|
|
1328
1634
|
states: opts.states,
|
|
@@ -1649,7 +1955,7 @@ export {
|
|
|
1649
1955
|
exportAudit,
|
|
1650
1956
|
detectAnomalies,
|
|
1651
1957
|
listEntanglements,
|
|
1652
|
-
|
|
1958
|
+
httpRequest,
|
|
1653
1959
|
checkSSRF,
|
|
1654
1960
|
registerHook,
|
|
1655
1961
|
removeHook,
|
|
@@ -1678,4 +1984,4 @@ export {
|
|
|
1678
1984
|
tunnelDestroy,
|
|
1679
1985
|
tunnelList
|
|
1680
1986
|
};
|
|
1681
|
-
//# sourceMappingURL=chunk-
|
|
1987
|
+
//# sourceMappingURL=chunk-LLMO64FE.js.map
|