contrast-agent 7.2.0 → 7.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +62 -23
- data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +37 -4
- data/lib/contrast/agent/protect/rule/base.rb +5 -1
- data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +27 -11
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +0 -1
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_input_classification.rb +2 -2
- data/lib/contrast/agent/protect/rule/input_classification/base.rb +191 -0
- data/lib/contrast/agent/protect/rule/input_classification/base64_statistic.rb +71 -0
- data/lib/contrast/agent/protect/rule/input_classification/cached_result.rb +37 -0
- data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +109 -0
- data/lib/contrast/agent/protect/rule/input_classification/encoding_rates.rb +47 -0
- data/lib/contrast/agent/protect/rule/input_classification/extendable.rb +80 -0
- data/lib/contrast/agent/protect/rule/input_classification/lru_cache.rb +198 -0
- data/lib/contrast/agent/protect/rule/input_classification/match_rates.rb +66 -0
- data/lib/contrast/agent/protect/rule/input_classification/rates.rb +53 -0
- data/lib/contrast/agent/protect/rule/input_classification/statistics.rb +115 -0
- data/lib/contrast/agent/protect/rule/input_classification/utils.rb +23 -0
- data/lib/contrast/agent/protect/rule/no_sqli/no_sqli_input_classification.rb +17 -7
- data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +18 -15
- data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +2 -2
- data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +18 -15
- data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +19 -17
- data/lib/contrast/agent/reporting/attack_result/attack_result.rb +6 -0
- data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +2 -7
- data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +11 -0
- data/lib/contrast/agent/reporting/input_analysis/input_type.rb +33 -1
- data/lib/contrast/agent/reporting/masker/masker_utils.rb +1 -1
- data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +1 -0
- data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +1 -0
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +1 -1
- data/lib/contrast/agent/telemetry/base.rb +28 -2
- data/lib/contrast/agent/telemetry/base64_hash.rb +55 -0
- data/lib/contrast/agent/telemetry/cache_hash.rb +55 -0
- data/lib/contrast/agent/telemetry/client.rb +10 -2
- data/lib/contrast/agent/telemetry/{hash.rb → exception_hash.rb} +1 -1
- data/lib/contrast/agent/telemetry/input_analysis_cache_event.rb +27 -0
- data/lib/contrast/agent/telemetry/input_analysis_encoding_event.rb +26 -0
- data/lib/contrast/agent/telemetry/input_analysis_event.rb +91 -0
- data/lib/contrast/agent/telemetry/metric_event.rb +12 -0
- data/lib/contrast/agent/telemetry/startup_metrics_event.rb +0 -8
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/components/config.rb +4 -4
- data/lib/contrast/components/protect.rb +11 -1
- data/lib/contrast/components/sampling.rb +15 -10
- data/lib/contrast/config/diagnostics/environment_variables.rb +3 -1
- data/lib/contrast/config/yaml_file.rb +8 -0
- data/lib/contrast/framework/rails/support.rb +3 -0
- data/lib/contrast/utils/assess/event_limit_utils.rb +13 -13
- data/lib/contrast/utils/metrics_hash.rb +1 -1
- data/lib/contrast/utils/object_share.rb +2 -1
- data/lib/contrast/utils/response_utils.rb +12 -0
- data/lib/contrast/utils/timer.rb +2 -0
- data/lib/contrast.rb +9 -2
- data/ruby-agent.gemspec +1 -1
- metadata +21 -6
- data/lib/contrast/utils/input_classification_base.rb +0 -169
@@ -0,0 +1,115 @@
|
|
1
|
+
# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'contrast/agent/protect/rule/input_classification/utils'
|
5
|
+
require 'contrast/agent/protect/rule/input_classification/match_rates'
|
6
|
+
require 'contrast/agent/telemetry/input_analysis_cache_event'
|
7
|
+
require 'contrast/agent/reporting/input_analysis/score_level'
|
8
|
+
require 'contrast/agent/reporting/input_analysis/input_type'
|
9
|
+
|
10
|
+
module Contrast
|
11
|
+
module Agent
|
12
|
+
module Protect
|
13
|
+
module Rule
|
14
|
+
module InputClassification
|
15
|
+
# This class will hold match information for each rule when input classification is being saved in LRU cache.
|
16
|
+
class Statistics
|
17
|
+
include Contrast::Agent::Protect::Rule::InputClassification::Utils
|
18
|
+
include Contrast::Components::Logger::InstanceMethods
|
19
|
+
|
20
|
+
attr_reader :data
|
21
|
+
|
22
|
+
# Protect rules will always be fixed number, on other hand the number of inputs will grow,
|
23
|
+
# we need to limit the number of inputs to be cached.
|
24
|
+
CAPACITY = 30
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
@data = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
# This method will handle the statistics for the input match
|
31
|
+
#
|
32
|
+
# @param rule_id [String] the Protect rule name.
|
33
|
+
# @param cached [Contrast::Agent::Protect::InputClassification::CachedResult]
|
34
|
+
# @param request [Contrast::Agent::Request] the current request.
|
35
|
+
def match! rule_id, cached, request
|
36
|
+
return unless Contrast::Agent::Telemetry::Base.enabled?
|
37
|
+
|
38
|
+
push(rule_id, cached)
|
39
|
+
fetch(rule_id, cached.result.input_type)&.increase_match_for_input
|
40
|
+
return unless cached.request_id == request.__id__
|
41
|
+
|
42
|
+
fetch(rule_id, cached.result.input_type)&.increase_match_for_request
|
43
|
+
end
|
44
|
+
|
45
|
+
# This method will handle the statistics for the input mismatch.
|
46
|
+
# Skip if this is the called with empty cache since it's not fair.
|
47
|
+
#
|
48
|
+
# @param rule_id [String] the Protect rule name.
|
49
|
+
# @param input_type [Symbol] Type of the input
|
50
|
+
def mismatch! rule_id, input_type
|
51
|
+
return unless Contrast::Agent::Telemetry::Base.enabled?
|
52
|
+
return if Contrast::Agent::Protect::InputAnalyzer.lru_cache.empty?
|
53
|
+
|
54
|
+
fetch(rule_id, input_type)&.increase_mismatch_for_input
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Array<Contrast::Agent::Telemetry::InputAnalysisCacheEvent>] the events to be sent.
|
58
|
+
def to_events
|
59
|
+
events = []
|
60
|
+
data.each do |_rule_id, match_rates|
|
61
|
+
match_rates.each do |match_rate|
|
62
|
+
event = Contrast::Agent::Telemetry::InputAnalysisCacheEvent.new(match_rate.rule_id, match_rate)
|
63
|
+
next if event.empty?
|
64
|
+
|
65
|
+
events << event
|
66
|
+
end
|
67
|
+
end
|
68
|
+
events
|
69
|
+
rescue StandardError => e
|
70
|
+
logger.error("[IA_LRU_Cache] Error while creating events: #{ e }", stacktrace: e.backtrace)
|
71
|
+
[]
|
72
|
+
end
|
73
|
+
|
74
|
+
# Creates new statisctics for protect rule.
|
75
|
+
#
|
76
|
+
# @param rule_id [String] the Protect rule name.
|
77
|
+
# @param cached [Contrast::Agent::Protect::InputClassification::CachedResult]
|
78
|
+
def push rule_id, cached
|
79
|
+
new_entry = Contrast::Agent::Protect::Rule::InputClassification::MatchRates.
|
80
|
+
new(rule_id, cached.result.input_type, cached.result.score_level)
|
81
|
+
|
82
|
+
@data[rule_id] = [] if Contrast::Utils::DuckUtils.empty_duck?(@data[rule_id])
|
83
|
+
@data[rule_id].shift if @data[rule_id].length >= CAPACITY
|
84
|
+
@data[rule_id] << new_entry unless saved?(rule_id, cached)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Get the statistics for the protect rule.
|
88
|
+
#
|
89
|
+
# @param rule_id [String] the Protect rule name.
|
90
|
+
# @param input_type [Symbol] Type of the input
|
91
|
+
def fetch rule_id, input_type
|
92
|
+
safe_extract(@data[rule_id]&.select { |e| e.input_type == input_type })
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
# Checks if the rate is saved for the input.
|
98
|
+
#
|
99
|
+
# @param rule_id [String] the Protect rule name.
|
100
|
+
# @param new_entry [Contrast::Agent::Protect::InputClassification::CachedResult]
|
101
|
+
def saved? rule_id, new_entry
|
102
|
+
!fetch(rule_id, new_entry&.result&.input_type).nil?
|
103
|
+
end
|
104
|
+
|
105
|
+
# Call this method from the LRU Cache to get the statistics for a given rule_id, so it could be
|
106
|
+
# thread safe.
|
107
|
+
def clear
|
108
|
+
@data.clear
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Contrast
|
5
|
+
module Agent
|
6
|
+
module Protect
|
7
|
+
module Rule
|
8
|
+
module InputClassification
|
9
|
+
# Utils module for Input Classification.
|
10
|
+
module Utils
|
11
|
+
# Extracts data return as array from the cache.
|
12
|
+
#
|
13
|
+
# @param object [NilClass, Array<Contrast::Agent::Protect::InputClassification::CachedResult>]
|
14
|
+
# @return object [Contrast::Agent::Protect::InputClassification::CachedResult]
|
15
|
+
def safe_extract object
|
16
|
+
Array(object)[0]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -4,7 +4,7 @@
|
|
4
4
|
require 'contrast/utils/object_share'
|
5
5
|
require 'contrast/agent/protect/rule/no_sqli/no_sqli'
|
6
6
|
require 'contrast/agent/protect/input_analyzer/input_analyzer'
|
7
|
-
require 'contrast/
|
7
|
+
require 'contrast/agent/protect/rule/input_classification/base'
|
8
8
|
|
9
9
|
module Contrast
|
10
10
|
module Agent
|
@@ -15,7 +15,7 @@ module Contrast
|
|
15
15
|
# to be analyzed at the sink level.
|
16
16
|
module NoSqliInputClassification
|
17
17
|
class << self
|
18
|
-
include
|
18
|
+
include Contrast::Agent::Protect::Rule::InputClassification::Base
|
19
19
|
|
20
20
|
NOSQL_COMMENT_REGEXP = %r{"\s*(?:<--|//)}.cs__freeze
|
21
21
|
NOSQL_OR_REGEXP = /(?=(\s+\|\|\s+))/.cs__freeze
|
@@ -96,8 +96,15 @@ module Contrast
|
|
96
96
|
#
|
97
97
|
# @return res [Contrast::Agent::Reporting::InputAnalysisResult]
|
98
98
|
def create_new_input_result request, rule_id, input_type, value
|
99
|
-
|
100
|
-
|
99
|
+
return unless Contrast::AGENT_LIB
|
100
|
+
|
101
|
+
# Cache retrieve
|
102
|
+
cached = Contrast::Agent::Protect::InputAnalyzer.lru_cache.lookout(rule_id, value, input_type, request)
|
103
|
+
return cached.result if cached.cs__is_a?(Contrast::Agent::Protect::InputClassification::CachedResult)
|
104
|
+
|
105
|
+
eval_value = base64_decode_input(value, input_type)
|
106
|
+
score = evaluate_patterns(eval_value)
|
107
|
+
score = evaluate_rules(eval_value, score)
|
101
108
|
|
102
109
|
score_level = if definite_attack?(score)
|
103
110
|
DEFINITEATTACK
|
@@ -106,9 +113,12 @@ module Contrast
|
|
106
113
|
else
|
107
114
|
IGNORE
|
108
115
|
end
|
109
|
-
|
110
|
-
add_needed_key(request,
|
111
|
-
|
116
|
+
ia_result = new_ia_result(rule_id, input_type, score_level, request.path, value)
|
117
|
+
add_needed_key(request, ia_result, input_type, value) if KEYS_NEEDED.include?(input_type)
|
118
|
+
|
119
|
+
# Cache save. Cache must be saved after the input evaluation is completed.
|
120
|
+
Contrast::Agent::Protect::InputAnalyzer.lru_cache.save(rule_id, ia_result, request)
|
121
|
+
ia_result
|
112
122
|
end
|
113
123
|
|
114
124
|
# This method evaluates the patterns relevant to NoSQL Injection to check whether
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require 'contrast/
|
4
|
+
require 'contrast/agent/protect/rule/input_classification/base'
|
5
5
|
|
6
6
|
module Contrast
|
7
7
|
module Agent
|
@@ -15,27 +15,31 @@ module Contrast
|
|
15
15
|
|
16
16
|
THRESHOLD = 90.cs__freeze
|
17
17
|
class << self
|
18
|
-
include
|
18
|
+
include Contrast::Agent::Protect::Rule::InputClassification::Base
|
19
19
|
|
20
20
|
private
|
21
21
|
|
22
|
-
#
|
23
|
-
# key if needed and Creates new isntance of InputAnalysisResult.
|
22
|
+
# Creates new instance of AgentLib evaluation result with direct call to AgentLib.
|
24
23
|
#
|
25
|
-
# @param request [Contrast::Agent::Request] the current request context.
|
26
24
|
# @param rule_id [String] The name of the Protect Rule.
|
27
25
|
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
28
26
|
# @param value [String, Array<String>] the value of the input.
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
Contrast::AGENT_LIB.rule_set[rule_id],
|
37
|
-
Contrast::AGENT_LIB.eval_option[:PREFER_WORTH_WATCHING])
|
27
|
+
def build_input_eval rule_id, input_type, value
|
28
|
+
Contrast::AGENT_LIB.eval_input(value,
|
29
|
+
Contrast::Agent::Protect::Rule::InputClassification::Base.
|
30
|
+
convert_input_type(input_type),
|
31
|
+
Contrast::AGENT_LIB.rule_set[rule_id],
|
32
|
+
Contrast::AGENT_LIB.eval_option[:PREFER_WORTH_WATCHING])
|
33
|
+
end
|
38
34
|
|
35
|
+
# Creates specific result from the AgentLib evaluation.
|
36
|
+
#
|
37
|
+
# @param rule_id [String] The name of the Protect Rule.
|
38
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
39
|
+
# @param value [String, Array<String>] the value of the input.
|
40
|
+
# @param request [Contrast::Agent::Request] the current request context.
|
41
|
+
# @param input_eval [Contrast::AgentLib::EvalResult] the result of the input evaluation.
|
42
|
+
def build_ia_result rule_id, input_type, value, request, input_eval
|
39
43
|
ia_result = new_ia_result(rule_id, input_type, request.path, value)
|
40
44
|
score = input_eval&.score || 0
|
41
45
|
if score >= THRESHOLD
|
@@ -50,7 +54,6 @@ module Contrast
|
|
50
54
|
else
|
51
55
|
ia_result.score_level = IGNORE
|
52
56
|
end
|
53
|
-
add_needed_key(request, ia_result, input_type, value)
|
54
57
|
ia_result
|
55
58
|
end
|
56
59
|
end
|
@@ -5,7 +5,7 @@ require 'contrast/utils/object_share'
|
|
5
5
|
require 'contrast/agent/reporting/input_analysis/input_type'
|
6
6
|
require 'contrast/agent/reporting/input_analysis/score_level'
|
7
7
|
require 'contrast/agent/protect/input_analyzer/input_analyzer'
|
8
|
-
require 'contrast/
|
8
|
+
require 'contrast/agent/protect/rule/input_classification/base'
|
9
9
|
|
10
10
|
module Contrast
|
11
11
|
module Agent
|
@@ -17,7 +17,7 @@ module Contrast
|
|
17
17
|
module SqliInputClassification
|
18
18
|
WORTHWATCHING_MATCH = 'sqli-worth-watching-v2'.cs__freeze
|
19
19
|
class << self
|
20
|
-
include
|
20
|
+
include Contrast::Agent::Protect::Rule::InputClassification::Base
|
21
21
|
include Contrast::Components::Logger::InstanceMethods
|
22
22
|
end
|
23
23
|
end
|
data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
require 'contrast/utils/object_share'
|
5
5
|
require 'contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload'
|
6
6
|
require 'contrast/agent/protect/input_analyzer/input_analyzer'
|
7
|
-
require 'contrast/
|
7
|
+
require 'contrast/agent/protect/rule/input_classification/base'
|
8
8
|
|
9
9
|
module Contrast
|
10
10
|
module Agent
|
@@ -16,26 +16,30 @@ module Contrast
|
|
16
16
|
UNSAFE_UPLOAD_MATCH = 'unsafe-file-upload-input-tracing-v1'
|
17
17
|
|
18
18
|
class << self
|
19
|
-
include
|
19
|
+
include Contrast::Agent::Protect::Rule::InputClassification::Base
|
20
20
|
|
21
21
|
private
|
22
22
|
|
23
|
-
#
|
24
|
-
# key if needed and Creates new isntance of InputAnalysisResult.
|
23
|
+
# Creates new instance of AgentLib evaluation result with direct call to AgentLib.
|
25
24
|
#
|
26
|
-
# @param request [Contrast::Agent::Request] the current request context.
|
27
25
|
# @param rule_id [String] The name of the Protect Rule.
|
28
|
-
# @param
|
26
|
+
# @param _input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
29
27
|
# @param value [String, Array<String>] the value of the input.
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
Contrast::AGENT_LIB.rule_set[rule_id],
|
37
|
-
Contrast::AGENT_LIB.eval_option[:NONE]))
|
28
|
+
def build_input_eval rule_id, _input_type, value
|
29
|
+
Contrast::AGENT_LIB.eval_input(value,
|
30
|
+
Contrast::AGENT_LIB.input_set[:MULTIPART_NAME],
|
31
|
+
Contrast::AGENT_LIB.rule_set[rule_id],
|
32
|
+
Contrast::AGENT_LIB.eval_option[:NONE])
|
33
|
+
end
|
38
34
|
|
35
|
+
# Creates specific result from the AgentLib evaluation.
|
36
|
+
#
|
37
|
+
# @param rule_id [String] The name of the Protect Rule.
|
38
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
39
|
+
# @param value [String, Array<String>] the value of the input.
|
40
|
+
# @param request [Contrast::Agent::Request] the current request context.
|
41
|
+
# @param input_eval [Contrast::AgentLib::EvalResult] the result of the input evaluation.
|
42
|
+
def build_ia_result rule_id, input_type, value, request, input_eval
|
39
43
|
ia_result = new_ia_result(rule_id, input_type, request.path, value)
|
40
44
|
if input_eval.score >= THRESHOLD
|
41
45
|
ia_result.score_level = DEFINITEATTACK
|
@@ -51,7 +55,6 @@ module Contrast
|
|
51
55
|
else
|
52
56
|
Contrast::Utils::ObjectShare::EMPTY_STRING
|
53
57
|
end
|
54
|
-
|
55
58
|
ia_result
|
56
59
|
end
|
57
60
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require 'contrast/
|
4
|
+
require 'contrast/agent/protect/rule/input_classification/base'
|
5
5
|
|
6
6
|
module Contrast
|
7
7
|
module Agent
|
@@ -13,28 +13,32 @@ module Contrast
|
|
13
13
|
REFLECTED_XSS_MATCH = 'reflected-xss-input-tracing-v1'.cs__freeze
|
14
14
|
WORTHWATCHING_MATCH = 'xss-worth-watching-v2'.cs__freeze
|
15
15
|
class << self
|
16
|
-
include
|
16
|
+
include Contrast::Agent::Protect::Rule::InputClassification::Base
|
17
17
|
|
18
18
|
private
|
19
19
|
|
20
|
-
#
|
21
|
-
# key if needed and Creates new isntance of InputAnalysisResult.
|
20
|
+
# Creates new instance of AgentLib evaluation result with direct call to AgentLib.
|
22
21
|
#
|
23
|
-
# @param request [Contrast::Agent::Request] the current request context.
|
24
22
|
# @param rule_id [String] The name of the Protect Rule.
|
25
23
|
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
26
24
|
# @param value [String, Array<String>] the value of the input.
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
Contrast::AGENT_LIB.
|
36
|
-
eval_option[:PREFER_WORTH_WATCHING])
|
25
|
+
def build_input_eval rule_id, input_type, value
|
26
|
+
Contrast::AGENT_LIB.eval_input(value,
|
27
|
+
Contrast::Agent::Protect::Rule::InputClassification::Base.
|
28
|
+
convert_input_type(input_type),
|
29
|
+
Contrast::AGENT_LIB.rule_set[rule_id],
|
30
|
+
Contrast::AGENT_LIB.
|
31
|
+
eval_option[:PREFER_WORTH_WATCHING])
|
32
|
+
end
|
37
33
|
|
34
|
+
# Creates specific result from the AgentLib evaluation.
|
35
|
+
#
|
36
|
+
# @param rule_id [String] The name of the Protect Rule.
|
37
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
38
|
+
# @param value [String, Array<String>] the value of the input.
|
39
|
+
# @param request [Contrast::Agent::Request] the current request context.
|
40
|
+
# @param input_eval [Contrast::AgentLib::EvalResult] the result of the input evaluation.
|
41
|
+
def build_ia_result rule_id, input_type, value, request, input_eval
|
38
42
|
score = input_eval&.score || 0
|
39
43
|
ia_result = new_ia_result(rule_id, input_type, request.path, value)
|
40
44
|
if score >= THRESHOLD
|
@@ -46,8 +50,6 @@ module Contrast
|
|
46
50
|
else
|
47
51
|
ia_result.score_level = IGNORE
|
48
52
|
end
|
49
|
-
|
50
|
-
add_needed_key(request, ia_result, input_type, value)
|
51
53
|
ia_result
|
52
54
|
end
|
53
55
|
end
|
@@ -5,6 +5,7 @@ require 'contrast/utils/object_share'
|
|
5
5
|
require 'contrast/utils/timer'
|
6
6
|
require 'contrast/agent/reporting/attack_result/response_type'
|
7
7
|
require 'contrast/agent/reporting/attack_result/rasp_rule_sample'
|
8
|
+
require 'contrast/utils/duck_utils'
|
8
9
|
|
9
10
|
module Contrast
|
10
11
|
module Agent
|
@@ -65,6 +66,11 @@ module Contrast
|
|
65
66
|
def details= protect_details
|
66
67
|
@_details = protect_details if protect_details.is_a?(Contrast::Agent::Reporting::Details::ProtectRuleDetails)
|
67
68
|
end
|
69
|
+
|
70
|
+
def empty?
|
71
|
+
Contrast::Utils::DuckUtils.empty_duck?(samples) || Contrast::Utils::DuckUtils.empty_duck?(rule_id) ||
|
72
|
+
response == ::Contrast::Agent::Reporting::ResponseType::NO_ACTION
|
73
|
+
end
|
68
74
|
end
|
69
75
|
end
|
70
76
|
end
|
@@ -9,13 +9,8 @@ module Contrast
|
|
9
9
|
module Reporting
|
10
10
|
# This class will do ia analysis for our protect rules
|
11
11
|
class InputAnalysis
|
12
|
-
|
13
|
-
|
14
|
-
end
|
15
|
-
|
16
|
-
def inputs= extracted_inputs
|
17
|
-
@_inputs = extracted_inputs
|
18
|
-
end
|
12
|
+
# @return [Hash] Stored request inputs for this context.
|
13
|
+
attr_accessor :inputs
|
19
14
|
|
20
15
|
def triggered_rules
|
21
16
|
@_triggered_rules ||= []
|
@@ -61,6 +61,17 @@ module Contrast
|
|
61
61
|
@_key = key if key.is_a?(String)
|
62
62
|
end
|
63
63
|
|
64
|
+
# @param key_type [Symbol]
|
65
|
+
# @return @_key [String]
|
66
|
+
def key_type= key_type
|
67
|
+
@_key_type = key_type if INPUT_TYPE.to_a.include?(key_type)
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return @_key [String]
|
71
|
+
def key_type
|
72
|
+
@_key_type ||= INPUT_TYPE::UNDEFINED_TYPE
|
73
|
+
end
|
74
|
+
|
64
75
|
# @return value [String]
|
65
76
|
def value
|
66
77
|
@_value ||= Contrast::Utils::ObjectShare::EMPTY_STRING
|
@@ -30,13 +30,45 @@ module Contrast
|
|
30
30
|
UNKNOWN = :UNKNOWN.cs__freeze
|
31
31
|
|
32
32
|
class << self
|
33
|
+
# @return
|
33
34
|
def to_a
|
34
|
-
[
|
35
|
+
@_to_a ||= [
|
35
36
|
UNDEFINED_TYPE, BODY, COOKIE_NAME, COOKIE_VALUE, HEADER, PARAMETER_NAME, PARAMETER_VALUE,
|
36
37
|
QUERYSTRING, URI, SOCKET, JSON_VALUE, JSON_ARRAYED_VALUE, MULTIPART_CONTENT_TYPE, MULTIPART_VALUE,
|
37
38
|
MULTIPART_FIELD_NAME, MULTIPART_NAME, XML_VALUE, DWR_VALUE, METHOD, REQUEST, URL_PARAMETER, UNKNOWN
|
38
39
|
]
|
39
40
|
end
|
41
|
+
|
42
|
+
# This is a hash of the input types and their corresponding values.
|
43
|
+
#
|
44
|
+
# @return [Hash]
|
45
|
+
|
46
|
+
def to_hash
|
47
|
+
{
|
48
|
+
UNDEFINED_TYPE: '1',
|
49
|
+
BODY: '2',
|
50
|
+
COOKIE_NAME: '3',
|
51
|
+
COOKIE_VALUE: '4',
|
52
|
+
HEADER: '5',
|
53
|
+
PARAMETER_NAME: '6',
|
54
|
+
PARAMETER_VALUE: '7',
|
55
|
+
QUERYSTRING: '8',
|
56
|
+
URI: '9',
|
57
|
+
SOCKET: '10',
|
58
|
+
JSON_VALUE: '11',
|
59
|
+
JSON_ARRAYED_VALUE: '12',
|
60
|
+
MULTIPART_CONTENT_TYPE: '13',
|
61
|
+
MULTIPART_VALUE: '14',
|
62
|
+
MULTIPART_FIELD_NAME: '15',
|
63
|
+
MULTIPART_NAME: '16',
|
64
|
+
XML_VALUE: '17',
|
65
|
+
DWR_VALUE: '18',
|
66
|
+
METHOD: '19',
|
67
|
+
REQUEST: '20',
|
68
|
+
URL_PARAMETER: '21',
|
69
|
+
UNKNOWN: '22'
|
70
|
+
}
|
71
|
+
end
|
40
72
|
end
|
41
73
|
end
|
42
74
|
end
|
@@ -23,7 +23,7 @@ module Contrast
|
|
23
23
|
hash = URI.decode_www_form(query).to_h
|
24
24
|
mask_with_dictionary(results, hash)
|
25
25
|
# Restore to string form.
|
26
|
-
hash.each { |k, v| masked += "#{ k }
|
26
|
+
hash.each { |k, v| masked += "#{ k }#{ EQUALS }#{ v }#{ AMPERSAND }" }
|
27
27
|
query = masked
|
28
28
|
query.chomp!(masked[-1])
|
29
29
|
end
|
@@ -40,6 +40,7 @@ module Contrast
|
|
40
40
|
# @param attack_result [Contrast::Agent::Reporting::AttackResult]
|
41
41
|
def attach_data attack_result
|
42
42
|
return unless attack_result&.cs__is_a?(Contrast::Agent::Reporting::AttackResult)
|
43
|
+
return if attack_result&.empty?
|
43
44
|
|
44
45
|
attacker_activity = Contrast::Agent::Reporting::ApplicationDefendAttackerActivity.new(ia_request: @request)
|
45
46
|
attacker_activity.attach_data(attack_result)
|
@@ -5,6 +5,7 @@ require 'contrast/components/logger'
|
|
5
5
|
require 'contrast/utils/object_share'
|
6
6
|
require 'contrast/utils/duck_utils'
|
7
7
|
require 'contrast/agent/reporting/reporting_events/reportable_hash'
|
8
|
+
require 'contrast/agent/reporting/attack_result/response_type'
|
8
9
|
require 'contrast/agent/reporting/reporting_events/application_defend_attack_activity'
|
9
10
|
|
10
11
|
module Contrast
|
@@ -118,7 +118,7 @@ module Contrast
|
|
118
118
|
mode.resend.reset_rescue_attempts
|
119
119
|
findings_to_return.each do |index|
|
120
120
|
preflight_message = event.messages[index.to_i]
|
121
|
-
corresponding_finding = Contrast::Agent::Reporting::ReportingStorage.delete(preflight_message
|
121
|
+
corresponding_finding = Contrast::Agent::Reporting::ReportingStorage.delete(preflight_message&.data)
|
122
122
|
next unless corresponding_finding
|
123
123
|
|
124
124
|
send_event(corresponding_finding, connection)
|
@@ -143,6 +143,8 @@ module Contrast
|
|
143
143
|
super
|
144
144
|
delete_queue!
|
145
145
|
Contrast::TELEMETRY_EXCEPTIONS&.clear
|
146
|
+
Contrast::TELEMETRY_IA_CACHE&.clear
|
147
|
+
Contrast::TELEMETRY_BASE64_HASH&.clear
|
146
148
|
end
|
147
149
|
|
148
150
|
private
|
@@ -174,8 +176,7 @@ module Contrast
|
|
174
176
|
break unless attempt_to_start?
|
175
177
|
|
176
178
|
# Start pushing exceptions to queue for reporting.
|
177
|
-
|
178
|
-
Contrast::TELEMETRY_EXCEPTIONS&.clear
|
179
|
+
gather_telemetry_events
|
179
180
|
until queue.empty?
|
180
181
|
event = queue.pop
|
181
182
|
begin
|
@@ -189,6 +190,31 @@ module Contrast
|
|
189
190
|
end
|
190
191
|
end
|
191
192
|
end
|
193
|
+
|
194
|
+
# Fills the queue with events that were not able to be sent previously.
|
195
|
+
def gather_telemetry_events
|
196
|
+
gather_exceptions
|
197
|
+
gather_encoding_events
|
198
|
+
gather_ia_cache_events
|
199
|
+
end
|
200
|
+
|
201
|
+
# Retrieves the exceptions that were accumulated.
|
202
|
+
def gather_exceptions
|
203
|
+
Contrast::TELEMETRY_EXCEPTIONS&.each_value { |value| queue << value }
|
204
|
+
Contrast::TELEMETRY_EXCEPTIONS&.clear
|
205
|
+
end
|
206
|
+
|
207
|
+
# Retrieves the base64 encoded events that were accumulated.
|
208
|
+
def gather_encoding_events
|
209
|
+
Contrast::TELEMETRY_BASE64_HASH&.each_value { |values| values.each { |event| queue << event } }
|
210
|
+
Contrast::TELEMETRY_BASE64_HASH&.clear
|
211
|
+
end
|
212
|
+
|
213
|
+
# Retrieves the IA cache events that were accumulated.
|
214
|
+
def gather_ia_cache_events
|
215
|
+
Contrast::TELEMETRY_IA_CACHE&.each_value { |values| values.each { |event| queue << event } }
|
216
|
+
Contrast::TELEMETRY_IA_CACHE&.clear
|
217
|
+
end
|
192
218
|
end
|
193
219
|
end
|
194
220
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'contrast/agent/telemetry/input_analysis_encoding_event'
|
5
|
+
|
6
|
+
module Contrast
|
7
|
+
module Agent
|
8
|
+
module Telemetry
|
9
|
+
# This hash will store the telemetry data for the Protect InputAnalysis cache.
|
10
|
+
class Base64Hash < Hash
|
11
|
+
include Contrast::Components::Logger::InstanceMethods
|
12
|
+
# Set per request:
|
13
|
+
HASH_SIZE_LIMIT = 100
|
14
|
+
|
15
|
+
# Wrapper to set a value in this Telemetry::Hash only if the provided value is of the data_type for this
|
16
|
+
# Telemetry::CacheHash or the hash has not reached its limit for unique keys.
|
17
|
+
# Saves Array of reportable events.
|
18
|
+
#
|
19
|
+
# @param key [Object] the key to which to associate the value
|
20
|
+
# @param events [array<Object>]
|
21
|
+
# @return [Object, nil] echo back out the value as the Hash#[]= method does, or nil if not of the expected
|
22
|
+
# data_type
|
23
|
+
def []= key, events
|
24
|
+
# If telemetry is not running, do not add more as we want to avoid a memory leak.
|
25
|
+
return unless Contrast::Agent.telemetry_queue&.running?
|
26
|
+
# If the Hash is full, do not add more as we want to avoid consuming all application resources.
|
27
|
+
return if at_limit?
|
28
|
+
# If the given value is of unexpected type, do not add it to avoid issues later where type is assumed.
|
29
|
+
return unless valid_event?(events)
|
30
|
+
|
31
|
+
super(key, events)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Determine if hash has reached exception event limit.
|
35
|
+
#
|
36
|
+
# @return [Boolean]
|
37
|
+
def at_limit?
|
38
|
+
unless length < HASH_SIZE_LIMIT
|
39
|
+
logger.debug("[Telemetry] Number of IA base64 events exceeds limit of #{ HASH_SIZE_LIMIT }")
|
40
|
+
return true
|
41
|
+
end
|
42
|
+
false
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# Checks to see if the given object is a valid event.
|
48
|
+
# @param events [Contrast::Agent::Telemetry::InputAnalysisEncodingEvent]
|
49
|
+
def valid_event? events
|
50
|
+
events&.all?(Contrast::Agent::Telemetry::InputAnalysisEncodingEvent)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|