security

package
v1.0.15 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 17, 2026 License: MIT Imports: 27 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var BlocklistCmd = &cobra.Command{
	Use:   "blocklist",
	Short: i18n.T(i18n.CmdBlocklistShort),
	Long:  i18n.T(i18n.CmdBlocklistLong),
}
View Source
var CloudflareCmd = &cobra.Command{
	Use:   "cloudflare",
	Short: i18n.T("Update Cloudflare security rules"),
	Long:  i18n.T("Create or update Cloudflare WAF rules with support for IP placeholders"),
	RunE: func(cmd *cobra.Command, args []string) error {
		ctx := context.Background()

		apiToken := os.Getenv("CF_AUTH_TOKEN")
		if apiToken == "" {
			apiToken = os.Getenv("CLOUDFLARE_API_TOKEN")
		}
		if apiToken == "" {
			return fmt.Errorf("CF_AUTH_TOKEN or CLOUDFLARE_API_TOKEN environment variable is required")
		}

		if cfZoneID == "" || cfRulesetID == "" {
			return fmt.Errorf("zone-id and ruleset-id are required")
		}

		if cfAction == "" || cfExpr == "" {
			return fmt.Errorf("action and expression are required")
		}

		usesDynamic := strings.Contains(cfExpr, "{{PUBLIC_")
		requireIPv4 := usesDynamic && (strings.Contains(cfExpr, "{{PUBLIC_IP}}") || strings.Contains(cfExpr, "{{PUBLIC_IPV4"))
		needsIPv6Address := usesDynamic && (strings.Contains(cfExpr, "{{PUBLIC_IPV6}}") || strings.Contains(cfExpr, "{{PUBLIC_IPV6/") || strings.Contains(cfExpr, "{{PUBLIC_IPV6_INTERFACE}}"))
		needsIPv6Network := usesDynamic && strings.Contains(cfExpr, "{{PUBLIC_IPV6_NETWORK")

		var ipv4, ipv6 string
		ipv4 = strings.TrimSpace(cfIP)
		ipv6 = strings.TrimSpace(cfIPv6)

		if usesDynamic {
			if ipv4 != "" || ipv6 != "" {
				log.Info("Using IP data provided via flags; skipping online lookups where possible")
			} else {
				log.Info("Fetching public IP addresses...")
			}

			type ipState struct{ v4, v6 string }
			state, err := utils.Retry[ipState](ctx, utils.RetryOptions{
				Attempts:  6,
				BaseDelay: 1 * time.Second,
				MaxDelay:  8 * time.Second,
				OnRetry: func(attempt int, nextDelay time.Duration, _ error) {
					if log != nil {
						log.Infof("Public IP not ready yet; retrying in %s (%d/%d)", nextDelay, attempt, 6)
					}
				},
				ExceededError: func(attempts int, _ error) error {
					var missing []string
					if requireIPv4 {
						missing = append(missing, "IPv4")
					}
					if needsIPv6Address || needsIPv6Network {
						missing = append(missing, "IPv6")
					}
					return fmt.Errorf("unable to determine required public IP(s) after %d attempt(s): %s", attempts, strings.Join(missing, ", "))
				},
			}, func(ctx context.Context) (ipState, bool, error) {
				cur := ipState{v4: ipv4, v6: ipv6}
				if requireIPv4 && strings.TrimSpace(cur.v4) == "" {
					v4, err := utils.GetPublicIPv4(ctx)
					if err != nil {
						return cur, false, err
					}
					cur.v4 = v4
				}
				if needsIPv6Address && strings.TrimSpace(cur.v6) == "" {
					v6, err := utils.GetPublicIPv6(ctx)
					if err != nil {
						return cur, false, err
					}
					cur.v6 = v6
				}

				if needsIPv6Network && strings.TrimSpace(cur.v6) == "" {
					v6, err := utils.GetPublicIPv6(ctx)
					if err != nil {
						return cur, false, err
					}
					cur.v6 = v6
				}

				missingV4 := requireIPv4 && strings.TrimSpace(cur.v4) == ""
				missingV6 := (needsIPv6Address || needsIPv6Network) && strings.TrimSpace(cur.v6) == ""
				if missingV4 || missingV6 {
					return cur, true, nil
				}
				return cur, false, nil
			})
			if err != nil {
				return err
			}
			ipv4, ipv6 = state.v4, state.v6
		}

		if ipv4 != "" {
			log.Infof("Public IPv4: %s", ipv4)
		}
		if ipv6 != "" {
			log.Infof("Public IPv6: %s", ipv6)
		}

		if cfSkipUnchanged {
			if lastCache, err := readLastCache(); err == nil {

				ipsUnchanged := (!usesDynamic || (ipv4 == lastCache.IPv4 && ipv6 == lastCache.IPv6))
				ruleUnchanged := cfExpr == lastCache.Expression &&
					cfAction == lastCache.Action &&
					cfEnabled == lastCache.Enabled &&
					cfRuleID == lastCache.RuleID

				if ipsUnchanged && ruleUnchanged {
					log.Info("No changes detected (IPs and rule configuration unchanged); skipping Cloudflare API call")
					return nil
				}
			}
		}

		expression, err := ReplacePlaceholders(cfExpr, ipv4, ipv6)
		if err != nil {
			return fmt.Errorf("failed to replace placeholders in expression: %w", err)
		}
		if expression != cfExpr {
			log.Info("Expression after placeholder replacement:")
			log.Info(expression)
		}

		rule := CloudflareRule{
			Action:      cfAction,
			Description: cfDesc,
			Enabled:     cfEnabled,
			Expression:  expression,
		}

		if cfPosition > 0 {
			rule.Position = &CloudflarePosition{
				Index: cfPosition,
			}
		}

		client := NewCloudflareClient(apiToken)

		// Upsert rule
		var result *CloudflareRule
		var created bool

		if cfRuleID != "" {
			result, created, err = client.UpsertRule(ctx, cfZoneID, cfRulesetID, cfRuleID, rule)
		} else {

			result, err = client.CreateRule(ctx, cfZoneID, cfRulesetID, rule)
			created = true
		}

		if err != nil {
			return fmt.Errorf("failed to upsert rule: %w", err)
		}

		if created {
			log.Info("✓ Rule created successfully!")
		} else {
			log.Info("✓ Rule updated successfully!")
		}

		log.Infof("Rule ID: %s", result.ID)
		log.Infof("Description: %s", result.Description)
		log.Infof("Action: %s", result.Action)
		log.Infof("Enabled: %v", result.Enabled)

		if cfSkipUnchanged {
			cache := &cloudflareCache{
				IPv4:       ipv4,
				IPv6:       ipv6,
				Expression: cfExpr,
				Action:     cfAction,
				Enabled:    cfEnabled,
				RuleID:     cfRuleID,
			}
			_ = writeCache(cache)
		}

		return nil
	},
}

