chainlesschain 0.66.0 → 0.81.0
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 +1 -1
- package/src/commands/a2a.js +380 -0
- package/src/commands/bi.js +348 -0
- package/src/commands/crosschain.js +218 -0
- package/src/commands/dlp.js +341 -0
- package/src/commands/evomap.js +394 -0
- package/src/commands/federation.js +283 -0
- package/src/commands/inference.js +318 -0
- package/src/commands/lowcode.js +356 -0
- package/src/commands/marketplace.js +256 -0
- package/src/commands/privacy.js +321 -0
- package/src/commands/reputation.js +261 -0
- package/src/commands/siem.js +246 -0
- package/src/commands/sla.js +259 -0
- package/src/commands/stress.js +230 -0
- package/src/commands/terraform.js +245 -0
- package/src/commands/zkp.js +335 -0
- package/src/lib/a2a-protocol.js +451 -0
- package/src/lib/app-builder.js +239 -0
- package/src/lib/bi-engine.js +338 -0
- package/src/lib/cross-chain.js +345 -0
- package/src/lib/dlp-engine.js +389 -0
- package/src/lib/evomap-federation.js +177 -0
- package/src/lib/evomap-governance.js +276 -0
- package/src/lib/federation-hardening.js +259 -0
- package/src/lib/inference-network.js +330 -0
- package/src/lib/privacy-computing.js +427 -0
- package/src/lib/reputation-optimizer.js +299 -0
- package/src/lib/siem-exporter.js +333 -0
- package/src/lib/skill-marketplace.js +325 -0
- package/src/lib/sla-manager.js +275 -0
- package/src/lib/stress-tester.js +330 -0
- package/src/lib/terraform-manager.js +363 -0
- package/src/lib/zkp-engine.js +274 -0
|
@@ -506,4 +506,303 @@ export function _resetState() {
|
|
|
506
506
|
_runs.clear();
|
|
507
507
|
_analytics.clear();
|
|
508
508
|
_seq = 0;
|
|
509
|
+
_maxConcurrentOptimizations = DEFAULT_MAX_CONCURRENT_OPTIMIZATIONS;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
513
|
+
* V2 (Phase 60) — Frozen enums + async optimization lifecycle +
|
|
514
|
+
* concurrency limiter + patch-merged setRunStatus + stats-v2.
|
|
515
|
+
* Strictly additive on top of the legacy surface above.
|
|
516
|
+
* ═══════════════════════════════════════════════════════════════ */
|
|
517
|
+
|
|
518
|
+
export const RUN_STATUS_V2 = Object.freeze({
|
|
519
|
+
RUNNING: "running",
|
|
520
|
+
COMPLETE: "complete",
|
|
521
|
+
APPLIED: "applied",
|
|
522
|
+
FAILED: "failed",
|
|
523
|
+
CANCELLED: "cancelled",
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
export const OBJECTIVE_V2 = Object.freeze({
|
|
527
|
+
ACCURACY: "accuracy",
|
|
528
|
+
FAIRNESS: "fairness",
|
|
529
|
+
RESILIENCE: "resilience",
|
|
530
|
+
CONVERGENCE_SPEED: "convergence_speed",
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
export const DECAY_MODEL_V2 = Object.freeze({
|
|
534
|
+
EXPONENTIAL: "exponential",
|
|
535
|
+
LINEAR: "linear",
|
|
536
|
+
STEP: "step",
|
|
537
|
+
NONE: "none",
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
export const ANOMALY_METHOD_V2 = Object.freeze({
|
|
541
|
+
IQR: "iqr",
|
|
542
|
+
Z_SCORE: "z_score",
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const DEFAULT_MAX_CONCURRENT_OPTIMIZATIONS = 2;
|
|
546
|
+
let _maxConcurrentOptimizations = DEFAULT_MAX_CONCURRENT_OPTIMIZATIONS;
|
|
547
|
+
export const REPUTATION_DEFAULT_MAX_CONCURRENT =
|
|
548
|
+
DEFAULT_MAX_CONCURRENT_OPTIMIZATIONS;
|
|
549
|
+
|
|
550
|
+
export function setMaxConcurrentOptimizations(n) {
|
|
551
|
+
if (typeof n !== "number" || !Number.isFinite(n) || n < 1) {
|
|
552
|
+
throw new Error("maxConcurrentOptimizations must be a positive integer");
|
|
553
|
+
}
|
|
554
|
+
_maxConcurrentOptimizations = Math.floor(n);
|
|
555
|
+
return _maxConcurrentOptimizations;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export function getMaxConcurrentOptimizations() {
|
|
559
|
+
return _maxConcurrentOptimizations;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Run state machine:
|
|
563
|
+
// running → { complete, failed, cancelled }
|
|
564
|
+
// complete → { applied }
|
|
565
|
+
// applied/failed/cancelled are terminal.
|
|
566
|
+
const _runTerminal = new Set([
|
|
567
|
+
RUN_STATUS_V2.APPLIED,
|
|
568
|
+
RUN_STATUS_V2.FAILED,
|
|
569
|
+
RUN_STATUS_V2.CANCELLED,
|
|
570
|
+
]);
|
|
571
|
+
const _runAllowed = new Map([
|
|
572
|
+
[
|
|
573
|
+
RUN_STATUS_V2.RUNNING,
|
|
574
|
+
new Set([
|
|
575
|
+
RUN_STATUS_V2.COMPLETE,
|
|
576
|
+
RUN_STATUS_V2.FAILED,
|
|
577
|
+
RUN_STATUS_V2.CANCELLED,
|
|
578
|
+
]),
|
|
579
|
+
],
|
|
580
|
+
[RUN_STATUS_V2.COMPLETE, new Set([RUN_STATUS_V2.APPLIED])],
|
|
581
|
+
[RUN_STATUS_V2.APPLIED, new Set([])],
|
|
582
|
+
[RUN_STATUS_V2.FAILED, new Set([])],
|
|
583
|
+
[RUN_STATUS_V2.CANCELLED, new Set([])],
|
|
584
|
+
]);
|
|
585
|
+
|
|
586
|
+
export function getActiveOptimizationCount() {
|
|
587
|
+
let count = 0;
|
|
588
|
+
for (const r of _runs.values()) {
|
|
589
|
+
if (r.status === RUN_STATUS_V2.RUNNING) count++;
|
|
590
|
+
}
|
|
591
|
+
return count;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* startOptimizationV2 — creates a RUNNING row without computing iterations.
|
|
596
|
+
* Caller drives the transition via completeOptimization or
|
|
597
|
+
* cancelOptimization / failOptimization, or the generic setRunStatus.
|
|
598
|
+
*/
|
|
599
|
+
export function startOptimizationV2(db, opts = {}) {
|
|
600
|
+
const objective = opts.objective || OBJECTIVE_V2.ACCURACY;
|
|
601
|
+
if (!Object.values(OBJECTIVE_V2).includes(objective)) {
|
|
602
|
+
throw new Error(`Unknown objective: ${objective}`);
|
|
603
|
+
}
|
|
604
|
+
const rawIter = opts.iterations == null ? 50 : Number(opts.iterations);
|
|
605
|
+
const iterations = Math.max(
|
|
606
|
+
1,
|
|
607
|
+
Math.min(1000, Number.isFinite(rawIter) ? rawIter : 50),
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
const activeCount = getActiveOptimizationCount();
|
|
611
|
+
if (activeCount >= _maxConcurrentOptimizations) {
|
|
612
|
+
throw new Error(
|
|
613
|
+
`Max concurrent optimizations reached: ${activeCount}/${_maxConcurrentOptimizations}`,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const runId = crypto.randomUUID();
|
|
618
|
+
const now = Date.now();
|
|
619
|
+
const paramSpace = {
|
|
620
|
+
lambda: [0.01, 0.5],
|
|
621
|
+
kappa: [0.5, 3.0],
|
|
622
|
+
contamination: [0.01, 0.2],
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const run = {
|
|
626
|
+
runId,
|
|
627
|
+
objective,
|
|
628
|
+
iterations,
|
|
629
|
+
paramSpace,
|
|
630
|
+
bestParams: null,
|
|
631
|
+
bestScore: null,
|
|
632
|
+
errorMessage: null,
|
|
633
|
+
status: RUN_STATUS_V2.RUNNING,
|
|
634
|
+
createdAt: now,
|
|
635
|
+
completedAt: null,
|
|
636
|
+
_seq: ++_seq,
|
|
637
|
+
};
|
|
638
|
+
_runs.set(runId, run);
|
|
639
|
+
|
|
640
|
+
db.prepare(
|
|
641
|
+
`INSERT INTO reputation_optimization_runs (run_id, objective, iterations, param_space, best_params, best_score, status, created_at, completed_at)
|
|
642
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
643
|
+
).run(
|
|
644
|
+
runId,
|
|
645
|
+
objective,
|
|
646
|
+
iterations,
|
|
647
|
+
JSON.stringify(paramSpace),
|
|
648
|
+
null,
|
|
649
|
+
null,
|
|
650
|
+
run.status,
|
|
651
|
+
now,
|
|
652
|
+
null,
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
const { _seq: _omit, ...rest } = run;
|
|
656
|
+
void _omit;
|
|
657
|
+
return rest;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* completeOptimization — advances RUNNING → COMPLETE, runs the iteration
|
|
662
|
+
* loop, writes analytics, and returns the same { ...run, analytics }
|
|
663
|
+
* shape as legacy startOptimization.
|
|
664
|
+
*/
|
|
665
|
+
export function completeOptimization(db, runId) {
|
|
666
|
+
const run = _runs.get(runId);
|
|
667
|
+
if (!run) throw new Error(`Optimization run not found: ${runId}`);
|
|
668
|
+
|
|
669
|
+
const allowed = _runAllowed.get(run.status);
|
|
670
|
+
if (!allowed || !allowed.has(RUN_STATUS_V2.COMPLETE)) {
|
|
671
|
+
throw new Error(`Invalid run status transition: ${run.status} → complete`);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
let bestParams = null;
|
|
675
|
+
let bestScore = -Infinity;
|
|
676
|
+
const history = [];
|
|
677
|
+
|
|
678
|
+
for (let i = 0; i < run.iterations; i++) {
|
|
679
|
+
const params = _sampleParams(run._seq * 101 + i * 17 + 1);
|
|
680
|
+
const score = _evaluateParams(params, run.objective, i);
|
|
681
|
+
history.push({ iteration: i, params, score });
|
|
682
|
+
if (score > bestScore) {
|
|
683
|
+
bestScore = score;
|
|
684
|
+
bestParams = params;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const now = Date.now();
|
|
689
|
+
run.status = RUN_STATUS_V2.COMPLETE;
|
|
690
|
+
run.bestParams = bestParams;
|
|
691
|
+
run.bestScore = Number(bestScore.toFixed(6));
|
|
692
|
+
run.completedAt = now;
|
|
693
|
+
run.history = history;
|
|
694
|
+
|
|
695
|
+
db.prepare(
|
|
696
|
+
`UPDATE reputation_optimization_runs SET status = ?, best_params = ?, best_score = ?, completed_at = ? WHERE run_id = ?`,
|
|
697
|
+
).run(run.status, JSON.stringify(bestParams), run.bestScore, now, runId);
|
|
698
|
+
|
|
699
|
+
const distribution = _buildDistribution(listScores({ decay: "none" }));
|
|
700
|
+
const anomalies = detectAnomalies({ method: "z_score" });
|
|
701
|
+
const recommendations = _buildRecommendations(run.objective, bestParams);
|
|
702
|
+
const analyticsId = crypto.randomUUID();
|
|
703
|
+
const analytics = {
|
|
704
|
+
analyticsId,
|
|
705
|
+
runId,
|
|
706
|
+
reputationDistribution: distribution,
|
|
707
|
+
anomalies,
|
|
708
|
+
recommendations,
|
|
709
|
+
createdAt: now,
|
|
710
|
+
};
|
|
711
|
+
_analytics.set(runId, analytics);
|
|
712
|
+
db.prepare(
|
|
713
|
+
`INSERT INTO reputation_analytics (analytics_id, run_id, reputation_distribution, anomalies, recommendations, created_at)
|
|
714
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
715
|
+
).run(
|
|
716
|
+
analyticsId,
|
|
717
|
+
runId,
|
|
718
|
+
JSON.stringify(distribution),
|
|
719
|
+
JSON.stringify(anomalies),
|
|
720
|
+
JSON.stringify(recommendations),
|
|
721
|
+
now,
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
const { _seq: _omit, ...rest } = run;
|
|
725
|
+
void _omit;
|
|
726
|
+
return { ...rest, analytics };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export function cancelOptimization(db, runId) {
|
|
730
|
+
return setRunStatus(db, runId, RUN_STATUS_V2.CANCELLED);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export function failOptimization(db, runId, errorMessage) {
|
|
734
|
+
return setRunStatus(db, runId, RUN_STATUS_V2.FAILED, { errorMessage });
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export function applyOptimization(db, runId) {
|
|
738
|
+
return setRunStatus(db, runId, RUN_STATUS_V2.APPLIED);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export function setRunStatus(db, runId, newStatus, patch = {}) {
|
|
742
|
+
const run = _runs.get(runId);
|
|
743
|
+
if (!run) throw new Error(`Optimization run not found: ${runId}`);
|
|
744
|
+
|
|
745
|
+
if (!Object.values(RUN_STATUS_V2).includes(newStatus)) {
|
|
746
|
+
throw new Error(`Unknown run status: ${newStatus}`);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const allowed = _runAllowed.get(run.status);
|
|
750
|
+
if (!allowed || !allowed.has(newStatus)) {
|
|
751
|
+
throw new Error(
|
|
752
|
+
`Invalid run status transition: ${run.status} → ${newStatus}`,
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
run.status = newStatus;
|
|
757
|
+
if (typeof patch.errorMessage === "string") {
|
|
758
|
+
run.errorMessage = patch.errorMessage;
|
|
759
|
+
}
|
|
760
|
+
if (_runTerminal.has(newStatus) && run.completedAt == null) {
|
|
761
|
+
run.completedAt = Date.now();
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
db.prepare(
|
|
765
|
+
`UPDATE reputation_optimization_runs SET status = ?, completed_at = ? WHERE run_id = ?`,
|
|
766
|
+
).run(newStatus, run.completedAt, runId);
|
|
767
|
+
|
|
768
|
+
const { _seq: _omit, ...rest } = run;
|
|
769
|
+
void _omit;
|
|
770
|
+
return rest;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
export function getReputationStatsV2() {
|
|
774
|
+
const runs = [..._runs.values()];
|
|
775
|
+
const observations = [];
|
|
776
|
+
for (const arr of _observations.values()) observations.push(...arr);
|
|
777
|
+
|
|
778
|
+
const byStatus = {};
|
|
779
|
+
for (const s of Object.values(RUN_STATUS_V2)) byStatus[s] = 0;
|
|
780
|
+
for (const r of runs) byStatus[r.status] = (byStatus[r.status] || 0) + 1;
|
|
781
|
+
|
|
782
|
+
const byObjective = {};
|
|
783
|
+
for (const o of Object.values(OBJECTIVE_V2)) byObjective[o] = 0;
|
|
784
|
+
for (const r of runs)
|
|
785
|
+
byObjective[r.objective] = (byObjective[r.objective] || 0) + 1;
|
|
786
|
+
|
|
787
|
+
let totalObservations = observations.length;
|
|
788
|
+
let totalDids = _observations.size;
|
|
789
|
+
let bestScore = null;
|
|
790
|
+
for (const r of runs) {
|
|
791
|
+
if (r.bestScore != null && (bestScore == null || r.bestScore > bestScore)) {
|
|
792
|
+
bestScore = r.bestScore;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return {
|
|
797
|
+
totalRuns: runs.length,
|
|
798
|
+
activeRuns: getActiveOptimizationCount(),
|
|
799
|
+
maxConcurrentOptimizations: _maxConcurrentOptimizations,
|
|
800
|
+
byStatus,
|
|
801
|
+
byObjective,
|
|
802
|
+
observations: {
|
|
803
|
+
totalDids,
|
|
804
|
+
totalObservations,
|
|
805
|
+
},
|
|
806
|
+
bestScoreEver: bestScore,
|
|
807
|
+
};
|
|
509
808
|
}
|
package/src/lib/siem-exporter.js
CHANGED
|
@@ -135,3 +135,336 @@ export function _resetState() {
|
|
|
135
135
|
_targets.clear();
|
|
136
136
|
_exports.length = 0;
|
|
137
137
|
}
|
|
138
|
+
|
|
139
|
+
// ═══════════════════════════════════════════════════════════════
|
|
140
|
+
// V2 Canonical Surface (Phase 51)
|
|
141
|
+
// ═══════════════════════════════════════════════════════════════
|
|
142
|
+
|
|
143
|
+
export const SIEM_FORMAT = Object.freeze({
|
|
144
|
+
JSON: "json",
|
|
145
|
+
CEF: "cef",
|
|
146
|
+
LEEF: "leef",
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
export const SIEM_TARGET_TYPE = Object.freeze({
|
|
150
|
+
SPLUNK_HEC: "splunk_hec",
|
|
151
|
+
ELASTICSEARCH: "elasticsearch",
|
|
152
|
+
AZURE_SENTINEL: "azure_sentinel",
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
export const SIEM_SEVERITY = Object.freeze({
|
|
156
|
+
DEBUG: "debug",
|
|
157
|
+
INFO: "info",
|
|
158
|
+
WARN: "warn",
|
|
159
|
+
ERROR: "error",
|
|
160
|
+
CRITICAL: "critical",
|
|
161
|
+
FATAL: "fatal",
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
export const SIEM_TARGET_STATUS = Object.freeze({
|
|
165
|
+
ACTIVE: "active",
|
|
166
|
+
PAUSED: "paused",
|
|
167
|
+
DISABLED: "disabled",
|
|
168
|
+
ERROR: "error",
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
export const SIEM_DEFAULT_BATCH_SIZE = 100;
|
|
172
|
+
|
|
173
|
+
// Design §二 severity-to-CEF table (0-10 scale, per ArcSight spec).
|
|
174
|
+
const _severityCEF = Object.freeze({
|
|
175
|
+
debug: 1,
|
|
176
|
+
info: 3,
|
|
177
|
+
warn: 5,
|
|
178
|
+
error: 7,
|
|
179
|
+
critical: 9,
|
|
180
|
+
fatal: 10,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const _allowedTargetTransitions = new Map([
|
|
184
|
+
["active", new Set(["paused", "disabled", "error"])],
|
|
185
|
+
["paused", new Set(["active", "disabled", "error"])],
|
|
186
|
+
["disabled", new Set(["active", "error"])],
|
|
187
|
+
["error", new Set(["active", "paused", "disabled"])],
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
function _validateFormat(format) {
|
|
191
|
+
if (!Object.values(SIEM_FORMAT).includes(format)) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Invalid format: ${format}. Expected one of ${Object.values(SIEM_FORMAT).join("|")}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function _validateTargetType(type) {
|
|
199
|
+
if (!Object.values(SIEM_TARGET_TYPE).includes(type)) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Invalid target type: ${type}. Expected one of ${Object.values(SIEM_TARGET_TYPE).join("|")}`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function _validateSeverity(severity) {
|
|
207
|
+
if (!Object.values(SIEM_SEVERITY).includes(severity)) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Invalid severity: ${severity}. Expected one of ${Object.values(SIEM_SEVERITY).join("|")}`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function _validateStatus(status) {
|
|
215
|
+
if (!Object.values(SIEM_TARGET_STATUS).includes(status)) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Invalid status: ${status}. Expected one of ${Object.values(SIEM_TARGET_STATUS).join("|")}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Map a SIEM_SEVERITY value to a CEF severity integer (0-10).
|
|
224
|
+
*/
|
|
225
|
+
export function severityToCEF(severity) {
|
|
226
|
+
const key = (severity || "").toLowerCase();
|
|
227
|
+
const n = _severityCEF[key];
|
|
228
|
+
if (n === undefined) {
|
|
229
|
+
throw new Error(`Unknown severity: ${severity}`);
|
|
230
|
+
}
|
|
231
|
+
return n;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// CEF extension-value escape (| \ = → escaped).
|
|
235
|
+
function _escapeCEFExt(value) {
|
|
236
|
+
return String(value == null ? "" : value)
|
|
237
|
+
.replace(/\\/g, "\\\\")
|
|
238
|
+
.replace(/\|/g, "\\|")
|
|
239
|
+
.replace(/=/g, "\\=")
|
|
240
|
+
.replace(/\r?\n/g, "\\n");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function _flatExtensions(log) {
|
|
244
|
+
const pairs = [];
|
|
245
|
+
const md = log.metadata || {};
|
|
246
|
+
for (const [k, v] of Object.entries(md)) {
|
|
247
|
+
pairs.push(`${k}=${_escapeCEFExt(v)}`);
|
|
248
|
+
}
|
|
249
|
+
if (log.userId != null) pairs.push(`suser=${_escapeCEFExt(log.userId)}`);
|
|
250
|
+
if (log.ip != null) pairs.push(`src=${_escapeCEFExt(log.ip)}`);
|
|
251
|
+
if (log.message != null) pairs.push(`msg=${_escapeCEFExt(log.message)}`);
|
|
252
|
+
if (log.timestamp != null) pairs.push(`rt=${_escapeCEFExt(log.timestamp)}`);
|
|
253
|
+
return pairs;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Format a log record as CEF (ArcSight).
|
|
258
|
+
* CEF:0|Vendor|Product|Version|{eventId}|{eventName}|{severity}|{extensions}
|
|
259
|
+
*/
|
|
260
|
+
export function formatLogCEF(log, opts = {}) {
|
|
261
|
+
const vendor = opts.vendor || "ChainlessChain";
|
|
262
|
+
const product = opts.product || "CLI";
|
|
263
|
+
const version = opts.version || "1.0.0";
|
|
264
|
+
const eventId = log.eventId || log.id || "unknown";
|
|
265
|
+
const eventName = log.eventName || log.message || "event";
|
|
266
|
+
const severity = severityToCEF(log.severity || "info");
|
|
267
|
+
const pipeEscape = (s) => String(s).replace(/\|/g, "\\|");
|
|
268
|
+
const extensions = _flatExtensions(log).join(" ");
|
|
269
|
+
return `CEF:0|${pipeEscape(vendor)}|${pipeEscape(product)}|${pipeEscape(version)}|${pipeEscape(eventId)}|${pipeEscape(eventName)}|${severity}|${extensions}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Format a log record as LEEF 2.0 (IBM QRadar).
|
|
274
|
+
* LEEF:2.0|Vendor|Product|Version|{eventId}|<tab-separated key=value>
|
|
275
|
+
*/
|
|
276
|
+
export function formatLogLEEF(log, opts = {}) {
|
|
277
|
+
const vendor = opts.vendor || "ChainlessChain";
|
|
278
|
+
const product = opts.product || "CLI";
|
|
279
|
+
const version = opts.version || "1.0.0";
|
|
280
|
+
const eventId = log.eventId || log.id || "unknown";
|
|
281
|
+
const pipeEscape = (s) => String(s).replace(/\|/g, "\\|");
|
|
282
|
+
const pairs = [];
|
|
283
|
+
if (log.timestamp != null) pairs.push(`devTime=${log.timestamp}`);
|
|
284
|
+
if (log.userId != null) pairs.push(`usrName=${log.userId}`);
|
|
285
|
+
if (log.action != null) pairs.push(`action=${log.action}`);
|
|
286
|
+
if (log.ip != null) pairs.push(`src=${log.ip}`);
|
|
287
|
+
if (log.message != null) pairs.push(`msg=${log.message}`);
|
|
288
|
+
const md = log.metadata || {};
|
|
289
|
+
for (const [k, v] of Object.entries(md)) {
|
|
290
|
+
pairs.push(`${k}=${v}`);
|
|
291
|
+
}
|
|
292
|
+
return `LEEF:2.0|${pipeEscape(vendor)}|${pipeEscape(product)}|${pipeEscape(version)}|${pipeEscape(eventId)}|${pairs.join("\t")}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Format a log record as the canonical JSON envelope (Phase 51 §二).
|
|
297
|
+
*/
|
|
298
|
+
export function formatLogJSON(log) {
|
|
299
|
+
return {
|
|
300
|
+
timestamp: log.timestamp || Date.now(),
|
|
301
|
+
severity: (log.severity || "info").toUpperCase(),
|
|
302
|
+
source: log.source || "chainlesschain-cli",
|
|
303
|
+
message: log.message || "",
|
|
304
|
+
metadata: {
|
|
305
|
+
...(log.metadata || {}),
|
|
306
|
+
eventId: log.eventId || log.id || null,
|
|
307
|
+
userId: log.userId || null,
|
|
308
|
+
action: log.action || null,
|
|
309
|
+
resource: log.resource || null,
|
|
310
|
+
ip: log.ip || null,
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Dispatch by format. Returns string for CEF/LEEF, object for JSON.
|
|
317
|
+
*/
|
|
318
|
+
export function formatLog(format, log, opts) {
|
|
319
|
+
_validateFormat(format);
|
|
320
|
+
if (format === SIEM_FORMAT.CEF) return formatLogCEF(log, opts);
|
|
321
|
+
if (format === SIEM_FORMAT.LEEF) return formatLogLEEF(log, opts);
|
|
322
|
+
return formatLogJSON(log);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Add a SIEM target with V2 canonical validation.
|
|
327
|
+
*/
|
|
328
|
+
export function addTargetV2(db, options) {
|
|
329
|
+
if (!options || typeof options !== "object") {
|
|
330
|
+
throw new Error("options object is required");
|
|
331
|
+
}
|
|
332
|
+
const { type, url, format = SIEM_FORMAT.JSON, config = {} } = options;
|
|
333
|
+
_validateTargetType(type);
|
|
334
|
+
if (!url) throw new Error("Target URL is required");
|
|
335
|
+
_validateFormat(format);
|
|
336
|
+
return addTarget(db, type, url, format, config);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Remove a target by id. Throws if unknown.
|
|
341
|
+
*/
|
|
342
|
+
export function removeTarget(db, targetId) {
|
|
343
|
+
const target = _targets.get(targetId);
|
|
344
|
+
if (!target) throw new Error(`Target not found: ${targetId}`);
|
|
345
|
+
_targets.delete(targetId);
|
|
346
|
+
db.prepare(`DELETE FROM siem_exports WHERE id = ?`).run(targetId);
|
|
347
|
+
return { success: true, targetId };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Transition a target's status using the allowed state machine.
|
|
352
|
+
* active ↔ paused/disabled/error; disabled → active/error; error → any.
|
|
353
|
+
*/
|
|
354
|
+
export function setTargetStatus(db, targetId, newStatus) {
|
|
355
|
+
const target = _targets.get(targetId);
|
|
356
|
+
if (!target) throw new Error(`Target not found: ${targetId}`);
|
|
357
|
+
_validateStatus(newStatus);
|
|
358
|
+
if (target.status === newStatus) {
|
|
359
|
+
return { ...target };
|
|
360
|
+
}
|
|
361
|
+
const allowed = _allowedTargetTransitions.get(target.status) || new Set();
|
|
362
|
+
if (!allowed.has(newStatus)) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`Invalid status transition: ${target.status} → ${newStatus}`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
target.status = newStatus;
|
|
368
|
+
db.prepare(`UPDATE siem_exports SET status = ? WHERE id = ?`).run(
|
|
369
|
+
newStatus,
|
|
370
|
+
targetId,
|
|
371
|
+
);
|
|
372
|
+
return { ...target };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Batch-export logs to a target.
|
|
377
|
+
* - Skips when target.status !== "active" (returns { skipped: true, ... }).
|
|
378
|
+
* - Chunks logs into slices of batchSize (default 100).
|
|
379
|
+
* - Returns { batches, exported, skipped, lastId }.
|
|
380
|
+
*/
|
|
381
|
+
export function exportLogsV2(db, options) {
|
|
382
|
+
if (!options || typeof options !== "object") {
|
|
383
|
+
throw new Error("options object is required");
|
|
384
|
+
}
|
|
385
|
+
const { targetId, logs = [], batchSize = SIEM_DEFAULT_BATCH_SIZE } = options;
|
|
386
|
+
if (!Number.isInteger(batchSize) || batchSize <= 0) {
|
|
387
|
+
throw new Error("batchSize must be a positive integer");
|
|
388
|
+
}
|
|
389
|
+
const target = _targets.get(targetId);
|
|
390
|
+
if (!target) throw new Error(`Target not found: ${targetId}`);
|
|
391
|
+
if (target.status !== SIEM_TARGET_STATUS.ACTIVE) {
|
|
392
|
+
return {
|
|
393
|
+
skipped: true,
|
|
394
|
+
reason: `target status is ${target.status}`,
|
|
395
|
+
batches: 0,
|
|
396
|
+
exported: 0,
|
|
397
|
+
lastId: target.lastExportedLogId,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
if (!Array.isArray(logs)) {
|
|
401
|
+
throw new Error("logs must be an array");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let batches = 0;
|
|
405
|
+
let exported = 0;
|
|
406
|
+
for (let i = 0; i < logs.length; i += batchSize) {
|
|
407
|
+
const slice = logs.slice(i, i + batchSize);
|
|
408
|
+
exportLogs(db, targetId, slice);
|
|
409
|
+
batches += 1;
|
|
410
|
+
exported += slice.length;
|
|
411
|
+
}
|
|
412
|
+
// Handle zero-length input — register one empty batch for stats parity.
|
|
413
|
+
if (logs.length === 0) {
|
|
414
|
+
exportLogs(db, targetId, []);
|
|
415
|
+
batches = 0; // no real batch was sent
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
skipped: false,
|
|
419
|
+
batches,
|
|
420
|
+
exported,
|
|
421
|
+
lastId: target.lastExportedLogId,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Extended stats with byFormat / byType / byStatus breakdowns + totals.
|
|
427
|
+
*/
|
|
428
|
+
export function getSIEMStatsV2() {
|
|
429
|
+
const targets = [..._targets.values()];
|
|
430
|
+
const byFormat = {};
|
|
431
|
+
for (const v of Object.values(SIEM_FORMAT)) byFormat[v] = 0;
|
|
432
|
+
const byType = {};
|
|
433
|
+
for (const v of Object.values(SIEM_TARGET_TYPE)) byType[v] = 0;
|
|
434
|
+
const byStatus = {};
|
|
435
|
+
for (const v of Object.values(SIEM_TARGET_STATUS)) byStatus[v] = 0;
|
|
436
|
+
let totalExported = 0;
|
|
437
|
+
for (const t of targets) {
|
|
438
|
+
if (byFormat[t.format] !== undefined) byFormat[t.format] += 1;
|
|
439
|
+
if (byType[t.type] !== undefined) byType[t.type] += 1;
|
|
440
|
+
if (byStatus[t.status] !== undefined) byStatus[t.status] += 1;
|
|
441
|
+
totalExported += t.exportedCount || 0;
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
totalTargets: targets.length,
|
|
445
|
+
totalExported,
|
|
446
|
+
totalBatches: _exports.length,
|
|
447
|
+
byFormat,
|
|
448
|
+
byType,
|
|
449
|
+
byStatus,
|
|
450
|
+
targets: targets.map((t) => ({
|
|
451
|
+
id: t.id,
|
|
452
|
+
type: t.type,
|
|
453
|
+
url: t.url,
|
|
454
|
+
format: t.format,
|
|
455
|
+
status: t.status,
|
|
456
|
+
exportedCount: t.exportedCount,
|
|
457
|
+
lastExportAt: t.lastExportAt,
|
|
458
|
+
})),
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* List targets filtered by status.
|
|
464
|
+
*/
|
|
465
|
+
export function listTargetsByStatus(status) {
|
|
466
|
+
_validateStatus(status);
|
|
467
|
+
return [..._targets.values()].filter((t) => t.status === status);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export { _severityCEF, _allowedTargetTransitions };
|