@sofagent/rules 1.3.7 → 1.3.9

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.
Files changed (45) hide show
  1. package/dist/approval-mode.js +1 -1
  2. package/dist/ast/engine.d.ts +52 -0
  3. package/dist/ast/engine.js +291 -0
  4. package/dist/ast/fixtures/vuln-db.json +25 -0
  5. package/dist/ast/index.d.ts +10 -0
  6. package/dist/ast/index.js +32 -0
  7. package/dist/ast/plugin-adapter.d.ts +35 -0
  8. package/dist/ast/plugin-adapter.js +71 -0
  9. package/dist/ast/rules/asi01-prompt-injection.d.ts +3 -0
  10. package/dist/ast/rules/asi01-prompt-injection.js +65 -0
  11. package/dist/ast/rules/asi04-sbom.d.ts +30 -0
  12. package/dist/ast/rules/asi04-sbom.js +158 -0
  13. package/dist/ast/rules/index.d.ts +6 -0
  14. package/dist/ast/rules/index.js +34 -0
  15. package/dist/ast/rules/no-child-process-shell.d.ts +3 -0
  16. package/dist/ast/rules/no-child-process-shell.js +57 -0
  17. package/dist/ast/rules/no-debugger.d.ts +3 -0
  18. package/dist/ast/rules/no-debugger.js +22 -0
  19. package/dist/ast/rules/no-dynamic-require.d.ts +3 -0
  20. package/dist/ast/rules/no-dynamic-require.js +34 -0
  21. package/dist/ast/rules/no-empty-catch.d.ts +3 -0
  22. package/dist/ast/rules/no-empty-catch.js +29 -0
  23. package/dist/ast/rules/no-eval.d.ts +3 -0
  24. package/dist/ast/rules/no-eval.js +33 -0
  25. package/dist/ast/rules/no-hardcoded-secret.d.ts +3 -0
  26. package/dist/ast/rules/no-hardcoded-secret.js +59 -0
  27. package/dist/ast/rules/no-insecure-url.d.ts +3 -0
  28. package/dist/ast/rules/no-insecure-url.js +32 -0
  29. package/dist/ast/rules/no-sql-string-concat.d.ts +3 -0
  30. package/dist/ast/rules/no-sql-string-concat.js +52 -0
  31. package/dist/ast/rules/semver.d.ts +11 -0
  32. package/dist/ast/rules/semver.js +58 -0
  33. package/dist/ast/types.d.ts +81 -0
  34. package/dist/ast/types.js +12 -0
  35. package/dist/ast/walk.d.ts +14 -0
  36. package/dist/ast/walk.js +69 -0
  37. package/dist/engine.js +1 -1
  38. package/dist/index.d.ts +6 -0
  39. package/dist/index.js +20 -6
  40. package/dist/rules/index.js +1 -1
  41. package/dist/rules/tool-injection.js +1 -1
  42. package/dist/rules/tool-secret-leak.js +2 -2
  43. package/dist/rules/tool-sensitive-file.js +1 -1
  44. package/dist/types.js +1 -1
  45. package/package.json +24 -5
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // asi04-sbom.ts · OWASP ASI04 供应链 SBOM 检测
4
+ // v1.3.9(一):扫描依赖清单(package.json / go.mod)→ 生成 SBOM →
5
+ // 查离线样例漏洞库(fixtures/vuln-db.json,不依赖 CI 联网)
6
+ //
7
+ // Microsoft AGT 启发:受损插件/子 agent 经由依赖清单注入恶意行为——
8
+ // SBOM 是把「装了什么」变成可审计事实的第一步
9
+ // ============================================================
10
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11
+ return (mod && mod.__esModule) ? mod : { "default": mod };
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.asi04SbomRule = void 0;
15
+ exports.parsePackageLock = parsePackageLock;
16
+ exports.parsePackageJson = parsePackageJson;
17
+ exports.parseGoMod = parseGoMod;
18
+ exports.buildSbom = buildSbom;
19
+ const vuln_db_json_1 = __importDefault(require("../fixtures/vuln-db.json"));
20
+ const semver_1 = require("./semver");
21
+ /** 依赖清单文件匹配——lockfile 优先(精确版本),manifest 兜底(range 近似) */
22
+ const MANIFEST_FILE = /(package-lock\.json|npm-shrinkwrap\.json|package\.json|go\.mod|go\.sum)$/;
23
+ /** 剥离 npm 版本前缀(^1.2.3 → 1.2.3) */
24
+ function stripPrefix(v) {
25
+ return v.replace(/^[\^~>=<\s]+/, '').split(' ')[0] ?? v;
26
+ }
27
+ /** 判断版本串是否带 range 前缀(^ / ~ / 区间),带则命中结果不确定 */
28
+ function hasRangePrefix(v) {
29
+ return /^[\^~<>]/.test(v);
30
+ }
31
+ /**
32
+ * 解析 package-lock.json → SBOM 条目(精确锁定版本——v2/v3 的 packages 对象;v1 兜底 dependencies 嵌套)。
33
+ * v1.3.9 阶段四修复(fresh-eyes 视角7):ASI04 此前只扫 manifest 的 range(^4.17.20),
34
+ * range 宽则误报、窄则漏报(注释自承「精确锁定版本在 lock 文件里」)。本函数用 lockfile
35
+ * 的精确版本做漏洞匹配——消除假阳/假阴;manifest 仅作无 lockfile 时的 fallback。
36
+ */
37
+ function parsePackageLock(text) {
38
+ let lock;
39
+ try {
40
+ lock = JSON.parse(text);
41
+ }
42
+ catch {
43
+ return [];
44
+ }
45
+ const entries = [];
46
+ // v2/v3:packages 对象,key 如 "node_modules/foo" / ""(根)
47
+ if (lock.packages) {
48
+ for (const [key, meta] of Object.entries(lock.packages)) {
49
+ if (!key || key === '' || !meta?.version)
50
+ continue; // 跳过根包("")与 workspace 链接(version 缺失)
51
+ entries.push({ name: key.replace(/^node_modules\//, ''), version: meta.version, ecosystem: 'npm', line: 1 });
52
+ }
53
+ return entries;
54
+ }
55
+ // v1:dependencies 嵌套(含 transitive)
56
+ const walk = (deps) => {
57
+ if (!deps)
58
+ return;
59
+ for (const [name, meta] of Object.entries(deps)) {
60
+ if (meta?.version)
61
+ entries.push({ name, version: meta.version, ecosystem: 'npm', line: 1 });
62
+ walk(meta?.dependencies);
63
+ }
64
+ };
65
+ walk(lock.dependencies);
66
+ return entries;
67
+ }
68
+ /**
69
+ * 解析 package.json → SBOM 条目。
70
+ * 版本声明形如 "^4.17.20"——无 lockfile 时剥离前缀近似(保守:命中漏洞标「range 不确定」)。
71
+ */
72
+ function parsePackageJson(text) {
73
+ let manifest;
74
+ try {
75
+ manifest = JSON.parse(text);
76
+ }
77
+ catch {
78
+ return [];
79
+ }
80
+ const entries = [];
81
+ const sections = [
82
+ manifest.dependencies,
83
+ manifest.devDependencies,
84
+ manifest.optionalDependencies,
85
+ ];
86
+ for (const section of sections) {
87
+ if (!section)
88
+ continue;
89
+ for (const [name, version] of Object.entries(section)) {
90
+ entries.push({ name, version: stripPrefix(version), ecosystem: 'npm', line: 1 });
91
+ }
92
+ }
93
+ return entries;
94
+ }
95
+ /** 解析 go.mod → SBOM 条目(require 行 + require 块两种形态) */
96
+ function parseGoMod(text) {
97
+ const entries = [];
98
+ const lines = text.split('\n');
99
+ let inRequireBlock = false;
100
+ for (let i = 0; i < lines.length; i++) {
101
+ const line = lines[i] ?? '';
102
+ const trimmed = line.trim();
103
+ if (trimmed === 'require (') {
104
+ inRequireBlock = true;
105
+ continue;
106
+ }
107
+ if (trimmed === ')') {
108
+ inRequireBlock = false;
109
+ continue;
110
+ }
111
+ const m = inRequireBlock
112
+ ? /^([\w./~-]+)\s+(v[\w.\-+]+)/.exec(trimmed)
113
+ : /^require\s+([\w./~-]+)\s+(v[\w.\-+]+)/.exec(trimmed);
114
+ if (m) {
115
+ // go.mod 版本带 v 前缀(v1.4.0)——剥掉再进区间比较
116
+ entries.push({ name: m[1] ?? '', version: (m[2] ?? '').replace(/^v/, ''), ecosystem: 'go', line: i + 1 });
117
+ }
118
+ }
119
+ return entries;
120
+ }
121
+ /** 生成 SBOM(按清单类型分派)——lockfile 优先(精确),manifest 兜底(range) */
122
+ function buildSbom(path, text) {
123
+ if (path.endsWith('package-lock.json') || path.endsWith('npm-shrinkwrap.json'))
124
+ return parsePackageLock(text);
125
+ if (path.endsWith('package.json'))
126
+ return parsePackageJson(text);
127
+ if (path.endsWith('go.mod'))
128
+ return parseGoMod(text);
129
+ return [];
130
+ }
131
+ exports.asi04SbomRule = {
132
+ id: 'asi04-sbom',
133
+ name: 'OWASP ASI04 供应链 SBOM 检测',
134
+ severity: 'FAIL',
135
+ description: '扫描依赖清单(lockfile 优先精确版本,manifest 兜底)生成 SBOM 并查离线样例漏洞库(受损插件注入恶意行为的供应链面)',
136
+ filePattern: MANIFEST_FILE,
137
+ checkText(ctx) {
138
+ const sbom = buildSbom(ctx.path, ctx.text);
139
+ if (sbom.length === 0)
140
+ return;
141
+ const db = vuln_db_json_1.default;
142
+ for (const entry of sbom) {
143
+ const advisories = db[entry.ecosystem]?.[entry.name];
144
+ if (!advisories)
145
+ continue;
146
+ for (const adv of advisories) {
147
+ const hit = adv.ranges.some((range) => (0, semver_1.inRange)(entry.version, range));
148
+ if (hit) {
149
+ // lockfile 精确命中 = 确定;manifest range 命中 = 不确定(区间内存在受影响版本,需核对 lockfile)
150
+ const isLock = /package-lock\.json|npm-shrinkwrap\.json/.test(ctx.path);
151
+ const qualifier = isLock ? '' : `(manifest range,受影响版本区间内;建议核对 ${entry.name.split('/')[0]}-lock 锁定版本)`;
152
+ ctx.report(entry.line, `[ASI04] 依赖 ${entry.name}@${entry.version} 命中已知漏洞 ${adv.id}(${adv.summary})${qualifier}`);
153
+ }
154
+ }
155
+ }
156
+ },
157
+ };
158
+ //# sourceMappingURL=asi04-sbom.js.map
@@ -0,0 +1,6 @@
1
+ import type { AstRule } from '../types';
2
+ /** 内置示范规则集(10 条:7 FAIL / 3 WARN) */
3
+ export declare const builtinAstRules: readonly AstRule[];
4
+ /** 按规则 ID 导出(规则集 JSON 生成与单测引用) */
5
+ export declare const astRuleById: ReadonlyMap<string, AstRule>;
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // rules/index.ts · 官方示范规则集注册表
4
+ // v1.3.9(一):10 条示范规则(8 条代码 AST + 2 条 OWASP 语义)
5
+ // 社区照这个结构写自己的 sofagent-ruleset-* 引擎
6
+ // ============================================================
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.astRuleById = exports.builtinAstRules = void 0;
9
+ const no_eval_1 = require("./no-eval");
10
+ const no_hardcoded_secret_1 = require("./no-hardcoded-secret");
11
+ const no_child_process_shell_1 = require("./no-child-process-shell");
12
+ const no_dynamic_require_1 = require("./no-dynamic-require");
13
+ const no_insecure_url_1 = require("./no-insecure-url");
14
+ const no_debugger_1 = require("./no-debugger");
15
+ const no_sql_string_concat_1 = require("./no-sql-string-concat");
16
+ const no_empty_catch_1 = require("./no-empty-catch");
17
+ const asi01_prompt_injection_1 = require("./asi01-prompt-injection");
18
+ const asi04_sbom_1 = require("./asi04-sbom");
19
+ /** 内置示范规则集(10 条:7 FAIL / 3 WARN) */
20
+ exports.builtinAstRules = [
21
+ no_eval_1.noEvalRule, // 禁止 eval() / new Function()
22
+ no_hardcoded_secret_1.noHardcodedSecretRule, // 禁止硬编码密钥(AST 语义级)
23
+ no_dynamic_require_1.noDynamicRequireRule, // 禁止动态 require(ASI04 关联)
24
+ no_debugger_1.noDebuggerRule, // 禁止 debugger 语句
25
+ no_child_process_shell_1.noChildProcessShellRule, // child_process shell 执行管控
26
+ no_sql_string_concat_1.noSqlStringConcatRule, // 禁止 SQL 字符串拼接
27
+ no_insecure_url_1.noInsecureUrlRule, // 禁止 http:// 明文端点
28
+ no_empty_catch_1.noEmptyCatchRule, // 禁止空 catch 块
29
+ asi01_prompt_injection_1.asi01PromptInjectionRule, // OWASP ASI01 目标劫持
30
+ asi04_sbom_1.asi04SbomRule,
31
+ ];
32
+ /** 按规则 ID 导出(规则集 JSON 生成与单测引用) */
33
+ exports.astRuleById = new Map(exports.builtinAstRules.map((r) => [r.id, r]));
34
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import type { AstRule } from '../types';
2
+ export declare const noChildProcessShellRule: AstRule;
3
+ //# sourceMappingURL=no-child-process-shell.d.ts.map
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // no-child-process-shell.ts · child_process shell 执行管控
4
+ // v1.3.9(一):exec/execSync 走 shell 解释器——参数拼接即命令注入。
5
+ // 语法级检测:文件里 import 了 child_process 的 exec* 绑定后又被调用
6
+ // ============================================================
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.noChildProcessShellRule = void 0;
9
+ const walk_1 = require("../walk");
10
+ /** child_process 家族里走 shell 的入口(spawn/spawnSync/fork 不走 shell,不在列) */
11
+ const SHELL_APIS = new Set(['exec', 'execSync']);
12
+ /** shell 元字符——出现在参数字面量里即高危 */
13
+ const SHELL_METACHARS = /[;&|`$><\n]/;
14
+ exports.noChildProcessShellRule = {
15
+ id: 'no-child-process-shell',
16
+ name: 'child_process shell 执行管控',
17
+ severity: 'WARN',
18
+ description: 'exec/execSync 走 shell——字面量含元字符或动态拼接参数即命令注入入口(动态参数 FAIL / 静态参数 WARN)',
19
+ checkCode(ctx) {
20
+ // 第一步:收集来自 child_process 的 import 绑定
21
+ const imports = (0, walk_1.collectImports)(ctx);
22
+ const shellBindings = new Set();
23
+ for (const [name, mod] of imports) {
24
+ if (mod === 'child_process' && SHELL_APIS.has(name))
25
+ shellBindings.add(name);
26
+ }
27
+ (0, walk_1.walk)(ctx.sourceFile, (node) => {
28
+ if (!(0, walk_1.is)(ctx, node, 'CallExpression'))
29
+ return;
30
+ const callee = node.expression;
31
+ if (!(0, walk_1.is)(ctx, callee, 'Identifier'))
32
+ return;
33
+ const name = (0, walk_1.nodeText)(callee);
34
+ if (!name || !shellBindings.has(name))
35
+ return;
36
+ // 命中 exec*/execSync* 调用——按参数形态定级
37
+ const args = node.arguments ?? [];
38
+ const first = args[0];
39
+ if (!first)
40
+ return;
41
+ if ((0, walk_1.is)(ctx, first, 'StringLiteral')) {
42
+ const cmd = (0, walk_1.nodeText)(first) ?? '';
43
+ if (SHELL_METACHARS.test(cmd)) {
44
+ ctx.report(node, `${name}() 的命令字面量含 shell 元字符——存在命令注入面`);
45
+ }
46
+ else {
47
+ ctx.report(node, `${name}() 走 shell 执行——优先用 spawn(arg, { shell: false }) 传参数组`);
48
+ }
49
+ }
50
+ else {
51
+ // 模板字符串 / 变量拼接 → 动态命令
52
+ ctx.report(node, `${name}() 的命令是动态构造(非字符串字面量)——命令注入高危入口`);
53
+ }
54
+ });
55
+ },
56
+ };
57
+ //# sourceMappingURL=no-child-process-shell.js.map
@@ -0,0 +1,3 @@
1
+ import type { AstRule } from '../types';
2
+ export declare const noDebuggerRule: AstRule;
3
+ //# sourceMappingURL=no-debugger.d.ts.map
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // no-debugger.ts · 禁止 debugger 语句
4
+ // v1.3.9(一):debugger 语句遗留在生产代码会冻结 Node 进程
5
+ // ============================================================
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.noDebuggerRule = void 0;
8
+ const walk_1 = require("../walk");
9
+ exports.noDebuggerRule = {
10
+ id: 'no-debugger',
11
+ name: '禁止 debugger 语句',
12
+ severity: 'FAIL',
13
+ description: 'debugger 语句遗留在生产代码会冻结 Node 进程',
14
+ checkCode(ctx) {
15
+ (0, walk_1.walk)(ctx.sourceFile, (node) => {
16
+ if ((0, walk_1.is)(ctx, node, 'DebuggerStatement')) {
17
+ ctx.report(node, '禁止遗留 debugger 语句——生产环境会冻结进程');
18
+ }
19
+ });
20
+ },
21
+ };
22
+ //# sourceMappingURL=no-debugger.js.map
@@ -0,0 +1,3 @@
1
+ import type { AstRule } from '../types';
2
+ export declare const noDynamicRequireRule: AstRule;
3
+ //# sourceMappingURL=no-dynamic-require.d.ts.map
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // no-dynamic-require.ts · 禁止动态 require
4
+ // v1.3.9(一):require(变量) 使依赖来源在静态分析下不可见——
5
+ // 供应链投毒(OWASP ASI04)的隐藏通道
6
+ // ============================================================
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.noDynamicRequireRule = void 0;
9
+ const walk_1 = require("../walk");
10
+ exports.noDynamicRequireRule = {
11
+ id: 'no-dynamic-require',
12
+ name: '禁止动态 require',
13
+ severity: 'FAIL',
14
+ description: 'require(非字面量) 的模块来源静态不可见——供应链投毒隐藏通道(ASI04 关联)',
15
+ checkCode(ctx) {
16
+ (0, walk_1.walk)(ctx.sourceFile, (node) => {
17
+ if (!(0, walk_1.is)(ctx, node, 'CallExpression'))
18
+ return;
19
+ const callee = node.expression;
20
+ if (!(0, walk_1.is)(ctx, callee, 'Identifier'))
21
+ return;
22
+ if ((0, walk_1.nodeText)(callee) !== 'require')
23
+ return;
24
+ const args = node.arguments ?? [];
25
+ const first = args[0];
26
+ if (!first)
27
+ return;
28
+ if (!(0, walk_1.is)(ctx, first, 'StringLiteral')) {
29
+ ctx.report(node, 'require() 参数不是字符串字面量——动态模块加载使依赖审计失效');
30
+ }
31
+ });
32
+ },
33
+ };
34
+ //# sourceMappingURL=no-dynamic-require.js.map
@@ -0,0 +1,3 @@
1
+ import type { AstRule } from '../types';
2
+ export declare const noEmptyCatchRule: AstRule;
3
+ //# sourceMappingURL=no-empty-catch.d.ts.map
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // no-empty-catch.ts · 禁止空 catch 块
4
+ // v1.3.9(一):空 catch 吞异常——错误静默扩散是审计盲区的经典成因
5
+ // (对齐 FORGE「空 catch 治理」工程纪律的语义级落地)
6
+ // ============================================================
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.noEmptyCatchRule = void 0;
9
+ const walk_1 = require("../walk");
10
+ exports.noEmptyCatchRule = {
11
+ id: 'no-empty-catch',
12
+ name: '禁止空 catch 块',
13
+ severity: 'WARN',
14
+ description: '空 catch 块吞掉异常——错误静默扩散;至少要留一行注释说明为什么可以忽略',
15
+ checkCode(ctx) {
16
+ (0, walk_1.walk)(ctx.sourceFile, (node) => {
17
+ if (!(0, walk_1.is)(ctx, node, 'CatchClause'))
18
+ return;
19
+ const block = node.block;
20
+ if (!(0, walk_1.is)(ctx, block, 'Block'))
21
+ return;
22
+ const stmts = block.statements ?? [];
23
+ if (stmts.length === 0) {
24
+ ctx.report(node, '空 catch 块——异常被静默吞掉;至少留一行注释说明忽略理由');
25
+ }
26
+ });
27
+ },
28
+ };
29
+ //# sourceMappingURL=no-empty-catch.js.map
@@ -0,0 +1,3 @@
1
+ import type { AstRule } from '../types';
2
+ export declare const noEvalRule: AstRule;
3
+ //# sourceMappingURL=no-eval.d.ts.map
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // no-eval.ts · 禁止 eval() / new Function()
4
+ // v1.3.9(一):官方示范 AST 规则——动态代码执行是注入攻击的标准入口
5
+ // ============================================================
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.noEvalRule = void 0;
8
+ const walk_1 = require("../walk");
9
+ exports.noEvalRule = {
10
+ id: 'no-eval',
11
+ name: '禁止动态代码执行',
12
+ severity: 'FAIL',
13
+ description: 'eval() / new Function() 会执行任意字符串代码,是 prompt 注入与供应链攻击的放大器',
14
+ checkCode(ctx) {
15
+ (0, walk_1.walk)(ctx.sourceFile, (node) => {
16
+ // eval(...):CallExpression,callee 是 Identifier "eval"
17
+ if ((0, walk_1.is)(ctx, node, 'CallExpression')) {
18
+ const callee = node.expression;
19
+ if (callee && callee.kind === ctx.kind('Identifier') && (0, walk_1.nodeText)(callee) === 'eval') {
20
+ ctx.report(node, '禁止使用 eval()——动态代码执行入口');
21
+ }
22
+ }
23
+ // new Function(...):NewExpression,callee 是 Identifier "Function"
24
+ if ((0, walk_1.is)(ctx, node, 'NewExpression')) {
25
+ const callee = node.expression;
26
+ if (callee && callee.kind === ctx.kind('Identifier') && (0, walk_1.nodeText)(callee) === 'Function') {
27
+ ctx.report(node, '禁止 new Function()——动态代码执行入口');
28
+ }
29
+ }
30
+ });
31
+ },
32
+ };
33
+ //# sourceMappingURL=no-eval.js.map
@@ -0,0 +1,3 @@
1
+ import type { AstRule } from '../types';
2
+ export declare const noHardcodedSecretRule: AstRule;
3
+ //# sourceMappingURL=no-hardcoded-secret.d.ts.map
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // no-hardcoded-secret.ts · 禁止硬编码密钥(AST 语义级)
4
+ // v1.3.9(一):与 pattern 规则(正则扫行)相比,AST 级检测只命中
5
+ // 「赋值给密钥类变量名的字符串字面量」,误报率显著更低
6
+ // ============================================================
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.noHardcodedSecretRule = void 0;
9
+ const walk_1 = require("../walk");
10
+ /** 密钥类变量名模式 */
11
+ const SECRET_NAME = /(secret|token|passwd|password|api[_-]?key|apikey|access[_-]?key|private[_-]?key|credential)/i;
12
+ /** 占位符白名单——这些值不是真实密钥 */
13
+ const PLACEHOLDER = /^(x+|<[^>]*>|\$\{[^}]*\}|your[_-].*|placeholder|changeme|redacted|test-?dummy)$/i;
14
+ /** 疑似真实密钥的最小长度(短字符串多为业务常量) */
15
+ const MIN_SECRET_LEN = 16;
16
+ function isSecretLiteral(name, value) {
17
+ if (!SECRET_NAME.test(name))
18
+ return false;
19
+ if (value.length < MIN_SECRET_LEN)
20
+ return false;
21
+ if (PLACEHOLDER.test(value))
22
+ return false;
23
+ return true;
24
+ }
25
+ exports.noHardcodedSecretRule = {
26
+ id: 'no-hardcoded-secret',
27
+ name: '禁止硬编码密钥(AST 语义级)',
28
+ severity: 'FAIL',
29
+ description: '赋值给 secret/token/apiKey 等密钥类变量的长字符串字面量——比正则扫行误报率低',
30
+ checkCode(ctx) {
31
+ (0, walk_1.walk)(ctx.sourceFile, (node) => {
32
+ // const secret = "sk-...":VariableDeclaration
33
+ if ((0, walk_1.is)(ctx, node, 'VariableDeclaration')) {
34
+ const decl = node;
35
+ const name = (0, walk_1.nodeText)(decl.name);
36
+ const init = decl.initializer;
37
+ if (name && (0, walk_1.is)(ctx, init, 'StringLiteral')) {
38
+ const value = (0, walk_1.nodeText)(init) ?? '';
39
+ if (isSecretLiteral(name, value)) {
40
+ ctx.report(node, `变量 ${name} 被赋值为疑似真实密钥的字符串字面量`);
41
+ }
42
+ }
43
+ }
44
+ // { apiKey: "sk-..." }:PropertyAssignment(对象字面量属性)
45
+ if ((0, walk_1.is)(ctx, node, 'PropertyAssignment')) {
46
+ const prop = node;
47
+ const name = (0, walk_1.nodeText)(prop.name);
48
+ const init = prop.initializer;
49
+ if (name && (0, walk_1.is)(ctx, init, 'StringLiteral')) {
50
+ const value = (0, walk_1.nodeText)(init) ?? '';
51
+ if (isSecretLiteral(name, value)) {
52
+ ctx.report(node, `属性 ${name} 被赋值为疑似真实密钥的字符串字面量`);
53
+ }
54
+ }
55
+ }
56
+ });
57
+ },
58
+ };
59
+ //# sourceMappingURL=no-hardcoded-secret.js.map
@@ -0,0 +1,3 @@
1
+ import type { AstRule } from '../types';
2
+ export declare const noInsecureUrlRule: AstRule;
3
+ //# sourceMappingURL=no-insecure-url.d.ts.map
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // no-insecure-url.ts · 禁止 http:// 明文端点
4
+ // v1.3.9(一):AST 级扫字符串字面量——只报代码里的端点 URL,
5
+ // 不碰注释与文档(pattern 规则会误伤)
6
+ // ============================================================
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.noInsecureUrlRule = void 0;
9
+ const walk_1 = require("../walk");
10
+ /** 本地/示例域名白名单——不是真实端点 */
11
+ const SAFE_HOST = /^(localhost|127\.0\.0\.1|0\.0\.0\.0|example\.(com|org)|test|.*\.local)(:|$)/;
12
+ exports.noInsecureUrlRule = {
13
+ id: 'no-insecure-url',
14
+ name: '禁止 http:// 明文端点',
15
+ severity: 'WARN',
16
+ description: '字符串字面量里的 http:// 端点(本地/示例域名除外)——明文传输可被中间人替换',
17
+ checkCode(ctx) {
18
+ (0, walk_1.walk)(ctx.sourceFile, (node) => {
19
+ if (!(0, walk_1.is)(ctx, node, 'StringLiteral'))
20
+ return;
21
+ const text = (0, walk_1.nodeText)(node) ?? '';
22
+ if (!/^http:\/\//.test(text))
23
+ return;
24
+ // 提取 host 部分做白名单
25
+ const host = text.replace(/^http:\/\//, '').split('/')[0] ?? '';
26
+ if (SAFE_HOST.test(host))
27
+ return;
28
+ ctx.report(node, `明文 http:// 端点(${host})——生产端点应走 https`);
29
+ });
30
+ },
31
+ };
32
+ //# sourceMappingURL=no-insecure-url.js.map
@@ -0,0 +1,3 @@
1
+ import type { AstRule } from '../types';
2
+ export declare const noSqlStringConcatRule: AstRule;
3
+ //# sourceMappingURL=no-sql-string-concat.d.ts.map
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // no-sql-string-concat.ts · 禁止 SQL 字符串拼接
4
+ // v1.3.9(一):query 类调用的参数若为「字面量 + 变量」拼接,
5
+ // 即 SQL 注入入口(AST 级只报高危形态,比正则扫行准)
6
+ // ============================================================
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.noSqlStringConcatRule = void 0;
9
+ const walk_1 = require("../walk");
10
+ /** 查询类函数名模式 */
11
+ const QUERY_FN = /(query|execute|exec|sql|raw)/i;
12
+ /** 变量名提示来自用户输入(弱信号,仅用于 message 措辞) */
13
+ const INPUT_HINT = /(req|input|param|user|body|query|ctx|data)/i;
14
+ exports.noSqlStringConcatRule = {
15
+ id: 'no-sql-string-concat',
16
+ name: '禁止 SQL 字符串拼接',
17
+ severity: 'WARN',
18
+ description: 'query 类调用的参数含「字符串 + 非字面量」拼接——SQL 注入入口',
19
+ checkCode(ctx) {
20
+ (0, walk_1.walk)(ctx.sourceFile, (node) => {
21
+ if (!(0, walk_1.is)(ctx, node, 'CallExpression'))
22
+ return;
23
+ const callee = node.expression;
24
+ if (!(0, walk_1.is)(ctx, callee, 'Identifier') && !(0, walk_1.is)(ctx, callee, 'PropertyAccessExpression'))
25
+ return;
26
+ const fnName = (0, walk_1.nodeText)(callee.name)
27
+ ?? (0, walk_1.nodeText)(callee)
28
+ ?? '';
29
+ if (!QUERY_FN.test(fnName))
30
+ return;
31
+ const args = node.arguments ?? [];
32
+ for (const arg of args) {
33
+ if (!(0, walk_1.is)(ctx, arg, 'BinaryExpression'))
34
+ continue;
35
+ const op = arg.operatorToken;
36
+ if (!(0, walk_1.is)(ctx, op, 'PlusToken'))
37
+ continue;
38
+ // 操作数任一是非字面量 → 拼接变量
39
+ const hasDynamic = [arg].some(() => true);
40
+ void hasDynamic;
41
+ const left = arg.left;
42
+ const right = arg.right;
43
+ const dynamicSide = [left, right].find((side) => side && !(0, walk_1.is)(ctx, side, 'StringLiteral') && !(0, walk_1.is)(ctx, side, 'NumericLiteral'));
44
+ if (dynamicSide) {
45
+ const hint = INPUT_HINT.test((0, walk_1.nodeText)(dynamicSide) ?? '') ? '(疑似用户输入)' : '';
46
+ ctx.report(arg, `${fnName}() 的 SQL 由字符串拼接构造${hint}——改用参数化查询`);
47
+ }
48
+ }
49
+ });
50
+ },
51
+ };
52
+ //# sourceMappingURL=no-sql-string-concat.js.map
@@ -0,0 +1,11 @@
1
+ /** 解析版本串为可比数组(非法返回 null) */
2
+ export declare function parseVersion(v: string): number[] | null;
3
+ /** 比较两个版本:a<b 返回 -1,相等 0,a>b 返回 1(不可解析按相等处理) */
4
+ export declare function compareVersions(a: string, b: string): number;
5
+ /**
6
+ * 判断版本是否命中区间串。
7
+ * 区间语法:空格分隔的多个条件(AND),每条形如 <x / <=x / >x / >=x / =x / x(等于)。
8
+ * 例:"=1.2.0" "<4.17.21" ">=1.3.0 <1.6.0"
9
+ */
10
+ export declare function inRange(version: string, range: string): boolean;
11
+ //# sourceMappingURL=semver.d.ts.map
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // semver.ts · 最小语义化版本比较(ASI04 漏洞区间匹配用)
4
+ // v1.3.9(一):只支持 MAJOR.MINOR.PATCH 数字三元组 + 简单前缀
5
+ // (如 1.2 / 1.x),prerelease/build 忽略——漏洞库区间足够用
6
+ // ============================================================
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.parseVersion = parseVersion;
9
+ exports.compareVersions = compareVersions;
10
+ exports.inRange = inRange;
11
+ /** 解析版本串为可比数组(非法返回 null) */
12
+ function parseVersion(v) {
13
+ const m = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(v.trim());
14
+ if (!m)
15
+ return null;
16
+ return [Number(m[1]), Number(m[2] ?? 0), Number(m[3] ?? 0)];
17
+ }
18
+ /** 比较两个版本:a<b 返回 -1,相等 0,a>b 返回 1(不可解析按相等处理) */
19
+ function compareVersions(a, b) {
20
+ const va = parseVersion(a);
21
+ const vb = parseVersion(b);
22
+ if (!va || !vb)
23
+ return 0;
24
+ for (let i = 0; i < 3; i++) {
25
+ const x = va[i] ?? 0;
26
+ const y = vb[i] ?? 0;
27
+ if (x !== y)
28
+ return x < y ? -1 : 1;
29
+ }
30
+ return 0;
31
+ }
32
+ /**
33
+ * 判断版本是否命中区间串。
34
+ * 区间语法:空格分隔的多个条件(AND),每条形如 <x / <=x / >x / >=x / =x / x(等于)。
35
+ * 例:"=1.2.0" "<4.17.21" ">=1.3.0 <1.6.0"
36
+ */
37
+ function inRange(version, range) {
38
+ const conditions = range.trim().split(/\s+/).filter(Boolean);
39
+ if (conditions.length === 0)
40
+ return false;
41
+ return conditions.every((cond) => {
42
+ const m = /^(<=|>=|<|>|=)?\s*(.+)$/.exec(cond);
43
+ if (!m || m[2] === undefined)
44
+ return false;
45
+ const op = m[1];
46
+ const ver = m[2];
47
+ const cmp = compareVersions(version, ver);
48
+ switch (op) {
49
+ case '<': return cmp < 0;
50
+ case '<=': return cmp <= 0;
51
+ case '>': return cmp > 0;
52
+ case '>=': return cmp >= 0;
53
+ case '=':
54
+ default: return cmp === 0;
55
+ }
56
+ });
57
+ }
58
+ //# sourceMappingURL=semver.js.map