Warning codes
Every scan warning SBOMFlow emits carries a stable code from a versioned catalog. Warnings are how SBOMFlow honours its core rule: malformed or recognized-but-unsupported input is surfaced, never silently ignored. A warning is an observation with an exact file path — it is not a failure, a vulnerability verdict, or a compliance conclusion.
Note
This page is generated from the product's warning catalog, so it always
matches the codes your installed version can emit.
How to work with warnings#
- Every warning appears in
scan-warnings.jsonwith the exact relative path of the input that triggered it and a human-readable reason. The one exception is theaicategory: those codes are recorded in thescan_warningsarray ofai-evidence.json, because they describe the AI inventory pass rather than the component scan. They carry no per-file path there, and--strictcannot match them. The path is relative to the scan root and does not depend on the directory you ran from, so the same target records the same path on every machine. - A path shown as
external/<name>means the input sits outside the scan root — an operator-supplied file elsewhere on the host, recorded by leaf name so the artifact carries no local directory layout. It is a display form, not a finding: a genuine refusal to read outside the product has its own code (symlink_outside_root). - The CLI summary prints the warning count; open
scan-warnings.jsonfor the details, or the HTML report for a readable view. - Explain any code offline with
sbomflow explain <output-dir> --warning <code>— it prints the code's catalogued meaning and the exact inputs that triggered it in that run (omit the code to list the codes seen in the run). - With no output directory to hand,
sbomflow help warning <code>prints the same catalogued meaning run-independently — for reading a colleague'sscan-warnings.json(omit the code to list every code by category). - Strict mode (
analyze --strictor--strict=code1,code2) re-runs your policy after all artifacts are written and exits with code5when matching warnings are present — so CI can fail on the codes you care about without losing the evidence pack. Codes passed to--strictare validated against this catalog. See Exit codes. - Severity below is the default severity; it describes how much attention a warning usually deserves, not a security rating.
Severity levels#
| Severity | Meaning |
|---|---|
| info | A recognized situation worth knowing about — for example, a recognized dependency manifest that is not parsed for components yet. |
| warning | Something was malformed, out of bounds, or could not be used as intended. The scan continued; the input was recorded. |
Codes by category#
Ai#
| Code | Default severity | Meaning |
|---|---|---|
ai_adapter_base_ambiguous | warning | The base model name an adapter declares matches more than one model in this release, and they disagree. No match is chosen, because picking one would attribute a lineage nobody established. Resolve which model is meant before treating the adapter's provenance as known. |
ai_adapter_base_not_declared | warning | An adapter configuration names no base model at all, so what it modifies cannot be established from the release. This is the most serious of the adapter lineage states: an adapter's behaviour is a property of the pair it forms with a base, and an adapter that will not say which base it was built on cannot be reasoned about at all. Ask its author. |
ai_adapter_base_unresolved | info | An adapter names a base model that is not present in this release. The relationship is still RECORDED, as a declared reference to a declared entity — never dropped and never assumed present. This is the ordinary shape of an adapter-only release and is not an error; what it means is that the pair the adapter's behaviour depends on is only assembled at deployment, outside anything this scan can see. |
ai_adapter_config_unreadable | warning | A file named as an adapter configuration could not be opened, decoded or parsed, so nothing it declared is in this run — including the base model it names. An unreadable adapter config and an absent one are different facts, and neither supports a statement about what the adapter modifies. |
ai_adapter_weights_absent | warning | An adapter configuration is present with no adapter weights beside it, so this tree declares an adapter it does not contain. The declaration is kept — it carries the lineage, which is evidence in its own right — but nothing here is the adapter itself. |
ai_agent_reference_unresolved | warning | A declared AI agent references a model, prompt or service that is neither declared in the same manifest nor present in the evidence graph, so no relationship could be recorded for it. The reference is kept verbatim on the agent under unresolvedReferences rather than dropped, and no entity is invented to satisfy it — inventing one would assert something nobody observed or declared, and dropping it would make the record read as though the reference had never been written. Usually a typo or a reference to something outside this release; it is never a finding about the referenced thing. |
ai_agent_without_autonomy_level | info | A declared AI agent does not say how much it may do without a human in the loop, so its autonomy is recorded as undeclared. undeclared is deliberately NOT a member of the ordered autonomy vocabulary: if absence were the lowest level, every later comparison would read an undeclared agent as the most restricted one, and unknown authority would quietly become safe authority. Nothing is inferred from the agent's other fields to fill it in. |
ai_agent_without_kill_switch | info | A declared AI agent names no mechanism for stopping, suspending or revoking it. The field is recorded as absent rather than defaulted, because every default here would be a claim nobody made. This is an observation about the DECLARATION, not about the deployment: a stop mechanism may well exist and simply not be written down, and equally a declared one is only a sentence — SBOMFlow never tests that a kill switch exists or works. Declaring one would not constrain the agent and would not mitigate runtime prompt injection. |
ai_bom_ingest_duplicate_component_id | warning | A supplied AI-BOM declares the same component identifier more than once. Both entries are kept and anything they disagree about stays visible rather than one silently replacing the other. A duplicated identifier is usually a merge that went wrong upstream, and the components it names may not be the same thing. |
ai_bom_ingest_format_unrecognised | warning | A document offered as an AI-BOM does not identify itself as a CycloneDX ML-BOM or an SPDX 3.0.1 document, so nothing was read from it. Its bytes are still recorded by digest: what a supplier sent is part of the record even when nothing could be made of it. |
ai_bom_ingest_information_not_represented | info | A supplied AI-BOM carries information this build has no field for — performance metrics, ethical considerations, energy figures or a supplier's own safety assessment — and it is listed rather than silently dropped. The emit path publishes an information-loss table for the same reason: a reader needs to know what did not survive the crossing, and the crossing has two directions. Adopting a supplier's metrics as evidence would make their claim indistinguishable from a measurement. |
ai_bom_ingest_not_accepted | info | A supplied AI-BOM was read and the supplier package carrying it has not been accepted by a human, so nothing from it was folded into this release's evidence. This is the ORDINARY state of supplier evidence awaiting review, not a fault in the document — the acceptance step is what keeps a supplier's claim from entering the record unexamined. |
ai_bom_ingest_reference_unresolved | warning | A supplied AI-BOM names a component reference that resolves to nothing in the same document. The reference is recorded and no entity is invented to satisfy it: minting one would fabricate a component the supplier never described. |
ai_bom_ingest_unreadable | warning | A supplied AI-BOM could not be opened, decoded or parsed within the input budgets, so none of the supplier's declarations are in this run. A supplier's file arrives from outside and is read under the same size, structural and nesting bounds as any other untrusted input; a document that exceeds them is refused rather than partially believed. |
ai_chat_template_not_declared | info | No chat template is declared in this release. That is NOT a statement that the model needs none: a serving stack, a client library or an operator may supply one entirely outside this release, and a template supplied there is invisible to a build-time scan. Where a template IS declared it is recorded by digest, so a change between releases is visible without the body ever travelling. |
ai_dataset_croissant_version_not_declared | info | A dataset declaration states no conformance, so it does not say which specification it follows. It is still read as far as it goes and the absence is recorded rather than assumed away. This is an absence in the document, not a fault in the dataset. |
ai_dataset_croissant_version_unrecognised | warning | A dataset declaration states a conformance this build does not recognise. The value is recorded exactly as written and deliberately NOT rounded to the version it resembles: a document read as a version it does not conform to invites a reader to expect fields that may not be there. |
ai_dataset_declared_digest_disagrees | warning | A dataset declaration states a digest for a file, and the bytes of that file in this release digest to something else. Both values are recorded and neither is preferred: which one is meant is a question for whoever assembled the release. Agreement, where it happens, is agreement between two recorded values and establishes only that the file whose digest was declared is the file that is here. |
ai_dataset_declared_value_truncated | info | A value in a Croissant declaration reached the per-value character ceiling and what is recorded is a PREFIX of what was declared, not the declaration. The flag is surfaced rather than inferred because a value cut at the ceiling and a value that happened to be exactly that long are indistinguishable afterwards, and one of them is incomplete evidence. It matters most for distribution.contentUrl, which is classified by its own text, and for distribution.sha256, where a cut digest is a wrong digest and the input is untrusted so its length is not ours to assume. This is not a statement that the declaration is wrong or that the dataset is misdescribed - only that the recorded value stops short of it, and the source must be read for the rest. |
ai_dataset_depth_budget_exceeded | warning | The dataset walk hit its directory-depth budget and PRUNED that branch, continuing elsewhere. Anything below the pruned directory was never examined, so the dataset inventory in this record is partial in that branch. |
ai_dataset_entry_budget_exhausted | warning | The dataset walk hit its entry budget and STOPPED, so the dataset inventory in this record is PARTIAL. Declarations and data containers beyond the budget were never examined, and their absence from the record is evidence of the budget rather than evidence that they are absent from the release. |
ai_dataset_extension_signature_mismatch | warning | A file's extension names a data format and its leading bytes are not that format's documented signature. The file is still recorded, with the weaker identification strength, and the disagreement is the part a reviewer needs: a container whose name and bytes tell different stories was renamed, converted, truncated, or is not what the release says it is. |
ai_dataset_field_unreadable | warning | A dataset declaration states a key and no value could be read from it — it holds nothing, or holds a shape this build cannot render as text. Recorded as its own state and deliberately NOT as absence: a key somebody wrote and nobody here could read is a different fact from a key nobody wrote, and reporting the first as the second would present a broken declaration as a modest one. |
ai_dataset_identified_by_extension_only | info | A data container was identified by its filename alone, because the format has no documented magic number or its bytes carried none. It is usable evidence and it is a weaker claim than a signature match, so the two are kept apart in identified_by rather than rounded up into one. |
ai_dataset_licence_not_declared | warning | A dataset declaration states no licence, so the release does not say what may be done with the data. Recorded as a finding for the same reason an undeclared model licence is: an absent licence is not a permissive one. Where a licence IS stated it is recorded exactly as written and read against nobody's circumstances — what it permits is a legal question about the party asking. |
ai_dataset_metadata_refused | warning | A file named as a dataset declaration was read and is not one — it declares no @type naming a dataset. It is refused BY NAME, so a reviewer sees that a candidate was examined and rejected instead of seeing nothing. Nothing is guessed from the rest of the document: reading a file that does not say what it is would invent a declaration nobody made. |
ai_dataset_metadata_unreadable | warning | A file whose name says it is a dataset declaration could not be read — unreadable on disk, invalid JSON, or over the byte, entry or nesting budget an untrusted document is held to. It is RECORDED rather than skipped: a declaration somebody wrote and nobody here could read produces the same empty inventory as a release that declares no dataset at all, and those are different facts. |
ai_dataset_no_declared_metadata | info | A data container was identified from its bytes and no dataset declaration in the release names it. The file is recorded by identity with the strength of that identification, and nothing is inferred about what it holds — its rows, columns and records are never read. This is an ordinary shape for an in-house corpus and is not an accusation. |
ai_dataset_no_distribution_declared | info | A dataset declaration names no distribution, so it describes a dataset without saying where the data is. Recorded as an absence in the declaration rather than as a finding about the release — a description without a location is still a description somebody published. |
ai_dataset_rai_fields_absent | info | A dataset declaration states none of the responsible-AI fields this build recognises — collection and processing method, annotation, use cases, declared limitations, distribution restrictions. An unstated field is a claim not made, which is neither a failing nor an error, and it is recorded so that a reader knows the declaration was read and was silent rather than that nobody looked. |
ai_dataset_record_budget_exhausted | warning | The dataset walk reached its ceiling on recorded declarations or data containers and stopped adding them. What is recorded is what fitted inside the budget, and the count in this record counts what was examined rather than what is in the release. |
ai_dataset_referenced_not_present | warning | A dataset declaration names data that is NOT in this release — a URL, or a path with nothing behind it. This is the COMMON REAL CASE and it is recorded as its own state: it is a different fact from a declaration that names no data at all, and a different fact again from a release with no dataset declaration in it. Nothing was contacted to find out what the reference points at. |
ai_discovery_agent_environment_unobservable | info | An agent was observed in static configuration, and no static file states which deployment environment it runs in. Environment is part of an agent's identity — the same agent name in staging and in production holds different credentials and reaches different systems — so the observed record carries none, and it therefore cannot be matched by identity to a declared agent that names one. The two remain separate entities and both stay unverified. Guessing an environment would either manufacture agreement about the record whose authority matters most, or invent a conflict on every field. |
ai_discovery_depth_budget_exceeded | warning | The AI asset discovery walk met the max_directory_depth budget and did not descend further, so at least one branch of the release tree is unexamined and the discovery record is PARTIAL. The rest of the tree was still walked — a single over-deep branch never discards the discovery of its siblings, because that would turn a depth bomb into a way of erasing an inventory — but the absence of a declaration below that depth is evidence of the budget, not evidence that none is there. |
ai_discovery_entity_budget_exhausted | warning | AI asset discovery reached its ceiling on the number of entities one pass may record, so the list of services and agents was truncated and the discovery record is PARTIAL. The entities beyond the ceiling were recognised and are not written down. A generated deployment tree declaring thousands of services reaches this in ordinary use; it is a bound on the size of the record, never a finding about the product, and the truncation is reported so an incomplete list cannot read as a complete one. |
ai_discovery_entry_budget_exhausted | warning | The AI asset discovery walk reached the max_entries budget for directory entries and STOPPED, so the release tree was not fully examined and the discovery record is PARTIAL. Entries beyond the budget were never classified, and a service, agent or MCP server missing from the record may simply be one the walk never reached. This is a statement about the budget, never about the product: a build tree with a very large vendored directory reaches it the same way a crafted one does. Raise the budget or scan a narrower root to examine the remainder. |
ai_discovery_env_assignment_unparsed | info | A line in an environment, container, compose or infrastructure-as-code file looked like a variable assignment and its left-hand side is not a valid variable name, so it was dropped instead of being half-parsed. This is deliberate and conservative: a mis-split assignment is exactly how the VALUE ends up recorded as if it were the name, and a value is a credential. The consequence is that a variable a reader can see in the file may be missing from the recorded names, which is why the drop is reported rather than silent. |
ai_discovery_framework_not_inventoried | info | A recognised AI or agent framework is present in this build — named by a dependency, a lock file or an import — and these detectors cannot enumerate the agents, tools or services it constructs, because it builds them in code at run time rather than declaring them in configuration. The sighting is real; the inventory behind it is empty for that reason and not because the framework creates nothing. Treat the agent and service lists as incomplete wherever this appears, and supply the missing entities through a declaration. |
ai_discovery_name_budget_exhausted | warning | AI asset discovery reached its ceiling on the number of environment-variable NAMES it will take from one file and stopped reading that file, so the discovery record is PARTIAL. This budget used to be entirely silent, which was the worst possible combination: a file whose recognised provider credential appeared after the ceiling produced no service, no warning and no bound, and the artifact reported the reassuring answer as complete. A generated environment file with hundreds of unrelated variables reaches it in ordinary use; it is a bound on reading, never a finding about the product. |
ai_discovery_oversize_skipped | warning | A candidate configuration file exceeded the per-file read bound for AI asset discovery and was refused rather than truncated. A half-read configuration parses into a different configuration, and reporting that as what the build declares would be worse than reporting nothing. The file is therefore missing from the discovery record by policy, not by observation, and this code is what stops that omission from reading as 'nothing was declared there'. A configuration this large is a resource claim rather than a configuration. |
ai_discovery_scanned_file_budget_exhausted | warning | AI asset discovery reached its ceiling on files opened opportunistically — source files scanned for framework imports, and JSON files opened on the chance they carry an MCP block under a name this build does not know — so some were never read and the discovery record is PARTIAL. A framework or MCP server declared only in an unread file is absent from the record for that reason. Files matched by an exact known configuration name are read regardless of this bound; it applies only to the speculative candidate sets, which grow with the size of the whole tree. |
ai_discovery_secret_value_refused | warning | A value the AI asset detectors were about to record matched a known credential shape and was refused, so it appears in no artifact, log or message. Only the fact of the refusal travels — the matched text is never quoted, because an evidence record is copied into packs and shared bundles and a warning is written to terminals and CI transcripts. Discovery already declines to record MCP arguments and environment-variable values at all; this code fires on the remaining fields, such as a server name or a tool name that carries a token. |
ai_discovery_symlink_skipped | info | A symbolic link was found while walking the release tree for AI asset configuration and was NOT followed. Following one would let a crafted tree pull an arbitrary host file into the evidence — a real environment file from the operator's home directory, for instance — and record its variable names as if they shipped in the product. The link is skipped and said so: if a real declaration is reachable only through a link, it is absent from this record and its absence is not evidence that it does not exist. |
ai_discovery_unreadable | warning | A configuration file the AI asset detectors wanted to read could not be opened, decoded or parsed, so nothing about it could be established — including whether it declares a service, an agent or an MCP server at all. Recorded rather than skipped in silence, because an unreadable file and a file with nothing in it are different facts and only one of them supports the sentence 'no agent was declared here'. Permissions, invalid UTF-8, malformed JSON or YAML, and a race with a build all produce this. Nothing is guessed in its place. |
ai_discovery_unsupported_shape | warning | A file whose name says it declares AI assets was read successfully and its structure is not one these detectors understand — an agents.yaml whose entries carry none of the recognised agent keys, or an MCP server entry that is not an object. Nothing was extracted from it, and that is reported rather than passed over, because a file named for agents that yields none would otherwise read as a positive finding that the product has none. The file may be perfectly valid for a framework this build does not support. |
ai_engine_builder_version_not_declared | info | A compiled engine declares no builder or runtime version. A serialised engine is generally only loadable by the toolchain version that produced it, so the version is part of what the artifact is locked to; without it, whether a given runtime can still load this engine cannot be established from the release. |
ai_engine_sidecar_unreadable | warning | A builder sidecar beside a compiled engine could not be opened, decoded or parsed, so the target, builder version and source model it may have declared are not in this run. An unreadable declaration and an absent one are different facts, and only one of them means the builder said nothing. |
ai_engine_source_not_declared | warning | A compiled inference engine declares no source model, so nothing in the release says what it was compiled from. For a compiled artifact this is the more serious gap: an engine nobody can rebuild is an engine nobody can update when its target hardware or its runtime moves. |
ai_engine_source_not_in_release | info | A compiled engine names a source model that is not in this release. The relationship is still RECORDED, against a declared entity, so the lineage survives — never dropped and never assumed present. Establish where the source lives before the engine needs rebuilding. |
ai_engine_target_not_declared | warning | A compiled inference engine is present and nothing beside it declares what it was built for. The engine is no less locked to a GPU architecture and a runtime version for that — only less documented — and the target is deliberately NOT guessed from a filename or a directory name, which would manufacture a support-lifetime fact out of a string. For a product shipped for ten years, what an engine is locked to is the fact that decides when it stops working. |
ai_evaluation_declared_threshold_unreadable | warning | An evaluation record declares an acceptance criterion in a shape this build cannot read; a readable one needs at least the name of what is bounded and the bound itself. The declaration is counted and reported rather than dropped, because a threshold nobody can read is not a threshold nobody wrote, and a silent drop would leave the record looking as though it declared no criterion at all. SBOMFlow records declared thresholds and applies none of them: comparing figures to a criterion is a human's reading, recorded as a review decision. |
ai_evaluation_depth_budget_exceeded | warning | The search for evaluation records reached its directory-depth ceiling and pruned that branch, continuing elsewhere. Pruning rather than stopping is deliberate — one over-deep directory must not discard the inventory of every sibling — but a record below the pruned point is absent for that reason and the inventory is PARTIAL. |
ai_evaluation_duplicate_id | warning | Two evaluation records claim the same identifier. Both are kept and folded onto one entity so that anything they disagree about — the result, the target, the tool — becomes a visible conflict rather than one silently replacing the other. A reused identifier is usually a copied template nobody edited, and the second evaluation it appears to record may not exist. |
ai_evaluation_entry_budget_exhausted | warning | The search for evaluation records reached its ceiling on directory entries and STOPPED, so parts of the tree were never searched and a record in them is absent for that reason. The evaluation inventory is PARTIAL; treat it as a lower bound and re-run against a narrower root or a raised budget. |
ai_evaluation_invalid | warning | An evaluation record is missing something without which it cannot be attributed, re-checked or acted on — an identifier, a result from the supported vocabulary, the kind and digest of what was evaluated, or who evaluated it. The record is refused and named rather than partially recorded, because a half-record inside an evidence artifact reads as evidence. |
ai_evaluation_limitations_absent | info | An evaluation record states no limitations. Every evaluation has a boundary — a sample size, a language, a modality, a threat model it does not cover — so an empty limitations list is far more likely to mean nobody wrote them down than that there were none. The absence is surfaced so it is not read as completeness. |
ai_evaluation_not_interpreted | info | An evaluation result is recorded with no human interpretation of what it means for this product. The measurement and the verdict are the evaluator's; what follows from them for this release is a judgement nobody has made yet. Recorded so an uninterpreted result is not mistaken for an accepted one. |
ai_evaluation_not_reproducible_from_record | info | An evaluation record does not state one or more of the fields somebody would need to run the evaluation again — the harness and its version, the dataset revision and digest, the environment, the prompt digest, or the sampling parameters. INFO, and deliberately so: an absent field is a claim the record did not make, not an error and not a failed evaluation. It is reported because the result cannot be reproduced from its own record, which is a fact about the evidence that anyone quoting the result needs, and because an unrecorded absence reads as completeness. The fields concerned are named in the record's own entry, each with what a re-run needs it for. |
ai_evaluation_raw_result_digest_mismatch | error | The raw evaluation output found where a record said it would be does not hash to the digest that record declared. The summary and the evidence behind it disagree, so the record cannot be relied on: either the raw output was replaced or edited after the record was written, or the record describes a different run. SBOMFlow hashes the file and reads nothing else from it, and guesses nothing about which of the two happened. |
ai_evaluation_raw_result_missing | warning | An evaluation record points at raw output that is not present at that path, or that exists and could not be read, so the summarised result has nothing behind it anyone can inspect. The record is kept — its claim is still on the record — but it is unverifiable from this tree alone. A declared path resolving outside the release tree is reported here too, and deliberately never followed. |
ai_evaluation_record_budget_exhausted | warning | More evaluation records are present in this tree than one run folds into the artifact, so some were not read. Which ones were read is deterministic — sorted by path, never sampled — and the ceiling is reported alongside the result, so a partial inventory is known to be partial rather than appearing complete. |
ai_evaluation_reproduction_field_unreadable | warning | An evaluation record carries something at a field a re-run would need, and this build cannot read a value from it — a container where a value belongs, an entry written empty, or a digest that is not one. Kept strictly apart from the field being absent: one is a claim withheld and the other is a claim garbled, and only the second is a defect in the record somebody can go and fix. Neither is ever rendered as a zero or a default. |
ai_evaluation_schema_version_unsupported | warning | An evaluation record declares a schema version this build does not read, so it was refused rather than parsed under the wrong rules. This is deliberate: evidence interpreted by rules it was not written for still produces an authoritative-looking result, and a record read wrongly is worse than one not read at all, because nothing about it looks wrong. Upgrade SBOMFlow, or ask the evaluator to emit the supported version. |
ai_evaluation_stale | warning | An evaluation no longer describes what is shipping: the model, prompt, toolset or environment it bound itself to is not among this release's observations, or its own declared validity period has ended. Expiry here is DERIVED from what the release actually contains rather than asserted by hand, so an evaluation stops applying the moment its subject changes and nobody has to remember to withdraw it. |
ai_evaluation_unbound | warning | An evaluation record declares nothing it depends on, so no change to this release can ever contradict it — and therefore it can never be shown to still apply. It is reported as unbound rather than current: a result that cannot go stale is not a result that is fresh. Add the digests of the model, prompt, toolset and environment the evaluation ran against so later releases re-check it automatically. |
ai_evaluation_unreadable | warning | A file named as an evaluation record could not be opened, decoded or parsed, so nothing it may have recorded is in this run — including whether it describes a passing or a failing evaluation. Recorded rather than skipped in silence, because an unreadable record and an absent one are different facts and neither supports the sentence 'this was evaluated'. Permissions, invalid UTF-8, malformed JSON, a resource budget, and a race with a build all produce this. |
ai_evidence_declaration_after_instant | warning | An AI declaration's own instant is at or after this run's evidence instant, so it describes something that had not happened when the release was pinned. Usually a --as-of predating the capture, or a clock disagreement between the collector and the build. Recorded as it stands rather than clamped to zero: a negative age is a fact about the inputs worth seeing. |
ai_evidence_declaration_long_lived | info | An AI declaration was captured more than a year before this run's evidence instant. For a product supported for a decade that is ordinary rather than a fault: it is recorded so a reviewer can choose to look again, which is the only action an age can honestly prompt. It is not a claim that anything is out of date. |
ai_evidence_declaration_undated | info | An AI declaration carries no instant, so it cannot be placed in time at all. That is NOT an age of zero and NOT a recent declaration — it is a gap in the evidence rather than a property of what the declaration describes. Also raised when the run itself was not pinned to an instant, because ageing against the wall clock would make the artifact depend on when it happened to be generated. |
ai_evidence_upstream_unaccounted | info | A model names an upstream that this release does not account for. NOTHING was fetched to settle it: a lookup would make the artifact depend on the network's opinion on the day it ran, and a reference failing to resolve here is not evidence that it no longer exists. Recorded as a gap for a human to close. |
ai_lineage_merge_config_unreadable | info | A merge configuration could not be read, so any lineage it declared is not in this run. The file is not parsed by hand when its loader is unavailable — a hand-rolled parser over an untrusted document is a crash surface for no gain. This says nothing about whether the artifacts beside it were merged. |
ai_lineage_origin_contested | warning | Two declarations in this release each claim to name THE origin of one artifact and they name different things — for example an adapter's declared base and the upstream its provenance records. Both are kept and neither is preferred: choosing one would destroy the only evidence that they disagreed. A merge naming several parents is NOT contested, because a merge legitimately has many. |
ai_lineage_origin_unexplained | warning | Nothing in this release accounts for how a model artifact came to be: no declared upstream, no conversion metadata in its header, and no merge configuration naming it. A digest proves the bytes did not change after they were observed and says nothing about their history, so this is the gap that digest cannot close. It is NOT an accusation — an ordinary in-house model has exactly this shape — but a shipped model nobody can account for is what a reviewer chasing a supply-chain question most needs named. |
ai_lineage_safetensors_header_unreadable | info | A SafeTensors header could not be read as bounded JSON, or declared a length that does not fit inside the file. An over-declared header is REFUSED rather than clamped: a header claiming more bytes than exist is not a header, and reading it as far as the file allows would treat a malformed or hostile declaration as a partial truth. No conversion metadata is recorded from it, which is not evidence that none was declared. |
ai_manifest_model_without_expected_hash | info | A declared AI manifest names a model but supplies no expectedHash, so the declaration cannot be matched by identity to any observed file. The entry is kept as a declared-only entity rather than being guessed onto the nearest observed model, because inventing that link would assert an identity claim nobody made. The consequence is recorded plainly: this model is declared and unverified, which is not the same as verified. |
ai_manifest_unusable | warning | A declared AI manifest was supplied with --ai-manifest and could not be used, so nothing was declared to this run and every AI entity in the record stays observed-only. ai-evidence.json records which of four states applied: absent (a path was named and no file is there), unreadable (something is there and its bytes could not be read at all), malformed (the bytes were read and are not JSON within the resource budgets) or unknown_schema (the document parsed as JSON and the strict reader refused it, naming the offending JSON pointer). The four are told apart by the exception the bounded read raised, never by an existence check, because an operator sent to look for a 'missing' file that is sitting right there with the wrong permissions acts on a false answer. This code exists so a supplied declaration that silently did nothing cannot produce a run that looks exactly like a clean one. |
ai_mcp_server_card_credential_shaped_value_refused | warning | A value an MCP server card declared matched a known credential shape and was refused, so it appears in no artifact, log or message; only the fact of the refusal travels. A card is a document written outside this tool and copied into an evidence pack that is shared, so a URL carrying a token in its userinfo, path or query is refused whole rather than minimised into a clean-looking host. Header default and choices values are never read at all — only header names are. |
ai_mcp_server_card_declaration_differs | warning | An MCP server card and the MCP configuration paired with it do not agree about a transport or an endpoint — the card lists connection details the configuration in this tree does not use. Compared only where BOTH documents stated a value, so an absence never manufactures a difference. This is a difference between two documents. Nothing here establishes which is right, whether either describes a running server, or why they differ; that is a human determination. |
ai_mcp_server_card_declared_field_unreadable | warning | A field of an MCP server card was present and could not be recorded — it was not a string, was longer than any value this record carries, or a remote entry, protocol-version list or header list was not the shape the format defines. The value is never echoed. Kept distinct from a field the card simply did not state: an absent field is a claim its publisher did not make, and something present that this build could not read is a third state that must not round into either. |
ai_mcp_server_card_declared_not_observed | info | An MCP server card in the release tree declares a server that no MCP configuration in the same tree references. Ordinary and not a defect: a card may be vendored, published for consumers elsewhere, or kept beside a configuration this walk could not reach. Recorded because the alternative is silence, and never as a finding about the server, its publisher or the release. |
ai_mcp_server_card_local_name_ambiguous | warning | Two or more MCP server cards in the release tree claim the same identifier, so no pairing with an observed server could be made without choosing between them. Neither was paired and the reason is reported here, because a coin toss recorded as a fact is worse than an unpaired entry — the servers involved appear as observed-not-declared and declared-not-observed instead. |
ai_mcp_server_card_observed_not_declared | info | An MCP configuration in the release tree references a server that no server card in the same tree declares. Raised only on a run where at least one card WAS read, so the absence of an optional document can never produce one of these per server. Publishing a card is optional under SEP-2127, so this is not a statement that the server is undocumented, unapproved or unknown to anyone — it says this tree holds one document and not the other. |
ai_mcp_server_card_paired_on_local_name_only | warning | A server card declaring namespace/local was paired with an MCP configuration that keyed its server on local alone, so the pairing rests on the local component and a different publisher's server of the same local name would pair identically. The basis is recorded on the entry and warned about here rather than presented as an established identity, because everything compared under that pairing inherits its strength. |
ai_mcp_server_card_record_budget_exhausted | warning | More MCP server cards, or more remote connection entries within one card, were present than one release may record, so the record is PARTIAL and the remainder was never examined. Recorded rather than truncated in silence, because a shortened list that does not say it was shortened reads as a complete one. |
ai_mcp_server_card_scan_depth_budget_exceeded | warning | The MCP server-card walk reached the directory-depth budget and PRUNED that branch, continuing elsewhere, so the server-card record is PARTIAL. Pruning rather than stopping is deliberate: letting one over-deep directory discard every sibling's inventory would turn a depth bomb into a record-erasure primitive. |
ai_mcp_server_card_scan_entry_budget_exhausted | warning | The MCP server-card walk reached the total directory-entry budget and STOPPED, so the server-card record is PARTIAL. Entries beyond the budget were never examined, and a card missing from the record may simply be one the walk never reached. Raise the budget or scan a narrower root to examine the remainder. |
ai_mcp_server_card_structure_unrecognised | warning | A file at a documented MCP server-card location read successfully and is not a server card: it does not declare name, version and description as strings, or it carries an MCP client-configuration block, which the static discovery detectors read instead. Nothing was extracted from it. The file may be perfectly valid for a purpose these readers do not cover, so this is a statement about what could be recognised and never about the file's correctness. |
ai_mcp_server_card_unreadable | warning | A file sits at a documented MCP server-card location and could not be opened, decoded as UTF-8, parsed as JSON within the byte, entry and nesting budgets, or digested — so nothing it declares was recorded, including whether it is a server card at all. Reported rather than skipped in silence, because a card that is present and unreadable and a location nobody wrote a card to are different facts and only one of them supports the sentence 'this tree declares no server'. |
ai_model_card_unreadable | info | A model card beside a model could not be read, so any upstream it named is not in this run. Only the front matter is ever consulted and it is matched line-wise rather than parsed — a model card is arbitrary text from an untrusted tree, and running a real parser over one is a crash surface for no gain. |
ai_model_config_absent | info | No configuration file was found beside a model, so its architecture, width, depth, declared context length and layout are unknown from this release. Recorded rather than defaulted: a defaulted architecture is a fact about SBOMFlow, not about the product. |
ai_model_config_unreadable | warning | A model configuration file is present and could not be opened, decoded or parsed, so nothing it declared about the architecture is in this run. Kept apart from an absent configuration: a file nobody wrote and a file nobody could parse are different facts about a release, and neither is a reason to substitute a default. |
ai_model_context_length_not_declared | info | A model's configuration declares no context length, so what it was built for is unknown from this release. Note that a declared length is a number in a file either way: it records what the configuration states and never that the model behaves as stated at that length. |
ai_model_extension_signature_mismatch | warning | A file's extension claims one model format and its leading bytes prove another — for example a .onnx name over a pickle stream. BOTH facts are recorded and the BYTES decide the format that goes into the inventory, because a filename is chosen by whoever wrote it. This is an observation about a disagreement, never a finding that the file is malicious, tampered with, or unsafe: a build system renaming an artifact produces exactly the same signal. Nothing was loaded or parsed to reach it. |
ai_model_gguf_header_budget_exhausted | warning | A GGUF header declares more metadata pairs, a longer string, or a larger array than this build will read. The declared quantity is attacker-controlled and is read before it can be validated, so it is capped rather than trusted: a header claiming an enormous pair count stops immediately instead of being believed. The container's metadata is not recorded for this file. |
ai_model_gguf_header_malformed | warning | A GGUF header declares a value or array type this build does not know, or does not carry the documented magic at all. Parsing stops at that point rather than guessing a length and continuing into whatever follows — a header is attacker-controlled, and a reader that keeps going past a shape it does not understand is reading bytes it cannot account for. |
ai_model_gguf_header_truncated | warning | A GGUF header ended before the metadata it declared, so the container's own account of itself could not be read to the end. What was read before the cut is kept and the record is marked incomplete; nothing is assumed about the pairs that were never present. A partial read must never become a partial answer. |
ai_model_identified_by_extension_only | info | A file carries a known model extension but nothing in its bytes matched a documented magic number or structural invariant, so the recorded format rests on the filename alone — which an attacker, or an ordinary build script, controls. The artifact is kept in the inventory with identified_by: extension_only rather than dropped, because omitting it would make the inventory read cleaner than the tree it describes. Treat the format as a claim, not as an identification. |
ai_model_layout_not_declared | info | Nothing in a model's configuration separates a dense layout from a sparse Mixture-of-Experts one. The layout is never inferred from a file size, a filename or a parameter count — a sparse model and a dense one can occupy similar disk while having materially different deployments, and guessing between them would be an assertion about a deployment nobody observed. |
ai_model_licence_contested | warning | Artifacts beside one model declare licences of different kinds. Every declaration is recorded with the file that made it and none is discarded; the recorded class is the most restrictive of them, because a record reporting the laxest would understate what the release is bound by. Which licence actually governs is a question for whoever assembled the release. |
ai_model_licence_not_declared | warning | Nothing in this release states a licence for a model, so the release does not say what may be done with the weights. This is the most common real case and is recorded as a finding — it is NOT a statement that nothing restricts them, and an absent licence is not a permissive one. |
ai_model_licence_research_only | warning | A model's declared licence limits use to research or non-commercial purposes. Recorded separately from other restrictions because it constrains the deployment as a whole rather than a particular use of it. Whether a given deployment is commercial is a question about the deployment. |
ai_model_licence_unrecognised | warning | A model declares a licence this build does not recognise. The text is recorded exactly as written and deliberately NOT normalised to a similar-looking SPDX identifier: a model licence rounded to the nearest identifier has lost the acceptable-use policy, scale threshold or field-of-use limit that was the only thing constraining the customer. Read the licence text. |
ai_model_licence_use_restricted | warning | A model's declared licence CONTAINS a use restriction — an acceptable-use policy, a scale threshold above which permission changes, a field-of-use limit, a commercial-use limit, a competitive-use limit, or an attribution requirement. The existence of the clause is the fact recorded here, with a pointer to the document carrying it. Whether a given business falls inside or outside that clause is a legal question about that business and is not evaluated anywhere in this tool. |
ai_model_oversize_skipped | warning | A file exceeded the per-artifact size bound for the AI inventory pass and was not hashed or identified. The bound exists because a scan root is untrusted input and an unbounded read is a denial-of-service surface. The file is therefore missing from the inventory by policy, not by observation: this code is what stops that omission from reading as 'nothing was there'. |
ai_model_provenance_contested | warning | Artifacts in this release name DIFFERENT upstreams for one model. Neither is preferred and none is discarded: a cache path and a configuration disagreeing usually means the model was converted, re-saved or assembled from more than one source, and which is meant is a question for whoever built it. Every claim is recorded with the strength of its evidence and the file that made it. |
ai_model_provenance_not_discoverable | warning | Nothing in this release says where a model came from — no downloader cache layout, no name in a configuration, no base named in a model card. This is the FINDING, not an empty field: it is the most common real case, and it means the release cannot answer the first question a reviewer asks about a model. It is not a statement that the model is untrustworthy, only that its origin is unrecorded here. |
ai_model_quantisation_config_unreadable | warning | A quantisation configuration file beside a model could not be opened, decoded or parsed, so the producer's account of how the model was quantised is not in this run. Recorded rather than passed over, because an unreadable declaration and an absent one are different facts. |
ai_model_quantisation_file_type_unknown | info | A GGUF header declares a file-type number this build does not map to a documented scheme. The NUMBER is recorded exactly as declared rather than being rounded to a neighbouring scheme — a wrong scheme name is worse than an unrecognised number, because only one of the two looks like knowledge. |
ai_model_quantisation_unknown | info | No quantisation scheme was declared anywhere this build reads. This is NOT a statement that the model is unquantised, and it is deliberately not guessed from the filename: a name containing q4 or int8 is chosen by whoever produced the file and is not a fact about its bytes. An unquantised model and one whose metadata could not be read produce the same silence here, and neither is a scheme. |
ai_model_revision_floating | info | A model's upstream reference names a branch or tag rather than a commit. main is not a revision: it names wherever the upstream happened to be when this was fetched, so the same reference resolves to different bytes over time and the release cannot be reproduced from it. Recorded separately from a pinned commit, because both look identical in a field that just says 'revision'. |
ai_model_revision_not_declared | info | A model's upstream is named and no revision is, so the release names a repository and not a point in it. Recorded rather than defaulted: an absent revision is not the same as a floating one, and treating them alike would hide which releases can be reproduced. |
ai_model_scan_depth_budget_exceeded | warning | The AI inventory walk met the max_directory_depth budget and did not descend further, so at least one branch of the release tree is unexamined and the inventory in ai-evidence.json is PARTIAL. The rest of the tree was still walked — a single over-deep branch never discards the inventory of its siblings — but traversal.complete is false and the absence of a model artifact below that depth is evidence of the budget, not evidence that no model is there. This says nothing about whether the deep branch is malicious: deeply nested vendored dependencies and generated build trees reach it in ordinary projects. |
ai_model_scan_entry_budget_exhausted | warning | The AI inventory walk reached the max_entries budget for directory entries and STOPPED, so the release tree was not fully examined. The inventory in ai-evidence.json is therefore PARTIAL and traversal.complete is false: entries beyond the budget were never opened, and a model artifact missing from the record may simply be one the walk never reached. This is a statement about the budget, never about the product — it is neither a finding that the tree is hostile nor a claim that anything was hidden, and a build tree with a very large vendored dependency directory reaches it the same way a crafted one does. Raise the budget or scan a narrower root to examine the remainder. |
ai_model_shard_index_references_absent_shard | error | A shard index names a shard file that is not on disk. The model's own manifest says the checkpoint needs a file this tree does not have, which is the most direct evidence available that what is present is not a loadable model. Reported separately from a shard the index never mentions: an interrupted download and a leftover from another checkpoint are different failures. |
ai_model_shard_index_size_mismatch | warning | A shard index declares a total size that the member files do not add up to. The manifest and the bytes disagree, so at least one of them does not describe what shipped. SBOMFlow records both and re-derives neither. |
ai_model_shard_index_unreadable | warning | A shard index manifest is present and could not be opened, decoded, parsed or hashed, so the set's identity rests on its member digests alone. The index arrives with the model from wherever the model came from and is untrusted input like any other; it is read under the standard bounded reader. Recorded rather than treated as though no index existed — those leave the identity resting on different things. |
ai_model_shard_not_referenced_by_index | warning | A shard file is present that the set's index manifest never names. It may be a leftover from a different checkpoint, a partially-replaced set, or a file added by hand. It is kept in the inventory and reported rather than dropped, because a file nobody can account for beside a model is evidence in its own right. |
ai_model_shard_set_incomplete | error | A sharded checkpoint is missing shards it declares. The set is recorded with the members that are present and marked INCOMPLETE, naming the missing indices — it is never recorded as a smaller model, because a set of 29 files where 30 shipped is not a checkpoint anyone can load, and reporting it as one would read exactly like a complete model. An interrupted download, a partial sync, or a copy that dropped a file all produce this. |
ai_model_shard_total_contested | warning | Members of one shard set disagree about how many shards the set has, or the index and the filenames disagree. Usually two checkpoints sharing a filename stem in one directory. Neither total is preferred and the set is not reported complete, because picking one would silently attribute a member to a model it may not belong to. |
ai_model_symlink_skipped | info | A symbolic link was found while walking the release tree for model artifacts and was NOT followed. Following one would let a crafted tree pull an arbitrary host file into the evidence and hash it as if it shipped in the product. The link is therefore skipped and said so: if a real model is reachable only through a link, it is absent from this inventory and its absence is not evidence that it is not there. |
ai_model_unreadable | warning | A file in the release tree could not be stat-ed or opened during the AI inventory pass, so nothing about it could be established — including whether it is a model at all. Recorded rather than skipped in silence, because an unreadable file and an absent file are different facts and only one of them supports the sentence 'no model was found here'. Permissions, a race with a build, and a broken mount all produce this. |
ai_model_unsafe_format_not_parsed | warning | A model artifact was recognised as a format whose only faithful reader executes code (anything pickle-backed, an executable graph, or an archive that can carry one). SBOMFlow records its path, size and SHA-256 and deliberately does NOT open it, so no field about this file describes its contents, architecture or behaviour. This is not a statement that the file is dangerous — it is a statement that the evidence about it stops at the bytes, and that the remaining facts must come from a human or a declaration. |
ai_multimodal_component_artifact_absent | info | A configuration declares a composed multimodal system and no observed model artifact was found beside it, so the system is recorded with its declared roles and no linked component. The declaration is still evidence — somebody wrote down what the system is built from — and no component entity is invented to satisfy the relationship. |
ai_multimodal_component_licences_differ | warning | The components of one composed multimodal system do not all carry the same licence — a vision tower under a research-only licence inside a system whose language weights are permissive is the ordinary case, not an exotic one. Each component keeps its own licence and none is promoted to govern the system: which terms apply to the product as shipped is a legal question about a real product that SBOMFlow does not answer. A component with no observed licence does not make the set disagree. |
ai_multimodal_component_role_not_declared | info | A component of a composed multimodal system carries no declared role, so what it is for is unstated. The role is left unstated rather than guessed from the filename: a name like mmproj-f16.gguf was chosen by whoever produced the file and is not a fact about its bytes. The component is still recorded as part of the system, because a configuration named it. |
ai_multimodal_config_unreadable | info | A model configuration could not be read as bounded JSON, so any composed system it declared is not in this run. This says nothing about whether the release ships a multimodal system: a composition declared in a document this build could not read is invisible here and is not thereby absent. |
ai_prompt_credential_shaped_value_refused | warning | A declared prompt identity or provenance value matched the shape of a credential and was refused on sight, matched on the value rather than on the field name because the field name is chosen by whoever wrote the document. The matched text is never echoed — not into the record, not into this warning, not into a log — and the field is recorded as unreadable with the reason, so the refusal is visible without the secret being republished. |
ai_prompt_declared_field_unreadable | info | A prompt asset or a manifest entry declares an identity or provenance field and this build could not record it as a value — it is a block, mapping, sequence or empty; it is declared more than once so which value was meant is not determinable; it is longer than an identity field may be here and is refused rather than truncated; or it matched the shape of a credential. This is the THIRD state and is never folded into 'not stated': somebody declared something here, and reporting that as an absence would erase their statement. |
ai_prompt_declared_not_present | warning | A prompt manifest names a path inside the release tree and no readable file is there, so the declaration stands unsupported and no digest was recorded for it. This is a fact about this release's evidence, not a finding about the supplier and not a statement that the prompt does not exist somewhere else. |
ai_prompt_declared_path_refused | warning | A prompt manifest declares a path that is absolute, traverses out of the release tree, is a symbolic link, or is longer than any path this reader will resolve. It was REFUSED before anything was opened. Kept distinct from a path that is simply missing: both leave the record without bytes and they send an operator to opposite repairs, and following one would let a supplied document make this engine read and hash an arbitrary host file into a pack that travels. |
ai_prompt_digest_differs | warning | A prompt manifest declared one digest for a prompt and the file at the path it names hashes to another. Both values are recorded and neither is preferred. This is an observation about a disagreement, never a finding that anything was tampered with: a rebuild, a line-ending change and a stale manifest all produce it, and which side is right is a human disposition. |
ai_prompt_held_outside_release_tree | info | A supplier declared a prompt that is held OUTSIDE the build tree — a database, a remote configuration service, a vendor prompt console. Nothing here observed it and nothing was contacted to look, so its declared identity is the whole of the evidence about it. This code exists so the case is visible rather than silently missing: it is the commonest place a production prompt actually lives, and no tree walk can ever see one. |
ai_prompt_identified_by_naming_convention_only | info | A file's name declares a documented prompt-asset format and its bytes did not satisfy that format's structural invariant, so the recorded format rests on the filename alone — which whoever wrote the tree controls. Kept in the record rather than dropped, because omitting it would make the record read cleaner than the tree it describes. Treat the format as a claim, not as an identification, and note that nothing was inferred from what the file's text says. |
ai_prompt_manifest_unreadable | warning | A supplied prompt manifest could not be read as bounded JSON, was not an object, or declared no prompt list, so nothing it claimed was recorded. The block reports this as its own state rather than as an empty result: prompt evidence that was present and unreadable produces exactly the same empty lists as a tree that held none, and the two send a reader to opposite conclusions. |
ai_prompt_record_budget_exhausted | warning | More prompt assets, manifests or manifest entries were present than one release may record, so the record is PARTIAL and the remainder was never examined. Recorded rather than truncated in silence, because a shortened list that does not say it was shortened reads as a complete one. |
ai_prompt_scan_depth_budget_exceeded | warning | The prompt walk reached the directory-depth budget and PRUNED that branch, continuing elsewhere, so the prompt record is PARTIAL. Pruning rather than stopping is deliberate: letting one over-deep directory discard every sibling's inventory would turn a depth bomb into a record-erasure primitive. |
ai_prompt_scan_entry_budget_exhausted | warning | The prompt walk reached the total directory-entry budget and STOPPED, so the prompt record is PARTIAL. Entries beyond the budget were never examined, and their absence is evidence of the budget rather than evidence that they are absent from the release. Raise the budget and rescan to see the rest. |
ai_prompt_unreadable | warning | A file named as a prompt asset could not be stat-ed, opened or read, or an entry in the walk could not be examined at all, so nothing about it could be established. Recorded rather than skipped in silence: an unreadable file and an absent file are different facts and only one of them supports the sentence 'no prompt was found here'. A file above the per-artifact size bound lands here too, with its reason recorded — that omission is policy, not observation. |
ai_provider_declaration_capture_undated | info | A captured provider document carries no capture instant, so it cannot be placed in time. When a declaration was captured is half of what makes it evidence: a model card captured today and one captured three years ago support very different statements, and neither the document nor its digest records which this is. |
ai_provider_declaration_document_missing | warning | A provider-declaration record names a captured document at a path that does not resolve to a readable file inside the release tree, so the claim that it was captured stands unsupported and no digest was recorded for it. A path escaping the release tree is treated exactly as an absent one and is never followed. This is a fact about THIS RELEASE's evidence, not about whether the provider published such a document. |
ai_provider_declaration_unreadable | warning | An operator-supplied provider-declaration record could not be read as bounded JSON, or was not an object, so nothing it claimed was recorded. The file is still named in the block's problems, because a declaration somebody captured and this build could not read is part of the evidence picture even when nothing could be made of it. This is a statement about the record, never about the provider or the model it names. |
ai_provider_declaration_without_subject | warning | A provider-declaration record does not name the model it is about, so it cannot be attached to anything in the release and contributes no drift digest. The record is still listed in full — a declaration nobody can attach is evidence that somebody captured something, and inventing a subject for it would fabricate the very link this evidence exists to establish honestly. |
ai_runtime_collector_unattributed | warning | An operational observation carries no usable collector identity, so the runtime facts in it cannot be traced to the tool that produced them, questioned, or re-collected. Runtime evidence is only as good as the thing that gathered it, and an anonymous report cannot be assessed at all. |
ai_runtime_coverage_unstated | info | An operational observation does not say whether its collector enumerated the environment fully. Kept apart from a collector that reported being incomplete: one declined to claim completeness and the other reported the opposite, and neither is a complete enumeration. Treat the environment's inventory as a lower bound. |
ai_runtime_declared_not_observed | info | The build declared something and no reporting environment was observed running it. Consistent with a rollout in progress, a decommissioned component, or a collector that cannot see that kind of thing — and consistent with an environment that never reported at all. It is never a statement that the component is absent from the fleet. |
ai_runtime_evidence_invalid | warning | An operational observation is missing something without which it cannot be reconciled: the environment it describes, the collector that produced it, or the instant it was taken. An observation of nowhere describes nothing; an unattributed one cannot be questioned or re-collected; and an unaged one is exactly how a single old snapshot starts reading as continuous monitoring. |
ai_runtime_evidence_schema_version_unsupported | warning | An operational-evidence document declares a schema version this build does not read, so it was refused rather than parsed under the wrong rules. Runtime evidence interpreted by rules it was not written for still produces an authoritative-looking reconciliation, and nothing about it looks wrong. |
ai_runtime_evidence_stale | warning | An operational observation is older than the freshness horizon, so what it describes may no longer be what is running. The observation is kept — a stale report is still the only report there is — but it is reported as stale so it cannot read as current. Point-in-time evidence never becomes monitoring by being re-read. |
ai_runtime_evidence_unreadable | warning | A file named as operational evidence could not be opened, decoded or parsed, so nothing it recorded about what is deployed is in this run. Recorded rather than skipped in silence: an unreadable report and an environment that reported nothing are different facts, and neither supports the sentence 'the fleet matches the build'. |
ai_runtime_observed_not_declared | warning | An environment was observed running something this release never declared. This is a fact, not a verdict: it may be a forgotten test deployment, a manual patch, or a genuine supply-chain problem. SBOMFlow reports what the collector saw and what the build declared, and reaches no conclusion about how the difference arose. |
ai_serving_config_unreadable | info | A serving configuration could not be read as text, so any model it named and any prompt it carried are not in this run. This says nothing about whether the release serves a model. |
ai_serving_model_not_declared | info | A serving configuration declares no model. It may be a fragment, a template, or a file whose directives this build does not cover — recorded as an absence in that file rather than as a finding about the release. |
ai_serving_model_not_in_release | warning | A serving configuration in this release names a model that is NOT in the release. The release therefore ships a serving path whose weights arrive from somewhere else at run time, and nothing in this evidence pack describes what will actually be loaded. Nothing was contacted to find out what the name refers to: a registry reference is recorded as the name it is, because resolving it would make the record depend on what a registry says today. |
ai_serving_system_prompt_declared | info | A serving configuration carries a system prompt. It is recorded by DIGEST only and its body is never retained, so a changed prompt is visible in drift without the text existing in any field — the same treatment the chat template gets. A system prompt changes how a model behaves without changing a weight, and the evidence question is whether it changed, not what it says. |
ai_tokenizer_artifacts_absent | info | No recognised tokenizer file was found beside a model. Tokenizer layouts differ by model family and some ship none at all, so this is an absence of recognised FILES rather than a fault — but a release whose tokenizer changed cannot be detected for this model. |
ai_tokenizer_config_unreadable | warning | A tokenizer configuration is present and could not be opened, decoded or parsed, so the chat template digest, special tokens and declared vocabulary size are not in this run. A swapped template between releases would be invisible for this model as a result, which is the change this evidence exists to surface. |
ai_tokenizer_special_tokens_not_declared | info | A tokenizer declares no special tokens this build records. Recorded because a release whose end-of-sequence or padding token changed has changed behaviour that nothing else in the evidence would show. |
Analysis ingest#
| Code | Default severity | Meaning |
|---|---|---|
sarif_ingest_malformed | warning | A SARIF file given for ingestion was missing/unreadable or not a SARIF log; it was skipped. |
sarif_ingest_unsupported_version | info | An ingested SARIF log declared a version other than 2.1.0; it was parsed conservatively. |
Analyzer adapter#
| Code | Default severity | Meaning |
|---|---|---|
analyzer_executable_hash_mismatch | warning | A pinned external analyzer's executable SHA-256 did not match the expected hash; it was NOT executed (PATH substitution / swapped or tampered binary / version drift). |
analyzer_execution_failed | warning | An external analyzer adapter could not be executed (missing executable or run error); it was skipped. |
analyzer_invalid_output | warning | An external analyzer adapter emitted output that was not a valid protocol observation; the line was skipped. |
analyzer_nonzero_exit | warning | An external analyzer adapter exited with a non-zero status; any valid observations were still ingested. |
analyzer_output_truncated | warning | An external analyzer adapter exceeded the captured-output byte cap and its output was truncated. |
analyzer_timeout | warning | An external analyzer adapter exceeded its wall-clock timeout and was terminated. |
cosign_missing_trust_material | warning | cosign verification was requested without a local key and offline bundle; skipped. |
cosign_not_available | info | The optional cosign tool is not installed; Sigstore verification was skipped (baseline never requires it). |
diffoscope_not_available | info | The optional diffoscope tool is not installed; the comparison was skipped (baseline never requires it). |
sigstore_bundle_unparsed | warning | A supplied Sigstore/cosign bundle could not be read as bounded JSON, or its shape matches neither the Sigstore bundle format nor the legacy cosign bundle. Its structure is not interpreted and no bundle metadata is recorded; the file is still fingerprinted for provenance. SBOMFlow never verifies a bundle itself — parsing records observed structure only. |
syft_not_available | info | The optional Syft tool is not installed; component discovery via Syft was skipped (baseline never requires it). |
west_import_nested_not_followed | warning | A project inside an IMPORTED west manifest itself declares import:, and west import resolution stops at that depth — so that project's own manifest was not read and every project it declares is absent from this SBOM. The intermediate project IS recorded, so the dependency tree looks resolved: a manifest cut at depth 1 produces the same component list as one that imports nothing, and this code is the only thing that tells the two apart. Manifest-of-manifests topologies (an application manifest importing a vendor SDK manifest that itself imports the Zephyr manifest) are the normal shape in this ecosystem, and there the resolver returns the vendor layer and drops the module set below it. Resolve the nested manifest into the mirror, or flatten the import, if those projects must appear. |
Artifact migration#
| Code | Default severity | Meaning |
|---|---|---|
unreadable_or_corrupt_json | warning | A JSON file in a directory named for upgrade plan or upgrade migrate-copy could not be read or parsed, so it contributed nothing to the plan and was not migrated. It is reported by name rather than passed over silently: an artifact that could not be read is not an artifact that needed no migration, and a run that reports no changes has not thereby reported that every file is current. Causes include truncation, hand editing, a path that could not be read, and nesting beyond the shared depth budget. The source directory is never rewritten in place, and migrate-copy still copies the file to the destination unchanged, so the original bytes survive for a human to inspect. |
Config#
| Code | Default severity | Meaning |
|---|---|---|
duplicate_config_key | warning | A key appears more than once in the same mapping of sbomflow.yaml. YAML and JSON both keep the LAST occurrence, so the earlier mapping is discarded WHOLE and every setting inside it silently reverts to its default — a second 'scan:' block does not merge with the first. The direction is not uniformly safe: 'offline' reverts to its secure default, but a tightened 'evidence_valid_days' reverts to the laxer 90 days. Detected during parsing because the repeat is already gone by the time the parsed mapping exists, and reported identically by the bundled loader and PyYAML so the two cannot disagree. It is a warning and never an error, and the value the engine applied is UNCHANGED: re-interpreting existing configs would be a worse defect than the ambiguity being reported. The human decides which reading was meant: merge the repeated blocks, and check what the discarded one configured. |
unknown_config_key | warning | An sbomflow.yaml key was not recognised and was therefore IGNORED, so anything it configures was not applied — a misspelled input key means the file it names is never read, and the run reports the resulting gaps as if nothing had been supplied. Emitted by analyze using the same check 'sbomflow validate-config' performs, so the two always agree. It is a warning and never an error: a config written for a newer SBOMFlow must still run. |
unknown_env_override | warning | An SBOMFLOW_* environment variable matched no config key and was IGNORED, so the setting it was meant to apply did not take effect. Distinct from unknown_config_key so a reviewer can tell a per-run environment typo from a committed sbomflow.yaml one — the follow-up differs. The run still succeeds and override precedence (file < env < flags) is unchanged. |
Container#
| Code | Default severity | Meaning |
|---|---|---|
base_image_unpinned | info | A Dockerfile base image is referenced by a mutable tag (or 'latest') with no @sha256: digest, so the image contents can change between builds. Recorded as observed context for review; SBOMFlow never resolves or pulls the image. |
compose_image_unresolved | info | A Compose file service image (services.*.image) uses an unexpanded ${...} interpolation. The image is unresolved and is not recorded as a component (never guessed); it is surfaced so the reference is not silently dropped. |
dockerfile_from_unresolved | info | A Dockerfile FROM instruction uses an ARG/${...} template for the base image. The image is unresolved and is not recorded as a component (never guessed); it is surfaced so the reference is not silently dropped. |
malformed_container_image | warning | A local container image archive could not be parsed as supported Docker or OCI image metadata. |
malformed_container_package_db | warning | A local container/rootfs package database could not be parsed. |
missing_container_os_release | warning | Container/rootfs package metadata lacked os-release vendor context, so generic purls were emitted. |
unreadable_container_image | warning | A local container image archive could not be read. |
unreadable_container_os_release | warning | A container/rootfs os-release file could not be read. |
unreadable_container_package_db | warning | A container/rootfs package database could not be read. |
unrecognized_container_package_db | info | A container/rootfs package database shape was not recognized by supported parsers. |
unsupported_container_image | warning | A local archive looked like a container image but did not match a supported Docker or OCI image shape. |
unsupported_container_package_db | info | A container/rootfs package database shape was recognized but is not supported. |
Cra#
| Code | Default severity | Meaning |
|---|---|---|
malformed_annex_ii_inputs | warning | The CRA Annex II operator-input file could not be parsed, so none of its supplied answers were applied. Nothing is inferred in their place: affected items stay marked as requiring manufacturer input rather than being auto-filled or silently dropped. |
missing_annex_ii_input_file | warning | A CRA Annex II input references a file that does not exist at the recorded path, so its content could not be read or hashed. The reference is preserved verbatim for human review and the item is not treated as evidenced. |
missing_annex_ii_inputs | info | No CRA Annex II operator-input file was supplied, so every item that needs manufacturer-supplied content remains marked as requiring it. Absence of input is recorded as absence, never as a satisfied requirement. |
unknown_annex_ii_section | warning | A CRA Annex II operator-input file names a section identifier that is not in the model. The entry is reported rather than silently ignored, so a typo or a stale identifier cannot quietly contribute nothing while appearing to have been supplied. |
Custom manifest#
| Code | Default severity | Meaning |
|---|---|---|
custom_manifest_dangling_dependency | info | A dependency declared in an imported SBOMFlow custom manifest named a package URL that is not a declared component in the same manifest (or is not a valid purl). The dependency edge was dropped; SBOMFlow never invents a dependency edge to a component it was not given. |
custom_manifest_invalid_component | warning | A component in an imported SBOMFlow custom manifest was structurally invalid (a missing or empty name/version/ecosystem/purl, a non-object entry, or a purl that does not parse). The component was skipped; SBOMFlow never infers a component identity from prose or guesses a missing field. |
custom_manifest_invalid_hash | warning | A component hash in an imported SBOMFlow custom manifest was not a valid hex digest for a recognized algorithm (md5/sha1/sha224/sha256/sha384/sha512). The single hash was dropped and the component imported without it; declared hashes are recorded as provenance, never verified against fetched bytes. |
Decision migration#
| Code | Default severity | Meaning |
|---|---|---|
legacy_decision_ambiguous | warning | A legacy advisory-keyed reviewer decision or waiver matched more than one finding instance and was not applied; re-review per finding_key (finding-identity v2). |
legacy_decision_no_match | warning | A legacy reviewer decision or waiver matched no current finding (stale) and was not applied. |
Due-diligence#
| Code | Default severity | Meaning |
|---|---|---|
malformed_due_diligence_record | warning | An Article 13(6) due-diligence record (or the records file) was malformed: bad id/date/reviewer/outcome/lists, a duplicate id, an unparseable file, or an over-budget file. The affected record (or file) is skipped and surfaced - never silently accepted. |
missing_due_diligence_inputs | warning | The due-diligence records file referenced by config (due_diligence) was not found. |
Embedded build#
| Code | Default severity | Meaning |
|---|---|---|
build_evidence_not_parsed | info | A recognized build-evidence output was retained and hashed but is not interpreted by the default scan; use its dedicated importer where available or review it manually. |
buildroot_license_files_without_license | info | A Buildroot package declared LICENSE FILES but no LICENSE value; surfaced for reviewer attention. |
cmake_declared_value_unresolved | info | A CMake FetchContent_Declare/ExternalProject_Add argument uses an unexpanded CMake variable (${...}). The declared source is recorded as declared-but-unresolved for review; SBOMFlow never runs CMake or guesses the resolved value. |
cmake_declares_unparsed | warning | A CMake FetchContent_Declare/ExternalProject_Add call had an unbalanced, oversized, or nameless argument list and was skipped rather than guessed. The file is still hashed for provenance. |
cmake_malformed_reply | warning | A CMake File API reply file was missing or not valid JSON; it was skipped. |
cmake_path_traversal | warning | A CMake File API reply referenced an unsafe (absolute or traversing) path; it was refused. |
cmake_unsupported_object_version | warning | A CMake File API object used an unsupported version; it was not trusted/parsed. |
esp_idf_malformed | warning | An ESP-IDF build artifact was unreadable or malformed; it was skipped. |
esp_idf_project_not_found | warning | No ESP-IDF outputs (project_description.json / sdkconfig) were found under the given path. |
linker_map_malformed | warning | A linker map file was missing or unreadable; it was skipped. |
linker_map_truncated | warning | A linker map exceeded the size/line limits and was truncated before parsing. |
linker_map_unrecognized_format | warning | A linker map file matched no supported (GNU ld / LLVM lld) shape; it was not parsed. |
twister_results_malformed | warning | A Zephyr Twister twister.json was unreadable or not a recognized Twister result; it was skipped. |
twister_results_not_found | warning | No Zephyr Twister results (twister.json) were found under the given path. |
zephyr_spdx_malformed | warning | A Zephyr west SPDX document was missing/unreadable or not an SPDX document; it was skipped. |
zephyr_spdx_not_found | warning | No Zephyr west SPDX set was found under the given build directory. |
Enrichment#
| Code | Default severity | Meaning |
|---|---|---|
missing_epss_snapshot | warning | A configured FIRST EPSS snapshot was missing. |
missing_kev_snapshot | warning | A configured CISA KEV snapshot was missing. |
missing_nvd_snapshot | warning | A configured NVD snapshot was missing. |
Evidence#
| Code | Default severity | Meaning |
|---|---|---|
evidence_reviewed_at_in_future | warning | A reviewer ACCEPTED an evidence item with a reviewed_at that reads as a valid ISO-8601 timestamp but names an instant LATER than the run's own (its --as-of, or the collection time when none is pinned). A run cannot have observed evidence that a human accepted after it, so the value is a detectable input error - typically a mistyped year. It is NOT treated as a fresh acceptance: freshness is anchored on the observation time instead, exactly as an empty or unreadable value already is, so the item ages normally and an enforced fail_on_stale_evidence gate can still block. Left unchecked such an item could never go stale at all, because no later run reaches the anchor. The comparison is against the run's PINNED instant and never the wall clock, so a pinned run stays byte-identical. Correct the acceptance date; an acceptance recorded exactly AT the run instant is honoured and is never reported here. |
evidence_reviewed_at_unparseable | warning | A reviewer ACCEPTED an evidence item with a reviewed_at value that is not an ISO-8601 timestamp, so the acceptance cannot be placed in time. It is NOT treated as a fresh acceptance: freshness is anchored on the observation time instead, so the item can still go stale and an enforced fail_on_stale_evidence gate can still block. Before this check the unreadable value fell through to the wall clock, which produced a valid_until months after the run's own pinned instant, reported is_stale: false, and made a fully pinned run non-deterministic. An EMPTY reviewed_at was always safe and is not affected. Re-record the acceptance with an ISO date. |
redaction_field_unassessable | warning | A field in an internal evidence record was WITHHELD from a redacted export because no redaction rule in this build could evaluate it - either no rule covers the field at all, or its value is a type the rule was not written for. Withheld is the deliberate answer: a field a redactor does not understand must never travel on the assumption that it is harmless, because a redaction that fails silently leaves an operator believing an export is safe when it is not. The consequence is recorded rather than absorbed: the export is incomplete by exactly the number of fields reported here, and a recipient must not read the omission as an absence of the thing. Usually means a later card added a section the export's rule table has not caught up with. |
External sbom#
| Code | Default severity | Meaning |
|---|---|---|
cyclonedx_fields_not_interpreted | info | A CycloneDX SBOM carried fields SBOMFlow does not model; they were named rather than dropped, because an import is not a lossless conversion. |
external_sbom_component_missing_purl | warning | A supplier SBOM component did not include a package URL and could not be matched as a normalized component. |
external_sbom_dangling_dependency | warning | A supplier SBOM dependency referenced a component that was not present in the imported component set. |
invalid_external_sbom_purl | warning | A supplier SBOM component contained an invalid package URL. |
malformed_cyclonedx_spec_version | warning | A document claimed bomFormat CycloneDX but its specVersion was missing, non-string, or not a MAJOR.MINOR version. |
malformed_external_sbom | warning | A supplier SBOM file could not be parsed as JSON. |
malformed_external_sbom_component | warning | A supplier SBOM component entry did not match a supported component shape. |
malformed_external_sbom_dependencies | warning | A supplier SBOM dependency section did not match a supported dependency shape. |
missing_external_sbom | warning | A configured external supplier SBOM file was missing. |
newer_spdx_minor_version | info | An SPDX document declared a minor version newer than this build models. SPDX defines a minor increment as backwards compatible, so it was imported with the older rules; anything the newer minor added was not interpreted. |
unreadable_external_sbom | warning | A supplier SBOM file could not be read. |
unrecognized_external_sbom_format | warning | A supplier SBOM was neither supported CycloneDX JSON nor supported SPDX JSON. |
unsupported_cyclonedx_spec_version | warning | A CycloneDX SBOM declared a spec version newer than this build models; it was not interpreted as the newest known version, because a later spec may redefine a field. |
unsupported_spdx_spec_version | warning | An SPDX document declared a MAJOR version newer than this build models; SPDX defines a major increment as carrying incompatible changes, so it was not interpreted. |
Firmware evidence#
| Code | Default severity | Meaning |
|---|---|---|
elf_core_file_skipped | info | An ET_CORE dump was skipped: a core file is a memory snapshot of a crashed process, not shipped product composition. It is reported rather than silently ignored. |
elf_malformed | warning | An ELF binary's structure was internally inconsistent (bad header entry sizes, an unlocatable dynamic string table, or a DT_NEEDED name pointing outside .dynstr). Parsing stopped safely at the bad region and the binary is recorded with parse_status 'partial'; names that could not be resolved are dropped, never guessed. |
elf_truncated | warning | An ELF binary's declared tables or sizes extend beyond the bytes present (or the file exceeds the read cap); parsing was bounded to the available bytes and the binary is recorded with parse_status 'partial' or 'unreadable'. |
elf_unsupported_binary_format | info | A recognized non-ELF executable (PE/COFF or Mach-O) was found while inventorying ELF binaries. SBOMFlow parses ELF structure only, so the file's structure was not read; it is surfaced rather than silently dropped. |
firmware_extraction_failed | warning | An optional firmware extraction tool failed; partial regular-file observations may remain. |
firmware_extraction_unsafe_output | warning | Firmware extraction produced a symlink, device, deep path, or output outside the designated extraction root; it was rejected. |
firmware_extractor_not_available | info | The selected optional binwalk/unblob executable was unavailable; baseline analysis continued. |
mcuboot_bad_magic | warning | A file given to the MCUboot parser did not start with the MCUboot image magic. |
mcuboot_malformed_image | warning | An MCUboot image was unreadable or its TLV structure was out of bounds; parsing stopped safely. |
mcuboot_truncated | warning | An MCUboot image's declared sizes extend beyond the file; parsing was bounded to the available bytes. |
Ingest#
| Code | Default severity | Meaning |
|---|---|---|
duplicate_json_key | warning | A trust-bearing JSON document repeats a key inside one object. This is valid JSON, so nothing is malformed, but json parsers keep only the LAST occurrence: a human reading the file and the engine evaluating it can disagree about what the document says (a VEX review carrying status twice shows one value to the reviewer and applies the other). SBOMFlow reports the file, the JSON path and the repeated key, and changes nothing: last-wins parsing is preserved byte-identically, and a duplicate never moves a VEX status, suppresses a finding, or alters a gate outcome. The human decides which reading is correct. |
License#
| Code | Default severity | Meaning |
|---|---|---|
invalid_license_expression | warning | A declared component license string is not a well-formed SPDX license expression (SPDX v2.3 Annex D grammar). The raw string is preserved verbatim in artifacts and never interpreted, guessed at, or dropped; this is a syntax observation, never a licensing or legal conclusion. |
license_conclusion_invalid | warning | A reviewer-supplied license conclusion (license_conclusions.json) is not a well-formed SPDX license expression, so it is not emitted as licenseConcluded. The engine never concludes a license; only a valid reviewer conclusion populates licenseConcluded. |
license_conclusion_unknown_component | info | A reviewer-supplied license conclusion references a package URL that does not match any observed component in this run. The conclusion is recorded but applies to nothing; surfaced so a stale or mistyped purl is not silently ignored. |
license_multiple_declarations | info | A component declares more than one distinct license string whose relationship (AND vs OR) is not stated by the source. SBOMFlow never guesses the operator: SBOM exports emit NOASSERTION for licenseDeclared while every declared string is preserved verbatim in a property/comment for human review. |
unknown_license_id | info | A declared license expression is syntactically valid but uses a license/exception identifier that is not in SBOMFlow's pinned SPDX License List snapshot. Unknown identifiers are not errors (the SPDX list evolves): the id is recorded as unknown for human review and is never mapped onto a 'closest' known id. |
Manufacturer evidence#
| Code | Default severity | Meaning |
|---|---|---|
ambiguous_annex_i_evidence_content | warning | Supplied Annex I evidence was present but deterministic content extraction found no useful routing fields. |
annex_i_evidence_type_requirement_mismatch | warning | An Annex I evidence input was routed to a requirement outside the supported evidence-type mapping. |
incomplete_annex_i_structured_evidence | warning | Structured Annex I metadata was present but omitted one or more expected fields for that evidence type. |
malformed_annex_i_evidence_inputs | warning | Annex I evidence input JSON did not match the supported local schema. |
malformed_annex_i_evidence_type | warning | An Annex I evidence input used an unsupported evidence type shape. |
malformed_annex_i_requirement_input | warning | An Annex I requirement entry did not match the supported input schema. |
malformed_annex_i_supplied_files | warning | An Annex I supplied-files entry was not a supported list of local file paths. |
missing_annex_i_evidence_input_file | warning | A configured Annex I manufacturer evidence file was missing. |
missing_annex_i_evidence_inputs | warning | A configured Annex I evidence input file was missing. |
oversize_annex_i_evidence_input_file | warning | A supplied Annex I evidence file exceeded the deterministic content-inspection size limit. |
oversized_annex_i_evidence_value | warning | An Annex I evidence string value exceeded the length cap and was truncated in the recorded metadata. |
unknown_annex_i_evidence_key | warning | An Annex I evidence input entry had an unrecognized key; it was surfaced and not recorded, never silently absorbed. |
unknown_annex_i_evidence_type | warning | An Annex I evidence input used an unknown evidence type. |
unknown_annex_i_requirement | warning | An Annex I evidence input referenced an unknown requirement id. |
unreadable_annex_i_evidence_input_file | warning | A supplied Annex I evidence file could not be read. |
Observation coverage#
| Code | Default severity | Meaning |
|---|---|---|
coverage_indeterminate | warning | Observation coverage could not be computed: either the scan root itself could not be read, or file discovery stopped at the structural entry budget before the whole tree was enumerated. Either way the run's component count says nothing about the product: coverage is unknown, not empty. Distinguishing 'we looked and found nothing' from 'we could not look' is the whole point of this code. |
possible_missed_composition | warning | Zero components were parsed even though composition signals were observed in the tree (build descriptors, source files, binaries, recognized-but-unsupported inputs, unknown candidates, or parse failures). Emitted by a deterministic decision table, never by a bare 'no components' rule, so it stays silent on a genuinely empty tree, a docs-only tree, and a deliberately component-free target whose manifest SBOMFlow did parse. The machine-readable cause is recorded in observation-coverage.json. This is an observation that the inventory may be incomplete — it is never a claim that components definitely exist, and it never changes an exit code by itself. |
unknown_structured_input_candidate | warning | No parser recognised this file, but at least two independent signals suggest it may carry composition evidence (an unparsed manifest, lockfile, build descriptor, on-device package database, or firmware blob). SBOMFlow has NOT created a component from it, emits no purl, and never guesses an ecosystem from a filename: this is a candidate for human review, not a finding. Confidence is a fixed label, never a probability, and a single weak signal never raises this code — low-signal rows live in unknown-input-candidates.json without warning. The number of these warnings is capped however many candidates a tree contains; the complete bounded list is in unknown-input-candidates.json. Informational: it never changes an exit code by itself. |
Oci evidence#
| Code | Default severity | Meaning |
|---|---|---|
oci_bad_digest_reference | warning | An OCI descriptor used a non-sha256/invalid digest reference; refused (path-traversal safe). |
oci_blob_digest_mismatch | warning | An OCI blob's content did not match its claimed digest. |
oci_layout_malformed | warning | An OCI layout index/manifest was unreadable or malformed; skipped. |
oci_layout_not_found | warning | The given path is not an OCI image layout (no oci-layout/index.json); skipped. |
Owners#
| Code | Default severity | Meaning |
|---|---|---|
malformed_owners_file | warning | The component-ownership rules file referenced by config (owners) was missing, unparseable, or contained a malformed rule (missing match/owner, or a duplicate pattern an earlier rule already shadows). The affected rule (or file) is skipped and surfaced - never silently accepted. |
owners_label_looks_personal | warning | An owner label in the ownership rules file looks like a person (an @ or a first.last shape) rather than a team. The rule still applies; the label is flagged because owner labels land on shared artifacts and tracker payloads, where a personal name is a privacy leak. |
Performance#
| Code | Default severity | Meaning |
|---|---|---|
scan_cache_unreadable | warning | The optional scan cache could not be read or written and was ignored. |
Postmarket#
| Code | Default severity | Meaning |
|---|---|---|
malformed_postmarket_entity | warning | A post-market record (PSIRT case, actor, deployed version, exposure assessment, ...) was malformed: wrong schema string, a missing required field, a value outside its closed vocabulary, or an unrecognized key. The record (or key) is refused and surfaced - never guessed, never silently dropped. |
postmarket_release_not_examined | warning | A stored release that advisory correlation was asked to consider could NOT be examined, so it was never evaluated against the supplied advisories. The warning names the release and the state that stopped it: stored without a copied output directory, an output directory that is not there, unreadable, malformed, not a release evidence pack, or a readable pack recording no component the matcher can evaluate. This is never a statement that the release is unaffected. A release missing from a candidate list is indistinguishable from one that produced no candidate, which is why the same fact is also recorded by name in the candidate document's scope statement rather than only in this stream. |
Provenance#
| Code | Default severity | Meaning |
|---|---|---|
build_provenance_subject_digest_mismatch | warning | A supplied build-provenance subject digest did not match the referenced local artifact hash. |
build_provenance_subject_invalid_sha256 | warning | A supplied build-provenance subject did not contain a valid SHA-256 digest. |
build_provenance_subject_missing_sha256 | warning | A supplied build-provenance subject omitted a SHA-256 digest. |
build_provenance_subject_unmatched | warning | A supplied build-provenance subject did not match a scanned local artifact. |
malformed_build_provenance | warning | A supplied build-provenance file could not be parsed as supported JSON provenance. |
missing_build_provenance_file | warning | A configured build-provenance file was missing. |
package_artifact_symlink_ignored | info | A symlink was found while indexing an operator-supplied package-artifact cache root; it was recorded and not followed, so its target was never hashed or compared against a lockfile digest. Only regular files are indexed. The link is reported so that a cache made largely of links is visibly unverified rather than appearing to have been checked. |
package_artifact_too_large | warning | A file under an operator-supplied package-artifact cache root exceeds the per-file byte limit for verification and was not hashed, so it was never compared against a lockfile digest. This is a bound on work, not a judgement about the file: an unhashed artifact is recorded as unverified and is never counted as either a digest match or a mismatch. |
package_artifact_unreadable | warning | A file under an operator-supplied package-artifact cache root could not be inspected, so its bytes were never hashed and it was never compared against a lockfile digest. The file is reported rather than passed over: an artifact that could not be read is not one that failed verification, and it is not one that matched. It contributes nothing to the verification result in either direction. |
reproducibility_comparison_unusable | warning | A configured reproducibility_comparison input was missing, unreadable, or not a sbomflow-reproducible-comparison-v1 document; it contributed no reproducibility evidence (never invented). |
unsupported_predicate | warning | An in-toto attestation used a predicateType outside the supported SLSA provenance registry; it was recorded verbatim but its build fields were not extracted, never silently mis-parsed as SLSA. A CNCF Witness attestation collection is a recognised special case: it is NAMED (collection name plus each attestor's type and window are recorded as labelled external machine evidence) and reported at info severity rather than dismissed as unknown, but it is still never SLSA-extracted and never human authority. |
Reachability#
| Code | Default severity | Meaning |
|---|---|---|
malformed_reachability_evidence_inputs | warning | Reachability evidence input JSON did not match the supported local schema. |
missing_reachability_evidence_input_file | warning | A configured reachability evidence file was missing. |
missing_reachability_evidence_inputs | warning | A configured reachability evidence input file was missing. |
reachability_python_parse_error | warning | A Python source file could not be parsed during deterministic reachability inspection. |
reachability_tree_sitter_limit | warning | Optional Tree-sitter analysis hit a deterministic traversal or output limit. |
reachability_tree_sitter_parse_error | warning | Optional Tree-sitter parsing failed or recovered from malformed C/C++ source. |
reachability_tree_sitter_unavailable | warning | Optional Tree-sitter runtime or C/C++ grammar was unavailable; baseline scanning continued. |
reachability_unreadable_file | warning | A source file could not be read during deterministic reachability inspection. |
unreadable_reachability_evidence_input_file | warning | A manufacturer-supplied reachability evidence file is present on disk but could not be read (permissions, an I/O error, or removal mid-run), so it could not be hashed. Present-but-unreadable is recorded as its own status, distinct from a file that is missing and from an entry that names no file at all: the run continues, every other supplied file in the same packet is still hashed, and this one supplies no reachability evidence because a file that cannot be hashed carries no integrity anchor to reference. |
unreadable_reachability_evidence_inputs | error | The reachability evidence inputs file itself is present but could not be read, so none of the manufacturer-supplied evidence it holds was applied. Distinct from a missing file (nothing was supplied) and from a malformed one (supplied but not parseable): the operator did supply it and the tool could not open it. Nothing is inferred in its place and the run continues without it. |
Release history#
| Code | Default severity | Meaning |
|---|---|---|
identity_variant_near_duplicate | warning | A declared release variant/channel differs from an already-recorded declaration of the same product only in case, whitespace, separators or non-printable characters, or is a prefix of it — so the two mint different product ids and split one product's history into a phantom second product. SBOMFlow records identity exactly as declared: it never rewrites, canonicalises or merges a product id, so this is a prompt for a human decision, never an automatic reconciliation. |
invalid_support_period_release_metadata | warning | Release support-period metadata could not be parsed as a supported date/time value. |
missing_support_policy_file | warning | A configured support-policy evidence file was missing. |
previous_output_unavailable | warning | A previous output directory was unavailable for release-drift comparison. |
product_identity_from_placeholder | warning | The release identity was derived from a placeholder rather than a declared one: product.name and/or product.version still hold the value SBOMFlow supplies when nobody supplies one (Unnamed Product, 0.0.0, or the REPLACE-ME that init scaffolds). product_id and release_id are hashes of those values, so every run that leaves them unset derives THE SAME ids — two unrelated code trees analysed this way are one release by identity, and the store, the release index and release drift cannot separate them. This is an observation about what was declared, NOT a finding about the product and NOT a failure: the run completes, exits as it otherwise would, and no other evidence in the pack is affected. It also does not mean a collision has happened — only that the ids carry nothing that would prevent one. Deriving the placeholder is the honest answer to an undeclared product; declaring product.name and product.version is what gives the release an identity of its own. Existing ids are never recomputed or renumbered by this warning. |
support_policy_declared_without_release_metadata | warning | A support policy was supplied but release support-period metadata was not declared. |
support_policy_missing_declared_support_period | warning | A supplied support policy did not contain a deterministic support-period date matching the release metadata shape. |
support_policy_multiple_support_period_dates | warning | A supplied support policy contained multiple support-period dates and needs reviewer interpretation. |
support_policy_support_period_mismatch | warning | A supplied support policy date did not match the declared release support period. |
unreadable_support_policy_file | warning | A supplied support-policy evidence file could not be read. |
Resource limits#
| Code | Default severity | Meaning |
|---|---|---|
resource_limit_exceeded | warning | An untrusted input exceeded a configured resource budget (size/expanded-bytes/ratio/entries/depth/path/symlink) and was bounded. |
secret_scan_content_skipped | warning | The exposed-secret pass did not read the CONTENT of one or more files in the scanned tree: they were over the 2,000,000-byte content-scan bound, or detected as binary (containing a NUL byte). Both are policy, not observation — the bytes were never searched, so the absence of a secret candidate in such a file says nothing about what it holds. This is one aggregate record per run rather than a warning per file, because the binary arm skips most of a real firmware tree; it names the counts per reason with bounded exemplars. The content-independent filename check still applies at any size, so a file literally named id_rsa is still flagged. Without this record, a PEM private key one byte over the bound produced no signal and no trace, and took analyze --strict=exposed_secret_candidate from exit 5 to exit 0. Raising the bound moves the boundary rather than closing the gap; gate on this code if any unread file should block. |
Review#
| Code | Default severity | Meaning |
|---|---|---|
cross_release_release_skipped | warning | A stored release in the local evidence store could not be read for one of the cross-release artifacts (evidence-aging.json, finding-regressions.json, carry-forward.json), so it contributed nothing to it — the file was absent, unreadable, over the read budget applied to untrusted stored input, or tombstoned by retention with its pack moved to the trash. This is not the same as a release that was read and recorded nothing, and the difference matters: a resolved-then-returned finding can read as merely persistent when the most recent prior release is unread, and a human VEX determination recorded only in an unread release is absent from the carry-forward worklist. The artifact names every skipped release, with the reason and the trash path where one applies, under provenance.prior_releases_skipped; its counts are a lower bound. Repair or restore the named release and re-run to get a complete comparison. |
decision_input_unreadable | warning | A reviewer decision file — waivers.json, approvals.json or vulnerability_reviews.json — is present at the path a gate replay reads it from and this run could not read it at all: a permission it does not hold on the file or on a directory above it, a symbolic link with no target, a symbolic link that loops, or a directory sitting where the file belongs. The bytes were never seen, so nothing is said or implied about which decisions the file holds. This is NOT the same fact as a release that recorded no decisions, and until this code existed the two were indistinguishable: the read answered absent for both, and the replay went ahead against a decision set smaller than the one the operator supplied with nothing anywhere saying so. It is also not corruption — the file may be perfectly well formed — so the repair is to the access, not to the contents; a file that WAS read and rejected is reported as malformed instead. The affected release is reported unverifiable or unsimulatable and no gate outcome is produced for it, because an outcome computed from the decisions that happened to open reads exactly like an outcome computed from all of them. |
malformed_approvals_file | warning | The release-approval ledger could not be parsed, so no approval could be read from it and the release is treated as UNAPPROVED. This is a parse observation about the file, never a statement that approval was withheld or refused: an unreadable ledger and an empty one are different facts, and the gate is told only that it has no approvals it can trust. |
malformed_waivers_file | warning | A configured waivers file exists but could not be parsed, so NO waivers were applied to the release gate and any finding a waiver would have covered is still counted. Mirrors malformed_approvals_file: a warning that changes how a release is regarded is always in the documented set. |
missing_approvals_file | warning | A configured approvals file was missing. |
missing_reviews_file | warning | A configured evidence reviews file was missing. |
missing_waivers_file | warning | A configured waivers file was missing. |
update_manifest_role_document_collision | warning | More than one TUF metadata document in the inspected directory describes the same role, and only one document's facts are reported. Previously the winner was whichever filename sorted last, so a superseded root could be reported as the repository's own — its version, threshold and keys — while the rotated-in key was absent from the whole artifact; renaming a file changed the answer. The reported document is now selected by highest declared version (path breaking a tie) and every candidate is listed under role_document_collisions with its path and version. A declared TUF delegation is NOT a collision: a targets-typed document whose role name appears in delegations.roles is reported under delegations instead, so a normal delegated repository no longer has its top-level payload inventory replaced. Signatures are not validated here, so the selection is a reporting choice and never a trust decision — a directory should hold one document per role. |
vex_fixed_no_evidence | info | A VEX review claims a finding is fixed but records neither a note nor an evidence reference (such as the fixing version). Informational quality lint; never blocks or edits the review. |
vex_justification_status_mismatch | info | A VEX review attaches a CISA justification to a status other than not_affected, which ignores it (a justification applies only to not_affected). Informational quality lint; the status is unchanged. |
vex_not_affected_no_explanation | info | A VEX review asserts not_affected with a valid CISA justification but no explanatory note. Informational quality lint: a bare justification is thin audit evidence. It never blocks a gate, edits a review, or changes the VEX status. |
vex_reused_note | info | The same substantive VEX note is reused verbatim across several distinct findings, a sign of copy-pasted rationale that weakens per-finding review. Informational quality lint; never blocks or edits reviews. |
vex_unedited_template | info | A VEX review submits a justification scaffold from review --template verbatim, without editing it into a specific per-finding explanation. Informational quality lint; never blocks or edits reviews. |
waiver_entry_unreadable | warning | A row in the waivers file could not be read as a decision: no scope this build recognises appears in the entry body or the record key, no target key survives, or the value stored under the record key is not a mapping at all. The row is never applied to the gate — it carries no identity to match a gap or a finding against — and no scope or key is invented for it. It is kept in the file exactly as it was written, under its original record key, so a reviewer-authored record is never removed by an unrelated write. The warning names the record key and which half of the identity is missing, so the repair goes to the right field. |
waiver_record_key_collision | warning | Two or more rows in the waivers file recover onto ONE record key, so only one of them can be the decision for it. The usual cause is documented scope aliases — gap, evidence_gap and evidence-gap are one scope, as are vulnerability, vuln, cve and finding, and license and licence — so two reviewers using two accepted spellings for the same target write two rows that mean one record; rows whose entry body overrides the record key they are filed under meet the same way. Every colliding row is well formed and readable: this is not the unreadable-row channel, and no row is malformed, expired or refused by being named here. The applied row is chosen by ordering the colliding record keys AS WRITTEN and taking the first — a deliberately content-free rule, so the engine never decides a reviewer's exception by picking the longer or shorter expiry — which means the same set of rows produces the same gate outcome whatever order the file holds them in. Before this code existed the survivor was whichever row the mapping yielded last, so an enforced gate returned PASS or BLOCK on a byte-identical set of decisions with nothing on any surface naming the row that was dropped. The warning names the record key, every member with the scope spelling and approver it was written with, and which member applied. Keep one row per record. |
waiver_target_ambiguous | warning | An active gate waiver names an advisory id only, but that advisory produced more than one finding in this run (the same CVE on two components is two distinct findings). The waiver still applies to all of them — it is never silently dropped — and every finding_key it reaches is named so the blast radius is explicit. Narrow it by targeting an exact finding_key. |
Sbom conformance#
| Code | Default severity | Meaning |
|---|---|---|
sbom_minimum_element_missing | info | An emitted SBOM (CycloneDX 1.6 or SPDX 2.3) does not carry one of the four data fields the CISA 2025 DRAFT 'Minimum Elements for a Software Bill of Materials' adds to the NTIA 2021 baseline — component hash, license, generation tool name, or generation context. SBOMFlow's validate path runs an engineering PRESENCE check over the SBOMs it emits and names the absent element, so a reviewer targeting the draft guidance sees exactly what is missing. It is never a conformance verdict: the guidance is a draft (Federal Register 2025-08-22, comment docket CISA-2025-0007, not a final rule), the check observes the document rather than judging it, and SBOMFlow never fabricates a value it did not observe — a registry-coordinate component carries a Package URL, never an invented content hash. |
Scan#
| Code | Default severity | Meaning |
|---|---|---|
zero_components_observed | info | This run parsed ZERO components, so the component inventory is empty and every count derived from it — advisory findings first among them — is a count over nothing. Zero findings over zero components is a statement about what this run examined, never a statement about the product. Recorded unconditionally whenever the final merged component set is empty, which is deliberately NOT what possible_missed_composition does: that code fires only when a signal suggests composition was MISSED, and stays silent on a genuinely component-free tree, so on an empty scan root nothing at all used to be recorded and a complete-looking evidence pack read exactly like a clean one. Severity is 'info' because an empty inventory is not a defect in the input — pointing the scan at a repository before the build ran, at the wrong subdirectory of a monorepo, or at a docs-only tree all produce it legitimately — and the fact is recorded so it cannot be invisible, never as a judgement about the target. It changes no exit code by itself; observation-coverage.json holds the per-file accounting behind it. |
Scanner#
| Code | Default severity | Meaning |
|---|---|---|
aosp_repo_manifest_not_parsed | info | An AOSP repo-tool manifest (default.xml carrying <remote>/<project> markers) was recognized but is not parsed yet; no components were derived from it. Generate an SBOM from the build output and supply it with --import-sbom. |
bazel_not_parsed | info | A Bazel build file (MODULE.bazel, WORKSPACE, WORKSPACE.bazel, BUILD.bazel) was recognized by name but is not parsed yet; no components were derived from it. Generate an SBOM from the Bazel build and supply it with --import-sbom. |
bun_not_parsed | info | Bun's binary lockfile (bun.lockb) was recognized by name but is not parsed; a binary format is only parsed against a real specification. The text bun.lock IS parsed — Bun >= 1.2 writes it by default, and bun install --save-text-lockfile migrates an existing bun.lockb — or generate an SBOM from the build and supply it with --import-sbom. |
clojure_not_parsed | info | A Clojure dependency file (deps.edn, project.clj) was recognized by name but is not parsed; no components were derived from it. deps.edn carries declared coordinates only (the resolved set lives in the classpath cache) and project.clj is executable Clojure, so parsing either would be a guess. Supply a CycloneDX SBOM from your build with --import-sbom. |
component_name_case_collapsed | warning | Two or more observed component names differ only in case and normalize onto the same purl, so they merged into a single component row and only one spelling is recorded. The Package URL specification lowercases the name and/or namespace for these purl types, and SBOMFlow follows it deliberately — un-folding the name loses real advisory matches — but the ecosystem underneath does not always agree: Go module paths are case sensitive, so a go.mod requiring both github.com/Sirupsen/logrus and github.com/sirupsen/logrus declares two distinct modules that the purl identity cannot tell apart. The warning names every observed spelling so the merge is visible instead of silent; nothing is changed by it, and whether the spellings are the same real artifact is a human call. PEP 503 (pypi) name normalization is not reported here: it is a documented semantic equivalence, not a lossy fold. |
conda_not_parsed | info | A conda-lock lockfile (conda-lock.yml) was recognized by name but is not parsed yet; no components were derived from it. Generate an SBOM from the environment and supply it with --import-sbom. Bare environment.yml is deliberately not claimed by name: it is a generic file-name convention far beyond conda. |
crystal_not_parsed | info | A Crystal shards file (shard.yml, shard.lock) was recognized by name but is not parsed; no components were derived from it. shard.lock is resolved, but there is no purl type or advisory ecosystem for Crystal to match it against. |
device_tree_not_parsed | info | A recognized device-tree source overlay was retained and hashed but is not parsed for components in the default scan; hardware-revision differences it encodes need human review. |
go_stdlib_observed_not_matched | info | go.mod's go and toolchain directives name the Go standard library and toolchain a module builds against. They are recorded BY NAME ONLY: no stdlib component is created and Go stdlib advisories (net/http, crypto/tls, archive/zip and similar) are not matched, so a run makes no claim either way about standard-library exposure. Before this warning the directives were recognised and then discarded with no disposition at all, which reads as nothing to report — a false clean. Treating stdlib or toolchain as a matchable component would change vulnerability-matching semantics and is a separate, owner-gated decision; until then, review Go stdlib advisories for the reported version separately. A value that is not a readable Go version, or a directive declared more than once, is reported verbatim rather than guessed at. |
gosum_entry_missing | warning | A Go module required in go.mod has no matching h1 hash in the sibling go.sum (a stale or incomplete go.sum). The component is still recorded; only the go.sum dirhash provenance is absent. An h1 dirhash is recorded metadata, never a file-content or authenticity proof. |
gradle_version_catalog_not_parsed | info | A Gradle version catalog (libs.versions.toml) was recognized by name but is not parsed yet; it declares Maven coordinates and no components were derived from it. Lock the build with gradle.lockfile (parsed) or supply an SBOM with --import-sbom. |
haskell_not_parsed | info | A Haskell lockfile (stack.yaml.lock, cabal.project.freeze) was recognized by name but is not parsed yet; no components were derived from it. Generate an SBOM from the build and supply it with --import-sbom. |
helm_not_parsed | info | A Helm chart file (Chart.yaml, Chart.lock) was recognized by name but is not parsed yet; no components were derived from it. Chart.lock is a resolved lockfile, but purl-spec defines no helm type and OSV serves no Helm ecosystem, so a parse would be un-matchable inventory at best — generate an SBOM from the rendered chart and supply it with --import-sbom. |
hex_not_parsed | info | An Elixir mix manifest (mix.exs) was recognized by name but is not parsed yet; no components were derived from it. The resolved mix.lock IS parsed — commit it, or generate an SBOM from the build and supply it with --import-sbom. |
julia_not_parsed | info | A Julia Pkg environment file (Project.toml, Manifest.toml) was recognized by name but is not parsed yet; no components were derived from it. Generate an SBOM from the environment and supply it with --import-sbom. |
lockfile_dependency_override | info | A lockfile declares a dependency override (a JS-ecosystem overrides block — bun.lock, pnpm-lock.yaml v9): the project forces the resolver's choice for a package, so the shipped graph can differ from the packages' own declared ranges. The resolved registry identity is what ships and is what the inventory records; the override declaration is recorded as evidence — a provenance row on the affected component(s) plus this warning — never as a new component or an altered identity. |
lockfile_dependency_patched | info | A lockfile declares a locally patched dependency (a JS-ecosystem patchedDependencies block — bun.lock, pnpm-lock.yaml v9): a patch file is applied to the package on install, so the shipped bytes differ from the registry artifact while the component keeps its registry identity — the coordinate advisories are keyed by. Advisory matches for that identity may not reflect the patched code in either direction; weighing that is a human-review decision. The patch reference is recorded as provenance evidence on the affected component; no identity is changed and no component is invented. |
lockfile_pin_without_release_version | info | A lockfile or manifest pinned a dependency to a branch/commit/source, or recorded it with no version at all (e.g. a SwiftPM Package.resolved branch pin, an ESP-IDF dependencies.lock entry with no version, a package.json git+https://…#<sha> or github:u/r#<ref> dependency). The component IS recorded — it ships — but WITHOUT a version, so it is honestly excluded from exact-version coverage and no advisory match is claimed for it; the commit/branch is preserved as provenance. A version is never invented from a branch name or a commit SHA. Pin the dependency to a released version to make it matchable. |
lockfile_trusted_dependency | info | A lockfile declares a trusted-dependency allowlist (bun.lock trustedDependencies): the named packages are permitted to run install/postinstall lifecycle scripts, which execute arbitrary code on the build host. This is reviewer-relevant supply-chain evidence — a package that can run code at install time is a larger trust surface than one that cannot. The allowlist is recorded as evidence — a provenance row on the affected component(s) plus this warning — never a gate, a conclusion, or an altered identity. |
lockfile_version_catalog | info | A lockfile declares a workspace version catalog (bun.lock catalog/catalogs): a named-catalog entry states a shared version specifier for a dependency name so every workspace member resolving catalog: picks the same one. The stated specifier is recorded as evidence — a provenance row on the affected component(s) plus this warning — never a new component and never a resolved-version claim (the catalog states a specifier, which the resolver may satisfy with any matching version). |
malformed_component_name | warning | A parsed component name was structurally invalid and could not form a valid component identity; the component was dropped with a warning. |
malformed_component_version | warning | A parsed component version was structurally invalid (contained table/JSON/line text); it was recorded without a version instead of emitting garbage. |
malformed_manifest | warning | A local manifest was malformed and could not be parsed for observed facts. |
mender_artifact_not_parsed | info | A Mender Artifact (*.mender tar archive) was recognized by name but is not parsed for components in the default scan; it was recorded and hashed for provenance. Use the opt-in mender-evidence importer, which inspects the tar in memory and never extracts or executes it. |
meson_not_parsed | info | A Meson build definition (meson.build) was recognized by name but is not parsed yet; no components were derived from it. Generate an SBOM from the build and supply it with --import-sbom. |
model_coreml_not_parsed | info | A Core ML model file (.mlmodel, Protocol Buffers) was recognized by extension and hashed (SHA-256) for provenance; it is never parsed, no component is created, and no model identity is claimed. A .mlpackage bundle is a directory whose inner .mlmodel/weights are recognized individually. Deserializing the protobuf is a parser-risk surface (a fact about the format, not a security verdict on this file). |
model_gguf_not_parsed | info | A GGUF (llama.cpp) model file was recognized by extension and hashed (SHA-256) for provenance; it is never parsed, no component is created, and no model identity is claimed. Parsing GGUF is a known memory-safety surface (the integer-overflow-to-heap-overflow CVE class, e.g. CVE-2025-53630 and its fix-bypass CVE-2026-27940) — a fact about the format, not a security verdict on this file — which is exactly why recognition hashes it rather than parsing it. |
model_onnx_not_parsed | info | An ONNX model file (Protocol Buffers) was recognized by extension and hashed (SHA-256) for provenance; it is never parsed, no component is created, and no model identity is claimed. Deserializing the protobuf is a parser-risk surface (a fact about the format, not a security verdict on this file), so recognition only names and hashes it. |
model_openvino_ir_not_parsed | info | An OpenVINO IR model topology (the .xml half of an .xml+.bin pair) was recognized by a content sniff (a <net> graph with <layers>/<edges>) and hashed (SHA-256) for provenance; the generic .xml extension is never claimed by name. It is never parsed, no component is created, and no model identity is claimed; the paired .bin weights stay an opaque hashed binary. |
model_pickle_checkpoint_not_parsed | info | A pickle-class model checkpoint (.pt/.pth/.ckpt/.pkl/.pickle/.joblib) was recognized by a content sniff (a pickle stream or a torch.save zip — a text file named model.pt or a site-packages .pth path file is refused) and hashed (SHA-256) for provenance. It is never parsed and no component or model identity is claimed. This is a pickle-based serialization format: loading it with the native loader executes arbitrary code, so it is labelled unsafe_to_deserialize — a fact about the format, not a security verdict on this file — and SBOMFlow only hashes it, never deserializes it. |
model_safetensors_not_parsed | info | A safetensors model-weights file was recognized by extension and hashed (SHA-256) for provenance, but the default scan never parses model artifacts: it decodes no metadata, creates no component, and makes no claim of model identity. Parsing model formats is the gated depth path (a live RCE surface), which is why recognition only names and hashes the file. |
model_tensorrt_engine_not_parsed | info | A TensorRT serialized engine (.engine/.trt, a hardware- and version-specific plan) was recognized by extension and hashed (SHA-256) for provenance; the generic .plan extension is deliberately not claimed. It is never parsed, no component is created, and no model identity is claimed. Deserializing a serialized engine invokes a format-specific deserializer (a parser-risk surface — a fact about the format, not a security verdict on this file). |
model_tflite_not_parsed | info | A TensorFlow Lite model file (FlatBuffer) was recognized by extension and hashed (SHA-256) for provenance; it is never parsed, no component is created, and no model identity is claimed. Deserializing the FlatBuffer is a parser-risk surface (a fact about the format, not a security verdict on this file), so recognition only names and hashes it. |
nim_not_parsed | info | A Nim nimble lockfile (nimble.lock) was recognized by name but is not parsed; no components were derived from it. It is a genuinely resolved lockfile, but purl-spec defines no nim type and OSV serves no Nim ecosystem, so a parse would be inventory nothing could match against. |
nix_not_parsed | info | A Nix expression (flake.nix, default.nix) was recognized by name but is not parsed yet; no components were derived from it. The resolved flake.lock IS parsed — commit it, or generate an SBOM from the Nix build and supply it with --import-sbom. |
oci_image_index_not_parsed | info | A recognized OCI image index/layout was found but is not parsed for components in the default scan; use the opt-in OCI evidence path. |
openwrt_feeds_not_parsed | info | An OpenWrt feed list (feeds.conf, feeds.conf.default, or etc/opkg/distfeeds.conf) was recognized but is not parsed yet; no components were derived from it. A feed line declares a package-source repository, never an installed package — the resolved installed set is the opkg status database (usr/lib/opkg/status, var/lib/opkg/status, usr/lib/ipkg/status), which IS parsed. |
os_recipe_not_parsed | info | An OS package recipe (APKBUILD, PKGBUILD) was recognized by name but is not parsed; no components were derived from it. A recipe BUILDS a package rather than declaring what is installed, so parsing it would report inventory the device may never have carried. Scan the built image or its installed-package database instead. |
perl_not_parsed | info | A Perl cpanfile (the declared dependency set) was recognized by name but is not parsed yet; no components were derived from it. The resolved cpanfile.snapshot IS parsed — commit it (carton install writes it), or generate an SBOM from the build and supply it with --import-sbom. |
php_platform_observed_not_matched | info | composer.lock's platform requirements (php, php-, ext-, lib-, composer) name the PHP runtime and extensions a package set builds against. They are recorded BY NAME ONLY: no runtime or extension component is created and PHP runtime/extension advisories are not matched against them, so a run makes no claim either way about runtime exposure. Before this warning they were skipped for dependency-edge building with no disposition surfaced at all, which reads as nothing to report — a false clean, the same shape as go_stdlib_observed_not_matched. Treating a runtime or extension as a matchable component would change vulnerability-matching semantics and is a separate, owner-gated decision; until then, review PHP runtime and extension advisories for the reported constraints separately. |
pub_not_parsed | info | A Dart pub manifest (pubspec.yaml) was recognized by name but is not parsed yet; no components were derived from it. The resolved pubspec.lock IS parsed — commit it, or generate an SBOM from the build and supply it with --import-sbom. |
rauc_artifact_not_parsed | info | A RAUC update artifact (a .raucb bundle, or a manifest.raucm) was recognized by name but is not parsed for components in the default scan; it was recorded and hashed for provenance. Use the opt-in rauc-evidence importer on the bundle's manifest.raucm (the .raucb container is a signed SquashFS/CMS blob). |
requirements_include_cycle | info | A requirements '-r'/'-c' include formed a cycle and was not followed again. |
requirements_include_depth_exceeded | warning | A requirements '-r'/'-c' include chain exceeded the maximum depth and was not followed further. |
requirements_include_missing | warning | A requirements '-r'/'-c' include target was not found and was skipped. |
requirements_include_outside_root | warning | A requirements '-r'/'-c' include resolved outside the scan root (absolute path or '..' escape) and was not followed. |
requirements_include_too_large | warning | A requirements '-r'/'-c' include exceeded the text-scan byte limit and was not followed. |
requirements_include_unreadable | warning | A requirements '-r'/'-c' include could not be read and was skipped. |
sbt_not_parsed | info | An sbt build definition (build.sbt) was recognized by name but is not parsed; no components were derived from it. It is executable Scala, the same class as pom.xml and setup.py, so its dependency set is only known after the build runs. Generate a CycloneDX from your build and supply it with --import-sbom. |
swupdate_artifact_not_parsed | info | An SWUpdate artifact (a *.swu newc/crc cpio container, or its mandatory sw-description member) was recognized by name but is not parsed for components in the default scan; it was recorded and hashed for provenance. Use the opt-in swupdate-evidence importer to observe its facts (which verifies no signature and installs nothing). |
symlink_outside_root | warning | A path resolved outside the scan root (symlink escape) and was skipped to avoid reading files outside the analyzed product. |
uboot_fit_not_parsed | info | A U-Boot FIT image was recognized but is not parsed for components in the default scan; it was recorded and hashed for provenance. The .its image-tree source is claimed only when its content is a FIT source (a /dts-v1/ document with an images or configurations node), and its verified-boot signature nodes are separately surfaced as review signals; the .itb flattened blob is named by extension. Use the opt-in firmware-extract importer to carve a flattened image's contents. |
unparseable_manifest | warning | A local manifest was detected but could not be parsed. |
unpinned_requirements | info | requirements.txt lines without exact '==' pins were not inventoried; pin them or supply a lockfile. |
unreadable_file | warning | A local file could not be read during scanning. |
unreadable_sbom_artifact | warning | A file named as an SBOM (CycloneDX/SPDX naming convention) contained no readable document, so no existing-SBOM-artifact evidence signal was claimed for it; a failed or empty export is not evidence that an SBOM was retained. |
unrecognized_manifest_format | info | A recognized manifest-like file did not match a supported deterministic parser shape. |
unsupported_lockfile | info | A lockfile was recognized but its format/version is not yet parsed. |
vendored_version_unparsed | warning | A vendored C-library source header defines its catalogued version macro, but the macro's value is not a recognizable version string. The component is still recorded with the raw value preserved verbatim; no concrete version is guessed, so the component carries no version and never matches an advisory range on an invented value. |
version_conflict | warning | The scan observed conflicting versions for the same normalized component identity. |
version_not_normalizable | warning | A declared component 'version' was a range/constraint (e.g. ^1.0.0, >=2, 1.x), not a point version; it was recorded without a concrete version (unknown matchability) with the raw string preserved, never treated as a concrete version. |
zig_not_parsed | info | A Zig package manifest (build.zig.zon) was recognized by name but is not parsed; no components were derived from it. It declares dependencies with content hashes, but purl-spec defines no zig type and no advisory ecosystem serves Zig, so a parse would produce un-matchable inventory. |
Signal#
| Code | Default severity | Meaning |
|---|---|---|
evidence_identity_conflict | warning | Two evidence observations shared the same identity (product version + signal key + source reference) but reported different observed statuses. SBOMFlow never silently reconciles the disagreement: both observations are recorded and surfaced so a human resolves which is authoritative. Identical-identity observations that agree are collapsed to one item so coverage never double-counts; this deduplication is within a single run only, never across releases. |
exposed_secret_candidate | warning | A high-precision secret pattern (a PEM private-key block, an AWS AKIA access-key id, or an exact OpenSSH private-key file name) was found in the scanned tree. The matched value is never recorded — only the kind, file path, and line. It is a candidate for reviewer confirmation, never a confirmed leak or a compliance verdict; pair it with --strict to gate release on it. |
missing_security_txt_field | warning | A security.txt file was present but lacked a required RFC 9116 field. |
security_txt_expired | warning | A security.txt file has an Expires value before the scan date. |
Ssvc#
| Code | Default severity | Meaning |
|---|---|---|
invalid_ssvc_context_value | warning | SSVC context used a value outside the supported decision-point options. |
malformed_ssvc_context | warning | SSVC context JSON did not match the supported local schema. |
missing_ssvc_context | warning | A configured SSVC context file was missing. |
unknown_ssvc_context_key | warning | SSVC context contained an unknown decision-point key. |
unreadable_ssvc_context | error | A configured SSVC context file exists but could not be read (permissions, a symlink loop, or a directory in its place), so the manufacturer's decision points were not applied and the assessment used the defaults. Distinct from a missing file: the repair is access, not authoring. |
Supplier#
| Code | Default severity | Meaning |
|---|---|---|
malformed_supplier_intake | warning | A suppliers/<name>/intake.json manifest was missing, malformed, undeclared-file-bearing, or declared an unusable entry (contact-bearing id, non-date received, unsafe or absent file path). The affected supplier is skipped - never the run - and an undeclared file is never silently ingested. |
supplier_claim_conflict | warning | Two suppliers claim the same component identity at different versions. Both claims are recorded with their supplier attribution and the disagreement is surfaced for a human - never auto-resolved and never a gate by itself. |
Supplier vex#
| Code | Default severity | Meaning |
|---|---|---|
malformed_supplier_vex | warning | Supplier VEX input was malformed or unsupported. |
malformed_supplier_vex_statement | warning | A supplier VEX statement could not be parsed into a supported advisory status. |
missing_supplier_vex | warning | A configured supplier VEX file was missing. |
supplier_vex_flag_names_no_product | warning | A CSAF supplier flag carries a recognized not_affected label but a scope that names no product id - an empty product_ids array, a product_ids value that is not usable as a list of ids, or a scope written under a key CSAF does not define. The justification is therefore recorded against no product. CSAF scopes a flag with product_ids and requires at least one entry, so an unreadable scope is UNKNOWN in scope and is never widened to every product in the status group; an ABSENT product_ids is different - the format defines that as covering the whole vulnerability - and is applied without this warning. Naming the covered product ids resolves it. |
supplier_vex_invalid_justification | warning | A supplier VEX not_affected statement used an unsupported justification. |
unreadable_supplier_vex | warning | A supplier VEX file could not be read. |
unrecognized_supplier_vex_format | warning | A supplier VEX file was neither supported OpenVEX JSON nor supported CSAF JSON. |
unsupported_supplier_vex_status | warning | A supplier VEX statement used a status outside the supported set. |
Technical documentation#
| Code | Default severity | Meaning |
|---|---|---|
ambiguous_technical_documentation_content | warning | Supplied technical-documentation evidence was present but deterministic content extraction found no useful routing fields. |
incomplete_technical_documentation_structured_input | warning | Structured technical-documentation metadata was present but omitted one or more expected fields for that input type. |
malformed_technical_documentation_input_type | warning | A technical-documentation input used an unsupported input-type shape. |
malformed_technical_documentation_inputs | warning | Technical-documentation input JSON did not match the supported local schema. |
malformed_technical_documentation_section | warning | A technical-documentation section entry did not match the supported input schema. |
malformed_technical_documentation_supplied_files | warning | A technical-documentation supplied-files entry was not a supported list of local file paths. |
missing_technical_documentation_input_file | warning | A configured technical-documentation evidence file was missing. |
missing_technical_documentation_inputs | warning | A configured technical-documentation input file was missing. |
oversize_technical_documentation_input_file | warning | A supplied technical-documentation evidence file exceeded the deterministic content-inspection size limit. |
technical_documentation_input_type_section_mismatch | warning | A technical-documentation input was routed to a section outside the supported input-type mapping. |
technical_documentation_metadata_content_mismatch | warning | Declared Annex VII metadata (support-period date, signed-DoC date, or standard reference) is inconsistent with the content parsed from the supplied document. Consistency signal only — not an adequacy or conformity judgement. |
unknown_technical_documentation_input_type | warning | A technical-documentation input used an unknown input type. |
unknown_technical_documentation_section | warning | A technical-documentation input referenced an unknown model section. |
unreadable_technical_documentation_input_file | warning | A supplied technical-documentation evidence file could not be read. |
Test results#
| Code | Default severity | Meaning |
|---|---|---|
inconsistent_test_result_totals | warning | A supplied test-result summary had totals that did not reconcile deterministically. |
malformed_test_result | warning | A supplied test-result file could not be parsed as supported JUnit XML or TAP. |
missing_test_result | warning | A configured test-result file was missing. |
unreadable_test_result | warning | A supplied test-result file could not be read. |
unrecognized_test_result_format | warning | A supplied test-result file was not recognized as supported JUnit XML or TAP. |
Update manifest#
| Code | Default severity | Meaning |
|---|---|---|
malformed_update_client_config | warning | An exactly-named update-client config (mender.conf or sota.toml) was recognized but could not be parsed, so no update endpoint was classified from it. The file is surfaced - never silently skipped. |
suit_draft_format | info | A SUIT CBOR manifest was parsed against the pinned current Internet-Draft, not a final RFC serialization. |
update_manifest_expired | warning | Update metadata declared an expiry before the selected as-of time; no trust conclusion was made. |
update_manifest_malformed | warning | A TUF or SUIT update-manifest artifact was malformed, truncated, or exceeded a resource limit. |
update_manifest_missing_role | warning | A supplied TUF metadata set omitted one of root, targets, snapshot, or timestamp. |
Vendor update#
| Code | Default severity | Meaning |
|---|---|---|
mender_artifact_malformed | warning | A Mender Artifact tar/header structure was malformed, unsafe, or exceeded a resource limit. |
mender_artifact_missing | warning | The requested Mender Artifact file was not found. |
rauc_bundle_container_unsupported | warning | A RAUC signed SquashFS/CMS bundle was supplied where the bounded stdlib adapter requires manifest.raucm. |
rauc_manifest_malformed | warning | A RAUC manifest.raucm file was malformed or exceeded a resource limit. |
rauc_manifest_missing | warning | A RAUC manifest.raucm file was not found at the requested path. |
rauc_unsafe_payload_path | warning | A RAUC manifest referenced an absolute or traversing payload path; it was rejected. |
swupdate_artifact_malformed | warning | An SWUpdate newc/crc cpio or sw-description artifact was malformed, unsafe, or exceeded a resource limit. |
swupdate_artifact_missing | warning | The requested SWUpdate .swu or sw-description artifact was not found. |
uptane_metadata_expired | warning | Uptane repository metadata declared an expiry before the selected as-of time. |
uptane_metadata_malformed | warning | Uptane Director/Image repository metadata was malformed or exceeded a resource limit. |
uptane_metadata_missing | warning | No Uptane/TUF JSON metadata was found at the requested path. |
uptane_root_chain_broken | warning | An Uptane/TUF root rotation was not signed by enough keys authorised by its predecessor root or by itself, so the root chain is not internally consistent. |
uptane_root_version_gap | warning | Supplied Uptane/TUF root versions are duplicated or non-contiguous, so the root-rotation chain cannot be walked. |
Vulnerability#
| Code | Default severity | Meaning |
|---|---|---|
os_package_layer_observed_not_matched | info | A Yocto/Buildroot OS package layer was inventoried but NO advisories were matched against it, because osv.dev serves no Yocto or Buildroot ecosystem (the ecosystem map's no_advisory_source list). The components are real, hashed and fully recorded, and match_scope: none states this per component in evidence-pack.json — but before this warning that fact reached no operator-facing surface, so the run showed 0 advisory findings and a passing gate with nothing saying the layer had not been assessed. On an embedded-Linux image the OS layer is the bulk of the SBOM, so that is the highest-consequence reading available: a reassuring count standing in for work not done. Zero findings for this layer means NOT ASSESSED, never NOT VULNERABLE. Same shape as go_stdlib_observed_not_matched and php_platform_observed_not_matched. This warning adds visibility only: it changes no match, no confidence and no gate outcome. |
vulnerability_enrichment_error | warning | A vulnerability data source or exploitation snapshot (OSV/NVD/KEV/EPSS) failed to load or enrich; results may be incomplete. |
Vulnerability data#
| Code | Default severity | Meaning |
|---|---|---|
malformed_distro_tracker_snapshot | warning | A pinned distro security-tracker snapshot named by --debian-tracker-snapshot / --alpine-secdb-snapshot was missing, oversized, not valid JSON, or not the verified shape. It is refused rather than truncated and loads nothing; the run continues, the scan note states how many of the named snapshots actually loaded, and the affected findings simply carry no distro context - never a guess, and never read as the tracker having nothing to say. |
vulndb_snapshot_legacy_pin | info | A vulnerability source resolved through the STORE-WIDE pin in a vulndb store written before per-source pins existed. That scalar may have been recorded for a different source entirely, so it can freeze this source on a snapshot nobody chose for it while looking identical to a current, deliberate activation. Distinct from vulndb_snapshot_auto_selected, where nothing was pinned at all: here the operator DID choose something, just not for this source, and the two need different follow-ups. Disclosed and never refused — the legacy activation is valid and refusing it would break every pre-registry store. Run 'sbomflow vulndb pin <snapshot-id>' to record an explicit per-source activation. |
vulndb_source_set_absent | info | A store-capable option named a path that was NOT a vulndb store, so the run recorded no content-addressed source set: its findings cannot later be checked against the source bytes that produced them, and 'vulndb sources --verify-set' has nothing to replay for it. The message states how many store-capable options were EXAMINED as well as how many named a path, because 'no option named a store' and 'no option could have named one' are different facts about a build and must not read the same. The run itself is unaffected: a source set is provenance about which data was consulted, never a gate input. Point the option at a vulndb store directory to record a replayable set. Distinct from vulndb_source_set_unavailable, where a store WAS named and could not be read. |
vulndb_source_set_unavailable | info | A vulndb store was named as an input but could not be read to record the run's content-addressed source set. The pack records NO set rather than a partial one: a half-recorded set is indistinguishable from a smaller real one, and would make a later replay claim a match it cannot support. The run itself is unaffected — the source set is provenance about which data was consulted, never a gate input. |
Vulnerability source#
| Code | Default severity | Meaning |
|---|---|---|
advisory_cvss4_malformed | warning | A source-supplied CVSS v4.0 vector failed strict syntax validation. SBOMFlow computes v4 base scores only from valid vectors (via the vendored official FIRST MacroVector table); a malformed vector is recorded verbatim and flagged for review — never silently dropped and never turned into a guessed score. |
advisory_distro_branch_unknown | info | An OS-package advisory is scoped to a specific distro release (OSV qualifies its records as 'Alpine:v3.20', 'Debian:12', 'Ubuntu:22.04:LTS'), but the component's own release could not be determined — it carries no 'distro' purl qualifier (no rootfs etc/os-release was observed), carries a 'distro' qualifier that is not text (so no scanner stated a release), names a codename outside the vendored release table, or sits on a rolling suite. A distro fix is per-branch, so the advisory cannot be confirmed to apply; it is surfaced with 'unknown' match confidence for human verification and is never dropped and never asserted. Scanning the rootfs (rather than importing a bare SBOM) resolves it. |
advisory_field_truncated | info | A reviewer-relevant advisory field (summary/details/references) exceeded the recorded length cap and was truncated in the captured advisory_context. The truncation is surfaced so a reviewer knows to consult the source for the full text; the field is never silently dropped. |
advisory_missing_id | warning | An offline OSV-format snapshot record matched a component but carried no usable advisory id (OSV requires a string 'id'; the field was missing, non-string, or empty after trimming). No finding was emitted — an identity-less finding cannot be reviewed, deduplicated, or matched to a reviewer decision, and it would crash run-wide alias reconciliation — and no id was invented from an alias (advisory identity is never fabricated). The skipped record is recorded in vulnerability-scan provenance (withdrawn_advisories) with any aliases kept as reviewer context; give the record a string id to surface the advisory. |
advisory_range_unresolved | info | An advisory affected-range could not be resolved locally (a GIT commit range, or a version comparison that fell back to incomparable); the finding is surfaced as needs-human-verification with low match confidence and is never silently asserted affected or dropped. |
advisory_withdrawn | info | An advisory was withdrawn (OSV 'withdrawn' timestamp) or rejected (NVD vulnStatus 'Rejected') and was excluded from active findings — a withdrawn advisory shown as active is a false positive. The exclusion is recorded in vulnerability-scan provenance (withdrawn_advisories) so it is fully auditable, never silent. |
advisory_withdrawn_after_as_of | warning | An advisory was excluded as withdrawn, but its 'withdrawn' timestamp is LATER than the evidence instant pinned by --as-of: it was live at the instant this evidence describes and is excluded on a fact that did not exist yet. The exclusion is unchanged and the finding is not restored (a withdrawn advisory shown as active is a false positive) — this surfaces that the historical record moved, instead of leaving a reviewer to compare the two timestamps by hand. Severity is 'warning' rather than the 'info' of an ordinary withdrawal because only this case changes what a pinned instant said. |
advisory_withdrawn_malformed | warning | An advisory's 'withdrawn' field was present but not a parseable timestamp. The finding is kept ACTIVE (never silently excluded on malformed data) and surfaced for human verification. |
cross_source_severity_conflict | info | Vulnerability sources disagree on a finding's severity; surfaced for human review, never merged into a consensus. |
distro_without_advisory_ecosystem | info | A component states a distro (its rootfs etc/os-release ID, carried as the purl namespace) whose packages have no served OSV advisory ecosystem — an OpenWrt apk rootfs (osv.dev serves no OpenWrt ecosystem), or an apk/deb derivative not in the vendored distro table. It is deliberately NOT matched against another distro's advisories: Alpine or Debian fixed-versions do not describe these builds, and a name match against them would report confidently wrong patched/affected states, so it is surfaced as an honest no-match rather than silently dropped. Scanning a distro with a served ecosystem, or supplying that distro's own advisory source, is what enables matching. |
no_usable_vulnerability_intelligence | warning | No configured vulnerability source produced usable intelligence on this run (distinct from zero findings). |
no_vulnerability_source_configured | warning | No vulnerability source was configured for this run, so it makes no statement whatsoever about this product's vulnerabilities. Zero findings here means 'not checked', never 'nothing found' - the two are distinguished by the no_source_configured flag on the vulnerability source-health artifact. |
stale_vulnerability_data | warning | A configured vulnerability source's data is older than the configured staleness window. |
supplier_vex_statement_unscoped | warning | A supplier VEX statement named no component purl, so it was not applied to any finding. OpenVEX requires products; a statement whose scope cannot be resolved to a component is UNKNOWN in scope, and unknown scope is not every component sharing the advisory id. The statement is reported rather than silently dropped, so a supplier assertion never disappears without trace. |
synthetic_sample_intelligence_only | warning | This run's advisory findings came from the bundled SYNTHETIC sample feed (offline-sample, CVE-SAMPLE-* identifiers). The feed really was consulted and really did match, but its records are illustrative fixture data about example components - not vulnerability intelligence about this product - so they must never inform a release, security or conformity conclusion. Supply a real source (--osv-snapshot / vulndb / --use-osv) before treating any advisory result as meaningful. |
vulndb_record_count_drop | warning | A newly stored vulnerability-data snapshot contains more than 30% fewer records than the most recent prior snapshot of the same source type. The update succeeds because legitimate mass withdrawals exist, but the snapshot is excluded from implicit latest-of-type activation unless the operator records --allow-shrink with a non-empty --shrink-reason. The previous snapshot remains available for verification or re-pinning; this warning is magnitude context, not proof that the new feed is corrupt. |
vulndb_snapshot_auto_selected | info | A vulndb store supplied this run's snapshot for a source, but NOTHING pinned that snapshot — the most recently retrieved one of its type was used because it was newest. The selection is reasonable and the run is unaffected; it is disclosed because an unchosen snapshot otherwise looks identical to one an operator deliberately activated. Run 'sbomflow vulndb pin <snapshot-id>' to make the activation explicit. |
vulnerability_data_age_unknown | warning | The primary advisory source records no data-production date, so its freshness cannot be judged at all — most commonly a loose local snapshot file of unknown vintage. The scan still runs, but staleness checks report 'unknown' rather than letting the run's own timestamp masquerade as data freshness. Supply a dated snapshot (or vulndb-resolved data, which carries its manifest retrieved_at) so age can be judged. |
vulnerability_data_date_after_run | warning | A vulnerability source records a data-production date LATER than the run's own pinned instant — a skewed clock, a mis-recorded retrieved_at, or a hand-edited snapshot. The data cannot be that old, so no age follows from it: the source is reported with a null age and freshness 'unknown', never as freshly retrieved. This is deliberately not the same code as vulnerability_data_age_unknown, which tells an operator to supply a date; here a date was supplied and it is the date (or the clock behind it) that needs checking. The recorded value is preserved verbatim in the source row — SBOMFlow reports the input, it never repairs it. |
vulnerability_source_zero_yield | info | A requested enrichment source (CISA KEV / FIRST EPSS / NVD) contributed zero usable records to a run that did have findings — it silently added nothing (e.g. an empty snapshot, an all-withdrawn catalogue, or a wrong-ecosystem file). Surfaced so a configured-but-useless source is not mistaken for a healthy quiet run; never an error (zero yield is legitimate for small SBOMs). |
West#
| Code | Default severity | Meaning |
|---|---|---|
west_import_dir_missing | warning | The local west import mirror directory (--west-import-dir) was not found; no imports were resolved and SBOMFlow did not fall back to the network. |
west_import_fetch_failed | warning | The opt-in Zephyr west import resolver could not fetch an imported manifest. |
west_import_mirror_escape | warning | A west project name would resolve outside the local import mirror directory; it was skipped. |
west_import_mirror_unreadable | warning | An imported west manifest path inside the local mirror could not be resolved. |
west_import_missing_in_mirror | warning | An imported west manifest is absent from the local mirror (--west-import-dir); it was skipped rather than fetched over the network. |
west_import_unparseable | warning | The opt-in Zephyr west import resolver fetched a manifest that could not be parsed. |
west_import_unrecognized | warning | The opt-in Zephyr west import resolver fetched a manifest with an unrecognized shape. |
west_import_unresolved_remote | warning | The opt-in Zephyr west import resolver could not resolve a remote URL for an imported project. |
west_import_unsupported_forge | warning | The opt-in Zephyr west import resolver encountered a remote forge it does not support. |
Yocto#
| Code | Default severity | Meaning |
|---|---|---|
yocto_spdx_incomplete_doc_set | warning | A Yocto create-spdx index lists documents that are not present on disk. Components were extracted only from the documents actually found; the missing documents are never assumed or reconstructed. |
yocto_spdx_malformed | warning | A file in a Yocto create-spdx directory could not be read as an SPDX document (or the create-spdx index could not be read). It is skipped, never guessed at. |
yocto_spdx_mismatch | warning | Yocto create-spdx and a Yocto image/license manifest disagree on a package's version. Both observations are recorded and both source files are named; the engine never decides which build artifact is correct. |
yocto_spdx_unknown_relationship | info | A Yocto create-spdx document declared a relationship type outside the set the class is known to emit. It is recorded verbatim but not mapped to a dependency edge. |
yocto_spdx_unsupported_version | warning | A document in a Yocto create-spdx directory is not SPDX 2.x (e.g. create-spdx 3.0 JSON-LD). The create-spdx ingester reads SPDX 2.x; import other versions explicitly with --import-sbom. |
If a warning looks wrong#
A warning that appears incorrect — firing on a valid file, or naming the wrong reason — is a bug we want to know about. Include the warning code, the file's shape (never customer content), and your SBOMFlow version. See the FAQ for how to get support.