chron-mcp 0.1.30 → 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 +278 -29
- 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: {
|
|
@@ -20038,10 +20062,10 @@ var init_rules = __esm({
|
|
|
20038
20062
|
id: "soc2.cc6_1.cc6_6.secret_detected",
|
|
20039
20063
|
framework: "soc2",
|
|
20040
20064
|
controls: ["CC6.1", "CC6.6"],
|
|
20041
|
-
severity: "
|
|
20042
|
-
description: "AI session contained
|
|
20043
|
-
finding: "
|
|
20044
|
-
not_claiming: "This is not evidence of a breach. Chron masks
|
|
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.",
|
|
20045
20069
|
suggested_evidence: [
|
|
20046
20070
|
"Confirm no plaintext credential was committed to version control",
|
|
20047
20071
|
"Rotate the credential if it was a live secret",
|
|
@@ -20110,6 +20134,91 @@ var init_rules = __esm({
|
|
|
20110
20134
|
}
|
|
20111
20135
|
});
|
|
20112
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
|
+
|
|
20113
20222
|
// src/review/engine.ts
|
|
20114
20223
|
async function runReview(db, rules, since) {
|
|
20115
20224
|
const client = db.$client;
|
|
@@ -20127,6 +20236,27 @@ async function runReview(db, rules, since) {
|
|
|
20127
20236
|
);
|
|
20128
20237
|
return findings;
|
|
20129
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
|
+
}
|
|
20130
20260
|
async function matchCodeChanges(client, rule, map, since) {
|
|
20131
20261
|
if (rule.match.type !== "code_change_path")
|
|
20132
20262
|
return;
|
|
@@ -20160,10 +20290,14 @@ async function matchCodeChanges(client, rule, map, since) {
|
|
|
20160
20290
|
const evidenceItem = `code_change: ${filePath || "unknown path"}`;
|
|
20161
20291
|
if (!map.has(mapKey)) {
|
|
20162
20292
|
map.set(mapKey, {
|
|
20293
|
+
id: findingId(rule.id, sessionId),
|
|
20163
20294
|
rule_id: rule.id,
|
|
20164
20295
|
framework: rule.framework,
|
|
20165
20296
|
controls: rule.controls,
|
|
20166
20297
|
severity: rule.severity,
|
|
20298
|
+
status: "open",
|
|
20299
|
+
note: null,
|
|
20300
|
+
reviewed_at: null,
|
|
20167
20301
|
session_id: sessionId,
|
|
20168
20302
|
session_prefix: sessionId.slice(0, 8),
|
|
20169
20303
|
session_title: String(row.title ?? ""),
|
|
@@ -20200,13 +20334,20 @@ async function matchSecrets(client, rule, map, since) {
|
|
|
20200
20334
|
const sessionId = String(row.session_id ?? "");
|
|
20201
20335
|
const mapKey = `${rule.id}::${sessionId}`;
|
|
20202
20336
|
const occurredAt = String(row.detected_at ?? "");
|
|
20203
|
-
const
|
|
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})`;
|
|
20204
20341
|
if (!map.has(mapKey)) {
|
|
20205
20342
|
map.set(mapKey, {
|
|
20343
|
+
id: findingId(rule.id, sessionId),
|
|
20206
20344
|
rule_id: rule.id,
|
|
20207
20345
|
framework: rule.framework,
|
|
20208
20346
|
controls: rule.controls,
|
|
20209
|
-
severity
|
|
20347
|
+
severity,
|
|
20348
|
+
status: "open",
|
|
20349
|
+
note: null,
|
|
20350
|
+
reviewed_at: null,
|
|
20210
20351
|
session_id: sessionId,
|
|
20211
20352
|
session_prefix: sessionId.slice(0, 8),
|
|
20212
20353
|
session_title: String(row.title ?? ""),
|
|
@@ -20216,10 +20357,15 @@ async function matchSecrets(client, rule, map, since) {
|
|
|
20216
20357
|
last_occurred_at: occurredAt,
|
|
20217
20358
|
finding: rule.finding,
|
|
20218
20359
|
not_claiming: rule.not_claiming,
|
|
20219
|
-
suggested_evidence:
|
|
20360
|
+
suggested_evidence: suggestedEvidence
|
|
20220
20361
|
});
|
|
20221
20362
|
}
|
|
20222
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
|
+
}
|
|
20223
20369
|
if (!finding.evidence_items.includes(evidenceItem)) {
|
|
20224
20370
|
finding.evidence_items.push(evidenceItem);
|
|
20225
20371
|
}
|
|
@@ -20227,11 +20373,38 @@ async function matchSecrets(client, rule, map, since) {
|
|
|
20227
20373
|
finding.last_occurred_at = occurredAt;
|
|
20228
20374
|
}
|
|
20229
20375
|
}
|
|
20230
|
-
var SEVERITY_ORDER;
|
|
20376
|
+
var SEVERITY_ORDER, SECRET_SEVERITY;
|
|
20231
20377
|
var init_engine = __esm({
|
|
20232
20378
|
"src/review/engine.ts"() {
|
|
20233
20379
|
"use strict";
|
|
20380
|
+
init_workflow();
|
|
20234
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
|
+
};
|
|
20235
20408
|
}
|
|
20236
20409
|
});
|
|
20237
20410
|
|
|
@@ -20240,16 +20413,22 @@ var review_exports = {};
|
|
|
20240
20413
|
__export(review_exports, {
|
|
20241
20414
|
runReview: () => runReview2
|
|
20242
20415
|
});
|
|
20243
|
-
function printFindings(findings, framework, sessionCount) {
|
|
20416
|
+
function printFindings(findings, framework, sessionCount, showAll) {
|
|
20244
20417
|
const controlsHit = new Set(findings.flatMap((f) => f.controls));
|
|
20245
|
-
process.stdout.write(
|
|
20246
|
-
|
|
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}
|
|
20247
20421
|
|
|
20248
|
-
`
|
|
20422
|
+
`
|
|
20423
|
+
);
|
|
20249
20424
|
if (findings.length === 0) {
|
|
20250
|
-
process.stdout.write(
|
|
20425
|
+
process.stdout.write(
|
|
20426
|
+
showAll ? `${DIM9}No findings. No sessions matched any control review criteria.${RESET10}
|
|
20251
20427
|
|
|
20252
|
-
`
|
|
20428
|
+
` : `${DIM9}No open findings. Use --all to include accepted, dismissed, and resolved findings.${RESET10}
|
|
20429
|
+
|
|
20430
|
+
`
|
|
20431
|
+
);
|
|
20253
20432
|
return;
|
|
20254
20433
|
}
|
|
20255
20434
|
process.stdout.write(
|
|
@@ -20265,12 +20444,16 @@ It is not a certification of compliance or evidence of any violation.${RESET10}
|
|
|
20265
20444
|
);
|
|
20266
20445
|
for (const f of findings) {
|
|
20267
20446
|
const sevColor = SEV_COLOR[f.severity] ?? DIM9;
|
|
20447
|
+
const statusColor = STATUS_COLOR[f.status] ?? DIM9;
|
|
20268
20448
|
const controls = f.controls.join(", ");
|
|
20269
20449
|
const sev = f.severity.toUpperCase().padEnd(8);
|
|
20450
|
+
const status = f.status.toUpperCase().padEnd(9);
|
|
20270
20451
|
process.stdout.write(
|
|
20271
|
-
` ${sevColor}${BOLD10}${sev}${RESET10} ${MAGENTA2}${controls.padEnd(16)}${RESET10} ${CYAN8}${f.session_prefix}${RESET10} ${f.session_title}
|
|
20452
|
+
` ${sevColor}${BOLD10}${sev}${RESET10} ${statusColor}${status}${RESET10} ${MAGENTA2}${controls.padEnd(16)}${RESET10} ${CYAN8}${f.session_prefix}${RESET10} ${f.session_title}
|
|
20272
20453
|
`
|
|
20273
20454
|
);
|
|
20455
|
+
process.stdout.write(` ${DIM9}id: ${f.id.slice(0, 16)}${RESET10}
|
|
20456
|
+
`);
|
|
20274
20457
|
process.stdout.write(` ${f.finding}
|
|
20275
20458
|
`);
|
|
20276
20459
|
for (const item of f.evidence_items.slice(0, 5)) {
|
|
@@ -20282,6 +20465,9 @@ It is not a certification of compliance or evidence of any violation.${RESET10}
|
|
|
20282
20465
|
`);
|
|
20283
20466
|
}
|
|
20284
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}
|
|
20285
20471
|
`);
|
|
20286
20472
|
process.stdout.write("\n");
|
|
20287
20473
|
}
|
|
@@ -20292,24 +20478,31 @@ function esc2(s) {
|
|
|
20292
20478
|
function badgeHtml(severity) {
|
|
20293
20479
|
return `<span class="badge badge-${esc2(severity)}">${esc2(severity.toUpperCase())}</span>`;
|
|
20294
20480
|
}
|
|
20481
|
+
function statusBadgeHtml(status) {
|
|
20482
|
+
return `<span class="status-badge status-${esc2(status)}">${esc2(status.toUpperCase())}</span>`;
|
|
20483
|
+
}
|
|
20295
20484
|
function buildReviewHtml(params) {
|
|
20296
20485
|
const { framework, host, generatedAt, sessionCount, findings } = params;
|
|
20297
20486
|
const controlsHit = new Set(findings.flatMap((f) => f.controls));
|
|
20298
20487
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
20299
20488
|
for (const f of findings)
|
|
20300
20489
|
bySeverity[f.severity] = (bySeverity[f.severity] ?? 0) + 1;
|
|
20301
|
-
const findingCards = findings.length === 0 ? '<p style="color:#6b7280">No findings
|
|
20490
|
+
const findingCards = findings.length === 0 ? '<p style="color:#6b7280">No findings matched this report scope.</p>' : findings.map((f) => `
|
|
20302
20491
|
<div class="finding-card">
|
|
20303
20492
|
<div class="finding-header">
|
|
20304
20493
|
${badgeHtml(f.severity)}
|
|
20494
|
+
${statusBadgeHtml(f.status)}
|
|
20305
20495
|
<span class="finding-controls">${esc2(f.controls.join(", "))}</span>
|
|
20306
20496
|
<span style="flex:1"></span>
|
|
20307
20497
|
<span class="finding-session">${esc2(f.session_prefix)}</span>
|
|
20308
20498
|
<span style="font-size:12px;color:#374151">${esc2(f.session_title)}</span>
|
|
20309
20499
|
</div>
|
|
20310
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>
|
|
20311
20502
|
<div class="finding-text">${esc2(f.finding)}</div>
|
|
20312
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>` : ""}
|
|
20313
20506
|
<strong style="font-size:11px;color:#374151">Evidence in session:</strong>
|
|
20314
20507
|
<ul class="evidence-list">
|
|
20315
20508
|
${f.evidence_items.map((e) => `<li>${esc2(e)}</li>`).join("")}
|
|
@@ -20395,17 +20588,28 @@ ${findingCards}
|
|
|
20395
20588
|
</body>
|
|
20396
20589
|
</html>`;
|
|
20397
20590
|
}
|
|
20398
|
-
async function countSessions(db) {
|
|
20591
|
+
async function countSessions(db, since) {
|
|
20399
20592
|
const client = db.$client;
|
|
20400
|
-
const result = await client.execute(
|
|
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
|
+
);
|
|
20401
20599
|
return Number(result.rows[0].n ?? 0);
|
|
20402
20600
|
}
|
|
20403
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
|
+
}
|
|
20404
20607
|
const frameworkArg = args2.find((a) => a.startsWith("--framework="))?.slice("--framework=".length)?.toLowerCase();
|
|
20405
20608
|
const outputArg = args2.find((a) => a.startsWith("--output="))?.slice("--output=".length);
|
|
20406
20609
|
const sinceArg = args2.find((a) => a.startsWith("--since="))?.slice("--since=".length);
|
|
20610
|
+
const showAll = args2.includes("--all");
|
|
20407
20611
|
if (!frameworkArg) {
|
|
20408
|
-
process.stderr.write("Usage: chron review --framework=soc2 [--since=<range>] [--output=<file>]\n");
|
|
20612
|
+
process.stderr.write("Usage: chron review --framework=soc2 [--since=<range>] [--all] [--output=<file>]\n");
|
|
20409
20613
|
process.exit(1);
|
|
20410
20614
|
}
|
|
20411
20615
|
const rules = FRAMEWORKS[frameworkArg];
|
|
@@ -20425,11 +20629,13 @@ async function runReview2(args2) {
|
|
|
20425
20629
|
}
|
|
20426
20630
|
}
|
|
20427
20631
|
const db = await initDb();
|
|
20428
|
-
const [
|
|
20632
|
+
const [rawFindings, sessionCount] = await Promise.all([
|
|
20429
20633
|
runReview(db, rules, since),
|
|
20430
|
-
countSessions(db)
|
|
20634
|
+
countSessions(db, since)
|
|
20431
20635
|
]);
|
|
20432
|
-
|
|
20636
|
+
const allFindings = await hydrateFindingStatuses(db, rawFindings);
|
|
20637
|
+
const findings = showAll ? allFindings : allFindings.filter((f) => f.status === "open");
|
|
20638
|
+
printFindings(findings, frameworkArg, sessionCount, showAll);
|
|
20433
20639
|
if (outputArg) {
|
|
20434
20640
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19) + " UTC";
|
|
20435
20641
|
const html = buildReviewHtml({
|
|
@@ -20444,7 +20650,34 @@ async function runReview2(args2) {
|
|
|
20444
20650
|
`);
|
|
20445
20651
|
}
|
|
20446
20652
|
}
|
|
20447
|
-
|
|
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;
|
|
20448
20681
|
var init_review = __esm({
|
|
20449
20682
|
"src/cli/review.ts"() {
|
|
20450
20683
|
"use strict";
|
|
@@ -20454,6 +20687,7 @@ var init_review = __esm({
|
|
|
20454
20687
|
init_report();
|
|
20455
20688
|
init_rules();
|
|
20456
20689
|
init_engine();
|
|
20690
|
+
init_workflow();
|
|
20457
20691
|
RESET10 = "\x1B[0m";
|
|
20458
20692
|
BOLD10 = "\x1B[1m";
|
|
20459
20693
|
DIM9 = "\x1B[2m";
|
|
@@ -20467,6 +20701,12 @@ var init_review = __esm({
|
|
|
20467
20701
|
medium: CYAN8,
|
|
20468
20702
|
low: DIM9
|
|
20469
20703
|
};
|
|
20704
|
+
STATUS_COLOR = {
|
|
20705
|
+
open: YELLOW8,
|
|
20706
|
+
accepted: CYAN8,
|
|
20707
|
+
dismissed: DIM9,
|
|
20708
|
+
resolved: CYAN8
|
|
20709
|
+
};
|
|
20470
20710
|
REPORT_CSS = `
|
|
20471
20711
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
20472
20712
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 13px; color: #111; background: #fff; line-height: 1.5; }
|
|
@@ -20490,6 +20730,11 @@ tr:nth-child(even) td { background: #fafafa; }
|
|
|
20490
20730
|
.badge-high { background:#d97706; }
|
|
20491
20731
|
.badge-medium { background:#2563eb; }
|
|
20492
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; }
|
|
20493
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; }
|
|
20494
20739
|
.finding-card { border: 1px solid #e5e7eb; border-radius: 6px; margin-bottom: 14px; overflow: hidden; }
|
|
20495
20740
|
.finding-header { display:flex; align-items:center; gap:10px; padding:10px 14px; background:#f9fafb; border-bottom:1px solid #e5e7eb; }
|
|
@@ -20642,7 +20887,11 @@ Options (import):
|
|
|
20642
20887
|
Options (review):
|
|
20643
20888
|
--framework=<name> Framework to review against: soc2
|
|
20644
20889
|
--since=<range> Limit to sessions since: 7d, 30d, or YYYY-MM-DD
|
|
20890
|
+
--all Include accepted, dismissed, and resolved findings
|
|
20645
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>
|
|
20646
20895
|
`
|
|
20647
20896
|
);
|
|
20648
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
|
|