neuralos 3.9.4 → 3.9.6

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.
Files changed (68) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +48 -347
  3. package/cli.js +78 -0
  4. package/engine.js +124 -0
  5. package/index.js +160 -0
  6. package/package.json +37 -43
  7. package/test.js +20 -0
  8. package/tools.js +47 -0
  9. package/bin/gybackend.cjs +0 -398530
  10. package/plugins/agentspan-bridge/agentspan-bridge.extreme.spec.ts +0 -328
  11. package/plugins/agentspan-bridge/agentspan-bridge.phase2.extreme.spec.ts +0 -282
  12. package/plugins/agentspan-bridge/conductorClient.mjs +0 -182
  13. package/plugins/agentspan-bridge/index.mjs +0 -389
  14. package/plugins/agentspan-bridge/playbookToWorkflowDef.mjs +0 -209
  15. package/plugins/agentspan-bridge/plugin.json +0 -27
  16. package/plugins/fraudops/index.d.mts +0 -9
  17. package/plugins/fraudops/index.mjs +0 -185
  18. package/plugins/fraudops/plugin.json +0 -10
  19. package/plugins/iam-connector/index.d.mts +0 -10
  20. package/plugins/iam-connector/index.mjs +0 -229
  21. package/plugins/iam-connector/plugin.json +0 -10
  22. package/plugins/mitmproxy-bridge/index.d.mts +0 -20
  23. package/plugins/mitmproxy-bridge/index.mjs +0 -256
  24. package/plugins/mitmproxy-bridge/plugin.json +0 -17
  25. package/plugins/monid-bridge/index.mjs +0 -228
  26. package/plugins/monid-bridge/monid-bridge.extreme.spec.mjs +0 -75
  27. package/plugins/monid-bridge/plugin.json +0 -15
  28. package/plugins/netdata-rterm/index.mjs +0 -229
  29. package/plugins/netdata-rterm/netdata-rterm.extreme.spec.ts +0 -252
  30. package/plugins/netdata-rterm/plugin.json +0 -22
  31. package/plugins/netexec-bridge/index.d.mts +0 -28
  32. package/plugins/netexec-bridge/index.mjs +0 -325
  33. package/plugins/netexec-bridge/plugin.json +0 -20
  34. package/plugins/neuralos/index.mjs +0 -137
  35. package/plugins/neuralos/neuralos.extreme.spec.mts +0 -386
  36. package/plugins/neuralos/neuralosEngine.mjs +0 -369
  37. package/plugins/neuralos/plugin.json +0 -15
  38. package/plugins/numbat-bridge/index.mjs +0 -229
  39. package/plugins/numbat-bridge/numbat-bridge.extreme.spec.mjs +0 -148
  40. package/plugins/numbat-bridge/plugin.json +0 -21
  41. package/plugins/patch-manager/index.d.mts +0 -10
  42. package/plugins/patch-manager/index.mjs +0 -288
  43. package/plugins/patch-manager/plugin.json +0 -10
  44. package/plugins/promptfoo-redteam/index.d.mts +0 -15
  45. package/plugins/promptfoo-redteam/index.mjs +0 -277
  46. package/plugins/promptfoo-redteam/plugin.json +0 -16
  47. package/plugins/reactorpro-bridge/index.mjs +0 -499
  48. package/plugins/reactorpro-bridge/plugin.json +0 -26
  49. package/plugins/reactorpro-bridge/reactorpro-bridge.extreme.spec.mts +0 -409
  50. package/plugins/reactorpro-bridge/reactorproAgent.mjs +0 -399
  51. package/plugins/request-router/index.d.mts +0 -9
  52. package/plugins/request-router/index.mjs +0 -239
  53. package/plugins/request-router/plugin.json +0 -10
  54. package/plugins/sample-k8s-slo/index.ts +0 -58
  55. package/plugins/sample-k8s-slo/plugin.json +0 -11
  56. package/plugins/sop-assistant/index.d.mts +0 -9
  57. package/plugins/sop-assistant/index.mjs +0 -325
  58. package/plugins/sop-assistant/plugin.json +0 -10
  59. package/plugins/synapse-bridge/index.mjs +0 -484
  60. package/plugins/synapse-bridge/plugin.json +0 -29
  61. package/plugins/synapse-bridge/synapse-bridge.extreme.spec.mjs +0 -204
  62. package/plugins/synapse-bridge/synapseAgent.extreme.spec.mjs +0 -236
  63. package/plugins/synapse-bridge/synapseAgent.mjs +0 -254
  64. package/plugins/web-intel/index.mjs +0 -381
  65. package/plugins/web-intel/plugin.json +0 -28
  66. package/plugins/web-intel/sidecar.mjs +0 -134
  67. package/plugins/web-intel/web-intel.extreme.spec.ts +0 -263
  68. package/plugins/web-intel/wigoloClient.mjs +0 -145
