github.com/rrashidov/libpak@v0.0.0-20230911084305-75119185bb4d/sbom/sbom.go (about)

     1  package sbom
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"os"
     7  
     8  	"github.com/buildpacks/libcnb"
     9  	"github.com/mitchellh/hashstructure/v2"
    10  	"github.com/paketo-buildpacks/libpak/bard"
    11  	"github.com/paketo-buildpacks/libpak/effect"
    12  )
    13  
    14  //go:generate mockery -name SBOMScanner -case=underscore
    15  
    16  type SBOMScanner interface {
    17  	ScanLayer(layer libcnb.Layer, scanDir string, formats ...libcnb.SBOMFormat) error
    18  	ScanBuild(scanDir string, formats ...libcnb.SBOMFormat) error
    19  	ScanLaunch(scanDir string, formats ...libcnb.SBOMFormat) error
    20  }
    21  
    22  type SyftDependency struct {
    23  	Artifacts  []SyftArtifact
    24  	Source     SyftSource
    25  	Descriptor SyftDescriptor
    26  	Schema     SyftSchema
    27  }
    28  
    29  func NewSyftDependency(dependencyPath string, artifacts []SyftArtifact) SyftDependency {
    30  	return SyftDependency{
    31  		Artifacts: artifacts,
    32  		Source: SyftSource{
    33  			Type:   "directory",
    34  			Target: dependencyPath,
    35  		},
    36  		Descriptor: SyftDescriptor{
    37  			Name:    "syft",
    38  			Version: "0.32.0",
    39  		},
    40  		Schema: SyftSchema{
    41  			Version: "1.1.0",
    42  			URL:     "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json",
    43  		},
    44  	}
    45  }
    46  
    47  func (s SyftDependency) WriteTo(path string) error {
    48  	output, err := json.Marshal(&s)
    49  	if err != nil {
    50  		return fmt.Errorf("unable to marshal to JSON\n%w", err)
    51  	}
    52  
    53  	err = os.WriteFile(path, output, 0644)
    54  	if err != nil {
    55  		return fmt.Errorf("unable to write to path %s\n%w", path, err)
    56  	}
    57  
    58  	return nil
    59  }
    60  
    61  type SyftArtifact struct {
    62  	ID        string
    63  	Name      string
    64  	Version   string
    65  	Type      string
    66  	FoundBy   string
    67  	Locations []SyftLocation
    68  	Licenses  []string
    69  	Language  string
    70  	CPEs      []string
    71  	PURL      string
    72  }
    73  
    74  func (s SyftArtifact) Hash() (string, error) {
    75  	f, err := hashstructure.Hash(s, hashstructure.FormatV2, &hashstructure.HashOptions{
    76  		ZeroNil:      true,
    77  		SlicesAsSets: true,
    78  	})
    79  	if err != nil {
    80  		return "", fmt.Errorf("could not build ID for artifact=%+v: %+v", s, err)
    81  	}
    82  
    83  	return fmt.Sprintf("%x", f), nil
    84  }
    85  
    86  type SyftLocation struct {
    87  	Path string
    88  }
    89  
    90  type SyftSource struct {
    91  	Type   string
    92  	Target string
    93  }
    94  
    95  type SyftDescriptor struct {
    96  	Name    string
    97  	Version string
    98  }
    99  
   100  type SyftSchema struct {
   101  	Version string
   102  	URL     string
   103  }
   104  
   105  type SyftCLISBOMScanner struct {
   106  	Executor effect.Executor
   107  	Layers   libcnb.Layers
   108  	Logger   bard.Logger
   109  }
   110  
   111  func NewSyftCLISBOMScanner(layers libcnb.Layers, executor effect.Executor, logger bard.Logger) SyftCLISBOMScanner {
   112  	return SyftCLISBOMScanner{
   113  		Executor: executor,
   114  		Layers:   layers,
   115  		Logger:   logger,
   116  	}
   117  }
   118  
   119  // ScanLayer will use syft CLI to scan the scanDir and write it's output to the layer SBoM file in the given formats
   120  func (b SyftCLISBOMScanner) ScanLayer(layer libcnb.Layer, scanDir string, formats ...libcnb.SBOMFormat) error {
   121  	return b.scan(func(fmt libcnb.SBOMFormat) string {
   122  		return layer.SBOMPath(fmt)
   123  	}, scanDir, formats...)
   124  }
   125  
   126  // ScanBuild will use syft CLI to scan the scanDir and write it's output to the build SBoM file in the given formats
   127  func (b SyftCLISBOMScanner) ScanBuild(scanDir string, formats ...libcnb.SBOMFormat) error {
   128  	return b.scan(func(fmt libcnb.SBOMFormat) string {
   129  		return b.Layers.BuildSBOMPath(fmt)
   130  	}, scanDir, formats...)
   131  }
   132  
   133  // ScanLaunch will use syft CLI to scan the scanDir and write it's output to the launch SBoM file in the given formats
   134  func (b SyftCLISBOMScanner) ScanLaunch(scanDir string, formats ...libcnb.SBOMFormat) error {
   135  	return b.scan(func(fmt libcnb.SBOMFormat) string {
   136  		return b.Layers.LaunchSBOMPath(fmt)
   137  	}, scanDir, formats...)
   138  }
   139  
   140  func (b SyftCLISBOMScanner) scan(sbomPathCreator func(libcnb.SBOMFormat) string, scanDir string, formats ...libcnb.SBOMFormat) error {
   141  	args := []string{"packages", "-q"}
   142  
   143  	for _, format := range formats {
   144  		args = append(args, "-o", fmt.Sprintf("%s=%s", SBOMFormatToSyftOutputFormat(format), sbomPathCreator(format)))
   145  	}
   146  
   147  	args = append(args, fmt.Sprintf("dir:%s", scanDir))
   148  
   149  	if err := b.Executor.Execute(effect.Execution{
   150  		Command: "syft",
   151  		Args:    args,
   152  		Stdout:  b.Logger.TerminalErrorWriter(),
   153  		Stderr:  b.Logger.TerminalErrorWriter(),
   154  	}); err != nil {
   155  		return fmt.Errorf("unable to run `syft %s`\n%w", args, err)
   156  	}
   157  
   158  	// cleans cyclonedx file which has a timestamp and unique id which always change
   159  	for _, format := range formats {
   160  		if format == libcnb.CycloneDXJSON {
   161  			if err := b.makeCycloneDXReproducible(sbomPathCreator(format)); err != nil {
   162  				return fmt.Errorf("unable to make cyclone dx file reproducible\n%w", err)
   163  			}
   164  		}
   165  	}
   166  
   167  	return nil
   168  }
   169  
   170  func (b SyftCLISBOMScanner) makeCycloneDXReproducible(path string) error {
   171  	input, err := loadCycloneDXFile(path)
   172  	if err != nil {
   173  		return err
   174  	}
   175  
   176  	delete(input, "serialNumber")
   177  
   178  	if md, exists := input["metadata"]; exists {
   179  		if metadata, ok := md.(map[string]interface{}); ok {
   180  			delete(metadata, "timestamp")
   181  		}
   182  	}
   183  
   184  	out, err := os.Create(path)
   185  	if err != nil {
   186  		return fmt.Errorf("unable to open CycloneDX JSON for writing %s\n%w", path, err)
   187  	}
   188  	defer out.Close()
   189  
   190  	if err := json.NewEncoder(out).Encode(input); err != nil {
   191  		return fmt.Errorf("unable to encode CycloneDX\n%w", err)
   192  	}
   193  
   194  	return nil
   195  }
   196  
   197  func loadCycloneDXFile(path string) (map[string]interface{}, error) {
   198  	in, err := os.Open(path)
   199  	if err != nil {
   200  		return nil, fmt.Errorf("unable to read CycloneDX JSON file %s\n%w", path, err)
   201  	}
   202  	defer in.Close()
   203  
   204  	raw := map[string]interface{}{}
   205  	if err := json.NewDecoder(in).Decode(&raw); err != nil {
   206  		return nil, fmt.Errorf("unable to decode CycloneDX JSON %s\n%w", path, err)
   207  	}
   208  
   209  	return raw, nil
   210  }
   211  
   212  // SBOMFormatToSyftOutputFormat converts a libcnb.SBOMFormat to the syft matching syft output format string
   213  func SBOMFormatToSyftOutputFormat(format libcnb.SBOMFormat) string {
   214  	var formatRaw string
   215  
   216  	switch format {
   217  	case libcnb.CycloneDXJSON:
   218  		formatRaw = "cyclonedx-json"
   219  	case libcnb.SPDXJSON:
   220  		formatRaw = "spdx-json"
   221  	case libcnb.SyftJSON:
   222  		formatRaw = "json"
   223  	}
   224  
   225  	return formatRaw
   226  }