diff --git a/e2e/fixtures/assets/scan_help b/e2e/fixtures/assets/scan_help index 04c5058983d..9e4598fa939 100644 --- a/e2e/fixtures/assets/scan_help +++ b/e2e/fixtures/assets/scan_help @@ -37,7 +37,7 @@ Flags: -d, --payload-path string path to store internal representation JSON file --preview-lines int number of lines to be display in CLI results (min: 1, max: 30) (default 3) -q, --queries-path string path to directory with queries (default "./assets/queries") - --report-formats strings formats in which the results will be exported (all, json, sarif, html, glsast) (default [json]) + --report-formats strings formats in which the results will be exported (all, json, sarif, html, glsast, pdf) (default [json]) --timeout int number of seconds the query has to execute before being canceled (default 60) -t, --type strings case insensitive list of platform types to scan (Ansible, CloudFormation, Dockerfile, Kubernetes, OpenAPI, Terraform) diff --git a/go.mod b/go.mod index b5ba59032e5..5919f73306f 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/gookit/color v1.4.2 github.com/hashicorp/hcl v1.0.0 github.com/hashicorp/hcl/v2 v2.10.0 + github.com/johnfercher/maroto v0.31.0 github.com/mailru/easyjson v0.7.7 github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/moby/buildkit v0.8.3 diff --git a/go.sum b/go.sum index c640cb1a191..168ac52b8d4 100644 --- a/go.sum +++ b/go.sum @@ -248,6 +248,8 @@ github.com/bombsimon/wsl/v2 v2.0.0/go.mod h1:mf25kr/SqFEPhhcxW1+7pxzGlW+hIl/hYTK github.com/bombsimon/wsl/v2 v2.2.0/go.mod h1:Azh8c3XGEJl9LyX0/sFC+CKMc7Ssgua0g+6abzXN4Pg= github.com/bombsimon/wsl/v3 v3.0.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= github.com/bombsimon/wsl/v3 v3.1.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= +github.com/boombuler/barcode v1.0.0 h1:s1TvRnXwL2xJRaccrdcBQMZxq6X7DvsMogtmJeHDdrc= +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bombsimon/wsl/v3 v3.3.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/bshuster-repo/logrus-logstash-hook v0.4.1 h1:pgAtgj+A31JBVtEHu2uHuEx0n+2ukqUJnS2vVe5pQNA= @@ -580,6 +582,7 @@ github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/gojp/goreportcard v0.0.0-20191001233754-41818f5fd295/go.mod h1:/DA2Xpp+OaR3EHafQSnT9SKOfbG2NPQR/qp6Qr8AgIw= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -836,6 +839,8 @@ github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xl github.com/jmoiron/sqlx v1.3.1 h1:aLN7YINNZ7cYOPK3QC83dbM6KT0NMqVMw961TqrejlE= github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= +github.com/johnfercher/maroto v0.31.0 h1:Ba3woJTMVlX257Bj/t0fyOuEddr77evNMpaA2YZUpAM= +github.com/johnfercher/maroto v0.31.0/go.mod h1:z/5eo/hH1g+01K4Mm0IVVbixHibtaNbZ9vHf+2H6fpM= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= @@ -859,6 +864,9 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jung-kurt/gofpdf v1.4.2 h1:3u2ojTwxPPu3ysIOc5iTwcECpvkFCAe2RJ/tQrvfLi0= +github.com/jung-kurt/gofpdf v1.4.2/go.mod h1:rZsO0wEsunjT/L9stF3fJjYbAHgqNYuQB4B8FWvBck0= github.com/julz/importas v0.0.0-20210419104244-841f0c0fe66d/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= @@ -1245,6 +1253,8 @@ github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvf github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58 h1:nlG4Wa5+minh3S9LVFtNoY+GVRiudA2e3EVfcCi3RCA= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ryancurrah/gomodguard v1.0.4/go.mod h1:9T/Cfuxs5StfsocWr4WzDL36HqnX0fVb9d5fSEaLhoE= github.com/ryancurrah/gomodguard v1.1.0/go.mod h1:4O8tr7hBODaGE6VIhfJDHcwzh5GUccKSJBU0UMXJFVM= github.com/ryancurrah/gomodguard v1.2.0/go.mod h1:rNqbC4TOIdUDcVMSIpNNAzTbzXAZa6W5lnUepvuMMgQ= @@ -1542,6 +1552,7 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190507092727-e4e5bf290fec/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/internal/console/helpers/helpers.go b/internal/console/helpers/helpers.go index 6f2e02c7afb..e4c4807ecd7 100644 --- a/internal/console/helpers/helpers.go +++ b/internal/console/helpers/helpers.go @@ -27,6 +27,7 @@ var reportGenerators = map[string]func(path, filename string, body interface{}) "sarif": report.PrintSarifReport, "html": report.PrintHTMLReport, "glsast": report.PrintGitlabSASTReport, + "pdf": report.PrintPdfReport, } // ProgressBar represents a Progress diff --git a/internal/console/scan.go b/internal/console/scan.go index 6033aa83bec..ce68de18bda 100644 --- a/internal/console/scan.go +++ b/internal/console/scan.go @@ -315,7 +315,7 @@ func initScanFlags(scanCmd *cobra.Command) { "", "directory path to store reports") scanCmd.Flags().StringSliceVar(&reportFormats, reportFormatsFlag, []string{"json"}, - "formats in which the results will be exported (all, json, sarif, html, glsast)", + "formats in which the results will be exported (all, json, sarif, html, glsast, pdf)", ) scanCmd.Flags().IntVar(&previewLines, previewLinesFlag, diff --git a/pkg/report/assets/vuln b/pkg/report/assets/vuln new file mode 100644 index 00000000000..7a201588606 --- /dev/null +++ b/pkg/report/assets/vuln @@ -0,0 +1 @@ +iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAYAAAA4TnrqAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAuIwAALiMBeKU/dgAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAMBSURBVHic7Zw7axRRFIC/TaLBjSlFEStjZRuQIAELY2MhCFrYLYqCf0DRP6BWPhpJoyJpFEwhIogRRNAUMQoiFmIg4BtRIT6Cyu5a3A1uZDa758yduXd3zwenmeXsnPsxh5m5c2fAMAzDMLqd3tAF/Mda4ABwAhgA5oDfQSuKkGFgHFgAqnWxCFwHxoBCsOoiYBNwHHjFckGN4jVwGhgKUWwI1gD7gbtAhdYkJcVj4AgwmG/5+dCozdJGx7SptM3SRtu1qa82SxtRt2lWbdYxbZp3m7Vlm44C9wnbZmmiUqt/VDrwHmkC7pDeQfuefQq4+sekiRpZXYvJEmCyBJgsAX2hC6jjE/AtYfsgsC7nWhKJSdYx4ErC9hJwOddKGmBtKMBkCTBZAkyWAJMlwGQJMFkCTJYAkyXAZAkwWQI0sqreqwiDeBwaWb8UOTGyKE3QyPqpyImRH9IEzRRN0pyTD3YD6xO2D2e0v+8Z/e8ydhH+cZaP2CkduKYN5xU5MTIvTdA8++vHHcIxzbJK+YNbZShaVag9Gz5X5MXEMxTLL7UXpTPKvFhQ1a9tpWngsDK3ES+AdwnbNwJbPe9r2vP/rcgG/C8MKTXYV8nzfsokX6I0RduGH4AnytzQzAIfNYlpbqRvpMgNyaQ2MY2sq7hDup0oAxPa5DSy3gJTKfJDcAd4o01OO591PmV+3pwLXcAs4e/zWomnRLBacR/hRbQSe7MSIOUB4WWsFA+J4KhaYhvuTBNaSlKUyW5OTM0FwotJirNZDlpLEXhJeDn1MYebiomSEdwUTmhJ1VodI9kONz1HCS+qinvRqS0YJ6yoi9kP0R+9wDXCiJokvhflm9IP3CZfUbeA1XkMLgv6gEvkI2oCWJXPsLKjBzhDdq/cVYBTRHSF7oM9wFf8ilrA3Zt2JEPAPfyImgI251t+/hSAg7h5fI2k97iHGB3Vds0YAE4CX2hN0mf+faOmaykCh3CfF0iSNFP7vRiqwCViO5S34E4E24FHwE3c2/2GYRiGYUTBXy34jZrggyC2AAAAAElFTkSuQmCC diff --git a/pkg/report/commons.go b/pkg/report/commons.go index 225cf911439..45d0732f701 100644 --- a/pkg/report/commons.go +++ b/pkg/report/commons.go @@ -11,20 +11,22 @@ import ( "github.com/rs/zerolog/log" ) -var templateFuncs = template.FuncMap{ - "lower": strings.ToLower, - "sprintf": fmt.Sprintf, - "severity": getSeverities, - "getCurrentTime": getCurrentTime, - "trimSpaces": trimSpaces, -} +var ( + stringsSeverity = map[string]model.Severity{ + "high": model.AllSeverities[0], + "medium": model.AllSeverities[1], + "low": model.AllSeverities[2], + "info": model.AllSeverities[3], + } -var stringsSeverity = map[string]model.Severity{ - "high": model.AllSeverities[0], - "medium": model.AllSeverities[1], - "low": model.AllSeverities[2], - "info": model.AllSeverities[3], -} + templateFuncs = template.FuncMap{ + "lower": strings.ToLower, + "sprintf": fmt.Sprintf, + "severity": getSeverities, + "getCurrentTime": getCurrentTime, + "trimSpaces": trimSpaces, + } +) func trimSpaces(value string) string { return strings.TrimPrefix(value, " ") @@ -48,3 +50,15 @@ func closeFile(path, filename string, file *os.File) { log.Info().Str("fileName", filename).Msgf("Results saved to file %s", path) fmt.Printf("Results saved to file %s\n", path) } + +func getPlatforms(queries model.VulnerableQuerySlice) string { + platforms := make([]string, 0) + alreadyAdded := make(map[string]string) + for idx := range queries { + if _, ok := alreadyAdded[queries[idx].Platform]; !ok { + alreadyAdded[queries[idx].Platform] = "" + platforms = append(platforms, queries[idx].Platform) + } + } + return strings.Join(platforms, ", ") +} diff --git a/pkg/report/html.go b/pkg/report/html.go index 076375f5301..fc58cbfe3e4 100644 --- a/pkg/report/html.go +++ b/pkg/report/html.go @@ -8,7 +8,6 @@ import ( "path/filepath" "strings" - "github.com/Checkmarx/kics/pkg/model" "github.com/tdewolff/minify/v2" minifyCSS "github.com/tdewolff/minify/v2/css" minifyHtml "github.com/tdewolff/minify/v2/html" @@ -71,18 +70,6 @@ func getPaths(paths []string) string { return strings.Join(paths, ", ") } -func getPlatforms(queries model.VulnerableQuerySlice) string { - platforms := make([]string, 0) - alreadyAdded := make(map[string]string) - for idx := range queries { - if _, ok := alreadyAdded[queries[idx].Platform]; !ok { - alreadyAdded[queries[idx].Platform] = "" - platforms = append(platforms, queries[idx].Platform) - } - } - return strings.Join(platforms, ", ") -} - // PrintHTMLReport creates a report file on HTML format func PrintHTMLReport(path, filename string, body interface{}) error { if !strings.HasSuffix(filename, ".html") { diff --git a/pkg/report/model/sarif.go b/pkg/report/model/sarif.go index 07b80ca2ae2..aeb72ee55a5 100644 --- a/pkg/report/model/sarif.go +++ b/pkg/report/model/sarif.go @@ -120,7 +120,8 @@ type sarifTaxonomy struct { TaxonomyDefinitions []sarifTaxanomyDefinition `json:"taxa"` } -type sarifRun struct { +// SarifRun - sarifRun is a component of the SARIF report +type SarifRun struct { Tool sarifTool `json:"tool"` Results []sarifResult `json:"results"` Taxonomies []sarifTaxonomy `json:"taxonomies"` @@ -135,7 +136,7 @@ type sarifReport struct { basePath string `json:"-"` Schema string `json:"$schema"` SarifVersion string `json:"version"` - Runs []sarifRun `json:"runs"` + Runs []SarifRun `json:"runs"` } func initSarifTool() sarifTool { @@ -174,8 +175,8 @@ func initSarifTaxonomies() []sarifTaxonomy { } } -func initSarifRun() []sarifRun { - return []sarifRun{ +func initSarifRun() []SarifRun { + return []SarifRun{ { Tool: initSarifTool(), Results: make([]sarifResult, 0), diff --git a/pkg/report/model/sarif_test.go b/pkg/report/model/sarif_test.go index f70bd40fee4..628914d8b8b 100644 --- a/pkg/report/model/sarif_test.go +++ b/pkg/report/model/sarif_test.go @@ -57,7 +57,7 @@ var sarifTests = []sarifTest{ }, }, want: sarifReport{ - Runs: []sarifRun{ + Runs: []SarifRun{ { Tool: sarifTool{ Driver: sarifDriver{ @@ -142,7 +142,7 @@ var sarifTests = []sarifTest{ }, }, want: sarifReport{ - Runs: []sarifRun{ + Runs: []SarifRun{ { Tool: sarifTool{ Driver: sarifDriver{ diff --git a/pkg/report/pdf.go b/pkg/report/pdf.go new file mode 100644 index 00000000000..4dfcbe4787a --- /dev/null +++ b/pkg/report/pdf.go @@ -0,0 +1,399 @@ +package report + +import ( + _ "embed" // used for embedding report static files + "fmt" + "os" + "path/filepath" + "time" + + "github.com/Checkmarx/kics/pkg/model" + "github.com/johnfercher/maroto/pkg/color" + "github.com/johnfercher/maroto/pkg/consts" + "github.com/johnfercher/maroto/pkg/pdf" + "github.com/johnfercher/maroto/pkg/props" + "github.com/rs/zerolog/log" +) + +const ( + defaultTextSize = 8 + pgMarginLeft = 10 + pgMarginTop = 15 + pgMarginRight = 10 + rowXSmall = 3 + rowSmall = 4 + rowMedium = 5 + rowLarge = 8 + rowXLarge = 15 + colOne = 1 + colTwo = 2 + colThree = 3 + colFour = 4 + colFive = 5 + colSix = 6 + colNine = 9 + colTen = 10 + colFullPage = 12 +) + +var ( + grayColor = getGrayColor() + //go:embed assets/vuln + vulnImageBase64 string +) + +func createQueryEntryMetadataField(m pdf.Maroto, label, value string, textSize int) { + m.Col(colTwo, func() { + m.Text(label, props.Text{ + Size: float64(textSize), + Align: consts.Left, + Extrapolate: false, + }) + }) + m.Col(colTwo, func() { + m.Text(value, props.Text{ + Size: float64(textSize), + Align: consts.Left, + Extrapolate: false, + }) + }) +} + +func createQueriesTable(m pdf.Maroto, queries []model.VulnerableQuery, basePath string) error { + for i := range queries { + m.SetBackgroundColor(color.NewWhite()) + queryName := queries[i].QueryName + resultsCount := fmt.Sprint(len(queries[i].Files)) + severity := string(queries[i].Severity) + platform := queries[i].Platform + category := queries[i].Category + + var err error + m.Row(rowLarge, func() { + m.Col(colOne, func() { + err = m.Base64Image(vulnImageBase64, consts.Png, props.Rect{ + Center: false, + Percent: 50, + Left: 2, + }) + }) + m.Col(colNine, func() { + m.Text(queryName, props.Text{ + Size: 11, + Style: consts.Bold, + Align: consts.Left, + Extrapolate: false, + }) + }) + m.Col(colOne, func() { + m.Text("Results", props.Text{ + Size: 8, + Style: consts.Bold, + Align: consts.Right, + Extrapolate: false, + }) + }) + m.Col(colOne, func() { + m.Text(resultsCount, props.Text{ + Size: 8, + Style: consts.Bold, + Align: consts.Right, + Extrapolate: false, + }) + }) + }) + if err != nil { + return err + } + m.Row(colFour, func() { + createQueryEntryMetadataField(m, "Severity", severity, 10) + }) + m.Row(colThree, func() { + createQueryEntryMetadataField(m, "Platform", platform, defaultTextSize) + }) + m.Row(colFive, func() { + createQueryEntryMetadataField(m, "Category", category, defaultTextSize) + }) + createResultsTable(m, &queries[i], basePath) + } + return nil +} + +func createResultsTable(m pdf.Maroto, query *model.VulnerableQuery, basePath string) { + for idx := range query.Files { + if idx%2 == 0 { + m.SetBackgroundColor(grayColor) + } else { + m.SetBackgroundColor(color.NewWhite()) + } + var filePath string + relativePath, err := filepath.Rel(basePath, query.Files[idx].FileName) + if err != nil { + log.Error().Msgf("Cannot make %s relative to %s", query.Files[idx].FileName, basePath) + filePath = query.Files[idx].FileName + } else { + filePath = relativePath + } + + fileLine := fmt.Sprintf("%s:%s", filePath, fmt.Sprint(query.Files[idx].Line)) + m.Row(colFive, func() { + m.Col(colFullPage, func() { + m.Text(fileLine, props.Text{ + Size: defaultTextSize, + Align: consts.Left, + Extrapolate: false, + }) + }) + }) + } + m.Line(1.0) +} + +func createHeaderArea(m pdf.Maroto) { + m.SetBackgroundColor(getPurpleColor()) + m.Row(rowXLarge, func() { + m.Col(colSix, func() { + m.Text(" KICS REPORT", props.Text{ + Size: 25, + Style: consts.Bold, + Align: consts.Left, + Extrapolate: false, + Color: color.NewWhite(), + }) + }) + m.ColSpace(colSix) + }) + m.SetBackgroundColor(color.NewWhite()) + m.Row(rowXSmall, func() { + m.ColSpace(colFullPage) + }) +} + +func createFooterArea(m pdf.Maroto) { + m.Row(rowMedium, func() { + m.Col(colOne, func() { + m.Text("https://kics.io") + }) + }) +} + +// PrintPdfReport creates a report file on the PDF format +func PrintPdfReport(path, filename string, body interface{}) error { + startTime := time.Now() + log.Info().Msg("Started generating pdf report") + + summary := body.(model.Summary) + basePath, err := os.Getwd() + if err != nil { + return err + } + + m := pdf.NewMaroto(consts.Portrait, consts.A4) + m.SetPageMargins(pgMarginLeft, pgMarginTop, pgMarginRight) + + m.SetFirstPageNb(1) + m.SetAliasNbPages("{total}") + + m.RegisterHeader(func() { + createHeaderArea(m) + }) + m.RegisterFooter(func() { + createFooterArea(m) + }) + + m.SetBackgroundColor(color.NewWhite()) + + createFirstPageHeader(m, &summary) + + m.Line(1.0) + + err = createQueriesTable(m, summary.Queries, basePath) + if err != nil { + return err + } + + err = m.OutputFileAndClose(filepath.Join(path, fmt.Sprintf("%s.pdf", filename))) + if err != nil { + return err + } + + log.Info().Msgf("Generate report duration: %v", time.Since(startTime)) + + return err +} + +func createDateField(m pdf.Maroto, label string, summary *model.Summary) { + m.Row(colFour, func() { + m.Col(colTwo, func() { + m.Text(label, props.Text{ + Size: defaultTextSize, + Align: consts.Left, + Extrapolate: false, + }) + }) + m.Col(colTwo, func() { + m.Text(summary.Start.Format("15:04:05, Jan 02 2006"), props.Text{ + Size: defaultTextSize, + Align: consts.Left, + Extrapolate: false, + }) + }) + }) +} + +func createDateArea(m pdf.Maroto, summary *model.Summary) { + createDateField(m, "START TIME", summary) + createDateField(m, "END TIME", summary) +} + +func createPlatformsArea(m pdf.Maroto, summary *model.Summary) { + m.Row(rowSmall, func() { + m.Col(colTwo, func() { + m.Text("PLATFORMS", props.Text{ + Size: defaultTextSize, + Align: consts.Left, + Extrapolate: false, + }) + }) + m.Col(colTen, func() { + m.Text(getPlatforms(summary.Queries), props.Text{ + Size: defaultTextSize, + Align: consts.Left, + Extrapolate: false, + }) + }) + }) +} + +func createSummaryResultsField(m pdf.Maroto, label, value string, mColor color.Color) { + m.Col(colOne, func() { + m.Text(label, props.Text{ + Size: defaultTextSize, + Align: consts.Left, + Style: consts.Bold, + Extrapolate: false, + Color: mColor, + }) + }) + m.Col(colOne, func() { + m.Text(value, props.Text{ + Size: defaultTextSize, + Align: consts.Left, + Style: consts.Bold, + Extrapolate: false, + Color: mColor, + }) + }) +} + +func createSummaryArea(m pdf.Maroto, summary *model.Summary) { + highSeverityCount := fmt.Sprint(summary.SeverityCounters["HIGH"]) + mediumSeverityCount := fmt.Sprint(summary.SeverityCounters["MEDIUM"]) + lowSeverityCount := fmt.Sprint(summary.SeverityCounters["LOW"]) + infoSeverityCount := fmt.Sprint(summary.SeverityCounters["INFO"]) + totalCount := fmt.Sprint(summary.TotalCounter) + + m.Row(rowMedium, func() { + createSummaryResultsField(m, "HIGH", highSeverityCount, getRedColor()) + createSummaryResultsField(m, "MEDIUM", mediumSeverityCount, getOrangeColor()) + createSummaryResultsField(m, "LOW", lowSeverityCount, getYellowColor()) + createSummaryResultsField(m, "INFO", infoSeverityCount, getBlueColor()) + + m.ColSpace(colTwo) + + m.Col(colOne, func() { + m.Text("TOTAL", props.Text{ + Size: defaultTextSize, + Align: consts.Right, + Style: consts.Bold, + Extrapolate: false, + }) + }) + m.Col(colOne, func() { + m.Text(totalCount, props.Text{ + Size: defaultTextSize, + Align: consts.Right, + Style: consts.Bold, + Extrapolate: false, + }) + }) + }) +} + +func createFirstPageHeader(m pdf.Maroto, summary *model.Summary) { + createSummaryArea(m, summary) + createPlatformsArea(m, summary) + createDateArea(m, summary) + m.Row(rowSmall, func() { + m.Col(colTwo, func() { + m.Text("SCANNED PATHS:", props.Text{ + Size: defaultTextSize, + Align: consts.Left, + Extrapolate: false, + }) + }) + }) + for i := range summary.ScannedPaths { + scannedPaths := summary.ScannedPaths[i] + m.Row(rowSmall, func() { + m.Col(colFullPage, func() { + m.Text(fmt.Sprintf("- %s", scannedPaths), props.Text{ + Size: defaultTextSize, + Align: consts.Left, + Extrapolate: true, + }) + }) + }) + } + m.Row(rowXSmall, func() { + m.ColSpace(colFullPage) + }) +} + +func getGrayColor() color.Color { + return color.Color{ + Red: 200, + Green: 200, + Blue: 200, + } +} + +func getRedColor() color.Color { + return color.Color{ + Red: 200, + Green: 0, + Blue: 0, + } +} + +func getYellowColor() color.Color { + return color.Color{ + Red: 206, + Green: 182, + Blue: 26, + } +} + +func getOrangeColor() color.Color { + return color.Color{ + Red: 255, + Green: 165, + Blue: 0, + } +} + +func getBlueColor() color.Color { + return color.Color{ + Red: 0, + Green: 0, + Blue: 200, + } +} + +func getPurpleColor() color.Color { + return color.Color{ + Red: 80, + Green: 62, + Blue: 158, + } +} diff --git a/pkg/report/pdf_test.go b/pkg/report/pdf_test.go new file mode 100644 index 00000000000..e22f7a893cd --- /dev/null +++ b/pkg/report/pdf_test.go @@ -0,0 +1,40 @@ +package report + +import ( + "fmt" + "os" + "testing" + + "github.com/Checkmarx/kics/test" +) + +var pdfTests = []reportTestCase{ + { + caseTest: jsonCaseTest{ + summary: test.SummaryMock, + path: "./testdir", + filename: "testpdf", + }, + }, + { + caseTest: jsonCaseTest{ + summary: test.SummaryMock, + path: "./testdir/newdir", + filename: "testpdf2", + }, + }, +} + +// TestPrintPdfReport tests the functions [PrintPdfReport()] and all the methods called by them +func TestPrintPdfReport(t *testing.T) { + for i, test := range pdfTests { + t.Run(fmt.Sprintf("PDF report test case %d", i), func(t *testing.T) { + if err := os.MkdirAll(test.caseTest.path, os.ModePerm); err != nil { + t.Fatal(err) + } + err := PrintPdfReport(test.caseTest.path, test.caseTest.filename, test.caseTest.summary) + checkFileExists(t, err, &test, "pdf") + os.RemoveAll(test.caseTest.path) + }) + } +} diff --git a/pkg/report/sarif_test.go b/pkg/report/sarif_test.go index 846b5a8a281..b21c5ba6774 100644 --- a/pkg/report/sarif_test.go +++ b/pkg/report/sarif_test.go @@ -1,20 +1,31 @@ package report import ( + "encoding/json" "fmt" "os" "path/filepath" "testing" "github.com/Checkmarx/kics/pkg/model" + reportModel "github.com/Checkmarx/kics/pkg/report/model" "github.com/Checkmarx/kics/test" "github.com/stretchr/testify/require" ) -var sarifTests = []struct { +type reportTestCase struct { caseTest jsonCaseTest expectedResult model.Summary -}{ +} + +type sarifReport struct { + basePath string `json:"-"` + Schema string `json:"$schema"` + SarifVersion string `json:"version"` + Runs []reportModel.SarifRun `json:"runs"` +} + +var sarifTests = []reportTestCase{ { caseTest: jsonCaseTest{ summary: test.SummaryMock, @@ -33,9 +44,22 @@ func TestPrintSarifReport(t *testing.T) { t.Fatal(err) } err := PrintSarifReport(test.caseTest.path, test.caseTest.filename, test.caseTest.summary) + checkFileExists(t, err, &test, "sarif") + jsonResult, err := os.ReadFile(filepath.Join(test.caseTest.path, test.caseTest.filename+".sarif")) require.NoError(t, err) - require.FileExists(t, filepath.Join(test.caseTest.path, test.caseTest.filename+".sarif")) + var resultSarif sarifReport + err = json.Unmarshal(jsonResult, &resultSarif) + require.NoError(t, err) + require.Equal(t, "", resultSarif.basePath) + require.Equal(t, "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", resultSarif.Schema) + require.Equal(t, "2.1.0", resultSarif.SarifVersion) + require.Len(t, resultSarif.Runs, len(test.expectedResult.Queries)) os.RemoveAll(test.caseTest.path) }) } } + +func checkFileExists(t *testing.T, err error, tc *reportTestCase, extension string) { + require.NoError(t, err) + require.FileExists(t, filepath.Join(tc.caseTest.path, tc.caseTest.filename+fmt.Sprintf(".%s", extension))) +}