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.
- package/LICENSE +202 -0
- package/README.md +48 -347
- package/cli.js +78 -0
- package/engine.js +124 -0
- package/index.js +160 -0
- package/package.json +37 -43
- package/test.js +20 -0
- package/tools.js +47 -0
- package/bin/gybackend.cjs +0 -398530
- package/plugins/agentspan-bridge/agentspan-bridge.extreme.spec.ts +0 -328
- package/plugins/agentspan-bridge/agentspan-bridge.phase2.extreme.spec.ts +0 -282
- package/plugins/agentspan-bridge/conductorClient.mjs +0 -182
- package/plugins/agentspan-bridge/index.mjs +0 -389
- package/plugins/agentspan-bridge/playbookToWorkflowDef.mjs +0 -209
- package/plugins/agentspan-bridge/plugin.json +0 -27
- package/plugins/fraudops/index.d.mts +0 -9
- package/plugins/fraudops/index.mjs +0 -185
- package/plugins/fraudops/plugin.json +0 -10
- package/plugins/iam-connector/index.d.mts +0 -10
- package/plugins/iam-connector/index.mjs +0 -229
- package/plugins/iam-connector/plugin.json +0 -10
- package/plugins/mitmproxy-bridge/index.d.mts +0 -20
- package/plugins/mitmproxy-bridge/index.mjs +0 -256
- package/plugins/mitmproxy-bridge/plugin.json +0 -17
- package/plugins/monid-bridge/index.mjs +0 -228
- package/plugins/monid-bridge/monid-bridge.extreme.spec.mjs +0 -75
- package/plugins/monid-bridge/plugin.json +0 -15
- package/plugins/netdata-rterm/index.mjs +0 -229
- package/plugins/netdata-rterm/netdata-rterm.extreme.spec.ts +0 -252
- package/plugins/netdata-rterm/plugin.json +0 -22
- package/plugins/netexec-bridge/index.d.mts +0 -28
- package/plugins/netexec-bridge/index.mjs +0 -325
- package/plugins/netexec-bridge/plugin.json +0 -20
- package/plugins/neuralos/index.mjs +0 -137
- package/plugins/neuralos/neuralos.extreme.spec.mts +0 -386
- package/plugins/neuralos/neuralosEngine.mjs +0 -369
- package/plugins/neuralos/plugin.json +0 -15
- package/plugins/numbat-bridge/index.mjs +0 -229
- package/plugins/numbat-bridge/numbat-bridge.extreme.spec.mjs +0 -148
- package/plugins/numbat-bridge/plugin.json +0 -21
- package/plugins/patch-manager/index.d.mts +0 -10
- package/plugins/patch-manager/index.mjs +0 -288
- package/plugins/patch-manager/plugin.json +0 -10
- package/plugins/promptfoo-redteam/index.d.mts +0 -15
- package/plugins/promptfoo-redteam/index.mjs +0 -277
- package/plugins/promptfoo-redteam/plugin.json +0 -16
- package/plugins/reactorpro-bridge/index.mjs +0 -499
- package/plugins/reactorpro-bridge/plugin.json +0 -26
- package/plugins/reactorpro-bridge/reactorpro-bridge.extreme.spec.mts +0 -409
- package/plugins/reactorpro-bridge/reactorproAgent.mjs +0 -399
- package/plugins/request-router/index.d.mts +0 -9
- package/plugins/request-router/index.mjs +0 -239
- package/plugins/request-router/plugin.json +0 -10
- package/plugins/sample-k8s-slo/index.ts +0 -58
- package/plugins/sample-k8s-slo/plugin.json +0 -11
- package/plugins/sop-assistant/index.d.mts +0 -9
- package/plugins/sop-assistant/index.mjs +0 -325
- package/plugins/sop-assistant/plugin.json +0 -10
- package/plugins/synapse-bridge/index.mjs +0 -484
- package/plugins/synapse-bridge/plugin.json +0 -29
- package/plugins/synapse-bridge/synapse-bridge.extreme.spec.mjs +0 -204
- package/plugins/synapse-bridge/synapseAgent.extreme.spec.mjs +0 -236
- package/plugins/synapse-bridge/synapseAgent.mjs +0 -254
- package/plugins/web-intel/index.mjs +0 -381
- package/plugins/web-intel/plugin.json +0 -28
- package/plugins/web-intel/sidecar.mjs +0 -134
- package/plugins/web-intel/web-intel.extreme.spec.ts +0 -263
- 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
|
-
}
|