@holmes-lab/holmes-kit 0.4.0 → 0.4.1

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.
@@ -1,6 +1,41 @@
1
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.TAINT_CASES = exports.TAINT_CONFIG = void 0;
37
+ exports.runTaintBenchmarkAsync = runTaintBenchmarkAsync;
38
+ exports.compareEngines = compareEngines;
4
39
  exports.runTaintBenchmark = runTaintBenchmark;
5
40
  exports.taintOnRealCorpus = taintOnRealCorpus;
6
41
  // @implements A-SPEC-354
@@ -78,7 +113,67 @@ exports.TAINT_CASES = [
78
113
  code: `function h(req) { const c = req.body.cmd; return c.length; }`,
79
114
  },
80
115
  ];
81
- /** Run the current engine over the labelled cases and score it. */
116
+ // @implements A-SPEC-512.1
117
+ /**
118
+ * The flow-SENSITIVE verdict for one case, judged on reaching definitions (the CPG foundation's
119
+ * first consumer). Async because the wasm substrate parses; `runTaintBenchmarkAsync` is the entry
120
+ * point that can use it, while the synchronous `runTaintBenchmark` keeps the original engine.
121
+ */
122
+ async function flowSensitiveVerdict(code) {
123
+ const { parseAst } = await Promise.resolve().then(() => __importStar(require('../cpg/foundation/ast-store')));
124
+ const { cfgOf, functionsIn } = await Promise.resolve().then(() => __importStar(require('../cpg/foundation/cfg')));
125
+ const { ddgOf } = await Promise.resolve().then(() => __importStar(require('../cpg/foundation/ddg')));
126
+ const { flowSensitiveTaint } = await Promise.resolve().then(() => __importStar(require('./flow-sensitive-taint')));
127
+ const ast = await parseAst(code, 'case.ts');
128
+ if (!ast)
129
+ return false;
130
+ for (const fn of functionsIn(ast)) {
131
+ const cfg = cfgOf(ast, fn, code);
132
+ if ('unsupported' in cfg)
133
+ continue;
134
+ const ddg = ddgOf(ast, cfg, fn, code);
135
+ if ('unsupported' in ddg)
136
+ continue;
137
+ if (flowSensitiveTaint({ ast, cfg, ddg, source: code, fnName: 'case', config: exports.TAINT_CONFIG }).findings.length > 0) {
138
+ return true;
139
+ }
140
+ }
141
+ return false;
142
+ }
143
+ // @implements A-SPEC-512.1
144
+ /** Score the corpus with EITHER engine — an improvement is reported as a contrast, never alone. */
145
+ async function runTaintBenchmarkAsync(cases = exports.TAINT_CASES, engine = 'flow-insensitive') {
146
+ if (engine === 'flow-insensitive')
147
+ return runTaintBenchmark(cases);
148
+ const outcomes = [];
149
+ for (const c of cases) {
150
+ const actual = await flowSensitiveVerdict(c.code);
151
+ const cell = c.vulnerable ? (actual ? 'tp' : 'fn') : (actual ? 'fp' : 'tn');
152
+ outcomes.push({ name: c.name, difficulty: c.difficulty, expected: c.vulnerable, actual, cell });
153
+ }
154
+ return scoreOutcomes(outcomes);
155
+ }
156
+ // @implements A-SPEC-512.1
157
+ /** before/after in ONE call — the single truth both the report and its test consume. */
158
+ async function compareEngines(cases = exports.TAINT_CASES) {
159
+ const before = await runTaintBenchmarkAsync(cases, 'flow-insensitive');
160
+ const after = await runTaintBenchmarkAsync(cases, 'flow-sensitive');
161
+ const moved = after.outcomes
162
+ .map((a, i) => ({ name: a.name, from: before.outcomes[i].cell, to: a.cell }))
163
+ .filter((m) => m.from !== m.to);
164
+ return { before, after, moved };
165
+ }
166
+ /** Cells → metrics. Shared so both engines are scored by exactly the same arithmetic. */
167
+ function scoreOutcomes(outcomes) {
168
+ const count = (k) => outcomes.filter((o) => o.cell === k).length;
169
+ const tp = count('tp'), tn = count('tn'), fp = count('fp'), fn = count('fn');
170
+ return {
171
+ outcomes, tp, tn, fp, fn,
172
+ precision: tp + fp === 0 ? null : tp / (tp + fp),
173
+ recall: tp + fn === 0 ? null : tp / (tp + fn),
174
+ controlFlowAttributable: outcomes.filter((o) => (o.cell === 'fp' || o.cell === 'fn') && o.difficulty !== 'flow-free').length,
175
+ };
176
+ }
82
177
  function runTaintBenchmark(cases = exports.TAINT_CASES) {
83
178
  const parser = new language_parser_1.TreeSitterTsParser();
84
179
  const outcomes = [];
@@ -94,14 +189,7 @@ function runTaintBenchmark(cases = exports.TAINT_CASES) {
94
189
  : (actual ? 'fp' : 'tn');
95
190
  outcomes.push({ name: c.name, difficulty: c.difficulty, expected: c.vulnerable, actual, cell });
96
191
  }
97
- const count = (k) => outcomes.filter((o) => o.cell === k).length;
98
- const tp = count('tp'), tn = count('tn'), fp = count('fp'), fn = count('fn');
99
- return {
100
- outcomes, tp, tn, fp, fn,
101
- precision: tp + fp === 0 ? null : tp / (tp + fp),
102
- recall: tp + fn === 0 ? null : tp / (tp + fn),
103
- controlFlowAttributable: outcomes.filter((o) => (o.cell === 'fp' || o.cell === 'fn') && o.difficulty !== 'flow-free').length,
104
- };
192
+ return scoreOutcomes(outcomes);
105
193
  }
