chron-mcp 0.1.29 → 0.1.31
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/dist/cli/index.js +771 -6
- package/dist/index.js +26 -2
- 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:
|
|
6165
|
+
var { randomBytes, createHash: createHash7 } = 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 = createHash7("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:
|
|
7232
|
+
var { createHash: createHash7 } = 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 = createHash7("sha1").update(key + GUID).digest("base64");
|
|
7542
7542
|
const headers = [
|
|
7543
7543
|
"HTTP/1.1 101 Switching Protocols",
|
|
7544
7544
|
"Upgrade: websocket",
|
|
@@ -16526,10 +16526,11 @@ var init_libsql = __esm({
|
|
|
16526
16526
|
var schema_exports = {};
|
|
16527
16527
|
__export(schema_exports, {
|
|
16528
16528
|
messages: () => messages,
|
|
16529
|
+
review_findings: () => review_findings,
|
|
16529
16530
|
secrets_detected: () => secrets_detected,
|
|
16530
16531
|
sessions: () => sessions
|
|
16531
16532
|
});
|
|
16532
|
-
var sessions, messages, secrets_detected;
|
|
16533
|
+
var sessions, messages, secrets_detected, review_findings;
|
|
16533
16534
|
var init_schema = __esm({
|
|
16534
16535
|
"src/db/schema.ts"() {
|
|
16535
16536
|
"use strict";
|
|
@@ -16565,6 +16566,16 @@ var init_schema = __esm({
|
|
|
16565
16566
|
masked_value: text("masked_value").notNull(),
|
|
16566
16567
|
detected_at: text("detected_at").notNull()
|
|
16567
16568
|
});
|
|
16569
|
+
review_findings = sqliteTable("review_findings", {
|
|
16570
|
+
id: text("id").primaryKey(),
|
|
16571
|
+
rule_id: text("rule_id").notNull(),
|
|
16572
|
+
session_id: text("session_id").notNull().references(() => sessions.id),
|
|
16573
|
+
status: text("status", { enum: ["open", "accepted", "dismissed", "resolved"] }).notNull(),
|
|
16574
|
+
note: text("note"),
|
|
16575
|
+
reviewed_at: text("reviewed_at"),
|
|
16576
|
+
created_at: text("created_at").notNull(),
|
|
16577
|
+
updated_at: text("updated_at").notNull()
|
|
16578
|
+
});
|
|
16568
16579
|
}
|
|
16569
16580
|
});
|
|
16570
16581
|
|
|
@@ -16668,11 +16679,24 @@ var init_db2 = __esm({
|
|
|
16668
16679
|
detected_at TEXT NOT NULL,
|
|
16669
16680
|
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
|
|
16670
16681
|
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE
|
|
16682
|
+
)`,
|
|
16683
|
+
`CREATE TABLE IF NOT EXISTS review_findings (
|
|
16684
|
+
id TEXT PRIMARY KEY,
|
|
16685
|
+
rule_id TEXT NOT NULL,
|
|
16686
|
+
session_id TEXT NOT NULL,
|
|
16687
|
+
status TEXT NOT NULL CHECK (status IN ('open', 'accepted', 'dismissed', 'resolved')),
|
|
16688
|
+
note TEXT,
|
|
16689
|
+
reviewed_at TEXT,
|
|
16690
|
+
created_at TEXT NOT NULL,
|
|
16691
|
+
updated_at TEXT NOT NULL,
|
|
16692
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
16671
16693
|
)`,
|
|
16672
16694
|
`CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_title ON sessions(title)`,
|
|
16673
16695
|
`CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id)`,
|
|
16674
16696
|
`CREATE INDEX IF NOT EXISTS idx_secrets_detected_session_id ON secrets_detected(session_id)`,
|
|
16675
16697
|
`CREATE INDEX IF NOT EXISTS idx_secrets_detected_message_id ON secrets_detected(message_id)`,
|
|
16698
|
+
`CREATE INDEX IF NOT EXISTS idx_review_findings_session_id ON review_findings(session_id)`,
|
|
16699
|
+
`CREATE INDEX IF NOT EXISTS idx_review_findings_status ON review_findings(status)`,
|
|
16676
16700
|
`CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
|
16677
16701
|
content,
|
|
16678
16702
|
session_id UNINDEXED,
|
|
@@ -17486,7 +17510,7 @@ var require_package = __commonJS({
|
|
|
17486
17510
|
"package.json"(exports2, module2) {
|
|
17487
17511
|
module2.exports = {
|
|
17488
17512
|
name: "chron-mcp",
|
|
17489
|
-
version: "0.1.
|
|
17513
|
+
version: "0.1.31",
|
|
17490
17514
|
mcpName: "io.github.sirinivask/chron",
|
|
17491
17515
|
description: "Audit-grade timestamped logs for every AI conversation",
|
|
17492
17516
|
repository: {
|
|
@@ -20010,6 +20034,732 @@ var init_import = __esm({
|
|
|
20010
20034
|
}
|
|
20011
20035
|
});
|
|
20012
20036
|
|
|
20037
|
+
// src/review/rules.ts
|
|
20038
|
+
var SOC2_RULES, FRAMEWORKS;
|
|
20039
|
+
var init_rules = __esm({
|
|
20040
|
+
"src/review/rules.ts"() {
|
|
20041
|
+
"use strict";
|
|
20042
|
+
SOC2_RULES = [
|
|
20043
|
+
{
|
|
20044
|
+
id: "soc2.cc6_1.ai_access_control_change",
|
|
20045
|
+
framework: "soc2",
|
|
20046
|
+
controls: ["CC6.1"],
|
|
20047
|
+
severity: "high",
|
|
20048
|
+
description: "AI touched access-control-related code",
|
|
20049
|
+
finding: "AI modified code in an access-control-sensitive path.",
|
|
20050
|
+
not_claiming: "This is not evidence of a security failure or SOC 2 non-compliance.",
|
|
20051
|
+
suggested_evidence: [
|
|
20052
|
+
"PR approval with human reviewer",
|
|
20053
|
+
"Change ticket linked to the work",
|
|
20054
|
+
"Manager or security team sign-off"
|
|
20055
|
+
],
|
|
20056
|
+
match: {
|
|
20057
|
+
type: "code_change_path",
|
|
20058
|
+
path_contains: ["auth", "iam", "rbac", "permission", "policy", "acl", "role", "login", "jwt", "oauth", "saml", "sso", "mfa", "access_control"]
|
|
20059
|
+
}
|
|
20060
|
+
},
|
|
20061
|
+
{
|
|
20062
|
+
id: "soc2.cc6_1.cc6_6.secret_detected",
|
|
20063
|
+
framework: "soc2",
|
|
20064
|
+
controls: ["CC6.1", "CC6.6"],
|
|
20065
|
+
severity: "high",
|
|
20066
|
+
description: "AI session contained detected sensitive data or credentials",
|
|
20067
|
+
finding: "Sensitive data or a credential was detected in an AI session.",
|
|
20068
|
+
not_claiming: "This is not evidence of a breach or credential compromise. Chron masks detected values at log time \u2014 no plaintext values are stored.",
|
|
20069
|
+
suggested_evidence: [
|
|
20070
|
+
"Confirm no plaintext credential was committed to version control",
|
|
20071
|
+
"Rotate the credential if it was a live secret",
|
|
20072
|
+
"Document the detection and resolution in the change log"
|
|
20073
|
+
],
|
|
20074
|
+
match: { type: "secret_detected" }
|
|
20075
|
+
},
|
|
20076
|
+
{
|
|
20077
|
+
id: "soc2.cc7_2.cc8_1.ai_infra_change",
|
|
20078
|
+
framework: "soc2",
|
|
20079
|
+
controls: ["CC7.2", "CC8.1"],
|
|
20080
|
+
severity: "high",
|
|
20081
|
+
description: "AI executed or modified deployment or infrastructure files",
|
|
20082
|
+
finding: "AI modified infrastructure or deployment configuration.",
|
|
20083
|
+
not_claiming: "This is not evidence of an unauthorized change.",
|
|
20084
|
+
suggested_evidence: [
|
|
20085
|
+
"Change ticket or approval record",
|
|
20086
|
+
"Pipeline approval gate log",
|
|
20087
|
+
"Human review of the infrastructure diff"
|
|
20088
|
+
],
|
|
20089
|
+
match: {
|
|
20090
|
+
type: "code_change_path",
|
|
20091
|
+
path_contains: [".tf", "terraform", "dockerfile", "docker-compose", "kubernetes", "k8s", "helm", "ansible", ".github/workflow", "cloudformation", "pipeline", "deploy"]
|
|
20092
|
+
}
|
|
20093
|
+
},
|
|
20094
|
+
{
|
|
20095
|
+
id: "soc2.cc7_2.ai_monitoring_change",
|
|
20096
|
+
framework: "soc2",
|
|
20097
|
+
controls: ["CC7.2"],
|
|
20098
|
+
severity: "high",
|
|
20099
|
+
description: "AI changed logging, monitoring, alerting, or security tooling",
|
|
20100
|
+
finding: "AI modified logging, monitoring, or security alerting code or configuration.",
|
|
20101
|
+
not_claiming: "This is not evidence that monitoring was disabled or circumvented.",
|
|
20102
|
+
suggested_evidence: [
|
|
20103
|
+
"Confirm monitoring coverage was not reduced",
|
|
20104
|
+
"Human review of the change",
|
|
20105
|
+
"Change ticket"
|
|
20106
|
+
],
|
|
20107
|
+
match: {
|
|
20108
|
+
type: "code_change_path",
|
|
20109
|
+
path_contains: ["logging", "monitoring", "alerting", "splunk", "datadog", "prometheus", "grafana", "cloudwatch", "sentry", "pagerduty", "audit_log", "audit-log", "logger"]
|
|
20110
|
+
}
|
|
20111
|
+
},
|
|
20112
|
+
{
|
|
20113
|
+
id: "soc2.cc6_1.cc6_7.ai_data_handling_change",
|
|
20114
|
+
framework: "soc2",
|
|
20115
|
+
controls: ["CC6.1", "CC6.7"],
|
|
20116
|
+
severity: "medium",
|
|
20117
|
+
description: "AI changed data retention, deletion, export, or encryption logic",
|
|
20118
|
+
finding: "AI modified data handling, retention, or privacy-related code.",
|
|
20119
|
+
not_claiming: "This is not evidence of a data handling violation.",
|
|
20120
|
+
suggested_evidence: [
|
|
20121
|
+
"Privacy or data officer review",
|
|
20122
|
+
"Change ticket documenting the business reason",
|
|
20123
|
+
"Confirm compliance with data retention policy"
|
|
20124
|
+
],
|
|
20125
|
+
match: {
|
|
20126
|
+
type: "code_change_path",
|
|
20127
|
+
path_contains: ["retention", "encrypt", "decrypt", "gdpr", "privacy", "anonymize", "redact", "purge", "archive", "backup"]
|
|
20128
|
+
}
|
|
20129
|
+
}
|
|
20130
|
+
];
|
|
20131
|
+
FRAMEWORKS = {
|
|
20132
|
+
soc2: SOC2_RULES
|
|
20133
|
+
};
|
|
20134
|
+
}
|
|
20135
|
+
});
|
|
20136
|
+
|
|
20137
|
+
// src/review/workflow.ts
|
|
20138
|
+
function findingId(ruleId, sessionId) {
|
|
20139
|
+
return (0, import_crypto7.createHash)("sha256").update(`${ruleId}:${sessionId}`).digest("hex");
|
|
20140
|
+
}
|
|
20141
|
+
async function hydrateFindingStatuses(db, findings) {
|
|
20142
|
+
if (findings.length === 0)
|
|
20143
|
+
return findings;
|
|
20144
|
+
const client = db.$client;
|
|
20145
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20146
|
+
for (const finding of findings) {
|
|
20147
|
+
await client.execute({
|
|
20148
|
+
sql: `INSERT INTO review_findings (id, rule_id, session_id, status, created_at, updated_at)
|
|
20149
|
+
VALUES (?, ?, ?, 'open', ?, ?)
|
|
20150
|
+
ON CONFLICT(id) DO NOTHING`,
|
|
20151
|
+
args: [finding.id, finding.rule_id, finding.session_id, now, now]
|
|
20152
|
+
});
|
|
20153
|
+
}
|
|
20154
|
+
const rows = await loadFindingStates(client, findings.map((f) => f.id));
|
|
20155
|
+
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
20156
|
+
return findings.map((finding) => {
|
|
20157
|
+
const state = byId.get(finding.id);
|
|
20158
|
+
return {
|
|
20159
|
+
...finding,
|
|
20160
|
+
status: state?.status ?? "open",
|
|
20161
|
+
note: state?.note ?? null,
|
|
20162
|
+
reviewed_at: state?.reviewed_at ?? null
|
|
20163
|
+
};
|
|
20164
|
+
});
|
|
20165
|
+
}
|
|
20166
|
+
async function updateFindingStatus(db, idOrPrefix, status, note) {
|
|
20167
|
+
const client = db.$client;
|
|
20168
|
+
const id = await resolveFindingId(client, idOrPrefix);
|
|
20169
|
+
const reviewedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
20170
|
+
await client.execute({
|
|
20171
|
+
sql: `UPDATE review_findings
|
|
20172
|
+
SET status = ?, note = ?, reviewed_at = ?, updated_at = ?
|
|
20173
|
+
WHERE id = ?`,
|
|
20174
|
+
args: [status, note ?? null, reviewedAt, reviewedAt, id]
|
|
20175
|
+
});
|
|
20176
|
+
const rows = await loadFindingStates(client, [id]);
|
|
20177
|
+
return rows[0];
|
|
20178
|
+
}
|
|
20179
|
+
async function resolveFindingId(client, idOrPrefix) {
|
|
20180
|
+
const prefix = idOrPrefix.trim();
|
|
20181
|
+
if (!prefix)
|
|
20182
|
+
throw new Error("Finding id is required");
|
|
20183
|
+
const result = await client.execute({
|
|
20184
|
+
sql: `SELECT id FROM review_findings WHERE id LIKE ? ORDER BY id LIMIT 2`,
|
|
20185
|
+
args: [`${prefix}%`]
|
|
20186
|
+
});
|
|
20187
|
+
if (result.rows.length === 0) {
|
|
20188
|
+
throw new Error(`Finding not found: ${prefix}. Run "chron review --framework=soc2" first to register current findings.`);
|
|
20189
|
+
}
|
|
20190
|
+
if (result.rows.length > 1) {
|
|
20191
|
+
throw new Error(`Finding id prefix is ambiguous: ${prefix}. Use more characters.`);
|
|
20192
|
+
}
|
|
20193
|
+
return String(result.rows[0].id);
|
|
20194
|
+
}
|
|
20195
|
+
async function loadFindingStates(client, ids) {
|
|
20196
|
+
if (ids.length === 0)
|
|
20197
|
+
return [];
|
|
20198
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
20199
|
+
const result = await client.execute({
|
|
20200
|
+
sql: `SELECT id, rule_id, session_id, status, note, reviewed_at
|
|
20201
|
+
FROM review_findings
|
|
20202
|
+
WHERE id IN (${placeholders})`,
|
|
20203
|
+
args: ids
|
|
20204
|
+
});
|
|
20205
|
+
return result.rows.map((row) => ({
|
|
20206
|
+
id: String(row.id),
|
|
20207
|
+
rule_id: String(row.rule_id),
|
|
20208
|
+
session_id: String(row.session_id),
|
|
20209
|
+
status: String(row.status),
|
|
20210
|
+
note: row.note == null ? null : String(row.note),
|
|
20211
|
+
reviewed_at: row.reviewed_at == null ? null : String(row.reviewed_at)
|
|
20212
|
+
}));
|
|
20213
|
+
}
|
|
20214
|
+
var import_crypto7;
|
|
20215
|
+
var init_workflow = __esm({
|
|
20216
|
+
"src/review/workflow.ts"() {
|
|
20217
|
+
"use strict";
|
|
20218
|
+
import_crypto7 = require("crypto");
|
|
20219
|
+
}
|
|
20220
|
+
});
|
|
20221
|
+
|
|
20222
|
+
// src/review/engine.ts
|
|
20223
|
+
async function runReview(db, rules, since) {
|
|
20224
|
+
const client = db.$client;
|
|
20225
|
+
const map = /* @__PURE__ */ new Map();
|
|
20226
|
+
for (const rule of rules) {
|
|
20227
|
+
if (rule.match.type === "code_change_path") {
|
|
20228
|
+
await matchCodeChanges(client, rule, map, since);
|
|
20229
|
+
} else if (rule.match.type === "secret_detected") {
|
|
20230
|
+
await matchSecrets(client, rule, map, since);
|
|
20231
|
+
}
|
|
20232
|
+
}
|
|
20233
|
+
const findings = Array.from(map.values());
|
|
20234
|
+
findings.sort(
|
|
20235
|
+
(a, b) => (SEVERITY_ORDER[a.severity] ?? 99) - (SEVERITY_ORDER[b.severity] ?? 99) || a.last_occurred_at.localeCompare(b.last_occurred_at)
|
|
20236
|
+
);
|
|
20237
|
+
return findings;
|
|
20238
|
+
}
|
|
20239
|
+
function higherSeverity(a, b) {
|
|
20240
|
+
return (SEVERITY_ORDER[a] ?? 99) <= (SEVERITY_ORDER[b] ?? 99) ? a : b;
|
|
20241
|
+
}
|
|
20242
|
+
function suggestedEvidenceForSecret(type, rule) {
|
|
20243
|
+
const severity = SECRET_SEVERITY[type] ?? rule.severity;
|
|
20244
|
+
if (severity === "medium") {
|
|
20245
|
+
return [
|
|
20246
|
+
"Confirm the personal or contact data was necessary for the AI session",
|
|
20247
|
+
"Verify no unnecessary personal data was committed to version control",
|
|
20248
|
+
"Document handling or remediation if the data was not required"
|
|
20249
|
+
];
|
|
20250
|
+
}
|
|
20251
|
+
if (severity === "low") {
|
|
20252
|
+
return [
|
|
20253
|
+
"Review whether the detected value contains secret material",
|
|
20254
|
+
"Confirm no plaintext credential was committed to version control",
|
|
20255
|
+
"Document why the value is acceptable or remediate it"
|
|
20256
|
+
];
|
|
20257
|
+
}
|
|
20258
|
+
return rule.suggested_evidence;
|
|
20259
|
+
}
|
|
20260
|
+
async function matchCodeChanges(client, rule, map, since) {
|
|
20261
|
+
if (rule.match.type !== "code_change_path")
|
|
20262
|
+
return;
|
|
20263
|
+
const keywords = rule.match.path_contains;
|
|
20264
|
+
const result = await client.execute(
|
|
20265
|
+
since ? {
|
|
20266
|
+
sql: `SELECT m.id, m.session_id, m.content, m.created_at, s.title, s.ai_tool
|
|
20267
|
+
FROM messages m JOIN sessions s ON s.id = m.session_id
|
|
20268
|
+
WHERE m.event_type = 'code_change' AND m.created_at >= ?
|
|
20269
|
+
ORDER BY m.created_at`,
|
|
20270
|
+
args: [since]
|
|
20271
|
+
} : `SELECT m.id, m.session_id, m.content, m.created_at, s.title, s.ai_tool
|
|
20272
|
+
FROM messages m JOIN sessions s ON s.id = m.session_id
|
|
20273
|
+
WHERE m.event_type = 'code_change'
|
|
20274
|
+
ORDER BY m.created_at`
|
|
20275
|
+
);
|
|
20276
|
+
for (const row of result.rows) {
|
|
20277
|
+
const content = String(row.content ?? "");
|
|
20278
|
+
let filePath = "";
|
|
20279
|
+
try {
|
|
20280
|
+
const parsed = JSON.parse(content);
|
|
20281
|
+
filePath = (parsed.file_path ?? parsed.path ?? "").toLowerCase();
|
|
20282
|
+
} catch {
|
|
20283
|
+
filePath = content.toLowerCase();
|
|
20284
|
+
}
|
|
20285
|
+
if (!keywords.some((kw) => filePath.includes(kw.toLowerCase())))
|
|
20286
|
+
continue;
|
|
20287
|
+
const sessionId = String(row.session_id ?? "");
|
|
20288
|
+
const mapKey = `${rule.id}::${sessionId}`;
|
|
20289
|
+
const occurredAt = String(row.created_at ?? "");
|
|
20290
|
+
const evidenceItem = `code_change: ${filePath || "unknown path"}`;
|
|
20291
|
+
if (!map.has(mapKey)) {
|
|
20292
|
+
map.set(mapKey, {
|
|
20293
|
+
id: findingId(rule.id, sessionId),
|
|
20294
|
+
rule_id: rule.id,
|
|
20295
|
+
framework: rule.framework,
|
|
20296
|
+
controls: rule.controls,
|
|
20297
|
+
severity: rule.severity,
|
|
20298
|
+
status: "open",
|
|
20299
|
+
note: null,
|
|
20300
|
+
reviewed_at: null,
|
|
20301
|
+
session_id: sessionId,
|
|
20302
|
+
session_prefix: sessionId.slice(0, 8),
|
|
20303
|
+
session_title: String(row.title ?? ""),
|
|
20304
|
+
ai_tool: row.ai_tool != null ? String(row.ai_tool) : null,
|
|
20305
|
+
evidence_items: [],
|
|
20306
|
+
first_occurred_at: occurredAt,
|
|
20307
|
+
last_occurred_at: occurredAt,
|
|
20308
|
+
finding: rule.finding,
|
|
20309
|
+
not_claiming: rule.not_claiming,
|
|
20310
|
+
suggested_evidence: rule.suggested_evidence
|
|
20311
|
+
});
|
|
20312
|
+
}
|
|
20313
|
+
const finding = map.get(mapKey);
|
|
20314
|
+
if (!finding.evidence_items.includes(evidenceItem)) {
|
|
20315
|
+
finding.evidence_items.push(evidenceItem);
|
|
20316
|
+
}
|
|
20317
|
+
if (occurredAt > finding.last_occurred_at)
|
|
20318
|
+
finding.last_occurred_at = occurredAt;
|
|
20319
|
+
}
|
|
20320
|
+
}
|
|
20321
|
+
async function matchSecrets(client, rule, map, since) {
|
|
20322
|
+
const result = await client.execute(
|
|
20323
|
+
since ? {
|
|
20324
|
+
sql: `SELECT sd.session_id, sd.type, sd.masked_value, sd.detected_at, s.title, s.ai_tool
|
|
20325
|
+
FROM secrets_detected sd JOIN sessions s ON s.id = sd.session_id
|
|
20326
|
+
WHERE sd.detected_at >= ?
|
|
20327
|
+
ORDER BY sd.detected_at`,
|
|
20328
|
+
args: [since]
|
|
20329
|
+
} : `SELECT sd.session_id, sd.type, sd.masked_value, sd.detected_at, s.title, s.ai_tool
|
|
20330
|
+
FROM secrets_detected sd JOIN sessions s ON s.id = sd.session_id
|
|
20331
|
+
ORDER BY sd.detected_at`
|
|
20332
|
+
);
|
|
20333
|
+
for (const row of result.rows) {
|
|
20334
|
+
const sessionId = String(row.session_id ?? "");
|
|
20335
|
+
const mapKey = `${rule.id}::${sessionId}`;
|
|
20336
|
+
const occurredAt = String(row.detected_at ?? "");
|
|
20337
|
+
const type = String(row.type ?? "");
|
|
20338
|
+
const severity = SECRET_SEVERITY[type] ?? rule.severity;
|
|
20339
|
+
const suggestedEvidence = suggestedEvidenceForSecret(type, rule);
|
|
20340
|
+
const evidenceItem = `secret_detected: ${type} (${row.masked_value})`;
|
|
20341
|
+
if (!map.has(mapKey)) {
|
|
20342
|
+
map.set(mapKey, {
|
|
20343
|
+
id: findingId(rule.id, sessionId),
|
|
20344
|
+
rule_id: rule.id,
|
|
20345
|
+
framework: rule.framework,
|
|
20346
|
+
controls: rule.controls,
|
|
20347
|
+
severity,
|
|
20348
|
+
status: "open",
|
|
20349
|
+
note: null,
|
|
20350
|
+
reviewed_at: null,
|
|
20351
|
+
session_id: sessionId,
|
|
20352
|
+
session_prefix: sessionId.slice(0, 8),
|
|
20353
|
+
session_title: String(row.title ?? ""),
|
|
20354
|
+
ai_tool: row.ai_tool != null ? String(row.ai_tool) : null,
|
|
20355
|
+
evidence_items: [],
|
|
20356
|
+
first_occurred_at: occurredAt,
|
|
20357
|
+
last_occurred_at: occurredAt,
|
|
20358
|
+
finding: rule.finding,
|
|
20359
|
+
not_claiming: rule.not_claiming,
|
|
20360
|
+
suggested_evidence: suggestedEvidence
|
|
20361
|
+
});
|
|
20362
|
+
}
|
|
20363
|
+
const finding = map.get(mapKey);
|
|
20364
|
+
const previousSeverity = finding.severity;
|
|
20365
|
+
finding.severity = higherSeverity(finding.severity, severity);
|
|
20366
|
+
if (finding.severity !== previousSeverity || finding.suggested_evidence.length === 0) {
|
|
20367
|
+
finding.suggested_evidence = suggestedEvidence;
|
|
20368
|
+
}
|
|
20369
|
+
if (!finding.evidence_items.includes(evidenceItem)) {
|
|
20370
|
+
finding.evidence_items.push(evidenceItem);
|
|
20371
|
+
}
|
|
20372
|
+
if (occurredAt > finding.last_occurred_at)
|
|
20373
|
+
finding.last_occurred_at = occurredAt;
|
|
20374
|
+
}
|
|
20375
|
+
}
|
|
20376
|
+
var SEVERITY_ORDER, SECRET_SEVERITY;
|
|
20377
|
+
var init_engine = __esm({
|
|
20378
|
+
"src/review/engine.ts"() {
|
|
20379
|
+
"use strict";
|
|
20380
|
+
init_workflow();
|
|
20381
|
+
SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
20382
|
+
SECRET_SEVERITY = {
|
|
20383
|
+
private_key: "critical",
|
|
20384
|
+
credit_card: "critical",
|
|
20385
|
+
ssn: "critical",
|
|
20386
|
+
iban: "critical",
|
|
20387
|
+
aws_access_key: "high",
|
|
20388
|
+
anthropic_api_key: "high",
|
|
20389
|
+
openai_api_key: "high",
|
|
20390
|
+
google_api_key: "high",
|
|
20391
|
+
github_token: "high",
|
|
20392
|
+
slack_token: "high",
|
|
20393
|
+
stripe_key: "high",
|
|
20394
|
+
sendgrid_key: "high",
|
|
20395
|
+
huggingface_token: "high",
|
|
20396
|
+
jwt: "high",
|
|
20397
|
+
url_credentials: "high",
|
|
20398
|
+
password: "high",
|
|
20399
|
+
credential_pair: "high",
|
|
20400
|
+
passport: "high",
|
|
20401
|
+
dob: "high",
|
|
20402
|
+
email: "medium",
|
|
20403
|
+
phone_us: "medium",
|
|
20404
|
+
phone_e164: "medium",
|
|
20405
|
+
internal_ip: "low",
|
|
20406
|
+
env_value: "low"
|
|
20407
|
+
};
|
|
20408
|
+
}
|
|
20409
|
+
});
|
|
20410
|
+
|
|
20411
|
+
// src/cli/review.ts
|
|
20412
|
+
var review_exports = {};
|
|
20413
|
+
__export(review_exports, {
|
|
20414
|
+
runReview: () => runReview2
|
|
20415
|
+
});
|
|
20416
|
+
function printFindings(findings, framework, sessionCount, showAll) {
|
|
20417
|
+
const controlsHit = new Set(findings.flatMap((f) => f.controls));
|
|
20418
|
+
process.stdout.write(
|
|
20419
|
+
`
|
|
20420
|
+
${BOLD10}Chron Review${RESET10} ${CYAN8}${framework.toUpperCase()}${RESET10} ${DIM9}${sessionCount} session(s) reviewed${showAll ? " \xB7 all statuses" : " \xB7 open findings"}${RESET10}
|
|
20421
|
+
|
|
20422
|
+
`
|
|
20423
|
+
);
|
|
20424
|
+
if (findings.length === 0) {
|
|
20425
|
+
process.stdout.write(
|
|
20426
|
+
showAll ? `${DIM9}No findings. No sessions matched any control review criteria.${RESET10}
|
|
20427
|
+
|
|
20428
|
+
` : `${DIM9}No open findings. Use --all to include accepted, dismissed, and resolved findings.${RESET10}
|
|
20429
|
+
|
|
20430
|
+
`
|
|
20431
|
+
);
|
|
20432
|
+
return;
|
|
20433
|
+
}
|
|
20434
|
+
process.stdout.write(
|
|
20435
|
+
`${BOLD10}${findings.length}${RESET10} finding${findings.length === 1 ? "" : "s"} across ${BOLD10}${controlsHit.size}${RESET10} control${controlsHit.size === 1 ? "" : "s"}
|
|
20436
|
+
|
|
20437
|
+
`
|
|
20438
|
+
);
|
|
20439
|
+
process.stdout.write(
|
|
20440
|
+
`${DIM9}This report identifies AI-session evidence that may require control-owner review.
|
|
20441
|
+
It is not a certification of compliance or evidence of any violation.${RESET10}
|
|
20442
|
+
|
|
20443
|
+
`
|
|
20444
|
+
);
|
|
20445
|
+
for (const f of findings) {
|
|
20446
|
+
const sevColor = SEV_COLOR[f.severity] ?? DIM9;
|
|
20447
|
+
const statusColor = STATUS_COLOR[f.status] ?? DIM9;
|
|
20448
|
+
const controls = f.controls.join(", ");
|
|
20449
|
+
const sev = f.severity.toUpperCase().padEnd(8);
|
|
20450
|
+
const status = f.status.toUpperCase().padEnd(9);
|
|
20451
|
+
process.stdout.write(
|
|
20452
|
+
` ${sevColor}${BOLD10}${sev}${RESET10} ${statusColor}${status}${RESET10} ${MAGENTA2}${controls.padEnd(16)}${RESET10} ${CYAN8}${f.session_prefix}${RESET10} ${f.session_title}
|
|
20453
|
+
`
|
|
20454
|
+
);
|
|
20455
|
+
process.stdout.write(` ${DIM9}id: ${f.id.slice(0, 16)}${RESET10}
|
|
20456
|
+
`);
|
|
20457
|
+
process.stdout.write(` ${f.finding}
|
|
20458
|
+
`);
|
|
20459
|
+
for (const item of f.evidence_items.slice(0, 5)) {
|
|
20460
|
+
process.stdout.write(` ${DIM9}\u2022 ${item}${RESET10}
|
|
20461
|
+
`);
|
|
20462
|
+
}
|
|
20463
|
+
if (f.evidence_items.length > 5) {
|
|
20464
|
+
process.stdout.write(` ${DIM9} \u2026 ${f.evidence_items.length - 5} more event(s)${RESET10}
|
|
20465
|
+
`);
|
|
20466
|
+
}
|
|
20467
|
+
process.stdout.write(` ${DIM9}Suggested: ${f.suggested_evidence[0]}${RESET10}
|
|
20468
|
+
`);
|
|
20469
|
+
if (f.note)
|
|
20470
|
+
process.stdout.write(` ${DIM9}Note: ${f.note}${RESET10}
|
|
20471
|
+
`);
|
|
20472
|
+
process.stdout.write("\n");
|
|
20473
|
+
}
|
|
20474
|
+
}
|
|
20475
|
+
function esc2(s) {
|
|
20476
|
+
return (s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
20477
|
+
}
|
|
20478
|
+
function badgeHtml(severity) {
|
|
20479
|
+
return `<span class="badge badge-${esc2(severity)}">${esc2(severity.toUpperCase())}</span>`;
|
|
20480
|
+
}
|
|
20481
|
+
function statusBadgeHtml(status) {
|
|
20482
|
+
return `<span class="status-badge status-${esc2(status)}">${esc2(status.toUpperCase())}</span>`;
|
|
20483
|
+
}
|
|
20484
|
+
function buildReviewHtml(params) {
|
|
20485
|
+
const { framework, host, generatedAt, sessionCount, findings } = params;
|
|
20486
|
+
const controlsHit = new Set(findings.flatMap((f) => f.controls));
|
|
20487
|
+
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
20488
|
+
for (const f of findings)
|
|
20489
|
+
bySeverity[f.severity] = (bySeverity[f.severity] ?? 0) + 1;
|
|
20490
|
+
const findingCards = findings.length === 0 ? '<p style="color:#6b7280">No findings matched this report scope.</p>' : findings.map((f) => `
|
|
20491
|
+
<div class="finding-card">
|
|
20492
|
+
<div class="finding-header">
|
|
20493
|
+
${badgeHtml(f.severity)}
|
|
20494
|
+
${statusBadgeHtml(f.status)}
|
|
20495
|
+
<span class="finding-controls">${esc2(f.controls.join(", "))}</span>
|
|
20496
|
+
<span style="flex:1"></span>
|
|
20497
|
+
<span class="finding-session">${esc2(f.session_prefix)}</span>
|
|
20498
|
+
<span style="font-size:12px;color:#374151">${esc2(f.session_title)}</span>
|
|
20499
|
+
</div>
|
|
20500
|
+
<div class="finding-body">
|
|
20501
|
+
<div style="font-family:monospace;font-size:11px;color:#6b7280;margin-bottom:5px">Finding ID: ${esc2(f.id)}</div>
|
|
20502
|
+
<div class="finding-text">${esc2(f.finding)}</div>
|
|
20503
|
+
<div class="finding-disclaimer">${esc2(f.not_claiming)}</div>
|
|
20504
|
+
${f.note ? `<div style="font-size:11px;color:#374151;margin-bottom:8px"><strong>Reviewer note:</strong> ${esc2(f.note)}</div>` : ""}
|
|
20505
|
+
${f.reviewed_at ? `<div style="font-size:11px;color:#6b7280;margin-bottom:8px">Reviewed: ${esc2(f.reviewed_at)}</div>` : ""}
|
|
20506
|
+
<strong style="font-size:11px;color:#374151">Evidence in session:</strong>
|
|
20507
|
+
<ul class="evidence-list">
|
|
20508
|
+
${f.evidence_items.map((e) => `<li>${esc2(e)}</li>`).join("")}
|
|
20509
|
+
</ul>
|
|
20510
|
+
<strong style="font-size:11px;color:#374151">Suggested review actions:</strong>
|
|
20511
|
+
<ul class="suggested-list">
|
|
20512
|
+
${f.suggested_evidence.map((s) => `<li>${esc2(s)}</li>`).join("")}
|
|
20513
|
+
</ul>
|
|
20514
|
+
</div>
|
|
20515
|
+
</div>`).join("");
|
|
20516
|
+
return `<!DOCTYPE html>
|
|
20517
|
+
<html lang="en">
|
|
20518
|
+
<head>
|
|
20519
|
+
<meta charset="UTF-8">
|
|
20520
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
20521
|
+
<title>Chron Review \u2014 ${esc2(framework.toUpperCase())} Controls</title>
|
|
20522
|
+
<style>${REPORT_CSS}</style>
|
|
20523
|
+
</head>
|
|
20524
|
+
<body>
|
|
20525
|
+
<div class="page">
|
|
20526
|
+
|
|
20527
|
+
<div style="min-height:50vh;display:flex;flex-direction:column;justify-content:center">
|
|
20528
|
+
<div class="cover-badge">${esc2(framework.toUpperCase())} Control Review</div>
|
|
20529
|
+
<h1>Chron Review Report</h1>
|
|
20530
|
+
<p class="meta">Generated by <strong>chron review</strong> | Host: <strong>${esc2(host)}</strong></p>
|
|
20531
|
+
<table style="width:auto;margin-bottom:0">
|
|
20532
|
+
<tr><th>Framework</th><td>${esc2(framework.toUpperCase())} Trust Services Criteria</td></tr>
|
|
20533
|
+
<tr><th>Generated</th><td>${esc2(generatedAt)}</td></tr>
|
|
20534
|
+
<tr><th>Sessions Reviewed</th><td>${sessionCount}</td></tr>
|
|
20535
|
+
<tr><th>Findings</th><td>${findings.length}</td></tr>
|
|
20536
|
+
<tr><th>Controls Flagged</th><td>${controlsHit.size > 0 ? Array.from(controlsHit).sort().join(", ") : "None"}</td></tr>
|
|
20537
|
+
</table>
|
|
20538
|
+
</div>
|
|
20539
|
+
|
|
20540
|
+
<div class="disclaimer">
|
|
20541
|
+
<strong>Important:</strong> This report identifies AI-session evidence that may require control-owner review.
|
|
20542
|
+
It is not a certification of compliance, a SOC 2 opinion, or evidence of any control failure.
|
|
20543
|
+
Each finding is a potential review item \u2014 not a violation. A licensed CPA conducting a SOC 2 examination
|
|
20544
|
+
must evaluate whether your controls were suitably designed and operated effectively over the audit period.
|
|
20545
|
+
</div>
|
|
20546
|
+
|
|
20547
|
+
<h2>Summary</h2>
|
|
20548
|
+
<div class="stat-grid">
|
|
20549
|
+
<div class="stat"><div class="stat-value">${sessionCount}</div><div class="stat-label">Sessions Reviewed</div></div>
|
|
20550
|
+
<div class="stat"><div class="stat-value">${findings.length}</div><div class="stat-label">Findings</div></div>
|
|
20551
|
+
<div class="stat"><div class="stat-value">${controlsHit.size}</div><div class="stat-label">Controls Flagged</div></div>
|
|
20552
|
+
</div>
|
|
20553
|
+
${bySeverity.critical > 0 ? `<p><span class="badge badge-critical">CRITICAL</span> ${bySeverity.critical} finding(s) require immediate attention.</p>` : ""}
|
|
20554
|
+
|
|
20555
|
+
<h2>Findings</h2>
|
|
20556
|
+
${findingCards}
|
|
20557
|
+
|
|
20558
|
+
<h2>Methodology</h2>
|
|
20559
|
+
<div class="appendix">
|
|
20560
|
+
<h3>What this report does</h3>
|
|
20561
|
+
<p>Chron Review evaluates AI session logs against a deterministic rule pack aligned to the AICPA Trust Services Criteria.
|
|
20562
|
+
Rules match on structured event types (<code>code_change</code>, <code>secret_detected</code>) recorded by the Chron MCP server
|
|
20563
|
+
during live AI conversations.</p>
|
|
20564
|
+
|
|
20565
|
+
<h3>Rule pack: ${esc2(framework.toUpperCase())} (v1)</h3>
|
|
20566
|
+
<ul>
|
|
20567
|
+
<li><strong>CC6.1</strong> \u2014 AI touched access-control code (auth, IAM, RBAC, permissions, login, JWT, OAuth, SAML, SSO)</li>
|
|
20568
|
+
<li><strong>CC6.1 / CC6.6</strong> \u2014 Sensitive credential detected in AI session (masked at log time, no plaintext stored)</li>
|
|
20569
|
+
<li><strong>CC7.2 / CC8.1</strong> \u2014 AI modified infrastructure or deployment configuration (Terraform, Docker, Kubernetes, pipelines)</li>
|
|
20570
|
+
<li><strong>CC7.2</strong> \u2014 AI modified logging, monitoring, or alerting code</li>
|
|
20571
|
+
<li><strong>CC6.1 / CC6.7</strong> \u2014 AI modified data retention, deletion, export, or encryption logic</li>
|
|
20572
|
+
</ul>
|
|
20573
|
+
|
|
20574
|
+
<h3>What this report does not do</h3>
|
|
20575
|
+
<ul>
|
|
20576
|
+
<li>Does not evaluate whether your controls were suitably designed or operated effectively (that is the auditor's role).</li>
|
|
20577
|
+
<li>Does not access code repositories, CI/CD systems, identity providers, or any environment outside the Chron audit database.</li>
|
|
20578
|
+
<li>Does not make any compliance determination. All findings use the language "potential review item."</li>
|
|
20579
|
+
<li>Does not cover CC1\u2013CC5 (governance, communication, risk assessment) \u2014 these require policy documents, board records, and cannot be evaluated from session logs.</li>
|
|
20580
|
+
</ul>
|
|
20581
|
+
|
|
20582
|
+
<p style="margin-top:12px;font-size:11px;color:#9ca3af">
|
|
20583
|
+
Generated by chron review | ${esc2(generatedAt)} | Host: ${esc2(host)}
|
|
20584
|
+
</p>
|
|
20585
|
+
</div>
|
|
20586
|
+
|
|
20587
|
+
</div>
|
|
20588
|
+
</body>
|
|
20589
|
+
</html>`;
|
|
20590
|
+
}
|
|
20591
|
+
async function countSessions(db, since) {
|
|
20592
|
+
const client = db.$client;
|
|
20593
|
+
const result = await client.execute(
|
|
20594
|
+
since ? {
|
|
20595
|
+
sql: `SELECT COUNT(*) AS n FROM sessions WHERE updated_at >= ?`,
|
|
20596
|
+
args: [since]
|
|
20597
|
+
} : "SELECT COUNT(*) AS n FROM sessions"
|
|
20598
|
+
);
|
|
20599
|
+
return Number(result.rows[0].n ?? 0);
|
|
20600
|
+
}
|
|
20601
|
+
async function runReview2(args2) {
|
|
20602
|
+
const action = args2[0];
|
|
20603
|
+
if (action === "accept" || action === "dismiss" || action === "resolve") {
|
|
20604
|
+
await runReviewAction(action, args2.slice(1));
|
|
20605
|
+
return;
|
|
20606
|
+
}
|
|
20607
|
+
const frameworkArg = args2.find((a) => a.startsWith("--framework="))?.slice("--framework=".length)?.toLowerCase();
|
|
20608
|
+
const outputArg = args2.find((a) => a.startsWith("--output="))?.slice("--output=".length);
|
|
20609
|
+
const sinceArg = args2.find((a) => a.startsWith("--since="))?.slice("--since=".length);
|
|
20610
|
+
const showAll = args2.includes("--all");
|
|
20611
|
+
if (!frameworkArg) {
|
|
20612
|
+
process.stderr.write("Usage: chron review --framework=soc2 [--since=<range>] [--all] [--output=<file>]\n");
|
|
20613
|
+
process.exit(1);
|
|
20614
|
+
}
|
|
20615
|
+
const rules = FRAMEWORKS[frameworkArg];
|
|
20616
|
+
if (!rules) {
|
|
20617
|
+
process.stderr.write(`Unknown framework: "${frameworkArg}". Available: ${Object.keys(FRAMEWORKS).join(", ")}
|
|
20618
|
+
`);
|
|
20619
|
+
process.exit(1);
|
|
20620
|
+
}
|
|
20621
|
+
let since;
|
|
20622
|
+
if (sinceArg) {
|
|
20623
|
+
try {
|
|
20624
|
+
since = parseSince(sinceArg);
|
|
20625
|
+
} catch (err) {
|
|
20626
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
20627
|
+
`);
|
|
20628
|
+
process.exit(1);
|
|
20629
|
+
}
|
|
20630
|
+
}
|
|
20631
|
+
const db = await initDb();
|
|
20632
|
+
const [rawFindings, sessionCount] = await Promise.all([
|
|
20633
|
+
runReview(db, rules, since),
|
|
20634
|
+
countSessions(db, since)
|
|
20635
|
+
]);
|
|
20636
|
+
const allFindings = await hydrateFindingStatuses(db, rawFindings);
|
|
20637
|
+
const findings = showAll ? allFindings : allFindings.filter((f) => f.status === "open");
|
|
20638
|
+
printFindings(findings, frameworkArg, sessionCount, showAll);
|
|
20639
|
+
if (outputArg) {
|
|
20640
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19) + " UTC";
|
|
20641
|
+
const html = buildReviewHtml({
|
|
20642
|
+
framework: frameworkArg,
|
|
20643
|
+
host: (0, import_os11.hostname)(),
|
|
20644
|
+
generatedAt,
|
|
20645
|
+
sessionCount,
|
|
20646
|
+
findings
|
|
20647
|
+
});
|
|
20648
|
+
(0, import_fs12.writeFileSync)(outputArg, html, "utf8");
|
|
20649
|
+
process.stdout.write(`Review report written to ${outputArg}
|
|
20650
|
+
`);
|
|
20651
|
+
}
|
|
20652
|
+
}
|
|
20653
|
+
async function runReviewAction(action, args2) {
|
|
20654
|
+
const id = args2.find((a) => !a.startsWith("--"));
|
|
20655
|
+
const note = args2.find((a) => a.startsWith("--note="))?.slice("--note=".length);
|
|
20656
|
+
if (!id) {
|
|
20657
|
+
process.stderr.write(`Usage: chron review ${action} <finding-id> [--note=<text>]
|
|
20658
|
+
`);
|
|
20659
|
+
process.exit(1);
|
|
20660
|
+
}
|
|
20661
|
+
const statusByAction = {
|
|
20662
|
+
accept: "accepted",
|
|
20663
|
+
dismiss: "dismissed",
|
|
20664
|
+
resolve: "resolved"
|
|
20665
|
+
};
|
|
20666
|
+
const status = statusByAction[action];
|
|
20667
|
+
const db = await initDb();
|
|
20668
|
+
try {
|
|
20669
|
+
const row = await updateFindingStatus(db, id, status, note);
|
|
20670
|
+
process.stdout.write(
|
|
20671
|
+
`Finding ${row.id.slice(0, 16)} ${status}${note ? ` with note: ${note}` : ""}
|
|
20672
|
+
`
|
|
20673
|
+
);
|
|
20674
|
+
} catch (err) {
|
|
20675
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
20676
|
+
`);
|
|
20677
|
+
process.exit(1);
|
|
20678
|
+
}
|
|
20679
|
+
}
|
|
20680
|
+
var import_fs12, import_os11, RESET10, BOLD10, DIM9, RED5, YELLOW8, CYAN8, MAGENTA2, SEV_COLOR, STATUS_COLOR, REPORT_CSS;
|
|
20681
|
+
var init_review = __esm({
|
|
20682
|
+
"src/cli/review.ts"() {
|
|
20683
|
+
"use strict";
|
|
20684
|
+
import_fs12 = require("fs");
|
|
20685
|
+
import_os11 = require("os");
|
|
20686
|
+
init_db2();
|
|
20687
|
+
init_report();
|
|
20688
|
+
init_rules();
|
|
20689
|
+
init_engine();
|
|
20690
|
+
init_workflow();
|
|
20691
|
+
RESET10 = "\x1B[0m";
|
|
20692
|
+
BOLD10 = "\x1B[1m";
|
|
20693
|
+
DIM9 = "\x1B[2m";
|
|
20694
|
+
RED5 = "\x1B[31m";
|
|
20695
|
+
YELLOW8 = "\x1B[33m";
|
|
20696
|
+
CYAN8 = "\x1B[36m";
|
|
20697
|
+
MAGENTA2 = "\x1B[35m";
|
|
20698
|
+
SEV_COLOR = {
|
|
20699
|
+
critical: RED5,
|
|
20700
|
+
high: YELLOW8,
|
|
20701
|
+
medium: CYAN8,
|
|
20702
|
+
low: DIM9
|
|
20703
|
+
};
|
|
20704
|
+
STATUS_COLOR = {
|
|
20705
|
+
open: YELLOW8,
|
|
20706
|
+
accepted: CYAN8,
|
|
20707
|
+
dismissed: DIM9,
|
|
20708
|
+
resolved: CYAN8
|
|
20709
|
+
};
|
|
20710
|
+
REPORT_CSS = `
|
|
20711
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
20712
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 13px; color: #111; background: #fff; line-height: 1.5; }
|
|
20713
|
+
.page { max-width: 900px; margin: 0 auto; padding: 40px 48px; }
|
|
20714
|
+
h1 { font-size: 28px; font-weight: 700; margin-bottom: 4px; }
|
|
20715
|
+
h2 { font-size: 17px; font-weight: 700; margin: 32px 0 10px; padding-bottom: 4px; border-bottom: 2px solid #e5e7eb; color: #1f2937; }
|
|
20716
|
+
h3 { font-size: 14px; font-weight: 600; margin: 20px 0 6px; color: #374151; }
|
|
20717
|
+
p { margin-bottom: 8px; color: #374151; }
|
|
20718
|
+
.meta { color: #6b7280; font-size: 12px; margin-bottom: 32px; }
|
|
20719
|
+
.cover-badge { display: inline-block; background: #1d4ed8; color: #fff; padding: 3px 10px; border-radius: 4px; font-size: 12px; font-weight: 600; margin-bottom: 16px; }
|
|
20720
|
+
table { width: 100%; border-collapse: collapse; margin-bottom: 16px; font-size: 12px; }
|
|
20721
|
+
th { background: #f3f4f6; text-align: left; padding: 6px 10px; font-weight: 600; color: #374151; border: 1px solid #e5e7eb; }
|
|
20722
|
+
td { padding: 5px 10px; border: 1px solid #e5e7eb; vertical-align: top; word-break: break-word; }
|
|
20723
|
+
tr:nth-child(even) td { background: #fafafa; }
|
|
20724
|
+
.stat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 20px; }
|
|
20725
|
+
.stat { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 12px 16px; }
|
|
20726
|
+
.stat-value { font-size: 24px; font-weight: 700; color: #1f2937; }
|
|
20727
|
+
.stat-label { font-size: 11px; color: #6b7280; margin-top: 2px; }
|
|
20728
|
+
.badge { display:inline-block; padding:1px 7px; border-radius:3px; font-size:11px; font-weight:600; color:#fff; }
|
|
20729
|
+
.badge-critical { background:#dc2626; }
|
|
20730
|
+
.badge-high { background:#d97706; }
|
|
20731
|
+
.badge-medium { background:#2563eb; }
|
|
20732
|
+
.badge-low { background:#6b7280; }
|
|
20733
|
+
.status-badge { display:inline-block; padding:1px 7px; border-radius:3px; font-size:11px; font-weight:600; border:1px solid #d1d5db; color:#374151; background:#fff; }
|
|
20734
|
+
.status-open { border-color:#f59e0b; color:#92400e; background:#fffbeb; }
|
|
20735
|
+
.status-accepted { border-color:#38bdf8; color:#075985; background:#f0f9ff; }
|
|
20736
|
+
.status-dismissed { border-color:#d1d5db; color:#6b7280; background:#f9fafb; }
|
|
20737
|
+
.status-resolved { border-color:#34d399; color:#065f46; background:#ecfdf5; }
|
|
20738
|
+
.disclaimer { background: #fffbeb; border: 1px solid #fde68a; border-left: 4px solid #f59e0b; border-radius: 4px; padding: 12px 14px; margin: 14px 0 20px; font-size: 12px; color: #374151; }
|
|
20739
|
+
.finding-card { border: 1px solid #e5e7eb; border-radius: 6px; margin-bottom: 14px; overflow: hidden; }
|
|
20740
|
+
.finding-header { display:flex; align-items:center; gap:10px; padding:10px 14px; background:#f9fafb; border-bottom:1px solid #e5e7eb; }
|
|
20741
|
+
.finding-controls { font-size:12px; font-weight:700; color:#1f2937; }
|
|
20742
|
+
.finding-session { font-family:monospace; font-size:11px; color:#6b7280; }
|
|
20743
|
+
.finding-body { padding:10px 14px; }
|
|
20744
|
+
.finding-text { font-size:13px; color:#111; margin-bottom:6px; }
|
|
20745
|
+
.finding-disclaimer { font-size:11px; color:#6b7280; margin-bottom:8px; }
|
|
20746
|
+
.evidence-list { margin:0 0 8px 16px; }
|
|
20747
|
+
.evidence-list li { font-size:11px; font-family:monospace; color:#374151; margin-bottom:2px; }
|
|
20748
|
+
.suggested-list { margin:0 0 0 16px; }
|
|
20749
|
+
.suggested-list li { font-size:11px; color:#374151; margin-bottom:2px; }
|
|
20750
|
+
.appendix { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 16px 20px; margin-top: 12px; }
|
|
20751
|
+
ul { padding-left: 20px; margin-bottom: 8px; }
|
|
20752
|
+
li { margin-bottom: 4px; }
|
|
20753
|
+
@media print {
|
|
20754
|
+
body { font-size: 11px; }
|
|
20755
|
+
.page { padding: 20px 24px; max-width: 100%; }
|
|
20756
|
+
h1 { font-size: 22px; }
|
|
20757
|
+
h2 { font-size: 14px; }
|
|
20758
|
+
}
|
|
20759
|
+
`;
|
|
20760
|
+
}
|
|
20761
|
+
});
|
|
20762
|
+
|
|
20013
20763
|
// src/cli/index.ts
|
|
20014
20764
|
var [, , command, ...args] = process.argv;
|
|
20015
20765
|
async function main() {
|
|
@@ -20074,6 +20824,11 @@ async function main() {
|
|
|
20074
20824
|
await runImport2(args);
|
|
20075
20825
|
break;
|
|
20076
20826
|
}
|
|
20827
|
+
case "review": {
|
|
20828
|
+
const { runReview: runReview3 } = await Promise.resolve().then(() => (init_review(), review_exports));
|
|
20829
|
+
await runReview3(args);
|
|
20830
|
+
break;
|
|
20831
|
+
}
|
|
20077
20832
|
default: {
|
|
20078
20833
|
const name = command ? `Unknown command: ${command}
|
|
20079
20834
|
|
|
@@ -20094,6 +20849,7 @@ Commands:
|
|
|
20094
20849
|
prune Delete sessions older than a retention cutoff
|
|
20095
20850
|
doctor Check your Chron setup \u2014 Node version, DB, MCP configs, SIEM
|
|
20096
20851
|
import Import conversations from external AI tools
|
|
20852
|
+
review Review AI sessions against compliance control criteria
|
|
20097
20853
|
|
|
20098
20854
|
Options (history):
|
|
20099
20855
|
--limit=<n> Max sessions to show (default: 20)
|
|
@@ -20127,6 +20883,15 @@ Options (doctor):
|
|
|
20127
20883
|
|
|
20128
20884
|
Options (import):
|
|
20129
20885
|
chatgpt <file> Import from ChatGPT export (.zip or conversations.json)
|
|
20886
|
+
|
|
20887
|
+
Options (review):
|
|
20888
|
+
--framework=<name> Framework to review against: soc2
|
|
20889
|
+
--since=<range> Limit to sessions since: 7d, 30d, or YYYY-MM-DD
|
|
20890
|
+
--all Include accepted, dismissed, and resolved findings
|
|
20891
|
+
--output=<file> Write HTML report to file (printable to PDF from browser)
|
|
20892
|
+
accept <id> Mark a finding as accepted; supports --note=<text>
|
|
20893
|
+
dismiss <id> Mark a finding as dismissed; supports --note=<text>
|
|
20894
|
+
resolve <id> Mark a finding as resolved; supports --note=<text>
|
|
20130
20895
|
`
|
|
20131
20896
|
);
|
|
20132
20897
|
process.exit(command ? 1 : 0);
|
package/dist/index.js
CHANGED
|
@@ -22736,10 +22736,11 @@ var init_libsql = __esm({
|
|
|
22736
22736
|
var schema_exports = {};
|
|
22737
22737
|
__export(schema_exports, {
|
|
22738
22738
|
messages: () => messages,
|
|
22739
|
+
review_findings: () => review_findings,
|
|
22739
22740
|
secrets_detected: () => secrets_detected,
|
|
22740
22741
|
sessions: () => sessions
|
|
22741
22742
|
});
|
|
22742
|
-
var sessions, messages, secrets_detected;
|
|
22743
|
+
var sessions, messages, secrets_detected, review_findings;
|
|
22743
22744
|
var init_schema = __esm({
|
|
22744
22745
|
"src/db/schema.ts"() {
|
|
22745
22746
|
"use strict";
|
|
@@ -22775,6 +22776,16 @@ var init_schema = __esm({
|
|
|
22775
22776
|
masked_value: text("masked_value").notNull(),
|
|
22776
22777
|
detected_at: text("detected_at").notNull()
|
|
22777
22778
|
});
|
|
22779
|
+
review_findings = sqliteTable("review_findings", {
|
|
22780
|
+
id: text("id").primaryKey(),
|
|
22781
|
+
rule_id: text("rule_id").notNull(),
|
|
22782
|
+
session_id: text("session_id").notNull().references(() => sessions.id),
|
|
22783
|
+
status: text("status", { enum: ["open", "accepted", "dismissed", "resolved"] }).notNull(),
|
|
22784
|
+
note: text("note"),
|
|
22785
|
+
reviewed_at: text("reviewed_at"),
|
|
22786
|
+
created_at: text("created_at").notNull(),
|
|
22787
|
+
updated_at: text("updated_at").notNull()
|
|
22788
|
+
});
|
|
22778
22789
|
}
|
|
22779
22790
|
});
|
|
22780
22791
|
|
|
@@ -22878,11 +22889,24 @@ var init_db2 = __esm({
|
|
|
22878
22889
|
detected_at TEXT NOT NULL,
|
|
22879
22890
|
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
|
|
22880
22891
|
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE
|
|
22892
|
+
)`,
|
|
22893
|
+
`CREATE TABLE IF NOT EXISTS review_findings (
|
|
22894
|
+
id TEXT PRIMARY KEY,
|
|
22895
|
+
rule_id TEXT NOT NULL,
|
|
22896
|
+
session_id TEXT NOT NULL,
|
|
22897
|
+
status TEXT NOT NULL CHECK (status IN ('open', 'accepted', 'dismissed', 'resolved')),
|
|
22898
|
+
note TEXT,
|
|
22899
|
+
reviewed_at TEXT,
|
|
22900
|
+
created_at TEXT NOT NULL,
|
|
22901
|
+
updated_at TEXT NOT NULL,
|
|
22902
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
22881
22903
|
)`,
|
|
22882
22904
|
`CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_title ON sessions(title)`,
|
|
22883
22905
|
`CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id)`,
|
|
22884
22906
|
`CREATE INDEX IF NOT EXISTS idx_secrets_detected_session_id ON secrets_detected(session_id)`,
|
|
22885
22907
|
`CREATE INDEX IF NOT EXISTS idx_secrets_detected_message_id ON secrets_detected(message_id)`,
|
|
22908
|
+
`CREATE INDEX IF NOT EXISTS idx_review_findings_session_id ON review_findings(session_id)`,
|
|
22909
|
+
`CREATE INDEX IF NOT EXISTS idx_review_findings_status ON review_findings(status)`,
|
|
22886
22910
|
`CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
|
22887
22911
|
content,
|
|
22888
22912
|
session_id UNINDEXED,
|
|
@@ -38502,7 +38526,7 @@ var init_time = __esm({
|
|
|
38502
38526
|
var version4;
|
|
38503
38527
|
var init_package = __esm({
|
|
38504
38528
|
"package.json"() {
|
|
38505
|
-
version4 = "0.1.
|
|
38529
|
+
version4 = "0.1.31";
|
|
38506
38530
|
}
|
|
38507
38531
|
});
|
|
38508
38532
|
|