@deveye/types 0.15.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/LICENSE +21 -0
- package/README.md +23 -0
- package/package.json +68 -0
- package/src/domain/audience.ts +549 -0
- package/src/domain/backup.ts +355 -0
- package/src/domain/credential.ts +55 -0
- package/src/domain/database.ts +467 -0
- package/src/domain/deploy.ts +231 -0
- package/src/domain/device.ts +172 -0
- package/src/domain/deviceFiles.ts +84 -0
- package/src/domain/deviceLogs.ts +82 -0
- package/src/domain/featureRegistry.ts +392 -0
- package/src/domain/finance.ts +477 -0
- package/src/domain/git.ts +419 -0
- package/src/domain/home.ts +314 -0
- package/src/domain/live.ts +272 -0
- package/src/domain/logs.ts +117 -0
- package/src/domain/mail.ts +394 -0
- package/src/domain/metrics.ts +127 -0
- package/src/domain/note.ts +202 -0
- package/src/domain/notifications.ts +268 -0
- package/src/domain/packages.ts +35 -0
- package/src/domain/password.ts +36 -0
- package/src/domain/presence.ts +21 -0
- package/src/domain/project.ts +168 -0
- package/src/domain/projectBoard.ts +130 -0
- package/src/domain/projectChat.ts +46 -0
- package/src/domain/projectHistory.ts +82 -0
- package/src/domain/projectLink.ts +87 -0
- package/src/domain/projectPlan.ts +68 -0
- package/src/domain/report.ts +492 -0
- package/src/domain/role.ts +8 -0
- package/src/domain/secrecy.ts +66 -0
- package/src/domain/sentinel.ts +623 -0
- package/src/domain/sharing.ts +186 -0
- package/src/domain/syncProtocol.ts +116 -0
- package/src/domain/twoFactor.ts +40 -0
- package/src/domain/uptime.ts +216 -0
- package/src/domain/user.ts +141 -0
- package/src/domain/workspace.ts +56 -0
- package/src/domain/workspaceRole.ts +251 -0
- package/src/features/admin.ts +112 -0
- package/src/features/audience.ts +275 -0
- package/src/features/backup.ts +230 -0
- package/src/features/database.ts +461 -0
- package/src/features/deploy.ts +245 -0
- package/src/features/device.ts +292 -0
- package/src/features/deviceFiles.ts +83 -0
- package/src/features/deviceLogs.ts +36 -0
- package/src/features/deviceTerminal.ts +57 -0
- package/src/features/finance.ts +360 -0
- package/src/features/git.ts +368 -0
- package/src/features/home.ts +32 -0
- package/src/features/live.ts +113 -0
- package/src/features/logs.ts +86 -0
- package/src/features/mail.ts +374 -0
- package/src/features/metrics.ts +185 -0
- package/src/features/note.ts +189 -0
- package/src/features/notify.ts +164 -0
- package/src/features/password.ts +67 -0
- package/src/features/project.ts +709 -0
- package/src/features/registry.ts +103 -0
- package/src/features/secrecy.ts +120 -0
- package/src/features/sentinel.ts +233 -0
- package/src/features/sharing.ts +79 -0
- package/src/features/twoFactor.ts +47 -0
- package/src/features/uptime.ts +186 -0
- package/src/features/user.ts +91 -0
- package/src/features/workspace.ts +200 -0
- package/src/http/auth.ts +94 -0
- package/src/http/device.ts +222 -0
- package/src/http/status.ts +45 -0
- package/src/index.ts +1700 -0
- package/src/protocol/agent.ts +1171 -0
- package/src/protocol/envelope.ts +46 -0
- package/src/protocol/error.ts +27 -0
- package/src/protocol/result.ts +17 -0
- package/src/protocol/version.ts +6 -0
- package/src/sdk/client-ambient.d.ts +238 -0
- package/src/sdk/client.ts +66 -0
- package/src/sdk/ids.ts +25 -0
- package/src/sdk/index.ts +11 -0
- package/src/sdk/manifest.ts +326 -0
- package/src/sdk/providers.ts +48 -0
- package/src/sdk/server.ts +378 -0
- package/src/sdk/testing.ts +179 -0
- package/src/utils/version.ts +28 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sentinelle — surveillance de sécurité des hôtes.
|
|
5
|
+
*
|
|
6
|
+
* Monitoring collecte, Sentinelle interprète. Rien ici n'ouvre une nouvelle
|
|
7
|
+
* sonde de son propre chef : le détecteur travaille sur l'instant que l'agent
|
|
8
|
+
* envoie déjà (`metricSnapshotSchema.processes`), sur le rapport horaire
|
|
9
|
+
* (`deviceReportSchema`) et sur les deux relevés que Sentinelle ajoute
|
|
10
|
+
* (persistance, authentification).
|
|
11
|
+
*
|
|
12
|
+
* ## Ce que Sentinelle ne fait pas
|
|
13
|
+
*
|
|
14
|
+
* Aucun suivi de l'activité des utilisateurs. Les relevés d'authentification
|
|
15
|
+
* portent des **issues** (une tentative a réussi ou échoué, depuis quelle
|
|
16
|
+
* adresse) et jamais ce que quelqu'un fait de sa session ; les relevés de
|
|
17
|
+
* persistance portent des **empreintes** et jamais le contenu d'un fichier.
|
|
18
|
+
* Cette frontière est un choix de conception, pas une limitation technique :
|
|
19
|
+
* l'agent a les droits de lire bien davantage, et ne le fait pas.
|
|
20
|
+
*
|
|
21
|
+
* ## Trois notions, à ne pas confondre
|
|
22
|
+
*
|
|
23
|
+
* - la **ligne de base** (`baselineEntrySchema`) : ce qui a été *observé*. Des
|
|
24
|
+
* faits, remis à jour à chaque instant, sans jugement ;
|
|
25
|
+
* - le **constat** (`findingSchema`) : un écart *jugé* digne d'être montré ;
|
|
26
|
+
* - l'**autorisation** (`allowEntrySchema`) : une décision *humaine*, « ceci est
|
|
27
|
+
* légitime ici ». Volontairement rangée à part de la ligne de base, pour
|
|
28
|
+
* qu'une reconstruction de celle-ci n'efface jamais une décision.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
// ────────────────────────────── gravité & état ──────────────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Gravité d'un constat. Quatre paliers, et pas cinq : au-delà, personne ne sait
|
|
35
|
+
* plus dire ce qui sépare deux voisins, et l'échelle cesse de trier.
|
|
36
|
+
*
|
|
37
|
+
* Le seuil de notification est `high` — voir `SENTINEL_NOTIFY_FROM`.
|
|
38
|
+
*/
|
|
39
|
+
export const findingSeveritySchema = z.enum(['info', 'low', 'high', 'critical']);
|
|
40
|
+
export type FindingSeverity = z.infer<typeof findingSeveritySchema>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Rang numérique d'une gravité. La colonne SQL porte ce rang (et non le nom)
|
|
44
|
+
* pour qu'un `ORDER BY severity DESC` trie juste : `'critical' < 'low'` en ordre
|
|
45
|
+
* lexical, ce qui remonterait exactement l'inverse de ce qu'on veut.
|
|
46
|
+
*/
|
|
47
|
+
export const SEVERITY_RANK: Record<FindingSeverity, number> = {
|
|
48
|
+
info: 1,
|
|
49
|
+
low: 2,
|
|
50
|
+
high: 3,
|
|
51
|
+
critical: 4
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const SEVERITY_BY_RANK: Record<number, FindingSeverity> = {
|
|
55
|
+
1: 'info',
|
|
56
|
+
2: 'low',
|
|
57
|
+
3: 'high',
|
|
58
|
+
4: 'critical'
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** À partir de quelle gravité un constat part sur les canaux de l'espace. */
|
|
62
|
+
export const SENTINEL_NOTIFY_FROM: FindingSeverity = 'high';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Cycle de vie d'un constat.
|
|
66
|
+
*
|
|
67
|
+
* - `open` : ouvert, personne ne l'a encore regardé ;
|
|
68
|
+
* - `acknowledged` : un humain l'a jugé légitime. Une autorisation a été écrite,
|
|
69
|
+
* et la même situation ne rouvrira plus ;
|
|
70
|
+
* - `resolved` : la situation a cessé d'elle-même (le programme a disparu, le
|
|
71
|
+
* port s'est refermé, la posture est redevenue correcte).
|
|
72
|
+
*
|
|
73
|
+
* Il n'y a pas d'état « ignoré sans décision » : masquer sans écrire pourquoi
|
|
74
|
+
* est ce qui transforme une liste de constats en liste morte.
|
|
75
|
+
*/
|
|
76
|
+
export const findingStateSchema = z.enum(['open', 'acknowledged', 'resolved']);
|
|
77
|
+
export type FindingState = z.infer<typeof findingStateSchema>;
|
|
78
|
+
|
|
79
|
+
// ────────────────────────────── catalogue de règles ─────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Les règles, figées. Ajouter une règle est un geste délibéré : son identifiant
|
|
83
|
+
* entre ici, sa métadonnée dans `SENTINEL_RULES`, et le moteur refuse de
|
|
84
|
+
* démarrer si les deux ne coïncident pas.
|
|
85
|
+
*/
|
|
86
|
+
export const sentinelRuleIdSchema = z.enum([
|
|
87
|
+
// Dérive de la ligne de base — n'existent qu'après la fenêtre d'apprentissage.
|
|
88
|
+
'process.new',
|
|
89
|
+
'process.user_changed',
|
|
90
|
+
'process.new_listener',
|
|
91
|
+
'process.resource_anomaly',
|
|
92
|
+
'process.vanished',
|
|
93
|
+
'port.exposed',
|
|
94
|
+
'port.unattributed',
|
|
95
|
+
// Heuristiques d'instant — indépendantes de la ligne de base, actives tout de suite.
|
|
96
|
+
'net.mining_pool',
|
|
97
|
+
'net.shell_outbound',
|
|
98
|
+
'net.connection_spike',
|
|
99
|
+
'exec.suspicious_path',
|
|
100
|
+
'exec.deleted_binary',
|
|
101
|
+
'exec.masquerade',
|
|
102
|
+
// Diff du manifeste de persistance.
|
|
103
|
+
'persistence.added',
|
|
104
|
+
'persistence.modified',
|
|
105
|
+
'persistence.removed',
|
|
106
|
+
// Issues d'authentification.
|
|
107
|
+
'auth.bruteforce',
|
|
108
|
+
'auth.success_after_failures',
|
|
109
|
+
'auth.new_account',
|
|
110
|
+
'auth.root_login',
|
|
111
|
+
// Posture : un défaut de configuration est un constat comme un autre, ce qui
|
|
112
|
+
// lui donne le même cycle de vie et le même chemin de notification.
|
|
113
|
+
'posture.firewall_off',
|
|
114
|
+
'posture.disk_unencrypted',
|
|
115
|
+
'posture.sip_off',
|
|
116
|
+
'posture.updates_stale',
|
|
117
|
+
'posture.ssh_root_login',
|
|
118
|
+
'posture.ssh_password_auth',
|
|
119
|
+
'posture.no_mac',
|
|
120
|
+
'posture.reboot_pending'
|
|
121
|
+
]);
|
|
122
|
+
export type SentinelRuleId = z.infer<typeof sentinelRuleIdSchema>;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* De quelle sonde une règle dépend. Sert à une seule chose, mais elle est
|
|
126
|
+
* essentielle : quand une machine tourne encore sur un agent qui ne renvoie pas
|
|
127
|
+
* la donnée, l'interface doit dire **« pas encore mesuré sur cet appareil »**
|
|
128
|
+
* plutôt que d'afficher un vert rassurant qui ne repose sur rien.
|
|
129
|
+
*/
|
|
130
|
+
export const ruleProbeSchema = z.enum([
|
|
131
|
+
/** Présent depuis toujours : instant métrique + liste de processus. */
|
|
132
|
+
'snapshot',
|
|
133
|
+
/** Présent depuis toujours : rapport horaire. */
|
|
134
|
+
'report',
|
|
135
|
+
/** Chemin d'exécutable et drapeau « binaire supprimé ». */
|
|
136
|
+
'execPath',
|
|
137
|
+
/** Sondes de posture étendues (sshd, MAC, reboot, MAJ de sécurité). */
|
|
138
|
+
'posture',
|
|
139
|
+
/** Manifeste des surfaces de persistance. */
|
|
140
|
+
'integrity',
|
|
141
|
+
/** Fenêtre d'issues d'authentification. */
|
|
142
|
+
'auth'
|
|
143
|
+
]);
|
|
144
|
+
export type RuleProbe = z.infer<typeof ruleProbeSchema>;
|
|
145
|
+
|
|
146
|
+
export interface SentinelRuleMeta {
|
|
147
|
+
/** Gravité par défaut. Le moteur peut la relever, jamais l'inventer. */
|
|
148
|
+
severity: FindingSeverity;
|
|
149
|
+
/** Libellé court, tel qu'il s'affiche en tête de constat. */
|
|
150
|
+
label: string;
|
|
151
|
+
/** Ce que la règle a vu, en une phrase. */
|
|
152
|
+
description: string;
|
|
153
|
+
/** Ce qu'il y a à faire. Vide n'est pas une option : un constat sans conduite à tenir ne sert personne. */
|
|
154
|
+
remediation: string;
|
|
155
|
+
/** La sonde dont elle dépend. */
|
|
156
|
+
probe: RuleProbe;
|
|
157
|
+
/** Vrai si la règle a besoin d'une ligne de base constituée (donc muette pendant l'apprentissage). */
|
|
158
|
+
needsBaseline: boolean;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const SENTINEL_RULES: Record<SentinelRuleId, SentinelRuleMeta> = {
|
|
162
|
+
'process.new': {
|
|
163
|
+
severity: 'low',
|
|
164
|
+
label: 'Nouveau programme',
|
|
165
|
+
description: "Un programme qui n'avait jamais été observé sur cette machine s'y exécute.",
|
|
166
|
+
remediation:
|
|
167
|
+
"Vérifiez son chemin et son compte. S'il vient d'une installation que vous avez faite, marquez-le légitime.",
|
|
168
|
+
probe: 'snapshot',
|
|
169
|
+
needsBaseline: true
|
|
170
|
+
},
|
|
171
|
+
'process.user_changed': {
|
|
172
|
+
severity: 'high',
|
|
173
|
+
label: 'Changement de compte',
|
|
174
|
+
description: "Un programme connu s'exécute désormais sous un autre compte système.",
|
|
175
|
+
remediation:
|
|
176
|
+
"Un passage vers root est la forme d'une élévation de privilèges. Vérifiez qui a modifié le service.",
|
|
177
|
+
probe: 'snapshot',
|
|
178
|
+
needsBaseline: true
|
|
179
|
+
},
|
|
180
|
+
'process.new_listener': {
|
|
181
|
+
severity: 'high',
|
|
182
|
+
label: 'Nouvelle écoute réseau',
|
|
183
|
+
description: "Un programme qui n'écoutait pas le réseau s'est mis à ouvrir un port.",
|
|
184
|
+
remediation: 'Vérifiez le port et son adresse de bind. Fermez-le, ou marquez-le légitime.',
|
|
185
|
+
probe: 'snapshot',
|
|
186
|
+
needsBaseline: true
|
|
187
|
+
},
|
|
188
|
+
'process.resource_anomaly': {
|
|
189
|
+
severity: 'low',
|
|
190
|
+
label: 'Consommation anormale',
|
|
191
|
+
description: 'Un programme consomme durablement bien au-delà de son habitude.',
|
|
192
|
+
remediation:
|
|
193
|
+
"C'est la forme d'un mineur détourné, mais aussi celle d'une fuite mémoire. Regardez l'instant épinglé.",
|
|
194
|
+
probe: 'snapshot',
|
|
195
|
+
needsBaseline: true
|
|
196
|
+
},
|
|
197
|
+
'process.vanished': {
|
|
198
|
+
severity: 'low',
|
|
199
|
+
label: 'Programme disparu',
|
|
200
|
+
description: 'Un programme présent en continu depuis des jours a cessé de tourner.',
|
|
201
|
+
remediation:
|
|
202
|
+
"Si c'est un service, vérifiez qu'il n'a pas été arrêté par quelqu'un d'autre que vous.",
|
|
203
|
+
probe: 'snapshot',
|
|
204
|
+
needsBaseline: true
|
|
205
|
+
},
|
|
206
|
+
'port.exposed': {
|
|
207
|
+
severity: 'high',
|
|
208
|
+
label: 'Port exposé au monde',
|
|
209
|
+
description:
|
|
210
|
+
'Un port vient de se lier à toutes les interfaces, et non à la seule boucle locale.',
|
|
211
|
+
remediation:
|
|
212
|
+
'Restreignez le bind à 127.0.0.1 si le service est local, ou couvrez-le par le pare-feu.',
|
|
213
|
+
probe: 'snapshot',
|
|
214
|
+
needsBaseline: true
|
|
215
|
+
},
|
|
216
|
+
'port.unattributed': {
|
|
217
|
+
severity: 'high',
|
|
218
|
+
label: 'Écoute sans propriétaire',
|
|
219
|
+
description: "Un port est en écoute sans qu'aucun processus n'ait pu lui être rattaché.",
|
|
220
|
+
remediation:
|
|
221
|
+
"L'agent est privilégié, il aurait donc dû trouver le propriétaire. Une écoute anonyme mérite un examen à la main.",
|
|
222
|
+
probe: 'snapshot',
|
|
223
|
+
needsBaseline: false
|
|
224
|
+
},
|
|
225
|
+
'net.mining_pool': {
|
|
226
|
+
severity: 'high',
|
|
227
|
+
label: 'Connexion vers un pool de minage',
|
|
228
|
+
description: 'Une connexion établie vise un port typique des pools de minage.',
|
|
229
|
+
remediation: "Identifiez le programme à l'origine de la connexion sur l'instant épinglé.",
|
|
230
|
+
probe: 'snapshot',
|
|
231
|
+
needsBaseline: false
|
|
232
|
+
},
|
|
233
|
+
'net.shell_outbound': {
|
|
234
|
+
severity: 'critical',
|
|
235
|
+
label: 'Interpréteur avec connexion sortante',
|
|
236
|
+
description: 'Un shell ou un interpréteur maintient une connexion sortante établie.',
|
|
237
|
+
remediation:
|
|
238
|
+
"C'est la forme d'un shell inversé. Sauf script d'administration connu, coupez la connexion et examinez la machine.",
|
|
239
|
+
probe: 'snapshot',
|
|
240
|
+
needsBaseline: false
|
|
241
|
+
},
|
|
242
|
+
'net.connection_spike': {
|
|
243
|
+
severity: 'low',
|
|
244
|
+
label: 'Explosion des connexions',
|
|
245
|
+
description: 'Le nombre de connexions établies dépasse très largement son habitude.',
|
|
246
|
+
remediation:
|
|
247
|
+
"Forme d'un balayage sortant ou d'une machine enrôlée. Regardez quel programme les porte.",
|
|
248
|
+
probe: 'snapshot',
|
|
249
|
+
needsBaseline: true
|
|
250
|
+
},
|
|
251
|
+
'exec.suspicious_path': {
|
|
252
|
+
severity: 'critical',
|
|
253
|
+
label: 'Exécution depuis un répertoire temporaire',
|
|
254
|
+
description: "Un programme s'exécute depuis un répertoire où rien ne devrait être exécuté.",
|
|
255
|
+
remediation:
|
|
256
|
+
"/tmp, /dev/shm et /var/tmp sont les points de chute classiques d'un dropper. Récupérez le binaire avant de tuer le processus.",
|
|
257
|
+
probe: 'execPath',
|
|
258
|
+
needsBaseline: false
|
|
259
|
+
},
|
|
260
|
+
'exec.deleted_binary': {
|
|
261
|
+
severity: 'critical',
|
|
262
|
+
label: 'Binaire disparu du disque',
|
|
263
|
+
description:
|
|
264
|
+
"Un programme tourne alors que plus rien n'existe à l'emplacement de son exécutable.",
|
|
265
|
+
remediation:
|
|
266
|
+
"Un des indicateurs les plus francs d'un implant résident. Copiez /proc/<pid>/exe avant tout redémarrage. À ne pas confondre avec un binaire simplement remplacé par une mise à jour, que l'agent ne signale pas.",
|
|
267
|
+
probe: 'execPath',
|
|
268
|
+
needsBaseline: false
|
|
269
|
+
},
|
|
270
|
+
'exec.masquerade': {
|
|
271
|
+
severity: 'critical',
|
|
272
|
+
label: 'Nom usurpé',
|
|
273
|
+
description:
|
|
274
|
+
"Un programme porte un nom de tâche noyau alors qu'il a un vrai exécutable sur disque.",
|
|
275
|
+
remediation: 'Un fil du noyau ne vient jamais du disque. Examinez le chemin.',
|
|
276
|
+
probe: 'execPath',
|
|
277
|
+
needsBaseline: false
|
|
278
|
+
},
|
|
279
|
+
'persistence.added': {
|
|
280
|
+
severity: 'high',
|
|
281
|
+
label: 'Nouvelle entrée de persistance',
|
|
282
|
+
description:
|
|
283
|
+
"Une entrée est apparue là où un programme s'installe pour survivre au redémarrage.",
|
|
284
|
+
remediation:
|
|
285
|
+
'Lisez le fichier et vérifiez son origine. Marquez-le légitime si vous venez de le créer.',
|
|
286
|
+
probe: 'integrity',
|
|
287
|
+
needsBaseline: true
|
|
288
|
+
},
|
|
289
|
+
'persistence.modified': {
|
|
290
|
+
severity: 'high',
|
|
291
|
+
label: 'Entrée de persistance modifiée',
|
|
292
|
+
description: "L'empreinte d'une entrée de persistance connue a changé.",
|
|
293
|
+
remediation:
|
|
294
|
+
"Comparez avec votre gestion de configuration. Un changement que vous n'avez pas fait est un signal.",
|
|
295
|
+
probe: 'integrity',
|
|
296
|
+
needsBaseline: true
|
|
297
|
+
},
|
|
298
|
+
'persistence.removed': {
|
|
299
|
+
severity: 'low',
|
|
300
|
+
label: 'Entrée de persistance supprimée',
|
|
301
|
+
description: 'Une entrée de persistance connue a disparu.',
|
|
302
|
+
remediation:
|
|
303
|
+
"Souvent une désinstallation. Signalé parce qu'un effacement peut aussi couvrir des traces.",
|
|
304
|
+
probe: 'integrity',
|
|
305
|
+
needsBaseline: true
|
|
306
|
+
},
|
|
307
|
+
'auth.bruteforce': {
|
|
308
|
+
severity: 'high',
|
|
309
|
+
label: 'Tentatives répétées',
|
|
310
|
+
description: "Une même adresse a échoué de nombreuses fois à s'authentifier.",
|
|
311
|
+
remediation:
|
|
312
|
+
"Bloquez l'adresse, et désactivez l'authentification par mot de passe si elle est encore ouverte.",
|
|
313
|
+
probe: 'auth',
|
|
314
|
+
needsBaseline: false
|
|
315
|
+
},
|
|
316
|
+
'auth.success_after_failures': {
|
|
317
|
+
severity: 'critical',
|
|
318
|
+
label: 'Réussite après échecs',
|
|
319
|
+
description:
|
|
320
|
+
"Une authentification a réussi depuis une adresse qui venait d'échouer plusieurs fois.",
|
|
321
|
+
remediation:
|
|
322
|
+
"Traitez la machine comme compromise jusqu'à preuve du contraire. Changez le secret concerné.",
|
|
323
|
+
probe: 'auth',
|
|
324
|
+
needsBaseline: false
|
|
325
|
+
},
|
|
326
|
+
'auth.new_account': {
|
|
327
|
+
severity: 'critical',
|
|
328
|
+
label: 'Compte créé',
|
|
329
|
+
description: 'Un compte système a été créé.',
|
|
330
|
+
remediation:
|
|
331
|
+
"Une création de compte que vous n'avez pas faite est une prise de pied durable.",
|
|
332
|
+
probe: 'auth',
|
|
333
|
+
needsBaseline: false
|
|
334
|
+
},
|
|
335
|
+
'auth.root_login': {
|
|
336
|
+
severity: 'high',
|
|
337
|
+
label: 'Connexion root directe',
|
|
338
|
+
description:
|
|
339
|
+
'Une session root a été ouverte directement, sans passer par un compte nominatif.',
|
|
340
|
+
remediation:
|
|
341
|
+
'Interdisez la connexion root directe et passez par sudo, qui laisse une trace nominative.',
|
|
342
|
+
probe: 'auth',
|
|
343
|
+
needsBaseline: false
|
|
344
|
+
},
|
|
345
|
+
'posture.firewall_off': {
|
|
346
|
+
severity: 'high',
|
|
347
|
+
label: 'Pare-feu désactivé',
|
|
348
|
+
description: "Le pare-feu de l'hôte est éteint.",
|
|
349
|
+
remediation: 'Activez ufw, firewalld ou le pare-feu applicatif selon la plateforme.',
|
|
350
|
+
probe: 'report',
|
|
351
|
+
needsBaseline: false
|
|
352
|
+
},
|
|
353
|
+
'posture.disk_unencrypted': {
|
|
354
|
+
severity: 'high',
|
|
355
|
+
label: 'Disque non chiffré',
|
|
356
|
+
description: "Le volume système n'est pas chiffré.",
|
|
357
|
+
remediation:
|
|
358
|
+
"Sur un poste mobile, c'est le seul rempart en cas de perte. LUKS ou FileVault.",
|
|
359
|
+
probe: 'report',
|
|
360
|
+
needsBaseline: false
|
|
361
|
+
},
|
|
362
|
+
'posture.sip_off': {
|
|
363
|
+
severity: 'high',
|
|
364
|
+
label: 'SIP désactivé',
|
|
365
|
+
description: "La protection de l'intégrité du système est désactivée.",
|
|
366
|
+
remediation: 'Réactivez SIP depuis la recovery, sauf besoin de développement explicite.',
|
|
367
|
+
probe: 'report',
|
|
368
|
+
needsBaseline: false
|
|
369
|
+
},
|
|
370
|
+
'posture.updates_stale': {
|
|
371
|
+
severity: 'high',
|
|
372
|
+
label: 'Mises à jour de sécurité en attente',
|
|
373
|
+
description: 'Des correctifs de sécurité attendent depuis trop longtemps.',
|
|
374
|
+
remediation:
|
|
375
|
+
'Appliquez-les. Un correctif publié est aussi une carte offerte à qui sait lire un journal de version.',
|
|
376
|
+
probe: 'posture',
|
|
377
|
+
needsBaseline: false
|
|
378
|
+
},
|
|
379
|
+
'posture.ssh_root_login': {
|
|
380
|
+
severity: 'high',
|
|
381
|
+
label: 'SSH autorise root',
|
|
382
|
+
description: 'Le serveur SSH accepte les connexions directes du compte root.',
|
|
383
|
+
remediation: 'Passez PermitRootLogin à no, puis rechargez sshd.',
|
|
384
|
+
probe: 'posture',
|
|
385
|
+
needsBaseline: false
|
|
386
|
+
},
|
|
387
|
+
'posture.ssh_password_auth': {
|
|
388
|
+
severity: 'high',
|
|
389
|
+
label: 'SSH autorise les mots de passe',
|
|
390
|
+
description: "Le serveur SSH accepte l'authentification par mot de passe.",
|
|
391
|
+
remediation:
|
|
392
|
+
'Passez aux clés et mettez PasswordAuthentication à no. Un mot de passe se devine, pas une clé.',
|
|
393
|
+
probe: 'posture',
|
|
394
|
+
needsBaseline: false
|
|
395
|
+
},
|
|
396
|
+
'posture.no_mac': {
|
|
397
|
+
severity: 'low',
|
|
398
|
+
label: 'Aucun contrôle d’accès obligatoire',
|
|
399
|
+
description: "Ni SELinux ni AppArmor n'est actif.",
|
|
400
|
+
remediation:
|
|
401
|
+
"Activez-en un si la distribution en fournit un : c'est ce qui limite les dégâts d'un service compromis.",
|
|
402
|
+
probe: 'posture',
|
|
403
|
+
needsBaseline: false
|
|
404
|
+
},
|
|
405
|
+
'posture.reboot_pending': {
|
|
406
|
+
severity: 'low',
|
|
407
|
+
label: 'Redémarrage requis',
|
|
408
|
+
description:
|
|
409
|
+
'Un redémarrage est requis pour que des correctifs déjà installés prennent effet.',
|
|
410
|
+
remediation:
|
|
411
|
+
"Un noyau corrigé mais non redémarré n'est pas un noyau corrigé. Planifiez le redémarrage.",
|
|
412
|
+
probe: 'posture',
|
|
413
|
+
needsBaseline: false
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/** Les règles muettes tant que la ligne de base n'est pas constituée. */
|
|
418
|
+
export const BASELINE_RULES: SentinelRuleId[] = (
|
|
419
|
+
Object.keys(SENTINEL_RULES) as SentinelRuleId[]
|
|
420
|
+
).filter((id) => SENTINEL_RULES[id].needsBaseline);
|
|
421
|
+
|
|
422
|
+
// ────────────────────────────── ligne de base ───────────────────────────────
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Nature d'une entrée de ligne de base.
|
|
426
|
+
*
|
|
427
|
+
* - `process` : un couple (nom, chemin d'exécutable) ;
|
|
428
|
+
* - `listener` : un port en écoute, avec sa joignabilité ;
|
|
429
|
+
* - `persistence` : un fichier d'une surface de persistance, avec son empreinte ;
|
|
430
|
+
* - `account` : un compte système vu se connecter.
|
|
431
|
+
*/
|
|
432
|
+
export const baselineKindSchema = z.enum(['process', 'listener', 'persistence', 'account']);
|
|
433
|
+
export type BaselineKind = z.infer<typeof baselineKindSchema>;
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Ce qu'on a retenu d'un élément observé, au-delà de sa simple existence.
|
|
437
|
+
*
|
|
438
|
+
* Tous les champs sont facultatifs : une entrée `listener` n'a pas d'enveloppe
|
|
439
|
+
* CPU, une entrée `persistence` n'a pas de comptes. Un seul schéma plutôt que
|
|
440
|
+
* quatre parce que le stockage est une colonne JSON unique, et qu'un
|
|
441
|
+
* discriminant y coûterait plus de code qu'il n'en éviterait.
|
|
442
|
+
*/
|
|
443
|
+
export const baselineAttrsSchema = z.object({
|
|
444
|
+
/** Comptes système sous lesquels l'élément a été vu (dédupliqués, bornés). */
|
|
445
|
+
users: z.array(z.string().max(64)).max(16).default([]),
|
|
446
|
+
/** Ports que ce programme a été vu ouvrir. */
|
|
447
|
+
listenPorts: z.array(z.number().int().min(0).max(65535)).max(64).default([]),
|
|
448
|
+
/** Enveloppe de consommation : p95 observé, base de `process.resource_anomaly`. */
|
|
449
|
+
cpuP95: z.number().nullable().default(null),
|
|
450
|
+
memP95: z.number().int().nonnegative().nullable().default(null),
|
|
451
|
+
/** Empreinte du fichier, pour les entrées `persistence`. */
|
|
452
|
+
sha256: z.string().max(64).nullable().default(null),
|
|
453
|
+
/** Surface de persistance d'origine (`cron`, `systemd`, `authorized_keys`…). */
|
|
454
|
+
surface: z.string().max(48).nullable().default(null)
|
|
455
|
+
});
|
|
456
|
+
export type BaselineAttrs = z.infer<typeof baselineAttrsSchema>;
|
|
457
|
+
|
|
458
|
+
export const baselineEntrySchema = z.object({
|
|
459
|
+
kind: baselineKindSchema,
|
|
460
|
+
/** Clé stable de l'élément : `nginx|/usr/sbin/nginx`, `tcp/0.0.0.0:22`, `/etc/cron.d/x`. */
|
|
461
|
+
key: z.string().max(512),
|
|
462
|
+
/** Unix ms. */
|
|
463
|
+
firstSeen: z.number().int().positive(),
|
|
464
|
+
lastSeen: z.number().int().positive(),
|
|
465
|
+
/** Nombre d'instants où l'élément a été vu. */
|
|
466
|
+
samples: z.number().int().nonnegative(),
|
|
467
|
+
attrs: baselineAttrsSchema,
|
|
468
|
+
/** Vrai si une autorisation humaine couvre cet élément. */
|
|
469
|
+
allowed: z.boolean().default(false)
|
|
470
|
+
});
|
|
471
|
+
export type BaselineEntry = z.infer<typeof baselineEntrySchema>;
|
|
472
|
+
|
|
473
|
+
// ────────────────────────────────── constats ─────────────────────────────────
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* La preuve, déjà mise en forme par le serveur.
|
|
477
|
+
*
|
|
478
|
+
* Volontairement des paires libellé/valeur et non le JSON brut de la règle :
|
|
479
|
+
* l'interface n'a alors aucun rendu ad hoc à écrire par règle, et une règle
|
|
480
|
+
* nouvelle s'affiche correctement le jour où elle est écrite. La contrepartie
|
|
481
|
+
* assumée est que la preuve n'est pas requêtable — c'est `subject` qui porte la
|
|
482
|
+
* clé machine, et c'est lui qui sert au dédoublonnage et aux autorisations.
|
|
483
|
+
*/
|
|
484
|
+
export const evidenceItemSchema = z.object({
|
|
485
|
+
label: z.string().max(64),
|
|
486
|
+
value: z.string().max(512)
|
|
487
|
+
});
|
|
488
|
+
export type EvidenceItem = z.infer<typeof evidenceItemSchema>;
|
|
489
|
+
|
|
490
|
+
export const findingSchema = z.object({
|
|
491
|
+
id: z.number().int().positive(),
|
|
492
|
+
deviceId: z.string().max(36),
|
|
493
|
+
/** Nom de l'appareil au moment de la lecture, pour éviter une jointure côté client. */
|
|
494
|
+
deviceName: z.string().max(128),
|
|
495
|
+
rule: sentinelRuleIdSchema,
|
|
496
|
+
severity: findingSeveritySchema,
|
|
497
|
+
state: findingStateSchema,
|
|
498
|
+
/** Ce sur quoi porte le constat : un programme, un port, un chemin, une adresse. */
|
|
499
|
+
subject: z.string().max(512),
|
|
500
|
+
evidence: z.array(evidenceItemSchema).max(24),
|
|
501
|
+
/**
|
|
502
|
+
* L'instant épinglé qui porte la preuve, en unix ms. `null` pour les constats
|
|
503
|
+
* qui ne naissent pas d'un instant (posture, persistance, authentification).
|
|
504
|
+
*/
|
|
505
|
+
snapshotTs: z.number().int().positive().nullable(),
|
|
506
|
+
firstSeen: z.number().int().positive(),
|
|
507
|
+
lastSeen: z.number().int().positive(),
|
|
508
|
+
occurrences: z.number().int().positive(),
|
|
509
|
+
ackedBy: z.number().int().positive().nullable(),
|
|
510
|
+
ackedAt: z.number().int().positive().nullable()
|
|
511
|
+
});
|
|
512
|
+
export type Finding = z.infer<typeof findingSchema>;
|
|
513
|
+
|
|
514
|
+
/** Portée d'un acquittement : cette machine, ou toute la flotte de l'espace. */
|
|
515
|
+
export const allowScopeSchema = z.enum(['device', 'fleet']);
|
|
516
|
+
export type AllowScope = z.infer<typeof allowScopeSchema>;
|
|
517
|
+
|
|
518
|
+
export const allowEntrySchema = z.object({
|
|
519
|
+
id: z.number().int().positive(),
|
|
520
|
+
/** `null` = toute la flotte de l'espace. */
|
|
521
|
+
deviceId: z.string().max(36).nullable(),
|
|
522
|
+
deviceName: z.string().max(128).nullable(),
|
|
523
|
+
rule: sentinelRuleIdSchema,
|
|
524
|
+
subject: z.string().max(512),
|
|
525
|
+
reason: z.string().max(255).nullable(),
|
|
526
|
+
createdBy: z.number().int().positive(),
|
|
527
|
+
created: z.number().int().positive()
|
|
528
|
+
});
|
|
529
|
+
export type AllowEntry = z.infer<typeof allowEntrySchema>;
|
|
530
|
+
|
|
531
|
+
// ──────────────────────────────── posture ────────────────────────────────────
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Un contrôle de posture tel qu'il s'affiche.
|
|
535
|
+
*
|
|
536
|
+
* `status` distingue trois issues, et la troisième est celle qui compte :
|
|
537
|
+
* `unknown` veut dire « la sonde n'a rien pu dire », jamais « tout va bien ».
|
|
538
|
+
* Afficher un vert sur une sonde absente est la façon la plus sûre de donner
|
|
539
|
+
* une fausse assurance — c'est l'invariant 6 de Monitoring, appliqué ici.
|
|
540
|
+
*/
|
|
541
|
+
export const postureStatusSchema = z.enum(['ok', 'fail', 'unknown', 'not_applicable']);
|
|
542
|
+
export type PostureStatus = z.infer<typeof postureStatusSchema>;
|
|
543
|
+
|
|
544
|
+
export const postureCheckSchema = z.object({
|
|
545
|
+
rule: sentinelRuleIdSchema,
|
|
546
|
+
status: postureStatusSchema,
|
|
547
|
+
label: z.string().max(64),
|
|
548
|
+
/** Détail mesuré, quand il y en a un : « 12 correctifs, le plus ancien depuis 34 j ». */
|
|
549
|
+
detail: z.string().max(255).nullable().default(null)
|
|
550
|
+
});
|
|
551
|
+
export type PostureCheck = z.infer<typeof postureCheckSchema>;
|
|
552
|
+
|
|
553
|
+
export const devicePostureSchema = z.object({
|
|
554
|
+
deviceId: z.string().max(36),
|
|
555
|
+
deviceName: z.string().max(128),
|
|
556
|
+
/**
|
|
557
|
+
* Score sur 100, calculé sur les seuls contrôles **concluants**. Un contrôle
|
|
558
|
+
* `unknown` ne compte ni en bien ni en mal : le diluer dans une moyenne
|
|
559
|
+
* reviendrait à récompenser une machine qui ne mesure rien.
|
|
560
|
+
*/
|
|
561
|
+
score: z.number().int().min(0).max(100).nullable(),
|
|
562
|
+
checks: z.array(postureCheckSchema).max(32)
|
|
563
|
+
});
|
|
564
|
+
export type DevicePosture = z.infer<typeof devicePostureSchema>;
|
|
565
|
+
|
|
566
|
+
// ──────────────────────────────── réglages ───────────────────────────────────
|
|
567
|
+
|
|
568
|
+
/** Bornes de la fenêtre d'apprentissage, en jours. */
|
|
569
|
+
export const SENTINEL_LEARNING_DAYS_MIN = 1;
|
|
570
|
+
export const SENTINEL_LEARNING_DAYS_MAX = 90;
|
|
571
|
+
export const DEFAULT_SENTINEL_LEARNING_DAYS = 7;
|
|
572
|
+
|
|
573
|
+
/** Bornes de la cadence du relevé de persistance, en minutes. */
|
|
574
|
+
export const SENTINEL_INTEGRITY_MINUTES_MIN = 15;
|
|
575
|
+
export const SENTINEL_INTEGRITY_MINUTES_MAX = 1440;
|
|
576
|
+
export const DEFAULT_SENTINEL_INTEGRITY_MINUTES = 360;
|
|
577
|
+
|
|
578
|
+
/** Combien de jours un constat résolu est conservé avant balayage. */
|
|
579
|
+
export const DEFAULT_SENTINEL_FINDING_RETENTION_DAYS = 180;
|
|
580
|
+
|
|
581
|
+
export const sentinelConfigSchema = z.object({
|
|
582
|
+
enabled: z.boolean(),
|
|
583
|
+
/**
|
|
584
|
+
* Fin de la fenêtre d'apprentissage, en unix ms. `null` quand Sentinelle n'a
|
|
585
|
+
* jamais été activée ; une date passée signifie « apprentissage terminé ».
|
|
586
|
+
*/
|
|
587
|
+
learningUntil: z.number().int().positive().nullable(),
|
|
588
|
+
integrityMinutes: z
|
|
589
|
+
.number()
|
|
590
|
+
.int()
|
|
591
|
+
.min(SENTINEL_INTEGRITY_MINUTES_MIN)
|
|
592
|
+
.max(SENTINEL_INTEGRITY_MINUTES_MAX),
|
|
593
|
+
authEvents: z.boolean()
|
|
594
|
+
});
|
|
595
|
+
export type SentinelConfig = z.infer<typeof sentinelConfigSchema>;
|
|
596
|
+
|
|
597
|
+
// ──────────────────────────────── vue d'ensemble ─────────────────────────────
|
|
598
|
+
|
|
599
|
+
/** Décompte des constats ouverts par gravité. */
|
|
600
|
+
export const severityCountsSchema = z.object({
|
|
601
|
+
info: z.number().int().nonnegative(),
|
|
602
|
+
low: z.number().int().nonnegative(),
|
|
603
|
+
high: z.number().int().nonnegative(),
|
|
604
|
+
critical: z.number().int().nonnegative()
|
|
605
|
+
});
|
|
606
|
+
export type SeverityCounts = z.infer<typeof severityCountsSchema>;
|
|
607
|
+
|
|
608
|
+
/** L'état d'une machine dans la vue de flotte. */
|
|
609
|
+
export const deviceSentinelStateSchema = z.object({
|
|
610
|
+
deviceId: z.string().max(36),
|
|
611
|
+
deviceName: z.string().max(128),
|
|
612
|
+
enabled: z.boolean(),
|
|
613
|
+
/** Vrai tant que la fenêtre d'apprentissage court. */
|
|
614
|
+
learning: z.boolean(),
|
|
615
|
+
learningUntil: z.number().int().positive().nullable(),
|
|
616
|
+
open: severityCountsSchema,
|
|
617
|
+
postureScore: z.number().int().min(0).max(100).nullable(),
|
|
618
|
+
/** Sondes dont on a effectivement reçu de la donnée — ce qui n'est pas mesuré se dit. */
|
|
619
|
+
probes: z.array(ruleProbeSchema).max(8),
|
|
620
|
+
/** Dernier relevé de persistance reçu, unix ms. */
|
|
621
|
+
lastIntegrityAt: z.number().int().positive().nullable()
|
|
622
|
+
});
|
|
623
|
+
export type DeviceSentinelState = z.infer<typeof deviceSentinelStateSchema>;
|