@cogenta/fleet 0.1.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/dist/agent/assert.d.ts +9 -0
- package/dist/agent/assert.d.ts.map +1 -0
- package/dist/agent/assert.js +64 -0
- package/dist/agent/assert.js.map +1 -0
- package/dist/agent/build.d.ts +21 -0
- package/dist/agent/build.d.ts.map +1 -0
- package/dist/agent/build.js +43 -0
- package/dist/agent/build.js.map +1 -0
- package/dist/agent/commands.d.ts +39 -0
- package/dist/agent/commands.d.ts.map +1 -0
- package/dist/agent/commands.js +43 -0
- package/dist/agent/commands.js.map +1 -0
- package/dist/agent/rollback-handler.d.ts +28 -0
- package/dist/agent/rollback-handler.d.ts.map +1 -0
- package/dist/agent/rollback-handler.js +52 -0
- package/dist/agent/rollback-handler.js.map +1 -0
- package/dist/agent/sign.d.ts +18 -0
- package/dist/agent/sign.d.ts.map +1 -0
- package/dist/agent/sign.js +19 -0
- package/dist/agent/sign.js.map +1 -0
- package/dist/agent/types.d.ts +86 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +2 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/alerts/alerts.d.ts +42 -0
- package/dist/alerts/alerts.d.ts.map +1 -0
- package/dist/alerts/alerts.js +98 -0
- package/dist/alerts/alerts.js.map +1 -0
- package/dist/alerts/conditions.d.ts +25 -0
- package/dist/alerts/conditions.d.ts.map +1 -0
- package/dist/alerts/conditions.js +28 -0
- package/dist/alerts/conditions.js.map +1 -0
- package/dist/alerts/tables.d.ts +13 -0
- package/dist/alerts/tables.d.ts.map +1 -0
- package/dist/alerts/tables.js +27 -0
- package/dist/alerts/tables.js.map +1 -0
- package/dist/control/commands.d.ts +38 -0
- package/dist/control/commands.d.ts.map +1 -0
- package/dist/control/commands.js +51 -0
- package/dist/control/commands.js.map +1 -0
- package/dist/control/identity.d.ts +13 -0
- package/dist/control/identity.d.ts.map +1 -0
- package/dist/control/identity.js +5 -0
- package/dist/control/identity.js.map +1 -0
- package/dist/control/ingest.d.ts +46 -0
- package/dist/control/ingest.d.ts.map +1 -0
- package/dist/control/ingest.js +56 -0
- package/dist/control/ingest.js.map +1 -0
- package/dist/control/risk.d.ts +66 -0
- package/dist/control/risk.d.ts.map +1 -0
- package/dist/control/risk.js +130 -0
- package/dist/control/risk.js.map +1 -0
- package/dist/control/state.d.ts +19 -0
- package/dist/control/state.d.ts.map +1 -0
- package/dist/control/state.js +67 -0
- package/dist/control/state.js.map +1 -0
- package/dist/control/tables.d.ts +18 -0
- package/dist/control/tables.d.ts.map +1 -0
- package/dist/control/tables.js +58 -0
- package/dist/control/tables.js.map +1 -0
- package/dist/enrollment/store.d.ts +84 -0
- package/dist/enrollment/store.d.ts.map +1 -0
- package/dist/enrollment/store.js +81 -0
- package/dist/enrollment/store.js.map +1 -0
- package/dist/enrollment/tables.d.ts +20 -0
- package/dist/enrollment/tables.d.ts.map +1 -0
- package/dist/enrollment/tables.js +55 -0
- package/dist/enrollment/tables.js.map +1 -0
- package/dist/enrollment/tokens.d.ts +4 -0
- package/dist/enrollment/tokens.d.ts.map +1 -0
- package/dist/enrollment/tokens.js +20 -0
- package/dist/enrollment/tokens.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/inventory/drift.d.ts +73 -0
- package/dist/inventory/drift.d.ts.map +1 -0
- package/dist/inventory/drift.js +91 -0
- package/dist/inventory/drift.js.map +1 -0
- package/dist/reporting/report.d.ts +65 -0
- package/dist/reporting/report.d.ts.map +1 -0
- package/dist/reporting/report.js +137 -0
- package/dist/reporting/report.js.map +1 -0
- package/dist/reporting/schedule.d.ts +10 -0
- package/dist/reporting/schedule.d.ts.map +1 -0
- package/dist/reporting/schedule.js +34 -0
- package/dist/reporting/schedule.js.map +1 -0
- package/dist/reporting/tables.d.ts +11 -0
- package/dist/reporting/tables.d.ts.map +1 -0
- package/dist/reporting/tables.js +22 -0
- package/dist/reporting/tables.js.map +1 -0
- package/dist/rollout/campaign.d.ts +103 -0
- package/dist/rollout/campaign.d.ts.map +1 -0
- package/dist/rollout/campaign.js +258 -0
- package/dist/rollout/campaign.js.map +1 -0
- package/dist/rollout/rollback.d.ts +51 -0
- package/dist/rollout/rollback.d.ts.map +1 -0
- package/dist/rollout/rollback.js +60 -0
- package/dist/rollout/rollback.js.map +1 -0
- package/dist/rollout/tables.d.ts +19 -0
- package/dist/rollout/tables.d.ts.map +1 -0
- package/dist/rollout/tables.js +65 -0
- package/dist/rollout/tables.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { buildAlert } from '@cogenta/channels';
|
|
2
|
+
/**
|
|
3
|
+
* "Alertes de flotte sur les canaux" (`docs/lots/L8-flotte.md`) — real,
|
|
4
|
+
* operator-facing alerts for the agency, distinct from `../reporting/report.js`'s
|
|
5
|
+
* commercial, end-client-facing monthly reports. Reuses `@cogenta/channels`'s
|
|
6
|
+
* real `buildAlert` constructor (L6 task 6) exactly, the same reuse
|
|
7
|
+
* discipline task 9 already established for `buildReport` — no hand-built
|
|
8
|
+
* `AlertChannelMessage` literal anywhere in this module.
|
|
9
|
+
*/
|
|
10
|
+
const ADMIN_URL_FALLBACK = 'https://admin.invalid/fleet';
|
|
11
|
+
/**
|
|
12
|
+
* A site crossing into `critical` risk (task 5's real `computeSiteRisk`) —
|
|
13
|
+
* "un site avec une CVE critique passe devant" on the dashboard is the same
|
|
14
|
+
* real signal an operator needs pushed to them immediately, not only seen
|
|
15
|
+
* next time they open it.
|
|
16
|
+
*/
|
|
17
|
+
export function detectCriticalRiskAlert(risk) {
|
|
18
|
+
if (risk.tier !== 'critical')
|
|
19
|
+
return null;
|
|
20
|
+
const topReason = [...risk.reasons].sort((a, b) => b.points - a.points)[0];
|
|
21
|
+
return buildAlert({
|
|
22
|
+
title: `${risk.siteName} — risque critique`,
|
|
23
|
+
severity: 'critical',
|
|
24
|
+
context: topReason === undefined ? 'Risque critique détecté.' : topReason.detail,
|
|
25
|
+
expectedAction: 'Examiner ce site dans le tableau de bord de flotte et agir sans délai.',
|
|
26
|
+
adminUrl: ADMIN_URL_FALLBACK,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A rollout campaign transitioning to `halted` (task 7's real
|
|
31
|
+
* `CampaignRecord`) — "un échec arrête toute la campagne" is exactly the
|
|
32
|
+
* kind of event an operator needs to know about the moment it happens, not
|
|
33
|
+
* discover by noticing an update never finished.
|
|
34
|
+
*/
|
|
35
|
+
export function detectCampaignHaltedAlert(campaign, siteNameById) {
|
|
36
|
+
if (campaign.status !== 'halted')
|
|
37
|
+
return null;
|
|
38
|
+
const failedNames = campaign.failedSiteIds.map((id) => siteNameById.get(id) ?? id).join(', ');
|
|
39
|
+
return buildAlert({
|
|
40
|
+
title: `Campagne "${campaign.componentName}" arrêtée`,
|
|
41
|
+
severity: 'critical',
|
|
42
|
+
context: campaign.haltedReason ??
|
|
43
|
+
`La mise à jour vers ${campaign.targetVersion} a échoué sur : ${failedNames || 'un site'}.`,
|
|
44
|
+
expectedAction: 'Vérifier les sites en échec et envisager un retour arrière.',
|
|
45
|
+
adminUrl: ADMIN_URL_FALLBACK,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* "## Pièges connus": "Les faux positifs de disponibilité. Un réseau
|
|
50
|
+
* instable produit des alertes de panne inexistantes. Exiger plusieurs
|
|
51
|
+
* échecs consécutifs depuis plusieurs points avant d'alerter."
|
|
52
|
+
*
|
|
53
|
+
* This architecture has no active probing at all — "le plan de contrôle
|
|
54
|
+
* n'ouvre jamais de connexion vers un site" (absolute since task 1, reused
|
|
55
|
+
* by task 7's async-verification design). "Plusieurs points" (multiple
|
|
56
|
+
* vantage points) has no honest equivalent here: there is only ONE real
|
|
57
|
+
* observation channel, a site's own periodic telemetry contact. The
|
|
58
|
+
* architecturally-consistent reading of the same anti-flapping INTENT is
|
|
59
|
+
* "require sustained absence across multiple real expected-contact
|
|
60
|
+
* windows" — a real, multi-cycle threshold, not a single missed window.
|
|
61
|
+
*/
|
|
62
|
+
const CONTACT_WINDOW_MS = 24 * 60 * 60 * 1000; // one real daily contact cycle, same reasoning as `../control/state.ts`'s retention and `../rollout/campaign.ts`'s DEFAULT_TIMEOUT_MS
|
|
63
|
+
const MISSED_WINDOWS_THRESHOLD = 3; // a single missed window is real network noise; three consecutive is a real, sustained absence
|
|
64
|
+
export function detectSiteSilentAlert(site, latestSnapshot, now = Date.now) {
|
|
65
|
+
const referenceTime = latestSnapshot === null
|
|
66
|
+
? new Date(site.registeredAt).getTime()
|
|
67
|
+
: new Date(latestSnapshot.ingestedAt).getTime();
|
|
68
|
+
const silentMs = now() - referenceTime;
|
|
69
|
+
const missedWindows = Math.floor(silentMs / CONTACT_WINDOW_MS);
|
|
70
|
+
if (missedWindows < MISSED_WINDOWS_THRESHOLD)
|
|
71
|
+
return null;
|
|
72
|
+
return buildAlert({
|
|
73
|
+
title: `${site.name} — silencieux`,
|
|
74
|
+
severity: 'warning',
|
|
75
|
+
context: `Aucun contact reçu depuis plus de ${missedWindows} jours.`,
|
|
76
|
+
expectedAction: "Vérifier que le site est joignable et que son processus d'envoi fonctionne.",
|
|
77
|
+
adminUrl: ADMIN_URL_FALLBACK,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Wraps any detection function with the real de-duplication layer
|
|
82
|
+
* (`./conditions.js`): the message is only returned — and thus only ever
|
|
83
|
+
* sent — on the check where the condition genuinely transitions from
|
|
84
|
+
* inactive to active. Every subsequent check while it remains active
|
|
85
|
+
* returns `null`, even though the underlying condition still holds.
|
|
86
|
+
*/
|
|
87
|
+
export async function raiseIfNew(conditionStore, siteId, conditionType, message, now) {
|
|
88
|
+
if (message === null) {
|
|
89
|
+
await conditionStore.clear(siteId, conditionType, now);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const { fired } = await conditionStore.raise(siteId, conditionType, now);
|
|
93
|
+
return fired ? message : null;
|
|
94
|
+
}
|
|
95
|
+
export async function dispatchAlert(sender, message) {
|
|
96
|
+
await sender.send(message);
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=alerts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alerts.js","sourceRoot":"","sources":["../../src/alerts/alerts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type DatabaseHandle } from '@cogenta/core';
|
|
2
|
+
/**
|
|
3
|
+
* The three real alert conditions built in this task, closed — a caller
|
|
4
|
+
* cannot invent a fourth condition type without a matching detection
|
|
5
|
+
* function to go with it (`./alerts.js`).
|
|
6
|
+
*/
|
|
7
|
+
export type AlertConditionType = 'critical-risk' | 'campaign-halted' | 'site-silent';
|
|
8
|
+
export interface AlertConditionStore {
|
|
9
|
+
/** `true` when this exact (site, condition) pair has an unresolved, unraised-since-clear alert. */
|
|
10
|
+
isActive(siteId: string, conditionType: AlertConditionType): Promise<boolean>;
|
|
11
|
+
/**
|
|
12
|
+
* Raises the condition if — and only if — it is not already active:
|
|
13
|
+
* de-duplication is the whole point. Returns `{fired: true}` the first
|
|
14
|
+
* time a condition becomes active, `{fired: false}` on every repeat check
|
|
15
|
+
* while it remains active — a caller sends a real alert only when `fired`
|
|
16
|
+
* is `true`, never once per check cycle.
|
|
17
|
+
*/
|
|
18
|
+
raise(siteId: string, conditionType: AlertConditionType, now?: () => number): Promise<{
|
|
19
|
+
readonly fired: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
/** Marks the condition resolved — its next `raise()` fires again, a real "was this cleared" check, never a permanent one-time suppression. */
|
|
22
|
+
clear(siteId: string, conditionType: AlertConditionType, now?: () => number): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export declare function createAlertConditionStore(db: DatabaseHandle, now?: () => number): AlertConditionStore;
|
|
25
|
+
//# sourceMappingURL=conditions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conditions.d.ts","sourceRoot":"","sources":["../../src/alerts/conditions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAA0B,MAAM,eAAe,CAAA;AAG3E;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,eAAe,GAAG,iBAAiB,GAAG,aAAa,CAAA;AAEpF,MAAM,WAAW,mBAAmB;IAClC,mGAAmG;IACnG,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC7E;;;;;;OAMG;IACH,KAAK,CACH,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,kBAAkB,EACjC,GAAG,CAAC,EAAE,MAAM,MAAM,GACjB,OAAO,CAAC;QAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;IACvC,8IAA8I;IAC9I,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,kBAAkB,EAAE,GAAG,CAAC,EAAE,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5F;AAUD,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,cAAc,EAClB,GAAG,GAAE,MAAM,MAAiB,GAC3B,mBAAmB,CA0BrB"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { identifier, newId, sql } from '@cogenta/core';
|
|
2
|
+
import { ALERT_TABLES } from './tables.js';
|
|
3
|
+
export function createAlertConditionStore(db, now = Date.now) {
|
|
4
|
+
const conditions = identifier(ALERT_TABLES.conditions, db.dialect);
|
|
5
|
+
return {
|
|
6
|
+
async isActive(siteId, conditionType) {
|
|
7
|
+
const result = await db.query(sql `
|
|
8
|
+
select id from ${conditions}
|
|
9
|
+
where site_id = ${siteId} and condition_type = ${conditionType} and cleared_at is null`);
|
|
10
|
+
return result.rows.length > 0;
|
|
11
|
+
},
|
|
12
|
+
async raise(siteId, conditionType, clock = now) {
|
|
13
|
+
const alreadyActive = await this.isActive(siteId, conditionType);
|
|
14
|
+
if (alreadyActive)
|
|
15
|
+
return { fired: false };
|
|
16
|
+
await db.query(sql `
|
|
17
|
+
insert into ${conditions} (id, site_id, condition_type, raised_at, cleared_at)
|
|
18
|
+
values (${newId(clock)}, ${siteId}, ${conditionType}, ${new Date(clock()).toISOString()}, ${null})`);
|
|
19
|
+
return { fired: true };
|
|
20
|
+
},
|
|
21
|
+
async clear(siteId, conditionType, clock = now) {
|
|
22
|
+
await db.query(sql `
|
|
23
|
+
update ${conditions} set cleared_at = ${new Date(clock()).toISOString()}
|
|
24
|
+
where site_id = ${siteId} and condition_type = ${conditionType} and cleared_at is null`);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=conditions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conditions.js","sourceRoot":"","sources":["../../src/alerts/conditions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAA;AAC3E,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAoC1C,MAAM,UAAU,yBAAyB,CACvC,EAAkB,EAClB,GAAG,GAAiB,IAAI,CAAC,GAAG;IAE5B,MAAM,UAAU,GAAG,UAAU,CAAC,YAAY,CAAC,UAAU,EAAE,EAAE,CAAC,OAAO,CAAC,CAAA;IAElE,OAAO;QACL,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,aAAa;YAClC,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,KAAK,CAAe,GAAG,CAAA;yBAC5B,UAAU;0BACT,MAAM,yBAAyB,aAAa,yBAAyB,CAAC,CAAA;YAC1F,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAA;QAC/B,CAAC;QAED,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,GAAG,GAAG;YAC5C,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;YAChE,IAAI,aAAa;gBAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;YAC1C,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;sBACF,UAAU;kBACd,KAAK,CAAC,KAAK,CAAC,KAAK,MAAM,KAAK,aAAa,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,WAAW,EAAE,KAAK,IAAI,GAAG,CAAC,CAAA;YACtG,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;QACxB,CAAC;QAED,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,GAAG,GAAG;YAC5C,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iBACP,UAAU,qBAAqB,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,WAAW,EAAE;0BACrD,MAAM,yBAAyB,aAAa,yBAAyB,CAAC,CAAA;QAC5F,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type DatabaseHandle } from '@cogenta/core';
|
|
2
|
+
export declare const ALERT_TABLES: {
|
|
3
|
+
readonly conditions: 'cogenta_fleet_alert_conditions';
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* One row per (site, condition type) currently-or-previously-raised alert —
|
|
7
|
+
* `cleared_at is null` means still active. Same `create table if not exists`
|
|
8
|
+
* pattern as `../reporting/tables.ts`/`../control/tables.ts`. Strictly
|
|
9
|
+
* per-site: no query here ever spans more than one site's row at once,
|
|
10
|
+
* matching every other L8 store's per-site isolation discipline.
|
|
11
|
+
*/
|
|
12
|
+
export declare function ensureAlertTables(db: DatabaseHandle): Promise<void>;
|
|
13
|
+
//# sourceMappingURL=tables.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../src/alerts/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAKpB,MAAM,eAAe,CAAA;AAEtB,eAAO,MAAM,YAAY;aACvB,UAAU,EAAE,gCAAgC;CACpC,CAAA;AAMV;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAWzE"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { identifier, sql, unsafeRaw, } from '@cogenta/core';
|
|
2
|
+
export const ALERT_TABLES = {
|
|
3
|
+
conditions: 'cogenta_fleet_alert_conditions',
|
|
4
|
+
};
|
|
5
|
+
function textColumn(dialect, length) {
|
|
6
|
+
return unsafeRaw(dialect === 'sqlite' ? 'text' : `varchar(${length})`);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* One row per (site, condition type) currently-or-previously-raised alert —
|
|
10
|
+
* `cleared_at is null` means still active. Same `create table if not exists`
|
|
11
|
+
* pattern as `../reporting/tables.ts`/`../control/tables.ts`. Strictly
|
|
12
|
+
* per-site: no query here ever spans more than one site's row at once,
|
|
13
|
+
* matching every other L8 store's per-site isolation discipline.
|
|
14
|
+
*/
|
|
15
|
+
export async function ensureAlertTables(db) {
|
|
16
|
+
const t255 = textColumn(db.dialect, 255);
|
|
17
|
+
const conditions = identifier(ALERT_TABLES.conditions, db.dialect);
|
|
18
|
+
await db.query(sql `
|
|
19
|
+
create table if not exists ${conditions} (
|
|
20
|
+
id ${t255} not null primary key,
|
|
21
|
+
site_id ${t255} not null,
|
|
22
|
+
condition_type ${t255} not null,
|
|
23
|
+
raised_at ${t255} not null,
|
|
24
|
+
cleared_at ${t255}
|
|
25
|
+
)`);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=tables.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tables.js","sourceRoot":"","sources":["../../src/alerts/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,UAAU,EAEV,GAAG,EACH,SAAS,GACV,MAAM,eAAe,CAAA;AAEtB,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,UAAU,EAAE,gCAAgC;CACpC,CAAA;AAEV,SAAS,UAAU,CAAC,OAAwB,EAAE,MAAc;IAC1D,OAAO,SAAS,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,MAAM,GAAG,CAAC,CAAA;AACxE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,EAAkB;IACxD,MAAM,IAAI,GAAG,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;IACxC,MAAM,UAAU,GAAG,UAAU,CAAC,YAAY,CAAC,UAAU,EAAE,EAAE,CAAC,OAAO,CAAC,CAAA;IAClE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU;WAChC,IAAI;gBACC,IAAI;uBACG,IAAI;kBACT,IAAI;mBACH,IAAI;MACjB,CAAC,CAAA;AACP,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type DatabaseHandle } from '@cogenta/core';
|
|
2
|
+
import type { ControlPlaneIdentity } from './identity.js';
|
|
3
|
+
/**
|
|
4
|
+
* The complete, closed set of things a control plane may ever ask a site to
|
|
5
|
+
* do — "## Appairage": "commandes... exécutables uniquement dans une liste
|
|
6
|
+
* blanche d'actions." `update` and `rollback` are named now, ahead of their
|
|
7
|
+
* real execution logic (tasks 7/8), so this task's transport/verification
|
|
8
|
+
* layer is the real, non-bypassable seam those tasks plug handlers into —
|
|
9
|
+
* not a whitelist widened later under a design already shipped.
|
|
10
|
+
*/
|
|
11
|
+
export declare const FLEET_COMMAND_ACTIONS: readonly ['update', 'rollback'];
|
|
12
|
+
export type FleetCommandAction = (typeof FLEET_COMMAND_ACTIONS)[number];
|
|
13
|
+
export interface FleetCommand {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly siteId: string;
|
|
16
|
+
readonly action: FleetCommandAction;
|
|
17
|
+
readonly payload: unknown;
|
|
18
|
+
readonly queuedAt: string;
|
|
19
|
+
}
|
|
20
|
+
export interface SignedFleetCommand {
|
|
21
|
+
readonly command: FleetCommand;
|
|
22
|
+
/** Base64 Ed25519 signature over the command's canonical content, made with the control plane's own private key — never the site's. */
|
|
23
|
+
readonly signatureBase64: string;
|
|
24
|
+
}
|
|
25
|
+
export interface CommandQueueStore {
|
|
26
|
+
/** Real, persisted, strictly per-site — no cross-site query shape exists anywhere in this store. */
|
|
27
|
+
enqueue(siteId: string, action: FleetCommandAction, payload: unknown): Promise<FleetCommand>;
|
|
28
|
+
/**
|
|
29
|
+
* A site's real, pending commands, signed and handed over. Fetching marks
|
|
30
|
+
* every returned command delivered — a site's next fetch never sees it
|
|
31
|
+
* again, closing the "re-fetch the same command forever" gap without
|
|
32
|
+
* pretending to know whether the site went on to execute it (that
|
|
33
|
+
* bookkeeping belongs with tasks 7/8's real execution logic, not here).
|
|
34
|
+
*/
|
|
35
|
+
fetchPending(siteId: string): Promise<readonly SignedFleetCommand[]>;
|
|
36
|
+
}
|
|
37
|
+
export declare function createCommandQueueStore(db: DatabaseHandle, controlPlaneIdentity: ControlPlaneIdentity, now?: () => number): CommandQueueStore;
|
|
38
|
+
//# sourceMappingURL=commands.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/control/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAA0B,MAAM,eAAe,CAAA;AAE3E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AAGzD;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,YAAI,QAAQ,EAAE,UAAU,CAAU,CAAA;AACpE,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEvE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAA;IACnC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;IAC9B,uIAAuI;IACvI,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,oGAAoG;IACpG,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAC5F;;;;;;OAMG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CAAA;CACrE;AAqBD,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,cAAc,EAClB,oBAAoB,EAAE,oBAAoB,EAC1C,GAAG,GAAE,MAAM,MAAiB,GAC3B,iBAAiB,CAiCnB"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { identifier, newId, sql } from '@cogenta/core';
|
|
2
|
+
import { signContent } from '@cogenta/plugins';
|
|
3
|
+
import { CONTROL_TABLES } from './tables.js';
|
|
4
|
+
/**
|
|
5
|
+
* The complete, closed set of things a control plane may ever ask a site to
|
|
6
|
+
* do — "## Appairage": "commandes... exécutables uniquement dans une liste
|
|
7
|
+
* blanche d'actions." `update` and `rollback` are named now, ahead of their
|
|
8
|
+
* real execution logic (tasks 7/8), so this task's transport/verification
|
|
9
|
+
* layer is the real, non-bypassable seam those tasks plug handlers into —
|
|
10
|
+
* not a whitelist widened later under a design already shipped.
|
|
11
|
+
*/
|
|
12
|
+
export const FLEET_COMMAND_ACTIONS = ['update', 'rollback'];
|
|
13
|
+
function toCommand(row) {
|
|
14
|
+
return {
|
|
15
|
+
id: row.id,
|
|
16
|
+
siteId: row.site_id,
|
|
17
|
+
action: row.action,
|
|
18
|
+
payload: JSON.parse(row.payload_json),
|
|
19
|
+
queuedAt: row.queued_at,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function createCommandQueueStore(db, controlPlaneIdentity, now = Date.now) {
|
|
23
|
+
const commands = identifier(CONTROL_TABLES.commands, db.dialect);
|
|
24
|
+
return {
|
|
25
|
+
async enqueue(siteId, action, payload) {
|
|
26
|
+
const id = newId(now);
|
|
27
|
+
const queuedAt = new Date(now()).toISOString();
|
|
28
|
+
await db.query(sql `
|
|
29
|
+
insert into ${commands} (id, site_id, action, payload_json, queued_at, delivered_at)
|
|
30
|
+
values (${id}, ${siteId}, ${action}, ${JSON.stringify(payload)}, ${queuedAt}, ${null})`);
|
|
31
|
+
return { id, siteId, action, payload, queuedAt };
|
|
32
|
+
},
|
|
33
|
+
async fetchPending(siteId) {
|
|
34
|
+
const result = await db.query(sql `
|
|
35
|
+
select id, site_id, action, payload_json, queued_at, delivered_at from ${commands}
|
|
36
|
+
where site_id = ${siteId} and delivered_at is null order by queued_at asc`);
|
|
37
|
+
const pending = result.rows.map(toCommand);
|
|
38
|
+
if (pending.length === 0)
|
|
39
|
+
return [];
|
|
40
|
+
const deliveredAt = new Date(now()).toISOString();
|
|
41
|
+
for (const command of pending) {
|
|
42
|
+
await db.query(sql `update ${commands} set delivered_at = ${deliveredAt} where id = ${command.id}`);
|
|
43
|
+
}
|
|
44
|
+
return pending.map((command) => ({
|
|
45
|
+
command,
|
|
46
|
+
signatureBase64: signContent(command, controlPlaneIdentity.privateKey),
|
|
47
|
+
}));
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/control/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAA;AAC3E,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE9C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAE5C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,QAAQ,EAAE,UAAU,CAAU,CAAA;AAuCpE,SAAS,SAAS,CAAC,GAAe;IAChC,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,MAAM,EAAE,GAAG,CAAC,OAAO;QACnB,MAAM,EAAE,GAAG,CAAC,MAA4B;QACxC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,YAAY,CAAY;QAChD,QAAQ,EAAE,GAAG,CAAC,SAAS;KACxB,CAAA;AACH,CAAC;AAED,MAAM,UAAU,uBAAuB,CACrC,EAAkB,EAClB,oBAA0C,EAC1C,GAAG,GAAiB,IAAI,CAAC,GAAG;IAE5B,MAAM,QAAQ,GAAG,UAAU,CAAC,cAAc,CAAC,QAAQ,EAAE,EAAE,CAAC,OAAO,CAAC,CAAA;IAEhE,OAAO;QACL,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO;YACnC,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,CAAA;YACrB,MAAM,QAAQ,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;YAC9C,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;sBACF,QAAQ;kBACZ,EAAE,KAAK,MAAM,KAAK,MAAM,KAAK,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,QAAQ,KAAK,IAAI,GAAG,CAAC,CAAA;YAC1F,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAA;QAClD,CAAC;QAED,KAAK,CAAC,YAAY,CAAC,MAAM;YACvB,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,KAAK,CAAa,GAAG,CAAA;iFAC8B,QAAQ;0BAC/D,MAAM,kDAAkD,CAAC,CAAA;YAC7E,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;YAC1C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAA;YAEnC,MAAM,WAAW,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;YACjD,KAAK,MAAM,OAAO,IAAI,OAAO,EAAE,CAAC;gBAC9B,MAAM,EAAE,CAAC,KAAK,CACZ,GAAG,CAAA,UAAU,QAAQ,uBAAuB,WAAW,eAAe,OAAO,CAAC,EAAE,EAAE,CACnF,CAAA;YACH,CAAC;YAED,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;gBAC/B,OAAO;gBACP,eAAe,EAAE,WAAW,CAAC,OAAO,EAAE,oBAAoB,CAAC,UAAU,CAAC;aACvE,CAAC,CAAC,CAAA;QACL,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type SigningKeyPair } from '@cogenta/plugins';
|
|
2
|
+
/**
|
|
3
|
+
* The control plane's own real Ed25519 identity — distinct from any site's.
|
|
4
|
+
* A site signs its outgoing telemetry with ITS key (`../agent/sign.ts`); the
|
|
5
|
+
* control plane signs outgoing commands with ITS OWN key, and a site
|
|
6
|
+
* verifies those against the public half recorded here, learned once at
|
|
7
|
+
* pairing time (`../enrollment/store.ts`'s `PairingToken.controlPlanePublicKey`).
|
|
8
|
+
* Two independent keypairs, two independent verification directions — never
|
|
9
|
+
* the same key checked both ways.
|
|
10
|
+
*/
|
|
11
|
+
export type ControlPlaneIdentity = SigningKeyPair;
|
|
12
|
+
export declare function generateControlPlaneIdentity(): ControlPlaneIdentity;
|
|
13
|
+
//# sourceMappingURL=identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/control/identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAE9E;;;;;;;;GAQG;AACH,MAAM,MAAM,oBAAoB,GAAG,cAAc,CAAA;AAEjD,wBAAgB,4BAA4B,IAAI,oBAAoB,CAEnE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/control/identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAuB,MAAM,kBAAkB,CAAA;AAa9E,MAAM,UAAU,4BAA4B;IAC1C,OAAO,sBAAsB,EAAE,CAAA;AACjC,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { SignedTelemetry } from '../agent/sign.js';
|
|
2
|
+
import type { EnrollmentStore } from '../enrollment/store.js';
|
|
3
|
+
import type { SiteStateStore, TelemetrySnapshot } from './state.js';
|
|
4
|
+
/**
|
|
5
|
+
* "Le plan de contrôle est un observateur... pas un propriétaire" only holds
|
|
6
|
+
* if it never trusts a sender's own claims — a discriminated result, not a
|
|
7
|
+
* raw exception, matching every other "here's exactly why this was refused"
|
|
8
|
+
* shape this session's L6/L7 forks established (`@cogenta/channels`' link
|
|
9
|
+
* codes, `@cogenta/plugins`' registries).
|
|
10
|
+
*/
|
|
11
|
+
export type IngestResult = {
|
|
12
|
+
readonly ok: true;
|
|
13
|
+
readonly snapshot: TelemetrySnapshot;
|
|
14
|
+
} | {
|
|
15
|
+
readonly ok: false;
|
|
16
|
+
readonly reason: 'unknown_site' | 'revoked' | 'invalid_signature' | 'forbidden_field';
|
|
17
|
+
readonly message: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The control plane's real ingestion boundary — the receiving-side half of
|
|
21
|
+
* "## Ce qui remonte, et ce qui ne remonte pas"'s "critère d'acceptation
|
|
22
|
+
* testé, pas une intention." Three independent checks, in this order:
|
|
23
|
+
*
|
|
24
|
+
* 1. **The claimed site is actually paired, and not revoked** — cheap, no
|
|
25
|
+
* cryptography needed, and refusing here first means an unpaired or
|
|
26
|
+
* revoked sender never gets far enough to learn whether its signature
|
|
27
|
+
* would otherwise have been accepted. Revocation is checked as its own
|
|
28
|
+
* condition, never folded into "does the signature verify": a site
|
|
29
|
+
* paired before being revoked still holds a cryptographically valid
|
|
30
|
+
* keypair, so signature validity alone can never be what keeps a revoked
|
|
31
|
+
* site out.
|
|
32
|
+
* 2. **The signature genuinely verifies against THAT site's registered
|
|
33
|
+
* public key** (`../enrollment/store.js`, task 1) — a payload signed
|
|
34
|
+
* with any other key, or tampered with after signing, is rejected here.
|
|
35
|
+
* 3. **The payload, independently re-inspected on receipt, still contains
|
|
36
|
+
* only the real, closed shape** (`assertNoForbiddenFields`, task 2's own
|
|
37
|
+
* defense-in-depth check, re-run here rather than trusted from the
|
|
38
|
+
* sender) — a compromised or buggy site is exactly the threat model this
|
|
39
|
+
* step defends against; trusting that a sender's own type system kept it
|
|
40
|
+
* honest is not a real security boundary.
|
|
41
|
+
*
|
|
42
|
+
* A rejection at any step never touches `stateStore` — a partially-applied,
|
|
43
|
+
* unverified update is worse than none.
|
|
44
|
+
*/
|
|
45
|
+
export declare function ingestTelemetry(signed: SignedTelemetry, enrollmentStore: EnrollmentStore, stateStore: SiteStateStore): Promise<IngestResult>;
|
|
46
|
+
//# sourceMappingURL=ingest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../../src/control/ingest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAEvD,OAAO,KAAK,EAAE,eAAe,EAAoB,MAAM,wBAAwB,CAAA;AAC/E,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEnE;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAA;CAAE,GAC3D;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAA;IAClB,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,GAAG,mBAAmB,GAAG,iBAAiB,CAAA;IACrF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB,CAAA;AAEL;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,eAAe,EACvB,eAAe,EAAE,eAAe,EAChC,UAAU,EAAE,cAAc,GACzB,OAAO,CAAC,YAAY,CAAC,CA6BvB"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { CogentaError } from '@cogenta/core';
|
|
2
|
+
import { assertNoForbiddenFields } from '../agent/assert.js';
|
|
3
|
+
import { verifyTelemetrySignature } from '../agent/sign.js';
|
|
4
|
+
/**
|
|
5
|
+
* The control plane's real ingestion boundary — the receiving-side half of
|
|
6
|
+
* "## Ce qui remonte, et ce qui ne remonte pas"'s "critère d'acceptation
|
|
7
|
+
* testé, pas une intention." Three independent checks, in this order:
|
|
8
|
+
*
|
|
9
|
+
* 1. **The claimed site is actually paired, and not revoked** — cheap, no
|
|
10
|
+
* cryptography needed, and refusing here first means an unpaired or
|
|
11
|
+
* revoked sender never gets far enough to learn whether its signature
|
|
12
|
+
* would otherwise have been accepted. Revocation is checked as its own
|
|
13
|
+
* condition, never folded into "does the signature verify": a site
|
|
14
|
+
* paired before being revoked still holds a cryptographically valid
|
|
15
|
+
* keypair, so signature validity alone can never be what keeps a revoked
|
|
16
|
+
* site out.
|
|
17
|
+
* 2. **The signature genuinely verifies against THAT site's registered
|
|
18
|
+
* public key** (`../enrollment/store.js`, task 1) — a payload signed
|
|
19
|
+
* with any other key, or tampered with after signing, is rejected here.
|
|
20
|
+
* 3. **The payload, independently re-inspected on receipt, still contains
|
|
21
|
+
* only the real, closed shape** (`assertNoForbiddenFields`, task 2's own
|
|
22
|
+
* defense-in-depth check, re-run here rather than trusted from the
|
|
23
|
+
* sender) — a compromised or buggy site is exactly the threat model this
|
|
24
|
+
* step defends against; trusting that a sender's own type system kept it
|
|
25
|
+
* honest is not a real security boundary.
|
|
26
|
+
*
|
|
27
|
+
* A rejection at any step never touches `stateStore` — a partially-applied,
|
|
28
|
+
* unverified update is worse than none.
|
|
29
|
+
*/
|
|
30
|
+
export async function ingestTelemetry(signed, enrollmentStore, stateStore) {
|
|
31
|
+
const siteId = signed.payload.siteId;
|
|
32
|
+
const site = await enrollmentStore.getSite(siteId);
|
|
33
|
+
if (site === null) {
|
|
34
|
+
return { ok: false, reason: 'unknown_site', message: `No paired site "${siteId}".` };
|
|
35
|
+
}
|
|
36
|
+
if (site.revoked) {
|
|
37
|
+
return { ok: false, reason: 'revoked', message: `Site "${siteId}" is revoked.` };
|
|
38
|
+
}
|
|
39
|
+
if (!verifyTelemetrySignature(signed, site.publicKey)) {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
reason: 'invalid_signature',
|
|
43
|
+
message: `Telemetry signature for site "${siteId}" does not verify against its registered key.`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
assertNoForbiddenFields(signed.payload);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const message = error instanceof CogentaError ? error.message : 'Telemetry payload failed the shape check.';
|
|
51
|
+
return { ok: false, reason: 'forbidden_field', message };
|
|
52
|
+
}
|
|
53
|
+
const snapshot = await stateStore.recordSnapshot(siteId, signed.payload);
|
|
54
|
+
return { ok: true, snapshot };
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=ingest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ingest.js","sourceRoot":"","sources":["../../src/control/ingest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AAE5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAA;AAmB3D;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAuB,EACvB,eAAgC,EAChC,UAA0B;IAE1B,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAA;IACpC,MAAM,IAAI,GAA4B,MAAM,eAAe,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;IAE3E,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,mBAAmB,MAAM,IAAI,EAAE,CAAA;IACtF,CAAC;IACD,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,MAAM,eAAe,EAAE,CAAA;IAClF,CAAC;IAED,IAAI,CAAC,wBAAwB,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QACtD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,mBAAmB;YAC3B,OAAO,EAAE,iCAAiC,MAAM,+CAA+C;SAChG,CAAA;IACH,CAAC;IAED,IAAI,CAAC;QACH,uBAAuB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACzC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GACX,KAAK,YAAY,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,2CAA2C,CAAA;QAC7F,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAA;IAC1D,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,CAAA;IACxE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;AAC/B,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { SiteRegistration } from '../enrollment/store.js';
|
|
2
|
+
import type { DriftEntry } from '../inventory/drift.js';
|
|
3
|
+
import type { TelemetrySnapshot } from './state.js';
|
|
4
|
+
/**
|
|
5
|
+
* "Tableau de bord de flotte : tri par risque, pas par ordre alphabétique"
|
|
6
|
+
* (`docs/lots/L8-flotte.md`) — real, deterministic scoring over exactly the
|
|
7
|
+
* `TelemetryPayload` fields that have a real data source today
|
|
8
|
+
* (`../agent/types.js`'s own field-by-field doc comments): `openCves`
|
|
9
|
+
* (real, security-agent pipeline) and `adminAccounts`' MFA coverage (real,
|
|
10
|
+
* `@cogenta/auth`). Version drift (`../inventory/drift.js`, real) also
|
|
11
|
+
* contributes. Fields still "shape only" — `availability`, `backups`,
|
|
12
|
+
* `certificateExpiry`, `aggregatedErrors` — contribute NOTHING here: a risk
|
|
13
|
+
* function that weighed a field with no real data source behind it would be
|
|
14
|
+
* fabricating a signal, not computing one. Each of those slots into the
|
|
15
|
+
* score the moment a later task wires up its real data source, without
|
|
16
|
+
* redesigning this function — the weight table below just grows.
|
|
17
|
+
*/
|
|
18
|
+
export type RiskTier = 'critical' | 'high' | 'medium' | 'low';
|
|
19
|
+
export interface RiskReason {
|
|
20
|
+
readonly code: 'cve' | 'mfa-gap' | 'version-drift';
|
|
21
|
+
readonly detail: string;
|
|
22
|
+
readonly points: number;
|
|
23
|
+
}
|
|
24
|
+
export interface SiteRisk {
|
|
25
|
+
readonly siteId: string;
|
|
26
|
+
readonly siteName: string;
|
|
27
|
+
readonly client: string | null;
|
|
28
|
+
readonly score: number;
|
|
29
|
+
readonly tier: RiskTier;
|
|
30
|
+
readonly reasons: readonly RiskReason[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Real, deterministic per-site score — never touches `snapshot`'s
|
|
34
|
+
* shape-only fields. `null` `snapshot` (a paired site that has never
|
|
35
|
+
* reported) is real risk in itself: an unresponsive/never-seen site is not
|
|
36
|
+
* "score zero, all clear."
|
|
37
|
+
*/
|
|
38
|
+
export declare function computeSiteRisk(site: SiteRegistration, snapshot: TelemetrySnapshot | null, driftEntries: readonly DriftEntry[]): SiteRisk;
|
|
39
|
+
/**
|
|
40
|
+
* Sorted highest-risk first — the dashboard's whole reason for existing
|
|
41
|
+
* ("tri par risque, pas par ordre alphabétique"). Ties break on site name,
|
|
42
|
+
* ascending, so the ordering is fully deterministic (never insertion order).
|
|
43
|
+
*/
|
|
44
|
+
export declare function rankSitesByRisk(sites: readonly {
|
|
45
|
+
readonly site: SiteRegistration;
|
|
46
|
+
readonly snapshot: TelemetrySnapshot | null;
|
|
47
|
+
readonly driftEntries: readonly DriftEntry[];
|
|
48
|
+
}[]): readonly SiteRisk[];
|
|
49
|
+
export interface FleetFilter {
|
|
50
|
+
readonly client?: string;
|
|
51
|
+
/** Only sites at or above this tier — "critical" is the narrowest, "low" is everyone. */
|
|
52
|
+
readonly minTier?: RiskTier;
|
|
53
|
+
/** Case-insensitive substring match against site name or client. */
|
|
54
|
+
readonly search?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Real, composable query over an already-ranked list — designed for a
|
|
58
|
+
* hundred sites from the start ("## Pièges connus": "Concevoir directement
|
|
59
|
+
* pour cent"), not a client-side afterthought: a real O(n) predicate pass,
|
|
60
|
+
* the same shape a server-side query would use once a live control plane
|
|
61
|
+
* exists to run one against.
|
|
62
|
+
*/
|
|
63
|
+
export declare function filterRisks(risks: readonly SiteRisk[], filter: FleetFilter): readonly SiteRisk[];
|
|
64
|
+
/** Real grouping by client — `null` (no client set) is its own real group, never dropped or merged into a fake "unknown" label. */
|
|
65
|
+
export declare function groupRisksByClient(risks: readonly SiteRisk[]): ReadonlyMap<string | null, readonly SiteRisk[]>;
|
|
66
|
+
//# sourceMappingURL=risk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"risk.d.ts","sourceRoot":"","sources":["../../src/control/risk.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AACvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEnD;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAA;AAE7D,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,SAAS,GAAG,eAAe,CAAA;IAClD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAA;CACxC;AAuCD;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,gBAAgB,EACtB,QAAQ,EAAE,iBAAiB,GAAG,IAAI,EAClC,YAAY,EAAE,SAAS,UAAU,EAAE,GAClC,QAAQ,CAkDV;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,SAAS;IACd,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAA;IAC/B,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAA;IAC3C,QAAQ,CAAC,YAAY,EAAE,SAAS,UAAU,EAAE,CAAA;CAC7C,EAAE,GACF,SAAS,QAAQ,EAAE,CAIrB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,yFAAyF;IACzF,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAA;IAC3B,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAID;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,EAAE,MAAM,EAAE,WAAW,GAAG,SAAS,QAAQ,EAAE,CAYhG;AAED,mIAAmI;AACnI,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,SAAS,QAAQ,EAAE,GACzB,WAAW,CAAC,MAAM,GAAG,IAAI,EAAE,SAAS,QAAQ,EAAE,CAAC,CAQjD"}
|