<|start_filename|>pkg/format/format.go<|end_filename|>
// SPDX-License-Identifier: Apache-2.0

package format

import (
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/go-git/go-git/v5"
	"github.com/google/uuid"

	"github.com/spdx/spdx-sbom-generator/pkg/models"
)

const (
	noAssertion = "NOASSERTION"
	httpPrefix  = "http"
)

var replacer *strings.Replacer

// Format ...
type Format struct {
	Config Config
}

// Config ...
type Config struct {
	ToolVersion  string
	Filename     string
	OutputFormat models.OutputFormat
	GetSource    func() []models.Module
}

func init() {
	replacers := []string{"/", ".", "_", "-"}
	replacer = strings.NewReplacer(replacers...)
}

// New ...
func New(cfg Config) (Format, error) {
	return Format{
		Config: cfg,
	}, nil
}

// SPDXRenderer is an interface that is to be implemented for every possible output format
type SPDXRenderer interface {
	RenderDocument(document models.Document) ([]byte, error)
}

// Render prepares and generates the final SPDX document in the specified format
func (f *Format) Render() error {
	modules := sortModules(f.Config.GetSource())
	document, err := buildBaseDocument(f.Config.ToolVersion, modules[0])
	if err != nil {
		return err
	}

	err = f.annotateDocumentWithPackages(modules, document)
	if err != nil {
		return err
	}

	file, err := os.Create(f.Config.Filename)
	if err != nil {
		return err
	}

	var spdxRenderer SPDXRenderer

	switch f.Config.OutputFormat {
	case models.OutputFormatSpdx:
		spdxRenderer = TagValueSPDXRenderer{}
	case models.OutputFormatJson:
		spdxRenderer = JsonSPDXRenderer{}
	}

	outputBytes, err := spdxRenderer.RenderDocument(*document)
	if err != nil {
		return err
	}

	// Write to file
	file.Write(outputBytes)
	file.Sync()

	return nil
}

func buildBaseDocument(toolVersion string, module models.Module) (*models.Document, error) {
	return &models.Document{
		SPDXVersion:       "SPDX-2.2",
		DataLicense:       "CC0-1.0",
		SPDXID:            "SPDXRef-DOCUMENT",
		DocumentName:      buildName(module.Name, module.Version),
		DocumentNamespace: buildNamespace(module.Name, module.Version),
		CreationInfo: models.CreationInfo{
			Creators: []string{fmt.Sprintf("Tool: spdx-sbom-generator-%s", toolVersion)},
			Created:  time.Now().UTC().Format(time.RFC3339),
		},
		Packages:                []models.Package{},
		Relationships:           []models.Relationship{},
		ExtractedLicensingInfos: []models.ExtractedLicensingInfo{},
	}, nil
}

// WIP
func (f *Format) annotateDocumentWithPackages(modules []models.Module, document *models.Document) error {
	for _, module := range modules {
		pkg, err := f.convertToPackage(module)
		if pkg.RootPackage {
			document.Relationships = append(document.Relationships, models.Relationship{
				SPDXElementID:      document.SPDXID,
				RelatedSPDXElement: pkg.SPDXID,
				RelationshipType:   "DESCRIBES",
			})
		}
		if err != nil {
			return fmt.Errorf("failed to convert module %w", err)
		}
		for _, subMod := range module.Modules {
			subPkg, err := f.convertToPackage(*subMod)
			if err != nil {
				return fmt.Errorf("failed to convert submodule %w", err)
			}
			document.Relationships = append(document.Relationships, models.Relationship{
				SPDXElementID:      pkg.SPDXID,
				RelatedSPDXElement: subPkg.SPDXID,
				RelationshipType:   "DEPENDS_ON",
			})
		}
		for licence := range module.OtherLicense {
			document.ExtractedLicensingInfos = append(document.ExtractedLicensingInfos, models.ExtractedLicensingInfo{
				LicenseID:      module.OtherLicense[licence].ID,
				ExtractedText:  module.OtherLicense[licence].ExtractedText,
				LicenseName:    module.OtherLicense[licence].Name,
				LicenseComment: module.OtherLicense[licence].Comments,
			})
		}
		document.Packages = append(document.Packages, pkg)
	}
	return nil
}

