baseline-rb 0.1.0 → 0.2.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.
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../cisco/parser"
4
+
3
5
  module Baseline
4
- module Parsers
6
+ module CiscoIos
5
7
  # 解析 IOS、IOS-XE 的 VTY 协议和 AAA 登录列表。
6
- class CiscoIos < Cisco
8
+ class Parser < Cisco::Parser
7
9
  private
8
10
 
9
11
  # 选择共享 Cisco 解析逻辑中的 NX-OS 分支。
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../cisco/parser"
4
+
3
5
  module Baseline
4
- module Parsers
6
+ module CiscoNxos
5
7
  # 解析 NX-OS 功能开关、角色权限及命名扩展 ACL。
6
- class CiscoNxos < Cisco
8
+ class Parser < Cisco::Parser
7
9
  private
8
10
 
9
11
  # 选择共享 Cisco 解析逻辑中的 NX-OS 分支。
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../parser"
4
+
3
5
  module Baseline
4
- module Parsers
6
+ module Comware
5
7
  # 复用 Comware 与 VRP 结构,由厂商子类选择账号、SSH 和 TACACS 语法。
6
- class Comware < Base
8
+ class Parser < Baseline::Parser
7
9
  private
8
10
 
9
11
  # 解析管理访问暴露检查使用的有序 ACL 定义。
10
12
  def access_lists
11
- sections(/\Aacl\s+/).each do |section|
13
+ matching_blocks(/\Aacl\s+/).each do |section|
12
14
  # IPv6 ACL 不能误建为名为 ipv6 的 IPv4 ACL,也不能覆盖其别名。
13
15
  if /\Aacl\s+ipv6\b/.match?(section.text)
14
16
  unknown("ipv6.acls", [ section ], code: :unsupported_acl_family)
@@ -30,13 +32,13 @@ module Baseline
30
32
  protocol = %w[ip tcp udp icmp].include?(parts.first) ? parts.shift : "ip"
31
33
  network = if parts.first == "source"
32
34
  parts.shift
33
- source(parts)
35
+ acl_source_network(parts)
34
36
  else
35
37
  "0.0.0.0/0"
36
38
  end
37
39
  if parts.first == "destination"
38
40
  parts.shift
39
- return unless source(parts) == "0.0.0.0/0"
41
+ return unless acl_source_network(parts) == "0.0.0.0/0"
40
42
  end
41
43
  if parts.first == "destination-port"
42
44
  return unless %w[tcp udp].include?(protocol)
@@ -54,56 +56,56 @@ module Baseline
54
56
 
55
57
  # 根据监听服务、认证方式与 ACL 绑定关系归纳管理访问事实。
56
58
  def management
57
- record("ssh.enabled", state(ssh_enable, ssh_disable), matching(/\A(?:undo )?(?:ssh|stelnet)/))
58
- compatible = state(/\Assh server compatible-ssh1x enable\z/, /\A(?:undo ssh server compatible-ssh1x enable|ssh server compatible-ssh1x disable)\z/)
59
+ record("ssh.enabled", configured_state(ssh_enable, ssh_disable), matching_blocks(/\A(?:undo )?(?:ssh|stelnet)/))
60
+ compatible = configured_state(/\Assh server compatible-ssh1x enable\z/, /\A(?:undo ssh server compatible-ssh1x enable|ssh server compatible-ssh1x disable)\z/)
59
61
  record("ssh.versions", compatible.nil? ? nil : compatible ? [ "1", "2" ] : [ "2" ])
60
- ssh_acl = last(/\Assh server acl(?: ipv4)?\s+(?:name\s+)?(\S+)\z/)
61
- defaults = sections(/\Aline class vty\z/).flat_map(&:children)
62
- vtys = sections(/\A(?:line|user-interface) vty\b/).map do |section|
62
+ ssh_acl = last_configured_value(/\Assh server acl(?: ipv4)?\s+(?:name\s+)?(\S+)\z/)
63
+ defaults = matching_blocks(/\Aline class vty\z/).flat_map(&:children)
64
+ vtys = matching_blocks(/\A(?:line|user-interface) vty\b/).map do |section|
63
65
  entries = defaults + section.children
