@@ -40,6 +40,29 @@ func (m *HarborCli) PublishImageAndSign(
4040 }
4141
4242 for _ , addr := range imageAddrs {
43+ // Generate SBOM (SPDX JSON) for the published image
44+ sbom := m .GenerateSBOM (
45+ ctx ,
46+ addr ,
47+ registryUsername ,
48+ registryPassword ,
49+ );
50+
51+ // Attest SBOM to the image using Cosign in-toto attestation
52+ if _ , err := m .AttestSBOM (
53+ ctx ,
54+ sbom ,
55+ addr ,
56+ registryUsername ,
57+ registryPassword ,
58+ githubToken ,
59+ actionsIdTokenRequestUrl ,
60+ actionsIdTokenRequestToken ,
61+ ); err != nil {
62+ return "" , fmt .Errorf ("failed to attest SBOM for image %s: %w" , addr , err )
63+ }
64+ fmt .Printf ("Attested SBOM for image: %s\n " , addr )
65+
4366 _ , err = m .Sign (
4467 ctx ,
4568 githubToken ,
@@ -264,3 +287,61 @@ func getVersion(tags []string) string {
264287 }
265288 return "latest"
266289}
290+
291+ // GenerateSBOM uses Syft to create an SPDX JSON SBOM for a given image and returns it as a file
292+ func (m * HarborCli ) GenerateSBOM (
293+ ctx context.Context ,
294+ imageAddr string ,
295+ registryUsername string ,
296+ registryPassword * dagger.Secret ,
297+ ) * dagger.File {
298+ syftCtr := dag .Container ().
299+ From ("anchore/syft:latest" ).
300+ WithSecretVariable ("SYFT_REGISTRY_AUTH_PASSWORD" , registryPassword ).
301+ WithEnvVariable ("SYFT_REGISTRY_AUTH_USERNAME" , registryUsername ).
302+ // Output SPDX JSON to a known path
303+ WithExec ([]string {"syft" , imageAddr , "-o" , "spdx-json=/sbom.spdx.json" })
304+
305+ return syftCtr .File ("/sbom.spdx.json" )
306+ }
307+
308+ // AttestSBOM attaches an in-toto attestation (SBOM predicate) to the image using Cosign
309+ func (m * HarborCli ) AttestSBOM (
310+ ctx context.Context ,
311+ sbomFile * dagger.File ,
312+ imageAddr string ,
313+ registryUsername string ,
314+ registryPassword * dagger.Secret ,
315+ // +optional
316+ githubToken * dagger.Secret ,
317+ // +optional
318+ actionsIdTokenRequestUrl * dagger.Secret ,
319+ // +optional
320+ actionsIdTokenRequestToken * dagger.Secret ,
321+ ) (string , error ) {
322+ cosignCtr := dag .Container ().
323+ From ("cgr.dev/chainguard/cosign" ).
324+ WithMountedFile ("/sbom.spdx.json" , sbomFile ).
325+ WithSecretVariable ("REGISTRY_PASSWORD" , registryPassword )
326+
327+ // If githubToken is provided, configure OIDC for keyless signing
328+ if githubToken != nil {
329+ if actionsIdTokenRequestUrl == nil || actionsIdTokenRequestToken == nil {
330+ return "" , fmt .Errorf ("actionsIdTokenRequestUrl (exist=%v) and actionsIdTokenRequestToken (exist=%t) must be provided when githubToken is provided" , actionsIdTokenRequestUrl != nil , actionsIdTokenRequestToken != nil )
331+ }
332+ cosignCtr = cosignCtr .
333+ WithSecretVariable ("GITHUB_TOKEN" , githubToken ).
334+ WithSecretVariable ("ACTIONS_ID_TOKEN_REQUEST_URL" , actionsIdTokenRequestUrl ).
335+ WithSecretVariable ("ACTIONS_ID_TOKEN_REQUEST_TOKEN" , actionsIdTokenRequestToken )
336+ }
337+
338+ // Use cosign attest to create in-toto attestation with SPDX JSON predicate
339+ // The registry password is already available as REGISTRY_PASSWORD env var
340+ return cosignCtr .WithExec ([]string {
341+ "sh" , "-c" ,
342+ fmt .Sprintf (
343+ "cosign attest --yes --type spdxjson --predicate /sbom.spdx.json --registry-username %s --registry-password $REGISTRY_PASSWORD %s --timeout 1m" ,
344+ registryUsername , imageAddr ,
345+ ),
346+ }).Stdout (ctx )
347+ }
0 commit comments