Skip to content
This repository has been archived by the owner on Jan 30, 2023. It is now read-only.

Commit

Permalink
refactor: refactored and added tests to "docker" package
Browse files Browse the repository at this point in the history
  • Loading branch information
temsa committed Aug 2, 2018
1 parent caf871d commit a9f3513
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 42 deletions.
2 changes: 1 addition & 1 deletion Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

126 changes: 85 additions & 41 deletions docker/docker.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package docker

import (
"context"
"encoding/json"
"fmt"
"io"
Expand All @@ -17,6 +16,7 @@ import (
unarr "github.com/gen2brain/go-unarr"
"github.com/nearform/gammaray/analyzer"
"github.com/nearform/gammaray/vulnfetcher"
"golang.org/x/net/context"
)

type DockerImageFiles struct {
Expand All @@ -40,59 +40,59 @@ func Cleanup(path string) {
}
}

// ScanImage extracts an image and analyzes its layers
func ScanImage(imageName string, projectPath string) (vulnfetcher.VulnerabilityReport, error) {
ctx := context.Background()
cli, err := docker.NewEnvClient()
if err != nil {
log.Println("Could not connect to docker")
return nil, err
}

func pullImageIfNecessary(ctx context.Context, imageName string, cli *docker.Client) error {
reader, err := cli.ImagePull(ctx, imageName, types.ImagePullOptions{})
if err != nil {
log.Println("Cannot pull image <", imageName, ">, will try to use a local version")
// return nil, err
} else {
// io.Copy(os.Stdout, reader) //JSONLD pull logs
_, err = ioutil.ReadAll(reader)
if err != nil {
log.Println("Could not pull image <", imageName, ">")
return nil, err
}
log.Println("⚠️ Cannot pull image <", imageName, ">, will try to use a local version")
return nil
}

response, err := cli.ImageSave(ctx, []string{imageName})
// io.Copy(os.Stdout, reader) //JSONLD pull logs
_, err = ioutil.ReadAll(reader)
if err != nil {
log.Println("Could not save image <", imageName, ">")
return nil, err
log.Println("Could not pull image <", imageName, ">")
return err
}
imageFolder := path.Join(os.TempDir(), strconv.FormatInt(time.Now().Unix(), 10))

defer Cleanup(imageFolder)

tarFile := imageFolder + ".tar"

f, err := os.Create(tarFile)
if err != nil {
log.Println("Could create image <", imageName, "> tar <", tarFile, ">")
return nil, err
}
io.Copy(f, response)
return nil
}

func extractImageArchive(tarFile string, imageFolder string, imageName string) error {
a, err := unarr.NewArchive(tarFile)
if err != nil {
log.Println("Could not open docker image <", tarFile, ">")
return nil, err
return err
}
err = a.Extract(imageFolder)
if err != nil {
log.Println("Could not extract docker image <", tarFile, ">")
return nil, err
return err
}

fmt.Println("Docker image <", imageName, "> decompressed in <", imageFolder, ">")
return nil
}

func exportImageLocally(ctx context.Context, imageName string, imageFolder string, cli *docker.Client) error {
response, err := cli.ImageSave(ctx, []string{imageName})
if err != nil {
log.Println("Could not save image <", imageName, ">")
return err
}

tarFile := imageFolder + ".tar"

f, err := os.Create(tarFile)
if err != nil {
log.Println("Could not create image <", imageName, "> tar <", tarFile, ">")
return err
}
io.Copy(f, response)

return extractImageArchive(tarFile, imageFolder, imageName)
}

func readManifest(imageFolder string, imageName string) (*DockerImageFiles, error) {
manifestFile, err := ioutil.ReadFile(path.Join(imageFolder, "manifest.json"))
if err != nil {
log.Println("Could not open docker image manifest!")
Expand All @@ -111,27 +111,29 @@ func ScanImage(imageName string, projectPath string) (vulnfetcher.VulnerabilityR
log.Println("⚠️ Will only analyze what is described by the first entry of the manifest.json of image <", imageName, "> : for more details, check ", path.Join(imageFolder, "manifest.json"))
}

manifest := manifests[0]
fmt.Println("Decompressing docker image layers...")

snapshotPath := path.Join(imageFolder, "snapshot")
return &manifests[0], nil
}

func extractLayers(imageFolder string, snapshotPath string, manifest *DockerImageFiles) error {
for _, layerFile := range manifest.Layers {
layerPath := path.Join(imageFolder, layerFile)
fmt.Println("Decompressing layer <", layerPath, ">")

a, err := unarr.NewArchive(layerPath)
if err != nil {
log.Println("Could not read layer <", layerPath, ">!")
return nil, err
return err
}
err = a.Extract(snapshotPath)
if err != nil {
log.Println("Could not extract layer <", layerPath, ">!")
return nil, err
return err
}
}
return nil
}

func readImageConfig(imageFolder string, manifest *DockerImageFiles) (*DockerConfig, error) {
configFile, err := ioutil.ReadFile(path.Join(imageFolder, manifest.Config))
if err != nil {
log.Println("Could not open docker image configuration!")
Expand All @@ -145,8 +147,50 @@ func ScanImage(imageName string, projectPath string) (vulnfetcher.VulnerabilityR
log.Println("Could not unmarshal docker image configuration!")
return nil, err
}
return &config, nil
}

// ScanImage extracts an image and analyzes its layers
func ScanImage(imageName string, projectPath string) (vulnfetcher.VulnerabilityReport, error) {
ctx := context.Background()
cli, err := docker.NewEnvClient()
if err != nil {
log.Println("Could not connect to docker")
return nil, err
}

err = pullImageIfNecessary(ctx, imageName, cli)
if err != nil {
return nil, err
}

imageFolder := path.Join(os.TempDir(), strconv.FormatInt(time.Now().Unix(), 10))
exportImageLocally(ctx, imageName, imageFolder, cli)
if err != nil {
return nil, err
}
defer Cleanup(imageFolder)

manifest, err := readManifest(imageFolder, imageName)
if err != nil {
return nil, err
}
fmt.Println("Decompressing docker image layers...")

snapshotPath := path.Join(imageFolder, "snapshot")

err = extractLayers(imageFolder, snapshotPath, manifest)
if err != nil {
return nil, err
}

config, err := readImageConfig(imageFolder, manifest)
if err != nil {
return nil, err
}
imageProjectPath := config.ContainerConfig.WorkingDir
if projectPath != "" {
fmt.Println("Using provided -path <", projectPath, "> instead of docker's working directory <", config.ContainerConfig.WorkingDir, ">")
imageProjectPath = projectPath
}

Expand Down
153 changes: 153 additions & 0 deletions docker/docker_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package docker

import (
"context"
"log"
"os"
"path"
"testing"

docker "github.com/docker/docker/client"
"github.com/google/go-cmp/cmp"
)

func TestCleanupInvalidFolder(t *testing.T) {
Cleanup("invalid-folder")
}

func TestScanImageHelloWorld(t *testing.T) {
vulns, err := ScanImage("gammaray-test-hello-world:1.0.0", "")
if err != nil {
Expand Down Expand Up @@ -36,3 +44,148 @@ func TestScanImageInsecureProject(t *testing.T) {
}
}
}

func TestPullImageIfNecessaryOfficialNodeAlpine(t *testing.T) {
ctx := context.Background()
cli, err := docker.NewEnvClient()
if err != nil {
t.Errorf("TestPullImageIfNecessaryOfficialNodeAlpine: Could not connect to docker: %s \n", err.Error())
}
err = pullImageIfNecessary(ctx, "node:latest", cli)
if err == nil {
return
}
if err.Error() != "" {
t.Errorf("TestPullImageIfNecessaryOfficialNodeAlpine: %s \n", err.Error())
}
}

func TestExportImageLocallyInvalidImage(t *testing.T) {
ctx := context.Background()
cli, err := docker.NewEnvClient()
if err != nil {
t.Errorf("TestExportImageLocallyInvalidImage: Could not connect to docker: %s \n", err.Error())
}
err = exportImageLocally(ctx, "🐭 Invalid test image name 😉", "", cli)
if err == nil {
t.Error("TestExportImageLocallyInvalidImage should make an error due to the image name being invalid")
return
}
if diff := cmp.Diff(err.Error(), "Error response from daemon: invalid reference format: repository name must be lowercase"); diff != "" {
t.Errorf("TestExportImageLocallyInvalidImage: expected a different error : (-got +want)\n%s", diff)
}
}

func TestExportImageArchiveInvalidTarFile(t *testing.T) {
err := extractImageArchive("/dev/null/invalid.tar", os.TempDir(), "image with invalid tar archive for tests")
if err == nil {
t.Error("TestExportImageArchiveInvalidTarFile should make an error due to the image destination folder being invalid")
return
}
if diff := cmp.Diff(err.Error(), "unarr: File not found"); diff != "" {
t.Errorf("TestExportImageArchiveInvalidTarFile: expected a different error : (-got +want)\n%s", diff)
}
}

func TestExportImageArchiveInvalidImageFolder(t *testing.T) {
err := extractImageArchive("./test_data/valid.tar", "/dev/null/invalid", "image with invalid destination folder")
if err == nil {
t.Error("TestExportImageArchiveInvalidImageFolder should make an error due to the image destination folder being invalid")
return
}
if diff := cmp.Diff(err.Error(), "open /dev/null/invalid/0297acce13fc12b22bf2caedde2829ab4847f5fe4ae6ba90073f90e7e26433a0/VERSION: not a directory"); diff != "" {
t.Errorf("TestExportImageArchiveInvalidImageFolder: expected a different error : (-got +want)\n%s", diff)
}
}

func TestExportImageArchiveValidImageFolder(t *testing.T) {
extractPath := path.Join(os.TempDir(), "gammaray-test-TestExportImageArchiveValidImageFolder")
defer Cleanup(extractPath)
err := extractImageArchive("./test_data/valid.tar", extractPath, "valid image")
if err != nil {
t.Error("TestExportImageArchiveValidImageFolder should not make an error:" + err.Error())
return
}
}

func TestReadManifestNoManifest(t *testing.T) {
_, err := readManifest("./test_data", "no manifest image")
if err == nil {
t.Error("TestReadManifestNoManifest should make an error due to the lack of manifest in folder")
return
}
if diff := cmp.Diff(err.Error(), "open test_data/manifest.json: no such file or directory"); diff != "" {
t.Errorf("TestReadManifestNoManifest: expected a different error : (-got +want)\n%s", diff)
}
}

func TestReadManifestInvalidManifest(t *testing.T) {
_, err := readManifest("./test_data/invalid-manifest", "invalid manifest image")
if err == nil {
t.Error("TestReadManifestInvalidManifest should make an error due to the invlid manifest content")
return
}
if diff := cmp.Diff(err.Error(), "json: cannot unmarshal object into Go value of type []docker.DockerImageFiles"); diff != "" {
t.Errorf("TestReadManifestInvalidManifest: expected a different error : (-got +want)\n%s", diff)
}
}

func TestExtractLayersNoLayer(t *testing.T) {
layers := []string{"not-existing"}
manifest := DockerImageFiles{
Layers: layers,
}
extractPath := path.Join(os.TempDir(), "gammaray-test-TestExtractLayersNoLayer")
err := extractLayers("./test_data", extractPath, &manifest)
defer Cleanup(extractPath)
if err == nil {
t.Error("TestExtractLayersNoLayer should make an error due to the lack of layer described by the manifest")
return
}
if diff := cmp.Diff(err.Error(), "unarr: File not found"); diff != "" {
t.Errorf("TestExtractLayersNoLayer: expected a different error : (-got +want)\n%s", diff)
}
}

func TestExtractLayersInvalidLayer(t *testing.T) {
layers := []string{"invalid-layer.tar"}
manifest := DockerImageFiles{
Layers: layers,
}
extractPath := path.Join(os.TempDir(), "gammaray-test-TestExtractLayersInvalidLayer")
err := extractLayers("./test_data/invalid-layers", extractPath, &manifest)
defer Cleanup(extractPath)
if err == nil {
t.Error("TestExtractLayersInvalidLayer should make an error due to the layer archive being dummy")
return
}
if diff := cmp.Diff(err.Error(), "unarr: No valid RAR, ZIP, 7Z or TAR archive"); diff != "" {
t.Errorf("TestExtractLayersInvalidLayer: expected a different error : (-got +want)\n%s", diff)
}
}

func TestReadManifestNoImageConfig(t *testing.T) {
manifest := DockerImageFiles{Config: "not-existing-config.json"}

_, err := readImageConfig("./test_data", &manifest)
if err == nil {
t.Error("TestReadManifestNoImageConfig should make an error due to the lack of manifest in folder")
return
}
if diff := cmp.Diff(err.Error(), "open test_data/not-existing-config.json: no such file or directory"); diff != "" {
t.Errorf("TestReadManifestNoImageConfig: expected a different error : (-got +want)\n%s", diff)
}
}

func TestReadManifestInvalidConfig(t *testing.T) {
manifest := DockerImageFiles{Config: "config.json"}

_, err := readImageConfig("./test_data/invalid-config", &manifest)
if err == nil {
t.Error("TestReadManifestInvalidConfig should make an error due to the invalid manifest content")
return
}
if diff := cmp.Diff(err.Error(), "json: cannot unmarshal array into Go value of type docker.DockerConfig"); diff != "" {
t.Errorf("TestReadManifestInvalidConfig: expected a different error : (-got +want)\n%s", diff)
}
}
3 changes: 3 additions & 0 deletions docker/test_data/invalid-config/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[{
"bad": "manifest"
}]
Empty file.
3 changes: 3 additions & 0 deletions docker/test_data/invalid-manifest/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"bad": "manifest"
}
Binary file added docker/test_data/valid.tar
Binary file not shown.

0 comments on commit a9f3513

Please sign in to comment.