Skip to content
This repository has been archived by the owner on Jan 16, 2025. It is now read-only.

Commit

Permalink
Merge pull request #15 from stacklok/issue-6
Browse files Browse the repository at this point in the history
feat: allow to fail on specific scores apart from global
  • Loading branch information
lukehinds authored Apr 27, 2024
2 parents 4da6a04 + 60c24b6 commit ed0d23f
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 32 deletions.
48 changes: 39 additions & 9 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,45 @@
name: 'TrustyPkg Action'
description: 'Run Trusty against your dependencies for supply chain risks'
name: "TrustyPkg Action"
description: "Run Trusty against your dependencies for supply chain risks"
inputs:
GITHUB_TOKEN:
description: 'GitHub token'
description: "GitHub token"
required: true
score_threshold:
description: 'Raise anything below this score as an issue'
required: false
default: 5
thresholds:
global:
description: "Raise global score below this score as an issue"
required: false
default: 5
repo_activity:
description: "Raise repo activity below this score as an issue"
required: false
default: 0
author_activity:
description: "Raise author activity below this score as an issue"
required: false
default: 0
provenance:
description: "Raise provenance below this score as an issue"
required: false
default: 0
typosquatting:
description: "Raise typosquatting below this score as an issue"
required: false
default: 0
fail_on:
malicious:
description: "Fail if package is malicious"
required: false
default: true
deprecated:
description: "Fail if package is deprecated"
required: false
default: true
archived:
description: "Fail if repo is archived"
required: false
default: true
runs:
using: 'docker'
image: 'Dockerfile'
using: "docker"
image: "Dockerfile"
args:
- ${{ inputs.recursive }}
44 changes: 34 additions & 10 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,42 @@ import (
"golang.org/x/oauth2"
)

