Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 16 additions & 21 deletions src/ciberwebscan/core/attacks/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,32 +215,27 @@ async def send_request(
return None

def extract_forms(self, html: str) -> list[dict[str, Any]]:
"""Extract forms from HTML response."""
forms = []
"""Extract forms from HTML response.

Uses the canonical extractor from core.scraping.extractor and normalizes
the output to use the 'inputs' key for backward compatibility with
attack modules (xss, sqli, csrf, traversal).
"""
try:
from ciberwebscan.core.scraping.extractor import (
extract_forms as _extract_forms,
)

soup = BeautifulSoup(html, "html.parser")
for form in soup.find_all("form"):
form_data = {
"action": form.get("action", ""),
"method": str(form.get("method", "GET")).upper(),
"inputs": [],
}

# Find all input fields
for input_field in form.find_all(["input", "textarea", "select"]):
field_data = {
"name": input_field.get("name", ""),
"type": input_field.get("type", "text"),
"value": input_field.get("value", ""),
}
form_data["inputs"].append(field_data)

forms.append(form_data)
raw_forms = _extract_forms(soup)
# Normalize: 'fields' → 'inputs' for attack module compatibility
for form in raw_forms:
form["inputs"] = form.pop("fields", [])
return raw_forms

except Exception as e:
self.logger.debug(f"Error extracting forms: {e}")

return forms
return []

def should_test_parameter(self, param_name: str) -> bool:
"""Check if parameter should be tested based on name."""
Expand Down
51 changes: 49 additions & 2 deletions src/ciberwebscan/core/scraping/extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,8 +434,12 @@ def extract_forms(
"fields": [],
}

# Extract input fields
for inp in form.select("input, select, textarea"):
# Extract input, select, textarea, and button elements
for inp in form.select("input, select, textarea, button"):
# Skip buttons without name (they don't submit form data)
if inp.name == "button" and not inp.get("name"):
continue

field_data: dict[str, Any] = {
"tag": inp.name,
"type": inp.get("type", "text") if inp.name == "input" else inp.name,
Expand All @@ -444,18 +448,61 @@ def extract_forms(
"id": inp.get("id", ""),
"placeholder": inp.get("placeholder", ""),
}

# Extract label association
field_data["label"] = _resolve_label(form, inp)

# Extract validation attributes
field_data["required"] = inp.has_attr("required")
field_data["pattern"] = inp.get("pattern", "")
field_data["maxlength"] = inp.get("maxlength", "")
field_data["minlength"] = inp.get("minlength", "")
field_data["readonly"] = inp.has_attr("readonly")
field_data["disabled"] = inp.has_attr("disabled")

# Extract select options
if inp.name == "select":
field_data["options"] = [
opt.get("value", opt.get_text(strip=True))
for opt in inp.select("option")
]

form_data["fields"].append(field_data)

forms.append(form_data)

return forms


def _resolve_label(form: Any, inp: Any) -> str:
"""Resolve the label text associated with a form field.

Checks two strategies:
1. <label for="fieldId"> matching the field's id attribute
2. <label> wrapping the field element directly

Args:
form: The parent form BeautifulSoup element.
inp: The input/select/textarea/button BeautifulSoup element.

Returns:
Associated label text, or empty string if none found.
"""
# Strategy 1: label[for="id"] matching field's id
field_id = inp.get("id", "")
if field_id:
label = form.select_one(f'label[for="{field_id}"]')
if label:
return label.get_text(strip=True)

# Strategy 2: label wrapping the field element
parent_label = inp.find_parent("label")
if parent_label:
return parent_label.get_text(strip=True)

return ""


def extract_scripts(
soup: BeautifulSoup,
*,
Expand Down
4 changes: 3 additions & 1 deletion src/ciberwebscan/export/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,10 @@ class FormInfo(BaseModel):

action: str = ""
method: str = "GET"
enctype: str = ""
id: str = ""
name: str = ""
fields: list[dict[str, str]] = Field(default_factory=list)
fields: list[dict[str, Any]] = Field(default_factory=list)


class ScriptInfo(BaseModel):
Expand Down
4 changes: 4 additions & 0 deletions src/ciberwebscan/services/scrape_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,8 @@ def _scrape_static(self, url: str, options: ScrapeOptions) -> ScrapeResult:
FormInfo(
action=f.get("action", ""),
method=f.get("method", "GET"),
enctype=f.get("enctype", ""),
id=f.get("id", ""),
name=f.get("name", ""),
fields=f.get("fields", []),
)
Expand Down Expand Up @@ -479,6 +481,8 @@ def _scrape_dynamic(self, url: str, options: ScrapeOptions) -> ScrapeResult:
FormInfo(
action=f.get("action", ""),
method=f.get("method", "GET"),
enctype=f.get("enctype", ""),
id=f.get("id", ""),
name=f.get("name", ""),
fields=f.get("fields", []),
)
Expand Down
Loading