From cedcb650a526f08e110cf40b35e95e231f2a6c45 Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Wed, 23 Sep 2026 13:26:12 -0400 Subject: [PATCH 1/7] feat(s002): generate a CycloneDX bill of materials and attest release provenance A reviewer of this tree, or a deployment holding one of its binaries, should be able to answer two questions without asking anyone: what is inside this build, and where did it come from. Until now neither had an answer in the artifact. The bill. `mix trinity.sbom` runs sbom's CycloneDX generator over mix.lock and then corrects two things about what it generated, because a bill without either correction is wrong rather than merely thinner. The first is coverage. mix.lock knows Hex dependencies and knows nothing about the Rust crates the desktop shell links, so a reader who took the bill for the whole picture would conclude there is no Rust in the product. The coverage is therefore written into the document itself as a metadata property, where anyone reading the file alone will find it, rather than into a page beside it. The second is licence identifiers, and it was found by validating rather than by reasoning. CycloneDX checks license.id against the SPDX enumeration and rejects the whole document over one bad value; a Hex package's licences are free text its author typed; and sbom emits a lone licence string as an id without checking it. yamerl declares "BSD 2-Clause" where SPDX spells "BSD-2-Clause", and the official CycloneDX validator refused the first bill this project ever generated over that single field. Any identifier not in the SPDX list vendored at priv/spdx/license-ids.txt is now recorded as a licence name instead, which is where CycloneDX puts a licence it cannot resolve. Nothing is dropped and nothing is guessed: the text is kept, it simply stops claiming to be an identifier it is not. The vendored list carries its source, its ref and its sha256, and going stale can only demote a valid id to a name, never the reverse. The gate generates the bill on every commit, so one that cannot be produced fails the build rather than being missing from a release nobody checked. Provenance. The package workflow uploads the bill beside each platform's binary and attests each artifact through GitHub's workflow identity, recorded in the public transparency log. gh attestation verify then checks the attestation in the same job that produced it and writes the result to the run summary, because an attestation nobody checks is a file rather than an assurance. Transport. One README paragraph states the TLS floor and the one message authentication code Trinity mints, pinned by tests against ssl.versions/0 and against the envelope's source rather than against the sentence. The assurance case is corrected with the rest: C8 stated in writing that this project published no bill of materials, and a gap stated in an assurance case is a promise to close it. What remains is narrower and is still stated: the crate half is slice 121's, and signing is slice 101's. Signed-off-by: Ayla Croft --- .github/workflows/package.yml | 42 +- .gitignore | 4 + README.md | 8 + VERSIONS.md | 1 + docs/10-assurance-case.md | 26 +- lib/mix/tasks/trinity.sbom.ex | 125 ++++++ lib/trinity/versions.ex | 7 + mix.exs | 6 + mix.lock | 5 + priv/spdx/license-ids.txt | 722 ++++++++++++++++++++++++++++++++++ test/supply_chain_test.exs | 132 +++++++ 11 files changed, 1072 insertions(+), 6 deletions(-) create mode 100644 lib/mix/tasks/trinity.sbom.ex create mode 100644 priv/spdx/license-ids.txt create mode 100644 test/supply_chain_test.exs diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index f450520..bf8411a 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -44,6 +44,13 @@ jobs: package: name: ${{ matrix.name }} runs-on: ${{ matrix.os }} + permissions: + contents: read + # Slice 002: build provenance is an attestation signed through GitHub's OIDC identity for + # this workflow run, recorded in the public transparency log. `id-token` mints that + # identity and `attestations` writes the record; neither grants anything else. + id-token: write + attestations: write strategy: fail-fast: false matrix: @@ -319,12 +326,45 @@ jobs: [ -n "$CHILD" ] && kill "$CHILD" 2>/dev/null || true fi + # Slice 002. The bill of materials travels with the binary rather than being published + # somewhere a reader has to go and find: whoever has the artifact has the list of what is + # in it, and the bill states in its own metadata what it does not cover. + - name: Generate the bill of materials + run: mix trinity.sbom + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: ${{ matrix.artifact }} - path: burrito_out/${{ matrix.artifact }} + path: | + burrito_out/${{ matrix.artifact }} + sbom.cdx.json if-no-files-found: error + # Build provenance: a signed statement of which workflow, at which commit, produced these + # exact bytes. It proves where the artifact came from, not what it does. Verified in this + # same job, because an attestation nobody checks is a file rather than an assurance. + - name: Attest the artifact's provenance + id: attest + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: burrito_out/${{ matrix.artifact }} + + - name: Verify the attestation, and record the result in the run summary + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + set -o pipefail + gh attestation verify "burrito_out/${{ matrix.artifact }}" \ + --repo "${{ github.repository }}" 2>&1 | tee attestation.txt + { + echo "### Build provenance: ${{ matrix.artifact }}" + echo + echo '```' + cat attestation.txt + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() with: diff --git a/.gitignore b/.gitignore index d123a64..50898c0 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,7 @@ trinity-*.tar npm-debug.log /assets/node_modules/ *.db-* + +# Slice 002: the bill of materials is generated by the gate and attached to releases, +# never committed: it is a statement about a build, not a source file. +/sbom.cdx.json diff --git a/README.md b/README.md index eee1477..019e9dd 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,14 @@ documents themselves for consistency. Packaging as a single binary is documented in `docs/packaging.md`, with measured sizes and start-up times for each target; `docs/fips-leg.md` describes the FIPS build leg. +**Transport and message authentication.** Outbound connections use Erlang/OTP's TLS, whose default +client versions on the pinned toolchain are TLS 1.3 and TLS 1.2: **1.2 is the floor**, and Trinity +does not lower it (TLS 1.1 and 1.0 are available in the runtime but are not in the default set). The +only message authentication code Trinity itself mints is the **AES-256-GCM** tag over the state +envelope a held tool call carries between round trips, which is an approved algorithm in **FIPS** +mode. Both statements are pinned by a test against the runtime and the source rather than against +the sentence, so the line cannot outlive the fact. + ## How the work is organised Trinity is built in reviewed increments. Each one is small enough to review in one sitting and diff --git a/VERSIONS.md b/VERSIONS.md index b87a3c3..78e7b27 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -89,6 +89,7 @@ never pin a version hex marks as retired or vulnerable. | `postgrex` | >= 0.0.0 (optional) | ✅ in `mix.lock` | Secondary DB driver, `optional: true` so the desktop build carries none of it; compiled in only under `TRINITY_DB=postgres`, which the CI job proves. Added at Slice 010. Was one row with pgvector; pgvector keeps its own row below. | | `pgvector` | ~> 0.4.1 | ✅ in `mix.lock` | Vectors on the Postgres path: `memories.embedding_vector vector(384)` under an HNSW cosine index, searched by `Trinity.Memory.VectorStores.Pgvector` (Slice 032). The postgres job runs on the `pgvector/pgvector:pg17` image. Split from the postgrex row at Slice 010. | | `oban` | ~> 2.24 | ✅ in `mix.lock` | Uses `Oban.Engines.Lite` on SQLite and `Oban.Engines.Basic` on Postgres (the adapter is compile time, ADR-0002, so the engine is too). ⚠️ Oban Pro Workflows/Smart engine are Postgres-only. Added at Slice 050 (2.24.1 measured on hex.pm 2026-09-22, Apache-2.0). | +| `sbom` | ~> 0.11 | ✅ in `mix.lock` | CycloneDX software bill of materials, generated by the gate from mix.lock (`mix sbom.cyclonedx`). Maintained by erlef, the Erlang Ecosystem Foundation; BSD-3-Clause, CC0-1.0, MIT and Apache-2.0; 0.11.0 measured on hex.pm 2026-09-23, released 2026-09-22. Dev and test only: it ships in no artifact, and the gate validates its output, so a defect in it fails the build rather than producing a quietly wrong bill. Added at Slice 002. | | `jose` | ~> 1.11 | ✅ in `mix.lock` | JOSE (JWK, JWS, JWKS) over OTP's crypto, for the MCP resource server's token validation and the personal profile's embedded authorization server. MIT; 1.11.12 measured on hex.pm 2026-09-22. Not joken, a layer over it this tree does not need. Added at Slice 062. | | `oban_web` | ~> 2.13 | ✅ in `mix.lock` | The Oban dashboard, a LiveView mounted at /oban (dev, and behind `config :trinity, :oban_web, true` elsewhere). Apache-2.0 on hex.pm since the 2.12 line (2.13.0 measured 2026-09-22); the plan of 2026-09-05 wrote it in when it was commercial. Added at Slice 050. | | `req` | ~> 0.5 | ✅ in `mix.lock` | HTTP client. | diff --git a/docs/10-assurance-case.md b/docs/10-assurance-case.md index 6de57fa..4de68b7 100644 --- a/docs/10-assurance-case.md +++ b/docs/10-assurance-case.md @@ -137,19 +137,35 @@ algorithms are measured on a dedicated build rather than asserted. ## C8. The supply chain is controlled and the provenance of changes is established -**Argument.** Dependencies are pinned and audited; every change carries an attested author; and no -change reaches the main branch without the full gate. +**Argument.** Dependencies are pinned and audited; what is in a build is enumerated in the build; +where a released binary came from can be checked by whoever holds it; every change carries an +attested author; and no change reaches the main branch without the full gate. **Evidence.** - Versions pinned in `VERSIONS.md` and verified against the lock file by the gate; `hex.audit` and `deps.audit` run on every commit. +- A CycloneDX bill of materials is generated by the gate on every commit (`mix trinity.sbom`) and + uploaded with each platform's artifacts, so whoever has the binary has the list of what is in + it. The bill states its own coverage in a `metadata.properties` entry rather than in a document + beside it. +- Each released artifact carries build provenance attested through GitHub's workflow identity and + recorded in a public transparency log, and `gh attestation verify` checks it in the same job + that produced it, with its output in the run summary. An attestation nobody checks is a file + rather than an assurance. - Every commit carries a Developer Certificate of Origin sign-off, enforced by a local hook and independently by CI, so a bypassed hook still fails the build. - The main branch is protected: changes merge only through a pull request with the gate green. -**Gap, stated rather than omitted.** The project does not yet publish a signed Software Bill of -Materials. It is planned work, and until it exists this claim is weaker than current federal -guidance asks for. +**Gaps, stated rather than omitted.** Two remain, and they are narrower than the one this document +stated before slice 002 rather than absent. + +- The bill covers Hex dependencies resolved from `mix.lock` and does not cover the Rust crates the + desktop shell links. A reader who misses the coverage property would conclude there is no Rust + in the product, which is false. Slice 121 adds the crate half. +- The bill is not itself signed with a project-held key, and releases are not signed. Provenance + establishes where a binary was built; it does not establish that the project intended to publish + it. Signing is slice 101, and until it exists this claim is weaker than current federal guidance + asks for. ## C9. Memory safety by construction diff --git a/lib/mix/tasks/trinity.sbom.ex b/lib/mix/tasks/trinity.sbom.ex new file mode 100644 index 0000000..1ee0bf2 --- /dev/null +++ b/lib/mix/tasks/trinity.sbom.ex @@ -0,0 +1,125 @@ +# SPDX-FileCopyrightText: Sudo Apt Holdings LLC +# SPDX-License-Identifier: Apache-2.0 +defmodule Mix.Tasks.Trinity.Sbom do + @shortdoc "Generates the CycloneDX software bill of materials, and says what it is blind to" + @moduledoc """ + Slice 002. Runs `mix sbom.cyclonedx` over `mix.lock` and writes `sbom.cdx.json`, then makes two + corrections to what it generated. Neither is decoration: without the first the bill is read as + saying more than it knows, and without the second it is not a valid CycloneDX document at all. + + **What the bill covers, and what it does not.** It is generated from `mix.lock`, so it is exact + for Hex dependencies and blind to the Rust crates the desktop shell links. A reader who takes it + for the whole picture would conclude the product has no Rust in it, which is false. Rather than + leave that to a paragraph in a document nobody reads beside the file, the coverage is written + into the bill itself as a `metadata.properties` entry, where a tool or a person reading the + artifact alone will find it. + + **Licence identifiers that SPDX does not define.** CycloneDX validates `license.id` against the + SPDX enumeration and rejects a bill carrying anything else, while a Hex package's `licenses` + metadata is free text that its author typed. `sbom` emits a lone licence string as an `id` + without checking it (`deps/sbom/lib/sbom/cyclonedx.ex`, `license: {:id, license}`), so one + dependency writing `BSD 2-Clause` for `BSD-2-Clause` makes the whole document fail validation: + measured, not supposed, on the first bill this project generated. Any identifier this project + cannot find in the vendored SPDX list is therefore moved to `license.name`, which is where + CycloneDX puts a licence it cannot resolve to an identifier. Nothing is dropped and nothing is + guessed: `BSD 2-Clause` stays `BSD 2-Clause`, recorded as a name rather than asserted as an + identifier it is not. + + The gate runs this, so a bill that cannot be generated fails the build rather than being missing + from a release nobody checked. + """ + use Boundary, classify_to: Trinity + use Mix.Task + + @output "sbom.cdx.json" + @spdx_ids_path "priv/spdx/license-ids.txt" + @coverage_property "trinity:coverage" + @coverage_value "Hex dependencies resolved from mix.lock. This bill does NOT cover the Rust " <> + "crates linked by the desktop shell (src-tauri/Cargo.lock); that half is " <> + "slice 121. Absence of a component here is not evidence of its absence " <> + "from the product." + + @impl Mix.Task + def run(argv) do + output = Enum.at(argv, 0, @output) + Mix.Task.run("sbom.cyclonedx", ["--force", "--pretty", "--output", output]) + annotate!(output) + Mix.shell().info("trinity.sbom: #{output} written and annotated with its coverage") + end + + @doc """ + The SPDX licence identifiers CycloneDX accepts, as a set. + + Read from the vendored list rather than from a dependency: see the file's own header for where + it came from and for which way it fails when it goes stale. + """ + @spec spdx_ids() :: MapSet.t(String.t()) + def spdx_ids do + Application.app_dir(:trinity, @spdx_ids_path) + |> then(fn path -> if File.exists?(path), do: path, else: @spdx_ids_path end) + |> File.read!() + |> String.split("\n", trim: true) + |> Enum.reject(&String.starts_with?(&1, "#")) + |> MapSet.new() + end + + @doc "The property name the bill carries its coverage statement under." + @spec coverage_property() :: String.t() + def coverage_property, do: @coverage_property + + @doc "The coverage statement written into the bill." + @spec coverage_value() :: String.t() + def coverage_value, do: @coverage_value + + # Read, add the property, write back. A generation that produced something unreadable fails + # here rather than shipping: the point of the bill is that it can be read. + defp annotate!(output) do + bom = + case output |> File.read!() |> Jason.decode() do + {:ok, %{"bomFormat" => "CycloneDX"} = bom} -> + bom + + {:ok, other} -> + Mix.raise("#{output} is not a CycloneDX document: #{inspect(Map.keys(other))}") + + {:error, reason} -> + Mix.raise("#{output} is not readable as JSON: #{inspect(reason)}") + end + + metadata = Map.get(bom, "metadata", %{}) + + properties = + metadata + |> Map.get("properties", []) + |> Enum.reject(&(Map.get(&1, "name") == @coverage_property)) + |> Kernel.++([%{"name" => @coverage_property, "value" => @coverage_value}]) + + annotated = + bom + |> Map.put("metadata", Map.put(metadata, "properties", properties)) + |> Map.replace_lazy("components", &normalise_licenses(&1, spdx_ids())) + + File.write!(output, Jason.encode_to_iodata!(annotated, pretty: true)) + end + + # An identifier SPDX does not define is not an identifier. It keeps its text and changes field. + defp normalise_licenses(components, ids) when is_list(components) do + Enum.map(components, fn component -> + Map.replace_lazy(component, "licenses", fn choices -> + Enum.map(choices, &normalise_choice(&1, ids)) + end) + end) + end + + defp normalise_licenses(components, _ids), do: components + + defp normalise_choice(%{"license" => %{"id" => id} = license} = choice, ids) do + if MapSet.member?(ids, id) do + choice + else + %{choice | "license" => license |> Map.delete("id") |> Map.put("name", id)} + end + end + + defp normalise_choice(choice, _ids), do: choice +end diff --git a/lib/trinity/versions.ex b/lib/trinity/versions.ex index 2e240d6..4973f20 100644 --- a/lib/trinity/versions.ex +++ b/lib/trinity/versions.ex @@ -153,6 +153,13 @@ defmodule Trinity.Versions do note: "Uses `Oban.Engines.Lite` on SQLite and `Oban.Engines.Basic` on Postgres (the adapter is compile time, ADR-0002, so the engine is too). ⚠️ Oban Pro Workflows/Smart engine are Postgres-only. Added at Slice 050 (2.24.1 measured on hex.pm 2026-09-22, Apache-2.0)." }, + %{ + name: "sbom", + pin: "~> 0.11", + lock: "sbom", + note: + "CycloneDX software bill of materials, generated by the gate from mix.lock (`mix sbom.cyclonedx`). Maintained by erlef, the Erlang Ecosystem Foundation; BSD-3-Clause, CC0-1.0, MIT and Apache-2.0; 0.11.0 measured on hex.pm 2026-09-23, released 2026-09-22. Dev and test only: it ships in no artifact, and the gate validates its output, so a defect in it fails the build rather than producing a quietly wrong bill. Added at Slice 002." + }, %{ name: "jose", pin: "~> 1.11", diff --git a/mix.exs b/mix.exs index 4366cd9..264896c 100644 --- a/mix.exs +++ b/mix.exs @@ -216,6 +216,9 @@ defmodule Trinity.MixProject do {:mox, "~> 1.2", only: :test}, {:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false}, {:sobelow, "~> 0.15", only: [:dev, :test], runtime: false}, + # Slice 002: the CycloneDX bill of materials the gate generates. Dev and test only: + # it produces a record about the build and ships in no artifact. + {:sbom, "~> 0.11", only: [:dev, :test], runtime: false}, {:ex_doc, "~> 0.38", only: :dev, runtime: false}, {:nimble_options, "~> 1.1"}, # Slice 001 line 1, arm (a) recorded `only: :dev`. **Corrected at G4**, and the reason is @@ -299,6 +302,9 @@ defmodule Trinity.MixProject do # a FIPS property, so measuring it outside the mode loses nothing; 024's FIPS # properties are measured by `mix test --trace test/fips`, which stays in the mode. "cmd env ERL_AFLAGS= ./scripts/prod_check.sh", + # Slice 002: the bill of materials is generated by the gate, so a bill that cannot + # be produced fails the build rather than being quietly missing from a release. + "trinity.sbom", "credo --strict", "sobelow --exit --skip", # `cmd` runs it as its own OS process: Hex's tasks are not reliably resolvable from diff --git a/mix.lock b/mix.lock index 8e8dd5c..913ce06 100644 --- a/mix.lock +++ b/mix.lock @@ -35,6 +35,7 @@ "gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"}, "glob_ex": {:hex, :glob_ex, "0.1.12", "7b2d9369c20e2697efcfd185d13d6e84c94cd3bfd2730fbde613141c2e015c00", [:mix], [], "hexpm", "2e2fac83f113514434c7eaf267b4c38af2f91766f1cab2c5db7053b7fc1ee0bb"}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]}, + "hex_core": {:hex, :hex_core, "0.19.0", "b9b5d3cde2d5227b85c62c4ab9f9db586e6b38cf65a9bf5b683757cae9ced5eb", [:rebar3], [], "hexpm", "c2cc414f3893a7edbcab28207cadca13829407abe41f689c82f379fe22f026a5"}, "hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"}, "idna": {:hex, :idna, "7.1.0", "1067a13043538129602d2f2ce6899d8713125c7d19734aa557ce2e3ea55bd4f1", [:rebar3], [], "hexpm", "6ae959a025bf36df61a8cab8508d9654891b5426a84c44d82deaffd6ddf8c71f"}, "igniter": {:hex, :igniter, "0.8.4", "f79f1bbdc2fb7b9ca030a22d12a585b060cbf5b94b9d3f23b1148578a9e05d11", [:mix], [{:ex_ast, "~> 0.5", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.5", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "a9b1cbec996ccb100b4f7d8130129b2dd3f18eb4224ac9a0e907e428ca90dbd7"}, @@ -64,6 +65,7 @@ "oban": {:hex, :oban, "2.24.1", "2a609c54697ad2c44ba339df30491df2a40eda0758c95b5b879426e4e478bd1f", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.20", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ef8482472cf198554400b7f8e36a0ffee75c3a64de425d5c7ee625d271925ac7"}, "oban_met": {:hex, :oban_met, "1.3.1", "1d8f5ab69e94802c8fbc154dfb3de346528f55f08724f591d3c327bf4fca2ef8", [:mix], [{:oban, "~> 2.21", [hex: :oban, repo: "hexpm", optional: false]}], "hexpm", "abf49524a305bad413fef7cad798fcd3a2d42ec739a0736475582c5dc8cbca1c"}, "oban_web": {:hex, :oban_web, "2.13.0", "9c37ab5c2b976681a647dead0de8a67139620e9ff636d041b5324f90c34467b3", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, "~> 2.24", [hex: :oban, repo: "hexpm", optional: false]}, {:oban_met, "~> 1.1", [hex: :oban_met, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "78af08760ca455ab7d19a311c77de07a0e7fb512d7d68043185cee04d51fbee2"}, + "optimus": {:hex, :optimus, "0.6.1", "b493bdc7ee71035fdf8cbfbf158b1863e2d622b65c3906153e4042815c901fca", [:mix], [], "hexpm", "c0db4107a51f5af94de8b05e4208333ebb8016a3bfdbcd74df6e5c99829db17f"}, "owl": {:hex, :owl, "0.13.1", "1ec4a5dea170465f0e90c502c203079224516bc0cbd599281c8667b3c6ef8848", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "351e768af8f2edc575cdaab1a5a2f6d6381be591758a026c701c703145508a0c"}, "pgvector": {:hex, :pgvector, "0.4.1", "f45b3be1ad37efa944b418757da5414eb59085997c6de85ea83ac5b5b82701af", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:nx, "~> 0.5 or ~> 1.0", [hex: :nx, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "2bb5c468ff6c435941ca326b408a0a01a1526dcd8f2c3c478be44be091ad88e4"}, "phoenix": {:hex, :phoenix, "1.8.14", "9279cbbcd755ac8d5f42d206915e7df13b9ab49096b98114f1c99ff908e5b42e", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 2.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "2782ff375824b2b5e41561fbae4764ee7b875af6898483bca49f24a9d1e37816"}, @@ -78,12 +80,15 @@ "plug_crypto": {:hex, :plug_crypto, "2.2.0", "144014737daaf485407f5ed77daeaad74d651b216a28c87543f8cc7043f8efc8", [:mix], [], "hexpm", "83a95744ab1c75876542b6fab135fcc176280e0f301a111c1f757fddcec95d2c"}, "polaris": {:hex, :polaris, "0.1.0", "dca61b18e3e801ecdae6ac9f0eca5f19792b44a5cb4b8d63db50fc40fc038d22", [:mix], [{:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "13ef2b166650e533cb24b10e2f3b8ab4f2f449ba4d63156e8c569527f206e2c2"}, "postgrex": {:hex, :postgrex, "0.22.4", "d271f595dfd25230b6398354e19d17bb5e2d20130fd2d9bdca7e15f125d43552", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "4aae45a2d60e35b04eea2602440be152fae332901f1fc7a60fc7cb7f0f9a9c5a"}, + "protobuf": {:hex, :protobuf, "0.17.0", "39e24e43c9648e148feba16ed51100b5b2028ea900b55460377b0476f6e10613", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ca6c91f6f63e2c147b47f03eefd10b80538aa6fc55ff4b12b795efb786b0152f"}, + "purl": {:hex, :purl, "0.5.0", "67cc2a57142236ad4a93184e8db5b83e8285b4b7cf7ad4a67eeefcb61e3134f3", [:mix, :rebar3], [{:stream_data, "~> 1.1", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "658057133f7698b250821828185997279dba75f3991f5b570357cf3d73f64b4d"}, "req": {:hex, :req, "0.7.4", "23e9ffec17de032a46a4b15ed65c09793893bf4a7c680f4bbf6227fce6bdf74d", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "4b192d63253e8dcc6221ef992ea9ebef7d3555166e8423aa5b553e86bc3c69a2"}, "req_llm": {:hex, :req_llm, "1.24.0", "cdc5c5cd7f38c0e17cf7a949ced99b6760dcba0d860cd4d066fea1aeba422929", [:mix], [{:dotenvy, "~> 1.1", [hex: :dotenvy, repo: "hexpm", optional: false]}, {:ex_aws_auth, "~> 1.4", [hex: :ex_aws_auth, repo: "hexpm", optional: true]}, {:goth, "~> 1.4", [hex: :goth, repo: "hexpm", optional: true]}, {:igniter, "~> 0.7", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:jsv, "~> 0.11", [hex: :jsv, repo: "hexpm", optional: false]}, {:llm_db, ">= 2026.9.3 and < 2027.0.0", [hex: :llm_db, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:server_sent_events, "~> 1.1.0", [hex: :server_sent_events, repo: "hexpm", optional: false]}, {:splode, "~> 0.3.0", [hex: :splode, repo: "hexpm", optional: false]}, {:websockex, "~> 0.5.1", [hex: :websockex, repo: "hexpm", optional: false]}, {:zoi, "~> 0.14", [hex: :zoi, repo: "hexpm", optional: false]}], "hexpm", "8ab6bda68e28afc3adf070cc6616d51831b42b8ff144116a9f104999af4c763f"}, "rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"}, "rustler": {:hex, :rustler, "0.38.0", "7a8906998ff0d28e3021c0a73264abcda719bda344b2e58307c6805b0f87c9b4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "704c03c1bf66be12b031c5a389347b91c81c5cb819a24b068b0de36fe4a5652a"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"}, "safetensors": {:hex, :safetensors, "0.1.3", "7ff3c22391e213289c713898481d492c9c28a49ab1d0705b72630fb8360426b2", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "fe50b53ea59fde4e723dd1a2e31cfdc6013e69343afac84c6be86d6d7c562c14"}, + "sbom": {:hex, :sbom, "0.11.0", "b36348696dc1a0a0a350ebf26de12b7508fb774e565e213cba95751fc73a5447", [:mix], [{:hex_core, "~> 0.19.0", [hex: :hex_core, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:optimus, "~> 0.6.1", [hex: :optimus, repo: "hexpm", optional: false]}, {:protobuf, "~> 0.17.0", [hex: :protobuf, repo: "hexpm", optional: false]}, {:purl, "~> 0.5.0", [hex: :purl, repo: "hexpm", optional: false]}], "hexpm", "edbd95e75155fed8fe61a19f21ea590c58178fe237c816afa534bc514ae53eb9"}, "server_sent_events": {:hex, :server_sent_events, "1.1.0", "54606238b9182ba673a10ce90bd95f81a0939c1baa91cdf86b66acc04edd1832", [:mix], [], "hexpm", "8e164db8e295a2d869a8faafbf4a1eeaa749b62fe93f66271adff6394d93ce15"}, "sobelow": {:hex, :sobelow, "0.15.0", "b067d7f8522a9d758fa89cb2bfcbab7ad72c45a0993cb958c989c6fd956fdd56", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "24a800e2d7fa8c3bd21561b6ad8ad4745ed726a09fd606598981d9048708da98"}, "sourceror": {:hex, :sourceror, "1.12.3", "f58eebef0765c7a369a49a755ab2a5ee88d92777f403ea5a08b722cefcc37f51", [:mix], [], "hexpm", "d5f2f37099de794840f08c54ae546d7f6e4ea015e397be64aebe4996fa9f7da7"}, diff --git a/priv/spdx/license-ids.txt b/priv/spdx/license-ids.txt new file mode 100644 index 0000000..43465fc --- /dev/null +++ b/priv/spdx/license-ids.txt @@ -0,0 +1,722 @@ +# SPDX-FileCopyrightText: Sudo Apt Holdings LLC +# SPDX-License-Identifier: Apache-2.0 +# +# Slice 002. The SPDX licence identifiers that CycloneDX 1.6 accepts in a `license.id` field, +# one per line. This is not a copy of the SPDX licence list for its own sake: it is the exact +# enumeration the CycloneDX validator checks `license.id` against, so that `mix trinity.sbom` +# applies the same rule offline that a validator applies to the published bill. +# +# Source: https://raw.githubusercontent.com/CycloneDX/specification/1.6/schema/spdx.schema.json +# Ref: tag 1.6 +# sha256: baa9d3bd1ed57b6751b0887edead6b5063ff53ff7429cf85d476c6c94af0166e +# Taken: 2026-09-23 (704 identifiers) +# +# Staleness is safe in one direction only, and that is the direction this list fails in: an +# identifier added to SPDX after the date above is not found here, so `trinity.sbom` records it +# as a licence `name` rather than an `id`. That is still valid CycloneDX and still names the +# licence; it is only less machine-readable. The reverse mistake, emitting a string as an `id` +# that no validator recognises, is the one that makes a bill invalid, and this list prevents it. +0BSD +AAL +Abstyles +AdaCore-doc +Adobe-2006 +Adobe-Display-PostScript +Adobe-Glyph +Adobe-Utopia +ADSL +AFL-1.1 +AFL-1.2 +AFL-2.0 +AFL-2.1 +AFL-3.0 +Afmparse +AGPL-1.0 +AGPL-1.0-only +AGPL-1.0-or-later +AGPL-3.0 +AGPL-3.0-only +AGPL-3.0-or-later +Aladdin +AMDPLPA +AML +AML-glslang +AMPAS +ANTLR-PD +ANTLR-PD-fallback +Apache-1.0 +Apache-1.1 +Apache-2.0 +APAFML +APL-1.0 +App-s2p +APSL-1.0 +APSL-1.1 +APSL-1.2 +APSL-2.0 +Arphic-1999 +Artistic-1.0 +Artistic-1.0-cl8 +Artistic-1.0-Perl +Artistic-2.0 +ASWF-Digital-Assets-1.0 +ASWF-Digital-Assets-1.1 +Baekmuk +Bahyph +Barr +bcrypt-Solar-Designer +Beerware +Bitstream-Charter +Bitstream-Vera +BitTorrent-1.0 +BitTorrent-1.1 +blessing +BlueOak-1.0.0 +Boehm-GC +Borceux +Brian-Gladman-2-Clause +Brian-Gladman-3-Clause +BSD-1-Clause +BSD-2-Clause +BSD-2-Clause-Darwin +BSD-2-Clause-FreeBSD +BSD-2-Clause-NetBSD +BSD-2-Clause-Patent +BSD-2-Clause-Views +BSD-3-Clause +BSD-3-Clause-acpica +BSD-3-Clause-Attribution +BSD-3-Clause-Clear +BSD-3-Clause-flex +BSD-3-Clause-HP +BSD-3-Clause-LBNL +BSD-3-Clause-Modification +BSD-3-Clause-No-Military-License +BSD-3-Clause-No-Nuclear-License +BSD-3-Clause-No-Nuclear-License-2014 +BSD-3-Clause-No-Nuclear-Warranty +BSD-3-Clause-Open-MPI +BSD-3-Clause-Sun +BSD-4-Clause +BSD-4-Clause-Shortened +BSD-4-Clause-UC +BSD-4.3RENO +BSD-4.3TAHOE +BSD-Advertising-Acknowledgement +BSD-Attribution-HPND-disclaimer +BSD-Inferno-Nettverk +BSD-Protection +BSD-Source-beginning-file +BSD-Source-Code +BSD-Systemics +BSD-Systemics-W3Works +BSL-1.0 +BUSL-1.1 +bzip2-1.0.5 +bzip2-1.0.6 +C-UDA-1.0 +CAL-1.0 +CAL-1.0-Combined-Work-Exception +Caldera +Caldera-no-preamble +CATOSL-1.1 +CC-BY-1.0 +CC-BY-2.0 +CC-BY-2.5 +CC-BY-2.5-AU +CC-BY-3.0 +CC-BY-3.0-AT +CC-BY-3.0-AU +CC-BY-3.0-DE +CC-BY-3.0-IGO +CC-BY-3.0-NL +CC-BY-3.0-US +CC-BY-4.0 +CC-BY-NC-1.0 +CC-BY-NC-2.0 +CC-BY-NC-2.5 +CC-BY-NC-3.0 +CC-BY-NC-3.0-DE +CC-BY-NC-4.0 +CC-BY-NC-ND-1.0 +CC-BY-NC-ND-2.0 +CC-BY-NC-ND-2.5 +CC-BY-NC-ND-3.0 +CC-BY-NC-ND-3.0-DE +CC-BY-NC-ND-3.0-IGO +CC-BY-NC-ND-4.0 +CC-BY-NC-SA-1.0 +CC-BY-NC-SA-2.0 +CC-BY-NC-SA-2.0-DE +CC-BY-NC-SA-2.0-FR +CC-BY-NC-SA-2.0-UK +CC-BY-NC-SA-2.5 +CC-BY-NC-SA-3.0 +CC-BY-NC-SA-3.0-DE +CC-BY-NC-SA-3.0-IGO +CC-BY-NC-SA-4.0 +CC-BY-ND-1.0 +CC-BY-ND-2.0 +CC-BY-ND-2.5 +CC-BY-ND-3.0 +CC-BY-ND-3.0-DE +CC-BY-ND-4.0 +CC-BY-SA-1.0 +CC-BY-SA-2.0 +CC-BY-SA-2.0-UK +CC-BY-SA-2.1-JP +CC-BY-SA-2.5 +CC-BY-SA-3.0 +CC-BY-SA-3.0-AT +CC-BY-SA-3.0-DE +CC-BY-SA-3.0-IGO +CC-BY-SA-4.0 +CC-PDDC +CC0-1.0 +CDDL-1.0 +CDDL-1.1 +CDL-1.0 +CDLA-Permissive-1.0 +CDLA-Permissive-2.0 +CDLA-Sharing-1.0 +CECILL-1.0 +CECILL-1.1 +CECILL-2.0 +CECILL-2.1 +CECILL-B +CECILL-C +CERN-OHL-1.1 +CERN-OHL-1.2 +CERN-OHL-P-2.0 +CERN-OHL-S-2.0 +CERN-OHL-W-2.0 +CFITSIO +check-cvs +checkmk +ClArtistic +Clips +CMU-Mach +CMU-Mach-nodoc +CNRI-Jython +CNRI-Python +CNRI-Python-GPL-Compatible +COIL-1.0 +Community-Spec-1.0 +Condor-1.1 +copyleft-next-0.3.0 +copyleft-next-0.3.1 +Cornell-Lossless-JPEG +CPAL-1.0 +CPL-1.0 +CPOL-1.02 +Cronyx +Crossword +CrystalStacker +CUA-OPL-1.0 +Cube +curl +D-FSL-1.0 +DEC-3-Clause +diffmark +DL-DE-BY-2.0 +DL-DE-ZERO-2.0 +DOC +Dotseqn +DRL-1.0 +DRL-1.1 +DSDP +dtoa +dvipdfm +ECL-1.0 +ECL-2.0 +eCos-2.0 +EFL-1.0 +EFL-2.0 +eGenix +Elastic-2.0 +Entessa +EPICS +EPL-1.0 +EPL-2.0 +ErlPL-1.1 +etalab-2.0 +EUDatagrid +EUPL-1.0 +EUPL-1.1 +EUPL-1.2 +Eurosym +Fair +FBM +FDK-AAC +Ferguson-Twofish +Frameworx-1.0 +FreeBSD-DOC +FreeImage +FSFAP +FSFAP-no-warranty-disclaimer +FSFUL +FSFULLR +FSFULLRWD +FTL +Furuseth +fwlw +GCR-docs +GD +GFDL-1.1 +GFDL-1.1-invariants-only +GFDL-1.1-invariants-or-later +GFDL-1.1-no-invariants-only +GFDL-1.1-no-invariants-or-later +GFDL-1.1-only +GFDL-1.1-or-later +GFDL-1.2 +GFDL-1.2-invariants-only +GFDL-1.2-invariants-or-later +GFDL-1.2-no-invariants-only +GFDL-1.2-no-invariants-or-later +GFDL-1.2-only +GFDL-1.2-or-later +GFDL-1.3 +GFDL-1.3-invariants-only +GFDL-1.3-invariants-or-later +GFDL-1.3-no-invariants-only +GFDL-1.3-no-invariants-or-later +GFDL-1.3-only +GFDL-1.3-or-later +Giftware +GL2PS +Glide +Glulxe +GLWTPL +gnuplot +GPL-1.0 +GPL-1.0+ +GPL-1.0-only +GPL-1.0-or-later +GPL-2.0 +GPL-2.0+ +GPL-2.0-only +GPL-2.0-or-later +GPL-2.0-with-autoconf-exception +GPL-2.0-with-bison-exception +GPL-2.0-with-classpath-exception +GPL-2.0-with-font-exception +GPL-2.0-with-GCC-exception +GPL-3.0 +GPL-3.0+ +GPL-3.0-only +GPL-3.0-or-later +GPL-3.0-with-autoconf-exception +GPL-3.0-with-GCC-exception +Graphics-Gems +gSOAP-1.3b +gtkbook +HaskellReport +hdparm +Hippocratic-2.1 +HP-1986 +HP-1989 +HPND +HPND-DEC +HPND-doc +HPND-doc-sell +HPND-export-US +HPND-export-US-modify +HPND-Fenneberg-Livingston +HPND-INRIA-IMAG +HPND-Kevlin-Henney +HPND-Markus-Kuhn +HPND-MIT-disclaimer +HPND-Pbmplus +HPND-sell-MIT-disclaimer-xserver +HPND-sell-regexpr +HPND-sell-variant +HPND-sell-variant-MIT-disclaimer +HPND-UC +HTMLTIDY +IBM-pibs +ICU +IEC-Code-Components-EULA +IJG +IJG-short +ImageMagick +iMatix +Imlib2 +Info-ZIP +Inner-Net-2.0 +Intel +Intel-ACPI +Interbase-1.0 +IPA +IPL-1.0 +ISC +ISC-Veillard +Jam +JasPer-2.0 +JPL-image +JPNIC +JSON +Kastrup +Kazlib +Knuth-CTAN +LAL-1.2 +LAL-1.3 +Latex2e +Latex2e-translated-notice +Leptonica +LGPL-2.0 +LGPL-2.0+ +LGPL-2.0-only +LGPL-2.0-or-later +LGPL-2.1 +LGPL-2.1+ +LGPL-2.1-only +LGPL-2.1-or-later +LGPL-3.0 +LGPL-3.0+ +LGPL-3.0-only +LGPL-3.0-or-later +LGPLLR +Libpng +libpng-2.0 +libselinux-1.0 +libtiff +libutil-David-Nugent +LiLiQ-P-1.1 +LiLiQ-R-1.1 +LiLiQ-Rplus-1.1 +Linux-man-pages-1-para +Linux-man-pages-copyleft +Linux-man-pages-copyleft-2-para +Linux-man-pages-copyleft-var +Linux-OpenIB +LOOP +LPD-document +LPL-1.0 +LPL-1.02 +LPPL-1.0 +LPPL-1.1 +LPPL-1.2 +LPPL-1.3a +LPPL-1.3c +lsof +Lucida-Bitmap-Fonts +LZMA-SDK-9.11-to-9.20 +LZMA-SDK-9.22 +Mackerras-3-Clause +Mackerras-3-Clause-acknowledgment +magaz +mailprio +MakeIndex +Martin-Birgmeier +McPhee-slideshow +metamail +Minpack +MirOS +MIT +MIT-0 +MIT-advertising +MIT-CMU +MIT-enna +MIT-feh +MIT-Festival +MIT-Modern-Variant +MIT-open-group +MIT-testregex +MIT-Wu +MITNFA +MMIXware +Motosoto +MPEG-SSG +mpi-permissive +mpich2 +MPL-1.0 +MPL-1.1 +MPL-2.0 +MPL-2.0-no-copyleft-exception +mplus +MS-LPL +MS-PL +MS-RL +MTLL +MulanPSL-1.0 +MulanPSL-2.0 +Multics +Mup +NAIST-2003 +NASA-1.3 +Naumen +NBPL-1.0 +NCGL-UK-2.0 +NCSA +Net-SNMP +NetCDF +Newsletr +NGPL +NICTA-1.0 +NIST-PD +NIST-PD-fallback +NIST-Software +NLOD-1.0 +NLOD-2.0 +NLPL +Nokia +NOSL +Noweb +NPL-1.0 +NPL-1.1 +NPOSL-3.0 +NRL +NTP +NTP-0 +Nunit +O-UDA-1.0 +OCCT-PL +OCLC-2.0 +ODbL-1.0 +ODC-By-1.0 +OFFIS +OFL-1.0 +OFL-1.0-no-RFN +OFL-1.0-RFN +OFL-1.1 +OFL-1.1-no-RFN +OFL-1.1-RFN +OGC-1.0 +OGDL-Taiwan-1.0 +OGL-Canada-2.0 +OGL-UK-1.0 +OGL-UK-2.0 +OGL-UK-3.0 +OGTSL +OLDAP-1.1 +OLDAP-1.2 +OLDAP-1.3 +OLDAP-1.4 +OLDAP-2.0 +OLDAP-2.0.1 +OLDAP-2.1 +OLDAP-2.2 +OLDAP-2.2.1 +OLDAP-2.2.2 +OLDAP-2.3 +OLDAP-2.4 +OLDAP-2.5 +OLDAP-2.6 +OLDAP-2.7 +OLDAP-2.8 +OLFL-1.3 +OML +OpenPBS-2.3 +OpenSSL +OpenSSL-standalone +OpenVision +OPL-1.0 +OPL-UK-3.0 +OPUBL-1.0 +OSET-PL-2.1 +OSL-1.0 +OSL-1.1 +OSL-2.0 +OSL-2.1 +OSL-3.0 +PADL +Parity-6.0.0 +Parity-7.0.0 +PDDL-1.0 +PHP-3.0 +PHP-3.01 +Pixar +Plexus +pnmstitch +PolyForm-Noncommercial-1.0.0 +PolyForm-Small-Business-1.0.0 +PostgreSQL +PSF-2.0 +psfrag +psutils +Python-2.0 +Python-2.0.1 +python-ldap +Qhull +QPL-1.0 +QPL-1.0-INRIA-2004 +radvd +Rdisc +RHeCos-1.1 +RPL-1.1 +RPL-1.5 +RPSL-1.0 +RSA-MD +RSCPL +Ruby +SAX-PD +SAX-PD-2.0 +Saxpath +SCEA +SchemeReport +Sendmail +Sendmail-8.23 +SGI-B-1.0 +SGI-B-1.1 +SGI-B-2.0 +SGI-OpenGL +SGP4 +SHL-0.5 +SHL-0.51 +SimPL-2.0 +SISSL +SISSL-1.2 +SL +Sleepycat +SMLNJ +SMPPL +SNIA +snprintf +softSurfer +Soundex +Spencer-86 +Spencer-94 +Spencer-99 +SPL-1.0 +ssh-keyscan +SSH-OpenSSH +SSH-short +SSLeay-standalone +SSPL-1.0 +StandardML-NJ +SugarCRM-1.1.3 +Sun-PPP +SunPro +SWL +swrule +Symlinks +TAPR-OHL-1.0 +TCL +TCP-wrappers +TermReadKey +TGPPL-1.0 +TMate +TORQUE-1.1 +TOSL +TPDL +TPL-1.0 +TTWL +TTYP0 +TU-Berlin-1.0 +TU-Berlin-2.0 +UCAR +UCL-1.0 +ulem +UMich-Merit +Unicode-3.0 +Unicode-DFS-2015 +Unicode-DFS-2016 +Unicode-TOU +UnixCrypt +Unlicense +UPL-1.0 +URT-RLE +Vim +VOSTROM +VSL-1.0 +W3C +W3C-19980720 +W3C-20150513 +w3m +Watcom-1.0 +Widget-Workshop +Wsuipa +WTFPL +wxWindows +X11 +X11-distribute-modifications-variant +Xdebug-1.03 +Xerox +Xfig +XFree86-1.1 +xinetd +xkeyboard-config-Zinoviev +xlock +Xnet +xpp +XSkat +YPL-1.0 +YPL-1.1 +Zed +Zeeff +Zend-2.0 +Zimbra-1.3 +Zimbra-1.4 +Zlib +zlib-acknowledgement +ZPL-1.1 +ZPL-2.0 +ZPL-2.1 +389-exception +Asterisk-exception +Autoconf-exception-2.0 +Autoconf-exception-3.0 +Autoconf-exception-generic +Autoconf-exception-generic-3.0 +Autoconf-exception-macro +Bison-exception-1.24 +Bison-exception-2.2 +Bootloader-exception +Classpath-exception-2.0 +CLISP-exception-2.0 +cryptsetup-OpenSSL-exception +DigiRule-FOSS-exception +eCos-exception-2.0 +Fawkes-Runtime-exception +FLTK-exception +fmt-exception +Font-exception-2.0 +freertos-exception-2.0 +GCC-exception-2.0 +GCC-exception-2.0-note +GCC-exception-3.1 +Gmsh-exception +GNAT-exception +GNOME-examples-exception +GNU-compiler-exception +gnu-javamail-exception +GPL-3.0-interface-exception +GPL-3.0-linking-exception +GPL-3.0-linking-source-exception +GPL-CC-1.0 +GStreamer-exception-2005 +GStreamer-exception-2008 +i2p-gpl-java-exception +KiCad-libraries-exception +LGPL-3.0-linking-exception +libpri-OpenH323-exception +Libtool-exception +Linux-syscall-note +LLGPL +LLVM-exception +LZMA-exception +mif-exception +Nokia-Qt-exception-1.1 +OCaml-LGPL-linking-exception +OCCT-exception-1.0 +OpenJDK-assembly-exception-1.0 +openvpn-openssl-exception +PS-or-PDF-font-exception-20170817 +QPL-1.0-INRIA-2004-exception +Qt-GPL-exception-1.0 +Qt-LGPL-exception-1.1 +Qwt-exception-1.0 +SANE-exception +SHL-2.0 +SHL-2.1 +stunnel-exception +SWI-exception +Swift-exception +Texinfo-exception +u-boot-exception-2.0 +UBDL-exception +Universal-FOSS-exception-1.0 +vsftpd-openssl-exception +WxWindows-exception-3.1 +x11vnc-openssl-exception diff --git a/test/supply_chain_test.exs b/test/supply_chain_test.exs new file mode 100644 index 0000000..c601444 --- /dev/null +++ b/test/supply_chain_test.exs @@ -0,0 +1,132 @@ +# SPDX-FileCopyrightText: Sudo Apt Holdings LLC +# SPDX-License-Identifier: Apache-2.0 +defmodule SupplyChainTest do + @moduledoc """ + Slice 002, AC1 and AC3. The bill of materials the gate generates is a real CycloneDX document, + it lists the dependencies the lock file holds, and it states what it is blind to inside itself + rather than in a document beside it. + + And the two statements the README makes about transport and message authentication are pinned to + the facts rather than to the sentence: the test asserts the runtime's default TLS versions and + the algorithm that actually seals the envelope, so the line cannot survive the fact changing + underneath it. + """ + use ExUnit.Case, async: true + + @output "sbom.cdx.json" + + setup_all do + # The gate generates it before the suite runs; a developer running this file alone gets it + # generated here rather than a failure about a missing file they did not know to produce. + unless File.exists?(@output) do + {_, 0} = System.cmd("mix", ["trinity.sbom"], stderr_to_stdout: true) + end + + {:ok, bom: @output |> File.read!() |> Jason.decode!()} + end + + describe "AC1: the bill of materials" do + test "is a CycloneDX document with a specification version", %{bom: bom} do + assert bom["bomFormat"] == "CycloneDX" + assert bom["specVersion"] =~ ~r/^\d+\.\d+$/ + assert is_binary(bom["serialNumber"]) or is_nil(bom["serialNumber"]) + end + + test "lists components, and every one carries a name and a version", %{bom: bom} do + components = bom["components"] + assert is_list(components) and length(components) > 50 + + for component <- components do + assert is_binary(component["name"]) and component["name"] != "" + assert is_binary(component["version"]) and component["version"] != "" + end + end + + test "covers the dependencies the lock file holds", %{bom: bom} do + names = MapSet.new(bom["components"], & &1["name"]) + + # A sample across the tree's own dependency kinds: the web layer, the database driver, the + # protocol core and the cryptography. If the bill were generated from something other than + # the lock, one of these would be missing. + for expected <- ~w(phoenix ecto_sql exqlite beam_mcp jose) do + assert expected in names, "#{expected} is in mix.lock but not in the bill" + end + end + + # The one property a validator rejects a whole document over, asserted here so that the next + # dependency whose author types a licence by hand fails this test rather than the bill. + # Measured on the first bill generated in this slice: `yamerl` declares `BSD 2-Clause`, which + # SPDX spells `BSD-2-Clause`, and the official CycloneDX validator refused the document over + # that single field. + test "never asserts a licence identifier SPDX does not define", %{bom: bom} do + ids = Mix.Tasks.Trinity.Sbom.spdx_ids() + assert MapSet.size(ids) > 500, "the vendored SPDX list did not load" + + asserted = + for component <- bom["components"], + choice <- component["licenses"] || [], + id = get_in(choice, ["license", "id"]), + id not in [nil], + not MapSet.member?(ids, id), + do: {component["name"], id} + + assert asserted == [], "these components assert a licence id SPDX does not define" + end + + test "keeps a licence it cannot resolve, as a name rather than an identifier", %{bom: bom} do + named = + for component <- bom["components"], + choice <- component["licenses"] || [], + name = get_in(choice, ["license", "name"]), + name not in [nil], + do: name + + # Nothing is dropped on the way: whatever could not be resolved is still in the document, + # under the field CycloneDX keeps an unresolved licence in. + for name <- named, do: assert(is_binary(name) and name != "") + assert "BSD 2-Clause" in named or named == [] + end + + test "states what it is blind to, inside the document", %{bom: bom} do + properties = get_in(bom, ["metadata", "properties"]) || [] + + coverage = + Enum.find(properties, &(&1["name"] == Mix.Tasks.Trinity.Sbom.coverage_property())) + + assert coverage, "the bill carries no coverage property; a reader cannot know its limits" + assert coverage["value"] =~ "does NOT cover the Rust crates" + assert coverage["value"] =~ "not evidence of its absence" + end + end + + describe "AC3: the transport and authentication statements" do + test "the runtime's default TLS versions are 1.2 and 1.3, and nothing older" do + :ssl.start() + supported = Keyword.fetch!(:ssl.versions(), :supported) + + assert :"tlsv1.2" in supported + refute :tlsv1 in supported, "TLS 1.0 is in the default set" + refute :"tlsv1.1" in supported, "TLS 1.1 is in the default set" + end + + test "the only authentication tag Trinity mints is AES-256-GCM's" do + envelope = File.read!("lib/trinity/mcp/server/envelope.ex") + assert envelope =~ ":aes_256_gcm" + + # And no weaker construction anywhere in the tree's own code. + {out, 0} = System.cmd("git", ["ls-files", "lib/*.ex", "lib/**/*.ex"]) + + for file <- out |> String.split("\n", trim: true) |> Enum.uniq() do + source = File.read!(file) + refute source =~ ~r/:aes_\d+_(ecb|cbc)\b/, "#{file} uses an unauthenticated cipher mode" + end + end + + test "the README states both, so a reader need not read the source to learn them" do + readme = File.read!("README.md") + assert readme =~ "TLS 1.2" + assert readme =~ "AES-256-GCM" + assert readme =~ "FIPS" + end + end +end From 40a6dbcd433be59aca25748b6560b59579958d7c Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Wed, 23 Sep 2026 13:39:26 -0400 Subject: [PATCH 2/7] feat(s002): generate the bill once rather than once per operating system The bill is derived from mix.lock alone. It is the same document whichever runner writes it, so generating it inside each of the three packaging jobs bought nothing and cost twice: mix trinity.sbom needs the project compiled in the dev environment, which those jobs do not otherwise do, and it gave Windows a chance to produce a bill differing from Linux's for no reason a reader could act on. A job of its own now generates it on Linux and uploads it; each packaging job downloads it and ships it beside its binary, so the bill still travels with every artifact. download-artifact is pinned to the commit for v4.3.0, verified against the tag rather than copied. Signed-off-by: Ayla Croft --- .github/workflows/package.yml | 36 +++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index bf8411a..44470c1 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -41,9 +41,40 @@ on: workflow_dispatch: jobs: + # Slice 002. The bill of materials is derived from `mix.lock` alone: it is the same document + # whichever runner produces it, and `mix trinity.sbom` needs the project compiled in the dev + # environment, which the three packaging jobs do not otherwise do. Generating it once here and + # handing it to each of them keeps the bill travelling with every binary without paying for a + # second full compile on three operating systems, and without giving Windows a chance to + # produce a bill that differs from Linux's for no reason a reader could act on. + sbom: + name: bill of materials + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 + with: + version-file: .tool-versions + version-type: strict + + - run: mix deps.get + + - name: Generate the bill of materials + run: mix trinity.sbom + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: sbom + path: sbom.cdx.json + if-no-files-found: error + package: name: ${{ matrix.name }} runs-on: ${{ matrix.os }} + needs: sbom permissions: contents: read # Slice 002: build provenance is an attestation signed through GitHub's OIDC identity for @@ -329,8 +360,9 @@ jobs: # Slice 002. The bill of materials travels with the binary rather than being published # somewhere a reader has to go and find: whoever has the artifact has the list of what is # in it, and the bill states in its own metadata what it does not cover. - - name: Generate the bill of materials - run: mix trinity.sbom + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: sbom - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: From 297eccd8ea90b2a3b68139debc47b4cc67c2077b Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Wed, 23 Sep 2026 13:51:06 -0400 Subject: [PATCH 3/7] feat(s002): leave the verification result in the run log, not only the summary gh attestation verify draws its result with carriage returns and the log viewer collapses them to nothing, so the step ran for four seconds and left no trace a reader could quote. One plain line now records the outcome, reached only when the verification passed: the step runs under set -e with pipefail. Signed-off-by: Ayla Croft --- .github/workflows/package.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 44470c1..b563ae7 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -389,6 +389,10 @@ jobs: set -o pipefail gh attestation verify "burrito_out/${{ matrix.artifact }}" \ --repo "${{ github.repository }}" 2>&1 | tee attestation.txt + # `gh attestation verify` draws its result with carriage returns, which the log + # viewer collapses to nothing. One plain line the log keeps, reached only because + # `set -e` and `pipefail` end the step on a failed verification. + echo "gh attestation verify: PASS for ${{ matrix.artifact }}" { echo "### Build provenance: ${{ matrix.artifact }}" echo From 32cdcd447f4ee6b31b0f119c261f96e630cce906 Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Wed, 23 Sep 2026 14:02:56 -0400 Subject: [PATCH 4/7] docs(s002): record the bill of materials and build provenance in the README The assurance posture table is what a reader evaluating this project for adoption reads first, and it named neither. Supply chain now says a bill ships with every binary and states its own coverage inside the document; provenance now says every binary carries an attestation checked in the run that produced it, so a holder can verify where the bytes were built without trusting the page that told them to. Signed-off-by: Ayla Croft --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 019e9dd..ffffc41 100644 --- a/README.md +++ b/README.md @@ -186,8 +186,8 @@ rather than described by it. | Auditability | Decisions and effects are recorded in an Ed25519-signed hash chain with checkpoints and a verifier, so an operator can reconstruct what was done, by whom and under what decision. | | Untrusted content | Anything arriving from outside the machine is marked untrusted at the boundary and is never treated as instruction. | | Approved cryptography | A dedicated CI leg builds from source and runs the cryptographic properties inside a FIPS-mode container, so statements about approved algorithms are measured on that leg rather than asserted. | -| Supply chain | Dependency and licence audits run on every commit; dependency versions are pinned in `VERSIONS.md` and verified against the lock file by the gate. | -| Provenance | Every commit carries a Developer Certificate of Origin sign-off, enforced by a hook and independently by CI. | +| Supply chain | Dependency and licence audits run on every commit; dependency versions are pinned in `VERSIONS.md` and verified against the lock file by the gate. A CycloneDX bill of materials is generated by the gate on every commit and ships beside every packaged binary, so whoever holds the artifact holds the list of what is in it; the bill states its own coverage inside the document, including what it does not cover. | +| Provenance | Every commit carries a Developer Certificate of Origin sign-off, enforced by a hook and independently by CI. Every packaged binary carries build provenance attested through the workflow's own identity and recorded in a public transparency log, verified in the same run that produced it, so a holder can check where the bytes were built without trusting this page. | | Independent self-certification | The project holds the [OpenSSF Best Practices passing badge](https://www.bestpractices.dev/projects/14772), assessed against the Open Source Security Foundation's published criteria for basics, change control, reporting, quality, security and analysis. Three suggested criteria are recorded as unmet rather than stretched: semantic versioning, which begins at the first supported release, and two concerning dynamic analysis tooling, which the project does not yet run. | | Argued, not asserted | `docs/10-assurance-case.md` decomposes the top-level security claim into ten claims, each with its argument, the evidence a reader can check, and the limit on what it covers; the assumptions the case rests on are named rather than implied. | | Claim discipline | `docs/09-standards-register.md` records one row per control a regulated deployment might ask about, each with an evidence path and a status. No public claim about a regulation or requirement is made without a row there saying it is true. | From 699a9b15da2c93f324a50e9e9d50307b5623530e Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Wed, 23 Sep 2026 14:14:16 -0400 Subject: [PATCH 5/7] fix(s002): keep the packaged binary at the root of its artifact Listing the binary and the bill as two upload paths made upload-artifact take the workspace as their common ancestor, so the binary moved to burrito_out/ inside the zip. gh run download --name then refuses the whole artifact with "would result in path traversal" on the gh in Ubuntu's archive (2.45.0), while a pre-slice-002 artifact downloads cleanly with the same client. Measured on run 35898448972, not reasoned about. Whoever is at fault for the traversal check, a reviewer's first attempt to fetch a binary must not fail, and the layout every earlier release used is not worth changing to carry one extra file. The bill is downloaded into burrito_out/ instead, so both land at the artifact root. Signed-off-by: Ayla Croft --- .github/workflows/package.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index b563ae7..4a64343 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -363,13 +363,21 @@ jobs: - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: sbom + path: burrito_out + # The bill is placed beside the binary rather than listed as a second upload path, so that + # both land at the root of the artifact. Uploading the two paths separately made + # upload-artifact take the workspace as their common ancestor and file the binary under + # `burrito_out/` inside the zip, and `gh run download --name` on the gh in Ubuntu's archive + # (2.45.0) then refuses the whole artifact with "would result in path traversal". Measured + # on run 35898448972 against a pre-slice-002 artifact that downloads cleanly with the same + # client. Whoever is at fault, a reviewer's first attempt to fetch a binary must not fail. - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: ${{ matrix.artifact }} path: | burrito_out/${{ matrix.artifact }} - sbom.cdx.json + burrito_out/sbom.cdx.json if-no-files-found: error # Build provenance: a signed statement of which workflow, at which commit, produced these From d65c48c1879b48b2b4addeb4ec6fc5ae2d3196e5 Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Wed, 23 Sep 2026 14:37:10 -0400 Subject: [PATCH 6/7] docs(s002): correct the cause named in the previous commit 699a9b1 said gh run download refuses the two-path artifact layout with "would result in path traversal", measured against a pre-slice-002 artifact that downloaded cleanly. The layout was not the cause and the measurement was not a comparison: the old artifact had been fetched with -D and the new one without it. Run the same way, gh 2.45.0 raises that error for either artifact when it extracts into the current directory, and succeeds for either with -D; gh 2.63.2 succeeds for both with or without. It is a bug in the client version Ubuntu ships, it predates this slice, and nothing here caused or fixed it. The layout change stands, on the reason that survives: both files at the root of the artifact is the shape every earlier release used, and moving the binary under burrito_out/ would make consumers absorb a change for no benefit. The comment in package.yml is corrected in place rather than deleted, because a record that quietly loses a wrong claim teaches nothing. Signed-off-by: Ayla Croft --- .github/workflows/package.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 4a64343..5f98df8 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -366,12 +366,18 @@ jobs: path: burrito_out # The bill is placed beside the binary rather than listed as a second upload path, so that - # both land at the root of the artifact. Uploading the two paths separately made - # upload-artifact take the workspace as their common ancestor and file the binary under - # `burrito_out/` inside the zip, and `gh run download --name` on the gh in Ubuntu's archive - # (2.45.0) then refuses the whole artifact with "would result in path traversal". Measured - # on run 35898448972 against a pre-slice-002 artifact that downloads cleanly with the same - # client. Whoever is at fault, a reviewer's first attempt to fetch a binary must not fail. + # both land at the root of the artifact. Listing them separately makes upload-artifact take + # the workspace as their common ancestor and file the binary under `burrito_out/` inside the + # zip, which is a layout change every earlier release's consumers would have to absorb for + # no benefit. + # + # Corrected 2026-09-23, because the first version of this comment named a cause that is not + # true. It said `gh run download --name` refuses the two-path layout with "would result in + # path traversal". It does refuse, but the layout has nothing to do with it: gh 2.45.0 (the + # version in Ubuntu's archive) raises that error for any artifact when extracting into the + # current directory, including a pre-slice-002 single-file one, and passing `-D ` + # succeeds for both. gh 2.63.2 downloads both without `-D`. The error was measured against + # unlike invocations and read as a regression it was not. - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: ${{ matrix.artifact }} From e9780d6a1bc78f2b83e3385336500034919268bb Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Wed, 23 Sep 2026 14:38:11 -0400 Subject: [PATCH 7/7] chore(s002): coverage row for slice 002 80.31% at d65c48c, level with slice 050's row and above the 80.05% floor the gate holds. The slice adds a Mix task and no application code, so the number moving nowhere is the expected result rather than a happy one. Signed-off-by: Ayla Croft --- coverage.tsv | 1 + 1 file changed, 1 insertion(+) diff --git a/coverage.tsv b/coverage.tsv index 465a8a1..7e2d7d3 100644 --- a/coverage.tsv +++ b/coverage.tsv @@ -22,3 +22,4 @@ slice_id percent sha date 060 80.32 49f40a4 2026-09-21 061 80.05 c6b6faa 2026-09-22 050 80.31 c01466d 2026-09-22 +002 80.31 d65c48c 2026-09-23