packages/constructs/L2/ec2-constructs/lib/securitygroup.ts (242 lines of code) (raw):

/*! * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ import { MdaaConstructProps, MdaaParamAndOutput } from '@aws-mdaa/construct'; //NOSONAR import { Token } from 'aws-cdk-lib'; import { CfnSecurityGroupEgress, CfnSecurityGroupIngress, IPeer, IVpc, Peer, Port, PortProps, Protocol, SecurityGroup, SecurityGroupProps, } from 'aws-cdk-lib/aws-ec2'; import { MdaaNagSuppressions } from '@aws-mdaa/construct'; //NOSONAR import { Construct } from 'constructs'; import { NagPackSuppression } from 'cdk-nag'; export interface MdaaSecurityGroupRuleProps { readonly ipv4?: MdaaCidrPeer[]; readonly sg?: MdaaSecurityGroupPeer[]; readonly prefixList?: MdaaPrefixListPeer[]; } export interface MdaaPeer { readonly port?: number; readonly toPort?: number; readonly protocol: string; readonly description?: string; readonly suppressions?: NagPackSuppression[]; } export interface MdaaPrefixListPeer extends MdaaPeer { readonly prefixList: string; } export interface MdaaCidrPeer extends MdaaPeer { readonly cidr: string; } export interface MdaaSecurityGroupPeer extends MdaaPeer { readonly sgId: string; } export interface MdaaSecurityGroupProps extends MdaaConstructProps { /** * The name of the security group. For valid values, see the GroupName * parameter of the CreateSecurityGroup action in the Amazon EC2 API * Reference. * * It is not recommended to use an explicit group name. * * @default If you don't specify a GroupName, AWS CloudFormation generates a * unique physical ID and uses that ID for the group name. */ readonly securityGroupName?: string; /** * A description of the security group. * * @default The default name will be the construct's CDK path. */ readonly description?: string; /** * The VPC in which to create the security group. */ readonly vpc: IVpc; /** * Whether to allow all outbound traffic by default. * * If this is set to true, there will only be a single egress rule which allows all * outbound traffic. If this is set to false, no outbound traffic will be allowed by * default and all egress traffic must be explicitly authorized. * * To allow all ipv6 traffic use allowAllIpv6Outbound * * @default false */ readonly allowAllOutbound?: boolean; /** * Whether to allow all outbound ipv6 traffic by default. * * If this is set to true, there will only be a single egress rule which allows all * outbound ipv6 traffic. If this is set to false, no outbound traffic will be allowed by * default and all egress ipv6 traffic must be explicitly authorized. * * To allow all ipv4 traffic use allowAllOutbound * * @default false */ readonly allowAllIpv6Outbound?: boolean; /** * Whether to disable inline ingress and egress rule optimization. * * If this is set to true, ingress and egress rules will not be declared under the * SecurityGroup in cloudformation, but will be separate elements. * * Inlining rules is an optimization for producing smaller stack templates. Sometimes * this is not desirable, for example when security group access is managed via tags. * * The default value can be overriden globally by setting the context variable * '@aws-cdk/aws-ec2.securityGroupDisableInlineRules'. * * @default false */ readonly disableInlineRules?: boolean; readonly ingressRules?: MdaaSecurityGroupRuleProps; readonly egressRules?: MdaaSecurityGroupRuleProps; readonly addSelfReferenceRule?: boolean; } /** * MDAA L2 Security Group. Provides a simplified interface for SG rules creation */ export class MdaaSecurityGroup extends SecurityGroup { private static setProps(props: MdaaSecurityGroupProps): SecurityGroupProps { const overrideProps = { //Invert the default for allowAllOutbound allowAllOutbound: props.allowAllOutbound ?? false, securityGroupName: props.naming.resourceName(props.securityGroupName), }; return { ...props, ...overrideProps }; } constructor(scope: Construct, id: string, props: MdaaSecurityGroupProps) { super(scope, id, MdaaSecurityGroup.setProps(props)); // Add Ingress rules props.ingressRules?.ipv4?.forEach(rule => { const peer = Peer.ipv4(rule.cidr); this.addSuppressableIngressRule( peer, MdaaSecurityGroup.resolvePeerToPort(rule), rule.description, false, rule.suppressions, ); }); props.ingressRules?.sg?.forEach(rule => { const peer = Peer.securityGroupId(rule.sgId); this.addSuppressableIngressRule( peer, MdaaSecurityGroup.resolvePeerToPort(rule), rule.description, false, rule.suppressions, ); }); props.ingressRules?.prefixList?.forEach(rule => { const peer = Peer.prefixList(rule.prefixList); this.addSuppressableIngressRule( peer, MdaaSecurityGroup.resolvePeerToPort(rule), rule.description, false, rule.suppressions, ); }); // Add Egress rules props.egressRules?.ipv4?.forEach(rule => { const peer = Peer.ipv4(rule.cidr); this.addSuppressableEgressRule( peer, MdaaSecurityGroup.resolvePeerToPort(rule), rule.description, false, rule.suppressions, ); }); props.egressRules?.sg?.forEach(rule => { const peer = Peer.securityGroupId(rule.sgId); this.addSuppressableEgressRule( peer, MdaaSecurityGroup.resolvePeerToPort(rule), rule.description, false, rule.suppressions, ); }); props.egressRules?.prefixList?.forEach(rule => { const peer = Peer.prefixList(rule.prefixList); this.addSuppressableEgressRule( peer, MdaaSecurityGroup.resolvePeerToPort(rule), rule.description, false, rule.suppressions, ); }); // Allow all tcp connections from the same security group if (props.addSelfReferenceRule != undefined && props.addSelfReferenceRule) { const suppressions = [ { id: 'NIST.800.53.R5-EC2RestrictedCommonPorts', reason: 'Ingress/Egress is limited to this security group', }, { id: 'HIPAA.Security-EC2RestrictedCommonPorts', reason: 'Ingress/Egress is limited to this security group', }, { id: 'PCI.DSS.321-EC2RestrictedCommonPorts', reason: 'Ingress/Egress is limited to this security group', }, ]; this.addSuppressableIngressRule(this, Port.allTraffic(), `Self-Ref`, false, suppressions); //Only add self ref egress rule if all outbound traffic is not otherwise allowed if (!props.allowAllOutbound) { this.addSuppressableEgressRule(this, Port.allTraffic(), `Self-Ref`, false, suppressions); } } new MdaaParamAndOutput( this, { naming: props.naming, resourceType: 'security-group', resourceId: props.securityGroupName, name: 'id', value: this.securityGroupId, }, scope, ); } public addSuppressableIngressRule( peer: IPeer, connection: Port, description?: string, remoteRule?: boolean, suppressions?: NagPackSuppression[], ) { if (description === undefined) { description = `from ${peer.uniqueId}:${connection}`; } const { scope, id } = this.determineRuleScope(peer, connection, 'from', remoteRule); // Skip duplicates if (scope.node.tryFindChild(id) === undefined) { const ingress = new CfnSecurityGroupIngress(scope, id, { groupId: this.securityGroupId, ...peer.toIngressRuleConfig(), ...connection.toRuleJson(), description, }); if (suppressions) { MdaaNagSuppressions.addConfigResourceSuppressions(ingress, suppressions, true); } } } public addSuppressableEgressRule( peer: IPeer, connection: Port, description?: string, remoteRule?: boolean, suppressions?: NagPackSuppression[], ) { if (description === undefined) { description = `to ${peer.uniqueId}:${connection}`; } const { scope, id } = this.determineRuleScope(peer, connection, 'to', remoteRule); // Skip duplicates if (scope.node.tryFindChild(id) === undefined) { const egress = new CfnSecurityGroupEgress(scope, id, { groupId: this.securityGroupId, ...peer.toEgressRuleConfig(), ...connection.toRuleJson(), description, }); if (suppressions) { MdaaNagSuppressions.addConfigResourceSuppressions(egress, suppressions, true); } } } public static resolvePeerToPort(peer: MdaaPeer): Port { const protocol: Protocol = Protocol[peer.protocol.toUpperCase() as keyof typeof Protocol]; if (typeof protocol === undefined || protocol == undefined) { throw new Error(`Unknown protocol defined: ${peer.protocol}`); } const fromPort = peer.port; const toPort = peer.toPort || fromPort; let stringRepresentation = `${protocol.toString()}`; if (protocol == Protocol.ALL) { stringRepresentation = `${stringRepresentation} ALL TRAFFIC`; } else { if (fromPort && toPort) { if (toPort == fromPort) { stringRepresentation = `${stringRepresentation} PORT ${this.renderPort(fromPort)}`; } else { stringRepresentation = `${stringRepresentation} RANGE ${this.renderPort(fromPort)}-${this.renderPort( toPort, )}`; } } else { throw new Error("Port must be specified if protocol is not 'ALL'"); } } const portProps: PortProps = { protocol: protocol, fromPort: fromPort, toPort: toPort, stringRepresentation: stringRepresentation, }; return new Port(portProps); } public static renderPort(port: number) { return Token.isUnresolved(port) ? '{IndirectPort}' : port.toString(); } public static mergeRules( rules1: MdaaSecurityGroupRuleProps, rules2: MdaaSecurityGroupRuleProps, ): MdaaSecurityGroupRuleProps { return { sg: [...(rules1.sg || []), ...(rules2.sg || [])], ipv4: [...(rules1.ipv4 || []), ...(rules2.ipv4 || [])], prefixList: [...(rules1.prefixList || []), ...(rules2.prefixList || [])], }; } }