chainlesschain 0.47.7 → 0.47.9
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/package.json +10 -8
- package/src/commands/activitypub.js +533 -0
- package/src/commands/compliance.js +597 -6
- package/src/commands/matrix.js +283 -0
- package/src/commands/mcp.js +344 -0
- package/src/commands/nostr.js +196 -7
- package/src/commands/social.js +265 -0
- package/src/index.js +2 -0
- package/src/lib/activitypub-bridge.js +623 -0
- package/src/lib/compliance-framework-reporter.js +600 -0
- package/src/lib/matrix-bridge.js +252 -0
- package/src/lib/mcp-registry.js +347 -0
- package/src/lib/mcp-scaffold.js +385 -0
- package/src/lib/nostr-bridge.js +214 -38
- package/src/lib/social-graph.js +408 -0
- package/src/lib/stix-parser.js +167 -0
- package/src/lib/threat-intel.js +268 -0
- package/src/lib/topic-classifier.js +400 -0
- package/src/lib/ueba.js +403 -0
- package/src/repl/agent-repl.js +23 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* chainlesschain compliance evidence|report|classify|scan|policies|check-access
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import fs from "fs";
|
|
6
7
|
import chalk from "chalk";
|
|
7
8
|
import { logger } from "../lib/logger.js";
|
|
8
9
|
import { bootstrap, shutdown } from "../runtime/bootstrap.js";
|
|
@@ -16,6 +17,69 @@ import {
|
|
|
16
17
|
addPolicy,
|
|
17
18
|
checkAccess,
|
|
18
19
|
} from "../lib/compliance-manager.js";
|
|
20
|
+
import {
|
|
21
|
+
generateFrameworkReport,
|
|
22
|
+
listFrameworks as listReporterFrameworks,
|
|
23
|
+
getFrameworkTemplate,
|
|
24
|
+
} from "../lib/compliance-framework-reporter.js";
|
|
25
|
+
import {
|
|
26
|
+
ensureThreatIntelTables,
|
|
27
|
+
importStixFile,
|
|
28
|
+
listIndicators,
|
|
29
|
+
matchObservable,
|
|
30
|
+
getStats as getThreatIntelStats,
|
|
31
|
+
removeIndicator,
|
|
32
|
+
} from "../lib/threat-intel.js";
|
|
33
|
+
import { IOC_TYPES } from "../lib/stix-parser.js";
|
|
34
|
+
import {
|
|
35
|
+
ensureUebaTables,
|
|
36
|
+
buildBaseline,
|
|
37
|
+
saveBaselines,
|
|
38
|
+
loadBaseline,
|
|
39
|
+
loadAllBaselines,
|
|
40
|
+
detectAnomalies,
|
|
41
|
+
rankEntities,
|
|
42
|
+
scoreEvent,
|
|
43
|
+
} from "../lib/ueba.js";
|
|
44
|
+
|
|
45
|
+
function _loadEvidenceFromDb(db, framework) {
|
|
46
|
+
return db
|
|
47
|
+
.prepare(
|
|
48
|
+
`SELECT id, framework, type, description, source, status, collected_at
|
|
49
|
+
FROM compliance_evidence
|
|
50
|
+
WHERE framework = ?`,
|
|
51
|
+
)
|
|
52
|
+
.all(framework)
|
|
53
|
+
.map((r) => ({
|
|
54
|
+
id: r.id,
|
|
55
|
+
framework: r.framework,
|
|
56
|
+
type: r.type,
|
|
57
|
+
description: r.description,
|
|
58
|
+
source: r.source,
|
|
59
|
+
status: r.status,
|
|
60
|
+
collectedAt: r.collected_at,
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function _loadPoliciesFromDb(db, framework) {
|
|
65
|
+
return db
|
|
66
|
+
.prepare(
|
|
67
|
+
`SELECT id, name, type, framework, rules, enabled, severity, created_at
|
|
68
|
+
FROM compliance_policies
|
|
69
|
+
WHERE framework = ? AND enabled = 1`,
|
|
70
|
+
)
|
|
71
|
+
.all(framework)
|
|
72
|
+
.map((r) => ({
|
|
73
|
+
id: r.id,
|
|
74
|
+
name: r.name,
|
|
75
|
+
type: r.type,
|
|
76
|
+
framework: r.framework,
|
|
77
|
+
rules: r.rules ? JSON.parse(r.rules) : {},
|
|
78
|
+
enabled: !!r.enabled,
|
|
79
|
+
severity: r.severity,
|
|
80
|
+
createdAt: r.created_at,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
19
83
|
|
|
20
84
|
export function registerComplianceCommand(program) {
|
|
21
85
|
const compliance = program
|
|
@@ -63,9 +127,21 @@ export function registerComplianceCommand(program) {
|
|
|
63
127
|
// compliance report
|
|
64
128
|
compliance
|
|
65
129
|
.command("report <framework>")
|
|
66
|
-
.description(
|
|
130
|
+
.description(
|
|
131
|
+
"Generate compliance report (frameworks: soc2, iso27001, gdpr, hipaa)",
|
|
132
|
+
)
|
|
67
133
|
.option("-t, --title <title>", "Report title")
|
|
68
|
-
.option(
|
|
134
|
+
.option(
|
|
135
|
+
"-f, --format <fmt>",
|
|
136
|
+
"Output format: summary | md | html | json",
|
|
137
|
+
"summary",
|
|
138
|
+
)
|
|
139
|
+
.option("-o, --output <path>", "Write report to file instead of stdout")
|
|
140
|
+
.option(
|
|
141
|
+
"--detailed",
|
|
142
|
+
"Use framework-aware template reporter (SOC2/ISO27001/GDPR)",
|
|
143
|
+
)
|
|
144
|
+
.option("--json", "Alias for --format=json (backwards-compat)")
|
|
69
145
|
.action(async (framework, options) => {
|
|
70
146
|
try {
|
|
71
147
|
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
@@ -76,15 +152,56 @@ export function registerComplianceCommand(program) {
|
|
|
76
152
|
const db = ctx.db.getDatabase();
|
|
77
153
|
ensureComplianceTables(db);
|
|
78
154
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
155
|
+
const fmt = options.json ? "json" : options.format;
|
|
156
|
+
const useDetailed =
|
|
157
|
+
options.detailed ||
|
|
158
|
+
fmt === "md" ||
|
|
159
|
+
fmt === "markdown" ||
|
|
160
|
+
fmt === "html" ||
|
|
161
|
+
!!options.output;
|
|
162
|
+
|
|
163
|
+
// Fast path — legacy generic report for backwards-compat.
|
|
164
|
+
if (!useDetailed && fmt === "summary") {
|
|
165
|
+
const result = generateReport(db, framework, options.title);
|
|
83
166
|
logger.success("Report generated");
|
|
84
167
|
logger.log(` ${chalk.bold("ID:")} ${chalk.cyan(result.id)}`);
|
|
85
168
|
logger.log(` ${chalk.bold("Title:")} ${result.title}`);
|
|
86
169
|
logger.log(` ${chalk.bold("Score:")} ${result.score}`);
|
|
87
170
|
logger.log(` ${chalk.bold("Summary:")} ${result.summary}`);
|
|
171
|
+
await shutdown();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!getFrameworkTemplate(framework)) {
|
|
176
|
+
logger.error(
|
|
177
|
+
`Framework "${framework}" has no detailed template. ` +
|
|
178
|
+
`Available: ${listReporterFrameworks().join(", ")}.`,
|
|
179
|
+
);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const evidence = _loadEvidenceFromDb(db, framework);
|
|
184
|
+
const policies = _loadPoliciesFromDb(db, framework);
|
|
185
|
+
|
|
186
|
+
const { analysis, body, format } = generateFrameworkReport(framework, {
|
|
187
|
+
evidence,
|
|
188
|
+
policies,
|
|
189
|
+
format: fmt === "summary" ? "markdown" : fmt,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (options.output) {
|
|
193
|
+
fs.writeFileSync(options.output, body, "utf-8");
|
|
194
|
+
logger.success(
|
|
195
|
+
`Report written to ${chalk.cyan(options.output)} ` +
|
|
196
|
+
`(${format}, ${body.length} bytes)`,
|
|
197
|
+
);
|
|
198
|
+
logger.log(
|
|
199
|
+
` ${chalk.bold("Score:")} ${analysis.score}/100 ` +
|
|
200
|
+
chalk.dim(`(${analysis.summary})`),
|
|
201
|
+
);
|
|
202
|
+
} else {
|
|
203
|
+
process.stdout.write(body);
|
|
204
|
+
if (!body.endsWith("\n")) process.stdout.write("\n");
|
|
88
205
|
}
|
|
89
206
|
|
|
90
207
|
await shutdown();
|
|
@@ -94,6 +211,34 @@ export function registerComplianceCommand(program) {
|
|
|
94
211
|
}
|
|
95
212
|
});
|
|
96
213
|
|
|
214
|
+
// compliance frameworks (list templates)
|
|
215
|
+
compliance
|
|
216
|
+
.command("frameworks")
|
|
217
|
+
.description("List supported report frameworks")
|
|
218
|
+
.option("--json", "Output as JSON")
|
|
219
|
+
.action(async (options) => {
|
|
220
|
+
const frameworks = listReporterFrameworks().map((id) => {
|
|
221
|
+
const t = getFrameworkTemplate(id);
|
|
222
|
+
return {
|
|
223
|
+
id,
|
|
224
|
+
name: t.name,
|
|
225
|
+
version: t.version,
|
|
226
|
+
category: t.category,
|
|
227
|
+
controlCount: t.controls.length,
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
if (options.json) {
|
|
231
|
+
console.log(JSON.stringify(frameworks, null, 2));
|
|
232
|
+
} else {
|
|
233
|
+
for (const f of frameworks) {
|
|
234
|
+
logger.log(
|
|
235
|
+
` ${chalk.cyan(f.id.padEnd(10))} ${f.name} ` +
|
|
236
|
+
chalk.dim(`(${f.version}, ${f.controlCount} controls)`),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
97
242
|
// compliance classify
|
|
98
243
|
compliance
|
|
99
244
|
.command("classify <content>")
|
|
@@ -213,4 +358,450 @@ export function registerComplianceCommand(program) {
|
|
|
213
358
|
process.exit(1);
|
|
214
359
|
}
|
|
215
360
|
});
|
|
361
|
+
|
|
362
|
+
// compliance threat-intel
|
|
363
|
+
const threat = compliance
|
|
364
|
+
.command("threat-intel")
|
|
365
|
+
.description(
|
|
366
|
+
"Threat-intelligence IoC store — STIX 2.1 import, list, match",
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
threat
|
|
370
|
+
.command("import <file>")
|
|
371
|
+
.description("Import a STIX 2.1 bundle JSON file")
|
|
372
|
+
.option("--json", "Output as JSON")
|
|
373
|
+
.action(async (file, options) => {
|
|
374
|
+
try {
|
|
375
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
376
|
+
if (!ctx.db) {
|
|
377
|
+
logger.error("Database not available");
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
const db = ctx.db.getDatabase();
|
|
381
|
+
ensureThreatIntelTables(db);
|
|
382
|
+
|
|
383
|
+
const result = importStixFile(db, file);
|
|
384
|
+
if (options.json) {
|
|
385
|
+
console.log(JSON.stringify(result, null, 2));
|
|
386
|
+
} else {
|
|
387
|
+
logger.success(
|
|
388
|
+
`Imported ${chalk.cyan(result.imported)} new, ` +
|
|
389
|
+
`${chalk.cyan(result.updated)} updated, ` +
|
|
390
|
+
`${chalk.dim(result.skipped)} skipped ` +
|
|
391
|
+
chalk.dim(`(of ${result.total} indicators)`),
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
await shutdown();
|
|
395
|
+
} catch (err) {
|
|
396
|
+
logger.error(`Failed: ${err.message}`);
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
threat
|
|
402
|
+
.command("list")
|
|
403
|
+
.description("List stored indicators")
|
|
404
|
+
.option("-t, --type <type>", `Filter by IOC type (${IOC_TYPES.join("|")})`)
|
|
405
|
+
.option("--limit <n>", "Max rows", "100")
|
|
406
|
+
.option("--json", "Output as JSON")
|
|
407
|
+
.action(async (options) => {
|
|
408
|
+
try {
|
|
409
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
410
|
+
if (!ctx.db) {
|
|
411
|
+
logger.error("Database not available");
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
const db = ctx.db.getDatabase();
|
|
415
|
+
ensureThreatIntelTables(db);
|
|
416
|
+
|
|
417
|
+
const rows = listIndicators(db, {
|
|
418
|
+
type: options.type,
|
|
419
|
+
limit: Number(options.limit) || 100,
|
|
420
|
+
});
|
|
421
|
+
if (options.json) {
|
|
422
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
423
|
+
} else if (rows.length === 0) {
|
|
424
|
+
logger.info("No indicators stored.");
|
|
425
|
+
} else {
|
|
426
|
+
for (const r of rows) {
|
|
427
|
+
const labels = r.labels.length ? ` [${r.labels.join(",")}]` : "";
|
|
428
|
+
logger.log(
|
|
429
|
+
` ${chalk.cyan(r.type.padEnd(12))} ${r.value}` +
|
|
430
|
+
chalk.dim(labels) +
|
|
431
|
+
(r.sourceName ? chalk.dim(` ← ${r.sourceName}`) : ""),
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
logger.log(chalk.dim(` (${rows.length} shown)`));
|
|
435
|
+
}
|
|
436
|
+
await shutdown();
|
|
437
|
+
} catch (err) {
|
|
438
|
+
logger.error(`Failed: ${err.message}`);
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
threat
|
|
444
|
+
.command("match <observable>")
|
|
445
|
+
.description("Check whether a value matches a stored indicator")
|
|
446
|
+
.option("--json", "Output as JSON")
|
|
447
|
+
.action(async (observable, options) => {
|
|
448
|
+
try {
|
|
449
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
450
|
+
if (!ctx.db) {
|
|
451
|
+
logger.error("Database not available");
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
const db = ctx.db.getDatabase();
|
|
455
|
+
ensureThreatIntelTables(db);
|
|
456
|
+
|
|
457
|
+
const result = matchObservable(db, observable);
|
|
458
|
+
if (options.json) {
|
|
459
|
+
console.log(JSON.stringify(result, null, 2));
|
|
460
|
+
} else if (result.matched) {
|
|
461
|
+
logger.log(
|
|
462
|
+
` ${chalk.red("⚠ MATCH")} ` +
|
|
463
|
+
`${chalk.cyan(result.type)} ${observable}` +
|
|
464
|
+
(result.indicator.sourceName
|
|
465
|
+
? chalk.dim(` ← ${result.indicator.sourceName}`)
|
|
466
|
+
: ""),
|
|
467
|
+
);
|
|
468
|
+
if (result.indicator.labels.length) {
|
|
469
|
+
logger.log(
|
|
470
|
+
chalk.dim(` labels: ${result.indicator.labels.join(", ")}`),
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
} else if (result.type === "unknown") {
|
|
474
|
+
logger.warn(`Observable type could not be classified.`);
|
|
475
|
+
// match result already includes matched:false via JSON path
|
|
476
|
+
} else {
|
|
477
|
+
logger.log(
|
|
478
|
+
` ${chalk.green("✓ clean")} ${chalk.cyan(result.type)} ${observable}`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
await shutdown();
|
|
482
|
+
if (result.matched) process.exit(2);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
logger.error(`Failed: ${err.message}`);
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
threat
|
|
490
|
+
.command("stats")
|
|
491
|
+
.description("Show indicator counts per type")
|
|
492
|
+
.option("--json", "Output as JSON")
|
|
493
|
+
.action(async (options) => {
|
|
494
|
+
try {
|
|
495
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
496
|
+
if (!ctx.db) {
|
|
497
|
+
logger.error("Database not available");
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
const db = ctx.db.getDatabase();
|
|
501
|
+
ensureThreatIntelTables(db);
|
|
502
|
+
|
|
503
|
+
const stats = getThreatIntelStats(db);
|
|
504
|
+
if (options.json) {
|
|
505
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
506
|
+
} else {
|
|
507
|
+
logger.log(` ${chalk.bold("Total:")} ${stats.total}`);
|
|
508
|
+
for (const [t, n] of Object.entries(stats.byType)) {
|
|
509
|
+
logger.log(` ${chalk.cyan(t.padEnd(12))} ${n}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
await shutdown();
|
|
513
|
+
} catch (err) {
|
|
514
|
+
logger.error(`Failed: ${err.message}`);
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
threat
|
|
520
|
+
.command("remove <type> <value>")
|
|
521
|
+
.description("Remove a single indicator")
|
|
522
|
+
.option("--json", "Output as JSON")
|
|
523
|
+
.action(async (type, value, options) => {
|
|
524
|
+
try {
|
|
525
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
526
|
+
if (!ctx.db) {
|
|
527
|
+
logger.error("Database not available");
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
const db = ctx.db.getDatabase();
|
|
531
|
+
ensureThreatIntelTables(db);
|
|
532
|
+
|
|
533
|
+
const removed = removeIndicator(db, type, value);
|
|
534
|
+
if (options.json) {
|
|
535
|
+
console.log(JSON.stringify({ removed }, null, 2));
|
|
536
|
+
} else if (removed) {
|
|
537
|
+
logger.success(`Removed ${chalk.cyan(type)} ${value}`);
|
|
538
|
+
} else {
|
|
539
|
+
logger.info(`No indicator matched ${type} ${value}`);
|
|
540
|
+
}
|
|
541
|
+
await shutdown();
|
|
542
|
+
} catch (err) {
|
|
543
|
+
logger.error(`Failed: ${err.message}`);
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// ── UEBA ────────────────────────────────────────────────────
|
|
549
|
+
const ueba = compliance
|
|
550
|
+
.command("ueba")
|
|
551
|
+
.description("User and Entity Behavior Analytics over audit_log events");
|
|
552
|
+
|
|
553
|
+
const _loadAuditEvents = (db, { entity, days } = {}) => {
|
|
554
|
+
const sql = entity
|
|
555
|
+
? `SELECT actor, operation, target, success, created_at
|
|
556
|
+
FROM audit_log
|
|
557
|
+
WHERE actor = ?`
|
|
558
|
+
: `SELECT actor, operation, target, success, created_at
|
|
559
|
+
FROM audit_log`;
|
|
560
|
+
const rows = entity ? db.prepare(sql).all(entity) : db.prepare(sql).all();
|
|
561
|
+
|
|
562
|
+
const cutoff = days
|
|
563
|
+
? Date.now() - Number(days) * 24 * 60 * 60 * 1000
|
|
564
|
+
: null;
|
|
565
|
+
|
|
566
|
+
return rows
|
|
567
|
+
.filter((r) => {
|
|
568
|
+
if (!cutoff) return true;
|
|
569
|
+
const t = new Date(r.created_at).getTime();
|
|
570
|
+
return Number.isFinite(t) && t >= cutoff;
|
|
571
|
+
})
|
|
572
|
+
.map((r) => ({
|
|
573
|
+
entity: r.actor,
|
|
574
|
+
action: r.operation,
|
|
575
|
+
resource: r.target,
|
|
576
|
+
timestamp: r.created_at,
|
|
577
|
+
success: r.success === 1 || r.success === true,
|
|
578
|
+
}));
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
ueba
|
|
582
|
+
.command("baseline")
|
|
583
|
+
.description("Build and persist per-entity baselines from audit_log")
|
|
584
|
+
.option("-e, --entity <entity>", "Only baseline this entity")
|
|
585
|
+
.option("-d, --days <n>", "Limit to events from last N days")
|
|
586
|
+
.option("--json", "Output as JSON")
|
|
587
|
+
.action(async (options) => {
|
|
588
|
+
try {
|
|
589
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
590
|
+
if (!ctx.db) {
|
|
591
|
+
logger.error("Database not available");
|
|
592
|
+
process.exit(1);
|
|
593
|
+
}
|
|
594
|
+
const db = ctx.db.getDatabase();
|
|
595
|
+
ensureUebaTables(db);
|
|
596
|
+
|
|
597
|
+
const events = _loadAuditEvents(db, {
|
|
598
|
+
entity: options.entity,
|
|
599
|
+
days: options.days,
|
|
600
|
+
});
|
|
601
|
+
const baselineMap = buildBaseline(events);
|
|
602
|
+
const saved = saveBaselines(db, baselineMap);
|
|
603
|
+
|
|
604
|
+
if (options.json) {
|
|
605
|
+
console.log(
|
|
606
|
+
JSON.stringify(
|
|
607
|
+
{
|
|
608
|
+
saved,
|
|
609
|
+
entities: [...baselineMap.keys()],
|
|
610
|
+
events: events.length,
|
|
611
|
+
},
|
|
612
|
+
null,
|
|
613
|
+
2,
|
|
614
|
+
),
|
|
615
|
+
);
|
|
616
|
+
} else {
|
|
617
|
+
logger.success(
|
|
618
|
+
`Built ${saved} baseline(s) from ${events.length} events.`,
|
|
619
|
+
);
|
|
620
|
+
for (const [entity, b] of baselineMap) {
|
|
621
|
+
logger.log(
|
|
622
|
+
` ${chalk.cyan(entity.padEnd(24))} ` +
|
|
623
|
+
`events=${b.eventCount} failures=${b.failureCount} ` +
|
|
624
|
+
`actions=${b.uniqueActions} resources=${b.uniqueResources}`,
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
await shutdown();
|
|
629
|
+
} catch (err) {
|
|
630
|
+
logger.error(`Failed: ${err.message}`);
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
ueba
|
|
636
|
+
.command("analyze")
|
|
637
|
+
.description(
|
|
638
|
+
"Score recent audit_log events against stored baselines; print anomalies",
|
|
639
|
+
)
|
|
640
|
+
.option("-e, --entity <entity>", "Only analyze this entity")
|
|
641
|
+
.option("-t, --threshold <n>", "Anomaly score threshold (default 0.7)")
|
|
642
|
+
.option("-d, --days <n>", "Candidate window in days (default 1)")
|
|
643
|
+
.option("--json", "Output as JSON")
|
|
644
|
+
.action(async (options) => {
|
|
645
|
+
try {
|
|
646
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
647
|
+
if (!ctx.db) {
|
|
648
|
+
logger.error("Database not available");
|
|
649
|
+
process.exit(1);
|
|
650
|
+
}
|
|
651
|
+
const db = ctx.db.getDatabase();
|
|
652
|
+
ensureUebaTables(db);
|
|
653
|
+
|
|
654
|
+
const threshold = options.threshold ? Number(options.threshold) : 0.7;
|
|
655
|
+
const candidates = _loadAuditEvents(db, {
|
|
656
|
+
entity: options.entity,
|
|
657
|
+
days: options.days || 1,
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
const baselineMap = options.entity
|
|
661
|
+
? (() => {
|
|
662
|
+
const m = new Map();
|
|
663
|
+
const b = loadBaseline(db, options.entity);
|
|
664
|
+
if (b) m.set(options.entity, b);
|
|
665
|
+
return m;
|
|
666
|
+
})()
|
|
667
|
+
: loadAllBaselines(db);
|
|
668
|
+
|
|
669
|
+
if (baselineMap.size === 0) {
|
|
670
|
+
logger.warn(
|
|
671
|
+
"No saved baselines found. Run `cc compliance ueba baseline` first.",
|
|
672
|
+
);
|
|
673
|
+
if (options.json) console.log(JSON.stringify([]));
|
|
674
|
+
await shutdown();
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const hits = detectAnomalies(baselineMap, candidates, { threshold });
|
|
679
|
+
if (options.json) {
|
|
680
|
+
console.log(JSON.stringify(hits, null, 2));
|
|
681
|
+
} else if (hits.length === 0) {
|
|
682
|
+
logger.info(
|
|
683
|
+
`No anomalies above ${threshold} (scanned ${candidates.length} events).`,
|
|
684
|
+
);
|
|
685
|
+
} else {
|
|
686
|
+
logger.log(
|
|
687
|
+
`${chalk.yellow(`⚠ ${hits.length} anomal${hits.length === 1 ? "y" : "ies"}`)}` +
|
|
688
|
+
` (threshold=${threshold}, scanned ${candidates.length}):`,
|
|
689
|
+
);
|
|
690
|
+
for (const h of hits) {
|
|
691
|
+
const score = h.score.toFixed(2);
|
|
692
|
+
logger.log(
|
|
693
|
+
` ${chalk.red(score)} ${chalk.cyan(h.event.entity.padEnd(18))}` +
|
|
694
|
+
` ${h.event.action} ${h.event.resource || ""}` +
|
|
695
|
+
chalk.dim(` @ ${h.event.timestamp}`),
|
|
696
|
+
);
|
|
697
|
+
for (const reason of h.reasons) {
|
|
698
|
+
logger.log(chalk.dim(` · ${reason}`));
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
await shutdown();
|
|
703
|
+
// CI hook: non-zero exit on anomaly hit (mirrors threat-intel match).
|
|
704
|
+
if (hits && hits.length > 0) process.exit(2);
|
|
705
|
+
} catch (err) {
|
|
706
|
+
logger.error(`Failed: ${err.message}`);
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
ueba
|
|
712
|
+
.command("top")
|
|
713
|
+
.description(
|
|
714
|
+
"Rank entities by composite risk score (direct over audit_log)",
|
|
715
|
+
)
|
|
716
|
+
.option("-k, --top-k <n>", "Return top K entities (default 10)")
|
|
717
|
+
.option("-d, --days <n>", "Window in days")
|
|
718
|
+
.option("--json", "Output as JSON")
|
|
719
|
+
.action(async (options) => {
|
|
720
|
+
try {
|
|
721
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
722
|
+
if (!ctx.db) {
|
|
723
|
+
logger.error("Database not available");
|
|
724
|
+
process.exit(1);
|
|
725
|
+
}
|
|
726
|
+
const db = ctx.db.getDatabase();
|
|
727
|
+
|
|
728
|
+
const events = _loadAuditEvents(db, { days: options.days });
|
|
729
|
+
const topK = options.topK ? Number(options.topK) : 10;
|
|
730
|
+
const rows = rankEntities(events, { topK });
|
|
731
|
+
|
|
732
|
+
if (options.json) {
|
|
733
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
734
|
+
} else if (rows.length === 0) {
|
|
735
|
+
logger.info("No events found in the selected window.");
|
|
736
|
+
} else {
|
|
737
|
+
logger.log(
|
|
738
|
+
`${chalk.bold("Top risky entities")} (scored over ${events.length} events):`,
|
|
739
|
+
);
|
|
740
|
+
for (const r of rows) {
|
|
741
|
+
logger.log(
|
|
742
|
+
` ${chalk.red(String(r.riskScore).padStart(6))} ` +
|
|
743
|
+
`${chalk.cyan(r.entity.padEnd(20))} ` +
|
|
744
|
+
`events=${r.eventCount} failRate=${r.failureRate.toFixed(2)} ` +
|
|
745
|
+
`actions=${r.uniqueActions} resources=${r.uniqueResources}`,
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
await shutdown();
|
|
750
|
+
} catch (err) {
|
|
751
|
+
logger.error(`Failed: ${err.message}`);
|
|
752
|
+
process.exit(1);
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
ueba
|
|
757
|
+
.command("show <entity>")
|
|
758
|
+
.description("Show a stored baseline for one entity")
|
|
759
|
+
.option("--json", "Output as JSON")
|
|
760
|
+
.action(async (entity, options) => {
|
|
761
|
+
try {
|
|
762
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
763
|
+
if (!ctx.db) {
|
|
764
|
+
logger.error("Database not available");
|
|
765
|
+
process.exit(1);
|
|
766
|
+
}
|
|
767
|
+
const db = ctx.db.getDatabase();
|
|
768
|
+
ensureUebaTables(db);
|
|
769
|
+
|
|
770
|
+
const b = loadBaseline(db, entity);
|
|
771
|
+
if (!b) {
|
|
772
|
+
if (options.json) console.log(JSON.stringify(null));
|
|
773
|
+
else logger.warn(`No baseline saved for ${entity}.`);
|
|
774
|
+
await shutdown();
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (options.json) {
|
|
779
|
+
// Strip runtime-only Maps for JSON.
|
|
780
|
+
const { actionCounts: _a, resourceCounts: _r, ...rest } = b;
|
|
781
|
+
console.log(JSON.stringify(rest, null, 2));
|
|
782
|
+
} else {
|
|
783
|
+
logger.log(`${chalk.bold(entity)}`);
|
|
784
|
+
logger.log(` events : ${b.eventCount}`);
|
|
785
|
+
logger.log(
|
|
786
|
+
` failures : ${b.failureCount} (rate ${b.failureRate.toFixed(2)})`,
|
|
787
|
+
);
|
|
788
|
+
logger.log(` unique acts : ${b.uniqueActions}`);
|
|
789
|
+
logger.log(` unique res : ${b.uniqueResources}`);
|
|
790
|
+
if (b.firstSeen) {
|
|
791
|
+
logger.log(
|
|
792
|
+
` first seen : ${new Date(b.firstSeen).toISOString()}`,
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
if (b.lastSeen) {
|
|
796
|
+
logger.log(
|
|
797
|
+
` last seen : ${new Date(b.lastSeen).toISOString()}`,
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
await shutdown();
|
|
802
|
+
} catch (err) {
|
|
803
|
+
logger.error(`Failed: ${err.message}`);
|
|
804
|
+
process.exit(1);
|
|
805
|
+
}
|
|
806
|
+
});
|
|
216
807
|
}
|