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.
@@ -9,6 +9,15 @@ module Baseline
9
9
  module Hillstone
10
10
  # StoneOS 管理端点与原生主机允许列表不同于路由器 VTY。
11
11
  class Parser < Baseline::Parser
12
+ # 原生管理端点没有 VTY 模型;未建模能力保留未知,不改变规则适用范围。
13
+ UNSUPPORTED_FACTS = {
14
+ "ssh.versions" => :ssh_version_unmodeled,
15
+ "ssh.vty_ssh_only" => :native_management_endpoint,
16
+ "ssh.vty_aaa" => :native_management_endpoint,
17
+ "tacacs.accounting" => :platform_semantics_unresolved
18
+ }.freeze
19
+ PARTIAL_FACTS = { "services" => :service_states_partially_modeled }.freeze
20
+
12
21
  MANAGEMENT_SERVICES = %w[ssh telnet http https ftp snmp].freeze
13
22
  HOST_PROTOCOLS = %w[ssh telnet http https netconf any].freeze
14
23
 
@@ -23,11 +32,11 @@ module Baseline
23
32
  end
24
33
 
25
34
  # 已配置主机 ACL 本身不能证明管理监听服务已启用。
26
- def management
35
+ def parse_management
27
36
  interfaces = matching_blocks(/\Ainterface\s+/)
28
37
  states = interfaces.map do |section|
29
38
  matching_blocks(/\A(?:no )?manage\s+/, section.children).each_with_object({}) do |entry, values|
30
- parts = tokens(entry)
39
+ parts = command_tokens(entry)
31
40
  negative = parts.first == "no"
32
41
  parts = parts.drop(1) if negative
33
42
  next unless MANAGEMENT_SERVICES.include?(parts[1])
@@ -38,110 +47,103 @@ module Baseline
38
47
  end
39
48
  end
40
49
  end
41
- services = MANAGEMENT_SERVICES.to_h do |name|
50
+ service_states = MANAGEMENT_SERVICES.to_h do |name|
42
51
  values = states.map { |state| state[name] }
43
52
  enabled = values.include?(true) ? true : values.any? && values.all?(false) ? false : nil
44
53
  [ name, enabled ]
45
54
  end
46
- record("ssh.enabled", services["ssh"])
47
- record("services", services.slice("telnet", "http", "https", "ftp"))
48
- management_sources
55
+ record_fact("ssh.enabled", service_states["ssh"])
56
+ record_fact("services", service_states.slice("telnet", "http", "https", "ftp"))
57
+ record_management_sources
49
58
  end
50
59
 
51
60
  # 信任主机可使用 CIDR、掩码、区间或 any;跨协议按保守并集处理。
52
- def management_sources
61
+ def record_management_sources
53
62
  entries = matching_blocks(/\A(?:no )?admin host\b/)
54
63
  sources = entries.flat_map do |entry|
55
- parts = tokens(entry)
64
+ parts = command_tokens(entry)
56
65
  raise ArgumentError unless parts.take(2) == %w[admin host] && HOST_PROTOCOLS.include?(parts.last)
57
- analyze(entry) { host_networks(parts[2...-1]) }
66
+ analyze(entry) { parse_host_networks(parts[2...-1]) }
58
67
  rescue ArgumentError, IPAddr::Error
59
- unknown("management.acl", [ entry ], code: :unsupported_trusted_host)
68
+ record_unknown_fact("management.acl", [ entry ], code: :unsupported_trusted_host)
60
69
  nil
61
70
  end
62
71
  if entries.empty?
63
- return unless @facts.complete?
72
+ return unless @fact_builder.complete?
64
73
  # 未显式配置受信任主机时,不能据此证明来源访问受限。
65
- @facts.direct_acl(fact: "management.acl", sources: nil, bound: false)
74
+ @fact_builder.record_source_restriction(fact: "management.acl", sources: nil, bound: false)
66
75
  else
