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

feat: allow to fail on specific scores apart from global #15

Merged
merged 4 commits into from
Apr 27, 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
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)

}
}
43 changes: 26 additions & 17 deletions pkg/trustyapi/trustyapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,25 +50,29 @@ 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
failAction bool // Flag to track if the GitHub Action should fail
)

reportHeader := "## 🐻 Trusty Dependency Analysis Action Report \n\n"

reportBuilder.WriteString(reportHeader)

warningMessage := fmt.Sprintf("#### The following dependencies have Trusty scores below the set threshold of `%.2f`:\n\n", scoreThreshold)
reportBuilder.WriteString(warningMessage)

// The following loop generates the report for each dependency and then adds
// 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 All @@ -86,7 +90,7 @@ func BuildReport(ctx context.Context,

// Trim whitespace for accurate comparison
trimmedCommentBody := strings.TrimSpace(commentBody)
trimmedHeaderAndFooter := strings.TrimSpace(reportHeader + warningMessage + reportFooter)
trimmedHeaderAndFooter := strings.TrimSpace(reportHeader + reportFooter)

// Check if the comment body has more content than just the header and footer combined
if len(trimmedCommentBody) > len(trimmedHeaderAndFooter) {
Expand All @@ -113,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 All @@ -139,13 +144,10 @@ func ProcessDependency(dep string, ecosystem string, scoreThreshold float64) (st
log.Printf("Processing result for dependency: %s\n", dep)
}

// Check if the Trusty score is greater than the scoreThreshold
if result.Summary.Score > scoreThreshold {
log.Printf("Skipping dependency %s due to score %.2f being above the threshold %.2f\n", dep, result.Summary.Score, scoreThreshold)
return "", shouldFail // shouldFail is false here, nothing to see.
}
// 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 @@ -158,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 @@ -193,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
24 changes: 14 additions & 10 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 @@ -33,14 +37,14 @@ func TestProcessGoDependencies(t *testing.T) {

func TestProcessDeprecatedDependencies(t *testing.T) {
ecosystem := "npm"
scoreThreshold := 10.0
scoreThreshold := 5.0

dependencies := []string{"@types/google-cloud__storage", "cutjs", "scriptoni", "stryker-mocha-framework", "grunt-html-smoosher", "moesif-express", "swagger-methods",
"@syncfusion/ej2-heatmap", "@cnbritain/wc-buttons", "gulp-google-cdn"}

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 @@ -50,13 +54,13 @@ func TestProcessDeprecatedDependencies(t *testing.T) {

func TestProcessMaliciousDependencies(t *testing.T) {
ecosystem := "pypi"
scoreThreshold := 10.0
scoreThreshold := 5.0

dependencies := []string{"lyft-service", "types-for-adobe", "reqargs"}

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 @@ -66,9 +70,9 @@ func TestProcessMaliciousDependencies(t *testing.T) {

func TestProcessSigstoreProvenance(t *testing.T) {
ecosystem := "npm"
scoreThreshold := 10.0
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 @@ -85,9 +89,9 @@ func TestProcessSigstoreProvenance(t *testing.T) {

func TestProcessHistoricalProvenance(t *testing.T) {
ecosystem := "npm"
scoreThreshold := 10.0
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
Loading