From bad3aa2de6c6044293c65ff801ec53e1209939e4 Mon Sep 17 00:00:00 2001 From: dlicheva Date: Tue, 12 Nov 2024 18:40:37 +0000 Subject: [PATCH] upgrade the pdf consumer with new formatting --- components/consumers/pdf/README.md | 39 ++ components/consumers/pdf/default.html | 400 +++++++++++++----- .../example_data/gosec.enriched.aggregated.pb | 216 ++++++++++ components/consumers/pdf/main.go | 56 ++- components/consumers/pdf/main_test.go | 1 - 5 files changed, 592 insertions(+), 120 deletions(-) create mode 100644 components/consumers/pdf/README.md create mode 100644 components/consumers/pdf/example_data/gosec.enriched.aggregated.pb diff --git a/components/consumers/pdf/README.md b/components/consumers/pdf/README.md new file mode 100644 index 000000000..33ef29abe --- /dev/null +++ b/components/consumers/pdf/README.md @@ -0,0 +1,39 @@ +# PDF consumer + +This consumer prints the pipeline results into a Go template, prints them into a +PDF and uploads them into an S3 bucket. + +# How it works + +The HTML template is in `/components/consumers/pdf/default.html` .\ +The styles for it are inline.\ +The PDF uses the Print styles, so it's slightly different from what you see in +the browser. +Then the component uses Playwright to render the template and print it into a +PDF. +The PDF is then uploaded into an S3 bucket. + +# How to test locally + +1. Install the requirements for the component. Check the Docker file ( + `/components/consumers/pdf/Dockerfile`) for the + latest versions: + +``` +$ go install github.com/playwright-community/playwright-go/cmd/playwright@v0.4702.0 +$ playwright install chromium --with-deps +``` + +2. Generate the PDF by running this in the `smithy` oss repo root. We don't want + to upload to s3, so we add the `skips3` + flag. The template file is in the component folder: + +``` +go run components/consumers/pdf/main.go +-in components/consumers/pdf/example_data/gosec.enriched.aggregated.pb +-skips3 -template="components/consumers/pdf/default.html" +``` + +This generates the PDF and the report.html in the root of your repo, without +uploading it to an S3 bucket. Don't forget +to delete it later. diff --git a/components/consumers/pdf/default.html b/components/consumers/pdf/default.html index 0a9d7ecc8..45cadda1b 100644 --- a/components/consumers/pdf/default.html +++ b/components/consumers/pdf/default.html @@ -8,9 +8,15 @@ -
- Logo -

Smithy Report

-
+
+ Logo +

Smithy Report

+
-
-
Scan Results
- -
-

This report summarizes the results of running Smithy.

+
+
+

This report summarizes the results of running Smithy.

+ + +
+

High Severity

+

Total

- -
-

Summary

- - -
-
-

Total Number of Findings

-

10

-
-
-

Total High Severity Findings

-

10

-
-
- -
+ + + + + + + + + + {{range .}} + {{range .Issues}} + + + + + + {{ end }} + {{end}} + +
NameSeen beforeSeverity
{{.RawIssue.Title}}{{.Count}} times{{.RawIssue.Severity}}
+
-

The vulnerability scans have identified potential issues that need attention. It is recommended to review - and address the findings promptly to enhance the security of our systems.

-
+

Detailed Results

- - {{range .}} -
-
{{.OriginalResults.ScanInfo.ScanUuid}} - {{.OriginalResults.ToolName}}
+ + {{range .}} +
+

{{.OriginalResults.ToolName}}

