@beesolve/aws-accounts 1.1.0 → 1.2.0

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/scanLogic.js CHANGED
@@ -4,11 +4,15 @@ import {
4
4
  ListUsersCommand
5
5
  } from "@aws-sdk/client-identitystore";
6
6
  import {
7
+ DescribeOrganizationCommand,
8
+ DescribePolicyCommand,
7
9
  ListAccountsCommand,
8
10
  ListOrganizationalUnitsForParentCommand,
9
11
  ListParentsCommand,
12
+ ListPoliciesCommand,
10
13
  ListRootsCommand,
11
- ListTagsForResourceCommand
14
+ ListTagsForResourceCommand,
15
+ ListTargetsForPolicyCommand
12
16
  } from "@aws-sdk/client-organizations";
13
17
  import {
14
18
  DescribePermissionSetCommand,
@@ -16,19 +20,27 @@ import {
16
20
  ListAccountAssignmentsCommand,
17
21
  ListAccountsForProvisionedPermissionSetCommand,
18
22
  ListCustomerManagedPolicyReferencesInPermissionSetCommand,
23
+ DescribeInstanceAccessControlAttributeConfigurationCommand,
19
24
  ListInstancesCommand,
20
25
  ListManagedPoliciesInPermissionSetCommand,
21
26
  ListPermissionSetsCommand
22
27
  } from "@aws-sdk/client-sso-admin";
28
+ import {
29
+ GetAlternateContactCommand
30
+ } from "@aws-sdk/client-account";
23
31
  import {
24
32
  createAccessRoleName
25
33
  } from "./state.js";
26
34
  async function scanOrganization(props) {
27
- const roots = await props.organizationsClient.send(new ListRootsCommand({}));
28
- const root = roots.Roots?.[0];
35
+ const [rootsResponse, orgResponse] = await Promise.all([
36
+ props.organizationsClient.send(new ListRootsCommand({})),
37
+ props.organizationsClient.send(new DescribeOrganizationCommand({}))
38
+ ]);
39
+ const root = rootsResponse.Roots?.[0];
29
40
  if (root?.Id == null) {
30
41
  throw new Error("No organization root found.");
31
42
  }
43
+ const managementAccountId = orgResponse.Organization?.MasterAccountId;
32
44
  const organizationalUnits = await collectOrganizationalUnits({
33
45
  organizationsClient: props.organizationsClient,
34
46
  parentId: root.Id
@@ -52,6 +64,11 @@ async function scanOrganization(props) {
52
64
  ResourceId: account.Id
53
65
  })
54
66
  );
67
+ const alternateContacts = await scanAlternateContacts({
68
+ accountClient: props.accountClient,
69
+ accountId: account.Id,
70
+ isManagementAccount: account.Id === managementAccountId
71
+ });
55
72
  accounts.push({
56
73
  id: account.Id,
57
74
  arn: account.Arn,
@@ -69,17 +86,95 @@ async function scanOrganization(props) {
69
86
  value: tag.Value ?? ""
70
87
  }
71
88
  ];
72
- })
89
+ }),
90
+ alternateContacts: alternateContacts.length > 0 ? alternateContacts : void 0
73
91
  });
74
92
  }
75
93
  nextToken = response.NextToken;
76
94
  } while (nextToken != null);
95
+ const { policies, policyAttachments } = await scanOrganizationPolicies({
96
+ organizationsClient: props.organizationsClient
97
+ });
77
98
  return {
78
99
  rootId: root.Id,
79
100
  organizationalUnits,
80
- accounts
101
+ accounts,
102
+ policies,
103
+ policyAttachments
81
104
  };
82
105
  }
