@beesolve/aws-accounts 1.0.7 → 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(
@@ -333,6 +453,7 @@ async function listPermissionSets(props) {
333
453
  permissionSetArn: permissionSet.PermissionSetArn,
334
454
  name: permissionSet.Name,
335
455
  description: permissionSet.Description ?? "",
456
+ sessionDuration: permissionSet.SessionDuration ?? null,
336
457
  inlinePolicy,
337
458
  awsManagedPolicies,
338
459
  customerManagedPolicies
@@ -450,6 +571,42 @@ async function listAccountsForPermissionSet(props) {
450
571
  } while (nextToken != null);
451
572
  return accountIds;
452
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
+ }
453
610
  export {
454
611
  scanIdentityCenter,
455
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,
@@ -46,6 +73,7 @@ const permissionSetSchema = v.strictObject({
46
73
  permissionSetArn: nonEmptyString,
47
74
  name: nonEmptyString,
48
75
  description: v.string(),
76
+ sessionDuration: v.nullable(v.string()),
49
77
  inlinePolicy: v.nullable(nonEmptyString),
50
78
  awsManagedPolicies: v.array(nonEmptyString),
51
79
  customerManagedPolicies: v.array(customerManagedPolicyReferenceSchema)
@@ -63,13 +91,19 @@ const accessRoleSchema = v.strictObject({
63
91
  principalType: principalTypeSchema,
64
92
  roleName: nonEmptyString
65
93
  });
94
+ const accessControlAttributeSchema = v.strictObject({
95
+ key: nonEmptyString,
96
+ source: v.array(nonEmptyString)
97
+ });
66
98
  const stateSchema = v.strictObject({
67
99
  version: nonEmptyString,
68
100
  generatedAt: nonEmptyString,
69
101
  organization: v.strictObject({
70
102
  rootId: nonEmptyString,
71
103
  organizationalUnits: v.array(organizationalUnitSchema),
72
- accounts: v.array(accountSchema)
104
+ accounts: v.array(accountSchema),
105
+ policies: v.optional(v.array(orgPolicySchema)),
106
+ policyAttachments: v.optional(v.array(orgPolicyAttachmentSchema))
73
107
  }),
74
108
  identityCenter: v.strictObject({
75
109
  instanceArn: nonEmptyString,
@@ -79,13 +113,16 @@ const stateSchema = v.strictObject({
79
113
  groupMemberships: v.array(groupMembershipSchema),
80
114
  permissionSets: v.array(permissionSetSchema),
81
115
  accountAssignments: v.array(accountAssignmentSchema),
82
- accessRoles: v.array(accessRoleSchema)
116
+ accessRoles: v.array(accessRoleSchema),
117
+ accessControlAttributes: v.array(accessControlAttributeSchema)
83
118
  })
84
119
  });
85
120
  function validateState(value) {
86
121
  return v.parse(stateSchema, value);
87
122
  }
88
123
  function createWorkingState(props) {
124
+ const policies = props.state.organization.policies ?? [];
125
+ const policyAttachments = props.state.organization.policyAttachments ?? [];
89
126
  return {
90
127
  version: props.state.version,
91
128
  generatedAt: props.state.generatedAt,
@@ -99,6 +136,13 @@ function createWorkingState(props) {
99
136
  accountsByName: toRecordByProperty(
100
137
  props.state.organization.accounts,
101
138
  "name"
139
+ ),
140
+ policiesById: toRecordByProperty(policies, "id"),
141
+ policiesByName: toRecordByProperty(policies, "name"),
142
+ policyAttachments: structuredClone(policyAttachments),
143
+ policyAttachmentsByKey: toRecordByProperty(
144
+ policyAttachments,
145
+ createOrgPolicyAttachmentKey
102
146
  )
103
147
  },
104
148
  identityCenter: createWorkingIdentityCenterState({
@@ -115,7 +159,11 @@ function materializeWorkingState(props) {
115
159
  organizationalUnits: Object.values(
116
160
  props.workingState.organization.organizationalUnitsById
117
161
  ),
118
- 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
+ )
119
167
  },
120
168
  identityCenter: {
121
169
  instanceArn: props.workingState.identityCenter.instanceArn,
@@ -133,6 +181,9 @@ function materializeWorkingState(props) {
133
181
  ),
134
182
  accessRoles: structuredClone(
135
183
  props.workingState.identityCenter.accessRoles
184
+ ),
185
+ accessControlAttributes: structuredClone(
186
+ props.workingState.identityCenter.accessControlAttributes
136
187
  )
137
188
  }
138
189
  };
@@ -345,7 +396,7 @@ function removeIdcGroupFromWorkingState(props) {
345
396
  }
346
397
  function upsertIdcPermissionSetInWorkingState(props) {
347
398
  const currentPermissionSet = props.workingState.identityCenter.permissionSetsByName[props.permissionSet.name];
348
- if (currentPermissionSet != null && currentPermissionSet.permissionSetArn === props.permissionSet.permissionSetArn && currentPermissionSet.name === props.permissionSet.name && currentPermissionSet.description === props.permissionSet.description && currentPermissionSet.inlinePolicy === props.permissionSet.inlinePolicy && JSON.stringify(currentPermissionSet.awsManagedPolicies) === JSON.stringify(props.permissionSet.awsManagedPolicies) && JSON.stringify(currentPermissionSet.customerManagedPolicies) === JSON.stringify(props.permissionSet.customerManagedPolicies)) {
399
+ if (currentPermissionSet != null && currentPermissionSet.permissionSetArn === props.permissionSet.permissionSetArn && currentPermissionSet.name === props.permissionSet.name && currentPermissionSet.description === props.permissionSet.description && currentPermissionSet.sessionDuration === props.permissionSet.sessionDuration && currentPermissionSet.inlinePolicy === props.permissionSet.inlinePolicy && JSON.stringify(currentPermissionSet.awsManagedPolicies) === JSON.stringify(props.permissionSet.awsManagedPolicies) && JSON.stringify(currentPermissionSet.customerManagedPolicies) === JSON.stringify(props.permissionSet.customerManagedPolicies)) {
349
400
  return props.workingState;
350
401
  }
351
402
  const remainingPermissionSets = props.workingState.identityCenter.permissionSets.filter(
@@ -487,6 +538,101 @@ function removeAccountAssignmentFromWorkingState(props) {
487
538
  })
488
539
  };
489
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
+ }
490
636
  async function readStateFile(path) {
491
637
  const content = await readFile(path, "utf8");
492
638
  const parsed = JSON.parse(content);
@@ -526,7 +672,10 @@ function createWorkingIdentityCenterState(props) {
526
672
  ),
527
673
  accessRoles: createAccessRoles({
528
674
  accountAssignments
529
- })
675
+ }),
676
+ accessControlAttributes: structuredClone(
677
+ props.identityCenter.accessControlAttributes ?? []
678
+ )
530
679
  };
531
680
  }
532
681
  function materializeWorkingIdentityCenterState(props) {
@@ -540,7 +689,10 @@ function materializeWorkingIdentityCenterState(props) {
540
689
  accountAssignments: structuredClone(
541
690
  props.identityCenter.accountAssignments
542
691
  ),
543
- accessRoles: structuredClone(props.identityCenter.accessRoles)
692
+ accessRoles: structuredClone(props.identityCenter.accessRoles),
693
+ accessControlAttributes: structuredClone(
694
+ props.identityCenter.accessControlAttributes
695
+ )
544
696
  };
545
697
  }
546
698
  function createAccessRoles(props) {
@@ -595,8 +747,10 @@ function sortJsonValue(value) {
595
747
  export {
596
748
  addAccountAssignmentToWorkingState,
597
749
  addGroupMembershipToWorkingState,
750
+ addOrgPolicyAttachmentToWorkingState,
598
751
  createAccessRoleName,
599
752
  createGroupMembershipKey,
753
+ createOrgPolicyAttachmentKey,
600
754
  createWorkingState,
601
755
  materializeWorkingState,
602
756
  moveAccountInWorkingState,
@@ -606,6 +760,8 @@ export {
606
760
  removeIdcGroupFromWorkingState,
607
761
  removeIdcPermissionSetFromWorkingState,
608
762
  removeIdcUserFromWorkingState,
763
+ removeOrgPolicyAttachmentFromWorkingState,
764
+ removeOrgPolicyFromWorkingState,
609
765
  removeOrganizationalUnitFromWorkingState,
610
766
  renameOrganizationalUnitInWorkingState,
611
767
  stateSchema,
@@ -613,6 +769,7 @@ export {
613
769
  upsertIdcGroupInWorkingState,
614
770
  upsertIdcPermissionSetInWorkingState,
615
771
  upsertIdcUserInWorkingState,
772
+ upsertOrgPolicyInWorkingState,
616
773
  upsertOrganizationalUnitInWorkingState,
617
774
  validateState
618
775
  };