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

✨ Feature: Dependency-diff ecosystem naming convention mapping (GitHub -> OSV) #2088

Merged
merged 12 commits into from Jul 25, 2022
96 changes: 68 additions & 28 deletions dependencydiff/dependencydiff.go
Expand Up @@ -17,12 +17,13 @@ package dependencydiff
import (
"context"
"fmt"
"strings"

"github.com/ossf/scorecard/v4/checker"
"github.com/ossf/scorecard/v4/checks"
"github.com/ossf/scorecard/v4/clients"
sce "github.com/ossf/scorecard/v4/errors"
"github.com/ossf/scorecard/v4/log"
sclog "github.com/ossf/scorecard/v4/log"
"github.com/ossf/scorecard/v4/pkg"
"github.com/ossf/scorecard/v4/policy"
)
Expand All @@ -31,44 +32,56 @@ import (
const Depdiff = "Dependency-diff"

type dependencydiffContext struct {
logger *log.Logger
ownerName, repoName, baseSHA, headSHA string
ctx context.Context
ghRepo clients.Repo
ghRepoClient clients.RepoClient
ossFuzzClient clients.RepoClient
vulnsClient clients.VulnerabilitiesClient
ciiClient clients.CIIBestPracticesClient
changeTypesToCheck map[pkg.ChangeType]bool
checkNamesToRun []string
dependencydiffs []dependency
results []pkg.DependencyCheckResult
logger *sclog.Logger
ownerName, repoName, base, head string
ctx context.Context
ghRepo clients.Repo
ghRepoClient clients.RepoClient
ossFuzzClient clients.RepoClient
vulnsClient clients.VulnerabilitiesClient
ciiClient clients.CIIBestPracticesClient
changeTypesToCheck map[pkg.ChangeType]bool
checkNamesToRun []string
dependencydiffs []dependency
results []pkg.DependencyCheckResult
}

// GetDependencyDiffResults gets dependency changes between two given code commits BASE and HEAD
// along with the Scorecard check results of the dependencies, and returns a slice of DependencyCheckResult.
// TO use this API, an access token must be set following https://github.com/ossf/scorecard#authentication.
// TO use this API, an access token must be set. See https://github.com/ossf/scorecard#authentication.
func GetDependencyDiffResults(
ctx context.Context, ownerName, repoName, baseSHA, headSHA string, scorecardChecksNames []string,
changeTypesToCheck map[pkg.ChangeType]bool) ([]pkg.DependencyCheckResult, error) {
// Fetch the raw dependency diffs.
ctx context.Context,
repoURI string, /* Use the format "ownerName/repoName" as the repo URI, such as "ossf/scorecard". */
base, head string, /* Two code commits base and head, can use either SHAs or branch names. */
checksToRun []string, /* A list of enabled check names to run. */
changeTypesToCheck map[pkg.ChangeType]bool, /* A list of change types for which to surface scorecard results. */
) ([]pkg.DependencyCheckResult, error) {

logger := sclog.NewLogger(sclog.DefaultLevel)
ownerAndRepo := strings.Split(repoURI, "/")
if len(ownerAndRepo) != 2 {
return nil, fmt.Errorf("%w: repo uri input", errInvalid)
}
owner, repo := ownerAndRepo[0], ownerAndRepo[1]
dCtx := dependencydiffContext{
logger: log.NewLogger(log.InfoLevel),
ownerName: ownerName,
repoName: repoName,
baseSHA: baseSHA,
headSHA: headSHA,
logger: logger,
ownerName: owner,
repoName: repo,
base: base,
head: head,
ctx: ctx,
changeTypesToCheck: changeTypesToCheck,
checkNamesToRun: scorecardChecksNames,
checkNamesToRun: checksToRun,
}
// Fetch the raw dependency diffs. This API will also handle error cases such as invalid base or head.
err := fetchRawDependencyDiffData(&dCtx)
// Map the ecosystem naming convention from GitHub to OSV.
if err != nil {
return nil, fmt.Errorf("error in fetchRawDependencyDiffData: %w", err)
}

err = mapDependencyEcosystemNaming(dCtx.dependencydiffs)
if err != nil {
return nil, fmt.Errorf("error in initRepoAndClientByChecks: %w", err)
return nil, fmt.Errorf("error in mapDependencyEcosystemNaming: %w", err)
}
err = getScorecardCheckResults(&dCtx)
if err != nil {
Expand All @@ -77,6 +90,26 @@ func GetDependencyDiffResults(
return dCtx.results, nil
}

func mapDependencyEcosystemNaming(deps []dependency) error {
for i := range deps {
if deps[i].Ecosystem == nil {
continue
}
ghEcosys := ecosystemGitHub(*deps[i].Ecosystem)
if !ghEcosys.isValid() {
return fmt.Errorf("%w: github ecosystem", errInvalid)
}
osvEcosys, err := ghEcosys.toOSV()
if err != nil {
wrappedErr := fmt.Errorf("error mapping dependency ecosystem: %w", err)
return wrappedErr
}
deps[i].Ecosystem = asPointer(string(osvEcosys))

}
return nil
}

func initRepoAndClientByChecks(dCtx *dependencydiffContext, dSrcRepo string) error {
repo, repoClient, ossFuzzClient, ciiClient, vulnsClient, err := checker.GetClients(
dCtx.ctx, dSrcRepo, "", dCtx.logger,
Expand Down Expand Up @@ -150,13 +183,20 @@ func getScorecardCheckResults(dCtx *dependencydiffContext) error {
// If the run fails, we leave the current dependency scorecard result empty and record the error
// rather than letting the entire API return nil since we still expect results for other dependencies.
if err != nil {
depCheckResult.ScorecardResultsWithError.Error = sce.WithMessage(sce.ErrScorecardInternal,
fmt.Sprintf("error running the scorecard checks: %v", err))
wrappedErr := sce.WithMessage(sce.ErrScorecardInternal,
fmt.Sprintf("scorecard running failed for %s: %v", d.Name, err))
dCtx.logger.Error(wrappedErr, "")
depCheckResult.ScorecardResultWithError.Error = wrappedErr

} else { // Otherwise, we record the scorecard check results for this dependency.
depCheckResult.ScorecardResultsWithError.ScorecardResults = &scorecardResult
depCheckResult.ScorecardResultWithError.ScorecardResult = &scorecardResult
}
}
dCtx.results = append(dCtx.results, depCheckResult)
}
return nil
}

func asPointer(s string) *string {
return &s
}
68 changes: 66 additions & 2 deletions dependencydiff/dependencydiff_test.go
Expand Up @@ -16,6 +16,7 @@ package dependencydiff

import (
"context"
"errors"
"path"
"testing"

Expand All @@ -40,8 +41,8 @@ func Test_fetchRawDependencyDiffData(t *testing.T) {
ctx: context.Background(),
ownerName: "no_such_owner",
repoName: "repo_not_exist",
baseSHA: "base",
headSHA: clients.HeadSHA,
base: "main",
head: clients.HeadSHA,
},
wantEmpty: true,
wantErr: true,
Expand Down Expand Up @@ -158,3 +159,66 @@ func Test_getScorecardCheckResults(t *testing.T) {
})
}
}

func Test_mapDependencyEcosystemNaming(t *testing.T) {
t.Parallel()
//nolint
tests := []struct {
name string
deps []dependency
errWanted error
}{
{
name: "error invalid github ecosystem",
deps: []dependency{
{
Name: "dependency_1",
Ecosystem: asPointer("not_supported"),
},
{
Name: "dependency_2",
Ecosystem: asPointer("gomod"),
},
},
errWanted: errInvalid,
},
{
name: "error cannot find mapping",
deps: []dependency{
{
Name: "dependency_3",
Ecosystem: asPointer("actions"),
},
},
errWanted: errInvalid,
},
{
name: "correct mapping",
deps: []dependency{
{
Name: "dependency_4",
Ecosystem: asPointer("gomod"),
},
{
Name: "dependency_5",
Ecosystem: asPointer("pip"),
},
{
Name: "dependency_6",
Ecosystem: asPointer("cargo"),
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := mapDependencyEcosystemNaming(tt.deps)
if tt.errWanted != nil && errors.Is(tt.errWanted, err) {
t.Errorf("not a wanted error, want:%v, got:%v", tt.errWanted, err)
return
}
})
}
}
23 changes: 23 additions & 0 deletions dependencydiff/errors.go
@@ -0,0 +1,23 @@
// Copyright 2022 Security Scorecard Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dependencydiff

import "errors"

// static Errors for mapping
var (
errMappingNotFound = errors.New("ecosystem mapping not found")
errInvalid = errors.New("invalid")
)
132 changes: 132 additions & 0 deletions dependencydiff/mapping.go
@@ -0,0 +1,132 @@
// Copyright 2022 Security Scorecard Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dependencydiff

import (
"fmt"
)

// EcosystemOSV is a package ecosystem supported by OSV.
type ecosystemOSV string

// Data source: https://ossf.github.io/osv-schema/#affectedpackage-field
const (
// The Go ecosystem.
goOSV ecosystemOSV = "Go"
aidenwang9867 marked this conversation as resolved.
Show resolved Hide resolved

// The NPM ecosystem.
npmOSV = "npm"

// The Android ecosystem
androidOSV = "Android" // nolint:unused

// The crates.io ecosystem for RUST.
cratesOSV = "crates.io"

// For reports from the OSS-Fuzz project that have no more appropriate ecosystem.
ossFuzzOSV = "OSS-Fuzz" // nolint:unused

// The Python PyPI ecosystem. PyPI is the main package source of pip.
pyPIOSV = "PyPI"

// The RubyGems ecosystem.
rubyGemsOSV = "RubyGems"

// The PHP package manager ecosystem. Packagist is the main Composer repository.
packagistOSV = "Packagist"

// The Maven Java package ecosystem.
mavenOSV = "Maven"

// The NuGet package ecosystem.
nuGetOSV = "Nuget"

// The Linux kernel.
linuxOSV = "Linux" // nolint:unused

// The Debian package ecosystem.
debianOSV = "Debian" // nolint:unused

// Hex is the package manager of Erlang.
// TODO: GitHub doesn't support hex as the ecosystem for Erlang yet. Add this mapping in the future.
hexOSV = "Hex" // nolint:unused
)

// ecosystemGitHub is a package ecosystem supported by GitHub.
type ecosystemGitHub string

// Data source: https://docs.github.com/en/code-security/supply-chain-security/understanding-
// your-software-supply-chain/about-the-dependency-graph#supported-package-ecosystems
// nolint
const (
aidenwang9867 marked this conversation as resolved.
Show resolved Hide resolved
// The Go ecosystem on GitHub includes go.mod and go.sum, but both use gomod as the ecosystem name.
goGitHub ecosystemGitHub = "gomod"

// Npm is the package manager of JavaScript.
// Yarn is another package manager of JavaScript, GitHub also uses "npm" as its ecosys name.
npmGitHub ecosystemGitHub = "npm"

// RubyGems is the package manager of Ruby.
rubyGemsGitHub ecosystemGitHub = "rubygems"

// Pip is the package manager of Python.
// Poetry is another package manager of Python, GitHub also uses "pip" as its ecosys name.
pipGitHub ecosystemGitHub = "pip"

// Action is the GitHub Action.
actionGitHub ecosystemGitHub = "actions" // nolint:unused

// Cargo is the package manager of RUST.
cargoGitHub ecosystemGitHub = "cargo"

// Composer is the package manager of PHP, there is currently no mapping to the OSV.
composerGitHub ecosystemGitHub = "composer"

// NuGet is the package manager of .NET languages (C#, F#, VB), C++.
nuGetGitHub ecosystemGitHub = "nuget"

// Maven is the package manager of Java and Scala.
mavenGitHub ecosystemGitHub = "maven"
)

func (e ecosystemGitHub) isValid() bool {
switch e {
case goGitHub, npmGitHub, rubyGemsGitHub, pipGitHub, actionGitHub,
cargoGitHub, composerGitHub, nuGetGitHub, mavenGitHub:
return true
default:
return false
}
}

var (
gitHubToOSV = map[ecosystemGitHub]ecosystemOSV{
goGitHub: goOSV, /* go.mod and go.sum */
cargoGitHub: cratesOSV,
pipGitHub: pyPIOSV, /* pip and poetry */
npmGitHub: npmOSV, /* npm and yarn */
mavenGitHub: mavenOSV,
composerGitHub: packagistOSV,
rubyGemsGitHub: rubyGemsOSV,
nuGetGitHub: nuGetOSV,
}
)

func (e ecosystemGitHub) toOSV() (ecosystemOSV, error) {
if ecosystemOSV, found := gitHubToOSV[e]; found {
return ecosystemOSV, nil
}
return "", fmt.Errorf("%w for github entry %s", errMappingNotFound, e)
}