Files
rengine/shared/services/celery_dispatch.py
T
Yogesh OjhaandClaude Opus 5 fe5bc8094c feat(bounty-hub): Bugcrowd, Intigriti and YesWeHack from the public feed
Bounty Hub was empty until someone pasted a HackerOne token. It now ships with
464 public programs and 6,889 scope assets from arkadiyt/bounty-targets-data
(MIT, republished every 30 minutes), so a fresh install is useful with no
credentials at all and the token becomes an upgrade rather than a prerequisite.

HackerOne is deliberately not read from the feed. The API gives us private
programs and richer scope, so the feed would be a downgrade — and a new
`source` column keeps that from happening by accident: an API row is
authoritative and the feed never overwrites one.

The three platforms do not agree on anything, so each gets its own entry in
shared/definitions/bounty_feed.py: the asset field is `target` on two of them
and `endpoint` on Intigriti; the type vocabularies are `website`/`network`
versus `url`/`wildcard`/`iprange` versus `web-application`/`mobile-application-ios`;
and only Intigriti and YesWeHack ship a handle, so Bugcrowd's is derived from
its engagement URL. All of it lands on the 17 asset types reNgine already has,
and target_for_scope needed no change to resolve 5,072 of them.

What the platforms do not report is not invented. Bugcrowd publishes no
submission state, so it stores `unknown` and the row shows no badge rather
than guessing "open". What they report and HackerOne does not — the actual
payout range — becomes a first-class field, a badge and a sort.

Provenance is visible rather than implied: every row names its platform, the
detail sheet says which source it came from and what that source can and
cannot contain, and Platform and Source are faceted filters.

The feed has its own schedule because it costs four small files rather than
640 requests: off, every 6 hours, daily or weekly, beside the HackerOne
interval in Settings. The change-detection layer needed nothing — it was
already platform-agnostic, so scope alerts work on all four platforms, and a
first feed load of 464 programs correctly announced none of them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SZEi41ZDRWtwYDPFwoCVnu
2026-09-07 16:22:05 +05:30

209 lines
6.5 KiB
Python

