diff --git a/cmd/score.go b/cmd/score.go index bee039d0..2ac93c3c 100644 --- a/cmd/score.go +++ b/cmd/score.go @@ -49,6 +49,7 @@ type userCmd struct { json bool basic bool detailed bool + color bool // directory control recurse bool @@ -156,6 +157,7 @@ func toUserCmd(cmd *cobra.Command, args []string) *userCmd { uCmd.json, _ = cmd.Flags().GetBool("json") uCmd.basic, _ = cmd.Flags().GetBool("basic") uCmd.detailed, _ = cmd.Flags().GetBool("detailed") + uCmd.color, _ = cmd.Flags().GetBool("color") if reportFormat != "" { uCmd.json = strings.ToLower(reportFormat) == "json" @@ -177,6 +179,7 @@ func toEngineParams(uCmd *userCmd) *engine.Params { JSON: uCmd.json, Basic: uCmd.basic, Detailed: uCmd.detailed, + Color: uCmd.color, Recurse: uCmd.recurse, Debug: uCmd.debug, ConfigPath: uCmd.configPath, @@ -241,6 +244,7 @@ func init() { scoreCmd.Flags().BoolP("json", "j", false, "results in json") scoreCmd.Flags().BoolP("detailed", "d", false, "results in table format, default") scoreCmd.Flags().BoolP("basic", "b", false, "results in single line format") + scoreCmd.Flags().BoolP("color", "l", false, "output in colorful") // Debug Control scoreCmd.Flags().BoolP("debug", "D", false, "enable debug logging") diff --git a/go.mod b/go.mod index 44cdbc72..005ba921 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,8 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -36,6 +38,7 @@ require ( github.com/anchore/go-struct-converter v0.0.0-20240925125616-a0883641c664 // indirect github.com/cloudflare/circl v1.5.0 // indirect github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect + github.com/fatih/color v1.18.0 github.com/go-git/go-billy/v5 v5.6.0 github.com/google/go-querystring v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/go.sum b/go.sum index 2eb9705f..ab4e3707 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/github/go-spdx/v2 v2.3.2 h1:IfdyNHTqzs4zAJjXdVQfRnxt1XMfycXoHBE2Vsm1bjs= github.com/github/go-spdx/v2 v2.3.2/go.mod h1:2ZxKsOhvBp+OYBDlsGnUMcchLeo2mrpEBn2L1C+U3IQ= github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= @@ -43,6 +45,11 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -133,6 +140,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/pkg/compliance/bsi.go b/pkg/compliance/bsi.go index a2a3718a..71a9dcc0 100644 --- a/pkg/compliance/bsi.go +++ b/pkg/compliance/bsi.go @@ -83,7 +83,7 @@ const ( SBOM_VULNERABILITES ) -func bsiResult(ctx context.Context, doc sbom.Document, fileName string, outFormat string) { +func bsiResult(ctx context.Context, doc sbom.Document, fileName string, outFormat string, colorOutput bool) { log := logger.FromContext(ctx) log.Debug("compliance.bsiResult()") @@ -107,7 +107,7 @@ func bsiResult(ctx context.Context, doc sbom.Document, fileName string, outForma } if outFormat == "detailed" { - bsiDetailedReport(dtb, fileName) + bsiDetailedReport(dtb, fileName, colorOutput) } } diff --git a/pkg/compliance/bsi_report.go b/pkg/compliance/bsi_report.go index a154b1f5..259f85e5 100644 --- a/pkg/compliance/bsi_report.go +++ b/pkg/compliance/bsi_report.go @@ -21,6 +21,7 @@ import ( "time" "github.com/google/uuid" + "github.com/interlynk-io/sbomqs/pkg/compliance/common" db "github.com/interlynk-io/sbomqs/pkg/compliance/db" "github.com/olekukonko/tablewriter" "sigs.k8s.io/release-utils/version" @@ -173,7 +174,7 @@ func constructSections(dtb *db.DB) []bsiSection { return sortedSections } -func bsiDetailedReport(dtb *db.DB, fileName string) { +func bsiDetailedReport(dtb *db.DB, fileName string, colorOutput bool) { table := tablewriter.NewWriter(os.Stdout) score := bsiAggregateScore(dtb) @@ -189,9 +190,25 @@ func bsiDetailedReport(dtb *db.DB, fileName string) { for _, section := range sections { sectionID := section.ID if !section.Required { - sectionID = sectionID + "*" + sectionID += "*" + } + + if colorOutput { + // disable tablewriter's auto-wrapping + table.SetAutoWrapText(false) + columnWidth := 30 + common.SetHeaderColor(table, 5) + + table = common.ColorTable(table, + section.ElementID, + section.ID, + section.ElementResult, + section.DataField, + section.Score, + columnWidth) + } else { + table.Append([]string{section.ElementID, sectionID, section.DataField, section.ElementResult, fmt.Sprintf("%0.1f", section.Score)}) } - table.Append([]string{section.ElementID, sectionID, section.DataField, section.ElementResult, fmt.Sprintf("%0.1f", section.Score)}) } table.Render() } diff --git a/pkg/compliance/common/common.go b/pkg/compliance/common/common.go index 21c97cff..e27f205e 100644 --- a/pkg/compliance/common/common.go +++ b/pkg/compliance/common/common.go @@ -15,6 +15,7 @@ package common import ( + "fmt" "path" "strings" "time" @@ -25,6 +26,7 @@ import ( "github.com/interlynk-io/sbomqs/pkg/sbom" "github.com/interlynk-io/sbomqs/pkg/swhid" "github.com/interlynk-io/sbomqs/pkg/swid" + "github.com/olekukonko/tablewriter" "github.com/samber/lo" ) @@ -375,3 +377,74 @@ func IsComponentPartOfPrimaryDependency(primaryCompDeps []string, comp string) b } return false } + +func SetHeaderColor(table *tablewriter.Table, header int) { + colors := make([]tablewriter.Colors, header) + + // each column with same color and style + for i := 0; i < header; i++ { + colors[i] = tablewriter.Colors{tablewriter.FgHiWhiteColor, tablewriter.Bold} + } + + table.SetHeaderColor(colors...) +} + +func ColorTable(table *tablewriter.Table, elementID, id string, elementResult string, dataFields string, score float64, columnWidth int) *tablewriter.Table { + elementRe := wrapAndColoredContent(elementResult, columnWidth, tablewriter.FgHiCyanColor) + dataField := wrapAndColoredContent(dataFields, columnWidth, tablewriter.FgHiBlueColor) + + scoreColor := GetScoreColor(score) + + table.Rich([]string{ + elementID, + id, + dataField, + elementRe, + fmt.Sprintf("%0.1f", score), + }, []tablewriter.Colors{ + {tablewriter.FgHiMagentaColor, tablewriter.Bold}, + {tablewriter.FgHiCyanColor}, + {}, + {}, + scoreColor, + }) + return table +} + +// custom wrapping function to ensure consistent coloring instead of tablewritter's in-built wrapping +// 1. split content into multiple lines, each fitting within the specified width +// 2. each line of the content is formatted with color and bold styling using ANSI escape codes +// 3. wrapped lines are joined together with newline characters (\n) to maintain proper multi-line formatting. +func wrapAndColoredContent(content string, width int, color int) string { + words := strings.Fields(content) + var wrappedContent []string + var currentLine string + + for _, word := range words { + if len(currentLine)+len(word)+1 > width { + + // wrap the current line and color it + wrappedContent = append(wrappedContent, fmt.Sprintf("\033[%d;%dm%s\033[0m", 1, color, currentLine)) + currentLine = word + } else { + if currentLine != "" { + currentLine += " " + } + currentLine += word + } + } + if currentLine != "" { + wrappedContent = append(wrappedContent, fmt.Sprintf("\033[%d;%dm%s\033[0m", 1, color, currentLine)) + } + + return strings.Join(wrappedContent, "\n") +} + +func GetScoreColor(score float64) tablewriter.Colors { + if score == 0.0 { + return tablewriter.Colors{tablewriter.FgRedColor, tablewriter.Bold} + } else if score < 5.0 { + return tablewriter.Colors{tablewriter.FgHiYellowColor, tablewriter.Bold} + } + return tablewriter.Colors{tablewriter.FgGreenColor, tablewriter.Bold} +} diff --git a/pkg/compliance/compliance.go b/pkg/compliance/compliance.go index 21ba1e5d..307ed807 100644 --- a/pkg/compliance/compliance.go +++ b/pkg/compliance/compliance.go @@ -70,20 +70,20 @@ func ComplianceResult(ctx context.Context, doc sbom.Document, reportType, fileNa switch { case reportType == BSI_REPORT: - bsiResult(ctx, doc, fileName, outFormat) + bsiResult(ctx, doc, fileName, outFormat, coloredOutput) case reportType == BSI_V2_REPORT: bsiV2Result(ctx, doc, fileName, outFormat) case reportType == NTIA_REPORT: - ntiaResult(ctx, doc, fileName, outFormat) + ntiaResult(ctx, doc, fileName, outFormat, coloredOutput) case reportType == OCT_TELCO: if doc.Spec().GetSpecType() != "spdx" { fmt.Println("The Provided SBOM spec is other than SPDX. Open Chain Telco only support SPDX specs SBOMs.") return nil } - octResult(ctx, doc, fileName, outFormat) + octResult(ctx, doc, fileName, outFormat, coloredOutput) case reportType == FSCT_V3: fsct.Result(ctx, doc, fileName, outFormat, coloredOutput) diff --git a/pkg/compliance/fsct/fsct_report.go b/pkg/compliance/fsct/fsct_report.go index 6cc185e1..9d93d73f 100644 --- a/pkg/compliance/fsct/fsct_report.go +++ b/pkg/compliance/fsct/fsct_report.go @@ -22,6 +22,7 @@ import ( "time" "github.com/google/uuid" + "github.com/interlynk-io/sbomqs/pkg/compliance/common" "github.com/interlynk-io/sbomqs/pkg/compliance/db" "github.com/olekukonko/tablewriter" "sigs.k8s.io/release-utils/version" @@ -199,15 +200,7 @@ func fsctDetailedReport(db *db.DB, fileName string, coloredOutput bool) { table.SetAutoMergeCellsByColumnIndex([]int{0}) if coloredOutput { - // Set header colors if the colors flag is true - table.SetHeaderColor( - tablewriter.Colors{tablewriter.FgHiWhiteColor, tablewriter.Bold}, - tablewriter.Colors{tablewriter.FgHiWhiteColor, tablewriter.Bold}, - tablewriter.Colors{tablewriter.FgHiWhiteColor, tablewriter.Bold}, - tablewriter.Colors{tablewriter.FgHiWhiteColor, tablewriter.Bold}, - tablewriter.Colors{tablewriter.FgHiWhiteColor, tablewriter.Bold}, - tablewriter.Colors{tablewriter.FgHiWhiteColor, tablewriter.Bold}, - ) + common.SetHeaderColor(table, 6) } sections := fsctConstructSections(db) @@ -218,23 +211,10 @@ func fsctDetailedReport(db *db.DB, fileName string, coloredOutput bool) { sectionID += "*" } - // Define maturity color based on the flag - var maturityColor tablewriter.Colors if coloredOutput { - switch section.Maturity { - case "None": - maturityColor = tablewriter.Colors{tablewriter.FgRedColor, tablewriter.Bold} - case "Minimum": - maturityColor = tablewriter.Colors{tablewriter.FgGreenColor, tablewriter.Bold} - case "Recommended": - maturityColor = tablewriter.Colors{tablewriter.FgCyanColor, tablewriter.Bold} - case "Aspirational": - maturityColor = tablewriter.Colors{tablewriter.FgHiYellowColor, tablewriter.Bold} - } - } - // Use Rich() with color settings only if colors is true - if coloredOutput { + maturityColor := getMaturityColor(section.Maturity) + table.Rich([]string{ section.ElementID, sectionID, @@ -244,9 +224,9 @@ func fsctDetailedReport(db *db.DB, fileName string, coloredOutput bool) { section.Maturity, }, []tablewriter.Colors{ {tablewriter.FgHiMagentaColor, tablewriter.Bold}, - {}, + {tablewriter.FgHiCyanColor}, {tablewriter.FgHiBlueColor, tablewriter.Bold}, - {tablewriter.FgHiWhiteColor, tablewriter.Bold}, + {tablewriter.FgHiCyanColor, tablewriter.Bold}, maturityColor, maturityColor, }) @@ -269,3 +249,18 @@ func fsctBasicReport(db *db.DB, fileName string) { fmt.Printf("Framing Software Component Transparency (v3)\n") fmt.Printf("Score:%0.1f for %s\n", score.totalScore(), fileName) } + +func getMaturityColor(maturity string) tablewriter.Colors { + switch maturity { + case "None": + return tablewriter.Colors{tablewriter.FgRedColor, tablewriter.Bold} + case "Minimum": + return tablewriter.Colors{tablewriter.FgGreenColor, tablewriter.Bold} + case "Recommended": + return tablewriter.Colors{tablewriter.FgCyanColor, tablewriter.Bold} + case "Aspirational": + return tablewriter.Colors{tablewriter.FgHiYellowColor, tablewriter.Bold} + default: + return tablewriter.Colors{} + } +} diff --git a/pkg/compliance/ntia.go b/pkg/compliance/ntia.go index f78ee7d6..065361dc 100644 --- a/pkg/compliance/ntia.go +++ b/pkg/compliance/ntia.go @@ -38,7 +38,7 @@ const ( SCORE_ZERO = 0.0 ) -func ntiaResult(ctx context.Context, doc sbom.Document, fileName string, outFormat string) { +func ntiaResult(ctx context.Context, doc sbom.Document, fileName string, outFormat string, colorOutput bool) { log := logger.FromContext(ctx) log.Debug("compliance.ntiaResult()") @@ -59,7 +59,7 @@ func ntiaResult(ctx context.Context, doc sbom.Document, fileName string, outForm } if outFormat == "detailed" { - ntiaDetailedReport(db, fileName) + ntiaDetailedReport(db, fileName, colorOutput) } } diff --git a/pkg/compliance/ntia_report.go b/pkg/compliance/ntia_report.go index 0dc65542..736719ec 100644 --- a/pkg/compliance/ntia_report.go +++ b/pkg/compliance/ntia_report.go @@ -8,6 +8,7 @@ import ( "time" "github.com/google/uuid" + "github.com/interlynk-io/sbomqs/pkg/compliance/common" "github.com/interlynk-io/sbomqs/pkg/compliance/db" "github.com/olekukonko/tablewriter" "sigs.k8s.io/release-utils/version" @@ -136,7 +137,7 @@ func ntiaConstructSections(db *db.DB) []ntiaSection { return sortedSections } -func ntiaDetailedReport(db *db.DB, fileName string) { +func ntiaDetailedReport(db *db.DB, fileName string, colorOutput bool) { table := tablewriter.NewWriter(os.Stdout) score := ntiaAggregateScore(db) @@ -162,7 +163,23 @@ func ntiaDetailedReport(db *db.DB, fileName string) { if !section.Required { sectionID = sectionID + "*" } - table.Append([]string{section.ElementID, sectionID, section.DataField, section.ElementResult, fmt.Sprintf("%0.1f", section.Score)}) + + if colorOutput { + // disable tablewriter's auto-wrapping + table.SetAutoWrapText(false) + columnWidth := 30 + common.SetHeaderColor(table, 5) + + table = common.ColorTable(table, + section.ElementID, + section.ID, + section.ElementResult, + section.DataField, + section.Score, + columnWidth) + } else { + table.Append([]string{section.ElementID, sectionID, section.DataField, section.ElementResult, fmt.Sprintf("%0.1f", section.Score)}) + } } table.Render() } diff --git a/pkg/compliance/oct.go b/pkg/compliance/oct.go index 3e3b95b1..d6d5ce70 100644 --- a/pkg/compliance/oct.go +++ b/pkg/compliance/oct.go @@ -27,7 +27,7 @@ import ( "github.com/samber/lo" ) -func octResult(ctx context.Context, doc sbom.Document, fileName string, outFormat string) { +func octResult(ctx context.Context, doc sbom.Document, fileName string, outFormat string, colorOutput bool) { log := logger.FromContext(ctx) log.Debug("compliance.octResult()") dtb := db.NewDB() @@ -58,7 +58,7 @@ func octResult(ctx context.Context, doc sbom.Document, fileName string, outForma } if outFormat == "detailed" { - octDetailedReport(dtb, fileName) + octDetailedReport(dtb, fileName, colorOutput) } } diff --git a/pkg/compliance/oct_report.go b/pkg/compliance/oct_report.go index e64c8a6d..d131292c 100644 --- a/pkg/compliance/oct_report.go +++ b/pkg/compliance/oct_report.go @@ -8,6 +8,7 @@ import ( "time" "github.com/google/uuid" + "github.com/interlynk-io/sbomqs/pkg/compliance/common" "github.com/interlynk-io/sbomqs/pkg/compliance/db" "github.com/olekukonko/tablewriter" "sigs.k8s.io/release-utils/version" @@ -155,7 +156,7 @@ func octConstructSections(dtb *db.DB) []octSection { return sortedSections } -func octDetailedReport(dtb *db.DB, fileName string) { +func octDetailedReport(dtb *db.DB, fileName string, colorOutput bool) { table := tablewriter.NewWriter(os.Stdout) score := octAggregateScore(dtb) @@ -172,7 +173,23 @@ func octDetailedReport(dtb *db.DB, fileName string) { if !section.Required { sectionID = sectionID + "*" } - table.Append([]string{section.ElementID, sectionID, section.DataField, section.ElementResult, fmt.Sprintf("%0.1f", section.Score)}) + + if colorOutput { + // disable tablewriter's auto-wrapping + table.SetAutoWrapText(false) + columnWidth := 30 + common.SetHeaderColor(table, 5) + + table = common.ColorTable(table, + section.ElementID, + section.ID, + section.ElementResult, + section.DataField, + section.Score, + columnWidth) + } else { + table.Append([]string{section.ElementID, sectionID, section.DataField, section.ElementResult, fmt.Sprintf("%0.1f", section.Score)}) + } } table.Render() } diff --git a/pkg/engine/score.go b/pkg/engine/score.go index 731c569d..2d491f81 100644 --- a/pkg/engine/score.go +++ b/pkg/engine/score.go @@ -232,12 +232,13 @@ func handlePaths(ctx context.Context, ep *Params) error { } else if ep.JSON { reportFormat = "json" } + coloredOutput := ep.Color nr := reporter.NewReport(ctx, docs, scores, paths, - reporter.WithFormat(strings.ToLower(reportFormat))) + reporter.WithFormat(strings.ToLower(reportFormat)), reporter.WithColor(coloredOutput)) nr.Report() diff --git a/pkg/reporter/detailed.go b/pkg/reporter/detailed.go index f647f029..118c861a 100644 --- a/pkg/reporter/detailed.go +++ b/pkg/reporter/detailed.go @@ -20,6 +20,7 @@ import ( "sort" "strings" + "github.com/fatih/color" "github.com/olekukonko/tablewriter" ) @@ -27,7 +28,7 @@ func (r *Reporter) detailedReport() { for index, path := range r.Paths { doc := r.Docs[index] scores := r.Scores[index] - + colorOp := r.Color outDoc := [][]string{} for _, score := range scores.ScoreList() { @@ -51,11 +52,46 @@ func (r *Reporter) detailedReport() { }) fmt.Printf("SBOM Quality by Interlynk Score:%0.1f\tcomponents:%d\t%s\n", scores.AvgScore(), len(doc.Components()), path) + + // Initialize tablewriter table with borders table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Category", "Feature", "Score", "Desc"}) table.SetRowLine(true) table.SetAutoMergeCellsByColumnIndex([]int{0}) - table.AppendBulk(outDoc) + + if colorOp { + for _, row := range outDoc { + scoreText := row[2] + scoreValue := parseScore(row[2]) + + // Apply color based on the score value + var coloredScore string + switch { + case scoreValue < 5.0: + coloredScore = color.New(color.FgRed).Sprintf("%s", scoreText) + default: + coloredScore = color.New(color.FgGreen).Sprintf("%s", scoreText) + } + coloredCategory := color.New(color.FgHiMagenta).Sprint(row[0]) + coloredFeature := color.New(color.FgHiCyan).Sprint(row[1]) + coloredDesc := color.New(color.FgHiBlue).Sprint(row[3]) + + table.Append([]string{coloredCategory, coloredFeature, coloredScore, coloredDesc}) + } + } else { + table.AppendBulk(outDoc) + } + table.Render() } } + +// parseScore extracts the numeric score value from a formatted score string (e.g., "9.7/10.0"). +func parseScore(scoreStr string) float64 { + var scoreValue float64 + if _, err := fmt.Sscanf(scoreStr, "%f", &scoreValue); err != nil { + fmt.Printf("Error scanning score: %v\n", err) + } + + return scoreValue +} diff --git a/pkg/reporter/report.go b/pkg/reporter/report.go index 2c168a71..aae628b7 100644 --- a/pkg/reporter/report.go +++ b/pkg/reporter/report.go @@ -31,6 +31,7 @@ type Reporter struct { // optional params Format string + Color bool } var ReportFormats = []string{"basic", "detailed", "json"} @@ -43,6 +44,12 @@ func WithFormat(c string) Option { } } +func WithColor(c bool) Option { + return func(r *Reporter) { + r.Color = c + } +} + func NewReport(ctx context.Context, doc []sbom.Document, scores []scorer.Scores, paths []string, opts ...Option) *Reporter { r := &Reporter{ Ctx: ctx,