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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2386adb307215040a989dccfcba29d403bd31f28d400cbb6ba9f34215c13e7bb
|
|
4
|
+
data.tar.gz: 975bc0b9e0136528a8af240d21fe623068ec334c3f444746ce84dcbc25e097e6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 81a940f8b6b82df076c2c1f541ee6b5f7e95154095ca3ccffc2b70580d129388d5ec40c6c6f9a926c30f813c31d5cc39f669f2c7f487dda94718954707e5f5f9
|
|
7
|
+
data.tar.gz: cc66c2181c786de9341533ae65caf8214d0e8d28d40628c7b22b9161e3189174144a38cb58f8599dd2b0848cc511b3ebb31a5634e807b957ef074201eee1624e
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
Changes to the public contract are recorded here. Versions follow Semantic Versioning.
|
|
4
4
|
|
|
5
|
+
## 0.2.1 (2026-10-17)
|
|
6
|
+
|
|
7
|
+
- Reject duplicate normalized context keys before policy selection; preserve source lines and cursor captures for scalar NTP and Syslog configuration facts.
|
|
8
|
+
|
|
9
|
+
## 0.2.0 (2026-10-17)
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Organize vendor parsers as `Baseline::<Vendor>::Parser`, with Hillstone and Palo Alto `Document` classes beside their parsers. Shared parsing uses `Baseline::Parser`, `Comware::Parser`, and `Cisco::Parser`; platform entry points load independently.
|
|
14
|
+
- Rename extraction helpers and ACL/credential APIs to describe their purpose: `permitted_source_networks`, `source_network`, `address_range`, `network_prefixes`, `networks_within?`, and `record_credential`. Remove redundant immutable-data and range-subtraction wrappers.
|
|
15
|
+
- Replace `snmp.read_only` with `snmp.write_enabled`. Set `expected: false` to prohibit writes. Old class names, methods, and rule keys have no compatibility aliases; update custom callers and policies.
|
|
16
|
+
- Require `ctx_parser ~> 0.2.0` for the shared configuration API.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Keep out-of-policy source findings unknown for incomplete ordered ACL input: an unseen earlier deny can change the permitted sources. Report caller input completeness separately from parser status.
|
|
21
|
+
- Copy ACL names and management bindings without freezing or borrowing caller-owned strings.
|
|
22
|
+
- Preserve NTP source and source-interface command recognition across the shared Cisco and Comware parser families.
|
|
23
|
+
|
|
24
|
+
### Performance and verification
|
|
25
|
+
|
|
26
|
+
- Compare IPv4 network unions and rejected sources with sorted scans, and index credential source lines to avoid quadratic lookups.
|
|
27
|
+
- Add regression coverage, independent platform loading checks, and synthetic benchmarks. Verify installed default policy and compliance behavior outside the checkout.
|
|
28
|
+
|
|
5
29
|
## 0.1.0 (2026-10-16)
|
|
6
30
|
|
|
7
31
|
### Added
|
data/README-zh.md
CHANGED
|
@@ -21,7 +21,7 @@ gem "baseline-rb", require: "baseline"
|
|
|
21
21
|
|
|
22
22
|
然后执行 `bundle install`;也可直接执行 `gem install baseline-rb`。Ruby 加载入口为 `baseline`,公开命名空间为 `Baseline`;gem 包名与 Ruby 命名空间分别维护。
|
|
23
23
|
|
|
24
|
-
模块运行依赖:[`ctx_parser`](https://github.com/gatework/ctx_parser)。 gemspec
|
|
24
|
+
模块运行依赖:[`ctx_parser`](https://github.com/gatework/ctx_parser)。 gemspec 要求 `ctx_parser ~> 0.2.0`,以使用共享配置解析 API;应用应提交自己的 `Gemfile.lock`,保持部署可复现。使用本库不需要启动 Rails 或配置兄弟模块的本地路径。
|
|
25
25
|
|
|
26
26
|
发布产物和校验值见 [GitHub Releases](https://github.com/gatework/baseline-rb/releases),已发布版本以 [RubyGems](https://rubygems.org/gems/baseline-rb) 为准。
|
|
27
27
|
|
|
@@ -65,8 +65,22 @@ puts report.to_text
|
|
|
65
65
|
|
|
66
66
|
详细厂商语义、历史验证记录和 Motor 专用集成说明保留在[架构参考](docs/architecture-zh.md)。其中宿主集成代码需要 Motor,不属于独立 gem API。适配层位于 [Motor 仓库](https://github.com/gatework/motor/tree/main/backend/lib) 的 `backend/lib/baseline`、`backend/lib/vxlan` 和 `backend/lib/wireless`。
|
|
67
67
|
|
|
68
|
+
## 领域术语
|
|
69
|
+
|
|
70
|
+
`Facts` 汇总配置证据,`Policy` 提供批准值,`Evaluator` 生成合规判定。`Acl#permitted_source_networks` 按有序规则、协议和目标端口计算允许的 IPv4 来源网段:`nil` 表示证据未解决,`[]` 表示没有允许来源。`Facts#record_credential` 记录安全的凭据属性。部分证据可以证明确定违规,但不能证明合规。
|
|
71
|
+
|
|
72
|
+
厂商代码按命名空间组织:`Baseline::Hillstone::Parser` 和 `Baseline::Hillstone::Document` 同放在 `baseline/hillstone/`;Palo Alto 使用相同布局。六个平台解析器复用 `Baseline::Parser`、`Comware::Parser` 或 `Cisco::Parser`。单个平台可用 `require "baseline/hillstone/parser"` 独立加载;完整评估入口仍为 `require "baseline"`。
|
|
73
|
+
|
|
74
|
+
解析辅助方法直接描述提取意图:`matching_blocks`、`configured_values`、`last_configured_value`、`configured_state` 和 `acl_source_network`。SNMP 写权限规则统一为 `snmp.write_enabled`,禁止写入时配置 `expected: false`。自定义策略需迁移旧键 `snmp.read_only`;旧类名与规则键不保留兼容别名。
|
|
75
|
+
|
|
76
|
+
上下文允许字符串或符号键,但归一化后重复的键会抛出 `ArgumentError`,避免依赖插入顺序选择设备策略。
|
|
77
|
+
|
|
78
|
+
`complete: true` 表示调用方提供了完整快照。报告通过 `input_complete` 保留这项声明,与解析状态分别记录。不完整的有序 ACL 可能缺少更早的 deny,因此不能仅根据已见 permit 判断来源越界,此时保持 `unknown`。
|
|
79
|
+
|
|
68
80
|
## 开发与测试
|
|
69
81
|
|
|
82
|
+
共享类按实际职责命名,平台信息放在命名空间中。直接对应设备命令的解析器保留文件名和类名。
|
|
83
|
+
|
|
70
84
|
```sh
|
|
71
85
|
bundle install
|
|
72
86
|
bundle exec rake
|
|
@@ -76,6 +90,8 @@ bundle exec rake build
|
|
|
76
90
|
|
|
77
91
|
`bundle exec rake` 执行 RSpec、RuboCop 及离线示例。测试放在 `spec/`,沿用项目 RSpec 约定;样例采用合成或脱敏证据。新增解析格式时补充成功、不完整、不支持及非法输入回归。离线测试不代表所有设备版本均通过实机验证。
|
|
78
92
|
|
|
93
|
+
运行 `bundle exec ruby -Ilib script/benchmark.rb` 可测量合成 ACL 覆盖比较、凭据来源行查询及越界来源报告;脚本报告三次运行的中位数,请在同一机器和 Ruby 版本下比较不同修订。
|
|
94
|
+
|
|
79
95
|
使用声明的最低 Ruby 版本检查兼容性。方法用途统一使用简明中文注释;复杂实现还需说明业务规则、实现思路及关键分支。保持公开返回值与错误契约。真实回显和凭据不可进入示例、fixture、日志或发布包。
|
|
80
96
|
|
|
81
97
|
## 发布
|
data/README.md
CHANGED
|
@@ -21,7 +21,7 @@ gem "baseline-rb", require: "baseline"
|
|
|
21
21
|
|
|
22
22
|
Then run `bundle install`, or install directly with `gem install baseline-rb`. The require path is `baseline` and the public namespace is `Baseline`; package names and Ruby namespaces are intentionally separate.
|
|
23
23
|
|
|
24
|
-
Internal runtime dependencies: [`ctx_parser`](https://github.com/gatework/ctx_parser). The gemspec
|
|
24
|
+
Internal runtime dependencies: [`ctx_parser`](https://github.com/gatework/ctx_parser). The gemspec requires `ctx_parser ~> 0.2.0` for the shared configuration API. Applications should commit their own `Gemfile.lock` to make deployments reproducible. No Rails boot or sibling checkout paths are required.
|
|
25
25
|
|
|
26
26
|
Release artifacts and checksums are available from [GitHub Releases](https://github.com/gatework/baseline-rb/releases). Check [RubyGems](https://rubygems.org/gems/baseline-rb) for published versions.
|
|
27
27
|
|
|
@@ -65,8 +65,22 @@ The library performs no device access, backup collection, remediation, or databa
|
|
|
65
65
|
|
|
66
66
|
Detailed vendor behavior, historical validation notes, and Motor-only integration are preserved in [the architecture reference](docs/architecture-zh.md). Host integration examples in that reference require Motor; they are not standalone gem APIs. Motor adapters live under `backend/lib/baseline`, `backend/lib/vxlan`, and `backend/lib/wireless` in the [Motor repository](https://github.com/gatework/motor/tree/main/backend/lib).
|
|
67
67
|
|
|
68
|
+
## Domain vocabulary
|
|
69
|
+
|
|
70
|
+
`Facts` assembles configuration evidence; `Policy` supplies approved values; `Evaluator` produces compliance findings. `Acl#permitted_source_networks` returns the IPv4 networks permitted by ordered rules for a protocol and destination port: `nil` means unresolved evidence, while `[]` means no permitted sources. `Facts#record_credential` records safe credential attributes. Partial evidence may establish a conclusive violation, but cannot establish compliance.
|
|
71
|
+
|
|
72
|
+
Vendor-specific code lives in a vendor namespace: `Baseline::Hillstone::Parser` and `Baseline::Hillstone::Document` reside in `baseline/hillstone/`; Palo Alto uses the same layout. The six platform parsers inherit shared behavior from `Baseline::Parser`, `Comware::Parser`, or `Cisco::Parser`. Load only one platform with `require "baseline/hillstone/parser"`, or load the assessment API with `require "baseline"`.
|
|
73
|
+
|
|
74
|
+
Parser helpers describe the evidence they extract: `matching_blocks`, `configured_values`, `last_configured_value`, `configured_state`, and `acl_source_network`. SNMP write permission uses the policy key `snmp.write_enabled` (`expected: false` to prohibit writes). Update custom policies that used `snmp.read_only`; old class names and rule keys have no compatibility aliases.
|
|
75
|
+
|
|
76
|
+
Context accepts string or symbol keys, but rejects duplicate keys after normalization with `ArgumentError` rather than selecting a device policy by insertion order.
|
|
77
|
+
|
|
78
|
+
`complete: true` asserts that the caller supplied the entire snapshot. Reports retain that assertion as `input_complete`, separately from parser status. A partial ordered ACL cannot prove an out-of-policy source: an unseen earlier deny may change its meaning, so the finding stays `unknown`.
|
|
79
|
+
|
|
68
80
|
## Development and tests
|
|
69
81
|
|
|
82
|
+
Name shared classes by their responsibility and use the namespace for platform context. Preserve filenames and class names that directly match device commands.
|
|
83
|
+
|
|
70
84
|
```sh
|
|
71
85
|
bundle install
|
|
72
86
|
bundle exec rake
|
|
@@ -76,6 +90,8 @@ bundle exec rake build
|
|
|
76
90
|
|
|
77
91
|
`bundle exec rake` runs RSpec, RuboCop, and the offline examples. Tests live in `spec/`, following the existing project's RSpec convention; fixtures use synthetic or sanitized evidence. Add regression cases for successful, incomplete, unsupported, and malformed inputs when extending a parser. Offline tests do not certify all device versions or replace live integration checks.
|
|
78
92
|
|
|
93
|
+
Run `bundle exec ruby -Ilib script/benchmark.rb` for synthetic network-coverage, credential-lookup, and rejected-source report timings. It reports the median of three runs at two input sizes; compare revisions on the same runtime and machine.
|
|
94
|
+
|
|
79
95
|
Use the declared minimum Ruby version for compatibility checks. Document source methods and non-obvious business logic in concise Chinese, preserve public return/error contracts, and never add raw device captures or credentials to examples, fixtures, logs, or release archives.
|
|
80
96
|
|
|
81
97
|
## Release
|
data/docs/architecture-zh.md
CHANGED
|
@@ -27,11 +27,11 @@ flowchart LR
|
|
|
27
27
|
| 对象 | 职责 |
|
|
28
28
|
| --- | --- |
|
|
29
29
|
| `Audit` | 纯文本入口,选择厂商解析器并组织判定 |
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
30
|
+
| `Parser < CtxParser::Configuration` | 输入边界、语法提取;通过公共 `analyze` / `capture` 管理逐行状态和来源 |
|
|
31
|
+
| `Comware::Parser` / `Cisco::Parser` | 分别共享 Comware/VRP 与 IOS/NX-OS 的语法族 |
|
|
32
|
+
| `H3c::Parser` / `Huawei::Parser` / `CiscoIos::Parser` / `CiscoNxos::Parser` / `Hillstone::Parser` / `PaloAlto::Parser` | 厂商差异,不把厂商分支散落到规则求值中 |
|
|
33
33
|
| `Document < CtxParser::Document` | 使用公共游标和缩进树,隔离 banner、接口子配置及不完整输入 |
|
|
34
|
-
| `QuotedDocument` / `
|
|
34
|
+
| `QuotedDocument` / `Hillstone::Document` / `PaloAlto::Document` | 隔离跨行字符串、证书和 UI 数据,保留厂商命令边界;注释和 banner 不参与字符串扫描 |
|
|
35
35
|
| `Facts` | 每次解析独享的事实集合,归约 VTY、SNMP 组引用、命名 ACL 与原生来源列表、凭据;输出独立的结果快照 |
|
|
36
36
|
| `Configuration` | 不可变事实、输入指纹、完整性和局部解析诊断 |
|
|
37
37
|
| `Acl` | IPv4 地址区间与有序规则,按首次匹配计算允许来源 |
|
|
@@ -43,9 +43,9 @@ flowchart LR
|
|
|
43
43
|
|
|
44
44
|
独立 Ruby 调用只需加载 `baseline`;`Check` 由 Rails 自动加载。持久化使用 `baseline_assessments` 表;纯文本分析不依赖数据库。没有新增外部依赖、HTTP 接口或后台任务。
|
|
45
45
|
|
|
46
|
-
厂商解析器把命令转换为普通 Ruby 值及 `Acl` / `Credential` 对象;`Facts` 不读取命令文本,也不操作游标。语法解释成功后由 `
|
|
46
|
+
厂商解析器把命令转换为普通 Ruby 值及 `Acl` / `Credential` 对象;`Facts` 不读取命令文本,也不操作游标。语法解释成功后由 `Configuration#analyze` 标记当前行,`capture` 关联脱敏数据与来源;未解释的子命令保留原状态。事实中的行号用于判定报告,游标中的捕获用于显式溯源,两者不将原文带入持久化结果。
|
|
47
47
|
|
|
48
|
-
新增厂商时,有相同语法族才继承 `Comware` 或 `Cisco`;防火墙直接继承 `
|
|
48
|
+
新增厂商时,有相同语法族才继承 `Comware::Parser` 或 `Cisco::Parser`;防火墙直接继承 `Baseline::Parser`,独立解释本机管理语法。新增跨厂商归约逻辑放入 `Facts` 或已有领域对象,批准值仍声明在策略 YAML 中。不要在厂商类中重复维护事实字典、诊断列表或引用解析状态。`Facts#direct_acl` 对原生允许来源统一归一化,区分 `nil`(未确认)、`[]`(明确没有允许来源)与 `0.0.0.0/0`(全网);`incomplete!` 使破损声明无法证明合规的缺省状态。
|
|
49
49
|
|
|
50
50
|
## 事实与检查
|
|
51
51
|
|
|
@@ -66,7 +66,7 @@ flowchart LR
|
|
|
66
66
|
|
|
67
67
|
`complete: true` 是调用方对**完整配置快照**的声明,不代表每个事实都可识别。分页残留、错误回显、未闭合 banner、无 hostname/sysname、非法编码会阻止合规结论。接口或 banner 内看似合法的全局命令不会变成全局事实。
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
部分配置可以保留已确认且不依赖未见记录的违规:禁用服务开启、意外集合成员、完整输入中可确认的越界管理来源、弱明文凭据、SNMP 写授权及不安全凭据存储。
|
|
70
70
|
缺少批准成员、数量不足、其他标量比较仍保持 unknown;部分证据不会产生 pass,报告仍不完整。
|
|
71
71
|
明确的上下文不匹配先返回 not_applicable;部分集合的“全部满足/不存在”不能用来判定策略适用范围。
|
|
72
72
|
部分输入或分页输入的无终止末行,以及内联分页/错误污染行不进入事实,避免截断密码/地址被当作有效配置;
|
|
@@ -104,7 +104,7 @@ PAN-OS 管理 profile 未指定 permitted IP 时没有来源限制,因此必
|
|
|
104
104
|
|
|
105
105
|
### SNMP 写权限证据
|
|
106
106
|
|
|
107
|
-
`snmp.
|
|
107
|
+
`snmp.write_enabled` 检查的是配置授予的写权限:任一已知写授权会得到 `fail`;所有授权均明确只读才会通过;缺失组引用、未知角色或不能完整解释的组语法保持 `unknown`。一个授权未知不会抹掉另一个已确认的写授权,也不会阻断 SSH 等无关检查。写视图声明不等于已经成功执行 SNMP SET,MIB 对象可写性、网络可达性和运行时认证需要另行验证。
|
|
108
108
|
|
|
109
109
|
默认策略在 `snmp.enabled: true` 时执行写权限检查,因此纯 SNMPv3 配置也纳入检查,不能仅以 community 是否存在决定规则范围。
|
|
110
110
|
|
data/lib/baseline/acl.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "ctx_parser/immutable"
|
|
4
|
+
|
|
3
5
|
require "ipaddr"
|
|
4
6
|
|
|
5
7
|
module Baseline
|
|
@@ -9,12 +11,12 @@ module Baseline
|
|
|
9
11
|
|
|
10
12
|
# 冻结有序 ACL 规则及其来源、支持状态。
|
|
11
13
|
def initialize(name:, rules:, line:, supported: true)
|
|
12
|
-
@name, @rules, @line, @supported = name
|
|
14
|
+
@name, @rules, @line, @supported = CtxParser::Immutable.copy(name), CtxParser::Immutable.copy(rules), line, supported
|
|
13
15
|
freeze
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
# 按 ACL 顺序匹配语义计算允许访问的来源范围。
|
|
17
|
-
def
|
|
19
|
+
def permitted_source_networks(protocol:, port: nil)
|
|
18
20
|
return unless supported && rules.any?
|
|
19
21
|
selected = rules.sort_by { |rule| rule.fetch(:sequence) }.select do |rule|
|
|
20
22
|
next unless [ "ip", protocol ].include?(rule.fetch(:protocol))
|
|
@@ -24,7 +26,7 @@ module Baseline
|
|
|
24
26
|
end
|
|
25
27
|
true
|
|
26
28
|
end
|
|
27
|
-
self.class.
|
|
29
|
+
self.class.network_prefixes(permitted_ranges(selected))
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
# 返回公开的数据结构,并保留状态与诊断信息。
|
|
@@ -33,7 +35,7 @@ module Baseline
|
|
|
33
35
|
def inspect = "#<#{self.class} name=#{name} rules=#{rules.size} supported=#{supported}>"
|
|
34
36
|
|
|
35
37
|
# 归一化 ACL 来源地址;不推测不支持语法的含义。
|
|
36
|
-
def self.
|
|
38
|
+
def self.source_network(address, wildcard = nil)
|
|
37
39
|
return "0.0.0.0/0" if address == "any"
|
|
38
40
|
ip = IPAddr.new(address)
|
|
39
41
|
raise ArgumentError, "IPv4 ACL required" unless ip.ipv4?
|
|
@@ -48,40 +50,41 @@ module Baseline
|
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
# 将网络前缀转换为包含首尾地址的整数区间。
|
|
51
|
-
def self.
|
|
53
|
+
def self.address_range(cidr)
|
|
52
54
|
ip = IPAddr.new(cidr)
|
|
53
55
|
raise ArgumentError, "IPv4 ACL required" unless ip.ipv4?
|
|
54
56
|
[ ip.to_range.first.to_i, ip.to_range.last.to_i ]
|
|
55
57
|
end
|
|
56
58
|
|
|
57
|
-
#
|
|
58
|
-
def self.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if right < first || left > last
|
|
62
|
-
[ [ first, last ] ]
|
|
63
|
-
else
|
|
64
|
-
[ ([ first, left - 1 ] if left > first), ([ right + 1, last ] if right < last) ].compact
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
59
|
+
# 判断所有实际来源范围是否均在允许范围内。
|
|
60
|
+
def self.networks_within?(actual, expected)
|
|
61
|
+
each_network_coverage(actual, expected) { |_index, covered| return false unless covered }
|
|
62
|
+
true
|
|
68
63
|
end
|
|
69
64
|
|
|
70
|
-
#
|
|
71
|
-
def self.
|
|
72
|
-
|
|
65
|
+
# 批量返回未被批准范围完整覆盖的原始网段,保持报告的输入顺序。
|
|
66
|
+
def self.unapproved_networks(actual, expected)
|
|
67
|
+
indexes = []
|
|
68
|
+
each_network_coverage(actual, expected) { |index, covered| indexes << index unless covered }
|
|
69
|
+
indexes.sort.map { |index| actual.fetch(index) }
|
|
73
70
|
end
|
|
74
71
|
|
|
75
|
-
#
|
|
76
|
-
def self.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
72
|
+
# 批准区间只解析、合并一次;实际网段按起点顺序与其线性比较。
|
|
73
|
+
def self.each_network_coverage(actual, expected)
|
|
74
|
+
approved = merge_ranges(expected.map { |cidr| address_range(cidr) })
|
|
75
|
+
networks = actual.each_with_index.map { |cidr, index| [ address_range(cidr), index ] }.sort_by(&:first)
|
|
76
|
+
approved_index = 0
|
|
77
|
+
networks.each do |(first, last), source_index|
|
|
78
|
+
approved_index += 1 while approved_index < approved.size && approved[approved_index].last < first
|
|
79
|
+
candidate = approved[approved_index]
|
|
80
|
+
yield source_index, candidate && candidate.first <= first && candidate.last >= last
|
|
83
81
|
end
|
|
84
|
-
|
|
82
|
+
end
|
|
83
|
+
private_class_method :each_network_coverage
|
|
84
|
+
|
|
85
|
+
# 将地址区间转换为数量最少的网络前缀。
|
|
86
|
+
def self.network_prefixes(ranges)
|
|
87
|
+
merge_ranges(ranges).flat_map do |first, last|
|
|
85
88
|
networks = []
|
|
86
89
|
while first <= last
|
|
87
90
|
aligned = first.zero? ? 32 : (first & -first).bit_length - 1
|
|
@@ -93,13 +96,25 @@ module Baseline
|
|
|
93
96
|
end
|
|
94
97
|
end
|
|
95
98
|
|
|
99
|
+
# 合并相邻或重叠区间,使覆盖比较只需顺序扫描,不反复切割整个集合。
|
|
100
|
+
def self.merge_ranges(ranges)
|
|
101
|
+
ranges.sort.each_with_object([]) do |(first, last), result|
|
|
102
|
+
if result.any? && first <= result.last.last + 1
|
|
103
|
+
result.last[1] = [ result.last.last, last ].max
|
|
104
|
+
else
|
|
105
|
+
result << [ first, last ]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
private_class_method :merge_ranges
|
|
110
|
+
|
|
96
111
|
private
|
|
97
112
|
|
|
98
113
|
# 按地址边界扫描一次;每个区间由最先匹配的生效规则决定。
|
|
99
114
|
def permitted_ranges(selected)
|
|
100
115
|
boundaries = Hash.new { |entries, address| entries[address] = [] }
|
|
101
116
|
selected.each_with_index do |rule, priority|
|
|
102
|
-
first, last = self.class.
|
|
117
|
+
first, last = self.class.address_range(rule.fetch(:source))
|
|
103
118
|
boundaries[first] << [ priority, true ]
|
|
104
119
|
# 将闭区间终点转为下一地址的失效事件,统一处理相邻和重叠网段。
|
|
105
120
|
boundaries[last + 1] << [ priority, false ]
|
data/lib/baseline/audit.rb
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "configuration"
|
|
4
|
+
require_relative "evaluator"
|
|
5
|
+
require_relative "h3c/parser"
|
|
6
|
+
require_relative "huawei/parser"
|
|
7
|
+
require_relative "cisco_ios/parser"
|
|
8
|
+
require_relative "cisco_nxos/parser"
|
|
9
|
+
require_relative "hillstone/parser"
|
|
10
|
+
require_relative "palo_alto/parser"
|
|
11
|
+
|
|
3
12
|
require "digest"
|
|
4
13
|
|
|
5
14
|
module Baseline
|
|
6
15
|
# 仅分析传入文本,不读取文件、访问数据库、连接设备或执行修复。
|
|
7
16
|
class Audit
|
|
8
|
-
PARSERS = { "h3c" =>
|
|
9
|
-
"cisco_ios" =>
|
|
10
|
-
"hillstone" =>
|
|
17
|
+
PARSERS = { "h3c" => H3c::Parser, "huawei" => Huawei::Parser,
|
|
18
|
+
"cisco_ios" => CiscoIos::Parser, "cisco_nxos" => CiscoNxos::Parser,
|
|
19
|
+
"hillstone" => Hillstone::Parser, "palo_alto" => PaloAlto::Parser }.freeze
|
|
11
20
|
ALIASES = { "h3c_wireless" => "h3c", "comware" => "h3c", "huawei_wireless" => "huawei", "vrp" => "huawei",
|
|
12
21
|
"ios" => "cisco_ios", "iosxe" => "cisco_ios", "ios_xe" => "cisco_ios", "cisco_ios_xe" => "cisco_ios",
|
|
13
22
|
"nxos" => "cisco_nxos", "nx_os" => "cisco_nxos", "stoneos" => "hillstone", "stone_os" => "hillstone",
|
|
@@ -28,7 +37,7 @@ module Baseline
|
|
|
28
37
|
parser = PARSERS[name]
|
|
29
38
|
result = parser ? parser.parse(configuration, complete: complete) :
|
|
30
39
|
CtxParser::Result.new(status: :unsupported, issues: [ { code: :unsupported_platform } ])
|
|
31
|
-
facts = Configuration.new(platform: name, sha256: Digest::SHA256.hexdigest(configuration), result: result)
|
|
40
|
+
facts = Configuration.new(platform: name, sha256: Digest::SHA256.hexdigest(configuration), result: result, input_complete: complete)
|
|
32
41
|
@evaluator.call(configuration: facts, context: context)
|
|
33
42
|
end
|
|
34
43
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../parser"
|
|
4
|
+
|
|
3
5
|
module Baseline
|
|
4
|
-
module
|
|
6
|
+
module Cisco
|
|
5
7
|
# 复用 Cisco 管理服务、IPv4 ACL、SNMP 和 AAA 的公共语法。
|
|
6
|
-
class
|
|
8
|
+
class Parser < Baseline::Parser
|
|
7
9
|
SNMP_ROLE_WRITE = { "network-admin" => true, "vdc-admin" => true,
|
|
8
10
|
"network-operator" => false, "vdc-operator" => false }.freeze
|
|
9
11
|
|
|
@@ -11,12 +13,12 @@ module Baseline
|
|
|
11
13
|
|
|
12
14
|
# 解析管理访问暴露检查使用的有序 ACL 定义。
|
|
13
15
|
def access_lists
|
|
14
|
-
|
|
16
|
+
matching_blocks(/\Aip access-list\s+/).each do |section|
|
|
15
17
|
name = section.text.split.last
|
|
16
18
|
extended = section.text.start_with?("ip access-list extended ") || nxos?
|
|
17
19
|
register_acl(name, section.children, source: section) { |entry, index| cisco_rule(entry, index, extended: extended) }
|
|
18
20
|
end
|
|
19
|
-
|
|
21
|
+
matching_blocks(/\Aaccess-list\s+/).group_by { |entry| entry.text.split[1] }.each do |name, entries|
|
|
20
22
|
extended = name.to_i.between?(100, 199) || name.to_i.between?(2000, 2699)
|
|
21
23
|
register_acl(name, entries) { |entry, index| cisco_rule(entry, index, extended: extended, prefix: /\Aaccess-list\s+\S+\s+/) }
|
|
22
24
|
end
|
|
@@ -30,8 +32,8 @@ module Baseline
|
|
|
30
32
|
parts = match[3].split
|
|
31
33
|
protocol = extended ? parts.shift : "ip"
|
|
32
34
|
return unless %w[ip tcp udp icmp].include?(protocol)
|
|
33
|
-
network =
|
|
34
|
-
return if extended &&
|
|
35
|
+
network = acl_source_network(parts)
|
|
36
|
+
return if extended && acl_source_network(parts) != "0.0.0.0/0"
|
|
35
37
|
if extended && parts.first == "eq"
|
|
36
38
|
return unless %w[tcp udp].include?(protocol)
|
|
37
39
|
parts.shift
|
|
@@ -47,54 +49,54 @@ module Baseline
|
|
|
47
49
|
|
|
48
50
|
# 根据监听服务、认证方式与 ACL 绑定关系归纳管理访问事实。
|
|
49
51
|
def management
|
|
50
|
-
version =
|
|
52
|
+
version = last_configured_value(/\Aip ssh version\s+([12])\z/)
|
|
51
53
|
record("ssh.versions", [ version ]) if version
|
|
52
|
-
ssh = nxos? ?
|
|
53
|
-
record("ssh.enabled", ssh,
|
|
54
|
-
default_aaa = nxos? ||
|
|
55
|
-
vtys =
|
|
56
|
-
authentication =
|
|
54
|
+
ssh = nxos? ? configured_state(/\Afeature ssh\z/, /\Ano feature ssh\z/) : version ? true : nil
|
|
55
|
+
record("ssh.enabled", ssh, matching_blocks(/\A(?:ip ssh|(?:no )?feature ssh)/))
|
|
56
|
+
default_aaa = nxos? || configured_state(/\Aaaa new-model\z/, /\Ano aaa new-model\z/) == true
|
|
57
|
+
vtys = matching_blocks(/\Aline vty(?:\s|\z)/).map do |section|
|
|
58
|
+
authentication = last_configured_value(/\Alogin authentication\s+(\S+)\z/, section.children)
|
|
57
59
|
local = section.children.any? { |entry| entry.text == "login local" }
|
|
58
|
-
{ name: section.text, protocols:
|
|
60
|
+
{ name: section.text, protocols: last_configured_value(/\Atransport input\s+(.+)\z/, section.children)&.split,
|
|
59
61
|
authentication: local ? "local" : (default_aaa ? "aaa" : nil),
|
|
60
62
|
authentication_list: authentication || (default_aaa ? "default" : nil),
|
|
61
|
-
acl:
|
|
63
|
+
acl: last_configured_value(/\Aaccess-class\s+(\S+)\s+in(?:\s+vrf-also)?\z/, section.children), line: section.line }
|
|
62
64
|
end
|
|
63
|
-
rotary =
|
|
64
|
-
rotary.concat(
|
|
65
|
+
rotary = matching_blocks(/\A(?:no )?ip ssh port(?:\s|\z)/)
|
|
66
|
+
rotary.concat(matching_blocks(/\Aline vty(?:\s|\z)/).flat_map { |section| matching_blocks(/\A(?:no )?rotary(?:\s|\z)/, section.children) })
|
|
65
67
|
unknown_listener_port("management.acl", rotary) unless rotary.empty?
|
|
66
|
-
telnet_ports =
|
|
68
|
+
telnet_ports = matching_blocks(/\A(?:no )?(?:ip )?telnet(?: server)? port(?:\s|\z)/)
|
|
67
69
|
unknown_listener_port("management.acl", telnet_ports) unless telnet_ports.empty?
|
|
68
70
|
@facts.management(vtys: vtys,
|
|
69
71
|
ssh_port: rotary.empty? ? listener_port("ssh port", default: 22, scope: "management.acl", negate: "no") : nil,
|
|
70
72
|
telnet_port: telnet_ports.empty? ? 23 : nil)
|
|
71
73
|
telnet = if nxos?
|
|
72
|
-
|
|
74
|
+
configured_state(/\Afeature telnet\z/, /\Ano feature telnet\z/)
|
|
73
75
|
elsif vtys.any? && vtys.all? { |entry| entry[:protocols] }
|
|
74
76
|
vtys.any? { |entry| (entry[:protocols] & %w[all telnet]).any? }
|
|
75
77
|
end
|
|
76
|
-
record("services", { "telnet" => telnet, "http" =>
|
|
77
|
-
"https" =>
|
|
78
|
-
"ftp" =>
|
|
79
|
-
"tftp" =>
|
|
80
|
-
"finger" =>
|
|
78
|
+
record("services", { "telnet" => telnet, "http" => configured_state(/\Aip http server\z/, /\Ano ip http server\z/),
|
|
79
|
+
"https" => configured_state(/\Aip http secure-server\z/, /\Ano ip http secure-server\z/),
|
|
80
|
+
"ftp" => configured_state(/\A(?:feature ftp-server|ftp-server enable)\z/, /\Ano (?:feature ftp-server|ftp-server enable)\z/),
|
|
81
|
+
"tftp" => configured_state(/\A(?:feature tftp-server|tftp-server\s+.+)\z/, /\Ano (?:feature tftp-server|tftp-server\s+.+)\z/),
|
|
82
|
+
"finger" => configured_state(/\A(?:ip finger|service finger)\z/, /\Ano (?:ip finger|service finger)\z/) })
|
|
81
83
|
end
|
|
82
84
|
|
|
83
85
|
# 采集明确启用或禁用的管理服务。
|
|
84
86
|
def services
|
|
85
|
-
record("ntp.servers",
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
hosts =
|
|
89
|
-
hosts |=
|
|
90
|
-
record("syslog.servers", hosts,
|
|
91
|
-
|
|
92
|
-
snmp =
|
|
93
|
-
enabled = nxos? ?
|
|
87
|
+
record("ntp.servers", configured_values(/\Antp server(?: vrf \S+)?\s+(\S+)/), matching_blocks(/\Antp server/))
|
|
88
|
+
record_configured_state("ntp.authentication", /\Antp authenticate\z/, /\Ano ntp authenticate\z/)
|
|
89
|
+
record_configured_value("ntp.source", /\Antp source(?:-interface)?\s+(\S+)\z/)
|
|
90
|
+
hosts = configured_values(nxos? ? /\Alogging server\s+(\S+)/ : /\Alogging host(?: ipv6)?\s+(\S+)/)
|
|
91
|
+
hosts |= configured_values(/\Alogging\s+(\d+(?:\.\d+){3})\z/) unless nxos?
|
|
92
|
+
record("syslog.servers", hosts, matching_blocks(/\Alogging (?:host|server)/))
|
|
93
|
+
record_configured_value("syslog.source", /\Alogging source-interface\s+(\S+)\z/)
|
|
94
|
+
snmp = matching_blocks(/\Asnmp-server\b/)
|
|
95
|
+
enabled = nxos? ? configured_state(/\Asnmp-server protocol enable\z/, /\Ano snmp-server protocol enable\z/) : nil
|
|
94
96
|
enabled = nxos? || snmp.any? if enabled.nil? && (@facts.complete? || snmp.any?)
|
|
95
97
|
record("snmp.enabled", enabled)
|
|
96
98
|
communities = {}
|
|
97
|
-
|
|
99
|
+
matching_blocks(/\Asnmp-server community\b/).each do |entry|
|
|
98
100
|
match = /\Asnmp-server community\s+(\S+)(?:\s+(.*))?\z/.match(entry.text)
|
|
99
101
|
unless match
|
|
100
102
|
unknown_snmp_access([ entry ])
|
|
@@ -108,17 +110,17 @@ module Baseline
|
|
|
108
110
|
community[:write] = SNMP_ROLE_WRITE[role] if role
|
|
109
111
|
acl = nxos? ? settings[/\buse-ipv4acl\s+(\S+)/, 1] : settings[/\A(?:ro|rw)\s+(\S+)\z/i, 1]
|
|
110
112
|
community[:acl] = acl if acl
|
|
111
|
-
|
|
113
|
+
record_credential("snmp-community", "snmp", "plain", match[1], entry) unless @facts.credential_recorded?(community[:line])
|
|
112
114
|
end
|
|
113
115
|
versions = communities.any? ? %w[v1 v2c] : []
|
|
114
|
-
groups = snmp_groups(
|
|
116
|
+
groups = snmp_groups(matching_blocks(/\Asnmp-server group\s+.+?\s+v3\b/)) do |entry|
|
|
115
117
|
name, settings = entry.text.match(/\Asnmp-server group\s+(.+?)\s+v3(?:\s+(.*))?\z/).captures
|
|
116
118
|
write = name.include?('"') ? nil : snmp_group_write(settings.to_s)
|
|
117
119
|
[ name, { acl: entry.text[/\baccess\s+(\S+)/, 1], line: entry.line, write: write } ]
|
|
118
120
|
end
|
|
119
121
|
record("snmp.groups", groups)
|
|
120
122
|
users = {}
|
|
121
|
-
|
|
123
|
+
matching_blocks(/\Asnmp-server user\b/).each do |entry|
|
|
122
124
|
match = /\Asnmp-server user\s+(\S+)\s+(\S+)(.*)\z/.match(entry.text)
|
|
123
125
|
unless match
|
|
124
126
|
unknown_snmp_access([ entry ])
|
|
@@ -153,14 +155,14 @@ module Baseline
|
|
|
153
155
|
acl = settings[/\b(?:access|use-ipv4acl)\s+(\S+)/, 1]
|
|
154
156
|
user[:acl] = acl if acl
|
|
155
157
|
end
|
|
156
|
-
port_overrides =
|
|
158
|
+
port_overrides = matching_blocks(/\A(?:no )?snmp-server (?:listen-port|udp-port|port)(?:\s|\z)/)
|
|
157
159
|
unknown_listener_port("snmp.acl", port_overrides) unless port_overrides.empty?
|
|
158
160
|
@facts.snmp(communities: communities.values, users: users.values, groups: nxos? ? nil : groups,
|
|
159
161
|
port: port_overrides.empty? ? 161 : nil)
|
|
160
162
|
nxos_snmp_coverage(users, enabled: enabled) if nxos?
|
|
161
163
|
unknown("snmp.v3_auth_priv", code: :snmp_user_unresolved) if users.values.any? { |user| !user.key?(:auth) }
|
|
162
|
-
versions |=
|
|
163
|
-
legacy_groups =
|
|
164
|
+
versions |= configured_values(/\Asnmp-server group\s+\S+\s+(v1|v2c|v3)\b/)
|
|
165
|
+
legacy_groups = matching_blocks(/\Asnmp-server group\s+\S+\s+v(?:1|2c)\b/)
|
|
164
166
|
if legacy_groups.any?
|
|
165
167
|
@facts.unknown_snmp_permission(lines: legacy_groups.map(&:line), code: :unsupported_snmp_group)
|
|
166
168
|
unknown("snmp.acl", legacy_groups, code: :unsupported_snmp_group)
|
|
@@ -171,10 +173,10 @@ module Baseline
|
|
|
171
173
|
# NX-OS 通常会同步 CLI 与 SNMP 账号;缺少证据时不能构造虚拟用户。
|
|
172
174
|
def nxos_snmp_coverage(users, enabled:)
|
|
173
175
|
return if enabled == false
|
|
174
|
-
sync_disabled =
|
|
176
|
+
sync_disabled = configured_state(/\Asnmp-server disable snmp-aaa sync\z/, /\Ano snmp-server disable snmp-aaa sync\z/)
|
|
175
177
|
return if sync_disabled == true
|
|
176
178
|
|
|
177
|
-
|
|
179
|
+
matching_blocks(/\Ausername\s+/).group_by { |entry| entry.text.split[1] }.each do |name, entries|
|
|
178
180
|
roles = entries.filter_map { |entry| entry.text[/\brole\s+(\S+)\s*\z/, 1] }.uniq
|
|
179
181
|
covered = users[name]&.fetch(:roles, []) || []
|
|
180
182
|
next if roles.any? && (roles - covered).empty?
|
|
@@ -195,9 +197,9 @@ module Baseline
|
|
|
195
197
|
# 解析实际生效的认证方法及外部服务器引用。
|
|
196
198
|
def authentication
|
|
197
199
|
servers, aliases, keyed = [], {}, []
|
|
198
|
-
global_key =
|
|
200
|
+
global_key = matching_blocks(/\Atacacs-server key\s+/)
|
|
199
201
|
global_key.each { |entry| parse_key("tacacs", entry.text.sub(/\Atacacs-server key\s+/, ""), entry) }
|
|
200
|
-
|
|
202
|
+
matching_blocks(/\Atacacs-server host\s+/).each do |entry|
|
|
201
203
|
host = entry.text.split[2]
|
|
202
204
|
servers << host
|
|
203
205
|
key = entry.text[/\bkey\s+(.+)\z/, 1]
|
|
@@ -206,17 +208,17 @@ module Baseline
|
|
|
206
208
|
keyed << host
|
|
207
209
|
end
|
|
208
210
|
end
|
|
209
|
-
|
|
210
|
-
hosts =
|
|
211
|
+
matching_blocks(/\Atacacs server\s+/).each do |section|
|
|
212
|
+
hosts = configured_values(/\Aaddress ipv[46]\s+(\S+)/, section.children)
|
|
211
213
|
aliases[section.text.split.last] = hosts
|
|
212
214
|
servers |= hosts
|
|
213
|
-
|
|
215
|
+
matching_blocks(/\Akey\s+/, section.children).each do |entry|
|
|
214
216
|
parse_key("tacacs:#{section.text.split.last}", entry.text.delete_prefix("key "), entry)
|
|
215
217
|
keyed |= hosts
|
|
216
218
|
end
|
|
217
219
|
end
|
|
218
|
-
groups =
|
|
219
|
-
members =
|
|
220
|
+
groups = matching_blocks(/\Aaaa group server tacacs\+\s+/).to_h do |section|
|
|
221
|
+
members = configured_values(/\Aserver(?: name)?\s+(\S+)/, section.children)
|
|
220
222
|
[ section.text.split.last, members.flat_map { |member| aliases.fetch(member, [ member ]) } ]
|
|
221
223
|
end
|
|
222
224
|
record("tacacs.servers", servers.uniq)
|
|
@@ -225,14 +227,14 @@ module Baseline
|
|
|
225
227
|
patterns = { "authorization" => /\Aaaa authorization (?:exec|commands(?: \d+)?) default\s+(.+)\z/,
|
|
226
228
|
"accounting" => /\Aaaa accounting (?:(?:exec|commands(?: \d+)?) )?default\s+(.+)\z/ }
|
|
227
229
|
patterns.each do |kind, pattern|
|
|
228
|
-
references =
|
|
230
|
+
references = configured_values(pattern).flat_map { |methods| methods.scan(/\bgroup\s+(\S+)/).flatten }
|
|
229
231
|
selected = references.flat_map { |name| name == "tacacs+" ? servers : groups.fetch(name, []) }.uniq
|
|
230
232
|
unresolved = references.any? { |name| name != "radius" && name != "tacacs+" && !groups.key?(name) } || (selected - servers).any?
|
|
231
233
|
if unresolved
|
|
232
|
-
unknown("tacacs.#{kind}",
|
|
234
|
+
unknown("tacacs.#{kind}", matching_blocks(pattern), code: :tacacs_group_unresolved)
|
|
233
235
|
else
|
|
234
236
|
used = selected.any?
|
|
235
|
-
record("tacacs.#{kind}", used,
|
|
237
|
+
record("tacacs.#{kind}", used, matching_blocks(pattern)) if @facts.complete? || used
|
|
236
238
|
end
|
|
237
239
|
end
|
|
238
240
|
record("tacacs.keys_present", global_key.any? || (servers - keyed).empty?) unless servers.empty?
|
|
@@ -242,7 +244,7 @@ module Baseline
|
|
|
242
244
|
# 解析各 VTY 实际选用的登录列表,包括命名列表。
|
|
243
245
|
def tacacs_login(servers, groups)
|
|
244
246
|
pattern = /\Aaaa authentication login\s+(\S+)\s+(.+)\z/
|
|
245
|
-
lists =
|
|
247
|
+
lists = matching_blocks(pattern).each_with_object({}) do |entry, result|
|
|
246
248
|
name, methods = analyze(entry) { entry.text.match(pattern).captures }
|
|
247
249
|
result[name] = { methods: methods, entry: entry }
|
|
248
250
|
end
|
|
@@ -296,26 +298,26 @@ module Baseline
|
|
|
296
298
|
# 提取已配置的凭据编码方式,不保留秘密值。
|
|
297
299
|
def parse_key(owner, text, entry)
|
|
298
300
|
match = /\A(?:(\d+)\s+)?(\S+)/.match(text)
|
|
299
|
-
|
|
301
|
+
record_credential(owner, "tacacs", match&.[](1) || "plain", match&.[](2), entry)
|
|
300
302
|
end
|
|
301
303
|
|
|
302
304
|
# 提取本地账号角色及凭据存储属性。
|
|
303
305
|
def accounts
|
|
304
|
-
|
|
306
|
+
matching_blocks(/\Ausername\s+/).group_by { |entry| entry.text.split[1] }.each do |name, entries|
|
|
305
307
|
passwords = entries.filter_map do |entry|
|
|
306
308
|
match = /\b(?:password|secret)\s+(?:(\d+)\s+)?(\S+)/.match(entry.text)
|
|
307
|
-
|
|
309
|
+
record_credential(name, "local_user", match[1] || "plain", match[2], entry) if match
|
|
308
310
|
end
|
|
309
|
-
|
|
311
|
+
record_credential(name, "local_user", "unknown", nil, entries.first) if passwords.empty?
|
|
310
312
|
end
|
|
311
|
-
|
|
313
|
+
matching_blocks(/\Aenable (?:password|secret)\s+/).each do |entry|
|
|
312
314
|
match = /\Aenable (?:password|secret)(?: level \d+)?\s+(?:(\d+)\s+)?(\S+)/.match(entry.text)
|
|
313
|
-
|
|
315
|
+
record_credential("enable", "enable", match ? match[1] || "plain" : "unknown", match&.[](2), entry)
|
|
314
316
|
end
|
|
315
|
-
|
|
316
|
-
|
|
317
|
+
matching_blocks(/\Aline\b/).each do |section|
|
|
318
|
+
matching_blocks(/\Apassword\b/, section.children).each do |entry|
|
|
317
319
|
match = /\Apassword\s+(?:(\d+)\s+)?(\S+)/.match(entry.text)
|
|
318
|
-
|
|
320
|
+
record_credential(section.text, "line", match ? match[1] || "plain" : "unknown", match&.[](2), entry)
|
|
319
321
|
end
|
|
320
322
|
end
|
|
321
323
|
end
|