← Files ECZ-ID DORA ReadinessARCHIVED FILE

skills/dora-evidence-review/scripts/review.mjs

21.9 KB · Oct 5, 2026 · 18:33 UTC

↓ Download file

#!/usr/bin/env node
// ECZ-ID DORA Readiness: portable evidence review (generated by the ECZ-ID Plugin Foundry; do not edit).
// Reads file NAMES and PATHS under the given root. Opens no file. No network. Writes nothing.
// Same detectors, Review Priority rules and next actions as the ECZ-ID VS Code extension.
import { readdirSync } from "node:fs";
import { join, relative } from "node:path";



export const DEFAULT_IGNORES = new Set(["node_modules", ".git", ".pnpm-store", "dist", "out", "build", ".next", ".turbo", ".venv", "venv", "__pycache__", "target", "coverage"]);
export const DOT_ALLOWLIST = new Set([".github", ".gitlab", ".well-known"]);

/** Workspace-relative file paths, filename and path only. Dot-entries are skipped except the allowlist. */
export function listFiles(root, { maxDepth = 8, maxFiles = 20000, extraDotEntries = [] } = {}) {
  const results = [];
  const dots = new Set([...DOT_ALLOWLIST, ...extraDotEntries]);
  const walk = (dir, depth) => {
    if (depth > maxDepth || results.length >= maxFiles) return;
    let entries;
    try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
    for (const e of entries) {
      if (results.length >= maxFiles) return;
      if (e.name.startsWith(".") && !dots.has(e.name)) continue;
      const full = join(dir, e.name);
      if (e.isDirectory()) { if (DEFAULT_IGNORES.has(e.name)) continue; walk(full, depth + 1); }
      else if (e.isFile()) results.push(relative(root, full).split("\\").join("/"));
    }
  };
  walk(root, 0);
  return results;
}

const RESOLVER_REF = [/(^|\/)\.well-known\/ecz-[a-z0-9-]*\.json$/i, /(^|\/)ecz-(agent|mcp|id)[a-z0-9-]*\.json$/i, /(^|\/)ecz-[a-z0-9-]+\.json$/i];
const REASON = {
  EVIDENCE_OBSERVED: "Evidence observed locally for the items listed.",
  EVIDENCE_NOT_OBSERVED: "Some expected evidence was not observed locally. This is neutral. It does not mean a problem exists.",
  REVIEW_RECOMMENDED: "Observed evidence may still need human review before reliance.",
  NO_PUBLIC_PROOF_REFERENCE: "No public resolver proof reference was found yet. This does not mean unsafe. Local policy decides.",
  PARTIAL_PUBLIC_PROOF: "Partial public proof reference detected. Resolver-verifiable proof may make this easier to review.",
  RECHECK_BEFORE_RELIANCE: "Re-check before reliance. Results reflect the workspace at scan time.",
  LOCAL_POLICY_DECIDES: "Your local policy decides whether the observed evidence is sufficient."
};

function matchAny(patterns, files) {
  for (const f of files) for (const re of patterns) if (re.test(f)) return f;
  return undefined;
}

/** Same semantics as family/detect.ts detectEvidence. */
export function detectEvidence(spec, files, workspaceName) {
  const observed = [], notObserved = [], reviewRequired = [];
  for (const d of spec.detectors) {
    const hit = matchAny(d.patterns.map((p) => new RegExp(p, "i")), files);
    if (hit) {
      const item = { id: d.id, label: d.label, status: "observed", detail: d.observedDetail, path: hit };
      observed.push(item);
      if (d.reviewWhenObserved) reviewRequired.push({ ...item, status: "review-required" });
    } else notObserved.push({ id: d.id, label: d.label, status: "not-observed", detail: d.notObservedDetail });
  }
  const codes = [];
  if (observed.length) codes.push("EVIDENCE_OBSERVED");
  if (notObserved.length) codes.push("EVIDENCE_NOT_OBSERVED");
  if (reviewRequired.length) codes.push("REVIEW_RECOMMENDED");
  codes.push(matchAny(RESOLVER_REF, files) ? "PARTIAL_PUBLIC_PROOF" : "NO_PUBLIC_PROOF_REFERENCE");
  codes.push("LOCAL_POLICY_DECIDES", "RECHECK_BEFORE_RELIANCE");
  return { specialistId: spec.extensionId ?? spec.name, scannedAt: new Date().toISOString(), workspaceName, observed, notObserved, reviewRequired, reasonCodes: codes.map((id) => ({ id, message: REASON[id] })) };
}

