baseline-rb 0.2.0 → 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.
@@ -3,12 +3,24 @@
3
3
  require "ctx_parser/configuration"
4
4
  require "ctx_parser/input"
5
5
  require_relative "document"
6
- require_relative "facts"
6
+ require_relative "fact_builder"
7
7
  require_relative "credential"
8
8
 
9
9
  module Baseline
10
- # 共享输入与提取边界;CtxParser 跟踪来源,Facts 归纳领域证据。
10
+ # 共享输入与提取边界;CtxParser 跟踪来源,FactBuilder 归纳领域证据。
11
11
  class Parser < CtxParser::Configuration
12
+ # 只有明确声明的事实才属于适配器契约;新增规则不能默认获得实现能力。
13
+ SUPPORTED_FACTS = %w[
14
+ snmp.enabled snmp.versions snmp.write_enabled snmp.v3_auth_priv snmp.acl.bound snmp.acl.restricted snmp.acl.sources
15
+ ntp.servers ntp.authentication ntp.source syslog.servers syslog.source
16
+ ssh.enabled ssh.versions ssh.vty_ssh_only ssh.vty_aaa management.acl.bound management.acl.restricted management.acl.sources
17
+ services credentials.storage_safe credentials.records tacacs.servers tacacs.login tacacs.authorization tacacs.accounting tacacs.keys_present
18
+ ].freeze
19
+
20
+ # 声明尚未建模的事实;未知能力不能通过空值或默认值伪装成已支持。
21
+ UNSUPPORTED_FACTS = {}.freeze
22
+ PARTIAL_FACTS = {}.freeze
23
+
12
24
  SERVICE_PORTS = { "ssh" => 22, "telnet" => 23, "snmp" => 161, "snmptrap" => 162 }.freeze
13
25
  SNMP_OPTIONS = %w[auth priv authentication-mode privacy-mode acl access use-ipv4acl group local engineid engineID
14
26
  encrypted localizedkey localizedV2key remote udp-port].freeze
@@ -28,28 +40,38 @@ module Baseline
28
40
  issues << { code: :incomplete_final_line, line: cursor.lines.last.number }
29
41
  end
30
42
  @source_issues = issues
31
- @facts = Facts.new(complete: complete, issues: issues)
43
+ @fact_builder = FactBuilder.new(complete: complete, issues: issues)
32
44
  if roots.empty?
33
45
  return CtxParser::Result.new(status: :failed, issues: issues + [ { code: :empty_configuration } ])
34
46
  end
35
47
  prepare_configuration
36
- if (hostname = matching_blocks(hostname_pattern).last)
37
- name = analyze(hostname) { hostname.text[hostname_pattern, 1] }
38
- record("hostname", name)
39
- capture(hostname, name, [ "facts", "hostname" ])
48
+ if (hostname = matching_blocks(hostname_family_pattern).last)
49
+ name = effective_configured_value(hostname_pattern, entries: [ hostname ], family: hostname_family_pattern, fact: "hostname")
50
+ if name
51
+ record_fact("hostname", name)
52
+ capture(hostname, name, [ "facts", "hostname" ])
53
+ end
40
54
  end
41
- access_lists
42
- management
43
- services
44
- authentication
45
- @facts.result
55
+ parse_access_lists
56
+ parse_management
57
+ parse_services
58
+ parse_authentication
59
+ self.class::UNSUPPORTED_FACTS.each_key { |path| record_unknown_fact(path, code: :unsupported_capability) }
60
+ @fact_builder.build
61
+ end
62
+
63
+ # 以相同顺序编排各厂商的原子领域能力,语法提取由适配器实现。
64
+ def parse_services
65
+ parse_ntp
66
+ parse_syslog
67
+ parse_snmp
46
68
  end
47
69
 
48
70
  # 在领域解析前准备当前配置的词元和引用索引。
49
71
  def prepare_configuration; end
50
72
 
51
73
  # 不使用命名 ACL 的平台由原生管理来源声明提供事实。
52
- def access_lists; end
74
+ def parse_access_lists; end
53
75
 
