baseline-rb 0.1.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.
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "digest"
5
+ require "json"
6
+
7
+ module Baseline
8
+ # 按主机、型号、站点、角色或厂商选择批准值;缺少批准值时不回退到当前配置。
9
+ class Policy
10
+ SCOPES = Rule::CONTEXT_FIELDS
11
+ SELECTORS = Baseline.immutable(SCOPES.map { |scope| "by_#{scope}" } + [ "default" ])
12
+ Resolution = Data.define(:value, :reason, :path) do
13
+ # 冻结解析后的批准值、原因及策略选择路径。
14
+ def initialize(value:, reason:, path:)
15
+ super(value: Baseline.immutable(value), reason: Baseline.immutable(reason), path: Baseline.immutable(path))
16
+ end
17
+ end
18
+ attr_reader :rules, :sha256
19
+
20
+ # 加载 gem 内附带的默认策略。
21
+ def self.default = load(File.expand_path("../../policy.yml", __dir__))
22
+
23
+ # 读取并校验策略文档,禁止实例化任意 YAML 对象。
24
+ def self.load(path)
25
+ text = File.binread(path)
26
+ raise ArgumentError, "Baseline policy is too large" if text.bytesize > 1_048_576
27
+ stream = Psych.parse_stream(text)
28
+ raise ArgumentError, "One baseline YAML document is required" unless stream.children.one?
29
+ validate_yaml!(stream)
30
+ new(YAML.safe_load(text, permitted_classes: [], permitted_symbols: [], aliases: false))
31
+ rescue Psych::Exception
32
+ raise ArgumentError, "Invalid baseline YAML", cause: nil
33
+ end
34
+
35
+ # 反序列化前拒绝不安全或嵌套过深的 YAML 结构。
36
+ def self.validate_yaml!(node, depth = 0)
37
+ raise ArgumentError, "Baseline YAML nesting is too deep" if depth > 32
38
+ if node.is_a?(Psych::Nodes::Alias)
39
+ raise ArgumentError, "Baseline YAML aliases are not allowed"
40
+ elsif node.is_a?(Psych::Nodes::Mapping)
41
+ keys = node.children.each_slice(2).map(&:first)
42
+ unless keys.all? { |key| key.is_a?(Psych::Nodes::Scalar) } && keys.map(&:value).uniq.size == keys.size
43
+ raise ArgumentError, "Duplicate or complex baseline YAML keys"
44
+ end
45
+ end
46
+ Array(node.children).each { |child| validate_yaml!(child, depth + 1) }
47
+ end
48
+ private_class_method :validate_yaml!
49
+
50
+ # 在设备评估前校验并冻结策略定义。
51
+ def initialize(definition)
52
+ unless definition.is_a?(Hash) && (definition.keys - %w[version checks]).empty? &&
53
+ definition["version"].is_a?(Integer) && definition["version"] == 1 &&
54
+ definition["checks"].is_a?(Hash) && !definition["checks"].empty?
55
+ raise ArgumentError, "Invalid baseline policy"
56
+ end
57
+ @rules = definition["checks"].map { |id, options| Rule.new(id, options) }.freeze
58
+ rules.each { |rule| validate_selection!(rule, rule.expected) }
59
+ @sha256 = Digest::SHA256.hexdigest(JSON.generate(definition)).freeze
60
+ freeze
61
+ end
62
+
63
+ # 解析上下文对应的策略值,并保留选择路径。
64
+ def resolve(rule, context)
65
+ resolution = resolve_with_source(rule, context)
66
+ [ resolution.value, resolution.reason ]
67
+ end
68
+
69
+ # 保留相对于 expected 的命中范围、默认分支或未解析选择器路径。
70
+ def resolve_with_source(rule, context)
71
+ value = rule.expected
72
+ path = []
73
+ while selector?(value)
74
+ selected = false
75
+ SCOPES.each do |scope|
76
+ selector = "by_#{scope}"
77
+ mapping = value[selector]
78
+ next unless mapping
79
+ return Resolution.new(value: nil, reason: :scope_unknown, path: path + [ selector ]) unless context.key?(scope)
80
+ next unless mapping.key?(context[scope])
81
+ path.concat([ selector, context[scope] ])
82
+ value, selected = mapping[context[scope]], true
83
+ break
84
+ end
85
+ unless selected
86
+ path << "default" if value.key?("default")
87
+ value = value["default"]
88
+ end
89
+ end
90
+ Resolution.new(value: value, reason: value.nil? ? :policy_missing : nil, path: path)
91
+ end
92
+
93
+ # 展示对象身份与状态,避免泄露原始回显或凭据。
94
+ def inspect = "#<#{self.class} rules=#{rules.size}>"
95
+
96
+ private
97
+
98
+ # 识别依赖上下文的策略选择器,避免当作普通值处理。
99
+ def selector?(value) = value.is_a?(Hash) && (value.keys & SELECTORS).any?
100
+
101
+ # 校验嵌套策略选择及其最终规则值。
102
+ def validate_selection!(rule, value, depth = 0)
103
+ raise ArgumentError, "Baseline scope nesting is too deep" if depth > 8
104
+ return if value.nil?
105
+ unless selector?(value)
106
+ rule.validate_expected!(value)
107
+ return
108
+ end
109
+ raise ArgumentError, "Invalid baseline selector" unless (value.keys - SELECTORS).empty?
110
+ value.each do |key, selection|
111
+ if key == "default"
112
+ validate_selection!(rule, selection, depth + 1)
113
+ else
114
+ unless selection.is_a?(Hash) && !selection.empty? && selection.keys.all? { |name| name.is_a?(String) && !name.strip.empty? }
115
+ raise ArgumentError, "Invalid baseline scope"
116
+ end
117
+ selection.each_value { |item| validate_selection!(rule, item, depth + 1) }
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "document"
4
+
5
+ module Baseline
6
+ # 多行引号内容始终作为数据处理,即使正文看起来像全局命令。
7
+ class QuotedDocument < Document
8
+ protected
9
+
10
+ # 区分配置结构行与引号、横幅中的文本内容。
11
+ def classify(line)
12
+ was_quoted = !@quote.nil?
13
+ # 注释和横幅具有独立语法,其中的单引号不能开启配置值。
14
+ unless was_quoted
15
+ kind = super
16
+ return kind unless kind == :entry && line.text.match?(/["']/)
17
+ end
18
+ escaped = false
19
+ line.text.each_char do |character|
20
+ if escaped
21
+ escaped = false
22
+ elsif character == "\\"
23
+ escaped = true
24
+ elsif @quote
25
+ @quote = nil if character == @quote
26
+ elsif character == '"' || character == "'"
27
+ @quote = character
28
+ end
29
+ end
30
+ if !was_quoted && @quote && multiline_fact?(line.text)
31
+ issues << { code: :multiline_fact_value, line: line.number }
32
+ end
33
+ return :ignored if was_quoted || @quote
34
+
35
+ kind
36
+ end
37
+
38
+ # 由领域文档识别不能安全跨行的已建模语句。
39
+ def multiline_fact?(_text) = false
40
+
41
+ # 保留父级词法诊断,并报告未闭合的引号值。
42
+ def finish
43
+ super
44
+ issues << { code: :incomplete_quoted_value } if @quote
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Baseline
4
+ # 分别报告评估结果与输入覆盖率;未知项不计分,也不视为合规。
5
+ class Report
6
+ REASONS = {
7
+ input_incomplete: "配置不完整,需提供完整备份后重查", unsupported_platform: "尚未支持此平台的配置语法",
8
+ scope_mismatch: "不在策略适用范围内", scope_unknown: "缺少判断适用范围或选择批准值所需的上下文",
9
+ policy_missing: "未定义当前范围的批准值,请完善基线策略", fact_unavailable: "配置事实缺失或包含尚未支持的语法,请查看解析诊断",
10
+ fact_type_mismatch: "解析事实类型与检查要求不一致", unnecessary_service_enabled: "存在策略要求关闭的服务",
11
+ service_state_unknown: "配置未明确声明部分服务状态,不能推断厂商默认值", deviation: "实际配置偏离批准值",
12
+ unsupported_address_family: "地址格式超出当前 IPv4 检查能力", weak_password: "发现未满足强度要求的明文凭据",
13
+ password_strength_unobservable: "加密或散列凭据无法从备份验证原始口令强度"
14
+ }.freeze
15
+
16
+ Finding = Data.define(:id, :title, :severity, :status, :reason, :actual, :expected, :lines, :policy_path, :diagnostics, :details) do
17
+ # 冻结单条检查结果的策略路径、诊断和支持细节。
18
+ def initialize(policy_path: [], diagnostics: [], details: {}, **attributes)
19
+ super(**Baseline.immutable(attributes.merge(policy_path: policy_path, diagnostics: diagnostics, details: details)))
20
+ end
21
+
22
+ # 将稳定的结果原因代码转换为易读说明。
23
+ def explanation = reason ? Report::REASONS.fetch(reason, reason.to_s) : "满足基线要求"
24
+ # 返回公开的数据结构,并保留状态与诊断信息。
25
+ def to_h = super.merge(explanation: explanation)
26
+
27
+ # 生成紧凑的评估与策略路径元数据,不内嵌大量事实或诊断数组。
28
+ def summary
29
+ { id: id, title: title, severity: severity, status: status, reason: reason, policy_path: policy_path }
30
+ end
31
+ end
32
+ attr_reader :configuration, :policy_sha256, :findings, :context, :counts, :applicable
33
+
34
+ # 冻结报告证据与检查结果,便于复现评估。
35
+ def initialize(configuration:, policy_sha256:, findings:, context:)
36
+ @configuration, @policy_sha256 = configuration, policy_sha256
37
+ @findings = findings.dup.freeze
38
+ @context = Baseline.immutable(context)
39
+ @counts = @findings.map(&:status).tally.freeze
40
+ @applicable = @findings.reject { |finding| finding.status == :not_applicable }.freeze
41
+ freeze
42
+ end
43
+
44
+ # 判断所有已选事实是否都有完整证据支持。
45
+ def complete? = configuration.complete? && applicable.any? && counts.fetch(:pass, 0) + counts.fetch(:fail, 0) == applicable.size
46
+ # 仅在证据完整且检查通过时声明合规。
47
+ def compliant? = complete? && counts.fetch(:pass, 0) == applicable.size
48
+
49
+ # 推导汇总状态,避免将证据缺失当作成功。
50
+ def status
51
+ return :fail if counts.fetch(:fail, 0).positive?
52
+ return :pass if compliant?
53
+ :unknown
54
+ end
55
+
56
+ # 独立衡量已评估证据的覆盖情况,不与设备是否通过混淆。
57
+ def coverage
58
+ return 0.0 if applicable.empty?
59
+ (100.0 * (counts.fetch(:pass, 0) + counts.fetch(:fail, 0)) / applicable.size).round(1)
60
+ end
61
+
62
+ # 计算适用规则中证据完整且检查通过的占比。
63
+ def score
64
+ return 0.0 if applicable.empty?
65
+ (100.0 * counts.fetch(:pass, 0) / applicable.size).round(1)
66
+ end
67
+
68
+ # 返回公开的数据结构,并保留状态与诊断信息。
69
+ def to_h
70
+ metadata.merge(configuration: configuration.to_h, findings: findings.map(&:to_h))
71
+ end
72
+
73
+ # 限制摘要大小;完整事实由 to_h 提供,避免 ACL 或账号数量膨胀元数据。
74
+ def summary
75
+ metadata.merge(configuration: { platform: configuration.platform, sha256: configuration.sha256, status: configuration.status,
76
+ issue_counts: configuration.issues.map { |issue| issue[:code] }.tally },
77
+ findings: findings.map(&:summary))
78
+ end
79
+
80
+ # 以有长度限制且经过转义的文本展示运维证据;完整结构仍保留在 JSON 中。
81
+ def to_text
82
+ output = [ "baseline: #{status}, coverage=#{coverage}%, score=#{score}, #{counts.map { |name, count| "#{name}=#{count}" }.join(', ')}",
83
+ "配置: #{display(configuration.platform)}, #{configuration.status}; SHA256: #{configuration.sha256}",
84
+ "策略 SHA256: #{policy_sha256}; 上下文: #{display(context)}" ]
85
+ findings.each do |finding|
86
+ output << " [#{finding.status}] #{finding.id} | #{finding.title} | #{finding.severity}"
87
+ output << " #{finding.explanation}#{finding.reason ? " (#{finding.reason})" : ''}"
88
+ output << " 策略分支: #{display(finding.policy_path)}" if finding.policy_path.any?
89
+ if %i[pass fail].include?(finding.status)
90
+ output << " 预期: #{display(finding.expected)}; 实际: #{display(finding.actual)}"
91
+ end
92
+ output << " 差异: #{display(finding.details)}" if finding.details.any?
93
+ if finding.lines.any?
94
+ output << " 行号: #{finding.lines.first(20).join(', ')}#{finding.lines.size > 20 ? "… (共 #{finding.lines.size} 行,详见 JSON 报告)" : ''}"
95
+ end
96
+ finding.diagnostics.first(10).each { |issue| output << " 解析诊断: #{display(issue)}" }
97
+ output << " 另有 #{finding.diagnostics.size - 10} 条诊断,详见 JSON 报告" if finding.diagnostics.size > 10
98
+ end
99
+ output.join("\n")
100
+ end
101
+
102
+ # 展示对象身份与状态,避免泄露原始回显或凭据。
103
+ def inspect = "#<#{self.class} status=#{status} coverage=#{coverage} findings=#{findings.size}>"
104
+
105
+ private
106
+
107
+ # 提供来源与策略身份,便于复现并追溯报告。
108
+ def metadata
109
+ { status: status, complete: complete?, compliant: compliant?, score: score, coverage: coverage, counts: counts,
110
+ policy_sha256: policy_sha256, context: context }
111
+ end
112
+
113
+ # 格式化报告值,不改变序列化后的评估契约。
114
+ def display(value)
115
+ text = JSON.generate(value)
116
+ text.length > 500 ? "#{text[0, 500]}… (详见 JSON 报告)" : text
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Baseline
4
+ # 规则声明事实、比较方式和批准值类型;厂商语法仅由解析器负责。
5
+ class Rule
6
+ # 校验与规则目录共用具名字段,避免从名称或元组位置猜测类型。
7
+ Definition = Data.define(:title, :fact, :operator, :value_type, :allowed_values) do
8
+ # 冻结规则目录条目的事实、比较方式及批准值类型。
9
+ def initialize(title:, fact:, operator:, value_type:, allowed_values: nil)
10
+ super(title: Baseline.immutable(title), fact: Baseline.immutable(fact), operator: Baseline.immutable(operator),
11
+ value_type: Baseline.immutable(value_type), allowed_values: Baseline.immutable(allowed_values))
12
+ end
13
+ end
14
+
15
+ DEFINITIONS = {
16
+ "snmp.enabled" => Definition.new(title: "SNMP 配置", fact: "snmp.enabled", operator: :equals, value_type: :boolean),
17
+ "snmp.versions" => Definition.new(title: "SNMP 允许版本", fact: "snmp.versions", operator: :subset, value_type: :enum_set, allowed_values: %w[v1 v2c v3]),
18
+ "snmp.read_only" => Definition.new(title: "SNMP 禁止写权限", fact: "snmp.write_enabled", operator: :equals, value_type: :boolean),
19
+ "snmp.auth_priv" => Definition.new(title: "SNMPv3 认证与加密", fact: "snmp.v3_auth_priv", operator: :equals, value_type: :boolean),
20
+ "snmp.acl_bound" => Definition.new(title: "SNMP 来源 ACL 引用", fact: "snmp.acl.bound", operator: :equals, value_type: :boolean),
21
+ "snmp.acl_restricted" => Definition.new(title: "SNMP 来源 ACL 限制", fact: "snmp.acl.restricted", operator: :equals, value_type: :boolean),
22
+ "snmp.sources" => Definition.new(title: "SNMP 批准来源", fact: "snmp.acl.sources", operator: :networks_within, value_type: :network_set),
23
+ "ntp.minimum_servers" => Definition.new(title: "NTP 服务器数量", fact: "ntp.servers", operator: :count_at_least, value_type: :positive_integer),
24
+ "ntp.servers" => Definition.new(title: "NTP 批准服务器", fact: "ntp.servers", operator: :set_equals, value_type: :string_set),
25
+ "ntp.authentication" => Definition.new(title: "NTP 认证开关", fact: "ntp.authentication", operator: :equals, value_type: :boolean),
26
+ "ntp.source" => Definition.new(title: "NTP 源接口", fact: "ntp.source", operator: :equals, value_type: :string),
27
+ "syslog.minimum_servers" => Definition.new(title: "Syslog 服务器数量", fact: "syslog.servers", operator: :count_at_least, value_type: :positive_integer),
28
+ "syslog.servers" => Definition.new(title: "Syslog 批准服务器", fact: "syslog.servers", operator: :set_equals, value_type: :string_set),
29
+ "syslog.source" => Definition.new(title: "Syslog 源接口", fact: "syslog.source", operator: :equals, value_type: :string),
30
+ "ssh.enabled" => Definition.new(title: "SSH 配置", fact: "ssh.enabled", operator: :equals, value_type: :boolean),
31
+ "ssh.versions" => Definition.new(title: "SSH 允许版本", fact: "ssh.versions", operator: :subset, value_type: :enum_set, allowed_values: %w[1 2]),
32
+ "ssh.vty_ssh_only" => Definition.new(title: "VTY 只允许 SSH", fact: "ssh.vty_ssh_only", operator: :equals, value_type: :boolean),
33
+ "ssh.vty_aaa" => Definition.new(title: "VTY 使用账户认证", fact: "ssh.vty_aaa", operator: :equals, value_type: :boolean),
34
+ "management.acl_bound" => Definition.new(title: "管理来源 ACL 引用", fact: "management.acl.bound", operator: :equals, value_type: :boolean),
35
+ "management.acl_restricted" => Definition.new(title: "管理来源 ACL 限制", fact: "management.acl.restricted", operator: :equals, value_type: :boolean),
36
+ "management.sources" => Definition.new(title: "管理批准来源", fact: "management.acl.sources", operator: :networks_within, value_type: :network_set),
37
+ "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: "凭据存储方式", fact: "credentials.storage_safe", operator: :equals, value_type: :boolean),
39
+ "credentials.strength" => Definition.new(title: "可观察口令强度", fact: "credentials.records", operator: :password_strength, value_type: :password_strength),
40
+ "tacacs.minimum_servers" => Definition.new(title: "TACACS 服务器数量", fact: "tacacs.servers", operator: :count_at_least, value_type: :positive_integer),
41
+ "tacacs.servers" => Definition.new(title: "TACACS 批准服务器", fact: "tacacs.servers", operator: :set_equals, value_type: :string_set),
42
+ "tacacs.login" => Definition.new(title: "管理认证引用 TACACS", fact: "tacacs.login", operator: :equals, value_type: :boolean),
43
+ "tacacs.authorization" => Definition.new(title: "管理授权引用 TACACS", fact: "tacacs.authorization", operator: :equals, value_type: :boolean),
44
+ "tacacs.accounting" => Definition.new(title: "管理审计引用 TACACS", fact: "tacacs.accounting", operator: :equals, value_type: :boolean),
45
+ "tacacs.keys" => Definition.new(title: "TACACS 密钥配置", fact: "tacacs.keys_present", operator: :equals, value_type: :boolean)
46
+ }.freeze
47
+ CONTEXT_FIELDS = %w[host model site role vendor].freeze
48
+ # 适用条件采用标量相等或备选值匹配;集合事实不隐含成员比较语义。
49
+ APPLICABILITY_FACTS = DEFINITIONS.values.each_with_object({}) do |definition, facts|
50
+ facts[definition.fact] = definition.value_type if %i[boolean string].include?(definition.value_type)
51
+ end.merge("hostname" => :string, "snmp.community_present" => :boolean, "credentials.present" => :boolean).freeze
52
+
53
+ attr_reader :id, :definition, :title, :fact, :operator, :value_type, :allowed_values, :expected, :severity, :applies_to
54
+
55
+ # 校验规则定义,并冻结比较契约。
56
+ def initialize(id, options)
57
+ @definition = DEFINITIONS.fetch(id) { raise ArgumentError, "Unknown baseline rule" }
58
+ unless options.is_a?(Hash) && (options.keys - %w[enabled expected severity applies_to]).empty? &&
59
+ [ true, false ].include?(options.fetch("enabled", true)) &&
60
+ %w[low medium high critical].include?(options.fetch("severity", "medium"))
61
+ raise ArgumentError, "Invalid baseline rule options"
62
+ end
63
+ @id = Baseline.immutable(id)
64
+ @title, @fact, @operator = definition.title, definition.fact, definition.operator
65
+ @value_type, @allowed_values = definition.value_type, definition.allowed_values
66
+ @expected = Baseline.immutable(options["expected"])
67
+ @enabled, @severity = options.fetch("enabled", true), Baseline.immutable(options.fetch("severity", "medium"))
68
+ @applies_to = Baseline.immutable(options.fetch("applies_to", {}))
69
+ validate_applicability!
70
+ freeze
71
+ end
72
+
73
+ # 判断策略规则是否已启用评估。
74
+ def enabled? = @enabled
75
+
76
+ # 将 nil 保留为批准值缺失;具体值则按规则目录声明的领域类型校验。
77
+ def validate_expected!(value)
78
+ valid = case value_type
79
+ when :boolean, :string then scalar?(value, value_type)
80
+ when :positive_integer then value.is_a?(Integer) && value.positive?
81
+ when :password_strength
82
+ value.is_a?(Hash) && value.size == 2 && (value.keys - %w[min_classes min_length]).empty? &&
83
+ value["min_length"].is_a?(Integer) && value["min_length"].between?(1, 1024) &&
84
+ value["min_classes"].is_a?(Integer) && value["min_classes"].between?(1, 4)
85
+ when :enum_set then string_set?(value) && !value.empty? && (value - allowed_values).empty?
86
+ when :string_set, :network_set then string_set?(value)
87
+ end
88
+ raise ArgumentError, "Invalid baseline expected value for #{id}" unless valid
89
+ value.each { |network| Acl.range(network) } if value_type == :network_set
90
+ end
91
+
92
+ # 展示对象身份与状态,避免泄露原始回显或凭据。
93
+ def inspect = "#<#{self.class} id=#{id} enabled=#{enabled?}>"
94
+
95
+ private
96
+
97
+ # 拒绝拼错的事实名和空条件,避免将应失败的检查静默排除为不适用。
98
+ def validate_applicability!
99
+ unless applies_to.is_a?(Hash) && (applies_to.keys - CONTEXT_FIELDS - [ "facts" ]).empty?
100
+ raise ArgumentError, "Invalid baseline applicability for #{id}"
101
+ end
102
+ applies_to.each do |key, value|
103
+ if key == "facts"
104
+ unless value.is_a?(Hash) && !value.empty?
105
+ raise ArgumentError, "Invalid baseline applicability facts for #{id}"
106
+ end
107
+ value.each do |name, criterion|
108
+ type = APPLICABILITY_FACTS.fetch(name) { raise ArgumentError, "Unknown baseline applicability fact for #{id}" }
109
+ validate_criterion!(criterion, type)
110
+ end
111
+ else
112
+ validate_criterion!(value, :string)
113
+ end
114
+ end
115
+ end
116
+
117
+ # 按声明的值类型校验策略比较操作数。
118
+ def validate_criterion!(criterion, type)
119
+ values = criterion.is_a?(Array) ? criterion : [ criterion ]
120
+ unless !values.empty? && values.all? { |value| scalar?(value, type) }
121
+ raise ArgumentError, "Invalid baseline applicability value for #{id}"
122
+ end
123
+ end
124
+
125
+ # 按声明类型校验标量策略值。
126
+ def scalar?(value, type)
127
+ case type
128
+ when :string then value.is_a?(String) && !value.strip.empty?
129
+ when :boolean then [ true, false ].include?(value)
130
+ end
131
+ end
132
+
133
+ # 要求策略值为内容有效且不重复的文本数组。
134
+ def string_set?(value)
135
+ value.is_a?(Array) && value.all? { |item| scalar?(item, :string) } && value.uniq == value
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Baseline
4
+ # 公开包版本,供 RubyGems 元数据和运行时调用方共用。
5
+ VERSION = "0.1.0"
6
+ end
data/lib/baseline.rb ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
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
+ require_relative "baseline/audit"
data/policy.yml ADDED
@@ -0,0 +1,47 @@
1
+ # 参考基线:组织批准的服务器、网段和源接口缺失时保持未知。
2
+ # expected 可使用 by_host > by_model > by_site > by_role > by_vendor > default。
3
+ # 此策略仅评估证据,不生成或执行设备配置。
4
+ version: 1
5
+ checks:
6
+ snmp.enabled: {expected: true}
7
+ snmp.versions: {expected: [v3], severity: high}
8
+ snmp.read_only:
9
+ expected: false
10
+ severity: high
11
+ applies_to: {facts: {snmp.enabled: true}}
12
+ snmp.auth_priv:
13
+ expected: true
14
+ severity: high
15
+ applies_to: {facts: {snmp.enabled: true}}
16
+ snmp.acl_bound: {expected: true, severity: high}
17
+ snmp.acl_restricted: {expected: true, severity: high}
18
+ snmp.sources: {expected: null, severity: high}
19
+ ntp.minimum_servers: {expected: 2}
20
+ ntp.servers: {expected: null}
21
+ ntp.authentication: {expected: true}
22
+ ntp.source: {expected: null}
23
+ syslog.minimum_servers: {expected: 2}
24
+ syslog.servers: {expected: null}
25
+ syslog.source: {expected: null}
26
+ ssh.enabled: {expected: true, severity: high}
27
+ ssh.versions: {expected: ['2'], severity: high}
28
+ ssh.vty_ssh_only: {expected: true, severity: high}
29
+ ssh.vty_aaa: {expected: true, severity: high}
30
+ management.acl_bound: {expected: true, severity: high}
31
+ management.acl_restricted: {expected: true, severity: high}
32
+ management.sources: {expected: null, severity: high}
33
+ services.disabled: {expected: [telnet, http, ftp, tftp], severity: high}
34
+ credentials.storage:
35
+ expected: true
36
+ severity: high
37
+ applies_to: {facts: {credentials.present: true}}
38
+ credentials.strength:
39
+ expected: {min_length: 12, min_classes: 3}
40
+ severity: high
41
+ applies_to: {facts: {credentials.present: true}}
42
+ tacacs.minimum_servers: {expected: 2}
43
+ tacacs.servers: {expected: null}
44
+ tacacs.login: {expected: true, severity: high}
45
+ tacacs.authorization: {expected: true}
46
+ tacacs.accounting: {expected: true}
47
+ tacacs.keys: {expected: true, severity: high}