Path matching
This commit is contained in:
412
app/assets/stylesheets/tom-select.css
Normal file
412
app/assets/stylesheets/tom-select.css
Normal file
@@ -0,0 +1,412 @@
|
||||
/**
|
||||
* tom-select.css (v2.3.1)
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
|
||||
* file except in compliance with the License. You may obtain a copy of the License at:
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
||||
* ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
.ts-control {
|
||||
border: 1px solid #d0d0d0;
|
||||
padding: 8px 8px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
box-shadow: none;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.ts-wrapper.multi.has-items .ts-control {
|
||||
padding: calc(8px - 2px - 0) 8px calc(8px - 2px - 3px - 0);
|
||||
}
|
||||
.full .ts-control {
|
||||
background-color: #fff;
|
||||
}
|
||||
.disabled .ts-control, .disabled .ts-control * {
|
||||
cursor: default !important;
|
||||
}
|
||||
.focus .ts-control {
|
||||
box-shadow: none;
|
||||
}
|
||||
.ts-control > * {
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
}
|
||||
.ts-wrapper.multi .ts-control > div {
|
||||
cursor: pointer;
|
||||
margin: 0 3px 3px 0;
|
||||
padding: 2px 6px;
|
||||
background: #f2f2f2;
|
||||
color: #303030;
|
||||
border: 0 solid #d0d0d0;
|
||||
}
|
||||
.ts-wrapper.multi .ts-control > div.active {
|
||||
background: #e8e8e8;
|
||||
color: #303030;
|
||||
border: 0 solid #cacaca;
|
||||
}
|
||||
.ts-wrapper.multi.disabled .ts-control > div, .ts-wrapper.multi.disabled .ts-control > div.active {
|
||||
color: #7d7d7d;
|
||||
background: white;
|
||||
border: 0 solid white;
|
||||
}
|
||||
.ts-control > input {
|
||||
flex: 1 1 auto;
|
||||
min-width: 7rem;
|
||||
display: inline-block !important;
|
||||
padding: 0 !important;
|
||||
min-height: 0 !important;
|
||||
max-height: none !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
text-indent: 0 !important;
|
||||
border: 0 none !important;
|
||||
background: none !important;
|
||||
line-height: inherit !important;
|
||||
-webkit-user-select: auto !important;
|
||||
-moz-user-select: auto !important;
|
||||
-ms-user-select: auto !important;
|
||||
user-select: auto !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.ts-control > input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
.ts-control > input:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
.has-items .ts-control > input {
|
||||
margin: 0 4px !important;
|
||||
}
|
||||
.ts-control.rtl {
|
||||
text-align: right;
|
||||
}
|
||||
.ts-control.rtl.single .ts-control:after {
|
||||
left: 15px;
|
||||
right: auto;
|
||||
}
|
||||
.ts-control.rtl .ts-control > input {
|
||||
margin: 0 4px 0 -2px !important;
|
||||
}
|
||||
.disabled .ts-control {
|
||||
opacity: 0.5;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
.input-hidden .ts-control > input {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
}
|
||||
|
||||
.ts-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
border: 1px solid #d0d0d0;
|
||||
background: #fff;
|
||||
margin: 0.25rem 0 0;
|
||||
border-top: 0 none;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0 0 3px 3px;
|
||||
}
|
||||
.ts-dropdown [data-selectable] {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ts-dropdown [data-selectable] .highlight {
|
||||
background: rgba(125, 168, 208, 0.2);
|
||||
border-radius: 1px;
|
||||
}
|
||||
.ts-dropdown .option,
|
||||
.ts-dropdown .optgroup-header,
|
||||
.ts-dropdown .no-results,
|
||||
.ts-dropdown .create {
|
||||
padding: 5px 8px;
|
||||
}
|
||||
.ts-dropdown .option, .ts-dropdown [data-disabled], .ts-dropdown [data-disabled] [data-selectable].option {
|
||||
cursor: inherit;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.ts-dropdown [data-selectable].option {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ts-dropdown .optgroup:first-child .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
.ts-dropdown .optgroup-header {
|
||||
color: #303030;
|
||||
background: #fff;
|
||||
cursor: default;
|
||||
}
|
||||
.ts-dropdown .active {
|
||||
background-color: #f5fafd;
|
||||
color: #495c68;
|
||||
}
|
||||
.ts-dropdown .active.create {
|
||||
color: #495c68;
|
||||
}
|
||||
.ts-dropdown .create {
|
||||
color: rgba(48, 48, 48, 0.5);
|
||||
}
|
||||
.ts-dropdown .spinner {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: 5px 8px;
|
||||
}
|
||||
.ts-dropdown .spinner::after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 3px;
|
||||
border-radius: 50%;
|
||||
border: 5px solid #d0d0d0;
|
||||
border-color: #d0d0d0 transparent #d0d0d0 transparent;
|
||||
animation: lds-dual-ring 1.2s linear infinite;
|
||||
}
|
||||
@keyframes lds-dual-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.ts-dropdown-content {
|
||||
overflow: hidden auto;
|
||||
max-height: 200px;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.ts-wrapper.plugin-drag_drop .ts-dragging {
|
||||
color: transparent !important;
|
||||
}
|
||||
.ts-wrapper.plugin-drag_drop .ts-dragging > * {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
.plugin-checkbox_options:not(.rtl) .option input {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.plugin-checkbox_options.rtl .option input {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
/* stylelint-disable function-name-case */
|
||||
.plugin-clear_button {
|
||||
--ts-pr-clear-button: 1em;
|
||||
}
|
||||
.plugin-clear_button .clear-button {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: calc(8px - 6px);
|
||||
margin-right: 0 !important;
|
||||
background: transparent !important;
|
||||
transition: opacity 0.5s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.plugin-clear_button.form-select .clear-button, .plugin-clear_button.single .clear-button {
|
||||
right: max(var(--ts-pr-caret), 8px);
|
||||
}
|
||||
.plugin-clear_button.focus.has-items .clear-button, .plugin-clear_button:not(.disabled):hover.has-items .clear-button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ts-wrapper .dropdown-header {
|
||||
position: relative;
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid #d0d0d0;
|
||||
background: color-mix(#fff, #d0d0d0, 85%);
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
.ts-wrapper .dropdown-header-close {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
color: #303030;
|
||||
opacity: 0.4;
|
||||
margin-top: -12px;
|
||||
line-height: 20px;
|
||||
font-size: 20px !important;
|
||||
}
|
||||
.ts-wrapper .dropdown-header-close:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.plugin-dropdown_input.focus.dropdown-active .ts-control {
|
||||
box-shadow: none;
|
||||
border: 1px solid #d0d0d0;
|
||||
}
|
||||
.plugin-dropdown_input .dropdown-input {
|
||||
border: 1px solid #d0d0d0;
|
||||
border-width: 0 0 1px;
|
||||
display: block;
|
||||
padding: 8px 8px;
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
.plugin-dropdown_input .items-placeholder {
|
||||
border: 0 none !important;
|
||||
box-shadow: none !important;
|
||||
width: 100%;
|
||||
}
|
||||
.plugin-dropdown_input.has-items .items-placeholder, .plugin-dropdown_input.dropdown-active .items-placeholder {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ts-wrapper.plugin-input_autogrow.has-items .ts-control > input {
|
||||
min-width: 0;
|
||||
}
|
||||
.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input {
|
||||
flex: none;
|
||||
min-width: 4px;
|
||||
}
|
||||
.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input::-ms-input-placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input::placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.ts-dropdown.plugin-optgroup_columns .ts-dropdown-content {
|
||||
display: flex;
|
||||
}
|
||||
.ts-dropdown.plugin-optgroup_columns .optgroup {
|
||||
border-right: 1px solid #f2f2f2;
|
||||
border-top: 0 none;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.ts-dropdown.plugin-optgroup_columns .optgroup:last-child {
|
||||
border-right: 0 none;
|
||||
}
|
||||
.ts-dropdown.plugin-optgroup_columns .optgroup::before {
|
||||
display: none;
|
||||
}
|
||||
.ts-dropdown.plugin-optgroup_columns .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.ts-wrapper.plugin-remove_button .item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.ts-wrapper.plugin-remove_button .item .remove {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
padding: 0 6px;
|
||||
border-radius: 0 2px 2px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ts-wrapper.plugin-remove_button .item .remove:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.ts-wrapper.plugin-remove_button.disabled .item .remove:hover {
|
||||
background: none;
|
||||
}
|
||||
.ts-wrapper.plugin-remove_button .remove-single {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
font-size: 23px;
|
||||
}
|
||||
|
||||
.ts-wrapper.plugin-remove_button:not(.rtl) .item {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
.ts-wrapper.plugin-remove_button:not(.rtl) .item .remove {
|
||||
border-left: 1px solid #d0d0d0;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.ts-wrapper.plugin-remove_button:not(.rtl) .item.active .remove {
|
||||
border-left-color: #cacaca;
|
||||
}
|
||||
.ts-wrapper.plugin-remove_button:not(.rtl).disabled .item .remove {
|
||||
border-left-color: white;
|
||||
}
|
||||
|
||||
.ts-wrapper.plugin-remove_button.rtl .item {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
.ts-wrapper.plugin-remove_button.rtl .item .remove {
|
||||
border-right: 1px solid #d0d0d0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.ts-wrapper.plugin-remove_button.rtl .item.active .remove {
|
||||
border-right-color: #cacaca;
|
||||
}
|
||||
.ts-wrapper.plugin-remove_button.rtl.disabled .item .remove {
|
||||
border-right-color: white;
|
||||
}
|
||||
|
||||
:root {
|
||||
--ts-pr-clear-button: 0;
|
||||
--ts-pr-caret: 0;
|
||||
--ts-pr-min: .75rem;
|
||||
}
|
||||
|
||||
.ts-wrapper.single .ts-control, .ts-wrapper.single .ts-control input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ts-control:not(.rtl) {
|
||||
padding-right: max(var(--ts-pr-min), var(--ts-pr-clear-button) + var(--ts-pr-caret)) !important;
|
||||
}
|
||||
|
||||
.ts-control.rtl {
|
||||
padding-left: max(var(--ts-pr-min), var(--ts-pr-clear-button) + var(--ts-pr-caret)) !important;
|
||||
}
|
||||
|
||||
.ts-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ts-dropdown,
|
||||
.ts-control,
|
||||
.ts-control input {
|
||||
color: #303030;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.ts-control,
|
||||
.ts-wrapper.single.input-active .ts-control {
|
||||
background: #fff;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.ts-hidden-accessible {
|
||||
border: 0 !important;
|
||||
clip: rect(0 0 0 0) !important;
|
||||
-webkit-clip-path: inset(50%) !important;
|
||||
clip-path: inset(50%) !important;
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
/*# sourceMappingURL=tom-select.css.map */
|
||||
@@ -9,11 +9,13 @@ class Event < ApplicationRecord
|
||||
has_one :waf_policy, through: :rule
|
||||
|
||||
# Enums for fixed value sets
|
||||
# Canonical WAF action order - aligned with Rule and Agent models
|
||||
enum :waf_action, {
|
||||
allow: 0, # allow/pass
|
||||
deny: 1, # deny/block
|
||||
deny: 0, # deny/block
|
||||
allow: 1, # allow/pass
|
||||
redirect: 2, # redirect
|
||||
challenge: 3 # challenge (future implementation)
|
||||
challenge: 3, # challenge (CAPTCHA, JS challenge, etc.)
|
||||
log: 4 # log only, no action (monitoring mode)
|
||||
}, default: :allow, scopes: false
|
||||
|
||||
enum :request_method, {
|
||||
@@ -42,7 +44,7 @@ class Event < ApplicationRecord
|
||||
scope :by_waf_action, ->(waf_action) { where(waf_action: waf_action) }
|
||||
scope :blocked, -> { where(waf_action: :deny) }
|
||||
scope :allowed, -> { where(waf_action: :allow) }
|
||||
scope :rate_limited, -> { where(waf_action: 'rate_limit') }
|
||||
scope :logged, -> { where(waf_action: :log) }
|
||||
|
||||
# Tag-based filtering scopes using PostgreSQL array operators
|
||||
scope :with_tag, ->(tag) { where("tags @> ARRAY[?]", tag.to_s) }
|
||||
@@ -346,8 +348,8 @@ class Event < ApplicationRecord
|
||||
waf_action.in?(['allow', 'pass'])
|
||||
end
|
||||
|
||||
def rate_limited?
|
||||
waf_action == 'rate_limit'
|
||||
def logged?
|
||||
waf_action == 'log'
|
||||
end
|
||||
|
||||
def challenged?
|
||||
|
||||
@@ -6,12 +6,10 @@
|
||||
# Network rules are associated with NetworkRange objects for rich context.
|
||||
class Rule < ApplicationRecord
|
||||
# Rule enums (prefix needed to avoid rate_limit collision)
|
||||
enum :waf_action, { allow: 0, deny: 1, rate_limit: 2, redirect: 3, log: 4, challenge: 5 }, prefix: :action
|
||||
# Canonical WAF action order - aligned with Agent and Event models
|
||||
enum :waf_action, { deny: 0, allow: 1, redirect: 2, challenge: 3, log: 4 }, prefix: :action
|
||||
enum :waf_rule_type, { network: 0, rate_limit: 1, path_pattern: 2 }, prefix: :type
|
||||
|
||||
# Legacy string constants for backward compatibility
|
||||
RULE_TYPES = %w[network rate_limit path_pattern].freeze
|
||||
ACTIONS = %w[allow deny rate_limit redirect log challenge].freeze
|
||||
SOURCES = %w[manual auto:scanner_detected auto:rate_limit_exceeded auto:bot_detected imported default manual:surgical_block manual:surgical_exception policy].freeze
|
||||
|
||||
# Associations
|
||||
@@ -27,14 +25,6 @@ class Rule < ApplicationRecord
|
||||
validates :enabled, inclusion: { in: [true, false] }
|
||||
validates :source, inclusion: { in: SOURCES }
|
||||
|
||||
# Legacy enum definitions (disabled to prevent conflicts)
|
||||
# enum :action, { allow: "allow", deny: "deny", rate_limit: "rate_limit", redirect: "redirect", log: "log", challenge: "challenge" }, scopes: false
|
||||
# enum :rule_type, { network: "network", rate_limit: "rate_limit", path_pattern: "path_pattern" }, scopes: false
|
||||
|
||||
# Legacy validations for backward compatibility during transition
|
||||
# validates :rule_type, presence: true, inclusion: { in: RULE_TYPES }, allow_nil: true
|
||||
# validates :action, presence: true, inclusion: { in: ACTIONS }, allow_nil: true
|
||||
|
||||
# Custom validations
|
||||
validate :validate_conditions_by_type
|
||||
validate :validate_metadata_by_action
|
||||
@@ -356,12 +346,12 @@ class Rule < ApplicationRecord
|
||||
[block_rule, exception_rule]
|
||||
end
|
||||
|
||||
def self.create_rate_limit_rule(cidr, limit:, window:, user: nil, **options)
|
||||
def self.create_rate_limit_rule(cidr, limit:, window:, user: nil, action: 'deny', **options)
|
||||
network_range = NetworkRange.find_or_create_by_cidr(cidr, user: user, source: 'user_created')
|
||||
|
||||
create!(
|
||||
waf_rule_type: 'rate_limit',
|
||||
waf_action: 'rate_limit',
|
||||
waf_action: action, # Action to take when rate limit exceeded (deny, redirect, challenge, log)
|
||||
network_range: network_range,
|
||||
conditions: { cidr: cidr, scope: 'ip' },
|
||||
metadata: {
|
||||
@@ -514,10 +504,6 @@ class Rule < ApplicationRecord
|
||||
if challenge_type_value && !%w[captcha javascript proof_of_work].include?(challenge_type_value)
|
||||
errors.add(:metadata, "challenge_type must be one of: captcha, javascript, proof_of_work")
|
||||
end
|
||||
when "rate_limit"
|
||||
unless metadata&.dig("limit").present? && metadata&.dig("window").present?
|
||||
errors.add(:metadata, "must include 'limit' and 'window' for rate_limit action")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -41,10 +41,11 @@ class EventNormalizer
|
||||
return unless raw_action.present?
|
||||
|
||||
action_enum = case raw_action.to_s.downcase
|
||||
when 'allow', 'pass' then :allow
|
||||
when 'deny', 'block' then :deny
|
||||
when 'challenge' then :challenge
|
||||
when 'allow', 'pass' then :allow
|
||||
when 'redirect' then :redirect
|
||||
when 'challenge' then :challenge
|
||||
when 'log', 'monitor' then :log
|
||||
else :allow
|
||||
end
|
||||
|
||||
|
||||
85
app/services/path_rule_matcher.rb
Normal file
85
app/services/path_rule_matcher.rb
Normal file
@@ -0,0 +1,85 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# PathRuleMatcher - Service to match Events against path_pattern Rules
|
||||
#
|
||||
# This service provides path pattern matching logic for evaluating whether
|
||||
# an event matches a path_pattern rule. Used for hub-side testing and validation
|
||||
# before agent deployment.
|
||||
#
|
||||
# Match Types:
|
||||
# - exact: All segments must match exactly
|
||||
# - prefix: Event path must start with rule segments
|
||||
# - suffix: Event path must end with rule segments
|
||||
# - contains: Rule segments must appear consecutively somewhere in event path
|
||||
class PathRuleMatcher
|
||||
def self.matches?(rule, event)
|
||||
return false unless rule.path_pattern_rule?
|
||||
return false if event.request_segment_ids.blank?
|
||||
|
||||
rule_segments = rule.path_segment_ids
|
||||
event_segments = event.request_segment_ids
|
||||
|
||||
return false if rule_segments.blank?
|
||||
|
||||
case rule.path_match_type
|
||||
when 'exact'
|
||||
exact_match?(event_segments, rule_segments)
|
||||
when 'prefix'
|
||||
prefix_match?(event_segments, rule_segments)
|
||||
when 'suffix'
|
||||
suffix_match?(event_segments, rule_segments)
|
||||
when 'contains'
|
||||
contains_match?(event_segments, rule_segments)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# Find all path_pattern rules that match the given event
|
||||
def self.matching_rules(event)
|
||||
return [] if event.request_segment_ids.blank?
|
||||
|
||||
Rule.path_pattern_rules.active.select do |rule|
|
||||
matches?(rule, event)
|
||||
end
|
||||
end
|
||||
|
||||
# Evaluate an event against path rules and return the first matching action
|
||||
def self.evaluate(event)
|
||||
matching_rule = matching_rules(event).first
|
||||
matching_rule&.waf_action || 'allow'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Exact match: all segments must match exactly
|
||||
# Example: [1, 2, 3] matches [1, 2, 3] only
|
||||
def self.exact_match?(event_segments, rule_segments)
|
||||
event_segments == rule_segments
|
||||
end
|
||||
|
||||
# Prefix match: event path must start with rule segments
|
||||
# Example: rule [1, 2] matches events [1, 2], [1, 2, 3], [1, 2, 3, 4]
|
||||
def self.prefix_match?(event_segments, rule_segments)
|
||||
return false if event_segments.length < rule_segments.length
|
||||
event_segments[0...rule_segments.length] == rule_segments
|
||||
end
|
||||
|
||||
# Suffix match: event path must end with rule segments
|
||||
# Example: rule [2, 3] matches events [2, 3], [1, 2, 3], [0, 1, 2, 3]
|
||||
def self.suffix_match?(event_segments, rule_segments)
|
||||
return false if event_segments.length < rule_segments.length
|
||||
event_segments[-rule_segments.length..-1] == rule_segments
|
||||
end
|
||||
|
||||
# Contains match: rule segments must appear consecutively somewhere in event path
|
||||
# Example: rule [2, 3] matches [1, 2, 3, 4], [2, 3], [0, 2, 3, 5]
|
||||
def self.contains_match?(event_segments, rule_segments)
|
||||
return false if event_segments.length < rule_segments.length
|
||||
|
||||
# Check if rule_segments appear consecutively anywhere in event_segments
|
||||
(0..event_segments.length - rule_segments.length).any? do |i|
|
||||
event_segments[i, rule_segments.length] == rule_segments
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -302,8 +302,15 @@
|
||||
<% @recent_events.first(3).each do |event| %>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<div class="flex items-center">
|
||||
<div class="w-2 h-2 rounded-full mr-2
|
||||
<%= event.waf_action == 'allow' ? 'bg-green-500' : 'bg-red-500' %>"></div>
|
||||
<% dot_color = case event.waf_action
|
||||
when 'allow' then 'bg-green-500'
|
||||
when 'deny' then 'bg-red-500'
|
||||
when 'redirect' then 'bg-blue-500'
|
||||
when 'challenge' then 'bg-yellow-500'
|
||||
when 'log' then 'bg-gray-500'
|
||||
else 'bg-gray-500'
|
||||
end %>
|
||||
<div class="w-2 h-2 rounded-full mr-2 <%= dot_color %>"></div>
|
||||
<span class="text-gray-900 truncate max-w-[120px]"><%= event.ip_address %></span>
|
||||
</div>
|
||||
<span class="text-gray-500"><%= time_ago_in_words(event.timestamp) %> ago</span>
|
||||
|
||||
@@ -119,8 +119,15 @@
|
||||
<% @recent_events.first(3).each do |event| %>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<div class="flex items-center">
|
||||
<div class="w-2 h-2 rounded-full mr-2
|
||||
<%= event.waf_action == 'allow' ? 'bg-green-500' : 'bg-red-500' %>"></div>
|
||||
<% dot_color = case event.waf_action
|
||||
when 'allow' then 'bg-green-500'
|
||||
when 'deny' then 'bg-red-500'
|
||||
when 'redirect' then 'bg-blue-500'
|
||||
when 'challenge' then 'bg-yellow-500'
|
||||
when 'log' then 'bg-gray-500'
|
||||
else 'bg-gray-500'
|
||||
end %>
|
||||
<div class="w-2 h-2 rounded-full mr-2 <%= dot_color %>"></div>
|
||||
<span class="text-gray-900 truncate max-w-[120px]"><%= event.ip_address %></span>
|
||||
</div>
|
||||
<span class="text-gray-500"><%= time_ago_in_words(event.timestamp) %> ago</span>
|
||||
|
||||
@@ -711,7 +711,15 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <%= event.waf_action == 'deny' ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800' %>">
|
||||
<% action_classes = case event.waf_action
|
||||
when 'deny' then 'bg-red-100 text-red-800'
|
||||
when 'allow' then 'bg-green-100 text-green-800'
|
||||
when 'redirect' then 'bg-blue-100 text-blue-800'
|
||||
when 'challenge' then 'bg-yellow-100 text-yellow-800'
|
||||
when 'log' then 'bg-gray-100 text-gray-800'
|
||||
else 'bg-gray-100 text-gray-800'
|
||||
end %>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <%= action_classes %>">
|
||||
<%= event.waf_action %>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -3,7 +3,15 @@
|
||||
<div class="flex items-center space-x-2 min-w-0 flex-1">
|
||||
<%= link_to rule, class: "flex items-center space-x-2 min-w-0 hover:text-blue-600" do %>
|
||||
<%# Action badge %>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium <%= rule.waf_action == 'deny' ? 'bg-red-100 text-red-800' : rule.waf_action == 'allow' ? 'bg-green-100 text-green-800' : 'bg-blue-100 text-blue-800' %>">
|
||||
<% action_classes = case rule.waf_action
|
||||
when 'deny' then 'bg-red-100 text-red-800'
|
||||
when 'allow' then 'bg-green-100 text-green-800'
|
||||
when 'redirect' then 'bg-blue-100 text-blue-800'
|
||||
when 'challenge' then 'bg-yellow-100 text-yellow-800'
|
||||
when 'log' then 'bg-gray-100 text-gray-800'
|
||||
else 'bg-gray-100 text-gray-800'
|
||||
end %>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium <%= action_classes %>">
|
||||
<%= rule.waf_action.upcase %>
|
||||
</span>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user