Skip to content
Closed
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
46 changes: 38 additions & 8 deletions pkg/infrastructure/scanner/trivy.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"fmt"
"os/exec"
"strings"
"unicode/utf8"

"github.com/microsoft/sbi/pkg/domain"
log "github.com/sirupsen/logrus"
Expand All @@ -49,13 +50,13 @@ type trivyOS struct {
}

type trivyResult struct {
Target string `json:"Target"`
Class string `json:"Class"`
Type string `json:"Type"`
Vulnerabilities []trivyVuln `json:"Vulnerabilities"`
Secrets []trivySecret `json:"Secrets"`
Misconfigs []trivyMisconfig `json:"Misconfigurations"`
Licenses []trivyLicense `json:"Licenses"`
Target string `json:"Target"`
Class string `json:"Class"`
Type string `json:"Type"`
Vulnerabilities []trivyVuln `json:"Vulnerabilities"`
Secrets []trivySecret `json:"Secrets"`
Misconfigs []trivyMisconfig `json:"Misconfigurations"`
Licenses []trivyLicense `json:"Licenses"`
}

type trivyCVSS struct {
Expand Down Expand Up @@ -183,10 +184,39 @@ func parseTrivyResult(output *trivyOutput) *domain.TrivyResult {
return result
}

// truncateString shortens s to at most maxLen bytes, never splitting a UTF-8
// rune. When truncation is needed, the result ends with "..." (included in
// maxLen). maxLen <= 0 yields ""; maxLen < 4 yields a pure rune prefix with
// no ellipsis when the string does not already fit.
func truncateString(s string, maxLen int) string {
if maxLen <= 0 {
return ""
}
if len(s) <= maxLen {
return s
}

return s[:maxLen-3] + "..."
// Reserve room for "..." when possible; otherwise keep a pure prefix.
ellipsis := "..."
budget := maxLen
if maxLen >= len(ellipsis) {
budget = maxLen - len(ellipsis)
} else {
ellipsis = ""
}

var b strings.Builder
b.Grow(budget)
for _, r := range s {
rl := utf8.RuneLen(r)
if rl < 0 {
rl = 1
}
if b.Len()+rl > budget {
break
}
b.WriteRune(r)
}

return b.String() + ellipsis
}
44 changes: 39 additions & 5 deletions pkg/infrastructure/scanner/trivy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
package scanner

import (
"strings"
"testing"
"unicode/utf8"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -365,13 +367,13 @@ func TestParseTrivyResult_VulnerabilityDetails(t *testing.T) {
// Find the CRITICAL vuln
var critVuln *struct {
id, severity, pkg, pkgVer, fixedVer, desc string
cvss float64
cvss float64
}
for _, v := range result.Vulnerabilities {
if v.Severity == "CRITICAL" {
critVuln = &struct {
id, severity, pkg, pkgVer, fixedVer, desc string
cvss float64
cvss float64
}{v.VulnerabilityID, v.Severity, v.PackageName, v.PackageVersion, v.FixedVersion, v.Description, v.CVSSScore}
break
}
Expand Down Expand Up @@ -403,8 +405,42 @@ func TestParseTrivyResult_DescriptionTruncation(t *testing.T) {

result := parseTrivyResult(output)
require.Len(t, result.Vulnerabilities, 1)
assert.Len(t, result.Vulnerabilities[0].Description, 500, "description should be truncated to 500 chars")
assert.Len(t, result.Vulnerabilities[0].Description, 500, "description should be truncated to 500 bytes")
assert.Equal(t, "...", result.Vulnerabilities[0].Description[497:], "should end with ...")
assert.True(t, utf8.ValidString(result.Vulnerabilities[0].Description))
}

func TestTruncateString_UTF8Safe(t *testing.T) {
tests := []struct {
name string
input string
maxLen int
}{
{"ascii short", "hello", 10},
{"ascii exact", "hello", 5},
{"ascii truncate", "hello world", 8},
{"cjk mid-rune boundary", "中文漏洞描述", 5},
{"cjk longer", strings.Repeat("中文", 40), 50},
{"emoji", "🚨CRITICAL vulnerability description", 10},
{"empty", "", 10},
{"max zero", "abc", 0},
{"max three", "abcdef", 3},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
out := truncateString(tt.input, tt.maxLen)
assert.True(t, utf8.ValidString(out), "output must be valid UTF-8: %q", out)
if tt.maxLen > 0 {
assert.LessOrEqual(t, len(out), tt.maxLen)
} else {
assert.Empty(t, out)
}
if len(tt.input) <= tt.maxLen {
assert.Equal(t, tt.input, out)
}
})
}
}

// ============================================================================
Expand Down Expand Up @@ -604,5 +640,3 @@ func TestParseTrivyResult_OSMetadata_AllOSFamilies(t *testing.T) {
})
}
}