90 lines
7.1 KiB
JavaScript
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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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 };
|