Compare commits

...

4 Commits

5 changed files with 172 additions and 1 deletions

View File

@@ -2,6 +2,12 @@
All notable changes to the **Prowler API** are documented in this file.
## [1.20.1] (UNRELEASED)
### 🐞 Fixed
- Attack Paths: Fix `exposed_internet` never set on ELB/ELBv2 nodes due to cartography sync ordering and em-dash typo in analysis Cypher [(#10251)](https://github.com/prowler-cloud/prowler/pull/10251)
## [1.20.0] (Prowler v5.19.0)
### 🚀 Added

View File

@@ -3,11 +3,14 @@
from typing import Any
from pathlib import Path
import aioboto3
import boto3
import neo4j
from cartography.config import Config as CartographyConfig
from cartography.graph.job import GraphJob
from cartography.intel import aws as cartography_aws
from celery.utils.log import get_task_logger
@@ -17,6 +20,7 @@ from api.models import (
)
from prowler.providers.common.provider import Provider as ProwlerSDKProvider
from tasks.jobs.attack_paths import db_utils, utils
from tasks.jobs.attack_paths.cartography_overrides_aws_sync_order import AWS_SYNC_ORDER
logger = get_task_logger(__name__)
@@ -47,7 +51,11 @@ def start_aws_ingestion(
boto3_session = get_boto3_session(prowler_api_provider, prowler_sdk_provider)
regions: list[str] = list(prowler_sdk_provider._enabled_regions)
requested_syncs = list(cartography_aws.RESOURCE_FUNCTIONS.keys())
# requested_syncs = list(cartography_aws.RESOURCE_FUNCTIONS.keys())
# TODO: Uncomment the line above and remove the block below when Cartography fixes the RESOURCE_FUNCTIONS ordering
requested_syncs = [
s for s in AWS_SYNC_ORDER if s in cartography_aws.RESOURCE_FUNCTIONS
]
sync_args = cartography_aws._build_aws_sync_kwargs(
neo4j_session,
@@ -144,6 +152,22 @@ def start_aws_ingestion(
cartography_aws._perform_aws_analysis(
requested_syncs, neo4j_session, common_job_parameters
)
# TODO: Remove when Cartography fixes em dashes in `aws_ec2_asset_exposure.json`
# Statements 5 and 6 use U+2014 (em dash) instead of U+002D (hyphen) in Cypher
# arrows, so `exposed_internet` is never set on ELB/ELBv2 nodes
logger.info(
f"Running ELB asset exposure fix for AWS account {prowler_api_provider.uid}"
)
elb_fix_path = (
Path(__file__).parent / "cartography_overrides_aws_ec2_asset_exposure.json"
)
GraphJob.run_from_json_file(
elb_fix_path,
neo4j_session,
common_job_parameters,
)
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 94)
return failed_syncs

View File

@@ -0,0 +1,31 @@
{
"statements": [
{
"query": "MATCH (n:LoadBalancer) WHERE n.exposed_internet IS NOT NULL WITH n LIMIT $LIMIT_SIZE REMOVE n.exposed_internet, n.exposed_internet_type",
"iterative": true,
"iterationsize": 1000
},
{
"query": "MATCH (n:LoadBalancerV2) WHERE n.exposed_internet IS NOT NULL WITH n LIMIT $LIMIT_SIZE REMOVE n.exposed_internet, n.exposed_internet_type",
"iterative": true,
"iterationsize": 1000
},
{
"query": "MATCH (cidr:IpRange{range:'0.0.0.0/0'})-->(perm:IpPermissionInbound)-->(sg:EC2SecurityGroup)<-[:MEMBER_OF_EC2_SECURITY_GROUP]-(elbv2:LoadBalancerV2{scheme: 'internet-facing'})-->(listener:ELBV2Listener)\nWHERE perm.protocol = '-1' OR (listener.port>=perm.fromport AND listener.port<=perm.toport)\nSET elbv2.exposed_internet = true",
"iterative": false
},
{
"query": "MATCH (cidr:IpRange{range:'0.0.0.0/0'})-->(perm:IpPermissionInbound)-->(sg:EC2SecurityGroup)<-[:SOURCE_SECURITY_GROUP]-(elb:LoadBalancer{scheme: 'internet-facing'})-->(listener:ELBListener)\nWHERE perm.protocol = '-1' OR (listener.port>=perm.fromport AND listener.port<=perm.toport)\nSET elb.exposed_internet = true",
"iterative": false
},
{
"query": "MATCH (elb:LoadBalancer{exposed_internet: true})-[:EXPOSE]->(e:EC2Instance)\nWITH e\nWHERE (e.exposed_internet_type IS NULL) OR (NOT 'elb' IN e.exposed_internet_type)\nSET e.exposed_internet = true, e.exposed_internet_type = coalesce(e.exposed_internet_type, []) + 'elb'",
"iterative": false
},
{
"query": "MATCH (elbv2:LoadBalancerV2{exposed_internet: true})-[:EXPOSE]->(e:EC2Instance)\nWITH e\nWHERE (e.exposed_internet_type IS NULL) OR (NOT 'elbv2' IN e.exposed_internet_type)\nSET e.exposed_internet = true, e.exposed_internet_type = coalesce(e.exposed_internet_type, []) + 'elbv2'",
"iterative": false
}
],
"name": "ELB/ELBv2 asset internet exposure (cartography em-dash fix)"
}

View File

@@ -0,0 +1,75 @@
# TODO: Remove this file when Cartography fixes the RESOURCE_FUNCTIONS ordering
#
# Explicit sync order for cartography AWS resource functions.
# Based on cartography_aws.RESOURCE_FUNCTIONS (v0.129.0) with one change:
# ec2:security_group moved before ec2:load_balancer, ec2:load_balancer_v2,
# and ec2:network_interface. These resources use OPTIONAL MATCH to link to
# EC2SecurityGroup nodes. On a fresh database the target nodes must exist
# first, otherwise MEMBER_OF_EC2_SECURITY_GROUP edges are silently dropped
# and exposed_internet is never set.
AWS_SYNC_ORDER: list[str] = [
"iam",
"iaminstanceprofiles",
"s3",
"kms",
"dynamodb",
"ec2:launch_templates",
"ec2:autoscalinggroup",
"ec2:instance",
"ec2:images",
"ec2:keypair",
"ec2:security_group", # moved here (was after ec2:network_interface)
"ec2:subnet",
"ec2:load_balancer", # depends on ec2:security_group
"ec2:load_balancer_v2", # depends on ec2:security_group
"ec2:network_acls",
"ec2:network_interface", # depends on ec2:security_group
"ec2:tgw",
"ec2:vpc",
"ec2:vpc_endpoint",
"ec2:route_table",
"ec2:vpc_peering",
"ec2:internet_gateway",
"ec2:reserved_instances",
"ec2:volumes",
"ec2:snapshots",
"ecr",
"ecr:image_layers",
"ecs",
"eks",
"elasticache",
"elastic_ip_addresses",
"emr",
"lambda_function",
"rds",
"redshift",
"route53",
"elasticsearch",
"permission_relationships",
"resourcegroupstaggingapi",
"apigateway",
"apigatewayv2",
"bedrock",
"cloudfront",
"secretsmanager",
"securityhub",
"s3accountpublicaccessblock",
"sagemaker",
"sns",
"sqs",
"ssm",
"acm:certificate",
"inspector",
"config",
"identitycenter",
"cloudtrail",
"cloudtrail_management_events",
"cloudwatch",
"efs",
"guardduty",
"codebuild",
"cognito",
"eventbridge",
"glue",
]

View File

@@ -3,9 +3,11 @@ from types import SimpleNamespace
from unittest.mock import MagicMock, call, patch
import pytest
from cartography.intel import aws as cartography_aws
from tasks.jobs.attack_paths import findings as findings_module
from tasks.jobs.attack_paths import internet as internet_module
from tasks.jobs.attack_paths import sync as sync_module
from tasks.jobs.attack_paths.cartography_overrides_aws_sync_order import AWS_SYNC_ORDER
from tasks.jobs.attack_paths.config import (
get_deprecated_provider_resource_label,
)
@@ -1494,3 +1496,36 @@ class TestAttackPathsDbUtilsGraphDataReady:
ap_scan_b.refresh_from_db()
assert ap_scan_a.graph_data_ready is False
assert ap_scan_b.graph_data_ready is True
class TestAWSSyncOrder:
"""Validate AWS_SYNC_ORDER covers all cartography resources and enforces SG ordering."""
def test_security_group_before_dependents(self):
"""ec2:security_group must sync before resources that link to SGs via OPTIONAL MATCH."""
sg_dependents = [
"ec2:load_balancer",
"ec2:load_balancer_v2",
"ec2:network_interface",
]
sg_idx = AWS_SYNC_ORDER.index("ec2:security_group")
for dep in sg_dependents:
assert (
AWS_SYNC_ORDER.index(dep) > sg_idx
), f"{dep} must come after ec2:security_group in AWS_SYNC_ORDER"
def test_sync_order_covers_all_resource_functions(self):
"""Every key in cartography RESOURCE_FUNCTIONS must appear in AWS_SYNC_ORDER."""
missing = set(cartography_aws.RESOURCE_FUNCTIONS.keys()) - set(AWS_SYNC_ORDER)
assert missing == set(), (
f"AWS_SYNC_ORDER is missing cartography resources: {missing}. "
"Add them to AWS_SYNC_ORDER in aws.py."
)
def test_sync_order_has_no_stale_entries(self):
"""Every key in AWS_SYNC_ORDER must exist in cartography RESOURCE_FUNCTIONS."""
stale = set(AWS_SYNC_ORDER) - set(cartography_aws.RESOURCE_FUNCTIONS.keys())
assert stale == set(), (
f"AWS_SYNC_ORDER has entries not in cartography: {stale}. "
"Remove them or update the cartography version."
)