mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-21 07:41:28 +02:00
reports/narrate/ai.py instructs the model: no marketing language, no hedging,
no filler, never an em dash, never a rhetorical question. The product broke all
three. There was a house style in this repository and it applied only to the
machine.
The activity feed printed Python dictionary keys. stage_count_summary rendered
key.replace('_', ' ') over StageResult.counts, so a live scan read "12 waf,
340 checked", "88 ptr", "412 http assets", "56 alive". _STAGE_COUNT_LABELS now
covers all 31 count keys with singular and plural, and an unlabelled key is
dropped rather than printed raw, so a new stage can never leak one again.
Errors had three registers: "Failed to X" (121), "Could not X" (28), and
"Couldn't X - try again" (6), alongside a passive form the result tables already
used. All of it is now "<Subject> could not be <verbed>", which names the thing
that failed before the failure. The rate limiter reads the key's TTL and says
how long to wait instead of "Please wait a moment".
Onboarding fired confetti and a party popper, opened with "Let's get your
instance set up", and asked an operator to tick "I've saved my backup codes".
confetti.ts is deleted.
Casing was split down the middle: the sidebar group read "Attack surface" while
the item under it read "Scheduled Scans", and page headings restated the nav in
Title Case. Sentence case throughout now, including SURFACE_LABELS, so the
backend and its mirror agree.
The KEV sentence in generated reports claimed a listing "means working exploit
code is public". It means exploitation has been observed in the wild. That one
printed under a client's letterhead.
Also: 18 "This action cannot be undone" in three spellings removed in favour of
the concrete consequence; every em dash, contraction and "Please" out of user
copy; surface vocabulary applied to alerts and the notification settings, not
only the result tables; the "X, not Y" antithesis thinned from ten uses to the
two that carry a real distinction; advice copy ("fix first", "worth a look",
"Add to renewal watchlist", which named a feature that does not exist) reduced
to the fact.
Nine rules are written up in the audit linked below and hold for new copy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LM9TbCGhGb4s6nag6PsuaT
88 lines
3.0 KiB
Python
88 lines
3.0 KiB
Python
from __future__ import annotations
|
|
|
|
from sqlalchemy import select
|
|
|
|
from reports.base import RenderContext, Section
|
|
from reports.config import SectionConfig, limit
|
|
from shared.definitions.domains import PRIVATE_TLDS, registrable_domain
|
|
from shared.definitions.reports import SectionGroup
|
|
from shared.definitions.surface import SurfaceDimension
|
|
from shared.models.http_asset import HttpAsset
|
|
|
|
_VENDOR_HINTS = (
|
|
"amazonaws",
|
|
"cloudfront",
|
|
"akamai",
|
|
"azure",
|
|
"googleusercontent",
|
|
"fastly",
|
|
"sni.cloudflare",
|
|
)
|
|
|
|
|
|
class ScopeRecommendationsConfig(SectionConfig):
|
|
max_rows: int = limit(20, title="Suggestions shown", minimum=1, maximum=100)
|
|
min_evidence: int = limit(
|
|
1, title="Minimum certificates naming the domain", minimum=1, maximum=20
|
|
)
|
|
|
|
|
|
class ScopeRecommendationsSection(Section):
|
|
name = "scope_recommendations"
|
|
title = "Scope recommendations"
|
|
description = "Other registrable domains named on certificates this estate serves."
|
|
group = SectionGroup.INTELLIGENCE.value
|
|
order = 40
|
|
requires = frozenset({SurfaceDimension.WEB_ASSETS.value})
|
|
default_enabled = False
|
|
config_model = ScopeRecommendationsConfig
|
|
|
|
def build(self, ctx: RenderContext, cfg: ScopeRecommendationsConfig) -> dict | None:
|
|
scan_id = ctx.data.scan_for(SurfaceDimension.WEB_ASSETS.value)
|
|
if scan_id is None:
|
|
return None
|
|
own = registrable_domain(ctx.data.subject) or ctx.data.subject
|
|
rows = ctx.data.session.execute(
|
|
select(HttpAsset.host, HttpAsset.tls_sans)
|
|
.where(HttpAsset.scan_id == scan_id, HttpAsset.tls_sans.is_not(None))
|
|
.limit(4000)
|
|
).all()
|
|
|
|
found: dict[str, dict] = {}
|
|
for host, sans in rows:
|
|
for name in sans or []:
|
|
cleaned = str(name).lstrip("*.").strip().lower()
|
|
if not cleaned or "." not in cleaned:
|
|
continue
|
|
apex = registrable_domain(cleaned)
|
|
if not apex or apex == own:
|
|
continue
|
|
if apex.rsplit(".", 1)[-1] in PRIVATE_TLDS:
|
|
continue
|
|
if any(hint in apex for hint in _VENDOR_HINTS):
|
|
continue
|
|
entry = found.setdefault(
|
|
apex, {"domain": apex, "names": set(), "seen_on": set()}
|
|
)
|
|
entry["names"].add(cleaned)
|
|
entry["seen_on"].add(host)
|
|
|
|
suggestions = [
|
|
{
|
|
"domain": entry["domain"],
|
|
"names": sorted(entry["names"])[:4],
|
|
"seen_on": sorted(entry["seen_on"])[:3],
|
|
"count": len(entry["seen_on"]),
|
|
}
|
|
for entry in found.values()
|
|
if len(entry["seen_on"]) >= cfg.min_evidence
|
|
]
|
|
if not suggestions:
|
|
return None
|
|
suggestions.sort(key=lambda row: -row["count"])
|
|
return {
|
|
"rows": suggestions[: cfg.max_rows],
|
|
"own": own,
|
|
"total": len(suggestions),
|
|
}
|