@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.
Files changed (3) hide show
  1. package/README.md +8 -7
  2. package/dist/index.js +136 -10
  3. 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.5.0 and may trigger Microsoft SmartScreen. The release build removes Node's inherited
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.5.0` is the coordinated release version for npm,
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.5.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.5.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.5.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.5.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 createHash2 } from "crypto";
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 fingerprint = sshPublicKeyFingerprint(parsed.publicKeyBlob);
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 = createHash2("sha256").update(reportArtifact.contents).digest("hex");
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 = createHash2("sha256").update(statement.contents).digest("hex");
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 = createHash2("sha256").update(reportArtifact.contents).digest("hex");
1167
- const attestationSha256 = createHash2("sha256").update(attestationArtifact.contents).digest("hex");
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 = createHash2("sha256").update(reproducedBytes, "utf8").digest("hex");
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cejel/cejel",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "mcpName": "io.github.BargLabs/cejel",
5
5
  "private": false,
6
6
  "publishConfig": {