package report import ( "log/slog" "strings" "github.com/pinchtab/pinchtab/internal/config" ) type SecurityWarning struct { ID string Message string Attrs []any } type SecurityPostureCheck struct { ID string Label string Passed bool Detail string } type SecurityPosture struct { Checks []SecurityPostureCheck Passed int Total int Level string Bar string } func AssessSecurityPosture(cfg *config.RuntimeConfig) SecurityPosture { if cfg == nil { return SecurityPosture{Level: "UNKNOWN"} } checks := []SecurityPostureCheck{ { ID: "bind_loopback", Label: "loopback bind", Passed: isLoopbackBind(cfg.Bind), Detail: cfg.Bind, }, { ID: "api_auth_enabled", Label: "api auth", Passed: strings.TrimSpace(cfg.Token) != "", Detail: map[bool]string{true: "required", false: "disabled"}[strings.TrimSpace(cfg.Token) != ""], }, { ID: "sensitive_endpoints_disabled", Label: "sensitive endpoints", Passed: len(cfg.EnabledSensitiveEndpoints()) == 0, Detail: formatEndpointStatus(cfg.EnabledSensitiveEndpoints()), }, { ID: "attach_local_only", Label: "attach host scope", Passed: !attachAllowsNonLocalHosts(cfg.AttachAllowHosts), Detail: formatHostScope(cfg.AttachAllowHosts), }, { ID: "idpi_whitelist_scoped", Label: "website whitelist", Passed: cfg.IDPI.Enabled && len(cfg.IDPI.AllowedDomains) > 0 && !allowsAllDomains(cfg.IDPI.AllowedDomains), Detail: formatWhitelistStatus(cfg), }, { ID: "idpi_strict_mode", Label: "IDPI strict mode", Passed: cfg.IDPI.Enabled && cfg.IDPI.StrictMode, Detail: formatStrictModeStatus(cfg), }, { ID: "idpi_content_protection", Label: "IDPI content guard", Passed: cfg.IDPI.Enabled && (cfg.IDPI.ScanContent || cfg.IDPI.WrapContent), Detail: formatContentGuardStatus(cfg), }, } passed := 0 for _, check := range checks { if check.Passed { passed++ } } return SecurityPosture{ Checks: checks, Passed: passed, Total: len(checks), Level: securityPostureLevel(passed, len(checks)), Bar: securityPostureBar(passed, len(checks)), } } func assessSecurityPosture(cfg *config.RuntimeConfig) SecurityPosture { return AssessSecurityPosture(cfg) } func securityPostureBar(passed, total int) string { if total == 0 { return "[ ]" } const width = 10 pct := float64(passed) / float64(total) filled := int(pct * width) if filled > width { filled = width } bar := "[" + strings.Repeat("■", filled) + strings.Repeat(" ", width-filled) + "]" return bar } func AssessSecurityWarnings(cfg *config.RuntimeConfig) []SecurityWarning { if cfg == nil { return nil } warnings := make([]SecurityWarning, 0, 8) enabled := cfg.EnabledSensitiveEndpoints() if len(enabled) > 0 { warnings = append(warnings, SecurityWarning{ ID: "sensitive_endpoints_enabled", Message: "sensitive endpoints enabled", Attrs: []any{"endpoints", enabled, "hint", "only enable them in trusted environments"}, }) } if cfg.Token == "" { warnings = append(warnings, SecurityWarning{ ID: "api_auth_disabled", Message: "api authentication disabled", Attrs: []any{"hint", "set PINCHTAB_TOKEN to require bearer auth for all endpoints"}, }) } if len(enabled) > 0 && cfg.Token == "" { warnings = append(warnings, SecurityWarning{ ID: "sensitive_endpoints_without_auth", Message: "high-risk configuration: sensitive endpoints enabled without API authentication", Attrs: []any{"endpoints", enabled, "hint", "set PINCHTAB_TOKEN or disable the sensitive endpoints"}, }) } if !isLoopbackBind(cfg.Bind) { warnings = append(warnings, SecurityWarning{ ID: "non_loopback_bind", Message: "server exposed on a non-loopback bind address", Attrs: []any{"bind", cfg.Bind, "hint", "prefer 127.0.0.1 or localhost unless remote access is intentional"}, }) } if !cfg.IDPI.Enabled { warnings = append(warnings, SecurityWarning{ ID: "idpi_disabled", Message: "IDPI disabled; website whitelist inactive", Attrs: []any{"setting", "security.idpi.enabled", "hint", "enable IDPI and keep security.idpi.allowedDomains scoped to approved websites"}, }) } else { if len(cfg.IDPI.AllowedDomains) == 0 { warnings = append(warnings, SecurityWarning{ ID: "idpi_whitelist_not_set", Message: "website whitelist is not set for IDPI", Attrs: []any{"setting", "security.idpi.allowedDomains", "hint", "configure allowedDomains to restrict which websites navigation may reach"}, }) } else if allowsAllDomains(cfg.IDPI.AllowedDomains) { warnings = append(warnings, SecurityWarning{ ID: "idpi_whitelist_allows_all", Message: "website whitelist allows all domains", Attrs: []any{"setting", "security.idpi.allowedDomains", "hint", "remove '*' and list only approved domains"}, }) } if !cfg.IDPI.StrictMode { warnings = append(warnings, SecurityWarning{ ID: "idpi_warn_mode", Message: "IDPI strict mode disabled", Attrs: []any{"setting", "security.idpi.strictMode", "hint", "enable strict mode to block requests instead of only emitting warnings"}, }) } if !cfg.IDPI.ScanContent && !cfg.IDPI.WrapContent { warnings = append(warnings, SecurityWarning{ ID: "idpi_content_protection_disabled", Message: "IDPI content protections are disabled", Attrs: []any{"hint", "enable security.idpi.scanContent or security.idpi.wrapContent to protect text and snapshot responses"}, }) } } if attachAllowsNonLocalHosts(cfg.AttachAllowHosts) { warnings = append(warnings, SecurityWarning{ ID: "attach_external_hosts", Message: "attach allowHosts includes non-local hosts", Attrs: []any{"allowHosts", cfg.AttachAllowHosts, "hint", "keep security.attach.allowHosts limited to local addresses unless external Chrome instances are intentional"}, }) } return warnings } func assessSecurityWarnings(cfg *config.RuntimeConfig) []SecurityWarning { return AssessSecurityWarnings(cfg) } func LogSecurityWarnings(cfg *config.RuntimeConfig) { for _, warning := range AssessSecurityWarnings(cfg) { attrs := append([]any{"category", "security", "warningId", warning.ID}, warning.Attrs...) slog.Warn(warning.Message, attrs...) } } func isLoopbackBind(bind string) bool { switch strings.TrimSpace(strings.ToLower(bind)) { case "127.0.0.1", "localhost", "::1", "": return true default: return false } } func allowsAllDomains(domains []string) bool { for _, domain := range domains { if strings.TrimSpace(domain) == "*" { return true } } return false } func attachAllowsNonLocalHosts(hosts []string) bool { if len(hosts) == 0 { return false } for _, host := range hosts { switch strings.TrimSpace(strings.ToLower(host)) { case "", "127.0.0.1", "localhost", "::1": default: return true } } return false } func securityPostureLevel(passed, total int) string { if total == 0 { return "UNKNOWN" } switch { case passed == total: return "LOCKED" case passed >= total-1: return "GUARDED" case passed >= 3: return "ELEVATED" default: return "EXPOSED" } } func formatEndpointStatus(enabled []string) string { if len(enabled) == 0 { return "disabled" } return strings.Join(enabled, ", ") } func formatHostScope(hosts []string) string { if attachAllowsNonLocalHosts(hosts) { return "external hosts allowed" } return "local-only" } func formatWhitelistStatus(cfg *config.RuntimeConfig) string { if !cfg.IDPI.Enabled { return "disabled" } if len(cfg.IDPI.AllowedDomains) == 0 { return "not set" } if allowsAllDomains(cfg.IDPI.AllowedDomains) { return "wildcard" } return strings.Join(cfg.IDPI.AllowedDomains, ", ") } func formatStrictModeStatus(cfg *config.RuntimeConfig) string { if !cfg.IDPI.Enabled { return "disabled" } if cfg.IDPI.StrictMode { return "enforcing" } return "warn-only" } func formatContentGuardStatus(cfg *config.RuntimeConfig) string { if !cfg.IDPI.Enabled { return "disabled" } if cfg.IDPI.ScanContent || cfg.IDPI.WrapContent { return "active" } return "disabled" }