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,42 +9,42 @@ module Baseline
|
|
|
9
9
|
private
|
|
10
10
|
|
|
11
11
|
# 解析管理访问暴露检查使用的有序 ACL 定义。
|
|
12
|
-
def
|
|
12
|
+
def parse_access_lists
|
|
13
13
|
matching_blocks(/\Aacl\s+/).each do |section|
|
|
14
14
|
# IPv6 ACL 不能误建为名为 ipv6 的 IPv4 ACL,也不能覆盖其别名。
|
|
15
15
|
if /\Aacl\s+ipv6\b/.match?(section.text)
|
|
16
|
-
|
|
16
|
+
record_unknown_fact("ipv6.acls", [ section ], code: :unsupported_acl_family)
|
|
17
17
|
next
|
|
18
18
|
end
|
|
19
19
|
match = /\Aacl\s+(?:number\s+|basic\s+|advanced\s+)?(?:name\s+)?(\S+)(?:\s+name\s+(\S+))?(?:\s+.*)?\z/.match(section.text)
|
|
20
20
|
next unless match
|
|
21
21
|
register_acl(match[2] || match[1], section.children, source: section, aliases: [ match[1] ], supported: !section.text.include?("match-order auto")) do |entry, index|
|
|
22
|
-
|
|
22
|
+
parse_acl_rule(entry, index)
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# 解析单条有序 Comware ACL 规则及来源限制。
|
|
28
|
-
def
|
|
28
|
+
def parse_acl_rule(entry, _index)
|
|
29
29
|
match = /\Arule\s+(\d+)\s+(permit|deny)(?:\s+(.*))?\z/.match(entry.text)
|
|
30
30
|
return unless match
|
|
31
31
|
parts = match[3].to_s.split
|
|
32
32
|
protocol = %w[ip tcp udp icmp].include?(parts.first) ? parts.shift : "ip"
|
|
33
33
|
network = if parts.first == "source"
|
|
34
34
|
parts.shift
|
|
35
|
-
|
|
35
|
+
parse_acl_source_network(parts)
|
|
36
36
|
else
|
|
37
37
|
"0.0.0.0/0"
|
|
38
38
|
end
|
|
39
39
|
if parts.first == "destination"
|
|
40
40
|
parts.shift
|
|
41
|
-
return unless
|
|
41
|
+
return unless parse_acl_source_network(parts) == "0.0.0.0/0"
|
|
42
42
|
end
|
|
43
43
|
if parts.first == "destination-port"
|
|
44
44
|
return unless %w[tcp udp].include?(protocol)
|
|
45
45
|
parts.shift
|
|
46
46
|
return unless parts.shift == "eq"
|
|
47
|
-
port =
|
|
47
|
+
port = parse_service_port(parts.shift)
|
|
48
48
|
return if port.nil?
|
|
49
49
|
end
|
|
50
50
|
return unless (parts - %w[logging counting]).empty?
|
|
@@ -55,60 +55,99 @@ module Baseline
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# 根据监听服务、认证方式与 ACL 绑定关系归纳管理访问事实。
|
|
58
|
-
def
|
|
59
|
-
|
|
58
|
+
def parse_management
|
|
59
|
+
record_fact("ssh.enabled", configured_state(ssh_enable_pattern, ssh_disable_pattern), matching_blocks(/\A(?:undo )?(?:ssh|stelnet)/))
|
|
60
60
|
compatible = configured_state(/\Assh server compatible-ssh1x enable\z/, /\A(?:undo ssh server compatible-ssh1x enable|ssh server compatible-ssh1x disable)\z/)
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
record_fact("ssh.versions", compatible.nil? ? nil : compatible ? [ "1", "2" ] : [ "2" ])
|
|
62
|
+
ssh_acl_entries = matching_blocks(/\A(?:undo )?ssh server acl\b/).reject { |entry| /\A(?:undo )?ssh server acl ipv6\b/.match?(entry.text) }
|
|
63
|
+
ssh_acl = effective_configured_value(/\Assh server acl(?: ipv4)?\s+(?!name\z|ipv4\z)(?:name\s+)?(\S+)\z/, entries: ssh_acl_entries,
|
|
64
|
+
family: /\A(?:undo )?ssh server acl\b/, fact: "management.acl", reset: /\Aundo ssh server acl(?: ipv4)?(?: (?:name )?\S+)?\z/)
|
|
63
65
|
defaults = matching_blocks(/\Aline class vty\z/).flat_map(&:children)
|
|
64
66
|
vtys = matching_blocks(/\A(?:line|user-interface) vty\b/).map do |section|
|
|
65
67
|
entries = defaults + section.children
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
acl_entries = entries.reject { |entry| /\A(?:undo )?acl\s+ipv6\b|\A(?:undo )?acl\s+(?:name\s+)?\S+\s+outbound(?:\s|\z)/.match?(entry.text) }
|
|
69
|
+
{ name: section.text, protocols: effective_configured_value(/\Aprotocol inbound\s+((?:all|ssh|stelnet|telnet)(?:\s+(?:ssh|stelnet|telnet))*)\z/, entries: entries,
|
|
70
|
+
family: /\A(?:undo )?protocol inbound\b/, fact: "ssh.vty_ssh_only", reset: /\Aundo protocol inbound(?:\s+\S+)*\z/)&.split,
|
|
71
|
+
authentication: effective_configured_value(/\Aauthentication-mode\s+(scheme|aaa|password|none)\z/, entries: entries,
|
|
72
|
+
family: /\A(?:undo )?authentication-mode\b/, fact: "ssh.vty_aaa", reset: /\Aundo authentication-mode(?: \S+)?\z/),
|
|
73
|
+
acl: effective_configured_value(/\Aacl\s+(?!name\z)(?:name\s+)?(\S+)(?:\s+inbound)?\z/, entries: acl_entries,
|
|
74
|
+
family: /\A(?:undo )?acl\b/, fact: "management.acl", reset: /\Aundo acl(?:\s+(?:name\s+)?\S+(?:\s+inbound)?)?\z/), line: section.line }
|
|
69
75
|
end
|
|
70
|
-
@
|
|
76
|
+
@fact_builder.record_management_access(vtys: vtys, ssh_acl: ssh_acl,
|
|
71
77
|
ssh_port: listener_port("ssh server port", default: 22, scope: "management.acl"),
|
|
72
78
|
telnet_port: listener_port("telnet server port", default: 23, scope: "management.acl"))
|
|
73
79
|
definitions = { "telnet" => "telnet server", "http" => "ip http", "https" => "ip https", "ftp" => "ftp server", "tftp" => "tftp server" }
|
|
74
|
-
|
|
80
|
+
record_fact("services", definitions.to_h do |name, command|
|
|
75
81
|
[ name, configured_state(/\A#{command} enable\z/, /\A(?:undo #{command} enable|#{command} disable)\z/) ]
|
|
76
82
|
end)
|
|
77
83
|
end
|
|
78
84
|
|
|
79
|
-
#
|
|
80
|
-
def
|
|
81
|
-
|
|
85
|
+
# 提取时间服务器、认证要求与源接口。
|
|
86
|
+
def parse_ntp
|
|
87
|
+
ntp_entries = matching_blocks(/\A(?:undo )?ntp-service (?:ipv6 )?unicast-server(?:\s|\z)/)
|
|
88
|
+
ntp_pattern = /\Antp-service (?:ipv6 )?unicast-server\s+(?:vpn-instance\s+\S+\s+)?((?!(?:vpn-instance|ip|ipv6)(?:\s|\z))\S+)/
|
|
89
|
+
servers = configured_server_addresses("ntp.servers", ntp_pattern, ntp_entries, negate: "undo") do |text, match|
|
|
90
|
+
options = parse_server_options(match.post_match, flags: [ "priority" ],
|
|
91
|
+
integers: { "version" => 1..4, "authentication-keyid" => 1..4_294_967_295, "authkey" => 1..4_294_967_295 },
|
|
92
|
+
names: [ "vpn-instance" ], interfaces: [ "source-interface" ])
|
|
93
|
+
vpn = text[/\Antp-service (?:ipv6 )?unicast-server\s+vpn-instance\s+(\S+)/, 1]
|
|
94
|
+
if options && !(vpn && options.key?("vpn-instance")) && !(options.key?("authkey") && options.key?("authentication-keyid"))
|
|
95
|
+
[ match[1], vpn || options["vpn-instance"] ]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
record_fact("ntp.servers", servers, ntp_entries)
|
|
82
99
|
record_configured_state("ntp.authentication", /\Antp-service authentication enable\z/, /\Aundo ntp-service authentication enable\z/)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
source_entries = matching_blocks(/\A(?:undo )?ntp-service source(?:-interface)?\b/)
|
|
101
|
+
source = effective_configured_value(/\Antp-service source(?:-interface)?\s+(\S+)\z/, entries: source_entries,
|
|
102
|
+
family: /\A/, fact: "ntp.source", reset: /\Aundo ntp-service source(?:-interface)?(?:\s+\S+)?\z/)
|
|
103
|
+
record_fact("ntp.source", source, source_entries.last(1))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# 提取日志目标与源接口,厂商命令不进入事实归约层。
|
|
107
|
+
def parse_syslog
|
|
108
|
+
log_entries = matching_blocks(/\A(?:undo )?info-center loghost(?:\s|\z)/).reject { |entry| /\A(?:undo )?info-center loghost source(?:\s|\z)/.match?(entry.text) }
|
|
109
|
+
hosts = configured_server_addresses("syslog.servers", /\Ainfo-center loghost\s+(?:vpn-instance\s+\S+\s+)?(?:ipv6\s+)?((?!(?:vpn-instance|ipv6)(?:\s|\z))\S+)/, log_entries, negate: "undo") do |text, match|
|
|
110
|
+
[ match[1], text[/\Ainfo-center loghost\s+vpn-instance\s+(\S+)/, 1] ]
|
|
111
|
+
end
|
|
112
|
+
record_fact("syslog.servers", hosts, log_entries)
|
|
113
|
+
source_entries = matching_blocks(/\A(?:undo )?info-center loghost source\b/)
|
|
114
|
+
source = effective_configured_value(/\Ainfo-center loghost source\s+(\S+)\z/, entries: source_entries,
|
|
115
|
+
family: /\A/, fact: "syslog.source", reset: /\Aundo info-center loghost source(?:\s+\S+)?\z/)
|
|
116
|
+
record_fact("syslog.source", source, source_entries.last(1))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# 提取轮询权限、用户安全属性及来源限制。
|
|
120
|
+
def parse_snmp
|
|
88
121
|
enabled = configured_state(/\Asnmp-agent(?:\s|\z)/, /\Aundo snmp-agent\z/)
|
|
89
|
-
|
|
122
|
+
record_fact("snmp.enabled", enabled.nil? && @fact_builder.complete? ? false : enabled)
|
|
90
123
|
versions = last_configured_value(/\Asnmp-agent sys-info version\s+(.+)\z/)&.split
|
|
91
124
|
versions = %w[v1 v2c v3] if versions == [ "all" ]
|
|
92
|
-
|
|
125
|
+
record_fact("snmp.versions", versions)
|
|
93
126
|
communities = matching_blocks(/\Asnmp-agent community\b/).filter_map do |entry|
|
|
94
127
|
match = /\Asnmp-agent community (read|write)\s+(?:(simple|cipher)\s+)?(\S+)(.*)\z/.match(entry.text)
|
|
95
128
|
unless match
|
|
96
|
-
|
|
129
|
+
record_unknown_snmp_access([ entry ])
|
|
97
130
|
next
|
|
98
131
|
end
|
|
99
132
|
record_credential("snmp-community", "snmp", match[2] || "plain", match[3], entry)
|
|
100
133
|
{ write: match[1] == "write", acl: match[4][/\bacl\s+(?:name\s+)?(\S+)/, 1], line: entry.line }
|
|
101
134
|
end
|
|
102
|
-
|
|
135
|
+
group_entries = matching_blocks(/\Asnmp-agent group\b/).select do |entry|
|
|
136
|
+
next true if /\Asnmp-agent group v3\s+\S+(?:\s|\z)/.match?(entry.text)
|
|
137
|
+
|
|
138
|
+
record_unknown_snmp_access([ entry ], code: :unsupported_snmp_group)
|
|
139
|
+
false
|
|
140
|
+
end
|
|
141
|
+
groups = configured_snmp_groups(group_entries) do |entry|
|
|
103
142
|
settings = entry.text.sub(/\Asnmp-agent group v3\s+\S+\s*/, "")
|
|
104
|
-
[ entry.text.split[3], { acl: entry.text[/\bacl\s+(?:name\s+)?(\S+)/, 1], line: entry.line, write:
|
|
143
|
+
[ entry.text.split[3], { acl: entry.text[/\bacl\s+(?:name\s+)?(\S+)/, 1], line: entry.line, write: snmp_group_write_state(settings) } ]
|
|
105
144
|
end
|
|
106
|
-
|
|
145
|
+
record_fact("snmp.groups", groups)
|
|
107
146
|
users = {}
|
|
108
147
|
matching_blocks(/\Asnmp-agent usm-user\b/).each do |entry|
|
|
109
148
|
match = /\Asnmp-agent usm-user v3\s+(\S+)(?:\s+(.*))?\z/.match(entry.text)
|
|
110
149
|
unless match
|
|
111
|
-
|
|
150
|
+
record_unknown_snmp_access([ entry ], code: :unsupported_snmp_user)
|
|
112
151
|
next
|
|
113
152
|
end
|
|
114
153
|
settings = match[2].to_s
|
|
@@ -117,18 +156,18 @@ module Baseline
|
|
|
117
156
|
first = settings.split.first
|
|
118
157
|
group ||= first unless %w[group authentication-mode privacy-mode acl local engineid].include?(first)
|
|
119
158
|
user[:group] = group if group
|
|
120
|
-
modes = { auth:
|
|
121
|
-
priv:
|
|
122
|
-
|
|
159
|
+
modes = { auth: parse_snmp_secret(settings, "authentication-mode", algorithms: %w[md5 sha sha2-224 sha2-256 sha2-384 sha2-512]),
|
|
160
|
+
priv: parse_snmp_secret(settings, "privacy-mode", algorithms: %w[des des56 3des aes128 aes192 aes256]) }
|
|
161
|
+
record_snmp_credentials("snmp:#{match[1]}", modes, entry).each { |key, value| user[key] ||= value }
|
|
123
162
|
acl = settings[/\bacl\s+(?:name\s+)?(\S+)/, 1]
|
|
124
163
|
user[:acl] = acl if acl
|
|
125
164
|
end
|
|
126
|
-
@
|
|
165
|
+
@fact_builder.record_snmp_access(communities: communities, users: users.values, groups: groups,
|
|
127
166
|
port: listener_port(snmp_port_command, default: 161, scope: "snmp.acl"))
|
|
128
167
|
end
|
|
129
168
|
|
|
130
169
|
# Comware、VRP 必须明确授予写视图;引号或未知语法保持未解析。
|
|
131
|
-
def
|
|
170
|
+
def snmp_group_write_state(settings)
|
|
132
171
|
syntax = /\A(?:(?:authentication|privacy|noauthentication)(?:\s+|\z))?(?:(?:read-view|(?<write>write-view)|notify-view)\s+[^\s"]+\s*|acl(?:\s+ipv6)?(?:\s+name)?\s+[^\s"]+\s*|acl-ipv[46]\s+[^\s"]+\s*)*\z/
|
|
133
172
|
match = syntax.match(settings)
|
|
134
173
|
return unless match
|
|
@@ -137,13 +176,13 @@ module Baseline
|
|
|
137
176
|
end
|
|
138
177
|
|
|
139
178
|
# 解析实际生效的认证方法及外部服务器引用。
|
|
140
|
-
def
|
|
141
|
-
schemes = matching_blocks(
|
|
142
|
-
server_types = matching_blocks(
|
|
143
|
-
.transform_values { |entries|
|
|
179
|
+
def parse_authentication
|
|
180
|
+
schemes = matching_blocks(tacacs_section_pattern).to_h do |section|
|
|
181
|
+
server_types = matching_blocks(tacacs_server_family_pattern, section.children).group_by { |entry| entry.text.delete_prefix("undo ").split[1] }
|
|
182
|
+
.transform_values { |entries| configured_server_addresses("tacacs", tacacs_server_pattern, entries, negate: "undo") }
|
|
144
183
|
servers = server_types.values.flatten.uniq
|
|
145
184
|
keys = section.children.filter_map do |entry|
|
|
146
|
-
match =
|
|
185
|
+
match = tacacs_key_pattern.match(entry.text)
|
|
147
186
|
next unless match
|
|
148
187
|
kind, format, secret = match.captures
|
|
149
188
|
record_credential("tacacs:#{section.text.split.last}", "tacacs", format, secret, entry)
|
|
@@ -151,29 +190,34 @@ module Baseline
|
|
|
151
190
|
end
|
|
152
191
|
[ section.text.split.last, { servers: servers, server_types: server_types, key_types: keys.uniq, line: section.line } ]
|
|
153
192
|
end
|
|
154
|
-
|
|
155
|
-
|
|
193
|
+
record_fact("tacacs.schemes", schemes)
|
|
194
|
+
record_fact("tacacs.servers", schemes.values.flat_map { |entry| entry[:servers] }.uniq)
|
|
156
195
|
domains = authentication_domains
|
|
157
196
|
methods = domains.flat_map { |entry| entry.children }
|
|
158
197
|
%w[authentication authorization accounting].each do |kind|
|
|
159
|
-
relevant = matching_blocks(/\A
|
|
160
|
-
relevant = matching_blocks(/\A
|
|
198
|
+
relevant = matching_blocks(/\A(?:undo )?#{kind}\s+login\b/, methods)
|
|
199
|
+
relevant = matching_blocks(/\A(?:undo )?#{kind}\s+default\b/, methods) if relevant.empty?
|
|
200
|
+
relevant = relevant.last(1)
|
|
201
|
+
if relevant.any? && !/\A#{kind}\s+(?:login|default)\s+(?:(?:hwtacacs-scheme|radius-scheme)\s+\S+(?:\s+(?:local|none))?|local|none)\z/.match?(relevant.first.text)
|
|
202
|
+
record_unknown_fact("tacacs.#{kind == 'authentication' ? 'login' : kind}", relevant)
|
|
203
|
+
next
|
|
204
|
+
end
|
|
161
205
|
refs = relevant.filter_map { |entry| entry.text[/\A#{kind}\s+(?:login|default)\s+hwtacacs-scheme\s+(\S+)/, 1] }
|
|
162
|
-
refs.concat(
|
|
206
|
+
refs.concat(domain_tacacs_references(kind, domains))
|
|
163
207
|
key = kind == "authentication" ? "login" : kind
|
|
164
208
|
if refs.any? && refs.any? { |name| !schemes.key?(name) || schemes[name][:server_types].fetch(kind, []).empty? }
|
|
165
|
-
|
|
209
|
+
record_unknown_fact("tacacs.#{key}", relevant, code: :tacacs_scheme_unresolved)
|
|
166
210
|
else
|
|
167
211
|
used = refs.any?
|
|
168
212
|
if used && key == "login"
|
|
169
|
-
|
|
170
|
-
used &&= @
|
|
213
|
+
record_unknown_fact("tacacs.login", code: :vty_authentication_unknown) if @fact_builder.vtys.empty? || @fact_builder.vtys.any? { |vty| vty[:authentication].nil? }
|
|
214
|
+
used &&= @fact_builder.vtys.any? && @fact_builder.vtys.all? { |vty| %w[scheme aaa].include?(vty[:authentication]) }
|
|
171
215
|
end
|
|
172
|
-
|
|
216
|
+
record_fact("tacacs.#{key}", used, relevant) if @fact_builder.complete? || used
|
|
173
217
|
end
|
|
174
218
|
end
|
|
175
|
-
|
|
176
|
-
|
|
219
|
+
record_fact("tacacs.keys_present", schemes.values.all? { |entry| tacacs_keys_complete?(entry[:key_types]) }) unless schemes.empty?
|
|
220
|
+
parse_local_accounts
|
|
177
221
|
matching_blocks(/\A(?:line|user-interface)\b/).each do |section|
|
|
178
222
|
matching_blocks(/\A(?:set authentication )?password\b/, section.children).each do |entry|
|
|
179
223
|
match = /\A(?:set authentication )?password\s+(simple|cipher|hash|irreversible-cipher)\s+(.+)\z/.match(entry.text)
|
|
@@ -190,18 +234,18 @@ module Baseline
|
|
|
190
234
|
def authentication_domains
|
|
191
235
|
name = last_configured_value(/\Adomain default enable\s+(\S+)\z/)
|
|
192
236
|
unless name
|
|
193
|
-
%w[login authorization accounting].each { |kind|
|
|
237
|
+
%w[login authorization accounting].each { |kind| record_unknown_fact("tacacs.#{kind}", code: :authentication_domain_unknown) }
|
|
194
238
|
return []
|
|
195
239
|
end
|
|
196
240
|
matching_blocks(/\Adomain #{Regexp.escape(name)}\z/)
|
|
197
241
|
end
|
|
198
242
|
# 解析 VRP 认证域引用,避免将 VRP 语法应用到 Comware。
|
|
199
|
-
def
|
|
243
|
+
def domain_tacacs_references(_kind, _domains) = []
|
|
200
244
|
# 返回当前平台修改 SNMP 监听端口的命令模式。
|
|
201
245
|
def snmp_port_command = "snmp-agent port"
|
|
202
246
|
|
|
203
247
|
# 提取本地账号角色及凭据存储属性。
|
|
204
|
-
def
|
|
248
|
+
def parse_local_accounts
|
|
205
249
|
matching_blocks(/\Alocal-user\s+/).each do |section|
|
|
206
250
|
next if section.text.start_with?("local-user password-")
|
|
207
251
|
name = section.text.split[1]
|
|
@@ -210,7 +254,7 @@ module Baseline
|
|
|
210
254
|
match = /\Apassword\s+(simple|cipher|hash|irreversible-cipher)\s+(.+)\z/.match(entry.text)
|
|
211
255
|
record_credential(name, "local_user", match ? match[1] : "unknown", match ? match[2] : nil, entry)
|
|
212
256
|
end
|
|
213
|
-
record_credential(name, "local_user", "unknown", nil, section) if passwords.empty? && @
|
|
257
|
+
record_credential(name, "local_user", "unknown", nil, section) if passwords.empty? && @fact_builder.complete?
|
|
214
258
|
end
|
|
215
259
|
end
|
|
216
260
|
end
|
data/lib/baseline/evaluator.rb
CHANGED
|
@@ -38,22 +38,24 @@ module Baseline
|
|
|
38
38
|
return finding(attributes, :unknown, configuration.status == :unsupported ? :unsupported_platform : :input_incomplete)
|
|
39
39
|
end
|
|
40
40
|
return finding(attributes, :unknown, :scope_unknown) if applicable.nil?
|
|
41
|
-
resolution = @policy.
|
|
41
|
+
resolution = @policy.resolve_with_path(rule, context)
|
|
42
42
|
attributes[:policy_path] = resolution.path
|
|
43
43
|
return finding(attributes, :unknown, resolution.reason) if resolution.reason
|
|
44
44
|
expected = attributes[:expected] = resolution.value
|
|
45
|
-
unless configuration.
|
|
46
|
-
|
|
45
|
+
unless configuration.fact_known?(rule.fact)
|
|
46
|
+
unavailable = attributes[:diagnostics].any? { |issue| issue[:code] == :unsupported_capability && issue[:fact] == rule.fact }
|
|
47
|
+
reason = !configuration.complete? ? :input_incomplete : unavailable ? :unsupported_capability : :fact_unavailable
|
|
48
|
+
return finding(attributes, :unknown, reason)
|
|
47
49
|
end
|
|
48
|
-
actual = configuration.
|
|
50
|
+
actual = configuration.fact(rule.fact)
|
|
49
51
|
attributes[:actual] = actual
|
|
50
|
-
status, reason = compare(rule.operator, actual, expected)
|
|
52
|
+
status, reason, details = compare(rule.operator, actual, expected)
|
|
51
53
|
# 不完整输入不能证明通过;仅保留补充记录也无法推翻的明确违规。
|
|
52
54
|
if !configuration.complete? && (status == :pass || status == :fail && !conclusive_violation?(rule, actual, expected, configuration))
|
|
53
55
|
status, reason = :unknown, :input_incomplete
|
|
54
56
|
end
|
|
55
57
|
if status == :fail
|
|
56
|
-
attributes[:details] = differences(rule.operator, actual, expected)
|
|
58
|
+
attributes[:details] = details || differences(rule.operator, actual, expected)
|
|
57
59
|
# 尚未采集的配置可能补齐缺项,因此不将缺项作为已确认偏差展示。
|
|
58
60
|
attributes[:details].delete(:missing) unless configuration.complete?
|
|
59
61
|
end
|
|
@@ -88,7 +90,6 @@ module Baseline
|
|
|
88
90
|
when :set_equals then { missing: expected - actual, unexpected: actual - expected }
|
|
89
91
|
when :subset then { unexpected: actual - expected }
|
|
90
92
|
when :count_at_least then { minimum: expected, actual_count: actual.size }
|
|
91
|
-
when :networks_within then { outside_approved: Acl.unapproved_networks(actual, expected) }
|
|
92
93
|
when :all_disabled then { enabled: expected.select { |name| actual[name] == true } }
|
|
93
94
|
else {}
|
|
94
95
|
end
|
|
@@ -105,8 +106,8 @@ module Baseline
|
|
|
105
106
|
rule.applies_to.each do |key, expected|
|
|
106
107
|
values = key == "facts" ? expected : { key => expected }
|
|
107
108
|
values.each do |name, criterion|
|
|
108
|
-
known = key == "facts" ? configuration.
|
|
109
|
-
value = key == "facts" ? configuration.
|
|
109
|
+
known = key == "facts" ? configuration.applicability_fact_known?(name) : context.key?(name)
|
|
110
|
+
value = key == "facts" ? configuration.fact(name) : context[name]
|
|
110
111
|
if !known
|
|
111
112
|
unknown = true
|
|
112
113
|
elsif !(criterion.is_a?(Array) ? criterion.include?(value) : criterion == value)
|
|
@@ -135,24 +136,38 @@ module Baseline
|
|
|
135
136
|
when :subset then (actual - expected).empty?
|
|
136
137
|
when :set_equals then actual.sort == expected.sort
|
|
137
138
|
when :count_at_least then actual.size >= expected
|
|
138
|
-
when :networks_within
|
|
139
|
+
when :networks_within
|
|
140
|
+
outside = Acl.unapproved_networks(actual, expected)
|
|
141
|
+
details = { outside_approved: outside }
|
|
142
|
+
outside.empty?
|
|
139
143
|
end
|
|
140
144
|
end
|
|
141
|
-
[ passed ? :pass : :fail, passed ? nil : :deviation ]
|
|
145
|
+
[ passed ? :pass : :fail, passed ? nil : :deviation, details ]
|
|
142
146
|
rescue ArgumentError, IPAddr::InvalidAddressError
|
|
143
147
|
[ :unknown, :unsupported_address_family ]
|
|
144
148
|
end
|
|
145
149
|
|
|
146
150
|
# 检查可度量的凭据属性,不保存秘密值。
|
|
147
151
|
def password_strength(actual, expected)
|
|
148
|
-
return [ :unknown, :fact_type_mismatch ] unless actual.is_a?(Array)
|
|
149
|
-
|
|
152
|
+
return [ :unknown, :fact_type_mismatch ] unless actual.is_a?(Array)
|
|
153
|
+
valid = actual.select { |item| valid_strength_attributes?(item) }
|
|
154
|
+
visible = valid.select { |item| item[:storage] == "plaintext" }
|
|
150
155
|
weak = visible.any? do |item|
|
|
151
156
|
item[:common] || item.fetch(:length) < expected.fetch("min_length") || item.fetch(:character_classes) < expected.fetch("min_classes")
|
|
152
157
|
end
|
|
153
158
|
return [ :fail, :weak_password ] if weak
|
|
159
|
+
return [ :unknown, :fact_type_mismatch ] if valid.size != actual.size
|
|
154
160
|
return [ :unknown, :password_strength_unobservable ] if actual.empty? || visible.size != actual.size
|
|
155
161
|
[ :pass, nil ]
|
|
156
162
|
end
|
|
163
|
+
|
|
164
|
+
# 公开事实入口可接受外部解析结果;损坏属性不能冒充可比较的口令证据。
|
|
165
|
+
def valid_strength_attributes?(item)
|
|
166
|
+
return false unless item.is_a?(Hash) && %w[plaintext legacy protected unknown].include?(item[:storage])
|
|
167
|
+
return true unless item[:storage] == "plaintext"
|
|
168
|
+
|
|
169
|
+
[ true, false ].include?(item[:common]) && item[:length].is_a?(Integer) && item[:length] >= 0 &&
|
|
170
|
+
item[:character_classes].is_a?(Integer) && item[:character_classes].between?(0, 4)
|
|
171
|
+
end
|
|
157
172
|
end
|
|
158
173
|
end
|
|
@@ -4,7 +4,7 @@ require "ctx_parser/immutable"
|
|
|
4
4
|
|
|
5
5
|
module Baseline
|
|
6
6
|
# 在归一化配置中区分输入完整性、已知事实与局部不支持的语法。
|
|
7
|
-
class
|
|
7
|
+
class Evidence
|
|
8
8
|
PARTIAL_SCOPE_FACTS = { "snmp.enabled" => true, "snmp.community_present" => true, "credentials.present" => true,
|
|
9
9
|
"snmp.write_enabled" => true, "credentials.storage_safe" => false }.freeze
|
|
10
10
|
attr_reader :platform, :sha256, :status, :facts, :sources, :issues
|
|
@@ -27,10 +27,10 @@ module Baseline
|
|
|
27
27
|
# 判断配置采集与解析是否均已完成。
|
|
28
28
|
def complete? = input_complete? && status == :complete
|
|
29
29
|
# 读取已知值,同时保留事实缺失或未知的区别。
|
|
30
|
-
def
|
|
30
|
+
def fact(path) = facts[path]
|
|
31
31
|
|
|
32
32
|
# 区分明确已知的事实与缺失、不支持的配置证据。
|
|
33
|
-
def
|
|
33
|
+
def fact_known?(path)
|
|
34
34
|
facts.key?(path) && issues.none? do |issue|
|
|
35
35
|
scope = issue[:fact]
|
|
36
36
|
scope && (path == scope || path.start_with?("#{scope}."))
|
|
@@ -38,12 +38,12 @@ module Baseline
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# 局部输入可证明存在或明确违规,但不能证明缺失或所有成员均满足条件。
|
|
41
|
-
def
|
|
42
|
-
return false unless
|
|
41
|
+
def applicability_fact_known?(path)
|
|
42
|
+
return false unless fact_known?(path)
|
|
43
43
|
return true if complete?
|
|
44
44
|
return false unless status == :partial
|
|
45
45
|
|
|
46
|
-
path == "hostname" || PARTIAL_SCOPE_FACTS.key?(path) && PARTIAL_SCOPE_FACTS[path] ==
|
|
46
|
+
path == "hostname" || PARTIAL_SCOPE_FACTS.key?(path) && PARTIAL_SCOPE_FACTS[path] == fact(path)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
# 返回公开的数据结构,并保留状态与诊断信息。
|