Files
rengine/shared/models/oast.py
T
Yogesh OjhaandClaude Opus 5 b0a47ba087 feat(oast): an instance-owned callback server, and a tier that pays the wait once
Out-of-band testing is an instance setting — `off`, `self_hosted` or
`public` — with the server, the token (the INTERACTSH API key row) and the
wait resolved once in `shared/services/oast.py` and read on the stage
thread. A mode that cannot be used is off and says why: self-hosted with no
server, or public without the disclosure acknowledged. A fresh instance is
off. `POST /oast/test` asks a public address for `/poll` over HTTPS and
reports that the server answered, never that OAST works.

The engine keeps one switch. `FieldNeeds` puts the resource key on the
field and `GET /engines/catalog` fills `unavailable_reason`, so the builder
renders the instance reason beside a switch that stays settable.

Measured on nuclei v3.11.1: with `-no-interactsh` a check that needs a
callback still loads and the request is refused — 0 requests, exit 0. So
the batch reported COMPLETED and every origin in it read `scanned` for
checks that never touched the target. The selection now drops them when
nothing is listening, one coverage row reports the count and the reason,
and `Tier.OAST` is left out of the planned tiers.

Membership is what a check references, not what it is tagged: 615 of the
12,586 official rows reference an interactsh placeholder and 233 of those
carry no `oast` tag. `vuln_templates.needs_oast` is parsed at index time; a
library resync fills the column.

The wait is paid on out-of-band batches alone, which sweep for 30 minutes
so it is paid fewer times, and the tier names the server on its own rows.
A finding's callback is rendered on the sheet from one label map; the list
path is bared to the metadata.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 20:53:01 +05:30

50 lines
1.2 KiB
Python

"""Out-of-band settings as the API reads and writes them."""
from __future__ import annotations
from datetime import datetime
from pydantic import BaseModel, ConfigDict
from pydantic import Field as PydanticField
from shared.definitions.oast import (
MAX_SERVER_LENGTH,
MAX_WAIT_SECONDS,
MIN_WAIT_SECONDS,
OastMode,
)
class OastRead(BaseModel):
mode: str = OastMode.OFF.value
server: str | None = None
wait_seconds: int = 0
public_acknowledged: bool = False
token_set: bool = False
public_servers: list[str] = PydanticField(default_factory=list)
checks: int = 0
reason: str | None = None
last_interaction_at: datetime | None = None
interactions: int = 0
class OastUpdate(BaseModel):
model_config = ConfigDict(extra="forbid")
mode: OastMode | None = None
server: str | None = PydanticField(default=None, max_length=MAX_SERVER_LENGTH)
wait_seconds: int | None = PydanticField(
default=None, ge=MIN_WAIT_SECONDS, le=MAX_WAIT_SECONDS
)
public_acknowledged: bool | None = None
class OastTest(BaseModel):
ok: bool = False
detail: str = ""
address: str | None = None
status_code: int | None = None
__all__ = ["OastRead", "OastTest", "OastUpdate"]