Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dev-docs/SECURITY_ASSURANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ start a subprocess, enable a plugin, or choose an output path.
| Repository manifests, lockfiles, and analyzer source | In-process file reads have a 64 MiB per-file limit that is checked before and during the read. Parsers never receive a partial over-limit document. Discovery is scoped to the selected target, does not follow directory symlinks, and has depth and exclusion controls. Pure parsers have registered fuzz targets and malformed-input tests. | `TestSDKContractReadLimitEnforcesBoundsAndGrowth`, `TestSDKContractReadRepositoryFileEnforces64MiBBound` (local contracts; full suite upstream in `bomly-dev/bomly-sdk` `system/read_test.go`), `test/assurance/REPOSITORY_INPUT_LIMITS.md`, `test/assurance/PARSER_FUZZING.md`, `scripts/run-fuzz.sh`, detector and analyzer package tests | A selected package manager's command output is not truncated because partial output could produce an incomplete graph. A local target may contain an unbounded number of individually bounded files. |
| Project discovery and baselines | Recursive discovery does not follow directory symlinks. Automatic baseline selection warns and ignores a symlinked `.bomly` directory or baseline file. Explicit baseline selection may use a symlink because the user chose that path. Baseline JSON is limited to 16 MiB and 10,000 entries, is strict, and validates duplicates in linear time. It can only supply policy-status decisions. | `TestPlanSubprojectsRecursiveDoesNotFollowSymlinkedDirs`, `TestResolversForTargetIgnoresAutomaticSymlinksAndAllowsExplicitSelection`, `TestResolversForTargetAllowsUserSelectedSymlinkAsProjectRoot`, `TestLoadRejectsMalformedAndUnsupportedDocuments`, `TestLoadRejectsOversizedBaseline`, `TestDocumentEntryLimit`, `TestDocumentRejectsIndexedAdvisoryOverlap`, `FuzzLoad` | An explicitly selected baseline path is trusted and may be a symlink. |
| SBOM and configuration input | Configuration reads are limited to 4 MiB and SBOM reads to 256 MiB before parsing. Strict configuration parsing rejects unknown keys. SBOM parsers are fuzzed and oversized documents fail clearly. SBOM ingest additionally refuses a document without a single unambiguous reading -- a repeated object member name or invalid UTF-8 (ADR-0039) -- with an error naming the member path or byte offset; the guarantee is exactly those two classes, and the check validates rather than re-decodes so no other parsing behavior changes. Duplicate detection retains an object's member names until that object closes, so the cost is set by every object open at once; the check is bounded by refusing a document that would hold more than 100,000 member names, or more than 16 MiB of them, simultaneously -- failing closed, since skipping the check on the largest inputs would place the gap where a crafted document would aim. The preflight also reads the document in place through a `*bytes.Buffer` rather than a generic reader, which would grow a buffer of its own and materialize each token into it -- a single 150 MiB string value retained 447 MiB that way and nothing at all in place; the name bounds do not reach values, so this is handled by not copying them. Both name bounds were needed and each was found by measurement rather than reasoning: bounding only the widest single object missed nested objects that accumulate (2.5 GiB from a 411 MiB document), and bounding names by count alone missed long names (900 MiB from a 194 MiB document, now 116 MiB and refused). An oversized name is refused on its source span before it is read, because reading one costs a copy of it -- measured at a full extra copy, escaped or not, on top of what the decoder retains; the span gates, the name accounts, and using either for both was wrong in a different direction each time. The strict reader is confined to `internal/sbom` by a guard test, so the trusted plugin wire cannot be tightened by a decoder swap. | `TestSDKContractReadLimitEnforcesBoundsAndGrowth`, `TestLoadFileRejectsOversizedFile`, `TestDetectorResolveGraph_RejectsOversizedSBOM`, `FuzzLoadFile`, `FuzzUnmarshalAutoJSON`, `TestIngestRejectsDuplicateObjectNames`, `TestIngestRejectsNestedDuplicateObjectNames`, `TestIngestRejectsInvalidUTF8`, `TestStrictIngestDoesNotRejectMerelyUnusualDocuments`, `TestBomlyOutputPassesTheStrictGate`, `TestStrictJSONStaysInSBOMIngest`, `TestADocumentTooLargeToCheckIsRefused`, `TestNestedObjectsCannotAccumulatePastTheBound`, `TestSiblingObjectsDoNotAccumulate`, `TestLongMemberNamesAreBoundedByTheirBytes`, `TestClosedObjectsReleaseTheirNameBytes`, `TestOrdinaryNamesDoNotApproachTheByteBound`, `TestStrictPreflightReadsInPlace`, `TestAnOversizedNameIsRefusedBeforeItIsRead`, `TestManyComponentsDoNotTripTheWidthBound`, `TestAmbiguityIsReportedBeforeFormatDetection` | A user-selected file can still consume work up to its limit. Case-variant field matching and other reader-divergence classes are outside the stated guarantee. |
| SBOM assertions re-published on export | An ingested document's component and document-level assertions are re-emitted under Bomly's name, so each clears its SDK publication gate on ingest *and* again on export -- a `GraphEntry` is reachable by any detector or plugin, so a value written straight onto one never passed a decoder. Contacts drop email addresses; unpublishable identities, names, comments, references, CPEs and digests are dropped rather than written. Reference types `distribution` and `vcs` are deliberately not ingested, so a detector's origin assertion cannot re-enter as a document assertion (ADR-0033, ADR-0042). Gates are idempotent, so a value cannot be laundered by another hop. | `FuzzIngestedAssertions`, `FuzzDocumentAssertions`, `TestSourceClaimsAreRegatedOnExport`, `TestOriginIsNotReadBackFromAnIngestedDocument`, `TestSingleSourceExportIsAFixedPoint`, `TestIngestLeavesComponentsEligibleForEnrichment` | A published value is only as safe as the SDK gate that admitted it; a gate defect is an SDK defect (see bomly-dev/bomly-sdk#54). A merged SPDX export cannot yet name its sources (bomly-dev/bomly-sdk#55). |
| SBOM assertions re-published on export | An ingested document's component and document-level assertions are re-emitted under Bomly's name, so each clears its SDK publication gate on ingest *and* again on export -- a `GraphEntry` is reachable by any detector or plugin, so a value written straight onto one never passed a decoder. Contacts drop email addresses; unpublishable identities, names, comments, references, CPEs and digests are dropped rather than written. Reference types `distribution` and `vcs` are deliberately not ingested, so a detector's origin assertion cannot re-enter as a document assertion (ADR-0033, ADR-0042). Gates are idempotent, so a value cannot be laundered by another hop. | `FuzzIngestedAssertions`, `FuzzDocumentAssertions`, `TestSourceClaimsAreRegatedOnExport`, `TestOriginIsNotReadBackFromAnIngestedDocument`, `TestSingleSourceExportIsAFixedPoint`, `TestIngestLeavesComponentsEligibleForEnrichment` | A published value is only as safe as the SDK gate that admitted it; a gate defect is an SDK defect -- bomly-dev/bomly-sdk#54 was one, and the local normalize-until-it-settles workaround it forced is gone since SDK v0.9.5. A merged export now names its sources in both formats, so a source identity and a source checksum are two more published values, each re-gated on export. |
| Plugin download and extraction | Direct URL packages require a checksum unless the user explicitly bypasses it. GitHub release metadata is limited to 4 MiB. ZIP and tar extraction reject traversal, links, and special files. Downloads are limited to 256 MiB; archives to 4,096 entries, 256 MiB per expanded file, and 512 MiB total. Partial files are removed. | `TestResolveGitHubReleaseRejectsOversizedMetadata`, `TestExtractZipArchiveRejectsEscapingAndSymlinkEntries`, `TestExtractTarGzArchiveRejectsEscapingLinksAndSpecialFiles`, `TestCopyDownloadWithLimit`, `TestInstallRemoteArchiveRejectsDeclaredDownloadOverLimit`, `TestArchiveExtractionLimitsAtBoundary`, `TestArchiveExtractionRejectsResourceLimits`, `TestWriteArchiveFileRemovesPartialFileAtLimit` | `--insecure-skip-checksum` is an explicit integrity bypass. |
| Plugin metadata and lifecycle | Manifests and runtime snapshots are limited to 1 MiB; the installed database is limited to 16 MiB. Plugins are installed disabled. Only enabled plugins register or run. The managed environment is allowlisted. | `TestReadFileWithLimitAcceptsExactBoundary`, `TestReadFileWithLimitRejectsOverBoundary`, `TestPluginJSONReadersRejectOversizedFiles`, `TestInstallDevBinaryVerifyEnableDisableAndUninstall`, `TestPrepareLoadsAndRunsExternalDetector`, `TestProtocolV1DetectorSnapshotDefaultsAbsentOptionalCapabilities`, `TestPluginEnvDoesNotForwardUnrelatedHostEnvironment` | Enabled plugins are trusted native processes with the user's privileges. The protocol is not an OS sandbox. |
| Package-manager and detector subprocesses | Debug logs contain executable, sanitized arguments, working directory, and raw subprocess stderr. Credential-like flag values and URL user information are redacted from command arguments. Raw stderr is hidden below debug verbosity and is not added to structured results. Build-tool commands have existing timeouts where their detector contract supplies one. | `TestSDKContractSanitizeArgsRedactsCredentials`, `TestSDKContractCommandStderrHiddenBelowDebug` (local contracts; full suites upstream in `bomly-dev/bomly-sdk` `logkit/command_test.go` and `logkit/stderr_test.go`), `TestNewConsole` (`internal/logging/logger_test.go`), `TestInstallLogsReproducibleCommandAndDebugStderr`, `TestRunGitLogsStderrAtDebug`, Java readiness command tests, representative detector command-log tests | The selected executable, its credential store, registry configuration, network traffic, and filesystem behavior are trusted host concerns. Bomly cannot reliably sanitize arbitrary tool output. Debug stderr may contain credentials or other sensitive values and users must handle debug logs as sensitive data. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,27 @@ written straight onto one never passed a decoder. Everything runs through
and asserts idempotence, the property whose failure in the component half
surfaced bomly-dev/bomly-sdk#54. The merged case is validated end to end by
the SBOM interoperability workflow, through both official validators.

