chron-mcp 0.1.17 → 0.1.19
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/.claude-plugin/plugin.json +2 -3
- package/dist/cli/index.js +610 -51
- package/dist/index.js +53600 -52719
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -6162,7 +6162,7 @@ var require_websocket = __commonJS({
|
|
|
6162
6162
|
var http = require("http");
|
|
6163
6163
|
var net = require("net");
|
|
6164
6164
|
var tls = require("tls");
|
|
6165
|
-
var { randomBytes, createHash } = require("crypto");
|
|
6165
|
+
var { randomBytes, createHash: createHash2 } = require("crypto");
|
|
6166
6166
|
var { Duplex, Readable } = require("stream");
|
|
6167
6167
|
var { URL: URL2 } = require("url");
|
|
6168
6168
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -6842,7 +6842,7 @@ var require_websocket = __commonJS({
|
|
|
6842
6842
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
6843
6843
|
return;
|
|
6844
6844
|
}
|
|
6845
|
-
const digest =
|
|
6845
|
+
const digest = createHash2("sha1").update(key + GUID).digest("base64");
|
|
6846
6846
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
6847
6847
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
6848
6848
|
return;
|
|
@@ -7229,7 +7229,7 @@ var require_websocket_server = __commonJS({
|
|
|
7229
7229
|
var EventEmitter = require("events");
|
|
7230
7230
|
var http = require("http");
|
|
7231
7231
|
var { Duplex } = require("stream");
|
|
7232
|
-
var { createHash } = require("crypto");
|
|
7232
|
+
var { createHash: createHash2 } = require("crypto");
|
|
7233
7233
|
var extension2 = require_extension();
|
|
7234
7234
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
7235
7235
|
var subprotocol2 = require_subprotocol();
|
|
@@ -7538,7 +7538,7 @@ var require_websocket_server = __commonJS({
|
|
|
7538
7538
|
}
|
|
7539
7539
|
if (this._state > RUNNING)
|
|
7540
7540
|
return abortHandshake(socket, 503);
|
|
7541
|
-
const digest =
|
|
7541
|
+
const digest = createHash2("sha1").update(key + GUID).digest("base64");
|
|
7542
7542
|
const headers = [
|
|
7543
7543
|
"HTTP/1.1 101 Switching Protocols",
|
|
7544
7544
|
"Upgrade: websocket",
|
|
@@ -16539,7 +16539,9 @@ var init_schema = __esm({
|
|
|
16539
16539
|
title: text("title").notNull().unique(),
|
|
16540
16540
|
ai_tool: text("ai_tool"),
|
|
16541
16541
|
created_at: text("created_at").notNull(),
|
|
16542
|
-
updated_at: text("updated_at").notNull()
|
|
16542
|
+
updated_at: text("updated_at").notNull(),
|
|
16543
|
+
parent_session_id: text("parent_session_id"),
|
|
16544
|
+
external_ref: text("external_ref")
|
|
16543
16545
|
});
|
|
16544
16546
|
messages = sqliteTable("messages", {
|
|
16545
16547
|
id: text("id").primaryKey(),
|
|
@@ -16548,7 +16550,9 @@ var init_schema = __esm({
|
|
|
16548
16550
|
content: text("content").notNull(),
|
|
16549
16551
|
created_at: text("created_at").notNull(),
|
|
16550
16552
|
prev_hash: text("prev_hash"),
|
|
16551
|
-
content_hash: text("content_hash")
|
|
16553
|
+
content_hash: text("content_hash"),
|
|
16554
|
+
// NULL = pre-migration row (treated as 'message'); set for all new rows
|
|
16555
|
+
event_type: text("event_type")
|
|
16552
16556
|
});
|
|
16553
16557
|
secrets_detected = sqliteTable("secrets_detected", {
|
|
16554
16558
|
id: text("id").primaryKey(),
|
|
@@ -16579,14 +16583,25 @@ async function initDb(dbPath2) {
|
|
|
16579
16583
|
for (const sql2 of CREATE_SQL) {
|
|
16580
16584
|
await client.execute(sql2);
|
|
16581
16585
|
}
|
|
16582
|
-
const
|
|
16583
|
-
const
|
|
16584
|
-
if (!
|
|
16586
|
+
const msgInfo = await client.execute("PRAGMA table_info(messages)");
|
|
16587
|
+
const msgCols = msgInfo.rows.map((r) => r[1]);
|
|
16588
|
+
if (!msgCols.includes("prev_hash")) {
|
|
16585
16589
|
await client.execute("ALTER TABLE messages ADD COLUMN prev_hash TEXT");
|
|
16586
16590
|
}
|
|
16587
|
-
if (!
|
|
16591
|
+
if (!msgCols.includes("content_hash")) {
|
|
16588
16592
|
await client.execute("ALTER TABLE messages ADD COLUMN content_hash TEXT");
|
|
16589
16593
|
}
|
|
16594
|
+
if (!msgCols.includes("event_type")) {
|
|
16595
|
+
await client.execute("ALTER TABLE messages ADD COLUMN event_type TEXT");
|
|
16596
|
+
}
|
|
16597
|
+
const sessInfo = await client.execute("PRAGMA table_info(sessions)");
|
|
16598
|
+
const sessCols = sessInfo.rows.map((r) => r[1]);
|
|
16599
|
+
if (!sessCols.includes("parent_session_id")) {
|
|
16600
|
+
await client.execute("ALTER TABLE sessions ADD COLUMN parent_session_id TEXT");
|
|
16601
|
+
}
|
|
16602
|
+
if (!sessCols.includes("external_ref")) {
|
|
16603
|
+
await client.execute("ALTER TABLE sessions ADD COLUMN external_ref TEXT");
|
|
16604
|
+
}
|
|
16590
16605
|
return drizzle(client, { schema: schema_exports });
|
|
16591
16606
|
}
|
|
16592
16607
|
var import_os, import_fs, import_path, CREATE_SQL;
|
|
@@ -16605,7 +16620,9 @@ var init_db2 = __esm({
|
|
|
16605
16620
|
title TEXT NOT NULL UNIQUE,
|
|
16606
16621
|
ai_tool TEXT,
|
|
16607
16622
|
created_at TEXT NOT NULL,
|
|
16608
|
-
updated_at TEXT NOT NULL
|
|
16623
|
+
updated_at TEXT NOT NULL,
|
|
16624
|
+
parent_session_id TEXT,
|
|
16625
|
+
external_ref TEXT
|
|
16609
16626
|
)`,
|
|
16610
16627
|
`CREATE TABLE IF NOT EXISTS messages (
|
|
16611
16628
|
id TEXT PRIMARY KEY,
|
|
@@ -16615,6 +16632,7 @@ var init_db2 = __esm({
|
|
|
16615
16632
|
created_at TEXT NOT NULL,
|
|
16616
16633
|
prev_hash TEXT,
|
|
16617
16634
|
content_hash TEXT,
|
|
16635
|
+
event_type TEXT,
|
|
16618
16636
|
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
16619
16637
|
)`,
|
|
16620
16638
|
`CREATE TABLE IF NOT EXISTS secrets_detected (
|
|
@@ -16731,6 +16749,366 @@ var init_history = __esm({
|
|
|
16731
16749
|
}
|
|
16732
16750
|
});
|
|
16733
16751
|
|
|
16752
|
+
// src/cli/soc2.ts
|
|
16753
|
+
var soc2_exports = {};
|
|
16754
|
+
__export(soc2_exports, {
|
|
16755
|
+
buildSoc2Report: () => buildSoc2Report
|
|
16756
|
+
});
|
|
16757
|
+
async function q(db, sql2) {
|
|
16758
|
+
const res = await db.$client.execute(sql2);
|
|
16759
|
+
return res.rows;
|
|
16760
|
+
}
|
|
16761
|
+
async function queryOverview(db) {
|
|
16762
|
+
const rows = await q(db, `
|
|
16763
|
+
SELECT
|
|
16764
|
+
(SELECT COUNT(*) FROM sessions) AS total_sessions,
|
|
16765
|
+
(SELECT COUNT(*) FROM messages) AS total_messages,
|
|
16766
|
+
(SELECT COUNT(*) FROM messages WHERE role = 'user') AS user_messages,
|
|
16767
|
+
(SELECT COUNT(*) FROM messages WHERE role = 'assistant') AS ai_messages,
|
|
16768
|
+
(SELECT COUNT(*) FROM secrets_detected) AS total_detections,
|
|
16769
|
+
(SELECT MIN(created_at) FROM sessions) AS period_start,
|
|
16770
|
+
(SELECT MAX(updated_at) FROM sessions) AS period_end
|
|
16771
|
+
`);
|
|
16772
|
+
return rows[0];
|
|
16773
|
+
}
|
|
16774
|
+
async function queryToolUsage(db) {
|
|
16775
|
+
return await q(db, `
|
|
16776
|
+
SELECT
|
|
16777
|
+
COALESCE(s.ai_tool, 'unknown') AS tool,
|
|
16778
|
+
COUNT(DISTINCT s.id) AS sessions,
|
|
16779
|
+
COUNT(m.id) AS messages
|
|
16780
|
+
FROM sessions s
|
|
16781
|
+
LEFT JOIN messages m ON m.session_id = s.id
|
|
16782
|
+
GROUP BY s.ai_tool
|
|
16783
|
+
ORDER BY sessions DESC
|
|
16784
|
+
`);
|
|
16785
|
+
}
|
|
16786
|
+
async function queryDetectionsByType(db) {
|
|
16787
|
+
return await q(db, `
|
|
16788
|
+
SELECT type, COUNT(*) AS cnt, GROUP_CONCAT(masked_value, ', ') AS samples
|
|
16789
|
+
FROM secrets_detected
|
|
16790
|
+
GROUP BY type
|
|
16791
|
+
ORDER BY cnt DESC
|
|
16792
|
+
`);
|
|
16793
|
+
}
|
|
16794
|
+
async function querySessionInventory(db) {
|
|
16795
|
+
return await q(db, `
|
|
16796
|
+
SELECT
|
|
16797
|
+
s.id, s.title, s.ai_tool,
|
|
16798
|
+
s.created_at, s.updated_at,
|
|
16799
|
+
COUNT(m.id) AS message_count
|
|
16800
|
+
FROM sessions s
|
|
16801
|
+
LEFT JOIN messages m ON m.session_id = s.id
|
|
16802
|
+
GROUP BY s.id
|
|
16803
|
+
ORDER BY s.created_at DESC
|
|
16804
|
+
LIMIT 500
|
|
16805
|
+
`);
|
|
16806
|
+
}
|
|
16807
|
+
async function queryIntegrity(db) {
|
|
16808
|
+
const rows = await q(db, `
|
|
16809
|
+
SELECT session_id, content_hash, prev_hash
|
|
16810
|
+
FROM messages
|
|
16811
|
+
ORDER BY session_id, created_at, rowid
|
|
16812
|
+
`);
|
|
16813
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
16814
|
+
for (const r of rows) {
|
|
16815
|
+
const list = bySession.get(r.session_id) ?? [];
|
|
16816
|
+
list.push({ content_hash: r.content_hash, prev_hash: r.prev_hash });
|
|
16817
|
+
bySession.set(r.session_id, list);
|
|
16818
|
+
}
|
|
16819
|
+
let intact = 0, broken = 0, unchained = 0;
|
|
16820
|
+
const brokenSessions = [];
|
|
16821
|
+
for (const [sid, chain] of bySession) {
|
|
16822
|
+
const chained = chain.filter((r) => r.content_hash !== null);
|
|
16823
|
+
if (chained.length === 0) {
|
|
16824
|
+
unchained++;
|
|
16825
|
+
continue;
|
|
16826
|
+
}
|
|
16827
|
+
let ok = true;
|
|
16828
|
+
for (let i = 1; i < chained.length; i++) {
|
|
16829
|
+
if (chained[i].prev_hash !== chained[i - 1].content_hash) {
|
|
16830
|
+
ok = false;
|
|
16831
|
+
break;
|
|
16832
|
+
}
|
|
16833
|
+
}
|
|
16834
|
+
if (ok)
|
|
16835
|
+
intact++;
|
|
16836
|
+
else {
|
|
16837
|
+
broken++;
|
|
16838
|
+
brokenSessions.push(sid.slice(0, 8));
|
|
16839
|
+
}
|
|
16840
|
+
}
|
|
16841
|
+
return { intact, broken, unchained, brokenSessions };
|
|
16842
|
+
}
|
|
16843
|
+
async function dbFingerprint(db) {
|
|
16844
|
+
const rows = await q(db, `SELECT COUNT(*) AS n, MAX(created_at) AS latest FROM messages`);
|
|
16845
|
+
const row = rows[0];
|
|
16846
|
+
const h = (0, import_crypto.createHash)("sha256").update(`${row?.n ?? 0}:${row?.latest ?? ""}`).digest("hex");
|
|
16847
|
+
return h.slice(0, 16);
|
|
16848
|
+
}
|
|
16849
|
+
function esc(s) {
|
|
16850
|
+
return (s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
16851
|
+
}
|
|
16852
|
+
function fmtDate2(iso) {
|
|
16853
|
+
if (!iso)
|
|
16854
|
+
return "\u2014";
|
|
16855
|
+
return iso.slice(0, 10);
|
|
16856
|
+
}
|
|
16857
|
+
function severityBadge(type) {
|
|
16858
|
+
const level = SEVERITY[type] ?? "Low";
|
|
16859
|
+
const color = level === "Critical" ? "#dc2626" : level === "High" ? "#d97706" : level === "Medium" ? "#2563eb" : "#6b7280";
|
|
16860
|
+
return `<span style="background:${color};color:#fff;padding:1px 6px;border-radius:3px;font-size:11px;font-weight:600">${level}</span>`;
|
|
16861
|
+
}
|
|
16862
|
+
function buildHtml(params) {
|
|
16863
|
+
const { host, fingerprint, overview, toolUsage, detections, inventory, integrity, generatedAt } = params;
|
|
16864
|
+
const periodStart = fmtDate2(overview.period_start) || "\u2014";
|
|
16865
|
+
const periodEnd = fmtDate2(overview.period_end) || "\u2014";
|
|
16866
|
+
const integrityStatus = integrity.broken === 0 ? `<span class="ok">✓ All ${integrity.intact} chained session(s) intact</span>` : `<span class="fail">⚠ ${integrity.broken} session(s) with broken chain</span>`;
|
|
16867
|
+
const toolRows = toolUsage.map(
|
|
16868
|
+
(r) => `<tr><td>${esc(r.tool)}</td><td>${r.sessions}</td><td>${r.messages}</td></tr>`
|
|
16869
|
+
).join("");
|
|
16870
|
+
const detectionRows = detections.map((r) => {
|
|
16871
|
+
const samples = (r.samples ?? "").split(", ").slice(0, 3).map(esc).join(", ");
|
|
16872
|
+
return `<tr><td>${esc(r.type)}</td><td>${severityBadge(r.type)}</td><td>${r.cnt}</td><td>${samples}</td></tr>`;
|
|
16873
|
+
}).join("") || '<tr><td colspan="4" style="color:#6b7280">No detections recorded</td></tr>';
|
|
16874
|
+
const inventoryRows = inventory.map(
|
|
16875
|
+
(r) => `<tr><td><code style="font-size:11px">${esc(r.id.slice(0, 8))}</code></td><td>${esc(r.title)}</td><td>${esc(r.ai_tool ?? "unknown")}</td><td>${fmtDate2(r.created_at)}</td><td>${fmtDate2(r.updated_at)}</td><td>${r.message_count}</td></tr>`
|
|
16876
|
+
).join("") || '<tr><td colspan="6" style="color:#6b7280">No sessions</td></tr>';
|
|
16877
|
+
const brokenList = integrity.brokenSessions.length > 0 ? `<p class="fail">Sessions with broken chains: ${integrity.brokenSessions.map(esc).join(", ")}</p>` : "";
|
|
16878
|
+
return `<!DOCTYPE html>
|
|
16879
|
+
<html lang="en">
|
|
16880
|
+
<head>
|
|
16881
|
+
<meta charset="UTF-8">
|
|
16882
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
16883
|
+
<title>Chron AI Audit \u2014 SOC 2 Evidence Package</title>
|
|
16884
|
+
<style>${CSS}</style>
|
|
16885
|
+
</head>
|
|
16886
|
+
<body>
|
|
16887
|
+
<div class="page">
|
|
16888
|
+
|
|
16889
|
+
<!-- \u2460 COVER PAGE -->
|
|
16890
|
+
<div style="min-height:60vh;display:flex;flex-direction:column;justify-content:center">
|
|
16891
|
+
<div class="cover-badge">SOC 2 Evidence Package</div>
|
|
16892
|
+
<h1>AI Conversation Audit Report</h1>
|
|
16893
|
+
<p class="meta">Prepared by <strong>chron-mcp</strong> | Organisation: <strong>${esc(host)}</strong></p>
|
|
16894
|
+
<table style="width:auto;margin-bottom:0">
|
|
16895
|
+
<tr><th>Reporting Period</th><td>${periodStart} \u2013 ${periodEnd}</td></tr>
|
|
16896
|
+
<tr><th>Generated</th><td>${generatedAt}</td></tr>
|
|
16897
|
+
<tr><th>Evidence DB Fingerprint</th><td><span class="fingerprint">${fingerprint}</span></td></tr>
|
|
16898
|
+
<tr><th>Total Sessions</th><td>${overview.total_sessions}</td></tr>
|
|
16899
|
+
<tr><th>Total Messages</th><td>${overview.total_messages}</td></tr>
|
|
16900
|
+
</table>
|
|
16901
|
+
<p style="margin-top:20px;font-size:11px;color:#9ca3af">
|
|
16902
|
+
This report is generated from a locally-stored, tamper-evident SQLite database. All AI conversation
|
|
16903
|
+
activity has been recorded with cryptographic hash chaining. No conversation content is transmitted
|
|
16904
|
+
externally. This document is intended as supporting evidence for SOC 2 Type II audits under the
|
|
16905
|
+
Availability and Confidentiality trust service criteria.
|
|
16906
|
+
</p>
|
|
16907
|
+
</div>
|
|
16908
|
+
|
|
16909
|
+
<!-- \u2461 EXECUTIVE SUMMARY -->
|
|
16910
|
+
<h2 class="section-break">Executive Summary</h2>
|
|
16911
|
+
<div class="stat-grid">
|
|
16912
|
+
<div class="stat"><div class="stat-value">${overview.total_sessions}</div><div class="stat-label">Total Sessions</div></div>
|
|
16913
|
+
<div class="stat"><div class="stat-value">${overview.total_messages}</div><div class="stat-label">Total Messages</div></div>
|
|
16914
|
+
<div class="stat"><div class="stat-value">${overview.total_detections}</div><div class="stat-label">PII / Secret Detections</div></div>
|
|
16915
|
+
<div class="stat"><div class="stat-value">${integrity.broken === 0 ? "\u2713" : "\u2717"}</div><div class="stat-label">Chain Integrity</div></div>
|
|
16916
|
+
</div>
|
|
16917
|
+
<p>Messages: <strong>${overview.user_messages}</strong> user turns, <strong>${overview.ai_messages}</strong> AI turns. Reporting period: <strong>${periodStart}</strong> to <strong>${periodEnd}</strong>.</p>
|
|
16918
|
+
<p>Chain integrity: ${integrityStatus}${integrity.unchained > 0 ? ` (${integrity.unchained} session(s) pre-date hash chaining)` : ""}.</p>
|
|
16919
|
+
${overview.total_detections > 0 ? `<p class="warn">⚠ ${overview.total_detections} sensitive data detection(s) recorded. See Section 5 for details.</p>` : `<p class="ok">✓ No sensitive data detections recorded in this period.</p>`}
|
|
16920
|
+
|
|
16921
|
+
<!-- \u2462 AI TOOL USAGE -->
|
|
16922
|
+
<h2>AI Tool Usage</h2>
|
|
16923
|
+
<table>
|
|
16924
|
+
<thead><tr><th>AI Tool / Provider</th><th>Sessions</th><th>Messages</th></tr></thead>
|
|
16925
|
+
<tbody>${toolRows || '<tr><td colspan="3" style="color:#6b7280">No data</td></tr>'}</tbody>
|
|
16926
|
+
</table>
|
|
16927
|
+
|
|
16928
|
+
<!-- \u2463 ACTIVITY SUMMARY -->
|
|
16929
|
+
<h2>Activity Summary</h2>
|
|
16930
|
+
<p>All sessions are listed in the Session Inventory (Section 7). The table below shows per-tool message distribution.</p>
|
|
16931
|
+
<table>
|
|
16932
|
+
<thead><tr><th>AI Tool</th><th>User Messages</th><th>AI Messages</th><th>Ratio</th></tr></thead>
|
|
16933
|
+
<tbody>
|
|
16934
|
+
${toolUsage.map((r) => {
|
|
16935
|
+
const ratio = r.messages > 0 ? (r.messages / 2 / r.sessions).toFixed(1) : "0";
|
|
16936
|
+
return `<tr><td>${esc(r.tool)}</td><td>~${Math.round(r.messages / 2)}</td><td>~${Math.round(r.messages / 2)}</td><td>${ratio} turns/session avg</td></tr>`;
|
|
16937
|
+
}).join("") || '<tr><td colspan="4" style="color:#6b7280">No data</td></tr>'}
|
|
16938
|
+
</tbody>
|
|
16939
|
+
</table>
|
|
16940
|
+
<p style="font-size:11px;color:#6b7280">Note: User/AI message split shown as estimate (\xF72) because exact per-session breakdown requires individual session queries.</p>
|
|
16941
|
+
|
|
16942
|
+
<!-- \u2464 SENSITIVE DATA CONTROLS -->
|
|
16943
|
+
<h2 class="section-break">Sensitive Data Controls</h2>
|
|
16944
|
+
<p>The following sensitive data types were detected during the reporting period. All values are masked \u2014 no plaintext credentials are stored in this report.</p>
|
|
16945
|
+
<table>
|
|
16946
|
+
<thead><tr><th>Detection Type</th><th>Severity</th><th>Count</th><th>Sample Masked Values</th></tr></thead>
|
|
16947
|
+
<tbody>${detectionRows}</tbody>
|
|
16948
|
+
</table>
|
|
16949
|
+
<h3>Control Measures</h3>
|
|
16950
|
+
<ul>
|
|
16951
|
+
<li>All detected values are masked at log time using token substitution (e.g. <code>$CHRON_CREDIT_CARD_1</code>).</li>
|
|
16952
|
+
<li>Original values are never stored in plaintext in the audit database.</li>
|
|
16953
|
+
<li>Detections trigger a <code>secret_detected</code> relay event to any connected SIEM (Splunk, Sentinel, LogScale).</li>
|
|
16954
|
+
<li>Luhn validation is applied to credit card numbers; IBAN mod-97 validation applied to bank account numbers.</li>
|
|
16955
|
+
</ul>
|
|
16956
|
+
|
|
16957
|
+
<!-- \u2465 AUDIT TRAIL INTEGRITY -->
|
|
16958
|
+
<h2>Audit Trail Integrity</h2>
|
|
16959
|
+
<p>Each message is linked to the previous message in its session via SHA-256 hash chaining. The chain is verified by confirming that each message's <code>prev_hash</code> matches the <code>content_hash</code> of the preceding message.</p>
|
|
16960
|
+
<table>
|
|
16961
|
+
<thead><tr><th>Metric</th><th>Value</th></tr></thead>
|
|
16962
|
+
<tbody>
|
|
16963
|
+
<tr><td>Sessions with intact chain</td><td class="ok">${integrity.intact}</td></tr>
|
|
16964
|
+
<tr><td>Sessions with broken chain</td><td class="${integrity.broken > 0 ? "fail" : "ok"}">${integrity.broken}</td></tr>
|
|
16965
|
+
<tr><td>Sessions pre-dating hash chaining</td><td>${integrity.unchained}</td></tr>
|
|
16966
|
+
</tbody>
|
|
16967
|
+
</table>
|
|
16968
|
+
${brokenList}
|
|
16969
|
+
<p style="font-size:11px;color:#6b7280">
|
|
16970
|
+
Note: This report verifies <em>chain linkage</em> (prev_hash continuity). Full content-hash recomputation
|
|
16971
|
+
(which detects in-place tampering) can be performed with <code>chron verify <session-id></code>.
|
|
16972
|
+
</p>
|
|
16973
|
+
|
|
16974
|
+
<!-- \u2466 SESSION INVENTORY -->
|
|
16975
|
+
<h2 class="section-break">Session Inventory</h2>
|
|
16976
|
+
<p>All sessions recorded in the audit database (most recent first, capped at 500 rows).</p>
|
|
16977
|
+
<table>
|
|
16978
|
+
<thead><tr><th>ID (prefix)</th><th>Title</th><th>AI Tool</th><th>Started</th><th>Last Active</th><th>Messages</th></tr></thead>
|
|
16979
|
+
<tbody>${inventoryRows}</tbody>
|
|
16980
|
+
</table>
|
|
16981
|
+
|
|
16982
|
+
<!-- \u2467 METHODOLOGY APPENDIX -->
|
|
16983
|
+
<h2>Methodology Appendix</h2>
|
|
16984
|
+
<div class="appendix">
|
|
16985
|
+
<h3>Data Collection</h3>
|
|
16986
|
+
<p>Chron MCP intercepts every AI conversation turn via the Model Context Protocol. Messages are written to a local SQLite database at <code>~/.chron/chron.db</code> (or the path specified by <code>CHRON_DB_PATH</code>).</p>
|
|
16987
|
+
|
|
16988
|
+
<h3>Hash Chain Construction</h3>
|
|
16989
|
+
<ul>
|
|
16990
|
+
<li>Each message record contains a <code>content_hash</code> field: <code>SHA-256(session_id || role || content || created_at || prev_hash)</code></li>
|
|
16991
|
+
<li>The <code>prev_hash</code> field links to the <code>content_hash</code> of the preceding message in the session.</li>
|
|
16992
|
+
<li>Any insertion, deletion, or modification of a message breaks the chain at that point.</li>
|
|
16993
|
+
</ul>
|
|
16994
|
+
|
|
16995
|
+
<h3>PII / Secret Detection</h3>
|
|
16996
|
+
<ul>
|
|
16997
|
+
<li>Detection uses regex patterns with algorithmic validation (Luhn for credit cards, mod-97 for IBAN).</li>
|
|
16998
|
+
<li>Context-aware patterns (DOB, passport) require relevant keywords within 120 characters.</li>
|
|
16999
|
+
<li>Severity levels: Critical (SSN, credit card, private key, IBAN), High (API keys, passwords, passport), Medium (email, phone), Low (internal IP, env vars).</li>
|
|
17000
|
+
</ul>
|
|
17001
|
+
|
|
17002
|
+
<h3>Limitations</h3>
|
|
17003
|
+
<ul>
|
|
17004
|
+
<li>This report covers conversation metadata and detection events. Message content is not included.</li>
|
|
17005
|
+
<li>Developer identity is derived from session titles and AI tool labels, not from authenticated principals.</li>
|
|
17006
|
+
<li>The chain integrity check in this report is a linkage check. Re-run <code>chron verify</code> per session for full content-hash verification.</li>
|
|
17007
|
+
</ul>
|
|
17008
|
+
|
|
17009
|
+
<p style="margin-top:12px;font-size:11px;color:#9ca3af">Generated by chron-mcp | Evidence DB fingerprint: <span class="fingerprint">${fingerprint}</span> | ${generatedAt}</p>
|
|
17010
|
+
</div>
|
|
17011
|
+
|
|
17012
|
+
</div>
|
|
17013
|
+
</body>
|
|
17014
|
+
</html>`;
|
|
17015
|
+
}
|
|
17016
|
+
async function buildSoc2Report(outputPath) {
|
|
17017
|
+
const db = await initDb();
|
|
17018
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19) + " UTC";
|
|
17019
|
+
const [overview, toolUsage, detections, inventory, integrity, fingerprint] = await Promise.all([
|
|
17020
|
+
queryOverview(db),
|
|
17021
|
+
queryToolUsage(db),
|
|
17022
|
+
queryDetectionsByType(db),
|
|
17023
|
+
querySessionInventory(db),
|
|
17024
|
+
queryIntegrity(db),
|
|
17025
|
+
dbFingerprint(db)
|
|
17026
|
+
]);
|
|
17027
|
+
const html = buildHtml({
|
|
17028
|
+
host: (0, import_os2.hostname)(),
|
|
17029
|
+
fingerprint,
|
|
17030
|
+
overview,
|
|
17031
|
+
toolUsage,
|
|
17032
|
+
detections,
|
|
17033
|
+
inventory,
|
|
17034
|
+
integrity,
|
|
17035
|
+
generatedAt
|
|
17036
|
+
});
|
|
17037
|
+
(0, import_fs2.writeFileSync)(outputPath, html, "utf8");
|
|
17038
|
+
}
|
|
17039
|
+
var import_fs2, import_os2, import_crypto, SEVERITY, CSS;
|
|
17040
|
+
var init_soc2 = __esm({
|
|
17041
|
+
"src/cli/soc2.ts"() {
|
|
17042
|
+
"use strict";
|
|
17043
|
+
import_fs2 = require("fs");
|
|
17044
|
+
import_os2 = require("os");
|
|
17045
|
+
import_crypto = require("crypto");
|
|
17046
|
+
init_db2();
|
|
17047
|
+
SEVERITY = {
|
|
17048
|
+
private_key: "Critical",
|
|
17049
|
+
credit_card: "Critical",
|
|
17050
|
+
ssn: "Critical",
|
|
17051
|
+
iban: "Critical",
|
|
17052
|
+
passport: "High",
|
|
17053
|
+
dob: "High",
|
|
17054
|
+
aws_access_key: "High",
|
|
17055
|
+
anthropic_api_key: "High",
|
|
17056
|
+
openai_api_key: "High",
|
|
17057
|
+
google_api_key: "High",
|
|
17058
|
+
github_token: "High",
|
|
17059
|
+
slack_token: "High",
|
|
17060
|
+
stripe_key: "High",
|
|
17061
|
+
sendgrid_key: "High",
|
|
17062
|
+
huggingface_token: "High",
|
|
17063
|
+
jwt: "High",
|
|
17064
|
+
url_credentials: "High",
|
|
17065
|
+
password: "High",
|
|
17066
|
+
credential_pair: "High",
|
|
17067
|
+
email: "Medium",
|
|
17068
|
+
phone_us: "Medium",
|
|
17069
|
+
phone_e164: "Medium",
|
|
17070
|
+
internal_ip: "Low",
|
|
17071
|
+
env_value: "Low"
|
|
17072
|
+
};
|
|
17073
|
+
CSS = `
|
|
17074
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
17075
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 13px; color: #111; background: #fff; line-height: 1.5; }
|
|
17076
|
+
.page { max-width: 900px; margin: 0 auto; padding: 40px 48px; }
|
|
17077
|
+
h1 { font-size: 28px; font-weight: 700; margin-bottom: 4px; }
|
|
17078
|
+
h2 { font-size: 17px; font-weight: 700; margin: 32px 0 10px; padding-bottom: 4px; border-bottom: 2px solid #e5e7eb; color: #1f2937; }
|
|
17079
|
+
h3 { font-size: 14px; font-weight: 600; margin: 20px 0 6px; color: #374151; }
|
|
17080
|
+
p { margin-bottom: 8px; color: #374151; }
|
|
17081
|
+
.meta { color: #6b7280; font-size: 12px; margin-bottom: 32px; }
|
|
17082
|
+
.cover-badge { display: inline-block; background: #1d4ed8; color: #fff; padding: 3px 10px; border-radius: 4px; font-size: 12px; font-weight: 600; margin-bottom: 16px; }
|
|
17083
|
+
table { width: 100%; border-collapse: collapse; margin-bottom: 16px; font-size: 12px; }
|
|
17084
|
+
th { background: #f3f4f6; text-align: left; padding: 6px 10px; font-weight: 600; color: #374151; border: 1px solid #e5e7eb; }
|
|
17085
|
+
td { padding: 5px 10px; border: 1px solid #e5e7eb; vertical-align: top; word-break: break-word; max-width: 300px; }
|
|
17086
|
+
tr:nth-child(even) td { background: #fafafa; }
|
|
17087
|
+
.stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
|
|
17088
|
+
.stat { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 12px 16px; }
|
|
17089
|
+
.stat-value { font-size: 24px; font-weight: 700; color: #1f2937; }
|
|
17090
|
+
.stat-label { font-size: 11px; color: #6b7280; margin-top: 2px; }
|
|
17091
|
+
.ok { color: #16a34a; font-weight: 600; }
|
|
17092
|
+
.warn { color: #d97706; font-weight: 600; }
|
|
17093
|
+
.fail { color: #dc2626; font-weight: 600; }
|
|
17094
|
+
.fingerprint { font-family: monospace; background: #f3f4f6; padding: 2px 6px; border-radius: 3px; font-size: 11px; }
|
|
17095
|
+
.section-break { page-break-before: always; }
|
|
17096
|
+
.appendix { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 16px 20px; margin-top: 12px; }
|
|
17097
|
+
.appendix p, .appendix li { color: #374151; font-size: 12px; }
|
|
17098
|
+
ul { padding-left: 20px; margin-bottom: 8px; }
|
|
17099
|
+
li { margin-bottom: 4px; }
|
|
17100
|
+
@media print {
|
|
17101
|
+
body { font-size: 11px; }
|
|
17102
|
+
.page { padding: 20px 24px; max-width: 100%; }
|
|
17103
|
+
h1 { font-size: 22px; }
|
|
17104
|
+
h2 { font-size: 14px; }
|
|
17105
|
+
.stat-value { font-size: 20px; }
|
|
17106
|
+
.section-break { page-break-before: always; }
|
|
17107
|
+
}
|
|
17108
|
+
`;
|
|
17109
|
+
}
|
|
17110
|
+
});
|
|
17111
|
+
|
|
16734
17112
|
// src/cli/report.ts
|
|
16735
17113
|
var report_exports = {};
|
|
16736
17114
|
__export(report_exports, {
|
|
@@ -16814,6 +17192,16 @@ ${BOLD2}Chron Report${RESET2} ${dateFrom} \u2192 ${dateTo}
|
|
|
16814
17192
|
process.stdout.write("\n");
|
|
16815
17193
|
}
|
|
16816
17194
|
async function runReport(args2) {
|
|
17195
|
+
const format = args2.find((a) => a.startsWith("--format="))?.slice("--format=".length);
|
|
17196
|
+
const outputArg = args2.find((a) => a.startsWith("--output="))?.slice("--output=".length);
|
|
17197
|
+
if (format === "soc2") {
|
|
17198
|
+
const output = outputArg ?? "soc2-report.html";
|
|
17199
|
+
const { buildSoc2Report: buildSoc2Report2 } = await Promise.resolve().then(() => (init_soc2(), soc2_exports));
|
|
17200
|
+
await buildSoc2Report2(output);
|
|
17201
|
+
process.stdout.write(`SOC 2 evidence package written to ${output}
|
|
17202
|
+
`);
|
|
17203
|
+
return;
|
|
17204
|
+
}
|
|
16817
17205
|
const sinceArg = args2.find((a) => a.startsWith("--since="))?.slice("--since=".length);
|
|
16818
17206
|
let cutoffDate = null;
|
|
16819
17207
|
if (sinceArg) {
|
|
@@ -16914,7 +17302,7 @@ __export(settings_exports, {
|
|
|
16914
17302
|
runSettings: () => runSettings
|
|
16915
17303
|
});
|
|
16916
17304
|
function dbPath() {
|
|
16917
|
-
return process.env.CHRON_DB_PATH ?? (0, import_path2.join)((0,
|
|
17305
|
+
return process.env.CHRON_DB_PATH ?? (0, import_path2.join)((0, import_os3.homedir)(), ".chron", "chron.db");
|
|
16918
17306
|
}
|
|
16919
17307
|
async function runSettings(_args) {
|
|
16920
17308
|
process.stdout.write(`
|
|
@@ -16928,11 +17316,11 @@ ${BOLD3}Chron Settings${RESET3}
|
|
|
16928
17316
|
|
|
16929
17317
|
`);
|
|
16930
17318
|
}
|
|
16931
|
-
var
|
|
17319
|
+
var import_os3, import_path2, RESET3, BOLD3, DIM2, CYAN2;
|
|
16932
17320
|
var init_settings = __esm({
|
|
16933
17321
|
"src/cli/settings.ts"() {
|
|
16934
17322
|
"use strict";
|
|
16935
|
-
|
|
17323
|
+
import_os3 = require("os");
|
|
16936
17324
|
import_path2 = require("path");
|
|
16937
17325
|
RESET3 = "\x1B[0m";
|
|
16938
17326
|
BOLD3 = "\x1B[1m";
|
|
@@ -16946,7 +17334,7 @@ var secrets_exports = {};
|
|
|
16946
17334
|
__export(secrets_exports, {
|
|
16947
17335
|
runSecrets: () => runSecrets
|
|
16948
17336
|
});
|
|
16949
|
-
function
|
|
17337
|
+
function fmtDate3(iso) {
|
|
16950
17338
|
return iso.slice(0, 16).replace("T", " ");
|
|
16951
17339
|
}
|
|
16952
17340
|
async function runSecrets(args2) {
|
|
@@ -16983,7 +17371,7 @@ ${BOLD4}${session.title}${RESET4}
|
|
|
16983
17371
|
`);
|
|
16984
17372
|
for (const row of rows) {
|
|
16985
17373
|
process.stdout.write(
|
|
16986
|
-
` ${DIM3}${
|
|
17374
|
+
` ${DIM3}${fmtDate3(row.detected_at)}${RESET4} ${YELLOW2}${row.type.padEnd(maxType)}${RESET4} ${row.masked_value}
|
|
16987
17375
|
`
|
|
16988
17376
|
);
|
|
16989
17377
|
}
|
|
@@ -17004,7 +17392,7 @@ ${BOLD4}${session.title}${RESET4}
|
|
|
17004
17392
|
process.stdout.write("\n");
|
|
17005
17393
|
for (const row of rows) {
|
|
17006
17394
|
process.stdout.write(
|
|
17007
|
-
` ${DIM3}${
|
|
17395
|
+
` ${DIM3}${fmtDate3(row.detected_at)}${RESET4} ${CYAN3}${row.session_id.slice(0, 8)}${RESET4} ${YELLOW2}${row.type.padEnd(maxType)}${RESET4} ${row.masked_value.padEnd(20)} ${DIM3}${row.title}${RESET4}
|
|
17008
17396
|
`
|
|
17009
17397
|
);
|
|
17010
17398
|
}
|
|
@@ -17031,7 +17419,7 @@ var require_package = __commonJS({
|
|
|
17031
17419
|
"package.json"(exports2, module2) {
|
|
17032
17420
|
module2.exports = {
|
|
17033
17421
|
name: "chron-mcp",
|
|
17034
|
-
version: "0.1.
|
|
17422
|
+
version: "0.1.19",
|
|
17035
17423
|
mcpName: "io.github.sirinivask/chron",
|
|
17036
17424
|
description: "Audit-grade timestamped logs for every AI conversation",
|
|
17037
17425
|
repository: {
|
|
@@ -17108,33 +17496,35 @@ function prompt(rl, question) {
|
|
|
17108
17496
|
return new Promise((resolve) => rl.question(question, resolve));
|
|
17109
17497
|
}
|
|
17110
17498
|
function configPath() {
|
|
17111
|
-
return (0, import_path3.join)((0,
|
|
17499
|
+
return (0, import_path3.join)((0, import_os4.homedir)(), ".chron", "config.json");
|
|
17112
17500
|
}
|
|
17113
17501
|
function loadConfig() {
|
|
17114
17502
|
try {
|
|
17115
|
-
return JSON.parse((0,
|
|
17503
|
+
return JSON.parse((0, import_fs3.readFileSync)(configPath(), "utf8"));
|
|
17116
17504
|
} catch {
|
|
17117
17505
|
return {};
|
|
17118
17506
|
}
|
|
17119
17507
|
}
|
|
17120
17508
|
function saveConfig(data) {
|
|
17121
|
-
const dir = (0, import_path3.join)((0,
|
|
17122
|
-
(0,
|
|
17123
|
-
(0,
|
|
17509
|
+
const dir = (0, import_path3.join)((0, import_os4.homedir)(), ".chron");
|
|
17510
|
+
(0, import_fs3.mkdirSync)(dir, { recursive: true });
|
|
17511
|
+
(0, import_fs3.writeFileSync)(configPath(), JSON.stringify(data, null, 2), "utf8");
|
|
17124
17512
|
}
|
|
17125
17513
|
function patchClaudeJson(vars) {
|
|
17126
|
-
const path = (0, import_path3.join)((0,
|
|
17127
|
-
if (!(0,
|
|
17514
|
+
const path = (0, import_path3.join)((0, import_os4.homedir)(), ".claude.json");
|
|
17515
|
+
if (!(0, import_fs3.existsSync)(path))
|
|
17128
17516
|
return false;
|
|
17129
17517
|
try {
|
|
17130
|
-
const raw = (0,
|
|
17518
|
+
const raw = (0, import_fs3.readFileSync)(path, "utf8");
|
|
17131
17519
|
const doc = JSON.parse(raw);
|
|
17132
17520
|
const servers = doc.mcpServers ?? {};
|
|
17133
17521
|
const chron = servers.chron ?? {};
|
|
17522
|
+
if (!chron.command)
|
|
17523
|
+
return false;
|
|
17134
17524
|
chron.env = { ...chron.env ?? {}, ...vars };
|
|
17135
17525
|
servers.chron = chron;
|
|
17136
17526
|
doc.mcpServers = servers;
|
|
17137
|
-
(0,
|
|
17527
|
+
(0, import_fs3.writeFileSync)(path, JSON.stringify(doc, null, 2), "utf8");
|
|
17138
17528
|
return true;
|
|
17139
17529
|
} catch {
|
|
17140
17530
|
return false;
|
|
@@ -17493,30 +17883,20 @@ ${DIM4}Authenticating with Azure AD...${RESET5} `);
|
|
|
17493
17883
|
process.exit(1);
|
|
17494
17884
|
}
|
|
17495
17885
|
const config = loadConfig();
|
|
17496
|
-
config.sentinel = { dce, dcrId, stream, tenantId, clientId, connected_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
17886
|
+
config.sentinel = { dce, dcrId, stream, tenantId, clientId, clientSecret, connected_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
17497
17887
|
saveConfig(config);
|
|
17888
|
+
patchClaudeJson({
|
|
17889
|
+
CHRON_SENTINEL_TENANT_ID: tenantId,
|
|
17890
|
+
CHRON_SENTINEL_CLIENT_ID: clientId,
|
|
17891
|
+
CHRON_SENTINEL_CLIENT_SECRET: clientSecret,
|
|
17892
|
+
CHRON_SENTINEL_DCE: dce,
|
|
17893
|
+
CHRON_SENTINEL_DCR_ID: dcrId,
|
|
17894
|
+
CHRON_SENTINEL_STREAM: stream
|
|
17895
|
+
});
|
|
17498
17896
|
process.stdout.write(`${GREEN}${BOLD5}Connected!${RESET5} Test event sent to Sentinel workspace.
|
|
17499
17897
|
|
|
17500
17898
|
`);
|
|
17501
|
-
process.stdout.write(`${
|
|
17502
|
-
|
|
17503
|
-
`);
|
|
17504
|
-
process.stdout.write(` ${CYAN4}CHRON_SENTINEL_TENANT_ID${RESET5} ${tenantId}
|
|
17505
|
-
`);
|
|
17506
|
-
process.stdout.write(` ${CYAN4}CHRON_SENTINEL_CLIENT_ID${RESET5} ${clientId}
|
|
17507
|
-
`);
|
|
17508
|
-
process.stdout.write(` ${CYAN4}CHRON_SENTINEL_CLIENT_SECRET${RESET5} ${DIM4}<your-client-secret>${RESET5}
|
|
17509
|
-
`);
|
|
17510
|
-
process.stdout.write(` ${CYAN4}CHRON_SENTINEL_DCE${RESET5} ${dce}
|
|
17511
|
-
`);
|
|
17512
|
-
process.stdout.write(` ${CYAN4}CHRON_SENTINEL_DCR_ID${RESET5} ${dcrId}
|
|
17513
|
-
`);
|
|
17514
|
-
process.stdout.write(` ${CYAN4}CHRON_SENTINEL_STREAM${RESET5} ${stream}
|
|
17515
|
-
|
|
17516
|
-
`);
|
|
17517
|
-
process.stdout.write(`${DIM4}For Claude Code \u2014 add to the "env" block of the chron entry in ~/.claude.json.
|
|
17518
|
-
`);
|
|
17519
|
-
process.stdout.write(`Connection saved to ~/.chron/config.json${RESET5}
|
|
17899
|
+
process.stdout.write(`${DIM4}Config saved to ~/.chron/config.json \u2014 events will flow immediately in all running sessions.${RESET5}
|
|
17520
17900
|
|
|
17521
17901
|
`);
|
|
17522
17902
|
}
|
|
@@ -17549,14 +17929,14 @@ Integrations:
|
|
|
17549
17929
|
}
|
|
17550
17930
|
}
|
|
17551
17931
|
}
|
|
17552
|
-
var import_readline,
|
|
17932
|
+
var import_readline, import_os4, import_path3, import_fs3, RESET5, BOLD5, DIM4, CYAN4, GREEN, RED, YELLOW3;
|
|
17553
17933
|
var init_connect = __esm({
|
|
17554
17934
|
"src/cli/connect.ts"() {
|
|
17555
17935
|
"use strict";
|
|
17556
17936
|
import_readline = require("readline");
|
|
17557
|
-
|
|
17937
|
+
import_os4 = require("os");
|
|
17558
17938
|
import_path3 = require("path");
|
|
17559
|
-
|
|
17939
|
+
import_fs3 = require("fs");
|
|
17560
17940
|
RESET5 = "\x1B[0m";
|
|
17561
17941
|
BOLD5 = "\x1B[1m";
|
|
17562
17942
|
DIM4 = "\x1B[2m";
|
|
@@ -17567,6 +17947,177 @@ var init_connect = __esm({
|
|
|
17567
17947
|
}
|
|
17568
17948
|
});
|
|
17569
17949
|
|
|
17950
|
+
// src/cli/summary.ts
|
|
17951
|
+
var summary_exports = {};
|
|
17952
|
+
__export(summary_exports, {
|
|
17953
|
+
buildSummary: () => buildSummary,
|
|
17954
|
+
runSummary: () => runSummary
|
|
17955
|
+
});
|
|
17956
|
+
function detectMutations(content) {
|
|
17957
|
+
const hits = [];
|
|
17958
|
+
for (const { label, re } of MUTATION_PATTERNS) {
|
|
17959
|
+
const m = re.exec(content);
|
|
17960
|
+
if (m)
|
|
17961
|
+
hits.push(`${label}: ${m[0].trim().slice(0, 80)}`);
|
|
17962
|
+
}
|
|
17963
|
+
return hits;
|
|
17964
|
+
}
|
|
17965
|
+
function formatLatency(ms) {
|
|
17966
|
+
if (ms < 1e3)
|
|
17967
|
+
return `${ms}ms`;
|
|
17968
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
17969
|
+
}
|
|
17970
|
+
function formatTs(iso) {
|
|
17971
|
+
return iso.replace("T", " ").replace(/\.\d+.*$/, "");
|
|
17972
|
+
}
|
|
17973
|
+
async function buildSummary(sessionPrefix) {
|
|
17974
|
+
const db = await initDb();
|
|
17975
|
+
const allSessions = await db.select().from(sessions);
|
|
17976
|
+
const session = allSessions.find((s) => s.id.startsWith(sessionPrefix));
|
|
17977
|
+
if (!session)
|
|
17978
|
+
return null;
|
|
17979
|
+
const msgs = await db.select().from(messages).where(eq(messages.session_id, session.id)).orderBy(asc(messages.created_at), asc(sql`rowid`));
|
|
17980
|
+
const secs = await db.select().from(secrets_detected).where(eq(secrets_detected.session_id, session.id)).orderBy(asc(secrets_detected.detected_at));
|
|
17981
|
+
const timeline = [];
|
|
17982
|
+
const mutationsList = [];
|
|
17983
|
+
const prodWrites = [];
|
|
17984
|
+
let userTurns = 0, aiTurns = 0;
|
|
17985
|
+
for (let i = 0; i < msgs.length; i++) {
|
|
17986
|
+
const m = msgs[i];
|
|
17987
|
+
const prev = msgs[i - 1];
|
|
17988
|
+
const latency_ms = prev ? new Date(m.created_at).getTime() - new Date(prev.created_at).getTime() : null;
|
|
17989
|
+
if (m.role === "user")
|
|
17990
|
+
userTurns++;
|
|
17991
|
+
else
|
|
17992
|
+
aiTurns++;
|
|
17993
|
+
const preview = m.content.replace(/\n/g, " ").slice(0, 120) + (m.content.length > 120 ? "\u2026" : "");
|
|
17994
|
+
timeline.push({ turn: i + 1, role: m.role, timestamp: m.created_at, latency_ms, preview });
|
|
17995
|
+
if (m.role === "assistant") {
|
|
17996
|
+
const mutations = detectMutations(m.content);
|
|
17997
|
+
for (const label of mutations) {
|
|
17998
|
+
mutationsList.push({ turn: i + 1, role: "assistant", snippet: label, label: label.split(":")[0] });
|
|
17999
|
+
}
|
|
18000
|
+
if (PROD_PATTERNS.test(m.content)) {
|
|
18001
|
+
prodWrites.push({ turn: i + 1, snippet: m.content.slice(0, 120) });
|
|
18002
|
+
}
|
|
18003
|
+
}
|
|
18004
|
+
}
|
|
18005
|
+
const firstTs = msgs[0]?.created_at;
|
|
18006
|
+
const lastTs = msgs[msgs.length - 1]?.created_at;
|
|
18007
|
+
const duration_minutes = firstTs && lastTs ? Math.round((new Date(lastTs).getTime() - new Date(firstTs).getTime()) / 6e4) : 0;
|
|
18008
|
+
return {
|
|
18009
|
+
session: {
|
|
18010
|
+
id: session.id,
|
|
18011
|
+
title: session.title,
|
|
18012
|
+
ai_tool: session.ai_tool,
|
|
18013
|
+
started: session.created_at,
|
|
18014
|
+
last_active: session.updated_at
|
|
18015
|
+
},
|
|
18016
|
+
stats: { total_messages: msgs.length, user_turns: userTurns, ai_turns: aiTurns, duration_minutes },
|
|
18017
|
+
secrets: secs.map((s) => ({ type: s.type, masked_value: s.masked_value, detected_at: s.detected_at })),
|
|
18018
|
+
mutations: mutationsList,
|
|
18019
|
+
prod_writes: prodWrites,
|
|
18020
|
+
timeline
|
|
18021
|
+
};
|
|
18022
|
+
}
|
|
18023
|
+
function printSummary(data) {
|
|
18024
|
+
const { session, stats, secrets, mutations, prod_writes, timeline } = data;
|
|
18025
|
+
process.stdout.write(`
|
|
18026
|
+
${BOLD6}Session Summary${RESET6}
|
|
18027
|
+
`);
|
|
18028
|
+
process.stdout.write(`${DIM5}${session.title}${RESET6}
|
|
18029
|
+
`);
|
|
18030
|
+
process.stdout.write(`${DIM5}ID: ${session.id.slice(0, 8)} | Tool: ${session.ai_tool ?? "unknown"} | Started: ${formatTs(session.started)}${RESET6}
|
|
18031
|
+
|
|
18032
|
+
`);
|
|
18033
|
+
process.stdout.write(`${BOLD6}Stats${RESET6}
|
|
18034
|
+
`);
|
|
18035
|
+
process.stdout.write(` Messages ${stats.total_messages} (you: ${stats.user_turns}, ai: ${stats.ai_turns})
|
|
18036
|
+
`);
|
|
18037
|
+
process.stdout.write(` Duration ${stats.duration_minutes}m
|
|
18038
|
+
|
|
18039
|
+
`);
|
|
18040
|
+
process.stdout.write(`${BOLD6}Secrets touched${RESET6} ${secrets.length === 0 ? `${GREEN2}none${RESET6}` : `${RED2}${secrets.length} detection(s)${RESET6}`}
|
|
18041
|
+
`);
|
|
18042
|
+
for (const s of secrets) {
|
|
18043
|
+
process.stdout.write(` ${RED2}[${s.type}]${RESET6} ${DIM5}${s.masked_value}${RESET6}
|
|
18044
|
+
`);
|
|
18045
|
+
}
|
|
18046
|
+
if (secrets.length)
|
|
18047
|
+
process.stdout.write("\n");
|
|
18048
|
+
process.stdout.write(`${BOLD6}Mutations detected${RESET6} ${mutations.length === 0 ? `${DIM5}none${RESET6}` : `${mutations.length}`}
|
|
18049
|
+
`);
|
|
18050
|
+
for (const m of mutations) {
|
|
18051
|
+
process.stdout.write(` ${CYAN5}[turn ${m.turn}]${RESET6} ${m.snippet}
|
|
18052
|
+
`);
|
|
18053
|
+
}
|
|
18054
|
+
if (mutations.length)
|
|
18055
|
+
process.stdout.write("\n");
|
|
18056
|
+
if (prod_writes.length > 0) {
|
|
18057
|
+
process.stdout.write(`${BOLD6}${YELLOW4}\u26A0 Possible prod references${RESET6} ${prod_writes.length}
|
|
18058
|
+
`);
|
|
18059
|
+
for (const p of prod_writes) {
|
|
18060
|
+
process.stdout.write(` ${YELLOW4}[turn ${p.turn}]${RESET6} ${DIM5}${p.snippet}\u2026${RESET6}
|
|
18061
|
+
`);
|
|
18062
|
+
}
|
|
18063
|
+
process.stdout.write("\n");
|
|
18064
|
+
}
|
|
18065
|
+
process.stdout.write(`${BOLD6}Timeline${RESET6}
|
|
18066
|
+
`);
|
|
18067
|
+
for (const t of timeline) {
|
|
18068
|
+
const role = t.role === "user" ? `${BOLD6}you${RESET6}` : `${CYAN5}ai ${RESET6}`;
|
|
18069
|
+
const lat = t.latency_ms !== null ? ` ${DIM5}+${formatLatency(t.latency_ms)}${RESET6}` : "";
|
|
18070
|
+
process.stdout.write(` ${DIM5}[${t.turn.toString().padStart(3)}]${RESET6} ${role}${lat} ${DIM5}${t.preview}${RESET6}
|
|
18071
|
+
`);
|
|
18072
|
+
}
|
|
18073
|
+
process.stdout.write("\n");
|
|
18074
|
+
}
|
|
18075
|
+
async function runSummary(args2) {
|
|
18076
|
+
const [prefix, ...flags] = args2;
|
|
18077
|
+
const jsonMode = flags.includes("--json");
|
|
18078
|
+
if (!prefix) {
|
|
18079
|
+
process.stdout.write("Usage: chron summary <session-id-prefix> [--json]\n");
|
|
18080
|
+
process.exit(1);
|
|
18081
|
+
}
|
|
18082
|
+
const data = await buildSummary(prefix);
|
|
18083
|
+
if (!data) {
|
|
18084
|
+
process.stderr.write(`No session found with prefix: ${prefix}
|
|
18085
|
+
`);
|
|
18086
|
+
process.exit(1);
|
|
18087
|
+
}
|
|
18088
|
+
if (jsonMode) {
|
|
18089
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
18090
|
+
} else {
|
|
18091
|
+
printSummary(data);
|
|
18092
|
+
}
|
|
18093
|
+
}
|
|
18094
|
+
var RESET6, BOLD6, DIM5, CYAN5, GREEN2, YELLOW4, RED2, MUTATION_PATTERNS, PROD_PATTERNS;
|
|
18095
|
+
var init_summary = __esm({
|
|
18096
|
+
"src/cli/summary.ts"() {
|
|
18097
|
+
"use strict";
|
|
18098
|
+
init_drizzle_orm();
|
|
18099
|
+
init_db2();
|
|
18100
|
+
init_schema();
|
|
18101
|
+
RESET6 = "\x1B[0m";
|
|
18102
|
+
BOLD6 = "\x1B[1m";
|
|
18103
|
+
DIM5 = "\x1B[2m";
|
|
18104
|
+
CYAN5 = "\x1B[36m";
|
|
18105
|
+
GREEN2 = "\x1B[32m";
|
|
18106
|
+
YELLOW4 = "\x1B[33m";
|
|
18107
|
+
RED2 = "\x1B[31m";
|
|
18108
|
+
MUTATION_PATTERNS = [
|
|
18109
|
+
{ label: "file write", re: /\b(?:wrote?|created?|saved?|updated?) (?:file |the file )?["']?([^\s"']{1,80})/i },
|
|
18110
|
+
{ label: "git", re: /\bgit (?:commit|push|merge|rebase|reset|checkout)\b/i },
|
|
18111
|
+
{ label: "package", re: /\b(?:npm|pip|yarn|pnpm|cargo|go get) (?:install|add|update|remove)\b/i },
|
|
18112
|
+
{ label: "infra", re: /\b(?:kubectl|terraform|helm|pulumi|ansible|docker) (?:apply|deploy|run|push|create|delete)\b/i },
|
|
18113
|
+
{ label: "db write", re: /\b(?:INSERT|UPDATE|DELETE|DROP|ALTER|CREATE TABLE)\b/i },
|
|
18114
|
+
{ label: "API call", re: /\b(?:POST|PUT|PATCH|DELETE) (?:request|call|to )?\/?(?:https?:\/\/)?[a-z0-9\-._]+\/[^\s]{1,60}/i },
|
|
18115
|
+
{ label: "deploy", re: /\b(?:deploy(?:ed|ing)?|ship(?:ped|ping)?|release(?:d|ing)?)\b/i }
|
|
18116
|
+
];
|
|
18117
|
+
PROD_PATTERNS = /\b(?:production|prod[^a-z]|live\s+server|live\s+env|live\s+database)\b/i;
|
|
18118
|
+
}
|
|
18119
|
+
});
|
|
18120
|
+
|
|
17570
18121
|
// src/cli/index.ts
|
|
17571
18122
|
var [, , command, ...args] = process.argv;
|
|
17572
18123
|
async function main() {
|
|
@@ -17601,6 +18152,11 @@ async function main() {
|
|
|
17601
18152
|
await runConnect2(args);
|
|
17602
18153
|
break;
|
|
17603
18154
|
}
|
|
18155
|
+
case "summary": {
|
|
18156
|
+
const { runSummary: runSummary2 } = await Promise.resolve().then(() => (init_summary(), summary_exports));
|
|
18157
|
+
await runSummary2(args);
|
|
18158
|
+
break;
|
|
18159
|
+
}
|
|
17604
18160
|
default: {
|
|
17605
18161
|
const name = command ? `Unknown command: ${command}
|
|
17606
18162
|
|
|
@@ -17615,13 +18171,16 @@ Commands:
|
|
|
17615
18171
|
secrets List detected secrets across sessions
|
|
17616
18172
|
settings View current configuration
|
|
17617
18173
|
connect Connect to a SIEM integration (crowdstrike, sentinel, splunk)
|
|
18174
|
+
summary Structured summary of a session (timeline, mutations, secrets)
|
|
17618
18175
|
|
|
17619
18176
|
Options (history):
|
|
17620
18177
|
--limit=<n> Max sessions to show (default: 20)
|
|
17621
18178
|
<id-prefix> Show full log for the session with this ID prefix
|
|
17622
18179
|
|
|
17623
18180
|
Options (report):
|
|
17624
|
-
--since=<range>
|
|
18181
|
+
--since=<range> Filter by date: 7d, 30d, or YYYY-MM-DD (default: all time)
|
|
18182
|
+
--format=soc2 Generate SOC 2 HTML evidence package
|
|
18183
|
+
--output=<file> Output file for --format=soc2 (default: soc2-report.html)
|
|
17625
18184
|
|
|
17626
18185
|
Options (export / secrets):
|
|
17627
18186
|
<id-prefix> Scope to a single session
|