func main() {
ctx := context.Background()
func parseScore(scoreStr string, defaultScore string) float64 {
if scoreStr == "" {
scoreStr = defaultScore
}
score, err := strconv.ParseFloat(scoreStr, 64)
if err != nil {
log.Printf("Invalid score threshold value: %s\n", scoreStr)
return 0
}
return score
}

scoreThresholdStr := os.Getenv("INPUT_SCORE_THRESHOLD")
if scoreThresholdStr == "" {
log.Println("No score threshold provided, using default value.")
scoreThresholdStr = "5" // Ensure this default value is appropriate (check with DS team)
func parseFail(failStr string, defaultFail string) bool {
if failStr == "" {
failStr = defaultFail
}
scoreThreshold, err := strconv.ParseFloat(scoreThresholdStr, 64)
fail, err := strconv.ParseBool(failStr)
if err != nil {
log.Printf("Invalid score threshold value: %s\n", scoreThresholdStr)
return
log.Printf("Invalid fail value: %s\n", failStr)
return false
}
return fail
}

func main() {
ctx := context.Background()

globalThreshold := parseScore(os.Getenv("INPUT_THRESHOLDS_GLOBAL"), "5")
repoActivityThreshold := parseScore(os.Getenv("INPUT_THRESHOLDS_REPO_ACTIVITY"), "0")
authorActivityThreshold := parseScore(os.Getenv("INPUT_THRESHOLDS_AUTHOR_ACTIVITY"), "0")
provenanceThreshold := parseScore(os.Getenv("INPUT_THRESHOLDS_PROVENANCE"), "0")
typosquattingThreshold := parseScore(os.Getenv("INPUT_THRESHOLDS_TYPOSQUATTING"), "0")

failOnMalicious := parseFail(os.Getenv("INPUT_FAIL_ON_MALICIOUS"), "true")
failOnDeprecated := parseFail(os.Getenv("INPUT_FAIL_ON_DEPRECATED"), "true")
failOnArchived := parseFail(os.Getenv("INPUT_FAIL_ON_ARCHIVED"), "true")

// Split the GITHUB_REPOSITORY environment variable to get owner and repo
repoFullName := os.Getenv("GITHUB_REPOSITORY")
Expand Down Expand Up @@ -168,7 +191,8 @@ func main() {
log.Printf("Added dependencies: %v\n", addedDepNames)

// In your main application where you call ProcessDependencies
trustyapi.BuildReport(ctx, ghClient, owner, repo, prNumber, addedDepNames, ecosystem, scoreThreshold)
trustyapi.BuildReport(ctx, ghClient, owner, repo, prNumber, addedDepNames, ecosystem, globalThreshold, repoActivityThreshold, authorActivityThreshold, provenanceThreshold, typosquattingThreshold,
failOnMalicious, failOnDeprecated, failOnArchived)

}
}
32 changes: 25 additions & 7 deletions pkg/trustyapi/trustyapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,14 @@ func BuildReport(ctx context.Context,
prNumber int,
dependencies []string,
ecosystem string,
scoreThreshold float64) {
globalThreshold float64,
repoActivityThreshold float64,
authorActivityThreshold float64,
provenanceThreshold float64,
typosquattingThreshold float64,
failOnMalicious bool,
failOnDeprecated bool,
failOnArchived bool) {

var (
reportBuilder strings.Builder
Expand All @@ -64,7 +71,8 @@ func BuildReport(ctx context.Context,
// it to the existing reportBuilder, between the header and footer.
for _, dep := range dependencies {
log.Printf("Analyzing dependency: %s\n", dep)
report, shouldFail := ProcessDependency(dep, ecosystem, scoreThreshold)
report, shouldFail := ProcessDependency(dep, ecosystem, globalThreshold, repoActivityThreshold, authorActivityThreshold, provenanceThreshold, typosquattingThreshold,
failOnMalicious, failOnDeprecated, failOnArchived)
// Check if the report is not just whitespace
if strings.TrimSpace(report) != "" {
reportBuilder.WriteString(report)
Expand Down Expand Up @@ -109,7 +117,8 @@ func BuildReport(ctx context.Context,
// Otherwise, it formats the report using Markdown and includes information about the dependency's Trusty score,
// whether it is malicious, deprecated or archived, and recommended alternative packages if available.
// The function returns the formatted report as a string.
func ProcessDependency(dep string, ecosystem string, scoreThreshold float64) (string, bool) {
func ProcessDependency(dep string, ecosystem string, globalThreshold float64, repoActivityThreshold float64, authorActivityThreshold float64, provenanceThreshold float64, typosquattingThreshold float64,
failOnMalicious bool, failOnDeprecated bool, failOnArchived bool) (string, bool) {
var reportBuilder strings.Builder
shouldFail := false

Expand Down Expand Up @@ -137,6 +146,8 @@ func ProcessDependency(dep string, ecosystem string, scoreThreshold float64) (st

// Format the report using Markdown
reportBuilder.WriteString(fmt.Sprintf("### :package: Dependency: [`%s`](https://www.trustypkg.dev/%s/%s)\n", dep, ecosystem, dep))

// Show score detail
// Highlight if the package is malicious, deprecated or archived
if result.PackageData.Origin == "malicious" {
reportBuilder.WriteString("### **⚠️ Malicious** (This package is marked as Malicious. Proceed with extreme caution!)\n\n")
Expand All @@ -149,7 +160,12 @@ func ProcessDependency(dep string, ecosystem string, scoreThreshold float64) (st
reportBuilder.WriteString("### **⚠️ Archived** (This package is marked as Archived. Proceed with caution!)\n\n")
}

// scores
reportBuilder.WriteString(fmt.Sprintf("### 📉 Trusty Score: `%.2f`\n", result.Summary.Score))
reportBuilder.WriteString(fmt.Sprintf("· Repo activity score: `%.2f`\n", result.Summary.Description.ActivityRepo))
reportBuilder.WriteString(fmt.Sprintf("· Author activity score: `%.2f`\n", result.Summary.Description.ActivityUser))
reportBuilder.WriteString(fmt.Sprintf("· Provenance score: `%.2f`\n", result.Summary.Description.Provenance))
reportBuilder.WriteString(fmt.Sprintf("· Typosquatting score: `%.2f`\n", result.Summary.Description.Typosquatting))

// write provenance information
if result.Provenance.Description.Provenance.Issuer != "" {
Expand Down Expand Up @@ -184,10 +200,12 @@ func ProcessDependency(dep string, ecosystem string, scoreThreshold float64) (st
reportBuilder.WriteString("\n---\n\n")

// Check if the Trusty score is below the scoreThreshold, if IsDeprecated, isMalicious, Archived, if so shouldFail is set to true
if result.PackageData.IsDeprecated ||
result.PackageData.Origin == "malicious" ||
result.PackageData.Archived ||
result.Summary.Score < scoreThreshold {
if (failOnDeprecated && result.PackageData.IsDeprecated) ||
(failOnMalicious && result.PackageData.Origin == "malicious") ||
(failOnArchived && result.PackageData.Archived) ||
result.Summary.Score < globalThreshold || result.Summary.Description.ActivityRepo < repoActivityThreshold ||
result.Summary.Description.ActivityUser < authorActivityThreshold || result.Summary.Description.Provenance < provenanceThreshold ||
result.Summary.Description.Typosquatting < typosquattingThreshold {
shouldFail = true
}

Expand Down
16 changes: 10 additions & 6 deletions pkg/trustyapi/trustyapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@ import (
func TestProcessGoDependencies(t *testing.T) {
ecosystem := "go"
scoreThreshold := 5.0
repoActivityThreshold := 5.0
authorActivityThreshold := 5.0
provenanceThreshold := 5.0
typosquattingThreshold := 5.0

dependencies := []string{"github.com/alecthomas/units", "github.com/prometheus/client_golang", "github.com/prometheus/common", "github.com/Tinkoff/libvirt-exporter",
"github.com/beorn7/perks", "golang.org/x/sys", "gopkg.in/alecthomas/kingpin.v2", "github.com/matttproud/golang_protobuf_extensions", "github.com/prometheus/client_model",
"libvirt.org/go/libvirt", "github.com/alecthomas/template", "github.com/golang/protobuf", "github.com/prometheus/procfs"}
expectedFail := []bool{false, false, false, true, true, true, true, true, false, true, true, false, false, true}
expectedFail := []bool{true, false, false, true, true, true, true, true, true, true, true, false, false, true}

for i, dep := range dependencies {
log.Printf("Analyzing dependency: %s\n", dep)
report, shouldFail := ProcessDependency(dep, ecosystem, scoreThreshold)
report, shouldFail := ProcessDependency(dep, ecosystem, repoActivityThreshold, authorActivityThreshold, provenanceThreshold, typosquattingThreshold, scoreThreshold, true, true, true)
if shouldFail != expectedFail[i] {
t.Errorf("Dependency %s failed check unexpectedly, expected %v, got %v", dep, expectedFail[i], shouldFail)
}
Expand All @@ -40,7 +44,7 @@ func TestProcessDeprecatedDependencies(t *testing.T) {

for _, dep := range dependencies {
log.Printf("Analyzing dependency: %s\n", dep)
report, _ := ProcessDependency(dep, ecosystem, scoreThreshold)
report, _ := ProcessDependency(dep, ecosystem, scoreThreshold, 0.0, 0.0, 0.0, 0.0, true, true, true)
if !strings.Contains(report, "Deprecated") {
t.Errorf("Expected report to contain 'Deprecated' for %s", dep)
}
Expand All @@ -56,7 +60,7 @@ func TestProcessMaliciousDependencies(t *testing.T) {

for _, dep := range dependencies {
log.Printf("Analyzing dependency: %s\n", dep)
report, _ := ProcessDependency(dep, ecosystem, scoreThreshold)
report, _ := ProcessDependency(dep, ecosystem, scoreThreshold, 0.0, 0.0, 0.0, 0.0, true, true, true)
if !strings.Contains(report, "Malicious") {
t.Errorf("Expected report to contain 'Malicious' for %s", dep)
}
Expand All @@ -68,7 +72,7 @@ func TestProcessSigstoreProvenance(t *testing.T) {
ecosystem := "npm"
scoreThreshold := 5.0

report, _ := ProcessDependency("sigstore", ecosystem, scoreThreshold)
report, _ := ProcessDependency("sigstore", ecosystem, scoreThreshold, 0.0, 0.0, 0.0, 0.0, true, true, true)
if !strings.Contains(report, "sigstore") {
t.Errorf("Expected report to contain 'sigstore'")
}
Expand All @@ -87,7 +91,7 @@ func TestProcessHistoricalProvenance(t *testing.T) {
ecosystem := "npm"
scoreThreshold := 5.0

report, _ := ProcessDependency("openpgp", ecosystem, scoreThreshold)
report, _ := ProcessDependency("openpgp", ecosystem, scoreThreshold, 0.0, 0.0, 0.0, 0.0, true, true, true)
if !strings.Contains(report, "Number of versions") {
t.Errorf("Versions for historical provenance not populated")
}
Expand Down

0 comments on commit ed0d23f

Please sign in to comment.