Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trusty: Refactor alternative classification, add tests #3336

Merged
merged 2 commits into from
May 16, 2024
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
63 changes: 36 additions & 27 deletions internal/engine/eval/trusty/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,32 +206,6 @@ func (sph *summaryPrHandler) generateSummary() (string, error) {
Score: score,
}

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":
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,
})
}
}

// If the package is malicious we list it separately
if slices.Contains(alternative.Reasons, TRUSTY_MALICIOUS_PKG) {
malicious = append(malicious, maliciousTemplateData{
Expand All @@ -246,7 +220,7 @@ func (sph *summaryPrHandler) generateSummary() (string, error) {
templatePackageData: packageData,
Deprecated: alternative.trustyReply.PackageData.Deprecated,
Archived: alternative.trustyReply.PackageData.Archived,
ScoreComponents: scoreComp,
ScoreComponents: buildScoreMatrix(alternative),
Alternatives: []templateAlternative{},
}
}
Expand Down Expand Up @@ -278,6 +252,41 @@ func (sph *summaryPrHandler) generateSummary() (string, error) {
return sph.compileTemplate(malicious, lowScorePackages)
}

// buildScoreMatrix builds the score components matrix that populates
// the score table in the PR comment template
func buildScoreMatrix(alternative dependencyAlternatives) []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,
})
}
}
return scoreComp
}

