Files
nodeagent/tests/unit/acl-v11.test.mjs
T
jkooe b9236dfaf1 chore(privacy): 公开仓库隐私脱敏 —— 清除可定位到个人与设备的字面量
按 security-reports/ 的隐私审查台账逐项收口。纯脱敏与表述泛化,
不含任何业务逻辑变更;实测结论与排查步骤一字未动。

P1(可定位到具体设备/个人)
- scripts/verify-b2.mjs:NA_URL 去掉硬编码缺省值(原指向一台真实机器),改为必填。
  这是全仓库唯一「拿着就能连到某台真机」的常量。
- 测试账户名 pdverify → na-test(docs/VM-VERIFICATION.md 7 处、docs/console-plan.md 1 处)
- 虚拟机与宿主的真实网段 → RFC 5737 文档保留段 192.0.2.x(含 CLOSE_WAIT 记录)
- 跨项目名与私人称呼 → 中性表述(docs/PRD.md、docs/DEVELOPMENT.md、docs/console-plan.md 等)

P2(低风险设备指纹)
- 虚拟化产品及其 CLI / 访客工具名 → 中性表述(12 个文件 + 1 个工作流注释)
- 测试目录 C:\nodeagent-test → C:\na-test
- NAS 设备型号与系统名 → NAS 设备 / NAS 系统(docs/DEPLOY-LINUX.md、scripts/install-linux.sh、README.md)
- 真机 CPU 型号、真机物理分辨率、代理软件品牌与监听端口、杀软品牌 → 泛化
- 单元测试示例段去掉真机 IP 尾八位:acl-v11 / net-scripts 的 fixture 与断言同步更新
- .gitignore 补 config.json / agent.json(含真实密钥与节点地址,绝不入库)
- 顺带移除混入技术文档的「不构成投资建议」字样(与文档主题无关)

验证:pnpm test:unit 340/340 通过;独立扫描器对 235 个跟踪文件做 17 类模式
全量扫描,命中 0。
2026-10-13 22:35:40 +08:00

