progmune-runtime 3.7.6 → 3.7.8
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.
- package/CHANGELOG.md +39 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/dist/annotation-suggest.js +200 -0
- package/dist/annotation-suggest.test.js +117 -0
- package/dist/frameworks/django-detector.js +133 -0
- package/dist/frameworks/django-detector.test.js +136 -0
- package/dist/frameworks/fastapi-detector.js +94 -0
- package/dist/frameworks/fastapi-detector.test.js +144 -0
- package/dist/sdk.js +1 -1
- package/dist/trust/engine.js +133 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.8] — 2026-08-28
|
|
4
|
+
|
|
5
|
+
### Django / DRF 框架结构适配(M2)——框架适配第 4 个
|
|
6
|
+
|
|
7
|
+
- **结构提取(Python AST)**:`tools/extract_framework_django.py`(与 extract_ir.py / extract_framework_py.py 解耦)——urlpatterns 解析(url()/path()/re_path() → FBV / CBV .as_view() / include)、FBV 登录装饰器(login_required/permission_required/staff_member_required/user_passes_test + 自定义 *auth* 装饰器)、CBV 基类与方法(View/generics + LoginRequiredMixin 等混入)、DRF permission_classes(AllowAny/IsAuthenticated/其他类名)与 @api_view(methods + permission_classes kwarg)
|
|
8
|
+
- **检测规则**(`src/frameworks/django-detector.ts`):
|
|
9
|
+
- `DJANGO_VIEW_NO_AUTH`——mutation 视图无保护:FBV 按动词名门控(add/create/update/delete/transfer 等——信息页 home/robots/error 不报);CBV 按方法含写操作(post/put/patch/delete/create/update/destroy)+ 无认证装饰器/无 LoginRequiredMixin
|
|
10
|
+
- `DRF_PERMISSION_BYPASS`——DRF 视图写方法 + 显式 AllowAny / 空权限类(非认证入口端点)
|
|
11
|
+
- 认证入口词汇豁免(login/signin/regist/token/health 等,视图名+URL 名+pattern)
|
|
12
|
+
- **引擎接线**:`collectDjangoViolations`(仅 Python,best-effort)+ `overall.djangoCoverage`——加性零漂移
|
|
13
|
+
- **合成金标**(`generate-projects-django.ts` + `scan-protocol-django.ts`):FBV/CBV/DRF × clean/V1/V2/V2b/V1V2 = 8 项目——**TP 6 / FP 0 / FN 0 → Precision 100% / Recall 100%**
|
|
14
|
+
- **真实应用验证**:django-realworld(15 路由结构全识别:DRF generics + permission_classes 接线、RegistrationAPIView/LoginAPIView AllowAny 正确豁免)——**0 FP**;PyGoat 133 路由结构全识别,适配器 2 条 flags 均为故意脆弱 lab(csrf_transfer_monei_api / DoItFast)——如实记录;fastapi-realworld 0 FP 保持
|
|
15
|
+
- **修复(子串陷阱)**:`registration` 不含 `register` 子串、`register` 不含 `registr` 子串——认证入口词干统一为 `regist`(FastAPI/Django 两检测器同步)+ 回归测试锁定
|
|
16
|
+
- **验证**:19 个新单元测试(django-detector 10 + fastapi 豁免回归);套件 139/139;Python 盲测 v1.2 64 零漂移;C 演示 SSG 结果不变
|
|
17
|
+
- **边界(如实)**:FBV 无法静态区分 HTTP 方法(动词名门控口径,非方法级);include() 递归不展开(被 include 的应用 urls.py 单独成文件时已覆盖);自定义 permission 类视为保护(保守)
|
|
18
|
+
|
|
19
|
+
### FastAPI 框架结构适配(M1)——框架适配第 3 个
|
|
20
|
+
|
|
21
|
+
- **结构提取(Python AST)**:`tools/extract_framework_py.py`(与 extract_ir.py 解耦,零风险)——路由(@app.get/@router.post/@r.api_route)、依赖注入(Depends()/Security(),含 Annotated[...] 订阅、嵌套调用解析、**装饰器级 dependencies=[...]**(realworld 风格)、认证方案声明(OAuth2PasswordBearer/HTTPBearer/APIKeyHeader 等 8 类)、全局中间件;跳过 tests/deps/venv 等非生产目录
|
|
22
|
+
- **检测规则**(`src/frameworks/fastapi-detector.ts`,结构提取与规则判定解耦):
|
|
23
|
+
- `FASTAPI_ROUTE_NO_AUTH`——写操作路由(post/put/patch/delete)无认证依赖且非认证入口端点(login/register/token/health 词汇豁免)→「每个 API 入口都有门禁」的精确形态;公开读(GET)不检查(realworld 的 tags/文章列表就是公开 GET——只盯 mutation 面把误报压到最低)
|
|
24
|
+
- `FASTAPI_DEAD_AUTH_SCHEME`——声明了认证方案但没有任何路由引用(认证设施是死的,装饰性声明)
|
|
25
|
+
- **引擎接线**:`collectFastapiViolations`(仅 Python 项目跑 python3 扫描;best-effort 永不阻断评估)+ `overall.fastapiCoverage`(apps/routes/filesScanned/issuesFound)——加性零漂移
|
|
26
|
+
- **合成金标**(`generate-projects-fastapi.ts` + `scan-protocol-fastapi.ts`,镜像 generate-projects-python 方法论):3 结构风格(直连/APIRouter/认证方案)× 4 违规变体(clean/V1 无认证写路由/V2 死方案/V1V2)= 12 项目——**TP 13 / FP 0 / FN 0 → Precision 100% / Recall 100%**
|
|
27
|
+
- **真实应用验证**:fastapi-realworld(19 路由全结构识别:签名 Depends 与装饰器 dependencies= 两种认证接线风格)——**0 FP**;django-realworld/django-unicorn 无 FastAPI 结构 → 适配器静默直通(结果不变)
|
|
28
|
+
- **零漂移**:Python 盲测 v1.2 64 违规零漂移;C 演示 SSG 结果不变;相关套件 128/128(含 fastapi-detector 9 个新回归)
|
|
29
|
+
- **边界(如实)**:全局中间件不视为认证(add_middleware 通常是 CORS;自定义认证中间件结构不可见);Depends 目标按 auth-like 词表+方案引用判定(无数据流分析);npm 安装态下 tools/ 不在包内 → 框架扫描与既有 Python IR 提取一样静默降级(安装态 Python 全链路为既有遗留问题,不随本版引入新差异)
|
|
30
|
+
|
|
31
|
+
## [3.7.7] — 2026-08-28
|
|
32
|
+
|
|
33
|
+
### C 注解采纳体验(采纳生死线工具)
|
|
34
|
+
|
|
35
|
+
- **注解建议引擎**(`src/annotation-suggest.ts`):确定性启发式(无 LLM)——按函数名词汇(3.7.6 金标 5/5 真实注解反推的词表)× 已注解状态,生成原语注解候选(verify/establish/guard/open/close 角色 + 命名空间/状态转移/注释块模板/置信度/命中理由/掩蔽风险标记);已注解、规则名、外部函数自动排除;两遍计算 maskRisk(体内调用规则原语或本批同被建议函数)
|
|
36
|
+
- **CLI 扫描模式**(`scripts/c-annotate.js --scan <dir> [--write] [--all] [--include-resource]`):dry-run 默认;--write 自动插入函数定义上方(写入后自动刷新 ir.json 防陈旧);保守门控(全部实测依据):establish/掩蔽风险跳过(不提供强制开关)、open/close 跳过(--include-resource 强制)
|
|
37
|
+
- **引擎加性字段**:`evaluateTrust.annotationSuggestions`(仅 C 生成;TS/Python 无该字段——零漂移)
|
|
38
|
+
- **验收(REALWORLD_C_V7.md)**:无注解 uftpd 副本金标恢复率 **4/4 角色正确**(check_user_pass=verify / handle_PASS=establish / do_RETR / do_STOR=guard);自动应用 7 条后与手写金标**等价**(真实代码 0 SSG FP、植入违规 2/2 精确定位、NEEDS_REVIEW 72 同分)
|
|
39
|
+
- **三个实测安全发现(自动写入的门控依据)**:①掩蔽风险——establish 注解把手握违规的流函数变「原语」(函数内顺序不检查)后植入违规被掩蔽(实测 2 处);②资源生命周期注解自动应用触发跨函数窗口 FP 类(实测 28 条,open/close 分处不同函数窗口);③new_session 词汇误判守卫(实测 4 FP,模式收紧为 channel 类)
|
|
40
|
+
- 回归:15 个新单元测试;引擎相关套件 98/98;Python 盲测 v1.2 零漂移(64);C 应用级金标 F1=95.7% 不变;演示重扫 SSG 结果不变
|
|
41
|
+
|
|
3
42
|
## [3.7.6] — 2026-08-28
|
|
4
43
|
|
|
5
44
|
### C 生产级路径收官:金标 5/5 + 采纳案例 + 正则层噪声治理
|
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ Progmune is honest about what it can and cannot verify.
|
|
|
108
108
|
|
|
109
109
|
**Multi-language IR (registry-based).** TypeScript (ts-morph) and Python (AST) extractors are registered entries in one registry (`src/extract-project-ir.ts`); `extractProjectIR` merges every detected language into a single function IR shared by the agent loop, `execute()`, and the MCP server — the agent composes function-protocol chains across both languages. Adding a language (Go, Java, …) = register one extractor entry; callers don't change.
|
|
110
110
|
|
|
111
|
-
**Framework adapters:
|
|
111
|
+
**Framework adapters: 4/13.** Express ✅, tRPC ✅, FastAPI ✅ (route/auth-dependency structural analysis — every mutation route checked for auth dependencies, dead auth schemes flagged) and Django ✅ (urlconf/CBV/DRF structural analysis — unprotected mutation views and `AllowAny` write endpoints flagged) have dedicated detectors; Next.js has version-aware governance; NestJS is partial. Next.js/Fastify/Flask and 5 more remain — framework adaptation is the #1 product gap.
|
|
112
112
|
|
|
113
113
|
### What Progmune does NOT cover (honest boundaries)
|
|
114
114
|
|
|
@@ -234,7 +234,7 @@ High-impact contribution areas:
|
|
|
234
234
|
- **Runtime Pipeline:** Detect → Explain → Repair → Validate (L1–L4)
|
|
235
235
|
- **Trust Engine:** 4-dimension scoring with binary explainability gate
|
|
236
236
|
- **MCP Tools:** 19 — `progmune_trust_check`, `progmune_score`, `progmune_policy_check`, `progmune_certify`, and more
|
|
237
|
-
- **Framework Adapters:** Express ✅, tRPC ✅, NestJS partial (
|
|
237
|
+
- **Framework Adapters:** Express ✅, tRPC ✅, FastAPI ✅, Django ✅, NestJS partial (4/13)
|
|
238
238
|
- **Knowledge Base:** 31 domains, 148 protocol rules, 22 detectors, 26 safeguards, PLSB 13/13 categories — plus 15 source-level detection rules (Python)
|
|
239
239
|
- **Corpus:** 2,500+ trajectories across 6+ repositories; blind benchmarks 100 (TS) + 90 (Python) projects; real-world validation on 4 application repos
|
|
240
240
|
- **Current focus:** Enterprise PoC validation + remaining framework-internal boundary FPs
|
package/README.zh-CN.md
CHANGED
|
@@ -108,7 +108,7 @@ Progmune 对能验证什么、不能验证什么保持诚实。
|
|
|
108
108
|
|
|
109
109
|
**多语言 IR(注册表式)。** TypeScript(ts-morph)与 Python(AST)提取器是同一注册表(`src/extract-project-ir.ts`)中的注册项;`extractProjectIR` 把检测到的所有语言合并为一份函数 IR,由 agent loop、`execute()` 与 MCP server 共享——agent 可在两种语言上编排函数协议链。新增语言(Go、Java……)= 注册一条提取器,调用方零改动。
|
|
110
110
|
|
|
111
|
-
**框架适配器:
|
|
111
|
+
**框架适配器:4/13。** Express ✅、tRPC ✅、FastAPI ✅(路由/认证依赖结构分析——逐条检查写操作路由的认证依赖、标记死认证方案)与 Django ✅(URL 配置/CBV/DRF 结构分析——标记无保护的写视图与 `AllowAny` 写端点)有专用检测器;Next.js 有版本感知治理;NestJS 部分支持。Next.js/Fastify/Flask 及另外 5 个待适配——框架适配是 #1 产品缺口。
|
|
112
112
|
|
|
113
113
|
### Progmune 不覆盖什么(诚实边界)
|
|
114
114
|
|
|
@@ -233,7 +233,7 @@ Progmune 建立在"**LLM 输出是统计表演而非推理**"这一前提上—
|
|
|
233
233
|
- **运行时管线:** 检测 → 解释 → 修复 → 验证(L1–L4)
|
|
234
234
|
- **信任引擎:** 四维评分 + 二元可解释性门
|
|
235
235
|
- **MCP 工具:** 19 个——`progmune_trust_check`、`progmune_score`、`progmune_policy_check`、`progmune_certify` 等
|
|
236
|
-
- **框架适配器:** Express ✅、tRPC ✅、NestJS 部分(
|
|
236
|
+
- **框架适配器:** Express ✅、tRPC ✅、FastAPI ✅、Django ✅、NestJS 部分(4/13)
|
|
237
237
|
- **知识库:** 31 个域、148 条协议规则、22 个检测器、26 条防护规则、PLSB 13/13 类别——另加 15 条源码级检测规则(Python)
|
|
238
238
|
- **语料:** 6+ 仓库 2,500+ 轨迹;盲测基准 100(TS)+ 90(Python)个项目;4 个应用仓库真实验证
|
|
239
239
|
- **当前重点:** 企业 PoC 验证 + 剩余框架内部边界误报
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* annotation-suggest.ts — C 注解建议引擎(确定性启发式,无 LLM)
|
|
4
|
+
*
|
|
5
|
+
* 注解驱动定位的「采纳生死线」:未注解 C 项目的 SSG 层静默(0 flags——
|
|
6
|
+
* 看不见问题,也看不见该在哪下注)。本模块按函数名词汇 + 已注解状态,
|
|
7
|
+
* 为项目生成「原语注解建议清单」——每条建议含角色(verify/establish/
|
|
8
|
+
* guard/open/close)、命名空间、状态转移与可直接粘贴的注释块模板。
|
|
9
|
+
*
|
|
10
|
+
* 设计原则:
|
|
11
|
+
* - 纯名称启发式,诚实标注置信度(high=多证据/强动词,medium=单证据);
|
|
12
|
+
* 不假装语义理解——建议是「填空起点」,人工确认后生效(与
|
|
13
|
+
* c-alias-propose.js 的人工确认门同一哲学)。
|
|
14
|
+
* - 词汇来自 3.7.6 金标 5/5 的真实注解反推(check_user_pass=verify、
|
|
15
|
+
* handle_PASS=establish、do_RETR/do_STOR=守卫、auth_password=verify、
|
|
16
|
+
* new_session_channel=会话守卫、open/close_data_connection=资源生命周期)。
|
|
17
|
+
* - 已注解函数、规则名函数(按名即命中,无需注解)、外部函数不重复建议。
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.suggestAnnotations = suggestAnnotations;
|
|
21
|
+
const ROLE_DEFS = [
|
|
22
|
+
{
|
|
23
|
+
// 凭证比对(密码/口令/凭证 × 比对动词)——verify_password 同款语义
|
|
24
|
+
role: "verify",
|
|
25
|
+
namespace: "auth",
|
|
26
|
+
pre: ["UNAUTHENTICATED"],
|
|
27
|
+
post: ["PASSWORD_VERIFIED"],
|
|
28
|
+
patterns: [
|
|
29
|
+
["password", "check"],
|
|
30
|
+
["password", "compare"],
|
|
31
|
+
["password", "verify"],
|
|
32
|
+
["password", "auth"],
|
|
33
|
+
["passwd", "check"],
|
|
34
|
+
["pass", "check"],
|
|
35
|
+
["pass", "compare"],
|
|
36
|
+
["credential", "check"],
|
|
37
|
+
["credential", "compare"],
|
|
38
|
+
["secret", "check"],
|
|
39
|
+
["secret", "compare"],
|
|
40
|
+
["kbdint", "get"],
|
|
41
|
+
["otp", "check"],
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
// 登录完成(认证循环/命令处理器的完成点)——establish 原语
|
|
46
|
+
role: "establish",
|
|
47
|
+
namespace: "auth",
|
|
48
|
+
pre: [],
|
|
49
|
+
post: ["AUTHENTICATED"],
|
|
50
|
+
patterns: [
|
|
51
|
+
["authenticate"],
|
|
52
|
+
["login"],
|
|
53
|
+
["logon"],
|
|
54
|
+
["sign_in"],
|
|
55
|
+
["auth_success"],
|
|
56
|
+
["auth_ok"],
|
|
57
|
+
["handle", "pass"],
|
|
58
|
+
["do", "login"],
|
|
59
|
+
["do", "user"],
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
// 权限/会话守卫(权限词 × 检查动词,或 FTP/传输类命令处理器、
|
|
64
|
+
// 通道开启回调)——check_resource_ownership 同款语义(auth 命名空间,
|
|
65
|
+
// 见 REALWORLD_C_V6.md 发现 G5:跨命名空间 pre 不可满足)
|
|
66
|
+
role: "guard",
|
|
67
|
+
namespace: "auth",
|
|
68
|
+
pre: ["AUTHENTICATED"],
|
|
69
|
+
post: ["AUTHORIZED"],
|
|
70
|
+
patterns: [
|
|
71
|
+
["perm", "check"],
|
|
72
|
+
["authoriz", "check"],
|
|
73
|
+
["acl", "check"],
|
|
74
|
+
["grant", "check"],
|
|
75
|
+
["privilege", "check"],
|
|
76
|
+
["do", "retr"],
|
|
77
|
+
["do", "stor"],
|
|
78
|
+
["handle", "retr"],
|
|
79
|
+
["handle", "stor"],
|
|
80
|
+
["do", "download"],
|
|
81
|
+
["do", "upload"],
|
|
82
|
+
["do", "transfer"],
|
|
83
|
+
["start", "transfer"],
|
|
84
|
+
["send", "transfer"],
|
|
85
|
+
["open", "channel"],
|
|
86
|
+
["accept", "channel"],
|
|
87
|
+
["new", "channel"],
|
|
88
|
+
["new", "session", "channel"],
|
|
89
|
+
// 注意:不带 ["new","session"]——new_session 是会话工厂(创建而非检查),
|
|
90
|
+
// 实测(V7):守卫注解会误报其调用方(ftp_cb/tftp_cb/main 在登录前建会话)。
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
// 资源获取(打开文件/连接/日志)——open_file 同款语义
|
|
95
|
+
role: "open",
|
|
96
|
+
namespace: "file",
|
|
97
|
+
pre: [],
|
|
98
|
+
post: ["FILE_OPEN"],
|
|
99
|
+
patterns: [
|
|
100
|
+
["open", "file"],
|
|
101
|
+
["open", "data"],
|
|
102
|
+
["open", "connection"],
|
|
103
|
+
["open", "conn"],
|
|
104
|
+
["open", "log"],
|
|
105
|
+
["open", "db"],
|
|
106
|
+
["create", "connection"],
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
// 资源释放(关闭/销毁文件/连接)——close_file 同款语义
|
|
111
|
+
role: "close",
|
|
112
|
+
namespace: "file",
|
|
113
|
+
pre: ["FILE_OPEN"],
|
|
114
|
+
post: [],
|
|
115
|
+
invalidate: ["FILE_OPEN"],
|
|
116
|
+
patterns: [
|
|
117
|
+
["close", "file"],
|
|
118
|
+
["close", "data"],
|
|
119
|
+
["close", "connection"],
|
|
120
|
+
["close", "conn"],
|
|
121
|
+
["close", "log"],
|
|
122
|
+
["close", "db"],
|
|
123
|
+
["release", "connection"],
|
|
124
|
+
["destroy", "connection"],
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
/** 匹配命中数 ≥2 视为高置信(多个独立证据) */
|
|
129
|
+
const HIGH_CONFIDENCE_HITS = 2;
|
|
130
|
+
/**
|
|
131
|
+
* 为一组函数生成注解建议(确定性,同输入同输出)。
|
|
132
|
+
*
|
|
133
|
+
* @param functions — 项目函数列表(C IR)
|
|
134
|
+
* @param ruleNames — 已加载规则名集合(规则名函数按名即命中,无需注解建议)
|
|
135
|
+
* @param limit — 输出上限(默认 20,防大仓库刷屏;按置信度排序)
|
|
136
|
+
*/
|
|
137
|
+
function suggestAnnotations(functions, ruleNames, limit = 20) {
|
|
138
|
+
const suggestions = [];
|
|
139
|
+
const seen = new Set();
|
|
140
|
+
for (const fn of functions) {
|
|
141
|
+
if (!fn.name)
|
|
142
|
+
continue;
|
|
143
|
+
if (fn.protocol && (fn.protocol.pre_states?.length || fn.protocol.post_states?.length)) {
|
|
144
|
+
continue; // 已注解
|
|
145
|
+
}
|
|
146
|
+
if (ruleNames?.has(fn.name))
|
|
147
|
+
continue; // 规则名函数按名命中,无需注解
|
|
148
|
+
if (fn.external)
|
|
149
|
+
continue;
|
|
150
|
+
if (seen.has(fn.name))
|
|
151
|
+
continue;
|
|
152
|
+
seen.add(fn.name);
|
|
153
|
+
const lower = fn.name.toLowerCase();
|
|
154
|
+
for (const def of ROLE_DEFS) {
|
|
155
|
+
const hits = [];
|
|
156
|
+
for (const pattern of def.patterns) {
|
|
157
|
+
if (pattern.every((word) => lower.includes(word))) {
|
|
158
|
+
hits.push(pattern.join("+"));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (hits.length === 0)
|
|
162
|
+
continue;
|
|
163
|
+
const confidence = hits.length >= HIGH_CONFIDENCE_HITS ? "high" : "medium";
|
|
164
|
+
const invalidatePart = def.invalidate
|
|
165
|
+
? `, invalidate=${fmtStates(def.invalidate)}`
|
|
166
|
+
: "";
|
|
167
|
+
suggestions.push({
|
|
168
|
+
function: fn.name,
|
|
169
|
+
file: fn.file || "",
|
|
170
|
+
role: def.role,
|
|
171
|
+
namespace: def.namespace,
|
|
172
|
+
pre: def.pre,
|
|
173
|
+
post: def.post,
|
|
174
|
+
invalidate: def.invalidate,
|
|
175
|
+
confidence,
|
|
176
|
+
reasons: hits,
|
|
177
|
+
template: `/* @progmune(namespace="${def.namespace}", pre=${fmtStates(def.pre)}, post=${fmtStates(def.post)}${invalidatePart}) */`,
|
|
178
|
+
maskRisk: false, // 两遍计算(见下)
|
|
179
|
+
});
|
|
180
|
+
break; // 一个函数只取最先命中的角色
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// 第二遍:掩蔽风险——函数体调用了「已存在规则原语」或「本批同被建议的
|
|
184
|
+
// 函数」→ 注解后体内序列不再被验证,可能掩蔽其握有的违规(V7 实测:
|
|
185
|
+
// 植入流被 establish 注解变不透明后违规消失)。
|
|
186
|
+
const coSuggested = new Set(suggestions.map((s) => s.function));
|
|
187
|
+
for (const s of suggestions) {
|
|
188
|
+
const fnInfo = functions.find((f) => f.name === s.function);
|
|
189
|
+
s.maskRisk = (fnInfo?.calls || []).some((c) => (ruleNames?.has(c) ?? false) || coSuggested.has(c));
|
|
190
|
+
}
|
|
191
|
+
// 稳定性排序:置信度高优先,其次按 文件+名字(结果可复现)
|
|
192
|
+
const rank = { high: 0, medium: 1 };
|
|
193
|
+
suggestions.sort((a, b) => rank[a.confidence] - rank[b.confidence] ||
|
|
194
|
+
a.file.localeCompare(b.file) ||
|
|
195
|
+
a.function.localeCompare(b.function));
|
|
196
|
+
return suggestions.slice(0, limit);
|
|
197
|
+
}
|
|
198
|
+
function fmtStates(states) {
|
|
199
|
+
return `[${states.map((s) => `"${s}"`).join(", ")}]`;
|
|
200
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* annotation-suggest.test.ts — 注解建议引擎回归(纯函数,无文件系统 I/O)
|
|
5
|
+
*
|
|
6
|
+
* 词汇模式来自 3.7.6 金标 5/5 真实注解反推(REALWORLD_C_V6.md):
|
|
7
|
+
* check_user_pass/auth_password=verify、handle_PASS=establish、
|
|
8
|
+
* do_RETR/do_STOR/new_session_channel=guard、open/close_data_connection=资源生命周期。
|
|
9
|
+
*/
|
|
10
|
+
const vitest_1 = require("vitest");
|
|
11
|
+
const annotation_suggest_1 = require("./annotation-suggest");
|
|
12
|
+
const fn = (name, extra = {}) => ({
|
|
13
|
+
name,
|
|
14
|
+
file: "src/demo.c",
|
|
15
|
+
...extra,
|
|
16
|
+
});
|
|
17
|
+
(0, vitest_1.describe)("annotation-suggest", () => {
|
|
18
|
+
(0, vitest_1.it)("verify:凭证名词 × 比对动词(check_user_pass / auth_password)", () => {
|
|
19
|
+
const s = (0, annotation_suggest_1.suggestAnnotations)([fn("check_user_pass"), fn("auth_password")]);
|
|
20
|
+
(0, vitest_1.expect)(s).toHaveLength(2);
|
|
21
|
+
for (const x of s) {
|
|
22
|
+
(0, vitest_1.expect)(x.role).toBe("verify");
|
|
23
|
+
(0, vitest_1.expect)(x.namespace).toBe("auth");
|
|
24
|
+
(0, vitest_1.expect)(x.pre).toEqual(["UNAUTHENTICATED"]);
|
|
25
|
+
(0, vitest_1.expect)(x.post).toEqual(["PASSWORD_VERIFIED"]);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
(0, vitest_1.it)("establish:登录完成原语(handle_PASS / authenticate)", () => {
|
|
29
|
+
const s = (0, annotation_suggest_1.suggestAnnotations)([fn("handle_PASS"), fn("authenticate")]);
|
|
30
|
+
(0, vitest_1.expect)(s.map((x) => [x.function, x.role])).toEqual(vitest_1.expect.arrayContaining([
|
|
31
|
+
["handle_PASS", "establish"],
|
|
32
|
+
["authenticate", "establish"],
|
|
33
|
+
]));
|
|
34
|
+
(0, vitest_1.expect)(s[0].pre).toEqual([]);
|
|
35
|
+
(0, vitest_1.expect)(s[0].post).toEqual(["AUTHENTICATED"]);
|
|
36
|
+
});
|
|
37
|
+
(0, vitest_1.it)("guard:FTP 命令处理器与通道开启回调(do_RETR/do_STOR/new_session_channel)", () => {
|
|
38
|
+
const s = (0, annotation_suggest_1.suggestAnnotations)([
|
|
39
|
+
fn("do_RETR"),
|
|
40
|
+
fn("do_STOR"),
|
|
41
|
+
fn("new_session_channel"),
|
|
42
|
+
]);
|
|
43
|
+
for (const x of s) {
|
|
44
|
+
(0, vitest_1.expect)(x.role).toBe("guard");
|
|
45
|
+
(0, vitest_1.expect)(x.pre).toEqual(["AUTHENTICATED"]);
|
|
46
|
+
(0, vitest_1.expect)(x.post).toEqual(["AUTHORIZED"]);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
(0, vitest_1.it)("资源生命周期:open/close_data_connection", () => {
|
|
50
|
+
const s = (0, annotation_suggest_1.suggestAnnotations)([
|
|
51
|
+
fn("open_data_connection"),
|
|
52
|
+
fn("close_data_connection"),
|
|
53
|
+
]);
|
|
54
|
+
const open = s.find((x) => x.function === "open_data_connection");
|
|
55
|
+
const close = s.find((x) => x.function === "close_data_connection");
|
|
56
|
+
(0, vitest_1.expect)(open?.role).toBe("open");
|
|
57
|
+
(0, vitest_1.expect)(open?.post).toEqual(["FILE_OPEN"]);
|
|
58
|
+
(0, vitest_1.expect)(close?.role).toBe("close");
|
|
59
|
+
(0, vitest_1.expect)(close?.pre).toEqual(["FILE_OPEN"]);
|
|
60
|
+
(0, vitest_1.expect)(close?.invalidate).toEqual(["FILE_OPEN"]);
|
|
61
|
+
});
|
|
62
|
+
(0, vitest_1.it)("已注解函数不再建议", () => {
|
|
63
|
+
const annotated = fn("check_user_pass", {
|
|
64
|
+
protocol: { pre_states: ["UNAUTHENTICATED"], post_states: ["PASSWORD_VERIFIED"], namespace: "auth" },
|
|
65
|
+
});
|
|
66
|
+
(0, vitest_1.expect)((0, annotation_suggest_1.suggestAnnotations)([annotated])).toHaveLength(0);
|
|
67
|
+
});
|
|
68
|
+
(0, vitest_1.it)("规则名函数(按名即命中)不再建议", () => {
|
|
69
|
+
const s = (0, annotation_suggest_1.suggestAnnotations)([fn("verify_password")], new Set(["verify_password"]));
|
|
70
|
+
(0, vitest_1.expect)(s).toHaveLength(0);
|
|
71
|
+
});
|
|
72
|
+
(0, vitest_1.it)("外部函数不再建议", () => {
|
|
73
|
+
(0, vitest_1.expect)((0, annotation_suggest_1.suggestAnnotations)([fn("check_password", { external: true })])).toHaveLength(0);
|
|
74
|
+
});
|
|
75
|
+
(0, vitest_1.it)("模板可直接粘贴:完整注释块文本", () => {
|
|
76
|
+
const [s] = (0, annotation_suggest_1.suggestAnnotations)([fn("check_user_pass")]);
|
|
77
|
+
(0, vitest_1.expect)(s.template).toBe('/* @progmune(namespace="auth", pre=["UNAUTHENTICATED"], post=["PASSWORD_VERIFIED"]) */');
|
|
78
|
+
});
|
|
79
|
+
(0, vitest_1.it)("确定性 + 上限:同输入同输出,超过 limit 截断(置信度优先)", () => {
|
|
80
|
+
const fns = Array.from({ length: 30 }, (_, i) => fn(`do_RETR_${i}`) // 全部 guard,单证据 medium
|
|
81
|
+
);
|
|
82
|
+
const a = (0, annotation_suggest_1.suggestAnnotations)(fns, undefined, 10);
|
|
83
|
+
const b = (0, annotation_suggest_1.suggestAnnotations)(fns, undefined, 10);
|
|
84
|
+
(0, vitest_1.expect)(a).toEqual(b);
|
|
85
|
+
(0, vitest_1.expect)(a).toHaveLength(10);
|
|
86
|
+
(0, vitest_1.expect)(a[0].function).toBe("do_RETR_0");
|
|
87
|
+
});
|
|
88
|
+
(0, vitest_1.it)("无命中词汇的函数不产生建议", () => {
|
|
89
|
+
const s = (0, annotation_suggest_1.suggestAnnotations)([fn("parse_int"), fn("strlcat"), fn("main")]);
|
|
90
|
+
(0, vitest_1.expect)(s).toHaveLength(0);
|
|
91
|
+
});
|
|
92
|
+
(0, vitest_1.it)("掩蔽风险:函数体调用已有规则原语 → maskRisk=true", () => {
|
|
93
|
+
const [s] = (0, annotation_suggest_1.suggestAnnotations)([fn("login_flow", { calls: ["verify_password"] })], new Set(["verify_password"]));
|
|
94
|
+
(0, vitest_1.expect)(s.maskRisk).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
(0, vitest_1.it)("掩蔽风险:函数体调用本批同被建议的函数 → maskRisk=true(co-suggestion)", () => {
|
|
97
|
+
const s = (0, annotation_suggest_1.suggestAnnotations)([
|
|
98
|
+
fn("login_flow", { calls: ["start_file_transfer"] }),
|
|
99
|
+
fn("start_file_transfer"),
|
|
100
|
+
]);
|
|
101
|
+
const flow = s.find((x) => x.function === "login_flow");
|
|
102
|
+
(0, vitest_1.expect)(flow?.maskRisk).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
(0, vitest_1.it)("叶子函数(体内无规则原语调用)→ maskRisk=false", () => {
|
|
105
|
+
const [s] = (0, annotation_suggest_1.suggestAnnotations)([fn("check_user_pass", { calls: ["strcmp", "strlcpy"] })]);
|
|
106
|
+
(0, vitest_1.expect)(s.maskRisk).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
(0, vitest_1.it)("会话工厂 new_session 不再误判为守卫", () => {
|
|
109
|
+
const s = (0, annotation_suggest_1.suggestAnnotations)([fn("new_session")]);
|
|
110
|
+
(0, vitest_1.expect)(s).toHaveLength(0);
|
|
111
|
+
});
|
|
112
|
+
(0, vitest_1.it)("通道守卫 new_session_channel 仍命中 guard", () => {
|
|
113
|
+
const s = (0, annotation_suggest_1.suggestAnnotations)([fn("new_session_channel")]);
|
|
114
|
+
(0, vitest_1.expect)(s).toHaveLength(1);
|
|
115
|
+
(0, vitest_1.expect)(s[0].role).toBe("guard");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Django Framework Adapter — Protocol Detection for Django / DRF
|
|
4
|
+
*
|
|
5
|
+
* 框架适配 M2:结构扫描由 tools/extract_framework_django.py(Python AST)
|
|
6
|
+
* 完成——urlpatterns 解析(FBV/CBV/DRF)、登录装饰器、DRF permission_classes。
|
|
7
|
+
* 本模块消费结构 JSON 做规则判定:
|
|
8
|
+
*
|
|
9
|
+
* DJANGO_VIEW_NO_AUTH mutation 视图(FBV 动词名门控 / CBV 方法
|
|
10
|
+
* 含写操作)无登录装饰器、无权限类、无
|
|
11
|
+
* LoginRequiredMixin 等保护——路由级
|
|
12
|
+
* missing-auth 的精确形态
|
|
13
|
+
* DRF_PERMISSION_BYPASS DRF 视图 mutation 方法 + 显式 AllowAny /
|
|
14
|
+
* 空权限类——显式绕过(非认证入口端点)
|
|
15
|
+
*
|
|
16
|
+
* 口径(如实):
|
|
17
|
+
* - FBV 无法静态区分 HTTP 方法——按视图名动词门控(add/create/update/
|
|
18
|
+
* delete/transfer 等),信息页(home/robots/error)不报;@api_view
|
|
19
|
+
* 声明的 methods 按真实方法判定
|
|
20
|
+
* - DRF 写方法 = post/put/patch/delete/create/update/destroy(generics
|
|
21
|
+
* 命名);read-only(list/retrieve/feed)不检查——公开读是常见设计
|
|
22
|
+
* - 认证入口端点按 视图名+URL 名 词汇豁免(login/register/signup/token/
|
|
23
|
+
* health/auth)
|
|
24
|
+
* - include() 递归与 admin.site.urls 等无法解析的引用跳过
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.analyzeDjangoStructure = analyzeDjangoStructure;
|
|
28
|
+
// ── Rule vocabulary ──
|
|
29
|
+
/** DRF 写方法(generics 命名 + HTTP 方法) */
|
|
30
|
+
const DRF_MUTATION_METHODS = new Set([
|
|
31
|
+
"post", "put", "patch", "delete", "create", "update", "destroy",
|
|
32
|
+
]);
|
|
33
|
+
/** FBV 动词名门控(信息页不报——home/robots/error 等是公开设计) */
|
|
34
|
+
const FBV_MUTATION_VERBS = [
|
|
35
|
+
"add", "create", "update", "delete", "remove", "transfer", "upload",
|
|
36
|
+
"submit", "edit", "change", "save", "write", "send", "post", "pay",
|
|
37
|
+
"checkout", "purchase", "reset", "revoke", "register_device",
|
|
38
|
+
];
|
|
39
|
+
const AUTH_ENTRY_WORDS = [
|
|
40
|
+
"login", "signin", "sign_in", "regist", "signup", "sign_up",
|
|
41
|
+
"token", "auth", "health", "status",
|
|
42
|
+
];
|
|
43
|
+
function isAuthEntry(route, viewName) {
|
|
44
|
+
const haystack = `${viewName || ""} ${route.urlname} ${route.pattern}`.toLowerCase();
|
|
45
|
+
return AUTH_ENTRY_WORDS.some((w) => haystack.includes(w));
|
|
46
|
+
}
|
|
47
|
+
function hasMutationMethod(view) {
|
|
48
|
+
if (view.apiViewMethods && view.apiViewMethods.length > 0) {
|
|
49
|
+
return view.apiViewMethods.some((m) => DRF_MUTATION_METHODS.has(m.toLowerCase()));
|
|
50
|
+
}
|
|
51
|
+
return (view.methods || []).some((m) => DRF_MUTATION_METHODS.has(m.toLowerCase()));
|
|
52
|
+
}
|
|
53
|
+
function fbvNameHasMutationVerb(name) {
|
|
54
|
+
const lower = name.toLowerCase();
|
|
55
|
+
return FBV_MUTATION_VERBS.some((v) => lower.includes(v));
|
|
56
|
+
}
|
|
57
|
+
// ── Analysis ──
|
|
58
|
+
function analyzeDjangoStructure(data) {
|
|
59
|
+
if (!data.hasDjango || !Array.isArray(data.routes)) {
|
|
60
|
+
return { hasDjango: false, issues: [] };
|
|
61
|
+
}
|
|
62
|
+
const issues = [];
|
|
63
|
+
for (const route of data.routes) {
|
|
64
|
+
if (route.kind !== "fbv" && route.kind !== "cbv")
|
|
65
|
+
continue;
|
|
66
|
+
const viewName = route.view;
|
|
67
|
+
if (!viewName)
|
|
68
|
+
continue;
|
|
69
|
+
const view = data.views[viewName];
|
|
70
|
+
if (!view)
|
|
71
|
+
continue; // 无法解析的引用(admin.site.urls 等)跳过
|
|
72
|
+
if (isAuthEntry(route, viewName))
|
|
73
|
+
continue;
|
|
74
|
+
if (view.kind === "fbv") {
|
|
75
|
+
const hasAuthDecorator = (view.authDecorators || []).length > 0;
|
|
76
|
+
if (hasAuthDecorator)
|
|
77
|
+
continue;
|
|
78
|
+
const protectedByApiView = (view.apiViewMethods || []).length > 0
|
|
79
|
+
&& (view.permissionClasses || []).length > 0
|
|
80
|
+
&& !(view.permissionClasses || []).every((p) => p === "AllowAny");
|
|
81
|
+
const isMutation = view.apiViewMethods && view.apiViewMethods.length > 0
|
|
82
|
+
? hasMutationMethod(view)
|
|
83
|
+
: fbvNameHasMutationVerb(viewName);
|
|
84
|
+
if (!isMutation)
|
|
85
|
+
continue;
|
|
86
|
+
if (protectedByApiView)
|
|
87
|
+
continue;
|
|
88
|
+
issues.push({
|
|
89
|
+
severity: "medium",
|
|
90
|
+
rule: "DJANGO_VIEW_NO_AUTH",
|
|
91
|
+
message: `View "${viewName}" (${route.pattern}) has no login decorator or ` +
|
|
92
|
+
`permission protection — any visitor can reach it.`,
|
|
93
|
+
route: route.pattern,
|
|
94
|
+
handler: viewName,
|
|
95
|
+
file: view.file,
|
|
96
|
+
});
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// CBV
|
|
100
|
+
if (view.isDrf) {
|
|
101
|
+
if (!hasMutationMethod(view))
|
|
102
|
+
continue;
|
|
103
|
+
if (view.openPermission) {
|
|
104
|
+
issues.push({
|
|
105
|
+
severity: "medium",
|
|
106
|
+
rule: "DRF_PERMISSION_BYPASS",
|
|
107
|
+
message: `DRF view "${viewName}" (${route.pattern}) accepts write methods ` +
|
|
108
|
+
`with permission_classes allowing any caller.`,
|
|
109
|
+
route: route.pattern,
|
|
110
|
+
handler: viewName,
|
|
111
|
+
file: view.file,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// 非 DRF CBV:写方法 + 无 method_decorator 认证 + 无 LoginRequiredMixin
|
|
117
|
+
if (!hasMutationMethod(view))
|
|
118
|
+
continue;
|
|
119
|
+
const authDecorators = (view.authDecorators || []).length > 0;
|
|
120
|
+
if (authDecorators || view.protectedByMixin)
|
|
121
|
+
continue;
|
|
122
|
+
issues.push({
|
|
123
|
+
severity: "medium",
|
|
124
|
+
rule: "DJANGO_VIEW_NO_AUTH",
|
|
125
|
+
message: `View "${viewName}" (${route.pattern}) has write methods without ` +
|
|
126
|
+
`login decorators or auth mixins.`,
|
|
127
|
+
route: route.pattern,
|
|
128
|
+
handler: viewName,
|
|
129
|
+
file: view.file,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return { hasDjango: true, issues };
|
|
133
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* django-detector.test.ts — Django 框架适配器规则回归(纯函数,无文件 I/O)
|
|
5
|
+
*
|
|
6
|
+
* 规则判定与结构提取解耦:扫描器(tools/extract_framework_django.py)只做
|
|
7
|
+
* 结构,本模块消费结构 JSON 做 DJANGO_VIEW_NO_AUTH / DRF_PERMISSION_BYPASS。
|
|
8
|
+
*/
|
|
9
|
+
const vitest_1 = require("vitest");
|
|
10
|
+
const django_detector_1 = require("./django-detector");
|
|
11
|
+
function structure(partial) {
|
|
12
|
+
return {
|
|
13
|
+
hasDjango: true,
|
|
14
|
+
routes: [],
|
|
15
|
+
views: {},
|
|
16
|
+
filesScanned: 1,
|
|
17
|
+
...partial,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const fbv = (name, extra = {}) => ({
|
|
21
|
+
file: "views.py", kind: "fbv", decorators: [], authDecorators: [],
|
|
22
|
+
apiViewMethods: null, permissionClasses: [],
|
|
23
|
+
...extra,
|
|
24
|
+
});
|
|
25
|
+
const cbv = (name, extra = {}) => ({
|
|
26
|
+
file: "views.py", kind: "cbv", decorators: [], authDecorators: [],
|
|
27
|
+
apiViewMethods: null, permissionClasses: [], bases: [], methods: [],
|
|
28
|
+
isDrf: false, protectedByMixin: false, openPermission: false,
|
|
29
|
+
...extra,
|
|
30
|
+
});
|
|
31
|
+
(0, vitest_1.describe)("django-detector", () => {
|
|
32
|
+
(0, vitest_1.it)("R1:无保护的 FBV 动词名视图 → DJANGO_VIEW_NO_AUTH", () => {
|
|
33
|
+
const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
|
|
34
|
+
routes: [{ pattern: "transfer", urlname: "", view: "transfer_money", kind: "fbv", file: "urls.py" }],
|
|
35
|
+
views: { transfer_money: fbv("transfer_money") },
|
|
36
|
+
}));
|
|
37
|
+
(0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("DJANGO_VIEW_NO_AUTH");
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.it)("R1 识别 @login_required 保护", () => {
|
|
40
|
+
const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
|
|
41
|
+
routes: [{ pattern: "transfer", urlname: "", view: "transfer_money", kind: "fbv", file: "urls.py" }],
|
|
42
|
+
views: { transfer_money: fbv("transfer_money", { authDecorators: ["login_required"] }) },
|
|
43
|
+
}));
|
|
44
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
45
|
+
});
|
|
46
|
+
(0, vitest_1.it)("R1 豁免:home/robots/error 等信息页动词门控不报", () => {
|
|
47
|
+
const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
|
|
48
|
+
routes: [
|
|
49
|
+
{ pattern: "", urlname: "", view: "home", kind: "fbv", file: "urls.py" },
|
|
50
|
+
{ pattern: "robots.txt", urlname: "", view: "robots", kind: "fbv", file: "urls.py" },
|
|
51
|
+
],
|
|
52
|
+
views: { home: fbv("home"), robots: fbv("robots") },
|
|
53
|
+
}));
|
|
54
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
55
|
+
});
|
|
56
|
+
(0, vitest_1.it)("R1 豁免:registration 词干(不含 register 子串的陷阱回归)", () => {
|
|
57
|
+
const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
|
|
58
|
+
routes: [{ pattern: "^users/?$", urlname: "", view: "RegistrationAPIView", kind: "cbv", file: "urls.py" }],
|
|
59
|
+
views: {
|
|
60
|
+
RegistrationAPIView: cbv("RegistrationAPIView", {
|
|
61
|
+
isDrf: true, methods: ["post"],
|
|
62
|
+
permissionClasses: ["AllowAny"], openPermission: true,
|
|
63
|
+
}),
|
|
64
|
+
},
|
|
65
|
+
}));
|
|
66
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
67
|
+
});
|
|
68
|
+
(0, vitest_1.it)("R2:DRF 写方法 + AllowAny → DRF_PERMISSION_BYPASS", () => {
|
|
69
|
+
const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
|
|
70
|
+
routes: [{ pattern: "send", urlname: "", view: "SendMoneyAPIView", kind: "cbv", file: "urls.py" }],
|
|
71
|
+
views: {
|
|
72
|
+
SendMoneyAPIView: cbv("SendMoneyAPIView", {
|
|
73
|
+
isDrf: true, methods: ["post"],
|
|
74
|
+
permissionClasses: ["AllowAny"], openPermission: true,
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
}));
|
|
78
|
+
(0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("DRF_PERMISSION_BYPASS");
|
|
79
|
+
});
|
|
80
|
+
(0, vitest_1.it)("R2:IsAuthenticated 保护不报;只读视图(list/retrieve)不报", () => {
|
|
81
|
+
const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
|
|
82
|
+
routes: [
|
|
83
|
+
{ pattern: "send", urlname: "", view: "SendMoneyAPIView", kind: "cbv", file: "urls.py" },
|
|
84
|
+
{ pattern: "tags", urlname: "", view: "TagListAPIView", kind: "cbv", file: "urls.py" },
|
|
85
|
+
],
|
|
86
|
+
views: {
|
|
87
|
+
SendMoneyAPIView: cbv("SendMoneyAPIView", {
|
|
88
|
+
isDrf: true, methods: ["post"],
|
|
89
|
+
permissionClasses: ["IsAuthenticated"], openPermission: false,
|
|
90
|
+
}),
|
|
91
|
+
TagListAPIView: cbv("TagListAPIView", {
|
|
92
|
+
isDrf: true, methods: ["list"],
|
|
93
|
+
permissionClasses: ["AllowAny"], openPermission: true,
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
}));
|
|
97
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
98
|
+
});
|
|
99
|
+
(0, vitest_1.it)("非 DRF CBV:写方法 + LoginRequiredMixin → 不报", () => {
|
|
100
|
+
const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
|
|
101
|
+
routes: [{ pattern: "edit", urlname: "", view: "EditView", kind: "cbv", file: "urls.py" }],
|
|
102
|
+
views: {
|
|
103
|
+
EditView: cbv("EditView", {
|
|
104
|
+
isDrf: false, methods: ["post"], protectedByMixin: true,
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
}));
|
|
108
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
109
|
+
});
|
|
110
|
+
(0, vitest_1.it)("@api_view 写方法 + permission_classes 保护 → 不报", () => {
|
|
111
|
+
const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
|
|
112
|
+
routes: [{ pattern: "api", urlname: "", view: "api_transfer", kind: "fbv", file: "urls.py" }],
|
|
113
|
+
views: {
|
|
114
|
+
api_transfer: fbv("api_transfer", {
|
|
115
|
+
apiViewMethods: ["post"], permissionClasses: ["IsAuthenticated"],
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
118
|
+
}));
|
|
119
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
120
|
+
});
|
|
121
|
+
(0, vitest_1.it)("include 与无法解析的引用(admin.site.urls)不报", () => {
|
|
122
|
+
const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
|
|
123
|
+
routes: [
|
|
124
|
+
{ pattern: "^admin/", urlname: "", view: "urls", kind: "fbv", file: "urls.py" },
|
|
125
|
+
{ pattern: "^api/", urlname: "", view: null, kind: "include", file: "urls.py" },
|
|
126
|
+
],
|
|
127
|
+
views: {},
|
|
128
|
+
}));
|
|
129
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
130
|
+
});
|
|
131
|
+
(0, vitest_1.it)("非 Django 结构不产生任何问题", () => {
|
|
132
|
+
const { hasDjango, issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({ hasDjango: false, routes: [] }));
|
|
133
|
+
(0, vitest_1.expect)(hasDjango).toBe(false);
|
|
134
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* FastAPI Framework Adapter — Protocol Detection for FastAPI
|
|
4
|
+
*
|
|
5
|
+
* 框架适配 M1(Python 侧结构提取 + TS 侧规则判定):
|
|
6
|
+
* 结构扫描由 tools/extract_framework_py.py(Python AST)完成——路由
|
|
7
|
+
* (@app.get/@router.post)、依赖注入(Depends()/Security(),含
|
|
8
|
+
* Annotated[...] 与装饰器级 dependencies=[...])、认证方案声明
|
|
9
|
+
* (OAuth2PasswordBearer/HTTPBearer/APIKeyHeader 等)、全局中间件。
|
|
10
|
+
* 本模块消费结构 JSON 做规则判定:
|
|
11
|
+
*
|
|
12
|
+
* FASTAPI_ROUTE_NO_AUTH 写操作路由(post/put/patch/delete)无
|
|
13
|
+
* 认证依赖,且不是认证入口端点(login/
|
|
14
|
+
* register/token 等)——「每个 API 入口
|
|
15
|
+
* 都有门禁」的精确形态
|
|
16
|
+
* FASTAPI_DEAD_AUTH_SCHEME 声明了认证方案但没有任何路由引用——
|
|
17
|
+
* 认证设施是死的(装饰性声明,防御性价值为零)
|
|
18
|
+
*
|
|
19
|
+
* 边界(如实):
|
|
20
|
+
* - 读操作(GET)不检查——公开读是常见设计(realworld 的 tags/文章
|
|
21
|
+
* 列表就是公开 GET),只盯写操作把误报压到最低(实测 realworld 0 FP)
|
|
22
|
+
* - 认证入口端点按 处理器名+路径 词汇豁免——登录/注册本身不能要求已认证
|
|
23
|
+
* - 不做数据流分析:Depends 目标是 auth-like 名或声明的方案即视为认证
|
|
24
|
+
* - 全局中间件不视为认证(add_middleware 通常是 CORS 等;认证中间件
|
|
25
|
+
* 是自定义 BaseHTTPMiddleware 且结构不可见——如实不认)
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.analyzeFastapiStructure = analyzeFastapiStructure;
|
|
29
|
+
// ── Rule vocabulary ──
|
|
30
|
+
/** 写操作方法——公开读是常见设计,只盯 mutation 面 */
|
|
31
|
+
const MUTATION_METHODS = new Set(["post", "put", "patch", "delete"]);
|
|
32
|
+
/** 认证入口端点豁免词(登录/注册/token 签发本身不能要求已认证) */
|
|
33
|
+
const AUTH_ENTRY_WORDS = [
|
|
34
|
+
"login", "signin", "sign_in", "regist", "signup", "sign_up",
|
|
35
|
+
"token", "auth", "health", "status",
|
|
36
|
+
];
|
|
37
|
+
/** 认证方案类型——引用即视为认证依赖 */
|
|
38
|
+
const SCHEME_TYPES = new Set([
|
|
39
|
+
"OAuth2PasswordBearer", "OAuth2AuthorizationCodeBearer", "HTTPBearer",
|
|
40
|
+
"HTTPBasic", "HTTPDigest", "APIKeyHeader", "APIKeyQuery", "APIKeyCookie",
|
|
41
|
+
"OpenIdConnect",
|
|
42
|
+
]);
|
|
43
|
+
function isAuthEntryRoute(route) {
|
|
44
|
+
const haystack = `${route.handler} ${route.path}`.toLowerCase();
|
|
45
|
+
return AUTH_ENTRY_WORDS.some((w) => haystack.includes(w));
|
|
46
|
+
}
|
|
47
|
+
/** 路由是否引用了某个认证方案(Depends(oauth2_scheme) 形态) */
|
|
48
|
+
function referencesScheme(route, schemeNames) {
|
|
49
|
+
return route.dependencies.some((d) => d.name !== null && schemeNames.has(d.name));
|
|
50
|
+
}
|
|
51
|
+
// ── Analysis ──
|
|
52
|
+
function analyzeFastapiStructure(data) {
|
|
53
|
+
if (!data.hasFastAPI || !Array.isArray(data.routes)) {
|
|
54
|
+
return { hasFastAPI: false, issues: [] };
|
|
55
|
+
}
|
|
56
|
+
const issues = [];
|
|
57
|
+
const schemeNames = new Set((data.authSchemes || []).map((s) => s.name));
|
|
58
|
+
// R1:无认证写操作路由
|
|
59
|
+
for (const route of data.routes) {
|
|
60
|
+
if (!MUTATION_METHODS.has(route.method.toLowerCase()))
|
|
61
|
+
continue;
|
|
62
|
+
const hasAuthDep = route.dependencies.some((d) => d.authLike);
|
|
63
|
+
if (hasAuthDep)
|
|
64
|
+
continue;
|
|
65
|
+
if (isAuthEntryRoute(route))
|
|
66
|
+
continue;
|
|
67
|
+
issues.push({
|
|
68
|
+
severity: "medium",
|
|
69
|
+
rule: "FASTAPI_ROUTE_NO_AUTH",
|
|
70
|
+
message: `Route ${route.method.toUpperCase()} ${route.path || "(root)"} has no ` +
|
|
71
|
+
`authentication dependency — any caller can invoke "${route.handler}" ` +
|
|
72
|
+
`without credentials.`,
|
|
73
|
+
route: `${route.method.toUpperCase()} ${route.path || "(root)"}`,
|
|
74
|
+
handler: route.handler,
|
|
75
|
+
file: route.file,
|
|
76
|
+
line: route.line,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// R2:声明了认证方案但没有路由引用(死设施)
|
|
80
|
+
for (const scheme of data.authSchemes || []) {
|
|
81
|
+
if (!SCHEME_TYPES.has(scheme.type))
|
|
82
|
+
continue; // 非认证方案类跳过
|
|
83
|
+
const used = data.routes.some((r) => referencesScheme(r, schemeNames));
|
|
84
|
+
if (!used) {
|
|
85
|
+
issues.push({
|
|
86
|
+
severity: "medium",
|
|
87
|
+
rule: "FASTAPI_DEAD_AUTH_SCHEME",
|
|
88
|
+
message: `Auth scheme "${scheme.name}" (${scheme.type}) is declared but no ` +
|
|
89
|
+
`route references it — the scheme does not protect any endpoint.`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { hasFastAPI: true, issues };
|
|
94
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* fastapi-detector.test.ts — FastAPI 框架适配器规则回归(纯函数,无文件 I/O)
|
|
5
|
+
*
|
|
6
|
+
* 规则判定与结构提取解耦:扫描器(tools/extract_framework_py.py)只做结构,
|
|
7
|
+
* 本模块消费结构 JSON 做 R1(无认证写操作路由)/ R2(死认证方案)。
|
|
8
|
+
*/
|
|
9
|
+
const vitest_1 = require("vitest");
|
|
10
|
+
const fastapi_detector_1 = require("./fastapi-detector");
|
|
11
|
+
function structure(partial) {
|
|
12
|
+
return {
|
|
13
|
+
hasFastAPI: true,
|
|
14
|
+
apps: ["app"],
|
|
15
|
+
routers: [],
|
|
16
|
+
authSchemes: [],
|
|
17
|
+
globalAuthMiddleware: [],
|
|
18
|
+
routes: [],
|
|
19
|
+
filesScanned: 1,
|
|
20
|
+
...partial,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const route = (p) => ({
|
|
24
|
+
method: p.method,
|
|
25
|
+
path: p.path || "",
|
|
26
|
+
handler: p.handler,
|
|
27
|
+
file: "main.py",
|
|
28
|
+
line: 1,
|
|
29
|
+
dependencies: p.deps || [],
|
|
30
|
+
});
|
|
31
|
+
(0, vitest_1.describe)("fastapi-detector", () => {
|
|
32
|
+
(0, vitest_1.it)("R1:无认证写操作路由 → FASTAPI_ROUTE_NO_AUTH", () => {
|
|
33
|
+
const { issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure({
|
|
34
|
+
routes: [route({ method: "post", handler: "create_article" })],
|
|
35
|
+
}));
|
|
36
|
+
(0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("FASTAPI_ROUTE_NO_AUTH");
|
|
37
|
+
});
|
|
38
|
+
(0, vitest_1.it)("R1 豁免:登录/注册/token 认证入口端点不报", () => {
|
|
39
|
+
const { issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure({
|
|
40
|
+
routes: [
|
|
41
|
+
route({ method: "post", path: "/login", handler: "login" }),
|
|
42
|
+
route({ method: "post", path: "/register", handler: "register_user" }),
|
|
43
|
+
route({ method: "post", path: "/token", handler: "issue_token" }),
|
|
44
|
+
],
|
|
45
|
+
}));
|
|
46
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
47
|
+
});
|
|
48
|
+
(0, vitest_1.it)("R1 豁免:registration 词干(不含 register 子串的陷阱回归)", () => {
|
|
49
|
+
const { issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure({
|
|
50
|
+
routes: [
|
|
51
|
+
route({ method: "post", path: "/users", handler: "registration_api_view" }),
|
|
52
|
+
],
|
|
53
|
+
}));
|
|
54
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
55
|
+
});
|
|
56
|
+
(0, vitest_1.it)("R1 豁免:GET 读操作不报(公开读是常见设计)", () => {
|
|
57
|
+
const { issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure({
|
|
58
|
+
routes: [route({ method: "get", path: "/articles", handler: "list_articles" })],
|
|
59
|
+
}));
|
|
60
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
61
|
+
});
|
|
62
|
+
(0, vitest_1.it)("R1 识别 Security/Depends 认证依赖(authLike 名)", () => {
|
|
63
|
+
const { issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure({
|
|
64
|
+
routes: [
|
|
65
|
+
route({
|
|
66
|
+
method: "put", path: "/users/me", handler: "update_me",
|
|
67
|
+
deps: [{ name: "get_current_user_authorizer", via: "Security", authLike: true }],
|
|
68
|
+
}),
|
|
69
|
+
],
|
|
70
|
+
}));
|
|
71
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
72
|
+
});
|
|
73
|
+
(0, vitest_1.it)("R1 识别声明的认证方案引用(Depends(oauth2_scheme))", () => {
|
|
74
|
+
const { issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure({
|
|
75
|
+
authSchemes: [{ name: "oauth2_scheme", type: "OAuth2PasswordBearer" }],
|
|
76
|
+
routes: [
|
|
77
|
+
route({
|
|
78
|
+
method: "get", path: "/me", handler: "read_me",
|
|
79
|
+
deps: [{ name: "oauth2_scheme", via: "Depends", authLike: true }],
|
|
80
|
+
}),
|
|
81
|
+
],
|
|
82
|
+
}));
|
|
83
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
84
|
+
});
|
|
85
|
+
(0, vitest_1.it)("R2:声明认证方案但无路由引用 → FASTAPI_DEAD_AUTH_SCHEME", () => {
|
|
86
|
+
const { issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure({
|
|
87
|
+
authSchemes: [{ name: "oauth2_scheme", type: "OAuth2PasswordBearer" }],
|
|
88
|
+
routes: [route({ method: "get", handler: "list_things" })],
|
|
89
|
+
}));
|
|
90
|
+
(0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("FASTAPI_DEAD_AUTH_SCHEME");
|
|
91
|
+
});
|
|
92
|
+
(0, vitest_1.it)("R2:方案被路由引用时不报", () => {
|
|
93
|
+
const { issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure({
|
|
94
|
+
authSchemes: [{ name: "oauth2_scheme", type: "OAuth2PasswordBearer" }],
|
|
95
|
+
routes: [
|
|
96
|
+
route({
|
|
97
|
+
method: "get", handler: "read_me",
|
|
98
|
+
deps: [{ name: "oauth2_scheme", via: "Depends", authLike: true }],
|
|
99
|
+
}),
|
|
100
|
+
],
|
|
101
|
+
}));
|
|
102
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
103
|
+
});
|
|
104
|
+
(0, vitest_1.it)("非 FastAPI 结构不产生任何问题", () => {
|
|
105
|
+
const { hasFastAPI, issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure({ hasFastAPI: false, routes: [] }));
|
|
106
|
+
(0, vitest_1.expect)(hasFastAPI).toBe(false);
|
|
107
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
108
|
+
});
|
|
109
|
+
(0, vitest_1.it)("realworld 快照:19 路由 0 问题(0 FP 回归锁)", () => {
|
|
110
|
+
const snapshot = {
|
|
111
|
+
hasFastAPI: true,
|
|
112
|
+
apps: ["application"],
|
|
113
|
+
routers: ["router"],
|
|
114
|
+
authSchemes: [],
|
|
115
|
+
globalAuthMiddleware: ["CORSMiddleware"],
|
|
116
|
+
routes: [
|
|
117
|
+
route({ method: "get", path: "", handler: "get_all_tags" }),
|
|
118
|
+
route({
|
|
119
|
+
method: "get", path: "", handler: "retrieve_current_user",
|
|
120
|
+
deps: [{ name: "get_current_user_authorizer", via: "Depends", authLike: true }],
|
|
121
|
+
}),
|
|
122
|
+
route({
|
|
123
|
+
method: "put", path: "", handler: "update_current_user",
|
|
124
|
+
deps: [{ name: "get_current_user_authorizer", via: "Depends", authLike: true }],
|
|
125
|
+
}),
|
|
126
|
+
route({ method: "post", path: "/login", handler: "login" }),
|
|
127
|
+
route({ method: "post", path: "", handler: "register" }),
|
|
128
|
+
route({
|
|
129
|
+
method: "post", path: "/{slug}/comments", handler: "create_comment",
|
|
130
|
+
deps: [{ name: "get_current_user_authorizer", via: "Security", authLike: true }],
|
|
131
|
+
}),
|
|
132
|
+
route({ method: "get", path: "/{slug}", handler: "retrieve_article_by_slug" }),
|
|
133
|
+
route({
|
|
134
|
+
method: "put", path: "/{slug}", handler: "update_article_by_slug",
|
|
135
|
+
deps: [{ name: "check_article_modification_permissions", via: "Depends", authLike: true }],
|
|
136
|
+
}),
|
|
137
|
+
],
|
|
138
|
+
filesScanned: 70,
|
|
139
|
+
};
|
|
140
|
+
const { hasFastAPI, issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure(snapshot));
|
|
141
|
+
(0, vitest_1.expect)(hasFastAPI).toBe(true);
|
|
142
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
143
|
+
});
|
|
144
|
+
});
|
package/dist/sdk.js
CHANGED
|
@@ -20,7 +20,7 @@ const risk_model_1 = require("./risk-model");
|
|
|
20
20
|
const protocol_knowledge_1 = require("./protocol-knowledge");
|
|
21
21
|
const evidence_repository_1 = require("./evidence-repository");
|
|
22
22
|
/** Runtime version — stable public identifier. Internal layers evolve underneath. */
|
|
23
|
-
exports.RUNTIME_VERSION = "3.7.
|
|
23
|
+
exports.RUNTIME_VERSION = "3.7.8";
|
|
24
24
|
function verify(filePath) {
|
|
25
25
|
const cert = (0, certify_1.certify)(filePath);
|
|
26
26
|
const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
|
package/dist/trust/engine.js
CHANGED
|
@@ -60,7 +60,10 @@ const confidence_calculator_1 = require("./confidence-calculator");
|
|
|
60
60
|
const violation_trace_1 = require("./violation-trace");
|
|
61
61
|
const api_semantic_mapper_1 = require("./api-semantic-mapper");
|
|
62
62
|
const protocol_domain_validator_1 = require("./protocol-domain-validator");
|
|
63
|
+
const annotation_suggest_1 = require("../annotation-suggest");
|
|
63
64
|
const call_graph_propagator_1 = require("./call-graph-propagator");
|
|
65
|
+
const fastapi_detector_1 = require("../frameworks/fastapi-detector");
|
|
66
|
+
const django_detector_1 = require("../frameworks/django-detector");
|
|
64
67
|
const ssg_bridge_1 = require("./ssg-bridge");
|
|
65
68
|
const call_sequence_1 = require("../call-sequence");
|
|
66
69
|
const extract_ir_1 = require("../extract-ir");
|
|
@@ -87,10 +90,12 @@ async function evaluateTrust(ctx) {
|
|
|
87
90
|
// ── Phase 5: Build call graph for cross-function propagation ──
|
|
88
91
|
const callGraph = (0, call_graph_propagator_1.buildCallGraphFromIR)(path.join(ctx.projectPath, "ir.json"));
|
|
89
92
|
const enterpriseViolations = collectEnterpriseViolations(ctx);
|
|
90
|
-
const { violations: protocolViolations, coverage: mappingCoverageData, ssgCoverage: ssgCov } = await collectProtocolViolations(ctx, callGraph);
|
|
93
|
+
const { violations: protocolViolations, coverage: mappingCoverageData, ssgCoverage: ssgCov, annotationSuggestions: cAnnotationSuggestions } = await collectProtocolViolations(ctx, callGraph);
|
|
91
94
|
const expressResult = collectExpressViolations(ctx);
|
|
92
95
|
const nestjsResult = collectNestJSViolations(ctx);
|
|
93
96
|
const trpcResult = collectTRPCViolations(ctx);
|
|
97
|
+
const fastapiResult = collectFastapiViolations(ctx);
|
|
98
|
+
const djangoResult = collectDjangoViolations(ctx);
|
|
94
99
|
const coverageData = collectVerificationCoverage(ctx);
|
|
95
100
|
const governanceDefects = collectGovernanceDefects(ctx);
|
|
96
101
|
// ═══════════════════════════════════════
|
|
@@ -108,6 +113,8 @@ async function evaluateTrust(ctx) {
|
|
|
108
113
|
...expressViolations,
|
|
109
114
|
...nestjsResult.violations,
|
|
110
115
|
...trpcResult.violations,
|
|
116
|
+
...fastapiResult.violations,
|
|
117
|
+
...djangoResult.violations,
|
|
111
118
|
];
|
|
112
119
|
// ═══════════════════════════════════════
|
|
113
120
|
// PHASE 3: SCORE
|
|
@@ -174,6 +181,7 @@ async function evaluateTrust(ctx) {
|
|
|
174
181
|
commit: ctx.commit,
|
|
175
182
|
timestamp,
|
|
176
183
|
engineVersion,
|
|
184
|
+
annotationSuggestions: cAnnotationSuggestions,
|
|
177
185
|
overall: {
|
|
178
186
|
score: effectiveScore,
|
|
179
187
|
decision,
|
|
@@ -226,6 +234,24 @@ async function evaluateTrust(ctx) {
|
|
|
226
234
|
issuesFound: nestjsResult.violations.length,
|
|
227
235
|
}
|
|
228
236
|
: undefined,
|
|
237
|
+
/** FastAPI framework adapter coverage — route/auth-dependency analysis */
|
|
238
|
+
fastapiCoverage: fastapiResult.coverage.routes > 0
|
|
239
|
+
? {
|
|
240
|
+
appsDetected: fastapiResult.coverage.apps,
|
|
241
|
+
totalRoutes: fastapiResult.coverage.routes,
|
|
242
|
+
filesScanned: fastapiResult.coverage.filesScanned,
|
|
243
|
+
issuesFound: fastapiResult.violations.length,
|
|
244
|
+
}
|
|
245
|
+
: undefined,
|
|
246
|
+
/** Django framework adapter coverage — urlconf/view/permission analysis */
|
|
247
|
+
djangoCoverage: djangoResult.coverage.routes > 0
|
|
248
|
+
? {
|
|
249
|
+
appsDetected: djangoResult.coverage.apps,
|
|
250
|
+
totalRoutes: djangoResult.coverage.routes,
|
|
251
|
+
filesScanned: djangoResult.coverage.filesScanned,
|
|
252
|
+
issuesFound: djangoResult.violations.length,
|
|
253
|
+
}
|
|
254
|
+
: undefined,
|
|
229
255
|
},
|
|
230
256
|
dimensions: {
|
|
231
257
|
policyCompliance: {
|
|
@@ -364,6 +390,102 @@ function mapPolicyViolation(rv, filePath, _enterprisePolicy) {
|
|
|
364
390
|
* Collect Express-specific security violations from the framework detector.
|
|
365
391
|
* Maps ExpressSecurityIssue[] → TrustViolation[].
|
|
366
392
|
*/
|
|
393
|
+
function collectDjangoViolations(ctx) {
|
|
394
|
+
const violations = [];
|
|
395
|
+
const coverage = { apps: 0, routes: 0, filesScanned: 0 };
|
|
396
|
+
// 仅 Python 项目跑框架结构扫描
|
|
397
|
+
const lang = ctx.language || "typescript";
|
|
398
|
+
if (lang !== "python")
|
|
399
|
+
return { violations, coverage };
|
|
400
|
+
try {
|
|
401
|
+
const { execSync } = require("child_process");
|
|
402
|
+
const fs = require("fs");
|
|
403
|
+
const os = require("os");
|
|
404
|
+
// engine.js 位于 dist/trust/ → 仓库根 tools/ 需要两级向上
|
|
405
|
+
const scriptPath = path.resolve(__dirname, "..", "..", "tools", "extract_framework_django.py");
|
|
406
|
+
const outPath = path.join(os.tmpdir(), `progmune-fwdj-${process.pid}-${Date.now()}.json`);
|
|
407
|
+
execSync(`python3 "${scriptPath}" "${ctx.projectPath}" "${outPath}"`, {
|
|
408
|
+
encoding: "utf-8",
|
|
409
|
+
stdio: "pipe",
|
|
410
|
+
timeout: 60000,
|
|
411
|
+
});
|
|
412
|
+
if (!fs.existsSync(outPath))
|
|
413
|
+
return { violations, coverage };
|
|
414
|
+
const structure = JSON.parse(fs.readFileSync(outPath, "utf-8"));
|
|
415
|
+
fs.unlinkSync(outPath);
|
|
416
|
+
const analysis = (0, django_detector_1.analyzeDjangoStructure)(structure);
|
|
417
|
+
if (!analysis.hasDjango)
|
|
418
|
+
return { violations, coverage };
|
|
419
|
+
coverage.apps = 1;
|
|
420
|
+
coverage.routes = (structure.routes || []).length;
|
|
421
|
+
coverage.filesScanned = structure.filesScanned || 0;
|
|
422
|
+
for (const issue of analysis.issues) {
|
|
423
|
+
violations.push({
|
|
424
|
+
severity: issue.severity === "low" ? "low" : issue.severity,
|
|
425
|
+
rule_id: issue.rule,
|
|
426
|
+
file: issue.file || "",
|
|
427
|
+
function: issue.handler || "unknown",
|
|
428
|
+
message: issue.message,
|
|
429
|
+
evidence: issue.route || "",
|
|
430
|
+
why: `Framework structural analysis: ${issue.message}`,
|
|
431
|
+
fix: issue.rule === "DRF_PERMISSION_BYPASS"
|
|
432
|
+
? `Require authenticated permissions on write methods (e.g. permission_classes = (IsAuthenticated,)).`
|
|
433
|
+
: `Add a login decorator (@login_required) or an auth mixin (LoginRequiredMixin) to the view.`,
|
|
434
|
+
policy_ref: "framework-safety.django",
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch { /* best-effort — framework analysis must never break evaluation */ }
|
|
439
|
+
return { violations, coverage };
|
|
440
|
+
}
|
|
441
|
+
function collectFastapiViolations(ctx) {
|
|
442
|
+
const violations = [];
|
|
443
|
+
const coverage = { apps: 0, routes: 0, filesScanned: 0 };
|
|
444
|
+
// 仅 Python 项目跑框架结构扫描(避免 TS 项目无谓的 python3 子进程)
|
|
445
|
+
const lang = ctx.language || "typescript";
|
|
446
|
+
if (lang !== "python")
|
|
447
|
+
return { violations, coverage };
|
|
448
|
+
try {
|
|
449
|
+
const { execSync } = require("child_process");
|
|
450
|
+
const fs = require("fs");
|
|
451
|
+
const os = require("os");
|
|
452
|
+
// engine.js 位于 dist/trust/ → 仓库根 tools/ 需要两级向上
|
|
453
|
+
const scriptPath = path.resolve(__dirname, "..", "..", "tools", "extract_framework_py.py");
|
|
454
|
+
const outPath = path.join(os.tmpdir(), `progmune-fw-${process.pid}-${Date.now()}.json`);
|
|
455
|
+
execSync(`python3 "${scriptPath}" "${ctx.projectPath}" "${outPath}"`, {
|
|
456
|
+
encoding: "utf-8",
|
|
457
|
+
stdio: "pipe",
|
|
458
|
+
timeout: 60000,
|
|
459
|
+
});
|
|
460
|
+
if (!fs.existsSync(outPath))
|
|
461
|
+
return { violations, coverage };
|
|
462
|
+
const structure = JSON.parse(fs.readFileSync(outPath, "utf-8"));
|
|
463
|
+
fs.unlinkSync(outPath);
|
|
464
|
+
const analysis = (0, fastapi_detector_1.analyzeFastapiStructure)(structure);
|
|
465
|
+
if (!analysis.hasFastAPI)
|
|
466
|
+
return { violations, coverage };
|
|
467
|
+
coverage.apps = (structure.apps || []).length + (structure.routers || []).length;
|
|
468
|
+
coverage.routes = (structure.routes || []).length;
|
|
469
|
+
coverage.filesScanned = structure.filesScanned || 0;
|
|
470
|
+
for (const issue of analysis.issues) {
|
|
471
|
+
violations.push({
|
|
472
|
+
severity: issue.severity === "low" ? "low" : issue.severity,
|
|
473
|
+
rule_id: issue.rule,
|
|
474
|
+
file: issue.file || "",
|
|
475
|
+
function: issue.handler || "unknown",
|
|
476
|
+
message: issue.message,
|
|
477
|
+
evidence: issue.route || "",
|
|
478
|
+
why: `Framework structural analysis: ${issue.message}`,
|
|
479
|
+
fix: issue.rule === "FASTAPI_ROUTE_NO_AUTH"
|
|
480
|
+
? `Add an auth dependency to the route (Depends/Security of an authenticator) or move it behind authenticated middleware.`
|
|
481
|
+
: `Reference the scheme from a route dependency or remove the unused scheme.`,
|
|
482
|
+
policy_ref: "framework-safety.fastapi",
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
catch { /* best-effort — framework analysis must never break evaluation */ }
|
|
487
|
+
return { violations, coverage };
|
|
488
|
+
}
|
|
367
489
|
function collectExpressViolations(ctx) {
|
|
368
490
|
const violations = [];
|
|
369
491
|
let expressApps = 0;
|
|
@@ -717,6 +839,8 @@ async function collectProtocolViolations(ctx, callGraph) {
|
|
|
717
839
|
let ssgTotalCalls = 0;
|
|
718
840
|
let ssgMatchedCalls = 0;
|
|
719
841
|
let ssgViolationCount = 0;
|
|
842
|
+
// C 注解建议(函数级作用域——返回值在 try 外组装)
|
|
843
|
+
let annotationSuggestions;
|
|
720
844
|
try {
|
|
721
845
|
// ── P4.5 校准:TS/JS 项目在 ir.json 缺失时先提取 IR ──
|
|
722
846
|
// 序列必须来自「函数体内真实调用」而非「文件内函数声明名单」——
|
|
@@ -801,6 +925,10 @@ async function collectProtocolViolations(ctx, callGraph) {
|
|
|
801
925
|
// ── P4.6.1: 词段匹配门控的项目函数集合(best-effort,与注解合并共用 ir.json) ──
|
|
802
926
|
// 词段匹配只对项目函数适用(改名协议原语);外部库调用走 alias/关键词桥接。
|
|
803
927
|
let projectFunctions;
|
|
928
|
+
// ── 注解建议(采纳生死线):未注解 C 项目的原语注解候选(加性,仅 C) ──
|
|
929
|
+
// 未注解项目 SSG 层静默(0 flags)——建议清单是「看不见的问题」的入口:
|
|
930
|
+
// 按函数名词汇启发式给出现成注释块模板(角色/命名空间/状态转移预填),
|
|
931
|
+
// 人工确认后生效(与 c-alias-propose 的确认门同一哲学)。
|
|
804
932
|
try {
|
|
805
933
|
const fs = require("fs");
|
|
806
934
|
const irPath = path.join(ctx.projectPath, "ir.json");
|
|
@@ -808,6 +936,9 @@ async function collectProtocolViolations(ctx, callGraph) {
|
|
|
808
936
|
const ir = JSON.parse(fs.readFileSync(irPath, "utf-8"));
|
|
809
937
|
const functions = Array.isArray(ir) ? ir : (ir.functions || []);
|
|
810
938
|
projectFunctions = (0, call_sequence_1.collectProjectFunctionNames)(functions);
|
|
939
|
+
if ((ctx.language || "typescript") === "c") {
|
|
940
|
+
annotationSuggestions = (0, annotation_suggest_1.suggestAnnotations)(functions, protocolRulesData ? new Set(protocolRulesData.rules.keys()) : undefined);
|
|
941
|
+
}
|
|
811
942
|
}
|
|
812
943
|
}
|
|
813
944
|
catch { /* best-effort */ }
|
|
@@ -962,6 +1093,7 @@ async function collectProtocolViolations(ctx, callGraph) {
|
|
|
962
1093
|
aliasWarnings: protocolRulesData.aliasWarnings?.length
|
|
963
1094
|
? protocolRulesData.aliasWarnings : undefined,
|
|
964
1095
|
} : undefined,
|
|
1096
|
+
annotationSuggestions,
|
|
965
1097
|
};
|
|
966
1098
|
}
|
|
967
1099
|
function extractCallSequencesFromProject(projectPath, language, keepNames) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "progmune-runtime",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.8",
|
|
4
4
|
"description": "Progmune — AI Trust Decision Engine. Verify AI-generated code before it reaches production. Outputs APPROVED / NEEDS_REVIEW / BLOCKED with evidence.",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/",
|