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

✨ Raw results for Pinned-Dependencies #1932

Merged
merged 27 commits into from Jun 6, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Expand Up @@ -879,5 +879,5 @@ jobs:
run: |
go env -w GOFLAGS=-mod=mod
go install github.com/google/addlicense@2fe3ee94479d08be985a84861de4e6b06a1c7208
addlicense -ignore "**/script-empty.sh" -ignore "pkg/testdata/**" -ignore "checks/testdata/**" -l apache -c 'Security Scorecard Authors' -v *
addlicense -ignore "**/script-empty.sh" -ignore "testdata/**" -ignore "**/testdata/**" -l apache -c 'Security Scorecard Authors' -v *
git diff --exit-code
46 changes: 42 additions & 4 deletions checker/raw_result.go
Expand Up @@ -31,6 +31,7 @@ type RawResults struct {
DependencyUpdateToolResults DependencyUpdateToolData
BranchProtectionResults BranchProtectionsData
CodeReviewResults CodeReviewData
PinningDependenciesResults PinningDependenciesData
WebhookResults WebhooksData
ContributorsResults ContributorsData
MaintainedResults MaintainedData
Expand Down Expand Up @@ -64,6 +65,42 @@ type Package struct {
Runs []Run
}

// DependencyUseType reprensets a type of dependency use.
type DependencyUseType string

const (
// DependencyUseTypeGHAction is an action.
DependencyUseTypeGHAction DependencyUseType = "GitHubAction"
// DependencyUseTypeDockerfileContainerImage a container image used via FROM.
DependencyUseTypeDockerfileContainerImage DependencyUseType = "containerImage"
// DependencyUseTypeDownloadThenRun is a download followed by a run.
DependencyUseTypeDownloadThenRun DependencyUseType = "downloadThenRun"
// DependencyUseTypeGoCommand is a go command.
DependencyUseTypeGoCommand DependencyUseType = "goCommand"
// DependencyUseTypeChocoCommand is a choco command.
DependencyUseTypeChocoCommand DependencyUseType = "chocoCommand"
// DependencyUseTypeNpmCommand is an npm command.
DependencyUseTypeNpmCommand DependencyUseType = "npmCommand"
// DependencyUseTypePipCommand is a pipp command.
DependencyUseTypePipCommand DependencyUseType = "pipCommand"
)

// PinningDependenciesData represents pinned dependency data.
type PinningDependenciesData struct {
Dependencies []Dependency
}

// Dependency represents a dependency.
type Dependency struct {
// TODO: unique dependency name.
// TODO: Job *WorkflowJob
Name *string
PinnedAt *string
Location *File
Msg *string // Only for debug messages.
Type DependencyUseType
}

// MaintainedData contains the raw results
// for the Maintained check.
type MaintainedData struct {
Expand Down Expand Up @@ -165,10 +202,11 @@ type ArchivedStatus struct {

// File represents a file.
type File struct {
Path string
Snippet string // Snippet of code
Offset uint // Offset in the file of Path (line for source/text files).
Type FileType // Type of file.
Path string
Snippet string // Snippet of code
Offset uint // Offset in the file of Path (line for source/text files).
EndOffset uint // End of offset in the file, e.g. if the command spans multiple lines.
Type FileType // Type of file.
// TODO: add hash.
}

Expand Down
1 change: 0 additions & 1 deletion checks/errors.go
Expand Up @@ -19,7 +19,6 @@ import (
)

var (
errInternalInvalidDockerFile = errors.New("invalid Dockerfile")
errInvalidGitHubWorkflow = errors.New("invalid GitHub workflow")
errInternalNameCannotBeEmpty = errors.New("name cannot be empty")
errInternalCheckFuncCannotBeNil = errors.New("checkFunc cannot be nil")
Expand Down
292 changes: 292 additions & 0 deletions checks/evaluation/pinned_dependencies.go
@@ -0,0 +1,292 @@
// Copyright 2021 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 evaluation

import (
"errors"
"fmt"

"github.com/ossf/scorecard/v4/checker"
"github.com/ossf/scorecard/v4/checks/fileparser"
sce "github.com/ossf/scorecard/v4/errors"
"github.com/ossf/scorecard/v4/remediation"
)

var errInvalidValue = errors.New("invalid value")

type pinnedResult int

const (
pinnedUndefined pinnedResult = iota
pinned
notPinned
)

// Structure to host information about pinned github
// or third party dependencies.
type worklowPinningResult struct {
thirdParties pinnedResult
gitHubOwned pinnedResult
}

// PinningDependencies applies the score policy for the Pinned-Dependencies check.
func PinningDependencies(name string, dl checker.DetailLogger,
r *checker.PinningDependenciesData,
) checker.CheckResult {
if r == nil {
e := sce.WithMessage(sce.ErrScorecardInternal, "empty raw data")
return checker.CreateRuntimeErrorResult(name, e)
}

var wp worklowPinningResult
pr := make(map[checker.DependencyUseType]pinnedResult)

for i := range r.Dependencies {
rr := r.Dependencies[i]
if rr.Location == nil {
if rr.Msg == nil {
e := sce.WithMessage(sce.ErrScorecardInternal, "empty File field")
return checker.CreateRuntimeErrorResult(name, e)
}
dl.Debug(&checker.LogMessage{
Text: *rr.Msg,
})
continue
}

if rr.Msg != nil {
dl.Debug(&checker.LogMessage{
Path: rr.Location.Path,
Type: rr.Location.Type,
Offset: rr.Location.Offset,
EndOffset: rr.Location.EndOffset,
Text: *rr.Msg,
Snippet: rr.Location.Snippet,
})
} else {
dl.Warn(&checker.LogMessage{
Path: rr.Location.Path,
Type: rr.Location.Type,
Offset: rr.Location.Offset,
EndOffset: rr.Location.EndOffset,
Text: generateText(&rr),
Snippet: rr.Location.Snippet,
Remediation: generateRemediation(&rr),
})

// Update the pinning status.
updatePinningResults(&rr, &wp, pr)
}
}

// Generate scores and Info results.
// GitHub actions.
actionScore, err := createReturnForIsGitHubActionsWorkflowPinned(wp, dl)
if err != nil {
return checker.CreateRuntimeErrorResult(name, err)
}

// Docker files.
dockerFromScore, err := createReturnForIsDockerfilePinned(pr, dl)
if err != nil {
return checker.CreateRuntimeErrorResult(name, err)
}

// Docker downloads.
dockerDownloadScore, err := createReturnForIsDockerfileFreeOfInsecureDownloads(pr, dl)
if err != nil {
return checker.CreateRuntimeErrorResult(name, err)
}

// Script downloads.
scriptScore, err := createReturnForIsShellScriptFreeOfInsecureDownloads(pr, dl)
if err != nil {
return checker.CreateRuntimeErrorResult(name, err)
}

// Scores may be inconclusive.
actionScore = maxScore(0, actionScore)
dockerFromScore = maxScore(0, dockerFromScore)
dockerDownloadScore = maxScore(0, dockerDownloadScore)
scriptScore = maxScore(0, scriptScore)

score := checker.AggregateScores(actionScore, dockerFromScore,
dockerDownloadScore, scriptScore)

if score == checker.MaxResultScore {
return checker.CreateMaxScoreResult(name, "all dependencies are pinned")
}

return checker.CreateProportionalScoreResult(name,
"dependency not pinned by hash detected", score, checker.MaxResultScore)
}

func generateRemediation(rr *checker.Dependency) *checker.Remediation {
if rr.Type == checker.DependencyUseTypeGHAction {
return remediation.CreateWorkflowPinningRemediation(rr.Location.Path)
}
return nil
}

func updatePinningResults(rr *checker.Dependency,
wp *worklowPinningResult, pr map[checker.DependencyUseType]pinnedResult,
) {
if rr.Type == checker.DependencyUseTypeGHAction {
// Note: `Snippet` contains `action/name@xxx`, so we cna use it to infer
// if it's a GitHub-owned action or not.
gitHubOwned := fileparser.IsGitHubOwnedAction(rr.Location.Snippet)
addWorkflowPinnedResult(wp, false, gitHubOwned)
return
}

// Update other result types.
var p pinnedResult
addPinnedResult(&p, false)
pr[rr.Type] = p
}

func generateText(rr *checker.Dependency) string {
if rr.Type == checker.DependencyUseTypeGHAction {
// Check if we are dealing with a GitHub action or a third-party one.
gitHubOwned := fileparser.IsGitHubOwnedAction(rr.Location.Snippet)
laurentsimon marked this conversation as resolved.
Show resolved Hide resolved
owner := generateOwnerToDisplay(gitHubOwned)
return fmt.Sprintf("%s %s not pinned by hash", owner, rr.Type)
}

return fmt.Sprintf("%s not pinned by hash", rr.Type)
}

func generateOwnerToDisplay(gitHubOwned bool) string {
if gitHubOwned {
return "GitHub-owned"
}
return "third-party"
}

// TODO(laurent): need to support GCB pinning.
//nolint
func maxScore(s1, s2 int) int {
if s1 > s2 {
return s1
}
return s2
}

// For the 'to' param, true means the file is pinning dependencies (or there are no dependencies),
// false means there are unpinned dependencies.
func addPinnedResult(r *pinnedResult, to bool) {
// If the result is `notPinned`, we keep it.
// In other cases, we always update the result.
if *r == notPinned {
return
}

switch to {
case true:
*r = pinned
case false:
*r = notPinned
}
}

func addWorkflowPinnedResult(w *worklowPinningResult, to, isGitHub bool) {
if isGitHub {
addPinnedResult(&w.gitHubOwned, to)
} else {
addPinnedResult(&w.thirdParties, to)
}
}

// Create the result for scripts.
func createReturnForIsShellScriptFreeOfInsecureDownloads(pr map[checker.DependencyUseType]pinnedResult,
dl checker.DetailLogger,
) (int, error) {
return createReturnValues(pr, checker.DependencyUseTypeDownloadThenRun,
"no insecure (not pinned by hash) dependency downloads found in shell scripts",
dl)
}

// Create the result for docker containers.
func createReturnForIsDockerfilePinned(pr map[checker.DependencyUseType]pinnedResult,
dl checker.DetailLogger,
) (int, error) {
return createReturnValues(pr, checker.DependencyUseTypeDockerfileContainerImage,
"Dockerfile dependencies are pinned",
dl)
}

// Create the result for docker commands.
func createReturnForIsDockerfileFreeOfInsecureDownloads(pr map[checker.DependencyUseType]pinnedResult,
dl checker.DetailLogger,
) (int, error) {
return createReturnValues(pr, checker.DependencyUseTypeDownloadThenRun,
"no insecure (not pinned by hash) dependency downloads found in Dockerfiles",
dl)
}

func createReturnValues(pr map[checker.DependencyUseType]pinnedResult,
t checker.DependencyUseType, infoMsg string,
dl checker.DetailLogger,
) (int, error) {
// Note: we don't check if the entry exists,
// as it will have the default value which is handled in the switch statement.
//nolint
r, _ := pr[t]
switch r {
default:
return checker.InconclusiveResultScore, fmt.Errorf("%w: %v", errInvalidValue, r)
case pinned, pinnedUndefined:
dl.Info(&checker.LogMessage{
Text: infoMsg,
})
return checker.MaxResultScore, nil
case notPinned:
// No logging needed as it's done by the checks.
return checker.MinResultScore, nil
}
}

// Create the result.
func createReturnForIsGitHubActionsWorkflowPinned(wp worklowPinningResult, dl checker.DetailLogger) (int, error) {
return createReturnValuesForGitHubActionsWorkflowPinned(wp,
fmt.Sprintf("%ss are pinned", checker.DependencyUseTypeGHAction),
dl)
}

func createReturnValuesForGitHubActionsWorkflowPinned(r worklowPinningResult, infoMsg string,
dl checker.DetailLogger,
) (int, error) {
score := checker.MinResultScore

if r.gitHubOwned != notPinned {
score += 2
dl.Info(&checker.LogMessage{
Type: checker.FileTypeSource,
Offset: checker.OffsetDefault,
Text: fmt.Sprintf("%s %s", "GitHub-owned", infoMsg),
})
}

if r.thirdParties != notPinned {
score += 8
dl.Info(&checker.LogMessage{
Type: checker.FileTypeSource,
Offset: checker.OffsetDefault,
Text: fmt.Sprintf("%s %s", "Third-party", infoMsg),
})
}

return score, nil
}