Skip to content

Commit

Permalink
Trusty: Refactor classification logic, tests
Browse files Browse the repository at this point in the history
This commit breaks the classification logic into more specialized
functions to make it testable, it also adds tests for some of the
new functions.

Signed-off-by: Adolfo García Veytia (puerco) <puerco@stacklok.com>
  • Loading branch information
puerco committed May 15, 2024
1 parent 882c5c4 commit a4b2365
Show file tree
Hide file tree
Showing 3 changed files with 326 additions and 54 deletions.
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
}
Loading

0 comments on commit a4b2365

Please sign in to comment.