@i4ctime/q-ring 0.17.0 → 0.17.6
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 +89 -7
- package/dist/{chunk-SQKTDYSO.js → chunk-5ZCQSBVS.js} +352 -18
- package/dist/chunk-5ZCQSBVS.js.map +1 -0
- package/dist/{chunk-KFILBHOY.js → chunk-VMDD5QTJ.js} +247 -18
- package/dist/chunk-VMDD5QTJ.js.map +1 -0
- package/dist/{dashboard-2MLSJZQR.js → dashboard-K7LYTWSL.js} +33 -6
- package/dist/dashboard-K7LYTWSL.js.map +1 -0
- package/dist/{dashboard-WVPR5BQO.js → dashboard-ZN3FPR73.js} +33 -6
- package/dist/dashboard-ZN3FPR73.js.map +1 -0
- package/dist/index.js +669 -116
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +68 -4
- package/dist/mcp.js.map +1 -1
- package/package.json +6 -6
- package/dist/chunk-KFILBHOY.js.map +0 -1
- package/dist/chunk-SQKTDYSO.js.map +0 -1
- package/dist/dashboard-2MLSJZQR.js.map +0 -1
- package/dist/dashboard-WVPR5BQO.js.map +0 -1
|
@@ -1502,10 +1502,23 @@ var secretsPolicySchema = z2.object({
|
|
|
1502
1502
|
requireRotationFormatForTags: stringArray.optional(),
|
|
1503
1503
|
maxTtlSeconds: z2.number().optional()
|
|
1504
1504
|
}).strict();
|
|
1505
|
+
var rateLimitSchema = z2.object({
|
|
1506
|
+
maxCalls: z2.number().int().positive(),
|
|
1507
|
+
perSeconds: z2.number().positive()
|
|
1508
|
+
}).strict();
|
|
1509
|
+
var wrapPolicySchema = z2.object({
|
|
1510
|
+
allowTools: stringArray.optional(),
|
|
1511
|
+
denyTools: stringArray.optional(),
|
|
1512
|
+
approveTools: stringArray.optional(),
|
|
1513
|
+
rateLimit: rateLimitSchema.optional(),
|
|
1514
|
+
toolRateLimits: z2.record(z2.string(), rateLimitSchema).optional(),
|
|
1515
|
+
redactResults: z2.boolean().optional()
|
|
1516
|
+
}).strict();
|
|
1505
1517
|
var policySchema = z2.object({
|
|
1506
1518
|
mcp: mcpPolicySchema.optional(),
|
|
1507
1519
|
exec: execPolicySchema.optional(),
|
|
1508
|
-
secrets: secretsPolicySchema.optional()
|
|
1520
|
+
secrets: secretsPolicySchema.optional(),
|
|
1521
|
+
wrap: wrapPolicySchema.optional()
|
|
1509
1522
|
}).strict();
|
|
1510
1523
|
var PolicyConfigError = class extends Error {
|
|
1511
1524
|
constructor(message) {
|
|
@@ -1681,6 +1694,7 @@ function getPolicySummary(projectPath) {
|
|
|
1681
1694
|
hasMcpPolicy: !!policy.mcp,
|
|
1682
1695
|
hasExecPolicy: !!policy.exec,
|
|
1683
1696
|
hasSecretPolicy: !!policy.secrets,
|
|
1697
|
+
hasWrapPolicy: !!policy.wrap,
|
|
1684
1698
|
details: policy
|
|
1685
1699
|
};
|
|
1686
1700
|
}
|
|
@@ -1785,6 +1799,129 @@ function notifyApprovalRequested(key, source) {
|
|
|
1785
1799
|
);
|
|
1786
1800
|
}
|
|
1787
1801
|
|
|
1802
|
+
// src/core/canary-webhooks.ts
|
|
1803
|
+
import { existsSync as existsSync7, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7 } from "fs";
|
|
1804
|
+
import { join as join10 } from "path";
|
|
1805
|
+
import { homedir as homedir7 } from "os";
|
|
1806
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1807
|
+
function getRegistryPath4() {
|
|
1808
|
+
const override = process.env.QRING_CANARY_ALERTS_PATH;
|
|
1809
|
+
if (override) return override;
|
|
1810
|
+
const dir = join10(homedir7(), ".config", "q-ring");
|
|
1811
|
+
if (!existsSync7(dir)) mkdirSync7(dir, { recursive: true, mode: 448 });
|
|
1812
|
+
return join10(dir, "canary-alerts.json");
|
|
1813
|
+
}
|
|
1814
|
+
function loadRegistry4() {
|
|
1815
|
+
return loadJsonRegistry(getRegistryPath4(), { channels: [] });
|
|
1816
|
+
}
|
|
1817
|
+
function listCanaryAlerts() {
|
|
1818
|
+
return loadRegistry4().channels;
|
|
1819
|
+
}
|
|
1820
|
+
function describeAlertUrl(url) {
|
|
1821
|
+
try {
|
|
1822
|
+
const u = new URL(url);
|
|
1823
|
+
const tail = u.pathname.length > 12 ? `${u.pathname.slice(0, 8)}\u2026${u.pathname.slice(-4)}` : u.pathname;
|
|
1824
|
+
return `${u.protocol}//${u.host}${tail}`;
|
|
1825
|
+
} catch {
|
|
1826
|
+
return url;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
function headline(event) {
|
|
1830
|
+
return event.test ? "q-ring canary alert test" : "q-ring: CANARY TRIPPED";
|
|
1831
|
+
}
|
|
1832
|
+
function summary(event) {
|
|
1833
|
+
const who = event.agent ? `${event.source} (${event.agent})` : event.source;
|
|
1834
|
+
const where = event.env ? `${event.scope}/${event.env}` : event.scope;
|
|
1835
|
+
return event.test ? `This is a test from \`qring canary alert test\`. Trips on honeytoken "${event.key}" (${where}) would arrive here.` : `Honeytoken "${event.key}" (${where}) was read by ${who} at ${event.timestamp}. ${event.detail}. This credential is fake \u2014 but something reached for it. Investigate: qring audit --action canary`;
|
|
1836
|
+
}
|
|
1837
|
+
function buildAlertPayload(type, event) {
|
|
1838
|
+
const json = { "Content-Type": "application/json", "User-Agent": "q-ring-canary/1.0" };
|
|
1839
|
+
switch (type) {
|
|
1840
|
+
case "discord":
|
|
1841
|
+
return {
|
|
1842
|
+
body: JSON.stringify({ content: `\u{1F6A8} **${headline(event)}**
|
|
1843
|
+
${summary(event)}` }),
|
|
1844
|
+
headers: json
|
|
1845
|
+
};
|
|
1846
|
+
case "slack":
|
|
1847
|
+
return {
|
|
1848
|
+
body: JSON.stringify({ text: `:rotating_light: *${headline(event)}*
|
|
1849
|
+
${summary(event)}` }),
|
|
1850
|
+
headers: json
|
|
1851
|
+
};
|
|
1852
|
+
case "ntfy":
|
|
1853
|
+
return {
|
|
1854
|
+
body: summary(event),
|
|
1855
|
+
headers: {
|
|
1856
|
+
"Content-Type": "text/plain",
|
|
1857
|
+
"User-Agent": "q-ring-canary/1.0",
|
|
1858
|
+
Title: headline(event),
|
|
1859
|
+
Priority: event.test ? "3" : "5",
|
|
1860
|
+
Tags: event.test ? "test_tube" : "rotating_light"
|
|
1861
|
+
}
|
|
1862
|
+
};
|
|
1863
|
+
case "generic":
|
|
1864
|
+
return {
|
|
1865
|
+
body: JSON.stringify({
|
|
1866
|
+
event: "canary",
|
|
1867
|
+
test: event.test ?? false,
|
|
1868
|
+
key: event.key,
|
|
1869
|
+
scope: event.scope,
|
|
1870
|
+
env: event.env,
|
|
1871
|
+
source: event.source,
|
|
1872
|
+
agent: event.agent ?? void 0,
|
|
1873
|
+
detail: event.detail,
|
|
1874
|
+
timestamp: event.timestamp
|
|
1875
|
+
}),
|
|
1876
|
+
headers: json
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
async function sendOne(channel, event) {
|
|
1881
|
+
const base = { channelId: channel.id, type: channel.type };
|
|
1882
|
+
const ssrfBlock = await checkSSRF(channel.url);
|
|
1883
|
+
if (ssrfBlock) {
|
|
1884
|
+
logAudit({
|
|
1885
|
+
action: "policy_deny",
|
|
1886
|
+
key: event.key,
|
|
1887
|
+
scope: event.scope,
|
|
1888
|
+
source: "hook",
|
|
1889
|
+
detail: `canary alert SSRF blocked: ${describeAlertUrl(channel.url)}`
|
|
1890
|
+
});
|
|
1891
|
+
return { ...base, success: false, message: ssrfBlock };
|
|
1892
|
+
}
|
|
1893
|
+
try {
|
|
1894
|
+
const { body, headers } = buildAlertPayload(channel.type, event);
|
|
1895
|
+
const res = await httpRequest({
|
|
1896
|
+
url: channel.url,
|
|
1897
|
+
method: "POST",
|
|
1898
|
+
headers,
|
|
1899
|
+
body,
|
|
1900
|
+
timeoutMs: 1e4
|
|
1901
|
+
});
|
|
1902
|
+
return {
|
|
1903
|
+
...base,
|
|
1904
|
+
success: res.statusCode >= 200 && res.statusCode < 300,
|
|
1905
|
+
message: `HTTP ${res.statusCode}`
|
|
1906
|
+
};
|
|
1907
|
+
} catch (err) {
|
|
1908
|
+
return { ...base, success: false, message: err instanceof Error ? err.message : "HTTP error" };
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
async function sendCanaryAlerts(event, onlyId) {
|
|
1912
|
+
const channels = listCanaryAlerts().filter((c) => onlyId ? c.id === onlyId : c.enabled);
|
|
1913
|
+
if (channels.length === 0) return [];
|
|
1914
|
+
const results = await Promise.allSettled(channels.map((c) => sendOne(c, event)));
|
|
1915
|
+
return results.map(
|
|
1916
|
+
(r, i) => r.status === "fulfilled" ? r.value : {
|
|
1917
|
+
channelId: channels[i].id,
|
|
1918
|
+
type: channels[i].type,
|
|
1919
|
+
success: false,
|
|
1920
|
+
message: String(r.reason)
|
|
1921
|
+
}
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1788
1925
|
// src/core/canary-alert.ts
|
|
1789
1926
|
var TRIP_THROTTLE_MS = 30 * 1e3;
|
|
1790
1927
|
var lastAlerted = /* @__PURE__ */ new Map();
|
|
@@ -1798,16 +1935,27 @@ function recordCanaryTrip(trip) {
|
|
|
1798
1935
|
source: trip.source,
|
|
1799
1936
|
detail: `CANARY TRIPPED: ${trip.detail ?? `honeytoken read via ${trip.source}`}`
|
|
1800
1937
|
});
|
|
1801
|
-
if (!notificationsEnabled()) return;
|
|
1802
1938
|
const now = Date.now();
|
|
1803
1939
|
const last = lastAlerted.get(trip.key);
|
|
1804
1940
|
if (last !== void 0 && now - last < TRIP_THROTTLE_MS) return;
|
|
1805
1941
|
lastAlerted.set(trip.key, now);
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1942
|
+
if (notificationsEnabled()) {
|
|
1943
|
+
const who = agent ? `${trip.source} (${agent})` : trip.source;
|
|
1944
|
+
notifyUser(
|
|
1945
|
+
"q-ring: CANARY TRIPPED",
|
|
1946
|
+
`Honeytoken "${trip.key}" was read by ${who}. This credential is fake \u2014 but something reached for it. Investigate: qring audit --action canary`
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
void sendCanaryAlerts({
|
|
1950
|
+
key: trip.key,
|
|
1951
|
+
scope: trip.scope,
|
|
1952
|
+
env: trip.env,
|
|
1953
|
+
source: trip.source,
|
|
1954
|
+
agent,
|
|
1955
|
+
detail: trip.detail ?? `honeytoken read via ${trip.source}`,
|
|
1956
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1957
|
+
}).catch(() => {
|
|
1958
|
+
});
|
|
1811
1959
|
}
|
|
1812
1960
|
|
|
1813
1961
|
// src/core/provision.ts
|
|
@@ -2502,9 +2650,9 @@ function tunnelList() {
|
|
|
2502
2650
|
}
|
|
2503
2651
|
|
|
2504
2652
|
// src/core/memory.ts
|
|
2505
|
-
import { existsSync as
|
|
2506
|
-
import { join as
|
|
2507
|
-
import { homedir as
|
|
2653
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync7, mkdirSync as mkdirSync8, chmodSync as chmodSync3 } from "fs";
|
|
2654
|
+
import { join as join11 } from "path";
|
|
2655
|
+
import { homedir as homedir8, hostname, userInfo } from "os";
|
|
2508
2656
|
import {
|
|
2509
2657
|
createCipheriv as createCipheriv2,
|
|
2510
2658
|
createDecipheriv as createDecipheriv2,
|
|
@@ -2516,21 +2664,21 @@ var MEMORY_FILE = "agent-memory.enc";
|
|
|
2516
2664
|
var KEYRING_SERVICE = "qring-memory-key";
|
|
2517
2665
|
var KEYRING_ACCOUNT = "encryption-key";
|
|
2518
2666
|
function getMemoryDir() {
|
|
2519
|
-
const dir =
|
|
2520
|
-
if (!
|
|
2521
|
-
|
|
2667
|
+
const dir = join11(homedir8(), ".config", "q-ring");
|
|
2668
|
+
if (!existsSync8(dir)) {
|
|
2669
|
+
mkdirSync8(dir, { recursive: true, mode: 448 });
|
|
2522
2670
|
}
|
|
2523
2671
|
return dir;
|
|
2524
2672
|
}
|
|
2525
2673
|
function writeMemoryFile(path, data) {
|
|
2526
|
-
|
|
2674
|
+
writeFileSync7(path, data, { mode: 384 });
|
|
2527
2675
|
try {
|
|
2528
2676
|
chmodSync3(path, 384);
|
|
2529
2677
|
} catch {
|
|
2530
2678
|
}
|
|
2531
2679
|
}
|
|
2532
2680
|
function getMemoryPath() {
|
|
2533
|
-
return
|
|
2681
|
+
return join11(getMemoryDir(), MEMORY_FILE);
|
|
2534
2682
|
}
|
|
2535
2683
|
var PBKDF2_ITERATIONS2 = 21e4;
|
|
2536
2684
|
var KEY_LENGTH2 = 32;
|
|
@@ -2624,7 +2772,7 @@ function decrypt(blob) {
|
|
|
2624
2772
|
}
|
|
2625
2773
|
function loadStore2() {
|
|
2626
2774
|
const path = getMemoryPath();
|
|
2627
|
-
if (!
|
|
2775
|
+
if (!existsSync8(path)) {
|
|
2628
2776
|
return { entries: {} };
|
|
2629
2777
|
}
|
|
2630
2778
|
try {
|
|
@@ -2672,6 +2820,84 @@ function forget(key) {
|
|
|
2672
2820
|
return false;
|
|
2673
2821
|
}
|
|
2674
2822
|
|
|
2823
|
+
// src/core/sessions.ts
|
|
2824
|
+
var DEFAULT_MAX_EVENTS = 200;
|
|
2825
|
+
var UNLABELED = "unlabeled";
|
|
2826
|
+
function slug(label) {
|
|
2827
|
+
return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || UNLABELED;
|
|
2828
|
+
}
|
|
2829
|
+
function sessionKeyFor(event) {
|
|
2830
|
+
if (event.action === "wrap" && event.correlationId) return `wrap:${event.correlationId}`;
|
|
2831
|
+
if (event.agent) return `pid:${event.pid}:${slug(event.agent)}`;
|
|
2832
|
+
if (event.source === "mcp" || event.source === "agent") return `pid:${event.pid}:${UNLABELED}`;
|
|
2833
|
+
return null;
|
|
2834
|
+
}
|
|
2835
|
+
function wrapLabelFrom(detail) {
|
|
2836
|
+
const m = detail?.match(/^airlock session started: (.+?)(?: \(env (?:inherited|stripped)\))?$/);
|
|
2837
|
+
return m?.[1];
|
|
2838
|
+
}
|
|
2839
|
+
function buildSessions(events, maxEvents = DEFAULT_MAX_EVENTS) {
|
|
2840
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2841
|
+
for (const e of events) {
|
|
2842
|
+
const key = sessionKeyFor(e);
|
|
2843
|
+
if (!key) continue;
|
|
2844
|
+
const bucket = byKey.get(key);
|
|
2845
|
+
if (bucket) bucket.push(e);
|
|
2846
|
+
else byKey.set(key, [e]);
|
|
2847
|
+
}
|
|
2848
|
+
const sessions = [];
|
|
2849
|
+
for (const [key, bucket] of byKey) {
|
|
2850
|
+
bucket.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
2851
|
+
const first = bucket[0];
|
|
2852
|
+
const last = bucket[bucket.length - 1];
|
|
2853
|
+
const countsByAction = {};
|
|
2854
|
+
const sources = /* @__PURE__ */ new Map();
|
|
2855
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2856
|
+
let denials = 0;
|
|
2857
|
+
let wrapLabel;
|
|
2858
|
+
for (const e of bucket) {
|
|
2859
|
+
countsByAction[e.action] = (countsByAction[e.action] ?? 0) + 1;
|
|
2860
|
+
sources.set(e.source, (sources.get(e.source) ?? 0) + 1);
|
|
2861
|
+
if (e.key) keys.add(e.key);
|
|
2862
|
+
if (e.action === "policy_deny") denials++;
|
|
2863
|
+
if (!wrapLabel && e.action === "wrap") wrapLabel = wrapLabelFrom(e.detail);
|
|
2864
|
+
}
|
|
2865
|
+
const source = [...sources.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
2866
|
+
const agent = bucket.find((e) => e.agent)?.agent ?? UNLABELED;
|
|
2867
|
+
const isWrap = key.startsWith("wrap:");
|
|
2868
|
+
sessions.push({
|
|
2869
|
+
id: isWrap ? key.slice("wrap:".length) : `${first.pid}-${slug(agent)}`,
|
|
2870
|
+
agent,
|
|
2871
|
+
source,
|
|
2872
|
+
pid: first.pid,
|
|
2873
|
+
startedAt: first.timestamp,
|
|
2874
|
+
endedAt: last.timestamp,
|
|
2875
|
+
eventCount: bucket.length,
|
|
2876
|
+
countsByAction,
|
|
2877
|
+
keys: [...keys].sort(),
|
|
2878
|
+
denials,
|
|
2879
|
+
...isWrap ? { wrapLabel: wrapLabel ?? "(unknown command)" } : {},
|
|
2880
|
+
events: bucket.slice(-maxEvents).reverse()
|
|
2881
|
+
});
|
|
2882
|
+
}
|
|
2883
|
+
sessions.sort((a, b) => new Date(b.endedAt).getTime() - new Date(a.endedAt).getTime());
|
|
2884
|
+
return sessions;
|
|
2885
|
+
}
|
|
2886
|
+
function agentVisibleEvents(events) {
|
|
2887
|
+
return events.filter((e) => e.action !== "canary");
|
|
2888
|
+
}
|
|
2889
|
+
function loadEvents(query) {
|
|
2890
|
+
return queryAudit({ since: query.since, agent: query.agent }).filter((e) => e.action !== "list");
|
|
2891
|
+
}
|
|
2892
|
+
function listAgentSessionsForAgents(query = {}) {
|
|
2893
|
+
const sessions = buildSessions(agentVisibleEvents(loadEvents(query)), query.maxEvents);
|
|
2894
|
+
return query.limit ? sessions.slice(0, query.limit) : sessions;
|
|
2895
|
+
}
|
|
2896
|
+
function summariseSession(session) {
|
|
2897
|
+
const { events: _events, ...summary2 } = session;
|
|
2898
|
+
return summary2;
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2675
2901
|
export {
|
|
2676
2902
|
PACKAGE_VERSION,
|
|
2677
2903
|
checkDecay,
|
|
@@ -2714,6 +2940,9 @@ export {
|
|
|
2714
2940
|
remember,
|
|
2715
2941
|
recall,
|
|
2716
2942
|
listMemory,
|
|
2717
|
-
forget
|
|
2943
|
+
forget,
|
|
2944
|
+
buildSessions,
|
|
2945
|
+
listAgentSessionsForAgents,
|
|
2946
|
+
summariseSession
|
|
2718
2947
|
};
|
|
2719
|
-
//# sourceMappingURL=chunk-
|
|
2948
|
+
//# sourceMappingURL=chunk-VMDD5QTJ.js.map
|