-
-
Notifications
You must be signed in to change notification settings - Fork 696
feat: integrate Atlas Cloud provider #1672
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| GOSEC_AI_API_KEY=your_atlas_cloud_api_key | ||
| ATLASCLOUD_API_KEY=your_atlas_cloud_api_key | ||
| GOSEC_AI_PROVIDER=atlas | ||
| GOSEC_AI_MODEL=atlas | ||
| GOSEC_AI_BASE_URL=https://api.atlascloud.ai/v1 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,10 +33,12 @@ coverage.out | |
| *.prof | ||
|
|
||
| .DS_Store | ||
| .env.local | ||
| .env.*.local | ||
|
|
||
| .vscode | ||
| .idea | ||
|
|
||
| # SBOMs generated during CI | ||
| /bom.json | ||
| 1 | ||
| 1 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| # Atlas Cloud Provider Review | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please remove these file. |
||
|
|
||
| ## What Changed | ||
|
|
||
| - Added a first-class `atlas` AI provider preset in `autofix`. | ||
| - Defaulted Atlas Cloud traffic to `https://api.atlascloud.ai/v1`. | ||
| - Added Atlas model aliases: | ||
| - `atlas` -> `deepseek-ai/deepseek-v4-flash` | ||
| - `atlas-deepseek-v4-flash` -> `deepseek-ai/deepseek-v4-flash` | ||
| - `atlas-qwen3-coder-next` -> `qwen/qwen3-coder-next` | ||
| - `atlas-kimi-k2.6` -> `moonshotai/kimi-k2.6` | ||
| - `atlas/<model-id>` and `atlas:<model-id>` for direct model pass-through | ||
| - Added `ATLASCLOUD_API_KEY` fallback support in the CLI when `-ai-api-provider` starts with `atlas`. | ||
| - Updated README with Atlas Cloud usage, examples, and the official link: | ||
| `https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=gosec` | ||
| - Added `.env.example` for local setup and ignored `.env.local` files. | ||
|
|
||
| ## Files Changed | ||
|
|
||
| - `autofix/ai.go` | ||
| - `autofix/atlas.go` | ||
| - `autofix/ai_test.go` | ||
| - `autofix/atlas_test.go` | ||
| - `cmd/gosec/main.go` | ||
| - `README.md` | ||
| - `.gitignore` | ||
| - `.env.example` | ||
|
|
||
| ## Local Validation Plan | ||
|
|
||
| - Unit test the `autofix` package. | ||
| - Build and run `gosec` against a temporary vulnerable sample with `-ai-api-provider=atlas`. | ||
| - Validate direct Atlas Cloud non-stream and stream responses with the provided API key. | ||
|
|
||
| ## Validation Results | ||
|
|
||
| - `go test ./...` passed. | ||
| - Atlas Cloud `/v1/models` responded successfully and returned account-accessible model IDs. | ||
| - Atlas Cloud non-stream chat completion succeeded with `deepseek-ai/deepseek-v4-flash`. | ||
| - Atlas Cloud stream chat completion succeeded with `deepseek-ai/deepseek-v4-flash`. | ||
| - `gosec` binary integration succeeded: | ||
| `-ai-api-provider=atlas` generated a live Autofix for a temporary `G402` sample. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ import ( | |
|
|
||
| const ( | ||
| AIProviderFlagHelp = `AI API provider to generate auto fixes to issues. Valid options are: | ||
| - atlas (Atlas Cloud default), atlas-deepseek-v4-flash, atlas-qwen3-coder-next, atlas-kimi-k2.6, atlas/<model-id>, atlas:<model-id>; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a valid syntax to reference the name of any model |
||
| - gemini-3-pro-preview (gemini, default), gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite; | ||
| - claude-sonnet-4-6 (claude, default), claude-opus-4-7, claude-opus-4-6, claude-sonnet-4-5, claude-opus-4-5, claude-haiku-4-5; | ||
| - gpt-5.4 (openai, default), gpt-5.4-mini, gpt-5.4-nano` | ||
|
|
@@ -32,6 +33,14 @@ func GenerateSolution(model, aiAPIKey, baseURL string, skipSSL bool, issues []*i | |
| var client GenAIClient | ||
|
|
||
| switch { | ||
| case model == "atlas" || strings.HasPrefix(model, "atlas-") || strings.HasPrefix(model, "atlas/") || strings.HasPrefix(model, "atlas:"): | ||
| config := AtlasConfig{ | ||
| Model: model, | ||
| APIKey: aiAPIKey, | ||
| BaseURL: baseURL, | ||
| SkipSSL: skipSSL, | ||
| } | ||
| client, err = NewAtlasClient(config) | ||
| case strings.HasPrefix(model, "claude"): | ||
| client, err = NewClaudeClient(model, aiAPIKey) | ||
| case strings.HasPrefix(model, "gemini"): | ||
|
|
@@ -76,11 +85,11 @@ func generateSolution(client GenAIClient, issues []*issue.Issue) error { | |
| prompt := fmt.Sprintf(AIPrompt, issue.What) | ||
| resp, err := client.GenerateSolution(ctx, prompt) | ||
| if err != nil { | ||
| return fmt.Errorf("generating autofix with gemini: %w", err) | ||
| return fmt.Errorf("generating autofix with AI provider: %w", err) | ||
| } | ||
|
|
||
| if resp == "" { | ||
| return errors.New("no autofix returned by gemini") | ||
| return errors.New("no autofix returned by AI provider") | ||
| } | ||
|
|
||
| issue.Autofix = resp | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| package autofix | ||
|
|
||
| import "strings" | ||
|
|
||
| const ( | ||
| ModelAtlasDefault = "deepseek-ai/deepseek-v4-flash" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these consts outside of this package? If not, I would use private names with lower cases. |
||
| ModelAtlasDeepSeekV4Flash = "deepseek-ai/deepseek-v4-flash" | ||
| ModelAtlasQwenCoderNext = "qwen/qwen3-coder-next" | ||
| ModelAtlasKimiK26 = "moonshotai/kimi-k2.6" | ||
|
|
||
| DefaultAtlasBaseURL = "https://api.atlascloud.ai/v1" | ||
| ) | ||
|
|
||
| type AtlasConfig struct { | ||
| Model string | ||
| APIKey string `json:"-"` | ||
| BaseURL string | ||
| MaxTokens int | ||
| Temperature float64 | ||
| SkipSSL bool | ||
| } | ||
|
|
||
| func NewAtlasClient(config AtlasConfig) (GenAIClient, error) { | ||
| baseURL := config.BaseURL | ||
| if baseURL == "" { | ||
| baseURL = DefaultAtlasBaseURL | ||
| } | ||
|
|
||
| return NewOpenAIClient(OpenAIConfig{ | ||
| Model: parseAtlasModel(config.Model), | ||
| APIKey: config.APIKey, | ||
| BaseURL: baseURL, | ||
| MaxTokens: config.MaxTokens, | ||
| Temperature: config.Temperature, | ||
| SkipSSL: config.SkipSSL, | ||
| }) | ||
| } | ||
|
|
||
| func parseAtlasModel(model string) string { | ||
| switch model { | ||
| case "", "atlas", "atlas-deepseek-v4-flash": | ||
| return ModelAtlasDefault | ||
| case "atlas-qwen3-coder-next", "atlas-qwen-turbo": | ||
| return ModelAtlasQwenCoderNext | ||
| case "atlas-kimi-k2.6", "atlas-kimi-k2": | ||
| return ModelAtlasKimiK26 | ||
| } | ||
|
|
||
| for _, prefix := range []string{"atlas/", "atlas:"} { | ||
| if strings.HasPrefix(model, prefix) { | ||
| trimmed := strings.TrimPrefix(model, prefix) | ||
| if trimmed != "" { | ||
| return trimmed | ||
| } | ||
| return ModelAtlasDefault | ||
| } | ||
| } | ||
|
|
||
| if strings.HasPrefix(model, "atlas-") { | ||
| trimmed := strings.TrimPrefix(model, "atlas-") | ||
| if trimmed != "" { | ||
| return trimmed | ||
| } | ||
| return ModelAtlasDefault | ||
| } | ||
|
|
||
| return model | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| package autofix | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestParseAtlasModel(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| input string | ||
| expected string | ||
| }{ | ||
| { | ||
| name: "atlas defaults to deepseek v3", | ||
| input: "atlas", | ||
| expected: ModelAtlasDefault, | ||
| }, | ||
| { | ||
| name: "atlas deepseek alias", | ||
| input: "atlas-deepseek-v4-flash", | ||
| expected: ModelAtlasDeepSeekV4Flash, | ||
| }, | ||
| { | ||
| name: "atlas qwen alias", | ||
| input: "atlas-qwen3-coder-next", | ||
| expected: ModelAtlasQwenCoderNext, | ||
| }, | ||
| { | ||
| name: "atlas kimi alias", | ||
| input: "atlas-kimi-k2.6", | ||
| expected: ModelAtlasKimiK26, | ||
| }, | ||
| { | ||
| name: "atlas slash syntax", | ||
| input: "atlas/deepseek-v3", | ||
| expected: "deepseek-v3", | ||
| }, | ||
| { | ||
| name: "atlas colon syntax", | ||
| input: "atlas:qwen-plus", | ||
| expected: "qwen-plus", | ||
| }, | ||
| { | ||
| name: "unknown non atlas model passes through", | ||
| input: "custom-model", | ||
| expected: "custom-model", | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| assert.Equal(t, tt.expected, parseAtlasModel(tt.input)) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestNewAtlasClient_Defaults(t *testing.T) { | ||
| client, err := NewAtlasClient(AtlasConfig{ | ||
| Model: "atlas", | ||
| APIKey: "test-key", | ||
| }) | ||
| require.NoError(t, err) | ||
| require.NotNil(t, client) | ||
|
|
||
| wrapper, ok := client.(*openaiWrapper) | ||
| require.True(t, ok) | ||
| assert.Equal(t, ModelAtlasDefault, string(wrapper.model)) | ||
| assert.Equal(t, 1024, wrapper.maxTokens) | ||
| assert.InEpsilon(t, 0.7, wrapper.temperature, 0.001) | ||
| } | ||
|
|
||
| func TestNewAtlasClient_CustomModelSyntax(t *testing.T) { | ||
| client, err := NewAtlasClient(AtlasConfig{ | ||
| Model: "atlas/moonshot-v1-8k", | ||
| APIKey: "test-key", | ||
| BaseURL: DefaultAtlasBaseURL, | ||
| }) | ||
| require.NoError(t, err) | ||
|
|
||
| wrapper := client.(*openaiWrapper) | ||
| assert.Equal(t, "moonshot-v1-8k", string(wrapper.model)) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,7 +66,9 @@ | |
| $ gosec --exclude-rules="scripts/.*:*" ./... | ||
| ` | ||
| // Environment variable for AI API key. | ||
| aiAPIKeyEnv = "GOSEC_AI_API_KEY" // #nosec G101 | ||
| aiAPIKeyEnv = "GOSEC_AI_API_KEY" // #nosec G101 | ||
| atlasAPIKeyEnv = "ATLASCLOUD_API_KEY" | ||
|
Check failure on line 70 in cmd/gosec/main.go
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why these env variables required? I would just used the generic I would like to keep the name of these environment variables generic. Thanks |
||
| atlasProviderEnv = "atlas" | ||
|
|
||
| // Exit codes | ||
| exitSuccess = 0 | ||
|
|
@@ -181,7 +183,7 @@ | |
| flagAiAPIKey = flag.String("ai-api-key", "", "Key to access the AI API") | ||
|
|
||
| // base URL for AI API (optional, for OpenAI-compatible APIs) | ||
| flagAiBaseURL = flag.String("ai-base-url", "", "Base URL for AI API (e.g., for OpenAI-compatible services)") | ||
| flagAiBaseURL = flag.String("ai-base-url", "", "Base URL for AI API (e.g., for Atlas Cloud or other OpenAI-compatible services)") | ||
|
|
||
| // skip SSL verification for AI API | ||
| flagAiSkipSSL = flag.Bool("ai-skip-ssl", false, "Skip SSL certificate verification for AI API") | ||
|
|
@@ -589,6 +591,9 @@ | |
|
|
||
| // Call AI request to solve the issues | ||
| aiAPIKey := os.Getenv(aiAPIKeyEnv) | ||
| if aiAPIKey == "" && strings.HasPrefix(*flagAiAPIProvider, atlasProviderEnv) { | ||
| aiAPIKey = os.Getenv(atlasAPIKeyEnv) | ||
| } | ||
| if aiAPIKey == "" { | ||
| aiAPIKey = *flagAiAPIKey | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would remove this env variable. It seems duplicate to me. They need to be generic and provider agnostic.