blob: e03105289f61e55b7f6a05d87e4bb5b350d9e227 [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"
34)
35
36var (
37 failNoneRequested = fmt.Errorf("\nNo license metadata files requested")
38 failNoLicenses = fmt.Errorf("No licenses found")
39)
40
41type context struct {
42 stdout io.Writer
43 stderr io.Writer
44 rootFS fs.FS
45 product string
46 stripPrefix []string
47 creationTime creationTimeGetter
48}
49
50func (ctx context) strip(installPath string) string {
51 for _, prefix := range ctx.stripPrefix {
52 if strings.HasPrefix(installPath, prefix) {
53 p := strings.TrimPrefix(installPath, prefix)
54 if 0 == len(p) {
55 p = ctx.product
56 }
57 if 0 == len(p) {
58 continue
59 }
60 return p
61 }
62 }
63 return installPath
64}
65
66// newMultiString creates a flag that allows multiple values in an array.
67func newMultiString(flags *flag.FlagSet, name, usage string) *multiString {
68 var f multiString
69 flags.Var(&f, name, usage)
70 return &f
71}
72
73// multiString implements the flag `Value` interface for multiple strings.
74type multiString []string
75
76func (ms *multiString) String() string { return strings.Join(*ms, ", ") }
77func (ms *multiString) Set(s string) error { *ms = append(*ms, s); return nil }
78
79func main() {
80 var expandedArgs []string
81 for _, arg := range os.Args[1:] {
82 if strings.HasPrefix(arg, "@") {
83 f, err := os.Open(strings.TrimPrefix(arg, "@"))
84 if err != nil {
85 fmt.Fprintln(os.Stderr, err.Error())
86 os.Exit(1)
87 }
88
89 respArgs, err := response.ReadRspFile(f)
90 f.Close()
91 if err != nil {
92 fmt.Fprintln(os.Stderr, err.Error())
93 os.Exit(1)
94 }
95 expandedArgs = append(expandedArgs, respArgs...)
96 } else {
97 expandedArgs = append(expandedArgs, arg)
98 }
99 }
100
101 flags := flag.NewFlagSet("flags", flag.ExitOnError)
102
103 flags.Usage = func() {
104 fmt.Fprintf(os.Stderr, `Usage: %s {options} file.meta_lic {file.meta_lic...}
105
106Outputs an SBOM.spdx.
107
108Options:
109`, filepath.Base(os.Args[0]))
110 flags.PrintDefaults()
111 }
112
113 outputFile := flags.String("o", "-", "Where to write the SBOM spdx file. (default stdout)")
114 depsFile := flags.String("d", "", "Where to write the deps file")
115 product := flags.String("product", "", "The name of the product for which the notice is generated.")
116 stripPrefix := newMultiString(flags, "strip_prefix", "Prefix to remove from paths. i.e. path to root (multiple allowed)")
117
118 flags.Parse(expandedArgs)
119
120 // Must specify at least one root target.
121 if flags.NArg() == 0 {
122 flags.Usage()
123 os.Exit(2)
124 }
125
126 if len(*outputFile) == 0 {
127 flags.Usage()
128 fmt.Fprintf(os.Stderr, "must specify file for -o; use - for stdout\n")
129 os.Exit(2)
130 } else {
131 dir, err := filepath.Abs(filepath.Dir(*outputFile))
132 if err != nil {
133 fmt.Fprintf(os.Stderr, "cannot determine path to %q: %s\n", *outputFile, err)
134 os.Exit(1)
135 }
136 fi, err := os.Stat(dir)
137 if err != nil {
138 fmt.Fprintf(os.Stderr, "cannot read directory %q of %q: %s\n", dir, *outputFile, err)
139 os.Exit(1)
140 }
141 if !fi.IsDir() {
142 fmt.Fprintf(os.Stderr, "parent %q of %q is not a directory\n", dir, *outputFile)
143 os.Exit(1)
144 }
145 }
146
147 var ofile io.Writer
148 ofile = os.Stdout
149 var obuf *bytes.Buffer
150 if *outputFile != "-" {
151 obuf = &bytes.Buffer{}
152 ofile = obuf
153 }
154
155 ctx := &context{ofile, os.Stderr, compliance.FS, *product, *stripPrefix, actualTime}
156
157 deps, err := sbomGenerator(ctx, flags.Args()...)
158 if err != nil {
159 if err == failNoneRequested {
160 flags.Usage()
161 }
162 fmt.Fprintf(os.Stderr, "%s\n", err.Error())
163 os.Exit(1)
164 }
165
166 if *outputFile != "-" {
167 err := os.WriteFile(*outputFile, obuf.Bytes(), 0666)
168 if err != nil {
169 fmt.Fprintf(os.Stderr, "could not write output to %q: %s\n", *outputFile, err)
170 os.Exit(1)
171 }
172 }
173
174 if *depsFile != "" {
175 err := deptools.WriteDepFile(*depsFile, *outputFile, deps)
176 if err != nil {
177 fmt.Fprintf(os.Stderr, "could not write deps to %q: %s\n", *depsFile, err)
178 os.Exit(1)
179 }
180 }
181 os.Exit(0)
182}
183
184type creationTimeGetter func() time.Time
185
186// actualTime returns current time in UTC
187func actualTime() time.Time {
188 return time.Now().UTC()
189}
190
191// replaceSlashes replaces "/" by "-" for the library path to be used for packages & files SPDXID
192func replaceSlashes(x string) string {
193 return strings.ReplaceAll(x, "/", "-")
194}
195
196// getPackageName returns a package name of a target Node
197func getPackageName(_ *context, tn *compliance.TargetNode) string {
198 return replaceSlashes(tn.Name())
199}
200
201// getDocumentName returns a package name of a target Node
202func getDocumentName(ctx *context, tn *compliance.TargetNode, pm *projectmetadata.ProjectMetadata) string {
203 if len(ctx.product) > 0 {
204 return replaceSlashes(ctx.product)
205 }
206 if len(tn.ModuleName()) > 0 {
207 if pm != nil {
208 return replaceSlashes(pm.Name() + ":" + tn.ModuleName())
209 }
210 return replaceSlashes(tn.ModuleName())
211 }
212
213 // TO DO: Replace tn.Name() with pm.Name() + parts of the target name
214 return replaceSlashes(tn.Name())
215}
216
217// getDownloadUrl returns the download URL if available (GIT, SVN, etc..),
218// or NOASSERTION if not available, none determined or ambiguous
219func getDownloadUrl(_ *context, pm *projectmetadata.ProjectMetadata) string {
220 if pm == nil {
221 return "NOASSERTION"
222 }
223
224 urlsByTypeName := pm.UrlsByTypeName()
225 if urlsByTypeName == nil {
226 return "NOASSERTION"
227 }
228
229 url := urlsByTypeName.DownloadUrl()
230 if url == "" {
231 return "NOASSERTION"
232 }
233 return url
234}
235
Ibrahim Kanouchea68ed082022-11-03 00:43:12 +0000236// getProjectMetadata returns the optimal project metadata for the target node
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000237func getProjectMetadata(_ *context, pmix *projectmetadata.Index,
238 tn *compliance.TargetNode) (*projectmetadata.ProjectMetadata, error) {
239 pms, err := pmix.MetadataForProjects(tn.Projects()...)
240 if err != nil {
241 return nil, fmt.Errorf("Unable to read projects for %q: %w\n", tn, err)
242 }
243 if len(pms) == 0 {
244 return nil, nil
245 }
246
Ibrahim Kanouchea68ed082022-11-03 00:43:12 +0000247 // Getting the project metadata that contains most of the info needed for sbomGenerator
248 score := -1
249 index := -1
250 for i := 0; i < len(pms); i++ {
251 tempScore := 0
252 if pms[i].Name() != "" {
253 tempScore += 1
254 }
255 if pms[i].Version() != "" {
256 tempScore += 1
257 }
258 if pms[i].UrlsByTypeName().DownloadUrl() != "" {
259 tempScore += 1
260 }
261
262 if tempScore == score {
263 if pms[i].Project() < pms[index].Project() {
264 index = i
265 }
266 } else if tempScore > score {
267 score = tempScore
268 index = i
269 }
270 }
271 return pms[index], nil
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000272}
273
Ibrahim Kanouche649b4d72022-11-12 05:46:12 +0000274// inputFiles returns the complete list of files read
275func inputFiles(lg *compliance.LicenseGraph, pmix *projectmetadata.Index, licenseTexts []string) []string {
276 projectMeta := pmix.AllMetadataFiles()
277 targets := lg.TargetNames()
278 files := make([]string, 0, len(licenseTexts)+len(targets)+len(projectMeta))
279 files = append(files, licenseTexts...)
280 files = append(files, targets...)
281 files = append(files, projectMeta...)
282 return files
283}
284
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000285// sbomGenerator implements the spdx bom utility
286
287// SBOM is part of the new government regulation issued to improve national cyber security
288// and enhance software supply chain and transparency, see https://www.cisa.gov/sbom
289
290// sbomGenerator uses the SPDX standard, see the SPDX specification (https://spdx.github.io/spdx-spec/)
291// sbomGenerator is also following the internal google SBOM styleguide (http://goto.google.com/spdx-style-guide)
292func sbomGenerator(ctx *context, files ...string) ([]string, error) {
293 // Must be at least one root file.
294 if len(files) < 1 {
295 return nil, failNoneRequested
296 }
297
298 pmix := projectmetadata.NewIndex(ctx.rootFS)
299
300 lg, err := compliance.ReadLicenseGraph(ctx.rootFS, ctx.stderr, files)
301
302 if err != nil {
303 return nil, fmt.Errorf("Unable to read license text file(s) for %q: %v\n", files, err)
304 }
305
306 // implementing the licenses references for the packages
307 licenses := make(map[string]string)
308 concludedLicenses := func(licenseTexts []string) string {
309 licenseRefs := make([]string, 0, len(licenseTexts))
310 for _, licenseText := range licenseTexts {
311 license := strings.SplitN(licenseText, ":", 2)[0]
312 if _, ok := licenses[license]; !ok {
313 licenseRef := "LicenseRef-" + replaceSlashes(license)
314 licenses[license] = licenseRef
315 }
316
317 licenseRefs = append(licenseRefs, licenses[license])
318 }
319 if len(licenseRefs) > 1 {
320 return "(" + strings.Join(licenseRefs, " AND ") + ")"
321 } else if len(licenseRefs) == 1 {
322 return licenseRefs[0]
323 }
324 return "NONE"
325 }
326
327 isMainPackage := true
328 var mainPackage string
329 visitedNodes := make(map[*compliance.TargetNode]struct{})
330
331 // performing a Breadth-first top down walk of licensegraph and building package information
332 compliance.WalkTopDownBreadthFirst(nil, lg,
333 func(lg *compliance.LicenseGraph, tn *compliance.TargetNode, path compliance.TargetEdgePath) bool {
334 if err != nil {
335 return false
336 }
337 var pm *projectmetadata.ProjectMetadata
338 pm, err = getProjectMetadata(ctx, pmix, tn)
339 if err != nil {
340 return false
341 }
342
343 if isMainPackage {
344 mainPackage = getDocumentName(ctx, tn, pm)
345 fmt.Fprintf(ctx.stdout, "SPDXVersion: SPDX-2.2\n")
346 fmt.Fprintf(ctx.stdout, "DataLicense: CC-1.0\n")
347 fmt.Fprintf(ctx.stdout, "DocumentName: %s\n", mainPackage)
348 fmt.Fprintf(ctx.stdout, "SPDXID: SPDXRef-DOCUMENT-%s\n", mainPackage)
349 fmt.Fprintf(ctx.stdout, "DocumentNamespace: Android\n")
350 fmt.Fprintf(ctx.stdout, "Creator: Organization: Google LLC\n")
351 fmt.Fprintf(ctx.stdout, "Created: %s\n", ctx.creationTime().Format("2006-01-02T15:04:05Z"))
352 isMainPackage = false
353 }
354
355 relationships := make([]string, 0, 1)
356 defer func() {
357 if r := recover(); r != nil {
358 panic(r)
359 }
360 for _, relationship := range relationships {
361 fmt.Fprintln(ctx.stdout, relationship)
362 }
363 }()
364 if len(path) == 0 {
365 relationships = append(relationships,
366 fmt.Sprintf("Relationship: SPDXRef-DOCUMENT-%s DESCRIBES SPDXRef-Package-%s",
367 mainPackage, getPackageName(ctx, tn)))
368 } else {
369 // Check parent and identify annotation
370 parent := path[len(path)-1]
371 targetEdge := parent.Edge()
372 if targetEdge.IsRuntimeDependency() {
373 // Adding the dynamic link annotation RUNTIME_DEPENDENCY_OF relationship
374 relationships = append(relationships, fmt.Sprintf("Relationship: SPDXRef-Package-%s RUNTIME_DEPENDENCY_OF SPDXRef-Package-%s", getPackageName(ctx, tn), getPackageName(ctx, targetEdge.Target())))
375
376 } else if targetEdge.IsDerivation() {
377 // Adding the derivation annotation as a CONTAINS relationship
378 relationships = append(relationships, fmt.Sprintf("Relationship: SPDXRef-Package-%s CONTAINS SPDXRef-Package-%s", getPackageName(ctx, targetEdge.Target()), getPackageName(ctx, tn)))
379
380 } else if targetEdge.IsBuildTool() {
381 // Adding the toolchain annotation as a BUILD_TOOL_OF relationship
382 relationships = append(relationships, fmt.Sprintf("Relationship: SPDXRef-Package-%s BUILD_TOOL_OF SPDXRef-Package-%s", getPackageName(ctx, tn), getPackageName(ctx, targetEdge.Target())))
383 } else {
384 panic(fmt.Errorf("Unknown dependency type: %v", targetEdge.Annotations()))
385 }
386 }
387
388 if _, alreadyVisited := visitedNodes[tn]; alreadyVisited {
389 return false
390 }
391 visitedNodes[tn] = struct{}{}
392 pkgName := getPackageName(ctx, tn)
393 fmt.Fprintf(ctx.stdout, "##### Package: %s\n", strings.Replace(pkgName, "-", "/", -2))
394 fmt.Fprintf(ctx.stdout, "PackageName: %s\n", pkgName)
395 if pm != nil && pm.Version() != "" {
396 fmt.Fprintf(ctx.stdout, "PackageVersion: %s\n", pm.Version())
397 }
398 fmt.Fprintf(ctx.stdout, "SPDXID: SPDXRef-Package-%s\n", pkgName)
399 fmt.Fprintf(ctx.stdout, "PackageDownloadLocation: %s\n", getDownloadUrl(ctx, pm))
400 fmt.Fprintf(ctx.stdout, "PackageLicenseConcluded: %s\n", concludedLicenses(tn.LicenseTexts()))
401 return true
402 })
403
404 fmt.Fprintf(ctx.stdout, "##### Non-standard license:\n")
405
406 licenseTexts := make([]string, 0, len(licenses))
407
408 for licenseText := range licenses {
409 licenseTexts = append(licenseTexts, licenseText)
410 }
411
412 sort.Strings(licenseTexts)
413
414 for _, licenseText := range licenseTexts {
415 fmt.Fprintf(ctx.stdout, "LicenseID: %s\n", licenses[licenseText])
416 // open the file
417 f, err := ctx.rootFS.Open(filepath.Clean(licenseText))
418 if err != nil {
419 return nil, fmt.Errorf("error opening license text file %q: %w", licenseText, err)
420 }
421
422 // read the file
423 text, err := io.ReadAll(f)
424 if err != nil {
425 return nil, fmt.Errorf("error reading license text file %q: %w", licenseText, err)
426 }
427 // adding the extracted license text
428 fmt.Fprintf(ctx.stdout, "ExtractedText: <text>%v</text>\n", string(text))
429 }
430
Ibrahim Kanouche649b4d72022-11-12 05:46:12 +0000431 deps := inputFiles(lg, pmix, licenseTexts)
432 sort.Strings(deps)
Ibrahim Kanouchebedf1a82022-10-22 01:28:05 +0000433 return deps, nil
434}