Skip to content
Merged
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
37 changes: 37 additions & 0 deletions .github/workflows/go-quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Go Quality Checks

on:
pull_request:
paths:
- '**/*.go'
- 'go.mod'
- 'go.sum'
- '.golangci.yml'
- '.github/workflows/go-quality.yml'

permissions:
contents: read

jobs:
go-quality:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Set up Go
uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with:
go-version: '1.24'
cache: true

- name: Run golangci-lint
uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0
with:
version: v2.8.0
args: --timeout=5m

- name: Run gosec
uses: securego/gosec@75533f497e6090cf7ae8e00a1419c97dca097807 # v2.23.0
with:
args: -exclude=G101,G104,G112,G114,G115,G117,G204,G301,G304,G306,G703,G704,G706 ./...
10 changes: 10 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: 2

run:
timeout: 5m
tests: false

linters:
default: none
enable:
- govet
4 changes: 3 additions & 1 deletion cmd/console/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ func ensureDir(path string) {
}
}
if dir != path && dir != "" {
os.MkdirAll(dir, 0755)
if err := os.MkdirAll(dir, 0750); err != nil {
log.Printf("failed to create directory %s: %v", dir, err)
}
}
}
2 changes: 1 addition & 1 deletion pkg/agent/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type AgentConfig struct {

// AgentKeyConfig holds API key configuration for a provider
type AgentKeyConfig struct {
APIKey string `yaml:"api_key"`
APIKey string `yaml:"api_key"` // #nosec G117 -- API key is expected in local config.
Model string `yaml:"model,omitempty"`
}

Expand Down
1 change: 1 addition & 0 deletions pkg/agent/metrics_history.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ func (mh *MetricsHistory) saveToDisk() {
func (mh *MetricsHistory) loadFromDisk() {
filePath := filepath.Join(mh.dataDir, metricsHistoryFile)

// #nosec G304 -- metrics history path is controlled by the agent configuration.
data, err := os.ReadFile(filePath)
if err != nil {
if !os.IsNotExist(err) {
Expand Down
10 changes: 8 additions & 2 deletions pkg/agent/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,12 @@ func (s *Server) Start() error {
log.Println("Device tracker started")
}

return http.ListenAndServe(addr, mux)
server := &http.Server{
Addr: addr,
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}
return server.ListenAndServe()
}

// handleHealth handles HTTP health checks
Expand Down Expand Up @@ -1958,6 +1963,7 @@ func getTokenUsagePath() string {
// loadTokenUsage loads token usage from disk on startup
func (s *Server) loadTokenUsage() {
path := getTokenUsagePath()
// #nosec G304 -- token usage path is resolved from a fixed location.
data, err := os.ReadFile(path)
if err != nil {
return // File doesn't exist yet
Expand Down Expand Up @@ -2022,7 +2028,7 @@ type KeysStatusResponse struct {
// SetKeyRequest is the request body for POST /settings/keys
type SetKeyRequest struct {
Provider string `json:"provider"`
APIKey string `json:"apiKey"`
APIKey string `json:"apiKey"` // #nosec G117 -- API key is expected in this request payload.
Model string `json:"model,omitempty"`
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/api/handlers/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func validateAndConsumeOAuthState(state string) bool {
type AuthConfig struct {
GitHubClientID string
GitHubSecret string
JWTSecret string
JWTSecret string // #nosec G117 -- configuration contains a JWT secret value.
FrontendURL string
BackendURL string // Backend URL for OAuth callback (defaults to http://localhost:8080)
DevUserLogin string
Expand Down
8 changes: 6 additions & 2 deletions pkg/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type Config struct {
DatabasePath string
GitHubClientID string
GitHubSecret string
JWTSecret string
JWTSecret string // #nosec G117 -- configuration contains a JWT secret value.
FrontendURL string
ClaudeAPIKey string
KubestellarOpsPath string
Expand Down Expand Up @@ -220,7 +220,11 @@ func startLoadingServer(addr string) *http.Server {
w.Write([]byte(startupLoadingHTML))
})

srv := &http.Server{Addr: addr, Handler: mux}
srv := &http.Server{
Addr: addr,
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}
go func() {
log.Printf("Loading page available on %s", addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
Expand Down
1 change: 1 addition & 0 deletions pkg/k8s/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ func NewMultiClusterClient(kubeconfig string) (*MultiClusterClient, error) {
}

// Try to detect if we're running in-cluster
// #nosec G703 -- kubeconfig path is provided explicitly via configuration.
if _, err := os.Stat(kubeconfig); os.IsNotExist(err) {
// No kubeconfig file, try in-cluster config
if inClusterConfig, err := rest.InClusterConfig(); err == nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/k8s/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ func (m *MultiClusterClient) ResolveDependencies(
// For Secrets: strip service-account-token type secrets (auto-generated, cluster-specific)
if dep.Kind == DepSecret {
secretType, _, _ := unstructured.NestedString(obj.Object, "type")
if secretType == "kubernetes.io/service-account-token" {
if secretType == "kubernetes.io/service-account-token" { // #nosec G101 -- this is a Kubernetes secret type identifier.
bundle.Warnings = append(bundle.Warnings,
fmt.Sprintf("Secret %s is a service-account-token (auto-generated, skipping)", dep.Name))
continue
Expand Down
5 changes: 4 additions & 1 deletion pkg/mcp/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ type ContentItem struct {

// NewClient creates a new MCP client for the given binary
func NewClient(name, binaryPath string, args ...string) (*Client, error) {
// #nosec G204 -- binaryPath is a trusted executable path from configuration.
cmd := exec.Command(binaryPath, args...)

stdin, err := cmd.StdinPipe()
Expand Down Expand Up @@ -230,7 +231,9 @@ func (c *Client) initialize(ctx context.Context) error {
}

// Send initialized notification
c.notify("notifications/initialized", nil)
if err := c.notify("notifications/initialized", nil); err != nil {
return fmt.Errorf("failed to send initialized notification: %w", err)
}

return nil
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/notifications/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type EmailNotifier struct {
SMTPHost string
SMTPPort int
Username string
Password string
Password string // #nosec G117 -- configuration struct stores a password value.
From string
To []string
UseTLS bool
Expand Down
4 changes: 3 additions & 1 deletion pkg/settings/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const (
// If the file doesn't exist, it generates 32 random bytes and writes them hex-encoded.
// Returns the raw 32-byte key.
func ensureKeyFile(path string) ([]byte, error) {
// #nosec G304 -- path is provided by configuration and expected to be trusted.
data, err := os.ReadFile(path)
if err == nil {
// Key file exists — decode hex
Expand Down Expand Up @@ -71,7 +72,8 @@ func encrypt(key []byte, plaintext []byte) (*EncryptedField, error) {
return nil, fmt.Errorf("failed to generate nonce: %w", err)
}

// Seal appends the ciphertext + GCM auth tag
// Seal appends the ciphertext + GCM auth tag.
// #nosec G407 -- nonce is generated randomly per encryption call.
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)

return &EncryptedField{
Expand Down
2 changes: 1 addition & 1 deletion pkg/settings/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ type AllSettings struct {

// APIKeyEntry holds a provider's API key and optional model override
type APIKeyEntry struct {
APIKey string `json:"apiKey"`
APIKey string `json:"apiKey"` // #nosec G117 -- API keys are stored in settings payloads.
Model string `json:"model,omitempty"`
}

Expand Down
Loading