Skip to content

The relationship linker

Section 7 · From docs/INVENTORY_INTERNALS.md on GitHub

RelationshipLinker(assets, materialize_external=True) subclasses IdentifierIndex.

7.1 The identifier index

Every asset is registered under:

  • its arn and its name;
  • the native short ID: the last / or : segment of arn (not for k8s://, k8s-gke://, gcp-principal:, entra:, loganalytics: identifiers, whose tails are not IDs);
  • known self-ID metadata keys: group_id, route_table_id, internet_gateway_id, nat_gateway_id, network_interface_id, volume_id, allocation_id, network_acl_id, peering_connection_id, transit_gateway_id, vpc_endpoint_id, launch_template_id, file_system_id, queue_url, repository_uri, zone_id;
  • every entry of metadata.aliases;
  • type-specific keys: vpc_id (VPC / VNet), subnet_id (subnet), public_ip (Elastic IP, EC2), account_id (account nodes);
  • names other services use by domain: <bucket>.s3.amazonaws.com, a load balancer's dns_name (as-is and lower-cased without the trailing dot), a distribution's domain_name.

An identifier that starts with /subscriptions/ or /providers/, or contains a dot and no slash (host names), is also registered lower-cased, so Azure IDs and DNS names match case-insensitively.

7.2 Resolution

resolve(identifier, context=None) returns an asset id or None:

  1. Exact lookup.
  2. Each variant of the identifier (_variants), in order: trimmed of whitespace, trailing . and /; lower-cased; without a dualstack. prefix; without a trailing :*; a Lambda ARN without its version / alias qualifier; an S3 object ARN reduced to the bucket ARN; an image reference reduced to its repository (for ECR hosts, matched by a full-match regular expression on the parsed registry host, and for any host/repo:tag); an API Gateway stage ARN reduced to the API; an Azure Container Registry image reduced to the registry login server.
  3. A role ARN that missed (built from a bare role name, while the real role has a path): the role with that name in that account.
  4. An STS assumed-role ARN: the role with that name in that account.

When several assets share an identifier, _pick chooses:

  1. drop placeholders when a real asset is among the candidates;
  2. a single candidate wins;
  3. globally unique identifier schemes (arn:, /subscriptions/, /providers/, //, k8s://, cloudg:, aws-security:, azure-security:, entra:, loganalytics:, gcp-principal:, k8s-gke://) take the first candidate;
  4. without context, the first candidate;
  5. a candidate in the referencing asset's account and region;
  6. a candidate in the same account;
  7. exactly one candidate in region global;
  8. otherwise no match: an ambiguous name such as default that exists in several accounts is left unresolved rather than guessed.

7.3 External accounts and unresolved references

When a declared target does not resolve, _external_account() checks whether it belongs to an account that was not mapped: an AWS ARN with an account field, a bare 12-digit account ID, an Azure ID under /subscriptions/<guid>, a GCP name under //….googleapis.com/projects/<id>. If the account differs from the referencing asset's, a CLOUD_ACCOUNT placeholder with external: true is created once per account (and indexed, so later references reuse it), and the edge points there with properties.external_reference set to the original identifier. AWS-owned references (account aws, as in AWS managed policies) are not externalised.

Anything else is recorded in unresolved (up to 2000 entries).

7.4 The three passes

link(include_generic=True) runs, each pass wrapped so that one malformed asset cannot stop the run (failures are logged at debug level):

  1. Declared relations (_link_declared). For each relation: parse edge (unknown → REFERENCES), resolve target with the asset as context, fall back to an external account, else record as unresolved. reverse swaps source and target. Properties are copied.
  2. Provider rules (_link_rules, in linker_rules.py), for metadata conventions that predate declared relations or are shared by many collectors:
    • security_groups[], nsg_id, vpc_config.SecurityGroupIds → ATTACHED_TO / PROTECTED_BY_SG;
    • subnet_id, vpc_config.SubnetIds → subnet CONTAINS / SUBNET_CONTAINS_INSTANCE;
    • raw_data.Role or role_arn → ASSUMES_ROLE / RUNS_ON for functions and state machines, REFERENCES otherwise; raw_data.IamInstanceProfile.Arn → ASSUMES_ROLE / RUNS_ON;
    • kms_key_id → REFERENCES / ENCRYPTED_BY_KMS;
    • CloudFront origins → REFERENCES / SERVES_TRAFFIC_TO (bucket origins resolved through their .s3 host; skipped when a typed relation already links the pair); web_acl_id → WAF PROTECTS / PROTECTED_BY_WAF;
    • route tables: each route's gateway / NAT / transit gateway / peering / ENI target → ROUTE (NAT_TRANSLATED or TRANSIT_ROUTED); associations → subnet ATTACHED_TO;
    • attachments[].VpcId → ATTACHED_TO; attached_instance_id(s), network_interface_id → ATTACHED_TO;
    • peering connections → requester and accepter VPCs PEERING / VPC_PEERED;
    • Azure network_interfaces → NIC ATTACHED_TO VM;
    • GCP parent_full_resource_name → parent CONTAINS. Then the SSO role rule (_link_sso_roles): an Identity Center permission set with provisioned_role_prefix MANAGES / OWNED_BY every IAM role under /aws-reserved/sso.amazonaws.com/ named <prefix><16 hex characters>.
  3. Generic references (_link_generic, with include_generic, from inventory.link_references). Walks every string in the asset's metadata (depth 6), except relations, aliases, assume_role_policy and the self-ID keys. A string that looks like an identifier (arn:…, /subscriptions/…, a GCP full resource name, or an AWS short ID such as vpc-…, sg-…, snap-…) and resolves to another asset becomes REFERENCES, unless the pair is already linked in either direction. This catches relationships no rule knows about.

Deduplication: one edge per (source, target, edge_type) across all passes and seeded edges; self-loops are dropped.