@i4ctime/q-ring 0.15.0 → 0.16.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 +36 -1
- package/dist/{chunk-MLBJCPX2.js → chunk-5LFKBZ3Q.js} +62 -7
- package/dist/chunk-5LFKBZ3Q.js.map +1 -0
- package/dist/{chunk-TPPPTL4J.js → chunk-NCM5GHNW.js} +62 -7
- package/dist/chunk-NCM5GHNW.js.map +1 -0
- package/dist/{dashboard-T2UG23KI.js → dashboard-A3GJQCJX.js} +2 -2
- package/dist/{dashboard-TFWCG23R.js → dashboard-R3FWTFFW.js} +2 -2
- package/dist/index.js +285 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +10 -3
- package/dist/mcp.js.map +1 -1
- package/package.json +2 -1
- package/dist/chunk-MLBJCPX2.js.map +0 -1
- package/dist/chunk-TPPPTL4J.js.map +0 -1
- /package/dist/{dashboard-T2UG23KI.js.map → dashboard-A3GJQCJX.js.map} +0 -0
- /package/dist/{dashboard-TFWCG23R.js.map → dashboard-R3FWTFFW.js.map} +0 -0
package/README.md
CHANGED
|
@@ -500,6 +500,41 @@ qring approve PROD_DB_URL --revoke
|
|
|
500
500
|
|
|
501
501
|
When an agent is blocked on an approval-protected key, q-ring raises a desktop notification (Linux `notify-send`, macOS `osascript`) naming the key and the exact `qring approve` command — throttled per key, disabled with `QRING_NOTIFY=off`.
|
|
502
502
|
|
|
503
|
+
### Canary Honeytokens
|
|
504
|
+
|
|
505
|
+
Plant fake credentials that look and read exactly like real ones. Anything that touches one — a compromised MCP server, an over-curious agent, exfiltrated tooling sweeping the ring — gets the fake value back with no tell, while q-ring fires a desktop alert and writes a `canary` event into the tamper-evident audit chain.
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
# Plant a canary shaped like a real AWS access key
|
|
509
|
+
qring canary plant AWS_SECRET_ACCESS_KEY --format aws
|
|
510
|
+
|
|
511
|
+
# Other shapes: github, openai, anthropic, stripe, generic
|
|
512
|
+
qring canary plant GHP_BACKUP_TOKEN --format github
|
|
513
|
+
|
|
514
|
+
# See what's been tripped
|
|
515
|
+
qring canary list
|
|
516
|
+
qring audit --action canary
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Values are CSPRNG noise in the provider's real token shape (an `aws` canary matches `AKIA[A-Z0-9]{16}`) — plausible enough to be taken, never valid. Alerts are throttled to one per key per 30 seconds; the audit trail records every read.
|
|
520
|
+
|
|
521
|
+
### MCP Airlock
|
|
522
|
+
|
|
523
|
+
Run a third-party MCP server behind q-ring. The airlock sits between your agent host and the wrapped server, spawns it with a **stripped environment** (no inherited API keys — opt back in with `--inherit-env`), and records every tool call that crosses it as a `wrap` event in the audit chain, grouped per session and labeled with the calling client's identity. Tool arguments are never logged — they may contain secrets.
|
|
524
|
+
|
|
525
|
+
```json
|
|
526
|
+
{
|
|
527
|
+
"mcpServers": {
|
|
528
|
+
"some-server": {
|
|
529
|
+
"command": "qring",
|
|
530
|
+
"args": ["mcp", "wrap", "--", "npx", "-y", "some-mcp-server"]
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Tools-only proxy today: `tools/list` and `tools/call` pass through verbatim, so the wrapped server behaves identically — it just can't read your environment, and everything it's asked to do is on the record.
|
|
537
|
+
|
|
503
538
|
### Just-In-Time (JIT) Provisioning
|
|
504
539
|
|
|
505
540
|
Instead of storing static credentials, configure `q-ring` to dynamically generate short-lived tokens on the fly when requested (e.g. AWS STS, generic HTTP endpoints).
|
|
@@ -648,7 +683,7 @@ qring exec -- echo "hello"
|
|
|
648
683
|
|
|
649
684
|
### Tamper-Evident Audit
|
|
650
685
|
|
|
651
|
-
Every audit event includes a SHA-256 hash of the previous event, creating a tamper-evident chain. Since v0.14 the chain is also anchored with a keyed HMAC stored in the OS keyring, so `qring audit:verify` detects truncation and whole-file rewrites — not just in-place edits. Verify integrity and export logs in multiple formats.
|
|
686
|
+
Every audit event includes a SHA-256 hash of the previous event, creating a tamper-evident chain. Since v0.14 the chain is also anchored with a keyed HMAC stored in the OS keyring, so `qring audit:verify` detects truncation and whole-file rewrites — not just in-place edits. Verify integrity and export logs in multiple formats. Events from MCP sessions are additionally stamped with the connecting client's self-reported identity (`clientInfo` name@version) — an audit label for "which agent did this", never an authorization boundary, since clients choose what to report.
|
|
652
687
|
|
|
653
688
|
```bash
|
|
654
689
|
# Verify the entire audit chain
|
|
@@ -40,7 +40,9 @@ var SecretMetadataSchema = z.object({
|
|
|
40
40
|
validationUrl: z.string().optional(),
|
|
41
41
|
requiresApproval: z.boolean().optional(),
|
|
42
42
|
jitProvider: z.string().optional(),
|
|
43
|
-
jitExpiresAt: z.string().optional()
|
|
43
|
+
jitExpiresAt: z.string().optional(),
|
|
44
|
+
canary: z.boolean().optional(),
|
|
45
|
+
canaryFormat: z.string().optional()
|
|
44
46
|
});
|
|
45
47
|
var QuantumEnvelopeSchema = z.object({
|
|
46
48
|
v: z.literal(1),
|
|
@@ -73,7 +75,9 @@ function createEnvelope(value, opts) {
|
|
|
73
75
|
rotationPrefix: opts?.rotationPrefix,
|
|
74
76
|
provider: opts?.provider,
|
|
75
77
|
requiresApproval: opts?.requiresApproval,
|
|
76
|
-
jitProvider: opts?.jitProvider
|
|
78
|
+
jitProvider: opts?.jitProvider,
|
|
79
|
+
canary: opts?.canary,
|
|
80
|
+
canaryFormat: opts?.canaryFormat
|
|
77
81
|
}
|
|
78
82
|
};
|
|
79
83
|
}
|
|
@@ -561,6 +565,18 @@ function writeStoredAnchor(hash) {
|
|
|
561
565
|
} catch {
|
|
562
566
|
}
|
|
563
567
|
}
|
|
568
|
+
var auditAgentLabel = null;
|
|
569
|
+
function setAuditAgentLabel(label) {
|
|
570
|
+
if (label === null) {
|
|
571
|
+
auditAgentLabel = null;
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const cleaned = label.replace(/[^\x20-\x7e]/g, "").trim().slice(0, 128);
|
|
575
|
+
auditAgentLabel = cleaned.length > 0 ? cleaned : null;
|
|
576
|
+
}
|
|
577
|
+
function getAuditAgentLabel() {
|
|
578
|
+
return auditAgentLabel;
|
|
579
|
+
}
|
|
564
580
|
function getAuditDir() {
|
|
565
581
|
if (process.env.QRING_AUDIT_DIR) {
|
|
566
582
|
if (!existsSync2(process.env.QRING_AUDIT_DIR)) {
|
|
@@ -612,6 +628,9 @@ function logAudit(event) {
|
|
|
612
628
|
pid: process.pid,
|
|
613
629
|
prevHash
|
|
614
630
|
};
|
|
631
|
+
if (full.agent === void 0 && auditAgentLabel) {
|
|
632
|
+
full.agent = auditAgentLabel;
|
|
633
|
+
}
|
|
615
634
|
const line = JSON.stringify(full);
|
|
616
635
|
const path = getAuditPath();
|
|
617
636
|
appendFileSync(path, line + "\n", { mode: 384 });
|
|
@@ -656,6 +675,7 @@ function queryAudit(query = {}) {
|
|
|
656
675
|
if (query.action) events = events.filter((e) => e.action === query.action);
|
|
657
676
|
if (query.source) events = events.filter((e) => e.source === query.source);
|
|
658
677
|
if (query.correlationId) events = events.filter((e) => e.correlationId === query.correlationId);
|
|
678
|
+
if (query.agent) events = events.filter((e) => e.agent === query.agent);
|
|
659
679
|
if (query.since) {
|
|
660
680
|
const since = new Date(query.since).getTime();
|
|
661
681
|
events = events.filter((e) => new Date(e.timestamp).getTime() >= since);
|
|
@@ -746,9 +766,9 @@ function exportAudit(opts = {}) {
|
|
|
746
766
|
return JSON.stringify(events, null, 2);
|
|
747
767
|
}
|
|
748
768
|
if (opts.format === "csv") {
|
|
749
|
-
const header = "timestamp,action,key,scope,env,source,pid,correlationId,detail";
|
|
769
|
+
const header = "timestamp,action,key,scope,env,source,agent,pid,correlationId,detail";
|
|
750
770
|
const rows = events.map(
|
|
751
|
-
(e) => `${e.timestamp},${e.action},${e.key ?? ""},${e.scope ?? ""},${e.env ?? ""},${e.source},${e.pid},${e.correlationId ?? ""},${(e.detail ?? "").replace(/,/g, ";")}`
|
|
771
|
+
(e) => `${e.timestamp},${e.action},${e.key ?? ""},${e.scope ?? ""},${e.env ?? ""},${e.source},${(e.agent ?? "").replace(/,/g, ";")},${e.pid},${e.correlationId ?? ""},${(e.detail ?? "").replace(/,/g, ";")}`
|
|
752
772
|
);
|
|
753
773
|
return [header, ...rows].join("\n");
|
|
754
774
|
}
|
|
@@ -1771,6 +1791,31 @@ function notifyApprovalRequested(key, source) {
|
|
|
1771
1791
|
);
|
|
1772
1792
|
}
|
|
1773
1793
|
|
|
1794
|
+
// src/core/canary-alert.ts
|
|
1795
|
+
var TRIP_THROTTLE_MS = 30 * 1e3;
|
|
1796
|
+
var lastAlerted = /* @__PURE__ */ new Map();
|
|
1797
|
+
function recordCanaryTrip(trip) {
|
|
1798
|
+
const agent = getAuditAgentLabel();
|
|
1799
|
+
logAudit({
|
|
1800
|
+
action: "canary",
|
|
1801
|
+
key: trip.key,
|
|
1802
|
+
scope: trip.scope,
|
|
1803
|
+
env: trip.env,
|
|
1804
|
+
source: trip.source,
|
|
1805
|
+
detail: `CANARY TRIPPED: honeytoken read via ${trip.source}`
|
|
1806
|
+
});
|
|
1807
|
+
if (!notificationsEnabled()) return;
|
|
1808
|
+
const now = Date.now();
|
|
1809
|
+
const last = lastAlerted.get(trip.key);
|
|
1810
|
+
if (last !== void 0 && now - last < TRIP_THROTTLE_MS) return;
|
|
1811
|
+
lastAlerted.set(trip.key, now);
|
|
1812
|
+
const who = agent ? `${trip.source} (${agent})` : trip.source;
|
|
1813
|
+
notifyUser(
|
|
1814
|
+
"q-ring: CANARY TRIPPED",
|
|
1815
|
+
`Honeytoken "${trip.key}" was read by ${who}. This credential is fake \u2014 but something reached for it. Investigate: qring audit --action canary`
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1774
1819
|
// src/core/provision.ts
|
|
1775
1820
|
import { execFileSync, spawnSync } from "child_process";
|
|
1776
1821
|
import { z as z3 } from "zod";
|
|
@@ -2052,6 +2097,9 @@ function getSecret(key, opts = {}) {
|
|
|
2052
2097
|
const latest = readEnvelope(service, key) ?? envelope;
|
|
2053
2098
|
writeEnvelope(service, key, recordAccess(latest));
|
|
2054
2099
|
logAudit({ action: "read", key, scope, env, source });
|
|
2100
|
+
if (envelope.meta.canary) {
|
|
2101
|
+
recordCanaryTrip({ key, scope, env, source });
|
|
2102
|
+
}
|
|
2055
2103
|
}
|
|
2056
2104
|
return value;
|
|
2057
2105
|
}
|
|
@@ -2084,6 +2132,8 @@ function setSecret(key, value, opts = {}) {
|
|
|
2084
2132
|
const prov = opts.provider ?? existing?.meta.provider;
|
|
2085
2133
|
const reqApp = opts.requiresApproval ?? existing?.meta.requiresApproval;
|
|
2086
2134
|
const jitProv = opts.jitProvider ?? existing?.meta.jitProvider;
|
|
2135
|
+
const canaryFlag = opts.canary ?? existing?.meta.canary;
|
|
2136
|
+
const canaryFmt = opts.canaryFormat ?? existing?.meta.canaryFormat;
|
|
2087
2137
|
const mergedTags = opts.tags ?? existing?.meta.tags;
|
|
2088
2138
|
const ttlForPolicy = opts.ttlSeconds ?? existing?.meta.ttlSeconds;
|
|
2089
2139
|
const life = checkSecretLifecyclePolicy(
|
|
@@ -2111,7 +2161,9 @@ function setSecret(key, value, opts = {}) {
|
|
|
2111
2161
|
rotationPrefix: rotPfx,
|
|
2112
2162
|
provider: prov,
|
|
2113
2163
|
requiresApproval: reqApp,
|
|
2114
|
-
jitProvider: jitProv
|
|
2164
|
+
jitProvider: jitProv,
|
|
2165
|
+
canary: canaryFlag,
|
|
2166
|
+
canaryFormat: canaryFmt
|
|
2115
2167
|
});
|
|
2116
2168
|
} else {
|
|
2117
2169
|
envelope = createEnvelope(value, {
|
|
@@ -2124,7 +2176,9 @@ function setSecret(key, value, opts = {}) {
|
|
|
2124
2176
|
rotationPrefix: rotPfx,
|
|
2125
2177
|
provider: prov,
|
|
2126
2178
|
requiresApproval: reqApp,
|
|
2127
|
-
jitProvider: jitProv
|
|
2179
|
+
jitProvider: jitProv,
|
|
2180
|
+
canary: canaryFlag,
|
|
2181
|
+
canaryFormat: canaryFmt
|
|
2128
2182
|
});
|
|
2129
2183
|
}
|
|
2130
2184
|
if (existing) {
|
|
@@ -2620,6 +2674,7 @@ export {
|
|
|
2620
2674
|
checkDecay,
|
|
2621
2675
|
readProjectConfig,
|
|
2622
2676
|
collapseEnvironment,
|
|
2677
|
+
setAuditAgentLabel,
|
|
2623
2678
|
logAudit,
|
|
2624
2679
|
queryAudit,
|
|
2625
2680
|
verifyAuditChain,
|
|
@@ -2661,4 +2716,4 @@ export {
|
|
|
2661
2716
|
forget,
|
|
2662
2717
|
clearMemory
|
|
2663
2718
|
};
|
|
2664
|
-
//# sourceMappingURL=chunk-
|
|
2719
|
+
//# sourceMappingURL=chunk-5LFKBZ3Q.js.map
|