@sanctuary-framework/mcp-server 1.2.8 → 1.2.10
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.cjs +470 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +470 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +448 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +82 -66
- package/dist/index.d.ts +82 -66
- package/dist/index.js +448 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -20809,6 +20809,11 @@ var SentinelDispatcher = class {
|
|
|
20809
20809
|
fortressId: this.fortressId,
|
|
20810
20810
|
auditLog: this.auditLog,
|
|
20811
20811
|
now: this.now,
|
|
20812
|
+
// Phi-5 meta-sentinel reads the per-fortress finding store to
|
|
20813
|
+
// detect patterns across other sentinels' findings. First-order
|
|
20814
|
+
// sentinels ignore the field; the dispatcher always attaches it
|
|
20815
|
+
// because the store is already in scope here.
|
|
20816
|
+
findingStore: this.findingStore,
|
|
20812
20817
|
...contextOverrides ?? {}
|
|
20813
20818
|
};
|
|
20814
20819
|
const sentinel = await this.registry.subscribe(sentinelId, context);
|
|
@@ -20947,6 +20952,218 @@ var SentinelDispatcher = class {
|
|
|
20947
20952
|
}
|
|
20948
20953
|
}
|
|
20949
20954
|
};
|
|
20955
|
+
var ANOMALY_AUDIT_OPS = {
|
|
20956
|
+
DETECTOR_REGISTERED: "anomaly_detector_registered",
|
|
20957
|
+
DETECTOR_UNREGISTERED: "anomaly_detector_unregistered",
|
|
20958
|
+
FINDING_EMITTED: "anomaly_finding_emitted",
|
|
20959
|
+
EVALUATION_FAILED: "anomaly_evaluation_failed",
|
|
20960
|
+
TRAINING_COMPLETED: "anomaly_training_completed",
|
|
20961
|
+
TRAINING_FAILED: "anomaly_training_failed"
|
|
20962
|
+
};
|
|
20963
|
+
var DEFAULT_TICK_INTERVAL_MS2 = 6e4;
|
|
20964
|
+
var AnomalyPipelineDispatcher = class {
|
|
20965
|
+
findingStore;
|
|
20966
|
+
auditLog;
|
|
20967
|
+
storage;
|
|
20968
|
+
masterKey;
|
|
20969
|
+
fortressId;
|
|
20970
|
+
identityId;
|
|
20971
|
+
now;
|
|
20972
|
+
tickIntervalMs;
|
|
20973
|
+
detectors = /* @__PURE__ */ new Map();
|
|
20974
|
+
listeners = /* @__PURE__ */ new Set();
|
|
20975
|
+
tickTimer = null;
|
|
20976
|
+
tickInFlight = false;
|
|
20977
|
+
constructor(deps) {
|
|
20978
|
+
this.findingStore = deps.findingStore;
|
|
20979
|
+
this.auditLog = deps.auditLog;
|
|
20980
|
+
this.storage = deps.storage;
|
|
20981
|
+
this.masterKey = deps.masterKey;
|
|
20982
|
+
this.fortressId = deps.fortressId;
|
|
20983
|
+
this.identityId = deps.identityId;
|
|
20984
|
+
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
20985
|
+
this.tickIntervalMs = deps.tickIntervalMs ?? DEFAULT_TICK_INTERVAL_MS2;
|
|
20986
|
+
}
|
|
20987
|
+
onEvent(listener) {
|
|
20988
|
+
this.listeners.add(listener);
|
|
20989
|
+
return () => this.listeners.delete(listener);
|
|
20990
|
+
}
|
|
20991
|
+
/**
|
|
20992
|
+
* Register + subscribe a detector to this fortress. Idempotent: a
|
|
20993
|
+
* second call with the same detectorId returns the already-
|
|
20994
|
+
* registered instance without re-subscribing.
|
|
20995
|
+
*/
|
|
20996
|
+
async registerDetector(detector) {
|
|
20997
|
+
const existing = this.detectors.get(detector.detectorId);
|
|
20998
|
+
if (existing) return existing;
|
|
20999
|
+
const context = {
|
|
21000
|
+
fortressId: this.fortressId,
|
|
21001
|
+
auditLog: this.auditLog,
|
|
21002
|
+
storage: this.storage,
|
|
21003
|
+
masterKey: this.masterKey,
|
|
21004
|
+
now: this.now
|
|
21005
|
+
};
|
|
21006
|
+
await detector.subscribe(context);
|
|
21007
|
+
this.detectors.set(detector.detectorId, detector);
|
|
21008
|
+
this.auditLog.append(
|
|
21009
|
+
"l2",
|
|
21010
|
+
ANOMALY_AUDIT_OPS.DETECTOR_REGISTERED,
|
|
21011
|
+
this.identityId,
|
|
21012
|
+
{ detector_id: detector.detectorId, fortress_id: this.fortressId }
|
|
21013
|
+
);
|
|
21014
|
+
return detector;
|
|
21015
|
+
}
|
|
21016
|
+
/**
|
|
21017
|
+
* Unregister + tear down a detector. Idempotent. Returns true when
|
|
21018
|
+
* an active registration was removed.
|
|
21019
|
+
*/
|
|
21020
|
+
async unregisterDetector(detectorId) {
|
|
21021
|
+
const detector = this.detectors.get(detectorId);
|
|
21022
|
+
if (!detector) return false;
|
|
21023
|
+
try {
|
|
21024
|
+
await detector.unsubscribe();
|
|
21025
|
+
} finally {
|
|
21026
|
+
this.detectors.delete(detectorId);
|
|
21027
|
+
}
|
|
21028
|
+
this.auditLog.append(
|
|
21029
|
+
"l2",
|
|
21030
|
+
ANOMALY_AUDIT_OPS.DETECTOR_UNREGISTERED,
|
|
21031
|
+
this.identityId,
|
|
21032
|
+
{ detector_id: detectorId, fortress_id: this.fortressId }
|
|
21033
|
+
);
|
|
21034
|
+
return true;
|
|
21035
|
+
}
|
|
21036
|
+
listDetectors() {
|
|
21037
|
+
return [...this.detectors.keys()];
|
|
21038
|
+
}
|
|
21039
|
+
/** Run one evaluation pass over every registered detector. */
|
|
21040
|
+
async tick() {
|
|
21041
|
+
if (this.tickInFlight) return [];
|
|
21042
|
+
this.tickInFlight = true;
|
|
21043
|
+
try {
|
|
21044
|
+
const findings = [];
|
|
21045
|
+
for (const [detectorId, detector] of this.detectors.entries()) {
|
|
21046
|
+
try {
|
|
21047
|
+
const detectorFindings = await detector.evaluate();
|
|
21048
|
+
for (const raw of detectorFindings) {
|
|
21049
|
+
const stamped = await this.routeFinding(detectorId, raw);
|
|
21050
|
+
findings.push(stamped);
|
|
21051
|
+
}
|
|
21052
|
+
try {
|
|
21053
|
+
const trainingResult = await detector.classifier.train();
|
|
21054
|
+
this.auditLog.append(
|
|
21055
|
+
"l2",
|
|
21056
|
+
ANOMALY_AUDIT_OPS.TRAINING_COMPLETED,
|
|
21057
|
+
this.identityId,
|
|
21058
|
+
{
|
|
21059
|
+
detector_id: detectorId,
|
|
21060
|
+
classifier_id: detector.classifier.classifierId,
|
|
21061
|
+
trained_at: trainingResult.trained_at,
|
|
21062
|
+
sample_count: trainingResult.sample_count,
|
|
21063
|
+
agent_count: trainingResult.agent_count,
|
|
21064
|
+
fortress_id: this.fortressId
|
|
21065
|
+
}
|
|
21066
|
+
);
|
|
21067
|
+
} catch (trainErr) {
|
|
21068
|
+
const message = trainErr instanceof Error ? trainErr.message : String(trainErr);
|
|
21069
|
+
this.auditLog.append(
|
|
21070
|
+
"l2",
|
|
21071
|
+
ANOMALY_AUDIT_OPS.TRAINING_FAILED,
|
|
21072
|
+
this.identityId,
|
|
21073
|
+
{
|
|
21074
|
+
detector_id: detectorId,
|
|
21075
|
+
error_message: message,
|
|
21076
|
+
fortress_id: this.fortressId
|
|
21077
|
+
},
|
|
21078
|
+
"failure"
|
|
21079
|
+
);
|
|
21080
|
+
}
|
|
21081
|
+
} catch (err) {
|
|
21082
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
21083
|
+
const observedAt = this.now().toISOString();
|
|
21084
|
+
this.auditLog.append(
|
|
21085
|
+
"l2",
|
|
21086
|
+
ANOMALY_AUDIT_OPS.EVALUATION_FAILED,
|
|
21087
|
+
this.identityId,
|
|
21088
|
+
{
|
|
21089
|
+
detector_id: detectorId,
|
|
21090
|
+
error_message: message,
|
|
21091
|
+
fortress_id: this.fortressId
|
|
21092
|
+
},
|
|
21093
|
+
"failure"
|
|
21094
|
+
);
|
|
21095
|
+
this.emit({
|
|
21096
|
+
type: "evaluation_failed",
|
|
21097
|
+
detector_id: detectorId,
|
|
21098
|
+
error_message: message,
|
|
21099
|
+
observed_at: observedAt
|
|
21100
|
+
});
|
|
21101
|
+
}
|
|
21102
|
+
}
|
|
21103
|
+
return findings;
|
|
21104
|
+
} finally {
|
|
21105
|
+
this.tickInFlight = false;
|
|
21106
|
+
}
|
|
21107
|
+
}
|
|
21108
|
+
start() {
|
|
21109
|
+
if (this.tickTimer !== null) return;
|
|
21110
|
+
if (this.tickIntervalMs <= 0) return;
|
|
21111
|
+
this.tickTimer = setInterval(() => {
|
|
21112
|
+
void this.tick();
|
|
21113
|
+
}, this.tickIntervalMs);
|
|
21114
|
+
if (typeof this.tickTimer.unref === "function") {
|
|
21115
|
+
this.tickTimer.unref();
|
|
21116
|
+
}
|
|
21117
|
+
}
|
|
21118
|
+
stop() {
|
|
21119
|
+
if (this.tickTimer === null) return;
|
|
21120
|
+
clearInterval(this.tickTimer);
|
|
21121
|
+
this.tickTimer = null;
|
|
21122
|
+
}
|
|
21123
|
+
async dispose() {
|
|
21124
|
+
this.stop();
|
|
21125
|
+
const ids = [...this.detectors.keys()];
|
|
21126
|
+
for (const id of ids) {
|
|
21127
|
+
try {
|
|
21128
|
+
await this.unregisterDetector(id);
|
|
21129
|
+
} catch {
|
|
21130
|
+
}
|
|
21131
|
+
}
|
|
21132
|
+
this.listeners.clear();
|
|
21133
|
+
}
|
|
21134
|
+
async routeFinding(detectorId, raw) {
|
|
21135
|
+
const stamped = {
|
|
21136
|
+
...raw,
|
|
21137
|
+
finding_id: raw.finding_id || crypto.randomUUID(),
|
|
21138
|
+
fortress_id: this.fortressId,
|
|
21139
|
+
observed_at: raw.observed_at || this.now().toISOString()
|
|
21140
|
+
};
|
|
21141
|
+
await this.findingStore.saveFinding(stamped);
|
|
21142
|
+
this.auditLog.append(
|
|
21143
|
+
"l2",
|
|
21144
|
+
ANOMALY_AUDIT_OPS.FINDING_EMITTED,
|
|
21145
|
+
this.identityId,
|
|
21146
|
+
{
|
|
21147
|
+
detector_id: detectorId,
|
|
21148
|
+
finding_id: stamped.finding_id,
|
|
21149
|
+
severity: stamped.severity,
|
|
21150
|
+
anomaly_score: stamped.details["anomaly_score"] ?? null,
|
|
21151
|
+
...stamped.agent_id !== void 0 ? { agent_id: stamped.agent_id } : {},
|
|
21152
|
+
fortress_id: this.fortressId
|
|
21153
|
+
}
|
|
21154
|
+
);
|
|
21155
|
+
this.emit({ type: "finding", finding: stamped });
|
|
21156
|
+
return stamped;
|
|
21157
|
+
}
|
|
21158
|
+
emit(event) {
|
|
21159
|
+
for (const listener of this.listeners) {
|
|
21160
|
+
try {
|
|
21161
|
+
listener(event);
|
|
21162
|
+
} catch {
|
|
21163
|
+
}
|
|
21164
|
+
}
|
|
21165
|
+
}
|
|
21166
|
+
};
|
|
20950
21167
|
|
|
20951
21168
|
// src/sentinel/sentinel.ts
|
|
20952
21169
|
var Sentinel = class {
|
|
@@ -22087,6 +22304,223 @@ function truncateSummary2(s) {
|
|
|
22087
22304
|
return s.length > 240 ? s.slice(0, 237) + "..." : s;
|
|
22088
22305
|
}
|
|
22089
22306
|
|
|
22307
|
+
// src/sentinel/sentinels/anomaly-trigger.ts
|
|
22308
|
+
var ANOMALY_TRIGGER_SENTINEL_ID = "anomaly-trigger";
|
|
22309
|
+
var WARN_SIGMA5 = 3;
|
|
22310
|
+
var ALERT_SIGMA5 = 6;
|
|
22311
|
+
var BASELINE_WINDOWS5 = 7;
|
|
22312
|
+
var QUERY_LIMIT5 = 5e3;
|
|
22313
|
+
var WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
22314
|
+
var COMPOUND_TRIGGER_MIN_SENTINELS = 2;
|
|
22315
|
+
var AnomalyTriggerWatcher = class extends Sentinel {
|
|
22316
|
+
sentinelId = ANOMALY_TRIGGER_SENTINEL_ID;
|
|
22317
|
+
description = "Meta-sentinel. Watches for patterns ACROSS other sentinels' findings: compound suspicious behavior on one agent, fortress-wide finding-count spikes, and novel cross-sentinel combinations. Closes WP-V1.3-1 Sentinel Baseline Pack.";
|
|
22318
|
+
async subscribe(context) {
|
|
22319
|
+
if (!context.findingStore) {
|
|
22320
|
+
throw new Error(
|
|
22321
|
+
`${ANOMALY_TRIGGER_SENTINEL_ID}: findingStore missing from SentinelContext; this meta-sentinel requires the Phi-1 finding store`
|
|
22322
|
+
);
|
|
22323
|
+
}
|
|
22324
|
+
await super.subscribe(context);
|
|
22325
|
+
}
|
|
22326
|
+
async evaluate() {
|
|
22327
|
+
const ctx = this.requireContext();
|
|
22328
|
+
const findingStore = ctx.findingStore;
|
|
22329
|
+
if (!findingStore) {
|
|
22330
|
+
return [];
|
|
22331
|
+
}
|
|
22332
|
+
const now = ctx.now();
|
|
22333
|
+
const nowMs = now.getTime();
|
|
22334
|
+
const windowSpanMs = (BASELINE_WINDOWS5 + 1) * WINDOW_MS;
|
|
22335
|
+
const sinceIso = new Date(nowMs - windowSpanMs).toISOString();
|
|
22336
|
+
let findings;
|
|
22337
|
+
try {
|
|
22338
|
+
findings = await findingStore.listFindings({
|
|
22339
|
+
since: sinceIso,
|
|
22340
|
+
limit: QUERY_LIMIT5
|
|
22341
|
+
});
|
|
22342
|
+
} catch {
|
|
22343
|
+
return [];
|
|
22344
|
+
}
|
|
22345
|
+
const firstOrderFindings = findings.filter(
|
|
22346
|
+
(f) => f.sentinel_id !== ANOMALY_TRIGGER_SENTINEL_ID
|
|
22347
|
+
);
|
|
22348
|
+
const windowed = bucketByWindow(firstOrderFindings, nowMs);
|
|
22349
|
+
const out = [];
|
|
22350
|
+
const compoundFindings = computeCompoundFindings(
|
|
22351
|
+
windowed[0] ?? [],
|
|
22352
|
+
now
|
|
22353
|
+
);
|
|
22354
|
+
out.push(...compoundFindings);
|
|
22355
|
+
const countSpikeFinding = computeCountSpikeFinding(windowed, now);
|
|
22356
|
+
if (countSpikeFinding) out.push(countSpikeFinding);
|
|
22357
|
+
const novelComboFinding = computeNovelComboFinding(windowed, now);
|
|
22358
|
+
if (novelComboFinding) out.push(novelComboFinding);
|
|
22359
|
+
return out;
|
|
22360
|
+
}
|
|
22361
|
+
};
|
|
22362
|
+
function computeCompoundFindings(windowZero, now) {
|
|
22363
|
+
const byAgent = /* @__PURE__ */ new Map();
|
|
22364
|
+
for (const f of windowZero) {
|
|
22365
|
+
if (!f.agent_id) continue;
|
|
22366
|
+
if (!isWarnOrAlert(f.severity)) continue;
|
|
22367
|
+
let bucket = byAgent.get(f.agent_id);
|
|
22368
|
+
if (!bucket) {
|
|
22369
|
+
bucket = [];
|
|
22370
|
+
byAgent.set(f.agent_id, bucket);
|
|
22371
|
+
}
|
|
22372
|
+
bucket.push(f);
|
|
22373
|
+
}
|
|
22374
|
+
const out = [];
|
|
22375
|
+
for (const [agentId, group] of byAgent.entries()) {
|
|
22376
|
+
const distinctSentinels = new Set(group.map((f) => f.sentinel_id));
|
|
22377
|
+
if (distinctSentinels.size < COMPOUND_TRIGGER_MIN_SENTINELS) continue;
|
|
22378
|
+
const contributingSentinels = [...distinctSentinels].sort();
|
|
22379
|
+
const evidence = group.map((f) => f.finding_id).filter((id) => id.length > 0).slice(0, 50);
|
|
22380
|
+
const summary = `${agentId} agent triggered ${distinctSentinels.size} distinct sentinels in the last 24h: ${contributingSentinels.join(", ")}. Compound suspicious behavior; review the contributing findings.`;
|
|
22381
|
+
out.push({
|
|
22382
|
+
finding_id: "",
|
|
22383
|
+
sentinel_id: ANOMALY_TRIGGER_SENTINEL_ID,
|
|
22384
|
+
severity: "alert",
|
|
22385
|
+
agent_id: agentId,
|
|
22386
|
+
summary,
|
|
22387
|
+
details: {
|
|
22388
|
+
trigger: "compound",
|
|
22389
|
+
agent_id: agentId,
|
|
22390
|
+
contributing_sentinels: contributingSentinels,
|
|
22391
|
+
contributing_finding_count: group.length
|
|
22392
|
+
},
|
|
22393
|
+
observed_at: now.toISOString(),
|
|
22394
|
+
evidence_audit_ids: evidence,
|
|
22395
|
+
fortress_id: ""
|
|
22396
|
+
});
|
|
22397
|
+
}
|
|
22398
|
+
return out;
|
|
22399
|
+
}
|
|
22400
|
+
function computeCountSpikeFinding(windowed, now) {
|
|
22401
|
+
const currentCount = (windowed[0] ?? []).length;
|
|
22402
|
+
const baselineCounts = [];
|
|
22403
|
+
for (let i = 1; i <= BASELINE_WINDOWS5; i += 1) {
|
|
22404
|
+
baselineCounts.push((windowed[i] ?? []).length);
|
|
22405
|
+
}
|
|
22406
|
+
const populated = baselineCounts.filter((c) => c > 0).length;
|
|
22407
|
+
if (populated < BASELINE_WINDOWS5) {
|
|
22408
|
+
return null;
|
|
22409
|
+
}
|
|
22410
|
+
const mean = baselineCounts.reduce((sum, c) => sum + c, 0) / baselineCounts.length;
|
|
22411
|
+
const variance = baselineCounts.reduce((sum, c) => sum + (c - mean) ** 2, 0) / baselineCounts.length;
|
|
22412
|
+
const stddev = Math.sqrt(variance);
|
|
22413
|
+
const warnThreshold = mean + WARN_SIGMA5 * stddev;
|
|
22414
|
+
const alertThreshold = mean + ALERT_SIGMA5 * stddev;
|
|
22415
|
+
if (currentCount > alertThreshold) {
|
|
22416
|
+
return buildCountFinding(
|
|
22417
|
+
currentCount,
|
|
22418
|
+
mean,
|
|
22419
|
+
stddev,
|
|
22420
|
+
ALERT_SIGMA5,
|
|
22421
|
+
"alert",
|
|
22422
|
+
windowed[0] ?? [],
|
|
22423
|
+
now
|
|
22424
|
+
);
|
|
22425
|
+
}
|
|
22426
|
+
if (currentCount > warnThreshold) {
|
|
22427
|
+
return buildCountFinding(
|
|
22428
|
+
currentCount,
|
|
22429
|
+
mean,
|
|
22430
|
+
stddev,
|
|
22431
|
+
WARN_SIGMA5,
|
|
22432
|
+
"warn",
|
|
22433
|
+
windowed[0] ?? [],
|
|
22434
|
+
now
|
|
22435
|
+
);
|
|
22436
|
+
}
|
|
22437
|
+
return null;
|
|
22438
|
+
}
|
|
22439
|
+
function buildCountFinding(currentCount, mean, stddev, sigma, severity, windowZero, now) {
|
|
22440
|
+
const ratio = mean === 0 ? Number.POSITIVE_INFINITY : currentCount / mean;
|
|
22441
|
+
const ratioStr = Number.isFinite(ratio) ? `${ratio.toFixed(1)}x normal` : "no prior baseline";
|
|
22442
|
+
const summary = `Fortress finding count is ${ratioStr}: ${currentCount} findings in last 24h, baseline ${mean.toFixed(1)} +/- ${stddev.toFixed(1)}. Crossed +${sigma} sigma threshold across all sentinels.`;
|
|
22443
|
+
const evidence = windowZero.map((f) => f.finding_id).filter((id) => id.length > 0).slice(0, 50);
|
|
22444
|
+
return {
|
|
22445
|
+
finding_id: "",
|
|
22446
|
+
sentinel_id: ANOMALY_TRIGGER_SENTINEL_ID,
|
|
22447
|
+
severity,
|
|
22448
|
+
summary,
|
|
22449
|
+
details: {
|
|
22450
|
+
trigger: "count_spike",
|
|
22451
|
+
current_count: currentCount,
|
|
22452
|
+
baseline_mean: mean,
|
|
22453
|
+
baseline_stddev: stddev,
|
|
22454
|
+
sigma_threshold: sigma,
|
|
22455
|
+
ratio: Number.isFinite(ratio) ? ratio : null
|
|
22456
|
+
},
|
|
22457
|
+
observed_at: now.toISOString(),
|
|
22458
|
+
evidence_audit_ids: evidence,
|
|
22459
|
+
fortress_id: ""
|
|
22460
|
+
};
|
|
22461
|
+
}
|
|
22462
|
+
function computeNovelComboFinding(windowed, now) {
|
|
22463
|
+
const distinctByWindow = [];
|
|
22464
|
+
for (let i = 0; i <= BASELINE_WINDOWS5; i += 1) {
|
|
22465
|
+
const set = /* @__PURE__ */ new Set();
|
|
22466
|
+
for (const f of windowed[i] ?? []) {
|
|
22467
|
+
set.add(f.sentinel_id);
|
|
22468
|
+
}
|
|
22469
|
+
distinctByWindow.push(set);
|
|
22470
|
+
}
|
|
22471
|
+
const populatedBaselineWindows = distinctByWindow.slice(1).filter((s) => s.size > 0).length;
|
|
22472
|
+
if (populatedBaselineWindows < BASELINE_WINDOWS5) {
|
|
22473
|
+
return null;
|
|
22474
|
+
}
|
|
22475
|
+
const currentCombo = distinctByWindow[0];
|
|
22476
|
+
if (currentCombo.size < 2) return null;
|
|
22477
|
+
const currentKey = comboKey(currentCombo);
|
|
22478
|
+
for (let i = 1; i <= BASELINE_WINDOWS5; i += 1) {
|
|
22479
|
+
if (comboKey(distinctByWindow[i]) === currentKey) {
|
|
22480
|
+
return null;
|
|
22481
|
+
}
|
|
22482
|
+
}
|
|
22483
|
+
const sentinelIds = [...currentCombo].sort();
|
|
22484
|
+
const summary = `Novel sentinel-ID combination this 24h window: ${sentinelIds.join(" + ")}. This co-occurrence pattern has not appeared in the prior ${BASELINE_WINDOWS5} baseline windows.`;
|
|
22485
|
+
const evidence = (windowed[0] ?? []).map((f) => f.finding_id).filter((id) => id.length > 0).slice(0, 50);
|
|
22486
|
+
return {
|
|
22487
|
+
finding_id: "",
|
|
22488
|
+
sentinel_id: ANOMALY_TRIGGER_SENTINEL_ID,
|
|
22489
|
+
severity: "info",
|
|
22490
|
+
summary,
|
|
22491
|
+
details: {
|
|
22492
|
+
trigger: "novel_combo",
|
|
22493
|
+
sentinel_ids: sentinelIds,
|
|
22494
|
+
baseline_window_count: BASELINE_WINDOWS5
|
|
22495
|
+
},
|
|
22496
|
+
observed_at: now.toISOString(),
|
|
22497
|
+
evidence_audit_ids: evidence,
|
|
22498
|
+
fortress_id: ""
|
|
22499
|
+
};
|
|
22500
|
+
}
|
|
22501
|
+
function isWarnOrAlert(s) {
|
|
22502
|
+
return s === "warn" || s === "alert";
|
|
22503
|
+
}
|
|
22504
|
+
function bucketByWindow(findings, nowMs) {
|
|
22505
|
+
const buckets = Array.from(
|
|
22506
|
+
{ length: BASELINE_WINDOWS5 + 1 },
|
|
22507
|
+
() => []
|
|
22508
|
+
);
|
|
22509
|
+
for (const f of findings) {
|
|
22510
|
+
const ts = Date.parse(f.observed_at);
|
|
22511
|
+
if (!Number.isFinite(ts)) continue;
|
|
22512
|
+
const age = nowMs - ts;
|
|
22513
|
+
if (age < 0) continue;
|
|
22514
|
+
const idx = Math.floor(age / WINDOW_MS);
|
|
22515
|
+
if (idx > BASELINE_WINDOWS5) continue;
|
|
22516
|
+
buckets[idx].push(f);
|
|
22517
|
+
}
|
|
22518
|
+
return buckets;
|
|
22519
|
+
}
|
|
22520
|
+
function comboKey(set) {
|
|
22521
|
+
return [...set].sort().join("|");
|
|
22522
|
+
}
|
|
22523
|
+
|
|
22090
22524
|
// src/sentinel/sentinels/index.ts
|
|
22091
22525
|
var PHI1_BASELINE_CATALOG = [
|
|
22092
22526
|
{
|
|
@@ -22108,6 +22542,11 @@ var PHI1_BASELINE_CATALOG = [
|
|
|
22108
22542
|
sentinelId: SUSPICIOUS_TOOL_CALL_SENTINEL_ID,
|
|
22109
22543
|
description: "Surfaces tool calls whose argument shape, call frequency, or permission combination looks unusual for the fortress's recent history.",
|
|
22110
22544
|
factory: () => new SuspiciousToolCallDetector()
|
|
22545
|
+
},
|
|
22546
|
+
{
|
|
22547
|
+
sentinelId: ANOMALY_TRIGGER_SENTINEL_ID,
|
|
22548
|
+
description: "Meta-sentinel. Watches for patterns ACROSS other sentinels' findings: compound suspicious behavior on one agent, fortress-wide finding-count spikes, and novel cross-sentinel combinations. Closes WP-V1.3-1 Sentinel Baseline Pack.",
|
|
22549
|
+
factory: () => new AnomalyTriggerWatcher()
|
|
22111
22550
|
}
|
|
22112
22551
|
];
|
|
22113
22552
|
var FILE_VERSION = 1;
|
|
@@ -40640,6 +41079,15 @@ ${err.message}
|
|
|
40640
41079
|
if (dashboard) {
|
|
40641
41080
|
dashboard.setSentinelDispatcher(sentinelDispatcher);
|
|
40642
41081
|
}
|
|
41082
|
+
const anomalyDispatcher = new AnomalyPipelineDispatcher({
|
|
41083
|
+
findingStore: sentinelFindingStore,
|
|
41084
|
+
auditLog,
|
|
41085
|
+
storage,
|
|
41086
|
+
masterKey,
|
|
41087
|
+
fortressId: fortressIdForAggregator,
|
|
41088
|
+
identityId: aggregatorIdentityId
|
|
41089
|
+
});
|
|
41090
|
+
anomalyDispatcher.start();
|
|
40643
41091
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
40644
41092
|
const { tools: sanctuaryMetaTools } = createSanctuaryTools({
|
|
40645
41093
|
config,
|