Skip to content

Commit 39594d7

Browse files
author
Ubuntu
committed
fix: aggregate lockfile poisoning threats by URL in markdown report
In the markdown summary report, multiple packages pointing to the same resolved URL were rendered as separate bullet points. For example: - glob resolved to untrusted URL https://github.com/.../v10.3.0.tar.gz - globx resolved to untrusted URL https://github.com/.../v10.3.0.tar.gz Now aggregated into a single entry per (manifest, URL): - packages `glob`, `globx` resolved to untrusted URL https://github.com/.../v10.3.0.tar.gz Events emitted by the analyzer are unchanged — aggregation is done only in the markdown reporter's addThreatsSection.
1 parent 11690c4 commit 39594d7

1 file changed

Lines changed: 90 additions & 8 deletions

File tree

pkg/reporter/markdown_summary.go

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"net/url"
77
"os"
8+
"regexp"
9+
"sort"
810
"strings"
911

1012
"github.com/jedib0t/go-pretty/v6/table"
@@ -310,6 +312,11 @@ func (r *markdownSummaryReporter) addThreatsSection(builder *markdown.MarkdownBu
310312
for threatType, threats := range internalModel.threats {
311313
builder.AddHeader(3, threatType.String())
312314

315+
if threatType == jsonreportspec.ReportThreat_LockfilePoisoning {
316+
r.addLockfilePoisoningThreats(builder, threats)
317+
continue
318+
}
319+
313320
for _, threat := range threats {
314321
foundOn := ""
315322
switch threat.GetSubjectType() {
@@ -327,14 +334,6 @@ func (r *markdownSummaryReporter) addThreatsSection(builder *markdown.MarkdownBu
327334

328335
subject := threat.GetSubject()
329336

330-
/*
331-
if threat.GetSubjectType() == jsonreportspec.ReportThreat_Manifest {
332-
if _, ok := r.fileMap[subject]; ok {
333-
subject = r.fileMap[subject]
334-
}
335-
}
336-
*/
337-
338337
builder.AddBulletPoint(fmt.Sprintf("%s Found in %s `%s`, %s. Refer to [this](%s) for more details",
339338
markdown.EmojiWarning,
340339
foundOn,
@@ -348,6 +347,89 @@ func (r *markdownSummaryReporter) addThreatsSection(builder *markdown.MarkdownBu
348347
return nil
349348
}
350349

350+
// lfpMessageRe extracts the URL from a lockfile poisoning threat message.
351+
// Messages follow the pattern: Package `name` resolved to ... `URL` ...
352+
var lfpMessageRe = regexp.MustCompile("`(https?://[^`]+)`")
353+
354+
// lfpPackageRe extracts the package name from a lockfile poisoning threat message.
355+
var lfpPackageRe = regexp.MustCompile("^Package `([^`]+)`")
356+
357+
// lfpThreatGroup aggregates lockfile poisoning findings that share the same
358+
// manifest subject and resolved URL.
359+
type lfpThreatGroup struct {
360+
subject string
361+
url string
362+
packages []string
363+
}
364+
365+
// addLockfilePoisoningThreats renders lockfile poisoning threats aggregated by
366+
// (manifest, URL) so that multiple packages pointing to the same URL are shown
367+
// as a single entry rather than repeated warnings.
368+
func (r *markdownSummaryReporter) addLockfilePoisoningThreats(
369+
builder *markdown.MarkdownBuilder,
370+
threats []*jsonreportspec.ReportThreat,
371+
) {
372+
// key: "subject\x00url"
373+
groupMap := map[string]*lfpThreatGroup{}
374+
keyOrder := []string{}
375+
376+
for _, threat := range threats {
377+
subject := threat.GetSubject()
378+
msg := threat.GetMessage()
379+
380+
urlMatches := lfpMessageRe.FindStringSubmatch(msg)
381+
if len(urlMatches) < 2 {
382+
// Fallback: render as-is
383+
builder.AddBulletPoint(fmt.Sprintf("%s %s. Refer to [this](%s) for more details",
384+
markdown.EmojiWarning, msg, lockfilePoisoningReference))
385+
continue
386+
}
387+
resolvedURL := urlMatches[1]
388+
389+
pkgMatches := lfpPackageRe.FindStringSubmatch(msg)
390+
pkgName := ""
391+
if len(pkgMatches) >= 2 {
392+
pkgName = pkgMatches[1]
393+
}
394+
395+
key := subject + "\x00" + resolvedURL
396+
if _, ok := groupMap[key]; !ok {
397+
groupMap[key] = &lfpThreatGroup{
398+
subject: subject,
399+
url: resolvedURL,
400+
packages: []string{},
401+
}
402+
keyOrder = append(keyOrder, key)
403+
}
404+
if pkgName != "" {
405+
groupMap[key].packages = append(groupMap[key].packages, pkgName)
406+
}
407+
}
408+
409+
for _, key := range keyOrder {
410+
g := groupMap[key]
411+
sort.Strings(g.packages)
412+
413+
pkgList := ""
414+
if len(g.packages) > 0 {
415+
quoted := make([]string, len(g.packages))
416+
for i, p := range g.packages {
417+
quoted[i] = fmt.Sprintf("`%s`", p)
418+
}
419+
pkgList = strings.Join(quoted, ", ")
420+
}
421+
422+
builder.AddBulletPoint(fmt.Sprintf(
423+
"%s Found in manifest `%s`, packages %s resolved to untrusted URL `%s`. Refer to [this](%s) for more details",
424+
markdown.EmojiWarning,
425+
g.subject,
426+
pkgList,
427+
g.url,
428+
lockfilePoisoningReference,
429+
))
430+
}
431+
}
432+
351433
func (r *markdownSummaryReporter) addChangedPackageSection(builder *markdown.MarkdownBuilder,
352434
internalModel *vetResultInternalModel,
353435
) error {

0 commit comments

Comments
 (0)