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

130 lines
4.0 KiB
Python

import uuid
from datetime import datetime
from pydantic import BaseModel
from sqlalchemy import Column
from sqlalchemy.types import JSON
from sqlmodel import Field, SQLModel, UniqueConstraint
from shared.enums.api_key import APIProvider
from shared.utils.datetime import utc_now
API_PROVIDER_META: dict[str, dict] = {
APIProvider.VIEWDNS: {
"name": "ViewDNS.info",
"description": "DNS intelligence, reverse lookups and DNS history",
"docs_url": "https://viewdns.info/api/?src=reNgine",
"requires_username": False,
"icon": "scan-search",
"color": "#6366f1",
},
APIProvider.CHAOS: {
"name": "Chaos",
"description": "ProjectDiscovery Chaos subdomain dataset",
"docs_url": "https://cloud.projectdiscovery.io",
"requires_username": False,
"icon": "radar",
"color": "#f59e0b",
},
APIProvider.NETLAS: {
"name": "Netlas",
"description": "Internet-wide asset and attack-surface intelligence",
"docs_url": "https://netlas.io",
"requires_username": False,
"icon": "globe",
"color": "#10b981",
},
APIProvider.SECURITYTRAILS: {
"name": "SecurityTrails",
"description": "DNS, domain and subdomain history intelligence",
"docs_url": "https://securitytrails.com",
"requires_username": False,
"icon": "route",
"color": "#3b82f6",
},
APIProvider.HACKERONE: {
"name": "HackerOne",
"description": "Program scope and reports from HackerOne",
"docs_url": "https://api.hackerone.com",
"requires_username": True,
"icon": "shield",
"color": "#ef4444",
},
APIProvider.INTIGRITI: {
"name": "Intigriti",
"description": "Program scope from Intigriti, including invite-only programs",
"docs_url": "https://app.intigriti.com/researcher/personal-access-tokens",
"requires_username": False,
"icon": "shield",
"color": "#8b5cf6",
},
APIProvider.VULNX: {
"name": "vulnx",
"description": "ProjectDiscovery vulnerability intelligence: exploits, coverage and exposure",
"docs_url": "https://cloud.projectdiscovery.io",
"requires_username": False,
"icon": "biohazard",
"color": "#f43f5e",
},
APIProvider.INTERACTSH: {
"name": "Interactsh",
"description": "Auth token for a self-hosted out-of-band callback server",
"docs_url": "https://github.com/projectdiscovery/interactsh",
"requires_username": False,
"icon": "satellite-dish",
"color": "#8b5cf6",
},
}
class APIKey(SQLModel, table=True):
__tablename__ = "api_keys"
__table_args__ = (UniqueConstraint("provider", name="uq_api_key_provider"),)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
provider: APIProvider = Field(index=True)
key_value: str = Field(max_length=1000)
key_meta: dict | None = Field(default=None, sa_column=Column(JSON, nullable=True))
is_enabled: bool = Field(default=True)
usage_counter: int = Field(default=0)
last_used_at: datetime | None = Field(default=None)
created_at: datetime = Field(default_factory=utc_now)
updated_at: datetime = Field(default_factory=utc_now)
class APIKeyCreate(BaseModel):
provider: APIProvider
key_value: str
key_meta: dict | None = None
class APIKeyUpdate(BaseModel):
key_value: str | None = None
is_enabled: bool | None = None
key_meta: dict | None = None
class APIKeyRead(BaseModel):
id: uuid.UUID
provider: APIProvider
key_value_masked: str
key_meta: dict | None = None
is_enabled: bool
usage_counter: int
last_used_at: datetime | None
created_at: datetime
updated_at: datetime
meta: dict
class ProviderInfo(BaseModel):
provider: APIProvider
name: str
description: str
docs_url: str
icon: str = "package"
color: str = "#64748b"
requires_username: bool = False
configured: bool = False
is_enabled: bool = False