From 7c76c408d604429169af1df1db7bbabc120f32d2 Mon Sep 17 00:00:00 2001 From: ashley-lowde Date: Mon, 8 May 2023 13:28:30 +0930 Subject: [PATCH 1/2] Add more detailed listeners, rules, actions to AWS ELBv2 load balancers. Record rules, conditions and actions in the form (ELBV2Listener)-[ELBV2_LISTENER_RULE]-(ELBV2Action). This allows us to analyze the path of requests that are intelligently routed based on HTTP request parameters by AWS Application Load Balancers. --- .../aws_ingest_load_balancers_v2_cleanup.json | 72 ++-- .../intel/aws/ec2/load_balancer_v2s.py | 322 +++++++++++++++++- docs/root/modules/aws/schema.md | 129 ++++++- 3 files changed, 465 insertions(+), 58 deletions(-) diff --git a/cartography/data/jobs/cleanup/aws_ingest_load_balancers_v2_cleanup.json b/cartography/data/jobs/cleanup/aws_ingest_load_balancers_v2_cleanup.json index d278cb047..c2873a65d 100644 --- a/cartography/data/jobs/cleanup/aws_ingest_load_balancers_v2_cleanup.json +++ b/cartography/data/jobs/cleanup/aws_ingest_load_balancers_v2_cleanup.json @@ -1,33 +1,45 @@ { - "statements": [{ - "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(n:LoadBalancerV2) WHERE n.lastupdated <> $UPDATE_TAG WITH n LIMIT $LIMIT_SIZE DETACH DELETE (n)", - "iterative": true, - "iterationsize": 100 - }, - { - "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(:LoadBalancerV2)-[r:SUBNET]->(:EC2Subnet) WHERE r.lastupdated <> $UPDATE_TAG WITH r LIMIT $LIMIT_SIZE DELETE (r)", - "iterative": true, - "iterationsize": 100 - }, - { - "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(:LoadBalancerV2)-[r:MEMBER_OF_EC2_SECURITY_GROUP]->(:EC2SecurityGroup) WHERE r.lastupdated <> $UPDATE_TAG WITH r LIMIT $LIMIT_SIZE DELETE (r)", - "iterative": true, - "iterationsize": 100 - }, - { - "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(:LoadBalancerV2)-[r:EXPOSE]->(:EC2Instance) WHERE r.lastupdated <> $UPDATE_TAG WITH r LIMIT $LIMIT_SIZE DELETE (r)", - "iterative": true, - "iterationsize": 100 - }, - { - "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(:LoadBalancerV2)-[:ELBV2_LISTENER]->(n:ELBV2Listener) WHERE n.lastupdated <> $UPDATE_TAG WITH n LIMIT $LIMIT_SIZE DETACH DELETE (n)", - "iterative": true, - "iterationsize": 100 - }, - { - "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(:LoadBalancerV2)-[r:ELBV2_LISTENER]->(:ELBV2Listener) WHERE r.lastupdated <> $UPDATE_TAG WITH r LIMIT $LIMIT_SIZE DELETE (r)", - "iterative": true, - "iterationsize": 100 - }], + "statements": [ + { + "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(n:LoadBalancerV2) WHERE n.lastupdated <> $UPDATE_TAG WITH n LIMIT $LIMIT_SIZE DETACH DELETE (n)", + "iterative": true, + "iterationsize": 100 + }, + { + "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(:LoadBalancerV2)-[r:SUBNET]->(:EC2Subnet) WHERE r.lastupdated <> $UPDATE_TAG WITH r LIMIT $LIMIT_SIZE DELETE (r)", + "iterative": true, + "iterationsize": 100 + }, + { + "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(:LoadBalancerV2)-[r:MEMBER_OF_EC2_SECURITY_GROUP]->(:EC2SecurityGroup) WHERE r.lastupdated <> $UPDATE_TAG WITH r LIMIT $LIMIT_SIZE DELETE (r)", + "iterative": true, + "iterationsize": 100 + }, + { + "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(:LoadBalancerV2)-[r:EXPOSE]->(:EC2Instance) WHERE r.lastupdated <> $UPDATE_TAG WITH r LIMIT $LIMIT_SIZE DELETE (r)", + "iterative": true, + "iterationsize": 100 + }, + { + "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(:LoadBalancerV2)-[:ELBV2_LISTENER]->(:ELBV2Listener)<-[:ELBV2_LISTENER_RULE]-(n:ELBV2Action) WHERE n.lastupdated <> $UPDATE_TAG WITH n LIMIT $LIMIT_SIZE DETACH DELETE (n)", + "iterative": true, + "iterationsize": 100 + }, + { + "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(:LoadBalancerV2)-[:ELBV2_LISTENER]->(:ELBV2Listener)<-[:ATTACHED_TO]-(n:ELBV2Rule) WHERE n.lastupdated <> $UPDATE_TAG WITH n LIMIT $LIMIT_SIZE DETACH DELETE (n)", + "iterative": true, + "iterationsize": 100 + }, + { + "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(:LoadBalancerV2)-[:ELBV2_LISTENER]->(n:ELBV2Listener) WHERE n.lastupdated <> $UPDATE_TAG WITH n LIMIT $LIMIT_SIZE DETACH DELETE (n)", + "iterative": true, + "iterationsize": 100 + }, + { + "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(:LoadBalancerV2)-[r:ELBV2_LISTENER]->(:ELBV2Listener) WHERE r.lastupdated <> $UPDATE_TAG WITH r LIMIT $LIMIT_SIZE DELETE (r)", + "iterative": true, + "iterationsize": 100 + } + ], "name": "cleanup LoadBalancerV2" } diff --git a/cartography/intel/aws/ec2/load_balancer_v2s.py b/cartography/intel/aws/ec2/load_balancer_v2s.py index a29e1634a..1b20831e7 100644 --- a/cartography/intel/aws/ec2/load_balancer_v2s.py +++ b/cartography/intel/aws/ec2/load_balancer_v2s.py @@ -22,9 +22,30 @@ def get_load_balancer_v2_listeners(client: botocore.client.BaseClient, load_bala for page in paginator.paginate(LoadBalancerArn=load_balancer_arn): listeners.extend(page['Listeners']) + # Technically listeners don't have names, but the AWS console titles them as 'protocol':'port' so we'll do the same. + for listener in listeners: + listener['name'] = listener['Protocol'] + ":" + str(listener['Port']) return listeners +@timeit +@aws_handle_regions +def get_load_balancer_v2_rules(client: botocore.client.BaseClient, listener_arn: str) -> List[Dict]: + paginator = client.get_paginator('describe_rules') + rules: List[Dict] = [] + for page in paginator.paginate(ListenerArn=listener_arn): + rules.extend(page['Rules']) + + # Enrich rule data with plain text summaries of any conditions and a unique ID for each action. + # Add a human-friendly name to actions for convenient display. + for i, rule in enumerate(rules): + rules[i]['ConditionStrings'] = rule_conditions_to_strings(rule['Conditions']) + for j, action in enumerate(rule['Actions']): + rules[i]['Actions'][j]['id'] = f"{rule['RuleArn']}-action-{action.setdefault('Order', 'default')}" + rules[i]['Actions'][j]['name'] = f"action-{action.setdefault('Order', 'default')}" + return rules + + @timeit def get_load_balancer_v2_target_groups(client: botocore.client.BaseClient, load_balancer_arn: str) -> List[Dict]: paginator = client.get_paginator('describe_target_groups') @@ -42,6 +63,23 @@ def get_load_balancer_v2_target_groups(client: botocore.client.BaseClient, load_ return target_groups +@timeit +def get_all_target_groups(client: botocore.client.BaseClient) -> List[Dict]: + paginator = client.get_paginator('describe_target_groups') + target_groups: List[Dict] = [] + for page in paginator.paginate(): + target_groups.extend(page['TargetGroups']) + + # Add instance data + for target_group in target_groups: + target_group['Targets'] = [] + target_health = client.describe_target_health(TargetGroupArn=target_group['TargetGroupArn']) + for target_health_description in target_health['TargetHealthDescriptions']: + target_group['Targets'].append(target_health_description['Target']['Id']) + + return target_groups + + @timeit @aws_handle_regions def get_loadbalancer_v2_data(boto3_session: boto3.Session, region: str) -> List[Dict]: @@ -51,10 +89,25 @@ def get_loadbalancer_v2_data(boto3_session: boto3.Session, region: str) -> List[ for page in paginator.paginate(): elbv2s.extend(page['LoadBalancers']) - # Make extra calls to get listeners + # Create a dict of load balancers keyed by ARN to their position in the list so that we can do a quick lookup + # rather than repeatedly looping over the list to find load balancers when assigning target groups + arn_list_index: Dict[str, int] = {elbv2s[i]['LoadBalancerArn']: i for i in range(0, len(elbv2s))} + + # Get all target groups in a single (paged) call and associate using the map to avoid making an API call for every + # load balancer. + for tg in get_all_target_groups(client): + for lb_arn in tg['LoadBalancerArns']: + if lb_arn in arn_list_index: + if 'TargetGroups' in elbv2s[arn_list_index[lb_arn]]: + elbv2s[arn_list_index[lb_arn]]['TargetGroups'].append(tg) + else: + elbv2s[arn_list_index[lb_arn]]['TargetGroups'] = [tg] + + # Make extra calls to get listeners, rules for elbv2 in elbv2s: elbv2['Listeners'] = get_load_balancer_v2_listeners(client, elbv2['LoadBalancerArn']) - elbv2['TargetGroups'] = get_load_balancer_v2_target_groups(client, elbv2['LoadBalancerArn']) + for listener in elbv2['Listeners']: + listener['Rules'] = get_load_balancer_v2_rules(client, listener['ListenerArn']) return elbv2s @@ -68,7 +121,7 @@ def load_load_balancer_v2s( ON CREATE SET elbv2.firstseen = timestamp(), elbv2.createdtime = $CREATED_TIME SET elbv2.lastupdated = $update_tag, elbv2.name = $NAME, elbv2.dnsname = $DNS_NAME, elbv2.canonicalhostedzonenameid = $HOSTED_ZONE_NAME_ID, - elbv2.type = $ELBv2_TYPE, + elbv2.type = $ELBv2_TYPE, elbv2.arn = $ARN, elbv2.scheme = $SCHEME, elbv2.region = $Region WITH elbv2 MATCH (aa:AWSAccount{id: $AWS_ACCOUNT_ID}) @@ -86,6 +139,7 @@ def load_load_balancer_v2s( NAME=lb["LoadBalancerName"], DNS_NAME=load_balancer_id, HOSTED_ZONE_NAME_ID=lb.get("CanonicalHostedZoneNameID"), + ARN=lb["LoadBalancerArn"], ELBv2_TYPE=lb.get("Type"), SCHEME=lb.get("Scheme"), AWS_ACCOUNT_ID=current_aws_account_id, @@ -114,20 +168,33 @@ def load_load_balancer_v2s( update_tag=update_tag, ) - if lb['Listeners']: - load_load_balancer_v2_listeners(neo4j_session, load_balancer_id, lb['Listeners'], update_tag) - + # Add an EXPOSE relationship to resources linked to the Load Balancer via a Target Group if lb['TargetGroups']: load_load_balancer_v2_target_groups( neo4j_session, load_balancer_id, lb['TargetGroups'], current_aws_account_id, update_tag, ) + if lb['Listeners']: + load_load_balancer_v2_listeners(neo4j_session, load_balancer_id, lb['Listeners'], update_tag) + for listener in lb['Listeners']: + load_load_balancer_v2_listener_rules( + neo4j_session, listener['ListenerArn'], listener['Rules'], update_tag, + ) + for rule in listener['Rules']: + load_load_balancer_v2_actions( + neo4j_session, rule['RuleArn'], rule['Actions'], update_tag, + ) + # Add TARGET_GROUP_TARGET relationships to resources for traffic flow analysis + load_load_balancer_v2_target_group_targets( + neo4j_session, rule, lb, current_aws_account_id, update_tag, + ) + @timeit def load_load_balancer_v2_subnets( - neo4j_session: neo4j.Session, load_balancer_id: str, az_data: List[Dict], - region: str, update_tag: int, + neo4j_session: neo4j.Session, load_balancer_id: str, az_data: List[Dict], + region: str, update_tag: int, ) -> None: ingest_load_balancer_subnet = """ MATCH (elbv2:LoadBalancerV2{id: $ID}) @@ -181,10 +248,98 @@ def load_load_balancer_v2_target_groups( ) +@timeit +def load_load_balancer_v2_target_group_targets( + neo4j_session: neo4j.Session, rule_data: Dict, lb_data: Dict, current_aws_account_id: str, update_tag: int +) -> None: + """Create target group relationships from ELBV2Actions to targets""" + + ingest_target_group_relationships = { + "instance": """ + MATCH (action:ELBV2ForwardAction{id: $ActionId}) + WITH action + UNWIND $Targets as target + MATCH (instance:EC2Instance{instanceid: target}) + MERGE (action)-[r:TARGET_GROUP_TARGET{weight: $WEIGHT}]->(instance) + ON CREATE SET r.firstseen = timestamp() + SET r.lastupdated = $update_tag, + r.target_group_name = $TargetGroupName + WITH instance + MATCH (aa:AWSAccount{id: $AWS_ACCOUNT_ID}) + MERGE (aa)-[r:RESOURCE]->(instance) + ON CREATE SET r.firstseen = timestamp() + SET r.lastupdated = $update_tag + """, + "ip": """ + MATCH (action:ELBV2ForwardAction{id: $ActionId}) + WITH action + UNWIND $Targets as target + MATCH (ip:Ip{id: target}) + MERGE (action)-[r:TARGET_GROUP_TARGET{weight: $WEIGHT}]->(ip) + ON CREATE SET r.firstseen = timestamp() + SET r.lastupdated = $update_tag, + r.target_group_name = $TargetGroupName + """, + "lambda": """ + MATCH (action:ELBV2ForwardAction{id: $ActionId}) + WITH action + UNWIND $Targets as target + MATCH (lambda:AWSLambda{id: target}) + MERGE (action)-[r:TARGET_GROUP_TARGET{weight: $WEIGHT}]->(lambda) + ON CREATE SET r.firstseen = timestamp() + SET r.lastupdated = $update_tag, + r.target_group_name = $TargetGroupName + WITH lambda + MATCH (aa:AWSAccount{id: $AWS_ACCOUNT_ID}) + MERGE (aa)-[r:RESOURCE]->(lambda) + ON CREATE SET r.firstseen = timestamp() + SET r.lastupdated = $update_tag + """, + "alb": """ + MATCH (action:ELBV2ForwardAction{id: $ActionId}) + WITH action + UNWIND $Targets as target + MATCH (lb:LoadBalancerV2{arn: target}) + MERGE (action)-[r:TARGET_GROUP_TARGET{weight: $WEIGHT}]->(lb) + ON CREATE SET r.firstseen = timestamp() + SET r.lastupdated = $update_tag, + r.target_group_name = $TargetGroupName + WITH lb + MATCH (aa:AWSAccount{id: $AWS_ACCOUNT_ID}) + MERGE (aa)-[r:RESOURCE]->(lb) + ON CREATE SET r.firstseen = timestamp() + SET r.lastupdated = $update_tag + """ + } + + for action in rule_data['Actions']: + # Only 'forward' type actions have links to target groups so others can be skipped + if action['Type'] != "forward": + continue + for forwarder_target_group in action['ForwardConfig']['TargetGroups']: + # Find the actual target group info in the load balancer data by iterating over the general load balancer + # data dict until we find a matching ARN. + for target_group in lb_data['TargetGroups']: + if target_group['TargetGroupArn'] == forwarder_target_group['TargetGroupArn']: + # Then add relationships to the appropriate objects using the TargetType to select one of the above + # queries. + neo4j_session.run( + ingest_target_group_relationships[target_group['TargetType']], + ActionId=f"{rule_data['RuleArn']}-action-{action.get('Order', 'default')}", + Targets=target_group['Targets'], + WEIGHT=forwarder_target_group.get('Weight', 0), + TargetGroupName=target_group['TargetGroupName'], + AWS_ACCOUNT_ID=current_aws_account_id, + update_tag=update_tag, + ) + break + return + + @timeit def load_load_balancer_v2_listeners( - neo4j_session: neo4j.Session, load_balancer_id: str, listener_data: List[Dict], - update_tag: int, + neo4j_session: neo4j.Session, load_balancer_id: str, listener_data: List[Dict], + update_tag: int, ) -> None: ingest_listener = """ MATCH (elbv2:LoadBalancerV2{id: $LoadBalancerId}) @@ -192,10 +347,11 @@ def load_load_balancer_v2_listeners( UNWIND $Listeners as data MERGE (l:Endpoint:ELBV2Listener{id: data.ListenerArn}) ON CREATE SET l.port = data.Port, l.protocol = data.Protocol, - l.firstseen = timestamp(), - l.targetgrouparn = data.TargetGroupArn + l.firstseen = timestamp(), + l.targetgrouparn = data.TargetGroupArn SET l.lastupdated = $update_tag, - l.ssl_policy = data.SslPolicy + l.ssl_policy = data.SslPolicy, + l.name = data.name WITH l, elbv2 MERGE (elbv2)-[r:ELBV2_LISTENER]->(l) ON CREATE SET r.firstseen = timestamp() @@ -209,6 +365,146 @@ def load_load_balancer_v2_listeners( ) +def rule_conditions_to_strings(conditions: List[Dict]) -> Dict: + result = {} + for cnd in conditions: + if cnd['Field'] == 'http-header': + result['http_header'] = f"{cnd['HttpHeaderConfig']['HttpHeaderName']}==[sensitive]" + elif cnd['Field'] == 'http-request-method': + result['http_request_method'] = '|'.join(cnd['HttpRequestMethodConfig']['Values']) + elif cnd['Field'] == 'host-header': + result['host_header'] = '|'.join(cnd['HostHeaderConfig']['Values']) + elif cnd['Field'] == 'path-pattern': + result['path_pattern'] = '|'.join(cnd['PathPatternConfig']['Values']) + elif cnd['Field'] == 'query-string': + result['query_string'] = '|'.join([f"{k}={v}" for k, v in cnd['QueryStringConfig']['Values'].items()]) + elif cnd['Field'] == 'source-ip': + result['source_ip'] = '|'.join(cnd['SourceIpConfig']['Values']) + return result + + +def action_ingest_statement(type: str, additional: str) -> str: + statement = """ + MATCH (rule:ELBV2Rule{id: $RuleId})-[ATTACHED_TO]->(listener:ELBV2Listener) + WITH listener, rule + UNWIND $Actions as action + MERGE (listener)-[rr:ELBV2_LISTENER_RULE]->(actionNode:ELBV2Action:""" + type + """{id: action.id}) + ON CREATE SET actionNode.firstseen = timestamp(), + rr.firstseen = timestamp() + SET actionNode.lastupdated = $update_tag, + actionNode.name = action.name, + // Copy rule properties to the relationship for simplified queries + rr.priority = rule.priority, + rr.http_header_condition = rule.http_header_condition, + rr.http_request_method_condition = rule.http_request_method_condition, + rr.host_header_condition = rule.host_header_condition, + rr.path_pattern_condition = rule.path_pattern_condition, + rr.query_string_condition = rule.query_string_condition, + rr.source_ip_condition = rule.source_ip_condition, + rr.lastupdated = $update_tag""" + if additional != "": + statement += f",\n{additional}" + statement += """ + WITH rule, actionNode + MERGE (rule)<-[r2:ATTACHED_TO]-(actionNode) + ON CREATE SET r2.firstseen = timestamp(), + r2.firstseen = timestamp() + """ + return statement + + +def load_load_balancer_v2_actions( + neo4j_session: neo4j.Session, rule_arn: str, action_data: List[Dict], update_tag: int, +) -> None: + oidc_additional_statements = """ + actionNode.on_unauthenticated_request = action.AuthenticateOidcActionConfig.OnUnauthenticatedRequest + """ + cognito_additional_statements = """ + actionNode.on_unauthenticated_request = action.AuthenticateCognitoActionConfig.OnUnauthenticatedRequest, + actionNode.user_pool_arn = action.AuthenticateCognitoActionConfig.UserPoolArn + """ + redirect_additional_statements = """ + actionNode.protocol = action.RedirectConfig.Protocol, + actionNode.port = action.RedirectConfig.Port, + actionNode.host = action.RedirectConfig.Host, + actionNode.path = action.RedirectConfig.Path, + actionNode.query = action.RedirectConfig.Query, + actionNode.status_code = action.RedirectConfig.StatusCode + """ + fixed_response_additional_statements = """ + actionNode.status_code = action.FixedResponseActionConfig.StatusCode, + actionNode.message_body = action.FixedResponseActionConfig.MessageBody, + actionNode.content_type = action.FixedResponseActionConfig.ContentType + """ + + # TODO: Clean this up by sorting actions by type only once, DRYing neo4j calls + + neo4j_session.run( + action_ingest_statement('ELBV2ForwardAction', ""), + RuleId=rule_arn, + Actions=[action for action in action_data if action['Type'] == "forward"], + update_tag=update_tag, + ) + neo4j_session.run( + action_ingest_statement('ELBV2AuthenticateOIDCAction', oidc_additional_statements), + RuleId=rule_arn, + Actions=[action for action in action_data if action['Type'] == "authenticate-oidc"], + update_tag=update_tag, + ) + neo4j_session.run( + action_ingest_statement('ELBV2AuthenticateCognitoAction', cognito_additional_statements), + RuleId=rule_arn, + Actions=[action for action in action_data if action['Type'] == "authenticate-cognito"], + update_tag=update_tag, + ) + neo4j_session.run( + action_ingest_statement('ELBV2RedirectAction', redirect_additional_statements), + RuleId=rule_arn, + Actions=[action for action in action_data if action['Type'] == "redirect"], + update_tag=update_tag, + ) + neo4j_session.run( + action_ingest_statement('ELBV2FixedResponseAction', fixed_response_additional_statements), + RuleId=rule_arn, + Actions=[action for action in action_data if action['Type'] == "fixed-response"], + update_tag=update_tag, + ) + return + + +@timeit +def load_load_balancer_v2_listener_rules( + neo4j_session: neo4j.Session, listener_arn: str, rule_data: List[Dict], + update_tag: int, +) -> None: + ingest_rule = """ + MATCH (listener:Endpoint:ELBV2Listener{id: $ListenerARN}) + WITH listener + UNWIND $Rules as data + MERGE (rule:ELBV2Rule{id: data.RuleArn}) + ON CREATE SET rule.firstseen = timestamp() + SET rule.lastupdated = $update_tag, + rule.priority = data.Priority, + rule.http_header_condition = data.ConditionStrings.http_header, + rule.http_request_method_condition = data.ConditionStrings.http_request_method, + rule.host_header_condition = data.ConditionStrings.host_header, + rule.path_pattern_condition = data.ConditionStrings.path_pattern, + rule.query_string_condition = data.ConditionStrings.query_string, + rule.source_ip_condition = data.ConditionStrings.source_ip + WITH rule, listener + MERGE (listener)<-[r:ATTACHED_TO]-(rule) + ON CREATE SET r.firstseen = timestamp() + SET r.lastupdated = $update_tag + """ + + neo4j_session.run( + ingest_rule, + ListenerARN=listener_arn, + Rules=rule_data, + update_tag=update_tag, + ) + + @timeit def cleanup_load_balancer_v2s(neo4j_session: neo4j.Session, common_job_parameters: Dict) -> None: """Delete elbv2's and dependent resources in the DB without the most recent lastupdated tag.""" diff --git a/docs/root/modules/aws/schema.md b/docs/root/modules/aws/schema.md index 3c0c4baae..0410ae31d 100644 --- a/docs/root/modules/aws/schema.md +++ b/docs/root/modules/aws/schema.md @@ -1519,15 +1519,14 @@ Representation of an AWS Elastic Load Balancer [Listener](https://docs.aws.amazo Representation of an AWS Elastic Load Balancer V2 [Listener](https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_Listener.html). -| Field | Description | -|-------|-------------| -| firstseen| Timestamp of when a sync job first discovered this node | -| lastupdated | Timestamp of the last time the node was updated | -| protocol | The protocol of this endpoint - One of `'HTTP''HTTPS''TCP''TLS''UDP''TCP_UDP'` | -| port | The port of this endpoint | -| ssl\_policy | Only set for HTTPS or TLS listener. The security policy that defines which protocols and ciphers are supported. | -| targetgrouparn | The ARN of the Target Group, if the Action type is `forward`. | - +| Field | Description | +|----------------|-----------------------------------------------------------------------------------------------------------------| +| firstseen | Timestamp of when a sync job first discovered this node | +| lastupdated | Timestamp of the last time the node was updated | +| protocol | The protocol of this endpoint - One of `'HTTP''HTTPS''TCP''TLS''UDP''TCP_UDP'` | +| port | The port of this endpoint | +| ssl\_policy | Only set for HTTPS or TLS listener. The security policy that defines which protocols and ciphers are supported. | +| targetgrouparn | The ARN of the Target Group, if the Action type is `forward`. | #### Relationships @@ -1537,17 +1536,117 @@ Representation of an AWS Elastic Load Balancer V2 [Listener](https://docs.aws.am (elbv2)-[r:ELBV2_LISTENER]->(ELBV2Listener) ``` +### Endpoint::ELBV2Rule + +Representation of an AWS Elastic Load Balancer +V2 [Rule](https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_Rule.html). + +| Field | Description | +|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| firstseen | Timestamp of when a sync job first discovered this node | +| lastupdated | Timestamp of the last time the node was updated | +| ***id*** | The ARN of the rule | +| priority | Priority of the rule. Rules are evaluated in priority order, from the lowest value to the highest value. | +| is_default | If true this rule will be executed if no other rules have matching conditions. | +| request_method_condition | If not empty, this rule will only match requests with one of the listed request methods. Methods are separated by `,` | +| host_header_condition | If not empty, this rule will only match requests for one of the listed host names. Host names are separated by `,` | +| path_pattern_condition | If not empty, this rule will only match requests with a URI matching one of the listed patterns. Patterns are separated by `,` and commas are escaped with `\`. | +| http_header_condition | If not empty, this rule will only match requests with a matching header key and value. Patterns are specified as header=value, separated by `,`, and commas escaped with `\`. | +| query_string_condition | If not empty, this rule will only match requests with a matching query string key and value. Patterns are specified as query-string=value, separated by `,`, and commas escaped with `\`. | +| source_ip_condition | If not empty, this rule will only match requests from one of the matching IP addresses. Addresses are separated by `,`. | + +#### Relationships + +- A ELBV2Rule is attached to an ELBV2Listener. + ``` + (ELBV2Listener)-[r:ELBV2_LISTENER_RULE]->(ELBV2Rule) + ``` + +### ELBV2Action + +Representation of an AWS Elastic Load Balancer +V2 [Action](https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_Action.html). +The `ELBV2Action` defines the base label type for `ELBV2ActionForward`, `ELBV2ActionAuthenticateOIDC`, +`ELBV2ActionAuthenticateCognito`, `ELBV2ActionRedirect`, and `ELBV2ActionFixedResponse`. + +| Field | Description | +|-------------|-----------------------------------------------------------------------| +| firstseen | Timestamp of when a sync job discovered this node | +| lastupdated | Timestamp of the last time the node was updated | +| ***id*** | A unique ID for the action of the form `listenerARN-\[order]-\[type]` | +| order | The position of the action | + +#### Relationships + +- An ELBV2Action is linked to an ELBV2Rule + ``` + (ELBV2Rule)-[r:ELBV2_LISTENER_ACTION]->(ELBV2Action) + ``` + +#### ELBV2Action::AWSForwardAction + +Representation of an AWS Elastic Load Balancer +V2 [Action](https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_Action.html) +that will forward traffic to one or more TargetGroups. + +#### ELBV2Action::AWSAuthenticateOIDCAction + +Representation of an AWS Elastic Load Balancer +V2 [Action](https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_Action.html) +that will authenticate a request via OIDC. + +| Field | Description | +|----------------------------|--------------------------------------------------------------------------------------------------| +| on_unauthenticated_request | Behaviour when an unauthenticated request is received. One of 'deny', 'allow', or 'authenticate' | + +#### ELBV2Action::AWSAuthenticateCognitoAction + +Representation of an AWS Elastic Load Balancer +V2 [Action](https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_Action.html) +that will authenticate a request using Cognito. + +| Field | Description | +|----------------------------|---------------------------------------------------------------------------------------------------| +| user_pool_arn | Associated Cognito pool ARN. | +| on_unauthenticated_request | Behaviour when an unauthenticated request is received. One of 'deny', 'allow', or 'authenticate'. | + +#### ELBV2Action::AWSRedirectAction + +Representation of an AWS Elastic Load Balancer +V2 [Action](https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_Action.html) +that will always redirect requests. + +| Field | Description | +|-------------|------------------------------------------------------------------| +| protocol | Protocol to which the request will be redirected | +| port | Port to which the request will be redirected | +| host | Host to which the request will be redirected | +| path | Path to which the request will be redirected | +| query | Query parameters that will be appended to the redirected request | +| status_code | Status code used in redirect | + +### ELBV2Action::AWSFixedResponseAction + +Representation of an AWS Elastic Load Balancer +V2 [Action](https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_Action.html) +that will always respond to requests with the specified response. + +| Field | Description | +|--------------|--------------------------------------------------| +| message_body | String that will be supplied as the message body | +| status_code | HTTP status code that will be returned | +| content_type | Content-type header that will be returned | ### Ip Represents a generic IP address. -| Field | Description | -|-------|-------------| -| firstseen| Timestamp of when a sync job first discovered this node | -| lastupdated | Timestamp of the last time the node was updated | -| **ip** | The IPv4 address | -| **id** | Same as `ip` | +| Field | Description | +|-------------|---------------------------------------------------------| +| firstseen | Timestamp of when a sync job first discovered this node | +| lastupdated | Timestamp of the last time the node was updated | +| **ip** | The IPv4 address | +| **id** | Same as `ip` | #### Relationships From 0515cb01c449933037ebd862e3b5fe7035b14e29 Mon Sep 17 00:00:00 2001 From: ashley-lowde Date: Thu, 15 Jun 2023 15:39:46 +0930 Subject: [PATCH 2/2] Fix code format and test data issues --- .../intel/aws/ec2/load_balancer_v2s.py | 4 ++-- tests/data/aws/ec2/load_balancers.py | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/cartography/intel/aws/ec2/load_balancer_v2s.py b/cartography/intel/aws/ec2/load_balancer_v2s.py index 1b20831e7..535b5a597 100644 --- a/cartography/intel/aws/ec2/load_balancer_v2s.py +++ b/cartography/intel/aws/ec2/load_balancer_v2s.py @@ -250,7 +250,7 @@ def load_load_balancer_v2_target_groups( @timeit def load_load_balancer_v2_target_group_targets( - neo4j_session: neo4j.Session, rule_data: Dict, lb_data: Dict, current_aws_account_id: str, update_tag: int + neo4j_session: neo4j.Session, rule_data: Dict, lb_data: Dict, current_aws_account_id: str, update_tag: int, ) -> None: """Create target group relationships from ELBV2Actions to targets""" @@ -309,7 +309,7 @@ def load_load_balancer_v2_target_group_targets( MERGE (aa)-[r:RESOURCE]->(lb) ON CREATE SET r.firstseen = timestamp() SET r.lastupdated = $update_tag - """ + """, } for action in rule_data['Actions']: diff --git a/tests/data/aws/ec2/load_balancers.py b/tests/data/aws/ec2/load_balancers.py index 9e5fd35e1..efc805a42 100644 --- a/tests/data/aws/ec2/load_balancers.py +++ b/tests/data/aws/ec2/load_balancers.py @@ -18,6 +18,25 @@ 'Port': 443, 'Protocol': 'HTTPS', 'TargetGroupArn': 'arn:aws:ec2:us-east-1:012345678912:targetgroup', + 'Rules': [ + { + 'ConditionStrings': { + "path_pattern": "foo", + }, + 'RuleArn': 'arn:aws:elasticloadbalancing:us-east-1:000000000000:listener-rule/app/myawesomeloadb/' + + '50dc6c495c0c9188/f2f7dc8efc522ab2/9683b2d02a6cabee', + 'Actions': [ + { + 'id': 'arn:aws:elasticloadbalancing:us-east-1:000000000000:listener-rule/app/myawesomeloadb/' + + '50dc6c495c0c9188/f2f7dc8efc522ab2/9683b2d02a6cabee-action-default', + 'Type': 'forward', + 'ForwardConfig': { + 'TargetGroups': [], + }, + }, + ], + }, + ], }, ] @@ -91,6 +110,8 @@ 'DNSName': 'myawesomeloadbalancer.amazonaws.com', 'CreatedTime': '10-27-2019 12:35AM', 'LoadBalancerName': 'myawesomeloadbalancer', + 'LoadBalancerArn': 'arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/' + + 'amyawesomeloadbalancer/50dc6c495c0c9188', 'Type': 'application', 'Scheme': 'internet-facing', 'AvailabilityZones': [