424 lines
19 KiB
JavaScript
424 lines
19 KiB
JavaScript
// test/control-negativ.mjs - controlul negativ al probei Aere PQ KMS.
|
|
//
|
|
// O proba care nu poate iesi rosie nu masoara nimic. Aici se planteaza, intr-o COPIE a codului,
|
|
// 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".
|
|
//
|
|
// Reguli, platite in alte parti ale casei:
|
|
// - verdictul se citeste din JSON-ul probei (AERE_KMS_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;
|
|
// - copia se reface pentru FIECARE plantare si se compara sha256 cu originalul inainte de a
|
|
// planta (altfel s-ar masura plantarea de dinainte);
|
|
// - 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 se fac cu o conditie falsa la RULARE sau cu o inlocuire care ramane cod valid,
|
|
// ca rosul sa vina din proba, nu din incarcarea modulului;
|
|
// - la sfarsit originalul trebuie sa fie identic octet cu octet (sha256).
|
|
//
|
|
// 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. Ruleaza momeli (ancora inexistenta, plantare fara efect,
|
|
// motiv gresit, proba inexistenta, modul care nu se incarca) si cere ca fiecare sa primeasca
|
|
// verdictul ei de ESEC. Un control care nu a fost vazut esuand nu se poate crede.
|
|
|
|
import crypto from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
const RADACINA = path.resolve(HERE, '..');
|
|
// 2026-09-28: server.mjs importa si hsm-radacina.mjs (radacina sigilata de HSM); fara el copia nu mai pornea si linia de baza iesea STRICATA
|
|
const FISIERE = ['kms.mjs', 'server.mjs', 'hsm-radacina.mjs', 'test/proba.mjs'];
|
|
const NICIODATA = "process.env.AERE_KMS_PLANTA_NICIODATA === 'da'";
|
|
|
|
// Fiecare plantare: fisierul, ancora (exact o aparitie), inlocuirea, si probele care TREBUIE sa
|
|
// iasa rosii, fiecare cu un fragment din motivul pe care trebuie sa cada.
|
|
const PLANTARI = [
|
|
{
|
|
id: 'P16',
|
|
defect: 'A4: mutatia ramane pe disc cand randul de audit nu se poate scrie (rollback-ul nu se mai face)',
|
|
fisier: 'kms.mjs',
|
|
ancora: ' if (!changed) return;',
|
|
inlocuire: " if (!changed || !(process.env.AERE_KMS_PLANTA_NICIODATA === 'da')) return;",
|
|
rosii: [
|
|
['audit: o mutatie al carei rand de audit nu se poate scrie NU ramane pe disc (rotire, minim, creare), iar lantul ramane valid', 'rotirea a ramas pe disc fara rand de audit'],
|
|
],
|
|
},
|
|
{
|
|
id: 'P01',
|
|
defect: 'KDF fara secretul ML-KEM (IKM = numai X25519)',
|
|
fisier: 'kms.mjs',
|
|
ancora: 'const ikm = Buffer.concat([ssK, ssX]);',
|
|
inlocuire: 'const ikm = Buffer.concat([ssX]);',
|
|
rosii: [
|
|
['criptare: atacatorul cu doar secretul X25519 nu poate deriva cheia plicului', 'plicul s-a deschis numai cu secretul X25519'],
|
|
['interop: plicul serviciului decapsulat si decriptat manual cu node:crypto', 'commit'],
|
|
['interop: plic construit fara unul din secrete e refuzat de serviciu cu HEADER_AUTH_FAILED', 'KDF doar-x: trebuia refuzat'],
|
|
],
|
|
},
|
|
{
|
|
id: 'P02',
|
|
defect: 'KDF fara secretul X25519 (IKM = numai ML-KEM)',
|
|
fisier: 'kms.mjs',
|
|
ancora: 'const ikm = Buffer.concat([ssK, ssX]);',
|
|
inlocuire: 'const ikm = Buffer.concat([ssK]);',
|
|
rosii: [
|
|
['criptare: atacatorul cu doar secretul ML-KEM nu poate deriva cheia plicului', 'plicul s-a deschis numai cu secretul ML-KEM'],
|
|
['interop: plic construit fara unul din secrete e refuzat de serviciu cu HEADER_AUTH_FAILED', 'KDF doar-k: trebuia refuzat'],
|
|
],
|
|
},
|
|
{
|
|
id: 'P03',
|
|
defect: 'verificarea accepta o singura semnatura (SAU in loc de SI)',
|
|
fisier: 'kms.mjs',
|
|
ancora: 'const valid = edOk && pqOk;',
|
|
inlocuire: 'const valid = edOk || pqOk;',
|
|
rosii: [
|
|
['semnare: jumatatea Ed25519 stricata e refuzata cu CLASSICAL_SIGNATURE_INVALID', 'valid: asteptat false'],
|
|
['semnare: jumatatea ML-DSA-65 stricata e refuzata cu PQ_SIGNATURE_INVALID', 'valid: asteptat false'],
|
|
['semnare: jumatati amestecate din doua semnaturi valide sunt refuzate', 'amestec peste "unu": asteptat false'],
|
|
],
|
|
},
|
|
{
|
|
id: 'P04',
|
|
defect: 'AAD ignorat (inlocuit cu sirul gol)',
|
|
fisier: 'kms.mjs',
|
|
ancora: "return toBytes(aad, 'aad', MAX_AAD);",
|
|
inlocuire: 'return Buffer.alloc(0);',
|
|
rosii: [
|
|
['criptare: alt aad refuzat cu AAD_MISMATCH', 'alt aad: trebuia refuzat cu AAD_MISMATCH'],
|
|
['datakey: cheia clara si cea invelita corespund; forma doar-invelita nu intoarce cheia clara', 'cheia de date legata de aad'],
|
|
['rewrap: reincapsuleaza la ultima versiune, acelasi text clar, raspunsul nu contine text clar', 'rewrap cu alt aad'],
|
|
],
|
|
},
|
|
{
|
|
id: 'P05',
|
|
defect: 'min_decryption_version ignorat',
|
|
fisier: 'kms.mjs',
|
|
ancora: 'if (ver < key.min_decryption_version) throw',
|
|
inlocuire: `if (ver < key.min_decryption_version && ${NICIODATA}) throw`,
|
|
rosii: [
|
|
['rotire: sub min_decryption_version decriptarea e refuzata cu VERSION_BELOW_MINIMUM', 'v1 sub minim: trebuia refuzat'],
|
|
['rotire: sub min_decryption_version verificarea semnaturii e refuzata cu VERSION_BELOW_MINIMUM', 'v1 sub minim: asteptat false'],
|
|
['server: flux complet prin HTTP (creare, criptare, decriptare, rotire, rewrap, datakey, semnare, verificare, audit)', 'sub minim prin HTTP'],
|
|
],
|
|
},
|
|
{
|
|
id: 'P06',
|
|
defect: 'jurnalul scris fara legatura cu randul anterior (prev constant)',
|
|
fisier: 'kms.mjs',
|
|
ancora: 'prev: this.#head.mac,',
|
|
inlocuire: 'prev: ZERO_MAC,',
|
|
rosii: [
|
|
['audit: jurnalul neatins se verifica OK, un rand pe operatie, si refuzurile au motivul lor', 'jurnal neatins'],
|
|
['audit: rand sters detectat cu AUDIT_CHAIN_BROKEN pe randul lui', 'randul: asteptat 4'],
|
|
],
|
|
},
|
|
{
|
|
id: 'P07',
|
|
defect: 'verificatorul jurnalului nu verifica legatura (seq/prev)',
|
|
fisier: 'kms.mjs',
|
|
ancora: 'if (row.seq !== seq + 1 || row.prev !== prev)',
|
|
inlocuire: `if ((row.seq !== seq + 1 || row.prev !== prev) && ${NICIODATA})`,
|
|
rosii: [
|
|
['audit: rand sters detectat cu AUDIT_CHAIN_BROKEN pe randul lui', 'rand sters: asteptat false'],
|
|
['audit: randuri reordonate detectate cu AUDIT_CHAIN_BROKEN', 'reordonare: asteptat false'],
|
|
['audit: pornirea refuza un jurnal stricat cu AUDIT_CHAIN_INVALID', 'trebuia refuzat cu AUDIT_CHAIN_INVALID'],
|
|
],
|
|
},
|
|
{
|
|
id: 'P08',
|
|
defect: 'verificatorul jurnalului nu verifica mac-ul randului',
|
|
fisier: 'kms.mjs',
|
|
ancora: "if (!macOk) return fail('AUDIT_ROW_MODIFIED', i + 1);",
|
|
inlocuire: `if (!macOk && ${NICIODATA}) return fail('AUDIT_ROW_MODIFIED', i + 1);`,
|
|
rosii: [['audit: rand schimbat detectat cu AUDIT_ROW_MODIFIED', 'rand schimbat: asteptat false']],
|
|
},
|
|
{
|
|
id: 'P09',
|
|
defect: 'cheia privata scrisa si necifrata langa sigiliu',
|
|
fisier: 'kms.mjs',
|
|
ancora: 'private_sealed: sealed,',
|
|
inlocuire: "private_sealed: sealed, private_debug: Buffer.from(privJson).toString('base64'),",
|
|
rosii: [['stocare: materialul privat nu apare in clar pe disc (cu controlul pozitiv al scanerului)', 'material privat gasit in clar']],
|
|
},
|
|
{
|
|
id: 'P10',
|
|
defect: 'integritatea fisierului de cheie neverificata',
|
|
fisier: 'kms.mjs',
|
|
ancora: "if (!macOk) throw new KmsError('KEY_FILE_TAMPERED'",
|
|
inlocuire: `if (!macOk && ${NICIODATA}) throw new KmsError('KEY_FILE_TAMPERED'`,
|
|
rosii: [
|
|
['stocare: min_decryption_version coborat pe disc refuzat cu KEY_FILE_TAMPERED', 'trebuia refuzat cu KEY_FILE_TAMPERED'],
|
|
['stocare: cheie publica inlocuita pe disc refuzata cu KEY_FILE_TAMPERED', 'trebuia refuzat cu KEY_FILE_TAMPERED'],
|
|
],
|
|
},
|
|
{
|
|
id: 'P11',
|
|
defect: 'fara cheie radacina se genereaza una in tacere',
|
|
fisier: 'kms.mjs',
|
|
ancora: "if (s === '') throw new KmsError('ROOT_KEY_MISSING',",
|
|
inlocuire: "if (s === '') return crypto.randomBytes(32); if (s === '') throw new KmsError('ROOT_KEY_MISSING',",
|
|
rosii: [
|
|
['radacina: lipsa cheii refuza pornirea cu ROOT_KEY_MISSING si nu creeaza nimic pe disc', 'trebuia refuzat cu ROOT_KEY_MISSING'],
|
|
['server: configurarea implicita asculta pe 127.0.0.1 si cere token si cheie radacina', 'fara cheie radacina: trebuia refuzat'],
|
|
['server: pornit din linia de comanda refuza fara AERE_KMS_ROOT_KEY si nu tipareste tokenul', 'codul de iesire'],
|
|
],
|
|
},
|
|
{
|
|
id: 'P12',
|
|
defect: 'amprenta cheii/versiunii din plic neverificata',
|
|
fisier: 'kms.mjs',
|
|
ancora: 'if (!fp.equals(fpExpected)) {',
|
|
inlocuire: `if (!fp.equals(fpExpected) && ${NICIODATA}) {`,
|
|
rosii: [
|
|
['criptare: alta cheie refuzata cu KEY_MISMATCH', 'motiv asteptat KEY_MISMATCH'],
|
|
['semnare: alta cheie refuzata cu KEY_MISMATCH, semnatura malformata cu MALFORMED_SIGNATURE', 'motivul: asteptat "KEY_MISMATCH"'],
|
|
],
|
|
},
|
|
{
|
|
id: 'P13',
|
|
defect: 'rewrap intoarce si textul clar',
|
|
fisier: 'kms.mjs',
|
|
ancora: 'return { value: { ciphertext: r.ciphertext, version: r.version }, version: r.version };',
|
|
inlocuire: "return { value: { ciphertext: r.ciphertext, version: r.version, plaintext: plaintext.toString('base64') }, version: r.version };",
|
|
rosii: [
|
|
['rewrap: reincapsuleaza la ultima versiune, acelasi text clar, raspunsul nu contine text clar', 'campurile raspunsului'],
|
|
['server: flux complet prin HTTP (creare, criptare, decriptare, rotire, rewrap, datakey, semnare, verificare, audit)', 'rewrap nu intoarce text clar'],
|
|
],
|
|
},
|
|
{
|
|
id: 'P14',
|
|
defect: 'serverul lasa sa treaca o cerere fara Authorization',
|
|
fisier: 'server.mjs',
|
|
ancora: "if (typeof header !== 'string' || header === '') return 'UNAUTHENTICATED';",
|
|
inlocuire: "if (typeof header !== 'string' || header === '') return null;",
|
|
rosii: [
|
|
['server: fara Authorization raspunde 401 UNAUTHENTICATED', 'fara token: asteptat 401'],
|
|
['server: pornit din linia de comanda asculta pe 127.0.0.1 si raspunde', 'cerere neautentificata'],
|
|
],
|
|
},
|
|
{
|
|
id: 'P15',
|
|
defect: 'serverul ignora limita de marime a cererii',
|
|
fisier: 'server.mjs',
|
|
ancora: 'return n > maxBody;',
|
|
inlocuire: 'return false;',
|
|
rosii: [
|
|
['server: cerere peste limita cu Content-Length raspunde 413 BODY_TOO_LARGE', 'peste limita: asteptat 413'],
|
|
['server: cerere peste limita fara Content-Length (chunked) raspunde 413 BODY_TOO_LARGE', 'peste limita, chunked: asteptat 413'],
|
|
],
|
|
},
|
|
];
|
|
|
|
// Momelile pentru --autoproba: controlul insusi trebuie sa poata iesi rosu. Fiecare momeala e o
|
|
// plantare stricata dinadins si are verdictul pe care controlul TREBUIE sa i-l dea.
|
|
const MOMELI = [
|
|
{
|
|
id: 'M1',
|
|
defect: 'ancora care nu exista in cod',
|
|
fisier: 'kms.mjs',
|
|
ancora: 'ACEASTA ANCORA NU EXISTA IN COD',
|
|
inlocuire: 'x',
|
|
rosii: [['semnare: semnatura hibrida se verifica, cu ambele jumatati raportate', 'x']],
|
|
asteptat: 'ESEC CONTROL',
|
|
},
|
|
{
|
|
id: 'M2',
|
|
defect: 'plantare fara efect (un comentariu)',
|
|
fisier: 'kms.mjs',
|
|
ancora: "export const PREFIX = 'aerekms';",
|
|
inlocuire: "export const PREFIX = 'aerekms'; // momeala fara efect",
|
|
rosii: [['semnare: semnatura hibrida se verifica, cu ambele jumatati raportate', 'x']],
|
|
asteptat: 'VERDE (NEPRINS)',
|
|
},
|
|
{
|
|
id: 'M3',
|
|
defect: 'defect real, dar motivul cerut nu e cel pe care cade proba',
|
|
fisier: 'kms.mjs',
|
|
ancora: 'const valid = edOk && pqOk;',
|
|
inlocuire: 'const valid = edOk || pqOk;',
|
|
rosii: [['semnare: jumatatea Ed25519 stricata e refuzata cu CLASSICAL_SIGNATURE_INVALID', 'un motiv care nu apare niciodata']],
|
|
asteptat: 'ROSU GRESIT',
|
|
},
|
|
{
|
|
id: 'M4',
|
|
defect: 'proba numita nu exista',
|
|
fisier: 'kms.mjs',
|
|
ancora: 'const valid = edOk && pqOk;',
|
|
inlocuire: 'const valid = edOk || pqOk;',
|
|
rosii: [['o proba care nu exista', 'x']],
|
|
asteptat: 'ESEC CONTROL',
|
|
},
|
|
{
|
|
id: 'M5',
|
|
defect: 'plantare care rupe incarcarea modulului',
|
|
fisier: 'kms.mjs',
|
|
ancora: 'const valid = edOk && pqOk;',
|
|
inlocuire: 'const valid = edOk && ;',
|
|
rosii: [['semnare: jumatatea Ed25519 stricata e refuzata cu CLASSICAL_SIGNATURE_INVALID', 'valid: asteptat false']],
|
|
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)` };
|
|
const nou = bucati.join(p.inlocuire);
|
|
fs.writeFileSync(fp, nou);
|
|
if (sha(fp) === sha(path.join(RADACINA, p.fisier))) return { ok: false, motiv: 'fisierul plantat e identic cu originalul' };
|
|
return { ok: true };
|
|
}
|
|
|
|
function envCurat() {
|
|
return Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith('AERE_KMS_')));
|
|
}
|
|
|
|
function ruleaza(dest) {
|
|
const out = path.join(dest, 'rezultat-proba.json');
|
|
fs.rmSync(out, { force: true });
|
|
const t0 = Date.now();
|
|
const r = spawnSync(process.execPath, [path.join(dest, 'test', 'proba.mjs')], {
|
|
cwd: dest,
|
|
env: { ...envCurat(), AERE_KMS_PROBA_JSON: out },
|
|
encoding: 'utf8',
|
|
timeout: 300000,
|
|
maxBuffer: 16 * 1024 * 1024,
|
|
});
|
|
let json = null;
|
|
try {
|
|
json = JSON.parse(fs.readFileSync(out, 'utf8'));
|
|
} catch {
|
|
json = null;
|
|
}
|
|
return { json, status: r.status, signal: r.signal, ms: Date.now() - t0, stderr: String(r.stderr || '').slice(-600) };
|
|
}
|
|
|
|
function main() {
|
|
const autoproba = process.argv.includes('--autoproba');
|
|
const PL = autoproba ? MOMELI : PLANTARI;
|
|
if (autoproba) console.log('AUTOPROBA: momelile de mai jos TREBUIE sa primeasca verdictul lor de esec\n');
|
|
const inainte = amprente();
|
|
const lucru = fs.mkdtempSync(path.join(os.tmpdir(), 'aerekms-control-'));
|
|
const dest = path.join(lucru, 'copie');
|
|
const linii = [];
|
|
let esecuri = 0;
|
|
try {
|
|
// 1. linia de baza: copia neatinsa trebuie sa fie VERDE
|
|
pregatesteCopie(dest);
|
|
const baza = ruleaza(dest);
|
|
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;
|
|
}
|
|
const numeBaza = new Set(baza.json.results.map((r) => r.nume));
|
|
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;
|
|
}
|
|
console.log(`linia de baza: VERDE, ${baza.json.passed}/${baza.json.total} probe, ${baza.ms} ms (node ${baza.json.node}, openssl ${baza.json.openssl})\n`);
|
|
|
|
// 2. fiecare plantare pe o copie proaspata
|
|
for (const p of PL) {
|
|
pregatesteCopie(dest);
|
|
const numite = p.rosii.map(([n]) => n);
|
|
const lipsa = numite.filter((n) => !numeBaza.has(n));
|
|
if (lipsa.length) {
|
|
esecuri++;
|
|
linii.push({ p, verdict: 'ESEC CONTROL', detaliu: `proba numita nu exista in proba: ${lipsa.join(' | ')}` });
|
|
continue;
|
|
}
|
|
const pl = planteaza(dest, p);
|
|
if (!pl.ok) {
|
|
esecuri++;
|
|
linii.push({ p, verdict: 'ESEC CONTROL', detaliu: `plantarea nu s-a putut pune: ${pl.motiv}` });
|
|
continue;
|
|
}
|
|
const r = ruleaza(dest);
|
|
if (!r.json || r.json.completed !== true || r.json.total !== baza.json.total) {
|
|
esecuri++;
|
|
linii.push({ 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(' ')}` });
|
|
continue;
|
|
}
|
|
const dupaNume = new Map(r.json.results.map((x) => [x.nume, x]));
|
|
const probleme = [];
|
|
let rosiiNumite = 0;
|
|
for (const [nume, motiv] of p.rosii) {
|
|
const x = dupaNume.get(nume);
|
|
if (x.ok) probleme.push(`VERDE: ${nume}`);
|
|
else if (!String(x.motiv).includes(motiv)) probleme.push(`ROSU DIN ALT MOTIV: ${nume} -> "${x.motiv.slice(0, 160)}" (cerut: "${motiv}")`);
|
|
else rosiiNumite++;
|
|
}
|
|
const inPlus = r.json.failed.filter((n) => !numite.includes(n));
|
|
if (probleme.length === 0) {
|
|
linii.push({ p, verdict: 'ROSU', detaliu: `${rosiiNumite}/${numite.length} numite rosii pe motivul cerut; alte ${inPlus.length} rosii in plus`, inPlus, ms: r.ms });
|
|
} else {
|
|
esecuri++;
|
|
linii.push({ p, verdict: r.json.failed.length === 0 ? 'VERDE (NEPRINS)' : 'ROSU GRESIT', detaliu: probleme.join('\n '), inPlus, ms: r.ms });
|
|
}
|
|
}
|
|
} finally {
|
|
fs.rmSync(lucru, { recursive: true, force: true });
|
|
}
|
|
|
|
for (const l of linii) {
|
|
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.join(' | ')}`);
|
|
}
|
|
|
|
// 3. originalul neatins
|
|
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) => `${f}=${dupa[f].slice(0, 12)}`).join(' ')}`);
|
|
}
|
|
if (autoproba) {
|
|
// Aici esecurile sunt ceruse: fiecare momeala trebuie sa primeasca exact verdictul ei.
|
|
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;
|
|
}
|
|
|
|
try {
|
|
process.exitCode = main();
|
|
} catch (e) {
|
|
console.log(`controlul negativ a cazut: ${(e && e.stack) || e}`);
|
|
process.exitCode = 2;
|
|
}
|