@deployfoundation/foundation-deploy 0.2.52 → 0.2.53

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/dist/bin/app.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  FoundationPages,
9
9
  FoundationPipeline,
10
10
  NewsletterStack
11
- } from "../chunk-59dpqyap.js";
11
+ } from "../chunk-spq5kr1t.js";
12
12
  import"../chunk-pxhftba1.js";
13
13
  import"../chunk-dz91avrb.js";
14
14
  import {
@@ -16,7 +16,7 @@ import {
16
16
  instanceNames,
17
17
  loadInstanceFile,
18
18
  provisionsIntegration
19
- } from "../chunk-rxetj2p9.js";
19
+ } from "../chunk-fjdgxw8h.js";
20
20
  import"../chunk-96fjaysn.js";
21
21
  import"../chunk-5wgwscd0.js";
22
22
 
@@ -32,13 +32,14 @@ import {
32
32
  secretExists,
33
33
  stackExists,
34
34
  stackOutput
35
- } from "../chunk-9zx95xs6.js";
35
+ } from "../chunk-nvsj7bbw.js";
36
36
  import {
37
37
  CUSTOMIZATION_ARTIFACT_NAME,
38
38
  instanceNames,
39
+ mongoCollectionPolicyEnv,
39
40
  slackCommandPrefix,
40
41
  versionedConfigKey
41
- } from "../chunk-rxetj2p9.js";
42
+ } from "../chunk-fjdgxw8h.js";
42
43
  import"../chunk-m76jt5rc.js";
43
44
  import"../chunk-3614v9rr.js";
44
45
  import {
@@ -1216,6 +1217,7 @@ function composeRuntimeSecret(inputs) {
1216
1217
  ...inputs.etradeProxyFunctionArn !== undefined && inputs.etradeProxyFunctionArn !== "" ? { FOUNDATION_ETRADE_PROXY_ARN: inputs.etradeProxyFunctionArn } : {},
1217
1218
  ...inputs.routineIngressFunctionArn !== undefined && inputs.routineIngressFunctionArn !== "" ? { FOUNDATION_ROUTINE_INGRESS_FUNCTION_ARN: inputs.routineIngressFunctionArn } : {},
1218
1219
  FOUNDATION_MONGODB_SECRET_ID: names.secretMongodbReadonly,
1220
+ ...instance.mongodbReadonly.deniedCollections.length > 0 || instance.mongodbReadonly.unsafeReadDefaultDeniedCollections.length > 0 ? { FOUNDATION_MONGODB_COLLECTION_POLICY: mongoCollectionPolicyEnv(instance.mongodbReadonly) } : {},
1219
1221
  FOUNDATION_SLACK_TEAM_ID: slackApp.team_id !== undefined && slackApp.team_id !== "" ? slackApp.team_id : instance.slack.teamId
1220
1222
  };
1221
1223
  }
@@ -2538,7 +2540,7 @@ ${USAGE}`);
2538
2540
  const tenantId = flag2(args, "--tenant");
2539
2541
  if (baseUrl === undefined || tenantId === undefined || !args.includes("--key-stdin"))
2540
2542
  throw new Error("crm-check --base-url needs --tenant <id> and --key-stdin");
2541
- const { crmCheckTarget, defaultCrmCommandDeps } = await import("../chunk-sgdcmme0.js");
2543
+ const { crmCheckTarget, defaultCrmCommandDeps } = await import("../chunk-1pww63wd.js");
2542
2544
  return crmCheckTarget({
2543
2545
  baseUrl,
2544
2546
  tenantId,
@@ -2611,21 +2613,21 @@ ${USAGE}`);
2611
2613
  return 0;
2612
2614
  }
2613
2615
  case "crm-key": {
2614
- const { crmKey, defaultCrmCommandDeps } = await import("../chunk-sgdcmme0.js");
2616
+ const { crmKey, defaultCrmCommandDeps } = await import("../chunk-1pww63wd.js");
2615
2617
  await crmKey(ctx, { stdin: args.includes("--stdin") }, defaultCrmCommandDeps());
2616
2618
  return 0;
2617
2619
  }
