Skip to content

Commit c4601e0

Browse files
committed
Migrate trusty eval engine to Trusty v2 API.
This change migrates `trusty` evaluation engine to use Trusty v2 API. Most of the changes apply to the intermediate representation we recently added to decouple Trusty from Minder, but some additional changes were required due to some fields becoming optional and nullable. Note: this change is again meant to be bug-compatible with the current evaluation engine, so we treat the lack of `"score"` as a score of `0`, thus triggering false positives as done by the current engine. The idea is to build on top of this to fix known issues of the engine before migrating it to Data Sources. Fixes stacklok/minder-stories#77
1 parent 657bf01 commit c4601e0

File tree

5 files changed

+172
-114
lines changed

5 files changed

+172
-114
lines changed

go.mod

+2-2
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ require (
7070
github.com/spf13/viper v1.19.0
7171
github.com/sqlc-dev/pqtype v0.3.0
7272
github.com/stacklok/frizbee v0.1.4
73-
github.com/stacklok/trusty-sdk-go v0.2.2
73+
github.com/stacklok/trusty-sdk-go v0.2.3-0.20241120172703-3643fb488d5e
7474
github.com/stretchr/testify v1.9.0
7575
github.com/styrainc/regal v0.29.2
7676
github.com/thomaspoignant/go-feature-flag v1.38.0
@@ -381,7 +381,7 @@ require (
381381
golang.org/x/mod v0.22.0
382382
golang.org/x/net v0.31.0 // indirect
383383
golang.org/x/sys v0.27.0 // indirect
384-
golang.org/x/text v0.20.0
384+
golang.org/x/text v0.20.0 // indirect
385385
google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect
386386
gopkg.in/ini.v1 v1.67.0 // indirect
387387
gopkg.in/warnings.v0 v0.1.2 // indirect

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -1019,8 +1019,8 @@ github.com/sqlc-dev/pqtype v0.3.0 h1:b09TewZ3cSnO5+M1Kqq05y0+OjqIptxELaSayg7bmqk
10191019
github.com/sqlc-dev/pqtype v0.3.0/go.mod h1:oyUjp5981ctiL9UYvj1bVvCKi8OXkCa0u645hce7CAs=
10201020
github.com/stacklok/frizbee v0.1.4 h1:00v6/2HBmwzNdOyVAP4e1isOeUAIWTlb5eggoNUpHmk=
10211021
github.com/stacklok/frizbee v0.1.4/go.mod h1:rFA90VkGFYLb7qCiUniAihmkgXfZAj2BnfF6jR8Csro=
1022-
github.com/stacklok/trusty-sdk-go v0.2.2 h1:55B2DrneLYZXxBaNEeyRMGac7mj+pFbrKomx/hSxUyI=
1023-
github.com/stacklok/trusty-sdk-go v0.2.2/go.mod h1:BbXNVVT32meuxyJuO4pmXRzVPjc/9AXob2sBbOPiKpk=
1022+
github.com/stacklok/trusty-sdk-go v0.2.3-0.20241120172703-3643fb488d5e h1:4fTGYQPNLex5VT4c4S7AifMJqUiM/r1B+J5qXUJMSFI=
1023+
github.com/stacklok/trusty-sdk-go v0.2.3-0.20241120172703-3643fb488d5e/go.mod h1:QR01jLW/yfwcXY38dwDpgeEjVc2MAR1LycH1fXtoSXs=
10241024
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
10251025
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
10261026
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

internal/engine/eval/trusty/actions.go

+11-5
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ func (sph *summaryPrHandler) generateSummary() (string, error) {
299299
malicious = append(malicious, maliciousTemplateData{
300300
templatePackageData: packageData,
301301
Summary: alternative.trustyReply.Malicious.Summary,
302-
Details: preprocessDetails(alternative.trustyReply.Malicious.Details),
302+
Details: alternative.trustyReply.Malicious.Details,
303303
})
304304
continue
305305
}
@@ -324,7 +324,7 @@ func (sph *summaryPrHandler) generateSummary() (string, error) {
324324
// (2) we don't suggest malicious packages, I
325325
// suggest getting rid of this check
326326
// altogether.
327-
if altData.Score != nil && *altData.Score <= lowScorePackages[alternative.Dependency.Name].Score {
327+
if altData.Score != nil && *altData.Score != 0 && *altData.Score <= lowScorePackages[alternative.Dependency.Name].Score {
328328
continue
329329
}
330330

@@ -333,9 +333,11 @@ func (sph *summaryPrHandler) generateSummary() (string, error) {
333333
Ecosystem: altData.PackageType,
334334
PackageName: altData.PackageName,
335335
TrustyURL: altData.TrustyURL,
336-
Score: *altData.Score,
337336
},
338337
}
338+
if altData.Score != nil {
339+
altPackageData.templatePackageData.Score = *altData.Score
340+
}
339341

340342
dep := lowScorePackages[alternative.Dependency.Name]
341343
dep.Alternatives = append(dep.Alternatives, altPackageData)
@@ -428,8 +430,12 @@ func newSummaryPrHandler(
428430
}, nil
429431
}
430432

431-
func preprocessDetails(s string) string {
432-
scanner := bufio.NewScanner(strings.NewReader(s))
433+
func preprocessDetails(s *string) string {
434+
if s == nil {
435+
return ""
436+
}
437+
438+
scanner := bufio.NewScanner(strings.NewReader(*s))
433439
text := ""
434440
for scanner.Scan() {
435441
if strings.HasPrefix(scanner.Text(), "#") {

internal/engine/eval/trusty/trusty.go

+141-90
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@ import (
1212
"strings"
1313

1414
"github.com/rs/zerolog"
15-
trusty "github.com/stacklok/trusty-sdk-go/pkg/v1/client"
16-
trustytypes "github.com/stacklok/trusty-sdk-go/pkg/v1/types"
17-
"golang.org/x/text/cases"
18-
"golang.org/x/text/language"
15+
trusty "github.com/stacklok/trusty-sdk-go/pkg/v2/client"
16+
trustytypes "github.com/stacklok/trusty-sdk-go/pkg/v2/types"
1917
"google.golang.org/protobuf/reflect/protoreflect"
2018

2119
"github.com/mindersec/minder/internal/constants"
@@ -39,7 +37,7 @@ const (
3937
type Evaluator struct {
4038
cli provifv1.GitHub
4139
endpoint string
42-
client *trusty.Trusty
40+
client trusty.Trusty
4341
}
4442

4543
// NewTrustyEvaluator creates a new trusty evaluator
@@ -296,135 +294,201 @@ type alternative struct {
296294

297295
func getDependencyScore(
298296
ctx context.Context,
299-
trustyClient *trusty.Trusty,
297+
trustyClient trusty.Trusty,
300298
dep *pbinternal.PrDependencies_ContextualDependency,
301299
) (*trustyReport, error) {
302300
// Call the Trusty API
303-
resp, err := trustyClient.Report(ctx, &trustytypes.Dependency{
304-
Name: dep.Dep.Name,
305-
Version: dep.Dep.Version,
306-
Ecosystem: trustytypes.Ecosystem(dep.Dep.Ecosystem),
307-
})
301+
packageType := dep.Dep.Ecosystem.AsString()
302+
input := &trustytypes.Dependency{
303+
PackageName: dep.Dep.Name,
304+
PackageVersion: &dep.Dep.Version,
305+
PackageType: &packageType,
306+
}
307+
308+
respSummary, err := trustyClient.Summary(ctx, input)
309+
if err != nil {
310+
return nil, fmt.Errorf("failed getting summary: %w", err)
311+
}
312+
313+
respPkg, err := trustyClient.PackageMetadata(ctx, input)
314+
if err != nil {
315+
return nil, fmt.Errorf("failed getting package metadata: %w", err)
316+
}
317+
318+
respAlternatives, err := trustyClient.Alternatives(ctx, input)
308319
if err != nil {
309-
return nil, fmt.Errorf("failed to send request: %w", err)
320+
return nil, fmt.Errorf("failed getting alternatives: %w", err)
310321
}
311322

312-
res := makeTrustyReport(dep, resp)
323+
respProvenance, err := trustyClient.Provenance(ctx, input)
324+
if err != nil {
325+
return nil, fmt.Errorf("failed getting provenance: %w", err)
326+
}
327+
328+
res := makeTrustyReport(dep,
329+
respSummary,
330+
respPkg,
331+
respAlternatives,
332+
respProvenance,
333+
)
313334

314335
return res, nil
315336
}
316337

317338
func makeTrustyReport(
318339
dep *pbinternal.PrDependencies_ContextualDependency,
319-
resp *trustytypes.Reply,
340+
respSummary *trustytypes.PackageSummaryAnnotation,
341+
respPkg *trustytypes.TrustyPackageData,
342+
respAlternatives *trustytypes.PackageAlternatives,
343+
respProvenance *trustytypes.Provenance,
320344
) *trustyReport {
321345
res := &trustyReport{
322-
PackageName: dep.Dep.Name,
323-
PackageVersion: dep.Dep.Version,
324-
PackageType: dep.Dep.Ecosystem.AsString(),
325-
TrustyURL: makeTrustyURL(dep.Dep.Name, strings.ToLower(dep.Dep.Ecosystem.AsString())),
326-
Score: resp.Summary.Score,
327-
IsDeprecated: resp.PackageData.Deprecated,
328-
IsArchived: resp.PackageData.Archived,
329-
ActivityScore: getValueFromMap[float64](resp.Summary.Description, "activity"),
330-
ProvenanceScore: getValueFromMap[float64](resp.Summary.Description, "provenance"),
331-
}
332-
333-
res.ScoreComponents = makeScoreComponents(resp.Summary.Description)
334-
res.Alternatives = makeAlternatives(dep.Dep.Ecosystem.AsString(), resp.Alternatives.Packages)
335-
336-
if getValueFromMap[bool](resp.Summary.Description, "malicious") {
337-
res.Malicious = &malicious{
338-
Summary: resp.PackageData.Malicious.Summary,
339-
Details: preprocessDetails(resp.PackageData.Malicious.Details),
340-
}
346+
PackageName: dep.Dep.Name,
347+
PackageVersion: dep.Dep.Version,
348+
PackageType: dep.Dep.Ecosystem.AsString(),
349+
TrustyURL: makeTrustyURL(dep.Dep.Name, strings.ToLower(dep.Dep.Ecosystem.AsString())),
341350
}
342351

343-
res.Provenance = makeProvenance(resp.Provenance)
352+
addSummaryDetails(res, respSummary)
353+
addMetadataDetails(res, respPkg)
354+
355+
res.ScoreComponents = makeScoreComponents(respSummary.Description)
356+
res.Alternatives = makeAlternatives(dep.Dep.Ecosystem.AsString(), respAlternatives.Packages)
357+
358+
if respSummary.Description.Malicious {
359+
res.Malicious = makeMaliciousDetails(respPkg.Malicious)
360+
}
361+
362+
res.Provenance = makeProvenance(respProvenance)
344363

345364
return res
346365
}
347366

348-
func makeScoreComponents(descr map[string]any) []scoreComponent {
367+
func addSummaryDetails(res *trustyReport, resp *trustytypes.PackageSummaryAnnotation) {
368+
if resp == nil {
369+
return
370+
}
371+
372+
res.Score = resp.Score
373+
res.ActivityScore = resp.Description.Activity
374+
res.ProvenanceScore = resp.Description.Provenance
375+
}
376+
377+
func addMetadataDetails(res *trustyReport, resp *trustytypes.TrustyPackageData) {
378+
if resp == nil {
379+
return
380+
}
381+
382+
res.IsDeprecated = resp.IsDeprecated != nil && *resp.IsDeprecated
383+
res.IsArchived = resp.Archived != nil && *resp.Archived
384+
}
385+
386+
func makeScoreComponents(resp trustytypes.SummaryDescription) []scoreComponent {
349387
scoreComponents := make([]scoreComponent, 0)
350388

351-
if descr == nil {
352-
return scoreComponents
353-
}
354-
355-
caser := cases.Title(language.Und, cases.NoLower)
356-
for l, v := range descr {
357-
switch l {
358-
case "activity":
359-
l = "Package activity"
360-
case "activity_repo":
361-
l = "Repository activity"
362-
case "activity_user":
363-
l = "User activity"
364-
case "provenance_type":
365-
l = "Provenance"
366-
case "typosquatting":
367-
if f, ok := v.(float64); ok && f > 5.0 {
368-
// skip typosquatting entry
369-
continue
370-
}
371-
l = "Typosquatting"
372-
v = "⚠️ Dependency may be trying to impersonate a well known package"
373-
}
389+
// activity scores
390+
if resp.Activity != 0 {
391+
scoreComponents = append(scoreComponents, scoreComponent{
392+
Label: "Package activity",
393+
Value: resp.Activity,
394+
})
395+
}
396+
if resp.ActivityRepo != 0 {
397+
scoreComponents = append(scoreComponents, scoreComponent{
398+
Label: "Repository activity",
399+
Value: resp.ActivityRepo,
400+
})
401+
}
402+
if resp.ActivityUser != 0 {
403+
scoreComponents = append(scoreComponents, scoreComponent{
404+
Label: "User activity",
405+
Value: resp.ActivityUser,
406+
})
407+
}
374408

375-
// Note: if none of the cases above match, we still
376-
// add the value to the list along with its
377-
// capitalized label.
409+
// provenance information
410+
if resp.ProvenanceType != nil {
411+
scoreComponents = append(scoreComponents, scoreComponent{
412+
Label: "Provenance",
413+
Value: string(*resp.ProvenanceType),
414+
})
415+
}
378416

417+
// typosquatting information
418+
if resp.TypoSquatting != 0 && resp.TypoSquatting <= 5.0 {
379419
scoreComponents = append(scoreComponents, scoreComponent{
380-
Label: fmt.Sprintf("%s%s", caser.String(l[0:1]), l[1:]),
381-
Value: v,
420+
Label: "Typosquatting",
421+
Value: "⚠️ Dependency may be trying to impersonate a well known package",
382422
})
383423
}
384424

425+
// Note: in the previous implementation based on Trusty v1
426+
// API, if new fields were added to the `"description"` field
427+
// of a package they were implicitly added to the table of
428+
// score components.
429+
//
430+
// This was possible because the `Description` field of the go
431+
// struct was defined as `map[string]any`.
432+
//
433+
// This is not the case with v2 API, so we need to keep track
434+
// of new measures being added to the API.
435+
385436
return scoreComponents
386437
}
387438

388439
func makeAlternatives(
389440
ecosystem string,
390-
trustyAlternatives []trustytypes.Alternative,
441+
trustyAlternatives []*trustytypes.PackageBasicInfo,
391442
) []alternative {
392443
alternatives := []alternative{}
393444
for _, alt := range trustyAlternatives {
394445
alternatives = append(alternatives, alternative{
395446
PackageName: alt.PackageName,
396447
PackageType: ecosystem,
397-
Score: &alt.Score,
448+
Score: alt.Score,
398449
TrustyURL: makeTrustyURL(alt.PackageName, ecosystem),
399450
})
400451
}
401452

402453
return alternatives
403454
}
404455

456+
func makeMaliciousDetails(
457+
maliciousInfo *trustytypes.PackageMaliciousPayload,
458+
) *malicious {
459+
if maliciousInfo == nil {
460+
return nil
461+
}
462+
463+
return &malicious{
464+
Summary: maliciousInfo.Summary,
465+
Details: preprocessDetails(maliciousInfo.Details),
466+
}
467+
}
468+
405469
func makeProvenance(
406-
trustyProvenance *trustytypes.Provenance,
470+
resp *trustytypes.Provenance,
407471
) *provenance {
408-
if trustyProvenance == nil {
472+
if resp == nil {
409473
return nil
410474
}
411475

412476
prov := &provenance{}
413-
if trustyProvenance.Description.Historical.Overlap != 0 {
477+
if resp.Historical.Overlap != 0 {
414478
prov.Historical = &historicalProvenance{
415-
Versions: int(trustyProvenance.Description.Historical.Versions),
416-
Tags: int(trustyProvenance.Description.Historical.Tags),
417-
Common: int(trustyProvenance.Description.Historical.Common),
418-
Overlap: trustyProvenance.Description.Historical.Overlap,
479+
Versions: int(resp.Historical.Versions),
480+
Tags: int(resp.Historical.Tags),
481+
Common: int(resp.Historical.Common),
482+
Overlap: resp.Historical.Overlap,
419483
}
420484
}
421485

422-
if trustyProvenance.Description.Sigstore.Issuer != "" {
486+
if resp.Sigstore.Issuer != "" {
423487
prov.Sigstore = &sigstoreProvenance{
424-
SourceRepository: trustyProvenance.Description.Sigstore.SourceRepository,
425-
Workflow: trustyProvenance.Description.Sigstore.Workflow,
426-
Issuer: trustyProvenance.Description.Sigstore.Issuer,
427-
RekorURI: trustyProvenance.Description.Sigstore.Transparency,
488+
SourceRepository: resp.Sigstore.SourceRepo,
489+
Workflow: resp.Sigstore.Workflow,
490+
Issuer: resp.Sigstore.Issuer,
491+
RekorURI: resp.Sigstore.Transparency,
428492
}
429493
}
430494

@@ -440,19 +504,6 @@ func makeTrustyURL(packageName string, ecosystem string) string {
440504
return trustyURL
441505
}
442506

443-
func getValueFromMap[T any](coll map[string]any, field string) T {
444-
var t T
445-
v, ok := coll[field]
446-
if !ok {
447-
return t
448-
}
449-
res, ok := v.(T)
450-
if !ok {
451-
return t
452-
}
453-
return res
454-
}
455-
456507
// classifyDependency checks the dependencies from the PR for maliciousness or
457508
// low scores and adds them to the summary if needed
458509
func classifyDependency(

0 commit comments

Comments
 (0)