@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,180 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { assertNonHtmlApiResponse } from "./devops-diagnostics.mjs";
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
export function normalizeBaseUrl(value) {
|
|
8
|
+
const raw = String(value || "").trim();
|
|
9
|
+
if (!raw) return "";
|
|
10
|
+
try {
|
|
11
|
+
const url = new URL(raw);
|
|
12
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
13
|
+
url.search = "";
|
|
14
|
+
url.hash = "";
|
|
15
|
+
return url.toString().replace(/\/+$/, "");
|
|
16
|
+
} catch {
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function appendQueryValue(searchParams, key, value) {
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
for (const item of value) appendQueryValue(searchParams, key, item);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (value === undefined || value === null) return;
|
|
27
|
+
searchParams.append(key, String(value));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildRequestUrl(baseUrl, requestPath, query) {
|
|
31
|
+
const url = new URL(`${baseUrl}${requestPath}`);
|
|
32
|
+
for (const [key, value] of Object.entries(query || {})) {
|
|
33
|
+
if (key) appendQueryValue(url.searchParams, key, value);
|
|
34
|
+
}
|
|
35
|
+
return url;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function extractTokenFromKeychainSecret(rawSecret = "") {
|
|
39
|
+
const text = typeof rawSecret === "string" ? rawSecret.trim() : "";
|
|
40
|
+
if (!text) return "";
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(text);
|
|
43
|
+
return typeof parsed?.token === "string" ? parsed.token.trim() : "";
|
|
44
|
+
} catch {
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function readTokenFromKeychain({ execFileImpl = execFileAsync, keychainService, keychainAccount }) {
|
|
50
|
+
try {
|
|
51
|
+
const result = await execFileImpl("/usr/bin/security", [
|
|
52
|
+
"find-generic-password",
|
|
53
|
+
"-s",
|
|
54
|
+
keychainService,
|
|
55
|
+
"-a",
|
|
56
|
+
keychainAccount,
|
|
57
|
+
"-w"
|
|
58
|
+
], { maxBuffer: 1024 * 1024 });
|
|
59
|
+
return extractTokenFromKeychainSecret(String(result?.stdout || ""));
|
|
60
|
+
} catch {
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseResponseBody(text) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(text);
|
|
68
|
+
} catch {
|
|
69
|
+
return String(text || "");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class KeychainBearerAuthProvider {
|
|
74
|
+
constructor(options = {}) {
|
|
75
|
+
this.mode = "keychain";
|
|
76
|
+
this.execFileImpl = options.execFileImpl || execFileAsync;
|
|
77
|
+
this.keychainService = options.keychainService;
|
|
78
|
+
this.keychainAccount = options.keychainAccount;
|
|
79
|
+
this.authorizationHeader = options.authorizationHeader || ((token) => `Bearer ${token}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async buildHeaders() {
|
|
83
|
+
const token = await readTokenFromKeychain({
|
|
84
|
+
execFileImpl: this.execFileImpl,
|
|
85
|
+
keychainService: this.keychainService,
|
|
86
|
+
keychainAccount: this.keychainAccount
|
|
87
|
+
});
|
|
88
|
+
if (!token) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Keychain token missing for ${this.keychainService}/${this.keychainAccount}. Save the personal access token in Keychain.`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return { authorization: this.authorizationHeader(token) };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Cookie SSO auth — inject `resolveCookieHeader(input)` from @nuvlore/extension-browser-sso. */
|
|
98
|
+
export class CookieSsoAuthProvider {
|
|
99
|
+
constructor(options = {}) {
|
|
100
|
+
this.mode = "cookie-sso";
|
|
101
|
+
this.resolveCookieHeader = options.resolveCookieHeader;
|
|
102
|
+
if (typeof this.resolveCookieHeader !== "function") {
|
|
103
|
+
throw new Error("CookieSsoAuthProvider requires resolveCookieHeader(input) async function.");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async buildHeaders(input = {}) {
|
|
108
|
+
const cookie = await this.resolveCookieHeader(input);
|
|
109
|
+
if (!cookie) {
|
|
110
|
+
throw new Error("Browser SSO cookie header is missing. Open the service in Chrome via Okta and retry.");
|
|
111
|
+
}
|
|
112
|
+
return { cookie };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class EnterpriseApiReadService {
|
|
117
|
+
constructor(options = {}) {
|
|
118
|
+
this.fetchImpl = options.fetchImpl || globalThis.fetch;
|
|
119
|
+
this.execFileImpl = options.execFileImpl || execFileAsync;
|
|
120
|
+
this.serviceName = options.serviceName;
|
|
121
|
+
this.defaultBaseUrl = options.defaultBaseUrl;
|
|
122
|
+
this.defaultUserAgent = options.defaultUserAgent;
|
|
123
|
+
this.keychainService = options.keychainService;
|
|
124
|
+
this.keychainAccount = options.keychainAccount;
|
|
125
|
+
this.normalizePath = options.normalizePath;
|
|
126
|
+
this.accept = options.accept || "application/json";
|
|
127
|
+
this.extraHeaders = options.extraHeaders || {};
|
|
128
|
+
this.authorizationHeader = options.authorizationHeader || ((token) => `Bearer ${token}`);
|
|
129
|
+
this.formatErrorPayload = options.formatErrorPayload;
|
|
130
|
+
this.includeMethodInPayload = options.includeMethodInPayload === true;
|
|
131
|
+
this.transformPayload = options.transformPayload || ((payload) => payload);
|
|
132
|
+
this.authProvider =
|
|
133
|
+
options.authProvider ??
|
|
134
|
+
new KeychainBearerAuthProvider({
|
|
135
|
+
execFileImpl: this.execFileImpl,
|
|
136
|
+
keychainService: this.keychainService,
|
|
137
|
+
keychainAccount: this.keychainAccount,
|
|
138
|
+
authorizationHeader: this.authorizationHeader
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async invoke(input = {}) {
|
|
143
|
+
if (typeof this.fetchImpl !== "function") {
|
|
144
|
+
throw new Error(`Fetch API is unavailable for ${this.serviceName} requests.`);
|
|
145
|
+
}
|
|
146
|
+
const baseUrl = normalizeBaseUrl(input.baseUrl || this.defaultBaseUrl);
|
|
147
|
+
if (!baseUrl) {
|
|
148
|
+
throw new Error(`${this.serviceName} base URL is missing or invalid. Expected something like ${this.defaultBaseUrl}.`);
|
|
149
|
+
}
|
|
150
|
+
const requestPath = this.normalizePath(input.restPath || input.path);
|
|
151
|
+
const url = buildRequestUrl(baseUrl, requestPath, input.query);
|
|
152
|
+
const authHeaders = await this.authProvider.buildHeaders(input);
|
|
153
|
+
const response = await this.fetchImpl(url, {
|
|
154
|
+
method: "GET",
|
|
155
|
+
headers: {
|
|
156
|
+
accept: this.accept,
|
|
157
|
+
"user-agent": this.defaultUserAgent,
|
|
158
|
+
...this.extraHeaders,
|
|
159
|
+
...authHeaders,
|
|
160
|
+
...(input.headers ?? {})
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
const text = await response.text();
|
|
164
|
+
assertNonHtmlApiResponse(this.serviceName, text);
|
|
165
|
+
const payload = parseResponseBody(text);
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
throw new Error(`${this.serviceName} request failed with status ${response.status}: ${this.formatErrorPayload(payload)}.`);
|
|
168
|
+
}
|
|
169
|
+
const output = {
|
|
170
|
+
baseUrl,
|
|
171
|
+
restPath: requestPath,
|
|
172
|
+
path: requestPath,
|
|
173
|
+
url: url.toString(),
|
|
174
|
+
status: response.status,
|
|
175
|
+
data: this.transformPayload(payload)
|
|
176
|
+
};
|
|
177
|
+
if (this.includeMethodInPayload) output.method = "GET";
|
|
178
|
+
return JSON.stringify(output, null, 2);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./read-only-plan.mjs";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function shellQuote(value) {
|
|
2
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function remoteCommandInput({ host, command, timeoutMs = 60_000 }) {
|
|
6
|
+
return {
|
|
7
|
+
targetType: "ssh",
|
|
8
|
+
host,
|
|
9
|
+
command,
|
|
10
|
+
timeoutMs
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ReadOnlyStep {
|
|
15
|
+
constructor({ id, purpose, command, expectedEvidence }) {
|
|
16
|
+
this.id = id;
|
|
17
|
+
this.purpose = purpose;
|
|
18
|
+
this.command = command;
|
|
19
|
+
this.expectedEvidence = expectedEvidence;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
toMarkdown() {
|
|
23
|
+
return [
|
|
24
|
+
`### ${this.id}`,
|
|
25
|
+
`Purpose: ${this.purpose}`,
|
|
26
|
+
"",
|
|
27
|
+
"```bash",
|
|
28
|
+
this.command,
|
|
29
|
+
"```",
|
|
30
|
+
"",
|
|
31
|
+
`Evidence to capture: ${this.expectedEvidence}`
|
|
32
|
+
].join("\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
toJson() {
|
|
36
|
+
return {
|
|
37
|
+
id: this.id,
|
|
38
|
+
purpose: this.purpose,
|
|
39
|
+
command: this.command,
|
|
40
|
+
expectedEvidence: this.expectedEvidence
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class ReadOnlyCommand extends ReadOnlyStep {}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type ReadOnlyValidation = {
|
|
2
|
+
allowed: boolean;
|
|
3
|
+
reason: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type ReadOnlyPolicyConfig = {
|
|
7
|
+
extraReadOnlyCommandPatterns?: RegExp[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const READ_ONLY_ALLOWED_TOOLS: readonly string[];
|
|
11
|
+
export const READ_ONLY_DISALLOWED_TOOLS: readonly string[];
|
|
12
|
+
export const MUTATING_SHELL_TOKENS: RegExp[];
|
|
13
|
+
export const READ_ONLY_COMMAND_PATTERNS: RegExp[];
|
|
14
|
+
|
|
15
|
+
export class ReadOnlyShellPolicy {
|
|
16
|
+
constructor(config?: ReadOnlyPolicyConfig);
|
|
17
|
+
validate(command: string): ReadOnlyValidation;
|
|
18
|
+
readOnlyPatterns(): RegExp[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class ReadOnlyPolicyDescription {
|
|
22
|
+
text(): string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isReadOnlyShellCommand(command: string, config?: ReadOnlyPolicyConfig): ReadOnlyValidation;
|
|
26
|
+
export function describeReadOnlyPolicy(): string;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
export const READ_ONLY_ALLOWED_TOOLS = [
|
|
2
|
+
"Read",
|
|
3
|
+
"Glob",
|
|
4
|
+
"Grep"
|
|
5
|
+
];
|
|
6
|
+
|
|
7
|
+
export const READ_ONLY_DISALLOWED_TOOLS = [
|
|
8
|
+
"Agent",
|
|
9
|
+
"Task",
|
|
10
|
+
"TaskCreate",
|
|
11
|
+
"Write",
|
|
12
|
+
"Edit",
|
|
13
|
+
"NotebookEdit",
|
|
14
|
+
"TodoWrite",
|
|
15
|
+
"ExitPlanMode",
|
|
16
|
+
"EnterWorktree",
|
|
17
|
+
"WebFetch",
|
|
18
|
+
"WebSearch",
|
|
19
|
+
"AskUserQuestion"
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export const MUTATING_SHELL_TOKENS = [
|
|
23
|
+
/\bapply\b/i,
|
|
24
|
+
/\bcreate\b/i,
|
|
25
|
+
/\bdelete\b/i,
|
|
26
|
+
/\bdestroy\b/i,
|
|
27
|
+
/\bedit\b/i,
|
|
28
|
+
/\binstall\b/i,
|
|
29
|
+
/\bpatch\b/i,
|
|
30
|
+
/\bpush\b/i,
|
|
31
|
+
/\breplace\b/i,
|
|
32
|
+
/\brestart\b/i,
|
|
33
|
+
/\brollout\s+(restart|undo)\b/i,
|
|
34
|
+
/\brm\b/i,
|
|
35
|
+
/\bscale\b/i,
|
|
36
|
+
/\bset\b/i,
|
|
37
|
+
/\bstart\b/i,
|
|
38
|
+
/\bstop\b/i,
|
|
39
|
+
/\btaint\b/i,
|
|
40
|
+
/\buntaint\b/i,
|
|
41
|
+
/\bupgrade\b/i,
|
|
42
|
+
/\bwrite\b/i,
|
|
43
|
+
/\bchmod\b/i,
|
|
44
|
+
/\bchown\b/i,
|
|
45
|
+
/\bmv\b/i,
|
|
46
|
+
/\bcp\b/i,
|
|
47
|
+
/\bmkdir\b/i,
|
|
48
|
+
/\btouch\b/i,
|
|
49
|
+
/\btee\b/i,
|
|
50
|
+
/\bcurl\b.*\|\s*(sh|bash)\b/i,
|
|
51
|
+
/\bwget\b.*\|\s*(sh|bash)\b/i,
|
|
52
|
+
/(^|[^<])>(?!\s*&\d)/,
|
|
53
|
+
/>>/,
|
|
54
|
+
/\|\s*(xargs\s+)?(sh|bash|kubectl\s+apply|terraform\s+apply|helm\s+upgrade|rm|tee)\b/i,
|
|
55
|
+
/(^|[^&|]);\s*/,
|
|
56
|
+
/&&|\|\|/,
|
|
57
|
+
/`|\$\(/,
|
|
58
|
+
/\b--write\b/i,
|
|
59
|
+
/\b--force\b/i,
|
|
60
|
+
/\b--yes\b|\b-y\b/i
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
export const READ_ONLY_COMMAND_PATTERNS = [
|
|
64
|
+
/^\s*pwd\s*$/,
|
|
65
|
+
/^\s*ls(\s|$)/,
|
|
66
|
+
/^\s*find\s+/,
|
|
67
|
+
/^\s*(rg|grep)\s+/,
|
|
68
|
+
/^\s*(cat|head|tail|sed|awk|wc)\s+/,
|
|
69
|
+
/^\s*curl\s+-k\s+-sS\s+-o\s+\/dev\/null\s+-w\s+'[A-Za-z0-9_=%{}:./ -]+\\n'\s+https:\/\/[A-Za-z0-9_.:-]+\/[A-Za-z0-9_./:@%+=,~-]+(\s|$)/,
|
|
70
|
+
/^\s*hostname(\s|$)/,
|
|
71
|
+
/^\s*command\s+-v\s+/,
|
|
72
|
+
/^\s*klist(\s|$)/,
|
|
73
|
+
/^\s*git\s+(status|diff|log|show|branch|remote|rev-parse|describe|ls-files)(\s|$)/,
|
|
74
|
+
/^\s*kubectl\s+(get|describe|logs|top|api-resources|api-versions|explain|version|cluster-info|config\s+(current-context|get-contexts|view))(\s|$)/,
|
|
75
|
+
/^\s*kubectl\s+exec\s+-n\s+airflow\s+(?:pod\/)?[A-Za-z0-9._:@%/-]+(?:\s+-c\s+[A-Za-z0-9._:@%/-]+)?\s+--\s+airflow\s+dags\s+list-runs\s+-d\s+[A-Za-z0-9_.:@/-]+(?:\s+--no-backfill)?(?:\s+--output\s+(?:json|table|yaml|plain))?(?:\s+\|\s+head\s+-\d+)?\s*$/,
|
|
76
|
+
/^\s*kubectl\s+exec\s+-n\s+airflow\s+(?:pod\/)?[A-Za-z0-9._:@%/-]+(?:\s+-c\s+[A-Za-z0-9._:@%/-]+)?\s+--\s+airflow\s+tasks\s+states-for-dag-run\s+[A-Za-z0-9_.:@/-]+\s+[A-Za-z0-9_.:@+=,~%/-]+(?:\s+--output\s+(?:json|table|yaml|plain))?(?:\s+\|\s+head\s+-\d+)?\s*$/,
|
|
77
|
+
/^\s*kubectl\s+exec\s+-n\s+airflow\s+(?:pod\/)?[A-Za-z0-9._:@%/-]+(?:\s+-c\s+[A-Za-z0-9._:@%/-]+)?\s+--\s+airflow\s+tasks\s+logs\s+[A-Za-z0-9_.:@/-]+\s+[A-Za-z0-9_.:@/-]+\s+[A-Za-z0-9_.:@+=,~%/-]+\s+--try-number\s+\d+\s+\|\s+head\s+-\d+\s*$/,
|
|
78
|
+
/^\s*kubectl\s+exec\s+-n\s+airflow\s+(?:pod\/)?[A-Za-z0-9._:@%/-]+(?:\s+-c\s+[A-Za-z0-9._:@%/-]+)?\s+--\s+airflow\s+tasks\s+render\s+[A-Za-z0-9_.:@/-]+\s+[A-Za-z0-9_.:@/-]+\s+[A-Za-z0-9_.:@+=,~%/-]+\s+\|\s+head\s+-\d+\s*$/,
|
|
79
|
+
/^\s*kubectl\s+exec\s+-n\s+airflow\s+(?:pod\/)?[A-Za-z0-9._:@%/-]+(?:\s+-c\s+[A-Za-z0-9._:@%/-]+)?\s+--\s+cat\s+\/(?:(?:data|hadoop\/disk2)\/workday-bds-airflow|data\/airflow)\/logs\/[A-Za-z0-9_.:@+=,~%/-]+(?:\s+\|\s+head\s+-\d+)?\s*$/,
|
|
80
|
+
/^\s*kubectl\s+exec\s+-n\s+airflow\s+(?:pod\/)?[A-Za-z0-9._:@%/-]+(?:\s+-c\s+[A-Za-z0-9._:@%/-]+)?\s+--\s+grep\s+-E\s+'[A-Za-z0-9_.:@+=,~%|/-]+'\s+\/(?:(?:data|hadoop\/disk2)\/workday-bds-airflow|data\/airflow)\/logs\/[A-Za-z0-9_.:@+=,~%/-]+(?:\s+\|\s+tail\s+-n\s+\d+)?\s*$/,
|
|
81
|
+
/^\s*kubectl\s+exec\s+-n\s+airflow\s+(?:pod\/)?[A-Za-z0-9._:@%/-]+(?:\s+-c\s+[A-Za-z0-9._:@%/-]+)?\s+--\s+grep\s+-E\s+'[A-Za-z0-9_.:@+=,~%|/-]+'\s+\/usr\/local\/airflow\/(?:airflow_s3_config\.json|dags\/[A-Za-z0-9_.:@+=,~%/-]+)(?:\s+\|\s+(?:head|tail)\s+-\d+)?\s*$/,
|
|
82
|
+
/^\s*kubectl\s+exec\s+-n\s+airflow\s+(?:pod\/)?[A-Za-z0-9._:@%/-]+(?:\s+-c\s+[A-Za-z0-9._:@%/-]+)?\s+--\s+printenv\s+\|\s+grep\s+-E\s+'\^\(AWS_\|GOOGLE_\|GOOGLE_APPLICATION_CREDENTIALS\|HADOOP_\|HADOOP_CONF_DIR\|AIRFLOW_S3_CONFIG_PATH\)'\s+\|\s+sed\s+-E\s+'s\/=\.\*\/=\[present\]\/'\s+\|\s+head\s+-\d+\s*$/,
|
|
83
|
+
/^\s*kubectl\s+exec\s+-n\s+airflow\s+(?:pod\/)?[A-Za-z0-9._:@%/-]+(?:\s+-c\s+[A-Za-z0-9._:@%/-]+)?\s+--\s+printenv\s+HADOOP_CREDENTIAL_PROVIDER_PATH\s*$/,
|
|
84
|
+
/^\s*kubectl\s+exec\s+-n\s+airflow\s+(?:pod\/)?[A-Za-z0-9._:@%/-]+(?:\s+-c\s+[A-Za-z0-9._:@%/-]+)?\s+--\s+hadoop\s+credential\s+list\s+\|\s+head\s+-\d+\s*$/,
|
|
85
|
+
/^\s*kubectl\s+exec\s+-n\s+airflow\s+(?:pod\/)?[A-Za-z0-9._:@%/-]+(?:\s+-c\s+[A-Za-z0-9._:@%/-]+)?\s+--\s+grep\s+-h\s+-E\s+'fs\.s3a\.aws\.credentials\.provider\|fs\.s3a\.endpoint\|fs\.s3a\.impl\|google\.cloud\.auth'\s+\/etc\/hadoop\/conf\/(?:core-site|hdfs-site|mapred-site|yarn-site)\.xml(?:\s+\/etc\/hadoop\/conf\/(?:core-site|hdfs-site|mapred-site|yarn-site)\.xml)*\s+\|\s+head\s+-\d+\s*$/,
|
|
86
|
+
/^\s*kubectl\s+exec\s+-n\s+airflow\s+(?:pod\/)?[A-Za-z0-9._:@%/-]+(?:\s+-c\s+[A-Za-z0-9._:@%/-]+)?\s+--\s+cat\s+\/usr\/local\/airflow\/(?:airflow_s3_config\.json|dags\/[A-Za-z0-9_.:@+=,~%/-]+)(?:\s+\|\s+head\s+-\d+)?\s*$/,
|
|
87
|
+
/^\s*kubectl\s+exec\s+-n\s+airflow\s+(?:pod\/)?[A-Za-z0-9._:@%/-]+(?:\s+-c\s+[A-Za-z0-9._:@%/-]+)?\s+--\s+hadoop\s+fs\s+-(?:ls|du|count|stat|test)(?:\s+-d)?\s+s3a:\/\/[A-Za-z0-9][A-Za-z0-9_.-]*[A-Za-z0-9](?:\/[A-Za-z0-9_.:@+=,~%/-]*)?(?:\s+\|\s+head\s+-\d+)?\s*$/,
|
|
88
|
+
/^\s*kcsb\s+(get|describe|logs|top|api-resources|api-versions|explain|version|cluster-info|config\s+(current-context|get-contexts|view))(\s|$)/,
|
|
89
|
+
/^\s*kinit\s+-kt\s+\/[A-Za-z0-9._/-]+\s+[A-Za-z0-9_.@/-]+(\s|$)/,
|
|
90
|
+
/^\s*yarn\s+application\s+-status\s+application_[0-9]+_[0-9]+(\s|$)/,
|
|
91
|
+
/^\s*yarn\s+logs\s+-applicationId\s+application_[0-9]+_[0-9]+(\s|$)/,
|
|
92
|
+
/^\s*yarn\s+node\s+-status\s+(?!-)[A-Za-z0-9._:@%-]+:45454(\s|$)/,
|
|
93
|
+
/^\s*hdfs\s+crypto\s+-listZones(\s|$)/,
|
|
94
|
+
/^\s*hdfs\s+dfs\s+-(ls|du|find|cat|tail|test|count|stat)\s+/,
|
|
95
|
+
/^\s*hdfs\s+dfsadmin\s+-report(\s|$)/,
|
|
96
|
+
/^\s*hadoop\s+fs\s+-(ls|cat|head|du|count|stat|test|text)(\s|$)/,
|
|
97
|
+
/^\s*helm\s+(list|status|history|get|version|repo\s+list)(\s|$)/,
|
|
98
|
+
/^\s*aws\s+(?:--profile\s+\S+\s+)?(?:--region\s+\S+\s+)?sts\s+get-caller-identity(\s|$)/,
|
|
99
|
+
/^\s*aws\s+(?:--profile\s+\S+\s+)?(?:--region\s+\S+\s+)?[a-z0-9-]+\s+(describe|list|get|head)-[a-z0-9-]+(\s|$)/,
|
|
100
|
+
/^\s*aws\s+(?:--profile\s+\S+\s+)?(?:--region\s+\S+\s+)?s3\s+ls(\s|$)/,
|
|
101
|
+
/^\s*gcloud\s+(?:--project=\S+\s+)?(?:--account=\S+\s+)?auth\s+list(\s|$)/,
|
|
102
|
+
/^\s*gcloud\s+(?:--project=\S+\s+)?(?:--account=\S+\s+)?config\s+list(\s|$)/,
|
|
103
|
+
/^\s*gcloud\s+(?:--project=\S+\s+)?(?:--account=\S+\s+)?projects\s+(describe|get-iam-policy)(\s|$)/,
|
|
104
|
+
/^\s*gcloud\s+(?:--project=\S+\s+)?(?:--account=\S+\s+)?[a-z0-9-]+(?:\s+[a-z0-9-]+)*\s+(list|describe|read)(\s|$)/,
|
|
105
|
+
/^\s*gcloud\s+storage\s+(buckets|objects)\s+list(\s|$)/,
|
|
106
|
+
/^\s*terraform\s+(version|show|state\s+list|state\s+show|providers|workspace\s+show|output)(\s|$)/,
|
|
107
|
+
/^\s*docker\s+(ps|images|logs|inspect|stats|version|info)(\s|$)/,
|
|
108
|
+
/^\s*(df|du|free|uptime|uname|whoami|id|date|env|printenv)(\s|$)/,
|
|
109
|
+
/^\s*journalctl\s+-k\s+--since\s+/,
|
|
110
|
+
/^\s*(systemctl|journalctl)\s+(status|-u|--unit)(\s|$)/
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
export class ReadOnlyShellPolicy {
|
|
114
|
+
constructor(config = {}) {
|
|
115
|
+
this.config = config;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
validate(command) {
|
|
119
|
+
const normalized = String(command ?? "").trim();
|
|
120
|
+
|
|
121
|
+
if (!normalized) {
|
|
122
|
+
return { allowed: false, reason: "empty shell command" };
|
|
123
|
+
}
|
|
124
|
+
if (normalized.includes("\n") || normalized.includes("\r")) {
|
|
125
|
+
return { allowed: false, reason: "multi-line shell commands are not allowed" };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const mutatingToken = MUTATING_SHELL_TOKENS.find((token) => token.test(normalized));
|
|
129
|
+
if (mutatingToken) {
|
|
130
|
+
return {
|
|
131
|
+
allowed: false,
|
|
132
|
+
reason: `shell command contains mutating or high-risk syntax: ${mutatingToken.source}`
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (this.readOnlyPatterns().some((pattern) => pattern.test(normalized))) {
|
|
137
|
+
return { allowed: true, reason: "matches read-only command allowlist" };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { allowed: false, reason: "command is not in the read-only allowlist" };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
readOnlyPatterns() {
|
|
144
|
+
return [
|
|
145
|
+
...READ_ONLY_COMMAND_PATTERNS,
|
|
146
|
+
...(this.config.extraReadOnlyCommandPatterns ?? [])
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export class ReadOnlyPolicyDescription {
|
|
152
|
+
text() {
|
|
153
|
+
return [
|
|
154
|
+
"Read-only DevOps policy:",
|
|
155
|
+
`- Auto-approved tools: ${READ_ONLY_ALLOWED_TOOLS.join(", ")}`,
|
|
156
|
+
`- Always denied tools: ${READ_ONLY_DISALLOWED_TOOLS.join(", ")}`,
|
|
157
|
+
"- Bash: denied by default; only explicit read-only command patterns are allowed.",
|
|
158
|
+
"- Filesystem sandbox: enabled with denyWrite on all paths.",
|
|
159
|
+
"- Permission mode: dontAsk, so unapproved tools fail closed."
|
|
160
|
+
].join("\n");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function isReadOnlyShellCommand(command, config = {}) {
|
|
165
|
+
return new ReadOnlyShellPolicy(config).validate(command);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function describeReadOnlyPolicy() {
|
|
169
|
+
return new ReadOnlyPolicyDescription().text();
|
|
170
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export const meta = {
|
|
2
|
+
name: "public-cloud-readonly-investigation",
|
|
3
|
+
description: "Investigate AWS CUST or GCP cloud state through approved federation/local identity and read-only evidence commands only.",
|
|
4
|
+
whenToUse: "Use when an incident, Bamboo result, Slack thread, or operator request requires AWS CUST or GCP resource inspection without cloud mutation.",
|
|
5
|
+
phases: ["Plan", "Identity", "Evidence", "Reply"],
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
phase("Plan");
|
|
9
|
+
const plan = await agent(
|
|
10
|
+
[
|
|
11
|
+
"Extract platform, account/account alias or GCP project, region/zone, service, resource, and incident evidence.",
|
|
12
|
+
"For AWS CUST federated access, call only mcp__devops_tool__aws_cust_federated_readonly_command_pack.",
|
|
13
|
+
"For GCP, call only mcp__devops_tool__gcp_readonly_command_pack.",
|
|
14
|
+
"If the platform is ambiguous, report that blocker and do not guess.",
|
|
15
|
+
"Do not authenticate, open browser federation, read credential files, use Bash, or call cloud APIs in this phase.",
|
|
16
|
+
].join("\n"),
|
|
17
|
+
{
|
|
18
|
+
label: "public-cloud-plan",
|
|
19
|
+
phase: "Plan",
|
|
20
|
+
tools: "mcp__devops_tool__aws_cust_federated_readonly_command_pack,mcp__devops_tool__gcp_readonly_command_pack",
|
|
21
|
+
disallowedTools: "Agent,Task,TaskCreate,Bash,Read,Glob,Grep,WebFetch,WebSearch,Write,Edit,NotebookEdit,AskUserQuestion",
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
phase("Identity");
|
|
26
|
+
const identity = await agent(
|
|
27
|
+
[
|
|
28
|
+
"Verify identity only after approved local federation/login outside this workflow.",
|
|
29
|
+
"For AWS, run the plan's aws sts get-caller-identity via permitted local read-only execution.",
|
|
30
|
+
"For GCP, run the plan's gcloud auth list and gcloud config list via permitted local read-only execution.",
|
|
31
|
+
"If local execution is unavailable or denied, return the exact identity command that must be run manually and mark identity as unverified.",
|
|
32
|
+
"Never run aws configure, gcloud auth login, gcloud config set, or read ~/.aws, ADC, or credential files.",
|
|
33
|
+
"",
|
|
34
|
+
"Plan:",
|
|
35
|
+
plan,
|
|
36
|
+
].join("\n"),
|
|
37
|
+
{
|
|
38
|
+
label: "public-cloud-identity",
|
|
39
|
+
phase: "Identity",
|
|
40
|
+
tools: "",
|
|
41
|
+
disallowedTools: "Agent,Task,TaskCreate,WebFetch,WebSearch,Write,Edit,NotebookEdit,AskUserQuestion",
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
phase("Evidence");
|
|
46
|
+
const evidence = await agent(
|
|
47
|
+
[
|
|
48
|
+
"Collect read-only cloud evidence using commands emitted by the plan.",
|
|
49
|
+
"Allowed action families are describe, list, get metadata, head, read logs, and get-iam-policy.",
|
|
50
|
+
"For AWS S3, never run cp, mv, rm, sync, put-object, delete-object, or get-object for secrets/private material.",
|
|
51
|
+
"For GCP Storage, never run cp, mv, rm, update, compose, or rewrite.",
|
|
52
|
+
"For IAM/KMS/Secrets Manager, read metadata only; never request secret values, private keys, or key material.",
|
|
53
|
+
"If a command is denied by the read-only runtime or cloud IAM, preserve the denied action as an evidence gap.",
|
|
54
|
+
"",
|
|
55
|
+
"Plan:",
|
|
56
|
+
plan,
|
|
57
|
+
"",
|
|
58
|
+
"Identity:",
|
|
59
|
+
identity,
|
|
60
|
+
].join("\n"),
|
|
61
|
+
{
|
|
62
|
+
label: "public-cloud-evidence",
|
|
63
|
+
phase: "Evidence",
|
|
64
|
+
tools: "",
|
|
65
|
+
disallowedTools: "Agent,Task,TaskCreate,WebFetch,WebSearch,Write,Edit,NotebookEdit,AskUserQuestion",
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
phase("Reply");
|
|
70
|
+
const reply = await agent(
|
|
71
|
+
[
|
|
72
|
+
"Draft a concise answer from collected cloud evidence only.",
|
|
73
|
+
"State platform, account/project, identity state, commands run/blocked, observations, and evidence gaps.",
|
|
74
|
+
"Distinguish IAM/access denial from resource absence.",
|
|
75
|
+
"State remediation only as a human-reviewed manual recommendation; do not execute writes.",
|
|
76
|
+
"",
|
|
77
|
+
"Plan:",
|
|
78
|
+
plan,
|
|
79
|
+
"",
|
|
80
|
+
"Identity:",
|
|
81
|
+
identity,
|
|
82
|
+
"",
|
|
83
|
+
"Evidence:",
|
|
84
|
+
evidence,
|
|
85
|
+
].join("\n"),
|
|
86
|
+
{
|
|
87
|
+
label: "public-cloud-reply",
|
|
88
|
+
phase: "Reply",
|
|
89
|
+
tools: "",
|
|
90
|
+
disallowedTools: "Agent,Task,TaskCreate,Bash,Read,Glob,Grep,WebFetch,WebSearch,Write,Edit,NotebookEdit,AskUserQuestion",
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
plan,
|
|
96
|
+
identity,
|
|
97
|
+
evidence,
|
|
98
|
+
reply,
|
|
99
|
+
};
|