54
76
  # 返回本次解析的根配置块。
55
77
  def roots = @blocks
@@ -57,6 +79,8 @@ module Baseline
57
79
  def document_class = Document
58
80
  # 匹配当前平台显式声明的主机名。
59
81
  def hostname_pattern = /\A(?:hostname|sysname)\s+(\S+)\z/
82
+ # 损坏的最终身份声明也属于同族,不能继续使用旧身份选择设备策略。
83
+ def hostname_family_pattern = /\A(?:hostname|sysname)(?:\s|\z)/
60
84
  # 保留原始文本,但不将可能截断的词元纳入事实构建树。
61
85
  def exclude_incomplete_final_line
62
86
  line = cursor.lines.last
@@ -90,6 +114,57 @@ module Baseline
90
114
  end.uniq
91
115
  end
92
116
 
117
+ # 地址语法在输入边界校验;损坏的服务器声明不能变成已知的空集或数量。
118
+ def configured_server_addresses(scope, pattern, entries, negate: nil)
119
+ values = {}
120
+ entries.each do |entry|
121
+ negative = negate && entry.text.start_with?("#{negate} ")
122
+ text = negative ? entry.text.delete_prefix("#{negate} ") : entry.text
123
+ match = pattern.match(text)
124
+ address = match && match[1]
125
+ unless server_address?(address)
126
+ record_unknown_fact(scope, [ entry ], code: :invalid_server_address)
127
+ next
128
+ end
129
+ identity = block_given? ? yield(text, match) : address
130
+ unless identity
131
+ record_unknown_fact(scope, [ entry ], code: :unsupported_server_options)
132
+ next
133
+ end
134
+ analyze(entry) { address }
135
+ negative ? values.delete(identity) : values[identity] = address
136
+ end
137
+ values.values.uniq
138
+ end
139
+
140
+ # 服务器可选项必须完整消费;未知参数、重复项及缺失值不能作为有效配置。
141
+ def parse_server_options(text, flags: [], integers: {}, names: [], interfaces: [])
142
+ parts, values = text.split, {}
143
+ keywords = flags + integers.keys + names + interfaces
144
+ until parts.empty?
145
+ key = parts.shift
146
+ return if values.key?(key) || !keywords.include?(key)
147
+ if flags.include?(key)
148
+ values[key] = true
149
+ next
150
+ end
151
+ value = parts.shift
152
+ return if value.nil? || keywords.include?(value)
153
+ if integers.key?(key)
154
+ return unless /\A\d+\z/.match?(value) && integers.fetch(key).cover?(value.to_i)
155
+ value = value.to_i
156
+ elsif interfaces.include?(key)
157
+ return unless /\A[a-zA-Z][\w.:\/-]*\z/.match?(value)
158
+ value += parts.shift if !/\d/.match?(value) && /\A\d[\d.:\/-]*\z/.match?(parts.first.to_s)
159
+ return unless /\d/.match?(value)
160
+ end
161
+ values[key] = value
162
+ end
163
+ return if values.key?("minpoll") && values.key?("maxpoll") && values["minpoll"] > values["maxpoll"]
164
+
165
+ values
166
+ end
167
+
93
168
  # 按配置顺序选取最后一条匹配声明。
94
169
  def last_configured_value(pattern, entries = roots)
95
170
  entry = entries.reverse.find { |candidate| !candidate.text[pattern, 1].nil? }
@@ -98,25 +173,51 @@ module Baseline
98
173
  analyze(entry) { entry.text[pattern, 1] }
99
174
  end
100
175
 