// WIP
func (f *Format) convertToPackage(module models.Module) (models.Package, error) {
	return models.Package{
		PackageName:             module.Name,
		SPDXID:                  setPkgSPDXID(module.Name, module.Version, module.Root),
		PackageVersion:          buildVersion(module),
		PackageSupplier:         setPkgValue(module.Supplier.Get()),
		PackageDownloadLocation: setPkgValue(module.PackageDownloadLocation),
		FilesAnalyzed:           false,
		PackageChecksums: []models.PackageChecksum{{
			Algorithm: module.CheckSum.Algorithm,
			Value:     module.CheckSum.String(),
		}},
		PackageHomePage:         buildHomepageURL(module.PackageURL),
		PackageLicenseConcluded: noAssertion, // setPkgValue(module.LicenseConcluded),
		PackageLicenseDeclared:  noAssertion, // setPkgValue(module.LicenseDeclared),
		PackageCopyrightText:    noAssertion, // setPkgValue(module.Copyright),
		PackageLicenseComments:  setPkgValue(""),
		PackageComment:          setPkgValue(""),
		RootPackage:             module.Root,
	}, nil
}

// todo: complete build package homepage rules
func buildHomepageURL(url string) string {
	if url == "" {
		return noAssertion
	}

	if strings.HasPrefix(url, httpPrefix) {
		return url
	}

	return fmt.Sprintf("https://%s", url)
}

func buildVersion(module models.Module) string {
	if module.Version != "" {
		return module.Version
	}

	if !module.Root {
		return module.Version
	}

	localGit, err := git.PlainOpen(module.LocalPath)
	if err != nil {
		return ""
	}

	head, err := localGit.Head()
	if err != nil {
		return ""
	}

	return head.Hash().String()[0:7]
}

func setPkgValue(s string) string {
	if s == "" {
		return noAssertion
	}

	return s
}

func setPkgSPDXID(s, v string, root bool) string {
	if root {
		return fmt.Sprintf("SPDXRef-Package-%s", replacer.Replace(s))
	}

	return fmt.Sprintf("SPDXRef-Package-%s-%s", replacer.Replace(s), v)
}

// todo: improve this logic
func sortModules(modules []models.Module) []models.Module {
	for i, m := range modules {
		if m.Root {
			modules = append(modules[:i], modules[i+1:]...)
			return append([]models.Module{m}, modules...)
		}
	}

	return modules
}

func buildNamespace(name, version string) string {
	uuid := uuid.New().String()
	if version == "" {
		return fmt.Sprintf("http://spdx.org/spdxpackages/%s-%s", name, uuid)
	}

	return fmt.Sprintf("http://spdx.org/spdxpackages/%s-%s-%s", name, version, uuid)
}

func buildName(name, version string) string {

	if version == "" {
		return name
	}

	return fmt.Sprintf("%s-%s", name, version)
}


<|start_filename|>pkg/format/tag_value.go<|end_filename|>
// SPDX-License-Identifier: Apache-2.0

package format

import (
	"bytes"
	"strings"
	"text/template"

	"github.com/spdx/spdx-sbom-generator/pkg/models"
)

// TagValueSPDXRenderer implements an SPDXRenderer that outputs JSON formatted SPDX documents
type TagValueSPDXRenderer struct{}

const tagValueTemplate = `SPDXVersion: {{ .SPDXVersion }}
DataLicense: {{ .DataLicense }}
SPDXID: {{ .SPDXID }}
DocumentName: {{ .DocumentName }}
DocumentNamespace: {{ .DocumentNamespace }}
Creator: {{ range .CreationInfo.Creators }}{{ . -}} {{ end }}
Created: {{ .CreationInfo.Created }}

{{ range .Packages }}
##### Package representing the {{.PackageName}}

PackageName: {{ .PackageName }}
SPDXID: {{ .SPDXID }}
{{ with .PackageVersion -}}
PackageVersion: {{ . }}
{{- end }}
PackageSupplier: {{ .PackageSupplier }}
PackageDownloadLocation: {{ .PackageDownloadLocation }}
FilesAnalyzed: {{ .FilesAnalyzed }}
{{- range .PackageChecksums }}
PackageChecksum: {{ .Algorithm }}: {{ .Value }}
{{- end }}
PackageHomePage: {{ .PackageHomePage }}
PackageLicenseConcluded: {{ .PackageLicenseConcluded }}
PackageLicenseDeclared: {{ .PackageLicenseDeclared }}
PackageCopyrightText: {{ .PackageCopyrightText }}
PackageLicenseComments: {{ .PackageLicenseComments }}
PackageComment: {{ .PackageComment }}
{{ end }}
{{- range .Relationships }}
Relationship: {{ .SPDXElementID }} {{ .RelationshipType }} {{ .RelatedSPDXElement }}
{{- end }}

{{- with .ExtractedLicensingInfos -}}
##### Non-standard license
{{ range . }}
LicenseID: {{ .LicenseID }}
ExtractedText: {{ .ExtractedText }}
LicenseName: {{ .LicenseName }}
LicenseComment: {{ .LicenseComment }}
{{- end -}}
{{- end -}}`

