baseline-rb 0.2.1 → 0.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.
@@ -4,11 +4,22 @@ require_relative "../parser"
4
4
 
5
5
  require "shellwords"
6
6
  require_relative "document"
7
+ require_relative "command_index"
7
8
 
8
9
  module Baseline
9
10
  module PaloAlto
10
11
  # 将 PAN-OS set 语法归纳为与配置块平台一致的事实。
11
12
  class Parser < Baseline::Parser
13
+ # 原生管理端点没有 VTY 模型;未建模能力保留未知,不改变规则适用范围。
14
+ UNSUPPORTED_FACTS = {
15
+ "ssh.versions" => :ssh_version_unmodeled,
16
+ "ssh.vty_ssh_only" => :native_management_endpoint,
17
+ "ssh.vty_aaa" => :native_management_endpoint,
18
+ "tacacs.authorization" => :platform_semantics_unresolved,
19
+ "tacacs.accounting" => :platform_semantics_unresolved
20
+ }.freeze
21
+ PARTIAL_FACTS = { "services" => :service_states_partially_modeled }.freeze
22
+
12
23
  MANAGEMENT_SERVICES = %w[ssh telnet http https].freeze
13
24
  RELEVANT = /\Aset (?:deviceconfig system |network (?:profiles interface-management-profile |interface )|shared (?:server-profile tacplus |authentication-profile |authentication-sequence |log-settings syslog )|mgt-config users )/
14
25
 
@@ -18,25 +29,31 @@ module Baseline
18
29
  def document_class = Document
19
30
  # 匹配当前平台显式声明的主机名。
20
31
  def hostname_pattern = /\Aset deviceconfig system hostname\s+(\S+)\z/
32
+ # set 语法使用独立的设备身份命令族。
33
+ def hostname_family_pattern = /\Aset deviceconfig system hostname(?:\s|\z)/
21
34
 
22
35
  # 仅对管理配置子集分词一次,不处理无关的安全规则。
23
36
  def prepare_configuration
24
- @statements = roots.filter_map do |entry|
37
+ statements = roots.filter_map do |entry|
25
38
  next unless RELEVANT.match?(entry.text)
26
39
  { entry: entry, tokens: Shellwords.shellsplit(entry.text).drop(1) }
27
40
  rescue ArgumentError
28
- @facts.incomplete!(code: :invalid_set_statement, lines: [ entry.line ])
41
+ @fact_builder.mark_incomplete!(code: :invalid_set_statement, lines: [ entry.line ])
29
42
  nil
30
43
  end
31
- @interface_profiles = rows("network", "profiles", "interface-management-profile").group_by { |row| row[:tokens][3] }
44
+ @command_index = CommandIndex.new(statements)
45
+ @interface_profiles = statements_with_prefix("network", "profiles", "interface-management-profile").group_by { |row| row[:tokens][3] }
32
46
  @unresolved_profile_bindings = false
33
- @profile_bindings = rows("network", "interface").select do |row|
47
+ # 同一接口作用域只保留最后绑定,其他接口继续独立参与管理端点归约。
48
+ bindings = statements_with_prefix("network", "interface").select { |row| row[:tokens].include?("interface-management-profile") }
49
+ .group_by { |row| row[:tokens].take(row[:tokens].index("interface-management-profile")) }.values.map(&:last)
50
+ @profile_bindings = bindings.select do |row|
34
51
  index = row[:tokens].index("interface-management-profile")
35
52
  next false unless index
36
53
  valid = row[:tokens].size == index + 2 && !row[:tokens].last.empty?
37
54
  unless valid
38
55
  @unresolved_profile_bindings = true
39
- @facts.incomplete!(code: :invalid_management_profile_binding, lines: [ row[:entry].line ])
56
+ @fact_builder.mark_incomplete!(code: :invalid_management_profile_binding, lines: [ row[:entry].line ])
40
57
  end
41
58
  valid
42
59
  end
@@ -44,20 +61,20 @@ module Baseline
44
61
  end
45
62
 