CloudflareCmd represents the cloudflare command

View Source
var HardenCmd = &cobra.Command{
	Use:   "harden",
	Short: i18n.T(i18n.CmdHardenShort),
	Long:  i18n.T(i18n.CmdHardenLong),
}
View Source
var PortscanCmd = &cobra.Command{
	Use:   "portscan",
	Short: i18n.T(i18n.CmdPortscanShort),
	Long:  i18n.T(i18n.CmdPortscanLong),
}
View Source
var VulnscanCmd = &cobra.Command{
	Use:   "vulnscan",
	Short: i18n.T(i18n.CmdVulnscanShort),
	Long:  i18n.T(i18n.CmdVulnscanLong),
}
View Source
var ZeroTrustPolicyCmd = &cobra.Command{
	Use:   "zerotrust-policy",
	Short: i18n.T("Update Zero Trust Access Application Policy"),
	Long: i18n.T(`Update a Zero Trust Access Application Policy with dynamic IP support.

This command updates an existing Access policy for a Zero Trust application.
It supports both app-specific policies and reusable (account-level) policies.

For app-specific policies, provide both --account-id and --app-id.
For reusable policies, use --reusable flag (no --app-id needed).

Supported placeholders in IP rules:
  {{PUBLIC_IP}}, {{PUBLIC_IPV4}} - Current public IPv4 address
  {{PUBLIC_IPV6}} - Current public IPv6 address
  {{PUBLIC_IPV4/24}}, {{PUBLIC_IPV6/64}} - CIDR notation
  {{PUBLIC_IPV6_NETWORK/64}} - IPv6 network prefix`),
	RunE: func(cmd *cobra.Command, args []string) error {
		ctx := context.Background()

		apiToken := os.Getenv("CF_AUTH_TOKEN")
		if apiToken == "" {
			apiToken = os.Getenv("CLOUDFLARE_API_TOKEN")
		}
		if apiToken == "" {
			return fmt.Errorf("CF_AUTH_TOKEN or CLOUDFLARE_API_TOKEN environment variable is required")
		}

		if ztAccountID == "" {
			return fmt.Errorf("account-id is required")
		}
		if !ztReusable && ztAppID == "" {
			return fmt.Errorf("app-id is required for app-specific policies (or use --reusable for reusable policies)")
		}
		if ztPolicyID == "" {
			return fmt.Errorf("policy-id is required")
		}

		needsIPv4 := false
		needsIPv6 := false
		allIPs := append(append([]string{}, ztIncludeIPs...), ztExcludeIPs...)
		for _, ip := range allIPs {
			if strings.Contains(ip, "{{PUBLIC_IPV4") || strings.Contains(ip, "{{PUBLIC_IP}}") {
				needsIPv4 = true
			}
			if strings.Contains(ip, "{{PUBLIC_IPV6") {
				needsIPv6 = true
			}
		}

		var ipv4, ipv6 string
		ipv4 = strings.TrimSpace(ztIP)
		ipv6 = strings.TrimSpace(ztIPv6)

		if needsIPv4 && ipv4 == "" {
			log.Info("Fetching public IPv4 address...")
			var err error
			ipv4, err = utils.GetPublicIPv4(ctx)
			if err != nil {
				log.Warnf("Could not fetch IPv4: %v", err)
			}
		}
		if needsIPv6 && ipv6 == "" {
			log.Info("Fetching public IPv6 address...")
			var err error
			ipv6, err = utils.GetPublicIPv6(ctx)
			if err != nil {
				log.Warnf("Could not fetch IPv6: %v", err)
			}
		}

		if ipv4 != "" {
			log.Infof("Public IPv4: %s", ipv4)
		}
		if ipv6 != "" {
			log.Infof("Public IPv6: %s", ipv6)
		}

		// Build include rules
		var includeRules []AccessRule
		for _, ip := range ztIncludeIPs {
			resolvedIP, err := ReplacePlaceholders(ip, ipv4, ipv6)
			if err != nil {
				return fmt.Errorf("failed to replace placeholders in include IP %q: %w", ip, err)
			}
			includeRules = append(includeRules, AccessRule{
				IP: &AccessIPRule{IP: resolvedIP},
			})
		}
		for _, email := range ztIncludeEmails {
			includeRules = append(includeRules, AccessRule{
				Email: &AccessEmailRule{Email: email},
			})
		}
		for _, groupID := range ztIncludeGroups {
			includeRules = append(includeRules, AccessRule{
				Group: &AccessGroupRule{ID: groupID},
			})
		}

		// Build exclude rules
		var excludeRules []AccessRule
		for _, ip := range ztExcludeIPs {
			resolvedIP, err := ReplacePlaceholders(ip, ipv4, ipv6)
			if err != nil {
				return fmt.Errorf("failed to replace placeholders in exclude IP %q: %w", ip, err)
			}
			excludeRules = append(excludeRules, AccessRule{
				IP: &AccessIPRule{IP: resolvedIP},
			})
		}

		client := NewCloudflareClient(apiToken)

		var existingPolicy *AccessPolicy
		var err error

		if ztReusable {
			log.Info("Using reusable policy endpoint...")
			existingPolicy, err = client.GetReusablePolicy(ctx, ztAccountID, ztPolicyID)
		} else {
			existingPolicy, err = client.GetAccessPolicy(ctx, ztAccountID, ztAppID, ztPolicyID)
		}
		if err != nil {
			return fmt.Errorf("failed to get existing policy: %w", err)
		}

		policy := AccessPolicy{
			ID:              ztPolicyID,
			Name:            existingPolicy.Name,
			Decision:        existingPolicy.Decision,
			Include:         existingPolicy.Include,
			Exclude:         existingPolicy.Exclude,
			Require:         existingPolicy.Require,
			SessionDuration: existingPolicy.SessionDuration,
			Precedence:      existingPolicy.Precedence,
		}

		if ztPolicyName != "" {
			policy.Name = ztPolicyName
		}
		if ztDecision != "" {
			policy.Decision = ztDecision
		}
		if len(includeRules) > 0 {
			policy.Include = includeRules
		}
		if len(excludeRules) > 0 {
			policy.Exclude = excludeRules
		}
		if ztSessionDur != "" {
			policy.SessionDuration = ztSessionDur
		}
		if ztPrecedence > 0 {
			policy.Precedence = ztPrecedence
		}

		log.Infof("Updating Zero Trust Access Policy: %s", policy.Name)

		var updatedPolicy *AccessPolicy
		if ztReusable {
			updatedPolicy, err = client.UpdateReusablePolicy(ctx, ztAccountID, ztPolicyID, policy)
		} else {
			updatedPolicy, err = client.UpdateAccessPolicy(ctx, ztAccountID, ztAppID, ztPolicyID, policy)
		}
		if err != nil {
			return fmt.Errorf("failed to update policy: %w", err)
		}

		log.Info("✓ Policy updated successfully!")
		log.Infof("Policy ID: %s", updatedPolicy.ID)
		log.Infof("Name: %s", updatedPolicy.Name)
		log.Infof("Decision: %s", updatedPolicy.Decision)
		log.Infof("Include rules: %d", len(updatedPolicy.Include))
		if len(updatedPolicy.Exclude) > 0 {
			log.Infof("Exclude rules: %d", len(updatedPolicy.Exclude))
		}

		return nil
	},
}

