Files
rengine/api/app/services/program_coverage.py
T
Yogesh OjhaandClaude Opus 5 a180a3020a feat(bounty-hub): say which program covers a target
The scope link ran one way: a program's scope showed whether each entry was
already a target. The question a hunter asks is the other one - I have this
asset, who pays for it - and nothing answered it.

`ProgramCoverageService` matches a host against every synced program's scope
through the machinery a watch already uses (`plan_scope`, `WatchItem.matches`,
`excluded_by`), so a wildcard entry covers its subtree, an exact entry covers
itself, and a program's own out-of-scope rows mark the match excluded rather
than hiding it. Measured against 7,173 synced scope rows on this instance:
`foo.bugcrowd.com` and `deep.nested.bugcrowd.com` resolve to Bugcrowd through
`*.bugcrowd.com/auth/*`, `notacme.com` is not a subtree of `*.acme.com`, and
`allegro.cz` comes back uncovered because that program lists it out of scope.

The target page carries it in the rail as Bounty: the program, its platform
and the scope entry that covers the target, hidden outside bug bounty mode.
`for_host` takes a bare hostname so a discovered asset can be asked the same
question next.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YYop31s6BqDUt4TBs5mbwa
2026-09-12 23:05:47 +05:30

114 lines
4.2 KiB
Python

"""Which bounty programs pay for a target."""
from __future__ import annotations
import re
from uuid import UUID
from sqlalchemy import func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from shared.definitions.relations import MAX_COVERED_PROGRAMS
from shared.definitions.watch import excluded_by, plan_scope
from shared.models.bounty_program import BountyProgram, BountyScope
from shared.models.relations import ProgramMatch, TargetPrograms
from shared.models.target import Target
_SCHEME = re.compile(r"^[a-z][a-z0-9+.-]*://")
def host_of(value: str | None) -> str:
"""The hostname a scope entry or a target value is about."""
raw = _SCHEME.sub("", (value or "").strip().lower()).split("/")[0]
return raw.split(":")[0].removeprefix("*.").rstrip(".")
def scope_match(host: str, scopes: list) -> tuple[object | None, bool]:
"""The scope entry this host sits under, and whether the program excludes it."""
plan = plan_scope(scopes)
item = next((i for i in plan.items if i.matches(host)), None)
if item is None:
return None, False
by_id = {str(scope.id): scope for scope in scopes}
return by_id.get(item.scope_id), excluded_by(host, plan.excluded_subdomains)
class ProgramCoverageService:
def __init__(self, session: AsyncSession):
self.session = session
async def for_target(self, project_id: UUID, target_id: UUID) -> TargetPrograms:
target = await self.session.scalar(
select(Target).where(
Target.id == target_id, Target.project_id == project_id
)
)
if target is None:
return TargetPrograms()
return await self.for_host(host_of(target.target_value))
async def for_host(self, host: str) -> TargetPrograms:
if not host:
return TargetPrograms()
programs = await self._candidates(host)
items: list[ProgramMatch] = []
for program in programs:
match = await self._match(program, host)
if match is not None:
items.append(match)
items.sort(key=lambda m: (not m.in_scope, not m.offers_bounties, m.name))
return TargetPrograms(items=items[:MAX_COVERED_PROGRAMS], total=len(items))
async def _candidates(self, host: str) -> list:
"""Programs with a scope entry this host could sit under."""
value = func.lower(BountyScope.target_value)
rows = await self.session.execute(
select(
BountyProgram.id,
BountyProgram.handle,
BountyProgram.name,
BountyProgram.platform,
BountyProgram.url,
BountyProgram.offers_bounties,
)
.join(BountyScope, BountyScope.program_id == BountyProgram.id)
.where(
BountyScope.target_value.isnot(None),
or_(value == host, func.strpos(host, "." + value) > 0),
)
.distinct()
)
return list(rows.all())
async def _match(self, program, host: str) -> ProgramMatch | None:
rows = await self.session.execute(
select(
BountyScope.id,
BountyScope.asset_type,
BountyScope.asset_identifier,
BountyScope.scope_state,
BountyScope.target_value,
BountyScope.target_type,
BountyScope.eligible_for_bounty,
BountyScope.max_severity,
).where(BountyScope.program_id == program.id)
)
scopes = list(rows.all())
scope, excluded = scope_match(host, scopes)
if scope is None:
return None
return ProgramMatch(
program_id=program.id,
handle=program.handle,
name=program.name,
platform=program.platform,
url=program.url,
scope_identifier=scope.asset_identifier,
asset_type=scope.asset_type,
wildcard=scope.asset_identifier.strip().startswith("*."),
in_scope=not excluded,
offers_bounties=bool(program.offers_bounties),
eligible_for_bounty=scope.eligible_for_bounty,
max_severity=scope.max_severity,
)