Skip to content

Commit

Permalink
✨ Raw results for Pinned-Dependencies (#1932)
Browse files Browse the repository at this point in the history
* backup

* update

* update

* draft

* updates

* updates

* updates

* updates

* fix

* linter

* updates

* updates

* updates

* updates

* updates

* updates

* updates

* linter

* comments

* linter

* linter

* tests

* updates

* updates

* tests
  • Loading branch information
laurentsimon committed Jun 6, 2022
1 parent 23523f6 commit 4bd3391
Show file tree
Hide file tree
Showing 73 changed files with 2,679 additions and 2,583 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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)
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
}
Loading

0 comments on commit 4bd3391

Please sign in to comment.