Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
Action script for DomainTools - Bulk Enrich Domains.

Batch-enriches up to 500 domains and returns aggregate statistics
Batch-enriches up to 100 domains and returns aggregate statistics
by risk category, plus per-domain enrichment details.
"""

Expand Down Expand Up @@ -61,6 +61,9 @@ def main() -> None:
include_young: bool = extract_action_param(
siemplify, param_name="Include Young Domains", input_type=bool, default_value=True
)
merge_with_entities: bool = extract_action_param(
siemplify, param_name="Merge With Case Entities", input_type=bool, default_value=False
)

status: int = EXECUTION_STATE_COMPLETED
output_message: str = ""
Expand All @@ -74,15 +77,20 @@ def main() -> None:
siemplify_logger=siemplify.LOGGER,
)

if domains_param:
domains = [d.strip() for d in domains_param.split(",") if d.strip()]
manual = [d.strip() for d in domains_param.split(",") if d.strip()] if domains_param else []
entity_domains = [
domain
for entity in siemplify.target_entities
if entity.entity_type in SUPPORTED_ENTITY_TYPES
if (domain := extract_domain_from_string(entity.identifier))
]

if manual and merge_with_entities:
domains = list(dict.fromkeys(manual + entity_domains))
elif manual:
domains = manual
else:
domains = list({
domain
for entity in siemplify.target_entities
if entity.entity_type in SUPPORTED_ENTITY_TYPES
if (domain := extract_domain_from_string(entity.identifier))
})
domains = list(dict.fromkeys(entity_domains))

if not domains:
output_message = "No domains provided or found in scope."
Expand All @@ -91,21 +99,27 @@ def main() -> None:
return

if len(domains) > BULK_ENRICH_MAX_DOMAINS:
siemplify.LOGGER.warning(
siemplify.LOGGER.warn(
f"Domain count {len(domains)} exceeds max {BULK_ENRICH_MAX_DOMAINS}. Truncating."
)
domains = domains[:BULK_ENRICH_MAX_DOMAINS]

enriched_results = dt_manager.enrich_domains_with_risk(domains)

summary: dict[str, int] = {
enriched_domain_set = {e.domain for e in enriched_results}
missing_domains = [d for d in domains if d not in enriched_domain_set]

summary: dict[str, Any] = {
"total_domains": len(enriched_results),
"high_risk_count": 0,
"medium_risk_count": 0,
"suspicious_count": 0,
"young_domain_count": 0,
"low_risk_count": 0,
"missing_count": len(missing_domains),
}
if missing_domains:
summary["missing_domains"] = missing_domains
domains_output: list[dict[str, Any]] = []

for enriched in enriched_results:
Expand Down Expand Up @@ -140,6 +154,10 @@ def main() -> None:
f"{summary['young_domain_count']} young domains, "
f"{summary['low_risk_count']} low risk."
)
if missing_domains:
output_message += (
f"\nNot returned by API ({len(missing_domains)}): {', '.join(missing_domains)}"
)

except Exception as err:
output_message = f"Error running action: {str(err)}"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Bulk Enrich Domains
description: Batch-enriches up to 500 domains with DomainTools risk data and returns aggregate statistics by risk category (high_risk, medium_risk, suspicious, young_domain, low_risk). Falls back to in-scope case entities if no explicit domain list is provided.
description: Batch-enriches up to 100 domains with DomainTools risk data and returns aggregate statistics by risk category (high_risk, medium_risk, suspicious, young_domain, low_risk). Falls back to in-scope case entities if no explicit domain list is provided.
integration_identifier: DomainTools
parameters:
- name: Domains
Expand All @@ -12,6 +12,11 @@ parameters:
type: boolean
description: Whether to include young domains (< 30 days old) in the output table.
is_mandatory: false
- name: Merge With Case Entities
default_value: 'false'
type: boolean
description: When enabled, merges the manual Domains list with in-scope case entities. Has no effect if Domains is empty.
is_mandatory: false
dynamic_results_metadata:
- result_example_path: resources/BulkEnrichDomains_JsonResult_example.json
result_name: JsonResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ def main() -> None:
entity.additional_properties.update(prefixed)

entity.is_enriched = True
if enriched.overall_risk_score >= risk_threshold or enriched.is_young_domain:
entity.is_suspicious = True
entity.is_suspicious = enriched.overall_risk_score >= risk_threshold
enriched.is_suspicious = entity.is_suspicious

success_entities.append(entity)
except Exception as e:
Expand All @@ -116,9 +116,16 @@ def main() -> None:
)
siemplify.LOGGER.exception(e)

enriched_domains = {e.domain for e in enriched_results}
missing_domains = [d for d in extracted_domains if d not in enriched_domains]

if success_entities:
siemplify.update_entities(success_entities)

if missing_domains:
json_results.append({"Entity": "MissingDomains", "EntityResult": {"missing_domains": missing_domains}})
siemplify.result.add_result_json(json_results)

csv_table_results = [
enriched.to_table_data()
for enriched in enriched_results
Expand All @@ -129,18 +136,20 @@ def main() -> None:
"Domain Risk Enrichment", construct_csv(csv_table_results)
)

suspicious_count = sum(
1 for e in success_entities if e.is_suspicious
)
suspicious_count = sum(1 for e in success_entities if e.is_suspicious)
output_message = (
f"Successfully enriched {len(success_entities)} domain(s). "
f"{suspicious_count} marked as suspicious."
f"{suspicious_count} marked as suspicious based on the given threshold of {risk_threshold}. \n"
)

if failed_entities:
output_message += (
f"\nFailed to enrich: {', '.join(str(e.identifier) for e in failed_entities)}"
)
if missing_domains:
output_message += (
f"\nNot returned by API: {', '.join(missing_domains)}"
)
else:
output_message = "No entities were enriched."
result_value = False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ parameters:
- name: Risk Threshold
default_value: '70'
type: string
description: Risk score above which a domain entity is marked as suspicious. Defaults to 70.
is_mandatory: false
description: Domains scoring at or above this value are marked suspicious. Cannot be set higher than 20 (suspicious category floor) — any domain scoring >= 20 is always flagged regardless of this value.
is_mandatory: true
dynamic_results_metadata:
- result_example_path: resources/EnrichDomainRisk_JsonResult_example.json
result_name: JsonResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def main() -> None:
csv_rows: list[dict] = []
success_entities: list = []
failed_entities: list = []
missing_domains: list[str] = []

target_entities = [
entity
Expand All @@ -78,11 +79,16 @@ def main() -> None:
iris_models = dt_manager.investigate_domains(domains=[domain])
iris_model = iris_models[0] if iris_models else None

create_date = iris_model.registration.create_date if iris_model else None
if not iris_model:
missing_domains.append(domain)
continue

create_date_raw = iris_model.registration.create_date if iris_model else None
create_date_str = create_date_raw.get("value") if isinstance(create_date_raw, dict) else create_date_raw
domain_age_days: int | None = None
if create_date:
if create_date_str:
try:
created = datetime.strptime(create_date[:10], "%Y-%m-%d")
created = datetime.strptime(create_date_str[:10], "%Y-%m-%d")
domain_age_days = (datetime.now() - created).days
except ValueError:
pass
Expand Down Expand Up @@ -111,7 +117,7 @@ def main() -> None:
"email_domains": iris_model.identity.email_domains if iris_model else [],
},
"registration": {
"create_date": create_date,
"create_date": create_date_raw,
"expiration_date": iris_model.registration.expiration_date if iris_model else None,
"domain_status": iris_model.registration.domain_status if iris_model else False,
"registrar_status": iris_model.registration.registrar_status if iris_model else [],
Expand Down Expand Up @@ -143,6 +149,8 @@ def main() -> None:

if success_entities:
siemplify.update_entities(success_entities)
if missing_domains:
json_results.append({"Entity": "MissingDomains", "EntityResult": {"missing_domains": missing_domains}})
siemplify.result.add_result_json(json_results)
if csv_rows:
siemplify.result.add_data_table("Domain Profile", construct_csv(csv_rows))
Expand All @@ -153,6 +161,10 @@ def main() -> None:
output_message += (
f"\nFailed to profile: {', '.join(str(e.identifier) for e in failed_entities)}"
)
if missing_domains:
output_message += (
f"\nNot returned by API: {', '.join(missing_domains)}"
)
else:
output_message = "No domain profiles could be built."
result_value = False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def get_parsed_domain_rdap(self, domain: str) -> ParsedDomainRDAPModel:

def get_whois_history(self, domain: str):
try:
self._check_license("parsed-domain-rdap")
self._check_license("whois-history")
response = self._api.whois_history(query=domain).response()
return self.parser.parse_whois_history(raw_data=response)
except NotFoundException:
Expand Down
Loading
Loading