Trace and crash evidence in the release record
Firmware teams keep runtime traces and crash dumps from many tools: a streaming trace from Tracealyzer and TraceRecorder, a SystemView recording, a Zephyr core dump, a CrashCatcher or minidump file, a CTF trace from LTTng, a perf.data file, a network capture. This page shows what SBOMFlow does with those files, step by step, on a synthetic example you can run yourself. Every command on this page is offline and is executed, in order, by SBOMFlow's own test suite.
In 60 seconds#
The question it answers. Months after a release, someone asks: which trace or dump did we review for exactly which firmware of which release, who decided what about it, and is it still the same file? SBOMFlow keeps the answer as part of the release record:
- These bytes. Each file gets a small record with its name, size and SHA-256, and what a person stated about it: the release, the scenario, the tool, the firmware image it ran on. A statement is recorded as a statement, never as a measurement.
- This firmware. The image you name is hashed and, at release time, looked up among the files the release scan hashed. A build id or version you state is compared with what the image declares about itself.
- This decision. A named reviewer records a conclusion and its limits against the file's digest. The decision covers those bytes and no others.
- Still the same. Each later run re-hashes the original. One changed byte suspends the decision, and a changed record fails bundle verification.
- Across releases. The next release's comparison lists what was added, removed or decided anew, and shows an earlier decision about identical bytes as context, never as a decision for the new release.
What it deliberately does not do.
- No trace analysis. SBOMFlow never decodes events, samples, registers or memory. For a few open formats it reads the identity a file's header declares about itself (the table below says exactly which fields); for every other format it reads nothing inside the file.
- No copies of your originals. Only the small records travel into the evidence directory, the bundle and the ZIP. A trace or dump can hold memory contents and device identifiers, and keeping the original stays with you.
- No upload. Nothing on this page opens a network connection, and SBOMFlow never runs, drives or connects to the tool that wrote a file.
- No gate. A trace, a missing decision, a rejection or a mismatch changes no release gate, VEX status or approval digest. The release decision stays with the people who make it.
- No compliance claim. The record is engineering evidence of what was reviewed. It says nothing about whether a product is secure or conforms to any regulation.
The journey at a glance.
| Step | Command | What you get |
|---|---|---|
| Record | sbomflow ingest-preserved-artifact | one record per file: digest, stated values, the firmware image hashed; what the header declares, shown at once |
| Declare | sbomflow.yaml | the records, their originals and the release's firmware image |
| Release record | sbomflow audit | preserved-artifacts.json: re-hash, firmware check, declared identity, review state |
| Decide | sbomflow review | a named decision bound to the file's digest |
| Drift | sbomflow compare-releases | what changed between two releases' records and decisions |
| Bundle | sbomflow audit --zip | a portable, hashed handoff that carries records and never originals |
| Verify | sbomflow validate, sbomflow verify-bundle | every copy re-hashed against the manifest |
| Tamper | the same commands | a changed record fails verification; a changed original suspends its decision |
The example#
The files ship with SBOMFlow's source under examples/trace-and-crash-evidence/, and each says what it is in its own bytes or in a sidecar file. None is a recording of a device. Every file describes one target, the one the firmware image is built for: a Zephyr application on a 32-bit little-endian ARM core. The field alert's entries were written by the open-source DFM library itself, compiled and run in a host harness, so they are that library's genuine output. The TraceRecorder files are synthetic, written from the library's pinned layout as that target stores them, because the library's host harness cannot write a 32-bit Zephyr header; the other files were written from the published formats, and the .svdat files are short text files rather than that tool's format. The example's PROVENANCE.md says how each file was produced. The tool names are what an engineer states when recording each file.
The example is two releases of one sensor node, each in its own directory. Its captures, by release:
| Release | File | What it stands in for | Stated tool |
|---|---|---|---|
| 2.4.0 | captures/boot-to-idle.psf | a streaming trace, boot to idle, cold start | Percepio Tracealyzer |
| 2.4.0 | captures/motor-loop.svdat | a recording of the motor-control loop at full load | SEGGER SystemView |
| 2.4.0 | captures/unit-0042.zephyr-coredump | a core dump after a watchdog reset on a field unit | Zephyr core dump |
| 2.4.1 | captures/boot-to-idle.psf | the boot trace, re-captured on 2.4.1 | Percepio Tracealyzer |
| 2.4.1 | captures/motor-loop.svdat | the same recording as 2.4.0, byte for byte | SEGGER SystemView |
| 2.4.1 | captures/dfm_trace.psfs | the trace half of one field alert | Percepio Detect |
| 2.4.1 | captures/cc_coredump.dmp | the core-dump half of the same alert | Percepio Detect |
| 2.4.1 | captures/alert-7f3.dfm-alert | the alert itself: its entries, declaring firmware version 2.4.1 | Percepio Detect |
Each release also has firmware/zephyr.elf, a stand-in image that declares a GNU build id (a default Zephyr build declares none; see Build identity in practice), and an sbomflow.yaml.
1. Record each file, and the image it ran on#
Work in a copy, so the example stays as it is:
cp -R examples/trace-and-crash-evidence trace-walkthrough
cd trace-walkthrough/2.4.0
sbomflow ingest-preserved-artifact captures/boot-to-idle.psf \
--release-id 2.4.0 --scenario "boot to idle, cold start" \
--tool-name "Percepio Tracealyzer" --tool-version 4.11 \
--artifact-kind runtime_trace \
--firmware-image firmware/zephyr.elf \
--firmware-build-id 4f1c2a9e7b3d5e6f80112233445566778899aabb \
--firmware-version 2.4.0 \
--output preserved/boot-to-idle.preserved-artifact.jsonsbomflow preserved artifact — PRESERVED, NOT ANALYSED
Artifact boot-to-idle.psf (532 bytes)
SHA-256 6fa5eca4839e5a378dc185311bc1c809648fd1cd68495bc5f465033a0b45b03f
Format hint psf extension trace [psf_extension_trace], from the file
name's extension [filename_extension]; the hint comes from
the name alone
Evidence kind runtime trace (runtime_trace), stated by the operator; not
read from the file
Stated by you (user_supplied), each value exactly as given:
Firmware build ID 4f1c2a9e7b3d5e6f80112233445566778899aabb
Firmware image name zephyr.elf
Firmware image SHA-256 ff13a15e0a2d4ef3c7c97f389f3ac4889ae800cb19b7876f739a7d24f8ca4ac2
Firmware version 2.4.0
Declared by the file, read by the tracerecorder-psf reader:
Byte order little [byte_order]
RTOS, named from the kernel ID Zephyr [kernel] (looked up)
64-bit target bit 0 [option_64_bit]
Core count 1 [core_count]
Extension syscalls [extension_name[1]]
Extension version 3.7.0 [extension_version[1]]
Read now for this screen only: the record does not store these values; the
release run that declares it reads the header again and records what
it finds.
Gate this record never affects a release gate.The record keeps the JSON field names (firmware_gnu_build_id, firmware_image_sha256, artifact_kind); the terminal prints each with a plain label, and ends with the Next: step that declares the record in sbomflow.yaml.
- Say which image the capture ran on, at capture time. The person at the bench knows it; the person assembling the release months later does not.
--firmware-imagehashes the image and records its SHA-256 and file name, never its path.--firmware-build-idand--firmware-versionrecord what you state; a build id is not a checksum and a version is never a stand-in for a digest. - What the file declares is shown at once. For a format a reader is registered for, the command reads the header exactly as the release run will, and prints it: here the kernel, the word size, the core count and the extension the TraceRecorder library registered. It reads no event, and the values are shown only: the record stores the digest and your statements, and the release run reads the header again. A closed format such as
.svdatsays so here, and a format no reader knows keeps "content not inspected". - The format hint comes from the file name, and only from it. It is a label, not a check; the header block above is what the bytes say.
--tool-nameand--tool-versionare the tool you state, here the host application. The TraceRecorder library built into the firmware has its own version, which its header does not declare; put it in--noteswhen a reviewer will need it.--artifact-kindis what you say the file is, from a closed vocabulary (runtime_trace,core_dump,crash_reportand others). It is never guessed from the name or the bytes.- Anything you did not state is listed as missing rather than filled in with a default.
The other two files of this release:
sbomflow ingest-preserved-artifact captures/motor-loop.svdat \
--release-id 2.4.0 --scenario "motor-control loop, full load" \
--tool-name "SEGGER SystemView" --artifact-kind runtime_trace \
--firmware-image firmware/zephyr.elf \
--output preserved/motor-loop.preserved-artifact.json
sbomflow ingest-preserved-artifact captures/unit-0042.zephyr-coredump \
--release-id 2.4.0 --scenario "field unit 0042, watchdog reset" \
--tool-name "Zephyr coredump" --artifact-kind core_dump \
--firmware-version 2.4.0 \
--output preserved/unit-0042.preserved-artifact.jsonThe core dump came back from a field unit that reported version 2.4.0; nobody has the image digest for it, so none is stated. That is recorded as it is, and the release run below says what it could and could not compare.
2. Declare the records for the release#
The example's sbomflow.yaml names each record, the original it describes and the release's firmware image:
release:
id: "2.4.0"
requirement_regime: none
preserved_artifacts:
- record: preserved/boot-to-idle.preserved-artifact.json
original: captures/boot-to-idle.psf
firmware_image: firmware/zephyr.elf
- record: preserved/motor-loop.preserved-artifact.json
original: captures/motor-loop.svdat
firmware_image: firmware/zephyr.elf
- record: preserved/unit-0042.preserved-artifact.json
original: captures/unit-0042.zephyr-coredump
firmware_image: firmware/zephyr.elfA record is read only when it is declared: SBOMFlow never searches a directory for traces. requirement_regime: none means no regulatory requirement is assessed in this run; the evidence record works the same with or without one.
3. Run the release audit#
sbomflow audit . --output evidence
sbomflow review evidence Preserved artifacts (decisions about preserved files, a separate unit; not
in awaiting_review):
Awaiting a decision 3
Decided 0
- boot-to-idle.psf
preserved-artifact:sha256:6fa5eca4839e5a378dc185311bc1c809648fd1cd68495bc5f465033a0b45b03f
Original verified
Capture states stated image is in this release
Build ID match not established — the file declares no GNU build ID
- unit-0042.zephyr-coredump
preserved-artifact:sha256:b534095e8424c4f0dcb8c25fc2d119946d79b7beeae509309b49ba6f51e8781a
Capture states stated without a digest
- motor-loop.svdat
preserved-artifact:sha256:bc436848c5f473d1340595d27e4bdfb3dede7c9560d2e104490d6dc4def80121Each file's first lines say what was checked: the original's bytes, then what the capture record states about the image (the positive tie most firmware has, since a default build declares no build id), then the build id.
evidence/preserved-artifacts.json holds one row per file. Each row carries the checks the run made, each saying exactly what it shows:
| Check | In this run | What it shows, and what it does not |
|---|---|---|
original_verification | verified for all three | the original you supplied has the recorded size and SHA-256. Byte identity, not who produced it |
firmware_image | in_release | the image the configuration names is byte-identical to a file this release scanned. It does not show the capture was taken on it |
capture_firmware | stated_image_in_release for the two traces; stated_without_a_digest for the core dump | what the capture record states about the image, compared with the release's image: SHA-256, build id, version |
file_declared_identity | read for the boot trace and the Zephyr core dump; the .svdat row says it is a closed format | the header fields the file declares about itself, under a cited specification or the pinned source code that defines the format |
For the boot trace, every stated value agrees with what the release shows: the image digest, the build id the image declares and the version. Each comparison is listed with its result:
{
"state": "stated_image_in_release",
"comparisons": [
{"with": "declared_firmware_image_sha256", "result": "agrees"},
{"with": "declared_firmware_image_gnu_build_id", "result": "agrees"},
{"with": "run_release_identity", "result": "agrees"}
]
}A differs in that list warns preserved_artifact_capture_firmware_differs: two statements about the same image disagree. It is not evidence of tampering, it suspends no decision and it reaches no gate; a person reconciles the two.
The core dump is in a format with a published specification, so SBOMFlow read its 12-byte file header and nothing after it — no registers, no memory:
{
"reader": "zephyr-coredump",
"fields": [
{"field": "magic", "value": "ZE", "origin": "extracted"},
{"field": "header_version", "value": "2", "origin": "extracted"},
{"field": "target_code", "value": "3", "origin": "extracted"},
{"field": "pointer_size_log2", "value": "5", "origin": "extracted"},
{"field": "flags", "value": "0x00", "origin": "extracted"},
{"field": "fatal_error_reason", "value": "0", "origin": "extracted"}
]
}Its meaning says declares_no_build_identity: this format has no field naming the firmware or the build, so the link to the release comes only from what a person declared. Numbers are shown as the file carries them; no architecture or fault reason is named from a code the specification does not list.
The .svdat recording is in a closed format, and its row says so rather than staying silent:
{
"state": "not_applicable",
"reader": null,
"fields": [],
"meaning": "a closed format: its layout is neither published in a specification nor defined by open-source code, so by policy SBOMFlow reads nothing inside the file and no reader may claim it; the file's hash, its custody record and the review decision are recorded all the same"
}The boot trace is a TraceRecorder streaming trace. No specification document defines that format; the open-source library that writes it does, so SBOMFlow read its 32-byte header under that library's source, pinned to one commit, then the timestamp block and the entry table the library writes after it (see The entry table below), and no event. The entry table holds one extension, the Zephyr port's syscalls extension, and four thread entries whose names are withheld and only counted:
{
"reader": "tracerecorder-psf",
"fields": [
{"field": "byte_order", "value": "little", "origin": "extracted"},
{"field": "format_version", "value": "0x000e", "origin": "extracted"},
{"field": "kernel_id", "value": "0x9aa9", "origin": "extracted"},
{"field": "kernel", "value": "Zephyr", "origin": "looked_up"},
{"field": "option_64_bit", "value": "0", "origin": "extracted"},
{"field": "core_count", "value": "1", "origin": "extracted"},
{"field": "platform_config", "value": "Zephyr", "origin": "extracted"},
{"field": "platform_config_version", "value": "4.3.0", "origin": "extracted"},
{"field": "timestamp_frequency_hz", "value": "64000000", "origin": "extracted"},
{"field": "os_tick_rate_hz", "value": "10000", "origin": "extracted"},
{"field": "entry_count", "value": "5", "origin": "extracted"},
{"field": "entries_object", "value": "4", "origin": "computed"},
{"field": "entries_extension", "value": "1", "origin": "computed"},
{"field": "extension_name[1]", "value": "syscalls", "origin": "extracted"},
{"field": "extension_version[1]", "value": "3.7.0", "origin": "extracted"}
],
"withheld": [
{"field": "entry_names", "reason": "4 entry name(s) withheld: entry names are the names the application gave its tasks, interrupts, queues and other objects, user text that can carry product or customer names; they were not read, and each entry is only counted by class"}
]
}The Zephyr port registers syscalls with the kernel's own version, so the 3.7.0 beside it and the v3.7.0 the release's west.yml pins can be read side by side; SBOMFlow shows both and compares neither. The clock values are this synthetic example's, not a board's.
The header declares a kernel by number; the name beside it is looked up in the kernel ports the pinned library defines, so its origin is looked_up, and the row's note says so. The entry counts are SBOMFlow's own count of the table's entries by class, so their origin is computed; entry_count is the number the table itself states. The header describes the same target as the image: Zephyr, little endian, 32-bit (option bit 3, option_64_bit, is clear), one core. It is the only link a header gives between a trace and a target, and it is a shape, not an identity. Like the dump, the header names no firmware or build (declares_no_build_identity). All three files are hashed, re-verified, linked to the image and reviewable, whichever of them a reader could open.
The kernel the trace declares is also set beside the components this release's SBOM lists. The example's west.yml pins zephyr at v3.7.0, so the SBOM lists it, and the row says so, as context:
{
"state": "sbom_lists_component",
"declared_kernel": "Zephyr",
"kernel_id": "0x9aa9",
"component_names_compared": ["zephyr", "zephyr-sources"],
"sbom_components": [
{"name": "zephyr", "version": "v3.7.0", "purl": "pkg:generic/zephyr@v3.7.0"}
]
}It is one rule: the SBOM component's name, case-folded, equals a name a pinned table gives that kernel. It shows the SBOM names the kernel the trace was built for, and nothing more: it does not show the trace was recorded on that component or version, and no version is compared, because the platform configuration version the header carries (4.3.0) is not the kernel's release.
4. Record what you concluded#
Open each file in the tool that produced it and decide what it shows. Then record the conclusion and its limits against the file's digest:
sbomflow review evidence \
--accept preserved-artifact:sha256:6fa5eca4839e5a378dc185311bc1c809648fd1cd68495bc5f465033a0b45b03f \
--reviewer fw-lead \
--note "Boot to idle within budget on a cold start; covers the cold-start path only"Recorded preserved-artifact:sha256:6fa5eca4839e5a378dc185311bc1c809648fd1cd68495bc5f465033a0b45b03f -> accepted
decision file: evidence/reviews.jsonsbomflow review evidence \
--accept preserved-artifact:sha256:bc436848c5f473d1340595d27e4bdfb3dede7c9560d2e104490d6dc4def80121 \
--reviewer fw-lead \
--note "Control loop met its deadline at full load for the recorded 60 s; no other load examined"
sbomflow review evidence \
--needs-more-evidence preserved-artifact:sha256:b534095e8424c4f0dcb8c25fc2d119946d79b7beeae509309b49ba6f51e8781a \
--reviewer fw-lead \
--note "Watchdog reset on unit 0042; root cause not established from this dump alone"
sbomflow audit . --output evidence --reviews evidence/reviews.json--accept,--rejectand--needs-more-evidencetake the full review key, a--reviewerand a non-empty--note; without them the decision is refused and nothing is written.- The key is the file's digest. A different file, even under the same name, is a different key and stays unreviewed.
- The re-run applies the decisions it is given with
--reviews. Every row now readsacceptedorneeds_more_evidence, with the reviewer and the note beside it, andevidence/release-record.jsonlists each record copy by its SHA-256 underpreserved_artifact_refs.
5. The next release, and what changed#
Release 2.4.1 has a new image, a re-captured boot trace, the same motor-loop recording, and one field alert that produced its own entries, a trace and a core dump. Record them the same way. The alert's three files share a --capture-session, so a reviewer sees they came from one event; they are still three files with three decisions:
cd ../2.4.1
sbomflow ingest-preserved-artifact captures/boot-to-idle.psf \
--release-id 2.4.1 --scenario "boot to idle, cold start" \
--tool-name "Percepio Tracealyzer" --tool-version 4.11 \
--artifact-kind runtime_trace \
--firmware-image firmware/zephyr.elf \
--firmware-build-id 9d2e4b6a8c0e1f3a5b7c9d0e1f2a3b4c5d6e7f80 \
--firmware-version 2.4.1 \
--output preserved/boot-to-idle.preserved-artifact.json
sbomflow ingest-preserved-artifact captures/motor-loop.svdat \
--release-id 2.4.1 --scenario "motor-control loop, full load" \
--tool-name "SEGGER SystemView" --artifact-kind runtime_trace \
--firmware-image firmware/zephyr.elf \
--output preserved/motor-loop.preserved-artifact.json
sbomflow ingest-preserved-artifact captures/dfm_trace.psfs \
--release-id 2.4.1 --scenario "field alert 7f3, trace" \
--tool-name "Percepio Detect" --artifact-kind runtime_trace \
--capture-session alert-7f3 --firmware-version 2.4.1 \
--output preserved/alert-7f3.trace.preserved-artifact.json
sbomflow ingest-preserved-artifact captures/cc_coredump.dmp \
--release-id 2.4.1 --scenario "field alert 7f3, core dump" \
--tool-name "Percepio Detect" --artifact-kind core_dump \
--capture-session alert-7f3 --firmware-version 2.4.1 \
--output preserved/alert-7f3.dump.preserved-artifact.json
sbomflow ingest-preserved-artifact captures/alert-7f3.dfm-alert \
--release-id 2.4.1 --scenario "field alert 7f3, alert entries" \
--tool-name "Percepio Detect" --artifact-kind crash_report \
--capture-session alert-7f3 \
--output preserved/alert-7f3.alert.preserved-artifact.jsonNo --firmware-version is stated for the alert's entries: the file declares one itself, and the release run below shows it.
Run the release audit against the previous release's output, then compare the two releases:
sbomflow audit . --output evidence --previous-output ../2.4.0/evidence
sbomflow compare-releases --previous ../2.4.0/evidence --current evidence --output driftWrote preserved-artifact changes: drift/preserved-artifact-changes.json
Preserved artifacts: compared, 10 changes between the two releases
added: cc_coredump.dmp sha256:314c8e74fca0 (not reviewed yet)
added: dfm_trace.psfs sha256:33a411897a3c (not reviewed yet)
added: alert-7f3.dfm-alert sha256:4b5abb25aa89 (not reviewed yet)
added: boot-to-idle.psf sha256:b95108bd6d61 (not reviewed yet)
removed: boot-to-idle.psf sha256:6fa5eca4839e (accepted)
removed: unit-0042.zephyr-coredump sha256:b534095e8424 (needs more evidence)
decision not carried: motor-loop.svdat sha256:bc436848c5f4: accepted -> not
reviewed yet (no decision is recorded in this release)
same stated scenario, decision not carried: boot-to-idle.psf
sha256:b95108bd6d61 (not reviewed yet) and the previous release's
boot-to-idle.psf sha256:6fa5eca4839e (accepted): two records state "boot
to idle, cold start"; not the same capture, and no decision is carried
other changes: 3 (stated associations, custody records or run checks); each
is listed in preserved-artifact-changes.jsonThe comparison is by digest, so each line names the file, its review status and the start of its SHA-256. The re-captured boot trace is new bytes: the 2.4.1 trace is added and the 2.4.0 trace removed, although both are called boot-to-idle.psf. Because both records state the same scenario text, the two are also set side by side as a same stated scenario pair, so the new capture can be read beside the old one and the decision made about it:
[
{
"stated_scenario": "boot to idle, cold start",
"decision_state": "decision_not_carried",
"current": {"name": "boot-to-idle.psf", "review_status": "unreviewed"},
"previous": {
"name": "boot-to-idle.psf",
"review_status": "accepted",
"reviewer": "fw-lead",
"in_current_release": false
}
}
]A pair means two records state the same scenario text, compared as exact strings, and nothing more: the file name is never used, the bytes differ, and it does not show the same test was run the same way. The 2.4.0 decision is shown as context; it is not carried, and the 2.4.1 trace still needs its own. The pairs are listed under stated_scenario_pairs and are not counted among the changes. The Zephyr dump is removed with the decision it carried. drift/preserved-artifact-changes.json holds every change in full, including the three this output only counts.
The motor-loop recording is the same bytes in both releases, so it is compared rather than added. It reads decision not carried because 2.4.0 accepted it and 2.4.1 records no decision yet: a decision is never carried across releases, and nobody re-decided anything. Its row in 2.4.1 shows what 2.4.0 decided about those bytes, as prior_decision_context, beside the row's own review and never inside it:
{
"state": "decision_recorded",
"status": "accepted",
"reviewer": "fw-lead",
"note": "Control loop met its deadline at full load for the recorded 60 s; no other load examined",
"previous_release": {"release_id": "2.4.0", "state": "read"},
"meaning": "a decision recorded for the same bytes in the previous release; it is not applied here and decides nothing for this release"
}The 2.4.1 row itself still reads unreviewed. Nothing is carried forward automatically, not even for identical bytes: the image changed, and whether a recording made on the old image still covers the new one is a person's call.
The CrashCatcher dump the alert carried has its header read, and nothing past it:
{
"reader": "crashcatcher-dump",
"fields": [
{"field": "magic", "value": "cC"},
{"field": "version_major", "value": "3"},
{"field": "version_minor", "value": "0"},
{"field": "flags", "value": "0x00000000"}
]
}The alert's entries are the one file in this release that declares a firmware version. The DFM library writes the version the device build was configured with into every alert, and SBOMFlow read it under the library's pinned source, then set it beside the version this release states, as context:
{
"state": "agree",
"declared_field": "firmware_version",
"declared_versions": ["2.4.1"],
"release_version": "2.4.1"
}agree means the two strings are equal, and nothing more: the version is free text the device's build configuration carries, and a copied configuration keeps old text. It is never a match, never a warning and never a gate input; differ would not be a finding either. In a file holding several alerts, every alert's version is read and all of them must equal the release version for agree, and one that cannot be read makes the context not_comparable, never an agree about part of the file.
6. Decide, and hand over a bundle you can verify#
sbomflow review evidence \
--accept preserved-artifact:sha256:b95108bd6d61035fd94be39a4ae129cc7326b1268e50421192f8ef67db8a0769 \
--reviewer fw-lead \
--note "Boot to idle within budget on a cold start; covers the cold-start path only"
sbomflow review evidence \
--accept preserved-artifact:sha256:bc436848c5f473d1340595d27e4bdfb3dede7c9560d2e104490d6dc4def80121 \
--reviewer fw-lead \
--note "Re-examined for 2.4.1; the control loop is unchanged in this release"
sbomflow review evidence \
--reject preserved-artifact:sha256:314c8e74fca056de8936fd7165296462b271a16c6378cdc87bc6a523644a2275 \
--reviewer fw-lead \
--note "Hard fault in the sensor driver; a fix is required before release"
sbomflow review evidence \
--needs-more-evidence preserved-artifact:sha256:33a411897a3cd1bd74870a022b5829e6239f04cdf766887b5d09d6c4b4ba6e7d \
--reviewer fw-lead \
--note "Trace window ends before the fault; a longer capture is needed"
sbomflow review evidence \
--accept preserved-artifact:sha256:4b5abb25aa89029fd7c18889c4f755c6935ea70162b26d2c325df24142f9454f \
--reviewer fw-lead \
--note "Alert from a 2.4.1 field unit; the fault is decided on its core dump"
sbomflow audit . --output evidence --reviews evidence/reviews.json --previous-output ../2.4.0/evidence --zip
sbomflow validate evidence
sbomflow verify-bundle evidence/evidence-bundle.zip --layered --require-layers contentverify-bundle (layered): NOT TRUSTED
content verified
envelope not present [not_present] — no signature in the
bundle [no_signature_artifact]
required content — all met- A
--rejectrecords a person's conclusion. It does not block the release: no decision about a preserved file reaches the release gate. Acting on it is the release owner's call. --zipwritesevidence/evidence-bundle.zip, a deterministic handoff. It carries the five records underpreserved-artifacts/andpreserved-artifacts.json, and none of the captures.content verifiedmeans every member re-hashed to the digest the bundle's manifest records. The verdict readsNOT TRUSTEDbecause the bundle is not signed: an unsigned bundle is checked only against itself. Sign it withsbomflow sign-bundlewhen the recipient needs to know who packaged it, and verify with--key.- Each layer's status and reason are printed in words with the code in brackets, such as
not present [not_present];--jsoncarries the codes alone.
7. What tampering looks like#
Someone edits one of the record copies in the evidence directory:
echo "edited" >> evidence/preserved-artifacts/b95108bd6d61035fd94be39a4ae129cc7326b1268e50421192f8ef67db8a0769.json
sbomflow verify-bundle evidence --layered --require-layers content # exits 3 content failed — a member does not match the
manifest [member_integrity_failed]
· member changed [member_hash_mismatch]:
preserved-artifacts/b95108bd6d61035fd94be39a4ae129cc7326b1268e50421192f8ef67db8a0769.jsonvalidate names the same copy, against its own record, and exits 4:
sbomflow validate evidence # exits 4 - preserved-artifacts.json: rows[3].records[0]: copy preserved-artifacts/b95108bd6d61035fd94be39a4ae129cc7326b1268e50421192f8ef67db8a0769.json does not hash to its record_sha256 [rows[3] is the row for boot-to-idle.psf; records[0] is its record 1]Both commands only read: re-running them gives the same verdict until the file changes back.
Someone changes the original trace after it was reviewed. One appended line is enough:
echo "edited" >> captures/boot-to-idle.psf
sbomflow audit . --output evidence-after-edit --reviews evidence/reviews.json
sbomflow review evidence-after-edit - boot-to-idle.psf
preserved-artifact:sha256:b95108bd6d61035fd94be39a4ae129cc7326b1268e50421192f8ef67db8a0769
Original CHANGED (hash differs)
Recorded accepted, NOT applied: the supplied original does not
match the bytes the record describes
(original_verification is mismatch), so this decision is
about other bytes; it is shown and suspended, never kept
Capture states stated image is in this releaseThe reviewer's decision is still shown, with the reason it is not applied. A conclusion about other bytes never carries over silently.
What SBOMFlow reads from your file#
For most trace and crash formats, SBOMFlow reads nothing inside the file: it records the digest, what you stated, and the review. A few formats declare something about themselves in their header — a version, a trace UUID, the tool or host that wrote them, occasionally a build id. For those, and only those, SBOMFlow reads the declared header fields under a cited basis:
- a published specification, cited by title, version and address; or
- open-source code that defines the format, when no specification document exists: an OSI-approved licence, pinned to an exact commit with the defining file's SHA-256, header and identity fields only, an unrecognised version refused by name, byte order taken from a marker in the file, and a row that says
basis: source code.
The file's name selects which reader may run, and the leading bytes must confirm it. A file whose bytes do not match is reported as unreadable (or, under a name several formats share, as not applicable) and is never handed to another reader. Every value carries the citation and an origin. extracted is a value the file stores, and is what the file says — not something SBOMFlow verified. computed is a value SBOMFlow produced from the bytes (a count, a derived number, a position it searched for, the outcome of a check), and looked_up is a name SBOMFlow took from a pinned table for a number the file stores under another field, such as the kernel named from a TraceRecorder kernel id. Neither is ever recorded as extracted, and the build-id comparison never lists either as a string the file declares; explain and custody show print "computed by SBOMFlow" or "looked up by SBOMFlow" beside them.
| Format | Selected by | Basis | What is read | Build id compared with the release's images |
|---|---|---|---|---|
| sarif | .sarif, .sarif.json | specification: SARIF 2.1.0 Plus Errata 01 | tool name and version, invocation start and end, automation id, version-control revision and repository | no |
| pcapng | *.pcapng | specification: pcapng, draft-ietf-opsawg-pcapng-06 | pcapng_version, shb_hardware, shb_os, shb_userappl, and each interface's if_name, if_description, if_os, if_hardware | no |
| elf-package-note | .elf, .so, .axf, .out | specification: UAPI.8 Package Metadata for Executable Files 1.0 | build_id and the package note's package_name, package_version, package_type, package_architecture, package_os_cpe, package_app_cpe | no |
| ctf1-metadata | .ctf, .ctf-metadata, metadata | specification: Common Trace Format v1.8.3 | the CTF version, the trace UUID, major and minor, and the env entries; for a packet-based stream (as LTTng writes it), the first metadata packet header's byte order, UUID, version, sizes and schemes, the TSDL text read across packets only as far as the walk goes, and a compression, encryption or checksum scheme named and not decoded | no |
| ctf2-metadata | .ctf2, .ctf2-metadata, metadata | specification: CTF2-SPEC-2.0 | the preamble's version and UUID; the trace class's namespace, name, uid and environment | no |
| perf-data | .perf-data, .perf.data, perf.data | specification: perf.data format, Linux v6.12 | kernel release, perf version, architecture, and the build ids of HEADER_BUILD_ID; the host name and command line are withheld | build_id |
| android-bugreport | .bugreport-zip, .bugreport.zip, bugreport-*.zip | specification: Bugreport file format, android-15.0.0_r1 | version.txt and the main entry's name | no |
| afl-fuzzer-stats | .fuzzer-stats, .fuzzer_stats, fuzzer_stats | specification: The afl-fuzz approach, AFL++ v5.03c | afl_version, target_mode, start_time; the command line is withheld | no |
| lcov-tracefile | *.info | specification: geninfo(1) TRACEFILE FORMAT, LCOV v2.5 | test names, source paths, source version IDs and record counts; no coverage figure | no |
| zephyr-coredump | *.zephyr-coredump, coredump.bin (a name other formats share) | specification: Zephyr core dump File Format, v4.4.2 | the 12-byte header: magic, header_version, target_code, pointer_size_log2, flags, fatal_error_reason; declares no build identity | no |
| zephyr-coredump-log | *.zephyr-coredump-log, coredump.log (a name other formats share) | source code: coredump_serial_log_parser.py, Apache-2.0, commit dccb09599635bdff17633fa7e9dab014b91dce90, sha256 4d05eb85ce73a4f791e529ab671c0ddf9903fa86f3d0112e474678cc1a596fd6, with coredump.h and the logging backend; the header as the File Format section specifies it | a console log holding the dump Zephyr's default logging backend prints: where #CD:BEGIN# is, the #CD: lines up to #CD:END# and how many bytes they decode to, other log lines between the markers (counted), whether the end or error marker was printed, and the 12-byte file header the dump begins with, read as zephyr-coredump reads it; no other decoded byte; declares no build identity | no |
| crashcatcher-dump | .crashcatcher, cc_coredump.dmp, .dmp (a name other formats share) | specification: CrashCatcher README, Dump Format | eight bytes: magic, version_major, version_minor, flags; declares no build identity | no |
| memfault-coredump | *.memfault-coredump | specification: Memfault Coredump Format | the header, every block's type, the Build ID and Software version blocks verbatim, a count of blocks per type | no |
| elf-core | .core (a name other formats share), core (a name other formats share), core.[0-9] (a name other formats share), vmcore (a name other formats share) | specification: elf(5) with the System V gABI | the file type, class, byte order and machine, and each note's owner, type number and size; declares no build identity. In an ESP-IDF core, the ESP_CORE_DUMP_INFO note's version and the application ELF's SHA-256 prefix (basis: source code, pinned ESP-IDF), set beside the release's ELF images as context, never a match | no |
| esp-core-dump-image | *.bin (any such file, read only when the format's marker is found within its first 28 bytes of a file carrying 0x01 at offset 5, whose length word at offset 0 fits the file) | source code: loader.py, Apache-2.0, commit d54b4a611c34efbc75cae1f862f58097f909261c, sha256 0b6f9d838b109ff73e2c145d30231562321452e5ddfe913359209e39f452e795, with the ESP-IDF writer's header for each version | the core-dump image ESP-IDF stores in its partition, as esptool reads it back: the header's total length, version word (chip id and data format), chip revision, whether the CRC-32 or SHA-256 checksum holds (computed exactly as Espressif's decoder computes it; the bytes are hashed, never decoded) and the bytes after the image; then the ELF core inside it, read as elf-core reads it, including the ESP_CORE_DUMP_INFO note's application ELF SHA-256 prefix; the older binary (non-ELF) data format is refused by name; declares no build identity | no |
| minidump | .mdmp, .dmp (a name other formats share) | source code: Breakpad minidump_format.h, BSD-3-Clause, commit 868abe6f016cfeabfb7208681e89031ae667c336, sha256 6e88e2f06e4fe60b8a3dee7f1e39e001c799f69ac3f244d3e36295c38c834c3c, with the vendor's minidumpapiset.h reference | the header, a count of streams per type, and each module's file name, size, timestamp, version and CodeView record as opaque hex | no |
| intel-hex | .ihex, .ihx, *.hex (a name other formats share) | specification: Hexadecimal Object File Format Specification (Intel) | every record's framing: a count per record type, checksums that hold and do not, malformed records named by line, the start address and the merged address ranges the data records load; no data byte; declares no build identity | no |
| motorola-srec | .srec, .s19, .s28, .s37, *.mot | specification: M68000 Family Programmer's Reference Manual (Motorola), Appendix C S-Record Output Format | every record's framing: a count per record type, the S0 header's text, checksums that hold and do not, malformed records named by line, the start address and the merged address ranges; no data byte; declares no build identity | no |
| uf2 | *.uf2 | specification: USB Flashing Format (UF2), README.md sections File format, Flags and Family ID | every 512-byte block's framing: blocks by flag, each family ID with its short name, block totals, missing and repeated block numbers and the merged address ranges written to main flash; no data byte; declares no build identity | no |
| dfm-alert | *.dfm-alert | source code: dfmEntry.c, Apache-2.0, commit c3055e4b6ae5627407b002e200f769596b7cfe01, sha256 aab4b74bb2b09f419458699e4355c0281cb1982117fa1bc61457ca2ff8071ee6 | per entry its type, alert id and chunk numbers; for the first alert its session id, device name, description, product, alert type and the firmware version string it declares, and in a file holding several alerts every later alert's firmware version (all set beside the release version as declared_version_context, never matched or verified); the checksum word is shown and not verified | no |
| dfm-alert-serial-capture | *.dfm-alert-log | source code: dfmCloudPort.c, Apache-2.0, commit c3055e4b6ae5627407b002e200f769596b7cfe01, sha256 5e5d00ed4888d1b8ea4555c719090c5383ce35ecb18fb527ad5b6edf0043a6d1 | the framed hex between the port's begin and end lines, decoded one entry per block and read as a .dfm-alert file is, with the block count and the lines outside the framing counted | no |
| tracerecorder-psf | trace.psf, *.psf (a name other formats share) | source code: trcStreamingRecorder.c, Apache-2.0, commit c3055e4b6ae5627407b002e200f769596b7cfe01, sha256 b192aa2af301a3e8a2ea5a682e3bc5d9872c594df60919156cee3171f4d8062f | the 32-byte header: byte order, format version (only 0x000e), the kernel id named through the pinned kernel ports (for example FreeRTOS or Zephyr), the option bits, core count and the platform definition's name and version; then, in a one-core recording whose start event follows its entries, the timer frequency, the OS tick rate, entry counts by class and each extension's name and version, with object names withheld; declares no build identity | no |
| tracerecorder-ringbuffer | *.bin (any such file, read only when the format's marker is at offset 4) | source code: trcStreamPort.h, Apache-2.0, commit c3055e4b6ae5627407b002e200f769596b7cfe01, sha256 58ef02c658f522365e22b2aea3f0df0839152d5bfa6815d82a6cdae07dd68758 | the streaming header inside a RingBuffer snapshot, read as a .psf header is, and whether the end markers sit where the declared sizes put them; declares no build identity | no |
| esp-app-image | *.bin (any such file, read only when the format's marker is at offset 32 of a file opening with 0xE9) | source code: esp_app_desc.h, Apache-2.0, commit 4d59230ddff16327812782151ef0afef202dc6d7, sha256 452c93fdd0c1dfe6396cee13f5d95d71ab304bdd4565d696d80d76a2e269a6b0 | the application descriptor esp_app_desc_t at offset 32: esp_app_version, esp_app_project_name, esp_app_compile_time, esp_app_compile_date, esp_app_idf_version and esp_app_elf_sha256, the SHA-256 of the application ELF that esptool writes, set beside a release ELF's digest | no |
| coresight-snapshot | snapshot.ini (a name other formats share) | specification: Debug and Trace Snapshot File Format (Arm document ARM-ECM-0611873) | the snapshot version (only 1.0), the device files [device_list] names, the clusters and the trace metadata file it names; paths recorded as text, never opened; declares no build identity | no |
| coresight-snapshot-trace-metadata | trace.ini (a name other formats share) | specification: Debug and Trace Snapshot File Format (Arm document ARM-ECM-0611873) | each trace buffer's id, name, format and file list, which core each trace source belongs to and which buffers each source writes to; never the buffer bytes; declares no build identity | no |
| coresight-snapshot-device | *.ini (any such file, read only when the format's marker is found within its first 4096 bytes) | specification: Debug and Trace Snapshot File Format (Arm document ARM-ECM-0611873) | a device file's [device] name, class and type (a core such as Cortex-M55, a trace protocol such as ETM4); never registers, dumps or memory images; declares no build identity | no |
| tracerecorder-streamport-buffer | dfm_trace.psfs, *.psfs (a name other formats share) | source code: trcStreamPort.h, Apache-2.0, commit c3055e4b6ae5627407b002e200f769596b7cfe01, sha256 58ef02c658f522365e22b2aea3f0df0839152d5bfa6815d82a6cdae07dd68758 | the same header inside the buffer a DFM alert attaches, found by a bounded search for its start markers, with the offset found; declares no build identity | no |
| tracerecorder-legacy-snapshot | dfm_trace.trc, *.bin (any such file, read only when the format's marker is at offset 0) | source code: trcRecorder.h, Apache-2.0, commit 225720467bd286267377d588e8581dc337abc4c8, sha256 0b46b376448eea5dfdc9aafe34b02a85e4a47e17acf7c823e0ad848d6ada3aa7 | the v4.10.3 RecorderData head: byte order and kernel id from the version word, the minor version (only 7), the IRQ priority order, the declared file size and whether the end markers are there; declares no build identity | no |
| px4-ulog | *.ulg | specification: ULog File Format (PX4 Guide, docs/en/dev_log/ulog_file_format.md) | the header's version and timestamp, the flag bits, and the identity information keys the format page predefines (sys_name, ver_hw, ver_sw, ver_sw_release, sys_os_name, sys_toolchain, sys_mcu and others); sys_uuid is withheld; a version-0 log (older PX4) is read as the pinned pyulog reads it, its version's note citing that source | no |
| mcap | *.mcap | specification: MCAP Format Specification (foxglove/mcap website/docs/spec/index.md) | the major version, the Header's profile and library, the footer offsets and the metadata records the summary indexes; declares no build identity | no |
| rosbag2-metadata | metadata.yaml (a name other formats share) | source code: yaml.hpp, Apache-2.0, commit d51dd4227726cb7cfdcd4a715a6ea97a382a1ec4, sha256 4cf85d3fe03a2ca6e54bec218d253c2e86b72774e9dc17e3d9e8088a265d0d4a | version, storage_identifier, duration, start time, message count, each topic's name and type, and ros_distro; declares no build identity | no |
| trace-cmd-dat | trace.dat (a name other formats share), *.dat (any such file, read only when the format's marker is at offset 0) | specification: trace-cmd.dat.v7(5), sections INITIAL FORMAT, FORMAT OF THE SECTION HEADER and OPTIONS SECTION (Documentation/trace-cmd/trace-cmd.dat.v7.5.txt), with trace-cmd.dat.v6(5), section INITIAL FORMAT, for file version 6 | file version, byte order, long size and page size; for version 7 the compression algorithm and the UNAME (system name, kernel release, machine), VERSION and TRACEID options; the host name is withheld | no |
| perfetto-trace | .perfetto-trace, .pftrace | source code: trace_packet.proto, Apache-2.0, commit 11fbaed836361258f23604daf67c837f3034e663, sha256 721c51c7cf3efbbadef58805d2217cd9bc7ce216962c8de1d25103bc200999c0 | from the first packets: utsname (sysname, version, release, machine), android_build_fingerprint and the trace UUID | no |
| threadx-tracex | *.trx (a name other formats share) | source code: tx_trace.h, MIT, commit 87ab09cce305eb9cd4aacac4e8c62af72f665bff, sha256 5cced857264b33e3d54fbf9294e8b5911a270ad4ce954211f525040c1747d79e | the 48-byte control header: byte order, timer valid mask, base address, registry and event-area pointers, object name size, and the registry entry count, event capacity and declared extent derived from them; declares no build identity | no |
Only a field in the last column is ever compared with the GNU build ids of the release's firmware images; a declared version or any other string is listed, never matched or verified (a declared firmware version is shown beside the release's stated version in declared_version_context, as context only). A match means the two files name the same build. It does not show the capture was taken on a device running that image. The full list of fields each reader reads, and the fields it never reads as a value, is in Keeping runtime and test evidence with a release, and every cited address and digest is in Supported inputs.
Recognised, and declaring nothing. A file named *.zephyr-ctf is recognised: the CTF metadata Zephyr ships declares only the CTF version and byte order, with no trace UUID and no environment block, so the row says so rather than staying silent.
Opaque formats. A format that is neither published nor defined by open-source code is never read, whatever its name, and no reader may claim it. In this build that is:
.svdat— SEGGER SystemView recordings.
An opaque file's row reads not_applicable with that meaning, in a release run's preserved-artifacts.json and in custody show --json alike, so "closed, never read" is never confused with "no reader yet".
For an opaque file, and for every format not in the table above, SBOMFlow still records everything the journey needs: the file's SHA-256 and size, the extension hint, what you stated (release, scenario, tool, firmware image, capture session), the re-hash on every run, the firmware link, the review decision, the drift between releases and the bundle entry. Only the header read is missing.
Tracealyzer and TraceRecorder files#
Percepio's TraceRecorder library is built into firmware on FreeRTOS, Zephyr, ThreadX, ESP-IDF and bare-metal targets. Tracealyzer reads the traces it writes, and a Percepio Detect alert can carry one beside a core dump. Nothing in SBOMFlow connects to those tools; this section says what happens to their files in this build.
| File | What it usually is | What SBOMFlow records in this build |
|---|---|---|
.psf | a TraceRecorder streaming trace, for example trace.psf | the header: byte order, format version, kernel id and the kernel it names, option bits, core count, platform definition and its version (tracerecorder-psf); where their place is certain, the timer frequency and OS tick rate, how many entries of each class the recorder registered and each extension's name and version, never an object's name; digest, size, the hint psf_extension_trace, your statements, the review. No event is read |
RingBuffer snapshot dump (*.bin) | a TraceRecorder snapshot-mode buffer saved from target RAM by a debugger, under a name you choose | the streaming header inside it and whether its end markers sit where its sizes put them, but only when its start markers are at offset 4 (tracerecorder-ringbuffer); any other .bin is recorded by digest. State --artifact-kind runtime_trace |
.psfs | the trace a Detect alert carries (dfm_trace.psfs) | the same header, found by a bounded search for the buffer's markers (tracerecorder-streamport-buffer); digest, size, the hint psfs_extension_trace, your statements, the review |
dfm_trace.trc, legacy snapshot *.bin | a TraceRecorder v4.10.3 and earlier snapshot (RecorderData) | the head: byte order, kernel, minor version, declared size and whether its end markers are there (tracerecorder-legacy-snapshot) |
*.dfm-alert | the alert entries a Detect alert writes | each entry's type and ids; the first alert's device name and description, and the firmware version every alert declares, shown beside the release's stated version as declared_version_context, never matched or verified (dfm-alert) |
*.dfm-alert-log | the same alert, printed on a serial console | the framed hex decoded one entry per block and read as a .dfm-alert file (dfm-alert-serial-capture) |
cc_coredump.dmp | the CrashCatcher core dump a Detect alert carries | the CrashCatcher header, read as in the table above, plus everything any record carries |
Beside the trace: the firmware image and a CoreSight snapshot. On an ESP-IDF target the application image you flash (<project>.bin) carries its own descriptor, including the SHA-256 of the application ELF it was built from. SBOMFlow reads it only from a file that opens with the image header's byte 0xE9 and has the descriptor's magic word at offset 32 (esp-app-image), and sets that digest beside the release ELF's. Any other .bin is read exactly as before. An Arm CoreSight trace kept as a snapshot directory is read from its snapshot.ini, trace.ini and device .ini files: the devices, cores, trace sources and buffers they declare (coresight-snapshot, coresight-snapshot-trace-metadata, coresight-snapshot-device). Trace bytes, memory images and registers are never read. Neither names a build by itself; both are context for a reviewer.
Kernel ids. A TraceRecorder header records which kernel port produced the trace as a number (kernel_id). SBOMFlow shows the number as the file carries it and, beside it, the kernel that number means in the pinned library's kernel ports (FreeRTOS, Zephyr, ThreadX, BareMetal); a number no pinned port defines is shown alone, with a note saying it is outside the table. The name is a lookup, and the row's note says so: the file declares the number, not the name.
The entry table, and the names it keeps back. After the header a streaming trace carries the recorder's timestamp block and its entry table: one entry for each extension an instrumenting library registered, and one for each task, interrupt, queue or other object the application named. SBOMFlow reads them only where their place in the file is certain — one core, not ThreadX, and the start event the library writes next found right after the entries — and otherwise says why not and keeps the header. From the table it shows how many entries of each class there are and each extension's name and version; the names your application gave its tasks and objects can carry product or customer text, so they are never read, only counted, and the row says how many were withheld. A trace the pinned library wrote in a host harness with one extension and eight named entries reads:
| Field | Value |
|---|---|
timestamp_frequency_hz | 1000000 |
os_tick_rate_hz | 1 |
entry_count | 9 |
entries_object, entries_extension, entries_state_machine, entries_state_machine_state, entries_interval_channel, entries_counter, entries_interval_channel_set | 3, 1, 1, 1, 1, 1, 1 |
extension_name[1], extension_version[1] | HarnessExt, 1.2.3 |
withheld entry_names | 8 entry names |
The example's boot traces carry these blocks, written from the pinned layout: their rows show five entries, one of them the syscalls extension, and withhold four entry names.
Keep an alert's files together. A Detect alert, like most crash-reporting alerts, arrives as more than one file. Record each file with its own record and give them the same --capture-session, as step 5 does: three digests, three review keys, three decisions, one session a reviewer can see. Keep the alert's own entries (*.dfm-alert) beside its payloads: they are the one file of the alert that declares a firmware version.
Build identity in practice#
A GNU build id is the strongest link a file can carry to a build, and most microcontroller firmware does not have one. Zephyr links with --build-id=none by default: line 7 of cmake/linker/ld/linker_flags.cmake at zephyrproject-rtos/zephyr commit 1ee3b93134be23c16ab16b9eb4183bfef63447a6 (Apache-2.0, sha256 d8fb87566fa172f7bec84b3b0fa80a062779df22615c78aca8674fa247bd1abc) sets it for every application, and the proposal to change that default, issue 54522, was closed as not planned. Many bare-metal projects do not pass the flag at all. The captures do not fill the gap: the TraceRecorder, DFM alert, CrashCatcher and Zephyr core dump formats in the table above declare no build id.
So for most firmware the link between a capture and a build is the image digest, and SBOMFlow treats it that way:
--firmware-imagehashes the image at capture time, and the release run compares that SHA-256 with the image the release declares and the files it scanned. When they are the same bytes,firmware_imagereadsin_releaseandcapture_firmwarereadsstated_image_in_release, whether or not the image carries a build id.- Where no build id exists,
build_id_matchreadsnot_establishedwith the reason (the file declares none, or the release's images carry none). That describes the formats, not a failed check, and it reaches no gate. - A version a file declares, such as a DFM alert's firmware version, is set beside the release's version as
declared_version_context, never matched.
This example's stand-in images carry a build id only so the comparison can be shown. If you want one in your own images, make the link pass --build-id=sha1 instead of --build-id=none. The id is written into a .note.gnu.build-id section of the ELF; if you also ship a .bin or .hex, check that your linker script places that section in a loaded region, or the load image will not carry it.
Which field answers which question. Context only: the questions a reviewer, a customer or an internal audit commonly asks of a test or incident record, and where this record keeps what was stated or observed. It does not say any record satisfies a standard, a regulation or a customer requirement, and SBOMFlow does not decide whether the evidence is sufficient.
| The question | Where the answer is | Stated or observed |
|---|---|---|
| Which file is this, and is it still the same file? | the record's artifact.sha256 and artifact.size_bytes; the row's run_checks.original_verification | observed: hashed on every run |
| Which software was running? | firmware_image_sha256 and firmware_image_name in the record; run_checks.firmware_image and run_checks.capture_firmware in the row | the image digest is observed when you pass --firmware-image; the build id and version you type are stated |
| Does the file itself say which firmware it came from? | run_checks.original_verification.file_declared_identity, and declared_version_context where the format declares a version | declared by the file, never verified |
| Which release is it filed against? | release_id in the record; the release run that declares it | stated |
| What was tested, and how? | test_scenario, test_environment_statement, test_configuration_sha256 (the file you pass with --configuration-file, hashed), procedure_ref | stated; the configuration file's digest is observed |
| With which tool? | source_tool and source_tool_version | stated |
| When, and who kept it? | captured_as_of and preserved_by | stated, and listed as missing when nobody stated them |
| Who performed it, and how independent were they? | test_performer and performer_independence | stated |
| Which event did it come from? | capture_session | stated |
| What did a person conclude, and within what limits? | the row's review: status, reviewer and note, bound to the file's digest | a human decision |
| What changed since the previous release? | preserved-artifact-changes.json from compare-releases; the row's prior_decision_context | observed, by digest |
| How long must it be kept? | the row's retention_statement, from --retain-until | stated; nothing is deleted |
Other tools#
The journey is the same for any tool. What changes is only whether a header is read:
| Tool or format | Typical file | In this build |
|---|---|---|
| Zephyr core dump | *.zephyr-coredump, coredump.bin | header read |
| Zephyr core dump printed by the logging backend (the default) | the console log, kept as coredump.log or *.zephyr-coredump-log | the dump's header read from its #CD: lines; the framing counted |
| CrashCatcher (Cortex-M) | cc_coredump.dmp, *.crashcatcher, crash.dmp | header read |
| Breakpad, Crashpad and Windows minidumps | .mdmp, .dmp | header and module list read |
| Linux and gdb core files | core, core.<pid>, *.core, vmcore | ELF header and note list read |
| ESP-IDF core dumps (ESP32 family) | the core-dump partition read back as a .bin; an ELF core saved as *.core | image header, checksum and the ELF core's note list read; the application ELF digest prefix set beside the release's ELF |
| Memfault MCU core dump | *.memfault-coredump | header, block types, Build ID and Software version read |
| LTTng, barectf and other CTF writers | the trace directory's metadata | CTF 1.8 or CTF 2 metadata identity read |
| Zephyr CTF tracing | *.zephyr-ctf | recognised; declares no identity |
Linux perf | perf.data | header identity and build ids read |
| Wireshark, tcpdump and dumpcap | *.pcapng | section and interface header options read |
| Microcontroller load images (Intel HEX, S-record, UF2) | .hex, .ihex, .srec, .s19, *.uf2 | every record's framing walked (no data byte), and the bytes compared with the paired ELF beside it |
| Eclipse ThreadX event trace (TraceX) | *.trx | control header read |
| trace-cmd, KernelShark (Linux ftrace) | trace.dat, *.dat | initial format and UNAME, VERSION and TRACEID options read; host name withheld |
| Perfetto (Android, Linux) | .perfetto-trace, .pftrace | SystemInfo and trace UUID from the first packets read |
| PX4 flight log | *.ulg | header, flag bits and the predefined identity information keys read; sys_uuid withheld |
| MCAP recordings (ROS and others) | *.mcap | header profile and library, and the metadata records the summary indexes, read |
| ROS 2 bag | metadata.yaml | version, storage, duration, message count and topics read |
| SEGGER SystemView | *.svdat | opaque: recorded, never read |
| Any other trace, log or dump | any name | recorded by digest; nothing read |
A file you rename to a name a reader claims is still checked by its leading bytes: renaming never makes SBOMFlow read something it cannot confirm.
FAQ#
Does SBOMFlow parse my trace? No. It never decodes events, samples, packets, registers or memory, and derives no finding from a trace. For the formats in the tables above it reads only the identity fields a file's header declares about itself — for a CrashCatcher dump that is eight bytes — and for a microcontroller load image the framing of each record, never a data byte. For an ESP-IDF core-dump image it also checks the image's own checksum, which hashes the bytes and decodes none of them. Everything else is recorded by digest.
Is my data uploaded? No. Every command on this page is offline, and the test suite that executes this page fails if any step opens a network connection. The originals are never copied into the evidence directory, the bundle or the ZIP; only the records are. Records contain what you typed, so do not put a customer's or a person's name into a free-text field you intend to share.
What if my tool's format is unknown? It gets the same journey without the header read: digest, statements, firmware link, review, drift, bundle, verification and tamper detection. The extension hint then reads unrecognised, which describes the file name, not the file. A reader is added only for a format with a published specification or with open-source code that defines it, under the rules above.
Can a trace block a release? No. A trace, a core dump, a missing decision, a rejection, a mismatch or a firmware disagreement changes no release gate, VEX status or approval digest, and no --fail-on-* option reads them. What it changes is what a reviewer sees: counts in START-HERE.md, the review queue, next-actions and the reviewer console. The release decision stays with the people who make it.
How does this help with the CRA or other regimes? As context. Whoever asks later — a customer questionnaire, an integrator, an internal handover, or a requirement regime your product falls under — reads the same record: which evidence was examined for which release, and what the reviewer concluded. SBOMFlow does not say whether that evidence is sufficient for any regime, maps no preserved file to a requirement, and makes no conformity claim. Reports are engineering gap assessments, not legal advice.
Can I keep files before a release run exists? Yes. A custody set keeps records, decisions and an audit log in a directory you own, with no scan and no release, and joins a release run later. See Keeping runtime evidence in custody.
Commands used on this page#
| Command | Purpose here | Reads | Writes | Exit codes | Reference |
|---|---|---|---|---|---|
sbomflow ingest-preserved-artifact | record one trace or dump, and the image it ran on | the file, hashed and never interpreted; --firmware-image, hashed | one record at --output | 0 ok · 2 a missing or symlinked artifact, a refused value or a destination collision | reference |
sbomflow audit | build the release record and join the declared records | the project, sbomflow.yaml, each declared record and original | the evidence directory, including preserved-artifacts.json, release-record.json and, with --zip, evidence-bundle.zip | 0 ok · 1 an enforced gate blocked the release · 2 usage, input, IO or config error · 4 a written artifact failed validation | reference |
sbomflow review | show the queue, and record a decision about one file | an evidence directory | reviews.json in that directory | 0 ok · 2 usage error or a refused decision | reference |
sbomflow compare-releases | compare two releases | two evidence directories | release-drift.json and preserved-artifact-changes.json into --output | 0 ok, including a comparison where one side was unavailable · 2 usage or input error, or with --require-both-releases a missing side | reference |
sbomflow validate | re-check every artifact and copy in an evidence directory | an evidence directory | the report on standard output | 0 every present artifact is valid · 2 the directory is missing or unusable · 4 an artifact is malformed or changed, or a pack whose producer always writes run-completion.json has none | reference |
sbomflow verify-bundle | re-hash a bundle directory or ZIP | the bundle, and a public key when one is given | the result on standard output | 0 ok · 2 a missing bundle, or a usage error · 3 the signature or a listed file failed verification · 7 --layered: a layer --require-layers named did not verify, or --refuse-extras found a member the manifest does not list | reference |
Every option of every command is in the CLI reference.
See also#
- Keeping runtime and test evidence with a release — every option, state and refusal, including test reports, laboratory reports and fuzzing campaigns.
- Keeping runtime evidence in custody — custody sets, handover exports and intake manifests.
- Supported inputs — the reader table with every citation.
- Observed vs reviewed — why SBOMFlow observes and people decide.