121 lines
14 KiB
JavaScript
121 lines
14 KiB
JavaScript
'use strict';
|
|
// proba-adaptoare-cloud.mjs (B15, 2026-10-18): proba adaptoarelor pe furnizori de cloud ale sidecar-ului (scan --source aws-ecs | aws-ec2 |
|
|
// aws-lambda | azure-vm | azure-container | azure-containerapp | azure-webapp | gcp-compute | gcp-run), pe fixturile DERIVATE din
|
|
// documentatia oficiala a fiecarui CLI (fixturi-cloud/). Pentru fiecare adaptor:
|
|
// 1 exportul documentat -> exact incarcaturile care RULEAZA, cu identitatile asteptate (nume si versiune in plic)
|
|
// 2 CONFIDENTIALITATE: valorile de mediu plantate (si customData/userData) NU apar nici in jurnal, nici in descriptori; NUMELE da
|
|
// 3 incarcatura OPRITA din varianta `.oprita` NU intra (acelasi numar de intrari, numele ei lipseste din tot ce s-a scris)
|
|
// 4 exportul ALTUI CLI e refuzat PE NUME (mesajul numeste comanda asteptata), nimic scris
|
|
// 5 fiecare descriptor re-hashat (canonic, chei sortate la toate nivelurile) = digestul din plicul lui din lant
|
|
// 6 fiecare plic e VALID pentru verificatorul AIP-23 comun (AERE_VERIFY_PROOF; altfel SARIT, cod 2)
|
|
// plus: fixturile de pe disc sunt cele derivate; o sursa necunoscuta e refuzata; ECS fara definitii spune `not-in-export`; `az vm list`
|
|
// fara -d si `az container list` fara instanceView sunt refuzate pe nume; referinta ARN a unui secret ECS si cheile Azure de retea nu intra.
|
|
// node proba-adaptoare-cloud.mjs -> 0 toate cum trebuia, 1 cel putin una rea, 2 cel putin una SARITA
|
|
// Mediu: AERE_SIDECAR_MODUL (copia masurata, pentru controlul negativ), AERE_VERIFY_PROOF (verificatorul AIP-23).
|
|
// NEMASURAT aici, spus si in fixturi-cloud/README.md: iesirea unui CONT REAL la oricare furnizor; nu avem conturi.
|
|
|
|
import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import crypto from 'node:crypto';
|
|
import { execFileSync } from 'node:child_process'; import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
|
const AICI = path.dirname(fileURLToPath(import.meta.url));
|
|
const SIDE = process.env.AERE_SIDECAR_MODUL ? path.resolve(process.env.AERE_SIDECAR_MODUL) : path.join(AICI, 'sidecar.mjs');
|
|
const VERIFY = process.env.AERE_VERIFY_PROOF ? path.resolve(process.env.AERE_VERIFY_PROOF) : path.resolve(AICI, '..', 'aere-proof-protocol', 'verify.mjs');
|
|
const areVerificator = fs.existsSync(VERIFY);
|
|
const FX = path.join(AICI, 'fixturi-cloud');
|
|
const { deriva, text: textFixtura, SECRET, SECRET_DATE } = await import(pathToFileURL(path.join(FX, 'deriva.mjs')).href);
|
|
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'avl-cloud-'));
|
|
let rele = 0, bune = 0, sarite = 0;
|
|
const cer = (n, c) => { console.log(` [${c ? 'OK ' : 'RAU '}] ${n}`); if (c) bune++; else rele++; };
|
|
const sari = (n) => { console.log(` [SARIT] ${n} (verificatorul AIP-23 nu e la ${VERIFY}; dati AERE_VERIFY_PROOF)`); sarite++; };
|
|
function side(args) { try { return { cod: 0, out: execFileSync(process.execPath, [SIDE, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }) }; } catch (e) { return { cod: e.status ?? 1, out: (e.stdout || '') + (e.stderr || '') }; } }
|
|
const linii = (p) => (fs.existsSync(p) ? fs.readFileSync(p, 'utf8').split('\n').filter((l) => l.trim()).map((l) => JSON.parse(l)) : []);
|
|
const textul = (p) => (fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '');
|
|
const valid = (f) => { try { execFileSync(process.execPath, [VERIFY, f], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); return true; } catch { return false; } };
|
|
// re-derivarea canonica, scrisa AICI, nu importata: chei sortate la toate nivelurile
|
|
const canon = (v) => (Array.isArray(v) ? '[' + v.map(canon).join(',') + ']' : v && typeof v === 'object' ? '{' + Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + canon(v[k])).join(',') + '}' : JSON.stringify(v));
|
|
const shaCanon = (o) => '0x' + crypto.createHash('sha256').update(Buffer.from(canon(o), 'utf8')).digest('hex');
|
|
const F = (f) => path.join(FX, f);
|
|
const egal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
const scan = (src, fixtura, log, extra = []) => side(['scan', '--source', src, '--input', F(fixtura), '--host', 'h', '--log', log, '--at', '2026-10-18T10:00:00Z', ...extra]);
|
|
|
|
// pentru fiecare adaptor: exportul, identitatile asteptate (din fixtura documentata), numele variabilelor plantate, marcajul incarcaturii oprite,
|
|
// exportul altui CLI si comanda pe care refuzul trebuie sa o numeasca
|
|
const H64 = '0123456789abcdef'.repeat(4);
|
|
const A = [
|
|
{ src: 'aws-ecs', extra: ['--task-definitions', F('aws-ecs.definitii.json')], extraSecrete: ['--task-definitions', F('aws-ecs.secrete.definitii.json')],
|
|
nume: ['ecs:MyCluster/4d590253bb114126b7afa7b58eea9221/simple-app'], versiuni: ['httpd:2.4'], numeEnv: ['API_TOKEN', 'HTTPD_PORT'], numePlantate: ['DB_PASSWORD', 'OVERRIDE_TOKEN'],
|
|
oprit: '0000000000000000000000oprita', altCli: 'aws-ec2.json', refuz: 'aws ecs describe-tasks' },
|
|
{ src: 'aws-ec2', nume: ['ec2:us-east-2/i-1234567890abcdef0'], versiuni: ['ami-0abcdef1234567890'], numeEnv: ['Name'], numePlantate: ['DB_PASSWORD'], oprit: 'i-0000000000oprita00', altCli: 'aws-lambda.json', refuz: 'aws ec2 describe-instances' },
|
|
{ src: 'aws-lambda', nume: ['lambda:us-west-2/helloworld', 'lambda:us-west-2/my-function', 'lambda:us-west-2/my-python-function'],
|
|
versiuni: ['dBG9m8SGdmlEjw/JYXlhhvCrAv5TxvXsbL/RMr0fT/I=', 'sU0cJ2/hOZevwV/lTxCuQqK3gDZP3i8gUoqUUVRmY6E=', 'ZQukCqxtkqFgyF2cU41Avj99TKQ/hNihPtDtRcc08mI='],
|
|
numeEnv: [], numePlantate: ['DB_PASSWORD', 'LOG_LEVEL'], oprit: 'inactive-function', altCli: 'aws-ecs.json', refuz: 'aws lambda list-functions' },
|
|
{ src: 'azure-vm', nume: ['azvm:{resourceGroupName}/{virtualMachineName}', 'azvm:{resourceGroupName}/{virtualMachineName}'], versiuni: ['aaaaaaaaaaaaa', 'aa'], numeEnv: [], numePlantate: ['DB_PASSWORD', 'env'],
|
|
plantateInPlus: [SECRET_DATE], oprit: 'vm-oprita', altCli: 'azure-container.json', refuz: 'az vm list -d' },
|
|
{ src: 'azure-container', nume: ['aci:demo/demo1/demo1'], versiuni: ['nginx'], numeEnv: [], numePlantate: ['API_KEY', 'DB_PASSWORD'], oprit: 'demo2', altCli: 'azure-vm.json', refuz: 'az container list' },
|
|
{ src: 'azure-containerapp', nume: ['aca:rg/testcontainerApp0/testcontainerApp0'], versiuni: ['repo/testcontainerApp0:v4'], numeEnv: [], numePlantate: ['API_KEY', 'DB_PASSWORD'], oprit: 'testcontainerApp1', altCli: 'azure-webapp.json', refuz: 'az containerapp list' },
|
|
{ src: 'azure-webapp', nume: ['azwebapp:testrg123/sitef6141', 'azwebapp:testrg123/sitef7252'], versiuni: ['mcr.microsoft.com/appsvc/staticsite:latest', '?'], numeEnv: [], numePlantate: ['DB_PASSWORD', 'WEBSITES_PORT'],
|
|
oprit: 'sitef0000', altCli: 'azure-containerapp.json', refuz: 'az webapp list --show-details' },
|
|
{ src: 'gcp-compute', nume: ['gce:us-central1-a/api-1'], versiuni: ['4218766341062832345'], numeEnv: ['enable-oslogin', 'ssh-keys', 'startup-script'], numePlantate: ['user-data'], oprit: 'api-2', altCli: 'gcp-run.json', refuz: 'gcloud compute instances list --format=json' },
|
|
{ src: 'gcp-run', nume: ['run:europe-west1/api/api-1'], versiuni: ['sha256:' + H64], numeEnv: ['DB_PASSWORD', 'LOG_LEVEL'], numePlantate: ['DB_PASSWORD_PLAIN'], oprit: '"batch"', altCli: 'gcp-compute.json', refuz: 'gcloud run services list --format=json' },
|
|
];
|
|
|
|
try {
|
|
// fixturile de pe disc sunt EXACT cele derivate din surse (altfel proba ar masura un fisier editat de mana)
|
|
const derivate = deriva(); const difera = Object.entries(derivate).filter(([f, o]) => textul(F(f)) !== textFixtura(o)).map(([f]) => f);
|
|
cer(`fixturile de pe disc sunt cele derivate de deriva.mjs (${Object.keys(derivate).length} fisiere)`, difera.length === 0 && Object.keys(derivate).length >= 27);
|
|
cer('CONTROL: o sursa necunoscuta e refuzata (cod 2) si uzanta numeste toate sursele', (() => { const r = side(['scan', '--source', 'aws-foo', '--input', F('aws-ecs.json'), '--log', path.join(T, 'foo.log')]); return r.cod === 2 && A.every((a) => r.out.includes(a.src)) && !fs.existsSync(path.join(T, 'foo.log')); })());
|
|
|
|
const plicuri = [];
|
|
for (const a of A) {
|
|
const P = (s) => `${a.src}: ${s}`;
|
|
// 1: exportul documentat -> incarcaturile care ruleaza, identitatile exacte
|
|
const log = path.join(T, a.src + '.log'); const des = log + '.descriptori.jsonl';
|
|
const r1 = scan(a.src, a.src + '.json', log, a.extra || []);
|
|
const L = linii(log); const D = linii(des);
|
|
const numeScrise = D.map((d) => d.name); const numeInPlic = L.map((e) => e.proof.statement.name);
|
|
cer(P(`exportul documentat -> ${a.nume.length} intrari, identitatile exacte (${a.nume.map((n, i) => `${n}@${a.versiuni[i]}`).join(', ')})`),
|
|
r1.cod === 0 && L.length === a.nume.length && egal(numeScrise, a.nume) && egal(numeInPlic, a.nume.map((n, i) => `h/${n}@${a.versiuni[i]}`)));
|
|
// 5: re-hash
|
|
cer(P('descriptorii re-hashati (canonic, adanc) = digestul din plicurile lantului'), D.length === L.length && D.length > 0 && D.every((d) => shaCanon(d.descriptor) === d.sha256 && L[d.seq].proof.statement.sha256 === d.sha256));
|
|
if (a.numeEnv.length) cer(P(`control pozitiv: numele documentate (${a.numeEnv.join(', ')}) sunt in descriptori`), a.numeEnv.every((n) => textul(des).includes(`"${n}"`)));
|
|
for (const e of L) plicuri.push({ src: a.src, f: path.join(T, `${a.src}-${e.seq}.json`), p: e.proof });
|
|
// 2: confidentialitate, pe varianta cu valori plantate
|
|
const logS = path.join(T, a.src + '.secrete.log'); const desS = logS + '.descriptori.jsonl';
|
|
const r2 = scan(a.src, a.src + '.secrete.json', logS, a.extraSecrete || a.extra || []);
|
|
const tot = textul(logS) + textul(desS); const plantate = [SECRET, ...(a.plantateInPlus || [])];
|
|
cer(P(`CONFIDENTIALITATE: valorile plantate NU apar nici in jurnal, nici in descriptori; NUMELE (${a.numePlantate.join(', ')}) da`),
|
|
r2.cod === 0 && linii(logS).length === a.nume.length && plantate.every((s) => !tot.includes(s)) && a.numePlantate.every((n) => textul(desS).includes(`"${n}"`)));
|
|
// 3: incarcatura oprita nu intra
|
|
const logO = path.join(T, a.src + '.oprita.log'); const r3 = scan(a.src, a.src + '.oprita.json', logO, a.extra || []);
|
|
const totO = textul(logO) + textul(logO + '.descriptori.jsonl');
|
|
cer(P(`incarcatura OPRITA (${a.oprit}) NU intra: tot atatea intrari ca fara ea, numele ei lipseste`), r3.cod === 0 && linii(logO).length === a.nume.length && !totO.includes(a.oprit));
|
|
// 4: exportul altui CLI, refuzat pe nume
|
|
const logA = path.join(T, a.src + '.alt.log'); const r4 = scan(a.src, a.altCli, logA);
|
|
cer(P(`CONTROL: export de alt CLI (${a.altCli}) refuzat PE NUME (cod 2, "not the output of \`${a.refuz}\`"), nimic scris`), r4.cod === 2 && r4.out.includes(`is not the output of \`${a.refuz}\``) && !fs.existsSync(logA));
|
|
}
|
|
// 6: moatul, pentru toate plicurile adaptoarelor deodata
|
|
if (areVerificator) { let toate = true; for (const x of plicuri) { fs.writeFileSync(x.f, JSON.stringify(x.p)); if (!valid(x.f)) toate = false; } cer(`moat: fiecare plic al celor ${A.length} adaptoare (${plicuri.length}) e VALID pentru verificatorul AIP-23 comun`, toate && plicuri.length >= 13); }
|
|
else sari(`moat: fiecare plic al celor ${A.length} adaptoare e VALID pentru verificatorul AIP-23 comun`);
|
|
|
|
// particularitati documentate
|
|
{ const log = path.join(T, 'ecs-fara-def.log'); const r = scan('aws-ecs', 'aws-ecs.json', log); const d = linii(log + '.descriptori.jsonl')[0]?.descriptor || {};
|
|
cer('aws-ecs fara --task-definitions: intra, cu envSource not-in-export si envNames goale (numele stau in definitie)', r.cod === 0 && d.envSource === 'not-in-export' && egal(d.envNames, []) && d.commandSha256 === null); }
|
|
{ const d = linii(path.join(T, 'aws-ecs.log.descriptori.jsonl'))[0]?.descriptor || {}; const t = textul(path.join(T, 'aws-ecs.log.descriptori.jsonl'));
|
|
cer('aws-ecs cu definitii: envSource task-definition, referinta ARN a secretului (valueFrom) NU intra, montarea intra cu calea din container', d.envSource === 'task-definition' && !t.includes('secretsmanager') && egal(d.mounts, ['/usr/local/apache2/htdocs'])); }
|
|
{ const r = scan('azure-vm', 'azure-vm.fara-d.json', path.join(T, 'vm-fara-d.log'));
|
|
cer('CONTROL: `az vm list` fara -d (fara powerState) -> refuzat pe nume (cod 2), nimic scris', r.cod === 2 && /has no powerState/.test(r.out) && r.out.includes('az vm list -d') && !fs.existsSync(path.join(T, 'vm-fara-d.log'))); }
|
|
{ const t = textul(path.join(T, 'azure-vm.secrete.log.descriptori.jsonl'));
|
|
cer('azure-vm: customData, userData, adminUsername, cheile ssh si adresele (publicIps, privateIps) NU sunt chei ale descriptorului', t.length > 0 && !/"(customData|userData|adminUsername|publicKeys|publicIps|privateIps|fqdns|macAddresses)"/.test(t)); }
|
|
{ const r = scan('azure-container', 'azure-container.fara-stare.json', path.join(T, 'aci-fara-stare.log'));
|
|
cer('CONTROL: `az container list` fara instanceView -> refuzat pe nume (cod 2, cere `az container show`), nimic scris', r.cod === 2 && /instanceView\.currentState/.test(r.out) && r.out.includes('az container show') && !fs.existsSync(path.join(T, 'aci-fara-stare.log'))); }
|
|
{ const d = linii(path.join(T, 'azure-webapp.log.descriptori.jsonl')).map((x) => x.descriptor);
|
|
cer('azure-webapp: situl cu DOCKER|imagine are imageIdentity declared si imaginea; cel fara runtime in export are not-in-export', d.length === 2 && d[0].imageIdentity === 'declared' && d[0].image === 'mcr.microsoft.com/appsvc/staticsite:latest' && d[1].imageIdentity === 'not-in-export' && d[1].image === null); }
|
|
{ const d = linii(path.join(T, 'gcp-run.log.descriptori.jsonl'))[0]?.descriptor || {};
|
|
cer('gcp-run: imaginea data prin digest -> imageIdentity digest, regiunea din eticheta cloud.googleapis.com/location, valueFrom nu intra', d.imageIdentity === 'digest' && d.imageDigest === 'sha256:' + H64 && d.region === 'europe-west1' && !JSON.stringify(d).includes('secretKeyRef')); }
|
|
{ const d = linii(path.join(T, 'aws-lambda.log.descriptori.jsonl')).map((x) => x.descriptor);
|
|
cer('aws-lambda: list-functions nu poarta State -> state not-in-export, identitatea e CodeSha256 (imageIdentity codeSha256)', d.length === 3 && d.every((x) => x.state === 'not-in-export' && x.imageIdentity === 'codeSha256' && x.codeSha256)); }
|
|
} catch (e) { console.log(` [RAU ] proba a cazut: ${e.stack || e.message}`); rele++; }
|
|
finally { fs.rmSync(T, { recursive: true, force: true }); }
|
|
console.log(`\nB15 adaptoare cloud: ${bune}/${bune + rele + sarite} cum trebuia${sarite ? `, ${sarite} SARITE` : ''} (sidecar: ${SIDE})`);
|
|
process.exitCode = rele ? 1 : sarite ? 2 : 0;
|