46
63
  # 覆盖所有已配置的管理端点,包括数据接口。
47
- def management
48
- services = (MANAGEMENT_SERVICES + [ "snmp" ]).to_h { |name| [ name, service_state(name) ] }
49
- record("ssh.enabled", services["ssh"], service_entries([ "ssh" ]))
50
- record("services", services.slice("telnet", "http", "https"), service_entries(%w[telnet http https]))
51
- record("snmp.enabled", services["snmp"], service_entries([ "snmp" ]))
52
- permitted_sources("management.acl", MANAGEMENT_SERVICES)
53
- permitted_sources("snmp.acl", [ "snmp" ])
64
+ def parse_management
65
+ service_states = (MANAGEMENT_SERVICES + [ "snmp" ]).to_h { |name| [ name, service_state(name) ] }
66
+ record_fact("ssh.enabled", service_states["ssh"], service_entries([ "ssh" ]))
67
+ record_fact("services", service_states.slice("telnet", "http", "https"), service_entries(%w[telnet http https]))
68
+ record_fact("snmp.enabled", service_states["snmp"], service_entries([ "snmp" ]))
69
+ record_permitted_sources("management.acl", MANAGEMENT_SERVICES)
70
+ record_permitted_sources("snmp.acl", [ "snmp" ])
54
71
  end
55
72
 
56
73
  # MGT 服务标志缺失时保持未知;明确启用的端点可证明服务已启用。
57
74
  def service_state(name)
58
- disabled = boolean(%w[deviceconfig system service] + [ "disable-#{name}" ])
75
+ disabled = configured_boolean(%w[deviceconfig system service] + [ "disable-#{name}" ])
59
76
  global = disabled.nil? ? nil : !disabled
60
- values = @active_profiles.map { |profile| profile_service(profile, name) }
77
+ values = @active_profiles.map { |profile| profile_service_state(profile, name) }
61
78
  return true if global == true || values.include?(true)
62
79
  return false if global == false && values.all?(false)
63
80
 
@@ -67,37 +84,37 @@ module Baseline
67
84
  # 保留参与跨端点服务事实归纳的显式声明。
68
85
  def service_entries(names)
69
86
  names.flat_map do |name|
70
- rows(*(%w[deviceconfig system service] + [ "disable-#{name}" ])) + @active_profiles.flat_map do |profile|
71
- rows("network", "profiles", "interface-management-profile", profile, name)
87
+ statements_with_prefix(*(%w[deviceconfig system service] + [ "disable-#{name}" ])) + @active_profiles.flat_map do |profile|
88
+ statements_with_prefix("network", "profiles", "interface-management-profile", profile, name)
72
89
  end
73
90
  end.map { |row| row[:entry] }
74
91
  end
75
92
 
76
93
  # 仅解释命名接口配置文件中显式声明的服务。
77
- def profile_service(profile, name)
94
+ def profile_service_state(profile, name)
78
95
  return unless @interface_profiles.key?(profile)
79
96
  prefix = [ "network", "profiles", "interface-management-profile", profile, name ]
80
- value = boolean(prefix)
97
+ value = configured_boolean(prefix)
81
98
  return value unless value.nil?
82
- return if rows(*prefix).any?
83
- return false if value.nil? && @facts.complete?
99
+ return if statements_with_prefix(*prefix).any?
100
+ return false if value.nil? && @fact_builder.complete?
84
101
 
85
102
  nil
86
103
  end
87
104
 
88
105
  # 端点的原生允许列表适用于该端点所有已启用服务。
89
- def permitted_sources(fact, services)
90
- global = services.map { |name| boolean(%w[deviceconfig system service] + [ "disable-#{name}" ]) }
106
+ def record_permitted_sources(fact, service_names)
107
+ global = service_names.map { |name| configured_boolean(%w[deviceconfig system service] + [ "disable-#{name}" ]) }
91
108
  prefixes = global.all?(true) ? [] : [ %w[deviceconfig system permitted-ip] ]
92
109
  unresolved = @unresolved_profile_bindings
