Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
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
Copy link
Copy Markdown
Member

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.

GOSEC_AI_PROVIDER=atlas
GOSEC_AI_MODEL=atlas
GOSEC_AI_BASE_URL=https://api.atlascloud.ai/v1
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ coverage.out
*.prof

.DS_Store
.env.local
.env.*.local

.vscode
.idea

# SBOMs generated during CI
/bom.json
1
1
42 changes: 42 additions & 0 deletions ATLAS_CLOUD_REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Atlas Cloud Provider Review
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,14 @@ line arguments:

- `ai-api-provider`: the name of the AI API provider.
Supported providers:
- **Atlas Cloud**: `atlas` (default model
`deepseek-ai/deepseek-v4-flash`),
`atlas-deepseek-v4-flash`,
`atlas-qwen3-coder-next`, `atlas-kimi-k2.6`, or
`atlas/<model-id>` / `atlas:<model-id>` for any Atlas Cloud
hosted chat model.
Atlas Cloud is an OpenAI-compatible provider available at
[atlascloud.ai](https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=gosec)
- **Gemini**: `gemini-3-pro-preview` (default),
`gemini-2.5-pro`, `gemini-2.5-flash`,
`gemini-2.5-flash-lite`
Expand All @@ -411,6 +419,8 @@ line arguments:
(requires `ai-base-url`)
- `ai-api-key` or set the environment variable
`GOSEC_AI_API_KEY`: the key to access the AI API
- For Atlas Cloud, you can also set `ATLASCLOUD_API_KEY`
and use the default base URL `https://api.atlascloud.ai/v1`
- For Gemini, you can create an API key following
[these instructions](https://ai.google.dev/gemini-api/docs/api-key)
- For Claude, get your API key from
Expand All @@ -420,12 +430,23 @@ line arguments:
- `ai-base-url`: (optional) custom base URL for
OpenAI-compatible APIs (e.g., Azure OpenAI, LocalAI,
Ollama)
- Atlas Cloud uses `https://api.atlascloud.ai/v1` by default,
so `ai-base-url` is optional for the built-in `atlas`
provider
- `ai-skip-ssl`: (optional) skip SSL certificate verification
for AI API (useful for self-signed certificates)

**Examples:**

```bash
# Using Atlas Cloud with the default DeepSeek V4 Flash model
export ATLASCLOUD_API_KEY="your_key"
gosec -ai-api-provider="atlas" ./...

# Using Atlas Cloud with an explicit hosted model
gosec -ai-api-provider="atlas:qwen/qwen3-coder-next" \
-ai-api-key="your_key" ./...

# Using Gemini
gosec -ai-api-provider="gemini-3-pro-preview" \
-ai-api-key="your_key" ./...
Expand Down
13 changes: 11 additions & 2 deletions autofix/ai.go
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a valid syntax to reference the name of any model atlas/<model-id>?

- 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`
Expand All @@ -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"):
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions autofix/ai_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func TestGenerateSolutionByGemini_NoCandidates(t *testing.T) {
err := generateSolution(mockClient, issues)

// Assert
require.EqualError(t, err, "no autofix returned by gemini")
require.EqualError(t, err, "no autofix returned by AI provider")
mock.AssertExpectationsForObjects(t, mockClient)
}

Expand All @@ -70,7 +70,7 @@ func TestGenerateSolutionByGemini_APIError(t *testing.T) {
err := generateSolution(mockClient, issues)

// Assert
require.EqualError(t, err, "generating autofix with gemini: API error")
require.EqualError(t, err, "generating autofix with AI provider: API error")
mock.AssertExpectationsForObjects(t, mockClient)
}

Expand Down
68 changes: 68 additions & 0 deletions autofix/atlas.go
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"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
}
85 changes: 85 additions & 0 deletions autofix/atlas_test.go
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))

Check failure on line 70 in autofix/atlas_test.go

View workflow job for this annotation

GitHub Actions / test (1.26.3, latest)

unnecessary conversion (unconvert)
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))

Check failure on line 84 in autofix/atlas_test.go

View workflow job for this annotation

GitHub Actions / test (1.26.3, latest)

unnecessary conversion (unconvert)
}
9 changes: 7 additions & 2 deletions cmd/gosec/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

View workflow job for this annotation

GitHub Actions / test (1.26.3, latest)

G101: Potential hardcoded credentials (gosec)

Check failure

Code scanning / gosec

Potential hardcoded credentials Error

Potential hardcoded credentials
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why these env variables required? I would just used the generic GOSEC_AI_API_KEY and make introduce something like GOSEC_AI_PROVIDER and you can set it to the specific provider like in this example atlas.

I would like to keep the name of these environment variables generic. Thanks

atlasProviderEnv = "atlas"

// Exit codes
exitSuccess = 0
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
}
Expand Down
Loading