diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe41c0f..c951341 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,9 +9,6 @@ on: permissions: {} # No default permissions -env: - GO_VERSION: '1.23.4' - jobs: test: runs-on: ubuntu-latest @@ -26,7 +23,7 @@ jobs: - name: Setup Go uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: - go-version: ${{ env.GO_VERSION }} + go-version: 'stable' cache: false - name: Test diff --git a/.golangci.yml b/.golangci.yml index 6a2d4ea..b455b96 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -152,10 +152,10 @@ linters: nakedret: # Default: 30 - max-func-lines: 4 + max-func-lines: 7 nestif: - min-complexity: 12 + min-complexity: 15 nolintlint: # Exclude following linters from requiring an explanation. @@ -171,17 +171,11 @@ linters: rules: - name: add-constant severity: warning - disabled: false - exclude: [""] - arguments: - - max-lit-count: "5" - allow-strs: '"","\n"' - allow-ints: "0,1,2,3,24,30,60,100,365,0o600,0o700,0o750,0o755" - allow-floats: "0.0,0.,1.0,1.,2.0,2." + disabled: true - name: cognitive-complexity - arguments: [55] + disabled: true # prefer maintidx - name: cyclomatic - arguments: [60] + disabled: true # prefer maintidx - name: function-length arguments: [150, 225] - name: line-length-limit @@ -192,6 +186,8 @@ linters: arguments: [10] - name: flag-parameter # fixes are difficult disabled: true + - name: bare-return + disabled: true rowserrcheck: # database/sql is always checked. @@ -213,8 +209,14 @@ linters: os-temp-dir: true varnamelen: - max-distance: 40 + max-distance: 75 min-name-length: 2 + check-receivers: false + ignore-names: + - r + - w + - f + - err exclusions: # Default: [] diff --git a/.yamllint b/.yamllint index 5893d89..9a08ad1 100644 --- a/.yamllint +++ b/.yamllint @@ -11,6 +11,6 @@ rules: document-start: disable line-length: level: warning - max: 200 + max: 160 allow-non-breakable-inline-mappings: true truthy: disable diff --git a/Makefile b/Makefile index 0c89efc..fb03d91 100644 --- a/Makefile +++ b/Makefile @@ -95,56 +95,117 @@ help: @echo " make ko-build - Build server container with ko" @echo " make ko-publish - Publish server container with ko" -# BEGIN: lint-install - POSIX-compliant version -# Works with both BSD make and GNU make +# BEGIN: lint-install . +# http://github.com/codeGROOVE-dev/lint-install .PHONY: lint lint: _lint -# Use simple assignment for maximum compatibility -LINT_ARCH != uname -m || echo x86_64 -LINT_OS != uname || echo Darwin -LINT_ROOT = . +LINT_ARCH := $(shell uname -m) +LINT_OS := $(shell uname) +LINT_OS_LOWER := $(shell echo $(LINT_OS) | tr '[:upper:]' '[:lower:]') +LINT_ROOT := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) -LINTERS = -FIXERS = +# shellcheck and hadolint lack arm64 native binaries: rely on x86-64 emulation +ifeq ($(LINT_OS),Darwin) + ifeq ($(LINT_ARCH),arm64) + LINT_ARCH=x86_64 + endif +endif -GOLANGCI_LINT_CONFIG = $(LINT_ROOT)/.golangci.yml -GOLANGCI_LINT_VERSION = v2.3.1 -GOLANGCI_LINT_BIN = $(LINT_ROOT)/out/linters/golangci-lint-$(GOLANGCI_LINT_VERSION)-$(LINT_ARCH) +LINTERS := +FIXERS := +SHELLCHECK_VERSION ?= v0.11.0 +SHELLCHECK_BIN := $(LINT_ROOT)/out/linters/shellcheck-$(SHELLCHECK_VERSION)-$(LINT_ARCH) +$(SHELLCHECK_BIN): + mkdir -p $(LINT_ROOT)/out/linters + curl -sSfL -o $@.tar.xz https://github.com/koalaman/shellcheck/releases/download/$(SHELLCHECK_VERSION)/shellcheck-$(SHELLCHECK_VERSION).$(LINT_OS_LOWER).$(LINT_ARCH).tar.xz \ + || echo "Unable to fetch shellcheck for $(LINT_OS)/$(LINT_ARCH): falling back to locally install" + test -f $@.tar.xz \ + && tar -C $(LINT_ROOT)/out/linters -xJf $@.tar.xz \ + && mv $(LINT_ROOT)/out/linters/shellcheck-$(SHELLCHECK_VERSION)/shellcheck $@ \ + || printf "#!/usr/bin/env shellcheck\n" > $@ + chmod u+x $@ + +LINTERS += shellcheck-lint +shellcheck-lint: $(SHELLCHECK_BIN) + $(SHELLCHECK_BIN) $(shell find . -name "*.sh") + +FIXERS += shellcheck-fix +shellcheck-fix: $(SHELLCHECK_BIN) + $(SHELLCHECK_BIN) $(shell find . -name "*.sh") -f diff | { read -t 1 line || exit 0; { echo "$$line" && cat; } | git apply -p2; } + +GOLANGCI_LINT_CONFIG := $(LINT_ROOT)/.golangci.yml +GOLANGCI_LINT_VERSION ?= v2.7.2 +GOLANGCI_LINT_BIN := $(LINT_ROOT)/out/linters/golangci-lint-$(GOLANGCI_LINT_VERSION)-$(LINT_ARCH) $(GOLANGCI_LINT_BIN): mkdir -p $(LINT_ROOT)/out/linters rm -rf $(LINT_ROOT)/out/linters/golangci-lint-* curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(LINT_ROOT)/out/linters $(GOLANGCI_LINT_VERSION) - mv $(LINT_ROOT)/out/linters/golangci-lint $(GOLANGCI_LINT_BIN) + mv $(LINT_ROOT)/out/linters/golangci-lint $@ LINTERS += golangci-lint-lint golangci-lint-lint: $(GOLANGCI_LINT_BIN) - find . -name go.mod -execdir $(GOLANGCI_LINT_BIN) run -c $(GOLANGCI_LINT_CONFIG) \; + find . -name go.mod -execdir "$(GOLANGCI_LINT_BIN)" run -c "$(GOLANGCI_LINT_CONFIG)" \; FIXERS += golangci-lint-fix golangci-lint-fix: $(GOLANGCI_LINT_BIN) - find . -name go.mod -execdir $(GOLANGCI_LINT_BIN) run -c $(GOLANGCI_LINT_CONFIG) --fix \; - -YAMLLINT_VERSION = 1.37.1 -YAMLLINT_ROOT = $(LINT_ROOT)/out/linters/yamllint-$(YAMLLINT_VERSION) -YAMLLINT_BIN = $(YAMLLINT_ROOT)/dist/bin/yamllint + find . -name go.mod -execdir "$(GOLANGCI_LINT_BIN)" run -c "$(GOLANGCI_LINT_CONFIG)" --fix \; +YAMLLINT_VERSION ?= 1.37.1 +YAMLLINT_ROOT := $(LINT_ROOT)/out/linters/yamllint-$(YAMLLINT_VERSION) +YAMLLINT_BIN := $(YAMLLINT_ROOT)/dist/bin/yamllint $(YAMLLINT_BIN): mkdir -p $(LINT_ROOT)/out/linters rm -rf $(LINT_ROOT)/out/linters/yamllint-* curl -sSfL https://github.com/adrienverge/yamllint/archive/refs/tags/v$(YAMLLINT_VERSION).tar.gz | tar -C $(LINT_ROOT)/out/linters -zxf - - cd $(YAMLLINT_ROOT) && (pip3 install --target dist . || pip install --target dist .) + cd $(YAMLLINT_ROOT) && pip3 install --target dist . || pip install --target dist . LINTERS += yamllint-lint yamllint-lint: $(YAMLLINT_BIN) - PYTHONPATH=$(YAMLLINT_ROOT)/dist $(YAMLLINT_BIN) . + PYTHONPATH=$(YAMLLINT_ROOT)/dist $(YAMLLINT_ROOT)/dist/bin/yamllint . + +BIOME_VERSION ?= 2.3.8 +BIOME_BIN := $(LINT_ROOT)/out/linters/biome-$(BIOME_VERSION)-$(LINT_ARCH) +BIOME_CONFIG := $(LINT_ROOT)/biome.json + +# Map architecture names for Biome downloads +BIOME_ARCH := $(LINT_ARCH) +ifeq ($(LINT_ARCH),x86_64) + BIOME_ARCH := x64 +endif + +$(BIOME_BIN): + mkdir -p $(LINT_ROOT)/out/linters + rm -rf $(LINT_ROOT)/out/linters/biome-* + curl -sSfL -o $@ https://github.com/biomejs/biome/releases/download/%40biomejs%2Fbiome%40$(BIOME_VERSION)/biome-$(LINT_OS_LOWER)-$(BIOME_ARCH) \ + || echo "Unable to fetch biome for $(LINT_OS_LOWER)/$(BIOME_ARCH), falling back to local install" + test -f $@ || printf "#!/usr/bin/env biome\n" > $@ + chmod u+x $@ + +LINTERS += biome-lint +biome-lint: $(BIOME_BIN) + $(BIOME_BIN) check --config-path=$(BIOME_CONFIG) . + +FIXERS += biome-fix +biome-fix: $(BIOME_BIN) + $(BIOME_BIN) check --write --config-path=$(BIOME_CONFIG) . .PHONY: _lint $(LINTERS) -_lint: $(LINTERS) +_lint: + @exit_code=0; \ + for target in $(LINTERS); do \ + $(MAKE) $$target || exit_code=1; \ + done; \ + exit $$exit_code .PHONY: fix $(FIXERS) -fix: $(FIXERS) - -# END: lint-install \ No newline at end of file +fix: + @exit_code=0; \ + for target in $(FIXERS); do \ + $(MAKE) $$target || exit_code=1; \ + done; \ + exit $$exit_code + +# END: lint-install . diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..b0107cc --- /dev/null +++ b/biome.json @@ -0,0 +1,129 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "includes": [ + "**", + "!**/node_modules", + "!**/dist", + "!**/build", + "!**/out", + "!**/coverage", + "!**/.next", + "!**/.nuxt", + "!**/*.html" + ] + }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "assist": { "actions": { "source": { "organizeImports": "on" } } }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noExtraBooleanCast": "error", + "noUselessCatch": "error", + "noUselessTypeConstraint": "error", + "noAdjacentSpacesInRegex": "error", + "noArguments": "error" + }, + "correctness": { + "noConstAssign": "error", + "noConstantCondition": "error", + "noEmptyCharacterClassInRegex": "error", + "noEmptyPattern": "error", + "noGlobalObjectCalls": "error", + "noInnerDeclarations": "error", + "noInvalidConstructorSuper": "error", + "noNonoctalDecimalEscape": "error", + "noPrecisionLoss": "error", + "noSelfAssign": "error", + "noSetterReturn": "error", + "noSwitchDeclarations": "error", + "noUndeclaredVariables": "error", + "noUnreachable": "error", + "noUnreachableSuper": "error", + "noUnsafeFinally": "error", + "noUnsafeOptionalChaining": "error", + "noUnusedLabels": "error", + "noUnusedVariables": "error", + "useIsNan": "error", + "useValidForDirection": "error", + "useYield": "error", + "noInvalidBuiltinInstantiation": "error", + "useValidTypeof": "error" + }, + "security": { + "noDangerouslySetInnerHtml": "warn", + "noDangerouslySetInnerHtmlWithChildren": "error" + }, + "style": { + "useConst": "error" + }, + "suspicious": { + "noAssignInExpressions": "error", + "noAsyncPromiseExecutor": "error", + "noCatchAssign": "error", + "noClassAssign": "error", + "noCompareNegZero": "error", + "noControlCharactersInRegex": "error", + "noDebugger": "error", + "noDoubleEquals": "warn", + "noDuplicateCase": "error", + "noDuplicateClassMembers": "error", + "noDuplicateObjectKeys": "error", + "noDuplicateParameters": "error", + "noEmptyBlockStatements": "warn", + "noExplicitAny": "warn", + "noExtraNonNullAssertion": "error", + "noFallthroughSwitchClause": "error", + "noFunctionAssign": "error", + "noGlobalAssign": "error", + "noImportAssign": "error", + "noMisleadingCharacterClass": "error", + "noPrototypeBuiltins": "error", + "noRedeclare": "error", + "noShadowRestrictedNames": "error", + "noUnsafeDeclarationMerging": "error", + "noUnsafeNegation": "error", + "useGetterReturn": "error", + "noWith": "error", + "noVar": "error" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "trailingCommas": "es5", + "semicolons": "always", + "arrowParentheses": "always", + "bracketSpacing": true, + "bracketSameLine": false + } + }, + "json": { + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "parser": { + "allowComments": true, + "allowTrailingCommas": false + } + } +} diff --git a/cmd/agent/executeCheck.go b/cmd/agent/executeCheck.go index ad02e3c..190a7e4 100644 --- a/cmd/agent/executeCheck.go +++ b/cmd/agent/executeCheck.go @@ -5,8 +5,6 @@ import ( "fmt" "log" "os" - "os/exec" - "strings" "time" "github.com/codeGROOVE-dev/gitMDM/internal/analyzer" @@ -81,43 +79,16 @@ func (*Agent) readFile(checkName string, rule config.CommandRule) gitmdm.Command } // checkCommandAvailable verifies that a command is available to execute. +// This is a wrapper around validateCommand that sets the appropriate fields for executeCheck. func checkCommandAvailable(checkName, command string) *gitmdm.CommandOutput { - if containsShellOperators(command) { - return nil // Commands with shell operators need shell interpretation - } - - commandParts := strings.Fields(command) - if len(commandParts) == 0 { - return nil - } - - primaryCmd := commandParts[0] - if isShellBuiltin(primaryCmd) || strings.Contains(primaryCmd, "/") { - return nil // Shell builtins and absolute paths don't need validation - } - - // Temporarily set PATH for LookPath - oldPath := os.Getenv("PATH") - if err := os.Setenv("PATH", securePath()); err != nil { - log.Printf("[WARN] Failed to set PATH for command check: %v", err) - } - _, lookupErr := exec.LookPath(primaryCmd) - if err := os.Setenv("PATH", oldPath); err != nil { - log.Printf("[WARN] Failed to restore PATH: %v", err) - } - - if lookupErr != nil { - if *debugMode { - log.Printf("[DEBUG] Command '%s' not found in PATH for check '%s', skipping", primaryCmd, checkName) - } - return &gitmdm.CommandOutput{ - Command: command, - Skipped: true, - FileMissing: true, // Treat missing command like missing file - Stderr: fmt.Sprintf("Skipped: %s not found", primaryCmd), - } + output := validateCommand(checkName, command) + if output != nil { + // Convert exitCode -2 to Skipped/FileMissing for executeCheck + output.Skipped = true + output.FileMissing = true + output.ExitCode = 0 } - return nil + return output } // executeCommand executes a command and returns its output. diff --git a/cmd/agent/executeCommand.go b/cmd/agent/executeCommand.go index 0e5e59a..16ff03e 100644 --- a/cmd/agent/executeCommand.go +++ b/cmd/agent/executeCommand.go @@ -35,16 +35,11 @@ func securePath() string { case "darwin": // macOS standard paths + ApplicationFirewall for socketfilterfw return "/usr/libexec/ApplicationFirewall:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" - case "linux": - return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" - case "freebsd", "openbsd", "netbsd", "dragonfly": - // BSD systems often have important tools in /usr/local - return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" case "solaris", "illumos": // Solaris/Illumos have additional paths return "/usr/sbin:/usr/bin:/sbin:/bin:/usr/gnu/bin:/opt/local/bin" default: - // Safe default for unknown Unix-like systems + // Standard Unix-like paths (Linux, BSD systems) return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" } } diff --git a/cmd/agent/install.go b/cmd/agent/install.go index 8ac4c13..2016398 100644 --- a/cmd/agent/install.go +++ b/cmd/agent/install.go @@ -473,7 +473,7 @@ func installCron(agentPath, _, _ string) error { log.Printf("[INFO] Cron job for %s already installed, updating entries", agentName) // Remove old entries to replace with new ones var filteredLines []string - for _, line := range strings.Split(currentCron, "\n") { + for line := range strings.SplitSeq(currentCron, "\n") { if !strings.Contains(line, agentName) { filteredLines = append(filteredLines, line) } @@ -487,13 +487,16 @@ func installCron(agentPath, _, _ string) error { fmt.Sprintf("@reboot %s", agentPath), fmt.Sprintf("*/15 * * * * %s", agentPath), // Every 15 minutes } - newCron := currentCron - if !strings.HasSuffix(newCron, "\n") && newCron != "" { - newCron += "\n" + var builder strings.Builder + builder.WriteString(currentCron) + if !strings.HasSuffix(currentCron, "\n") && currentCron != "" { + builder.WriteString("\n") } for _, entry := range entries { - newCron += entry + "\n" + builder.WriteString(entry) + builder.WriteString("\n") } + newCron := builder.String() // Install new crontab log.Printf("[INFO] Installing cron entries for %s", agentPath) @@ -578,7 +581,7 @@ func uninstallCron() error { // Remove gitmdm-agent entries var newLines []string - for _, line := range strings.Split(currentCron, "\n") { + for line := range strings.SplitSeq(currentCron, "\n") { if !strings.Contains(line, "gitmdm-agent") { newLines = append(newLines, line) } diff --git a/cmd/agent/main.go b/cmd/agent/main.go index cad051b..c589286 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -159,10 +159,7 @@ func (a *Agent) verifyServerConnection(ctx context.Context, report gitmdm.Device var lastErr error for attempt := 0; attempt <= maxRetries; attempt++ { if attempt > 0 { - backoff := time.Duration(float64(initialBackoff) * math.Pow(2, float64(attempt-1))) - if backoff > maxBackoff { - backoff = maxBackoff - } + backoff := min(time.Duration(float64(initialBackoff)*math.Pow(2, float64(attempt-1))), maxBackoff) log.Printf("[INFO] Retrying verification (attempt %d/%d) after %v...", attempt, maxRetries, backoff) select { case <-time.After(backoff): @@ -632,15 +629,15 @@ func (a *Agent) processFailedReports(ctx context.Context) { return case <-ticker.C: // Drain all queued reports + drainLoop: for { select { case report := <-a.failedReports: a.retryFailedReport(ctx, report) default: - goto drained + break drainLoop } } - drained: } } } @@ -773,19 +770,19 @@ func (a *Agent) runSingleCheck(checkName string) string { return fmt.Sprintf("Check '%s' not available for %s", checkName, osName) } - var outputBuilder strings.Builder + var buf strings.Builder var outputs []gitmdm.CommandOutput for i, rule := range rules { if i > 0 { - outputBuilder.WriteString("\n\n=== Rule " + strconv.Itoa(i+1) + " ===\n") + buf.WriteString("\n\n=== Rule " + strconv.Itoa(i+1) + " ===\n") } // Display what we're checking if rule.File != "" { - outputBuilder.WriteString("File: " + rule.File + "\n") + buf.WriteString("File: " + rule.File + "\n") } else if rule.Output != "" { - outputBuilder.WriteString("Command: " + rule.Output + "\n") + buf.WriteString("Command: " + rule.Output + "\n") } output := a.executeCheck(ctx, checkName, rule) @@ -793,50 +790,50 @@ func (a *Agent) runSingleCheck(checkName string) string { switch { case output.FileMissing: - outputBuilder.WriteString("File not found\n") + buf.WriteString("File not found\n") case output.Skipped: - outputBuilder.WriteString("Command not available\n") + buf.WriteString("Command not available\n") default: if output.Stdout != "" { - outputBuilder.WriteString(output.Stdout) + buf.WriteString(output.Stdout) } if output.Stderr != "" { - outputBuilder.WriteString("\n--- STDERR ---\n" + output.Stderr) + buf.WriteString("\n--- STDERR ---\n" + output.Stderr) } if output.ExitCode != 0 { - outputBuilder.WriteString(fmt.Sprintf("\n--- EXIT CODE: %d ---", output.ExitCode)) + buf.WriteString(fmt.Sprintf("\n--- EXIT CODE: %d ---", output.ExitCode)) } } // Show analysis for this specific rule if output.Failed { - outputBuilder.WriteString(fmt.Sprintf("\n--- FAILED: %s ---", output.FailReason)) + buf.WriteString(fmt.Sprintf("\n--- FAILED: %s ---", output.FailReason)) } else if !output.Skipped && !output.FileMissing { - outputBuilder.WriteString("\n--- PASSED ---") + buf.WriteString("\n--- PASSED ---") } } // Overall status analysis status, reason, remediation := analyzer.DetermineOverallStatus(outputs) - outputBuilder.WriteString("\n\n=== OVERALL RESULT ===") + buf.WriteString("\n\n=== OVERALL RESULT ===") switch status { case statusPass: - outputBuilder.WriteString(fmt.Sprintf("\n✅ PASS: %s", reason)) + buf.WriteString(fmt.Sprintf("\n✅ PASS: %s", reason)) case statusFail: - outputBuilder.WriteString(fmt.Sprintf("\n❌ FAIL: %s", reason)) + buf.WriteString(fmt.Sprintf("\n❌ FAIL: %s", reason)) // Show command-specific remediation steps for failed checks if len(remediation) > 0 { - outputBuilder.WriteString("\n\n=== HOW TO FIX ===") + buf.WriteString("\n\n=== HOW TO FIX ===") for i, step := range remediation { - outputBuilder.WriteString(fmt.Sprintf("\n%d. %s", i+1, step)) + buf.WriteString(fmt.Sprintf("\n%d. %s", i+1, step)) } } default: - outputBuilder.WriteString(fmt.Sprintf("\n➖ NOT APPLICABLE: %s", reason)) + buf.WriteString(fmt.Sprintf("\n➖ NOT APPLICABLE: %s", reason)) } - return outputBuilder.String() + return buf.String() } func (*Agent) systemUptime(ctx context.Context) string { @@ -845,9 +842,7 @@ func (*Agent) systemUptime(ctx context.Context) string { var cmd *exec.Cmd switch runtime.GOOS { - case "linux", "darwin", "freebsd", "openbsd", "netbsd", "dragonfly": - cmd = exec.CommandContext(ctx, "uptime") - case "solaris", "illumos": + case "linux", "darwin", "freebsd", "openbsd", "netbsd", "dragonfly", "solaris", "illumos": cmd = exec.CommandContext(ctx, "uptime") case osWindows: cmd = exec.CommandContext(ctx, wmicCmd, "os", wmicGetArg, "lastbootuptime") @@ -948,10 +943,8 @@ func (*Agent) osInfo(ctx context.Context) string { case osLinux: // Try to get pretty name from os-release if data, err := os.ReadFile("/etc/os-release"); err == nil { - lines := strings.Split(string(data), "\n") - for _, line := range lines { - if strings.HasPrefix(line, "PRETTY_NAME=") { - name := strings.TrimPrefix(line, "PRETTY_NAME=") + for line := range strings.SplitSeq(string(data), "\n") { + if name, ok := strings.CutPrefix(line, "PRETTY_NAME="); ok { return strings.Trim(name, `"`) } } @@ -985,13 +978,12 @@ func (*Agent) osVersion(ctx context.Context) string { defer cancel() var cmd *exec.Cmd switch runtime.GOOS { - case osLinux: - cmd = exec.CommandContext(ctx, "uname", "-r") case osDarwin: cmd = exec.CommandContext(ctx, "sw_vers", "-productVersion") case osWindows: cmd = exec.CommandContext(ctx, wmicCmd, "os", wmicGetArg, "Version", "/value") default: + // Linux and other Unix-like systems cmd = exec.CommandContext(ctx, "uname", "-r") } if output, err := cmd.Output(); err == nil { @@ -1016,8 +1008,7 @@ func darwinHardwareID(ctx context.Context) string { return "" } - lines := strings.Split(string(output), "\n") - for _, line := range lines { + for line := range strings.SplitSeq(string(output), "\n") { if strings.Contains(line, "IOPlatformUUID") { parts := strings.Split(line, "\"") if len(parts) >= minUUIDParts { @@ -1100,10 +1091,8 @@ func illumosHardwareID(ctx context.Context) string { cmd := exec.CommandContext(ctx, "sysinfo", "-p") output, err := cmd.Output() if err == nil { - lines := strings.Split(string(output), "\n") - for _, line := range lines { - if strings.HasPrefix(line, "UUID=") { - id := strings.TrimPrefix(line, "UUID=") + for line := range strings.SplitSeq(string(output), "\n") { + if id, ok := strings.CutPrefix(line, "UUID="); ok { id = strings.TrimSpace(id) if *debugMode { log.Printf("[DEBUG] Found Illumos UUID: %s", id) @@ -1128,8 +1117,7 @@ func windowsHardwareID(ctx context.Context) string { return "" } - lines := strings.Split(string(output), "\n") - for _, line := range lines { + for line := range strings.SplitSeq(string(output), "\n") { trimmed := strings.TrimSpace(line) if trimmed != "" && trimmed != "UUID" { if *debugMode { diff --git a/cmd/agent/verify_embedded.go b/cmd/agent/verify_embedded.go index 140f96e..11dcb18 100644 --- a/cmd/agent/verify_embedded.go +++ b/cmd/agent/verify_embedded.go @@ -49,9 +49,7 @@ func verifyEmbeddedConfig() error { signerEmail, err := verifySignatureBundle(checksConfig, checksConfigSignature, allowedSigners) if err != nil { // Check if it's an invalid signature (modified file) - if strings.HasPrefix(err.Error(), "invalid_signature:") { - signer := strings.TrimPrefix(err.Error(), "invalid_signature:") - + if signer, ok := strings.CutPrefix(err.Error(), "invalid_signature:"); ok { log.Print("[ERROR] ⚠️ Configuration Modified After Signing") log.Print(errorPrefix) log.Printf("[ERROR] The checks.yaml was changed after being signed by: %s", signer) diff --git a/cmd/server/main.go b/cmd/server/main.go index 2111290..4f725fc 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -179,7 +179,7 @@ func main() { // Validate flags if *gitURL == "" && *clone == "" { - log.Fatal("Either -git (repository to clone) or -clone (existing local clone) is required") + log.Fatal("Either -git (repository to clone) or -clone (existing local clone) is ruired") } if *gitURL != "" && *clone != "" { log.Fatal("Cannot specify both -git and -clone") @@ -204,10 +204,10 @@ func main() { // Create server funcMap := template.FuncMap{ - "formatTime": formatTimeFunc, - "formatAgo": formatAgoFunc, + "formatTime": formatTime, + "formatAgo": formatAgo, "inc": func(i int) int { return i + 1 }, - "truncateLines": truncateLinesFunc, + "truncateLines": truncateLines, "safeID": func(name string) string { return strings.ReplaceAll(strings.ReplaceAll(name, "_", "-"), ".", "-") }, @@ -321,16 +321,16 @@ func main() { } } -// Template function implementations. +// Template helper functions. -func formatTimeFunc(t time.Time) string { +func formatTime(t time.Time) string { if t.IsZero() { return "N/A" } return t.Format("2006-01-02 15:04:05") } -func formatAgoFunc(t time.Time) string { +func formatAgo(t time.Time) string { if t.IsZero() { return "never" } @@ -347,7 +347,7 @@ func formatAgoFunc(t time.Time) string { return fmt.Sprintf("%d days ago", int(dur.Hours()/24)) } -func truncateLinesFunc(text string, maxLines any) string { +func truncateLines(text string, maxLines any) string { if text == "" { return text } @@ -393,12 +393,19 @@ func (s *Server) loadDevices(ctx context.Context) error { // updateComplianceCacheLocked updates the compliance cache for a device. // Caller must hold s.mu lock. func (s *Server) updateComplianceCacheLocked(device *gitmdm.Device) { + s.updateComplianceCacheLockedWithCheckin(device, false) +} + +func (s *Server) updateComplianceCacheLockedWithCheckin(device *gitmdm.Device, markCheckedIn bool) { // Get existing cache to preserve HasCheckedIn flag existingCache, exists := s.complianceCache[device.HardwareID] cache := &ComplianceCache{} if exists { cache.HasCheckedIn = existingCache.HasCheckedIn } + if markCheckedIn { + cache.HasCheckedIn = true + } // Determine staleness threshold var staleThreshold time.Time @@ -439,16 +446,16 @@ func (s *Server) processFailedReports(ctx context.Context) { return case <-ticker.C: // Process all queued devices + processLoop: for { select { case device := <-s.failedReports: s.retryFailedDevice(ctx, device) default: // No more reports to process - goto nextTick + break processLoop } } - nextTick: } } } @@ -476,22 +483,22 @@ func (s *Server) retryFailedDevice(ctx context.Context, device *gitmdm.Device) { } } -func (s *Server) handleIndex(writer http.ResponseWriter, req *http.Request) { +func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { s.incrementRequestCount() - if req.URL.Path != "/" { - s.incrementErrorCount() - http.NotFound(writer, req) + if r.URL.Path != "/" { + s.incrementErrorequestCount() + http.NotFound(w, r) return } // Get filter parameters - search := strings.ToLower(strings.TrimSpace(req.URL.Query().Get("search"))) - status := req.URL.Query().Get("status") + search := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("search"))) + status := r.URL.Query().Get("status") // Get pagination parameters page := 1 - if pageStr := req.URL.Query().Get("page"); pageStr != "" { + if pageStr := r.URL.Query().Get("page"); pageStr != "" { if p, err := strconv.Atoi(pageStr); err == nil && p > 0 { page = p } @@ -598,10 +605,7 @@ func (s *Server) handleIndex(writer http.ResponseWriter, req *http.Request) { // Calculate pagination slice start := (page - 1) * itemsPerPage - end := start + itemsPerPage - if end > totalDevices { - end = totalDevices - } + end := min(start+itemsPerPage, totalDevices) var pagedDevices []viewmodels.DeviceListItem if start < totalDevices { @@ -613,7 +617,7 @@ func (s *Server) handleIndex(writer http.ResponseWriter, req *http.Request) { // Create view model with filter state viewModel := viewmodels.DeviceListView{ Devices: pagedDevices, - Search: req.URL.Query().Get("search"), // Keep original case for display + Search: r.URL.Query().Get("search"), // Keep original case for display Status: status, Page: page, TotalPages: totalPages, @@ -622,28 +626,28 @@ func (s *Server) handleIndex(writer http.ResponseWriter, req *http.Request) { HasNext: page < totalPages, } - writer.Header().Set("Content-Type", "text/html; charset=utf-8") - if err := s.tmpl.ExecuteTemplate(writer, "index.html", viewModel); err != nil { - s.incrementErrorCount() + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := s.tmpl.ExecuteTemplate(w, "index.html", viewModel); err != nil { + s.incrementErrorequestCount() log.Printf("[ERROR] Template error: %v", err) - http.Error(writer, "Internal server error", http.StatusInternalServerError) + http.Error(w, "Internal server error", http.StatusInternalServerError) return } } -func (s *Server) handleDevice(writer http.ResponseWriter, r *http.Request) { +func (s *Server) handleDevice(w http.ResponseWriter, r *http.Request) { s.incrementRequestCount() - // Security: Only allow GET requests for device viewing + // Security: Only allow GET rs for device viewing if r.Method != http.MethodGet { - http.Error(writer, "Method not allowed", http.StatusMethodNotAllowed) + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } hardwareID := strings.TrimPrefix(r.URL.Path, "/device/") if hardwareID == "" { - s.incrementErrorCount() - http.NotFound(writer, r) + s.incrementErrorequestCount() + http.NotFound(w, r) return } @@ -653,8 +657,8 @@ func (s *Server) handleDevice(writer http.ResponseWriter, r *http.Request) { s.mu.RUnlock() if !exists { - s.incrementErrorCount() - http.NotFound(writer, r) + s.incrementErrorequestCount() + http.NotFound(w, r) return } @@ -671,20 +675,15 @@ func (s *Server) handleDevice(writer http.ResponseWriter, r *http.Request) { // Build detailed view model with compliance analysis viewData := viewmodels.BuildDeviceDetail(device, staleThreshold) - writer.Header().Set("Content-Type", "text/html; charset=utf-8") - if err := s.tmpl.ExecuteTemplate(writer, "device.html", viewData); err != nil { - s.incrementErrorCount() + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := s.tmpl.ExecuteTemplate(w, "device.html", viewData); err != nil { + s.incrementErrorequestCount() log.Printf("[ERROR] Template error: %v", err) - http.Error(writer, "Internal server error", http.StatusInternalServerError) + http.Error(w, "Internal server error", http.StatusInternalServerError) return } } -// validateJoinKey checks if the provided join key is valid. -func validateJoinKey(providedKey string) bool { - return len(providedKey) == len(*joinKey) && subtle.ConstantTimeCompare([]byte(providedKey), []byte(*joinKey)) == 1 -} - // validateHardwareID validates the hardware ID format. func validateHardwareID(id string) error { if id == "" || len(id) > maxFieldLength { @@ -739,60 +738,61 @@ func validateChecks(checks map[string]gitmdm.Check) error { return nil } -func (s *Server) handleReport(writer http.ResponseWriter, request *http.Request) { +func (s *Server) handleReport(w http.ResponseWriter, r *http.Request) { start := time.Now() s.incrementRequestCount() - if request.Method != http.MethodPost { - s.incrementErrorCount() - http.Error(writer, "Method not allowed", http.StatusMethodNotAllowed) + if r.Method != http.MethodPost { + s.incrementErrorequestCount() + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } - // Security: Check join key - if !validateJoinKey(request.Header.Get("X-Join-Key")) { - s.incrementErrorCount() - log.Printf("[WARN] Unauthorized request from %s - invalid join key", request.RemoteAddr) - http.Error(writer, "Unauthorized - invalid join key", http.StatusUnauthorized) + // Security: Check join key using constant-time comparison + providedKey := r.Header.Get("X-Join-Key") + if len(providedKey) != len(*joinKey) || subtle.ConstantTimeCompare([]byte(providedKey), []byte(*joinKey)) != 1 { + s.incrementErrorequestCount() + log.Printf("[WARN] Unauthorized r from %s - invalid join key", r.RemoteAddr) + http.Error(w, "Unauthorized - invalid join key", http.StatusUnauthorized) return } - ctx := request.Context() + ctx := r.Context() var report gitmdm.DeviceReport - // Security: Limit request body size to prevent DoS - request.Body = http.MaxBytesReader(writer, request.Body, maxRequestBody) + // Security: Limit r body size to prevent DoS + r.Body = http.MaxBytesReader(w, r.Body, maxRequestBody) - if err := json.NewDecoder(request.Body).Decode(&report); err != nil { - s.incrementErrorCount() - log.Printf("[ERROR] Failed to decode report from %s: %v", request.RemoteAddr, err) - http.Error(writer, "Invalid request body", http.StatusBadRequest) + if err := json.NewDecoder(r.Body).Decode(&report); err != nil { + s.incrementErrorequestCount() + log.Printf("[ERROR] Failed to decode report from %s: %v", r.RemoteAddr, err) + http.Error(w, "Invalid r body", http.StatusBadRequest) return } // Validate report fields if err := validateReportFields(report); err != nil { - s.incrementErrorCount() - log.Printf("[WARN] Invalid report from %s: %v", request.RemoteAddr, err) - http.Error(writer, fmt.Sprintf("Invalid report: %v", err), http.StatusBadRequest) + s.incrementErrorequestCount() + log.Printf("[WARN] Invalid report from %s: %v", r.RemoteAddr, err) + http.Error(w, fmt.Sprintf("Invalid report: %v", err), http.StatusBadRequest) return } // Validate checks if err := validateChecks(report.Checks); err != nil { - s.incrementErrorCount() - log.Printf("[WARN] Invalid checks from %s: %v", request.RemoteAddr, err) - http.Error(writer, fmt.Sprintf("Invalid checks: %v", err), http.StatusBadRequest) + s.incrementErrorequestCount() + log.Printf("[WARN] Invalid checks from %s: %v", r.RemoteAddr, err) + http.Error(w, fmt.Sprintf("Invalid checks: %v", err), http.StatusBadRequest) return } // Extract client IP (handle X-Forwarded-For for proxies) - clientIP := request.RemoteAddr - if xff := request.Header.Get("X-Forwarded-For"); xff != "" { + clientIP := r.RemoteAddr + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { // Take the first IP from X-Forwarded-For if parts := strings.Split(xff, ","); len(parts) > 0 { clientIP = strings.TrimSpace(parts[0]) } - } else if xri := request.Header.Get("X-Real-IP"); xri != "" { + } else if xri := r.Header.Get("X-Real-IP"); xri != "" { clientIP = xri } @@ -816,18 +816,12 @@ func (s *Server) handleReport(writer http.ResponseWriter, request *http.Request) // Always update in-memory cache first for immediate availability s.mu.Lock() s.devices[device.HardwareID] = device - // Mark that this device has checked in since server startup - if existingCache, exists := s.complianceCache[device.HardwareID]; exists { - existingCache.HasCheckedIn = true - } else { - s.complianceCache[device.HardwareID] = &ComplianceCache{HasCheckedIn: true} - } - // Update compliance cache - s.updateComplianceCacheLocked(device) + // Update compliance cache and mark that this device has checked in since server startup + s.updateComplianceCacheLockedWithCheckin(device, true) s.mu.Unlock() log.Printf("[INFO] Received report from %s (device: %s, checks: %d) in %v", - request.RemoteAddr, device.HardwareID, len(device.Checks), time.Since(start)) + r.RemoteAddr, device.HardwareID, len(device.Checks), time.Since(start)) // Attempt to save to git store with graceful degradation retryCount := 0 @@ -854,23 +848,23 @@ func (s *Server) handleReport(writer http.ResponseWriter, request *http.Request) } s.healthMu.Unlock() } - // Continue processing - don't fail the request due to storage issues + // Continue processing - don't fail the r due to storage issues } // Always respond successfully since we have the data in memory - writer.Header().Set("Content-Type", "application/json") - writer.WriteHeader(http.StatusOK) - if _, err := writer.Write([]byte(`{"status":"ok"}`)); err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if _, err := w.Write([]byte(`{"status":"ok"}`)); err != nil { log.Printf("[WARN] Error writing response: %v", err) } } -func (s *Server) handleAPIDevices(writer http.ResponseWriter, r *http.Request) { +func (s *Server) handleAPIDevices(w http.ResponseWriter, r *http.Request) { s.incrementRequestCount() if r.Method != http.MethodGet { - s.incrementErrorCount() - http.Error(writer, "Method not allowed", http.StatusMethodNotAllowed) + s.incrementErrorequestCount() + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } @@ -881,16 +875,16 @@ func (s *Server) handleAPIDevices(writer http.ResponseWriter, r *http.Request) { } s.mu.RUnlock() - writer.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(writer).Encode(devices); err != nil { - s.incrementErrorCount() + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(devices); err != nil { + s.incrementErrorequestCount() log.Printf("[ERROR] Failed to encode devices: %v", err) - http.Error(writer, "Internal server error", http.StatusInternalServerError) + http.Error(w, "Internal server error", http.StatusInternalServerError) return } } -func (s *Server) handleHealth(writer http.ResponseWriter, _ *http.Request) { +func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request) { s.incrementRequestCount() s.healthMu.RLock() @@ -913,35 +907,35 @@ func (s *Server) handleHealth(writer http.ResponseWriter, _ *http.Request) { statusCode = http.StatusServiceUnavailable } - response := fmt.Sprintf(`{"status":%q,"devices":%d,"requests":%d,"errors":%d}`, + response := fmt.Sprintf(`{"status":%q,"devices":%d,"rs":%d,"errors":%d}`, status, deviceCount, requestCount, errorCount) - writer.Header().Set("Content-Type", "application/json") - writer.WriteHeader(statusCode) - if _, err := writer.Write([]byte(response)); err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + if _, err := w.Write([]byte(response)); err != nil { log.Printf("[WARN] Error writing health response: %v", err) } } func loggingMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Security: Add comprehensive security headers - writer.Header().Set("X-Content-Type-Options", "nosniff") - writer.Header().Set("X-Frame-Options", "DENY") - writer.Header().Set("X-XSS-Protection", "1; mode=block") - writer.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("X-XSS-Protection", "1; mode=block") + w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") // Strict CSP - only allow self-hosted resources // Keep unsafe-inline for styles as templates use inline styles cspPolicy := "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data:; font-src 'self'" - writer.Header().Set("Content-Security-Policy", cspPolicy) + w.Header().Set("Content-Security-Policy", cspPolicy) start := time.Now() - next.ServeHTTP(writer, r) + next.ServeHTTP(w, r) duration := time.Since(start) // Log with different levels based on duration and status if duration > 1*time.Second { - log.Printf("[WARN] Slow request: %s %s %s %v", r.RemoteAddr, r.Method, r.URL.Path, duration) + log.Printf("[WARN] Slow r: %s %s %s %v", r.RemoteAddr, r.Method, r.URL.Path, duration) } else { log.Printf("[DEBUG] %s %s %s %v", r.RemoteAddr, r.Method, r.URL.Path, duration) } @@ -955,7 +949,7 @@ func (s *Server) incrementRequestCount() { s.statsMu.Unlock() } -func (s *Server) incrementErrorCount() { +func (s *Server) incrementErrorequestCount() { s.statsMu.Lock() s.errorCount++ s.statsMu.Unlock() diff --git a/cmd/server/static/device.js b/cmd/server/static/device.js index a56e8c8..b082df2 100644 --- a/cmd/server/static/device.js +++ b/cmd/server/static/device.js @@ -1,38 +1,40 @@ function toggleCheck(header) { - const checkItem = header.parentElement; - checkItem.classList.toggle('expanded'); + const checkItem = header.parentElement; + checkItem.classList.toggle("expanded"); } -document.addEventListener('DOMContentLoaded', function() { - // Add click listeners to check headers - document.querySelectorAll('.check-header').forEach(header => { - header.addEventListener('click', function() { - toggleCheck(this); - }); +document.addEventListener("DOMContentLoaded", () => { + // Add click listeners to check headers + document.querySelectorAll(".check-header").forEach((header) => { + header.addEventListener("click", function () { + toggleCheck(this); }); + }); - // Filter functionality - document.querySelectorAll('.filter-btn').forEach(btn => { - btn.addEventListener('click', function() { - // Update active button - document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); - this.classList.add('active'); - - // Filter checks - const filter = this.dataset.filter; - document.querySelectorAll('.check-item').forEach(item => { - if (filter === 'all' || item.dataset.status === filter) { - item.style.display = ''; - } else { - item.style.display = 'none'; - } - }); - }); - }); + // Filter functionality + document.querySelectorAll(".filter-btn").forEach((btn) => { + btn.addEventListener("click", function () { + // Update active button + document.querySelectorAll(".filter-btn").forEach((b) => { + b.classList.remove("active"); + }); + this.classList.add("active"); - // Auto-expand failed checks - document.querySelectorAll('.check-item[data-status="fail"]').forEach(item => { - // Optionally auto-expand failed checks - // item.classList.add('expanded'); + // Filter checks + const filter = this.dataset.filter; + document.querySelectorAll(".check-item").forEach((item) => { + if (filter === "all" || item.dataset.status === filter) { + item.style.display = ""; + } else { + item.style.display = "none"; + } + }); }); -}); \ No newline at end of file + }); + + // Auto-expand failed checks + document.querySelectorAll('.check-item[data-status="fail"]').forEach((_item) => { + // Optionally auto-expand failed checks + // item.classList.add('expanded'); + }); +}); diff --git a/cmd/server/static/index.js b/cmd/server/static/index.js index d12ff29..f212481 100644 --- a/cmd/server/static/index.js +++ b/cmd/server/static/index.js @@ -1,48 +1,48 @@ // Search functionality -const searchInput = document.getElementById('searchInput'); +const searchInput = document.getElementById("searchInput"); let searchTimeout; -searchInput?.addEventListener('input', function() { - clearTimeout(searchTimeout); - searchTimeout = setTimeout(() => { - const searchValue = this.value.trim(); - const urlParams = new URLSearchParams(window.location.search); - - if (searchValue) { - urlParams.set('search', searchValue); - } else { - urlParams.delete('search'); - } - - urlParams.delete('page'); // Reset to page 1 - window.location.search = urlParams.toString(); - }, 500); // Debounce search +searchInput?.addEventListener("input", function () { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + const searchValue = this.value.trim(); + const urlParams = new URLSearchParams(window.location.search); + + if (searchValue) { + urlParams.set("search", searchValue); + } else { + urlParams.delete("search"); + } + + urlParams.delete("page"); // Reset to page 1 + window.location.search = urlParams.toString(); + }, 500); // Debounce search }); // Filter tabs -document.querySelectorAll('.filter-tab').forEach(tab => { - tab.addEventListener('click', function() { - const status = this.dataset.status; - const urlParams = new URLSearchParams(window.location.search); - - if (status) { - urlParams.set('status', status); - } else { - urlParams.delete('status'); - } - - urlParams.delete('page'); // Reset to page 1 - window.location.search = urlParams.toString(); - }); +document.querySelectorAll(".filter-tab").forEach((tab) => { + tab.addEventListener("click", function () { + const status = this.dataset.status; + const urlParams = new URLSearchParams(window.location.search); + + if (status) { + urlParams.set("status", status); + } else { + urlParams.delete("status"); + } + + urlParams.delete("page"); // Reset to page 1 + window.location.search = urlParams.toString(); + }); }); // Clear search on Escape key -document.addEventListener('keydown', function(e) { - if (e.key === 'Escape' && searchInput) { - searchInput.value = ''; - const urlParams = new URLSearchParams(window.location.search); - urlParams.delete('search'); - urlParams.delete('page'); - window.location.search = urlParams.toString(); - } -}); \ No newline at end of file +document.addEventListener("keydown", (e) => { + if (e.key === "Escape" && searchInput) { + searchInput.value = ""; + const urlParams = new URLSearchParams(window.location.search); + urlParams.delete("search"); + urlParams.delete("page"); + window.location.search = urlParams.toString(); + } +}); diff --git a/cmd/server/static/remediation.js b/cmd/server/static/remediation.js index e4e78cf..7529cd3 100644 --- a/cmd/server/static/remediation.js +++ b/cmd/server/static/remediation.js @@ -1,19 +1,19 @@ -document.addEventListener('DOMContentLoaded', function() { - // Add click event listeners to all remediation headers - var headers = document.querySelectorAll('.remediation-header'); - headers.forEach(function(header) { - header.addEventListener('click', function() { - var checkId = header.getAttribute('data-check-id'); - var content = document.getElementById('remediation-' + checkId); - var toggle = document.getElementById('toggle-' + checkId); - - if (content.style.display === 'none' || content.style.display === '') { - content.style.display = 'block'; - toggle.textContent = '▼'; - } else { - content.style.display = 'none'; - toggle.textContent = '▶'; - } - }); +document.addEventListener("DOMContentLoaded", () => { + // Add click event listeners to all remediation headers + const headers = document.querySelectorAll(".remediation-header"); + headers.forEach((header) => { + header.addEventListener("click", () => { + const checkId = header.getAttribute("data-check-id"); + const content = document.getElementById(`remediation-${checkId}`); + const toggle = document.getElementById(`toggle-${checkId}`); + + if (content.style.display === "none" || content.style.display === "") { + content.style.display = "block"; + toggle.textContent = "▼"; + } else { + content.style.display = "none"; + toggle.textContent = "▶"; + } }); -}); \ No newline at end of file + }); +}); diff --git a/cmd/server/static/style.css b/cmd/server/static/style.css index dba2627..38d86e5 100644 --- a/cmd/server/static/style.css +++ b/cmd/server/static/style.css @@ -10,517 +10,570 @@ */ :root { - /* Original colors */ - --color-canvas-default: #ffffff; - --color-canvas-subtle: #f6f8fa; - --color-canvas-inset: #f0f3f6; - --color-border-default: #d1d9e0; - --color-border-muted: #d1d9e0b3; - --color-fg-default: #1f2328; - --color-fg-muted: #656d76; - --color-fg-subtle: #6e7781; - --color-fg-on-emphasis: #ffffff; - --color-accent-fg: #0969da; - --color-accent-emphasis: #0969da; - --color-success-fg: #1a7f37; - --color-success-emphasis: #2da44e; - --color-success-subtle: #2da44e1a; - --color-danger-fg: #d1242f; - --color-danger-emphasis: #cf222e; - --color-danger-subtle: #cf222e1a; - --color-attention-fg: #9a6700; - --color-attention-emphasis: #bf8700; - --color-attention-subtle: #fff8c5; - --color-neutral-emphasis: #6e7781; - --color-neutral-subtle: #afb8c133; - - /* Funky additions */ - --funk-purple: #7c3aed; - --funk-pink: #ec4899; - --funk-gradient: linear-gradient(135deg, var(--funk-purple), var(--funk-pink)); - --shadow-small: 0 1px 0 rgba(31, 35, 40, 0.04); - --shadow-medium: 0 3px 6px rgba(140, 149, 159, 0.15); - --shadow-funk: 0 10px 40px rgba(124, 58, 237, 0.15); + /* Original colors */ + --color-canvas-default: #ffffff; + --color-canvas-subtle: #f6f8fa; + --color-canvas-inset: #f0f3f6; + --color-border-default: #d1d9e0; + --color-border-muted: #d1d9e0b3; + --color-fg-default: #1f2328; + --color-fg-muted: #656d76; + --color-fg-subtle: #6e7781; + --color-fg-on-emphasis: #ffffff; + --color-accent-fg: #0969da; + --color-accent-emphasis: #0969da; + --color-success-fg: #1a7f37; + --color-success-emphasis: #2da44e; + --color-success-subtle: #2da44e1a; + --color-danger-fg: #d1242f; + --color-danger-emphasis: #cf222e; + --color-danger-subtle: #cf222e1a; + --color-attention-fg: #9a6700; + --color-attention-emphasis: #bf8700; + --color-attention-subtle: #fff8c5; + --color-neutral-emphasis: #6e7781; + --color-neutral-subtle: #afb8c133; + + /* Funky additions */ + --funk-purple: #7c3aed; + --funk-pink: #ec4899; + --funk-gradient: linear-gradient(135deg, var(--funk-purple), var(--funk-pink)); + --shadow-small: 0 1px 0 rgba(31, 35, 40, 0.04); + --shadow-medium: 0 3px 6px rgba(140, 149, 159, 0.15); + --shadow-funk: 0 10px 40px rgba(124, 58, 237, 0.15); } * { - margin: 0; - padding: 0; - box-sizing: border-box; + margin: 0; + padding: 0; + box-sizing: border-box; } body { - font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; - font-size: 14px; - line-height: 1.6; - color: var(--color-fg-default); - background: var(--color-canvas-default); - position: relative; + font-family: + "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace; + font-size: 14px; + line-height: 1.6; + color: var(--color-fg-default); + background: var(--color-canvas-default); + position: relative; } /* Subtle background pattern */ body::before { - content: ''; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-image: - repeating-linear-gradient(45deg, transparent, transparent 35px, rgba(124, 58, 237, 0.01) 35px, rgba(124, 58, 237, 0.01) 70px); - pointer-events: none; - z-index: 0; + content: ""; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: repeating-linear-gradient( + 45deg, + transparent, + transparent 35px, + rgba(124, 58, 237, 0.01) 35px, + rgba(124, 58, 237, 0.01) 70px + ); + pointer-events: none; + z-index: 0; } .container { - max-width: 1280px; - margin: 0 auto; - padding: 24px; - position: relative; - z-index: 1; + max-width: 1280px; + margin: 0 auto; + padding: 24px; + position: relative; + z-index: 1; } /* Page Header with funk */ .page-header { - border-bottom: 2px solid transparent; - background: linear-gradient(white, white) padding-box, - var(--funk-gradient) border-box; - padding-bottom: 16px; - margin-bottom: 24px; - position: relative; + border-bottom: 2px solid transparent; + background: + linear-gradient(white, white) padding-box, + var(--funk-gradient) border-box; + padding-bottom: 16px; + margin-bottom: 24px; + position: relative; } .page-title { - font-size: 32px; - font-weight: 700; - margin-bottom: 8px; - background: var(--funk-gradient); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - animation: shimmer 3s ease-in-out infinite; + font-size: 32px; + font-weight: 700; + margin-bottom: 8px; + background: var(--funk-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + animation: shimmer 3s ease-in-out infinite; } @keyframes shimmer { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.8; } + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.8; + } } .page-subtitle { - color: var(--color-fg-muted); - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - font-style: italic; + color: var(--color-fg-muted); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-style: italic; } /* Stats with pop */ .stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); - gap: 16px; - margin-bottom: 32px; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 16px; + margin-bottom: 32px; } .stat-card { - background: var(--color-canvas-subtle); - border: 1px solid var(--color-border-default); - border-radius: 12px; - padding: 16px; - text-align: center; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - position: relative; - overflow: hidden; + background: var(--color-canvas-subtle); + border: 1px solid var(--color-border-default); + border-radius: 12px; + padding: 16px; + text-align: center; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; } .stat-card::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); - transition: left 0.5s; + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transition: left 0.5s; } .stat-card:hover { - transform: translateY(-4px); - box-shadow: var(--shadow-funk); - border-color: var(--funk-purple); + transform: translateY(-4px); + box-shadow: var(--shadow-funk); + border-color: var(--funk-purple); } .stat-card:hover::before { - left: 100%; + left: 100%; } .stat-value { - font-size: 36px; - font-weight: 800; - margin-bottom: 4px; - font-family: 'SF Mono', Monaco, monospace; - letter-spacing: -1px; + font-size: 36px; + font-weight: 800; + margin-bottom: 4px; + font-family: "SF Mono", Monaco, monospace; + letter-spacing: -1px; } .stat-label { - font-size: 11px; - color: var(--color-fg-muted); - text-transform: uppercase; - letter-spacing: 1px; - font-weight: 600; + font-size: 11px; + color: var(--color-fg-muted); + text-transform: uppercase; + letter-spacing: 1px; + font-weight: 600; } -.stat-value.success { - color: var(--color-success-emphasis); - text-shadow: 0 0 20px rgba(45, 164, 78, 0.3); +.stat-value.success { + color: var(--color-success-emphasis); + text-shadow: 0 0 20px rgba(45, 164, 78, 0.3); } -.stat-value.warning { - color: var(--color-attention-emphasis); - text-shadow: 0 0 20px rgba(191, 135, 0, 0.3); +.stat-value.warning { + color: var(--color-attention-emphasis); + text-shadow: 0 0 20px rgba(191, 135, 0, 0.3); } -.stat-value.danger { - color: var(--color-danger-emphasis); - text-shadow: 0 0 20px rgba(207, 34, 46, 0.3); +.stat-value.danger { + color: var(--color-danger-emphasis); + text-shadow: 0 0 20px rgba(207, 34, 46, 0.3); } -.stat-value.neutral { color: var(--color-neutral-emphasis); } +.stat-value.neutral { + color: var(--color-neutral-emphasis); +} /* Controls with style */ .controls-bar { - display: flex; - gap: 16px; - margin-bottom: 16px; - align-items: center; - flex-wrap: wrap; + display: flex; + gap: 16px; + margin-bottom: 16px; + align-items: center; + flex-wrap: wrap; } .search-box { - flex: 1; - min-width: 200px; - position: relative; + flex: 1; + min-width: 200px; + position: relative; } .search-input { - width: 100%; - padding: 10px 12px 10px 36px; - border: 2px solid var(--color-border-default); - border-radius: 8px; - font-size: 14px; - background: var(--color-canvas-default); - transition: all 0.2s; - font-family: 'SF Mono', Monaco, monospace; + width: 100%; + padding: 10px 12px 10px 36px; + border: 2px solid var(--color-border-default); + border-radius: 8px; + font-size: 14px; + background: var(--color-canvas-default); + transition: all 0.2s; + font-family: "SF Mono", Monaco, monospace; } .search-input:focus { - outline: none; - border-color: var(--funk-purple); - box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1); - transform: translateX(2px); + outline: none; + border-color: var(--funk-purple); + box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1); + transform: translateX(2px); } .search-icon { - position: absolute; - left: 12px; - top: 50%; - transform: translateY(-50%); - width: 16px; - height: 16px; - color: var(--color-fg-muted); - pointer-events: none; + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + width: 16px; + height: 16px; + color: var(--color-fg-muted); + pointer-events: none; } .filter-tabs { - display: flex; - background: var(--color-canvas-subtle); - border: 2px solid var(--color-border-default); - border-radius: 8px; - overflow: hidden; + display: flex; + background: var(--color-canvas-subtle); + border: 2px solid var(--color-border-default); + border-radius: 8px; + overflow: hidden; } .filter-tab { - padding: 8px 16px; - background: transparent; - border: none; - color: var(--color-fg-default); - font-size: 13px; - font-weight: 600; - cursor: pointer; - transition: all 0.2s; - position: relative; - font-family: 'SF Mono', Monaco, monospace; + padding: 8px 16px; + background: transparent; + border: none; + color: var(--color-fg-default); + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + position: relative; + font-family: "SF Mono", Monaco, monospace; } .filter-tab:not(:last-child)::after { - content: ''; - position: absolute; - right: 0; - top: 25%; - height: 50%; - width: 1px; - background: var(--color-border-default); + content: ""; + position: absolute; + right: 0; + top: 25%; + height: 50%; + width: 1px; + background: var(--color-border-default); } .filter-tab:hover { - background: rgba(124, 58, 237, 0.05); + background: rgba(124, 58, 237, 0.05); } .filter-tab.active { - background: var(--funk-gradient); - color: var(--color-fg-on-emphasis); - text-shadow: 0 1px 2px rgba(0,0,0,0.1); + background: var(--funk-gradient); + color: var(--color-fg-on-emphasis); + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } -.filter-tab.active::after { display: none; } +.filter-tab.active::after { + display: none; +} /* Table with personality */ .devices-table { - background: var(--color-canvas-default); - border: 2px solid var(--color-border-default); - border-radius: 12px; - overflow: hidden; - box-shadow: var(--shadow-medium); + background: var(--color-canvas-default); + border: 2px solid var(--color-border-default); + border-radius: 12px; + overflow: hidden; + box-shadow: var(--shadow-medium); } .table-wrapper { - overflow-x: auto; + overflow-x: auto; } table { - width: 100%; - border-collapse: collapse; + width: 100%; + border-collapse: collapse; } thead { - background: var(--color-canvas-subtle); - border-bottom: 2px solid var(--color-border-default); + background: var(--color-canvas-subtle); + border-bottom: 2px solid var(--color-border-default); } th { - text-align: left; - padding: 14px 16px; - font-weight: 700; - font-size: 11px; - color: var(--funk-purple); - text-transform: uppercase; - letter-spacing: 1px; + text-align: left; + padding: 14px 16px; + font-weight: 700; + font-size: 11px; + color: var(--funk-purple); + text-transform: uppercase; + letter-spacing: 1px; } tbody tr { - border-bottom: 1px solid var(--color-border-muted); - transition: all 0.2s; + border-bottom: 1px solid var(--color-border-muted); + transition: all 0.2s; } tbody tr:last-child { - border-bottom: none; + border-bottom: none; } tbody tr:hover { - background: linear-gradient(90deg, - transparent 0%, - rgba(124, 58, 237, 0.03) 50%, - transparent 100%); - transform: translateX(2px); + background: linear-gradient( + 90deg, + transparent 0%, + rgba(124, 58, 237, 0.03) 50%, + transparent 100% + ); + transform: translateX(2px); } td { - padding: 14px 16px; - font-size: 13px; - font-family: 'SF Mono', Monaco, monospace; + padding: 14px 16px; + font-size: 13px; + font-family: "SF Mono", Monaco, monospace; } .device-link { - color: var(--funk-purple); - text-decoration: none; - font-weight: 600; - transition: all 0.2s; - display: inline-block; + color: var(--funk-purple); + text-decoration: none; + font-weight: 600; + transition: all 0.2s; + display: inline-block; } .device-link:hover { - color: var(--funk-pink); - transform: translateX(2px); - text-decoration: underline; - text-decoration-style: wavy; - text-underline-offset: 3px; + color: var(--funk-pink); + transform: translateX(2px); + text-decoration: underline; + text-decoration-style: wavy; + text-underline-offset: 3px; } .os-badge { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 3px 10px; - background: var(--color-neutral-subtle); - border: 1px solid var(--color-border-default); - border-radius: 20px; - font-size: 11px; - color: var(--color-fg-muted); - font-weight: 600; + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + background: var(--color-neutral-subtle); + border: 1px solid var(--color-border-default); + border-radius: 20px; + font-size: 11px; + color: var(--color-fg-muted); + font-weight: 600; } .compliance-badge { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 5px 12px; - border-radius: 20px; - font-size: 12px; - font-weight: 700; - transition: all 0.2s; - animation: pulse 2s infinite; + display: inline-flex; + align-items: center; + gap: 4px; + padding: 5px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: 700; + transition: all 0.2s; + animation: pulse 2s infinite; } @keyframes pulse { - 0%, 100% { transform: scale(1); } - 50% { transform: scale(1.02); } + 0%, + 100% { + transform: scale(1); + } + 50% { + transform: scale(1.02); + } } .compliance-badge.excellent { - background: linear-gradient(135deg, #10b981, #059669); - color: white; - box-shadow: 0 2px 10px rgba(16, 185, 129, 0.3); + background: linear-gradient(135deg, #10b981, #059669); + color: white; + box-shadow: 0 2px 10px rgba(16, 185, 129, 0.3); } .compliance-badge.good { - background: linear-gradient(135deg, #3b82f6, #2563eb); - color: white; - box-shadow: 0 2px 10px rgba(59, 130, 246, 0.3); + background: linear-gradient(135deg, #3b82f6, #2563eb); + color: white; + box-shadow: 0 2px 10px rgba(59, 130, 246, 0.3); } .compliance-badge.fair { - background: linear-gradient(135deg, #f59e0b, #d97706); - color: white; - box-shadow: 0 2px 10px rgba(245, 158, 11, 0.3); + background: linear-gradient(135deg, #f59e0b, #d97706); + color: white; + box-shadow: 0 2px 10px rgba(245, 158, 11, 0.3); } .compliance-badge.poor { - background: linear-gradient(135deg, #ef4444, #dc2626); - color: white; - box-shadow: 0 2px 10px rgba(239, 68, 68, 0.3); + background: linear-gradient(135deg, #ef4444, #dc2626); + color: white; + box-shadow: 0 2px 10px rgba(239, 68, 68, 0.3); } .compliance-badge.checking { - background: linear-gradient(135deg, #6b7280, #4b5563); - color: white; - animation: rotate 2s linear infinite; + background: linear-gradient(135deg, #6b7280, #4b5563); + color: white; + animation: rotate 2s linear infinite; } @keyframes rotate { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } .time-ago { - color: var(--color-fg-muted); - font-size: 11px; - font-style: italic; + color: var(--color-fg-muted); + font-size: 11px; + font-style: italic; } /* Empty State */ .empty-state { - text-align: center; - padding: 80px 24px; - color: var(--color-fg-muted); + text-align: center; + padding: 80px 24px; + color: var(--color-fg-muted); } .empty-state-icon { - width: 80px; - height: 80px; - margin: 0 auto 20px; - opacity: 0.2; - animation: float 3s ease-in-out infinite; + width: 80px; + height: 80px; + margin: 0 auto 20px; + opacity: 0.2; + animation: float 3s ease-in-out infinite; } @keyframes float { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-10px); } + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } } .empty-state-title { - font-size: 28px; - font-weight: 800; - margin-bottom: 8px; - background: var(--funk-gradient); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; + font-size: 28px; + font-weight: 800; + margin-bottom: 8px; + background: var(--funk-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; } .empty-state-description { - max-width: 400px; - margin: 0 auto; - font-family: -apple-system, BlinkMacSystemFont, sans-serif; + max-width: 400px; + margin: 0 auto; + font-family: -apple-system, BlinkMacSystemFont, sans-serif; } /* Pagination */ .pagination { - display: flex; - justify-content: center; - align-items: center; - gap: 12px; - margin-top: 32px; - padding-top: 32px; - border-top: 2px solid transparent; - background: linear-gradient(white, white) padding-box, - var(--funk-gradient) border-box; + display: flex; + justify-content: center; + align-items: center; + gap: 12px; + margin-top: 32px; + padding-top: 32px; + border-top: 2px solid transparent; + background: + linear-gradient(white, white) padding-box, + var(--funk-gradient) border-box; } .pagination-btn { - padding: 8px 16px; - border: 2px solid var(--color-border-default); - border-radius: 8px; - background: var(--color-canvas-default); - color: var(--color-fg-default); - text-decoration: none; - font-size: 13px; - font-weight: 600; - transition: all 0.2s; - font-family: 'SF Mono', Monaco, monospace; + padding: 8px 16px; + border: 2px solid var(--color-border-default); + border-radius: 8px; + background: var(--color-canvas-default); + color: var(--color-fg-default); + text-decoration: none; + font-size: 13px; + font-weight: 600; + transition: all 0.2s; + font-family: "SF Mono", Monaco, monospace; } .pagination-btn:hover:not(.disabled) { - background: var(--funk-gradient); - color: white; - border-color: var(--funk-purple); - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(124, 58, 237, 0.2); + background: var(--funk-gradient); + color: white; + border-color: var(--funk-purple); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(124, 58, 237, 0.2); } .pagination-btn.disabled { - opacity: 0.3; - cursor: not-allowed; - color: var(--color-fg-muted); + opacity: 0.3; + cursor: not-allowed; + color: var(--color-fg-muted); } .pagination-info { - color: var(--color-fg-muted); - font-size: 13px; - font-weight: 600; - margin: 0 16px; + color: var(--color-fg-muted); + font-size: 13px; + font-weight: 600; + margin: 0 16px; } /* ASCII art header (hidden but fun) */ .ascii-header { - display: none; - font-family: 'Courier New', monospace; - font-size: 10px; - line-height: 1; - color: var(--funk-purple); - opacity: 0.1; - white-space: pre; - position: absolute; - top: 10px; - right: 10px; - pointer-events: none; + display: none; + font-family: "Courier New", monospace; + font-size: 10px; + line-height: 1; + color: var(--funk-purple); + opacity: 0.1; + white-space: pre; + position: absolute; + top: 10px; + right: 10px; + pointer-events: none; } /* Responsive */ @media (max-width: 768px) { - .container { padding: 16px; } - .page-title { font-size: 24px; } - .stats-grid { grid-template-columns: repeat(2, 1fr); } - .controls-bar { flex-direction: column; align-items: stretch; } - .search-box { width: 100%; } - .table-wrapper { margin: 0 -16px; width: calc(100% + 32px); } - th, td { padding: 10px 12px; font-size: 12px; } - .os-badge { display: none; } -} \ No newline at end of file + .container { + padding: 16px; + } + .page-title { + font-size: 24px; + } + .stats-grid { + grid-template-columns: repeat(2, 1fr); + } + .controls-bar { + flex-direction: column; + align-items: stretch; + } + .search-box { + width: 100%; + } + .table-wrapper { + margin: 0 -16px; + width: calc(100% + 32px); + } + th, + td { + padding: 10px 12px; + font-size: 12px; + } + .os-badge { + display: none; + } +} diff --git a/cmd/sign/main.go b/cmd/sign/main.go index ec03d11..dd6c975 100644 --- a/cmd/sign/main.go +++ b/cmd/sign/main.go @@ -164,26 +164,28 @@ func showSignerInfo(certPEM []byte) { // Find email if idx := strings.Index(certStr, "@"); idx > 0 { // Get a reasonable chunk around the @ - start := idx - emailContextBefore - if start < 0 { - start = 0 - } - end := idx + emailContextAfter - if end > len(certStr) { - end = len(certStr) - } + start := max(0, idx-emailContextBefore) + end := min(idx+emailContextAfter, len(certStr)) chunk := certStr[start:end] // Find @ again in chunk and extract email-like string if at := strings.Index(chunk, "@"); at > 0 { // Simple extraction: take non-space characters around @ - email := "" - for i := at; i >= 0 && chunk[i] > asciiSpace; i-- { - email = string(chunk[i]) + email + var builder strings.Builder + // Collect characters before @ (in reverse order) + start := at + for start >= 0 && chunk[start] > asciiSpace { + start-- } - for i := at + 1; i < len(chunk) && chunk[i] > asciiSpace; i++ { - email += string(chunk[i]) + start++ + builder.WriteString(chunk[start : at+1]) + // Collect characters after @ + end := at + 1 + for end < len(chunk) && chunk[end] > asciiSpace { + end++ } + builder.WriteString(chunk[at+1 : end]) + email := builder.String() if strings.Contains(email, ".") { fmt.Printf("\n✓ Signed by: %s:%s\n", provider, email) fmt.Printf("✓ To allow: --signed-by \"%s:%s\"\n", provider, email) diff --git a/go.mod b/go.mod index 48a2028..b81b744 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/codeGROOVE-dev/gitMDM go 1.24.0 require ( - github.com/codeGROOVE-dev/retry v1.3.0 + github.com/codeGROOVE-dev/retry v1.3.1 github.com/go-git/go-git/v5 v5.16.4 gopkg.in/yaml.v3 v3.0.1 ) @@ -12,20 +12,21 @@ require ( dario.cat/mergo v1.0.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect - github.com/cloudflare/circl v1.6.1 // indirect - github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/cloudflare/circl v1.6.2 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-git/go-billy/v5 v5.7.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/pjbgf/sha1cd v0.4.0 // indirect + github.com/kevinburke/ssh_config v1.4.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect - github.com/skeema/knownhosts v1.3.1 // indirect + github.com/skeema/knownhosts v1.3.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sys v0.38.0 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/sys v0.40.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index 42f9701..8930ead 100644 --- a/go.sum +++ b/go.sum @@ -9,12 +9,12 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= -github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= -github.com/codeGROOVE-dev/retry v1.3.0 h1:/+ipAWRJLL6y1R1vprYo0FSjSBvH6fE5j9LKXjpD54g= -github.com/codeGROOVE-dev/retry v1.3.0/go.mod h1:8OgefgV1XP7lzX2PdKlCXILsYKuz6b4ZpHa/20iLi8E= -github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= -github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ= +github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/codeGROOVE-dev/retry v1.3.1 h1:BAkfDzs6FssxLCGWGgM97bb+6/8GTa40Cs147vXkJOg= +github.com/codeGROOVE-dev/retry v1.3.1/go.mod h1:+b3huqYGY1+ZJyuCmR8nBVLjd3WJ7qAFss+sI4s6FSc= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -26,8 +26,8 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= -github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= +github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= @@ -38,8 +38,10 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= -github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= +github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -49,8 +51,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= -github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY= -github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= +github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -60,37 +62,37 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7 github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= -github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= +github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/analyzer/analyzer.go b/internal/analyzer/analyzer.go index 91564ae..b1b323c 100644 --- a/internal/analyzer/analyzer.go +++ b/internal/analyzer/analyzer.go @@ -6,6 +6,7 @@ import ( "log" "os" "regexp" + "slices" "strings" "github.com/codeGROOVE-dev/gitMDM/internal/config" @@ -42,15 +43,13 @@ func AnalyzeCheck(output *gitmdm.CommandOutput, rule config.CommandRule) error { return fmt.Errorf("invalid includes regex: %w", err) } - // Split content into lines and check each one - lines := strings.Split(content, "\n") - for _, line := range lines { - if includesRegex.MatchString(line) { - output.Failed = true - output.FailReason = fmt.Sprintf("Output matched failure pattern: %s", rule.Includes) - output.Remediation = rule.Remediation - return nil // Not an error, just a failed check - } + // Check if any line matches the failure pattern + lines := slices.Collect(strings.SplitSeq(content, "\n")) + if slices.ContainsFunc(lines, includesRegex.MatchString) { + output.Failed = true + output.FailReason = fmt.Sprintf("Output matched failure pattern: %s", rule.Includes) + output.Remediation = rule.Remediation + return nil // Not an error, just a failed check } } @@ -61,15 +60,9 @@ func AnalyzeCheck(output *gitmdm.CommandOutput, rule config.CommandRule) error { return fmt.Errorf("invalid excludes regex: %w", err) } - // Split content into lines and check if any line matches - lines := strings.Split(content, "\n") - matchFound := false - for _, line := range lines { - if excludesRegex.MatchString(line) { - matchFound = true - break - } - } + // Check if any line matches the pattern + lines := slices.Collect(strings.SplitSeq(content, "\n")) + matchFound := slices.ContainsFunc(lines, excludesRegex.MatchString) if !matchFound { output.Failed = true diff --git a/internal/config/types.go b/internal/config/types.go index 9cda3cb..7b5bd98 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -49,8 +49,7 @@ func (cd CheckDefinition) CommandsForOS(osName string) []CommandRule { // Check comma-separated keys for key := range cd { if strings.Contains(key, ",") { - parts := strings.Split(key, ",") - for _, part := range parts { + for part := range strings.SplitSeq(key, ",") { if strings.TrimSpace(part) == osName { if rules := cd.parseRules(key); rules != nil { return rules diff --git a/internal/gitstore/store.go b/internal/gitstore/store.go index d51f65a..e981606 100644 --- a/internal/gitstore/store.go +++ b/internal/gitstore/store.go @@ -137,17 +137,10 @@ func NewRemote(ctx context.Context, gitURL string) (*Store, error) { log.Printf("[INFO] Cloning repository from %s to %s", gitURL, tempDir) var repo *git.Repository + // Setup authentication - go-git will use default SSH agent or keys + // Could add HTTP basic auth or SSH keys here if needed var auth transport.AuthMethod - // Setup authentication if needed - if strings.HasPrefix(gitURL, "https://") || strings.HasPrefix(gitURL, "http://") { - // Could add HTTP basic auth here if needed - auth = nil - } else if strings.HasPrefix(gitURL, "git@") || strings.Contains(gitURL, ":") { - // SSH auth - will use SSH agent or default keys - auth = nil // go-git will try default SSH auth - } - err = retry.Do(func() error { cloneOptions := &git.CloneOptions{ URL: gitURL, diff --git a/internal/viewmodels/viewmodels.go b/internal/viewmodels/viewmodels.go index ece52da..056a7e5 100644 --- a/internal/viewmodels/viewmodels.go +++ b/internal/viewmodels/viewmodels.go @@ -341,12 +341,11 @@ func parseMacOSOutput(output string) (osName, version string) { } var productName, productVersion string - lines := strings.Split(output, "\n") - for _, line := range lines { - if strings.HasPrefix(line, "ProductName:") { - productName = strings.TrimSpace(strings.TrimPrefix(line, "ProductName:")) - } else if strings.HasPrefix(line, "ProductVersion:") { - productVersion = strings.TrimSpace(strings.TrimPrefix(line, "ProductVersion:")) + for line := range strings.SplitSeq(output, "\n") { + if name, ok := strings.CutPrefix(line, "ProductName:"); ok { + productName = strings.TrimSpace(name) + } else if version, ok := strings.CutPrefix(line, "ProductVersion:"); ok { + productVersion = strings.TrimSpace(version) } } @@ -363,15 +362,20 @@ func parseLinuxOutput(output string) (osName, version string) { } var name, versionStr string - lines := strings.Split(output, "\n") - for _, line := range lines { + for line := range strings.SplitSeq(output, "\n") { switch { case strings.HasPrefix(line, "NAME="): - name = strings.Trim(strings.TrimPrefix(line, "NAME="), `"`) + if val, ok := strings.CutPrefix(line, "NAME="); ok { + name = strings.Trim(val, `"`) + } case strings.HasPrefix(line, "VERSION="): - versionStr = strings.Trim(strings.TrimPrefix(line, "VERSION="), `"`) + if val, ok := strings.CutPrefix(line, "VERSION="); ok { + versionStr = strings.Trim(val, `"`) + } case strings.HasPrefix(line, "VERSION_ID=") && versionStr == "": - versionStr = strings.Trim(strings.TrimPrefix(line, "VERSION_ID="), `"`) + if val, ok := strings.CutPrefix(line, "VERSION_ID="); ok { + versionStr = strings.Trim(val, `"`) + } default: // No action needed for other lines } @@ -417,8 +421,7 @@ func parseWindowsOutput(output string) (osName, version string) { } var osNameStr, osVersion string - lines := strings.Split(output, "\n") - for _, line := range lines { + for line := range strings.SplitSeq(output, "\n") { if strings.Contains(line, "OS Name:") { parts := strings.Split(line, ":") if len(parts) > 1 {