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
|
@@ -12,32 +12,32 @@ module Baseline
|
|
|
12
12
|
private
|
|
13
13
|
|
|
14
14
|
# 解析管理访问暴露检查使用的有序 ACL 定义。
|
|
15
|
-
def
|
|
15
|
+
def parse_access_lists
|
|
16
16
|
matching_blocks(/\Aip access-list\s+/).each do |section|
|
|
17
17
|
name = section.text.split.last
|
|
18
18
|
extended = section.text.start_with?("ip access-list extended ") || nxos?
|
|
19
|
-
register_acl(name, section.children, source: section) { |entry, index|
|
|
19
|
+
register_acl(name, section.children, source: section) { |entry, index| parse_acl_rule(entry, index, extended: extended) }
|
|
20
20
|
end
|
|
21
21
|
matching_blocks(/\Aaccess-list\s+/).group_by { |entry| entry.text.split[1] }.each do |name, entries|
|
|
22
22
|
extended = name.to_i.between?(100, 199) || name.to_i.between?(2000, 2699)
|
|
23
|
-
register_acl(name, entries) { |entry, index|
|
|
23
|
+
register_acl(name, entries) { |entry, index| parse_acl_rule(entry, index, extended: extended, prefix: /\Aaccess-list\s+\S+\s+/) }
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# 解析 Cisco ACL 条目,保留规则顺序及地址语义。
|
|
28
|
-
def
|
|
28
|
+
def parse_acl_rule(entry, index, extended:, prefix: /\A/)
|
|
29
29
|
text = entry.text.sub(prefix, "")
|
|
30
30
|
match = /\A(?:(\d+)\s+)?(permit|deny)\s+(.+)\z/.match(text)
|
|
31
31
|
return unless match
|
|
32
32
|
parts = match[3].split
|
|
33
33
|
protocol = extended ? parts.shift : "ip"
|
|
34
34
|
return unless %w[ip tcp udp icmp].include?(protocol)
|
|
35
|
-
network =
|
|
36
|
-
return if extended &&
|
|
35
|
+
network = parse_acl_source_network(parts)
|
|
36
|
+
return if extended && parse_acl_source_network(parts) != "0.0.0.0/0"
|
|
37
37
|
if extended && parts.first == "eq"
|
|
38
38
|
return unless %w[tcp udp].include?(protocol)
|
|
39
39
|
parts.shift
|
|
40
|
-
port =
|
|
40
|
+
port = parse_service_port(parts.shift)
|
|
41
41
|
return if port.nil?
|
|
42
42
|
end
|
|
43
43
|
return unless (parts - %w[log log-input]).empty?
|
|
@@ -48,26 +48,29 @@ module Baseline
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
# 根据监听服务、认证方式与 ACL 绑定关系归纳管理访问事实。
|
|
51
|
-
def
|
|
52
|
-
version =
|
|
53
|
-
|
|
51
|
+
def parse_management
|
|
52
|
+
version = effective_configured_value(/\Aip ssh version\s+([12])\z/,
|
|
53
|
+
family: /\A(?:no )?ip ssh version\b/, fact: "ssh.versions", reset: /\Ano ip ssh version(?: [12])?\z/)
|
|
54
|
+
record_fact("ssh.versions", [ version ]) if version
|
|
54
55
|
ssh = nxos? ? configured_state(/\Afeature ssh\z/, /\Ano feature ssh\z/) : version ? true : nil
|
|
55
|
-
|
|
56
|
+
record_fact("ssh.enabled", ssh, matching_blocks(/\A(?:ip ssh|(?:no )?feature ssh)/))
|
|
56
57
|
default_aaa = nxos? || configured_state(/\Aaaa new-model\z/, /\Ano aaa new-model\z/) == true
|
|
57
58
|
vtys = matching_blocks(/\Aline vty(?:\s|\z)/).map do |section|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
{ name: section.text, protocols:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
acl:
|
|
59
|
+
login = configured_vty_login(section, default_aaa: default_aaa)
|
|
60
|
+
acl_entries = section.children.reject { |entry| /\A(?:no )?access-class\s+\S+\s+out(?:\s|\z)/.match?(entry.text) }
|
|
61
|
+
{ name: section.text, protocols: effective_configured_value(/\Atransport input\s+((?:all|none|ssh|telnet)(?:\s+(?:ssh|telnet))*)\z/, entries: section.children,
|
|
62
|
+
family: /\A(?:no )?transport input\b/, fact: "ssh.vty_ssh_only", reset: /\Ano transport input(?:\s+\S+)*\z/)&.split,
|
|
63
|
+
authentication: login[:authentication], authentication_list: login[:authentication_list],
|
|
64
|
+
acl: effective_configured_value(/\Aaccess-class\s+(\S+)\s+in(?:\s+vrf-also)?\z/, entries: acl_entries,
|
|
65
|
+
family: /\A(?:no )?access-class\b/, fact: "management.acl",
|
|
66
|
+
reset: /\Ano access-class(?:\s+\S+\s+in(?:\s+vrf-also)?)?\z/), line: section.line }
|
|
64
67
|
end
|
|
65
68
|
rotary = matching_blocks(/\A(?:no )?ip ssh port(?:\s|\z)/)
|
|
66
69
|
rotary.concat(matching_blocks(/\Aline vty(?:\s|\z)/).flat_map { |section| matching_blocks(/\A(?:no )?rotary(?:\s|\z)/, section.children) })
|
|
67
|
-
|
|
70
|
+
record_unknown_listener_port("management.acl", rotary) unless rotary.empty?
|
|
68
71
|
telnet_ports = matching_blocks(/\A(?:no )?(?:ip )?telnet(?: server)? port(?:\s|\z)/)
|
|
69
|
-
|
|
70
|
-
@
|
|
72
|
+
record_unknown_listener_port("management.acl", telnet_ports) unless telnet_ports.empty?
|
|
73
|
+
@fact_builder.record_management_access(vtys: vtys,
|
|
71
74
|
ssh_port: rotary.empty? ? listener_port("ssh port", default: 22, scope: "management.acl", negate: "no") : nil,
|
|
72
75
|
telnet_port: telnet_ports.empty? ? 23 : nil)
|
|
73
76
|
telnet = if nxos?
|
|
@@ -75,31 +78,80 @@ module Baseline
|
|
|
75
78
|
elsif vtys.any? && vtys.all? { |entry| entry[:protocols] }
|
|
76
79
|
vtys.any? { |entry| (entry[:protocols] & %w[all telnet]).any? }
|
|
77
80
|
end
|
|
78
|
-
|
|
81
|
+
record_fact("services", { "telnet" => telnet, "http" => configured_state(/\Aip http server\z/, /\Ano ip http server\z/),
|
|
79
82
|
"https" => configured_state(/\Aip http secure-server\z/, /\Ano ip http secure-server\z/),
|
|
80
83
|
"ftp" => configured_state(/\A(?:feature ftp-server|ftp-server enable)\z/, /\Ano (?:feature ftp-server|ftp-server enable)\z/),
|
|
81
84
|
"tftp" => configured_state(/\A(?:feature tftp-server|tftp-server\s+.+)\z/, /\Ano (?:feature tftp-server|tftp-server\s+.+)\z/),
|
|
82
85
|
"finger" => configured_state(/\A(?:ip finger|service finger)\z/, /\Ano (?:ip finger|service finger)\z/) })
|
|
83
86
|
end
|
|
84
87
|
|
|
85
|
-
#
|
|
86
|
-
def
|
|
87
|
-
|
|
88
|
+
# VTY 登录方式属于同一声明族,不能让先前的本地登录覆盖后续列表或撤销。
|
|
89
|
+
def configured_vty_login(section, default_aaa:)
|
|
90
|
+
entry = matching_blocks(/\A(?:no )?login\b/, section.children).last
|
|
91
|
+
fallback = { authentication: default_aaa ? "aaa" : nil, authentication_list: default_aaa ? "default" : nil }
|
|
92
|
+
return fallback unless entry
|
|
93
|
+
|
|
94
|
+
case entry.text
|
|
95
|
+
when "login local"
|
|
96
|
+
analyze(entry) { { authentication: "local", authentication_list: nil } }
|
|
97
|
+
when "login", "no login"
|
|
98
|
+
analyze(entry) { { authentication: "none", authentication_list: nil } }
|
|
99
|
+
when /\Alogin authentication\s+(\S+)\z/
|
|
100
|
+
analyze(entry) { fallback.merge(authentication_list: entry.text.split.last) }
|
|
101
|
+
else
|
|
102
|
+
record_unknown_fact("ssh.vty_aaa", [ entry ])
|
|
103
|
+
record_unknown_fact("tacacs.login", [ entry ])
|
|
104
|
+
{ authentication: nil, authentication_list: nil }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# 提取时间服务器、认证要求与源接口。
|
|
109
|
+
def parse_ntp
|
|
110
|
+
ntp_entries = matching_blocks(/\A(?:no )?ntp server(?:\s|\z)/)
|
|
111
|
+
ntp_pattern = /\Antp server\s+(?:vrf\s+\S+\s+)?(?:(?:ip|ipv6)\s+)?((?!(?:vrf|ip|ipv6)(?:\s|\z))\S+)/
|
|
112
|
+
servers = configured_server_addresses("ntp.servers", ntp_pattern, ntp_entries, negate: "no") do |text, match|
|
|
113
|
+
options = parse_server_options(match.post_match, flags: %w[prefer normal-sync burst iburst],
|
|
114
|
+
integers: { "version" => 1..4, "key" => 1..65_535, "minpoll" => 4..17, "maxpoll" => 4..17 },
|
|
115
|
+
names: [ "use-vrf" ], interfaces: [ "source" ])
|
|
116
|
+
vrf = text[/\Antp server\s+vrf\s+(\S+)/, 1]
|
|
117
|
+
[ match[1], vrf || options["use-vrf"] ] if options && !(vrf && options.key?("use-vrf"))
|
|
118
|
+
end
|
|
119
|
+
record_fact("ntp.servers", servers, ntp_entries)
|
|
88
120
|
record_configured_state("ntp.authentication", /\Antp authenticate\z/, /\Ano ntp authenticate\z/)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
121
|
+
source_entries = matching_blocks(/\A(?:no )?ntp source(?:-interface)?\b/)
|
|
122
|
+
source = effective_configured_value(/\Antp source(?:-interface)?\s+(\S+)\z/, entries: source_entries,
|
|
123
|
+
family: /\A/, fact: "ntp.source", reset: /\Ano ntp source(?:-interface)?(?:\s+\S+)?\z/)
|
|
124
|
+
record_fact("ntp.source", source, source_entries.last(1))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# 提取日志目标与源接口,厂商命令不进入事实归约层。
|
|
128
|
+
def parse_syslog
|
|
129
|
+
log_entries = matching_blocks(nxos? ? /\A(?:no )?logging server(?:\s|\z)/ : /\A(?:no )?logging host(?:\s|\z)/)
|
|
130
|
+
log_pattern = nxos? ? /\Alogging server\s+(\S+)/ : /\Alogging host\s+(?:ipv6\s+)?((?!ipv6(?:\s|\z))\S+)/
|
|
131
|
+
unless nxos?
|
|
132
|
+
log_entries += matching_blocks(/\A(?:no )?logging\s+\d+(?:\.\d+){3}\z/)
|
|
133
|
+
log_entries.sort_by!(&:index)
|
|
134
|
+
log_pattern = /\Alogging\s+(?:host\s+(?:ipv6\s+)?)?((?!(?:host|ipv6)(?:\s|\z))\S+)/
|
|
135
|
+
end
|
|
136
|
+
hosts = configured_server_addresses("syslog.servers", log_pattern, log_entries, negate: "no")
|
|
137
|
+
record_fact("syslog.servers", hosts, log_entries)
|
|
138
|
+
source_entries = matching_blocks(/\A(?:no )?logging source-interface\b/)
|
|
139
|
+
source = effective_configured_value(/\Alogging source-interface\s+(\S+)\z/, entries: source_entries,
|
|
140
|
+
family: /\A/, fact: "syslog.source", reset: /\Ano logging source-interface(?:\s+\S+)?\z/)
|
|
141
|
+
record_fact("syslog.source", source, source_entries.last(1))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# 提取轮询权限、用户安全属性及来源限制。
|
|
145
|
+
def parse_snmp
|
|
146
|
+
snmp_entries = matching_blocks(/\Asnmp-server\b/)
|
|
95
147
|
enabled = nxos? ? configured_state(/\Asnmp-server protocol enable\z/, /\Ano snmp-server protocol enable\z/) : nil
|
|
96
|
-
enabled = nxos? ||
|
|
97
|
-
|
|
98
|
-
communities = {}
|
|
99
|
-
|
|
148
|
+
enabled = nxos? || snmp_entries.any? if enabled.nil? && (@fact_builder.complete? || snmp_entries.any?)
|
|
149
|
+
record_fact("snmp.enabled", enabled)
|
|
150
|
+
communities, community_acls = {}, {}
|
|
151
|
+
configured_community_entries.each do |entry|
|
|
100
152
|
match = /\Asnmp-server community\s+(\S+)(?:\s+(.*))?\z/.match(entry.text)
|
|
101
153
|
unless match
|
|
102
|
-
|
|
154
|
+
record_unknown_snmp_access([ entry ])
|
|
103
155
|
next
|
|
104
156
|
end
|
|
105
157
|
settings = match[2].to_s
|
|
@@ -110,20 +162,32 @@ module Baseline
|
|
|
110
162
|
community[:write] = SNMP_ROLE_WRITE[role] if role
|
|
111
163
|
acl = nxos? ? settings[/\buse-ipv4acl\s+(\S+)/, 1] : settings[/\A(?:ro|rw)\s+(\S+)\z/i, 1]
|
|
112
164
|
community[:acl] = acl if acl
|
|
113
|
-
|
|
165
|
+
unless nxos?
|
|
166
|
+
access = (community_acls[match[1]] ||= { value: acl, conflicted: false })
|
|
167
|
+
access[:conflicted] ||= access[:value] != acl
|
|
168
|
+
if access[:conflicted]
|
|
169
|
+
community.delete(:acl)
|
|
170
|
+
record_unknown_fact("snmp.acl", [ entry ], code: :conflicting_community_acl)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
record_credential("snmp-community", "snmp", "plain", match[1], entry) unless @fact_builder.credential_recorded?(community[:line])
|
|
114
174
|
end
|
|
115
175
|
versions = communities.any? ? %w[v1 v2c] : []
|
|
116
|
-
|
|
176
|
+
unsupported_groups = matching_blocks(/\Asnmp-server group\b/).reject do |entry|
|
|
177
|
+
/\Asnmp-server group\s+\S+\s+v(?:1|2c|3)(?:\s|\z)/.match?(entry.text)
|
|
178
|
+
end
|
|
179
|
+
record_unknown_snmp_access(unsupported_groups, code: :unsupported_snmp_group) unless unsupported_groups.empty?
|
|
180
|
+
groups = configured_snmp_groups(matching_blocks(/\Asnmp-server group\s+\S+\s+v3(?:\s|\z)/)) do |entry|
|
|
117
181
|
name, settings = entry.text.match(/\Asnmp-server group\s+(.+?)\s+v3(?:\s+(.*))?\z/).captures
|
|
118
|
-
write = name.include?('"') ? nil :
|
|
182
|
+
write = name.include?('"') ? nil : snmp_group_write_state(settings.to_s)
|
|
119
183
|
[ name, { acl: entry.text[/\baccess\s+(\S+)/, 1], line: entry.line, write: write } ]
|
|
120
184
|
end
|
|
121
|
-
|
|
185
|
+
record_fact("snmp.groups", groups)
|
|
122
186
|
users = {}
|
|
123
187
|
matching_blocks(/\Asnmp-server user\b/).each do |entry|
|
|
124
188
|
match = /\Asnmp-server user\s+(\S+)\s+(\S+)(.*)\z/.match(entry.text)
|
|
125
189
|
unless match
|
|
126
|
-
|
|
190
|
+
record_unknown_snmp_access([ entry ])
|
|
127
191
|
next
|
|
128
192
|
end
|
|
129
193
|
if nxos? && match[2] == "use-ipv4acl"
|
|
@@ -133,11 +197,11 @@ module Baseline
|
|
|
133
197
|
end
|
|
134
198
|
settings = match[3]
|
|
135
199
|
if /\bv[12]c?\b/.match?(settings)
|
|
136
|
-
|
|
200
|
+
record_unknown_snmp_access([ entry ], code: :unsupported_snmp_user)
|
|
137
201
|
next
|
|
138
202
|
end
|
|
139
203
|
unless nxos? || /\bv3\b/.match?(settings)
|
|
140
|
-
|
|
204
|
+
record_unknown_snmp_access([ entry ])
|
|
141
205
|
next
|
|
142
206
|
end
|
|
143
207
|
versions |= [ "v3" ]
|
|
@@ -145,33 +209,51 @@ module Baseline
|
|
|
145
209
|
if nxos?
|
|
146
210
|
(user[:roles] ||= []) << match[2]
|
|
147
211
|
user[:roles].uniq!
|
|
148
|
-
user[:write] =
|
|
212
|
+
user[:write] = FactBuilder.write_permission_state(user[:roles].map { |role| SNMP_ROLE_WRITE[role] })
|
|
149
213
|
next if settings.strip.empty?
|
|
150
214
|
end
|
|
151
|
-
modes = { auth:
|
|
152
|
-
priv:
|
|
153
|
-
security =
|
|
215
|
+
modes = { auth: parse_snmp_secret(settings, "auth", algorithms: %w[md5 sha sha-224 sha-256 sha-384 sha-512]),
|
|
216
|
+
priv: parse_snmp_secret(settings, "priv", algorithms: %w[des 3des aes aes-128 aes-192 aes-256], optional_algorithm: nxos?, aes_size: !nxos?) }
|
|
217
|
+
security = record_snmp_credentials("snmp:#{match[1]}", modes, entry, format: /\bencrypted\b/.match?(settings) ? "cipher" : "unknown")
|
|
154
218
|
user.merge!(security).merge!(group: match[2])
|
|
155
219
|
acl = settings[/\b(?:access|use-ipv4acl)\s+(\S+)/, 1]
|
|
156
220
|
user[:acl] = acl if acl
|
|
157
221
|
end
|
|
158
222
|
port_overrides = matching_blocks(/\A(?:no )?snmp-server (?:listen-port|udp-port|port)(?:\s|\z)/)
|
|
159
|
-
|
|
160
|
-
@
|
|
223
|
+
record_unknown_listener_port("snmp.acl", port_overrides) unless port_overrides.empty?
|
|
224
|
+
@fact_builder.record_snmp_access(communities: communities.values, users: users.values, groups: nxos? ? nil : groups,
|
|
161
225
|
port: port_overrides.empty? ? 161 : nil)
|
|
162
|
-
|
|
163
|
-
|
|
226
|
+
record_unverified_snmp_user_sync(users, enabled: enabled) if nxos?
|
|
227
|
+
record_unknown_fact("snmp.v3_auth_priv", code: :snmp_user_unresolved) if users.values.any? { |user| !user.key?(:auth) }
|
|
164
228
|
versions |= configured_values(/\Asnmp-server group\s+\S+\s+(v1|v2c|v3)\b/)
|
|
165
229
|
legacy_groups = matching_blocks(/\Asnmp-server group\s+\S+\s+v(?:1|2c)\b/)
|
|
166
230
|
if legacy_groups.any?
|
|
167
|
-
@
|
|
168
|
-
|
|
231
|
+
@fact_builder.record_unknown_snmp_permission(lines: legacy_groups.map(&:line), code: :unsupported_snmp_group)
|
|
232
|
+
record_unknown_fact("snmp.acl", legacy_groups, code: :unsupported_snmp_group)
|
|
169
233
|
end
|
|
170
|
-
|
|
234
|
+
record_fact("snmp.versions", versions) unless versions.empty?
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# IOS 的 no 声明撤销整项团体字;先归约有效来源,避免留下访问或凭据事实。
|
|
238
|
+
def configured_community_entries
|
|
239
|
+
return matching_blocks(/\Asnmp-server community\b/) if nxos?
|
|
240
|
+
|
|
241
|
+
active = {}
|
|
242
|
+
matching_blocks(/\A(?:no )?snmp-server community\b/).each do |entry|
|
|
243
|
+
if (match = /\Ano snmp-server community\s+(\S+)\z/.match(entry.text))
|
|
244
|
+
analyze(entry) { active.delete(match[1]) }
|
|
245
|
+
elsif entry.text.start_with?("no ")
|
|
246
|
+
record_unknown_snmp_access([ entry ])
|
|
247
|
+
else
|
|
248
|
+
name = entry.text.split[2]
|
|
249
|
+
(active[name] ||= []) << entry
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
active.values.flatten.sort_by(&:line)
|
|
171
253
|
end
|
|
172
254
|
|
|
173
255
|
# NX-OS 通常会同步 CLI 与 SNMP 账号;缺少证据时不能构造虚拟用户。
|
|
174
|
-
def
|
|
256
|
+
def record_unverified_snmp_user_sync(users, enabled:)
|
|
175
257
|
return if enabled == false
|
|
176
258
|
sync_disabled = configured_state(/\Asnmp-server disable snmp-aaa sync\z/, /\Ano snmp-server disable snmp-aaa sync\z/)
|
|
177
259
|
return if sync_disabled == true
|
|
@@ -181,12 +263,12 @@ module Baseline
|
|
|
181
263
|
covered = users[name]&.fetch(:roles, []) || []
|
|
182
264
|
next if roles.any? && (roles - covered).empty?
|
|
183
265
|
|
|
184
|
-
@
|
|
266
|
+
@fact_builder.record_unknown_snmp_permission(lines: entries.map(&:line), code: :snmp_user_sync_unconfirmed)
|
|
185
267
|
end
|
|
186
268
|
end
|
|
187
269
|
|
|
188
270
|
# IOS 默认不授予写视图;不熟悉的组配置项不能证明只读权限。
|
|
189
|
-
def
|
|
271
|
+
def snmp_group_write_state(settings)
|
|
190
272
|
syntax = /\A(?:auth|noauth|priv)(?:\s+(?:read|(?<write>write)|notify|context)\s+[^\s"]+|\s+match\s+(?:exact|prefix)|\s+access(?:\s+ipv6)?\s+[^\s"]+)*\z/
|
|
191
273
|
match = syntax.match(settings)
|
|
192
274
|
return unless match
|
|
@@ -195,25 +277,32 @@ module Baseline
|
|
|
195
277
|
end
|
|
196
278
|
|
|
197
279
|
# 解析实际生效的认证方法及外部服务器引用。
|
|
198
|
-
def
|
|
280
|
+
def parse_authentication
|
|
199
281
|
servers, aliases, keyed = [], {}, []
|
|
200
282
|
global_key = matching_blocks(/\Atacacs-server key\s+/)
|
|
201
|
-
global_key.each { |entry|
|
|
202
|
-
matching_blocks(/\
|
|
203
|
-
|
|
204
|
-
|
|
283
|
+
global_key.each { |entry| record_tacacs_key("tacacs", entry.text.sub(/\Atacacs-server key\s+/, ""), entry) }
|
|
284
|
+
legacy_hosts = matching_blocks(/\A(?:no )?tacacs-server host(?:\s|\z)/)
|
|
285
|
+
hosts = configured_server_addresses("tacacs", /\Atacacs-server host\s+(\S+)/, legacy_hosts, negate: "no")
|
|
286
|
+
servers |= hosts
|
|
287
|
+
active_hosts = {}
|
|
288
|
+
legacy_hosts.each do |entry|
|
|
289
|
+
host = entry.text[/\A(?:no )?tacacs-server host\s+(\S+)/, 1]
|
|
290
|
+
next unless server_address?(host)
|
|
291
|
+
entry.text.start_with?("no ") ? active_hosts.delete(host) : active_hosts[host] = entry
|
|
292
|
+
end
|
|
293
|
+
active_hosts.each do |host, entry|
|
|
205
294
|
key = entry.text[/\bkey\s+(.+)\z/, 1]
|
|
206
295
|
if key
|
|
207
|
-
|
|
296
|
+
record_tacacs_key("tacacs:#{host}", key, entry)
|
|
208
297
|
keyed << host
|
|
209
298
|
end
|
|
210
299
|
end
|
|
211
300
|
matching_blocks(/\Atacacs server\s+/).each do |section|
|
|
212
|
-
hosts =
|
|
301
|
+
hosts = configured_server_addresses("tacacs", /\Aaddress ipv[46]\s+(\S+)/, matching_blocks(/\A(?:no )?address ipv[46](?:\s|\z)/, section.children), negate: "no")
|
|
213
302
|
aliases[section.text.split.last] = hosts
|
|
214
303
|
servers |= hosts
|
|
215
304
|
matching_blocks(/\Akey\s+/, section.children).each do |entry|
|
|
216
|
-
|
|
305
|
+
record_tacacs_key("tacacs:#{section.text.split.last}", entry.text.delete_prefix("key "), entry)
|
|
217
306
|
keyed |= hosts
|
|
218
307
|
end
|
|
219
308
|
end
|
|
@@ -221,57 +310,72 @@ module Baseline
|
|
|
221
310
|
members = configured_values(/\Aserver(?: name)?\s+(\S+)/, section.children)
|
|
222
311
|
[ section.text.split.last, members.flat_map { |member| aliases.fetch(member, [ member ]) } ]
|
|
223
312
|
end
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
313
|
+
record_fact("tacacs.servers", servers.uniq)
|
|
314
|
+
record_fact("tacacs.groups", groups)
|
|
315
|
+
record_tacacs_login_usage(servers, groups)
|
|
316
|
+
%w[authorization accounting].each { |kind| record_tacacs_action_usage(kind, servers, groups) }
|
|
317
|
+
record_fact("tacacs.keys_present", global_key.any? || (servers - keyed).empty?) unless servers.empty?
|
|
318
|
+
parse_local_accounts
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# 授权和计费按动作及命令级别分别覆盖,只有全部最终声明都首选 TACACS 才确认使用。
|
|
322
|
+
def record_tacacs_action_usage(kind, servers, groups)
|
|
323
|
+
pattern = /\A(?:no )?aaa #{kind} (?:(exec|commands(?: \d+)?) )?default(?:\s|\z)/
|
|
324
|
+
entries = matching_blocks(pattern).group_by { |entry| entry.text[pattern, 1] }.values.map(&:last)
|
|
325
|
+
results = entries.map do |entry|
|
|
326
|
+
match = /\Aaaa #{kind} (?:(?:exec|commands(?: \d+)?) )?default\s+(.+)\z/.match(entry.text)
|
|
327
|
+
next :unsupported_authentication_method unless match
|
|
328
|
+
|
|
329
|
+
methods = match[1]
|
|
330
|
+
if kind == "accounting"
|
|
331
|
+
next analyze(entry) { false } if methods == "none"
|
|
332
|
+
methods = methods.sub(/\A(?:start-stop|stop-only)\s+/, "")
|
|
238
333
|
end
|
|
334
|
+
analyze(entry) { tacacs_login_method_state(methods, servers, groups) }
|
|
335
|
+
end
|
|
336
|
+
if (reason = results.find { |value| value.is_a?(Symbol) })
|
|
337
|
+
record_unknown_fact("tacacs.#{kind}", entries, code: reason)
|
|
338
|
+
else
|
|
339
|
+
used = results.any? && results.all?(true)
|
|
340
|
+
record_fact("tacacs.#{kind}", used, entries) if @fact_builder.complete? || used
|
|
239
341
|
end
|
|
240
|
-
record("tacacs.keys_present", global_key.any? || (servers - keyed).empty?) unless servers.empty?
|
|
241
|
-
accounts
|
|
242
342
|
end
|
|
243
343
|
|
|
244
344
|
# 解析各 VTY 实际选用的登录列表,包括命名列表。
|
|
245
|
-
def
|
|
345
|
+
def record_tacacs_login_usage(servers, groups)
|
|
246
346
|
pattern = /\Aaaa authentication login\s+(\S+)\s+(.+)\z/
|
|
247
|
-
|
|
248
|
-
|
|
347
|
+
family = /\A(?:no )?aaa authentication login\s+(\S+)(?:\s|\z)/
|
|
348
|
+
lists = matching_blocks(family).each_with_object({}) do |entry, result|
|
|
349
|
+
name = entry.text[family, 1]
|
|
350
|
+
match = pattern.match(entry.text)
|
|
351
|
+
# 最后同名声明决定列表证据;撤销或损坏不能复用先前的方法。
|
|
352
|
+
methods = match ? analyze(entry) { match[2] } : nil
|
|
249
353
|
result[name] = { methods: methods, entry: entry }
|
|
250
354
|
end
|
|
251
|
-
entries = @
|
|
252
|
-
results = @
|
|
355
|
+
entries = @fact_builder.vtys.filter_map { |vty| lists[vty[:authentication_list]]&.fetch(:entry) }.uniq
|
|
356
|
+
results = @fact_builder.vtys.map do |vty|
|
|
253
357
|
if vty[:authentication].nil?
|
|
254
358
|
:vty_authentication_unknown
|
|
255
359
|
elsif vty[:authentication] != "aaa"
|
|
256
360
|
false
|
|
257
361
|
elsif (list = lists[vty[:authentication_list]])
|
|
258
|
-
|
|
362
|
+
list[:methods] ? tacacs_login_method_state(list[:methods], servers, groups) : :authentication_list_unresolved
|
|
259
363
|
else
|
|
260
364
|
:authentication_list_unresolved
|
|
261
365
|
end
|
|
262
366
|
end
|
|
263
367
|
results << :vty_authentication_unknown if results.empty?
|
|
264
368
|
if results.include?(false)
|
|
265
|
-
|
|
369
|
+
record_fact("tacacs.login", false, entries) if @fact_builder.complete?
|
|
266
370
|
elsif (reason = results.find { |value| value.is_a?(Symbol) })
|
|
267
|
-
|
|
371
|
+
record_unknown_fact("tacacs.login", entries, code: reason)
|
|
268
372
|
else
|
|
269
|
-
|
|
373
|
+
record_fact("tacacs.login", true, entries)
|
|
270
374
|
end
|
|
271
375
|
end
|
|
272
376
|
|
|
273
377
|
# 仅当首选认证方法引用可解析的 TACACS 组时,才确认使用 TACACS。
|
|
274
|
-
def
|
|
378
|
+
def tacacs_login_method_state(methods, servers, groups)
|
|
275
379
|
tokens = methods.split
|
|
276
380
|
outcomes = []
|
|
277
381
|
until tokens.empty?
|
|
@@ -296,13 +400,13 @@ module Baseline
|
|
|
296
400
|
end
|
|
297
401
|
|
|
298
402
|
# 提取已配置的凭据编码方式,不保留秘密值。
|
|
299
|
-
def
|
|
403
|
+
def record_tacacs_key(owner, text, entry)
|
|
300
404
|
match = /\A(?:(\d+)\s+)?(\S+)/.match(text)
|
|
301
405
|
record_credential(owner, "tacacs", match&.[](1) || "plain", match&.[](2), entry)
|
|
302
406
|
end
|
|
303
407
|
|
|
304
408
|
# 提取本地账号角色及凭据存储属性。
|
|
305
|
-
def
|
|
409
|
+
def parse_local_accounts
|
|
306
410
|
matching_blocks(/\Ausername\s+/).group_by { |entry| entry.text.split[1] }.each do |name, entries|
|
|
307
411
|
passwords = entries.filter_map do |entry|
|
|
308
412
|
match = /\b(?:password|secret)\s+(?:(\d+)\s+)?(\S+)/.match(entry.text)
|