Skip to content

Commit 1df333f

Browse files
bougymanclaude
andcommitted
feat(ci): add a full SBOM - app deps, OTP/Elixir runtime, container OS packages
mix.lock has exact versions + checksums for the Hex dependency tree, but it's not a real SBOM - wrong format (Erlang-term syntax, not SPDX/ CycloneDX), no license data, and doesn't cover the OTP/ERTS runtime Burrito bundles into each binary or the Alpine packages baked into the container image. Two real SBOMs, kept as separate files since a container SBOM has no use to anyone not using the container: - App + Hex deps + Erlang/OTP + Elixir runtime, via erlef/mix_sbom (verified: includes the OTP standard library apps - kernel/stdlib/ crypto/ssl/etc - and Elixir at the exact pinned version by default, no manual merge needed). Generated in the burrito job, attached to the release as sbom.cdx.json in the same atomic `gh release create` call that creates the release. - Container OS packages (Alpine/apk), via aquasecurity/trivy-action (--format cyclonedx). Generated in the container job, which runs after the release is already published (and thus immutable, #18) - published as a workflow artifact, not a release asset, since it genuinely can't be attached to the already-locked release. Closes #53. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 2155977 commit 1df333f

4 files changed

Lines changed: 54 additions & 2 deletions

File tree

.github/workflows/main.yaml

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,22 @@ jobs:
154154
run: |
155155
version=$(ruby -rjson -e "print JSON.parse(File.read('../.release-please-manifest.json'))['.']")
156156
printf 'tag_name=v%s\n' "$version" >> "$GITHUB_OUTPUT"
157+
-
158+
# Covers the app + Hex deps + the actual Erlang/OTP and Elixir
159+
# versions this release was built with (mix_sbom includes those by
160+
# default - verified they show up as individual OTP application
161+
# components like kernel/stdlib/crypto/ssl, not one umbrella
162+
# "erlang" entry). Not the container image's own OS packages -
163+
# that's a separate SBOM, generated in the container job below,
164+
# since it's meaningless to anyone not using the container.
165+
name: Generate the app SBOM
166+
uses: erlef/mix_sbom@v0
167+
id: sbom
168+
with:
169+
project-path: ${{ github.workspace }}/app
170+
reuse-beam: true
171+
schema: "1.6"
172+
format: "json"
157173
-
158174
# One atomic command creates the tag, the release, and uploads
159175
# every asset together - no separate release object sits around
@@ -163,7 +179,7 @@ jobs:
163179
name: Create the release with every asset attached
164180
env:
165181
GH_TOKEN: ${{ github.token }}
166-
run: gh release create "${{ steps.version.outputs.tag_name }}" burrito_out/*.tar.gz burrito_out/SHA256SUMS --generate-notes
182+
run: gh release create "${{ steps.version.outputs.tag_name }}" burrito_out/*.tar.gz burrito_out/SHA256SUMS "${{ steps.sbom.outputs.sbom-path }}" --generate-notes
167183
working-directory: app
168184
-
169185
# release-please labels its own release PR "autorelease: pending"
@@ -215,9 +231,34 @@ jobs:
215231
env:
216232
APP_VERSION: ${{ needs.burrito.outputs.tag_name }}
217233
run: ./ci/build_image.sh "${{ needs.burrito.outputs.tag_name }}"
234+
-
235+
# Covers the container's own OS packages (Alpine/apk - ca-certificates,
236+
# bash) - meaningless to anyone not using the container, which is
237+
# exactly why it's a separate file from the app SBOM in the burrito
238+
# job above, not merged into it. Scans the local image ci/build_image.sh
239+
# just built (tagged <repo-basename>:<tag_name> - no registry prefix),
240+
# before publishing, though nothing here gates the publish step on it.
241+
name: Generate the container SBOM
242+
uses: aquasecurity/trivy-action@v0.36.0
243+
with:
244+
image-ref: "linear-cli-ex:${{ needs.burrito.outputs.tag_name }}"
245+
scan-type: image
246+
format: cyclonedx
247+
output: container-sbom.cdx.json
218248
-
219249
name: Publish the image
220250
env:
221251
GITHUB_TOKEN: ${{ github.token }}
222252
GITHUB_ACTOR: ${{ github.actor }}
223253
run: ./ci/publish.sh "${{ needs.burrito.outputs.tag_name }}"
254+
-
255+
# Can't attach this to the GitHub release the burrito job already
256+
# created - it's published (and thus immutable, see #18) by the time
257+
# this job runs. A workflow artifact is the honest option here, not
258+
# a release asset pretending to be one.
259+
name: Upload the container SBOM as a workflow artifact
260+
uses: actions/upload-artifact@v7
261+
with:
262+
name: container-sbom
263+
path: container-sbom.cdx.json
264+
retention-days: 90

Readme.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ $ sudo mv lc/* /usr/local/bin/
6666
----
6767
<1> Linux: `sha256sum -c -` instead of `shasum -a 256 -c -`.
6868

69+
Every release also publishes `sbom.cdx.json` - a CycloneDX SBOM covering the
70+
app, its Hex dependencies, and the Erlang/OTP and Elixir versions it was
71+
built with. The container image has its own separate SBOM (its OS packages -
72+
irrelevant if you're not using the container), published as a workflow
73+
artifact on the release's build rather than a release asset.
74+
6975
NOTE: A Homebrew tap is planned but not yet available.
7076

7177
On macOS, Gatekeeper blocks `lc` itself (the wrapper scripts are plain shell,

app/mix.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ defmodule LinearCli.MixProject do
7373
{:burrito, "~> 1.6"},
7474
{:sourceror, "~> 1.8", only: [:dev, :test]},
7575
{:ash, "~> 3.0"},
76-
{:igniter, "~> 0.6", only: [:dev, :test]}
76+
{:igniter, "~> 0.6", only: [:dev, :test]},
77+
{:sbom, "~> 0.8", only: :dev, runtime: false}
7778
# {:dep_from_hexpm, "~> 0.3.0"},
7879
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
7980
]

app/mix.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"exqlite": {:hex, :exqlite, "0.39.0", "08f8ff646e14d14bab289f4cfc61b95ffc5fe7c82d420049a480eee69ca8b292", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "603de0f7637adc88275fa12ccbd58954ff6000f75386e876565b49032d9aede9"},
1515
"finch": {:hex, :finch, "0.23.0", "e3f9287ac25a8832f848b144c2b57346aac65b205e2e0629a52adfe6507fd837", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80e58d3f936f57e3fdf404f83a3642897ae6d9fb642934e46da4d8fe761b99d5"},
1616
"glob_ex": {:hex, :glob_ex, "0.1.12", "7b2d9369c20e2697efcfd185d13d6e84c94cd3bfd2730fbde613141c2e015c00", [:mix], [], "hexpm", "2e2fac83f113514434c7eaf267b4c38af2f91766f1cab2c5db7053b7fc1ee0bb"},
17+
"hex_core": {:hex, :hex_core, "0.15.0", "8eadc0ccb08e3742f2313073d04f39eaa7904617329039e9d3c402f5dd227673", [:rebar3], [], "hexpm", "c2093764c7af8ef0818c104fa141eba431e7be93f8374638c45c7037b26a52f8"},
1718
"hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"},
1819
"igniter": {:hex, :igniter, "0.8.3", "9de74d3885efae43b0b58dc6f7b816963c4bbd391e6b6fe6922ee21c4e384c76", [: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", "afc5e3848d885e680da5c3b65e5e7717555a08cd12305190ff2be76427af39ff"},
1920
"iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"},
@@ -33,10 +34,13 @@
3334
"plug": {:hex, :plug, "1.20.3", "56c480c633ec2ce10140e236e15233bf576e1d323887d7c96711bd02ab5160db", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "be266aee1b8536ef6409d58cf39a3121319f0ec47cfa1b24024485aa0e76ad76"},
3435
"plug_crypto": {:hex, :plug_crypto, "2.2.0", "144014737daaf485407f5ed77daeaad74d651b216a28c87543f8cc7043f8efc8", [:mix], [], "hexpm", "83a95744ab1c75876542b6fab135fcc176280e0f301a111c1f757fddcec95d2c"},
3536
"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"},
37+
"protobuf": {:hex, :protobuf, "0.16.1", "7bf0c3e6b24fd297e2e3722ecb985c3900bcc55d004b7faf0737e248c3e64691", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "97fb9f1fe48c38c4ebfc0f8a7a20adb22b2491bbfbdb192fab9a6821d5d604e1"},
38+
"purl": {:hex, :purl, "0.3.0", "b6e2f792e944007ed474299e56ea43ea4819f064a4a4f67e4153e1ba02d066de", [:mix, :rebar3], [{:stream_data, "~> 1.1", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "158cf2c0533517fe0d68752bd790392f065c5ffdfbde38524890066ebe640302"},
3639
"reactor": {:hex, :reactor, "1.0.4", "e19b3bc00776935282e5e0fc1b0dcb87682ec6f3c238ce3150fe7f50bbde502a", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:multigraph, "~> 0.16.1-mg.2", [hex: :multigraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "ba5ec7c9ddc7781f65b6e9818456c4c6b4e997a9a926d83b6f3f12192ef5b395"},
3740
"req": {:hex, :req, "0.7.2", "364eae2e5f5c984f2dac6d71c07f8c8c89ce0bc49c4d746dacb7a306823020de", [: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", "c9cdfa276b05d8db2a27fda5d233e6858b764d47189d76cbb186e130a871ae0b"},
3841
"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"},
3942
"rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"},
43+
"sbom": {:hex, :sbom, "0.10.0", "b99be5407bc196d0ad71b8061126a67aae46dc3bfaa852b4c1c04645dd1ad984", [:mix], [{:hex_core, "~> 0.15.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.16.0", [hex: :protobuf, repo: "hexpm", optional: false]}, {:purl, "~> 0.3.0", [hex: :purl, repo: "hexpm", optional: false]}], "hexpm", "a8116ef965c1ebd103e223545794bd0a6691edd3ec678ec07972d473e2badc95"},
4044
"sourceror": {:hex, :sourceror, "1.12.2", "85bfd48159f020c0cbfc72f289f11456fdc05dc43719b6f2589fb969faefa113", [:mix], [], "hexpm", "da37d3da09c5b890528802c7056a8f585a061973820d7656b6e3649c14f0e9cb"},
4145
"spark": {:hex, :spark, "2.7.2", "36becc6ff03b40908cc821d403d7f06d893498e293d2f718afc6ca097fcb9d93", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "adb323ddbf9dbbe326f9e5def54ac96c47911e852b2c270bb19a5147c56f1b45"},
4246
"spitfire": {:hex, :spitfire, "0.4.0", "6d98c10cf585434b9439ba0c6dd3cc7aeff0e06ab73bfe5488f42e7c0f883d9b", [:mix], [], "hexpm", "7e5c6d1523c111b59f332f9dc49edc0377111d0c17167a29830f0e98233f5472"},

0 commit comments

Comments
 (0)