// RenderDocument uses golang templates to generated an SPDX tag value format output
func (t TagValueSPDXRenderer) RenderDocument(document models.Document) ([]byte, error) {
	tmpl := template.New("tagValue")
	tmpl, err := tmpl.Funcs(template.FuncMap{
		"isAsserted": func(s string) bool {
			return !strings.Contains(s, noAssertion)
		},
	}).Parse(tagValueTemplate)

	if err != nil {
		return nil, err
	}
	templateBuffer := new(bytes.Buffer)
	err = tmpl.Execute(templateBuffer, document)
	if err != nil {
		return nil, err
	}
	return templateBuffer.Bytes(), err
}


<|start_filename|>pkg/models/spdx.go<|end_filename|>
// SPDX-License-Identifier: Apache-2.0

package models

// Package
// JSON tags annotated from official example (https://github.com/spdx/spdx-spec/blob/v2.2.2/examples/SPDXJSONExample-v2.2.spdx.json)
// and official schema (https://github.com/spdx/spdx-spec/blob/v2.2.2/schemas/spdx-schema.json)
type Package struct {
	PackageName             string            `json:"name,omitempty"`
	SPDXID                  string            `json:"SPDXID,omitempty"`
	PackageVersion          string            `json:"versionInfo,omitempty"`
	PackageSupplier         string            `json:"supplier,omitempty"`
	PackageDownloadLocation string            `json:"downloadLocation,omitempty"`
	FilesAnalyzed           bool              `json:"filesAnalyzed"`
	PackageChecksums        []PackageChecksum `json:"checksums"`
	PackageHomePage         string            `json:"homepage,omitempty"`
	PackageLicenseConcluded string            `json:"licenseConcluded,omitempty"`
	PackageLicenseDeclared  string            `json:"licenseDeclared,omitempty"`
	PackageCopyrightText    string            `json:"copyrightText,omitempty"`
	PackageLicenseComments  string            `json:"licenseComments,omitempty"`
	PackageComment          string            `json:"comment,omitempty"`
	RootPackage             bool              `json:"-"`
}

// Document
// JSON tags annotated from official example (https://github.com/spdx/spdx-spec/blob/v2.2.2/examples/SPDXJSONExample-v2.2.spdx.json)
// and official schema (https://github.com/spdx/spdx-spec/blob/v2.2.2/schemas/spdx-schema.json
type Document struct {
	SPDXVersion             string                   `json:"spdxVersion,omitempty"`
	DataLicense             string                   `json:"dataLicense,omitempty"`
	SPDXID                  string                   `json:"SPDXID,omitempty"`
	DocumentName            string                   `json:"name,omitempty"`
	DocumentNamespace       string                   `json:"documentNamespace,omitempty"`
	CreationInfo            CreationInfo             `json:"creationInfo,omitempty"`
	Packages                []Package                `json:"packages,omitempty"`
	Relationships           []Relationship           `json:"relationships,omitempty"`
	ExtractedLicensingInfos []ExtractedLicensingInfo `json:"hasExtractedLicensingInfos,omitempty"`
}

// CreationInfo
// JSON tags annotated from official example (https://github.com/spdx/spdx-spec/blob/v2.2.2/examples/SPDXJSONExample-v2.2.spdx.json)
// and official schema (https://github.com/spdx/spdx-spec/blob/v2.2.2/schemas/spdx-schema.json
type CreationInfo struct {
	Comment            string   `json:"comment,omitempty"`
	Created            string   `json:"created,omitempty"`
	Creators           []string `json:"creators,omitempty"`
	LicenceListVersion string   `json:"licenseListVersion,omitempty"`
}

// Relationship
// JSON tags annotated from official example (https://github.com/spdx/spdx-spec/blob/v2.2.2/examples/SPDXJSONExample-v2.2.spdx.json)
// and official schema (https://github.com/spdx/spdx-spec/blob/v2.2.2/schemas/spdx-schema.json
type Relationship struct {
	SPDXElementID      string `json:"spdxElementId,omitempty"`
	RelatedSPDXElement string `json:"relatedSpdxElement,omitempty"`
	RelationshipType   string `json:"relationshipType,omitempty"`
}

