Skip to content

Commit

Permalink
Removes metadata.timestamp and serialNumber fields from generates…
Browse files Browse the repository at this point in the history
… CycloneDX JSON

These two properties generate unique values every time syft runs, thus they break reproducible builds (a scan runs on each build, so two different builds of the same application source/buildpacks/builder will result in two different CycloneDX JSON files and thus two different images).

This fix is to manually read the generated CycloneDX file, parse the JSON, remove the two fields and write the JSON back out. This should be OK as the CycloneDX spec says that the fields are optional: https://cyclonedx.org/docs/1.3/json/.

Signed-off-by: Daniel Mikusa <[email protected]>
  • Loading branch information
Daniel Mikusa committed May 5, 2022
1 parent d82231c commit 9395235
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 8 deletions.
60 changes: 58 additions & 2 deletions sbom/sbom.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"os"

"github.com/buildpacks/libcnb"
"github.com/mitchellh/hashstructure/v2"
Expand Down Expand Up @@ -146,12 +147,67 @@ func (b SyftCLISBOMScanner) scan(sbomPathCreator func(libcnb.SBOMFormat) string,

args = append(args, fmt.Sprintf("dir:%s", scanDir))

return b.Executor.Execute(effect.Execution{
if err := b.Executor.Execute(effect.Execution{
Command: "syft",
Args: args,
Stdout: b.Logger.TerminalErrorWriter(),
Stderr: b.Logger.TerminalErrorWriter(),
})
}); err != nil {
return fmt.Errorf("unable to run `syft %s`\n%w", args, err)
}

// cleans cyclonedx file which has a timestamp and unique id which always change
for _, format := range formats {
if format == libcnb.CycloneDXJSON {
if err := b.makeCycloneDXReproducible(sbomPathCreator(format)); err != nil {
return fmt.Errorf("unable to make cyclone dx file reproducible\n%w", err)
}
}
}

return nil
}

func (b SyftCLISBOMScanner) makeCycloneDXReproducible(path string) error {
input, err := loadCycloneDXFile(path)
if err != nil {
return err
}

delete(input, "serialNumber")

if md, exists := input["metadata"]; exists {
if metadata, ok := md.(map[string]interface{}); ok {
delete(metadata, "timestamp")
}
}

out, err := os.Create(path)
if err != nil {
return fmt.Errorf("unable to open CycloneDX JSON for writing %s\n%w", path, err)
}
defer out.Close()

if err := json.NewEncoder(out).Encode(input); err != nil {
return fmt.Errorf("unable to encode CycloneDX\n%w", err)
}

return nil
}

func loadCycloneDXFile(path string) (map[string]interface{}, error) {
in, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("unable to read CycloneDX JSON file %s\n%w", path, err)
}
defer in.Close()

raw := map[string]interface{}{}
if err := json.NewDecoder(in).Decode(&raw); err != nil {
return nil, fmt.Errorf("unable to decode CycloneDX JSON %s\n%w", path, err)
}

return raw, nil
}

// SBOMFormatToSyftOutputFormat converts a libcnb.SBOMFormat to the syft matching syft output format string
Expand Down
58 changes: 52 additions & 6 deletions sbom/sbom_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,52 @@ func testSBOM(t *testing.T, context spec.G, it spec.S) {
Expect(string(result)).To(Equal("succeed1"))
})

