Skip to content

Commit

Permalink
✨ Use crane to add hash suggestion to unpinned Docker images (#2037)
Browse files Browse the repository at this point in the history
* Use crane to add hash suggestion to unpinned Docker images

* Add nil check before dereferencing name for image digest

* Reformat changes to comply with linter

* Add basic remediation for dockerfile pinning

* Deduplicate remediation code

* Remove reference to linux/amd64, as crane digest should be universal

* add remediation info to scorecard output. switch to using strings.Builder for more maintainable logic
  • Loading branch information
spencerschrock committed Jul 19, 2022
1 parent a905d66 commit 096cbd0
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 12 deletions.
8 changes: 6 additions & 2 deletions checks/evaluation/pinned_dependencies.go
Expand Up @@ -134,10 +134,14 @@ func PinningDependencies(name string, dl checker.DetailLogger,
}

func generateRemediation(rr *checker.Dependency) *checker.Remediation {
if rr.Type == checker.DependencyUseTypeGHAction {
switch rr.Type {
case checker.DependencyUseTypeGHAction:
return remediation.CreateWorkflowPinningRemediation(rr.Location.Path)
case checker.DependencyUseTypeDockerfileContainerImage:
return remediation.CreateDockerfilePinningRemediation(rr.Name)
default:
return nil
}
return nil
}

func updatePinningResults(rr *checker.Dependency,
Expand Down
26 changes: 17 additions & 9 deletions pkg/common.go
Expand Up @@ -32,16 +32,24 @@ func DetailToString(d *checker.CheckDetail, logLevel log.Level) string {
return ""
}

switch {
case d.Msg.Path != "" && d.Msg.Offset != 0 && d.Msg.EndOffset != 0 && d.Msg.Offset < d.Msg.EndOffset:
return fmt.Sprintf("%s: %s: %s:%d-%d", typeToString(d.Type), d.Msg.Text, d.Msg.Path, d.Msg.Offset, d.Msg.EndOffset)
case d.Msg.Path != "" && d.Msg.Offset != 0:
return fmt.Sprintf("%s: %s: %s:%d", typeToString(d.Type), d.Msg.Text, d.Msg.Path, d.Msg.Offset)
case d.Msg.Path != "" && d.Msg.Offset == 0:
return fmt.Sprintf("%s: %s: %s", typeToString(d.Type), d.Msg.Text, d.Msg.Path)
default:
return fmt.Sprintf("%s: %s", typeToString(d.Type), d.Msg.Text)
var sb strings.Builder
sb.WriteString(fmt.Sprintf("%s: %s", typeToString(d.Type), d.Msg.Text))

if d.Msg.Path != "" {
sb.WriteString(fmt.Sprintf(": %s", d.Msg.Path))
if d.Msg.Offset != 0 {
sb.WriteString(fmt.Sprintf(":%d", d.Msg.Offset))
}
if d.Msg.EndOffset != 0 && d.Msg.Offset < d.Msg.EndOffset {
sb.WriteString(fmt.Sprintf("-%d", d.Msg.EndOffset))
}
}

if d.Msg.Remediation != nil {
sb.WriteString(fmt.Sprintf(": %s", d.Msg.Remediation.HelpText))
}

return sb.String()
}

func detailsToString(details []checker.CheckDetail, logLevel log.Level) (string, bool) {
Expand Down
144 changes: 144 additions & 0 deletions pkg/common_test.go
@@ -0,0 +1,144 @@
// 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 pkg

import (
"testing"

"github.com/ossf/scorecard/v4/checker"
"github.com/ossf/scorecard/v4/log"
)

func TestDetailString(t *testing.T) {
t.Parallel()
tests := []struct {
name string
detail checker.CheckDetail
log log.Level
want string
}{
{
name: "ignoreDebug",
detail: checker.CheckDetail{
Msg: checker.LogMessage{
Text: "should not appear",
},
Type: checker.DetailDebug,
},
log: log.DefaultLevel,
want: "",
},
{
name: "includeDebug",
detail: checker.CheckDetail{
Msg: checker.LogMessage{
Text: "should appear",
},
Type: checker.DetailDebug,
},
log: log.DebugLevel,
want: "Debug: should appear",
},
{
name: "onlyType",
detail: checker.CheckDetail{
Msg: checker.LogMessage{
Text: "some meaningful text",
},
Type: checker.DetailWarn,
},
log: log.DefaultLevel,
want: "Warn: some meaningful text",
},
{
name: "displayPath",
detail: checker.CheckDetail{
Msg: checker.LogMessage{
Text: "some meaningful text",
Path: "Dockerfile",
},
Type: checker.DetailWarn,
},
log: log.DefaultLevel,
want: "Warn: some meaningful text: Dockerfile",
},
{
name: "displayStartOffset",
detail: checker.CheckDetail{
Msg: checker.LogMessage{
Text: "some meaningful text",
Path: "Dockerfile",
Offset: 1,
},
Type: checker.DetailWarn,
},
log: log.DefaultLevel,
want: "Warn: some meaningful text: Dockerfile:1",
},
{
name: "displayEndOffset",
detail: checker.CheckDetail{
Msg: checker.LogMessage{
Text: "some meaningful text",
Path: "Dockerfile",
Offset: 1,
EndOffset: 7,
},
Type: checker.DetailWarn,
},
log: log.DefaultLevel,
want: "Warn: some meaningful text: Dockerfile:1-7",
},
{
name: "ignoreInvalidEndOffset",
detail: checker.CheckDetail{
Msg: checker.LogMessage{
Text: "some meaningful text",
Path: "Dockerfile",
Offset: 3,
EndOffset: 2,
},
Type: checker.DetailWarn,
},
log: log.DefaultLevel,
want: "Warn: some meaningful text: Dockerfile:3",
},
{
name: "includeRemediation",
detail: checker.CheckDetail{
Msg: checker.LogMessage{
Text: "some meaningful text",
Path: "Dockerfile",
Remediation: &checker.Remediation{
HelpText: "fix x by doing y",
},
},
Type: checker.DetailWarn,
},
log: log.DefaultLevel,
want: "Warn: some meaningful text: Dockerfile: fix x by doing y",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := DetailToString(&tt.detail, tt.log)
if got != tt.want {
t.Errorf("got %v, want %v", got, tt.want)
}
})
}
}
24 changes: 23 additions & 1 deletion remediation/remediations.go
Expand Up @@ -20,6 +20,8 @@ import (
"strings"
"sync"

"github.com/google/go-containerregistry/pkg/crane"

"github.com/ossf/scorecard/v4/checker"
"github.com/ossf/scorecard/v4/clients"
)
Expand All @@ -36,7 +38,8 @@ var errInvalidArg = errors.New("invalid argument")
var (
workflowText = "update your workflow using https://app.stepsecurity.io/secureworkflow/%s/%s/%s?enable=%s"
//nolint
workflowMarkdown = "update your workflow using [https://app.stepsecurity.io](https://app.stepsecurity.io/secureworkflow/%s/%s/%s?enable=%s)"
workflowMarkdown = "update your workflow using [https://app.stepsecurity.io](https://app.stepsecurity.io/secureworkflow/%s/%s/%s?enable=%s)"
dockerfilePinText = "pin your Docker image by updating %[1]s to %[1]s@%s"
)

//nolint:gochecknoinits
Expand Down Expand Up @@ -94,3 +97,22 @@ func createWorkflowRemediation(path, t string) *checker.Remediation {
HelpMarkdown: markdown,
}
}

// CreateDockerfilePinningRemediation create remediaiton for pinning Dockerfile images.
func CreateDockerfilePinningRemediation(name *string) *checker.Remediation {
if name == nil {
return nil
}
hash, err := crane.Digest(*name)
if err != nil {
return nil
}

text := fmt.Sprintf(dockerfilePinText, *name, hash)
markdown := text

return &checker.Remediation{
HelpText: text,
HelpMarkdown: markdown,
}
}

0 comments on commit 096cbd0

Please sign in to comment.