@cejel/cejel 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -7
- package/dist/index.js +136 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,7 +92,7 @@ if (-not $expected -or $actual -ne $expected.ToLowerInvariant()) {
|
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
> **Windows signing status:** `cejel-Windows-x86_64.exe` is intentionally unsigned in
|
|
95
|
-
> 0.
|
|
95
|
+
> 0.6.0 and may trigger Microsoft SmartScreen. The release build removes Node's inherited
|
|
96
96
|
> signature before SEA injection and fails unless Windows reports the result as `NotSigned`;
|
|
97
97
|
> it does not ship an invalid signature. Before running it, verify `SHA256SUMS` and the
|
|
98
98
|
> GitHub build-provenance attestation. Each binary also has an attached SPDX SBOM and an
|
|
@@ -115,8 +115,9 @@ docker run --rm --network=none -v "$PWD:/w" -w /w -v "$PWD/cejel:/cejel:ro" debi
|
|
|
115
115
|
npx @cejel/cejel@latest .
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
-
> **Distribution note:** Cejel `0.
|
|
119
|
-
> standalone binaries, Docker/OCI, GitHub Action, Homebrew, and MCP Registry.
|
|
118
|
+
> **Distribution note:** Cejel `0.6.0` is the coordinated release version for npm,
|
|
119
|
+
> standalone binaries, Docker/OCI, GitHub Action, Homebrew, and MCP Registry. A GitLab CI
|
|
120
|
+
> template and Code Quality export live in [`docs/gitlab-ci.md`](./docs/gitlab-ci.md).
|
|
120
121
|
|
|
121
122
|
`npx` can reuse a stale cached package. Force the current npm release with the `@latest`
|
|
122
123
|
specifier above, and check the version that will run before comparing certificates:
|
|
@@ -177,16 +178,16 @@ gh attestation verify ./cejel-Windows-x86_64.exe -R BargLabs/cejel
|
|
|
177
178
|
This is cryptographically signed provenance. It is distinct from Apple Developer ID or
|
|
178
179
|
Microsoft Authenticode code-signing.
|
|
179
180
|
|
|
180
|
-
**Docker / OCI.** The current container release is `0.
|
|
181
|
+
**Docker / OCI.** The current container release is `0.6.0`:
|
|
181
182
|
|
|
182
183
|
```bash
|
|
183
|
-
docker run --rm -i -v "$PWD:/workspace:ro" ghcr.io/barglabs/cejel:0.
|
|
184
|
+
docker run --rm -i -v "$PWD:/workspace:ro" ghcr.io/barglabs/cejel:0.6.0
|
|
184
185
|
```
|
|
185
186
|
|
|
186
187
|
The image defaults to `cejel-mcp` over stdio. To use the CLI instead:
|
|
187
188
|
|
|
188
189
|
```bash
|
|
189
|
-
docker run --rm -v "$PWD:/workspace:ro" --entrypoint cejel ghcr.io/barglabs/cejel:0.
|
|
190
|
+
docker run --rm -v "$PWD:/workspace:ro" --entrypoint cejel ghcr.io/barglabs/cejel:0.6.0 .
|
|
190
191
|
```
|
|
191
192
|
|
|
192
193
|
The OCI image carries an SBOM, maximum-mode build provenance, and a signed registry
|
|
@@ -516,7 +517,7 @@ The OCI image is an alternative when Docker is the preferred execution boundary.
|
|
|
516
517
|
the host path with the repository OpenClaw should allow Cejel to read:
|
|
517
518
|
|
|
518
519
|
```bash
|
|
519
|
-
npx -y openclaw@latest mcp set cejel-oci '{"command":"docker","args":["run","--rm","-i","-v","/absolute/path/to/repo:/workspace:ro","ghcr.io/barglabs/cejel:0.
|
|
520
|
+
npx -y openclaw@latest mcp set cejel-oci '{"command":"docker","args":["run","--rm","-i","-v","/absolute/path/to/repo:/workspace:ro","ghcr.io/barglabs/cejel:0.6.0"]}'
|
|
520
521
|
npx -y openclaw@latest mcp doctor cejel-oci --probe
|
|
521
522
|
```
|
|
522
523
|
|
package/dist/index.js
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
|
|
35
35
|
// src/index.ts
|
|
36
36
|
init_esm_shims();
|
|
37
|
-
import { createHash as
|
|
37
|
+
import { createHash as createHash3 } from "crypto";
|
|
38
38
|
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "fs";
|
|
39
39
|
import { dirname, join, resolve } from "path";
|
|
40
40
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
@@ -372,7 +372,7 @@ function verifyWitanIssuanceSignature(options) {
|
|
|
372
372
|
reason: `key type ${JSON.stringify(parsed.keyType)} is not one this build verifies (supported: ${WITAN_ISSUANCE_SUPPORTED_KEY_TYPES.join(", ")}); verify with OpenSSH instead \u2014 see docs/issuance.md`
|
|
373
373
|
};
|
|
374
374
|
}
|
|
375
|
-
const
|
|
375
|
+
const fingerprint2 = sshPublicKeyFingerprint(parsed.publicKeyBlob);
|
|
376
376
|
let signatureVerified;
|
|
377
377
|
try {
|
|
378
378
|
const publicKey = ed25519PublicKeyFromBlob(parsed.publicKeyBlob);
|
|
@@ -407,16 +407,16 @@ function verifyWitanIssuanceSignature(options) {
|
|
|
407
407
|
return { status: "invalid", reason: error instanceof Error ? error.message : String(error) };
|
|
408
408
|
}
|
|
409
409
|
if (signers.length === 0) {
|
|
410
|
-
return { status: "unlisted_key", keyType: parsed.keyType, fingerprint };
|
|
410
|
+
return { status: "unlisted_key", keyType: parsed.keyType, fingerprint: fingerprint2 };
|
|
411
411
|
}
|
|
412
412
|
const signedKeyBase64 = parsed.publicKeyBlob.toString("base64");
|
|
413
413
|
const listed = signers.some(
|
|
414
414
|
(entry) => entry.uninterpretedOptions === void 0 && entry.keyType === parsed.keyType && normalizeBase64(entry.keyBase64) === signedKeyBase64 && entry.principals.includes(expectedPrincipal) && (entry.namespaces === void 0 || entry.namespaces.includes(expectedNamespace))
|
|
415
415
|
);
|
|
416
416
|
if (!listed) {
|
|
417
|
-
return { status: "unlisted_key", keyType: parsed.keyType, fingerprint };
|
|
417
|
+
return { status: "unlisted_key", keyType: parsed.keyType, fingerprint: fingerprint2 };
|
|
418
418
|
}
|
|
419
|
-
return { status: "valid", principal: expectedPrincipal, keyType: parsed.keyType, fingerprint };
|
|
419
|
+
return { status: "valid", principal: expectedPrincipal, keyType: parsed.keyType, fingerprint: fingerprint2 };
|
|
420
420
|
}
|
|
421
421
|
function normalizeBase64(value) {
|
|
422
422
|
return Buffer.from(value, "base64").toString("base64");
|
|
@@ -470,6 +470,71 @@ function parseIssuerPublicKey(contents) {
|
|
|
470
470
|
return { keyType, keyBase64, fingerprint: sshPublicKeyFingerprint(blob) };
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
+
// src/export/gitlab-codequality.ts
|
|
474
|
+
init_esm_shims();
|
|
475
|
+
import { createHash as createHash2 } from "crypto";
|
|
476
|
+
var GITLAB_CODEQUALITY_SEVERITY = {
|
|
477
|
+
critical: "critical",
|
|
478
|
+
warning: "major",
|
|
479
|
+
info: "info"
|
|
480
|
+
};
|
|
481
|
+
function normalizeRepoRelativePath(raw) {
|
|
482
|
+
let path = raw.replaceAll("\\", "/");
|
|
483
|
+
while (path.startsWith("./")) path = path.slice(2);
|
|
484
|
+
if (path.length === 0 || path.startsWith("/") || /^[A-Za-z]:/.test(path)) return void 0;
|
|
485
|
+
if (path.split("/").some((segment) => segment === ".." || segment === "")) return void 0;
|
|
486
|
+
return path;
|
|
487
|
+
}
|
|
488
|
+
function fingerprint(rule, path, line, identity) {
|
|
489
|
+
return createHash2("sha256").update(JSON.stringify([rule, path, line, identity])).digest("hex");
|
|
490
|
+
}
|
|
491
|
+
function exportGitLabCodeQuality(report) {
|
|
492
|
+
const ingestedCount = (report.consumedSignals ?? []).reduce(
|
|
493
|
+
(total, signal) => total + signal.findings.length,
|
|
494
|
+
0
|
|
495
|
+
);
|
|
496
|
+
if (report.verdict === "insufficient_source") {
|
|
497
|
+
return {
|
|
498
|
+
entries: [],
|
|
499
|
+
excludedUnlocated: 0,
|
|
500
|
+
excludedIngested: ingestedCount,
|
|
501
|
+
abstainedReason: report.insufficientSourceReason
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
const entries = [];
|
|
505
|
+
let excludedUnlocated = 0;
|
|
506
|
+
const seen = /* @__PURE__ */ new Set();
|
|
507
|
+
for (const criterion of report.criteria) {
|
|
508
|
+
for (const finding of criterion.findings) {
|
|
509
|
+
const path = finding.evidence.path === void 0 ? void 0 : normalizeRepoRelativePath(finding.evidence.path);
|
|
510
|
+
const line = finding.evidence.line;
|
|
511
|
+
if (path === void 0 || line == null) {
|
|
512
|
+
excludedUnlocated += 1;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
const print = fingerprint(criterion.id, path, line, finding.summary);
|
|
516
|
+
if (seen.has(print)) continue;
|
|
517
|
+
seen.add(print);
|
|
518
|
+
entries.push({
|
|
519
|
+
description: finding.summary,
|
|
520
|
+
check_name: criterion.id,
|
|
521
|
+
fingerprint: print,
|
|
522
|
+
location: { path, lines: { begin: line } },
|
|
523
|
+
severity: GITLAB_CODEQUALITY_SEVERITY[finding.severity]
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return { entries, excludedUnlocated, excludedIngested: ingestedCount };
|
|
528
|
+
}
|
|
529
|
+
function renderGitLabCodeQualityFooter(result) {
|
|
530
|
+
if (result.abstainedReason !== void 0) {
|
|
531
|
+
return `Cejel: abstained report (insufficient_source): exported 0 entries. ${result.abstainedReason}
|
|
532
|
+
`;
|
|
533
|
+
}
|
|
534
|
+
return `Cejel: exported ${result.entries.length} located finding(s); ${result.excludedUnlocated} without a file and line excluded; ${result.excludedIngested} ingested third-party finding(s) not exported.
|
|
535
|
+
`;
|
|
536
|
+
}
|
|
537
|
+
|
|
473
538
|
// src/terminal.ts
|
|
474
539
|
init_esm_shims();
|
|
475
540
|
function renderMinScoreAbstentionFailure(summary, minScore) {
|
|
@@ -739,12 +804,14 @@ Usage:
|
|
|
739
804
|
npx ${NPX_PACKAGE_NAME} [path] [options]
|
|
740
805
|
npx ${NPX_PACKAGE_NAME} scan [path] [options]
|
|
741
806
|
npx ${NPX_PACKAGE_NAME} verify <report.json> <attestation.json> [issuance.json issuance.json.sig]
|
|
807
|
+
npx ${NPX_PACKAGE_NAME} export gitlab-codequality <report.json> [-o gl-code-quality-report.json]
|
|
742
808
|
npx ${NPX_PACKAGE_NAME} issue [path] --report <report.json> --attestation <attestation.json> --key <key.pub> --engagement-ref <ref>
|
|
743
809
|
|
|
744
810
|
Commands:
|
|
745
811
|
scan score a repository (default when the command is omitted)
|
|
746
812
|
verify verify the report/attestation binding; with an issuance pair, also check the issuer
|
|
747
813
|
signature against docs/security/${ISSUER_SIGNERS_FILE} and docs/security/${ISSUER_REVOCATIONS_FILE}
|
|
814
|
+
export write report.json's located findings as a GitLab Code Quality report (offline)
|
|
748
815
|
issue re-run this version at the certificate's revision and, only if report.json reproduces
|
|
749
816
|
byte for byte, write an unsigned issuance.json for the issuer to sign through ssh-agent
|
|
750
817
|
|
|
@@ -789,6 +856,9 @@ async function runWitanCli(args, rubricVersion) {
|
|
|
789
856
|
if (invocation.command === "issue") {
|
|
790
857
|
return runIssue(invocation.options);
|
|
791
858
|
}
|
|
859
|
+
if (invocation.command === "export") {
|
|
860
|
+
return runExport(invocation);
|
|
861
|
+
}
|
|
792
862
|
const options = invocation.options;
|
|
793
863
|
if (options.showVersion) {
|
|
794
864
|
process.stdout.write(`${cliVersion()}
|
|
@@ -890,6 +960,13 @@ function parseCliInvocation(args) {
|
|
|
890
960
|
}
|
|
891
961
|
return parseVerifyArgs(verifyArgs);
|
|
892
962
|
}
|
|
963
|
+
if (command === "export") {
|
|
964
|
+
const exportArgs = args.slice(1);
|
|
965
|
+
if (exportArgs.some((arg) => arg === "-h" || arg === "--help")) {
|
|
966
|
+
return { command: "scan", options: parseArgs(["--help"]) };
|
|
967
|
+
}
|
|
968
|
+
return parseExportArgs(exportArgs);
|
|
969
|
+
}
|
|
893
970
|
if (command === "issue") {
|
|
894
971
|
const issueArgs = args.slice(1);
|
|
895
972
|
if (issueArgs.some((arg) => arg === "-h" || arg === "--help")) {
|
|
@@ -902,6 +979,54 @@ function parseCliInvocation(args) {
|
|
|
902
979
|
}
|
|
903
980
|
return { command: "scan", options: parseArgs(args) };
|
|
904
981
|
}
|
|
982
|
+
var EXPORT_USAGE = () => `Usage: npx ${NPX_PACKAGE_NAME} export gitlab-codequality <report.json> [-o gl-code-quality-report.json]`;
|
|
983
|
+
function parseExportArgs(args) {
|
|
984
|
+
const [format, ...rest] = args;
|
|
985
|
+
if (format !== "gitlab-codequality") throw new Error(EXPORT_USAGE());
|
|
986
|
+
const positionals = [];
|
|
987
|
+
let outPath = "gl-code-quality-report.json";
|
|
988
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
989
|
+
const arg = rest[index];
|
|
990
|
+
if (arg === void 0) continue;
|
|
991
|
+
if (arg === "-o" || arg === "--out") {
|
|
992
|
+
const value = rest[index + 1];
|
|
993
|
+
if (!value) throw new Error(`Missing value for ${arg}`);
|
|
994
|
+
outPath = value;
|
|
995
|
+
index += 1;
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
if (arg.startsWith("-")) throw new Error(`Unknown Cejel export flag: ${arg}`);
|
|
999
|
+
positionals.push(arg);
|
|
1000
|
+
}
|
|
1001
|
+
const [reportPath] = positionals;
|
|
1002
|
+
if (positionals.length !== 1 || !reportPath) throw new Error(EXPORT_USAGE());
|
|
1003
|
+
return {
|
|
1004
|
+
command: "export",
|
|
1005
|
+
format,
|
|
1006
|
+
reportPath: resolve(reportPath),
|
|
1007
|
+
outPath: resolve(outPath)
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
function runExport(invocation) {
|
|
1011
|
+
const reportArtifact = readJsonArtifact(invocation.reportPath, "report");
|
|
1012
|
+
const reportResult = WitanReportSchema.safeParse(reportArtifact.value);
|
|
1013
|
+
if (!reportResult.success) {
|
|
1014
|
+
const members = reportResult.error.issues.map(
|
|
1015
|
+
(issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`
|
|
1016
|
+
);
|
|
1017
|
+
process.stderr.write(
|
|
1018
|
+
`Cejel: report validation failed:
|
|
1019
|
+
${members.map((member) => ` - ${member}`).join("\n")}
|
|
1020
|
+
`
|
|
1021
|
+
);
|
|
1022
|
+
return 1;
|
|
1023
|
+
}
|
|
1024
|
+
const result = exportGitLabCodeQuality(reportResult.data);
|
|
1025
|
+
writeFileSync(invocation.outPath, `${JSON.stringify(result.entries, null, 2)}
|
|
1026
|
+
`, "utf8");
|
|
1027
|
+
process.stderr.write(renderGitLabCodeQualityFooter(result));
|
|
1028
|
+
return 0;
|
|
1029
|
+
}
|
|
905
1030
|
var VERIFY_USAGE = () => `Usage: npx ${NPX_PACKAGE_NAME} verify <report.json> <attestation.json> [issuance.json issuance.json.sig] [--signers <file>] [--revocations <file>]`;
|
|
906
1031
|
function parseVerifyArgs(args) {
|
|
907
1032
|
const positionals = [];
|
|
@@ -1043,7 +1168,7 @@ ${members.map((member) => ` - ${member}`).join("\n")}
|
|
|
1043
1168
|
return 1;
|
|
1044
1169
|
}
|
|
1045
1170
|
const statement = readJsonArtifact(invocation.attestationPath, "attestation");
|
|
1046
|
-
const reportSha256 =
|
|
1171
|
+
const reportSha256 = createHash3("sha256").update(reportArtifact.contents).digest("hex");
|
|
1047
1172
|
const result = verifyWitanAttestationBinding(statement.value, reportResult.data, {
|
|
1048
1173
|
reportSha256
|
|
1049
1174
|
});
|
|
@@ -1060,7 +1185,7 @@ ${result.errors.map((error) => ` - ${error}`).join("\n")}
|
|
|
1060
1185
|
process.stdout.write("Cejel: signature and signer identity were not verified.\n");
|
|
1061
1186
|
return 0;
|
|
1062
1187
|
}
|
|
1063
|
-
const attestationSha256 =
|
|
1188
|
+
const attestationSha256 = createHash3("sha256").update(statement.contents).digest("hex");
|
|
1064
1189
|
return runVerifyIssuance(invocation, {
|
|
1065
1190
|
reportSha256,
|
|
1066
1191
|
attestationSha256,
|
|
@@ -1163,8 +1288,8 @@ ${members.map((member) => ` - ${member}`).join("\n")}
|
|
|
1163
1288
|
}
|
|
1164
1289
|
const suppliedReport = reportResult.data;
|
|
1165
1290
|
const attestationArtifact = readJsonArtifact(options.attestationPath, "attestation");
|
|
1166
|
-
const reportSha256 =
|
|
1167
|
-
const attestationSha256 =
|
|
1291
|
+
const reportSha256 = createHash3("sha256").update(reportArtifact.contents).digest("hex");
|
|
1292
|
+
const attestationSha256 = createHash3("sha256").update(attestationArtifact.contents).digest("hex");
|
|
1168
1293
|
const attestationBinding = verifyWitanAttestationBinding(
|
|
1169
1294
|
attestationArtifact.value,
|
|
1170
1295
|
suppliedReport,
|
|
@@ -1196,7 +1321,7 @@ ${attestationBinding.errors.map((error) => ` - ${error}`).join("\n")}
|
|
|
1196
1321
|
toolVersion: cliVersion()
|
|
1197
1322
|
});
|
|
1198
1323
|
const reproducedBytes = serializeWitanReport(reproduction.report);
|
|
1199
|
-
const reproducedSha256 =
|
|
1324
|
+
const reproducedSha256 = createHash3("sha256").update(reproducedBytes, "utf8").digest("hex");
|
|
1200
1325
|
if (reproducedSha256 !== reportSha256) {
|
|
1201
1326
|
process.stderr.write(
|
|
1202
1327
|
[
|
|
@@ -1417,6 +1542,7 @@ export {
|
|
|
1417
1542
|
WitanReportSchema,
|
|
1418
1543
|
parseArgs,
|
|
1419
1544
|
parseCliInvocation,
|
|
1545
|
+
parseExportArgs,
|
|
1420
1546
|
parseIssueArgs,
|
|
1421
1547
|
parseVerifyArgs,
|
|
1422
1548
|
runWitanFreeCli,
|