67
- @facts.direct_acl(fact: "management.acl", sources: sources.include?(nil) ? nil : sources,
76
+ @fact_builder.record_source_restriction(fact: "management.acl", sources: sources.include?(nil) ? nil : sources,
68
77
  bound: true, lines: entries.map(&:line))
69
78
  end
70
79
  end
71
80
 
72
- # 采集明确启用或禁用的管理服务。
73
- def services
74
- ntp
75
- syslog
76
- snmp
77
- end
78
-
79
81
  # 提取时间同步服务器及相关认证配置。
80
- def ntp
82
+ def parse_ntp
81
83
  entries = matching_blocks(/\Antp server\b/)
82
84
  sources = []
83
85
  servers = entries.filter_map do |entry|
84
- parts = tokens(entry)
86
+ parts = command_tokens(entry)
85
87
  unless server_address?(parts[2])
86
- unknown("ntp", [ entry ])
88
+ record_unknown_fact("ntp", [ entry ])
87
89
  next
88
90
  end
89
- options = pairs(parts.drop(3), flags: [ "prefer" ], keys: %w[key source vrouter])
91
+ options = parse_keyword_options(parts.drop(3), flags: [ "prefer" ], keys: %w[key source vrouter])
90
92
  unless options && (!options["key"] || /\A[1-9]\d*\z/.match?(options["key"]))
91
- unknown("ntp", [ entry ])
93
+ record_unknown_fact("ntp", [ entry ])
92
94
  next
93
95
  end
94
96
  sources << options["source"]
95
97
  analyze(entry) { parts[2] }
96
98
  end
97
- record("ntp.servers", servers.uniq, entries)
99
+ record_fact("ntp.servers", servers.uniq, entries)
98
100
  record_configured_state("ntp.authentication", /\Antp authentication\z/, /\Ano ntp authentication\z/)
99
- record("ntp.source", sources.first, entries) if sources.any? && sources.first && sources.uniq.one?
100
- unknown("ntp", matching_blocks(/\Ano ntp server\b/)) if matching_blocks(/\Ano ntp server\b/).any?
101
+ record_fact("ntp.source", sources.first, entries) if sources.any? && sources.first && sources.uniq.one?
102
+ record_unknown_fact("ntp", matching_blocks(/\Ano ntp server\b/)) if matching_blocks(/\Ano ntp server\b/).any?
101
103
  end
102
104
 
103
105
  # 同一目标可供多个日志类别使用,统计时只计一次。
104
- def syslog
106
+ def parse_syslog
105
107
  entries = matching_blocks(/\Alogging syslog\b/)
106
108
  sources = []
107
109
  servers = entries.filter_map do |entry|
108
- parts = tokens(entry)
110
+ parts = command_tokens(entry)
109
111
  options = parts.drop(3)
110
112
  source = nil
111
113
  if %w[vrouter source-interface].include?(options.first)
112
114
  source = options[1] if options.first == "source-interface"
113
115
  options = options.drop(2)
114
116
  end
115
- valid = server_address?(parts[2]) && %w[tcp udp secure-tcp].include?(options[0]) && service_port(options[1])&.positive?
117
+ valid = server_address?(parts[2]) && %w[tcp udp secure-tcp].include?(options[0]) && parse_service_port(options[1])&.positive?
116
118
  tail = options.drop(2)
117
119
  tail = tail.drop(1) if options[0] == "secure-tcp" && tail.first == "server-cert-check-disable"
118
120
  valid &&= tail.empty? || tail.first == "type" && tail.size >= 2
119
121
  unless valid
120
- unknown("syslog", [ entry ])
122
+ record_unknown_fact("syslog", [ entry ])
121
123
  next
122
124
  end
123
125
  sources << source
124
126
  analyze(entry) { parts[2] }
125
127
  end
126
- record("syslog.servers", servers.uniq, entries)
127
- record("syslog.source", sources.first, entries) if sources.any? && sources.first && sources.uniq.one?
128
- unknown("syslog", matching_blocks(/\Ano logging syslog\b/)) if matching_blocks(/\Ano logging syslog\b/).any?
128
+ record_fact("syslog.servers", servers.uniq, entries)
129
+ record_fact("syslog.source", sources.first, entries) if sources.any? && sources.first && sources.uniq.one?
130
+ record_unknown_fact("syslog", matching_blocks(/\Ano logging syslog\b/)) if matching_blocks(/\Ano logging syslog\b/).any?
129
131
  end
130
132
 
131
133
  # 轮询主机权限独立于出站 Trap 接收端。
132
- def snmp
134
+ def parse_snmp
133
135
  record_configured_state("snmp.enabled", /\Asnmp-server manager\z/, /\Ano snmp-server manager\z/)
134
136
  entries = matching_blocks(/\Asnmp-server host\b/)
135
137
  versions, sources, communities = [], [], []
136
138
  entries.each do |entry|
137
- parts = tokens(entry)
139
+ parts = command_tokens(entry)
138
140
  offset = parts.index("version")
139
141
  raise ArgumentError unless offset && offset > 2
140
- networks = host_networks(parts[2...offset])
142
+ networks = parse_host_networks(parts[2...offset])
141
143
  settings = parts.drop(offset + 1)
142
144
  if %w[1 2c].include?(settings.first) && settings[1] == "community" && !settings[2].to_s.empty? &&
143
145
  (settings.size == 3 || settings.size == 4 && %w[ro rw].include?(settings[3]))
144
- record_credential("snmp-community", "snmp", secret_format(settings[2]), settings[2], entry)
146
+ record_credential("snmp-community", "snmp", credential_storage_format(settings[2]), settings[2], entry)
145
147
  versions << (settings.first == "1" ? "v1" : "v2c")
146
148
  communities << { write: settings[3] == "rw", line: entry.line }
147
149
  sources.concat(networks)
@@ -153,35 +155,86 @@ module Baseline
153
155
  raise ArgumentError
154
156
  end
155
157
  rescue ArgumentError, IPAddr::Error
156
- unknown_snmp_access([ entry ], code: :unsupported_snmp_host)
158
+ record_unknown_snmp_access([ entry ], code: :unsupported_snmp_host)
159
+ end
160
+ groups = parse_snmp_access_groups
161
+ users = parse_snmp_access_users
162
+ versions |= [ "v3" ] if users.any? || groups.any?
163
+ record_fact("snmp.groups", groups)
164
+ record_fact("snmp.versions", versions.uniq, entries)
165
+ unsupported = matching_blocks(/\Ano snmp-server (?:host|user|group)\b/)
166
+ record_unknown_snmp_access(unsupported, code: :unsupported_snmp_security) if unsupported.any?
167
+ if versions.include?("v3") && users.empty?
168
+ record_unknown_snmp_access(entries, code: :snmp_user_unresolved)
169
+ end
170
+ @fact_builder.record_snmp_access(communities: communities, users: users, groups: groups)
171
+ @fact_builder.record_source_restriction(fact: "snmp.acl", sources: sources, bound: true, lines: entries.map(&:line)) if entries.any?
172
+ end
173
+
174
+ # 组只声明访问权限;认证级别要求不能替代用户的实际密钥证据。
175
+ def parse_snmp_access_groups
176
+ entries = matching_blocks(/\Asnmp-server group\b/).select do |entry|
177
+ parts = command_tokens(entry)
178
+ options = parse_keyword_options(parts.drop(5), flags: [], keys: %w[read-view write-view])
179
+ valid = parts.size >= 5 && parts[3] == "v3" && %w[noauth auth auth-enc].include?(parts[4]) &&
180
+ !parts[2].to_s.empty? && options && options.values.none?(&:empty?)
181
+ record_unknown_snmp_access([ entry ], code: :unsupported_snmp_group) unless valid
182
+ valid
183
+ end
184
+ configured_snmp_groups(entries) do |entry|
185
+ parts = command_tokens(entry)
186
+ options = parse_keyword_options(parts.drop(5), flags: [], keys: %w[read-view write-view])
187
+ analyze(entry) { true }
188
+ [ parts[2], { write: options.key?("write-view"), line: entry.line, acl: nil } ]
189
+ end
190
+ end
191
+
192
+ # StoneOS 用户显式引用组及远端地址,秘密值仅用于提取安全属性。
193
+ def parse_snmp_access_users
194
+ matching_blocks(/\Asnmp-server user\b/).filter_map do |entry|
195
+ parts = command_tokens(entry)
196
+ valid = parts.size >= 8 && !parts[2].to_s.empty? && parts[3] == "group" && !parts[4].to_s.empty? &&
197
+ parts[5] == "v3" && parts[6] == "remote"
198
+ address = IPAddr.new(parts[7]) if valid
199
+ valid &&= address.ipv4? && !parts[7].include?("/")
200
+ security = parts.drop(8)
201
+ valid &&= security.empty? || [ 3, 6 ].include?(security.size) && security[0] == "auth-protocol" &&
202
+ %w[md5 sha].include?(security[1]) && !security[2].to_s.empty? &&
203
+ (security.size == 3 || security[3] == "enc-protocol" && %w[des aes].include?(security[4]) && !security[5].to_s.empty?)
204
+ unless valid
205
+ record_unknown_snmp_access([ entry ], code: :unsupported_snmp_user)
206
+ next
207
+ end
208
+ security.each_slice(3) do |mode|
209
+ record_credential("snmp:#{parts[2]}", "snmp_user", credential_storage_format(mode[2]), mode[2], entry)
210
+ end
211
+ analyze(entry) { { name: parts[2], group: parts[4], auth: security.any?, priv: security.size == 6, line: entry.line } }
212
+ rescue ArgumentError, IPAddr::Error
213
+ record_unknown_snmp_access([ entry ], code: :unsupported_snmp_user)
214
+ nil
157
215
  end
158
- record("snmp.versions", versions.uniq, entries)
159
- unsupported = matching_blocks(/\A(?:snmp-server (?:user|group)\b|no snmp-server (?:host|user|group)\b)/)
160
- unknown_snmp_access(unsupported, code: :unsupported_snmp_security) if unsupported.any? || versions.include?("v3")
161
- @facts.snmp(communities: communities, users: [])
162
- @facts.direct_acl(fact: "snmp.acl", sources: sources, bound: true, lines: entries.map(&:line)) if entries.any?
163
216
  end
164
217
 
165
218
  # 分别记录 TACACS 定义与管理认证引用。
166
- def authentication
219
+ def parse_authentication
167
220
  definitions = matching_blocks(/\Aaaa-server\s+/).filter_map do |section|
168
- parts = tokens(section)
221
+ parts = command_tokens(section)
169
222
  unless parts.size == 4 && parts[2..] == %w[type tacacs+]
170
- unknown("tacacs", [ section ]) if parts.include?("tacacs+")
223
+ record_unknown_fact("tacacs", [ section ]) if parts.include?("tacacs+")
171
224
  next
172
225
  end
173
226
  endpoints = matching_blocks(/\A(?:host|backup[12])\b/, section.children)
174
227
  servers = endpoints.filter_map do |entry|
175
- values = tokens(entry)
228
+ values = command_tokens(entry)
176
229
  if server_address?(values[1]) && (values.size == 2 || values.size == 4 && values[2] == "vrouter")
177
230
  analyze(entry) { values[1] }
178
231
  else
179
- unknown("tacacs", [ entry ])
232
+ record_unknown_fact("tacacs", [ entry ])
180
233
  nil
181
234
  end
182
235
  end
183
236
  secrets = matching_blocks(/\Asecret\b/, section.children)
184
- keys = secrets.map { |entry| stored_secret("tacacs:#{parts[1]}", "tacacs", entry) }
237
+ keys = secrets.map { |entry| record_stored_credential("tacacs:#{parts[1]}", "tacacs", entry) }
185
238
  [ parts[1], { servers: servers.uniq, keys_present: keys.any? && keys.all?, line: section.line,
186
239
  declaration: section.children.empty? } ]
187
240
  end
@@ -189,62 +242,62 @@ module Baseline
189
242
  # StoneOS 会先导出空的前向声明,再导出包含内容的服务器块。
190
243
  populated = entries.map(&:last).reject { |entry| entry[:declaration] }
191
244
  selected = populated.last || entries.last.last
192
- @facts.unknown("tacacs", lines: populated.map { |entry| entry[:line] }, code: :duplicate_tacacs_scheme) if populated.size > 1
245
+ @fact_builder.record_unknown_fact("tacacs", lines: populated.map { |entry| entry[:line] }, code: :duplicate_tacacs_scheme) if populated.size > 1
193
246
  if selected[:servers].empty?
194
- @facts.unknown("tacacs.servers", lines: [ selected[:line] ], code: :tacacs_server_unresolved)
247
+ @fact_builder.record_unknown_fact("tacacs.servers", lines: [ selected[:line] ], code: :tacacs_server_unresolved)
195
248
  end
196
249
  [ name, selected.except(:declaration) ]
197
250
  end
198
- record("tacacs.schemes", schemes)
199
- record("tacacs.servers", schemes.values.flat_map { |scheme| scheme[:servers] }.uniq)
200
- record("tacacs.keys_present", schemes.values.all? { |scheme| scheme[:keys_present] }) if schemes.any?
251
+ record_fact("tacacs.schemes", schemes)
252
+ record_fact("tacacs.servers", schemes.values.flat_map { |scheme| scheme[:servers] }.uniq)
253
+ record_fact("tacacs.keys_present", schemes.values.all? { |scheme| scheme[:keys_present] }) if schemes.any?
201
254
  { "authentication-server" => "login", "authorization-mode" => "authorization" }.each do |command, fact|
202
255
  entries = matching_blocks(/\Aadmin #{command}\b/)
203
256
  next if entries.empty?
204
- parts = tokens(entries.last)
257
+ parts = command_tokens(entries.last)
205
258
  name = parts[3] if parts.size == 4 && parts[2] == "aaa-server"
206
259
  if name && schemes[name] && schemes[name][:servers].any?
207
- record("tacacs.#{fact}", true, entries)
260
+ record_fact("tacacs.#{fact}", true, entries)
208
261
  else
209
- unknown("tacacs.#{fact}", entries, code: :tacacs_scheme_unresolved)
262
+ record_unknown_fact("tacacs.#{fact}", entries, code: :tacacs_scheme_unresolved)
210
263
  end
211
264
  end
212
265
  matching_blocks(/\Aadmin user\s+/).each do |section|
213
- owner = tokens(section)[2]
266
+ owner = command_tokens(section)[2]
214
267
  passwords = matching_blocks(/\Apassword\b/, section.children)
215
268
  record_credential(owner, "local_user", "unknown", nil, section) if passwords.empty?
216
- passwords.each { |entry| stored_secret(owner, "local_user", entry) }
269
+ passwords.each { |entry| record_stored_credential(owner, "local_user", entry) }
217
270
  end
218
271
  end
219
272
 
220
273
  # 编码格式的存储语义未经确认时保持未知,不能猜测为明文。
221
- def secret_format(value) = value.start_with?("$") ? "unknown" : "plain"
274
+ def credential_storage_format(value) = value.start_with?("$") ? "unknown" : "plain"
222
275
 
223
276
  # 仅记录已配置秘密值的存储属性。
224
- def stored_secret(owner, kind, entry)
225
- parts = tokens(entry).drop(1)
277
+ def record_stored_credential(owner, kind, entry)
278
+ parts = command_tokens(entry).drop(1)
226
279
  valid = parts.size == 1 && !%w[cipher hash plain].include?(parts.first) || parts.size == 2 && %w[cipher hash plain].include?(parts.first)
227
280
  unless valid && !parts.last.to_s.empty?
228
- @facts.incomplete!(code: :invalid_credential_statement, lines: [ entry.line ])
281
+ @fact_builder.mark_incomplete!(code: :invalid_credential_statement, lines: [ entry.line ])
229
282
  record_credential(owner, kind, "unknown", nil, entry)
230
283
  return false
231
284
  end
232
- format, secret = parts.size == 2 && %w[cipher hash plain].include?(parts.first) ? parts : [ secret_format(parts.first.to_s), parts.first ]
285
+ format, secret = parts.size == 2 && %w[cipher hash plain].include?(parts.first) ? parts : [ credential_storage_format(parts.first.to_s), parts.first ]
233
286
  record_credential(owner, kind, format, secret, entry)
234
287
  true
235
288
  end
236
289
 
237
290
  # 切分含引号的命令值,不将其作为 Ruby 代码解释。
238
- def tokens(entry)
291
+ def command_tokens(entry)
239
292
  @tokens[entry.index] ||= Shellwords.shellsplit(entry.text)
240
293
  rescue ArgumentError
241
- @facts.incomplete!(code: :invalid_quoted_statement, lines: [ entry.line ])
294
+ @fact_builder.mark_incomplete!(code: :invalid_quoted_statement, lines: [ entry.line ])
242
295
  cursor.mark(entry.index, state: :unsupported)
243
296
  @tokens[entry.index] = []
244
297
  end
245
298
 
246
299
  # 校验可选参数,避免接受任意尾随命令片段。
247
- def pairs(parts, flags:, keys:)
300
+ def parse_keyword_options(parts, flags:, keys:)
248
301
  values = {}
249
302
  until parts.empty?
250
303
  key = parts.shift
@@ -261,12 +314,12 @@ module Baseline
261
314
  end
262
315
 
263
316
  # 将命名主机或网络对象归一化为地址区间。
264
- def host_networks(parts)
317
+ def parse_host_networks(parts)
265
318
  return [ "0.0.0.0/0" ] if parts == [ "any" ]
266
319
  if parts.size == 3 && parts.first == "range"
267
320
  first, last = parts.drop(1).map { |value| IPAddr.new(value) }
268
321
  raise ArgumentError unless first.ipv4? && last.ipv4? && first.prefix == 32 && last.prefix == 32 && first.to_i <= last.to_i
269
- Acl.network_prefixes([ [ first.to_i, last.to_i ] ])
322
+ Acl.cidrs_from_ranges([ [ first.to_i, last.to_i ] ])
270
323
  elsif parts.size == 1 || parts.size == 2
271
324
  value = IPAddr.new(parts.join("/"))
272
325
  raise ArgumentError unless value.ipv4?
@@ -9,40 +9,42 @@ module Baseline
9
9
  private
10
10
 
11
11
  # 匹配当前平台显式启用 SSH 的命令。
12
- def ssh_enable = /\Astelnet server enable\z/
12
+ def ssh_enable_pattern = /\Astelnet server enable\z/
13
13
  # 匹配当前平台显式禁用 SSH 的命令。
14
- def ssh_disable = /\A(?:undo stelnet server enable|stelnet server disable)\z/
14
+ def ssh_disable_pattern = /\A(?:undo stelnet server enable|stelnet server disable)\z/
15
15
  # 匹配当前平台 TACACS 配置块的根语句。
16
- def tacacs_section = /\Ahwtacacs-server template\s+/
16
+ def tacacs_section_pattern = /\Ahwtacacs-server template\s+/
17
17
  # 匹配认证、授权或计费服务器声明。
18
- def tacacs_server = /\Ahwtacacs-server (?:authentication|authorization|accounting)\s+(\S+)/
18
+ def tacacs_server_pattern = /\Ahwtacacs-server (?:authentication|authorization|accounting)\s+(\S+)/
19
+ # 完整选择服务器命令族,缺失地址也必须保留为损坏证据。
20
+ def tacacs_server_family_pattern = /\A(?:undo )?hwtacacs-server (?:authentication|authorization|accounting)(?:\s|\z)/
19
21
  # 匹配已配置 TACACS 密钥的存储格式。
20
- def tacacs_key = /\Ahwtacacs-server (shared)-key (cipher|simple) (.+)\z/
22
+ def tacacs_key_pattern = /\Ahwtacacs-server (shared)-key (cipher|simple) (.+)\z/
21
23
  # 要求具备当前平台 TACACS 方案使用的全部密钥角色。
22
24
  def tacacs_keys_complete?(types) = types.include?("shared")
23
25
  # 返回当前平台修改 SNMP 监听端口的命令模式。
24
26
  def snmp_port_command = "snmp-agent udp-port"
25
27
  # 从实际所属配置作用域中选取 AAA 声明。
26
- def aaa_entries = matching_blocks(/\Aaaa\z/).flat_map(&:children)
28
+ def aaa_configuration_entries = matching_blocks(/\Aaaa\z/).flat_map(&:children)
27
29
  # 解析认证域引用及其服务器组。
28
30
  def authentication_domains
29
31
  name = last_configured_value(/\Adomain\s+(\S+)\s+admin\z/)
30
32
  unless name
31
- %w[login authorization accounting].each { |kind| unknown("tacacs.#{kind}", code: :authentication_domain_unknown) }
33
+ %w[login authorization accounting].each { |kind| record_unknown_fact("tacacs.#{kind}", code: :authentication_domain_unknown) }
32
34
  return []
33
35
  end
34
- aaa_entries.select { |entry| entry.text == "domain #{name}" }
36
+ aaa_configuration_entries.select { |entry| entry.text == "domain #{name}" }
35
37
  end
36
38
 
37
39
  # 解析 VRP 认证域引用,避免将 VRP 语法应用到 Comware。
38
- def vrp_tacacs_references(kind, domains)
40
+ def domain_tacacs_references(kind, domains)
39
41
  domains.filter_map do |domain|
40
42
  template = last_configured_value(/\Ahwtacacs-server\s+(\S+)\z/, domain.children)
41
43
  scheme = last_configured_value(/\A#{kind}-scheme\s+(\S+)\z/, domain.children)
42
44
  next unless template && scheme
43
- definition = aaa_entries.find { |entry| entry.text == "#{kind}-scheme #{scheme}" }
45
+ definition = aaa_configuration_entries.find { |entry| entry.text == "#{kind}-scheme #{scheme}" }
44
46
  unless definition
45
- unknown("tacacs.#{kind == 'authentication' ? 'login' : kind}", [ domain ], code: :tacacs_scheme_unresolved)
47
+ record_unknown_fact("tacacs.#{kind == 'authentication' ? 'login' : kind}", [ domain ], code: :tacacs_scheme_unresolved)
46
48
  next
47
49
  end
48
50
  template if last_configured_value(/\A#{kind}-mode\s+(.+)\z/, definition.children).to_s.split.first == "hwtacacs"
@@ -50,8 +52,8 @@ module Baseline
50
52
  end
51
53
 
52
54
  # 提取本地账号角色及凭据存储属性。
53
- def accounts
54
- matching_blocks(/\Alocal-user\s+\S+\s+password\b/, aaa_entries).each do |entry|
55
+ def parse_local_accounts
56
+ matching_blocks(/\Alocal-user\s+\S+\s+password\b/, aaa_configuration_entries).each do |entry|
55
57
  match = /\Alocal-user\s+(\S+)\s+password\s+(cipher|irreversible-cipher|simple)\s+(.+)\z/.match(entry.text)
56
58
  if match
57
59
  record_credential(match[1], "local_user", match[2], match[3], entry)
@@ -4,7 +4,7 @@ require_relative "document"
4
4
 
5
5
  module Baseline
6
6
  # 多行引号内容始终作为数据处理,即使正文看起来像全局命令。
7
- class QuotedDocument < Document
7
+ class MultilineDocument < Document
8
8
  protected
9
9
 
10
10
  # 区分配置结构行与引号、横幅中的文本内容。
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Baseline
4
+ module PaloAlto
5
+ # 按词元建立前缀索引,使账号、服务和配置文件查询不反复扫描整个导出。
6
+ class CommandIndex
7
+ # 深层查询扫描候选行,避免无效标量尾部扩大索引;完整词元仍保留供语法校验。
8
+ MAX_INDEX_DEPTH = 7
9
+ Node = Struct.new(:children, :rows)
10
+ private_constant :MAX_INDEX_DEPTH, :Node
11
+
12
+ # 来源顺序与重复声明均保留,标量调用者仍可选择最后一行。
13
+ def initialize(rows)
14
+ @root = Node.new({}, rows.dup.freeze)
15
+ nodes = [ @root ]
16
+ rows.each do |row|
17
+ node = @root
18
+ row.fetch(:tokens).take(MAX_INDEX_DEPTH).each do |token|
19
+ node = node.children[token] ||= begin
20
+ child = Node.new({}, [])
21
+ nodes << child
22
+ child
23
+ end
24
+ node.rows << row
25
+ end
26
+ end
27
+ nodes.each { |node| node.children.freeze; node.rows.freeze; node.freeze }
28
+ freeze
29
+ end
30
+
31
+ # 按完整词元匹配前缀,避免同名账号、层级或包含空格的值串扰。
32
+ def matching_prefix(prefix)
33
+ node = @root
34
+ prefix.take(MAX_INDEX_DEPTH).each do |token|
35
+ node = node.children[token]
36
+ return [] unless node
37
+ end
38
+ return node.rows if prefix.size <= MAX_INDEX_DEPTH
39
+
40
+ node.rows.select { |row| row.fetch(:tokens).take(prefix.size) == prefix }.freeze
41
+ end
42
+
43
+ # 索引持有词元,默认调试输出不能展开配置或凭据。
44
+ def inspect = "#<#{self.class}>"
45
+ end
46
+ private_constant :CommandIndex
47
+ end
48
+ end
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../quoted_document"
3
+ require_relative "../multiline_document"
4
4
 
5
5
  module Baseline
6
6
  module PaloAlto
7
7
  # PAN-OS 的 set 导出可能包含引号包裹的多行证书与描述。
8
- class Document < QuotedDocument
8
+ class Document < MultilineDocument
9
9
  protected
10
10
 
11
11
  # 已建模的多行值不能被误判为配置缺失;证书正文不参与语义分析。
12
12
  def multiline_fact?(text)
13
- /\Aset (?:deviceconfig system (?:hostname|service|permitted-ip|ntp-servers|snmp-setting|authentication-profile|route service)\b|shared (?:server-profile tacplus|authentication-profile|authentication-sequence|log-settings syslog)\b|mgt-config users\b|network profiles interface-management-profile\b)/.match?(text)
13
+ /\Aset (?:deviceconfig system (?:hostname|service|permitted-ip|ntp-servers|snmp-setting|authentication-profile|route service)\b|shared (?:server-profile tacplus|authentication-profile|authentication-sequence|log-settings syslog)\b|mgt-config users\b|network (?:profiles interface-management-profile|interface)\b)/.match?(text)
14
14
  end
15
15
  end
16
16
  end