progmune-runtime 3.7.36 → 3.7.37

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 CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.37] — 2026-09-19
4
+
5
+ ### G2:自定义校验函数的调用点抑制(判别力)
6
+
7
+ 缺口实证(taintpath_B `dispatchToolGuarded`,measured):
8
+
9
+ ```ts
10
+ assertTemplateName(args.name); // 函数体内是 /^[A-Za-z0-9_-]+$/ —— 真校验
11
+ return loadTemplate(args.name); // 仍被标记(误报)
12
+ ```
13
+
14
+ `assertTemplateName` 名字不含路径语义后缀——`Name` 在修 `ensureDir()` 误判时被
15
+ 整体移出了 G-C 后缀表,于是调用点侧认不出来。这是 G1 收紧词表的已知代价。
16
+
17
+ 修法**不是**把 `Name` 加回词表(那是按名字猜语义,G-C 已经为这份宽松付过代价:
18
+ fr-007 pre 侧召回归零),而是看**被调用方函数体内到底有没有校验证据**:
19
+
20
+ - `pathGuardFunctionNames` 拆两档:tier-0 `direct`(自身含证据)/ tier-1+ `all`(推断)
21
+ - `collectSanitizedExprs`:被调用方属 tier-0 ⇒ 把调用实参里的取值表达式收进
22
+ 「已净化」集合;sink 实参命中污点但已被净化 ⇒ 不算污点
23
+
24
+ ### 三处精度取舍(全部比 G1 的函数级 selfGuarded 窄)
25
+
26
+ | 取舍 | 说明 |
27
+ |---|---|
28
+ | 表达式级,非函数级 | G1 是一个校验词汇压掉整个函数体的所有流;G2 只净化被传进守卫调用的那个表达式,同函数体里另一条未校验的流仍标记 |
29
+ | 只认 tier-0 | 推断出来的守卫不用于抑制 —— 升为方法学规则 **R9-no-inferred-suppression**(抑制不可逆,压掉的真阳性不会出现在任何回归统计里) |
30
+ | 前缀 `(?:^\|[^\w$.])` | 净化 `name` 不连坐 `other.name`;但 `args.name` 能命中 `path.join(DIR, args.name)` |
31
+
32
+ ### 验收
33
+
34
+ - taintpath 闸门 **24/24**(`dispatchToolGuarded` mark → suppressed;正对照
35
+ `dispatchToolBare` 仍 mark)
36
+ - **fr-007 openhop 真实语料维持 pre 5 / post 0** —— G2 未压掉真阳性
37
+ - TS 盲测(102 项目):**LOST 1 / ADDED 0**,唯一变化就是本条;覆盖力 19 → 18
38
+ - `extract-ir-taint-guard.test.ts` 新增 G2 组 6 条(含正/负对照,R6)
39
+ - 反向验证:回退 v3.7.36 后 G2 两条正例失败,其余 18 条仍绿
40
+ - 新增 `blind-benchmark/check-fr-corpus.ts` —— 把 fr-007 的 pre/post 复测固化成
41
+ 一条命令;快照置于 `blind-benchmark/fr-corpus/`(**受保护资产**,不再放 /tmp)
42
+
43
+ ### 已知缺口 G2b
44
+
45
+ 经项目自有 helper 转手的校验证据不生效(`checkName(n)` 内部再调守卫函数)——
46
+ 只认 tier-0 的代价,与 C4b 同族。缺口是可见的(仍有误报),压掉的真阳性不可见。
47
+
3
48
  ## [3.7.36] — 2026-09-19
4
49
 
5
50
  ### C4:污点经路径塑形表达式包装后仍传播
@@ -356,6 +356,157 @@ export function read(req: any) {
356
356
  }
357
357
  });
358
358
  });