export const PRIORITY_DISCLAIMER = "Review Priority is not a safety, approval or compliance determination. It indicates how much attention this evidence review deserves, based only on what was observed locally by filename and path.";
export const PRIORITY_MEANING = {
  LOW: "Every evidence class this review looks for was observed. Review the documents themselves before reliance.",
  NORMAL: "Observed evidence still needs human review, or supporting evidence was not observed. Worth completing before the next review.",
  ELEVATED: "A primary evidence class was not observed. Review before you rely on this workspace as an evidence source.",
  HIGH: "Evidence that a regulator, auditor or customer is likely to ask for first was not observed, or several primary classes are missing together."
};
export const ELEVATED_GAP_AGGREGATION_THRESHOLD = 2;
const RANK = { LOW: 0, NORMAL: 1, ELEVATED: 2, HIGH: 3 };
const FROM_WEIGHT = { high: "HIGH", elevated: "ELEVATED", normal: "NORMAL" };

/** Same rules as family/valueLayer.ts computeEvidenceReviewPriority. */
export function computeReviewPriority(spec, result, profile) {
  const observed = new Map(result.observed.map((i) => [i.id, i]));
  const review = new Set(result.reviewRequired.map((i) => i.id));
  const lines = [];
  for (const d of spec.detectors) {
    const g = profile.guidance.find((x) => x.detectorId === d.id);
    if (observed.has(d.id)) {
      const needs = review.has(d.id);
      lines.push({ detectorId: d.id, label: d.label, status: needs ? "review-required" : "observed", weight: "none", contributes: needs ? "NORMAL" : "LOW", detail: needs ? "Observed by filename and path; the document itself still needs human review." : "Observed by filename and path." });
    } else {
      const w = g?.weightWhenNotObserved ?? "normal";
      lines.push({ detectorId: d.id, label: d.label, status: "not-observed", weight: w, contributes: FROM_WEIGHT[w], detail: `Not observed by filename and path (${w === "normal" ? "supporting" : "primary"} evidence class).` });
    }
  }
  const counts = { LOW: 0, NORMAL: 0, ELEVATED: 0, HIGH: 0 };
  for (const l of lines) counts[l.contributes]++;
  let priority, rationale;
  if (counts.HIGH > 0) { priority = "HIGH"; rationale = `HIGH because ${counts.HIGH} evidence class${counts.HIGH === 1 ? "" : "es"} that ${counts.HIGH === 1 ? "is" : "are"} usually requested first ${counts.HIGH === 1 ? "was" : "were"} not observed.`; }
  else if (counts.ELEVATED >= ELEVATED_GAP_AGGREGATION_THRESHOLD) { priority = "HIGH"; rationale = `HIGH because ${counts.ELEVATED} primary evidence classes were not observed together (threshold ${ELEVATED_GAP_AGGREGATION_THRESHOLD}).`; }
  else if (counts.ELEVATED > 0) { priority = "ELEVATED"; rationale = "ELEVATED because one primary evidence class was not observed."; }
  else if (counts.NORMAL > 0) { priority = "NORMAL"; rationale = `NORMAL because ${counts.NORMAL} item${counts.NORMAL === 1 ? "" : "s"} ${counts.NORMAL === 1 ? "needs" : "need"} human review or supporting evidence was not observed.`; }
  else { priority = "LOW"; rationale = "LOW because every evidence class was observed and none is flagged for review."; }
  lines.sort((a, b) => RANK[b.contributes] - RANK[a.contributes] || a.detectorId.localeCompare(b.detectorId));
  return { priority, meaning: PRIORITY_MEANING[priority], disclaimer: PRIORITY_DISCLAIMER, rationale, reasons: lines, counts };
}

