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
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: CI Pipeline

on:
push:
branches: ["*"] # Runs on all branches
branches: ["*"] # Runs on all branches
pull_request:
branches: ["*"] # Runs on all pull requests
branches: ["*"] # Runs on all pull requests

jobs:
tests:
Expand All @@ -18,7 +18,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.8"
python-version: "3.9"

- name: Install dependencies
run: |
Expand All @@ -27,4 +27,4 @@ jobs:
pip install pytest pytest-cov

- name: Run Tests with Coverage
run: pytest --cov=src --cov-report=term --cov-fail-under=75
run: pytest --cov=src --cov-report=term --cov-fail-under=75
20 changes: 3 additions & 17 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,13 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.8"

- name: Cache Python dependencies
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
python-version: "3.9"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install bandit pip-audit
pip install bandit

- name: Run Bandit Security Scan
id: bandit
continue-on-error: true
run: bandit -r src/pentestkit/ --verbose

- name: Run pip-audit
id: pip-audit
continue-on-error: true
run: pip-audit -r requirements.txt
run: bandit -c bandit.yaml -r src/pentestkit/ --verbose
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,7 @@ cython_debug/
.pypirc

#VSCode
.vscode
.vscode

# .ipynb files
*.ipynb
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include requirements.txt
include VERSION
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,5 @@ This project is licensed under the **Apache 2.0 License**. See [LICENSE](LICENSE

## Additional Documentation
For more detailed information, please refer to the following documents:
- [API Reference](docs/API_REFERENCE.md): Comprehensive guide to the API endpoints and their usage.
- [Changelog](docs/CHANGELOG.md): A log of all the changes, updates, and fixes made to the project.
- [Contributing Guide](docs/CONTRIBUTING.md): Guidelines for contributing to the project, including how to report issues and submit code changes.
10 changes: 10 additions & 0 deletions bandit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Bandit configuration file

# Skip warnings for specific tests
skips:
- B311 # Skip B311 which warns about `random` usage
- B101 # Skip B101 which warns about assert statements in tests

#Additional configurations
exclude_dirs:
- tests # Exclude test directories as they often contain intentional test patterns
30 changes: 30 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# API Reference

This document provides detailed documentation for the pentest library.

## Table of Contents

- [Core Components](#core-components)
- [Parser Module](#parser-module)

## Core Components

### Parser Module

The parser module handles parsing OpenAPI/Swagger specifications.

#### `OpenAPIParser`

```python
from pentestkit.parser import OpenAPIParser

parser = OpenAPIParser(source="swagger.json")
endpoints = parser.parse("swagger.json")
base_url = parser.get_base_url()
```

**Methods:**

- `parse(source: str) -> List[Endpoint]`: Parse API specification from a URL or file path
- `get_format() -> SpecFormat`: Get the format of the specification (OpenAPI v2 or v3)
- `get_base_url() -> Optional[str]`: Get the base URL from the specification
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
requests==2.32.3
urllib3>=2.2.3
urllib3==2.4.0
rstr==3.2.2
11 changes: 10 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,13 @@ omit =

[coverage:report]
exclude_lines =
pass
pass

[mypy]
warn_unused_configs = True

[mypy-pytest.*]
ignore_missing_imports = True

[mypy-pentestkit.*]
ignore_missing_imports = True
221 changes: 217 additions & 4 deletions src/pentestkit/dataclasses/body.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,230 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import random
from ..utils import generate_test_value
from dataclasses import dataclass, field
from typing import Any, Dict, Optional
from typing import Any, Optional, Union


@dataclass
class RequestBody:
"""Represents a request body."""

content_type: str
schema: Dict[str, Any]
schema: dict[str, Any]
required: bool = False
example: Optional[Any] = None
properties: Dict[str, Any] = field(default_factory=dict)
resolved_schema: Dict[str, Any] = field(default_factory=dict)
properties: dict[str, Any] = field(default_factory=dict)
resolved_schema: dict[str, Any] = field(default_factory=dict)

def __post_init__(self):
"""Generates example values if not provided"""
if self.example is None:
self.example = generate_example_body(self)


def generate_example_body(request_body: RequestBody) -> Any:
"""
Generate an example body based on the request body's content type.

Args:
request_body (RequestBody): The request body object.

Returns:
Any: A suitable example value for the request body based on its content type.
"""

content_type_handlers = {
"application/json": lambda: generate_json_example(request_body.properties),
}

handler = content_type_handlers.get(request_body.content_type)
return handler() if handler else None


def generate_json_example(properties: dict[str, Any]) -> dict[str, Any]:
"""
Generate a JSON example based on property schemas.

Args:
properties (dict[str, Any]): A dictionary mapping property names to their schemas.

Returns:
dict[str, Any]: A dictionary containing example values for each property.
"""
result = {}
for prop_name, prop_schema in properties.items():
# Handle anyOf case
if "anyOf" in prop_schema:
for schema_option in prop_schema["anyOf"]:
if schema_option.get("type") != "null":
result[prop_name] = generate_property_value(schema_option)
break
continue

# Handle normal types
result[prop_name] = generate_property_value(prop_schema)

return result


def generate_property_value(schema: dict[str, Any]) -> Any:
"""
Generate an example value for a property based on its schema.

Args:
schema (dict[str, Any]): The schema definition of the property.

Returns:
Any: A suitable example value based on the property's type and constraints.
"""
try:
prop_type = schema.get("type", "string")

if prop_type == "string":
return generate_string_property(schema)
elif prop_type == "number" or prop_type == "integer":
return generate_numerical_property(schema, prop_type == "integer")
elif prop_type == "boolean":
return random.choice([True, False])
elif prop_type == "object":
return generate_object_property(schema)
elif prop_type == "array":
return generate_array_property(schema)

# Default fallback
return "example"
except:
return "example"


def generate_string_property(schema: dict[str, Any]) -> str:
"""
Generate an example string value based on schema constraints.

Args:
schema (dict[str, Any]): The schema definition for a string property.

Returns:
str: A string example that satisfies the schema constraints.
"""

for attr in ("examples", "enum", "example"):
if values := schema.get(attr):
return random.choice(values) if isinstance(values, list) else values

pattern = schema.get("pattern")
min_length = schema.get("minLength", 3)
max_length = schema.get("maxLength", 50)

if pattern:
return generate_test_value(pattern, min_length, max_length)

format_values = {
"date": "2025-04-23",
"date-time": "2025-04-23T14:30:00Z",
"email": "user@example.com",
"uri": "https://example.com/resource",
"hostname": "example.com",
"ipv4": "192.168.1.1",
"ipv6": "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
}

return format_values.get(schema.get("format", "none"), "example")


def generate_numerical_property(
schema: dict[str, Any], is_integer: bool
) -> Union[int, float]:
"""
Generate an example numerical value based on schema constraints.

Args:
schema (dict[str, Any]): The schema definition for a numerical property.
is_integer (bool): Whether the value should be an integer (True) or float (False).

Returns:
Union[int, float]: A numerical example that satisfies the schema constraints.
"""
try:

for attr in ("examples", "enum", "example"):
if values := schema.get(attr):

value = random.choice(values) if isinstance(values, list) else values
return int(value) if is_integer else float(value)

if "exclusiveMinimum" in schema:
minimum = schema["exclusiveMinimum"] + (1 if is_integer else 0.01)
else:
minimum = schema.get("minimum", 1)

if "exclusiveMaximum" in schema:
maximum = schema["exclusiveMaximum"] - (1 if is_integer else 0.01)
else:
maximum = schema.get("maximum", 100)

# Ensure min doesn't exceed max
if minimum > maximum:
minimum, maximum = maximum, minimum

if is_integer:
return random.randint(int(minimum), int(maximum))

multiple_of = schema.get("multipleOf")
if multiple_of:
value = random.uniform(float(minimum), float(maximum))
return round(value / multiple_of) * multiple_of

return round(random.uniform(float(minimum), float(maximum)), 2)

except (ValueError, TypeError):
return 1 if is_integer else 1.0


def generate_object_property(schema: dict[str, Any]) -> dict[str, Any]:
"""
Generate an example object based on schema definition.

Args:
schema (dict[str, Any]): The schema definition for an object property.

Returns:
dict[str, Any]: An object example that satisfies the schema definition.
"""

if "properties" in schema:
return generate_json_example(schema.get("properties", {}))
elif "additionalProperties" in schema and isinstance(
schema["additionalProperties"], dict
):
sample_props = {
"key1": generate_property_value(schema["additionalProperties"]),
"key2": generate_property_value(schema["additionalProperties"]),
}
return sample_props
return {"example": "object-value"}


def generate_array_property(schema: dict[str, Any]) -> list[Any]:
"""
Generate an example array based on schema definition.

Args:
schema (dict[str, Any]): The schema definition for an array property.

Returns:
list[Any]: An array example that satisfies the schema constraints including
min/max items and item schema.
"""

items_schema = schema.get("items", {})
min_items = schema.get("minItems", 1)
max_items = schema.get("maxItems", 3)
num_items = min(min_items, max_items)

result = []
for _ in range(num_items):
result.append(generate_property_value(items_schema))
return result
Loading