zabbix_manager 5.1.6 → 6.0.1
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/CHANGELOG.md +44 -1
- data/README.md +73 -45
- data/lib/zabbix_manager/classes/actions.rb +3 -11
- data/lib/zabbix_manager/classes/applications.rb +2 -8
- data/lib/zabbix_manager/classes/configurations.rb +4 -9
- data/lib/zabbix_manager/classes/{drules.rb → discovery_rules.rb} +2 -8
- data/lib/zabbix_manager/classes/errors.rb +3 -0
- data/lib/zabbix_manager/classes/events.rb +2 -1
- data/lib/zabbix_manager/classes/graphs.rb +19 -15
- data/lib/zabbix_manager/classes/{hostgroups.rb → host_groups.rb} +5 -11
- data/lib/zabbix_manager/classes/host_interfaces.rb +261 -0
- data/lib/zabbix_manager/classes/hosts.rb +139 -89
- data/lib/zabbix_manager/classes/{httptests.rb → http_tests.rb} +2 -8
- data/lib/zabbix_manager/classes/items.rb +139 -92
- data/lib/zabbix_manager/classes/maintenance.rb +2 -1
- data/lib/zabbix_manager/classes/{mediatypes.rb → media_types.rb} +2 -7
- data/lib/zabbix_manager/classes/problems.rb +11 -40
- data/lib/zabbix_manager/classes/proxies.rb +7 -15
- data/lib/zabbix_manager/classes/proxy_groups.rb +16 -0
- data/lib/zabbix_manager/classes/roles.rb +13 -19
- data/lib/zabbix_manager/classes/screens.rb +11 -30
- data/lib/zabbix_manager/classes/scripts.rb +2 -8
- data/lib/zabbix_manager/classes/server.rb +1 -0
- data/lib/zabbix_manager/classes/templates.rb +2 -65
- data/lib/zabbix_manager/classes/triggers.rb +62 -53
- data/lib/zabbix_manager/classes/user_groups.rb +63 -0
- data/lib/zabbix_manager/classes/user_macros.rb +111 -0
- data/lib/zabbix_manager/classes/users.rb +15 -54
- data/lib/zabbix_manager/classes/value_maps.rb +52 -0
- data/lib/zabbix_manager/client.rb +162 -279
- data/lib/zabbix_manager/configuration.rb +115 -0
- data/lib/zabbix_manager/http_transport.rb +145 -97
- data/lib/zabbix_manager/log_sanitizer.rb +18 -29
- data/lib/zabbix_manager/monitoring/device.rb +394 -0
- data/lib/zabbix_manager/monitoring/expressions.rb +36 -0
- data/lib/zabbix_manager/monitoring/line.rb +176 -0
- data/lib/zabbix_manager/monitoring/line_items.rb +110 -0
- data/lib/zabbix_manager/monitoring/line_lifecycle.rb +169 -0
- data/lib/zabbix_manager/monitoring/line_plan.rb +113 -0
- data/lib/zabbix_manager/monitoring/thresholds.rb +182 -0
- data/lib/zabbix_manager/monitoring/traffic_items.rb +133 -0
- data/lib/zabbix_manager/monitoring/validation.rb +92 -0
- data/lib/zabbix_manager/monitoring.rb +216 -642
- data/lib/zabbix_manager/resource.rb +262 -0
- data/lib/zabbix_manager/traffic.rb +239 -0
- data/lib/zabbix_manager/version.rb +1 -1
- data/lib/zabbix_manager.rb +66 -179
- data/zabbix_manager.gemspec +6 -22
- metadata +41 -190
- data/lib/zabbix_manager/basic/basic_alias.rb +0 -42
- data/lib/zabbix_manager/basic/basic_func.rb +0 -55
- data/lib/zabbix_manager/basic/basic_init.rb +0 -50
- data/lib/zabbix_manager/basic/basic_logic.rb +0 -239
- data/lib/zabbix_manager/classes/hostinterfaces.rb +0 -189
- data/lib/zabbix_manager/classes/usergroups.rb +0 -75
- data/lib/zabbix_manager/classes/usermacros.rb +0 -222
- data/lib/zabbix_manager/classes/valuemaps.rb +0 -26
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ZabbixManager
|
|
4
|
+
# 在首次网络访问之前统一校验连接配置。
|
|
5
|
+
class Configuration
|
|
6
|
+
ENVIRONMENT_KEYS = {
|
|
7
|
+
url: "ZABBIX_URL", api_token: "ZABBIX_API_TOKEN",
|
|
8
|
+
username: "ZABBIX_USERNAME", password: "ZABBIX_PASSWORD"
|
|
9
|
+
}.freeze
|
|
10
|
+
CREDENTIALS = %i[api_token username password http_user http_password].freeze
|
|
11
|
+
BOOLEAN_OPTIONS = %i[verify_ssl allow_insecure_http no_proxy ignore_version].freeze
|
|
12
|
+
OPTIONS = (CREDENTIALS + BOOLEAN_OPTIONS + %i[
|
|
13
|
+
url proxy ca_file timeout request_timeout max_response_bytes
|
|
14
|
+
open_timeout read_timeout write_timeout keep_alive_timeout
|
|
15
|
+
logger log_level upsert_lock uncertain_write_delays
|
|
16
|
+
]).freeze
|
|
17
|
+
DEFAULT_UNCERTAIN_WRITE_DELAYS = [0, 0.25, 1, 2].freeze
|
|
18
|
+
|
|
19
|
+
# 仅从指定环境映射读取连接身份字段。
|
|
20
|
+
# 显式选项优先于环境值,包含用于清除凭据的 nil。
|
|
21
|
+
def self.from_env(env, overrides)
|
|
22
|
+
ENVIRONMENT_KEYS.to_h { |key, name| [key, env[name]] }.merge(overrides)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# 返回已校验的配置,不修改调用方持有的值。
|
|
26
|
+
def self.parse(options)
|
|
27
|
+
new(options).to_h
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(options)
|
|
31
|
+
raise Invalid, "options must be a Hash" unless options.is_a?(Hash)
|
|
32
|
+
|
|
33
|
+
@options = options.symbolize_keys
|
|
34
|
+
@options.assert_valid_keys(*OPTIONS)
|
|
35
|
+
@options.transform_values! { |value| value.is_a?(String) ? value.dup.freeze : value }
|
|
36
|
+
validate_credentials!
|
|
37
|
+
validate_booleans!
|
|
38
|
+
validate_delays!
|
|
39
|
+
validate_collaborators!
|
|
40
|
+
rescue ArgumentError => error
|
|
41
|
+
raise Invalid, error.message, cause: nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_h
|
|
45
|
+
@options.freeze
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def inspect
|
|
49
|
+
"#<#{self.class}>"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def validate_credentials!
|
|
55
|
+
CREDENTIALS.each do |key|
|
|
56
|
+
value = @options[key]
|
|
57
|
+
raise Invalid, "#{key} must be a String" unless value.nil? || value.is_a?(String)
|
|
58
|
+
|
|
59
|
+
if key == :api_token && value && !value.empty?
|
|
60
|
+
unless value.valid_encoding? && value.ascii_only? && value.match?(/\A[!-~]+\z/)
|
|
61
|
+
raise Invalid, "api_token must contain printable ASCII characters without whitespace"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
@options.delete(key) if value.blank?
|
|
66
|
+
end
|
|
67
|
+
raise Invalid, "url is required" if @options[:url].blank?
|
|
68
|
+
|
|
69
|
+
token = @options[:api_token]
|
|
70
|
+
username = @options[:username]
|
|
71
|
+
password = @options[:password]
|
|
72
|
+
if token && (username || password)
|
|
73
|
+
raise Invalid, "api_token cannot be combined with username/password"
|
|
74
|
+
end
|
|
75
|
+
raise Invalid, "provide api_token or both username and password" unless token || (username && password)
|
|
76
|
+
return unless @options[:http_password] && !@options[:http_user]
|
|
77
|
+
|
|
78
|
+
raise Invalid, "http_password requires http_user"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def validate_booleans!
|
|
82
|
+
BOOLEAN_OPTIONS.each do |key|
|
|
83
|
+
next unless @options.key?(key)
|
|
84
|
+
next if [true, false].include?(@options[key])
|
|
85
|
+
|
|
86
|
+
raise Invalid, "#{key} must be true or false"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def validate_delays!
|
|
91
|
+
values = @options.fetch(:uncertain_write_delays, DEFAULT_UNCERTAIN_WRITE_DELAYS)
|
|
92
|
+
raise Invalid, "uncertain_write_delays must be an Array" unless values.is_a?(Array)
|
|
93
|
+
|
|
94
|
+
delays = values.map { |value| Float(value, exception: false) }
|
|
95
|
+
finite_nonnegative = delays.all? { |value| value && value.finite? && value >= 0 }
|
|
96
|
+
unless (1..10).cover?(delays.size) && finite_nonnegative && delays.sum <= 60
|
|
97
|
+
raise Invalid, "uncertain_write_delays must contain 1 to 10 non-negative seconds totaling at most 60"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
@options[:uncertain_write_delays] = delays.freeze
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def validate_collaborators!
|
|
104
|
+
lock = @options[:upsert_lock]
|
|
105
|
+
raise Invalid, "upsert_lock must respond to call(key, &block)" if lock && !lock.respond_to?(:call)
|
|
106
|
+
|
|
107
|
+
logger = @options[:logger]
|
|
108
|
+
required = %i[debug info warn formatter formatter=]
|
|
109
|
+
required << :level= if @options.key?(:log_level)
|
|
110
|
+
return if logger.nil? || required.all? { |method| logger.respond_to?(method) }
|
|
111
|
+
|
|
112
|
+
raise Invalid, "logger must support the Ruby Logger interface"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -7,9 +7,15 @@ require "timeout"
|
|
|
7
7
|
require "uri"
|
|
8
8
|
|
|
9
9
|
class ZabbixManager
|
|
10
|
+
# 持有串行复用的 Net::HTTP 会话,负责代理、TLS、连接关闭及网络错误转换。
|
|
11
|
+
# 超时配置作用于连接、读取和写入阶段;传输失败不重放请求。
|
|
10
12
|
class HttpTransport
|
|
11
13
|
DEFAULT_TIMEOUT = 60
|
|
12
14
|
DEFAULT_KEEP_ALIVE_TIMEOUT = 30
|
|
15
|
+
DEFAULT_MAX_RESPONSE_BYTES = 64 * 1024 * 1024
|
|
16
|
+
# 仅用于中止单次网络操作,不包裹业务编排或应用层事务。
|
|
17
|
+
class RequestDeadline < Timeout::Error; end
|
|
18
|
+
private_constant :RequestDeadline
|
|
13
19
|
NETWORK_ERRORS = [
|
|
14
20
|
Timeout::Error, EOFError, IOError, SystemCallError, SocketError, OpenSSL::SSL::SSLError,
|
|
15
21
|
Net::ProtocolError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError
|
|
@@ -18,10 +24,19 @@ class ZabbixManager
|
|
|
18
24
|
attr_reader :uri
|
|
19
25
|
|
|
20
26
|
# 解析连接、超时和代理配置,并准备持久会话。
|
|
27
|
+
# @param options [Hash] URL、超时、TLS 和代理设置;不会在初始化时连接
|
|
28
|
+
# @raise [Invalid] URL、超时或布尔选项无效
|
|
21
29
|
def initialize(options)
|
|
22
|
-
@options = options
|
|
30
|
+
@options = options.dup.freeze
|
|
31
|
+
validate_boolean_options!
|
|
23
32
|
@uri = parse_uri(options.fetch(:url))
|
|
24
33
|
@timeout = positive_number(options.fetch(:timeout, DEFAULT_TIMEOUT), :timeout)
|
|
34
|
+
@request_timeout = positive_number(options.fetch(:request_timeout, @timeout), :request_timeout)
|
|
35
|
+
@max_response_bytes = options.fetch(:max_response_bytes, DEFAULT_MAX_RESPONSE_BYTES)
|
|
36
|
+
unless @max_response_bytes.is_a?(Integer) && @max_response_bytes.positive?
|
|
37
|
+
raise Invalid, "max_response_bytes must be a positive integer"
|
|
38
|
+
end
|
|
39
|
+
|
|
25
40
|
@open_timeout = positive_number(options.fetch(:open_timeout, @timeout), :open_timeout)
|
|
26
41
|
@read_timeout = positive_number(options.fetch(:read_timeout, @timeout), :read_timeout)
|
|
27
42
|
@write_timeout = positive_number(options.fetch(:write_timeout, @timeout), :write_timeout)
|
|
@@ -35,20 +50,21 @@ class ZabbixManager
|
|
|
35
50
|
end
|
|
36
51
|
|
|
37
52
|
# 串行发送请求,失败时关闭连接但不自动重放写操作。
|
|
53
|
+
# @param body [String] 已序列化的 JSON-RPC 请求
|
|
54
|
+
# @param bearer_token [String, nil] 当前请求的 API 认证凭据
|
|
55
|
+
# @return [String] HTTP 成功响应正文,由 Client 验证 JSON-RPC 协议
|
|
56
|
+
# @raise [TransportError] HTTP 状态失败、连接中断或超时;写入可能已生效
|
|
38
57
|
def request(body, bearer_token: nil)
|
|
58
|
+
request = build_request(body, bearer_token)
|
|
39
59
|
@mutex.synchronize do
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
raise TransportError, "HTTP Error: #{response.code} on #{safe_url}"
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
response.body
|
|
60
|
+
# Net::HTTP 原生阶段超时仍生效;额外期限只覆盖连接、发送及读取,不重放 POST。
|
|
61
|
+
Timeout.timeout(@request_timeout, RequestDeadline) { read_response(request) }
|
|
46
62
|
rescue TransportError
|
|
47
63
|
disconnect
|
|
48
64
|
raise
|
|
49
65
|
rescue *NETWORK_ERRORS => e
|
|
50
66
|
disconnect
|
|
51
|
-
raise TransportError, "#{e.class}: request failed for #{safe_url}"
|
|
67
|
+
raise TransportError, "#{e.class}: request failed for #{safe_url}", cause: nil
|
|
52
68
|
rescue StandardError
|
|
53
69
|
disconnect
|
|
54
70
|
raise
|
|
@@ -56,12 +72,14 @@ class ZabbixManager
|
|
|
56
72
|
end
|
|
57
73
|
|
|
58
74
|
# 显式关闭当前持久连接。
|
|
75
|
+
# @return [true] 重复关闭也成功,下一次请求按需重新连接
|
|
59
76
|
def close
|
|
60
77
|
@mutex.synchronize { disconnect }
|
|
61
78
|
true
|
|
62
79
|
end
|
|
63
80
|
|
|
64
81
|
# 返回不包含 URL 凭据和查询串的安全地址。
|
|
82
|
+
# @return [String]
|
|
65
83
|
def safe_url
|
|
66
84
|
port = @uri.port == @uri.default_port ? nil : ":#{@uri.port}"
|
|
67
85
|
"#{@uri.scheme}://#{@uri.host}#{port}#{@uri.path}"
|
|
@@ -76,117 +94,147 @@ class ZabbixManager
|
|
|
76
94
|
|
|
77
95
|
private
|
|
78
96
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
97
|
+
# 流式累计解压后的响应字节,避免在检查上限之前缓冲整个正文。
|
|
98
|
+
def read_response(request)
|
|
99
|
+
body = +""
|
|
100
|
+
active_http.request(request) do |response|
|
|
101
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
102
|
+
raise TransportError, "HTTP Error: #{response.code} on #{safe_url}"
|
|
84
103
|
end
|
|
85
104
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
response.read_body do |chunk|
|
|
106
|
+
if body.bytesize + chunk.bytesize > @max_response_bytes
|
|
107
|
+
raise TransportError, "HTTP response exceeds max_response_bytes on #{safe_url}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
body << chunk
|
|
111
|
+
end
|
|
89
112
|
end
|
|
113
|
+
body
|
|
114
|
+
end
|
|
90
115
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
116
|
+
# 校验并解析 HTTP 或 HTTPS API 地址。
|
|
117
|
+
def parse_uri(value)
|
|
118
|
+
uri = URI.parse(value)
|
|
119
|
+
unless %w[http https].include?(uri.scheme) && uri.hostname && !uri.hostname.empty? &&
|
|
120
|
+
(1..65_535).cover?(uri.port)
|
|
121
|
+
raise Invalid, "url must be an absolute HTTP or HTTPS URL with a valid port"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
uri.freeze
|
|
125
|
+
rescue URI::InvalidURIError, TypeError
|
|
126
|
+
raise Invalid, "url must be an absolute HTTP or HTTPS URL", cause: nil
|
|
127
|
+
end
|
|
95
128
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
129
|
+
# 防止字符串布尔值改变 TLS 校验或代理开关。
|
|
130
|
+
def validate_boolean_options!
|
|
131
|
+
%i[verify_ssl no_proxy].each do |name|
|
|
132
|
+
next unless @options.key?(name)
|
|
133
|
+
next if [true, false].include?(@options[name])
|
|
134
|
+
|
|
135
|
+
raise Invalid, "#{name} must be true or false"
|
|
99
136
|
end
|
|
137
|
+
end
|
|
100
138
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
139
|
+
# 把超时配置转换为有限正数。
|
|
140
|
+
def positive_number(value, name)
|
|
141
|
+
number = Float(value)
|
|
142
|
+
raise ArgumentError unless number.finite? && number.positive?
|
|
104
143
|
|
|
105
|
-
|
|
106
|
-
|
|
144
|
+
number
|
|
145
|
+
rescue ArgumentError, TypeError
|
|
146
|
+
raise Invalid, "#{name} must be a finite number greater than zero", cause: nil
|
|
147
|
+
end
|
|
107
148
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
end
|
|
149
|
+
# 复用标准库的代理发现与排除规则,并拒绝无法兑现的代理协议。
|
|
150
|
+
def proxy_uri(options)
|
|
151
|
+
return if options[:no_proxy]
|
|
112
152
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
153
|
+
value = options[:proxy]
|
|
154
|
+
uri = value.nil? ? URI::HTTP.build(host: @uri.hostname, port: @uri.port).find_proxy : URI.parse(value)
|
|
155
|
+
return unless uri
|
|
156
|
+
unless uri.is_a?(URI::HTTP) && uri.scheme == "http" && uri.hostname &&
|
|
157
|
+
(1..65_535).cover?(uri.port) && ["", "/"].include?(uri.path) && !uri.query && !uri.fragment
|
|
158
|
+
raise Invalid, "proxy must be an absolute HTTP URL without a path, query or fragment"
|
|
116
159
|
end
|
|
117
160
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
161
|
+
uri.freeze
|
|
162
|
+
rescue URI::InvalidURIError, TypeError
|
|
163
|
+
raise Invalid, "proxy must be an absolute HTTP URL", cause: nil
|
|
164
|
+
end
|
|
121
165
|
|
|
122
|
-
|
|
123
|
-
|
|
166
|
+
# 复用已启动连接,并在 fork 后放弃继承套接字。
|
|
167
|
+
def active_http
|
|
168
|
+
abandon_inherited_connection if @pid && @pid != Process.pid
|
|
169
|
+
@http ||= build_http
|
|
170
|
+
@http.start unless @http.started?
|
|
171
|
+
@pid = Process.pid
|
|
172
|
+
@http
|
|
173
|
+
end
|
|
124
174
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
@pid = Process.pid
|
|
131
|
-
@http
|
|
132
|
-
end
|
|
175
|
+
# 子进程不能向父进程继承的 TLS 套接字发送 close_notify。
|
|
176
|
+
def abandon_inherited_connection
|
|
177
|
+
@http = nil
|
|
178
|
+
@pid = nil
|
|
179
|
+
end
|
|
133
180
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
181
|
+
# 创建并配置 Net::HTTP 实例。
|
|
182
|
+
def build_http
|
|
183
|
+
http = Net::HTTP.new(@uri.hostname, @uri.port, *proxy_arguments)
|
|
184
|
+
|
|
185
|
+
http.open_timeout = @open_timeout
|
|
186
|
+
http.read_timeout = @read_timeout
|
|
187
|
+
http.write_timeout = @write_timeout
|
|
188
|
+
http.keep_alive_timeout = @keep_alive_timeout
|
|
189
|
+
http.max_retries = 0
|
|
190
|
+
configure_tls(http)
|
|
191
|
+
http
|
|
192
|
+
end
|
|
139
193
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
194
|
+
# 向 Net::HTTP 传入已解析代理,避免二次环境发现;凭据使用 URI 百分号解码。
|
|
195
|
+
def proxy_arguments
|
|
196
|
+
return [nil] unless @proxy_uri
|
|
143
197
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
198
|
+
[
|
|
199
|
+
@proxy_uri.hostname,
|
|
200
|
+
@proxy_uri.port,
|
|
201
|
+
@proxy_uri.user && URI::DEFAULT_PARSER.unescape(@proxy_uri.user),
|
|
202
|
+
@proxy_uri.password && URI::DEFAULT_PARSER.unescape(@proxy_uri.password)
|
|
203
|
+
]
|
|
204
|
+
end
|
|
151
205
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
@proxy_uri.host,
|
|
156
|
-
@proxy_uri.port,
|
|
157
|
-
@proxy_uri.user,
|
|
158
|
-
@proxy_uri.password
|
|
159
|
-
).new(@uri.host, @uri.port)
|
|
160
|
-
end
|
|
206
|
+
# 配置 HTTPS;证书校验默认关闭,按调用方选项开启。
|
|
207
|
+
def configure_tls(http)
|
|
208
|
+
return unless @uri.scheme == "https"
|
|
161
209
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
210
|
+
http.use_ssl = true
|
|
211
|
+
http.verify_mode = @options[:verify_ssl] == true ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
|
212
|
+
http.ca_file = @options[:ca_file] if @options[:ca_file]
|
|
213
|
+
end
|
|
165
214
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
215
|
+
# 构建 JSON-RPC POST,并在需要时写入 Bearer 或 Basic 认证。
|
|
216
|
+
def build_request(body, bearer_token)
|
|
217
|
+
if bearer_token && @options[:http_user]
|
|
218
|
+
raise Invalid, "HTTP Basic authentication cannot be combined with Bearer authentication"
|
|
169
219
|
end
|
|
170
220
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
request
|
|
180
|
-
end
|
|
221
|
+
request = Net::HTTP::Post.new(@uri.request_uri)
|
|
222
|
+
request["Content-Type"] = "application/json-rpc"
|
|
223
|
+
request["Accept"] = "application/json"
|
|
224
|
+
request["Authorization"] = "Bearer #{bearer_token}" if bearer_token
|
|
225
|
+
request.basic_auth(@options[:http_user], @options[:http_password]) if @options[:http_user]
|
|
226
|
+
request.body = body
|
|
227
|
+
request
|
|
228
|
+
end
|
|
181
229
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
230
|
+
# 安全结束连接并清理当前进程的会话状态。
|
|
231
|
+
def disconnect
|
|
232
|
+
@http.finish if @pid == Process.pid && @http&.started?
|
|
233
|
+
rescue *NETWORK_ERRORS
|
|
234
|
+
nil
|
|
235
|
+
ensure
|
|
236
|
+
@http = nil
|
|
237
|
+
@pid = nil
|
|
238
|
+
end
|
|
191
239
|
end
|
|
192
240
|
end
|
|
@@ -1,32 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "uri"
|
|
4
|
+
require "active_support/parameter_filter"
|
|
4
5
|
|
|
5
6
|
class ZabbixManager
|
|
7
|
+
# 日志与诊断数据的脱敏边界;过滤已知敏感字段、认证头和 URL 凭据。
|
|
8
|
+
# 自由文本过滤不能识别任意秘密,因此客户端不记录请求参数或服务端原始错误文本。
|
|
6
9
|
module LogSanitizer
|
|
7
10
|
REDACTED = "[FILTERED]"
|
|
8
11
|
MAX_STRING_LENGTH = 2_000
|
|
9
|
-
SENSITIVE_KEY_NAMES = "auth|authorization|api_token|community|cookie|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
SENSITIVE_KEYS = /\A(?:#{SENSITIVE_KEY_NAMES})\z/i
|
|
12
|
+
SENSITIVE_KEY_NAMES = "auth|authorization|authpassphrase|api_token|community|cookie|" \
|
|
13
|
+
"http_password|password|passwd|privatekey|privpassphrase|secret|sessionid|snmp_community|snmpv3_authpassphrase|" \
|
|
14
|
+
"snmpv3_privpassphrase|tls_psk|tls_psk_identity|token"
|
|
15
|
+
SENSITIVE_KEYS = /\A(?:#{SENSITIVE_KEY_NAMES})\z/i
|
|
16
|
+
FILTER = ActiveSupport::ParameterFilter.new(
|
|
17
|
+
[SENSITIVE_KEYS, ->(_key, value) { value.replace(sanitize_string(value)) if value.is_a?(String) }]
|
|
18
|
+
).freeze
|
|
13
19
|
|
|
14
20
|
module_function
|
|
15
21
|
|
|
16
22
|
# 递归脱敏结构化对象,并限制字符串长度。
|
|
17
23
|
def sanitize(value)
|
|
18
|
-
|
|
19
|
-
when Hash
|
|
20
|
-
value.each_with_object({}) do |(key, child), result|
|
|
21
|
-
result[key] = sensitive_key?(key) ? REDACTED : sanitize(child)
|
|
22
|
-
end
|
|
23
|
-
when Array
|
|
24
|
-
value.map { |child| sanitize(child) }
|
|
25
|
-
when String
|
|
26
|
-
sanitize_string(value)
|
|
27
|
-
else
|
|
28
|
-
value
|
|
29
|
-
end
|
|
24
|
+
FILTER.filter_param("value", value)
|
|
30
25
|
end
|
|
31
26
|
|
|
32
27
|
# 移除 URL 中的用户名、密码、查询串和片段。
|
|
@@ -38,31 +33,25 @@ class ZabbixManager
|
|
|
38
33
|
uri.fragment = nil
|
|
39
34
|
uri.to_s
|
|
40
35
|
rescue URI::InvalidURIError
|
|
41
|
-
|
|
36
|
+
REDACTED
|
|
42
37
|
end
|
|
43
38
|
|
|
44
39
|
# 清理字符串中的认证头和常见敏感键值。
|
|
45
40
|
def sanitize_string(value)
|
|
46
41
|
sanitized = value.dup
|
|
47
|
-
|
|
48
|
-
sanitized.gsub!(
|
|
49
|
-
/((?:authorization)["']?\s*(?:=>|:|=)\s*)[^,;}\]]+/i,
|
|
50
|
-
"\\1#{REDACTED}"
|
|
51
|
-
)
|
|
42
|
+
# 先处理完整引号值,避免认证头规则在值内的分隔符处截断秘密。
|
|
52
43
|
sanitized.gsub!(
|
|
53
|
-
/((?:#{SENSITIVE_KEY_NAMES})["']?\s*(?:=>|:|=)\s*)(["'])(
|
|
44
|
+
/((?:#{SENSITIVE_KEY_NAMES})["']?\s*(?:=>|:|=)\s*)(["'])((?:\\.|(?!\2)[^\\])*)\2/im,
|
|
54
45
|
"\\1\\2#{REDACTED}\\2"
|
|
55
46
|
)
|
|
56
47
|
sanitized.gsub!(
|
|
57
|
-
/((?:#{SENSITIVE_KEY_NAMES})["']?\s*(?:=>|:|=)\s
|
|
48
|
+
/((?:#{SENSITIVE_KEY_NAMES})["']?\s*(?:=>|:|=)\s*+)
|
|
49
|
+
(?!["']|#{Regexp.escape(REDACTED)}(?=[,;}\]]|\z))
|
|
50
|
+
(?:#{Regexp.escape(REDACTED)}|[^,;}\]])+/ix,
|
|
58
51
|
"\\1#{REDACTED}"
|
|
59
52
|
)
|
|
53
|
+
sanitized.gsub!(/\b(Bearer|Basic)\s+[^\s,;]+/i, "\\1 #{REDACTED}")
|
|
60
54
|
sanitized.length > MAX_STRING_LENGTH ? "#{sanitized[0, MAX_STRING_LENGTH]}...[TRUNCATED]" : sanitized
|
|
61
55
|
end
|
|
62
|
-
|
|
63
|
-
# 判断字段名是否属于敏感配置。
|
|
64
|
-
def sensitive_key?(key)
|
|
65
|
-
key.to_s.match?(SENSITIVE_KEYS)
|
|
66
|
-
end
|
|
67
56
|
end
|
|
68
57
|
end
|