github.com/vchain-us/vcn@v0.9.11-0.20210921212052-a2484d23c0b3/pkg/bom/spdx.go (about)

     1  /*
     2   * Copyright (c) 2021 CodeNotary, Inc. All Rights Reserved.
     3   * This software is released under GPL3.
     4   * The full license information can be found under:
     5   * https://www.gnu.org/licenses/gpl-3.0.en.html
     6   *
     7   */
     8  
     9  package bom
    10  
    11  import (
    12  	"fmt"
    13  	"os"
    14  	"path/filepath"
    15  	"strconv"
    16  	"time"
    17  
    18  	"github.com/google/uuid"
    19  
    20  	"github.com/vchain-us/vcn/pkg/bom/artifact"
    21  )
    22  
    23  const (
    24  	optional = iota
    25  	mandatory
    26  )
    27  
    28  type headerLine struct {
    29  	tag      string
    30  	presense int
    31  	fn       func(artifact.Artifact) (string, error)
    32  }
    33  
    34  var headerContent = []headerLine{
    35  	{"SPDXVersion", mandatory, func(artifact.Artifact) (string, error) {
    36  		return "SPDX-2.2", nil
    37  	}},
    38  	{"DataLicense", mandatory, func(artifact.Artifact) (string, error) {
    39  		return "CC0-1.0", nil
    40  	}},
    41  	{"SPDXID", mandatory, func(artifact.Artifact) (string, error) {
    42  		return "SPDXRef-DOCUMENT", nil
    43  	}},
    44  	{"DocumentName", mandatory, func(p artifact.Artifact) (string, error) {
    45  		path, err := filepath.Abs(p.Path())
    46  		if err != nil {
    47  			return "", err
    48  		}
    49  		return filepath.Base(path), nil
    50  	}},
    51  	{"DocumentNamespace", mandatory, func(p artifact.Artifact) (string, error) {
    52  		path, err := filepath.Abs(p.Path())
    53  		if err != nil {
    54  			return "", err
    55  		}
    56  		return "http://spdx.org/spdxdocs/" + filepath.Base(path) + "-" + uuid.NewString(), nil
    57  	}},
    58  	{"Creator", mandatory, func(artifact.Artifact) (string, error) {
    59  		return "Tool: CodeNotary vcn", nil
    60  	}},
    61  	{"Created", mandatory, func(artifact.Artifact) (string, error) {
    62  		return time.Now().UTC().Format(time.RFC3339), nil
    63  	}},
    64  }
    65  
    66  type componentLine struct {
    67  	tag      string
    68  	presense int
    69  	fn       func(artifact.Artifact, artifact.Dependency, int) (string, error)
    70  }
    71  
    72  var componentContent = []componentLine{
    73  	{"PackageName", mandatory, func(a artifact.Artifact, d artifact.Dependency, seq int) (string, error) {
    74  		return d.Name, nil
    75  	}},
    76  	{"SPDXID", mandatory, func(a artifact.Artifact, d artifact.Dependency, seq int) (string, error) {
    77  		return "SPDXRef-Package-" + strconv.Itoa(seq), nil
    78  	}},
    79  	{"PackageVersion", mandatory, func(a artifact.Artifact, d artifact.Dependency, seq int) (string, error) {
    80  		return d.Version, nil
    81  	}},
    82  	{"PackageDownloadLocation", mandatory, noAssertion},
    83  	// FilesAnalysed is optional, but by default it is true, which requires presence of many other fields
    84  	{"FilesAnalyzed", mandatory, func(a artifact.Artifact, d artifact.Dependency, seq int) (string, error) {
    85  		return "false", nil
    86  	}},
    87  	{"PackageChecksum", mandatory, func(a artifact.Artifact, d artifact.Dependency, seq int) (string, error) {
    88  		return artifact.HashTypeName(d.HashType) + ": " + d.Hash, nil
    89  	}},
    90  	{"PackageSourceInfo", optional, func(a artifact.Artifact, d artifact.Dependency, seq int) (string, error) {
    91  		return "<text>" + Purl(a, d) + "</text>", nil
    92  	}},
    93  	{"PackageLicenseConcluded", mandatory, func(a artifact.Artifact, d artifact.Dependency, seq int) (string, error) {
    94  		if d.License != "" {
    95  			return d.License, nil
    96  		}
    97  		return "NOASSERTION", nil
    98  	}},
    99  	{"PackageLicenseDeclared", mandatory, noAssertion},
   100  	{"PackageCopyrightText", mandatory, noAssertion},
   101  	{"PackageComment", optional, func(a artifact.Artifact, d artifact.Dependency, seq int) (string, error) {
   102  		l := artifact.TrustLevelName(d.TrustLevel)
   103  		if l != "" {
   104  			return "<text>" + artifact.TrustLevelName(d.TrustLevel) + ", " + DepLinkType(a, d) + "</text>", nil
   105  		}
   106  		return "", nil
   107  	}},
   108  }
   109  
   110  func noAssertion(a artifact.Artifact, d artifact.Dependency, seq int) (string, error) {
   111  	return "NOASSERTION", nil
   112  }
   113  
   114  // Output info about package and its components in SPDX text (tag:value) format, according to
   115  // SPDX spec 2.2: https://spdx.dev/wp-content/uploads/sites/41/2020/08/SPDX-specification-2-2.pdf
   116  func OutputSpdxText(a artifact.Artifact, filename string) error {
   117  	deps := a.Dependencies()
   118  	f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
   119  	if err != nil {
   120  		return err
   121  	}
   122  
   123  	defer f.Close()
   124  
   125  	// SPDX header
   126  	for _, line := range headerContent {
   127  		value, err := line.fn(a)
   128  		if err != nil {
   129  			if line.presense == mandatory {
   130  				return fmt.Errorf("cannot get value for tag %s: %w", line.tag, err)
   131  			}
   132  			continue // optional tag - ignore error
   133  		}
   134  		if value == "" {
   135  			if line.presense == mandatory {
   136  				return fmt.Errorf("no value found for mandatory header tag %s", line.tag)
   137  			}
   138  			continue // optional
   139  		}
   140  		if _, err = fmt.Fprintf(f, "%s: %s\n", line.tag, value); err != nil {
   141  			return err
   142  		}
   143  	}
   144  
   145  	if _, err = fmt.Fprintf(f, "\n##### Software components\n\n"); err != nil {
   146  		return err
   147  	}
   148  
   149  	for i, dep := range deps {
   150  		for _, line := range componentContent {
   151  			value, err := line.fn(a, dep, i+1)
   152  			if err != nil {
   153  				if line.presense == mandatory {
   154  					return fmt.Errorf("cannot get value for tag %s for component %s: %w", line.tag, dep.Name, err)
   155  				}
   156  				continue // optional tag - ignore error
   157  			}
   158  			if value == "" {
   159  				if line.presense == mandatory {
   160  					return fmt.Errorf("no value found for mandatory component tag %s for component %s", line.tag, dep.Name)
   161  				}
   162  				continue // optional
   163  			}
   164  			if _, err = fmt.Fprintf(f, "%s: %s\n", line.tag, value); err != nil {
   165  				return err
   166  			}
   167  		}
   168  		if _, err = fmt.Fprintln(f); err != nil {
   169  			return err
   170  		}
   171  	}
   172  
   173  	return nil
   174  }