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+
351433func (r * markdownSummaryReporter ) addChangedPackageSection (builder * markdown.MarkdownBuilder ,
352434 internalModel * vetResultInternalModel ,
353435) error {
0 commit comments