WitNote / cmd /pinchtab /cmd_security.go
AUXteam's picture
Upload folder using huggingface_hub
6a7089a verified
package main
import (
"encoding/json"
"fmt"
"os"
"slices"
"strings"
"github.com/pinchtab/pinchtab/internal/cli"
"github.com/pinchtab/pinchtab/internal/config"
"github.com/spf13/cobra"
)
var securityCmd = &cobra.Command{
Use: "security",
Short: "Review runtime security posture",
Long: "Shows runtime security posture and offers to restore recommended security defaults.",
Run: func(cmd *cobra.Command, args []string) {
cfg := loadConfig()
handleSecurityCommand(cfg)
},
}
func init() {
securityCmd.GroupID = "config"
securityCmd.AddCommand(&cobra.Command{
Use: "up",
Short: "Apply recommended security defaults",
Run: func(cmd *cobra.Command, args []string) {
handleSecurityUpCommand()
},
})
securityCmd.AddCommand(&cobra.Command{
Use: "down",
Short: "Lower guards while keeping loopback bind and API auth enabled",
Run: func(cmd *cobra.Command, args []string) {
handleSecurityDownCommand()
},
})
rootCmd.AddCommand(securityCmd)
}
func handleSecurityCommand(cfg *config.RuntimeConfig) {
interactive := isInteractiveTerminal()
for {
posture := cli.AssessSecurityPosture(cfg)
warnings := cli.AssessSecurityWarnings(cfg)
recommended := cli.RecommendedSecurityDefaultLines(cfg)
printSecuritySummary(posture, interactive)
if len(warnings) > 0 {
fmt.Println()
fmt.Println(cli.StyleStdout(cli.HeadingStyle, "Warnings"))
fmt.Println()
for _, warning := range warnings {
fmt.Printf(" - %s\n", cli.StyleStdout(cli.WarningStyle, warning.Message))
for i := 0; i+1 < len(warning.Attrs); i += 2 {
key, ok := warning.Attrs[i].(string)
if !ok || key == "hint" {
continue
}
fmt.Printf(" %s: %s\n", cli.StyleStdout(cli.MutedStyle, key), cli.StyleStdout(cli.ValueStyle, formatSecurityValue(warning.Attrs[i+1])))
}
for i := 0; i+1 < len(warning.Attrs); i += 2 {
key, ok := warning.Attrs[i].(string)
if ok && key == "hint" {
fmt.Printf(" %s: %s\n", cli.StyleStdout(cli.MutedStyle, "hint"), cli.StyleStdout(cli.ValueStyle, formatSecurityValue(warning.Attrs[i+1])))
}
}
}
}
if len(recommended) == 0 && len(warnings) == 0 {
fmt.Println()
fmt.Println(" " + cli.StyleStdout(cli.SuccessStyle, "All recommended security defaults are active."))
} else if len(recommended) > 0 {
fmt.Println()
fmt.Println(cli.StyleStdout(cli.HeadingStyle, "Recommended defaults"))
fmt.Println()
printRecommendedSecurityDefaults(recommended)
}
if !interactive {
if len(recommended) > 0 {
fmt.Println()
fmt.Println(cli.StyleStdout(cli.MutedStyle, "Interactive editing skipped because stdin/stdout is not a terminal."))
}
return
}
nextCfg, changed, done, err := promptSecurityEdit(cfg, posture, len(recommended) > 0)
if err != nil {
fmt.Fprintln(os.Stderr, cli.StyleStderr(cli.ErrorStyle, err.Error()))
os.Exit(1)
}
if done {
return
}
if !changed {
fmt.Println()
fmt.Println(cli.StyleStdout(cli.MutedStyle, "No changes made."))
return
}
cfg = nextCfg
fmt.Println()
}
}
func formatSecurityValue(value any) string {
switch v := value.(type) {
case []string:
return strings.Join(v, ", ")
default:
return fmt.Sprint(v)
}
}
func printRecommendedSecurityDefaults(lines []string) {
for _, line := range lines {
fmt.Printf(" - %s\n", cli.StyleStdout(cli.ValueStyle, line))
}
}
func printSecuritySummary(posture cli.SecurityPosture, interactive bool) {
fmt.Println(cli.StyleStdout(cli.HeadingStyle, "Security"))
fmt.Println()
fmt.Printf(" %s %s\n", posture.Bar, cli.StyleStdout(cli.ValueStyle, posture.Level))
for i, check := range posture.Checks {
indicator := cli.StyleStdout(cli.WarningStyle, "!!")
if check.Passed {
indicator = cli.StyleStdout(cli.SuccessStyle, "ok")
}
if interactive {
fmt.Printf(" %d. %s %-20s %s\n", i+1, indicator, check.Label, check.Detail)
continue
}
fmt.Printf(" %s %-20s %s\n", indicator, check.Label, check.Detail)
}
}
func promptSecurityEdit(cfg *config.RuntimeConfig, posture cli.SecurityPosture, canRestoreDefaults bool) (*config.RuntimeConfig, bool, bool, error) {
fmt.Println()
prompt := "Edit item (1-8"
if canRestoreDefaults {
prompt += ", u = security up"
}
prompt += ", d = security down, blank to exit):"
choice, err := promptInput(cli.StyleStdout(cli.HeadingStyle, prompt), "")
if err != nil {
return nil, false, false, err
}
choice = strings.ToLower(strings.TrimSpace(choice))
if choice == "" {
return nil, false, true, nil
}
if (choice == "u" || choice == "up") && canRestoreDefaults {
nextCfg, changed, err := applySecurityUp()
return nextCfg, changed, false, err
}
if choice == "d" || choice == "down" {
nextCfg, changed, err := applySecurityDown()
return nextCfg, changed, false, err
}
index := strings.TrimSpace(choice)
for i, check := range posture.Checks {
if index == fmt.Sprint(i+1) {
nextCfg, changed, err := editSecurityCheck(cfg, check)
return nextCfg, changed, false, err
}
}
return nil, false, false, fmt.Errorf("invalid selection %q", choice)
}
func editSecurityCheck(cfg *config.RuntimeConfig, check cli.SecurityPostureCheck) (*config.RuntimeConfig, bool, error) {
switch check.ID {
case "bind_loopback":
value, err := promptInput("Set server.bind:", cfg.Bind)
if err != nil {
return nil, false, err
}
return updateConfigValue("server.bind", value)
case "api_auth_enabled":
picked, err := promptSelect("API authentication", []menuOption{
{label: "Generate new token (Recommended)", value: "generate"},
{label: "Set custom token", value: "custom"},
{label: "Disable token", value: "disable"},
{label: "Cancel", value: "cancel"},
})
if err != nil || picked == "" || picked == "cancel" {
return cfg, false, nil
}
switch picked {
case "generate":
token, err := config.GenerateAuthToken()
if err != nil {
return nil, false, err
}
return updateConfigValue("server.token", token)
case "custom":
token, err := promptInput("Set server.token:", cfg.Token)
if err != nil {
return nil, false, err
}
return updateConfigValue("server.token", token)
case "disable":
return updateConfigValue("server.token", "")
}
case "sensitive_endpoints_disabled":
current := strings.Join(cfg.EnabledSensitiveEndpoints(), ",")
value, err := promptInput("Enable sensitive endpoints (evaluate,macro,screencast,download,upload; blank = disable all):", current)
if err != nil {
return nil, false, err
}
return updateSensitiveEndpoints(value)
case "attach_disabled":
picked, err := promptSelect("Attach endpoint", []menuOption{
{label: "Disable (Recommended)", value: "disable"},
{label: "Enable", value: "enable"},
{label: "Cancel", value: "cancel"},
})
if err != nil || picked == "" || picked == "cancel" {
return cfg, false, nil
}
return updateConfigValue("security.attach.enabled", fmt.Sprintf("%t", picked == "enable"))
case "attach_local_only":
value, err := promptInput("Set security.attach.allowHosts (comma-separated):", strings.Join(cfg.AttachAllowHosts, ","))
if err != nil {
return nil, false, err
}
return updateConfigValue("security.attach.allowHosts", value)
case "idpi_whitelist_scoped":
value, err := promptInput("Set security.idpi.allowedDomains (comma-separated):", strings.Join(cfg.IDPI.AllowedDomains, ","))
if err != nil {
return nil, false, err
}
return updateConfigValue("security.idpi.allowedDomains", value)
case "idpi_strict_mode":
picked, err := promptSelect("IDPI strict mode", []menuOption{
{label: "Enforce (Recommended)", value: "true"},
{label: "Warn only", value: "false"},
{label: "Cancel", value: "cancel"},
})
if err != nil || picked == "" || picked == "cancel" {
return cfg, false, nil
}
return updateConfigValue("security.idpi.strictMode", picked)
case "idpi_content_protection":
picked, err := promptSelect("IDPI content guard", []menuOption{
{label: "Active: scan + wrap (Recommended)", value: "both"},
{label: "Scan only", value: "scan"},
{label: "Wrap only", value: "wrap"},
{label: "Disable", value: "off"},
{label: "Cancel", value: "cancel"},
})
if err != nil || picked == "" || picked == "cancel" {
return cfg, false, nil
}
return updateContentGuard(picked)
}
return cfg, false, nil
}
func updateConfigValue(path, value string) (*config.RuntimeConfig, bool, error) {
fc, configPath, err := config.LoadFileConfig()
if err != nil {
return nil, false, fmt.Errorf("load config: %w", err)
}
if err := config.SetConfigValue(fc, path, value); err != nil {
return nil, false, fmt.Errorf("set %s: %w", path, err)
}
if errs := config.ValidateFileConfig(fc); len(errs) > 0 {
return nil, false, errs[0]
}
if err := config.SaveFileConfig(fc, configPath); err != nil {
return nil, false, fmt.Errorf("save config: %w", err)
}
return config.Load(), true, nil
}
func updateSensitiveEndpoints(value string) (*config.RuntimeConfig, bool, error) {
fc, configPath, err := config.LoadFileConfig()
if err != nil {
return nil, false, fmt.Errorf("load config: %w", err)
}
selected := map[string]bool{}
for _, item := range splitCommaList(value) {
selected[item] = true
}
for endpoint, path := range map[string]string{
"evaluate": "security.allowEvaluate",
"macro": "security.allowMacro",
"screencast": "security.allowScreencast",
"download": "security.allowDownload",
"upload": "security.allowUpload",
} {
enabled := selected[endpoint]
if err := config.SetConfigValue(fc, path, fmt.Sprintf("%t", enabled)); err != nil {
return nil, false, fmt.Errorf("set %s: %w", endpoint, err)
}
}
if errs := config.ValidateFileConfig(fc); len(errs) > 0 {
return nil, false, errs[0]
}
if err := config.SaveFileConfig(fc, configPath); err != nil {
return nil, false, fmt.Errorf("save config: %w", err)
}
return config.Load(), true, nil
}
func updateContentGuard(mode string) (*config.RuntimeConfig, bool, error) {
fc, configPath, err := config.LoadFileConfig()
if err != nil {
return nil, false, fmt.Errorf("load config: %w", err)
}
scan := mode == "both" || mode == "scan"
wrap := mode == "both" || mode == "wrap"
for _, item := range []struct {
path string
value bool
}{
{path: "security.idpi.scanContent", value: scan},
{path: "security.idpi.wrapContent", value: wrap},
} {
if err := config.SetConfigValue(fc, item.path, fmt.Sprintf("%t", item.value)); err != nil {
return nil, false, fmt.Errorf("set %s: %w", item.path, err)
}
}
if errs := config.ValidateFileConfig(fc); len(errs) > 0 {
return nil, false, errs[0]
}
if err := config.SaveFileConfig(fc, configPath); err != nil {
return nil, false, fmt.Errorf("save config: %w", err)
}
return config.Load(), true, nil
}
func applyGuardsDownPreset() (*config.RuntimeConfig, string, bool, error) {
fc, configPath, err := config.LoadFileConfig()
if err != nil {
return nil, "", false, fmt.Errorf("load config: %w", err)
}
originalJSON, err := formatFileConfigJSON(fc)
if err != nil {
return nil, "", false, err
}
original, err := config.GetConfigValue(fc, "server.token")
if err != nil {
return nil, "", false, fmt.Errorf("read server.token: %w", err)
}
if strings.TrimSpace(original) == "" {
token, err := config.GenerateAuthToken()
if err != nil {
return nil, "", false, fmt.Errorf("generate token: %w", err)
}
if err := config.SetConfigValue(fc, "server.token", token); err != nil {
return nil, "", false, fmt.Errorf("set server.token: %w", err)
}
}
for _, item := range []struct {
path string
value string
}{
{path: "server.bind", value: "127.0.0.1"},
{path: "security.allowEvaluate", value: "true"},
{path: "security.allowMacro", value: "true"},
{path: "security.allowScreencast", value: "true"},
{path: "security.allowDownload", value: "true"},
{path: "security.allowUpload", value: "true"},
{path: "security.attach.enabled", value: "true"},
{path: "security.attach.allowHosts", value: "127.0.0.1,localhost,::1"},
{path: "security.attach.allowSchemes", value: "ws,wss"},
{path: "security.idpi.enabled", value: "false"},
{path: "security.idpi.strictMode", value: "false"},
{path: "security.idpi.scanContent", value: "false"},
{path: "security.idpi.wrapContent", value: "false"},
} {
if err := config.SetConfigValue(fc, item.path, item.value); err != nil {
return nil, "", false, fmt.Errorf("set %s: %w", item.path, err)
}
}
if errs := config.ValidateFileConfig(fc); len(errs) > 0 {
return nil, "", false, errs[0]
}
nextJSON, err := formatFileConfigJSON(fc)
if err != nil {
return nil, "", false, err
}
changed := originalJSON != nextJSON
if !changed {
return config.Load(), configPath, false, nil
}
if err := config.SaveFileConfig(fc, configPath); err != nil {
return nil, "", false, fmt.Errorf("save config: %w", err)
}
return config.Load(), configPath, true, nil
}
func applySecurityUp() (*config.RuntimeConfig, bool, error) {
configPath, changed, err := cli.RestoreSecurityDefaults()
if err != nil {
return nil, false, fmt.Errorf("restore defaults: %w", err)
}
if !changed {
fmt.Println(cli.StyleStdout(cli.MutedStyle, fmt.Sprintf("Security defaults already match %s", configPath)))
return config.Load(), false, nil
}
fmt.Println(cli.StyleStdout(cli.SuccessStyle, fmt.Sprintf("Security defaults restored in %s", configPath)))
fmt.Println(cli.StyleStdout(cli.MutedStyle, "Restart PinchTab to apply file-based changes."))
return config.Load(), true, nil
}
func applySecurityDown() (*config.RuntimeConfig, bool, error) {
nextCfg, configPath, changed, err := applyGuardsDownPreset()
if err != nil {
return nil, false, fmt.Errorf("guards down: %w", err)
}
if !changed {
fmt.Println(cli.StyleStdout(cli.MutedStyle, fmt.Sprintf("Guards down preset already matches %s", configPath)))
return nextCfg, false, nil
}
fmt.Println(cli.StyleStdout(cli.WarningStyle, fmt.Sprintf("Guards down preset applied in %s", configPath)))
fmt.Println(cli.StyleStdout(cli.MutedStyle, "Loopback bind and API auth remain enabled; sensitive endpoints and attach are enabled, IDPI is disabled."))
return nextCfg, true, nil
}
func handleSecurityUpCommand() {
if _, _, err := applySecurityUp(); err != nil {
fmt.Fprintln(os.Stderr, cli.StyleStderr(cli.ErrorStyle, err.Error()))
os.Exit(1)
}
}
func handleSecurityDownCommand() {
if _, _, err := applySecurityDown(); err != nil {
fmt.Fprintln(os.Stderr, cli.StyleStderr(cli.ErrorStyle, err.Error()))
os.Exit(1)
}
}
func formatFileConfigJSON(fc *config.FileConfig) (string, error) {
data, err := json.Marshal(fc)
if err != nil {
return "", fmt.Errorf("marshal config: %w", err)
}
return string(data), nil
}
func splitCommaList(value string) []string {
parts := strings.Split(value, ",")
items := make([]string, 0, len(parts))
for _, part := range parts {
trimmed := strings.TrimSpace(strings.ToLower(part))
if trimmed != "" {
items = append(items, trimmed)
}
}
slices.Sort(items)
return slices.Compact(items)
}