@nosserb/prmt 0.0.0-stage → 2.0.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 +191 -2
- package/bin/prmt.mjs +174 -0
- package/dist/assets/AuditModule-BOO_iRy8.js +1 -0
- package/dist/assets/Audits-yUBm0EpW.js +1 -0
- package/dist/assets/BrainPage-FIWO_9Nc.js +3 -0
- package/dist/assets/BrainPage-XQCEGRc5.css +1 -0
- package/dist/assets/Carte-CPuiR1Ha.css +1 -0
- package/dist/assets/Carte-xycu1mQn.js +1 -0
- package/dist/assets/Dashboard-DKvCQcX8.js +2 -0
- package/dist/assets/Dashboard-XdhC6_hM.css +1 -0
- package/dist/assets/Field-C7c2mCbb.js +2 -0
- package/dist/assets/Journal-BLIIlh80.css +1 -0
- package/dist/assets/Journal-BaelFjwV.js +1 -0
- package/dist/assets/Landing-B2jxV3zs.css +1 -0
- package/dist/assets/Landing-Dr612Zdu.js +26 -0
- package/dist/assets/Register-DA_Aq5f8.js +1 -0
- package/dist/assets/Reports-8wfDNSxH.js +1 -0
- package/dist/assets/Reports-B8GXN95N.css +1 -0
- package/dist/assets/SavoirPage-BTxbH3LY.css +1 -0
- package/dist/assets/SavoirPage-Dp3eUCHp.js +1 -0
- package/dist/assets/SettingsPage-BElIP46c.css +1 -0
- package/dist/assets/SettingsPage-BiQbnw3o.js +1 -0
- package/dist/assets/Shell-BAyHVljp.css +1 -0
- package/dist/assets/Shell-CQRlx9-V.js +2 -0
- package/dist/assets/Table-DUmGel5e.js +1 -0
- package/dist/assets/Tabs-CfhbELZk.js +1 -0
- package/dist/assets/Terminal-Bv1IJfi9.js +3 -0
- package/dist/assets/Terminal-CEfj38NV.css +1 -0
- package/dist/assets/arrow-right-Cx3pAEhl.js +1 -0
- package/dist/assets/auditViews-BMza2Sdz.js +1 -0
- package/dist/assets/auditViews-BePWj26h.css +1 -0
- package/dist/assets/bricolage-grotesque-latin-ext-opsz-normal-IcJDqblK.woff2 +0 -0
- package/dist/assets/bricolage-grotesque-latin-opsz-normal-Cre6nC2_.woff2 +0 -0
- package/dist/assets/bricolage-grotesque-vietnamese-opsz-normal-D9N6E8K1.woff2 +0 -0
- package/dist/assets/charts-BcijSJ4J.js +1 -0
- package/dist/assets/circle-stop-C33BAcKW.js +1 -0
- package/dist/assets/command-ChA49Xk7.js +1 -0
- package/dist/assets/command-Ck4pdQCv.css +1 -0
- package/dist/assets/download-CeuKOenD.js +1 -0
- package/dist/assets/eye-CXXHBBpr.js +1 -0
- package/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/assets/habillage-B5mMBadn.js +1 -0
- package/dist/assets/index-BH1etemR.css +1 -0
- package/dist/assets/index-COSiM-R9.js +14 -0
- package/dist/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
- package/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/assets/layers-2x-Bpkbi35X.png +0 -0
- package/dist/assets/layers-BWBAp2CZ.png +0 -0
- package/dist/assets/markdown-DF87LQh1.css +1 -0
- package/dist/assets/markdown-o5sMhPHN.js +4 -0
- package/dist/assets/marker-icon-hN30_KVU.png +0 -0
- package/dist/assets/play-Daw4K_73.js +1 -0
- package/dist/assets/plus-D_QHm8ud.js +1 -0
- package/dist/assets/trash-DLpHM-s8.js +1 -0
- package/dist/assets/web-6j1-aY67.js +1 -0
- package/dist/favicon.svg +1 -0
- package/dist/index.html +18 -0
- package/docs/ANALYSE.md +87 -0
- package/docs/SECURITE.md +188 -0
- package/lib/scripts/lier.js +15 -0
- package/lib/scripts/searxng.js +91 -0
- package/lib/server/app.js +444 -0
- package/lib/server/audits/modules/local.js +429 -0
- package/lib/server/audits/modules/network.js +331 -0
- package/lib/server/audits/modules/web.js +302 -0
- package/lib/server/audits/net.js +141 -0
- package/lib/server/audits/registry.js +202 -0
- package/lib/server/audits/types.js +58 -0
- package/lib/server/brain/brain.js +624 -0
- package/lib/server/brain/conversations.js +82 -0
- package/lib/server/brain/ollama.js +197 -0
- package/lib/server/brain/prompt.js +188 -0
- package/lib/server/brain/provider.js +1 -0
- package/lib/server/cli/bridge.js +292 -0
- package/lib/server/cli/pont.js +62 -0
- package/lib/server/config.js +68 -0
- package/lib/server/engine/analyzer.js +205 -0
- package/lib/server/engine/catalog.js +229 -0
- package/lib/server/engine/decoder.js +91 -0
- package/lib/server/engine/engine.js +194 -0
- package/lib/server/engine/paths.js +129 -0
- package/lib/server/engine/policy.js +397 -0
- package/lib/server/engine/relax.js +10 -0
- package/lib/server/engine/runner.js +193 -0
- package/lib/server/errors.js +23 -0
- package/lib/server/http/guard.js +75 -0
- package/lib/server/http/router.js +154 -0
- package/lib/server/http/static.js +76 -0
- package/lib/server/index.js +40 -0
- package/lib/server/journal.js +139 -0
- package/lib/server/lien.js +33 -0
- package/lib/server/savoir/base.js +284 -0
- package/lib/server/savoir/lecture.js +406 -0
- package/lib/server/savoir/memoire.js +115 -0
- package/lib/server/search/categories.js +179 -0
- package/lib/server/search/fiche.js +326 -0
- package/lib/server/search/lieux.js +134 -0
- package/lib/server/search/overpass.js +216 -0
- package/lib/server/search/searxng.js +223 -0
- package/lib/server/search/tiles.js +103 -0
- package/lib/server/status.js +157 -0
- package/lib/server/stores.js +141 -0
- package/lib/server/telephone/acces.js +457 -0
- package/lib/server/telephone/certificat.js +79 -0
- package/lib/server/telephone/qr.js +308 -0
- package/lib/shared/analysis.js +40 -0
- package/lib/shared/schemas.js +105 -0
- package/lib/shared/types.js +10 -0
- package/package.json +81 -4
- package/searxng/settings.yml +35 -0
- package/tui/Modelfile +23 -0
- package/tui/README.md +103 -0
- package/tui/prmt_tui.py +65 -0
- package/tui/promethee/__init__.py +1 -0
- package/tui/promethee/__main__.py +4 -0
- package/tui/promethee/accueil.py +66 -0
- package/tui/promethee/agents.py +220 -0
- package/tui/promethee/app/__init__.py +4 -0
- package/tui/promethee/app/application.py +299 -0
- package/tui/promethee/app/clavier.py +99 -0
- package/tui/promethee/app/commandes_slash.py +164 -0
- package/tui/promethee/app/contexte.py +188 -0
- package/tui/promethee/app/definitions.py +518 -0
- package/tui/promethee/app/ecran.py +177 -0
- package/tui/promethee/app/envoi.py +141 -0
- package/tui/promethee/app/historique.py +373 -0
- package/tui/promethee/app/modele.py +108 -0
- package/tui/promethee/app/notes.py +63 -0
- package/tui/promethee/app/outils.py +589 -0
- package/tui/promethee/app/plateforme.py +409 -0
- package/tui/promethee/app/saisie.py +100 -0
- package/tui/promethee/app/taches.py +92 -0
- package/tui/promethee/app/tour.py +539 -0
- package/tui/promethee/app/validation.py +210 -0
- package/tui/promethee/appels.py +35 -0
- package/tui/promethee/commandes.py +154 -0
- package/tui/promethee/completion.py +140 -0
- package/tui/promethee/config.py +12 -0
- package/tui/promethee/consignes.py +278 -0
- package/tui/promethee/conversation.py +94 -0
- package/tui/promethee/etat.py +253 -0
- package/tui/promethee/fichiers.py +354 -0
- package/tui/promethee/journal.py +116 -0
- package/tui/promethee/logo.py +132 -0
- package/tui/promethee/memoire.py +50 -0
- package/tui/promethee/ollama.py +52 -0
- package/tui/promethee/plan.py +91 -0
- package/tui/promethee/politique.py +429 -0
- package/tui/promethee/preferences.py +65 -0
- package/tui/promethee/presse_papiers.py +44 -0
- package/tui/promethee/recherche.py +581 -0
- package/tui/promethee/securite.py +204 -0
- package/tui/promethee/shell.py +190 -0
- package/tui/promethee/textes.py +190 -0
- package/tui/promethee/theme.py +92 -0
- package/tui/promethee/widgets.py +493 -0
- package/tui/requirements.txt +1 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/** Modules locaux : configuration de sécurité de Windows, analyse de fichiers. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { badRequest, forbidden } from '../../errors.js';
|
|
6
|
+
import { classifyPath } from '../../engine/paths.js';
|
|
7
|
+
import { plural, resteACorrigerEnTete } from '../types.js';
|
|
8
|
+
/** Lit le JSON produit par ConvertTo-Json (un objet ou un tableau) au milieu d'une sortie de commande. */
|
|
9
|
+
export function parseRows(output) {
|
|
10
|
+
const start = output.search(/[[{]/);
|
|
11
|
+
if (start < 0)
|
|
12
|
+
return [];
|
|
13
|
+
try {
|
|
14
|
+
const data = JSON.parse(output.slice(start).trim());
|
|
15
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
16
|
+
return rows.filter((r) => r !== null && typeof r === 'object');
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const truthy = (v) => v === true || v === 1 || v === 'True' || v === '1';
|
|
23
|
+
/** Une valeur présente dans la sortie. Absente ou nulle, elle n'a pas été lue : on n'en déduit rien. */
|
|
24
|
+
const lu = (v) => v !== null && v !== undefined && v !== '';
|
|
25
|
+
/**
|
|
26
|
+
* Une vérification dont la sortie ne dit rien de lisible (vide, `[]`, sans JSON, champ absent) : « Non vérifié », sans constat.
|
|
27
|
+
* Jamais « conforme » sur une protection qu'on n'a pas lue, jamais d'alerte non plus (relecture D, groupe A).
|
|
28
|
+
*/
|
|
29
|
+
const nonLu = (value) => ({ check: { status: 'skip', value }, findings: [] });
|
|
30
|
+
const RISKY_LISTENERS = { 135: 'RPC', 139: 'NetBIOS', 445: 'SMB', 3389: 'RDP', 5985: 'WinRM', 5900: 'VNC', 23: 'Telnet', 21: 'FTP' };
|
|
31
|
+
export const CONFIG_CHECKS = [
|
|
32
|
+
{
|
|
33
|
+
label: 'Pare-feu Windows',
|
|
34
|
+
command: 'Get-NetFirewallProfile | Select-Object Name, Enabled | ConvertTo-Json -Compress',
|
|
35
|
+
judge(rows) {
|
|
36
|
+
// Aucun profil dans la sortie : rien ne dit que le pare-feu est actif. Non vérifié, jamais « conforme ».
|
|
37
|
+
const profils = rows.filter((r) => lu(r.Enabled));
|
|
38
|
+
if (profils.length === 0)
|
|
39
|
+
return nonLu('aucun profil lu dans la sortie');
|
|
40
|
+
const off = profils.filter((r) => !truthy(r.Enabled)).map((r) => String(r.Name));
|
|
41
|
+
return {
|
|
42
|
+
check: { status: off.length === 0 ? 'pass' : 'fail', value: off.length === 0 ? plural(profils.length, 'profil actif', 'profils actifs') : `désactivé : ${off.join(', ')}` },
|
|
43
|
+
findings: off.length === 0 ? [] : [{ severity: 'high', title: 'Pare-feu désactivé', detail: `Profil${off.length > 1 ? 's' : ''} sans pare-feu : ${off.join(', ')}. Tous les services en écoute sont joignables sur ces réseaux.`, recommendation: 'Réactiver le pare-feu : Paramètres › Confidentialité et sécurité › Sécurité Windows › Pare-feu.' }],
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: 'Antivirus Defender',
|
|
49
|
+
command: 'Get-MpComputerStatus | Select-Object AntivirusEnabled, RealTimeProtectionEnabled, AntivirusSignatureAge, IsTamperProtected | ConvertTo-Json -Compress',
|
|
50
|
+
judge(rows) {
|
|
51
|
+
const r = rows[0];
|
|
52
|
+
if (!r || !lu(r.RealTimeProtectionEnabled))
|
|
53
|
+
return nonLu('état de la protection en temps réel non lu dans la sortie');
|
|
54
|
+
const findings = [];
|
|
55
|
+
const realtime = truthy(r.RealTimeProtectionEnabled);
|
|
56
|
+
const age = lu(r.AntivirusSignatureAge) && Number.isFinite(Number(r.AntivirusSignatureAge)) ? Number(r.AntivirusSignatureAge) : null;
|
|
57
|
+
if (!realtime)
|
|
58
|
+
findings.push({ severity: 'high', title: 'Protection en temps réel désactivée', detail: 'Defender n’analyse pas les fichiers à leur ouverture.', recommendation: 'Réactiver la protection en temps réel dans Sécurité Windows (sauf si un autre antivirus la remplace).' });
|
|
59
|
+
if (age !== null && age > 7)
|
|
60
|
+
findings.push({ severity: 'medium', title: 'Signatures antivirus anciennes', detail: `Les signatures ont ${age} jours.`, recommendation: 'Lancer une mise à jour de Windows Update.' });
|
|
61
|
+
if (realtime && lu(r.IsTamperProtected) && !truthy(r.IsTamperProtected))
|
|
62
|
+
findings.push({ severity: 'low', title: 'Protection contre les falsifications désactivée', detail: 'Un programme peut désactiver Defender sans ton accord.', recommendation: 'Activer la protection contre les falsifications dans Sécurité Windows.' });
|
|
63
|
+
return { check: { status: findings.length === 0 ? 'pass' : realtime ? 'warn' : 'fail', value: `temps réel ${realtime ? 'actif' : 'inactif'} · ${age === null ? 'âge des signatures non lu' : `signatures de ${age} j`}` }, findings };
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
label: 'Contrôle de compte (UAC)',
|
|
68
|
+
command: "Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System' | Select-Object EnableLUA, ConsentPromptBehaviorAdmin | ConvertTo-Json -Compress",
|
|
69
|
+
judge(rows) {
|
|
70
|
+
const r = rows[0];
|
|
71
|
+
if (!r || !lu(r.EnableLUA))
|
|
72
|
+
return nonLu('EnableLUA non lu dans la sortie');
|
|
73
|
+
const on = Number(r.EnableLUA) === 1;
|
|
74
|
+
const silent = Number(r.ConsentPromptBehaviorAdmin ?? 5) === 0;
|
|
75
|
+
const findings = [];
|
|
76
|
+
if (!on)
|
|
77
|
+
findings.push({ severity: 'high', title: 'UAC désactivé', detail: 'Tous les programmes lancés par un administrateur obtiennent les droits complets sans demande.', recommendation: 'Remettre EnableLUA à 1 et redémarrer.' });
|
|
78
|
+
else if (silent)
|
|
79
|
+
findings.push({ severity: 'medium', title: 'UAC sans demande de confirmation', detail: 'L’élévation est accordée silencieusement aux administrateurs.', recommendation: 'Remonter le curseur UAC au niveau par défaut.' });
|
|
80
|
+
return { check: { status: !on ? 'fail' : silent ? 'warn' : 'pass', value: !on ? 'désactivé' : silent ? 'actif, sans confirmation' : 'actif' }, findings };
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
label: 'Bureau à distance (RDP)',
|
|
85
|
+
command: "Get-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server' | Select-Object fDenyTSConnections | ConvertTo-Json -Compress",
|
|
86
|
+
judge(rows) {
|
|
87
|
+
const r = rows[0];
|
|
88
|
+
if (!r || !lu(r.fDenyTSConnections))
|
|
89
|
+
return nonLu('fDenyTSConnections non lu dans la sortie');
|
|
90
|
+
const enabled = Number(r.fDenyTSConnections) === 0;
|
|
91
|
+
return {
|
|
92
|
+
check: { status: enabled ? 'warn' : 'pass', value: enabled ? 'activé' : 'désactivé' },
|
|
93
|
+
findings: enabled ? [{ severity: 'medium', title: 'Bureau à distance activé', detail: 'La machine accepte les connexions RDP. C’est une cible fréquente des attaques par force brute.', recommendation: 'Le désactiver s’il ne sert pas, sinon le limiter au réseau local ou à un VPN.' }] : [],
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
label: 'Partage SMB',
|
|
99
|
+
command: 'Get-SmbServerConfiguration | Select-Object EnableSMB1Protocol, RequireSecuritySignature | ConvertTo-Json -Compress',
|
|
100
|
+
judge(rows) {
|
|
101
|
+
const r = rows[0];
|
|
102
|
+
if (!r || !lu(r.EnableSMB1Protocol))
|
|
103
|
+
return nonLu('état de SMBv1 non lu dans la sortie');
|
|
104
|
+
const smb1 = truthy(r.EnableSMB1Protocol);
|
|
105
|
+
const signature = !lu(r.RequireSecuritySignature) ? 'non lue' : truthy(r.RequireSecuritySignature) ? 'exigée' : 'facultative';
|
|
106
|
+
return {
|
|
107
|
+
check: { status: smb1 ? 'fail' : 'pass', value: `SMBv1 ${smb1 ? 'activé' : 'désactivé'} · signature ${signature}` },
|
|
108
|
+
findings: smb1 ? [{ severity: 'high', title: 'SMBv1 activé', detail: 'SMBv1 est le protocole exploité par WannaCry (EternalBlue). Il n’a plus de raison d’être activé.', recommendation: 'Désactiver SMBv1 dans les fonctionnalités de Windows.' }] : [],
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
label: 'Comptes locaux',
|
|
114
|
+
command: 'Get-LocalUser | Select-Object Name, Enabled, PasswordRequired | ConvertTo-Json -Compress',
|
|
115
|
+
judge(rows) {
|
|
116
|
+
const comptes = rows.filter((r) => lu(r.Name) && lu(r.Enabled));
|
|
117
|
+
if (comptes.length === 0)
|
|
118
|
+
return nonLu('aucun compte lu dans la sortie');
|
|
119
|
+
const active = comptes.filter((r) => truthy(r.Enabled));
|
|
120
|
+
const noPassword = active.filter((r) => !truthy(r.PasswordRequired)).map((r) => String(r.Name));
|
|
121
|
+
const guest = active.some((r) => /^(invité|guest)$/i.test(String(r.Name)));
|
|
122
|
+
const findings = [];
|
|
123
|
+
if (guest)
|
|
124
|
+
findings.push({ severity: 'high', title: 'Compte Invité activé', detail: 'Le compte Invité permet d’ouvrir une session sans mot de passe.', recommendation: 'Désactiver le compte Invité.' });
|
|
125
|
+
if (noPassword.length > 0)
|
|
126
|
+
findings.push({ severity: 'medium', title: 'Compte actif sans mot de passe obligatoire', detail: `Compte${noPassword.length > 1 ? 's' : ''} concerné${noPassword.length > 1 ? 's' : ''} : ${noPassword.join(', ')}.`, recommendation: 'Exiger un mot de passe pour chaque compte actif.' });
|
|
127
|
+
return { check: { status: findings.length === 0 ? 'pass' : 'warn', value: `${plural(active.length, 'compte actif', 'comptes actifs')} sur ${comptes.length}` }, findings };
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
label: 'Services en écoute',
|
|
132
|
+
command: 'Get-NetTCPConnection -State Listen | Select-Object LocalAddress, LocalPort | ConvertTo-Json -Compress',
|
|
133
|
+
judge(rows) {
|
|
134
|
+
if (!rows.some((r) => lu(r.LocalAddress) && lu(r.LocalPort)))
|
|
135
|
+
return nonLu('aucun port en écoute lu dans la sortie');
|
|
136
|
+
const exposed = [...new Set(rows.filter((r) => ['0.0.0.0', '::'].includes(String(r.LocalAddress))).map((r) => Number(r.LocalPort)))].sort((a, b) => a - b);
|
|
137
|
+
const risky = exposed.filter((p) => RISKY_LISTENERS[p] !== undefined);
|
|
138
|
+
return {
|
|
139
|
+
check: { status: risky.length > 0 ? 'warn' : 'info', value: `${plural(exposed.length, 'port en écoute', 'ports en écoute')} sur toutes les interfaces${risky.length > 0 ? `, dont ${risky.map((p) => `${p} (${RISKY_LISTENERS[p]})`).join(', ')}` : ''}` },
|
|
140
|
+
findings: risky.length > 0 ? [{ severity: 'low', title: 'Services Windows à l’écoute sur toutes les interfaces', detail: `Ports ${risky.map((p) => `${p} (${RISKY_LISTENERS[p]})`).join(', ')}. Ils sont joignables depuis le réseau si le pare-feu les laisse passer.`, recommendation: 'Vérifier que le pare-feu bloque ces ports sur les réseaux publics.' }] : [],
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
label: 'Dernière mise à jour',
|
|
146
|
+
command: 'Get-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 1 HotFixID, InstalledOn | ConvertTo-Json -Compress',
|
|
147
|
+
judge(rows, raw, now) {
|
|
148
|
+
const ms = Number(/Date\((\d+)\)/.exec(raw)?.[1] ?? NaN);
|
|
149
|
+
if (Number.isNaN(ms))
|
|
150
|
+
return { check: { status: 'skip', value: 'date du dernier correctif illisible' }, findings: [] };
|
|
151
|
+
const days = Math.floor((now.getTime() - ms) / 86_400_000);
|
|
152
|
+
const id = String(rows[0]?.HotFixID ?? 'correctif');
|
|
153
|
+
return {
|
|
154
|
+
check: { status: days > 60 ? 'warn' : 'pass', value: `${id} il y a ${days} j` },
|
|
155
|
+
findings: days > 60 ? [{ severity: 'medium', title: 'Aucun correctif installé depuis plus de deux mois', detail: `Le dernier correctif (${id}) date de ${days} jours.`, recommendation: 'Lancer Windows Update.' }] : [],
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
label: 'Politique d’exécution PowerShell',
|
|
161
|
+
command: 'Get-ExecutionPolicy',
|
|
162
|
+
judge(_rows, raw) {
|
|
163
|
+
const policy = raw.trim().split(/\r?\n/).pop()?.trim() ?? '';
|
|
164
|
+
if (!/^(Restricted|AllSigned|RemoteSigned|Unrestricted|Bypass|Undefined|Default)$/i.test(policy))
|
|
165
|
+
return nonLu('politique non lue dans la sortie');
|
|
166
|
+
const loose = /^(Unrestricted|Bypass)$/i.test(policy);
|
|
167
|
+
return {
|
|
168
|
+
check: { status: loose ? 'warn' : 'pass', value: policy },
|
|
169
|
+
findings: loose ? [{ severity: 'low', title: 'Scripts PowerShell sans restriction', detail: `La politique d’exécution est « ${policy} » : tout script téléchargé s’exécute sans avertissement.`, recommendation: 'Revenir à RemoteSigned.' }] : [],
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
function failure(result) {
|
|
175
|
+
const text = result.output.trim().split(/\r?\n/)[0] ?? '';
|
|
176
|
+
if (/acc[èe]s refus|access is denied|privil|[ée]l[ée]vation|administrat/i.test(result.output))
|
|
177
|
+
return 'droits administrateur requis';
|
|
178
|
+
return text ? text.slice(0, 120) : `code de sortie ${result.exitCode ?? '?'}`;
|
|
179
|
+
}
|
|
180
|
+
export const configModule = {
|
|
181
|
+
info: {
|
|
182
|
+
id: 'config',
|
|
183
|
+
letter: 'η',
|
|
184
|
+
title: 'Configuration de la machine',
|
|
185
|
+
objective: 'Contrôler les réglages de sécurité de ce PC : pare-feu, antivirus, UAC, RDP, SMB, comptes, mises à jour.',
|
|
186
|
+
perimeter: `${CONFIG_CHECKS.length} commandes PowerShell de lecture, listées ci-dessous, exécutées par le moteur avec la même liste blanche que le terminal. Rien n’est modifié.`,
|
|
187
|
+
results: 'L’état de chaque réglage, et pour chaque constat le risque et le réglage à changer.',
|
|
188
|
+
input: 'none',
|
|
189
|
+
fields: [],
|
|
190
|
+
commands: CONFIG_CHECKS.map((c) => c.command),
|
|
191
|
+
},
|
|
192
|
+
resolve: () => ({ label: 'Cette machine', host: null }),
|
|
193
|
+
async run(_input, ctx) {
|
|
194
|
+
const findings = [];
|
|
195
|
+
const checks = [];
|
|
196
|
+
let done = 0;
|
|
197
|
+
for (const item of CONFIG_CHECKS) {
|
|
198
|
+
ctx.progress(item.label, done, CONFIG_CHECKS.length);
|
|
199
|
+
try {
|
|
200
|
+
const result = await ctx.engine.runRead(item.command);
|
|
201
|
+
if (result.exitCode !== 0 && parseRows(result.output).length === 0 && !/^[A-Za-z]+\s*$/.test(result.output.trim()))
|
|
202
|
+
checks.push({ label: item.label, status: 'skip', value: failure(result) });
|
|
203
|
+
else {
|
|
204
|
+
const judged = item.judge(parseRows(result.output), result.output, ctx.now);
|
|
205
|
+
checks.push({ label: item.label, ...judged.check });
|
|
206
|
+
findings.push(...judged.findings);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
checks.push({ label: item.label, status: 'skip', value: err instanceof Error ? err.message.slice(0, 140) : 'erreur' });
|
|
211
|
+
}
|
|
212
|
+
done++;
|
|
213
|
+
}
|
|
214
|
+
ctx.progress('Terminé', done, CONFIG_CHECKS.length);
|
|
215
|
+
const skipped = checks.filter((c) => c.status === 'skip').length;
|
|
216
|
+
return { summary: `${resteACorrigerEnTete(findings.filter((f) => f.severity !== 'info').length)} sur ${checks.length - skipped} réglages vérifiés${skipped > 0 ? `, ${skipped} non vérifié${skipped > 1 ? 's' : ''}` : ''}.`, findings, checks };
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
// ------------------------------------------------------------------ Analyse de fichiers
|
|
220
|
+
const MAGIC = [
|
|
221
|
+
{ kind: 'pe', label: 'exécutable Windows', test: (b) => b[0] === 0x4d && b[1] === 0x5a },
|
|
222
|
+
{ kind: 'elf', label: 'exécutable Linux', test: (b) => b.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])) },
|
|
223
|
+
{ kind: 'zip', label: 'archive ZIP ou document Office', test: (b) => b.subarray(0, 4).equals(Buffer.from([0x50, 0x4b, 0x03, 0x04])) },
|
|
224
|
+
{ kind: 'pdf', label: 'PDF', test: (b) => b.subarray(0, 4).toString('latin1') === '%PDF' },
|
|
225
|
+
{ kind: 'png', label: 'image PNG', test: (b) => b.subarray(0, 4).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47])) },
|
|
226
|
+
{ kind: 'jpg', label: 'image JPEG', test: (b) => b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff },
|
|
227
|
+
{ kind: 'gif', label: 'image GIF', test: (b) => b.subarray(0, 4).toString('latin1') === 'GIF8' },
|
|
228
|
+
{ kind: 'ole', label: 'document Office ancien ou installeur MSI', test: (b) => b.subarray(0, 4).equals(Buffer.from([0xd0, 0xcf, 0x11, 0xe0])) },
|
|
229
|
+
{ kind: 'gzip', label: 'archive gzip', test: (b) => b[0] === 0x1f && b[1] === 0x8b },
|
|
230
|
+
{ kind: '7z', label: 'archive 7-Zip', test: (b) => b.subarray(0, 4).equals(Buffer.from([0x37, 0x7a, 0xbc, 0xaf])) },
|
|
231
|
+
{ kind: 'rar', label: 'archive RAR', test: (b) => b.subarray(0, 4).toString('latin1') === 'Rar!' },
|
|
232
|
+
];
|
|
233
|
+
const EXPECTED = {
|
|
234
|
+
'.exe': 'pe', '.dll': 'pe', '.sys': 'pe', '.scr': 'pe', '.com': 'pe', '.pdf': 'pdf', '.png': 'png', '.jpg': 'jpg', '.jpeg': 'jpg', '.gif': 'gif',
|
|
235
|
+
'.zip': 'zip', '.docx': 'zip', '.xlsx': 'zip', '.pptx': 'zip', '.jar': 'zip', '.doc': 'ole', '.xls': 'ole', '.ppt': 'ole', '.msi': 'ole',
|
|
236
|
+
};
|
|
237
|
+
const DOUBLE_EXTENSION = /\.(?:pdf|docx?|xlsx?|pptx?|jpe?g|png|gif|txt|zip)\.(?:exe|scr|bat|cmd|com|pif|js|vbs|ps1|lnk|hta)$/i;
|
|
238
|
+
const TEXT_EXTENSIONS = new Set(['.env', '.ini', '.conf', '.config', '.cfg', '.json', '.yml', '.yaml', '.xml', '.toml', '.properties', '.ps1', '.psm1', '.bat', '.cmd', '.sh', '.py', '.js', '.ts', '.php', '.rb', '.go', '.rs', '.java', '.cs', '.sql', '.txt', '.md', '.log', '.tf', '.pem', '.key', '']);
|
|
239
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', '.venv', 'venv', '__pycache__', 'dist', 'build', 'target', 'appdata', '$recycle.bin', '.cache']);
|
|
240
|
+
const SECRET_RULES = [
|
|
241
|
+
{ label: 'Clé privée', pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/, severity: 'critical' },
|
|
242
|
+
{ label: 'Clé d’accès AWS', pattern: /\bAKIA[0-9A-Z]{16}\b/, severity: 'high' },
|
|
243
|
+
{ label: 'Jeton GitHub', pattern: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/, severity: 'high' },
|
|
244
|
+
{ label: 'Jeton Slack', pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}/, severity: 'high' },
|
|
245
|
+
{ label: 'Mot de passe ou clé en clair', pattern: /(?:password|passwd|pwd|mot_?de_?passe|(?:client[_-]?)?secret(?:[_-]?key)?|api[_-]?key|private[_-]?key|(?:access|auth)[_-]?token)\s*[:=]\s*["']?([^\s"'#;,<>{}$]{6,})/i, severity: 'medium' },
|
|
246
|
+
];
|
|
247
|
+
const PLACEHOLDER = /^(?:changeme|change_me|example|exemple|password|secret|xxx+|\*+|null|none|true|false|undefined|your[_-].*|<.*>)$/i;
|
|
248
|
+
export function scanSecrets(text) {
|
|
249
|
+
const hits = [];
|
|
250
|
+
const lines = text.split(/\r?\n/);
|
|
251
|
+
for (let i = 0; i < lines.length && hits.length < 20; i++) {
|
|
252
|
+
const line = lines[i] ?? '';
|
|
253
|
+
if (line.length > 2000)
|
|
254
|
+
continue;
|
|
255
|
+
for (const rule of SECRET_RULES) {
|
|
256
|
+
const m = rule.pattern.exec(line);
|
|
257
|
+
if (!m)
|
|
258
|
+
continue;
|
|
259
|
+
const value = m[1] ?? m[0];
|
|
260
|
+
if (m[1] !== undefined && PLACEHOLDER.test(value))
|
|
261
|
+
continue;
|
|
262
|
+
hits.push({ label: rule.label, severity: rule.severity, line: i + 1, excerpt: `${value.slice(0, 4)}…` });
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return hits;
|
|
267
|
+
}
|
|
268
|
+
export function entropy(bytes) {
|
|
269
|
+
if (bytes.length === 0)
|
|
270
|
+
return 0;
|
|
271
|
+
const counts = new Array(256).fill(0);
|
|
272
|
+
for (const b of bytes)
|
|
273
|
+
counts[b] = (counts[b] ?? 0) + 1;
|
|
274
|
+
let h = 0;
|
|
275
|
+
for (const c of counts) {
|
|
276
|
+
if (c === 0)
|
|
277
|
+
continue;
|
|
278
|
+
const p = c / bytes.length;
|
|
279
|
+
h -= p * Math.log2(p);
|
|
280
|
+
}
|
|
281
|
+
return Math.round(h * 100) / 100;
|
|
282
|
+
}
|
|
283
|
+
/** Analyse un fichier à partir de son nom et de son contenu (ou de son début). */
|
|
284
|
+
export function inspectFile(name, head, text) {
|
|
285
|
+
const findings = [];
|
|
286
|
+
const ext = path.extname(name).toLowerCase();
|
|
287
|
+
const magic = MAGIC.find((m) => m.test(head)) ?? null;
|
|
288
|
+
if (DOUBLE_EXTENSION.test(name))
|
|
289
|
+
findings.push({ severity: 'high', title: `Double extension : ${name}`, detail: 'Le nom se termine comme un document mais le fichier est un programme. C’est une ruse classique de pièce jointe piégée.', recommendation: 'Ne pas l’ouvrir. Le supprimer s’il ne vient pas d’une source sûre.' });
|
|
290
|
+
const expected = EXPECTED[ext];
|
|
291
|
+
if (magic?.kind === 'pe' && expected !== 'pe')
|
|
292
|
+
findings.push({ severity: 'high', title: `Exécutable déguisé : ${name}`, detail: `Le contenu est un exécutable Windows alors que l’extension est « ${ext || 'absente'} ».`, recommendation: 'Vérifier sa provenance et son empreinte avant tout usage.' });
|
|
293
|
+
else if (expected && magic && magic.kind !== expected)
|
|
294
|
+
findings.push({ severity: 'low', title: `Extension trompeuse : ${name}`, detail: `L’extension annonce autre chose que le contenu (${magic.label}).` });
|
|
295
|
+
let secrets = 0;
|
|
296
|
+
if (text !== null) {
|
|
297
|
+
for (const hit of scanSecrets(text)) {
|
|
298
|
+
secrets++;
|
|
299
|
+
findings.push({ severity: hit.severity, title: `${hit.label} dans ${name}`, detail: `Ligne ${hit.line}. Un secret en clair dans un fichier finit souvent dans une sauvegarde ou un dépôt.`, evidence: `ligne ${hit.line} : ${hit.excerpt}`, recommendation: 'Déplacer le secret dans un coffre ou une variable d’environnement, puis le renouveler.' });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return { kind: magic?.kind ?? null, kindLabel: magic?.label ?? (text !== null ? 'texte' : 'format non reconnu'), findings, secrets };
|
|
303
|
+
}
|
|
304
|
+
const MAX_FILES = 400;
|
|
305
|
+
const MAX_DEPTH = 5;
|
|
306
|
+
const MAX_TEXT = 1024 * 1024;
|
|
307
|
+
function readHead(file, size) {
|
|
308
|
+
const fd = fs.openSync(file, 'r');
|
|
309
|
+
try {
|
|
310
|
+
const buf = Buffer.alloc(Math.min(size, 262_144));
|
|
311
|
+
const read = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
312
|
+
return buf.subarray(0, read);
|
|
313
|
+
}
|
|
314
|
+
finally {
|
|
315
|
+
fs.closeSync(fd);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function looksText(name, head) {
|
|
319
|
+
return TEXT_EXTENSIONS.has(path.extname(name).toLowerCase()) && !head.subarray(0, 4096).includes(0);
|
|
320
|
+
}
|
|
321
|
+
export const filesModule = {
|
|
322
|
+
info: {
|
|
323
|
+
id: 'files',
|
|
324
|
+
letter: 'θ',
|
|
325
|
+
title: 'Analyse de fichiers',
|
|
326
|
+
objective: 'Inspecter un fichier ou un dossier : secrets en clair, exécutables déguisés, doubles extensions.',
|
|
327
|
+
perimeter: `Lecture seule, dans tes dossiers autorisés uniquement. Un dossier est parcouru sur ${MAX_DEPTH} niveaux et ${MAX_FILES} fichiers au plus, sans entrer dans node_modules, .git, AppData, dist, build ni ${SKIP_DIRS.size - 5} autres dossiers de ce genre. Les secrets trouvés ne sont pas recopiés : seuls leurs quatre premiers caractères le sont.`,
|
|
328
|
+
results: 'Pour un fichier : empreinte SHA-256, type réel, entropie. Pour un dossier : chaque fichier suspect avec sa raison.',
|
|
329
|
+
input: 'path',
|
|
330
|
+
fields: [{ key: 'path', label: 'Fichier ou dossier', help: 'Chemin absolu, dans l’un de tes dossiers autorisés.', placeholder: 'C:\\Users\\toi\\Documents\\projet', required: true }],
|
|
331
|
+
},
|
|
332
|
+
resolve: (input) => {
|
|
333
|
+
const raw = (input.path ?? '').trim();
|
|
334
|
+
if (!raw)
|
|
335
|
+
throw badRequest('Indique le chemin d’un fichier ou d’un dossier.');
|
|
336
|
+
return { label: raw, host: null };
|
|
337
|
+
},
|
|
338
|
+
async run(input, ctx) {
|
|
339
|
+
const raw = (input.path ?? '').trim();
|
|
340
|
+
const resolved = classifyPath(raw, { cwd: ctx.roots[0] ?? ctx.home, roots: [...ctx.roots], home: ctx.home });
|
|
341
|
+
if (resolved.kind !== 'file' || !resolved.inScope)
|
|
342
|
+
throw forbidden(`« ${raw} » est en dehors de tes dossiers autorisés (${ctx.roots.join(', ')}).`, 'path_out_of_scope');
|
|
343
|
+
const stat = fs.statSync(resolved.absolute, { throwIfNoEntry: false });
|
|
344
|
+
if (!stat)
|
|
345
|
+
throw badRequest(`« ${resolved.absolute} » n’existe pas.`);
|
|
346
|
+
const findings = [];
|
|
347
|
+
const checks = [];
|
|
348
|
+
if (stat.isFile()) {
|
|
349
|
+
ctx.progress('Lecture du fichier', 0, 1);
|
|
350
|
+
const head = readHead(resolved.absolute, stat.size);
|
|
351
|
+
const name = path.basename(resolved.absolute);
|
|
352
|
+
const text = looksText(name, head) && stat.size <= MAX_TEXT ? fs.readFileSync(resolved.absolute, 'utf8') : null;
|
|
353
|
+
const report = inspectFile(name, head, text);
|
|
354
|
+
const hash = createHash('sha256');
|
|
355
|
+
await new Promise((resolve, reject) => fs.createReadStream(resolved.absolute).on('data', (c) => hash.update(c)).on('end', resolve).on('error', reject));
|
|
356
|
+
const h = entropy(head);
|
|
357
|
+
checks.push({ label: 'Taille', status: 'info', value: `${stat.size.toLocaleString('fr-FR')} octets` });
|
|
358
|
+
checks.push({ label: 'Type réel', status: 'info', value: report.kindLabel });
|
|
359
|
+
checks.push({ label: 'SHA-256', status: 'info', value: hash.digest('hex') });
|
|
360
|
+
checks.push({ label: 'Entropie', status: h > 7.5 && !['zip', 'gzip', '7z', 'rar', 'png', 'jpg', 'pdf'].includes(report.kind ?? '') ? 'warn' : 'info', value: `${h} bits par octet${h > 7.5 ? ' (contenu compressé ou chiffré)' : ''}` });
|
|
361
|
+
checks.push({ label: 'Secrets en clair', status: report.secrets > 0 ? 'fail' : text === null ? 'skip' : 'pass', value: text === null ? 'fichier binaire ou trop gros, non analysé' : String(report.secrets) });
|
|
362
|
+
findings.push(...report.findings);
|
|
363
|
+
ctx.progress('Terminé', 1, 1);
|
|
364
|
+
return { summary: findings.length === 0 ? 'Rien de suspect dans ce fichier.' : `${plural(findings.length, 'point à vérifier', 'points à vérifier')} dans ce fichier.`, findings, checks };
|
|
365
|
+
}
|
|
366
|
+
const files = [];
|
|
367
|
+
let truncated = false;
|
|
368
|
+
const walk = (dir, depth) => {
|
|
369
|
+
if (depth > MAX_DEPTH || truncated)
|
|
370
|
+
return;
|
|
371
|
+
let entries;
|
|
372
|
+
try {
|
|
373
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
for (const entry of entries) {
|
|
379
|
+
if (entry.isSymbolicLink())
|
|
380
|
+
continue;
|
|
381
|
+
const full = path.join(dir, entry.name);
|
|
382
|
+
if (entry.isDirectory()) {
|
|
383
|
+
if (!SKIP_DIRS.has(entry.name.toLowerCase()))
|
|
384
|
+
walk(full, depth + 1);
|
|
385
|
+
}
|
|
386
|
+
else if (entry.isFile()) {
|
|
387
|
+
if (files.length >= MAX_FILES) {
|
|
388
|
+
truncated = true;
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
files.push(full);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
walk(resolved.absolute, 0);
|
|
396
|
+
let executables = 0;
|
|
397
|
+
let textFiles = 0;
|
|
398
|
+
let secrets = 0;
|
|
399
|
+
for (let i = 0; i < files.length; i++) {
|
|
400
|
+
const file = files[i] ?? '';
|
|
401
|
+
if (i % 20 === 0)
|
|
402
|
+
ctx.progress(path.basename(file), i, files.length);
|
|
403
|
+
try {
|
|
404
|
+
const size = fs.statSync(file).size;
|
|
405
|
+
const head = readHead(file, size);
|
|
406
|
+
const name = path.basename(file);
|
|
407
|
+
const text = looksText(name, head) && size <= MAX_TEXT ? fs.readFileSync(file, 'utf8') : null;
|
|
408
|
+
if (text !== null)
|
|
409
|
+
textFiles++;
|
|
410
|
+
const report = inspectFile(name, head, text);
|
|
411
|
+
if (report.kind === 'pe')
|
|
412
|
+
executables++;
|
|
413
|
+
secrets += report.secrets;
|
|
414
|
+
const relative = path.relative(resolved.absolute, file);
|
|
415
|
+
for (const f of report.findings.slice(0, 5))
|
|
416
|
+
findings.push({ ...f, evidence: `${relative}${f.evidence ? ` · ${f.evidence}` : ''}` });
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
/* fichier verrouillé ou illisible : ignoré */
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
ctx.progress('Terminé', files.length, files.length);
|
|
423
|
+
checks.push({ label: 'Fichiers parcourus', status: truncated ? 'warn' : 'info', value: `${files.length}${truncated ? ` (limite de ${MAX_FILES} atteinte)` : ''}` });
|
|
424
|
+
checks.push({ label: 'Fichiers texte analysés', status: 'info', value: String(textFiles) });
|
|
425
|
+
checks.push({ label: 'Exécutables Windows', status: 'info', value: String(executables) });
|
|
426
|
+
checks.push({ label: 'Secrets en clair', status: secrets > 0 ? 'fail' : 'pass', value: String(secrets) });
|
|
427
|
+
return { summary: findings.length === 0 ? `Rien de suspect dans les ${files.length} fichiers parcourus.` : `${plural(findings.length, 'constat à vérifier', 'constats à vérifier')} dans ${files.length} fichiers.`, findings: findings.slice(0, 80), checks };
|
|
428
|
+
},
|
|
429
|
+
};
|