ZeroTrustPolicyCmd represents the zerotrust-policy subcommand

Functions

func AddDropRule

func AddDropRule(ctx context.Context, chain, ip string) error

AddDropRule appends a DROP rule for the given IP in the specified chain.

func ClearChain

func ClearChain(ctx context.Context, chain string) error

ClearChain removes all rules from the given chain.

func CreateChain

func CreateChain(ctx context.Context, chain string) error

CreateChain creates a new chain if it does not already exist.

func ExtractAndMergeIPs

func ExtractAndMergeIPs(lists []Blocklist, filterCloudflare bool, filterLocal bool) ([]string, error)

ExtractAndMergeIPs extracts IPs from provided blocklists and applies optional filters.

func LinkChain

func LinkChain(ctx context.Context, chain string) error

LinkChain ensures the custom chain is referenced at the top of INPUT.

func ReplacePlaceholders added in v1.0.5

func ReplacePlaceholders(expression string, ipv4, ipv6 string) (string, error)

ReplacePlaceholders replaces placeholders in expressions with actual values. Returns the replaced string and an error if required placeholders cannot be replaced.

func SetLogger

func SetLogger(l *logger.Logger)

SetLogger sets the logger instance for the security package

Types

type AccessEmailDomainRule added in v1.0.14

type AccessEmailDomainRule struct {
	Domain string `json:"domain"`
}

