@@ -230,6 +230,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
230230 testSBOMScan ,
231231 testSBOMScanSingleRef ,
232232 testSBOMSupplements ,
233+ testSBOMCycloneDX ,
233234 testMultipleCacheExports ,
234235 testMountStubsDirectory ,
235236 testMountStubsTimestamp ,
@@ -12184,6 +12185,246 @@ func testSBOMSupplements(t *testing.T, sb integration.Sandbox) {
1218412185 checkAllReleasable (t , c , sb , true )
1218512186}
1218612187
12188+ func testSBOMCycloneDX (t * testing.T , sb integration.Sandbox ) {
12189+ workers .CheckFeatureCompat (t , sb , workers .FeatureDirectPush , workers .FeatureSBOM )
12190+ requiresLinux (t )
12191+ c , err := New (sb .Context (), sb .Address ())
12192+ require .NoError (t , err )
12193+ defer c .Close ()
12194+
12195+ registry , err := sb .NewRegistry ()
12196+ if errors .Is (err , integration .ErrRequirements ) {
12197+ t .Skip (err .Error ())
12198+ }
12199+ require .NoError (t , err )
12200+
12201+ p := platforms .MustParse (integration .UnixOrWindows ("linux/amd64" , "windows/amd64" ))
12202+ pk := platforms .Format (p )
12203+
12204+ // Build a CycloneDX scanner image
12205+ scannerFrontend := func (ctx context.Context , c gateway.Client ) (* gateway.Result , error ) {
12206+ res := gateway .NewResult ()
12207+
12208+ st := llb .Image ("busybox" )
12209+ def , err := st .Marshal (sb .Context ())
12210+ require .NoError (t , err )
12211+
12212+ r , err := c .Solve (ctx , gateway.SolveRequest {
12213+ Definition : def .ToPB (),
12214+ })
12215+ if err != nil {
12216+ return nil , err
12217+ }
12218+ ref , err := r .SingleRef ()
12219+ if err != nil {
12220+ return nil , err
12221+ }
12222+ _ , err = ref .ToState ()
12223+ if err != nil {
12224+ return nil , err
12225+ }
12226+ res .AddRef (pk , ref )
12227+
12228+ expPlatforms := & exptypes.Platforms {
12229+ Platforms : []exptypes.Platform {{ID : pk , Platform : p }},
12230+ }
12231+ dt , err := json .Marshal (expPlatforms )
12232+ if err != nil {
12233+ return nil , err
12234+ }
12235+ res .AddMeta (exptypes .ExporterPlatformsKey , dt )
12236+
12237+ var img ocispecs.Image
12238+ cmd := `
12239+ cat <<EOF > $BUILDKIT_SCAN_DESTINATION/cdx.json
12240+ {
12241+ "_type": "https://in-toto.io/Statement/v0.1",
12242+ "predicateType": "https://cyclonedx.org/bom",
12243+ "predicate": {
12244+ "bomFormat": "CycloneDX",
12245+ "specVersion": "1.4"
12246+ }
12247+ }
12248+ EOF
12249+ `
12250+ img .Config .Cmd = []string {"/bin/sh" , "-c" , cmd }
12251+ img .Platform = p
12252+ config , err := json .Marshal (img )
12253+ if err != nil {
12254+ return nil , errors .Wrapf (err , "failed to marshal image config" )
12255+ }
12256+ res .AddMeta (fmt .Sprintf ("%s/%s" , exptypes .ExporterImageConfigKey , pk ), config )
12257+
12258+ return res , nil
12259+ }
12260+
12261+ scannerTarget := registry + "/buildkit/testcdxscanner:latest"
12262+ _ , err = c .Build (sb .Context (), SolveOpt {
12263+ Exports : []ExportEntry {
12264+ {
12265+ Type : ExporterImage ,
12266+ Attrs : map [string ]string {
12267+ "name" : scannerTarget ,
12268+ "push" : "true" ,
12269+ },
12270+ },
12271+ },
12272+ }, "" , scannerFrontend , nil )
12273+ require .NoError (t , err )
12274+
12275+ // makeBaseResult builds a scratch image with a greeting file and platform metadata,
12276+ // shared by the plain target and the CDX-annotated frontend.
12277+ makeBaseResult := func (ctx context.Context , c gateway.Client ) (* gateway.Result , error ) {
12278+ res := gateway .NewResult ()
12279+
12280+ st := llb .Scratch ().File (
12281+ llb .Mkfile ("/greeting" , 0600 , []byte ("hello cyclonedx!" )),
12282+ )
12283+ def , err := st .Marshal (ctx )
12284+ if err != nil {
12285+ return nil , err
12286+ }
12287+ r , err := c .Solve (ctx , gateway.SolveRequest {
12288+ Definition : def .ToPB (),
12289+ })
12290+ if err != nil {
12291+ return nil , err
12292+ }
12293+ ref , err := r .SingleRef ()
12294+ if err != nil {
12295+ return nil , err
12296+ }
12297+ if _ , err = ref .ToState (); err != nil {
12298+ return nil , err
12299+ }
12300+ res .AddRef (pk , ref )
12301+
12302+ expPlatforms := & exptypes.Platforms {
12303+ Platforms : []exptypes.Platform {{ID : pk , Platform : p }},
12304+ }
12305+ dt , err := json .Marshal (expPlatforms )
12306+ if err != nil {
12307+ return nil , err
12308+ }
12309+ res .AddMeta (exptypes .ExporterPlatformsKey , dt )
12310+
12311+ return res , nil
12312+ }
12313+
12314+ // Test CycloneDX fallback scanner
12315+ target := registry + "/buildkit/testcdx:latest"
12316+ _ , err = c .Build (sb .Context (), SolveOpt {
12317+ FrontendAttrs : map [string ]string {
12318+ "attest:sbom" : "generator=" + scannerTarget ,
12319+ },
12320+ Exports : []ExportEntry {
12321+ {
12322+ Type : ExporterImage ,
12323+ Attrs : map [string ]string {
12324+ "name" : target ,
12325+ "push" : "true" ,
12326+ },
12327+ },
12328+ },
12329+ }, "" , makeBaseResult , nil )
12330+ require .NoError (t , err )
12331+
12332+ desc , provider , err := contentutil .ProviderFromRef (target )
12333+ require .NoError (t , err )
12334+
12335+ imgs , err := testutil .ReadImages (sb .Context (), provider , desc )
12336+ require .NoError (t , err )
12337+ require .Len (t , imgs .Images , 2 , "expected main image + attestation manifest" )
12338+
12339+ att := imgs .Find ("unknown/unknown" )
12340+ require .NotNil (t , att )
12341+ attest := intoto.Statement {}
12342+ require .NoError (t , json .Unmarshal (att .LayersRaw [0 ], & attest ))
12343+ require .Equal (t , intoto .StatementInTotoV1 , attest .Type )
12344+ require .Equal (t , intoto .PredicateCycloneDX , attest .PredicateType )
12345+ require .NotEmpty (t , attest .Subject )
12346+ pred , ok := attest .Predicate .(map [string ]interface {})
12347+ require .True (t , ok , "predicate should be a JSON object" )
12348+ require .Equal (t , "CycloneDX" , pred ["bomFormat" ])
12349+ require .Equal (t , "1.4" , pred ["specVersion" ])
12350+
12351+ // Test that HasSBOM recognizes CycloneDX attestations from frontend
12352+ cdxFrontend := func (ctx context.Context , c gateway.Client ) (* gateway.Result , error ) {
12353+ res , err := makeBaseResult (ctx , c )
12354+ if err != nil {
12355+ return nil , err
12356+ }
12357+
12358+ // Attach a CycloneDX attestation from the frontend
12359+ st := llb .Scratch ().
12360+ File (llb .Mkfile ("/result.cdx" , 0600 , []byte (`{"bomFormat": "CycloneDX"}` )))
12361+ def , err := st .Marshal (ctx )
12362+ if err != nil {
12363+ return nil , err
12364+ }
12365+ r , err := c .Solve (ctx , gateway.SolveRequest {
12366+ Definition : def .ToPB (),
12367+ })
12368+ if err != nil {
12369+ return nil , err
12370+ }
12371+ refAttest , err := r .SingleRef ()
12372+ if err != nil {
12373+ return nil , err
12374+ }
12375+ if _ , err = refAttest .ToState (); err != nil {
12376+ return nil , err
12377+ }
12378+
12379+ res .AddAttestation (pk , gateway.Attestation {
12380+ Kind : gatewaypb .AttestationKind_InToto ,
12381+ Ref : refAttest ,
12382+ Path : "/result.cdx" ,
12383+ InToto : result.InTotoAttestation {
12384+ PredicateType : intoto .PredicateCycloneDX ,
12385+ },
12386+ })
12387+
12388+ return res , nil
12389+ }
12390+
12391+ // When frontend provides CycloneDX SBOM, the fallback scanner should be skipped
12392+ target = registry + "/buildkit/testcdx2:latest"
12393+ _ , err = c .Build (sb .Context (), SolveOpt {
12394+ FrontendAttrs : map [string ]string {
12395+ "attest:sbom" : "" ,
12396+ },
12397+ Exports : []ExportEntry {
12398+ {
12399+ Type : ExporterImage ,
12400+ Attrs : map [string ]string {
12401+ "name" : target ,
12402+ "push" : "true" ,
12403+ },
12404+ },
12405+ },
12406+ }, "" , cdxFrontend , nil )
12407+ require .NoError (t , err )
12408+
12409+ desc , provider , err = contentutil .ProviderFromRef (target )
12410+ require .NoError (t , err )
12411+
12412+ imgs , err = testutil .ReadImages (sb .Context (), provider , desc )
12413+ require .NoError (t , err )
12414+ require .Len (t , imgs .Images , 2 , "expected main image + attestation manifest" )
12415+
12416+ att = imgs .Find ("unknown/unknown" )
12417+ require .NotNil (t , att )
12418+ attest = intoto.Statement {}
12419+ require .NoError (t , json .Unmarshal (att .LayersRaw [0 ], & attest ))
12420+ require .Equal (t , intoto .StatementInTotoV1 , attest .Type )
12421+ require .Equal (t , intoto .PredicateCycloneDX , attest .PredicateType )
12422+ require .NotEmpty (t , attest .Subject )
12423+ pred , ok = attest .Predicate .(map [string ]interface {})
12424+ require .True (t , ok , "predicate should be a JSON object" )
12425+ require .Equal (t , "CycloneDX" , pred ["bomFormat" ])
12426+ }
12427+
1218712428func testMultipleCacheExports (t * testing.T , sb integration.Sandbox ) {
1218812429 workers .CheckFeatureCompat (t , sb , workers .FeatureMultiCacheExport )
1218912430 c , err := New (sb .Context (), sb .Address ())
0 commit comments