@getmikk/core 2.0.13 → 2.0.15
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/README.md +4 -4
- package/package.json +2 -1
- package/src/analysis/index.ts +9 -0
- package/src/analysis/taint-analysis.ts +419 -0
- package/src/analysis/type-flow.ts +247 -0
- package/src/cache/incremental-cache.ts +278 -0
- package/src/cache/index.ts +1 -0
- package/src/contract/contract-generator.ts +31 -3
- package/src/contract/contract-reader.ts +1 -0
- package/src/contract/lock-compiler.ts +125 -12
- package/src/contract/schema.ts +4 -0
- package/src/error-handler.ts +2 -1
- package/src/graph/cluster-detector.ts +2 -4
- package/src/graph/dead-code-detector.ts +303 -117
- package/src/graph/graph-builder.ts +21 -161
- package/src/graph/impact-analyzer.ts +1 -0
- package/src/graph/index.ts +2 -0
- package/src/graph/rich-function-index.ts +1080 -0
- package/src/graph/symbol-table.ts +252 -0
- package/src/hash/hash-store.ts +1 -0
- package/src/index.ts +4 -0
- package/src/parser/base-extractor.ts +19 -0
- package/src/parser/boundary-checker.ts +31 -12
- package/src/parser/error-recovery.ts +647 -0
- package/src/parser/function-body-extractor.ts +248 -0
- package/src/parser/go/go-extractor.ts +249 -676
- package/src/parser/index.ts +138 -295
- package/src/parser/language-registry.ts +57 -0
- package/src/parser/oxc-parser.ts +166 -28
- package/src/parser/oxc-resolver.ts +179 -11
- package/src/parser/parser-constants.ts +1 -0
- package/src/parser/rust/rust-extractor.ts +109 -0
- package/src/parser/tree-sitter/parser.ts +400 -66
- package/src/parser/tree-sitter/queries.ts +106 -10
- package/src/parser/types.ts +20 -1
- package/src/search/bm25.ts +21 -8
- package/src/search/direct-search.ts +472 -0
- package/src/search/embedding-provider.ts +249 -0
- package/src/search/index.ts +12 -0
- package/src/search/semantic-search.ts +435 -0
- package/src/security/index.ts +1 -0
- package/src/security/scanner.ts +342 -0
- package/src/utils/artifact-transaction.ts +1 -0
- package/src/utils/atomic-write.ts +1 -0
- package/src/utils/errors.ts +89 -4
- package/src/utils/fs.ts +150 -65
- package/src/utils/json.ts +1 -0
- package/src/utils/language-registry.ts +96 -5
- package/src/utils/minimatch.ts +49 -6
- package/src/utils/path.ts +26 -0
- package/tests/dead-code.test.ts +3 -2
- package/tests/direct-search.test.ts +435 -0
- package/tests/error-recovery.test.ts +143 -0
- package/tests/fixtures/simple-api/src/index.ts +1 -1
- package/tests/go-parser.test.ts +19 -335
- package/tests/js-parser.test.ts +18 -1089
- package/tests/language-registry-all.test.ts +276 -0
- package/tests/language-registry.test.ts +6 -4
- package/tests/parse-diagnostics.test.ts +9 -96
- package/tests/parser.test.ts +42 -771
- package/tests/polyglot-parser.test.ts +117 -0
- package/tests/rich-function-index.test.ts +703 -0
- package/tests/tree-sitter-parser.test.ts +108 -80
- package/tests/ts-parser.test.ts +8 -8
- package/tests/verification.test.ts +175 -0
- package/src/parser/base-parser.ts +0 -16
- package/src/parser/go/go-parser.ts +0 -43
- package/src/parser/javascript/js-extractor.ts +0 -278
- package/src/parser/javascript/js-parser.ts +0 -101
- package/src/parser/typescript/ts-extractor.ts +0 -447
- package/src/parser/typescript/ts-parser.ts +0 -36
package/README.md
CHANGED
|
@@ -75,10 +75,10 @@ One root hash comparison = instant full drift detection. Persisted in SQLite wit
|
|
|
75
75
|
|
|
76
76
|
Compiles a `DependencyGraph` + `MikkContract` + parsed files into a `MikkLock`. The lock file is the single source of truth for all MCP tools and CLI commands.
|
|
77
77
|
|
|
78
|
-
Lock format
|
|
79
|
-
- Integer-based function index (`fnIndex`) — call graph edges stored as integer references
|
|
80
|
-
- Compact JSON output
|
|
81
|
-
- Backward-compatible hydration
|
|
78
|
+
Lock format:
|
|
79
|
+
- Integer-based function index (`fnIndex`) — call graph edges stored as integer references
|
|
80
|
+
- Compact JSON output
|
|
81
|
+
- Backward-compatible hydration
|
|
82
82
|
|
|
83
83
|
### ContractReader / ContractWriter / LockReader
|
|
84
84
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getmikk/core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.15",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"eslint": "^9.39.2"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"@google/generative-ai": "^0.21.0",
|
|
36
37
|
"better-sqlite3": "^12.6.2",
|
|
37
38
|
"fast-glob": "^3.3.0",
|
|
38
39
|
"tree-sitter-wasms": "^0.1.13",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analysis modules - Type Flow and Taint Analysis for semantic code understanding
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { TypeFlowAnalyzer } from './type-flow.js'
|
|
6
|
+
export type { TypeFlowInfo, TypeParam, TypeEdge, TypeFlowResult } from './type-flow.js'
|
|
7
|
+
|
|
8
|
+
export { TaintAnalyzer } from './taint-analysis.js'
|
|
9
|
+
export type { TaintSource, TaintSink, TaintFlow, DataFlowResult } from './taint-analysis.js'
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Flow & Taint Analysis — tracks data propagation through code
|
|
3
|
+
* for security vulnerability detection.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MikkLock, MikkLockFunction } from '../contract/schema.js'
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Types
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export interface TaintSource {
|
|
13
|
+
name: string
|
|
14
|
+
description: string
|
|
15
|
+
severity: 'critical' | 'high' | 'medium' | 'low'
|
|
16
|
+
patterns: RegExp[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TaintSink {
|
|
20
|
+
name: string
|
|
21
|
+
description: string
|
|
22
|
+
severity: 'critical' | 'high' | 'medium' | 'low'
|
|
23
|
+
patterns: RegExp[]
|
|
24
|
+
sanitizers: string[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TaintFlow {
|
|
28
|
+
source: string
|
|
29
|
+
sink: string
|
|
30
|
+
path: string[]
|
|
31
|
+
vulnerability: string
|
|
32
|
+
severity: 'critical' | 'high' | 'medium' | 'low'
|
|
33
|
+
confidence: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DataFlowResult {
|
|
37
|
+
flows: TaintFlow[]
|
|
38
|
+
summary: {
|
|
39
|
+
totalFlows: number
|
|
40
|
+
critical: number
|
|
41
|
+
high: number
|
|
42
|
+
medium: number
|
|
43
|
+
low: number
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Default Taint Sources and Sinks
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
const DEFAULT_TAINT_SOURCES: TaintSource[] = [
|
|
52
|
+
{
|
|
53
|
+
name: 'user-input',
|
|
54
|
+
description: 'User-controlled input',
|
|
55
|
+
severity: 'high',
|
|
56
|
+
patterns: [
|
|
57
|
+
/req\.(body|query|params|headers)/i,
|
|
58
|
+
/request\.(body|query|params|headers)/i,
|
|
59
|
+
/input\(/i,
|
|
60
|
+
/process\.env/i,
|
|
61
|
+
/process\.argv/i,
|
|
62
|
+
/\bstdin\b/i,
|
|
63
|
+
/readline\(/i,
|
|
64
|
+
/readFile\(/i,
|
|
65
|
+
/fetch\(/i,
|
|
66
|
+
/axios\(/i,
|
|
67
|
+
/http\.request\(/i,
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'filesystem',
|
|
72
|
+
description: 'File system input',
|
|
73
|
+
severity: 'medium',
|
|
74
|
+
patterns: [
|
|
75
|
+
/readFile\(/i,
|
|
76
|
+
/readFileSync\(/i,
|
|
77
|
+
/readdir\(/i,
|
|
78
|
+
/createReadStream\(/i,
|
|
79
|
+
/fs\.readFile\(/i,
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'database',
|
|
84
|
+
description: 'Database query results',
|
|
85
|
+
severity: 'medium',
|
|
86
|
+
patterns: [
|
|
87
|
+
/query\(/i,
|
|
88
|
+
/\.find\(/i,
|
|
89
|
+
/\.select\(/i,
|
|
90
|
+
/execute\(/i,
|
|
91
|
+
/\.fetch\(/i,
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'network',
|
|
96
|
+
description: 'Network/API responses',
|
|
97
|
+
severity: 'medium',
|
|
98
|
+
patterns: [
|
|
99
|
+
/fetch\(/i,
|
|
100
|
+
/axios\(/i,
|
|
101
|
+
/http\.get\(/i,
|
|
102
|
+
/https\.get\(/i,
|
|
103
|
+
/request\(/i,
|
|
104
|
+
/\.json\(\)/i,
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
const DEFAULT_TAINT_SINKS: TaintSink[] = [
|
|
110
|
+
{
|
|
111
|
+
name: 'sql-query',
|
|
112
|
+
description: 'SQL query execution',
|
|
113
|
+
severity: 'critical',
|
|
114
|
+
patterns: [
|
|
115
|
+
/execute\s*\(/i,
|
|
116
|
+
/query\s*\(/i,
|
|
117
|
+
/\.exec\(/i,
|
|
118
|
+
/cursor\.execute\(/i,
|
|
119
|
+
/db\.query\(/i,
|
|
120
|
+
],
|
|
121
|
+
sanitizers: ['escape', 'sanitize', 'param', 'bind', 'prepare'],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'command-injection',
|
|
125
|
+
description: 'OS command execution',
|
|
126
|
+
severity: 'critical',
|
|
127
|
+
patterns: [
|
|
128
|
+
/exec\s*\(/i,
|
|
129
|
+
/spawn\s*\(/i,
|
|
130
|
+
/execSync\s*\(/i,
|
|
131
|
+
/system\s*\(/i,
|
|
132
|
+
/popen\s*\(/i,
|
|
133
|
+
/child_process\./i,
|
|
134
|
+
],
|
|
135
|
+
sanitizers: ['execFile', 'spawnSync', 'execFileSync'],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'code-execution',
|
|
139
|
+
description: 'Dynamic code execution',
|
|
140
|
+
severity: 'critical',
|
|
141
|
+
patterns: [
|
|
142
|
+
/\beval\s*\(/i,
|
|
143
|
+
/\bFunction\s*\(/i,
|
|
144
|
+
/setTimeout\s*\(\s*\w+\s*,/i,
|
|
145
|
+
/setInterval\s*\(\s*\w+\s*,/i,
|
|
146
|
+
],
|
|
147
|
+
sanitizers: [],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'path-traversal',
|
|
151
|
+
description: 'File system operations',
|
|
152
|
+
severity: 'high',
|
|
153
|
+
patterns: [
|
|
154
|
+
/readFile\(/i,
|
|
155
|
+
/writeFile\(/i,
|
|
156
|
+
/open\(/i,
|
|
157
|
+
/createReadStream\(/i,
|
|
158
|
+
/stat\(/i,
|
|
159
|
+
/lstat\(/i,
|
|
160
|
+
/access\(/i,
|
|
161
|
+
/exists\(/i,
|
|
162
|
+
],
|
|
163
|
+
sanitizers: ['normalize', 'resolve', 'basename', 'dirname', 'join'],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'xss',
|
|
167
|
+
description: 'HTML/JS injection',
|
|
168
|
+
severity: 'high',
|
|
169
|
+
patterns: [
|
|
170
|
+
/\.innerHTML\s*=/i,
|
|
171
|
+
/\.outerHTML\s*=/i,
|
|
172
|
+
/dangerouslySetInnerHTML/i,
|
|
173
|
+
/document\.write\(/i,
|
|
174
|
+
/\.html\s*\(/i,
|
|
175
|
+
],
|
|
176
|
+
sanitizers: ['escape', 'sanitize', 'text', 'encode', 'DOMPurify'],
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'prototype-pollution',
|
|
180
|
+
description: 'Object prototype manipulation',
|
|
181
|
+
severity: 'high',
|
|
182
|
+
patterns: [
|
|
183
|
+
/\[\s*['"]__proto__['"]\s*\]/i,
|
|
184
|
+
/\[\s*['"]constructor['"]\s*\]/i,
|
|
185
|
+
/Object\.assign\s*\(\s*\w+\s*,\s*\w+\s*\)/i,
|
|
186
|
+
],
|
|
187
|
+
sanitizers: [],
|
|
188
|
+
},
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Taint Analyzer
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
export class TaintAnalyzer {
|
|
196
|
+
private lock: MikkLock
|
|
197
|
+
private sources: TaintSource[]
|
|
198
|
+
private sinks: TaintSink[]
|
|
199
|
+
|
|
200
|
+
constructor(
|
|
201
|
+
lock: MikkLock,
|
|
202
|
+
sources?: TaintSource[],
|
|
203
|
+
sinks?: TaintSink[]
|
|
204
|
+
) {
|
|
205
|
+
this.lock = lock
|
|
206
|
+
this.sources = sources || DEFAULT_TAINT_SOURCES
|
|
207
|
+
this.sinks = sinks || DEFAULT_TAINT_SINKS
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Analyze the codebase for taint flows.
|
|
212
|
+
*/
|
|
213
|
+
analyze(): DataFlowResult {
|
|
214
|
+
const flows: TaintFlow[] = []
|
|
215
|
+
const allFunctions = Object.values(this.lock.functions)
|
|
216
|
+
|
|
217
|
+
// Find functions that contain taint sources
|
|
218
|
+
const sourceFunctions = this.findTaintSources(allFunctions)
|
|
219
|
+
|
|
220
|
+
// Find functions that contain taint sinks
|
|
221
|
+
const sinkFunctions = this.findTaintSinks(allFunctions)
|
|
222
|
+
|
|
223
|
+
// Trace taint flows through call graph
|
|
224
|
+
for (const sourceFn of sourceFunctions) {
|
|
225
|
+
for (const sinkFn of sinkFunctions) {
|
|
226
|
+
if (sourceFn.id === sinkFn.fn.id) continue
|
|
227
|
+
|
|
228
|
+
const flow = this.traceTaintFlow(sourceFn, sinkFn.fn, sinkFn.sink, allFunctions)
|
|
229
|
+
if (flow) {
|
|
230
|
+
flows.push(flow)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
flows,
|
|
237
|
+
summary: {
|
|
238
|
+
totalFlows: flows.length,
|
|
239
|
+
critical: flows.filter(f => f.severity === 'critical').length,
|
|
240
|
+
high: flows.filter(f => f.severity === 'high').length,
|
|
241
|
+
medium: flows.filter(f => f.severity === 'medium').length,
|
|
242
|
+
low: flows.filter(f => f.severity === 'low').length,
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Find functions that contain taint sources.
|
|
249
|
+
*/
|
|
250
|
+
private findTaintSources(functions: MikkLockFunction[]): MikkLockFunction[] {
|
|
251
|
+
const sources: MikkLockFunction[] = []
|
|
252
|
+
|
|
253
|
+
for (const fn of functions) {
|
|
254
|
+
const fnText = `${fn.name} ${fn.purpose || ''}`.toLowerCase()
|
|
255
|
+
|
|
256
|
+
for (const source of this.sources) {
|
|
257
|
+
for (const pattern of source.patterns) {
|
|
258
|
+
if (pattern.test(fnText)) {
|
|
259
|
+
sources.push(fn)
|
|
260
|
+
break
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return sources
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Find functions that contain taint sinks.
|
|
271
|
+
*/
|
|
272
|
+
private findTaintSinks(functions: MikkLockFunction[]): Array<{ fn: MikkLockFunction; sink: TaintSink }> {
|
|
273
|
+
const sinks: Array<{ fn: MikkLockFunction; sink: TaintSink }> = []
|
|
274
|
+
|
|
275
|
+
for (const fn of functions) {
|
|
276
|
+
const fnText = `${fn.name} ${fn.purpose || ''}`.toLowerCase()
|
|
277
|
+
|
|
278
|
+
for (const sink of this.sinks) {
|
|
279
|
+
for (const pattern of sink.patterns) {
|
|
280
|
+
if (pattern.test(fnText)) {
|
|
281
|
+
sinks.push({ fn, sink })
|
|
282
|
+
break
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return sinks
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Trace taint flow from source to sink through call graph.
|
|
293
|
+
*/
|
|
294
|
+
private traceTaintFlow(
|
|
295
|
+
source: MikkLockFunction,
|
|
296
|
+
sinkFn: MikkLockFunction,
|
|
297
|
+
sink: TaintSink,
|
|
298
|
+
allFunctions: MikkLockFunction[]
|
|
299
|
+
): TaintFlow | null {
|
|
300
|
+
// Direct call: source calls sink directly
|
|
301
|
+
if (source.calls?.includes(sinkFn.id)) {
|
|
302
|
+
return {
|
|
303
|
+
source: source.name,
|
|
304
|
+
sink: sinkFn.name,
|
|
305
|
+
path: [source.name, sinkFn.name],
|
|
306
|
+
vulnerability: `${source.name} -> ${sinkFn.name}`,
|
|
307
|
+
severity: sink.severity,
|
|
308
|
+
confidence: 0.9,
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Check if there's a path through the call graph
|
|
313
|
+
const path = this.findPath(source.id, sinkFn.id, allFunctions)
|
|
314
|
+
if (path) {
|
|
315
|
+
return {
|
|
316
|
+
source: source.name,
|
|
317
|
+
sink: sinkFn.name,
|
|
318
|
+
path: path.map(id => this.lock.functions[id]?.name || id),
|
|
319
|
+
vulnerability: `${source.name} -> ${path.length - 1} intermediate(s) -> ${sinkFn.name}`,
|
|
320
|
+
severity: sink.severity,
|
|
321
|
+
confidence: 0.7,
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return null
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Find path from source to sink through call graph.
|
|
330
|
+
*/
|
|
331
|
+
private findPath(
|
|
332
|
+
sourceId: string,
|
|
333
|
+
sinkId: string,
|
|
334
|
+
allFunctions: MikkLockFunction[]
|
|
335
|
+
): string[] | null {
|
|
336
|
+
const visited = new Set<string>()
|
|
337
|
+
const path: string[] = []
|
|
338
|
+
|
|
339
|
+
function dfs(currentId: string): boolean {
|
|
340
|
+
if (currentId === sinkId) {
|
|
341
|
+
path.push(currentId)
|
|
342
|
+
return true
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (visited.has(currentId)) return false
|
|
346
|
+
visited.add(currentId)
|
|
347
|
+
path.push(currentId)
|
|
348
|
+
|
|
349
|
+
const fn = allFunctions.find(f => f.id === currentId)
|
|
350
|
+
if (fn?.calls) {
|
|
351
|
+
for (const calleeId of fn.calls) {
|
|
352
|
+
if (dfs(calleeId)) return true
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
path.pop()
|
|
357
|
+
return false
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (dfs(sourceId)) {
|
|
361
|
+
return path
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return null
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Check if a function has sanitizers that mitigate taint.
|
|
369
|
+
*/
|
|
370
|
+
private hasSanitizer(fn: MikkLockFunction, sink: TaintSink): boolean {
|
|
371
|
+
const fnText = `${fn.name} ${fn.purpose || ''}`.toLowerCase()
|
|
372
|
+
|
|
373
|
+
for (const sanitizer of sink.sanitizers) {
|
|
374
|
+
if (fnText.includes(sanitizer.toLowerCase())) {
|
|
375
|
+
return true
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return false
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Get security findings from taint analysis.
|
|
384
|
+
*/
|
|
385
|
+
getFindings(): Array<{
|
|
386
|
+
severity: string
|
|
387
|
+
title: string
|
|
388
|
+
file: string
|
|
389
|
+
line: number
|
|
390
|
+
description: string
|
|
391
|
+
}> {
|
|
392
|
+
const result = this.analyze()
|
|
393
|
+
const findings: Array<{
|
|
394
|
+
severity: string
|
|
395
|
+
title: string
|
|
396
|
+
file: string
|
|
397
|
+
line: number
|
|
398
|
+
description: string
|
|
399
|
+
}> = []
|
|
400
|
+
|
|
401
|
+
for (const flow of result.flows) {
|
|
402
|
+
const sourceFn = Object.values(this.lock.functions).find(
|
|
403
|
+
f => f.name === flow.source
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
if (sourceFn) {
|
|
407
|
+
findings.push({
|
|
408
|
+
severity: flow.severity,
|
|
409
|
+
title: `Potential ${flow.sink} vulnerability`,
|
|
410
|
+
file: sourceFn.file,
|
|
411
|
+
line: sourceFn.startLine,
|
|
412
|
+
description: `Tainted data from ${flow.source} flows to ${flow.sink} via: ${flow.path.join(' -> ')}`,
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return findings
|
|
418
|
+
}
|
|
419
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type Flow Analysis — tracks type propagation through function calls
|
|
3
|
+
* and provides type-aware code understanding beyond syntactic parsing.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MikkLock, MikkLockFunction } from '../contract/schema.js'
|
|
7
|
+
import type { DependencyGraph } from '../graph/types.js'
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Types
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export interface TypeFlowInfo {
|
|
14
|
+
/** Function ID */
|
|
15
|
+
functionId: string
|
|
16
|
+
/** Inferred parameter types */
|
|
17
|
+
paramTypes: TypeParam[]
|
|
18
|
+
/** Inferred return type */
|
|
19
|
+
returnType: string
|
|
20
|
+
/** Types that flow into this function from callers */
|
|
21
|
+
incomingTypes: TypeEdge[]
|
|
22
|
+
/** Types that flow out to callees */
|
|
23
|
+
outgoingTypes: TypeEdge[]
|
|
24
|
+
/** Type confidence score (0-1) */
|
|
25
|
+
confidence: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TypeParam {
|
|
29
|
+
name: string
|
|
30
|
+
type: string
|
|
31
|
+
source: 'annotation' | 'inference' | 'usage'
|
|
32
|
+
confidence: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TypeEdge {
|
|
36
|
+
from: string
|
|
37
|
+
to: string
|
|
38
|
+
type: string
|
|
39
|
+
confidence: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TypeFlowResult {
|
|
43
|
+
flows: Map<string, TypeFlowInfo>
|
|
44
|
+
summary: {
|
|
45
|
+
totalFunctions: number
|
|
46
|
+
typedFunctions: number
|
|
47
|
+
inferredFunctions: number
|
|
48
|
+
averageConfidence: number
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Type Flow Analyzer
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
export class TypeFlowAnalyzer {
|
|
57
|
+
private lock: MikkLock
|
|
58
|
+
private graph: DependencyGraph
|
|
59
|
+
|
|
60
|
+
constructor(lock: MikkLock, graph: DependencyGraph) {
|
|
61
|
+
this.lock = lock
|
|
62
|
+
this.graph = graph
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Analyze type flow across the entire codebase.
|
|
67
|
+
*/
|
|
68
|
+
analyze(): TypeFlowResult {
|
|
69
|
+
const flows = new Map<string, TypeFlowInfo>()
|
|
70
|
+
const allFunctions = Object.values(this.lock.functions)
|
|
71
|
+
|
|
72
|
+
// Phase 1: Extract explicit type annotations
|
|
73
|
+
for (const fn of allFunctions) {
|
|
74
|
+
flows.set(fn.id, this.extractExplicitTypes(fn))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Phase 2: Propagate types through call graph
|
|
78
|
+
this.propagateTypes(flows)
|
|
79
|
+
|
|
80
|
+
// Phase 3: Compute summary statistics
|
|
81
|
+
const summary = this.computeSummary(flows)
|
|
82
|
+
|
|
83
|
+
return { flows, summary }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get type flow for a specific function.
|
|
88
|
+
*/
|
|
89
|
+
getFunctionFlow(functionId: string): TypeFlowInfo | null {
|
|
90
|
+
const result = this.analyze()
|
|
91
|
+
return result.flows.get(functionId) ?? null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Find all functions that return a specific type.
|
|
96
|
+
*/
|
|
97
|
+
findFunctionsByReturnType(typeName: string): MikkLockFunction[] {
|
|
98
|
+
const result = this.analyze()
|
|
99
|
+
const matches: MikkLockFunction[] = []
|
|
100
|
+
|
|
101
|
+
for (const [fnId, flow] of result.flows) {
|
|
102
|
+
if (flow.returnType.toLowerCase().includes(typeName.toLowerCase())) {
|
|
103
|
+
const fn = this.lock.functions[fnId]
|
|
104
|
+
if (fn) matches.push(fn)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return matches
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Find all functions that accept a specific parameter type.
|
|
113
|
+
*/
|
|
114
|
+
findFunctionsByParamType(typeName: string): MikkLockFunction[] {
|
|
115
|
+
const result = this.analyze()
|
|
116
|
+
const matches: MikkLockFunction[] = []
|
|
117
|
+
|
|
118
|
+
for (const [fnId, flow] of result.flows) {
|
|
119
|
+
for (const param of flow.paramTypes) {
|
|
120
|
+
if (param.type.toLowerCase().includes(typeName.toLowerCase())) {
|
|
121
|
+
const fn = this.lock.functions[fnId]
|
|
122
|
+
if (fn) matches.push(fn)
|
|
123
|
+
break
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return matches
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Extract explicit type annotations from function metadata.
|
|
133
|
+
*/
|
|
134
|
+
private extractExplicitTypes(fn: MikkLockFunction): TypeFlowInfo {
|
|
135
|
+
const paramTypes: TypeParam[] = []
|
|
136
|
+
|
|
137
|
+
if (fn.params) {
|
|
138
|
+
for (const param of fn.params) {
|
|
139
|
+
paramTypes.push({
|
|
140
|
+
name: param.name,
|
|
141
|
+
type: param.type || 'unknown',
|
|
142
|
+
source: param.type ? 'annotation' : 'inference',
|
|
143
|
+
confidence: param.type ? 1.0 : 0.3,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
functionId: fn.id,
|
|
150
|
+
paramTypes,
|
|
151
|
+
returnType: fn.returnType || 'unknown',
|
|
152
|
+
incomingTypes: [],
|
|
153
|
+
outgoingTypes: [],
|
|
154
|
+
confidence: this.computeConfidence(paramTypes, fn.returnType),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Propagate types through the call graph.
|
|
160
|
+
*/
|
|
161
|
+
private propagateTypes(flows: Map<string, TypeFlowInfo>): void {
|
|
162
|
+
// Build type propagation edges
|
|
163
|
+
for (const [fnId, flow] of flows) {
|
|
164
|
+
const fn = this.lock.functions[fnId]
|
|
165
|
+
if (!fn) continue
|
|
166
|
+
|
|
167
|
+
// Find outgoing calls
|
|
168
|
+
for (const calleeId of fn.calls || []) {
|
|
169
|
+
const calleeFlow = flows.get(calleeId)
|
|
170
|
+
if (!calleeFlow) continue
|
|
171
|
+
|
|
172
|
+
// Create type edges for parameters
|
|
173
|
+
for (let i = 0; i < calleeFlow.paramTypes.length; i++) {
|
|
174
|
+
const param = calleeFlow.paramTypes[i]
|
|
175
|
+
if (param.type !== 'unknown') {
|
|
176
|
+
flow.outgoingTypes.push({
|
|
177
|
+
from: fnId,
|
|
178
|
+
to: calleeId,
|
|
179
|
+
type: param.type,
|
|
180
|
+
confidence: param.confidence * 0.8,
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Create type edges for return type
|
|
186
|
+
if (calleeFlow.returnType !== 'unknown') {
|
|
187
|
+
flow.incomingTypes.push({
|
|
188
|
+
from: calleeId,
|
|
189
|
+
to: fnId,
|
|
190
|
+
type: calleeFlow.returnType,
|
|
191
|
+
confidence: calleeFlow.confidence * 0.8,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Compute confidence score for type information.
|
|
200
|
+
*/
|
|
201
|
+
private computeConfidence(paramTypes: TypeParam[], returnType?: string): number {
|
|
202
|
+
let totalConfidence = 0
|
|
203
|
+
let count = 0
|
|
204
|
+
|
|
205
|
+
for (const param of paramTypes) {
|
|
206
|
+
totalConfidence += param.confidence
|
|
207
|
+
count++
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (returnType && returnType !== 'unknown') {
|
|
211
|
+
totalConfidence += 0.9
|
|
212
|
+
count++
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return count > 0 ? totalConfidence / count : 0.1
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Compute summary statistics.
|
|
220
|
+
*/
|
|
221
|
+
private computeSummary(flows: Map<string, TypeFlowInfo>): TypeFlowResult['summary'] {
|
|
222
|
+
const totalFunctions = flows.size
|
|
223
|
+
let typedFunctions = 0
|
|
224
|
+
let inferredFunctions = 0
|
|
225
|
+
let totalConfidence = 0
|
|
226
|
+
|
|
227
|
+
for (const flow of flows.values()) {
|
|
228
|
+
if (flow.returnType !== 'unknown') {
|
|
229
|
+
typedFunctions++
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const hasInferred = flow.paramTypes.some(p => p.source === 'inference' || p.source === 'usage')
|
|
233
|
+
if (hasInferred) {
|
|
234
|
+
inferredFunctions++
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
totalConfidence += flow.confidence
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
totalFunctions,
|
|
242
|
+
typedFunctions,
|
|
243
|
+
inferredFunctions,
|
|
244
|
+
averageConfidence: totalFunctions > 0 ? totalConfidence / totalFunctions : 0,
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|