64
- { name: section.text, protocols: last(/\Aprotocol inbound\s+(.+)\z/, entries)&.split,
65
- authentication: last(/\Aauthentication-mode\s+(\S+)\z/, entries),
66
- acl: last(/\Aacl\s+(?:name\s+)?(\S+)(?:\s+inbound)?\z/, entries), line: section.line }
66
+ { name: section.text, protocols: last_configured_value(/\Aprotocol inbound\s+(.+)\z/, entries)&.split,
67
+ authentication: last_configured_value(/\Aauthentication-mode\s+(\S+)\z/, entries),
68
+ acl: last_configured_value(/\Aacl\s+(?:name\s+)?(\S+)(?:\s+inbound)?\z/, entries), line: section.line }
67
69
  end
68
70
  @facts.management(vtys: vtys, ssh_acl: ssh_acl,
69
71
  ssh_port: listener_port("ssh server port", default: 22, scope: "management.acl"),
70
72
  telnet_port: listener_port("telnet server port", default: 23, scope: "management.acl"))
71
73
  definitions = { "telnet" => "telnet server", "http" => "ip http", "https" => "ip https", "ftp" => "ftp server", "tftp" => "tftp server" }
72
74
  record("services", definitions.to_h do |name, command|
73
- [ name, state(/\A#{command} enable\z/, /\A(?:undo #{command} enable|#{command} disable)\z/) ]
75
+ [ name, configured_state(/\A#{command} enable\z/, /\A(?:undo #{command} enable|#{command} disable)\z/) ]
74
76
  end)
75
77
  end
76
78
 
77
79
  # 采集明确启用或禁用的管理服务。
78
80
  def services
79
- record("ntp.servers", values(/\Antp-service unicast-server\s+(\S+)/), matching(/\Antp-service unicast-server/))
80
- record("ntp.authentication", state(/\Antp-service authentication enable\z/, /\Aundo ntp-service authentication enable\z/))
81
- record("ntp.source", last(/\Antp-service source(?:-interface)?\s+(\S+)\z/))
81
+ record("ntp.servers", configured_values(/\Antp-service unicast-server\s+(\S+)/), matching_blocks(/\Antp-service unicast-server/))
82
+ record_configured_state("ntp.authentication", /\Antp-service authentication enable\z/, /\Aundo ntp-service authentication enable\z/)
83
+ record_configured_value("ntp.source", /\Antp-service source(?:-interface)?\s+(\S+)\z/)
82
84
  # 从日志服务器中排除 source 子命令,并保留评估所需的归纳事实。
83
- hosts = values(/\Ainfo-center loghost\s+(\S+)/) - [ "source" ]
84
- record("syslog.servers", hosts, matching(/\Ainfo-center loghost/))
85
- record("syslog.source", last(/\Ainfo-center loghost source\s+(\S+)\z/))
86
- enabled = state(/\Asnmp-agent(?:\s|\z)/, /\Aundo snmp-agent\z/)
85
+ hosts = configured_values(/\Ainfo-center loghost\s+(\S+)/) - [ "source" ]
86
+ record("syslog.servers", hosts, matching_blocks(/\Ainfo-center loghost/))
87
+ record_configured_value("syslog.source", /\Ainfo-center loghost source\s+(\S+)\z/)
88
+ enabled = configured_state(/\Asnmp-agent(?:\s|\z)/, /\Aundo snmp-agent\z/)
87
89
  record("snmp.enabled", enabled.nil? && @facts.complete? ? false : enabled)
88
- versions = last(/\Asnmp-agent sys-info version\s+(.+)\z/)&.split
90
+ versions = last_configured_value(/\Asnmp-agent sys-info version\s+(.+)\z/)&.split
89
91
  versions = %w[v1 v2c v3] if versions == [ "all" ]
90
92
  record("snmp.versions", versions)
91
- communities = matching(/\Asnmp-agent community\b/).filter_map do |entry|
93
+ communities = matching_blocks(/\Asnmp-agent community\b/).filter_map do |entry|
92
94
  match = /\Asnmp-agent community (read|write)\s+(?:(simple|cipher)\s+)?(\S+)(.*)\z/.match(entry.text)
93
95
  unless match
94
96
  unknown_snmp_access([ entry ])
95
97
  next
96
98
  end
97
- credential("snmp-community", "snmp", match[2] || "plain", match[3], entry)
99
+ record_credential("snmp-community", "snmp", match[2] || "plain", match[3], entry)
98
100
  { write: match[1] == "write", acl: match[4][/\bacl\s+(?:name\s+)?(\S+)/, 1], line: entry.line }
99
101
  end
100
- groups = snmp_groups(matching(/\Asnmp-agent group v3\s+/)) do |entry|
102
+ groups = snmp_groups(matching_blocks(/\Asnmp-agent group v3\s+/)) do |entry|
101
103
  settings = entry.text.sub(/\Asnmp-agent group v3\s+\S+\s*/, "")
102
104
  [ entry.text.split[3], { acl: entry.text[/\bacl\s+(?:name\s+)?(\S+)/, 1], line: entry.line, write: snmp_group_write(settings) } ]
103
105
  end
104
106
  record("snmp.groups", groups)
105
107
  users = {}
106
- matching(/\Asnmp-agent usm-user\b/).each do |entry|
108
+ matching_blocks(/\Asnmp-agent usm-user\b/).each do |entry|
107
109
  match = /\Asnmp-agent usm-user v3\s+(\S+)(?:\s+(.*))?\z/.match(entry.text)
108
110
  unless match
109
111
  unknown_snmp_access([ entry ], code: :unsupported_snmp_user)
@@ -136,15 +138,15 @@ module Baseline
136
138
 
137
139
  # 解析实际生效的认证方法及外部服务器引用。
138
140
  def authentication
139
- schemes = sections(tacacs_section).to_h do |section|
140
- server_types = matching(tacacs_server, section.children).group_by { |entry| entry.text.split[1] }
141
- .transform_values { |entries| values(tacacs_server, entries) }
141
+ schemes = matching_blocks(tacacs_section).to_h do |section|
142
+ server_types = matching_blocks(tacacs_server, section.children).group_by { |entry| entry.text.split[1] }
143
+ .transform_values { |entries| configured_values(tacacs_server, entries) }
142
144
  servers = server_types.values.flatten.uniq
143
145
  keys = section.children.filter_map do |entry|
144
146
  match = tacacs_key.match(entry.text)
145
147
  next unless match
146
148
  kind, format, secret = match.captures
147
- credential("tacacs:#{section.text.split.last}", "tacacs", format, secret, entry)
149
+ record_credential("tacacs:#{section.text.split.last}", "tacacs", format, secret, entry)
148
150
  kind
149
151
  end
150
152
  [ section.text.split.last, { servers: servers, server_types: server_types, key_types: keys.uniq, line: section.line } ]
@@ -154,8 +156,8 @@ module Baseline
154
156
  domains = authentication_domains
155
157
  methods = domains.flat_map { |entry| entry.children }
156
158
  %w[authentication authorization accounting].each do |kind|
157
- relevant = matching(/\A#{kind}\s+login\b/, methods)
158
- relevant = matching(/\A#{kind}\s+default\b/, methods) if relevant.empty?
159
+ relevant = matching_blocks(/\A#{kind}\s+login\b/, methods)
160
+ relevant = matching_blocks(/\A#{kind}\s+default\b/, methods) if relevant.empty?
159
161
  refs = relevant.filter_map { |entry| entry.text[/\A#{kind}\s+(?:login|default)\s+hwtacacs-scheme\s+(\S+)/, 1] }
160
162
  refs.concat(vrp_tacacs_references(kind, domains))
161
163
  key = kind == "authentication" ? "login" : kind
@@ -172,26 +174,26 @@ module Baseline
172
174
  end
173
175
  record("tacacs.keys_present", schemes.values.all? { |entry| tacacs_keys_complete?(entry[:key_types]) }) unless schemes.empty?
174
176
  accounts
175
- sections(/\A(?:line|user-interface)\b/).each do |section|
176
- matching(/\A(?:set authentication )?password\b/, section.children).each do |entry|
177
+ matching_blocks(/\A(?:line|user-interface)\b/).each do |section|
178
+ matching_blocks(/\A(?:set authentication )?password\b/, section.children).each do |entry|
177
179
  match = /\A(?:set authentication )?password\s+(simple|cipher|hash|irreversible-cipher)\s+(.+)\z/.match(entry.text)
178
- credential(section.text, "line", match ? match[1] : "unknown", match&.[](2), entry)
180
+ record_credential(section.text, "line", match ? match[1] : "unknown", match&.[](2), entry)
179
181
  end
180
182
  end
181
- matching(/\Asuper password\b/).each do |entry|
183
+ matching_blocks(/\Asuper password\b/).each do |entry|
182
184
  match = /\Asuper password(?: level \d+)?\s+(simple|cipher|hash)\s+(.+)\z/.match(entry.text)
183
- credential("super", "enable", match ? match[1] : "unknown", match&.[](2), entry)
185
+ record_credential("super", "enable", match ? match[1] : "unknown", match&.[](2), entry)
184
186
  end
185
187
  end
186
188
 
187
189
  # 解析认证域引用及其服务器组。
188
190
  def authentication_domains
189
- name = last(/\Adomain default enable\s+(\S+)\z/)
191
+ name = last_configured_value(/\Adomain default enable\s+(\S+)\z/)
190
192
  unless name
191
193
  %w[login authorization accounting].each { |kind| unknown("tacacs.#{kind}", code: :authentication_domain_unknown) }
192
194
  return []
193
195
  end
194
- sections(/\Adomain #{Regexp.escape(name)}\z/)
196
+ matching_blocks(/\Adomain #{Regexp.escape(name)}\z/)
195
197
  end
196
198
  # 解析 VRP 认证域引用,避免将 VRP 语法应用到 Comware。
197
199
  def vrp_tacacs_references(_kind, _domains) = []
@@ -200,15 +202,15 @@ module Baseline
200
202
 
201
203
  # 提取本地账号角色及凭据存储属性。
202
204
  def accounts
203
- sections(/\Alocal-user\s+/).each do |section|
205
+ matching_blocks(/\Alocal-user\s+/).each do |section|
204
206
  next if section.text.start_with?("local-user password-")
205
207
  name = section.text.split[1]
206
- passwords = matching(/\Apassword\s+/, section.children)
208
+ passwords = matching_blocks(/\Apassword\s+/, section.children)
207
209
  passwords.each do |entry|
208
210
  match = /\Apassword\s+(simple|cipher|hash|irreversible-cipher)\s+(.+)\z/.match(entry.text)
209
- credential(name, "local_user", match ? match[1] : "unknown", match ? match[2] : nil, entry)
211
+ record_credential(name, "local_user", match ? match[1] : "unknown", match ? match[2] : nil, entry)
210
212
  end
211
- credential(name, "local_user", "unknown", nil, section) if passwords.empty? && @facts.complete?
213
+ record_credential(name, "local_user", "unknown", nil, section) if passwords.empty? && @facts.complete?
212
214
  end
213
215
  end
214
216
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ctx_parser/immutable"
4
+
3
5
  module Baseline
4
6
  # 在归一化配置中区分输入完整性、已知事实与局部不支持的语法。
5
7
  class Configuration
@@ -8,17 +10,22 @@ module Baseline
8
10
  attr_reader :platform, :sha256, :status, :facts, :sources, :issues
9
11
 
10
12
  # 冻结配置事实及其平台、来源身份。
11
- def initialize(platform:, sha256:, result:)
13
+ def initialize(platform:, sha256:, result:, input_complete: result.status == :complete)
14
+ raise ArgumentError, "Input completeness must be boolean" unless input_complete == true || input_complete == false
15
+ @input_complete = input_complete
12
16
  @platform, @sha256, @status = platform.dup.freeze, sha256.dup.freeze, result.status
13
17
  record = result.records.first || {}
14
- @facts = Baseline.immutable(record.fetch("facts", {}))
15
- @sources = Baseline.immutable(record.fetch("sources", {}))
16
- @issues = Baseline.immutable(result.issues)
18
+ @facts = CtxParser::Immutable.copy(record.fetch("facts", {}))
19
+ @sources = CtxParser::Immutable.copy(record.fetch("sources", {}))
20
+ @issues = CtxParser::Immutable.copy(result.issues)
17
21
  freeze
18
22
  end
19
23
 
24
+ # 仅记录调用方是否确认完整接收;局部解析不支持仍会降低 complete?。
25
+ def input_complete? = @input_complete
26
+
20
27
  # 判断配置采集与解析是否均已完成。
21
- def complete? = status == :complete
28
+ def complete? = input_complete? && status == :complete
22
29
  # 读取已知值,同时保留事实缺失或未知的区别。
23
30
  def value(path) = facts[path]
24
31
 
@@ -41,7 +48,7 @@ module Baseline
41
48
 
42
49
  # 返回公开的数据结构,并保留状态与诊断信息。
43
50
  def to_h
44
- { platform: platform, sha256: sha256, status: status, facts: facts, sources: sources, issues: issues }
51
+ { platform: platform, sha256: sha256, status: status, input_complete: input_complete?, facts: facts, sources: sources, issues: issues }
45
52
  end
46
53
 
47
54
  # 展示对象身份与状态,避免泄露原始回显或凭据。
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ctx_parser/immutable"
4
+
3
5
  module Baseline
4
6
  # 仅在构造时分析凭据,不保留明文、密文或可枚举的摘要。
5
7
  class Credential
@@ -20,7 +22,7 @@ module Baseline
20
22
  values.merge!(length: text.length, character_classes: [ /[a-z]/, /[A-Z]/, /\d/, /[^a-zA-Z\d]/ ].count { |pattern| pattern.match?(text) },
21
23
  common: COMMON.include?(text.downcase) || text.downcase == owner.to_s.downcase || /\A(.)\1+\z/.match?(text))
22
24
  end
23
- @attributes = Baseline.immutable(values)
25
+ @attributes = CtxParser::Immutable.copy(values)
24
26
  freeze
25
27
  end
26
28
 
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "policy"
4
+ require_relative "report"
5
+
3
6
  module Baseline
4
7
  # 根据策略评估归一化事实;厂商语法差异由解析器负责。
5
8
  class Evaluator
@@ -12,6 +15,9 @@ module Baseline
12
15
  # 评估适用基线规则,分别报告合规结果与证据覆盖率。
13
16
  def call(configuration:, context: {})
14
17
  raise ArgumentError, "Invalid baseline context" unless context.is_a?(Hash)
18
+ unless context.keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) } && context.keys.map(&:to_s).uniq.size == context.size
19
+ raise ArgumentError, "Ambiguous baseline context"
20
+ end
15
21
  context = context.transform_keys(&:to_s).merge("vendor" => configuration.platform)
16
22
  unless (context.keys - Policy::SCOPES).empty? && context.values.all? { |value| value.is_a?(String) && !value.strip.empty? }
17
23
  raise ArgumentError, "Invalid baseline context"
@@ -43,7 +49,7 @@ module Baseline
43
49
  attributes[:actual] = actual
44
50
  status, reason = compare(rule.operator, actual, expected)
45
51
  # 不完整输入不能证明通过;仅保留补充记录也无法推翻的明确违规。
46
- if !configuration.complete? && (status == :pass || status == :fail && !partial_failure?(rule, actual, expected))
52
+ if !configuration.complete? && (status == :pass || status == :fail && !conclusive_violation?(rule, actual, expected, configuration))
47
53
  status, reason = :unknown, :input_incomplete
48
54
  end
49
55
  if status == :fail
@@ -55,9 +61,10 @@ module Baseline
55
61
  end
56
62
 
57
63
  # 采集尚未完成时,仅报告后续缺失记录也无法推翻的明确违规。
58
- def partial_failure?(rule, actual, expected)
64
+ def conclusive_violation?(rule, actual, expected, configuration)
59
65
  case rule.operator
60
- when :all_disabled, :subset, :networks_within, :password_strength then true
66
+ when :all_disabled, :subset, :password_strength then true
67
+ when :networks_within then configuration.input_complete?
61
68
  when :set_equals then (actual - expected).any?
62
69
  when :equals
63
70
  rule.fact == "snmp.write_enabled" && actual == true && expected == false ||
@@ -81,7 +88,7 @@ module Baseline
81
88
  when :set_equals then { missing: expected - actual, unexpected: actual - expected }
82
89
  when :subset then { unexpected: actual - expected }
83
90
  when :count_at_least then { minimum: expected, actual_count: actual.size }
84
- when :networks_within then { outside_approved: actual.reject { |network| Acl.within?([ network ], expected) } }
91
+ when :networks_within then { outside_approved: Acl.unapproved_networks(actual, expected) }
85
92
  when :all_disabled then { enabled: expected.select { |name| actual[name] == true } }
86
93
  else {}
87
94
  end
@@ -128,7 +135,7 @@ module Baseline
128
135
  when :subset then (actual - expected).empty?
129
136
  when :set_equals then actual.sort == expected.sort
130
137
  when :count_at_least then actual.size >= expected
131
- when :networks_within then Acl.within?(actual, expected)
138
+ when :networks_within then Acl.networks_within?(actual, expected)
132
139
  end
133
140
  end
134
141
  [ passed ? :pass : :fail, passed ? nil : :deviation ]
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "acl"
4
+ require_relative "credential"
5
+
3
6
  require "ctx_parser/result"
4
7
 
5
8
  module Baseline
@@ -13,6 +16,7 @@ module Baseline
13
16
  @complete = complete && issues.empty?
14
17
  @values, @sources, @issues, @acls = {}, {}, issues.dup, {}
15
18
  @vtys, @communities, @snmp_users, @credentials = [], [], [], []
19
+ @credential_lines = {}
16
20
  @snmp_groups = {}
17
21
  @snmp_permission_issues = []
18
22
  @ssh_acl = nil
@@ -36,7 +40,7 @@ module Baseline
36
40
  (sources.nil? || sources.is_a?(Array))
37
41
  raise ArgumentError, "Direct access requires a known scope, binding state and source networks"
38
42
  end
39
- networks = sources && Acl.networks(sources.map { |source| Acl.range(source) })
43
+ networks = sources && Acl.network_prefixes(sources.map { |source| Acl.address_range(source) })
40
44
  @direct_acls[fact] = true
41
45
  record("#{fact}.bound", bound, lines: lines)
42
46
  if networks
@@ -51,7 +55,7 @@ module Baseline
51
55
  def record(path, value, lines: [])
52
56
  return false if value.nil? || (!complete? && value.respond_to?(:empty?) && value.empty?)
53
57
 
54
- @values[path] = Baseline.immutable(value)
58
+ @values[path] = CtxParser::Immutable.copy(value)
55
59
  @sources[path] = lines.uniq unless lines.empty?
56
60
  true
57
61
  end
@@ -72,15 +76,15 @@ module Baseline
72
76
 
73
77
  # 根据监听服务、认证方式与 ACL 绑定关系归纳管理访问事实。
74
78
  def management(vtys:, ssh_acl: nil, ssh_port: 22, telnet_port: 23)
75
- @vtys, @ssh_acl = Baseline.immutable(vtys), ssh_acl
79
+ @vtys, @ssh_acl = CtxParser::Immutable.copy(vtys), CtxParser::Immutable.copy(ssh_acl)
76
80
  @management_ports = { "ssh" => ssh_port, "stelnet" => ssh_port, "telnet" => telnet_port }
77
81
  end
78
82
 
79
83
  # 根据用户、组和团体字归纳 SNMP 暴露面及凭据事实。
80
84
  def snmp(communities:, users:, groups: nil, port: 161)
81
85
  @snmp_port = port
82
- @communities = Baseline.immutable(communities)
83
- @snmp_groups = Baseline.immutable(groups || {})
86
+ @communities = CtxParser::Immutable.copy(communities)
87
+ @snmp_groups = CtxParser::Immutable.copy(groups || {})
84
88
  @snmp_users = users.map do |user|
85
89
  if groups && user[:group]
86
90
  if groups.key?(user[:group])
@@ -94,7 +98,7 @@ module Baseline
94
98
  unknown("snmp.acl", code: :snmp_group_unresolved)
95
99
  end
96
100
  end
97
- Baseline.immutable(user)
101
+ CtxParser::Immutable.copy(user)
98
102
  end
99
103
  end
100
104
 
@@ -112,12 +116,14 @@ module Baseline
112
116
  end
113
117
 
114
118
  # 检查此来源行是否已经产生凭据事实。
115
- def credential_recorded?(line) = @credentials.any? { |entry| entry[:line] == line }
119
+ def credential_recorded?(line) = @credential_lines.key?(line)
116
120
 
117
121
  # 记录凭据属性,不保留凭据秘密值。
118
- def credential(credential)
122
+ def record_credential(credential)
119
123
  index = @credentials.size
120
- @credentials << credential.to_h
124
+ attributes = credential.to_h
125
+ @credentials << attributes
126
+ @credential_lines[attributes[:line]] = true
121
127
  index
122
128
  end
123
129
 
@@ -131,7 +137,7 @@ module Baseline
131
137
  @issues << { code: :input_not_confirmed_complete } unless @confirmed_complete
132
138
  status = complete? && @values.key?("hostname") ? :complete : :partial
133
139
  CtxParser::Result.new(status: status,
134
- records: Baseline.immutable([ { "facts" => @values, "sources" => @sources } ]), issues: Baseline.immutable(@issues.uniq))
140
+ records: CtxParser::Immutable.copy([ { "facts" => @values, "sources" => @sources } ]), issues: CtxParser::Immutable.copy(@issues.uniq))
135
141
  end
136
142
 
137
143
  # 展示对象身份与状态,避免泄露原始回显或凭据。
@@ -212,7 +218,7 @@ module Baseline
212
218
  acl = @acls[name]
213
219
  duplicate = @issues.any? { |issue| issue[:fact] == "acls.#{name}" }
214
220
  next if !acl || duplicate
215
- sources = (binding[:ports] || [ nil ]).map { |port| acl.sources(protocol: protocol, port: port) }
221
+ sources = (binding[:ports] || [ nil ]).map { |port| acl.permitted_source_networks(protocol: protocol, port: port) }
216
222
  sources.flatten unless sources.any?(&:nil?)
217
223
  end
218
224
  if resolved.any?(&:nil?)
@@ -220,7 +226,7 @@ module Baseline
220
226
  unknown("#{fact}.restricted", code: :acl_unresolved)
221
227
  return
222
228
  end
223
- sources = Acl.networks(resolved.flatten.uniq.map { |cidr| Acl.range(cidr) })
229
+ sources = Acl.network_prefixes(resolved.flatten.uniq.map { |cidr| Acl.address_range(cidr) })
224
230
  record("#{fact}.sources", sources)
225
231
  record("#{fact}.restricted", !sources.include?("0.0.0.0/0"))
226
232
  lines = names.uniq.flat_map { |name| @acls.fetch(name).rules.map { |rule| rule[:line] } }.compact.uniq
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../comware/parser"
4
+
3
5
  module Baseline
4
- module Parsers
6
+ module H3c
5
7
  # 解析 H3C Comware 的 SSH、域认证及三类 TACACS 密钥角色。
6
- class H3c < Comware
8
+ class Parser < Comware::Parser
7
9
  private
8
10
 
9
11
  # 匹配当前平台显式启用 SSH 的命令。
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../quoted_document"
4
+
5
+ module Baseline
6
+ module Hillstone
7
+ # StoneOS 导出内容可能包含命令回显、显式退出及引号包裹的界面状态。
8
+ class Document < QuotedDocument
9
+ protected
10
+
11
+ # 区分配置结构行与引号、横幅中的文本内容。
12
+ def classify(line)
13
+ unless @quote
14
+ return :ignored if line.text.match?(/\A(?:show (?:configuration|config)|Running configuration|Version\b|[\w.():-]+#\s*\z)/)
15
+ return :ignored if line.text == "exit"
16
+ return :boundary if line.text == "End"
17
+ end
18
+ super
19
+ end
20
+
21
+ # 识别配置值可能跨行延续的语句。
22
+ def multiline_fact?(text)
23
+ text.match?(/\A(?:hostname|admin host|snmp-server|ntp|logging syslog|password|secret|host|backup[12])\b/)
24
+ end
25
+ end
26
+ end
27
+ end