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.
@@ -7,7 +7,7 @@ require "ctx_parser/result"
7
7
 
8
8
  module Baseline
9
9
  # 归并跨命令引用及未知状态,不依赖厂商语法或解析游标。
10
- class Facts
10
+ class FactBuilder
11
11
  attr_reader :vtys
12
12
 
13
13
  # 初始化带来源信息的事实存储,并显式记录证据完整性。
@@ -18,6 +18,7 @@ module Baseline
18
18
  @vtys, @communities, @snmp_users, @credentials = [], [], [], []
19
19
  @credential_lines = {}
20
20
  @snmp_groups = {}
21
+ @snmp_group_issues = []
21
22
  @snmp_permission_issues = []
22
23
  @ssh_acl = nil
23
24
  @management_ports = { "ssh" => 22, "stelnet" => 22, "telnet" => 23 }
@@ -29,39 +30,43 @@ module Baseline
29
30
  def complete? = @complete
30
31
 
31
32
  # 相关配置语句格式错误时,不能继续依据配置缺失推导结论。
32
- def incomplete!(code:, lines: [])
33
+ def mark_incomplete!(code:, lines: [])
33
34
  @complete = false
34
35
  @issues << { code: code, lines: lines.uniq }
35
36
  end
36
37
 
37
38
  # 防火墙可直接声明来源网段,不要求一定引用路由器式命名 ACL。
38
- def direct_acl(fact:, sources:, bound:, lines: [])
39
+ def record_source_restriction(fact:, sources:, bound:, lines: [])
39
40
  unless %w[management.acl snmp.acl].include?(fact) && [ true, false ].include?(bound) &&
40
41
  (sources.nil? || sources.is_a?(Array))
41
42
  raise ArgumentError, "Direct access requires a known scope, binding state and source networks"
42
43
  end
43
- networks = sources && Acl.network_prefixes(sources.map { |source| Acl.address_range(source) })
44
+ networks = sources && Acl.cidrs_from_ranges(sources.map { |source| Acl.ipv4_range(source) })
44
45
  @direct_acls[fact] = true
45
- record("#{fact}.bound", bound, lines: lines)
46
+ record_fact("#{fact}.bound", bound, lines: lines)
46
47
  if networks
47
- record("#{fact}.sources", networks, lines: lines)
48
- record("#{fact}.restricted", !networks.include?("0.0.0.0/0"), lines: lines)
48
+ record_fact("#{fact}.sources", networks, lines: lines)
49
+ record_fact("#{fact}.restricted", !networks.include?("0.0.0.0/0"), lines: lines)
49
50
  else
50
- %w[sources restricted].each { |key| unknown("#{fact}.#{key}", lines: lines, code: :acl_unresolved) }
51
+ %w[sources restricted].each { |key| record_unknown_fact("#{fact}.#{key}", lines: lines, code: :acl_unresolved) }
51
52
  end
52
53
  end
53
54
 
54
55
  # 将 false 保留为已知事实;输入不完整时,不接受 nil 或空集作为确定值。
55
- def record(path, value, lines: [])
56
+ def record_fact(path, value, lines: [])
56
57
  return false if value.nil? || (!complete? && value.respond_to?(:empty?) && value.empty?)
57
58
 
58
59
  @values[path] = CtxParser::Immutable.copy(value)
59
- @sources[path] = lines.uniq unless lines.empty?
60
+ if lines.empty?
61
+ @sources.delete(path)
62
+ else
63
+ @sources[path] = lines.uniq
64
+ end
60
65
  true
61
66
  end
62
67
 
63
68
  # 记录事实不可用的原因,并保留对应的来源行。
64
- def unknown(path, lines: [], code: :unsupported_syntax)
69
+ def record_unknown_fact(path, lines: [], code: :unsupported_syntax)
65
70
  @issues << { code: code, fact: path, lines: lines.uniq }
66
71
  end
67
72
 
@@ -69,22 +74,23 @@ module Baseline
69
74
  def register_acl(acl, aliases: [], lines: [])
70
75
  keys = ([ acl.name ] + aliases).uniq
71
76
  duplicates = keys.select { |key| @acls.key?(key) }
72
- duplicates.each { |key| unknown("acls.#{key}", lines: lines, code: :duplicate_acl) }
77
+ duplicates.each { |key| record_unknown_fact("acls.#{key}", lines: lines, code: :duplicate_acl) }
73
78
  keys.each { |key| @acls[key] = acl }
74
79
  duplicates
75
80
  end
76
81
 
77
82
  # 根据监听服务、认证方式与 ACL 绑定关系归纳管理访问事实。
