threatstack-agent-ruby 0.2.1 → 0.2.2
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/ci/requirements.txt +4 -0
- data/ci/trigger.py +408 -0
- data/ext/libinjection/libinjection.h +9 -0
- data/ext/libinjection/libinjection_pathtraversal.c +43 -0
- data/ext/libinjection/libinjection_pathtraversal_data.h +240 -0
- data/ext/libinjection/libinjection_wrap.c +36 -0
- data/lib/constants.rb +29 -5
- data/lib/control.rb +19 -6
- data/lib/events/event_accumulator.rb +16 -7
- data/lib/events/models/dependency_event.rb +3 -16
- data/lib/instrumentation/common.rb +31 -5
- data/lib/instrumentation/frameworks/kernel.rb +39 -0
- data/lib/instrumentation/frameworks/rails.rb +95 -0
- data/lib/instrumentation/frameworks/random.rb +37 -0
- data/lib/instrumentation/instrumenter.rb +111 -30
- data/lib/jobs/delayed_job.rb +2 -2
- data/lib/jobs/event_submitter.rb +22 -37
- data/lib/jobs/job_queue.rb +14 -5
- data/lib/jobs/recurrent_job.rb +2 -2
- data/lib/utils/capped_queue.rb +39 -0
- data/lib/utils/logger.rb +5 -3
- data/threatstack-agent-ruby.gemspec +3 -2
- metadata +13 -6
- data/lib/instrumentation/kernel.rb +0 -45
- data/lib/instrumentation/rails.rb +0 -61
data/lib/jobs/delayed_job.rb
CHANGED
@@ -6,8 +6,8 @@ module Threatstack
|
|
6
6
|
module Jobs
|
7
7
|
|
8
8
|
class DelayedJob
|
9
|
-
def initialize(name_or_logger, initial_delay = 0, args
|
10
|
-
@job = RecurrentJob.new(name_or_logger,1, initial_delay, 1, args, &block)
|
9
|
+
def initialize(name_or_logger, initial_delay = 0, *args, &block)
|
10
|
+
@job = RecurrentJob.new(name_or_logger, 1, initial_delay, 1, *args, &block)
|
11
11
|
end
|
12
12
|
|
13
13
|
def stop
|
data/lib/jobs/event_submitter.rb
CHANGED
@@ -46,40 +46,34 @@ module Threatstack
|
|
46
46
|
end
|
47
47
|
|
48
48
|
def submit
|
49
|
-
@logger.debug 'Starting event submission'
|
50
|
-
|
51
49
|
# fetch events from the accumulator
|
52
|
-
events =
|
53
|
-
@logger.debug "Found a total of #{events.length} events"
|
54
|
-
|
50
|
+
events = @accumulator.remove_events EVENTS_PER_REQ
|
55
51
|
if events.empty?
|
56
52
|
@logger.debug 'No events to report'
|
57
53
|
return
|
58
54
|
end
|
55
|
+
@logger.debug "Found a total of #{events.length} events"
|
59
56
|
|
60
|
-
#
|
61
|
-
events.
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
rescue StandardError => e
|
81
|
-
@logger.error "StandardError: #{e.inspect}"
|
82
|
-
end
|
57
|
+
# convert payload to JSON
|
58
|
+
json_payload = "[#{events.collect(&:to_json_string).join(', ')}]"
|
59
|
+
# submit http request
|
60
|
+
uri = URI.parse(APPSEC_BASE_URL + APPSEC_EVENTS_URL)
|
61
|
+
headers = {
|
62
|
+
'Content-Type' => 'application/json',
|
63
|
+
'bluefyre-agent-id' => AGENT_ID,
|
64
|
+
'bluefyre-agent-instance-id' => AGENT_INSTANCE_ID
|
65
|
+
}
|
66
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
67
|
+
http.use_ssl = true
|
68
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
69
|
+
req = Net::HTTP::Post.new(uri.request_uri, headers)
|
70
|
+
req.body = json_payload
|
71
|
+
@logger.debug "Sending #{events.length} events with payload: #{json_payload}"
|
72
|
+
begin
|
73
|
+
resp = http.request(req)
|
74
|
+
@logger.debug "Sent #{events.length} events, HTTP code: #{resp.code}"
|
75
|
+
rescue StandardError => e
|
76
|
+
@logger.error "StandardError: #{e.inspect}"
|
83
77
|
end
|
84
78
|
end
|
85
79
|
|
@@ -87,15 +81,6 @@ module Threatstack
|
|
87
81
|
start
|
88
82
|
@accumulator.add_event event
|
89
83
|
end
|
90
|
-
|
91
|
-
def fetch_events
|
92
|
-
# check total number of events first
|
93
|
-
total_events = @accumulator.events.length
|
94
|
-
return [] unless total_events > 0
|
95
|
-
|
96
|
-
# retrieve events from the accumulator in a thread-safe manner
|
97
|
-
@accumulator.remove_events total_events
|
98
|
-
end
|
99
84
|
end
|
100
85
|
end
|
101
86
|
end
|
data/lib/jobs/job_queue.rb
CHANGED
@@ -7,22 +7,31 @@ module Threatstack
|
|
7
7
|
attr_reader :job_queue
|
8
8
|
attr_reader :job_thread
|
9
9
|
attr_reader :stopped
|
10
|
+
attr_reader :working
|
10
11
|
|
11
12
|
def initialize
|
12
13
|
@stopped = false
|
14
|
+
@working = false
|
13
15
|
@job_queue = Queue.new
|
14
16
|
@job_thread = Thread.new do
|
15
17
|
# Fetch an item from the worker queue, or wait until one is available
|
16
18
|
while (job = @job_queue.pop)
|
17
|
-
|
18
|
-
|
19
|
+
if @stopped
|
20
|
+
# clear the queue
|
21
|
+
@job_queue.clear
|
22
|
+
break
|
23
|
+
end
|
24
|
+
@working = true
|
25
|
+
job[:action].call(*job[:args])
|
26
|
+
@working = false if @job_queue.empty?
|
19
27
|
end
|
28
|
+
@working = false
|
20
29
|
@stopped = true
|
21
30
|
Thread.stop
|
22
31
|
end
|
23
32
|
end
|
24
33
|
|
25
|
-
def add_job(args
|
34
|
+
def add_job(*args, &block)
|
26
35
|
raise 'Invalid proc provided' unless block_given?
|
27
36
|
|
28
37
|
@job_queue.push(:action => block, :args => args)
|
@@ -30,8 +39,8 @@ module Threatstack
|
|
30
39
|
|
31
40
|
def stop
|
32
41
|
@stopped = true
|
33
|
-
# push
|
34
|
-
add_job
|
42
|
+
# push empty job to stop the while loop
|
43
|
+
add_job(nil) {}
|
35
44
|
end
|
36
45
|
end
|
37
46
|
end
|
data/lib/jobs/recurrent_job.rb
CHANGED
@@ -12,7 +12,7 @@ module Threatstack
|
|
12
12
|
attr_reader :stopped
|
13
13
|
attr_reader :logger
|
14
14
|
|
15
|
-
def initialize(name_or_logger, repeat_every_sec = 10, initial_delay_sec = 0, total_times = nil, args
|
15
|
+
def initialize(name_or_logger, repeat_every_sec = 10, initial_delay_sec = 0, total_times = nil, *args, &block)
|
16
16
|
raise 'Block not specified' unless block_given?
|
17
17
|
|
18
18
|
# create logger or use passed one if available
|
@@ -43,7 +43,7 @@ module Threatstack
|
|
43
43
|
# perform main action
|
44
44
|
begin
|
45
45
|
@logger.debug 'Calling job action'
|
46
|
-
block.call args
|
46
|
+
block.call *args
|
47
47
|
@logger.debug 'Job action called'
|
48
48
|
rescue StandardError => e
|
49
49
|
@logger.error "StandardError in job action: #{e.inspect}"
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
module Threatstack
|
6
|
+
module Utils
|
7
|
+
class CappedQueue
|
8
|
+
|
9
|
+
def initialize(max)
|
10
|
+
@queue = []
|
11
|
+
@max = max
|
12
|
+
@mutex = Mutex.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def length
|
16
|
+
@queue.length
|
17
|
+
end
|
18
|
+
|
19
|
+
def push(*args)
|
20
|
+
@mutex.synchronize do
|
21
|
+
@queue.push(*args)
|
22
|
+
@queue.shift(@queue.length - @max) if @queue.length > @max
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def shift(*args)
|
27
|
+
@mutex.synchronize do
|
28
|
+
@queue.shift(*args)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def clear
|
33
|
+
@mutex.synchronize do
|
34
|
+
@queue.clear
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/utils/logger.rb
CHANGED
@@ -8,15 +8,17 @@ require_relative './formatter'
|
|
8
8
|
module Threatstack
|
9
9
|
module Utils
|
10
10
|
module TSLogger
|
11
|
-
|
11
|
+
COLORS = ['blue', 'gray', 'green', 'yellow', 'cyan', 'magenta'].freeze
|
12
|
+
|
13
|
+
def self.create(name, opts = {})
|
12
14
|
formatter = TSFormatter.new
|
13
15
|
logger = Logger.new STDOUT
|
14
16
|
logger.progname = "TS#{name}"
|
15
17
|
logger.datetime_format = '%FT%T%:z'
|
16
18
|
logger.level = Threatstack::Constants::LOG_LEVEL
|
17
19
|
colors = Threatstack::Constants::LOG_COLORS
|
18
|
-
|
19
|
-
progname = colors ? formatter.send(
|
20
|
+
log_color = opts[:color] || COLORS.sample
|
21
|
+
progname = colors ? formatter.send(log_color, '%16.16s' % logger.progname) : '%16.16s' % logger.progname
|
20
22
|
logger.formatter = proc do |severity, datetime, _progname, msg|
|
21
23
|
pid = Process.pid
|
22
24
|
sev_padded = '%5.5s' % severity
|
@@ -5,7 +5,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = 'threatstack-agent-ruby'
|
8
|
-
spec.version = '0.2.
|
8
|
+
spec.version = '0.2.2'
|
9
9
|
spec.authors = ['Threat Stack Inc']
|
10
10
|
spec.email = ['support@threatstack.com']
|
11
11
|
spec.summary = 'Ruby version of the ThreatStack agent which helps identify security vulnerabilities at runtime'
|
@@ -13,7 +13,8 @@ Gem::Specification.new do |spec|
|
|
13
13
|
spec.homepage = 'https://www.threatstack.com'
|
14
14
|
spec.extra_rdoc_files = [
|
15
15
|
"LICENSE"
|
16
|
-
]
|
16
|
+
]
|
17
|
+
spec.required_ruby_version = '>= 1.8.7'
|
17
18
|
|
18
19
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
19
20
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^((test|spec|features|)/|Gemfile_release|Rakefile|README.md|.gitlab-ci.yml|.rubocop.yml|Gemfile.lock|.gitignore)}) }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: threatstack-agent-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Threat Stack Inc
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-03-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: network_interface
|
@@ -161,11 +161,15 @@ extra_rdoc_files:
|
|
161
161
|
files:
|
162
162
|
- Gemfile
|
163
163
|
- LICENSE
|
164
|
+
- ci/requirements.txt
|
165
|
+
- ci/trigger.py
|
164
166
|
- ext/libinjection/extconf.rb
|
165
167
|
- ext/libinjection/libinjection.h
|
166
168
|
- ext/libinjection/libinjection.i
|
167
169
|
- ext/libinjection/libinjection_html5.c
|
168
170
|
- ext/libinjection/libinjection_html5.h
|
171
|
+
- ext/libinjection/libinjection_pathtraversal.c
|
172
|
+
- ext/libinjection/libinjection_pathtraversal_data.h
|
169
173
|
- ext/libinjection/libinjection_sqli.c
|
170
174
|
- ext/libinjection/libinjection_sqli.h
|
171
175
|
- ext/libinjection/libinjection_sqli_data.h
|
@@ -182,15 +186,17 @@ files:
|
|
182
186
|
- lib/events/models/instrumentation_event.rb
|
183
187
|
- lib/exceptions/request_blocked_error.rb
|
184
188
|
- lib/instrumentation/common.rb
|
189
|
+
- lib/instrumentation/frameworks/kernel.rb
|
190
|
+
- lib/instrumentation/frameworks/rails.rb
|
191
|
+
- lib/instrumentation/frameworks/random.rb
|
185
192
|
- lib/instrumentation/instrumenter.rb
|
186
|
-
- lib/instrumentation/kernel.rb
|
187
|
-
- lib/instrumentation/rails.rb
|
188
193
|
- lib/jobs/delayed_job.rb
|
189
194
|
- lib/jobs/event_submitter.rb
|
190
195
|
- lib/jobs/job_queue.rb
|
191
196
|
- lib/jobs/recurrent_job.rb
|
192
197
|
- lib/threatstack-agent-ruby.rb
|
193
198
|
- lib/utils/aws_utils.rb
|
199
|
+
- lib/utils/capped_queue.rb
|
194
200
|
- lib/utils/formatter.rb
|
195
201
|
- lib/utils/logger.rb
|
196
202
|
- threatstack-agent-ruby.gemspec
|
@@ -206,14 +212,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
206
212
|
requirements:
|
207
213
|
- - ">="
|
208
214
|
- !ruby/object:Gem::Version
|
209
|
-
version:
|
215
|
+
version: 1.8.7
|
210
216
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
211
217
|
requirements:
|
212
218
|
- - ">="
|
213
219
|
- !ruby/object:Gem::Version
|
214
220
|
version: '0'
|
215
221
|
requirements: []
|
216
|
-
|
222
|
+
rubyforge_project:
|
223
|
+
rubygems_version: 2.7.6
|
217
224
|
signing_key:
|
218
225
|
specification_version: 4
|
219
226
|
summary: Ruby version of the ThreatStack agent which helps identify security vulnerabilities
|
@@ -1,45 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative './common.rb'
|
4
|
-
require_relative './instrumenter.rb'
|
5
|
-
require_relative '../utils/logger'
|
6
|
-
|
7
|
-
module Threatstack
|
8
|
-
module Instrumentation
|
9
|
-
module TSKernel
|
10
|
-
@@logger = Threatstack::Utils::TSLogger.create 'KernelInstrumentation'
|
11
|
-
|
12
|
-
# methods to wrap
|
13
|
-
METHOD_NAMES = ['exec', 'system', '`'].freeze
|
14
|
-
|
15
|
-
def self.wrap_methods
|
16
|
-
# executed every time a wrapped method is called
|
17
|
-
on_method_call = Proc.new do |params|
|
18
|
-
module_name = params[:target_class].name.downcase
|
19
|
-
method_name = params[:method_name].downcase
|
20
|
-
called_by = params[:caller_loc] ? params[:caller_loc].first : nil
|
21
|
-
file_path = called_by ? called_by.absolute_path : nil
|
22
|
-
# special case for ` method emulation
|
23
|
-
if method_name == '`' && !file_path.nil? && file_path =~ /.*\/kernel\/agnostics\.rb$/
|
24
|
-
called_by = params[:caller_loc][1]
|
25
|
-
file_path = called_by ? called_by.absolute_path : nil
|
26
|
-
end
|
27
|
-
line_num = called_by ? called_by.lineno : nil
|
28
|
-
|
29
|
-
arg = params[:args] ? params[:args].first : nil
|
30
|
-
args = arg ? [arg] : []
|
31
|
-
|
32
|
-
# create and queue the event
|
33
|
-
Threatstack::Instrumentation.create_instrumentation_event(module_name, method_name, file_path, line_num, args)
|
34
|
-
end
|
35
|
-
@@logger.info "Instrumenting Kernel methods: #{METHOD_NAMES}"
|
36
|
-
instrumenter = Threatstack::Instrumentation::Instrumenter.instance
|
37
|
-
METHOD_NAMES.each do |method_name|
|
38
|
-
instrumenter.wrap_class_method(Kernel, method_name, &on_method_call)
|
39
|
-
instrumenter.wrap_instance_method(Kernel, method_name, &on_method_call)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
@@ -1,61 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'json'
|
4
|
-
|
5
|
-
require_relative '../constants'
|
6
|
-
require_relative '../exceptions/request_blocked_error'
|
7
|
-
require_relative './common'
|
8
|
-
require_relative '../utils/logger'
|
9
|
-
|
10
|
-
module Threatstack
|
11
|
-
module Instrumentation
|
12
|
-
module TSRails
|
13
|
-
@@logger = Threatstack::Utils::TSLogger.create 'RailsInstrumentation'
|
14
|
-
|
15
|
-
def self.patch_action_controller
|
16
|
-
@@logger.info 'Looking for Rails gem'
|
17
|
-
return unless defined?(::Rails) && defined?(::Rails::VERSION)
|
18
|
-
return unless defined?(ActionController) && defined?(ActionController::Base)
|
19
|
-
|
20
|
-
@@logger.info "Rails #{Rails::VERSION::MAJOR.to_s} gem found, instrumenting ActionController"
|
21
|
-
ActionController::Base.class_eval do
|
22
|
-
include Threatstack::Instrumentation::TSRails::TSActionController
|
23
|
-
end
|
24
|
-
@@logger.info 'Rails instrumentation done'
|
25
|
-
end
|
26
|
-
|
27
|
-
module TSActionController
|
28
|
-
include Threatstack::Constants
|
29
|
-
@@logger = Threatstack::Utils::TSLogger.create 'RailsInstrumentation'
|
30
|
-
|
31
|
-
def process_action(*args)
|
32
|
-
# we need the headers hack below because Rails adds a lot of internal headers
|
33
|
-
headers = request.headers.each_with_object({}) do |(k, v), obj|
|
34
|
-
obj[k] = v if k.in?(CGI_VARIABLES) || k =~ /^HTTP_/
|
35
|
-
end
|
36
|
-
@@logger.debug("Incoming request: #{{ :headers => headers, :path => request.path_parameters,
|
37
|
-
:query => Threatstack::Instrumentation.drop_sensitive_fields(request.query_parameters),
|
38
|
-
:body => Threatstack::Instrumentation.drop_sensitive_fields(request.request_parameters) }}")
|
39
|
-
backtrace = caller.join("\n")
|
40
|
-
|
41
|
-
# check path/query/body parameters
|
42
|
-
path_res = Threatstack::Instrumentation.check_parameters(request.path_parameters, 'path', request, headers, backtrace)
|
43
|
-
query_res = Threatstack::Instrumentation.check_parameters(request.query_parameters, 'query', request, headers, backtrace)
|
44
|
-
body_res = Threatstack::Instrumentation.check_parameters(request.request_parameters, 'body', request, headers, backtrace)
|
45
|
-
|
46
|
-
@@logger.debug "RequestStats -- Path: #{path_res}, Query: #{query_res}, Body: #{body_res}"
|
47
|
-
sqli_found = (path_res[:sqli] || query_res[:sqli] || body_res[:sqli])
|
48
|
-
xss_found = (path_res[:xss] || query_res[:xss] || body_res[:xss])
|
49
|
-
# raise an exception if any attack payloads were detected and blocking is enabled
|
50
|
-
if (BLOCK_SQLI && sqli_found) || (BLOCK_XSS && xss_found)
|
51
|
-
raise Threatstack::Exceptions::RequestBlockedError, REQUEST_BLOCKED
|
52
|
-
end
|
53
|
-
|
54
|
-
# continue processing the request normally if no issues were found
|
55
|
-
super
|
56
|
-
end
|
57
|
-
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|