Files
rengine/shared/models/bounty_program.py
T
Yogesh OjhaandClaude Opus 5 dcdba5cfd4 feat(bounty-hub): read Intigriti through its researcher API
A platform with a researcher API is now a provider: one module under
shared/services/bounty_providers implementing programs() and scopes(),
returning rows the sync engine stores. bounty_programs.py became the
platform-agnostic engine, so adding a platform is one module, one
APIProvider value, one PlatformSpec.api_provider and a key tester.

Intigriti is the second connected platform, on a personal access token
the hunter creates. On this instance: 220 programs, 54 of them private,
155 with scope and 65 the platform refuses, which accounts for all 220.

Measured against the live API, against the docs:

- Tier ids are unordered and undocumented. Id 4 is Tier 1, not No Bounty,
  so the first mapping stored the best-paying assets as paying nothing.
  Eligibility reads the label; out of scope is matched on id and label.
- A 403 carrying FORBID001 is a refusal, not the documented rate-limit
  overage, and it covers public programs the account has not joined.
  Retrying it cost 11s a program; AccessDeniedError ends it in one
  request and records scope_access, keeping the failure budget for
  real breakage.
- Membership is proved by the platform letting the token read a private
  program's scope. confidentialityLevel never reports "invite only", so
  joined was always false and its filter was dead.

Bounty Hub follows what the data actually says:

- source becomes sources, a JSONB array. A program is routinely in both
  a platform API and the public feed; the feed adds itself without
  taking an API row back, and facet counts overlap accordingly.
- The row hint read "Bookmarked on HackerOne" while rendering
  Intigriti's follow flag. It now reads PlatformSpec.follow_label.
- Refresh queued the sync and returned, so nothing changed on screen.
  It waits for the sweep, reloads, and says so.
- The dashed connect card per platform is one dismissible Alert.
- Connecting a platform no longer diffs API scope against feed scope,
  which would report every asset removed and re-added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SiHx8Sm34hS1nanrsPdBaD
2026-09-12 23:35:59 +05:30

275 lines
8.6 KiB
Python

