@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,99 @@
|
|
|
1
|
+
# SPC, Argo, And ComponentRelease Access Runbook
|
|
2
|
+
|
|
3
|
+
Use for public cloud SPC connection, Argo UI inspection,
|
|
4
|
+
ComponentRelease/HelmRelease drift, and smoke-test dependency namespace checks.
|
|
5
|
+
Private cloud/DC login uses Boundary through `bauth init` instead. Owner:
|
|
6
|
+
`workday-infra-access/SKILL.md`.
|
|
7
|
+
|
|
8
|
+
## SPC And Argo UI
|
|
9
|
+
|
|
10
|
+
- Confirm Workday Security Network login/session is active before SPC login.
|
|
11
|
+
- If the user gives a logical environment name instead of an SPC cluster, map it
|
|
12
|
+
first with `workday_public_cloud_environment_map`. Do not treat logical names
|
|
13
|
+
like `CP2` as SPC cluster names.
|
|
14
|
+
- Preserve the operator-provided tier and cluster mapping. Current SPC CLI shape
|
|
15
|
+
is `spc [-d] connect <tier> <cluster>`; `-d` means developer access for
|
|
16
|
+
DEV/ENG, not the tier argument. Examples:
|
|
17
|
+
`spc -d connect dev proteusaws00020` and `spc connect cust s0026`.
|
|
18
|
+
- Use `workday_public_cloud_spc_login` for the reusable public-cloud login
|
|
19
|
+
step. It may update local SPC kubeconfig/session state only after structured
|
|
20
|
+
HITL approval.
|
|
21
|
+
- The verified public-cloud login sequence is:
|
|
22
|
+
1. Confirm Workday Security Network is active.
|
|
23
|
+
2. Refresh the AWS source credential with
|
|
24
|
+
`sso-cli aws --force --profile sso-cust`.
|
|
25
|
+
3. Start SPC with `AWS_PROFILE=sso-cust spc connect cust <cluster>`.
|
|
26
|
+
4. Verify with
|
|
27
|
+
`AWS_PROFILE=sso-cust kubectl --kubeconfig=$HOME/.kube/spc-config config current-context`,
|
|
28
|
+
`auth whoami`, and `auth can-i get pods -n <namespace>`.
|
|
29
|
+
Do not export `AWS_REGION` or `AWS_DEFAULT_REGION` into the `spc connect` or
|
|
30
|
+
`kubectl` environment. Do not pass `--region` to `sso-cli aws` unless the
|
|
31
|
+
operator explicitly requests it; forcing a region into SPC caused DynamoDB
|
|
32
|
+
`ResourceNotFoundException` during live testing.
|
|
33
|
+
- Use macOS Keychain service `eldwin`, account `eldwin.workday_prod_ldap` for
|
|
34
|
+
public cloud SPC login across tiers. Legacy `wallee` entries are fallback
|
|
35
|
+
secret sources only. Some entries are JSON; extract only the top-level
|
|
36
|
+
`token` and pass it through `expect`/stdin.
|
|
37
|
+
- `spc` requires a TTY password prompt. A plain pipe may fail with
|
|
38
|
+
`inappropriate ioctl for device`; use an `expect` wrapper. If `spc` exits
|
|
39
|
+
before a password prompt during debugging, a TTY wrapper such as
|
|
40
|
+
`/usr/bin/script -q /tmp/spc-<cluster>.out spc connect <tier> <cluster>` can
|
|
41
|
+
establish the session without exposing the secret.
|
|
42
|
+
- Ensure local `PATH` includes `session-manager-plugin`, `sso-cli`, `kubectl`,
|
|
43
|
+
and `spc`; missing PATH entries can make `spc connect` or kubeconfig exec auth
|
|
44
|
+
fail even when binaries are installed.
|
|
45
|
+
- On this workstation the verified binary locations are:
|
|
46
|
+
`/Users/boqiang.liang/bin/sso-cli`,
|
|
47
|
+
`/usr/local/bin/spc`,
|
|
48
|
+
`/usr/local/bin/kubectl`, and
|
|
49
|
+
`/usr/local/bin/session-manager-plugin`. Non-login shells may not include
|
|
50
|
+
these paths by default, so tools must set the PATH explicitly.
|
|
51
|
+
- `spc list cust --url` is a useful preflight after AWS refresh. SPC cluster
|
|
52
|
+
names are values such as `s0026` and `s0109`; names like `wd103perf` can be
|
|
53
|
+
AWS profile aliases or legacy kube contexts and are not necessarily valid SPC
|
|
54
|
+
cluster names.
|
|
55
|
+
- CP2 examples:
|
|
56
|
+
- AWS-backed public cloud: refresh AWS with `sso-cli aws --force --profile
|
|
57
|
+
sso-cust`, then `AWS_PROFILE=sso-cust spc connect cust s0054`, then use
|
|
58
|
+
bounded `kcsb get pod` or explicit `kubectl --kubeconfig=$HOME/.kube/spc-config`
|
|
59
|
+
reads.
|
|
60
|
+
- GCS-backed public cloud: refresh AWS with `sso-cli aws --force --profile
|
|
61
|
+
sso-cust`, then `AWS_PROFILE=sso-cust spc connect cust s0029`, then use
|
|
62
|
+
bounded `kcsb get pod`; if approved read-only pod shell access is needed,
|
|
63
|
+
`kcsb exec -it bdm-0 -- bash` may be used only for bounded reads such as
|
|
64
|
+
`gsutil ls gs://scylla-218441665564-bds-tenant-s0020/data/tenants/`.
|
|
65
|
+
- Use `kcsb` or explicit `kubectl` commands so evidence is copyable, bounded,
|
|
66
|
+
and auditable.
|
|
67
|
+
- Clean up long-running `spc connect` and port-forward processes when done.
|
|
68
|
+
- After `spc` is connected, Argo UI can be inspected through
|
|
69
|
+
`kubectl --kubeconfig=$HOME/.kube/spc-config -n argocdapp port-forward svc/argocdapp-server 8080:443`
|
|
70
|
+
and `https://localhost:8080/`.
|
|
71
|
+
- `spc connect` proves connectivity, not Kubernetes authorization. Before any
|
|
72
|
+
approved remediation such as `kubectl scale`, `patch`, or `rollout restart`,
|
|
73
|
+
verify identity with `kubectl auth whoami` and the target verb with
|
|
74
|
+
`kubectl auth can-i ... -n <namespace>`.
|
|
75
|
+
- Argo UI inspection is read-only only if you avoid state-changing controls:
|
|
76
|
+
`Sync`, `Hard Refresh`, `Delete`, `Rollback`, `Scale`, `Patch`, and `Restart`.
|
|
77
|
+
- Prefer CLI reads where possible:
|
|
78
|
+
`kubectl --kubeconfig=$HOME/.kube/spc-config get app -n argocdapp ... -o json`,
|
|
79
|
+
`kubectl get componentrelease -n <namespace> ...`, and
|
|
80
|
+
`kubectl get deploy,statefulset,pods -n <namespace> -o wide`.
|
|
81
|
+
- Before claiming a service, pod, or log is absent, check namespace RBAC with
|
|
82
|
+
`kubectl --kubeconfig=$HOME/.kube/spc-config auth can-i get pods -n <namespace>`,
|
|
83
|
+
plus equivalent service, endpoints, deployment, and logs checks.
|
|
84
|
+
|
|
85
|
+
## ComponentRelease And Smoke-Test Dependencies
|
|
86
|
+
|
|
87
|
+
- For BDS/Proteus ComponentRelease investigations, verify all five layers:
|
|
88
|
+
scylla `component-config` source of truth, Argo Application sync/health,
|
|
89
|
+
ComponentRelease desired/deployed status, Flux HelmRelease desired status and
|
|
90
|
+
`spec.driftDetection`, and live Deployment/StatefulSet/pod replicas.
|
|
91
|
+
- Treat `HelmRelease.spec.driftDetection` as a Flux Helm Controller setting. If
|
|
92
|
+
it is null/disabled and live workloads drift from non-zero desired replicas,
|
|
93
|
+
the deeper platform question is whether `isd-component-controller` and the
|
|
94
|
+
ComponentRelease CRD/default HelmRelease spec expose or set drift detection.
|
|
95
|
+
- When a Bamboo smoke test waits on a dependency selector such as `name=prism`,
|
|
96
|
+
inspect that dependency namespace directly. Compare ComponentRelease and
|
|
97
|
+
HelmRelease desired state with live deployments, pods selected by the exact
|
|
98
|
+
label, and service endpoints; do not assume a successful ComponentRelease
|
|
99
|
+
means dependency pods are running.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
export const DEFAULT_DC_BOUNDARY_ADDR = "https://dccust.pam.gowday.com";
|
|
2
|
+
export const DEFAULT_BOUNDARY_BINARY_DIR = "/opt/homebrew/bin";
|
|
3
|
+
export const DEFAULT_BOUNDARY_PROXY_JUMP_HOST = "proxy-jump.atl.bnd";
|
|
4
|
+
export const DEFAULT_WORKDAY_LDAP_KEYCHAIN_SERVICE = "eldwin";
|
|
5
|
+
export const DEFAULT_WORKDAY_LDAP_SERVICE_KEY = "workday_prod_ldap";
|
|
6
|
+
export const DEFAULT_WORKDAY_LDAP_PROD_SERVICE_KEY = "workday_prod_ldap";
|
|
7
|
+
export const DEFAULT_WORKDAY_LDAP_CORP_SERVICE_KEY = "workday_corp_ldap";
|
|
8
|
+
export const DEFAULT_WORKDAY_LDAP_KEYCHAIN_ACCOUNT = "eldwin.workday_prod_ldap";
|
|
9
|
+
export const DEFAULT_WORKDAY_LDAP_PROD_KEYCHAIN_ACCOUNT = "eldwin.workday_prod_ldap";
|
|
10
|
+
export const DEFAULT_WORKDAY_LDAP_CORP_KEYCHAIN_ACCOUNT = "eldwin.workday_corp_ldap";
|
|
11
|
+
export const LEGACY_WORKDAY_LDAP_KEYCHAIN_SERVICE = ["wal", "lee"].join("");
|
|
12
|
+
export const LEGACY_WORKDAY_LDAP_KEYCHAIN_ACCOUNT = `${LEGACY_WORKDAY_LDAP_KEYCHAIN_SERVICE}.workday_prod_ldap`;
|
|
13
|
+
export const LEGACY_WORKDAY_LDAP_PROD_KEYCHAIN_ACCOUNT = `${LEGACY_WORKDAY_LDAP_KEYCHAIN_SERVICE}.workday_prod_ldap`;
|
|
14
|
+
export const LEGACY_WORKDAY_LDAP_CORP_KEYCHAIN_ACCOUNT = `${LEGACY_WORKDAY_LDAP_KEYCHAIN_SERVICE}.workday_corp_ldap`;
|
|
15
|
+
export const DEFAULT_ADMIN_KUBECONFIG_PATH = "/etc/kubernetes/admin.conf";
|
|
16
|
+
export const DEFAULT_SCYLLA_NAMESPACE = "bds";
|
|
17
|
+
export const DEFAULT_SCYLLA_BDM_POD = "bdm-0";
|
|
18
|
+
|
|
19
|
+
function normalizeText(value) {
|
|
20
|
+
return typeof value === "string" ? value.trim() : "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function shellQuote(value) {
|
|
24
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function extractTokenFromKeychainSecret(rawSecret = "") {
|
|
28
|
+
const text = normalizeText(rawSecret);
|
|
29
|
+
if (!text) return "";
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(text);
|
|
32
|
+
return normalizeText(parsed?.token);
|
|
33
|
+
} catch {
|
|
34
|
+
return text;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function keychainAccountCandidates(account, { canonicalPrefix = "eldwin." } = {}) {
|
|
39
|
+
const raw = normalizeText(account);
|
|
40
|
+
if (!raw) return [];
|
|
41
|
+
const prefixed = raw.startsWith(canonicalPrefix) ? raw : `${canonicalPrefix}${raw}`;
|
|
42
|
+
return Array.from(new Set([raw, prefixed]));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function uniqueCredentialCandidates(candidates = []) {
|
|
46
|
+
return candidates
|
|
47
|
+
.filter((candidate) => candidate?.service && candidate?.account)
|
|
48
|
+
.filter((candidate, index, all) =>
|
|
49
|
+
all.findIndex((item) => item.service === candidate.service && item.account === candidate.account) === index
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function workdayProdCredentialCandidates() {
|
|
54
|
+
return uniqueCredentialCandidates([
|
|
55
|
+
{ service: DEFAULT_WORKDAY_LDAP_KEYCHAIN_SERVICE, account: DEFAULT_WORKDAY_LDAP_PROD_KEYCHAIN_ACCOUNT },
|
|
56
|
+
{ service: LEGACY_WORKDAY_LDAP_KEYCHAIN_SERVICE, account: LEGACY_WORKDAY_LDAP_PROD_KEYCHAIN_ACCOUNT }
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class DcLoginProfile {
|
|
61
|
+
constructor(input = {}) {
|
|
62
|
+
this.boundaryAddr = input.boundaryAddr ?? DEFAULT_DC_BOUNDARY_ADDR;
|
|
63
|
+
this.boundaryBinaryDir = input.boundaryBinaryDir ?? DEFAULT_BOUNDARY_BINARY_DIR;
|
|
64
|
+
this.boundaryProxyJumpHost = input.boundaryProxyJumpHost ?? DEFAULT_BOUNDARY_PROXY_JUMP_HOST;
|
|
65
|
+
this.useBoundaryBnd = input.useBoundaryBnd ?? false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
boundaryHostFor(host) {
|
|
69
|
+
const value = String(host || "");
|
|
70
|
+
if (!this.useBoundaryBnd || value.includes("<") || value.endsWith(".bnd")) return value;
|
|
71
|
+
return `${value}.bnd`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
environmentPrefix() {
|
|
75
|
+
return `BOUNDARY_ADDR=${shellQuote(this.boundaryAddr)} BOUNDARY_SKIP_CACHE_DAEMON=true PATH=${shellQuote(this.boundaryBinaryDir)}:/Users/boqiang.liang/bin:/usr/local/bin:$PATH`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
sshCommandFor(host, remoteCommand, { connectTimeout = 20 } = {}) {
|
|
79
|
+
return `${this.environmentPrefix()} ssh -o BatchMode=yes -o ConnectTimeout=${connectTimeout} -- ${this.boundaryHostFor(host)} ${JSON.stringify(remoteCommand)}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
proxyJumpCommandFor(host, remoteCommand, { connectTimeout = 20 } = {}) {
|
|
83
|
+
return `${this.environmentPrefix()} ssh -o BatchMode=yes -o ConnectTimeout=${connectTimeout} -J ${shellQuote(this.boundaryProxyJumpHost)} -- ${host} ${JSON.stringify(remoteCommand)}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
toJson() {
|
|
87
|
+
return {
|
|
88
|
+
boundaryAddr: this.boundaryAddr,
|
|
89
|
+
boundaryBinaryDir: this.boundaryBinaryDir,
|
|
90
|
+
boundaryProxyJumpHost: this.boundaryProxyJumpHost,
|
|
91
|
+
useBoundaryBnd: this.useBoundaryBnd
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class SudoEnvironmentProfile {
|
|
97
|
+
constructor(input = {}) {
|
|
98
|
+
this.keychainService = input.keychainService ?? DEFAULT_WORKDAY_LDAP_KEYCHAIN_SERVICE;
|
|
99
|
+
this.keychainAccount = input.keychainAccount ?? DEFAULT_WORKDAY_LDAP_KEYCHAIN_ACCOUNT;
|
|
100
|
+
this.fallbackCredentials = input.fallbackCredentials ?? workdayProdCredentialCandidates().slice(1);
|
|
101
|
+
this.kubeconfigPath = input.kubeconfigPath ?? DEFAULT_ADMIN_KUBECONFIG_PATH;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
keychainCandidates() {
|
|
105
|
+
return uniqueCredentialCandidates([
|
|
106
|
+
{ service: this.keychainService, account: this.keychainAccount },
|
|
107
|
+
...this.fallbackCredentials
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
keychainReadCommand() {
|
|
112
|
+
return "Direct PROD password reads are restricted. Use workday_private_cloud_sudo_readonly_command.";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
stdinSudoRemoteCommand(command) {
|
|
116
|
+
return `IFS= read -r SUDOPASS || exit 11; printf '%s\\n' "$SUDOPASS" | sudo -S -p '' bash -lc ${JSON.stringify(command)}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
sudoWrapperFor({ dcProfile, host, command }) {
|
|
120
|
+
const target = dcProfile.boundaryHostFor(host);
|
|
121
|
+
return `mcp__devops_tool__workday_private_cloud_sudo_readonly_command ${JSON.stringify({
|
|
122
|
+
action: "run",
|
|
123
|
+
host: target,
|
|
124
|
+
command,
|
|
125
|
+
humanApprovedSudoRead: true,
|
|
126
|
+
hitlApproval: {
|
|
127
|
+
approved: true,
|
|
128
|
+
approvedBy: "<operator>",
|
|
129
|
+
action: "Run private-cloud sudo read-only command",
|
|
130
|
+
target,
|
|
131
|
+
reason: "<why this sudo read is needed>",
|
|
132
|
+
expectedImpact: "Read-only sudo command; no remote writes.",
|
|
133
|
+
risk: "Privileged read may expose sensitive host state."
|
|
134
|
+
}
|
|
135
|
+
})}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
sudoKubectl(command) {
|
|
139
|
+
return `sudo -n kubectl --kubeconfig=${this.kubeconfigPath} ${command}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
verificationCommand({ host, dcProfile = new DcLoginProfile() }) {
|
|
143
|
+
return this.sudoWrapperFor({ dcProfile, host, command: "hostname" });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
toJson() {
|
|
147
|
+
return {
|
|
148
|
+
keychainService: this.keychainService,
|
|
149
|
+
keychainAccount: this.keychainAccount,
|
|
150
|
+
keychainCandidates: this.keychainCandidates(),
|
|
151
|
+
kubeconfigPath: this.kubeconfigPath
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export class ScyllaAccessProfile {
|
|
157
|
+
constructor(input = {}) {
|
|
158
|
+
this.cluster = input.cluster;
|
|
159
|
+
this.namespace = input.namespace ?? DEFAULT_SCYLLA_NAMESPACE;
|
|
160
|
+
this.bdmPod = input.bdmPod ?? DEFAULT_SCYLLA_BDM_POD;
|
|
161
|
+
this.kubeconfigPath = input.kubeconfigPath ?? "$HOME/.kube/spc-config";
|
|
162
|
+
this.connectionTool = input.connectionTool ?? "kcsb";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
podExec(command, { container } = {}) {
|
|
166
|
+
const containerArg = container ? ` -c ${container}` : "";
|
|
167
|
+
return `${this.connectionTool} exec ${this.bdmPod}${containerArg} -- sh -lc ${shellQuote(command)}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
podLogs({ pod = this.bdmPod, container = "bdm", since = "4h", pattern = "" } = {}) {
|
|
171
|
+
const grep = pattern ? ` | egrep ${shellQuote(pattern)}` : "";
|
|
172
|
+
return `${this.connectionTool} logs ${pod} -c ${container} --since=${since}${grep}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
bucketConfigCommand() {
|
|
176
|
+
return `${this.connectionTool} get cm bucketdetails-config -o yaml`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
rbacBoundaryCommand(namespaces = ["sir", "galahad", "ors", "oms", "argocdapp", "bds"]) {
|
|
180
|
+
return `for ns in ${namespaces.map(shellQuote).join(" ")}; do for r in pods services endpoints deployments.apps; do kubectl --kubeconfig=${this.kubeconfigPath} auth can-i get "$r" -n "$ns"; done; done`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
toJson() {
|
|
184
|
+
return {
|
|
185
|
+
cluster: this.cluster,
|
|
186
|
+
namespace: this.namespace,
|
|
187
|
+
bdmPod: this.bdmPod,
|
|
188
|
+
kubeconfigPath: this.kubeconfigPath,
|
|
189
|
+
connectionTool: this.connectionTool,
|
|
190
|
+
bucketConfigCommand: this.bucketConfigCommand(),
|
|
191
|
+
rbacBoundaryCommand: this.rbacBoundaryCommand()
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
export const description = "Create a local-only AWS CUST federated access read-only CLI command pack based on the approved CCOE guide; does not authenticate, read credentials, or call AWS APIs.";
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { defineReadOnlyTool, fail, ok } from "@nuvlore/extension-read-only-toolkit/devops-diagnostics";
|
|
5
|
+
|
|
6
|
+
const AWS_FEDERATED_ACCESS_GUIDE_URL = "https://confluence.workday.com/display/CCOE/AWS+-+Federated+Access+to+CUST+Accounts+-+User+Guide";
|
|
7
|
+
const DEFAULT_REGION = "us-west-2";
|
|
8
|
+
const READ_ONLY_SERVICES = new Set([
|
|
9
|
+
"account",
|
|
10
|
+
"cloudformation",
|
|
11
|
+
"cloudtrail",
|
|
12
|
+
"cloudwatch",
|
|
13
|
+
"ec2",
|
|
14
|
+
"eks",
|
|
15
|
+
"elb",
|
|
16
|
+
"iam",
|
|
17
|
+
"kms",
|
|
18
|
+
"lambda",
|
|
19
|
+
"logs",
|
|
20
|
+
"rds",
|
|
21
|
+
"route53",
|
|
22
|
+
"s3",
|
|
23
|
+
"s3api",
|
|
24
|
+
"secretsmanager",
|
|
25
|
+
"sts"
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function normalizeText(value) {
|
|
29
|
+
return typeof value === "string" ? value.trim() : "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function shellQuote(value) {
|
|
33
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeService(value) {
|
|
37
|
+
const service = normalizeText(value || "sts").toLowerCase();
|
|
38
|
+
if (!READ_ONLY_SERVICES.has(service)) {
|
|
39
|
+
throw new Error(`Unsupported AWS read-only service: ${service}. Supported services: ${[...READ_ONLY_SERVICES].sort().join(", ")}.`);
|
|
40
|
+
}
|
|
41
|
+
return service;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeInput(input = {}) {
|
|
45
|
+
return {
|
|
46
|
+
accountId: normalizeText(input.accountId),
|
|
47
|
+
accountAlias: normalizeText(input.accountAlias),
|
|
48
|
+
roleName: normalizeText(input.roleName),
|
|
49
|
+
profile: normalizeText(input.profile),
|
|
50
|
+
region: normalizeText(input.region) || DEFAULT_REGION,
|
|
51
|
+
service: normalizeService(input.service),
|
|
52
|
+
resource: normalizeText(input.resource),
|
|
53
|
+
guideUrl: normalizeText(input.guideUrl) || AWS_FEDERATED_ACCESS_GUIDE_URL
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function baseAws(input) {
|
|
58
|
+
const profile = input.profile ? ` --profile ${shellQuote(input.profile)}` : "";
|
|
59
|
+
const region = input.region ? ` --region ${shellQuote(input.region)}` : "";
|
|
60
|
+
return `aws${profile}${region}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function serviceCommands(input) {
|
|
64
|
+
const aws = baseAws(input);
|
|
65
|
+
const resource = input.resource;
|
|
66
|
+
switch (input.service) {
|
|
67
|
+
case "account":
|
|
68
|
+
return [
|
|
69
|
+
`${aws} account get-contact-information`,
|
|
70
|
+
`${aws} account get-alternate-contact --alternate-contact-type OPERATIONS`,
|
|
71
|
+
`${aws} account get-region-opt-status --region-name ${shellQuote(input.region)}`
|
|
72
|
+
];
|
|
73
|
+
case "cloudformation":
|
|
74
|
+
return [
|
|
75
|
+
`${aws} cloudformation list-stacks --stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE UPDATE_ROLLBACK_COMPLETE ROLLBACK_COMPLETE`,
|
|
76
|
+
resource ? `${aws} cloudformation describe-stacks --stack-name ${shellQuote(resource)}` : `${aws} cloudformation describe-stacks`
|
|
77
|
+
];
|
|
78
|
+
case "cloudtrail":
|
|
79
|
+
return [
|
|
80
|
+
`${aws} cloudtrail describe-trails`,
|
|
81
|
+
`${aws} cloudtrail get-trail-status --name ${shellQuote(resource || "trail-name")}`
|
|
82
|
+
];
|
|
83
|
+
case "cloudwatch":
|
|
84
|
+
return [
|
|
85
|
+
`${aws} cloudwatch describe-alarms`,
|
|
86
|
+
`${aws} cloudwatch list-metrics`
|
|
87
|
+
];
|
|
88
|
+
case "ec2":
|
|
89
|
+
return [
|
|
90
|
+
`${aws} ec2 describe-instances`,
|
|
91
|
+
`${aws} ec2 describe-security-groups`,
|
|
92
|
+
`${aws} ec2 describe-subnets`,
|
|
93
|
+
`${aws} ec2 describe-vpcs`
|
|
94
|
+
];
|
|
95
|
+
case "eks":
|
|
96
|
+
return [
|
|
97
|
+
`${aws} eks list-clusters`,
|
|
98
|
+
resource ? `${aws} eks describe-cluster --name ${shellQuote(resource)}` : `${aws} eks describe-cluster --name ${shellQuote("cluster-name")}`
|
|
99
|
+
];
|
|
100
|
+
case "elb":
|
|
101
|
+
return [
|
|
102
|
+
`${aws} elbv2 describe-load-balancers`,
|
|
103
|
+
`${aws} elbv2 describe-target-groups`
|
|
104
|
+
];
|
|
105
|
+
case "iam":
|
|
106
|
+
return [
|
|
107
|
+
`${aws} iam get-account-summary`,
|
|
108
|
+
`${aws} iam list-roles`,
|
|
109
|
+
resource ? `${aws} iam get-role --role-name ${shellQuote(resource)}` : `${aws} iam list-policies --scope Local`
|
|
110
|
+
];
|
|
111
|
+
case "kms":
|
|
112
|
+
return [
|
|
113
|
+
`${aws} kms list-keys`,
|
|
114
|
+
resource ? `${aws} kms describe-key --key-id ${shellQuote(resource)}` : `${aws} kms list-aliases`
|
|
115
|
+
];
|
|
116
|
+
case "lambda":
|
|
117
|
+
return [
|
|
118
|
+
`${aws} lambda list-functions`,
|
|
119
|
+
resource ? `${aws} lambda get-function-configuration --function-name ${shellQuote(resource)}` : `${aws} lambda list-event-source-mappings`
|
|
120
|
+
];
|
|
121
|
+
case "logs":
|
|
122
|
+
return [
|
|
123
|
+
`${aws} logs describe-log-groups`,
|
|
124
|
+
resource ? `${aws} logs describe-log-streams --log-group-name ${shellQuote(resource)} --max-items 25` : `${aws} logs describe-log-groups --max-items 50`
|
|
125
|
+
];
|
|
126
|
+
case "rds":
|
|
127
|
+
return [
|
|
128
|
+
`${aws} rds describe-db-instances`,
|
|
129
|
+
`${aws} rds describe-db-clusters`
|
|
130
|
+
];
|
|
131
|
+
case "route53":
|
|
132
|
+
return [
|
|
133
|
+
`${aws} route53 list-hosted-zones`,
|
|
134
|
+
resource ? `${aws} route53 list-resource-record-sets --hosted-zone-id ${shellQuote(resource)}` : `${aws} route53 list-health-checks`
|
|
135
|
+
];
|
|
136
|
+
case "s3":
|
|
137
|
+
return [
|
|
138
|
+
`${aws} s3 ls${resource ? ` ${shellQuote(resource)}` : ""}`,
|
|
139
|
+
`${aws} s3 ls${resource ? ` ${shellQuote(resource)}` : " s3://<bucket>"} --recursive --summarize --human-readable`
|
|
140
|
+
];
|
|
141
|
+
case "s3api":
|
|
142
|
+
return [
|
|
143
|
+
resource ? `${aws} s3api get-bucket-location --bucket ${shellQuote(resource)}` : `${aws} s3api list-buckets`,
|
|
144
|
+
resource ? `${aws} s3api get-bucket-encryption --bucket ${shellQuote(resource)}` : `${aws} s3api list-buckets`
|
|
145
|
+
];
|
|
146
|
+
case "secretsmanager":
|
|
147
|
+
return [
|
|
148
|
+
`${aws} secretsmanager list-secrets`,
|
|
149
|
+
resource ? `${aws} secretsmanager describe-secret --secret-id ${shellQuote(resource)}` : `${aws} secretsmanager list-secrets --max-results 50`
|
|
150
|
+
];
|
|
151
|
+
case "sts":
|
|
152
|
+
default:
|
|
153
|
+
return [
|
|
154
|
+
`${aws} sts get-caller-identity`
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function awsCustFederatedReadonlyPlan(input) {
|
|
160
|
+
const normalized = normalizeInput(input);
|
|
161
|
+
return JSON.stringify({
|
|
162
|
+
mode: "atomic-helper",
|
|
163
|
+
platform: "aws",
|
|
164
|
+
accessPattern: "cust-federated-readonly",
|
|
165
|
+
sourceGuide: {
|
|
166
|
+
title: "AWS - Federated Access to CUST Accounts - User Guide",
|
|
167
|
+
url: normalized.guideUrl,
|
|
168
|
+
status: "referenced; not executed by this tool"
|
|
169
|
+
},
|
|
170
|
+
target: {
|
|
171
|
+
accountId: normalized.accountId,
|
|
172
|
+
accountAlias: normalized.accountAlias,
|
|
173
|
+
roleName: normalized.roleName,
|
|
174
|
+
profile: normalized.profile,
|
|
175
|
+
region: normalized.region,
|
|
176
|
+
service: normalized.service,
|
|
177
|
+
resource: normalized.resource
|
|
178
|
+
},
|
|
179
|
+
credentialBoundary: [
|
|
180
|
+
"Complete AWS CUST federated access outside this tool using the approved CCOE guide.",
|
|
181
|
+
"This tool never opens a browser login, never stores SAML/OIDC tokens, and never reads AWS credential files.",
|
|
182
|
+
"After federation, use an explicit AWS_PROFILE or exported session supplied by the operator."
|
|
183
|
+
],
|
|
184
|
+
commands: {
|
|
185
|
+
identity: `${baseAws(normalized)} sts get-caller-identity`,
|
|
186
|
+
serviceReads: serviceCommands(normalized)
|
|
187
|
+
},
|
|
188
|
+
forbiddenActions: [
|
|
189
|
+
"aws s3 cp",
|
|
190
|
+
"aws s3 mv",
|
|
191
|
+
"aws s3 rm",
|
|
192
|
+
"aws s3 sync",
|
|
193
|
+
"aws s3api put-object",
|
|
194
|
+
"aws s3api delete-object",
|
|
195
|
+
"aws ec2 start-instances",
|
|
196
|
+
"aws ec2 stop-instances",
|
|
197
|
+
"aws eks update-*",
|
|
198
|
+
"aws iam create-*",
|
|
199
|
+
"aws iam put-*",
|
|
200
|
+
"aws secretsmanager get-secret-value"
|
|
201
|
+
],
|
|
202
|
+
remoteWrite: "not_executed"
|
|
203
|
+
}, null, 2);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export default defineReadOnlyTool({
|
|
207
|
+
name: "aws_cust_federated_readonly_command_pack",
|
|
208
|
+
description: "Create a local-only AWS CUST federated access read-only CLI command pack based on the approved CCOE guide; does not authenticate, read credentials, or call AWS APIs.",
|
|
209
|
+
inputSchema: {
|
|
210
|
+
accountId: z.string().trim().max(64).default(""),
|
|
211
|
+
accountAlias: z.string().trim().max(256).default(""),
|
|
212
|
+
roleName: z.string().trim().max(256).default(""),
|
|
213
|
+
profile: z.string().trim().max(256).default(""),
|
|
214
|
+
region: z.string().trim().max(64).default(DEFAULT_REGION),
|
|
215
|
+
service: z.string().trim().max(64).default("sts"),
|
|
216
|
+
resource: z.string().trim().max(2048).default(""),
|
|
217
|
+
guideUrl: z.string().trim().url().max(2048).default(AWS_FEDERATED_ACCESS_GUIDE_URL)
|
|
218
|
+
},
|
|
219
|
+
async handler(input) {
|
|
220
|
+
try {
|
|
221
|
+
return ok(awsCustFederatedReadonlyPlan(input));
|
|
222
|
+
} catch (error) {
|
|
223
|
+
return fail(error);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
export const __test = {
|
|
229
|
+
AWS_FEDERATED_ACCESS_GUIDE_URL,
|
|
230
|
+
READ_ONLY_SERVICES,
|
|
231
|
+
normalizeInput,
|
|
232
|
+
serviceCommands,
|
|
233
|
+
awsCustFederatedReadonlyPlan
|
|
234
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
export const description = "Create a local-only GCP read-only CLI command pack; does not authenticate, read credentials, or call GCP APIs.";
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { defineReadOnlyTool, fail, ok } from "@nuvlore/extension-read-only-toolkit/devops-diagnostics";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_REGION = "us-west1";
|
|
7
|
+
const READ_ONLY_SERVICES = new Set([
|
|
8
|
+
"billing",
|
|
9
|
+
"compute",
|
|
10
|
+
"gke",
|
|
11
|
+
"iam",
|
|
12
|
+
"logging",
|
|
13
|
+
"monitoring",
|
|
14
|
+
"projects",
|
|
15
|
+
"storage"
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function normalizeText(value) {
|
|
19
|
+
return typeof value === "string" ? value.trim() : "";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function shellQuote(value) {
|
|
23
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeService(value) {
|
|
27
|
+
const service = normalizeText(value || "projects").toLowerCase();
|
|
28
|
+
if (!READ_ONLY_SERVICES.has(service)) {
|
|
29
|
+
throw new Error(`Unsupported GCP read-only service: ${service}. Supported services: ${[...READ_ONLY_SERVICES].sort().join(", ")}.`);
|
|
30
|
+
}
|
|
31
|
+
return service;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeInput(input = {}) {
|
|
35
|
+
return {
|
|
36
|
+
projectId: normalizeText(input.projectId),
|
|
37
|
+
account: normalizeText(input.account),
|
|
38
|
+
region: normalizeText(input.region) || DEFAULT_REGION,
|
|
39
|
+
zone: normalizeText(input.zone),
|
|
40
|
+
service: normalizeService(input.service),
|
|
41
|
+
resource: normalizeText(input.resource)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function baseGcloud(input) {
|
|
46
|
+
const project = input.projectId ? ` --project=${shellQuote(input.projectId)}` : "";
|
|
47
|
+
const account = input.account ? ` --account=${shellQuote(input.account)}` : "";
|
|
48
|
+
return `gcloud${project}${account}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function serviceCommands(input) {
|
|
52
|
+
const gcloud = baseGcloud(input);
|
|
53
|
+
const resource = input.resource;
|
|
54
|
+
switch (input.service) {
|
|
55
|
+
case "billing":
|
|
56
|
+
return [
|
|
57
|
+
`${gcloud} billing projects describe ${shellQuote(input.projectId || "PROJECT_ID")}`
|
|
58
|
+
];
|
|
59
|
+
case "compute":
|
|
60
|
+
return [
|
|
61
|
+
`${gcloud} compute instances list`,
|
|
62
|
+
resource ? `${gcloud} compute instances describe ${shellQuote(resource)}${input.zone ? ` --zone=${shellQuote(input.zone)}` : ""}` : `${gcloud} compute networks list`,
|
|
63
|
+
`${gcloud} compute firewall-rules list`
|
|
64
|
+
];
|
|
65
|
+
case "gke":
|
|
66
|
+
return [
|
|
67
|
+
`${gcloud} container clusters list`,
|
|
68
|
+
resource ? `${gcloud} container clusters describe ${shellQuote(resource)}${input.region ? ` --region=${shellQuote(input.region)}` : ""}` : `${gcloud} container clusters list`
|
|
69
|
+
];
|
|
70
|
+
case "iam":
|
|
71
|
+
return [
|
|
72
|
+
`${gcloud} projects get-iam-policy ${shellQuote(input.projectId || "PROJECT_ID")}`,
|
|
73
|
+
`${gcloud} iam service-accounts list`
|
|
74
|
+
];
|
|
75
|
+
case "logging":
|
|
76
|
+
return [
|
|
77
|
+
`${gcloud} logging sinks list`,
|
|
78
|
+
`${gcloud} logging read ${shellQuote(resource || "timestamp >= \"$(date -u -v-1H +%Y-%m-%dT%H:%M:%SZ)\"")} --limit=50`
|
|
79
|
+
];
|
|
80
|
+
case "monitoring":
|
|
81
|
+
return [
|
|
82
|
+
`${gcloud} monitoring policies list`,
|
|
83
|
+
`${gcloud} monitoring uptime list-configs`
|
|
84
|
+
];
|
|
85
|
+
case "storage":
|
|
86
|
+
return [
|
|
87
|
+
`gcloud storage buckets list${input.projectId ? ` --project=${shellQuote(input.projectId)}` : ""}`,
|
|
88
|
+
resource ? `gcloud storage objects list ${shellQuote(resource)}` : "gcloud storage objects list gs://<bucket>"
|
|
89
|
+
];
|
|
90
|
+
case "projects":
|
|
91
|
+
default:
|
|
92
|
+
return [
|
|
93
|
+
`${gcloud} auth list`,
|
|
94
|
+
`${gcloud} config list`,
|
|
95
|
+
`${gcloud} projects describe ${shellQuote(input.projectId || "PROJECT_ID")}`
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function gcpReadonlyAccessPlan(input) {
|
|
101
|
+
const normalized = normalizeInput(input);
|
|
102
|
+
return JSON.stringify({
|
|
103
|
+
mode: "atomic-helper",
|
|
104
|
+
platform: "gcp",
|
|
105
|
+
accessPattern: "readonly-cli",
|
|
106
|
+
target: {
|
|
107
|
+
projectId: normalized.projectId,
|
|
108
|
+
account: normalized.account,
|
|
109
|
+
region: normalized.region,
|
|
110
|
+
zone: normalized.zone,
|
|
111
|
+
service: normalized.service,
|
|
112
|
+
resource: normalized.resource
|
|
113
|
+
},
|
|
114
|
+
credentialBoundary: [
|
|
115
|
+
"Use an already-approved local gcloud identity; this tool does not run gcloud auth login or read credential files.",
|
|
116
|
+
"Run auth list/config list first and verify the active account and project match incident evidence.",
|
|
117
|
+
"Do not change active config, impersonation, IAM policy, project settings, buckets, clusters, instances, or logs."
|
|
118
|
+
],
|
|
119
|
+
commands: {
|
|
120
|
+
identity: [
|
|
121
|
+
`${baseGcloud(normalized)} auth list`,
|
|
122
|
+
`${baseGcloud(normalized)} config list`
|
|
123
|
+
],
|
|
124
|
+
serviceReads: serviceCommands(normalized)
|
|
125
|
+
},
|
|
126
|
+
forbiddenActions: [
|
|
127
|
+
"gcloud auth login",
|
|
128
|
+
"gcloud config set",
|
|
129
|
+
"gcloud projects set-iam-policy",
|
|
130
|
+
"gcloud iam service-accounts keys create",
|
|
131
|
+
"gcloud compute instances start",
|
|
132
|
+
"gcloud compute instances stop",
|
|
133
|
+
"gcloud container clusters update",
|
|
134
|
+
"gcloud storage cp",
|
|
135
|
+
"gcloud storage rm",
|
|
136
|
+
"gcloud storage mv"
|
|
137
|
+
],
|
|
138
|
+
remoteWrite: "not_executed"
|
|
139
|
+
}, null, 2);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default defineReadOnlyTool({
|
|
143
|
+
name: "gcp_readonly_command_pack",
|
|
144
|
+
description: "Create a local-only GCP read-only CLI command pack; does not authenticate, read credentials, or call GCP APIs.",
|
|
145
|
+
inputSchema: {
|
|
146
|
+
projectId: z.string().trim().max(256).default(""),
|
|
147
|
+
account: z.string().trim().max(256).default(""),
|
|
148
|
+
region: z.string().trim().max(64).default(DEFAULT_REGION),
|
|
149
|
+
zone: z.string().trim().max(64).default(""),
|
|
150
|
+
service: z.string().trim().max(64).default("projects"),
|
|
151
|
+
resource: z.string().trim().max(2048).default("")
|
|
152
|
+
},
|
|
153
|
+
async handler(input) {
|
|
154
|
+
try {
|
|
155
|
+
return ok(gcpReadonlyAccessPlan(input));
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return fail(error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
export const __test = {
|
|
163
|
+
READ_ONLY_SERVICES,
|
|
164
|
+
normalizeInput,
|
|
165
|
+
serviceCommands,
|
|
166
|
+
gcpReadonlyAccessPlan
|
|
167
|
+
};
|