78
- def management(vtys:, ssh_acl: nil, ssh_port: 22, telnet_port: 23)
83
+ def record_management_access(vtys:, ssh_acl: nil, ssh_port: 22, telnet_port: 23)
79
84
  @vtys, @ssh_acl = CtxParser::Immutable.copy(vtys), CtxParser::Immutable.copy(ssh_acl)
80
85
  @management_ports = { "ssh" => ssh_port, "stelnet" => ssh_port, "telnet" => telnet_port }
81
86
  end
82
87
 
83
88
  # 根据用户、组和团体字归纳 SNMP 暴露面及凭据事实。
84
- def snmp(communities:, users:, groups: nil, port: 161)
89
+ def record_snmp_access(communities:, users:, groups: nil, port: 161)
85
90
  @snmp_port = port
86
91
  @communities = CtxParser::Immutable.copy(communities)
87
92
  @snmp_groups = CtxParser::Immutable.copy(groups || {})
93
+ @snmp_group_issues = []
88
94
  @snmp_users = users.map do |user|
89
95
  if groups && user[:group]
90
96
  if groups.key?(user[:group])
@@ -92,10 +98,10 @@ module Baseline
92
98
  user = user.merge(write: group[:write])
93
99
  unless user[:acl]
94
100
  user = user.merge(acl: group[:acl])
95
- unknown("snmp.acl", code: :snmp_group_ambiguous) if group[:acl_ambiguous]
101
+ @snmp_group_issues << { code: :snmp_group_ambiguous, fact: "snmp.acl", lines: [] } if group[:acl_ambiguous]
96
102
  end
97
103
  elsif !user[:acl]
98
- unknown("snmp.acl", code: :snmp_group_unresolved)
104
+ @snmp_group_issues << { code: :snmp_group_unresolved, fact: "snmp.acl", lines: [] }
99
105
  end
100
106
  end
101
107
  CtxParser::Immutable.copy(user)
@@ -103,7 +109,7 @@ module Baseline
103
109
  end
104
110
 
105
111
  # 即使其他权限尚未解析,已确认的写权限仍属于违规证据。
106
- def self.write_permission(permissions)
112
+ def self.write_permission_state(permissions)
107
113
  return true if permissions.include?(true)
108
114
  return false if permissions.all?(false)
109
115
 
@@ -111,7 +117,7 @@ module Baseline
111
117
  end
112
118
 
113
119
  # 暂存不完整的访问证据,优先保留已确认的权限授予事实。
114
- def unknown_snmp_permission(lines: [], code: :snmp_permission_unresolved)
120
+ def record_unknown_snmp_permission(lines: [], code: :snmp_permission_unresolved)
115
121
  @snmp_permission_issues << { code: code, fact: "snmp.write_enabled", lines: lines.uniq }
116
122
  end
117
123
 
@@ -128,32 +134,47 @@ module Baseline
128
134
  end
129
135
 
130
136
  # 汇总事实与诊断,并保留证据完整性状态。
131
- def result
132
- record("acls", @acls.values.uniq.map(&:to_h))
133
- management_facts
134
- snmp_facts
135
- credential_facts
137
+ def build
138
+ dup.build_snapshot
139
+ end
140
+
141
+ # 展示对象身份与状态,避免泄露原始回显或凭据。
142
+ def inspect = "#<#{self.class} complete=#{complete?} facts=#{@values.size}>"
143
+
144
+ protected
145
+
146
+ # 归约仅写入当前副本,后续快照始终从解析器提供的原始证据重新生成。
147
+ def build_snapshot
148
+ record_fact("acls", @acls.values.uniq.map(&:to_h))
149
+ record_management_facts
150
+ record_snmp_facts
151
+ record_credential_facts
136
152
  @issues << { code: :hostname_missing } unless @values.key?("hostname")
137
153
  @issues << { code: :input_not_confirmed_complete } unless @confirmed_complete
138
154
  status = complete? && @values.key?("hostname") ? :complete : :partial
139
155
  CtxParser::Result.new(status: status,
140
- records: CtxParser::Immutable.copy([ { "facts" => @values, "sources" => @sources } ]), issues: CtxParser::Immutable.copy(@issues.uniq))
156
+ records: [ { "facts" => @values, "sources" => @sources } ], issues: @issues.uniq)
141
157
  end
142
158
 
143
- # 展示对象身份与状态,避免泄露原始回显或凭据。
144
- def inspect = "#<#{self.class} complete=#{complete?} facts=#{@values.size}>"
145
-
146
159
  private
147
160
 
161
+ # 已记录的值不可变;复制归约会更新的容器即可隔离派生事实与诊断。
162
+ def initialize_copy(original)
163
+ super
164
+ @values = @values.dup
165
+ @sources = @sources.dup
166
+ @issues = @issues.dup
167
+ end
168
+
148
169
  # 归纳管理服务监听端点及其实际生效的访问限制。
