baseline-rb 0.1.0 → 0.2.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 +20 -0
- data/README-zh.md +15 -1
- data/README.md +15 -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 +9 -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} +30 -28
- 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} +12 -10
- data/lib/baseline/parser.rb +260 -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
data/lib/baseline/rule.rb
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "acl"
|
|
4
|
+
|
|
5
|
+
require "ctx_parser/immutable"
|
|
6
|
+
|
|
3
7
|
module Baseline
|
|
4
8
|
# 规则声明事实、比较方式和批准值类型;厂商语法仅由解析器负责。
|
|
5
9
|
class Rule
|
|
@@ -7,15 +11,15 @@ module Baseline
|
|
|
7
11
|
Definition = Data.define(:title, :fact, :operator, :value_type, :allowed_values) do
|
|
8
12
|
# 冻结规则目录条目的事实、比较方式及批准值类型。
|
|
9
13
|
def initialize(title:, fact:, operator:, value_type:, allowed_values: nil)
|
|
10
|
-
super(title:
|
|
11
|
-
value_type:
|
|
14
|
+
super(title: CtxParser::Immutable.copy(title), fact: CtxParser::Immutable.copy(fact), operator: CtxParser::Immutable.copy(operator),
|
|
15
|
+
value_type: CtxParser::Immutable.copy(value_type), allowed_values: CtxParser::Immutable.copy(allowed_values))
|
|
12
16
|
end
|
|
13
17
|
end
|
|
14
18
|
|
|
15
19
|
DEFINITIONS = {
|
|
16
20
|
"snmp.enabled" => Definition.new(title: "SNMP 配置", fact: "snmp.enabled", operator: :equals, value_type: :boolean),
|
|
17
21
|
"snmp.versions" => Definition.new(title: "SNMP 允许版本", fact: "snmp.versions", operator: :subset, value_type: :enum_set, allowed_values: %w[v1 v2c v3]),
|
|
18
|
-
"snmp.
|
|
22
|
+
"snmp.write_enabled" => Definition.new(title: "SNMP 写权限", fact: "snmp.write_enabled", operator: :equals, value_type: :boolean),
|
|
19
23
|
"snmp.auth_priv" => Definition.new(title: "SNMPv3 认证与加密", fact: "snmp.v3_auth_priv", operator: :equals, value_type: :boolean),
|
|
20
24
|
"snmp.acl_bound" => Definition.new(title: "SNMP 来源 ACL 引用", fact: "snmp.acl.bound", operator: :equals, value_type: :boolean),
|
|
21
25
|
"snmp.acl_restricted" => Definition.new(title: "SNMP 来源 ACL 限制", fact: "snmp.acl.restricted", operator: :equals, value_type: :boolean),
|
|
@@ -35,7 +39,7 @@ module Baseline
|
|
|
35
39
|
"management.acl_restricted" => Definition.new(title: "管理来源 ACL 限制", fact: "management.acl.restricted", operator: :equals, value_type: :boolean),
|
|
36
40
|
"management.sources" => Definition.new(title: "管理批准来源", fact: "management.acl.sources", operator: :networks_within, value_type: :network_set),
|
|
37
41
|
"services.disabled" => Definition.new(title: "关闭非必要服务", fact: "services", operator: :all_disabled, value_type: :enum_set, allowed_values: %w[telnet http https ftp tftp finger]),
|
|
38
|
-
"credentials.storage" => Definition.new(title: "
|
|
42
|
+
"credentials.storage" => Definition.new(title: "凭据安全存储", fact: "credentials.storage_safe", operator: :equals, value_type: :boolean),
|
|
39
43
|
"credentials.strength" => Definition.new(title: "可观察口令强度", fact: "credentials.records", operator: :password_strength, value_type: :password_strength),
|
|
40
44
|
"tacacs.minimum_servers" => Definition.new(title: "TACACS 服务器数量", fact: "tacacs.servers", operator: :count_at_least, value_type: :positive_integer),
|
|
41
45
|
"tacacs.servers" => Definition.new(title: "TACACS 批准服务器", fact: "tacacs.servers", operator: :set_equals, value_type: :string_set),
|
|
@@ -60,12 +64,12 @@ module Baseline
|
|
|
60
64
|
%w[low medium high critical].include?(options.fetch("severity", "medium"))
|
|
61
65
|
raise ArgumentError, "Invalid baseline rule options"
|
|
62
66
|
end
|
|
63
|
-
@id =
|
|
67
|
+
@id = CtxParser::Immutable.copy(id)
|
|
64
68
|
@title, @fact, @operator = definition.title, definition.fact, definition.operator
|
|
65
69
|
@value_type, @allowed_values = definition.value_type, definition.allowed_values
|
|
66
|
-
@expected =
|
|
67
|
-
@enabled, @severity = options.fetch("enabled", true),
|
|
68
|
-
@applies_to =
|
|
70
|
+
@expected = CtxParser::Immutable.copy(options["expected"])
|
|
71
|
+
@enabled, @severity = options.fetch("enabled", true), CtxParser::Immutable.copy(options.fetch("severity", "medium"))
|
|
72
|
+
@applies_to = CtxParser::Immutable.copy(options.fetch("applies_to", {}))
|
|
69
73
|
validate_applicability!
|
|
70
74
|
freeze
|
|
71
75
|
end
|
|
@@ -86,7 +90,7 @@ module Baseline
|
|
|
86
90
|
when :string_set, :network_set then string_set?(value)
|
|
87
91
|
end
|
|
88
92
|
raise ArgumentError, "Invalid baseline expected value for #{id}" unless valid
|
|
89
|
-
value.each { |network| Acl.
|
|
93
|
+
value.each { |network| Acl.address_range(network) } if value_type == :network_set
|
|
90
94
|
end
|
|
91
95
|
|
|
92
96
|
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
data/lib/baseline/version.rb
CHANGED
data/lib/baseline.rb
CHANGED
|
@@ -1,34 +1,4 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "baseline/version"
|
|
4
|
-
|
|
5
|
-
require "ctx_parser/immutable"
|
|
6
|
-
|
|
7
|
-
module Baseline
|
|
8
|
-
# 复制并冻结可序列化的基线事实,避免调用方修改策略或报告状态。
|
|
9
|
-
def self.immutable(value)
|
|
10
|
-
CtxParser::Immutable.copy(value)
|
|
11
|
-
rescue ArgumentError
|
|
12
|
-
raise ArgumentError, "Baseline values must be plain data"
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
require_relative "baseline/configuration"
|
|
17
|
-
require_relative "baseline/document"
|
|
18
|
-
require_relative "baseline/acl"
|
|
19
|
-
require_relative "baseline/credential"
|
|
20
|
-
require_relative "baseline/facts"
|
|
21
|
-
require_relative "baseline/parsers/base"
|
|
22
|
-
require_relative "baseline/parsers/comware"
|
|
23
|
-
require_relative "baseline/parsers/h3c"
|
|
24
|
-
require_relative "baseline/parsers/huawei"
|
|
25
|
-
require_relative "baseline/parsers/cisco"
|
|
26
|
-
require_relative "baseline/parsers/cisco_ios"
|
|
27
|
-
require_relative "baseline/parsers/cisco_nxos"
|
|
28
|
-
require_relative "baseline/parsers/hillstone"
|
|
29
|
-
require_relative "baseline/parsers/palo_alto"
|
|
30
|
-
require_relative "baseline/rule"
|
|
31
|
-
require_relative "baseline/policy"
|
|
32
|
-
require_relative "baseline/report"
|
|
33
|
-
require_relative "baseline/evaluator"
|
|
34
4
|
require_relative "baseline/audit"
|
data/policy.yml
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: baseline-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gatework contributors
|
|
@@ -13,16 +13,16 @@ dependencies:
|
|
|
13
13
|
name: ctx_parser
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
18
|
+
version: 0.2.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
|
-
- - "
|
|
23
|
+
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
25
|
+
version: 0.2.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: digest
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -109,22 +109,22 @@ files:
|
|
|
109
109
|
- lib/baseline.rb
|
|
110
110
|
- lib/baseline/acl.rb
|
|
111
111
|
- lib/baseline/audit.rb
|
|
112
|
+
- lib/baseline/cisco/parser.rb
|
|
113
|
+
- lib/baseline/cisco_ios/parser.rb
|
|
114
|
+
- lib/baseline/cisco_nxos/parser.rb
|
|
115
|
+
- lib/baseline/comware/parser.rb
|
|
112
116
|
- lib/baseline/configuration.rb
|
|
113
117
|
- lib/baseline/credential.rb
|
|
114
118
|
- lib/baseline/document.rb
|
|
115
119
|
- lib/baseline/evaluator.rb
|
|
116
120
|
- lib/baseline/facts.rb
|
|
117
|
-
- lib/baseline/
|
|
118
|
-
- lib/baseline/
|
|
119
|
-
- lib/baseline/
|
|
120
|
-
- lib/baseline/
|
|
121
|
-
- lib/baseline/
|
|
122
|
-
- lib/baseline/
|
|
123
|
-
- lib/baseline/
|
|
124
|
-
- lib/baseline/parsers/h3c.rb
|
|
125
|
-
- lib/baseline/parsers/hillstone.rb
|
|
126
|
-
- lib/baseline/parsers/huawei.rb
|
|
127
|
-
- lib/baseline/parsers/palo_alto.rb
|
|
121
|
+
- lib/baseline/h3c/parser.rb
|
|
122
|
+
- lib/baseline/hillstone/document.rb
|
|
123
|
+
- lib/baseline/hillstone/parser.rb
|
|
124
|
+
- lib/baseline/huawei/parser.rb
|
|
125
|
+
- lib/baseline/palo_alto/document.rb
|
|
126
|
+
- lib/baseline/palo_alto/parser.rb
|
|
127
|
+
- lib/baseline/parser.rb
|
|
128
128
|
- lib/baseline/policy.rb
|
|
129
129
|
- lib/baseline/quoted_document.rb
|
|
130
130
|
- lib/baseline/report.rb
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "quoted_document"
|
|
4
|
-
|
|
5
|
-
module Baseline
|
|
6
|
-
# StoneOS 导出内容可能包含命令回显、显式退出及引号包裹的界面状态。
|
|
7
|
-
class HillstoneDocument < QuotedDocument
|
|
8
|
-
protected
|
|
9
|
-
|
|
10
|
-
# 区分配置结构行与引号、横幅中的文本内容。
|
|
11
|
-
def classify(line)
|
|
12
|
-
unless @quote
|
|
13
|
-
return :ignored if line.text.match?(/\A(?:show (?:configuration|config)|Running configuration|Version\b|[\w.():-]+#\s*\z)/)
|
|
14
|
-
return :ignored if line.text == "exit"
|
|
15
|
-
return :boundary if line.text == "End"
|
|
16
|
-
end
|
|
17
|
-
super
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# 识别配置值可能跨行延续的语句。
|
|
21
|
-
def multiline_fact?(text)
|
|
22
|
-
text.match?(/\A(?:hostname|admin host|snmp-server|ntp|logging syslog|password|secret|host|backup[12])\b/)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "quoted_document"
|
|
4
|
-
|
|
5
|
-
module Baseline
|
|
6
|
-
# PAN-OS 的 set 导出可能包含引号包裹的多行证书与描述。
|
|
7
|
-
class PaloAltoDocument < QuotedDocument
|
|
8
|
-
protected
|
|
9
|
-
|
|
10
|
-
# 已建模的多行值不能被误判为配置缺失;证书正文不参与语义分析。
|
|
11
|
-
def multiline_fact?(text)
|
|
12
|
-
/\Aset (?:deviceconfig system (?:hostname|service|permitted-ip|ntp-servers|snmp-setting|authentication-profile|route service)\b|shared (?:server-profile tacplus|authentication-profile|authentication-sequence|log-settings syslog)\b|mgt-config users\b|network profiles interface-management-profile\b)/.match?(text)
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "ctx_parser/configuration_parser"
|
|
4
|
-
require "ctx_parser/input"
|
|
5
|
-
require_relative "../document"
|
|
6
|
-
|
|
7
|
-
module Baseline
|
|
8
|
-
module Parsers
|
|
9
|
-
# 共享输入与提取边界;CtxParser 跟踪来源,Facts 归纳领域证据。
|
|
10
|
-
class Base < CtxParser::ConfigurationParser
|
|
11
|
-
SERVICE_PORTS = { "ssh" => 22, "telnet" => 23, "snmp" => 161, "snmptrap" => 162 }.freeze
|
|
12
|
-
SNMP_OPTIONS = %w[auth priv authentication-mode privacy-mode acl access use-ipv4acl group local engineid engineID
|
|
13
|
-
encrypted localizedkey localizedV2key remote udp-port].freeze
|
|
14
|
-
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
# 解释当前命令回显,保留不支持或格式错误的证据。
|
|
18
|
-
def parse_output(text, complete:)
|
|
19
|
-
begin
|
|
20
|
-
text = CtxParser::Input.text(text, encoding: :utf8, max_bytes: 16 * 1024 * 1024)
|
|
21
|
-
rescue ArgumentError, EncodingError
|
|
22
|
-
return CtxParser::Result.new(status: :failed, issues: [ { code: :invalid_configuration } ])
|
|
23
|
-
end
|
|
24
|
-
read_configuration(text, document_class: configuration_document_class)
|
|
25
|
-
issues = document.issues.dup
|
|
26
|
-
if (!complete || issues.any?) && exclude_incomplete_final_line
|
|
27
|
-
issues << { code: :incomplete_final_line, line: cursor.lines.last.number }
|
|
28
|
-
end
|
|
29
|
-
@source_issues = issues
|
|
30
|
-
@facts = Facts.new(complete: complete, issues: issues)
|
|
31
|
-
if roots.empty?
|
|
32
|
-
return CtxParser::Result.new(status: :failed, issues: issues + [ { code: :empty_configuration } ])
|
|
33
|
-
end
|
|
34
|
-
if (hostname = matching(hostname_pattern).last)
|
|
35
|
-
name = analyze(hostname) { hostname.text[hostname_pattern, 1] }
|
|
36
|
-
record("hostname", name)
|
|
37
|
-
capture(hostname, name, [ "facts", "hostname" ])
|
|
38
|
-
end
|
|
39
|
-
access_lists
|
|
40
|
-
management
|
|
41
|
-
services
|
|
42
|
-
authentication
|
|
43
|
-
@facts.result
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# 返回本次解析的根配置块。
|
|
47
|
-
def roots = @blocks
|
|
48
|
-
# 选择适合当前平台语法的文档词法解析器。
|
|
49
|
-
def configuration_document_class = Document
|
|
50
|
-
# 匹配当前平台显式声明的主机名。
|
|
51
|
-
def hostname_pattern = /\A(?:hostname|sysname)\s+(\S+)\z/
|
|
52
|
-
# 选择符合目标命令语法的根配置块。
|
|
53
|
-
def sections(pattern) = roots.select { |entry| pattern.match?(entry.text) }
|
|
54
|
-
|
|
55
|
-
# 保留原始文本,但不将可能截断的词元纳入事实构建树。
|
|
56
|
-
def exclude_incomplete_final_line
|
|
57
|
-
line = cursor.lines.last
|
|
58
|
-
return false unless line && !line.raw.end_with?("\n", "\r") && cursor.state(line.index) != :ignored
|
|
59
|
-
|
|
60
|
-
entries, ancestors = roots, []
|
|
61
|
-
while (entry = entries.last)
|
|
62
|
-
if entry.index == line.index
|
|
63
|
-
retained = entries[0...-1].freeze
|
|
64
|
-
ancestors.reverse_each do |siblings, parent|
|
|
65
|
-
block = CtxParser::Block.new(cursor: cursor, header: parent.header, children: retained, last_index: parent.last_index)
|
|
66
|
-
retained = (siblings[0...-1] + [ block ]).freeze
|
|
67
|
-
end
|
|
68
|
-
@blocks = retained
|
|
69
|
-
cursor.mark(line.index, state: :unsupported)
|
|
70
|
-
return true
|
|
71
|
-
end
|
|
72
|
-
ancestors << [ entries, entry ]
|
|
73
|
-
entries = entry.children
|
|
74
|
-
end
|
|
75
|
-
false
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# 筛选匹配已支持命令模式的来源条目。
|
|
79
|
-
def matching(pattern, entries = roots) = entries.select { |entry| pattern.match?(entry.text) }
|
|
80
|
-
# 提取匹配值,并关联对应的来源条目。
|
|
81
|
-
def values(pattern, entries = roots)
|
|
82
|
-
entries.filter_map do |entry|
|
|
83
|
-
value = entry.text[pattern, 1]
|
|
84
|
-
analyze(entry) { value } unless value.nil?
|
|
85
|
-
end.uniq
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# 按配置顺序选取最后一条匹配声明。
|
|
89
|
-
def last(pattern, entries = roots)
|
|
90
|
-
entry = entries.reverse.find { |candidate| !candidate.text[pattern, 1].nil? }
|
|
91
|
-
return unless entry
|
|
92
|
-
|
|
93
|
-
analyze(entry) { entry.text[pattern, 1] }
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# 保存归一化事实及其来源证据。
|
|
97
|
-
def record(path, value, entries = [])
|
|
98
|
-
return unless @facts.record(path, value, lines: entries.map(&:line))
|
|
99
|
-
|
|
100
|
-
# 来源块中尚未解释的命令不能标记为解析成功。
|
|
101
|
-
cursor.capture_many(entries.select { |entry| cursor.parsed?(entry.index) }, data: value, path: [ "facts", path ])
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# 记录事实不可用的原因,并保留对应的来源行。
|
|
105
|
-
def unknown(path, entries = [], code: :unsupported_syntax)
|
|
106
|
-
@facts.unknown(path, lines: entries.map(&:line), code: code)
|
|
107
|
-
entries.each { |entry| cursor.mark(entry.index, state: :unsupported) }
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
# 未解析的访问声明不能抹除其他已确认的写权限。
|
|
111
|
-
def unknown_snmp_access(entries, code: :unsupported_syntax)
|
|
112
|
-
@facts.unknown_snmp_permission(lines: entries.map(&:line), code: code)
|
|
113
|
-
%w[snmp.communities snmp.community_present snmp.users snmp.versions snmp.v3_auth_priv snmp.acl].each do |path|
|
|
114
|
-
unknown(path, entries, code: code)
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
# 按来源顺序确定最终生效的显式启用或禁用声明。
|
|
119
|
-
def state(on, off, entries = roots)
|
|
120
|
-
entries.reduce(nil) do |value, entry|
|
|
121
|
-
if off.match?(entry.text)
|
|
122
|
-
analyze(entry) { false }
|
|
123
|
-
elsif on.match?(entry.text)
|
|
124
|
-
analyze(entry) { true }
|
|
125
|
-
else
|
|
126
|
-
value
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# 记录凭据属性,不保留凭据秘密值。
|
|
132
|
-
def credential(owner, kind, format, secret, entry)
|
|
133
|
-
value = analyze(entry) { Credential.new(owner: owner, kind: kind, format: format, secret: secret, line: entry.line) }
|
|
134
|
-
index = @facts.credential(value)
|
|
135
|
-
capture(entry, value.to_h, [ "facts", "credentials.records", index ])
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# 注册有序 ACL 规则与别名,同时保留不支持的语法。
|
|
139
|
-
def register_acl(name, entries, aliases: [], supported: true, source: nil, &block)
|
|
140
|
-
commands = entries.reject { |entry| /\A(?:\d+\s+)?(?:description|remark|step|statistics|hardware-count)\b/.match?(entry.text) }
|
|
141
|
-
parsed = commands.map.with_index do |entry, index|
|
|
142
|
-
value = analyze(entry) { block.call(entry, index) }
|
|
143
|
-
cursor.mark(entry.index, state: :unsupported) unless value
|
|
144
|
-
value
|
|
145
|
-
end
|
|
146
|
-
damaged = @source_issues.any? do |issue|
|
|
147
|
-
line = issue[:line]
|
|
148
|
-
next false unless line
|
|
149
|
-
|
|
150
|
-
source ? source.line_range.cover?(line) : damaged_numbered_acl?(cursor.lines.fetch(line - 1).text, name)
|
|
151
|
-
end
|
|
152
|
-
acl = Acl.new(name: name, line: entries.first&.line, rules: parsed.compact,
|
|
153
|
-
supported: supported && !damaged && parsed.none?(&:nil?))
|
|
154
|
-
if @facts.register_acl(acl, aliases: aliases, lines: entries.map(&:line)).any?
|
|
155
|
-
entries.each { |entry| cursor.mark(entry.index, state: :unsupported) }
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
# 名称被分页截断时,所有匹配其前缀的编号 ACL 都不能声明完整。
|
|
160
|
-
def damaged_numbered_acl?(text, name)
|
|
161
|
-
prefix = text.split(/--\s*More\s*--|\e\[/i, 2).first
|
|
162
|
-
match = /\Aaccess-list(?:\s+(\S*))?(.*)\z/.match(prefix)
|
|
163
|
-
return false unless match
|
|
164
|
-
|
|
165
|
-
identity, suffix = match.captures
|
|
166
|
-
identity.to_s.empty? || (suffix.empty? ? name.start_with?(identity) : name == identity)
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
# 归一化 ACL 来源地址;不推测不支持语法的含义。
|
|
170
|
-
def source(parts)
|
|
171
|
-
token = parts.shift
|
|
172
|
-
return "0.0.0.0/0" if token == "any"
|
|
173
|
-
token = parts.shift if token == "host"
|
|
174
|
-
wildcard = parts.shift if parts.first&.match?(/\A(?:\d+(?:\.\d+){3}|0)\z/)
|
|
175
|
-
Acl.source(token, wildcard)
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
# 显式端口名称仅属于配置语法,不代表组织批准值。
|
|
179
|
-
def service_port(token)
|
|
180
|
-
return SERVICE_PORTS[token] unless token&.match?(/\A\d+\z/)
|
|
181
|
-
value = token.to_i
|
|
182
|
-
value if value.between?(0, 65_535)
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
# 服务器目标只接受 IP 地址或 DNS 名称,不接受网段或空词元。
|
|
186
|
-
def server_address?(value)
|
|
187
|
-
return false unless value.is_a?(String) && !value.empty?
|
|
188
|
-
IPAddr.new(value)
|
|
189
|
-
!value.include?("/")
|
|
190
|
-
rescue IPAddr::Error
|
|
191
|
-
value.bytesize <= 253 && !/\A[\d.]+\z/.match?(value) && value.delete_suffix(".").split(".", -1).all? do |label|
|
|
192
|
-
/\A[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\z/i.match?(label)
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
# 配置的监听端口仅替代当前服务的默认端口。
|
|
197
|
-
def listener_port(command, default:, scope:, negate: "undo")
|
|
198
|
-
prefix = Regexp.escape(command)
|
|
199
|
-
entries = matching(/\A(?:#{negate} )?#{prefix}(?:\s|\z)/)
|
|
200
|
-
return default if entries.empty?
|
|
201
|
-
entry = entries.last
|
|
202
|
-
return analyze(entry) { default } if entry.text == "#{negate} #{command}"
|
|
203
|
-
token = entry.text[/\A#{prefix}\s+(\d+)\z/, 1]
|
|
204
|
-
port = service_port(token)
|
|
205
|
-
return analyze(entry) { port } if port && port.positive?
|
|
206
|
-
unknown_listener_port(scope, entries)
|
|
207
|
-
nil
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
# 监听端口无法解析时,将依赖该端口的事实保留为未知。
|
|
211
|
-
def unknown_listener_port(scope, entries)
|
|
212
|
-
%w[sources restricted].each { |field| unknown("#{scope}.#{field}", entries, code: :unsupported_service_port) }
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
# 区分模式未声明、格式无效及包含完整算法与秘密值的声明。
|
|
216
|
-
def snmp_secret(settings, keyword, algorithms:, optional_algorithm: false, aes_size: false)
|
|
217
|
-
tokens = settings.split
|
|
218
|
-
index = tokens.index(keyword)
|
|
219
|
-
return false unless index
|
|
220
|
-
return if tokens.count(keyword) > 1
|
|
221
|
-
parameters = tokens.drop(index + 1).take_while { |token| !SNMP_OPTIONS.include?(token) }
|
|
222
|
-
if algorithms.include?(parameters.first)
|
|
223
|
-
algorithm = parameters.shift
|
|
224
|
-
elsif !optional_algorithm || parameters.size != 1 || %w[aes des md5 sha].include?(parameters.first)
|
|
225
|
-
return
|
|
226
|
-
end
|
|
227
|
-
return if algorithm == "aes" && aes_size && !%w[128 192 256].include?(parameters.shift)
|
|
228
|
-
format = parameters.shift if %w[cipher simple].include?(parameters.first)
|
|
229
|
-
return unless parameters.size == 1
|
|
230
|
-
[ format, parameters.first ]
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
# 提取 SNMP 凭据属性,不持久化秘密值。
|
|
234
|
-
def snmp_credentials(owner, modes, entry, format: nil)
|
|
235
|
-
modes.each_value do |value|
|
|
236
|
-
next if value == false
|
|
237
|
-
stored_format, secret = value || [ "unknown", nil ]
|
|
238
|
-
credential(owner, "snmp_user", stored_format || format || "unknown", secret, entry)
|
|
239
|
-
end
|
|
240
|
-
unknown("snmp.v3_auth_priv", [ entry ], code: :invalid_snmp_security) if modes.value?(nil)
|
|
241
|
-
modes.transform_values { |value| value ? true : value }
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
# 同名组可出现在不同安全级别;后续只读声明不能消除已有写权限。
|
|
245
|
-
def snmp_groups(entries)
|
|
246
|
-
entries.map { |entry| yield(entry) }.group_by(&:first).transform_values do |definitions|
|
|
247
|
-
records = definitions.map(&:last)
|
|
248
|
-
acls = records.map { |record| record[:acl] }.uniq
|
|
249
|
-
{ acl: acls.one? ? acls.first : nil, acl_ambiguous: acls.size > 1,
|
|
250
|
-
write: Facts.write_permission(records.map { |record| record[:write] }),
|
|
251
|
-
line: records.first[:line], lines: records.map { |record| record[:line] } }
|
|
252
|
-
end
|
|
253
|
-
end
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
end
|