diff --git a/src/ciberwebscan/core/attacks/base.py b/src/ciberwebscan/core/attacks/base.py index a6ae652..38b5d30 100644 --- a/src/ciberwebscan/core/attacks/base.py +++ b/src/ciberwebscan/core/attacks/base.py @@ -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.""" diff --git a/src/ciberwebscan/core/scraping/extractor.py b/src/ciberwebscan/core/scraping/extractor.py index 5b14041..9408f03 100644 --- a/src/ciberwebscan/core/scraping/extractor.py +++ b/src/ciberwebscan/core/scraping/extractor.py @@ -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, @@ -444,11 +448,25 @@ 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) @@ -456,6 +474,35 @@ def extract_forms( return forms +def _resolve_label(form: Any, inp: Any) -> str: + """Resolve the label text associated with a form field. + + Checks two strategies: + 1.