359
+ (0, vitest_1.describe)("G2:调用点抑制 —— 按被调用方的实际证据定案(种子=taintpath_B assertTemplateName)", () => {
360
+ // 缺口形态(2026-09-19 measured,taintpath_B dispatchToolGuarded):
361
+ // `assertTemplateName(args.name); return loadTemplate(args.name);`
362
+ // `assertTemplateName` 函数体内是锚定字符集白名单(G-D),是真校验,
363
+ // 但它的名字不含路径语义后缀 —— `Name` 在修 `ensureDir()` 误判时被整体
364
+ // 移出了 G-C 守卫后缀表,于是调用点侧认不出来,仍被标记。
365
+ //
366
+ // G2 的修法不是把 `Name` 加回词表(那是按【名字】猜语义,G-C 已经为这份
367
+ // 宽松付过一次代价:fr-007 pre 侧召回归零),而是看**被调用方函数体内
368
+ // 到底有没有校验证据**。
369
+ //
370
+ // 精度取舍:净化作用在【表达式】上,不是整函数 —— 同函数体里另一条未被
371
+ // 校验的流仍会标记。这是 G2 相对 G1(函数级 selfGuarded)的收窄。
372
+ const body = (guardCall) => `
373
+ import * as fs from "fs";
374
+ export function read(req: any) {
375
+ const name = req.params.name;
376
+ ${guardCall} return fs.readFileSync("/data/" + name, "utf-8");
377
+ }
378
+ function assertTemplateName(name: string): void {
379
+ if (!/^[A-Za-z0-9_-]+$/.test(name)) throw new Error("bad template name");
380
+ }
381
+ `;
382
+ (0, vitest_1.it)("正对照:去掉自定义校验调用后必须标记", () => {
383
+ const dir = makeProject({ "a.ts": body("") });
384
+ try {
385
+ (0, vitest_1.expect)(marksFor(dir, "read")).toContain(PATH_MARK);
386
+ }
387
+ finally {
388
+ fs.rmSync(dir, { recursive: true, force: true });
389
+ }
390
+ });
391
+ (0, vitest_1.it)("自定义校验函数(名字不含路径语义后缀)被调用 → 不得标记", () => {
392
+ const dir = makeProject({ "a.ts": body(" assertTemplateName(name);\n") });
393
+ try {
394
+ (0, vitest_1.expect)(marksFor(dir, "read")).not.toContain(PATH_MARK);
395
+ }
396
+ finally {
397
+ fs.rmSync(dir, { recursive: true, force: true });
398
+ }
399
+ });
400
+ (0, vitest_1.it)("负对照:被调用方体内**没有**校验证据时,调用它不得抑制(不能见调用就净化)", () => {
401
+ const dir = makeProject({
402
+ "a.ts": `
403
+ import * as fs from "fs";
404
+ export function read(req: any) {
405
+ const name = req.params.name;
406
+ logIt(name);
407
+ return fs.readFileSync("/data/" + name, "utf-8");
408
+ }
409
+ function logIt(x: string): void { console.log(x); }
410
+ `,
411
+ });
412
+ try {
413
+ (0, vitest_1.expect)(marksFor(dir, "read")).toContain(PATH_MARK);
414
+ }
415
+ finally {
416
+ fs.rmSync(dir, { recursive: true, force: true });
417
+ }
418
+ });
419
+ (0, vitest_1.it)("表达式级净化:同函数体里另一条未被校验的流仍要标记", () => {
420
+ // G1 的函数级 selfGuarded 会把整个函数体一起静默;G2 只净化真正被传进
421
+ // 守卫调用的表达式 —— 这条用例锁住这个差别。
422
+ const dir = makeProject({
423
+ "a.ts": `
424
+ import * as fs from "fs";
425
+ export function read(req: any) {
426
+ const name = req.params.name;
427
+ const other = req.params.other;
428
+ assertTemplateName(name);
429
+ fs.readFileSync("/data/" + name, "utf-8");
430
+ return fs.writeFileSync("/data/" + other, "x");
431
+ }
432
+ function assertTemplateName(name: string): void {
433
+ if (!/^[A-Za-z0-9_-]+$/.test(name)) throw new Error("bad template name");
434
+ }
435
+ `,
436
+ });
437
+ try {
438
+ (0, vitest_1.expect)(marksFor(dir, "read")).toContain(PATH_MARK);
439
+ }
440
+ finally {
441
+ fs.rmSync(dir, { recursive: true, force: true });
442
+ }
443
+ });
444
+ (0, vitest_1.it)("跨函数(taintpath_B 形态):净化只发生在调用点,被调用方定义照旧被反标", () => {
445
+ // 语料里 `loadTemplate` 自身不含任何不可信根,它被标记是因为调用点命中了
446
+ // 跨函数一跳(onMethodHit 反标)。故本用例在同一文件里放两个几乎相同的
447
+ // 调用点 —— 一个净化、一个不净化 —— 才能同时锁住两侧。
448
+ const dir = makeProject({
449
+ "store.ts": `
450
+ import * as fs from "fs";
451
+ export function loadTemplate(p: string): string {
452
+ return fs.readFileSync("/srv/templates/" + p, "utf-8");
453
+ }
454
+ `,
455
+ "handler.ts": `
456
+ import { loadTemplate } from "./store";
457
+ export function dispatchToolGuarded(params: any) {
458
+ const args = params.arguments;
459
+ assertTemplateName(args.name);
460
+ return loadTemplate(args.name);
461
+ }
462
+ export function dispatchToolBare(params: any) {
463
+ const args = params.arguments;
464
+ return loadTemplate(args.name);
465
+ }
466
+ function assertTemplateName(name: string): void {
467
+ if (!/^[A-Za-z0-9_-]+$/.test(name)) throw new Error("bad template name");
468
+ }
469
+ `,
470
+ });
471
+ try {
472
+ // 净化侧:污点表达式已被校验 ⇒ 调用点不标记
473
+ (0, vitest_1.expect)(marksFor(dir, "dispatchToolGuarded")).not.toContain(PATH_MARK);
474
+ // 正对照:同形状去掉校验 ⇒ 照旧标记,且被调用方被反标
475
+ (0, vitest_1.expect)(marksFor(dir, "dispatchToolBare")).toContain(PATH_MARK);
476
+ (0, vitest_1.expect)(marksFor(dir, "loadTemplate")).toContain(PATH_MARK);
477
+ }
478
+ finally {
479
+ fs.rmSync(dir, { recursive: true, force: true });
480
+ }
481
+ });
482
+ (0, vitest_1.it)("已知缺口 G2b:经项目自有 helper 转手的校验证据不生效(保守边界,仍标记)", () => {
483
+ // 只认 tier-0(函数体自身含校验证据)。经 helper 转手 ⇒ 那是【推断】出来
484
+ // 的守卫,拿推断结果做抑制会把推断误差直接放大成误报消除。
485
+ // 代价:这类形态仍会误报,与 C4b(helper 不传播)是同一族缺口。
486
+ const dir = makeProject({
487
+ "a.ts": `
488
+ import * as fs from "fs";
489
+ export function read(req: any) {
490
+ const name = req.params.name;
491
+ checkName(name);
492
+ return fs.readFileSync("/data/" + name, "utf-8");
493
+ }
494
+ function checkName(name: string): void {
495
+ assertTemplateName(name);
496
+ }
497
+ function assertTemplateName(name: string): void {
498
+ if (!/^[A-Za-z0-9_-]+$/.test(name)) throw new Error("bad template name");
499
+ }
500
+ `,
501
+ });
502
+ try {
503
+ (0, vitest_1.expect)(marksFor(dir, "read")).toContain(PATH_MARK);
504
+ }
505
+ finally {
506
+ fs.rmSync(dir, { recursive: true, force: true });
507
+ }
508
+ });
509
+ });
359
510
  /**
360
511
  * ── 已知边界(本次写用例时实测踩到,记录以免后人重复踩)──
361
512
  *
@@ -516,15 +516,20 @@ function escapeRe(s) {
516
516
  */
