Files
ForgeFlow/src/main/git-validator-policy.cjs
T
NuklearRabbit 258f0b1324
ForgeFlow quality gate / quality (push) Canceled after 0s
fix: restore scrolling and validator enforcement
2026-08-01 12:44:31 +02:00

90 lines
7.1 KiB
JavaScript

"use strict";
const crypto = require("node:crypto");
const PROFILE_DEFINITIONS = Object.freeze({
minimal: { label: "Minimal", requiredScore: 55, severities: ["error"], allowSuppressions: true, maxSuppressionDays: 180 },
standard: { label: "Standard", requiredScore: 70, severities: ["error", "warning"], allowSuppressions: true, maxSuppressionDays: 90 },
strict: { label: "Strict", requiredScore: 82, severities: ["error", "warning"], allowSuppressions: true, maxSuppressionDays: 30 },
production: { label: "Production", requiredScore: 90, severities: ["error", "warning"], allowSuppressions: true, maxSuppressionDays: 14 },
});
function normalizePolicy(policy = {}) {
const id = String(policy.id || policy.profile || "standard").toLowerCase();
const base = PROFILE_DEFINITIONS[id] || PROFILE_DEFINITIONS.standard;
const custom = id === "organization" ? policy : {};
return {
id,
label: custom.label || base.label || "Organization custom",
requiredScore: Math.min(100, Math.max(0, Number(custom.requiredScore ?? base.requiredScore ?? 80))),
enabledChecks: Array.isArray(custom.enabledChecks) ? [...new Set(custom.enabledChecks.map(String))] : null,
severityOverrides: custom.severityOverrides && typeof custom.severityOverrides === "object" ? { ...custom.severityOverrides } : {},
blockingChecks: [...new Set((custom.blockingChecks || policy.blockingChecks || []).map(String))],
blockingSeverities: [...new Set((custom.blockingSeverities || policy.blockingSeverities || base.severities || ["error"]).map(String))]
.filter((severity) => ["warning", "error"].includes(severity)),
allowSuppressions: custom.allowSuppressions ?? base.allowSuppressions ?? true,
maxSuppressionDays: Math.max(1, Number(custom.maxSuppressionDays ?? base.maxSuppressionDays ?? 30)),
};
}
function validateSuppression(input, policy, now = new Date()) {
if (!policy.allowSuppressions) throw new Error("The selected policy does not allow suppressions.");
const checkId = String(input?.checkId || "").trim();
const reason = String(input?.reason || "").trim();
const author = String(input?.author || "").trim();
const scope = String(input?.scope || "repository").trim();
const evidence = String(input?.evidence || "").trim();
const expiresAt = new Date(input?.expiresAt || "");
if (!checkId || reason.length < 10 || !author || !evidence) throw new Error("A suppression requires a check ID, author, evidence and a reason of at least 10 characters.");
if (!Number.isFinite(expiresAt.getTime()) || expiresAt <= now) throw new Error("A suppression expiry must be in the future.");
const maximum = new Date(now.getTime() + policy.maxSuppressionDays * 86_400_000);
if (expiresAt > maximum) throw new Error(`This policy permits suppressions for at most ${policy.maxSuppressionDays} days.`);
return {
id: crypto.randomUUID(), checkId, reason, author,
createdAt: now.toISOString(), ticket: String(input.ticket || "").trim() || null,
expiresAt: expiresAt.toISOString(), scope, evidence,
};
}
function applyPolicy(checks, policyInput, suppressions = [], now = new Date()) {
const policy = normalizePolicy(policyInput);
const enabled = policy.enabledChecks ? new Set(policy.enabledChecks) : null;
const relevant = checks.filter((check) => !enabled || enabled.has(check.id)).map((check) => {
const status = policy.severityOverrides[check.id] || check.status;
const suppression = suppressions.find((item) => item.checkId === check.id && new Date(item.expiresAt) > now);
const expiredSuppression = suppressions.find((item) => item.checkId === check.id && new Date(item.expiresAt) <= now);
return {
...check,
status,
suppressed: Boolean(suppression),
suppression: suppression || null,
expiredSuppression: expiredSuppression || null,
blocking: !suppression && status !== "pass" && (policy.blockingSeverities.includes(status) || policy.blockingChecks.includes(check.id)),
};
});
return { policy, checks: relevant };
}
function buildTrend(previous, report) {
const prior = new Map((previous?.checks || []).map((check) => [check.id, check]));
const current = new Map(report.checks.map((check) => [check.id, check]));
const active = (check) => check && check.status !== "pass" && !check.suppressed;
const newlyFound = [...current.values()].filter((check) => active(check) && !active(prior.get(check.id))).map((check) => check.id);
const resolved = [...prior.values()].filter((check) => active(check) && !active(current.get(check.id))).map((check) => check.id);
const regressions = [...current.values()].filter((check) => active(check) && prior.get(check.id)?.status === "warning" && check.status === "error").map((check) => check.id);
return { score: report.score, categories: report.categories, newlyFound, resolved, regressions, suppressions: report.checks.filter((check) => check.suppressed).map((check) => check.id), checkedAt: report.checkedAt, commitSha: report.commitSha || null, checks: report.checks.map(({ id, status, suppressed }) => ({ id, status, suppressed })) };
}
function exportReport(report, format = "json") {
if (format === "json") return { extension: "json", mimeType: "application/json", content: `${JSON.stringify(report, null, 2)}\n` };
const rows = report.checks.map((check) => `| ${check.id} | ${check.category} | ${check.status}${check.suppressed ? " (suppressed)" : ""} | ${String(check.detail).replace(/\|/g, "\\|")} |`).join("\n");
const markdown = `# Git assurance report — ${report.repository}\n\nPolicy: **${report.policy.label}** · Score: **${report.score}/100** · Commit: \`${report.commitSha || "unknown"}\`\n\n| Check | Category | Status | Evidence |\n|---|---|---|---|\n${rows}\n`;
if (format === "markdown") return { extension: "md", mimeType: "text/markdown", content: markdown };
if (format !== "html") throw new Error("Unsupported Git Validator export format.");
const escape = (value) => String(value).replace(/[&<>"']/g, (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]);
const htmlRows = report.checks.map((check) => `<tr><td>${escape(check.id)}</td><td>${escape(check.category)}</td><td>${escape(check.status)}${check.suppressed ? " (suppressed)" : ""}</td><td>${escape(check.detail)}</td></tr>`).join("");
return { extension: "html", mimeType: "text/html", content: `<!doctype html><html lang="en"><meta charset="utf-8"><title>Git assurance — ${escape(report.repository)}</title><style>body{font:15px system-ui;max-width:1100px;margin:40px auto;padding:0 24px;color:#172033}table{border-collapse:collapse;width:100%}th,td{padding:10px;border:1px solid #ccd4e0;text-align:left}th{background:#edf2f7}</style><h1>Git assurance — ${escape(report.repository)}</h1><p>Policy: <strong>${escape(report.policy.label)}</strong> · Score: <strong>${report.score}/100</strong> · Commit: <code>${escape(report.commitSha || "unknown")}</code></p><table><thead><tr><th>Check</th><th>Category</th><th>Status</th><th>Evidence</th></tr></thead><tbody>${htmlRows}</tbody></table></html>` };
}
module.exports = { PROFILE_DEFINITIONS, normalizePolicy, validateSuppression, applyPolicy, buildTrend, exportReport };