106
+ const ORG_POLICY_TYPES = [
107
+ "SERVICE_CONTROL_POLICY",
108
+ "RESOURCE_CONTROL_POLICY",
109
+ "TAG_POLICY",
110
+ "AISERVICES_OPT_OUT_POLICY"
111
+ ];
112
+ async function scanOrganizationPolicies(props) {
113
+ const policies = [];
114
+ const policyAttachments = [];
115
+ for (const policyType of ORG_POLICY_TYPES) {
116
+ let nextToken;
117
+ const policyIds = [];
118
+ do {
119
+ const response = await props.organizationsClient.send(
120
+ new ListPoliciesCommand({ Filter: policyType, NextToken: nextToken })
121
+ );
122
+ for (const summary of response.Policies ?? []) {
123
+ if (summary.Id == null || summary.AwsManaged === true) {
124
+ continue;
125
+ }
126
+ policyIds.push(summary.Id);
127
+ }
128
+ nextToken = response.NextToken;
129
+ } while (nextToken != null);
130
+ for (const policyId of policyIds) {
131
+ const describeResponse = await props.organizationsClient.send(
132
+ new DescribePolicyCommand({ PolicyId: policyId })
133
+ );
134
+ const policy = describeResponse.Policy;
135
+ if (policy?.PolicySummary?.Id == null || policy.PolicySummary.Arn == null || policy.PolicySummary.Name == null) {
136
+ continue;
137
+ }
138
+ const content = policy.Content;
139
+ if (content == null || content.length === 0) {
140
+ continue;
141
+ }
142
+ policies.push({
143
+ id: policy.PolicySummary.Id,
144
+ arn: policy.PolicySummary.Arn,
145
+ name: policy.PolicySummary.Name,
146
+ description: policy.PolicySummary.Description ?? "",
147
+ type: policyType,
148
+ content
149
+ });
150
+ let targetsNextToken;
151
+ do {
152
+ const targetsResponse = await props.organizationsClient.send(
153
+ new ListTargetsForPolicyCommand({
154
+ PolicyId: policyId,
155
+ NextToken: targetsNextToken
156
+ })
157
+ );
158
+ for (const target of targetsResponse.Targets ?? []) {
159
+ if (target.TargetId == null || target.Type == null) {
160
+ continue;
161
+ }
162
+ const targetType = target.Type;
163
+ if (targetType !== "ROOT" && targetType !== "ORGANIZATIONAL_UNIT" && targetType !== "ACCOUNT") {
164
+ continue;
165
+ }
166
+ policyAttachments.push({
167
+ policyId,
168
+ targetId: target.TargetId,
169
+ targetType
170
+ });
171
+ }
172
+ targetsNextToken = targetsResponse.NextToken;
173
+ } while (targetsNextToken != null);
174
+ }
175
+ }
176
+ return { policies, policyAttachments };
177
+ }
83
178
  async function collectOrganizationalUnits(props) {
84
179
  const children = [];
85
180
  let nextToken;
@@ -122,7 +217,7 @@ async function scanIdentityCenter(props) {
122
217
  instances,
123
218
  requestedInstanceArn: props.requestedInstanceArn
124
219
  });