+ {{ if and .OriginalResults.ScanInfo.ScanStartTime (ne .OriginalResults.ScanInfo.ScanStartTime.Seconds 0) }}
-
Start Time: {{.OriginalResults.ScanInfo.ScanStartTime}}
+
{{.OriginalResults.ScanInfo.ScanStartTime | formatTime}}
- {{range .Issues}} -
-
{{.RawIssue.Title}}
-
-
Target: {{.RawIssue.Target}}
-
Type: {{.RawIssue.Type}}
-
CVSS: {{.RawIssue.Cvss}}
-
CVE: {{.RawIssue.Cve}}
-
Confidence: {{.RawIssue.Confidence}}
-
Severity: {{.RawIssue.Severity}}
-
Description: {{.RawIssue.Description}}
-
First Seen: {{.FirstSeen}}
-
Seen Before Times: {{.Count}}
-
False Positive?:{{.FalsePositive}}
-
Last Updated: {{.UpdatedAt}}
- {{ range $key,$element := .Annotations }} -

{{$key}}:{{$element}}

- {{end}} -
SBOM
{{.RawIssue.CycloneDXSBOM}}
-
-
- {{end}} + {{ end }} + {{range .Issues}} +
+

{{ .RawIssue.Title }}

+ + + + {{ if and .RawIssue.Severity (ne .RawIssue.Severity nil) }} + + + + + {{ end }} + + {{ if and .RawIssue.Cvss (ne .RawIssue.Cvss 0.0) }} + + + + + {{ end }} + + {{ if and .RawIssue.Confidence (ne .RawIssue.Confidence nil) }} + + + + + {{ end }} + + {{ if and .RawIssue.Type (ne .RawIssue.Type "") }} + + + + + {{ end }} + + {{ if and .RawIssue.Cve (ne .RawIssue.Cve "") }} + + + + + {{ end }} + + {{if gt (len .RawIssue.Cwe) 0}} + + + + + {{end}} + + {{ if and .RawIssue.Target (ne .RawIssue.Target "") }} + + + + + {{ end }} + + {{ if and .RawIssue.Description (ne .RawIssue.Description "") }} + + + + + {{ end }} - + {{ if and .FirstSeen (ne .FirstSeen nil) }} + + + + + {{ end }} + + + + + + {{ if and .FalsePositive (ne .FalsePositive "") }} + + + + + {{ end }} + + {{ if and .UpdatedAt (ne .UpdatedAt.Seconds 0) }} + + + + + {{ end }} + + {{ range $key,$element := .Annotations }} + + + + + {{end}} + +
Severity{{.RawIssue.Severity}}
CVSS{{.RawIssue.Cvss}}
Confidence{{.RawIssue.Confidence}}
Type{{.RawIssue.Type}}
CVE + {{.RawIssue.Cve}} +
CWE + {{range .RawIssue.Cwe}} + {{ . }} + {{end}} +
Target{{.RawIssue.Target}}
Description +
{{.RawIssue.Description}}
+
First Seen{{.FirstSeen | formatTime}}
Seen Before{{.Count}} times
False Positive?{{.FalsePositive}}
Last Updated{{ .UpdatedAt | formatTime }}
{{$key}}{{$element}}
+ + {{ if .RawIssue.CycloneDXSBOM }} +
+
SBOM for {{.RawIssue.Title}}
+
{{ .RawIssue.CycloneDXSBOM }}
+
+ {{ end }}
{{end}} + + + +
+ {{end}} +
+ + diff --git a/components/consumers/pdf/example_data/gosec.enriched.aggregated.pb b/components/consumers/pdf/example_data/gosec.enriched.aggregated.pb new file mode 100644 index 000000000..9d42afcfa --- /dev/null +++ b/components/consumers/pdf/example_data/gosec.enriched.aggregated.pb @@ -0,0 +1,216 @@ + + + Яgosec +0file:///tmp/wspc/go-dvwa/vulnerable/sql.go:52-52G404VUse of weak random number generator (math/rand or math/rand/v2 instead of crypto/rand) 0:?51: "sneaker", +52: rand.Intn(500)) +53: if err != nil { +BunknownR%:d01e22b2-88b0-4873-a13b-9e3d1279af09b fmt.Sprintf("secret password %d", i)) + if err != nil { + return nil, err + } + + _, err = db.Exec( + "INSERT INTO product (name, category, price) VALUES (?, ?, ?)", + fmt.Sprintf("Product %d", i), + "sneaker", + rand.Intn(500)) + if err != nil { + return nil, err + } + } + + return db, nil +} + +type Product struct { + Id intj +-file:///tmp/wspc/go-dvwa/server/main.go:27-27G114GUse of net/http serve function that has no support for setting timeouts 0:26: log.Println("Serving application on", addr) +27: err = http.ListenAndServe(addr, sqhttp.Middleware(router)) +28: if err != nil { +BunknownR%:34ca912e-8df6-47f2-9bd9-7a1bd3771e81b if err != nil { + log.Panic("could not get the executable filename:", err) + } + templateDir := filepath.Join(filepath.Dir(bin), "template") + + router := NewRouter(templateDir) + + addr := ":8080" + log.Println("Serving application on", addr) + err = http.ListenAndServe(addr, sqhttp.Middleware(router)) + if err != nil { + log.Fatalln(err) + } +}j +0file:///tmp/wspc/go-dvwa/vulnerable/sql.go:69-69G202SQL string concatenation 0:68: func GetProducts(ctx context.Context, db *sql.DB, category string) ([]Product, error) { +69: rows, err := db.QueryContext(ctx, "SELECT * FROM product WHERE category='"+category+"'") +70: if err != nil { +BunknownR%:ff3580cd-5ce4-4f88-b125-e66a23dbc41ab +type Product struct { + Id int + Name string + Category string + Price string +} + +func GetProducts(ctx context.Context, db *sql.DB, category string) ([]Product, error) { + rows, err := db.QueryContext(ctx, "SELECT * FROM product WHERE category='"+category+"'") + if err != nil { + return nil, err + } + defer rows.Close() + var products []Product + for rows.Next() { + var product Product + if err := rows.Scan(&product.Id, &product.Name, &product.Category, &product.Price); err != nil { + return nil, err + }jY +1file:///tmp/wspc/go-dvwa/vulnerable/open.go:13-13G304%Potential file inclusion via variable 0:812: // restricted. +13: return os.Open(filepath) +14: } +BunknownR%:9e858968-7fa7-44b4-9da6-960907c3db38b +package vulnerable + +import "os" + +func Open(filepath string) (*os.File, error) { + // Nothing special is needed to make Open vulnerable to local file inclusion + // (LFi). LFi is actually possible when the filepath is not properly + // restricted. + return os.Open(filepath) +}j +/file:///tmp/wspc/go-dvwa/server/router.go:55-59G104Errors unhandled. 0:54: enc := json.NewEncoder(w) +55: enc.Encode(struct { +56: Output string +57: }{ +58: Output: string(output), +59: }) +60: }) +BunknownR%:7f02a510-9b80-4bb6-a4ba-38fc53c6fe53b extra := r.FormValue("extra") + output, err := vulnerable.System(r.Context(), "ping -c1 sqreen.com"+extra) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + enc := json.NewEncoder(w) + enc.Encode(struct { + Output string + }{ + Output: string(output), + }) + }) + + r.PathPrefix("/").Handler(http.FileServer(http.Dir(templateDir))) + + return r +}j + +0file:///tmp/wspc/go-dvwa/vulnerable/sql.go:52-52G404VUse of weak random number generator (math/rand or math/rand/v2 instead of crypto/rand) 0:?51: "sneaker", +52: rand.Intn(500)) +53: if err != nil { +BunknownR%:d01e22b2-88b0-4873-a13b-9e3d1279af09b fmt.Sprintf("secret password %d", i)) + if err != nil { + return nil, err + } + + _, err = db.Exec( + "INSERT INTO product (name, category, price) VALUES (?, ?, ?)", + fmt.Sprintf("Product %d", i), + "sneaker", + rand.Intn(500)) + if err != nil { + return nil, err + } + } + + return db, nil +} + +type Product struct { + Id intj + +-file:///tmp/wspc/go-dvwa/server/main.go:27-27G114GUse of net/http serve function that has no support for setting timeouts 0:26: log.Println("Serving application on", addr) +27: err = http.ListenAndServe(addr, sqhttp.Middleware(router)) +28: if err != nil { +BunknownR%:34ca912e-8df6-47f2-9bd9-7a1bd3771e81b if err != nil { + log.Panic("could not get the executable filename:", err) + } + templateDir := filepath.Join(filepath.Dir(bin), "template") + + router := NewRouter(templateDir) + + addr := ":8080" + log.Println("Serving application on", addr) + err = http.ListenAndServe(addr, sqhttp.Middleware(router)) + if err != nil { + log.Fatalln(err) + } +}j + +0file:///tmp/wspc/go-dvwa/vulnerable/sql.go:69-69G202SQL string concatenation 0:68: func GetProducts(ctx context.Context, db *sql.DB, category string) ([]Product, error) { +69: rows, err := db.QueryContext(ctx, "SELECT * FROM product WHERE category='"+category+"'") +70: if err != nil { +BunknownR%:ff3580cd-5ce4-4f88-b125-e66a23dbc41ab +type Product struct { + Id int + Name string + Category string + Price string +} + +func GetProducts(ctx context.Context, db *sql.DB, category string) ([]Product, error) { + rows, err := db.QueryContext(ctx, "SELECT * FROM product WHERE category='"+category+"'") + if err != nil { + return nil, err + } + defer rows.Close() + var products []Product + for rows.Next() { + var product Product + if err := rows.Scan(&product.Id, &product.Name, &product.Category, &product.Price); err != nil { + return nil, err + }jY + +1file:///tmp/wspc/go-dvwa/vulnerable/open.go:13-13G304%Potential file inclusion via variable 0:812: // restricted. +13: return os.Open(filepath) +14: } +BunknownR%:9e858968-7fa7-44b4-9da6-960907c3db38b +package vulnerable + +import "os" + +func Open(filepath string) (*os.File, error) { + // Nothing special is needed to make Open vulnerable to local file inclusion + // (LFi). LFi is actually possible when the filepath is not properly + // restricted. + return os.Open(filepath) +}j + +/file:///tmp/wspc/go-dvwa/server/router.go:55-59G104Errors unhandled. 0:54: enc := json.NewEncoder(w) +55: enc.Encode(struct { +56: Output string +57: }{ +58: Output: string(output), +59: }) +60: }) +BunknownR%:7f02a510-9b80-4bb6-a4ba-38fc53c6fe53b extra := r.FormValue("extra") + output, err := vulnerable.System(r.Context(), "ping -c1 sqreen.com"+extra) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + enc := json.NewEncoder(w) + enc.Encode(struct { + Output string + }{ + Output: string(output), + }) + }) + + r.PathPrefix("/").Handler(http.FileServer(http.Dir(templateDir))) + + return r +}j \ No newline at end of file diff --git a/components/consumers/pdf/main.go b/components/consumers/pdf/main.go index 5643fe5cd..4488801e1 100644 --- a/components/consumers/pdf/main.go +++ b/components/consumers/pdf/main.go @@ -15,6 +15,9 @@ import ( "log/slog" "os" "path/filepath" + "time" + + "google.golang.org/protobuf/types/known/timestamppb" "github.com/smithy-security/smithy/components/consumers" playwright "github.com/smithy-security/smithy/pkg/playwright" @@ -25,23 +28,25 @@ var ( bucket string region string reportTemplate string + skipS3Upload bool ) func main() { flag.StringVar(&bucket, "bucket", "", "s3 bucket name") flag.StringVar(®ion, "region", "", "s3 bucket region") - flag.StringVar(&reportTemplate, "template", "", "report html template location") + flag.StringVar(&reportTemplate, "template", "default.html", "report html template location") + flag.BoolVar(&skipS3Upload, "skips3", false, "skip s3 upload") if err := consumers.ParseFlags(); err != nil { log.Fatal(err) } - if bucket == "" { + if bucket == "" && !skipS3Upload { log.Fatal("bucket is empty, you need to provide a bucket value") } - if region == "" { + if region == "" && !skipS3Upload { log.Fatal("region is empty, you need to provide a region value") } @@ -63,18 +68,16 @@ func main() { scanID = r[0].OriginalResults.ScanInfo.ScanUuid } - cleanupRun := func(msg string, cleanup func() error) { - if err := cleanup(); err != nil { - slog.Error(msg, "error", err) - } - } - pw, err := playwright.NewClient() if err != nil { log.Fatalf("could not launch playwright: %s", err) } - defer cleanupRun("could not stop Playwright: %w", pw.Stop) + defer func() { + if err := pw.Stop(); err != nil { + slog.Error("could not stop Playwright", slog.String("err", err.Error())) + } + }() client, err := s3client.NewClient(region) if err != nil { @@ -90,17 +93,29 @@ func run(responses any, s3FilenamePostfix string, pw playwright.Wrapper, s3Wrapp slog.Info("reading pdf") resultFilename, pdfBytes, err := buildPdf(responses, pw) if err != nil { - return err + return fmt.Errorf("could not build pdf: %w", err) } + slog.Info("result filename", slog.String("filename", resultFilename)) - slog.Info("uploading pdf to s3", slog.String("filename", resultFilename), slog.String("bucket", bucket), slog.String("region", region)) - return s3Wrapper.UpsertFile(resultFilename, bucket, s3FilenamePostfix, pdfBytes) + if !skipS3Upload { + slog.Info("uploading pdf to s3", slog.String("filename", resultFilename), slog.String("bucket", bucket), slog.String("region", region)) + return s3Wrapper.UpsertFile(resultFilename, bucket, s3FilenamePostfix, pdfBytes) + } + return nil } func buildPdf(data any, pw playwright.Wrapper) (string, []byte, error) { - tmpl, err := template.ParseFiles("default.html") + templateFile := reportTemplate + if templateFile == "" { + templateFile = "default.html" + } + + // process the default template into a html result + tmpl, err := template.New("default.html").Funcs(template.FuncMap{ + "formatTime": formatTime, + }).ParseFiles(templateFile) if err != nil { - return "", nil, err + return "", nil, fmt.Errorf("could not parse files: %w", err) } currentPath, err := os.Getwd() @@ -118,11 +133,18 @@ func buildPdf(data any, pw playwright.Wrapper) (string, []byte, error) { return "", nil, fmt.Errorf("could not apply data to template: %w", err) } + reportPDFPath := filepath.Join(currentPath, "report.pdf") reportPage := fmt.Sprintf("file:///%s", reportHTMLPath) - pdfBytes, err := pw.GetPDFOfPage(reportPage, reportHTMLPath) + pdfBytes, err := pw.GetPDFOfPage(reportPage, reportPDFPath) if err != nil { return "", nil, fmt.Errorf("could not generate pdf from page %s, err: %w", reportPage, err) } - return reportHTMLPath, pdfBytes, err + return reportPDFPath, pdfBytes, err +} + +// formatTime is a template function that converts a timestamp to a human-readable format +func formatTime(timestamp *timestamppb.Timestamp) string { + parsedTime := timestamp.AsTime() + return parsedTime.Format(time.DateTime) } diff --git a/components/consumers/pdf/main_test.go b/components/consumers/pdf/main_test.go index c559c6073..631dc7e72 100644 --- a/components/consumers/pdf/main_test.go +++ b/components/consumers/pdf/main_test.go @@ -42,7 +42,6 @@ func Test_run(t *testing.T) { require.NoError(t, err) require.True(t, pdfCalled) require.True(t, s3Called) - } func Test_buildPdf(t *testing.T) {