From b1264a928938eea42e7dcde27c79f34e560dfb34 Mon Sep 17 00:00:00 2001 From: Bujjibabukatta Date: Mon, 15 Jun 2026 13:12:04 +0530 Subject: [PATCH 1/2] feat(plugin): add CheckmarxOne data source plugin (#8869) --- backend/plugins/checkmarxone/README.md | 91 ++++++++++ .../checkmarxone/api/connection_api.go | 109 ++++++++++++ backend/plugins/checkmarxone/api/init.go | 19 ++ backend/plugins/checkmarxone/checkmarxone.go | 45 +++++ backend/plugins/checkmarxone/impl/impl.go | 168 ++++++++++++++++++ .../plugins/checkmarxone/models/connection.go | 35 ++++ .../plugins/checkmarxone/models/finding.go | 46 +++++ .../models/migrationscripts/register.go | 56 ++++++ .../plugins/checkmarxone/models/project.go | 38 ++++ .../plugins/checkmarxone/tasks/api_client.go | 160 +++++++++++++++++ .../checkmarxone/tasks/findings_collector.go | 60 +++++++ .../checkmarxone/tasks/findings_extractor.go | 97 ++++++++++ .../plugins/checkmarxone/tasks/register.go | 31 ++++ .../plugins/checkmarxone/tasks/task_data.go | 33 ++++ 14 files changed, 988 insertions(+) create mode 100644 backend/plugins/checkmarxone/README.md create mode 100644 backend/plugins/checkmarxone/api/connection_api.go create mode 100644 backend/plugins/checkmarxone/api/init.go create mode 100644 backend/plugins/checkmarxone/checkmarxone.go create mode 100644 backend/plugins/checkmarxone/impl/impl.go create mode 100644 backend/plugins/checkmarxone/models/connection.go create mode 100644 backend/plugins/checkmarxone/models/finding.go create mode 100644 backend/plugins/checkmarxone/models/migrationscripts/register.go create mode 100644 backend/plugins/checkmarxone/models/project.go create mode 100644 backend/plugins/checkmarxone/tasks/api_client.go create mode 100644 backend/plugins/checkmarxone/tasks/findings_collector.go create mode 100644 backend/plugins/checkmarxone/tasks/findings_extractor.go create mode 100644 backend/plugins/checkmarxone/tasks/register.go create mode 100644 backend/plugins/checkmarxone/tasks/task_data.go diff --git a/backend/plugins/checkmarxone/README.md b/backend/plugins/checkmarxone/README.md new file mode 100644 index 00000000000..e2f76047d08 --- /dev/null +++ b/backend/plugins/checkmarxone/README.md @@ -0,0 +1,91 @@ +# CheckmarxOne Plugin + +## Summary + +This plugin collects security findings and vulnerabilities from [CheckmarxOne](https://checkmarx.com/checkmarx-one/) - a leading application security testing platform. + +## Features + +- Collect security findings/vulnerabilities from CheckmarxOne projects +- Track vulnerability severity, status, and remediation progress +- Support for multiple projects +- Integration with DevLake's security domain layer + +## Requirements + +- CheckmarxOne account and API access +- Server URL, Client ID, and Client Secret from CheckmarxOne + +## Configuration + +### Connection Setup + +Create a connection to CheckmarxOne using the following fields: + +- **Server URL**: The base URL of your CheckmarxOne instance (e.g., `https://checkmarx.mycompany.com`) +- **Client ID**: OAuth client ID for API access +- **Client Secret**: OAuth client secret for API access +- **Username**: (Optional) Username for authentication +- **Password**: (Optional) Password for authentication + +### Scope Configuration + +Select the CheckmarxOne projects you want to collect data from: + +- **Project ID**: The unique identifier of the CheckmarxOne project + +## Data Collection + +The plugin collects the following data: + +### Findings +- Finding ID and Name +- Severity Level (Critical, High, Medium, Low) +- Status (Open, Fixed, Suppressed) +- First Found and Last Found timestamps +- Finding Description +- Type of finding + +## API Reference + +### POST /connections +Create a new CheckmarxOne connection + +### GET /connections +List all CheckmarxOne connections + +### GET /connections/:connectionId +Get details of a specific connection + +### PATCH /connections/:connectionId +Update a CheckmarxOne connection + +### DELETE /connections/:connectionId +Delete a CheckmarxOne connection + +## Troubleshooting + +### Authentication Issues +- Verify Client ID and Client Secret are correct +- Ensure the API user has appropriate permissions in CheckmarxOne +- Check that the Server URL is accessible from the DevLake instance + +### No Data Collected +- Verify that the project ID exists in CheckmarxOne +- Check that the API client has access to the specified project +- Review the logs for any API errors + +## Development + +To build and test the plugin locally: + +```bash +cd backend/plugins/checkmarxone +go build +``` + +For standalone debugging: + +```bash +./checkmarxone --connectionId=1 --projectId=myproject +``` diff --git a/backend/plugins/checkmarxone/api/connection_api.go b/backend/plugins/checkmarxone/api/connection_api.go new file mode 100644 index 00000000000..ea5ace896d8 --- /dev/null +++ b/backend/plugins/checkmarxone/api/connection_api.go @@ -0,0 +1,109 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 api + +import ( + "net/http" + "strconv" + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/core/plugin" + "github.com/apache/incubator-devlake/plugins/checkmarxone/models" +) + +func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + connection := &models.CheckmarxoneConnection{} + err := input.GetBody(connection) + if err != nil { + return nil, errors.BadInput.Wrap(err, "invalid request body") + } + + basicRes := input.Ctx.Value(plugin.CTX_KEY_BASIC_RES).(plugin.BasicRes) + if err := basicRes.GetDal().Create(connection); err != nil { + return nil, errors.Default.Wrap(err, "failed to create connection") + } + + return &plugin.ApiResourceOutput{Body: connection, Status: http.StatusCreated}, nil +} + +func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + var connections []models.CheckmarxoneConnection + basicRes := input.Ctx.Value(plugin.CTX_KEY_BASIC_RES).(plugin.BasicRes) + + if err := basicRes.GetDal().All(&connections); err != nil { + return nil, errors.Default.Wrap(err, "failed to list connections") + } + + return &plugin.ApiResourceOutput{Body: connections}, nil +} + +func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + connectionId := input.Params["connectionId"] + connId, err := strconv.ParseUint(connectionId, 10, 64) + if err != nil { + return nil, errors.BadInput.New("invalid connection id") + } + + connection := &models.CheckmarxoneConnection{} + basicRes := input.Ctx.Value(plugin.CTX_KEY_BASIC_RES).(plugin.BasicRes) + + if err := basicRes.GetDal().First(connection, map[string]interface{}{"id": connId}); err != nil { + return nil, errors.NotFound.Wrap(err, "connection not found") + } + + return &plugin.ApiResourceOutput{Body: connection}, nil +} + +func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + connectionId := input.Params["connectionId"] + connId, err := strconv.ParseUint(connectionId, 10, 64) + if err != nil { + return nil, errors.BadInput.New("invalid connection id") + } + + connection := &models.CheckmarxoneConnection{} + basicRes := input.Ctx.Value(plugin.CTX_KEY_BASIC_RES).(plugin.BasicRes) + + err2 := input.GetBody(connection) + if err2 != nil { + return nil, errors.BadInput.Wrap(err2, "invalid request body") + } + + connection.ID = connId + if err := basicRes.GetDal().Update(connection); err != nil { + return nil, errors.Default.Wrap(err, "failed to update connection") + } + + return &plugin.ApiResourceOutput{Body: connection}, nil +} + +func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + connectionId := input.Params["connectionId"] + connId, err := strconv.ParseUint(connectionId, 10, 64) + if err != nil { + return nil, errors.BadInput.New("invalid connection id") + } + + basicRes := input.Ctx.Value(plugin.CTX_KEY_BASIC_RES).(plugin.BasicRes) + connection := &models.CheckmarxoneConnection{} + + if err := basicRes.GetDal().Delete(connection, map[string]interface{}{"id": connId}); err != nil { + return nil, errors.Default.Wrap(err, "failed to delete connection") + } + + return &plugin.ApiResourceOutput{Status: http.StatusNoContent}, nil +} diff --git a/backend/plugins/checkmarxone/api/init.go b/backend/plugins/checkmarxone/api/init.go new file mode 100644 index 00000000000..9632c62e56f --- /dev/null +++ b/backend/plugins/checkmarxone/api/init.go @@ -0,0 +1,19 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 api + diff --git a/backend/plugins/checkmarxone/checkmarxone.go b/backend/plugins/checkmarxone/checkmarxone.go new file mode 100644 index 00000000000..eb506eab9eb --- /dev/null +++ b/backend/plugins/checkmarxone/checkmarxone.go @@ -0,0 +1,45 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 main // must be main for plugin entry point + +import ( + "github.com/apache/incubator-devlake/core/runner" + "github.com/apache/incubator-devlake/plugins/checkmarxone/impl" + "github.com/spf13/cobra" +) + +// PluginEntry is a variable exported for Framework to search and load +var PluginEntry impl.CheckmarxOne //nolint + +// standalone mode for debugging +func main() { + cmd := &cobra.Command{Use: "checkmarxone"} + connectionId := cmd.Flags().Uint64P("connectionId", "c", 0, "checkmarxone connection id") + projectId := cmd.Flags().StringP("projectId", "p", "", "checkmarxone project id") + timeAfter := cmd.Flags().StringP("timeAfter", "a", "", "collect data that are created after specified time, ie 2006-01-02T15:04:05Z") + _ = cmd.MarkFlagRequired("connectionId") + _ = cmd.MarkFlagRequired("projectId") + + cmd.Run = func(cmd *cobra.Command, args []string) { + runner.DirectRun(cmd, args, PluginEntry, map[string]interface{}{ + "connectionId": *connectionId, + "projectId": *projectId, + }, *timeAfter) + } + runner.RunCmd(cmd) +} diff --git a/backend/plugins/checkmarxone/impl/impl.go b/backend/plugins/checkmarxone/impl/impl.go new file mode 100644 index 00000000000..34fd04b8281 --- /dev/null +++ b/backend/plugins/checkmarxone/impl/impl.go @@ -0,0 +1,168 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 impl + +import ( + "github.com/apache/incubator-devlake/core/context" + "github.com/apache/incubator-devlake/core/dal" + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/core/plugin" + "github.com/apache/incubator-devlake/plugins/checkmarxone/models" + "github.com/apache/incubator-devlake/plugins/checkmarxone/models/migrationscripts" + "github.com/apache/incubator-devlake/plugins/checkmarxone/tasks" +) + +var _ interface { + plugin.PluginMeta + plugin.PluginInit + plugin.PluginTask + plugin.PluginApi + plugin.PluginModel + plugin.PluginMigration + plugin.PluginSource + plugin.DataSourcePluginBlueprintV200 + plugin.CloseablePluginTask +} = (*CheckmarxOne)(nil) + +type CheckmarxOne struct{} + +func (p CheckmarxOne) Name() string { + return "checkmarxone" +} + +func (p CheckmarxOne) Description() string { + return "collect security findings from CheckmarxOne" +} + +func (p CheckmarxOne) RootPkgPath() string { + return "github.com/apache/incubator-devlake/plugins/checkmarxone" +} + +func (p CheckmarxOne) Init(basicRes context.BasicRes) errors.Error { + return nil +} + +func (p CheckmarxOne) Connection() dal.Tabler { + return &models.CheckmarxoneConnection{} +} + +func (p CheckmarxOne) Scope() plugin.ToolLayerScope { + return &models.CheckmarxoneProject{} +} + +func (p CheckmarxOne) ScopeConfig() dal.Tabler { + return nil +} + +func (p CheckmarxOne) GetTablesInfo() []dal.Tabler { + return []dal.Tabler{ + &models.CheckmarxoneConnection{}, + &models.CheckmarxoneProject{}, + &models.CheckmarxoneFinding{}, + } +} + +func (p CheckmarxOne) SubTaskMetas() []plugin.SubTaskMeta { + return tasks.CollectDataTaskMetas() +} + +func (p CheckmarxOne) PrepareTaskData(taskCtx plugin.TaskContext, options map[string]interface{}) (interface{}, errors.Error) { + var op tasks.CheckmarxoneOptions + err := plugin.Helper.JsonToModel(options, &op) + if err != nil { + return nil, errors.BadInput.Wrap(err, "invalid options") + } + + basicRes := taskCtx.GetContext().Value(plugin.CTX_KEY_BASIC_RES).(plugin.BasicRes) + connection := &models.CheckmarxoneConnection{} + err = basicRes.GetDal().First(connection, map[string]interface{}{"id": op.ConnectionId}) + if err != nil { + return nil, errors.NotFound.Wrap(err, "connection not found") + } + + logger := taskCtx.GetLogger() + apiClient, err := tasks.NewCheckmarxoneApiClient(logger, connection) + if err != nil { + return nil, err + } + + return &tasks.CheckmarxoneTaskData{ + Options: &op, + ApiClient: apiClient, + Connection: connection, + }, nil +} + +func (p CheckmarxOne) MigrationScripts() []plugin.MigrationScript { + return migrationscripts.MigrationScripts{}.MigrationScripts() +} + +func (p CheckmarxOne) ApiResources() map[string]map[string]plugin.ApiResourceHandler { + return map[string]map[string]plugin.ApiResourceHandler{ + "connections": { + "POST": api.PostConnections, + "GET": api.ListConnections, + }, + "connections/:connectionId": { + "GET": api.GetConnection, + "PATCH": api.PatchConnection, + "DELETE": api.DeleteConnection, + }, + } +} + +func (p CheckmarxOne) Close(taskCtx plugin.TaskContext) errors.Error { + data, _ := taskCtx.GetData().(*tasks.CheckmarxoneTaskData) + if data != nil && data.ApiClient != nil { + data.ApiClient.Close() + } + return nil +} + +func (p CheckmarxOne) MakeDataSourcePipelinePlanV200( + connectionId uint64, + scopes []plugin.Scope, + syncPolicy plugin.BlueprintSyncPolicy, +) (plugin.PipelinePlan, errors.Error) { + var err errors.Error + plan := plugin.PipelinePlan{} + + for _, scope := range scopes { + scopeItem, ok := scope.(*models.CheckmarxoneProject) + if !ok { + return nil, errors.BadInput.New("invalid scope item") + } + + stage := plugin.PipelineStage{} + for _, task := range p.SubTaskMetas() { + options := map[string]interface{}{ + "connectionId": connectionId, + "projectId": scopeItem.ProjectId, + } + stage = append(stage, &plugin.PipelineTask{ + Plugin: p.Name(), + Subtasks: []string{task.Name}, + Options: options, + }) + } + + plan = append(plan, stage) + } + + return plan, err +} diff --git a/backend/plugins/checkmarxone/models/connection.go b/backend/plugins/checkmarxone/models/connection.go new file mode 100644 index 00000000000..3a7d1b9d003 --- /dev/null +++ b/backend/plugins/checkmarxone/models/connection.go @@ -0,0 +1,35 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 models + +import ( + "github.com/apache/incubator-devlake/core/models" +) + +type CheckmarxoneConnection struct { + models.BaseConnection `mapstructure:",squash"` + ServerUrl string `mapstructure:"serverUrl" json:"serverUrl"` + Username string `mapstructure:"username" json:"username"` + Password string `mapstructure:"password" json:"-"` + ClientId string `mapstructure:"clientId" json:"clientId"` + ClientSecret string `mapstructure:"clientSecret" json:"-"` +} + +func (CheckmarxoneConnection) TableName() string { + return "checkmarxone_connections" +} diff --git a/backend/plugins/checkmarxone/models/finding.go b/backend/plugins/checkmarxone/models/finding.go new file mode 100644 index 00000000000..b4fe1c36f3b --- /dev/null +++ b/backend/plugins/checkmarxone/models/finding.go @@ -0,0 +1,46 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 models + +import ( + "github.com/apache/incubator-devlake/core/models" + "time" +) + +type CheckmarxoneFinding struct { + ConnectionId uint64 `gorm:"primaryKey" json:"connectionId"` + ProjectId string `gorm:"primaryKey" json:"projectId"` + FindingId string `gorm:"primaryKey" json:"findingId"` + Name string `json:"name"` + Severity string `json:"severity"` + Status string `json:"status"` + Description string `json:"description"` + FirstFound time.Time `json:"firstFound"` + LastFound time.Time `json:"lastFound"` + State string `json:"state"` + Type string `json:"type"` + Count int `json:"count"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + + models.NoPKModel +} + +func (CheckmarxoneFinding) TableName() string { + return "checkmarxone_findings" +} diff --git a/backend/plugins/checkmarxone/models/migrationscripts/register.go b/backend/plugins/checkmarxone/models/migrationscripts/register.go new file mode 100644 index 00000000000..679c91d05f4 --- /dev/null +++ b/backend/plugins/checkmarxone/models/migrationscripts/register.go @@ -0,0 +1,56 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 migrationscripts + +import ( + "github.com/apache/incubator-devlake/core/context" + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/core/models/migrationscripts" + "github.com/apache/incubator-devlake/core/plugin" + "github.com/apache/incubator-devlake/plugins/checkmarxone/models" +) + +var _ migrationscripts.MigrationScript = (*InitSchemas)(nil) + +type InitSchemas struct{} + +func (InitSchemas) Up(basicRes context.BasicRes) errors.Error { + db := basicRes.GetDal() + err := db.AutoMigrate( + &models.CheckmarxoneConnection{}, + &models.CheckmarxoneProject{}, + &models.CheckmarxoneFinding{}, + ) + return err +} + +func (InitSchemas) Version() string { + return "20250101_init_schema" +} + +func (InitSchemas) Name() string { + return "init_schema" +} + +type MigrationScripts struct{} + +func (MigrationScripts) MigrationScripts() []plugin.MigrationScript { + return []plugin.MigrationScript{ + InitSchemas{}, + } +} diff --git a/backend/plugins/checkmarxone/models/project.go b/backend/plugins/checkmarxone/models/project.go new file mode 100644 index 00000000000..42fea176b3c --- /dev/null +++ b/backend/plugins/checkmarxone/models/project.go @@ -0,0 +1,38 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 models + +import ( + "github.com/apache/incubator-devlake/core/models" + "time" +) + +type CheckmarxoneProject struct { + ConnectionId uint64 `gorm:"primaryKey" json:"connectionId"` + ProjectId string `gorm:"primaryKey" json:"projectId"` + Name string `json:"name"` + Description string `json:"description"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + + models.NoPKModel +} + +func (CheckmarxoneProject) TableName() string { + return "checkmarxone_projects" +} diff --git a/backend/plugins/checkmarxone/tasks/api_client.go b/backend/plugins/checkmarxone/tasks/api_client.go new file mode 100644 index 00000000000..11e3d133f9f --- /dev/null +++ b/backend/plugins/checkmarxone/tasks/api_client.go @@ -0,0 +1,160 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 tasks + +import ( + "encoding/base64" + "fmt" + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/core/log" + "github.com/apache/incubator-devlake/core/utils" + "github.com/apache/incubator-devlake/plugins/checkmarxone/models" + "net/http" + "time" +) + +type CheckmarxoneApiClient struct { + client *http.Client + headers map[string]string + logger log.Logger + serverUrl string + username string + password string + clientId string + clientSecret string + token string + tokenExpire time.Time +} + +func NewCheckmarxoneApiClient(logger log.Logger, connection *models.CheckmarxoneConnection) (*CheckmarxoneApiClient, errors.Error) { + client := &CheckmarxoneApiClient{ + client: &http.Client{Timeout: 30 * time.Second}, + logger: logger, + serverUrl: connection.ServerUrl, + username: connection.Username, + password: connection.Password, + clientId: connection.ClientId, + clientSecret: connection.ClientSecret, + headers: map[string]string{ + "Accept": "application/json", + }, + } + + err := client.authenticate() + if err != nil { + return nil, err + } + + return client, nil +} + +func (c *CheckmarxoneApiClient) authenticate() errors.Error { + auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", c.clientId, c.clientSecret))) + + headers := map[string]string{ + "Authorization": fmt.Sprintf("Basic %s", auth), + "Content-Type": "application/x-www-form-urlencoded", + } + + res, err := utils.HTTPRequest( + "POST", + fmt.Sprintf("%s/auth/oauth/token", c.serverUrl), + nil, + map[string]string{"grant_type": "client_credentials"}, + headers, + c.client, + false, + ) + if err != nil { + return errors.Default.Wrap(err, "failed to authenticate with CheckmarxOne") + } + + if res.StatusCode != 200 { + return errors.HttpStatus(res.StatusCode).New(fmt.Sprintf("failed to authenticate: %s", res.Body)) + } + + type TokenResponse struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + } + + var tokenResp TokenResponse + err = utils.UnmarshalResponse(res, &tokenResp) + if err != nil { + return errors.Default.Wrap(err, "failed to parse token response") + } + + c.token = tokenResp.AccessToken + c.tokenExpire = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second) + c.headers["Authorization"] = fmt.Sprintf("Bearer %s", c.token) + + return nil +} + +func (c *CheckmarxoneApiClient) GetProjects() ([]map[string]interface{}, errors.Error) { + url := fmt.Sprintf("%s/api/projects", c.serverUrl) + return c.fetch(url) +} + +func (c *CheckmarxoneApiClient) GetFindings(projectId string) ([]map[string]interface{}, errors.Error) { + url := fmt.Sprintf("%s/api/projects/%s/results-summary", c.serverUrl, projectId) + return c.fetch(url) +} + +func (c *CheckmarxoneApiClient) fetch(url string) ([]map[string]interface{}, errors.Error) { + err := c.checkAndRefreshToken() + if err != nil { + return nil, err + } + + res, err := utils.HTTPRequest("GET", url, nil, nil, c.headers, c.client, false) + if err != nil { + return nil, errors.Default.Wrap(err, "failed to fetch data from CheckmarxOne") + } + + if res.StatusCode != 200 { + return nil, errors.HttpStatus(res.StatusCode).New(fmt.Sprintf("failed to fetch: %s", res.Body)) + } + + type DataResponse struct { + Results []map[string]interface{} `json:"results"` + Data []map[string]interface{} `json:"data"` + } + + var dataResp DataResponse + err = utils.UnmarshalResponse(res, &dataResp) + if err != nil { + return nil, errors.Default.Wrap(err, "failed to parse response") + } + + if len(dataResp.Results) > 0 { + return dataResp.Results, nil + } + return dataResp.Data, nil +} + +func (c *CheckmarxoneApiClient) checkAndRefreshToken() errors.Error { + if time.Now().Before(c.tokenExpire) { + return nil + } + return c.authenticate() +} + +func (c *CheckmarxoneApiClient) Close() { + c.client.CloseIdleConnections() +} diff --git a/backend/plugins/checkmarxone/tasks/findings_collector.go b/backend/plugins/checkmarxone/tasks/findings_collector.go new file mode 100644 index 00000000000..5bc018f5356 --- /dev/null +++ b/backend/plugins/checkmarxone/tasks/findings_collector.go @@ -0,0 +1,60 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 tasks + +import ( + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/core/plugin" +) + +const RAW_FINDINGS_TABLE = "checkmarxone_api_findings" + +var CollectFindingsMeta = plugin.SubTaskMeta{ + Name: "collectFindings", + EntryPoint: CollectFindings, + EnabledByDefault: true, + Description: "Collect findings from CheckmarxOne API", + DomainTypes: []string{plugin.DOMAIN_TYPE_SECURITY}, +} + +func CollectFindings(taskCtx plugin.SubTaskContext) errors.Error { + data := taskCtx.GetData().(*CheckmarxoneTaskData) + logger := taskCtx.GetLogger() + + findings, err := data.ApiClient.GetFindings(data.Options.ProjectId) + if err != nil { + logger.Error(err, "failed to fetch findings") + return err + } + + for _, finding := range findings { + select { + case <-taskCtx.GetContext().Done(): + return taskCtx.GetContext().Err() + default: + } + + err := taskCtx.SaveRawData(RAW_FINDINGS_TABLE, finding) + if err != nil { + logger.Error(err, "failed to save raw data") + return err + } + } + + return nil +} \ No newline at end of file diff --git a/backend/plugins/checkmarxone/tasks/findings_extractor.go b/backend/plugins/checkmarxone/tasks/findings_extractor.go new file mode 100644 index 00000000000..612a9b778ce --- /dev/null +++ b/backend/plugins/checkmarxone/tasks/findings_extractor.go @@ -0,0 +1,97 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 tasks + +import ( + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/core/plugin" + checkmarxoneModels "github.com/apache/incubator-devlake/plugins/checkmarxone/models" +) + +var ExtractFindingsMeta = plugin.SubTaskMeta{ + Name: "extractFindings", + EntryPoint: ExtractFindings, + EnabledByDefault: true, + Description: "Extract findings data", + DomainTypes: []string{plugin.DOMAIN_TYPE_SECURITY}, +} + +var ExtractFindingsMeta = plugin.SubTaskMeta{ + Name: "extractFindings", + EntryPoint: ExtractFindings, + EnabledByDefault: true, + Description: "Extract findings data", + DomainTypes: []string{plugin.DOMAIN_TYPE_SECURITY}, +} + +func ExtractFindings(taskCtx plugin.SubTaskContext) errors.Error { + data := taskCtx.GetData().(*CheckmarxoneTaskData) + logger := taskCtx.GetLogger() + + extractor, err := plugin.NewDataConverter(plugin.DataConverterArgs{ + RawDataSubTaskArgs: plugin.RawDataSubTaskArgs{ + Ctx: taskCtx, + Table: RAW_FINDINGS_TABLE, + }, + InputRowType: func() interface{} { + return make(map[string]interface{}) + }, + Input: nil, + Convert: func(inputRow interface{}) ([]interface{}, errors.Error) { + rawData := inputRow.(map[string]interface{}) + + finding := checkmarxoneModels.CheckmarxoneFinding{ + ConnectionId: data.Options.ConnectionId, + ProjectId: data.Options.ProjectId, + } + + if id, ok := rawData["id"].(string); ok { + finding.FindingId = id + } + if name, ok := rawData["name"].(string); ok { + finding.Name = name + } + if severity, ok := rawData["severity"].(string); ok { + finding.Severity = severity + } + if status, ok := rawData["status"].(string); ok { + finding.Status = status + } + if desc, ok := rawData["description"].(string); ok { + finding.Description = desc + } + if state, ok := rawData["state"].(string); ok { + finding.State = state + } + if fType, ok := rawData["type"].(string); ok { + finding.Type = fType + } + if count, ok := rawData["count"].(float64); ok { + finding.Count = int(count) + } + + return []interface{}{&finding}, nil + }, + }) + if err != nil { + logger.Error(err, "failed to create converter") + return err + } + + return extractor.Execute() +} diff --git a/backend/plugins/checkmarxone/tasks/register.go b/backend/plugins/checkmarxone/tasks/register.go new file mode 100644 index 00000000000..158126b9e58 --- /dev/null +++ b/backend/plugins/checkmarxone/tasks/register.go @@ -0,0 +1,31 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 tasks + +import ( + "github.com/apache/incubator-devlake/core/plugin" +) + +var TaskMetas = []plugin.SubTaskMeta{ + CollectFindingsMeta, + ExtractFindingsMeta, +} + +func CollectDataTaskMetas() []plugin.SubTaskMeta { + return TaskMetas +} diff --git a/backend/plugins/checkmarxone/tasks/task_data.go b/backend/plugins/checkmarxone/tasks/task_data.go new file mode 100644 index 00000000000..f56a0bd31a5 --- /dev/null +++ b/backend/plugins/checkmarxone/tasks/task_data.go @@ -0,0 +1,33 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 tasks + +import ( + "github.com/apache/incubator-devlake/plugins/checkmarxone/models" +) + +type CheckmarxoneTaskData struct { + Options *CheckmarxoneOptions + ApiClient *CheckmarxoneApiClient + Connection *models.CheckmarxoneConnection +} + +type CheckmarxoneOptions struct { + ConnectionId uint64 `json:"connectionId"` + ProjectId string `json:"projectId"` +} From 0ca1f785d12268fbf60558b68823ec7de22ce48f Mon Sep 17 00:00:00 2001 From: Bujjibabukatta Date: Thu, 25 Jun 2026 17:22:13 +0530 Subject: [PATCH 2/2] fix(plugin/checkmarxone): fix license header, migration scripts, compile errors and ci checks --- backend/plugins/checkmarxone/README.md | 18 +++ .../checkmarxone/api/connection_api.go | 91 +++----------- backend/plugins/checkmarxone/api/init.go | 32 +++++ backend/plugins/checkmarxone/impl/impl.go | 58 ++++----- .../plugins/checkmarxone/models/connection.go | 52 ++++++-- .../plugins/checkmarxone/models/finding.go | 28 ++--- .../20250601_init_checkmarxone.go | 44 +++++++ .../migrationscripts/archived/models.go | 72 +++++++++++ .../models/migrationscripts/register.go | 39 +----- .../plugins/checkmarxone/models/project.go | 46 +++++-- .../plugins/checkmarxone/tasks/api_client.go | 113 ++++++++---------- .../checkmarxone/tasks/findings_collector.go | 36 +++++- .../checkmarxone/tasks/findings_extractor.go | 64 +++++----- 13 files changed, 411 insertions(+), 282 deletions(-) create mode 100644 backend/plugins/checkmarxone/models/migrationscripts/20250601_init_checkmarxone.go create mode 100644 backend/plugins/checkmarxone/models/migrationscripts/archived/models.go diff --git a/backend/plugins/checkmarxone/README.md b/backend/plugins/checkmarxone/README.md index e2f76047d08..00f2d0cda10 100644 --- a/backend/plugins/checkmarxone/README.md +++ b/backend/plugins/checkmarxone/README.md @@ -1,3 +1,21 @@ + + + # CheckmarxOne Plugin ## Summary diff --git a/backend/plugins/checkmarxone/api/connection_api.go b/backend/plugins/checkmarxone/api/connection_api.go index ea5ace896d8..f6fe257825d 100644 --- a/backend/plugins/checkmarxone/api/connection_api.go +++ b/backend/plugins/checkmarxone/api/connection_api.go @@ -18,92 +18,31 @@ limitations under the License. package api import ( - "net/http" - "strconv" "github.com/apache/incubator-devlake/core/errors" "github.com/apache/incubator-devlake/core/plugin" - "github.com/apache/incubator-devlake/plugins/checkmarxone/models" ) +// PostConnections creates a new CheckmarxOne connection func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - connection := &models.CheckmarxoneConnection{} - err := input.GetBody(connection) - if err != nil { - return nil, errors.BadInput.Wrap(err, "invalid request body") - } - - basicRes := input.Ctx.Value(plugin.CTX_KEY_BASIC_RES).(plugin.BasicRes) - if err := basicRes.GetDal().Create(connection); err != nil { - return nil, errors.Default.Wrap(err, "failed to create connection") - } - - return &plugin.ApiResourceOutput{Body: connection, Status: http.StatusCreated}, nil -} - -func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - var connections []models.CheckmarxoneConnection - basicRes := input.Ctx.Value(plugin.CTX_KEY_BASIC_RES).(plugin.BasicRes) - - if err := basicRes.GetDal().All(&connections); err != nil { - return nil, errors.Default.Wrap(err, "failed to list connections") - } - - return &plugin.ApiResourceOutput{Body: connections}, nil -} - -func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - connectionId := input.Params["connectionId"] - connId, err := strconv.ParseUint(connectionId, 10, 64) - if err != nil { - return nil, errors.BadInput.New("invalid connection id") - } - - connection := &models.CheckmarxoneConnection{} - basicRes := input.Ctx.Value(plugin.CTX_KEY_BASIC_RES).(plugin.BasicRes) - - if err := basicRes.GetDal().First(connection, map[string]interface{}{"id": connId}); err != nil { - return nil, errors.NotFound.Wrap(err, "connection not found") - } - - return &plugin.ApiResourceOutput{Body: connection}, nil + return dsHelper.ConnApi.Post(input) } +// PatchConnection updates a CheckmarxOne connection func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - connectionId := input.Params["connectionId"] - connId, err := strconv.ParseUint(connectionId, 10, 64) - if err != nil { - return nil, errors.BadInput.New("invalid connection id") - } - - connection := &models.CheckmarxoneConnection{} - basicRes := input.Ctx.Value(plugin.CTX_KEY_BASIC_RES).(plugin.BasicRes) - - err2 := input.GetBody(connection) - if err2 != nil { - return nil, errors.BadInput.Wrap(err2, "invalid request body") - } - - connection.ID = connId - if err := basicRes.GetDal().Update(connection); err != nil { - return nil, errors.Default.Wrap(err, "failed to update connection") - } - - return &plugin.ApiResourceOutput{Body: connection}, nil + return dsHelper.ConnApi.Patch(input) } +// DeleteConnection deletes a CheckmarxOne connection func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - connectionId := input.Params["connectionId"] - connId, err := strconv.ParseUint(connectionId, 10, 64) - if err != nil { - return nil, errors.BadInput.New("invalid connection id") - } - - basicRes := input.Ctx.Value(plugin.CTX_KEY_BASIC_RES).(plugin.BasicRes) - connection := &models.CheckmarxoneConnection{} - - if err := basicRes.GetDal().Delete(connection, map[string]interface{}{"id": connId}); err != nil { - return nil, errors.Default.Wrap(err, "failed to delete connection") - } + return dsHelper.ConnApi.Delete(input) +} - return &plugin.ApiResourceOutput{Status: http.StatusNoContent}, nil +// ListConnections lists all CheckmarxOne connections +func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + return dsHelper.ConnApi.GetAll(input) } + +// GetConnection gets details of a specific CheckmarxOne connection +func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + return dsHelper.ConnApi.GetDetail(input) +} \ No newline at end of file diff --git a/backend/plugins/checkmarxone/api/init.go b/backend/plugins/checkmarxone/api/init.go index 9632c62e56f..efee9278d2f 100644 --- a/backend/plugins/checkmarxone/api/init.go +++ b/backend/plugins/checkmarxone/api/init.go @@ -17,3 +17,35 @@ limitations under the License. package api +import ( + "github.com/apache/incubator-devlake/core/context" + "github.com/apache/incubator-devlake/core/plugin" + "github.com/apache/incubator-devlake/helpers/pluginhelper/api" + "github.com/apache/incubator-devlake/helpers/srvhelper" + "github.com/apache/incubator-devlake/plugins/checkmarxone/models" + "github.com/go-playground/validator/v10" +) + +var vld *validator.Validate +var basicRes context.BasicRes +var dsHelper *api.DsHelper[models.CheckmarxoneConnection, models.CheckmarxoneProject, srvhelper.NoScopeConfig] + +// Init initialises the api package variables +func Init(br context.BasicRes, p plugin.PluginMeta) { + basicRes = br + vld = validator.New() + dsHelper = api.NewDataSourceHelper[ + models.CheckmarxoneConnection, + models.CheckmarxoneProject, + srvhelper.NoScopeConfig, + ]( + br, + p.Name(), + []string{"name"}, + func(c models.CheckmarxoneConnection) models.CheckmarxoneConnection { + return c.Sanitize() + }, + nil, + nil, + ) +} \ No newline at end of file diff --git a/backend/plugins/checkmarxone/impl/impl.go b/backend/plugins/checkmarxone/impl/impl.go index 34fd04b8281..e82df3ecf00 100644 --- a/backend/plugins/checkmarxone/impl/impl.go +++ b/backend/plugins/checkmarxone/impl/impl.go @@ -22,11 +22,14 @@ import ( "github.com/apache/incubator-devlake/core/dal" "github.com/apache/incubator-devlake/core/errors" "github.com/apache/incubator-devlake/core/plugin" + helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" + "github.com/apache/incubator-devlake/plugins/checkmarxone/api" "github.com/apache/incubator-devlake/plugins/checkmarxone/models" "github.com/apache/incubator-devlake/plugins/checkmarxone/models/migrationscripts" "github.com/apache/incubator-devlake/plugins/checkmarxone/tasks" ) +// compile-time interface assertion var _ interface { plugin.PluginMeta plugin.PluginInit @@ -41,33 +44,20 @@ var _ interface { type CheckmarxOne struct{} -func (p CheckmarxOne) Name() string { - return "checkmarxone" -} - -func (p CheckmarxOne) Description() string { - return "collect security findings from CheckmarxOne" -} - +func (p CheckmarxOne) Name() string { return "checkmarxone" } +func (p CheckmarxOne) Description() string { return "collect security findings from CheckmarxOne" } func (p CheckmarxOne) RootPkgPath() string { return "github.com/apache/incubator-devlake/plugins/checkmarxone" } func (p CheckmarxOne) Init(basicRes context.BasicRes) errors.Error { + api.Init(basicRes, p) return nil } -func (p CheckmarxOne) Connection() dal.Tabler { - return &models.CheckmarxoneConnection{} -} - -func (p CheckmarxOne) Scope() plugin.ToolLayerScope { - return &models.CheckmarxoneProject{} -} - -func (p CheckmarxOne) ScopeConfig() dal.Tabler { - return nil -} +func (p CheckmarxOne) Connection() dal.Tabler { return &models.CheckmarxoneConnection{} } +func (p CheckmarxOne) Scope() plugin.ToolLayerScope { return &models.CheckmarxoneProject{} } +func (p CheckmarxOne) ScopeConfig() dal.Tabler { return nil } func (p CheckmarxOne) GetTablesInfo() []dal.Tabler { return []dal.Tabler{ @@ -83,16 +73,14 @@ func (p CheckmarxOne) SubTaskMetas() []plugin.SubTaskMeta { func (p CheckmarxOne) PrepareTaskData(taskCtx plugin.TaskContext, options map[string]interface{}) (interface{}, errors.Error) { var op tasks.CheckmarxoneOptions - err := plugin.Helper.JsonToModel(options, &op) - if err != nil { + if err := helper.Decode(options, &op, nil); err != nil { return nil, errors.BadInput.Wrap(err, "invalid options") } - basicRes := taskCtx.GetContext().Value(plugin.CTX_KEY_BASIC_RES).(plugin.BasicRes) + connectionHelper := helper.NewConnectionHelper(taskCtx, nil, p.Name()) connection := &models.CheckmarxoneConnection{} - err = basicRes.GetDal().First(connection, map[string]interface{}{"id": op.ConnectionId}) - if err != nil { - return nil, errors.NotFound.Wrap(err, "connection not found") + if err := connectionHelper.FirstById(connection, op.ConnectionId); err != nil { + return nil, errors.Default.Wrap(err, "connection not found") } logger := taskCtx.GetLogger() @@ -109,7 +97,7 @@ func (p CheckmarxOne) PrepareTaskData(taskCtx plugin.TaskContext, options map[st } func (p CheckmarxOne) MigrationScripts() []plugin.MigrationScript { - return migrationscripts.MigrationScripts{}.MigrationScripts() + return migrationscripts.All() } func (p CheckmarxOne) ApiResources() map[string]map[string]plugin.ApiResourceHandler { @@ -139,30 +127,24 @@ func (p CheckmarxOne) MakeDataSourcePipelinePlanV200( scopes []plugin.Scope, syncPolicy plugin.BlueprintSyncPolicy, ) (plugin.PipelinePlan, errors.Error) { - var err errors.Error plan := plugin.PipelinePlan{} - for _, scope := range scopes { scopeItem, ok := scope.(*models.CheckmarxoneProject) if !ok { return nil, errors.BadInput.New("invalid scope item") } - stage := plugin.PipelineStage{} for _, task := range p.SubTaskMetas() { - options := map[string]interface{}{ - "connectionId": connectionId, - "projectId": scopeItem.ProjectId, - } stage = append(stage, &plugin.PipelineTask{ Plugin: p.Name(), Subtasks: []string{task.Name}, - Options: options, + Options: map[string]interface{}{ + "connectionId": connectionId, + "projectId": scopeItem.ProjectId, + }, }) } - plan = append(plan, stage) } - - return plan, err -} + return plan, nil +} \ No newline at end of file diff --git a/backend/plugins/checkmarxone/models/connection.go b/backend/plugins/checkmarxone/models/connection.go index 3a7d1b9d003..560ac9fdb4c 100644 --- a/backend/plugins/checkmarxone/models/connection.go +++ b/backend/plugins/checkmarxone/models/connection.go @@ -18,18 +18,54 @@ limitations under the License. package models import ( - "github.com/apache/incubator-devlake/core/models" + "github.com/apache/incubator-devlake/core/utils" + helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" ) +// CheckmarxoneConn holds the essential fields to connect to CheckmarxOne API +type CheckmarxoneConn struct { + ServerUrl string `mapstructure:"serverUrl" validate:"required" json:"serverUrl"` + Username string `mapstructure:"username" json:"username"` + Password string `mapstructure:"password" json:"-" encrypt:"yes"` + ClientId string `mapstructure:"clientId" json:"clientId"` + ClientSecret string `mapstructure:"clientSecret" json:"-" encrypt:"yes"` +} + +// Sanitize returns a sanitized copy (masks secrets for safe display) +func (c CheckmarxoneConn) Sanitize() CheckmarxoneConn { + c.Password = utils.SanitizeString(c.Password) + c.ClientSecret = utils.SanitizeString(c.ClientSecret) + return c +} + +// CheckmarxoneConnection is the DB model for a connection record type CheckmarxoneConnection struct { - models.BaseConnection `mapstructure:",squash"` - ServerUrl string `mapstructure:"serverUrl" json:"serverUrl"` - Username string `mapstructure:"username" json:"username"` - Password string `mapstructure:"password" json:"-"` - ClientId string `mapstructure:"clientId" json:"clientId"` - ClientSecret string `mapstructure:"clientSecret" json:"-"` + helper.BaseConnection `mapstructure:",squash"` + CheckmarxoneConn `mapstructure:",squash"` } func (CheckmarxoneConnection) TableName() string { - return "checkmarxone_connections" + return "_tool_checkmarxone_connections" } + +// Sanitize returns a sanitized copy +func (c CheckmarxoneConnection) Sanitize() CheckmarxoneConnection { + c.CheckmarxoneConn = c.CheckmarxoneConn.Sanitize() + return c +} + +// MergeFromRequest merges request body into connection, preserving existing secrets +func (c *CheckmarxoneConnection) MergeFromRequest(target *CheckmarxoneConnection, body map[string]interface{}) error { + password := target.Password + secret := target.ClientSecret + if err := helper.DecodeMapStruct(body, target, true); err != nil { + return err + } + if target.Password == "" || target.Password == utils.SanitizeString(password) { + target.Password = password + } + if target.ClientSecret == "" || target.ClientSecret == utils.SanitizeString(secret) { + target.ClientSecret = secret + } + return nil +} \ No newline at end of file diff --git a/backend/plugins/checkmarxone/models/finding.go b/backend/plugins/checkmarxone/models/finding.go index b4fe1c36f3b..2f69d643ba3 100644 --- a/backend/plugins/checkmarxone/models/finding.go +++ b/backend/plugins/checkmarxone/models/finding.go @@ -18,29 +18,27 @@ limitations under the License. package models import ( - "github.com/apache/incubator-devlake/core/models" "time" + + "github.com/apache/incubator-devlake/core/models/common" ) type CheckmarxoneFinding struct { ConnectionId uint64 `gorm:"primaryKey" json:"connectionId"` - ProjectId string `gorm:"primaryKey" json:"projectId"` - FindingId string `gorm:"primaryKey" json:"findingId"` - Name string `json:"name"` - Severity string `json:"severity"` - Status string `json:"status"` - Description string `json:"description"` + ProjectId string `gorm:"primaryKey;type:varchar(255)" json:"projectId"` + FindingId string `gorm:"primaryKey;type:varchar(255)" json:"findingId"` + Name string `gorm:"type:varchar(255)" json:"name"` + Severity string `gorm:"type:varchar(50)" json:"severity"` + Status string `gorm:"type:varchar(50)" json:"status"` + Description string `gorm:"type:text" json:"description"` FirstFound time.Time `json:"firstFound"` LastFound time.Time `json:"lastFound"` - State string `json:"state"` - Type string `json:"type"` + State string `gorm:"type:varchar(50)" json:"state"` + Type string `gorm:"type:varchar(100)" json:"type"` Count int `json:"count"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - - models.NoPKModel + common.NoPKModel } func (CheckmarxoneFinding) TableName() string { - return "checkmarxone_findings" -} + return "_tool_checkmarxone_findings" +} \ No newline at end of file diff --git a/backend/plugins/checkmarxone/models/migrationscripts/20250601_init_checkmarxone.go b/backend/plugins/checkmarxone/models/migrationscripts/20250601_init_checkmarxone.go new file mode 100644 index 00000000000..e26f2eb271c --- /dev/null +++ b/backend/plugins/checkmarxone/models/migrationscripts/20250601_init_checkmarxone.go @@ -0,0 +1,44 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 migrationscripts + +import ( + "github.com/apache/incubator-devlake/core/context" + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/helpers/migrationhelper" + "github.com/apache/incubator-devlake/plugins/checkmarxone/models/migrationscripts/archived" +) + +type addInitTables struct{} + +func (*addInitTables) Up(basicRes context.BasicRes) errors.Error { + return migrationhelper.AutoMigrateTables( + basicRes, + &archived.CheckmarxoneConnection{}, + &archived.CheckmarxoneProject{}, + &archived.CheckmarxoneFinding{}, + ) +} + +func (*addInitTables) Version() uint64 { + return 20250601000001 +} + +func (*addInitTables) Name() string { + return "checkmarxone init schemas" +} \ No newline at end of file diff --git a/backend/plugins/checkmarxone/models/migrationscripts/archived/models.go b/backend/plugins/checkmarxone/models/migrationscripts/archived/models.go new file mode 100644 index 00000000000..bc555642c82 --- /dev/null +++ b/backend/plugins/checkmarxone/models/migrationscripts/archived/models.go @@ -0,0 +1,72 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 archived + +import ( + "time" + + "github.com/apache/incubator-devlake/core/models/migrationscripts/archived" +) + +type CheckmarxoneConnection struct { + archived.Model + Name string `gorm:"type:varchar(100);uniqueIndex"` + ServerUrl string `gorm:"type:varchar(255)"` + Username string `gorm:"type:varchar(255)"` + Password string `gorm:"type:varchar(255)"` + ClientId string `gorm:"type:varchar(255)"` + ClientSecret string `gorm:"type:varchar(255)"` +} + +func (CheckmarxoneConnection) TableName() string { + return "_tool_checkmarxone_connections" +} + +type CheckmarxoneProject struct { + ConnectionId uint64 `gorm:"primaryKey"` + ProjectId string `gorm:"primaryKey;type:varchar(255)"` + Name string `gorm:"type:varchar(255)"` + Description string `gorm:"type:text"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + archived.NoPKModel +} + +func (CheckmarxoneProject) TableName() string { + return "_tool_checkmarxone_projects" +} + +type CheckmarxoneFinding struct { + ConnectionId uint64 `gorm:"primaryKey"` + ProjectId string `gorm:"primaryKey;type:varchar(255)"` + FindingId string `gorm:"primaryKey;type:varchar(255)"` + Name string `gorm:"type:varchar(255)"` + Severity string `gorm:"type:varchar(50)"` + Status string `gorm:"type:varchar(50)"` + Description string `gorm:"type:text"` + FirstFound time.Time `json:"firstFound"` + LastFound time.Time `json:"lastFound"` + State string `gorm:"type:varchar(50)"` + Type string `gorm:"type:varchar(100)"` + Count int + archived.NoPKModel +} + +func (CheckmarxoneFinding) TableName() string { + return "_tool_checkmarxone_findings" +} \ No newline at end of file diff --git a/backend/plugins/checkmarxone/models/migrationscripts/register.go b/backend/plugins/checkmarxone/models/migrationscripts/register.go index 679c91d05f4..9326b4897db 100644 --- a/backend/plugins/checkmarxone/models/migrationscripts/register.go +++ b/backend/plugins/checkmarxone/models/migrationscripts/register.go @@ -17,40 +17,11 @@ limitations under the License. package migrationscripts -import ( - "github.com/apache/incubator-devlake/core/context" - "github.com/apache/incubator-devlake/core/errors" - "github.com/apache/incubator-devlake/core/models/migrationscripts" - "github.com/apache/incubator-devlake/core/plugin" - "github.com/apache/incubator-devlake/plugins/checkmarxone/models" -) +import "github.com/apache/incubator-devlake/core/plugin" -var _ migrationscripts.MigrationScript = (*InitSchemas)(nil) - -type InitSchemas struct{} - -func (InitSchemas) Up(basicRes context.BasicRes) errors.Error { - db := basicRes.GetDal() - err := db.AutoMigrate( - &models.CheckmarxoneConnection{}, - &models.CheckmarxoneProject{}, - &models.CheckmarxoneFinding{}, - ) - return err -} - -func (InitSchemas) Version() string { - return "20250101_init_schema" -} - -func (InitSchemas) Name() string { - return "init_schema" -} - -type MigrationScripts struct{} - -func (MigrationScripts) MigrationScripts() []plugin.MigrationScript { +// All returns all the migration scripts for checkmarxone plugin +func All() []plugin.MigrationScript { return []plugin.MigrationScript{ - InitSchemas{}, + new(addInitTables), } -} +} \ No newline at end of file diff --git a/backend/plugins/checkmarxone/models/project.go b/backend/plugins/checkmarxone/models/project.go index 42fea176b3c..6bc6f22a571 100644 --- a/backend/plugins/checkmarxone/models/project.go +++ b/backend/plugins/checkmarxone/models/project.go @@ -18,21 +18,45 @@ limitations under the License. package models import ( - "github.com/apache/incubator-devlake/core/models" - "time" + "github.com/apache/incubator-devlake/core/models/common" + "github.com/apache/incubator-devlake/core/plugin" ) +// compile-time interface check +var _ plugin.ToolLayerScope = (*CheckmarxoneProject)(nil) + type CheckmarxoneProject struct { - ConnectionId uint64 `gorm:"primaryKey" json:"connectionId"` - ProjectId string `gorm:"primaryKey" json:"projectId"` - Name string `json:"name"` - Description string `json:"description"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - - models.NoPKModel + common.Scope `mapstructure:",squash"` + ProjectId string `json:"projectId" gorm:"primaryKey;type:varchar(255)" mapstructure:"projectId"` + Name string `json:"name" gorm:"type:varchar(255)"` + Description string `json:"description" gorm:"type:text"` } func (CheckmarxoneProject) TableName() string { - return "checkmarxone_projects" + return "_tool_checkmarxone_projects" +} + +func (p CheckmarxoneProject) ScopeId() string { + return p.ProjectId +} + +func (p CheckmarxoneProject) ScopeName() string { + return p.Name } + +func (p CheckmarxoneProject) ScopeFullName() string { + return p.Name +} + +func (p CheckmarxoneProject) ScopeParams() interface{} { + return CheckmarxoneApiParams{ + ConnectionId: p.ConnectionId, + ProjectId: p.ProjectId, + } +} + +// CheckmarxoneApiParams identifies a unique set of data for raw data storage +type CheckmarxoneApiParams struct { + ConnectionId uint64 `json:"connectionId"` + ProjectId string `json:"projectId"` +} \ No newline at end of file diff --git a/backend/plugins/checkmarxone/tasks/api_client.go b/backend/plugins/checkmarxone/tasks/api_client.go index 11e3d133f9f..f011f6dd61e 100644 --- a/backend/plugins/checkmarxone/tasks/api_client.go +++ b/backend/plugins/checkmarxone/tasks/api_client.go @@ -19,126 +19,118 @@ package tasks import ( "encoding/base64" + "encoding/json" "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + "github.com/apache/incubator-devlake/core/errors" "github.com/apache/incubator-devlake/core/log" - "github.com/apache/incubator-devlake/core/utils" "github.com/apache/incubator-devlake/plugins/checkmarxone/models" - "net/http" - "time" ) +// CheckmarxoneApiClient is a simple HTTP client for CheckmarxOne API type CheckmarxoneApiClient struct { - client *http.Client - headers map[string]string - logger log.Logger - serverUrl string - username string - password string - clientId string + client *http.Client + logger log.Logger + serverUrl string + clientId string clientSecret string - token string - tokenExpire time.Time + token string + tokenExpire time.Time } +// NewCheckmarxoneApiClient creates a new authenticated API client func NewCheckmarxoneApiClient(logger log.Logger, connection *models.CheckmarxoneConnection) (*CheckmarxoneApiClient, errors.Error) { - client := &CheckmarxoneApiClient{ + c := &CheckmarxoneApiClient{ client: &http.Client{Timeout: 30 * time.Second}, logger: logger, serverUrl: connection.ServerUrl, - username: connection.Username, - password: connection.Password, clientId: connection.ClientId, clientSecret: connection.ClientSecret, - headers: map[string]string{ - "Accept": "application/json", - }, } - - err := client.authenticate() - if err != nil { + if err := c.authenticate(); err != nil { return nil, err } - - return client, nil + return c, nil } func (c *CheckmarxoneApiClient) authenticate() errors.Error { + tokenURL := fmt.Sprintf("%s/auth/oauth/token", c.serverUrl) auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", c.clientId, c.clientSecret))) - headers := map[string]string{ - "Authorization": fmt.Sprintf("Basic %s", auth), - "Content-Type": "application/x-www-form-urlencoded", + body := url.Values{} + body.Set("grant_type", "client_credentials") + + req, err := http.NewRequest("POST", tokenURL, strings.NewReader(body.Encode())) + if err != nil { + return errors.Default.Wrap(err, "failed to create auth request") } + req.Header.Set("Authorization", fmt.Sprintf("Basic %s", auth)) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - res, err := utils.HTTPRequest( - "POST", - fmt.Sprintf("%s/auth/oauth/token", c.serverUrl), - nil, - map[string]string{"grant_type": "client_credentials"}, - headers, - c.client, - false, - ) + res, err := c.client.Do(req) if err != nil { return errors.Default.Wrap(err, "failed to authenticate with CheckmarxOne") } + defer res.Body.Close() - if res.StatusCode != 200 { - return errors.HttpStatus(res.StatusCode).New(fmt.Sprintf("failed to authenticate: %s", res.Body)) + if res.StatusCode != http.StatusOK { + return errors.HttpStatus(res.StatusCode).New("CheckmarxOne authentication failed") } type TokenResponse struct { AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` } - var tokenResp TokenResponse - err = utils.UnmarshalResponse(res, &tokenResp) - if err != nil { + if err := json.NewDecoder(res.Body).Decode(&tokenResp); err != nil { return errors.Default.Wrap(err, "failed to parse token response") } c.token = tokenResp.AccessToken c.tokenExpire = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second) - c.headers["Authorization"] = fmt.Sprintf("Bearer %s", c.token) - return nil } -func (c *CheckmarxoneApiClient) GetProjects() ([]map[string]interface{}, errors.Error) { - url := fmt.Sprintf("%s/api/projects", c.serverUrl) - return c.fetch(url) -} - +// GetFindings fetches findings for a project func (c *CheckmarxoneApiClient) GetFindings(projectId string) ([]map[string]interface{}, errors.Error) { - url := fmt.Sprintf("%s/api/projects/%s/results-summary", c.serverUrl, projectId) - return c.fetch(url) + endpoint := fmt.Sprintf("%s/api/projects/%s/results-summary", c.serverUrl, projectId) + return c.fetch(endpoint) } -func (c *CheckmarxoneApiClient) fetch(url string) ([]map[string]interface{}, errors.Error) { - err := c.checkAndRefreshToken() - if err != nil { +func (c *CheckmarxoneApiClient) fetch(endpoint string) ([]map[string]interface{}, errors.Error) { + if err := c.checkAndRefreshToken(); err != nil { return nil, err } - res, err := utils.HTTPRequest("GET", url, nil, nil, c.headers, c.client, false) + req, err := http.NewRequest("GET", endpoint, nil) if err != nil { - return nil, errors.Default.Wrap(err, "failed to fetch data from CheckmarxOne") + return nil, errors.Default.Wrap(err, "failed to build request") } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token)) + req.Header.Set("Accept", "application/json") - if res.StatusCode != 200 { - return nil, errors.HttpStatus(res.StatusCode).New(fmt.Sprintf("failed to fetch: %s", res.Body)) + res, err := c.client.Do(req) + if err != nil { + return nil, errors.Default.Wrap(err, "HTTP request failed") + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + b, _ := io.ReadAll(res.Body) + return nil, errors.HttpStatus(res.StatusCode).New(fmt.Sprintf("API error: %s", string(b))) } type DataResponse struct { Results []map[string]interface{} `json:"results"` Data []map[string]interface{} `json:"data"` } - var dataResp DataResponse - err = utils.UnmarshalResponse(res, &dataResp) - if err != nil { + if err := json.NewDecoder(res.Body).Decode(&dataResp); err != nil { return nil, errors.Default.Wrap(err, "failed to parse response") } @@ -155,6 +147,7 @@ func (c *CheckmarxoneApiClient) checkAndRefreshToken() errors.Error { return c.authenticate() } +// Close releases idle connections func (c *CheckmarxoneApiClient) Close() { c.client.CloseIdleConnections() -} +} \ No newline at end of file diff --git a/backend/plugins/checkmarxone/tasks/findings_collector.go b/backend/plugins/checkmarxone/tasks/findings_collector.go index 5bc018f5356..794aecf05a5 100644 --- a/backend/plugins/checkmarxone/tasks/findings_collector.go +++ b/backend/plugins/checkmarxone/tasks/findings_collector.go @@ -18,8 +18,12 @@ limitations under the License. package tasks import ( + "encoding/json" + "github.com/apache/incubator-devlake/core/errors" "github.com/apache/incubator-devlake/core/plugin" + helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" + "github.com/apache/incubator-devlake/plugins/checkmarxone/models" ) const RAW_FINDINGS_TABLE = "checkmarxone_api_findings" @@ -29,7 +33,7 @@ var CollectFindingsMeta = plugin.SubTaskMeta{ EntryPoint: CollectFindings, EnabledByDefault: true, Description: "Collect findings from CheckmarxOne API", - DomainTypes: []string{plugin.DOMAIN_TYPE_SECURITY}, + DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY}, } func CollectFindings(taskCtx plugin.SubTaskContext) errors.Error { @@ -42,17 +46,37 @@ func CollectFindings(taskCtx plugin.SubTaskContext) errors.Error { return err } + params := models.CheckmarxoneApiParams{ + ConnectionId: data.Options.ConnectionId, + ProjectId: data.Options.ProjectId, + } + for _, finding := range findings { select { case <-taskCtx.GetContext().Done(): - return taskCtx.GetContext().Err() + return errors.Convert(taskCtx.GetContext().Err()) default: } - err := taskCtx.SaveRawData(RAW_FINDINGS_TABLE, finding) - if err != nil { - logger.Error(err, "failed to save raw data") - return err + b, jsonErr := json.Marshal(finding) + if jsonErr != nil { + return errors.Convert(jsonErr) + } + + paramsBytes, jsonErr := json.Marshal(params) + if jsonErr != nil { + return errors.Convert(jsonErr) + } + + rawData := &helper.RawData{ + Params: string(paramsBytes), + Data: b, + Table: RAW_FINDINGS_TABLE, + } + + if saveErr := taskCtx.GetDal().Create(rawData); saveErr != nil { + logger.Error(saveErr, "failed to save raw data") + return saveErr } } diff --git a/backend/plugins/checkmarxone/tasks/findings_extractor.go b/backend/plugins/checkmarxone/tasks/findings_extractor.go index 612a9b778ce..8ea7547b34d 100644 --- a/backend/plugins/checkmarxone/tasks/findings_extractor.go +++ b/backend/plugins/checkmarxone/tasks/findings_extractor.go @@ -18,80 +18,76 @@ limitations under the License. package tasks import ( + "encoding/json" + "github.com/apache/incubator-devlake/core/errors" "github.com/apache/incubator-devlake/core/plugin" - checkmarxoneModels "github.com/apache/incubator-devlake/plugins/checkmarxone/models" + helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" + "github.com/apache/incubator-devlake/plugins/checkmarxone/models" ) var ExtractFindingsMeta = plugin.SubTaskMeta{ Name: "extractFindings", EntryPoint: ExtractFindings, EnabledByDefault: true, - Description: "Extract findings data", - DomainTypes: []string{plugin.DOMAIN_TYPE_SECURITY}, -} - -var ExtractFindingsMeta = plugin.SubTaskMeta{ - Name: "extractFindings", - EntryPoint: ExtractFindings, - EnabledByDefault: true, - Description: "Extract findings data", - DomainTypes: []string{plugin.DOMAIN_TYPE_SECURITY}, + Description: "Extract findings data from CheckmarxOne", + DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY}, } func ExtractFindings(taskCtx plugin.SubTaskContext) errors.Error { data := taskCtx.GetData().(*CheckmarxoneTaskData) - logger := taskCtx.GetLogger() - extractor, err := plugin.NewDataConverter(plugin.DataConverterArgs{ - RawDataSubTaskArgs: plugin.RawDataSubTaskArgs{ - Ctx: taskCtx, - Table: RAW_FINDINGS_TABLE, - }, - InputRowType: func() interface{} { - return make(map[string]interface{}) + extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{ + RawDataSubTaskArgs: helper.RawDataSubTaskArgs{ + Ctx: taskCtx, + Table: RAW_FINDINGS_TABLE, + Params: models.CheckmarxoneApiParams{ + ConnectionId: data.Options.ConnectionId, + ProjectId: data.Options.ProjectId, + }, }, - Input: nil, - Convert: func(inputRow interface{}) ([]interface{}, errors.Error) { - rawData := inputRow.(map[string]interface{}) + Extract: func(resData *helper.RawData) ([]interface{}, errors.Error) { + rawMap := make(map[string]interface{}) + if e := json.Unmarshal(resData.Data, &rawMap); e != nil { + return nil, errors.Convert(e) + } - finding := checkmarxoneModels.CheckmarxoneFinding{ + finding := &models.CheckmarxoneFinding{ ConnectionId: data.Options.ConnectionId, ProjectId: data.Options.ProjectId, } - if id, ok := rawData["id"].(string); ok { + if id, ok := rawMap["id"].(string); ok { finding.FindingId = id } - if name, ok := rawData["name"].(string); ok { + if name, ok := rawMap["name"].(string); ok { finding.Name = name } - if severity, ok := rawData["severity"].(string); ok { + if severity, ok := rawMap["severity"].(string); ok { finding.Severity = severity } - if status, ok := rawData["status"].(string); ok { + if status, ok := rawMap["status"].(string); ok { finding.Status = status } - if desc, ok := rawData["description"].(string); ok { + if desc, ok := rawMap["description"].(string); ok { finding.Description = desc } - if state, ok := rawData["state"].(string); ok { + if state, ok := rawMap["state"].(string); ok { finding.State = state } - if fType, ok := rawData["type"].(string); ok { + if fType, ok := rawMap["type"].(string); ok { finding.Type = fType } - if count, ok := rawData["count"].(float64); ok { + if count, ok := rawMap["count"].(float64); ok { finding.Count = int(count) } - return []interface{}{&finding}, nil + return []interface{}{finding}, nil }, }) if err != nil { - logger.Error(err, "failed to create converter") return err } return extractor.Execute() -} +} \ No newline at end of file