166 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
ipMatchesCidr,
ipAllowed,
inTimeWindow,
rateLimitFor,
authorize,
normalizeIp,
} from '../../packages/protocol/dist/index.js';
// ---------- B3:对端地址规范化(真机踩坑回归) ----------
test('normalizeIp:IPv4-mapped IPv6 归一为 IPv4(真机坑)', () => {
assert.equal(normalizeIp('::ffff:192.168.1.10'), '192.168.1.10');
assert.equal(normalizeIp('::FFFF:192.168.1.10'), '192.168.1.10');
});
test('normalizeIp:IPv4:port 去端口;纯 IP 原样', () => {
assert.equal(normalizeIp('192.168.1.10:51234'), '192.168.1.10');
assert.equal(normalizeIp('192.168.1.10'), '192.168.1.10');
});
test('normalizeIp:真 IPv6 原样保留(不误当 IPv4)', () => {
assert.equal(normalizeIp('fe80::1'), 'fe80::1');
assert.equal(normalizeIp('[fe80::1]:8080'), 'fe80::1');
assert.equal(normalizeIp(''), '');
assert.equal(normalizeIp(undefined), '');
});
test('normalizeIp + deny_cidr:mapped 地址也能命中黑名单(端到端复现)', () => {
const ip = normalizeIp('::ffff:192.168.1.10');
const client = { client_id: 'mac_01', allow: [], deny_cidr: ['192.168.1.10'] };
assert.equal(ipAllowed(client, ip).allowed, false);
});
// ---------- B3:CIDR ----------
test('CIDR:单 IP 精确匹配', () => {
assert.equal(ipMatchesCidr('192.168.1.10', '192.168.1.10'), true);
assert.equal(ipMatchesCidr('192.168.1.11', '192.168.1.10'), false);
});
test('CIDR:/24 网段匹配', () => {
assert.equal(ipMatchesCidr('192.168.1.10', '192.168.1.0/24'), true);
assert.equal(ipMatchesCidr('192.168.1.255', '192.168.1.0/24'), true);
// ⚠️ 这条测的是「**跨网段不匹配**」—— 必须用不同网段的地址。
// (2026-10-10 一次机械 IP 替换曾把它改成同段,导致断言必红 —— 语义测试别被批量替换破坏)
assert.equal(ipMatchesCidr('192.168.2.1', '192.168.1.0/24'), false);
});
test('CIDR:/16 与 0.0.0.0/0', () => {
assert.equal(ipMatchesCidr('10.0.1.2', '10.0.0.0/16'), true, '10.0.x.x 在 10.0.0.0/16 内');
assert.equal(ipMatchesCidr('10.1.2.3', '10.0.0.0/16'), false, '10.1.x.x 不在 10.0.0.0/16 内');
assert.equal(ipMatchesCidr('10.1.2.3', '10.1.0.0/16'), true);
assert.equal(ipMatchesCidr('8.8.8.8', '0.0.0.0/0'), true);
});
test('CIDR:非法输入不误放行(IPv6/主机名/越界位)', () => {
assert.equal(ipMatchesCidr('::1', '0.0.0.0/0'), false);
assert.equal(ipMatchesCidr('localhost', '0.0.0.0/0'), false);
assert.equal(ipMatchesCidr('192.168.1.1', '192.168.1.0/99'), false);
assert.equal(ipMatchesCidr('192.168.1.999', '192.168.1.0/24'), false);
});
test('F-01:空的掩码位数必须判非法,不得被当成 /0 而放行全部', () => {
// 真机复现:原来只做 `Number.isFinite(Number(bitsRaw))`,而 **Number('') === 0**
// → '10.0.0.0/' 被当成 /0 = 匹配全部,于是 `ipMatchesCidr('8.8.8.8','10.0.0.0/')` 返回 true。
// 这类「配置写漏了却被读成最宽松」的形态最危险:不报错、且方向正好相反。
assert.equal(ipMatchesCidr('8.8.8.8', '10.0.0.0/'), false, '空掩码不得被当成 /0');
assert.equal(ipMatchesCidr('10.0.0.1', '10.0.0.0/'), false);
assert.equal(ipMatchesCidr('8.8.8.8', '0.0.0.0/'), false);
// 其它畸形位长同样不得放行
assert.equal(ipMatchesCidr('8.8.8.8', '0.0.0.0/ '), false);
assert.equal(ipMatchesCidr('8.8.8.8', '0.0.0.0/+0'), false);
assert.equal(ipMatchesCidr('8.8.8.8', '0.0.0.0/0x0'), false);
assert.equal(ipMatchesCidr('8.8.8.8', '0.0.0.0/-1'), false);
// 对照组(铁律 20:预期逐条推演,别把正确行为误报成缺陷)
assert.equal(ipMatchesCidr('8.8.8.8', '0.0.0.0/0'), true, '显式的 /0 是合法的「匹配全部」');
assert.equal(ipMatchesCidr('10.0.0.0', '10.0.0.0/32'), true, '/32 只含这一个地址');
assert.equal(ipMatchesCidr('10.0.0.1', '10.0.0.0/32'), false, '/32 不含 10.0.0.1');
});
test('IP 白名单:deny 优先、白名单为空则放行', () => {
assert.equal(ipAllowed({ client_id: 'a', allow: [] }, '1.2.3.4').allowed, true);
assert.equal(
ipAllowed({ client_id: 'a', allow: [], allow_cidr: ['192.168.1.0/24'] }, '192.168.1.5').allowed,
true,
);
assert.equal(
ipAllowed({ client_id: 'a', allow: [], allow_cidr: ['192.168.1.0/24'] }, '10.0.0.1').allowed,
false,
);
const both = { client_id: 'a', allow: [], allow_cidr: ['192.168.1.0/24'], deny_cidr: ['192.168.1.10'] };
assert.equal(ipAllowed(both, '192.168.1.10').allowed, false, '黑名单优先');
assert.equal(ipAllowed(both, '192.168.1.11').allowed, true);
});
// ---------- B3:生效时段 ----------
test('时段:未配置即全天可用', () => {
assert.equal(inTimeWindow(undefined, new Date('2026-09-29T03:00:00')).allowed, true);
});
test('时段:普通区间与跨零点区间', () => {
const work = { from: '09:00', to: '18:00' };
assert.equal(inTimeWindow(work, new Date('2026-09-29T10:30:00')).allowed, true);
assert.equal(inTimeWindow(work, new Date('2026-09-29T20:00:00')).allowed, false);
const night = { from: '22:00', to: '06:00' };
assert.equal(inTimeWindow(night, new Date('2026-09-29T23:30:00')).allowed, true, '当天深夜');
assert.equal(inTimeWindow(night, new Date('2026-09-29T02:00:00')).allowed, true, '次日凌晨');
assert.equal(inTimeWindow(night, new Date('2026-09-29T12:00:00')).allowed, false);
});
test('时段:星期限制(1=周一…7=周日)', () => {
const weekday = { days: [1, 2, 3, 4, 5] };
assert.equal(inTimeWindow(weekday, new Date('2026-09-29T10:00:00')).allowed, true, '周二');
assert.equal(inTimeWindow(weekday, new Date('2026-10-03T10:00:00')).allowed, false, '周六');
assert.equal(inTimeWindow({ days: [7] }, new Date('2026-10-04T10:00:00')).allowed, true, '周日=7');
});
test('时段:非法格式拒绝(不静默放行)', () => {
assert.equal(inTimeWindow({ from: '9am', to: '6pm' }, new Date()).allowed, false);
assert.equal(inTimeWindow({ from: '25:00', to: '26:00' }, new Date()).allowed, false);
});
// ---------- B3:按能力限速 ----------
test('限速:精确匹配优先,其次 glob,最后回退全局', () => {
const client = {
client_id: 'mac_01',
allow: ['*'],
max_calls_per_min: 60,
rate_limits: { 'fs.read': 10, 'fs.*': 30, 'system.shell.exec': 5 },
};
assert.equal(rateLimitFor(client, 'fs.read'), 10, '精确优先于 glob');
assert.equal(rateLimitFor(client, 'fs.write'), 30, '继承 glob');
assert.equal(rateLimitFor(client, 'system.shell.exec'), 5);
assert.equal(rateLimitFor(client, 'system.info'), 60, '回退全局');
});
test('限速:无任何配置即不限速', () => {
assert.equal(rateLimitFor({ client_id: 'a', allow: [] }, 'system.info'), undefined);
});
// ---------- 回归:CIDR/时段不影响原有授权语义 ----------
test('授权语义:deny 优先于 allow(与 v11 新字段共存)', () => {
const policy = {
default_effect: 'deny',
clients: [
{
client_id: 'mac_01',
allow: ['fs.*'],
deny: ['fs.write'],
allow_cidr: ['192.168.1.0/24'],
rate_limits: { 'fs.*': 20 },
},
],
};
assert.equal(authorize(policy, 'mac_01', 'fs.read').allowed, true);
assert.equal(authorize(policy, 'mac_01', 'fs.write').allowed, false);
assert.equal(authorize(policy, 'mac_01', 'system.info').allowed, false);
});