refactor: improve type safety and enhance AWS SDK integration
All checks were successful
/ deploy_site (push) Successful in 2m14s

- Updated the `Memorizer` class to use a more specific `AnyClient` type for AWS SDK clients.
- Enhanced type safety in various AWS service classes by replacing `unknown` casts with specific types (e.g., `AuthorizationType`, `DistributionConfig`, `PolicyDocument`).
- Refactored error handling to ensure proper type assertions for error objects across multiple services.
- Simplified the `fix` method signatures in several classes to accept only non-compliant resources, improving clarity and usability.
This commit is contained in:
2025-01-16 17:03:13 +09:00
parent ae1fd64367
commit cd1d80a0b7
29 changed files with 117 additions and 101 deletions

View File

@ -1,6 +1,9 @@
import { Client } from '@smithy/smithy-client'
import shajs from 'sha.js'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyClient = Client<any, any, any, any>
/**
* Memorize AWS SDK operation results.
* This util class tend to be always re-use AWS SDK Client
@ -15,7 +18,7 @@ import shajs from 'sha.js'
export class Memorizer {
private static memorized = new Map<string, Memorizer>()
public static memo(client: Client<unknown, unknown, unknown, unknown>, salt = '') {
public static memo(client: AnyClient, salt = '') {
const serialized = JSON.stringify([client.constructor.name, salt])
const hashed = shajs('sha256').update(serialized).digest('hex')
@ -35,7 +38,7 @@ export class Memorizer {
private memorized = new Map<string, unknown>()
private constructor(private client: Client<unknown, unknown, unknown, unknown>) {}
private constructor(private client: AnyClient) {}
public readonly send: typeof this.client.send = async (command) => {
const serialized = JSON.stringify([command.constructor.name, command.input])

View File

@ -1,4 +1,10 @@
import { ApiGatewayV2Client, GetApisCommand, GetRoutesCommand, UpdateRouteCommand } from '@aws-sdk/client-apigatewayv2'
import {
ApiGatewayV2Client,
AuthorizationType,
GetApisCommand,
GetRoutesCommand,
UpdateRouteCommand
} from '@aws-sdk/client-apigatewayv2'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'
@ -135,7 +141,7 @@ export class APIGatewayV2AuthorizationTypeConfigured implements BPSet {
new UpdateRouteCommand({
ApiId: api.ApiId!,
RouteId: route.RouteId!,
AuthorizationType: authorizationType as unknown
AuthorizationType: authorizationType as AuthorizationType
})
)
}

View File

@ -2,7 +2,8 @@ import {
CloudFrontClient,
ListDistributionsCommand,
GetDistributionCommand,
UpdateDistributionCommand
UpdateDistributionCommand,
DistributionConfig
} from '@aws-sdk/client-cloudfront'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'
@ -147,7 +148,7 @@ export class CloudFrontAccessLogsEnabled implements BPSet {
new UpdateDistributionCommand({
Id: distributionId,
IfMatch: etag,
DistributionConfig: updatedConfig as unknown
DistributionConfig: updatedConfig as DistributionConfig
})
)
}

View File

@ -2,7 +2,8 @@ import {
CloudFrontClient,
ListDistributionsCommand,
GetDistributionCommand,
UpdateDistributionCommand
UpdateDistributionCommand,
DistributionConfig
} from '@aws-sdk/client-cloudfront'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'
@ -137,7 +138,7 @@ export class CloudFrontAssociatedWithWAF implements BPSet {
new UpdateDistributionCommand({
Id: distributionId,
IfMatch: etag,
DistributionConfig: updatedConfig as unknown
DistributionConfig: updatedConfig as DistributionConfig
})
)
}

View File

@ -2,7 +2,8 @@ import {
CloudFrontClient,
ListDistributionsCommand,
GetDistributionCommand,
UpdateDistributionCommand
UpdateDistributionCommand,
DistributionConfig
} from '@aws-sdk/client-cloudfront'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'
@ -143,7 +144,7 @@ export class CloudFrontDefaultRootObjectConfigured implements BPSet {
new UpdateDistributionCommand({
Id: distributionId,
IfMatch: etag,
DistributionConfig: updatedConfig as unknown
DistributionConfig: updatedConfig as DistributionConfig
})
)
}

View File

@ -2,7 +2,8 @@ import {
CloudFrontClient,
ListDistributionsCommand,
GetDistributionCommand,
UpdateDistributionCommand
UpdateDistributionCommand,
DistributionConfig
} from '@aws-sdk/client-cloudfront'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'
@ -151,7 +152,7 @@ export class CloudFrontNoDeprecatedSSLProtocols implements BPSet {
new UpdateDistributionCommand({
Id: distributionId,
IfMatch: etag,
DistributionConfig: updatedConfig as unknown
DistributionConfig: updatedConfig as DistributionConfig
})
)
}

View File

@ -2,7 +2,8 @@ import {
CloudFrontClient,
ListDistributionsCommand,
GetDistributionCommand,
UpdateDistributionCommand
UpdateDistributionCommand,
DistributionConfig
} from '@aws-sdk/client-cloudfront'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'
@ -160,7 +161,7 @@ export class CloudFrontS3OriginAccessControlEnabled implements BPSet {
new UpdateDistributionCommand({
Id: distributionId,
IfMatch: etag,
DistributionConfig: updatedConfig as unknown
DistributionConfig: updatedConfig as DistributionConfig
})
)
}

View File

@ -2,7 +2,8 @@ import {
CloudFrontClient,
ListDistributionsCommand,
GetDistributionCommand,
UpdateDistributionCommand
UpdateDistributionCommand,
DistributionConfig
} from '@aws-sdk/client-cloudfront'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'
@ -145,7 +146,7 @@ export class CloudFrontViewerPolicyHTTPS implements BPSet {
new UpdateDistributionCommand({
Id: distributionId,
IfMatch: etag,
DistributionConfig: updatedConfig as unknown
DistributionConfig: updatedConfig as DistributionConfig
})
)
}

View File

@ -1,4 +1,10 @@
import { CloudWatchClient, DescribeAlarmsCommand, PutMetricAlarmCommand } from '@aws-sdk/client-cloudwatch'
import {
CloudWatchClient,
ComparisonOperator,
DescribeAlarmsCommand,
PutMetricAlarmCommand,
Statistic
} from '@aws-sdk/client-cloudwatch'
import { BPSet, BPSetFixFn, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'
@ -88,7 +94,7 @@ export class CloudWatchAlarmSettingsCheck implements BPSet {
const compliantResources: string[] = []
const nonCompliantResources: string[] = []
const alarms = await this.getAlarms()
const parameters = {
const parameters: Record<string, string | null> = {
MetricName: '', // Required
Threshold: null,
EvaluationPeriods: null,
@ -100,7 +106,7 @@ export class CloudWatchAlarmSettingsCheck implements BPSet {
for (const alarm of alarms) {
let isCompliant = true
for (const key of Object.keys(parameters).filter((k) => (parameters as unknown)[k] !== null)) {
for (const key of Object.keys(parameters).filter((k) => parameters[k] !== null)) {
if (alarm[key as keyof typeof alarm] !== parameters[key as keyof typeof parameters]) {
isCompliant = false
break
@ -144,8 +150,8 @@ export class CloudWatchAlarmSettingsCheck implements BPSet {
Threshold: parseFloat(requiredSettings['threshold']),
EvaluationPeriods: parseInt(requiredSettings['evaluation-periods'], 10),
Period: parseInt(requiredSettings['period'], 10),
ComparisonOperator: requiredSettings['comparison-operator'] as unknown,
Statistic: requiredSettings['statistic'] as unknown
ComparisonOperator: requiredSettings['comparison-operator'] as ComparisonOperator,
Statistic: requiredSettings['statistic'] as Statistic
})
)
}

View File

@ -125,9 +125,9 @@ export class CodeBuildProjectEnvironmentPrivilegedCheck implements BPSet {
new UpdateProjectCommand({
name: projectName,
environment: {
...projectToFix.environment,
...projectToFix.environment!,
privilegedMode: false
} as unknown
}
})
)
}

View File

@ -101,7 +101,7 @@ export class ECRPrivateLifecyclePolicyConfigured implements BPSet {
)
compliantResources.push(repository.repositoryArn!)
} catch (error: unknown) {
if (error.name === 'LifecyclePolicyNotFoundException') {
if ((error as Error).name === 'LifecyclePolicyNotFoundException') {
nonCompliantResources.push(repository.repositoryArn!)
} else {
throw error

View File

@ -5,7 +5,7 @@ import {
CreatePolicyVersionCommand,
DeletePolicyVersionCommand
} from '@aws-sdk/client-iam'
import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
import { BPSet, BPSetMetadata, BPSetStats, PolicyDocument } from '../../types'
import { Memorizer } from '../../Memorizer'
export class IAMPolicyNoStatementsWithFullAccess implements BPSet {
@ -88,9 +88,11 @@ export class IAMPolicyNoStatementsWithFullAccess implements BPSet {
})
)
const policyDocument = JSON.parse(decodeURIComponent(policyVersionResponse.PolicyVersion!.Document as string))
const policyDocument = JSON.parse(
decodeURIComponent(policyVersionResponse.PolicyVersion!.Document as string)
) as PolicyDocument
const hasFullAccess = policyDocument.Statement.some((statement: unknown) => {
const hasFullAccess = policyDocument.Statement.some((statement) => {
if (statement.Effect === 'Deny') return false
const actions = Array.isArray(statement.Action) ? statement.Action : [statement.Action]
return actions.some((action: string) => action.endsWith(':*'))
@ -143,9 +145,11 @@ export class IAMPolicyNoStatementsWithFullAccess implements BPSet {
})
)
const policyDocument = JSON.parse(decodeURIComponent(policyVersionResponse.PolicyVersion!.Document as string))
const policyDocument = JSON.parse(
decodeURIComponent(policyVersionResponse.PolicyVersion!.Document as string)
) as PolicyDocument
policyDocument.Statement = policyDocument.Statement.filter((statement: unknown) => {
policyDocument.Statement = policyDocument.Statement.filter((statement) => {
if (statement.Effect === 'Deny') return true
const actions = Array.isArray(statement.Action) ? statement.Action : [statement.Action]
return !actions.some((action: string) => action.endsWith(':*'))

View File

@ -1,5 +1,5 @@
import { LambdaClient, ListFunctionsCommand, GetPolicyCommand, RemovePermissionCommand } from '@aws-sdk/client-lambda'
import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
import { BPSet, BPSetMetadata, BPSetStats, PolicyDocument } from '../../types'
import { Memorizer } from '../../Memorizer'
export class LambdaFunctionPublicAccessProhibited implements BPSet {
@ -75,10 +75,10 @@ export class LambdaFunctionPublicAccessProhibited implements BPSet {
for (const func of functions) {
try {
const response = await this.memoClient.send(new GetPolicyCommand({ FunctionName: func.FunctionName! }))
const policy = JSON.parse(response.Policy!)
const policy = JSON.parse(response.Policy!) as PolicyDocument
const hasPublicAccess = policy.Statement.some(
(statement: unknown) => statement.Principal === '*' || statement.Principal?.AWS === '*'
(statement) => statement.Principal === '*' || statement.Principal?.AWS === '*'
)
if (hasPublicAccess) {
@ -87,7 +87,7 @@ export class LambdaFunctionPublicAccessProhibited implements BPSet {
compliantResources.push(func.FunctionArn!)
}
} catch (error) {
if ((error as unknown).name === 'ResourceNotFoundException') {
if ((error as Error).name === 'ResourceNotFoundException') {
compliantResources.push(func.FunctionArn!)
} else {
throw error
@ -132,7 +132,7 @@ export class LambdaFunctionPublicAccessProhibited implements BPSet {
}
}
} catch (error) {
if ((error as unknown).name !== 'ResourceNotFoundException') {
if ((error as Error).name !== 'ResourceNotFoundException') {
throw error
}
}

View File

@ -81,13 +81,10 @@ export class RDSClusterIAMAuthenticationEnabled implements BPSet {
this.stats.nonCompliantResources = nonCompliantResources
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
public readonly fix = async (nonCompliantResources: string[]) => {
this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
await this.fixImpl(nonCompliantResources)
.then(() => {
this.stats.status = 'FINISHED'
})

View File

@ -83,13 +83,10 @@ export class RDSClusterMultiAZEnabled implements BPSet {
this.stats.nonCompliantResources = nonCompliantResources
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
public readonly fix = async () => {
this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
await this.fixImpl()
.then(() => {
this.stats.status = 'FINISHED'
})

View File

@ -81,13 +81,10 @@ export class RDSInstancePublicAccessCheck implements BPSet {
this.stats.nonCompliantResources = nonCompliantResources
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
public readonly fix = async (nonCompliantResources: string[]) => {
this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
await this.fixImpl(nonCompliantResources)
.then(() => {
this.stats.status = 'FINISHED'
})

View File

@ -91,13 +91,10 @@ export class RDSLoggingEnabled implements BPSet {
this.stats.nonCompliantResources = nonCompliantResources
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
public readonly fix = async (nonCompliantResources: string[]) => {
this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
await this.fixImpl(nonCompliantResources)
.then(() => {
this.stats.status = 'FINISHED'
})

View File

@ -80,7 +80,7 @@ export class S3BucketDefaultLockEnabled implements BPSet {
await this.memoClient.send(new GetObjectLockConfigurationCommand({ Bucket: bucket.Name! }))
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} catch (error) {
if ((error as unknown).name === 'ObjectLockConfigurationNotFoundError') {
if ((error as Error).name === 'ObjectLockConfigurationNotFoundError') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
throw error
@ -92,13 +92,10 @@ export class S3BucketDefaultLockEnabled implements BPSet {
this.stats.nonCompliantResources = nonCompliantResources
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
public readonly fix = async (nonCompliantResources: string[]) => {
this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
await this.fixImpl(nonCompliantResources)
.then(() => {
this.stats.status = 'FINISHED'
})

View File

@ -96,13 +96,10 @@ export class S3BucketLevelPublicAccessProhibited implements BPSet {
this.stats.nonCompliantResources = nonCompliantResources
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
public readonly fix = async (nonCompliantResources: string[]) => {
this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
await this.fixImpl(nonCompliantResources)
.then(() => {
this.stats.status = 'FINISHED'
})

View File

@ -1,5 +1,5 @@
import { S3Client, ListBucketsCommand, GetBucketPolicyCommand, PutBucketPolicyCommand } from '@aws-sdk/client-s3'
import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
import { BPSet, BPSetMetadata, BPSetStats, PolicyDocument, Statement } from '../../types'
import { Memorizer } from '../../Memorizer'
export class S3BucketSSLRequestsOnly implements BPSet {
@ -71,10 +71,9 @@ export class S3BucketSSLRequestsOnly implements BPSet {
for (const bucket of buckets) {
try {
const response = await this.memoClient.send(new GetBucketPolicyCommand({ Bucket: bucket.Name! }))
const policy = JSON.parse(response.Policy!)
const policy = JSON.parse(response.Policy!) as PolicyDocument
const hasSSLCondition = policy.Statement.some(
(stmt: unknown) =>
stmt.Condition && stmt.Condition.Bool && stmt.Condition.Bool['aws:SecureTransport'] === 'false'
(stmt) => stmt.Condition && stmt.Condition.Bool && stmt.Condition.Bool['aws:SecureTransport'] === 'false'
)
if (hasSSLCondition) {
@ -83,7 +82,7 @@ export class S3BucketSSLRequestsOnly implements BPSet {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
}
} catch (error) {
if ((error as unknown).name === 'NoSuchBucketPolicy') {
if ((error as Error).name === 'NoSuchBucketPolicy') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
throw error
@ -95,13 +94,10 @@ export class S3BucketSSLRequestsOnly implements BPSet {
this.stats.nonCompliantResources = nonCompliantResources
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
public readonly fix = async (nonCompliantResources: string[]) => {
this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
await this.fixImpl(nonCompliantResources)
.then(() => {
this.stats.status = 'FINISHED'
})
@ -117,18 +113,18 @@ export class S3BucketSSLRequestsOnly implements BPSet {
private readonly fixImpl = async (nonCompliantResources: string[]) => {
for (const bucketArn of nonCompliantResources) {
const bucketName = bucketArn.split(':::')[1]!
let existingPolicy: unknown
let existingPolicy
try {
const response = await this.memoClient.send(new GetBucketPolicyCommand({ Bucket: bucketName }))
existingPolicy = JSON.parse(response.Policy!)
existingPolicy = JSON.parse(response.Policy!) as PolicyDocument
} catch (error) {
if ((error as unknown).name !== 'NoSuchBucketPolicy') {
if ((error as Error).name !== 'NoSuchBucketPolicy') {
throw error
}
}
const sslPolicyStatement = {
const sslPolicyStatement: Statement = {
Sid: 'DenyNonSSLRequests',
Effect: 'Deny',
Principal: '*',

View File

@ -87,13 +87,10 @@ export class S3BucketVersioningEnabled implements BPSet {
this.stats.nonCompliantResources = nonCompliantResources
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
public readonly fix = async (nonCompliantResources: string[]) => {
this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
await this.fixImpl(nonCompliantResources)
.then(() => {
this.stats.status = 'FINISHED'
})

View File

@ -96,7 +96,7 @@ export class S3DefaultEncryptionKMS implements BPSet {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
}
} catch (error) {
if ((error as unknown).name === 'ServerSideEncryptionConfigurationNotFoundError') {
if ((error as Error).name === 'ServerSideEncryptionConfigurationNotFoundError') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
throw error

View File

@ -2,7 +2,8 @@ import {
S3Client,
ListBucketsCommand,
GetBucketNotificationConfigurationCommand,
PutBucketNotificationConfigurationCommand
PutBucketNotificationConfigurationCommand,
Event
} from '@aws-sdk/client-s3'
import { BPSet, BPSetMetadata, BPSetStats } from '../../types'
import { Memorizer } from '../../Memorizer'
@ -141,7 +142,7 @@ export class S3EventNotificationsEnabled implements BPSet {
LambdaFunctionConfigurations: [
{
LambdaFunctionArn: lambdaArn,
Events: [eventType as unknown]
Events: [eventType] as Event[]
}
]
}

View File

@ -92,7 +92,7 @@ export class S3LifecyclePolicyCheck implements BPSet {
await this.memoClient.send(new GetBucketLifecycleConfigurationCommand({ Bucket: bucket.Name! }))
compliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} catch (error) {
if ((error as unknown).name === 'NoSuchLifecycleConfiguration') {
if ((error as Error).name === 'NoSuchLifecycleConfiguration') {
nonCompliantResources.push(`arn:aws:s3:::${bucket.Name!}`)
} else {
throw error

View File

@ -81,13 +81,10 @@ export class SecretsManagerRotationEnabledCheck implements BPSet {
this.stats.nonCompliantResources = nonCompliantResources
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
public readonly fix = async (nonCompliantResources: string[]) => {
this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
await this.fixImpl(nonCompliantResources)
.then(() => {
this.stats.status = 'FINISHED'
})

View File

@ -93,13 +93,10 @@ export class SecretsManagerScheduledRotationSuccessCheck implements BPSet {
this.stats.nonCompliantResources = nonCompliantResources
}
public readonly fix = async (
nonCompliantResources: string[],
requiredParametersForFix: { name: string; value: string }[]
) => {
public readonly fix = async (nonCompliantResources: string[]) => {
this.stats.status = 'CHECKING'
await this.fixImpl(nonCompliantResources, requiredParametersForFix)
await this.fixImpl(nonCompliantResources)
.then(() => {
this.stats.status = 'FINISHED'
})

View File

@ -76,7 +76,7 @@ export class SecurityHubEnabled implements BPSet {
await this.memoSecurityHubClient.send(new DescribeHubCommand({}))
compliantResources.push(awsAccountId)
} catch (error: unknown) {
if (error.name === 'InvalidAccessException') {
if ((error as Error).name === 'InvalidAccessException') {
nonCompliantResources.push(awsAccountId)
} else {
throw error

View File

@ -80,7 +80,7 @@ export class SNSEncryptedKMS implements BPSet {
const topics = await this.getTopics()
for (const topic of topics) {
if ((topic as unknown).KmsMasterKeyId) {
if (topic.KmsMasterKeyId) {
compliantResources.push(topic.TopicArn!)
} else {
nonCompliantResources.push(topic.TopicArn!)
@ -135,7 +135,7 @@ export class SNSEncryptedKMS implements BPSet {
const topicsResponse = await this.memoClient.send(new ListTopicsCommand({}))
const topics = topicsResponse.Topics || []
const topicDetails = []
const topicDetails: Record<string, string>[] = []
for (const topic of topics) {
const attributes = await this.memoClient.send(new GetTopicAttributesCommand({ TopicArn: topic.TopicArn! }))
topicDetails.push({ ...attributes.Attributes, TopicArn: topic.TopicArn! })

21
src/types.d.ts vendored
View File

@ -57,3 +57,24 @@ export interface BPSetStats {
message: string
}[]
}
interface PolicyDocument {
Version: '2012-10-17' | '2008-10-17' // Only valid AWS IAM policy versions
Statement: Statement[]
}
interface Statement {
Sid?: string // Optional statement ID
Effect: 'Allow' | 'Deny' // Effect of the statement
Action: string | string[] // Action(s) the statement applies to
Resource: string | string[] // Resource(s) the statement applies to
Principal?:
| '*'
| {
[key: string]: string | string[]
} // Optional principal(s) the statement applies to, can be "*"
Condition?: {
[key: string]: {
[operator: string]: string | string[]
}
} // Optional conditions for the statement
}