chron-mcp 0.1.29 → 0.1.30
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 +517 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -17486,7 +17486,7 @@ var require_package = __commonJS({
|
|
|
17486
17486
|
"package.json"(exports2, module2) {
|
|
17487
17487
|
module2.exports = {
|
|
17488
17488
|
name: "chron-mcp",
|
|
17489
|
-
version: "0.1.
|
|
17489
|
+
version: "0.1.30",
|
|
17490
17490
|
mcpName: "io.github.sirinivask/chron",
|
|
17491
17491
|
description: "Audit-grade timestamped logs for every AI conversation",
|
|
17492
17492
|
repository: {
|
|
@@ -20010,6 +20010,511 @@ var init_import = __esm({
|
|
|
20010
20010
|
}
|
|
20011
20011
|
});
|
|
20012
20012
|
|
|
20013
|
+
// src/review/rules.ts
|
|
20014
|
+
var SOC2_RULES, FRAMEWORKS;
|
|
20015
|
+
var init_rules = __esm({
|
|
20016
|
+
"src/review/rules.ts"() {
|
|
20017
|
+
"use strict";
|
|
20018
|
+
SOC2_RULES = [
|
|
20019
|
+
{
|
|
20020
|
+
id: "soc2.cc6_1.ai_access_control_change",
|
|
20021
|
+
framework: "soc2",
|
|
20022
|
+
controls: ["CC6.1"],
|
|
20023
|
+
severity: "high",
|
|
20024
|
+
description: "AI touched access-control-related code",
|
|
20025
|
+
finding: "AI modified code in an access-control-sensitive path.",
|
|
20026
|
+
not_claiming: "This is not evidence of a security failure or SOC 2 non-compliance.",
|
|
20027
|
+
suggested_evidence: [
|
|
20028
|
+
"PR approval with human reviewer",
|
|
20029
|
+
"Change ticket linked to the work",
|
|
20030
|
+
"Manager or security team sign-off"
|
|
20031
|
+
],
|
|
20032
|
+
match: {
|
|
20033
|
+
type: "code_change_path",
|
|
20034
|
+
path_contains: ["auth", "iam", "rbac", "permission", "policy", "acl", "role", "login", "jwt", "oauth", "saml", "sso", "mfa", "access_control"]
|
|
20035
|
+
}
|
|
20036
|
+
},
|
|
20037
|
+
{
|
|
20038
|
+
id: "soc2.cc6_1.cc6_6.secret_detected",
|
|
20039
|
+
framework: "soc2",
|
|
20040
|
+
controls: ["CC6.1", "CC6.6"],
|
|
20041
|
+
severity: "critical",
|
|
20042
|
+
description: "AI session contained a detected secret or credential",
|
|
20043
|
+
finding: "A sensitive credential or secret was detected in an AI session.",
|
|
20044
|
+
not_claiming: "This is not evidence of a breach. Chron masks all detected values at log time \u2014 no plaintext credentials are stored.",
|
|
20045
|
+
suggested_evidence: [
|
|
20046
|
+
"Confirm no plaintext credential was committed to version control",
|
|
20047
|
+
"Rotate the credential if it was a live secret",
|
|
20048
|
+
"Document the detection and resolution in the change log"
|
|
20049
|
+
],
|
|
20050
|
+
match: { type: "secret_detected" }
|
|
20051
|
+
},
|
|
20052
|
+
{
|
|
20053
|
+
id: "soc2.cc7_2.cc8_1.ai_infra_change",
|
|
20054
|
+
framework: "soc2",
|
|
20055
|
+
controls: ["CC7.2", "CC8.1"],
|
|
20056
|
+
severity: "high",
|
|
20057
|
+
description: "AI executed or modified deployment or infrastructure files",
|
|
20058
|
+
finding: "AI modified infrastructure or deployment configuration.",
|
|
20059
|
+
not_claiming: "This is not evidence of an unauthorized change.",
|
|
20060
|
+
suggested_evidence: [
|
|
20061
|
+
"Change ticket or approval record",
|
|
20062
|
+
"Pipeline approval gate log",
|
|
20063
|
+
"Human review of the infrastructure diff"
|
|
20064
|
+
],
|
|
20065
|
+
match: {
|
|
20066
|
+
type: "code_change_path",
|
|
20067
|
+
path_contains: [".tf", "terraform", "dockerfile", "docker-compose", "kubernetes", "k8s", "helm", "ansible", ".github/workflow", "cloudformation", "pipeline", "deploy"]
|
|
20068
|
+
}
|
|
20069
|
+
},
|
|
20070
|
+
{
|
|
20071
|
+
id: "soc2.cc7_2.ai_monitoring_change",
|
|
20072
|
+
framework: "soc2",
|
|
20073
|
+
controls: ["CC7.2"],
|
|
20074
|
+
severity: "high",
|
|
20075
|
+
description: "AI changed logging, monitoring, alerting, or security tooling",
|
|
20076
|
+
finding: "AI modified logging, monitoring, or security alerting code or configuration.",
|
|
20077
|
+
not_claiming: "This is not evidence that monitoring was disabled or circumvented.",
|
|
20078
|
+
suggested_evidence: [
|
|
20079
|
+
"Confirm monitoring coverage was not reduced",
|
|
20080
|
+
"Human review of the change",
|
|
20081
|
+
"Change ticket"
|
|
20082
|
+
],
|
|
20083
|
+
match: {
|
|
20084
|
+
type: "code_change_path",
|
|
20085
|
+
path_contains: ["logging", "monitoring", "alerting", "splunk", "datadog", "prometheus", "grafana", "cloudwatch", "sentry", "pagerduty", "audit_log", "audit-log", "logger"]
|
|
20086
|
+
}
|
|
20087
|
+
},
|
|
20088
|
+
{
|
|
20089
|
+
id: "soc2.cc6_1.cc6_7.ai_data_handling_change",
|
|
20090
|
+
framework: "soc2",
|
|
20091
|
+
controls: ["CC6.1", "CC6.7"],
|
|
20092
|
+
severity: "medium",
|
|
20093
|
+
description: "AI changed data retention, deletion, export, or encryption logic",
|
|
20094
|
+
finding: "AI modified data handling, retention, or privacy-related code.",
|
|
20095
|
+
not_claiming: "This is not evidence of a data handling violation.",
|
|
20096
|
+
suggested_evidence: [
|
|
20097
|
+
"Privacy or data officer review",
|
|
20098
|
+
"Change ticket documenting the business reason",
|
|
20099
|
+
"Confirm compliance with data retention policy"
|
|
20100
|
+
],
|
|
20101
|
+
match: {
|
|
20102
|
+
type: "code_change_path",
|
|
20103
|
+
path_contains: ["retention", "encrypt", "decrypt", "gdpr", "privacy", "anonymize", "redact", "purge", "archive", "backup"]
|
|
20104
|
+
}
|
|
20105
|
+
}
|
|
20106
|
+
];
|
|
20107
|
+
FRAMEWORKS = {
|
|
20108
|
+
soc2: SOC2_RULES
|
|
20109
|
+
};
|
|
20110
|
+
}
|
|
20111
|
+
});
|
|
20112
|
+
|
|
20113
|
+
// src/review/engine.ts
|
|
20114
|
+
async function runReview(db, rules, since) {
|
|
20115
|
+
const client = db.$client;
|
|
20116
|
+
const map = /* @__PURE__ */ new Map();
|
|
20117
|
+
for (const rule of rules) {
|
|
20118
|
+
if (rule.match.type === "code_change_path") {
|
|
20119
|
+
await matchCodeChanges(client, rule, map, since);
|
|
20120
|
+
} else if (rule.match.type === "secret_detected") {
|
|
20121
|
+
await matchSecrets(client, rule, map, since);
|
|
20122
|
+
}
|
|
20123
|
+
}
|
|
20124
|
+
const findings = Array.from(map.values());
|
|
20125
|
+
findings.sort(
|
|
20126
|
+
(a, b) => (SEVERITY_ORDER[a.severity] ?? 99) - (SEVERITY_ORDER[b.severity] ?? 99) || a.last_occurred_at.localeCompare(b.last_occurred_at)
|
|
20127
|
+
);
|
|
20128
|
+
return findings;
|
|
20129
|
+
}
|
|
20130
|
+
async function matchCodeChanges(client, rule, map, since) {
|
|
20131
|
+
if (rule.match.type !== "code_change_path")
|
|
20132
|
+
return;
|
|
20133
|
+
const keywords = rule.match.path_contains;
|
|
20134
|
+
const result = await client.execute(
|
|
20135
|
+
since ? {
|
|
20136
|
+
sql: `SELECT m.id, m.session_id, m.content, m.created_at, s.title, s.ai_tool
|
|
20137
|
+
FROM messages m JOIN sessions s ON s.id = m.session_id
|
|
20138
|
+
WHERE m.event_type = 'code_change' AND m.created_at >= ?
|
|
20139
|
+
ORDER BY m.created_at`,
|
|
20140
|
+
args: [since]
|
|
20141
|
+
} : `SELECT m.id, m.session_id, m.content, m.created_at, s.title, s.ai_tool
|
|
20142
|
+
FROM messages m JOIN sessions s ON s.id = m.session_id
|
|
20143
|
+
WHERE m.event_type = 'code_change'
|
|
20144
|
+
ORDER BY m.created_at`
|
|
20145
|
+
);
|
|
20146
|
+
for (const row of result.rows) {
|
|
20147
|
+
const content = String(row.content ?? "");
|
|
20148
|
+
let filePath = "";
|
|
20149
|
+
try {
|
|
20150
|
+
const parsed = JSON.parse(content);
|
|
20151
|
+
filePath = (parsed.file_path ?? parsed.path ?? "").toLowerCase();
|
|
20152
|
+
} catch {
|
|
20153
|
+
filePath = content.toLowerCase();
|
|
20154
|
+
}
|
|
20155
|
+
if (!keywords.some((kw) => filePath.includes(kw.toLowerCase())))
|
|
20156
|
+
continue;
|
|
20157
|
+
const sessionId = String(row.session_id ?? "");
|
|
20158
|
+
const mapKey = `${rule.id}::${sessionId}`;
|
|
20159
|
+
const occurredAt = String(row.created_at ?? "");
|
|
20160
|
+
const evidenceItem = `code_change: ${filePath || "unknown path"}`;
|
|
20161
|
+
if (!map.has(mapKey)) {
|
|
20162
|
+
map.set(mapKey, {
|
|
20163
|
+
rule_id: rule.id,
|
|
20164
|
+
framework: rule.framework,
|
|
20165
|
+
controls: rule.controls,
|
|
20166
|
+
severity: rule.severity,
|
|
20167
|
+
session_id: sessionId,
|
|
20168
|
+
session_prefix: sessionId.slice(0, 8),
|
|
20169
|
+
session_title: String(row.title ?? ""),
|
|
20170
|
+
ai_tool: row.ai_tool != null ? String(row.ai_tool) : null,
|
|
20171
|
+
evidence_items: [],
|
|
20172
|
+
first_occurred_at: occurredAt,
|
|
20173
|
+
last_occurred_at: occurredAt,
|
|
20174
|
+
finding: rule.finding,
|
|
20175
|
+
not_claiming: rule.not_claiming,
|
|
20176
|
+
suggested_evidence: rule.suggested_evidence
|
|
20177
|
+
});
|
|
20178
|
+
}
|
|
20179
|
+
const finding = map.get(mapKey);
|
|
20180
|
+
if (!finding.evidence_items.includes(evidenceItem)) {
|
|
20181
|
+
finding.evidence_items.push(evidenceItem);
|
|
20182
|
+
}
|
|
20183
|
+
if (occurredAt > finding.last_occurred_at)
|
|
20184
|
+
finding.last_occurred_at = occurredAt;
|
|
20185
|
+
}
|
|
20186
|
+
}
|
|
20187
|
+
async function matchSecrets(client, rule, map, since) {
|
|
20188
|
+
const result = await client.execute(
|
|
20189
|
+
since ? {
|
|
20190
|
+
sql: `SELECT sd.session_id, sd.type, sd.masked_value, sd.detected_at, s.title, s.ai_tool
|
|
20191
|
+
FROM secrets_detected sd JOIN sessions s ON s.id = sd.session_id
|
|
20192
|
+
WHERE sd.detected_at >= ?
|
|
20193
|
+
ORDER BY sd.detected_at`,
|
|
20194
|
+
args: [since]
|
|
20195
|
+
} : `SELECT sd.session_id, sd.type, sd.masked_value, sd.detected_at, s.title, s.ai_tool
|
|
20196
|
+
FROM secrets_detected sd JOIN sessions s ON s.id = sd.session_id
|
|
20197
|
+
ORDER BY sd.detected_at`
|
|
20198
|
+
);
|
|
20199
|
+
for (const row of result.rows) {
|
|
20200
|
+
const sessionId = String(row.session_id ?? "");
|
|
20201
|
+
const mapKey = `${rule.id}::${sessionId}`;
|
|
20202
|
+
const occurredAt = String(row.detected_at ?? "");
|
|
20203
|
+
const evidenceItem = `secret_detected: ${row.type} (${row.masked_value})`;
|
|
20204
|
+
if (!map.has(mapKey)) {
|
|
20205
|
+
map.set(mapKey, {
|
|
20206
|
+
rule_id: rule.id,
|
|
20207
|
+
framework: rule.framework,
|
|
20208
|
+
controls: rule.controls,
|
|
20209
|
+
severity: rule.severity,
|
|
20210
|
+
session_id: sessionId,
|
|
20211
|
+
session_prefix: sessionId.slice(0, 8),
|
|
20212
|
+
session_title: String(row.title ?? ""),
|
|
20213
|
+
ai_tool: row.ai_tool != null ? String(row.ai_tool) : null,
|
|
20214
|
+
evidence_items: [],
|
|
20215
|
+
first_occurred_at: occurredAt,
|
|
20216
|
+
last_occurred_at: occurredAt,
|
|
20217
|
+
finding: rule.finding,
|
|
20218
|
+
not_claiming: rule.not_claiming,
|
|
20219
|
+
suggested_evidence: rule.suggested_evidence
|
|
20220
|
+
});
|
|
20221
|
+
}
|
|
20222
|
+
const finding = map.get(mapKey);
|
|
20223
|
+
if (!finding.evidence_items.includes(evidenceItem)) {
|
|
20224
|
+
finding.evidence_items.push(evidenceItem);
|
|
20225
|
+
}
|
|
20226
|
+
if (occurredAt > finding.last_occurred_at)
|
|
20227
|
+
finding.last_occurred_at = occurredAt;
|
|
20228
|
+
}
|
|
20229
|
+
}
|
|
20230
|
+
var SEVERITY_ORDER;
|
|
20231
|
+
var init_engine = __esm({
|
|
20232
|
+
"src/review/engine.ts"() {
|
|
20233
|
+
"use strict";
|
|
20234
|
+
SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
20235
|
+
}
|
|
20236
|
+
});
|
|
20237
|
+
|
|
20238
|
+
// src/cli/review.ts
|
|
20239
|
+
var review_exports = {};
|
|
20240
|
+
__export(review_exports, {
|
|
20241
|
+
runReview: () => runReview2
|
|
20242
|
+
});
|
|
20243
|
+
function printFindings(findings, framework, sessionCount) {
|
|
20244
|
+
const controlsHit = new Set(findings.flatMap((f) => f.controls));
|
|
20245
|
+
process.stdout.write(`
|
|
20246
|
+
${BOLD10}Chron Review${RESET10} ${CYAN8}${framework.toUpperCase()}${RESET10} ${DIM9}${sessionCount} session(s) reviewed${RESET10}
|
|
20247
|
+
|
|
20248
|
+
`);
|
|
20249
|
+
if (findings.length === 0) {
|
|
20250
|
+
process.stdout.write(`${DIM9}No findings. No sessions matched any control review criteria.${RESET10}
|
|
20251
|
+
|
|
20252
|
+
`);
|
|
20253
|
+
return;
|
|
20254
|
+
}
|
|
20255
|
+
process.stdout.write(
|
|
20256
|
+
`${BOLD10}${findings.length}${RESET10} finding${findings.length === 1 ? "" : "s"} across ${BOLD10}${controlsHit.size}${RESET10} control${controlsHit.size === 1 ? "" : "s"}
|
|
20257
|
+
|
|
20258
|
+
`
|
|
20259
|
+
);
|
|
20260
|
+
process.stdout.write(
|
|
20261
|
+
`${DIM9}This report identifies AI-session evidence that may require control-owner review.
|
|
20262
|
+
It is not a certification of compliance or evidence of any violation.${RESET10}
|
|
20263
|
+
|
|
20264
|
+
`
|
|
20265
|
+
);
|
|
20266
|
+
for (const f of findings) {
|
|
20267
|
+
const sevColor = SEV_COLOR[f.severity] ?? DIM9;
|
|
20268
|
+
const controls = f.controls.join(", ");
|
|
20269
|
+
const sev = f.severity.toUpperCase().padEnd(8);
|
|
20270
|
+
process.stdout.write(
|
|
20271
|
+
` ${sevColor}${BOLD10}${sev}${RESET10} ${MAGENTA2}${controls.padEnd(16)}${RESET10} ${CYAN8}${f.session_prefix}${RESET10} ${f.session_title}
|
|
20272
|
+
`
|
|
20273
|
+
);
|
|
20274
|
+
process.stdout.write(` ${f.finding}
|
|
20275
|
+
`);
|
|
20276
|
+
for (const item of f.evidence_items.slice(0, 5)) {
|
|
20277
|
+
process.stdout.write(` ${DIM9}\u2022 ${item}${RESET10}
|
|
20278
|
+
`);
|
|
20279
|
+
}
|
|
20280
|
+
if (f.evidence_items.length > 5) {
|
|
20281
|
+
process.stdout.write(` ${DIM9} \u2026 ${f.evidence_items.length - 5} more event(s)${RESET10}
|
|
20282
|
+
`);
|
|
20283
|
+
}
|
|
20284
|
+
process.stdout.write(` ${DIM9}Suggested: ${f.suggested_evidence[0]}${RESET10}
|
|
20285
|
+
`);
|
|
20286
|
+
process.stdout.write("\n");
|
|
20287
|
+
}
|
|
20288
|
+
}
|
|
20289
|
+
function esc2(s) {
|
|
20290
|
+
return (s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
20291
|
+
}
|
|
20292
|
+
function badgeHtml(severity) {
|
|
20293
|
+
return `<span class="badge badge-${esc2(severity)}">${esc2(severity.toUpperCase())}</span>`;
|
|
20294
|
+
}
|
|
20295
|
+
function buildReviewHtml(params) {
|
|
20296
|
+
const { framework, host, generatedAt, sessionCount, findings } = params;
|
|
20297
|
+
const controlsHit = new Set(findings.flatMap((f) => f.controls));
|
|
20298
|
+
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
20299
|
+
for (const f of findings)
|
|
20300
|
+
bySeverity[f.severity] = (bySeverity[f.severity] ?? 0) + 1;
|
|
20301
|
+
const findingCards = findings.length === 0 ? '<p style="color:#6b7280">No findings. No sessions matched any control review criteria.</p>' : findings.map((f) => `
|
|
20302
|
+
<div class="finding-card">
|
|
20303
|
+
<div class="finding-header">
|
|
20304
|
+
${badgeHtml(f.severity)}
|
|
20305
|
+
<span class="finding-controls">${esc2(f.controls.join(", "))}</span>
|
|
20306
|
+
<span style="flex:1"></span>
|
|
20307
|
+
<span class="finding-session">${esc2(f.session_prefix)}</span>
|
|
20308
|
+
<span style="font-size:12px;color:#374151">${esc2(f.session_title)}</span>
|
|
20309
|
+
</div>
|
|
20310
|
+
<div class="finding-body">
|
|
20311
|
+
<div class="finding-text">${esc2(f.finding)}</div>
|
|
20312
|
+
<div class="finding-disclaimer">${esc2(f.not_claiming)}</div>
|
|
20313
|
+
<strong style="font-size:11px;color:#374151">Evidence in session:</strong>
|
|
20314
|
+
<ul class="evidence-list">
|
|
20315
|
+
${f.evidence_items.map((e) => `<li>${esc2(e)}</li>`).join("")}
|
|
20316
|
+
</ul>
|
|
20317
|
+
<strong style="font-size:11px;color:#374151">Suggested review actions:</strong>
|
|
20318
|
+
<ul class="suggested-list">
|
|
20319
|
+
${f.suggested_evidence.map((s) => `<li>${esc2(s)}</li>`).join("")}
|
|
20320
|
+
</ul>
|
|
20321
|
+
</div>
|
|
20322
|
+
</div>`).join("");
|
|
20323
|
+
return `<!DOCTYPE html>
|
|
20324
|
+
<html lang="en">
|
|
20325
|
+
<head>
|
|
20326
|
+
<meta charset="UTF-8">
|
|
20327
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
20328
|
+
<title>Chron Review \u2014 ${esc2(framework.toUpperCase())} Controls</title>
|
|
20329
|
+
<style>${REPORT_CSS}</style>
|
|
20330
|
+
</head>
|
|
20331
|
+
<body>
|
|
20332
|
+
<div class="page">
|
|
20333
|
+
|
|
20334
|
+
<div style="min-height:50vh;display:flex;flex-direction:column;justify-content:center">
|
|
20335
|
+
<div class="cover-badge">${esc2(framework.toUpperCase())} Control Review</div>
|
|
20336
|
+
<h1>Chron Review Report</h1>
|
|
20337
|
+
<p class="meta">Generated by <strong>chron review</strong> | Host: <strong>${esc2(host)}</strong></p>
|
|
20338
|
+
<table style="width:auto;margin-bottom:0">
|
|
20339
|
+
<tr><th>Framework</th><td>${esc2(framework.toUpperCase())} Trust Services Criteria</td></tr>
|
|
20340
|
+
<tr><th>Generated</th><td>${esc2(generatedAt)}</td></tr>
|
|
20341
|
+
<tr><th>Sessions Reviewed</th><td>${sessionCount}</td></tr>
|
|
20342
|
+
<tr><th>Findings</th><td>${findings.length}</td></tr>
|
|
20343
|
+
<tr><th>Controls Flagged</th><td>${controlsHit.size > 0 ? Array.from(controlsHit).sort().join(", ") : "None"}</td></tr>
|
|
20344
|
+
</table>
|
|
20345
|
+
</div>
|
|
20346
|
+
|
|
20347
|
+
<div class="disclaimer">
|
|
20348
|
+
<strong>Important:</strong> This report identifies AI-session evidence that may require control-owner review.
|
|
20349
|
+
It is not a certification of compliance, a SOC 2 opinion, or evidence of any control failure.
|
|
20350
|
+
Each finding is a potential review item \u2014 not a violation. A licensed CPA conducting a SOC 2 examination
|
|
20351
|
+
must evaluate whether your controls were suitably designed and operated effectively over the audit period.
|
|
20352
|
+
</div>
|
|
20353
|
+
|
|
20354
|
+
<h2>Summary</h2>
|
|
20355
|
+
<div class="stat-grid">
|
|
20356
|
+
<div class="stat"><div class="stat-value">${sessionCount}</div><div class="stat-label">Sessions Reviewed</div></div>
|
|
20357
|
+
<div class="stat"><div class="stat-value">${findings.length}</div><div class="stat-label">Findings</div></div>
|
|
20358
|
+
<div class="stat"><div class="stat-value">${controlsHit.size}</div><div class="stat-label">Controls Flagged</div></div>
|
|
20359
|
+
</div>
|
|
20360
|
+
${bySeverity.critical > 0 ? `<p><span class="badge badge-critical">CRITICAL</span> ${bySeverity.critical} finding(s) require immediate attention.</p>` : ""}
|
|
20361
|
+
|
|
20362
|
+
<h2>Findings</h2>
|
|
20363
|
+
${findingCards}
|
|
20364
|
+
|
|
20365
|
+
<h2>Methodology</h2>
|
|
20366
|
+
<div class="appendix">
|
|
20367
|
+
<h3>What this report does</h3>
|
|
20368
|
+
<p>Chron Review evaluates AI session logs against a deterministic rule pack aligned to the AICPA Trust Services Criteria.
|
|
20369
|
+
Rules match on structured event types (<code>code_change</code>, <code>secret_detected</code>) recorded by the Chron MCP server
|
|
20370
|
+
during live AI conversations.</p>
|
|
20371
|
+
|
|
20372
|
+
<h3>Rule pack: ${esc2(framework.toUpperCase())} (v1)</h3>
|
|
20373
|
+
<ul>
|
|
20374
|
+
<li><strong>CC6.1</strong> \u2014 AI touched access-control code (auth, IAM, RBAC, permissions, login, JWT, OAuth, SAML, SSO)</li>
|
|
20375
|
+
<li><strong>CC6.1 / CC6.6</strong> \u2014 Sensitive credential detected in AI session (masked at log time, no plaintext stored)</li>
|
|
20376
|
+
<li><strong>CC7.2 / CC8.1</strong> \u2014 AI modified infrastructure or deployment configuration (Terraform, Docker, Kubernetes, pipelines)</li>
|
|
20377
|
+
<li><strong>CC7.2</strong> \u2014 AI modified logging, monitoring, or alerting code</li>
|
|
20378
|
+
<li><strong>CC6.1 / CC6.7</strong> \u2014 AI modified data retention, deletion, export, or encryption logic</li>
|
|
20379
|
+
</ul>
|
|
20380
|
+
|
|
20381
|
+
<h3>What this report does not do</h3>
|
|
20382
|
+
<ul>
|
|
20383
|
+
<li>Does not evaluate whether your controls were suitably designed or operated effectively (that is the auditor's role).</li>
|
|
20384
|
+
<li>Does not access code repositories, CI/CD systems, identity providers, or any environment outside the Chron audit database.</li>
|
|
20385
|
+
<li>Does not make any compliance determination. All findings use the language "potential review item."</li>
|
|
20386
|
+
<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>
|
|
20387
|
+
</ul>
|
|
20388
|
+
|
|
20389
|
+
<p style="margin-top:12px;font-size:11px;color:#9ca3af">
|
|
20390
|
+
Generated by chron review | ${esc2(generatedAt)} | Host: ${esc2(host)}
|
|
20391
|
+
</p>
|
|
20392
|
+
</div>
|
|
20393
|
+
|
|
20394
|
+
</div>
|
|
20395
|
+
</body>
|
|
20396
|
+
</html>`;
|
|
20397
|
+
}
|
|
20398
|
+
async function countSessions(db) {
|
|
20399
|
+
const client = db.$client;
|
|
20400
|
+
const result = await client.execute("SELECT COUNT(*) AS n FROM sessions");
|
|
20401
|
+
return Number(result.rows[0].n ?? 0);
|
|
20402
|
+
}
|
|
20403
|
+
async function runReview2(args2) {
|
|
20404
|
+
const frameworkArg = args2.find((a) => a.startsWith("--framework="))?.slice("--framework=".length)?.toLowerCase();
|
|
20405
|
+
const outputArg = args2.find((a) => a.startsWith("--output="))?.slice("--output=".length);
|
|
20406
|
+
const sinceArg = args2.find((a) => a.startsWith("--since="))?.slice("--since=".length);
|
|
20407
|
+
if (!frameworkArg) {
|
|
20408
|
+
process.stderr.write("Usage: chron review --framework=soc2 [--since=<range>] [--output=<file>]\n");
|
|
20409
|
+
process.exit(1);
|
|
20410
|
+
}
|
|
20411
|
+
const rules = FRAMEWORKS[frameworkArg];
|
|
20412
|
+
if (!rules) {
|
|
20413
|
+
process.stderr.write(`Unknown framework: "${frameworkArg}". Available: ${Object.keys(FRAMEWORKS).join(", ")}
|
|
20414
|
+
`);
|
|
20415
|
+
process.exit(1);
|
|
20416
|
+
}
|
|
20417
|
+
let since;
|
|
20418
|
+
if (sinceArg) {
|
|
20419
|
+
try {
|
|
20420
|
+
since = parseSince(sinceArg);
|
|
20421
|
+
} catch (err) {
|
|
20422
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
20423
|
+
`);
|
|
20424
|
+
process.exit(1);
|
|
20425
|
+
}
|
|
20426
|
+
}
|
|
20427
|
+
const db = await initDb();
|
|
20428
|
+
const [findings, sessionCount] = await Promise.all([
|
|
20429
|
+
runReview(db, rules, since),
|
|
20430
|
+
countSessions(db)
|
|
20431
|
+
]);
|
|
20432
|
+
printFindings(findings, frameworkArg, sessionCount);
|
|
20433
|
+
if (outputArg) {
|
|
20434
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19) + " UTC";
|
|
20435
|
+
const html = buildReviewHtml({
|
|
20436
|
+
framework: frameworkArg,
|
|
20437
|
+
host: (0, import_os11.hostname)(),
|
|
20438
|
+
generatedAt,
|
|
20439
|
+
sessionCount,
|
|
20440
|
+
findings
|
|
20441
|
+
});
|
|
20442
|
+
(0, import_fs12.writeFileSync)(outputArg, html, "utf8");
|
|
20443
|
+
process.stdout.write(`Review report written to ${outputArg}
|
|
20444
|
+
`);
|
|
20445
|
+
}
|
|
20446
|
+
}
|
|
20447
|
+
var import_fs12, import_os11, RESET10, BOLD10, DIM9, RED5, YELLOW8, CYAN8, MAGENTA2, SEV_COLOR, REPORT_CSS;
|
|
20448
|
+
var init_review = __esm({
|
|
20449
|
+
"src/cli/review.ts"() {
|
|
20450
|
+
"use strict";
|
|
20451
|
+
import_fs12 = require("fs");
|
|
20452
|
+
import_os11 = require("os");
|
|
20453
|
+
init_db2();
|
|
20454
|
+
init_report();
|
|
20455
|
+
init_rules();
|
|
20456
|
+
init_engine();
|
|
20457
|
+
RESET10 = "\x1B[0m";
|
|
20458
|
+
BOLD10 = "\x1B[1m";
|
|
20459
|
+
DIM9 = "\x1B[2m";
|
|
20460
|
+
RED5 = "\x1B[31m";
|
|
20461
|
+
YELLOW8 = "\x1B[33m";
|
|
20462
|
+
CYAN8 = "\x1B[36m";
|
|
20463
|
+
MAGENTA2 = "\x1B[35m";
|
|
20464
|
+
SEV_COLOR = {
|
|
20465
|
+
critical: RED5,
|
|
20466
|
+
high: YELLOW8,
|
|
20467
|
+
medium: CYAN8,
|
|
20468
|
+
low: DIM9
|
|
20469
|
+
};
|
|
20470
|
+
REPORT_CSS = `
|
|
20471
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
20472
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 13px; color: #111; background: #fff; line-height: 1.5; }
|
|
20473
|
+
.page { max-width: 900px; margin: 0 auto; padding: 40px 48px; }
|
|
20474
|
+
h1 { font-size: 28px; font-weight: 700; margin-bottom: 4px; }
|
|
20475
|
+
h2 { font-size: 17px; font-weight: 700; margin: 32px 0 10px; padding-bottom: 4px; border-bottom: 2px solid #e5e7eb; color: #1f2937; }
|
|
20476
|
+
h3 { font-size: 14px; font-weight: 600; margin: 20px 0 6px; color: #374151; }
|
|
20477
|
+
p { margin-bottom: 8px; color: #374151; }
|
|
20478
|
+
.meta { color: #6b7280; font-size: 12px; margin-bottom: 32px; }
|
|
20479
|
+
.cover-badge { display: inline-block; background: #1d4ed8; color: #fff; padding: 3px 10px; border-radius: 4px; font-size: 12px; font-weight: 600; margin-bottom: 16px; }
|
|
20480
|
+
table { width: 100%; border-collapse: collapse; margin-bottom: 16px; font-size: 12px; }
|
|
20481
|
+
th { background: #f3f4f6; text-align: left; padding: 6px 10px; font-weight: 600; color: #374151; border: 1px solid #e5e7eb; }
|
|
20482
|
+
td { padding: 5px 10px; border: 1px solid #e5e7eb; vertical-align: top; word-break: break-word; }
|
|
20483
|
+
tr:nth-child(even) td { background: #fafafa; }
|
|
20484
|
+
.stat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 20px; }
|
|
20485
|
+
.stat { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 12px 16px; }
|
|
20486
|
+
.stat-value { font-size: 24px; font-weight: 700; color: #1f2937; }
|
|
20487
|
+
.stat-label { font-size: 11px; color: #6b7280; margin-top: 2px; }
|
|
20488
|
+
.badge { display:inline-block; padding:1px 7px; border-radius:3px; font-size:11px; font-weight:600; color:#fff; }
|
|
20489
|
+
.badge-critical { background:#dc2626; }
|
|
20490
|
+
.badge-high { background:#d97706; }
|
|
20491
|
+
.badge-medium { background:#2563eb; }
|
|
20492
|
+
.badge-low { background:#6b7280; }
|
|
20493
|
+
.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; }
|
|
20494
|
+
.finding-card { border: 1px solid #e5e7eb; border-radius: 6px; margin-bottom: 14px; overflow: hidden; }
|
|
20495
|
+
.finding-header { display:flex; align-items:center; gap:10px; padding:10px 14px; background:#f9fafb; border-bottom:1px solid #e5e7eb; }
|
|
20496
|
+
.finding-controls { font-size:12px; font-weight:700; color:#1f2937; }
|
|
20497
|
+
.finding-session { font-family:monospace; font-size:11px; color:#6b7280; }
|
|
20498
|
+
.finding-body { padding:10px 14px; }
|
|
20499
|
+
.finding-text { font-size:13px; color:#111; margin-bottom:6px; }
|
|
20500
|
+
.finding-disclaimer { font-size:11px; color:#6b7280; margin-bottom:8px; }
|
|
20501
|
+
.evidence-list { margin:0 0 8px 16px; }
|
|
20502
|
+
.evidence-list li { font-size:11px; font-family:monospace; color:#374151; margin-bottom:2px; }
|
|
20503
|
+
.suggested-list { margin:0 0 0 16px; }
|
|
20504
|
+
.suggested-list li { font-size:11px; color:#374151; margin-bottom:2px; }
|
|
20505
|
+
.appendix { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 16px 20px; margin-top: 12px; }
|
|
20506
|
+
ul { padding-left: 20px; margin-bottom: 8px; }
|
|
20507
|
+
li { margin-bottom: 4px; }
|
|
20508
|
+
@media print {
|
|
20509
|
+
body { font-size: 11px; }
|
|
20510
|
+
.page { padding: 20px 24px; max-width: 100%; }
|
|
20511
|
+
h1 { font-size: 22px; }
|
|
20512
|
+
h2 { font-size: 14px; }
|
|
20513
|
+
}
|
|
20514
|
+
`;
|
|
20515
|
+
}
|
|
20516
|
+
});
|
|
20517
|
+
|
|
20013
20518
|
// src/cli/index.ts
|
|
20014
20519
|
var [, , command, ...args] = process.argv;
|
|
20015
20520
|
async function main() {
|
|
@@ -20074,6 +20579,11 @@ async function main() {
|
|
|
20074
20579
|
await runImport2(args);
|
|
20075
20580
|
break;
|
|
20076
20581
|
}
|
|
20582
|
+
case "review": {
|
|
20583
|
+
const { runReview: runReview3 } = await Promise.resolve().then(() => (init_review(), review_exports));
|
|
20584
|
+
await runReview3(args);
|
|
20585
|
+
break;
|
|
20586
|
+
}
|
|
20077
20587
|
default: {
|
|
20078
20588
|
const name = command ? `Unknown command: ${command}
|
|
20079
20589
|
|
|
@@ -20094,6 +20604,7 @@ Commands:
|
|
|
20094
20604
|
prune Delete sessions older than a retention cutoff
|
|
20095
20605
|
doctor Check your Chron setup \u2014 Node version, DB, MCP configs, SIEM
|
|
20096
20606
|
import Import conversations from external AI tools
|
|
20607
|
+
review Review AI sessions against compliance control criteria
|
|
20097
20608
|
|
|
20098
20609
|
Options (history):
|
|
20099
20610
|
--limit=<n> Max sessions to show (default: 20)
|
|
@@ -20127,6 +20638,11 @@ Options (doctor):
|
|
|
20127
20638
|
|
|
20128
20639
|
Options (import):
|
|
20129
20640
|
chatgpt <file> Import from ChatGPT export (.zip or conversations.json)
|
|
20641
|
+
|
|
20642
|
+
Options (review):
|
|
20643
|
+
--framework=<name> Framework to review against: soc2
|
|
20644
|
+
--since=<range> Limit to sessions since: 7d, 30d, or YYYY-MM-DD
|
|
20645
|
+
--output=<file> Write HTML report to file (printable to PDF from browser)
|
|
20130
20646
|
`
|
|
20131
20647
|
);
|
|
20132
20648
|
process.exit(command ? 1 : 0);
|
package/dist/index.js
CHANGED
|
@@ -38502,7 +38502,7 @@ var init_time = __esm({
|
|
|
38502
38502
|
var version4;
|
|
38503
38503
|
var init_package = __esm({
|
|
38504
38504
|
"package.json"() {
|
|
38505
|
-
version4 = "0.1.
|
|
38505
|
+
version4 = "0.1.30";
|
|
38506
38506
|
}
|
|
38507
38507
|
});
|
|
38508
38508
|
|