Files
rengine/shared/models/relations.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

43 lines
911 B
Python

import uuid
from pydantic import BaseModel, Field
class RelationEvidence(BaseModel):
kind: str
label: str
value: str
detail: str = ""
class RelatedTarget(BaseModel):
target_id: uuid.UUID
target_value: str
reasons: list[RelationEvidence] = Field(default_factory=list)
class TargetRelations(BaseModel):
items: list[RelatedTarget] = Field(default_factory=list)
total: int = 0
considered: int = 0
class ProgramMatch(BaseModel):
program_id: uuid.UUID
handle: str
name: str
platform: str
url: str | None = None
scope_identifier: str = ""
asset_type: str = ""
wildcard: bool = False
in_scope: bool = True
offers_bounties: bool = False
eligible_for_bounty: bool | None = None
max_severity: str | None = None
class TargetPrograms(BaseModel):
items: list[ProgramMatch] = Field(default_factory=list)
total: int = 0