progmune-runtime 3.7.56 → 3.7.58
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 +59 -0
- package/README.md +12 -11
- package/README.zh-CN.md +12 -11
- package/dist/frameworks/nestjs-detector.js +428 -161
- package/dist/frameworks/nestjs-detector.test.js +220 -0
- package/dist/protocol-detector-identity-substring.test.js +139 -0
- package/dist/protocol-detector.js +28 -11
- package/dist/sdk.js +1 -1
- package/dist/trust/alert-ranker.js +3 -1
- package/dist/trust/confidence-calculator.js +3 -1
- package/dist/trust/engine.js +20 -2
- package/dist/trust/score-calculator.js +88 -8
- package/dist/trust/score-calculator.test.js +149 -0
- package/dist/trust/types.js +17 -1
- package/package.json +1 -1
- package/tools/extract_ir.py +38 -0
|
@@ -418,3 +418,223 @@ export class NotesController {
|
|
|
418
418
|
(0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST notes/create")).toBe(true);
|
|
419
419
|
});
|
|
420
420
|
});
|
|
421
|
+
// ═══════════════════════════════════════════════════════════
|
|
422
|
+
// §52(2026-10-03):app-level provider 注册事件
|
|
423
|
+
//
|
|
424
|
+
// immich held-out 实测倒在了第三种注册途径上:
|
|
425
|
+
// const commonMiddleware = [{ provide: APP_PIPE, useClass: ZodValidationPipe }, ...];
|
|
426
|
+
// const apiMiddleware = [FileUploadInterceptor, ...commonMiddleware, {...}];
|
|
427
|
+
// @Module({ providers: [...common, ...apiMiddleware, ...] })
|
|
428
|
+
// `APP_PIPE` 与 `@Module` 之间隔了**两跳数组 spread**,只看装饰器那一层永远
|
|
429
|
+
// 看不到 ⇒ 153 条 NO_VALIDATION 误报。修复原则是「注册事件」而非「写法」:
|
|
430
|
+
// 哪里有注册意图,哪里就是证据,不再要求它必须直连某个 @Module。
|
|
431
|
+
//
|
|
432
|
+
// 同组反向测试是硬要求(R75):压制放宽后必须证明「该报的还在报」。
|
|
433
|
+
// ═══════════════════════════════════════════════════════════
|
|
434
|
+
(0, vitest_1.describe)("nestjs-detector app-level provider 注册事件(§52)", () => {
|
|
435
|
+
const ZOD_STUB = `
|
|
436
|
+
export function createZodDto(schema: any): any { return class {} as any; }
|
|
437
|
+
export const CreateAlbumSchema = { albumName: "string" };
|
|
438
|
+
export class CreateAlbumDto extends createZodDto(CreateAlbumSchema) {}
|
|
439
|
+
export class PlainAlbumDto { albumName: string = ""; }
|
|
440
|
+
`;
|
|
441
|
+
const ALBUM_CONTROLLER = `
|
|
442
|
+
import { Body, Controller, Post } from "@nestjs/common";
|
|
443
|
+
import { CreateAlbumDto } from "./album.dto";
|
|
444
|
+
@Controller("albums")
|
|
445
|
+
export class AlbumController {
|
|
446
|
+
@Post()
|
|
447
|
+
create(@Body() dto: CreateAlbumDto) { return {}; }
|
|
448
|
+
}
|
|
449
|
+
`;
|
|
450
|
+
const APP_MODULE_SPREAD = `
|
|
451
|
+
import { Module } from "@nestjs/common";
|
|
452
|
+
import { APP_PIPE } from "@nestjs/core";
|
|
453
|
+
import { ZodValidationPipe } from "nestjs-zod";
|
|
454
|
+
import { FileUploadInterceptor } from "./middleware";
|
|
455
|
+
|
|
456
|
+
const commonMiddleware = [
|
|
457
|
+
{ provide: APP_PIPE, useClass: ZodValidationPipe },
|
|
458
|
+
];
|
|
459
|
+
const apiMiddleware = [FileUploadInterceptor, ...commonMiddleware];
|
|
460
|
+
|
|
461
|
+
@Module({ providers: [...apiMiddleware] })
|
|
462
|
+
export class AppModule {}
|
|
463
|
+
`;
|
|
464
|
+
function writeImmishApp() {
|
|
465
|
+
write("tsconfig.json", TSCONFIG);
|
|
466
|
+
write("src/album.dto.ts", ZOD_STUB);
|
|
467
|
+
write("src/album.controller.ts", ALBUM_CONTROLLER);
|
|
468
|
+
write("src/middleware.ts", `
|
|
469
|
+
export class FileUploadInterceptor {}
|
|
470
|
+
`);
|
|
471
|
+
}
|
|
472
|
+
(0, vitest_1.it)("APP_PIPE 经两跳 spread 进 @Module ⇒ 已校验 DTO 的 mutation 不报 NO_VALIDATION(immich 形态)", () => {
|
|
473
|
+
writeImmishApp();
|
|
474
|
+
write("src/app.module.ts", APP_MODULE_SPREAD);
|
|
475
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
476
|
+
(0, vitest_1.expect)(a.globalValidationPipes).toContain("ZodValidationPipe");
|
|
477
|
+
const nv = a.issues.filter((i) => i.type === "NESTJS_NO_VALIDATION");
|
|
478
|
+
(0, vitest_1.expect)(nv.map((i) => i.route)).not.toContain("POST albums");
|
|
479
|
+
});
|
|
480
|
+
(0, vitest_1.it)("反向:同一形态但摘掉 APP_PIPE 注册 ⇒ 精确转红(不是无差别压制)", () => {
|
|
481
|
+
writeImmishApp();
|
|
482
|
+
write("src/app.module.ts", `
|
|
483
|
+
import { Module } from "@nestjs/common";
|
|
484
|
+
import { FileUploadInterceptor } from "./middleware";
|
|
485
|
+
const apiMiddleware = [FileUploadInterceptor];
|
|
486
|
+
@Module({ providers: [...apiMiddleware] })
|
|
487
|
+
export class AppModule {}
|
|
488
|
+
`);
|
|
489
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
490
|
+
(0, vitest_1.expect)(a.globalValidationPipes).toEqual([]);
|
|
491
|
+
(0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST albums")).toBe(true);
|
|
492
|
+
});
|
|
493
|
+
(0, vitest_1.it)("反向:有全局管道但入参不是已校验 DTO ⇒ 仍报(保守豁免不扩散到裸类型)", () => {
|
|
494
|
+
writeImmishApp();
|
|
495
|
+
write("src/app.module.ts", APP_MODULE_SPREAD);
|
|
496
|
+
write("src/album.controller.ts", `
|
|
497
|
+
import { Body, Controller, Post } from "@nestjs/common";
|
|
498
|
+
import { PlainAlbumDto } from "./album.dto";
|
|
499
|
+
@Controller("albums")
|
|
500
|
+
export class AlbumController {
|
|
501
|
+
@Post()
|
|
502
|
+
create(@Body() dto: PlainAlbumDto) { return {}; }
|
|
503
|
+
}
|
|
504
|
+
`);
|
|
505
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
506
|
+
(0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST albums")).toBe(true);
|
|
507
|
+
});
|
|
508
|
+
(0, vitest_1.it)("反向:注释里的 APP_PIPE 不算注册事件 ⇒ 仍报(text-fallback 抗噪)", () => {
|
|
509
|
+
writeImmishApp();
|
|
510
|
+
write("src/app.module.ts", `
|
|
511
|
+
import { Module } from "@nestjs/common";
|
|
512
|
+
// 计划中:providers: [{ provide: APP_PIPE, useClass: ZodValidationPipe }]
|
|
513
|
+
// * 文档注释形态同样不应被当作注册证据
|
|
514
|
+
@Module({ providers: [] })
|
|
515
|
+
export class AppModule {}
|
|
516
|
+
`);
|
|
517
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
518
|
+
(0, vitest_1.expect)(a.globalValidationPipes).toEqual([]);
|
|
519
|
+
(0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST albums")).toBe(true);
|
|
520
|
+
});
|
|
521
|
+
(0, vitest_1.it)("@UsePipes 自定义管道名(不带 Pipe 后缀)⇒ 识别为已注册管道", () => {
|
|
522
|
+
writeImmishApp();
|
|
523
|
+
write("src/album.controller.ts", `
|
|
524
|
+
import { Body, Controller, Post, UsePipes } from "@nestjs/common";
|
|
525
|
+
import { CreateAlbumDto } from "./album.dto";
|
|
526
|
+
@Controller("albums")
|
|
527
|
+
export class AlbumController {
|
|
528
|
+
@Post()
|
|
529
|
+
@UsePipes(bodyTrimSanitizer)
|
|
530
|
+
create(@Body() dto: CreateAlbumDto) { return {}; }
|
|
531
|
+
}
|
|
532
|
+
`);
|
|
533
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
534
|
+
(0, vitest_1.expect)(a.issues.filter((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST albums")).toEqual([]);
|
|
535
|
+
});
|
|
536
|
+
(0, vitest_1.it)("@UseGuards(AuthGuard('JWT')) 的参数字符串不产生假认证证据(反向 Sophie)", () => {
|
|
537
|
+
write("tsconfig.json", TSCONFIG);
|
|
538
|
+
write("src/auth.guard.ts", GUARD);
|
|
539
|
+
write("src/api.controller.ts", `
|
|
540
|
+
import { Controller, Post, UseGuards } from "@nestjs/common";
|
|
541
|
+
import { JWTStrategy } from "./jwt";
|
|
542
|
+
@Controller("api")
|
|
543
|
+
export class ApiController {
|
|
544
|
+
@Post("transfer")
|
|
545
|
+
@UseGuards(JWTStrategy)
|
|
546
|
+
transfer() { return {}; }
|
|
547
|
+
}
|
|
548
|
+
`);
|
|
549
|
+
write("src/jwt.ts", `
|
|
550
|
+
export const JWT = "Bearer";
|
|
551
|
+
export class JWTStrategy { canActivate(): boolean { return false; } }
|
|
552
|
+
`);
|
|
553
|
+
write("src/app.module.ts", MODULE_EMPTY);
|
|
554
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
555
|
+
// JWTStrategy 名字里带 jwt ⇒ 认证守卫(EXPECT 有保护)
|
|
556
|
+
(0, vitest_1.expect)(a.issues.filter((i) => i.type === "NESTJS_NO_AUTH")).toEqual([]);
|
|
557
|
+
// 但若写成字符串参数,不得凭 'JWT' 这个 token 冒充守卫名
|
|
558
|
+
write("src/api.controller.ts", `
|
|
559
|
+
import { Controller, Post, UseGuards } from "@nestjs/common";
|
|
560
|
+
import { AnonymousGate } from "./gate";
|
|
561
|
+
@Controller("api")
|
|
562
|
+
export class ApiController {
|
|
563
|
+
@Post("transfer")
|
|
564
|
+
@UseGuards(AnonymousGate("JWT"))
|
|
565
|
+
transfer() { return {}; }
|
|
566
|
+
}
|
|
567
|
+
`);
|
|
568
|
+
write("src/gate.ts", `
|
|
569
|
+
export function AnonymousGate(scheme: string): any { return class { canActivate() { return true; } }; }
|
|
570
|
+
`);
|
|
571
|
+
const b = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
572
|
+
(0, vitest_1.expect)(b.issues.some((i) => i.type === "NESTJS_NO_AUTH" && i.route === "POST api/transfer")).toBe(true);
|
|
573
|
+
});
|
|
574
|
+
(0, vitest_1.it)("registrations 带可复核的 (file, line) 与发现途径", () => {
|
|
575
|
+
writeImmishApp();
|
|
576
|
+
write("src/app.module.ts", APP_MODULE_SPREAD);
|
|
577
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
578
|
+
const pipeReg = a.registrations.filter((r) => r.token === "APP_PIPE");
|
|
579
|
+
(0, vitest_1.expect)(pipeReg.length).toBeGreaterThan(0);
|
|
580
|
+
for (const r of pipeReg) {
|
|
581
|
+
// 可复核:每条注册都必须能指回某个文件的某一行
|
|
582
|
+
(0, vitest_1.expect)(r.file).toMatch(/app\.module\.ts$/);
|
|
583
|
+
(0, vitest_1.expect)(r.line).toBeGreaterThan(0);
|
|
584
|
+
(0, vitest_1.expect)(r.impl).toBe("ZodValidationPipe");
|
|
585
|
+
(0, vitest_1.expect)(["module-decorator", "module-property", "array-literal", "text-fallback"]).toContain(r.source);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
(0, vitest_1.it)("数组路径别名:展开成多条 route,公开豁免重新生效(nocodb 形态,普查 72%)", () => {
|
|
589
|
+
write("tsconfig.json", TSCONFIG);
|
|
590
|
+
write("src/app.module.ts", MODULE_EMPTY);
|
|
591
|
+
write("src/token.controller.ts", `
|
|
592
|
+
import { Controller, Post } from "@nestjs/common";
|
|
593
|
+
@Controller(["meta/bases/:id", "api/v1/db/meta/projects/:id"])
|
|
594
|
+
export class TokenController {
|
|
595
|
+
@Post(["login", "signin"])
|
|
596
|
+
login() { return {}; }
|
|
597
|
+
@Post(["api-tokens"])
|
|
598
|
+
createToken() { return {}; }
|
|
599
|
+
}
|
|
600
|
+
`);
|
|
601
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
602
|
+
const paths = a.routes.map((r) => `${r.method} ${r.path}`).sort();
|
|
603
|
+
// 2 个 controller 别名 × 3 个方法别名 = 6 条
|
|
604
|
+
(0, vitest_1.expect)(paths).toEqual([
|
|
605
|
+
"POST api/v1/db/meta/projects/:id/api-tokens",
|
|
606
|
+
"POST api/v1/db/meta/projects/:id/login",
|
|
607
|
+
"POST api/v1/db/meta/projects/:id/signin",
|
|
608
|
+
"POST meta/bases/:id/api-tokens",
|
|
609
|
+
"POST meta/bases/:id/login",
|
|
610
|
+
"POST meta/bases/:id/signin",
|
|
611
|
+
]);
|
|
612
|
+
// 旧实现下 path 是整段数组文本 ⇒ /login 豁免匹配不到 ⇒ 4 条 login/signin 全被误报
|
|
613
|
+
const noAuth = a.issues.filter((i) => i.type === "NESTJS_NO_AUTH").map((i) => i.route);
|
|
614
|
+
(0, vitest_1.expect)(noAuth.sort()).toEqual([
|
|
615
|
+
"POST api/v1/db/meta/projects/:id/api-tokens",
|
|
616
|
+
"POST meta/bases/:id/api-tokens",
|
|
617
|
+
]);
|
|
618
|
+
});
|
|
619
|
+
(0, vitest_1.it)("@Module(configVar):装饰器参数是变量时仍能读到 providers(nocodb 形态,普查 71%)", () => {
|
|
620
|
+
writeImmishApp();
|
|
621
|
+
write("src/app.module.ts", `
|
|
622
|
+
import { Module } from "@nestjs/common";
|
|
623
|
+
import { APP_PIPE } from "@nestjs/core";
|
|
624
|
+
import { ZodValidationPipe } from "nestjs-zod";
|
|
625
|
+
|
|
626
|
+
const moduleMetadata = {
|
|
627
|
+
imports: [],
|
|
628
|
+
controllers: [],
|
|
629
|
+
providers: [{ provide: APP_PIPE, useClass: ZodValidationPipe }],
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
@Module(moduleMetadata)
|
|
633
|
+
export class AppModule {}
|
|
634
|
+
`);
|
|
635
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
636
|
+
(0, vitest_1.expect)(a.globalValidationPipes).toContain("ZodValidationPipe");
|
|
637
|
+
const nv = a.issues.filter((i) => i.type === "NESTJS_NO_VALIDATION");
|
|
638
|
+
(0, vitest_1.expect)(nv.map((i) => i.route)).not.toContain("POST albums");
|
|
639
|
+
});
|
|
640
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
/**
|
|
37
|
+
* fr-001 定向回归:身份映射查找用 JSON.contains 子串匹配
|
|
38
|
+
* (REALWORLD_FIX_REGRESSION_V1 fr-001,open-webui CVE-2026-87016)
|
|
39
|
+
*
|
|
40
|
+
* 判据在 tools/extract_ir.py 的 has_identity_substring_match:
|
|
41
|
+
* ① 函数名身份查找形态(oauth/scim/sso/saml + sub/subject/external_id/identity)
|
|
42
|
+
* ② 体内有 <对象>.contains( 调用
|
|
43
|
+
* ③ contains 对象链含 oauth/scim 字段
|
|
44
|
+
* 三条件同时成立才注入 __progmune_identity_substring_match__ 标记。
|
|
45
|
+
*/
|
|
46
|
+
const vitest_1 = require("vitest");
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const os = __importStar(require("os"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const extract_ir_python_1 = require("./extract-ir-python");
|
|
51
|
+
let dir;
|
|
52
|
+
(0, vitest_1.beforeEach)(() => {
|
|
53
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), "fr001-"));
|
|
54
|
+
});
|
|
55
|
+
(0, vitest_1.afterEach)(() => {
|
|
56
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
57
|
+
});
|
|
58
|
+
function write(rel, content) {
|
|
59
|
+
const full = path.join(dir, rel);
|
|
60
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
61
|
+
fs.writeFileSync(full, content);
|
|
62
|
+
}
|
|
63
|
+
const MARKER = "__progmune_identity_substring_match__";
|
|
64
|
+
(0, vitest_1.describe)("fr-001 identity substring match(提取器标记)", () => {
|
|
65
|
+
(0, vitest_1.it)("修复前形态:oauth JSON 列 contains() 做身份映射 → 注入标记", () => {
|
|
66
|
+
write("users.py", `
|
|
67
|
+
class User:
|
|
68
|
+
oauth = None
|
|
69
|
+
scim = None
|
|
70
|
+
|
|
71
|
+
async def get_user_by_oauth_sub(provider, sub, db=None):
|
|
72
|
+
query = select(User)
|
|
73
|
+
if dialect == 'sqlite':
|
|
74
|
+
oauth_match = User.oauth.contains({provider: {'sub': sub}})
|
|
75
|
+
query = query.where(oauth_match)
|
|
76
|
+
row = await session.execute(query)
|
|
77
|
+
return row
|
|
78
|
+
|
|
79
|
+
async def get_user_by_scim_external_id(provider, external_id, db=None):
|
|
80
|
+
query = select(User)
|
|
81
|
+
if dialect == 'sqlite':
|
|
82
|
+
scim_match = User.scim.contains({provider: {'external_id': external_id}})
|
|
83
|
+
query = query.where(scim_match)
|
|
84
|
+
return (await session.execute(query)).scalars().first()
|
|
85
|
+
`);
|
|
86
|
+
const ir = (0, extract_ir_python_1.extractIRPython)(dir);
|
|
87
|
+
const oauthFn = ir.find((f) => f.name === "get_user_by_oauth_sub");
|
|
88
|
+
const scimFn = ir.find((f) => f.name === "get_user_by_scim_external_id");
|
|
89
|
+
(0, vitest_1.expect)(oauthFn).toBeDefined();
|
|
90
|
+
(0, vitest_1.expect)(scimFn).toBeDefined();
|
|
91
|
+
(0, vitest_1.expect)(oauthFn.calls).toContain(MARKER);
|
|
92
|
+
(0, vitest_1.expect)(scimFn.calls).toContain(MARKER);
|
|
93
|
+
});
|
|
94
|
+
(0, vitest_1.it)("修复后形态:下标提取 + 精确等值 → 无标记", () => {
|
|
95
|
+
write("users.py", `
|
|
96
|
+
async def get_user_by_oauth_sub(provider, sub, db=None):
|
|
97
|
+
# Subscript, never contains(): on a JSON column contains() degrades to a substring LIKE.
|
|
98
|
+
query = select(User).where(User.oauth[provider]['sub'].as_string() == sub)
|
|
99
|
+
row = (await session.execute(query)).scalars().first()
|
|
100
|
+
return row
|
|
101
|
+
|
|
102
|
+
async def get_user_by_scim_external_id(provider, external_id, db=None):
|
|
103
|
+
query = select(User).where(User.scim[provider]['external_id'].as_string() == external_id)
|
|
104
|
+
return (await session.execute(query)).scalars().first()
|
|
105
|
+
`);
|
|
106
|
+
const ir = (0, extract_ir_python_1.extractIRPython)(dir);
|
|
107
|
+
for (const f of ir) {
|
|
108
|
+
(0, vitest_1.expect)(f.calls).not.toContain(MARKER);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
(0, vitest_1.it)("负例:非身份查找函数用 .contains → 无标记(函数名不含 oauth/scim 词形)", () => {
|
|
112
|
+
write("utils.py", `
|
|
113
|
+
def has_feature(config, name):
|
|
114
|
+
return config.features.contains({'name': name})
|
|
115
|
+
|
|
116
|
+
async def get_user_by_id(user_id, db=None):
|
|
117
|
+
query = select(User).where(User.id == user_id)
|
|
118
|
+
return (await session.execute(query)).scalars().first()
|
|
119
|
+
`);
|
|
120
|
+
const ir = (0, extract_ir_python_1.extractIRPython)(dir);
|
|
121
|
+
for (const f of ir) {
|
|
122
|
+
(0, vitest_1.expect)(f.calls).not.toContain(MARKER);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
(0, vitest_1.it)("负例:身份词形函数但 contains 对象不是 oauth/scim 字段 → 无标记", () => {
|
|
126
|
+
write("utils.py", `
|
|
127
|
+
async def get_oauth_subject_from_claim(claim, db=None):
|
|
128
|
+
# 函数名含 oauth+subject,但 contains 的对象是普通 dict,不是模型 JSON 列
|
|
129
|
+
payload = {'claims': {'sub': claim}}
|
|
130
|
+
if payload.contains({'provider': 'x'}):
|
|
131
|
+
return claim
|
|
132
|
+
return None
|
|
133
|
+
`);
|
|
134
|
+
const ir = (0, extract_ir_python_1.extractIRPython)(dir);
|
|
135
|
+
for (const f of ir) {
|
|
136
|
+
(0, vitest_1.expect)(f.calls).not.toContain(MARKER);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -845,6 +845,23 @@ const SAFEGUARD_RULES = [
|
|
|
845
845
|
conceptMissing: ["ResourceOwnership", "CrossUserReference"],
|
|
846
846
|
conceptExpected: ["folder write-access check", "get_*_by_id_and_user_id query"],
|
|
847
847
|
},
|
|
848
|
+
{
|
|
849
|
+
name: "Identity Lookup with Substring Match",
|
|
850
|
+
category: "auth",
|
|
851
|
+
// 2026-10-04(REALWORLD_FIX_REGRESSION_V1 fr-001):Python 提取器在
|
|
852
|
+
// 「身份映射查找函数(oauth_sub/scim_external_id 词形)对 oauth/scim
|
|
853
|
+
// JSON 列调用 .contains()」时注入标记——JSON 列的 contains() 退化为
|
|
854
|
+
// 子串 LIKE(open-webui CVE-2026-87016 修复注释原话),subject 含
|
|
855
|
+
// %/_ 通配符可绑定任意账户(含管理员)。证据语义在提取器侧
|
|
856
|
+
// (has_identity_substring_match,三条件:函数名身份查找形态 +
|
|
857
|
+
// 体内有 .contains( 调用 + 对象链含 oauth/scim 字段)。
|
|
858
|
+
languages: ["python"],
|
|
859
|
+
trigger: /\b(__progmune_identity_substring_match__)\b/,
|
|
860
|
+
safeguards: [],
|
|
861
|
+
violationMessage: "Identity mapping lookup (OAuth subject / SCIM external-id) matches with JSON contains() — on a JSON column contains() degrades to a substring LIKE, so a subject containing %/_ wildcards can bind to an arbitrary account (incl. admin). Use subscript extraction + equality (e.g. User.oauth[provider]['sub'].as_string() == sub).",
|
|
862
|
+
conceptMissing: ["ExactIdentityMatch", "SubscriptExtraction"],
|
|
863
|
+
conceptExpected: ["oauth[provider]['sub'] == sub", "scim[provider]['external_id'] == external_id"],
|
|
864
|
+
},
|
|
848
865
|
{
|
|
849
866
|
name: "XSS (Unsafe Template Rendering)",
|
|
850
867
|
category: "xss",
|
|
@@ -1121,17 +1138,17 @@ const SAFEGUARD_RULES = [
|
|
|
1121
1138
|
conceptExpected: ["checkExpiry", "autoRenew", "certificate watch"],
|
|
1122
1139
|
},
|
|
1123
1140
|
// ── Data Integrity: Mutation without audit ──
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1141
|
+
// 2026-10-04(修冤枉第一刀):移除 "Data Mutation Without Audit Trail"。
|
|
1142
|
+
// 数据依据:fp-gold 131/131 全 FP(docmost/hedgedoc/hoppscotch/verdaccio/
|
|
1143
|
+
// 3×realworld 七仓九片);TS 盲测 gold 795 条 0 条此族;PyGoat 232 条里
|
|
1144
|
+
// 此族 8 条全 unlabeled 且样本全 FP 形态;84 条 GHSA 无一条审计轨迹类。
|
|
1145
|
+
// 根因是概念性失明(非词表问题):真实架构里审计在框架/中间件层
|
|
1146
|
+
// (ORM 拦截器/数据库触发器/日志系统),函数名里永远不会出现 audit
|
|
1147
|
+
// 字样 ⇒ 判据「mutation 函数名附近有审计词汇」在真实代码形态上永远
|
|
1148
|
+
// 不可能满足。与授权族保留的理由对照(R92):授权族在真实世界有 TP
|
|
1149
|
+
// (fr 语料 8/9 位置命中是授权类),此族在所有口径 TP=0。
|
|
1150
|
+
// 若未来有函数级审计判据(如 decorator/@Audited 标记通道),按
|
|
1151
|
+
// 「先语料后规则」重新提案。
|
|
1135
1152
|
// ═══════════════════════════════════════════════════════════════
|
|
1136
1153
|
// Governance meta-rule: Framework Version Convention Check
|
|
1137
1154
|
// Lesson from proxy.ts incident (2026-08-03)
|
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.58";
|
|
24
24
|
function verify(filePath) {
|
|
25
25
|
const cert = (0, certify_1.certify)(filePath);
|
|
26
26
|
const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
|
|
@@ -79,7 +79,9 @@ exports.DEFAULT_RULE_PRIOR = {
|
|
|
79
79
|
"Password Hashing": 0.0525,
|
|
80
80
|
"Password Hashing (Weak)": 0.0525,
|
|
81
81
|
"Registration Without Email Verification": 0.0525,
|
|
82
|
-
|
|
82
|
+
// 2026-10-04:Data Mutation Without Audit Trail 规则已移除(fp-gold
|
|
83
|
+
// 131/131 全 FP、全口径 TP=0,概念性失明——见 protocol-detector.ts 注记),
|
|
84
|
+
// 先验条目随之删除。
|
|
83
85
|
};
|
|
84
86
|
/** 未知族的兜底先验 = 全局基线 */
|
|
85
87
|
exports.DEFAULT_BASE = 0.0984;
|
|
@@ -207,6 +207,8 @@ function computeCoverageConfidence(projectPath) {
|
|
|
207
207
|
// 1. Load protocol transition space
|
|
208
208
|
const space = loadTransitionSpace(projectPath);
|
|
209
209
|
const namespaces = [...space.perNamespace.keys()];
|
|
210
|
+
// §53:协议定义不存在 ⇒ 本指标不可测(0 分是「没得测」,不是「测得低」)
|
|
211
|
+
const applicable = fs.existsSync(path.join(projectPath, "protocols.json"));
|
|
210
212
|
// 2. Load trajectory coverage
|
|
211
213
|
const coverage = loadTrajectoryCoverage(projectPath);
|
|
212
214
|
// 3. Compute per-namespace confidence
|
|
@@ -250,7 +252,7 @@ function computeCoverageConfidence(projectPath) {
|
|
|
250
252
|
const summary = `Weighted coverage: ${score}% ±${margin}%. ` +
|
|
251
253
|
`${saturated} namespaces saturated, ${partial} partial, ${noVocab} no vocabulary. ` +
|
|
252
254
|
(noVocab > 0 ? `Top gap: add trajectories for ${noVocab} uncovered namespaces.` : "");
|
|
253
|
-
return { score, margin, level, breakdown, summary };
|
|
255
|
+
return { score, margin, level, breakdown, summary, applicable };
|
|
254
256
|
}
|
|
255
257
|
/**
|
|
256
258
|
* Clear internal caches (for testing or when corpus is updated).
|
package/dist/trust/engine.js
CHANGED
|
@@ -178,16 +178,30 @@ async function evaluateTrust(ctx) {
|
|
|
178
178
|
const effectiveScore = policyResult.hasCritical
|
|
179
179
|
? Math.min(overallScore, types_1.DECISION_THRESHOLDS.criticalLock)
|
|
180
180
|
: overallScore;
|
|
181
|
-
const decision = (0, score_calculator_1.determineDecision)(effectiveScore, policyResult.hasCritical, explainResult.status);
|
|
182
181
|
// ── Phase 1: Coverage-based confidence (replaces qualitative labels) ──
|
|
183
182
|
const coverageConfidence = (0, confidence_calculator_1.computeCoverageConfidence)(ctx.projectPath);
|
|
183
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
184
|
+
// §53 / R97:决策门禁
|
|
185
|
+
//
|
|
186
|
+
// 实测触发场景(immich / nocodb):authentication 子协议 0 分(21 / 586 条违规),
|
|
187
|
+
// coverageConfidence 0% LOW,mappingCoverage 4%~8% LOW —— 三项独立的
|
|
188
|
+
// 「其实没看到什么」的指标全部触底,聚合结果却仍是 83 / APPROVED / HIGH。
|
|
189
|
+
//
|
|
190
|
+
// 门禁只做**封顶**(不许说通过),不做扣分:没有观察就没有扣分依据。
|
|
191
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
192
|
+
const decisionGates = (0, score_calculator_1.evaluateDecisionGates)({
|
|
193
|
+
protocolSafety: protocolResult,
|
|
194
|
+
coverageLevel: coverageConfidence.level,
|
|
195
|
+
coverageApplicable: coverageConfidence.applicable,
|
|
196
|
+
});
|
|
197
|
+
const decision = (0, score_calculator_1.determineDecision)(effectiveScore, policyResult.hasCritical, explainResult.status, decisionGates);
|
|
184
198
|
const dimConfidences = [
|
|
185
199
|
policyResult.hasCritical ? "LOW" : coverageResult.confidence,
|
|
186
200
|
protocolResult.confidence,
|
|
187
201
|
coverageResult.confidence,
|
|
188
202
|
governanceResult.confidence,
|
|
189
203
|
];
|
|
190
|
-
const confidence = (0, score_calculator_1.determineConfidence)(dimConfidences, explainResult.status);
|
|
204
|
+
const confidence = (0, score_calculator_1.determineConfidence)(dimConfidences, explainResult.status, decisionGates.observationIncomplete);
|
|
191
205
|
// ═══════════════════════════════════════
|
|
192
206
|
// PHASE 5: ASSEMBLE
|
|
193
207
|
// ═══════════════════════════════════════
|
|
@@ -227,17 +241,21 @@ async function evaluateTrust(ctx) {
|
|
|
227
241
|
score: effectiveScore,
|
|
228
242
|
decision,
|
|
229
243
|
confidence,
|
|
244
|
+
decisionGates,
|
|
230
245
|
coverageConfidence: extractionWarning
|
|
231
246
|
? {
|
|
232
247
|
score: 0,
|
|
233
248
|
margin: 25,
|
|
234
249
|
level: "LOW",
|
|
250
|
+
// §53:IR 提取失败同样是「没得测」,不是「测得低」
|
|
251
|
+
applicable: false,
|
|
235
252
|
summary: `${extractionWarning}——本次扫描是废票,不得据此得出「干净」结论`,
|
|
236
253
|
}
|
|
237
254
|
: {
|
|
238
255
|
score: coverageConfidence.score,
|
|
239
256
|
margin: coverageConfidence.margin,
|
|
240
257
|
level: coverageConfidence.level,
|
|
258
|
+
applicable: coverageConfidence.applicable,
|
|
241
259
|
summary: coverageConfidence.summary,
|
|
242
260
|
},
|
|
243
261
|
extractionWarning,
|