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 }