93
110
  @active_profiles.each do |profile|
94
- enabled = services.map { |name| profile_service(profile, name) }
111
+ enabled = service_names.map { |name| profile_service_state(profile, name) }
95
112
  unresolved ||= enabled.include?(nil)
96
113
  prefixes << [ "network", "profiles", "interface-management-profile", profile, "permitted-ip" ] if enabled.include?(true)
97
114
  end
98
115
  entries = []
99
116
  sources = prefixes.flat_map do |prefix|
100
- matches = rows(*prefix)
117
+ matches = statements_with_prefix(*prefix)
101
118
  entries.concat(matches.map { |row| row[:entry] })
102
119
  matches.empty? ? [ "0.0.0.0/0" ] : matches.map do |row|
103
120
  tokens = row[:tokens].drop(prefix.size)
@@ -117,41 +134,34 @@ module Baseline
117
134
  end
118
135
  end.compact
119
136
  bound = !sources.include?("0.0.0.0/0")
120
- @facts.direct_acl(fact: fact, sources: unresolved ? nil : sources, bound: bound, lines: entries.map(&:line))
121
- unknown("#{fact}.bound", code: :management_profile_unresolved) if unresolved
137
+ @fact_builder.record_source_restriction(fact: fact, sources: unresolved ? nil : sources, bound: bound, lines: entries.map(&:line))
138
+ record_unknown_fact("#{fact}.bound", code: :management_profile_unresolved) if unresolved
122
139
  cursor.capture_many(entries.select { |entry| cursor.parsed?(entry.index) }, data: sources, path: [ "facts", "#{fact}.sources" ]) unless unresolved
123
140
  end
124
141
 
125
- # 采集明确启用或禁用的管理服务。
126
- def services
127
- ntp
128
- syslog
129
- snmp
130
- end
131
-
132
142
  # 认证要求必须覆盖全部已配置服务器,不能由任意一台满足来证明。
133
- def ntp
134
- entries = rows("deviceconfig", "system", "ntp-servers")
143
+ def parse_ntp
144
+ entries = statements_with_prefix("deviceconfig", "system", "ntp-servers")
135
145
  servers = entries.group_by { |row| row[:tokens][3] }.filter_map do |name, definitions|
136
146
  prefix = [ "deviceconfig", "system", "ntp-servers", name ]
137
- address = endpoint(prefix + [ "ntp-server-address" ])
147
+ address = configured_server_address(prefix + [ "ntp-server-address" ])
138
148
  unless address && %w[primary-ntp-server secondary-ntp-server].include?(name)
139
- unknown("ntp.servers", definitions.map { |row| row[:entry] }, code: :ntp_server_unresolved)
140
- unknown("ntp.authentication", definitions.map { |row| row[:entry] }, code: :ntp_server_unresolved)
149
+ record_unknown_fact("ntp.servers", definitions.map { |row| row[:entry] }, code: :ntp_server_unresolved)
150
+ record_unknown_fact("ntp.authentication", definitions.map { |row| row[:entry] }, code: :ntp_server_unresolved)
141
151
  next
142
152
  end
143
- { address: address, authenticated: ntp_authentication(prefix) }
153
+ { address: address, authenticated: ntp_authentication_state(prefix) }
144
154
  end
145
- record("ntp.servers", servers.map { |server| server[:address] }.uniq, entries.map { |row| row[:entry] })
155
+ record_fact("ntp.servers", servers.map { |server| server[:address] }.uniq, entries.map { |row| row[:entry] })
146
156
  if servers.any? && servers.none? { |server| server[:authenticated].nil? }
147
- record("ntp.authentication", servers.all? { |server| server[:authenticated] }, entries.map { |row| row[:entry] })
157
+ record_fact("ntp.authentication", servers.all? { |server| server[:authenticated] }, entries.map { |row| row[:entry] })
148
158
  end
149
- record_scalar("ntp.source", %w[deviceconfig system route service ntp source interface])
159
+ record_configured_scalar("ntp.source", %w[deviceconfig system route service ntp source interface])
150
160
  end
