opensecurity 0.1.0
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/LICENSE +21 -0
- package/README.md +268 -0
- package/dist/analysis/ast.js +20 -0
- package/dist/analysis/graphs.js +295 -0
- package/dist/analysis/patterns.js +230 -0
- package/dist/analysis/rules.js +48 -0
- package/dist/analysis/taint.js +199 -0
- package/dist/cli.js +396 -0
- package/dist/config.js +71 -0
- package/dist/deps/cve.js +102 -0
- package/dist/deps/engine.js +27 -0
- package/dist/deps/patch.js +11 -0
- package/dist/deps/scanners.js +114 -0
- package/dist/deps/scoring.js +46 -0
- package/dist/deps/simulate.js +9 -0
- package/dist/deps/types.js +1 -0
- package/dist/fileWalker.js +27 -0
- package/dist/login.js +583 -0
- package/dist/oauthStore.js +48 -0
- package/dist/pr-comment.js +118 -0
- package/dist/progress.js +150 -0
- package/dist/proxy.js +93 -0
- package/dist/rules/defaultRules.js +177 -0
- package/dist/rules/loadRules.js +14 -0
- package/dist/scan.js +1129 -0
- package/dist/telemetry.js +72 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 eliophan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# OpenSecurity
|
|
2
|
+
|
|
3
|
+
OpenSecurity is an open-source CLI for scanning codebases for security risks, with first-class static analysis for JavaScript/TypeScript and optional AI scanning for broader files.
|
|
4
|
+
It combines:
|
|
5
|
+
|
|
6
|
+
- Static analysis with AST-based taint rules (OWASP-focused).
|
|
7
|
+
- Dependency scanning with CVE lookup (local cache or API).
|
|
8
|
+
- Optional AI-assisted scanning for deeper findings.
|
|
9
|
+
|
|
10
|
+
## Project Status
|
|
11
|
+
|
|
12
|
+
Active. This repo is maintained and intended for open-source use. Contributions are welcome.
|
|
13
|
+
|
|
14
|
+
## Scope
|
|
15
|
+
|
|
16
|
+
- Target languages for static analysis: JavaScript and TypeScript.
|
|
17
|
+
- Dependency scanning: npm and PyPI manifests.
|
|
18
|
+
- AI scanning is optional and requires an API key (defaults to scanning all text files).
|
|
19
|
+
|
|
20
|
+
## Non-Goals
|
|
21
|
+
|
|
22
|
+
- This tool is not a full SAST replacement or compliance scanner.
|
|
23
|
+
- It does not execute or sandbox code.
|
|
24
|
+
- It does not guarantee complete coverage of all vulnerabilities.
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install
|
|
30
|
+
npm run dev -- scan --dry-run
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Build the CLI:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm run build
|
|
37
|
+
./dist/cli.js scan --dry-run
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
From source:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install
|
|
46
|
+
npm run build
|
|
47
|
+
npm link
|
|
48
|
+
opensecurity scan --dry-run
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Supported Platforms
|
|
52
|
+
|
|
53
|
+
- Node.js 20+
|
|
54
|
+
- macOS, Linux, Windows
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
- AST taint engine with configurable sources/sinks/sanitizers.
|
|
59
|
+
- OWASP-aligned default rules (injection, SSRF, path traversal, XSS templates, SQLi).
|
|
60
|
+
- Pattern-based detectors (hardcoded secrets, insecure crypto, unsafe deserialization).
|
|
61
|
+
- Dependency scanning for npm and PyPI (`package.json`, `package-lock.json`, `requirements.txt`).
|
|
62
|
+
- Text, JSON, and SARIF output.
|
|
63
|
+
- Optional AI scan (API key or OAuth flow) with multiple providers.
|
|
64
|
+
- Configurable include/exclude filters and scan scope.
|
|
65
|
+
|
|
66
|
+
## CLI
|
|
67
|
+
|
|
68
|
+
### `scan`
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
opensecurity scan [options]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Common options:
|
|
75
|
+
|
|
76
|
+
- `--format <format>`: `text|json|sarif` (default: `text`)
|
|
77
|
+
- `--include <pattern...>` / `--exclude <pattern...>`: override project filters
|
|
78
|
+
- `--rules <path>`: rules JSON override
|
|
79
|
+
- `--cve-cache <path>`: CVE cache JSON
|
|
80
|
+
- `--cve-api-url <url>`: CVE lookup API endpoint
|
|
81
|
+
- `--simulate`: include payload + impact for dependency findings
|
|
82
|
+
- `--provider <provider>`: `openai|anthropic|google|mistral|xai|cohere`
|
|
83
|
+
- `--ai-all-text`: allow AI scan on all text files (non-JS/TS) (default)
|
|
84
|
+
- `--ai-js-only`: limit AI scan to JS/TS only
|
|
85
|
+
- `--path <path>`: scan a specific file or directory
|
|
86
|
+
- `--diff-only`: scan only files changed in git
|
|
87
|
+
- `--diff-base <ref>`: git base ref for diff-only (default: HEAD)
|
|
88
|
+
- `--ai-multi-agent`: split AI scan into worker batches
|
|
89
|
+
- `--ai-batch-size <n>`: files per AI worker batch (default: 25)
|
|
90
|
+
- `--ai-batch-depth <n>`: path depth for AI batching (default: 2)
|
|
91
|
+
- `--ai-cache`: enable AI per-file cache (default)
|
|
92
|
+
- `--no-ai-cache`: disable AI per-file cache
|
|
93
|
+
- `--ai-cache-path <path>`: path to AI cache file
|
|
94
|
+
- `--dependency-only`: only run dependency scan
|
|
95
|
+
- `--no-ai`: disable AI scanning
|
|
96
|
+
- `--dry-run`: list matched files without scanning
|
|
97
|
+
- `--fail-on <severity>`: exit 1 if findings >= severity
|
|
98
|
+
- `--fail-on-high`: exit 1 if findings >= high
|
|
99
|
+
- `--sarif-output <path>`: write SARIF alongside primary output
|
|
100
|
+
- `--concurrency <n>`: parallel scan workers
|
|
101
|
+
- `--max-chars <n>`: max chars per chunk for AI scanning
|
|
102
|
+
|
|
103
|
+
Example commands:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
opensecurity scan --no-ai
|
|
107
|
+
opensecurity scan --format sarif --sarif-output reports/opensecurity.sarif
|
|
108
|
+
opensecurity scan --provider anthropic --model claude-sonnet-4-20250514
|
|
109
|
+
opensecurity scan --dependency-only --simulate
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### `login`
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
opensecurity login --mode oauth
|
|
116
|
+
opensecurity login --mode api_key
|
|
117
|
+
opensecurity login --mode api_key --provider anthropic
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Stores auth config in `~/.config/opensecurity/config.json`.
|
|
121
|
+
|
|
122
|
+
### `proxy`
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
opensecurity proxy --port 8787
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Runs a local OAuth proxy for the OAuth flow.
|
|
129
|
+
|
|
130
|
+
### `telemetry`
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
opensecurity telemetry on
|
|
134
|
+
opensecurity telemetry off
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Configuration
|
|
138
|
+
|
|
139
|
+
### Precedence
|
|
140
|
+
|
|
141
|
+
1. CLI flags
|
|
142
|
+
2. Project config (`.opensecurity.json`)
|
|
143
|
+
3. Global config (`~/.config/opensecurity/config.json`)
|
|
144
|
+
4. Built-in defaults
|
|
145
|
+
|
|
146
|
+
Project config: `.opensecurity.json`
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
151
|
+
"exclude": ["**/*.test.ts"],
|
|
152
|
+
"rulesPath": "rules.json",
|
|
153
|
+
"cveCachePath": "cve-cache.json",
|
|
154
|
+
"cveApiUrl": "https://example.com/osv",
|
|
155
|
+
"dataSensitivity": "medium",
|
|
156
|
+
"maxChars": 4000,
|
|
157
|
+
"concurrency": 2
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Global config: `~/.config/opensecurity/config.json`
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"provider": "openai",
|
|
166
|
+
"apiKey": "sk-...",
|
|
167
|
+
"baseUrl": "https://api.openai.com/v1/responses",
|
|
168
|
+
"model": "gpt-4o-mini",
|
|
169
|
+
"apiType": "responses",
|
|
170
|
+
"authMode": "api_key",
|
|
171
|
+
"authProfileId": "codex",
|
|
172
|
+
"oauthProvider": "proxy",
|
|
173
|
+
"providerApiKey": "..."
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Rules
|
|
178
|
+
|
|
179
|
+
Default rules are in `src/rules/defaultRules.ts`.
|
|
180
|
+
You can override with a JSON file (`--rules` or `rulesPath`).
|
|
181
|
+
|
|
182
|
+
Pattern-based detectors run alongside rules (hardcoded secrets, insecure crypto, unsafe deserialization).
|
|
183
|
+
|
|
184
|
+
Rule schema (simplified):
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"id": "rule-id",
|
|
189
|
+
"name": "Human name",
|
|
190
|
+
"severity": "low|medium|high|critical",
|
|
191
|
+
"owasp": "A03:2021 Injection",
|
|
192
|
+
"sources": [{ "id": "src", "name": "getUserInput", "matcher": { "callee": "getUserInput" } }],
|
|
193
|
+
"sinks": [{ "id": "sink", "name": "eval", "matcher": { "callee": "eval" } }],
|
|
194
|
+
"sanitizers": [{ "id": "san", "name": "escape", "matcher": { "callee": "escape" } }]
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Output
|
|
199
|
+
|
|
200
|
+
- **Text**: grouped by severity
|
|
201
|
+
- **JSON**: machine-readable, includes `schemaVersion`
|
|
202
|
+
- **SARIF**: for CI and code scanning tools
|
|
203
|
+
|
|
204
|
+
## Language Support
|
|
205
|
+
|
|
206
|
+
- Static analysis: JavaScript and TypeScript
|
|
207
|
+
- Dependency scanning: npm and PyPI manifests
|
|
208
|
+
|
|
209
|
+
## Security Notes
|
|
210
|
+
|
|
211
|
+
- Do not commit secrets.
|
|
212
|
+
- AI scanning sends code chunks to the configured API endpoint.
|
|
213
|
+
- Use `--no-ai` if you want purely local scanning.
|
|
214
|
+
- This tool provides best-effort findings and should be validated in your environment.
|
|
215
|
+
|
|
216
|
+
## Providers
|
|
217
|
+
|
|
218
|
+
Supported providers for AI scanning:
|
|
219
|
+
|
|
220
|
+
- OpenAI (Responses or Chat Completions)
|
|
221
|
+
- Anthropic Messages API
|
|
222
|
+
- Google Gemini API
|
|
223
|
+
- Mistral Chat Completions API
|
|
224
|
+
- xAI Chat Completions API
|
|
225
|
+
- Cohere Chat API
|
|
226
|
+
|
|
227
|
+
API keys can be stored via `opensecurity login --mode api_key --provider <provider>` or set via environment:
|
|
228
|
+
|
|
229
|
+
- `OPENAI_API_KEY`
|
|
230
|
+
- `ANTHROPIC_API_KEY`
|
|
231
|
+
- `GEMINI_API_KEY`
|
|
232
|
+
- `MISTRAL_API_KEY`
|
|
233
|
+
- `XAI_API_KEY`
|
|
234
|
+
- `COHERE_API_KEY`
|
|
235
|
+
|
|
236
|
+
When an API key is available, the model picker will try to fetch a live model list for the provider.
|
|
237
|
+
|
|
238
|
+
## Contributing
|
|
239
|
+
|
|
240
|
+
- Run `npm test`, `npm run lint`, and `npm run build` before submitting changes.
|
|
241
|
+
- Keep changes focused and add tests for new behavior.
|
|
242
|
+
- Do not add or log real secrets.
|
|
243
|
+
- For large changes, open an issue first to align on scope.
|
|
244
|
+
|
|
245
|
+
See `CONTRIBUTING.md` for full guidelines.
|
|
246
|
+
|
|
247
|
+
## Security
|
|
248
|
+
|
|
249
|
+
If you discover a vulnerability:
|
|
250
|
+
|
|
251
|
+
- Prefer opening a private **Security Advisory** on GitHub (if enabled), or
|
|
252
|
+
- Open a minimal public issue without sensitive details and request private follow‑up.
|
|
253
|
+
|
|
254
|
+
## Support
|
|
255
|
+
|
|
256
|
+
For questions or help, open a GitHub issue with clear reproduction steps.
|
|
257
|
+
|
|
258
|
+
## Development
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
npm install
|
|
262
|
+
npm run dev -- scan --dry-run
|
|
263
|
+
npm test
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT (see `LICENSE`).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { parse } from "@babel/parser";
|
|
2
|
+
export function parseSource(code, filePath, options = {}) {
|
|
3
|
+
const ast = parse(code, {
|
|
4
|
+
sourceType: options.sourceType ?? "module",
|
|
5
|
+
sourceFilename: filePath,
|
|
6
|
+
allowReturnOutsideFunction: true,
|
|
7
|
+
errorRecovery: true,
|
|
8
|
+
plugins: [
|
|
9
|
+
"typescript",
|
|
10
|
+
"jsx",
|
|
11
|
+
"classProperties",
|
|
12
|
+
"classPrivateProperties",
|
|
13
|
+
"decorators-legacy",
|
|
14
|
+
"dynamicImport",
|
|
15
|
+
"importMeta",
|
|
16
|
+
"topLevelAwait"
|
|
17
|
+
]
|
|
18
|
+
});
|
|
19
|
+
return { filePath, code, ast };
|
|
20
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import traverseImport from "@babel/traverse";
|
|
2
|
+
import * as t from "@babel/types";
|
|
3
|
+
export function buildImportGraph(ast, filePath) {
|
|
4
|
+
const traverse = normalizeTraverse(traverseImport);
|
|
5
|
+
const graph = new Map();
|
|
6
|
+
graph.set(filePath, new Set());
|
|
7
|
+
traverse(ast, {
|
|
8
|
+
ImportDeclaration(path) {
|
|
9
|
+
const source = path.node.source.value;
|
|
10
|
+
graph.get(filePath)?.add(source);
|
|
11
|
+
},
|
|
12
|
+
CallExpression(path) {
|
|
13
|
+
const callee = path.node.callee;
|
|
14
|
+
if (!t.isIdentifier(callee) || callee.name !== "require")
|
|
15
|
+
return;
|
|
16
|
+
const arg = path.node.arguments[0];
|
|
17
|
+
if (!t.isStringLiteral(arg))
|
|
18
|
+
return;
|
|
19
|
+
graph.get(filePath)?.add(arg.value);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
return graph;
|
|
23
|
+
}
|
|
24
|
+
export function buildFunctionMap(ast, filePath) {
|
|
25
|
+
const traverse = normalizeTraverse(traverseImport);
|
|
26
|
+
const map = new Map();
|
|
27
|
+
const registerFunction = (name, node, kind) => {
|
|
28
|
+
const loc = node.loc?.start ? { line: node.loc.start.line, column: node.loc.start.column } : undefined;
|
|
29
|
+
const params = node.params.map((param) => (t.isIdentifier(param) ? param.name : "<pattern>"));
|
|
30
|
+
const id = `${filePath}:${name}:${loc?.line ?? 0}`;
|
|
31
|
+
map.set(id, {
|
|
32
|
+
id,
|
|
33
|
+
name,
|
|
34
|
+
file: filePath,
|
|
35
|
+
loc,
|
|
36
|
+
params,
|
|
37
|
+
isAsync: Boolean(node.async),
|
|
38
|
+
kind
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
const moduleId = `${filePath}:<module>:0`;
|
|
42
|
+
map.set(moduleId, {
|
|
43
|
+
id: moduleId,
|
|
44
|
+
name: "<module>",
|
|
45
|
+
file: filePath,
|
|
46
|
+
loc: { line: 0, column: 0 },
|
|
47
|
+
params: [],
|
|
48
|
+
isAsync: false,
|
|
49
|
+
kind: "module"
|
|
50
|
+
});
|
|
51
|
+
traverse(ast, {
|
|
52
|
+
FunctionDeclaration(path) {
|
|
53
|
+
const name = path.node.id?.name ?? "<anonymous>";
|
|
54
|
+
registerFunction(name, path.node, "function");
|
|
55
|
+
},
|
|
56
|
+
FunctionExpression(path) {
|
|
57
|
+
const name = inferFunctionName(path) ?? "<anonymous>";
|
|
58
|
+
registerFunction(name, path.node, "function");
|
|
59
|
+
},
|
|
60
|
+
ArrowFunctionExpression(path) {
|
|
61
|
+
const name = inferFunctionName(path) ?? "<anonymous>";
|
|
62
|
+
registerFunction(name, path.node, "arrow");
|
|
63
|
+
},
|
|
64
|
+
ObjectMethod(path) {
|
|
65
|
+
const name = getPropertyName(path.node.key) ?? "<anonymous>";
|
|
66
|
+
registerFunction(name, path.node, "method");
|
|
67
|
+
},
|
|
68
|
+
ClassMethod(path) {
|
|
69
|
+
const name = getPropertyName(path.node.key) ?? "<anonymous>";
|
|
70
|
+
registerFunction(name, path.node, "method");
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return map;
|
|
74
|
+
}
|
|
75
|
+
export function buildCallGraph(ast, filePath, functions) {
|
|
76
|
+
const traverse = normalizeTraverse(traverseImport);
|
|
77
|
+
const graph = new Map();
|
|
78
|
+
const fnStack = [];
|
|
79
|
+
const ensure = (id) => {
|
|
80
|
+
if (!graph.has(id))
|
|
81
|
+
graph.set(id, new Set());
|
|
82
|
+
};
|
|
83
|
+
const moduleId = [...functions.values()].find((f) => f.kind === "module" && f.file === filePath)?.id;
|
|
84
|
+
const rootId = moduleId ?? `${filePath}:<module>:0`;
|
|
85
|
+
const pushFn = (id) => {
|
|
86
|
+
fnStack.push(id);
|
|
87
|
+
ensure(id);
|
|
88
|
+
};
|
|
89
|
+
const popFn = () => {
|
|
90
|
+
fnStack.pop();
|
|
91
|
+
};
|
|
92
|
+
const currentFn = () => fnStack[fnStack.length - 1] ?? rootId;
|
|
93
|
+
traverse(ast, {
|
|
94
|
+
Program: {
|
|
95
|
+
enter() {
|
|
96
|
+
pushFn(rootId);
|
|
97
|
+
},
|
|
98
|
+
exit() {
|
|
99
|
+
popFn();
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
FunctionDeclaration: {
|
|
103
|
+
enter(path) {
|
|
104
|
+
const name = path.node.id?.name ?? "<anonymous>";
|
|
105
|
+
const id = findFunctionId(functions, filePath, name, path.node.loc?.start.line);
|
|
106
|
+
pushFn(id ?? `${filePath}:${name}:${path.node.loc?.start.line ?? 0}`);
|
|
107
|
+
},
|
|
108
|
+
exit() {
|
|
109
|
+
popFn();
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
FunctionExpression: {
|
|
113
|
+
enter(path) {
|
|
114
|
+
const name = inferFunctionName(path) ?? "<anonymous>";
|
|
115
|
+
const id = findFunctionId(functions, filePath, name, path.node.loc?.start.line);
|
|
116
|
+
pushFn(id ?? `${filePath}:${name}:${path.node.loc?.start.line ?? 0}`);
|
|
117
|
+
},
|
|
118
|
+
exit() {
|
|
119
|
+
popFn();
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
ArrowFunctionExpression: {
|
|
123
|
+
enter(path) {
|
|
124
|
+
const name = inferFunctionName(path) ?? "<anonymous>";
|
|
125
|
+
const id = findFunctionId(functions, filePath, name, path.node.loc?.start.line);
|
|
126
|
+
pushFn(id ?? `${filePath}:${name}:${path.node.loc?.start.line ?? 0}`);
|
|
127
|
+
},
|
|
128
|
+
exit() {
|
|
129
|
+
popFn();
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
ObjectMethod: {
|
|
133
|
+
enter(path) {
|
|
134
|
+
const name = getPropertyName(path.node.key) ?? "<anonymous>";
|
|
135
|
+
const id = findFunctionId(functions, filePath, name, path.node.loc?.start.line);
|
|
136
|
+
pushFn(id ?? `${filePath}:${name}:${path.node.loc?.start.line ?? 0}`);
|
|
137
|
+
},
|
|
138
|
+
exit() {
|
|
139
|
+
popFn();
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
ClassMethod: {
|
|
143
|
+
enter(path) {
|
|
144
|
+
const name = getPropertyName(path.node.key) ?? "<anonymous>";
|
|
145
|
+
const id = findFunctionId(functions, filePath, name, path.node.loc?.start.line);
|
|
146
|
+
pushFn(id ?? `${filePath}:${name}:${path.node.loc?.start.line ?? 0}`);
|
|
147
|
+
},
|
|
148
|
+
exit() {
|
|
149
|
+
popFn();
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
CallExpression(path) {
|
|
153
|
+
const caller = currentFn();
|
|
154
|
+
const callee = getCalleeName(path.node);
|
|
155
|
+
if (!callee)
|
|
156
|
+
return;
|
|
157
|
+
ensure(caller);
|
|
158
|
+
graph.get(caller)?.add(callee);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return graph;
|
|
162
|
+
}
|
|
163
|
+
export function buildDataFlowGraph(ast, filePath) {
|
|
164
|
+
const traverse = normalizeTraverse(traverseImport);
|
|
165
|
+
const nodes = [];
|
|
166
|
+
const edges = [];
|
|
167
|
+
const scopes = [];
|
|
168
|
+
const pushScope = () => scopes.push(new Map());
|
|
169
|
+
const popScope = () => scopes.pop();
|
|
170
|
+
const currentScope = () => scopes[scopes.length - 1];
|
|
171
|
+
const recordDef = (name, node) => {
|
|
172
|
+
const loc = node.loc?.start ? { line: node.loc.start.line, column: node.loc.start.column } : undefined;
|
|
173
|
+
const dataNode = {
|
|
174
|
+
id: `${filePath}:${name}:${loc?.line ?? 0}:${loc?.column ?? 0}`,
|
|
175
|
+
name,
|
|
176
|
+
file: filePath,
|
|
177
|
+
loc
|
|
178
|
+
};
|
|
179
|
+
nodes.push(dataNode);
|
|
180
|
+
currentScope()?.set(name, dataNode);
|
|
181
|
+
};
|
|
182
|
+
const recordUse = (name, node) => {
|
|
183
|
+
const loc = node.loc?.start ? { line: node.loc.start.line, column: node.loc.start.column } : undefined;
|
|
184
|
+
const useNode = {
|
|
185
|
+
id: `${filePath}:${name}:${loc?.line ?? 0}:${loc?.column ?? 0}:use`,
|
|
186
|
+
name,
|
|
187
|
+
file: filePath,
|
|
188
|
+
loc
|
|
189
|
+
};
|
|
190
|
+
nodes.push(useNode);
|
|
191
|
+
const defNode = currentScope()?.get(name);
|
|
192
|
+
if (defNode) {
|
|
193
|
+
edges.push({ from: defNode, to: useNode, kind: "data" });
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
traverse(ast, {
|
|
197
|
+
Program: {
|
|
198
|
+
enter() {
|
|
199
|
+
pushScope();
|
|
200
|
+
},
|
|
201
|
+
exit() {
|
|
202
|
+
popScope();
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
Function: {
|
|
206
|
+
enter() {
|
|
207
|
+
pushScope();
|
|
208
|
+
},
|
|
209
|
+
exit() {
|
|
210
|
+
popScope();
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
VariableDeclarator(path) {
|
|
214
|
+
const id = path.node.id;
|
|
215
|
+
if (t.isIdentifier(id)) {
|
|
216
|
+
recordDef(id.name, id);
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
AssignmentExpression(path) {
|
|
220
|
+
const left = path.node.left;
|
|
221
|
+
if (t.isIdentifier(left)) {
|
|
222
|
+
recordDef(left.name, left);
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
UpdateExpression(path) {
|
|
226
|
+
const arg = path.node.argument;
|
|
227
|
+
if (t.isIdentifier(arg)) {
|
|
228
|
+
recordUse(arg.name, arg);
|
|
229
|
+
recordDef(arg.name, arg);
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
Identifier(path) {
|
|
233
|
+
if (!path.isReferencedIdentifier())
|
|
234
|
+
return;
|
|
235
|
+
recordUse(path.node.name, path.node);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
return { nodes, edges };
|
|
239
|
+
}
|
|
240
|
+
function getCalleeName(node) {
|
|
241
|
+
const callee = node.callee;
|
|
242
|
+
if (t.isIdentifier(callee))
|
|
243
|
+
return callee.name;
|
|
244
|
+
if (t.isMemberExpression(callee))
|
|
245
|
+
return memberExpressionToString(callee);
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
function memberExpressionToString(node) {
|
|
249
|
+
if (node.computed)
|
|
250
|
+
return null;
|
|
251
|
+
const object = node.object;
|
|
252
|
+
const property = node.property;
|
|
253
|
+
const objectName = t.isIdentifier(object)
|
|
254
|
+
? object.name
|
|
255
|
+
: t.isMemberExpression(object)
|
|
256
|
+
? memberExpressionToString(object)
|
|
257
|
+
: null;
|
|
258
|
+
if (!objectName)
|
|
259
|
+
return null;
|
|
260
|
+
if (!t.isIdentifier(property))
|
|
261
|
+
return null;
|
|
262
|
+
return `${objectName}.${property.name}`;
|
|
263
|
+
}
|
|
264
|
+
function inferFunctionName(path) {
|
|
265
|
+
const parent = path.parentPath;
|
|
266
|
+
if (parent?.isVariableDeclarator() && t.isIdentifier(parent.node.id))
|
|
267
|
+
return parent.node.id.name;
|
|
268
|
+
if (parent?.isAssignmentExpression() && t.isIdentifier(parent.node.left))
|
|
269
|
+
return parent.node.left.name;
|
|
270
|
+
if (parent?.isObjectProperty())
|
|
271
|
+
return getPropertyName(parent.node.key);
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
function getPropertyName(key) {
|
|
275
|
+
if (t.isIdentifier(key))
|
|
276
|
+
return key.name;
|
|
277
|
+
if (t.isStringLiteral(key))
|
|
278
|
+
return key.value;
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
function findFunctionId(functions, filePath, name, line) {
|
|
282
|
+
for (const info of functions.values()) {
|
|
283
|
+
if (info.file !== filePath)
|
|
284
|
+
continue;
|
|
285
|
+
if (info.name !== name)
|
|
286
|
+
continue;
|
|
287
|
+
if (line && info.loc?.line !== line)
|
|
288
|
+
continue;
|
|
289
|
+
return info.id;
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
function normalizeTraverse(value) {
|
|
294
|
+
return value.default ?? value;
|
|
295
|
+
}
|