@nuvlore/extension-workday-infra-access 0.1.1
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 +3 -0
- package/agents/workday-infra-access-readonly.yaml +183 -0
- package/package.json +50 -0
- package/skills/workday-infra-access/SKILL.md +102 -0
- package/skills/workday-infra-access/references/access-boundary-runbook.md +43 -0
- package/skills/workday-infra-access/references/access-runbook.md +15 -0
- package/skills/workday-infra-access/references/access-scylla-cloud-runbook.md +39 -0
- package/skills/workday-infra-access/references/access-spc-argo-runbook.md +99 -0
- package/src/workday-access-profiles.mjs +194 -0
- package/tools/aws_cust_federated_readonly_command_pack.mjs +234 -0
- package/tools/gcp_readonly_command_pack.mjs +167 -0
- package/tools/public_cloud_access_status.mjs +48 -0
- package/tools/workday_boundary_login.mjs +460 -0
- package/tools/workday_private_cloud_sudo_readonly_command.mjs +386 -0
- package/tools/workday_public_cloud_environment_map.mjs +189 -0
- package/tools/workday_public_cloud_kubernetes_read.mjs +297 -0
- package/tools/workday_public_cloud_spc_login.mjs +505 -0
- package/tools/workday_spc_argo_readonly_command_pack.mjs +215 -0
- package/vendor/extension-read-only-toolkit/LICENSE +202 -0
- package/vendor/extension-read-only-toolkit/README.md +29 -0
- package/vendor/extension-read-only-toolkit/package.json +46 -0
- package/vendor/extension-read-only-toolkit/src/devops-data-paths.mjs +222 -0
- package/vendor/extension-read-only-toolkit/src/devops-diagnostics.mjs +440 -0
- package/vendor/extension-read-only-toolkit/src/enterprise-api-read.mjs +180 -0
- package/vendor/extension-read-only-toolkit/src/read-only-command-pack.mjs +1 -0
- package/vendor/extension-read-only-toolkit/src/read-only-plan.mjs +45 -0
- package/vendor/extension-read-only-toolkit/src/read-only-shell-policy.d.mts +26 -0
- package/vendor/extension-read-only-toolkit/src/read-only-shell-policy.mjs +170 -0
- package/workflows/public-cloud-readonly-investigation.js +99 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
export const description = "Run a single read-only sudo command on an explicit Workday private-cloud/DC host by running bauth init with the dedicated LDAP PROD password, then using ordinary ssh. The secret is never returned.";
|
|
2
|
+
|
|
3
|
+
import { execFile, spawn } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { defineReadOnlyTool, fail, ok } from "@nuvlore/extension-read-only-toolkit/devops-diagnostics";
|
|
7
|
+
import { isReadOnlyShellCommand } from "@nuvlore/extension-read-only-toolkit/read-only-shell-policy";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_DC_BOUNDARY_ADDR,
|
|
10
|
+
DEFAULT_WORKDAY_LDAP_KEYCHAIN_SERVICE,
|
|
11
|
+
DEFAULT_WORKDAY_LDAP_PROD_KEYCHAIN_ACCOUNT,
|
|
12
|
+
extractTokenFromKeychainSecret,
|
|
13
|
+
workdayProdCredentialCandidates
|
|
14
|
+
} from "../src/workday-access-profiles.mjs";
|
|
15
|
+
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
17
|
+
|
|
18
|
+
const DEFAULT_BAUTH_COMMAND = "/Users/boqiang.liang/bin/bauth";
|
|
19
|
+
const DEFAULT_BAUTH_ARGS = ["init"];
|
|
20
|
+
const DEFAULT_EXPECT_BINARY = "/usr/bin/expect";
|
|
21
|
+
const DEFAULT_CONNECT_TIMEOUT_SECONDS = 20;
|
|
22
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
23
|
+
|
|
24
|
+
function normalizeText(value) {
|
|
25
|
+
return typeof value === "string" ? value.trim() : "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sshHostFor(host) {
|
|
29
|
+
return normalizeText(host);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function boundaryProxyJumpFor(host) {
|
|
33
|
+
const value = sshHostFor(host);
|
|
34
|
+
const match = value.match(/\.cust\.(ash|atl|ams|dub|pdx)\.wd$/u) || value.match(/\.(ash|atl|ams|dub|pdx)\.wd$/u);
|
|
35
|
+
return match ? `boundary-proxy.${match[1]}.bnd` : "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function assertStructuredHitlApproval(input, context) {
|
|
39
|
+
const approval = input.hitlApproval;
|
|
40
|
+
if (!approval || typeof approval !== "object") {
|
|
41
|
+
throw new Error(`${context} requires structured HITL approval with approved, approvedBy, action, target, reason, expectedImpact, and risk.`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const missing = ["approvedBy", "action", "target", "reason", "expectedImpact", "risk"]
|
|
45
|
+
.filter((key) => !normalizeText(approval[key]));
|
|
46
|
+
if (approval.approved !== true || missing.length > 0) {
|
|
47
|
+
throw new Error(`${context} requires approved=true and complete HITL details: ${missing.join(", ") || "approved"}.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeInput(input = {}) {
|
|
52
|
+
return {
|
|
53
|
+
action: normalizeText(input.action) || "plan",
|
|
54
|
+
host: normalizeText(input.host),
|
|
55
|
+
command: normalizeText(input.command),
|
|
56
|
+
bauthCommand: DEFAULT_BAUTH_COMMAND,
|
|
57
|
+
bauthArgs: DEFAULT_BAUTH_ARGS,
|
|
58
|
+
expectBinary: DEFAULT_EXPECT_BINARY,
|
|
59
|
+
roleIndex: 0,
|
|
60
|
+
proxyJumpHost: normalizeText(input.proxyJumpHost),
|
|
61
|
+
boundaryAddr: normalizeText(input.boundaryAddr) || DEFAULT_DC_BOUNDARY_ADDR,
|
|
62
|
+
connectTimeoutSeconds: Number.isInteger(input.connectTimeoutSeconds)
|
|
63
|
+
? Math.max(1, input.connectTimeoutSeconds)
|
|
64
|
+
: DEFAULT_CONNECT_TIMEOUT_SECONDS,
|
|
65
|
+
timeoutMs: Number.isInteger(input.timeoutMs) ? Math.max(1000, input.timeoutMs) : DEFAULT_TIMEOUT_MS,
|
|
66
|
+
humanApprovedSudoRead: input.humanApprovedSudoRead === true,
|
|
67
|
+
hitlApproval: input.hitlApproval
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function assertTarget(input) {
|
|
72
|
+
if (!input.host) throw new Error("host is required for private-cloud sudo reads.");
|
|
73
|
+
if (!input.command) throw new Error("command is required for private-cloud sudo reads.");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validateReadOnlyCommand(command) {
|
|
77
|
+
const result = isReadOnlyShellCommand(command);
|
|
78
|
+
if (!result.allowed) {
|
|
79
|
+
throw new Error(`Refusing sudo command: ${result.reason}`);
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildRemoteSudoScript(command) {
|
|
85
|
+
return `IFS= read -r SUDOPASS || exit 11; printf '%s\\n' "$SUDOPASS" | sudo -S -p '' bash -lc ${JSON.stringify(command)}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildSshArgs(input) {
|
|
89
|
+
const args = [
|
|
90
|
+
"-o",
|
|
91
|
+
"BatchMode=yes",
|
|
92
|
+
"-o",
|
|
93
|
+
`ConnectTimeout=${input.connectTimeoutSeconds}`,
|
|
94
|
+
];
|
|
95
|
+
const proxyJumpHost = input.proxyJumpHost || boundaryProxyJumpFor(input.host);
|
|
96
|
+
if (proxyJumpHost) {
|
|
97
|
+
args.push("-o", `ProxyJump=${proxyJumpHost}`);
|
|
98
|
+
}
|
|
99
|
+
args.push(
|
|
100
|
+
"--",
|
|
101
|
+
sshHostFor(input.host),
|
|
102
|
+
buildRemoteSudoScript(input.command)
|
|
103
|
+
);
|
|
104
|
+
return args;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function privateCloudSshEnv(input) {
|
|
108
|
+
return {
|
|
109
|
+
...process.env,
|
|
110
|
+
BOUNDARY_ADDR: input?.boundaryAddr || DEFAULT_DC_BOUNDARY_ADDR,
|
|
111
|
+
BOUNDARY_SKIP_CACHE_DAEMON: "true",
|
|
112
|
+
PATH: [
|
|
113
|
+
"/opt/homebrew/bin",
|
|
114
|
+
"/Users/boqiang.liang/bin",
|
|
115
|
+
"/usr/local/bin",
|
|
116
|
+
process.env.PATH || ""
|
|
117
|
+
].filter(Boolean).join(":")
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function bauthEnv(input, password) {
|
|
122
|
+
return {
|
|
123
|
+
...privateCloudSshEnv(input),
|
|
124
|
+
DEVOPS_LDAP_PASSWORD: password,
|
|
125
|
+
DEVOPS_LOGIN_TIMEOUT_SECONDS: String(Math.max(1, Math.ceil(input.timeoutMs / 1000))),
|
|
126
|
+
DEVOPS_ROLE_INDEX: String(input.roleIndex)
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildPlan(input) {
|
|
131
|
+
const normalized = normalizeInput(input);
|
|
132
|
+
assertTarget(normalized);
|
|
133
|
+
const policy = validateReadOnlyCommand(normalized.command);
|
|
134
|
+
return {
|
|
135
|
+
mode: "private-cloud-sudo-readonly",
|
|
136
|
+
action: normalized.action,
|
|
137
|
+
target: {
|
|
138
|
+
accessPlane: "private-cloud",
|
|
139
|
+
loginMethod: "bauth-init-then-plain-ssh",
|
|
140
|
+
host: sshHostFor(normalized.host),
|
|
141
|
+
proxyJumpHost: normalized.proxyJumpHost || boundaryProxyJumpFor(normalized.host),
|
|
142
|
+
boundaryAddr: normalized.boundaryAddr
|
|
143
|
+
},
|
|
144
|
+
command: normalized.command,
|
|
145
|
+
policy,
|
|
146
|
+
credentialSource: {
|
|
147
|
+
keychainService: DEFAULT_WORKDAY_LDAP_KEYCHAIN_SERVICE,
|
|
148
|
+
keychainAccount: DEFAULT_WORKDAY_LDAP_PROD_KEYCHAIN_ACCOUNT,
|
|
149
|
+
fallbackKeychainEntry: "wallee/wallee.workday_prod_ldap",
|
|
150
|
+
secretHandling: "read LDAP PROD token from macOS Keychain and pass it only to remote sudo stdin; never return it"
|
|
151
|
+
},
|
|
152
|
+
prerequisites: [
|
|
153
|
+
"Workday Security Network login/session is active",
|
|
154
|
+
"Tool will run bauth init with the dedicated LDAP PROD password before plain ssh"
|
|
155
|
+
],
|
|
156
|
+
localStateChanges: [],
|
|
157
|
+
remoteWrite: "not_executed"
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function readProdPasswordFromKeychain(execFileImpl) {
|
|
162
|
+
for (const candidate of workdayProdCredentialCandidates()) {
|
|
163
|
+
try {
|
|
164
|
+
const result = await execFileImpl("/usr/bin/security", [
|
|
165
|
+
"find-generic-password",
|
|
166
|
+
"-s",
|
|
167
|
+
candidate.service,
|
|
168
|
+
"-a",
|
|
169
|
+
candidate.account,
|
|
170
|
+
"-w"
|
|
171
|
+
], {
|
|
172
|
+
maxBuffer: 1024 * 1024,
|
|
173
|
+
timeout: 10_000
|
|
174
|
+
});
|
|
175
|
+
const password = extractTokenFromKeychainSecret(result?.stdout);
|
|
176
|
+
if (password) return { password, keychainService: candidate.service, keychainAccount: candidate.account };
|
|
177
|
+
} catch (_error) {
|
|
178
|
+
// Try the legacy Workday PROD credential source next.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { password: "", keychainService: DEFAULT_WORKDAY_LDAP_KEYCHAIN_SERVICE, keychainAccount: DEFAULT_WORKDAY_LDAP_PROD_KEYCHAIN_ACCOUNT };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function redactSecret(text, password) {
|
|
185
|
+
const raw = String(text || "");
|
|
186
|
+
if (!password) return raw;
|
|
187
|
+
return raw.split(password).join("[redacted]");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function tclListValue(value) {
|
|
191
|
+
return `{${String(value).replaceAll("\\", "\\\\").replaceAll("{", "\\{").replaceAll("}", "\\}")}}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildBauthExpectScript(input) {
|
|
195
|
+
const commandList = [input.bauthCommand, ...input.bauthArgs].map(tclListValue).join(" ");
|
|
196
|
+
return [
|
|
197
|
+
"set timeout $env(DEVOPS_LOGIN_TIMEOUT_SECONDS)",
|
|
198
|
+
"set password $env(DEVOPS_LDAP_PASSWORD)",
|
|
199
|
+
`set command [list ${commandList}]`,
|
|
200
|
+
"set role_index $env(DEVOPS_ROLE_INDEX)",
|
|
201
|
+
"spawn -noecho {*}$command",
|
|
202
|
+
"expect {",
|
|
203
|
+
" -re {Please enter your Okta/AD password:|Please enter your CORP LDAP password:|Password:} { send -- \"$password\\r\"; exp_continue }",
|
|
204
|
+
" -re {Please Select a Role:|Select a Role:|Select role:} { send -- \"$role_index\\r\"; exp_continue }",
|
|
205
|
+
" eof { catch wait result; exit [lindex $result 3] }",
|
|
206
|
+
" timeout { puts \"Workday bauth init timed out.\"; exit 124 }",
|
|
207
|
+
"}"
|
|
208
|
+
].join("\n");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function runBauthInit(execFileImpl, input, password) {
|
|
212
|
+
const result = await execFileImpl(input.expectBinary, [
|
|
213
|
+
"-c",
|
|
214
|
+
buildBauthExpectScript(input)
|
|
215
|
+
], {
|
|
216
|
+
env: bauthEnv(input, password),
|
|
217
|
+
maxBuffer: 1024 * 1024,
|
|
218
|
+
timeout: input.timeoutMs + 5_000
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
stdout: redactSecret(result.stdout, password),
|
|
222
|
+
stderr: redactSecret(result.stderr, password),
|
|
223
|
+
exitCode: 0
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function runSshSudoRead(spawnImpl, input, password) {
|
|
228
|
+
return await new Promise((resolve, reject) => {
|
|
229
|
+
const child = spawnImpl("ssh", buildSshArgs(input), {
|
|
230
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
231
|
+
env: privateCloudSshEnv(input)
|
|
232
|
+
});
|
|
233
|
+
let stdout = "";
|
|
234
|
+
let stderr = "";
|
|
235
|
+
const timer = setTimeout(() => {
|
|
236
|
+
child.kill("SIGTERM");
|
|
237
|
+
reject(new Error("private cloud sudo read timed out."));
|
|
238
|
+
}, input.timeoutMs);
|
|
239
|
+
child.stdout?.setEncoding?.("utf8");
|
|
240
|
+
child.stderr?.setEncoding?.("utf8");
|
|
241
|
+
child.stdout?.on?.("data", (chunk) => {
|
|
242
|
+
stdout += chunk;
|
|
243
|
+
});
|
|
244
|
+
child.stderr?.on?.("data", (chunk) => {
|
|
245
|
+
stderr += chunk;
|
|
246
|
+
});
|
|
247
|
+
child.on("error", (error) => {
|
|
248
|
+
clearTimeout(timer);
|
|
249
|
+
reject(error);
|
|
250
|
+
});
|
|
251
|
+
child.on("exit", (code) => {
|
|
252
|
+
clearTimeout(timer);
|
|
253
|
+
const result = {
|
|
254
|
+
exitCode: code ?? -1,
|
|
255
|
+
stdout: redactSecret(stdout, password),
|
|
256
|
+
stderr: redactSecret(stderr, password)
|
|
257
|
+
};
|
|
258
|
+
if (code === 0) {
|
|
259
|
+
resolve(result);
|
|
260
|
+
} else {
|
|
261
|
+
reject(new Error(`private cloud sudo read exited with ${code ?? "unknown"}: ${result.stderr || result.stdout}`));
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
child.stdin?.end(`${password}\n`);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function privateCloudSudoReadonlyCommand(input, options = {}) {
|
|
269
|
+
const normalized = normalizeInput(input);
|
|
270
|
+
assertTarget(normalized);
|
|
271
|
+
validateReadOnlyCommand(normalized.command);
|
|
272
|
+
const execFileImpl = options.execFileImpl || execFileAsync;
|
|
273
|
+
const spawnImpl = options.spawnImpl || spawn;
|
|
274
|
+
|
|
275
|
+
if (normalized.action === "plan") {
|
|
276
|
+
return JSON.stringify(buildPlan(normalized), null, 2);
|
|
277
|
+
}
|
|
278
|
+
if (normalized.action !== "run") {
|
|
279
|
+
throw new Error(`Unsupported workday_private_cloud_sudo_readonly_command action: ${normalized.action}`);
|
|
280
|
+
}
|
|
281
|
+
if (!normalized.humanApprovedSudoRead) {
|
|
282
|
+
throw new Error("workday_private_cloud_sudo_readonly_command action=run requires humanApprovedSudoRead=true.");
|
|
283
|
+
}
|
|
284
|
+
assertStructuredHitlApproval(normalized, "workday_private_cloud_sudo_readonly_command action=run");
|
|
285
|
+
|
|
286
|
+
const keychain = await readProdPasswordFromKeychain(execFileImpl);
|
|
287
|
+
if (!keychain.password) {
|
|
288
|
+
throw new Error(`Missing LDAP PROD password in macOS keychain service ${keychain.keychainService}, account ${keychain.keychainAccount}. Store it in eldwin/eldwin.workday_prod_ldap first; wallee/wallee.workday_prod_ldap is supported only as a fallback source.`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const bauth = await runBauthInit(execFileImpl, normalized, keychain.password);
|
|
292
|
+
const result = await runSshSudoRead(spawnImpl, normalized, keychain.password);
|
|
293
|
+
return JSON.stringify({
|
|
294
|
+
action: "run",
|
|
295
|
+
target: buildPlan(normalized).target,
|
|
296
|
+
command: normalized.command,
|
|
297
|
+
keychainService: keychain.keychainService,
|
|
298
|
+
keychainAccount: keychain.keychainAccount,
|
|
299
|
+
bauth: {
|
|
300
|
+
command: normalized.bauthCommand,
|
|
301
|
+
args: normalized.bauthArgs,
|
|
302
|
+
roleIndex: normalized.roleIndex,
|
|
303
|
+
exitCode: bauth.exitCode,
|
|
304
|
+
transcript: [bauth.stdout, bauth.stderr].filter(Boolean).join("\n").slice(-1000)
|
|
305
|
+
},
|
|
306
|
+
result,
|
|
307
|
+
remoteWrite: "not_executed"
|
|
308
|
+
}, null, 2);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export class WorkdayPrivateCloudSudoReadonlyCommandService {
|
|
312
|
+
constructor(options = {}) {
|
|
313
|
+
this.options = options;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async invoke(input) {
|
|
317
|
+
return privateCloudSudoReadonlyCommand(input, this.options);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const defaultService = new WorkdayPrivateCloudSudoReadonlyCommandService();
|
|
322
|
+
|
|
323
|
+
export const __test = {
|
|
324
|
+
WorkdayPrivateCloudSudoReadonlyCommandService,
|
|
325
|
+
boundaryHostFor: sshHostFor,
|
|
326
|
+
sshHostFor,
|
|
327
|
+
boundaryProxyJumpFor,
|
|
328
|
+
bauthEnv,
|
|
329
|
+
buildPlan,
|
|
330
|
+
buildBauthExpectScript,
|
|
331
|
+
buildRemoteSudoScript,
|
|
332
|
+
buildSshArgs,
|
|
333
|
+
privateCloudSshEnv,
|
|
334
|
+
normalizeInput,
|
|
335
|
+
privateCloudSudoReadonlyCommand,
|
|
336
|
+
readProdPasswordFromKeychain,
|
|
337
|
+
runBauthInit,
|
|
338
|
+
redactSecret,
|
|
339
|
+
runSshSudoRead,
|
|
340
|
+
validateReadOnlyCommand
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const sharedTool = defineReadOnlyTool({
|
|
344
|
+
name: "workday_private_cloud_sudo_readonly_command",
|
|
345
|
+
description: "Run a single read-only sudo command on an explicit Workday private-cloud/DC host by running bauth init with the dedicated LDAP PROD password, then using ordinary ssh. The secret is never returned.",
|
|
346
|
+
meta: {
|
|
347
|
+
permissions: { rule: "ask" },
|
|
348
|
+
secret_provider: "keychain"
|
|
349
|
+
},
|
|
350
|
+
inputSchema: {
|
|
351
|
+
action: z.enum(["plan", "run"]).default("plan"),
|
|
352
|
+
host: z.string().trim().min(1).max(253),
|
|
353
|
+
command: z.string().trim().min(1).max(4096),
|
|
354
|
+
proxyJumpHost: z.string().trim().max(253).optional().describe("Optional explicit Boundary proxy jump host. Defaults to boundary-proxy.<dc>.bnd inferred from the private-cloud host."),
|
|
355
|
+
boundaryAddr: z.string().trim().url().max(2048).default(DEFAULT_DC_BOUNDARY_ADDR),
|
|
356
|
+
connectTimeoutSeconds: z.number().int().positive().max(120).default(DEFAULT_CONNECT_TIMEOUT_SECONDS),
|
|
357
|
+
humanApprovedSudoRead: z.boolean().default(false).describe("Required for action=run because this uses saved LDAP PROD password for remote sudo."),
|
|
358
|
+
hitlApproval: z.object({
|
|
359
|
+
approved: z.literal(true),
|
|
360
|
+
approvedBy: z.string().trim().min(1).max(256),
|
|
361
|
+
action: z.string().trim().min(1).max(256),
|
|
362
|
+
target: z.string().trim().min(1).max(4096),
|
|
363
|
+
reason: z.string().trim().min(1).max(2000),
|
|
364
|
+
expectedImpact: z.string().trim().min(1).max(2000),
|
|
365
|
+
risk: z.string().trim().min(1).max(2000)
|
|
366
|
+
}).optional().describe("Required for action=run because sudo read access is privileged."),
|
|
367
|
+
timeoutMs: z.number().int().positive().max(600_000).default(DEFAULT_TIMEOUT_MS)
|
|
368
|
+
},
|
|
369
|
+
async handler(input) {
|
|
370
|
+
try {
|
|
371
|
+
return ok(await defaultService.invoke(input));
|
|
372
|
+
} catch (error) {
|
|
373
|
+
return fail(error);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
export default {
|
|
379
|
+
name: sharedTool.name,
|
|
380
|
+
description,
|
|
381
|
+
inputSchema: sharedTool.inputSchema,
|
|
382
|
+
meta: sharedTool.meta,
|
|
383
|
+
annotations: sharedTool.annotations,
|
|
384
|
+
invoke: sharedTool.invoke,
|
|
385
|
+
handler: sharedTool.handler
|
|
386
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
export const description = "Map logical Workday public-cloud environments such as CP2 to concrete SPC clusters, platform-specific login flows, and bounded read-only follow-up commands.";
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { defineReadOnlyTool, fail, ok } from "@nuvlore/extension-read-only-toolkit/devops-diagnostics";
|
|
5
|
+
|
|
6
|
+
export const PUBLIC_CLOUD_ENVIRONMENT_MAP = {
|
|
7
|
+
cp2: {
|
|
8
|
+
environment: "CP2",
|
|
9
|
+
tier: "cust",
|
|
10
|
+
namespace: "bds",
|
|
11
|
+
entries: {
|
|
12
|
+
aws: {
|
|
13
|
+
platform: "AWS",
|
|
14
|
+
cluster: "s0054",
|
|
15
|
+
purpose: "CP2 AWS-backed public-cloud BDS access.",
|
|
16
|
+
loginFlow: [
|
|
17
|
+
"okta2aws login",
|
|
18
|
+
"sso-cli aws --force --profile sso-cust",
|
|
19
|
+
"AWS_PROFILE=sso-cust spc connect cust s0054"
|
|
20
|
+
],
|
|
21
|
+
readOnlyChecks: [
|
|
22
|
+
"kcsb get pod",
|
|
23
|
+
"kubectl --kubeconfig=$HOME/.kube/spc-config -n bds get pods"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
gcs: {
|
|
27
|
+
platform: "GCS",
|
|
28
|
+
cluster: "s0029",
|
|
29
|
+
purpose: "CP2 GCS-backed public-cloud BDS access.",
|
|
30
|
+
loginFlow: [
|
|
31
|
+
"okta2aws login",
|
|
32
|
+
"sso-cli aws --force --profile sso-cust",
|
|
33
|
+
"AWS_PROFILE=sso-cust spc connect cust s0029"
|
|
34
|
+
],
|
|
35
|
+
readOnlyChecks: [
|
|
36
|
+
"kcsb get pod",
|
|
37
|
+
"kcsb exec -it bdm-0 -- bash",
|
|
38
|
+
"gsutil ls gs://scylla-218441665564-bds-tenant-s0020/data/tenants/"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const ENVIRONMENT_ALIASES = new Map([
|
|
46
|
+
["cp2", "cp2"],
|
|
47
|
+
["cp_2", "cp2"],
|
|
48
|
+
["customer_preview_2", "cp2"],
|
|
49
|
+
["customerpreview2", "cp2"]
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const PLATFORM_ALIASES = new Map([
|
|
53
|
+
["aws", "aws"],
|
|
54
|
+
["amazon", "aws"],
|
|
55
|
+
["gcs", "gcs"],
|
|
56
|
+
["gcp", "gcs"],
|
|
57
|
+
["google", "gcs"],
|
|
58
|
+
["google_cloud_storage", "gcs"]
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
function normalizeText(value) {
|
|
62
|
+
return typeof value === "string" ? value.trim() : "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeKey(value) {
|
|
66
|
+
return normalizeText(value).toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeEnvironment(value) {
|
|
70
|
+
const key = normalizeKey(value);
|
|
71
|
+
return ENVIRONMENT_ALIASES.get(key) ?? key;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizePlatform(value) {
|
|
75
|
+
const key = normalizeKey(value);
|
|
76
|
+
return PLATFORM_ALIASES.get(key) ?? key;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildKubectlCommands(envMapping, entry) {
|
|
80
|
+
return {
|
|
81
|
+
publicCloudLoginTool: {
|
|
82
|
+
tool: "workday_public_cloud_spc_login",
|
|
83
|
+
input: {
|
|
84
|
+
action: "login",
|
|
85
|
+
tier: envMapping.tier,
|
|
86
|
+
cluster: entry.cluster,
|
|
87
|
+
namespace: envMapping.namespace,
|
|
88
|
+
humanApprovedLocalWrite: true
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
verify: [
|
|
92
|
+
`AWS_PROFILE=sso-cust kubectl --kubeconfig=$HOME/.kube/spc-config config current-context`,
|
|
93
|
+
`AWS_PROFILE=sso-cust kubectl --kubeconfig=$HOME/.kube/spc-config auth whoami`,
|
|
94
|
+
`AWS_PROFILE=sso-cust kubectl --kubeconfig=$HOME/.kube/spc-config auth can-i get pods -n ${envMapping.namespace}`,
|
|
95
|
+
`AWS_PROFILE=sso-cust kubectl --kubeconfig=$HOME/.kube/spc-config auth can-i get pods/log -n ${envMapping.namespace}`
|
|
96
|
+
],
|
|
97
|
+
readOnly: entry.readOnlyChecks
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function mapEntry(envMapping, platformKey, entry) {
|
|
102
|
+
return {
|
|
103
|
+
environment: envMapping.environment,
|
|
104
|
+
platformKey,
|
|
105
|
+
platform: entry.platform,
|
|
106
|
+
tier: envMapping.tier,
|
|
107
|
+
namespace: envMapping.namespace,
|
|
108
|
+
cluster: entry.cluster,
|
|
109
|
+
purpose: entry.purpose,
|
|
110
|
+
loginFlow: entry.loginFlow,
|
|
111
|
+
commands: buildKubectlCommands(envMapping, entry),
|
|
112
|
+
notes: [
|
|
113
|
+
"Do not specify AWS region by default.",
|
|
114
|
+
"Use kcsb or explicit kubectl so evidence is bounded and auditable.",
|
|
115
|
+
"SPC cluster names are concrete values such as s0054 or s0029; the logical environment CP2 is not itself an SPC cluster name."
|
|
116
|
+
]
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export class WorkdayPublicCloudEnvironmentMapService {
|
|
121
|
+
constructor({ mappings = PUBLIC_CLOUD_ENVIRONMENT_MAP } = {}) {
|
|
122
|
+
this.mappings = mappings;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
invoke(input = {}) {
|
|
126
|
+
const environmentKey = normalizeEnvironment(input.environment || "cp2");
|
|
127
|
+
const platformKey = normalizePlatform(input.platform);
|
|
128
|
+
const envMapping = this.mappings[environmentKey];
|
|
129
|
+
if (!envMapping) {
|
|
130
|
+
throw new Error(`Unsupported public cloud environment mapping: ${input.environment}. Supported: ${Object.keys(this.mappings).join(", ")}.`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const entries = {};
|
|
134
|
+
for (const [key, entry] of Object.entries(envMapping.entries)) {
|
|
135
|
+
if (platformKey && key !== platformKey) continue;
|
|
136
|
+
entries[key] = mapEntry(envMapping, key, entry);
|
|
137
|
+
}
|
|
138
|
+
if (platformKey && Object.keys(entries).length === 0) {
|
|
139
|
+
throw new Error(`Unsupported platform for ${envMapping.environment}: ${input.platform}. Supported: ${Object.keys(envMapping.entries).join(", ")}.`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return JSON.stringify({
|
|
143
|
+
environment: envMapping.environment,
|
|
144
|
+
requestedPlatform: normalizeText(input.platform),
|
|
145
|
+
entries,
|
|
146
|
+
remoteWrite: "not_executed"
|
|
147
|
+
}, null, 2);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const defaultService = new WorkdayPublicCloudEnvironmentMapService();
|
|
152
|
+
|
|
153
|
+
export function workdayPublicCloudEnvironmentMap(input) {
|
|
154
|
+
return defaultService.invoke(input);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const __test = {
|
|
158
|
+
PUBLIC_CLOUD_ENVIRONMENT_MAP,
|
|
159
|
+
WorkdayPublicCloudEnvironmentMapService,
|
|
160
|
+
normalizeEnvironment,
|
|
161
|
+
normalizePlatform,
|
|
162
|
+
workdayPublicCloudEnvironmentMap
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const sharedTool = defineReadOnlyTool({
|
|
166
|
+
name: "workday_public_cloud_environment_map",
|
|
167
|
+
description: "Map logical Workday public-cloud environments such as CP2 to concrete SPC clusters, platform-specific login flows, and bounded read-only follow-up commands.",
|
|
168
|
+
inputSchema: {
|
|
169
|
+
environment: z.string().trim().min(1).max(128).default("CP2"),
|
|
170
|
+
platform: z.string().trim().max(64).default("").describe("Optional platform filter, for example AWS or GCS.")
|
|
171
|
+
},
|
|
172
|
+
async handler(input) {
|
|
173
|
+
try {
|
|
174
|
+
return ok(workdayPublicCloudEnvironmentMap(input));
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return fail(error);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
export default {
|
|
182
|
+
name: sharedTool.name,
|
|
183
|
+
description,
|
|
184
|
+
inputSchema: sharedTool.inputSchema,
|
|
185
|
+
meta: sharedTool.meta,
|
|
186
|
+
annotations: sharedTool.annotations,
|
|
187
|
+
invoke: sharedTool.invoke,
|
|
188
|
+
handler: sharedTool.handler
|
|
189
|
+
};
|