AccessEmailDomainRule represents an email domain-based access rule

type AccessEmailRule added in v1.0.14

type AccessEmailRule struct {
	Email string `json:"email"`
}

AccessEmailRule represents an email-based access rule

type AccessEveryoneRule added in v1.0.14

type AccessEveryoneRule struct{}

AccessEveryoneRule matches all users

type AccessGeoRule added in v1.0.14

type AccessGeoRule struct {
	CountryCode string `json:"country_code"`
}

AccessGeoRule represents a geographic access rule

type AccessGroupRule added in v1.0.14

type AccessGroupRule struct {
	ID string `json:"id"`
}

AccessGroupRule represents a group-based access rule

type AccessIPRule added in v1.0.14

type AccessIPRule struct {
	IP string `json:"ip"`
}

AccessIPRule represents an IP-based access rule

type AccessPolicy added in v1.0.14

type AccessPolicy struct {
	ID                string       `json:"id,omitempty"`
	Name              string       `json:"name"`
	Decision          string       `json:"decision"`
	Precedence        int          `json:"precedence,omitempty"`
	Include           []AccessRule `json:"include"`
	Exclude           []AccessRule `json:"exclude,omitempty"`
	Require           []AccessRule `json:"require,omitempty"`
	SessionDuration   string       `json:"session_duration,omitempty"`
	PurposeJustReq    bool         `json:"purpose_justification_required,omitempty"`
	PurposeJustPrompt string       `json:"purpose_justification_prompt,omitempty"`
	ApprovalRequired  bool         `json:"approval_required,omitempty"`
	CreatedAt         string       `json:"created_at,omitempty"`
	UpdatedAt         string       `json:"updated_at,omitempty"`
}

