@appthreat/chennai 0.0.1 → 0.3.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 +1 -1
- package/README.md +291 -3
- package/index.js +61 -0
- package/package.json +34 -3
- package/resolve.js +226 -0
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,5 +1,293 @@
|
|
|
1
|
-
#
|
|
1
|
+
# chennai
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
chennai (chen & ai) is a hybrid AI agent and a terminal user interface for exploring AppThreat [atom](https://github.com/AppThreat/atom) files. It is built on top of the [chen](https://github.com/AppThreat/chen) library (Code Hierarchy Exploratory Network) and provides a keyboard-driven interactive environment for static analysis, data-flow tracing, and AI-assisted code and security analysis.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Unlike a traditional SAST engine or a generic LLM chatbot, chennai gives you direct access to the underlying graph queries so you can ask arbitrary questions about a program's structure and data flows without leaving the terminal. It runs entirely on your machine with no server backend and no data leaving your environment.
|
|
6
|
+
|
|
7
|
+
chennai includes a built-in AI agent that uses atom as grounding context. The agent can run chen DSL queries, traverse data-flow paths, run graph algorithms, read source files, and search code with ripgrep, all orchestrated from a single chat interface inside the TUI. The agent supports both Anthropic and OpenAI compatible providers and can work either with or without an LLM.
|
|
8
|
+
|
|
9
|
+
## Use cases
|
|
10
|
+
|
|
11
|
+
- Interactive exploration of atom files generated by the atom tool. Open an atom, browse methods, calls, namespaces, tags, and literals without writing queries by hand.
|
|
12
|
+
- Data-flow analysis with source to sink tracing. The TUI displays flows with grouped paths, sub-flow toggling, and mitigation indicators.
|
|
13
|
+
- Graph algorithm execution including PageRank, strongly connected components, topological sort, dominator trees, and interprocedural path finding.
|
|
14
|
+
- AI-assisted vulnerability analysis with slash commands for security review, code review, explain, and trace. The agent routes tool calls through the engine and returns structured findings.
|
|
15
|
+
- REPL-driven querying with tab completions, command history, and the full chen DSL available through the eval command.
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- An atom file (from the atom tool) to open. The atom file is a Code Property Graph produced by running atom against a codebase.
|
|
20
|
+
- Java 23 or newer if running the engine via the JAR fallback distribution.
|
|
21
|
+
- At least 4GB of available memory for the TUI and engine together. Larger codebases may need more.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
chennai is distributed as an npm package with platform-specific native binaries.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
npm install -g @appthreat/chennai
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
After installation the `chennai` command is available globally.
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
chennai <path-to-atom-file-or-directory> [options]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Setup
|
|
38
|
+
|
|
39
|
+
The `chennai setup` command installs or updates the required analysis tools (cdxgen, atom, atom-parsetools) via npm:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
chennai setup
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This runs `npm install -g --ignore-scripts @cyclonedx/cdxgen @appthreat/atom @appthreat/atom-parsetools`.
|
|
46
|
+
|
|
47
|
+
### Tool discovery
|
|
48
|
+
|
|
49
|
+
chennai auto-detects these tools in the following order:
|
|
50
|
+
|
|
51
|
+
| Tool | Env var | Search path |
|
|
52
|
+
| ------ | ------------ | ---------------------------------------------- |
|
|
53
|
+
| cdxgen | `CDXGEN_CMD` | PATH → `node_modules/.bin/cdxgen` → npm global |
|
|
54
|
+
| atom | `ATOM_CMD` | `ATOM_CMD` → PATH |
|
|
55
|
+
| npm | — | PATH |
|
|
56
|
+
|
|
57
|
+
### Atom auto-generation
|
|
58
|
+
|
|
59
|
+
When you point chennai at a project directory that has no `.atom` file, it will prompt you to generate one interactively:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
$ chennai /path/to/project
|
|
63
|
+
No .atom file found in /path/to/project
|
|
64
|
+
Generate one for analysis? This will:
|
|
65
|
+
└ 1. Run cdxgen to produce a CycloneDX SBOM
|
|
66
|
+
└ 2. Run atom --with-data-deps to build the atom file
|
|
67
|
+
|
|
68
|
+
Source: /path/to/project [Y/n]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If the tools are not installed but `npm` is available, chennai offers to install them automatically before proceeding.
|
|
72
|
+
|
|
73
|
+
### Software Bill of Materials
|
|
74
|
+
|
|
75
|
+
chennai integrates with [cdxgen](https://github.com/AppThreat/cdxgen) to automatically generate and load CycloneDX SBOMs for deeper dependency-aware analysis. When you invoke chennai with a source or reports directory, it first looks for any existing `.cdx.json` files. If none are found, it invokes cdxgen to create a BOM with the naming convention `sbom-<language>-<lifecycle>.cdx.json`.
|
|
76
|
+
|
|
77
|
+
#### Installing cdxgen (recommended for best experience)
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
npm install -g @cyclonedx/cdxgen
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
cdxgen will be auto-detected in PATH. You can also set the `CDXGEN_CMD` environment variable to point to a custom location. Once installed, chennai will automatically generate BOMs on startup when a source directory is available.
|
|
84
|
+
|
|
85
|
+
### BOM commands
|
|
86
|
+
|
|
87
|
+
| Command | Description |
|
|
88
|
+
| ------------- | -------------------------------------------------------------- |
|
|
89
|
+
| `bom` | Display all BOM components as a searchable, sortable table |
|
|
90
|
+
| `bom <query>` | Filter BOM components by name, type, version, PURL, or license |
|
|
91
|
+
|
|
92
|
+
The BOM data is also injected into AI agent prompts for security and code reviews, improving the LLM's understanding of third-party dependencies, their licenses, and known vulnerabilities.
|
|
93
|
+
|
|
94
|
+
### Options
|
|
95
|
+
|
|
96
|
+
| Option | Default | Description |
|
|
97
|
+
| -------------------- | --------------------- | --------------------------------------------------- |
|
|
98
|
+
| `--engine PATH` | auto | Path to the chennai-engine binary |
|
|
99
|
+
| `--theme dark/light` | dark | Color theme |
|
|
100
|
+
| `--source PATH` | atom dir | Project source root for file resolution |
|
|
101
|
+
| `--ask TEXT` | -- | Headless mode: ask a question and print the answer |
|
|
102
|
+
| `--provider STR` | anthropic | LLM provider (anthropic or openai) |
|
|
103
|
+
| `--model STR` | claude-opus-4-8 | LLM model name |
|
|
104
|
+
| `--api-key STR` | env var | API key for the LLM provider |
|
|
105
|
+
| `--base-url STR` | -- | Custom API base URL for OpenAI-compatible endpoints |
|
|
106
|
+
| `--reports-dir PATH` | .chen/chennai-reports | Directory for markdown reports and BOM files |
|
|
107
|
+
| `--no-thinking` | false | Omit thinking blocks from the LLM request |
|
|
108
|
+
| `--effort STR` | high | Reasoning effort (low, medium, high, xhigh, max) |
|
|
109
|
+
|
|
110
|
+
### Environment variables
|
|
111
|
+
|
|
112
|
+
- `CHENNAI_ENGINE`: Path to the engine binary. Overrides auto-detection.
|
|
113
|
+
- `ANTHROPIC_API_KEY`: API key for Anthropic provider.
|
|
114
|
+
- `OPENAI_API_KEY`: API key for OpenAI-compatible providers.
|
|
115
|
+
- `CDXGEN_CMD`: Path to the cdxgen binary.
|
|
116
|
+
- `ATOM_CMD`: Path to the atom CLI binary (e.g. `/path/to/atom/atom.sh`).
|
|
117
|
+
- `CHENNAI_DEBUG`: Set to any value to enable resolver diagnostics.
|
|
118
|
+
|
|
119
|
+
### Platform support
|
|
120
|
+
|
|
121
|
+
| Platform | Architecture | Engine | TUI |
|
|
122
|
+
| -------- | ----------------- | ------------- | ------------- |
|
|
123
|
+
| Linux | x64 (glibc) | native | native |
|
|
124
|
+
| Linux | arm64 (glibc) | native | native |
|
|
125
|
+
| Linux | x64 (musl/Alpine) | native (musl) | native (musl) |
|
|
126
|
+
| macOS | Apple Silicon | native | native |
|
|
127
|
+
| Windows | x64 | native | native |
|
|
128
|
+
|
|
129
|
+
Other platforms fall back to the JAR distribution for the engine while the TUI runs natively where available.
|
|
130
|
+
|
|
131
|
+
## Architecture
|
|
132
|
+
|
|
133
|
+
chennai has two processes that communicate over NDJSON on stdin/stdout.
|
|
134
|
+
|
|
135
|
+
### Engine (chennai-engine)
|
|
136
|
+
|
|
137
|
+
The engine is a Scala 3 application built with the chen library. It reads atom files and runs queries against the atom file. It is distributed as a GraalVM native-image binary on supported platforms and as a JAR distribution elsewhere.
|
|
138
|
+
|
|
139
|
+
The engine accepts NDJSON request lines on stdin and writes NDJSON response lines to stdout. Each request has an id, a command, and arguments. The engine opens an atom file on startup and keeps it in memory for the duration of the session.
|
|
140
|
+
|
|
141
|
+
### TUI (chennai)
|
|
142
|
+
|
|
143
|
+
The TUI is a Rust application using ratatui and crossterm. It spawns the engine as a child process, opens the atom file, and presents a three-panel interface: a summary panel with row counts, a REPL panel for input, and an output panel for results. The TUI auto-detects the engine binary location -- by default it looks for chennai-engine in the same directory, then checks the standard development build paths.
|
|
144
|
+
|
|
145
|
+
## User interface
|
|
146
|
+
|
|
147
|
+
The TUI has three panels navigated with Tab and Shift+Tab.
|
|
148
|
+
|
|
149
|
+
### Summary panel
|
|
150
|
+
|
|
151
|
+
Displays the atom summary: language, version, and row counts for files, methods, calls, literals, namespaces, annotations, config files, and dependencies. Pressing Enter on a row runs a query for that type and displays results in the output panel.
|
|
152
|
+
|
|
153
|
+
### REPL panel
|
|
154
|
+
|
|
155
|
+
A command-line input at the bottom of the screen. Enter queries here to run them. The REPL supports:
|
|
156
|
+
|
|
157
|
+
- Command labels: `files`, `methods`, `calls`, `external methods`, `internal methods`, `namespaces`, `annotations`, `imports`, `literals`, `config files`
|
|
158
|
+
- DSL expressions: any chen DSL expression such as `atom.method.name(".*Handler")`
|
|
159
|
+
- Tag queries: `atom.tag.name("crypto.*")`
|
|
160
|
+
- Flow presets: `dataflows`, `reachables`, `cryptos`
|
|
161
|
+
- Custom flow DSL: expressions containing `reachableByFlows` or `.df(`
|
|
162
|
+
- DSL prefix with `=`: forces raw eval mode, e.g. `=atom.call.code(".*query.*")`
|
|
163
|
+
|
|
164
|
+
Tab completions are available with Ctrl+Space. Command history is accessible with Up and Down arrows.
|
|
165
|
+
|
|
166
|
+
### Output panel
|
|
167
|
+
|
|
168
|
+
Displays query results. The output panel has several display modes:
|
|
169
|
+
|
|
170
|
+
**Table view**: Query results are shown as a scrollable, sortable table. Columns are sorted by pressing 1 through 9. Filter rows with /. Press Enter to open a detail pane for a row. The detail pane shows node properties, a child table (arguments, parameters, locals), and source code side by side.
|
|
171
|
+
|
|
172
|
+
**Flow view**: Data-flow paths are displayed as master-detail groups. Each group represents a source to sink path. Groups can be collapsed. Sub-flows are shown with `s` and mitigated flows (passing through sanitizers or validators) can be hidden with `m`.
|
|
173
|
+
|
|
174
|
+
**Agent view**: The AI agent output is rendered as a streaming transcript with thinking blocks, tool calls, and results. Tools results that return flow data can be installed into the flow view for further exploration.
|
|
175
|
+
|
|
176
|
+
### Keybindings
|
|
177
|
+
|
|
178
|
+
| Key | Action |
|
|
179
|
+
| ---------------------------- | ----------------------------------- |
|
|
180
|
+
| q | Quit |
|
|
181
|
+
| Tab / Shift+Tab | Cycle panels forward/backward |
|
|
182
|
+
| Up/Down (or j/k) | Navigate lists, scroll output |
|
|
183
|
+
| PageUp/PageDown (or b/Space) | Page through lists |
|
|
184
|
+
| Enter | Open detail or execute command |
|
|
185
|
+
| / | Filter table in output panel |
|
|
186
|
+
| 1-9 | Sort table by column |
|
|
187
|
+
| s | Toggle sub-flows in flow view |
|
|
188
|
+
| m | Toggle mitigated flows in flow view |
|
|
189
|
+
| d / r | Run dataflows or reachables preset |
|
|
190
|
+
| Ctrl+S | Save report to markdown file |
|
|
191
|
+
|
|
192
|
+
## DSL queries and traversals
|
|
193
|
+
|
|
194
|
+
The full chen DSL is available through the eval command. The reference documentation for all traversal steps and node types is maintained in the chen repository at `docs/TRAVERSAL.md`. The atom repository at `docs/lessons/` has nineteen lessons covering everything from frontend setup through data-flow analysis and graph algorithms.
|
|
195
|
+
|
|
196
|
+
The entry point for queries is `atom`, representing the root of the Code Property Graph. Common starters include `atom.file`, `atom.method`, `atom.call`, `atom.literal`, `atom.tag`, and `atom.namespace`. These can be chained with steps like `.name(".*Handler")`, `.isExternal(false)`, `.callee`, `.caller`, `.cfgNext`, `.dominatedBy`, and many others.
|
|
197
|
+
|
|
198
|
+
For type-based filtering the DSL provides `.isCall`, `.isLiteral`, `.isIdentifier`, `.isMethod`, `.isFile`, and similar predicates on any AST node.
|
|
199
|
+
|
|
200
|
+
## Data-flow analysis
|
|
201
|
+
|
|
202
|
+
chennai supports three flow presets accessible from the REPL or the summary panel:
|
|
203
|
+
|
|
204
|
+
- `dataflows`: Runs reachableByFlows from framework-input tagged sources to framework-output tagged sinks.
|
|
205
|
+
- `reachables`: Runs reachableByFlows using the default reachables configuration.
|
|
206
|
+
- `cryptos`: Runs crypto-related data-flow analysis.
|
|
207
|
+
|
|
208
|
+
Custom flow queries use the `reachableByFlows` or `df` steps. For example:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
atom.call.name(".*execute.*").reachableByFlows(atom.call.name(".*getInput.*"))
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The flow view groups results by source-sink pair, shows each step in the path, and indicates methods that have sanitization or validation tags. The engine supports two reaching-definitions solvers: the default Flux solver (low-allocation, fast on large methods) and a classic solver for comparison.
|
|
215
|
+
|
|
216
|
+
## Graph algorithms
|
|
217
|
+
|
|
218
|
+
Run via the `algo` command in the engine. Available algorithms:
|
|
219
|
+
|
|
220
|
+
- **PageRank** (`centrality`): Ranks methods by PageRank score and in-degree centrality on the call graph.
|
|
221
|
+
- **Strongly connected components** (`scc`): Finds recursion clusters in the call graph.
|
|
222
|
+
- **Topological sort** (`toposort`): Orders methods callee-before-caller, grouped by SCC.
|
|
223
|
+
- **Dominator tree** (`dominators`): Computes immediate dominators per method over CFG edges.
|
|
224
|
+
- **Interprocedural paths** (`paths`): Finds BFS-limited call paths between a source and target method.
|
|
225
|
+
|
|
226
|
+
## AI agent
|
|
227
|
+
|
|
228
|
+
When an API key is configured (via environment variable or config file), the slash commands activate. These are entered in the REPL panel like any other command.
|
|
229
|
+
|
|
230
|
+
### Slash commands
|
|
231
|
+
|
|
232
|
+
| Command | Purpose |
|
|
233
|
+
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
234
|
+
| `/security-review` | Reachability-grounded vulnerability analysis. Finds source-to-sink taint paths, ranks findings, and produces a markdown report. Uses `high` effort. |
|
|
235
|
+
| `/code-review` | Review code changes (diff or methods) for correctness and security. Uses `medium` effort. |
|
|
236
|
+
| `/explain` | Natural-language explanation of a method, data-flow, or code structure. Uses `medium` effort. |
|
|
237
|
+
| `/trace` | Prove or disprove a taint path between a given source and sink. Uses `high` effort. |
|
|
238
|
+
| `/help` | List available slash commands. |
|
|
239
|
+
|
|
240
|
+
Free text input (without a slash) is also routed to the agent for ad hoc questions. The agent has access to thirteen tools: atom_summary, atom_query, atom_dsl_eval, atom_flows, atom_flows_through, atom_detail, atom_algorithms, bom_query, ripgrep, read_file, git_diff, git_log, and git_show.
|
|
241
|
+
|
|
242
|
+
The agent uses the streaming API of the configured provider and renders thinking blocks, text deltas, and tool calls in real time. Tool results are truncated to 48 KiB to stay within model token limits. When the agent produces flow results they are automatically installed into the flow view for interactive exploration.
|
|
243
|
+
|
|
244
|
+
Agent reports can be saved to markdown with Ctrl+S. Reports include the full transcript, token usage, and any generated data-flow paths or analysis results.
|
|
245
|
+
|
|
246
|
+
### Offline operation
|
|
247
|
+
|
|
248
|
+
The agent is optional. When no API key is set, chennai operates as a fully offline code analysis tool with all query, data-flow, and algorithm features available through the REPL. The slash commands and free-text agent features only activate when a provider is configured.
|
|
249
|
+
|
|
250
|
+
## Configuration
|
|
251
|
+
|
|
252
|
+
The agent can be configured with a TOML file at `~/.config/chennai/config.toml`, environment variables, or CLI flags. CLI flags take precedence over environment variables, which take precedence over the config file.
|
|
253
|
+
|
|
254
|
+
Example config file:
|
|
255
|
+
|
|
256
|
+
```toml
|
|
257
|
+
provider = "openai"
|
|
258
|
+
model = "deepseek-chat"
|
|
259
|
+
api_key = "sk-..."
|
|
260
|
+
base_url = "https://api.deepseek.com"
|
|
261
|
+
effort = "high"
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Developing
|
|
265
|
+
|
|
266
|
+
### Prerequisites
|
|
267
|
+
|
|
268
|
+
- Rust toolchain (edition 2024)
|
|
269
|
+
- Scala 3.8.4 and sbt
|
|
270
|
+
- GraalVM Community Edition 25 for native-image builds (optional for development)
|
|
271
|
+
|
|
272
|
+
### Build
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
# Engine (staged JAR distribution)
|
|
276
|
+
(cd engine && sbt stage)
|
|
277
|
+
|
|
278
|
+
# Engine native image (requires GraalVM)
|
|
279
|
+
bash ci/native-image.sh
|
|
280
|
+
|
|
281
|
+
# TUI
|
|
282
|
+
(cd tui && cargo build --release)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Build all npm packages locally
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
bash wrapper/nodejs/scripts/build-local.sh
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## License
|
|
292
|
+
|
|
293
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { platform as _platform } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { locateChennaiBinary, describeChennaiSearch } from "./resolve.js";
|
|
9
|
+
|
|
10
|
+
const isWin = _platform() === "win32";
|
|
11
|
+
const dirName = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const selfPJson = JSON.parse(readFileSync(join(dirName, "package.json"), "utf8"));
|
|
13
|
+
|
|
14
|
+
export const CHENNAI_VERSION = selfPJson.version;
|
|
15
|
+
|
|
16
|
+
const provider = locateChennaiBinary();
|
|
17
|
+
|
|
18
|
+
export const executeChennai = (chennaiArgs) => {
|
|
19
|
+
if (!provider) {
|
|
20
|
+
console.error("Error: The '@appthreat/chennai' package was not installed correctly or is unsupported on this platform.");
|
|
21
|
+
console.error("Please verify your installation and make sure optional dependencies are not blocked.");
|
|
22
|
+
try {
|
|
23
|
+
const diag = describeChennaiSearch();
|
|
24
|
+
console.error(
|
|
25
|
+
`\n[chennai] resolution diagnostics:\n` +
|
|
26
|
+
` dispatcher dir: ${diag.selfDir}\n` +
|
|
27
|
+
` platform=${diag.platform} arch=${diag.arch} libc=${diag.libc}\n` +
|
|
28
|
+
` preferred package: ${diag.preferredPkg}\n` +
|
|
29
|
+
` paths checked (${diag.attempts.length}):`
|
|
30
|
+
);
|
|
31
|
+
for (const a of diag.attempts) {
|
|
32
|
+
console.error(` [${a.exists ? "found" : "missing"}] (${a.pkg}, ${a.kind}) ${a.path}`);
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.error(`[chennai] failed to produce diagnostics: ${e?.message || e}`);
|
|
36
|
+
}
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const cwd = process.env.CHENNAI_CWD || process.cwd();
|
|
41
|
+
const timeout = process.env.CHENNAI_TIMEOUT ? parseInt(process.env.CHENNAI_TIMEOUT, 10) : undefined;
|
|
42
|
+
|
|
43
|
+
const env = { ...process.env };
|
|
44
|
+
if (provider.enginePath) {
|
|
45
|
+
env.CHENNAI_ENGINE = provider.enginePath;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = spawnSync(provider.binPath, chennaiArgs, {
|
|
49
|
+
encoding: "utf-8",
|
|
50
|
+
env,
|
|
51
|
+
cwd,
|
|
52
|
+
stdio: "inherit",
|
|
53
|
+
timeout,
|
|
54
|
+
});
|
|
55
|
+
process.exit(result.status !== null ? result.status : 1);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (process.argv[1]) {
|
|
59
|
+
const argv = process.argv.slice(2);
|
|
60
|
+
executeChennai(argv);
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appthreat/chennai",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Interactive terminal UI for exploring AppThreat atom files with AI agent",
|
|
5
|
+
"exports": "./index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"chennai": "index.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
5
13
|
"repository": {
|
|
6
14
|
"type": "git",
|
|
7
15
|
"url": "git+https://github.com/AppThreat/chennai.git"
|
|
8
16
|
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"code",
|
|
19
|
+
"analysis",
|
|
20
|
+
"threat",
|
|
21
|
+
"tui",
|
|
22
|
+
"cpg"
|
|
23
|
+
],
|
|
9
24
|
"author": "Team AppThreat <cloud@appthreat.com>",
|
|
10
25
|
"license": "MIT",
|
|
11
26
|
"bugs": {
|
|
12
27
|
"url": "https://github.com/AppThreat/chennai/issues"
|
|
13
28
|
},
|
|
14
|
-
"homepage": "https://github.com/AppThreat/chennai#readme"
|
|
29
|
+
"homepage": "https://github.com/AppThreat/chennai#readme",
|
|
30
|
+
"files": [
|
|
31
|
+
"index.js",
|
|
32
|
+
"resolve.js",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"optionalDependencies": {
|
|
37
|
+
"@appthreat/chennai-linux-amd64": "0.3.0",
|
|
38
|
+
"@appthreat/chennai-linux-arm64": "0.3.0",
|
|
39
|
+
"@appthreat/chennai-darwin-arm64": "0.3.0",
|
|
40
|
+
"@appthreat/chennai-darwin-amd64": "0.3.0",
|
|
41
|
+
"@appthreat/chennai-windows-amd64": "0.3.0",
|
|
42
|
+
"@appthreat/chennai-windows-arm64": "0.3.0",
|
|
43
|
+
"@appthreat/chennai-linux-amd64-musl": "0.3.0",
|
|
44
|
+
"@appthreat/chennai-linux-arm64-musl": "0.3.0"
|
|
45
|
+
}
|
|
15
46
|
}
|
package/resolve.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { dirname, join, sep } from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const SELF_DIR = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
const NATIVE_PACKAGES = new Set([
|
|
9
|
+
"@appthreat/chennai-linux-amd64",
|
|
10
|
+
"@appthreat/chennai-linux-arm64",
|
|
11
|
+
"@appthreat/chennai-darwin-arm64",
|
|
12
|
+
"@appthreat/chennai-windows-amd64",
|
|
13
|
+
"@appthreat/chennai-linux-amd64-musl",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export function getLinuxLibc() {
|
|
17
|
+
if (process.platform !== "linux") return null;
|
|
18
|
+
try {
|
|
19
|
+
const report = process.report?.getReport();
|
|
20
|
+
if (typeof report === "object" && report?.header) {
|
|
21
|
+
if (report.header.glibcVersionRuntime) return "glibc";
|
|
22
|
+
}
|
|
23
|
+
} catch (_) {}
|
|
24
|
+
try {
|
|
25
|
+
if (fs.existsSync("/etc/alpine-release")) return "musl";
|
|
26
|
+
} catch (_) {}
|
|
27
|
+
try {
|
|
28
|
+
const out = execSync("ldd --version", { stdio: ["ignore", "pipe", "ignore"] }).toString();
|
|
29
|
+
if (out.includes("musl")) return "musl";
|
|
30
|
+
} catch (_) {}
|
|
31
|
+
return "glibc";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveChennaiProvider(opts = {}) {
|
|
35
|
+
const platform = opts.platform || process.platform;
|
|
36
|
+
const arch = opts.arch || process.arch;
|
|
37
|
+
let libc = opts.libc;
|
|
38
|
+
if (platform === "linux" && !libc) libc = getLinuxLibc();
|
|
39
|
+
|
|
40
|
+
let preferredPkg = null;
|
|
41
|
+
let kind = "jar";
|
|
42
|
+
|
|
43
|
+
if (platform === "win32") {
|
|
44
|
+
if (arch === "x64") {
|
|
45
|
+
preferredPkg = "@appthreat/chennai-windows-amd64";
|
|
46
|
+
kind = "native";
|
|
47
|
+
} else {
|
|
48
|
+
preferredPkg = "@appthreat/chennai-win32-arm64";
|
|
49
|
+
kind = "jar";
|
|
50
|
+
}
|
|
51
|
+
} else if (platform === "darwin") {
|
|
52
|
+
if (arch === "arm64") {
|
|
53
|
+
preferredPkg = "@appthreat/chennai-darwin-arm64";
|
|
54
|
+
kind = "native";
|
|
55
|
+
} else {
|
|
56
|
+
preferredPkg = "@appthreat/chennai-darwin-amd64";
|
|
57
|
+
kind = "jar";
|
|
58
|
+
}
|
|
59
|
+
} else if (platform === "linux") {
|
|
60
|
+
if (arch === "x64") {
|
|
61
|
+
preferredPkg = libc === "musl"
|
|
62
|
+
? "@appthreat/chennai-linux-amd64-musl"
|
|
63
|
+
: "@appthreat/chennai-linux-amd64";
|
|
64
|
+
kind = "native";
|
|
65
|
+
} else if (arch === "arm64") {
|
|
66
|
+
preferredPkg = libc === "musl"
|
|
67
|
+
? "@appthreat/chennai-linux-arm64-musl"
|
|
68
|
+
: "@appthreat/chennai-linux-arm64";
|
|
69
|
+
kind = libc === "musl" ? "jar" : "native";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!preferredPkg) {
|
|
74
|
+
preferredPkg = "@appthreat/chennai-jar";
|
|
75
|
+
kind = "jar";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { preferredPkg, kind, platform, arch, libc };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readSelfVersion() {
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(fs.readFileSync(join(SELF_DIR, "package.json"), "utf8")).version;
|
|
84
|
+
} catch (_) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function staticGlobalRoots() {
|
|
90
|
+
const roots = new Set();
|
|
91
|
+
const env = process.env;
|
|
92
|
+
if (env.GLOBAL_NODE_MODULES_PATH) roots.add(env.GLOBAL_NODE_MODULES_PATH);
|
|
93
|
+
const prefix = env.npm_config_prefix || env.PREFIX;
|
|
94
|
+
if (prefix) {
|
|
95
|
+
roots.add(join(prefix, "lib", "node_modules"));
|
|
96
|
+
roots.add(join(prefix, "node_modules"));
|
|
97
|
+
}
|
|
98
|
+
const launch = process.argv[1];
|
|
99
|
+
if (launch) {
|
|
100
|
+
const binDir = dirname(launch);
|
|
101
|
+
roots.add(join(binDir, "..", "lib", "node_modules"));
|
|
102
|
+
roots.add(join(binDir, "node_modules"));
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const execDir = dirname(process.execPath);
|
|
106
|
+
roots.add(join(execDir, "..", "lib", "node_modules"));
|
|
107
|
+
roots.add(join(execDir, "node_modules"));
|
|
108
|
+
} catch (_) {}
|
|
109
|
+
return [...roots];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let _queriedGlobalRoots;
|
|
113
|
+
function queriedGlobalRoots() {
|
|
114
|
+
if (_queriedGlobalRoots) return _queriedGlobalRoots;
|
|
115
|
+
const roots = new Set();
|
|
116
|
+
for (const cmd of ["npm root -g", "pnpm root -g"]) {
|
|
117
|
+
try {
|
|
118
|
+
const out = execSync(cmd, { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" }).trim();
|
|
119
|
+
if (out) roots.add(out);
|
|
120
|
+
} catch (_) {}
|
|
121
|
+
}
|
|
122
|
+
_queriedGlobalRoots = [...roots];
|
|
123
|
+
return _queriedGlobalRoots;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function candidatePackageDirs(pkgName, searchOpts = {}) {
|
|
127
|
+
const folder = pkgName.split("/")[1];
|
|
128
|
+
const version = readSelfVersion();
|
|
129
|
+
const parts = SELF_DIR.split(sep);
|
|
130
|
+
const dirs = [];
|
|
131
|
+
|
|
132
|
+
dirs.push(join(SELF_DIR, "node_modules", "@appthreat", folder));
|
|
133
|
+
|
|
134
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
135
|
+
if (parts[i] === "node_modules") {
|
|
136
|
+
const root = parts.slice(0, i + 1).join(sep) || sep;
|
|
137
|
+
dirs.push(join(root, "@appthreat", folder));
|
|
138
|
+
if (version) {
|
|
139
|
+
dirs.push(join(root, ".pnpm", `@appthreat+${folder}@${version}`, "node_modules", "@appthreat", folder));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const pnpmMarker = `${sep}.pnpm${sep}`;
|
|
145
|
+
const pnpmIdx = SELF_DIR.indexOf(pnpmMarker);
|
|
146
|
+
if (pnpmIdx !== -1 && version) {
|
|
147
|
+
const base = SELF_DIR.slice(0, pnpmIdx);
|
|
148
|
+
dirs.push(join(base, ".pnpm", `@appthreat+${folder}@${version}`, "node_modules", "@appthreat", folder));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const globalRoots = staticGlobalRoots();
|
|
152
|
+
if (searchOpts.includeQueriedGlobals) globalRoots.push(...queriedGlobalRoots());
|
|
153
|
+
for (const root of globalRoots) {
|
|
154
|
+
dirs.push(join(root, "@appthreat", folder));
|
|
155
|
+
if (version) {
|
|
156
|
+
dirs.push(join(root, ".pnpm", `@appthreat+${folder}@${version}`, "node_modules", "@appthreat", folder));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return [...new Set(dirs)];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function describeChennaiSearch(opts = {}) {
|
|
164
|
+
const { preferredPkg, platform, arch, libc } = resolveChennaiProvider(opts);
|
|
165
|
+
const packagesToTry = [preferredPkg];
|
|
166
|
+
if (preferredPkg !== "@appthreat/chennai-jar") {
|
|
167
|
+
packagesToTry.push("@appthreat/chennai-jar");
|
|
168
|
+
}
|
|
169
|
+
const attempts = [];
|
|
170
|
+
for (const pkg of packagesToTry) {
|
|
171
|
+
const isNative = NATIVE_PACKAGES.has(pkg);
|
|
172
|
+
for (const pkgDir of candidatePackageDirs(pkg, { includeQueriedGlobals: true })) {
|
|
173
|
+
const checkPath = isNative
|
|
174
|
+
? join(pkgDir, "bin", platform === "win32" ? "chennai.exe" : "chennai")
|
|
175
|
+
: join(pkgDir, "plugins");
|
|
176
|
+
attempts.push({ pkg, kind: isNative ? "native" : "jar", path: checkPath, exists: fs.existsSync(checkPath) });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return { selfDir: SELF_DIR, platform, arch, libc, preferredPkg, attempts };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function locateChennaiBinary(opts = {}) {
|
|
183
|
+
const debug = !!process.env.CHENNAI_DEBUG;
|
|
184
|
+
const { preferredPkg, platform } = resolveChennaiProvider(opts);
|
|
185
|
+
|
|
186
|
+
if (debug) {
|
|
187
|
+
console.error(`[chennai] resolver self dir: ${SELF_DIR}`);
|
|
188
|
+
console.error(`[chennai] platform=${platform} preferred=${preferredPkg}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const packagesToTry = [preferredPkg];
|
|
192
|
+
if (preferredPkg !== "@appthreat/chennai-jar") {
|
|
193
|
+
packagesToTry.push("@appthreat/chennai-jar");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const tryPackage = (pkg, pkgDir) => {
|
|
197
|
+
const isNative = NATIVE_PACKAGES.has(pkg);
|
|
198
|
+
const exeName = platform === "win32" ? "chennai.exe" : "chennai";
|
|
199
|
+
const binaryPath = join(pkgDir, "bin", exeName);
|
|
200
|
+
if (debug) console.error(`[chennai] check ${binaryPath} -> ${fs.existsSync(binaryPath)}`);
|
|
201
|
+
if (fs.existsSync(binaryPath)) {
|
|
202
|
+
const engineName = platform === "win32" ? "chennai-engine.exe" : "chennai-engine";
|
|
203
|
+
let enginePath = null;
|
|
204
|
+
if (isNative) {
|
|
205
|
+
enginePath = join(pkgDir, "bin", engineName);
|
|
206
|
+
enginePath = fs.existsSync(enginePath) ? enginePath : null;
|
|
207
|
+
} else {
|
|
208
|
+
// JAR fallback: engine launcher lives under plugins/bin/
|
|
209
|
+
const jarEnginePath = join(pkgDir, "plugins", "bin", engineName);
|
|
210
|
+
enginePath = fs.existsSync(jarEnginePath) ? jarEnginePath : null;
|
|
211
|
+
}
|
|
212
|
+
return { kind: isNative ? "native" : "jar", pkg, binPath: binaryPath, enginePath, pkgDir };
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
for (const includeQueriedGlobals of [false, true]) {
|
|
218
|
+
for (const pkg of packagesToTry) {
|
|
219
|
+
for (const pkgDir of candidatePackageDirs(pkg, { includeQueriedGlobals })) {
|
|
220
|
+
const found = tryPackage(pkg, pkgDir);
|
|
221
|
+
if (found) return found;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|