149
- def management_facts
150
- record("ssh.vty", vtys)
170
+ def record_management_facts
171
+ record_fact("ssh.vty", vtys)
151
172
  if vtys.any? && vtys.all? { |entry| entry[:protocols] }
152
173
  protocols = vtys.flat_map { |entry| entry[:protocols] }.uniq
153
- record("ssh.vty_ssh_only", protocols.any? && (protocols - %w[ssh stelnet]).empty?)
174
+ record_fact("ssh.vty_ssh_only", protocols.any? && (protocols - %w[ssh stelnet]).empty?)
154
175
  end
155
176
  if vtys.any? && vtys.all? { |entry| entry[:authentication] }
156
- record("ssh.vty_aaa", vtys.all? { |entry| %w[scheme aaa local].include?(entry[:authentication]) })
177
+ record_fact("ssh.vty_aaa", vtys.all? { |entry| %w[scheme aaa local].include?(entry[:authentication]) })
157
178
  end
158
179
  # 全局 SSH ACL 仅覆盖明确限制为 SSH 的管理监听端点。
159
180
  bindings = if @ssh_acl && @values["ssh.vty_ssh_only"] == true
@@ -161,40 +182,44 @@ module Baseline
161
182
  else
162
183
  vtys.map { |entry| { name: entry[:acl], ports: management_ports(entry[:protocols]) } }
163
184
  end
164
- acl_sources(bindings, protocol: "tcp", fact: "management.acl") unless @direct_acls["management.acl"]
185
+ record_acl_sources(bindings, protocol: "tcp", fact: "management.acl") unless @direct_acls["management.acl"]
165
186
  if vtys.any?
166
187
  %w[ssh.vty_ssh_only ssh.vty_aaa management.acl.bound].each { |path| @sources[path] = vtys.map { |entry| entry[:line] } }
167
188
  end
168
189
  end
169
190
 
170
191
  # 归纳 SNMP 监听、团体字及用户的安全属性。
171
- def snmp_facts
172
- record("snmp.communities", @communities)
173
- record("snmp.community_present", @communities.any?) if complete? || @communities.any?
192
+ def record_snmp_facts
193
+ @issues.concat(@snmp_group_issues)
194
+ record_fact("snmp.communities", @communities)
195
+ record_fact("snmp.community_present", @communities.any?) if complete? || @communities.any?
174
196
  access = @communities + @snmp_users + @snmp_groups.values
175
197
  permissions = access.map { |entry| entry[:write] }
176
198
  permissions << nil unless @snmp_permission_issues.empty?
177
- writable = self.class.write_permission(permissions)
178
- record("snmp.write_enabled", writable, lines: access.flat_map { |entry| entry[:lines] || [ entry[:line] ] }.compact)
199
+ writable = self.class.write_permission_state(permissions)
200
+ record_fact("snmp.write_enabled", writable, lines: access.flat_map { |entry| entry[:lines] || [ entry[:line] ] }.compact)
179
201
  if writable.nil?
180
- unknown("snmp.write_enabled", code: :snmp_permission_unresolved)
202
+ record_unknown_fact("snmp.write_enabled", code: :snmp_permission_unresolved)
181
203
  @issues.concat(@snmp_permission_issues)
182
204
  end
183
- record("snmp.users", @snmp_users)
184
- record("snmp.v3_auth_priv", @snmp_users.all? { |entry| entry[:auth] && entry[:priv] }) if @snmp_users.any?
205
+ record_fact("snmp.users", @snmp_users)
206
+ if @snmp_users.any?
207
+ record_fact("snmp.v3_auth_priv", @snmp_users.all? { |entry| entry[:auth] && entry[:priv] },
208
+ lines: @snmp_users.map { |entry| entry[:line] }.compact)
209
+ end
185
210
  bindings = (@communities + @snmp_users).map { |entry| { name: entry[:acl], ports: @snmp_port ? [ @snmp_port ] : nil } }
186
- acl_sources(bindings, protocol: "udp", fact: "snmp.acl") unless @direct_acls["snmp.acl"]
211
+ record_acl_sources(bindings, protocol: "udp", fact: "snmp.acl") unless @direct_acls["snmp.acl"]
187
212
  end
188
213
 
189
214
  # 汇总凭据存储方式及可度量的强度属性。
190
- def credential_facts
191
- record("credentials.records", @credentials)
192
- record("credentials.present", @credentials.any?) if complete? || @credentials.any?
215
+ def record_credential_facts
216
+ record_fact("credentials.records", @credentials)
217
+ record_fact("credentials.present", @credentials.any?) if complete? || @credentials.any?
193
218
  return if @credentials.empty?
194
219
 
195
220
  storage = @credentials.map { |entry| entry[:storage] }
196
221
  safe = storage.any? { |value| %w[plaintext legacy].include?(value) } ? false : storage.include?("unknown") ? nil : true
