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
|
@@ -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
|
-
|
|
148
|
+
enabled = nxos? || snmp_entries.any? if enabled.nil? && (@fact_builder.complete? || snmp_entries.any?)
|
|
149
|
+
record_fact("snmp.enabled", enabled)
|
|
98
150
|
communities = {}
|
|
99
151
|
matching_blocks(/\Asnmp-server community\b/).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,24 @@ 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
|
-
record_credential("snmp-community", "snmp", "plain", match[1], entry) unless @
|
|
165
|
+
record_credential("snmp-community", "snmp", "plain", match[1], entry) unless @fact_builder.credential_recorded?(community[:line])
|
|
114
166
|
end
|
|
115
167
|
versions = communities.any? ? %w[v1 v2c] : []
|
|
116
|
-
|
|
168
|
+
unsupported_groups = matching_blocks(/\Asnmp-server group\b/).reject do |entry|
|
|
169
|
+
/\Asnmp-server group\s+\S+\s+v(?:1|2c|3)(?:\s|\z)/.match?(entry.text)
|
|
170
|
+
end
|
|
171
|
+
record_unknown_snmp_access(unsupported_groups, code: :unsupported_snmp_group) unless unsupported_groups.empty?
|
|
172
|
+
groups = configured_snmp_groups(matching_blocks(/\Asnmp-server group\s+\S+\s+v3(?:\s|\z)/)) do |entry|
|
|
117
173
|
name, settings = entry.text.match(/\Asnmp-server group\s+(.+?)\s+v3(?:\s+(.*))?\z/).captures
|
|
118
|
-
write = name.include?('"') ? nil :
|
|
174
|
+
write = name.include?('"') ? nil : snmp_group_write_state(settings.to_s)
|
|
119
175
|
[ name, { acl: entry.text[/\baccess\s+(\S+)/, 1], line: entry.line, write: write } ]
|
|
120
176
|
end
|
|
121
|
-
|
|
177
|
+
record_fact("snmp.groups", groups)
|
|
122
178
|
users = {}
|
|
123
179
|
matching_blocks(/\Asnmp-server user\b/).each do |entry|
|
|
124
180
|
match = /\Asnmp-server user\s+(\S+)\s+(\S+)(.*)\z/.match(entry.text)
|
|
125
181
|
unless match
|
|
126
|
-
|
|
182
|
+
record_unknown_snmp_access([ entry ])
|
|
127
183
|
next
|
|
128
184
|
end
|
|
129
185
|
if nxos? && match[2] == "use-ipv4acl"
|
|
@@ -133,11 +189,11 @@ module Baseline
|
|
|
133
189
|
end
|
|
134
190
|
settings = match[3]
|
|
135
191
|
if /\bv[12]c?\b/.match?(settings)
|
|
136
|
-
|
|
192
|
+
record_unknown_snmp_access([ entry ], code: :unsupported_snmp_user)
|
|
137
193
|
next
|
|
138
194
|
end
|
|
139
195
|
unless nxos? || /\bv3\b/.match?(settings)
|
|
140
|
-
|
|
196
|
+
record_unknown_snmp_access([ entry ])
|
|
141
197
|
next
|
|
142
198
|
end
|
|
143
199
|
versions |= [ "v3" ]
|
|
@@ -145,33 +201,33 @@ module Baseline
|
|
|
145
201
|
if nxos?
|
|
146
202
|
(user[:roles] ||= []) << match[2]
|
|
147
203
|
user[:roles].uniq!
|
|
148
|
-
user[:write] =
|
|
204
|
+
user[:write] = FactBuilder.write_permission_state(user[:roles].map { |role| SNMP_ROLE_WRITE[role] })
|
|
149
205
|
next if settings.strip.empty?
|
|
150
206
|
end
|
|
151
|
-
modes = { auth:
|
|
152
|
-
priv:
|
|
153
|
-
security =
|
|
207
|
+
modes = { auth: parse_snmp_secret(settings, "auth", algorithms: %w[md5 sha sha-224 sha-256 sha-384 sha-512]),
|
|
208
|
+
priv: parse_snmp_secret(settings, "priv", algorithms: %w[des 3des aes aes-128 aes-192 aes-256], optional_algorithm: nxos?, aes_size: !nxos?) }
|
|
209
|
+
security = record_snmp_credentials("snmp:#{match[1]}", modes, entry, format: /\bencrypted\b/.match?(settings) ? "cipher" : "unknown")
|
|
154
210
|
user.merge!(security).merge!(group: match[2])
|
|
155
211
|
acl = settings[/\b(?:access|use-ipv4acl)\s+(\S+)/, 1]
|
|
156
212
|
user[:acl] = acl if acl
|
|
157
213
|
end
|
|
158
214
|
port_overrides = matching_blocks(/\A(?:no )?snmp-server (?:listen-port|udp-port|port)(?:\s|\z)/)
|
|
159
|
-
|
|
160
|
-
@
|
|
215
|
+
record_unknown_listener_port("snmp.acl", port_overrides) unless port_overrides.empty?
|
|
216
|
+
@fact_builder.record_snmp_access(communities: communities.values, users: users.values, groups: nxos? ? nil : groups,
|
|
161
217
|
port: port_overrides.empty? ? 161 : nil)
|
|
162
|
-
|
|
163
|
-
|
|
218
|
+
record_unverified_snmp_user_sync(users, enabled: enabled) if nxos?
|
|
219
|
+
record_unknown_fact("snmp.v3_auth_priv", code: :snmp_user_unresolved) if users.values.any? { |user| !user.key?(:auth) }
|
|
164
220
|
versions |= configured_values(/\Asnmp-server group\s+\S+\s+(v1|v2c|v3)\b/)
|
|
165
221
|
legacy_groups = matching_blocks(/\Asnmp-server group\s+\S+\s+v(?:1|2c)\b/)
|
|
166
222
|
if legacy_groups.any?
|
|
167
|
-
@
|
|
168
|
-
|
|
223
|
+
@fact_builder.record_unknown_snmp_permission(lines: legacy_groups.map(&:line), code: :unsupported_snmp_group)
|
|
224
|
+
record_unknown_fact("snmp.acl", legacy_groups, code: :unsupported_snmp_group)
|
|
169
225
|
end
|
|
170
|
-
|
|
226
|
+
record_fact("snmp.versions", versions) unless versions.empty?
|
|
171
227
|
end
|
|
172
228
|
|
|
173
229
|
# NX-OS 通常会同步 CLI 与 SNMP 账号;缺少证据时不能构造虚拟用户。
|
|
174
|
-
def
|
|
230
|
+
def record_unverified_snmp_user_sync(users, enabled:)
|
|
175
231
|
return if enabled == false
|
|
176
232
|
sync_disabled = configured_state(/\Asnmp-server disable snmp-aaa sync\z/, /\Ano snmp-server disable snmp-aaa sync\z/)
|
|
177
233
|
return if sync_disabled == true
|
|
@@ -181,12 +237,12 @@ module Baseline
|
|
|
181
237
|
covered = users[name]&.fetch(:roles, []) || []
|
|
182
238
|
next if roles.any? && (roles - covered).empty?
|
|
183
239
|
|
|
184
|
-
@
|
|
240
|
+
@fact_builder.record_unknown_snmp_permission(lines: entries.map(&:line), code: :snmp_user_sync_unconfirmed)
|
|
185
241
|
end
|
|
186
242
|
end
|
|
187
243
|
|
|
188
244
|
# IOS 默认不授予写视图;不熟悉的组配置项不能证明只读权限。
|
|
189
|
-
def
|
|
245
|
+
def snmp_group_write_state(settings)
|
|
190
246
|
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
247
|
match = syntax.match(settings)
|
|
192
248
|
return unless match
|
|
@@ -195,25 +251,32 @@ module Baseline
|
|
|
195
251
|
end
|
|
196
252
|
|
|
197
253
|
# 解析实际生效的认证方法及外部服务器引用。
|
|
198
|
-
def
|
|
254
|
+
def parse_authentication
|
|
199
255
|
servers, aliases, keyed = [], {}, []
|
|
200
256
|
global_key = matching_blocks(/\Atacacs-server key\s+/)
|
|
201
|
-
global_key.each { |entry|
|
|
202
|
-
matching_blocks(/\
|
|
203
|
-
|
|
204
|
-
|
|
257
|
+
global_key.each { |entry| record_tacacs_key("tacacs", entry.text.sub(/\Atacacs-server key\s+/, ""), entry) }
|
|
258
|
+
legacy_hosts = matching_blocks(/\A(?:no )?tacacs-server host(?:\s|\z)/)
|
|
259
|
+
hosts = configured_server_addresses("tacacs", /\Atacacs-server host\s+(\S+)/, legacy_hosts, negate: "no")
|
|
260
|
+
servers |= hosts
|
|
261
|
+
active_hosts = {}
|
|
262
|
+
legacy_hosts.each do |entry|
|
|
263
|
+
host = entry.text[/\A(?:no )?tacacs-server host\s+(\S+)/, 1]
|
|
264
|
+
next unless server_address?(host)
|
|
265
|
+
entry.text.start_with?("no ") ? active_hosts.delete(host) : active_hosts[host] = entry
|
|
266
|
+
end
|
|
267
|
+
active_hosts.each do |host, entry|
|
|
205
268
|
key = entry.text[/\bkey\s+(.+)\z/, 1]
|
|
206
269
|
if key
|
|
207
|
-
|
|
270
|
+
record_tacacs_key("tacacs:#{host}", key, entry)
|
|
208
271
|
keyed << host
|
|
209
272
|
end
|
|
210
273
|
end
|
|
211
274
|
matching_blocks(/\Atacacs server\s+/).each do |section|
|
|
212
|
-
hosts =
|
|
275
|
+
hosts = configured_server_addresses("tacacs", /\Aaddress ipv[46]\s+(\S+)/, matching_blocks(/\A(?:no )?address ipv[46](?:\s|\z)/, section.children), negate: "no")
|
|
213
276
|
aliases[section.text.split.last] = hosts
|
|
214
277
|
servers |= hosts
|
|
215
278
|
matching_blocks(/\Akey\s+/, section.children).each do |entry|
|
|
216
|
-
|
|
279
|
+
record_tacacs_key("tacacs:#{section.text.split.last}", entry.text.delete_prefix("key "), entry)
|
|
217
280
|
keyed |= hosts
|
|
218
281
|
end
|
|
219
282
|
end
|
|
@@ -221,57 +284,68 @@ module Baseline
|
|
|
221
284
|
members = configured_values(/\Aserver(?: name)?\s+(\S+)/, section.children)
|
|
222
285
|
[ section.text.split.last, members.flat_map { |member| aliases.fetch(member, [ member ]) } ]
|
|
223
286
|
end
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
287
|
+
record_fact("tacacs.servers", servers.uniq)
|
|
288
|
+
record_fact("tacacs.groups", groups)
|
|
289
|
+
record_tacacs_login_usage(servers, groups)
|
|
290
|
+
%w[authorization accounting].each { |kind| record_tacacs_action_usage(kind, servers, groups) }
|
|
291
|
+
record_fact("tacacs.keys_present", global_key.any? || (servers - keyed).empty?) unless servers.empty?
|
|
292
|
+
parse_local_accounts
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# 授权和计费按动作及命令级别分别覆盖,只有全部最终声明都首选 TACACS 才确认使用。
|
|
296
|
+
def record_tacacs_action_usage(kind, servers, groups)
|
|
297
|
+
pattern = /\A(?:no )?aaa #{kind} (?:(exec|commands(?: \d+)?) )?default(?:\s|\z)/
|
|
298
|
+
entries = matching_blocks(pattern).group_by { |entry| entry.text[pattern, 1] }.values.map(&:last)
|
|
299
|
+
results = entries.map do |entry|
|
|
300
|
+
match = /\Aaaa #{kind} (?:(?:exec|commands(?: \d+)?) )?default\s+(.+)\z/.match(entry.text)
|
|
301
|
+
next :unsupported_authentication_method unless match
|
|
302
|
+
|
|
303
|
+
methods = match[1]
|
|
304
|
+
if kind == "accounting"
|
|
305
|
+
next analyze(entry) { false } if methods == "none"
|
|
306
|
+
methods = methods.sub(/\A(?:start-stop|stop-only)\s+/, "")
|
|
238
307
|
end
|
|
308
|
+
analyze(entry) { tacacs_login_method_state(methods, servers, groups) }
|
|
309
|
+
end
|
|
310
|
+
if (reason = results.find { |value| value.is_a?(Symbol) })
|
|
311
|
+
record_unknown_fact("tacacs.#{kind}", entries, code: reason)
|
|
312
|
+
else
|
|
313
|
+
used = results.any? && results.all?(true)
|
|
314
|
+
record_fact("tacacs.#{kind}", used, entries) if @fact_builder.complete? || used
|
|
239
315
|
end
|
|
240
|
-
record("tacacs.keys_present", global_key.any? || (servers - keyed).empty?) unless servers.empty?
|
|
241
|
-
accounts
|
|
242
316
|
end
|
|
243
317
|
|
|
244
318
|
# 解析各 VTY 实际选用的登录列表,包括命名列表。
|
|
245
|
-
def
|
|
319
|
+
def record_tacacs_login_usage(servers, groups)
|
|
246
320
|
pattern = /\Aaaa authentication login\s+(\S+)\s+(.+)\z/
|
|
247
321
|
lists = matching_blocks(pattern).each_with_object({}) do |entry, result|
|
|
248
322
|
name, methods = analyze(entry) { entry.text.match(pattern).captures }
|
|
249
323
|
result[name] = { methods: methods, entry: entry }
|
|
250
324
|
end
|
|
251
|
-
entries = @
|
|
252
|
-
results = @
|
|
325
|
+
entries = @fact_builder.vtys.filter_map { |vty| lists[vty[:authentication_list]]&.fetch(:entry) }.uniq
|
|
326
|
+
results = @fact_builder.vtys.map do |vty|
|
|
253
327
|
if vty[:authentication].nil?
|
|
254
328
|
:vty_authentication_unknown
|
|
255
329
|
elsif vty[:authentication] != "aaa"
|
|
256
330
|
false
|
|
257
331
|
elsif (list = lists[vty[:authentication_list]])
|
|
258
|
-
|
|
332
|
+
tacacs_login_method_state(list[:methods], servers, groups)
|
|
259
333
|
else
|
|
260
334
|
:authentication_list_unresolved
|
|
261
335
|
end
|
|
262
336
|
end
|
|
263
337
|
results << :vty_authentication_unknown if results.empty?
|
|
264
338
|
if results.include?(false)
|
|
265
|
-
|
|
339
|
+
record_fact("tacacs.login", false, entries) if @fact_builder.complete?
|
|
266
340
|
elsif (reason = results.find { |value| value.is_a?(Symbol) })
|
|
267
|
-
|
|
341
|
+
record_unknown_fact("tacacs.login", entries, code: reason)
|
|
268
342
|
else
|
|
269
|
-
|
|
343
|
+
record_fact("tacacs.login", true, entries)
|
|
270
344
|
end
|
|
271
345
|
end
|
|
272
346
|
|
|
273
347
|
# 仅当首选认证方法引用可解析的 TACACS 组时,才确认使用 TACACS。
|
|
274
|
-
def
|
|
348
|
+
def tacacs_login_method_state(methods, servers, groups)
|
|
275
349
|
tokens = methods.split
|
|
276
350
|
outcomes = []
|
|
277
351
|
until tokens.empty?
|
|
@@ -296,13 +370,13 @@ module Baseline
|
|
|
296
370
|
end
|
|
297
371
|
|
|
298
372
|
# 提取已配置的凭据编码方式,不保留秘密值。
|
|
299
|
-
def
|
|
373
|
+
def record_tacacs_key(owner, text, entry)
|
|
300
374
|
match = /\A(?:(\d+)\s+)?(\S+)/.match(text)
|
|
301
375
|
record_credential(owner, "tacacs", match&.[](1) || "plain", match&.[](2), entry)
|
|
302
376
|
end
|
|
303
377
|
|
|
304
378
|
# 提取本地账号角色及凭据存储属性。
|
|
305
|
-
def
|
|
379
|
+
def parse_local_accounts
|
|
306
380
|
matching_blocks(/\Ausername\s+/).group_by { |entry| entry.text.split[1] }.each do |name, entries|
|
|
307
381
|
passwords = entries.filter_map do |entry|
|
|
308
382
|
match = /\b(?:password|secret)\s+(?:(\d+)\s+)?(\S+)/.match(entry.text)
|