blob: 1477ca56acf37c4895fe8d225cd2715888227a07 [file] [log] [blame]
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +00001// Copyright 2022 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package main
16
17import (
18 "bytes"
19 "flag"
20 "fmt"
21 "io"
22 "io/fs"
23 "os"
24 "path/filepath"
25 "sort"
26 "strings"
27 "time"
28
29 "android/soong/response"
30 "android/soong/tools/compliance"
31 "android/soong/tools/compliance/projectmetadata"
32
33 "github.com/google/blueprint/deptools"
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +000034
35 "github.com/spdx/tools-golang/builder/builder2v2"
36 "github.com/spdx/tools-golang/json"
37 "github.com/spdx/tools-golang/spdx/common"
38 spdx "github.com/spdx/tools-golang/spdx/v2_2"
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +000039)
40
41var (
42 failNoneRequested = fmt.Errorf("\nNo license metadata files requested")
43 failNoLicenses = fmt.Errorf("No licenses found")
44)
45
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +000046const NOASSERTION = "NOASSERTION"
47
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +000048type context struct {
49 stdout io.Writer
50 stderr io.Writer
51 rootFS fs.FS
52 product string
53 stripPrefix []string
54 creationTime creationTimeGetter
55}
56
57func (ctx context) strip(installPath string) string {
58 for _, prefix := range ctx.stripPrefix {
59 if strings.HasPrefix(installPath, prefix) {
60 p := strings.TrimPrefix(installPath, prefix)
61 if 0 == len(p) {
62 p = ctx.product
63 }
64 if 0 == len(p) {
65 continue
66 }
67 return p
68 }
69 }
70 return installPath
71}
72
73// newMultiString creates a flag that allows multiple values in an array.
74func newMultiString(flags *flag.FlagSet, name, usage string) *multiString {
75 var f multiString
76 flags.Var(&f, name, usage)
77 return &f
78}
79
80// multiString implements the flag `Value` interface for multiple strings.
81type multiString []string
82
83func (ms *multiString) String() string { return strings.Join(*ms, ", ") }
84func (ms *multiString) Set(s string) error { *ms = append(*ms, s); return nil }
85
86func main() {
87 var expandedArgs []string
88 for _, arg := range os.Args[1:] {
89 if strings.HasPrefix(arg, "@") {
90 f, err := os.Open(strings.TrimPrefix(arg, "@"))
91 if err != nil {
92 fmt.Fprintln(os.Stderr, err.Error())
93 os.Exit(1)
94 }
95
96 respArgs, err := response.ReadRspFile(f)
97 f.Close()
98 if err != nil {
99 fmt.Fprintln(os.Stderr, err.Error())
100 os.Exit(1)
101 }
102 expandedArgs = append(expandedArgs, respArgs...)
103 } else {
104 expandedArgs = append(expandedArgs, arg)
105 }
106 }
107
108 flags := flag.NewFlagSet("flags", flag.ExitOnError)
109
110 flags.Usage = func() {
111 fmt.Fprintf(os.Stderr, `Usage: %s {options} file.meta_lic {file.meta_lic...}
112
113Outputs an SBOM.spdx.
114
115Options:
116`, filepath.Base(os.Args[0]))
117 flags.PrintDefaults()
118 }
119
120 outputFile := flags.String("o", "-", "Where to write the SBOM spdx file. (default stdout)")
121 depsFile := flags.String("d", "", "Where to write the deps file")
122 product := flags.String("product", "", "The name of the product for which the notice is generated.")
123 stripPrefix := newMultiString(flags, "strip_prefix", "Prefix to remove from paths. i.e. path to root (multiple allowed)")
124
125 flags.Parse(expandedArgs)
126
127 // Must specify at least one root target.
128 if flags.NArg() == 0 {
129 flags.Usage()
130 os.Exit(2)
131 }
132
133 if len(*outputFile) == 0 {
134 flags.Usage()
135 fmt.Fprintf(os.Stderr, "must specify file for -o; use - for stdout\n")
136 os.Exit(2)
137 } else {
138 dir, err := filepath.Abs(filepath.Dir(*outputFile))
139 if err != nil {
140 fmt.Fprintf(os.Stderr, "cannot determine path to %q: %s\n", *outputFile, err)
141 os.Exit(1)
142 }
143 fi, err := os.Stat(dir)
144 if err != nil {
145 fmt.Fprintf(os.Stderr, "cannot read directory %q of %q: %s\n", dir, *outputFile, err)
146 os.Exit(1)
147 }
148 if !fi.IsDir() {
149 fmt.Fprintf(os.Stderr, "parent %q of %q is not a directory\n", dir, *outputFile)
150 os.Exit(1)
151 }
152 }
153
154 var ofile io.Writer
155 ofile = os.Stdout
156 var obuf *bytes.Buffer
157 if *outputFile != "-" {
158 obuf = &bytes.Buffer{}
159 ofile = obuf
160 }
161
162 ctx := &context{ofile, os.Stderr, compliance.FS, *product, *stripPrefix, actualTime}
163
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000164 spdxDoc, deps, err := sbomGenerator(ctx, flags.Args()...)
165
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000166 if err != nil {
167 if err == failNoneRequested {
168 flags.Usage()
169 }
170 fmt.Fprintf(os.Stderr, "%s\n", err.Error())
171 os.Exit(1)
172 }
173
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000174 if err := spdx_json.Save2_2(spdxDoc, ofile); err != nil {
175 fmt.Fprintf(os.Stderr, "failed to write document to %v: %v", *outputFile, err)
176 os.Exit(1)
177 }
178
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000179 if *outputFile != "-" {
180 err := os.WriteFile(*outputFile, obuf.Bytes(), 0666)
181 if err != nil {
182 fmt.Fprintf(os.Stderr, "could not write output to %q: %s\n", *outputFile, err)
183 os.Exit(1)
184 }
185 }
186
187 if *depsFile != "" {
188 err := deptools.WriteDepFile(*depsFile, *outputFile, deps)
189 if err != nil {
190 fmt.Fprintf(os.Stderr, "could not write deps to %q: %s\n", *depsFile, err)
191 os.Exit(1)
192 }
193 }
194 os.Exit(0)
195}
196
197type creationTimeGetter func() time.Time
198
199// actualTime returns current time in UTC
200func actualTime() time.Time {
201 return time.Now().UTC()
202}
203
204// replaceSlashes replaces "/" by "-" for the library path to be used for packages & files SPDXID
205func replaceSlashes(x string) string {
206 return strings.ReplaceAll(x, "/", "-")
207}
208
209// getPackageName returns a package name of a target Node
210func getPackageName(_ *context, tn *compliance.TargetNode) string {
211 return replaceSlashes(tn.Name())
212}
213
214// getDocumentName returns a package name of a target Node
215func getDocumentName(ctx *context, tn *compliance.TargetNode, pm *projectmetadata.ProjectMetadata) string {
216 if len(ctx.product) > 0 {
217 return replaceSlashes(ctx.product)
218 }
219 if len(tn.ModuleName()) > 0 {
220 if pm != nil {
221 return replaceSlashes(pm.Name() + ":" + tn.ModuleName())
222 }
223 return replaceSlashes(tn.ModuleName())
224 }
225
226 // TO DO: Replace tn.Name() with pm.Name() + parts of the target name
227 return replaceSlashes(tn.Name())
228}
229
230// getDownloadUrl returns the download URL if available (GIT, SVN, etc..),
231// or NOASSERTION if not available, none determined or ambiguous
232func getDownloadUrl(_ *context, pm *projectmetadata.ProjectMetadata) string {
233 if pm == nil {
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000234 return NOASSERTION
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000235 }
236
237 urlsByTypeName := pm.UrlsByTypeName()
238 if urlsByTypeName == nil {
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000239 return NOASSERTION
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000240 }
241
242 url := urlsByTypeName.DownloadUrl()
243 if url == "" {
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000244 return NOASSERTION
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000245 }
246 return url
247}
248
Ibrahim Kanouchea68ed082022-11-03 00:43:12 +0000249// getProjectMetadata returns the optimal project metadata for the target node
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000250func getProjectMetadata(_ *context, pmix *projectmetadata.Index,
251 tn *compliance.TargetNode) (*projectmetadata.ProjectMetadata, error) {
252 pms, err := pmix.MetadataForProjects(tn.Projects()...)
253 if err != nil {
254 return nil, fmt.Errorf("Unable to read projects for %q: %w\n", tn, err)
255 }
256 if len(pms) == 0 {
257 return nil, nil
258 }
259
Ibrahim Kanouchea68ed082022-11-03 00:43:12 +0000260 // Getting the project metadata that contains most of the info needed for sbomGenerator
261 score := -1
262 index := -1
263 for i := 0; i < len(pms); i++ {
264 tempScore := 0
265 if pms[i].Name() != "" {
266 tempScore += 1
267 }
268 if pms[i].Version() != "" {
269 tempScore += 1
270 }
271 if pms[i].UrlsByTypeName().DownloadUrl() != "" {
272 tempScore += 1
273 }
274
275 if tempScore == score {
276 if pms[i].Project() < pms[index].Project() {
277 index = i
278 }
279 } else if tempScore > score {
280 score = tempScore
281 index = i
282 }
283 }
284 return pms[index], nil
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000285}
286
Ibrahim Kanouche649b4d72022-11-12 05:46:12 +0000287// inputFiles returns the complete list of files read
288func inputFiles(lg *compliance.LicenseGraph, pmix *projectmetadata.Index, licenseTexts []string) []string {
289 projectMeta := pmix.AllMetadataFiles()
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000290 targets := lg.TargetNames()
Ibrahim Kanouche649b4d72022-11-12 05:46:12 +0000291 files := make([]string, 0, len(licenseTexts)+len(targets)+len(projectMeta))
292 files = append(files, licenseTexts...)
293 files = append(files, targets...)
294 files = append(files, projectMeta...)
295 return files
296}
297
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000298// sbomGenerator implements the spdx bom utility
299
300// SBOM is part of the new government regulation issued to improve national cyber security
301// and enhance software supply chain and transparency, see https://www.cisa.gov/sbom
302
303// sbomGenerator uses the SPDX standard, see the SPDX specification (https://spdx.github.io/spdx-spec/)
304// sbomGenerator is also following the internal google SBOM styleguide (http://goto.google.com/spdx-style-guide)
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000305func sbomGenerator(ctx *context, files ...string) (*spdx.Document, []string, error) {
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000306 // Must be at least one root file.
307 if len(files) < 1 {
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000308 return nil, nil, failNoneRequested
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000309 }
310
311 pmix := projectmetadata.NewIndex(ctx.rootFS)
312
313 lg, err := compliance.ReadLicenseGraph(ctx.rootFS, ctx.stderr, files)
314
315 if err != nil {
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000316 return nil, nil, fmt.Errorf("Unable to read license text file(s) for %q: %v\n", files, err)
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000317 }
318
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000319 // creating the packages section
320 pkgs := []*spdx.Package{}
321
322 // creating the relationship section
323 relationships := []*spdx.Relationship{}
324
325 // creating the license section
326 otherLicenses := []*spdx.OtherLicense{}
327
328 // main package name
329 var mainPkgName string
330
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000331 // implementing the licenses references for the packages
332 licenses := make(map[string]string)
333 concludedLicenses := func(licenseTexts []string) string {
334 licenseRefs := make([]string, 0, len(licenseTexts))
335 for _, licenseText := range licenseTexts {
336 license := strings.SplitN(licenseText, ":", 2)[0]
337 if _, ok := licenses[license]; !ok {
338 licenseRef := "LicenseRef-" + replaceSlashes(license)
339 licenses[license] = licenseRef
340 }
341
342 licenseRefs = append(licenseRefs, licenses[license])
343 }
344 if len(licenseRefs) > 1 {
345 return "(" + strings.Join(licenseRefs, " AND ") + ")"
346 } else if len(licenseRefs) == 1 {
347 return licenseRefs[0]
348 }
349 return "NONE"
350 }
351
352 isMainPackage := true
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000353 visitedNodes := make(map[*compliance.TargetNode]struct{})
354
355 // performing a Breadth-first top down walk of licensegraph and building package information
356 compliance.WalkTopDownBreadthFirst(nil, lg,
357 func(lg *compliance.LicenseGraph, tn *compliance.TargetNode, path compliance.TargetEdgePath) bool {
358 if err != nil {
359 return false
360 }
361 var pm *projectmetadata.ProjectMetadata
362 pm, err = getProjectMetadata(ctx, pmix, tn)
363 if err != nil {
364 return false
365 }
366
367 if isMainPackage {
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000368 mainPkgName = replaceSlashes(getPackageName(ctx, tn))
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000369 isMainPackage = false
370 }
371
Bob Badour928ee9d2023-03-31 14:51:36 +0000372 if len(path) == 0 {
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000373 // Add the describe relationship for the main package
374 rln := &spdx.Relationship{
375 RefA: common.MakeDocElementID("" /* this document */, "DOCUMENT"),
376 RefB: common.MakeDocElementID("", mainPkgName),
377 Relationship: "DESCRIBES",
378 }
379 relationships = append(relationships, rln)
380
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000381 } else {
382 // Check parent and identify annotation
383 parent := path[len(path)-1]
384 targetEdge := parent.Edge()
385 if targetEdge.IsRuntimeDependency() {
386 // Adding the dynamic link annotation RUNTIME_DEPENDENCY_OF relationship
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000387 rln := &spdx.Relationship{
388 RefA: common.MakeDocElementID("", replaceSlashes(getPackageName(ctx, tn))),
389 RefB: common.MakeDocElementID("", replaceSlashes(getPackageName(ctx, targetEdge.Target()))),
390 Relationship: "RUNTIME_DEPENDENCY_OF",
391 }
392 relationships = append(relationships, rln)
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000393
394 } else if targetEdge.IsDerivation() {
395 // Adding the derivation annotation as a CONTAINS relationship
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000396 rln := &spdx.Relationship{
397 RefA: common.MakeDocElementID("", replaceSlashes(getPackageName(ctx, targetEdge.Target()))),
398 RefB: common.MakeDocElementID("", replaceSlashes(getPackageName(ctx, tn))),
399 Relationship: "CONTAINS",
400 }
401 relationships = append(relationships, rln)
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000402
403 } else if targetEdge.IsBuildTool() {
404 // Adding the toolchain annotation as a BUILD_TOOL_OF relationship
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000405 rln := &spdx.Relationship{
406 RefA: common.MakeDocElementID("", replaceSlashes(getPackageName(ctx, tn))),
407 RefB: common.MakeDocElementID("", replaceSlashes(getPackageName(ctx, targetEdge.Target()))),
408 Relationship: "BUILD_TOOL_OF",
409 }
410 relationships = append(relationships, rln)
411
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000412 } else {
413 panic(fmt.Errorf("Unknown dependency type: %v", targetEdge.Annotations()))
414 }
415 }
416
417 if _, alreadyVisited := visitedNodes[tn]; alreadyVisited {
418 return false
419 }
420 visitedNodes[tn] = struct{}{}
421 pkgName := getPackageName(ctx, tn)
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000422
423 // Making an spdx package and adding it to pkgs
424 pkg := &spdx.Package{
425 PackageName: replaceSlashes(pkgName),
426 PackageDownloadLocation: getDownloadUrl(ctx, pm),
427 PackageSPDXIdentifier: common.ElementID(replaceSlashes(pkgName)),
428 PackageLicenseConcluded: concludedLicenses(tn.LicenseTexts()),
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000429 }
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000430
431 if pm != nil && pm.Version() != "" {
432 pkg.PackageVersion = pm.Version()
433 } else {
434 pkg.PackageVersion = NOASSERTION
435 }
436
437 pkgs = append(pkgs, pkg)
438
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000439 return true
440 })
441
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000442 // Adding Non-standard licenses
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000443
444 licenseTexts := make([]string, 0, len(licenses))
445
446 for licenseText := range licenses {
447 licenseTexts = append(licenseTexts, licenseText)
448 }
449
450 sort.Strings(licenseTexts)
451
452 for _, licenseText := range licenseTexts {
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000453 // open the file
454 f, err := ctx.rootFS.Open(filepath.Clean(licenseText))
455 if err != nil {
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000456 return nil, nil, fmt.Errorf("error opening license text file %q: %w", licenseText, err)
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000457 }
458
459 // read the file
460 text, err := io.ReadAll(f)
461 if err != nil {
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000462 return nil, nil, fmt.Errorf("error reading license text file %q: %w", licenseText, err)
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000463 }
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000464 // Making an spdx License and adding it to otherLicenses
465 otherLicenses = append(otherLicenses, &spdx.OtherLicense{
466 LicenseName: strings.Replace(licenses[licenseText], "LicenseRef-", "", -1),
467 LicenseIdentifier: string(licenses[licenseText]),
468 ExtractedText: string(text),
469 })
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000470 }
471
Ibrahim Kanouche649b4d72022-11-12 05:46:12 +0000472 deps := inputFiles(lg, pmix, licenseTexts)
473 sort.Strings(deps)
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000474
475 // Making the SPDX doc
476 ci, err := builder2v2.BuildCreationInfoSection2_2("Organization", "Google LLC", nil)
477 if err != nil {
478 return nil, nil, fmt.Errorf("Unable to build creation info section for SPDX doc: %v\n", err)
479 }
480
481 return &spdx.Document{
482 SPDXIdentifier: "DOCUMENT",
483 CreationInfo: ci,
484 Packages: pkgs,
485 Relationships: relationships,
486 OtherLicenses: otherLicenses,
487 }, deps, nil
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000488}