from celery import Celery
from shared.config import BaseAppSettings
from shared.definitions.constants import SCANS_QUEUE
from shared.logging import get_logger
logger = get_logger(__name__)
_celery_client: Celery | None = None
def get_celery_client() -> Celery:
global _celery_client # noqa: PLW0603
if _celery_client is None:
_settings = BaseAppSettings()
# never set_as_current: this client carries no result backend, and adopting it as
# the ambient app leaves every later chord in the process unable to start
_celery_client = Celery(
broker=_settings.celery_broker_url, set_as_current=False
)
_celery_client.conf.update(
task_serializer="json",
accept_content=["json"],
)
return _celery_client
def dispatch_whois_lookups(target_ids: list[str]) -> None:
if not target_ids:
return
logger.info("Dispatching WHOIS lookups for %d targets", len(target_ids))
get_celery_client().send_task(
"app.tasks.whois.perform_whois_lookups",
kwargs={"target_ids": target_ids},
queue="default",
)
def dispatch_ripestat_enrichment(target_ids: list[str]) -> None:
if not target_ids:
return
logger.info("Dispatching RIPEstat enrichment for %d targets", len(target_ids))
get_celery_client().send_task(
"app.tasks.ripestat.enrich_targets_bgp",
kwargs={"target_ids": target_ids},
queue="default",
)
def dispatch_dns_lookups(target_ids: list[str]) -> None:
if not target_ids:
return
logger.info(f"Dispatching DNS lookup for {len(target_ids)} target(s)")
get_celery_client().send_task(
"app.tasks.dns.perform_dns_lookups",
kwargs={"target_ids": target_ids},
queue="default",
)
def dispatch_scan_run(scan_id: str) -> None:
logger.info("Dispatching scan run %s", scan_id)
get_celery_client().send_task(
"app.tasks.scan.run_scan",
kwargs={"scan_id": scan_id},
queue=SCANS_QUEUE,
)
def revoke_scan_tasks(task_ids: list[str]) -> None:
"""SIGKILL-revoke a scan's celery tasks so an in-flight scan stops promptly."""
if not task_ids:
return
logger.info("Revoking %d scan task(s)", len(task_ids))
try:
get_celery_client().control.revoke(
list(task_ids), terminate=True, signal="SIGKILL"
)
except Exception:
logger.warning("scan task revoke failed", exc_info=True)
def dispatch_template_sync() -> bool:
"""Kick off a library refresh. Returns whether the queue accepted it."""
try:
get_celery_client().send_task("app.tasks.vuln_templates.sync", queue="default")
except Exception:
logger.warning("template sync dispatch failed", exc_info=True)
return False
return True
def dispatch_endpoint_verify(
scan_id: str, host: str, dir_path: str | None, limit: int
) -> bool:
"""Verify one branch of a scan's endpoints on demand. Returns whether the queue took it."""
try:
get_celery_client().send_task(
"app.tasks.endpoints.verify_branch",
kwargs={
"scan_id": scan_id,
"host": host,
"dir_path": dir_path,
"limit": limit,
},
queue="default",
)
except Exception:
logger.warning("endpoint verify dispatch failed", exc_info=True)
return False
return True
def dispatch_report(report_id: str) -> bool:
"""Queue a report render. Returns whether the queue took it."""
try:
get_celery_client().send_task(
"app.tasks.reports.generate", args=[report_id], queue="default"
)
except Exception:
logger.warning("report dispatch failed", exc_info=True)
return False
return True
def dispatch_interest_evaluation(
scan_id: str, *, include_ai: bool = True, notify: bool = True
) -> None:
logger.info("Dispatching interest evaluation for scan %s", scan_id)
get_celery_client().send_task(
"app.tasks.interest.evaluate_scan",
kwargs={"scan_id": scan_id, "include_ai": include_ai, "notify": notify},
queue="default",
)
def dispatch_interest_refresh(project_id: str) -> None:
logger.info("Dispatching interest refresh for project %s", project_id)
get_celery_client().send_task(
"app.tasks.interest.refresh_project",
kwargs={"project_id": project_id},
queue="default",
)
def dispatch_threat_intel(scan_id: str, *, enrich: bool = True) -> None:
"""Score a finished scan from the feeds, then fill the provider cache behind it."""
try:
client = get_celery_client()
client.send_task(
"app.tasks.threat_intel.apply_scan", args=[scan_id], queue="default"
)
if enrich:
client.send_task(
"app.tasks.threat_intel.enrich", args=[scan_id], queue="default"
)
except Exception:
logger.warning("threat intel dispatch failed", exc_info=True)
def dispatch_threat_intel_refresh(*, force: bool = False) -> bool:
"""Kick off a feed refresh. Returns whether the queue accepted it."""
try:
get_celery_client().send_task(
"app.tasks.threat_intel.refresh", kwargs={"force": force}, queue="default"
)
except Exception:
logger.warning("threat intel refresh dispatch failed", exc_info=True)
return False
return True
def dispatch_bounty_sync(*, scopes: bool = True) -> bool:
"""Refresh the bug bounty program library. Returns whether the queue accepted it."""
try:
get_celery_client().send_task(
"app.tasks.bounty_programs.sync", kwargs={"scopes": scopes}, queue="default"
)
except Exception:
logger.warning("bounty program sync dispatch failed", exc_info=True)
return False
return True
def dispatch_bounty_program_sync(handle: str) -> bool:
"""Refresh one program's scope."""
try:
get_celery_client().send_task(
"app.tasks.bounty_programs.sync_program", args=[handle], queue="default"
)
except Exception:
logger.warning("bounty program scope dispatch failed", exc_info=True)
return False
return True
def dispatch_bounty_feed_sync() -> bool:
"""Refresh the public program feed. Returns whether the queue accepted it."""
try:
get_celery_client().send_task(
"app.tasks.bounty_programs.sync_feed", queue="default"
)
except Exception:
logger.warning("bounty feed sync dispatch failed", exc_info=True)
return False
return True