Files
rengine/shared/models/ip_address.py
T
Yogesh OjhaandClaude Opus 5 2375f1f1e4 feat(scan): service-level port pipeline, Services tab and Exposure panel
The unit of exposure is now a service — (ip, port) — not a bare open port.

Pipeline (EXPANSION): L0 produce hosts, L1 cdn_check + passive_ports,
L2 port_scan, L3 http_probe, L4 screenshot/waf_detect/service_fingerprint.

- cdn_check applies to every target type and records cdn_type, so a domain
  scan finally knows which addresses are CDN-fronted. cdn/waf is an edge you
  do not own (edge ports only); cloud is your own machine (scanned in full).
- port_scan decides and records a per-address scan_policy (full/web/skip)
  honouring the scan context's excluded_ips, and offers port profiles,
  exclusions, scan type, a tarpit threshold and an address budget.
- passive_ports reads Shodan internetdb through naabu -passive: keyless,
  touches_target=False, so a passive-intensity scan still gets exposure data.
- http_probe seeds its host x port matrix from open ports and folds every
  live response back onto the port (is_http, tls, product).
- service_fingerprint names the software behind non-web ports with a pure
  Python banner prober (tools/banner/), including a SOCKS5 CONNECT so it
  honours the context proxy. naabu -sV is unimplemented.
- shared/definitions/ports.py is the one vocabulary; ip_inventory and
  port_inventory are the only writers of ip_addresses/ports.
- open_ports_found is measured from the table (DERIVED_COUNTS): four stages
  write ports, so summing stage results double-counted.
- naabu failures no longer report zero open ports as success.

Surfaces: a Services tab (SERVICE_QUERY grammar, 16 fields, 18 counted
leads, 9 group dimensions, detail sheet) built on the shared results shell,
and an outlier-first Exposure panel on the Overview. Every count is a
promise — leads, groups and panel pivots were verified to equal the row
count they land on.

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

90 lines
2.9 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.utils.datetime import utc_now
class IpAddress(SQLModel, table=True):
__tablename__ = "ip_addresses"
__table_args__ = (UniqueConstraint("scan_id", "ip", name="uq_ipaddress_scan_ip"),)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True)
scan_id: uuid.UUID = Field(foreign_key="scans.id", index=True, ondelete="CASCADE")
target_id: uuid.UUID = Field(
foreign_key="targets.id", index=True, ondelete="CASCADE"
)
project_id: uuid.UUID = Field(foreign_key="projects.id", index=True)
ip: str = Field(max_length=45, index=True)
version: int = Field(default=4)
source: str = Field(max_length=30, index=True)
ptr_hostnames: list = Field(
default_factory=list, sa_column=Column(JSON, nullable=False)
)
asn: int | None = Field(default=None, index=True)
asn_org: str | None = Field(default=None, max_length=255)
prefix: str | None = Field(default=None, max_length=64)
country: str | None = Field(default=None, max_length=10)
is_cdn: bool = Field(default=False, index=True)
cdn_name: str | None = Field(default=None, max_length=100)
cdn_type: str | None = Field(default=None, max_length=20)
is_alive: bool | None = Field(default=None)
scan_policy: str | None = Field(default=None, max_length=16, index=True)
scan_policy_reason: str | None = Field(default=None, max_length=32)
discovered_at: datetime = Field(default_factory=utc_now)
created_at: datetime = Field(default_factory=utc_now)
class IpAddressRead(BaseModel):
id: uuid.UUID
scan_id: uuid.UUID
target_id: uuid.UUID
ip: str
version: int
source: str
ptr_hostnames: list[str] = Field(default_factory=list)
asn: int | None = None
asn_org: str | None = None
prefix: str | None = None
country: str | None = None
is_cdn: bool = False
cdn_name: str | None = None
cdn_type: str | None = None
is_alive: bool | None = None
scan_policy: str | None = None
scan_policy_reason: str | None = None
discovered_at: datetime
class IpAddressSummary(BaseModel):
total: int
alive: int
cdn: int
by_source: dict[str, int] = Field(default_factory=dict)
class TargetIpAddressRead(BaseModel):
ip: str
version: int
source: str
ptr_hostnames: list[str] = Field(default_factory=list)
asn: int | None = None
asn_org: str | None = None
prefix: str | None = None
country: str | None = None
is_cdn: bool = False
cdn_name: str | None = None
cdn_type: str | None = None
is_alive: bool | None = None
scan_policy: str | None = None
scan_count: int
last_scan_id: uuid.UUID
first_seen: datetime
last_seen: datetime