Files
rengine/reports/sections/web_assets/section.py
T
Yogesh OjhaandClaude Sonnet 5 7a960f1de1 fix(net): a malformed URL port is rejected, not raised, everywhere .port is read
urllib.parse.SplitResult.port raises ValueError when the authority
carries a non-numeric port; nine call sites read it unguarded, so one
malformed URL (a stray quote, a client typo) turned into an unhandled
exception in the middle of a scan, a report, an interest rule or a
proxy dial. shared/utils/net.py:url_port makes the read itself the
failure boundary; unreadable_port names the bogus text so the naabu
and banner proxy paths can report what they refused rather than
retrying against a host string that dropped its port silently.

shared/definitions/ports.py:SCHEME_PORTS replaces four private copies
of the same http/https default-port map (endpoints.py, endpoint_judge,
scan_surface/normalize, nuclei's parser, dalfox's parser).

Also renames live_hosts to answered_hosts in the report source and the
interest context, and lifts the asset query's inline is:web predicate
into predicates.answered() — the existing distinction between is:live
(2xx) and is:web (answered at all) was already the rule; live_hosts
was the one name still claiming the stronger one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VckaDx6mBB4TsG76u61gvv
2026-09-16 08:04:06 +05:30

66 lines
2.2 KiB
Python

from __future__ import annotations
from reports.base import RenderContext, Section
from reports.config import SectionConfig, choice, columns, flag, limit
from shared.definitions.reports import SectionGroup
from shared.definitions.surface import SurfaceDimension
_COLUMNS = {
"status": "HTTP status",
"title": "Page title",
"ip": "Address",
"tech": "Technology",
"server": "Server",
"cdn": "CDN or WAF",
"asn": "Network",
"tls": "Certificate expiry",
"findings": "Findings",
}
class WebAssetsConfig(SectionConfig):
only_live: bool = flag(True, title="Only web assets that answered")
max_rows: int = limit(80, title="Rows shown", minimum=5, maximum=2000)
fields: list[str] = columns(
["status", "title", "ip", "tech"], title="Columns", options=_COLUMNS
)
order: str = choice(
"findings",
title="Order",
options={"findings": "Findings first", "status": "Status", "name": "Name"},
)
show_new: bool = flag(True, title="Mark new web assets")
class WebAssetsSection(Section):
name = "web_assets"
title = "Web assets"
description = "Web asset inventory."
group = SectionGroup.SURFACE.value
order = 10
launch_fields = frozenset({"max_rows"})
requires = frozenset({SurfaceDimension.WEB_ASSETS.value})
config_model = WebAssetsConfig
def build(self, ctx: RenderContext, cfg: WebAssetsConfig) -> dict | None:
rows = ctx.data.answered_hosts if cfg.only_live else ctx.data.host_rows
if not rows:
return None
if cfg.order == "findings":
rows = sorted(rows, key=lambda h: (-h.findings, h.name))
elif cfg.order == "status":
rows = sorted(rows, key=lambda h: (h.status or 999, h.name))
else:
rows = sorted(rows, key=lambda h: h.name)
total = len(rows)
return {
"rows": rows[: cfg.max_rows],
"total": total,
"hidden": max(0, total - cfg.max_rows),
"fields": [f for f in cfg.fields if f in _COLUMNS],
"labels": _COLUMNS,
"show_new": cfg.show_new,
"only_live": cfg.only_live,
"all_hosts": ctx.data.count_of(SurfaceDimension.WEB_ASSETS.value),
}