Documentation
¶
Index ¶
- Variables
- func AddDropRule(ctx context.Context, chain, ip string) error
- func ClearChain(ctx context.Context, chain string) error
- func CreateChain(ctx context.Context, chain string) error
- func ExtractAndMergeIPs(lists []Blocklist, filterCloudflare bool, filterLocal bool) ([]string, error)
- func LinkChain(ctx context.Context, chain string) error
- func ReplacePlaceholders(expression string, ipv4, ipv6 string) (string, error)
- func SetLogger(l *logger.Logger)
- type AccessEmailDomainRule
- type AccessEmailRule
- type AccessEveryoneRule
- type AccessGeoRule
- type AccessGroupRule
- type AccessIPRule
- type AccessPolicy
- type AccessRule
- type AccessServiceTokenRule
- type Blocklist
- type CloudflareClient
- func (c *CloudflareClient) CreateRule(ctx context.Context, zoneID, rulesetID string, rule CloudflareRule) (*CloudflareRule, error)
- func (c *CloudflareClient) GetAccessPolicy(ctx context.Context, accountID, appID, policyID string) (*AccessPolicy, error)
- func (c *CloudflareClient) GetReusablePolicy(ctx context.Context, accountID, policyID string) (*AccessPolicy, error)
- func (c *CloudflareClient) GetRule(ctx context.Context, zoneID, rulesetID, ruleID string) (*CloudflareRule, error)
- func (c *CloudflareClient) GetRuleset(ctx context.Context, zoneID, rulesetID string) (*CloudflareRuleset, error)
- func (c *CloudflareClient) ListAccessPolicies(ctx context.Context, accountID, appID string) ([]AccessPolicy, error)
- func (c *CloudflareClient) UpdateAccessPolicy(ctx context.Context, accountID, appID, policyID string, policy AccessPolicy) (*AccessPolicy, error)
- func (c *CloudflareClient) UpdateReusablePolicy(ctx context.Context, accountID, policyID string, policy AccessPolicy) (*AccessPolicy, error)
- func (c *CloudflareClient) UpdateRule(ctx context.Context, zoneID, rulesetID, ruleID string, rule CloudflareRule) (*CloudflareRule, error)
- func (c *CloudflareClient) UpsertRule(ctx context.Context, zoneID, rulesetID, ruleID string, rule CloudflareRule) (*CloudflareRule, bool, error)
- type CloudflareError
- type CloudflarePosition
- type CloudflareResponse
- type CloudflareRule
- type CloudflareRuleset
- type SafetyManager
Constants ¶
This section is empty.
Variables ¶
var BlocklistCmd = &cobra.Command{ Use: "blocklist", Short: i18n.T(i18n.CmdBlocklistShort), Long: i18n.T(i18n.CmdBlocklistLong), }
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
var HardenCmd = &cobra.Command{ Use: "harden", Short: i18n.T(i18n.CmdHardenShort), Long: i18n.T(i18n.CmdHardenLong), }
var PortscanCmd = &cobra.Command{ Use: "portscan", Short: i18n.T(i18n.CmdPortscanShort), Long: i18n.T(i18n.CmdPortscanLong), }
var VulnscanCmd = &cobra.Command{ Use: "vulnscan", Short: i18n.T(i18n.CmdVulnscanShort), Long: i18n.T(i18n.CmdVulnscanLong), }
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 ¶
AddDropRule appends a DROP rule for the given IP in the specified chain.
func ClearChain ¶
ClearChain removes all rules from the given chain.
func CreateChain ¶
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 ReplacePlaceholders ¶ added in v1.0.5
ReplacePlaceholders replaces placeholders in expressions with actual values. Returns the replaced string and an error if required placeholders cannot be replaced.
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 ¶
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
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()