/** Same rules as family/valueLayer.ts selectContextualActions. */
export function selectContextualActions(result, profile) {
  const observed = new Set(result.observed.map((i) => i.id));
  const notObserved = new Set(result.notObserved.map((i) => i.id));
  const max = profile.maxActions ?? 3;
  return profile.actions
    .map((a, idx) => ({ a, idx }))
    .filter(({ a }) => a.always || a.whenNotObserved?.some((id) => notObserved.has(id)) || a.whenObserved?.some((id) => observed.has(id)))
    .sort((x, y) => (y.a.rank ?? 0) - (x.a.rank ?? 0) || x.idx - y.idx)
    .map(({ a }) => a)
    .slice(0, Math.max(0, max));
}

const NEUTRAL = [
  "This is an evidence-organising review, not a verdict.",
  "It does not assert safety, certification, approval or compliance.",
  "Filename and path detection shows that a document exists where you expect it. It does not read the document and cannot judge its quality.",
  "Missing evidence is neutral. Your local policy decides what is sufficient.",
  "Re-check before reliance; results reflect the workspace at scan time."
];

export function renderReview(spec, result, profile) {
  const p = computeReviewPriority(spec, result, profile);
  const actions = selectContextualActions(result, profile);
  const observed = new Map(result.observed.map((i) => [i.id, i]));
  const review = new Set(result.reviewRequired.map((i) => i.id));
  const L = [];
  L.push(`# ${spec.displayName}: Evidence Review`, "", `**${profile.question}**`, "", profile.hook, "");
  if (result.workspaceName) L.push(`Workspace: **${result.workspaceName}**  |  Scanned: ${result.scannedAt}  |  Method: filename and path only`, "");
  L.push(`## Review Priority: ${p.priority}`, "", p.meaning, "", `Why: ${p.rationale}`, "");
  L.push(...p.reasons.map((r) => `- ${r.label}: ${r.status.toUpperCase().replace("-", " ")} (contributes ${r.contributes}). ${r.detail}`), "");
  L.push(`_${p.disclaimer}_`, "");
  L.push("## What we observed, what we did not, and why it matters", "");
  for (const d of spec.detectors) {
    const g = profile.guidance.find((x) => x.detectorId === d.id);
    const hit = observed.get(d.id);
    const status = hit ? (review.has(d.id) ? "OBSERVED, REVIEW REQUIRED" : "OBSERVED") : "NOT OBSERVED";
    L.push(`### ${d.label}: ${status}`, "");
    if (hit?.path) L.push(`- Where: \`${hit.path}\``);
    if (hit?.detail) L.push(`- Observed: ${hit.detail}`);
    if (!hit && d.notObservedDetail) L.push(`- Observed: ${d.notObservedDetail}`);
    if (g) {
      L.push(`- Why it matters: ${g.whyItMatters}`);
      L.push(`- Review next: ${hit ? g.reviewWhenObserved : g.reviewWhenNotObserved}`);
      if (g.capability) L.push(`- If you want to go further: ${g.capability.label}. ${g.capability.note} ${g.capability.url}`);
    }
    L.push("");
  }
  L.push("## What this means", "", ...result.reasonCodes.map((rc) => `- ${rc.message}`), "");
  L.push("## What this does not mean", "", ...NEUTRAL.map((s) => `- ${s}`), "");
  L.push("## Next actions for this result", "");
  if (actions.length) { actions.forEach((a, i) => { L.push(`${i + 1}. **${a.label}**: ${a.note}`); L.push(`   ${a.url}`); }); L.push(""); }
  else L.push("_No contextual action for this result._", "");
  if (profile.discovery) L.push(`${profile.discovery.label}: ${profile.discovery.url}`, "");
  L.push("TrustOps handles setup and checkout. This review runs no payment and creates no ECZ-ID truth, entitlement or Resolver proof.", "");
  return L.join("\n");
}