176
+ # 先选取最后一条同族声明,再校验语法,避免回落到已被撤销或损坏的旧值。
177
+ def effective_configured_value(pattern, entries: roots, family:, fact:, reset: nil)
178
+ entry = entries.reverse.find { |candidate| family.match?(candidate.text) }
179
+ return unless entry
180
+ return analyze(entry) { nil } if reset&.match?(entry.text)
181
+ match = pattern.match(entry.text)
182
+ return analyze(entry) { match[1] } if match
183
+
184
+ record_unknown_fact(fact, [ entry ])
185
+ nil
186
+ end
187
+
188
+ # 保存最后生效的标量声明及对应来源,不将被覆盖的声明作为当前证据。
189
+ def record_configured_value(path, pattern, entries = roots)
190
+ entry = entries.reverse.find { |candidate| !candidate.text[pattern, 1].nil? }
191
+ return unless entry
192
+
193
+ record_fact(path, analyze(entry) { entry.text[pattern, 1] }, [ entry ])
194
+ end
195
+
196
+ # 启用状态可能由正向或撤销声明决定,保留所有参与归约的来源。
197
+ def record_configured_state(path, on, off)
198
+ entries = roots.select { |entry| on.match?(entry.text) || off.match?(entry.text) }
199
+ record_fact(path, configured_state(on, off, entries), entries)
200
+ end
201
+
101
202
  # 保存归一化事实及其来源证据。
102
- def record(path, value, entries = [])
103
- return unless @facts.record(path, value, lines: entries.map(&:line))
203
+ def record_fact(path, value, entries = [])
204
+ return unless @fact_builder.record_fact(path, value, lines: entries.map(&:line))
104
205
 
105
206
  # 来源块中尚未解释的命令不能标记为解析成功。
106
207
  cursor.capture_many(entries.select { |entry| cursor.parsed?(entry.index) }, data: value, path: [ "facts", path ])
107
208
  end
108
209
 
109
210
  # 记录事实不可用的原因,并保留对应的来源行。
110
- def unknown(path, entries = [], code: :unsupported_syntax)
111
- @facts.unknown(path, lines: entries.map(&:line), code: code)
211
+ def record_unknown_fact(path, entries = [], code: :unsupported_syntax)
212
+ @fact_builder.record_unknown_fact(path, lines: entries.map(&:line), code: code)
112
213
  entries.each { |entry| cursor.mark(entry.index, state: :unsupported) }
113
214
  end
114
215
 
115
216
  # 未解析的访问声明不能抹除其他已确认的写权限。
116
- def unknown_snmp_access(entries, code: :unsupported_syntax)
117
- @facts.unknown_snmp_permission(lines: entries.map(&:line), code: code)
217
+ def record_unknown_snmp_access(entries, code: :unsupported_syntax)
218
+ @fact_builder.record_unknown_snmp_permission(lines: entries.map(&:line), code: code)
118
219
  %w[snmp.communities snmp.community_present snmp.users snmp.versions snmp.v3_auth_priv snmp.acl].each do |path|
119
- unknown(path, entries, code: code)
220
+ record_unknown_fact(path, entries, code: code)
120
221
  end
121
222
  end
122
223
 
@@ -136,7 +237,7 @@ module Baseline
136
237
  # 记录凭据属性,不保留凭据秘密值。
137
238
  def record_credential(owner, kind, format, secret, entry)
138
239
  value = analyze(entry) { Credential.new(owner: owner, kind: kind, format: format, secret: secret, line: entry.line) }
139
- index = @facts.record_credential(value)
240
+ index = @fact_builder.record_credential(value)
140
241
  capture(entry, value.to_h, [ "facts", "credentials.records", index ])
141
242
  end
142
243
 
@@ -156,7 +257,7 @@ module Baseline
156
257
  end
157
258
  acl = Acl.new(name: name, line: entries.first&.line, rules: parsed.compact,
158
259
  supported: supported && !damaged && parsed.none?(&:nil?))
159
- if @facts.register_acl(acl, aliases: aliases, lines: entries.map(&:line)).any?
260
+ if @fact_builder.register_acl(acl, aliases: aliases, lines: entries.map(&:line)).any?
160
261
  entries.each { |entry| cursor.mark(entry.index, state: :unsupported) }
161
262
  end
162
263
  end
@@ -172,16 +273,20 @@ module Baseline
172
273
  end
173
274
 
174
275
  # 归一化 ACL 来源地址;不推测不支持语法的含义。
175
- def acl_source_network(parts)
276
+ def parse_acl_source_network(parts)
176
277
  token = parts.shift
