From ebda0f4c4981cf437ee0da5d597ec4bd19a96ba2 Mon Sep 17 00:00:00 2001 From: Topman3579 Date: Mon, 27 Jul 2026 14:29:02 +0700 Subject: [PATCH] fix(enrichers): dnsx stdin input + configurable Scamalytics API host Two enrichers silently return empty results instead of failing loudly. 1. domain_to_dns dnsx >= 1.2 treats -d as "bruteforce this domain" and exits with "missing wordlist(w) flag required with domain(d) input" unless -w is also given, so `dnsx -d example.com -a -aaaa -json -silent` never produces output. The enricher catches nothing (the tool exits 0) and returns [], while Celery still reports the task as succeeded. Feed the single target on stdin instead, which is the supported single-target invocation. DockerTool.launch() gains an optional entrypoint override so a shell can be put in front of the image entrypoint; the domain is passed through shlex.quote(). 2. ip_to_fraudscore Scamalytics assigns each account a numbered API node (api11, api12, ...) and the host was hardcoded to api12, which returns 404 for every account provisioned on a different node. The host is now read from a SCAMLYTICS_API_HOST secret/env var, defaulting to the previous api12 so existing deployments are unaffected. Verified against dnsx v1.2.2 and a live Scamalytics v3 account. --- .../src/flowsint_enrichers/ip/to_fraudscore.py | 9 +++++++-- flowsint-enrichers/src/tools/dockertool.py | 10 +++++++--- flowsint-enrichers/src/tools/network/dnsx.py | 11 +++++++++-- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/flowsint-enrichers/src/flowsint_enrichers/ip/to_fraudscore.py b/flowsint-enrichers/src/flowsint_enrichers/ip/to_fraudscore.py index ff31eb7a..3ceb7014 100644 --- a/flowsint-enrichers/src/flowsint_enrichers/ip/to_fraudscore.py +++ b/flowsint-enrichers/src/flowsint_enrichers/ip/to_fraudscore.py @@ -74,11 +74,16 @@ class IpToFraudScore(Enricher): self.ip_risk_mapping = [] api_username = self.get_secret("SCAMLYTICS_USERNAME", os.getenv("SCAMLYTICS_USERNAME")) - api_key = self.get_secret("SCAMLYTICS_API_KEY", os.getenv("SCAMLYTICS_API_KEY")) + api_key = self.get_secret("SCAMLYTICS_API_KEY", os.getenv("SCAMLYTICS_API_KEY")) + # Scamalytics assigns each account a numbered API node (api11, api12, + # ...); a hardcoded host 404s for every account that is not on it. + api_host = self.get_secret( + "SCAMLYTICS_API_HOST", os.getenv("SCAMLYTICS_API_HOST", "api12") + ) for ip in data: try: - api_request = requests.get(f'https://api12.scamalytics.com/v3/{api_username}/?key={api_key}&ip={ip.address}', timeout=30) + api_request = requests.get(f'https://{api_host}.scamalytics.com/v3/{api_username}/?key={api_key}&ip={ip.address}', timeout=30) if api_request.status_code != 200: Logger.error( diff --git a/flowsint-enrichers/src/tools/dockertool.py b/flowsint-enrichers/src/tools/dockertool.py index bf906668..94077da8 100644 --- a/flowsint-enrichers/src/tools/dockertool.py +++ b/flowsint-enrichers/src/tools/dockertool.py @@ -46,7 +46,7 @@ class DockerTool(Tool): except ImageNotFound: return False - def launch(self, command: str, volumes: dict = None, timeout: int = 30, environment: dict = None): + def launch(self, command, volumes: dict = None, timeout: int = 30, environment: dict = None, entrypoint=None): self.install() # Merge default environment with custom environment env = {"TERM": "dumb"} # Set terminal type to avoid TTY issues @@ -54,8 +54,7 @@ class DockerTool(Tool): env.update(environment) try: - result = self.client.containers.run( - self.image, + run_kwargs = dict( command=command, remove=True, stdout=True, @@ -67,6 +66,11 @@ class DockerTool(Tool): stdin_open=False, # Ensure stdin is not open environment=env, ) + # Some tools only accept a single target on stdin, which needs a + # shell in front of the image's default entrypoint. + if entrypoint is not None: + run_kwargs["entrypoint"] = entrypoint + result = self.client.containers.run(self.image, **run_kwargs) return result.decode() except ImageNotFound: raise RuntimeError(f"Image {self.image} not found. Did you run install()?") diff --git a/flowsint-enrichers/src/tools/network/dnsx.py b/flowsint-enrichers/src/tools/network/dnsx.py index 57d88b30..cfa140ce 100644 --- a/flowsint-enrichers/src/tools/network/dnsx.py +++ b/flowsint-enrichers/src/tools/network/dnsx.py @@ -1,4 +1,5 @@ import json +import shlex from typing import List from ..dockertool import DockerTool @@ -109,14 +110,20 @@ class DnsxTool(DockerTool): flags.append("-aaaa") flags += ["-json", "-silent"] - command = f"-d {domain} {' '.join(flags)}" + # dnsx >= 1.2 treats `-d` as "bruteforce this domain" and refuses to + # run without a `-w` wordlist, so a plain `-d example.com` exits with + # "missing wordlist(w) flag required with domain(d) input" and the + # enricher silently returns nothing. Feeding the single target on + # stdin is the supported invocation. + inner = f"dnsx {' '.join(flags)}" + command = ["-c", f"echo {shlex.quote(domain)} | {inner}"] env = {} if api_key: env["PDCP_API_KEY"] = api_key try: - result = super().launch(command, environment=env) + result = super().launch(command, environment=env, entrypoint="sh") except Exception as e: raise RuntimeError( f"Error running dnsx: {str(e)}. Output: {getattr(e, 'output', 'No output')}"