it("runs syft to generate reproducible cycloneDX JSON", func() {
format := libcnb.CycloneDXJSON
outputPath := layers.BuildSBOMPath(format)

executor.On("Execute", mock.MatchedBy(func(e effect.Execution) bool {
return e.Command == "syft" &&
len(e.Args) == 5 &&
strings.HasPrefix(e.Args[3], "cyclonedx-json=") &&
e.Args[4] == "dir:something"
})).Run(func(args mock.Arguments) {
Expect(ioutil.WriteFile(outputPath, []byte(`{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:fcfa5e19-bf49-47b4-8c85-ab61e2728f8e",
"version": 1,
"metadata": {
"timestamp": "2022-05-05T11:33:13-04:00",
"tools": [
{
"vendor": "anchore",
"name": "syft",
"version": "0.45.1"
}
],
"component": {
"bom-ref": "555d623e4777b7ae",
"type": "file",
"name": "target/demo-0.0.1-SNAPSHOT.jar"
}
}
}`), 0644)).To(Succeed())
}).Return(nil)

// uses interface here intentionally, to force that inteface and implementation match
scanner = sbom.NewSyftCLISBOMScanner(layers, &executor, bard.NewLogger(io.Discard))

Expect(scanner.ScanBuild("something", format)).To(Succeed())

result, err := ioutil.ReadFile(outputPath)
Expect(err).ToNot(HaveOccurred())
Expect(string(result)).ToNot(ContainSubstring("serialNumber"))
Expect(string(result)).ToNot(ContainSubstring("urn:uuid:fcfa5e19-bf49-47b4-8c85-ab61e2728f8e"))
Expect(string(result)).ToNot(ContainSubstring("timestamp"))
Expect(string(result)).ToNot(ContainSubstring("2022-05-05T11:33:13-04:00"))
})

it("runs syft once to generate layer-specific JSON", func() {
format := libcnb.SyftJSON
outputPath := layer.SBOMPath(format)
Expand Down Expand Up @@ -114,9 +160,9 @@ func testSBOM(t *testing.T, context spec.G, it spec.S) {
strings.HasPrefix(e.Args[7], sbom.SBOMFormatToSyftOutputFormat(libcnb.SPDXJSON)) &&
e.Args[8] == "dir:something"
})).Run(func(args mock.Arguments) {
Expect(ioutil.WriteFile(layers.LaunchSBOMPath(libcnb.CycloneDXJSON), []byte("succeed1"), 0644)).To(Succeed())
Expect(ioutil.WriteFile(layers.LaunchSBOMPath(libcnb.SyftJSON), []byte("succeed2"), 0644)).To(Succeed())
Expect(ioutil.WriteFile(layers.LaunchSBOMPath(libcnb.SPDXJSON), []byte("succeed3"), 0644)).To(Succeed())
Expect(ioutil.WriteFile(layers.LaunchSBOMPath(libcnb.CycloneDXJSON), []byte(`{"succeed":1}`), 0644)).To(Succeed())
Expect(ioutil.WriteFile(layers.LaunchSBOMPath(libcnb.SyftJSON), []byte(`{"succeed":2}`), 0644)).To(Succeed())
Expect(ioutil.WriteFile(layers.LaunchSBOMPath(libcnb.SPDXJSON), []byte(`{"succeed":3}`), 0644)).To(Succeed())
}).Return(nil)

scanner := sbom.SyftCLISBOMScanner{
Expand All @@ -129,15 +175,15 @@ func testSBOM(t *testing.T, context spec.G, it spec.S) {

result, err := ioutil.ReadFile(layers.LaunchSBOMPath(libcnb.CycloneDXJSON))
Expect(err).ToNot(HaveOccurred())
Expect(string(result)).To(Equal("succeed1"))
Expect(string(result)).To(HavePrefix(`{"succeed":1}`))

result, err = ioutil.ReadFile(layers.LaunchSBOMPath(libcnb.SyftJSON))
Expect(err).ToNot(HaveOccurred())
Expect(string(result)).To(Equal("succeed2"))
Expect(string(result)).To(HavePrefix(`{"succeed":2}`))

result, err = ioutil.ReadFile(layers.LaunchSBOMPath(libcnb.SPDXJSON))
Expect(err).ToNot(HaveOccurred())
Expect(string(result)).To(Equal("succeed3"))
Expect(string(result)).To(HavePrefix(`{"succeed":3}`))
})

it("writes out a manual BOM entry", func() {
Expand Down

0 comments on commit 9395235

Please sign in to comment.