github.com/wolfi-dev/wolfictl@v0.16.11/pkg/cli/sbom.go (about) 1 package cli 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "sort" 8 "strings" 9 10 "github.com/anchore/syft/syft/file" 11 "github.com/anchore/syft/syft/pkg" 12 sbomSyft "github.com/anchore/syft/syft/sbom" 13 "github.com/chainguard-dev/clog" 14 "github.com/samber/lo" 15 "github.com/spf13/cobra" 16 "github.com/wolfi-dev/wolfictl/pkg/sbom" 17 "golang.org/x/exp/slices" 18 ) 19 20 const ( 21 sbomFormatOutline = "outline" 22 sbomFormatSyftJSON = "syft-json" 23 ) 24 25 func cmdSBOM() *cobra.Command { 26 p := &sbomParams{} 27 cmd := &cobra.Command{ 28 Use: "sbom <path/to/package.apk>", 29 Short: "Generate a software bill of materials (SBOM) for an APK file", 30 Hidden: true, 31 SilenceErrors: true, 32 Args: cobra.ExactArgs(1), 33 RunE: func(cmd *cobra.Command, args []string) error { 34 logger := clog.NewLogger(newLogger(p.verbosity)) 35 ctx := clog.WithLogger(cmd.Context(), logger) 36 37 if !slices.Contains([]string{sbomFormatOutline, sbomFormatSyftJSON}, p.outputFormat) { 38 return fmt.Errorf("invalid output format %q, must be one of [%s]", p.outputFormat, strings.Join([]string{sbomFormatOutline, sbomFormatSyftJSON}, ", ")) 39 } 40 41 // TODO: Bring input retrieval options in line with `wolfictl scan`. 42 43 apkFilePath := args[0] 44 apkFile, err := os.Open(apkFilePath) 45 if err != nil { 46 return fmt.Errorf("failed to open apk file: %w", err) 47 } 48 49 if p.outputFormat == outputFormatOutline { 50 fmt.Printf("🔎 Scanning %q\n", apkFilePath) 51 } 52 53 var s *sbomSyft.SBOM 54 if p.disableSBOMCache { 55 s, err = sbom.Generate(ctx, apkFilePath, apkFile, p.distro) 56 } else { 57 s, err = sbom.CachedGenerate(ctx, apkFilePath, apkFile, p.distro) 58 } 59 if err != nil { 60 return fmt.Errorf("failed to generate SBOM: %w", err) 61 } 62 63 switch p.outputFormat { 64 case sbomFormatOutline: 65 tree := newPackageTree(s.Artifacts.Packages.Sorted()) 66 fmt.Println(tree.render()) 67 68 case sbomFormatSyftJSON: 69 jsonReader, err := sbom.ToSyftJSON(s) 70 if err != nil { 71 return fmt.Errorf("failed to encode SBOM: %w", err) 72 } 73 74 _, err = io.Copy(os.Stdout, jsonReader) 75 if err != nil { 76 return fmt.Errorf("failed to write SBOM: %w", err) 77 } 78 } 79 80 return nil 81 }, 82 } 83 84 p.addFlagsTo(cmd) 85 return cmd 86 } 87 88 type sbomParams struct { 89 outputFormat string 90 distro string 91 disableSBOMCache bool 92 verbosity int 93 } 94 95 func (p *sbomParams) addFlagsTo(cmd *cobra.Command) { 96 cmd.Flags().StringVarP(&p.outputFormat, "output", "o", sbomFormatOutline, "output format (outline, syft-json)") 97 cmd.Flags().StringVar(&p.distro, "distro", "wolfi", "distro to report in SBOM") 98 cmd.Flags().BoolVar(&p.disableSBOMCache, "disable-sbom-cache", false, "don't use the SBOM cache") 99 addVerboseFlag(&p.verbosity, cmd) 100 } 101 102 type packageTree struct { 103 packagesByLocation map[string][]pkg.Package 104 } 105 106 func newPackageTree(packages []pkg.Package) *packageTree { 107 packagesByLocation := map[string][]pkg.Package{} 108 for i := range packages { 109 p := packages[i] 110 locs := lo.Map(p.Locations.ToSlice(), func(l file.Location, _ int) string { 111 return "/" + l.RealPath 112 }) 113 114 location := strings.Join(locs, ", ") 115 packagesByLocation[location] = append(packagesByLocation[location], p) 116 } 117 return &packageTree{ 118 packagesByLocation: packagesByLocation, 119 } 120 } 121 122 func (t *packageTree) render() string { 123 locations := lo.Keys(t.packagesByLocation) 124 sort.Strings(locations) 125 126 var lines []string 127 for i, location := range locations { 128 var treeStem, verticalLine string 129 if i == len(locations)-1 { 130 treeStem = "└── " 131 verticalLine = " " 132 } else { 133 treeStem = "├── " 134 verticalLine = "│" 135 } 136 137 line := treeStem + fmt.Sprintf("📄 %s", location) 138 lines = append(lines, line) 139 140 packages := t.packagesByLocation[location] 141 142 sort.SliceStable(packages, func(i, j int) bool { 143 return packages[i].Name < packages[j].Name 144 }) 145 146 for i := range packages { 147 p := packages[i] 148 line := fmt.Sprintf( 149 "%s 📦 %s %s %s", 150 verticalLine, 151 p.Name, 152 p.Version, 153 styleSubtle.Render("("+string(p.Type)+")"), 154 ) 155 lines = append(lines, line) 156 } 157 158 lines = append(lines, verticalLine) 159 } 160 161 return strings.Join(lines, "\n") 162 }