Add source files: generator, validator, fuzzer modules
Some checks failed
CI / test (push) Failing after 14s

This commit is contained in:
2026-02-02 19:55:35 +00:00
parent 730c2f7ab8
commit 81f3b49890

361
src/core/fuzzer.py Normal file
View File

@@ -0,0 +1,361 @@
"""Fuzzer for generating edge case responses."""
import random
from typing import Any
class FuzzerError(Exception):
"""Base exception for fuzzer errors."""
pass
class Fuzzer:
"""Generates edge case data for resilience testing."""
NULL_VALUES = [None, "", [], {}, "null", "NULL", "Null"]
INVALID_STRINGS = [
"",
" ",
"\n\t",
"undefined",
"NaN",
"Infinity",
"-Infinity",
"true",
"false",
"123abc",
"<script>alert('xss')</script>",
"' OR '1'='1",
"../../../etc/passwd",
"\x00\x00\x00",
]
INVALID_NUMBERS = [
0,
-1,
-9999999,
9999999,
float("inf"),
float("-inf"),
float("nan"),
1e308,
-1e308,
]
BOUNDARY_VALUES = [
-2147483649,
-2147483648,
2147483647,
2147483648,
-32769,
-32768,
32767,
32768,
-1,
0,
1,
]
ARRAY_BOUNDARIES = [
[],
[None],
[None] * 100,
[{}] * 50,
[{"a": None}] * 25,
]
def __init__(self, seed: int | None = None) -> None:
"""Initialize the fuzzer.
Args:
seed: Optional seed for reproducible fuzzing.
"""
if seed is not None:
random.seed(seed)
def fuzz_string(self, schema: dict[str, Any]) -> Any:
"""Fuzz a string field with edge cases.
Args:
schema: String schema definition.
Returns:
Fuzzed string value.
"""
pattern = schema.get("pattern")
if pattern and random.random() < 0.3:
return random.choice(self.INVALID_STRINGS)
min_length = schema.get("minLength", 0)
max_length = schema.get("maxLength", 100)
if min_length > 0 and random.random() < 0.2:
return ""
if random.random() < 0.1:
return random.choice(self.INVALID_STRINGS)
if random.random() < 0.05:
return "a" * (max_length + 1)
length = max(0, min_length)
return "x" * length
def fuzz_integer(self, schema: dict[str, Any]) -> int:
"""Fuzz an integer field with boundary values.
Args:
schema: Integer schema definition.
Returns:
Fuzzed integer value.
"""
minimum = schema.get("minimum", -2147483648)
maximum = schema.get("maximum", 2147483647)
multiple_of = schema.get("multipleOf", 1)
if random.random() < 0.2:
if random.random() < 0.5:
return minimum - 1
else:
return maximum + 1
if random.random() < 0.15:
return 0
if random.random() < 0.1:
return random.choice(self.BOUNDARY_VALUES)
value = random.randint(minimum, min(maximum, minimum + 1000))
if multiple_of > 1:
value = (value // multiple_of) * multiple_of
return value
def fuzz_number(self, schema: dict[str, Any]) -> float:
"""Fuzz a number field with special values.
Args:
schema: Number schema definition.
Returns:
Fuzzed number value.
"""
if random.random() < 0.2:
return random.choice(self.INVALID_NUMBERS)
minimum = schema.get("minimum", 0.0)
maximum = schema.get("maximum", 1000.0)
value = random.uniform(minimum, maximum)
if random.random() < 0.1:
value = round(value, 10)
return value
def fuzz_boolean(self, schema: dict[str, Any]) -> Any:
"""Fuzz a boolean field with invalid values.
Args:
schema: Boolean schema definition.
Returns:
Fuzzed value (not necessarily boolean).
"""
if random.random() < 0.3:
return random.choice([0, 1, "true", "false", "yes", "no", ""])
return random.choice([True, False])
def fuzz_array(self, schema: dict[str, Any]) -> Any:
"""Fuzz an array field with edge cases.
Args:
schema: Array schema definition.
Returns:
Fuzzed array value.
"""
min_items = schema.get("minItems", 0)
max_items = schema.get("maxItems", 10)
if random.random() < 0.2:
return random.choice(self.ARRAY_BOUNDARIES)
if min_items > 0 and random.random() < 0.2:
return []
count = random.randint(0, max(min_items, max_items + 5))
return [None] * count
def fuzz_object(self, schema: dict[str, Any]) -> dict[str, Any] | None:
"""Fuzz an object field with edge cases.
Args:
schema: Object schema definition.
Returns:
Fuzzed object value.
"""
properties = schema.get("properties", {})
required_fields = schema.get("required", [])
additional_properties = schema.get("additionalProperties", True)
result: dict[str, Any] = {}
for prop_name, prop_schema in properties.items():
if prop_name in required_fields:
result[prop_name] = self.fuzz_schema(prop_schema)
elif additional_properties and random.random() < 0.5:
result[prop_name] = self.fuzz_schema(prop_schema)
if random.random() < 0.2:
result["__fuzz__"] = "injected"
if random.random() < 0.1:
return None
return result
def fuzz_null(self, schema: dict[str, Any]) -> Any:
"""Fuzz a null field.
Args:
schema: Null schema definition.
Returns:
Null value or injected null.
"""
if random.random() < 0.7:
return None
return random.choice(self.NULL_VALUES)
def fuzz_schema(self, schema: dict[str, Any]) -> Any:
"""Fuzz a complete schema with edge cases.
Args:
schema: JSON Schema definition.
Returns:
Fuzzed value matching the schema type.
"""
if not schema:
return None
if "$ref" in schema:
return self.fuzz_schema(schema.get("resolved", {}))
schema_type = schema.get("type", "string").lower()
if random.random() < 0.05:
return None
if schema_type == "string":
return self.fuzz_string(schema)
elif schema_type == "integer":
return self.fuzz_integer(schema)
elif schema_type == "number":
return self.fuzz_number(schema)
elif schema_type == "boolean":
return self.fuzz_boolean(schema)
elif schema_type == "array":
return self.fuzz_array(schema)
elif schema_type == "object":
return self.fuzz_object(schema)
elif schema_type == "null":
return self.fuzz_null(schema)
else:
return None
def fuzz_request_body(self, schema: dict[str, Any] | None) -> Any:
"""Fuzz a request body schema.
Args:
schema: Request body schema.
Returns:
Fuzzed request body.
"""
if schema is None:
return None
return self.fuzz_schema(schema)
def fuzz_response(
self, schema: dict[str, Any] | None, status_code: int = 200
) -> dict[str, Any]:
"""Generate a complete fuzzed response.
Args:
schema: Response schema.
status_code: Response status code.
Returns:
Fuzzed response dictionary.
"""
body = self.fuzz_schema(schema) if schema else None
if random.random() < 0.1:
body = None
return {
"status_code": status_code,
"body": body,
}
def inject_null_values(self, data: Any, depth: int = 0, max_depth: int = 3) -> Any:
"""Inject null values into data structure.
Args:
data: Data structure to inject nulls into.
depth: Current recursion depth.
max_depth: Maximum recursion depth.
Returns:
Data with injected nulls.
"""
if depth >= max_depth:
return data
if isinstance(data, dict):
result = {}
for key, value in data.items():
if random.random() < 0.1:
result[key] = None
else:
result[key] = self.inject_null_values(value, depth + 1, max_depth)
return result
elif isinstance(data, list):
return [
self.inject_null_values(item, depth + 1, max_depth)
for item in data
]
return data
def create_edge_case_response(
self, schema: dict[str, Any] | None = None
) -> dict[str, Any]:
"""Create an edge case response for testing.
Args:
schema: Optional schema to base the response on.
Returns:
Edge case response dictionary.
"""
if schema:
body = self.fuzz_schema(schema)
else:
body = {"message": "edge case test", "data": None}
return {
"status_code": random.choice([200, 400, 404, 500]),
"body": body,
}