// ExtractedLicensingInfo
// JSON tags annotated from official example (https://github.com/spdx/spdx-spec/blob/v2.2.2/examples/SPDXJSONExample-v2.2.spdx.json)
// and official schema (https://github.com/spdx/spdx-spec/blob/v2.2.2/schemas/spdx-schema.json
type ExtractedLicensingInfo struct {
	LicenseID      string `json:"licenseId,omitempty"`
	ExtractedText  string `json:"extractedText,omitempty"`
	LicenseName    string `json:"name,omitempty"`
	LicenseComment string `json:"comment,omitempty"`
}

// PackageChecksum
// JSON tags annotated from official example (https://github.com/spdx/spdx-spec/blob/v2.2.2/examples/SPDXJSONExample-v2.2.spdx.json)
// and official schema (https://github.com/spdx/spdx-spec/blob/v2.2.2/schemas/spdx-schema.json
type PackageChecksum struct {
	Algorithm HashAlgorithm `json:"algorithm"`
	Value     string        `json:"checksumValue"`
}


<|start_filename|>pkg/models/models.go<|end_filename|>
// SPDX-License-Identifier: Apache-2.0

package models

import (
	"crypto/sha1"
	"crypto/sha256"
	"crypto/sha512"
	"encoding/hex"
	"fmt"
	"hash"
	"strings"
)

// IPlugin ...
type IPlugin interface {
	SetRootModule(path string) error
	GetVersion() (string, error)
	GetMetadata() PluginMetadata
	GetRootModule(path string) (*Module, error)
	ListUsedModules(path string) ([]Module, error)
	ListModulesWithDeps(path string) ([]Module, error)
	IsValid(path string) bool
	HasModulesInstalled(path string) error
}

// PluginMetadata ...
type PluginMetadata struct {
	Name       string
	Slug       string
	Manifest   []string
	ModulePath []string
}

// Module ... ...
type Module struct {
	Version                 string `json:"Version,omitempty"`
	Name                    string
	Path                    string `json:"Path,omitempty"`
	LocalPath               string `json:"Dir,noempty"`
	Supplier                SupplierContact
	PackageURL              string
	CheckSum                *CheckSum
	PackageHomePage         string
	PackageDownloadLocation string
	LicenseConcluded        string
	LicenseDeclared         string
	CommentsLicense         string
	OtherLicense            []*License
	Copyright               string
	PackageComment          string
	Root                    bool
	Modules                 map[string]*Module
}

// SupplierContact ...
type SupplierContact struct {
	Type            TypeContact
	Name            string
	Email           string
	FuncGetSupplier func() string
}

// Get default supplier based on Name value or let each plugin build its own logic
func (s *SupplierContact) Get() string {
	if s.FuncGetSupplier != nil {
		return s.FuncGetSupplier()
	}

	if s.Name == "" {
		return ""
	}

	if s.Type == "" {
		s.Type = Organization
	}

	pkgSupplier := fmt.Sprintf("%s: %s", s.Type, s.Name)
	if !s.isEmptyEmail() {
		pkgSupplier += fmt.Sprintf(" (%s)", s.Email)
	}

	return pkgSupplier
}

func (s *SupplierContact) isEmptyEmail() bool {
	email := strings.ToLower(s.Email)
	return (len(s.Email) == 0) ||
		(strings.Compare(email, "none") == 0) ||
		(strings.Compare(email, "unknown") == 0)
}

// TypeContact ...
type TypeContact string

const (
	Person       TypeContact = "Person"
	Organization TypeContact = "Organization"
)

type CheckSum struct {
	Algorithm HashAlgorithm
	Content   []byte
	Value     string
}

func (c *CheckSum) String() string {
	if c.Value == "" {
		c.Value = c.calculateCheckSum(c.Content)
	}
	return c.Value
}

func (c *CheckSum) calculateCheckSum(content []byte) string {
	var h hash.Hash
	switch c.Algorithm {
	case HashAlgoSHA256:
		h = sha256.New()
	case HashAlgoSHA512:
		h = sha512.New()
	default:
		h = sha1.New()
	}
	h.Write(content)
	return hex.EncodeToString(h.Sum(nil))
}

// HashAlgorithm ...
type HashAlgorithm string

const (
	HashAlgoSHA1   HashAlgorithm = "SHA1"
	HashAlgoSHA224 HashAlgorithm = "SHA224"
	HashAlgoSHA256 HashAlgorithm = "SHA256"
	HashAlgoSHA384 HashAlgorithm = "SHA384"
	HashAlgoSHA512 HashAlgorithm = "SHA512"
	HashAlgoMD2    HashAlgorithm = "MD2"
	HashAlgoMD4    HashAlgorithm = "MD4"
	HashAlgoMD5    HashAlgorithm = "MD5"
	HashAlgoMD6    HashAlgorithm = "MD6"
)

