@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.
Files changed (30) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +3 -0
  3. package/agents/workday-infra-access-readonly.yaml +183 -0
  4. package/package.json +50 -0
  5. package/skills/workday-infra-access/SKILL.md +102 -0
  6. package/skills/workday-infra-access/references/access-boundary-runbook.md +43 -0
  7. package/skills/workday-infra-access/references/access-runbook.md +15 -0
  8. package/skills/workday-infra-access/references/access-scylla-cloud-runbook.md +39 -0
  9. package/skills/workday-infra-access/references/access-spc-argo-runbook.md +99 -0
  10. package/src/workday-access-profiles.mjs +194 -0
  11. package/tools/aws_cust_federated_readonly_command_pack.mjs +234 -0
  12. package/tools/gcp_readonly_command_pack.mjs +167 -0
  13. package/tools/public_cloud_access_status.mjs +48 -0
  14. package/tools/workday_boundary_login.mjs +460 -0
  15. package/tools/workday_private_cloud_sudo_readonly_command.mjs +386 -0
  16. package/tools/workday_public_cloud_environment_map.mjs +189 -0
  17. package/tools/workday_public_cloud_kubernetes_read.mjs +297 -0
  18. package/tools/workday_public_cloud_spc_login.mjs +505 -0
  19. package/tools/workday_spc_argo_readonly_command_pack.mjs +215 -0
  20. package/vendor/extension-read-only-toolkit/LICENSE +202 -0
  21. package/vendor/extension-read-only-toolkit/README.md +29 -0
  22. package/vendor/extension-read-only-toolkit/package.json +46 -0
  23. package/vendor/extension-read-only-toolkit/src/devops-data-paths.mjs +222 -0
  24. package/vendor/extension-read-only-toolkit/src/devops-diagnostics.mjs +440 -0
  25. package/vendor/extension-read-only-toolkit/src/enterprise-api-read.mjs +180 -0
  26. package/vendor/extension-read-only-toolkit/src/read-only-command-pack.mjs +1 -0
  27. package/vendor/extension-read-only-toolkit/src/read-only-plan.mjs +45 -0
  28. package/vendor/extension-read-only-toolkit/src/read-only-shell-policy.d.mts +26 -0
  29. package/vendor/extension-read-only-toolkit/src/read-only-shell-policy.mjs +170 -0
  30. 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
+ };