veriskit 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +82 -2
- package/dist/index.js +788 -49
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 — 2026-07-13
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Publish to GitHub. `veris verify --github` posts and updates one sticky PR comment with the verdict and report, and creates a Check Run (verified passes, failed fails, partial is neutral). Token read from `GITHUB_TOKEN`; publishing never changes the verdict or exit code. `veris badge` writes a shields.io endpoint JSON. GitHub API over built-in fetch, no new dependency.
|
|
7
|
+
- Browser tests. A real Playwright runner, opt-in with `veris verify --browser` (or a `browser` entry in `.veris/config.json`). Detected Playwright now shows as an available capability.
|
|
8
|
+
- `veris log` lists past runs from the stored evidence records, and `veris log --flaky` flags checks that both passed and failed across recent runs. Local per-machine history.
|
|
9
|
+
|
|
10
|
+
## 0.4.1 — 2026-07-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Signed evidence. `veris evidence keygen` creates an Ed25519 keypair (via Node's built-in crypto, no new dependency); `veris evidence sign <evidence.json>` writes a detached signature; `veris evidence verify` checks a sibling signature automatically and can assert the signer with `--pubkey` or `--key-id`. Bundles carry the signature. Signing is opt-in; unsigned evidence still verifies for integrity. `VERISKIT_SIGNING_KEY` supplies the key in CI.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- `veris init` now gitignores `keys/`.
|
|
17
|
+
|
|
3
18
|
## 0.4.0 — 2026-07-10
|
|
4
19
|
|
|
5
20
|
### Added
|
package/README.md
CHANGED
|
@@ -190,7 +190,7 @@ veris evidence verify .veris/runs/<run-id>/evidence.json
|
|
|
190
190
|
This recomputes the digest and reports whether the record was edited or
|
|
191
191
|
corrupted since it was written. An integrity digest is not forgery-proof on its
|
|
192
192
|
own. To prove authorship, publish the digest separately (a CI log or PR) or sign
|
|
193
|
-
|
|
193
|
+
the record with an Ed25519 key (see Signing below).
|
|
194
194
|
|
|
195
195
|
Package a run as a single portable file (the record, its report, and its logs,
|
|
196
196
|
each with a digest, plus a bundle digest over everything):
|
|
@@ -202,6 +202,28 @@ veris evidence verify <bundle> # checks the record, every log, and the report
|
|
|
202
202
|
|
|
203
203
|
`veris evidence show` prints the latest record's key facts.
|
|
204
204
|
|
|
205
|
+
### Signing (optional)
|
|
206
|
+
|
|
207
|
+
An integrity digest proves a record was not edited. A signature proves a
|
|
208
|
+
specific key vouched for it. VerisKit signs with Ed25519 from Node's built-in
|
|
209
|
+
crypto, so signing adds no dependency and works offline.
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
veris evidence keygen # writes .veris/keys/veriskit-signing.key(.pub)
|
|
213
|
+
veris evidence sign .veris/runs/<id>/evidence.json --key .veris/keys/veriskit-signing.key
|
|
214
|
+
veris evidence verify .veris/runs/<id>/evidence.json --pubkey .veris/keys/veriskit-signing.key.pub
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`sign` writes a detached `<evidence.json>.sig` next to the record. `verify`
|
|
218
|
+
picks it up automatically and checks it. In CI, pass the key through the
|
|
219
|
+
`VERISKIT_SIGNING_KEY` environment variable instead of a file.
|
|
220
|
+
|
|
221
|
+
A signature proves that whoever holds the private key signed the record. It
|
|
222
|
+
does not prove who that is. VerisKit prints the key fingerprint; to bind it to
|
|
223
|
+
a person or system, compare that fingerprint to one you already trust, or
|
|
224
|
+
assert it with `--pubkey` or `--key-id`. Keep the private key secret; `keygen`
|
|
225
|
+
writes it into `.veris/keys/`, which `veris init` gitignores.
|
|
226
|
+
|
|
205
227
|
Commit `.veris/config.json` and `.veris/.gitignore`. `veris init` keeps `runs/`,
|
|
206
228
|
`reports/`, `cache/`, `graph.json`, and `evidence/` out of your history.
|
|
207
229
|
|
|
@@ -213,7 +235,65 @@ VerisKit says what it cannot do as plainly as what it can:
|
|
|
213
235
|
- **No test generation.** `plan` tells you what to test. Writing the tests is a later release.
|
|
214
236
|
- **One project root.** A monorepo with several `tsconfig.json` files is not modeled yet. Resolution runs against the root project.
|
|
215
237
|
- **Scanner fallback on plain-JS or TS 7.x-native projects.** The accurate resolver needs the classic TypeScript compiler API. Without it you get the labeled, relative-imports-only graph described above, and no dependency is added to paper over the gap.
|
|
216
|
-
- **No
|
|
238
|
+
- **No keyless or identity-bound signing.** Evidence can be signed with a local Ed25519 key (see Signing), but sigstore-style keyless signing that ties a signature to an identity is not built yet.
|
|
239
|
+
|
|
240
|
+
## Publish to a pull request
|
|
241
|
+
|
|
242
|
+
In CI, surface the verdict where reviewers look. Opt in with `--github`; VerisKit
|
|
243
|
+
reads `GITHUB_TOKEN` from the environment and never stores it.
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
veris verify --github # posts/updates a sticky PR comment + a Check Run
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
It edits one comment on re-runs (no spam) and creates a Check Run whose
|
|
250
|
+
conclusion follows the verdict (verified passes, failed fails, partial is
|
|
251
|
+
neutral). Publishing is a side channel: if there is no token or PR, VerisKit
|
|
252
|
+
prints a notice and the exit code still reflects the verdict, and a GitHub API
|
|
253
|
+
error is reported but never changes the result.
|
|
254
|
+
|
|
255
|
+
The workflow needs permission to write them:
|
|
256
|
+
|
|
257
|
+
```yaml
|
|
258
|
+
permissions:
|
|
259
|
+
contents: read
|
|
260
|
+
pull-requests: write
|
|
261
|
+
checks: write
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
For a README badge, write a shields.io endpoint file:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
veris badge # writes .veris/badge.json
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
```markdown
|
|
271
|
+

