diff --git a/e2e/kics_test.go b/e2e/kics_test.go index ca85ebeb6..8b7c69ecf 100644 --- a/e2e/kics_test.go +++ b/e2e/kics_test.go @@ -18,6 +18,7 @@ package e2e import ( "context" "path/filepath" + "reflect" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" @@ -98,3 +99,65 @@ var _ = ginkgo.Describe("Running KICS scan", func() { }) }) }) + +var _ = ginkgo.Describe("Running a KICS scan", func() { + ginkgo.Context("which scans an openapi.yaml file and has report-formats set to sarif", func() { + ginkgo.It("should finish successfully, and output both JSON and Sarif format as well as VMClarity output", func(ctx ginkgo.SpecContext) { + if cfg.TestEnvConfig.Images.PluginKics == "" { + ginkgo.Skip("KICS plugin image not provided") + } + + input, err := filepath.Abs("./testdata") + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + notifier := &Notifier{} + + errs := families.New(&families.Config{ + Plugins: plugins.Config{ + Enabled: true, + ScannersList: []string{scannerPluginName}, + Inputs: []types.Input{ + { + Input: input, + InputType: string(utils.ROOTFS), + }, + }, + ScannersConfig: &common.ScannersConfig{ + scannerPluginName: config.Config{ + Name: scannerPluginName, + ImageName: cfg.TestEnvConfig.Images.PluginKics, + InputDir: "", + ScannerConfig: "{\"report-formats\": [\"sarif\"]}", + }, + }, + }, + }).Run(ctx, notifier) + gomega.Expect(errs).To(gomega.BeEmpty()) + + gomega.Eventually(func() bool { + results := notifier.Results[0].Result.(*plugins.Results).PluginOutputs[scannerPluginName] // nolint:forcetypeassert + + isEmptyFuncs := []func() bool{ + func() bool { return isEmpty(results.RawJSON) }, + func() bool { return isEmpty(results.RawSarif) }, + func() bool { return isEmpty(results.Vmclarity) }, + } + + for _, f := range isEmptyFuncs { + if f() { + return false + } + } + + return true + }, DefaultTimeout, DefaultPeriod).Should(gomega.BeTrue()) + }) + }) +}) + +func isEmpty(x interface{}) bool { + if x == nil { + return true + } + + return reflect.DeepEqual(x, reflect.Zero(reflect.TypeOf(x)).Interface()) +} diff --git a/plugins/README.md b/plugins/README.md index 1a1c30a94..40ddbfb5a 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -7,7 +7,7 @@ Project structure: - **sdk-*** - Language-specific libraries, templates, and examples to aid with the implementation of scanner plugins. - **store** - Collection of available plugins that can be directly used in VMClarity. -### Requirements +## Requirements Scanner plugins are distributed as containers and require [**Docker Engine**](https://docs.docker.com/engine/) on the host that runs the actual scanning via VMClarity CLI to work. @@ -15,12 +15,14 @@ VMClarity CLI to work. ## Support ✅ List of supported environments: + 1. AWS 2. GCP 3. Azure 4. Docker ❌ List of unsupported environments: + - _Kubernetes_ - We plan on adding plugin support to Kubernetes once we have dealt with all the security considerations. _Note:_ Plugin support has been tested against [VMClarity installation artifacts](../installation) for the given environments. @@ -51,5 +53,6 @@ plugins: You can use one of available SDKs in your language of choice to quickly develop scanner plugins for VMClarity. ✅ List of supported languages: + - [Golang](sdk-go) - [Python](sdk-python) diff --git a/plugins/openapi.yaml b/plugins/openapi.yaml index 877e651f1..56507ede4 100644 --- a/plugins/openapi.yaml +++ b/plugins/openapi.yaml @@ -223,6 +223,11 @@ components: # Required. type: null description: Defines scan result data that is not consumed by VMClarity API. + rawSarif: + # Specifies raw scan result data in SARIF format. + # Optional. + type: null + description: Defines scan result data in that is not consumed by the VMClarity API. VMClarityData: type: object diff --git a/plugins/sdk-go/internal/plugin/plugin.gen.go b/plugins/sdk-go/internal/plugin/plugin.gen.go index f819ceec1..49405616d 100644 --- a/plugins/sdk-go/internal/plugin/plugin.gen.go +++ b/plugins/sdk-go/internal/plugin/plugin.gen.go @@ -148,29 +148,29 @@ var swaggerSpec = []string{ "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", - "CQAA//9Tpmlo8ioAAA==", + "8VznEkLaCKurwiV+P95d5AxtB/OJnqc9ZYqnryBu/VWYPp719hqLInYth+TeTB1g+N5PwrZktlsSyrgm", + "UpqnUH7TnSZFVLlJnZjH9z8egXcnO0PDhtek1Ua6d5d3D5OfaERvLyf3lyMa0f54PBpe9B+HD9h/NZzc", + "ve9PLo90TzvrbUHqTuMdFC5O77TdghKQN9v6Llh6B77TccVVYcEbeqMpxAoCu38ohwSRXMi8KnZ760Te", + "dY+4gHAnnslxEHWiQhuocy+x0Y7bQDxKd+Bym6w0cO6KF/4YVIL/UgXhq613vCSaHdAlXMiApsZWLQ7A", + "W8NMpXeqO70P4ncyda3173cy2JIgr//9jjRPGj+y3/DKn6N5L80EWLLa8DkU3II+FMv7310Q79UKypWX", + "dIjPXYKW5tRP1mCqEhWJpzkHA7aAxn05BicZidAL+ALqKoz2NCtXgG3R9HU6D9pwpivLNUoImqi9+Ujz", + "ypV82jTTZsdrdDSQAkhI9lrkpBG+jqBJo43nqpWFXqH+6+RCZ2WZphFFFo70WNbAd6ja7+bHhr793Cxi", + "v9xK/qBU+thE0J2YduTOmTaPiglt05JHXgTO3ohpGx7BlT639unOW8Y0iTMm5pAg/VSqghl6TnHGCc54", + "FXK/qQobk1liMxc/rtfhfMzBiOX8yX68dVOjkPgfg05JlgfBVOsmQ5ZHXmQcqj73U4M00SP7od6/qwWo", + "TaqNC5ZOB39H3fiI+nAT87wS4mHjDF6CePvbCK7428TsL9lGXS1et5E7b1RUj6K2U4QNECy2FamjqNUV", + "rBCpvaTpeIFbVZEA9dJlXMcTrVO0AC2PLI+nVePbAC2HZo4n5bFagNKiygUoNuM531wlHkPxx515qzbh", + "EJppTgncSug/uPwFzjwiAz2ETBOujZKvWnrgplgYuXzVzCu+dIn3CtQw6ci7xdOfTLrLbcngSLOti7p/", + "uG7TkHJbtGma2uoVdd62rlu2M2MaprFUuyRFVcx8SuFcW81S5zhXZ+zuX9g79DeoSIRMqB0RbDsGygVP", + "QNsCHqIJBPCujsdIAgZixHQjLqolsQbMZxWOaYeE4WDEnwIoA8PncPDv0fD2kqQc8sTfg+3ULE/BxKdS", + "nyjIgWmXYdHoDYpB3UlcW6IQ7Fl0ld99Xb6bGvmsYJ+kIlIR+0+v4EIq4gkGKqwHFXnlPEBz0ze4rIv1", + "N7//DJ+/nfrDxWT4OLzoj2jUURG9v7weDa+H70bHFh6CS14obq86aAdLvmga7NtUTIO9rlwa7LqHec7n", + "HDfso0Vp9kLTwkp7B03rdGjs3pP0x0O6Y0X0895Z7wx3UZYgWMnpOf2yd9b7nDrHaHV0Gm+ehpRS26IB", + "atyeTPTkdCy18c9HHEwEbd7JxCrB3nS4SgPblllOP2lnxM6JHnKxnvgeDEUDsQ3uDYLl9YuzzwMQmgh4", + "dkgT85UZgCCxAmYQMK8j+tXZ2Zux2nwWYTluX777PSIzmawwWebCQm/PzHd/LzM7SbuQhnDhSwP2JPfc", + "E6GqKJhaeVXXJQAjQy+W3NMypjYvfRyJ0wxYbrJfkeU5BIzoGsyNH9LS6Vn4DcMO5476yq6+eYvjCxSY", + "ftXFDbvFX599eZAgbsUeUWx6gW5jn9ylXHt7/q9m1e9KsXOn27Utm3vf8L68iaVs1ggYSe1Cal73Zb0G", + "s30a6B8/tW+hrbh6U7foEtZXNv5CUf0KXadhU13cESIksXl58Kn2ZYNul2kLC3+Nw7Sk195fHnKPjy+U", + "E/67vGNDCdNGjcXt+3r9nwAAAP//cvKrQVYrAAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/plugins/sdk-go/types/types.gen.go b/plugins/sdk-go/types/types.gen.go index d8f7ecd90..7ae2f3ada 100644 --- a/plugins/sdk-go/types/types.gen.go +++ b/plugins/sdk-go/types/types.gen.go @@ -164,6 +164,9 @@ type Result struct { // RawJSON Defines scan result data that is not consumed by VMClarity API. RawJSON interface{} `json:"rawJSON"` + // RawSarif Defines scan result data in that is not consumed by the VMClarity API. + RawSarif *interface{} `json:"rawSarif,omitempty"` + // Vmclarity Defines scan result data that can be consumed by VMClarity API. Vmclarity VMClarityData `json:"vmclarity"` } diff --git a/plugins/sdk-python/plugin/models/result.py b/plugins/sdk-python/plugin/models/result.py index 98de42370..de7027fe4 100644 --- a/plugins/sdk-python/plugin/models/result.py +++ b/plugins/sdk-python/plugin/models/result.py @@ -14,7 +14,7 @@ class Result(Model): Do not edit the class manually. """ - def __init__(self, annotations=None, vmclarity=None, raw_json=None): # noqa: E501 + def __init__(self, annotations=None, vmclarity=None, raw_json=None, raw_sarif=None): # noqa: E501 """Result - a model defined in OpenAPI :param annotations: The annotations of this Result. # noqa: E501 @@ -23,22 +23,27 @@ def __init__(self, annotations=None, vmclarity=None, raw_json=None): # noqa: E5 :type vmclarity: VMClarityData :param raw_json: The raw_json of this Result. # noqa: E501 :type raw_json: object + :param raw_sarif: The raw_sarif of this Result. # noqa: E501 + :type raw_sarif: object """ self.openapi_types = { 'annotations': Dict[str, str], 'vmclarity': VMClarityData, - 'raw_json': object + 'raw_json': object, + 'raw_sarif': object } self.attribute_map = { 'annotations': 'annotations', 'vmclarity': 'vmclarity', - 'raw_json': 'rawJSON' + 'raw_json': 'rawJSON', + 'raw_sarif': 'rawSarif' } self._annotations = annotations self._vmclarity = vmclarity self._raw_json = raw_json + self._raw_sarif = raw_sarif @classmethod def from_dict(cls, dikt) -> 'Result': @@ -121,3 +126,26 @@ def raw_json(self, raw_json: object): raise ValueError("Invalid value for `raw_json`, must not be `None`") # noqa: E501 self._raw_json = raw_json + + @property + def raw_sarif(self) -> object: + """Gets the raw_sarif of this Result. + + Defines scan result data in that is not consumed by the VMClarity API. # noqa: E501 + + :return: The raw_sarif of this Result. + :rtype: object + """ + return self._raw_sarif + + @raw_sarif.setter + def raw_sarif(self, raw_sarif: object): + """Sets the raw_sarif of this Result. + + Defines scan result data in that is not consumed by the VMClarity API. # noqa: E501 + + :param raw_sarif: The raw_sarif of this Result. + :type raw_sarif: object + """ + + self._raw_sarif = raw_sarif diff --git a/plugins/store/kics/formatter/formatter.go b/plugins/store/kics/formatter/formatter.go new file mode 100644 index 000000000..00b35e4a2 --- /dev/null +++ b/plugins/store/kics/formatter/formatter.go @@ -0,0 +1,90 @@ +// Copyright © 2024 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package formatter + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + + "github.com/Checkmarx/kics/pkg/model" + + "github.com/openclarity/vmclarity/plugins/sdk-go/types" +) + +var mapKICSSeverity = map[model.Severity]types.MisconfigurationSeverity{ + model.SeverityHigh: types.MisconfigurationSeverityHigh, + model.SeverityMedium: types.MisconfigurationSeverityMedium, + model.SeverityLow: types.MisconfigurationSeverityLow, + model.SeverityInfo: types.MisconfigurationSeverityInfo, + model.SeverityTrace: types.MisconfigurationSeverityInfo, +} + +func FormatJSONOutput(rawOutputDir string) (model.Summary, error) { + var summaryJSON model.Summary + err := decodeFile(filepath.Join(rawOutputDir, "kics.json"), &summaryJSON) + if err != nil { + return model.Summary{}, fmt.Errorf("failed to decode kics.json: %w", err) + } + + return summaryJSON, nil +} + +func FormatVMClarityOutput(summaryJSON model.Summary) (*[]types.Misconfiguration, error) { + var misconfigurations []types.Misconfiguration + for _, q := range summaryJSON.Queries { + for _, file := range q.Files { + misconfigurations = append(misconfigurations, types.Misconfiguration{ + Id: types.Ptr(file.SimilarityID), + Location: types.Ptr(file.FileName + "#" + strconv.Itoa(file.Line)), + Category: types.Ptr(q.Category + ":" + string(file.IssueType)), + Message: types.Ptr(file.KeyActualValue), + Description: types.Ptr(q.Description), + Remediation: types.Ptr(file.KeyExpectedValue), + Severity: types.Ptr(mapKICSSeverity[q.Severity]), + }) + } + } + + return types.Ptr(misconfigurations), nil +} + +func FormatSarifOutput(rawOutputDir string) (*interface{}, error) { + var summarySarif interface{} + err := decodeFile(filepath.Join(rawOutputDir, "kics.sarif"), &summarySarif) + if err != nil { + return nil, fmt.Errorf("failed to decode kics.sarif: %w", err) + } + + return types.Ptr(summarySarif), nil +} + +func decodeFile(filePath string, target interface{}) error { + file, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer file.Close() + + err = json.NewDecoder(file).Decode(target) + if err != nil { + return fmt.Errorf("failed to decode file: %w", err) + } + + return nil +} diff --git a/plugins/store/kics/main.go b/plugins/store/kics/main.go index 5ae91c811..79b727994 100644 --- a/plugins/store/kics/main.go +++ b/plugins/store/kics/main.go @@ -18,16 +18,16 @@ package main import ( "context" "encoding/json" + "errors" "fmt" "log/slog" "os" - "path/filepath" - "strconv" + "sync" "time" "github.com/openclarity/vmclarity/plugins/sdk-go/plugin" + "github.com/openclarity/vmclarity/plugins/store/kics/formatter" - "github.com/Checkmarx/kics/pkg/model" "github.com/Checkmarx/kics/pkg/printer" "github.com/Checkmarx/kics/pkg/progress" "github.com/Checkmarx/kics/pkg/scan" @@ -35,14 +35,6 @@ import ( "github.com/openclarity/vmclarity/plugins/sdk-go/types" ) -var mapKICSSeverity = map[model.Severity]types.MisconfigurationSeverity{ - model.SeverityHigh: types.MisconfigurationSeverityHigh, - model.SeverityMedium: types.MisconfigurationSeverityMedium, - model.SeverityLow: types.MisconfigurationSeverityLow, - model.SeverityInfo: types.MisconfigurationSeverityInfo, - model.SeverityTrace: types.MisconfigurationSeverityInfo, -} - //nolint:containedctx type Scanner struct { status *types.Status @@ -51,6 +43,7 @@ type Scanner struct { type ScannerConfig struct { PreviewLines int `json:"preview-lines" yaml:"preview-lines" toml:"preview-lines" hcl:"preview-lines"` + ReportFormats []string `json:"report-formats" yaml:"report-formats" toml:"report-formats" hcl:"report-formats"` Platform []string `json:"platform" yaml:"platform" toml:"platform" hcl:"platform"` MaxFileSizeFlag int `json:"max-file-size-flag" yaml:"max-file-size-flag" toml:"max-file-size-flag" hcl:"max-file-size-flag"` DisableSecrets bool `json:"disable-secrets" yaml:"disable-secrets" toml:"disable-secrets" hcl:"disable-secrets"` @@ -60,10 +53,10 @@ type ScannerConfig struct { } func (s *Scanner) Metadata() *types.Metadata { - return &types.Metadata{ + return types.Ptr(types.Metadata{ Name: types.Ptr("KICS"), Version: types.Ptr("v1.7.13"), - } + }) } func (s *Scanner) GetStatus() *types.Status { @@ -92,15 +85,17 @@ func (s *Scanner) Start(config types.Config) { return } - rawOutputFile := filepath.Join(os.TempDir(), "kics.json") + // Used to store the raw outputs of a KICS scan + rawOutputDir := os.TempDir() c, err := scan.NewClient( &scan.Parameters{ Path: []string{config.InputDir}, QueriesPath: []string{"../../../queries"}, PreviewLines: clientConfig.PreviewLines, + ReportFormats: clientConfig.ReportFormats, Platform: clientConfig.Platform, - OutputPath: filepath.Dir(rawOutputFile), + OutputPath: rawOutputDir, MaxFileSizeFlag: clientConfig.MaxFileSizeFlag, DisableSecrets: clientConfig.DisableSecrets, QueryExecTimeout: clientConfig.QueryExecTimeout, @@ -128,7 +123,7 @@ func (s *Scanner) Start(config types.Config) { return } - err = s.formatOutput(rawOutputFile, config.OutputFile) + err = s.formatOutput(rawOutputDir, config.OutputFile, clientConfig.ReportFormats) if err != nil { logger.Error("Failed to format KICS output", slog.Any("error", err)) s.SetStatus(types.NewScannerStatus(types.StateFailed, types.Ptr(fmt.Errorf("failed to format KICS output: %w", err).Error()))) @@ -149,63 +144,90 @@ func (s *Scanner) Stop(_ types.Stop) { } //nolint:mnd -func (s *Scanner) createConfig(input *string) (*ScannerConfig, error) { - config := ScannerConfig{ +func (s *Scanner) createConfig(scannerConfig *string) (*ScannerConfig, error) { + config := types.Ptr(ScannerConfig{ PreviewLines: 3, + ReportFormats: []string{"json"}, Platform: []string{"Ansible", "CloudFormation", "Common", "Crossplane", "Dockerfile", "DockerCompose", "Knative", "Kubernetes", "OpenAPI", "Terraform", "AzureResourceManager", "GRPC", "GoogleDeploymentManager", "Buildah", "Pulumi", "ServerlessFW", "CICD"}, MaxFileSizeFlag: 100, DisableSecrets: true, QueryExecTimeout: 60, Silent: true, Minimal: true, - } + }) - if input == nil || *input == "" { - return &config, nil + if scannerConfig == nil || *scannerConfig == "" { + return config, nil } - if err := json.Unmarshal([]byte(*input), &config); err != nil { + if err := json.Unmarshal([]byte(*scannerConfig), config); err != nil { return nil, fmt.Errorf("failed to unmarshal JSON config: %w", err) } - return &config, nil -} + // Ensure JSON format is always included, + // since it's the only format that can be consumed by VMClarity + config.ReportFormats = ensureJSONFormat(config.ReportFormats) -func (s *Scanner) formatOutput(rawFile, outputFile string) error { - file, err := os.Open(rawFile) - if err != nil { - return fmt.Errorf("failed to open kics.json: %w", err) - } - defer file.Close() + return config, nil +} - var summary model.Summary - err = json.NewDecoder(file).Decode(&summary) - if err != nil { - return fmt.Errorf("failed to decode kics.json: %w", err) +func (s *Scanner) formatOutput(rawOutputDir, outputFile string, reportFormats []string) error { + var wg sync.WaitGroup + var resultMutex sync.Mutex + var result types.Result + errCh := make(chan error, len(reportFormats)) + for _, format := range reportFormats { + wg.Add(1) + + go func() { + defer wg.Done() + + switch format { + case "json": + summaryJSON, err := formatter.FormatJSONOutput(rawOutputDir) + if err != nil { + errCh <- fmt.Errorf("failed to format JSON output: %w", err) + } + + misconfigurations, err := formatter.FormatVMClarityOutput(summaryJSON) + if err != nil { + errCh <- fmt.Errorf("failed to format VMClarity output: %w", err) + } + + resultMutex.Lock() + result.RawJSON = summaryJSON + result.Vmclarity.Misconfigurations = misconfigurations + resultMutex.Unlock() + + case "sarif": + summarySarif, err := formatter.FormatSarifOutput(rawOutputDir) + if err != nil { + errCh <- fmt.Errorf("failed to format Sarif output: %w", err) + } + + resultMutex.Lock() + result.RawSarif = summarySarif + resultMutex.Unlock() + + default: + errCh <- fmt.Errorf("unsupported report format: %s", format) + } + }() } - - var misconfigurations []types.Misconfiguration - for _, q := range summary.Queries { - for _, file := range q.Files { - misconfigurations = append(misconfigurations, types.Misconfiguration{ - Id: types.Ptr(file.SimilarityID), - Location: types.Ptr(file.FileName + "#" + strconv.Itoa(file.Line)), - Category: types.Ptr(q.Category + ":" + string(file.IssueType)), - Message: types.Ptr(file.KeyActualValue), - Description: types.Ptr(q.Description), - Remediation: types.Ptr(file.KeyExpectedValue), - Severity: types.Ptr(mapKICSSeverity[q.Severity]), - }) + wg.Wait() + close(errCh) + + // Check for errors + var errs error + for e := range errCh { + if e != nil { + errs = errors.Join(errs, e) } } - - // Save result - result := types.Result{ - Vmclarity: types.VMClarityData{ - Misconfigurations: &misconfigurations, - }, - RawJSON: summary, + if errs != nil { + return errs } + if err := result.Export(outputFile); err != nil { return fmt.Errorf("failed to save KICS result: %w", err) } @@ -213,6 +235,16 @@ func (s *Scanner) formatOutput(rawFile, outputFile string) error { return nil } +func ensureJSONFormat(reportFormats []string) []string { + for _, format := range reportFormats { + if format == "json" { + return reportFormats + } + } + + return append(reportFormats, "json") +} + func main() { plugin.Run(&Scanner{ status: types.NewScannerStatus(types.StateReady, types.Ptr("Starting scanner...")),