Skip to content
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
1 change: 1 addition & 0 deletions go/internal/website/vulnerability.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ func (s *Server) handleVulnerabilityDetails(w http.ResponseWriter, r *http.Reque
KnownIDs: knownIDs,
UpstreamHierarchy: ConstructHierarchyHTML(vuln.GetId(), upstreamHierarchy, knownIDs),
DownstreamHierarchy: ConstructHierarchyHTML(vuln.GetId(), downstreamHierarchy, knownIDs),
RelatedHTML: ConstructRelatedHTML(vuln.GetId(), vuln.GetRelated(), knownIDs),
}

s.render(w, r, "vulnerability.html", http.StatusOK, &data)
Expand Down
208 changes: 173 additions & 35 deletions go/internal/website/vulnerability_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,61 +110,199 @@ func ParseDatabaseSpecificKVs(s *structpb.Struct) []DatabaseSpecificKV {
return kvs
}

// ConstructHierarchyHTML formats a models.Hierarchy into a template.HTML tree string.
func ConstructHierarchyHTML(targetID string, hierarchy *models.Hierarchy, knownIDs map[string]struct{}) template.HTML {
if hierarchy == nil || len(hierarchy.Roots) == 0 {
return ""
// ExtractPrefix returns the prefix of an ID (everything before the first '-').
// If the ID does not contain a '-', or starts with '-', it returns the entire ID.
func ExtractPrefix(id string) string {
prefix, _, found := strings.Cut(id, "-")
if !found || prefix == "" {
return id
}

var sb strings.Builder
visited := make(map[string]bool)
return prefix
}

func sortStringsCaseInsensitive(s []string) {
slices.SortFunc(s, func(a, b string) int {
if c := strings.Compare(strings.ToLower(a), strings.ToLower(b)); c != 0 {
return c
}

return strings.Compare(a, b)
})
}

// VulnTree represents a hierarchical group of vulnerability IDs,
// where top-level keys are prefixes (everything before the first '-'),
// second-level keys are root vulnerability IDs,
// and further levels are recursive child vulnerability IDs.
type VulnTree map[string]VulnTree

// BuildVulnTree builds a VulnTree grouped by prefix (everything before the first '-')
// at the top level, with root vulnerability IDs at the second level, and recursive
// child IDs underneath based on the provided graph.
// targetID is excluded from roots and children.
func BuildVulnTree(targetID string, roots []string, graph map[string][]string) VulnTree {
if len(roots) == 0 {
return nil
}

seenRoots := make(map[string]struct{}, len(roots))
var validRoots []string
for _, r := range roots {
if r == "" || r == targetID {
continue
}
if _, seen := seenRoots[r]; !seen {
seenRoots[r] = struct{}{}
validRoots = append(validRoots, r)
}
}
if len(validRoots) == 0 {
return nil
}

var printSubtree func(vulnID string)
printSubtree = func(vulnID string) {
if visited[vulnID] {
return
var buildSubtree func(id string, visited map[string]bool) VulnTree
buildSubtree = func(id string, visited map[string]bool) VulnTree {
children := VulnTree{}
if graph == nil {
return children
}
visited[vulnID] = true
defer func() {
delete(visited, vulnID)
}()

if vulnID != targetID {
escapedID := template.HTMLEscapeString(vulnID)
if _, known := knownIDs[vulnID]; known {
fmt.Fprintf(&sb, `<li><a href="/vulnerability/%s">%s</a></li>`, escapedID, escapedID)
} else {
fmt.Fprintf(&sb, "<li>%s</li>", escapedID)
for _, child := range graph[id] {
if child == "" || child == targetID || visited[child] {
continue
}
if _, exists := children[child]; exists {
continue
}
visited[child] = true
children[child] = buildSubtree(child, visited)
delete(visited, child)
}

return children
}

tree := VulnTree{}
for _, root := range validRoots {
p := ExtractPrefix(root)
if tree[p] == nil {
tree[p] = VulnTree{}
}
visited := map[string]bool{root: true}
tree[p][root] = buildSubtree(root, visited)
}

if len(tree) == 0 {
return nil
}

if children, exists := hierarchy.Graph[vulnID]; exists && len(children) > 0 {
sortedChildren := slices.Clone(children)
slices.Sort(sortedChildren)
return tree
}

// RenderVulnTreeHTML converts a VulnTree nested map into formatted template.HTML.
// If any prefix has >= 2 roots, all prefixes are rendered as collapsible <details> elements.
// Otherwise, all entries are rendered as loose list items.
func RenderVulnTreeHTML(tree VulnTree, knownIDs map[string]struct{}) template.HTML {
if len(tree) == 0 {
return ""
}

for _, child := range sortedChildren {
if child != targetID && !visited[child] {
sb.WriteString(`<ul class="substream">`)
printSubtree(child)
sb.WriteString(`</ul>`)
}
shouldCollapseAll := false
for _, roots := range tree {
if len(roots) >= 2 {
shouldCollapseAll = true
break
}
}

sortedPrefixes := make([]string, 0, len(tree))
for p := range tree {
sortedPrefixes = append(sortedPrefixes, p)
}
sortStringsCaseInsensitive(sortedPrefixes)

var sb strings.Builder

var renderNode func(id string, children VulnTree)
renderNode = func(id string, children VulnTree) {
escapedID := template.HTMLEscapeString(id)
if _, known := knownIDs[id]; known {
fmt.Fprintf(&sb, `<li><a href="/vulnerability/%s">%s</a></li>`, url.PathEscape(id), escapedID)
} else {
fmt.Fprintf(&sb, "<li>%s</li>", escapedID)
}

if len(children) > 0 {
childIDs := make([]string, 0, len(children))
for cID := range children {
childIDs = append(childIDs, cID)
}
sortStringsCaseInsensitive(childIDs)

for _, cID := range childIDs {
sb.WriteString(`<ul class="substream">`)
renderNode(cID, children[cID])
sb.WriteString(`</ul>`)
}
}
}

sortedRoots := slices.Clone(hierarchy.Roots)
slices.Sort(sortedRoots)
type prefixGroup struct {
prefix string
rootIDs []string
}

groups := make([]prefixGroup, 0, len(sortedPrefixes))
for _, p := range sortedPrefixes {
roots := tree[p]
if len(roots) == 0 {
continue
}

for _, root := range sortedRoots {
rootIDs := make([]string, 0, len(roots))
for id := range roots {
rootIDs = append(rootIDs, id)
}
sortStringsCaseInsensitive(rootIDs)
groups = append(groups, prefixGroup{prefix: p, rootIDs: rootIDs})
}

if shouldCollapseAll {
for _, g := range groups {
fmt.Fprintf(&sb, `<details class="prefix-group"><summary class="prefix-header">%s (%d)</summary><ul class="aliases">`, template.HTMLEscapeString(g.prefix), len(g.rootIDs))
for _, rootID := range g.rootIDs {
renderNode(rootID, tree[g.prefix][rootID])
}
sb.WriteString(`</ul></details>`)
}
} else {
sb.WriteString(`<ul class="aliases">`)
printSubtree(root)
for _, g := range groups {
for _, rootID := range g.rootIDs {
renderNode(rootID, tree[g.prefix][rootID])
}
}
sb.WriteString(`</ul>`)
}

//nolint:gosec // Hierarchy IDs are explicitly HTML escaped via template.HTMLEscapeString
//nolint:gosec // IDs and prefixes are explicitly HTML escaped via template.HTMLEscapeString
return template.HTML(sb.String())
}

// ConstructHierarchyHTML formats a models.Hierarchy into a template.HTML tree string.
func ConstructHierarchyHTML(targetID string, hierarchy *models.Hierarchy, knownIDs map[string]struct{}) template.HTML {
if hierarchy == nil {
return ""
}

return RenderVulnTreeHTML(BuildVulnTree(targetID, hierarchy.Roots, hierarchy.Graph), knownIDs)
}

// ConstructRelatedHTML formats a list of related vulnerability IDs into a template.HTML string.
func ConstructRelatedHTML(targetID string, related []string, knownIDs map[string]struct{}) template.HTML {
return RenderVulnTreeHTML(BuildVulnTree(targetID, related, nil), knownIDs)
}

// GitCommitLink converts a repository URL and commit hash or tag into a web viewer URL.
func GitCommitLink(repoURL, commit string) string {
if repoURL == "" || commit == "" || commit == "0" {
Expand Down
Loading
Loading