151
161
 
152
162
  # 仅完整的认证声明才能证明所有服务器均满足认证要求。
153
- def ntp_authentication(prefix)
154
- entries = rows(*(prefix + [ "authentication-type" ]))
163
+ def ntp_authentication_state(prefix)
164
+ entries = statements_with_prefix(*(prefix + [ "authentication-type" ]))
155
165
  options = entries.map { |row| row[:tokens].drop(prefix.size + 1) }.uniq
156
166
  return if options.empty?
157
167
  return false if options == [ [ "none" ] ]
@@ -162,128 +172,126 @@ module Baseline
162
172
  key[3] == "authentication-key" && !key[4].empty? && id.size == 3 && /\A\d+\z/.match?(id[2]) && id[2].to_i.between?(1, 65_534)
163
173
  return true
164
174
  end
165
- unknown("ntp.authentication", entries.map { |row| row[:entry] }, code: :unsupported_ntp_authentication)
175
+ record_unknown_fact("ntp.authentication", entries.map { |row| row[:entry] }, code: :unsupported_ntp_authentication)
166
176
  nil
167
177
  end
168
178
 
169
179
  # 配置的采集端地址与运行时日志转发成功是两类事实。
170
- def syslog
171
- entries = rows("shared", "log-settings", "syslog").select { |row| row[:tokens][4] == "server" }
180
+ def parse_syslog
181
+ entries = statements_with_prefix("shared", "log-settings", "syslog").select { |row| row[:tokens][4] == "server" }
172
182
  servers = entries.group_by { |row| [ row[:tokens][3], row[:tokens][5] ] }.values.filter_map do |definitions|
173
183
  address = definitions.reverse.find { |row| row[:tokens][6] == "server" }
174
- value = address && endpoint(address[:tokens].take(7))
175
- unknown("syslog.servers", definitions.map { |row| row[:entry] }, code: :syslog_server_unresolved) unless value
184
+ value = address && configured_server_address(address[:tokens].take(7))
185
+ record_unknown_fact("syslog.servers", definitions.map { |row| row[:entry] }, code: :syslog_server_unresolved) unless value
176
186
  value
177
187
  end
178
- record("syslog.servers", servers.uniq, entries.map { |row| row[:entry] })
179
- record_scalar("syslog.source", %w[deviceconfig system route service syslog source interface])
188
+ record_fact("syslog.servers", servers.uniq, entries.map { |row| row[:entry] })
189
+ record_configured_scalar("syslog.source", %w[deviceconfig system route service syslog source interface])
180
190
  end
181
191
 
182
192
  # 轮询用户、团体字与出站 SNMP Trap 配置相互独立。
183
- def snmp
193
+ def parse_snmp
184
194
  prefix = %w[deviceconfig system snmp-setting access-setting version]
185
- versions = rows(*prefix).map { |row| row[:tokens][prefix.size] }.compact.uniq
186
- record("snmp.versions", versions)
187
- communities = rows(*(prefix + %w[v2c snmp-community-string])).filter_map do |row|
195
+ versions = statements_with_prefix(*prefix).map { |row| row[:tokens][prefix.size] }.compact.uniq
196
+ record_fact("snmp.versions", versions)
197
+ communities = statements_with_prefix(*(prefix + %w[v2c snmp-community-string])).filter_map do |row|
188
198
  unless row[:tokens].size == prefix.size + 3 && !row[:tokens].last.empty?
189
- @facts.incomplete!(code: :invalid_set_statement, lines: [ row[:entry].line ])
199
+ @fact_builder.mark_incomplete!(code: :invalid_set_statement, lines: [ row[:entry].line ])
190
200
  next
191
201
  end
192
- record_credential("snmp-community", "snmp", secret_format(row[:tokens].last), row[:tokens].last, row[:entry])
202
+ record_credential("snmp-community", "snmp", credential_storage_format(row[:tokens].last), row[:tokens].last, row[:entry])
193
203
  { write: false, line: row[:entry].line }