/** JSON projection for machine consumers. */
export function projectReview(spec, result, profile) {
  const p = computeReviewPriority(spec, result, profile);
  return {
    schema_version: "1.0.0",
    product: spec.name,
    display_name: spec.displayName,
    generated_at_utc: result.scannedAt,
    method: "filename-and-path-only",
    workspace: result.workspaceName,
    review_priority: { level: p.priority, meaning: p.meaning, rationale: p.rationale, disclaimer: p.disclaimer, reasons: p.reasons },
    observations: [...result.observed.map((i) => ({ ...i, status: result.reviewRequired.some((r) => r.id === i.id) ? "review-required" : "observed" })), ...result.notObserved].sort((a, b) => a.id.localeCompare(b.id)),
    public_safe_reason_codes: result.reasonCodes,
    contextual_next_actions: selectContextualActions(result, profile).map((a) => ({ id: a.id, label: a.label, url: a.url, kind: a.kind, note: a.note })),
    discovery: profile.discovery ?? null,
    privacy: { local_first: true, source_upload: false, hidden_telemetry: false, network_during_review: "none" },
    do_not_infer: ["safety", "approval", "certification", "compliance", "entitlement", "binding", "current_identity_state"]
  };
}

const SPEC = {"extensionId":"ecocitizenz.eczid-dora-readiness","name":"eczid-dora-readiness","prefix":"eczidDora","displayName":"ECZ-ID DORA Readiness","purpose":"Find the ICT resilience evidence gaps before a regulator, auditor or customer finds them.","searchIntent":"DORA ICT third-party register operational resilience evidence incident","guidanceRouteId":"dora","setupRouteId":"dora-readiness","setupFlow":"dora-sbom","detectors":[{"id":"dora.thirdparty","label":"ICT third-party register","patterns":["third-?party-?register","ict-?third-?party","outsourcing-?register","register-?of-?information","ict-?register"],"observedDetail":"An ICT third-party register (register of information) was observed.","notObservedDetail":"No ICT third-party register observed. This is the artefact a competent authority can request in full.","reviewWhenObserved":true},{"id":"dora.policy","label":"Operational-resilience / ICT risk policy","patterns":["(^|[^a-z])dora([^a-z]|$)","ict-?risk","operational-?resilience","resilience-?policy","risk-?management-?framework"],"observedDetail":"An operational-resilience or ICT risk policy document was observed.","notObservedDetail":"No operational-resilience or ICT risk policy observed.","reviewWhenObserved":true},{"id":"dora.incident","label":"Incident-response evidence","patterns":["incident-?response","incident-?register","incident-?report","incident-?log","incident-?management","major-?incident","major-?ict"],"observedDetail":"Incident-response evidence was observed.","notObservedDetail":"No incident-response evidence observed.","reviewWhenObserved":true},{"id":"dora.testing","label":"Resilience-testing / continuity evidence","patterns":["resilience-?test","tlpt","threat-?led","continuity-?test","business-?continuity","disaster-?recovery","bcdr","dr-?plan","pentest","penetration-?test"],"observedDetail":"Resilience-testing or continuity evidence was observed.","notObservedDetail":"No resilience-testing or continuity evidence observed.","reviewWhenObserved":true},{"id":"dora.contracts","label":"ICT contractual arrangements / exit plans","patterns":["ict-?contract","contractual-?arrangement","outsourcing-?agreement","exit-?plan","exit-?strateg","service-?level-?agreement","(^|/)sla[._-]"],"observedDetail":"ICT contractual arrangement or exit-plan evidence was observed.","notObservedDetail":"No ICT contractual arrangement or exit-plan evidence observed.","reviewWhenObserved":true}]};
const PROFILE = {"question":"Can you produce your ICT third-party evidence today?","hook":"DORA is already in force. Find the ICT resilience evidence gaps before a regulator, auditor or customer finds them. Regulation (EU) 2022/2554 (DORA) has applied since 17 January 2025 (Article 64). This review shows what DORA-relevant ICT resilience evidence is visible in this workspace, what is not observed, and what deserves review next.","maxActions":3,"guidance":[{"detectorId":"dora.thirdparty","whyItMatters":"DORA Article 28(3) requires financial entities to maintain and update a register of information covering all contractual arrangements on the use of ICT services provided by ICT third-party service providers, distinguishing those that support critical or important functions, and to make the full register available to the competent authority on request. It is usually the first artefact a supervisor, auditor or customer asks for.","reviewWhenObserved":"Open the register and confirm it names each ICT provider and service, distinguishes arrangements supporting critical or important functions, and shows an owner and a last-updated date. Check it matches what the contracts say.","reviewWhenNotObserved":"Locate the register if it lives outside this workspace, or start one from the contract list. Until it is here, this workspace cannot show ICT third-party evidence.","weightWhenNotObserved":"high","capability":{"label":"ECZ-ID Vendor Risk and Counterparty Trust (free VS Code extensions)","url":"https://open-vsx.org/extension/ecocitizenz/eczid-pack-dora-sbom","note":"Review the supplier and counterparty evidence behind each register entry, locally and free."}},{"detectorId":"dora.policy","whyItMatters":"DORA Article 6 requires a sound, comprehensive and well-documented ICT risk management framework, including a digital operational resilience strategy (Article 6(8)). A written policy is how a reviewer sees that the framework exists.","reviewWhenObserved":"Check the policy is current, signed off by the management body, and states the risk tolerance for ICT risk and the ICT objectives it supports.","reviewWhenNotObserved":"Add or link the operational-resilience or ICT risk policy. If it lives in a document system, record where, so an auditor can find it from here.","weightWhenNotObserved":"elevated","capability":{"label":"Cyber Resilience Passport (TrustOps)","url":"https://trustops.ecocitizenz.com/start?flow=critical-cyber-resilience","note":"Turn a reviewed resilience posture into a resolver-verifiable credential a counterparty can check."}},{"detectorId":"dora.incident","whyItMatters":"DORA Article 17 requires an ICT-related incident management process to detect, manage and notify ICT-related incidents, and Article 19 requires major ICT-related incidents to be reported to the competent authority. Suppliers are asked to show their side of that process.","reviewWhenObserved":"Confirm the process names who classifies incidents, how major incidents are escalated to the financial entity, and where the incident register is kept.","reviewWhenNotObserved":"Add the incident-response process or the incident register, or a pointer to where they live. Without it a reviewer cannot see how incidents reach the financial entity.","weightWhenNotObserved":"elevated","capability":{"label":"Cyber Resilience Passport (TrustOps)","url":"https://trustops.ecocitizenz.com/start?flow=critical-cyber-resilience","note":"Make incident-handling evidence part of a credential others can verify in Resolver."}},{"detectorId":"dora.testing","whyItMatters":"DORA Article 24 requires a digital operational resilience testing programme, Article 11 an ICT business continuity policy, and Article 26 threat-led testing (TLPT) at least every three years for the entities identified for it. Test and continuity records are what show the programme runs.","reviewWhenObserved":"Check the most recent test or continuity exercise date, its scope, and whether findings were tracked to closure.","reviewWhenNotObserved":"Add the latest resilience test report, continuity exercise record or DR plan, or a pointer to it.","weightWhenNotObserved":"elevated","capability":{"label":"Cyber Resilience Passport (TrustOps)","url":"https://trustops.ecocitizenz.com/start?flow=critical-cyber-resilience","note":"Testing and continuity evidence becomes part of a verifiable resilience posture."}},{"detectorId":"dora.contracts","whyItMatters":"DORA Article 30 requires the rights and obligations of the financial entity and the ICT provider to be set out in one written contract including the service level agreements, and Article 28(8) requires exit strategies for ICT services supporting critical or important functions. Reviewers ask for both alongside the register.","reviewWhenObserved":"Confirm the contract evidence covers service levels, data location, audit and access rights, termination and exit, and that exit plans exist for services supporting critical or important functions.","reviewWhenNotObserved":"Record where contractual arrangements and exit plans are kept. This is supporting evidence; the register comes first.","weightWhenNotObserved":"normal","capability":{"label":"DORA vendor credentialing (TrustOps)","url":"https://trustops.ecocitizenz.com/start?flow=dora-sbom","note":"Give supervised entities a resolver-verifiable view of your vendor posture instead of exchanging documents."}}],"actions":[{"id":"free-vendor-counterparty-reviews","label":"Free: add Vendor Risk and Counterparty Trust","url":"https://open-vsx.org/extension/ecocitizenz/eczid-pack-dora-sbom","note":"Free sibling extensions that review supplier and counterparty evidence in this workspace. The ECZ-ID DORA & SBOM Pack installs them together.","kind":"free-tool","whenNotObserved":["dora.thirdparty","dora.contracts"],"rank":9},{"id":"dora-guidance","label":"Read the DORA guidance","url":"https://developers.ecocitizenz.com/dora/","note":"What each evidence class means, how supervised entities ask for it, and how to make it verifiable. Developer Gateway, documentation only.","kind":"guidance","always":true,"rank":8},{"id":"cyber-resilience-passport","label":"Cyber Resilience Passport (TrustOps)","url":"https://trustops.ecocitizenz.com/start?flow=critical-cyber-resilience","note":"Once the resilience evidence exists, make the posture resolver-verifiable so counterparties stop asking for documents.","kind":"product","whenNotObserved":["dora.policy","dora.incident","dora.testing"],"rank":7},{"id":"dora-vendor-credentialing","label":"DORA vendor credentialing (TrustOps)","url":"https://trustops.ecocitizenz.com/start?flow=dora-sbom","note":"Register evidence exists here. Give supervised entities a resolver-verifiable view of it.","kind":"product","whenObserved":["dora.thirdparty"],"rank":7},{"id":"verified-or-assured-eczid","label":"Verified or Assured ECZ-ID","url":"https://trustops.ecocitizenz.com/start#parent-tiers","note":"The public identity spine every ECZ-ID credential attaches to. Counterparties check it in Resolver.","kind":"identity","always":true,"rank":3}],"discovery":{"label":"View all relevant DORA / resilience products","url":"https://developers.ecocitizenz.com/dora-sbom-suite/"}};
const EXTRA_DOT_ENTRIES = [];

const args = process.argv.slice(2);
const wantJson = args.includes("--json");
const root = args.find((a) => !a.startsWith("--")) ?? process.cwd();
const { resolve: resolvePath, basename } = await import("node:path");
const { statSync } = await import("node:fs");
const abs = resolvePath(root);
let st;
try { st = statSync(abs); } catch { console.error("not a directory: " + root); process.exit(2); }
if (!st.isDirectory()) { console.error("not a directory: " + root); process.exit(2); }
const files = listFiles(abs, { extraDotEntries: EXTRA_DOT_ENTRIES });
const result = detectEvidence(SPEC, files, basename(abs));
if (wantJson) console.log(JSON.stringify(projectReview(SPEC, result, PROFILE), null, 2));
else console.log(renderReview(SPEC, result, PROFILE));

SHA-256: fa523df7f3173c53bc069c51dffd5bf7678c14878291e673ed78f953fb699ce5