# SBOM Desk > https://sbom-desk.skillsafe.ai/ The software bill of materials your build just produced, read properly in your browser and then judged. Drop a CycloneDX JSON, SPDX JSON or SPDX tag-value document; a real reader parses it locally — no upload, no converter, no vulnerability database — and four AI lanes then work over the same parsed inventory. Built on SkillSafe. Derived from four agent skills, credited in full: [@patricio0312rev/artifact-sbom-publisher](https://skillsafe.ai/skill/@patricio0312rev/artifact-sbom-publisher/) (publishing a release SBOM as a build artifact), [@jorgealves/license-compliance-auditor](https://skillsafe.ai/skill/@jorgealves/license-compliance-auditor/) (verifying dependency licences against an approved allow-list), [@patricio0312rev/dependency-vulnerability-triage](https://skillsafe.ai/skill/@patricio0312rev/dependency-vulnerability-triage/) (triaging dependency risk into an order of work) and [@patricio0312rev/dependency-doctor](https://skillsafe.ai/skill/@patricio0312rev/dependency-doctor/) (diagnosing and repairing dependency problems). This is a derived work and is not affiliated with those skills' authors, with the OWASP Foundation and the CycloneDX project, with the Linux Foundation and the SPDX project, or with any tool named here. Nothing on the page is legal advice. ## What it is for One work object: a single SBOM document. One sitting: your build emits an SBOM, you find out whether it can go to a customer, you rule on its licences, you decide where a real scan should be pointed, and you produce the fix plan plus the statement that goes with it. ## The free lane — a real SBOM reader, in the browser Three formats, parsed rather than grepped: - **CycloneDX JSON**, 1.4 through 1.7 — `bomFormat`, `specVersion`, `serialNumber`, `version`, `metadata.timestamp`, `metadata.component` as the graph root, `metadata.authors`, `metadata.lifecycles`, nested `components[]`, `dependencies[]` (`dependsOn` and `provides`), and `vulnerabilities[]` with their `ratings` and `analysis.state`. `metadata.tools` is read in **both** shapes: the pre-1.5 array and the 1.5+ `{components, services}` object, because both are in the wild. - **SPDX JSON**, 2.2 through 3.0 — `spdxVersion`, `documentNamespace`, `creationInfo.creators` split into tools and people, `packages[]` with `supplier`/`originator`, `externalRefs` for the purl and the CPE, `checksums[]`, `documentDescribes`, and `relationships[]` mapped by type, with the reversed relations (`DEPENDENCY_OF`, `CONTAINED_BY`, `BUILD_DEPENDENCY_OF`) turned the right way round so the graph points from parent to child. - **SPDX tag-value** — line-oriented, with the one wrinkle that breaks naive readers handled: `` blocks span lines, so a copyright block containing its own `PackageName:` looking line is consumed as part of the value rather than opening a new package. What it decides that a keyword search cannot: - **Package URLs are parsed.** `pkg:npm/@scope/name@1.2.3` resolves to namespace `@scope`, name `name`, version `1.2.3`; `pkg:npm/name` is reported as pinning nothing, so a tool keying on the purl alone would match every release; `pkg:PyPI/x` is flagged because the specification requires a lowercase type and a consumer comparing purl strings byte for byte will treat it as a different package. Qualifiers and the subpath are separated properly. - **SPDX licence expressions are parsed with the operator honoured.** `GPL-3.0-only OR MIT` is a **choice** the user gets to make and carries no copyleft obligation once the MIT branch is taken; `MIT AND GPL-3.0-only` stacks both. `WITH` exceptions are separated from identifiers. `LicenseRef-…` is its own class — a document-local reference whose text has to be read, not an unrecognised SPDX string. A substring search for "GPL" gets all four of those wrong. - **Retired SPDX identifiers are named with their replacement.** Over thirty are mapped: `GPL-2.0` is silent on whether later versions may be used and `GPL-2.0-only` is not, which is exactly the question compliance has to answer. This is reported as *missing information*, not as a refusal. - **Licence classes, seven of them** — permissive, weak copyleft, strong copyleft, network copyleft, source-available, restricted, public domain. **Source-available is deliberately separate**: SSPL, BUSL, Elastic and the PolyForm family are published under a licence but are not OSI-approved, and an allow-list built on the phrase "open source" lets every one of them through. - **SPDX `licenseConcluded` versus `licenseDeclared`** are read as the two different statements they are, so an SBOM author's own homework is not reported as missing data and does not silently override the package's own claim. - **The dependency graph is walked from the declared subject.** A document whose `dependencies` key is present but whose only entry is the root is reported as having *no graph*, not as having one. Edges, depth, reachability, isolated components and references pointing at components that are not in the document are all counted. - **Checksum strength**, not just presence: MD5 and SHA-1 both have practical collision attacks, so a checksum in either is reported as evidence of an accident rather than of integrity. - **Duplicate coordinates** across purl and name-version, which is what merging two generators produces and what inflates every count in the document. **The seven NTIA minimum elements**, each scored as a *share of components* rather than a boolean — supplier name, component name, version, other unique identifiers, dependency relationship, author of SBOM data, timestamp. "Supplier name present" means nothing without saying for how many of the 812 components it is present. **Twelve conformance checks**, decided locally: `C-FORMAT`, `C-IDENTITY`, `C-AUTHOR`, `C-TIMESTAMP`, `C-SUBJECT`, `C-NAMES`, `C-VERSIONS`, `C-IDS`, `C-SUPPLIER`, `C-LICENSES`, `C-HASHES`, `C-GRAPH`. Free exports, no account needed: a component inventory CSV (one row per component with ecosystem, purl validity, licence class, supplier field, checksum strength, graph depth and reachability), a licence summary CSV, an NTIA scorecard CSV, a dependency edge-list CSV, a package-URL list, an attribution `NOTICE.txt` draft, prescan Markdown and prescan JSON. The NOTICE draft lists the components whose copyright line the SBOM does **not** carry in their own block rather than dropping them, because a notice that looks complete and is not is the worst of the three possible outcomes. ## The four lanes Every lane takes the same work object and sends the same derived digest. The model returns one JSON object with a common envelope — `lane`, `title`, `posture` (`ready` / `conditional` / `blocked`), `verdict`, `headline`, `exec_summary`, `findings[]`, `reconciliation[]`, `assumptions[]`, `open_questions[]`, `summary` — plus that lane's own body. | Lane (`task`) | Stage | What it adds | | --- | --- | --- | | `conformance` | Inspect | `checks[]` (the twelve, re-decided for your audience), `ntia[]` (N1-N7 with a gap and a real fix per element), and `publish_gate` with a `publish` / `publish_with_caveats` / `hold` decision | | `licenses` | Decide | `rulings[]` — one per distinct licence expression, with `allow` / `review` / `deny` / `resolve` — plus `obligations[]`, `notice_gaps[]` and `policy_exceptions[]` | | `triage` | Decide | `triage[]` ordered `now` / `this-sprint` / `this-quarter` / `accept`, `blind_spots[]`, `monitoring[]`, and `embedded_vulnerabilities` | | `remediation` | Produce | `steps[]` with a real command per step and the flag ids each closes, `verification[]`, a sendable `statement`, and `residual[]` | Three form fields change almost every answer and are sent with every lane: `distribution` (SaaS, container image, on-premise binary, library or SDK, embedded device, internal only), `policy` (permissive only, permissive plus weak copyleft, any OSI-approved, no written policy) and `audience` (a customer's security team, a procurement questionnaire, a federal buyer scoring the NTIA elements, an EU Cyber Resilience Act technical file, an internal release gate). A strong-copyleft component is a live obligation in an on-premise binary and a much narrower one in a SaaS backend; an AGPL component is a live obligation in both, because network copyleft triggers on network use. Any lane hands its output to the fix plan with one button, so the plan and the customer statement are written from the findings rather than retyped. ## What this app will not do It reads. It never modifies your document, never uploads it, never runs a scanner, a validator or a build, and **has no vulnerability database**. It will not tell you whether a component is affected by anything, and the prompt explicitly forbids inventing a CVE identifier or a CVSS score. What the triage lane gives you instead is *exposure* reasoning — graph position, pinning, and whether a scanner could match the component at all — and the most useful thing it usually says is structural: components with no purl and no CPE are not clean, they are **unscanned**. The prescan figures are computed locally and are reliable. The lanes are written by a language model and are advice, not a certification. Nothing here is legal advice: the licence lane classifies obligations and flags the decisions somebody has to make, and never states that a particular use is or is not permitted. ## What leaves your browser Never the document. What is sent is a derived digest: the document metadata, every coverage percentage, the ecosystem and licence-class distributions, the licence expressions present with their counts, the NTIA scorecard, the twelve check statuses, the flag list, the graph figures, and a bounded stratified sample of real component rows — one block per ecosystem, weighted toward the rows that attracted flags. Every count and coverage figure in that digest is computed over the whole parsed document; the sample is a sample, and the digest's `sample_completeness:` line tells both you and the model whether counting over those rows is legitimate. The page states the digest's size and how many components it sampled before you pay for a run. ## Practical notes - CycloneDX XML and SPDX RDF/XML are detected and named, but not parsed — convert to JSON first. The page says so rather than reporting an empty document. - A `package-lock.json`, a `requirements.txt`, a dependency-tree printout or a scanner report is an *input* to an SBOM generator, not an SBOM. The reader detects each case and says which. - The browser reader handles the leading 6 MB of a document and says so when it truncates. - Runs are metered and need a signed-in SkillSafe account. `estimate` is free and the price is shown before you run. Both bundled examples replay a saved run for **every** lane at no cost. - Assessments are saved to your SkillSafe account (a declared `assessments` collection) and mirrored in the browser, with a per-lane filter and a re-run delta showing which findings closed, stayed open or appeared since your last assessment of the same document. - Every prescan flag must be reconciled by the model exactly once, and the page names the ones it skipped. The fix-plan lane is additionally checked: a step claiming to close a flag the prescan never raised is called out by id. ## Pages - `/` — the app - `/api.html` — driving it programmatically, with the `task` router documented first - `/tokens.html` — token management (noindex)