517
517
  function pathGuardFunctionNames(project) {
518
518
  const guarded = new Set();
519
+ /** tier-0:函数体自身含校验证据的名字(G1 原语义) */
520
+ const direct = new Set();
519
521
  const bodies = [];
520
522
  const note = (names, text) => {
521
523
  if (!text)
522
524
  return;
523
525
  bodies.push({ names, text });
524
- if (hasPathGuardEvidence(text))
526
+ if (hasPathGuardEvidence(text)) {
525
527
  for (const n of names)
526
- if (n)
528
+ if (n) {
527
529
  guarded.add(n);
530
+ direct.add(n);
531
+ }
532
+ }
528
533
  };
529
534
  for (const sf of project.getSourceFiles()) {
530
535
  for (const cls of sf.getClasses()) {
@@ -567,7 +572,55 @@ function pathGuardFunctionNames(project) {
567
572
  if (!added)
568
573
  break;
569
574
  }
570
- return guarded;
575
+ return { all: guarded, direct };
576
+ }
577
+ // ═══════════════════════════════════════════════════════════════
578
+ // G2(2026-09-19):调用点抑制 —— 污点【表达式级】净化
579
+ // ═══════════════════════════════════════════════════════════════
580
+ // 缺口实证(taintpath_B,2026-09-19 measured):
581
+ // `assertTemplateName(args.name); return loadTemplate(args.name);`
582
+ // 仍被标记。`assertTemplateName` 的函数体内是锚定字符集白名单(G-D),
583
+ // 是**真的**校验,但它的名字不含路径语义后缀 —— `Name` 在修 `ensureDir()`
584
+ // 误判时被整体移出了 G-C 守卫后缀表,于是调用点侧认不出来。
585
+ //
586
+ // 为什么不直接放宽后缀表:那是按【名字】猜语义,会把判别力稀释成猜函数名
587
+ // (G-C 的实证代价已经量过一次:放宽 ⇒ fr-007 pre 侧召回归零)。
588
+ // 本实现改为按【被调用方的实际证据】定案:
589
+ // 函数体内调用了 tier-0 守卫函数(自身含校验证据),且污点表达式被当作
590
+ // 实参传进去 ⇒ 该表达式视为已净化,再流进 sink 不算污点。
591
+ //
592
+ // 精度取舍(关键点):净化作用在【表达式】上,不是整函数。
593
+ // G1 的 selfGuarded 是函数级的(一个校验词汇压掉整个函数体所有流);
594
+ // G2 只净化真正被传进守卫调用的那个表达式 —— 同函数体里另一条未被校验
595
+ // 的流仍会标记。这是 G2 相对 G1 的收窄,而不是又一次放宽。
596
+ // ═══════════════════════════════════════════════════════════════
597
+ /**
598
+ * 收集函数体内「被传进 tier-0 守卫函数」的实参表达式,编译成一个判定正则。
599
+ *
600
+ * 边界:只收**简单取值表达式**(标识符 / 成员 / 下标 / 字符串字面量),
601
+ * 复杂表达式(模板串、拼接、调用)不收 —— 那些形态无法在 sink 侧可靠比对。
602
+ */
603
+ function collectSanitizedExprs(text, directGuardFns) {
604
+ if (!directGuardFns || directGuardFns.size === 0)
605
+ return null;
606
+ const names = [...directGuardFns].filter((n) => !n.includes("."));
607
+ if (names.length === 0)
608
+ return null;
609
+ const callRe = new RegExp(`(?:^|[^\\w$.])(?:${names.map(escapeRe).join("|")})\\s*\\(([\\s\\S]{0,250}?)\\)`, "g");
610
+ const exprs = new Set();
611
+ let m;
612
+ while ((m = callRe.exec(text)) !== null) {
613
+ for (const a of splitArgWindow(m[1] || "")) {
614
+ const t = a.trim();
615
+ if (t && /^[A-Za-z_$][\w$]*(?:\s*\.\s*[\w$]+|\[[^\]]*\])*$/.test(t))
616
+ exprs.add(t.replace(/\s+/g, ""));
617
+ }
618
+ }
619
+ if (exprs.size === 0)
620
+ return null;
621
+ // 前缀 `(?:^|[^\w$.])` 是精度关键:`name` 不能匹配 `other.name` 里的 name,
622
+ // 但 `args.name` 能匹配 `path.join(DIR, args.name)` 里的 args.name。
623
+ return new RegExp([...exprs].map((e) => `(?:^|[^\\w$.])${escapeRe(e)}\\b`).join("|"));
571
624
  }
572
625
  const TS_FILE_SINK_NAMES = [
573
626
  "readFile", "readFileSync", "writeFile", "writeFileSync",
@@ -712,16 +765,33 @@ function taintPattern(tainted) {
712
765
  }
713
766
  return new RegExp(parts.join("|"));
714
767
  }
715
- /** 本函数体内:文件 sink 的实参窗口含 request 污点 → true */
716
- function hasTaintedSinkCall(text, tainted) {
768
+ /**
769
+ * 本函数体内:文件 sink 的实参窗口含 request 污点 → true。
770
+ *
771
+ * `sanitizedRe` 非空时(G2):实参里被净化过的表达式不再算污点 ——
772
+ * 逐实参判定,只要还有一个实参含【未被净化】的污点就照旧标记。
773
+ */
774
+ function hasTaintedSinkCall(text, tainted, sanitizedRe) {
717
775
  const taint = taintPattern(tainted);
718
776
  if (!taint)
719
777
  return false;
720
778
  const sinkRe = tsSinkCallRegex();
721
779
  let m;
722
780
  while ((m = sinkRe.exec(text)) !== null) {
723
- if (taint.test(m[2] || ""))
724
- return true;
781
+ const win = m[2] || "";
782
+ if (!taint.test(win))
783
+ continue;
784
+ if (sanitizedRe) {
785
+ const args = splitArgWindow(win);
786
+ if (args.length > 0) {
787
+ if (!args.some((a) => taint.test(a) && !sanitizedRe.test(a)))
788
+ continue;
789
+ }
790
+ else if (sanitizedRe.test(win)) {
791
+ continue;
792
+ }
793
+ }
794
+ return true;
725
795
  }
726
796
  return false;
727
797
  }
@@ -833,7 +903,7 @@ function collectUrlParamNames(params) {
833
903
  * 项目方法体内的文件 sink(跨函数一跳,onMethodHit 回调登记方法条目)
834
904
  * - SSRF:URL 形参/request 污点 → HTTP fetch sink,无守卫词汇
835
905
  */
836
- function computeMarkerCalls(text, paramNames, sinkParams, onMethodHit, guardFns) {
906
+ function computeMarkerCalls(text, paramNames, sinkParams, onMethodHit, guardFns, directGuardFns) {
837
907
  const markers = [];
838
908
  // ── 路径穿越 ──
839
909
  // G1(2026-09-19):有流还不够,必须【没有校验证据】才标记——
@@ -842,6 +912,10 @@ function computeMarkerCalls(text, paramNames, sinkParams, onMethodHit, guardFns)
842
912
  if (hasRequestRootedExpr(text)) {
843
913
  const tainted = collectTaintedNames(text);
844
914
  const selfGuarded = guardFns ? hasPathGuardEvidence(text) !== null : false;
915
+ // G2:调用点抑制 —— 被传进「自身含校验证据的函数」的表达式视为已净化。
916
+ // 与 selfGuarded(函数级、按词形)互补:这条按【被调用方的实际证据】定案,
917
+ // 因此认得出 assertTemplateName 这类不含路径语义后缀的自定义校验函数。
918
+ const sanitizedRe = collectSanitizedExprs(text, directGuardFns);
845
919
  // C5(2026-09-19):不可信根可以直接写在 sink 实参里,不需要中间变量。
846
920
  // 此前外层要求 collectTaintedNames 非空,于是
847
921
  // `fs.readFileSync("/data/" + req.params.name)` —— 真实工程里最常见的形态
@@ -849,7 +923,7 @@ function computeMarkerCalls(text, paramNames, sinkParams, onMethodHit, guardFns)
849
923
  // SSRF 侧从来不是这样(它把 UNTRUSTED_ROOT_SRC 并进 taint 模式去匹配实参窗口),
850
924
  // 这是同一条数据流上的又一处口径不一致。
851
925
  if (!selfGuarded) {
852
- if (hasTaintedSinkCall(text, tainted)) {
926
+ if (hasTaintedSinkCall(text, tainted, sanitizedRe)) {
853
927
  markers.push("__progmune_path_traversal__");
854
928
  }
855
929
  else {
@@ -872,7 +946,7 @@ function computeMarkerCalls(text, paramNames, sinkParams, onMethodHit, guardFns)
872
946
  if (guardFns && guardFns.has(m[1]))
873
947
  continue;
874
948
  const args = splitArgWindow(m[2] || "");
875
- const hit = args.some((arg, i) => rec.idxs.has(i) && taint.test(arg));
949
+ const hit = args.some((arg, i) => rec.idxs.has(i) && taint.test(arg) && !(sanitizedRe && sanitizedRe.test(arg)));
876
950
  if (hit) {
877
951
  markers.push("__progmune_path_traversal__");
878
952
  onMethodHit(rec);
@@ -990,7 +1064,10 @@ function _extractSingleProject(absRoot, tsconfigPath) {
990
1064
  // pendingMethodMarks = 跨函数命中待标记的方法条目(主循环后统一应用)
991
1065
  const sinkParams = methodSinkParamMap(project, absRoot);
992
1066
  // G1(2026-09-19):路径校验识别。自身含校验证据的函数名 + 向调用方传播一跳。
993
- const guardFns = pathGuardFunctionNames(project);
1067
+ // direct = tier-0(自身含校验证据);all = 再向调用方传播一跳后的集合。
1068
+ // G2 只用 direct —— 传播得到的名字是「被推断为守卫」,拿它做抑制会把
1069
+ // 推断误差直接放大成误报消除。
1070
+ const { all: guardFns, direct: directGuardFns } = pathGuardFunctionNames(project);
994
1071
  const pendingMethodMarks = new Set();
995
1072
  const onMethodHit = (rec) => {
996
1073
  for (const me of rec.entries)
@@ -1008,7 +1085,7 @@ function _extractSingleProject(absRoot, tsconfigPath) {
1008
1085
  const fParams = f.getParameters();
1009
1086
  const fText = f.getText();
1010
1087
  const fCalls = extractDirectCalls(f, fText);
1011
- fCalls.push(...computeMarkerCalls(fText, fParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns));
1088
+ fCalls.push(...computeMarkerCalls(fText, fParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns, directGuardFns));
1012
1089
  funcs.push({
1013
1090
  name,
1014
1091
  params: fParams.map(p => ({
@@ -1038,7 +1115,7 @@ function _extractSingleProject(absRoot, tsconfigPath) {
1038
1115
  const initParams = init.getParameters();
1039
1116
  const initText = init.getText();
1040
1117
  const initCalls = extractDirectCalls(init, initText);
1041
- initCalls.push(...computeMarkerCalls(initText, initParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns));
1118
+ initCalls.push(...computeMarkerCalls(initText, initParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns, directGuardFns));
1042
1119
  funcs.push({
1043
1120
  name,
1044
1121
  params: initParams.map(p => ({
@@ -1064,7 +1141,7 @@ function _extractSingleProject(absRoot, tsconfigPath) {
1064
1141
  const argParams = arg.getParameters();
1065
1142
  const argText = arg.getText();
1066
1143
  const argCalls = extractDirectCalls(arg, argText);
1067
- argCalls.push(...computeMarkerCalls(argText, argParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns));
1144
+ argCalls.push(...computeMarkerCalls(argText, argParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns, directGuardFns));
1068
1145
  funcs.push({
1069
1146
  name,
1070
1147
  params: argParams.map(p => ({
@@ -1098,7 +1175,7 @@ function _extractSingleProject(absRoot, tsconfigPath) {
1098
1175
  continue;
1099
1176
  const mParams = m.getParameters();
1100
1177
  const mText = m.getText();
1101
- const mCalls = computeMarkerCalls(mText, mParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns);
1178
+ const mCalls = computeMarkerCalls(mText, mParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns, directGuardFns);
1102
1179
  funcs.push({
1103
1180
  name: `${cn}.${mn}`,
1104
1181
  params: mParams.map((p) => ({ name: p.getName(), type: getParamType(p), typeDetail: getParamTypeDetail(p) })),
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.36";
23
+ exports.RUNTIME_VERSION = "3.7.37";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.36",
3
+ "version": "3.7.37",
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/",