cejel --ingest folds other scanners' output into a certificate. An ingested document is
evidence about some repository, at some commit, observed at some time, by some producer. Before
report format 1.7 Cejel read none of that. A Scorecard result for another repository, for an older
commit, or from a run made long before the scanned revision existed was scored exactly like a
matching one, and the certificate attributed it only by tool name and operator_supplied.
From report format 1.7, every ingested document is recorded in report.json under
inputBinding, with what its own format says about its subject and producer and whether that
subject is the revision the certificate is about.
This page is the specification. The conformance suite at the end is checked mechanically by
src/witan/__tests__/input-binding-conformance.test.ts, which reads the table on this page.
| Format | Subject repository | Subject commit | Observed at | Producer |
|---|---|---|---|---|
| OpenSSF Scorecard JSON | repo.name |
repo.commit |
date |
scorecard, scorecard.version, scorecard.commit |
| SARIF 2.1.0 | runs[].versionControlProvenance[].repositoryUri |
runs[].versionControlProvenance[].revisionId |
earliest runs[].invocations[].startTimeUtc / endTimeUtc |
runs[].tool.driver.name, .semanticVersion or .version |
| Generic contract 1.1 | subject.repository |
subject.commit |
observedAt |
producer.name (default tool), producer.version |
| Generic contract 1.0 | none | none | none | tool |
A date-only Scorecard date (2026-10-01) is read as the last second of that UTC day, so day
granularity never makes an input look older than it is. A time whose UTC form falls outside years
0000-9999 (9999-12-31T23:00:00-02:00 is year 10000 in UTC) is treated as not declared. The
earliest of several SARIF times is chosen by instant, not by text.
A SARIF document with several runs, or several provenance entries, is checked against every subject
it declares, however many there are; the worst result wins. report.json records at most eight
distinct subjects per input. When a document declares more, subjectsOmitted counts the rest, and
the recorded eight start with the subjects that on their own disagree with the scanned subject, so
the record shows what decided the status. The cap applies to the record, never to the comparison.
A SARIF document is matched only when every declared subject declares a commit and every declared
commit is the scanned HEAD. A run with no version-control provenance, beside runs that have it, is
recorded as an empty subject ({}); it and a provenance entry with no revisionId leave the
document unbound, because the findings of that run are bound to no revision. A document in which
no run declares a subject records subjects: [] and is unbound as before.
- Commit: the scanned HEAD. A 40- or 64-character input commit is compared exactly; a 7- to 39-character one as a prefix, the way Git resolves an abbreviated id.
- Repository: only what the operator names with
--subject-repository. Cejel never infers a repository identity from clone state (a remote URL describes the clone, not the revision). Both sides are normalized tohost/path, lowercase, without scheme, credentials, port,.gitor a trailing slash; normalizing a normalized identity returns it unchanged. Without--subject-repository, an input's repository is recorded asunverifiable, and binding rests on the commit. - A repository Cejel cannot normalize (an IP address or single-label host, a host with a
trailing dot, a URL with no path, a path that still holds an
@, or a declared value that is not a string, such as an object) is never recorded: the declared text is where URL credentials live. The subject recordsrepositoryWithheld: trueinstead. With--subject-repository, a withheld repository counts asmismatched(it is not demonstrably the named repository) and is refused under--strict-ingest; without it, the repository check isunverifiable, as for any declared repository. A--subject-repositoryvalue that does not normalize is refused without echoing it. - Freshness: the HEAD commit's committer time, read offline with
git show -s --format=%ct HEAD. An input observed more thanfreshnessWindowSeconds(86,400, one day, to absorb clock skew between a scanner host and a committer) before that time was produced before the scanned code existed. The comparison is never against the wall clock, so a re-run on another day produces the same bytes. Committer time is set by whoever made the commit; this is a consistency check between two declared times, not a proof of either.
Each record carries three checks and one status derived from them, worst first:
| Status | Meaning |
|---|---|
mismatched-repository |
The input declares a repository, and it is not the one named with --subject-repository. |
mismatched-commit |
The input declares a commit, and it is not the scanned HEAD. |
stale |
The input was observed more than the declared window before the scanned HEAD's commit time. |
matched |
The input declares a commit, and it is the scanned HEAD; nothing above applies. |
unverifiable |
The input declares a commit, but the scanned directory has no git HEAD to compare it with. |
unbound |
The input declares no commit. It may name a repository, but a repository alone does not bind it to a revision. |
checks.repository and checks.commit are each matched, mismatched, absent (the input
declares none) or unverifiable (the scan has nothing to compare with). checks.freshness is
within-window, stale, absent or unverifiable.
Deduplication is unchanged, and each duplicate is recorded:
same-path: a path supplied twice, or both with--ingestand through.cejel/inputs, is read and scored once, as it always was. The first repeat is recorded withscored: false; later repeats of the same path add no further record.same-content: identical bytes at two paths are both scored, as they always were. The second is recorded as asame-contentduplicate withscored: true. Scoring it twice counts the same findings twice;--strict-ingestrefuses it.
- Default,
record-only. The binding record is attached after scoring. It moves no score, status, abstention, badge or--min-scoreoutcome. A certificate scored with a mismatched input has exactly the scores it had before report format 1.7. --strict-ingest. Before scoring, Cejel refuses every input whose status ismismatched-repository,mismatched-commitorstale, and every duplicate, and writes no certificate. The error names each refused file, why, and what it was compared against.unboundandunverifiableinputs are accepted and recorded: refusing them would refuse every 1.0 generic document and every SARIF file without version-control provenance. The certificate recordspolicy: "strict".
inputBinding.inputsis ordered by digest, not by command-line order, and records no file path, so a reproduction that supplies the same files from another directory, in another order, produces the same bytes.cejel issuere-runs the scan and refuses to issue unlessreport.jsonreproduces byte for byte. It reads--strict-ingestand--subject-repositoryfrom the supplied report'sinputBinding, so the reproduction uses the invocation the report records. A swapped input changes the recorded digest and therefore the bytes, and is refused.cejel verify <report.json> <attestation.json> --input <file> ...checks a set of input files against the recorded digests. The comparison is between sets of distinct sha256 digests: every supplied file's digest must be a recorded, scored input's, and every recorded, scored input's digest must be supplied. Supplying one file twice, or two files with identical bytes, counts once, so asame-contentduplicate is satisfied by either copy. A file swapped for another, even with the same name, tool and finding count, fails. Each--inputis read under the same checks ingest applies: a regular file, not a symlink, in a directory that is not a symlink, within the 8 MiB ingest budget. Anything else is refused before it is read.inputBinding.inputsholds at most 256 records: one per document (the ingest budget is 128 documents) and at most onesame-pathrepeat per document, however many times, or through however many routes, the path was supplied.- The assembled report is validated against the schema
cejel verifyapplies before anything is written. A scan that would produce a report its own verifier rejects fails instead.
Every row is run under the default policy and under --strict-ingest, with
--subject-repository github.com/example/conformance, against a fixture repository whose HEAD was
committed at 2026-10-01T12:00:00Z. The inputs are constructed in the test, not vendored:
- matching: the scanned repository and HEAD commit, observed one hour after the commit.
- wrong-repository:
github.com/other/projectat another commit. - wrong-commit: the scanned repository at another commit.
- stale: the scanned repository, no commit, observed one year before the commit.
- duplicate: the matching document supplied again at a second path. The table shows the second
record; the first is
matched. - unbound: no subject at all (a SARIF run without
versionControlProvenance, a Scorecard document withoutrepo, a generic 1.0 document).
| Format | Case | Binding status | Duplicate | Default | Strict |
|---|---|---|---|---|---|
| sarif | matching | matched | - | scored, recorded | accepted |
| sarif | wrong-repository | mismatched-repository | - | scored, recorded | refused |
| sarif | wrong-commit | mismatched-commit | - | scored, recorded | refused |
| sarif | stale | stale | - | scored, recorded | refused |
| sarif | duplicate | matched | same-content | scored, recorded | refused |
| sarif | unbound | unbound | - | scored, recorded | accepted |
| scorecard | matching | matched | - | scored, recorded | accepted |
| scorecard | wrong-repository | mismatched-repository | - | scored, recorded | refused |
| scorecard | wrong-commit | mismatched-commit | - | scored, recorded | refused |
| scorecard | stale | stale | - | scored, recorded | refused |
| scorecard | duplicate | matched | same-content | scored, recorded | refused |
| scorecard | unbound | unbound | - | scored, recorded | accepted |
| generic | matching | matched | - | scored, recorded | accepted |
| generic | wrong-repository | mismatched-repository | - | scored, recorded | refused |
| generic | wrong-commit | mismatched-commit | - | scored, recorded | refused |
| generic | stale | stale | - | scored, recorded | refused |
| generic | duplicate | matched | same-content | scored, recorded | refused |
| generic | unbound | unbound | - | scored, recorded | accepted |
Two more properties are checked for every format:
- Scoring is independent of binding under the default policy. The scoring surface
(
projectWitanScoringSurface, the projection the rubric behaviour fingerprint digests) of a report with a mismatched input equals that of a report with the matching input carrying the same findings. - Without
--subject-repository, a wrong-repository input that also declares a commit is recorded asmismatched-commit, withchecks.repository: "unverifiable", and is still refused under--strict-ingest.