197
- record("credentials.storage_safe", safe)
222
+ record_fact("credentials.storage_safe", safe)
198
223
  %w[credentials.records credentials.storage_safe].each { |path| @sources[path] = @credentials.map { |entry| entry[:line] }.uniq }
199
224
  end
200
225
 
@@ -206,13 +231,13 @@ module Baseline
206
231
  end
207
232
 
208
233
  # 解析已绑定 ACL 的来源范围,并保留不支持的证据。
209
- def acl_sources(bindings, protocol:, fact:)
234
+ def record_acl_sources(bindings, protocol:, fact:)
210
235
  names = bindings.map { |binding| binding[:name] }
211
236
  if names.empty? || names.any? { |name| name.nil? || name.empty? }
212
- record("#{fact}.bound", false) if complete?
237
+ record_fact("#{fact}.bound", false) if complete?
213
238
  return
214
239
  end
215
- record("#{fact}.bound", true)
240
+ record_fact("#{fact}.bound", true)
216
241
  resolved = bindings.uniq.map do |binding|
217
242
  name = binding[:name]
218
243
  acl = @acls[name]
@@ -222,13 +247,13 @@ module Baseline
222
247
  sources.flatten unless sources.any?(&:nil?)
223
248
  end
224
249
  if resolved.any?(&:nil?)
225
- unknown("#{fact}.sources", code: :acl_unresolved)
226
- unknown("#{fact}.restricted", code: :acl_unresolved)
250
+ record_unknown_fact("#{fact}.sources", code: :acl_unresolved)
251
+ record_unknown_fact("#{fact}.restricted", code: :acl_unresolved)
227
252
  return
228
253
  end
229
- sources = Acl.network_prefixes(resolved.flatten.uniq.map { |cidr| Acl.address_range(cidr) })
230
- record("#{fact}.sources", sources)
231
- record("#{fact}.restricted", !sources.include?("0.0.0.0/0"))
254
+ sources = Acl.cidrs_from_ranges(resolved.flatten.uniq.map { |cidr| Acl.ipv4_range(cidr) })
255
+ record_fact("#{fact}.sources", sources)
256
+ record_fact("#{fact}.restricted", !sources.include?("0.0.0.0/0"))
232
257
  lines = names.uniq.flat_map { |name| @acls.fetch(name).rules.map { |rule| rule[:line] } }.compact.uniq
233
258
  %w[sources restricted].each { |field| @sources["#{fact}.#{field}"] = lines }
234
259
  end
@@ -9,15 +9,17 @@ module Baseline
9
9
  private
10
10
 
11
11
  # 匹配当前平台显式启用 SSH 的命令。
12
- def ssh_enable = /\Assh server enable\z/
12
+ def ssh_enable_pattern = /\Assh server enable\z/
13
13
  # 匹配当前平台显式禁用 SSH 的命令。
14
- def ssh_disable = /\A(?:undo ssh server enable|ssh server disable)\z/
14
+ def ssh_disable_pattern = /\A(?:undo ssh server enable|ssh server disable)\z/
15
15
  # 匹配当前平台 TACACS 配置块的根语句。
16
- def tacacs_section = /\Ahwtacacs scheme\s+/
16
+ def tacacs_section_pattern = /\Ahwtacacs scheme\s+/
17
17
  # 匹配认证、授权或计费服务器声明。
18
- def tacacs_server = /\A(?:primary|secondary) (?:authentication|authorization|accounting)\s+(\S+)/
18
+ def tacacs_server_pattern = /\A(?:primary|secondary) (?:authentication|authorization|accounting)\s+(\S+)/
19
+ # 完整选择服务器命令族,缺失地址也必须保留为损坏证据。
20
+ def tacacs_server_family_pattern = /\A(?:undo )?(?:primary|secondary) (?:authentication|authorization|accounting)(?:\s|\z)/
19
21
  # 匹配已配置 TACACS 密钥的存储格式。
20
- def tacacs_key = /\Akey (authentication|authorization|accounting) (simple|cipher) (.+)\z/
22
+ def tacacs_key_pattern = /\Akey (authentication|authorization|accounting) (simple|cipher) (.+)\z/
21
23
  # 要求具备当前平台 TACACS 方案使用的全部密钥角色。
22
24
  def tacacs_keys_complete?(types) = (%w[authentication authorization accounting] - types).empty?
23
25
  end
@@ -1,11 +1,11 @@
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 Hillstone
7
7
  # StoneOS 导出内容可能包含命令回显、显式退出及引号包裹的界面状态。
8
- class Document < QuotedDocument
8
+ class Document < MultilineDocument
9
9
  protected
10
10
 
11
11
  # 区分配置结构行与引号、横幅中的文本内容。