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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -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 +162 -88
- 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 +127 -36
- 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
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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 =
|
|
64
|
+
parts = command_tokens(entry)
|
|
56
65
|
raise ArgumentError unless parts.take(2) == %w[admin host] && HOST_PROTOCOLS.include?(parts.last)
|
|
57
|
-
analyze(entry) {
|
|
66
|
+
analyze(entry) { parse_host_networks(parts[2...-1]) }
|
|
58
67
|
rescue ArgumentError, IPAddr::Error
|
|
59
|
-
|
|
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 @
|
|
72
|
+
return unless @fact_builder.complete?
|
|
64
73
|
# 未显式配置受信任主机时,不能据此证明来源访问受限。
|
|
65
|
-
@
|
|
74
|
+
@fact_builder.record_source_restriction(fact: "management.acl", sources: nil, bound: false)
|
|
66
75
|
else
|
|
67
|
-
@
|
|
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
|
|
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 =
|
|
86
|
+
parts = command_tokens(entry)
|
|
85
87
|
unless server_address?(parts[2])
|
|
86
|
-
|
|
88
|
+
record_unknown_fact("ntp", [ entry ])
|
|
87
89
|
next
|
|
88
90
|
end
|
|
89
|
-
options =
|
|
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
|
-
|
|
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
|
-
|
|
99
|
+
record_fact("ntp.servers", servers.uniq, entries)
|
|
98
100
|
record_configured_state("ntp.authentication", /\Antp authentication\z/, /\Ano ntp authentication\z/)
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
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 =
|
|
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]) &&
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
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 =
|
|
139
|
+
parts = command_tokens(entry)
|
|
138
140
|
offset = parts.index("version")
|
|
139
141
|
raise ArgumentError unless offset && offset > 2
|
|
140
|
-
networks =
|
|
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",
|
|
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
|
-
|
|
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
|
|
219
|
+
def parse_authentication
|
|
167
220
|
definitions = matching_blocks(/\Aaaa-server\s+/).filter_map do |section|
|
|
168
|
-
parts =
|
|
221
|
+
parts = command_tokens(section)
|
|
169
222
|
unless parts.size == 4 && parts[2..] == %w[type tacacs+]
|
|
170
|
-
|
|
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 =
|
|
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
|
-
|
|
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|
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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 =
|
|
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
|
-
|
|
260
|
+
record_fact("tacacs.#{fact}", true, entries)
|
|
208
261
|
else
|
|
209
|
-
|
|
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 =
|
|
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|
|
|
269
|
+
passwords.each { |entry| record_stored_credential(owner, "local_user", entry) }
|
|
217
270
|
end
|
|
218
271
|
end
|
|
219
272
|
|
|
220
273
|
# 编码格式的存储语义未经确认时保持未知,不能猜测为明文。
|
|
221
|
-
def
|
|
274
|
+
def credential_storage_format(value) = value.start_with?("$") ? "unknown" : "plain"
|
|
222
275
|
|
|
223
276
|
# 仅记录已配置秘密值的存储属性。
|
|
224
|
-
def
|
|
225
|
-
parts =
|
|
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
|
-
@
|
|
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 : [
|
|
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
|
|
291
|
+
def command_tokens(entry)
|
|
239
292
|
@tokens[entry.index] ||= Shellwords.shellsplit(entry.text)
|
|
240
293
|
rescue ArgumentError
|
|
241
|
-
@
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
12
|
+
def ssh_enable_pattern = /\Astelnet server enable\z/
|
|
13
13
|
# 匹配当前平台显式禁用 SSH 的命令。
|
|
14
|
-
def
|
|
14
|
+
def ssh_disable_pattern = /\A(?:undo stelnet server enable|stelnet server disable)\z/
|
|
15
15
|
# 匹配当前平台 TACACS 配置块的根语句。
|
|
16
|
-
def
|
|
16
|
+
def tacacs_section_pattern = /\Ahwtacacs-server template\s+/
|
|
17
17
|
# 匹配认证、授权或计费服务器声明。
|
|
18
|
-
def
|
|
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
|
|
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
|
|
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|
|
|
33
|
+
%w[login authorization accounting].each { |kind| record_unknown_fact("tacacs.#{kind}", code: :authentication_domain_unknown) }
|
|
32
34
|
return []
|
|
33
35
|
end
|
|
34
|
-
|
|
36
|
+
aaa_configuration_entries.select { |entry| entry.text == "domain #{name}" }
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
# 解析 VRP 认证域引用,避免将 VRP 语法应用到 Comware。
|
|
38
|
-
def
|
|
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 =
|
|
45
|
+
definition = aaa_configuration_entries.find { |entry| entry.text == "#{kind}-scheme #{scheme}" }
|
|
44
46
|
unless definition
|
|
45
|
-
|
|
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
|
|
54
|
-
matching_blocks(/\Alocal-user\s+\S+\s+password\b/,
|
|
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)
|
|
@@ -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 "../
|
|
3
|
+
require_relative "../multiline_document"
|
|
4
4
|
|
|
5
5
|
module Baseline
|
|
6
6
|
module PaloAlto
|
|
7
7
|
# PAN-OS 的 set 导出可能包含引号包裹的多行证书与描述。
|
|
8
|
-
class Document <
|
|
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
|