## Update (2026-09-06): both gaps this ADR left open are closed

Two consequences above were open questions with SDK issues attached. SDK
v0.9.5 closed both, and this repository consumes them.

**A merged SPDX document now links its sources.** The blocker was that
`DocumentAssertions` had nowhere to keep a checksum over a source document's
bytes, which SPDX requires on every `externalDocumentRefs` entry
(bomly-dev/bomly-sdk#55). The carrier grew a document version and a source
checksum, and ingest computes that checksum where the original bytes are --
in the codec entry point, once, for every format including one added later.
The SPDX projection now sits beside the CycloneDX one. A source that reached
a graph entry without passing through ingest has no checksum and is left
unnamed rather than written as an invalid reference.

**Those links are read back.** `DocumentAssertions.Sources`
(bomly-dev/bomly-sdk#61) gives the documents behind a document a home, so
both codecs read the links on ingest and re-emit them on export. A merged
export converted again still names its inputs. Each source contributes its
own link tuple and the tuples it recorded, which is the SDK's declared merge
class for the set -- inheritance, not a rule re-decided here. The CycloneDX
`bom` reference carries the checksum too, so converting a merged CycloneDX
document to SPDX can still name every source.
60 changes: 38 additions & 22 deletions docs/SBOM.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,22 @@ Every generated document carries a stable identity:
stays the scanned project's name.
- **Several source documents** (a merge). The output mints its own identity
— both formats give a document exactly one, and adopting a source's would
name a document that is not this one — and *links* each source: a
CycloneDX external reference of type `bom`, carrying a BOM-Link
(`urn:cdx:<serial>/<version>`) for a CycloneDX source or the namespace URI
for an SPDX one. People and tools credited by any source are credited by
name a document that is not this one — and *links* each source. CycloneDX
writes a document-level external reference of type `bom`, carrying a
BOM-Link (`urn:cdx:<serial>/<version>`) for a CycloneDX source or the
namespace URI for an SPDX one. SPDX writes an `externalDocumentRefs`
entry naming the same identity, with a SHA-256 checksum over the source
document's bytes, which the specification requires on every entry; the
CycloneDX reference carries that checksum too, so the link stays usable
if the merged document is later converted to SPDX. The checksum is
computed while the source document is being read, because it cannot be
recovered from the parsed model afterwards — a source that reached Bomly
without those bytes is left unnamed in SPDX rather than written as an
invalid reference. People and tools credited by any source are credited by
the merged document too.
- Source links are read back on import, so provenance survives more than one
conversion: converting a merged document again produces a document that
still names the documents behind it, in either format.
- The producing tool with its version (CycloneDX `metadata.tools[]`; SPDX
`Creator: Tool: bomly-cli-<version>`), plus one tool entry per detector that
contributed to the graph.
Expand Down Expand Up @@ -406,13 +417,25 @@ Some information necessarily becomes less specific during conversion:
prefers: a `bomly:scopes` CycloneDX property, and the `scope=` field of the
SPDX package comment. A Bomly document therefore round-trips its scope set
exactly, and a document from any other producer still yields a usable scope.
Reading the other way, CycloneDX `required` and `optional` both become
runtime and `excluded` becomes development.
- A source document's own scope word is not preserved verbatim. A component
a CycloneDX document marked `optional` imports as runtime and re-exports as
`required`, because Bomly's model has no slot for a source-asserted scope
beside the set it derives (tracked as bomly-dev/bomly-sdk#57). Bomly's own
documents are unaffected: their scope sets survive through the carrier.
Reading the other way, CycloneDX `required` becomes runtime, while
`optional` and `excluded` become development — the specification defines an
optional component as one that is not installed or otherwise reachable, so
it is absent from what runs.
- A scope carrier naming a token this build does not recognize keeps the
scopes it does recognize. The carrier is Bomly's own, so an unreadable token
is almost always one a newer Bomly wrote; refusing the whole value would have
left the component unscoped, which in SPDX — where there is no native scope
field to fall back on — meant losing the scope entirely. The tokens that were
not read are reported as a warning naming the file.
- A source document's own scope word is preserved and written back. A
component a CycloneDX document marked `optional` re-exports as `optional`,
not as Bomly's projection of the set it derives — so `optional` and
`excluded` do not collapse into `required` across a round trip that asserted
neither. The word yields to the projection when Bomly's own scope set stops
meaning what the word meant, for example once the package turns out to be
reachable from a development root as well. SPDX 2.3 has no scope field, so
the word is carried only through a CycloneDX export; an SPDX document still
carries the full set in its package comment.
- Package origin is written on export but not read back on ingest: scanning an
SBOM produces packages with no origin, so re-exporting that graph emits
`NOASSERTION` and no distribution or vcs reference. Origin comes from a
Expand Down Expand Up @@ -440,17 +463,10 @@ Some information necessarily becomes less specific during conversion:
package that is
both the document's subject and a component describes itself the same way
in both places.
- A merged SPDX document does not link its sources. SPDX names another
document through `externalDocumentRefs`, and every entry there requires a
checksum over that document's bytes, which Bomly's document carrier cannot
yet hold (tracked as bomly-dev/bomly-sdk#55). Merged CycloneDX documents do
link their sources, and merged SPDX documents still preserve every
component assertion.
- Bomly does not read those source links back. A merged CycloneDX document
names the documents it was built from, but converting that document again
produces one that names no sources: Bomly records what a document says about
itself and has no place for the documents behind it (tracked as
bomly-dev/bomly-sdk#61). Keep the merged output if you need its provenance.
- A conversion is a fixed point within a format, not across one. A CycloneDX
serial can hold only a UUID URN, so an SPDX source converted to CycloneDX is
linked rather than adopted, and converting back produces a different
document. See "Document identity" above for how sources are named.
- The CycloneDX `group` namespace survives a CycloneDX round trip. SPDX 2.3
has no group field, so an SPDX round trip recovers the namespace only from
the PURL.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ require (
github.com/bomly-dev/bomly-plugin-pyreach-analyzer v0.2.0
github.com/bomly-dev/bomly-plugin-scorecard-matcher v0.2.0
github.com/bomly-dev/bomly-plugin-syft-detector v0.2.0
github.com/bomly-dev/bomly-sdk v0.9.6
github.com/bomly-dev/bomly-sdk v0.9.7
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,8 @@ github.com/bomly-dev/bomly-plugin-scorecard-matcher v0.2.0 h1:VOK4+GfVGukbccabjD
github.com/bomly-dev/bomly-plugin-scorecard-matcher v0.2.0/go.mod h1:3ux1Su5UCCKeF+wtttQrlw7r+B7sc6UXrK98Ybuq5gw=
github.com/bomly-dev/bomly-plugin-syft-detector v0.2.0 h1:UlKwTqp+ZWu25leky9J6NITCKDdePpdgJQl/8dInApg=
github.com/bomly-dev/bomly-plugin-syft-detector v0.2.0/go.mod h1:NVVrSMHkjC3VEDPtCI7+1c4tWBzwI1eve8tynO1pRoM=
github.com/bomly-dev/bomly-sdk v0.9.6 h1:igV2PbQBciou8jfzQ8QYSVnvQ8dC3RFUtR7priyrqIY=
github.com/bomly-dev/bomly-sdk v0.9.6/go.mod h1:gAGOEa88BE8gELt2rM/ZrqGIJnOzXiR6ocuuKW6AaJ0=
github.com/bomly-dev/bomly-sdk v0.9.7 h1:kYRSCR5jjzuAM81xyXExKC8yDstaUwxWKR/ml5gAQmQ=
github.com/bomly-dev/bomly-sdk v0.9.7/go.mod h1:gAGOEa88BE8gELt2rM/ZrqGIJnOzXiR6ocuuKW6AaJ0=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
Expand Down
Loading
Loading