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

Docker Scan - scan Dockerfiles #931

Closed
wants to merge 46 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
32c823a
Refactor scan to allow the reuse of docker scan
EyalDelarea Aug 29, 2023
da9bd27
stash
EyalDelarea Aug 31, 2023
1a617cf
pull dev
EyalDelarea Sep 3, 2023
5e891d7
stash
EyalDelarea Sep 3, 2023
0011ffa
Init refactor, change calls with crashing tests
EyalDelarea Sep 3, 2023
947f07b
pull dev
EyalDelarea Sep 4, 2023
59a2513
pull dev
EyalDelarea Sep 4, 2023
a3152b8
fix static analysis and change order
EyalDelarea Sep 4, 2023
ffa9481
refactor
EyalDelarea Sep 4, 2023
6f4ab1a
Merge branch 'dev' of https://github.com/jfrog/jfrog-cli-core into re…
EyalDelarea Sep 4, 2023
08373b4
pull refactor table branch
EyalDelarea Sep 4, 2023
4c57a72
add new table
EyalDelarea Sep 4, 2023
db07a5f
add set scan type
EyalDelarea Sep 4, 2023
3fa6ee4
pull refactor
EyalDelarea Sep 4, 2023
1d5cf13
customize docker table output
EyalDelarea Sep 5, 2023
4d4fbf8
pull dev
EyalDelarea Sep 5, 2023
3edbbe0
fix static check
EyalDelarea Sep 5, 2023
0ae9afa
fix static check
EyalDelarea Sep 5, 2023
343398b
pull dev
EyalDelarea Sep 5, 2023
eb06af3
replace to dev
EyalDelarea Sep 5, 2023
87a30b3
remove predefined binary scan
EyalDelarea Sep 5, 2023
38881b1
pull refactor branch
EyalDelarea Sep 5, 2023
f632481
change name
EyalDelarea Sep 5, 2023
8575602
stash progress
EyalDelarea Sep 6, 2023
455055d
Merge branch 'dev' of https://github.com/jfrog/jfrog-cli-core into re…
EyalDelarea Sep 6, 2023
113d9e6
Merge branch 'dev' of https://github.com/jfrog/jfrog-cli-core into im…
EyalDelarea Sep 6, 2023
7537a30
map commands to line number
EyalDelarea Sep 6, 2023
391f666
handle more than one FROM command
EyalDelarea Sep 6, 2023
6ba2220
stash docker progress
EyalDelarea Sep 6, 2023
32f61cd
Merge branch 'dev' of https://github.com/jfrog/jfrog-cli-core into re…
EyalDelarea Sep 7, 2023
f33c559
pull dev
EyalDelarea Sep 10, 2023
54da48c
Merge dev
EyalDelarea Sep 14, 2023
e03fbf6
Fix static check
EyalDelarea Sep 14, 2023
b316e92
Merge branch 'dev' of https://github.com/jfrog/jfrog-cli-core into re…
EyalDelarea Sep 18, 2023
4f26435
pull refactor table
EyalDelarea Sep 18, 2023
fc61273
Add test
EyalDelarea Sep 18, 2023
1b30588
change replacce
EyalDelarea Sep 18, 2023
16e7d30
fix static check
EyalDelarea Sep 18, 2023
0d61c59
remove unneeded field
EyalDelarea Sep 18, 2023
3758764
pull dev
EyalDelarea Sep 20, 2023
3c278e9
line numbers in table are optional
EyalDelarea Sep 20, 2023
30c51f9
set dockerfile scanned bool
EyalDelarea Sep 20, 2023
3d065bb
Don't show extended CVEs on dockerscan
EyalDelarea Sep 20, 2023
8f2091f
Merge branch 'dev' of https://github.com/jfrog/jfrog-cli-core into im…
EyalDelarea Sep 20, 2023
250c14c
pull dev
EyalDelarea Sep 27, 2023
ed804f6
Add error info to docker scan
EyalDelarea Sep 27, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ require (
github.com/stretchr/testify v1.8.4
github.com/urfave/cli v1.22.14
github.com/vbauerster/mpb/v7 v7.5.3
github.com/wagoodman/dive v0.11.0
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
golang.org/x/mod v0.12.0
golang.org/x/sync v0.3.0
Expand All @@ -41,16 +42,28 @@ require (
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/acomagu/bufpipe v1.0.4 // indirect
github.com/andybalholm/brotli v1.0.1 // indirect
github.com/awesome-gocui/gocui v1.1.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v24.0.5+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v24.0.2+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/gdamore/tcell/v2 v2.4.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/go-git/go-git/v5 v5.9.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/snappy v0.0.2 // indirect
Expand All @@ -60,6 +73,8 @@ require (
github.com/klauspost/compress v1.11.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.3 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b // indirect
github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
Expand All @@ -68,14 +83,19 @@ require (
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/nwaples/rardecode v1.1.0 // indirect
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee // indirect
github.com/pierrec/lz4/v4 v4.1.2 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/term v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.3 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/skeema/knownhosts v1.2.0 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
Expand All @@ -93,3 +113,5 @@ require (
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

replace github.com/jfrog/jfrog-client-go => github.com/eyaldelarea/jfrog-client-go v1.28.1-0.20230927080409-9ac8dd6d0dd1
190 changes: 188 additions & 2 deletions go.sum

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions xray/commands/scan/buildscan.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"errors"
rtutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils"
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
"github.com/jfrog/jfrog-cli-core/v2/xray/utils"
xrutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils"
clientutils "github.com/jfrog/jfrog-client-go/utils"
"github.com/jfrog/jfrog-client-go/utils/log"
Expand Down Expand Up @@ -72,7 +71,7 @@ func (bsc *BuildScanCommand) SetRescan(rescan bool) *BuildScanCommand {

// Scan published builds with Xray
func (bsc *BuildScanCommand) Run() (err error) {
xrayManager, xrayVersion, err := utils.CreateXrayServiceManagerAndGetVersion(bsc.serverDetails)
xrayManager, xrayVersion, err := xrutils.CreateXrayServiceManagerAndGetVersion(bsc.serverDetails)
if err != nil {
return err
}
Expand Down
211 changes: 206 additions & 5 deletions xray/commands/scan/dockerscan.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package scan

import (
"bufio"
"bytes"
"fmt"
"github.com/jfrog/jfrog-cli-core/v2/common/spec"
Expand All @@ -9,21 +10,30 @@ import (
"github.com/jfrog/jfrog-client-go/utils/errorutils"
"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
"github.com/jfrog/jfrog-client-go/utils/log"
"github.com/jfrog/jfrog-client-go/xray/services"
"github.com/wagoodman/dive/dive"
"github.com/wagoodman/dive/dive/image"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
)

const (
indexerEnvPrefix = "JFROG_INDEXER_"
DockerScanMinXrayVersion = "3.40.0"
maxDisplayCommandLength = 60
layerDigestPrefix = "sha256:"
buildKitSuffix = " # buildkit"
)

type DockerScanCommand struct {
ScanCommand
imageTag string
targetRepoPath string
dockerFilePath string
scanner *bufio.Scanner
}

func NewDockerScanCommand() *DockerScanCommand {
Expand Down Expand Up @@ -61,28 +71,49 @@ func (dsc *DockerScanCommand) Run() (err error) {
err = e
}
}()
// Check for dockerfile to scan
var isDockerFileScanned bool
if dsc.imageTag == "" {
if err = dsc.buildDockerImage(); err != nil {
return
}
// Load content of dockerfile to memory to allow search by file line.
cleanUp, err := dsc.loadDockerfileToMemory()
defer cleanUp()
if err != nil {
return err
}
isDockerFileScanned = true
}

// Run the 'docker save' command, to create tar file from the docker image, and pass it to the indexer-app
if dsc.progress != nil {
dsc.progress.SetHeadlineMsg("Creating image archive 📦")
}
log.Info("Creating image archive...")
imageTarPath := filepath.Join(tempDirPath, "image.tar")

dockerSaveCmd := exec.Command("docker", "save", dsc.imageTag, "-o", imageTarPath)
var stderr bytes.Buffer
dockerSaveCmd.Stderr = &stderr
err = dockerSaveCmd.Run()
if err != nil {
if err = dockerSaveCmd.Run(); err != nil {
return fmt.Errorf("failed running command: '%s' with error: %s - %s", strings.Join(dockerSaveCmd.Args, " "), err.Error(), stderr.String())
}

// Map layers sha to build commands
// If dockerfile exists, will also map to line number.
dockerCommandsMapping, err := dsc.mapDockerLayerToCommand()
if err != nil {
return
}

// Perform scan on image.tar
dsc.SetSpec(spec.NewBuilder().
Pattern(imageTarPath).
Target(dsc.targetRepoPath).
BuildSpec()).SetThreads(1)
err = dsc.setCredentialEnvsForIndexerApp()
if err != nil {

if err = dsc.setCredentialEnvsForIndexerApp(); err != nil {
return errorutils.CheckError(err)
}
defer func() {
Expand All @@ -91,7 +122,76 @@ func (dsc *DockerScanCommand) Run() (err error) {
err = errorutils.CheckError(e)
}
}()
return dsc.ScanCommand.Run()
extendedScanResults, cleanup, scanErrors, err := dsc.ScanCommand.binaryScan()
defer cleanup()
if err != nil {
return
}

// Print results
err = xrayutils.NewResultsWriter(extendedScanResults).
SetOutputFormat(dsc.outputFormat).
SetIncludeVulnerabilities(dsc.includeVulnerabilities).
SetIncludeLicenses(dsc.includeLicenses).
SetPrintExtendedTable(dsc.printExtendedTable).
SetIsMultipleRootProject(true).
SetDockerCommandsMapping(dockerCommandsMapping, isDockerFileScanned).
SetScanType(services.Docker).
PrintScanResults()

return dsc.ScanCommand.handlePossibleErrors(extendedScanResults.XrayResults, scanErrors, err)
}

func (dsc *DockerScanCommand) buildDockerImage() (err error) {
if exists, _ := fileutils.IsFileExists(".dockerfile", false); !exists {
return fmt.Errorf("didn't find Dockerfile in the provided path: %s", dsc.dockerFilePath)
}
if dsc.progress != nil {
dsc.progress.SetHeadlineMsg("Building Docker image 🏗....️")
}
dsc.imageTag = "audittag"
log.Info("Building docker image... ")
var stderr bytes.Buffer
dockerBuildCommand := exec.Command("docker", "build", ".", "-f", ".dockerfile", "-t", dsc.imageTag)
dockerBuildCommand.Stderr = &stderr
if err = dockerBuildCommand.Run(); err != nil {
return fmt.Errorf("failed to build docker image. Is docker running on your computer? error: %s", err.Error())
}
log.Info("Successfully build image from dockerfile")
return
}

func (dsc *DockerScanCommand) mapDockerLayerToCommand() (layersMapping map[string]services.DockerfileCommandDetails, err error) {
log.Debug("Mapping docker layers into commands ")
resolver, err := dive.GetImageResolver(dive.SourceDockerEngine)
if err != nil {
return
}
dockerImage, err := resolver.Fetch(dsc.imageTag)
if err != nil {
return
}
// Create mapping between sha256 hash to dockerfile Command.
layersMapping = make(map[string]services.DockerfileCommandDetails)
for _, layer := range dockerImage.Layers {
layerHash := strings.TrimPrefix(layer.Digest, layerDigestPrefix)
layersMapping[layerHash] = services.DockerfileCommandDetails{LayerHash: layer.Digest, Command: formatCommand(layer)}
}
return dsc.mapDockerfileCommands(layersMapping)
}

func formatCommand(layer *image.Layer) string {
command := trimSpacesInMiddle(layer.Command)
command = strings.TrimSuffix(command, buildKitSuffix)
if len(command) > maxDisplayCommandLength {
command = command[:maxDisplayCommandLength] + " ..."
}
return command
}

func trimSpacesInMiddle(input string) string {
parts := strings.Fields(input) // Split the string by spaces
return strings.Join(parts, " ")
}

// When indexing RPM files inside the docker container, the indexer-app needs to connect to the Xray Server.
Expand Down Expand Up @@ -143,3 +243,104 @@ func (dsc *DockerScanCommand) unsetCredentialEnvsForIndexerApp() error {
func (dsc *DockerScanCommand) CommandName() string {
return "xr_docker_scan"
}

func (dsc *DockerScanCommand) loadDockerfileToMemory() (cleanUp func(), err error) {
file, err := os.Open(".dockerfile")
if err != nil {
err = fmt.Errorf("failed while trying to load dockerfile")
return
}
cleanUp = func() {
err = file.Close()
}
dsc.scanner = bufio.NewScanner(file)
return
}

const (
emptyDockerfileLine = ""
dockerfileCommentPrefix = "#"
backslash = "\\"
fromCommand = "FROM"
)

// Scans the dockerfile line by line and match docker commands to their respective lines.
// Lines which don't appear in dockerfile would get assigned to the corresponding FROM command.
func (dsc *DockerScanCommand) mapDockerfileCommands(dockerCommandsMap map[string]services.DockerfileCommandDetails) (map[string]services.DockerfileCommandDetails, error) {
if dsc.scanner == nil {
return dockerCommandsMap, nil
}
lineNumber := 1
fromLineNumber := 1
firstAppearanceFrom := true
for dsc.scanner.Scan() {
scannedCommand := dsc.scanner.Text()
// Skip comments in the dockerfile
if strings.HasPrefix(scannedCommand, dockerfileCommentPrefix) || scannedCommand == emptyDockerfileLine {
lineNumber++
continue
}
// Read the next line as it is the same command.
for strings.HasSuffix(scannedCommand, backslash) {
dsc.scanner.Scan()
lineNumber++
scannedCommand += dsc.scanner.Text()
}
// Assign all the unassigned commands to the FROM command before moving on.
if strings.Contains(scannedCommand, fromCommand) {
if !firstAppearanceFrom {
for key := range dockerCommandsMap {
current := dockerCommandsMap[key]
if len(current.Line) == 0 {
current.Line = append(current.Line, strconv.Itoa(fromLineNumber))
}
dockerCommandsMap[key] = current
}
}
fromLineNumber = lineNumber
firstAppearanceFrom = false
}

// TODO optimize this
for key, cmd := range dockerCommandsMap {
current := dockerCommandsMap[key]
if CommandContains(cmd.Command, scannedCommand) {
current.Line = append(current.Line, strconv.Itoa(lineNumber))
dockerCommandsMap[key] = current
break
}
}
lineNumber++
}

// Iterate again and assign all unassigned commands to that nearest FROM command
for key := range dockerCommandsMap {
current := dockerCommandsMap[key]
if len(current.Line) == 0 {
current.Line = append(current.Line, strconv.Itoa(fromLineNumber))
dockerCommandsMap[key] = current
}
}
// Check for scanner errors
if err := dsc.scanner.Err(); err != nil {
return nil, fmt.Errorf("error scanning dockerfile:%s", err.Error())
}
return dockerCommandsMap, nil
}
func CommandContains(commandFromLayer, scannedCommand string) bool {
// Normalize and split the commands into arguments
args1 := strings.Fields(commandFromLayer)
args2 := strings.Fields(scannedCommand)
// Create a map to store the arguments of commandFromLayer
argMap1 := make(map[string]bool)
for _, arg := range args1 {
argMap1[arg] = true
}
// Check if all arguments of scannedCommand are present in commandFromLayer
for _, arg := range args2 {
if !argMap1[arg] {
return false
}
}
return true
}
Loading
Loading