diff --git a/backend/plugins/checkmarxone/README.md b/backend/plugins/checkmarxone/README.md new file mode 100644 index 00000000000..00f2d0cda10 --- /dev/null +++ b/backend/plugins/checkmarxone/README.md @@ -0,0 +1,109 @@ + + + +# 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..f6fe257825d --- /dev/null +++ b/backend/plugins/checkmarxone/api/connection_api.go @@ -0,0 +1,48 @@ +/* +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 ( + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/core/plugin" +) + +// PostConnections creates a new CheckmarxOne connection +func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + return dsHelper.ConnApi.Post(input) +} + +// PatchConnection updates a CheckmarxOne connection +func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + return dsHelper.ConnApi.Patch(input) +} + +// DeleteConnection deletes a CheckmarxOne connection +func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + return dsHelper.ConnApi.Delete(input) +} + +// 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 new file mode 100644 index 00000000000..efee9278d2f --- /dev/null +++ b/backend/plugins/checkmarxone/api/init.go @@ -0,0 +1,51 @@ +/* +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 ( + "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/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..e82df3ecf00 --- /dev/null +++ b/backend/plugins/checkmarxone/impl/impl.go @@ -0,0 +1,150 @@ +/* +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" + 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 + 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 { + 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) 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 + if err := helper.Decode(options, &op, nil); err != nil { + return nil, errors.BadInput.Wrap(err, "invalid options") + } + + connectionHelper := helper.NewConnectionHelper(taskCtx, nil, p.Name()) + connection := &models.CheckmarxoneConnection{} + if err := connectionHelper.FirstById(connection, op.ConnectionId); err != nil { + return nil, errors.Default.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.All() +} + +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) { + 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() { + stage = append(stage, &plugin.PipelineTask{ + Plugin: p.Name(), + Subtasks: []string{task.Name}, + Options: map[string]interface{}{ + "connectionId": connectionId, + "projectId": scopeItem.ProjectId, + }, + }) + } + plan = append(plan, stage) + } + 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 new file mode 100644 index 00000000000..560ac9fdb4c --- /dev/null +++ b/backend/plugins/checkmarxone/models/connection.go @@ -0,0 +1,71 @@ +/* +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/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 { + helper.BaseConnection `mapstructure:",squash"` + CheckmarxoneConn `mapstructure:",squash"` +} + +func (CheckmarxoneConnection) TableName() string { + 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 new file mode 100644 index 00000000000..2f69d643ba3 --- /dev/null +++ b/backend/plugins/checkmarxone/models/finding.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 models + +import ( + "time" + + "github.com/apache/incubator-devlake/core/models/common" +) + +type CheckmarxoneFinding struct { + ConnectionId uint64 `gorm:"primaryKey" json:"connectionId"` + 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 `gorm:"type:varchar(50)" json:"state"` + Type string `gorm:"type:varchar(100)" json:"type"` + Count int `json:"count"` + common.NoPKModel +} + +func (CheckmarxoneFinding) TableName() string { + 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 new file mode 100644 index 00000000000..9326b4897db --- /dev/null +++ b/backend/plugins/checkmarxone/models/migrationscripts/register.go @@ -0,0 +1,27 @@ +/* +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/plugin" + +// All returns all the migration scripts for checkmarxone plugin +func All() []plugin.MigrationScript { + return []plugin.MigrationScript{ + new(addInitTables), + } +} \ No newline at end of file diff --git a/backend/plugins/checkmarxone/models/project.go b/backend/plugins/checkmarxone/models/project.go new file mode 100644 index 00000000000..6bc6f22a571 --- /dev/null +++ b/backend/plugins/checkmarxone/models/project.go @@ -0,0 +1,62 @@ +/* +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/common" + "github.com/apache/incubator-devlake/core/plugin" +) + +// compile-time interface check +var _ plugin.ToolLayerScope = (*CheckmarxoneProject)(nil) + +type CheckmarxoneProject struct { + 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 "_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 new file mode 100644 index 00000000000..f011f6dd61e --- /dev/null +++ b/backend/plugins/checkmarxone/tasks/api_client.go @@ -0,0 +1,153 @@ +/* +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" + "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/plugins/checkmarxone/models" +) + +// CheckmarxoneApiClient is a simple HTTP client for CheckmarxOne API +type CheckmarxoneApiClient struct { + client *http.Client + logger log.Logger + serverUrl string + clientId string + clientSecret string + token string + tokenExpire time.Time +} + +// NewCheckmarxoneApiClient creates a new authenticated API client +func NewCheckmarxoneApiClient(logger log.Logger, connection *models.CheckmarxoneConnection) (*CheckmarxoneApiClient, errors.Error) { + c := &CheckmarxoneApiClient{ + client: &http.Client{Timeout: 30 * time.Second}, + logger: logger, + serverUrl: connection.ServerUrl, + clientId: connection.ClientId, + clientSecret: connection.ClientSecret, + } + if err := c.authenticate(); err != nil { + return nil, err + } + 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))) + + 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 := c.client.Do(req) + if err != nil { + return errors.Default.Wrap(err, "failed to authenticate with CheckmarxOne") + } + defer res.Body.Close() + + 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 + 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) + return nil +} + +// GetFindings fetches findings for a project +func (c *CheckmarxoneApiClient) GetFindings(projectId string) ([]map[string]interface{}, errors.Error) { + endpoint := fmt.Sprintf("%s/api/projects/%s/results-summary", c.serverUrl, projectId) + return c.fetch(endpoint) +} + +func (c *CheckmarxoneApiClient) fetch(endpoint string) ([]map[string]interface{}, errors.Error) { + if err := c.checkAndRefreshToken(); err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", endpoint, nil) + if err != nil { + 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") + + 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 + if err := json.NewDecoder(res.Body).Decode(&dataResp); 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() +} + +// 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 new file mode 100644 index 00000000000..794aecf05a5 --- /dev/null +++ b/backend/plugins/checkmarxone/tasks/findings_collector.go @@ -0,0 +1,84 @@ +/* +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/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" + +var CollectFindingsMeta = plugin.SubTaskMeta{ + Name: "collectFindings", + EntryPoint: CollectFindings, + EnabledByDefault: true, + Description: "Collect findings from CheckmarxOne API", + DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY}, +} + +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 + } + + params := models.CheckmarxoneApiParams{ + ConnectionId: data.Options.ConnectionId, + ProjectId: data.Options.ProjectId, + } + + for _, finding := range findings { + select { + case <-taskCtx.GetContext().Done(): + return errors.Convert(taskCtx.GetContext().Err()) + default: + } + + 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 + } + } + + 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..8ea7547b34d --- /dev/null +++ b/backend/plugins/checkmarxone/tasks/findings_extractor.go @@ -0,0 +1,93 @@ +/* +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/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" +) + +var ExtractFindingsMeta = plugin.SubTaskMeta{ + Name: "extractFindings", + EntryPoint: ExtractFindings, + EnabledByDefault: true, + Description: "Extract findings data from CheckmarxOne", + DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY}, +} + +func ExtractFindings(taskCtx plugin.SubTaskContext) errors.Error { + data := taskCtx.GetData().(*CheckmarxoneTaskData) + + 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, + }, + }, + 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 := &models.CheckmarxoneFinding{ + ConnectionId: data.Options.ConnectionId, + ProjectId: data.Options.ProjectId, + } + + if id, ok := rawMap["id"].(string); ok { + finding.FindingId = id + } + if name, ok := rawMap["name"].(string); ok { + finding.Name = name + } + if severity, ok := rawMap["severity"].(string); ok { + finding.Severity = severity + } + if status, ok := rawMap["status"].(string); ok { + finding.Status = status + } + if desc, ok := rawMap["description"].(string); ok { + finding.Description = desc + } + if state, ok := rawMap["state"].(string); ok { + finding.State = state + } + if fType, ok := rawMap["type"].(string); ok { + finding.Type = fType + } + if count, ok := rawMap["count"].(float64); ok { + finding.Count = int(count) + } + + return []interface{}{finding}, nil + }, + }) + if err != nil { + return err + } + + return extractor.Execute() +} \ No newline at end of file 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"` +}