150 lines
11 KiB
JavaScript
150 lines
11 KiB
JavaScript
// fixturi-organizatie.mjs: artefactele pe care se masoara judecata organizatiei (organizatie.mjs), DERIVATE din producatori, niciunul scris de
|
|
// mana in forma pe care o presupune consumatorul:
|
|
// - pentru cele 23 de feluri ale suitei de conformitate: vectorii ei (tools/aere-conformance/vectori, publicati in aere-quantum/conformance),
|
|
// pe care generatorul lor ii deriva din producatori si ii verifica octet cu octet; fiecare vector folosit aici se cere egal cu amprenta lui din
|
|
// INDEX.json. Un vector `{ artifact, input }` se IMPARTE dupa clasele din organizatie.mjs: artefactul si ce e semnat de un tert merg in pachet,
|
|
// intrarile verificatorului (ceasul, increderea, provocarea, citirile lui) in contextul celui care verifica;
|
|
// - pentru felurile planului de control: starea Shield facuta acum de sidecar-ul real (doua inregistrari, un buraf), planificatorul real si
|
|
// executorul real (uscat), iar raportul de conformitate PQ facut de raport-conformitate.mjs pe planul acela.
|
|
// Fiecare fel are un artefact BUN si un frate STRICAT, refuzat de verificatorul lui pentru o regula numita.
|
|
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';
|
|
import { INTRARI, PACK_KIND, CONTEXT_KIND, FELURI_PROPRII } from './organizatie.mjs';
|
|
|
|
const AICI = path.dirname(fileURLToPath(import.meta.url));
|
|
const RAD = path.resolve(AICI, '..', '..');
|
|
const sha = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex');
|
|
|
|
export function dosarulSuitei() {
|
|
const c = [path.join(AICI, '..', 'conformance'), path.join(RAD, 'tools', 'aere-conformance')];
|
|
return c.find((d) => fs.existsSync(path.join(d, 'vectori', 'INDEX.json'))) || null;
|
|
}
|
|
let INDEX = null;
|
|
export function indexul() {
|
|
if (INDEX) return INDEX;
|
|
const d = dosarulSuitei(); if (!d) throw new Error('the conformance vectors are not beside the control plane (../conformance/vectori) nor in tools/aere-conformance');
|
|
INDEX = { dir: path.join(d, 'vectori'), j: JSON.parse(fs.readFileSync(path.join(d, 'vectori', 'INDEX.json'), 'utf8')) };
|
|
return INDEX;
|
|
}
|
|
/** Un vector, cerut egal octet cu octet cu amprenta lui din INDEX.json (deci chiar cel scris de generator). */
|
|
export function vectorul(id) {
|
|
const { dir, j } = indexul();
|
|
const e = j.vectors.find((v) => v.id === id); if (!e) throw new Error('no vector ' + id + ' in INDEX.json');
|
|
const b = fs.readFileSync(path.join(dir, ...id.split('/')) + '.json');
|
|
if (sha(b) !== e.sha256) throw new Error(`vector ${id}: sha256 ${sha(b).slice(0, 14)} is not the one in INDEX.json (${e.sha256.slice(0, 14)})`);
|
|
return JSON.parse(b.toString('utf8'));
|
|
}
|
|
/** inputul unui vector -> partea pachetului (artefactul, ce e semnat de un tert) si partea contextului (verificatorul, ceasul) */
|
|
export function imparte(fel, input) {
|
|
const cls = INTRARI[fel];
|
|
const pack = {}, context = {};
|
|
for (const [k, v] of Object.entries(input)) {
|
|
// '*': o cheie pe care modulul nu o citeste pe nume, la un fel citit intreg (un membru in plus): a artefactului, ca in organizatie.mjs
|
|
const c = Object.hasOwn(cls, k) ? cls[k] : cls['*'];
|
|
if (c === undefined) throw new Error(`${fel}: the vector has an input ${k} that organizatie.mjs does not classify`);
|
|
if (c === 'artifact' || c === 'stapled') pack[k] = v; else context[k] = v;
|
|
}
|
|
return { pack, context };
|
|
}
|
|
const egal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
|
|
/** Pentru fiecare fel al suitei: un vector bun (nu INVALID, fara retea; VALID cand exista) si un frate stricat: INVALID, fara retea, facut din
|
|
* vectorul bun, cu ACELASI context si alt artefact (deci artefactul e stricat, nu increderea celui care verifica), o singura regula cand se poate. */
|
|
export function alegeVectorii() {
|
|
const { j } = indexul();
|
|
const feluri = Object.keys(j.artifacts);
|
|
const ales = {};
|
|
for (const fel of feluri) {
|
|
const ale = j.vectors.filter((v) => v.artifact === fel && !v.requires.length);
|
|
const bun = ale.find((v) => v.verdict === 'VALID') || ale.find((v) => v.verdict === 'PARTIAL');
|
|
if (!bun) throw new Error(`${fel}: no vector that is not INVALID and needs no network`);
|
|
const vb = vectorul(bun.id); const ib = imparte(fel, vb.input);
|
|
const candidati = ale.filter((v) => v.verdict === 'INVALID').map((v) => vectorul(v.id))
|
|
.filter((x) => x.base === bun.id).map((x) => ({ x, i: imparte(fel, x.input) }))
|
|
.filter(({ i }) => egal(i.context, ib.context) && !egal(i.pack, ib.pack));
|
|
// o singura regula, si nu una de forma (un artefact care nu se poate citi deloc spune putin despre compunere), cand se poate
|
|
const deForma = (r) => /[.-]form$/.test(r);
|
|
const rau = candidati.find(({ x }) => x.expected.failed.length === 1 && !deForma(x.expected.failed[0])) || candidati.find(({ x }) => x.expected.failed.length === 1) || candidati[0];
|
|
if (!rau) throw new Error(`${fel}: no INVALID vector made from ${bun.id} with the same verifier inputs and another artifact`);
|
|
ales[fel] = { bun: { id: bun.id, v: vb, ...ib }, rau: { id: rau.x.id, v: rau.x, ...rau.i } };
|
|
}
|
|
return ales;
|
|
}
|
|
|
|
/** Starea Shield, facuta de uneltele reale: un jurnal al sidecar-ului (doua inregistrari), burafu lui, planul planificatorului pe un inventar
|
|
* cu un schimb de cheie si o semnatura clasice si o scanare cu expunere HNDL, si executia lui uscata. Fratele stricat: o inregistrare a executiei
|
|
* schimbata dupa scriere (lantul executiei rupt, nimic altceva). */
|
|
export async function stareaShield() {
|
|
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'org-shield-'));
|
|
try {
|
|
const { caleaSidecarului } = await import(pathToFileURL(path.join(AICI, 'consola.mjs')).href);
|
|
const SIDE = caleaSidecarului();
|
|
const side = (args) => execFileSync(process.execPath, [SIDE, ...args], { encoding: 'utf8', stdio: 'pipe' });
|
|
const log = path.join(T, 'audit.log'); const bin = path.join(T, 'node'); fs.writeFileSync(bin, 'BIN');
|
|
side(['record', '--kind', 'runtime', '--artifact', bin, '--attested', sha(fs.readFileSync(bin)), '--host', 'org-prod-1', '--log', log, '--at', '2026-10-20T08:00:00Z']);
|
|
side(['record', '--kind', 'deployment', '--name', 'checkout-api', '--version', '4.2.1', '--content-file', bin, '--log', log, '--at', '2026-10-20T08:05:00Z']);
|
|
const bf = path.join(T, 'b.json'); side(['bundle', '--log', log, '--out', bf, '--host', 'org-prod-1', '--at', '2026-10-20T08:10:00Z']);
|
|
const bundle = JSON.parse(fs.readFileSync(bf, 'utf8'));
|
|
const { planeaza } = await import(pathToFileURL(path.join(AICI, 'plan-migrare.mjs')).href);
|
|
const plan = planeaza({ inventory: [
|
|
{ assetType: 'algorithm', name: 'ECDH', primitive: 'key-agree', quantumVulnerable: true, ref: 'a:1' },
|
|
{ assetType: 'algorithm', name: 'RSA-2048', primitive: 'signature', quantumVulnerable: true, ref: 'a:2' },
|
|
{ assetType: 'algorithm', name: 'ML-KEM-768', primitive: 'kem', quantumVulnerable: false, ref: 'a:3' },
|
|
], scan: { domain: 'exemplu.test', findings: [{ id: 'hndl-exposed', severity: 'high', title: 'No post-quantum key exchange' }] } });
|
|
const { executa } = await import(pathToFileURL(path.join(AICI, 'executa-migrare.mjs')).href);
|
|
const x = await executa(plan, { consent: 'all', execute: false, out: path.join(T, 'exec') });
|
|
const execution = JSON.parse(fs.readFileSync(x.file, 'utf8'));
|
|
if (!(Array.isArray(execution.records) && execution.records.length >= 2)) throw new Error('the dry execution wrote fewer than two records');
|
|
const stricata = JSON.parse(JSON.stringify(execution)); stricata.records[1].record.verdict = 'OK';
|
|
return { bun: { bundle, plan, execution }, rau: { bundle, plan, execution: stricata }, rauRegula: 'shield.execution-chain', plan };
|
|
} finally { fs.rmSync(T, { recursive: true, force: true }); }
|
|
}
|
|
/** Raportul de conformitate PQ pe planul real, legat de plicul lui AIP-23; fratele stricat: raportul schimbat dupa legare. */
|
|
export async function raportulPQ(plan) {
|
|
const { raportConformitate } = await import(pathToFileURL(path.join(AICI, 'raport-conformitate.mjs')).href);
|
|
const { report, envelope } = raportConformitate({ plan, organization: 'Example Organization', risk: 'high', date: '2026-10-20' });
|
|
const schimbat = JSON.parse(JSON.stringify(report)); schimbat.organization = 'Another Organization';
|
|
return { bun: { report, envelope }, rau: { report: schimbat, envelope }, rauRegula: 'pqreport.binding' };
|
|
}
|
|
|
|
/**
|
|
* Pachetul bun al unei organizatii cu un artefact din fiecare fel (23 ale suitei + 2 ale planului de control), contextul derivat al celui care
|
|
* verifica, si ce se asteapta pentru fiecare (verdictul, regulile, starile nivelurilor din vector). `stricate[fel]` e fratele stricat al fiecaruia.
|
|
*/
|
|
export async function pachetulBun() {
|
|
const ales = alegeVectorii();
|
|
const artifacts = [], byArtifact = {}, asteptat = {}, stricate = {};
|
|
const idDe = (fel) => fel + '-1';
|
|
for (const [fel, { bun, rau }] of Object.entries(ales)) {
|
|
const id = idDe(fel);
|
|
artifacts.push({ id, artifact: fel, input: bun.pack });
|
|
if (Object.keys(bun.context).length) byArtifact[id] = bun.context;
|
|
asteptat[id] = { verdict: bun.v.expected.verdict, failed: bun.v.expected.failed, levels: bun.v.expected.levels, vector: bun.id };
|
|
stricate[fel] = { id, artifact: { id, artifact: fel, input: rau.pack }, failed: rau.v.expected.failed, levels: rau.v.expected.levels, vector: rau.id };
|
|
}
|
|
const sh = await stareaShield(); const pq = await raportulPQ(sh.plan);
|
|
artifacts.unshift({ id: 'shield-console-1', artifact: 'shield-console', input: sh.bun });
|
|
stricate['shield-console'] = { id: 'shield-console-1', artifact: { id: 'shield-console-1', artifact: 'shield-console', input: sh.rau }, failed: [sh.rauRegula], vector: null };
|
|
asteptat['shield-console-1'] = { verdict: 'VALID', failed: [] };
|
|
artifacts.push({ id: 'pq-compliance-report-1', artifact: 'pq-compliance-report', input: pq.bun });
|
|
stricate['pq-compliance-report'] = { id: 'pq-compliance-report-1', artifact: { id: 'pq-compliance-report-1', artifact: 'pq-compliance-report', input: pq.rau }, failed: [pq.rauRegula], vector: null };
|
|
asteptat['pq-compliance-report-1'] = { verdict: 'VALID', failed: [] };
|
|
for (const f of FELURI_PROPRII) if (!stricate[f]) throw new Error('no broken sibling for ' + f);
|
|
return {
|
|
pack: { v: 1, kind: PACK_KIND, organization: 'Example Organization', createdAt: '2026-10-20T09:00:00Z', artifacts },
|
|
context: { v: 1, kind: CONTEXT_KIND, byArtifact },
|
|
asteptat, stricate, ales,
|
|
};
|
|
}
|
|
/** pachetul cu artefactul `id` inlocuit (sau scos, cu null) */
|
|
export function inlocuieste(pack, id, artefact) {
|
|
const p = JSON.parse(JSON.stringify(pack));
|
|
const i = p.artifacts.findIndex((a) => a.id === id); if (i < 0) throw new Error('no artifact ' + id);
|
|
if (artefact === null) p.artifacts.splice(i, 1); else p.artifacts[i] = JSON.parse(JSON.stringify(artefact));
|
|
return p;
|
|
}
|