194
204
  end
195
205
  users_prefix = prefix + %w[v3 users]
196
- users = rows(*users_prefix).group_by { |row| row[:tokens][users_prefix.size] }.filter_map do |name, entries|
206
+ users = statements_with_prefix(*users_prefix).group_by { |row| row[:tokens][users_prefix.size] }.filter_map do |name, entries|
197
207
  next unless name
198
208
  user = { name: name, auth: false, priv: false, write: false, line: entries.first[:entry].line }
199
209
  { "authpwd" => :auth, "privpwd" => :priv }.each do |field, key|
200
210
  row = entries.reverse.find { |entry| entry[:tokens][users_prefix.size + 1] == field }
201
211
  next unless row
202
212
  unless row[:tokens].size == users_prefix.size + 3 && !row[:tokens].last.empty?
203
- @facts.incomplete!(code: :invalid_set_statement, lines: [ row[:entry].line ])
213
+ @fact_builder.mark_incomplete!(code: :invalid_set_statement, lines: [ row[:entry].line ])
204
214
  next
205
215
  end
206
216
  user[key] = !row[:tokens].last.empty?
207
- record_credential("snmp:#{name}", "snmp_user", secret_format(row[:tokens].last), row[:tokens].last, row[:entry])
217
+ record_credential("snmp:#{name}", "snmp_user", credential_storage_format(row[:tokens].last), row[:tokens].last, row[:entry])
208
218
  end
209
219
  user
210
220
  end
211
- @facts.snmp(communities: communities, users: users)
221
+ @fact_builder.record_snmp_access(communities: communities, users: users)
212
222
  if (versions - %w[v2c v3]).any? || versions.size > 1
213
- unknown_snmp_access(rows(*prefix).map { |row| row[:entry] }, code: :unsupported_snmp_version)
223
+ record_unknown_snmp_access(statements_with_prefix(*prefix).map { |row| row[:entry] }, code: :unsupported_snmp_version)
214
224
  end
215
225
  end
216
226
 
217
227
  # 将选中的登录配置或有序认证序列解析为 TACACS 服务器。
218
- def authentication
219
- server_rows = rows("shared", "server-profile", "tacplus")
228
+ def parse_authentication
229
+ server_rows = statements_with_prefix("shared", "server-profile", "tacplus")
220
230
  schemes = server_rows.group_by { |row| row[:tokens][3] }.to_h do |name, entries|
221
231
  servers = entries.select { |row| row[:tokens][4] == "server" }.group_by { |row| row[:tokens][5] }
222
232
  addresses = servers.values.filter_map do |server|
223
233
  address = server.reverse.find { |row| row[:tokens][6] == "address" }
224
- value = address && endpoint(address[:tokens].take(7))
225
- unknown("tacacs.servers", server.map { |row| row[:entry] }, code: :tacacs_server_unresolved) unless value
234
+ value = address && configured_server_address(address[:tokens].take(7))
235
+ record_unknown_fact("tacacs.servers", server.map { |row| row[:entry] }, code: :tacacs_server_unresolved) unless value
226
236
  value
227
237
  end
228
- unknown("tacacs.servers", entries.map { |row| row[:entry] }, code: :tacacs_server_unresolved) if servers.empty?
238
+ record_unknown_fact("tacacs.servers", entries.map { |row| row[:entry] }, code: :tacacs_server_unresolved) if servers.empty?
229
239
  keys = servers.values.map do |server|
230
240
  secret = server.reverse.find { |row| row[:tokens][6] == "secret" }
231
- value = secret && scalar(secret[:tokens].take(7))
232
- record_credential("tacacs:#{name}", "tacacs", secret_format(value), value, secret[:entry]) if value
241
+ value = secret && configured_scalar(secret[:tokens].take(7))
242
+ record_credential("tacacs:#{name}", "tacacs", credential_storage_format(value), value, secret[:entry]) if value
233
243
  !value.to_s.empty?
234
244
  end
