tcell_agent 2.1.0 → 2.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/bin/tcell_agent +42 -146
- data/lib/tcell_agent.rb +8 -16
- data/lib/tcell_agent/agent.rb +76 -46
- data/lib/tcell_agent/config_initializer.rb +66 -0
- data/lib/tcell_agent/configuration.rb +72 -267
- data/lib/tcell_agent/instrument_servers.rb +14 -18
- data/lib/tcell_agent/instrumentation/cmdi.rb +15 -15
- data/lib/tcell_agent/instrumentation/lfi.rb +16 -5
- data/lib/tcell_agent/instrumentation/monkey_patches/kernel.rb +39 -100
- data/lib/tcell_agent/logger.rb +1 -2
- data/lib/tcell_agent/rails/auth/authlogic.rb +49 -44
- data/lib/tcell_agent/rails/auth/authlogic_helper.rb +20 -0
- data/lib/tcell_agent/rails/auth/devise.rb +103 -102
- data/lib/tcell_agent/rails/auth/devise_helper.rb +29 -0
- data/lib/tcell_agent/rails/auth/doorkeeper.rb +54 -58
- data/lib/tcell_agent/{userinfo.rb → rails/auth/userinfo.rb} +0 -0
- data/lib/tcell_agent/rails/csrf_exception.rb +0 -8
- data/lib/tcell_agent/rails/dlp.rb +0 -4
- data/lib/tcell_agent/rails/middleware/global_middleware.rb +4 -1
- data/lib/tcell_agent/rails/{on_start.rb → railties/tcell_agent_railties.rb} +9 -16
- data/lib/tcell_agent/rails/railties/tcell_agent_unicorn_railties.rb +8 -0
- data/lib/tcell_agent/rails/routes.rb +3 -6
- data/lib/tcell_agent/rails/routes/grape.rb +4 -12
- data/lib/tcell_agent/rails/tcell_body_proxy.rb +0 -1
- data/lib/tcell_agent/rust/agent_config.rb +43 -32
- data/lib/tcell_agent/rust/{libtcellagent-4.17.1.dylib → libtcellagent-6.2.1.dylib} +0 -0
- data/lib/tcell_agent/rust/{libtcellagent-4.17.1.so → libtcellagent-6.2.1.so} +0 -0
- data/lib/tcell_agent/rust/{libtcellagent-alpine-4.17.1.so → libtcellagent-alpine-6.2.1.so} +0 -0
- data/lib/tcell_agent/rust/models.rb +9 -0
- data/lib/tcell_agent/rust/native_agent.rb +18 -0
- data/lib/tcell_agent/rust/native_library.rb +2 -1
- data/lib/tcell_agent/rust/{tcellagent-4.17.1.dll → tcellagent-6.2.1.dll} +0 -0
- data/lib/tcell_agent/servers/puma.rb +7 -7
- data/lib/tcell_agent/servers/rack_puma_handler.rb +23 -0
- data/lib/tcell_agent/servers/rails_server.rb +4 -4
- data/lib/tcell_agent/servers/unicorn.rb +1 -1
- data/lib/tcell_agent/servers/webrick.rb +0 -1
- data/lib/tcell_agent/settings_reporter.rb +0 -79
- data/lib/tcell_agent/tcell_context.rb +1 -1
- data/lib/tcell_agent/version.rb +1 -1
- data/spec/lib/tcell_agent/configuration_spec.rb +62 -212
- data/spec/lib/tcell_agent/instrument_servers_spec.rb +95 -0
- data/spec/lib/tcell_agent/instrumentation/cmdi_spec.rb +46 -4
- data/spec/lib/tcell_agent/instrumentation/lfi_spec.rb +47 -2
- data/spec/lib/tcell_agent/rust/agent_config_spec.rb +27 -0
- data/spec/lib/tcell_agent/settings_reporter_spec.rb +0 -73
- data/spec/spec_helper.rb +6 -0
- data/spec/support/builders.rb +6 -6
- data/spec/support/server_mocks/passenger_mock.rb +7 -0
- data/spec/support/server_mocks/puma_mock.rb +17 -0
- data/spec/support/server_mocks/rails_mock.rb +7 -0
- data/spec/support/server_mocks/thin_mock.rb +7 -0
- data/spec/support/server_mocks/unicorn_mock.rb +11 -0
- metadata +27 -14
- data/lib/tcell_agent/authlogic.rb +0 -23
- data/lib/tcell_agent/config/unknown_options.rb +0 -119
- data/lib/tcell_agent/devise.rb +0 -33
- data/lib/tcell_agent/rails/start_agent_after_initializers.rb +0 -12
- data/spec/lib/tcell_agent/config/unknown_options_spec.rb +0 -195
data/lib/tcell_agent/version.rb
CHANGED
@@ -2,228 +2,78 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
module TCellAgent
|
4
4
|
describe Configuration do
|
5
|
-
describe '
|
6
|
-
context '
|
7
|
-
it 'should
|
8
|
-
|
5
|
+
describe 'should_instrument?' do
|
6
|
+
context 'with the agent disabled' do
|
7
|
+
it 'should return false' do
|
8
|
+
config = Configuration.new
|
9
|
+
config.enabled = false
|
9
10
|
|
10
|
-
expect(
|
11
|
-
File.join(Dir.getwd, 'tcell/logs/tcell_agent.log')
|
12
|
-
)
|
13
|
-
expect(configuration.config_filename).to eq(
|
14
|
-
File.join(Dir.getwd, 'config/tcell_agent.config')
|
15
|
-
)
|
11
|
+
expect(config.should_instrument?).to be_falsey
|
16
12
|
end
|
17
13
|
end
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
it 'should set config filename to default, cache file and log file are updated' do
|
40
|
-
old_tcell_agent_home = ENV['TCELL_AGENT_HOME']
|
41
|
-
old_tcell_agent_log_dir = ENV['TCELL_AGENT_LOG_DIR']
|
42
|
-
|
43
|
-
ENV['TCELL_AGENT_HOME'] = 'spec_tcell_home'
|
44
|
-
ENV['TCELL_AGENT_LOG_DIR'] = 'spec_tcell_log_dir'
|
45
|
-
|
46
|
-
configuration = Configuration.new
|
47
|
-
|
48
|
-
expect(configuration.log_filename).to eq(
|
49
|
-
'spec_tcell_log_dir/tcell_agent.log'
|
50
|
-
)
|
51
|
-
expect(configuration.config_filename).to eq(
|
52
|
-
File.join(Dir.getwd, 'config/tcell_agent.config')
|
53
|
-
)
|
54
|
-
|
55
|
-
ENV['TCELL_AGENT_HOME'] = old_tcell_agent_home
|
56
|
-
ENV['TCELL_AGENT_LOG_DIR'] = old_tcell_agent_log_dir
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
context 'TCELL_AGENT_HOME, TCELL_AGENT_LOG_DIR, and TCELL_AGENT_CONFIG defined ' do
|
61
|
-
it 'should update config filename, cache file, and log file' do
|
62
|
-
old_tcell_agent_home = ENV['TCELL_AGENT_HOME']
|
63
|
-
old_tcell_agent_log_dir = ENV['TCELL_AGENT_LOG_DIR']
|
64
|
-
old_config_filename = ENV['TCELL_AGENT_CONFIG']
|
65
|
-
|
66
|
-
ENV['TCELL_AGENT_HOME'] = 'spec_tcell_home'
|
67
|
-
ENV['TCELL_AGENT_LOG_DIR'] = 'spec_tcell_log_dir'
|
68
|
-
ENV['TCELL_AGENT_CONFIG'] = 'spec_config/tcell_agent.config'
|
69
|
-
|
70
|
-
configuration = Configuration.new
|
71
|
-
|
72
|
-
expect(configuration.log_filename).to eq(
|
73
|
-
'spec_tcell_log_dir/tcell_agent.log'
|
74
|
-
)
|
75
|
-
expect(configuration.config_filename).to eq(
|
76
|
-
'spec_config/tcell_agent.config'
|
77
|
-
)
|
78
|
-
|
79
|
-
ENV['TCELL_AGENT_HOME'] = old_tcell_agent_home
|
80
|
-
ENV['TCELL_AGENT_LOG_DIR'] = old_tcell_agent_log_dir
|
81
|
-
ENV['TCELL_AGENT_CONFIG'] = old_config_filename
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
describe '#data_exposure' do
|
87
|
-
context 'no data_exposure defined' do
|
88
|
-
it 'should set max_data_ex_db_records_per_request to default' do
|
89
|
-
no_data_ex = double(
|
90
|
-
'no_data_ex',
|
91
|
-
:read => {
|
92
|
-
:version => 1,
|
93
|
-
:applications => [
|
94
|
-
:app_id => 'app_id',
|
95
|
-
:name => 'test',
|
96
|
-
:api_key => 'api_key'
|
97
|
-
]
|
98
|
-
}.to_json
|
99
|
-
)
|
100
|
-
expect(File).to receive(:file?).with(
|
101
|
-
File.join(Dir.getwd, 'no_data_ex.config')
|
102
|
-
).and_return(true)
|
103
|
-
expect(File).to receive(:open).with(
|
104
|
-
File.join(Dir.getwd, 'no_data_ex.config')
|
105
|
-
).and_return(no_data_ex)
|
106
|
-
configuration = Configuration.new('no_data_ex.config')
|
107
|
-
|
108
|
-
expect(configuration.max_data_ex_db_records_per_request).to eq(1000)
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
context 'data_exposure is empty' do
|
113
|
-
it 'should set max_data_ex_db_records_per_request to default' do
|
114
|
-
no_data_ex = double(
|
115
|
-
'no_data_ex',
|
116
|
-
:read => {
|
117
|
-
:version => 1,
|
118
|
-
:applications => [
|
119
|
-
:app_id => 'app_id',
|
120
|
-
:name => 'test',
|
121
|
-
:api_key => 'api_key',
|
122
|
-
:data_exposure => {}
|
123
|
-
]
|
124
|
-
}.to_json
|
125
|
-
)
|
126
|
-
expect(File).to receive(:file?).with(
|
127
|
-
File.join(Dir.getwd, 'no_data_ex.config')
|
128
|
-
).and_return(true)
|
129
|
-
expect(File).to receive(:open).with(
|
130
|
-
File.join(Dir.getwd, 'no_data_ex.config')
|
131
|
-
).and_return(no_data_ex)
|
132
|
-
configuration = Configuration.new('no_data_ex.config')
|
133
|
-
|
134
|
-
expect(configuration.max_data_ex_db_records_per_request).to eq(1000)
|
14
|
+
context 'with the agent enabled' do
|
15
|
+
context 'with all instrumentation enabled' do
|
16
|
+
context 'with no parameters' do
|
17
|
+
it 'should return true' do
|
18
|
+
config = Configuration.new
|
19
|
+
config.enabled = true
|
20
|
+
config.instrument = true
|
21
|
+
|
22
|
+
expect(config.should_instrument?).to be_truthy
|
23
|
+
end
|
24
|
+
end
|
25
|
+
context 'with parameters' do
|
26
|
+
it 'should return true' do
|
27
|
+
config = Configuration.new
|
28
|
+
config.enabled = true
|
29
|
+
config.instrument = true
|
30
|
+
config.disabled_instrumentation = Set.new
|
31
|
+
|
32
|
+
expect(config.should_instrument?('devise')).to be_truthy
|
33
|
+
end
|
34
|
+
end
|
135
35
|
end
|
136
|
-
|
36
|
+
context 'with auth frameworks disabled' do
|
37
|
+
it 'should return false' do
|
38
|
+
config = Configuration.new
|
39
|
+
config.disabled_instrumentation = Set.new(%w[authlogic devise doorkeeper])
|
137
40
|
|
138
|
-
|
139
|
-
|
140
|
-
no_data_ex = double(
|
141
|
-
'no_data_ex',
|
142
|
-
:read => {
|
143
|
-
:version => 1,
|
144
|
-
:applications => [
|
145
|
-
:app_id => 'app_id',
|
146
|
-
:name => 'test',
|
147
|
-
:api_key => 'api_key',
|
148
|
-
:data_exposure => {
|
149
|
-
:max_data_ex_db_records_per_request => 5000
|
150
|
-
}
|
151
|
-
]
|
152
|
-
}.to_json
|
153
|
-
)
|
154
|
-
expect(File).to receive(:file?).with(
|
155
|
-
File.join(Dir.getwd, 'no_data_ex.config')
|
156
|
-
).and_return(true)
|
157
|
-
expect(File).to receive(:open).with(
|
158
|
-
File.join(Dir.getwd, 'no_data_ex.config')
|
159
|
-
).and_return(no_data_ex)
|
160
|
-
configuration = Configuration.new('no_data_ex.config')
|
161
|
-
|
162
|
-
expect(configuration.max_data_ex_db_records_per_request).to eq(5000)
|
41
|
+
expect(config.should_instrument?('devise')).to be_falsey
|
42
|
+
end
|
163
43
|
end
|
164
44
|
end
|
165
45
|
end
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
:applications => [
|
176
|
-
:app_id => 'app_id',
|
177
|
-
:api_key => 'api_key',
|
178
|
-
:allow_payloads => false
|
179
|
-
]
|
180
|
-
}.to_json
|
181
|
-
)
|
182
|
-
expect(File).to receive(:file?).with(
|
183
|
-
File.join(Dir.getwd, 'config/tcell_agent.config')
|
184
|
-
).and_return(true)
|
185
|
-
expect(File).to receive(:open).with(
|
186
|
-
File.join(Dir.getwd, 'config/tcell_agent.config')
|
187
|
-
).and_return(allow_payloads_enabled)
|
188
|
-
|
189
|
-
configuration = Configuration.new
|
190
|
-
|
191
|
-
expect(configuration.allow_payloads).to eq(false)
|
192
|
-
end
|
46
|
+
describe 'populate_configuration' do
|
47
|
+
context 'with a poor native_agent_config_response' do
|
48
|
+
it 'should not throw an error' do
|
49
|
+
native_agent_config_response = {}
|
50
|
+
|
51
|
+
config = Configuration.new
|
52
|
+
expect do
|
53
|
+
config.populate_configuration(native_agent_config_response)
|
54
|
+
end.not_to raise_error
|
193
55
|
end
|
194
56
|
end
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
File.join(Dir.getwd, 'config/tcell_agent.config')
|
216
|
-
).and_return(true)
|
217
|
-
expect(File).to receive(:open).with(
|
218
|
-
File.join(Dir.getwd, 'config/tcell_agent.config')
|
219
|
-
).and_return(allow_payloads_enabled)
|
220
|
-
|
221
|
-
configuration = Configuration.new
|
222
|
-
|
223
|
-
ENV['TCELL_AGENT_ALLOW_PAYLOADS'] = old_tcell_agent_allow_payloads
|
224
|
-
|
225
|
-
expect(configuration.allow_payloads).to eq(false)
|
226
|
-
end
|
57
|
+
context 'with an elaborate native_agent_config_response' do
|
58
|
+
it 'should set all the correct configurations' do
|
59
|
+
native_agent_config_response = { 'enabled' => true,
|
60
|
+
'disabled_instrumentation' => %w[devise doorkeeper],
|
61
|
+
'update_policy' => 'true',
|
62
|
+
'applications' => { 'first' => { 'app_id' => 'app_id_placeholder',
|
63
|
+
'api_key' => 'api_key_paceholder',
|
64
|
+
'hmac_key' => 'hmac_key_placeholder',
|
65
|
+
'password_hmac_key' => 'password_hmac_key_placeholder',
|
66
|
+
'proxy_config' => { 'reverse_proxy' => true,
|
67
|
+
'reverse_proxy_ip_address_header' => 'X-Forwarded-For' } } },
|
68
|
+
'endpoint_config' => { 'api_url' => 'https://us.agent.tcell.insight.rapid7.com/api/v1' },
|
69
|
+
'ruby_config' => { 'enable_policy_polling' => true } }
|
70
|
+
|
71
|
+
config = Configuration.new
|
72
|
+
config.populate_configuration(native_agent_config_response)
|
73
|
+
|
74
|
+
expect(config.disabled_instrumentation).to be_a(Set)
|
75
|
+
expect(config.disabled_instrumentation).to include('devise', 'doorkeeper')
|
76
|
+
expect(config.enable_intercept_requests).to be_truthy
|
227
77
|
end
|
228
78
|
end
|
229
79
|
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
def test_rails
|
4
|
+
expect(Rails::Server.instance_methods.include?(:tcell_build_app)).to be_truthy
|
5
|
+
end
|
6
|
+
|
7
|
+
def test_thin
|
8
|
+
expect(Thin::Server.instance_methods.include?(:original_start)).to be_truthy
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_unicorn
|
12
|
+
expect(Unicorn::HttpServer::START_CTX[0]).to be_falsy
|
13
|
+
expect(Unicorn::HttpServer.instance_methods.include?(:tcell_init_worker_process)).to be_truthy
|
14
|
+
expect(Unicorn::HttpServer.instance_methods.include?(:tcell_load_config!)).to be_truthy
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_passenger
|
18
|
+
expect(PhusionPassenger::LoaderSharedHelpers.instance_methods.include?(:tcell_before_handling_requests))
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_puma
|
22
|
+
expect(Puma.cli_config.options[:preload_app]).to be_falsey
|
23
|
+
expect(Puma::Server.instance_methods.include?(:tcell_original_run)).to be_truthy
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_server(filenames, funcs)
|
27
|
+
fork do
|
28
|
+
filenames.each do |file|
|
29
|
+
load file
|
30
|
+
end
|
31
|
+
|
32
|
+
load 'tcell_agent/instrument_servers.rb'
|
33
|
+
|
34
|
+
funcs.each do |func|
|
35
|
+
method(func).call
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'instrument_servers' do
|
41
|
+
context 'with single server dependency' do
|
42
|
+
context 'with webrick server' do
|
43
|
+
it 'should instrument Webrick' do
|
44
|
+
mocks = ['spec/support/server_mocks/rails_mock.rb']
|
45
|
+
tests = [:test_rails]
|
46
|
+
test_server(mocks, tests)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'with Thin server' do
|
51
|
+
it 'should instrument Thin' do
|
52
|
+
mocks = ['spec/support/server_mocks/thin_mock.rb']
|
53
|
+
tests = [:test_thin]
|
54
|
+
test_server(mocks, tests)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'with Puma server' do
|
59
|
+
it 'should instrument Puma' do
|
60
|
+
mocks = ['spec/support/server_mocks/puma_mock.rb']
|
61
|
+
tests = [:test_puma]
|
62
|
+
test_server(mocks, tests)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'with Unicorn server' do
|
67
|
+
it 'should instrument Unicorn' do
|
68
|
+
mocks = ['spec/support/server_mocks/unicorn_mock.rb']
|
69
|
+
tests = [:test_unicorn]
|
70
|
+
test_server(mocks, tests)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'with Passenger server' do
|
75
|
+
it 'should instrument Unicorn' do
|
76
|
+
mocks = ['spec/support/server_mocks/passenger_mock.rb']
|
77
|
+
tests = [:test_passenger]
|
78
|
+
test_server(mocks, tests)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
context 'with multiple server dependencies' do
|
83
|
+
it 'should instrument all servers available' do
|
84
|
+
mocks = ['spec/support/server_mocks/rails_mock.rb',
|
85
|
+
'spec/support/server_mocks/thin_mock.rb',
|
86
|
+
'spec/support/server_mocks/puma_mock.rb',
|
87
|
+
'spec/support/server_mocks/unicorn_mock.rb',
|
88
|
+
'spec/support/server_mocks/passenger_mock.rb']
|
89
|
+
|
90
|
+
tests = %i[test_rails test_thin test_puma test_unicorn test_passenger]
|
91
|
+
|
92
|
+
test_server(mocks, tests)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -148,10 +148,52 @@ module TCellAgent
|
|
148
148
|
end
|
149
149
|
end
|
150
150
|
describe '.parse_command_from_open' do
|
151
|
-
context 'with
|
152
|
-
it 'should
|
153
|
-
cmd = TCellAgent::Cmdi.parse_command_from_open
|
154
|
-
expect(cmd).to eq('
|
151
|
+
context 'with empty parameters' do
|
152
|
+
it 'should not return a command' do
|
153
|
+
cmd = TCellAgent::Cmdi.parse_command_from_open
|
154
|
+
expect(cmd).to eq('')
|
155
|
+
end
|
156
|
+
end
|
157
|
+
context 'with empty array' do
|
158
|
+
it 'should not return a command' do
|
159
|
+
args = []
|
160
|
+
cmd = TCellAgent::Cmdi.parse_command_from_open(*args)
|
161
|
+
expect(cmd).to eq('')
|
162
|
+
end
|
163
|
+
end
|
164
|
+
context 'with a string command' do
|
165
|
+
context 'with an empty string' do
|
166
|
+
it 'should return an empty string' do
|
167
|
+
cmd = TCellAgent::Cmdi.parse_command_from_open('')
|
168
|
+
expect(cmd).to eq('')
|
169
|
+
end
|
170
|
+
end
|
171
|
+
context 'with an empty command' do
|
172
|
+
it 'should return an empty string' do
|
173
|
+
cmd = TCellAgent::Cmdi.parse_command_from_open('|')
|
174
|
+
expect(cmd).to eq('')
|
175
|
+
end
|
176
|
+
end
|
177
|
+
context 'with a non-empty command' do
|
178
|
+
it 'should parse the command properly' do
|
179
|
+
cmd = TCellAgent::Cmdi.parse_command_from_open('|echo')
|
180
|
+
expect(cmd).to eq('echo')
|
181
|
+
|
182
|
+
cmd = TCellAgent::Cmdi.parse_command_from_open('|ls -l /tmp')
|
183
|
+
expect(cmd).to eq('ls -l /tmp')
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
context 'with a filename argument' do
|
188
|
+
it 'should not return a command' do
|
189
|
+
cmd = TCellAgent::Cmdi.parse_command_from_open('/tmp')
|
190
|
+
expect(cmd).to eq('')
|
191
|
+
end
|
192
|
+
end
|
193
|
+
context 'with a non-string first argument' do
|
194
|
+
it 'should return an empty string' do
|
195
|
+
cmd = TCellAgent::Cmdi.parse_command_from_open({})
|
196
|
+
expect(cmd).to eq('')
|
155
197
|
end
|
156
198
|
end
|
157
199
|
end
|