Compare commits
2 Commits
main
...
expand-geo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e53e782223 | ||
|
|
108caf2fe6 |
@@ -10,22 +10,9 @@ class RulesController < ApplicationController
|
|||||||
|
|
||||||
# GET /rules
|
# GET /rules
|
||||||
def index
|
def index
|
||||||
# Start with base scope
|
@pagy, @rules = pagy(policy_scope(Rule).includes(:user, :network_range).order(created_at: :desc))
|
||||||
rules = policy_scope(Rule).includes(:user, :network_range)
|
|
||||||
|
|
||||||
# Apply status filter
|
|
||||||
rules = apply_status_filter(rules)
|
|
||||||
|
|
||||||
# Order by creation date (newest first)
|
|
||||||
rules = rules.order(created_at: :desc)
|
|
||||||
|
|
||||||
# Paginate results
|
|
||||||
@pagy, @rules = pagy(rules)
|
|
||||||
|
|
||||||
# Load filter options for view
|
|
||||||
@waf_rule_types = Rule.waf_rule_types
|
@waf_rule_types = Rule.waf_rule_types
|
||||||
@waf_actions = Rule.waf_actions
|
@waf_actions = Rule.waf_actions
|
||||||
@current_status = params[:status] || 'all'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /rules/new
|
# GET /rules/new
|
||||||
@@ -130,21 +117,6 @@ class RulesController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def apply_status_filter(rules)
|
|
||||||
case params[:status]
|
|
||||||
when 'enabled'
|
|
||||||
rules.enabled
|
|
||||||
when 'disabled'
|
|
||||||
rules.disabled
|
|
||||||
when 'active'
|
|
||||||
rules.active
|
|
||||||
when 'expired'
|
|
||||||
rules.expired
|
|
||||||
else
|
|
||||||
rules # 'all' or no filter
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_rule
|
def set_rule
|
||||||
@rule = Rule.find(params[:id])
|
@rule = Rule.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ class Event < ApplicationRecord
|
|||||||
results = {}
|
results = {}
|
||||||
|
|
||||||
windows.each do |label, duration|
|
windows.each do |label, duration|
|
||||||
scope = where("timestamp >= ?", duration.ago)
|
scope = where('timestamp >= ?', duration.ago)
|
||||||
|
|
||||||
stats = scope.pick(
|
stats = scope.pick(
|
||||||
Arel.sql(<<~SQL.squish)
|
Arel.sql(<<~SQL.squish)
|
||||||
@@ -184,7 +184,7 @@ class Event < ApplicationRecord
|
|||||||
return request_path if request_segment_ids.blank?
|
return request_path if request_segment_ids.blank?
|
||||||
|
|
||||||
segments = PathSegment.where(id: request_segment_ids).index_by(&:id)
|
segments = PathSegment.where(id: request_segment_ids).index_by(&:id)
|
||||||
"/" + request_segment_ids.map { |id| segments[id]&.segment }.compact.join("/")
|
'/' + request_segment_ids.map { |id| segments[id]&.segment }.compact.join('/')
|
||||||
end
|
end
|
||||||
|
|
||||||
# Extract key fields from payload before saving
|
# Extract key fields from payload before saving
|
||||||
@@ -370,19 +370,19 @@ class Event < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def blocked?
|
def blocked?
|
||||||
waf_action == "deny" # deny = 0
|
waf_action == 'deny' # deny = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
def allowed?
|
def allowed?
|
||||||
waf_action == "allow" # allow = 1
|
waf_action == 'allow' # allow = 1
|
||||||
end
|
end
|
||||||
|
|
||||||
def logged?
|
def logged?
|
||||||
waf_action == "log"
|
waf_action == 'log'
|
||||||
end
|
end
|
||||||
|
|
||||||
def challenged?
|
def challenged?
|
||||||
waf_action == "challenge"
|
waf_action == 'challenge'
|
||||||
end
|
end
|
||||||
|
|
||||||
def rule_matched?
|
def rule_matched?
|
||||||
@@ -392,7 +392,7 @@ class Event < ApplicationRecord
|
|||||||
# New path methods for normalization
|
# New path methods for normalization
|
||||||
def path_segments
|
def path_segments
|
||||||
return [] unless request_path.present?
|
return [] unless request_path.present?
|
||||||
request_path.split("/").reject(&:blank?)
|
request_path.split('/').reject(&:blank?)
|
||||||
end
|
end
|
||||||
|
|
||||||
def path_segments_array
|
def path_segments_array
|
||||||
@@ -401,11 +401,7 @@ class Event < ApplicationRecord
|
|||||||
|
|
||||||
def request_hostname
|
def request_hostname
|
||||||
return nil unless request_url.present?
|
return nil unless request_url.present?
|
||||||
begin
|
URI.parse(request_url).hostname rescue nil
|
||||||
URI.parse(request_url).hostname
|
|
||||||
rescue
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Tag helper methods
|
# Tag helper methods
|
||||||
@@ -424,7 +420,7 @@ class Event < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def tag_list
|
def tag_list
|
||||||
tags.join(", ")
|
tags.join(', ')
|
||||||
end
|
end
|
||||||
|
|
||||||
# Normalize headers to lower case keys during import phase
|
# Normalize headers to lower case keys during import phase
|
||||||
@@ -517,7 +513,7 @@ class Event < ApplicationRecord
|
|||||||
.where(network_range_id: range_ids)
|
.where(network_range_id: range_ids)
|
||||||
.enabled
|
.enabled
|
||||||
.includes(:network_range)
|
.includes(:network_range)
|
||||||
.order("masklen(network_ranges.network) DESC")
|
.order('masklen(network_ranges.network) DESC')
|
||||||
end
|
end
|
||||||
|
|
||||||
def active_blocking_rules
|
def active_blocking_rules
|
||||||
@@ -649,13 +645,9 @@ class Event < ApplicationRecord
|
|||||||
|
|
||||||
# Find or create the tracking network
|
# Find or create the tracking network
|
||||||
NetworkRange.find_or_create_by!(network: network_cidr) do |nr|
|
NetworkRange.find_or_create_by!(network: network_cidr) do |nr|
|
||||||
nr.source = "auto_generated"
|
nr.source = 'auto_generated'
|
||||||
nr.creation_reason = "tracking unit for IPAPI deduplication"
|
nr.creation_reason = 'tracking unit for IPAPI deduplication'
|
||||||
nr.is_datacenter = begin
|
nr.is_datacenter = NetworkRangeGenerator.datacenter_ip?(ip_addr) rescue false
|
||||||
NetworkRangeGenerator.datacenter_ip?(ip_addr)
|
|
||||||
rescue
|
|
||||||
false
|
|
||||||
end
|
|
||||||
nr.is_vpn = false
|
nr.is_vpn = false
|
||||||
nr.is_proxy = false
|
nr.is_proxy = false
|
||||||
end
|
end
|
||||||
@@ -671,17 +663,17 @@ class Event < ApplicationRecord
|
|||||||
|
|
||||||
# Private and reserved ranges
|
# Private and reserved ranges
|
||||||
[
|
[
|
||||||
IPAddr.new("10.0.0.0/8"),
|
IPAddr.new('10.0.0.0/8'),
|
||||||
IPAddr.new("172.16.0.0/12"),
|
IPAddr.new('172.16.0.0/12'),
|
||||||
IPAddr.new("192.168.0.0/16"),
|
IPAddr.new('192.168.0.0/16'),
|
||||||
IPAddr.new("127.0.0.0/8"),
|
IPAddr.new('127.0.0.0/8'),
|
||||||
IPAddr.new("169.254.0.0/16"),
|
IPAddr.new('169.254.0.0/16'),
|
||||||
IPAddr.new("224.0.0.0/4"),
|
IPAddr.new('224.0.0.0/4'),
|
||||||
IPAddr.new("240.0.0.0/4"),
|
IPAddr.new('240.0.0.0/4'),
|
||||||
IPAddr.new("::1/128"),
|
IPAddr.new('::1/128'),
|
||||||
IPAddr.new("fc00::/7"),
|
IPAddr.new('fc00::/7'),
|
||||||
IPAddr.new("fe80::/10"),
|
IPAddr.new('fe80::/10'),
|
||||||
IPAddr.new("ff00::/8")
|
IPAddr.new('ff00::/8')
|
||||||
].any? { |range| range.include?(ip) }
|
].any? { |range| range.include?(ip) }
|
||||||
rescue IPAddr::InvalidAddressError
|
rescue IPAddr::InvalidAddressError
|
||||||
true # Treat invalid IPs as "reserved"
|
true # Treat invalid IPs as "reserved"
|
||||||
@@ -719,6 +711,7 @@ class Event < ApplicationRecord
|
|||||||
self.server_name = payload["server_name"]
|
self.server_name = payload["server_name"]
|
||||||
self.environment = payload["environment"]
|
self.environment = payload["environment"]
|
||||||
|
|
||||||
|
|
||||||
# Extract agent info
|
# Extract agent info
|
||||||
agent_data = payload.dig("agent") || {}
|
agent_data = payload.dig("agent") || {}
|
||||||
self.agent_version = agent_data["version"]
|
self.agent_version = agent_data["version"]
|
||||||
@@ -749,7 +742,7 @@ class Event < ApplicationRecord
|
|||||||
detector = DeviceDetector.new(user_agent)
|
detector = DeviceDetector.new(user_agent)
|
||||||
if detector.bot?
|
if detector.bot?
|
||||||
# Add bot tag with specific bot name
|
# Add bot tag with specific bot name
|
||||||
bot_name = detector.bot_name&.downcase&.gsub(/\s+/, "_") || "unknown"
|
bot_name = detector.bot_name&.downcase&.gsub(/\s+/, '_') || 'unknown'
|
||||||
add_tag("bot:#{bot_name}")
|
add_tag("bot:#{bot_name}")
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
@@ -763,23 +756,23 @@ class Event < ApplicationRecord
|
|||||||
range = NetworkRange.find_by(id: network_range_id)
|
range = NetworkRange.find_by(id: network_range_id)
|
||||||
if range
|
if range
|
||||||
# Check if the network range source indicates a bot import
|
# Check if the network range source indicates a bot import
|
||||||
if range.source&.start_with?("bot_import_")
|
if range.source&.start_with?('bot_import_')
|
||||||
# Extract bot type from source (e.g., 'bot_import_googlebot' -> 'googlebot')
|
# Extract bot type from source (e.g., 'bot_import_googlebot' -> 'googlebot')
|
||||||
bot_type = range.source.sub("bot_import_", "")
|
bot_type = range.source.sub('bot_import_', '')
|
||||||
add_tag("bot:#{bot_type}")
|
add_tag("bot:#{bot_type}")
|
||||||
add_tag("network:#{range.company&.downcase&.gsub(/\s+/, "_")}") if range.company.present?
|
add_tag("network:#{range.company&.downcase&.gsub(/\s+/, '_')}") if range.company.present?
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
# Check if the company is a known bot provider (from bot imports)
|
# Check if the company is a known bot provider (from bot imports)
|
||||||
# Common bot companies: Google, Amazon, OpenAI, Cloudflare, Microsoft, etc.
|
# Common bot companies: Google, Amazon, OpenAI, Cloudflare, Microsoft, etc.
|
||||||
known_bot_companies = ["googlebot", "google bot", "amazon", "aws", "openai",
|
known_bot_companies = ['googlebot', 'google bot', 'amazon', 'aws', 'openai',
|
||||||
"anthropic", "cloudflare", "microsoft", "facebook",
|
'anthropic', 'cloudflare', 'microsoft', 'facebook',
|
||||||
"meta", "apple", "duckduckgo"]
|
'meta', 'apple', 'duckduckgo']
|
||||||
company_lower = company&.downcase
|
company_lower = company&.downcase
|
||||||
if company_lower && known_bot_companies.any? { |bot| company_lower.include?(bot) }
|
if company_lower && known_bot_companies.any? { |bot| company_lower.include?(bot) }
|
||||||
add_tag("bot:#{company_lower.gsub(/\s+/, "_")}")
|
add_tag("bot:#{company_lower.gsub(/\s+/, '_')}")
|
||||||
add_tag("network:#{company_lower.gsub(/\s+/, "_")}")
|
add_tag("network:#{company_lower.gsub(/\s+/, '_')}")
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -791,7 +784,7 @@ class Event < ApplicationRecord
|
|||||||
if is_datacenter && user_agent.present?
|
if is_datacenter && user_agent.present?
|
||||||
# Generic/common bot user agents in datacenter networks
|
# Generic/common bot user agents in datacenter networks
|
||||||
ua_lower = user_agent.downcase
|
ua_lower = user_agent.downcase
|
||||||
bot_keywords = ["bot", "crawler", "spider", "scraper", "curl", "wget", "python", "go-http-client"]
|
bot_keywords = ['bot', 'crawler', 'spider', 'scraper', 'curl', 'wget', 'python', 'go-http-client']
|
||||||
if bot_keywords.any? { |keyword| ua_lower.include?(keyword) }
|
if bot_keywords.any? { |keyword| ua_lower.include?(keyword) }
|
||||||
add_tag("bot:datacenter")
|
add_tag("bot:datacenter")
|
||||||
add_tag("datacenter:true")
|
add_tag("datacenter:true")
|
||||||
|
|||||||
@@ -152,30 +152,60 @@ validate :targets_must_be_array
|
|||||||
def create_rule_for_network_range(network_range)
|
def create_rule_for_network_range(network_range)
|
||||||
return nil unless matches_network_range?(network_range)
|
return nil unless matches_network_range?(network_range)
|
||||||
|
|
||||||
|
# For country policies, expand to largest matching ancestor
|
||||||
|
# This consolidates /24 rules into /16, /8, etc. when possible
|
||||||
|
expanded_range = find_largest_matching_ancestor(network_range)
|
||||||
|
|
||||||
# Check for existing supernet rules before attempting to create
|
# Check for existing supernet rules before attempting to create
|
||||||
if network_range.supernet_rules.any?
|
if expanded_range.supernet_rules.any?
|
||||||
supernet = network_range.supernet_rules.first
|
supernet = expanded_range.supernet_rules.first
|
||||||
Rails.logger.debug "Skipping rule creation for #{network_range.cidr} - covered by supernet rule ##{supernet.id} (#{supernet.network_range.cidr})"
|
Rails.logger.debug "Skipping rule creation for #{expanded_range.cidr} - covered by supernet rule ##{supernet.id} (#{supernet.network_range.cidr})"
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
# Find existing rule (enabled or disabled)
|
# Try to create the rule, handling duplicates gracefully
|
||||||
existing_rule = Rule.find_by(
|
begin
|
||||||
|
rule = Rule.create!(
|
||||||
|
waf_rule_type: 'network',
|
||||||
|
waf_action: policy_action.to_sym,
|
||||||
|
network_range: expanded_range,
|
||||||
|
waf_policy: self,
|
||||||
|
user: user,
|
||||||
|
source: "policy",
|
||||||
|
metadata: build_rule_metadata(expanded_range),
|
||||||
|
priority: expanded_range.prefix_length
|
||||||
|
)
|
||||||
|
rescue ActiveRecord::RecordNotUnique
|
||||||
|
# Rule already exists (created by another job or earlier in this job)
|
||||||
|
# Find and return the existing rule
|
||||||
|
Rails.logger.debug "Rule already exists for #{expanded_range.cidr} with policy #{name}"
|
||||||
|
return Rule.find_by(
|
||||||
waf_rule_type: 'network',
|
waf_rule_type: 'network',
|
||||||
waf_action: policy_action,
|
waf_action: policy_action,
|
||||||
network_range: network_range,
|
network_range: expanded_range,
|
||||||
waf_policy: self,
|
waf_policy: self,
|
||||||
source: "policy"
|
source: "policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
if existing_rule
|
|
||||||
# Re-enable disabled rules or extend expiration for enabled rules
|
|
||||||
update_existing_rule(existing_rule)
|
|
||||||
return existing_rule
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Create new rule
|
# Handle redirect/challenge specific data
|
||||||
create_new_rule(network_range)
|
if redirect_action? && additional_data['redirect_url']
|
||||||
|
rule.update!(
|
||||||
|
metadata: rule.metadata.merge(
|
||||||
|
redirect_url: additional_data['redirect_url'],
|
||||||
|
redirect_status: additional_data['redirect_status'] || 302
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elsif challenge_action?
|
||||||
|
rule.update!(
|
||||||
|
metadata: rule.metadata.merge(
|
||||||
|
challenge_type: additional_data['challenge_type'] || 'captcha',
|
||||||
|
challenge_message: additional_data['challenge_message']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
rule
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_rule_for_event(event)
|
def create_rule_for_event(event)
|
||||||
@@ -425,74 +455,6 @@ validate :targets_must_be_array
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_existing_rule(rule)
|
|
||||||
updates = { updated_at: Time.current }
|
|
||||||
|
|
||||||
# Re-enable if disabled
|
|
||||||
unless rule.enabled?
|
|
||||||
updates[:enabled] = true
|
|
||||||
Rails.logger.info "Re-enabling rule ##{rule.id} for #{rule.network_range.cidr}"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Set/extend expiration to 7 days from now
|
|
||||||
# (Can be made configurable later via policy.rule_ttl field)
|
|
||||||
updates[:expires_at] = 7.days.from_now
|
|
||||||
|
|
||||||
rule.update!(updates) unless updates.empty?
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_new_rule(network_range)
|
|
||||||
begin
|
|
||||||
rule = Rule.create!(
|
|
||||||
waf_rule_type: 'network',
|
|
||||||
waf_action: policy_action.to_sym,
|
|
||||||
network_range: network_range,
|
|
||||||
waf_policy: self,
|
|
||||||
user: user,
|
|
||||||
source: "policy",
|
|
||||||
metadata: build_rule_metadata(network_range),
|
|
||||||
priority: network_range.prefix_length,
|
|
||||||
expires_at: 7.days.from_now # Set expiration for new rules
|
|
||||||
)
|
|
||||||
rescue ActiveRecord::RecordNotUnique
|
|
||||||
# Race condition: rule created between find_by and create
|
|
||||||
# Retry by finding and updating
|
|
||||||
existing_rule = Rule.find_by(
|
|
||||||
waf_rule_type: 'network',
|
|
||||||
waf_action: policy_action,
|
|
||||||
network_range: network_range,
|
|
||||||
waf_policy: self,
|
|
||||||
source: "policy"
|
|
||||||
)
|
|
||||||
|
|
||||||
if existing_rule
|
|
||||||
update_existing_rule(existing_rule)
|
|
||||||
return existing_rule
|
|
||||||
else
|
|
||||||
raise # Re-raise if we still can't find it
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Handle redirect/challenge specific data
|
|
||||||
if redirect_action? && additional_data['redirect_url']
|
|
||||||
rule.update!(
|
|
||||||
metadata: rule.metadata.merge(
|
|
||||||
redirect_url: additional_data['redirect_url'],
|
|
||||||
redirect_status: additional_data['redirect_status'] || 302
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elsif challenge_action?
|
|
||||||
rule.update!(
|
|
||||||
metadata: rule.metadata.merge(
|
|
||||||
challenge_type: additional_data['challenge_type'] || 'captcha',
|
|
||||||
challenge_message: additional_data['challenge_message']
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
rule
|
|
||||||
end
|
|
||||||
|
|
||||||
# Matching logic for different policy types
|
# Matching logic for different policy types
|
||||||
def matches_country?(network_range)
|
def matches_country?(network_range)
|
||||||
country = network_range.country || network_range.inherited_intelligence[:country]
|
country = network_range.country || network_range.inherited_intelligence[:country]
|
||||||
@@ -547,6 +509,64 @@ validate :targets_must_be_array
|
|||||||
base_metadata.merge!(additional_data || {})
|
base_metadata.merge!(additional_data || {})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# For country policies, find the largest ancestor network that matches the same country
|
||||||
|
# This allows consolidating /24 rules into /16, /8, etc. when the entire block is in the same country
|
||||||
|
def find_largest_matching_ancestor(network_range)
|
||||||
|
return network_range unless country_policy?
|
||||||
|
|
||||||
|
country = network_range.country || network_range.inherited_intelligence[:country]
|
||||||
|
return network_range unless country
|
||||||
|
|
||||||
|
# Check if this network has IPAPI data with a larger CIDR (asn.route or ipapi_returned_cidr)
|
||||||
|
ipapi_cidr = network_range.network_data&.dig('ipapi', 'asn', 'route') ||
|
||||||
|
network_range.network_data&.dig('ipapi_returned_cidr')
|
||||||
|
|
||||||
|
if ipapi_cidr && ipapi_cidr != network_range.cidr
|
||||||
|
# IPAPI returned a larger network - use it if it exists
|
||||||
|
existing = NetworkRange.find_by(network: ipapi_cidr)
|
||||||
|
if existing
|
||||||
|
existing_country = existing.country || existing.inherited_intelligence[:country]
|
||||||
|
if existing_country == country
|
||||||
|
Rails.logger.debug "Using IPAPI CIDR #{existing.cidr} instead of #{network_range.cidr} (both #{country})"
|
||||||
|
return existing
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# Create the IPAPI network range if it doesn't exist
|
||||||
|
begin
|
||||||
|
ipapi_network = NetworkRange.create!(
|
||||||
|
network: ipapi_cidr,
|
||||||
|
source: 'inherited',
|
||||||
|
country: country
|
||||||
|
)
|
||||||
|
Rails.logger.info "Created IPAPI network range #{ipapi_cidr} for country #{country}"
|
||||||
|
return ipapi_network
|
||||||
|
rescue ActiveRecord::RecordNotUnique
|
||||||
|
# Race condition - another process created it
|
||||||
|
existing = NetworkRange.find_by(network: ipapi_cidr)
|
||||||
|
return existing || network_range
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Fallback: Look for existing parent networks with IPAPI data and same country
|
||||||
|
# Query for all networks that contain this network and have IPAPI data
|
||||||
|
parent_with_ipapi = NetworkRange.where(
|
||||||
|
"?::inet << network", network_range.cidr
|
||||||
|
).where(
|
||||||
|
"network_data ? 'ipapi' AND " \
|
||||||
|
"network_data -> 'ipapi' ->> 'location' ->> 'country_code' = ?",
|
||||||
|
country
|
||||||
|
).order("masklen(network) DESC").first
|
||||||
|
|
||||||
|
if parent_with_ipapi
|
||||||
|
Rails.logger.debug "Found existing IPAPI parent #{parent_with_ipapi.cidr} for #{network_range.cidr} (both #{country})"
|
||||||
|
return parent_with_ipapi
|
||||||
|
end
|
||||||
|
|
||||||
|
# No expansion possible - use original network
|
||||||
|
network_range
|
||||||
|
end
|
||||||
|
|
||||||
def matched_field(network_range)
|
def matched_field(network_range)
|
||||||
case policy_type
|
case policy_type
|
||||||
when 'country'
|
when 'country'
|
||||||
|
|||||||
@@ -12,43 +12,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filter Bar -->
|
|
||||||
<div class="bg-white shadow rounded-lg p-4">
|
|
||||||
<div class="flex flex-wrap gap-2 items-center">
|
|
||||||
<span class="text-sm font-medium text-gray-700 mr-2">Filter by Status:</span>
|
|
||||||
|
|
||||||
<%= link_to "All", rules_path(status: 'all'),
|
|
||||||
class: "px-4 py-2 rounded-lg text-sm font-medium transition-colors #{@current_status == 'all' ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}" %>
|
|
||||||
|
|
||||||
<%= link_to "Active", rules_path(status: 'active'),
|
|
||||||
class: "px-4 py-2 rounded-lg text-sm font-medium transition-colors #{@current_status == 'active' ? 'bg-green-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}" %>
|
|
||||||
|
|
||||||
<%= link_to "Enabled", rules_path(status: 'enabled'),
|
|
||||||
class: "px-4 py-2 rounded-lg text-sm font-medium transition-colors #{@current_status == 'enabled' ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}" %>
|
|
||||||
|
|
||||||
<%= link_to "Disabled", rules_path(status: 'disabled'),
|
|
||||||
class: "px-4 py-2 rounded-lg text-sm font-medium transition-colors #{@current_status == 'disabled' ? 'bg-gray-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}" %>
|
|
||||||
|
|
||||||
<%= link_to "Expired", rules_path(status: 'expired'),
|
|
||||||
class: "px-4 py-2 rounded-lg text-sm font-medium transition-colors #{@current_status == 'expired' ? 'bg-red-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}" %>
|
|
||||||
|
|
||||||
<% if @current_status != 'all' %>
|
|
||||||
<%= link_to rules_path, class: "ml-2 text-sm text-blue-600 hover:text-blue-800" do %>
|
|
||||||
<span class="inline-flex items-center">
|
|
||||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
||||||
</svg>
|
|
||||||
Clear filter
|
|
||||||
</span>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Statistics Cards -->
|
<!-- Statistics Cards -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
<%= link_to rules_path(status: 'all'), class: "block" do %>
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||||
<div class="bg-white overflow-hidden shadow rounded-lg hover:shadow-lg transition-shadow">
|
|
||||||
<div class="p-5">
|
<div class="p-5">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
@@ -65,10 +31,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= link_to rules_path(status: 'active'), class: "block" do %>
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||||
<div class="bg-white overflow-hidden shadow rounded-lg hover:shadow-lg transition-shadow">
|
|
||||||
<div class="p-5">
|
<div class="p-5">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
@@ -85,10 +49,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= link_to rules_path(status: 'disabled'), class: "block" do %>
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||||
<div class="bg-white overflow-hidden shadow rounded-lg hover:shadow-lg transition-shadow">
|
|
||||||
<div class="p-5">
|
<div class="p-5">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
@@ -105,10 +67,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= link_to rules_path(status: 'expired'), class: "block" do %>
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||||
<div class="bg-white overflow-hidden shadow rounded-lg hover:shadow-lg transition-shadow">
|
|
||||||
<div class="p-5">
|
<div class="p-5">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
@@ -125,7 +85,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Rules List -->
|
<!-- Rules List -->
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module BaffleHub
|
module BaffleHub
|
||||||
VERSION = "0.6.2"
|
VERSION = "0.4.0"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -442,7 +442,7 @@ class WafPolicyTest < ActiveSupport::TestCase
|
|||||||
assert_equal 0, @policy.generated_rules_count
|
assert_equal 0, @policy.generated_rules_count
|
||||||
|
|
||||||
# Create some rules
|
# Create some rules
|
||||||
network_range = NetworkRange.create!(cidr: "192.168.1.0/24", country: "BR")
|
network_range = NetworkRange.create!(ip_range: "192.168.1.0/24")
|
||||||
@policy.create_rule_for_network_range(network_range)
|
@policy.create_rule_for_network_range(network_range)
|
||||||
|
|
||||||
assert_equal 1, @policy.generated_rules_count
|
assert_equal 1, @policy.generated_rules_count
|
||||||
@@ -461,75 +461,6 @@ class WafPolicyTest < ActiveSupport::TestCase
|
|||||||
assert_equal 2, stats[:targets_count]
|
assert_equal 2, stats[:targets_count]
|
||||||
end
|
end
|
||||||
|
|
||||||
# Rule creation and re-enablement tests
|
|
||||||
test "create_rule_for_network_range re-enables disabled rule" do
|
|
||||||
@policy.save!
|
|
||||||
|
|
||||||
# Create a network range that matches the policy
|
|
||||||
network_range = NetworkRange.create!(
|
|
||||||
cidr: "192.168.1.0/24",
|
|
||||||
country: "BR"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a rule via policy
|
|
||||||
rule = @policy.create_rule_for_network_range(network_range)
|
|
||||||
assert rule.enabled?, "Rule should be enabled initially"
|
|
||||||
assert rule.expires_at.present?, "Rule should have expiration"
|
|
||||||
|
|
||||||
# Disable it (simulating expiration cleanup)
|
|
||||||
rule.update!(enabled: false)
|
|
||||||
assert_not rule.enabled?, "Rule should be disabled"
|
|
||||||
|
|
||||||
# Try to create again - should re-enable
|
|
||||||
result = @policy.create_rule_for_network_range(network_range)
|
|
||||||
|
|
||||||
assert_equal rule.id, result.id, "Should return same rule"
|
|
||||||
assert result.reload.enabled?, "Rule should be re-enabled"
|
|
||||||
assert result.expires_at.present?, "Should have new expiration"
|
|
||||||
assert result.expires_at > Time.current, "Expiration should be in the future"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "create_rule_for_network_range extends enabled rule expiration" do
|
|
||||||
@policy.save!
|
|
||||||
|
|
||||||
# Create a network range that matches the policy
|
|
||||||
network_range = NetworkRange.create!(
|
|
||||||
cidr: "192.168.1.0/24",
|
|
||||||
country: "BR"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a rule with expiration
|
|
||||||
rule = @policy.create_rule_for_network_range(network_range)
|
|
||||||
original_expires_at = rule.expires_at
|
|
||||||
|
|
||||||
travel 3.days do
|
|
||||||
# Try to create again - should extend expiration
|
|
||||||
result = @policy.create_rule_for_network_range(network_range)
|
|
||||||
|
|
||||||
assert_equal rule.id, result.id, "Should return same rule"
|
|
||||||
assert result.reload.expires_at > original_expires_at, "Expiration should be extended"
|
|
||||||
assert_in_delta 7.days.from_now, result.expires_at, 5.seconds, "Expiration should be ~7 days from now"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "create_rule_for_network_range creates new rule when none exists" do
|
|
||||||
@policy.save!
|
|
||||||
|
|
||||||
# Create a network range that matches the policy
|
|
||||||
network_range = NetworkRange.create!(
|
|
||||||
cidr: "192.168.1.0/24",
|
|
||||||
country: "BR"
|
|
||||||
)
|
|
||||||
|
|
||||||
rule = @policy.create_rule_for_network_range(network_range)
|
|
||||||
|
|
||||||
assert rule.persisted?, "Rule should be persisted"
|
|
||||||
assert rule.enabled?, "Rule should be enabled"
|
|
||||||
assert_equal network_range, rule.network_range
|
|
||||||
assert rule.expires_at.present?, "New rule should have expiration"
|
|
||||||
assert_in_delta 7.days.from_now, rule.expires_at, 5.seconds, "Expiration should be ~7 days from now"
|
|
||||||
end
|
|
||||||
|
|
||||||
# String representations
|
# String representations
|
||||||
test "to_s returns name" do
|
test "to_s returns name" do
|
||||||
assert_equal @policy.name, @policy.to_s
|
assert_equal @policy.name, @policy.to_s
|
||||||
|
|||||||
Reference in New Issue
Block a user