235
245
  [ name, { servers: addresses, keys_present: keys.any? && keys.all?, line: entries.first[:entry].line } ]
236
246
  end
237
- record("tacacs.schemes", schemes)
238
- record("tacacs.servers", schemes.values.flat_map { |scheme| scheme[:servers] }.uniq)
239
- record("tacacs.keys_present", schemes.values.all? { |scheme| scheme[:keys_present] }) if schemes.any?
240
- selected = scalar(%w[deviceconfig system authentication-profile])
241
- login = selected ? tacacs_profile(selected, schemes) : false
242
- login.nil? ? unknown("tacacs.login", code: :tacacs_scheme_unresolved) : record("tacacs.login", login)
243
- # PAN-OS 角色映射与命令计费不能套用路由器式 AAA 开关语义。
244
- %w[authorization accounting].each { |kind| unknown("tacacs.#{kind}", code: :platform_semantics_unresolved) }
245
- accounts
247
+ record_fact("tacacs.schemes", schemes)
248
+ record_fact("tacacs.servers", schemes.values.flat_map { |scheme| scheme[:servers] }.uniq)
249
+ record_fact("tacacs.keys_present", schemes.values.all? { |scheme| scheme[:keys_present] }) if schemes.any?
250
+ selected = configured_scalar(%w[deviceconfig system authentication-profile])
251
+ login = selected ? tacacs_login_state(selected, schemes) : false
252
+ login.nil? ? record_unknown_fact("tacacs.login", code: :tacacs_scheme_unresolved) : record_fact("tacacs.login", login)
253
+ parse_local_accounts
246
254
  end
247
255
 
248
256
  # 认证序列具有顺序;后备 TACACS 不能视为首选 TACACS 登录。
249
- def tacacs_profile(name, schemes)
257
+ def tacacs_login_state(name, schemes)
250
258
  prefix = [ "shared", "authentication-profile", name, "method" ]
251
- methods = rows(*prefix)
259
+ methods = statements_with_prefix(*prefix)
252
260
  if methods.empty?
253
- sequence = rows("shared", "authentication-sequence", name, "authentication-profiles").last
261
+ sequence = statements_with_prefix("shared", "authentication-sequence", name, "authentication-profiles").last
254
262
  return unless sequence
255
263
  names = sequence[:tokens].drop(4).reject { |token| %w[[ ]].include?(token) }
256
264
  return if names.empty? || names.first == name
257
- return tacacs_profile_definition(names.first, schemes)
265
+ return authentication_profile_tacacs_state(names.first, schemes)
258
266
  end
259
- tacacs_profile_definition(name, schemes)
267
+ authentication_profile_tacacs_state(name, schemes)
260
268
  end
261
269
 
262
270
  # 选中的 TACACS 配置必须至少解析出一台具有地址的服务器。
263
- def tacacs_profile_definition(name, schemes)
271
+ def authentication_profile_tacacs_state(name, schemes)
264
272
  prefix = [ "shared", "authentication-profile", name, "method" ]
265
- methods = rows(*prefix)
273
+ methods = statements_with_prefix(*prefix)
266
274
  return if methods.empty?
267
275
  kinds = methods.map { |row| row[:tokens][4] }.uniq
268
276
  return unless kinds.one?
269
277
  return false if %w[none local-database radius ldap kerberos saml-idp cloud].include?(kinds.first)
270
278
  return unless kinds.first == "tacplus"
271
- target = scalar(prefix + %w[tacplus server-profile])
279
+ target = configured_scalar(prefix + %w[tacplus server-profile])
272
280
  return unless target && schemes[target] && schemes[target][:servers].any?
273
281
 
274
282
  true
275
283
  end
276
284
 
277
285
  # 仅存储凭据元数据,导出的加密值不进入报告。
278
- def accounts
279
- rows("mgt-config", "users").group_by { |row| row[:tokens][2] }.each do |name, entries|
286
+ def parse_local_accounts
287
+ statements_with_prefix("mgt-config", "users").group_by { |row| row[:tokens][2] }.each do |name, entries|
280
288
  row = entries.reverse.find { |entry| entry[:tokens][3] == "phash" }
