blob: eddcd8b71b4c306822f951aa3727dedd3adaac10 [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 Kanouchee97adc52023-03-20 16:42:09 +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")
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +000044 mainPkgName = flag.String("main_package_name", "", "The name of the first target node in the licensegraph")
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +000045)
46
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +000047const NOASSERTION = "NOASSERTION"
48
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +000049type context struct {
50 stdout io.Writer
51 stderr io.Writer
52 rootFS fs.FS
53 product string
54 stripPrefix []string
55 creationTime creationTimeGetter
56}
57
58func (ctx context) strip(installPath string) string {
59 for _, prefix := range ctx.stripPrefix {
60 if strings.HasPrefix(installPath, prefix) {
61 p := strings.TrimPrefix(installPath, prefix)
62 if 0 == len(p) {
63 p = ctx.product
64 }
65 if 0 == len(p) {
66 continue
67 }
68 return p
69 }
70 }
71 return installPath
72}
73
74// newMultiString creates a flag that allows multiple values in an array.
75func newMultiString(flags *flag.FlagSet, name, usage string) *multiString {
76 var f multiString
77 flags.Var(&f, name, usage)
78 return &f
79}
80
81// multiString implements the flag `Value` interface for multiple strings.
82type multiString []string
83
84func (ms *multiString) String() string { return strings.Join(*ms, ", ") }
85func (ms *multiString) Set(s string) error { *ms = append(*ms, s); return nil }
86
87func main() {
88 var expandedArgs []string
89 for _, arg := range os.Args[1:] {
90 if strings.HasPrefix(arg, "@") {
91 f, err := os.Open(strings.TrimPrefix(arg, "@"))
92 if err != nil {
93 fmt.Fprintln(os.Stderr, err.Error())
94 os.Exit(1)
95 }
96
97 respArgs, err := response.ReadRspFile(f)
98 f.Close()
99 if err != nil {
100 fmt.Fprintln(os.Stderr, err.Error())
101 os.Exit(1)
102 }
103 expandedArgs = append(expandedArgs, respArgs...)
104 } else {
105 expandedArgs = append(expandedArgs, arg)
106 }
107 }
108
109 flags := flag.NewFlagSet("flags", flag.ExitOnError)
110
111 flags.Usage = func() {
112 fmt.Fprintf(os.Stderr, `Usage: %s {options} file.meta_lic {file.meta_lic...}
113
114Outputs an SBOM.spdx.
115
116Options:
117`, filepath.Base(os.Args[0]))
118 flags.PrintDefaults()
119 }
120
121 outputFile := flags.String("o", "-", "Where to write the SBOM spdx file. (default stdout)")
122 depsFile := flags.String("d", "", "Where to write the deps file")
123 product := flags.String("product", "", "The name of the product for which the notice is generated.")
124 stripPrefix := newMultiString(flags, "strip_prefix", "Prefix to remove from paths. i.e. path to root (multiple allowed)")
125
126 flags.Parse(expandedArgs)
127
128 // Must specify at least one root target.
129 if flags.NArg() == 0 {
130 flags.Usage()
131 os.Exit(2)
132 }
133
134 if len(*outputFile) == 0 {
135 flags.Usage()
136 fmt.Fprintf(os.Stderr, "must specify file for -o; use - for stdout\n")
137 os.Exit(2)
138 } else {
139 dir, err := filepath.Abs(filepath.Dir(*outputFile))
140 if err != nil {
141 fmt.Fprintf(os.Stderr, "cannot determine path to %q: %s\n", *outputFile, err)
142 os.Exit(1)
143 }
144 fi, err := os.Stat(dir)
145 if err != nil {
146 fmt.Fprintf(os.Stderr, "cannot read directory %q of %q: %s\n", dir, *outputFile, err)
147 os.Exit(1)
148 }
149 if !fi.IsDir() {
150 fmt.Fprintf(os.Stderr, "parent %q of %q is not a directory\n", dir, *outputFile)
151 os.Exit(1)
152 }
153 }
154
155 var ofile io.Writer
156 ofile = os.Stdout
157 var obuf *bytes.Buffer
158 if *outputFile != "-" {
159 obuf = &bytes.Buffer{}
160 ofile = obuf
161 }
162
163 ctx := &context{ofile, os.Stderr, compliance.FS, *product, *stripPrefix, actualTime}
164
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000165 spdxDoc, deps, err := sbomGenerator(ctx, flags.Args()...)
166
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000167 if err != nil {
168 if err == failNoneRequested {
169 flags.Usage()
170 }
171 fmt.Fprintf(os.Stderr, "%s\n", err.Error())
172 os.Exit(1)
173 }
174
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000175 if err := spdx_json.Save2_2(spdxDoc, ofile); err != nil {
176 fmt.Fprintf(os.Stderr, "failed to write document to %v: %v", *outputFile, err)
177 os.Exit(1)
178 }
179
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000180 if *outputFile != "-" {
181 err := os.WriteFile(*outputFile, obuf.Bytes(), 0666)
182 if err != nil {
183 fmt.Fprintf(os.Stderr, "could not write output to %q: %s\n", *outputFile, err)
184 os.Exit(1)
185 }
186 }
187
188 if *depsFile != "" {
189 err := deptools.WriteDepFile(*depsFile, *outputFile, deps)
190 if err != nil {
191 fmt.Fprintf(os.Stderr, "could not write deps to %q: %s\n", *depsFile, err)
192 os.Exit(1)
193 }
194 }
195 os.Exit(0)
196}
197
198type creationTimeGetter func() time.Time
199
200// actualTime returns current time in UTC
201func actualTime() time.Time {
202 return time.Now().UTC()
203}
204
205// replaceSlashes replaces "/" by "-" for the library path to be used for packages & files SPDXID
206func replaceSlashes(x string) string {
207 return strings.ReplaceAll(x, "/", "-")
208}
209
210// getPackageName returns a package name of a target Node
211func getPackageName(_ *context, tn *compliance.TargetNode) string {
212 return replaceSlashes(tn.Name())
213}
214
215// getDocumentName returns a package name of a target Node
216func getDocumentName(ctx *context, tn *compliance.TargetNode, pm *projectmetadata.ProjectMetadata) string {
217 if len(ctx.product) > 0 {
218 return replaceSlashes(ctx.product)
219 }
220 if len(tn.ModuleName()) > 0 {
221 if pm != nil {
222 return replaceSlashes(pm.Name() + ":" + tn.ModuleName())
223 }
224 return replaceSlashes(tn.ModuleName())
225 }
226
227 // TO DO: Replace tn.Name() with pm.Name() + parts of the target name
228 return replaceSlashes(tn.Name())
229}
230
231// getDownloadUrl returns the download URL if available (GIT, SVN, etc..),
232// or NOASSERTION if not available, none determined or ambiguous
233func getDownloadUrl(_ *context, pm *projectmetadata.ProjectMetadata) string {
234 if pm == nil {
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000235 return NOASSERTION
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000236 }
237
238 urlsByTypeName := pm.UrlsByTypeName()
239 if urlsByTypeName == nil {
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000240 return NOASSERTION
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000241 }
242
243 url := urlsByTypeName.DownloadUrl()
244 if url == "" {
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000245 return NOASSERTION
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000246 }
247 return url
248}
249
Ibrahim Kanouchea68ed082022-11-03 00:43:12 +0000250// getProjectMetadata returns the optimal project metadata for the target node
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000251func getProjectMetadata(_ *context, pmix *projectmetadata.Index,
252 tn *compliance.TargetNode) (*projectmetadata.ProjectMetadata, error) {
253 pms, err := pmix.MetadataForProjects(tn.Projects()...)
254 if err != nil {
255 return nil, fmt.Errorf("Unable to read projects for %q: %w\n", tn, err)
256 }
257 if len(pms) == 0 {
258 return nil, nil
259 }
260
Ibrahim Kanouchea68ed082022-11-03 00:43:12 +0000261 // Getting the project metadata that contains most of the info needed for sbomGenerator
262 score := -1
263 index := -1
264 for i := 0; i < len(pms); i++ {
265 tempScore := 0
266 if pms[i].Name() != "" {
267 tempScore += 1
268 }
269 if pms[i].Version() != "" {
270 tempScore += 1
271 }
272 if pms[i].UrlsByTypeName().DownloadUrl() != "" {
273 tempScore += 1
274 }
275
276 if tempScore == score {
277 if pms[i].Project() < pms[index].Project() {
278 index = i
279 }
280 } else if tempScore > score {
281 score = tempScore
282 index = i
283 }
284 }
285 return pms[index], nil
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000286}
287
Ibrahim Kanouche649b4d72022-11-12 05:46:12 +0000288// inputFiles returns the complete list of files read
289func inputFiles(lg *compliance.LicenseGraph, pmix *projectmetadata.Index, licenseTexts []string) []string {
290 projectMeta := pmix.AllMetadataFiles()
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000291 targets := lg.TargetNames()
Ibrahim Kanouche649b4d72022-11-12 05:46:12 +0000292 files := make([]string, 0, len(licenseTexts)+len(targets)+len(projectMeta))
293 files = append(files, licenseTexts...)
294 files = append(files, targets...)
295 files = append(files, projectMeta...)
296 return files
297}
298
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000299// sbomGenerator implements the spdx bom utility
300
301// SBOM is part of the new government regulation issued to improve national cyber security
302// and enhance software supply chain and transparency, see https://www.cisa.gov/sbom
303
304// sbomGenerator uses the SPDX standard, see the SPDX specification (https://spdx.github.io/spdx-spec/)
305// sbomGenerator is also following the internal google SBOM styleguide (http://goto.google.com/spdx-style-guide)
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000306func sbomGenerator(ctx *context, files ...string) (*spdx.Document, []string, error) {
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000307 // Must be at least one root file.
308 if len(files) < 1 {
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000309 return nil, nil, failNoneRequested
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000310 }
311
312 pmix := projectmetadata.NewIndex(ctx.rootFS)
313
314 lg, err := compliance.ReadLicenseGraph(ctx.rootFS, ctx.stderr, files)
315
316 if err != nil {
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000317 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 +0000318 }
319
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000320 // creating the packages section
321 pkgs := []*spdx.Package{}
322
323 // creating the relationship section
324 relationships := []*spdx.Relationship{}
325
326 // creating the license section
327 otherLicenses := []*spdx.OtherLicense{}
328
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000329 // implementing the licenses references for the packages
330 licenses := make(map[string]string)
331 concludedLicenses := func(licenseTexts []string) string {
332 licenseRefs := make([]string, 0, len(licenseTexts))
333 for _, licenseText := range licenseTexts {
334 license := strings.SplitN(licenseText, ":", 2)[0]
335 if _, ok := licenses[license]; !ok {
336 licenseRef := "LicenseRef-" + replaceSlashes(license)
337 licenses[license] = licenseRef
338 }
339
340 licenseRefs = append(licenseRefs, licenses[license])
341 }
342 if len(licenseRefs) > 1 {
343 return "(" + strings.Join(licenseRefs, " AND ") + ")"
344 } else if len(licenseRefs) == 1 {
345 return licenseRefs[0]
346 }
347 return "NONE"
348 }
349
350 isMainPackage := true
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000351 visitedNodes := make(map[*compliance.TargetNode]struct{})
352
353 // performing a Breadth-first top down walk of licensegraph and building package information
354 compliance.WalkTopDownBreadthFirst(nil, lg,
355 func(lg *compliance.LicenseGraph, tn *compliance.TargetNode, path compliance.TargetEdgePath) bool {
356 if err != nil {
357 return false
358 }
359 var pm *projectmetadata.ProjectMetadata
360 pm, err = getProjectMetadata(ctx, pmix, tn)
361 if err != nil {
362 return false
363 }
364
365 if isMainPackage {
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000366 *mainPkgName = replaceSlashes(getPackageName(ctx, tn))
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000367 isMainPackage = false
368 }
369
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000370 if len(path) == 0 {
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000371 // Add the describe relationship for the main package
372 rln := &spdx.Relationship{
373 RefA: common.MakeDocElementID("" /* this document */, "DOCUMENT"),
374 RefB: common.MakeDocElementID("", *mainPkgName),
375 Relationship: "DESCRIBES",
376 }
377 relationships = append(relationships, rln)
378
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000379 } else {
380 // Check parent and identify annotation
381 parent := path[len(path)-1]
382 targetEdge := parent.Edge()
383 if targetEdge.IsRuntimeDependency() {
384 // Adding the dynamic link annotation RUNTIME_DEPENDENCY_OF relationship
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000385 rln := &spdx.Relationship{
386 RefA: common.MakeDocElementID("", replaceSlashes(getPackageName(ctx, tn))),
387 RefB: common.MakeDocElementID("", replaceSlashes(getPackageName(ctx, targetEdge.Target()))),
388 Relationship: "RUNTIME_DEPENDENCY_OF",
389 }
390 relationships = append(relationships, rln)
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000391
392 } else if targetEdge.IsDerivation() {
393 // Adding the derivation annotation as a CONTAINS relationship
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000394 rln := &spdx.Relationship{
395 RefA: common.MakeDocElementID("", replaceSlashes(getPackageName(ctx, targetEdge.Target()))),
396 RefB: common.MakeDocElementID("", replaceSlashes(getPackageName(ctx, tn))),
397 Relationship: "CONTAINS",
398 }
399 relationships = append(relationships, rln)
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000400
401 } else if targetEdge.IsBuildTool() {
402 // Adding the toolchain annotation as a BUILD_TOOL_OF relationship
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000403 rln := &spdx.Relationship{
404 RefA: common.MakeDocElementID("", replaceSlashes(getPackageName(ctx, tn))),
405 RefB: common.MakeDocElementID("", replaceSlashes(getPackageName(ctx, targetEdge.Target()))),
406 Relationship: "BUILD_TOOL_OF",
407 }
408 relationships = append(relationships, rln)
409
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000410 } else {
411 panic(fmt.Errorf("Unknown dependency type: %v", targetEdge.Annotations()))
412 }
413 }
414
415 if _, alreadyVisited := visitedNodes[tn]; alreadyVisited {
416 return false
417 }
418 visitedNodes[tn] = struct{}{}
419 pkgName := getPackageName(ctx, tn)
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000420
421 // Making an spdx package and adding it to pkgs
422 pkg := &spdx.Package{
423 PackageName: replaceSlashes(pkgName),
424 PackageDownloadLocation: getDownloadUrl(ctx, pm),
425 PackageSPDXIdentifier: common.ElementID(replaceSlashes(pkgName)),
426 PackageLicenseConcluded: concludedLicenses(tn.LicenseTexts()),
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000427 }
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000428
429 if pm != nil && pm.Version() != "" {
430 pkg.PackageVersion = pm.Version()
431 } else {
432 pkg.PackageVersion = NOASSERTION
433 }
434
435 pkgs = append(pkgs, pkg)
436
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000437 return true
438 })
439
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000440 // Adding Non-standard licenses
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000441
442 licenseTexts := make([]string, 0, len(licenses))
443
444 for licenseText := range licenses {
445 licenseTexts = append(licenseTexts, licenseText)
446 }
447
448 sort.Strings(licenseTexts)
449
450 for _, licenseText := range licenseTexts {
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000451 // open the file
452 f, err := ctx.rootFS.Open(filepath.Clean(licenseText))
453 if err != nil {
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000454 return nil, nil, fmt.Errorf("error opening license text file %q: %w", licenseText, err)
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000455 }
456
457 // read the file
458 text, err := io.ReadAll(f)
459 if err != nil {
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000460 return nil, nil, fmt.Errorf("error reading license text file %q: %w", licenseText, err)
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000461 }
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000462 // Making an spdx License and adding it to otherLicenses
463 otherLicenses = append(otherLicenses, &spdx.OtherLicense{
464 LicenseName: strings.Replace(licenses[licenseText], "LicenseRef-", "", -1),
465 LicenseIdentifier: string(licenses[licenseText]),
466 ExtractedText: string(text),
467 })
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000468 }
469
Ibrahim Kanouche649b4d72022-11-12 05:46:12 +0000470 deps := inputFiles(lg, pmix, licenseTexts)
471 sort.Strings(deps)
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000472
473 // Making the SPDX doc
474 ci, err := builder2v2.BuildCreationInfoSection2_2("Organization", "Google LLC", nil)
475 if err != nil {
476 return nil, nil, fmt.Errorf("Unable to build creation info section for SPDX doc: %v\n", err)
477 }
478
479 return &spdx.Document{
480 SPDXIdentifier: "DOCUMENT",
481 CreationInfo: ci,
482 Packages: pkgs,
483 Relationships: relationships,
484 OtherLicenses: otherLicenses,
485 }, deps, nil
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000486}