rterm-backend 2.7.2 → 2.7.4
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/bin/gybackend.js +12 -1
- package/package.json +2 -1
- package/plugins/fraudops/index.d.mts +9 -0
- package/plugins/fraudops/index.mjs +184 -0
- package/plugins/fraudops/plugin.json +10 -0
- package/plugins/iam-connector/index.d.mts +10 -0
- package/plugins/iam-connector/index.mjs +225 -0
- package/plugins/iam-connector/plugin.json +10 -0
- package/plugins/netdata-rterm/index.mjs +229 -0
- package/plugins/netdata-rterm/netdata-rterm.extreme.spec.ts +252 -0
- package/plugins/netdata-rterm/plugin.json +22 -0
- package/plugins/patch-manager/index.d.mts +10 -0
- package/plugins/patch-manager/index.mjs +288 -0
- package/plugins/patch-manager/plugin.json +10 -0
- package/plugins/request-router/index.d.mts +9 -0
- package/plugins/request-router/index.mjs +235 -0
- package/plugins/request-router/plugin.json +10 -0
- package/plugins/sop-assistant/index.d.mts +9 -0
- package/plugins/sop-assistant/index.mjs +325 -0
- package/plugins/sop-assistant/plugin.json +10 -0
package/bin/gybackend.js
CHANGED
|
@@ -377295,6 +377295,9 @@ function createTriggerRuntime(deps) {
|
|
|
377295
377295
|
return Object.assign(engine, { busReady });
|
|
377296
377296
|
}
|
|
377297
377297
|
|
|
377298
|
+
// ../../packages/backend/src/services/observability.ts
|
|
377299
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
377300
|
+
|
|
377298
377301
|
// ../../packages/backend/src/services/sre/metricsLedger.ts
|
|
377299
377302
|
var DEFAULT_LIMIT = 1e4;
|
|
377300
377303
|
function flattenSnapshot(host, snap) {
|
|
@@ -382285,8 +382288,16 @@ function createObservability(deps) {
|
|
|
382285
382288
|
const auditLedger = new AuditLedger({});
|
|
382286
382289
|
const evidenceSealer = new EvidenceSealer({});
|
|
382287
382290
|
const pluginScanRoot = (process.env.GYBACKEND_DATA_DIR ?? "./.gybackend-data") + "/plugins";
|
|
382291
|
+
const bundlePluginRoot = new URL("../../plugins/", import.meta.url).pathname;
|
|
382292
|
+
const scanRoots = [pluginScanRoot, "./plugins"];
|
|
382293
|
+
try {
|
|
382294
|
+
const req = createRequire2(import.meta.url);
|
|
382295
|
+
const fs22 = req("node:fs");
|
|
382296
|
+
if (fs22.existsSync(bundlePluginRoot)) scanRoots.push(bundlePluginRoot);
|
|
382297
|
+
} catch {
|
|
382298
|
+
}
|
|
382288
382299
|
const pluginRegistry = new PluginRegistry({
|
|
382289
|
-
scanRoots
|
|
382300
|
+
scanRoots,
|
|
382290
382301
|
createContext: (record2) => PluginRegistry.defaultContext(
|
|
382291
382302
|
record2,
|
|
382292
382303
|
async (cmd, opts) => deps.agentService ? `exec(${cmd} on ${opts?.host ?? "local"})` : "",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rterm-backend",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.4",
|
|
4
4
|
"description": "RTerm headless backend — run RTerm-as-a-service (AI agent, SSH/WinRM/Serial/local terminals, fleet orchestration, advanced automation with event-driven triggers + NATS event mesh, scheduled automation, SRE observability, Netdata integration, AWS APerf performance deep-dive, plugin system) and drive it over a WebSocket JSON-RPC gateway. No desktop UI required.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"main": "bin/gybackend.js",
|
|
11
11
|
"files": [
|
|
12
12
|
"bin/",
|
|
13
|
+
"plugins/",
|
|
13
14
|
"README.md",
|
|
14
15
|
"LICENSE.md"
|
|
15
16
|
],
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// fraudops plugin type declarations
|
|
2
|
+
export function register(ctx: any): void
|
|
3
|
+
export function buildPipelineHealthCommand(): string
|
|
4
|
+
export function buildNatsStatusCommand(): string
|
|
5
|
+
export function buildKafkaLagCommand(): string
|
|
6
|
+
export function parsePipelineHealth(output: string): { status: string; jobs: any[]; running: number; failed: number }
|
|
7
|
+
export function buildStrCase(txnId: string, decision: string, indicators?: string[], assignedTo?: string): any
|
|
8
|
+
export function buildDecisionSummary(decisions: Array<{ decision: string }>): { total: number; blocks: number; reviews: number; approves: number; blockRate: number; reviewRate: number; approveRate: number }
|
|
9
|
+
export default any
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fraudops plugin — FraudOps for RTerm.
|
|
3
|
+
*
|
|
4
|
+
* Operational layer for the fraud detection pipeline. Monitors pipeline health
|
|
5
|
+
* (Flink, NATS, Kafka), manages STR workflow, tracks fraud incidents, and
|
|
6
|
+
* provides the unified fraud operations dashboard. Bridges Kafka/NATS metrics
|
|
7
|
+
* into RTerm's SRE pillar.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// --- Pure: build pipeline health check command ---
|
|
11
|
+
export function buildPipelineHealthCommand() {
|
|
12
|
+
return 'curl -s http://localhost:8081/jobs 2>/dev/null | head -20 || echo "flink not reachable"'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// --- Pure: build NATS JetStream status command ---
|
|
16
|
+
export function buildNatsStatusCommand() {
|
|
17
|
+
return 'curl -s http://localhost:8222/streaming/channelsz 2>/dev/null | head -30 || echo "nats not reachable"'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// --- Pure: build Kafka consumer lag command ---
|
|
21
|
+
export function buildKafkaLagCommand() {
|
|
22
|
+
return 'kafka-consumer-groups --bootstrap-server localhost:9092 --describe --group fraud-pipeline 2>/dev/null | head -20 || echo "kafka not reachable"'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// --- Pure: parse pipeline health output ---
|
|
26
|
+
export function parsePipelineHealth(output) {
|
|
27
|
+
const health = { status: 'unknown', jobs: [], running: 0, failed: 0 }
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(output)
|
|
30
|
+
if (parsed.jobs && Array.isArray(parsed.jobs)) {
|
|
31
|
+
health.jobs = parsed.jobs.map((j) => ({ id: j.jid, name: j.name, status: j.state }))
|
|
32
|
+
health.running = parsed.jobs.filter((j) => j.state === 'RUNNING').length
|
|
33
|
+
health.failed = parsed.jobs.filter((j) => j.state === 'FAILED').length
|
|
34
|
+
health.status = health.failed > 0 ? 'degraded' : health.running > 0 ? 'healthy' : 'unknown'
|
|
35
|
+
}
|
|
36
|
+
} catch { /* not JSON */ }
|
|
37
|
+
return health
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// --- Pure: build an STR case record ---
|
|
41
|
+
export function buildStrCase(txnId, decision, indicators, assignedTo) {
|
|
42
|
+
return {
|
|
43
|
+
id: `str-${txnId}-${Date.now().toString(36)}`,
|
|
44
|
+
txnId,
|
|
45
|
+
decision, // BLOCK | REVIEW
|
|
46
|
+
indicators: indicators ?? [],
|
|
47
|
+
assignedTo: assignedTo ?? 'unassigned',
|
|
48
|
+
status: 'pending', // pending | assigned | reviewed | filed | expired
|
|
49
|
+
deadline: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days (CBN requirement)
|
|
50
|
+
createdAt: Date.now(),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --- Pure: build a decision summary ---
|
|
55
|
+
export function buildDecisionSummary(decisions) {
|
|
56
|
+
const total = decisions.length
|
|
57
|
+
const blocks = decisions.filter((d) => d.decision === 'BLOCK').length
|
|
58
|
+
const reviews = decisions.filter((d) => d.decision === 'REVIEW').length
|
|
59
|
+
const approves = decisions.filter((d) => d.decision === 'APPROVE').length
|
|
60
|
+
return {
|
|
61
|
+
total,
|
|
62
|
+
blocks,
|
|
63
|
+
reviews,
|
|
64
|
+
approves,
|
|
65
|
+
blockRate: total > 0 ? Math.round((blocks / total) * 100) : 0,
|
|
66
|
+
reviewRate: total > 0 ? Math.round((reviews / total) * 100) : 0,
|
|
67
|
+
approveRate: total > 0 ? Math.round((approves / total) * 100) : 0,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- Plugin entry ---
|
|
72
|
+
export function register(ctx) {
|
|
73
|
+
const { registerTool, registerTrigger, registerPanel, exec, readLedger, log } = ctx
|
|
74
|
+
const strCases = []
|
|
75
|
+
|
|
76
|
+
// Tool: fraudops_pipeline_status — check the health of the fraud pipeline
|
|
77
|
+
registerTool({
|
|
78
|
+
name: 'fraudops_pipeline_status',
|
|
79
|
+
description: 'Check the health of the fraud detection pipeline (Flink jobs, NATS JetStream, Kafka consumer lag). Returns status for each component.',
|
|
80
|
+
params: {},
|
|
81
|
+
handler: async () => {
|
|
82
|
+
const flinkOut = await exec(buildPipelineHealthCommand(), {})
|
|
83
|
+
const natsOut = await exec(buildNatsStatusCommand(), {})
|
|
84
|
+
const kafkaOut = await exec(buildKafkaLagCommand(), {})
|
|
85
|
+
const flink = parsePipelineHealth(flinkOut)
|
|
86
|
+
log(`[fraudops] pipeline status: flink=${flink.status} nats=${natsOut.slice(0, 50)} kafka=${kafkaOut.slice(0, 50)}`)
|
|
87
|
+
return { flink, nats: natsOut.slice(0, 500), kafka: kafkaOut.slice(0, 500) }
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Tool: fraudops_str_assign — assign an STR case to an analyst
|
|
92
|
+
registerTool({
|
|
93
|
+
name: 'fraudops_str_assign',
|
|
94
|
+
description: 'Assign an STR (Suspicious Transaction Report) case to an analyst. Creates the case with a 7-day CBN deadline.',
|
|
95
|
+
params: {
|
|
96
|
+
txnId: { type: 'string', description: 'Transaction ID' },
|
|
97
|
+
decision: { type: 'string', description: 'BLOCK or REVIEW' },
|
|
98
|
+
indicators: { type: 'array', description: 'Fraud indicators (e.g., ["high_velocity", "new_device"])' },
|
|
99
|
+
assignedTo: { type: 'string', description: 'Analyst username' },
|
|
100
|
+
},
|
|
101
|
+
handler: async (params) => {
|
|
102
|
+
const { txnId, decision, indicators, assignedTo } = params ?? {}
|
|
103
|
+
if (!txnId || !decision) return { error: 'txnId and decision are required' }
|
|
104
|
+
const strCase = buildStrCase(txnId, decision, indicators, assignedTo)
|
|
105
|
+
strCase.status = 'assigned'
|
|
106
|
+
strCases.push(strCase)
|
|
107
|
+
log(`[fraudops] STR case ${strCase.id} assigned to ${assignedTo}`)
|
|
108
|
+
return strCase
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// Tool: fraudops_str_status — get STR case status
|
|
113
|
+
registerTool({
|
|
114
|
+
name: 'fraudops_str_status',
|
|
115
|
+
description: 'Get the status of STR cases. Filter by status, analyst, or overdue.',
|
|
116
|
+
params: {
|
|
117
|
+
status: { type: 'string', description: 'Filter by status: pending, assigned, reviewed, filed, expired' },
|
|
118
|
+
assignedTo: { type: 'string', description: 'Filter by analyst' },
|
|
119
|
+
overdue: { type: 'boolean', description: 'Only show overdue cases (past 7-day deadline)' },
|
|
120
|
+
},
|
|
121
|
+
handler: async (params) => {
|
|
122
|
+
let filtered = [...strCases]
|
|
123
|
+
if (params?.status) filtered = filtered.filter((c) => c.status === params.status)
|
|
124
|
+
if (params?.assignedTo) filtered = filtered.filter((c) => c.assignedTo === params.assignedTo)
|
|
125
|
+
if (params?.overdue) filtered = filtered.filter((c) => Date.now() > c.deadline)
|
|
126
|
+
return { total: filtered.length, cases: filtered }
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// Tool: fraudops_decision_summary — summarize fraud decisions
|
|
131
|
+
registerTool({
|
|
132
|
+
name: 'fraudops_decision_summary',
|
|
133
|
+
description: 'Summarize fraud decisions (BLOCK/REVIEW/APPROVE counts and rates) from the decision stream.',
|
|
134
|
+
params: {
|
|
135
|
+
decisions: { type: 'array', description: 'Array of decision objects with { decision: "BLOCK"|"REVIEW"|"APPROVE" }' },
|
|
136
|
+
},
|
|
137
|
+
handler: async (params) => {
|
|
138
|
+
const decisions = params?.decisions ?? []
|
|
139
|
+
const summary = buildDecisionSummary(decisions)
|
|
140
|
+
log(`[fraudops] decision summary: ${summary.total} total, ${summary.blockRate}% block, ${summary.reviewRate}% review`)
|
|
141
|
+
return summary
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Trigger: fraudops_str_overdue — fires when an STR case is overdue
|
|
146
|
+
registerTrigger({
|
|
147
|
+
name: 'fraudops_str_overdue',
|
|
148
|
+
description: 'Fires when an STR case exceeds the 7-day CBN deadline. Use for escalation to senior analyst.',
|
|
149
|
+
match: (event) => {
|
|
150
|
+
if (event?.source !== 'fraudops') return false
|
|
151
|
+
return event.labels?.status === 'expired' || event.labels?.overdue === true
|
|
152
|
+
},
|
|
153
|
+
action: 'propose-change',
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// Trigger: fraudops_pipeline_down — fires when a pipeline component is down
|
|
157
|
+
registerTrigger({
|
|
158
|
+
name: 'fraudops_pipeline_down',
|
|
159
|
+
description: 'Fires when a fraud pipeline component (Flink, NATS, Kafka) is detected as down. Use for immediate incident response.',
|
|
160
|
+
match: (event) => {
|
|
161
|
+
if (event?.source !== 'fraudops') return false
|
|
162
|
+
return event.labels?.status === 'down' || event.labels?.failed === true
|
|
163
|
+
},
|
|
164
|
+
action: 'run-playbook',
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// Panel: fraudops-dashboard — unified fraud operations dashboard
|
|
168
|
+
registerPanel({
|
|
169
|
+
name: 'fraudops-dashboard',
|
|
170
|
+
title: 'Fraud Operations Dashboard',
|
|
171
|
+
render: (data) => {
|
|
172
|
+
const cases = Array.isArray(data?.cases) ? data.cases : strCases
|
|
173
|
+
const summary = data?.summary ?? { total: 0, blocks: 0, reviews: 0, approves: 0, blockRate: 0, reviewRate: 0, approveRate: 0 }
|
|
174
|
+
const rows = cases.map((c) =>
|
|
175
|
+
`<tr><td>${c.id}</td><td>${c.txnId}</td><td>${c.decision}</td><td>${c.assignedTo}</td><td>${c.status}</td><td>${new Date(c.deadline).toLocaleDateString()}</td></tr>`
|
|
176
|
+
).join('')
|
|
177
|
+
return `<div class="fraudops-dashboard"><h3>Fraud Operations Dashboard</h3><p>Decisions: ${summary.total} | Block: ${summary.blockRate}% | Review: ${summary.reviewRate}% | Approve: ${summary.approveRate}%</p><p>STR Cases: ${cases.length} | Overdue: ${cases.filter((c) => Date.now() > c.deadline).length}</p><table><thead><tr><th>ID</th><th>Txn ID</th><th>Decision</th><th>Assigned</th><th>Status</th><th>Deadline</th></tr></thead><tbody>${rows}</tbody></table></div>`
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
log('[fraudops] registered: 4 tools, 2 triggers, 1 panel')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export default { register, buildPipelineHealthCommand, buildNatsStatusCommand, buildKafkaLagCommand, parsePipelineHealth, buildStrCase, buildDecisionSummary }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fraudops",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "FraudOps for RTerm — operational layer for the fraud detection pipeline. Monitors pipeline health (Flink, NATS, Kafka), manages STR workflow, tracks fraud incidents, and provides the unified fraud operations dashboard. Bridges Kafka/NATS metrics into RTerm's SRE pillar.",
|
|
5
|
+
"entry": "index.mjs",
|
|
6
|
+
"tools": ["fraudops_pipeline_status", "fraudops_str_assign", "fraudops_str_status", "fraudops_decision_summary"],
|
|
7
|
+
"triggers": ["fraudops_str_overdue", "fraudops_pipeline_down"],
|
|
8
|
+
"panels": ["fraudops-dashboard"],
|
|
9
|
+
"permissions": ["exec", "readLedger:metrics", "readLedger:incidents"]
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// iam-connector plugin type declarations
|
|
2
|
+
export function register(ctx: any): void
|
|
3
|
+
export function buildUserInfoCommand(username: string, os?: string): string
|
|
4
|
+
export function buildUserGroupsCommand(username: string, os?: string): string
|
|
5
|
+
export function buildDisableUserCommand(username: string, os?: string): string
|
|
6
|
+
export function buildAccessReviewCommand(os?: string): string
|
|
7
|
+
export function parseUserInfo(output: string, os?: string): { username: string; groups: string[]; enabled: boolean; locked: boolean }
|
|
8
|
+
export function parseAccessReview(output: string, os?: string): Array<{ username: string; groups?: string[]; enabled?: boolean }>
|
|
9
|
+
export function isPrivileged(userInfo: { groups: string[] }, privilegedGroups?: string[]): boolean
|
|
10
|
+
export default any
|
|
@@ -0,0 +1,225 @@
|
|
|
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).toLowerCase()
|
|
50
|
+
const info = { username: '', groups: [], enabled: true, locked: false }
|
|
51
|
+
|
|
52
|
+
if (o === 'windows' || o === 'win32') {
|
|
53
|
+
const nameMatch = output.match(/Name\s+:\s+(\S+)/)
|
|
54
|
+
const enabledMatch = output.match(/Enabled\s+:\s+(True|False)/i)
|
|
55
|
+
const lockedMatch = output.match(/LockedOut\s+:\s+(True|False)/i)
|
|
56
|
+
if (nameMatch) info.username = nameMatch[1]
|
|
57
|
+
if (enabledMatch) info.enabled = enabledMatch[1].toLowerCase() === 'true'
|
|
58
|
+
if (lockedMatch) info.locked = lockedMatch[1].toLowerCase() === 'true'
|
|
59
|
+
return info
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Linux: parse id + groups + passwd -S output
|
|
63
|
+
const lines = output.split(/\r?\n/)
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
const l = line.trim()
|
|
66
|
+
const idMatch = l.match(/uid=\d+\((\S+)\)/)
|
|
67
|
+
if (idMatch) info.username = idMatch[1]
|
|
68
|
+
const groupsMatch = l.match(/groups=(.+)/)
|
|
69
|
+
if (groupsMatch) {
|
|
70
|
+
// groups=1000(john),27(sudo) — split by comma, then extract names from NNN(name) format
|
|
71
|
+
info.groups = groupsMatch[1]
|
|
72
|
+
.split(',')
|
|
73
|
+
.map((g) => g.replace(/\d+\(([^)]+)\)/, '$1').trim())
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
}
|
|
76
|
+
if (l.includes(' L ')) info.locked = true
|
|
77
|
+
}
|
|
78
|
+
return info
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- Pure: parse access review output into a user list ---
|
|
82
|
+
export function parseAccessReview(output, os = 'linux') {
|
|
83
|
+
const o = String(os).toLowerCase()
|
|
84
|
+
const users = []
|
|
85
|
+
|
|
86
|
+
if (o === 'windows' || o === 'win32') {
|
|
87
|
+
const lines = output.split(/\r?\n/)
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
const l = line.trim()
|
|
90
|
+
const match = l.match(/^(\S+)\s+(True|False)\s+/)
|
|
91
|
+
if (match) {
|
|
92
|
+
users.push({ username: match[1], enabled: match[2].toLowerCase() === 'true' })
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return users
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Linux: parse "user: group1 group2" lines
|
|
99
|
+
const lines = output.split(/\r?\n/)
|
|
100
|
+
for (const line of lines) {
|
|
101
|
+
const l = line.trim()
|
|
102
|
+
const match = l.match(/^(\S+):\s*(.*)/)
|
|
103
|
+
if (match) {
|
|
104
|
+
users.push({ username: match[1], groups: match[2].split(/\s+/).filter(Boolean) })
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return users
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- Pure: check if a user has privileged access ---
|
|
111
|
+
export function isPrivileged(userInfo, privilegedGroups = ['sudo', 'wheel', 'admin', 'root', 'Administrators']) {
|
|
112
|
+
return userInfo.groups.some((g) => privilegedGroups.some((pg) => g.toLowerCase().includes(pg.toLowerCase())))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --- Plugin entry ---
|
|
116
|
+
export function register(ctx) {
|
|
117
|
+
const { registerTool, registerTrigger, registerPanel, exec, readLedger, log } = ctx
|
|
118
|
+
|
|
119
|
+
// Tool: iam_user_info — get user info
|
|
120
|
+
registerTool({
|
|
121
|
+
name: 'iam_user_info',
|
|
122
|
+
description: 'Get user information (username, groups, enabled status, locked status) for a user on a host.',
|
|
123
|
+
params: {
|
|
124
|
+
username: { type: 'string', description: 'Username to query' },
|
|
125
|
+
host: { type: 'string', description: 'Host to query' },
|
|
126
|
+
os: { type: 'string', description: 'OS type: linux, windows' },
|
|
127
|
+
},
|
|
128
|
+
handler: async (params) => {
|
|
129
|
+
const { username, host, os = 'linux' } = params ?? {}
|
|
130
|
+
if (!username || !host) return { error: 'username and host are required' }
|
|
131
|
+
const cmd = buildUserInfoCommand(username, os)
|
|
132
|
+
log(`[iam-connector] querying user ${username} on ${host}`)
|
|
133
|
+
const output = await exec(cmd, { host })
|
|
134
|
+
const info = parseUserInfo(output, os)
|
|
135
|
+
return { username, host, os, ...info, privileged: isPrivileged(info) }
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// Tool: iam_user_groups — get user group memberships
|
|
140
|
+
registerTool({
|
|
141
|
+
name: 'iam_user_groups',
|
|
142
|
+
description: 'Get group memberships for a user on a host.',
|
|
143
|
+
params: {
|
|
144
|
+
username: { type: 'string', description: 'Username to query' },
|
|
145
|
+
host: { type: 'string', description: 'Host to query' },
|
|
146
|
+
os: { type: 'string', description: 'OS type' },
|
|
147
|
+
},
|
|
148
|
+
handler: async (params) => {
|
|
149
|
+
const { username, host, os = 'linux' } = params ?? {}
|
|
150
|
+
if (!username || !host) return { error: 'username and host are required' }
|
|
151
|
+
const cmd = buildUserGroupsCommand(username, os)
|
|
152
|
+
const output = await exec(cmd, { host })
|
|
153
|
+
const info = parseUserInfo(output, os)
|
|
154
|
+
return { username, host, groups: info.groups, privileged: isPrivileged(info) }
|
|
155
|
+
},
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Tool: iam_disable_user — disable a user account (requires approval)
|
|
159
|
+
registerTool({
|
|
160
|
+
name: 'iam_disable_user',
|
|
161
|
+
description: 'Disable a user account on a host. This is a destructive operation — requires approval. Returns the result.',
|
|
162
|
+
params: {
|
|
163
|
+
username: { type: 'string', description: 'Username to disable' },
|
|
164
|
+
host: { type: 'string', description: 'Host to disable on' },
|
|
165
|
+
os: { type: 'string', description: 'OS type' },
|
|
166
|
+
},
|
|
167
|
+
handler: async (params) => {
|
|
168
|
+
const { username, host, os = 'linux' } = params ?? {}
|
|
169
|
+
if (!username || !host) return { error: 'username and host are required' }
|
|
170
|
+
const cmd = buildDisableUserCommand(username, os)
|
|
171
|
+
log(`[iam-connector] disabling user ${username} on ${host}`)
|
|
172
|
+
const output = await exec(cmd, { host })
|
|
173
|
+
const info = parseUserInfo(output, os)
|
|
174
|
+
return { username, host, disabled: !info.enabled, output: output.slice(0, 500) }
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// Tool: iam_access_review — review all user access on a host
|
|
179
|
+
registerTool({
|
|
180
|
+
name: 'iam_access_review',
|
|
181
|
+
description: 'Review all user accounts and group memberships on a host. Identifies privileged users.',
|
|
182
|
+
params: {
|
|
183
|
+
host: { type: 'string', description: 'Host to review' },
|
|
184
|
+
os: { type: 'string', description: 'OS type' },
|
|
185
|
+
},
|
|
186
|
+
handler: async (params) => {
|
|
187
|
+
const { host, os = 'linux' } = params ?? {}
|
|
188
|
+
if (!host) return { error: 'host is required' }
|
|
189
|
+
const cmd = buildAccessReviewCommand(os)
|
|
190
|
+
const output = await exec(cmd, { host })
|
|
191
|
+
const users = parseAccessReview(output, os)
|
|
192
|
+
const privilegedUsers = users.filter((u) => isPrivileged({ groups: u.groups ?? [] }))
|
|
193
|
+
return { host, totalUsers: users.length, privilegedUsers: privilegedUsers.length, users: users.slice(0, 50), privilegedUsers: privilegedUsers.map((u) => u.username) }
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
// Trigger: iam_privileged_change — fires when a privileged user's access changes
|
|
198
|
+
registerTrigger({
|
|
199
|
+
name: 'iam_privileged_change',
|
|
200
|
+
description: 'Fires when a privileged user account is modified (disabled, group change). Use for compliance monitoring.',
|
|
201
|
+
match: (event) => {
|
|
202
|
+
if (event?.source !== 'iam-connector') return false
|
|
203
|
+
return event.labels?.privileged === true
|
|
204
|
+
},
|
|
205
|
+
action: 'propose-change',
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// Panel: iam-access-dashboard — IAM access dashboard
|
|
209
|
+
registerPanel({
|
|
210
|
+
name: 'iam-access-dashboard',
|
|
211
|
+
title: 'IAM Access Dashboard',
|
|
212
|
+
render: (data) => {
|
|
213
|
+
const users = Array.isArray(data) ? data : []
|
|
214
|
+
const privileged = users.filter((u) => isPrivileged({ groups: u.groups ?? [] }))
|
|
215
|
+
const rows = users.map((u) =>
|
|
216
|
+
`<tr><td>${u.username}</td><td>${(u.groups ?? []).join(', ')}</td><td>${u.enabled !== false ? '✅' : '❌'}</td><td>${isPrivileged({ groups: u.groups ?? [] }) ? '🔐' : ''}</td></tr>`
|
|
217
|
+
).join('')
|
|
218
|
+
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>`
|
|
219
|
+
},
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
log('[iam-connector] registered: 4 tools, 1 trigger, 1 panel')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export default { register, buildUserInfoCommand, buildUserGroupsCommand, buildDisableUserCommand, buildAccessReviewCommand, parseUserInfo, parseAccessReview, isPrivileged }
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
}
|