baseline-rb 0.1.0 → 0.2.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 +24 -0
- data/README-zh.md +17 -1
- data/README.md +17 -1
- data/docs/architecture-zh.md +8 -8
- data/lib/baseline/acl.rb +44 -29
- data/lib/baseline/audit.rb +13 -4
- data/lib/baseline/{parsers/cisco.rb → cisco/parser.rb} +63 -61
- data/lib/baseline/{parsers/cisco_ios.rb → cisco_ios/parser.rb} +4 -2
- data/lib/baseline/{parsers/cisco_nxos.rb → cisco_nxos/parser.rb} +4 -2
- data/lib/baseline/{parsers/comware.rb → comware/parser.rb} +45 -43
- data/lib/baseline/configuration.rb +13 -6
- data/lib/baseline/credential.rb +3 -1
- data/lib/baseline/evaluator.rb +12 -5
- data/lib/baseline/facts.rb +18 -12
- data/lib/baseline/{parsers/h3c.rb → h3c/parser.rb} +4 -2
- data/lib/baseline/hillstone/document.rb +27 -0
- data/lib/baseline/{parsers/hillstone.rb → hillstone/parser.rb} +32 -30
- data/lib/baseline/{parsers/huawei.rb → huawei/parser.rb} +12 -10
- data/lib/baseline/palo_alto/document.rb +17 -0
- data/lib/baseline/{parsers/palo_alto.rb → palo_alto/parser.rb} +21 -13
- data/lib/baseline/parser.rb +274 -0
- data/lib/baseline/policy.rb +4 -2
- data/lib/baseline/report.rb +8 -3
- data/lib/baseline/rule.rb +13 -9
- data/lib/baseline/version.rb +1 -1
- data/lib/baseline.rb +0 -30
- data/policy.yml +1 -1
- metadata +16 -16
- data/lib/baseline/hillstone_document.rb +0 -25
- data/lib/baseline/palo_alto_document.rb +0 -15
- data/lib/baseline/parsers/base.rb +0 -256
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../cisco/parser"
|
|
4
|
+
|
|
3
5
|
module Baseline
|
|
4
|
-
module
|
|
6
|
+
module CiscoIos
|
|
5
7
|
# 解析 IOS、IOS-XE 的 VTY 协议和 AAA 登录列表。
|
|
6
|
-
class
|
|
8
|
+
class Parser < Cisco::Parser
|
|
7
9
|
private
|
|
8
10
|
|
|
9
11
|
# 选择共享 Cisco 解析逻辑中的 NX-OS 分支。
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../cisco/parser"
|
|
4
|
+
|
|
3
5
|
module Baseline
|
|
4
|
-
module
|
|
6
|
+
module CiscoNxos
|
|
5
7
|
# 解析 NX-OS 功能开关、角色权限及命名扩展 ACL。
|
|
6
|
-
class
|
|
8
|
+
class Parser < Cisco::Parser
|
|
7
9
|
private
|
|
8
10
|
|
|
9
11
|
# 选择共享 Cisco 解析逻辑中的 NX-OS 分支。
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../parser"
|
|
4
|
+
|
|
3
5
|
module Baseline
|
|
4
|
-
module
|
|
6
|
+
module Comware
|
|
5
7
|
# 复用 Comware 与 VRP 结构,由厂商子类选择账号、SSH 和 TACACS 语法。
|
|
6
|
-
class
|
|
8
|
+
class Parser < Baseline::Parser
|
|
7
9
|
private
|
|
8
10
|
|
|
9
11
|
# 解析管理访问暴露检查使用的有序 ACL 定义。
|
|
10
12
|
def access_lists
|
|
11
|
-
|
|
13
|
+
matching_blocks(/\Aacl\s+/).each do |section|
|
|
12
14
|
# IPv6 ACL 不能误建为名为 ipv6 的 IPv4 ACL,也不能覆盖其别名。
|
|
13
15
|
if /\Aacl\s+ipv6\b/.match?(section.text)
|
|
14
16
|
unknown("ipv6.acls", [ section ], code: :unsupported_acl_family)
|
|
@@ -30,13 +32,13 @@ module Baseline
|
|
|
30
32
|
protocol = %w[ip tcp udp icmp].include?(parts.first) ? parts.shift : "ip"
|
|
31
33
|
network = if parts.first == "source"
|
|
32
34
|
parts.shift
|
|
33
|
-
|
|
35
|
+
acl_source_network(parts)
|
|
34
36
|
else
|
|
35
37
|
"0.0.0.0/0"
|
|
36
38
|
end
|
|
37
39
|
if parts.first == "destination"
|
|
38
40
|
parts.shift
|
|
39
|
-
return unless
|
|
41
|
+
return unless acl_source_network(parts) == "0.0.0.0/0"
|
|
40
42
|
end
|
|
41
43
|
if parts.first == "destination-port"
|
|
42
44
|
return unless %w[tcp udp].include?(protocol)
|
|
@@ -54,56 +56,56 @@ module Baseline
|
|
|
54
56
|
|
|
55
57
|
# 根据监听服务、认证方式与 ACL 绑定关系归纳管理访问事实。
|
|
56
58
|
def management
|
|
57
|
-
record("ssh.enabled",
|
|
58
|
-
compatible =
|
|
59
|
+
record("ssh.enabled", configured_state(ssh_enable, ssh_disable), matching_blocks(/\A(?:undo )?(?:ssh|stelnet)/))
|
|
60
|
+
compatible = configured_state(/\Assh server compatible-ssh1x enable\z/, /\A(?:undo ssh server compatible-ssh1x enable|ssh server compatible-ssh1x disable)\z/)
|
|
59
61
|
record("ssh.versions", compatible.nil? ? nil : compatible ? [ "1", "2" ] : [ "2" ])
|
|
60
|
-
ssh_acl =
|
|
61
|
-
defaults =
|
|
62
|
-
vtys =
|
|
62
|
+
ssh_acl = last_configured_value(/\Assh server acl(?: ipv4)?\s+(?:name\s+)?(\S+)\z/)
|
|
63
|
+
defaults = matching_blocks(/\Aline class vty\z/).flat_map(&:children)
|
|
64
|
+
vtys = matching_blocks(/\A(?:line|user-interface) vty\b/).map do |section|
|
|
63
65
|
entries = defaults + section.children
|
|
64
|
-
{ name: section.text, protocols:
|
|
65
|
-
authentication:
|
|
66
|
-
acl:
|
|
66
|
+
{ name: section.text, protocols: last_configured_value(/\Aprotocol inbound\s+(.+)\z/, entries)&.split,
|
|
67
|
+
authentication: last_configured_value(/\Aauthentication-mode\s+(\S+)\z/, entries),
|
|
68
|
+
acl: last_configured_value(/\Aacl\s+(?:name\s+)?(\S+)(?:\s+inbound)?\z/, entries), line: section.line }
|
|
67
69
|
end
|
|
68
70
|
@facts.management(vtys: vtys, ssh_acl: ssh_acl,
|
|
69
71
|
ssh_port: listener_port("ssh server port", default: 22, scope: "management.acl"),
|
|
70
72
|
telnet_port: listener_port("telnet server port", default: 23, scope: "management.acl"))
|
|
71
73
|
definitions = { "telnet" => "telnet server", "http" => "ip http", "https" => "ip https", "ftp" => "ftp server", "tftp" => "tftp server" }
|
|
72
74
|
record("services", definitions.to_h do |name, command|
|
|
73
|
-
[ name,
|
|
75
|
+
[ name, configured_state(/\A#{command} enable\z/, /\A(?:undo #{command} enable|#{command} disable)\z/) ]
|
|
74
76
|
end)
|
|
75
77
|
end
|
|
76
78
|
|
|
77
79
|
# 采集明确启用或禁用的管理服务。
|
|
78
80
|
def services
|
|
79
|
-
record("ntp.servers",
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
record("ntp.servers", configured_values(/\Antp-service unicast-server\s+(\S+)/), matching_blocks(/\Antp-service unicast-server/))
|
|
82
|
+
record_configured_state("ntp.authentication", /\Antp-service authentication enable\z/, /\Aundo ntp-service authentication enable\z/)
|
|
83
|
+
record_configured_value("ntp.source", /\Antp-service source(?:-interface)?\s+(\S+)\z/)
|
|
82
84
|
# 从日志服务器中排除 source 子命令,并保留评估所需的归纳事实。
|
|
83
|
-
hosts =
|
|
84
|
-
record("syslog.servers", hosts,
|
|
85
|
-
|
|
86
|
-
enabled =
|
|
85
|
+
hosts = configured_values(/\Ainfo-center loghost\s+(\S+)/) - [ "source" ]
|
|
86
|
+
record("syslog.servers", hosts, matching_blocks(/\Ainfo-center loghost/))
|
|
87
|
+
record_configured_value("syslog.source", /\Ainfo-center loghost source\s+(\S+)\z/)
|
|
88
|
+
enabled = configured_state(/\Asnmp-agent(?:\s|\z)/, /\Aundo snmp-agent\z/)
|
|
87
89
|
record("snmp.enabled", enabled.nil? && @facts.complete? ? false : enabled)
|
|
88
|
-
versions =
|
|
90
|
+
versions = last_configured_value(/\Asnmp-agent sys-info version\s+(.+)\z/)&.split
|
|
89
91
|
versions = %w[v1 v2c v3] if versions == [ "all" ]
|
|
90
92
|
record("snmp.versions", versions)
|
|
91
|
-
communities =
|
|
93
|
+
communities = matching_blocks(/\Asnmp-agent community\b/).filter_map do |entry|
|
|
92
94
|
match = /\Asnmp-agent community (read|write)\s+(?:(simple|cipher)\s+)?(\S+)(.*)\z/.match(entry.text)
|
|
93
95
|
unless match
|
|
94
96
|
unknown_snmp_access([ entry ])
|
|
95
97
|
next
|
|
96
98
|
end
|
|
97
|
-
|
|
99
|
+
record_credential("snmp-community", "snmp", match[2] || "plain", match[3], entry)
|
|
98
100
|
{ write: match[1] == "write", acl: match[4][/\bacl\s+(?:name\s+)?(\S+)/, 1], line: entry.line }
|
|
99
101
|
end
|
|
100
|
-
groups = snmp_groups(
|
|
102
|
+
groups = snmp_groups(matching_blocks(/\Asnmp-agent group v3\s+/)) do |entry|
|
|
101
103
|
settings = entry.text.sub(/\Asnmp-agent group v3\s+\S+\s*/, "")
|
|
102
104
|
[ entry.text.split[3], { acl: entry.text[/\bacl\s+(?:name\s+)?(\S+)/, 1], line: entry.line, write: snmp_group_write(settings) } ]
|
|
103
105
|
end
|
|
104
106
|
record("snmp.groups", groups)
|
|
105
107
|
users = {}
|
|
106
|
-
|
|
108
|
+
matching_blocks(/\Asnmp-agent usm-user\b/).each do |entry|
|
|
107
109
|
match = /\Asnmp-agent usm-user v3\s+(\S+)(?:\s+(.*))?\z/.match(entry.text)
|
|
108
110
|
unless match
|
|
109
111
|
unknown_snmp_access([ entry ], code: :unsupported_snmp_user)
|
|
@@ -136,15 +138,15 @@ module Baseline
|
|
|
136
138
|
|
|
137
139
|
# 解析实际生效的认证方法及外部服务器引用。
|
|
138
140
|
def authentication
|
|
139
|
-
schemes =
|
|
140
|
-
server_types =
|
|
141
|
-
.transform_values { |entries|
|
|
141
|
+
schemes = matching_blocks(tacacs_section).to_h do |section|
|
|
142
|
+
server_types = matching_blocks(tacacs_server, section.children).group_by { |entry| entry.text.split[1] }
|
|
143
|
+
.transform_values { |entries| configured_values(tacacs_server, entries) }
|
|
142
144
|
servers = server_types.values.flatten.uniq
|
|
143
145
|
keys = section.children.filter_map do |entry|
|
|
144
146
|
match = tacacs_key.match(entry.text)
|
|
145
147
|
next unless match
|
|
146
148
|
kind, format, secret = match.captures
|
|
147
|
-
|
|
149
|
+
record_credential("tacacs:#{section.text.split.last}", "tacacs", format, secret, entry)
|
|
148
150
|
kind
|
|
149
151
|
end
|
|
150
152
|
[ section.text.split.last, { servers: servers, server_types: server_types, key_types: keys.uniq, line: section.line } ]
|
|
@@ -154,8 +156,8 @@ module Baseline
|
|
|
154
156
|
domains = authentication_domains
|
|
155
157
|
methods = domains.flat_map { |entry| entry.children }
|
|
156
158
|
%w[authentication authorization accounting].each do |kind|
|
|
157
|
-
relevant =
|
|
158
|
-
relevant =
|
|
159
|
+
relevant = matching_blocks(/\A#{kind}\s+login\b/, methods)
|
|
160
|
+
relevant = matching_blocks(/\A#{kind}\s+default\b/, methods) if relevant.empty?
|
|
159
161
|
refs = relevant.filter_map { |entry| entry.text[/\A#{kind}\s+(?:login|default)\s+hwtacacs-scheme\s+(\S+)/, 1] }
|
|
160
162
|
refs.concat(vrp_tacacs_references(kind, domains))
|
|
161
163
|
key = kind == "authentication" ? "login" : kind
|
|
@@ -172,26 +174,26 @@ module Baseline
|
|
|
172
174
|
end
|
|
173
175
|
record("tacacs.keys_present", schemes.values.all? { |entry| tacacs_keys_complete?(entry[:key_types]) }) unless schemes.empty?
|
|
174
176
|
accounts
|
|
175
|
-
|
|
176
|
-
|
|
177
|
+
matching_blocks(/\A(?:line|user-interface)\b/).each do |section|
|
|
178
|
+
matching_blocks(/\A(?:set authentication )?password\b/, section.children).each do |entry|
|
|
177
179
|
match = /\A(?:set authentication )?password\s+(simple|cipher|hash|irreversible-cipher)\s+(.+)\z/.match(entry.text)
|
|
178
|
-
|
|
180
|
+
record_credential(section.text, "line", match ? match[1] : "unknown", match&.[](2), entry)
|
|
179
181
|
end
|
|
180
182
|
end
|
|
181
|
-
|
|
183
|
+
matching_blocks(/\Asuper password\b/).each do |entry|
|
|
182
184
|
match = /\Asuper password(?: level \d+)?\s+(simple|cipher|hash)\s+(.+)\z/.match(entry.text)
|
|
183
|
-
|
|
185
|
+
record_credential("super", "enable", match ? match[1] : "unknown", match&.[](2), entry)
|
|
184
186
|
end
|
|
185
187
|
end
|
|
186
188
|
|
|
187
189
|
# 解析认证域引用及其服务器组。
|
|
188
190
|
def authentication_domains
|
|
189
|
-
name =
|
|
191
|
+
name = last_configured_value(/\Adomain default enable\s+(\S+)\z/)
|
|
190
192
|
unless name
|
|
191
193
|
%w[login authorization accounting].each { |kind| unknown("tacacs.#{kind}", code: :authentication_domain_unknown) }
|
|
192
194
|
return []
|
|
193
195
|
end
|
|
194
|
-
|
|
196
|
+
matching_blocks(/\Adomain #{Regexp.escape(name)}\z/)
|
|
195
197
|
end
|
|
196
198
|
# 解析 VRP 认证域引用,避免将 VRP 语法应用到 Comware。
|
|
197
199
|
def vrp_tacacs_references(_kind, _domains) = []
|
|
@@ -200,15 +202,15 @@ module Baseline
|
|
|
200
202
|
|
|
201
203
|
# 提取本地账号角色及凭据存储属性。
|
|
202
204
|
def accounts
|
|
203
|
-
|
|
205
|
+
matching_blocks(/\Alocal-user\s+/).each do |section|
|
|
204
206
|
next if section.text.start_with?("local-user password-")
|
|
205
207
|
name = section.text.split[1]
|
|
206
|
-
passwords =
|
|
208
|
+
passwords = matching_blocks(/\Apassword\s+/, section.children)
|
|
207
209
|
passwords.each do |entry|
|
|
208
210
|
match = /\Apassword\s+(simple|cipher|hash|irreversible-cipher)\s+(.+)\z/.match(entry.text)
|
|
209
|
-
|
|
211
|
+
record_credential(name, "local_user", match ? match[1] : "unknown", match ? match[2] : nil, entry)
|
|
210
212
|
end
|
|
211
|
-
|
|
213
|
+
record_credential(name, "local_user", "unknown", nil, section) if passwords.empty? && @facts.complete?
|
|
212
214
|
end
|
|
213
215
|
end
|
|
214
216
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "ctx_parser/immutable"
|
|
4
|
+
|
|
3
5
|
module Baseline
|
|
4
6
|
# 在归一化配置中区分输入完整性、已知事实与局部不支持的语法。
|
|
5
7
|
class Configuration
|
|
@@ -8,17 +10,22 @@ module Baseline
|
|
|
8
10
|
attr_reader :platform, :sha256, :status, :facts, :sources, :issues
|
|
9
11
|
|
|
10
12
|
# 冻结配置事实及其平台、来源身份。
|
|
11
|
-
def initialize(platform:, sha256:, result:)
|
|
13
|
+
def initialize(platform:, sha256:, result:, input_complete: result.status == :complete)
|
|
14
|
+
raise ArgumentError, "Input completeness must be boolean" unless input_complete == true || input_complete == false
|
|
15
|
+
@input_complete = input_complete
|
|
12
16
|
@platform, @sha256, @status = platform.dup.freeze, sha256.dup.freeze, result.status
|
|
13
17
|
record = result.records.first || {}
|
|
14
|
-
@facts =
|
|
15
|
-
@sources =
|
|
16
|
-
@issues =
|
|
18
|
+
@facts = CtxParser::Immutable.copy(record.fetch("facts", {}))
|
|
19
|
+
@sources = CtxParser::Immutable.copy(record.fetch("sources", {}))
|
|
20
|
+
@issues = CtxParser::Immutable.copy(result.issues)
|
|
17
21
|
freeze
|
|
18
22
|
end
|
|
19
23
|
|
|
24
|
+
# 仅记录调用方是否确认完整接收;局部解析不支持仍会降低 complete?。
|
|
25
|
+
def input_complete? = @input_complete
|
|
26
|
+
|
|
20
27
|
# 判断配置采集与解析是否均已完成。
|
|
21
|
-
def complete? = status == :complete
|
|
28
|
+
def complete? = input_complete? && status == :complete
|
|
22
29
|
# 读取已知值,同时保留事实缺失或未知的区别。
|
|
23
30
|
def value(path) = facts[path]
|
|
24
31
|
|
|
@@ -41,7 +48,7 @@ module Baseline
|
|
|
41
48
|
|
|
42
49
|
# 返回公开的数据结构,并保留状态与诊断信息。
|
|
43
50
|
def to_h
|
|
44
|
-
{ platform: platform, sha256: sha256, status: status, facts: facts, sources: sources, issues: issues }
|
|
51
|
+
{ platform: platform, sha256: sha256, status: status, input_complete: input_complete?, facts: facts, sources: sources, issues: issues }
|
|
45
52
|
end
|
|
46
53
|
|
|
47
54
|
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
data/lib/baseline/credential.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "ctx_parser/immutable"
|
|
4
|
+
|
|
3
5
|
module Baseline
|
|
4
6
|
# 仅在构造时分析凭据,不保留明文、密文或可枚举的摘要。
|
|
5
7
|
class Credential
|
|
@@ -20,7 +22,7 @@ module Baseline
|
|
|
20
22
|
values.merge!(length: text.length, character_classes: [ /[a-z]/, /[A-Z]/, /\d/, /[^a-zA-Z\d]/ ].count { |pattern| pattern.match?(text) },
|
|
21
23
|
common: COMMON.include?(text.downcase) || text.downcase == owner.to_s.downcase || /\A(.)\1+\z/.match?(text))
|
|
22
24
|
end
|
|
23
|
-
@attributes =
|
|
25
|
+
@attributes = CtxParser::Immutable.copy(values)
|
|
24
26
|
freeze
|
|
25
27
|
end
|
|
26
28
|
|
data/lib/baseline/evaluator.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "policy"
|
|
4
|
+
require_relative "report"
|
|
5
|
+
|
|
3
6
|
module Baseline
|
|
4
7
|
# 根据策略评估归一化事实;厂商语法差异由解析器负责。
|
|
5
8
|
class Evaluator
|
|
@@ -12,6 +15,9 @@ module Baseline
|
|
|
12
15
|
# 评估适用基线规则,分别报告合规结果与证据覆盖率。
|
|
13
16
|
def call(configuration:, context: {})
|
|
14
17
|
raise ArgumentError, "Invalid baseline context" unless context.is_a?(Hash)
|
|
18
|
+
unless context.keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) } && context.keys.map(&:to_s).uniq.size == context.size
|
|
19
|
+
raise ArgumentError, "Ambiguous baseline context"
|
|
20
|
+
end
|
|
15
21
|
context = context.transform_keys(&:to_s).merge("vendor" => configuration.platform)
|
|
16
22
|
unless (context.keys - Policy::SCOPES).empty? && context.values.all? { |value| value.is_a?(String) && !value.strip.empty? }
|
|
17
23
|
raise ArgumentError, "Invalid baseline context"
|
|
@@ -43,7 +49,7 @@ module Baseline
|
|
|
43
49
|
attributes[:actual] = actual
|
|
44
50
|
status, reason = compare(rule.operator, actual, expected)
|
|
45
51
|
# 不完整输入不能证明通过;仅保留补充记录也无法推翻的明确违规。
|
|
46
|
-
if !configuration.complete? && (status == :pass || status == :fail && !
|
|
52
|
+
if !configuration.complete? && (status == :pass || status == :fail && !conclusive_violation?(rule, actual, expected, configuration))
|
|
47
53
|
status, reason = :unknown, :input_incomplete
|
|
48
54
|
end
|
|
49
55
|
if status == :fail
|
|
@@ -55,9 +61,10 @@ module Baseline
|
|
|
55
61
|
end
|
|
56
62
|
|
|
57
63
|
# 采集尚未完成时,仅报告后续缺失记录也无法推翻的明确违规。
|
|
58
|
-
def
|
|
64
|
+
def conclusive_violation?(rule, actual, expected, configuration)
|
|
59
65
|
case rule.operator
|
|
60
|
-
when :all_disabled, :subset, :
|
|
66
|
+
when :all_disabled, :subset, :password_strength then true
|
|
67
|
+
when :networks_within then configuration.input_complete?
|
|
61
68
|
when :set_equals then (actual - expected).any?
|
|
62
69
|
when :equals
|
|
63
70
|
rule.fact == "snmp.write_enabled" && actual == true && expected == false ||
|
|
@@ -81,7 +88,7 @@ module Baseline
|
|
|
81
88
|
when :set_equals then { missing: expected - actual, unexpected: actual - expected }
|
|
82
89
|
when :subset then { unexpected: actual - expected }
|
|
83
90
|
when :count_at_least then { minimum: expected, actual_count: actual.size }
|
|
84
|
-
when :networks_within then { outside_approved:
|
|
91
|
+
when :networks_within then { outside_approved: Acl.unapproved_networks(actual, expected) }
|
|
85
92
|
when :all_disabled then { enabled: expected.select { |name| actual[name] == true } }
|
|
86
93
|
else {}
|
|
87
94
|
end
|
|
@@ -128,7 +135,7 @@ module Baseline
|
|
|
128
135
|
when :subset then (actual - expected).empty?
|
|
129
136
|
when :set_equals then actual.sort == expected.sort
|
|
130
137
|
when :count_at_least then actual.size >= expected
|
|
131
|
-
when :networks_within then Acl.
|
|
138
|
+
when :networks_within then Acl.networks_within?(actual, expected)
|
|
132
139
|
end
|
|
133
140
|
end
|
|
134
141
|
[ passed ? :pass : :fail, passed ? nil : :deviation ]
|
data/lib/baseline/facts.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "acl"
|
|
4
|
+
require_relative "credential"
|
|
5
|
+
|
|
3
6
|
require "ctx_parser/result"
|
|
4
7
|
|
|
5
8
|
module Baseline
|
|
@@ -13,6 +16,7 @@ module Baseline
|
|
|
13
16
|
@complete = complete && issues.empty?
|
|
14
17
|
@values, @sources, @issues, @acls = {}, {}, issues.dup, {}
|
|
15
18
|
@vtys, @communities, @snmp_users, @credentials = [], [], [], []
|
|
19
|
+
@credential_lines = {}
|
|
16
20
|
@snmp_groups = {}
|
|
17
21
|
@snmp_permission_issues = []
|
|
18
22
|
@ssh_acl = nil
|
|
@@ -36,7 +40,7 @@ module Baseline
|
|
|
36
40
|
(sources.nil? || sources.is_a?(Array))
|
|
37
41
|
raise ArgumentError, "Direct access requires a known scope, binding state and source networks"
|
|
38
42
|
end
|
|
39
|
-
networks = sources && Acl.
|
|
43
|
+
networks = sources && Acl.network_prefixes(sources.map { |source| Acl.address_range(source) })
|
|
40
44
|
@direct_acls[fact] = true
|
|
41
45
|
record("#{fact}.bound", bound, lines: lines)
|
|
42
46
|
if networks
|
|
@@ -51,7 +55,7 @@ module Baseline
|
|
|
51
55
|
def record(path, value, lines: [])
|
|
52
56
|
return false if value.nil? || (!complete? && value.respond_to?(:empty?) && value.empty?)
|
|
53
57
|
|
|
54
|
-
@values[path] =
|
|
58
|
+
@values[path] = CtxParser::Immutable.copy(value)
|
|
55
59
|
@sources[path] = lines.uniq unless lines.empty?
|
|
56
60
|
true
|
|
57
61
|
end
|
|
@@ -72,15 +76,15 @@ module Baseline
|
|
|
72
76
|
|
|
73
77
|
# 根据监听服务、认证方式与 ACL 绑定关系归纳管理访问事实。
|
|
74
78
|
def management(vtys:, ssh_acl: nil, ssh_port: 22, telnet_port: 23)
|
|
75
|
-
@vtys, @ssh_acl =
|
|
79
|
+
@vtys, @ssh_acl = CtxParser::Immutable.copy(vtys), CtxParser::Immutable.copy(ssh_acl)
|
|
76
80
|
@management_ports = { "ssh" => ssh_port, "stelnet" => ssh_port, "telnet" => telnet_port }
|
|
77
81
|
end
|
|
78
82
|
|
|
79
83
|
# 根据用户、组和团体字归纳 SNMP 暴露面及凭据事实。
|
|
80
84
|
def snmp(communities:, users:, groups: nil, port: 161)
|
|
81
85
|
@snmp_port = port
|
|
82
|
-
@communities =
|
|
83
|
-
@snmp_groups =
|
|
86
|
+
@communities = CtxParser::Immutable.copy(communities)
|
|
87
|
+
@snmp_groups = CtxParser::Immutable.copy(groups || {})
|
|
84
88
|
@snmp_users = users.map do |user|
|
|
85
89
|
if groups && user[:group]
|
|
86
90
|
if groups.key?(user[:group])
|
|
@@ -94,7 +98,7 @@ module Baseline
|
|
|
94
98
|
unknown("snmp.acl", code: :snmp_group_unresolved)
|
|
95
99
|
end
|
|
96
100
|
end
|
|
97
|
-
|
|
101
|
+
CtxParser::Immutable.copy(user)
|
|
98
102
|
end
|
|
99
103
|
end
|
|
100
104
|
|
|
@@ -112,12 +116,14 @@ module Baseline
|
|
|
112
116
|
end
|
|
113
117
|
|
|
114
118
|
# 检查此来源行是否已经产生凭据事实。
|
|
115
|
-
def credential_recorded?(line) = @
|
|
119
|
+
def credential_recorded?(line) = @credential_lines.key?(line)
|
|
116
120
|
|
|
117
121
|
# 记录凭据属性,不保留凭据秘密值。
|
|
118
|
-
def
|
|
122
|
+
def record_credential(credential)
|
|
119
123
|
index = @credentials.size
|
|
120
|
-
|
|
124
|
+
attributes = credential.to_h
|
|
125
|
+
@credentials << attributes
|
|
126
|
+
@credential_lines[attributes[:line]] = true
|
|
121
127
|
index
|
|
122
128
|
end
|
|
123
129
|
|
|
@@ -131,7 +137,7 @@ module Baseline
|
|
|
131
137
|
@issues << { code: :input_not_confirmed_complete } unless @confirmed_complete
|
|
132
138
|
status = complete? && @values.key?("hostname") ? :complete : :partial
|
|
133
139
|
CtxParser::Result.new(status: status,
|
|
134
|
-
records:
|
|
140
|
+
records: CtxParser::Immutable.copy([ { "facts" => @values, "sources" => @sources } ]), issues: CtxParser::Immutable.copy(@issues.uniq))
|
|
135
141
|
end
|
|
136
142
|
|
|
137
143
|
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
|
@@ -212,7 +218,7 @@ module Baseline
|
|
|
212
218
|
acl = @acls[name]
|
|
213
219
|
duplicate = @issues.any? { |issue| issue[:fact] == "acls.#{name}" }
|
|
214
220
|
next if !acl || duplicate
|
|
215
|
-
sources = (binding[:ports] || [ nil ]).map { |port| acl.
|
|
221
|
+
sources = (binding[:ports] || [ nil ]).map { |port| acl.permitted_source_networks(protocol: protocol, port: port) }
|
|
216
222
|
sources.flatten unless sources.any?(&:nil?)
|
|
217
223
|
end
|
|
218
224
|
if resolved.any?(&:nil?)
|
|
@@ -220,7 +226,7 @@ module Baseline
|
|
|
220
226
|
unknown("#{fact}.restricted", code: :acl_unresolved)
|
|
221
227
|
return
|
|
222
228
|
end
|
|
223
|
-
sources = Acl.
|
|
229
|
+
sources = Acl.network_prefixes(resolved.flatten.uniq.map { |cidr| Acl.address_range(cidr) })
|
|
224
230
|
record("#{fact}.sources", sources)
|
|
225
231
|
record("#{fact}.restricted", !sources.include?("0.0.0.0/0"))
|
|
226
232
|
lines = names.uniq.flat_map { |name| @acls.fetch(name).rules.map { |rule| rule[:line] } }.compact.uniq
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../comware/parser"
|
|
4
|
+
|
|
3
5
|
module Baseline
|
|
4
|
-
module
|
|
6
|
+
module H3c
|
|
5
7
|
# 解析 H3C Comware 的 SSH、域认证及三类 TACACS 密钥角色。
|
|
6
|
-
class
|
|
8
|
+
class Parser < Comware::Parser
|
|
7
9
|
private
|
|
8
10
|
|
|
9
11
|
# 匹配当前平台显式启用 SSH 的命令。
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../quoted_document"
|
|
4
|
+
|
|
5
|
+
module Baseline
|
|
6
|
+
module Hillstone
|
|
7
|
+
# StoneOS 导出内容可能包含命令回显、显式退出及引号包裹的界面状态。
|
|
8
|
+
class Document < QuotedDocument
|
|
9
|
+
protected
|
|
10
|
+
|
|
11
|
+
# 区分配置结构行与引号、横幅中的文本内容。
|
|
12
|
+
def classify(line)
|
|
13
|
+
unless @quote
|
|
14
|
+
return :ignored if line.text.match?(/\A(?:show (?:configuration|config)|Running configuration|Version\b|[\w.():-]+#\s*\z)/)
|
|
15
|
+
return :ignored if line.text == "exit"
|
|
16
|
+
return :boundary if line.text == "End"
|
|
17
|
+
end
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# 识别配置值可能跨行延续的语句。
|
|
22
|
+
def multiline_fact?(text)
|
|
23
|
+
text.match?(/\A(?:hostname|admin host|snmp-server|ntp|logging syslog|password|secret|host|backup[12])\b/)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|