baseline-rb 0.2.1 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/README-zh.md +48 -4
- data/README.md +57 -4
- data/docs/architecture-zh.md +8 -8
- data/lib/baseline/acl.rb +6 -6
- data/lib/baseline/audit.rb +34 -5
- data/lib/baseline/cisco/parser.rb +196 -92
- data/lib/baseline/comware/parser.rb +100 -56
- data/lib/baseline/evaluator.rb +28 -13
- data/lib/baseline/{configuration.rb → evidence.rb} +6 -6
- data/lib/baseline/{facts.rb → fact_builder.rb} +78 -53
- data/lib/baseline/h3c/parser.rb +7 -5
- data/lib/baseline/hillstone/document.rb +2 -2
- data/lib/baseline/hillstone/parser.rb +124 -71
- data/lib/baseline/huawei/parser.rb +15 -13
- data/lib/baseline/{quoted_document.rb → multiline_document.rb} +1 -1
- data/lib/baseline/palo_alto/command_index.rb +48 -0
- data/lib/baseline/palo_alto/document.rb +3 -3
- data/lib/baseline/palo_alto/parser.rb +113 -105
- data/lib/baseline/parser.rb +135 -37
- data/lib/baseline/policy.rb +2 -2
- data/lib/baseline/report.rb +2 -1
- data/lib/baseline/rule.rb +1 -1
- data/lib/baseline/version.rb +1 -1
- metadata +9 -8
|
@@ -7,7 +7,7 @@ require "ctx_parser/result"
|
|
|
7
7
|
|
|
8
8
|
module Baseline
|
|
9
9
|
# 归并跨命令引用及未知状态,不依赖厂商语法或解析游标。
|
|
10
|
-
class
|
|
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
|
|
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
|
|
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.
|
|
44
|
+
networks = sources && Acl.cidrs_from_ranges(sources.map { |source| Acl.ipv4_range(source) })
|
|
44
45
|
@direct_acls[fact] = true
|
|
45
|
-
|
|
46
|
+
record_fact("#{fact}.bound", bound, lines: lines)
|
|
46
47
|
if networks
|
|
47
|
-
|
|
48
|
-
|
|
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|
|
|
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
|
|
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
|
-
|
|
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
|
|
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|
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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:
|
|
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
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
172
|
-
|
|
173
|
-
|
|
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.
|
|
178
|
-
|
|
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
|
-
|
|
202
|
+
record_unknown_fact("snmp.write_enabled", code: :snmp_permission_unresolved)
|
|
181
203
|
@issues.concat(@snmp_permission_issues)
|
|
182
204
|
end
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
211
|
+
record_acl_sources(bindings, protocol: "udp", fact: "snmp.acl") unless @direct_acls["snmp.acl"]
|
|
187
212
|
end
|
|
188
213
|
|
|
189
214
|
# 汇总凭据存储方式及可度量的强度属性。
|
|
190
|
-
def
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
237
|
+
record_fact("#{fact}.bound", false) if complete?
|
|
213
238
|
return
|
|
214
239
|
end
|
|
215
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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.
|
|
230
|
-
|
|
231
|
-
|
|
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
|
data/lib/baseline/h3c/parser.rb
CHANGED
|
@@ -9,15 +9,17 @@ module Baseline
|
|
|
9
9
|
private
|
|
10
10
|
|
|
11
11
|
# 匹配当前平台显式启用 SSH 的命令。
|
|
12
|
-
def
|
|
12
|
+
def ssh_enable_pattern = /\Assh server enable\z/
|
|
13
13
|
# 匹配当前平台显式禁用 SSH 的命令。
|
|
14
|
-
def
|
|
14
|
+
def ssh_disable_pattern = /\A(?:undo ssh server enable|ssh server disable)\z/
|
|
15
15
|
# 匹配当前平台 TACACS 配置块的根语句。
|
|
16
|
-
def
|
|
16
|
+
def tacacs_section_pattern = /\Ahwtacacs scheme\s+/
|
|
17
17
|
# 匹配认证、授权或计费服务器声明。
|
|
18
|
-
def
|
|
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
|
|
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 "../
|
|
3
|
+
require_relative "../multiline_document"
|
|
4
4
|
|
|
5
5
|
module Baseline
|
|
6
6
|
module Hillstone
|
|
7
7
|
# StoneOS 导出内容可能包含命令回显、显式退出及引号包裹的界面状态。
|
|
8
|
-
class Document <
|
|
8
|
+
class Document < MultilineDocument
|
|
9
9
|
protected
|
|
10
10
|
|
|
11
11
|
# 区分配置结构行与引号、横幅中的文本内容。
|