106
194
  /**
107
195
  * How often the taint analysis fires on REAL code.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Taint vocabulary, per language — because "we found nothing" and "we do not speak this language"
3
+ * are different sentences and the tool was saying the first while meaning the second.
4
+ *
5
+ * Measured on jarvis (27,232 Python files) 2026-09-02: the shipped list matched `subprocess.Popen`
6
+ * and `exec(`, and missed `subprocess.run` (7 files), `os.environ` (5) and `input(` (4) — the three
7
+ * most common Python spellings. The engine already handled Python (its CFG/DDG landed in S-510.7);
8
+ * it simply had no Python words to look for.
9
+ *
10
+ * The lists are DATA and the judgement is one function: adding a language grows this table, not the
11
+ * code. And the vocabulary is SELECTED by file language, never unioned — a union would let Python's
12
+ * `input(` taint an ordinary TypeScript `input` and each language would inherit the other's false
13
+ * positives.
14
+ */
15
+ import type { DataFlowTaintConfig } from './dataflow-taint';
16
+ export declare const TAINT_VOCABULARY: Record<'typescript' | 'python', DataFlowTaintConfig>;
17
+ /**
18
+ * The vocabulary this file should be judged with, or null when the file is outside the analysable
19
+ * set. Null is deliberate: degrading an unknown language to the TypeScript list would produce
20
+ * findings in a language whose control flow we cannot even build.
21
+ */
22
+ export declare function vocabularyFor(relPath: string): DataFlowTaintConfig | null;
23
+ /** Languages this vocabulary table covers — the matrix derives its taint row from this. */
24
+ export declare const TAINT_LANGUAGES: ReadonlySet<string>;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TAINT_LANGUAGES = exports.TAINT_VOCABULARY = void 0;
4
+ exports.vocabularyFor = vocabularyFor;
5
+ /**
6
+ * TypeScript / JavaScript — value-identical to the shipped list, which is what makes "no regression
7
+ * for TS" checkable rather than asserted (a test compares the two).
8
+ */
9
+ const TYPESCRIPT_VOCABULARY = {
10
+ sources: ['getenv', 'argv', 'req.body', 'request', 'stdin', 'readfile', 'readinput', 'query.get'],
11
+ sinks: ['exec', 'eval', 'system', 'query', 'spawn', 'popen', 'deserialize'],
12
+ sanitizers: ['escape', 'sanitize', 'encodeuricomponent'],
13
+ };
14
+ /**
15
+ * Python. Sinks are matched on the callee's LAST segment, which is why `subprocess.run` appears as
16
+ * `run` and `pickle.loads` as `loads`. Sources are matched against expression TEXT, so they carry
17
+ * their qualifier (`os.environ`) — that qualifier is what keeps a local variable named `environ`
18
+ * from becoming a source.
19
+ *
20
+ * `input` is listed with its opening parenthesis on purpose: bare `input` is an extremely common
21
+ * identifier (a parameter, a field, a React prop in mixed repos), so only the CALL is a source.
22
+ * That decision closes H-SPEC-513's open question, and it is the same "the act, not the name" rule
23
+ * the sink matcher follows.
24
+ */
25
+ const PYTHON_VOCABULARY = {
26
+ sources: [
27
+ 'os.environ', 'os.getenv', 'sys.argv', 'sys.stdin', 'input(',
28
+ 'request.args', 'request.form', 'request.json', 'request.data', 'request.values',
29
+ ],
30
+ sinks: [
31
+ 'run', 'call', 'check_output', 'check_call', // subprocess.*
32
+ 'Popen', 'popen', 'system', // subprocess.Popen / os.popen / os.system
33
+ 'exec', 'eval', 'compile', // built-in code execution
34
+ 'loads', 'load', // pickle / yaml deserialization
35
+ ],
36
+ sanitizers: ['quote', 'escape', 'shlex.quote', 're.escape'],
37
+ };
38
+ exports.TAINT_VOCABULARY = {
39
+ typescript: TYPESCRIPT_VOCABULARY,
40
+ python: PYTHON_VOCABULARY,
41
+ };
42
+ const TS_EXT = /\.(ts|mts|cts|tsx|js|mjs|cjs|jsx)$/i;
43
+ const PY_EXT = /\.py$/i;
44
+ // @implements A-SPEC-513.1
45
+ /**
46
+ * The vocabulary this file should be judged with, or null when the file is outside the analysable
47
+ * set. Null is deliberate: degrading an unknown language to the TypeScript list would produce
48
+ * findings in a language whose control flow we cannot even build.
49
+ */
50
+ function vocabularyFor(relPath) {
51
+ if (TS_EXT.test(relPath))
52
+ return exports.TAINT_VOCABULARY.typescript;
53
+ if (PY_EXT.test(relPath))
54
+ return exports.TAINT_VOCABULARY.python;
55
+ return null;
56
+ }
57
+ /** Languages this vocabulary table covers — the matrix derives its taint row from this. */
58
+ exports.TAINT_LANGUAGES = new Set(Object.keys(exports.TAINT_VOCABULARY));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "@implements A-SPEC-209",
3
3
  "name": "@holmes-lab/holmes-kit",
4
- "version": "0.4.0",
4
+ "version": "0.4.1",
5
5
  "description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
6
6
  "main": "dist/holmes/mcp/server.js",
7
7
  "types": "dist/holmes/mcp/server.d.ts",