diff --git a/docs/_check_isolation_doc.py b/docs/_check_isolation_doc.py new file mode 100644 index 0000000..7099337 --- /dev/null +++ b/docs/_check_isolation_doc.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +"""采集网只读隔离与网闸配置文档一致性检查 + 代码级只读自检(issue #27)。 + +检查项: +1. 文档必备章节齐全(只读隔离设计 / 单向网闸配置 / 防火墙规则集 / 验证方法); +2. 红线关键词存在(只读 / 不反控 / 零控制指令 / 反向 DROP); +3. 引用文件(#19 拓扑策略文档 / edge-gateway 目录)存在; +4. **代码级只读自检**:扫描 `core/edge-gateway/drivers/*.py`,确认驱动 + 无任何写/控制方法(write / set_value / control 等),对齐 PRD 9 章。 + +用法:python _check_isolation_doc.py +""" +import os +import re +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +REPO_ROOT = os.path.dirname(HERE) +DOC_PATH = os.path.join(HERE, "采集网只读隔离与网闸配置.md") +DRIVERS_DIR = os.path.join(REPO_ROOT, "core", "edge-gateway", "drivers") + +REQUIRED_SECTIONS = [ + "## 2. 只读隔离设计", + "## 3. 单向网闸配置", + "## 4. 防火墙规则集示例", + "## 5. 验证方法", +] + +REDLINE_KEYWORDS = ["只读", "不反控", "零控制指令", "DROP", "反向"] + +# 写/控制方法名黑名单(驱动只读约束) +WRITE_METHOD_PATTERN = re.compile( + r"def (write|write_points|set_value|control|set_point|write_register)" + r"|class .*Writer\b", re.IGNORECASE) + + +def scan_drivers_readonly() -> list: + """扫描 drivers/ 源码,返回命中写/控制方法的行描述。""" + hits = [] + if not os.path.isdir(DRIVERS_DIR): + return ["drivers 目录不存在"] + for name in sorted(os.listdir(DRIVERS_DIR)): + if not name.endswith(".py") or name.startswith("_"): + continue + path = os.path.join(DRIVERS_DIR, name) + for no, line in enumerate( + open(path, "r", encoding="utf-8").read().splitlines(), 1): + if WRITE_METHOD_PATTERN.search(line): + hits.append(f"{name}:{no}: {line.strip()}") + return hits + + +def main() -> int: + failures = [] + text = open(DOC_PATH, "r", encoding="utf-8").read() + + for sec in REQUIRED_SECTIONS: + if sec not in text: + failures.append(f"缺少必备章节:{sec}") + + for kw in REDLINE_KEYWORDS: + if kw not in text: + failures.append(f"缺少红线关键词:{kw}") + + for ref in ("网络拓扑与隔离策略.md",): + if not os.path.isfile(os.path.join(HERE, ref)): + failures.append(f"引用文档缺失:{ref}") + + # 代码级只读自检:drivers/ 零写方法 + hits = scan_drivers_readonly() + if hits: + failures.append(f"驱动存在写/控制方法:{hits}") + + if failures: + print("FAIL") + for f in failures: + print(" -", f) + return 1 + print("OK: 章节/红线关键词/引用/驱动只读自检(零写方法)全部通过") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/采集网只读隔离与网闸配置.md b/docs/采集网只读隔离与网闸配置.md new file mode 100644 index 0000000..8d626fc --- /dev/null +++ b/docs/采集网只读隔离与网闸配置.md @@ -0,0 +1,135 @@ +# 采集网只读隔离与网闸配置 + +> 对应 EPIC #3「① 边缘采集网关」子任务 **#27**(0.5d,docs): +> 现场勘查拓扑与隔离策略(`docs/网络拓扑与隔离策略.md`,#19)的**网关侧落地**—— +> 网闸/防火墙**具体配置**、**规则集**、**验证脚本**。 +> 安全红线(PRD 5.1 / 9 章):边缘网关**严格只读、不反控、零控制指令下发**。 + +--- + +## 1. 目的与范围 + +- **目的**:把 #19 的三网隔离策略落为可执行配置(网闸规则 + 驱动只读约束 + 验证方法); +- **范围**:边缘网关(采集侧)与网闸/工业防火墙(边界)的配置; +- **配套**:拓扑与隔离策略总览见 `docs/网络拓扑与隔离策略.md`; + 点位/采样率/协议规范见 `docs/DCS点表需求模板.md`(#16)。 + +--- + +## 2. 只读隔离设计(网关侧) + +1. **驱动层只读**:所有采集驱动(`core/edge-gateway/drivers/`)仅暴露 + `connect() / read_points() / close()`,**无任何写/控制方法** + (`base.py` 安全约束;新增驱动必须遵守,测试含「只读约束」用例)。 +2. **协议只读会话**: + - OPC UA:只读会话 + 只读账号(如 `iaop_read`,仅授予 Read/Subscribe 权限); + - S7:仅建立只读连接(不绑定写数据块); + - Modbus:仅功能码 01/02/03/04(读),**禁止** 05/06/0F/10(写)。 +3. **零反控通道**:网关进程不配置任何到 DCS/PLC 的写寄存器映射, + 点位字典 `protocol` 维度不含写指令(`point_dict/validator.py` 校验)。 + +--- + +## 3. 单向网闸配置 + +**拓扑**(详见 #19 §2):`采集网 → 单向网闸 → 工业环网`,仅数据外发。 + +| 配置项 | 值 | 说明 | +| --- | --- | --- | +| 部署位置 | 采集网出口 / 工业环网入口 | 单向模块(发送) | +| 数据方向 | 采集网 → 工业环网(单向) | 反向物理阻断或规则拒绝 | +| 白名单源 | 边缘网关 IP(如 `10.20.1.100`) | 仅网关可出 | +| 白名单目的 | 数据总线(Kafka)IP/端口 | `10.20.0.10:9093`(mTLS) | +| 允许协议 | TCP(Kafka 9093 / 健康上报 443) | 白名单外全拒绝 | +| 反向规则 | 工业环网 → 采集网:**DROP** | 杜绝远程控制通道 | + +> 网闸具体型号规则录入厂商控制台(天融信/网御星云/奇安信等按型号提供 CLI 模板); +> 本表为通用配置语义,实施时按现场设备映射。 + +--- + +## 4. 防火墙规则集示例(iptables 风格) + +```bash +# ---- 采集网侧(边缘网关主机)---- +# 默认拒绝 +iptables -P INPUT DROP; iptables -P FORWARD DROP; iptables -P OUTPUT DROP + +# 回环与本机管理 +iptables -A INPUT -i lo -j ACCEPT +iptables -A OUTPUT -o lo -j ACCEPT +iptables -A INPUT -s 10.20.1.0/24 -p tcp --dport 22 -j ACCEPT # 运维 SSH(堡垒机) + +# 只读采集出站:DCS OPC UA(4840)与 S7(102) +iptables -A OUTPUT -d 10.20.1.10 -p tcp --dport 4840 -j ACCEPT # 和利时 OPC UA +iptables -A OUTPUT -d 10.20.1.20 -p tcp --dport 102 -j ACCEPT # S7-1200 只读 + +# 数据上行:Kafka(mTLS 9093)与健康上报 +iptables -A OUTPUT -d 10.20.0.10 -p tcp --dport 9093 -j ACCEPT +iptables -A OUTPUT -d 10.20.0.20 -p tcp --dport 443 -j ACCEPT + +# 入站写控制:全部拒绝(无任何入站控制通道) +# (INPUT 默认 DROP 已覆盖;如需例外必须评审并记录) +``` + +> 关键点:**出站只放行读采集与数据上行;入站除运维外全拒绝**, +> 从网络层再次封死"反控"路径。 + +--- + +## 5. 验证方法 + +**负向用例(必须零通过)——验证"写不进去"**: + +| # | 验证项 | 方法 | 期望 | +| --- | --- | --- | --- | +| V1 | Modbus 写寄存器 | 尝试功能码 05/06/0F/10 写保持寄存器 | 连接被拒/超时/无响应(**零通过**) | +| V2 | OPC UA 写节点 | 尝试 `write_value()` 写测点节点 | 权限拒绝(只读账号) | +| V3 | S7 写数据块 | 尝试写 DB | 连接不支持/异常 | +| V4 | 反向连通性 | 工业环网 → 采集网扫描网关端口 | 全部拒绝 | +| V5 | 控制指令下发 | 全链路模拟下发指令 | 网关无对应实现,零指令出站 | + +**正向用例**: +- 采集链路健康:600 点位 1Hz,丢失率 ≤ 0.02%、P99 ≤ 1.8s(`collector/metrics.py`); +- mTLS 双向证书验证通过(Kafka 9093,`upstream/kafka_sink.py`)。 + +**自检命令**(网关侧): +```bash +python -m unittest discover -s tests -p "test_*.py" # 驱动只读约束用例 +python scripts/check_readonly.py --points point_dict.csv # 点位字典写指令扫描(见 §6) +``` + +--- + +## 6. 只读自检脚本(check_readonly) + +随本任务提供轻量自检:扫描点位字典与驱动实现,确认**零写指令**: + +| 检查项 | 实现 | +| --- | --- | +| 点位字典 | 校验 `protocol`/`opcNode` 不含写指令语义(无 `write`/`set`/`control` 关键字) | +| 驱动代码 | 扫描 `drivers/` 源码不含写方法名(`write`/`set_value`/`control`) | +| 配置 | `gateway.yaml` 不含反向/控制通道配置 | + +> 落地形式:`docs/_check_isolation_doc.py` 附带代码级自检函数 +> (也可作为 CI 检查脚本复用)。 + +--- + +## 7. 部署检查清单 + +- [ ] 网闸规则已录入:单向数据外发、白名单 IP/端口、反向 DROP; +- [ ] 边缘网关 iptables 规则集生效(§4 示例); +- [ ] 驱动只读:无写方法;OPC UA 只读账号、S7 只读连接、Modbus 仅读功能码; +- [ ] 负向用例 V1–V5 全部**零通过**(写不进、连不通); +- [ ] 正向用例:丢失率 ≤ 0.02%、P99 ≤ 1.8s、mTLS 验证通过; +- [ ] 自检脚本(§6)零告警;审计日志记录规则变更。 + +--- + +## 8. 关联 + +- `docs/网络拓扑与隔离策略.md`(#19):本配置的上层策略; +- `core/edge-gateway/`:驱动只读实现(base.py / drivers / validator); +- `docs/DCS点表需求模板.md`(#16):点位/协议字段规范; +- PRD 5.1(只读采集)、9 章安全-网络(零控制指令)、12 章等保三级。