func (sph *summaryPrHandler) compileTemplate(malicious []maliciousTemplateData, deps map[string]templatePackage) (string, error) {
var summary strings.Builder
var headerBuf bytes.Buffer
Expand Down
71 changes: 44 additions & 27 deletions internal/engine/eval/trusty/trusty.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,19 @@ func (e *Evaluator) Eval(ctx context.Context, pol map[string]any, res *engif.Res

// Classify all dependencies, tracking all that are malicious or scored low
for _, dep := range prDependencies.Deps {
if err := classifyDependency(ctx, &logger, e.client, ruleConfig, prSummaryHandler, dep); err != nil {
return fmt.Errorf("classifying dependency: %w", err)
depscore, err := getDependencyScore(ctx, e.client, dep)
if err != nil {
return fmt.Errorf("getting dependency score: %w", err)
}

if depscore == nil || depscore.PackageName == "" {
logger.Info().
Str("dependency", dep.Dep.Name).
Msgf("no trusty data for dependency, skipping")
return nil
}

classifyDependency(ctx, &logger, depscore, ruleConfig, prSummaryHandler, dep)
}

// If there are no problematic dependencies, return here
Expand All @@ -119,6 +129,20 @@ func (e *Evaluator) Eval(ctx context.Context, pol map[string]any, res *engif.Res
return buildEvalResult(prSummaryHandler)
}

func getEcosystemConfig(
logger *zerolog.Logger, ruleConfig *config, dep *pb.PrDependencies_ContextualDependency,
) *ecosystemConfig {
ecoConfig := ruleConfig.getEcosystemConfig(dep.Dep.Ecosystem)
if ecoConfig == nil {
logger.Info().
Str("dependency", dep.Dep.Name).
Str("ecosystem", dep.Dep.Ecosystem.AsString()).
Msgf("no config for ecosystem, skipping")
return nil
}
return ecoConfig
}

// readPullRequestDependencies returns the dependencies found in theingestion results
func readPullRequestDependencies(res *engif.Result) (*pb.PrDependencies, error) {
prdeps, ok := res.Object.(*pb.PrDependencies)
Expand Down Expand Up @@ -194,40 +218,32 @@ func buildEvalResult(prSummary *summaryPrHandler) error {
return nil
}

func getDependencyScore(ctx context.Context, trusty *trustyClient, dep *pb.PrDependencies_ContextualDependency) (*Reply, error) {
// Call the Trusty API
resp, err := trusty.SendRecvRequest(ctx, dep.Dep)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
return resp, nil
}

// classifyDependency checks the dependencies from the PR for maliciousness or
// low scores and adds them to the summary if needed
func classifyDependency(
ctx context.Context, logger *zerolog.Logger, trusty *trustyClient, ruleConfig *config,
_ context.Context, logger *zerolog.Logger, resp *Reply, ruleConfig *config,
prSummary *summaryPrHandler, dep *pb.PrDependencies_ContextualDependency,
) error {
) {
// Check all the policy violations
reasons := []RuleViolationReason{}

ecoConfig := ruleConfig.getEcosystemConfig(dep.Dep.Ecosystem)
ecoConfig := getEcosystemConfig(logger, ruleConfig, dep)
if ecoConfig == nil {
logger.Info().
Str("dependency", dep.Dep.Name).
Str("ecosystem", dep.Dep.Ecosystem.AsString()).
Msgf("no config for ecosystem, skipping")
return nil
}

// Call the Trusty API
resp, err := trusty.SendRecvRequest(ctx, dep.Dep)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}

if resp == nil || resp.PackageName == "" {
logger.Info().
Str("dependency", dep.Dep.Name).
Msgf("no trusty data for dependency, skipping")
return nil
return
}

// 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.Published.String() != "" {
if resp.PackageData.Malicious != nil && resp.PackageData.Malicious.Summary != "" {
logger.Debug().
Str("dependency", fmt.Sprintf("%s@%s", dep.Dep.Name, dep.Dep.Version)).
Str("malicious", "true").
Expand Down Expand Up @@ -256,13 +272,15 @@ func classifyDependency(
if ecoConfig.Score > packageScore {
reasons = append(reasons, TRUSTY_LOW_SCORE)
}
if ecoConfig.Provenance > descr["provenance"].(float64) {

if ecoConfig.Provenance > descr["provenance"].(float64) && descr["provenance"].(float64) > 0 {
reasons = append(reasons, TRUSTY_LOW_PROVENANCE)
}

if ecoConfig.Activity > descr["activity"].(float64) {
if ecoConfig.Activity > descr["activity"].(float64) && descr["activity"].(float64) > 0 {
reasons = append(reasons, TRUSTY_LOW_ACTIVITY)
}

if len(reasons) > 0 {
logger.Debug().
Str("dependency", dep.Dep.Name).
Expand All @@ -278,5 +296,4 @@ func classifyDependency(
Float64("threshold", ecoConfig.Score).
Msgf("the dependency has lower score than threshold or is malicious, tracking")
}
return nil
}
30 changes: 18 additions & 12 deletions internal/engine/eval/trusty/trusty_rest_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,26 +60,32 @@ type Alternative struct {
PackageNameURL string
}

// AlternativesList is the alternatives block in the trusty API response
type AlternativesList struct {
Status string `json:"status"`
Packages []Alternative `json:"packages"`
}

// ScoreSummary is the summary score returned from the package intelligence API
type ScoreSummary struct {
Score *float64 `json:"score"`
Description map[string]any `json:"description"`
}

// PackageData contains the data about the queried package
type PackageData struct {
Archived bool `json:"archived"`
Deprecated bool `json:"is_deprecated"`
Malicious *MaliciousData `json:"malicious"`
}

// Reply is the response from the package intelligence API
type Reply struct {
PackageName string `json:"package_name"`
PackageType string `json:"package_type"`
Summary ScoreSummary `json:"summary"`
Alternatives struct {
Status string `json:"status"`
Packages []Alternative `json:"packages"`
} `json:"alternatives"`
PackageData struct {
Archived bool `json:"archived"`
Deprecated bool `json:"is_deprecated"`
Malicious *MaliciousData `json:"malicious"`
} `json:"package_data"`
PackageName string `json:"package_name"`
PackageType string `json:"package_type"`
Summary ScoreSummary `json:"summary"`
Alternatives AlternativesList `json:"alternatives"`
PackageData PackageData `json:"package_data"`
}

// MaliciousData contains the security details when a dependency is malicious
Expand Down
Loading