Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to output to JSON #230

Merged
merged 6 commits into from
Nov 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ The following list supports various formats in which you can generate the SPDX S

- `spdx` (Default format)

- `JSON` (In progress)
- `JSON`

- `RDF` (In progress)

Expand Down
15 changes: 14 additions & 1 deletion cmd/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ package main
import (
"errors"
"os"
"strings"

log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"

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

const jsonLogFormat = "json"
Expand Down Expand Up @@ -49,6 +51,17 @@ func init() {
cobra.OnInitialize(setupLogger)
}

func parseOutputFormat(formatOption string) models.OutputFormat {
switch processedFormatOption := strings.ToLower(formatOption); processedFormatOption {
case "spdx":
return models.OutputFormatSpdx
case "json":
return models.OutputFormatJson
default:
return models.OutputFormatSpdx
}
}

func setupLogger() {
log.SetFormatter(&log.TextFormatter{
ForceColors: true,
Expand Down Expand Up @@ -84,7 +97,7 @@ func generate(cmd *cobra.Command, args []string) {
path := checkOpt("path")
outputDir := checkOpt("output-dir")
schema := checkOpt("schema")
format := checkOpt("format")
format := parseOutputFormat(checkOpt("format"))
license, err := cmd.Flags().GetBool("include-license-text")
if err != nil {
log.Fatalf("Failed to read command option: %v", err)
Expand Down
148 changes: 63 additions & 85 deletions pkg/format/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ type Format struct {

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

func init() {
Expand All @@ -45,129 +46,103 @@ func New(cfg Config) (Format, error) {
}, nil
}

// Build ...
// todo: refactor this Render into interface that different priting format can leverage
// move into go templates
// 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 := buildDocument(f.Config.ToolVersion, modules[0])
document, err := buildBaseDocument(f.Config.ToolVersion, modules[0])
if err != nil {
return err
}

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

file, err2 := os.Create(f.Config.Filename)
if err2 != nil {
return err2
file, err := os.Create(f.Config.Filename)
if err != nil {
return err
}
// todo organize file generation code below
//Print DOCUMENT
file.WriteString(fmt.Sprintf("SPDXVersion: %s\n", document.SPDXVersion))
file.WriteString(fmt.Sprintf("DataLicense: %s\n", document.DataLicense))
file.WriteString(fmt.Sprintf("SPDXID: %s\n", document.SPDXID))
file.WriteString(fmt.Sprintf("DocumentName: %s\n", document.DocumentName))
file.WriteString(fmt.Sprintf("DocumentNamespace: %s\n", document.DocumentNamespace))
file.WriteString(fmt.Sprintf("Creator: %s\n", document.Creator))
file.WriteString(fmt.Sprintf("Created: %v\n\n", document.Created))
//Print Package
for _, pkg := range packages {
file.WriteString(fmt.Sprintf("##### Package representing the %s\n\n", pkg.PackageName))
generatePackage(file, pkg)
if pkg.RootPackage {
file.WriteString(fmt.Sprintf("Relationship: %s DESCRIBES %s \n\n", document.SPDXID, pkg.SPDXID))
}
//Print DEPS ON
if len(pkg.DependsOn) > 0 {
for _, subPkg := range pkg.DependsOn {
file.WriteString(fmt.Sprintf("Relationship: %s DEPENDS_ON %s \n", pkg.SPDXID, subPkg.SPDXID))
}
file.WriteString("\n")
}

var spdxRenderer SPDXRenderer

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

//Print Other Licenses
if len(otherLicenses) > 0 {
file.WriteString("##### Non-standard license\n\n")
for lic := range otherLicenses {
file.WriteString(fmt.Sprintf("LicenseID: LicenseRef-%s\n", lic))
file.WriteString(fmt.Sprintf("ExtractedText: %s\n", otherLicenses[lic].ExtractedText))
file.WriteString(fmt.Sprintf("LicenseName: %s\n", otherLicenses[lic].Name))
file.WriteString(fmt.Sprintf("LicenseComment: %s\n\n", otherLicenses[lic].Comments))
}
outputBytes, err := spdxRenderer.RenderDocument(*document)
if err != nil {
return err
}

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

return nil
}

func generatePackage(file *os.File, pkg models.Package) {
file.WriteString(fmt.Sprintf("PackageName: %s\n", pkg.PackageName))
file.WriteString(fmt.Sprintf("SPDXID: %s\n", pkg.SPDXID))
if pkg.PackageVersion != "" {
file.WriteString(fmt.Sprintf("PackageVersion: %s\n", pkg.PackageVersion))
}

file.WriteString(fmt.Sprintf("PackageSupplier: %s\n", pkg.PackageSupplier))
file.WriteString(fmt.Sprintf("PackageDownloadLocation: %s\n", pkg.PackageDownloadLocation))
file.WriteString(fmt.Sprintf("FilesAnalyzed: %v\n", pkg.FilesAnalyzed))
if !strings.Contains(pkg.PackageChecksum, noAssertion) {
file.WriteString(fmt.Sprintf("PackageChecksum: %v\n", pkg.PackageChecksum))
}
file.WriteString(fmt.Sprintf("PackageHomePage: %v\n", pkg.PackageHomePage))
file.WriteString(fmt.Sprintf("PackageLicenseConcluded: %v\n", pkg.PackageLicenseConcluded))
file.WriteString(fmt.Sprintf("PackageLicenseDeclared: %v\n", pkg.PackageLicenseDeclared))
file.WriteString(fmt.Sprintf("PackageCopyrightText: %v\n", pkg.PackageCopyrightText))
file.WriteString(fmt.Sprintf("PackageLicenseComments: %v\n", pkg.PackageLicenseComments))
file.WriteString(fmt.Sprintf("PackageComment: %v\n\n", pkg.PackageComment))
}

func buildDocument(toolVersion string, module models.Module) (*models.Document, error) {
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),
Creator: fmt.Sprintf("Tool: spdx-sbom-generator-%s", toolVersion),
Created: time.Now().UTC().Format(time.RFC3339),
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) buildPackages(modules []models.Module) ([]models.Package, map[string]*models.License, error) {
packages := make([]models.Package, len(modules))
otherLicenses := map[string]*models.License{}
for i, module := range modules {
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 nil, nil, fmt.Errorf("failed to convert module %w", err)
return fmt.Errorf("failed to convert module %w", err)
}

subPackages := make([]models.Package, len(module.Modules))
idx := 0
for _, subMod := range module.Modules {
subPkg, err := f.convertToPackage(*subMod)
if err != nil {
return nil, nil, err
return fmt.Errorf("failed to convert submodule %w", err)
}
subPackages[idx] = subPkg
idx++
document.Relationships = append(document.Relationships, models.Relationship{
SPDXElementID: pkg.SPDXID,
RelatedSPDXElement: subPkg.SPDXID,
RelationshipType: "DEPENDS_ON",
})
}
pkg.DependsOn = subPackages
for l := range module.OtherLicense {
otherLicenses[module.OtherLicense[l].ID] = module.OtherLicense[l]
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,
})
}
packages[i] = pkg
document.Packages = append(document.Packages, pkg)
}

return packages, otherLicenses, nil
return nil
}

// WIP
Expand All @@ -179,7 +154,10 @@ func (f *Format) convertToPackage(module models.Module) (models.Package, error)
PackageSupplier: setPkgValue(module.Supplier.Get()),
PackageDownloadLocation: setPkgValue(module.PackageDownloadLocation),
FilesAnalyzed: false,
PackageChecksum: module.CheckSum.String(),
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),
Expand Down
21 changes: 21 additions & 0 deletions pkg/format/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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
}
77 changes: 77 additions & 0 deletions pkg/format/tag_value.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 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
}
Loading