import uuid
from datetime import datetime
from pydantic import BaseModel
from sqlalchemy import Column, Text, text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.types import JSON
from sqlmodel import Field, SQLModel, UniqueConstraint
from shared.enums.target import TargetType
from shared.models.organization import OrganizationSummary
from shared.models.tag import TagSummary
from shared.utils.datetime import utc_now
class BountyProgram(SQLModel, table=True):
__tablename__ = "bounty_programs"
__table_args__ = (
UniqueConstraint("platform", "handle", name="uq_bounty_program_handle"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
platform: str = Field(max_length=32, index=True)
source: str = Field(default="api", max_length=16, index=True)
sources: list[str] = Field(
default_factory=list,
sa_column=Column(JSONB, nullable=False, server_default=text("'[]'::jsonb")),
)
handle: str = Field(max_length=200, index=True)
external_id: str | None = Field(default=None, max_length=100, index=True)
name: str = Field(max_length=300)
url: str | None = Field(default=None, max_length=500)
profile_picture: str | None = Field(
default=None, sa_column=Column(Text, nullable=True)
)
program_state: str = Field(max_length=16, index=True)
raw_state: str | None = Field(default=None, max_length=32)
joined: bool = Field(default=False, index=True)
joined_at: datetime | None = Field(default=None)
submission_state: str = Field(max_length=16, index=True)
offers_bounties: bool = Field(default=False, index=True)
open_scope: bool | None = Field(default=None)
gold_standard_safe_harbor: bool | None = Field(default=None)
currency: str | None = Field(default=None, max_length=16)
started_accepting_at: datetime | None = Field(default=None, index=True)
bookmarked: bool = Field(default=False, index=True)
reports_for_user: int | None = Field(default=None)
earnings_for_user: float | None = Field(default=None)
min_payout: float | None = Field(default=None)
max_payout: float | None = Field(default=None)
payout_currency: str | None = Field(default=None, max_length=16)
safe_harbor: str | None = Field(default=None, max_length=32)
requires_2fa: bool | None = Field(default=None)
scopes_synced_at: datetime | None = Field(default=None)
scope_access: str | None = Field(default=None, max_length=16)
synced_at: datetime = Field(default_factory=utc_now, index=True)
class BountyScope(SQLModel, table=True):
__tablename__ = "bounty_scopes"
__table_args__ = (
UniqueConstraint(
"program_id", "asset_type", "asset_identifier", name="uq_bounty_scope_asset"
),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
program_id: uuid.UUID = Field(
foreign_key="bounty_programs.id", index=True, ondelete="CASCADE"
)
asset_type: str = Field(max_length=48, index=True)
asset_identifier: str = Field(max_length=1000)
scope_state: str = Field(max_length=16, index=True)
eligible_for_bounty: bool | None = Field(default=None)
max_severity: str | None = Field(default=None, max_length=16)
tier: str | None = Field(default=None, max_length=32)
instruction: str | None = Field(default=None, sa_column=Column(Text))
reference: str | None = Field(default=None, max_length=500)
target_value: str | None = Field(default=None, max_length=500, index=True)
target_type: TargetType | None = Field(default=None)
raw: dict | None = Field(default=None, sa_column=Column(JSON, nullable=True))
synced_at: datetime = Field(default_factory=utc_now)
class BountyEventRow(SQLModel, table=True):
__tablename__ = "bounty_events"
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
platform: str = Field(max_length=32, index=True)
program_id: uuid.UUID = Field(
foreign_key="bounty_programs.id", index=True, ondelete="CASCADE"
)
handle: str = Field(max_length=200, index=True)
program_name: str = Field(max_length=300)
kind: str = Field(max_length=32, index=True)
asset_type: str | None = Field(default=None, max_length=48)
asset_identifier: str | None = Field(default=None, max_length=1000)
detail: str | None = Field(default=None, max_length=500)
created_at: datetime = Field(default_factory=utc_now, index=True)
class BountyEventRead(BaseModel):
id: uuid.UUID
platform: str
handle: str
program_name: str
kind: str
label: str = ""
description: str = ""
icon: str = "circle-help"
tone: str = "muted"
actionable: bool = False
asset_type: str | None
asset_identifier: str | None
detail: str | None
created_at: datetime
class BountyScopeRead(BaseModel):
id: uuid.UUID
asset_type: str
asset_type_label: str
asset_group: str
icon: str
asset_identifier: str
scope_state: str
eligible_for_bounty: bool | None
max_severity: str | None
tier: str | None
instruction: str | None
target_value: str | None
target_type: str | None
importable: bool
already_target: bool = False
class BountyProgramRead(BaseModel):
id: uuid.UUID
platform: str
platform_label: str = ""
source: str = "api"
source_label: str = ""
sources: list[str] = []
source_labels: list[str] = []
follow_label: str = ""
handle: str
name: str
url: str | None
profile_picture: str | None
program_state: str
raw_state: str | None
raw_state_label: str = ""
joined: bool
joined_at: datetime | None
submission_state: str
offers_bounties: bool
open_scope: bool | None
gold_standard_safe_harbor: bool | None
currency: str | None
started_accepting_at: datetime | None
bookmarked: bool
reports_for_user: int | None
earnings_for_user: float | None
min_payout: float | None = None
max_payout: float | None = None
payout_currency: str | None = None
safe_harbor: str | None = None
requires_2fa: bool | None = None
scopes_synced_at: datetime | None
scope_access: str | None = None
synced_at: datetime
in_scope_count: int = 0
out_of_scope_count: int = 0
importable_count: int = 0
imported_count: int = 0
watched: bool = False
watch_id: uuid.UUID | None = None
class BountyProgramDetail(BountyProgramRead):
scopes: list[BountyScopeRead] = []
unreachable: dict[str, int] = {}
class BountyScopeCounts(BaseModel):
total: int
in_scope: int
out_of_scope: int
importable: int
class BountySyncResult(BaseModel):
platform: str
programs: int
created: int
updated: int
duration_ms: int
error: str | None = None
class BountyImportRequest(BaseModel):
project_id: uuid.UUID
scope_ids: list[uuid.UUID] | None = None
include_out_of_scope: bool = False
group_by_program: bool = True
# blank falls back to the program name
organization_name: str | None = None
tags: list[str] = []
class BountyImportResult(BaseModel):
created: list[str]
existing: list[str]
skipped: list[str]
organization: OrganizationSummary | None = None
tags: list[TagSummary] = []
class PlatformCount(BaseModel):
platform: str
label: str
source: str
programs: int
private_programs: int = 0
feed_programs: int = 0
has_feed: bool = False
api_provider: str | None = None
supports_private: bool = False
credential: str = ""
note: str = ""
configured: bool = False
class BountyStatus(BaseModel):
configured: bool
platform: str
username: str | None
programs: int
private_programs: int
last_synced_at: datetime | None
sync_interval: str
next_sync_at: datetime | None = None
unseen_events: int = 0
platforms: list[PlatformCount] = []
source_counts: dict[str, int] = {}
feed_interval: str = ""
feed_synced_at: datetime | None = None
error: str | None = None
class BountySettingsRead(BaseModel):
platforms: list[PlatformCount] = []
sync_interval: str
feed_interval: str
feed_synced_at: datetime | None
feed_next_sync_at: datetime | None
feed_programs: int
feed_source: str
feed_url: str
feed_license: str
notify: bool
notify_events: list[str]
notifiable_events: list[str]
last_synced_at: datetime | None
next_sync_at: datetime | None
programs: int
events_recorded: int
class BountySettingsUpdate(BaseModel):
sync_interval: str | None = None
feed_interval: str | None = None
notify: bool | None = None
notify_events: list[str] | None = None