witnora 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +275 -12
- package/dist/artifact-manifest.js +37 -0
- package/dist/artifact-validation.js +102 -0
- package/dist/badge.js +31 -0
- package/dist/browser-adapter.js +156 -0
- package/dist/bundle.js +100 -0
- package/dist/cli.js +971 -0
- package/dist/command-help.js +71 -0
- package/dist/companion-artifacts.js +170 -0
- package/dist/conformance.js +115 -0
- package/dist/control-plane.js +409 -0
- package/dist/corpus-governance.js +126 -0
- package/dist/corpus-store.js +239 -0
- package/dist/corpus.js +438 -0
- package/dist/credentials.js +127 -0
- package/dist/evidence-signing.js +85 -0
- package/dist/failure-review.js +188 -0
- package/dist/guided-setup.js +101 -0
- package/dist/index.js +16 -0
- package/dist/lab.js +323 -0
- package/dist/local-server.js +491 -0
- package/dist/monitor.js +100 -0
- package/dist/normalizers.js +265 -0
- package/dist/onboarding-templates.js +158 -0
- package/dist/release-gate.js +457 -0
- package/dist/report.js +165 -0
- package/dist/runner.js +349 -0
- package/dist/sandbox.js +424 -0
- package/dist/schema-validator.js +503 -0
- package/dist/try.js +135 -0
- package/dist/types.js +3 -0
- package/dist/vendor/onegent-runtime/browser-adapter-kit.d.ts +107 -0
- package/dist/vendor/onegent-runtime/browser-adapter-kit.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/browser-adapter-kit.js +187 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts +183 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.js +344 -0
- package/dist/vendor/onegent-runtime/controlled-adapter.d.ts +6 -0
- package/dist/vendor/onegent-runtime/controlled-adapter.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/controlled-adapter.js +40 -0
- package/dist/vendor/onegent-runtime/mock-procurement.d.ts +6 -0
- package/dist/vendor/onegent-runtime/mock-procurement.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/mock-procurement.js +33 -0
- package/dist/vendor/onegent-runtime/policies.d.ts +6 -0
- package/dist/vendor/onegent-runtime/policies.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/policies.js +109 -0
- package/dist/vendor/onegent-runtime/risk.d.ts +3 -0
- package/dist/vendor/onegent-runtime/risk.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/risk.js +54 -0
- package/dist/vendor/onegent-runtime/sandbox-adapter-kit.d.ts +45 -0
- package/dist/vendor/onegent-runtime/sandbox-adapter-kit.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sandbox-adapter-kit.js +148 -0
- package/dist/vendor/onegent-runtime/sandbox-harness.d.ts +167 -0
- package/dist/vendor/onegent-runtime/sandbox-harness.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sandbox-harness.js +729 -0
- package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts +77 -0
- package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sandbox-hosted.js +152 -0
- package/dist/vendor/onegent-runtime/sdk.d.ts +29 -0
- package/dist/vendor/onegent-runtime/sdk.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sdk.js +140 -0
- package/dist/vendor/onegent-runtime/service.d.ts +27 -0
- package/dist/vendor/onegent-runtime/service.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/service.js +423 -0
- package/dist/vendor/onegent-runtime/store.d.ts +16 -0
- package/dist/vendor/onegent-runtime/store.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/store.js +29 -0
- package/dist/vendor/onegent-runtime/stripe-test-readonly.d.ts +79 -0
- package/dist/vendor/onegent-runtime/stripe-test-readonly.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/stripe-test-readonly.js +163 -0
- package/dist/vendor/onegent-runtime/trust-crypto.d.ts +11 -0
- package/dist/vendor/onegent-runtime/trust-crypto.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/trust-crypto.js +70 -0
- package/dist/vendor/onegent-runtime/trust-types.d.ts +163 -0
- package/dist/vendor/onegent-runtime/trust-types.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/trust-types.js +7 -0
- package/dist/vendor/onegent-runtime/trusted-recorder.d.ts +49 -0
- package/dist/vendor/onegent-runtime/trusted-recorder.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/trusted-recorder.js +347 -0
- package/dist/vendor/onegent-runtime/types.d.ts +287 -0
- package/dist/vendor/onegent-runtime/types.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/types.js +1 -0
- package/dist/vendor/onegent-runtime/vendor-sandbox-egress.d.ts +49 -0
- package/dist/vendor/onegent-runtime/vendor-sandbox-egress.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/vendor-sandbox-egress.js +152 -0
- package/package.json +33 -13
- package/bin/witnora.js +0 -3
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
export const AGENTCERT_RELEASE_GATE_SCHEMA_VERSION = "agentcert.release_gate.v0.1";
|
|
5
|
+
export const RELEASE_GATE_CONTROL_IDS = [
|
|
6
|
+
"permission-boundary",
|
|
7
|
+
"data-boundary",
|
|
8
|
+
"tool-contract",
|
|
9
|
+
"state-verification",
|
|
10
|
+
"human-handoff",
|
|
11
|
+
"rate-loop-cost-limits",
|
|
12
|
+
"idempotency-retry-safety",
|
|
13
|
+
"observability-auditability",
|
|
14
|
+
"rollback-kill-switch",
|
|
15
|
+
"supply-chain-dependency-boundary",
|
|
16
|
+
];
|
|
17
|
+
const CONTROL_DEFINITIONS = [
|
|
18
|
+
{
|
|
19
|
+
id: "permission-boundary",
|
|
20
|
+
name: "Identity, authorization, and least privilege",
|
|
21
|
+
mode: "evidence-required",
|
|
22
|
+
evaluate: evaluatePermissionBoundary,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "data-boundary",
|
|
26
|
+
name: "Sensitive data boundary",
|
|
27
|
+
mode: "evidence-required",
|
|
28
|
+
evaluate: evaluateDataBoundary,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "tool-contract",
|
|
32
|
+
name: "Tool and MCP contract",
|
|
33
|
+
mode: "automated",
|
|
34
|
+
evaluate: (bundle) => evaluateProduct(bundle, "mcpbench", "MCPBench tool contract evidence is not present."),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "state-verification",
|
|
38
|
+
name: "Observed state verification",
|
|
39
|
+
mode: "automated",
|
|
40
|
+
evaluate: evaluateStateVerification,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "human-handoff",
|
|
44
|
+
name: "High-risk action approval and handoff",
|
|
45
|
+
mode: "evidence-required",
|
|
46
|
+
evaluate: evaluateHumanHandoff,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "rate-loop-cost-limits",
|
|
50
|
+
name: "Rate, loop, timeout, and cost limits",
|
|
51
|
+
mode: "evidence-required",
|
|
52
|
+
evaluate: () => unresolved("needs-evidence", "Provide configured step, timeout, retry, and budget limits."),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "idempotency-retry-safety",
|
|
56
|
+
name: "Idempotency and retry safety",
|
|
57
|
+
mode: "manual",
|
|
58
|
+
evaluate: () => unresolved("manual-review", "A named owner must review duplicate execution and partial failure behavior."),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "observability-auditability",
|
|
62
|
+
name: "Observability, incident trace, and accountability",
|
|
63
|
+
mode: "automated",
|
|
64
|
+
evaluate: evaluateObservability,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "rollback-kill-switch",
|
|
68
|
+
name: "Rollback and kill switch",
|
|
69
|
+
mode: "manual",
|
|
70
|
+
evaluate: () => unresolved("manual-review", "A named owner must provide a rollback or kill-switch runbook."),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "supply-chain-dependency-boundary",
|
|
74
|
+
name: "Supply-chain and dependency boundary",
|
|
75
|
+
mode: "manual",
|
|
76
|
+
evaluate: () => unresolved("manual-review", "A named owner must review models, packages, MCP servers, and adapters."),
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
export async function buildReleaseGateReport(bundle, options = {}) {
|
|
80
|
+
const strict = options.strict ?? false;
|
|
81
|
+
const controls = CONTROL_DEFINITIONS.map((definition) => {
|
|
82
|
+
const evaluated = definition.evaluate(bundle);
|
|
83
|
+
return applyAttestation({ id: definition.id, name: definition.name, mode: definition.mode, ...evaluated }, options.attestations?.[definition.id]);
|
|
84
|
+
});
|
|
85
|
+
const regression = compareWithBaseline(bundle, options.baseline, {
|
|
86
|
+
requireBaseline: options.requireBaseline ?? false,
|
|
87
|
+
maxScoreDrop: options.maxScoreDrop ?? 0,
|
|
88
|
+
});
|
|
89
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
90
|
+
const sourceArtifacts = await digestArtifacts(options.sourceArtifacts ?? {}, cwd);
|
|
91
|
+
const evidenceBundle = options.evidenceBundlePath
|
|
92
|
+
? await digestArtifact("agentcert-evidence", options.evidenceBundlePath, cwd)
|
|
93
|
+
: undefined;
|
|
94
|
+
const blockers = collectBlockers(bundle, controls, regression, strict, options.requireBaseline ?? false, sourceArtifacts, evidenceBundle);
|
|
95
|
+
return {
|
|
96
|
+
schemaVersion: AGENTCERT_RELEASE_GATE_SCHEMA_VERSION,
|
|
97
|
+
kind: "agentcert.release_gate",
|
|
98
|
+
runId: `release_gate_${createHash("sha256").update(`${bundle.runId}:${JSON.stringify(controls)}`).digest("hex").slice(0, 12)}`,
|
|
99
|
+
generatedAt: new Date().toISOString(),
|
|
100
|
+
subject: bundle.subject,
|
|
101
|
+
strict,
|
|
102
|
+
verdict: {
|
|
103
|
+
passed: blockers.length === 0,
|
|
104
|
+
blockers,
|
|
105
|
+
passedControls: controls.filter((control) => control.status === "pass").length,
|
|
106
|
+
failedControls: controls.filter((control) => control.status === "fail").length,
|
|
107
|
+
needsEvidenceControls: controls.filter((control) => control.status === "needs-evidence").length,
|
|
108
|
+
manualReviewControls: controls.filter((control) => control.status === "manual-review").length,
|
|
109
|
+
},
|
|
110
|
+
bundleVerdict: bundle.verdict,
|
|
111
|
+
controls,
|
|
112
|
+
regression,
|
|
113
|
+
provenance: { evidenceBundle, sourceArtifacts },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export async function writeReleaseGateArtifacts(outDir, report) {
|
|
117
|
+
const resolvedOutDir = resolve(outDir);
|
|
118
|
+
await mkdir(resolvedOutDir, { recursive: true });
|
|
119
|
+
const paths = {
|
|
120
|
+
json: resolve(resolvedOutDir, "agentcert-release-gate.json"),
|
|
121
|
+
markdown: resolve(resolvedOutDir, "agentcert-release-gate.md"),
|
|
122
|
+
html: resolve(resolvedOutDir, "agentcert-release-gate.html"),
|
|
123
|
+
junit: resolve(resolvedOutDir, "agentcert-release-gate-junit.xml"),
|
|
124
|
+
badge: resolve(resolvedOutDir, "release-gate-badge.svg"),
|
|
125
|
+
};
|
|
126
|
+
await Promise.all([
|
|
127
|
+
writeFile(paths.json, `${JSON.stringify(report, null, 2)}\n`),
|
|
128
|
+
writeFile(paths.markdown, renderReleaseGateMarkdown(report)),
|
|
129
|
+
writeFile(paths.html, renderReleaseGateHtml(report)),
|
|
130
|
+
writeFile(paths.junit, renderReleaseGateJunit(report)),
|
|
131
|
+
writeFile(paths.badge, renderReleaseGateBadge(report)),
|
|
132
|
+
]);
|
|
133
|
+
return paths;
|
|
134
|
+
}
|
|
135
|
+
export function renderReleaseGateSummary(report) {
|
|
136
|
+
const lines = [
|
|
137
|
+
"# Witnora Release Gate",
|
|
138
|
+
"",
|
|
139
|
+
`Subject: ${report.subject.name}`,
|
|
140
|
+
`Verdict: ${report.verdict.passed ? "PASS" : "FAIL"}`,
|
|
141
|
+
`Controls: ${report.verdict.passedControls}/10 passed, ${report.verdict.failedControls} failed, ${report.verdict.needsEvidenceControls} need evidence, ${report.verdict.manualReviewControls} need manual review`,
|
|
142
|
+
`Regression: ${report.regression.status}`,
|
|
143
|
+
];
|
|
144
|
+
if (report.verdict.blockers.length > 0) {
|
|
145
|
+
lines.push("", "Blockers:", ...report.verdict.blockers.map((blocker) => `- ${blocker}`));
|
|
146
|
+
}
|
|
147
|
+
return `${lines.join("\n")}\n`;
|
|
148
|
+
}
|
|
149
|
+
export function validateReleaseGateReport(input) {
|
|
150
|
+
const errors = [];
|
|
151
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
152
|
+
return ["$ must be an object."];
|
|
153
|
+
const value = input;
|
|
154
|
+
if (value.schemaVersion !== AGENTCERT_RELEASE_GATE_SCHEMA_VERSION) {
|
|
155
|
+
errors.push(`schemaVersion must be ${JSON.stringify(AGENTCERT_RELEASE_GATE_SCHEMA_VERSION)}.`);
|
|
156
|
+
}
|
|
157
|
+
if (value.kind !== "agentcert.release_gate")
|
|
158
|
+
errors.push('kind must be "agentcert.release_gate".');
|
|
159
|
+
if (typeof value.runId !== "string" || value.runId.length === 0)
|
|
160
|
+
errors.push("runId must be a non-empty string.");
|
|
161
|
+
if (!Array.isArray(value.controls) || value.controls.length !== RELEASE_GATE_CONTROL_IDS.length) {
|
|
162
|
+
errors.push(`controls must contain ${RELEASE_GATE_CONTROL_IDS.length} entries.`);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const seen = new Set();
|
|
166
|
+
for (const [index, controlInput] of value.controls.entries()) {
|
|
167
|
+
const control = record(controlInput);
|
|
168
|
+
if (typeof control.id !== "string" || !RELEASE_GATE_CONTROL_IDS.includes(control.id)) {
|
|
169
|
+
errors.push(`controls[${index}].id is not a supported release control.`);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
seen.add(control.id);
|
|
173
|
+
}
|
|
174
|
+
if (!["automated", "evidence-required", "manual"].includes(control.mode)) {
|
|
175
|
+
errors.push(`controls[${index}].mode is invalid.`);
|
|
176
|
+
}
|
|
177
|
+
if (!["pass", "fail", "needs-evidence", "manual-review"].includes(control.status)) {
|
|
178
|
+
errors.push(`controls[${index}].status is invalid.`);
|
|
179
|
+
}
|
|
180
|
+
if (!Array.isArray(control.evidence))
|
|
181
|
+
errors.push(`controls[${index}].evidence must be an array.`);
|
|
182
|
+
}
|
|
183
|
+
if (seen.size !== RELEASE_GATE_CONTROL_IDS.length)
|
|
184
|
+
errors.push("controls must contain every release control exactly once.");
|
|
185
|
+
}
|
|
186
|
+
if (!value.verdict || typeof value.verdict !== "object" || Array.isArray(value.verdict)) {
|
|
187
|
+
errors.push("verdict must be an object.");
|
|
188
|
+
}
|
|
189
|
+
else if (typeof record(value.verdict).passed !== "boolean") {
|
|
190
|
+
errors.push("verdict.passed must be a boolean.");
|
|
191
|
+
}
|
|
192
|
+
if (!value.regression || typeof value.regression !== "object" || Array.isArray(value.regression))
|
|
193
|
+
errors.push("regression must be an object.");
|
|
194
|
+
if (!value.provenance || typeof value.provenance !== "object" || Array.isArray(value.provenance))
|
|
195
|
+
errors.push("provenance must be an object.");
|
|
196
|
+
return errors;
|
|
197
|
+
}
|
|
198
|
+
function evaluatePermissionBoundary(bundle) {
|
|
199
|
+
const decision = bundle.evidence.find((item) => item.kind === "authorization_decision");
|
|
200
|
+
if (!decision) {
|
|
201
|
+
return unresolved("needs-evidence", "No runtime authorization decision proves the principal and granted permissions.");
|
|
202
|
+
}
|
|
203
|
+
const metadata = record(decision.metadata);
|
|
204
|
+
const allowed = metadata.decision === "ALLOW";
|
|
205
|
+
return {
|
|
206
|
+
status: allowed ? "pass" : "fail",
|
|
207
|
+
summary: allowed ? "Runtime authorization allowed the recorded principal within granted permissions." : "Runtime authorization denied the action.",
|
|
208
|
+
evidence: evidenceReferences([decision]),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function evaluateDataBoundary(bundle) {
|
|
212
|
+
const exposures = bundle.evidence.filter((item) => item.kind === "sensitive_data_exposure" ||
|
|
213
|
+
item.kind === "secret_exposure" ||
|
|
214
|
+
/sensitive|secret|credential|pii/i.test(item.kind));
|
|
215
|
+
if (exposures.some((item) => item.severity === "critical" || item.severity === "high")) {
|
|
216
|
+
return { status: "fail", summary: "Sensitive-data evidence contains a blocking finding.", evidence: evidenceReferences(exposures) };
|
|
217
|
+
}
|
|
218
|
+
return unresolved("needs-evidence", "No blocking leak was observed, but an explicit data-boundary test or attestation is required.");
|
|
219
|
+
}
|
|
220
|
+
function evaluateProduct(bundle, product, missingSummary) {
|
|
221
|
+
const result = bundle.results.find((item) => item.product === product);
|
|
222
|
+
if (!result)
|
|
223
|
+
return unresolved("needs-evidence", missingSummary);
|
|
224
|
+
return {
|
|
225
|
+
status: result.passed ? "pass" : "fail",
|
|
226
|
+
summary: result.summary ?? `${product} ${result.passed ? "passed" : "failed"}.`,
|
|
227
|
+
evidence: resultEvidence(result),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function evaluateStateVerification(bundle) {
|
|
231
|
+
const results = bundle.results.filter((result) => result.product === "tripwire-ci" || result.product === "onegent-runtime");
|
|
232
|
+
if (results.length === 0) {
|
|
233
|
+
return unresolved("needs-evidence", "No Tripwire or Onegent result proves expected state against observed state.");
|
|
234
|
+
}
|
|
235
|
+
const failed = results.filter((result) => !result.passed);
|
|
236
|
+
return {
|
|
237
|
+
status: failed.length === 0 ? "pass" : "fail",
|
|
238
|
+
summary: failed.length === 0 ? "Configured browser/runtime state verification passed." : `${failed.map((item) => item.product).join(", ")} state verification failed.`,
|
|
239
|
+
evidence: results.flatMap(resultEvidence),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function evaluateHumanHandoff(bundle) {
|
|
243
|
+
const onegent = bundle.results.find((result) => result.product === "onegent-runtime");
|
|
244
|
+
if (!onegent)
|
|
245
|
+
return unresolved("needs-evidence", "No runtime approval or human-handoff record is present.");
|
|
246
|
+
const approval = onegent.evidence.find((item) => item.kind === "approval_record");
|
|
247
|
+
const risk = onegent.evidence.find((item) => item.kind === "runtime_risk_assessment");
|
|
248
|
+
const requiresApproval = record(risk?.metadata).requiresHumanApproval;
|
|
249
|
+
if (!approval && requiresApproval === false) {
|
|
250
|
+
return { status: "pass", summary: "Recorded risk assessment did not require human approval.", evidence: evidenceReferences(risk ? [risk] : []) };
|
|
251
|
+
}
|
|
252
|
+
if (!approval)
|
|
253
|
+
return unresolved("needs-evidence", "The runtime result does not contain an approval decision.");
|
|
254
|
+
const status = record(approval.metadata).status;
|
|
255
|
+
return {
|
|
256
|
+
status: status === "APPROVED" ? "pass" : "fail",
|
|
257
|
+
summary: status === "APPROVED" ? "The high-risk action has a recorded human approval." : `Approval status ${String(status ?? "UNKNOWN")} does not allow execution.`,
|
|
258
|
+
evidence: evidenceReferences([approval]),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function evaluateObservability(bundle) {
|
|
262
|
+
const missingArtifacts = bundle.results.filter((result) => Object.values(result.artifacts).filter(Boolean).length === 0);
|
|
263
|
+
if (!bundle.runId || !bundle.generatedAt || bundle.results.length === 0 || missingArtifacts.length > 0) {
|
|
264
|
+
return { status: "fail", summary: "The bundle is missing traceable run metadata or product artifacts.", evidence: [] };
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
status: "pass",
|
|
268
|
+
summary: "Run identity, timestamps, normalized results, findings, and artifact pointers are present.",
|
|
269
|
+
evidence: bundle.results.flatMap(resultEvidence),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function applyAttestation(control, attestation) {
|
|
273
|
+
if (!attestation)
|
|
274
|
+
return control;
|
|
275
|
+
const attestationEvidence = Array.isArray(attestation.evidence) ? attestation.evidence.filter(Boolean) : [];
|
|
276
|
+
const complete = Boolean(attestation.owner && attestation.reviewedAt && attestationEvidence.length > 0);
|
|
277
|
+
if (!complete) {
|
|
278
|
+
return {
|
|
279
|
+
...control,
|
|
280
|
+
status: control.status === "fail" ? "fail" : control.mode === "manual" ? "manual-review" : "needs-evidence",
|
|
281
|
+
summary: `${control.summary} The configured attestation is incomplete; owner, reviewedAt, and evidence are required.`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (control.status === "fail") {
|
|
285
|
+
return { ...control, evidence: [...control.evidence, ...attestationEvidence], owner: attestation.owner, reviewedAt: attestation.reviewedAt };
|
|
286
|
+
}
|
|
287
|
+
if (control.mode === "automated" && control.status !== "pass") {
|
|
288
|
+
return {
|
|
289
|
+
...control,
|
|
290
|
+
summary: `${control.summary} An attestation cannot replace missing automated evidence.`,
|
|
291
|
+
evidence: [...control.evidence, ...attestationEvidence],
|
|
292
|
+
owner: attestation.owner,
|
|
293
|
+
reviewedAt: attestation.reviewedAt,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
...control,
|
|
298
|
+
status: attestation.status,
|
|
299
|
+
summary: attestation.note ?? `Control attested ${attestation.status} by ${attestation.owner}.`,
|
|
300
|
+
evidence: [...control.evidence, ...attestationEvidence],
|
|
301
|
+
owner: attestation.owner,
|
|
302
|
+
reviewedAt: attestation.reviewedAt,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function compareWithBaseline(current, baseline, options) {
|
|
306
|
+
if (!baseline) {
|
|
307
|
+
return {
|
|
308
|
+
status: options.requireBaseline ? "fail" : "not-configured",
|
|
309
|
+
currentRunId: current.runId,
|
|
310
|
+
maxScoreDrop: options.maxScoreDrop,
|
|
311
|
+
regressions: options.requireBaseline ? ["A baseline evidence bundle is required but was not provided."] : [],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const regressions = [];
|
|
315
|
+
const scoreDelta = current.verdict.score - baseline.verdict.score;
|
|
316
|
+
if (current.subject.name !== baseline.subject.name) {
|
|
317
|
+
regressions.push(`Baseline subject ${baseline.subject.name} does not match current subject ${current.subject.name}.`);
|
|
318
|
+
}
|
|
319
|
+
if (baseline.verdict.passed && !current.verdict.passed)
|
|
320
|
+
regressions.push("Overall verdict changed from pass to fail.");
|
|
321
|
+
if (scoreDelta < -options.maxScoreDrop) {
|
|
322
|
+
regressions.push(`Overall score dropped by ${Math.abs(scoreDelta)} points; allowed drop is ${options.maxScoreDrop}.`);
|
|
323
|
+
}
|
|
324
|
+
for (const baselineResult of baseline.results) {
|
|
325
|
+
const currentResult = current.results.find((item) => item.product === baselineResult.product);
|
|
326
|
+
if (!currentResult) {
|
|
327
|
+
regressions.push(`${baselineResult.product} evidence is missing from the current run.`);
|
|
328
|
+
}
|
|
329
|
+
else if (baselineResult.passed && !currentResult.passed) {
|
|
330
|
+
regressions.push(`${baselineResult.product} changed from pass to fail.`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
status: regressions.length === 0 ? "pass" : "fail",
|
|
335
|
+
baselineRunId: baseline.runId,
|
|
336
|
+
currentRunId: current.runId,
|
|
337
|
+
scoreDelta,
|
|
338
|
+
maxScoreDrop: options.maxScoreDrop,
|
|
339
|
+
regressions,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function collectBlockers(bundle, controls, regression, strict, requireBaseline, sourceArtifacts, evidenceBundle) {
|
|
343
|
+
const blockers = [];
|
|
344
|
+
if (!bundle.verdict.passed)
|
|
345
|
+
blockers.push("The unified Witnora evidence bundle verdict failed.");
|
|
346
|
+
for (const control of controls) {
|
|
347
|
+
if (control.status === "fail")
|
|
348
|
+
blockers.push(`${control.name}: ${control.summary}`);
|
|
349
|
+
if (strict && control.status === "needs-evidence")
|
|
350
|
+
blockers.push(`${control.name}: evidence is required in strict mode.`);
|
|
351
|
+
if (strict && control.status === "manual-review")
|
|
352
|
+
blockers.push(`${control.name}: manual review is required in strict mode.`);
|
|
353
|
+
}
|
|
354
|
+
if (regression.status === "fail" && (regression.baselineRunId || requireBaseline))
|
|
355
|
+
blockers.push(...regression.regressions);
|
|
356
|
+
for (const artifact of [...sourceArtifacts, ...(evidenceBundle ? [evidenceBundle] : [])]) {
|
|
357
|
+
if (artifact.status === "missing")
|
|
358
|
+
blockers.push(`Referenced artifact is missing: ${artifact.path}`);
|
|
359
|
+
}
|
|
360
|
+
return [...new Set(blockers)];
|
|
361
|
+
}
|
|
362
|
+
async function digestArtifacts(input, cwd) {
|
|
363
|
+
const entries = Object.entries(input).filter((entry) => Boolean(entry[1]));
|
|
364
|
+
return Promise.all(entries.map(([name, path]) => digestArtifact(name, path, cwd)));
|
|
365
|
+
}
|
|
366
|
+
async function digestArtifact(name, path, cwd) {
|
|
367
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(path))
|
|
368
|
+
return { name, path, status: "remote" };
|
|
369
|
+
const fullPath = resolve(cwd, path);
|
|
370
|
+
try {
|
|
371
|
+
await access(fullPath);
|
|
372
|
+
if ((await stat(fullPath)).isDirectory()) {
|
|
373
|
+
return { name, path: path.replace(/\\/g, "/"), status: "verified" };
|
|
374
|
+
}
|
|
375
|
+
const bytes = await readFile(fullPath);
|
|
376
|
+
return { name, path: path.replace(/\\/g, "/"), sha256: createHash("sha256").update(bytes).digest("hex"), status: "verified" };
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return { name, path: path.replace(/\\/g, "/"), status: "missing" };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function resultEvidence(result) {
|
|
383
|
+
const evidence = evidenceReferences(result.evidence);
|
|
384
|
+
const artifacts = Object.values(result.artifacts).filter(Boolean);
|
|
385
|
+
return [...new Set([...evidence, ...artifacts])];
|
|
386
|
+
}
|
|
387
|
+
function evidenceReferences(evidence) {
|
|
388
|
+
return evidence.map((item) => item.artifactPath ?? `${item.source ?? "agentcert"}:${item.id}`);
|
|
389
|
+
}
|
|
390
|
+
function unresolved(status, summary) {
|
|
391
|
+
return { status, summary, evidence: [] };
|
|
392
|
+
}
|
|
393
|
+
function record(value) {
|
|
394
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
395
|
+
}
|
|
396
|
+
function renderReleaseGateMarkdown(report) {
|
|
397
|
+
const lines = [
|
|
398
|
+
"# Witnora Release Gate",
|
|
399
|
+
"",
|
|
400
|
+
`Subject: ${report.subject.name}`,
|
|
401
|
+
`Generated: ${report.generatedAt}`,
|
|
402
|
+
`Verdict: ${report.verdict.passed ? "PASS" : "FAIL"}`,
|
|
403
|
+
`Mode: ${report.strict ? "strict" : "advisory"}`,
|
|
404
|
+
"",
|
|
405
|
+
"## Controls",
|
|
406
|
+
"",
|
|
407
|
+
"| Control | Mode | Status | Summary |",
|
|
408
|
+
"|---|---|---|---|",
|
|
409
|
+
...report.controls.map((control) => `| ${control.name} | ${control.mode} | ${control.status} | ${control.summary.replaceAll("|", "\\|")} |`),
|
|
410
|
+
"",
|
|
411
|
+
"## Regression",
|
|
412
|
+
"",
|
|
413
|
+
`Status: ${report.regression.status}`,
|
|
414
|
+
...(report.regression.regressions.length > 0 ? report.regression.regressions.map((item) => `- ${item}`) : ["No regression detected or no baseline configured."]),
|
|
415
|
+
"",
|
|
416
|
+
"## Blockers",
|
|
417
|
+
"",
|
|
418
|
+
...(report.verdict.blockers.length > 0 ? report.verdict.blockers.map((item) => `- ${item}`) : ["No release blockers."]),
|
|
419
|
+
"",
|
|
420
|
+
"## Provenance",
|
|
421
|
+
"",
|
|
422
|
+
...[...(report.provenance.evidenceBundle ? [report.provenance.evidenceBundle] : []), ...report.provenance.sourceArtifacts].map((artifact) => `- ${artifact.name}: \`${artifact.path}\` (${artifact.status}${artifact.sha256 ? `, sha256 ${artifact.sha256}` : ""})`),
|
|
423
|
+
];
|
|
424
|
+
return `${lines.join("\n")}\n`;
|
|
425
|
+
}
|
|
426
|
+
function renderReleaseGateHtml(report) {
|
|
427
|
+
const rows = report.controls.map((control) => `<tr><td>${escapeHtml(control.name)}</td><td>${escapeHtml(control.mode)}</td><td><strong class="${control.status}">${escapeHtml(control.status)}</strong></td><td>${escapeHtml(control.summary)}</td></tr>`).join("");
|
|
428
|
+
const blockers = report.verdict.blockers.length > 0 ? report.verdict.blockers.map((item) => `<li>${escapeHtml(item)}</li>`).join("") : "<li>No release blockers.</li>";
|
|
429
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Witnora Release Gate</title><style>:root{--ink:#132033;--muted:#617083;--line:#d7dee8;--bg:#f5f7fa;--pass:#087f5b;--fail:#c92a2a;--warn:#a45a00}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.5 system-ui,sans-serif}main{width:min(1100px,calc(100% - 32px));margin:auto;padding:36px 0}h1{font-size:42px;margin:0}.verdict{font-size:28px;color:${report.verdict.passed ? "var(--pass)" : "var(--fail)"}}section{background:#fff;border:1px solid var(--line);border-radius:8px;padding:20px;margin-top:16px;overflow:auto}table{width:100%;border-collapse:collapse;min-width:760px}th,td{text-align:left;padding:10px;border-bottom:1px solid var(--line);vertical-align:top}.pass{color:var(--pass)}.fail{color:var(--fail)}.needs-evidence,.manual-review{color:var(--warn)}p{color:var(--muted)}</style></head><body><main><p>Witnora Assurance Control Plane</p><h1>${escapeHtml(report.subject.name)}</h1><strong class="verdict">${report.verdict.passed ? "RELEASE READY" : "RELEASE BLOCKED"}</strong><p>${escapeHtml(report.generatedAt)} · ${report.strict ? "strict" : "advisory"} mode · baseline ${escapeHtml(report.regression.status)}</p><section><h2>Ten release controls</h2><table><thead><tr><th>Control</th><th>Mode</th><th>Status</th><th>Decision</th></tr></thead><tbody>${rows}</tbody></table></section><section><h2>Blockers</h2><ul>${blockers}</ul></section></main></body></html>\n`;
|
|
430
|
+
}
|
|
431
|
+
function renderReleaseGateJunit(report) {
|
|
432
|
+
const failures = report.controls.filter((control) => control.status === "fail" || (report.strict && control.status !== "pass")).length + (report.regression.status === "fail" ? 1 : 0);
|
|
433
|
+
const skipped = (report.strict ? 0 : report.controls.filter((control) => control.status === "needs-evidence" || control.status === "manual-review").length) + (report.regression.status === "not-configured" ? 1 : 0);
|
|
434
|
+
const tests = report.controls.length + 1;
|
|
435
|
+
const cases = report.controls.map((control) => {
|
|
436
|
+
const body = control.status === "fail" || (report.strict && control.status !== "pass")
|
|
437
|
+
? `<failure message="${escapeXml(control.summary)}"/>`
|
|
438
|
+
: control.status === "needs-evidence" || control.status === "manual-review"
|
|
439
|
+
? `<skipped message="${escapeXml(control.summary)}"/>`
|
|
440
|
+
: "";
|
|
441
|
+
return `<testcase classname="agentcert.release-gate" name="${escapeXml(control.id)}">${body}</testcase>`;
|
|
442
|
+
});
|
|
443
|
+
const regressionBody = report.regression.status === "fail" ? `<failure message="${escapeXml(report.regression.regressions.join(" "))}"/>` : report.regression.status === "not-configured" ? '<skipped message="No baseline configured."/>' : "";
|
|
444
|
+
cases.push(`<testcase classname="agentcert.release-gate" name="continuous-regression">${regressionBody}</testcase>`);
|
|
445
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<testsuite name="Witnora release gate" tests="${tests}" failures="${failures}" skipped="${skipped}">\n ${cases.join("\n ")}\n</testsuite>\n`;
|
|
446
|
+
}
|
|
447
|
+
function renderReleaseGateBadge(report) {
|
|
448
|
+
const status = report.verdict.passed ? "ready" : "blocked";
|
|
449
|
+
const color = report.verdict.passed ? "#087f5b" : "#c92a2a";
|
|
450
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="188" height="20" role="img" aria-label="agentcert release: ${status}"><title>agentcert release: ${status}</title><rect width="112" height="20" rx="3" fill="#132033"/><rect x="109" width="79" height="20" rx="3" fill="${color}"/><g fill="#fff" text-anchor="middle" font-family="Verdana,sans-serif" font-size="11"><text x="56" y="14">agentcert release</text><text x="149" y="14">${status}</text></g></svg>\n`;
|
|
451
|
+
}
|
|
452
|
+
function escapeHtml(value) {
|
|
453
|
+
return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
454
|
+
}
|
|
455
|
+
function escapeXml(value) {
|
|
456
|
+
return escapeHtml(value).replaceAll("'", "'");
|
|
457
|
+
}
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
export function renderMarkdownReport(bundle) {
|
|
2
|
+
const lines = [
|
|
3
|
+
"# Witnora Evidence Report",
|
|
4
|
+
"",
|
|
5
|
+
`Subject: ${bundle.subject.name}`,
|
|
6
|
+
`Generated: ${bundle.generatedAt}`,
|
|
7
|
+
`Verdict: ${bundle.verdict.passed ? "PASS" : "FAIL"}`,
|
|
8
|
+
`Score: ${bundle.verdict.score}`,
|
|
9
|
+
`Evidence strength: ${bundle.evidenceStrength?.level ?? "reported"}`,
|
|
10
|
+
"",
|
|
11
|
+
"## Evidence Strength",
|
|
12
|
+
"",
|
|
13
|
+
...(bundle.evidenceStrength?.claims ?? ["A producer supplied a result for this run."]).map((claim) => `- Claim: ${claim}`),
|
|
14
|
+
...(bundle.evidenceStrength?.limitations ?? ["This legacy bundle does not declare stronger source controls."]).map((limitation) => `- Limitation: ${limitation}`),
|
|
15
|
+
"",
|
|
16
|
+
"## Results",
|
|
17
|
+
"",
|
|
18
|
+
];
|
|
19
|
+
for (const result of bundle.results) {
|
|
20
|
+
lines.push(`- ${result.product}: ${result.passed ? "PASS" : "FAIL"} (${result.score}/100, ${result.phase})`, ` ${result.summary ?? ""}`.trimEnd());
|
|
21
|
+
}
|
|
22
|
+
lines.push("", "## Evidence", "");
|
|
23
|
+
if (bundle.evidence.length === 0) {
|
|
24
|
+
lines.push("No blocking evidence recorded.");
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
for (const evidence of bundle.evidence) {
|
|
28
|
+
lines.push(`- [${evidence.severity}] ${evidence.kind}: ${evidence.message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
lines.push("", "## Standards Mapping", "");
|
|
32
|
+
for (const standard of bundle.standards) {
|
|
33
|
+
lines.push(`- ${standard.name}: ${standard.note}`);
|
|
34
|
+
}
|
|
35
|
+
lines.push("", "## Artifacts", "");
|
|
36
|
+
for (const [name, path] of Object.entries(bundle.artifacts)) {
|
|
37
|
+
lines.push(`- ${name}: \`${path}\``);
|
|
38
|
+
}
|
|
39
|
+
return `${lines.join("\n")}\n`;
|
|
40
|
+
}
|
|
41
|
+
export function renderHtmlReport(bundle) {
|
|
42
|
+
const strength = bundle.evidenceStrength ?? {
|
|
43
|
+
level: "reported",
|
|
44
|
+
claims: ["A producer supplied a result for this run."],
|
|
45
|
+
limitations: ["This legacy bundle does not declare stronger source controls."],
|
|
46
|
+
};
|
|
47
|
+
const resultCards = bundle.results
|
|
48
|
+
.map((result) => `<article class="card ${result.passed ? "pass" : "fail"}">
|
|
49
|
+
<span>${escapeHtml(result.phase)}</span>
|
|
50
|
+
<h2>${escapeHtml(result.product)}</h2>
|
|
51
|
+
<strong>${result.passed ? "PASS" : "FAIL"} ${result.score}/100</strong>
|
|
52
|
+
<p>${escapeHtml(result.summary ?? "No summary provided.")}</p>
|
|
53
|
+
</article>`)
|
|
54
|
+
.join("");
|
|
55
|
+
const evidenceRows = bundle.evidence.length === 0
|
|
56
|
+
? `<tr><td colspan="4">No blocking evidence recorded.</td></tr>`
|
|
57
|
+
: bundle.evidence
|
|
58
|
+
.map((evidence) => `<tr>
|
|
59
|
+
<td><span class="severity ${escapeHtml(evidence.severity)}">${escapeHtml(evidence.severity)}</span></td>
|
|
60
|
+
<td>${escapeHtml(evidence.kind)}</td>
|
|
61
|
+
<td>${escapeHtml(evidence.message)}</td>
|
|
62
|
+
<td>${evidence.artifactPath ? artifactLink(evidence.artifactPath) : "-"}</td>
|
|
63
|
+
</tr>`)
|
|
64
|
+
.join("");
|
|
65
|
+
const artifactRows = Object.entries(bundle.artifacts)
|
|
66
|
+
.map(([name, path]) => `<tr><td>${escapeHtml(name)}</td><td>${artifactLink(path)}</td></tr>`)
|
|
67
|
+
.join("");
|
|
68
|
+
const standardItems = bundle.standards
|
|
69
|
+
.map((standard) => `<li><strong>${escapeHtml(standard.name)}</strong><span>${escapeHtml(standard.note)}</span></li>`)
|
|
70
|
+
.join("");
|
|
71
|
+
return `<!doctype html>
|
|
72
|
+
<html lang="en">
|
|
73
|
+
<head>
|
|
74
|
+
<meta charset="utf-8">
|
|
75
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
76
|
+
<title>Witnora Evidence Report</title>
|
|
77
|
+
<style>
|
|
78
|
+
:root { color-scheme: light; --ink: #102033; --muted: #5d6978; --line: #d9e0e8; --bg: #f6f8fb; --panel: #fff; --pass: #087f5b; --fail: #c92a2a; }
|
|
79
|
+
body { margin: 0; font: 15px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--ink); background: var(--bg); }
|
|
80
|
+
main { width: min(1120px, calc(100% - 32px)); margin: 0 auto; padding: 32px 0 48px; }
|
|
81
|
+
header { display: grid; gap: 16px; margin-bottom: 24px; }
|
|
82
|
+
h1 { margin: 0; font-size: clamp(32px, 6vw, 62px); line-height: 0.95; }
|
|
83
|
+
h2 { margin: 0 0 10px; font-size: 18px; }
|
|
84
|
+
p { color: var(--muted); margin: 0; }
|
|
85
|
+
.summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin: 24px 0; }
|
|
86
|
+
.metric, .card, section { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 18px; }
|
|
87
|
+
.metric span, .card span { display: block; color: var(--muted); font-size: 12px; text-transform: uppercase; font-weight: 700; }
|
|
88
|
+
.metric strong { display: block; margin-top: 8px; font-size: 28px; }
|
|
89
|
+
.verdict-pass { color: var(--pass); }
|
|
90
|
+
.verdict-fail { color: var(--fail); }
|
|
91
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
|
|
92
|
+
.card { border-left: 5px solid var(--line); }
|
|
93
|
+
.card.pass { border-left-color: var(--pass); }
|
|
94
|
+
.card.fail { border-left-color: var(--fail); }
|
|
95
|
+
.card strong { display: block; margin: 8px 0; font-size: 20px; }
|
|
96
|
+
section { margin-top: 16px; overflow-x: auto; }
|
|
97
|
+
table { width: 100%; border-collapse: collapse; min-width: 720px; }
|
|
98
|
+
th, td { padding: 10px 8px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
|
|
99
|
+
th { font-size: 12px; text-transform: uppercase; color: var(--muted); }
|
|
100
|
+
a { color: #0b63ce; text-decoration: none; font-weight: 650; }
|
|
101
|
+
ul { margin: 0; padding-left: 20px; }
|
|
102
|
+
li { margin: 8px 0; }
|
|
103
|
+
li span { display: block; color: var(--muted); }
|
|
104
|
+
.severity { display: inline-block; border-radius: 999px; padding: 2px 8px; font-size: 12px; font-weight: 800; color: #fff; background: #687385; }
|
|
105
|
+
.severity.critical, .severity.high { background: var(--fail); }
|
|
106
|
+
.severity.medium { background: #b35c00; }
|
|
107
|
+
.severity.low, .severity.info { background: #24745a; }
|
|
108
|
+
@media (max-width: 760px) { .summary { grid-template-columns: 1fr 1fr; } main { width: min(100% - 24px, 1120px); padding-top: 20px; } }
|
|
109
|
+
</style>
|
|
110
|
+
</head>
|
|
111
|
+
<body>
|
|
112
|
+
<main>
|
|
113
|
+
<header>
|
|
114
|
+
<p>Witnora Evidence Report</p>
|
|
115
|
+
<h1>${escapeHtml(bundle.subject.name)}</h1>
|
|
116
|
+
<p>Generated ${escapeHtml(bundle.generatedAt)}. Portable evidence bundle ${escapeHtml(bundle.runId)}.</p>
|
|
117
|
+
</header>
|
|
118
|
+
<div class="summary">
|
|
119
|
+
<div class="metric"><span>Verdict</span><strong class="${bundle.verdict.passed ? "verdict-pass" : "verdict-fail"}">${bundle.verdict.passed ? "PASS" : "FAIL"}</strong></div>
|
|
120
|
+
<div class="metric"><span>Score</span><strong>${bundle.verdict.score}/100</strong></div>
|
|
121
|
+
<div class="metric"><span>Evidence strength</span><strong>${escapeHtml(bundle.evidenceStrength?.level ?? "reported")}</strong></div>
|
|
122
|
+
<div class="metric"><span>Evidence</span><strong>${bundle.summary.totalEvidence}</strong></div>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="cards">${resultCards}</div>
|
|
125
|
+
<section>
|
|
126
|
+
<h2>Evidence strength: ${escapeHtml(strength.level)}</h2>
|
|
127
|
+
<h3>Supported claims</h3>
|
|
128
|
+
<ul>${strength.claims.map((claim) => `<li>${escapeHtml(claim)}</li>`).join("")}</ul>
|
|
129
|
+
<h3>Limitations</h3>
|
|
130
|
+
<ul>${strength.limitations.map((limitation) => `<li>${escapeHtml(limitation)}</li>`).join("")}</ul>
|
|
131
|
+
</section>
|
|
132
|
+
<section>
|
|
133
|
+
<h2>Evidence</h2>
|
|
134
|
+
<table>
|
|
135
|
+
<thead><tr><th>Severity</th><th>Kind</th><th>Message</th><th>Artifact</th></tr></thead>
|
|
136
|
+
<tbody>${evidenceRows}</tbody>
|
|
137
|
+
</table>
|
|
138
|
+
</section>
|
|
139
|
+
<section>
|
|
140
|
+
<h2>Artifacts</h2>
|
|
141
|
+
<table>
|
|
142
|
+
<thead><tr><th>Name</th><th>Path</th></tr></thead>
|
|
143
|
+
<tbody>${artifactRows || `<tr><td colspan="2">No artifacts recorded.</td></tr>`}</tbody>
|
|
144
|
+
</table>
|
|
145
|
+
</section>
|
|
146
|
+
<section>
|
|
147
|
+
<h2>Standards Mapping</h2>
|
|
148
|
+
<ul>${standardItems}</ul>
|
|
149
|
+
</section>
|
|
150
|
+
</main>
|
|
151
|
+
</body>
|
|
152
|
+
</html>
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
function artifactLink(path) {
|
|
156
|
+
const escaped = escapeHtml(path);
|
|
157
|
+
return `<a href="${escapeHtml(path)}">${escaped}</a>`;
|
|
158
|
+
}
|
|
159
|
+
function escapeHtml(value) {
|
|
160
|
+
return String(value)
|
|
161
|
+
.replaceAll("&", "&")
|
|
162
|
+
.replaceAll("<", "<")
|
|
163
|
+
.replaceAll(">", ">")
|
|
164
|
+
.replaceAll('"', """);
|
|
165
|
+
}
|