AccessPolicy represents a Zero Trust Access Application Policy

type AccessRule added in v1.0.14

type AccessRule struct {
	IP           *AccessIPRule           `json:"ip,omitempty"`
	Email        *AccessEmailRule        `json:"email,omitempty"`
	EmailDomain  *AccessEmailDomainRule  `json:"email_domain,omitempty"`
	Everyone     *AccessEveryoneRule     `json:"everyone,omitempty"`
	Group        *AccessGroupRule        `json:"group,omitempty"`
	ServiceToken *AccessServiceTokenRule `json:"service_token,omitempty"`
	Geo          *AccessGeoRule          `json:"geo,omitempty"`
}

AccessRule represents a rule condition for Zero Trust Access policies

type AccessServiceTokenRule added in v1.0.14

type AccessServiceTokenRule struct {
	TokenID string `json:"token_id"`
}

AccessServiceTokenRule represents a service token-based access rule

type Blocklist

type Blocklist struct {
	Name string
	URL  string
}

func GetBlocklists

func GetBlocklists() []Blocklist

type CloudflareClient added in v1.0.5

type CloudflareClient struct {
	// contains filtered or unexported fields
}

CloudflareClient handles Cloudflare API interactions

func NewCloudflareClient added in v1.0.5

func NewCloudflareClient(apiToken string) *CloudflareClient

NewCloudflareClient creates a new Cloudflare API client

func (*CloudflareClient) CreateRule added in v1.0.5

func (c *CloudflareClient) CreateRule(ctx context.Context, zoneID, rulesetID string, rule CloudflareRule) (*CloudflareRule, error)

CreateRule creates a new rule in a ruleset

func (*CloudflareClient) GetAccessPolicy added in v1.0.14

func (c *CloudflareClient) GetAccessPolicy(ctx context.Context, accountID, appID, policyID string) (*AccessPolicy, error)

GetAccessPolicy retrieves a Zero Trust Access Application Policy

func (*CloudflareClient) GetReusablePolicy added in v1.0.14

func (c *CloudflareClient) GetReusablePolicy(ctx context.Context, accountID, policyID string) (*AccessPolicy, error)

GetReusablePolicy retrieves a Zero Trust Access Reusable Policy (account-level)

func (*CloudflareClient) GetRule added in v1.0.5

func (c *CloudflareClient) GetRule(ctx context.Context, zoneID, rulesetID, ruleID string) (*CloudflareRule, error)

