From a9e9608eb0ac4bdbb881854700edae7b969d35ca Mon Sep 17 00:00:00 2001 From: Martin Hrabovcin Date: Tue, 20 Feb 2024 23:40:40 +0100 Subject: [PATCH] feat: add cve counts to md report --- .github/actions/copacetic-action/action.yaml | 10 +++--- .../cmd/copa-action/report.go | 7 +++-- .../copacetic-action/pkg/image/scan.go | 11 +++++-- .../copacetic-action/pkg/patch/report.go | 31 ++++++++++++++++++- .../copacetic-action/pkg/patch/task.go | 12 +++---- .github/workflows/cve-patch-images.yaml | 3 +- 6 files changed, 55 insertions(+), 19 deletions(-) diff --git a/.github/actions/copacetic-action/action.yaml b/.github/actions/copacetic-action/action.yaml index bb0aa07..9fdc4fd 100644 --- a/.github/actions/copacetic-action/action.yaml +++ b/.github/actions/copacetic-action/action.yaml @@ -9,9 +9,6 @@ inputs: description: 'List of images to process separated by newline' required: true type: string - # buildkit-version: - # description: 'Buildkit version' - # type: string skip-upload: description: 'Skip uploading to remote registry' default: false @@ -27,6 +24,11 @@ inputs: default: 1h required: false type: string + report-cves: + description: 'Scan and report number of Critical and Highs' + default: false + required: false + type: boolean outputs: result: description: "Patching result" @@ -83,7 +85,7 @@ runs: run: | JSON_REPORT_PATH=$(mktemp) echo "$JSON_REPORT" > $JSON_REPORT_PATH - RESULT="$(devbox run -- go run main.go markdown $JSON_REPORT_PATH)" + RESULT="$(devbox run -- go run main.go markdown $JSON_REPORT_PATH) --print-cves=${{ inputs.report-cves }}" echo "$RESULT" >> $GITHUB_STEP_SUMMARY env: JSON_REPORT: ${{ steps.execute-patch.outputs.result }} diff --git a/.github/actions/copacetic-action/cmd/copa-action/report.go b/.github/actions/copacetic-action/cmd/copa-action/report.go index 8ea1a6f..957728c 100644 --- a/.github/actions/copacetic-action/cmd/copa-action/report.go +++ b/.github/actions/copacetic-action/cmd/copa-action/report.go @@ -11,12 +11,14 @@ import ( "github.com/spf13/cobra" ) +var printCVEs = false + func NewMarkdownCmd() *cobra.Command { cmd := &cobra.Command{ Use: "markdown PATH | -", Short: "Generate markdown report from JSON output", Args: cobra.ExactArgs(1), - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { input, err := cli.OpenFileOrStdin(args[0]) if err != nil { return err @@ -31,8 +33,9 @@ func NewMarkdownCmd() *cobra.Command { return fmt.Errorf("failed to read JSON report: %w", err) } - return patch.WriteMarkdown(report, os.Stdout) + return patch.WriteMarkdown(cmd.Context(), report, os.Stdout, printCVEs) }, } + cmd.Flags().BoolVar(&printCVEs, "print-cves", printCVEs, "enable scanning and printing number of Critical and High CVEs") return cmd } diff --git a/.github/actions/copacetic-action/pkg/image/scan.go b/.github/actions/copacetic-action/pkg/image/scan.go index f2fba5a..e4213a2 100644 --- a/.github/actions/copacetic-action/pkg/image/scan.go +++ b/.github/actions/copacetic-action/pkg/image/scan.go @@ -53,10 +53,17 @@ func (e *CmdErr) Error() string { return e.Err.Error() } +var ( + ScanFixableOS = []string{"--vuln-type", "os", "--ignore-unfixed"} + ScanAllOS = []string{"--vuln-type", "os"} +) + // Scan runs a trivy scan of a image and returns back report. -func Scan(ctx context.Context, imageName string) (*Report, error) { +func Scan(ctx context.Context, imageName string, scanType []string) (*Report, error) { + flags := append([]string{"image"}, scanType...) + flags = append(flags, "--format", "json", imageName) cmd, stdout, stderr := prepareCmd( - ctx, "trivy", "image", "--vuln-type", "os", "--ignore-unfixed", "--format", "json", imageName, + ctx, "trivy", flags..., ) err := cmd.Run() if err != nil { diff --git a/.github/actions/copacetic-action/pkg/patch/report.go b/.github/actions/copacetic-action/pkg/patch/report.go index c78f835..a602495 100644 --- a/.github/actions/copacetic-action/pkg/patch/report.go +++ b/.github/actions/copacetic-action/pkg/patch/report.go @@ -1,6 +1,7 @@ package patch import ( + "context" "encoding/json" "errors" "fmt" @@ -44,7 +45,7 @@ func WriteJSON(tasks []*Task, w io.Writer) error { return json.NewEncoder(w).Encode(r) } -func WriteMarkdown(report Report, w io.Writer) error { +func WriteMarkdown(ctx context.Context, report Report, w io.Writer, printCVEs bool) error { doc := md.NewMarkdown(w) imagesTable := md.TableSet{ @@ -59,6 +60,11 @@ func WriteMarkdown(report Report, w io.Writer) error { md.Code(row.Patched), } + if printCVEs { + mdRow[0] = fmt.Sprintf("%s
%s", row.Image, scanImage(ctx, row.Image)) + mdRow[1] = fmt.Sprintf("%s
%s", row.Patched, scanImage(ctx, row.Patched)) + } + if row.Error != "" { mdRow = append(mdRow, md.Link("View error", fmt.Sprintf("#error-%d", i))) @@ -96,3 +102,26 @@ func WriteMarkdown(report Report, w io.Writer) error { return doc.Build() } + +func scanImage(ctx context.Context, imageRef string) string { + report, err := image.Scan(ctx, imageRef, image.ScanAllOS) + if err != nil { + return md.Code(err.Error()) + } + + counts := map[string]int{ + "CRITICAL": 0, + "HIGH": 0, + } + for _, vuln := range report.Vulnerabilities() { + if _, ok := counts[vuln.Severity]; ok { + counts[vuln.Severity] = counts[vuln.Severity] + 1 + } + } + + parts := []string{} + for severity, count := range counts { + parts = append(parts, fmt.Sprintf("`%d` %s", count, md.Bold(severity))) + } + return strings.Join(parts, " ") +} diff --git a/.github/actions/copacetic-action/pkg/patch/task.go b/.github/actions/copacetic-action/pkg/patch/task.go index aedbb71..3a3e4a4 100644 --- a/.github/actions/copacetic-action/pkg/patch/task.go +++ b/.github/actions/copacetic-action/pkg/patch/task.go @@ -50,7 +50,7 @@ func Run(ctx context.Context, imageRef string, reg registry.Registry, imageTagSu // To avoid generating same patched image always scan the latest patched // tag in the patch registry and only build image if there are available // fixes that would change the latest patched version. - report, err := image.Scan(ctx, imagePatch.Scanned) + report, err := image.Scan(ctx, imagePatch.Scanned, image.ScanFixableOS) if err != nil { return withErr(t, err), err } @@ -96,21 +96,17 @@ func Run(ctx context.Context, imageRef string, reg registry.Registry, imageTagSu patchedRef := imagePatch.SourceRef().Context().Tag(buildTag) logger.Info("regenerated image using copa", "patchedRef", patchedRef.String()) - patchedReport, err := image.Scan(ctx, patchedRef.String()) + patchedReport, err := image.Scan(ctx, patchedRef.String(), image.ScanFixableOS) if err != nil { return withErr(t, err), err } - logger.Info( - "patched vulnerability report", - "original", report.Vulnerabilities(), - "patched", patchedReport.Vulnerabilities(), - ) if slices.Equal( image.VulnerabilitiesIdsSorted(report.Vulnerabilities()), image.VulnerabilitiesIdsSorted(patchedReport.Vulnerabilities()), ) { - logger.Warn("no vulnerabilties were fixed by running copa", + logger.Warn( + "no vulnerabilties were fixed by running copa patch", "scannedImage", imagePatch.Scanned, "scanned", image.VulnerabilitiesIdsSorted(report.Vulnerabilities()), "patched", image.VulnerabilitiesIdsSorted(patchedReport.Vulnerabilities()), diff --git a/.github/workflows/cve-patch-images.yaml b/.github/workflows/cve-patch-images.yaml index a05509c..67c5f1c 100644 --- a/.github/workflows/cve-patch-images.yaml +++ b/.github/workflows/cve-patch-images.yaml @@ -55,7 +55,6 @@ jobs: with: images: ${{ steps.save-images.outputs.images }} github-token: ${{ secrets.GITHUB_TOKEN }} - debug: true - skip-upload: true timeout: 2h + report-cves: true