2618
2620
  case "crm-check": {
2619
2621
  if (args.includes("--write"))
2620
2622
  throw new Error("crm-check --write creates records and spends the week's outreach allowance, so it never runs against the instance's own tenant: use --base-url with a sandbox tenant");
2621
- const { crmCheck, defaultCrmCommandDeps } = await import("../chunk-sgdcmme0.js");
2623
+ const { crmCheck, defaultCrmCommandDeps } = await import("../chunk-1pww63wd.js");
2622
2624
  return crmCheck(ctx, { allowSkips: args.includes("--allow-skips") }, defaultCrmCommandDeps());
2623
2625
  }
2624
2626
  case "crm-replay": {
2625
2627
  const from = flag2(args, "--from");
2626
2628
  if (from === undefined || from.startsWith("-"))
2627
2629
  throw new Error("crm-replay needs --from <file>: the writes to replay, as JSONL");
2628
- const { crmReplay, defaultCrmCommandDeps } = await import("../chunk-sgdcmme0.js");
2630
+ const { crmReplay, defaultCrmCommandDeps } = await import("../chunk-1pww63wd.js");
2629
2631
  const result = await crmReplay(ctx, { from }, defaultCrmCommandDeps());
2630
2632
  return result.refused.length > 0 ? 1 : 0;
2631
2633
  }