177
278
  return "0.0.0.0/0" if token == "any"
178
- token = parts.shift if token == "host"
279
+ if token == "host"
280
+ address = parts.shift
281
+ raise ArgumentError, "A host requires one IPv4 address" unless address&.match?(/\A\d+(?:\.\d+){3}\z/)
282
+ return Acl.source_network(address)
283
+ end
179
284
  wildcard = parts.shift if parts.first&.match?(/\A(?:\d+(?:\.\d+){3}|0)\z/)
180
285
  Acl.source_network(token, wildcard)
181
286
  end
182
287
 
183
288
  # 显式端口名称仅属于配置语法,不代表组织批准值。
184
- def service_port(token)
289
+ def parse_service_port(token)
185
290
  return SERVICE_PORTS[token] unless token&.match?(/\A\d+\z/)
186
291
  value = token.to_i
187
292
  value if value.between?(0, 65_535)
@@ -206,19 +311,19 @@ module Baseline
206
311
  entry = entries.last
207
312
  return analyze(entry) { default } if entry.text == "#{negate} #{command}"
208
313
  token = entry.text[/\A#{prefix}\s+(\d+)\z/, 1]
209
- port = service_port(token)
314
+ port = parse_service_port(token)
210
315
  return analyze(entry) { port } if port && port.positive?
211
- unknown_listener_port(scope, entries)
316
+ record_unknown_listener_port(scope, entries)
212
317
  nil
213
318
  end
214
319
 
215
320
  # 监听端口无法解析时,将依赖该端口的事实保留为未知。
216
- def unknown_listener_port(scope, entries)
217
- %w[sources restricted].each { |field| unknown("#{scope}.#{field}", entries, code: :unsupported_service_port) }
321
+ def record_unknown_listener_port(scope, entries)
322
+ %w[sources restricted].each { |field| record_unknown_fact("#{scope}.#{field}", entries, code: :unsupported_service_port) }
218
323
  end
219
324
 
220
325
  # 区分模式未声明、格式无效及包含完整算法与秘密值的声明。
221
- def snmp_secret(settings, keyword, algorithms:, optional_algorithm: false, aes_size: false)
326
+ def parse_snmp_secret(settings, keyword, algorithms:, optional_algorithm: false, aes_size: false)
222
327
  tokens = settings.split
223
328
  index = tokens.index(keyword)
224
329
  return false unless index
@@ -236,23 +341,23 @@ module Baseline
236
341
  end
237
342
 
238
343
  # 提取 SNMP 凭据属性,不持久化秘密值。
239
- def snmp_credentials(owner, modes, entry, format: nil)
344
+ def record_snmp_credentials(owner, modes, entry, format: nil)
240
345
  modes.each_value do |value|
241
346
  next if value == false
242
347
  stored_format, secret = value || [ "unknown", nil ]
243
348
  record_credential(owner, "snmp_user", stored_format || format || "unknown", secret, entry)
244
349
  end
245
- unknown("snmp.v3_auth_priv", [ entry ], code: :invalid_snmp_security) if modes.value?(nil)
350
+ record_unknown_fact("snmp.v3_auth_priv", [ entry ], code: :invalid_snmp_security) if modes.value?(nil)
246
351
  modes.transform_values { |value| value ? true : value }
247
352
  end
248
353
 
249
354
  # 同名组可出现在不同安全级别;后续只读声明不能消除已有写权限。
250
- def snmp_groups(entries)
355
+ def configured_snmp_groups(entries)
251
356
  entries.map { |entry| yield(entry) }.group_by(&:first).transform_values do |definitions|
252
357
  records = definitions.map(&:last)
253
358
  acls = records.map { |record| record[:acl] }.uniq
254
359
  { acl: acls.one? ? acls.first : nil, acl_ambiguous: acls.size > 1,
255
- write: Facts.write_permission(records.map { |record| record[:write] }),
360
+ write: FactBuilder.write_permission_state(records.map { |record| record[:write] }),
256
361
  line: records.first[:line], lines: records.map { |record| record[:line] } }
257
362
  end
258
363
  end
@@ -64,12 +64,12 @@ module Baseline
64
64
 
65
65
  # 解析上下文对应的策略值,并保留选择路径。
66
66
  def resolve(rule, context)
67
- resolution = resolve_with_source(rule, context)
67
+ resolution = resolve_with_path(rule, context)
68
68
  [ resolution.value, resolution.reason ]
69
69
  end
70
70
 
71
71
  # 保留相对于 expected 的命中范围、默认分支或未解析选择器路径。
72
- def resolve_with_source(rule, context)
72
+ def resolve_with_path(rule, context)
73
73
  value = rule.expected
74
74
  path = []
75
75
  while selector?(value)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "configuration"
3
+ require_relative "evidence"
4
4
 
5
5
  require "ctx_parser/immutable"
6
6
  require "json"
@@ -12,6 +12,7 @@ module Baseline
12
12
  input_incomplete: "配置不完整,需提供完整备份后重查", unsupported_platform: "尚未支持此平台的配置语法",
13
13
  scope_mismatch: "不在策略适用范围内", scope_unknown: "缺少判断适用范围或选择批准值所需的上下文",
14
14
  policy_missing: "未定义当前范围的批准值,请完善基线策略", fact_unavailable: "配置事实缺失或包含尚未支持的语法,请查看解析诊断",
15
+ unsupported_capability: "此厂商的当前适配器尚未建模该能力,不能据此声明合规",
15
16
  fact_type_mismatch: "解析事实类型与检查要求不一致", unnecessary_service_enabled: "存在策略要求关闭的服务",
16
17
  service_state_unknown: "配置未明确声明部分服务状态,不能推断厂商默认值", deviation: "实际配置偏离批准值",
17
18
  unsupported_address_family: "地址格式超出当前 IPv4 检查能力", weak_password: "发现未满足强度要求的明文凭据",
data/lib/baseline/rule.rb CHANGED
@@ -90,7 +90,7 @@ module Baseline
90
90
  when :string_set, :network_set then string_set?(value)
91
91
  end
92
92
  raise ArgumentError, "Invalid baseline expected value for #{id}" unless valid
93
- value.each { |network| Acl.address_range(network) } if value_type == :network_set
93
+ value.each { |network| Acl.ipv4_range(network) } if value_type == :network_set
94
94
  end
95
95
 
96
96
  # 展示对象身份与状态,避免泄露原始回显或凭据。
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Baseline
4
4
  # 公开包版本,供 RubyGems 元数据和运行时调用方共用。
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: baseline-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gatework contributors
@@ -13,16 +13,16 @@ dependencies:
13
13
  name: ctx_parser
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 0.2.0
18
+ version: '0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - "~>"
23
+ - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: 0.2.0
25
+ version: '0'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: digest
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -113,20 +113,21 @@ files:
113
113
  - lib/baseline/cisco_ios/parser.rb
114
114
  - lib/baseline/cisco_nxos/parser.rb
115
115
  - lib/baseline/comware/parser.rb
116
- - lib/baseline/configuration.rb
117
116
  - lib/baseline/credential.rb
118
117
  - lib/baseline/document.rb
119
118
  - lib/baseline/evaluator.rb
120
- - lib/baseline/facts.rb
119
+ - lib/baseline/evidence.rb
120
+ - lib/baseline/fact_builder.rb
121
121
  - lib/baseline/h3c/parser.rb
122
122
  - lib/baseline/hillstone/document.rb
123
123
  - lib/baseline/hillstone/parser.rb
124
124
  - lib/baseline/huawei/parser.rb
125
+ - lib/baseline/multiline_document.rb
126
+ - lib/baseline/palo_alto/command_index.rb
125
127
  - lib/baseline/palo_alto/document.rb
126
128
  - lib/baseline/palo_alto/parser.rb
127
129
  - lib/baseline/parser.rb
128
130
  - lib/baseline/policy.rb
129
- - lib/baseline/quoted_document.rb
130
131
  - lib/baseline/report.rb
131
132
  - lib/baseline/rule.rb
132
133
  - lib/baseline/version.rb