@cejel/cejel 0.1.4 → 0.1.5
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 +31 -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 +4 -2
package/README.md
CHANGED
|
@@ -20,6 +20,10 @@ certificate + badge over all of them. See "Aggregate your scanners" below.
|
|
|
20
20
|
> monorepo — the ten-product studio it was built inside — which it currently scores
|
|
21
21
|
> 3.1/4.0 ("Conditional"). We score ourselves before asking you to score yourself.
|
|
22
22
|
|
|
23
|
+
The reusable Barg Labs listing mark is kept at [`brand/barg-icon.png`](./brand/barg-icon.png)
|
|
24
|
+
for directories and registries that accept a custom image. GitHub Actions Marketplace uses
|
|
25
|
+
GitHub's predefined `action.yml` branding icons instead.
|
|
26
|
+
|
|
23
27
|
## Install
|
|
24
28
|
|
|
25
29
|
No account, no key, no signup.
|
|
@@ -27,7 +31,15 @@ No account, no key, no signup.
|
|
|
27
31
|
**Single-file binary.** One file. No Node, no npm, no `node_modules`, nothing installed.
|
|
28
32
|
|
|
29
33
|
```bash
|
|
30
|
-
|
|
34
|
+
asset="cejel-$(uname -s)-$(uname -m)"
|
|
35
|
+
curl -fsSL "https://github.com/BargLabs/cejel/releases/latest/download/$asset" -o "$asset"
|
|
36
|
+
curl -fsSL https://github.com/BargLabs/cejel/releases/latest/download/SHA256SUMS -o SHA256SUMS
|
|
37
|
+
if command -v sha256sum >/dev/null; then
|
|
38
|
+
grep " $asset$" SHA256SUMS | sha256sum -c -
|
|
39
|
+
else
|
|
40
|
+
grep " $asset$" SHA256SUMS | shasum -a 256 -c -
|
|
41
|
+
fi
|
|
42
|
+
mv "$asset" cejel
|
|
31
43
|
chmod +x cejel
|
|
32
44
|
./cejel .
|
|
33
45
|
```
|
|
@@ -41,15 +53,13 @@ product, and you can falsify it in ten seconds:
|
|
|
41
53
|
docker run --rm --network=none -v "$PWD:/w" -w /w -v "$PWD/cejel:/cejel:ro" debian:stable-slim /cejel .
|
|
42
54
|
```
|
|
43
55
|
|
|
44
|
-
**npm
|
|
56
|
+
**npm.**
|
|
45
57
|
|
|
46
58
|
```bash
|
|
47
|
-
npx
|
|
59
|
+
npx @cejel/cejel .
|
|
48
60
|
```
|
|
49
61
|
|
|
50
|
-
The package is
|
|
51
|
-
registry review. The installed executable remains `cejel`. For a global install, run
|
|
52
|
-
`npm install --global @cejel/cejel`, then invoke `cejel .`.
|
|
62
|
+
The npm package is scoped as `@cejel/cejel`; its executable remains the short command `cejel`.
|
|
53
63
|
|
|
54
64
|
**GitHub Action** — score every PR and publish the badge:
|
|
55
65
|
|
|
@@ -67,9 +77,9 @@ pnpm install && pnpm build
|
|
|
67
77
|
node dist/index.js .
|
|
68
78
|
```
|
|
69
79
|
|
|
70
|
-
Released binaries: `cejel-Darwin-arm64`, `cejel-
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
Released binaries: `cejel-Darwin-arm64`, `cejel-Darwin-x86_64`, `cejel-Linux-aarch64`, and
|
|
81
|
+
`cejel-Linux-x86_64`. The release also carries `SHA256SUMS`; each binary is executed against
|
|
82
|
+
the source build and with networking denied before attachment.
|
|
73
83
|
|
|
74
84
|
## Leaderboard
|
|
75
85
|
|
|
@@ -78,7 +88,7 @@ corpus scored and published in the open — elite OSS projects, Cejel itself, an
|
|
|
78
88
|
studio monorepo Cejel was built inside — with a per-repository evidence report for every
|
|
79
89
|
row under [`leaderboard/reports/`](./leaderboard/reports/). The board is also hosted at
|
|
80
90
|
[cejel.dev](https://cejel.dev). Every score on this board is produced by the same sealed
|
|
81
|
-
public scorer used by `npx cejel .` — check out the source commit printed in the
|
|
91
|
+
public scorer used by `npx @cejel/cejel .` — check out the source commit printed in the
|
|
82
92
|
report, run the tool, and you will get this number. A required guard does exactly that for
|
|
83
93
|
every corpus row and compares score, verdict, measured coverage, and evidence. No private
|
|
84
94
|
domain collector contributes to any published score, ours included; nobody is exempt.
|
|
@@ -123,7 +133,8 @@ better-evidenced rows.
|
|
|
123
133
|
Scores the current directory with sensible defaults: no flags, no signup, fully offline.
|
|
124
134
|
Prints a concise terminal certificate and writes to `.cejel/`:
|
|
125
135
|
|
|
126
|
-
- `report.json` — the full structured report
|
|
136
|
+
- `report.json` — the full structured report; scored runs carry numeric headline scores and a
|
|
137
|
+
score-band `verdict`, while abstained runs carry `null` scores and `verdict: "insufficient_source"`
|
|
127
138
|
- `attestation.json` — an unsigned in-toto statement binding the report digest, repository
|
|
128
139
|
revision, rubric, and scored-or-abstained outcome; ready for an external signer
|
|
129
140
|
- `certificate.html` — a self-contained HTML certificate (no external assets)
|
|
@@ -141,8 +152,9 @@ or provenance system signs it, `assurance.status` is `unsigned` and `issuer` is
|
|
|
141
152
|
- `<path>` — repo to score (default: current directory)
|
|
142
153
|
- `--out <dir>` — where to write report/certificate/badge files (default: `.cejel`;
|
|
143
154
|
`--out-dir` remains available as a compatibility alias)
|
|
144
|
-
- `--min-score <n>` — exit non-zero if the overall score is below `n` (0–4)
|
|
145
|
-
GitHub Action's optional
|
|
155
|
+
- `--min-score <n>` — exit non-zero if the overall score is below `n` (0–4), or if Cejel
|
|
156
|
+
abstains and therefore cannot evaluate the threshold; used by the GitHub Action's optional
|
|
157
|
+
threshold gate
|
|
146
158
|
- `--ingest <file|glob>` — fold another scanner's output into the score (repeatable). Accepts
|
|
147
159
|
SARIF, OpenSSF Scorecard JSON, or the generic Cejel external-signal shape — format is
|
|
148
160
|
auto-detected. See "Aggregate your scanners" below.
|
|
@@ -168,9 +180,10 @@ honestly rather than guessing:
|
|
|
168
180
|
- **Not yet recognised** — other ecosystems, including COBOL, Fortran, and MATLAB. Cejel does
|
|
169
181
|
not score these as "source" at all: a repository with zero
|
|
170
182
|
recognised-extension files gets the `unrecognised_ecosystem` archetype, an explicit
|
|
171
|
-
`insufficient_data` status,
|
|
172
|
-
|
|
173
|
-
|
|
183
|
+
`insufficient_data` criterion status, `null` headline scores, and an explicit machine verdict
|
|
184
|
+
of `insufficient_source` — never a numeric score-band verdict, and never the word
|
|
185
|
+
"Unverified" for the sole reason that cejel cannot read the language. The certificate states
|
|
186
|
+
plainly which of the 11 dimensions were and were not measured.
|
|
174
187
|
|
|
175
188
|
This list will grow. It will never be "any codebase" — that claim is a promise the parser
|
|
176
189
|
cannot keep, and an honest support matrix is worth more than a marketing line the code
|
|
@@ -278,7 +291,7 @@ Add it to an MCP client config:
|
|
|
278
291
|
"mcpServers": {
|
|
279
292
|
"cejel": {
|
|
280
293
|
"command": "npx",
|
|
281
|
-
"args": ["-y", "--package
|
|
294
|
+
"args": ["-y", "--package=@cejel/cejel", "cejel-mcp"]
|
|
282
295
|
}
|
|
283
296
|
}
|
|
284
297
|
}
|
|
@@ -299,7 +312,7 @@ and the server writes no files.
|
|
|
299
312
|
## What "offline" means here
|
|
300
313
|
|
|
301
314
|
Scoring a repo — `cejel .` itself, and the Action's scoring step — makes zero network
|
|
302
|
-
calls: no telemetry, no signup, no model call. Fetching the
|
|
315
|
+
calls: no telemetry, no signup, no model call. Fetching the `@cejel/cejel` package the
|
|
303
316
|
first time (like any npm-distributed CLI, including this Action's own dependency install)
|
|
304
317
|
does need network; that's a one-time install cost, not part of the scoring guarantee.
|
|
305
318
|
|
|
@@ -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
|
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
renderWitanHtmlReport,
|
|
8
8
|
runCejelScan,
|
|
9
9
|
serializeWitanReport
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-A7NZKJ5H.js";
|
|
11
11
|
|
|
12
12
|
// src/index.ts
|
|
13
13
|
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "fs";
|
|
@@ -16,11 +16,15 @@ import { pathToFileURL } from "url";
|
|
|
16
16
|
|
|
17
17
|
// src/terminal.ts
|
|
18
18
|
function renderTerminalCertificate(summary) {
|
|
19
|
-
const
|
|
19
|
+
const abstained = summary.verdict === "Insufficient source";
|
|
20
|
+
if (!abstained && (summary.overallScore === null || summary.codeTrustScore === null || summary.processTrustScore === null)) {
|
|
21
|
+
throw new Error("A scored Cejel summary must carry numeric headline scores.");
|
|
22
|
+
}
|
|
23
|
+
const lines = abstained ? [
|
|
20
24
|
`Cejel Trust Certificate \u2014 ${summary.productDisplayName}`,
|
|
21
25
|
"",
|
|
22
26
|
"Insufficient source to certify.",
|
|
23
|
-
` ${summary.insufficientSourceReason}`,
|
|
27
|
+
` ${summary.insufficientSourceReason ?? "No ratable source was found."}`,
|
|
24
28
|
""
|
|
25
29
|
] : [
|
|
26
30
|
`Cejel Trust Certificate \u2014 ${summary.productDisplayName}`,
|
|
@@ -37,7 +41,7 @@ function renderTerminalCertificate(summary) {
|
|
|
37
41
|
}
|
|
38
42
|
lines.push("");
|
|
39
43
|
}
|
|
40
|
-
if (
|
|
44
|
+
if (abstained) {
|
|
41
45
|
lines.push("See report.json / certificate.html for the full per-criterion detail.");
|
|
42
46
|
} else if (summary.topFindings.length === 0) {
|
|
43
47
|
lines.push("No evidence-backed findings.");
|
|
@@ -51,7 +55,7 @@ function renderTerminalCertificate(summary) {
|
|
|
51
55
|
lines.push(` ...and ${remaining} more \u2014 see the written report for the full list.`);
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
|
-
if (!
|
|
58
|
+
if (!abstained && summary.externalFindingCount > 0) {
|
|
55
59
|
lines.push(
|
|
56
60
|
"",
|
|
57
61
|
`External findings (${summary.externalFindingCount} total, attributed to tool + criterion):`
|
|
@@ -70,6 +74,7 @@ function renderTerminalCertificate(summary) {
|
|
|
70
74
|
`;
|
|
71
75
|
}
|
|
72
76
|
function formatScore(score) {
|
|
77
|
+
if (score === null) throw new Error("A scored Cejel summary must carry numeric scores.");
|
|
73
78
|
return score.toFixed(1);
|
|
74
79
|
}
|
|
75
80
|
|
|
@@ -117,6 +122,9 @@ var CLI_FLAG_KIND_BY_TOKEN = new Map(
|
|
|
117
122
|
CLI_FLAG_SPECS.flatMap((spec) => spec.tokens.map((token) => [token, spec.kind]))
|
|
118
123
|
);
|
|
119
124
|
function cliVersion() {
|
|
125
|
+
if (typeof __CEJEL_SEA_VERSION__ === "string" && __CEJEL_SEA_VERSION__.length > 0) {
|
|
126
|
+
return __CEJEL_SEA_VERSION__;
|
|
127
|
+
}
|
|
120
128
|
const manifest = JSON.parse(
|
|
121
129
|
readFileSync(new URL("../package.json", import.meta.url), "utf8")
|
|
122
130
|
);
|
|
@@ -191,12 +199,21 @@ Wrote:
|
|
|
191
199
|
`
|
|
192
200
|
);
|
|
193
201
|
}
|
|
194
|
-
if (options.minScore != null
|
|
195
|
-
|
|
196
|
-
|
|
202
|
+
if (options.minScore != null) {
|
|
203
|
+
if (report.verdict === "insufficient_source") {
|
|
204
|
+
process.stderr.write(
|
|
205
|
+
`Cejel: cannot evaluate the required minimum ${options.minScore.toFixed(1)}/4.0 because this repository has insufficient source.
|
|
197
206
|
`
|
|
198
|
-
|
|
199
|
-
|
|
207
|
+
);
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
if (report.overallScore < options.minScore) {
|
|
211
|
+
process.stderr.write(
|
|
212
|
+
`Cejel: overall score ${report.overallScore.toFixed(1)}/4.0 is below the required minimum ${options.minScore.toFixed(1)}/4.0
|
|
213
|
+
`
|
|
214
|
+
);
|
|
215
|
+
return 1;
|
|
216
|
+
}
|
|
200
217
|
}
|
|
201
218
|
return 0;
|
|
202
219
|
}
|
package/dist/mcp/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
renderWitanBadgeSvg,
|
|
11
11
|
renderWitanHtmlReport,
|
|
12
12
|
runCejelScan
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-A7NZKJ5H.js";
|
|
14
14
|
|
|
15
15
|
// node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/codegen/code.js
|
|
16
16
|
var require_code = __commonJS({
|
|
@@ -17061,7 +17061,7 @@ function createCejelMcpServer(identity) {
|
|
|
17061
17061
|
});
|
|
17062
17062
|
server.tool(
|
|
17063
17063
|
"scan",
|
|
17064
|
-
`Score a repository's engineering-trust signals (tests, secrets, isolation, claim-vs-reality, CI discipline) with the ${identity.packageName} trust-certificate scan \u2014 the exact same offline, deterministic scoring as running the ${identity.packageName} CLI on the path. Returns the trust cert as JSON: overall + code/process sub-scores (0-4), verdict
|
|
17064
|
+
`Score a repository's engineering-trust signals (tests, secrets, isolation, claim-vs-reality, CI discipline) with the ${identity.packageName} trust-certificate scan \u2014 the exact same offline, deterministic scoring as running the ${identity.packageName} CLI on the path. Returns the trust cert as JSON: overall + code/process sub-scores (0-4) for a scored repository, or null scores with an insufficient-source verdict when Cejel cannot read it; plus the verdict and top findings. No network, no telemetry, no signup. After a scan, the full HTML certificate and SVG badge are readable as the resources ${certificateUri} and ${badgeUri}.`,
|
|
17065
17065
|
{
|
|
17066
17066
|
path: external_exports.string().min(1).describe("Path to the repository to score (absolute, or relative to the server CWD)"),
|
|
17067
17067
|
format: external_exports.enum(["summary", "json"]).optional().describe(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cejel/cejel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
],
|
|
23
23
|
"license": "AGPL-3.0-only",
|
|
24
24
|
"author": "Barg Labs",
|
|
25
|
-
"homepage": "https://
|
|
25
|
+
"homepage": "https://cejel.dev",
|
|
26
26
|
"bugs": {
|
|
27
27
|
"url": "https://github.com/BargLabs/cejel/issues"
|
|
28
28
|
},
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "tsup",
|
|
48
|
+
"build:sea-js": "tsup --config tsup.public-sea.config.ts",
|
|
48
49
|
"prepublishOnly": "pnpm run build",
|
|
49
50
|
"prepack": "pnpm run build",
|
|
50
51
|
"dev": "tsx src/index.ts",
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
56
57
|
"@types/node": "^25.9.1",
|
|
58
|
+
"postject": "^1.0.0-alpha.6",
|
|
57
59
|
"tsup": "^8.0.0",
|
|
58
60
|
"tsx": "^4.0.0",
|
|
59
61
|
"typescript": "^5.5.0",
|