281
289
  unless row
282
- external_profile = scalar([ "mgt-config", "users", name, "authentication-profile" ])
290
+ external_profile = configured_scalar([ "mgt-config", "users", name, "authentication-profile" ])
283
291
  record_credential(name, "local_user", "unknown", nil, entries.first[:entry]) unless external_profile && !external_profile.empty?
284
292
  next
285
293
  end
286
- secret = scalar(row[:tokens].take(4))
294
+ secret = configured_scalar(row[:tokens].take(4))
287
295
  format = if /\A\$(?:1|5|6)\$[^$]+\$[^$]+\z/.match?(secret.to_s)
288
296
  secret.start_with?("$1$") ? "5" : "hash"
289
297
  else
@@ -294,45 +302,45 @@ module Baseline
294
302
  end
295
303
 
296
304
  # 识别 PAN-OS 带标记的加密 set 值;其余情况保留明文风险。
297
- def secret_format(value) = value.start_with?("-AQ", "-AR") ? "cipher" : "plain"
305
+ def credential_storage_format(value) = value.start_with?("-AQ", "-AR") ? "cipher" : "plain"
298
306
 
299
307
  # 按词元前缀选择归一化后的 PAN-OS set 命令行。
300
- def rows(*prefix)
301
- @statements.select { |row| row[:tokens].take(prefix.size) == prefix }
308
+ def statements_with_prefix(*prefix)
309
+ @command_index.matching_prefix(prefix)
302
310
  end
303
311
 
304
312
  # 地址仅接受 IP 或 DNS 名称,不接受不完整转义或任意文本。
305
- def endpoint(prefix)
306
- value = scalar(prefix)
313
+ def configured_server_address(prefix)
314
+ value = configured_scalar(prefix)
307
315
  return if value.nil?
308
316
  return value if server_address?(value)
309
317
 
310
- @facts.incomplete!(code: :invalid_server_address, lines: rows(*prefix).map { |row| row[:entry].line })
318
+ @fact_builder.mark_incomplete!(code: :invalid_server_address, lines: statements_with_prefix(*prefix).map { |row| row[:entry].line })
311
319
  nil
312
320
  end
313
321
 
314
322
  # 显式标志格式错误时,不能用平台默认值替代。
315
- def boolean(prefix)
316
- value = scalar(prefix)
323
+ def configured_boolean(prefix)
324
+ value = configured_scalar(prefix)
317
325
  return if value.nil?
318
326
  return value == "yes" if %w[yes no].include?(value)
319
327
 
320
- @facts.incomplete!(code: :invalid_set_statement, lines: rows(*prefix).map { |row| row[:entry].line })
328
+ @fact_builder.mark_incomplete!(code: :invalid_set_statement, lines: statements_with_prefix(*prefix).map { |row| row[:entry].line })
321
329
  nil
322
330
  end
323
331
 
324
332
  # 标量语法与事实来源共享同一条最终声明。
325
- def record_scalar(path, prefix)
326
- row = rows(*prefix).last
327
- record(path, scalar(prefix), row ? [ row[:entry] ] : [])
333
+ def record_configured_scalar(path, prefix)
334
+ row = statements_with_prefix(*prefix).last
335
+ record_fact(path, configured_scalar(prefix), row ? [ row[:entry] ] : [])
328
336
  end
329
337
 
330
338
  # 严格校验词元数量,避免尾随内容使错误配置被误判为已知事实。
331
- def scalar(prefix)
332
- row = rows(*prefix).last
339
+ def configured_scalar(prefix)
340
+ row = statements_with_prefix(*prefix).last
333
341
  return unless row
334
342
  unless row[:tokens].size == prefix.size + 1
335
- @facts.incomplete!(code: :invalid_set_statement, lines: [ row[:entry].line ])
343
+ @fact_builder.mark_incomplete!(code: :invalid_set_statement, lines: [ row[:entry].line ])
336
344
  return
337
345
  end
338
346