Skip to content

Commit 2848805

Browse files
committed
Add release SBOM artifact
1 parent e572edc commit 2848805

10 files changed

Lines changed: 335 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
- Added a release installer script that detects the local platform, verifies the selected artifact against `SHA256SUMS` and `RELEASE_MANIFEST.json`, and is smoke-tested by the release gate.
4343
- Added `openexit completion` plus release completion assets for Bash, Zsh, Fish, and PowerShell.
4444
- Extended release manifests and `SHA256SUMS` to cover installer and shell-completion assets as well as platform binaries.
45+
- Added `openexit sbom` and a verified release `SBOM.cdx.json` asset with CycloneDX JSON dependency metadata.
4546
- Added `make release-check` and wired the draft-release workflow to run the full release gate before packaging artifacts.
4647
- Updated push CI to run the same release readiness gate, including smoke pipelines and bundle verification.
4748
- Updated CI and release workflows to Node.js 24-native GitHub Actions.

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ LDFLAGS := -s -w -X github.com/RamazanKara/openexit/internal/version.Version=$(V
77
GOFILES := $(shell find . -name '*.go' -not -path './bin/*' -not -path './dist/*')
88
PLATFORMS ?= linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64
99
RELEASE_MANIFEST ?= RELEASE_MANIFEST.json
10-
RELEASE_ASSETS ?= install.sh openexit.bash _openexit openexit.fish openexit.ps1
10+
RELEASE_SBOM ?= SBOM.cdx.json
11+
RELEASE_ASSETS ?= install.sh openexit.bash _openexit openexit.fish openexit.ps1 $(RELEASE_SBOM)
1112
GOLANGCI_LINT_VERSION ?= v2.12.2
1213
GOLANGCI_LINT ?= bin/golangci-lint
1314
EXAMPLE_INPUT ?= examples/datadog-to-grafana/input/datadog-fixture.json
@@ -135,6 +136,7 @@ release-dist:
135136
$(GO) run -trimpath -ldflags "$(LDFLAGS)" ./cmd/openexit completion zsh > dist/_openexit
136137
$(GO) run -trimpath -ldflags "$(LDFLAGS)" ./cmd/openexit completion fish > dist/openexit.fish
137138
$(GO) run -trimpath -ldflags "$(LDFLAGS)" ./cmd/openexit completion powershell > dist/openexit.ps1
139+
$(GO) run -trimpath -ldflags "$(LDFLAGS)" ./cmd/openexit sbom --out dist/$(RELEASE_SBOM)
138140
$(GO) run -trimpath -ldflags "$(LDFLAGS)" ./cmd/openexit release-manifest --dist dist --out dist/$(RELEASE_MANIFEST) $(foreach target,$(PLATFORMS),--platform $(target)) $(foreach asset,$(RELEASE_ASSETS),--asset $(asset))
139141
cd dist && sha256sum openexit_* $(RELEASE_ASSETS) > SHA256SUMS
140142

@@ -152,6 +154,7 @@ release-check: verify release-dist install-smoke
152154
@test -s dist/_openexit
153155
@test -s dist/openexit.fish
154156
@test -s dist/openexit.ps1
157+
@test -s dist/$(RELEASE_SBOM)
155158
@expected=$$(printf '%s\n' $(PLATFORMS) $(RELEASE_ASSETS) | wc -w | tr -d ' '); \
156159
actual=$$(wc -l < dist/SHA256SUMS | tr -d ' '); \
157160
if [ "$$actual" != "$$expected" ]; then \

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Release candidates can be built locally with:
5151
make release-check VERSION=0.1.0
5252
```
5353

54-
This runs the release gate, writes OS/architecture binaries, writes `dist/SHA256SUMS`, writes `dist/RELEASE_MANIFEST.json`, and verifies the release artifacts.
54+
This runs the release gate, writes OS/architecture binaries, writes `dist/SHA256SUMS`, writes `dist/RELEASE_MANIFEST.json`, writes `dist/SBOM.cdx.json`, and verifies the release artifacts.
5555

5656
Refresh the checked-in Datadog example project with:
5757

@@ -89,6 +89,7 @@ make example VERSION=0.1.0-dev
8989
- `openexit release-manifest [--dist dist --out dist/RELEASE_MANIFEST.json]`
9090
- `openexit verify-release <manifest.json> [--dist dist] [--artifact <name>] [--require-checksums] [--json]`
9191
- `openexit completion [bash|zsh|fish|powershell]`
92+
- `openexit sbom [--out SBOM.cdx.json]`
9293
- `openexit assist summarize --project <project-dir> --provider noop`
9394

9495
The Datadog, GitHub, Okta, Auth0, Cloudflare, Akamai, OpenAI, and Anthropic collectors are read-only. API tokens are read from environment variables or local credential files, are not printed, and are not stored.
@@ -140,6 +141,7 @@ Included in the current implementation:
140141
- Offline release artifact verification for binaries and auxiliary assets against `RELEASE_MANIFEST.json` and optional `SHA256SUMS`.
141142
- Release installer script that selects the current platform binary and verifies it before installation.
142143
- Shell completion generation for Bash, Zsh, Fish, and PowerShell, including release-provided completion assets.
144+
- CycloneDX JSON SBOM generation for the OpenExit binary and Go module dependencies.
143145
- Evidence bundle path-safety checks that reject symlinks in exported project sections.
144146
- No-op assist provider and explicit opt-in LiteLLM assist.
145147
- GitHub Enterprise to Forgejo assessment path with fixture import and live repository inventory collection.

docs/cli.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ The checked-in Datadog example can be refreshed with `make example VERSION=0.1.0
4141

4242
`openexit completion bash|zsh|fish|powershell` prints shell completion scripts. Release artifacts also include `openexit.bash`, `_openexit`, `openexit.fish`, and `openexit.ps1` so package managers and manual installs can wire completion without rebuilding from source.
4343

44+
`openexit sbom --out SBOM.cdx.json` writes a CycloneDX JSON SBOM for the current OpenExit binary. It records the stamped OpenExit version, commit, build date, Go toolchain version, and Go module dependencies reported by the binary build info. Release builds include `SBOM.cdx.json` and cover it with both `RELEASE_MANIFEST.json` and `SHA256SUMS`.
45+
4446
Generate individual artifacts with `openexit generate --artifact <name>`. The primary Datadog path supports `mapping`, `assessment`, `risk-register`, `manual-review`, `cost-drivers`, `target-architecture`, `acceptance-criteria`, `rollback-plan`, `runbook`, `restore-drill-checklist`, `alert-shadowing-plan`, `migration-plan`, `grafana-dashboards`, `prometheus-rules`, `opentelemetry`, and `argocd`. The GitHub Enterprise path also supports `forgejo-migration-candidate`; the identity path also supports `realm-client-candidate`; the edge path also supports `vcl-candidates`, `haproxy-candidates`, and `coraza-rule-candidates`; the AI provider path also supports `litellm-config-candidate`.
4547

4648
The GitHub Enterprise to Forgejo fixture path uses local JSON metadata:

docs/release.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Release-blocking v0.1 requirements:
2323
- Machine-readable release manifest generation and offline release artifact verification for OS/architecture binaries, auxiliary release assets, and `SHA256SUMS`.
2424
- Verified release installer for Linux/macOS `amd64` and `arm64` downloads.
2525
- Shell completion generation and release completion assets for Bash, Zsh, Fish, and PowerShell.
26+
- CycloneDX JSON SBOM generation for release binaries and Go module dependencies.
2627
- No-op AI assist and optional external assist behind explicit opt-in.
2728
- Documentation, examples, CI, release draft workflow, and reproducible release artifacts.
2829

@@ -42,7 +43,7 @@ The AI provider path is complete for local fixture assessment workflows and incl
4243
- [ ] `make release-check VERSION=0.1.0` passes locally, including verification, CLI smoke pipelines, bundle verification, release artifact builds, installer smoke, and checksum count checks.
4344
- [ ] `make verify VERSION=0.1.0` passes, including CLI smoke pipelines.
4445
- [ ] `make lint` runs `gofmt`, `golangci-lint`, and `go vet`.
45-
- [ ] `make release-dist VERSION=0.1.0` produces binaries, `dist/SHA256SUMS`, `dist/RELEASE_MANIFEST.json`, `dist/install.sh`, and completion assets `openexit.bash`, `_openexit`, `openexit.fish`, and `openexit.ps1`.
46+
- [ ] `make release-dist VERSION=0.1.0` produces binaries, `dist/SHA256SUMS`, `dist/RELEASE_MANIFEST.json`, `dist/SBOM.cdx.json`, `dist/install.sh`, and completion assets `openexit.bash`, `_openexit`, `openexit.fish`, and `openexit.ps1`.
4647
- [ ] `openexit verify-release dist/RELEASE_MANIFEST.json --dist dist --require-checksums` passes and covers binaries, `install.sh`, and completion assets; it fails when a release artifact is tampered with.
4748
- [ ] `OPENEXIT_VERSION=0.1.0 OPENEXIT_BASE_URL=$PWD/dist BIN_DIR=$(mktemp -d)/bin sh scripts/install.sh` installs a verified local release binary and `openexit version` reports `0.1.0`.
4849
- [ ] `make example VERSION=0.1.0-dev` refreshes `examples/datadog-to-grafana/output/` and exports `examples/datadog-to-grafana/openexit-example.zip`.
@@ -55,6 +56,7 @@ The AI provider path is complete for local fixture assessment workflows and incl
5556
- [ ] `openexit run --project <demo> --export --out <zip>` completes a collected project through assessment, mapping, generation, validation, status reporting, and bundle export.
5657
- [ ] `openexit status --project <demo>` reports inventory, assessment, mapping, generated artifacts, validation status, export readiness, and matching `--json` output.
5758
- [ ] `openexit completion bash`, `zsh`, `fish`, and `powershell` each generate non-empty shell completion scripts.
59+
- [ ] `openexit sbom --out SBOM.cdx.json` generates valid CycloneDX JSON with OpenExit build metadata and Go module dependencies.
5860
- [ ] `README.md`, `docs/cli.md`, `docs/security.md`, and this checklist reflect current behavior.
5961
- [ ] `examples/datadog-to-grafana/README.md` reproduces the primary local demo.
6062
- [ ] `assessment/openexit.migration-plan.yaml`, `.json`, and `migration-plan.md` are generated by the demo pipeline and included in exported bundles.
@@ -73,7 +75,7 @@ The AI provider path is complete for local fixture assessment workflows and incl
7375
- [ ] Exported bundle README includes version, commit, build date, bundle timestamp, and candidate warning.
7476
- [ ] Exported bundle `manifest.json` includes build metadata, project source/target, validation totals, and per-file SHA-256 digests, and validates against `schemas/openexit.evidence-bundle.schema.json`.
7577
- [ ] `openexit verify-bundle <zip>` passes for exported bundles and fails when an archived file is tampered with.
76-
- [ ] Release manifest validates against `schemas/openexit.release-manifest.schema.json` and includes version, commit, build date, generation time, artifact type, binary OS/architecture metadata, sizes, and SHA-256 digests.
78+
- [ ] Release manifest validates against `schemas/openexit.release-manifest.schema.json` and includes version, commit, build date, generation time, artifact type, binary OS/architecture metadata, sizes, and SHA-256 digests, including `SBOM.cdx.json`.
7779
- [ ] Export refuses symlinks in exported project sections, including when `--force` is used.
7880
- [ ] No credentials, tokens, passwords, or private keys are present in fixtures, generated files, docs, or bundles.
7981
- [ ] Draft release notes have been reviewed.

docs/schemas.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ OpenExit schemas live under `schemas/` and mirror the typed Go manifests. Releas
44

55
Evidence bundle exports include `openexit-evidence/manifest.json`, which follows `schemas/openexit.evidence-bundle.schema.json`. The manifest records OpenExit build metadata, project source/target metadata, validation totals, and SHA-256 digests for exported project files so downstream review tooling can verify a bundle without parsing every human-readable report first.
66

7-
Release builds include `RELEASE_MANIFEST.json`, which follows `schemas/openexit.release-manifest.schema.json`. The manifest records OpenExit build metadata and every release artifact that should be covered by `SHA256SUMS`: platform binaries use `type: binary` with `os` and `arch`, while installer and shell-completion files use `type: asset`.
7+
Release builds include `RELEASE_MANIFEST.json`, which follows `schemas/openexit.release-manifest.schema.json`. The manifest records OpenExit build metadata and every release artifact that should be covered by `SHA256SUMS`: platform binaries use `type: binary` with `os` and `arch`, while installer, shell-completion, and SBOM files use `type: asset`.
88

99
Project manifests must use one of the supported source/target pairs: Datadog to Grafana LGTM, GitHub Enterprise to Forgejo, Okta/Auth0 to Keycloak/Zitadel, Cloudflare/Akamai to Varnish/HAProxy/Coraza, or OpenAI/Anthropic to vLLM/LiteLLM.
1010

internal/app/command.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/RamazanKara/openexit/internal/inventory"
2626
"github.com/RamazanKara/openexit/internal/mapping"
2727
openrelease "github.com/RamazanKara/openexit/internal/release"
28+
opensbom "github.com/RamazanKara/openexit/internal/sbom"
2829
"github.com/RamazanKara/openexit/internal/validate"
2930
"github.com/RamazanKara/openexit/internal/version"
3031
"github.com/spf13/cobra"
@@ -54,6 +55,7 @@ func NewRootCommand() *cobra.Command {
5455
root.AddCommand(newReleaseManifestCommand())
5556
root.AddCommand(newVerifyReleaseCommand())
5657
root.AddCommand(newCompletionCommand(root))
58+
root.AddCommand(newSBOMCommand())
5759
root.AddCommand(newAssistCommand())
5860
return root
5961
}
@@ -1059,6 +1061,38 @@ func newCompletionCommand(root *cobra.Command) *cobra.Command {
10591061
return cmd
10601062
}
10611063

1064+
func newSBOMCommand() *cobra.Command {
1065+
var out string
1066+
cmd := &cobra.Command{
1067+
Use: "sbom",
1068+
Short: "Generate a CycloneDX JSON SBOM for this OpenExit binary",
1069+
RunE: func(cmd *cobra.Command, args []string) error {
1070+
bom, err := opensbom.Generate(opensbom.Options{
1071+
Name: version.Name,
1072+
Version: version.Version,
1073+
Commit: version.Commit,
1074+
Date: version.Date,
1075+
})
1076+
if err != nil {
1077+
return err
1078+
}
1079+
if out == "" {
1080+
enc := json.NewEncoder(cmd.OutOrStdout())
1081+
enc.SetIndent("", " ")
1082+
return enc.Encode(bom)
1083+
}
1084+
if err := opensbom.Write(out, bom); err != nil {
1085+
return err
1086+
}
1087+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "sbom: %s\n", out)
1088+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "components: %d\n", len(bom.Components))
1089+
return nil
1090+
},
1091+
}
1092+
cmd.Flags().StringVar(&out, "out", "", "Write SBOM to file instead of stdout")
1093+
return cmd
1094+
}
1095+
10621096
func newAssistCommand() *cobra.Command {
10631097
root := &cobra.Command{
10641098
Use: "assist",

internal/app/e2e_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"testing"
1414

1515
openrelease "github.com/RamazanKara/openexit/internal/release"
16+
opensbom "github.com/RamazanKara/openexit/internal/sbom"
1617
publicschemas "github.com/RamazanKara/openexit/schemas"
1718
"github.com/santhosh-tekuri/jsonschema/v6"
1819
)
@@ -271,6 +272,36 @@ func TestCompletionCommandGeneratesShellScripts(t *testing.T) {
271272
}
272273
}
273274

275+
func TestSBOMCommandGeneratesCycloneDXJSON(t *testing.T) {
276+
out, err := executeForTestWithOutput("sbom")
277+
if err != nil {
278+
t.Fatalf("openexit sbom failed: %v\n%s", err, out)
279+
}
280+
var bom opensbom.BOM
281+
if err := json.Unmarshal([]byte(out), &bom); err != nil {
282+
t.Fatalf("decode SBOM JSON: %v\n%s", err, out)
283+
}
284+
if bom.BOMFormat != opensbom.BOMFormat || bom.SpecVersion != opensbom.SpecVersion || bom.Metadata.Component.Name != "openexit" {
285+
t.Fatalf("unexpected SBOM: %+v", bom)
286+
}
287+
288+
path := filepath.Join(t.TempDir(), "SBOM.cdx.json")
289+
out, err = executeForTestWithOutput("sbom", "--out", path)
290+
if err != nil {
291+
t.Fatalf("openexit sbom --out failed: %v\n%s", err, out)
292+
}
293+
if !strings.Contains(out, "components:") {
294+
t.Fatalf("expected SBOM summary, got:\n%s", out)
295+
}
296+
data, err := os.ReadFile(path)
297+
if err != nil {
298+
t.Fatal(err)
299+
}
300+
if err := json.Unmarshal(data, &bom); err != nil {
301+
t.Fatalf("decode written SBOM JSON: %v\n%s", err, string(data))
302+
}
303+
}
304+
274305
func TestStatusReportsPipelineReadiness(t *testing.T) {
275306
projectDir := filepath.Join(t.TempDir(), "demo")
276307
fixturePath := filepath.Join("..", "..", "testdata", "datadog", "small.json")

0 commit comments

Comments
 (0)