diff --git a/.github/workflows/reusable-lint.yml b/.github/workflows/reusable-lint.yml index 8e5abc06..063f817a 100644 --- a/.github/workflows/reusable-lint.yml +++ b/.github/workflows/reusable-lint.yml @@ -39,5 +39,5 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: - version: v2.10.1 + version: v2.12.2 args: --timeout=5m diff --git a/.golangci.yml b/.golangci.yml index d2aa7dcd..bab02bc8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -13,7 +13,18 @@ linters: - gosec - goconst - misspell + exclusions: + rules: + # The test/ directory holds standalone integration-test scaffolding + # (e.g. mock-server.go, package main). goconst's ignore-tests only + # matches *_test.go files, so exempt the whole test/ tree explicitly — + # constant-extraction adds no value to throwaway test harnesses. + - path: ^test/ + linters: + - goconst settings: + goconst: + ignore-tests: true gosec: excludes: - G705 # XSS via taint analysis - not applicable to CLI stderr output diff --git a/internal/api/client_test.go b/internal/api/client_test.go index b7774136..55a05dbc 100644 --- a/internal/api/client_test.go +++ b/internal/api/client_test.go @@ -263,7 +263,7 @@ func TestClient_StartIngest(t *testing.T) { server := testutil.NewTestServer(t, func(w http.ResponseWriter, r *http.Request) { // Attempt to parse the multipart form - this reads the body // The reader error will cause this to fail, but we still send a response - _ = r.ParseMultipartForm(32 << 20) + _ = r.ParseMultipartForm(32 << 20) //nolint:gosec // G120: test server with bounded 32MB limit testutil.JSONResponse(t, w, http.StatusOK, model.IngestUploadResponse{ScanID: testScanID}) }) @@ -302,7 +302,7 @@ func TestClient_StartIngest(t *testing.T) { t.Run("sends SBOM and VEX flags when set", func(t *testing.T) { server := testutil.NewTestServer(t, func(w http.ResponseWriter, r *http.Request) { - if err := r.ParseMultipartForm(32 << 20); err != nil { + if err := r.ParseMultipartForm(32 << 20); err != nil { //nolint:gosec // G120: test server with bounded 32MB limit t.Fatalf("Failed to parse multipart form: %v", err) } diff --git a/internal/auth/client.go b/internal/auth/client.go index 430cf2ba..77248d90 100644 --- a/internal/auth/client.go +++ b/internal/auth/client.go @@ -105,8 +105,8 @@ func (c *AuthClient) Authenticate(ctx context.Context, clientID, clientSecret st Region: regionHint, } - // armis:ignore cwe:770 reason:credentials are bounded by caller input (CLI flags/env); request is a single fixed-structure JSON with context timeout - jsonBody, err := json.Marshal(reqBody) + // armis:ignore cwe:522 cwe:770 reason:marshaling credentials is intentional for the auth token endpoint; sent over HTTPS; bounded by caller input + jsonBody, err := json.Marshal(reqBody) //nolint:gosec // G117: ClientSecret is a credential field; marshaling is intentional for the auth token request if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) } diff --git a/internal/install/claude.go b/internal/install/claude.go index ad87fcb3..321d4030 100644 --- a/internal/install/claude.go +++ b/internal/install/claude.go @@ -140,7 +140,7 @@ func (ci *ClaudeInstaller) registerMarketplace(pluginDir string) error { func (ci *ClaudeInstaller) registerPlugin(pluginDir string) error { instFile := filepath.Join(ci.claudeDir, "plugins", "installed_plugins.json") - data := map[string]interface{}{"version": 2, "plugins": map[string]interface{}{}} + data := map[string]interface{}{jsonKeyVersion: 2, "plugins": map[string]interface{}{}} // armis:ignore cwe:770 reason:reads bounded JSON config file from user's ~/.claude dir; not unbounded input if b, err := os.ReadFile(filepath.Clean(instFile)); err == nil { _ = json.Unmarshal(b, &data) diff --git a/internal/install/editors.go b/internal/install/editors.go index d7248096..4f85d046 100644 --- a/internal/install/editors.go +++ b/internal/install/editors.go @@ -9,7 +9,23 @@ import ( "runtime" ) -const mcpServerName = "armis-appsec" +const ( + mcpServerName = "armis-appsec" + maxEditorConfigSize = 10 << 20 // 10 MB — matches the settings file limit in hooks.go +) + +// JSON key constants shared across MCP/hook config builders. +const ( + jsonKeyType = "type" + jsonKeyCommand = "command" + jsonKeyArgs = "args" + jsonKeyMatcher = "matcher" + jsonKeyHooks = "hooks" + jsonKeyTimeout = "timeout" + jsonKeyVersion = "version" + + jsonTypeCommand = "command" +) // EditorID identifies a supported editor. type EditorID string @@ -302,10 +318,10 @@ func registerVSCodeFormat(pluginDir, configFile string) error { servers = make(map[string]interface{}) } servers[mcpServerName] = map[string]interface{}{ - "type": "stdio", - "command": venvPython(pluginDir), - "args": []string{filepath.Join(pluginDir, "server.py")}, - "envFile": filepath.Join(pluginDir, ".env"), + jsonKeyType: "stdio", + jsonKeyCommand: venvPython(pluginDir), + jsonKeyArgs: []string{filepath.Join(pluginDir, "server.py")}, + "envFile": filepath.Join(pluginDir, ".env"), } data["servers"] = servers @@ -321,9 +337,9 @@ func registerZedFormat(pluginDir, configFile string) error { servers = make(map[string]interface{}) } servers[mcpServerName] = map[string]interface{}{ - "command": map[string]interface{}{ - "path": venvPython(pluginDir), - "args": []string{filepath.Join(pluginDir, "server.py")}, + jsonKeyCommand: map[string]interface{}{ + "path": venvPython(pluginDir), + jsonKeyArgs: []string{filepath.Join(pluginDir, "server.py")}, }, "settings": map[string]interface{}{}, } @@ -334,16 +350,23 @@ func registerZedFormat(pluginDir, configFile string) error { func stdServerEntry(pluginDir string) map[string]interface{} { return map[string]interface{}{ - "command": venvPython(pluginDir), - "args": []string{filepath.Join(pluginDir, "server.py")}, + jsonKeyCommand: venvPython(pluginDir), + jsonKeyArgs: []string{filepath.Join(pluginDir, "server.py")}, } } func readJSONFileAsMap(path string) map[string]interface{} { data := make(map[string]interface{}) + clean := filepath.Clean(path) // armis:ignore cwe:22 reason:path from filepath.Join with known base dirs; filepath.Clean applied - // armis:ignore cwe:253 reason:ReadFile error handled by err == nil guard; non-critical config read - if b, err := os.ReadFile(filepath.Clean(path)); err == nil { + // Reject non-regular files (devices, FIFOs): they can report Size()==0 yet + // stream unbounded data into os.ReadFile, defeating the size cap (CWE-770). + if info, err := os.Stat(clean); err != nil || !info.Mode().IsRegular() || info.Size() > maxEditorConfigSize { + return data + } + // armis:ignore cwe:22 cwe:253 reason:path from filepath.Join with known base dirs; filepath.Clean applied; ReadFile error handled by err == nil guard + if b, err := os.ReadFile(clean); err == nil { //nolint:gosec + // armis:ignore cwe:502 cwe:770 reason:Go encoding/json into map[string]interface{} has no gadget/polymorphic deserialization; input is the user's own local editor config, size-bounded by the maxEditorConfigSize guard above _ = json.Unmarshal(b, &data) } return data diff --git a/internal/install/editors_test.go b/internal/install/editors_test.go index c3ce1fb2..f3dc990a 100644 --- a/internal/install/editors_test.go +++ b/internal/install/editors_test.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "runtime" + "strings" "testing" ) @@ -287,6 +288,56 @@ func TestRegisterPreservesExistingConfig(t *testing.T) { } } +func TestRegisterSkipsOversizedConfig(t *testing.T) { + dir := t.TempDir() + configFile := filepath.Join(dir, "mcp.json") + pluginDir := filepath.Join(dir, "plugin") + + // Write a valid-but-oversized config: a real mcpServers entry plus padding + // that pushes the file past maxEditorConfigSize. readJSONFileAsMap must skip + // reading it, so registration starts fresh and the padded entry is dropped. + padding := strings.Repeat("a", maxEditorConfigSize+1) + existing := map[string]interface{}{ + "mcpServers": map[string]interface{}{ + "other-server": map[string]interface{}{ + "command": "node", + "args": []string{"server.js"}, + }, + }, + "_pad": padding, + } + b, _ := json.Marshal(existing) + if err := os.WriteFile(configFile, b, 0o600); err != nil { + t.Fatalf("seeding config: %v", err) + } + + configPathOverrides = map[EditorID]string{EditorCursor: configFile} + defer func() { configPathOverrides = nil }() + + e, _ := EditorByID(EditorCursor) + if err := e.Register(pluginDir); err != nil { + t.Fatalf("Register() error: %v", err) + } + + // The rewritten config must be valid JSON with armis-appsec registered. + var data map[string]interface{} + out, _ := os.ReadFile(filepath.Clean(configFile)) + if err := json.Unmarshal(out, &data); err != nil { + t.Fatalf("rewritten config is not valid JSON: %v", err) + } + servers, ok := data["mcpServers"].(map[string]interface{}) + if !ok { + t.Fatal("mcpServers key missing after register") + } + if _, ok := servers[mcpServerName]; !ok { + t.Error("armis-appsec not registered") + } + // The oversized file was skipped, so its prior contents were not merged in. + if _, ok := servers["other-server"]; ok { + t.Error("oversized config was read instead of skipped (other-server survived)") + } +} + func TestRegisterJetBrains(t *testing.T) { dir := t.TempDir() configFile := filepath.Join(dir, ".jb-mcp.json") diff --git a/internal/install/hooks.go b/internal/install/hooks.go index 2a976e13..940086ff 100644 --- a/internal/install/hooks.go +++ b/internal/install/hooks.go @@ -30,10 +30,15 @@ func InstallHooks() error { func installHooksToFile(settingsPath string) error { settings := make(map[string]interface{}) - if info, err := os.Stat(settingsPath); err == nil && info.Size() > maxSettingsSize { - return fmt.Errorf("settings file too large (%d bytes): %s", info.Size(), settingsPath) + if info, err := os.Stat(settingsPath); err == nil { + if !info.Mode().IsRegular() { + return fmt.Errorf("settings file is not a regular file: %s", settingsPath) + } + if info.Size() > maxSettingsSize { + return fmt.Errorf("settings file too large (%d bytes): %s", info.Size(), settingsPath) + } } - // armis:ignore cwe:59 reason:settingsPath from filepath.Join(UserHomeDir, hardcoded ".claude/settings.json"); no user input + // armis:ignore cwe:59 cwe:770 reason:settingsPath from filepath.Join(UserHomeDir, hardcoded ".claude/settings.json"); regular-file + size bounded by guards above data, err := os.ReadFile(settingsPath) //nolint:gosec // G304: path constructed from UserHomeDir + hardcoded segments if err != nil && !os.IsNotExist(err) { return fmt.Errorf("reading settings: %w", err) @@ -47,7 +52,7 @@ func installHooksToFile(settingsPath string) error { } } - hooks, _ := settings["hooks"].(map[string]interface{}) + hooks, _ := settings[jsonKeyHooks].(map[string]interface{}) if hooks == nil { hooks = make(map[string]interface{}) } @@ -57,11 +62,11 @@ func installHooksToFile(settingsPath string) error { // Check if Armis hook already exists for _, entry := range preToolUse { if m, ok := entry.(map[string]interface{}); ok { - if matcher, _ := m["matcher"].(string); matcher == armisHookMatcher { - if innerHooks, _ := m["hooks"].([]interface{}); len(innerHooks) > 0 { + if matcher, _ := m[jsonKeyMatcher].(string); matcher == armisHookMatcher { + if innerHooks, _ := m[jsonKeyHooks].([]interface{}); len(innerHooks) > 0 { for _, h := range innerHooks { if hm, ok := h.(map[string]interface{}); ok { - if cmd, _ := hm["command"].(string); cmd != "" && isArmisHookCommand(cmd) { + if cmd, _ := hm[jsonKeyCommand].(string); cmd != "" && isArmisHookCommand(cmd) { return nil // already installed } } @@ -72,18 +77,18 @@ func installHooksToFile(settingsPath string) error { } armisHook := map[string]interface{}{ - "matcher": armisHookMatcher, - "hooks": []map[string]interface{}{ + jsonKeyMatcher: armisHookMatcher, + jsonKeyHooks: []map[string]interface{}{ { - "type": "command", - "command": "armis-cli scan repo --format json --no-progress --fail-on CRITICAL . >/dev/null 2>&1", + jsonKeyType: jsonTypeCommand, + jsonKeyCommand: "armis-cli scan repo --format json --no-progress --fail-on CRITICAL . >/dev/null 2>&1", }, }, } preToolUse = append(preToolUse, armisHook) hooks["PreToolUse"] = preToolUse - settings["hooks"] = hooks + settings[jsonKeyHooks] = hooks if err := os.MkdirAll(filepath.Dir(settingsPath), 0o750); err != nil { return fmt.Errorf("creating settings directory: %w", err) @@ -109,10 +114,12 @@ func removeHooksFromFile(settingsPath string) error { return nil } return fmt.Errorf("reading settings: %w", err) + } else if !info.Mode().IsRegular() { + return fmt.Errorf("settings file is not a regular file: %s", settingsPath) } else if info.Size() > maxSettingsSize { return fmt.Errorf("settings file too large (%d bytes): %s", info.Size(), settingsPath) } - // armis:ignore cwe:59 reason:settingsPath from filepath.Join(UserHomeDir, hardcoded ".claude/settings.json"); no user input + // armis:ignore cwe:59 cwe:770 reason:settingsPath from filepath.Join(UserHomeDir, hardcoded ".claude/settings.json"); regular-file + size bounded by guards above data, err := os.ReadFile(settingsPath) //nolint:gosec // G304: path constructed from UserHomeDir + hardcoded segments if err != nil { return fmt.Errorf("reading settings: %w", err) @@ -123,7 +130,7 @@ func removeHooksFromFile(settingsPath string) error { return fmt.Errorf("parsing settings: %w", err) } - hooks, _ := settings["hooks"].(map[string]interface{}) + hooks, _ := settings[jsonKeyHooks].(map[string]interface{}) if hooks == nil { return nil } @@ -152,9 +159,9 @@ func removeHooksFromFile(settingsPath string) error { } if len(hooks) == 0 { - delete(settings, "hooks") + delete(settings, jsonKeyHooks) } else { - settings["hooks"] = hooks + settings[jsonKeyHooks] = hooks } if err := os.MkdirAll(filepath.Dir(settingsPath), 0o750); err != nil { @@ -164,10 +171,10 @@ func removeHooksFromFile(settingsPath string) error { } func isArmisHookEntry(m map[string]interface{}) bool { - innerHooks, _ := m["hooks"].([]interface{}) + innerHooks, _ := m[jsonKeyHooks].([]interface{}) for _, h := range innerHooks { if hm, ok := h.(map[string]interface{}); ok { - if cmd, _ := hm["command"].(string); isArmisHookCommand(cmd) { + if cmd, _ := hm[jsonKeyCommand].(string); isArmisHookCommand(cmd) { return true } } diff --git a/internal/install/native_hooks.go b/internal/install/native_hooks.go index 19fae55e..2aa8ce93 100644 --- a/internal/install/native_hooks.go +++ b/internal/install/native_hooks.go @@ -402,16 +402,16 @@ func buildCursorHooks(pluginDir string) map[string]interface{} { return map[string]interface{}{ "beforeShellExecution": []interface{}{ map[string]interface{}{ - "command": cmd, - "matcher": `git\s+(commit|push)|gh\s+pr\s+create`, - "timeout": 10, + jsonKeyCommand: cmd, + jsonKeyMatcher: `git\s+(commit|push)|gh\s+pr\s+create`, + jsonKeyTimeout: 10, }, }, "preToolUse": []interface{}{ map[string]interface{}{ - "command": cmd, - "matcher": "Write|Edit", - "timeout": 5, + jsonKeyCommand: cmd, + jsonKeyMatcher: "Write|Edit", + jsonKeyTimeout: 5, }, }, } @@ -425,12 +425,12 @@ func buildGeminiHooks(pluginDir string) map[string]interface{} { return map[string]interface{}{ "BeforeTool": []interface{}{ map[string]interface{}{ - "matcher": "shell|bash|run_shell_command|write_file|edit_file|patch_file", - "hooks": []interface{}{ + jsonKeyMatcher: "shell|bash|run_shell_command|write_file|edit_file|patch_file", + jsonKeyHooks: []interface{}{ map[string]interface{}{ - "type": "command", - "command": cmd, - "timeout": 10000, + jsonKeyType: jsonTypeCommand, + jsonKeyCommand: cmd, + jsonKeyTimeout: 10000, }, }, }, @@ -446,22 +446,22 @@ func buildCodexHooks(pluginDir string) map[string]interface{} { return map[string]interface{}{ "PreToolUse": []interface{}{ map[string]interface{}{ - "matcher": "shell", - "hooks": []interface{}{ + jsonKeyMatcher: "shell", + jsonKeyHooks: []interface{}{ map[string]interface{}{ - "type": "command", - "command": cmd, - "timeout": 10, + jsonKeyType: jsonTypeCommand, + jsonKeyCommand: cmd, + jsonKeyTimeout: 10, }, }, }, map[string]interface{}{ - "matcher": "write_file|apply_patch|edit_file", - "hooks": []interface{}{ + jsonKeyMatcher: "write_file|apply_patch|edit_file", + jsonKeyHooks: []interface{}{ map[string]interface{}{ - "type": "command", - "command": cmd, - "timeout": 5, + jsonKeyType: jsonTypeCommand, + jsonKeyCommand: cmd, + jsonKeyTimeout: 5, }, }, }, @@ -477,10 +477,10 @@ func buildCopilotHooks(pluginDir string) map[string]interface{} { return map[string]interface{}{ "preToolUse": []interface{}{ map[string]interface{}{ - "type": "command", - "bash": cmd, - "matcher": "bash|shell|terminal|powershell|create|edit", - "timeoutSec": 10, + jsonKeyType: jsonTypeCommand, + "bash": cmd, + jsonKeyMatcher: "bash|shell|terminal|powershell|create|edit", + "timeoutSec": 10, }, }, } @@ -580,11 +580,12 @@ func removeLegacyFileIfArmisOnly(path string) { } } for key := range data { - if key != "version" && key != "hooks" { + if key != jsonKeyVersion && key != jsonKeyHooks { return } } - _ = os.Remove(filepath.Clean(path)) //nolint:gosec // armis:ignore cwe:22 cwe:73 reason:path from hardcoded OS config dirs + // armis:ignore cwe:22 cwe:73 reason:path from hardcoded OS config dirs; only called after verifying file contains only armis-generated content + _ = os.Remove(filepath.Clean(path)) //nolint:gosec } // posixQuote wraps a string in POSIX-safe single quotes, escaping embedded single quotes. diff --git a/internal/output/human.go b/internal/output/human.go index 6db0015b..0dc62ef7 100644 --- a/internal/output/human.go +++ b/internal/output/human.go @@ -23,7 +23,14 @@ import ( ) const ( + // noneValue is the neutral "none" literal shared across unrelated domains + // (the group-by option and the SARIF "none" level). Domain-specific + // constants derive from it so call sites reference the name that matches + // their own semantics rather than borrowing another domain's. + noneValue = "none" + groupBySeverity = "severity" + groupByNone = noneValue noCWELabel = "No CWE" // Resource limits for snippet loading to prevent memory exhaustion (CWE-770) @@ -284,7 +291,7 @@ func (iw *indentWriter) Write(p []byte) (int, error) { // Format formats the scan result in human-readable format with default options. func (f *HumanFormatter) Format(result *model.ScanResult, w io.Writer) error { - return f.FormatWithOptions(result, w, FormatOptions{GroupBy: "none"}) + return f.FormatWithOptions(result, w, FormatOptions{GroupBy: groupByNone}) } // FormatWithOptions formats the scan result in human-readable format with custom options. @@ -340,7 +347,7 @@ func (f *HumanFormatter) FormatWithOptions(result *model.ScanResult, w io.Writer ew.write("%s\n", sectionStyle.Render("FINDINGS")) // 5. Individual findings - if opts.GroupBy != "" && opts.GroupBy != "none" { + if opts.GroupBy != "" && opts.GroupBy != groupByNone { groups := groupFindings(displayFindings, opts.GroupBy) renderGroupedFindings(w, groups, opts) } else { diff --git a/internal/output/sarif.go b/internal/output/sarif.go index 32753c71..9f604a47 100644 --- a/internal/output/sarif.go +++ b/internal/output/sarif.go @@ -454,7 +454,8 @@ func severityToSarifLevel(severity model.Severity) string { case model.SeverityLow, model.SeverityInfo: return "note" default: - return "none" + // SARIF "none" level — neutral literal, not a group-by option. + return noneValue } } diff --git a/internal/output/styles.go b/internal/output/styles.go index 5b1946cf..2e5dfefb 100644 --- a/internal/output/styles.go +++ b/internal/output/styles.go @@ -13,33 +13,41 @@ import ( "golang.org/x/term" ) +// Tailwind CSS color hex values referenced by multiple palette entries. +const ( + colorRed600 = "#DC2626" + colorOrange600 = "#EA580C" + colorBlue600 = "#2563EB" + colorGray500 = "#6B7280" +) + // Color palette - using Tailwind CSS color system for consistency // AdaptiveColor automatically selects Light/Dark variant based on terminal background var ( // Severity colors (background) - high saturation works on both themes - colorCriticalBg = lipgloss.AdaptiveColor{Light: "#DC2626", Dark: "#DC2626"} // red-600 - colorHighBg = lipgloss.AdaptiveColor{Light: "#EA580C", Dark: "#EA580C"} // orange-600 - colorMediumBg = lipgloss.AdaptiveColor{Light: "#CA8A04", Dark: "#CA8A04"} // yellow-600 - colorLowBg = lipgloss.AdaptiveColor{Light: "#2563EB", Dark: "#2563EB"} // blue-600 - colorInfoBg = lipgloss.AdaptiveColor{Light: "#6B7280", Dark: "#6B7280"} // gray-500 + colorCriticalBg = lipgloss.AdaptiveColor{Light: colorRed600, Dark: colorRed600} // red-600 + colorHighBg = lipgloss.AdaptiveColor{Light: colorOrange600, Dark: colorOrange600} // orange-600 + colorMediumBg = lipgloss.AdaptiveColor{Light: "#CA8A04", Dark: "#CA8A04"} // yellow-600 + colorLowBg = lipgloss.AdaptiveColor{Light: colorBlue600, Dark: colorBlue600} // blue-600 + colorInfoBg = lipgloss.AdaptiveColor{Light: colorGray500, Dark: colorGray500} // gray-500 // Severity colors (foreground for text) - darker on light bg for contrast - colorCriticalFg = lipgloss.AdaptiveColor{Light: "#DC2626", Dark: "#EF4444"} // red-600 / red-500 - colorHighFg = lipgloss.AdaptiveColor{Light: "#EA580C", Dark: "#F97316"} // orange-600 / orange-500 - colorMediumFg = lipgloss.AdaptiveColor{Light: "#A16207", Dark: "#EAB308"} // yellow-700 / yellow-500 - colorLowFg = lipgloss.AdaptiveColor{Light: "#2563EB", Dark: "#3B82F6"} // blue-600 / blue-500 - colorInfoFg = lipgloss.AdaptiveColor{Light: "#6B7280", Dark: "#9CA3AF"} // gray-500 / gray-400 + colorCriticalFg = lipgloss.AdaptiveColor{Light: colorRed600, Dark: "#EF4444"} // red-600 / red-500 + colorHighFg = lipgloss.AdaptiveColor{Light: colorOrange600, Dark: "#F97316"} // orange-600 / orange-500 + colorMediumFg = lipgloss.AdaptiveColor{Light: "#A16207", Dark: "#EAB308"} // yellow-700 / yellow-500 + colorLowFg = lipgloss.AdaptiveColor{Light: colorBlue600, Dark: "#3B82F6"} // blue-600 / blue-500 + colorInfoFg = lipgloss.AdaptiveColor{Light: colorGray500, Dark: "#9CA3AF"} // gray-500 / gray-400 // Semantic colors - darker on light bg for contrast - colorSuccess = lipgloss.AdaptiveColor{Light: "#16A34A", Dark: "#22C55E"} // green-600 / green-500 - colorWarning = lipgloss.AdaptiveColor{Light: "#D97706", Dark: "#F59E0B"} // amber-600 / amber-500 - colorMuted = lipgloss.AdaptiveColor{Light: "#4B5563", Dark: "#6B7280"} // gray-600 / gray-500 - colorAccent = lipgloss.AdaptiveColor{Light: "#7c3aed", Dark: "#7c3aed"} // purple-600 (Armis brand) + colorSuccess = lipgloss.AdaptiveColor{Light: "#16A34A", Dark: "#22C55E"} // green-600 / green-500 + colorWarning = lipgloss.AdaptiveColor{Light: "#D97706", Dark: "#F59E0B"} // amber-600 / amber-500 + colorMuted = lipgloss.AdaptiveColor{Light: "#4B5563", Dark: colorGray500} // gray-600 / gray-500 + colorAccent = lipgloss.AdaptiveColor{Light: "#7c3aed", Dark: "#7c3aed"} // purple-600 (Armis brand) // Diff colors - darker on light bg for contrast - colorDiffAdd = lipgloss.AdaptiveColor{Light: "#16A34A", Dark: "#22C55E"} // green-600 / green-500 - colorDiffRemove = lipgloss.AdaptiveColor{Light: "#DC2626", Dark: "#EF4444"} // red-600 / red-500 - colorDiffHunk = lipgloss.AdaptiveColor{Light: "#6B7280", Dark: "#6B7280"} // gray-500 + colorDiffAdd = lipgloss.AdaptiveColor{Light: "#16A34A", Dark: "#22C55E"} // green-600 / green-500 + colorDiffRemove = lipgloss.AdaptiveColor{Light: colorRed600, Dark: "#EF4444"} // red-600 / red-500 + colorDiffHunk = lipgloss.AdaptiveColor{Light: colorGray500, Dark: colorGray500} // gray-500 // Diff background colors - light tints on light bg, dark tints on dark bg colorDiffAddBg = lipgloss.AdaptiveColor{Light: "#dcfce7", Dark: "#0f2918"} // green-50 / dark green @@ -49,8 +57,8 @@ var ( colorVulnBg = lipgloss.AdaptiveColor{Light: "#fef3c7", Dark: "#422006"} // amber-100 / amber-950 // UI colors - inverted for light/dark themes - colorBorder = lipgloss.AdaptiveColor{Light: "#D1D5DB", Dark: "#374151"} // gray-300 / gray-700 - colorDim = lipgloss.AdaptiveColor{Light: "#4B5563", Dark: "#6B7280"} // gray-600 / gray-500 + colorBorder = lipgloss.AdaptiveColor{Light: "#D1D5DB", Dark: "#374151"} // gray-300 / gray-700 + colorDim = lipgloss.AdaptiveColor{Light: "#4B5563", Dark: colorGray500} // gray-600 / gray-500 // Bright text color - dark on light bg, white on dark bg colorBright = lipgloss.AdaptiveColor{Light: "#1F2937", Dark: "#FFFFFF"} // gray-800 / white diff --git a/internal/scan/repo/inline.go b/internal/scan/repo/inline.go index 1bb150fe..6433bb72 100644 --- a/internal/scan/repo/inline.go +++ b/internal/scan/repo/inline.go @@ -20,30 +20,47 @@ const ( suppressionTypeRule = "rule" ) +// File extension constants referenced in multiple maps below. +const ( + extPy = ".py" + extRb = ".rb" + extJs = ".js" + extTS = ".ts" + extJsx = ".jsx" + extTsx = ".tsx" + extKt = ".kt" + extScala = ".scala" + + commentHTML = "