@cejel/cejel 0.5.0 → 0.6.1
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 +17 -12
- package/dist/calibration/llm-detector.js +23 -1
- package/dist/{chunk-2453M7C2.js → chunk-UBU3PP7P.js} +123 -18
- package/dist/index.js +146 -13
- package/dist/mcp/index.js +1 -1
- package/dist/packs/d-series/index.js +36 -0
- package/dist/packs/decision-contracts/index.js +11 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -92,11 +92,12 @@ 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.1 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
|
-
> GitHub build-provenance attestation. Each binary also has an attached SPDX SBOM
|
|
99
|
-
>
|
|
98
|
+
> GitHub build-provenance attestation. Each binary also has an attached SPDX SBOM (the packages
|
|
99
|
+
> bundled into it, listed from the build, and the Node.js runtime it embeds; from the release after
|
|
100
|
+
> 0.6.0 it is attested to the binary) and an own-platform verification receipt covering `--version`, `--help`, a real scan,
|
|
100
101
|
> source/binary parity, and a network-denied scan. If your policy requires Authenticode,
|
|
101
102
|
> use the npm package or the OCI image until a human-approved signing path is available.
|
|
102
103
|
|
|
@@ -115,8 +116,9 @@ docker run --rm --network=none -v "$PWD:/w" -w /w -v "$PWD/cejel:/cejel:ro" debi
|
|
|
115
116
|
npx @cejel/cejel@latest .
|
|
116
117
|
```
|
|
117
118
|
|
|
118
|
-
> **Distribution note:** Cejel `0.
|
|
119
|
-
> standalone binaries, Docker/OCI, GitHub Action, Homebrew, and MCP Registry.
|
|
119
|
+
> **Distribution note:** Cejel `0.6.1` is the coordinated release version for npm,
|
|
120
|
+
> standalone binaries, Docker/OCI, GitHub Action, Homebrew, and MCP Registry. A GitLab CI
|
|
121
|
+
> template and Code Quality export live in [`docs/gitlab-ci.md`](./docs/gitlab-ci.md).
|
|
120
122
|
|
|
121
123
|
`npx` can reuse a stale cached package. Force the current npm release with the `@latest`
|
|
122
124
|
specifier above, and check the version that will run before comparing certificates:
|
|
@@ -165,7 +167,9 @@ node dist/index.js .
|
|
|
165
167
|
|
|
166
168
|
Released binaries: `cejel-Darwin-arm64`, `cejel-Darwin-x86_64`, `cejel-Linux-aarch64`,
|
|
167
169
|
`cejel-Linux-x86_64`, and `cejel-Windows-x86_64.exe`. The release also carries
|
|
168
|
-
`SHA256SUMS`, a per-binary SPDX SBOM, and an own-platform verification receipt.
|
|
170
|
+
`SHA256SUMS`, a per-binary SPDX SBOM, and an own-platform verification receipt. The SBOM lists
|
|
171
|
+
the packages bundled into the binary plus its embedded Node.js runtime. The 0.6.0 SBOMs do not: they
|
|
172
|
+
list only the scanned directory and the binary. See [`docs/release-process.md`](docs/release-process.md). Each binary
|
|
169
173
|
is executed against the source build and with networking denied before attachment. Releases
|
|
170
174
|
from v0.1.6 also carry a Sigstore bundle containing GitHub's signed build-provenance
|
|
171
175
|
attestation for the release set. Verify a downloaded binary with:
|
|
@@ -177,20 +181,21 @@ gh attestation verify ./cejel-Windows-x86_64.exe -R BargLabs/cejel
|
|
|
177
181
|
This is cryptographically signed provenance. It is distinct from Apple Developer ID or
|
|
178
182
|
Microsoft Authenticode code-signing.
|
|
179
183
|
|
|
180
|
-
**Docker / OCI.** The current container release is `0.
|
|
184
|
+
**Docker / OCI.** The current container release is `0.6.1`:
|
|
181
185
|
|
|
182
186
|
```bash
|
|
183
|
-
docker run --rm -i -v "$PWD:/workspace:ro" ghcr.io/barglabs/cejel:0.
|
|
187
|
+
docker run --rm -i -v "$PWD:/workspace:ro" ghcr.io/barglabs/cejel:0.6.1
|
|
184
188
|
```
|
|
185
189
|
|
|
186
190
|
The image defaults to `cejel-mcp` over stdio. To use the CLI instead:
|
|
187
191
|
|
|
188
192
|
```bash
|
|
189
|
-
docker run --rm -v "$PWD:/workspace:ro" --entrypoint cejel ghcr.io/barglabs/cejel:0.
|
|
193
|
+
docker run --rm -v "$PWD:/workspace:ro" --entrypoint cejel ghcr.io/barglabs/cejel:0.6.1 .
|
|
190
194
|
```
|
|
191
195
|
|
|
192
|
-
The OCI image carries an SBOM, maximum-mode build provenance, and a
|
|
193
|
-
attestation.
|
|
196
|
+
The OCI image carries an SBOM of its base-image packages, maximum-mode build provenance, and a
|
|
197
|
+
signed registry attestation. From the release after 0.6.0 it also carries a second SPDX attestation
|
|
198
|
+
listing the packages Cejel bundles. The 0.6.0 image's SBOM does not list those.
|
|
194
199
|
|
|
195
200
|
## Leaderboard
|
|
196
201
|
|
|
@@ -516,7 +521,7 @@ The OCI image is an alternative when Docker is the preferred execution boundary.
|
|
|
516
521
|
the host path with the repository OpenClaw should allow Cejel to read:
|
|
517
522
|
|
|
518
523
|
```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.
|
|
524
|
+
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.1"]}'
|
|
520
525
|
npx -y openclaw@latest mcp doctor cejel-oci --probe
|
|
521
526
|
```
|
|
522
527
|
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
deriveProductIdentity,
|
|
6
6
|
runCejelScan,
|
|
7
7
|
serializeWitanReport
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-UBU3PP7P.js";
|
|
9
9
|
import {
|
|
10
10
|
listCejelLlmPackFiles
|
|
11
11
|
} from "../chunk-M6YYPJSN.js";
|
|
@@ -1597,6 +1597,8 @@ function detectSideEffectingToolWithoutAuthorityBoundary(file) {
|
|
|
1597
1597
|
var CEJEL_LLM_ACTION_RULES = [
|
|
1598
1598
|
{
|
|
1599
1599
|
id: "LLM-VAL-001",
|
|
1600
|
+
tier: "semantic",
|
|
1601
|
+
tierEvidence: "python-lineage-rules.ts detectPythonInterproceduralModelOutput() emits this id from a 12-pass fixpoint of taint across local call arguments, returns and tainted-branch control dependence; the JS detectUnvalidatedConsequentialAction() tracks aliases",
|
|
1600
1602
|
title: "Consequential structured action lacks validation",
|
|
1601
1603
|
detectorConfidence: "high",
|
|
1602
1604
|
evidenceContract: "A direct local JSON.parse of recognized model output supplies an object field to a named consequential dispatcher without an intervening fail-closed runtime parse or allowlist guard.",
|
|
@@ -1610,6 +1612,8 @@ var CEJEL_LLM_ACTION_RULES = [
|
|
|
1610
1612
|
},
|
|
1611
1613
|
{
|
|
1612
1614
|
id: "LLM-AGY-001",
|
|
1615
|
+
tier: "semantic",
|
|
1616
|
+
tierEvidence: "boundedTaintedIdentifiers() propagates handler-parameter taint through assignments, destructuring and push, and helperCallSideEffects() follows it into one local helper call (bounded data flow; ambiguous with structural, so the higher tier)",
|
|
1613
1617
|
title: "Side-effecting tool lacks an authority boundary",
|
|
1614
1618
|
detectorConfidence: "high",
|
|
1615
1619
|
evidenceContract: "A complete local tool declaration passes a recognized handler parameter or bounded alias to an import-resolved Node filesystem or child-process mutation API, directly or through one unique local helper, without a preceding fail-closed approval or operation-allowlist guard.",
|
|
@@ -2273,6 +2277,8 @@ function detectPythonSensitivePromptData(file) {
|
|
|
2273
2277
|
var CEJEL_LLM_PYTHON_RULES = [
|
|
2274
2278
|
{
|
|
2275
2279
|
id: "LLM-IOH-001",
|
|
2280
|
+
tier: "semantic",
|
|
2281
|
+
tierEvidence: "python-lineage-rules.ts detectPythonInterproceduralModelOutput() emits this id from a 12-pass fixpoint of taint across local call arguments, returns and tainted-branch control dependence",
|
|
2276
2282
|
title: "Python model output passed to a consequential sink",
|
|
2277
2283
|
detectorConfidence: "high",
|
|
2278
2284
|
evidenceContract: "An official OpenAI or Anthropic Python integration, a supported response-output shape or still-live direct alias in the visible function or class scope, and a direct eval, exec, os.system, or subprocess sink are observable in one non-test source file.",
|
|
@@ -2286,6 +2292,8 @@ var CEJEL_LLM_PYTHON_RULES = [
|
|
|
2286
2292
|
},
|
|
2287
2293
|
{
|
|
2288
2294
|
id: "LLM-AGY-002",
|
|
2295
|
+
tier: "structural",
|
|
2296
|
+
tierEvidence: "detectPythonUnboundedAgentLoop() finds a while True loop, takes its indentation-delimited body and tests for guard presence; no data or control flow",
|
|
2289
2297
|
title: "Unbounded Python agent loop",
|
|
2290
2298
|
detectorConfidence: "medium",
|
|
2291
2299
|
evidenceContract: "A literal while True loop has a complete local indentation-delimited body containing an official SDK call or an abstract model-base response call and no observable mandatory finite counter, deadline, cancellation, or budget exit guard; alternatively, one model-facing agent class declares both tools and an explicitly unset per-run tool-call limit.",
|
|
@@ -2298,6 +2306,8 @@ var CEJEL_LLM_PYTHON_RULES = [
|
|
|
2298
2306
|
},
|
|
2299
2307
|
{
|
|
2300
2308
|
id: "LLM-DAT-001",
|
|
2309
|
+
tier: "structural",
|
|
2310
|
+
tierEvidence: "detectPythonSensitivePromptData() paren-matches the full argument extent of an import-resolved SDK model call across lines, then pattern-tests it for secret-named env reads; no flow",
|
|
2301
2311
|
title: "Sensitive Python environment data passed to a model request",
|
|
2302
2312
|
detectorConfidence: "high",
|
|
2303
2313
|
evidenceContract: "A narrowly named os.environ or os.getenv secret-like value occurs inside the executable, syntactically matched arguments of an official SDK model call after comments and strings are excluded from call extraction.",
|
|
@@ -3936,6 +3946,8 @@ function hasModelJudgeSurface(files) {
|
|
|
3936
3946
|
var CEJEL_LLM_EVALUATION_RULES = [
|
|
3937
3947
|
{
|
|
3938
3948
|
id: "LLM-PRV-001",
|
|
3949
|
+
tier: "semantic",
|
|
3950
|
+
tierEvidence: "latestAssignmentsForReferences() and hasLocalOrResolvedHelperInvocationBefore() resolve the latest in-scope assignment and the calling helper that owns the model invocation, following an aggregate to its emitter (data and call-graph flow within a file)",
|
|
3939
3951
|
title: "Declared evaluation lacks reproducible system provenance",
|
|
3940
3952
|
detectorConfidence: "high",
|
|
3941
3953
|
evidenceContract: "A recognized direct or locally called-helper model invocation precedes a complete local evaluation result that is emitted or returned while lacking model or prompt/policy evaluation-configuration lineage. Recognized invocations include import-resolved SDK calls, a complete authenticated OpenAI-compatible REST request, and a complete local Flowise evaluation request. Supported results are aggregate-to-emitter paths, identifier-bound discrete results with model/case/outcome fields, and incrementally built per-case collections with an evaluation identity and resolved return.",
|
|
@@ -3951,6 +3963,8 @@ var CEJEL_LLM_EVALUATION_RULES = [
|
|
|
3951
3963
|
},
|
|
3952
3964
|
{
|
|
3953
3965
|
id: "LLM-EVL-001",
|
|
3966
|
+
tier: "semantic",
|
|
3967
|
+
tierEvidence: "detectMissingDenominator() follows aggregate assignments and denominator aliases (denominatorAliasAssignments, latestAssignmentsForReferences) to the object that is emitted, within function scopes",
|
|
3954
3968
|
title: "Evaluation result omits its eligible denominator or exclusions",
|
|
3955
3969
|
detectorConfidence: "high",
|
|
3956
3970
|
evidenceContract: "A complete local case-iteration, aggregate-calculation, and direct result-emission path is visible, while the emitted object lacks a denominator or raw case results.",
|
|
@@ -3964,6 +3978,8 @@ var CEJEL_LLM_EVALUATION_RULES = [
|
|
|
3964
3978
|
},
|
|
3965
3979
|
{
|
|
3966
3980
|
id: "LLM-EVL-002",
|
|
3981
|
+
tier: "structural",
|
|
3982
|
+
tierEvidence: "detectSoleSelfJudge() counts producer and judge invocations in one file and resolves a model identifier to its literal binding (literalBindings); detectPythonConfiguredSelfJudge() reads class blocks and self.attribute aliasing; no flow beyond one file",
|
|
3967
3983
|
title: "Evaluated system is its own sole judge",
|
|
3968
3984
|
detectorConfidence: "high",
|
|
3969
3985
|
evidenceContract: "Exactly one local producer and one explicit model-assisted judge resolve to the same literal model identity, a result is locally emitted, and no observable independent review signal participates in evaluation acceptance or gating.",
|
|
@@ -4751,6 +4767,8 @@ function hasUnsafeSinkSurface(file) {
|
|
|
4751
4767
|
var CEJEL_LLM_V1_RULES = [
|
|
4752
4768
|
{
|
|
4753
4769
|
id: "LLM-IOH-001",
|
|
4770
|
+
tier: "semantic",
|
|
4771
|
+
tierEvidence: "detectUnsafeSink() keeps a brace-scope stack and propagates model-output taint through assignments, and javaScriptExecutableHelperParameterSinks() runs a taint fixpoint; the Python path (python-rules.ts) is an interprocedural fixpoint",
|
|
4754
4772
|
title: "Model output passed to a consequential sink",
|
|
4755
4773
|
detectorConfidence: "high",
|
|
4756
4774
|
evidenceContract: "A supported model-output expression, or an identifier whose latest assignment in the visible lexical scope is directly from one, appears on the same line as a supported sink; process sinks additionally require a recognized local import or require binding. A local helper is also supported when one of its parameters is observably interpolated into an executable template or passed to dynamic evaluation. Imported model-facing member-tool handlers are supported when their input parameter or bounded alias reaches an import-resolved execution sink directly, through one unique helper, or by writing an executed local artifact.",
|
|
@@ -4768,6 +4786,8 @@ var CEJEL_LLM_V1_RULES = [
|
|
|
4768
4786
|
},
|
|
4769
4787
|
{
|
|
4770
4788
|
id: "LLM-AGY-002",
|
|
4789
|
+
tier: "structural",
|
|
4790
|
+
tierEvidence: "detectUnboundedLoop() finds a literal while(true) or for(;;), brace-matches its whole body and tests for guard presence and a supported model call inside; no data or control flow",
|
|
4771
4791
|
title: "Unbounded agent loop",
|
|
4772
4792
|
detectorConfidence: "medium",
|
|
4773
4793
|
evidenceContract: "A literal while(true) or for(;;) loop has a complete local brace-delimited body containing a recognized model call and no observable conditional counter, deadline, cancellation, budget, return, throw, or break guard.",
|
|
@@ -4781,6 +4801,8 @@ var CEJEL_LLM_V1_RULES = [
|
|
|
4781
4801
|
},
|
|
4782
4802
|
{
|
|
4783
4803
|
id: "LLM-DAT-001",
|
|
4804
|
+
tier: "structural",
|
|
4805
|
+
tierEvidence: "detectSensitivePromptData() paren-matches the full argument extent of an import-resolved SDK model call across lines, then pattern-tests it for secret-named process.env reads; no flow",
|
|
4784
4806
|
title: "Sensitive data passed to a model request",
|
|
4785
4807
|
detectorConfidence: "high",
|
|
4786
4808
|
evidenceContract: "A narrowly named secret-like process.env value occurs inside the executable, syntactically matched arguments of a recognized model call; comments and string-literal lookalikes are masked before call extraction.",
|
|
@@ -502,9 +502,6 @@ function shortCategoryLabel(category) {
|
|
|
502
502
|
function categoryLabel(category) {
|
|
503
503
|
return category.replaceAll("_", " ");
|
|
504
504
|
}
|
|
505
|
-
function formatCoverageCounts(coverage) {
|
|
506
|
-
return coverage.byCategory.map((entry) => `${shortCategoryLabel(entry.category)} ${entry.measured}/${entry.total}`).join(" \xB7 ");
|
|
507
|
-
}
|
|
508
505
|
function formatCoverageSummary(coverage) {
|
|
509
506
|
const parts = coverage.byCategory.map(
|
|
510
507
|
(entry) => `${categoryLabel(entry.category)} ${entry.measured}/${entry.total}`
|
|
@@ -1368,7 +1365,7 @@ function roundScore(score) {
|
|
|
1368
1365
|
}
|
|
1369
1366
|
|
|
1370
1367
|
// src/witan/certificate-presentation.ts
|
|
1371
|
-
var CALLER_CONTEXT_PRODUCT_IDENTITY_NOTICE = "
|
|
1368
|
+
var CALLER_CONTEXT_PRODUCT_IDENTITY_NOTICE = "The product name and its short ID were given by whoever ran the scan, or taken from package.json or the folder name. They are not evidence from the repository, and they are not covered when Cejel says two scans of the same revision give identical results.";
|
|
1372
1369
|
var CERTIFICATE_SCOPE_NOTICE = "This certificate is a statement about the repository tree at the revision pinned above, not about the system that tree is part of. Evidence outside that tree \u2014 for example, a test suite kept in a separate repository, or CI configuration that changes into a different working directory before running its checks \u2014 is neither seen by this scan nor claimed to be absent.";
|
|
1373
1370
|
var CERTIFICATE_METRIC_REGISTRY = {
|
|
1374
1371
|
test_to_source_ratio: { displayLabel: "Test-to-source file ratio" },
|
|
@@ -1430,7 +1427,7 @@ var CERTIFICATE_GLOSSARY = [
|
|
|
1430
1427
|
{
|
|
1431
1428
|
key: "static-coverage",
|
|
1432
1429
|
term: "Static coverage percentage",
|
|
1433
|
-
definition: "A percentage read from a coverage report or threshold that the repository itself publishes. Cejel does not run the repository's tests. The scoring rubric defines how much credit the published percentage receives.",
|
|
1430
|
+
definition: "A percentage read from a coverage report or threshold that the repository itself publishes. Cejel does not run the repository's tests. The scoring rubric defines how much credit the published percentage receives. When no percentage above 0 is published, the rubric scores this metric as 0 of 100.",
|
|
1434
1431
|
metricNames: ["coverage_percent"]
|
|
1435
1432
|
},
|
|
1436
1433
|
{
|
|
@@ -1670,6 +1667,9 @@ function isCoverageNotMeasured(criterion, metric2) {
|
|
|
1670
1667
|
...criterion.findings.map((finding) => finding.evidence)
|
|
1671
1668
|
].some((evidence) => evidence.kind === "coverage");
|
|
1672
1669
|
}
|
|
1670
|
+
function isCoveragePercentageUnpublished(criterion, metric2) {
|
|
1671
|
+
return metric2.name === "coverage_percent" && metric2.value === 0 && !isCoverageNotMeasured(criterion, metric2);
|
|
1672
|
+
}
|
|
1673
1673
|
function labelAlreadyStatesUnit(label, unit) {
|
|
1674
1674
|
if (!unit) return false;
|
|
1675
1675
|
const escapedUnit = unit.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -1733,7 +1733,10 @@ function formatEnumerableMetric(metric2) {
|
|
|
1733
1733
|
}
|
|
1734
1734
|
function formatCertificateMetricValue(criterion, metric2) {
|
|
1735
1735
|
if (isCoverageNotMeasured(criterion, metric2)) {
|
|
1736
|
-
return "no coverage report found
|
|
1736
|
+
return "no coverage report or threshold found, so there is no percentage to read (Cejel does not run tests); the rubric scores this as 0 of 100";
|
|
1737
|
+
}
|
|
1738
|
+
if (isCoveragePercentageUnpublished(criterion, metric2)) {
|
|
1739
|
+
return "coverage configuration found, but no published coverage percentage above 0 (Cejel reads published reports and thresholds and does not run tests); the rubric scores this as 0 of 100";
|
|
1737
1740
|
}
|
|
1738
1741
|
if (metric2.name === "test_to_source_ratio" && metric2.max !== void 0) {
|
|
1739
1742
|
const tests = formatMetricNumber(metric2.value);
|
|
@@ -1803,7 +1806,12 @@ function buildNextSteps(report, unmeasured, notApplicable, a1, coverageMetric) {
|
|
|
1803
1806
|
}
|
|
1804
1807
|
if (a1 && coverageMetric && isCoverageNotMeasured(a1, coverageMetric)) {
|
|
1805
1808
|
steps.push(
|
|
1806
|
-
"Obtain a coverage report or configured threshold for A1's static coverage metric; none was found, so
|
|
1809
|
+
"Obtain a coverage report or configured threshold for A1's static coverage metric; none was found, so no percentage was measured."
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
if (a1 && coverageMetric && isCoveragePercentageUnpublished(a1, coverageMetric)) {
|
|
1813
|
+
steps.push(
|
|
1814
|
+
"Publish a coverage report or threshold for A1's static coverage metric; coverage configuration was found, but no published percentage above 0."
|
|
1807
1815
|
);
|
|
1808
1816
|
}
|
|
1809
1817
|
if (!report.repo.headSha && report.criteria.some(
|
|
@@ -1825,10 +1833,10 @@ function buildNextSteps(report, unmeasured, notApplicable, a1, coverageMetric) {
|
|
|
1825
1833
|
`Resolve the ${scanLimitationCount} scan limitation${scanLimitationCount === 1 ? "" : "s"} qualifying the readable evidence, then reproduce the scan.`
|
|
1826
1834
|
);
|
|
1827
1835
|
}
|
|
1828
|
-
const
|
|
1829
|
-
if (
|
|
1836
|
+
const actionableSkips = actionableSkipReasons(report.contentReadSummary);
|
|
1837
|
+
if (actionableSkips.count > 0) {
|
|
1830
1838
|
steps.push(
|
|
1831
|
-
`
|
|
1839
|
+
`Resolve the ${actionableSkips.count} content entr${actionableSkips.count === 1 ? "y" : "ies"} skipped as ${joinWithOr(actionableSkips.reasons)} (see the itemized reasons in the certificate), then reproduce the scan.`
|
|
1832
1840
|
);
|
|
1833
1841
|
}
|
|
1834
1842
|
if (steps.length === 0) {
|
|
@@ -1838,6 +1846,76 @@ function buildNextSteps(report, unmeasured, notApplicable, a1, coverageMetric) {
|
|
|
1838
1846
|
}
|
|
1839
1847
|
return steps;
|
|
1840
1848
|
}
|
|
1849
|
+
function joinWithConjunction(items, conjunction) {
|
|
1850
|
+
if (items.length <= 1) return items.join("");
|
|
1851
|
+
return `${items.slice(0, -1).join(", ")} ${conjunction} ${items[items.length - 1]}`;
|
|
1852
|
+
}
|
|
1853
|
+
function joinWithOr(items) {
|
|
1854
|
+
return joinWithConjunction(items, "or");
|
|
1855
|
+
}
|
|
1856
|
+
function joinWithAnd(items) {
|
|
1857
|
+
return joinWithConjunction(items, "and");
|
|
1858
|
+
}
|
|
1859
|
+
var ACTIONABLE_SKIP_REASONS = [
|
|
1860
|
+
{ key: "unreadable", label: "unreadable" },
|
|
1861
|
+
{ key: "tooLarge", label: "too large" },
|
|
1862
|
+
{ key: "deniedPath", label: "a denied path" },
|
|
1863
|
+
{ key: "nonRegularFile", label: "not a regular file" }
|
|
1864
|
+
];
|
|
1865
|
+
function actionableSkipReasons(summary) {
|
|
1866
|
+
if (!summary) return { count: 0, reasons: [] };
|
|
1867
|
+
const present = ACTIONABLE_SKIP_REASONS.filter(({ key }) => summary.byReason[key] > 0);
|
|
1868
|
+
return {
|
|
1869
|
+
count: present.reduce((sum, { key }) => sum + summary.byReason[key], 0),
|
|
1870
|
+
reasons: present.map(({ label }) => label)
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
function contentEntries(count, singular, plural) {
|
|
1874
|
+
return `${count} content entr${count === 1 ? `y ${singular}` : `ies ${plural}`}`;
|
|
1875
|
+
}
|
|
1876
|
+
function formatMeasuredDimensionCounts(coverage) {
|
|
1877
|
+
return coverage.byCategory.map((entry) => `${shortCategoryLabel(entry.category)} ${entry.measured} of ${entry.total}`).join(", ");
|
|
1878
|
+
}
|
|
1879
|
+
function notApplicableCount(report) {
|
|
1880
|
+
return report.criteria.filter((criterion) => criterion.status === "not_applicable").length;
|
|
1881
|
+
}
|
|
1882
|
+
function formatDimensionsMeasuredSentence(report) {
|
|
1883
|
+
const notApplicable = notApplicableCount(report);
|
|
1884
|
+
const counts = `Dimensions measured: ${formatMeasuredDimensionCounts(computeMeasuredCoverage(report))}.`;
|
|
1885
|
+
return notApplicable > 0 ? `${counts} ${notApplicable} do not apply to this repository.` : counts;
|
|
1886
|
+
}
|
|
1887
|
+
function formatLowConfidenceSentence(report) {
|
|
1888
|
+
const coverage = computeMeasuredCoverage(report);
|
|
1889
|
+
if (!coverage.lowConfidence) return void 0;
|
|
1890
|
+
const isLow = (counts) => counts.total > 0 && counts.measured / counts.total < LOW_CONFIDENCE_COVERAGE_THRESHOLD;
|
|
1891
|
+
const parts = [
|
|
1892
|
+
...coverage.byCategory.filter(isLow).map((entry) => `${shortCategoryLabel(entry.category)} (${entry.measured} of ${entry.total})`),
|
|
1893
|
+
...isLow(coverage.overall) ? [`overall (${coverage.overall.measured} of ${coverage.overall.total})`] : []
|
|
1894
|
+
];
|
|
1895
|
+
const sentence = `Low confidence: fewer than half of the ${joinWithAnd(parts)} dimensions were measured.`;
|
|
1896
|
+
return notApplicableCount(report) > 0 ? `${sentence} Dimensions that do not apply count as not measured here.` : sentence;
|
|
1897
|
+
}
|
|
1898
|
+
var SCORED_VERDICT_BANDS = {
|
|
1899
|
+
conditional: { label: "Conditional", band: "2.5 up to 3.5" },
|
|
1900
|
+
at_risk: { label: "At risk", band: "1.5 up to 2.5" },
|
|
1901
|
+
unverified: { label: "Unverified", band: "below 1.5" }
|
|
1902
|
+
};
|
|
1903
|
+
var MAX_LOWEST_DIMENSIONS = 3;
|
|
1904
|
+
function buildVerdictExplanation(report) {
|
|
1905
|
+
if (report.verdict === "insufficient_source" || report.verdict === "verified") return void 0;
|
|
1906
|
+
if (report.overallScore === null) return void 0;
|
|
1907
|
+
const hasCriticalOrWarning = report.criteria.some(
|
|
1908
|
+
(criterion) => criterion.findings.some((finding) => finding.severity === "critical" || finding.severity === "warning")
|
|
1909
|
+
);
|
|
1910
|
+
if (hasCriticalOrWarning) return void 0;
|
|
1911
|
+
const { label, band } = SCORED_VERDICT_BANDS[report.verdict];
|
|
1912
|
+
const lowest = report.criteria.map((criterion, index) => ({ criterion, index })).filter(
|
|
1913
|
+
({ criterion }) => isMeasuredCriterionStatus(criterion.status) && witanVerdictForScore(criterion.score) !== "verified"
|
|
1914
|
+
).sort((a, b) => a.criterion.score - b.criterion.score || a.index - b.index).slice(0, MAX_LOWEST_DIMENSIONS).map(({ criterion }) => `${criterion.id} (${criterion.score.toFixed(1)})`);
|
|
1915
|
+
const verdictSentence = `The ${label} verdict comes from the overall score of ${report.overallScore.toFixed(1)} out of 4.0, which is in the ${label} band (${band}). No finding set it.`;
|
|
1916
|
+
if (lowest.length === 0) return verdictSentence;
|
|
1917
|
+
return lowest.length === 1 ? `${verdictSentence} The measured dimension scoring lowest is ${lowest[0]}.` : `${verdictSentence} The measured dimensions scoring lowest are ${joinWithAnd(lowest)}.`;
|
|
1918
|
+
}
|
|
1841
1919
|
var WITHHELD_PATH_READ_FAILURE_REASONS = /* @__PURE__ */ new Set([
|
|
1842
1920
|
"unreadable",
|
|
1843
1921
|
"denied_path"
|
|
@@ -1876,6 +1954,13 @@ function buildWithheldPathsGapSentence(report) {
|
|
|
1876
1954
|
const reportsOnWhatItRead = signals.length === 1 ? "that signal reports on what it read" : "these signals report on what they read";
|
|
1877
1955
|
return `${count} ${entryWord} withheld (${reasonLabel}); ${signalList} would have read ${pronoun}. Under this rubric ${reportsOnWhatItRead}; the prospective v23 rubric abstains instead.`;
|
|
1878
1956
|
}
|
|
1957
|
+
function describeEstablishedDimensions(report, measured, applicable) {
|
|
1958
|
+
const total = report.criteria.length;
|
|
1959
|
+
const counts = formatMeasuredDimensionCounts(computeMeasuredCoverage(report));
|
|
1960
|
+
const measuredSentence = `The report measured ${measured} of the ${total} rubric dimensions (${counts}).`;
|
|
1961
|
+
const notApplicable = total - applicable;
|
|
1962
|
+
return notApplicable > 0 ? `${measuredSentence} ${notApplicable} do not apply to this repository; ${measured} of the ${applicable} that apply were measured.` : `${measuredSentence} All ${total} apply to this repository.`;
|
|
1963
|
+
}
|
|
1879
1964
|
function buildRelyingPartySummary(report) {
|
|
1880
1965
|
const applicable = report.criteria.filter((criterion) => criterion.status !== "not_applicable");
|
|
1881
1966
|
const measured = applicable.filter((criterion) => criterion.status !== "insufficient_data");
|
|
@@ -1886,7 +1971,12 @@ function buildRelyingPartySummary(report) {
|
|
|
1886
1971
|
const gaps = [];
|
|
1887
1972
|
if (a1 && coverageMetric && isCoverageNotMeasured(a1, coverageMetric)) {
|
|
1888
1973
|
gaps.push(
|
|
1889
|
-
"No coverage report or configured threshold was found, so static coverage was
|
|
1974
|
+
"No coverage report or configured threshold was found, so no static coverage percentage was measured and the rubric scored it as 0 of 100; Cejel does not run the subject's tests."
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
if (a1 && coverageMetric && isCoveragePercentageUnpublished(a1, coverageMetric)) {
|
|
1978
|
+
gaps.push(
|
|
1979
|
+
"Coverage configuration was found, but no coverage report or threshold in this tree publishes a percentage above 0, so the rubric scored static coverage as 0 of 100; Cejel does not run the subject's tests."
|
|
1890
1980
|
);
|
|
1891
1981
|
}
|
|
1892
1982
|
if (!report.repo.headSha && report.criteria.some(
|
|
@@ -1910,10 +2000,16 @@ function buildRelyingPartySummary(report) {
|
|
|
1910
2000
|
const count = report.scanLimitations?.length ?? 0;
|
|
1911
2001
|
gaps.push(`${count} scan limitation${count === 1 ? "" : "s"} qualifies the readable evidence.`);
|
|
1912
2002
|
}
|
|
1913
|
-
|
|
1914
|
-
|
|
2003
|
+
const actionableSkips = actionableSkipReasons(report.contentReadSummary).count;
|
|
2004
|
+
if (actionableSkips > 0) {
|
|
2005
|
+
gaps.push(
|
|
2006
|
+
`${contentEntries(actionableSkips, "was", "were")} skipped for the reasons itemized in the certificate.`
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
const excludedByExtension = report.contentReadSummary?.byReason.excludedByExtension ?? 0;
|
|
2010
|
+
if (excludedByExtension > 0) {
|
|
1915
2011
|
gaps.push(
|
|
1916
|
-
`${
|
|
2012
|
+
`${contentEntries(excludedByExtension, "was", "were")} not read because ${excludedByExtension === 1 ? "its file type is" : "their file types are"} outside what Cejel reads. This is a fixed limit of the tool, not something the repository needs to fix.`
|
|
1917
2013
|
);
|
|
1918
2014
|
}
|
|
1919
2015
|
const withheldPathsSentence = buildWithheldPathsGapSentence(report);
|
|
@@ -1926,7 +2022,7 @@ function buildRelyingPartySummary(report) {
|
|
|
1926
2022
|
return {
|
|
1927
2023
|
scope: CERTIFICATE_SCOPE_NOTICE,
|
|
1928
2024
|
examined: `Cejel examined the repository evidence recorded for ${report.productDisplayName}${revision} under ${report.rubricVersion}.`,
|
|
1929
|
-
established:
|
|
2025
|
+
established: `${describeEstablishedDimensions(report, measured.length, applicable.length)} It recorded ${findings} evidence-backed finding${findings === 1 ? "" : "s"}.`,
|
|
1930
2026
|
notEstablished: gaps.join(" "),
|
|
1931
2027
|
next: buildNextSteps(report, unmeasured, notApplicable, a1, coverageMetric).join(" ")
|
|
1932
2028
|
};
|
|
@@ -2022,6 +2118,7 @@ function renderWitanHtmlReport(report, options = {}) {
|
|
|
2022
2118
|
const coverage = computeMeasuredCoverage(report);
|
|
2023
2119
|
const gitHistoryUnavailable = !report.repo.headSha;
|
|
2024
2120
|
const relyingPartySummary = buildRelyingPartySummary(report);
|
|
2121
|
+
const lowConfidenceSentence = formatLowConfidenceSentence(report);
|
|
2025
2122
|
return `<!doctype html>
|
|
2026
2123
|
<html lang="en">
|
|
2027
2124
|
<head>
|
|
@@ -2043,7 +2140,6 @@ function renderWitanHtmlReport(report, options = {}) {
|
|
|
2043
2140
|
<p class="eyebrow">Trust Certificate</p>
|
|
2044
2141
|
<h1>${escapeHtml(report.productDisplayName)}</h1>
|
|
2045
2142
|
<dl class="meta">
|
|
2046
|
-
<div><dt>Product identity</dt><dd>${escapeHtml(CALLER_CONTEXT_PRODUCT_IDENTITY_NOTICE)}</dd></div>
|
|
2047
2143
|
${options.generatedAt ? `<div><dt>Date</dt><dd>${escapeHtml(formatDate(options.generatedAt))}</dd></div>` : ""}
|
|
2048
2144
|
<div><dt>Run</dt><dd>${escapeHtml(renderRepo(report))}</dd></div>
|
|
2049
2145
|
<div><dt>CLI</dt><dd>${escapeHtml(options.cliVersion ? `Cejel ${options.cliVersion}` : "Not recorded")}</dd></div>
|
|
@@ -2052,6 +2148,7 @@ function renderWitanHtmlReport(report, options = {}) {
|
|
|
2052
2148
|
${// Gated on the rubric actually being prospective — a calibrated (v17) certificate's
|
|
2053
2149
|
// HTML stays byte-identical to before --rubric-pin existed.
|
|
2054
2150
|
isProspectiveRubricVersion(report.rubricVersion) ? `<div><dt>Calibration</dt><dd class="prospective-rubric-notice">${escapeHtml(PROSPECTIVE_RUBRIC_NOTICE)}</dd></div>` : ""}
|
|
2151
|
+
<div><dt>About the name</dt><dd>${escapeHtml(CALLER_CONTEXT_PRODUCT_IDENTITY_NOTICE)}</dd></div>
|
|
2055
2152
|
${contributingSources.length > 0 ? `<div><dt>Sources</dt><dd>Incorporates findings from: ${escapeHtml(contributingSources.join(", "))}<ul class="source-counts">${externalSourceSummaries.map((s) => `<li>${escapeHtml(formatSourceProvenanceLine(s))}</li>`).join("")}</ul></dd></div>` : ""}
|
|
2056
2153
|
</dl>
|
|
2057
2154
|
</section>
|
|
@@ -2065,7 +2162,8 @@ function renderWitanHtmlReport(report, options = {}) {
|
|
|
2065
2162
|
<span>Code ${renderCategoryScore(report.codeTrustScore, coverage, "code_trust")}</span>
|
|
2066
2163
|
<span>Process ${renderCategoryScore(report.processTrustScore, coverage, "process_trust")}</span>
|
|
2067
2164
|
</div>
|
|
2068
|
-
<div class="coverage-note">${escapeHtml(
|
|
2165
|
+
<div class="coverage-note">${escapeHtml(formatDimensionsMeasuredSentence(report))}</div>${lowConfidenceSentence ? `
|
|
2166
|
+
<div class="coverage-note">${escapeHtml(lowConfidenceSentence)}</div>` : ""}`}
|
|
2069
2167
|
</aside>
|
|
2070
2168
|
</div>
|
|
2071
2169
|
</header>
|
|
@@ -2128,9 +2226,12 @@ function collectFindingsFirstEntries(report) {
|
|
|
2128
2226
|
function renderFindingsFirstSummary(report) {
|
|
2129
2227
|
const entries = collectFindingsFirstEntries(report);
|
|
2130
2228
|
const body = entries.length > 0 ? `<ul>${entries.map(({ criterion, finding }) => `<li>${renderFindingEvidence(criterion, finding)}</li>`).join("")}</ul>` : '<p class="findings-first-empty">No critical or warning findings were identified across any criterion in this scan.</p>';
|
|
2229
|
+
const verdictExplanation = buildVerdictExplanation(report);
|
|
2230
|
+
const explanation = verdictExplanation ? `
|
|
2231
|
+
<p class="verdict-explanation">${escapeHtml(verdictExplanation)}</p>` : "";
|
|
2131
2232
|
return `<section class="findings-first-summary" aria-labelledby="findings-first-heading">
|
|
2132
2233
|
<h2 id="findings-first-heading">Critical and warning findings</h2>
|
|
2133
|
-
${body}
|
|
2234
|
+
${body}${explanation}
|
|
2134
2235
|
</section>`;
|
|
2135
2236
|
}
|
|
2136
2237
|
function renderRelyingPartySummary(summary) {
|
|
@@ -2457,6 +2558,7 @@ dd { margin: 0; color: var(--muted); overflow-wrap: anywhere; }
|
|
|
2457
2558
|
.findings-first-summary { margin-top: 20px; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--surface); padding: 22px; }
|
|
2458
2559
|
.findings-first-summary ul { margin: 0; padding-left: 18px; display: grid; gap: 10px; }
|
|
2459
2560
|
.findings-first-empty { margin: 0; color: var(--muted); }
|
|
2561
|
+
.verdict-explanation { margin: 10px 0 0; color: var(--muted); }
|
|
2460
2562
|
.relying-party-summary, .glossary { margin-top: 28px; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--surface); padding: 22px; }
|
|
2461
2563
|
.relying-party-summary dl { display: grid; gap: 14px; margin: 0; }
|
|
2462
2564
|
.relying-party-summary dl div { display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 18px; }
|
|
@@ -9937,6 +10039,9 @@ export {
|
|
|
9937
10039
|
formatCertificateMetricLabel,
|
|
9938
10040
|
formatCertificateMetricAppliedWeightPercent,
|
|
9939
10041
|
formatCertificateMetricValue,
|
|
10042
|
+
formatDimensionsMeasuredSentence,
|
|
10043
|
+
formatLowConfidenceSentence,
|
|
10044
|
+
buildVerdictExplanation,
|
|
9940
10045
|
buildRelyingPartySummary,
|
|
9941
10046
|
renderWitanHtmlReport,
|
|
9942
10047
|
renderWitanBadgeEndpoint,
|
package/dist/index.js
CHANGED
|
@@ -9,13 +9,16 @@ import {
|
|
|
9
9
|
WitanReportSchema,
|
|
10
10
|
assertSelectableRubricVersion,
|
|
11
11
|
buildRelyingPartySummary,
|
|
12
|
+
buildVerdictExplanation,
|
|
12
13
|
computeApplicableMeasuredCoverage,
|
|
13
14
|
createWitanAttestation,
|
|
14
15
|
formatCertificateMetricAppliedWeightPercent,
|
|
15
16
|
formatCertificateMetricLabel,
|
|
16
17
|
formatCertificateMetricValue,
|
|
17
18
|
formatCoverageSummary,
|
|
19
|
+
formatDimensionsMeasuredSentence,
|
|
18
20
|
formatExternalSourceLine,
|
|
21
|
+
formatLowConfidenceSentence,
|
|
19
22
|
isProspectiveRubricVersion,
|
|
20
23
|
renderWitanBadgeEndpoint,
|
|
21
24
|
renderWitanBadgeSvg,
|
|
@@ -23,7 +26,7 @@ import {
|
|
|
23
26
|
runCejelScan,
|
|
24
27
|
serializeWitanReport,
|
|
25
28
|
verifyWitanAttestationBinding
|
|
26
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-UBU3PP7P.js";
|
|
27
30
|
import "./chunk-63XQDYQ5.js";
|
|
28
31
|
import {
|
|
29
32
|
external_exports
|
|
@@ -34,7 +37,7 @@ import {
|
|
|
34
37
|
|
|
35
38
|
// src/index.ts
|
|
36
39
|
init_esm_shims();
|
|
37
|
-
import { createHash as
|
|
40
|
+
import { createHash as createHash3 } from "crypto";
|
|
38
41
|
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "fs";
|
|
39
42
|
import { dirname, join, resolve } from "path";
|
|
40
43
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
@@ -372,7 +375,7 @@ function verifyWitanIssuanceSignature(options) {
|
|
|
372
375
|
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
376
|
};
|
|
374
377
|
}
|
|
375
|
-
const
|
|
378
|
+
const fingerprint2 = sshPublicKeyFingerprint(parsed.publicKeyBlob);
|
|
376
379
|
let signatureVerified;
|
|
377
380
|
try {
|
|
378
381
|
const publicKey = ed25519PublicKeyFromBlob(parsed.publicKeyBlob);
|
|
@@ -407,16 +410,16 @@ function verifyWitanIssuanceSignature(options) {
|
|
|
407
410
|
return { status: "invalid", reason: error instanceof Error ? error.message : String(error) };
|
|
408
411
|
}
|
|
409
412
|
if (signers.length === 0) {
|
|
410
|
-
return { status: "unlisted_key", keyType: parsed.keyType, fingerprint };
|
|
413
|
+
return { status: "unlisted_key", keyType: parsed.keyType, fingerprint: fingerprint2 };
|
|
411
414
|
}
|
|
412
415
|
const signedKeyBase64 = parsed.publicKeyBlob.toString("base64");
|
|
413
416
|
const listed = signers.some(
|
|
414
417
|
(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
418
|
);
|
|
416
419
|
if (!listed) {
|
|
417
|
-
return { status: "unlisted_key", keyType: parsed.keyType, fingerprint };
|
|
420
|
+
return { status: "unlisted_key", keyType: parsed.keyType, fingerprint: fingerprint2 };
|
|
418
421
|
}
|
|
419
|
-
return { status: "valid", principal: expectedPrincipal, keyType: parsed.keyType, fingerprint };
|
|
422
|
+
return { status: "valid", principal: expectedPrincipal, keyType: parsed.keyType, fingerprint: fingerprint2 };
|
|
420
423
|
}
|
|
421
424
|
function normalizeBase64(value) {
|
|
422
425
|
return Buffer.from(value, "base64").toString("base64");
|
|
@@ -470,6 +473,71 @@ function parseIssuerPublicKey(contents) {
|
|
|
470
473
|
return { keyType, keyBase64, fingerprint: sshPublicKeyFingerprint(blob) };
|
|
471
474
|
}
|
|
472
475
|
|
|
476
|
+
// src/export/gitlab-codequality.ts
|
|
477
|
+
init_esm_shims();
|
|
478
|
+
import { createHash as createHash2 } from "crypto";
|
|
479
|
+
var GITLAB_CODEQUALITY_SEVERITY = {
|
|
480
|
+
critical: "critical",
|
|
481
|
+
warning: "major",
|
|
482
|
+
info: "info"
|
|
483
|
+
};
|
|
484
|
+
function normalizeRepoRelativePath(raw) {
|
|
485
|
+
let path = raw.replaceAll("\\", "/");
|
|
486
|
+
while (path.startsWith("./")) path = path.slice(2);
|
|
487
|
+
if (path.length === 0 || path.startsWith("/") || /^[A-Za-z]:/.test(path)) return void 0;
|
|
488
|
+
if (path.split("/").some((segment) => segment === ".." || segment === "")) return void 0;
|
|
489
|
+
return path;
|
|
490
|
+
}
|
|
491
|
+
function fingerprint(rule, path, line, identity) {
|
|
492
|
+
return createHash2("sha256").update(JSON.stringify([rule, path, line, identity])).digest("hex");
|
|
493
|
+
}
|
|
494
|
+
function exportGitLabCodeQuality(report) {
|
|
495
|
+
const ingestedCount = (report.consumedSignals ?? []).reduce(
|
|
496
|
+
(total, signal) => total + signal.findings.length,
|
|
497
|
+
0
|
|
498
|
+
);
|
|
499
|
+
if (report.verdict === "insufficient_source") {
|
|
500
|
+
return {
|
|
501
|
+
entries: [],
|
|
502
|
+
excludedUnlocated: 0,
|
|
503
|
+
excludedIngested: ingestedCount,
|
|
504
|
+
abstainedReason: report.insufficientSourceReason
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
const entries = [];
|
|
508
|
+
let excludedUnlocated = 0;
|
|
509
|
+
const seen = /* @__PURE__ */ new Set();
|
|
510
|
+
for (const criterion of report.criteria) {
|
|
511
|
+
for (const finding of criterion.findings) {
|
|
512
|
+
const path = finding.evidence.path === void 0 ? void 0 : normalizeRepoRelativePath(finding.evidence.path);
|
|
513
|
+
const line = finding.evidence.line;
|
|
514
|
+
if (path === void 0 || line == null) {
|
|
515
|
+
excludedUnlocated += 1;
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const print = fingerprint(criterion.id, path, line, finding.summary);
|
|
519
|
+
if (seen.has(print)) continue;
|
|
520
|
+
seen.add(print);
|
|
521
|
+
entries.push({
|
|
522
|
+
description: finding.summary,
|
|
523
|
+
check_name: criterion.id,
|
|
524
|
+
fingerprint: print,
|
|
525
|
+
location: { path, lines: { begin: line } },
|
|
526
|
+
severity: GITLAB_CODEQUALITY_SEVERITY[finding.severity]
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return { entries, excludedUnlocated, excludedIngested: ingestedCount };
|
|
531
|
+
}
|
|
532
|
+
function renderGitLabCodeQualityFooter(result) {
|
|
533
|
+
if (result.abstainedReason !== void 0) {
|
|
534
|
+
return `Cejel: abstained report (insufficient_source): exported 0 entries. ${result.abstainedReason}
|
|
535
|
+
`;
|
|
536
|
+
}
|
|
537
|
+
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.
|
|
538
|
+
`;
|
|
539
|
+
}
|
|
540
|
+
|
|
473
541
|
// src/terminal.ts
|
|
474
542
|
init_esm_shims();
|
|
475
543
|
function renderMinScoreAbstentionFailure(summary, minScore) {
|
|
@@ -510,13 +578,15 @@ function renderTerminalCertificate(summary, report) {
|
|
|
510
578
|
`Overall: ${formatScore(summary.overallScore)}/4.0 (${summary.verdict})`,
|
|
511
579
|
` Code trust: ${formatScore(summary.codeTrustScore)}/4.0`,
|
|
512
580
|
` Process trust: ${formatScore(summary.processTrustScore)}/4.0`,
|
|
581
|
+
...report ? [` ${formatDimensionsMeasuredSentence(report)}`] : [],
|
|
582
|
+
...report && formatLowConfidenceSentence(report) ? [` ${formatLowConfidenceSentence(report)}`] : [],
|
|
513
583
|
""
|
|
514
584
|
];
|
|
515
|
-
lines.splice(1, 0, `
|
|
585
|
+
lines.splice(lines.length - 1, 0, `About the name: ${CALLER_CONTEXT_PRODUCT_IDENTITY_NOTICE}`);
|
|
516
586
|
if (report) {
|
|
517
587
|
const relyingPartySummary = buildRelyingPartySummary(report);
|
|
518
588
|
lines.splice(
|
|
519
|
-
|
|
589
|
+
2,
|
|
520
590
|
0,
|
|
521
591
|
`Scope: ${relyingPartySummary.scope}`,
|
|
522
592
|
`What was examined: ${relyingPartySummary.examined}`,
|
|
@@ -588,6 +658,8 @@ function renderTerminalCertificate(summary, report) {
|
|
|
588
658
|
lines.push(` ...and ${remaining} more \u2014 see the written report for the full list.`);
|
|
589
659
|
}
|
|
590
660
|
}
|
|
661
|
+
const verdictExplanation = !abstained && report ? buildVerdictExplanation(report) : void 0;
|
|
662
|
+
if (verdictExplanation) lines.push(verdictExplanation);
|
|
591
663
|
if (!abstained && summary.externalFindingCount > 0) {
|
|
592
664
|
lines.push(
|
|
593
665
|
"",
|
|
@@ -739,12 +811,14 @@ Usage:
|
|
|
739
811
|
npx ${NPX_PACKAGE_NAME} [path] [options]
|
|
740
812
|
npx ${NPX_PACKAGE_NAME} scan [path] [options]
|
|
741
813
|
npx ${NPX_PACKAGE_NAME} verify <report.json> <attestation.json> [issuance.json issuance.json.sig]
|
|
814
|
+
npx ${NPX_PACKAGE_NAME} export gitlab-codequality <report.json> [-o gl-code-quality-report.json]
|
|
742
815
|
npx ${NPX_PACKAGE_NAME} issue [path] --report <report.json> --attestation <attestation.json> --key <key.pub> --engagement-ref <ref>
|
|
743
816
|
|
|
744
817
|
Commands:
|
|
745
818
|
scan score a repository (default when the command is omitted)
|
|
746
819
|
verify verify the report/attestation binding; with an issuance pair, also check the issuer
|
|
747
820
|
signature against docs/security/${ISSUER_SIGNERS_FILE} and docs/security/${ISSUER_REVOCATIONS_FILE}
|
|
821
|
+
export write report.json's located findings as a GitLab Code Quality report (offline)
|
|
748
822
|
issue re-run this version at the certificate's revision and, only if report.json reproduces
|
|
749
823
|
byte for byte, write an unsigned issuance.json for the issuer to sign through ssh-agent
|
|
750
824
|
|
|
@@ -789,6 +863,9 @@ async function runWitanCli(args, rubricVersion) {
|
|
|
789
863
|
if (invocation.command === "issue") {
|
|
790
864
|
return runIssue(invocation.options);
|
|
791
865
|
}
|
|
866
|
+
if (invocation.command === "export") {
|
|
867
|
+
return runExport(invocation);
|
|
868
|
+
}
|
|
792
869
|
const options = invocation.options;
|
|
793
870
|
if (options.showVersion) {
|
|
794
871
|
process.stdout.write(`${cliVersion()}
|
|
@@ -890,6 +967,13 @@ function parseCliInvocation(args) {
|
|
|
890
967
|
}
|
|
891
968
|
return parseVerifyArgs(verifyArgs);
|
|
892
969
|
}
|
|
970
|
+
if (command === "export") {
|
|
971
|
+
const exportArgs = args.slice(1);
|
|
972
|
+
if (exportArgs.some((arg) => arg === "-h" || arg === "--help")) {
|
|
973
|
+
return { command: "scan", options: parseArgs(["--help"]) };
|
|
974
|
+
}
|
|
975
|
+
return parseExportArgs(exportArgs);
|
|
976
|
+
}
|
|
893
977
|
if (command === "issue") {
|
|
894
978
|
const issueArgs = args.slice(1);
|
|
895
979
|
if (issueArgs.some((arg) => arg === "-h" || arg === "--help")) {
|
|
@@ -902,6 +986,54 @@ function parseCliInvocation(args) {
|
|
|
902
986
|
}
|
|
903
987
|
return { command: "scan", options: parseArgs(args) };
|
|
904
988
|
}
|
|
989
|
+
var EXPORT_USAGE = () => `Usage: npx ${NPX_PACKAGE_NAME} export gitlab-codequality <report.json> [-o gl-code-quality-report.json]`;
|
|
990
|
+
function parseExportArgs(args) {
|
|
991
|
+
const [format, ...rest] = args;
|
|
992
|
+
if (format !== "gitlab-codequality") throw new Error(EXPORT_USAGE());
|
|
993
|
+
const positionals = [];
|
|
994
|
+
let outPath = "gl-code-quality-report.json";
|
|
995
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
996
|
+
const arg = rest[index];
|
|
997
|
+
if (arg === void 0) continue;
|
|
998
|
+
if (arg === "-o" || arg === "--out") {
|
|
999
|
+
const value = rest[index + 1];
|
|
1000
|
+
if (!value) throw new Error(`Missing value for ${arg}`);
|
|
1001
|
+
outPath = value;
|
|
1002
|
+
index += 1;
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
if (arg.startsWith("-")) throw new Error(`Unknown Cejel export flag: ${arg}`);
|
|
1006
|
+
positionals.push(arg);
|
|
1007
|
+
}
|
|
1008
|
+
const [reportPath] = positionals;
|
|
1009
|
+
if (positionals.length !== 1 || !reportPath) throw new Error(EXPORT_USAGE());
|
|
1010
|
+
return {
|
|
1011
|
+
command: "export",
|
|
1012
|
+
format,
|
|
1013
|
+
reportPath: resolve(reportPath),
|
|
1014
|
+
outPath: resolve(outPath)
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
function runExport(invocation) {
|
|
1018
|
+
const reportArtifact = readJsonArtifact(invocation.reportPath, "report");
|
|
1019
|
+
const reportResult = WitanReportSchema.safeParse(reportArtifact.value);
|
|
1020
|
+
if (!reportResult.success) {
|
|
1021
|
+
const members = reportResult.error.issues.map(
|
|
1022
|
+
(issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`
|
|
1023
|
+
);
|
|
1024
|
+
process.stderr.write(
|
|
1025
|
+
`Cejel: report validation failed:
|
|
1026
|
+
${members.map((member) => ` - ${member}`).join("\n")}
|
|
1027
|
+
`
|
|
1028
|
+
);
|
|
1029
|
+
return 1;
|
|
1030
|
+
}
|
|
1031
|
+
const result = exportGitLabCodeQuality(reportResult.data);
|
|
1032
|
+
writeFileSync(invocation.outPath, `${JSON.stringify(result.entries, null, 2)}
|
|
1033
|
+
`, "utf8");
|
|
1034
|
+
process.stderr.write(renderGitLabCodeQualityFooter(result));
|
|
1035
|
+
return 0;
|
|
1036
|
+
}
|
|
905
1037
|
var VERIFY_USAGE = () => `Usage: npx ${NPX_PACKAGE_NAME} verify <report.json> <attestation.json> [issuance.json issuance.json.sig] [--signers <file>] [--revocations <file>]`;
|
|
906
1038
|
function parseVerifyArgs(args) {
|
|
907
1039
|
const positionals = [];
|
|
@@ -1043,7 +1175,7 @@ ${members.map((member) => ` - ${member}`).join("\n")}
|
|
|
1043
1175
|
return 1;
|
|
1044
1176
|
}
|
|
1045
1177
|
const statement = readJsonArtifact(invocation.attestationPath, "attestation");
|
|
1046
|
-
const reportSha256 =
|
|
1178
|
+
const reportSha256 = createHash3("sha256").update(reportArtifact.contents).digest("hex");
|
|
1047
1179
|
const result = verifyWitanAttestationBinding(statement.value, reportResult.data, {
|
|
1048
1180
|
reportSha256
|
|
1049
1181
|
});
|
|
@@ -1060,7 +1192,7 @@ ${result.errors.map((error) => ` - ${error}`).join("\n")}
|
|
|
1060
1192
|
process.stdout.write("Cejel: signature and signer identity were not verified.\n");
|
|
1061
1193
|
return 0;
|
|
1062
1194
|
}
|
|
1063
|
-
const attestationSha256 =
|
|
1195
|
+
const attestationSha256 = createHash3("sha256").update(statement.contents).digest("hex");
|
|
1064
1196
|
return runVerifyIssuance(invocation, {
|
|
1065
1197
|
reportSha256,
|
|
1066
1198
|
attestationSha256,
|
|
@@ -1163,8 +1295,8 @@ ${members.map((member) => ` - ${member}`).join("\n")}
|
|
|
1163
1295
|
}
|
|
1164
1296
|
const suppliedReport = reportResult.data;
|
|
1165
1297
|
const attestationArtifact = readJsonArtifact(options.attestationPath, "attestation");
|
|
1166
|
-
const reportSha256 =
|
|
1167
|
-
const attestationSha256 =
|
|
1298
|
+
const reportSha256 = createHash3("sha256").update(reportArtifact.contents).digest("hex");
|
|
1299
|
+
const attestationSha256 = createHash3("sha256").update(attestationArtifact.contents).digest("hex");
|
|
1168
1300
|
const attestationBinding = verifyWitanAttestationBinding(
|
|
1169
1301
|
attestationArtifact.value,
|
|
1170
1302
|
suppliedReport,
|
|
@@ -1196,7 +1328,7 @@ ${attestationBinding.errors.map((error) => ` - ${error}`).join("\n")}
|
|
|
1196
1328
|
toolVersion: cliVersion()
|
|
1197
1329
|
});
|
|
1198
1330
|
const reproducedBytes = serializeWitanReport(reproduction.report);
|
|
1199
|
-
const reproducedSha256 =
|
|
1331
|
+
const reproducedSha256 = createHash3("sha256").update(reproducedBytes, "utf8").digest("hex");
|
|
1200
1332
|
if (reproducedSha256 !== reportSha256) {
|
|
1201
1333
|
process.stderr.write(
|
|
1202
1334
|
[
|
|
@@ -1417,6 +1549,7 @@ export {
|
|
|
1417
1549
|
WitanReportSchema,
|
|
1418
1550
|
parseArgs,
|
|
1419
1551
|
parseCliInvocation,
|
|
1552
|
+
parseExportArgs,
|
|
1420
1553
|
parseIssueArgs,
|
|
1421
1554
|
parseVerifyArgs,
|
|
1422
1555
|
runWitanFreeCli,
|
package/dist/mcp/index.js
CHANGED
|
@@ -19,6 +19,41 @@ import {
|
|
|
19
19
|
// src/packs/d-series/index.ts
|
|
20
20
|
init_esm_shims();
|
|
21
21
|
|
|
22
|
+
// src/packs/d-series/tiers.ts
|
|
23
|
+
init_esm_shims();
|
|
24
|
+
var D_SERIES_RULE_TIERS = {
|
|
25
|
+
D1: {
|
|
26
|
+
tier: "semantic",
|
|
27
|
+
sourceFile: "src/packs/d-series/declared-but-unread-config.ts",
|
|
28
|
+
evidence: "resolvedSymbol() follows import aliases with checker.getAliasedSymbol across the first-party module graph to find read sites of a declared key"
|
|
29
|
+
},
|
|
30
|
+
D2: {
|
|
31
|
+
tier: "structural",
|
|
32
|
+
sourceFile: "src/packs/d-series/swallowed-error.ts",
|
|
33
|
+
evidence: "walks the TypeScript AST for catch clauses; bindingIsUnused() compares checker symbols for the catch binding inside its own block"
|
|
34
|
+
},
|
|
35
|
+
D3: {
|
|
36
|
+
tier: "structural",
|
|
37
|
+
sourceFile: "src/packs/d-series/unasserted-set-transform.ts",
|
|
38
|
+
evidence: "walks each file's TypeScript AST for a direct-parameter .filter call, an empty ledger and a literal-success return; no symbol resolution"
|
|
39
|
+
},
|
|
40
|
+
D4: {
|
|
41
|
+
tier: "semantic",
|
|
42
|
+
sourceFile: "src/packs/d-series/empty-failure-conflation.ts",
|
|
43
|
+
evidence: "calleeContract() resolves a call to its declaration in another first-party file via checker.getAliasedSymbol and reads that callee's return states"
|
|
44
|
+
},
|
|
45
|
+
D5: {
|
|
46
|
+
tier: "semantic",
|
|
47
|
+
sourceFile: "src/packs/d-series/self-referential-verification.ts",
|
|
48
|
+
evidence: "importedBinding() resolves test imports to their declaring source files with checker.getAliasedSymbol and compares the module under test across files"
|
|
49
|
+
},
|
|
50
|
+
D6: {
|
|
51
|
+
tier: "lexical",
|
|
52
|
+
sourceFile: "src/packs/d-series/unobserved-control.ts",
|
|
53
|
+
evidence: "exitStatusAnalysis() regex-matches single shell lines plus the next line, with a whole-file presence test for set -e; no parse, no name resolution, no flow"
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
22
57
|
// src/packs/d-series/declared-but-unread-config.ts
|
|
23
58
|
init_esm_shims();
|
|
24
59
|
var import_typescript2 = __toESM(require_typescript(), 1);
|
|
@@ -1112,6 +1147,7 @@ function scanUnobservedControls(repoRoot) {
|
|
|
1112
1147
|
return inspectionFromAnalyses(analyses, findings, anchorAbstentions);
|
|
1113
1148
|
}
|
|
1114
1149
|
export {
|
|
1150
|
+
D_SERIES_RULE_TIERS,
|
|
1115
1151
|
detectDeclaredButUnreadConfig,
|
|
1116
1152
|
detectEmptyFailureConflation,
|
|
1117
1153
|
detectSelfReferentialVerification,
|
|
@@ -16,6 +16,16 @@ import {
|
|
|
16
16
|
// src/packs/decision-contracts/index.ts
|
|
17
17
|
init_esm_shims();
|
|
18
18
|
|
|
19
|
+
// src/packs/decision-contracts/tiers.ts
|
|
20
|
+
init_esm_shims();
|
|
21
|
+
var DECISION_CONTRACT_RULE_TIERS = {
|
|
22
|
+
"DECISION-CONTRACT-EDGE": {
|
|
23
|
+
tier: "semantic",
|
|
24
|
+
sourceFile: "src/packs/decision-contracts/decision-contract-conformance.ts",
|
|
25
|
+
evidence: "parses one file with ts.createSourceFile (structural), but transitiveDecisionReferences() expands local initializers to follow which premises the returned decision depends on (data flow, within one function); ambiguous, so the higher tier"
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
19
29
|
// src/packs/decision-contracts/decision-contract-conformance.ts
|
|
20
30
|
init_esm_shims();
|
|
21
31
|
var import_typescript = __toESM(require_typescript(), 1);
|
|
@@ -308,6 +318,7 @@ function scanDecisionContracts(repoRoot) {
|
|
|
308
318
|
}
|
|
309
319
|
export {
|
|
310
320
|
DECISION_CONTRACT_MANIFEST_PATH,
|
|
321
|
+
DECISION_CONTRACT_RULE_TIERS,
|
|
311
322
|
decisionContractManifestSchema,
|
|
312
323
|
decisionContractSchema,
|
|
313
324
|
evaluateDecisionContracts,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cejel/cejel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"mcpName": "io.github.BargLabs/cejel",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -62,7 +62,10 @@
|
|
|
62
62
|
"gate:d4": "pnpm run build && node scripts/d4-precision-gate.mjs",
|
|
63
63
|
"gate:d5": "pnpm run build && node scripts/d5-precision-gate.mjs",
|
|
64
64
|
"gate:d6": "pnpm run build && node scripts/d6-precision-gate.mjs",
|
|
65
|
-
"
|
|
65
|
+
"sbom:generate": "node scripts/sbom/generate-bundle-sbom.mjs",
|
|
66
|
+
"sbom:check": "node scripts/sbom/check-sbom-inventory.mjs",
|
|
67
|
+
"test:sbom": "node --test scripts/sbom/sbom.node-test.mjs",
|
|
68
|
+
"validate:distribution":"node scripts/validate-distribution-metadata.mjs",
|
|
66
69
|
"test:release": "bash scripts/release_tests.sh",
|
|
67
70
|
"pnpm:devPreinstall": "node scripts/assert-dev-environment.mjs --phase=preinstall",
|
|
68
71
|
"prepublishOnly": "pnpm run build",
|