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,252 +0,0 @@
1
- import {
2
- parseNetdataAlert, mapSeverity, buildFingerprint, toTriggerEvent, correlateWithRterm, register,
3
- } from './index.mjs'
4
-
5
- const cases: Array<{ name: string; run: () => void | Promise<void> }> = []
6
- function test(n: string, r: () => void | Promise<void>) { cases.push({ name: n, run: r }) }
7
-
8
- // ---- parseNetdataAlert ----
9
- test('parse: alert notification with all fields', () => {
10
- const payload = {
11
- message: 'CPU usage is 95%', alert: 'cpu_usage', info: 'CPU utilization too high',
12
- chart: 'system.cpu', context: 'system.cpu', space: 'prod-cluster', family: 'cpu',
13
- class: 'Error', severity: 'critical', date: '2026-07-22T10:00:00Z', duration: '5m',
14
- additional_active_critical_alerts: 2, additional_active_warning_alerts: 1,
15
- alert_url: 'https://app.netdata.cloud/alert/123',
16
- }
17
- const p = parseNetdataAlert(payload)
18
- if (!p || p.kind !== 'alert') throw new Error('should parse alert')
19
- if (p.alert !== 'cpu_usage') throw new Error('alert name')
20
- if (p.severity !== 'critical') throw new Error('severity')
21
- if (p.chart !== 'system.cpu') throw new Error('chart')
22
- if (p.additionalCritical !== 2) throw new Error('additional critical')
23
- if (p.host !== 'prod-cluster') throw new Error('host from space')
24
- if (p.alertUrl !== 'https://app.netdata.cloud/alert/123') throw new Error('alert url')
25
- })
26
-
27
- test('parse: warning severity', () => {
28
- const p = parseNetdataAlert({ alert: 'disk_space', severity: 'warning', message: 'Disk 80% full', space: 'web-01' })
29
- if (!p || p.severity !== 'warning') throw new Error('should be warning')
30
- })
31
-
32
- test('parse: clear severity (alert resolved)', () => {
33
- const p = parseNetdataAlert({ alert: 'cpu_usage', severity: 'clear', message: 'CPU back to normal', space: 'web-01' })
34
- if (!p || p.severity !== 'clear') throw new Error('should be clear')
35
- })
36
-
37
- test('parse: reachability notification (node down)', () => {
38
- const p = parseNetdataAlert({ message: 'Node unreachable', node: 'web-02', space: 'prod', status: 'down', date: '2026-07-22T10:00:00Z', duration: '2m' })
39
- if (!p || p.kind !== 'reachability') throw new Error('should parse reachability')
40
- if (p.status !== 'down') throw new Error('status')
41
- if (p.host !== 'web-02') throw new Error('host from node')
42
- })
43
-
44
- test('parse: reachability notification (node up)', () => {
45
- const p = parseNetdataAlert({ node: 'web-02', status: 'up', date: '2026-07-22T10:05:00Z' })
46
- if (!p || p.status !== 'up') throw new Error('should be up')
47
- })
48
-
49
- test('parse: null for invalid payload', () => {
50
- if (parseNetdataAlert(null) !== null) throw new Error('null payload')
51
- if (parseNetdataAlert({}) !== null) throw new Error('empty object')
52
- if (parseNetdataAlert('not an object') !== null) throw new Error('string')
53
- if (parseNetdataAlert({ foo: 'bar' }) !== null) throw new Error('missing required fields')
54
- })
55
-
56
- // ---- mapSeverity ----
57
- test('mapSeverity: critical -> critical', () => {
58
- if (mapSeverity('critical') !== 'critical') throw new Error('critical')
59
- })
60
- test('mapSeverity: warning -> warning', () => {
61
- if (mapSeverity('warning') !== 'warning') throw new Error('warning')
62
- })
63
- test('mapSeverity: clear -> info', () => {
64
- if (mapSeverity('clear') !== 'info') throw new Error('clear should map to info')
65
- })
66
- test('mapSeverity: unknown -> info', () => {
67
- if (mapSeverity('unknown') !== 'info') throw new Error('unknown')
68
- })
69
-
70
- // ---- buildFingerprint ----
71
- test('buildFingerprint: alert fingerprint', () => {
72
- const p = parseNetdataAlert({ alert: 'cpu_usage', severity: 'critical', space: 'web-01' })
73
- const fp = buildFingerprint(p)
74
- if (fp !== 'netdata:web-01:cpu_usage:critical') throw new Error(`got ${fp}`)
75
- })
76
- test('buildFingerprint: reachability fingerprint', () => {
77
- const p = parseNetdataAlert({ node: 'web-02', status: 'down' })
78
- const fp = buildFingerprint(p)
79
- if (fp !== 'netdata:reachability:web-02:down') throw new Error(`got ${fp}`)
80
- })
81
- test('buildFingerprint: empty for null', () => {
82
- if (buildFingerprint(null) !== '') throw new Error('should be empty')
83
- })
84
-
85
- // ---- toTriggerEvent ----
86
- test('toTriggerEvent: alert -> trigger event with correct severity', () => {
87
- const p = parseNetdataAlert({ alert: 'disk_full', severity: 'critical', space: 'db-01', message: 'Disk 95%', date: '2026-07-22T10:00:00Z' })
88
- const evt = toTriggerEvent(p)
89
- if (!evt) throw new Error('should produce event')
90
- if (evt.source !== 'netdata') throw new Error('source')
91
- if (evt.severity !== 'critical') throw new Error('severity')
92
- if (!evt.title.includes('disk_full')) throw new Error('title')
93
- if (!evt.title.includes('db-01')) throw new Error('title host')
94
- if (evt.labels.host !== 'db-01') throw new Error('labels host')
95
- if (evt.labels.alert !== 'disk_full') throw new Error('labels alert')
96
- })
97
-
98
- test('toTriggerEvent: reachability down -> critical', () => {
99
- const p = parseNetdataAlert({ node: 'web-03', status: 'down', date: '2026-07-22T10:00:00Z' })
100
- const evt = toTriggerEvent(p)
101
- if (!evt || evt.severity !== 'critical') throw new Error('down should be critical')
102
- if (!evt.title.includes('DOWN')) throw new Error('title')
103
- })
104
-
105
- test('toTriggerEvent: reachability up -> info', () => {
106
- const p = parseNetdataAlert({ node: 'web-03', status: 'up', date: '2026-07-22T10:00:00Z' })
107
- const evt = toTriggerEvent(p)
108
- if (!evt || evt.severity !== 'info') throw new Error('up should be info')
109
- })
110
-
111
- test('toTriggerEvent: null parsed -> null event', () => {
112
- if (toTriggerEvent(null) !== null) throw new Error('should be null')
113
- })
114
-
115
- // ---- correlateWithRterm ----
116
- test('correlate: with metrics + incidents', () => {
117
- const p = parseNetdataAlert({ alert: 'cpu_usage', severity: 'critical', space: 'web-01', additional_active_critical_alerts: 3 })
118
- const mockMetrics = { snapshot: (host: string) => ({ host, cpuUsagePercent: 95, memoryUsagePercent: 70 }) }
119
- const mockIncidents = { list: () => [
120
- { title: 'web-01 disk full', affected: ['web-01'], status: 'open' },
121
- { title: 'web-02 network issue', affected: ['web-02'], status: 'open' },
122
- { title: 'web-01 resolved issue', affected: ['web-01'], status: 'resolved' },
123
- ] }
124
- const result = correlateWithRterm(p, mockMetrics as any, mockIncidents as any)
125
- if (!result.recentMetrics || result.recentMetrics.cpuUsagePercent !== 95) throw new Error('metrics')
126
- if (result.openIncidents.length !== 1) throw new Error(`expected 1 open incident, got ${result.openIncidents.length}`)
127
- if (!result.correlation.includes('cpu_usage')) throw new Error('correlation should mention alert')
128
- if (!result.correlation.includes('disk full')) throw new Error('correlation should mention incident')
129
- if (!result.correlation.includes('3 additional critical')) throw new Error('correlation should mention additional alerts')
130
- })
131
-
132
- test('correlate: no prior context', () => {
133
- const p = parseNetdataAlert({ alert: 'mem_usage', severity: 'warning', space: 'new-host' })
134
- const result = correlateWithRterm(p, null, null)
135
- if (result.recentMetrics !== null) throw new Error('no metrics')
136
- if (result.openIncidents.length !== 0) throw new Error('no incidents')
137
- if (!result.correlation.includes('No prior RTerm context')) throw new Error('should say no context')
138
- })
139
-
140
- test('correlate: null parsed -> empty', () => {
141
- const result = correlateWithRterm(null, null, null)
142
- if (result.recentMetrics !== null || result.openIncidents.length !== 0) throw new Error('should be empty')
143
- })
144
-
145
- // ---- register (plugin lifecycle) ----
146
- test('register: registers 2 tools, 2 triggers, 1 panel', () => {
147
- const tools: any[] = [], triggers: any[] = [], panels: any[] = [], logs: string[] = []
148
- register({
149
- registerTool: (t) => tools.push(t),
150
- registerTrigger: (t) => triggers.push(t),
151
- registerPanel: (p) => panels.push(p),
152
- exec: async () => '',
153
- readLedger: () => ({}),
154
- log: (line: string) => logs.push(line),
155
- } as any)
156
- if (tools.length !== 2) throw new Error(`expected 2 tools, got ${tools.length}`)
157
- if (triggers.length !== 2) throw new Error(`expected 2 triggers, got ${triggers.length}`)
158
- if (panels.length !== 1) throw new Error(`expected 1 panel, got ${panels.length}`)
159
- if (!tools.some((t) => t.name === 'netdata_alert_summary')) throw new Error('missing alert_summary tool')
160
- if (!tools.some((t) => t.name === 'netdata_correlate')) throw new Error('missing correlate tool')
161
- if (!triggers.some((t) => t.name === 'netdata_critical_alert')) throw new Error('missing critical trigger')
162
- if (!triggers.some((t) => t.name === 'netdata_warning_alert')) throw new Error('missing warning trigger')
163
- if (!panels.some((p) => p.name === 'netdata-alert-feed')) throw new Error('missing alert feed panel')
164
- if (!logs.some((l) => l.includes('registered'))) throw new Error('should log registration')
165
- })
166
-
167
- test('register: critical trigger matches critical events only', () => {
168
- const triggers: any[] = []
169
- register({
170
- registerTool: () => {}, registerTrigger: (t) => triggers.push(t), registerPanel: () => {},
171
- exec: async () => '', readLedger: () => ({}), log: () => {},
172
- } as any)
173
- const critTrigger = triggers.find((t) => t.name === 'netdata_critical_alert')
174
- if (!critTrigger) throw new Error('missing critical trigger')
175
- if (!critTrigger.match({ source: 'netdata', severity: 'critical' })) throw new Error('should match critical')
176
- if (critTrigger.match({ source: 'netdata', severity: 'warning' })) throw new Error('should NOT match warning')
177
- if (critTrigger.match({ source: 'other', severity: 'critical' })) throw new Error('should NOT match non-netdata')
178
- if (critTrigger.match({})) throw new Error('should NOT match empty')
179
- })
180
-
181
- test('register: warning trigger matches warning events only', () => {
182
- const triggers: any[] = []
183
- register({
184
- registerTool: () => {}, registerTrigger: (t) => triggers.push(t), registerPanel: () => {},
185
- exec: async () => '', readLedger: () => ({}), log: () => {},
186
- } as any)
187
- const warnTrigger = triggers.find((t) => t.name === 'netdata_warning_alert')
188
- if (!warnTrigger) throw new Error('missing warning trigger')
189
- if (!warnTrigger.match({ source: 'netdata', severity: 'warning' })) throw new Error('should match warning')
190
- if (warnTrigger.match({ source: 'netdata', severity: 'critical' })) throw new Error('should NOT match critical')
191
- })
192
-
193
- test('register: panel renders alert rows', () => {
194
- const panels: any[] = []
195
- register({
196
- registerTool: () => {}, registerTrigger: () => {}, registerPanel: (p) => panels.push(p),
197
- exec: async () => '', readLedger: () => ({}), log: () => {},
198
- } as any)
199
- const panel = panels[0]
200
- const html = panel.render([
201
- { host: 'web-01', alert: 'cpu_high', severity: 'critical', date: '2026-07-22' },
202
- { host: 'web-02', alert: 'disk_full', severity: 'warning', date: '2026-07-22' },
203
- ])
204
- if (!html.includes('cpu_high') || !html.includes('disk_full')) throw new Error('should contain alert names')
205
- if (!html.includes('<table>')) throw new Error('should render table')
206
- })
207
-
208
- test('register: panel renders empty feed', () => {
209
- const panels: any[] = []
210
- register({
211
- registerTool: () => {}, registerTrigger: () => {}, registerPanel: (p) => panels.push(p),
212
- exec: async () => '', readLedger: () => ({}), log: () => {},
213
- } as any)
214
- const html = panels[0].render(null)
215
- if (!html.includes('Netdata Alerts')) throw new Error('should have title even when empty')
216
- })
217
-
218
- test('register: netdata_correlate tool handles invalid payload', async () => {
219
- const tools: any[] = []
220
- register({
221
- registerTool: (t) => tools.push(t), registerTrigger: () => {}, registerPanel: () => {},
222
- exec: async () => '', readLedger: () => ({}), log: () => {},
223
- } as any)
224
- const correlateTool = tools.find((t) => t.name === 'netdata_correlate')
225
- const result = await correlateTool.handler({ alert: { foo: 'bar' } })
226
- if (!result.error) throw new Error('should return error for invalid payload')
227
- })
228
-
229
- test('register: netdata_correlate tool correlates valid payload', async () => {
230
- const tools: any[] = []
231
- register({
232
- registerTool: (t) => tools.push(t), registerTrigger: () => {}, registerPanel: () => {},
233
- exec: async () => '', readLedger: () => null, log: () => {},
234
- } as any)
235
- const correlateTool = tools.find((t) => t.name === 'netdata_correlate')
236
- const result = await correlateTool.handler({
237
- alert: { alert: 'cpu_usage', severity: 'critical', space: 'web-01', message: 'CPU 95%' },
238
- })
239
- if (!result.parsed) throw new Error('should return parsed alert')
240
- if (result.parsed.alert !== 'cpu_usage') throw new Error('alert name')
241
- })
242
-
243
- async function main() {
244
- let pass = 0, fail = 0
245
- for (const c of cases) {
246
- try { await c.run(); pass++; console.log(`PASS ${c.name}`) }
247
- catch (e: any) { fail++; console.log(`FAIL ${c.name}: ${e?.message ?? e}`) }
248
- }
249
- console.log(`\n${pass}/${cases.length} passed, ${fail} failed`)
250
- if (fail > 0) process.exit(1)
251
- }
252
- void main()
@@ -1,22 +0,0 @@
1
- {
2
- "name": "netdata-rterm",
3
- "version": "1.0.0",
4
- "description": "Netdata integration for RTerm — ingests Netdata alert webhooks as triggers, correlates anomalies with changes/incidents for agent RCA + auto-remediation. Bridges Netdata's per-second monitoring + ML anomaly detection with RTerm's AI agent and SRE pillar.",
5
- "entry": "index.mjs",
6
- "tools": [
7
- "netdata_alert_summary",
8
- "netdata_correlate"
9
- ],
10
- "triggers": [
11
- "netdata_critical_alert",
12
- "netdata_warning_alert"
13
- ],
14
- "panels": [
15
- "netdata-alert-feed"
16
- ],
17
- "permissions": [
18
- "exec",
19
- "readLedger:metrics",
20
- "readLedger:incidents"
21
- ]
22
- }
@@ -1,28 +0,0 @@
1
- // netexec-bridge plugin type declarations
2
- export function register(ctx: any): void
3
- export function buildNetexecCommand(opts: {
4
- protocol: string
5
- targets: string
6
- action: string
7
- extraArgs?: string[]
8
- username?: string
9
- passwordRef?: string
10
- domain?: string
11
- timeoutSec?: number
12
- }): { cmd: string; args: string[] }
13
- export function validateTargets(targets: string | null | undefined, allowlist: string[] | null | undefined): { ok: boolean; reason?: string; targets?: string[] }
14
- export function parseNetexecOutput(raw: string | null | undefined): {
15
- hosts: Array<{ ip: string; hostname: string; status: string; detail?: string }>
16
- authSuccess: Array<{ protocol: string; ip: string; port: number; hostname: string; detail: string; pwned: boolean }>
17
- authFailed: Array<{ protocol: string; ip: string; port: number; hostname: string; detail: string }>
18
- errors: string[]
19
- raw: string
20
- }
21
- export function buildSprayPlan(opts: {
22
- targets: string
23
- usernames: string[]
24
- attemptsPerUser?: number
25
- delayMs?: number
26
- jitterMs?: number
27
- }): { targets: string; steps: Array<{ username: string; attempt: number; waitBeforeMs: number }>; totalAttempts: number; note: string }
28
- export default any
@@ -1,325 +0,0 @@
1
- /**
2
- * netexec-bridge — NetExec external attack-simulation for RTerm.
3
- *
4
- * Complements rmagent (which watches FROM the boxes) by testing FROM the
5
- * outside: credential validation, SMB/LDAP enumeration, spray simulation.
6
- * The purple-team loop: NetExec stages an external attack, rmagent's attest
7
- * must catch the resulting 4625s — the drill then scores both halves.
8
- *
9
- * GOVERNANCE (non-negotiable):
10
- * - Target allowlist required on every call. No unbounded scanning.
11
- * - Targets must be hosts the operator administers (same rule as rmagent).
12
- * - Credential sprays are rate-limited and jittered by default.
13
- * - The plugin never stores credentials; they resolve from the vault/env.
14
- *
15
- * Pure + injectable: process spawning injected; command building and output
16
- * parsing are pure and fully testable.
17
- */
18
-
19
- // --- Pure: build the netexec command line ---
20
-
21
- export function buildNetexecCommand(opts) {
22
- const {
23
- protocol, // smb | ldap | winrm | mssql | ssh | ftp | rdp | wmi
24
- targets, // string: CIDR, range, or comma-separated hosts
25
- action, // netexec subcommand: users, groups, shares, --sam, etc.
26
- extraArgs = [],
27
- username,
28
- passwordRef, // vault key — never the password itself
29
- domain,
30
- timeoutSec = 120,
31
- } = opts || {}
32
-
33
- if (!protocol) throw new Error('buildNetexecCommand needs a protocol (smb, ldap, winrm, ...)')
34
- if (!targets) throw new Error('buildNetexecCommand needs targets')
35
- if (!action) throw new Error('buildNetexecCommand needs an action')
36
-
37
- const args = ['netexec', protocol, String(targets)]
38
- if (username) args.push('-u', String(username))
39
- // password resolves at runtime from the vault — we pass an env var name
40
- if (passwordRef) args.push('-p', `env:${passwordRef}`)
41
- if (domain) args.push('-d', String(domain))
42
- args.push('--timeout', String(timeoutSec))
43
- const actionStr = String(action)
44
- if (actionStr.startsWith('--')) args.push(actionStr)
45
- else args.push(actionStr)
46
- for (const a of extraArgs) args.push(String(a))
47
-
48
- return { cmd: 'netexec', args }
49
- }
50
-
51
- // --- Pure: validate targets against the authorized allowlist ---
52
-
53
- export function validateTargets(targets, allowlist) {
54
- if (!targets || typeof targets !== 'string') {
55
- return { ok: false, reason: 'targets must be a string (CIDR, range, or comma-separated hosts)' }
56
- }
57
- if (!Array.isArray(allowlist) || allowlist.length === 0) {
58
- return { ok: false, reason: 'an authorized target allowlist is required — unbounded scanning is not permitted' }
59
- }
60
-
61
- const requested = targets.split(',').map((t) => t.trim()).filter(Boolean)
62
- const allowedSet = new Set(allowlist.map((a) => String(a).trim()))
63
- const denied = []
64
-
65
- for (const t of requested) {
66
- // exact host match
67
- if (allowedSet.has(t)) continue
68
- // CIDR membership: allowlist entry is a CIDR and target is a bare IP
69
- const inCidr = allowlist.some((a) => {
70
- const cidr = String(a).trim()
71
- if (!cidr.includes('/')) return false
72
- return ipInCidr(t, cidr)
73
- })
74
- if (inCidr) continue
75
- denied.push(t)
76
- }
77
-
78
- if (denied.length > 0) {
79
- return { ok: false, reason: `target(s) not in the authorized allowlist: ${denied.join(', ')}` }
80
- }
81
- return { ok: true, targets: requested }
82
- }
83
-
84
- function ipInCidr(ip, cidr) {
85
- const [base, bitsStr] = cidr.split('/')
86
- const bits = Number(bitsStr)
87
- if (!Number.isInteger(bits) || bits < 0 || bits > 32) return false
88
- const toInt = (s) => {
89
- const parts = s.split('.').map(Number)
90
- if (parts.length !== 4 || parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) return null
91
- return ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0
92
- }
93
- const ipInt = toInt(ip)
94
- const baseInt = toInt(base)
95
- if (ipInt === null || baseInt === null) return false
96
- if (bits === 0) return true
97
- const mask = (0xFFFFFFFF << (32 - bits)) >>> 0
98
- return (ipInt & mask) === (baseInt & mask)
99
- }
100
-
101
- // --- Pure: parse netexec output ---
102
-
103
- /**
104
- * netexec prints lines like:
105
- * SMB 192.168.1.10 445 HOSTNAME [+] hostname\user (Pwn3d!)
106
- * SMB 192.168.1.11 445 HOSTNAME2 [-] hostname\user:BADPW
107
- * Parse into hosts + auth results.
108
- */
109
- export function parseNetexecOutput(raw) {
110
- const result = {
111
- hosts: [],
112
- authSuccess: [],
113
- authFailed: [],
114
- errors: [],
115
- raw: typeof raw === 'string' ? raw.slice(0, 5000) : '',
116
- }
117
-
118
- if (typeof raw !== 'string') return result
119
- const lines = raw.split('\n')
120
-
121
- for (const line of lines) {
122
- const trimmed = line.trim()
123
- if (!trimmed) continue
124
-
125
- // Host status line: PROTOCOL ip port hostname [status]
126
- const m = trimmed.match(/^(\w+)\s+(\S+)\s+(\d+)\s+(\S+)\s+(.*)$/)
127
- if (!m) {
128
- if (/error|failed to|traceback/i.test(trimmed)) result.errors.push(trimmed.slice(0, 200))
129
- continue
130
- }
131
-
132
- const [, proto, ip, port, hostname, rest] = m
133
- const entry = { protocol: proto, ip, port: Number(port), hostname, detail: rest.slice(0, 200) }
134
-
135
- if (/\[\+\]/.test(rest)) {
136
- result.authSuccess.push({ ...entry, pwned: /\(Pwn3d!?\)/.test(rest) })
137
- result.hosts.push({ ip, hostname, status: 'auth-ok' })
138
- } else if (/\[-\]/.test(rest)) {
139
- result.authFailed.push(entry)
140
- result.hosts.push({ ip, hostname, status: 'auth-failed' })
141
- } else if (/\[\*\]/.test(rest)) {
142
- result.hosts.push({ ip, hostname, status: 'info', detail: rest.slice(0, 120) })
143
- } else {
144
- result.hosts.push({ ip, hostname, status: 'unknown' })
145
- }
146
- }
147
-
148
- return result
149
- }
150
-
151
- // --- Pure: build a spray plan (rate-limited, jittered) ---
152
-
153
- export function buildSprayPlan(opts) {
154
- const {
155
- targets,
156
- usernames, // array of usernames to try
157
- attemptsPerUser = 1,
158
- delayMs = 5000, // between attempts — slow by default
159
- jitterMs = 2000,
160
- } = opts || {}
161
-
162
- if (!Array.isArray(usernames) || usernames.length === 0) {
163
- throw new Error('buildSprayPlan needs usernames')
164
- }
165
- if (!targets) throw new Error('buildSprayPlan needs targets')
166
-
167
- const plan = []
168
- for (const user of usernames) {
169
- for (let i = 0; i < Math.max(1, attemptsPerUser); i++) {
170
- const jitter = Math.floor(Math.random() * Math.max(0, jitterMs))
171
- plan.push({
172
- username: String(user),
173
- attempt: i + 1,
174
- waitBeforeMs: delayMs + jitter,
175
- })
176
- }
177
- }
178
- return {
179
- targets: String(targets),
180
- steps: plan,
181
- totalAttempts: plan.length,
182
- note: `Spray plan: ${plan.length} attempt(s) across ${usernames.length} user(s), ≥${delayMs}ms between attempts (jitter ${jitterMs}ms). This is a SLOW spray by design — verify rmagent attest catches the 4625s.`,
183
- }
184
- }
185
-
186
- // --- Plugin entry ---
187
-
188
- export function register(ctx) {
189
- const { registerTool, registerTrigger, registerPanel, log, spawnProcess } = ctx
190
-
191
- let lastResult = null
192
-
193
- const runNetexec = async (plan) => {
194
- if (typeof spawnProcess !== 'function') {
195
- return { error: 'netexec-bridge requires process spawning, which is not available in this RTerm build.' }
196
- }
197
- return await new Promise((resolve) => {
198
- let stdout = ''
199
- let stderr = ''
200
- let child
201
- try {
202
- child = spawnProcess(plan.cmd, plan.args, { stdio: 'pipe' })
203
- } catch (e) {
204
- resolve({ error: `failed to spawn netexec: ${e?.message ?? e}. Install with: pip install netexec` })
205
- return
206
- }
207
- if (!child || typeof child.on !== 'function') {
208
- resolve({ error: 'spawnProcess returned no child process' })
209
- return
210
- }
211
- let settled = false
212
- const settle = (v) => { if (!settled) { settled = true; resolve(v) } }
213
- child.stdout?.on?.('data', (d) => { stdout += String(d) })
214
- child.stderr?.on?.('data', (d) => { stderr += String(d) })
215
- child.on('error', (e) => settle({ error: `netexec spawn error: ${e?.message ?? e}` }))
216
- child.on('close', (code) => settle({ code, stdout, stderr }))
217
- const t = setTimeout(() => {
218
- try { child.kill?.() } catch { /* ignore */ }
219
- settle({ error: 'netexec timed out', stdout, stderr })
220
- }, 300_000)
221
- child.on('close', () => clearTimeout(t))
222
- })
223
- }
224
-
225
- registerTool({
226
- name: 'netexec_check',
227
- description: 'Run a NetExec check against authorized targets (credential validation, enumeration). Targets MUST be in the authorized allowlist — the same hosts you administer. Results parse into per-host auth outcomes; successful auths are findings.',
228
- params: {
229
- protocol: { type: 'string', description: 'smb | ldap | winrm | mssql | ssh | ftp | rdp | wmi' },
230
- targets: { type: 'string', description: 'Target hosts (comma-separated IPs or a CIDR). Must be within the authorized allowlist.' },
231
- action: { type: 'string', description: 'NetExec action: users, groups, shares, --sam, --users, etc.' },
232
- allowlist: { type: 'array', description: 'Authorized target hosts/CIDRs. REQUIRED — no unbounded scanning.' },
233
- username: { type: 'string', description: 'Username to test' },
234
- passwordRef: { type: 'string', description: 'Vault key holding the password (never the password itself)' },
235
- domain: { type: 'string', description: 'Optional domain' },
236
- },
237
- handler: async (params) => {
238
- const { protocol, targets, action } = params || {}
239
- const allowlist = Array.isArray(params?.allowlist) ? params.allowlist : []
240
-
241
- // Governance gate FIRST.
242
- const check = validateTargets(targets, allowlist)
243
- if (!check.ok) {
244
- return { error: `Authorization failed: ${check.reason}. Only test hosts you administer.` }
245
- }
246
-
247
- let plan
248
- try {
249
- plan = buildNetexecCommand({
250
- protocol, targets, action,
251
- username: params?.username,
252
- passwordRef: params?.passwordRef,
253
- domain: params?.domain,
254
- })
255
- } catch (e) {
256
- return { error: e?.message ?? String(e) }
257
- }
258
-
259
- log(`[netexec] ${protocol} ${action} against ${check.targets.length} authorized target(s)`)
260
- const run = await runNetexec(plan)
261
- if (run?.error) return { error: run.error, stderr: run.stderr?.slice(0, 400) }
262
-
263
- const parsed = parseNetexecOutput(run.stdout)
264
- lastResult = parsed
265
-
266
- log(`[netexec] ${parsed.hosts.length} host(s), ${parsed.authSuccess.length} auth success, ${parsed.authFailed.length} auth failed`)
267
- return {
268
- hosts: parsed.hosts,
269
- authSuccess: parsed.authSuccess,
270
- authFailed: parsed.authFailed,
271
- errors: parsed.errors,
272
- note: parsed.authSuccess.length > 0
273
- ? '⚠ Successful authentication found — treat as a finding and verify rmagent attest saw the corresponding logons.'
274
- : 'No successful authentications.',
275
- }
276
- },
277
- })
278
-
279
- registerTool({
280
- name: 'netexec_spray_plan',
281
- description: 'Build a rate-limited, jittered credential-spray plan (does NOT execute it). Returns the step schedule to review before running via netexec_check. Slow by design so rmagent attest can catch the 4625 pattern.',
282
- params: {
283
- targets: { type: 'string', description: 'Target hosts (must be authorized)' },
284
- usernames: { type: 'array', description: 'Usernames to try' },
285
- attemptsPerUser: { type: 'number', description: 'Attempts per username (default 1)' },
286
- },
287
- handler: async (params) => {
288
- try {
289
- const plan = buildSprayPlan({
290
- targets: params?.targets,
291
- usernames: params?.usernames,
292
- attemptsPerUser: params?.attemptsPerUser,
293
- })
294
- return plan
295
- } catch (e) {
296
- return { error: e?.message ?? String(e) }
297
- }
298
- },
299
- })
300
-
301
- registerTrigger({
302
- name: 'netexec_auth_success',
303
- description: 'Fires when a NetExec check finds a successful authentication on an authorized target. Use for incident + rmagent correlation playbooks.',
304
- match: (event) => {
305
- if (event?.source !== 'netexec') return false
306
- return Array.isArray(event?.authSuccess) && event.authSuccess.length > 0
307
- },
308
- action: 'run-playbook',
309
- })
310
-
311
- registerPanel({
312
- name: 'netexec-results',
313
- title: 'NetExec',
314
- render: async () => {
315
- if (!lastResult) return '<div class="panel-section"><h3>NetExec</h3><p>No check run yet. Use netexec_check.</p></div>'
316
- const rows = lastResult.hosts.slice(0, 15)
317
- .map((h) => `<tr><td>${h.ip}</td><td>${h.hostname}</td><td>${h.status}</td></tr>`)
318
- .join('')
319
- return `<div class="panel-section"><h3>NetExec — ${lastResult.hosts.length} host(s)</h3>
320
- <table><tr><th>ip</th><th>hostname</th><th>status</th></tr>${rows}</table>
321
- ${lastResult.authSuccess.length ? `<p class="warn">⚠ ${lastResult.authSuccess.length} successful auth(s)</p>` : ''}
322
- </div>`
323
- },
324
- })
325
- }
@@ -1,20 +0,0 @@
1
- {
2
- "name": "netexec-bridge",
3
- "version": "1.0.0",
4
- "description": "NetExec external attack-simulation for RTerm — credential validation, SMB/LDAP enumeration, and rate-limited spray simulation against authorized targets. Complements rmagent (inside view) with the outside view; closes the purple-team loop.",
5
- "entry": "index.mjs",
6
- "tools": [
7
- "netexec_check",
8
- "netexec_spray_plan"
9
- ],
10
- "triggers": [
11
- "netexec_auth_success"
12
- ],
13
- "panels": [
14
- "netexec-results"
15
- ],
16
- "permissions": [
17
- "spawnProcess",
18
- "readLedger:incidents"
19
- ]
20
- }