Enrichment Plugins & Support Utilities¶
Enrichment plugins expand the reconnaissance scope and add contextual data to discovered assets. They analyze relationships between assets to determine if newly discovered entities should be added to the session scope based on association with in-scope assets.
Overview: Enrichment Plugin Architecture¶
graph TB
subgraph "Input Assets"
FQDN["FQDN Asset"]
IP["IPAddress Asset"]
Contact["ContactRecord Asset"]
end
subgraph "Horizontals Plugin"
HorFQDN["horfqdn handler<br/>Priority: 6"]
HorContact["horContact handler"]
AssocCheck["Association Check<br/>scope.IsAssociated()"]
ScopeExpansion["Scope Expansion<br/>session.Scope().Add()"]
end
subgraph "IP-Netblock Plugin"
IPHandler["IP-Netblock-Handler<br/>Priority: 4"]
CIDRLookup["CIDRanger Lookup<br/>support.IPNetblock()"]
BGPQuery["BGP.Tools Query<br/>if not cached"]
end
subgraph "Enrichment Actions"
PTRCheck["PTR Record Check<br/>ipPTRTargetsInScope()"]
IPSweep["IP Address Sweep<br/>support.IPAddressSweep()"]
NetblockEnum["Netblock Enumeration<br/>sweepAroundIPs()"]
AssocRel["Associated Relationships<br/>addAssociatedRelationship()"]
end
subgraph "Output"
NewAssets["New In-Scope Assets"]
NewEvents["New Events Dispatched"]
Relationships["Associated_with Edges"]
end
FQDN --> HorFQDN
Contact --> HorContact
IP --> IPHandler
HorFQDN --> AssocCheck
HorContact --> AssocCheck
AssocCheck --> ScopeExpansion
HorFQDN --> PTRCheck
ScopeExpansion --> IPSweep
ScopeExpansion --> NetblockEnum
ScopeExpansion --> AssocRel
IPHandler --> CIDRLookup
CIDRLookup --> BGPQuery
PTRCheck --> NewAssets
IPSweep --> NewAssets
NetblockEnum --> NewAssets
AssocRel --> Relationships
BGPQuery --> NewAssets
NewAssets --> NewEvents
Horizontals Plugin¶
The Horizontals plugin (NewHorizontals()) is the primary scope expansion mechanism in Amass. It analyzes asset relationships to determine if out-of-scope discoveries should be brought into scope based on their association with in-scope assets.
Plugin Structure¶
graph LR
subgraph "horizPlugin"
PluginCore["horizPlugin struct<br/>name: 'Horizontals'<br/>confidence: 50"]
HorFQDN["horfqdn handler"]
HorContact["horContact handler"]
Source["source: et.Source<br/>Name: Horizontals<br/>Confidence: 50"]
end
subgraph "Handler Registration"
FQDNHandler["Handler<br/>Name: Horizontals-FQDN-Handler<br/>Priority: 6<br/>EventType: oam.FQDN<br/>Transforms: [oam.FQDN]"]
ContactHandler["Handler<br/>Name: Horizontals-ContactRecord-Handler<br/>EventType: oam.ContactRecord<br/>Transforms: [oam.ContactRecord]"]
end
PluginCore --> HorFQDN
PluginCore --> HorContact
PluginCore --> Source
HorFQDN --> FQDNHandler
HorContact --> ContactHandler
FQDN Handler: Scope Expansion via Associations¶
The horfqdn handler processes FQDN assets to determine if they should be added to scope based on associations with in-scope assets.
Handler Logic Flow:
graph TD
Start["FQDN Event Received<br/>horfqdn.check()"]
ExtractFQDN["Extract oamdns.FQDN<br/>from e.Entity.Asset"]
CheckPTR{"Has PTR Records?<br/>RRType == 12"}
CheckDNS{"Has A/AAAA/CNAME?<br/>HasDNSRecordType()"}
CheckInScope{"Already in scope?<br/>IsAssetInScope()"}
CheckTransform{"Transformation allowed?<br/>CheckTransformations()<br/>FQDN -> FQDN"}
PTRPath["checkPTR() path"]
AssocPath["Association lookup path"]
LookupAssoc["lookup()<br/>scope.IsAssociated()"]
HasAssoc{"Associations found?"}
StoreFinding["store() assets"]
ProcessAssets["process() assets<br/>dispatch events"]
AddAssocRel["addAssociatedRelationship()"]
End["Return"]
Start --> ExtractFQDN
ExtractFQDN --> CheckPTR
CheckPTR -->|"Yes"| PTRPath
CheckPTR -->|"No"| CheckDNS
CheckDNS -->|"No"| End
CheckDNS -->|"Yes"| CheckInScope
CheckInScope -->|"Yes"| End
CheckInScope -->|"No"| CheckTransform
CheckTransform -->|"No"| End
CheckTransform -->|"Yes"| AssocPath
PTRPath --> End
AssocPath --> LookupAssoc
LookupAssoc --> HasAssoc
HasAssoc -->|"No"| End
HasAssoc -->|"Yes"| StoreFinding
StoreFinding --> ProcessAssets
ProcessAssets --> AddAssocRel
AddAssocRel --> End
PTR Record Analysis:
When a PTR record is found, the handler performs bidirectional scope expansion:
| Scenario | Action |
|---|---|
| PTR source IP is in-scope | Extract eTLD+1 from PTR target, add domain to scope |
| PTR target FQDN is in-scope | Add source IP to scope, perform IP sweep (size 100 or 250 if active) |
Contact Handler: Organization and Location Expansion¶
The horContact handler processes ContactRecord assets to expand scope based on organizational and location relationships. It follows three types of edges from ContactRecords: organization, location, and id.
graph TD
ContactEvent["ContactRecord Event"]
CheckTransform["CheckTransformations()<br/>ContactRecord -> ContactRecord"]
OutgoingEdges["OutgoingEdges()<br/>labels: [organization, location, id]"]
IterateEdges["Iterate each edge"]
GetEntity["FindEntityById()<br/>edge.ToEntity"]
CheckAssoc["scope.IsAssociated()<br/>ScopeChange: true"]
HasAssoc{"Associations found?"}
LogRationale["Log rationale"]
ProcessImpacted["process() impacted assets"]
AddAssocRel["addAssociatedRelationship()"]
End["Return"]
ContactEvent --> CheckTransform
CheckTransform --> OutgoingEdges
OutgoingEdges --> IterateEdges
IterateEdges --> GetEntity
GetEntity --> CheckAssoc
CheckAssoc --> HasAssoc
HasAssoc -->|"Yes"| LogRationale
HasAssoc -->|"No"| End
LogRationale --> ProcessImpacted
ProcessImpacted --> AddAssocRel
AddAssocRel --> End
Association Logic¶
Association checking is performed by scope.IsAssociated(), which traverses the asset graph to find relationships between submitted assets and in-scope assets.
Traversal Paths:
| Asset Type | Outgoing Relations | Incoming Relations | Purpose |
|---|---|---|---|
| DomainRecord | registrant_contact |
— | Registration data provides association |
| IPNetRecord | registrant |
registration |
Network registration |
| AutnumRecord | registrant |
registration |
AS registration |
| TLSCertificate | subject_contact |
— | Certificate ownership |
| ContactRecord | organization, location |
registrant, registrant_contact, subject_contact |
Organizational links |
| FQDN | registration |
node |
Domain ownership |
| IPAddress | — | contains |
Network membership |
| Netblock | registration |
— | Network ownership |
| Service | — | port |
Service attachment |
Association Confidence Scoring:
type Association struct {
Submission *dbt.Entity // Asset being checked
Match *dbt.Entity // In-scope asset that matched
Rationale string // Human-readable explanation
Confidence int // 0-100 confidence score
ScopeChange bool // Whether scope was modified
ImpactedAssets []*dbt.Entity // Assets added to scope
}
When ScopeChange: true, all related assets are added to scope and the rationale is extended with the list of impacted assets.
IP Sweeping¶
When a new network range is added to scope, the Horizontals plugin performs IP address sweeps:
graph LR
Netblock["Netblock Entity<br/>Contains IPs"]
CheckActive{"Active mode?<br/>e.Session.Config().Active"}
Size100["size = 100"]
Size250["size = 250"]
ExtractIP["Extract IPAddress"]
CallSweep["support.IPAddressSweep()<br/>addr, size, callback"]
Netblock --> ExtractIP
ExtractIP --> CheckActive
CheckActive -->|"No"| Size100
CheckActive -->|"Yes"| Size250
Size100 --> CallSweep
Size250 --> CallSweep
The sweep size is determined by active mode: 100 IPs in passive mode, 250 IPs in active mode.
IP-Netblock Plugin¶
The IP-Netblock plugin (NewIPNetblock()) maps IP addresses to their containing network blocks and autonomous systems, providing network infrastructure context.
Plugin Architecture¶
graph TB
subgraph "ipNetblock Plugin"
Plugin["ipNetblock struct<br/>name: 'IP-Netblock'<br/>priority: 4<br/>confidence: 100"]
Handler["IP-Netblock-Handler<br/>MaxInstances: 100<br/>Transforms: [oam.Netblock]<br/>EventType: oam.IPAddress"]
end
subgraph "Lookup Flow"
CheckReserved{"Reserved address?<br/>amassnet.IsReservedAddress()"}
ReservedPath["reservedAS()<br/>Creates AS0<br/>AutnumRecord"]
NormalPath["Normal lookup"]
end
subgraph "CIDRanger Lookup"
CheckCIDR["support.IPNetblock()<br/>session.CIDRanger()"]
WaitLoop["Retry up to 120 seconds<br/>sleep 1 second"]
GetEntry["CIDRangerEntry<br/>Net, ASN, Src"]
end
subgraph "Asset Creation"
CreateNetblock["CreateAsset()<br/>oamnet.Netblock"]
CreateAS["CreateAsset()<br/>oamnet.AutonomousSystem"]
CreateEdge["CreateEdge()<br/>relation: contains<br/>from: Netblock<br/>to: IPAddress"]
CreateEdge2["CreateEdge()<br/>relation: announces<br/>from: AS<br/>to: Netblock"]
end
Plugin --> Handler
Handler --> CheckReserved
CheckReserved -->|"Yes"| ReservedPath
CheckReserved -->|"No"| NormalPath
NormalPath --> CheckCIDR
CheckCIDR --> WaitLoop
WaitLoop --> GetEntry
GetEntry --> CreateNetblock
CreateNetblock --> CreateAS
CreateAS --> CreateEdge
CreateEdge --> CreateEdge2
CIDRanger Integration¶
The plugin queries the session's CIDRanger, which is populated by other plugins (primarily BGP.Tools):
for i := 0; i < 120; i++ {
entry = support.IPNetblock(e.Session, ip.Address.String())
if entry != nil {
break
}
time.Sleep(time.Second)
}
The retry loop gives upstream plugins time to populate CIDRanger data.
Asset and Relationship Creation¶
The store() method creates the infrastructure graph:
- Netblock Asset — Creates
oamnet.Netblockwith CIDR and type (IPv4/IPv6) - Contains Edge — Links Netblock → IPAddress with relation
"contains" - AutonomousSystem Asset — Creates
oamnet.AutonomousSystemwith ASN - Announces Edge — Links AutonomousSystem → Netblock with relation
"announces"
Reserved Address Handling¶
For reserved IP addresses (RFC 1918, loopback, etc.), the plugin creates a special AS0 structure:
| Asset Type | Value | Purpose |
|---|---|---|
| Netblock | Detected reserved CIDR (e.g., 10.0.0.0/8) |
Represents reserved address space |
| AutonomousSystem | Number: 0 | Special AS for reserved addresses |
| AutnumRecord | Handle: "AS0", Name: "Reserved Network Address Blocks" |
Registration record for AS0 |
Support Utilities¶
The engine/plugins/support package provides shared utilities used by all plugins.
IP Address Sweeping¶
The IPAddressSweep() function performs controlled scanning of IP ranges:
graph TD
Start["IPAddressSweep()<br/>input: addr, size"]
CheckProcessed{"Already processed?<br/>FindEntitiesByContent()"}
DetermineType{"IP type?"}
IPv4Mask["IPv4: /18 mask<br/>16,384 addresses"]
IPv6Mask["IPv6: /64 mask<br/>2^64 addresses"]
ApplyMask["ip.Mask(mask)<br/>Create IPNet"]
CIDRSubset["amassnet.CIDRSubset()<br/>cidr, center_ip, size"]
IterateIPs["For each IP in subset"]
InvokeCallback["callback(e, addr, src)"]
End["Return"]
Start --> CheckProcessed
CheckProcessed -->|"Yes"| End
CheckProcessed -->|"No"| DetermineType
DetermineType -->|"IPv4"| IPv4Mask
DetermineType -->|"IPv6"| IPv6Mask
IPv4Mask --> ApplyMask
IPv6Mask --> ApplyMask
ApplyMask --> CIDRSubset
CIDRSubset --> IterateIPs
IterateIPs --> InvokeCallback
InvokeCallback --> End
The function creates a large subnet centered on the target IP, then selects a subset of addresses evenly distributed across the range.
Netblock Lookup¶
The IPNetblock() function queries the session's CIDRanger to find the most specific (longest prefix) netblock containing an IP address:
Returns the entry with the highest number of mask bits (most specific prefix), e.g., preferring /24 over /16.
TTL Checking¶
graph TB
subgraph "Asset Monitoring Flow"
Handler["Plugin Handler<br/>check(e *et.Event)"]
CalcTTL["support.TTLStartTime()"]
CheckMonitor["support.AssetMonitoredWithinTTL()"]
Decision{"Already<br/>monitored?"}
Lookup["Lookup cached results<br/>from previous run"]
Query["Query external source<br/>or perform computation"]
Mark["support.MarkAssetMonitored()"]
Process["Process results"]
end
Handler --> CalcTTL
CalcTTL -->|"since time.Time"| CheckMonitor
CheckMonitor --> Decision
Decision -->|"Yes"| Lookup
Decision -->|"No"| Query
Lookup --> Process
Query --> Mark
Mark --> Process
| Function | Purpose |
|---|---|
AssetMonitoredWithinTTL(session, entity, source, since) |
Check if asset was monitored by source after since |
MarkAssetMonitored(session, entity, source) |
Mark asset as monitored by source at current time |
SourceToAssetsWithinTTL(session, key, assetType, source, since) |
Find assets created by source after since |
FQDN Resolution Helpers¶
| Function | Purpose |
|---|---|
IsCNAME(session, fqdn) |
Returns the CNAME target if the FQDN has a CNAME record |
NameIPAddresses(session, fqdn) |
Returns all IP addresses (A/AAAA) associated with an FQDN |
NameResolved(session, fqdn) |
Returns true if the FQDN has resolved IP addresses |
ScrapeSubdomainNames(s) |
Extracts valid subdomain patterns from arbitrary text |
ExtractURLFromString(s) |
Extracts the first URL from a string |
FQDN Metadata Management¶
The support package attaches and queries metadata to FQDN events without modifying the underlying asset:
| Function | Purpose |
|---|---|
AddSLDInScope(e) |
Mark that FQDN's SLD is in scope |
HasSLDInScope(e) |
Check if FQDN's SLD is in scope |
AddDNSRecordType(e, rrtype) |
Record that RRType was queried |
HasDNSRecordType(e, rrtype) |
Check if RRType was queried |
Usage example from HTTP-Probes:
if !support.HasDNSRecordType(e, int(dns.TypeA)) &&
!support.HasDNSRecordType(e, int(dns.TypeAAAA)) &&
!support.HasDNSRecordType(e, int(dns.TypeCNAME)) {
return nil
}
Organization Creation and Deduplication¶
The org sub-package provides utilities for creating organization assets with automatic deduplication:
graph TD
Start["CreateOrgAsset(session, obj, rel, org, src)"]
Lock["Acquire createOrgLock"]
Dedup["dedupChecks(session, obj, org)"]
Found{"Existing org<br/>found?"}
CreateIdent["Create Identifier asset<br/>Type: OrganizationName"]
DetermineID["determineOrgID(name)<br/>GLEIF lookup"]
CreateOrg["Create Organization asset"]
LinkRelation["Link obj->rel->org"]
Unlock["Release lock"]
Start --> Lock
Lock --> Dedup
Dedup --> Found
Found -->|"Yes"| LinkRelation
Found -->|"No"| CreateIdent
CreateIdent --> DetermineID
DetermineID --> CreateOrg
CreateOrg --> LinkRelation
LinkRelation --> Unlock
Deduplication checks (in order):
- Name matching in existing organization entities
- Location sharing with other contact records or organizations
- Shared ancestor detection in the asset graph
- Session membership — checking if related organizations are in the work queue
Organization ID Determination:
When an LEI record is found via GLEIF, the ID format is:
Example: AMAZON.COM, INC.:US-DE:5351052
When no LEI is found, a UUID is generated as the unique identifier.
GLEIF API Functions¶
| Function | Purpose | Rate Limit |
|---|---|---|
GLEIFSearchFuzzyCompletions(name) |
Search for LEI codes by name | 3 sec/req |
GLEIFGetLEIRecord(id) |
Get detailed LEI record | 3 sec/req |
GLEIFGetDirectParentRecord(id) |
Get parent organization | 3 sec/req |
GLEIFGetDirectChildrenRecords(id) |
Get all subsidiaries | 3 sec/req |
Organization Name Matching¶
The NameMatch function checks if an organization entity corresponds to any of the provided names:
func NameMatch(session et.Session, orgent *dbt.Entity,
names []string) (exact []string, partial []string, found bool)
Matching Logic:
- Extract organization names from
NameandLegalNamefields - Query for
Identifierentities linked with"id"relation - For each name pair: check exact match (case-insensitive); calculate Smith-Waterman-Gotoh similarity; accept if similarity >= 0.85
The ExtractBrandName function removes common legal suffixes (Inc, LLC, Ltd, GmbH, etc.) to get the core brand name for better matching.
Service Discovery Helpers¶
ServiceWithIdentifier — Generates unique service identifiers by hashing the session ID concatenated with the target address, ensuring the same endpoint always gets the same service ID.
X509ToOAMTLSCertificate — Converts Go's *x509.Certificate to OAM's *oamcert.TLSCertificate format.
ProcessAssetsWithSource — Takes a slice of Finding structs and creates edges, adds SourceProperty, dispatches events, and logs relationship discoveries.
type Finding struct {
From *dbt.Entity
FromName string
To *dbt.Entity
ToName string
ToMeta interface{}
Rel oam.Relation
}
Global Constants¶
Default value for handler MaxInstances field, limiting concurrent handler executions. Used across all plugin registrations.
Integration with Other Plugins¶
graph LR
subgraph "Upstream Plugins"
DNS["DNS Plugins<br/>Priority 1-3"]
WHOIS["WHOIS Plugins<br/>Priority 1"]
APIs["API Plugins<br/>Priority 6-7"]
end
subgraph "Enrichment Plugins"
IPNetblock["IP-Netblock<br/>Priority 4"]
Horizontals["Horizontals<br/>Priority 6"]
end
subgraph "Downstream Plugins"
Service["Service Discovery<br/>Priority 9"]
end
DNS -->|"IPAddress"| IPNetblock
WHOIS -->|"Netblock, ASN"| IPNetblock
IPNetblock -->|"Netblock events"| Horizontals
APIs -->|"Organizations,<br/>ContactRecords"| Horizontals
Horizontals -->|"Expanded scope<br/>New FQDNs, IPs"| DNS
Horizontals -->|"In-scope assets"| Service
The IP-Netblock plugin operates at priority 4, between DNS resolution and the Horizontals plugin. This ordering ensures netblock data is available when Horizontals evaluates scope expansion.
Summary¶
Enrichment plugins expand reconnaissance scope and add infrastructure context through:
- Scope Expansion — The Horizontals plugin analyzes associations between assets to automatically expand the session scope when discovering related entities with sufficient confidence
- Infrastructure Mapping — The IP-Netblock plugin maps IP addresses to their containing network blocks and autonomous systems, providing BGP and network ownership context
- Intelligent Sweeping — IP address sweeps discover nearby hosts within newly added network ranges, with sweep size adjusted based on active/passive mode
- Association Traversal — Graph traversal algorithms follow typed edges (organization, location, registration, certificate) to find relationships between assets
- TTL Management — Plugins respect transformation TTL configurations to avoid redundant processing while ensuring data freshness