Skip to content

Commit

Permalink
support json format (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomoyamachi authored Jun 14, 2019
1 parent 68d44e3 commit 28f8dbe
Show file tree
Hide file tree
Showing 7 changed files with 309 additions and 49 deletions.
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
.circleci
.git
imgs
.github
.goreleaser.yaml
.gitignore
Dockerfile
LICENSE
README.md
107 changes: 106 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ $ dockle [YOUR_IMAGE_NAME]
- [Examples](#examples)
- [Scan an image](#scan-an-image)
- [Scan an image file](#scan-an-image-file)
- [Save the results as JSON](#save-the-results-as-json)
- [Specify exit code](#specify-exit-code)
- [Ignore the specified checkpoints](#ignore-the-specified-checkpoints)
- [Clear image caches](#clear-image-caches)
Expand Down Expand Up @@ -325,6 +326,110 @@ $ docker save alpine:latest -o alpine.tar
$ dockle --input alpine.tar
```

## Save the results as JSON

```bash
$ dockle -f json goodwithtech/test-image:v1
$ dockle -f json -o results.json goodwithtech/test-image:v1
```

<details>
<summary>Result</summary>

```json
{
"summary": {
"fatal": 6,
"warn": 2,
"info": 2,
"pass": 7
},
"details": [
{
"code": "CIS-DI-0001",
"title": "Create a user for the container",
"level": "WARN",
"alerts": [
"Last user should not be root"
]
},
{
"code": "CIS-DI-0005",
"title": "Enable Content trust for Docker",
"level": "INFO",
"alerts": [
"export DOCKER_CONTENT_TRUST=1 before docker pull/build"
]
},
{
"code": "CIS-DI-0006",
"title": "Add HEALTHCHECK instruction to the container image",
"level": "WARN",
"alerts": [
"not found HEALTHCHECK statement"
]
},
{
"code": "CIS-DI-0008",
"title": "Remove setuid and setgid permissions in the images",
"level": "INFO",
"alerts": [
"Found setuid file: usr/lib/openssh/ssh-keysign urwxr-xr-x"
]
},
{
"code": "CIS-DI-0009",
"title": "Use COPY instead of ADD in Dockerfile",
"level": "FATAL",
"alerts": [
"Use COPY : /bin/sh -c #(nop) ADD file:81c0a803075715d1a6b4f75a29f8a01b21cc170cfc1bff6702317d1be2fe71a3 in /app/credentials.json "
]
},
{
"code": "CIS-DI-0010",
"title": "Do not store secrets in ENVIRONMENT variables",
"level": "FATAL",
"alerts": [
"Suspicious ENV key found : MYSQL_PASSWD"
]
},
{
"code": "CIS-DI-0010",
"title": "Do not store secret files",
"level": "FATAL",
"alerts": [
"Suspicious filename found : app/credentials.json "
]
},
{
"code": "DKL-DI-0002",
"title": "Avoid sensitive directory mounting",
"level": "FATAL",
"alerts": [
"Avoid mounting sensitive dirs : /usr"
]
},
{
"code": "DKL-DI-0005",
"title": "Clear apt-get caches",
"level": "FATAL",
"alerts": [
"Use 'rm -rf /var/lib/apt/lists' after 'apt-get install' : /bin/sh -c apt-get update \u0026\u0026 apt-get install -y git"
]
},
{
"code": "DKL-LI-0001",
"title": "Avoid empty password",
"level": "FATAL",
"alerts": [
"No password user found! username : nopasswd"
]
}
]
}
```
<details>

## Specify exit code
By default, `Dockle` exits with code 0 even if there are some problems.
Use the --exit-code option if you may want to exit with a non-zero exit code.
Expand Down Expand Up @@ -647,7 +752,7 @@ AGPLv3
[@tomoyamachi](https://github.com/tomoyamachi) (Tomoya Amachi)

# Roadmap
- [ ] JSON output
- [x] JSON output
- [ ] Check php.ini file
- [ ] Check nginx.conf file
- [ ] create CI badges
Expand Down
9 changes: 9 additions & 0 deletions cmd/dockle/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ OPTIONS:
Value: "",
Usage: "input file path instead of image name",
},
cli.StringFlag{
Name: "format, f",
Value: "",
Usage: "format (json)",
},
cli.StringFlag{
Name: "output, o",
Usage: "output file name",
},
cli.IntFlag{
Name: "exit-code",
Usage: "Exit code when alert were found",
Expand Down
103 changes: 103 additions & 0 deletions pkg/report/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package report

import (
"encoding/json"
"fmt"
"io"

"golang.org/x/xerrors"

"github.com/goodwithtech/dockle/pkg/types"
)

type JsonWriter struct {
Output io.Writer
IgnoreMap map[string]struct{}
}

type JsonOutputFormat struct {
Summary JsonSummary `json:"summary"`
Details []*JsonDetail `json:"details"`
}
type JsonSummary struct {
Fatal int `json:"fatal"`
Warn int `json:"warn"`
Info int `json:"info"`
Pass int `json:"pass"`
}
type JsonDetail struct {
Code string `json:"code"`
Title string `json:"title"`
Level string `json:"level"`
Alerts []string `json:"alerts"`
}

func (jw JsonWriter) Write(assessments []*types.Assessment) (bool, error) {
var abendAssessments []*types.Assessment
jsonSummary := JsonSummary{}
jsonDetails := []*JsonDetail{}
targetType := types.MinTypeNumber
for targetType <= types.MaxTypeNumber {
filtered := filteredAssessments(jw.IgnoreMap, targetType, assessments)
level, detail := jsonDetail(targetType, filtered)
if detail != nil {
jsonDetails = append(jsonDetails, detail)
}

// increment summary
switch level {
case types.FatalLevel:
jsonSummary.Fatal++
case types.WarnLevel:
jsonSummary.Warn++
case types.InfoLevel:
jsonSummary.Info++
default:
jsonSummary.Pass++
}

for _, assessment := range filtered {
abendAssessments = filterAbendAssessments(jw.IgnoreMap, abendAssessments, assessment)
}
targetType++
}
result := JsonOutputFormat{
Summary: jsonSummary,
Details: jsonDetails,
}
output, err := json.MarshalIndent(result, "", " ")
if err != nil {
return false, xerrors.Errorf("failed to marshal json: %w", err)
}

if _, err = fmt.Fprint(jw.Output, string(output)); err != nil {
return false, xerrors.Errorf("failed to write json: %w", err)
}
return len(abendAssessments) > 0, nil
}
func jsonDetail(assessmentType int, assessments []*types.Assessment) (level int, jsonInfo *JsonDetail) {
if len(assessments) == 0 {
return types.PassLevel, nil
}
if assessments[0].Level == types.SkipLevel {
return types.SkipLevel, nil
}

detail := types.AlertDetails[assessmentType]
level = detail.DefaultLevel
if assessments[0].Level == types.IgnoreLevel {
level = types.IgnoreLevel
}

alerts := []string{}
for _, assessment := range assessments {
alerts = append(alerts, assessment.Desc)
}
jsonInfo = &JsonDetail{
Code: detail.Code,
Title: detail.Title,
Level: AlertLabels[level],
Alerts: alerts,
}
return level, jsonInfo
}
35 changes: 24 additions & 11 deletions pkg/writer/writer.go → pkg/report/list.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package writer
package report

import (
"fmt"
"io"

"github.com/goodwithtech/dockle/pkg/color"
"github.com/goodwithtech/dockle/pkg/types"
Expand All @@ -15,15 +16,6 @@ const (
NEWLINE = "\n"
)

var AlertLabels = []string{
"INFO",
"WARN",
"FATAL",
"PASS",
"SKIP",
"IGNORE",
}

var AlertLevelColors = []color.Color{
color.Magenta,
color.Yellow,
Expand All @@ -33,7 +25,28 @@ var AlertLevelColors = []color.Color{
color.Blue,
}

func ShowTargetResult(assessmentType int, assessments []*types.Assessment) {
type ListWriter struct {
Output io.Writer
IgnoreMap map[string]struct{}
}

func (lw ListWriter) Write(assessments []*types.Assessment) (bool, error) {
var abendAssessments []*types.Assessment

targetType := types.MinTypeNumber
for targetType <= types.MaxTypeNumber {
filtered := filteredAssessments(lw.IgnoreMap, targetType, assessments)
showTargetResult(targetType, filtered)

for _, assessment := range filtered {
abendAssessments = filterAbendAssessments(lw.IgnoreMap, abendAssessments, assessment)
}
targetType++
}
return len(abendAssessments) > 0, nil
}

func showTargetResult(assessmentType int, assessments []*types.Assessment) {
if len(assessments) == 0 {
showTitleLine(assessmentType, types.PassLevel)
return
Expand Down
43 changes: 43 additions & 0 deletions pkg/report/writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package report

import (
"github.com/goodwithtech/dockle/pkg/types"
)

var AlertLabels = []string{
"INFO",
"WARN",
"FATAL",
"PASS",
"SKIP",
"IGNORE",
}

type Writer interface {
Write(assessments []*types.Assessment) (bool, error)
}

func filteredAssessments(ignoreCheckpointMap map[string]struct{}, target int, assessments []*types.Assessment) (filtered []*types.Assessment) {
detail := types.AlertDetails[target]
for _, assessment := range assessments {
if assessment.Type == target {
if _, ok := ignoreCheckpointMap[detail.Code]; ok {
assessment.Level = types.IgnoreLevel
}
filtered = append(filtered, assessment)
}
}
return filtered
}

func filterAbendAssessments(ignoreCheckpointMap map[string]struct{}, abendAssessments []*types.Assessment, assessment *types.Assessment) []*types.Assessment {
if assessment.Level == types.SkipLevel {
return abendAssessments
}

detail := types.AlertDetails[assessment.Type]
if _, ok := ignoreCheckpointMap[detail.Code]; ok {
return abendAssessments
}
return append(abendAssessments, assessment)
}
Loading

0 comments on commit 28f8dbe

Please sign in to comment.