125
- const [users, groups, permissionSets] = await Promise.all([
220
+ const [users, groups, permissionSets, accessControlAttributes] = await Promise.all([
126
221
  listIdentityStoreUsers({
127
222
  identityStoreClient: props.identityStoreClient,
128
223
  identityStoreId: instance.identityStoreId
@@ -134,6 +229,10 @@ async function scanIdentityCenter(props) {
134
229
  listPermissionSets({
135
230
  ssoAdminClient: props.ssoAdminClient,
136
231
  instanceArn: instance.instanceArn
232
+ }),
233
+ scanAccessControlAttributes({
234
+ ssoAdminClient: props.ssoAdminClient,
235
+ instanceArn: instance.instanceArn
137
236
  })
138
237
  ]);
139
238
  const groupMemberships = await listGroupMemberships({
@@ -158,9 +257,30 @@ async function scanIdentityCenter(props) {
158
257
  groupMemberships,
159
258
  permissionSets,
160
259
  accountAssignments,
161
- accessRoles
260
+ accessRoles,
261
+ accessControlAttributes
162
262
  };
163
263
  }
264
+ async function scanAccessControlAttributes(props) {
265
+ let response;
266
+ try {
267
+ response = await props.ssoAdminClient.send(
268
+ new DescribeInstanceAccessControlAttributeConfigurationCommand({
269
+ InstanceArn: props.instanceArn
270
+ })
271
+ );
272
+ } catch (err) {
273
+ if (err != null && typeof err === "object" && "name" in err && err.name === "ResourceNotFoundException") {
274
+ return [];
275
+ }
276
+ throw err;
277
+ }
278
+ const attributes = response.InstanceAccessControlAttributeConfiguration?.AccessControlAttributes ?? [];
279
+ return attributes.filter((attr) => attr.Key != null).map((attr) => ({
280
+ key: attr.Key,
281
+ source: attr.Value?.Source ?? []
282
+ }));
283
+ }
164
284
  function selectIdentityCenterInstance(props) {
165
285
  if (props.requestedInstanceArn != null) {
166
286
  const selected2 = props.instances.find(
@@ -451,6 +571,42 @@ async function listAccountsForPermissionSet(props) {
451
571
  } while (nextToken != null);
452
572
  return accountIds;
453
573
  }
574
+ const ALTERNATE_CONTACT_TYPES = [
575
+ "BILLING",
576
+ "OPERATIONS",
577
+ "SECURITY"
578
+ ];
579
+ async function scanAlternateContacts(props) {
580
+ const results = await Promise.all(
581
+ ALTERNATE_CONTACT_TYPES.map(async (contactType) => {
582
+ try {
583
+ const response = await props.accountClient.send(
584
+ new GetAlternateContactCommand({
585
+ AccountId: props.isManagementAccount ? void 0 : props.accountId,
586
+ AlternateContactType: contactType
587
+ })
588
+ );
589
+ const c = response.AlternateContact;
590
+ if (c == null || c.EmailAddress == null || c.Name == null) {
591
+ return null;
592
+ }
593
+ return {
594
+ contactType,
595
+ name: c.Name,
596
+ email: c.EmailAddress,
597
+ phone: c.PhoneNumber ?? "",
598
+ ...c.Title != null ? { title: c.Title } : {}
599
+ };
600
+ } catch (error) {
601
+ if (error != null && typeof error === "object" && "name" in error && error.name === "ResourceNotFoundException") {
602
+ return null;
603
+ }
604
+ throw error;
605
+ }
606
+ })
607
+ );
608
+ return results.filter((c) => c != null);
609
+ }
454
610
  export {
455
611
  scanIdentityCenter,
456
612
  scanOrganization
package/dist/state.js CHANGED
@@ -9,10 +9,36 @@ const organizationalUnitSchema = v.strictObject({
9
9
  arn: nonEmptyString,
10
10
  name: nonEmptyString
11
11
  });
12
+ const orgPolicyTypeSchema = v.picklist([
13
+ "SERVICE_CONTROL_POLICY",
14
+ "RESOURCE_CONTROL_POLICY",
15
+ "TAG_POLICY",
16
+ "AISERVICES_OPT_OUT_POLICY"
17
+ ]);
18
+ const orgPolicySchema = v.strictObject({
19
+ id: nonEmptyString,
20
+ arn: nonEmptyString,
21
+ name: nonEmptyString,
22
+ description: v.string(),
23
+ type: orgPolicyTypeSchema,
24
+ content: nonEmptyString
25
+ });
26
+ const orgPolicyAttachmentSchema = v.strictObject({
27
+ policyId: nonEmptyString,
28
+ targetId: nonEmptyString,
29
+ targetType: v.picklist(["ROOT", "ORGANIZATIONAL_UNIT", "ACCOUNT"])
30
+ });
12
31
  const accountTagSchema = v.strictObject({
13
32
  key: nonEmptyString,
14
33
  value: v.string()
15
34
  });
35
+ const alternateContactSchema = v.strictObject({
36
+ contactType: v.picklist(["BILLING", "OPERATIONS", "SECURITY"]),
37
+ name: v.string(),
38
+ email: v.string(),
39
+ phone: v.string(),
40
+ title: v.optional(v.string())
41
+ });
16
42
  const accountSchema = v.strictObject({
17
43
  id: nonEmptyString,
18
44
  arn: nonEmptyString,
@@ -20,7 +46,8 @@ const accountSchema = v.strictObject({
20
46
  email: nonEmptyString,
21
47
  status: nonEmptyString,
22
48
  parentId: nonEmptyString,
23
- tags: v.array(accountTagSchema)
49
+ tags: v.array(accountTagSchema),
50
+ alternateContacts: v.optional(v.array(alternateContactSchema))
24
51
  });
25
52
  const userSchema = v.strictObject({
26
53
  userId: nonEmptyString,
@@ -64,13 +91,19 @@ const accessRoleSchema = v.strictObject({
64
91
  principalType: principalTypeSchema,
65
92
  roleName: nonEmptyString
66
93
  });
94
+ const accessControlAttributeSchema = v.strictObject({
95
+ key: nonEmptyString,
96
+ source: v.array(nonEmptyString)
97
+ });
67
98
  const stateSchema = v.strictObject({
68
99
  version: nonEmptyString,
69
100
  generatedAt: nonEmptyString,
70
101
  organization: v.strictObject({
71
102
  rootId: nonEmptyString,
72
103
  organizationalUnits: v.array(organizationalUnitSchema),
73
- accounts: v.array(accountSchema)
104
+ accounts: v.array(accountSchema),
105
+ policies: v.optional(v.array(orgPolicySchema)),
106
+ policyAttachments: v.optional(v.array(orgPolicyAttachmentSchema))
74
107
  }),
75
108
  identityCenter: v.strictObject({
76
109
  instanceArn: nonEmptyString,
@@ -80,13 +113,16 @@ const stateSchema = v.strictObject({
80
113
  groupMemberships: v.array(groupMembershipSchema),
81
114
  permissionSets: v.array(permissionSetSchema),
82
115
  accountAssignments: v.array(accountAssignmentSchema),
83
- accessRoles: v.array(accessRoleSchema)
116
+ accessRoles: v.array(accessRoleSchema),
117
+ accessControlAttributes: v.array(accessControlAttributeSchema)
84
118
  })
85
119
  });
86
120
  function validateState(value) {
87
121
  return v.parse(stateSchema, value);
88
122
  }
89
123
  function createWorkingState(props) {
124
+ const policies = props.state.organization.policies ?? [];
125
+ const policyAttachments = props.state.organization.policyAttachments ?? [];
90
126
  return {
91
127
  version: props.state.version,
92
128
  generatedAt: props.state.generatedAt,
@@ -100,6 +136,13 @@ function createWorkingState(props) {
100
136
  accountsByName: toRecordByProperty(
101
137
  props.state.organization.accounts,
102
138
  "name"
139
+ ),
140
+ policiesById: toRecordByProperty(policies, "id"),
141
+ policiesByName: toRecordByProperty(policies, "name"),
142
+ policyAttachments: structuredClone(policyAttachments),
143
+ policyAttachmentsByKey: toRecordByProperty(
144
+ policyAttachments,
145
+ createOrgPolicyAttachmentKey
103
146
  )
104
147
  },
105
148
  identityCenter: createWorkingIdentityCenterState({
@@ -116,7 +159,11 @@ function materializeWorkingState(props) {
116
159
  organizationalUnits: Object.values(
117
160
  props.workingState.organization.organizationalUnitsById
118
161
  ),
119
- accounts: Object.values(props.workingState.organization.accountsById)
162
+ accounts: Object.values(props.workingState.organization.accountsById),
163
+ policies: Object.values(props.workingState.organization.policiesById),
164
+ policyAttachments: structuredClone(
165
+ props.workingState.organization.policyAttachments
166
+ )
120
167
  },
121
168
  identityCenter: {
122
169
  instanceArn: props.workingState.identityCenter.instanceArn,
@@ -134,6 +181,9 @@ function materializeWorkingState(props) {
134
181
  ),
135
182
  accessRoles: structuredClone(
136
183
  props.workingState.identityCenter.accessRoles
184
+ ),
185
+ accessControlAttributes: structuredClone(
186
+ props.workingState.identityCenter.accessControlAttributes
137
187
  )
138
188
  }
139
189
  };
@@ -488,6 +538,101 @@ function removeAccountAssignmentFromWorkingState(props) {
488
538
  })
489
539
  };
490
540
  }
541
+ function createOrgPolicyAttachmentKey(props) {
542
+ return [props.policyId, props.targetId].join("|");
543
+ }
544
+ function upsertOrgPolicyInWorkingState(props) {
545
+ const currentPolicy = props.workingState.organization.policiesById[props.policy.id];
546
+ if (currentPolicy != null && currentPolicy.id === props.policy.id && currentPolicy.arn === props.policy.arn && currentPolicy.name === props.policy.name && currentPolicy.description === props.policy.description && currentPolicy.type === props.policy.type && currentPolicy.content === props.policy.content) {
547
+ return props.workingState;
548
+ }
549
+ const remainingPolicies = Object.values(
550
+ props.workingState.organization.policiesById
551
+ ).filter((p) => p.id !== props.policy.id);
552
+ const nextPolicies = [...remainingPolicies, props.policy];
553
+ return {
554
+ ...props.workingState,
555
+ organization: {
556
+ ...props.workingState.organization,
557
+ policiesById: toRecordByProperty(nextPolicies, "id"),
558
+ policiesByName: toRecordByProperty(nextPolicies, "name")
559
+ }
560
+ };
561
+ }
562
+ function removeOrgPolicyFromWorkingState(props) {
563
+ if (props.workingState.organization.policiesById[props.policyId] == null) {
564
+ return props.workingState;
565
+ }
566
+ const nextPolicies = Object.values(
567
+ props.workingState.organization.policiesById
568
+ ).filter((p) => p.id !== props.policyId);
569
+ const nextAttachments = props.workingState.organization.policyAttachments.filter(
570
+ (a) => a.policyId !== props.policyId
571
+ );
572
+ return {
573
+ ...props.workingState,
574
+ organization: {
575
+ ...props.workingState.organization,
576
+ policiesById: toRecordByProperty(nextPolicies, "id"),
577
+ policiesByName: toRecordByProperty(nextPolicies, "name"),
578
+ policyAttachments: nextAttachments,
579
+ policyAttachmentsByKey: toRecordByProperty(
580
+ nextAttachments,
581
+ createOrgPolicyAttachmentKey
582
+ )
583
+ }
584
+ };
585
+ }
586
+ function addOrgPolicyAttachmentToWorkingState(props) {
587
+ const key = createOrgPolicyAttachmentKey({
588
+ policyId: props.attachment.policyId,
589
+ targetId: props.attachment.targetId
590
+ });
591
+ if (props.workingState.organization.policyAttachmentsByKey[key] != null) {
592
+ return props.workingState;
593
+ }
594
+ const nextAttachments = [
595
+ ...props.workingState.organization.policyAttachments,
596
+ props.attachment
597
+ ];
598
+ return {
599
+ ...props.workingState,
600
+ organization: {
601
+ ...props.workingState.organization,
602
+ policyAttachments: nextAttachments,
603
+ policyAttachmentsByKey: toRecordByProperty(
604
+ nextAttachments,
605
+ createOrgPolicyAttachmentKey
606
+ )
607
+ }
608
+ };
609
+ }
610
+ function removeOrgPolicyAttachmentFromWorkingState(props) {
611
+ const key = createOrgPolicyAttachmentKey({
612
+ policyId: props.policyId,
613
+ targetId: props.targetId
614
+ });
615
+ if (props.workingState.organization.policyAttachmentsByKey[key] == null) {
616
+ return props.workingState;
617
+ }
618
+ const nextAttachments = props.workingState.organization.policyAttachments.filter(
619
+ (a) => createOrgPolicyAttachmentKey({
620
+ policyId: a.policyId,
621
+ targetId: a.targetId
622
+ }) !== key
623
+ );
624
+ return {
625
+ ...props.workingState,
626
+ organization: {
627
+ ...props.workingState.organization,
628
+ policyAttachments: nextAttachments,
629
+ policyAttachmentsByKey: toRecordByProperty(
630
+ nextAttachments,
631
+ createOrgPolicyAttachmentKey
632
+ )
633
+ }
634
+ };
635
+ }
491
636
  async function readStateFile(path) {
492
637
  const content = await readFile(path, "utf8");
493
638
  const parsed = JSON.parse(content);
@@ -527,7 +672,10 @@ function createWorkingIdentityCenterState(props) {
527
672
  ),
528
673
  accessRoles: createAccessRoles({
529
674
  accountAssignments
530
- })
675
+ }),
676
+ accessControlAttributes: structuredClone(
677
+ props.identityCenter.accessControlAttributes ?? []
678
+ )
531
679
  };
532
680
  }
533
681
  function materializeWorkingIdentityCenterState(props) {
@@ -541,7 +689,10 @@ function materializeWorkingIdentityCenterState(props) {
541
689
  accountAssignments: structuredClone(
542
690
  props.identityCenter.accountAssignments
543
691
  ),
544
- accessRoles: structuredClone(props.identityCenter.accessRoles)
692
+ accessRoles: structuredClone(props.identityCenter.accessRoles),
693
+ accessControlAttributes: structuredClone(
694
+ props.identityCenter.accessControlAttributes
695
+ )
545
696
  };
546
697
  }
547
698
  function createAccessRoles(props) {
@@ -596,8 +747,10 @@ function sortJsonValue(value) {
596
747
  export {
597
748
  addAccountAssignmentToWorkingState,
598
749
  addGroupMembershipToWorkingState,
750
+ addOrgPolicyAttachmentToWorkingState,
599
751
  createAccessRoleName,
600
752
  createGroupMembershipKey,
753
+ createOrgPolicyAttachmentKey,
601
754
  createWorkingState,
602
755
  materializeWorkingState,
603
756
  moveAccountInWorkingState,
@@ -607,6 +760,8 @@ export {
607
760
  removeIdcGroupFromWorkingState,
608
761
  removeIdcPermissionSetFromWorkingState,
609
762
  removeIdcUserFromWorkingState,
763
+ removeOrgPolicyAttachmentFromWorkingState,
764
+ removeOrgPolicyFromWorkingState,
610
765
  removeOrganizationalUnitFromWorkingState,
611
766
  renameOrganizationalUnitInWorkingState,
612
767
  stateSchema,
@@ -614,6 +769,7 @@ export {
614
769
  upsertIdcGroupInWorkingState,
615
770
  upsertIdcPermissionSetInWorkingState,
616
771
  upsertIdcUserInWorkingState,
772
+ upsertOrgPolicyInWorkingState,
617
773
  upsertOrganizationalUnitInWorkingState,
618
774
  validateState
619
775
  };