@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,492 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* "Latest known state" report for a device — distinct from the time-series
|
|
5
|
+
* metric snapshots. It carries slow-moving signals (OS info, security posture)
|
|
6
|
+
* that don't belong in the per-cycle metric stream.
|
|
7
|
+
*
|
|
8
|
+
* The agent emits one on connect and then periodically. The server persists only
|
|
9
|
+
* the most recent report per device (`devices.report_json`) and fans it out live.
|
|
10
|
+
*
|
|
11
|
+
* Every security field is nullable: collectors are best-effort and shell out to
|
|
12
|
+
* OS tools that may be absent or require privileges. `null` means "unknown".
|
|
13
|
+
*
|
|
14
|
+
* Processes are *not* in the report: they ride along with each metric snapshot
|
|
15
|
+
* (`metricSnapshotSchema.processes`) so every graph point has the process list of
|
|
16
|
+
* that exact instant, and are historised under the same `ts`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* One *program* at sample time, aggregated across every process sharing its name
|
|
21
|
+
* (modern apps are multi-process: a browser splits work across helpers, so a
|
|
22
|
+
* single PID looks idle while the app is busy).
|
|
23
|
+
*
|
|
24
|
+
* Every field beyond CPU/memory is best-effort: the collectors read OS surfaces
|
|
25
|
+
* that may be unavailable on a platform (`threads` on macOS) or unreadable
|
|
26
|
+
* without privileges (`/proc/<pid>/io`, socket→process mapping for other users'
|
|
27
|
+
* processes). Unknown is always `null` — never a misleading `0`. The UI can
|
|
28
|
+
* explain *why* via `report.agent.privileged`.
|
|
29
|
+
*/
|
|
30
|
+
export const reportProcessSchema = z.object({
|
|
31
|
+
name: z.string().min(1).max(128),
|
|
32
|
+
/**
|
|
33
|
+
* Chemin de l'exécutable, et **seconde moitié de la clé d'agrégation**.
|
|
34
|
+
*
|
|
35
|
+
* Agréger sur le seul nom fusionnait deux binaires homonymes rangés à des
|
|
36
|
+
* endroits différents — exactement ce derrière quoi un imposteur se cache.
|
|
37
|
+
* La clé est donc `(name, execPath)`, et deux `nginx` de chemins distincts
|
|
38
|
+
* forment désormais deux entrées, ce qui est l'information utile.
|
|
39
|
+
*
|
|
40
|
+
* `null` = inconnu : agent trop ancien pour le renvoyer, ou chemin illisible
|
|
41
|
+
* faute de droits. Les règles qui en dépendent restent alors muettes plutôt
|
|
42
|
+
* que de conclure dans le vide (invariant 6 de Monitoring).
|
|
43
|
+
*/
|
|
44
|
+
execPath: z.string().max(512).nullable().default(null),
|
|
45
|
+
/**
|
|
46
|
+
* L'exécutable a été effacé du disque mais le processus tourne toujours
|
|
47
|
+
* (`/proc/<pid>/exe` pointe vers un chemin suffixé « (deleted) »).
|
|
48
|
+
*
|
|
49
|
+
* Un des indicateurs les plus francs d'un implant résident en mémoire, et il
|
|
50
|
+
* ne coûte rien : le lien symbolique est déjà lu pour `execPath`. `null` là
|
|
51
|
+
* où la plateforme ne l'expose pas (macOS, Windows).
|
|
52
|
+
*/
|
|
53
|
+
deleted: z.boolean().nullable().default(null),
|
|
54
|
+
/** Number of PIDs aggregated under this name. */
|
|
55
|
+
instances: z.number().int().positive().default(1),
|
|
56
|
+
/** Summed CPU%, cumulative across cores (can exceed 100 — divide by `os.cores`). */
|
|
57
|
+
cpuPercent: z.number().min(0),
|
|
58
|
+
/** Summed resident memory, in bytes. */
|
|
59
|
+
memBytes: z.number().int().nonnegative(),
|
|
60
|
+
/** Summed thread count; null on macOS (`ps` exposes no thread column). */
|
|
61
|
+
threads: z.number().int().nonnegative().nullable().default(null),
|
|
62
|
+
/** Owning OS account (the most frequent one among the aggregated PIDs). */
|
|
63
|
+
user: z.string().max(64).nullable().default(null),
|
|
64
|
+
/** Age of the oldest instance, in seconds. */
|
|
65
|
+
uptimeSeconds: z.number().int().nonnegative().nullable().default(null),
|
|
66
|
+
/** Cumulative bytes read; null when unreadable (privileges) or unsupported. */
|
|
67
|
+
diskReadBytes: z.number().int().nonnegative().nullable().default(null),
|
|
68
|
+
/** Cumulative bytes written; null when unreadable or unsupported. */
|
|
69
|
+
diskWriteBytes: z.number().int().nonnegative().nullable().default(null),
|
|
70
|
+
/**
|
|
71
|
+
* Established connections *to* one of this program's listening ports
|
|
72
|
+
* (inbound) and away from it (outbound). Byte counters per process are not
|
|
73
|
+
* collected: no OS exposes them without eBPF/packet capture.
|
|
74
|
+
*/
|
|
75
|
+
connIn: z.number().int().nonnegative().nullable().default(null),
|
|
76
|
+
connOut: z.number().int().nonnegative().nullable().default(null),
|
|
77
|
+
/** Ports this program listens on (ascending, deduped). */
|
|
78
|
+
listenPorts: z.array(z.number().int().min(0).max(65535)).max(32).default([])
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export type ReportProcess = z.infer<typeof reportProcessSchema>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Per-device process capture mode (set from the UI, pushed to the agent):
|
|
85
|
+
* - `off`: don't collect processes at all (saves the most space);
|
|
86
|
+
* - `top`: only the ~20 heaviest (scored on CPU% + memory%);
|
|
87
|
+
* - `all`: every process.
|
|
88
|
+
*/
|
|
89
|
+
export const processCaptureSchema = z.enum(['off', 'top', 'all']);
|
|
90
|
+
export type ProcessCapture = z.infer<typeof processCaptureSchema>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Kind of a stored process sample = the capture mode in effect when it was
|
|
94
|
+
* taken (`top` or `all`; `off` produces no sample). Recorded per-sample so the
|
|
95
|
+
* UI can label history correctly even after the mode later changes.
|
|
96
|
+
*/
|
|
97
|
+
export const processKindSchema = z.enum(['top', 'all']);
|
|
98
|
+
export type ProcessKind = z.infer<typeof processKindSchema>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A stored process list at one instant. **Read model only**: the agent no longer
|
|
102
|
+
* emits it on its own — processes travel inside `metricSnapshotSchema.processes`
|
|
103
|
+
* so a graph point and its process list always share one `ts`. This is what
|
|
104
|
+
* `metrics.processesAt` returns when reading history back.
|
|
105
|
+
*/
|
|
106
|
+
export const processSampleSchema = z.object({
|
|
107
|
+
ts: z.number().int().positive(),
|
|
108
|
+
kind: processKindSchema,
|
|
109
|
+
processes: z.array(reportProcessSchema).max(2000)
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export type ProcessSample = z.infer<typeof processSampleSchema>;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Contrôle d'accès obligatoire actif sur l'hôte. C'est ce qui borne les dégâts
|
|
116
|
+
* d'un service compromis, d'où sa place dans la posture.
|
|
117
|
+
*/
|
|
118
|
+
export const mandatoryAccessControlSchema = z.enum([
|
|
119
|
+
'selinux-enforcing',
|
|
120
|
+
'selinux-permissive',
|
|
121
|
+
'apparmor',
|
|
122
|
+
'none'
|
|
123
|
+
]);
|
|
124
|
+
export type MandatoryAccessControl = z.infer<typeof mandatoryAccessControlSchema>;
|
|
125
|
+
|
|
126
|
+
/** Security posture of the monitored machine. `null` = could not be determined. */
|
|
127
|
+
export const deviceSecuritySchema = z.object({
|
|
128
|
+
/** Host firewall enabled (macOS ALF / Linux ufw|firewalld). */
|
|
129
|
+
firewall: z.boolean().nullable(),
|
|
130
|
+
/** System volume encrypted (macOS FileVault / Linux LUKS). */
|
|
131
|
+
diskEncryption: z.boolean().nullable(),
|
|
132
|
+
/** System Integrity Protection (macOS only; null elsewhere). */
|
|
133
|
+
sip: z.boolean().nullable(),
|
|
134
|
+
/** Count of pending OS updates (null when not collected, e.g. macOS). */
|
|
135
|
+
pendingUpdates: z.number().int().nonnegative().nullable(),
|
|
136
|
+
/**
|
|
137
|
+
* Correctifs de **sécurité** en attente, distingués du total.
|
|
138
|
+
*
|
|
139
|
+
* La distinction porte toute la valeur du signal : quarante mises à jour
|
|
140
|
+
* dont aucune de sécurité n'est qu'un retard d'entretien, tandis qu'une
|
|
141
|
+
* seule faille non corrigée est une porte. Ces champs sont facultatifs et
|
|
142
|
+
* défaillent à `null` — un agent antérieur à Sentinelle n'en dit rien, et
|
|
143
|
+
* `posture.updates_stale` reste alors muette.
|
|
144
|
+
*/
|
|
145
|
+
pendingSecurityUpdates: z.number().int().nonnegative().nullable().default(null),
|
|
146
|
+
/** Unix ms du dernier contrôle des mises à jour ; sert à mesurer l'ancienneté. */
|
|
147
|
+
updatesCheckedAt: z.number().int().positive().nullable().default(null),
|
|
148
|
+
/** `PermitRootLogin` du serveur SSH ; `null` s'il n'y en a pas, ou config illisible. */
|
|
149
|
+
sshRootLogin: z.boolean().nullable().default(null),
|
|
150
|
+
/** `PasswordAuthentication` du serveur SSH. */
|
|
151
|
+
sshPasswordAuth: z.boolean().nullable().default(null),
|
|
152
|
+
/** SELinux / AppArmor. */
|
|
153
|
+
mandatoryAccessControl: mandatoryAccessControlSchema.nullable().default(null),
|
|
154
|
+
/** Des correctifs déjà installés attendent un redémarrage pour prendre effet. */
|
|
155
|
+
rebootRequired: z.boolean().nullable().default(null)
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export type DeviceSecurity = z.infer<typeof deviceSecuritySchema>;
|
|
159
|
+
|
|
160
|
+
/** One mounted disk/volume, for the per-disk breakdown (multi-disk machines). */
|
|
161
|
+
export const reportDiskSchema = z.object({
|
|
162
|
+
/** Representative mount point (e.g. `/` or `/Volumes/Data`). */
|
|
163
|
+
mount: z.string().min(1).max(256),
|
|
164
|
+
usedBytes: z.number().int().nonnegative(),
|
|
165
|
+
totalBytes: z.number().int().nonnegative()
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
export type ReportDisk = z.infer<typeof reportDiskSchema>;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* One listening socket on the monitored machine.
|
|
172
|
+
*
|
|
173
|
+
* `address` is the bind address (`0.0.0.0`/`::` = all interfaces,
|
|
174
|
+
* `127.0.0.1`/`::1` = loopback only, anything else = one specific interface), so
|
|
175
|
+
* the UI can tell world-exposed ports from local ones and group them per
|
|
176
|
+
* interface. One entry per *bind address*: a dual-stack service legitimately
|
|
177
|
+
* yields two entries (`0.0.0.0:22` and `:::22`) which the UI merges into a
|
|
178
|
+
* single bubble.
|
|
179
|
+
*/
|
|
180
|
+
export const openPortSchema = z.object({
|
|
181
|
+
proto: z.enum(['tcp', 'udp']),
|
|
182
|
+
port: z.number().int().min(0).max(65535),
|
|
183
|
+
address: z.string().max(64),
|
|
184
|
+
/** IPv6 scope id — the interface a link-local socket is bound to (`fe80::1%eth0`). */
|
|
185
|
+
zone: z.string().max(64).nullable().default(null),
|
|
186
|
+
/** Owning process id; null when the mapping needs privileges we don't have. */
|
|
187
|
+
pid: z.number().int().nonnegative().nullable().default(null),
|
|
188
|
+
/** Owning program name; null for the same reason as `pid`. */
|
|
189
|
+
process: z.string().max(128).nullable().default(null)
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
export type OpenPort = z.infer<typeof openPortSchema>;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* One established TCP connection at collection time — the detail behind the
|
|
196
|
+
* `activeConnections` metric (which only carries the count). Addresses are kept
|
|
197
|
+
* as strings so both IPv4 and IPv6 peers display as-is.
|
|
198
|
+
*/
|
|
199
|
+
export const tcpConnectionSchema = z.object({
|
|
200
|
+
localAddress: z.string().max(64),
|
|
201
|
+
localPort: z.number().int().min(0).max(65535),
|
|
202
|
+
remoteAddress: z.string().max(64),
|
|
203
|
+
remotePort: z.number().int().min(0).max(65535)
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
export type TcpConnection = z.infer<typeof tcpConnectionSchema>;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The agent's own runtime identity. Lets the UI explain *why* some best-effort
|
|
210
|
+
* probes are limited — chiefly whether it runs with privileges (root/elevated).
|
|
211
|
+
*/
|
|
212
|
+
export const agentServiceScopeSchema = z.enum(['none', 'user', 'system']);
|
|
213
|
+
export type AgentServiceScope = z.infer<typeof agentServiceScopeSchema>;
|
|
214
|
+
|
|
215
|
+
export const agentInfoSchema = z.object({
|
|
216
|
+
/** Running as root (Unix euid 0) / elevated (Windows). */
|
|
217
|
+
privileged: z.boolean(),
|
|
218
|
+
/** OS account the agent runs as (e.g. `root`, `deploy`). */
|
|
219
|
+
user: z.string().max(128),
|
|
220
|
+
/**
|
|
221
|
+
* How the agent is installed for persistence: `none` (transient run), `user`
|
|
222
|
+
* (per-user autostart, login session) or `system` (root/system service, boot).
|
|
223
|
+
* Optional + defaulted so reports from agents predating this field still parse.
|
|
224
|
+
*/
|
|
225
|
+
serviceScope: agentServiceScopeSchema.default('none'),
|
|
226
|
+
/** True when launched by a service manager (so a self-update just exits to be relaunched). */
|
|
227
|
+
managed: z.boolean().default(false),
|
|
228
|
+
/**
|
|
229
|
+
* Ce que cet agent sait relever, déclaré par lui-même.
|
|
230
|
+
*
|
|
231
|
+
* Sans cette liste, rien ne distingue « la sonde a échoué » d'« un agent
|
|
232
|
+
* trop ancien pour l'avoir ». Les deux rendent `null`, et l'interface
|
|
233
|
+
* afficherait le même vide pour deux situations qui n'appellent pas la même
|
|
234
|
+
* réaction — mettre l'agent à jour, ou aller regarder la machine.
|
|
235
|
+
*
|
|
236
|
+
* On ne peut pas s'en remettre à la version : elle est injectée à la
|
|
237
|
+
* compilation par la CI et vaut `0.0.0` sur une construction locale. Une
|
|
238
|
+
* capacité déclarée est de toute façon plus honnête qu'un numéro dont on
|
|
239
|
+
* déduirait ce qu'il contient.
|
|
240
|
+
*
|
|
241
|
+
* Vide par défaut : un agent antérieur à Sentinelle ne dit rien, et c'est
|
|
242
|
+
* exactement ce qu'il faut comprendre.
|
|
243
|
+
*/
|
|
244
|
+
probes: z.array(z.string().max(32)).max(16).default([])
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
export type AgentInfo = z.infer<typeof agentInfoSchema>;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Processor identity (best-effort, read from `sysinfo`). `frequencyMhz` is the
|
|
251
|
+
* nominal/base frequency the OS reports — `null` when it couldn't be read.
|
|
252
|
+
*/
|
|
253
|
+
export const cpuInfoSchema = z.object({
|
|
254
|
+
/** Brand string, e.g. "Apple M1 Pro" or "Intel(R) Core(TM) i7-1185G7". */
|
|
255
|
+
model: z.string().max(256),
|
|
256
|
+
/** Vendor id (e.g. `GenuineIntel`, `AuthenticAMD`); null when unknown. */
|
|
257
|
+
vendor: z.string().max(128).nullable().default(null),
|
|
258
|
+
/** Physical cores; null when the OS can't report them. */
|
|
259
|
+
physicalCores: z.number().int().nonnegative().nullable().default(null),
|
|
260
|
+
/** Logical cores (threads). */
|
|
261
|
+
logicalCores: z.number().int().nonnegative(),
|
|
262
|
+
/** Nominal/base frequency in MHz; null when unknown. */
|
|
263
|
+
frequencyMhz: z.number().int().nonnegative().nullable().default(null)
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
export type CpuInfo = z.infer<typeof cpuInfoSchema>;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* A network interface's inferred class. Best-effort: derived from the OS hardware
|
|
270
|
+
* port (macOS) or the interface name, so `other`/`virtual` cover anything we
|
|
271
|
+
* can't confidently bucket.
|
|
272
|
+
*/
|
|
273
|
+
export const netInterfaceKindSchema = z.enum([
|
|
274
|
+
'wifi',
|
|
275
|
+
'ethernet',
|
|
276
|
+
'bluetooth',
|
|
277
|
+
'loopback',
|
|
278
|
+
'virtual',
|
|
279
|
+
'other'
|
|
280
|
+
]);
|
|
281
|
+
export type NetInterfaceKind = z.infer<typeof netInterfaceKindSchema>;
|
|
282
|
+
|
|
283
|
+
/** One network interface on the host (name + hardware address + inferred kind). */
|
|
284
|
+
export const netInterfaceSchema = z.object({
|
|
285
|
+
name: z.string().min(1).max(128),
|
|
286
|
+
kind: netInterfaceKindSchema,
|
|
287
|
+
/** MAC address, `null` when unavailable or all-zero (e.g. loopback). */
|
|
288
|
+
mac: z.string().max(64).nullable().default(null),
|
|
289
|
+
/**
|
|
290
|
+
* IP addresses assigned to the interface. Lets the ports view attribute a
|
|
291
|
+
* bind address to the interface it belongs to. Truncated rather than
|
|
292
|
+
* rejected (an interface can carry many addresses), `[]` when unknown.
|
|
293
|
+
*/
|
|
294
|
+
addresses: z
|
|
295
|
+
.preprocess(
|
|
296
|
+
(v) => (Array.isArray(v) ? v.slice(0, 16) : v),
|
|
297
|
+
z.array(z.string().max(64)).max(16)
|
|
298
|
+
)
|
|
299
|
+
.catch([])
|
|
300
|
+
.default([])
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
export type NetInterface = z.infer<typeof netInterfaceSchema>;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Static hardware inventory of the monitored machine — slow-moving facts (CPU,
|
|
307
|
+
* RAM, GPU, connectivity) carried alongside the report. Every list is best-effort
|
|
308
|
+
* and may be empty; `bluetooth` is `null` when no adapter was detected.
|
|
309
|
+
*/
|
|
310
|
+
export const deviceHardwareSchema = z.object({
|
|
311
|
+
cpu: cpuInfoSchema,
|
|
312
|
+
/** Total physical RAM, in bytes. */
|
|
313
|
+
memoryTotalBytes: z.number().int().nonnegative(),
|
|
314
|
+
/** GPU model names (best-effort; may be empty). Truncated, never fatal. */
|
|
315
|
+
gpus: z
|
|
316
|
+
.preprocess(
|
|
317
|
+
(v) => (Array.isArray(v) ? v.slice(0, 16) : v),
|
|
318
|
+
z.array(z.string().max(256)).max(16)
|
|
319
|
+
)
|
|
320
|
+
.catch([])
|
|
321
|
+
.default([]),
|
|
322
|
+
/**
|
|
323
|
+
* Network interfaces (best-effort; may be empty). A container host can expose
|
|
324
|
+
* dozens of virtual `veth*`/`br-*` devices, so an over-long list is *truncated*
|
|
325
|
+
* (and any residual error degrades to `[]`) rather than rejecting the whole
|
|
326
|
+
* report — one noisy field must never drop the agent's entire posture, which is
|
|
327
|
+
* validated at the agent socket's ingress (`deviceReportSchema`).
|
|
328
|
+
*/
|
|
329
|
+
network: z
|
|
330
|
+
.preprocess(
|
|
331
|
+
(v) => (Array.isArray(v) ? v.slice(0, 64) : v),
|
|
332
|
+
z.array(netInterfaceSchema).max(64)
|
|
333
|
+
)
|
|
334
|
+
.catch([])
|
|
335
|
+
.default([]),
|
|
336
|
+
/** Bluetooth adapter descriptor; `null` when none detected. */
|
|
337
|
+
bluetooth: z.string().max(256).nullable().default(null)
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
export type DeviceHardware = z.infer<typeof deviceHardwareSchema>;
|
|
341
|
+
|
|
342
|
+
export const deviceReportSchema = z.object({
|
|
343
|
+
/** Unix ms when this report was collected on the agent. */
|
|
344
|
+
collectedAt: z.number().int().positive(),
|
|
345
|
+
os: z.object({
|
|
346
|
+
name: z.string().min(1).max(64),
|
|
347
|
+
version: z.string().max(64),
|
|
348
|
+
arch: z.string().max(32),
|
|
349
|
+
/** Logical CPU cores, for interpreting the load average (0 = unknown). */
|
|
350
|
+
cores: z.number().int().nonnegative().default(0)
|
|
351
|
+
}),
|
|
352
|
+
security: deviceSecuritySchema,
|
|
353
|
+
/** Per-disk usage (deduped across shared APFS volumes). Empty if unknown. */
|
|
354
|
+
disks: z.array(reportDiskSchema).default([]),
|
|
355
|
+
/**
|
|
356
|
+
* The agent's runtime identity (privilege level + account). `null` on legacy
|
|
357
|
+
* reports stored before this field existed; the agent always sends it now.
|
|
358
|
+
*/
|
|
359
|
+
agent: agentInfoSchema.nullable().default(null),
|
|
360
|
+
/**
|
|
361
|
+
* Static hardware inventory (CPU, RAM, GPU, network, bluetooth). `null` on
|
|
362
|
+
* legacy reports stored before this field existed; the agent always sends it.
|
|
363
|
+
*/
|
|
364
|
+
hardware: deviceHardwareSchema.nullable().default(null),
|
|
365
|
+
/**
|
|
366
|
+
* Listening sockets, one entry per bind address. `null` = not collected
|
|
367
|
+
* (legacy report); `[]` = collected and none found. Sorted by port, capped at
|
|
368
|
+
* 500 by the agent.
|
|
369
|
+
*/
|
|
370
|
+
openPorts: z.array(openPortSchema).max(500).nullable().default(null),
|
|
371
|
+
/**
|
|
372
|
+
* Established TCP connections (the detail behind the `activeConnections`
|
|
373
|
+
* metric). `null` = not collected (legacy report); `[]` = collected and none.
|
|
374
|
+
* Sorted, capped at 500 by the agent.
|
|
375
|
+
*/
|
|
376
|
+
connections: z.array(tcpConnectionSchema).max(500).nullable().default(null)
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
export type DeviceReport = z.infer<typeof deviceReportSchema>;
|
|
380
|
+
|
|
381
|
+
// ─────────────────────── relevés Sentinelle (persistance, auth) ──────────────
|
|
382
|
+
//
|
|
383
|
+
// Deux relevés de plus, volontairement **hors** de `deviceReportSchema`.
|
|
384
|
+
//
|
|
385
|
+
// Le rapport est un « dernier état connu » : le serveur n'en garde qu'un par
|
|
386
|
+
// appareil, écrasé à chaque envoi. Cela convient à la posture, pas à ces
|
|
387
|
+
// deux-là. Le manifeste de persistance est trop gros pour être réécrit en
|
|
388
|
+
// entier chaque heure dans `devices.report_json`, et la fenêtre
|
|
389
|
+
// d'authentification est **additive** — l'écraser perdrait des tentatives, ce
|
|
390
|
+
// qui est précisément ce qu'on cherche à compter.
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Une entrée d'une surface de persistance : l'endroit où un programme s'installe
|
|
394
|
+
* pour survivre au redémarrage.
|
|
395
|
+
*
|
|
396
|
+
* **Jamais le contenu du fichier** — seulement son empreinte et ses métadonnées.
|
|
397
|
+
* C'est ce qui rend la sonde acceptable sur une machine partagée : elle prouve
|
|
398
|
+
* qu'un fichier a changé sans jamais révéler ce qu'il contient, et un `sha256`
|
|
399
|
+
* suffit entièrement au diff que le serveur en fait.
|
|
400
|
+
*/
|
|
401
|
+
export const persistenceEntrySchema = z.object({
|
|
402
|
+
/** Famille d'origine : `cron`, `systemd`, `launchd`, `authorized_keys`, `sudoers`, `run_key`, `scheduled_task`… */
|
|
403
|
+
surface: z.string().min(1).max(48),
|
|
404
|
+
path: z.string().min(1).max(512),
|
|
405
|
+
sha256: z.string().length(64),
|
|
406
|
+
sizeBytes: z.number().int().nonnegative(),
|
|
407
|
+
/** Unix ms de dernière modification ; `null` si le système ne l'expose pas. */
|
|
408
|
+
mtime: z.number().int().nonnegative().nullable().default(null),
|
|
409
|
+
/** Mode POSIX en octal (`0644`) ; `null` sur Windows. */
|
|
410
|
+
mode: z.string().max(8).nullable().default(null),
|
|
411
|
+
/** Propriétaire du fichier ; `null` quand illisible. */
|
|
412
|
+
owner: z.string().max(64).nullable().default(null)
|
|
413
|
+
});
|
|
414
|
+
export type PersistenceEntry = z.infer<typeof persistenceEntrySchema>;
|
|
415
|
+
|
|
416
|
+
/** Plafond d'entrées d'un manifeste, aligné sur la borne de l'agent. */
|
|
417
|
+
export const PERSISTENCE_ENTRY_LIMIT = 2000;
|
|
418
|
+
|
|
419
|
+
export const integrityReportSchema = z.object({
|
|
420
|
+
/** Unix ms de la collecte sur l'agent. */
|
|
421
|
+
collectedAt: z.number().int().positive(),
|
|
422
|
+
entries: z.array(persistenceEntrySchema).max(PERSISTENCE_ENTRY_LIMIT),
|
|
423
|
+
/**
|
|
424
|
+
* Le plafond a été atteint. Le serveur **n'émet alors aucun
|
|
425
|
+
* `persistence.removed`** : un manifeste tronqué ne prouve pas qu'une entrée
|
|
426
|
+
* a disparu, seulement qu'on a cessé de regarder.
|
|
427
|
+
*/
|
|
428
|
+
truncated: z.boolean().default(false)
|
|
429
|
+
});
|
|
430
|
+
export type IntegrityReport = z.infer<typeof integrityReportSchema>;
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Une adresse et ce qu'elle a tenté, sur la fenêtre écoulée.
|
|
434
|
+
*
|
|
435
|
+
* `users` porte les comptes **visés**, pas les comptes d'utilisateurs suivis :
|
|
436
|
+
* savoir qu'une adresse chinoise a essayé `root`, `admin` puis `oracle` est ce
|
|
437
|
+
* qui distingue un balayage automatique d'une erreur de frappe.
|
|
438
|
+
*/
|
|
439
|
+
export const authSourceSchema = z.object({
|
|
440
|
+
address: z.string().min(1).max(64),
|
|
441
|
+
failed: z.number().int().nonnegative(),
|
|
442
|
+
accepted: z.number().int().nonnegative(),
|
|
443
|
+
users: z.array(z.string().max(64)).max(16).default([])
|
|
444
|
+
});
|
|
445
|
+
export type AuthSource = z.infer<typeof authSourceSchema>;
|
|
446
|
+
|
|
447
|
+
/** Une authentification **réussie**, seul événement nominatif qu'on remonte. */
|
|
448
|
+
export const authLoginSchema = z.object({
|
|
449
|
+
user: z.string().max(64),
|
|
450
|
+
address: z.string().max(64).nullable().default(null),
|
|
451
|
+
/** `publickey`, `password`, `keyboard-interactive`, `gssapi`… */
|
|
452
|
+
method: z.string().max(32).nullable().default(null),
|
|
453
|
+
at: z.number().int().positive()
|
|
454
|
+
});
|
|
455
|
+
export type AuthLogin = z.infer<typeof authLoginSchema>;
|
|
456
|
+
|
|
457
|
+
export const AUTH_SOURCE_LIMIT = 50;
|
|
458
|
+
export const AUTH_LOGIN_LIMIT = 50;
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Les issues d'authentification sur une fenêtre glissante.
|
|
462
|
+
*
|
|
463
|
+
* Des **compteurs**, pas un flux de journal : l'agent lit les journaux, en
|
|
464
|
+
* extrait des totaux et une liste bornée d'adresses, et n'envoie que cela. Ce
|
|
465
|
+
* n'est pas une optimisation de taille, c'est la frontière de la feature — un
|
|
466
|
+
* flux brut aurait remonté des lignes de commande sudo et des noms de service,
|
|
467
|
+
* c'est-à-dire l'activité des gens.
|
|
468
|
+
*/
|
|
469
|
+
export const authWindowSchema = z.object({
|
|
470
|
+
/** Bornes de la fenêtre, unix ms. `from` = fin de la fenêtre précédente. */
|
|
471
|
+
from: z.number().int().nonnegative(),
|
|
472
|
+
to: z.number().int().positive(),
|
|
473
|
+
failed: z.number().int().nonnegative(),
|
|
474
|
+
accepted: z.number().int().nonnegative(),
|
|
475
|
+
/** Tentatives visant un compte inexistant : signature d'un balayage. */
|
|
476
|
+
invalidUser: z.number().int().nonnegative(),
|
|
477
|
+
/** Nombre d'élévations sudo, sans les commandes exécutées. */
|
|
478
|
+
sudo: z.number().int().nonnegative(),
|
|
479
|
+
/** Comptes système créés pendant la fenêtre. */
|
|
480
|
+
newAccounts: z.array(z.string().max(64)).max(16).default([]),
|
|
481
|
+
/** Sessions root ouvertes directement. */
|
|
482
|
+
rootLogins: z.number().int().nonnegative().default(0),
|
|
483
|
+
topSources: z.array(authSourceSchema).max(AUTH_SOURCE_LIMIT).default([]),
|
|
484
|
+
logins: z.array(authLoginSchema).max(AUTH_LOGIN_LIMIT).default([]),
|
|
485
|
+
/**
|
|
486
|
+
* La source n'a pas pu être lue (pas de journal, pas les droits). Distinguer
|
|
487
|
+
* « zéro tentative » de « je n'ai pas pu regarder » : sans ce drapeau, une
|
|
488
|
+
* machine aveugle passerait pour une machine tranquille.
|
|
489
|
+
*/
|
|
490
|
+
unavailable: z.boolean().default(false)
|
|
491
|
+
});
|
|
492
|
+
export type AuthWindow = z.infer<typeof authWindowSchema>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Global account role. `admin` may manage every device and confirm/revoke
|
|
5
|
+
* machines belonging to other users; `user` only manages its own devices.
|
|
6
|
+
*/
|
|
7
|
+
export const userRoleSchema = z.enum(['user', 'admin']);
|
|
8
|
+
export type UserRole = z.infer<typeof userRoleSchema>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Password-based encryption ("chiffrement par mot de passe").
|
|
5
|
+
*
|
|
6
|
+
* Uses envelope encryption: every user owns a random per-user Data Encryption
|
|
7
|
+
* Key (DEK) that encrypts all their feature data. The DEK is stored *wrapped*:
|
|
8
|
+
* - mode `server` → wrapped by the server key (feature disabled).
|
|
9
|
+
* - mode `password` → wrapped by a key derived from the user's password
|
|
10
|
+
* (Argon2id) — the server cannot read the data without the live password.
|
|
11
|
+
*
|
|
12
|
+
* Toggling the feature or changing the password only re-wraps the DEK; the
|
|
13
|
+
* encrypted content is never rewritten.
|
|
14
|
+
*/
|
|
15
|
+
export const secrecyStatusSchema = z.object({
|
|
16
|
+
/** True when the DEK is wrapped by the user's password (feature ON). */
|
|
17
|
+
enabled: z.boolean(),
|
|
18
|
+
/** True when the current session has unlocked the DEK in memory. */
|
|
19
|
+
unlocked: z.boolean(),
|
|
20
|
+
/** True when a recovery code can also unwrap the DEK (safety net). */
|
|
21
|
+
recoveryEnabled: z.boolean(),
|
|
22
|
+
/**
|
|
23
|
+
* How long (in seconds) the password stays validated after being entered:
|
|
24
|
+
* within this sliding window encrypted actions don't re-prompt. `0` means the
|
|
25
|
+
* password is required for every action. `null` means the server default.
|
|
26
|
+
*/
|
|
27
|
+
reAuthInterval: z.number().int().min(0).nullable(),
|
|
28
|
+
/**
|
|
29
|
+
* Epoch ms at which the current grace window expires (when the cached DEK
|
|
30
|
+
* will be flushed if no further activity slides it forward). `null` when the
|
|
31
|
+
* session isn't unlocked, when the feature is off, or in "validate on every
|
|
32
|
+
* action" mode — i.e. whenever there is no countdown to display. Lets the
|
|
33
|
+
* topbar timer widget render a live progress bar without guessing the window.
|
|
34
|
+
*/
|
|
35
|
+
unlockedUntil: z.number().int().nullable()
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type SecrecyStatus = z.infer<typeof secrecyStatusSchema>;
|
|
39
|
+
|
|
40
|
+
/** How the DEK is wrapped at rest. Mirrors the `wrap_mode` SQL column. */
|
|
41
|
+
export const secrecyWrapModeSchema = z.enum(['server', 'password']);
|
|
42
|
+
export type SecrecyWrapMode = z.infer<typeof secrecyWrapModeSchema>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Server-only row of `user_secret_keys`. Holds the wrapped DEK and the KDF
|
|
46
|
+
* material; never leaves the server. Salts are stored as raw bytes
|
|
47
|
+
* (`VARBINARY`); the MySQL driver surfaces them as `Buffer`, kept loose here so
|
|
48
|
+
* this shared package stays free of Node typings.
|
|
49
|
+
*/
|
|
50
|
+
export interface UserSecretKeyRow {
|
|
51
|
+
user_id: number;
|
|
52
|
+
dek_wrapped: string;
|
|
53
|
+
/**
|
|
54
|
+
* Second, distinct DEK, always wrapped by the server key — the "open" tier
|
|
55
|
+
* for data that must stay readable without the password. Created lazily on
|
|
56
|
+
* the first open write, so `null` until then. Unaffected by `wrap_mode`.
|
|
57
|
+
*/
|
|
58
|
+
open_dek_wrapped: string | null;
|
|
59
|
+
wrap_mode: SecrecyWrapMode;
|
|
60
|
+
kdf_salt: Uint8Array | null;
|
|
61
|
+
recovery_wrapped: string | null;
|
|
62
|
+
recovery_salt: Uint8Array | null;
|
|
63
|
+
version: number;
|
|
64
|
+
created: number;
|
|
65
|
+
updated: number;
|
|
66
|
+
}
|