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
92 changes: 64 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,22 @@ func GetDependencyDiffResults(
return dCtx.results, nil
}

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

}
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 +179,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")
)
89 changes: 89 additions & 0 deletions dependencydiff/mapping.go
@@ -0,0 +1,89 @@
// 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"
)

// Ecosystem is a package ecosystem supported by OSV, GitHub, etc.
type ecosystem string

// OSV ecosystem naming data source: https://ossf.github.io/osv-schema/#affectedpackage-field
// nolint
const (
// The Go ecosystem.
ecosystemGo ecosystem = "Go"

// The NPM ecosystem.
ecosystemNpm ecosystem = "npm"

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

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

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

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

// The RubyGems ecosystem.
ecosystemRubyGems ecosystem = "RubyGems"

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

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

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

// The Linux kernel.
ecosystemLinux ecosystem = "Linux" // nolint:unused

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

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

var (
//gitHubToOSV defines the ecosystem naming mapping relationship between GitHub and others.
gitHubToOSV = map[string]ecosystem{
// GitHub ecosystem naming data source: https://docs.github.com/en/code-security/supply-chain-security/
// understanding-your-software-supply-chain/about-the-dependency-graph#supported-package-ecosystems
"gomod": ecosystemGo, /* go.mod and go.sum */
"cargo": ecosystemCrates,
"pip": ecosystemPyPI, /* pip and poetry */
"npm": ecosystemNpm, /* npm and yarn */
"maven": ecosystemMaven,
"composer": ecosystemPackagist,
"rubygems": ecosystemRubyGems,
"nuget": ecosystemNuGet,
}
)

func toEcosystem(e string) (ecosystem, error) {
if ecosystemOSV, found := gitHubToOSV[e]; found {
return ecosystemOSV, nil
}
return "", fmt.Errorf("%w for github entry %s", errMappingNotFound, e)
}
7 changes: 6 additions & 1 deletion dependencydiff/raw_dependencies.go
Expand Up @@ -58,7 +58,7 @@ func fetchRawDependencyDiffData(dCtx *dependencydiffContext) error {
req, err := ghClient.NewRequest(
"GET",
path.Join("repos", dCtx.ownerName, dCtx.repoName,
"dependency-graph", "compare", dCtx.baseSHA+"..."+dCtx.headSHA),
"dependency-graph", "compare", dCtx.base+"..."+dCtx.head),
nil,
)
if err != nil {
Expand All @@ -68,5 +68,10 @@ func fetchRawDependencyDiffData(dCtx *dependencydiffContext) error {
if err != nil {
return fmt.Errorf("error parsing the dependency-diff reponse: %w", err)
}
for _, d := range dCtx.dependencydiffs {
if !d.ChangeType.IsValid() {
return fmt.Errorf("%w: change type", errInvalid)
}
}
return nil
}