@@ -3,8 +3,8 @@ import {
3
3
  putSecretString,
4
4
  readSecretString,
5
5
  secretExists
6
- } from "./chunk-9zx95xs6.js";
7
- import"./chunk-rxetj2p9.js";
6
+ } from "./chunk-nvsj7bbw.js";
7
+ import"./chunk-fjdgxw8h.js";
8
8
  import {
9
9
  CrmIdempotencyConflictError,
10
10
  CrmRecordNotFoundError
@@ -7,6 +7,34 @@ import {
7
7
  // ../core/src/instance.ts
8
8
  import { parse as parseYaml } from "yaml";
9
9
  import { z } from "zod";
10
+
11
+ // ../core/src/mongodb-collections.ts
12
+ var DEFAULT_DENIED_MONGO_COLLECTIONS = [
13
+ "users",
14
+ "sessions",
15
+ "refreshtokens",
16
+ "passwordresets",
17
+ "servicekeys",
18
+ "machineapirequests",
19
+ "apikeys",
20
+ "oauthtokens",
21
+ "integrationtokens",
22
+ "integrations"
23
+ ];
24
+ var DENIED_MONGO_COLLECTION_PATTERN = /token|secret|session|credential|password/i;
25
+ function deniedByDefault(name) {
26
+ const lower = name.toLowerCase();
27
+ return DEFAULT_DENIED_MONGO_COLLECTIONS.includes(lower) || DENIED_MONGO_COLLECTION_PATTERN.test(lower);
28
+ }
29
+ function mongoCollectionPolicyEnv(section) {
30
+ const policy = {
31
+ denied: section.deniedCollections.map((name) => name.toLowerCase()),
32
+ unsafeAllowed: section.unsafeReadDefaultDeniedCollections.map((name) => name.toLowerCase())
33
+ };
34
+ return JSON.stringify(policy);
35
+ }
36
+
37
+ // ../core/src/instance.ts
10
38
  var PENDING = "TBD";
11
39
  var CUSTOMIZATION_ARTIFACT_NAME = "Customization";
12
40
  var RawAws = z.object({
@@ -234,6 +262,27 @@ var RawCrmProvider = z.discriminatedUnion("kind", [
234
262
  }).strict()
235
263
  ]);
236
264
  var RawCrm = z.object({ provider: RawCrmProvider.default({ kind: "dynamodb" }) }).strict().default({});
265
+ var MongoCollectionName = z.string().regex(/^[A-Za-z0-9_.$-]{1,120}$/, "a MongoDB collection name, e.g. auditlogs");
266
+ var RawMongodbReadonly = z.object({
267
+ deniedCollections: z.array(MongoCollectionName).max(200).default([]),
268
+ unsafeReadDefaultDeniedCollections: z.array(MongoCollectionName).max(50).default([])
269
+ }).strict().superRefine((section, ctx) => {
270
+ const denied = new Set(section.deniedCollections.map((name) => name.toLowerCase()));
271
+ section.unsafeReadDefaultDeniedCollections.forEach((name, index) => {
272
+ if (denied.has(name.toLowerCase()))
273
+ ctx.addIssue({
274
+ code: z.ZodIssueCode.custom,
275
+ path: ["unsafeReadDefaultDeniedCollections", index],
276
+ message: `${name} is in deniedCollections too; remove it from one list`
277
+ });
278
+ else if (!deniedByDefault(name))
279
+ ctx.addIssue({
280
+ code: z.ZodIssueCode.custom,
281
+ path: ["unsafeReadDefaultDeniedCollections", index],
282
+ message: `${name} is not denied by default, so there is nothing to opt out of`
283
+ });
284
+ });
285
+ }).default({});
237
286
  var RawInstance = z.object({
238
287
  name: z.string().min(1).max(63).regex(/^[a-z][a-z0-9-]*$/),
239
288
  displayName: z.string().min(1).max(120),
@@ -252,7 +301,8 @@ var RawInstance = z.object({
252
301
  integrations: RawIntegrations,
253
302
  newsletter: RawNewsletter,
254
303
  pages: RawPages,
255
- crm: RawCrm
304
+ crm: RawCrm,
305
+ mongodbReadonly: RawMongodbReadonly
256
306
  }).strict().superRefine((doc, ctx) => {
257
307
  if (doc.crm.provider.kind === "http" && !doc.integrations.crm)
258
308
  ctx.addIssue({
@@ -463,4 +513,4 @@ var WEB_SEARCH_TARGET = "websearch";
463
513
  var WEB_SEARCH_CONNECTOR_VERSION = "1.2.0";
464
514
  var LIVE_ENDPOINT_NAME = "live";
465
515
 
466
- export { CUSTOMIZATION_ARTIFACT_NAME, newsletterManifestFor, FOUNDATION_VERSION_RE, versionedConfigKey, instanceNames, slackCommandPrefix, slackCommandPrefixes, loadInstanceFile, configPathFor, capabilityEnabled, requiresAuthenticatedQueue, enabledIntegrations, provisionsIntegration, adminsFor, WEB_SEARCH_TARGET, WEB_SEARCH_CONNECTOR_VERSION, LIVE_ENDPOINT_NAME };
516
+ export { mongoCollectionPolicyEnv, CUSTOMIZATION_ARTIFACT_NAME, newsletterManifestFor, FOUNDATION_VERSION_RE, versionedConfigKey, instanceNames, slackCommandPrefix, slackCommandPrefixes, loadInstanceFile, configPathFor, capabilityEnabled, requiresAuthenticatedQueue, enabledIntegrations, provisionsIntegration, adminsFor, WEB_SEARCH_TARGET, WEB_SEARCH_CONNECTOR_VERSION, LIVE_ENDPOINT_NAME };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  instanceNames,
3
3
  loadInstanceFile
4
- } from "./chunk-rxetj2p9.js";
4
+ } from "./chunk-fjdgxw8h.js";
5
5
 
6
6
  // src/deploy/aws.ts
7
7
  import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
@@ -17,7 +17,7 @@ import {
17
17
  provisionsIntegration,
18
18
  requiresAuthenticatedQueue,
19
19
  slackCommandPrefixes
20
- } from "./chunk-rxetj2p9.js";
20
+ } from "./chunk-fjdgxw8h.js";
21
21
  import {
22
22
  capabilityScheduledJobs,
23
23
  crmOutreachConfigured,
@@ -35,6 +35,70 @@ import * as cdk from "aws-cdk-lib";
35
35
  import * as ecr from "aws-cdk-lib/aws-ecr";
36
36
  import * as iam from "aws-cdk-lib/aws-iam";
37
37
  import * as logs from "aws-cdk-lib/aws-logs";
38
+
39
+ // src/stacks/secret-bearing-reads.ts
40
+ var SECRET_BEARING_READ_ACTIONS = [
41
+ "wafv2:GetSampledRequests",
42
+ "waf:GetSampledRequests",
43
+ "waf-regional:GetSampledRequests",
44
+ "lambda:GetFunction",
45
+ "lambda:GetFunctionConfiguration",
46
+ "lambda:ListFunctions",
47
+ "lambda:ListVersionsByFunction",
48
+ "lambda:GetLayerVersion",
49
+ "lambda:GetLayerVersionByArn",
50
+ "ecs:DescribeTaskDefinition",
51
+ "apprunner:DescribeService",
52
+ "batch:DescribeJobDefinitions",
53
+ "batch:DescribeJobs",
54
+ "codebuild:BatchGetProjects",
55
+ "codebuild:BatchGetBuilds",
56
+ "codebuild:BatchGetBuildBatches",
57
+ "amplify:GetApp",
58
+ "amplify:ListApps",
59
+ "amplify:GetBranch",
60
+ "amplify:ListBranches",
61
+ "amplify:GetBackendEnvironment",
62
+ "elasticbeanstalk:DescribeConfigurationSettings",
63
+ "ec2:DescribeLaunchTemplateVersions",
64
+ "ec2:DescribeInstanceAttribute",
65
+ "ssm:GetParameter",
66
+ "ssm:GetParameters",
67
+ "ssm:GetParametersByPath",
68
+ "ssm:GetParameterHistory",
69
+ "cloudformation:GetTemplate",
70
+ "appconfig:GetConfiguration",
71
+ "appconfig:GetHostedConfigurationVersion",
72
+ "glue:GetJob",
73
+ "glue:GetJobs",
74
+ "glue:BatchGetJobs",
75
+ "glue:GetConnection",
76
+ "glue:GetConnections",
77
+ "glue:GetDevEndpoint",
78
+ "glue:GetDevEndpoints",
79
+ "glue:BatchGetDevEndpoints",
80
+ "sagemaker:DescribeModel",
81
+ "sagemaker:DescribeTrainingJob",
82
+ "sagemaker:DescribeProcessingJob",
83
+ "sagemaker:DescribeTransformJob",
84
+ "datapipeline:GetPipelineDefinition",
85
+ "appsync:ListApiKeys",
86
+ "appsync:GetDataSource",
87
+ "appsync:ListDataSources",
88
+ "kinesisanalytics:DescribeApplication",
89
+ "lightsail:GetBucketAccessKeys",
90
+ "lightsail:GetContainerServices",
91
+ "lightsail:GetContainerServiceDeployments",
92
+ "opsworks:DescribeApps",
93
+ "opsworks:DescribeStacks",
94
+ "cognito-identity:GetCredentialsForIdentity",
95
+ "cognito-identity:GetOpenIdToken",
96
+ "cognito-identity:GetOpenIdTokenForDeveloperIdentity",
97
+ "connect:GetFederationToken",
98
+ "codeartifact:GetAuthorizationToken"
99
+ ];
100
+
101
+ // src/stacks/agent-stack.ts
38
102
  class FoundationAgent extends cdk.Stack {
39
103
  repository;
40
104
  executionRole;
@@ -77,6 +141,7 @@ class FoundationAgent extends cdk.Stack {
77
141
  this.accessPoint = accessPoint;
78
142
  this.executionRole = this.buildExecutionRole(props);
79
143
  this.readOnlyRole = this.buildReadOnlyRole(props);
144
+ this.secretBearingReadsPolicy(props.instance, [this.executionRole, this.readOnlyRole]);
80
145
  this.buildSpansIngestionPolicy();
81
146
  const gateway = this.buildToolGateway(display);
82
147
  this.webSearchUrl = gateway.getAtt("GatewayUrl").toString();
@@ -439,6 +504,20 @@ class FoundationAgent extends cdk.Stack {
439
504
  }));
440
505
  return role;
441
506
  }
507
+ secretBearingReadsPolicy(instance, roles) {
508
+ new iam.ManagedPolicy(this, "AgentSecretBearingReadsDeny", {
509
+ description: `${instance.displayName} agent: no AWS read whose response carries a secret`,
510
+ roles,
511
+ statements: [
512
+ new iam.PolicyStatement({
513
+ sid: "DenySecretBearingReads",
514
+ effect: iam.Effect.DENY,
515
+ actions: [...SECRET_BEARING_READ_ACTIONS],
516
+ resources: ["*"]
517
+ })
518
+ ]
519
+ });
520
+ }
442
521
  buildSpansIngestionPolicy() {
443
522
  return new logs.ResourcePolicy(this, "AgentSpansIngestion", {
444
523
  resourcePolicyName: `${this.stackName}-AgentSpansFromXray`,
package/dist/src/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  FoundationPipeline,
11
11
  NewsletterStack,
12
12
  deployStatements
13
- } from "../chunk-59dpqyap.js";
13
+ } from "../chunk-spq5kr1t.js";
14
14
  import {
15
15
  BUN_VERSION,
16
16
  LAMBDA_ENTRY_POINTS,
@@ -47,7 +47,7 @@ import {
47
47
  loadInstanceFile,
48
48
  provisionsIntegration,
49
49
  requiresAuthenticatedQueue
50
- } from "../chunk-rxetj2p9.js";
50
+ } from "../chunk-fjdgxw8h.js";
51
51
  import"../chunk-96fjaysn.js";
52
52
  import"../chunk-5wgwscd0.js";
53
53
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deployfoundation/foundation-deploy",
3
- "version": "0.2.52",
3
+ "version": "0.2.53",
4
4
  "description": "Deploys one Foundation instance from a published, signed release: the CDK app and the deploy tool.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -12,6 +12,7 @@ import {
12
12
  instanceNames as namesFor,
13
13
  slackCommandPrefix,
14
14
  } from "@deployfoundation/foundation-core/instance";
15
+ import { mongoCollectionPolicyEnv } from "@deployfoundation/foundation-core/mongodb-collections";
15
16
 
16
17
  /** The pinned AgentCore endpoint a deploy promotes and the invoker calls. */
17
18
  export const LIVE_ENDPOINT = "live";
@@ -258,6 +259,14 @@ export function composeRuntimeSecret(inputs: RuntimeSecretInputs): Record<string
258
259
  // LAZILY, so a secret still holding the CDK placeholder costs nothing
259
260
  // until someone asks a MongoDB question.
260
261
  FOUNDATION_MONGODB_SECRET_ID: names.secretMongodbReadonly,
262
+ // Collections the MongoDB tools refuse beyond the compiled-in defaults,
263
+ // from the instance file and never from config.yaml, so only a reviewed
264
+ // deploy changes it. Absent when the instance file adds nothing: the
265
+ // defaults hold without it.
266
+ ...(instance.mongodbReadonly.deniedCollections.length > 0 ||
267
+ instance.mongodbReadonly.unsafeReadDefaultDeniedCollections.length > 0
268
+ ? { FOUNDATION_MONGODB_COLLECTION_POLICY: mongoCollectionPolicyEnv(instance.mongodbReadonly) }
269
+ : {}),
261
270
  // CRM storage, the Otter credential, the Knock OAuth secrets, and the
262
271
  // Upwork/E*TRADE OAuth secrets are intentionally absent here: the
263
272
  // shell-capable agent receives only their fixed proxy ARNs.
@@ -23,6 +23,7 @@ import {
23
23
  provisionsIntegration,
24
24
  requiresAuthenticatedQueue,
25
25
  } from "../names.ts";
26
+ import { SECRET_BEARING_READ_ACTIONS } from "./secret-bearing-reads.ts";
26
27
 
27
28
  export interface FoundationAgentProps extends cdk.StackProps {
28
29
  /** The deployment this stack belongs to; every name below comes from it. */
@@ -154,6 +155,7 @@ export class FoundationAgent extends cdk.Stack {
154
155
  this.accessPoint = accessPoint;
155
156
  this.executionRole = this.buildExecutionRole(props);
156
157
  this.readOnlyRole = this.buildReadOnlyRole(props);
158
+ this.secretBearingReadsPolicy(props.instance, [this.executionRole, this.readOnlyRole]);
157
159
  this.buildSpansIngestionPolicy();
158
160
 
159
161
  // The gateway is independent of the runtime: it can (and does) exist
@@ -637,6 +639,8 @@ export class FoundationAgent extends cdk.Stack {
637
639
  * `ReadOnlyAccess` is AWS's own managed read-only policy. The inline
638
640
  * additions are the two things it does NOT cover which the observability
639
641
  * tools need: starting a Logs Insights query, and reading trace summaries.
642
+ * What it covers but must not, the reads whose responses carry secrets,
643
+ * is denied by `secretBearingReadsPolicy`.
640
644
  */
641
645
  private buildReadOnlyRole(props: FoundationAgentProps): iam.Role {
642
646
  const role = new iam.Role(this, "AgentReadOnlyRole", {
@@ -709,6 +713,36 @@ export class FoundationAgent extends cdk.Stack {
709
713
  return role;
710
714
  }
711
715
 
716
+ /**
717
+ * `ReadOnlyAccess` reads that return secrets: Lambda and container
718
+ * environment variables, user data, job and connection arguments, sampled
719
+ * WAF request headers, minted tokens ({@link SECRET_BEARING_READ_ACTIONS}).
720
+ *
721
+ * Both shell-reachable roles carry the deny. The read-only role needs it
722
+ * today, since its managed policy grants every one of these. The execution
723
+ * role grants none of them, and it gets the deny anyway for the same reason
724
+ * as the secret denies above: no future broad grant may reopen them.
725
+ *
726
+ * A managed policy of its own, like the pages policy, because the execution
727
+ * role's inline policy is near IAM's size limit. `Resource: "*"` because
728
+ * the point is every function, project and parameter in the account, not
729
+ * the instance's own.
730
+ */
731
+ private secretBearingReadsPolicy(instance: Instance, roles: iam.IRole[]): void {
732
+ new iam.ManagedPolicy(this, "AgentSecretBearingReadsDeny", {
733
+ description: `${instance.displayName} agent: no AWS read whose response carries a secret`,
734
+ roles,
735
+ statements: [
736
+ new iam.PolicyStatement({
737
+ sid: "DenySecretBearingReads",
738
+ effect: iam.Effect.DENY,
739
+ actions: [...SECRET_BEARING_READ_ACTIONS],
740
+ resources: ["*"],
741
+ }),
742
+ ],
743
+ });
744
+ }
745
+
712
746
  /**
713
747
  * Let X-Ray write this runtime's spans into the runtime's own log group.
714
748
  *
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Reads that AWS's `ReadOnlyAccess` grants whose RESPONSES carry secrets in
3
+ * plaintext: environment variables, user data, connection properties, request
4
+ * headers, issued tokens. Both shell-reachable agent roles deny every one of
5
+ * them (`FoundationAgent.secretBearingReadsPolicy`), so "read-only" never means
6
+ * "can read the credentials that write".
7
+ *
8
+ * Found 2026-10-26 in a company account: three Lambdas held a read-write
9
+ * MongoDB connection string in their environment, and sampled WAF requests
10
+ * held the web app's admin bearer tokens. Either one turned the read-only role
11
+ * into a write path.
12
+ *
13
+ * Judged against `ReadOnlyAccess` v190 (2026-09-24). The rule for adding an
14
+ * action: its response can contain a value someone typed in as configuration
15
+ * (env, user data, connection or job arguments, hosted config), a credential
16
+ * AWS mints for the caller, or another caller's request. Data-plane reads
17
+ * (S3 objects, DynamoDB items, queue messages, log events) are a different
18
+ * question, decided separately: see docs/capabilities/aws-readonly.md.
19
+ *
20
+ * What stays readable for diagnosis: CloudWatch Logs (Insights included),
21
+ * metrics, alarms, X-Ray, CloudTrail, every `Describe*`/`List*` not named here,
22
+ * stack events and outputs, SSM parameter NAMES, and Lambda's event source
23
+ * mappings, aliases, concurrency and policies.
24
+ *
25
+ * Every entry is an exact action name, never a wildcard, so a reviewer can
26
+ * check each one against the service authorization reference.
27
+ */
28
+ export const SECRET_BEARING_READ_ACTIONS: readonly string[] = [
29
+ // WAF sampled requests return full request headers, Authorization included.
30
+ "wafv2:GetSampledRequests",
31
+ "waf:GetSampledRequests",
32
+ "waf-regional:GetSampledRequests",
33
+
34
+ // Lambda: function configuration includes `Environment.Variables`, and
35
+ // GetFunction / GetLayerVersion add a presigned code download URL. List
36
+ // calls return the same configuration for every function or version.
37
+ "lambda:GetFunction",
38
+ "lambda:GetFunctionConfiguration",
39
+ "lambda:ListFunctions",
40
+ "lambda:ListVersionsByFunction",
41
+ "lambda:GetLayerVersion",
42
+ "lambda:GetLayerVersionByArn",
43
+
44
+ // Container and build configuration: plaintext environment variables.
45
+ "ecs:DescribeTaskDefinition",
46
+ "apprunner:DescribeService",
47
+ "batch:DescribeJobDefinitions",
48
+ "batch:DescribeJobs",
49
+ "codebuild:BatchGetProjects",
50
+ "codebuild:BatchGetBuilds",
51
+ "codebuild:BatchGetBuildBatches",
52
+
53
+ // Amplify apps and branches carry environment variables and basic-auth
54
+ // credentials; backend environments carry deployment settings.
55
+ "amplify:GetApp",
56
+ "amplify:ListApps",
57
+ "amplify:GetBranch",
58
+ "amplify:ListBranches",
59
+ "amplify:GetBackendEnvironment",
60
+
61
+ // Elastic Beanstalk environment properties are its environment variables.
62
+ "elasticbeanstalk:DescribeConfigurationSettings",
63
+
64
+ // EC2 user data (instance attribute and launch template versions).
65
+ "ec2:DescribeLaunchTemplateVersions",
66
+ "ec2:DescribeInstanceAttribute",
67
+
68
+ // SSM parameter VALUES. Denied for plain String parameters too: teams
69
+ // store secrets as String by mistake, and with the account's default
70
+ // `aws/ssm` key a SecureString can decrypt through the same call.
71
+ // DescribeParameters (names, types, versions) stays readable.
72
+ "ssm:GetParameter",
73
+ "ssm:GetParameters",
74
+ "ssm:GetParametersByPath",
75
+ "ssm:GetParameterHistory",
76
+
77
+ // CloudFormation template bodies can hold literal secrets and parameter
78
+ // defaults. Denied: DescribeStacks, DescribeStackEvents and
79
+ // DescribeStackResources answer the diagnosis questions without them.
80
+ "cloudformation:GetTemplate",
81
+
82
+ // AppConfig hosted configuration content is application configuration.
83
+ "appconfig:GetConfiguration",
84
+ "appconfig:GetHostedConfigurationVersion",
85
+
86
+ // Glue job default arguments and connection properties (JDBC user and
87
+ // password when not in Secrets Manager); dev endpoints carry job arguments.
88
+ // v190 does not grant GetConnection(s); they are named anyway because AWS
89
+ // revises the policy without notice and these return the password.
90
+ "glue:GetJob",
91
+ "glue:GetJobs",
92
+ "glue:BatchGetJobs",
93
+ "glue:GetConnection",
94
+ "glue:GetConnections",
95
+ "glue:GetDevEndpoint",
96
+ "glue:GetDevEndpoints",
97
+ "glue:BatchGetDevEndpoints",
98
+
99
+ // SageMaker container `Environment` maps on models and jobs.
100
+ "sagemaker:DescribeModel",
101
+ "sagemaker:DescribeTrainingJob",
102
+ "sagemaker:DescribeProcessingJob",
103
+ "sagemaker:DescribeTransformJob",
104
+
105
+ // Data Pipeline definitions hold activity fields, database passwords included.
106
+ "datapipeline:GetPipelineDefinition",
107
+
108
+ // AppSync: API key values, and HTTP data source authorization config.
109
+ "appsync:ListApiKeys",
110
+ "appsync:GetDataSource",
111
+ "appsync:ListDataSources",
112
+
113
+ // Kinesis Data Analytics / Managed Flink environment properties.
114
+ "kinesisanalytics:DescribeApplication",
115
+
116
+ // Lightsail bucket access keys and container service environment.
117
+ "lightsail:GetBucketAccessKeys",
118
+ "lightsail:GetContainerServices",
119
+ "lightsail:GetContainerServiceDeployments",
120
+
121
+ // OpsWorks app environment variables and stack custom JSON.
122
+ "opsworks:DescribeApps",
123
+ "opsworks:DescribeStacks",
124
+
125
+ // Calls that MINT a credential rather than describe one.
126
+ "cognito-identity:GetCredentialsForIdentity",
127
+ "cognito-identity:GetOpenIdToken",
128
+ "cognito-identity:GetOpenIdTokenForDeveloperIdentity",
129
+ "connect:GetFederationToken",
130
+ "codeartifact:GetAuthorizationToken",
131
+ ];