Files
rengine/shared/utils/validation.py
T
Yogesh OjhaandClaude Opus 5 2cb8d73280 style: strip explanatory comments across the codebase
The comment rule in CLAUDE.md is one line maximum, never narration: no
multi-line blocks, no module-header explainers, no sentences that explain or
justify the code to the reader. The tree had drifted a long way from it, and
most of the drift is mine.

Removed:
  232  lines of multi-line comment blocks
  203  single-line comments that argued instead of stating
  154  comments joined by so/because/otherwise/rather-than
  143  narrating comments in the frontend
  112  test docstrings (the test name already says what it asserts)
   25  test module docstrings and 25 decorative section dividers

Collapsed:
  324  multi-line docstrings to one factual line
  271  one-line docstrings that had a justifying clause bolted on

Kept: functional directives, section dividers, short field labels, alembic
revision headers, and the eight FastAPI route docstrings that render in /docs.
Where a fact was load-bearing it is in CLAUDE.md, which is where it belongs.

284 python tests and 34 frontend tests pass, ruff and ruff-format clean across
ten packages, svelte-check 0 errors, prettier and eslint clean, app.main imports.

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

140 lines
3.8 KiB
Python

import ipaddress
import re
import validators
from shared.enums.target import TargetType
HEX_COLOR_LENGTH = 7 # e.g., #RRGGBB
MAX_NAME_LEN = 120
def clean_name(v: str, *, max_len: int = MAX_NAME_LEN) -> str:
v = (v or "").strip()
if not v:
msg = "Name must not be empty."
raise ValueError(msg)
if len(v) > max_len:
msg = f"Name must be {max_len} characters or fewer."
raise ValueError(msg)
return v
def clean_optional_name(v: str | None, *, max_len: int = MAX_NAME_LEN) -> str | None:
"""An update that omits the name leaves it alone."""
return None if v is None else clean_name(v, max_len=max_len)
def validate_domain(value: str) -> bool:
return validators.domain(value) is True
def validate_ip(value: str) -> bool:
try:
ipaddress.ip_address(value)
return True
except ValueError:
return False
def validate_ip_range(value: str) -> bool:
try:
ipaddress.ip_network(value, strict=False)
return "/" in value
except ValueError:
return False
def validate_asn(value: str) -> bool:
asn_pattern = r"^AS\d+$"
return bool(re.match(asn_pattern, value.upper()))
WEB_SCHEMES = ("http://", "https://")
_SCHEME = re.compile(r"^[A-Za-z][A-Za-z0-9+.\-]*://")
def validate_url(value: str) -> bool:
return value.lower().startswith(WEB_SCHEMES) and validators.url(value) is True
def normalize_target_value(value: str) -> str:
"""What a user typed, reduced to the form reNgine validates and stores."""
v = (value or "").strip()
if not v or _SCHEME.match(v):
return v
v = v.removeprefix("*.")
return v.rstrip("/").rstrip(".")
TARGET_FORMAT_HINT = "Enter a domain, IP address, CIDR range, URL or ASN."
_MAX_ECHO = 120
def unrecognised_target(value: str) -> str:
"""The one wording for a target value reNgine cannot read."""
shown = (value or "").strip()[:_MAX_ECHO]
return f"Unrecognised target: {shown}. {TARGET_FORMAT_HINT}"
def validate_target(target_value: str) -> TargetType | None:
value = normalize_target_value(target_value)
if not value:
return None
_validators = [
(TargetType.IP_RANGE, validate_ip_range),
(TargetType.IP, validate_ip),
(TargetType.URL, validate_url),
(TargetType.ASN, validate_asn),
(TargetType.DOMAIN, validate_domain),
]
for target_type, validator_func in _validators:
if validator_func(value):
return target_type
return None
def normalize_query(query: str, target_type: TargetType) -> str:
match target_type:
case TargetType.DOMAIN:
return normalize_domain(query)
case TargetType.URL:
return normalize_domain(extract_domain_from_url(query))
case TargetType.ASN:
return str(extract_asn_number(query))
case _:
return query.strip()
def normalize_domain(domain: str) -> str:
domain = domain.strip().lower()
domain = re.sub(r"^https?://", "", domain)
domain = domain.split("/")[0].split("?")[0].split("#")[0].split(":")[0]
return domain.rstrip(".")
def extract_asn_number(query: str) -> int:
cleaned = re.sub(r"^[Aa][Ss]", "", query.strip())
return int(cleaned)
def extract_domain_from_url(url: str) -> str:
return normalize_domain(url)
def validate_hex_color(color: str) -> str:
color = color.strip()
if not color.startswith("#"):
msg = "Color must start with #"
raise ValueError(msg)
if len(color) != HEX_COLOR_LENGTH:
msg = f"Color must be in format #RRGGBB ({HEX_COLOR_LENGTH} characters)"
raise ValueError(msg)
hex_pattern = r"^#[0-9A-Fa-f]{6}$"
if not re.match(hex_pattern, color):
msg = "Color must contain valid hexadecimal characters (0-9, A-F)"
raise ValueError(msg)
return color.upper()