Skip to content
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
GOSEC_AI_API_KEY=your_ai_api_key
GOSEC_AI_PROVIDER=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
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,13 @@ 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>` 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 @@ -420,12 +427,32 @@ 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
- `GOSEC_AI_PROVIDER`: (optional) environment variable
alternative to `ai-api-provider`
- `GOSEC_AI_BASE_URL`: (optional) environment variable
alternative to `ai-base-url`
- `ai-skip-ssl`: (optional) skip SSL certificate verification
for AI API (useful for self-signed certificates)

> 🎁 **[Atlas Cloud](https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=gosec)** is a full-modal AI inference platform that gives developers a single AI API to access video generation, image generation, and LLM APIs. Instead of managing multiple vendor integrations, you connect once and get unified access to 300+ curated models across all modalities.
>
> Check out Atlas Cloud's new coding plan promotion for more budget-friendly API access: [https://www.atlascloud.ai/console/coding-plan](https://www.atlascloud.ai/console/coding-plan)

**Examples:**

```bash
# Using Atlas Cloud with the default DeepSeek V4 Flash model
export GOSEC_AI_API_KEY="your_key"
export GOSEC_AI_PROVIDER="atlas"
gosec ./...

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

# 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>;
- 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"
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, 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", wrapper.model)
}
18 changes: 15 additions & 3 deletions cmd/gosec/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ USAGE:
$ gosec --exclude-rules="scripts/.*:*" ./...
`
// Environment variable for AI API key.
aiAPIKeyEnv = "GOSEC_AI_API_KEY" // #nosec G101
aiAPIKeyEnv = "GOSEC_AI_API_KEY" // #nosec G101
aiProviderEnv = "GOSEC_AI_PROVIDER"
aiBaseURLEnv = "GOSEC_AI_BASE_URL"

// Exit codes
exitSuccess = 0
Expand Down Expand Up @@ -588,15 +590,25 @@ func run() int {
reportInfo := gosec.NewReportInfo(issues, metrics, errors).WithVersion(Version)

// Call AI request to solve the issues
aiProvider := *flagAiAPIProvider
if aiProvider == "" {
aiProvider = os.Getenv(aiProviderEnv)
}

aiAPIKey := os.Getenv(aiAPIKeyEnv)
if aiAPIKey == "" {
aiAPIKey = *flagAiAPIKey
}

aiEnabled := *flagAiAPIProvider != ""
aiBaseURL := *flagAiBaseURL
if aiBaseURL == "" {
aiBaseURL = os.Getenv(aiBaseURLEnv)
}

aiEnabled := aiProvider != ""

if len(issues) > 0 && aiEnabled {
err := autofix.GenerateSolution(*flagAiAPIProvider, aiAPIKey, *flagAiBaseURL, *flagAiSkipSSL, issues)
err := autofix.GenerateSolution(aiProvider, aiAPIKey, aiBaseURL, *flagAiSkipSSL, issues)
if err != nil {
logger.Print(err)
}
Expand Down