Auto-Link¶
Some edges are not analysis, they are arithmetic: a URL contains a host, an e-mail address
contains a domain. AutoLink draws those edges the moment the containing observable is created,
under a source of its own, so two analysts — or two agents — building the same case end up with the
same graph.
from cyvest import AutoLink, Cyvest
cv = Cyvest(root_data={"case": "IR-2431"}, auto_link=AutoLink())
url = cv.observable(cv.OBS.URL, "hxxp://evil[.]example/login")
cv.observable_get(cv.OBS.DOMAIN, "evil.example") # created for you
cv.relation_get_all() # one EXTRACTION edge: url → domain
What is derived¶
| Parent | Child | Edge |
|---|---|---|
url |
its host: a domain, an ipv4 or an ipv6 |
EXTRACTION, parent → child |
email |
the domain after @ |
EXTRACTION, parent → child |
The value is refanged first, so hxxp://evil[.]example and https://evil.example derive the same
domain. A URL whose host is not an indicator — localhost, a bare NetBIOS name — derives nothing.
Domains, addresses and hashes contain nothing, which is what bounds the recursion.
Every derived relation carries source.name == "cyvest.autolink", so a report can tell an edge
the library inferred from one an analyst drew.
Why it changes the score¶
Under the default policy EXTRACTION propagates with attenuation 1.0: the child's score flows
to the parent unchanged. Give the domain a malicious signal and the URL becomes malicious too.
cv.observable_get(cv.OBS.DOMAIN, "evil.example").with_ti("virustotal", 7.0)
url.score # 7.0 — propagated through the derived edge
That is usually what "this URL points at a bad domain" means, and it is why the feature is opt-in rather than the default. See Scoring Model for the propagation rule.
Options¶
AutoLink() # every rule on — today, the structural one
AutoLink(structural=False) # off, while keeping the object around for future rules
AutoLink(inherit_internal=False) # derived children are always external
AutoLink(comment="derived") # the comment written on every derived relation
inherit_internal defaults to True: the domain of an internal mailbox is an internal domain.
Where the setting lives¶
AutoLink is a facade setting, not a fact. It is not serialized: a document loaded with
Cyvest.io_load_dict(data) comes back with auto_link=None, and you pass it again if the loaded
investigation should keep deriving — Cyvest.io_load_dict(data, auto_link=AutoLink()). The
relations already derived are facts and travel with the document.
A SharedInvestigationContext hands its auto_link to every worker it creates, so the derived
edges are the same whichever task first saw the observable.
Investigations built before the option was switched on can be caught up:
from cyvest.autolink import backfill_structural_links
backfill_structural_links(cv) # returns the number of relations added; idempotent
API summary¶
| Name | Purpose |
|---|---|
AutoLink(structural=True, inherit_internal=True, comment=…) |
the rules to apply |
Cyvest(..., auto_link=AutoLink()) |
apply them on every observable created through this facade |
Cyvest.io_load_dict(data, auto_link=…) / io_load_json |
keep deriving on a loaded document |
SharedInvestigationContext(..., auto_link=…) |
propagate to every worker |
derive_structural(obs_type, value) |
the pure derivation, no store involved |
cv.observable_create(..., resolve=False) |
create an identity as given, without the resolvers — what auto-link uses for the derived children |
backfill_structural_links(cv, config=None) |
derive for everything already in the store |
cv.relation_get_all() |
every relation, keyed |
AUTOLINK_SOURCE |
the SourceRef derived relations are attributed to |