"""Scope validation utilities for ensuring authorized testing."""
import ipaddress
import re
from typing import List, Optional, Tuple
from urllib.parse import urlparse
from ..models import BugBountyProgram, ScopeRule, ScopeType
class ScopeValidator:
"""Validates targets against program scope rules."""
def __init__(self, program: BugBountyProgram):
"""Initialize validator with a program.
Args:
program: The bug bounty program to validate against
"""
self.program = program
def validate_target(self, target: str) -> Tuple[bool, str]:
"""Validate if a target is in scope.
Args:
target: The target to validate (domain, IP, URL, etc.)
Returns:
Tuple of (is_valid, reason)
"""
# Check if program is enrolled
if not self.program.enrolled:
return False, f"Program '{self.program.name}' is not enrolled. Cannot test unenrolled programs."
# Normalize target
normalized = self._normalize_target(target)
target_type = self._detect_target_type(normalized)
# Check out of scope first
for rule in self.program.out_of_scope:
if self._matches_rule(normalized, rule, target_type):
return False, f"Target is explicitly out of scope: {rule.target}"
# Check in scope
for rule in self.program.in_scope:
if self._matches_rule(normalized, rule, target_type):
return True, f"Target matches in-scope rule: {rule.target}"
return False, "Target does not match any in-scope rules"
def _normalize_target(self, target: str) -> str:
"""Normalize target string for comparison."""
target = target.strip().lower()
# If it's a URL, extract the domain/host
if target.startswith(('http://', 'https://')):
parsed = urlparse(target)
return parsed.netloc or parsed.path
return target
def _detect_target_type(self, target: str) -> ScopeType:
"""Detect the type of target."""
# Check if IP address or CIDR
try:
ipaddress.ip_address(target)
return ScopeType.IP_RANGE
except ValueError:
pass
# Check if CIDR notation
if '/' in target:
try:
ipaddress.ip_network(target, strict=False)
return ScopeType.IP_RANGE
except ValueError:
pass
# Default to domain
return ScopeType.DOMAIN
def _matches_rule(self, target: str, rule: ScopeRule, target_type: ScopeType) -> bool:
"""Check if target matches a scope rule."""
if rule.type != target_type:
return False
if rule.type == ScopeType.DOMAIN:
return self._matches_domain_rule(target, rule)
elif rule.type == ScopeType.IP_RANGE:
return self._matches_ip_rule(target, rule)
# For other types, do exact match
return target == rule.target.lower()
def _matches_domain_rule(self, target: str, rule: ScopeRule) -> bool:
"""Check if domain matches rule with wildcard support."""
rule_target = rule.target.lower()
# Handle wildcard
if rule_target.startswith('*.'):
# Wildcard subdomain matching
base_domain = rule_target[2:] # Remove *.
# Target must end with the base domain
if target.endswith(base_domain):
return True
# Or be a subdomain of it
if target.endswith('.' + base_domain):
return True
return False
else:
# Exact match
return target == rule_target
def _matches_ip_rule(self, target: str, rule: ScopeRule) -> bool:
"""Check if IP matches rule with CIDR support."""
try:
target_ip = ipaddress.ip_address(target)
# Check if rule is CIDR
if '/' in rule.target:
network = ipaddress.ip_network(rule.target, strict=False)
return target_ip in network
else:
# Exact IP match
rule_ip = ipaddress.ip_address(rule.target)
return target_ip == rule_ip
except ValueError:
return False
def validate_url(self, url: str) -> Tuple[bool, str]:
"""Validate a full URL against scope.
Args:
url: Full URL to validate
Returns:
Tuple of (is_valid, reason)
"""
try:
parsed = urlparse(url)
host = parsed.netloc or parsed.path
return self.validate_target(host)
except Exception as e:
return False, f"Invalid URL format: {str(e)}"
def filter_in_scope_targets(self, targets: List[str]) -> List[str]:
"""Filter a list of targets to only in-scope ones.
Args:
targets: List of targets to filter
Returns:
List of in-scope targets
"""
in_scope = []
for target in targets:
is_valid, _ = self.validate_target(target)
if is_valid:
in_scope.append(target)
return in_scope
def get_scope_summary(self) -> dict:
"""Get a summary of the program scope.
Returns:
Dictionary containing scope information
"""
return {
'program_id': self.program.program_id,
'program_name': self.program.name,
'enrolled': self.program.enrolled,
'in_scope_count': len(self.program.in_scope),
'out_of_scope_count': len(self.program.out_of_scope),
'in_scope_rules': [
{'type': rule.type, 'target': rule.target, 'notes': rule.notes}
for rule in self.program.in_scope
],
'out_of_scope_rules': [
{'type': rule.type, 'target': rule.target, 'notes': rule.notes}
for rule in self.program.out_of_scope
]
}