Skip to content

Commit

Permalink
feat: add "vertical" output format
Browse files Browse the repository at this point in the history
  • Loading branch information
G-Rath committed Mar 26, 2024
1 parent 1c01916 commit 505d532
Show file tree
Hide file tree
Showing 7 changed files with 318 additions and 2 deletions.
2 changes: 1 addition & 1 deletion cmd/osv-scanner/__snapshots__/main_test.snap
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Scanned <rootdir>/fixtures/locks-many/package-lock.json file and found 1 package
---

[TestRun/#06 - 2]
unsupported output format "unknown" - must be one of: table, json, markdown, sarif, gh-annotations
unsupported output format "unknown" - must be one of: table, vertical, json, markdown, sarif, gh-annotations

---

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/charmbracelet/glamour v0.6.0
github.com/charmbracelet/lipgloss v0.10.0
github.com/dghubble/trie v0.1.0
github.com/fatih/color v1.15.0
github.com/gkampitakis/go-snaps v0.5.2
github.com/go-git/go-billy/v5 v5.5.0
github.com/go-git/go-git/v5 v5.11.0
Expand Down Expand Up @@ -73,6 +74,7 @@ require (
github.com/kr/text v0.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/maruel/natural v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcej
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/gkampitakis/ciinfo v0.3.0 h1:gWZlOC2+RYYttL0hBqcoQhM7h1qNkVqvRCV1fOvpAv8=
github.com/gkampitakis/ciinfo v0.3.0/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
Expand Down Expand Up @@ -133,6 +135,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
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=
Expand Down
142 changes: 142 additions & 0 deletions internal/output/vertical.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package output

import (
"fmt"
"io"
"strings"
"unicode"

"github.com/fatih/color"
"github.com/google/osv-scanner/pkg/models"
)

func PrintVerticalResults(vulnResult *models.VulnerabilityResults, outputWriter io.Writer) {
for _, result := range vulnResult.Results {
fmt.Fprintln(outputWriter, toString(result))
}
}

func countVulnerabilities(result models.PackageSource) int {
count := 0

for _, pkg := range result.Packages {
count += len(pkg.Vulnerabilities)
}

return count
}

// truncate ensures that the given string is shorter than the provided limit.
//
// If the string is longer than the limit, it's trimmed and suffixed with an ellipsis.
// Ideally the string will be trimmed at the space that's closest to the limit to
// preserve whole words; if a string has no spaces before the limit, it'll be forcefully truncated.
func truncate(str string, limit int) string {
count := 0
truncateAt := -1

for i, c := range str {
if unicode.IsSpace(c) {
truncateAt = i
}

count++

if count >= limit {
// ideally we want to keep words whole when truncating,
// but if we can't find a space just truncate at the limit
if truncateAt == -1 {
truncateAt = limit
}

return str[:truncateAt] + "..."
}
}

return str
}

func describe(vulnerability models.Vulnerability) string {
description := vulnerability.Summary

if description == "" {
description += truncate(vulnerability.Details, 80)
}

if description == "" {
description += "(no details available)"
}

description += " (" + OSVBaseVulnerabilityURL + vulnerability.ID + ")"

return description
}

func formatLineByLine(result models.PackageSource) string {
lines := make([]string, 0, len(result.Packages))

for _, pkg := range result.Packages {
if len(pkg.Vulnerabilities) == 0 {
continue
}

lines = append(lines, fmt.Sprintf(
" %s %s",
color.YellowString("%s@%s", pkg.Package.Name, pkg.Package.Version),
color.RedString("is affected by the following vulnerabilities:"),
))

for _, vulnerability := range pkg.Vulnerabilities {
lines = append(lines, fmt.Sprintf(
" %s %s",
color.CyanString("%s:", vulnerability.ID),
describe(vulnerability),
))
}
}

return strings.Join(lines, "\n")
}

func toString(result models.PackageSource) string {
count := countVulnerabilities(result) // r.countKnownVulnerabilities()
ignoreMsg := "" // r.describeIgnores()
word := "known"

out := ""
out += fmt.Sprintf(
"%s: found %s %s\n",
color.MagentaString("%s", result.Source.Path),
color.YellowString("%d", len(result.Packages)),
Form(len(result.Packages), "package", "packages"),
)

if ignoreMsg != "" {
word = "new"
}

if count == 0 {
return out + fmt.Sprintf(
" %s%s\n",
color.GreenString("no %s vulnerabilities found", word),
ignoreMsg,
)
}

out += "\n"
out += formatLineByLine(result)
out += "\n"

out += fmt.Sprintf("\n %s%s\n",
color.RedString(
"%d %s %s found in %s",
count,
word,
Form(count, "vulnerability", "vulnerabilities"),
result.Source.Path,
),
ignoreMsg,
)

return out
}
4 changes: 3 additions & 1 deletion pkg/reporter/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"io"
)

var format = []string{"table", "json", "markdown", "sarif", "gh-annotations"}
var format = []string{"table", "vertical", "json", "markdown", "sarif", "gh-annotations"}

func Format() []string {
return format
Expand All @@ -17,6 +17,8 @@ func New(format string, stdout, stderr io.Writer, level VerbosityLevel, terminal
switch format {
case "json":
return NewJSONReporter(stdout, stderr, level), nil
case "vertical":
return NewVerticalReporter(stdout, stderr, level, false, terminalWidth), nil
case "table":
return NewTableReporter(stdout, stderr, level, false, terminalWidth), nil
case "markdown":
Expand Down
68 changes: 68 additions & 0 deletions pkg/reporter/vertical_reporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package reporter

import (
"fmt"
"io"

"github.com/google/osv-scanner/internal/output"
"github.com/google/osv-scanner/pkg/models"
)

type VerticalReporter struct {
hasErrored bool
stdout io.Writer
stderr io.Writer
level VerbosityLevel
markdown bool
// 0 indicates not a terminal output
terminalWidth int
}

func NewVerticalReporter(stdout io.Writer, stderr io.Writer, level VerbosityLevel, markdown bool, terminalWidth int) *VerticalReporter {
return &VerticalReporter{
stdout: stdout,
stderr: stderr,
hasErrored: false,
level: level,
markdown: markdown,
terminalWidth: terminalWidth,
}
}

func (r *VerticalReporter) Errorf(format string, a ...any) {
fmt.Fprintf(r.stderr, format, a...)
r.hasErrored = true
}

func (r *VerticalReporter) HasErrored() bool {
return r.hasErrored
}

func (r *VerticalReporter) Warnf(format string, a ...any) {
if WarnLevel <= r.level {
fmt.Fprintf(r.stdout, format, a...)
}
}

func (r *VerticalReporter) Infof(format string, a ...any) {
if InfoLevel <= r.level {
fmt.Fprintf(r.stdout, format, a...)
}
}

func (r *VerticalReporter) Verbosef(format string, a ...any) {
if VerboseLevel <= r.level {
fmt.Fprintf(r.stdout, format, a...)
}
}

func (r *VerticalReporter) PrintResult(vulnResult *models.VulnerabilityResults) error {
if len(vulnResult.Results) == 0 && !r.hasErrored {
fmt.Fprintf(r.stdout, "No issues found\n")
return nil
}

output.PrintVerticalResults(vulnResult, r.stdout)

return nil
}
98 changes: 98 additions & 0 deletions pkg/reporter/vertical_reporter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package reporter_test

import (
"bytes"
"io"
"testing"

"github.com/google/osv-scanner/pkg/reporter"
)

func TestVerticalReporter_Errorf(t *testing.T) {
t.Parallel()

writer := &bytes.Buffer{}
r := reporter.NewVerticalReporter(io.Discard, writer, reporter.ErrorLevel, false, 0)
text := "hello world!"

r.Errorf(text)

if writer.String() != text {
t.Error("Error level message should have been printed")
}
if !r.HasErrored() {
t.Error("HasErrored() should have returned true")
}
}

func TestVerticalReporter_Warnf(t *testing.T) {
t.Parallel()

text := "hello world!"
tests := []struct {
lvl reporter.VerbosityLevel
expectedPrintout string
}{
{lvl: reporter.WarnLevel, expectedPrintout: text},
{lvl: reporter.ErrorLevel, expectedPrintout: ""},
}

for _, test := range tests {
writer := &bytes.Buffer{}
r := reporter.NewVerticalReporter(writer, io.Discard, test.lvl, false, 0)

r.Warnf(text)

if writer.String() != test.expectedPrintout {
t.Errorf("expected \"%s\", got \"%s\"", test.expectedPrintout, writer.String())
}
}
}

func TestVerticalReporter_Infof(t *testing.T) {
t.Parallel()

text := "hello world!"
tests := []struct {
lvl reporter.VerbosityLevel
expectedPrintout string
}{
{lvl: reporter.InfoLevel, expectedPrintout: text},
{lvl: reporter.WarnLevel, expectedPrintout: ""},
}

for _, test := range tests {
writer := &bytes.Buffer{}
r := reporter.NewVerticalReporter(writer, io.Discard, test.lvl, false, 0)

r.Infof(text)

if writer.String() != test.expectedPrintout {
t.Errorf("expected \"%s\", got \"%s\"", test.expectedPrintout, writer.String())
}
}
}

func TestVerticalReporter_Verbosef(t *testing.T) {
t.Parallel()

text := "hello world!"
tests := []struct {
lvl reporter.VerbosityLevel
expectedPrintout string
}{
{lvl: reporter.VerboseLevel, expectedPrintout: text},
{lvl: reporter.InfoLevel, expectedPrintout: ""},
}

for _, test := range tests {
writer := &bytes.Buffer{}
r := reporter.NewVerticalReporter(writer, io.Discard, test.lvl, false, 0)

r.Verbosef(text)

if writer.String() != test.expectedPrintout {
t.Errorf("expected \"%s\", got \"%s\"", test.expectedPrintout, writer.String())
}
}
}

0 comments on commit 505d532

Please sign in to comment.