|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Browser tests
|
|
275
|
+
|
|
276
|
+
VerisKit can run your Playwright suite as part of the verdict. It is opt-in, so
|
|
277
|
+
a normal `veris verify` stays fast:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
veris verify --browser # also runs `playwright test`, folded into the verdict
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
When Playwright is detected, `veris doctor` lists `browser` as available. You can
|
|
284
|
+
also add `browser` to the `checks` array in `.veris/config.json` to run it every
|
|
285
|
+
time.
|
|
286
|
+
|
|
287
|
+
## History
|
|
288
|
+
|
|
289
|
+
Every run leaves an evidence record, so VerisKit can show you a trend:
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
veris log # past runs, newest first
|
|
293
|
+
veris log --flaky # checks that both passed and failed across recent runs
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
History is local to your machine (the `.veris/runs` directory is gitignored).
|
|
217
297
|
|
|
218
298
|
## Part of Baseframe Labs
|
|
219
299
|
|
package/dist/index.js
CHANGED
|
@@ -105,13 +105,9 @@ function detectLint(root, has) {
|
|
|
105
105
|
return { id: "lint", available: false, reason: "no linter configured" };
|
|
106
106
|
}
|
|
107
107
|
function detectBrowser(root, has) {
|
|
108
|
-
if (has("@playwright/test") || existsSync2(join(root, "playwright.config.ts")))
|
|
109
|
-
return {
|
|
110
|
-
|
|
111
|
-
available: false,
|
|
112
|
-
runner: "playwright",
|
|
113
|
-
reason: "detected; browser execution deferred to v0.5"
|
|
114
|
-
};
|
|
108
|
+
if (has("@playwright/test") || existsSync2(join(root, "playwright.config.ts")) || existsSync2(join(root, "playwright.config.js"))) {
|
|
109
|
+
return { id: "browser", available: true, runner: "playwright" };
|
|
110
|
+
}
|
|
115
111
|
return {
|
|
116
112
|
id: "browser",
|
|
117
113
|
available: false,
|
|
@@ -548,6 +544,76 @@ var init_node_test = __esm({
|
|
|
548
544
|
}
|
|
549
545
|
});
|
|
550
546
|
|
|
547
|
+
// src/runners/playwright.ts
|
|
548
|
+
function parsePlaywrightStats(stdout) {
|
|
549
|
+
try {
|
|
550
|
+
const json = JSON.parse(stdout);
|
|
551
|
+
return json.stats ?? null;
|
|
552
|
+
} catch {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
var TAIL_LINES2, playwrightRunner;
|
|
557
|
+
var init_playwright = __esm({
|
|
558
|
+
"src/runners/playwright.ts"() {
|
|
559
|
+
"use strict";
|
|
560
|
+
init_store();
|
|
561
|
+
init_exec();
|
|
562
|
+
init_base();
|
|
563
|
+
TAIL_LINES2 = 20;
|
|
564
|
+
playwrightRunner = {
|
|
565
|
+
id: "playwright",
|
|
566
|
+
toCheck(project, _cap) {
|
|
567
|
+
return {
|
|
568
|
+
id: "browser",
|
|
569
|
+
title: "Browser tests",
|
|
570
|
+
runner: "playwright",
|
|
571
|
+
cmd: localBin(project.root, "playwright"),
|
|
572
|
+
args: ["test", "--reporter=json"]
|
|
573
|
+
};
|
|
574
|
+
},
|
|
575
|
+
async run(check, ctx) {
|
|
576
|
+
const r = await exec(check.cmd, check.args, {
|
|
577
|
+
cwd: ctx.root,
|
|
578
|
+
timeoutMs: 15 * 6e4
|
|
579
|
+
});
|
|
580
|
+
const output = [r.stdout, r.stderr].filter(Boolean).join("\n").trim();
|
|
581
|
+
const logRef = await writeLog(ctx.runDir, check.id, `${output}
|
|
582
|
+
`);
|
|
583
|
+
const stats = parsePlaywrightStats(r.stdout);
|
|
584
|
+
let status;
|
|
585
|
+
if (r.timedOut) {
|
|
586
|
+
status = "unknown";
|
|
587
|
+
} else if (stats) {
|
|
588
|
+
status = r.code === 0 && (stats.unexpected ?? 0) === 0 ? "passed" : "failed";
|
|
589
|
+
} else {
|
|
590
|
+
status = r.code === 0 ? "unknown" : "failed";
|
|
591
|
+
}
|
|
592
|
+
const result = {
|
|
593
|
+
checkId: "browser",
|
|
594
|
+
status,
|
|
595
|
+
durationMs: r.durationMs,
|
|
596
|
+
summary: status === "passed" ? "browser tests passed" : r.timedOut ? "timed out" : status === "unknown" ? "browser tests ran but the results could not be parsed" : "browser tests failed",
|
|
597
|
+
logRef
|
|
598
|
+
};
|
|
599
|
+
if (stats) {
|
|
600
|
+
const passed = stats.expected ?? 0;
|
|
601
|
+
const failed = stats.unexpected ?? 0;
|
|
602
|
+
result.counts = {
|
|
603
|
+
passed,
|
|
604
|
+
failed,
|
|
605
|
+
total: passed + failed + (stats.flaky ?? 0) + (stats.skipped ?? 0)
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
if (status !== "passed" && output) {
|
|
609
|
+
result.outputTail = output.split("\n").slice(-TAIL_LINES2).join("\n");
|
|
610
|
+
}
|
|
611
|
+
return result;
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
551
617
|
// src/runners/tsc.ts
|
|
552
618
|
var tscRunner;
|
|
553
619
|
var init_tsc = __esm({
|
|
@@ -614,6 +680,7 @@ var init_runners = __esm({
|
|
|
614
680
|
init_eslint();
|
|
615
681
|
init_jest();
|
|
616
682
|
init_node_test();
|
|
683
|
+
init_playwright();
|
|
617
684
|
init_tsc();
|
|
618
685
|
init_vitest();
|
|
619
686
|
init_base();
|
|
@@ -623,7 +690,8 @@ var init_runners = __esm({
|
|
|
623
690
|
jest: jestRunner,
|
|
624
691
|
"node-test": nodeTestRunner,
|
|
625
692
|
eslint: eslintRunner,
|
|
626
|
-
biome: biomeRunner
|
|
693
|
+
biome: biomeRunner,
|
|
694
|
+
playwright: playwrightRunner
|
|
627
695
|
};
|
|
628
696
|
}
|
|
629
697
|
});
|
|
@@ -849,6 +917,7 @@ var init_init = __esm({
|
|
|
849
917
|
"cache/",
|
|
850
918
|
"graph.json",
|
|
851
919
|
"evidence/",
|
|
920
|
+
"keys/",
|
|
852
921
|
""
|
|
853
922
|
].join("\n");
|
|
854
923
|
}
|
|
@@ -996,15 +1065,205 @@ var init_markdown = __esm({
|
|
|
996
1065
|
}
|
|
997
1066
|
});
|
|
998
1067
|
|
|
1068
|
+
// src/publish/comment.ts
|
|
1069
|
+
function renderComment(run, record) {
|
|
1070
|
+
const summary = run.results.filter((r) => r.status !== "skipped").map((r) => `${r.checkId} ${r.status}`).join(" \xB7 ");
|
|
1071
|
+
return [
|
|
1072
|
+
MARKER,
|
|
1073
|
+
`**VerisKit: ${STATE_LABEL2[run.verdict.state]}**`,
|
|
1074
|
+
"",
|
|
1075
|
+
summary,
|
|
1076
|
+
"",
|
|
1077
|
+
"<details><summary>Report</summary>",
|
|
1078
|
+
"",
|
|
1079
|
+
renderMarkdown(run, record),
|
|
1080
|
+
"",
|
|
1081
|
+
"</details>",
|
|
1082
|
+
""
|
|
1083
|
+
].join("\n");
|
|
1084
|
+
}
|
|
1085
|
+
var MARKER, STATE_LABEL2;
|
|
1086
|
+
var init_comment = __esm({
|
|
1087
|
+
"src/publish/comment.ts"() {
|
|
1088
|
+
"use strict";
|
|
1089
|
+
init_markdown();
|
|
1090
|
+
MARKER = "<!-- veriskit-report -->";
|
|
1091
|
+
STATE_LABEL2 = {
|
|
1092
|
+
verified: "verified",
|
|
1093
|
+
failed: "failed",
|
|
1094
|
+
partial: "partial"
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
// src/publish/context.ts
|
|
1100
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1101
|
+
function defaultReadEvent(path) {
|
|
1102
|
+
try {
|
|
1103
|
+
return JSON.parse(readFileSync2(path, "utf8"));
|
|
1104
|
+
} catch {
|
|
1105
|
+
return null;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function resolvePublishContext(env = process.env, readEvent = defaultReadEvent) {
|
|
1109
|
+
const repository = env.GITHUB_REPOSITORY;
|
|
1110
|
+
const token = env.GITHUB_TOKEN;
|
|
1111
|
+
if (!repository || !token) return null;
|
|
1112
|
+
const [owner, repo] = repository.split("/");
|
|
1113
|
+
if (!owner || !repo) return null;
|
|
1114
|
+
let prNumber = null;
|
|
1115
|
+
let headSha = env.GITHUB_SHA ?? "";
|
|
1116
|
+
const refMatch = (env.GITHUB_REF ?? "").match(/^refs\/pull\/(\d+)\//);
|
|
1117
|
+
if (refMatch) prNumber = Number(refMatch[1]);
|
|
1118
|
+
const event = readEvent(env.GITHUB_EVENT_PATH ?? "");
|
|
1119
|
+
const pr = event?.pull_request;
|
|
1120
|
+
if (pr) {
|
|
1121
|
+
if (prNumber === null && typeof pr.number === "number") {
|
|
1122
|
+
prNumber = pr.number;
|
|
1123
|
+
}
|
|
1124
|
+
if (pr.head?.sha) headSha = pr.head.sha;
|
|
1125
|
+
}
|
|
1126
|
+
if (prNumber === null || !headSha) return null;
|
|
1127
|
+
return { owner, repo, token, prNumber, headSha };
|
|
1128
|
+
}
|
|
1129
|
+
var init_context = __esm({
|
|
1130
|
+
"src/publish/context.ts"() {
|
|
1131
|
+
"use strict";
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
// src/publish/github.ts
|
|
1136
|
+
function headers(token) {
|
|
1137
|
+
return {
|
|
1138
|
+
Authorization: `Bearer ${token}`,
|
|
1139
|
+
Accept: "application/vnd.github+json",
|
|
1140
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
1141
|
+
"User-Agent": "veriskit",
|
|
1142
|
+
"Content-Type": "application/json"
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
async function gh(method, url, token, body) {
|
|
1146
|
+
const res = await fetch(url, {
|
|
1147
|
+
method,
|
|
1148
|
+
headers: headers(token),
|
|
1149
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
1150
|
+
});
|
|
1151
|
+
if (!res.ok) {
|
|
1152
|
+
const path = url.replace("https://api.github.com", "");
|
|
1153
|
+
throw new GitHubApiError(
|
|
1154
|
+
res.status,
|
|
1155
|
+
`GitHub API ${method} ${path} -> ${res.status}`
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
return res.json();
|
|
1159
|
+
}
|
|
1160
|
+
function repoBase(ctx) {
|
|
1161
|
+
return `https://api.github.com/repos/${ctx.owner}/${ctx.repo}`;
|
|
1162
|
+
}
|
|
1163
|
+
async function upsertComment(ctx, body) {
|
|
1164
|
+
const base = repoBase(ctx);
|
|
1165
|
+
const comments = await gh(
|
|
1166
|
+
"GET",
|
|
1167
|
+
`${base}/issues/${ctx.prNumber}/comments?per_page=100`,
|
|
1168
|
+
ctx.token
|
|
1169
|
+
);
|
|
1170
|
+
const existing = comments.find((c) => (c.body ?? "").includes(MARKER));
|
|
1171
|
+
if (existing) {
|
|
1172
|
+
await gh("PATCH", `${base}/issues/comments/${existing.id}`, ctx.token, {
|
|
1173
|
+
body
|
|
1174
|
+
});
|
|
1175
|
+
} else {
|
|
1176
|
+
await gh("POST", `${base}/issues/${ctx.prNumber}/comments`, ctx.token, {
|
|
1177
|
+
body
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
async function createCheckRun(ctx, opts) {
|
|
1182
|
+
await gh("POST", `${repoBase(ctx)}/check-runs`, ctx.token, {
|
|
1183
|
+
name: opts.name,
|
|
1184
|
+
head_sha: ctx.headSha,
|
|
1185
|
+
status: "completed",
|
|
1186
|
+
conclusion: opts.conclusion,
|
|
1187
|
+
output: { title: opts.title, summary: opts.summary }
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
var GitHubApiError;
|
|
1191
|
+
var init_github = __esm({
|
|
1192
|
+
"src/publish/github.ts"() {
|
|
1193
|
+
"use strict";
|
|
1194
|
+
init_comment();
|
|
1195
|
+
GitHubApiError = class extends Error {
|
|
1196
|
+
status;
|
|
1197
|
+
constructor(status, message) {
|
|
1198
|
+
super(message);
|
|
1199
|
+
this.name = "GitHubApiError";
|
|
1200
|
+
this.status = status;
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
// src/publish/publish.ts
|
|
1207
|
+
async function publishToGitHub(run, record) {
|
|
1208
|
+
const ctx = resolvePublishContext();
|
|
1209
|
+
if (!ctx) {
|
|
1210
|
+
process.stdout.write(
|
|
1211
|
+
"veris: --github set but no GitHub PR context found (need GITHUB_TOKEN + a pull_request event); skipping publish.\n"
|
|
1212
|
+
);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
try {
|
|
1216
|
+
await upsertComment(ctx, renderComment(run, record));
|
|
1217
|
+
await createCheckRun(ctx, {
|
|
1218
|
+
name: "VerisKit",
|
|
1219
|
+
conclusion: CONCLUSION[run.verdict.state],
|
|
1220
|
+
title: `VerisKit: ${run.verdict.state}`,
|
|
1221
|
+
summary: renderMarkdown(run, record)
|
|
1222
|
+
});
|
|
1223
|
+
process.stdout.write(
|
|
1224
|
+
`veris: published the verdict to PR #${ctx.prNumber}
|
|
1225
|
+
`
|
|
1226
|
+
);
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1229
|
+
process.stderr.write(`veris: GitHub publish failed: ${msg}
|
|
1230
|
+
`);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
var CONCLUSION;
|
|
1234
|
+
var init_publish = __esm({
|
|
1235
|
+
"src/publish/publish.ts"() {
|
|
1236
|
+
"use strict";
|
|
1237
|
+
init_markdown();
|
|
1238
|
+
init_comment();
|
|
1239
|
+
init_context();
|
|
1240
|
+
init_github();
|
|
1241
|
+
CONCLUSION = {
|
|
1242
|
+
verified: "success",
|
|
1243
|
+
failed: "failure",
|
|
1244
|
+
partial: "neutral"
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
|
|
999
1249
|
// src/cli/commands/verify.ts
|
|
1000
1250
|
var verify_exports = {};
|
|
1001
1251
|
__export(verify_exports, {
|
|
1252
|
+
resolveChecks: () => resolveChecks,
|
|
1002
1253
|
runVerify: () => runVerify
|
|
1003
1254
|
});
|
|
1255
|
+
function resolveChecks(configChecks, project, opts) {
|
|
1256
|
+
let checks = configChecks?.length ? configChecks : DEFAULT_CHECKS;
|
|
1257
|
+
if (opts.browser && !checks.includes("browser")) {
|
|
1258
|
+
const cap = project.capabilities.find((c) => c.id === "browser");
|
|
1259
|
+
if (cap?.available) checks = [...checks, "browser"];
|
|
1260
|
+
}
|
|
1261
|
+
return checks;
|
|
1262
|
+
}
|
|
1004
1263
|
async function runVerify(root, opts = {}) {
|
|
1005
1264
|
const project = await detectProject(root);
|
|
1006
1265
|
const config = await loadConfig(root);
|
|
1007
|
-
const checks = config?.checks
|
|
1266
|
+
const checks = resolveChecks(config?.checks, project, opts);
|
|
1008
1267
|
const run = await runChecks(project, checks, root);
|
|
1009
1268
|
const git = await gitAnchor(root);
|
|
1010
1269
|
const logDigests = await digestLogs(run);
|
|
@@ -1019,6 +1278,7 @@ async function runVerify(root, opts = {}) {
|
|
|
1019
1278
|
await writeEvidence(runDir, record);
|
|
1020
1279
|
process.stdout.write(`${renderRun(run, record)}
|
|
1021
1280
|
`);
|
|
1281
|
+
if (opts.github) await publishToGitHub(run, record);
|
|
1022
1282
|
return verdictExitCode(run.verdict, opts);
|
|
1023
1283
|
}
|
|
1024
1284
|
var DEFAULT_CHECKS;
|
|
@@ -1032,6 +1292,7 @@ var init_verify = __esm({
|
|
|
1032
1292
|
init_record();
|
|
1033
1293
|
init_store();
|
|
1034
1294
|
init_changes();
|
|
1295
|
+
init_publish();
|
|
1035
1296
|
init_markdown();
|
|
1036
1297
|
init_terminal();
|
|
1037
1298
|
init_version();
|
|
@@ -1044,7 +1305,7 @@ var report_exports = {};
|
|
|
1044
1305
|
__export(report_exports, {
|
|
1045
1306
|
runReport: () => runReport
|
|
1046
1307
|
});
|
|
1047
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
1308
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
1048
1309
|
import { join as join6 } from "path";
|
|
1049
1310
|
async function runReport(root) {
|
|
1050
1311
|
const dir = join6(root, ".veris", "reports");
|
|
@@ -1060,7 +1321,7 @@ async function runReport(root) {
|
|
|
1060
1321
|
process.stdout.write("No reports yet. Run `veris verify` first.\n");
|
|
1061
1322
|
return 0;
|
|
1062
1323
|
}
|
|
1063
|
-
process.stdout.write(`${
|
|
1324
|
+
process.stdout.write(`${readFileSync3(join6(dir, latest), "utf8")}
|
|
1064
1325
|
`);
|
|
1065
1326
|
return 0;
|
|
1066
1327
|
}
|
|
@@ -1175,7 +1436,7 @@ var init_discover = __esm({
|
|
|
1175
1436
|
});
|
|
1176
1437
|
|
|
1177
1438
|
// src/project-graph/scanner-resolver.ts
|
|
1178
|
-
import { existsSync as existsSync3, readFileSync as
|
|
1439
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
1179
1440
|
import { dirname, join as join8, relative as relative3, resolve } from "path";
|
|
1180
1441
|
function extractSpecifiers(text) {
|
|
1181
1442
|
const specs = [];
|
|
@@ -1214,7 +1475,7 @@ function resolveRelative(fromDir, spec) {
|
|
|
1214
1475
|
function scannerImports(root, file) {
|
|
1215
1476
|
let text;
|
|
1216
1477
|
try {
|
|
1217
|
-
text =
|
|
1478
|
+
text = readFileSync4(join8(root, file), "utf8");
|
|
1218
1479
|
} catch {
|
|
1219
1480
|
return [];
|
|
1220
1481
|
}
|
|
@@ -1238,7 +1499,7 @@ var init_scanner_resolver = __esm({
|
|
|
1238
1499
|
});
|
|
1239
1500
|
|
|
1240
1501
|
// src/project-graph/ts-resolver.ts
|
|
1241
|
-
import { existsSync as existsSync4, readFileSync as
|
|
1502
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
1242
1503
|
import { createRequire } from "module";
|
|
1243
1504
|
import { join as join9, relative as relative4, sep as sep2 } from "path";
|
|
1244
1505
|
function hasClassicApi(mod) {
|
|
@@ -1265,7 +1526,7 @@ function loadCompilerOptions(tsmod, root) {
|
|
|
1265
1526
|
function tsImports(tsmod, root, options, file) {
|
|
1266
1527
|
let text;
|
|
1267
1528
|
try {
|
|
1268
|
-
text =
|
|
1529
|
+
text = readFileSync5(join9(root, file), "utf8");
|
|
1269
1530
|
} catch {
|
|
1270
1531
|
return [];
|
|
1271
1532
|
}
|
|
@@ -1517,6 +1778,7 @@ async function runAffected(root, opts = {}) {
|
|
|
1517
1778
|
`);
|
|
1518
1779
|
if (narrowedNote) process.stdout.write(`${narrowedNote}
|
|
1519
1780
|
`);
|
|
1781
|
+
if (opts.github) await publishToGitHub(run, record);
|
|
1520
1782
|
return verdictExitCode(run.verdict, opts);
|
|
1521
1783
|
}
|
|
1522
1784
|
var init_affected = __esm({
|
|
@@ -1529,6 +1791,7 @@ var init_affected = __esm({
|
|
|
1529
1791
|
init_record();
|
|
1530
1792
|
init_store();
|
|
1531
1793
|
init_changes();
|
|
1794
|
+
init_publish();
|
|
1532
1795
|
init_markdown();
|
|
1533
1796
|
init_terminal();
|
|
1534
1797
|
init_version();
|
|
@@ -1897,7 +2160,84 @@ var init_plan = __esm({
|
|
|
1897
2160
|
}
|
|
1898
2161
|
});
|
|
1899
2162
|
|
|
2163
|
+
// src/evidence/signing.ts
|
|
2164
|
+
import {
|
|
2165
|
+
createHash as createHash2,
|
|
2166
|
+
createPrivateKey,
|
|
2167
|
+
createPublicKey,
|
|
2168
|
+
sign as cryptoSign,
|
|
2169
|
+
verify as cryptoVerify,
|
|
2170
|
+
generateKeyPairSync
|
|
2171
|
+
} from "crypto";
|
|
2172
|
+
function derOf(publicKey) {
|
|
2173
|
+
return publicKey.export({ type: "spki", format: "der" });
|
|
2174
|
+
}
|
|
2175
|
+
function keyId(publicKey) {
|
|
2176
|
+
const key = typeof publicKey === "string" ? createPublicKey(publicKey) : publicKey;
|
|
2177
|
+
return createHash2("sha256").update(derOf(key)).digest("hex").slice(0, 8);
|
|
2178
|
+
}
|
|
2179
|
+
function signatureKeyId(sig) {
|
|
2180
|
+
const key = createPublicKey({
|
|
2181
|
+
key: Buffer.from(sig.publicKey, "base64"),
|
|
2182
|
+
format: "der",
|
|
2183
|
+
type: "spki"
|
|
2184
|
+
});
|
|
2185
|
+
return keyId(key);
|
|
2186
|
+
}
|
|
2187
|
+
function generateKeyPair() {
|
|
2188
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
2189
|
+
return {
|
|
2190
|
+
publicKeyPem: publicKey.export({ type: "spki", format: "pem" }),
|
|
2191
|
+
privateKeyPem: privateKey.export({
|
|
2192
|
+
type: "pkcs8",
|
|
2193
|
+
format: "pem"
|
|
2194
|
+
}),
|
|
2195
|
+
keyId: keyId(publicKey)
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
function signDigest(digest, privateKeyPem) {
|
|
2199
|
+
const privateKey = createPrivateKey(privateKeyPem);
|
|
2200
|
+
const publicKey = createPublicKey(
|
|
2201
|
+
privateKey
|
|
2202
|
+
);
|
|
2203
|
+
const sig = cryptoSign(null, Buffer.from(digest, "utf8"), privateKey);
|
|
2204
|
+
return {
|
|
2205
|
+
schema: SIGNATURE_SCHEMA,
|
|
2206
|
+
alg: "ed25519",
|
|
2207
|
+
keyId: keyId(publicKey),
|
|
2208
|
+
publicKey: derOf(publicKey).toString("base64"),
|
|
2209
|
+
digest,
|
|
2210
|
+
signature: sig.toString("base64"),
|
|
2211
|
+
signedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
function verifySignature(sig) {
|
|
2215
|
+
try {
|
|
2216
|
+
const publicKey = createPublicKey({
|
|
2217
|
+
key: Buffer.from(sig.publicKey, "base64"),
|
|
2218
|
+
format: "der",
|
|
2219
|
+
type: "spki"
|
|
2220
|
+
});
|
|
2221
|
+
return cryptoVerify(
|
|
2222
|
+
null,
|
|
2223
|
+
Buffer.from(sig.digest, "utf8"),
|
|
2224
|
+
publicKey,
|
|
2225
|
+
Buffer.from(sig.signature, "base64")
|
|
2226
|
+
);
|
|
2227
|
+
} catch {
|
|
2228
|
+
return false;
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
var SIGNATURE_SCHEMA;
|
|
2232
|
+
var init_signing = __esm({
|
|
2233
|
+
"src/evidence/signing.ts"() {
|
|
2234
|
+
"use strict";
|
|
2235
|
+
SIGNATURE_SCHEMA = "veriskit/signature@1";
|
|
2236
|
+
}
|
|
2237
|
+
});
|
|
2238
|
+
|
|
1900
2239
|
// src/evidence/verify-evidence.ts
|
|
2240
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1901
2241
|
import { readFile as readFile3 } from "fs/promises";
|
|
1902
2242
|
function verifyRecord(record) {
|
|
1903
2243
|
const recomputed = computeDigest(
|
|
@@ -1911,20 +2251,71 @@ function verifyRecord(record) {
|
|
|
1911
2251
|
detail: ok ? "matches the canonical record" : `recomputed ${recomputed}, record claims ${record.digest}`
|
|
1912
2252
|
}
|
|
1913
2253
|
];
|
|
1914
|
-
return { ok, kind: "record", record, checks };
|
|
2254
|
+
return { ok, kind: "record", record, checks, signed: false };
|
|
2255
|
+
}
|
|
2256
|
+
function signatureChecks(recordDigest, sig, opts = {}) {
|
|
2257
|
+
const checks = [];
|
|
2258
|
+
const kid = signatureKeyId(sig);
|
|
2259
|
+
const validSig = verifySignature(sig) && sig.digest === recordDigest;
|
|
2260
|
+
checks.push({
|
|
2261
|
+
name: "signature",
|
|
2262
|
+
ok: validSig,
|
|
2263
|
+
detail: validSig ? `valid ed25519 signature by key ${kid}` : "signature does not match this record"
|
|
2264
|
+
});
|
|
2265
|
+
if (opts.expectedKeyId || opts.expectedPubKeyPem) {
|
|
2266
|
+
let matches = false;
|
|
2267
|
+
if (opts.expectedPubKeyPem) {
|
|
2268
|
+
try {
|
|
2269
|
+
matches = keyId(opts.expectedPubKeyPem) === kid;
|
|
2270
|
+
} catch {
|
|
2271
|
+
matches = false;
|
|
2272
|
+
}
|
|
2273
|
+
} else if (opts.expectedKeyId) {
|
|
2274
|
+
matches = opts.expectedKeyId.toLowerCase() === kid.toLowerCase();
|
|
2275
|
+
}
|
|
2276
|
+
checks.push({
|
|
2277
|
+
name: "signer",
|
|
2278
|
+
ok: matches,
|
|
2279
|
+
detail: matches ? `signer matches the expected key ${kid}` : `signer ${kid} does not match the expected key`
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
return checks;
|
|
1915
2283
|
}
|
|
1916
|
-
async function verifyEvidenceFile(path) {
|
|
2284
|
+
async function verifyEvidenceFile(path, opts = {}) {
|
|
1917
2285
|
const parsed = JSON.parse(await readFile3(path, "utf8"));
|
|
1918
2286
|
if (parsed?.schema === "veriskit/bundle@1") {
|
|
1919
2287
|
const { verifyBundle: verifyBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
|
|
1920
|
-
return verifyBundle2(parsed);
|
|
2288
|
+
return verifyBundle2(parsed, opts);
|
|
1921
2289
|
}
|
|
1922
|
-
|
|
2290
|
+
const result = verifyRecord(parsed);
|
|
2291
|
+
const assertedSigner = Boolean(
|
|
2292
|
+
opts.expectedKeyId || opts.expectedPubKeyPem || opts.sigPath
|
|
2293
|
+
);
|
|
2294
|
+
const sigPath = opts.sigPath ?? `${path}.sig`;
|
|
2295
|
+
if (existsSync5(sigPath)) {
|
|
2296
|
+
const sig = JSON.parse(await readFile3(sigPath, "utf8"));
|
|
2297
|
+
result.checks.push(
|
|
2298
|
+
...signatureChecks(result.record.digest, sig, {
|
|
2299
|
+
expectedKeyId: opts.expectedKeyId,
|
|
2300
|
+
expectedPubKeyPem: opts.expectedPubKeyPem
|
|
2301
|
+
})
|
|
2302
|
+
);
|
|
2303
|
+
result.signed = true;
|
|
2304
|
+
} else if (assertedSigner) {
|
|
2305
|
+
result.checks.push({
|
|
2306
|
+
name: "signature",
|
|
2307
|
+
ok: false,
|
|
2308
|
+
detail: "a signature was required (--pubkey/--key-id/--sig) but none was found"
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
result.ok = result.checks.every((c) => c.ok);
|
|
2312
|
+
return result;
|
|
1923
2313
|
}
|
|
1924
2314
|
var init_verify_evidence = __esm({
|
|
1925
2315
|
"src/evidence/verify-evidence.ts"() {
|
|
1926
2316
|
"use strict";
|
|
1927
2317
|
init_record();
|
|
2318
|
+
init_signing();
|
|
1928
2319
|
}
|
|
1929
2320
|
});
|
|
1930
2321
|
|
|
@@ -1935,7 +2326,7 @@ __export(bundle_exports, {
|
|
|
1935
2326
|
buildBundle: () => buildBundle,
|
|
1936
2327
|
verifyBundle: () => verifyBundle
|
|
1937
2328
|
});
|
|
1938
|
-
function buildBundle(record, report, logs) {
|
|
2329
|
+
function buildBundle(record, report, logs, signature) {
|
|
1939
2330
|
const manifest = {
|
|
1940
2331
|
record: record.digest,
|
|
1941
2332
|
report: sha256(report),
|
|
@@ -1943,10 +2334,10 @@ function buildBundle(record, report, logs) {
|
|
|
1943
2334
|
Object.entries(logs).map(([k, v]) => [k, sha256(v)])
|
|
1944
2335
|
)
|
|
1945
2336
|
};
|
|
1946
|
-
const base = { schema: BUNDLE_SCHEMA, record, report, logs, manifest };
|
|
2337
|
+
const base = signature ? { schema: BUNDLE_SCHEMA, record, report, logs, manifest, signature } : { schema: BUNDLE_SCHEMA, record, report, logs, manifest };
|
|
1947
2338
|
return { ...base, bundleDigest: sha256(canonicalize(base)) };
|
|
1948
2339
|
}
|
|
1949
|
-
function verifyBundle(bundle) {
|
|
2340
|
+
function verifyBundle(bundle, opts = {}) {
|
|
1950
2341
|
const checks = [];
|
|
1951
2342
|
const recordResult = verifyRecord(bundle.record);
|
|
1952
2343
|
checks.push(...recordResult.checks);
|
|
@@ -1971,11 +2362,28 @@ function verifyBundle(bundle) {
|
|
|
1971
2362
|
ok: recomputed === bundle.bundleDigest,
|
|
1972
2363
|
detail: recomputed === bundle.bundleDigest ? "matches" : "mismatch"
|
|
1973
2364
|
});
|
|
2365
|
+
let signed = false;
|
|
2366
|
+
if (bundle.signature) {
|
|
2367
|
+
signed = true;
|
|
2368
|
+
checks.push(
|
|
2369
|
+
...signatureChecks(bundle.record.digest, bundle.signature, {
|
|
2370
|
+
expectedKeyId: opts.expectedKeyId,
|
|
2371
|
+
expectedPubKeyPem: opts.expectedPubKeyPem
|
|
2372
|
+
})
|
|
2373
|
+
);
|
|
2374
|
+
} else if (opts.expectedKeyId || opts.expectedPubKeyPem) {
|
|
2375
|
+
checks.push({
|
|
2376
|
+
name: "signature",
|
|
2377
|
+
ok: false,
|
|
2378
|
+
detail: "a signature was required (--pubkey/--key-id) but the bundle is unsigned"
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
1974
2381
|
return {
|
|
1975
2382
|
ok: checks.every((c) => c.ok),
|
|
1976
2383
|
kind: "bundle",
|
|
1977
2384
|
record: bundle.record,
|
|
1978
|
-
checks
|
|
2385
|
+
checks,
|
|
2386
|
+
signed
|
|
1979
2387
|
};
|
|
1980
2388
|
}
|
|
1981
2389
|
var BUNDLE_SCHEMA;
|
|
@@ -1992,17 +2400,33 @@ var init_bundle = __esm({
|
|
|
1992
2400
|
var evidence_exports = {};
|
|
1993
2401
|
__export(evidence_exports, {
|
|
1994
2402
|
runEvidenceBundle: () => runEvidenceBundle,
|
|
2403
|
+
runEvidenceKeygen: () => runEvidenceKeygen,
|
|
1995
2404
|
runEvidenceShow: () => runEvidenceShow,
|
|
2405
|
+
runEvidenceSign: () => runEvidenceSign,
|
|
1996
2406
|
runEvidenceVerify: () => runEvidenceVerify
|
|
1997
2407
|
});
|
|
1998
|
-
import { readFileSync as
|
|
2408
|
+
import { chmodSync, existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync } from "fs";
|
|
1999
2409
|
import { writeFile as writeFile4 } from "fs/promises";
|
|
2000
|
-
import { basename as basename3, join as join12 } from "path";
|
|
2410
|
+
import { basename as basename3, dirname as dirname2, join as join12 } from "path";
|
|
2001
2411
|
import pc5 from "picocolors";
|
|
2002
|
-
async function runEvidenceVerify(path) {
|
|
2412
|
+
async function runEvidenceVerify(path, opts = {}) {
|
|
2413
|
+
let expectedPubKeyPem;
|
|
2414
|
+
if (opts.pubkey) {
|
|
2415
|
+
try {
|
|
2416
|
+
expectedPubKeyPem = readFileSync6(opts.pubkey, "utf8");
|
|
2417
|
+
} catch {
|
|
2418
|
+
process.stderr.write(`veris: cannot read public key at ${opts.pubkey}
|
|
2419
|
+
`);
|
|
2420
|
+
return 1;
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2003
2423
|
let result;
|
|
2004
2424
|
try {
|
|
2005
|
-
result = await verifyEvidenceFile(path
|
|
2425
|
+
result = await verifyEvidenceFile(path, {
|
|
2426
|
+
sigPath: opts.sig,
|
|
2427
|
+
expectedKeyId: opts.keyId,
|
|
2428
|
+
expectedPubKeyPem
|
|
2429
|
+
});
|
|
2006
2430
|
} catch (err) {
|
|
2007
2431
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2008
2432
|
process.stderr.write(`veris: cannot read evidence at ${path}: ${msg}
|
|
@@ -2027,8 +2451,93 @@ ${result.ok ? "digest OK" : "TAMPERED"} \xB7 verdict ${result.record.verdict.sta
|
|
|
2027
2451
|
process.stdout.write(`
|
|
2028
2452
|
${HONESTY}
|
|
2029
2453
|
`);
|
|
2454
|
+
if (result.signed && !opts.pubkey && !opts.keyId) {
|
|
2455
|
+
process.stdout.write(
|
|
2456
|
+
"\nThis record is signed. Confirm you trust the signing key by comparing its\nkey id above to one you already trust, or re-run with --pubkey / --key-id.\n"
|
|
2457
|
+
);
|
|
2458
|
+
}
|
|
2030
2459
|
return result.ok ? 0 : 1;
|
|
2031
2460
|
}
|
|
2461
|
+
async function runEvidenceKeygen(root, opts = {}) {
|
|
2462
|
+
const out = opts.out ?? join12(root, ".veris", "keys", "veriskit-signing.key");
|
|
2463
|
+
const pub = `${out}.pub`;
|
|
2464
|
+
if (existsSync6(out) || existsSync6(pub)) {
|
|
2465
|
+
process.stderr.write(
|
|
2466
|
+
`veris: a key already exists at ${out}. Refusing to overwrite it.
|
|
2467
|
+
`
|
|
2468
|
+
);
|
|
2469
|
+
return 1;
|
|
2470
|
+
}
|
|
2471
|
+
const { mkdir: mkdir3 } = await import("fs/promises");
|
|
2472
|
+
await mkdir3(dirname2(out), { recursive: true });
|
|
2473
|
+
const kp = generateKeyPair();
|
|
2474
|
+
writeFileSync(out, kp.privateKeyPem, { mode: 384 });
|
|
2475
|
+
chmodSync(out, 384);
|
|
2476
|
+
writeFileSync(pub, kp.publicKeyPem, "utf8");
|
|
2477
|
+
process.stdout.write(
|
|
2478
|
+
[
|
|
2479
|
+
`Wrote signing key ${out} (keep this secret; do not commit it)`,
|
|
2480
|
+
`Wrote public key ${pub}`,
|
|
2481
|
+
`Key id ${kp.keyId}`,
|
|
2482
|
+
""
|
|
2483
|
+
].join("\n")
|
|
2484
|
+
);
|
|
2485
|
+
return 0;
|
|
2486
|
+
}
|
|
2487
|
+
async function runEvidenceSign(evidencePath, opts = {}) {
|
|
2488
|
+
let privateKeyPem;
|
|
2489
|
+
if (process.env.VERISKIT_SIGNING_KEY) {
|
|
2490
|
+
privateKeyPem = process.env.VERISKIT_SIGNING_KEY;
|
|
2491
|
+
} else if (opts.key) {
|
|
2492
|
+
try {
|
|
2493
|
+
privateKeyPem = readFileSync6(opts.key, "utf8");
|
|
2494
|
+
} catch {
|
|
2495
|
+
process.stderr.write(`veris: cannot read signing key at ${opts.key}
|
|
2496
|
+
`);
|
|
2497
|
+
return 1;
|
|
2498
|
+
}
|
|
2499
|
+
} else {
|
|
2500
|
+
process.stderr.write(
|
|
2501
|
+
"veris: no signing key. Pass --key <path> or set VERISKIT_SIGNING_KEY.\n"
|
|
2502
|
+
);
|
|
2503
|
+
return 1;
|
|
2504
|
+
}
|
|
2505
|
+
let record;
|
|
2506
|
+
try {
|
|
2507
|
+
record = JSON.parse(readFileSync6(evidencePath, "utf8"));
|
|
2508
|
+
} catch {
|
|
2509
|
+
process.stderr.write(`veris: cannot read evidence at ${evidencePath}
|
|
2510
|
+
`);
|
|
2511
|
+
return 1;
|
|
2512
|
+
}
|
|
2513
|
+
const recomputed = computeDigest(
|
|
2514
|
+
record
|
|
2515
|
+
);
|
|
2516
|
+
if (recomputed !== record.digest) {
|
|
2517
|
+
process.stderr.write(
|
|
2518
|
+
"veris: refusing to sign, the record digest does not match its contents.\n"
|
|
2519
|
+
);
|
|
2520
|
+
return 1;
|
|
2521
|
+
}
|
|
2522
|
+
let sig;
|
|
2523
|
+
try {
|
|
2524
|
+
sig = signDigest(record.digest, privateKeyPem);
|
|
2525
|
+
} catch (err) {
|
|
2526
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2527
|
+
process.stderr.write(`veris: signing failed: ${msg}
|
|
2528
|
+
`);
|
|
2529
|
+
return 1;
|
|
2530
|
+
}
|
|
2531
|
+
const out = opts.out ?? `${evidencePath}.sig`;
|
|
2532
|
+
writeFileSync(out, `${JSON.stringify(sig, null, 2)}
|
|
2533
|
+
`, "utf8");
|
|
2534
|
+
process.stdout.write(
|
|
2535
|
+
`Signed ${evidencePath}
|
|
2536
|
+
Wrote ${out} (key ${sig.keyId})
|
|
2537
|
+
`
|
|
2538
|
+
);
|
|
2539
|
+
return 0;
|
|
2540
|
+
}
|
|
2032
2541
|
async function runEvidenceBundle(root, opts = {}) {
|
|
2033
2542
|
const runDir = latestRunDir(root);
|
|
2034
2543
|
if (!runDir) {
|
|
@@ -2038,7 +2547,7 @@ async function runEvidenceBundle(root, opts = {}) {
|
|
|
2038
2547
|
let record;
|
|
2039
2548
|
try {
|
|
2040
2549
|
record = JSON.parse(
|
|
2041
|
-
|
|
2550
|
+
readFileSync6(join12(runDir, "evidence.json"), "utf8")
|
|
2042
2551
|
);
|
|
2043
2552
|
} catch {
|
|
2044
2553
|
process.stderr.write(`veris: no evidence.json in ${runDir}
|
|
@@ -2047,7 +2556,7 @@ async function runEvidenceBundle(root, opts = {}) {
|
|
|
2047
2556
|
}
|
|
2048
2557
|
let report = "";
|
|
2049
2558
|
try {
|
|
2050
|
-
report =
|
|
2559
|
+
report = readFileSync6(
|
|
2051
2560
|
join12(root, ".veris", "reports", `verify-${record.id}.md`),
|
|
2052
2561
|
"utf8"
|
|
2053
2562
|
);
|
|
@@ -2055,7 +2564,15 @@ async function runEvidenceBundle(root, opts = {}) {
|
|
|
2055
2564
|
}
|
|
2056
2565
|
const logIds = record.checks.filter((c) => c.logDigest).map((c) => c.id);
|
|
2057
2566
|
const logs = await readRunLogs(runDir, logIds);
|
|
2058
|
-
|
|
2567
|
+
let signature;
|
|
2568
|
+
const sigPath = join12(runDir, "evidence.json.sig");
|
|
2569
|
+
if (existsSync6(sigPath)) {
|
|
2570
|
+
try {
|
|
2571
|
+
signature = JSON.parse(readFileSync6(sigPath, "utf8"));
|
|
2572
|
+
} catch {
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
const bundle = buildBundle(record, report, logs, signature);
|
|
2059
2576
|
const outDir = await ensureEvidenceDir(root);
|
|
2060
2577
|
const out = opts.out ?? join12(outDir, `${record.id}.bundle.json`);
|
|
2061
2578
|
await writeFile4(out, `${JSON.stringify(bundle, null, 2)}
|
|
@@ -2075,7 +2592,7 @@ async function runEvidenceShow(root, path) {
|
|
|
2075
2592
|
}
|
|
2076
2593
|
let record;
|
|
2077
2594
|
try {
|
|
2078
|
-
record = JSON.parse(
|
|
2595
|
+
record = JSON.parse(readFileSync6(target, "utf8"));
|
|
2079
2596
|
} catch {
|
|
2080
2597
|
process.stderr.write(`veris: cannot read evidence at ${target}
|
|
2081
2598
|
`);
|
|
@@ -2101,10 +2618,187 @@ var init_evidence = __esm({
|
|
|
2101
2618
|
"src/cli/commands/evidence.ts"() {
|
|
2102
2619
|
"use strict";
|
|
2103
2620
|
init_bundle();
|
|
2621
|
+
init_record();
|
|
2622
|
+
init_signing();
|
|
2104
2623
|
init_store();
|
|
2105
2624
|
init_verify_evidence();
|
|
2106
2625
|
init_tty();
|
|
2107
|
-
HONESTY = "An integrity digest confirms the record was not edited or corrupted since it was written.\nIt is not forgery-proof on its own: publish the digest separately (CI log, PR) or sign it (
|
|
2626
|
+
HONESTY = "An integrity digest confirms the record was not edited or corrupted since it was written.\nIt is not forgery-proof on its own: publish the digest separately (CI log, PR) or sign it (veris evidence sign) to prove authorship.";
|
|
2627
|
+
}
|
|
2628
|
+
});
|
|
2629
|
+
|
|
2630
|
+
// src/publish/badge.ts
|
|
2631
|
+
function buildBadge(verdict) {
|
|
2632
|
+
const s = STYLE[verdict.state];
|
|
2633
|
+
return {
|
|
2634
|
+
schemaVersion: 1,
|
|
2635
|
+
label: "veriskit",
|
|
2636
|
+
message: s.message,
|
|
2637
|
+
color: s.color
|
|
2638
|
+
};
|
|
2639
|
+
}
|
|
2640
|
+
var STYLE;
|
|
2641
|
+
var init_badge = __esm({
|
|
2642
|
+
"src/publish/badge.ts"() {
|
|
2643
|
+
"use strict";
|
|
2644
|
+
STYLE = {
|
|
2645
|
+
verified: { message: "verified", color: "14b8a6" },
|
|
2646
|
+
failed: { message: "failed", color: "e5484d" },
|
|
2647
|
+
partial: { message: "partial", color: "f5a623" }
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
});
|
|
2651
|
+
|
|
2652
|
+
// src/cli/commands/badge.ts
|
|
2653
|
+
var badge_exports = {};
|
|
2654
|
+
__export(badge_exports, {
|
|
2655
|
+
runBadge: () => runBadge
|
|
2656
|
+
});
|
|
2657
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
2658
|
+
import { mkdir as mkdir2, writeFile as writeFile5 } from "fs/promises";
|
|
2659
|
+
import { dirname as dirname3, join as join13 } from "path";
|
|
2660
|
+
async function runBadge(root, opts = {}) {
|
|
2661
|
+
const dir = latestRunDir(root);
|
|
2662
|
+
if (!dir) {
|
|
2663
|
+
process.stdout.write("No runs yet. Run `veris verify` first.\n");
|
|
2664
|
+
return 1;
|
|
2665
|
+
}
|
|
2666
|
+
let record;
|
|
2667
|
+
try {
|
|
2668
|
+
record = JSON.parse(
|
|
2669
|
+
readFileSync7(join13(dir, "evidence.json"), "utf8")
|
|
2670
|
+
);
|
|
2671
|
+
} catch {
|
|
2672
|
+
process.stderr.write(`veris: no evidence.json in ${dir}
|
|
2673
|
+
`);
|
|
2674
|
+
return 1;
|
|
2675
|
+
}
|
|
2676
|
+
const out = opts.out ?? join13(root, ".veris", "badge.json");
|
|
2677
|
+
await mkdir2(dirname3(out), { recursive: true });
|
|
2678
|
+
await writeFile5(
|
|
2679
|
+
out,
|
|
2680
|
+
`${JSON.stringify(buildBadge(record.verdict), null, 2)}
|
|
2681
|
+
`,
|
|
2682
|
+
"utf8"
|
|
2683
|
+
);
|
|
2684
|
+
process.stdout.write(`Wrote badge ${out}
|
|
2685
|
+
`);
|
|
2686
|
+
return 0;
|
|
2687
|
+
}
|
|
2688
|
+
var init_badge2 = __esm({
|
|
2689
|
+
"src/cli/commands/badge.ts"() {
|
|
2690
|
+
"use strict";
|
|
2691
|
+
init_store();
|
|
2692
|
+
init_badge();
|
|
2693
|
+
}
|
|
2694
|
+
});
|
|
2695
|
+
|
|
2696
|
+
// src/history/flaky.ts
|
|
2697
|
+
function detectFlaky(records) {
|
|
2698
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2699
|
+
for (const rec of records) {
|
|
2700
|
+
for (const c of rec.checks ?? []) {
|
|
2701
|
+
const arr = byId.get(c.id) ?? [];
|
|
2702
|
+
arr.push(c.status);
|
|
2703
|
+
byId.set(c.id, arr);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
const flaky = [];
|
|
2707
|
+
for (const [id, statuses] of byId) {
|
|
2708
|
+
if (statuses.includes("passed") && statuses.includes("failed")) {
|
|
2709
|
+
flaky.push({ id, statuses });
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
return flaky;
|
|
2713
|
+
}
|
|
2714
|
+
var init_flaky = __esm({
|
|
2715
|
+
"src/history/flaky.ts"() {
|
|
2716
|
+
"use strict";
|
|
2717
|
+
}
|
|
2718
|
+
});
|
|
2719
|
+
|
|
2720
|
+
// src/history/read.ts
|
|
2721
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync8 } from "fs";
|
|
2722
|
+
import { join as join14 } from "path";
|
|
2723
|
+
function loadRuns(root, limit = 20) {
|
|
2724
|
+
const runs = join14(root, ".veris", "runs");
|
|
2725
|
+
let names;
|
|
2726
|
+
try {
|
|
2727
|
+
names = readdirSync5(runs, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name !== "watch").map((d) => d.name);
|
|
2728
|
+
} catch {
|
|
2729
|
+
return [];
|
|
2730
|
+
}
|
|
2731
|
+
const records = [];
|
|
2732
|
+
for (const name of names) {
|
|
2733
|
+
try {
|
|
2734
|
+
const rec = JSON.parse(
|
|
2735
|
+
readFileSync8(join14(runs, name, "evidence.json"), "utf8")
|
|
2736
|
+
);
|
|
2737
|
+
if (rec?.schema === "veriskit/evidence@1") records.push(rec);
|
|
2738
|
+
} catch {
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
records.sort(
|
|
2742
|
+
(a, b) => a.startedAt < b.startedAt ? 1 : a.startedAt > b.startedAt ? -1 : 0
|
|
2743
|
+
);
|
|
2744
|
+
return records.slice(0, limit);
|
|
2745
|
+
}
|
|
2746
|
+
var init_read = __esm({
|
|
2747
|
+
"src/history/read.ts"() {
|
|
2748
|
+
"use strict";
|
|
2749
|
+
}
|
|
2750
|
+
});
|
|
2751
|
+
|
|
2752
|
+
// src/cli/commands/log.ts
|
|
2753
|
+
var log_exports = {};
|
|
2754
|
+
__export(log_exports, {
|
|
2755
|
+
runLog: () => runLog
|
|
2756
|
+
});
|
|
2757
|
+
async function runLog(root, opts = {}) {
|
|
2758
|
+
const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? opts.limit : 20;
|
|
2759
|
+
const records = loadRuns(root, limit);
|
|
2760
|
+
if (records.length === 0) {
|
|
2761
|
+
process.stdout.write("No runs yet. Run `veris verify` first.\n");
|
|
2762
|
+
return 0;
|
|
2763
|
+
}
|
|
2764
|
+
if (opts.flaky) {
|
|
2765
|
+
const flaky = detectFlaky(records);
|
|
2766
|
+
if (flaky.length === 0) {
|
|
2767
|
+
process.stdout.write(
|
|
2768
|
+
`No flaky checks in the last ${records.length} run(s).
|
|
2769
|
+
`
|
|
2770
|
+
);
|
|
2771
|
+
return 0;
|
|
2772
|
+
}
|
|
2773
|
+
process.stdout.write(
|
|
2774
|
+
`Flaky checks (both passed and failed in the last ${records.length} run(s), newest first):
|
|
2775
|
+
`
|
|
2776
|
+
);
|
|
2777
|
+
for (const f of flaky) {
|
|
2778
|
+
process.stdout.write(` ${f.id.padEnd(8)} ${f.statuses.join(" ")}
|
|
2779
|
+
`);
|
|
2780
|
+
}
|
|
2781
|
+
return 0;
|
|
2782
|
+
}
|
|
2783
|
+
for (const rec of records) {
|
|
2784
|
+
const date = rec.startedAt.slice(0, 19).replace("T", " ");
|
|
2785
|
+
const checks = (rec.checks ?? []).map((c) => `${c.id}:${c.status}`).join(" ");
|
|
2786
|
+
const commit = rec.git ? rec.git.commit.slice(0, 7) : "-";
|
|
2787
|
+
process.stdout.write(
|
|
2788
|
+
`${date} ${rec.verdict.state.padEnd(9)} ${checks} ${commit}
|
|
2789
|
+
`
|
|
2790
|
+
);
|
|
2791
|
+
}
|
|
2792
|
+
process.stdout.write(
|
|
2793
|
+
"\nHistory is local to this machine (.veris/runs is gitignored).\n"
|
|
2794
|
+
);
|
|
2795
|
+
return 0;
|
|
2796
|
+
}
|
|
2797
|
+
var init_log = __esm({
|
|
2798
|
+
"src/cli/commands/log.ts"() {
|
|
2799
|
+
"use strict";
|
|
2800
|
+
init_flaky();
|
|
2801
|
+
init_read();
|
|
2108
2802
|
}
|
|
2109
2803
|
});
|
|
2110
2804
|
|
|
@@ -2130,23 +2824,36 @@ function buildCli() {
|
|
|
2130
2824
|
const { runInit: runInit2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
2131
2825
|
process.exitCode = await runInit2(process.cwd());
|
|
2132
2826
|
});
|
|
2133
|
-
cli.command("verify", "Run the full check set and produce a verdict + report").option("--partial-ok", "Exit 0 even when the verdict is partial").
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2827
|
+
cli.command("verify", "Run the full check set and produce a verdict + report").option("--partial-ok", "Exit 0 even when the verdict is partial").option(
|
|
2828
|
+
"--github",
|
|
2829
|
+
"Publish the verdict to the GitHub PR (comment + check run)"
|
|
2830
|
+
).option("--browser", "Also run browser tests (Playwright), when available").action(
|
|
2831
|
+
async (opts) => {
|
|
2832
|
+
const { runVerify: runVerify2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
|
|
2833
|
+
process.exitCode = await runVerify2(process.cwd(), {
|
|
2834
|
+
partialOk: opts.partialOk,
|
|
2835
|
+
github: opts.github,
|
|
2836
|
+
browser: opts.browser
|
|
2837
|
+
});
|
|
2838
|
+
}
|
|
2839
|
+
);
|
|
2139
2840
|
cli.command("report", "Print the latest verification report").action(async () => {
|
|
2140
2841
|
const { runReport: runReport2 } = await Promise.resolve().then(() => (init_report(), report_exports));
|
|
2141
2842
|
process.exitCode = await runReport2(process.cwd());
|
|
2142
2843
|
});
|
|
2143
|
-
cli.command("affected", "Run only the checks affected by changed files").option("--base <ref>", "Compare against a git ref instead of HEAD").option("--partial-ok", "Exit 0 even when the verdict is partial").
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2844
|
+
cli.command("affected", "Run only the checks affected by changed files").option("--base <ref>", "Compare against a git ref instead of HEAD").option("--partial-ok", "Exit 0 even when the verdict is partial").option(
|
|
2845
|
+
"--github",
|
|
2846
|
+
"Publish the verdict to the GitHub PR (comment + check run)"
|
|
2847
|
+
).action(
|
|
2848
|
+
async (opts) => {
|
|
2849
|
+
const { runAffected: runAffected2 } = await Promise.resolve().then(() => (init_affected(), affected_exports));
|
|
2850
|
+
process.exitCode = await runAffected2(process.cwd(), {
|
|
2851
|
+
base: opts.base,
|
|
2852
|
+
partialOk: opts.partialOk,
|
|
2853
|
+
github: opts.github
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
);
|
|
2150
2857
|
cli.command("watch", "Re-run affected checks as files change (Ctrl-C to stop)").option("--poll", "Use mtime polling instead of native fs.watch").action(async (opts) => {
|
|
2151
2858
|
const { runWatch: runWatch2 } = await Promise.resolve().then(() => (init_watch(), watch_exports));
|
|
2152
2859
|
process.exitCode = await runWatch2(process.cwd(), { poll: opts.poll });
|
|
@@ -2164,8 +2871,8 @@ function buildCli() {
|
|
|
2164
2871
|
});
|
|
2165
2872
|
cli.command(
|
|
2166
2873
|
"evidence <action> [file]",
|
|
2167
|
-
"Evidence
|
|
2168
|
-
).option("--out <file>", "For bundle:
|
|
2874
|
+
"Evidence: verify <file> | bundle | show [file] | keygen | sign <file>"
|
|
2875
|
+
).option("--out <file>", "For bundle/keygen/sign: output path").option("--key <file>", "For sign: the private signing key (PEM)").option("--pubkey <file>", "For verify: assert the signer's public key").option("--key-id <id>", "For verify: assert the signer's key id").option("--sig <file>", "For verify: signature path (default <file>.sig)").action(
|
|
2169
2876
|
async (action, file, opts) => {
|
|
2170
2877
|
const mod = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
|
|
2171
2878
|
if (action === "verify") {
|
|
@@ -2174,22 +2881,54 @@ function buildCli() {
|
|
|
2174
2881
|
process.exitCode = 1;
|
|
2175
2882
|
return;
|
|
2176
2883
|
}
|
|
2177
|
-
process.exitCode = await mod.runEvidenceVerify(file
|
|
2884
|
+
process.exitCode = await mod.runEvidenceVerify(file, {
|
|
2885
|
+
pubkey: opts.pubkey,
|
|
2886
|
+
keyId: opts.keyId,
|
|
2887
|
+
sig: opts.sig
|
|
2888
|
+
});
|
|
2178
2889
|
} else if (action === "bundle") {
|
|
2179
2890
|
process.exitCode = await mod.runEvidenceBundle(process.cwd(), {
|
|
2180
2891
|
out: opts.out
|
|
2181
2892
|
});
|
|
2182
2893
|
} else if (action === "show") {
|
|
2183
2894
|
process.exitCode = await mod.runEvidenceShow(process.cwd(), file);
|
|
2895
|
+
} else if (action === "keygen") {
|
|
2896
|
+
process.exitCode = await mod.runEvidenceKeygen(process.cwd(), {
|
|
2897
|
+
out: opts.out
|
|
2898
|
+
});
|
|
2899
|
+
} else if (action === "sign") {
|
|
2900
|
+
if (!file) {
|
|
2901
|
+
process.stderr.write("veris: evidence sign needs a <file>\n");
|
|
2902
|
+
process.exitCode = 1;
|
|
2903
|
+
return;
|
|
2904
|
+
}
|
|
2905
|
+
process.exitCode = await mod.runEvidenceSign(file, {
|
|
2906
|
+
key: opts.key,
|
|
2907
|
+
out: opts.out
|
|
2908
|
+
});
|
|
2184
2909
|
} else {
|
|
2185
2910
|
process.stderr.write(
|
|
2186
|
-
`veris: unknown evidence action '${action}' (use verify | bundle | show)
|
|
2911
|
+
`veris: unknown evidence action '${action}' (use verify | bundle | show | keygen | sign)
|
|
2187
2912
|
`
|
|
2188
2913
|
);
|
|
2189
2914
|
process.exitCode = 1;
|
|
2190
2915
|
}
|
|
2191
2916
|
}
|
|
2192
2917
|
);
|
|
2918
|
+
cli.command(
|
|
2919
|
+
"badge",
|
|
2920
|
+
"Write a shields.io endpoint JSON from the latest verdict"
|
|
2921
|
+
).option("--out <file>", "Output path (default .veris/badge.json)").action(async (opts) => {
|
|
2922
|
+
const { runBadge: runBadge2 } = await Promise.resolve().then(() => (init_badge2(), badge_exports));
|
|
2923
|
+
process.exitCode = await runBadge2(process.cwd(), { out: opts.out });
|
|
2924
|
+
});
|
|
2925
|
+
cli.command("log", "List past verification runs (local history)").option("--limit <n>", "How many runs to show (default 20)").option("--flaky", "Show only checks that flip-flop across runs").action(async (opts) => {
|
|
2926
|
+
const { runLog: runLog2 } = await Promise.resolve().then(() => (init_log(), log_exports));
|
|
2927
|
+
process.exitCode = await runLog2(process.cwd(), {
|
|
2928
|
+
limit: opts.limit ? Number(opts.limit) : void 0,
|
|
2929
|
+
flaky: opts.flaky
|
|
2930
|
+
});
|
|
2931
|
+
});
|
|
2193
2932
|
return { raw: cli, version: VERSION };
|
|
2194
2933
|
}
|
|
2195
2934
|
async function main(argv2) {
|