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
|
@@ -4,11 +4,22 @@ require_relative "../parser"
|
|
|
4
4
|
|
|
5
5
|
require "shellwords"
|
|
6
6
|
require_relative "document"
|
|
7
|
+
require_relative "command_index"
|
|
7
8
|
|
|
8
9
|
module Baseline
|
|
9
10
|
module PaloAlto
|
|
10
11
|
# 将 PAN-OS set 语法归纳为与配置块平台一致的事实。
|
|
11
12
|
class Parser < Baseline::Parser
|
|
13
|
+
# 原生管理端点没有 VTY 模型;未建模能力保留未知,不改变规则适用范围。
|
|
14
|
+
UNSUPPORTED_FACTS = {
|
|
15
|
+
"ssh.versions" => :ssh_version_unmodeled,
|
|
16
|
+
"ssh.vty_ssh_only" => :native_management_endpoint,
|
|
17
|
+
"ssh.vty_aaa" => :native_management_endpoint,
|
|
18
|
+
"tacacs.authorization" => :platform_semantics_unresolved,
|
|
19
|
+
"tacacs.accounting" => :platform_semantics_unresolved
|
|
20
|
+
}.freeze
|
|
21
|
+
PARTIAL_FACTS = { "services" => :service_states_partially_modeled }.freeze
|
|
22
|
+
|
|
12
23
|
MANAGEMENT_SERVICES = %w[ssh telnet http https].freeze
|
|
13
24
|
RELEVANT = /\Aset (?:deviceconfig system |network (?:profiles interface-management-profile |interface )|shared (?:server-profile tacplus |authentication-profile |authentication-sequence |log-settings syslog )|mgt-config users )/
|
|
14
25
|
|
|
@@ -18,25 +29,31 @@ module Baseline
|
|
|
18
29
|
def document_class = Document
|
|
19
30
|
# 匹配当前平台显式声明的主机名。
|
|
20
31
|
def hostname_pattern = /\Aset deviceconfig system hostname\s+(\S+)\z/
|
|
32
|
+
# set 语法使用独立的设备身份命令族。
|
|
33
|
+
def hostname_family_pattern = /\Aset deviceconfig system hostname(?:\s|\z)/
|
|
21
34
|
|
|
22
35
|
# 仅对管理配置子集分词一次,不处理无关的安全规则。
|
|
23
36
|
def prepare_configuration
|
|
24
|
-
|
|
37
|
+
statements = roots.filter_map do |entry|
|
|
25
38
|
next unless RELEVANT.match?(entry.text)
|
|
26
39
|
{ entry: entry, tokens: Shellwords.shellsplit(entry.text).drop(1) }
|
|
27
40
|
rescue ArgumentError
|
|
28
|
-
@
|
|
41
|
+
@fact_builder.mark_incomplete!(code: :invalid_set_statement, lines: [ entry.line ])
|
|
29
42
|
nil
|
|
30
43
|
end
|
|
31
|
-
@
|
|
44
|
+
@command_index = CommandIndex.new(statements)
|
|
45
|
+
@interface_profiles = statements_with_prefix("network", "profiles", "interface-management-profile").group_by { |row| row[:tokens][3] }
|
|
32
46
|
@unresolved_profile_bindings = false
|
|
33
|
-
|
|
47
|
+
# 同一接口作用域只保留最后绑定,其他接口继续独立参与管理端点归约。
|
|
48
|
+
bindings = statements_with_prefix("network", "interface").select { |row| row[:tokens].include?("interface-management-profile") }
|
|
49
|
+
.group_by { |row| row[:tokens].take(row[:tokens].index("interface-management-profile")) }.values.map(&:last)
|
|
50
|
+
@profile_bindings = bindings.select do |row|
|
|
34
51
|
index = row[:tokens].index("interface-management-profile")
|
|
35
52
|
next false unless index
|
|
36
53
|
valid = row[:tokens].size == index + 2 && !row[:tokens].last.empty?
|
|
37
54
|
unless valid
|
|
38
55
|
@unresolved_profile_bindings = true
|
|
39
|
-
@
|
|
56
|
+
@fact_builder.mark_incomplete!(code: :invalid_management_profile_binding, lines: [ row[:entry].line ])
|
|
40
57
|
end
|
|
41
58
|
valid
|
|
42
59
|
end
|
|
@@ -44,20 +61,20 @@ module Baseline
|
|
|
44
61
|
end
|
|
45
62
|
|
|
46
63
|
# 覆盖所有已配置的管理端点,包括数据接口。
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
def parse_management
|
|
65
|
+
service_states = (MANAGEMENT_SERVICES + [ "snmp" ]).to_h { |name| [ name, service_state(name) ] }
|
|
66
|
+
record_fact("ssh.enabled", service_states["ssh"], service_entries([ "ssh" ]))
|
|
67
|
+
record_fact("services", service_states.slice("telnet", "http", "https"), service_entries(%w[telnet http https]))
|
|
68
|
+
record_fact("snmp.enabled", service_states["snmp"], service_entries([ "snmp" ]))
|
|
69
|
+
record_permitted_sources("management.acl", MANAGEMENT_SERVICES)
|
|
70
|
+
record_permitted_sources("snmp.acl", [ "snmp" ])
|
|
54
71
|
end
|
|
55
72
|
|
|
56
73
|
# MGT 服务标志缺失时保持未知;明确启用的端点可证明服务已启用。
|
|
57
74
|
def service_state(name)
|
|
58
|
-
disabled =
|
|
75
|
+
disabled = configured_boolean(%w[deviceconfig system service] + [ "disable-#{name}" ])
|
|
59
76
|
global = disabled.nil? ? nil : !disabled
|
|
60
|
-
values = @active_profiles.map { |profile|
|
|
77
|
+
values = @active_profiles.map { |profile| profile_service_state(profile, name) }
|
|
61
78
|
return true if global == true || values.include?(true)
|
|
62
79
|
return false if global == false && values.all?(false)
|
|
63
80
|
|
|
@@ -67,37 +84,37 @@ module Baseline
|
|
|
67
84
|
# 保留参与跨端点服务事实归纳的显式声明。
|
|
68
85
|
def service_entries(names)
|
|
69
86
|
names.flat_map do |name|
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
statements_with_prefix(*(%w[deviceconfig system service] + [ "disable-#{name}" ])) + @active_profiles.flat_map do |profile|
|
|
88
|
+
statements_with_prefix("network", "profiles", "interface-management-profile", profile, name)
|
|
72
89
|
end
|
|
73
90
|
end.map { |row| row[:entry] }
|
|
74
91
|
end
|
|
75
92
|
|
|
76
93
|
# 仅解释命名接口配置文件中显式声明的服务。
|
|
77
|
-
def
|
|
94
|
+
def profile_service_state(profile, name)
|
|
78
95
|
return unless @interface_profiles.key?(profile)
|
|
79
96
|
prefix = [ "network", "profiles", "interface-management-profile", profile, name ]
|
|
80
|
-
value =
|
|
97
|
+
value = configured_boolean(prefix)
|
|
81
98
|
return value unless value.nil?
|
|
82
|
-
return if
|
|
83
|
-
return false if value.nil? && @
|
|
99
|
+
return if statements_with_prefix(*prefix).any?
|
|
100
|
+
return false if value.nil? && @fact_builder.complete?
|
|
84
101
|
|
|
85
102
|
nil
|
|
86
103
|
end
|
|
87
104
|
|
|
88
105
|
# 端点的原生允许列表适用于该端点所有已启用服务。
|
|
89
|
-
def
|
|
90
|
-
global =
|
|
106
|
+
def record_permitted_sources(fact, service_names)
|
|
107
|
+
global = service_names.map { |name| configured_boolean(%w[deviceconfig system service] + [ "disable-#{name}" ]) }
|
|
91
108
|
prefixes = global.all?(true) ? [] : [ %w[deviceconfig system permitted-ip] ]
|
|
92
109
|
unresolved = @unresolved_profile_bindings
|
|
93
110
|
@active_profiles.each do |profile|
|
|
94
|
-
enabled =
|
|
111
|
+
enabled = service_names.map { |name| profile_service_state(profile, name) }
|
|
95
112
|
unresolved ||= enabled.include?(nil)
|
|
96
113
|
prefixes << [ "network", "profiles", "interface-management-profile", profile, "permitted-ip" ] if enabled.include?(true)
|
|
97
114
|
end
|
|
98
115
|
entries = []
|
|
99
116
|
sources = prefixes.flat_map do |prefix|
|
|
100
|
-
matches =
|
|
117
|
+
matches = statements_with_prefix(*prefix)
|
|
101
118
|
entries.concat(matches.map { |row| row[:entry] })
|
|
102
119
|
matches.empty? ? [ "0.0.0.0/0" ] : matches.map do |row|
|
|
103
120
|
tokens = row[:tokens].drop(prefix.size)
|
|
@@ -117,41 +134,34 @@ module Baseline
|
|
|
117
134
|
end
|
|
118
135
|
end.compact
|
|
119
136
|
bound = !sources.include?("0.0.0.0/0")
|
|
120
|
-
@
|
|
121
|
-
|
|
137
|
+
@fact_builder.record_source_restriction(fact: fact, sources: unresolved ? nil : sources, bound: bound, lines: entries.map(&:line))
|
|
138
|
+
record_unknown_fact("#{fact}.bound", code: :management_profile_unresolved) if unresolved
|
|
122
139
|
cursor.capture_many(entries.select { |entry| cursor.parsed?(entry.index) }, data: sources, path: [ "facts", "#{fact}.sources" ]) unless unresolved
|
|
123
140
|
end
|
|
124
141
|
|
|
125
|
-
# 采集明确启用或禁用的管理服务。
|
|
126
|
-
def services
|
|
127
|
-
ntp
|
|
128
|
-
syslog
|
|
129
|
-
snmp
|
|
130
|
-
end
|
|
131
|
-
|
|
132
142
|
# 认证要求必须覆盖全部已配置服务器,不能由任意一台满足来证明。
|
|
133
|
-
def
|
|
134
|
-
entries =
|
|
143
|
+
def parse_ntp
|
|
144
|
+
entries = statements_with_prefix("deviceconfig", "system", "ntp-servers")
|
|
135
145
|
servers = entries.group_by { |row| row[:tokens][3] }.filter_map do |name, definitions|
|
|
136
146
|
prefix = [ "deviceconfig", "system", "ntp-servers", name ]
|
|
137
|
-
address =
|
|
147
|
+
address = configured_server_address(prefix + [ "ntp-server-address" ])
|
|
138
148
|
unless address && %w[primary-ntp-server secondary-ntp-server].include?(name)
|
|
139
|
-
|
|
140
|
-
|
|
149
|
+
record_unknown_fact("ntp.servers", definitions.map { |row| row[:entry] }, code: :ntp_server_unresolved)
|
|
150
|
+
record_unknown_fact("ntp.authentication", definitions.map { |row| row[:entry] }, code: :ntp_server_unresolved)
|
|
141
151
|
next
|
|
142
152
|
end
|
|
143
|
-
{ address: address, authenticated:
|
|
153
|
+
{ address: address, authenticated: ntp_authentication_state(prefix) }
|
|
144
154
|
end
|
|
145
|
-
|
|
155
|
+
record_fact("ntp.servers", servers.map { |server| server[:address] }.uniq, entries.map { |row| row[:entry] })
|
|
146
156
|
if servers.any? && servers.none? { |server| server[:authenticated].nil? }
|
|
147
|
-
|
|
157
|
+
record_fact("ntp.authentication", servers.all? { |server| server[:authenticated] }, entries.map { |row| row[:entry] })
|
|
148
158
|
end
|
|
149
|
-
|
|
159
|
+
record_configured_scalar("ntp.source", %w[deviceconfig system route service ntp source interface])
|
|
150
160
|
end
|
|
151
161
|
|
|
152
162
|
# 仅完整的认证声明才能证明所有服务器均满足认证要求。
|
|
153
|
-
def
|
|
154
|
-
entries =
|
|
163
|
+
def ntp_authentication_state(prefix)
|
|
164
|
+
entries = statements_with_prefix(*(prefix + [ "authentication-type" ]))
|
|
155
165
|
options = entries.map { |row| row[:tokens].drop(prefix.size + 1) }.uniq
|
|
156
166
|
return if options.empty?
|
|
157
167
|
return false if options == [ [ "none" ] ]
|
|
@@ -162,128 +172,126 @@ module Baseline
|
|
|
162
172
|
key[3] == "authentication-key" && !key[4].empty? && id.size == 3 && /\A\d+\z/.match?(id[2]) && id[2].to_i.between?(1, 65_534)
|
|
163
173
|
return true
|
|
164
174
|
end
|
|
165
|
-
|
|
175
|
+
record_unknown_fact("ntp.authentication", entries.map { |row| row[:entry] }, code: :unsupported_ntp_authentication)
|
|
166
176
|
nil
|
|
167
177
|
end
|
|
168
178
|
|
|
169
179
|
# 配置的采集端地址与运行时日志转发成功是两类事实。
|
|
170
|
-
def
|
|
171
|
-
entries =
|
|
180
|
+
def parse_syslog
|
|
181
|
+
entries = statements_with_prefix("shared", "log-settings", "syslog").select { |row| row[:tokens][4] == "server" }
|
|
172
182
|
servers = entries.group_by { |row| [ row[:tokens][3], row[:tokens][5] ] }.values.filter_map do |definitions|
|
|
173
183
|
address = definitions.reverse.find { |row| row[:tokens][6] == "server" }
|
|
174
|
-
value = address &&
|
|
175
|
-
|
|
184
|
+
value = address && configured_server_address(address[:tokens].take(7))
|
|
185
|
+
record_unknown_fact("syslog.servers", definitions.map { |row| row[:entry] }, code: :syslog_server_unresolved) unless value
|
|
176
186
|
value
|
|
177
187
|
end
|
|
178
|
-
|
|
179
|
-
|
|
188
|
+
record_fact("syslog.servers", servers.uniq, entries.map { |row| row[:entry] })
|
|
189
|
+
record_configured_scalar("syslog.source", %w[deviceconfig system route service syslog source interface])
|
|
180
190
|
end
|
|
181
191
|
|
|
182
192
|
# 轮询用户、团体字与出站 SNMP Trap 配置相互独立。
|
|
183
|
-
def
|
|
193
|
+
def parse_snmp
|
|
184
194
|
prefix = %w[deviceconfig system snmp-setting access-setting version]
|
|
185
|
-
versions =
|
|
186
|
-
|
|
187
|
-
communities =
|
|
195
|
+
versions = statements_with_prefix(*prefix).map { |row| row[:tokens][prefix.size] }.compact.uniq
|
|
196
|
+
record_fact("snmp.versions", versions)
|
|
197
|
+
communities = statements_with_prefix(*(prefix + %w[v2c snmp-community-string])).filter_map do |row|
|
|
188
198
|
unless row[:tokens].size == prefix.size + 3 && !row[:tokens].last.empty?
|
|
189
|
-
@
|
|
199
|
+
@fact_builder.mark_incomplete!(code: :invalid_set_statement, lines: [ row[:entry].line ])
|
|
190
200
|
next
|
|
191
201
|
end
|
|
192
|
-
record_credential("snmp-community", "snmp",
|
|
202
|
+
record_credential("snmp-community", "snmp", credential_storage_format(row[:tokens].last), row[:tokens].last, row[:entry])
|
|
193
203
|
{ write: false, line: row[:entry].line }
|
|
194
204
|
end
|
|
195
205
|
users_prefix = prefix + %w[v3 users]
|
|
196
|
-
users =
|
|
206
|
+
users = statements_with_prefix(*users_prefix).group_by { |row| row[:tokens][users_prefix.size] }.filter_map do |name, entries|
|
|
197
207
|
next unless name
|
|
198
208
|
user = { name: name, auth: false, priv: false, write: false, line: entries.first[:entry].line }
|
|
199
209
|
{ "authpwd" => :auth, "privpwd" => :priv }.each do |field, key|
|
|
200
210
|
row = entries.reverse.find { |entry| entry[:tokens][users_prefix.size + 1] == field }
|
|
201
211
|
next unless row
|
|
202
212
|
unless row[:tokens].size == users_prefix.size + 3 && !row[:tokens].last.empty?
|
|
203
|
-
@
|
|
213
|
+
@fact_builder.mark_incomplete!(code: :invalid_set_statement, lines: [ row[:entry].line ])
|
|
204
214
|
next
|
|
205
215
|
end
|
|
206
216
|
user[key] = !row[:tokens].last.empty?
|
|
207
|
-
record_credential("snmp:#{name}", "snmp_user",
|
|
217
|
+
record_credential("snmp:#{name}", "snmp_user", credential_storage_format(row[:tokens].last), row[:tokens].last, row[:entry])
|
|
208
218
|
end
|
|
209
219
|
user
|
|
210
220
|
end
|
|
211
|
-
@
|
|
221
|
+
@fact_builder.record_snmp_access(communities: communities, users: users)
|
|
212
222
|
if (versions - %w[v2c v3]).any? || versions.size > 1
|
|
213
|
-
|
|
223
|
+
record_unknown_snmp_access(statements_with_prefix(*prefix).map { |row| row[:entry] }, code: :unsupported_snmp_version)
|
|
214
224
|
end
|
|
215
225
|
end
|
|
216
226
|
|
|
217
227
|
# 将选中的登录配置或有序认证序列解析为 TACACS 服务器。
|
|
218
|
-
def
|
|
219
|
-
server_rows =
|
|
228
|
+
def parse_authentication
|
|
229
|
+
server_rows = statements_with_prefix("shared", "server-profile", "tacplus")
|
|
220
230
|
schemes = server_rows.group_by { |row| row[:tokens][3] }.to_h do |name, entries|
|
|
221
231
|
servers = entries.select { |row| row[:tokens][4] == "server" }.group_by { |row| row[:tokens][5] }
|
|
222
232
|
addresses = servers.values.filter_map do |server|
|
|
223
233
|
address = server.reverse.find { |row| row[:tokens][6] == "address" }
|
|
224
|
-
value = address &&
|
|
225
|
-
|
|
234
|
+
value = address && configured_server_address(address[:tokens].take(7))
|
|
235
|
+
record_unknown_fact("tacacs.servers", server.map { |row| row[:entry] }, code: :tacacs_server_unresolved) unless value
|
|
226
236
|
value
|
|
227
237
|
end
|
|
228
|
-
|
|
238
|
+
record_unknown_fact("tacacs.servers", entries.map { |row| row[:entry] }, code: :tacacs_server_unresolved) if servers.empty?
|
|
229
239
|
keys = servers.values.map do |server|
|
|
230
240
|
secret = server.reverse.find { |row| row[:tokens][6] == "secret" }
|
|
231
|
-
value = secret &&
|
|
232
|
-
record_credential("tacacs:#{name}", "tacacs",
|
|
241
|
+
value = secret && configured_scalar(secret[:tokens].take(7))
|
|
242
|
+
record_credential("tacacs:#{name}", "tacacs", credential_storage_format(value), value, secret[:entry]) if value
|
|
233
243
|
!value.to_s.empty?
|
|
234
244
|
end
|
|
235
245
|
[ name, { servers: addresses, keys_present: keys.any? && keys.all?, line: entries.first[:entry].line } ]
|
|
236
246
|
end
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
selected =
|
|
241
|
-
login = selected ?
|
|
242
|
-
login.nil? ?
|
|
243
|
-
|
|
244
|
-
%w[authorization accounting].each { |kind| unknown("tacacs.#{kind}", code: :platform_semantics_unresolved) }
|
|
245
|
-
accounts
|
|
247
|
+
record_fact("tacacs.schemes", schemes)
|
|
248
|
+
record_fact("tacacs.servers", schemes.values.flat_map { |scheme| scheme[:servers] }.uniq)
|
|
249
|
+
record_fact("tacacs.keys_present", schemes.values.all? { |scheme| scheme[:keys_present] }) if schemes.any?
|
|
250
|
+
selected = configured_scalar(%w[deviceconfig system authentication-profile])
|
|
251
|
+
login = selected ? tacacs_login_state(selected, schemes) : false
|
|
252
|
+
login.nil? ? record_unknown_fact("tacacs.login", code: :tacacs_scheme_unresolved) : record_fact("tacacs.login", login)
|
|
253
|
+
parse_local_accounts
|
|
246
254
|
end
|
|
247
255
|
|
|
248
256
|
# 认证序列具有顺序;后备 TACACS 不能视为首选 TACACS 登录。
|
|
249
|
-
def
|
|
257
|
+
def tacacs_login_state(name, schemes)
|
|
250
258
|
prefix = [ "shared", "authentication-profile", name, "method" ]
|
|
251
|
-
methods =
|
|
259
|
+
methods = statements_with_prefix(*prefix)
|
|
252
260
|
if methods.empty?
|
|
253
|
-
sequence =
|
|
261
|
+
sequence = statements_with_prefix("shared", "authentication-sequence", name, "authentication-profiles").last
|
|
254
262
|
return unless sequence
|
|
255
263
|
names = sequence[:tokens].drop(4).reject { |token| %w[[ ]].include?(token) }
|
|
256
264
|
return if names.empty? || names.first == name
|
|
257
|
-
return
|
|
265
|
+
return authentication_profile_tacacs_state(names.first, schemes)
|
|
258
266
|
end
|
|
259
|
-
|
|
267
|
+
authentication_profile_tacacs_state(name, schemes)
|
|
260
268
|
end
|
|
261
269
|
|
|
262
270
|
# 选中的 TACACS 配置必须至少解析出一台具有地址的服务器。
|
|
263
|
-
def
|
|
271
|
+
def authentication_profile_tacacs_state(name, schemes)
|
|
264
272
|
prefix = [ "shared", "authentication-profile", name, "method" ]
|
|
265
|
-
methods =
|
|
273
|
+
methods = statements_with_prefix(*prefix)
|
|
266
274
|
return if methods.empty?
|
|
267
275
|
kinds = methods.map { |row| row[:tokens][4] }.uniq
|
|
268
276
|
return unless kinds.one?
|
|
269
277
|
return false if %w[none local-database radius ldap kerberos saml-idp cloud].include?(kinds.first)
|
|
270
278
|
return unless kinds.first == "tacplus"
|
|
271
|
-
target =
|
|
279
|
+
target = configured_scalar(prefix + %w[tacplus server-profile])
|
|
272
280
|
return unless target && schemes[target] && schemes[target][:servers].any?
|
|
273
281
|
|
|
274
282
|
true
|
|
275
283
|
end
|
|
276
284
|
|
|
277
285
|
# 仅存储凭据元数据,导出的加密值不进入报告。
|
|
278
|
-
def
|
|
279
|
-
|
|
286
|
+
def parse_local_accounts
|
|
287
|
+
statements_with_prefix("mgt-config", "users").group_by { |row| row[:tokens][2] }.each do |name, entries|
|
|
280
288
|
row = entries.reverse.find { |entry| entry[:tokens][3] == "phash" }
|
|
281
289
|
unless row
|
|
282
|
-
external_profile =
|
|
290
|
+
external_profile = configured_scalar([ "mgt-config", "users", name, "authentication-profile" ])
|
|
283
291
|
record_credential(name, "local_user", "unknown", nil, entries.first[:entry]) unless external_profile && !external_profile.empty?
|
|
284
292
|
next
|
|
285
293
|
end
|
|
286
|
-
secret =
|
|
294
|
+
secret = configured_scalar(row[:tokens].take(4))
|
|
287
295
|
format = if /\A\$(?:1|5|6)\$[^$]+\$[^$]+\z/.match?(secret.to_s)
|
|
288
296
|
secret.start_with?("$1$") ? "5" : "hash"
|
|
289
297
|
else
|
|
@@ -294,45 +302,45 @@ module Baseline
|
|
|
294
302
|
end
|
|
295
303
|
|
|
296
304
|
# 识别 PAN-OS 带标记的加密 set 值;其余情况保留明文风险。
|
|
297
|
-
def
|
|
305
|
+
def credential_storage_format(value) = value.start_with?("-AQ", "-AR") ? "cipher" : "plain"
|
|
298
306
|
|
|
299
307
|
# 按词元前缀选择归一化后的 PAN-OS set 命令行。
|
|
300
|
-
def
|
|
301
|
-
@
|
|
308
|
+
def statements_with_prefix(*prefix)
|
|
309
|
+
@command_index.matching_prefix(prefix)
|
|
302
310
|
end
|
|
303
311
|
|
|
304
312
|
# 地址仅接受 IP 或 DNS 名称,不接受不完整转义或任意文本。
|
|
305
|
-
def
|
|
306
|
-
value =
|
|
313
|
+
def configured_server_address(prefix)
|
|
314
|
+
value = configured_scalar(prefix)
|
|
307
315
|
return if value.nil?
|
|
308
316
|
return value if server_address?(value)
|
|
309
317
|
|
|
310
|
-
@
|
|
318
|
+
@fact_builder.mark_incomplete!(code: :invalid_server_address, lines: statements_with_prefix(*prefix).map { |row| row[:entry].line })
|
|
311
319
|
nil
|
|
312
320
|
end
|
|
313
321
|
|
|
314
322
|
# 显式标志格式错误时,不能用平台默认值替代。
|
|
315
|
-
def
|
|
316
|
-
value =
|
|
323
|
+
def configured_boolean(prefix)
|
|
324
|
+
value = configured_scalar(prefix)
|
|
317
325
|
return if value.nil?
|
|
318
326
|
return value == "yes" if %w[yes no].include?(value)
|
|
319
327
|
|
|
320
|
-
@
|
|
328
|
+
@fact_builder.mark_incomplete!(code: :invalid_set_statement, lines: statements_with_prefix(*prefix).map { |row| row[:entry].line })
|
|
321
329
|
nil
|
|
322
330
|
end
|
|
323
331
|
|
|
324
332
|
# 标量语法与事实来源共享同一条最终声明。
|
|
325
|
-
def
|
|
326
|
-
row =
|
|
327
|
-
|
|
333
|
+
def record_configured_scalar(path, prefix)
|
|
334
|
+
row = statements_with_prefix(*prefix).last
|
|
335
|
+
record_fact(path, configured_scalar(prefix), row ? [ row[:entry] ] : [])
|
|
328
336
|
end
|
|
329
337
|
|
|
330
338
|
# 严格校验词元数量,避免尾随内容使错误配置被误判为已知事实。
|
|
331
|
-
def
|
|
332
|
-
row =
|
|
339
|
+
def configured_scalar(prefix)
|
|
340
|
+
row = statements_with_prefix(*prefix).last
|
|
333
341
|
return unless row
|
|
334
342
|
unless row[:tokens].size == prefix.size + 1
|
|
335
|
-
@
|
|
343
|
+
@fact_builder.mark_incomplete!(code: :invalid_set_statement, lines: [ row[:entry].line ])
|
|
336
344
|
return
|
|
337
345
|
end
|
|
338
346
|
|