@@ -1,229 +0,0 @@
1
- /**
2
- * iam-connector plugin — IAM integration for RTerm.
3
- *
4
- * User/group management, role assignment, access review, and audit trail for
5
- * Active Directory, local users, and service accounts. Covers user lifecycle
6
- * (create, disable, offboard), group membership, and access compliance.
7
- * Commands are built for both Linux (id, groups, usermod) and Windows
8
- * (Get-LocalUser, Get-LocalGroupMember, Disable-LocalUser).
9
- */
10
-
11
- // --- Pure: build user info command ---
12
- export function buildUserInfoCommand(username, os = 'linux') {
13
- const o = String(os).toLowerCase()
14
- if (o === 'windows' || o === 'win32') {
15
- return `powershell -Command "Get-LocalUser -Name '${username}' | Select-Object Name,Enabled,LastLogon,PasswordExpires,LockedOut,Description | Format-List"`
16
- }
17
- return `id '${username}' 2>/dev/null && groups '${username}' 2>/dev/null && passwd -S '${username}' 2>/dev/null`
18
- }
19
-
20
- // --- Pure: build user groups command ---
21
- export function buildUserGroupsCommand(username, os = 'linux') {
22
- const o = String(os).toLowerCase()
23
- if (o === 'windows' || o === 'win32') {
24
- return `powershell -Command "Get-LocalGroupMember -Group Administrators | Where-Object {$_.Name -like '*${username}*'} | Select-Object Name; Get-LocalUser -Name '${username}' | Select-Object Name,Enabled"`
25
- }
26
- return `groups '${username}' 2>/dev/null && id -Gn '${username}' 2>/dev/null`
27
- }
28
-
29
- // --- Pure: build disable user command ---
30
- export function buildDisableUserCommand(username, os = 'linux') {
31
- const o = String(os).toLowerCase()
32
- if (o === 'windows' || o === 'win32') {
33
- return `powershell -Command "Disable-LocalUser -Name '${username}'; Get-LocalUser -Name '${username}' | Select-Object Name,Enabled"`
34
- }
35
- return `usermod -L '${username}' 2>/dev/null && echo "user ${username} disabled" || echo "failed to disable ${username}"`
36
- }
37
-
38
- // --- Pure: build access review command (list all users + groups) ---
39
- export function buildAccessReviewCommand(os = 'linux') {
40
- const o = String(os).toLowerCase()
41
- if (o === 'windows' || o === 'win32') {
42
- return 'powershell -Command "Get-LocalUser | Select-Object Name,Enabled,LastLogon | Format-Table -AutoSize; Write-Host \'---\'; Get-LocalGroup | Select-Object Name | Format-Table -AutoSize"'
43
- }
44
- return 'cut -d: -f1 /etc/passwd | while read u; do echo "$u: $(id -Gn $u 2>/dev/null)"; done 2>/dev/null | head -50'
45
- }
46
-
47
- // --- Pure: parse user info output ---
48
- export function parseUserInfo(output, os = 'linux') {
49
- const o = String(os ?? 'linux').toLowerCase()
50
- const text = String(output ?? '')
51
- const info = { username: '', groups: [], enabled: true, locked: false }
52
-
53
- if (o === 'windows' || o === 'win32') {
54
- const nameMatch = text.match(/Name\s+:\s+(\S+)/)
55
- const enabledMatch = text.match(/Enabled\s+:\s+(True|False)/i)
56
- const lockedMatch = text.match(/LockedOut\s+:\s+(True|False)/i)
57
- if (nameMatch) info.username = nameMatch[1]
58
- if (enabledMatch) info.enabled = enabledMatch[1].toLowerCase() === 'true'
59
- if (lockedMatch) info.locked = lockedMatch[1].toLowerCase() === 'true'
60
- return info
61
- }
62
-
63
- // Linux: parse id + groups + passwd -S output
64
- const lines = text.split(/\r?\n/)
65
- for (const line of lines) {
66
- const l = line.trim()
67
- const idMatch = l.match(/uid=\d+\((\S+)\)/)
68
- if (idMatch) info.username = idMatch[1]
69
- const groupsMatch = l.match(/groups=(.+)/)
70
- if (groupsMatch) {
71
- // groups=1000(john),27(sudo) — split by comma, then extract names from NNN(name) format
72
- info.groups = groupsMatch[1]
73
- .split(',')
74
- .map((g) => g.replace(/\d+\(([^)]+)\)/, '$1').trim())
75
- .filter(Boolean)
76
- }
77
- if (l.includes(' L ')) info.locked = true
78
- }
79
- return info
80
- }
81
-
82
- // --- Pure: parse access review output into a user list ---
83
- export function parseAccessReview(output, os = 'linux') {
84
- const o = String(os ?? 'linux').toLowerCase()
85
- const text = String(output ?? '')
86
- const users = []
87
-
88
- if (o === 'windows' || o === 'win32') {
89
- const lines = text.split(/\r?\n/)
90
- for (const line of lines) {
91
- const l = line.trim()
92
- const match = l.match(/^(\S+)\s+(True|False)\s+/)
93
- if (match) {
94
- users.push({ username: match[1], enabled: match[2].toLowerCase() === 'true' })
95
- }
96
- }
97
- return users
98
- }
99
-
100
- // Linux: parse "user: group1 group2" lines
101
- const lines = text.split(/\r?\n/)
102
- for (const line of lines) {
103
- const l = line.trim()
104
- const match = l.match(/^(\S+):\s*(.*)/)
105
- if (match) {
106
- users.push({ username: match[1], groups: match[2].split(/\s+/).filter(Boolean) })
107
- }
108
- }
109
- return users
110
- }
111
-
112
- // --- Pure: check if a user has privileged access ---
113
- export function isPrivileged(userInfo, privilegedGroups = ['sudo', 'wheel', 'admin', 'root', 'Administrators']) {
114
- const groups = Array.isArray(userInfo?.groups) ? userInfo.groups : []
115
- const privs = Array.isArray(privilegedGroups) ? privilegedGroups : []
116
- return groups.some((g) => privs.some((pg) => String(g).toLowerCase().includes(String(pg).toLowerCase())))
117
- }
118
-
119
- // --- Plugin entry ---
120
- export function register(ctx) {
121
- const { registerTool, registerTrigger, registerPanel, exec, readLedger, log } = ctx
122
-
123
- // Tool: iam_user_info — get user info
124
- registerTool({
125
- name: 'iam_user_info',
126
- description: 'Get user information (username, groups, enabled status, locked status) for a user on a host.',
127
- params: {
128
- username: { type: 'string', description: 'Username to query' },
129
- host: { type: 'string', description: 'Host to query' },
130
- os: { type: 'string', description: 'OS type: linux, windows' },
131
- },
132
- handler: async (params) => {
133
- const { username, host, os = 'linux' } = params ?? {}
134
- if (!username || !host) return { error: 'username and host are required' }
135
- const cmd = buildUserInfoCommand(username, os)
136
- log(`[iam-connector] querying user ${username} on ${host}`)
137
- const output = await exec(cmd, { host })
138
- const info = parseUserInfo(output, os)
139
- return { username, host, os, ...info, privileged: isPrivileged(info) }
140
- },
141
- })
142
-
143
- // Tool: iam_user_groups — get user group memberships
144
- registerTool({
145
- name: 'iam_user_groups',
146
- description: 'Get group memberships for a user on a host.',
147
- params: {
148
- username: { type: 'string', description: 'Username to query' },
149
- host: { type: 'string', description: 'Host to query' },
150
- os: { type: 'string', description: 'OS type' },
151
- },
152
- handler: async (params) => {
153
- const { username, host, os = 'linux' } = params ?? {}
154
- if (!username || !host) return { error: 'username and host are required' }
155
- const cmd = buildUserGroupsCommand(username, os)
156
- const output = await exec(cmd, { host })
157
- const info = parseUserInfo(output, os)
158
- return { username, host, groups: info.groups, privileged: isPrivileged(info) }
159
- },
160
- })
161
-
162
- // Tool: iam_disable_user — disable a user account (requires approval)
163
- registerTool({
164
- name: 'iam_disable_user',
165
- description: 'Disable a user account on a host. This is a destructive operation — requires approval. Returns the result.',
166
- params: {
167
- username: { type: 'string', description: 'Username to disable' },
168
- host: { type: 'string', description: 'Host to disable on' },
169
- os: { type: 'string', description: 'OS type' },
170
- },
171
- handler: async (params) => {
172
- const { username, host, os = 'linux' } = params ?? {}
173
- if (!username || !host) return { error: 'username and host are required' }
174
- const cmd = buildDisableUserCommand(username, os)
175
- log(`[iam-connector] disabling user ${username} on ${host}`)
176
- const output = await exec(cmd, { host })
177
- const info = parseUserInfo(output, os)
178
- return { username, host, disabled: !info.enabled, output: output.slice(0, 500) }
179
- },
180
- })
181
-
182
- // Tool: iam_access_review — review all user access on a host
183
- registerTool({
184
- name: 'iam_access_review',
185
- description: 'Review all user accounts and group memberships on a host. Identifies privileged users.',
186
- params: {
187
- host: { type: 'string', description: 'Host to review' },
188
- os: { type: 'string', description: 'OS type' },
189
- },
190
- handler: async (params) => {
191
- const { host, os = 'linux' } = params ?? {}
192
- if (!host) return { error: 'host is required' }
193
- const cmd = buildAccessReviewCommand(os)
194
- const output = await exec(cmd, { host })
195
- const users = parseAccessReview(output, os)
196
- const privilegedUsers = users.filter((u) => isPrivileged({ groups: u.groups ?? [] }))
197
- return { host, totalUsers: users.length, privilegedUsers: privilegedUsers.length, users: users.slice(0, 50), privilegedUsers: privilegedUsers.map((u) => u.username) }
198
- },
199
- })
200
-
201
- // Trigger: iam_privileged_change — fires when a privileged user's access changes
202
- registerTrigger({
203
- name: 'iam_privileged_change',
204
- description: 'Fires when a privileged user account is modified (disabled, group change). Use for compliance monitoring.',
205
- match: (event) => {
206
- if (event?.source !== 'iam-connector') return false
207
- return event.labels?.privileged === true
208
- },
209
- action: 'propose-change',
210
- })
211
-
212
- // Panel: iam-access-dashboard — IAM access dashboard
213
- registerPanel({
214
- name: 'iam-access-dashboard',
215
- title: 'IAM Access Dashboard',
216
- render: (data) => {
217
- const users = Array.isArray(data) ? data : []
218
- const privileged = users.filter((u) => isPrivileged({ groups: u.groups ?? [] }))
219
- const rows = users.map((u) =>
220
- `<tr><td>${u.username}</td><td>${(u.groups ?? []).join(', ')}</td><td>${u.enabled !== false ? '✅' : '❌'}</td><td>${isPrivileged({ groups: u.groups ?? [] }) ? '🔐' : ''}</td></tr>`
221
- ).join('')
222
- return `<div class="iam-access-dashboard"><h3>IAM Access Dashboard</h3><p>Total users: ${users.length} | Privileged: ${privileged.length}</p><table><thead><tr><th>User</th><th>Groups</th><th>Enabled</th><th>Privileged</th></tr></thead><tbody>${rows}</tbody></table></div>`
223
- },
224
- })
225
-
226
- log('[iam-connector] registered: 4 tools, 1 trigger, 1 panel')
227
- }
228
-
229
- export default { register, buildUserInfoCommand, buildUserGroupsCommand, buildDisableUserCommand, buildAccessReviewCommand, parseUserInfo, parseAccessReview, isPrivileged }
@@ -1,10 +0,0 @@
1
- {
2
- "name": "iam-connector",
3
- "version": "1.0.0",
4
- "description": "IAM integration for RTerm — user/group management, role assignment, access review, and audit trail for Active Directory, local users, and service accounts. Covers user lifecycle (create, disable, offboard), group membership, and access compliance.",
5
- "entry": "index.mjs",
6
- "tools": ["iam_user_info", "iam_user_groups", "iam_disable_user", "iam_access_review"],
7
- "triggers": ["iam_privileged_change"],
8
- "panels": ["iam-access-dashboard"],
9
- "permissions": ["exec", "readLedger:incidents"]
10
- }
@@ -1,20 +0,0 @@
1
- // mitmproxy-bridge plugin type declarations
2
- export function register(ctx: any): void
3
- export function buildMitmCommand(opts: {
4
- mode?: 'regular' | 'reverse'
5
- listenPort?: number
6
- upstreamTarget?: string
7
- flowsFile: string
8
- filterExpr?: string
9
- extraArgs?: string[]
10
- }): { cmd: string; args: string[] }
11
- export function parseFlows(rawFlows: unknown): {
12
- total: number
13
- byHost: Record<string, { count: number; methods: Record<string, number> }>
14
- byStatus: Record<string, number>
15
- requests: Array<{ host: string; method: string; path: string; status: string; contentType: string }>
16
- error?: string
17
- }
18
- export function detectSecrets(text: string | null | undefined): Array<{ kind: string; count: number; preview: string }>
19
- export function isHostAllowed(host: string, allowlist: string[]): boolean
20
- export default any
@@ -1,256 +0,0 @@
1
- /**
2
- * mitmproxy-bridge — mitmproxy traffic capture for RTerm.
3
- *
4
- * Two use modes:
5
- * 1. Agent self-inspection — capture what the LLM actually sends over the
6
- * wire (verify no secrets leak in prompts, inspect provider requests).
7
- * 2. Authorized interception — capture traffic on hosts you administer,
8
- * following the APerf deploy pattern (run on the RTerm host as a sidecar).
9
- *
10
- * Pure + injectable: process spawning and file reads are injected; command
11
- * building, flow parsing, and secret detection are pure and fully testable.
12
- */
13
-
14
- // --- Pure: build the mitmdump command line ---
15
-
16
- export function buildMitmCommand(opts) {
17
- const {
18
- mode = 'regular', // regular | reverse
19
- listenPort = 8080,
20
- upstreamTarget, // for reverse mode: the upstream host:port
21
- flowsFile, // where to write the flow file
22
- filterExpr, // optional mitmproxy filter expression
23
- extraArgs = [],
24
- } = opts || {}
25
-
26
- if (!flowsFile) throw new Error('buildMitmCommand needs flowsFile')
27
- const args = ['mitmdump']
28
-
29
- if (mode === 'reverse') {
30
- if (!upstreamTarget) throw new Error('reverse mode needs upstreamTarget')
31
- args.push('--mode', `reverse:${upstreamTarget}`)
32
- } else {
33
- args.push('--mode', 'regular')
34
- }
35
- args.push('--listen-port', String(listenPort))
36
- args.push('-w', flowsFile)
37
- if (filterExpr) args.push('--set', `flow_detail=0`, '--set', `intercept=${filterExpr}`)
38
- for (const a of extraArgs) args.push(String(a))
39
- return { cmd: 'mitmdump', args }
40
- }
41
-
42
- // --- Pure: parse a mitmproxy flows file (JSON lines from mitmdump --flow-detail) ---
43
-
44
- /**
45
- * Parse mitmproxy flow records (the JSON array/dump format) into a compact
46
- * summary: per-host request counts, methods, status codes, and content types.
47
- */
48
- export function parseFlows(rawFlows) {
49
- const summary = {
50
- total: 0,
51
- byHost: {},
52
- byStatus: {},
53
- requests: [],
54
- }
55
-
56
- let flows = rawFlows
57
- if (typeof rawFlows === 'string') {
58
- try { flows = JSON.parse(rawFlows) } catch { return { ...summary, error: 'unparseable flows' } }
59
- }
60
- if (!Array.isArray(flows)) return { ...summary, error: 'flows is not an array' }
61
-
62
- for (const f of flows) {
63
- const host = String(f?.request?.host ?? f?.host ?? 'unknown')
64
- const method = String(f?.request?.method ?? f?.method ?? '?')
65
- const status = f?.response?.status_code ?? f?.status_code
66
- const path = String(f?.request?.path ?? f?.path ?? '')
67
- const contentType = String(f?.response?.headers?.['content-type'] ?? '')
68
-
69
- summary.total += 1
70
- const byHost = summary.byHost[host] || (summary.byHost[host] = { count: 0, methods: {} })
71
- byHost.count += 1
72
- byHost.methods[method] = (byHost.methods[method] || 0) + 1
73
-
74
- const statusKey = status !== undefined && status !== null ? String(status) : 'no-response'
75
- summary.byStatus[statusKey] = (summary.byStatus[statusKey] || 0) + 1
76
-
77
- if (summary.requests.length < 100) {
78
- summary.requests.push({ host, method, path: path.slice(0, 120), status: statusKey, contentType: contentType.slice(0, 60) })
79
- }
80
- }
81
-
82
- return summary
83
- }
84
-
85
- // --- Pure: detect secret-looking content in captured request bodies ---
86
-
87
- const SECRET_PATTERNS = [
88
- { re: /\bsk-[A-Za-z0-9]{20,}/g, name: 'openai-style-key' },
89
- { re: /\bghp_[A-Za-z0-9]{30,}/g, name: 'github-token' },
90
- { re: /\bAKIA[A-Z0-9]{16}\b/g, name: 'aws-access-key' },
91
- { re: /\bxox[baprs]-[A-Za-z0-9-]{10,}/g, name: 'slack-token' },
92
- // JWT: header/payload/signature. Real JWT headers are commonly exactly 20
93
- // chars total (17 after the "eyJ" prefix) — {17,} catches those; {20,} was
94
- // an FN that missed short-header JWTs.
95
- { re: /\beyJ[A-Za-z0-9_-]{17,}\.eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}\b/g, name: 'jwt' },
96
- { re: /\b(?:password|passwd|pwd|secret|token|api[_-]?key)\s*[:=]\s*['"]?[^\s'"]{8,}/gi, name: 'credential-assignment' },
97
- ]
98
-
99
- export function detectSecrets(text) {
100
- const findings = []
101
- if (!text || typeof text !== 'string') return findings
102
- for (const { re, name } of SECRET_PATTERNS) {
103
- const matches = text.match(re)
104
- if (matches) {
105
- findings.push({
106
- kind: name,
107
- count: matches.length,
108
- // redact: show only a prefix so the secret itself never lands in output
109
- preview: matches[0].slice(0, 8) + '…',
110
- })
111
- }
112
- }
113
- return findings
114
- }
115
-
116
- // --- Pure: which hosts are allowed to be intercepted ---
117
-
118
- export function isHostAllowed(host, allowlist) {
119
- if (!Array.isArray(allowlist) || allowlist.length === 0) return false
120
- const h = String(host || '').toLowerCase()
121
- return allowlist.some((a) => {
122
- const pat = String(a || '').toLowerCase()
123
- if (pat === h) return true
124
- // allow "*.example.com" style suffix matching
125
- if (pat.startsWith('*.')) return h.endsWith(pat.slice(1))
126
- return false
127
- })
128
- }
129
-
130
- // --- Plugin entry ---
131
-
132
- export function register(ctx) {
133
- const { registerTool, registerPanel, log, spawnProcess } = ctx
134
-
135
- let mitmChild = null
136
- let lastSummary = null
137
-
138
- registerTool({
139
- name: 'mitm_start',
140
- description: 'Start a mitmproxy capture. Mode "regular" listens as an HTTP proxy on listenPort; mode "reverse" forwards to upstreamTarget. Flows are written to a file for later analysis. Only start captures for hosts you are authorized to intercept.',
141
- params: {
142
- mode: { type: 'string', description: '"regular" (proxy) or "reverse" (forward to upstreamTarget)' },
143
- listenPort: { type: 'number', description: 'Port to listen on (default 8080)' },
144
- upstreamTarget: { type: 'string', description: 'For reverse mode: host:port to forward to' },
145
- allowlist: { type: 'array', description: 'Hosts/patterns authorized for interception, e.g. ["api.example.com", "*.internal"]' },
146
- },
147
- handler: async (params) => {
148
- if (typeof spawnProcess !== 'function') {
149
- return { error: 'mitmproxy-bridge requires process spawning, which is not available in this RTerm build.' }
150
- }
151
- if (mitmChild) {
152
- return { error: 'A mitmproxy capture is already running. Stop it with mitm_stop first.' }
153
- }
154
-
155
- const mode = params?.mode === 'reverse' ? 'reverse' : 'regular'
156
- if (mode === 'reverse' && !params?.upstreamTarget) {
157
- return { error: 'reverse mode needs upstreamTarget (host:port).' }
158
- }
159
- // Governance: an allowlist must be provided — no unbounded interception.
160
- const allowlist = Array.isArray(params?.allowlist) ? params.allowlist : []
161
- if (mode === 'reverse' && allowlist.length === 0) {
162
- return { error: 'reverse mode requires an allowlist of authorized hosts. Unbounded interception is not permitted.' }
163
- }
164
-
165
- const fs = await import('node:fs')
166
- const os = await import('node:os')
167
- const path = await import('node:path')
168
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mitm-rterm-'))
169
- const flowsFile = path.join(dir, 'flows.mitm')
170
-
171
- let plan
172
- try {
173
- plan = buildMitmCommand({
174
- mode,
175
- listenPort: params?.listenPort,
176
- upstreamTarget: params?.upstreamTarget,
177
- flowsFile,
178
- })
179
- } catch (e) {
180
- return { error: e?.message ?? String(e) }
181
- }
182
-
183
- log(`[mitmproxy] starting ${mode} capture on :${params?.listenPort ?? 8080} → ${flowsFile}`)
184
- try {
185
- mitmChild = spawnProcess(plan.cmd, plan.args, { stdio: 'pipe', detached: false })
186
- } catch (e) {
187
- return { error: `failed to spawn mitmdump: ${e?.message ?? e}. Is mitmproxy installed (pip install mitmproxy / brew install mitmproxy)?` }
188
- }
189
- if (!mitmChild) return { error: 'spawnProcess returned no child process' }
190
-
191
- return {
192
- started: true,
193
- mode,
194
- listenPort: params?.listenPort ?? 8080,
195
- flowsFile,
196
- note: mode === 'regular'
197
- ? 'Configure clients to use this host as HTTP proxy. Stop with mitm_stop.'
198
- : `Point clients at :${params?.listenPort ?? 8080}; traffic forwards to ${params?.upstreamTarget}. Stop with mitm_stop.`,
199
- }
200
- },
201
- })
202
-
203
- registerTool({
204
- name: 'mitm_stop',
205
- description: 'Stop the running mitmproxy capture and summarize the captured flows.',
206
- params: {},
207
- handler: async () => {
208
- if (!mitmChild) return { error: 'No mitmproxy capture is running.' }
209
- try { mitmChild.kill?.() } catch { /* ignore */ }
210
- mitmChild = null
211
- return { stopped: true }
212
- },
213
- })
214
-
215
- registerTool({
216
- name: 'mitm_flows',
217
- description: 'Summarize captured flows from a mitmproxy flow dump: per-host counts, methods, status codes, and secret detection on request bodies. Use mitmdump -nr <flowsFile> --flow-detail 3 to export, or pass pre-parsed flows.',
218
- params: {
219
- flows: { type: 'array', description: 'Pre-parsed flow records [{request:{host,method,path}, response:{status_code}}]' },
220
- },
221
- handler: async (params) => {
222
- const flows = Array.isArray(params?.flows) ? params.flows : []
223
- if (flows.length === 0) {
224
- return { error: 'No flows given. Export flows from the capture first (mitmdump -nr <file>) and pass the parsed records.' }
225
- }
226
- const summary = parseFlows(flows)
227
-
228
- // Secret scan across request paths + any body text present in the records.
229
- const textBlob = flows
230
- .map((f) => `${f?.request?.path ?? ''} ${typeof f?.request?.body === 'string' ? f.request.body : ''}`)
231
- .join('\n')
232
- .slice(0, 200_000)
233
- const secrets = detectSecrets(textBlob)
234
-
235
- lastSummary = { summary, secrets }
236
- log(`[mitmproxy] summarized ${summary.total} flows across ${Object.keys(summary.byHost).length} host(s); ${secrets.length} secret pattern(s) found`)
237
- return { summary, secrets }
238
- },
239
- })
240
-
241
- registerPanel({
242
- name: 'mitmproxy-flows',
243
- title: 'Traffic Capture',
244
- render: async () => {
245
- if (!lastSummary) return '<div class="panel-section"><h3>Traffic Capture</h3><p>No flows analyzed yet. Use mitm_start / mitm_flows.</p></div>'
246
- const hostRows = Object.entries(lastSummary.summary.byHost)
247
- .slice(0, 10)
248
- .map(([h, s]) => `<tr><td>${h}</td><td>${s.count}</td></tr>`)
249
- .join('')
250
- return `<div class="panel-section"><h3>Traffic Capture — ${lastSummary.summary.total} flows</h3>
251
- <table><tr><th>host</th><th>requests</th></tr>${hostRows}</table>
252
- ${lastSummary.secrets.length ? `<p class="warn">⚠ ${lastSummary.secrets.length} secret pattern(s) detected</p>` : '<p>No secrets detected</p>'}
253
- </div>`
254
- },
255
- })
256
- }
@@ -1,17 +0,0 @@
1
- {
2
- "name": "mitmproxy-bridge",
3
- "version": "1.0.0",
4
- "description": "mitmproxy traffic capture for RTerm — agent self-inspection (what the LLM sends over the wire) and authorized interception on administered hosts. Flow summaries + secret-pattern detection on captured bodies.",
5
- "entry": "index.mjs",
6
- "tools": [
7
- "mitm_start",
8
- "mitm_stop",
9
- "mitm_flows"
10
- ],
11
- "panels": [
12
- "mitmproxy-flows"
13
- ],
14
- "permissions": [
15
- "spawnProcess"
16
- ]
17
- }