deduplicate(assets, edges): keyed on arn. The first copy is kept unless it came from a
sweep / placeholder (discovered_via) and the new one did not, in which case they swap.
relations and aliases are unioned onto the kept copy, is_internet_exposed is OR-ed, and
edges are re-pointed to the kept copy. An edge is dropped only when it became a self-loop or an
exact repeat of an earlier one: same endpoints, edge_type, port_range, ports,
protocol, cidr and direction. Parallel security group, NACL and internet-exposure rules
for different ports or protocols are kept. This is what merges an S3 bucket seen
from every region, an account seen as an organization member and as a trust principal, or an
Azure subscription seen by the hierarchy and by the subscription collector.
add_account_hierarchy(assets, edges): one CLOUD_ACCOUNT per (provider, account_id),
reusing an existing node with the canonical identifier (arn:aws:iam::<id>:root,
/subscriptions/<id>, //cloudresourcemanager.googleapis.com/projects/<id>); every asset
that nothing CONTAINS gets an account CONTAINS / ACCOUNT_CONTAINS_REGION edge with
properties.hierarchy: true. Hierarchy-type assets are never contained this way.