blob: 3cdfa0a889ceecc259456c66338180c95c37fc6e [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"
Ibrahim Kanouchef89fc4a2023-04-03 20:15:14 +000019 "crypto/sha1"
20 "encoding/hex"
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +000021 "flag"
22 "fmt"
23 "io"
24 "io/fs"
25 "os"
26 "path/filepath"
27 "sort"
28 "strings"
29 "time"
30
31 "android/soong/response"
32 "android/soong/tools/compliance"
33 "android/soong/tools/compliance/projectmetadata"
34
35 "github.com/google/blueprint/deptools"
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +000036
37 "github.com/spdx/tools-golang/builder/builder2v2"
38 "github.com/spdx/tools-golang/json"
39 "github.com/spdx/tools-golang/spdx/common"
40 spdx "github.com/spdx/tools-golang/spdx/v2_2"
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +000041)
42
43var (
44 failNoneRequested = fmt.Errorf("\nNo license metadata files requested")
45 failNoLicenses = fmt.Errorf("No licenses found")
46)
47
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +000048const NOASSERTION = "NOASSERTION"
49
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +000050type context struct {
51 stdout io.Writer
52 stderr io.Writer
53 rootFS fs.FS
54 product string
55 stripPrefix []string
56 creationTime creationTimeGetter
57}
58
59func (ctx context) strip(installPath string) string {
60 for _, prefix := range ctx.stripPrefix {
61 if strings.HasPrefix(installPath, prefix) {
62 p := strings.TrimPrefix(installPath, prefix)
63 if 0 == len(p) {
64 p = ctx.product
65 }
66 if 0 == len(p) {
67 continue
68 }
69 return p
70 }
71 }
72 return installPath
73}
74
75// newMultiString creates a flag that allows multiple values in an array.
76func newMultiString(flags *flag.FlagSet, name, usage string) *multiString {
77 var f multiString
78 flags.Var(&f, name, usage)
79 return &f
80}
81
82// multiString implements the flag `Value` interface for multiple strings.
83type multiString []string
84
85func (ms *multiString) String() string { return strings.Join(*ms, ", ") }
86func (ms *multiString) Set(s string) error { *ms = append(*ms, s); return nil }
87
88func main() {
89 var expandedArgs []string
90 for _, arg := range os.Args[1:] {
91 if strings.HasPrefix(arg, "@") {
92 f, err := os.Open(strings.TrimPrefix(arg, "@"))
93 if err != nil {
94 fmt.Fprintln(os.Stderr, err.Error())
95 os.Exit(1)
96 }
97
98 respArgs, err := response.ReadRspFile(f)
99 f.Close()
100 if err != nil {
101 fmt.Fprintln(os.Stderr, err.Error())
102 os.Exit(1)
103 }
104 expandedArgs = append(expandedArgs, respArgs...)
105 } else {
106 expandedArgs = append(expandedArgs, arg)
107 }
108 }
109
110 flags := flag.NewFlagSet("flags", flag.ExitOnError)
111
112 flags.Usage = func() {
113 fmt.Fprintf(os.Stderr, `Usage: %s {options} file.meta_lic {file.meta_lic...}
114
115Outputs an SBOM.spdx.
116
117Options:
118`, filepath.Base(os.Args[0]))
119 flags.PrintDefaults()
120 }
121
122 outputFile := flags.String("o", "-", "Where to write the SBOM spdx file. (default stdout)")
123 depsFile := flags.String("d", "", "Where to write the deps file")
124 product := flags.String("product", "", "The name of the product for which the notice is generated.")
125 stripPrefix := newMultiString(flags, "strip_prefix", "Prefix to remove from paths. i.e. path to root (multiple allowed)")
126
127 flags.Parse(expandedArgs)
128
129 // Must specify at least one root target.
130 if flags.NArg() == 0 {
131 flags.Usage()
132 os.Exit(2)
133 }
134
135 if len(*outputFile) == 0 {
136 flags.Usage()
137 fmt.Fprintf(os.Stderr, "must specify file for -o; use - for stdout\n")
138 os.Exit(2)
139 } else {
140 dir, err := filepath.Abs(filepath.Dir(*outputFile))
141 if err != nil {
142 fmt.Fprintf(os.Stderr, "cannot determine path to %q: %s\n", *outputFile, err)
143 os.Exit(1)
144 }
145 fi, err := os.Stat(dir)
146 if err != nil {
147 fmt.Fprintf(os.Stderr, "cannot read directory %q of %q: %s\n", dir, *outputFile, err)
148 os.Exit(1)
149 }
150 if !fi.IsDir() {
151 fmt.Fprintf(os.Stderr, "parent %q of %q is not a directory\n", dir, *outputFile)
152 os.Exit(1)
153 }
154 }
155
156 var ofile io.Writer
157 ofile = os.Stdout
158 var obuf *bytes.Buffer
159 if *outputFile != "-" {
160 obuf = &bytes.Buffer{}
161 ofile = obuf
162 }
163
164 ctx := &context{ofile, os.Stderr, compliance.FS, *product, *stripPrefix, actualTime}
165
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000166 spdxDoc, deps, err := sbomGenerator(ctx, flags.Args()...)
167
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000168 if err != nil {
169 if err == failNoneRequested {
170 flags.Usage()
171 }
172 fmt.Fprintf(os.Stderr, "%s\n", err.Error())
173 os.Exit(1)
174 }
175
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000176 if err := spdx_json.Save2_2(spdxDoc, ofile); err != nil {
177 fmt.Fprintf(os.Stderr, "failed to write document to %v: %v", *outputFile, err)
178 os.Exit(1)
179 }
180
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000181 if *outputFile != "-" {
182 err := os.WriteFile(*outputFile, obuf.Bytes(), 0666)
183 if err != nil {
184 fmt.Fprintf(os.Stderr, "could not write output to %q: %s\n", *outputFile, err)
185 os.Exit(1)
186 }
187 }
188
189 if *depsFile != "" {
190 err := deptools.WriteDepFile(*depsFile, *outputFile, deps)
191 if err != nil {
192 fmt.Fprintf(os.Stderr, "could not write deps to %q: %s\n", *depsFile, err)
193 os.Exit(1)
194 }
195 }
196 os.Exit(0)
197}
198
Ibrahim Kanouchef89fc4a2023-04-03 20:15:14 +0000199type creationTimeGetter func() string
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000200
201// actualTime returns current time in UTC
Ibrahim Kanouchef89fc4a2023-04-03 20:15:14 +0000202func actualTime() string {
203 t := time.Now().UTC()
204 return t.UTC().Format("2006-01-02T15:04:05Z")
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000205}
206
207// replaceSlashes replaces "/" by "-" for the library path to be used for packages & files SPDXID
208func replaceSlashes(x string) string {
209 return strings.ReplaceAll(x, "/", "-")
210}
211
Ibrahim Kanouchef89fc4a2023-04-03 20:15:14 +0000212// stripDocName removes the outdir prefix and meta_lic suffix from a target Name
213func stripDocName(name string) string {
214 // remove outdir prefix
215 if strings.HasPrefix(name, "out/") {
216 name = name[4:]
217 }
218
219 // remove suffix
220 if strings.HasSuffix(name, ".meta_lic") {
221 name = name[:len(name)-9]
222 } else if strings.HasSuffix(name, "/meta_lic") {
223 name = name[:len(name)-9] + "/"
224 }
225
226 return name
227}
228
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000229// getPackageName returns a package name of a target Node
230func getPackageName(_ *context, tn *compliance.TargetNode) string {
231 return replaceSlashes(tn.Name())
232}
233
234// getDocumentName returns a package name of a target Node
235func getDocumentName(ctx *context, tn *compliance.TargetNode, pm *projectmetadata.ProjectMetadata) string {
236 if len(ctx.product) > 0 {
237 return replaceSlashes(ctx.product)
238 }
239 if len(tn.ModuleName()) > 0 {
240 if pm != nil {
241 return replaceSlashes(pm.Name() + ":" + tn.ModuleName())
242 }
243 return replaceSlashes(tn.ModuleName())
244 }
245
Ibrahim Kanouchef89fc4a2023-04-03 20:15:14 +0000246 return stripDocName(replaceSlashes(tn.Name()))
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000247}
248
249// getDownloadUrl returns the download URL if available (GIT, SVN, etc..),
250// or NOASSERTION if not available, none determined or ambiguous
251func getDownloadUrl(_ *context, pm *projectmetadata.ProjectMetadata) string {
252 if pm == nil {
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000253 return NOASSERTION
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000254 }
255
256 urlsByTypeName := pm.UrlsByTypeName()
257 if urlsByTypeName == nil {
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000258 return NOASSERTION
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000259 }
260
261 url := urlsByTypeName.DownloadUrl()
262 if url == "" {
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000263 return NOASSERTION
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000264 }
265 return url
266}
267
Ibrahim Kanouchea68ed082022-11-03 00:43:12 +0000268// getProjectMetadata returns the optimal project metadata for the target node
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000269func getProjectMetadata(_ *context, pmix *projectmetadata.Index,
270 tn *compliance.TargetNode) (*projectmetadata.ProjectMetadata, error) {
271 pms, err := pmix.MetadataForProjects(tn.Projects()...)
272 if err != nil {
273 return nil, fmt.Errorf("Unable to read projects for %q: %w\n", tn, err)
274 }
275 if len(pms) == 0 {
276 return nil, nil
277 }
278
Ibrahim Kanouchea68ed082022-11-03 00:43:12 +0000279 // Getting the project metadata that contains most of the info needed for sbomGenerator
280 score := -1
281 index := -1
282 for i := 0; i < len(pms); i++ {
283 tempScore := 0
284 if pms[i].Name() != "" {
285 tempScore += 1
286 }
287 if pms[i].Version() != "" {
288 tempScore += 1
289 }
290 if pms[i].UrlsByTypeName().DownloadUrl() != "" {
291 tempScore += 1
292 }
293
294 if tempScore == score {
295 if pms[i].Project() < pms[index].Project() {
296 index = i
297 }
298 } else if tempScore > score {
299 score = tempScore
300 index = i
301 }
302 }
303 return pms[index], nil
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000304}
305
Ibrahim Kanouche649b4d72022-11-12 05:46:12 +0000306// inputFiles returns the complete list of files read
307func inputFiles(lg *compliance.LicenseGraph, pmix *projectmetadata.Index, licenseTexts []string) []string {
308 projectMeta := pmix.AllMetadataFiles()
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000309 targets := lg.TargetNames()
Ibrahim Kanouche649b4d72022-11-12 05:46:12 +0000310 files := make([]string, 0, len(licenseTexts)+len(targets)+len(projectMeta))
311 files = append(files, licenseTexts...)
312 files = append(files, targets...)
313 files = append(files, projectMeta...)
314 return files
315}
316
Ibrahim Kanouchef89fc4a2023-04-03 20:15:14 +0000317// generateSPDXNamespace generates a unique SPDX Document Namespace using a SHA1 checksum
318// and the CreationInfo.Created field as the date.
319func generateSPDXNamespace(created string) string {
320 // Compute a SHA1 checksum of the CreationInfo.Created field.
321 hash := sha1.Sum([]byte(created))
322 checksum := hex.EncodeToString(hash[:])
323
324 // Combine the checksum and timestamp to generate the SPDX Namespace.
325 namespace := fmt.Sprintf("SPDXRef-DOCUMENT-%s-%s", created, checksum)
326
327 return namespace
328}
329
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000330// sbomGenerator implements the spdx bom utility
331
332// SBOM is part of the new government regulation issued to improve national cyber security
333// and enhance software supply chain and transparency, see https://www.cisa.gov/sbom
334
335// sbomGenerator uses the SPDX standard, see the SPDX specification (https://spdx.github.io/spdx-spec/)
336// sbomGenerator is also following the internal google SBOM styleguide (http://goto.google.com/spdx-style-guide)
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000337func sbomGenerator(ctx *context, files ...string) (*spdx.Document, []string, error) {
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000338 // Must be at least one root file.
339 if len(files) < 1 {
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000340 return nil, nil, failNoneRequested
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000341 }
342
343 pmix := projectmetadata.NewIndex(ctx.rootFS)
344
345 lg, err := compliance.ReadLicenseGraph(ctx.rootFS, ctx.stderr, files)
346
347 if err != nil {
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000348 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 +0000349 }
350
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000351 // creating the packages section
352 pkgs := []*spdx.Package{}
353
354 // creating the relationship section
355 relationships := []*spdx.Relationship{}
356
357 // creating the license section
358 otherLicenses := []*spdx.OtherLicense{}
359
Ibrahim Kanouchef89fc4a2023-04-03 20:15:14 +0000360 // spdx document name
361 var docName string
362
363 // main package name
364 var mainPkgName string
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000365
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000366 // implementing the licenses references for the packages
367 licenses := make(map[string]string)
368 concludedLicenses := func(licenseTexts []string) string {
369 licenseRefs := make([]string, 0, len(licenseTexts))
370 for _, licenseText := range licenseTexts {
371 license := strings.SplitN(licenseText, ":", 2)[0]
372 if _, ok := licenses[license]; !ok {
373 licenseRef := "LicenseRef-" + replaceSlashes(license)
374 licenses[license] = licenseRef
375 }
376
377 licenseRefs = append(licenseRefs, licenses[license])
378 }
379 if len(licenseRefs) > 1 {
380 return "(" + strings.Join(licenseRefs, " AND ") + ")"
381 } else if len(licenseRefs) == 1 {
382 return licenseRefs[0]
383 }
384 return "NONE"
385 }
386
387 isMainPackage := true
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000388 visitedNodes := make(map[*compliance.TargetNode]struct{})
389
390 // performing a Breadth-first top down walk of licensegraph and building package information
391 compliance.WalkTopDownBreadthFirst(nil, lg,
392 func(lg *compliance.LicenseGraph, tn *compliance.TargetNode, path compliance.TargetEdgePath) bool {
393 if err != nil {
394 return false
395 }
396 var pm *projectmetadata.ProjectMetadata
397 pm, err = getProjectMetadata(ctx, pmix, tn)
398 if err != nil {
399 return false
400 }
401
402 if isMainPackage {
Ibrahim Kanouchef89fc4a2023-04-03 20:15:14 +0000403 docName = getDocumentName(ctx, tn, pm)
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000404 mainPkgName = replaceSlashes(getPackageName(ctx, tn))
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000405 isMainPackage = false
406 }
407
Bob Badour928ee9d2023-03-31 14:51:36 +0000408 if len(path) == 0 {
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000409 // Add the describe relationship for the main package
410 rln := &spdx.Relationship{
411 RefA: common.MakeDocElementID("" /* this document */, "DOCUMENT"),
412 RefB: common.MakeDocElementID("", mainPkgName),
413 Relationship: "DESCRIBES",
414 }
415 relationships = append(relationships, rln)
416
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000417 } else {
418 // Check parent and identify annotation
419 parent := path[len(path)-1]
420 targetEdge := parent.Edge()
421 if targetEdge.IsRuntimeDependency() {
422 // Adding the dynamic link annotation RUNTIME_DEPENDENCY_OF relationship
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000423 rln := &spdx.Relationship{
424 RefA: common.MakeDocElementID("", replaceSlashes(getPackageName(ctx, tn))),
425 RefB: common.MakeDocElementID("", replaceSlashes(getPackageName(ctx, targetEdge.Target()))),
426 Relationship: "RUNTIME_DEPENDENCY_OF",
427 }
428 relationships = append(relationships, rln)
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000429
430 } else if targetEdge.IsDerivation() {
431 // Adding the derivation annotation as a CONTAINS relationship
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000432 rln := &spdx.Relationship{
433 RefA: common.MakeDocElementID("", replaceSlashes(getPackageName(ctx, targetEdge.Target()))),
434 RefB: common.MakeDocElementID("", replaceSlashes(getPackageName(ctx, tn))),
435 Relationship: "CONTAINS",
436 }
437 relationships = append(relationships, rln)
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000438
439 } else if targetEdge.IsBuildTool() {
440 // Adding the toolchain annotation as a BUILD_TOOL_OF relationship
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000441 rln := &spdx.Relationship{
442 RefA: common.MakeDocElementID("", replaceSlashes(getPackageName(ctx, tn))),
443 RefB: common.MakeDocElementID("", replaceSlashes(getPackageName(ctx, targetEdge.Target()))),
444 Relationship: "BUILD_TOOL_OF",
445 }
446 relationships = append(relationships, rln)
447
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000448 } else {
449 panic(fmt.Errorf("Unknown dependency type: %v", targetEdge.Annotations()))
450 }
451 }
452
453 if _, alreadyVisited := visitedNodes[tn]; alreadyVisited {
454 return false
455 }
456 visitedNodes[tn] = struct{}{}
457 pkgName := getPackageName(ctx, tn)
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000458
459 // Making an spdx package and adding it to pkgs
460 pkg := &spdx.Package{
461 PackageName: replaceSlashes(pkgName),
462 PackageDownloadLocation: getDownloadUrl(ctx, pm),
463 PackageSPDXIdentifier: common.ElementID(replaceSlashes(pkgName)),
464 PackageLicenseConcluded: concludedLicenses(tn.LicenseTexts()),
Ibrahim Kanouchee97adc52023-03-20 16:42:09 +0000465 }
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000466
467 if pm != nil && pm.Version() != "" {
468 pkg.PackageVersion = pm.Version()
469 } else {
470 pkg.PackageVersion = NOASSERTION
471 }
472
473 pkgs = append(pkgs, pkg)
474
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000475 return true
476 })
477
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000478 // Adding Non-standard licenses
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000479
480 licenseTexts := make([]string, 0, len(licenses))
481
482 for licenseText := range licenses {
483 licenseTexts = append(licenseTexts, licenseText)
484 }
485
486 sort.Strings(licenseTexts)
487
488 for _, licenseText := range licenseTexts {
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000489 // open the file
490 f, err := ctx.rootFS.Open(filepath.Clean(licenseText))
491 if err != nil {
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000492 return nil, nil, fmt.Errorf("error opening license text file %q: %w", licenseText, err)
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000493 }
494
495 // read the file
496 text, err := io.ReadAll(f)
497 if err != nil {
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000498 return nil, nil, fmt.Errorf("error reading license text file %q: %w", licenseText, err)
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000499 }
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000500 // Making an spdx License and adding it to otherLicenses
501 otherLicenses = append(otherLicenses, &spdx.OtherLicense{
502 LicenseName: strings.Replace(licenses[licenseText], "LicenseRef-", "", -1),
503 LicenseIdentifier: string(licenses[licenseText]),
504 ExtractedText: string(text),
505 })
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000506 }
507
Ibrahim Kanouche649b4d72022-11-12 05:46:12 +0000508 deps := inputFiles(lg, pmix, licenseTexts)
509 sort.Strings(deps)
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000510
511 // Making the SPDX doc
512 ci, err := builder2v2.BuildCreationInfoSection2_2("Organization", "Google LLC", nil)
513 if err != nil {
514 return nil, nil, fmt.Errorf("Unable to build creation info section for SPDX doc: %v\n", err)
515 }
516
Ibrahim Kanouchef89fc4a2023-04-03 20:15:14 +0000517 ci.Created = ctx.creationTime()
518
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000519 return &spdx.Document{
Ibrahim Kanouchef89fc4a2023-04-03 20:15:14 +0000520 SPDXVersion: "SPDX-2.2",
521 DataLicense: "CC0-1.0",
522 SPDXIdentifier: "DOCUMENT",
523 DocumentName: docName,
524 DocumentNamespace: generateSPDXNamespace(ci.Created),
525 CreationInfo: ci,
526 Packages: pkgs,
527 Relationships: relationships,
528 OtherLicenses: otherLicenses,
Ibrahim Kanouche91f2f9d2023-04-01 05:05:32 +0000529 }, deps, nil
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000530}