310 lines
28 KiB
JavaScript
310 lines
28 KiB
JavaScript
// test/control-negativ-secrete.mjs - controlul negativ al probei Aere PQ Secrets.
|
|
//
|
|
// O proba care nu poate iesi rosie nu masoara nimic. Aici se planteaza, intr-o COPIE a codului (serviciul, KMS-ul, identitatea,
|
|
// conformitatea, proof-kinds si sidecar-ul, in aceeasi asezare relativa), cate un defect real, si se cere ca proba sa iasa ROSIE pe exact
|
|
// probele numite, pentru MOTIVUL numit (un fragment din mesajul afirmatiei care a cazut), nu doar "ceva a picat".
|
|
//
|
|
// Regulile casei, platite in alte parti:
|
|
// - verdictul se citeste din JSON-ul probei (AERE_SECRETS_PROBA_JSON), nu din codul de iesire; o proba care nu a ajuns la capat sau a
|
|
// rulat alt numar de probe e STRICAT, nu ROSU (trei stari: VERDE, ROSU, STRICAT);
|
|
// - copia se reface pentru FIECARE plantare si se compara sha256 cu originalul inainte de a planta;
|
|
// - ancora unei plantari trebuie sa apara EXACT o data; o plantare care nu se poate pune e un ESEC al controlului, nu o trecere;
|
|
// - plantarile raman cod valid, ca rosul sa vina din proba, nu din incarcarea modulului; la sfarsit originalul e identic octet cu octet.
|
|
// 2026-10-18 (revizuirea adversariala B8): plantarile paznicilor noi (cererea, atenuarea, raspunsul generic, id-urile verificate,
|
|
// mutexul, lacatul dosarului, marimea cererii, numele si cheile rezervate, plafoanele, nonce-urile pe origine, ordinea la mutatie,
|
|
// delegatii, vocabularul felului) si patru lucratori in paralel, fiecare cu copia lui (o plantare = o rulare intreaga a probei).
|
|
//
|
|
// Iesire: 0 numai daca proba e VERDE pe copia neatinsa, fiecare plantare e ROSIE pe probele numite si originalul e neatins; altfel 1.
|
|
// --autoproba: controlul controlului, cu momeli care TREBUIE sa primeasca fiecare verdictul ei de esec.
|
|
|
|
import crypto from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { spawn } from 'node:child_process';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
// Doua asezari, ca in secrete.mjs: depozitul de dezvoltare (aerenew/cloud-gateway/pq-secrets, tools/aere-identity, tools/proof-kinds,
|
|
// tools/aere-verify-layer) si forma publicata (pq-secrets/ langa pq-kms/, identity/, proof-kinds/, verify-layer/). Asezarea se alege dupa
|
|
// ce exista pe disc; daca niciuna nu e intreaga, controlul refuza sa porneasca si spune ce lipseste (nu un ENOENT din mijlocul lui).
|
|
const ASEZARI = {
|
|
dezvoltare: { radacina: path.resolve(HERE, '..', '..', '..'), sec: 'cloud-gateway/pq-secrets', kms: 'cloud-gateway/pq-kms', id: 'tools/aere-identity', pk: 'tools/proof-kinds', vl: 'tools/aere-verify-layer' },
|
|
publicata: { radacina: path.resolve(HERE, '..', '..'), sec: 'pq-secrets', kms: 'pq-kms', id: 'identity', pk: 'proof-kinds', vl: 'verify-layer' },
|
|
};
|
|
const fisiereDin = (a) => [`${a.sec}/secrete.mjs`, `${a.sec}/server.mjs`, `${a.sec}/test/proba-secrete.mjs`, `${a.kms}/kms.mjs`, `${a.kms}/server.mjs`,
|
|
`${a.kms}/hsm-radacina.mjs`, `${a.id}/identity.mjs`, `${a.id}/conformitate.mjs`, `${a.pk}/proof-kinds.mjs`, `${a.vl}/sidecar.mjs`];
|
|
const intreaga = (a) => fisiereDin(a).every((f) => fs.existsSync(path.join(a.radacina, f)));
|
|
const ASEZARE_NUME = intreaga(ASEZARI.dezvoltare) ? 'dezvoltare' : intreaga(ASEZARI.publicata) ? 'publicata' : null;
|
|
if (!ASEZARE_NUME) {
|
|
for (const [n, a] of Object.entries(ASEZARI)) console.log(`asezarea ${n}: lipsesc ${fisiereDin(a).filter((f) => !fs.existsSync(path.join(a.radacina, f))).join(', ')}`);
|
|
console.log('NEMASURAT: componentele surori nu sunt nici in forma publicata, nici in depozitul de dezvoltare');
|
|
process.exit(2);
|
|
}
|
|
const ASEZARE = ASEZARI[ASEZARE_NUME];
|
|
const RADACINA = ASEZARE.radacina;
|
|
const FISIERE = fisiereDin(ASEZARE);
|
|
const [S, SV, PR, , , , IDN, CFM, PKS] = FISIERE;
|
|
const DEV_VERIFY = path.join(RADACINA, 'tools', 'aere-proof-protocol', 'verify.mjs');
|
|
const VERIFY = process.env.AERE_VERIFY_PROOF ? path.resolve(process.env.AERE_VERIFY_PROOF) : (fs.existsSync(DEV_VERIFY) ? DEV_VERIFY : '');
|
|
const NICIODATA = "process.env.AERE_SECRETS_PLANTA_NICIODATA === 'da'";
|
|
const LUCRATORI = 4;
|
|
|
|
// Fiecare plantare: fisierul, ancora (exact o aparitie), inlocuirea, si probele care TREBUIE sa iasa rosii: [inceputul numelui probei
|
|
// (unic), un fragment din motivul pe care trebuie sa cada].
|
|
const PLANTARI = [
|
|
{ id: 'P01', defect: 'nonce-ul nu se mai consuma la prima vedere (replay acceptat)', fisier: S,
|
|
ancora: ' const exp = rec ? rec.exp : 0;\n this.#uitaNonce(n);', inlocuire: ' const exp = rec ? rec.exp : 0;',
|
|
rosii: [['refuz: nonce refolosit', 'a doua oara aceeasi prezentare: trebuia refuzat cu NONCE_INVALID'], ['server: flux complet', 'replay 401']] },
|
|
{ id: 'P02', defect: 'nonce-ul nu se mai judeca (neemis si expirat acceptate)', fisier: S,
|
|
ancora: 'if (!(exp > acum /*PAZA:nonce*/))', inlocuire: 'if (!(exp > acum || true))',
|
|
rosii: [['refuz: nonce refolosit', 'trebuia refuzat cu NONCE_INVALID'], ['refuz: nonce expirat', 'la 121 s: trebuia refuzat']] },
|
|
{ id: 'P03', defect: 'audienta serviciului nu se mai cere (se accepta cea din prezentare)', fisier: S,
|
|
ancora: 'return { audience: this.#audience, nonce: p.binding.nonce,', inlocuire: 'return { audience: p.binding.audience, nonce: p.binding.nonce,',
|
|
// 2026-10-18: cu regula request, o prezentare facuta pentru ALT serviciu e refuzata si pe cerere (digestul ei numeste serviciul):
|
|
// plantarea se vede ca refuzul care nu mai e numit audience
|
|
rosii: [['refuz: prezentare facuta pentru alt serviciu', 'alt public: regula identitatii asteptata audience'], ['server: flux complet', 'regula audience in raspuns']] },
|
|
{ id: 'P04', defect: 'revocarile delegarilor din dosarul de incredere nu se mai citesc', fisier: S,
|
|
ancora: 'revocations: incredere.revocations, request: rh };', inlocuire: 'revocations: [], request: rh };',
|
|
rosii: [['delegare: telefonul', 'delegare revocata: trebuia refuzat']] },
|
|
{ id: 'P05', defect: 'un grant poate scuti starea credentialului (requireStatus: false acceptat)', fisier: S,
|
|
ancora: "if (!(g.policy.requireStatus !== false /*PAZA:grant-form*/)) throw", inlocuire: 'if (false) throw',
|
|
rosii: [['refuz: credential fara lista de stare nu deschide nimic', 'grant care scuteste starea: trebuia refuzat']] },
|
|
{ id: 'P06', defect: 'operatiile grantului nu se mai verifica (un grant get permite put)', fisier: S,
|
|
ancora: 'if (!(oricare || g.operations === null || g.operations.includes(op) /*PAZA:authorization*/)) continue;', inlocuire: 'if (false) continue;',
|
|
rosii: [['refuz: operatia nu e in grant', 'put: trebuia refuzat']] },
|
|
{ id: 'P07', defect: 'verdictul politicii grantului e ignorat (afirmatiile nu mai conteaza)', fisier: S,
|
|
ancora: 'if (!(r.compliant && !strain && !delegat /*PAZA:authorization*/)) continue;', inlocuire: 'if (false) continue;',
|
|
rosii: [['refuz: afirmatiile nu indeplinesc politica', 'cititorul: trebuia refuzat'], ['list: admin vede toate secretele', 'deployer: asteptat "db"']] },
|
|
{ id: 'P08', defect: 'o versiune revocata nu mai e refuzata pe nume', fisier: S,
|
|
ancora: "if (!(v.state !== 'revoked' /*PAZA:version-revoked*/)) throw", inlocuire: 'if (false) throw',
|
|
rosii: [['revoke-version: versiunea 1 revocata', 'auditorul la versiunea revocata']] },
|
|
{ id: 'P09', defect: 'versiunile vechi se citesc fara versions: any', fisier: S,
|
|
ancora: "if (!(ceruta === secret.current_version || grant.versions === 'any' /*PAZA:version-scope*/)) throw", inlocuire: 'if (false) throw',
|
|
rosii: [['rotatie: rotate face versiunea 2', 'deployer la versiunea 1: trebuia refuzat'], ['server: flux complet', 'versiunea 1 fara any 403']] },
|
|
{ id: 'P10', defect: 'serverul tipareste valoarea citita (un print de depanare uitat)', fisier: SV,
|
|
ancora: " const out = { name: r.name, version: r.version, current: r.current, value: r.value.toString('base64'), metadata: r.metadata, createdAt: r.createdAt };",
|
|
inlocuire: " const out = { name: r.name, version: r.version, current: r.current, value: r.value.toString('base64'), metadata: r.metadata, createdAt: r.createdAt }; process.stdout.write('debug get ' + out.value + '\\n');",
|
|
rosii: [['server: pornit din linia de comanda', 'valoarea nu e in stdout/stderr']] },
|
|
{ id: 'P11', defect: 'valoarea scrisa in clar in fisierul secretului, langa forma sigilata', fisier: S,
|
|
ancora: ' dek.fill(0); valoare.fill(0);', inlocuire: " dek.fill(0); sec.plain_debug = valoare.toString('base64'); valoare.fill(0);",
|
|
rosii: [['stocare: valoarea nu apare pe disc', 'valoarea nu e nicaieri pe disc'], ['server: pornit din linia de comanda', 'valoarea nu e pe discul procesului']] },
|
|
{ id: 'P12', defect: 'fisierul se scrie INAINTEA randului de audit (ordinea veche: o cadere lasa o schimbare fara intrare)', fisier: S,
|
|
ancora: ' this.#scrieAudit(a, acum); // AUDIT_WRITE_FAILED aici: nimic nu s-a schimbat pe disc\n if (r.salveaza) {',
|
|
inlocuire: ` if (r.salveaza) this.#salveaza(r.salveaza);\n this.#scrieAudit(a, acum);\n if (r.salveaza && (${NICIODATA})) {`,
|
|
rosii: [['jurnal: o mutatie al carei rand de audit', 'rotirea nu a ramas pe disc'], ['ordinea la mutatie', 'a aruncat altceva decat SecretsError']] },
|
|
{ id: 'P13', defect: 'o prezentare semnata de alta cheie decat a detinatorului e acceptata (verificatorul de identitate, regula presenter)', fisier: IDN,
|
|
ancora: 'asteptat && B.presenter.id === asteptat.id && canonical(B.presenter.keys) === canonical(asteptat.keys) /*PAZA:presenter*/,', inlocuire: 'true,',
|
|
rosii: [['refuz: un strain prezinta credentialul', 'strainul cu credentialul altuia: trebuia refuzat']] },
|
|
{ id: 'P14', defect: 'termenul prezentarii nu se mai cere (verificatorul de identitate, regula freshness)', fisier: IDN,
|
|
ancora: 'Math.abs(acum - t) <= maxAgeS * 1000 /*PAZA:freshness*/,', inlocuire: 'true,',
|
|
rosii: [['refuz: prezentare veche', 'prezentare veche: trebuia refuzat']] },
|
|
{ id: 'P15', defect: 'starea credentialului nu mai e ceruta de politica (verificatorul de conformitate, regula status)', fisier: CFM,
|
|
ancora: '!policy.requireStatus || !!(status && status.pass === true) /*PAZA:status*/,', inlocuire: 'true,',
|
|
rosii: [['refuz: credential fara lista de stare nu deschide nimic', 'fara lista de stare: trebuia refuzat'], ['refuz: credential revocat in lista', 'fara lista: starea nejudecata: trebuia refuzat']] },
|
|
{ id: 'P16', defect: 'refuzurile nu mai sunt scrise in jurnal', fisier: S,
|
|
ancora: 'const refuza = (e, reguli = []) => { a.rules = [...new Set([e.rule, ...reguli])]; this.#scrieAudit(a, acum); throw e; };',
|
|
inlocuire: 'const refuza = (e, reguli = []) => { a.rules = [...new Set([e.rule, ...reguli])]; throw e; };',
|
|
rosii: [['refuz: cerere fara prezentare', 'doua intrari de refuz: asteptat'], ['jurnal: dupa 100 de operatii', 'capul se schimba la operatia']] },
|
|
{ id: 'P17', defect: 'hash-ul prezentarii din jurnal nu mai e al legaturii semnate', fisier: S,
|
|
ancora: "try { prezentare = ID.canonical(a.binding); }", inlocuire: "try { prezentare = 'no-presentation'; }",
|
|
rosii: [['jurnal: fiecare operatie', 'hash-ul legaturii semnate: asteptat']] },
|
|
// ---------------------------------------------------------------------------------- 2026-10-18: paznicii revizuirii adversariale
|
|
{ id: 'P18', defect: 'cererea nu se mai compara (verificatorul de identitate, regula request): o prezentare pentru get deschide revoke', fisier: IDN,
|
|
ancora: 'B.request === request /*PAZA:request*/,', inlocuire: 'true,',
|
|
rosii: [['cererea (substitutie)', 'revoke cu prezentarea adminului pentru get: trebuia refuzat'], ['cererea prin HTTP', 'revoke-version cu corpul de get']] },
|
|
{ id: 'P19', defect: 'serviciul nu mai da digestul cererii verificatorului (cererea ramane nejudecata)', fisier: S,
|
|
ancora: 'revocations: incredere.revocations, request: rh };', inlocuire: 'revocations: incredere.revocations, request: null };',
|
|
rosii: [['cererea (substitutie)', 'revoke cu prezentarea adminului pentru get: trebuia refuzat']] },
|
|
{ id: 'P20', defect: 'digestul cererii nu mai acopera corpul (numai operatia si secretul)', fisier: S,
|
|
ancora: ' for (const k of Object.keys(body)) {\n if (k === \'presentation\' || body[k] === undefined) continue;', inlocuire: ' for (const k of []) {\n if (k === \'presentation\' || body[k] === undefined) continue;',
|
|
rosii: [['cererea (substitutie)', 'get db cu version adaugat: trebuia refuzat'], ['cererea prin HTTP', 'put cu valoarea schimbata']] },
|
|
{ id: 'P21', defect: 'un grant facut de un ne-admin nu mai e atenuat (auto-acordarea din revizuire)', fisier: S,
|
|
ancora: "if (origine.id !== 'admin') atenuare(g, origine);", inlocuire: 'if (false) atenuare(g, origine);',
|
|
rosii: [['atenuare:', 'put, pe care grantul lui nu il are: trebuia refuzat']] },
|
|
{ id: 'P22', defect: 'un ne-admin poate da mai departe grant si revoke (re-delegarea administrarii)', fisier: S,
|
|
ancora: 'o.operations.includes(x) && !OPERATII_ADMIN.includes(x)', inlocuire: 'o.operations.includes(x)',
|
|
rosii: [['atenuare:', 'grant mai departe: trebuia refuzat']] },
|
|
{ id: 'P23', defect: 'un ne-admin retrage si grant-urile altora', fisier: S,
|
|
ancora: "origine.id === 'admin' || tinta.createdBy === result.holder /*PAZA:grant-scope*/", inlocuire: 'true',
|
|
rosii: [['atenuare:', 'un grant al adminului: trebuia refuzat']] },
|
|
{ id: 'P24', defect: 'refuzul de autorizare numeste grant-urile in mesaj (oracolul din revizuire)', fisier: S,
|
|
ancora: "refuza(refuz('authorization', 'UNAUTHORIZED', MESAJ_NEAUTORIZAT), ", inlocuire: "refuza(refuz('authorization', 'UNAUTHORIZED', MESAJ_NEAUTORIZAT + ' [' + j.judged.map((x) => x.grant).join(', ') + ']'), ",
|
|
rosii: [['raspunsul generic', 'db (3 grant-uri) si api-key (0): asteptat']] },
|
|
{ id: 'P25', defect: 'refuzul de autorizare poarta grant-urile judecate in eroare (si deci in HTTP, daca serverul le-ar trimite)', fisier: S,
|
|
ancora: "refuza(refuz('authorization', 'UNAUTHORIZED', MESAJ_NEAUTORIZAT), ", inlocuire: "refuza(refuz('authorization', 'UNAUTHORIZED', MESAJ_NEAUTORIZAT, { grants: j.judged }), ",
|
|
rosii: [['raspunsul generic', 'db (3 grant-uri) si api-key (0): asteptat']] },
|
|
{ id: 'P26', defect: 'id-urile pretinse intra in jurnal oricum ar arata (100.000 de caractere)', fisier: S,
|
|
ancora: "const idPretins = (x) => (typeof x === 'string' && AERE_ID_RE.test(x) ? x : null);", inlocuire: "const idPretins = (x) => (typeof x === 'string' ? x : null);",
|
|
rosii: [['jurnalul unui refuz', 'anonimul: cod asteptat NONCE_INVALID, primit AUDIT_WRITE_FAILED']] },
|
|
{ id: 'P27', defect: 'id-urile pretinse scrise ca holder/presenter verificate', fisier: S,
|
|
ancora: 'if (!(tin /*identitatile verificate*/)) return;', inlocuire: 'if (!(true /*identitatile verificate*/)) return;',
|
|
rosii: [['jurnalul unui refuz', 'semnatura picata: holder nescris']] },
|
|
{ id: 'P28', defect: 'mutexul secretului nu mai asteapta (pierderi de actualizari cu KMS-ul la distanta)', fisier: S,
|
|
ancora: ' this.#lacate.set(cheie, coada);\n await inainte;', inlocuire: ' this.#lacate.set(cheie, coada);',
|
|
rosii: [['concurenta cu KMS-ul la distanta', 'versiunile intoarse: asteptat']] },
|
|
{ id: 'P29', defect: 'lacatul dosarului nu se mai ia (doua instante pe acelasi dosar)', fisier: S,
|
|
ancora: ' s.#iaLacatulDosarului(staleLock);\n', inlocuire: '',
|
|
rosii: [['lacatul dosarului', 'a doua deschidere in acelasi proces: asteptat']] },
|
|
{ id: 'P30', defect: 'lacatul unui proces mort e furat in tacere', fisier: S,
|
|
ancora: "if (staleLock !== 'take') throw new SecretsError('DATA_DIR_LOCK_STALE',", inlocuire: "if (false) throw new SecretsError('DATA_DIR_LOCK_STALE',",
|
|
rosii: [['lacatul dosarului', 'lacatul unui proces mort: asteptat']] },
|
|
{ id: 'P31', defect: 'marimea cererii nu se mai judeca (JSON adanc: RangeError si refuz fara rand)', fisier: S,
|
|
ancora: 'if (!(prea === null /*PAZA:request-form*/)) refuza(', inlocuire: 'if (false) refuza(',
|
|
rosii: [['JSON adanc', 'binding adanc: cod asteptat REQUEST_TOO_COMPLEX']] },
|
|
{ id: 'P32', defect: 'numele de dispozitiv ale Windows primite', fisier: S,
|
|
ancora: 'if (NUME_REZERVATE.test(name)) throw', inlocuire: 'if (false) throw',
|
|
rosii: [['nume si chei rezervate', 'con: trebuia refuzat']] },
|
|
{ id: 'P33', defect: 'cheile __proto__, constructor, prototype primite in metadate', fisier: S,
|
|
ancora: 'if (CHEI_REZERVATE.has(k)) throw', inlocuire: 'if (false) throw',
|
|
rosii: [['nume si chei rezervate', '__proto__: trebuia refuzat']] },
|
|
{ id: 'P34', defect: 'versiunea ceruta reflectata in mesaj', fisier: S,
|
|
ancora: "const versiuneAfisata = (v) => (Number.isSafeInteger(v) ? String(v) : '(not an integer)');", inlocuire: 'const versiuneAfisata = (v) => String(v);',
|
|
rosii: [['mesajele nu reflecta', 'mesajul trebuia sa contina "(not an integer)"']] },
|
|
{ id: 'P35', defect: 'un grant poate purta id-ul admin', fisier: S,
|
|
ancora: "if (!(g.id !== 'admin' /*PAZA:grant-form*/)) throw", inlocuire: 'if (false) throw',
|
|
rosii: [['mesajele nu reflecta', 'grant admin: trebuia refuzat']] },
|
|
{ id: 'P36', defect: 'fara plafon de grant-uri la creare', fisier: S,
|
|
ancora: 'if (!(grants.length <= MAX_GRANTS /*PAZA:grant-limit*/)) throw', inlocuire: 'if (false) throw',
|
|
rosii: [['plafonul grant-urilor', '33 la creare: trebuia refuzat']] },
|
|
{ id: 'P37', defect: 'fara plafon de grant-uri la grant', fisier: S,
|
|
ancora: 'if (!(secret.grants.length < MAX_GRANTS /*PAZA:grant-limit*/)) throw', inlocuire: 'if (false) throw',
|
|
rosii: [['plafonul grant-urilor', 'al 33-lea: trebuia refuzat']] },
|
|
{ id: 'P38', defect: 'prezentarea se verifica din nou pentru fiecare grant (fara cache)', fisier: S,
|
|
ancora: 'r = CF.checkCompliance(p, g.policy, { ...ctx, cache });', inlocuire: 'r = CF.checkCompliance(p, g.policy, { ...ctx });',
|
|
rosii: [['plafonul grant-urilor', 'citirile semnaturii']] },
|
|
{ id: 'P39', defect: 'fara plafon de nonce-uri pe origine', fisier: S,
|
|
ancora: 'if (!(inZbor < this.#noncePerOrigin /*paznicul nonce-urilor pe origine*/)) throw', inlocuire: 'if (false) throw',
|
|
rosii: [['nonce-uri pe origine', 'al 33-lea: asteptat']] },
|
|
{ id: 'P40', defect: 'serverul nu mai da adresa la emiterea nonce-ului', fisier: SV,
|
|
ancora: "secrets.nonce({ origin: String(req.socket.remoteAddress || 'unknown') })", inlocuire: 'secrets.nonce()',
|
|
rosii: [['nonce-uri pe origine', '32 de 200: asteptat']] },
|
|
{ id: 'P41', defect: 'o scriere cazuta dupa rand nu mai primeste randul failed', fisier: S,
|
|
ancora: "try { this.#scrieAudit({ ...a, verdict: 'failed', rules: ['storage'] }, acum); } catch", inlocuire: 'try { /* fara randul failed */ } catch',
|
|
rosii: [['ordinea la mutatie', 'randul inainte, apoi failed']] },
|
|
{ id: 'P42', defect: 'delegates: false nu mai opreste delegatii', fisier: S,
|
|
ancora: 'const delegat = g.delegates === false && !!v.subject && v.subject.delegations > 0;', inlocuire: 'const delegat = false;',
|
|
rosii: [['delegates: false', 'telefonul: trebuia refuzat']] },
|
|
{ id: 'P43', defect: 'vocabularul felului secret-access nu se mai judeca (verdict banana construit)', fisier: PKS,
|
|
ancora: "if (statement[camp] !== undefined && !bun(statement[camp])) throw", inlocuire: 'if (false) throw',
|
|
rosii: [['vocabularul plicului secret-access', 'verdict="banana": construit']] },
|
|
];
|
|
|
|
const MOMELI = [
|
|
{ id: 'M1', defect: 'ancora care nu exista in cod', fisier: S, ancora: 'ACEASTA ANCORA NU EXISTA IN COD', inlocuire: 'x', rosii: [['refuz: nonce refolosit', 'x']], asteptat: 'ESEC CONTROL' },
|
|
{ id: 'M2', defect: 'plantare fara efect (un comentariu)', fisier: S, ancora: "export const KIND = 'secret-access';", inlocuire: "export const KIND = 'secret-access'; // momeala fara efect", rosii: [['refuz: nonce refolosit', 'x']], asteptat: 'VERDE (NEPRINS)' },
|
|
{ id: 'M3', defect: 'defect real, dar motivul cerut nu e cel pe care cade proba', fisier: S, ancora: 'revocations: incredere.revocations, request: rh };', inlocuire: 'revocations: [], request: rh };', rosii: [['delegare: telefonul', 'un motiv care nu apare niciodata']], asteptat: 'ROSU GRESIT' },
|
|
{ id: 'M4', defect: 'proba numita nu exista', fisier: S, ancora: 'revocations: incredere.revocations, request: rh };', inlocuire: 'revocations: [], request: rh };', rosii: [['o proba care nu exista', 'x']], asteptat: 'ESEC CONTROL' },
|
|
{ id: 'M5', defect: 'plantare care rupe incarcarea modulului', fisier: S, ancora: 'revocations: incredere.revocations, request: rh };', inlocuire: 'revocations: incredere.revocations ,, request: rh };', rosii: [['delegare: telefonul', 'delegare revocata']], asteptat: 'STRICAT' },
|
|
];
|
|
|
|
const sha = (p) => crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex');
|
|
const amprente = () => Object.fromEntries(FISIERE.map((f) => [f, sha(path.join(RADACINA, f))]));
|
|
function pregatesteCopie(dest) {
|
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
for (const f of FISIERE) { const d = path.join(dest, f); fs.mkdirSync(path.dirname(d), { recursive: true }); fs.copyFileSync(path.join(RADACINA, f), d); }
|
|
for (const f of FISIERE) if (sha(path.join(dest, f)) !== sha(path.join(RADACINA, f))) throw new Error(`copia lui ${f} difera de original inainte de plantare`);
|
|
}
|
|
function planteaza(dest, p) {
|
|
const fp = path.join(dest, p.fisier);
|
|
const text = fs.readFileSync(fp, 'utf8');
|
|
const bucati = text.split(p.ancora);
|
|
if (bucati.length !== 2) return { ok: false, motiv: `ancora apare de ${bucati.length - 1} ori in ${p.fisier} (trebuie exact o data)` };
|
|
fs.writeFileSync(fp, bucati.join(p.inlocuire));
|
|
if (sha(fp) === sha(path.join(RADACINA, p.fisier))) return { ok: false, motiv: 'fisierul plantat e identic cu originalul' };
|
|
return { ok: true };
|
|
}
|
|
const envCurat = () => Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith('AERE_KMS_') && !k.startsWith('AERE_SECRETS_')));
|
|
function ruleaza(dest) {
|
|
const out = path.join(dest, 'rezultat-proba.json');
|
|
fs.rmSync(out, { force: true });
|
|
const t0 = Date.now();
|
|
const env = { ...envCurat(), AERE_SECRETS_PROBA_JSON: out };
|
|
if (VERIFY) env.AERE_VERIFY_PROOF = VERIFY; else delete env.AERE_VERIFY_PROOF;
|
|
return new Promise((gata) => {
|
|
const c = spawn(process.execPath, [path.join(dest, PR)], { cwd: path.join(dest, ASEZARE.sec), env, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
let stderr = ''; c.stderr.on('data', (d) => { stderr = (stderr + d).slice(-4000); });
|
|
const ceas = setTimeout(() => c.kill(), 600000);
|
|
c.on('close', (status, signal) => {
|
|
clearTimeout(ceas);
|
|
let json = null;
|
|
try { json = JSON.parse(fs.readFileSync(out, 'utf8')); } catch { json = null; }
|
|
gata({ json, status, signal, ms: Date.now() - t0, stderr: stderr.slice(-600) });
|
|
});
|
|
});
|
|
}
|
|
// o proba se numeste prin INCEPUTUL numelui ei, care trebuie sa fie unic
|
|
function gaseste(nume, prefix) { const c = nume.filter((n) => n.startsWith(prefix)); return c.length === 1 ? c[0] : null; }
|
|
|
|
async function judecaPlantarea(dest, p, nume, baza) {
|
|
pregatesteCopie(dest);
|
|
const numite = p.rosii.map(([pre]) => [pre, gaseste(nume, pre)]);
|
|
const lipsa = numite.filter(([, n]) => !n).map(([pre]) => pre);
|
|
if (lipsa.length) return { p, verdict: 'ESEC CONTROL', detaliu: `proba numita nu exista (sau prefixul nu e unic): ${lipsa.join(' | ')}`, esec: true };
|
|
const pl = planteaza(dest, p);
|
|
if (!pl.ok) return { p, verdict: 'ESEC CONTROL', detaliu: `plantarea nu s-a putut pune: ${pl.motiv}`, esec: true };
|
|
const r = await ruleaza(dest);
|
|
if (!r.json || r.json.completed !== true || r.json.total !== baza.json.total) return { p, verdict: 'STRICAT', detaliu: `proba nu a rulat intreaga (cod ${r.status}, probe ${r.json ? r.json.total : '-'}/${baza.json.total}); ${r.stderr.split('\n').slice(-3).join(' ')}`, esec: true };
|
|
const dupaNume = new Map(r.json.results.map((x) => [x.nume, x]));
|
|
const probleme = []; let rosiiNumite = 0;
|
|
for (let i = 0; i < p.rosii.length; i++) {
|
|
const [, n] = numite[i]; const motiv = p.rosii[i][1]; const x = dupaNume.get(n);
|
|
if (x.ok) probleme.push(`VERDE: ${n.slice(0, 70)}`);
|
|
else if (!String(x.motiv).includes(motiv)) probleme.push(`ROSU DIN ALT MOTIV: ${n.slice(0, 60)} -> "${String(x.motiv).slice(0, 160)}" (cerut: "${motiv}")`);
|
|
else rosiiNumite++;
|
|
}
|
|
const inPlus = r.json.failed.filter((n) => !numite.some(([, m]) => m === n));
|
|
if (probleme.length === 0) return { p, verdict: 'ROSU', detaliu: `${rosiiNumite}/${p.rosii.length} numite rosii pe motivul cerut; alte ${inPlus.length} rosii in plus`, inPlus, ms: r.ms };
|
|
return { p, verdict: r.json.failed.length === 0 ? 'VERDE (NEPRINS)' : 'ROSU GRESIT', detaliu: probleme.join('\n '), inPlus, ms: r.ms, esec: true };
|
|
}
|
|
|
|
async function main() {
|
|
const autoproba = process.argv.includes('--autoproba');
|
|
const PL = autoproba ? MOMELI : PLANTARI;
|
|
// --ancore: numai verificarea ancorelor pe original (fiecare exact o data), fara nicio rulare
|
|
if (process.argv.includes('--ancore')) { let rele = 0; for (const p of PL) { const n = fs.readFileSync(path.join(RADACINA, p.fisier), 'utf8').split(p.ancora).length - 1; if (n !== 1) rele++; console.log(`${n === 1 ? 'OK ' : 'RAU '} ${p.id} ancora de ${n} ori in ${p.fisier}`); } return rele ? 1 : 0; }
|
|
if (new Set(PL.map((p) => p.id)).size !== PL.length) throw new Error('doua plantari cu acelasi id');
|
|
if (autoproba) console.log('AUTOPROBA: momelile de mai jos TREBUIE sa primeasca verdictul lor de esec\n');
|
|
console.log(`asezarea: ${ASEZARE_NUME} (${FISIERE.length} fisiere copiate in fiecare copie)`);
|
|
if (!VERIFY) console.log('verificatorul AIP-23 lipseste (AERE_VERIFY_PROOF): proba plicurilor va fi SARITA in toate rularile\n');
|
|
const inainte = amprente();
|
|
const lucru = fs.mkdtempSync(path.join(os.tmpdir(), 'aeresecrets-control-'));
|
|
const linii = new Array(PL.length);
|
|
let esecuri = 0;
|
|
let baza;
|
|
try {
|
|
const d0 = path.join(lucru, 'baza');
|
|
pregatesteCopie(d0);
|
|
baza = await ruleaza(d0);
|
|
if (!baza.json || baza.json.completed !== true) { console.log(`LINIA DE BAZA STRICATA: proba nu a ajuns la capat (cod ${baza.status}, semnal ${baza.signal})\n${baza.stderr}`); return 1; }
|
|
if (baza.json.failed.length !== 0) { console.log(`LINIA DE BAZA ROSIE: ${baza.json.failed.length} probe pica pe codul neatins:\n ${baza.json.failed.join('\n ')}`); return 1; }
|
|
const nume = baza.json.results.map((r) => r.nume);
|
|
console.log(`linia de baza: VERDE, ${baza.json.passed}/${baza.json.total} probe (${baza.json.skipped.length} sarite), ${baza.ms} ms (node ${baza.json.node}, openssl ${baza.json.openssl}); ${LUCRATORI} lucratori\n`);
|
|
let urm = 0;
|
|
await Promise.all(Array.from({ length: LUCRATORI }, async (_, k) => {
|
|
const dest = path.join(lucru, 'copie-' + k);
|
|
while (urm < PL.length) { const i = urm++; linii[i] = await judecaPlantarea(dest, PL[i], nume, baza); }
|
|
}));
|
|
} finally { fs.rmSync(lucru, { recursive: true, force: true }); }
|
|
for (const l of linii) {
|
|
if (l.esec) esecuri++;
|
|
console.log(`${l.verdict.padEnd(15)} ${l.p.id} ${l.p.defect}${l.ms ? ` (${l.ms} ms)` : ''}\n ${l.detaliu}`);
|
|
if (l.inPlus && l.inPlus.length) console.log(` in plus: ${l.inPlus.map((n) => n.slice(0, 60)).join(' | ')}`);
|
|
}
|
|
const dupa = amprente();
|
|
const schimbate = FISIERE.filter((f) => inainte[f] !== dupa[f]);
|
|
if (schimbate.length) { esecuri++; console.log(`\nORIGINALUL S-A SCHIMBAT: ${schimbate.join(', ')}`); } else console.log(`\noriginal neatins (sha256): ${FISIERE.map((f) => `${path.basename(f)}=${dupa[f].slice(0, 10)}`).join(' ')}`);
|
|
if (autoproba) {
|
|
let potrivite = 0;
|
|
for (const l of linii) { const ok = l.verdict === l.p.asteptat; if (ok) potrivite++; console.log(`${ok ? 'CORECT ' : 'GRESIT '} ${l.p.id}: controlul a spus "${l.verdict}", trebuia "${l.p.asteptat}"`); }
|
|
const originalOk = schimbate.length === 0;
|
|
console.log(`\nautoproba controlului: ${potrivite}/${MOMELI.length} momeli cu verdictul cerut; original ${originalOk ? 'neatins' : 'SCHIMBAT'}`);
|
|
return potrivite === MOMELI.length && linii.length === MOMELI.length && originalOk ? 0 : 1;
|
|
}
|
|
const rosii = linii.filter((l) => l.verdict === 'ROSU').length;
|
|
console.log(`\ncontrol negativ: ${rosii}/${PLANTARI.length} plantari prinse pe probele numite; ${esecuri} esecuri ale controlului`);
|
|
return esecuri === 0 && rosii === PLANTARI.length ? 0 : 1;
|
|
}
|
|
|
|
main().then((c) => { process.exitCode = c; }, (e) => { console.log(`controlul negativ a cazut: ${(e && e.stack) || e}`); process.exitCode = 2; });
|