From fd4bd48345ed24fe892826fe98073093e1a7b42c Mon Sep 17 00:00:00 2001 From: Michelangelo Mori Date: Tue, 19 Nov 2024 20:28:53 +0100 Subject: [PATCH] Decouple Trusty engine from Trusty SDK structs. This change aims to limit the impact of changes deriving from Trusty SDK to fewer lines of code. Specifically, business logic implemented in Trusty evaluation engine looks into the response as returned by the SDK itself, making it harder to move to Trusty API v2. Note: this change is meant to be bug-compatible with the current Trusty evaluation engine. --- internal/engine/eval/trusty/actions.go | 95 +++---- internal/engine/eval/trusty/actions_test.go | 68 +++-- internal/engine/eval/trusty/trusty.go | 259 ++++++++++++++++--- internal/engine/eval/trusty/trusty_test.go | 268 ++++++-------------- 4 files changed, 357 insertions(+), 333 deletions(-) diff --git a/internal/engine/eval/trusty/actions.go b/internal/engine/eval/trusty/actions.go index b15e69840d..0748a615f4 100644 --- a/internal/engine/eval/trusty/actions.go +++ b/internal/engine/eval/trusty/actions.go @@ -13,11 +13,9 @@ import ( "slices" "strings" template "text/template" - "unicode" "github.com/google/go-github/v63/github" "github.com/rs/zerolog" - trustytypes "github.com/stacklok/trusty-sdk-go/pkg/v1/types" "github.com/mindersec/minder/internal/constants" "github.com/mindersec/minder/internal/engine/eval/pr_actions" @@ -194,7 +192,7 @@ type dependencyAlternatives struct { BlockPR bool // trustyReply is the complete response from trusty for this package - trustyReply *trustytypes.Reply + trustyReply *trustyReport } // summaryPrHandler is a prStatusHandler that adds a summary text to the PR as a comment. @@ -276,8 +274,8 @@ func (sph *summaryPrHandler) generateSummary() (string, error) { for _, alternative := range sph.trackedAlternatives { if _, ok := lowScorePackages[alternative.Dependency.Name]; !ok { var score float64 - if alternative.trustyReply.Summary.Score != nil { - score = *alternative.trustyReply.Summary.Score + if alternative.trustyReply.Score != nil { + score = *alternative.trustyReply.Score } packageUIURL, err := url.JoinPath( @@ -300,37 +298,42 @@ func (sph *summaryPrHandler) generateSummary() (string, error) { if slices.Contains(alternative.Reasons, TRUSTY_MALICIOUS_PKG) { malicious = append(malicious, maliciousTemplateData{ templatePackageData: packageData, - Summary: alternative.trustyReply.PackageData.Malicious.Summary, - Details: preprocessDetails(alternative.trustyReply.PackageData.Malicious.Details), + Summary: alternative.trustyReply.Malicious.Summary, + Details: preprocessDetails(alternative.trustyReply.Malicious.Details), }) continue } lowScorePackages[alternative.Dependency.Name] = templatePackage{ templatePackageData: packageData, - Deprecated: alternative.trustyReply.PackageData.Deprecated, - Archived: alternative.trustyReply.PackageData.Archived, - ScoreComponents: buildScoreMatrix(alternative), + Deprecated: alternative.trustyReply.IsDeprecated, + Archived: alternative.trustyReply.IsArchived, + ScoreComponents: buildScoreMatrix(alternative.trustyReply.ScoreComponents), Alternatives: []templateAlternative{}, Provenance: buildProvenanceStruct(alternative.trustyReply), } } - for _, altData := range alternative.trustyReply.Alternatives.Packages { - if altData.Score <= lowScorePackages[alternative.Dependency.Name].Score { + for _, altData := range alternative.trustyReply.Alternatives { + // Note: now that the score is deprecated and + // effectively `nil` for all packages, this + // loop will always discard all alternatives, + // rendering the whole block dead code. + // + // Since (1) we don't have score anymore, and + // (2) we don't suggest malicious packages, I + // suggest getting rid of this check + // altogether. + if altData.Score != nil && *altData.Score <= lowScorePackages[alternative.Dependency.Name].Score { continue } altPackageData := templateAlternative{ templatePackageData: templatePackageData{ - Ecosystem: alternative.Dependency.Ecosystem.AsString(), + Ecosystem: altData.PackageType, PackageName: altData.PackageName, - TrustyURL: fmt.Sprintf( - "%s%s/%s", constants.TrustyHttpURL, - strings.ToLower(alternative.Dependency.Ecosystem.AsString()), - url.PathEscape(altData.PackageName), - ), - Score: altData.Score, + TrustyURL: altData.TrustyURL, + Score: *altData.Score, }, } @@ -344,27 +347,27 @@ func (sph *summaryPrHandler) generateSummary() (string, error) { } // buildProvenanceStruct builds the provenance data structure for the PR template -func buildProvenanceStruct(r *trustytypes.Reply) *templateProvenance { +func buildProvenanceStruct(r *trustyReport) *templateProvenance { if r == nil || r.Provenance == nil { return nil } var provenance *templateProvenance if r.Provenance != nil { provenance = &templateProvenance{} - if r.Provenance.Description.Historical.Overlap != 0 { + if r.Provenance.Historical != nil && r.Provenance.Historical.Overlap != 0 { provenance.Historical = &templateHistoricalProvenance{ - NumVersions: int(r.Provenance.Description.Historical.Versions), - NumTags: int(r.Provenance.Description.Historical.Tags), - MatchedVersions: int(r.Provenance.Description.Historical.Common), + NumVersions: int(r.Provenance.Historical.Versions), + NumTags: int(r.Provenance.Historical.Tags), + MatchedVersions: int(r.Provenance.Historical.Common), } } - if r.Provenance.Description.Sigstore.Issuer != "" { + if r.Provenance.Sigstore != nil && r.Provenance.Sigstore.Issuer != "" { provenance.Sigstore = &templateSigstoreProvenance{ - SourceRepository: r.Provenance.Description.Sigstore.SourceRepository, - Workflow: r.Provenance.Description.Sigstore.Workflow, - Issuer: r.Provenance.Description.Sigstore.Issuer, - RekorURI: r.Provenance.Description.Sigstore.Transparency, + SourceRepository: r.Provenance.Sigstore.SourceRepository, + Workflow: r.Provenance.Sigstore.Workflow, + Issuer: r.Provenance.Sigstore.Issuer, + RekorURI: r.Provenance.Sigstore.RekorURI, } } @@ -377,35 +380,13 @@ func buildProvenanceStruct(r *trustytypes.Reply) *templateProvenance { // buildScoreMatrix builds the score components matrix that populates // the score table in the PR comment template -func buildScoreMatrix(alternative dependencyAlternatives) []templateScoreComponent { +func buildScoreMatrix(components []scoreComponent) []templateScoreComponent { scoreComp := []templateScoreComponent{} - if alternative.trustyReply.Summary.Description != nil { - for l, v := range alternative.trustyReply.Summary.Description { - switch l { - case "activity": - l = "Package activity" - case "activity_user": - l = "User activity" - case "provenance": - l = "Provenance" - case "typosquatting": - if v.(float64) > 5.00 { - continue - } - v = "⚠️ Dependency may be trying to impersonate a well known package" - l = "Typosquatting" - case "activity_repo": - l = "Repository activity" - default: - if len(l) > 1 { - l = string(unicode.ToUpper([]rune(l)[0])) + l[1:] - } - } - scoreComp = append(scoreComp, templateScoreComponent{ - Label: l, - Value: v, - }) - } + for _, component := range components { + scoreComp = append(scoreComp, templateScoreComponent{ + Label: component.Label, + Value: component.Value, + }) } return scoreComp } diff --git a/internal/engine/eval/trusty/actions_test.go b/internal/engine/eval/trusty/actions_test.go index daf8c94ada..db20e5e45a 100644 --- a/internal/engine/eval/trusty/actions_test.go +++ b/internal/engine/eval/trusty/actions_test.go @@ -7,7 +7,6 @@ package trusty import ( "testing" - trustytypes "github.com/stacklok/trusty-sdk-go/pkg/v1/types" "github.com/stretchr/testify/require" v1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" @@ -26,28 +25,25 @@ func TestBuildProvenanceStruct(t *testing.T) { t.Parallel() for _, tc := range []struct { name string - sut *trustytypes.Reply + sut *trustyReport mustNil bool expected *templateProvenance }{ { name: "full-response", - sut: &trustytypes.Reply{ - Provenance: &trustytypes.Provenance{ - Score: 8.0, - Description: trustytypes.ProvenanceDescription{ - Historical: trustytypes.HistoricalProvenance{ - Tags: 10, - Common: 8, - Overlap: 80, - Versions: 10, - }, - Sigstore: trustytypes.SigstoreProvenance{ - Issuer: "CN=sigstore-intermediate,O=sigstore.dev", - Workflow: ".github/workflows/build_and_deploy.yml", - SourceRepository: "https://github.com/vercel/next.js", - Transparency: "https://search.sigstore.dev/?logIndex=88381843", - }, + sut: &trustyReport{ + Provenance: &provenance{ + Historical: &historicalProvenance{ + Tags: 10, + Common: 8, + Overlap: 80, + Versions: 10, + }, + Sigstore: &sigstoreProvenance{ + Issuer: "CN=sigstore-intermediate,O=sigstore.dev", + Workflow: ".github/workflows/build_and_deploy.yml", + SourceRepository: "https://github.com/vercel/next.js", + RekorURI: "https://search.sigstore.dev/?logIndex=88381843", }, }, }, @@ -68,16 +64,13 @@ func TestBuildProvenanceStruct(t *testing.T) { }, { name: "only-historical", - sut: &trustytypes.Reply{ - Provenance: &trustytypes.Provenance{ - Score: 8.0, - Description: trustytypes.ProvenanceDescription{ - Historical: trustytypes.HistoricalProvenance{ - Tags: 10, - Common: 8, - Overlap: 80, - Versions: 10, - }, + sut: &trustyReport{ + Provenance: &provenance{ + Historical: &historicalProvenance{ + Tags: 10, + Common: 8, + Overlap: 80, + Versions: 10, }, }, }, @@ -92,16 +85,13 @@ func TestBuildProvenanceStruct(t *testing.T) { }, { name: "only-sigstore", - sut: &trustytypes.Reply{ - Provenance: &trustytypes.Provenance{ - Score: 8.0, - Description: trustytypes.ProvenanceDescription{ - Sigstore: trustytypes.SigstoreProvenance{ - Issuer: "CN=sigstore-intermediate,O=sigstore.dev", - Workflow: ".github/workflows/build_and_deploy.yml", - SourceRepository: "https://github.com/vercel/next.js", - Transparency: "https://search.sigstore.dev/?logIndex=88381843", - }, + sut: &trustyReport{ + Provenance: &provenance{ + Sigstore: &sigstoreProvenance{ + Issuer: "CN=sigstore-intermediate,O=sigstore.dev", + Workflow: ".github/workflows/build_and_deploy.yml", + SourceRepository: "https://github.com/vercel/next.js", + RekorURI: "https://search.sigstore.dev/?logIndex=88381843", }, }, }, @@ -122,7 +112,7 @@ func TestBuildProvenanceStruct(t *testing.T) { }, { name: "no-provenance", - sut: &trustytypes.Reply{}, + sut: &trustyReport{}, mustNil: true, }, } { diff --git a/internal/engine/eval/trusty/trusty.go b/internal/engine/eval/trusty/trusty.go index 7826afc50d..aa62b192fb 100644 --- a/internal/engine/eval/trusty/trusty.go +++ b/internal/engine/eval/trusty/trusty.go @@ -7,14 +7,18 @@ package trusty import ( "context" "fmt" + "net/url" "os" "strings" "github.com/rs/zerolog" trusty "github.com/stacklok/trusty-sdk-go/pkg/v1/client" trustytypes "github.com/stacklok/trusty-sdk-go/pkg/v1/types" + "golang.org/x/text/cases" + "golang.org/x/text/language" "google.golang.org/protobuf/reflect/protoreflect" + "github.com/mindersec/minder/internal/constants" evalerrors "github.com/mindersec/minder/internal/engine/errors" "github.com/mindersec/minder/internal/engine/eval/pr_actions" "github.com/mindersec/minder/internal/engine/eval/templates" @@ -200,9 +204,7 @@ func buildEvalResult(prSummary *summaryPrHandler) error { // Craft an evaluation failed error with the dependency data: var lowScoringPackages, maliciousPackages []string for _, d := range prSummary.trackedAlternatives { - if d.trustyReply.PackageData.Malicious != nil && - d.trustyReply.PackageData.Malicious.Published != nil && - d.trustyReply.PackageData.Malicious.Published.String() != "" { + if d.trustyReply.Malicious != nil { maliciousPackages = append(maliciousPackages, d.trustyReply.PackageName) } else { lowScoringPackages = append(lowScoringPackages, d.trustyReply.PackageName) @@ -240,9 +242,63 @@ func buildEvalResult(prSummary *summaryPrHandler) error { return nil } +type trustyReport struct { + PackageName string + PackageType string + PackageVersion string + TrustyURL string + IsDeprecated bool + IsArchived bool + Score *float64 + ActivityScore float64 + ProvenanceScore float64 + ScoreComponents []scoreComponent + Alternatives []alternative + Provenance *provenance + Malicious *malicious +} + +type scoreComponent struct { + Label string + Value any +} + +type provenance struct { + Historical *historicalProvenance + Sigstore *sigstoreProvenance +} + +type historicalProvenance struct { + Versions int + Tags int + Common int + Overlap float64 +} + +type sigstoreProvenance struct { + SourceRepository string + Workflow string + Issuer string + RekorURI string +} + +type malicious struct { + Summary string + Details string +} + +type alternative struct { + PackageName string + PackageType string + Score *float64 + TrustyURL string +} + func getDependencyScore( - ctx context.Context, trustyClient *trusty.Trusty, dep *pbinternal.PrDependencies_ContextualDependency, -) (*trustytypes.Reply, error) { + ctx context.Context, + trustyClient *trusty.Trusty, + dep *pbinternal.PrDependencies_ContextualDependency, +) (*trustyReport, error) { // Call the Trusty API resp, err := trustyClient.Report(ctx, &trustytypes.Dependency{ Name: dep.Dep.Name, @@ -252,14 +308,160 @@ func getDependencyScore( if err != nil { return nil, fmt.Errorf("failed to send request: %w", err) } - return resp, nil + + res := makeTrustyReport(dep, resp) + + return res, nil +} + +func makeTrustyReport( + dep *pbinternal.PrDependencies_ContextualDependency, + resp *trustytypes.Reply, +) *trustyReport { + res := &trustyReport{ + PackageName: dep.Dep.Name, + PackageVersion: dep.Dep.Version, + PackageType: dep.Dep.Ecosystem.AsString(), + TrustyURL: makeTrustyURL(dep.Dep.Name, strings.ToLower(dep.Dep.Ecosystem.AsString())), + Score: resp.Summary.Score, + IsDeprecated: resp.PackageData.Deprecated, + IsArchived: resp.PackageData.Archived, + ActivityScore: getValueFromMap[float64](resp.Summary.Description, "activity"), + ProvenanceScore: getValueFromMap[float64](resp.Summary.Description, "provenance"), + } + + res.ScoreComponents = makeScoreComponents(resp.Summary.Description) + res.Alternatives = makeAlternatives(dep.Dep.Ecosystem.AsString(), resp.Alternatives.Packages) + + if getValueFromMap[bool](resp.Summary.Description, "malicious") { + res.Malicious = &malicious{ + Summary: resp.PackageData.Malicious.Summary, + Details: preprocessDetails(resp.PackageData.Malicious.Details), + } + } + + res.Provenance = makeProvenance(resp.Provenance) + + return res +} + +func makeScoreComponents(descr map[string]any) []scoreComponent { + scoreComponents := make([]scoreComponent, 0) + + if descr == nil { + return scoreComponents + } + + caser := cases.Title(language.Und, cases.NoLower) + for l, v := range descr { + switch l { + case "activity": + l = "Package activity" + case "activity_repo": + l = "Repository activity" + case "activity_user": + l = "User activity" + case "provenance_type": + l = "Provenance" + case "typosquatting": + if f, ok := v.(float64); ok && f > 5.0 { + // skip typosquatting entry + continue + } + l = "Typosquatting" + v = "⚠️ Dependency may be trying to impersonate a well known package" + } + + // Note: if none of the cases above match, we still + // add the value to the list along with its + // capitalized label. + + scoreComponents = append(scoreComponents, scoreComponent{ + Label: fmt.Sprintf("%s%s", caser.String(l[0:1]), l[1:]), + Value: v, + }) + } + + return scoreComponents +} + +func makeAlternatives( + ecosystem string, + trustyAlternatives []trustytypes.Alternative, +) []alternative { + alternatives := []alternative{} + for _, alt := range trustyAlternatives { + alternatives = append(alternatives, alternative{ + PackageName: alt.PackageName, + PackageType: ecosystem, + Score: &alt.Score, + TrustyURL: makeTrustyURL(alt.PackageName, ecosystem), + }) + } + + return alternatives +} + +func makeProvenance( + trustyProvenance *trustytypes.Provenance, +) *provenance { + if trustyProvenance == nil { + return nil + } + + prov := &provenance{} + if trustyProvenance.Description.Historical.Overlap != 0 { + prov.Historical = &historicalProvenance{ + Versions: int(trustyProvenance.Description.Historical.Versions), + Tags: int(trustyProvenance.Description.Historical.Tags), + Common: int(trustyProvenance.Description.Historical.Common), + Overlap: trustyProvenance.Description.Historical.Overlap, + } + } + + if trustyProvenance.Description.Sigstore.Issuer != "" { + prov.Sigstore = &sigstoreProvenance{ + SourceRepository: trustyProvenance.Description.Sigstore.SourceRepository, + Workflow: trustyProvenance.Description.Sigstore.Workflow, + Issuer: trustyProvenance.Description.Sigstore.Issuer, + RekorURI: trustyProvenance.Description.Sigstore.Transparency, + } + } + + return prov +} + +func makeTrustyURL(packageName string, ecosystem string) string { + trustyURL, _ := url.JoinPath( + constants.TrustyHttpURL, + "report", + strings.ToLower(ecosystem), + url.PathEscape(packageName)) + return trustyURL +} + +func getValueFromMap[T any](coll map[string]any, field string) T { + var t T + v, ok := coll[field] + if !ok { + return t + } + res, ok := v.(T) + if !ok { + return t + } + return res } // classifyDependency checks the dependencies from the PR for maliciousness or // low scores and adds them to the summary if needed func classifyDependency( - _ context.Context, logger *zerolog.Logger, resp *trustytypes.Reply, ruleConfig *config, - prSummary *summaryPrHandler, dep *pbinternal.PrDependencies_ContextualDependency, + _ context.Context, + logger *zerolog.Logger, + resp *trustyReport, + ruleConfig *config, + prSummary *summaryPrHandler, + dep *pbinternal.PrDependencies_ContextualDependency, ) { // Check all the policy violations reasons := []RuleViolationReason{} @@ -274,7 +476,7 @@ func classifyDependency( // If the package is malicious, ensure that the score is 0 to avoid it // getting ignored from the report - if resp.PackageData.Malicious != nil && resp.PackageData.Malicious.Summary != "" { + if resp.Malicious != nil && resp.Malicious.Summary != "" { logger.Debug(). Str("dependency", fmt.Sprintf("%s@%s", dep.Dep.Name, dep.Dep.Version)). Str("malicious", "true"). @@ -288,11 +490,11 @@ func classifyDependency( } // Note if the packages is deprecated or archived - if resp.PackageData.Deprecated || resp.PackageData.Archived { + if resp.IsDeprecated || resp.IsArchived { logger.Debug(). Str("dependency", fmt.Sprintf("%s@%s", dep.Dep.Name, dep.Dep.Version)). - Bool("deprecated", resp.PackageData.Deprecated). - Bool("archived", resp.PackageData.Archived). + Bool("deprecated", resp.IsDeprecated). + Bool("archived", resp.IsArchived). Msgf("deprecated dependency") if !ecoConfig.AllowDeprecated { @@ -303,26 +505,24 @@ func classifyDependency( } else { logger.Debug(). Str("dependency", fmt.Sprintf("%s@%s", dep.Dep.Name, dep.Dep.Version)). - Bool("deprecated", resp.PackageData.Deprecated). + Bool("deprecated", resp.IsDeprecated). Msgf("not deprecated dependency") } packageScore := float64(0) - if resp.Summary.Score != nil { - packageScore = *resp.Summary.Score + if resp.Score != nil { + packageScore = *resp.Score } - descr := readPackageDescription(resp) - if ecoConfig.Score > packageScore { reasons = append(reasons, TRUSTY_LOW_SCORE) } - if ecoConfig.Provenance > descr["provenance"].(float64) && descr["provenance"].(float64) > 0 { + if ecoConfig.Provenance > resp.ProvenanceScore && resp.ProvenanceScore > 0 { reasons = append(reasons, TRUSTY_LOW_PROVENANCE) } - if ecoConfig.Activity > descr["activity"].(float64) && descr["activity"].(float64) > 0 { + if ecoConfig.Activity > resp.ActivityScore && resp.ActivityScore > 0 { reasons = append(reasons, TRUSTY_LOW_ACTIVITY) } @@ -342,28 +542,7 @@ func classifyDependency( } else { logger.Debug(). Str("dependency", dep.Dep.Name). - Float64("score", *resp.Summary.Score). Float64("threshold", ecoConfig.Score). Msgf("dependency ok") } } - -// readPackageDescription reads the description from the package summary and -// normlizes the required values when missing from a partial Trusty response -func readPackageDescription(resp *trustytypes.Reply) map[string]any { - descr := map[string]any{} - if resp == nil { - resp = &trustytypes.Reply{} - } - if resp.Summary.Description != nil { - descr = resp.Summary.Description - } - - // Ensure don't panic checking all fields are there - for _, fld := range []string{"activity", "provenance"} { - if _, ok := descr[fld]; !ok || descr[fld] == nil { - descr[fld] = float64(0) - } - } - return descr -} diff --git a/internal/engine/eval/trusty/trusty_test.go b/internal/engine/eval/trusty/trusty_test.go index ca574e1de5..f1bd3f9a09 100644 --- a/internal/engine/eval/trusty/trusty_test.go +++ b/internal/engine/eval/trusty/trusty_test.go @@ -8,10 +8,8 @@ import ( "fmt" "slices" "testing" - "time" "github.com/rs/zerolog" - trustytypes "github.com/stacklok/trusty-sdk-go/pkg/v1/types" "github.com/stretchr/testify/require" evalerrors "github.com/mindersec/minder/internal/engine/errors" @@ -26,7 +24,7 @@ import ( func TestBuildEvalResult(t *testing.T) { t.Parallel() sg := float64(6.4) - now := time.Now() + for _, tc := range []struct { name string sut *summaryPrHandler @@ -42,19 +40,14 @@ func TestBuildEvalResult(t *testing.T) { Name: "requests", Version: "0.0.1", }, - trustyReply: &trustytypes.Reply{ - PackageName: "requests", - PackageType: pbinternal.DepEcosystem_DEP_ECOSYSTEM_PYPI.AsString(), - Summary: trustytypes.ScoreSummary{ - Score: &sg, - }, - PackageData: trustytypes.PackageData{ - Archived: false, - Deprecated: false, - Malicious: &trustytypes.MaliciousData{ - Summary: "malicuous", - Published: &now, - }, + trustyReply: &trustyReport{ + PackageName: "requests", + PackageType: pbinternal.DepEcosystem_DEP_ECOSYSTEM_PYPI.AsString(), + Score: &sg, + IsArchived: false, + IsDeprecated: false, + Malicious: &malicious{ + Summary: "malicuous", }, }, }, @@ -68,12 +61,10 @@ func TestBuildEvalResult(t *testing.T) { Name: "requests", Version: "0.0.1", }, - trustyReply: &trustytypes.Reply{ + trustyReply: &trustyReport{ PackageName: "requests", PackageType: pbinternal.DepEcosystem_DEP_ECOSYSTEM_PYPI.AsString(), - Summary: trustytypes.ScoreSummary{ - Score: &sg, - }, + Score: &sg, }, }, }, @@ -86,12 +77,10 @@ func TestBuildEvalResult(t *testing.T) { Name: "python-oauth", Version: "0.0.1", }, - trustyReply: &trustytypes.Reply{ + trustyReply: &trustyReport{ PackageName: "requests", PackageType: pbinternal.DepEcosystem_DEP_ECOSYSTEM_PYPI.AsString(), - Summary: trustytypes.ScoreSummary{ - Score: &sg, - }, + Score: &sg, }, }, { @@ -100,19 +89,14 @@ func TestBuildEvalResult(t *testing.T) { Name: "requestts", Version: "0.0.1", }, - trustyReply: &trustytypes.Reply{ - PackageName: "requests", - PackageType: pbinternal.DepEcosystem_DEP_ECOSYSTEM_PYPI.AsString(), - Summary: trustytypes.ScoreSummary{ - Score: &sg, - }, - PackageData: trustytypes.PackageData{ - Archived: false, - Deprecated: false, - Malicious: &trustytypes.MaliciousData{ - Summary: "malicuous", - Published: &now, - }, + trustyReply: &trustyReport{ + PackageName: "requests", + PackageType: pbinternal.DepEcosystem_DEP_ECOSYSTEM_PYPI.AsString(), + Score: &sg, + IsArchived: false, + IsDeprecated: false, + Malicious: &malicious{ + Summary: "malicuous", }, }, }, @@ -237,19 +221,17 @@ func TestClassifyDependency(t *testing.T) { } for _, tc := range []struct { name string - score *trustytypes.Reply + score *trustyReport config *config mustFilter bool expected *dependencyAlternatives }{ { name: "normal-good-score", - score: &trustytypes.Reply{ + score: &trustyReport{ PackageName: "test", PackageType: "npm", - Summary: trustytypes.ScoreSummary{ - Score: mkfloat(6.4), - }, + Score: mkfloat(6.4), }, config: defaultConfig(), expected: &dependencyAlternatives{ @@ -259,91 +241,75 @@ func TestClassifyDependency(t *testing.T) { }, { name: "normal-bad-score", - score: &trustytypes.Reply{ + score: &trustyReport{ PackageName: "test", PackageType: "npm", - Summary: trustytypes.ScoreSummary{ - Score: mkfloat(4.0), - }, + Score: mkfloat(4.0), }, config: defaultConfig(), expected: &dependencyAlternatives{ Reasons: []RuleViolationReason{TRUSTY_LOW_SCORE}, - trustyReply: &trustytypes.Reply{}, + trustyReply: &trustyReport{}, }, mustFilter: true, }, { name: "normal-malicious", - score: &trustytypes.Reply{ - PackageName: "test", - PackageType: "npm", - Summary: trustytypes.ScoreSummary{Score: mkfloat(8.0)}, - PackageData: trustytypes.PackageData{ - Archived: false, - Deprecated: false, - Malicious: &trustytypes.MaliciousData{ - Summary: "it is malicious", - Details: "some details", - }, + score: &trustyReport{ + PackageName: "test", + PackageType: "npm", + Score: mkfloat(8.0), + IsArchived: false, + IsDeprecated: false, + Malicious: &malicious{ + Summary: "it is malicious", + Details: "some details", }, }, config: defaultConfig(), expected: &dependencyAlternatives{ Reasons: []RuleViolationReason{TRUSTY_MALICIOUS_PKG}, - trustyReply: &trustytypes.Reply{}, + trustyReply: &trustyReport{}, }, mustFilter: true, }, { name: "normal-lowactivity", - score: &trustytypes.Reply{ - PackageName: "test", - PackageType: "npm", - Summary: trustytypes.ScoreSummary{ - Score: mkfloat(8.0), - Description: map[string]any{ - "activity": float64(3.0), - }, - }, + score: &trustyReport{ + PackageName: "test", + PackageType: "npm", + Score: mkfloat(8.0), + ActivityScore: float64(3.0), }, config: defaultConfig(), expected: &dependencyAlternatives{ Reasons: []RuleViolationReason{TRUSTY_LOW_ACTIVITY}, - trustyReply: &trustytypes.Reply{}, + trustyReply: &trustyReport{}, }, mustFilter: true, }, { name: "normal-low-provenance", - score: &trustytypes.Reply{ - PackageName: "test", - PackageType: "npm", - Summary: trustytypes.ScoreSummary{ - Score: mkfloat(8.0), - Description: map[string]any{ - "provenance": float64(3.0), - }, - }, + score: &trustyReport{ + PackageName: "test", + PackageType: "npm", + Score: mkfloat(8.0), + ProvenanceScore: float64(3.0), }, config: defaultConfig(), expected: &dependencyAlternatives{ Reasons: []RuleViolationReason{TRUSTY_LOW_PROVENANCE}, - trustyReply: &trustytypes.Reply{}, + trustyReply: &trustyReport{}, }, mustFilter: true, }, { name: "nil-activity", - score: &trustytypes.Reply{ - PackageName: "test", - PackageType: "npm", - Summary: trustytypes.ScoreSummary{ - Score: mkfloat(8.0), - Description: map[string]any{ - "provenance": nil, - }, - }, + score: &trustyReport{ + PackageName: "test", + PackageType: "npm", + Score: mkfloat(8.0), + ProvenanceScore: float64(0.0), }, config: defaultConfig(), expected: &dependencyAlternatives{ @@ -373,63 +339,26 @@ func TestClassifyDependency(t *testing.T) { } } -func TestBuildScoreMatrix(t *testing.T) { +func TestMakeScoreComponents(t *testing.T) { t.Parallel() for _, tc := range []struct { name string - sut dependencyAlternatives - expected []templateScoreComponent + sut map[string]any + expected []scoreComponent }{ { name: "no-description", - sut: dependencyAlternatives{ - Dependency: &pbinternal.Dependency{}, - Reasons: []RuleViolationReason{}, - trustyReply: &trustytypes.Reply{ - Summary: trustytypes.ScoreSummary{}, - }, - }, - }, - { - name: "normal-response", - sut: dependencyAlternatives{ - Dependency: &pbinternal.Dependency{}, - Reasons: []RuleViolationReason{}, - trustyReply: &trustytypes.Reply{ - Summary: trustytypes.ScoreSummary{ - Description: map[string]any{ - "activity": "a", - "activity_user": "b", - "provenance": "c", - "activity_repo": "d", - }, - }, - }, - }, - expected: []templateScoreComponent{ - {Label: "Package activity", Value: "a"}, - {Label: "User activity", Value: "b"}, - {Label: "Provenance", Value: "c"}, - {Label: "Repository activity", Value: "d"}, - }, + sut: nil, }, { name: "normal-response", - sut: dependencyAlternatives{ - Dependency: &pbinternal.Dependency{}, - Reasons: []RuleViolationReason{}, - trustyReply: &trustytypes.Reply{ - Summary: trustytypes.ScoreSummary{ - Description: map[string]any{ - "activity": "a", - "activity_user": "b", - "provenance": "c", - "activity_repo": "d", - }, - }, - }, + sut: map[string]any{ + "activity": "a", + "activity_user": "b", + "provenance": "c", + "activity_repo": "d", }, - expected: []templateScoreComponent{ + expected: []scoreComponent{ {Label: "Package activity", Value: "a"}, {Label: "User activity", Value: "b"}, {Label: "Provenance", Value: "c"}, @@ -438,33 +367,17 @@ func TestBuildScoreMatrix(t *testing.T) { }, { name: "typosquatting-low", - sut: dependencyAlternatives{ - Dependency: &pbinternal.Dependency{}, - Reasons: []RuleViolationReason{}, - trustyReply: &trustytypes.Reply{ - Summary: trustytypes.ScoreSummary{ - Description: map[string]any{ - "typosquatting": float64(10), - }, - }, - }, + sut: map[string]any{ + "typosquatting": float64(10), }, - expected: []templateScoreComponent{}, + expected: []scoreComponent{}, }, { name: "typosquatting-high", - sut: dependencyAlternatives{ - Dependency: &pbinternal.Dependency{}, - Reasons: []RuleViolationReason{}, - trustyReply: &trustytypes.Reply{ - Summary: trustytypes.ScoreSummary{ - Description: map[string]any{ - "typosquatting": float64(1), - }, - }, - }, + sut: map[string]any{ + "typosquatting": float64(1), }, - expected: []templateScoreComponent{ + expected: []scoreComponent{ {Label: "Typosquatting", Value: "⚠️ Dependency may be trying to impersonate a well known package"}, }, }, @@ -472,7 +385,7 @@ func TestBuildScoreMatrix(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() - scoreMatrix := buildScoreMatrix(tc.sut) + scoreMatrix := makeScoreComponents(tc.sut) require.Len(t, scoreMatrix, len(tc.expected)) if len(tc.expected) == 0 { return @@ -484,45 +397,6 @@ func TestBuildScoreMatrix(t *testing.T) { } } -func TestReadPackageDescription(t *testing.T) { - t.Parallel() - for _, tc := range []struct { - name string - sut *trustytypes.Reply - }{ - { - name: "normal", - sut: &trustytypes.Reply{}, - }, - { - name: "no-provenance", - sut: &trustytypes.Reply{ - Summary: trustytypes.ScoreSummary{ - Description: map[string]any{ - "provenance": 1, - }, - }, - }, - }, - { - name: "nil-response", - sut: nil, - }, - } { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - data := readPackageDescription(tc.sut) - require.NotNil(t, data) - require.NotNil(t, data) - _, ok := data["provenance"] - require.True(t, ok) - _, ok = data["activity"] - require.True(t, ok) - }) - } -} - func TestEvaluationDetailRendering(t *testing.T) { t.Parallel()