// License ...
type License struct {
	ID            string
	Name          string
	ExtractedText string
	Comments      string
	File          string
}

// OutputFormat defines an int enum of supported output formats
type OutputFormat int

const (
	OutputFormatSpdx OutputFormat = iota
	OutputFormatJson
)


<|start_filename|>pkg/handler/spdx.go<|end_filename|>
// SPDX-License-Identifier: Apache-2.0

package handler

import (
	"errors"
	"fmt"
	"path/filepath"

	log "github.com/sirupsen/logrus"

	"github.com/spdx/spdx-sbom-generator/pkg/format"
	"github.com/spdx/spdx-sbom-generator/pkg/helper"
	"github.com/spdx/spdx-sbom-generator/pkg/models"
	"github.com/spdx/spdx-sbom-generator/pkg/modules"
)

var errNoModuleManagerFound = errors.New("No module manager found")
var errOutputDirDoesNotExist = errors.New("Output Directory does not exist")

// SPDXSettings ...
type SPDXSettings struct {
	Version   string
	Path      string
	License   bool
	Depth     string
	OutputDir string
	Schema    string
	Format    models.OutputFormat
}

type spdxHandler struct {
	config         SPDXSettings
	modulesManager []*modules.Manager
	format         format.Format
	outputFiles    map[string]string
	errors         map[string]error
}

// getFiletypeForOutputFormat gets the type suffix for the type of output chosen
func getFiletypeForOutputFormat(outputFormat models.OutputFormat) string {
	switch outputFormat {
	case models.OutputFormatSpdx:
		return "spdx"
	case models.OutputFormatJson:
		return "json"
	default:
		return "spdx"
	}
}

// NewSPDX ...
func NewSPDX(settings SPDXSettings) (Handler, error) {
	if !helper.Exists(settings.OutputDir) {
		return nil, errOutputDirDoesNotExist
	}

	mm, err := modules.New(modules.Config{
		Path: settings.Path,
	})
	if err != nil {
		return nil, err
	}

	return &spdxHandler{
		config:         settings,
		modulesManager: mm,
		outputFiles:    map[string]string{},
		errors:         map[string]error{},
	}, err
}

// Run ...
func (sh *spdxHandler) Run() error {
	if len(sh.modulesManager) == 0 {
		return errNoModuleManagerFound
	}

	for _, mm := range sh.modulesManager {
		plugin := mm.Plugin.GetMetadata()
		filename := fmt.Sprintf("bom-%s.%s", plugin.Slug, getFiletypeForOutputFormat(sh.config.Format))
		outputFile := filepath.Join(sh.config.OutputDir, filename)

		log.Infof("Running generator for Module Manager: `%s` with output `%s`", plugin.Slug, outputFile)
		if err := mm.Run(); err != nil {
			sh.errors[plugin.Slug] = err
			continue
		}

		format, err := format.New(format.Config{
			Filename:     outputFile,
			ToolVersion:  sh.config.Version,
			OutputFormat: sh.config.Format,
			GetSource: func() []models.Module {
				return mm.GetSource()
			},
		})
		if err != nil {
			sh.errors[plugin.Slug] = err
			continue
		}
		if err := format.Render(); err != nil {
			sh.errors[plugin.Slug] = err
			continue
		}
		sh.outputFiles[plugin.Slug] = outputFile
	}

	return nil
}

// Complete ...
func (sh *spdxHandler) Complete() error {
	if len(sh.errors) > 0 {
		log.Info("Command has completed with errors for some package managers, see details below")
		for plugin, err := range sh.errors {
			log.Infof("Plugin %s return error %v", plugin, err)
		}
	}

	if len(sh.outputFiles) > 0 {
		log.Info("Command completed successful for below package managers")
		for plugin, filepath := range sh.outputFiles {
			log.Infof("Plugin %s generated output at %s", plugin, filepath)
		}
	}
	return nil
}


<|start_filename|>pkg/format/json.go<|end_filename|>
// SPDX-License-Identifier: Apache-2.0

package format

import (
	"encoding/json"

	"github.com/spdx/spdx-sbom-generator/pkg/models"
)

// JsonSPDXRenderer implements an SPDXRenderer that outputs JSON formatted SPDX documents
type JsonSPDXRenderer struct{}

// RenderDocument uses golang JSON utilities to generated an indented output
func (j JsonSPDXRenderer) RenderDocument(document models.Document) ([]byte, error) {
	jsonBytes, err := json.MarshalIndent(document, "", "\t")
	if err != nil {
		return nil, err
	}
	return jsonBytes, err
}