GetRule retrieves a specific rule from a ruleset

func (*CloudflareClient) GetRuleset added in v1.0.5

func (c *CloudflareClient) GetRuleset(ctx context.Context, zoneID, rulesetID string) (*CloudflareRuleset, error)

GetRuleset retrieves a ruleset by ID

func (*CloudflareClient) ListAccessPolicies added in v1.0.14

func (c *CloudflareClient) ListAccessPolicies(ctx context.Context, accountID, appID string) ([]AccessPolicy, error)

ListAccessPolicies retrieves all policies for a Zero Trust Access Application

func (*CloudflareClient) UpdateAccessPolicy added in v1.0.14

func (c *CloudflareClient) UpdateAccessPolicy(ctx context.Context, accountID, appID, policyID string, policy AccessPolicy) (*AccessPolicy, error)

UpdateAccessPolicy updates a Zero Trust Access Application Policy

func (*CloudflareClient) UpdateReusablePolicy added in v1.0.14

func (c *CloudflareClient) UpdateReusablePolicy(ctx context.Context, accountID, policyID string, policy AccessPolicy) (*AccessPolicy, error)

UpdateReusablePolicy updates a Zero Trust Access Reusable Policy (account-level)

func (*CloudflareClient) UpdateRule added in v1.0.5

func (c *CloudflareClient) UpdateRule(ctx context.Context, zoneID, rulesetID, ruleID string, rule CloudflareRule) (*CloudflareRule, error)

UpdateRule updates an existing rule

func (*CloudflareClient) UpsertRule added in v1.0.5

func (c *CloudflareClient) UpsertRule(ctx context.Context, zoneID, rulesetID, ruleID string, rule CloudflareRule) (*CloudflareRule, bool, error)

UpsertRule creates or updates a rule intelligently

type CloudflareError added in v1.0.5

type CloudflareError struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

CloudflareError represents Cloudflare API error

type CloudflarePosition added in v1.0.5

type CloudflarePosition struct {
	Index  int    `json:"index,omitempty"`
	Before string `json:"before,omitempty"`
	After  string `json:"after,omitempty"`
}

CloudflarePosition represents rule position in ruleset

type CloudflareResponse added in v1.0.5

type CloudflareResponse struct {
	Success  bool              `json:"success"`
	Errors   []CloudflareError `json:"errors"`
	Messages []string          `json:"messages"`
	Result   json.RawMessage   `json:"result"`
}

CloudflareResponse represents Cloudflare API response

type CloudflareRule added in v1.0.5

type CloudflareRule struct {
	ID          string              `json:"id,omitempty"`
	Action      string              `json:"action"`
	Description string              `json:"description"`
	Enabled     bool                `json:"enabled"`
	Expression  string              `json:"expression"`
	Ref         string              `json:"ref,omitempty"`
	LastUpdated string              `json:"last_updated,omitempty"`
	Version     string              `json:"version,omitempty"`
	Position    *CloudflarePosition `json:"position,omitempty"`
}

CloudflareRule represents a Cloudflare WAF rule

type CloudflareRuleset added in v1.0.5

type CloudflareRuleset struct {
	ID          string           `json:"id"`
	Name        string           `json:"name"`
	Description string           `json:"description"`
	Kind        string           `json:"kind"`
	Phase       string           `json:"phase"`
	Rules       []CloudflareRule `json:"rules"`
}

CloudflareRuleset represents a Cloudflare ruleset

type SafetyManager

type SafetyManager struct {
	// contains filtered or unexported fields
}

SafetyManager handles automatic revert of blocklist changes if connection is lost.

func NewSafetyManager

func NewSafetyManager(chain string, delay time.Duration) *SafetyManager

func (*SafetyManager) Refresh

func (sm *SafetyManager) Refresh()

func (*SafetyManager) Start

func (sm *SafetyManager) Start() error

func (*SafetyManager) Stop

func (sm *SafetyManager) Stop()

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL