Skip to content

Commit d035a38

Browse files
committed
feat: read manifest for paths and create/upload sbom
1 parent 2e97fa8 commit d035a38

7 files changed

Lines changed: 315 additions & 32 deletions

File tree

.github/workflows/ami-release-nix.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,36 @@ jobs:
206206
207207
echo "Catalog uploaded to ${CATALOG_S3}"
208208
209+
- name: Assume SBOM artifacts role
210+
uses: aws-actions/configure-aws-credentials@v4.1.0
211+
with:
212+
aws-region: ap-southeast-1
213+
role-to-assume: arn:aws:iam::279559813984:role/supabase-github-oidc-role
214+
role-session-name: shared-services-jump
215+
216+
- name: Upload comprehensive SBOM to shared artifacts
217+
uses: aws-actions/configure-aws-credentials@v4.1.0
218+
with:
219+
aws-region: ap-southeast-1
220+
role-to-assume: arn:aws:iam::279559813984:role/supabase-sbom-artifacts-role
221+
role-skip-session-tagging: true
222+
role-session-name: upload-sbom
223+
role-chaining: true
224+
225+
- name: Upload SBOM
226+
run: |
227+
VERSION="${{ steps.process_release_version.outputs.version }}"
228+
229+
# Check if comprehensive SBOM exists (generated during AMI build)
230+
if [ -f "nix-sbom.spdx.json" ]; then
231+
aws s3 cp nix-sbom.spdx.json \
232+
"s3://${{ secrets.SHARED_AWS_ARTIFACTS_BUCKET }}/sbom/${VERSION}/sbom.spdx.json" \
233+
--content-type "application/json"
234+
echo "::notice title=SBOM Uploaded::Comprehensive SBOM for ${VERSION} uploaded to shared artifacts"
235+
else
236+
echo "::warning title=SBOM Missing::Comprehensive SBOM file not found, skipping upload"
237+
fi
238+
209239
- name: Create release
210240
uses: softprops/action-gh-release@v2
211241
with:

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,8 @@ common-nix.vars.pkr.hcl
3333
nixos.qcow2
3434
.lsp
3535
.clj-kondo
36+
#sbom development files
37+
http_cache.sqlite
38+
nix-sbom.spdx.json
39+
sbom.cdx.json
40+
sbom.csv

nix/packages/default.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
sbom-ubuntu
4343
sbom-nix
4444
sbom-generator
45+
collect-nix-paths
4546
sbomnix
4647
;
4748

nix/packages/sbom/cmd/sbom/main.go

Lines changed: 74 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,26 @@ import (
55
"fmt"
66
"log"
77
"os"
8+
"strings"
89

910
"github.com/supabase/postgres/nix/packages/sbom/internal/merge"
1011
"github.com/supabase/postgres/nix/packages/sbom/internal/nix"
12+
"github.com/supabase/postgres/nix/packages/sbom/internal/spdx"
1113
"github.com/supabase/postgres/nix/packages/sbom/internal/ubuntu"
1214
)
1315

16+
// stringSliceFlag allows multiple values for a single flag
17+
type stringSliceFlag []string
18+
19+
func (s *stringSliceFlag) String() string {
20+
return strings.Join(*s, ", ")
21+
}
22+
23+
func (s *stringSliceFlag) Set(value string) error {
24+
*s = append(*s, value)
25+
return nil
26+
}
27+
1428
func main() {
1529
if len(os.Args) < 2 {
1630
printUsage()
@@ -91,12 +105,12 @@ func nixCommand(args []string) {
91105
outputFile := fs.String("output", "nix-sbom.spdx.json", "Output file path")
92106

93107
fs.Usage = func() {
94-
fmt.Println("Usage: sbom nix <derivation-path> [flags]")
108+
fmt.Println("Usage: sbom nix <derivation-path> [derivation-path...] [flags]")
95109
fmt.Println()
96110
fmt.Println("Generate Nix-only SBOM using sbomnix")
97111
fmt.Println()
98112
fmt.Println("Arguments:")
99-
fmt.Println(" derivation-path Path to the Nix derivation (required)")
113+
fmt.Println(" derivation-path Path(s) to Nix derivation(s) (at least one required)")
100114
fmt.Println()
101115
fmt.Println("Flags:")
102116
fs.PrintDefaults()
@@ -107,34 +121,43 @@ func nixCommand(args []string) {
107121
}
108122

109123
if fs.NArg() < 1 {
110-
fmt.Println("Error: derivation path required")
124+
fmt.Println("Error: at least one derivation path required")
111125
fmt.Println()
112126
fs.Usage()
113127
os.Exit(1)
114128
}
115129

116-
derivationPath := fs.Arg(0)
117-
118130
// Use sbomnix from PATH
119131
wrapper := nix.NewWrapper("sbomnix")
120132

121-
if err := wrapper.Generate(derivationPath, *outputFile); err != nil {
122-
log.Fatalf("Failed to generate Nix SBOM: %v", err)
133+
if fs.NArg() == 1 {
134+
// Single path - use original method
135+
if err := wrapper.Generate(fs.Arg(0), *outputFile); err != nil {
136+
log.Fatalf("Failed to generate Nix SBOM: %v", err)
137+
}
138+
} else {
139+
// Multiple paths - use new method
140+
paths := fs.Args()
141+
if err := wrapper.GenerateMultiple(paths, *outputFile); err != nil {
142+
log.Fatalf("Failed to generate Nix SBOM: %v", err)
143+
}
123144
}
124145

125146
fmt.Printf("Nix SBOM generated successfully: %s\n", *outputFile)
126147
}
127148

128149
func combinedCommand(args []string) {
129150
fs := flag.NewFlagSet("combined", flag.ExitOnError)
130-
nixTarget := fs.String("nix-target", "", "Path to Nix derivation (required)")
151+
var nixTargets stringSliceFlag
152+
fs.Var(&nixTargets, "nix-target", "Path to Nix derivation (can be specified multiple times)")
131153
outputFile := fs.String("output", "merged-sbom.spdx.json", "Output file path")
132154
includeFiles := fs.Bool("include-files", false, "Include file checksums for Ubuntu packages")
133155
progress := fs.Bool("progress", true, "Show progress indicators")
134156
noProgress := fs.Bool("no-progress", false, "Disable progress indicators")
157+
nixOnly := fs.Bool("nix-only", false, "Generate Nix-only SBOM (skip Ubuntu packages)")
135158

136159
fs.Usage = func() {
137-
fmt.Println("Usage: sbom combined --nix-target <derivation> [flags]")
160+
fmt.Println("Usage: sbom combined --nix-target <derivation> [--nix-target <derivation>...] [flags]")
138161
fmt.Println()
139162
fmt.Println("Generate and merge both Ubuntu and Nix SBOMs")
140163
fmt.Println()
@@ -146,8 +169,8 @@ func combinedCommand(args []string) {
146169
os.Exit(1)
147170
}
148171

149-
if *nixTarget == "" {
150-
fmt.Println("Error: --nix-target is required")
172+
if len(nixTargets) == 0 {
173+
fmt.Println("Error: at least one --nix-target is required")
151174
fmt.Println()
152175
fs.Usage()
153176
os.Exit(1)
@@ -162,33 +185,52 @@ func combinedCommand(args []string) {
162185
}
163186
defer os.RemoveAll(tmpDir)
164187

165-
ubuntuSBOM := fmt.Sprintf("%s/ubuntu-sbom.spdx.json", tmpDir)
166-
nixSBOM := fmt.Sprintf("%s/nix-sbom.spdx.json", tmpDir)
188+
merger := merge.NewMerger()
189+
nixWrapper := nix.NewWrapper("sbomnix")
167190

168-
// Generate Ubuntu SBOM
169-
fmt.Println("Generating Ubuntu SBOM...")
170-
ubuntuGen := ubuntu.NewGenerator(*includeFiles, showProgress)
171-
ubuntuDoc, err := ubuntuGen.Generate()
172-
if err != nil {
173-
log.Fatalf("Failed to generate Ubuntu SBOM: %v", err)
174-
}
175-
if err := ubuntuGen.Save(ubuntuDoc, ubuntuSBOM); err != nil {
176-
log.Fatalf("Failed to save Ubuntu SBOM: %v", err)
177-
}
191+
var ubuntuSBOM string
192+
if !*nixOnly {
193+
// Generate Ubuntu SBOM
194+
fmt.Println("Generating Ubuntu SBOM...")
195+
ubuntuSBOM = fmt.Sprintf("%s/ubuntu-sbom.spdx.json", tmpDir)
196+
ubuntuGen := ubuntu.NewGenerator(*includeFiles, showProgress)
197+
ubuntuDoc, err := ubuntuGen.Generate()
198+
if err != nil {
199+
log.Fatalf("Failed to generate Ubuntu SBOM: %v", err)
200+
}
201+
if err := ubuntuGen.Save(ubuntuDoc, ubuntuSBOM); err != nil {
202+
log.Fatalf("Failed to save Ubuntu SBOM: %v", err)
203+
}
204+
}
205+
206+
// Generate Nix SBOM(s)
207+
fmt.Printf("Generating Nix SBOM from %d target(s)...\n", len(nixTargets))
208+
nixSBOM := fmt.Sprintf("%s/nix-sbom.spdx.json", tmpDir)
178209

179-
// Generate Nix SBOM
180-
fmt.Println("Generating Nix SBOM...")
181-
nixWrapper := nix.NewWrapper("sbomnix")
182-
if err := nixWrapper.Generate(*nixTarget, nixSBOM); err != nil {
183-
log.Fatalf("Failed to generate Nix SBOM: %v", err)
210+
if len(nixTargets) == 1 {
211+
if err := nixWrapper.Generate(nixTargets[0], nixSBOM); err != nil {
212+
log.Fatalf("Failed to generate Nix SBOM: %v", err)
213+
}
214+
} else {
215+
if err := nixWrapper.GenerateMultiple([]string(nixTargets), nixSBOM); err != nil {
216+
log.Fatalf("Failed to generate Nix SBOM: %v", err)
217+
}
184218
}
185219

186220
// Merge SBOMs
187221
fmt.Println("Merging SBOMs...")
188-
merger := merge.NewMerger()
189-
mergedDoc, err := merger.Merge(ubuntuSBOM, nixSBOM)
190-
if err != nil {
191-
log.Fatalf("Failed to merge SBOMs: %v", err)
222+
var mergedDoc *spdx.Document
223+
if *nixOnly {
224+
// Load and output Nix SBOM directly
225+
mergedDoc, err = merger.LoadDocument(nixSBOM)
226+
if err != nil {
227+
log.Fatalf("Failed to load Nix SBOM: %v", err)
228+
}
229+
} else {
230+
mergedDoc, err = merger.Merge(ubuntuSBOM, nixSBOM)
231+
if err != nil {
232+
log.Fatalf("Failed to merge SBOMs: %v", err)
233+
}
192234
}
193235

194236
if err := merger.Save(mergedDoc, *outputFile); err != nil {

nix/packages/sbom/default.nix

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,26 @@ let
3131
export PATH="${sbomnix}/bin:$PATH"
3232
${sbom}/bin/sbom combined "$@"
3333
'';
34+
35+
# Script to collect nix store paths from all user profile manifests
36+
collect-nix-paths = pkgs.writeShellScriptBin "collect-nix-paths" ''
37+
set -euo pipefail
38+
USERS="adminapi envoy gotrue kong nginx pgbouncer postgres postgrest supabase-admin-agent ubuntu wal-g"
39+
for user in $USERS; do
40+
manifest="/home/$user/.nix-profile/manifest.json"
41+
if [ -f "$manifest" ]; then
42+
${pkgs.jq}/bin/jq -r '.elements | to_entries[].value.storePaths[]' "$manifest" 2>/dev/null || true
43+
fi
44+
done | sort -u | grep -v '^$'
45+
'';
3446
in
3547
{
3648
inherit
3749
sbom
3850
sbom-ubuntu
3951
sbom-nix
4052
sbom-generator
53+
collect-nix-paths
4154
sbomnix
4255
;
4356
}

nix/packages/sbom/internal/merge/merger.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,130 @@ func (m *Merger) Merge(ubuntuPath, nixPath string) (*spdx.Document, error) {
127127
return mergedDoc, nil
128128
}
129129

130+
// MergeMultipleNix merges multiple Nix SPDX documents into a single document
131+
func (m *Merger) MergeMultipleNix(nixPaths []string) (*spdx.Document, error) {
132+
if len(nixPaths) == 0 {
133+
return nil, fmt.Errorf("no SPDX files provided")
134+
}
135+
136+
if len(nixPaths) == 1 {
137+
return m.LoadDocument(nixPaths[0])
138+
}
139+
140+
uuid, err := spdx.GenerateUUID()
141+
if err != nil {
142+
return nil, fmt.Errorf("failed to generate UUID: %w", err)
143+
}
144+
145+
// Create merged document
146+
mergedDoc := &spdx.Document{
147+
SPDXVersion: "SPDX-2.3",
148+
DataLicense: "CC0-1.0",
149+
SPDXID: "SPDXRef-DOCUMENT",
150+
Name: fmt.Sprintf("Nix-System-SBOM-%s", time.Now().Format("2006-01-02")),
151+
DocumentNamespace: fmt.Sprintf("https://sbom.nix.system/%s", uuid),
152+
CreationInfo: spdx.CreationInfo{
153+
Created: time.Now().UTC().Format(time.RFC3339),
154+
Creators: []string{"Tool: nix-sbom-merger-1.0"},
155+
LicenseListVersion: "3.20",
156+
},
157+
Packages: []spdx.Package{},
158+
Relationships: []spdx.Relationship{},
159+
}
160+
161+
// Create the single root System package
162+
systemPkg := spdx.Package{
163+
SPDXID: "SPDXRef-System",
164+
Name: "Nix-System",
165+
DownloadLocation: "NOASSERTION",
166+
FilesAnalyzed: false,
167+
LicenseConcluded: "NOASSERTION",
168+
LicenseDeclared: "NOASSERTION",
169+
CopyrightText: "NOASSERTION",
170+
Description: "Combined Nix package system from multiple profiles",
171+
}
172+
mergedDoc.Packages = append(mergedDoc.Packages, systemPkg)
173+
174+
// Add document describes relationship
175+
mergedDoc.Relationships = append(mergedDoc.Relationships, spdx.Relationship{
176+
SPDXElementID: "SPDXRef-DOCUMENT",
177+
RelatedSPDXElement: "SPDXRef-System",
178+
RelationshipType: "DESCRIBES",
179+
})
180+
181+
// Track seen packages by SPDXID to deduplicate
182+
seenPackages := make(map[string]bool)
183+
totalCount := 0
184+
185+
for i, nixPath := range nixPaths {
186+
nixDoc, err := m.LoadDocument(nixPath)
187+
if err != nil {
188+
return nil, fmt.Errorf("failed to load Nix SBOM %s: %w", nixPath, err)
189+
}
190+
191+
// Collect creators
192+
for _, creator := range nixDoc.CreationInfo.Creators {
193+
found := false
194+
for _, existing := range mergedDoc.CreationInfo.Creators {
195+
if existing == creator {
196+
found = true
197+
break
198+
}
199+
}
200+
if !found {
201+
mergedDoc.CreationInfo.Creators = append(mergedDoc.CreationInfo.Creators, creator)
202+
}
203+
}
204+
205+
// Process Nix packages
206+
for _, pkg := range nixDoc.Packages {
207+
// Skip root/system packages
208+
if strings.Contains(strings.ToLower(pkg.Name), "system") &&
209+
(pkg.SPDXID == "SPDXRef-DOCUMENT" || strings.HasSuffix(pkg.SPDXID, "-System")) {
210+
continue
211+
}
212+
213+
// Ensure SPDXID has Nix prefix with source index to avoid conflicts
214+
originalID := pkg.SPDXID
215+
if !strings.HasPrefix(pkg.SPDXID, "SPDXRef-Nix-") {
216+
pkg.SPDXID = m.renumberSPDXID(pkg.SPDXID, fmt.Sprintf("Nix%d", i))
217+
}
218+
219+
// Skip if we've already seen this package (by name+version for deduplication)
220+
pkgKey := fmt.Sprintf("%s-%s", pkg.Name, pkg.PackageVersion)
221+
if seenPackages[pkgKey] {
222+
continue
223+
}
224+
seenPackages[pkgKey] = true
225+
226+
// Clean up invalid CPE references from sbomnix
227+
pkg.ExternalRefs = m.cleanExternalRefs(pkg.ExternalRefs)
228+
229+
mergedDoc.Packages = append(mergedDoc.Packages, pkg)
230+
231+
// Add relationship to system root
232+
mergedDoc.Relationships = append(mergedDoc.Relationships, spdx.Relationship{
233+
SPDXElementID: "SPDXRef-System",
234+
RelatedSPDXElement: pkg.SPDXID,
235+
RelationshipType: "CONTAINS",
236+
})
237+
totalCount++
238+
239+
_ = originalID // suppress unused warning
240+
}
241+
}
242+
243+
fmt.Printf("Merged %d unique Nix packages from %d sources\n", totalCount, len(nixPaths))
244+
245+
return mergedDoc, nil
246+
}
247+
130248
func (m *Merger) loadDocument(path string) (*spdx.Document, error) {
249+
return m.LoadDocument(path)
250+
}
251+
252+
// LoadDocument loads an SPDX document from a file
253+
func (m *Merger) LoadDocument(path string) (*spdx.Document, error) {
131254
data, err := os.ReadFile(path)
132255
if err != nil {
133256
return nil, err

0 commit comments

Comments
 (0)