@cejel/cejel 0.1.4 → 0.1.6
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 +60 -18
- package/dist/{chunk-SQNTNX5U.js → chunk-A7NZKJ5H.js} +530 -468
- package/dist/index.js +27 -10
- package/dist/mcp/index.js +2 -2
- package/package.json +6 -2
|
@@ -29,450 +29,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
29
29
|
mod
|
|
30
30
|
));
|
|
31
31
|
|
|
32
|
-
// src/witan/external-findings.ts
|
|
33
|
-
var EXTERNAL_FINDINGS_DISPLAY_LIMIT = 10;
|
|
34
|
-
var SEVERITY_RANK = {
|
|
35
|
-
critical: 0,
|
|
36
|
-
warning: 1,
|
|
37
|
-
info: 2
|
|
38
|
-
};
|
|
39
|
-
function formatExternalSourceLabel(source) {
|
|
40
|
-
return source.startsWith("sarif:") ? source.slice("sarif:".length) : source;
|
|
41
|
-
}
|
|
42
|
-
function summarizeExternalSources(consumedSignals) {
|
|
43
|
-
const bySource = /* @__PURE__ */ new Map();
|
|
44
|
-
for (const signal of consumedSignals) {
|
|
45
|
-
const entry = bySource.get(signal.source) ?? { findingCount: 0, dimensions: /* @__PURE__ */ new Set() };
|
|
46
|
-
entry.findingCount += signal.findingCount;
|
|
47
|
-
entry.dimensions.add(signal.dimension);
|
|
48
|
-
bySource.set(signal.source, entry);
|
|
49
|
-
}
|
|
50
|
-
return Array.from(bySource.entries()).map(([source, { findingCount, dimensions }]) => ({
|
|
51
|
-
source,
|
|
52
|
-
label: formatExternalSourceLabel(source),
|
|
53
|
-
findingCount,
|
|
54
|
-
dimensions: Array.from(dimensions).sort()
|
|
55
|
-
})).sort((a, b) => a.source.localeCompare(b.source));
|
|
56
|
-
}
|
|
57
|
-
function formatExternalSourceLine(summary) {
|
|
58
|
-
const noun = summary.findingCount === 1 ? "finding" : "findings";
|
|
59
|
-
return `${summary.label}: ${summary.findingCount} ${noun} ingested (folded into ${summary.dimensions.join(", ")})`;
|
|
60
|
-
}
|
|
61
|
-
function collectExternalFindings(consumedSignals) {
|
|
62
|
-
const findings = [];
|
|
63
|
-
for (const signal of consumedSignals) {
|
|
64
|
-
for (const finding of signal.findings) {
|
|
65
|
-
findings.push({
|
|
66
|
-
source: signal.source,
|
|
67
|
-
label: formatExternalSourceLabel(signal.source),
|
|
68
|
-
dimension: signal.dimension,
|
|
69
|
-
severity: finding.severity,
|
|
70
|
-
ruleId: finding.ruleId,
|
|
71
|
-
message: finding.message,
|
|
72
|
-
...finding.location ? { location: finding.location } : {}
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return findings.sort((a, b) => {
|
|
77
|
-
const bySeverity = SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
|
|
78
|
-
if (bySeverity !== 0) return bySeverity;
|
|
79
|
-
const bySource = a.source.localeCompare(b.source);
|
|
80
|
-
if (bySource !== 0) return bySource;
|
|
81
|
-
return a.ruleId.localeCompare(b.ruleId);
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// src/witan/html.ts
|
|
86
|
-
function renderWitanHtmlReport(report) {
|
|
87
|
-
const codeCriteria = criteriaByCategory(report, "code_trust");
|
|
88
|
-
const processCriteria = criteriaByCategory(report, "process_trust");
|
|
89
|
-
const verifiedEvidence = report.criteria.flatMap(
|
|
90
|
-
(criterion) => criterion.status === "verified" ? criterion.evidence.map((evidence) => renderEvidenceListItem(criterion, evidence)) : []
|
|
91
|
-
);
|
|
92
|
-
const openItems = report.criteria.flatMap(renderOpenItems);
|
|
93
|
-
const contributingSources = renderContributingSources(report.consumedSignals ?? []);
|
|
94
|
-
const externalSourceSummaries = summarizeExternalSources(report.consumedSignals ?? []);
|
|
95
|
-
const externalFindings = collectExternalFindings(report.consumedSignals ?? []);
|
|
96
|
-
return `<!doctype html>
|
|
97
|
-
<html lang="en">
|
|
98
|
-
<head>
|
|
99
|
-
<meta charset="utf-8">
|
|
100
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
101
|
-
<title>Cejel Trust Certificate - ${escapeHtml(report.productDisplayName)}</title>
|
|
102
|
-
<style>${CERTIFICATE_CSS}</style>
|
|
103
|
-
</head>
|
|
104
|
-
<body>
|
|
105
|
-
<main class="certificate">
|
|
106
|
-
<header class="hero">
|
|
107
|
-
<div class="brand">
|
|
108
|
-
<span class="brand-mark" aria-hidden="true">C</span>
|
|
109
|
-
<span class="brand-name">Cejel</span>
|
|
110
|
-
</div>
|
|
111
|
-
<div class="hero-grid">
|
|
112
|
-
<section>
|
|
113
|
-
<p class="eyebrow">Trust Certificate</p>
|
|
114
|
-
<h1>${escapeHtml(report.productDisplayName)}</h1>
|
|
115
|
-
<dl class="meta">
|
|
116
|
-
<div><dt>Date</dt><dd>${escapeHtml(formatDate(report.generatedAt))}</dd></div>
|
|
117
|
-
<div><dt>Run</dt><dd>${escapeHtml(renderRepo(report))}</dd></div>
|
|
118
|
-
<div><dt>Rubric</dt><dd>${escapeHtml(report.rubricVersion)}</dd></div>
|
|
119
|
-
${contributingSources.length > 0 ? `<div><dt>Sources</dt><dd>Incorporates findings from: ${escapeHtml(contributingSources.join(", "))}<ul class="source-counts">${externalSourceSummaries.map((s) => `<li>${escapeHtml(formatExternalSourceLine(s))}</li>`).join("")}</ul></dd></div>` : ""}
|
|
120
|
-
</dl>
|
|
121
|
-
</section>
|
|
122
|
-
<aside class="score-panel" aria-label="Summary trust score">
|
|
123
|
-
<div class="score-badge">Summary</div>
|
|
124
|
-
${report.insufficientSourceReason ? `<div class="verdict">Insufficient source</div>
|
|
125
|
-
<p class="insufficient-note">${escapeHtml(report.insufficientSourceReason)}</p>` : `<div class="score">${formatScore(report.overallScore)}</div>
|
|
126
|
-
<div class="score-unit">/ 4.0 overall</div>
|
|
127
|
-
<div class="verdict">${escapeHtml(renderVerdict(report.overallScore))}</div>
|
|
128
|
-
<div class="subscores">
|
|
129
|
-
<span>Code ${formatScore(report.codeTrustScore)}</span>
|
|
130
|
-
<span>Process ${formatScore(report.processTrustScore)}</span>
|
|
131
|
-
</div>`}
|
|
132
|
-
</aside>
|
|
133
|
-
</div>
|
|
134
|
-
</header>
|
|
135
|
-
|
|
136
|
-
<section class="trust-grid" aria-label="Rubric criteria">
|
|
137
|
-
${renderCriterionColumn("Code trust", codeCriteria)}
|
|
138
|
-
${renderCriterionColumn("Process trust", processCriteria)}
|
|
139
|
-
</section>
|
|
140
|
-
|
|
141
|
-
<section class="evidence-grid" aria-label="Evidence and gaps">
|
|
142
|
-
<article class="evidence-section">
|
|
143
|
-
<h2>Verified evidence</h2>
|
|
144
|
-
<ul>${verifiedEvidence.length > 0 ? verifiedEvidence.join("") : "<li>No verified evidence supplied.</li>"}</ul>
|
|
145
|
-
</article>
|
|
146
|
-
<article class="evidence-section">
|
|
147
|
-
<h2>Open / unverified</h2>
|
|
148
|
-
<ul>${openItems.length > 0 ? openItems.join("") : "<li>No open or unverified criteria supplied.</li>"}</ul>
|
|
149
|
-
</article>
|
|
150
|
-
</section>
|
|
151
|
-
|
|
152
|
-
${externalFindings.length > 0 ? `<section class="external-findings-section" aria-label="External findings">
|
|
153
|
-
<article class="evidence-section">
|
|
154
|
-
<h2>External findings</h2>
|
|
155
|
-
<p class="external-note">Findings surfaced by ingested external scanners, attributed to the tool and the cejel criterion they were folded into \u2014 distinct from cejel's own findings above.</p>
|
|
156
|
-
<ul>${renderExternalFindingItems(externalFindings).join("")}</ul>
|
|
157
|
-
</article>
|
|
158
|
-
</section>` : ""}
|
|
159
|
-
</main>
|
|
160
|
-
</body>
|
|
161
|
-
</html>
|
|
162
|
-
`;
|
|
163
|
-
}
|
|
164
|
-
function renderContributingSources(signals) {
|
|
165
|
-
return Array.from(new Set(signals.map((s) => s.source))).sort();
|
|
166
|
-
}
|
|
167
|
-
function renderExternalFindingItems(findings) {
|
|
168
|
-
const shown = findings.slice(0, EXTERNAL_FINDINGS_DISPLAY_LIMIT);
|
|
169
|
-
const items = shown.map(
|
|
170
|
-
(f) => `<li><strong>${escapeHtml(f.label)} \u2192 ${escapeHtml(f.dimension)}</strong><span>${escapeHtml(f.severity)}: ${escapeHtml(f.ruleId)} \u2014 ${escapeHtml(f.message)}${f.location ? ` (${escapeHtml(f.location)})` : ""}</span></li>`
|
|
171
|
-
);
|
|
172
|
-
const remaining = findings.length - shown.length;
|
|
173
|
-
if (remaining > 0) {
|
|
174
|
-
items.push(`<li><em>...and ${remaining} more \u2014 see report.json for the full list.</em></li>`);
|
|
175
|
-
}
|
|
176
|
-
return items;
|
|
177
|
-
}
|
|
178
|
-
function criteriaByCategory(report, category) {
|
|
179
|
-
return report.criteria.filter((criterion) => criterion.category === category);
|
|
180
|
-
}
|
|
181
|
-
function renderCriterionColumn(title, criteria) {
|
|
182
|
-
const applicable = criteria.filter((criterion) => criterion.status !== "not_applicable");
|
|
183
|
-
const notApplicable = criteria.filter((criterion) => criterion.status === "not_applicable");
|
|
184
|
-
return `<section class="criteria-column">
|
|
185
|
-
<h2>${escapeHtml(title)}</h2>
|
|
186
|
-
<div class="criteria-list">
|
|
187
|
-
${applicable.map(renderCriterionCard).join("")}
|
|
188
|
-
</div>
|
|
189
|
-
${notApplicable.length > 0 ? renderNotApplicableGroup(notApplicable) : ""}
|
|
190
|
-
</section>`;
|
|
191
|
-
}
|
|
192
|
-
function renderNotApplicableGroup(criteria) {
|
|
193
|
-
return `<div class="na-group">
|
|
194
|
-
<h3 class="na-heading">Not applicable to this repository</h3>
|
|
195
|
-
<ul class="na-list">
|
|
196
|
-
${criteria.map(renderNotApplicableItem).join("")}
|
|
197
|
-
</ul>
|
|
198
|
-
</div>`;
|
|
199
|
-
}
|
|
200
|
-
function renderNotApplicableItem(criterion) {
|
|
201
|
-
const reason = criterion.notes ?? "Not applicable to this repository.";
|
|
202
|
-
return `<li><span class="na-id">${escapeHtml(criterion.id)}</span> ${escapeHtml(criterion.title)} \u2014 <span class="na-reason">${escapeHtml(reason)}</span></li>`;
|
|
203
|
-
}
|
|
204
|
-
function renderCriterionCard(criterion) {
|
|
205
|
-
const evidence = [
|
|
206
|
-
...criterion.evidence.map(renderEvidencePointer),
|
|
207
|
-
...criterion.findings.map((finding) => renderFindingEvidence(finding))
|
|
208
|
-
];
|
|
209
|
-
const metrics = criterion.metrics.map(renderMetric);
|
|
210
|
-
return `<article class="criterion">
|
|
211
|
-
<div class="criterion-top">
|
|
212
|
-
<div>
|
|
213
|
-
<div class="criterion-id">${escapeHtml(criterion.id)}</div>
|
|
214
|
-
<h3>${escapeHtml(criterion.title)}</h3>
|
|
215
|
-
</div>
|
|
216
|
-
${renderStatusChip(criterion.status)}
|
|
217
|
-
</div>
|
|
218
|
-
<div class="criterion-score">${criterion.status === "not_applicable" ? "N/A" : criterion.status === "insufficient_data" ? "No data" : formatScore(criterion.score)}</div>
|
|
219
|
-
<ul class="criterion-metrics">
|
|
220
|
-
${metrics.length > 0 ? metrics.map((item) => `<li>${item}</li>`).join("") : "<li>No measured depth metrics supplied.</li>"}
|
|
221
|
-
</ul>
|
|
222
|
-
<ul class="criterion-evidence">
|
|
223
|
-
${evidence.length > 0 ? evidence.map((item) => `<li>${item}</li>`).join("") : "<li>No concrete evidence supplied.</li>"}
|
|
224
|
-
</ul>
|
|
225
|
-
</article>`;
|
|
226
|
-
}
|
|
227
|
-
function renderMetric(metric2) {
|
|
228
|
-
const max = metric2.max ? `/${formatMetricValue(metric2.max)}` : "";
|
|
229
|
-
const unit = metric2.unit ? ` ${escapeHtml(metric2.unit)}` : "";
|
|
230
|
-
return `<strong>${escapeHtml(metric2.label)}</strong><span>${formatMetricValue(metric2.value)}${max}${unit}</span>`;
|
|
231
|
-
}
|
|
232
|
-
function renderStatusChip(status) {
|
|
233
|
-
return `<span class="status" data-status="${status}">${status}</span>`;
|
|
234
|
-
}
|
|
235
|
-
function renderEvidenceListItem(criterion, evidence) {
|
|
236
|
-
return `<li><strong>${escapeHtml(criterion.id)} - ${escapeHtml(criterion.title)}</strong><span>${renderEvidencePointer(evidence)}</span></li>`;
|
|
237
|
-
}
|
|
238
|
-
function renderOpenItems(criterion) {
|
|
239
|
-
const findingItems = criterion.findings.map(
|
|
240
|
-
(finding) => `<li><strong>${escapeHtml(criterion.id)} - ${escapeHtml(criterion.title)}</strong><span>${escapeHtml(finding.severity)}: ${escapeHtml(finding.summary)} (${renderEvidencePointer(finding.evidence)})</span></li>`
|
|
241
|
-
);
|
|
242
|
-
if ((criterion.status === "unverified" || criterion.status === "insufficient_data") && criterion.evidence.length === 0 && criterion.findings.length === 0) {
|
|
243
|
-
const message = criterion.status === "insufficient_data" ? "Insufficient data \u2014 no measurable signal; excluded from composite (unmeasured, not inapplicable)." : "No concrete evidence supplied.";
|
|
244
|
-
return [
|
|
245
|
-
`<li><strong>${escapeHtml(criterion.id)} - ${escapeHtml(criterion.title)}</strong><span>${escapeHtml(message)}</span></li>`
|
|
246
|
-
];
|
|
247
|
-
}
|
|
248
|
-
if (criterion.status === "verified") return [];
|
|
249
|
-
return findingItems;
|
|
250
|
-
}
|
|
251
|
-
function renderFindingEvidence(finding) {
|
|
252
|
-
return `${escapeHtml(finding.severity)}: ${escapeHtml(finding.summary)} (${renderEvidencePointer(
|
|
253
|
-
finding.evidence
|
|
254
|
-
)})`;
|
|
255
|
-
}
|
|
256
|
-
function renderEvidencePointer(evidence) {
|
|
257
|
-
const target = evidence.url ?? renderPathEvidenceTarget(evidence);
|
|
258
|
-
const label = escapeHtml(evidence.label);
|
|
259
|
-
if (evidence.url) {
|
|
260
|
-
return `<a href="${escapeAttribute(evidence.url)}">${label}</a>`;
|
|
261
|
-
}
|
|
262
|
-
return [label, target ? `<code>${escapeHtml(target)}</code>` : void 0].filter(Boolean).join(" ");
|
|
263
|
-
}
|
|
264
|
-
function renderPathEvidenceTarget(evidence) {
|
|
265
|
-
const pathTarget = evidence.path ? `${evidence.path}${evidence.line ? `:${evidence.line}` : ""}` : void 0;
|
|
266
|
-
const hashTarget = evidence.contentHash ? `sha256:${evidence.contentHash.slice(0, 12)}` : void 0;
|
|
267
|
-
return [pathTarget, hashTarget].filter(Boolean).join(" \xB7 ");
|
|
268
|
-
}
|
|
269
|
-
function renderRepo(report) {
|
|
270
|
-
const repo = report.repo.path ?? report.repo.url ?? "unknown";
|
|
271
|
-
return report.repo.headSha ? `${repo} @ ${report.repo.headSha}` : repo;
|
|
272
|
-
}
|
|
273
|
-
function renderVerdict(score) {
|
|
274
|
-
if (score >= 3.5) return "Verified";
|
|
275
|
-
if (score >= 2.5) return "Conditional";
|
|
276
|
-
if (score >= 1.5) return "At risk";
|
|
277
|
-
return "Unverified";
|
|
278
|
-
}
|
|
279
|
-
function renderReportVerdict(report) {
|
|
280
|
-
if (report.insufficientSourceReason) return "Insufficient source";
|
|
281
|
-
return renderVerdict(report.overallScore);
|
|
282
|
-
}
|
|
283
|
-
function formatScore(score) {
|
|
284
|
-
return score.toFixed(1);
|
|
285
|
-
}
|
|
286
|
-
function formatMetricValue(value) {
|
|
287
|
-
return Number.isInteger(value) ? value.toString() : value.toFixed(2);
|
|
288
|
-
}
|
|
289
|
-
function formatDate(value) {
|
|
290
|
-
return value.slice(0, 10);
|
|
291
|
-
}
|
|
292
|
-
function escapeHtml(value) {
|
|
293
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
294
|
-
}
|
|
295
|
-
function escapeAttribute(value) {
|
|
296
|
-
return escapeHtml(value);
|
|
297
|
-
}
|
|
298
|
-
var CERTIFICATE_CSS = `
|
|
299
|
-
:root {
|
|
300
|
-
color-scheme: dark;
|
|
301
|
-
--steel: #16212a;
|
|
302
|
-
--surface: #1d2b35;
|
|
303
|
-
--surface-2: #273946;
|
|
304
|
-
--line: rgba(238, 244, 247, .13);
|
|
305
|
-
--line-strong: rgba(238, 244, 247, .22);
|
|
306
|
-
--text: #eef4f7;
|
|
307
|
-
--muted: #b7c5cc;
|
|
308
|
-
--faint: #8799a4;
|
|
309
|
-
--teal: #2fc8a6;
|
|
310
|
-
--teal-weak: rgba(47, 200, 166, .14);
|
|
311
|
-
--periwinkle: #93a6da;
|
|
312
|
-
--periwinkle-weak: rgba(147, 166, 218, .16);
|
|
313
|
-
--warn: #e7bf72;
|
|
314
|
-
--warn-weak: rgba(231, 191, 114, .16);
|
|
315
|
-
--danger: #f09a8f;
|
|
316
|
-
--danger-weak: rgba(240, 154, 143, .14);
|
|
317
|
-
--quiet: rgba(238, 244, 247, .08);
|
|
318
|
-
--serif: "Instrument Serif", Georgia, "Times New Roman", serif;
|
|
319
|
-
--sans: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
320
|
-
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
321
|
-
}
|
|
322
|
-
* { box-sizing: border-box; }
|
|
323
|
-
body {
|
|
324
|
-
margin: 0;
|
|
325
|
-
background: var(--steel);
|
|
326
|
-
color: var(--text);
|
|
327
|
-
font-family: var(--sans);
|
|
328
|
-
line-height: 1.55;
|
|
329
|
-
-webkit-font-smoothing: antialiased;
|
|
330
|
-
}
|
|
331
|
-
.certificate { max-width: 1120px; margin: 0 auto; padding: 44px 32px 64px; }
|
|
332
|
-
.hero { border-bottom: 1px solid var(--line); padding-bottom: 38px; }
|
|
333
|
-
.brand { display: flex; align-items: center; gap: 12px; margin-bottom: 44px; }
|
|
334
|
-
.brand-mark {
|
|
335
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
336
|
-
width: 34px; height: 34px; border-radius: 8px;
|
|
337
|
-
background: var(--teal); color: #06231d; font-family: var(--serif); font-size: 24px;
|
|
338
|
-
}
|
|
339
|
-
.brand-name { font-family: var(--serif); font-size: 24px; }
|
|
340
|
-
.hero-grid { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 36px; align-items: end; }
|
|
341
|
-
.eyebrow {
|
|
342
|
-
margin: 0 0 14px; color: var(--teal); font-family: var(--mono);
|
|
343
|
-
font-size: 11px; letter-spacing: .18em; text-transform: uppercase;
|
|
344
|
-
}
|
|
345
|
-
h1, h2, h3, p { margin-top: 0; }
|
|
346
|
-
h1 { margin-bottom: 24px; font-family: var(--serif); font-size: 64px; line-height: 1; font-weight: 400; letter-spacing: 0; }
|
|
347
|
-
.meta { display: grid; gap: 10px; margin: 0; max-width: 780px; }
|
|
348
|
-
.meta div { display: grid; grid-template-columns: 86px minmax(0, 1fr); gap: 16px; }
|
|
349
|
-
dt { color: var(--faint); font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: .12em; }
|
|
350
|
-
dd { margin: 0; color: var(--muted); overflow-wrap: anywhere; }
|
|
351
|
-
.score-panel { border: 1px solid var(--line-strong); border-radius: 8px; padding: 24px; background: var(--surface); }
|
|
352
|
-
.score-badge {
|
|
353
|
-
display: inline-flex; margin-bottom: 14px; border: 1px solid rgba(147, 166, 218, .38);
|
|
354
|
-
border-radius: 999px; padding: 5px 10px; color: var(--periwinkle);
|
|
355
|
-
background: var(--periwinkle-weak); font-family: var(--mono); font-size: 12px;
|
|
356
|
-
}
|
|
357
|
-
.score { display: inline; font-family: var(--mono); font-size: 56px; line-height: 1; color: var(--teal); }
|
|
358
|
-
.score-unit { display: inline; margin-left: 6px; color: var(--faint); font-family: var(--mono); }
|
|
359
|
-
.verdict { margin-top: 12px; font-family: var(--serif); font-size: 30px; color: var(--periwinkle); }
|
|
360
|
-
.insufficient-note { margin-top: 10px; color: var(--muted); font-size: 13px; line-height: 1.5; }
|
|
361
|
-
.subscores { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 18px; color: var(--muted); font-family: var(--mono); font-size: 12px; }
|
|
362
|
-
.source-counts { margin: 8px 0 0; padding-left: 18px; font-family: var(--mono); font-size: 12px; color: var(--muted); }
|
|
363
|
-
.trust-grid, .evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; margin-top: 28px; }
|
|
364
|
-
.external-findings-section { margin-top: 18px; }
|
|
365
|
-
.external-note { margin: 0 0 14px; color: var(--muted); font-size: 13px; }
|
|
366
|
-
.criteria-column, .evidence-section { border: 1px solid var(--line); border-radius: 8px; background: var(--surface); padding: 22px; }
|
|
367
|
-
h2 { font-family: var(--serif); font-size: 30px; font-weight: 400; letter-spacing: 0; margin-bottom: 18px; }
|
|
368
|
-
.criteria-list { display: grid; gap: 12px; }
|
|
369
|
-
.na-group { margin-top: 16px; padding-top: 14px; border-top: 1px dashed var(--line-strong); }
|
|
370
|
-
.na-heading { font-family: var(--sans); font-size: 12px; font-weight: 650; letter-spacing: .04em; text-transform: uppercase; color: var(--faint); margin: 0 0 10px; }
|
|
371
|
-
.na-list { display: grid; gap: 8px; font-size: 13px; }
|
|
372
|
-
.na-list li { margin: 0; color: var(--muted); }
|
|
373
|
-
.na-id { color: var(--faint); font-family: var(--mono); font-size: 11px; margin-right: 6px; }
|
|
374
|
-
.na-reason { color: var(--muted); }
|
|
375
|
-
.criterion { border: 1px solid var(--line); border-radius: 8px; padding: 16px; background: var(--surface-2); }
|
|
376
|
-
.criterion-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; }
|
|
377
|
-
.criterion-id { color: var(--periwinkle); font-family: var(--mono); font-size: 12px; margin-bottom: 4px; }
|
|
378
|
-
h3 { font-size: 15px; line-height: 1.35; font-weight: 600; margin-bottom: 0; }
|
|
379
|
-
.criterion-score { margin-top: 12px; color: var(--text); font-family: var(--mono); font-size: 24px; }
|
|
380
|
-
.criterion-metrics { display: grid; gap: 7px; margin-top: 10px; padding: 0; list-style: none; font-size: 12px; }
|
|
381
|
-
.criterion-metrics li { display: flex; justify-content: space-between; gap: 12px; border-bottom: 1px solid rgba(238, 244, 247, .08); padding-bottom: 6px; }
|
|
382
|
-
.criterion-metrics strong { color: var(--text); font-weight: 650; }
|
|
383
|
-
.criterion-metrics span { white-space: nowrap; color: var(--muted); }
|
|
384
|
-
.status { flex: none; border-radius: 999px; padding: 4px 9px; font-family: var(--mono); font-size: 11px; border: 1px solid var(--line-strong); }
|
|
385
|
-
.status[data-status="verified"] { color: var(--teal); background: var(--teal-weak); border-color: rgba(47, 200, 166, .42); }
|
|
386
|
-
.status[data-status="info"] { color: var(--periwinkle); background: var(--periwinkle-weak); border-color: rgba(147, 166, 218, .4); }
|
|
387
|
-
.status[data-status="warning"] { color: var(--warn); background: var(--warn-weak); border-color: rgba(231, 191, 114, .44); }
|
|
388
|
-
.status[data-status="critical"] { color: var(--danger); background: var(--danger-weak); border-color: rgba(240, 154, 143, .46); }
|
|
389
|
-
.status[data-status="unverified"] { color: var(--muted); background: var(--quiet); }
|
|
390
|
-
.status[data-status="insufficient_data"] { color: var(--muted); background: var(--quiet); border-style: dashed; }
|
|
391
|
-
ul { margin: 0; padding-left: 18px; }
|
|
392
|
-
li { margin: 8px 0; color: var(--muted); }
|
|
393
|
-
.criterion-evidence { margin-top: 12px; font-size: 13px; }
|
|
394
|
-
.evidence-section li strong { display: block; color: var(--text); font-size: 14px; }
|
|
395
|
-
.evidence-section li span { display: block; margin-top: 2px; font-size: 13px; }
|
|
396
|
-
code { color: var(--periwinkle); font-family: var(--mono); font-size: .92em; overflow-wrap: anywhere; }
|
|
397
|
-
a { color: var(--teal); text-decoration: none; }
|
|
398
|
-
a:hover { text-decoration: underline; text-underline-offset: 2px; }
|
|
399
|
-
@media (max-width: 840px) {
|
|
400
|
-
.certificate { padding: 28px 18px 48px; }
|
|
401
|
-
.hero-grid, .trust-grid, .evidence-grid { grid-template-columns: 1fr; }
|
|
402
|
-
h1 { font-size: 46px; }
|
|
403
|
-
}
|
|
404
|
-
`;
|
|
405
|
-
|
|
406
|
-
// src/witan/badge.ts
|
|
407
|
-
var VERDICT_COLOR = {
|
|
408
|
-
Verified: "brightgreen",
|
|
409
|
-
Conditional: "yellow",
|
|
410
|
-
"At risk": "orange",
|
|
411
|
-
Unverified: "red",
|
|
412
|
-
"Insufficient source": "lightgrey"
|
|
413
|
-
};
|
|
414
|
-
var VERDICT_SVG_FILL = {
|
|
415
|
-
Verified: "#2fc86a",
|
|
416
|
-
Conditional: "#dfb92f",
|
|
417
|
-
"At risk": "#e08a3c",
|
|
418
|
-
Unverified: "#d64545",
|
|
419
|
-
"Insufficient source": "#8c8c8c"
|
|
420
|
-
};
|
|
421
|
-
function badgeMessage(report, verdict) {
|
|
422
|
-
if (report.insufficientSourceReason) return "unrated: no source";
|
|
423
|
-
return `${formatScore2(report.overallScore)}/4.0 ${verdict.toLowerCase()}`;
|
|
424
|
-
}
|
|
425
|
-
function renderWitanBadgeEndpoint(report) {
|
|
426
|
-
const verdict = renderReportVerdict(report);
|
|
427
|
-
return {
|
|
428
|
-
schemaVersion: 1,
|
|
429
|
-
label: "cejel trust",
|
|
430
|
-
message: badgeMessage(report, verdict),
|
|
431
|
-
color: VERDICT_COLOR[verdict] ?? "lightgrey"
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
function renderWitanBadgeSvg(report) {
|
|
435
|
-
const verdict = renderReportVerdict(report);
|
|
436
|
-
const label = "cejel trust";
|
|
437
|
-
const message = badgeMessage(report, verdict);
|
|
438
|
-
const fill = VERDICT_SVG_FILL[verdict] ?? "#8c8c8c";
|
|
439
|
-
const charWidth = 6.5;
|
|
440
|
-
const padding = 10;
|
|
441
|
-
const labelWidth = Math.round(label.length * charWidth + padding);
|
|
442
|
-
const messageWidth = Math.round(message.length * charWidth + padding);
|
|
443
|
-
const totalWidth = labelWidth + messageWidth;
|
|
444
|
-
const height = 20;
|
|
445
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="${height}" role="img" aria-label="${escapeXml(label)}: ${escapeXml(message)}">
|
|
446
|
-
<title>${escapeXml(label)}: ${escapeXml(message)}</title>
|
|
447
|
-
<linearGradient id="witan-badge-smooth" x2="0" y2="100%">
|
|
448
|
-
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
449
|
-
<stop offset="1" stop-opacity=".1"/>
|
|
450
|
-
</linearGradient>
|
|
451
|
-
<clipPath id="witan-badge-round">
|
|
452
|
-
<rect width="${totalWidth}" height="${height}" rx="3" fill="#fff"/>
|
|
453
|
-
</clipPath>
|
|
454
|
-
<g clip-path="url(#witan-badge-round)">
|
|
455
|
-
<rect width="${labelWidth}" height="${height}" fill="#555"/>
|
|
456
|
-
<rect x="${labelWidth}" width="${messageWidth}" height="${height}" fill="${fill}"/>
|
|
457
|
-
<rect width="${totalWidth}" height="${height}" fill="url(#witan-badge-smooth)"/>
|
|
458
|
-
</g>
|
|
459
|
-
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
|
|
460
|
-
<text x="${labelWidth / 2}" y="14">${escapeXml(label)}</text>
|
|
461
|
-
<text x="${labelWidth + messageWidth / 2}" y="14">${escapeXml(message)}</text>
|
|
462
|
-
</g>
|
|
463
|
-
</svg>
|
|
464
|
-
`;
|
|
465
|
-
}
|
|
466
|
-
function formatScore2(score) {
|
|
467
|
-
return score.toFixed(1);
|
|
468
|
-
}
|
|
469
|
-
function escapeXml(value) {
|
|
470
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// src/witan/attestation.ts
|
|
474
|
-
import { createHash } from "crypto";
|
|
475
|
-
|
|
476
32
|
// node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
477
33
|
var external_exports = {};
|
|
478
34
|
__export(external_exports, {
|
|
@@ -4686,30 +4242,70 @@ var WitanConsumedSignalSummarySchema = external_exports.object({
|
|
|
4686
4242
|
// this field always carries every finding.
|
|
4687
4243
|
findings: external_exports.array(WitanInputSignalFindingSchema).default([])
|
|
4688
4244
|
}).strict();
|
|
4689
|
-
var
|
|
4245
|
+
var WitanScoredVerdictSchema = external_exports.enum([
|
|
4246
|
+
"verified",
|
|
4247
|
+
"conditional",
|
|
4248
|
+
"at_risk",
|
|
4249
|
+
"unverified"
|
|
4250
|
+
]);
|
|
4251
|
+
var WitanReportVerdictSchema = external_exports.union([
|
|
4252
|
+
WitanScoredVerdictSchema,
|
|
4253
|
+
external_exports.literal("insufficient_source")
|
|
4254
|
+
]);
|
|
4255
|
+
function witanVerdictForScore(score) {
|
|
4256
|
+
if (score >= 3.5) return "verified";
|
|
4257
|
+
if (score >= 2.5) return "conditional";
|
|
4258
|
+
if (score >= 1.5) return "at_risk";
|
|
4259
|
+
return "unverified";
|
|
4260
|
+
}
|
|
4261
|
+
var WitanReportCommonSchema = external_exports.object({
|
|
4690
4262
|
productSlug: external_exports.string().regex(/^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/),
|
|
4691
4263
|
productDisplayName: external_exports.string().min(1).max(120),
|
|
4692
4264
|
repo: WitanRepoRefSchema,
|
|
4693
4265
|
generatedAt: external_exports.string().datetime({ offset: true }),
|
|
4694
4266
|
rubricVersion: external_exports.string().min(1).max(120),
|
|
4695
|
-
codeTrustScore: external_exports.number().min(0).max(4),
|
|
4696
|
-
processTrustScore: external_exports.number().min(0).max(4),
|
|
4697
|
-
overallScore: external_exports.number().min(0).max(4),
|
|
4698
4267
|
// Rubric-driven: length matches whatever rubric produced this report, not a fixed enum.
|
|
4699
4268
|
criteria: external_exports.array(WitanCriterionScoreSchema).min(1),
|
|
4700
4269
|
consumedSignals: external_exports.array(WitanConsumedSignalSummarySchema).optional(),
|
|
4270
|
+
archetype: WitanRepoArchetypeSchema.optional()
|
|
4271
|
+
});
|
|
4272
|
+
var WitanScoredReportSchema = WitanReportCommonSchema.extend({
|
|
4273
|
+
verdict: WitanScoredVerdictSchema,
|
|
4274
|
+
codeTrustScore: external_exports.number().min(0).max(4),
|
|
4275
|
+
processTrustScore: external_exports.number().min(0).max(4),
|
|
4276
|
+
overallScore: external_exports.number().min(0).max(4),
|
|
4701
4277
|
// Present only when the rubric has more than two criterion categories — the legacy
|
|
4702
4278
|
// codeTrustScore/processTrustScore pair can't losslessly represent 3+ category buckets.
|
|
4703
4279
|
categoryScores: external_exports.record(external_exports.string(), external_exports.number().min(0).max(4)).optional(),
|
|
4704
|
-
|
|
4705
|
-
// callers that don't classify an archetype (e.g. the trading rubric). insufficientSourceReason
|
|
4706
|
-
// is present only for the non-source archetypes ('docs_only' | 'binary_only' |
|
|
4707
|
-
// 'unrecognised_ecosystem' | 'empty') — its presence is what presentation layers
|
|
4708
|
-
// (badge/terminal/verdict) key off to show an explicit insufficient-source verdict instead of
|
|
4709
|
-
// a confident numeric score.
|
|
4710
|
-
archetype: WitanRepoArchetypeSchema.optional(),
|
|
4711
|
-
insufficientSourceReason: external_exports.string().min(1).max(2e3).optional()
|
|
4280
|
+
insufficientSourceReason: external_exports.never().optional()
|
|
4712
4281
|
}).strict();
|
|
4282
|
+
var WitanAbstainedReportSchema = WitanReportCommonSchema.extend({
|
|
4283
|
+
verdict: external_exports.literal("insufficient_source"),
|
|
4284
|
+
// Null is deliberate and explicit: omission is ambiguous, while zero is a real score.
|
|
4285
|
+
codeTrustScore: external_exports.null(),
|
|
4286
|
+
processTrustScore: external_exports.null(),
|
|
4287
|
+
overallScore: external_exports.null(),
|
|
4288
|
+
categoryScores: external_exports.never().optional(),
|
|
4289
|
+
insufficientSourceReason: external_exports.string().min(1).max(2e3)
|
|
4290
|
+
}).strict();
|
|
4291
|
+
var StrictWitanReportSchema = external_exports.union([WitanScoredReportSchema, WitanAbstainedReportSchema]).superRefine((report, context) => {
|
|
4292
|
+
if (report.verdict !== "insufficient_source" && report.verdict !== witanVerdictForScore(report.overallScore)) {
|
|
4293
|
+
context.addIssue({
|
|
4294
|
+
code: external_exports.ZodIssueCode.custom,
|
|
4295
|
+
path: ["verdict"],
|
|
4296
|
+
message: `verdict does not match overallScore ${report.overallScore}`
|
|
4297
|
+
});
|
|
4298
|
+
}
|
|
4299
|
+
});
|
|
4300
|
+
var WitanReportSchema = external_exports.preprocess((value) => {
|
|
4301
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value) && !("verdict" in value) && !("insufficientSourceReason" in value) && typeof value.overallScore === "number") {
|
|
4302
|
+
return {
|
|
4303
|
+
...value,
|
|
4304
|
+
verdict: witanVerdictForScore(value.overallScore)
|
|
4305
|
+
};
|
|
4306
|
+
}
|
|
4307
|
+
return value;
|
|
4308
|
+
}, StrictWitanReportSchema);
|
|
4713
4309
|
var WITAN_ATTESTATION_STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
|
|
4714
4310
|
var WITAN_ATTESTATION_PREDICATE_TYPE = "https://cejel.dev/attestations/scan/v1";
|
|
4715
4311
|
var WitanAttestationOutcomeSchema = external_exports.discriminatedUnion("status", [
|
|
@@ -4761,7 +4357,459 @@ var WitanAttestationStatementSchema = external_exports.object({
|
|
|
4761
4357
|
}).strict()
|
|
4762
4358
|
}).strict();
|
|
4763
4359
|
|
|
4360
|
+
// src/witan/external-findings.ts
|
|
4361
|
+
var EXTERNAL_FINDINGS_DISPLAY_LIMIT = 10;
|
|
4362
|
+
var SEVERITY_RANK = {
|
|
4363
|
+
critical: 0,
|
|
4364
|
+
warning: 1,
|
|
4365
|
+
info: 2
|
|
4366
|
+
};
|
|
4367
|
+
function formatExternalSourceLabel(source) {
|
|
4368
|
+
return source.startsWith("sarif:") ? source.slice("sarif:".length) : source;
|
|
4369
|
+
}
|
|
4370
|
+
function summarizeExternalSources(consumedSignals) {
|
|
4371
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
4372
|
+
for (const signal of consumedSignals) {
|
|
4373
|
+
const entry = bySource.get(signal.source) ?? { findingCount: 0, dimensions: /* @__PURE__ */ new Set() };
|
|
4374
|
+
entry.findingCount += signal.findingCount;
|
|
4375
|
+
entry.dimensions.add(signal.dimension);
|
|
4376
|
+
bySource.set(signal.source, entry);
|
|
4377
|
+
}
|
|
4378
|
+
return Array.from(bySource.entries()).map(([source, { findingCount, dimensions }]) => ({
|
|
4379
|
+
source,
|
|
4380
|
+
label: formatExternalSourceLabel(source),
|
|
4381
|
+
findingCount,
|
|
4382
|
+
dimensions: Array.from(dimensions).sort()
|
|
4383
|
+
})).sort((a, b) => a.source.localeCompare(b.source));
|
|
4384
|
+
}
|
|
4385
|
+
function formatExternalSourceLine(summary) {
|
|
4386
|
+
const noun = summary.findingCount === 1 ? "finding" : "findings";
|
|
4387
|
+
return `${summary.label}: ${summary.findingCount} ${noun} ingested (folded into ${summary.dimensions.join(", ")})`;
|
|
4388
|
+
}
|
|
4389
|
+
function collectExternalFindings(consumedSignals) {
|
|
4390
|
+
const findings = [];
|
|
4391
|
+
for (const signal of consumedSignals) {
|
|
4392
|
+
for (const finding of signal.findings) {
|
|
4393
|
+
findings.push({
|
|
4394
|
+
source: signal.source,
|
|
4395
|
+
label: formatExternalSourceLabel(signal.source),
|
|
4396
|
+
dimension: signal.dimension,
|
|
4397
|
+
severity: finding.severity,
|
|
4398
|
+
ruleId: finding.ruleId,
|
|
4399
|
+
message: finding.message,
|
|
4400
|
+
...finding.location ? { location: finding.location } : {}
|
|
4401
|
+
});
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
return findings.sort((a, b) => {
|
|
4405
|
+
const bySeverity = SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
|
|
4406
|
+
if (bySeverity !== 0) return bySeverity;
|
|
4407
|
+
const bySource = a.source.localeCompare(b.source);
|
|
4408
|
+
if (bySource !== 0) return bySource;
|
|
4409
|
+
return a.ruleId.localeCompare(b.ruleId);
|
|
4410
|
+
});
|
|
4411
|
+
}
|
|
4412
|
+
|
|
4413
|
+
// src/witan/html.ts
|
|
4414
|
+
function renderWitanHtmlReport(report) {
|
|
4415
|
+
const codeCriteria = criteriaByCategory(report, "code_trust");
|
|
4416
|
+
const processCriteria = criteriaByCategory(report, "process_trust");
|
|
4417
|
+
const verifiedEvidence = report.criteria.flatMap(
|
|
4418
|
+
(criterion) => criterion.status === "verified" ? criterion.evidence.map((evidence) => renderEvidenceListItem(criterion, evidence)) : []
|
|
4419
|
+
);
|
|
4420
|
+
const openItems = report.criteria.flatMap(renderOpenItems);
|
|
4421
|
+
const contributingSources = renderContributingSources(report.consumedSignals ?? []);
|
|
4422
|
+
const externalSourceSummaries = summarizeExternalSources(report.consumedSignals ?? []);
|
|
4423
|
+
const externalFindings = collectExternalFindings(report.consumedSignals ?? []);
|
|
4424
|
+
return `<!doctype html>
|
|
4425
|
+
<html lang="en">
|
|
4426
|
+
<head>
|
|
4427
|
+
<meta charset="utf-8">
|
|
4428
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
4429
|
+
<title>Cejel Trust Certificate - ${escapeHtml(report.productDisplayName)}</title>
|
|
4430
|
+
<style>${CERTIFICATE_CSS}</style>
|
|
4431
|
+
</head>
|
|
4432
|
+
<body>
|
|
4433
|
+
<main class="certificate">
|
|
4434
|
+
<header class="hero">
|
|
4435
|
+
<div class="brand">
|
|
4436
|
+
<span class="brand-mark" aria-hidden="true">C</span>
|
|
4437
|
+
<span class="brand-name">Cejel</span>
|
|
4438
|
+
</div>
|
|
4439
|
+
<div class="hero-grid">
|
|
4440
|
+
<section>
|
|
4441
|
+
<p class="eyebrow">Trust Certificate</p>
|
|
4442
|
+
<h1>${escapeHtml(report.productDisplayName)}</h1>
|
|
4443
|
+
<dl class="meta">
|
|
4444
|
+
<div><dt>Date</dt><dd>${escapeHtml(formatDate(report.generatedAt))}</dd></div>
|
|
4445
|
+
<div><dt>Run</dt><dd>${escapeHtml(renderRepo(report))}</dd></div>
|
|
4446
|
+
<div><dt>Rubric</dt><dd>${escapeHtml(report.rubricVersion)}</dd></div>
|
|
4447
|
+
${contributingSources.length > 0 ? `<div><dt>Sources</dt><dd>Incorporates findings from: ${escapeHtml(contributingSources.join(", "))}<ul class="source-counts">${externalSourceSummaries.map((s) => `<li>${escapeHtml(formatExternalSourceLine(s))}</li>`).join("")}</ul></dd></div>` : ""}
|
|
4448
|
+
</dl>
|
|
4449
|
+
</section>
|
|
4450
|
+
<aside class="score-panel" aria-label="Summary trust score">
|
|
4451
|
+
<div class="score-badge">Summary</div>
|
|
4452
|
+
${report.verdict === "insufficient_source" ? `<div class="verdict">Insufficient source</div>
|
|
4453
|
+
<p class="insufficient-note">${escapeHtml(report.insufficientSourceReason)}</p>` : `<div class="score">${formatScore(report.overallScore)}</div>
|
|
4454
|
+
<div class="score-unit">/ 4.0 overall</div>
|
|
4455
|
+
<div class="verdict">${escapeHtml(renderVerdict(report.overallScore))}</div>
|
|
4456
|
+
<div class="subscores">
|
|
4457
|
+
<span>Code ${formatScore(report.codeTrustScore)}</span>
|
|
4458
|
+
<span>Process ${formatScore(report.processTrustScore)}</span>
|
|
4459
|
+
</div>`}
|
|
4460
|
+
</aside>
|
|
4461
|
+
</div>
|
|
4462
|
+
</header>
|
|
4463
|
+
|
|
4464
|
+
<section class="trust-grid" aria-label="Rubric criteria">
|
|
4465
|
+
${renderCriterionColumn("Code trust", codeCriteria)}
|
|
4466
|
+
${renderCriterionColumn("Process trust", processCriteria)}
|
|
4467
|
+
</section>
|
|
4468
|
+
|
|
4469
|
+
<section class="evidence-grid" aria-label="Evidence and gaps">
|
|
4470
|
+
<article class="evidence-section">
|
|
4471
|
+
<h2>Verified evidence</h2>
|
|
4472
|
+
<ul>${verifiedEvidence.length > 0 ? verifiedEvidence.join("") : "<li>No verified evidence supplied.</li>"}</ul>
|
|
4473
|
+
</article>
|
|
4474
|
+
<article class="evidence-section">
|
|
4475
|
+
<h2>Open / unverified</h2>
|
|
4476
|
+
<ul>${openItems.length > 0 ? openItems.join("") : "<li>No open or unverified criteria supplied.</li>"}</ul>
|
|
4477
|
+
</article>
|
|
4478
|
+
</section>
|
|
4479
|
+
|
|
4480
|
+
${externalFindings.length > 0 ? `<section class="external-findings-section" aria-label="External findings">
|
|
4481
|
+
<article class="evidence-section">
|
|
4482
|
+
<h2>External findings</h2>
|
|
4483
|
+
<p class="external-note">Findings surfaced by ingested external scanners, attributed to the tool and the cejel criterion they were folded into \u2014 distinct from cejel's own findings above.</p>
|
|
4484
|
+
<ul>${renderExternalFindingItems(externalFindings).join("")}</ul>
|
|
4485
|
+
</article>
|
|
4486
|
+
</section>` : ""}
|
|
4487
|
+
</main>
|
|
4488
|
+
</body>
|
|
4489
|
+
</html>
|
|
4490
|
+
`;
|
|
4491
|
+
}
|
|
4492
|
+
function renderContributingSources(signals) {
|
|
4493
|
+
return Array.from(new Set(signals.map((s) => s.source))).sort();
|
|
4494
|
+
}
|
|
4495
|
+
function renderExternalFindingItems(findings) {
|
|
4496
|
+
const shown = findings.slice(0, EXTERNAL_FINDINGS_DISPLAY_LIMIT);
|
|
4497
|
+
const items = shown.map(
|
|
4498
|
+
(f) => `<li><strong>${escapeHtml(f.label)} \u2192 ${escapeHtml(f.dimension)}</strong><span>${escapeHtml(f.severity)}: ${escapeHtml(f.ruleId)} \u2014 ${escapeHtml(f.message)}${f.location ? ` (${escapeHtml(f.location)})` : ""}</span></li>`
|
|
4499
|
+
);
|
|
4500
|
+
const remaining = findings.length - shown.length;
|
|
4501
|
+
if (remaining > 0) {
|
|
4502
|
+
items.push(`<li><em>...and ${remaining} more \u2014 see report.json for the full list.</em></li>`);
|
|
4503
|
+
}
|
|
4504
|
+
return items;
|
|
4505
|
+
}
|
|
4506
|
+
function criteriaByCategory(report, category) {
|
|
4507
|
+
return report.criteria.filter((criterion) => criterion.category === category);
|
|
4508
|
+
}
|
|
4509
|
+
function renderCriterionColumn(title, criteria) {
|
|
4510
|
+
const applicable = criteria.filter((criterion) => criterion.status !== "not_applicable");
|
|
4511
|
+
const notApplicable = criteria.filter((criterion) => criterion.status === "not_applicable");
|
|
4512
|
+
return `<section class="criteria-column">
|
|
4513
|
+
<h2>${escapeHtml(title)}</h2>
|
|
4514
|
+
<div class="criteria-list">
|
|
4515
|
+
${applicable.map(renderCriterionCard).join("")}
|
|
4516
|
+
</div>
|
|
4517
|
+
${notApplicable.length > 0 ? renderNotApplicableGroup(notApplicable) : ""}
|
|
4518
|
+
</section>`;
|
|
4519
|
+
}
|
|
4520
|
+
function renderNotApplicableGroup(criteria) {
|
|
4521
|
+
return `<div class="na-group">
|
|
4522
|
+
<h3 class="na-heading">Not applicable to this repository</h3>
|
|
4523
|
+
<ul class="na-list">
|
|
4524
|
+
${criteria.map(renderNotApplicableItem).join("")}
|
|
4525
|
+
</ul>
|
|
4526
|
+
</div>`;
|
|
4527
|
+
}
|
|
4528
|
+
function renderNotApplicableItem(criterion) {
|
|
4529
|
+
const reason = criterion.notes ?? "Not applicable to this repository.";
|
|
4530
|
+
return `<li><span class="na-id">${escapeHtml(criterion.id)}</span> ${escapeHtml(criterion.title)} \u2014 <span class="na-reason">${escapeHtml(reason)}</span></li>`;
|
|
4531
|
+
}
|
|
4532
|
+
function renderCriterionCard(criterion) {
|
|
4533
|
+
const evidence = [
|
|
4534
|
+
...criterion.evidence.map(renderEvidencePointer),
|
|
4535
|
+
...criterion.findings.map((finding) => renderFindingEvidence(finding))
|
|
4536
|
+
];
|
|
4537
|
+
const metrics = criterion.metrics.map(renderMetric);
|
|
4538
|
+
return `<article class="criterion">
|
|
4539
|
+
<div class="criterion-top">
|
|
4540
|
+
<div>
|
|
4541
|
+
<div class="criterion-id">${escapeHtml(criterion.id)}</div>
|
|
4542
|
+
<h3>${escapeHtml(criterion.title)}</h3>
|
|
4543
|
+
</div>
|
|
4544
|
+
${renderStatusChip(criterion.status)}
|
|
4545
|
+
</div>
|
|
4546
|
+
<div class="criterion-score">${criterion.status === "not_applicable" ? "N/A" : criterion.status === "insufficient_data" ? "No data" : formatScore(criterion.score)}</div>
|
|
4547
|
+
<ul class="criterion-metrics">
|
|
4548
|
+
${metrics.length > 0 ? metrics.map((item) => `<li>${item}</li>`).join("") : "<li>No measured depth metrics supplied.</li>"}
|
|
4549
|
+
</ul>
|
|
4550
|
+
<ul class="criterion-evidence">
|
|
4551
|
+
${evidence.length > 0 ? evidence.map((item) => `<li>${item}</li>`).join("") : "<li>No concrete evidence supplied.</li>"}
|
|
4552
|
+
</ul>
|
|
4553
|
+
</article>`;
|
|
4554
|
+
}
|
|
4555
|
+
function renderMetric(metric2) {
|
|
4556
|
+
const max = metric2.max ? `/${formatMetricValue(metric2.max)}` : "";
|
|
4557
|
+
const unit = metric2.unit ? ` ${escapeHtml(metric2.unit)}` : "";
|
|
4558
|
+
return `<strong>${escapeHtml(metric2.label)}</strong><span>${formatMetricValue(metric2.value)}${max}${unit}</span>`;
|
|
4559
|
+
}
|
|
4560
|
+
function renderStatusChip(status) {
|
|
4561
|
+
return `<span class="status" data-status="${status}">${status}</span>`;
|
|
4562
|
+
}
|
|
4563
|
+
function renderEvidenceListItem(criterion, evidence) {
|
|
4564
|
+
return `<li><strong>${escapeHtml(criterion.id)} - ${escapeHtml(criterion.title)}</strong><span>${renderEvidencePointer(evidence)}</span></li>`;
|
|
4565
|
+
}
|
|
4566
|
+
function renderOpenItems(criterion) {
|
|
4567
|
+
const findingItems = criterion.findings.map(
|
|
4568
|
+
(finding) => `<li><strong>${escapeHtml(criterion.id)} - ${escapeHtml(criterion.title)}</strong><span>${escapeHtml(finding.severity)}: ${escapeHtml(finding.summary)} (${renderEvidencePointer(finding.evidence)})</span></li>`
|
|
4569
|
+
);
|
|
4570
|
+
if ((criterion.status === "unverified" || criterion.status === "insufficient_data") && criterion.evidence.length === 0 && criterion.findings.length === 0) {
|
|
4571
|
+
const message = criterion.status === "insufficient_data" ? "Insufficient data \u2014 no measurable signal; excluded from composite (unmeasured, not inapplicable)." : "No concrete evidence supplied.";
|
|
4572
|
+
return [
|
|
4573
|
+
`<li><strong>${escapeHtml(criterion.id)} - ${escapeHtml(criterion.title)}</strong><span>${escapeHtml(message)}</span></li>`
|
|
4574
|
+
];
|
|
4575
|
+
}
|
|
4576
|
+
if (criterion.status === "verified") return [];
|
|
4577
|
+
return findingItems;
|
|
4578
|
+
}
|
|
4579
|
+
function renderFindingEvidence(finding) {
|
|
4580
|
+
return `${escapeHtml(finding.severity)}: ${escapeHtml(finding.summary)} (${renderEvidencePointer(
|
|
4581
|
+
finding.evidence
|
|
4582
|
+
)})`;
|
|
4583
|
+
}
|
|
4584
|
+
function renderEvidencePointer(evidence) {
|
|
4585
|
+
const target = evidence.url ?? renderPathEvidenceTarget(evidence);
|
|
4586
|
+
const label = escapeHtml(evidence.label);
|
|
4587
|
+
if (evidence.url) {
|
|
4588
|
+
return `<a href="${escapeAttribute(evidence.url)}">${label}</a>`;
|
|
4589
|
+
}
|
|
4590
|
+
return [label, target ? `<code>${escapeHtml(target)}</code>` : void 0].filter(Boolean).join(" ");
|
|
4591
|
+
}
|
|
4592
|
+
function renderPathEvidenceTarget(evidence) {
|
|
4593
|
+
const pathTarget = evidence.path ? `${evidence.path}${evidence.line ? `:${evidence.line}` : ""}` : void 0;
|
|
4594
|
+
const hashTarget = evidence.contentHash ? `sha256:${evidence.contentHash.slice(0, 12)}` : void 0;
|
|
4595
|
+
return [pathTarget, hashTarget].filter(Boolean).join(" \xB7 ");
|
|
4596
|
+
}
|
|
4597
|
+
function renderRepo(report) {
|
|
4598
|
+
const repo = report.repo.path ?? report.repo.url ?? "unknown";
|
|
4599
|
+
return report.repo.headSha ? `${repo} @ ${report.repo.headSha}` : repo;
|
|
4600
|
+
}
|
|
4601
|
+
function renderVerdict(score) {
|
|
4602
|
+
return renderMachineVerdict(witanVerdictForScore(score));
|
|
4603
|
+
}
|
|
4604
|
+
function renderReportVerdict(report) {
|
|
4605
|
+
return renderMachineVerdict(report.verdict);
|
|
4606
|
+
}
|
|
4607
|
+
function renderMachineVerdict(verdict) {
|
|
4608
|
+
switch (verdict) {
|
|
4609
|
+
case "verified":
|
|
4610
|
+
return "Verified";
|
|
4611
|
+
case "conditional":
|
|
4612
|
+
return "Conditional";
|
|
4613
|
+
case "at_risk":
|
|
4614
|
+
return "At risk";
|
|
4615
|
+
case "unverified":
|
|
4616
|
+
return "Unverified";
|
|
4617
|
+
case "insufficient_source":
|
|
4618
|
+
return "Insufficient source";
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
function formatScore(score) {
|
|
4622
|
+
return score.toFixed(1);
|
|
4623
|
+
}
|
|
4624
|
+
function formatMetricValue(value) {
|
|
4625
|
+
return Number.isInteger(value) ? value.toString() : value.toFixed(2);
|
|
4626
|
+
}
|
|
4627
|
+
function formatDate(value) {
|
|
4628
|
+
return value.slice(0, 10);
|
|
4629
|
+
}
|
|
4630
|
+
function escapeHtml(value) {
|
|
4631
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
4632
|
+
}
|
|
4633
|
+
function escapeAttribute(value) {
|
|
4634
|
+
return escapeHtml(value);
|
|
4635
|
+
}
|
|
4636
|
+
var CERTIFICATE_CSS = `
|
|
4637
|
+
:root {
|
|
4638
|
+
color-scheme: dark;
|
|
4639
|
+
--steel: #16212a;
|
|
4640
|
+
--surface: #1d2b35;
|
|
4641
|
+
--surface-2: #273946;
|
|
4642
|
+
--line: rgba(238, 244, 247, .13);
|
|
4643
|
+
--line-strong: rgba(238, 244, 247, .22);
|
|
4644
|
+
--text: #eef4f7;
|
|
4645
|
+
--muted: #b7c5cc;
|
|
4646
|
+
--faint: #8799a4;
|
|
4647
|
+
--teal: #2fc8a6;
|
|
4648
|
+
--teal-weak: rgba(47, 200, 166, .14);
|
|
4649
|
+
--periwinkle: #93a6da;
|
|
4650
|
+
--periwinkle-weak: rgba(147, 166, 218, .16);
|
|
4651
|
+
--warn: #e7bf72;
|
|
4652
|
+
--warn-weak: rgba(231, 191, 114, .16);
|
|
4653
|
+
--danger: #f09a8f;
|
|
4654
|
+
--danger-weak: rgba(240, 154, 143, .14);
|
|
4655
|
+
--quiet: rgba(238, 244, 247, .08);
|
|
4656
|
+
--serif: "Instrument Serif", Georgia, "Times New Roman", serif;
|
|
4657
|
+
--sans: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
4658
|
+
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
4659
|
+
}
|
|
4660
|
+
* { box-sizing: border-box; }
|
|
4661
|
+
body {
|
|
4662
|
+
margin: 0;
|
|
4663
|
+
background: var(--steel);
|
|
4664
|
+
color: var(--text);
|
|
4665
|
+
font-family: var(--sans);
|
|
4666
|
+
line-height: 1.55;
|
|
4667
|
+
-webkit-font-smoothing: antialiased;
|
|
4668
|
+
}
|
|
4669
|
+
.certificate { max-width: 1120px; margin: 0 auto; padding: 44px 32px 64px; }
|
|
4670
|
+
.hero { border-bottom: 1px solid var(--line); padding-bottom: 38px; }
|
|
4671
|
+
.brand { display: flex; align-items: center; gap: 12px; margin-bottom: 44px; }
|
|
4672
|
+
.brand-mark {
|
|
4673
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
4674
|
+
width: 34px; height: 34px; border-radius: 8px;
|
|
4675
|
+
background: var(--teal); color: #06231d; font-family: var(--serif); font-size: 24px;
|
|
4676
|
+
}
|
|
4677
|
+
.brand-name { font-family: var(--serif); font-size: 24px; }
|
|
4678
|
+
.hero-grid { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 36px; align-items: end; }
|
|
4679
|
+
.eyebrow {
|
|
4680
|
+
margin: 0 0 14px; color: var(--teal); font-family: var(--mono);
|
|
4681
|
+
font-size: 11px; letter-spacing: .18em; text-transform: uppercase;
|
|
4682
|
+
}
|
|
4683
|
+
h1, h2, h3, p { margin-top: 0; }
|
|
4684
|
+
h1 { margin-bottom: 24px; font-family: var(--serif); font-size: 64px; line-height: 1; font-weight: 400; letter-spacing: 0; }
|
|
4685
|
+
.meta { display: grid; gap: 10px; margin: 0; max-width: 780px; }
|
|
4686
|
+
.meta div { display: grid; grid-template-columns: 86px minmax(0, 1fr); gap: 16px; }
|
|
4687
|
+
dt { color: var(--faint); font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: .12em; }
|
|
4688
|
+
dd { margin: 0; color: var(--muted); overflow-wrap: anywhere; }
|
|
4689
|
+
.score-panel { border: 1px solid var(--line-strong); border-radius: 8px; padding: 24px; background: var(--surface); }
|
|
4690
|
+
.score-badge {
|
|
4691
|
+
display: inline-flex; margin-bottom: 14px; border: 1px solid rgba(147, 166, 218, .38);
|
|
4692
|
+
border-radius: 999px; padding: 5px 10px; color: var(--periwinkle);
|
|
4693
|
+
background: var(--periwinkle-weak); font-family: var(--mono); font-size: 12px;
|
|
4694
|
+
}
|
|
4695
|
+
.score { display: inline; font-family: var(--mono); font-size: 56px; line-height: 1; color: var(--teal); }
|
|
4696
|
+
.score-unit { display: inline; margin-left: 6px; color: var(--faint); font-family: var(--mono); }
|
|
4697
|
+
.verdict { margin-top: 12px; font-family: var(--serif); font-size: 30px; color: var(--periwinkle); }
|
|
4698
|
+
.insufficient-note { margin-top: 10px; color: var(--muted); font-size: 13px; line-height: 1.5; }
|
|
4699
|
+
.subscores { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 18px; color: var(--muted); font-family: var(--mono); font-size: 12px; }
|
|
4700
|
+
.source-counts { margin: 8px 0 0; padding-left: 18px; font-family: var(--mono); font-size: 12px; color: var(--muted); }
|
|
4701
|
+
.trust-grid, .evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; margin-top: 28px; }
|
|
4702
|
+
.external-findings-section { margin-top: 18px; }
|
|
4703
|
+
.external-note { margin: 0 0 14px; color: var(--muted); font-size: 13px; }
|
|
4704
|
+
.criteria-column, .evidence-section { border: 1px solid var(--line); border-radius: 8px; background: var(--surface); padding: 22px; }
|
|
4705
|
+
h2 { font-family: var(--serif); font-size: 30px; font-weight: 400; letter-spacing: 0; margin-bottom: 18px; }
|
|
4706
|
+
.criteria-list { display: grid; gap: 12px; }
|
|
4707
|
+
.na-group { margin-top: 16px; padding-top: 14px; border-top: 1px dashed var(--line-strong); }
|
|
4708
|
+
.na-heading { font-family: var(--sans); font-size: 12px; font-weight: 650; letter-spacing: .04em; text-transform: uppercase; color: var(--faint); margin: 0 0 10px; }
|
|
4709
|
+
.na-list { display: grid; gap: 8px; font-size: 13px; }
|
|
4710
|
+
.na-list li { margin: 0; color: var(--muted); }
|
|
4711
|
+
.na-id { color: var(--faint); font-family: var(--mono); font-size: 11px; margin-right: 6px; }
|
|
4712
|
+
.na-reason { color: var(--muted); }
|
|
4713
|
+
.criterion { border: 1px solid var(--line); border-radius: 8px; padding: 16px; background: var(--surface-2); }
|
|
4714
|
+
.criterion-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; }
|
|
4715
|
+
.criterion-id { color: var(--periwinkle); font-family: var(--mono); font-size: 12px; margin-bottom: 4px; }
|
|
4716
|
+
h3 { font-size: 15px; line-height: 1.35; font-weight: 600; margin-bottom: 0; }
|
|
4717
|
+
.criterion-score { margin-top: 12px; color: var(--text); font-family: var(--mono); font-size: 24px; }
|
|
4718
|
+
.criterion-metrics { display: grid; gap: 7px; margin-top: 10px; padding: 0; list-style: none; font-size: 12px; }
|
|
4719
|
+
.criterion-metrics li { display: flex; justify-content: space-between; gap: 12px; border-bottom: 1px solid rgba(238, 244, 247, .08); padding-bottom: 6px; }
|
|
4720
|
+
.criterion-metrics strong { color: var(--text); font-weight: 650; }
|
|
4721
|
+
.criterion-metrics span { white-space: nowrap; color: var(--muted); }
|
|
4722
|
+
.status { flex: none; border-radius: 999px; padding: 4px 9px; font-family: var(--mono); font-size: 11px; border: 1px solid var(--line-strong); }
|
|
4723
|
+
.status[data-status="verified"] { color: var(--teal); background: var(--teal-weak); border-color: rgba(47, 200, 166, .42); }
|
|
4724
|
+
.status[data-status="info"] { color: var(--periwinkle); background: var(--periwinkle-weak); border-color: rgba(147, 166, 218, .4); }
|
|
4725
|
+
.status[data-status="warning"] { color: var(--warn); background: var(--warn-weak); border-color: rgba(231, 191, 114, .44); }
|
|
4726
|
+
.status[data-status="critical"] { color: var(--danger); background: var(--danger-weak); border-color: rgba(240, 154, 143, .46); }
|
|
4727
|
+
.status[data-status="unverified"] { color: var(--muted); background: var(--quiet); }
|
|
4728
|
+
.status[data-status="insufficient_data"] { color: var(--muted); background: var(--quiet); border-style: dashed; }
|
|
4729
|
+
ul { margin: 0; padding-left: 18px; }
|
|
4730
|
+
li { margin: 8px 0; color: var(--muted); }
|
|
4731
|
+
.criterion-evidence { margin-top: 12px; font-size: 13px; }
|
|
4732
|
+
.evidence-section li strong { display: block; color: var(--text); font-size: 14px; }
|
|
4733
|
+
.evidence-section li span { display: block; margin-top: 2px; font-size: 13px; }
|
|
4734
|
+
code { color: var(--periwinkle); font-family: var(--mono); font-size: .92em; overflow-wrap: anywhere; }
|
|
4735
|
+
a { color: var(--teal); text-decoration: none; }
|
|
4736
|
+
a:hover { text-decoration: underline; text-underline-offset: 2px; }
|
|
4737
|
+
@media (max-width: 840px) {
|
|
4738
|
+
.certificate { padding: 28px 18px 48px; }
|
|
4739
|
+
.hero-grid, .trust-grid, .evidence-grid { grid-template-columns: 1fr; }
|
|
4740
|
+
h1 { font-size: 46px; }
|
|
4741
|
+
}
|
|
4742
|
+
`;
|
|
4743
|
+
|
|
4744
|
+
// src/witan/badge.ts
|
|
4745
|
+
var VERDICT_COLOR = {
|
|
4746
|
+
Verified: "brightgreen",
|
|
4747
|
+
Conditional: "yellow",
|
|
4748
|
+
"At risk": "orange",
|
|
4749
|
+
Unverified: "red",
|
|
4750
|
+
"Insufficient source": "lightgrey"
|
|
4751
|
+
};
|
|
4752
|
+
var VERDICT_SVG_FILL = {
|
|
4753
|
+
Verified: "#2fc86a",
|
|
4754
|
+
Conditional: "#dfb92f",
|
|
4755
|
+
"At risk": "#e08a3c",
|
|
4756
|
+
Unverified: "#d64545",
|
|
4757
|
+
"Insufficient source": "#8c8c8c"
|
|
4758
|
+
};
|
|
4759
|
+
function badgeMessage(report, verdict) {
|
|
4760
|
+
if (report.verdict === "insufficient_source") return "unrated: no source";
|
|
4761
|
+
return `${formatScore2(report.overallScore)}/4.0 ${verdict.toLowerCase()}`;
|
|
4762
|
+
}
|
|
4763
|
+
function renderWitanBadgeEndpoint(report) {
|
|
4764
|
+
const verdict = renderReportVerdict(report);
|
|
4765
|
+
return {
|
|
4766
|
+
schemaVersion: 1,
|
|
4767
|
+
label: "cejel trust",
|
|
4768
|
+
message: badgeMessage(report, verdict),
|
|
4769
|
+
color: VERDICT_COLOR[verdict] ?? "lightgrey"
|
|
4770
|
+
};
|
|
4771
|
+
}
|
|
4772
|
+
function renderWitanBadgeSvg(report) {
|
|
4773
|
+
const verdict = renderReportVerdict(report);
|
|
4774
|
+
const label = "cejel trust";
|
|
4775
|
+
const message = badgeMessage(report, verdict);
|
|
4776
|
+
const fill = VERDICT_SVG_FILL[verdict] ?? "#8c8c8c";
|
|
4777
|
+
const charWidth = 6.5;
|
|
4778
|
+
const padding = 10;
|
|
4779
|
+
const labelWidth = Math.round(label.length * charWidth + padding);
|
|
4780
|
+
const messageWidth = Math.round(message.length * charWidth + padding);
|
|
4781
|
+
const totalWidth = labelWidth + messageWidth;
|
|
4782
|
+
const height = 20;
|
|
4783
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="${height}" role="img" aria-label="${escapeXml(label)}: ${escapeXml(message)}">
|
|
4784
|
+
<title>${escapeXml(label)}: ${escapeXml(message)}</title>
|
|
4785
|
+
<linearGradient id="witan-badge-smooth" x2="0" y2="100%">
|
|
4786
|
+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
4787
|
+
<stop offset="1" stop-opacity=".1"/>
|
|
4788
|
+
</linearGradient>
|
|
4789
|
+
<clipPath id="witan-badge-round">
|
|
4790
|
+
<rect width="${totalWidth}" height="${height}" rx="3" fill="#fff"/>
|
|
4791
|
+
</clipPath>
|
|
4792
|
+
<g clip-path="url(#witan-badge-round)">
|
|
4793
|
+
<rect width="${labelWidth}" height="${height}" fill="#555"/>
|
|
4794
|
+
<rect x="${labelWidth}" width="${messageWidth}" height="${height}" fill="${fill}"/>
|
|
4795
|
+
<rect width="${totalWidth}" height="${height}" fill="url(#witan-badge-smooth)"/>
|
|
4796
|
+
</g>
|
|
4797
|
+
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
|
|
4798
|
+
<text x="${labelWidth / 2}" y="14">${escapeXml(label)}</text>
|
|
4799
|
+
<text x="${labelWidth + messageWidth / 2}" y="14">${escapeXml(message)}</text>
|
|
4800
|
+
</g>
|
|
4801
|
+
</svg>
|
|
4802
|
+
`;
|
|
4803
|
+
}
|
|
4804
|
+
function formatScore2(score) {
|
|
4805
|
+
return score.toFixed(1);
|
|
4806
|
+
}
|
|
4807
|
+
function escapeXml(value) {
|
|
4808
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4809
|
+
}
|
|
4810
|
+
|
|
4764
4811
|
// src/witan/attestation.ts
|
|
4812
|
+
import { createHash } from "crypto";
|
|
4765
4813
|
var ATTESTATION_LIMITATIONS = [
|
|
4766
4814
|
"Cejel measures observable repository signals; it is not a security or compliance guarantee.",
|
|
4767
4815
|
"This statement is self-generated and unsigned until an external signer binds their identity to it."
|
|
@@ -4774,7 +4822,7 @@ function hashWitanReport(report) {
|
|
|
4774
4822
|
}
|
|
4775
4823
|
function createWitanAttestation(report, options) {
|
|
4776
4824
|
const reportSha256 = hashWitanReport(report);
|
|
4777
|
-
const outcome = report.
|
|
4825
|
+
const outcome = report.verdict === "insufficient_source" ? {
|
|
4778
4826
|
status: "abstained",
|
|
4779
4827
|
reason: report.insufficientSourceReason
|
|
4780
4828
|
} : {
|
|
@@ -7755,20 +7803,22 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
|
|
|
7755
7803
|
const overallScore = roundScore(
|
|
7756
7804
|
categoryOrder.reduce((sum, category) => sum + (categoryScoreMap[category] ?? 0), 0) / Math.max(categoryOrder.length, 1)
|
|
7757
7805
|
);
|
|
7806
|
+
const abstained = parsedInput.insufficientSourceReason != null;
|
|
7758
7807
|
return WitanReportSchema.parse({
|
|
7759
7808
|
productSlug: parsedInput.productSlug,
|
|
7760
7809
|
productDisplayName: parsedInput.productDisplayName,
|
|
7761
7810
|
repo: parsedInput.repo,
|
|
7762
7811
|
generatedAt: parsedInput.generatedAt,
|
|
7763
7812
|
rubricVersion: parsedInput.rubricVersion,
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7813
|
+
verdict: abstained ? "insufficient_source" : witanVerdictForScore(overallScore),
|
|
7814
|
+
codeTrustScore: abstained ? null : codeTrustScore,
|
|
7815
|
+
processTrustScore: abstained ? null : processTrustScore,
|
|
7816
|
+
overallScore: abstained ? null : overallScore,
|
|
7767
7817
|
criteria,
|
|
7768
7818
|
...consumedSignals.length > 0 ? { consumedSignals } : {},
|
|
7769
7819
|
// Only surface the full per-category map for rubrics with more than two buckets —
|
|
7770
7820
|
// the default two-category rubric is fully represented by codeTrustScore/processTrustScore.
|
|
7771
|
-
|
|
7821
|
+
...!abstained && categoryOrder.length > 2 ? { categoryScores: categoryScoreMap } : {},
|
|
7772
7822
|
// Pass through repo-archetype metadata verbatim — createWitanReport does not compute or
|
|
7773
7823
|
// alter it (see classifyRepoArchetype in repo-signals.ts); per-criterion scoring above is
|
|
7774
7824
|
// unaffected by archetype, only presentation layers (badge/terminal/verdict) key off it.
|
|
@@ -8084,21 +8134,33 @@ function buildWitanCliSummary(report) {
|
|
|
8084
8134
|
).sort();
|
|
8085
8135
|
const externalSources = summarizeExternalSources(report.consumedSignals ?? []);
|
|
8086
8136
|
const allExternalFindings = collectExternalFindings(report.consumedSignals ?? []);
|
|
8087
|
-
|
|
8137
|
+
const common = {
|
|
8088
8138
|
productSlug: report.productSlug,
|
|
8089
8139
|
productDisplayName: report.productDisplayName,
|
|
8090
8140
|
generatedAt: report.generatedAt,
|
|
8091
|
-
overallScore: report.overallScore,
|
|
8092
|
-
codeTrustScore: report.codeTrustScore,
|
|
8093
|
-
processTrustScore: report.processTrustScore,
|
|
8094
|
-
verdict: renderReportVerdict(report),
|
|
8095
8141
|
findingCount: allFindings.length,
|
|
8096
8142
|
topFindings: sorted.slice(0, TOP_FINDINGS_LIMIT),
|
|
8097
8143
|
contributingSources,
|
|
8098
8144
|
externalSources,
|
|
8099
8145
|
externalFindingCount: allExternalFindings.length,
|
|
8100
|
-
topExternalFindings: allExternalFindings.slice(0, EXTERNAL_FINDINGS_DISPLAY_LIMIT)
|
|
8101
|
-
|
|
8146
|
+
topExternalFindings: allExternalFindings.slice(0, EXTERNAL_FINDINGS_DISPLAY_LIMIT)
|
|
8147
|
+
};
|
|
8148
|
+
if (report.verdict === "insufficient_source") {
|
|
8149
|
+
return {
|
|
8150
|
+
...common,
|
|
8151
|
+
overallScore: null,
|
|
8152
|
+
codeTrustScore: null,
|
|
8153
|
+
processTrustScore: null,
|
|
8154
|
+
verdict: "Insufficient source",
|
|
8155
|
+
insufficientSourceReason: report.insufficientSourceReason
|
|
8156
|
+
};
|
|
8157
|
+
}
|
|
8158
|
+
return {
|
|
8159
|
+
...common,
|
|
8160
|
+
overallScore: report.overallScore,
|
|
8161
|
+
codeTrustScore: report.codeTrustScore,
|
|
8162
|
+
processTrustScore: report.processTrustScore,
|
|
8163
|
+
verdict: renderReportVerdict(report)
|
|
8102
8164
|
};
|
|
8103
8165
|
}
|
|
8104
8166
|
|