@nahisaho/musubix-security 3.0.11 → 3.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/dist/extractors/go-extractor.d.ts +43 -70
- package/dist/extractors/go-extractor.d.ts.map +1 -1
- package/dist/extractors/go-extractor.js +596 -648
- package/dist/extractors/go-extractor.js.map +1 -1
- package/dist/extractors/index.d.ts +15 -3
- package/dist/extractors/index.d.ts.map +1 -1
- package/dist/extractors/index.js +30 -5
- package/dist/extractors/index.js.map +1 -1
- package/dist/extractors/java-extractor.d.ts +5 -0
- package/dist/extractors/java-extractor.d.ts.map +1 -1
- package/dist/extractors/java-extractor.js +7 -0
- package/dist/extractors/java-extractor.js.map +1 -1
- package/dist/extractors/ruby-extractor.d.ts +64 -0
- package/dist/extractors/ruby-extractor.d.ts.map +1 -0
- package/dist/extractors/ruby-extractor.js +491 -0
- package/dist/extractors/ruby-extractor.js.map +1 -0
- package/dist/extractors/rust-extractor.d.ts +68 -0
- package/dist/extractors/rust-extractor.d.ts.map +1 -0
- package/dist/extractors/rust-extractor.js +585 -0
- package/dist/extractors/rust-extractor.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,159 +1,263 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Go Language Extractor
|
|
3
3
|
* @module @nahisaho/musubix-security/extractors/go-extractor
|
|
4
|
-
* @trace TSK-
|
|
5
|
-
* @trace REQ-SEC-
|
|
4
|
+
* @trace TSK-GO-001, TSK-GO-002, TSK-GO-003, TSK-GO-004, TSK-GO-005
|
|
5
|
+
* @trace REQ-SEC-GO-001, REQ-SEC-GO-002, REQ-SEC-GO-003, REQ-SEC-GO-004
|
|
6
|
+
* @trace REQ-SEC-GO-005, REQ-SEC-GO-006, REQ-SEC-GO-007, REQ-SEC-GO-008
|
|
6
7
|
*/
|
|
7
8
|
import { BaseExtractor, } from './base-extractor.js';
|
|
8
9
|
/**
|
|
9
|
-
* Go framework
|
|
10
|
+
* Go framework models with proper FrameworkModel interface
|
|
11
|
+
* @trace REQ-SEC-GO-004
|
|
10
12
|
*/
|
|
11
|
-
const
|
|
12
|
-
// net/http
|
|
13
|
+
const GO_FRAMEWORK_MODELS = [
|
|
14
|
+
// net/http (Standard Library HTTP Server)
|
|
13
15
|
{
|
|
14
|
-
id: 'go-net-http',
|
|
15
16
|
name: 'net/http',
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
{ pattern:
|
|
19
|
-
{ pattern:
|
|
20
|
-
{ pattern:
|
|
21
|
-
{ pattern:
|
|
22
|
-
{ pattern:
|
|
23
|
-
{ pattern: '
|
|
17
|
+
languages: ['go'],
|
|
18
|
+
sources: [
|
|
19
|
+
{ pattern: /r\.URL\.Query\(\)/, type: 'user_input', description: 'URL query parameters', taintLabel: 'user_input' },
|
|
20
|
+
{ pattern: /r\.FormValue\(/, type: 'user_input', description: 'Form value access', taintLabel: 'user_input' },
|
|
21
|
+
{ pattern: /r\.PostFormValue\(/, type: 'user_input', description: 'POST form value', taintLabel: 'user_input' },
|
|
22
|
+
{ pattern: /r\.Header\.Get\(/, type: 'user_input', description: 'HTTP header access', taintLabel: 'user_input' },
|
|
23
|
+
{ pattern: /r\.Body/, type: 'user_input', description: 'Request body', taintLabel: 'user_input' },
|
|
24
|
+
{ pattern: /r\.Cookies\(\)/, type: 'user_input', description: 'Cookie access', taintLabel: 'user_input' },
|
|
24
25
|
],
|
|
25
|
-
|
|
26
|
-
{ pattern:
|
|
27
|
-
{ pattern:
|
|
28
|
-
{ pattern: '
|
|
29
|
-
{ pattern: 'http.Redirect', type: 'open_redirect' },
|
|
26
|
+
sinks: [
|
|
27
|
+
{ pattern: /fmt\.Fprintf\(w,/, type: 'xss', vulnerabilityType: 'xss', severity: 'high' },
|
|
28
|
+
{ pattern: /w\.Write\(/, type: 'xss', vulnerabilityType: 'xss', severity: 'high' },
|
|
29
|
+
{ pattern: /http\.Redirect\(/, type: 'redirect', vulnerabilityType: 'open_redirect', severity: 'medium' },
|
|
30
30
|
],
|
|
31
|
-
|
|
32
|
-
{ pattern:
|
|
33
|
-
{ pattern:
|
|
34
|
-
{ pattern: 'url.PathEscape', type: 'path_traversal' },
|
|
31
|
+
sanitizers: [
|
|
32
|
+
{ pattern: /html\.EscapeString\(/, sanitizes: ['xss'] },
|
|
33
|
+
{ pattern: /template\.HTMLEscapeString\(/, sanitizes: ['xss'] },
|
|
35
34
|
],
|
|
36
35
|
},
|
|
37
|
-
//
|
|
36
|
+
// database/sql (Standard Library Database)
|
|
37
|
+
{
|
|
38
|
+
name: 'database/sql',
|
|
39
|
+
languages: ['go'],
|
|
40
|
+
sources: [],
|
|
41
|
+
sinks: [
|
|
42
|
+
{ pattern: /db\.Query\([^,)]*\+/, type: 'sql', vulnerabilityType: 'sql_injection', severity: 'critical' },
|
|
43
|
+
{ pattern: /db\.QueryRow\([^,)]*\+/, type: 'sql', vulnerabilityType: 'sql_injection', severity: 'critical' },
|
|
44
|
+
{ pattern: /db\.Exec\([^,)]*\+/, type: 'sql', vulnerabilityType: 'sql_injection', severity: 'critical' },
|
|
45
|
+
{ pattern: /db\.Prepare\([^)]*\+/, type: 'sql', vulnerabilityType: 'sql_injection', severity: 'critical' },
|
|
46
|
+
{ pattern: /fmt\.Sprintf\([^)]*SELECT/, type: 'sql', vulnerabilityType: 'sql_injection', severity: 'critical' },
|
|
47
|
+
],
|
|
48
|
+
sanitizers: [
|
|
49
|
+
{ pattern: /db\.Query\([^,]+,\s*\w+\)/, sanitizes: ['sql_injection'] },
|
|
50
|
+
{ pattern: /db\.QueryRow\([^,]+,\s*\w+\)/, sanitizes: ['sql_injection'] },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
// os/exec (Command Execution)
|
|
54
|
+
{
|
|
55
|
+
name: 'os/exec',
|
|
56
|
+
languages: ['go'],
|
|
57
|
+
sources: [
|
|
58
|
+
{ pattern: /os\.Args/, type: 'user_input', description: 'Command line arguments', taintLabel: 'user_input' },
|
|
59
|
+
{ pattern: /os\.Getenv\(/, type: 'user_input', description: 'Environment variable', taintLabel: 'env_input' },
|
|
60
|
+
],
|
|
61
|
+
sinks: [
|
|
62
|
+
{ pattern: /exec\.Command\(/, type: 'command', vulnerabilityType: 'command_injection', severity: 'critical' },
|
|
63
|
+
{ pattern: /exec\.CommandContext\(/, type: 'command', vulnerabilityType: 'command_injection', severity: 'critical' },
|
|
64
|
+
],
|
|
65
|
+
sanitizers: [],
|
|
66
|
+
},
|
|
67
|
+
// os (File Operations)
|
|
68
|
+
{
|
|
69
|
+
name: 'os',
|
|
70
|
+
languages: ['go'],
|
|
71
|
+
sources: [],
|
|
72
|
+
sinks: [
|
|
73
|
+
{ pattern: /os\.Open\(/, type: 'file', vulnerabilityType: 'path_traversal', severity: 'high' },
|
|
74
|
+
{ pattern: /os\.OpenFile\(/, type: 'file', vulnerabilityType: 'path_traversal', severity: 'high' },
|
|
75
|
+
{ pattern: /os\.Create\(/, type: 'file', vulnerabilityType: 'path_traversal', severity: 'high' },
|
|
76
|
+
{ pattern: /os\.ReadFile\(/, type: 'file', vulnerabilityType: 'path_traversal', severity: 'high' },
|
|
77
|
+
{ pattern: /os\.WriteFile\(/, type: 'file', vulnerabilityType: 'path_traversal', severity: 'high' },
|
|
78
|
+
{ pattern: /ioutil\.ReadFile\(/, type: 'file', vulnerabilityType: 'path_traversal', severity: 'high' },
|
|
79
|
+
],
|
|
80
|
+
sanitizers: [
|
|
81
|
+
{ pattern: /filepath\.Clean\(/, sanitizes: ['path_traversal'] },
|
|
82
|
+
{ pattern: /filepath\.Base\(/, sanitizes: ['path_traversal'] },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
// encoding/xml (XML Processing)
|
|
86
|
+
{
|
|
87
|
+
name: 'encoding/xml',
|
|
88
|
+
languages: ['go'],
|
|
89
|
+
sources: [],
|
|
90
|
+
sinks: [
|
|
91
|
+
{ pattern: /xml\.Unmarshal\(/, type: 'xml', vulnerabilityType: 'xxe', severity: 'high' },
|
|
92
|
+
{ pattern: /xml\.NewDecoder\(/, type: 'xml', vulnerabilityType: 'xxe', severity: 'high' },
|
|
93
|
+
],
|
|
94
|
+
sanitizers: [],
|
|
95
|
+
},
|
|
96
|
+
// Gin Framework
|
|
38
97
|
{
|
|
39
|
-
id: 'go-gin',
|
|
40
98
|
name: 'Gin',
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
{ pattern:
|
|
44
|
-
{ pattern:
|
|
45
|
-
{ pattern:
|
|
46
|
-
{ pattern:
|
|
47
|
-
{ pattern:
|
|
48
|
-
{ pattern:
|
|
99
|
+
languages: ['go'],
|
|
100
|
+
sources: [
|
|
101
|
+
{ pattern: /c\.Query\(/, type: 'user_input', description: 'Gin query parameter', taintLabel: 'user_input' },
|
|
102
|
+
{ pattern: /c\.Param\(/, type: 'user_input', description: 'Gin path parameter', taintLabel: 'user_input' },
|
|
103
|
+
{ pattern: /c\.PostForm\(/, type: 'user_input', description: 'Gin POST form', taintLabel: 'user_input' },
|
|
104
|
+
{ pattern: /c\.ShouldBindJSON\(/, type: 'user_input', description: 'Gin JSON binding', taintLabel: 'user_input' },
|
|
105
|
+
{ pattern: /c\.GetHeader\(/, type: 'user_input', description: 'Gin header access', taintLabel: 'user_input' },
|
|
106
|
+
{ pattern: /c\.Cookie\(/, type: 'user_input', description: 'Gin cookie access', taintLabel: 'user_input' },
|
|
49
107
|
],
|
|
50
|
-
|
|
51
|
-
{ pattern:
|
|
52
|
-
{ pattern:
|
|
53
|
-
{ pattern:
|
|
108
|
+
sinks: [
|
|
109
|
+
{ pattern: /c\.HTML\(/, type: 'xss', vulnerabilityType: 'xss', severity: 'high' },
|
|
110
|
+
{ pattern: /c\.String\(/, type: 'xss', vulnerabilityType: 'xss', severity: 'medium' },
|
|
111
|
+
{ pattern: /c\.Redirect\(/, type: 'redirect', vulnerabilityType: 'open_redirect', severity: 'medium' },
|
|
54
112
|
],
|
|
55
|
-
|
|
113
|
+
sanitizers: [],
|
|
56
114
|
},
|
|
57
|
-
//
|
|
115
|
+
// Echo Framework
|
|
58
116
|
{
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
{ pattern: '
|
|
65
|
-
{ pattern: '
|
|
66
|
-
{ pattern: '
|
|
67
|
-
{ pattern: 'tx.Query', type: 'sql_injection' },
|
|
68
|
-
{ pattern: 'tx.Exec', type: 'sql_injection' },
|
|
117
|
+
name: 'Echo',
|
|
118
|
+
languages: ['go'],
|
|
119
|
+
sources: [
|
|
120
|
+
{ pattern: /c\.QueryParam\(/, type: 'user_input', description: 'Echo query parameter', taintLabel: 'user_input' },
|
|
121
|
+
{ pattern: /c\.Param\(/, type: 'user_input', description: 'Echo path parameter', taintLabel: 'user_input' },
|
|
122
|
+
{ pattern: /c\.FormValue\(/, type: 'user_input', description: 'Echo form value', taintLabel: 'user_input' },
|
|
123
|
+
{ pattern: /c\.Bind\(/, type: 'user_input', description: 'Echo request binding', taintLabel: 'user_input' },
|
|
124
|
+
{ pattern: /c\.Request\(\)\.Header\.Get\(/, type: 'user_input', description: 'Echo header access', taintLabel: 'user_input' },
|
|
69
125
|
],
|
|
70
|
-
|
|
71
|
-
{ pattern: '
|
|
126
|
+
sinks: [
|
|
127
|
+
{ pattern: /c\.HTML\(/, type: 'xss', vulnerabilityType: 'xss', severity: 'high' },
|
|
128
|
+
{ pattern: /c\.String\(/, type: 'xss', vulnerabilityType: 'xss', severity: 'medium' },
|
|
129
|
+
{ pattern: /c\.Redirect\(/, type: 'redirect', vulnerabilityType: 'open_redirect', severity: 'medium' },
|
|
72
130
|
],
|
|
131
|
+
sanitizers: [],
|
|
73
132
|
},
|
|
74
|
-
//
|
|
133
|
+
// Fiber Framework
|
|
75
134
|
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
{ pattern: '
|
|
82
|
-
{ pattern: '
|
|
135
|
+
name: 'Fiber',
|
|
136
|
+
languages: ['go'],
|
|
137
|
+
sources: [
|
|
138
|
+
{ pattern: /c\.Query\(/, type: 'user_input', description: 'Fiber query parameter', taintLabel: 'user_input' },
|
|
139
|
+
{ pattern: /c\.Params\(/, type: 'user_input', description: 'Fiber path parameter', taintLabel: 'user_input' },
|
|
140
|
+
{ pattern: /c\.FormValue\(/, type: 'user_input', description: 'Fiber form value', taintLabel: 'user_input' },
|
|
141
|
+
{ pattern: /c\.BodyParser\(/, type: 'user_input', description: 'Fiber body parser', taintLabel: 'user_input' },
|
|
142
|
+
{ pattern: /c\.Get\(/, type: 'user_input', description: 'Fiber header access', taintLabel: 'user_input' },
|
|
143
|
+
],
|
|
144
|
+
sinks: [
|
|
145
|
+
{ pattern: /c\.SendString\(/, type: 'xss', vulnerabilityType: 'xss', severity: 'medium' },
|
|
146
|
+
{ pattern: /c\.Redirect\(/, type: 'redirect', vulnerabilityType: 'open_redirect', severity: 'medium' },
|
|
83
147
|
],
|
|
84
|
-
|
|
148
|
+
sanitizers: [],
|
|
85
149
|
},
|
|
86
|
-
//
|
|
150
|
+
// GORM ORM
|
|
87
151
|
{
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
{ pattern: '
|
|
94
|
-
{ pattern: '
|
|
95
|
-
{ pattern: 'os.ReadFile', type: 'path_traversal' },
|
|
96
|
-
{ pattern: 'os.WriteFile', type: 'path_traversal' },
|
|
97
|
-
{ pattern: 'ioutil.ReadFile', type: 'path_traversal' },
|
|
98
|
-
{ pattern: 'ioutil.WriteFile', type: 'path_traversal' },
|
|
152
|
+
name: 'GORM',
|
|
153
|
+
languages: ['go'],
|
|
154
|
+
sources: [],
|
|
155
|
+
sinks: [
|
|
156
|
+
{ pattern: /db\.Raw\(/, type: 'sql', vulnerabilityType: 'sql_injection', severity: 'critical' },
|
|
157
|
+
{ pattern: /db\.Exec\(/, type: 'sql', vulnerabilityType: 'sql_injection', severity: 'critical' },
|
|
158
|
+
{ pattern: /db\.Where\([^,)]*\+/, type: 'sql', vulnerabilityType: 'sql_injection', severity: 'critical' },
|
|
99
159
|
],
|
|
100
|
-
|
|
101
|
-
{ pattern:
|
|
102
|
-
|
|
160
|
+
sanitizers: [
|
|
161
|
+
{ pattern: /db\.Where\([^,]+,\s*\w+\)/, sanitizes: ['sql_injection'] },
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
// Go SSRF vulnerabilities
|
|
165
|
+
{
|
|
166
|
+
name: 'Go SSRF',
|
|
167
|
+
languages: ['go'],
|
|
168
|
+
sources: [],
|
|
169
|
+
sinks: [
|
|
170
|
+
{ pattern: /http\.Get\(/, type: 'ssrf', vulnerabilityType: 'ssrf', severity: 'high' },
|
|
171
|
+
{ pattern: /http\.Post\(/, type: 'ssrf', vulnerabilityType: 'ssrf', severity: 'high' },
|
|
172
|
+
{ pattern: /http\.NewRequest\(/, type: 'ssrf', vulnerabilityType: 'ssrf', severity: 'high' },
|
|
173
|
+
{ pattern: /client\.Do\(/, type: 'ssrf', vulnerabilityType: 'ssrf', severity: 'high' },
|
|
174
|
+
],
|
|
175
|
+
sanitizers: [
|
|
176
|
+
{ pattern: /url\.Parse\(/, sanitizes: ['ssrf'] },
|
|
103
177
|
],
|
|
104
178
|
},
|
|
105
179
|
];
|
|
106
180
|
/**
|
|
107
|
-
* Go
|
|
181
|
+
* Check if a Go identifier is exported (starts with uppercase)
|
|
182
|
+
* @param name The identifier name to check
|
|
183
|
+
* @returns true if exported, false otherwise
|
|
184
|
+
*/
|
|
185
|
+
function isExported(name) {
|
|
186
|
+
if (!name || name.length === 0)
|
|
187
|
+
return false;
|
|
188
|
+
const firstChar = name.charAt(0);
|
|
189
|
+
return firstChar >= 'A' && firstChar <= 'Z';
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Go Language Extractor
|
|
193
|
+
* @trace TSK-GO-001, REQ-SEC-GO-001
|
|
108
194
|
*/
|
|
109
195
|
export class GoExtractor extends BaseExtractor {
|
|
196
|
+
language = 'go';
|
|
197
|
+
extensions = ['.go'];
|
|
110
198
|
parser = null;
|
|
111
199
|
tree = null;
|
|
112
200
|
nodeIdCounter = 0;
|
|
113
201
|
blockIdCounter = 0;
|
|
114
202
|
/**
|
|
115
|
-
*
|
|
203
|
+
* Get framework models for Go
|
|
204
|
+
* @trace REQ-SEC-GO-004
|
|
116
205
|
*/
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
this.frameworkModels = GO_FRAMEWORK_PATTERNS;
|
|
206
|
+
getFrameworkModels() {
|
|
207
|
+
return GO_FRAMEWORK_MODELS;
|
|
120
208
|
}
|
|
121
209
|
/**
|
|
122
210
|
* Initialize tree-sitter parser
|
|
211
|
+
* @trace REQ-SEC-GO-002
|
|
123
212
|
*/
|
|
124
213
|
async initParser() {
|
|
125
214
|
if (this.parser)
|
|
126
215
|
return;
|
|
127
216
|
try {
|
|
128
|
-
// Dynamic import for tree-sitter
|
|
129
217
|
const Parser = (await import('tree-sitter')).default;
|
|
130
218
|
const Go = (await import('tree-sitter-go')).default;
|
|
131
219
|
this.parser = new Parser();
|
|
132
220
|
this.parser.setLanguage(Go);
|
|
133
221
|
}
|
|
134
222
|
catch {
|
|
135
|
-
//
|
|
223
|
+
// tree-sitter-go not available, use fallback
|
|
136
224
|
this.parser = null;
|
|
137
225
|
}
|
|
138
226
|
}
|
|
139
227
|
/**
|
|
140
|
-
*
|
|
228
|
+
* Build AST from source code
|
|
229
|
+
* @trace TSK-GO-002, REQ-SEC-GO-002
|
|
141
230
|
*/
|
|
142
|
-
async
|
|
231
|
+
async buildAST(source, filePath) {
|
|
143
232
|
await this.initParser();
|
|
233
|
+
this.nodeIdCounter = 0;
|
|
234
|
+
const astNodes = new Map();
|
|
235
|
+
const astEdges = [];
|
|
236
|
+
let ast;
|
|
144
237
|
if (this.parser) {
|
|
145
|
-
this.tree = this.parser.parse(
|
|
146
|
-
|
|
238
|
+
this.tree = this.parser.parse(source);
|
|
239
|
+
ast = this.convertTreeSitterNode(this.tree.rootNode, filePath, astNodes, astEdges);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
ast = this.createFallbackAST(source, filePath, astNodes);
|
|
147
243
|
}
|
|
148
|
-
|
|
149
|
-
return this.createFallbackAST(code, filePath);
|
|
244
|
+
return { ast, astNodes, astEdges };
|
|
150
245
|
}
|
|
151
246
|
/**
|
|
152
|
-
* Convert tree-sitter node to
|
|
247
|
+
* Convert tree-sitter node to AST format
|
|
153
248
|
*/
|
|
154
|
-
convertTreeSitterNode(node, filePath) {
|
|
249
|
+
convertTreeSitterNode(node, filePath, astNodes, astEdges) {
|
|
155
250
|
const id = `${filePath}#${this.nodeIdCounter++}`;
|
|
156
|
-
const
|
|
251
|
+
const childIds = [];
|
|
252
|
+
for (const child of node.namedChildren) {
|
|
253
|
+
const childNode = this.convertTreeSitterNode(child, filePath, astNodes, astEdges);
|
|
254
|
+
childIds.push(childNode.id);
|
|
255
|
+
astEdges.push({
|
|
256
|
+
from: id,
|
|
257
|
+
to: childNode.id,
|
|
258
|
+
label: 'child',
|
|
259
|
+
});
|
|
260
|
+
}
|
|
157
261
|
const astNode = {
|
|
158
262
|
id,
|
|
159
263
|
type: node.type,
|
|
@@ -165,641 +269,485 @@ export class GoExtractor extends BaseExtractor {
|
|
|
165
269
|
startColumn: node.startPosition.column,
|
|
166
270
|
endColumn: node.endPosition.column,
|
|
167
271
|
},
|
|
168
|
-
|
|
169
|
-
|
|
272
|
+
properties: this.extractNodeProperties(node),
|
|
273
|
+
children: childIds,
|
|
274
|
+
metadata: {},
|
|
170
275
|
};
|
|
171
|
-
|
|
172
|
-
for (const child of children) {
|
|
173
|
-
child.parent = astNode;
|
|
174
|
-
}
|
|
276
|
+
astNodes.set(id, astNode);
|
|
175
277
|
return astNode;
|
|
176
278
|
}
|
|
177
279
|
/**
|
|
178
|
-
* Extract
|
|
280
|
+
* Extract properties from node
|
|
281
|
+
* @trace REQ-SEC-GO-003
|
|
179
282
|
*/
|
|
180
|
-
|
|
181
|
-
const
|
|
283
|
+
extractNodeProperties(node) {
|
|
284
|
+
const props = {};
|
|
182
285
|
switch (node.type) {
|
|
183
|
-
case 'function_declaration':
|
|
286
|
+
case 'function_declaration': {
|
|
287
|
+
const nameNode = node.childForFieldName('name');
|
|
288
|
+
const paramsNode = node.childForFieldName('parameters');
|
|
289
|
+
const resultNode = node.childForFieldName('result');
|
|
290
|
+
const name = nameNode?.text;
|
|
291
|
+
props.name = name;
|
|
292
|
+
props.parameters = paramsNode?.namedChildren.map((p) => p.text) ?? [];
|
|
293
|
+
props.returnType = resultNode?.text;
|
|
294
|
+
props.isExported = name ? isExported(name) : false;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
184
297
|
case 'method_declaration': {
|
|
185
298
|
const nameNode = node.childForFieldName('name');
|
|
299
|
+
const receiverNode = node.childForFieldName('receiver');
|
|
186
300
|
const paramsNode = node.childForFieldName('parameters');
|
|
187
301
|
const resultNode = node.childForFieldName('result');
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
302
|
+
const name = nameNode?.text;
|
|
303
|
+
props.name = name;
|
|
304
|
+
props.receiver = receiverNode?.text;
|
|
305
|
+
props.parameters = paramsNode?.namedChildren.map((p) => p.text) ?? [];
|
|
306
|
+
props.returnType = resultNode?.text;
|
|
307
|
+
props.isExported = name ? isExported(name) : false;
|
|
195
308
|
break;
|
|
196
309
|
}
|
|
197
310
|
case 'type_declaration': {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
311
|
+
const specNode = node.namedChildren[0];
|
|
312
|
+
if (specNode?.type === 'type_spec') {
|
|
313
|
+
const nameNode = specNode.childForFieldName('name');
|
|
314
|
+
const typeNode = specNode.childForFieldName('type');
|
|
315
|
+
const name = nameNode?.text;
|
|
316
|
+
props.name = name;
|
|
317
|
+
props.underlyingType = typeNode?.type;
|
|
318
|
+
props.isExported = name ? isExported(name) : false;
|
|
319
|
+
}
|
|
203
320
|
break;
|
|
204
321
|
}
|
|
205
|
-
case '
|
|
206
|
-
const
|
|
207
|
-
|
|
322
|
+
case 'struct_type': {
|
|
323
|
+
const fields = [];
|
|
324
|
+
for (const fieldDecl of node.namedChildren) {
|
|
325
|
+
if (fieldDecl.type === 'field_declaration') {
|
|
326
|
+
const fieldNameNode = fieldDecl.childForFieldName('name');
|
|
327
|
+
const fieldTypeNode = fieldDecl.childForFieldName('type');
|
|
328
|
+
const tagNode = fieldDecl.namedChildren.find((n) => n.type === 'raw_string_literal');
|
|
329
|
+
fields.push({
|
|
330
|
+
name: fieldNameNode?.text ?? '',
|
|
331
|
+
type: fieldTypeNode?.text ?? '',
|
|
332
|
+
tag: tagNode?.text,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
props.fields = fields;
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
case 'interface_type': {
|
|
340
|
+
const methods = [];
|
|
341
|
+
for (const methodSpec of node.namedChildren) {
|
|
342
|
+
if (methodSpec.type === 'method_spec') {
|
|
343
|
+
const methodNameNode = methodSpec.childForFieldName('name');
|
|
344
|
+
if (methodNameNode) {
|
|
345
|
+
methods.push(methodNameNode.text);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
props.methods = methods;
|
|
208
350
|
break;
|
|
209
351
|
}
|
|
210
352
|
case 'call_expression': {
|
|
211
|
-
const
|
|
353
|
+
const functionNode = node.childForFieldName('function');
|
|
212
354
|
const argsNode = node.childForFieldName('arguments');
|
|
213
|
-
|
|
214
|
-
|
|
355
|
+
props.functionName = functionNode?.text;
|
|
356
|
+
props.argumentCount = argsNode?.namedChildCount ?? 0;
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
case 'selector_expression': {
|
|
360
|
+
const operandNode = node.childForFieldName('operand');
|
|
361
|
+
const fieldNode = node.childForFieldName('field');
|
|
362
|
+
props.operand = operandNode?.text;
|
|
363
|
+
props.field = fieldNode?.text;
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
case 'short_var_declaration':
|
|
367
|
+
case 'var_declaration':
|
|
368
|
+
case 'assignment_statement': {
|
|
369
|
+
const leftNode = node.childForFieldName('left');
|
|
370
|
+
const rightNode = node.childForFieldName('right');
|
|
371
|
+
props.left = leftNode?.text;
|
|
372
|
+
props.right = rightNode?.text;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
case 'package_clause': {
|
|
376
|
+
const pkgNameNode = node.childForFieldName('name');
|
|
377
|
+
props.packageName = pkgNameNode?.text;
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
case 'import_declaration': {
|
|
381
|
+
const specs = [];
|
|
382
|
+
for (const child of node.namedChildren) {
|
|
383
|
+
if (child.type === 'import_spec' || child.type === 'import_spec_list') {
|
|
384
|
+
const pathNode = child.childForFieldName('path') ?? child;
|
|
385
|
+
specs.push(pathNode.text.replace(/"/g, ''));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
props.imports = specs;
|
|
215
389
|
break;
|
|
216
390
|
}
|
|
217
391
|
case 'if_statement':
|
|
392
|
+
props.hasElse = node.namedChildren.some((n) => n.type === 'block' && n !== node.namedChildren[0]);
|
|
393
|
+
break;
|
|
218
394
|
case 'for_statement':
|
|
395
|
+
props.hasRange = node.text.includes('range');
|
|
396
|
+
break;
|
|
219
397
|
case 'switch_statement':
|
|
220
|
-
|
|
398
|
+
case 'type_switch_statement':
|
|
399
|
+
props.caseCount = node.namedChildren.filter((n) => n.type === 'expression_case' || n.type === 'type_case').length;
|
|
221
400
|
break;
|
|
222
401
|
case 'go_statement':
|
|
223
|
-
|
|
402
|
+
props.isGoroutine = true;
|
|
224
403
|
break;
|
|
225
404
|
case 'defer_statement':
|
|
226
|
-
|
|
405
|
+
props.isDeferred = true;
|
|
406
|
+
break;
|
|
407
|
+
case 'select_statement':
|
|
408
|
+
props.caseCount = node.namedChildren.filter((n) => n.type === 'communication_case').length;
|
|
227
409
|
break;
|
|
228
410
|
}
|
|
229
|
-
return
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* Extract function parameters
|
|
233
|
-
*/
|
|
234
|
-
extractParameters(paramsNode) {
|
|
235
|
-
if (!paramsNode)
|
|
236
|
-
return [];
|
|
237
|
-
const params = [];
|
|
238
|
-
const paramDecls = paramsNode.namedChildren.filter((c) => c.type === 'parameter_declaration');
|
|
239
|
-
for (const decl of paramDecls) {
|
|
240
|
-
const names = decl.childrenForFieldName('name');
|
|
241
|
-
const typeNode = decl.childForFieldName('type');
|
|
242
|
-
const typeName = typeNode?.text ?? 'unknown';
|
|
243
|
-
for (const nameNode of names) {
|
|
244
|
-
params.push({ name: nameNode.text, type: typeName });
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return params;
|
|
248
|
-
}
|
|
249
|
-
/**
|
|
250
|
-
* Extract import specifications
|
|
251
|
-
*/
|
|
252
|
-
extractImports(specs) {
|
|
253
|
-
const imports = [];
|
|
254
|
-
for (const spec of specs) {
|
|
255
|
-
if (spec.type === 'import_spec') {
|
|
256
|
-
const pathNode = spec.childForFieldName('path');
|
|
257
|
-
const aliasNode = spec.childForFieldName('name');
|
|
258
|
-
imports.push({
|
|
259
|
-
path: pathNode?.text?.replace(/"/g, '') ?? '',
|
|
260
|
-
alias: aliasNode?.text,
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
else if (spec.type === 'import_spec_list') {
|
|
264
|
-
imports.push(...this.extractImports(spec.namedChildren));
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return imports;
|
|
411
|
+
return props;
|
|
268
412
|
}
|
|
269
413
|
/**
|
|
270
|
-
*
|
|
414
|
+
* Create fallback AST when tree-sitter is unavailable
|
|
271
415
|
*/
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
'
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
statements: [],
|
|
290
|
-
predecessors: [],
|
|
291
|
-
successors: blocks.filter((b) => b.predecessors.length === 0).map((b) => b.id),
|
|
292
|
-
};
|
|
293
|
-
const exit = {
|
|
294
|
-
id: `${ast.location.file}#exit`,
|
|
295
|
-
statements: [],
|
|
296
|
-
predecessors: blocks.filter((b) => b.successors.length === 0).map((b) => b.id),
|
|
297
|
-
successors: [],
|
|
298
|
-
};
|
|
299
|
-
return {
|
|
300
|
-
entry: entry.id,
|
|
301
|
-
exit: exit.id,
|
|
302
|
-
blocks: [entry, ...blocks, exit],
|
|
303
|
-
edges,
|
|
416
|
+
createFallbackAST(source, filePath, astNodes) {
|
|
417
|
+
const lines = source.split('\n');
|
|
418
|
+
const id = `${filePath}#root`;
|
|
419
|
+
const ast = {
|
|
420
|
+
id,
|
|
421
|
+
type: 'source_file',
|
|
422
|
+
text: source,
|
|
423
|
+
location: {
|
|
424
|
+
file: filePath,
|
|
425
|
+
startLine: 1,
|
|
426
|
+
endLine: lines.length,
|
|
427
|
+
startColumn: 0,
|
|
428
|
+
endColumn: lines[lines.length - 1]?.length ?? 0,
|
|
429
|
+
},
|
|
430
|
+
properties: { fallback: true, lineCount: lines.length },
|
|
431
|
+
children: [],
|
|
432
|
+
metadata: {},
|
|
304
433
|
};
|
|
434
|
+
astNodes.set(id, ast);
|
|
435
|
+
return ast;
|
|
305
436
|
}
|
|
306
437
|
/**
|
|
307
|
-
* Build
|
|
308
|
-
|
|
309
|
-
buildFunctionCFG(func) {
|
|
310
|
-
const blocks = [];
|
|
311
|
-
const edges = [];
|
|
312
|
-
const bodyNode = func.children.find((c) => c.type === 'block');
|
|
313
|
-
if (!bodyNode) {
|
|
314
|
-
return { blocks, edges };
|
|
315
|
-
}
|
|
316
|
-
// Process function body
|
|
317
|
-
const { blocks: bodyBlocks, edges: bodyEdges } = this.processBlock(bodyNode);
|
|
318
|
-
blocks.push(...bodyBlocks);
|
|
319
|
-
edges.push(...bodyEdges);
|
|
320
|
-
return { blocks, edges };
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Process a block of statements
|
|
438
|
+
* Build Data Flow Graph
|
|
439
|
+
* @trace TSK-GO-003, REQ-SEC-GO-005
|
|
324
440
|
*/
|
|
325
|
-
|
|
326
|
-
const
|
|
441
|
+
async buildDFG(astNodes, _astEdges, frameworkModels) {
|
|
442
|
+
const nodes = new Map();
|
|
327
443
|
const edges = [];
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
statements: [],
|
|
348
|
-
predecessors: [],
|
|
349
|
-
successors: [],
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
currentBlock.statements.push(stmt.id);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
// Add final block
|
|
356
|
-
if (currentBlock && currentBlock.statements.length > 0) {
|
|
357
|
-
blocks.push(currentBlock);
|
|
358
|
-
}
|
|
359
|
-
// Connect sequential blocks
|
|
360
|
-
for (let i = 0; i < blocks.length - 1; i++) {
|
|
361
|
-
if (blocks[i].successors.length === 0) {
|
|
362
|
-
blocks[i].successors.push(blocks[i + 1].id);
|
|
363
|
-
blocks[i + 1].predecessors.push(blocks[i].id);
|
|
364
|
-
edges.push({
|
|
365
|
-
from: blocks[i].id,
|
|
366
|
-
to: blocks[i + 1].id,
|
|
367
|
-
type: 'sequential',
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return { blocks, edges };
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
|
-
* Check if statement is control flow
|
|
375
|
-
*/
|
|
376
|
-
isControlFlowStatement(stmt) {
|
|
377
|
-
return [
|
|
378
|
-
'if_statement',
|
|
379
|
-
'for_statement',
|
|
380
|
-
'switch_statement',
|
|
381
|
-
'select_statement',
|
|
382
|
-
'return_statement',
|
|
383
|
-
'go_statement',
|
|
384
|
-
'defer_statement',
|
|
385
|
-
].includes(stmt.type);
|
|
386
|
-
}
|
|
387
|
-
/**
|
|
388
|
-
* Process control flow statement
|
|
389
|
-
*/
|
|
390
|
-
processControlFlow(stmt) {
|
|
391
|
-
const blocks = [];
|
|
392
|
-
const edges = [];
|
|
393
|
-
switch (stmt.type) {
|
|
394
|
-
case 'if_statement': {
|
|
395
|
-
// Create condition block
|
|
396
|
-
const condBlock = {
|
|
397
|
-
id: `${stmt.location.file}#if_cond_${this.blockIdCounter++}`,
|
|
398
|
-
statements: [stmt.id],
|
|
399
|
-
predecessors: [],
|
|
400
|
-
successors: [],
|
|
401
|
-
};
|
|
402
|
-
blocks.push(condBlock);
|
|
403
|
-
// Process then branch
|
|
404
|
-
const thenBlock = stmt.children.find((c) => c.type === 'block');
|
|
405
|
-
if (thenBlock) {
|
|
406
|
-
const { blocks: thenBlocks, edges: thenEdges } = this.processBlock(thenBlock);
|
|
407
|
-
blocks.push(...thenBlocks);
|
|
408
|
-
edges.push(...thenEdges);
|
|
409
|
-
if (thenBlocks.length > 0) {
|
|
410
|
-
condBlock.successors.push(thenBlocks[0].id);
|
|
411
|
-
thenBlocks[0].predecessors.push(condBlock.id);
|
|
412
|
-
edges.push({
|
|
413
|
-
from: condBlock.id,
|
|
414
|
-
to: thenBlocks[0].id,
|
|
415
|
-
type: 'conditional',
|
|
416
|
-
condition: 'true',
|
|
444
|
+
const sources = [];
|
|
445
|
+
const sinks = [];
|
|
446
|
+
const models = frameworkModels.length > 0 ? frameworkModels : GO_FRAMEWORK_MODELS;
|
|
447
|
+
for (const [_nodeId, astNode] of astNodes) {
|
|
448
|
+
if (!astNode.text)
|
|
449
|
+
continue;
|
|
450
|
+
// Check for sources
|
|
451
|
+
for (const model of models) {
|
|
452
|
+
for (const source of model.sources) {
|
|
453
|
+
if (source.pattern.test(astNode.text)) {
|
|
454
|
+
const dfgId = `dfg_source_${astNode.id}`;
|
|
455
|
+
nodes.set(dfgId, {
|
|
456
|
+
id: dfgId,
|
|
457
|
+
astNodeId: astNode.id,
|
|
458
|
+
nodeType: 'source',
|
|
459
|
+
taintLabel: source.taintLabel,
|
|
460
|
+
expression: astNode.text,
|
|
461
|
+
location: astNode.location,
|
|
462
|
+
properties: { sourceType: source.type, framework: model.name },
|
|
417
463
|
});
|
|
464
|
+
sources.push(dfgId);
|
|
418
465
|
}
|
|
419
466
|
}
|
|
420
|
-
//
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
467
|
+
// Check for sinks
|
|
468
|
+
for (const sink of model.sinks) {
|
|
469
|
+
if (sink.pattern.test(astNode.text)) {
|
|
470
|
+
const dfgId = `dfg_sink_${astNode.id}`;
|
|
471
|
+
nodes.set(dfgId, {
|
|
472
|
+
id: dfgId,
|
|
473
|
+
astNodeId: astNode.id,
|
|
474
|
+
nodeType: 'sink',
|
|
475
|
+
expression: astNode.text,
|
|
476
|
+
location: astNode.location,
|
|
477
|
+
properties: {
|
|
478
|
+
sinkType: sink.type,
|
|
479
|
+
vulnerabilityType: sink.vulnerabilityType,
|
|
480
|
+
severity: sink.severity,
|
|
481
|
+
framework: model.name,
|
|
482
|
+
},
|
|
434
483
|
});
|
|
484
|
+
sinks.push(dfgId);
|
|
435
485
|
}
|
|
436
486
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
// Process loop body
|
|
449
|
-
const bodyBlock = stmt.children.find((c) => c.type === 'block');
|
|
450
|
-
if (bodyBlock) {
|
|
451
|
-
const { blocks: bodyBlocks, edges: bodyEdges } = this.processBlock(bodyBlock);
|
|
452
|
-
blocks.push(...bodyBlocks);
|
|
453
|
-
edges.push(...bodyEdges);
|
|
454
|
-
if (bodyBlocks.length > 0) {
|
|
455
|
-
// Entry to body
|
|
456
|
-
headerBlock.successors.push(bodyBlocks[0].id);
|
|
457
|
-
bodyBlocks[0].predecessors.push(headerBlock.id);
|
|
458
|
-
edges.push({
|
|
459
|
-
from: headerBlock.id,
|
|
460
|
-
to: bodyBlocks[0].id,
|
|
461
|
-
type: 'conditional',
|
|
462
|
-
condition: 'true',
|
|
463
|
-
});
|
|
464
|
-
// Back edge
|
|
465
|
-
const lastBody = bodyBlocks[bodyBlocks.length - 1];
|
|
466
|
-
lastBody.successors.push(headerBlock.id);
|
|
467
|
-
headerBlock.predecessors.push(lastBody.id);
|
|
468
|
-
edges.push({
|
|
469
|
-
from: lastBody.id,
|
|
470
|
-
to: headerBlock.id,
|
|
471
|
-
type: 'back',
|
|
487
|
+
// Check for sanitizers
|
|
488
|
+
for (const sanitizer of model.sanitizers) {
|
|
489
|
+
if (sanitizer.pattern.test(astNode.text)) {
|
|
490
|
+
const dfgId = `dfg_sanitizer_${astNode.id}`;
|
|
491
|
+
nodes.set(dfgId, {
|
|
492
|
+
id: dfgId,
|
|
493
|
+
astNodeId: astNode.id,
|
|
494
|
+
nodeType: 'sanitizer',
|
|
495
|
+
expression: astNode.text,
|
|
496
|
+
location: astNode.location,
|
|
497
|
+
properties: { sanitizes: sanitizer.sanitizes, framework: model.name },
|
|
472
498
|
});
|
|
473
499
|
}
|
|
474
500
|
}
|
|
475
|
-
break;
|
|
476
501
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
default: {
|
|
488
|
-
const block = {
|
|
489
|
-
id: `${stmt.location.file}#stmt_${this.blockIdCounter++}`,
|
|
490
|
-
statements: [stmt.id],
|
|
491
|
-
predecessors: [],
|
|
492
|
-
successors: [],
|
|
493
|
-
};
|
|
494
|
-
blocks.push(block);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
return { blocks, edges };
|
|
498
|
-
}
|
|
499
|
-
/**
|
|
500
|
-
* Build data flow graph
|
|
501
|
-
*/
|
|
502
|
-
async buildDFG(ast, cfg) {
|
|
503
|
-
const nodes = [];
|
|
504
|
-
const edges = [];
|
|
505
|
-
const nodeMap = new Map();
|
|
506
|
-
// Process all assignments and declarations
|
|
507
|
-
const assignments = this.findNodesByType(ast, [
|
|
508
|
-
'short_var_declaration',
|
|
509
|
-
'var_declaration',
|
|
510
|
-
'const_declaration',
|
|
511
|
-
'assignment_statement',
|
|
512
|
-
]);
|
|
513
|
-
for (const assign of assignments) {
|
|
514
|
-
const dfgNodes = this.createDFGNodesForAssignment(assign, nodeMap);
|
|
515
|
-
nodes.push(...dfgNodes);
|
|
516
|
-
}
|
|
517
|
-
// Process function parameters as sources
|
|
518
|
-
const functions = this.findNodesByType(ast, [
|
|
519
|
-
'function_declaration',
|
|
520
|
-
'method_declaration',
|
|
521
|
-
]);
|
|
522
|
-
for (const func of functions) {
|
|
523
|
-
const paramNodes = this.createDFGNodesForParameters(func, nodeMap);
|
|
524
|
-
nodes.push(...paramNodes);
|
|
525
|
-
}
|
|
526
|
-
// Build edges based on data dependencies
|
|
527
|
-
edges.push(...this.buildDFGEdges(nodes, nodeMap));
|
|
528
|
-
return {
|
|
529
|
-
nodes,
|
|
530
|
-
edges,
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
/**
|
|
534
|
-
* Create DFG nodes for assignment
|
|
535
|
-
*/
|
|
536
|
-
createDFGNodesForAssignment(assign, nodeMap) {
|
|
537
|
-
const nodes = [];
|
|
538
|
-
// Get left-hand side (defined variables)
|
|
539
|
-
const lhs = assign.children.filter((c) => ['identifier', 'expression_list'].includes(c.type));
|
|
540
|
-
for (const target of lhs) {
|
|
541
|
-
if (target.type === 'identifier') {
|
|
542
|
-
const node = {
|
|
543
|
-
id: `dfg_${target.id}`,
|
|
544
|
-
astNodeId: target.id,
|
|
545
|
-
variable: target.text,
|
|
502
|
+
// Handle variable declarations and assignments
|
|
503
|
+
if (astNode.type === 'short_var_declaration' ||
|
|
504
|
+
astNode.type === 'var_declaration' ||
|
|
505
|
+
astNode.type === 'assignment_statement') {
|
|
506
|
+
const dfgId = `dfg_assign_${astNode.id}`;
|
|
507
|
+
nodes.set(dfgId, {
|
|
508
|
+
id: dfgId,
|
|
509
|
+
astNodeId: astNode.id,
|
|
510
|
+
nodeType: 'propagator',
|
|
511
|
+
variable: astNode.properties.left,
|
|
546
512
|
operation: 'write',
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
};
|
|
551
|
-
nodes.push(node);
|
|
552
|
-
nodeMap.set(target.text, node);
|
|
513
|
+
expression: astNode.text,
|
|
514
|
+
location: astNode.location,
|
|
515
|
+
properties: {},
|
|
516
|
+
});
|
|
553
517
|
}
|
|
554
518
|
}
|
|
555
|
-
return nodes;
|
|
519
|
+
return { nodes, edges, sources, sinks };
|
|
556
520
|
}
|
|
557
521
|
/**
|
|
558
|
-
*
|
|
522
|
+
* Build Control Flow Graph
|
|
523
|
+
* @trace TSK-GO-004, REQ-SEC-GO-006
|
|
559
524
|
*/
|
|
560
|
-
|
|
561
|
-
const
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
525
|
+
async buildCFG(astNodes, _astEdges) {
|
|
526
|
+
const blocks = new Map();
|
|
527
|
+
const edges = [];
|
|
528
|
+
const entryBlocks = [];
|
|
529
|
+
const exitBlocks = [];
|
|
530
|
+
// Find functions and build CFG for each
|
|
531
|
+
for (const [_nodeId, astNode] of astNodes) {
|
|
532
|
+
if (astNode.type === 'function_declaration' || astNode.type === 'method_declaration') {
|
|
533
|
+
const blockId = `block_${this.blockIdCounter++}`;
|
|
534
|
+
blocks.set(blockId, {
|
|
535
|
+
id: blockId,
|
|
536
|
+
statements: [astNode.id],
|
|
571
537
|
predecessors: [],
|
|
572
538
|
successors: [],
|
|
573
|
-
taintLabel: this.inferTaintLabel(param.name, param.type),
|
|
574
|
-
};
|
|
575
|
-
nodes.push(node);
|
|
576
|
-
nodeMap.set(param.name, node);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
return nodes;
|
|
580
|
-
}
|
|
581
|
-
/**
|
|
582
|
-
* Infer taint label from parameter
|
|
583
|
-
*/
|
|
584
|
-
inferTaintLabel(name, type) {
|
|
585
|
-
// HTTP request parameters
|
|
586
|
-
if (name === 'r' && type.includes('Request'))
|
|
587
|
-
return 'user_input';
|
|
588
|
-
if (name === 'req' && type.includes('Request'))
|
|
589
|
-
return 'user_input';
|
|
590
|
-
if (name === 'c' && type.includes('Context'))
|
|
591
|
-
return 'user_input'; // Gin context
|
|
592
|
-
return undefined;
|
|
593
|
-
}
|
|
594
|
-
/**
|
|
595
|
-
* Build DFG edges
|
|
596
|
-
*/
|
|
597
|
-
buildDFGEdges(nodes, nodeMap) {
|
|
598
|
-
const edges = [];
|
|
599
|
-
// Connect reads to their definitions
|
|
600
|
-
for (const node of nodes) {
|
|
601
|
-
if (node.operation === 'read') {
|
|
602
|
-
const def = nodeMap.get(node.variable);
|
|
603
|
-
if (def && def !== node) {
|
|
604
|
-
edges.push({
|
|
605
|
-
from: def.id,
|
|
606
|
-
to: node.id,
|
|
607
|
-
type: 'data',
|
|
608
|
-
variable: node.variable,
|
|
609
|
-
});
|
|
610
|
-
def.successors.push(node.id);
|
|
611
|
-
node.predecessors.push(def.id);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
return edges;
|
|
616
|
-
}
|
|
617
|
-
/**
|
|
618
|
-
* Build symbol table
|
|
619
|
-
*/
|
|
620
|
-
async buildSymbolTable(ast) {
|
|
621
|
-
const global = new Map();
|
|
622
|
-
const scopes = [];
|
|
623
|
-
// Extract package name
|
|
624
|
-
const packageNode = this.findNodesByType(ast, ['package_clause'])[0];
|
|
625
|
-
const packageName = packageNode?.children[0]?.text ?? 'main';
|
|
626
|
-
// Process imports
|
|
627
|
-
const importNodes = this.findNodesByType(ast, ['import_declaration']);
|
|
628
|
-
for (const importDecl of importNodes) {
|
|
629
|
-
const imports = importDecl.metadata?.imports;
|
|
630
|
-
if (imports) {
|
|
631
|
-
for (const imp of imports) {
|
|
632
|
-
const alias = imp.alias ?? imp.path.split('/').pop() ?? imp.path;
|
|
633
|
-
global.set(alias, {
|
|
634
|
-
name: alias,
|
|
635
|
-
kind: 'import',
|
|
636
|
-
type: imp.path,
|
|
637
|
-
location: importDecl.location,
|
|
638
|
-
scope: 'global',
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
// Process type declarations
|
|
644
|
-
const typeDecls = this.findNodesByType(ast, ['type_declaration']);
|
|
645
|
-
for (const typeDecl of typeDecls) {
|
|
646
|
-
const types = typeDecl.metadata?.types;
|
|
647
|
-
if (types) {
|
|
648
|
-
for (const t of types) {
|
|
649
|
-
global.set(t.name, {
|
|
650
|
-
name: t.name,
|
|
651
|
-
kind: 'type',
|
|
652
|
-
type: t.kind,
|
|
653
|
-
location: typeDecl.location,
|
|
654
|
-
scope: 'global',
|
|
655
|
-
exported: this.isExported(t.name),
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
// Process function declarations
|
|
661
|
-
const funcDecls = this.findNodesByType(ast, [
|
|
662
|
-
'function_declaration',
|
|
663
|
-
'method_declaration',
|
|
664
|
-
]);
|
|
665
|
-
for (const funcDecl of funcDecls) {
|
|
666
|
-
const name = funcDecl.metadata?.name;
|
|
667
|
-
const params = funcDecl.metadata?.parameters;
|
|
668
|
-
const returnType = funcDecl.metadata?.returnType;
|
|
669
|
-
const receiver = funcDecl.metadata?.receiver;
|
|
670
|
-
if (name) {
|
|
671
|
-
const fullName = receiver ? `${receiver}.${name}` : name;
|
|
672
|
-
global.set(fullName, {
|
|
673
|
-
name: fullName,
|
|
674
|
-
kind: 'function',
|
|
675
|
-
type: `func(${params?.map((p) => p.type).join(', ') ?? ''}) ${returnType ?? ''}`,
|
|
676
|
-
location: funcDecl.location,
|
|
677
|
-
scope: 'global',
|
|
678
|
-
exported: this.isExported(name),
|
|
679
539
|
});
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
if (params) {
|
|
683
|
-
for (const param of params) {
|
|
684
|
-
funcScope.set(param.name, {
|
|
685
|
-
name: param.name,
|
|
686
|
-
kind: 'parameter',
|
|
687
|
-
type: param.type,
|
|
688
|
-
location: funcDecl.location,
|
|
689
|
-
scope: fullName,
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
scopes.push({ scopeId: fullName, symbols: funcScope });
|
|
540
|
+
entryBlocks.push(blockId);
|
|
541
|
+
exitBlocks.push(blockId);
|
|
694
542
|
}
|
|
695
543
|
}
|
|
696
|
-
//
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
}
|
|
544
|
+
// Add entry and exit blocks if none found
|
|
545
|
+
if (entryBlocks.length === 0) {
|
|
546
|
+
const entryId = 'entry_block';
|
|
547
|
+
const exitId = 'exit_block';
|
|
548
|
+
blocks.set(entryId, {
|
|
549
|
+
id: entryId,
|
|
550
|
+
statements: [],
|
|
551
|
+
predecessors: [],
|
|
552
|
+
successors: [exitId],
|
|
553
|
+
isEntry: true,
|
|
554
|
+
});
|
|
555
|
+
blocks.set(exitId, {
|
|
556
|
+
id: exitId,
|
|
557
|
+
statements: [],
|
|
558
|
+
predecessors: [entryId],
|
|
559
|
+
successors: [],
|
|
560
|
+
isExit: true,
|
|
561
|
+
});
|
|
562
|
+
entryBlocks.push(entryId);
|
|
563
|
+
exitBlocks.push(exitId);
|
|
564
|
+
edges.push({
|
|
565
|
+
from: entryId,
|
|
566
|
+
to: exitId,
|
|
567
|
+
edgeType: 'normal',
|
|
568
|
+
});
|
|
714
569
|
}
|
|
715
570
|
return {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
571
|
+
blocks,
|
|
572
|
+
edges,
|
|
573
|
+
entryBlocks,
|
|
574
|
+
exitBlocks,
|
|
575
|
+
entry: entryBlocks[0],
|
|
576
|
+
exit: exitBlocks[exitBlocks.length - 1],
|
|
719
577
|
};
|
|
720
578
|
}
|
|
721
579
|
/**
|
|
722
|
-
*
|
|
723
|
-
|
|
724
|
-
isExported(name) {
|
|
725
|
-
return /^[A-Z]/.test(name);
|
|
726
|
-
}
|
|
727
|
-
/**
|
|
728
|
-
* Detect framework usage
|
|
580
|
+
* Extract symbols from AST
|
|
581
|
+
* @trace TSK-GO-005, REQ-SEC-GO-007
|
|
729
582
|
*/
|
|
730
|
-
async
|
|
731
|
-
const
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
583
|
+
async extractSymbols(astNodes) {
|
|
584
|
+
const symbols = new Map();
|
|
585
|
+
const functions = new Map();
|
|
586
|
+
const classes = new Map();
|
|
587
|
+
const scopes = new Map();
|
|
588
|
+
// Package scope
|
|
589
|
+
let packageName = 'main';
|
|
590
|
+
for (const [, astNode] of astNodes) {
|
|
591
|
+
if (astNode.type === 'package_clause' && astNode.properties.packageName) {
|
|
592
|
+
packageName = astNode.properties.packageName;
|
|
593
|
+
break;
|
|
737
594
|
}
|
|
738
595
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
596
|
+
const packageScope = {
|
|
597
|
+
id: packageName,
|
|
598
|
+
symbols: [],
|
|
599
|
+
kind: 'global',
|
|
600
|
+
};
|
|
601
|
+
scopes.set(packageName, packageScope);
|
|
602
|
+
for (const [_nodeId, astNode] of astNodes) {
|
|
603
|
+
switch (astNode.type) {
|
|
604
|
+
case 'function_declaration': {
|
|
605
|
+
const funcId = `func_${astNode.id}`;
|
|
606
|
+
const name = astNode.properties.name ?? 'anonymous';
|
|
607
|
+
const params = astNode.properties.parameters ?? [];
|
|
608
|
+
const isExp = astNode.properties.isExported ?? false;
|
|
609
|
+
const funcSymbol = {
|
|
610
|
+
name,
|
|
611
|
+
kind: 'function',
|
|
612
|
+
location: astNode.location,
|
|
613
|
+
scopeId: packageName,
|
|
614
|
+
properties: {
|
|
615
|
+
isExported: isExp,
|
|
616
|
+
},
|
|
617
|
+
parameters: params.map((p, i) => ({
|
|
618
|
+
name: p,
|
|
619
|
+
index: i,
|
|
620
|
+
})),
|
|
621
|
+
returnType: astNode.properties.returnType,
|
|
622
|
+
};
|
|
623
|
+
functions.set(funcId, funcSymbol);
|
|
624
|
+
symbols.set(funcId, funcSymbol);
|
|
625
|
+
packageScope.symbols.push(funcId);
|
|
748
626
|
break;
|
|
749
|
-
|
|
750
|
-
|
|
627
|
+
}
|
|
628
|
+
case 'method_declaration': {
|
|
629
|
+
const methodId = `method_${astNode.id}`;
|
|
630
|
+
const name = astNode.properties.name ?? 'anonymous';
|
|
631
|
+
const params = astNode.properties.parameters ?? [];
|
|
632
|
+
const receiver = astNode.properties.receiver;
|
|
633
|
+
const isExp = astNode.properties.isExported ?? false;
|
|
634
|
+
const methodSymbol = {
|
|
635
|
+
name,
|
|
636
|
+
kind: 'method',
|
|
637
|
+
location: astNode.location,
|
|
638
|
+
scopeId: packageName,
|
|
639
|
+
properties: {
|
|
640
|
+
receiver,
|
|
641
|
+
isExported: isExp,
|
|
642
|
+
},
|
|
643
|
+
parameters: params.map((p, i) => ({
|
|
644
|
+
name: p,
|
|
645
|
+
index: i,
|
|
646
|
+
})),
|
|
647
|
+
returnType: astNode.properties.returnType,
|
|
648
|
+
};
|
|
649
|
+
functions.set(methodId, methodSymbol);
|
|
650
|
+
symbols.set(methodId, methodSymbol);
|
|
651
|
+
packageScope.symbols.push(methodId);
|
|
751
652
|
break;
|
|
752
|
-
|
|
753
|
-
|
|
653
|
+
}
|
|
654
|
+
case 'type_declaration': {
|
|
655
|
+
const typeId = `type_${astNode.id}`;
|
|
656
|
+
const name = astNode.properties.name ?? 'Anonymous';
|
|
657
|
+
const underlyingType = astNode.properties.underlyingType;
|
|
658
|
+
const isExp = astNode.properties.isExported ?? false;
|
|
659
|
+
if (underlyingType === 'struct_type') {
|
|
660
|
+
const structSymbol = {
|
|
661
|
+
name,
|
|
662
|
+
kind: 'class',
|
|
663
|
+
location: astNode.location,
|
|
664
|
+
scopeId: packageName,
|
|
665
|
+
methods: [],
|
|
666
|
+
properties: [],
|
|
667
|
+
};
|
|
668
|
+
classes.set(typeId, structSymbol);
|
|
669
|
+
const structAsSymbol = {
|
|
670
|
+
name,
|
|
671
|
+
kind: 'class',
|
|
672
|
+
location: astNode.location,
|
|
673
|
+
scopeId: packageName,
|
|
674
|
+
properties: {
|
|
675
|
+
structType: true,
|
|
676
|
+
isExported: isExp,
|
|
677
|
+
},
|
|
678
|
+
};
|
|
679
|
+
symbols.set(typeId, structAsSymbol);
|
|
680
|
+
}
|
|
681
|
+
else if (underlyingType === 'interface_type') {
|
|
682
|
+
const interfaceSymbol = {
|
|
683
|
+
name,
|
|
684
|
+
kind: 'interface',
|
|
685
|
+
location: astNode.location,
|
|
686
|
+
scopeId: packageName,
|
|
687
|
+
properties: {
|
|
688
|
+
isExported: isExp,
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
symbols.set(typeId, interfaceSymbol);
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
// Type alias
|
|
695
|
+
const typeSymbol = {
|
|
696
|
+
name,
|
|
697
|
+
kind: 'type',
|
|
698
|
+
location: astNode.location,
|
|
699
|
+
scopeId: packageName,
|
|
700
|
+
properties: {
|
|
701
|
+
underlyingType,
|
|
702
|
+
isExported: isExp,
|
|
703
|
+
},
|
|
704
|
+
};
|
|
705
|
+
symbols.set(typeId, typeSymbol);
|
|
706
|
+
}
|
|
707
|
+
packageScope.symbols.push(typeId);
|
|
754
708
|
break;
|
|
755
|
-
|
|
756
|
-
|
|
709
|
+
}
|
|
710
|
+
case 'const_declaration':
|
|
711
|
+
case 'var_declaration': {
|
|
712
|
+
const varId = `var_${astNode.id}`;
|
|
713
|
+
const name = astNode.properties.left ?? 'anonymous';
|
|
714
|
+
const isConst = astNode.type === 'const_declaration';
|
|
715
|
+
const varSymbol = {
|
|
716
|
+
name,
|
|
717
|
+
kind: isConst ? 'constant' : 'variable',
|
|
718
|
+
location: astNode.location,
|
|
719
|
+
scopeId: packageName,
|
|
720
|
+
properties: {
|
|
721
|
+
isExported: isExported(name),
|
|
722
|
+
},
|
|
723
|
+
};
|
|
724
|
+
symbols.set(varId, varSymbol);
|
|
725
|
+
packageScope.symbols.push(varId);
|
|
757
726
|
break;
|
|
758
|
-
|
|
759
|
-
if (matched) {
|
|
760
|
-
detected.push(framework);
|
|
727
|
+
}
|
|
761
728
|
}
|
|
762
729
|
}
|
|
763
|
-
return detected;
|
|
764
|
-
}
|
|
765
|
-
/**
|
|
766
|
-
* Create fallback AST for when tree-sitter is unavailable
|
|
767
|
-
*/
|
|
768
|
-
createFallbackAST(code, filePath) {
|
|
769
|
-
const lines = code.split('\n');
|
|
770
730
|
return {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
startLine: 1,
|
|
777
|
-
endLine: lines.length,
|
|
778
|
-
startColumn: 0,
|
|
779
|
-
endColumn: lines[lines.length - 1]?.length ?? 0,
|
|
780
|
-
},
|
|
781
|
-
children: [],
|
|
782
|
-
metadata: {
|
|
783
|
-
fallback: true,
|
|
784
|
-
lineCount: lines.length,
|
|
785
|
-
},
|
|
731
|
+
symbols,
|
|
732
|
+
functions,
|
|
733
|
+
classes,
|
|
734
|
+
scopes,
|
|
735
|
+
global: symbols,
|
|
786
736
|
};
|
|
787
737
|
}
|
|
788
738
|
/**
|
|
789
|
-
*
|
|
739
|
+
* Check if identifier is exported (public)
|
|
740
|
+
* Go exports identifiers starting with uppercase
|
|
790
741
|
*/
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
const visit = (node) => {
|
|
794
|
-
if (types.includes(node.type)) {
|
|
795
|
-
results.push(node);
|
|
796
|
-
}
|
|
797
|
-
for (const child of node.children) {
|
|
798
|
-
visit(child);
|
|
799
|
-
}
|
|
800
|
-
};
|
|
801
|
-
visit(ast);
|
|
802
|
-
return results;
|
|
742
|
+
isExported(name) {
|
|
743
|
+
return isExported(name);
|
|
803
744
|
}
|
|
804
745
|
}
|
|
746
|
+
/**
|
|
747
|
+
* Create Go extractor instance
|
|
748
|
+
* @trace REQ-SEC-GO-001
|
|
749
|
+
*/
|
|
750
|
+
export function createGoExtractor() {
|
|
751
|
+
return new GoExtractor();
|
|
752
|
+
}
|
|
805
753
|
//# sourceMappingURL=go-extractor.js.map
|