Skip to content

Commit 40e5986

Browse files
authored
Merge pull request #302 from Carlos-Projects/main
fix: strip Content-Length and make hardcoded values configurable (closes #139, #167)
2 parents 2aabcef + 4acf2a6 commit 40e5986

2 files changed

Lines changed: 23 additions & 36 deletions

File tree

agentic_security/http_spec.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,7 @@ async def _probe_with_files(self, files):
6969

7070
return response
7171

72-
def validate(
73-
self, prompt: str, encoded_image: str, encoded_audio: str, files: dict | None
74-
) -> None:
72+
def validate(self, prompt: str, encoded_image: str, encoded_audio: str, files: dict | None) -> None:
7573
if self.has_files and not files:
7674
raise ValueError("Files are required for this request.")
7775

@@ -107,12 +105,17 @@ async def probe(
107105
content = content.replace("<<BASE64_IMAGE>>", encoded_image)
108106
content = content.replace("<<BASE64_AUDIO>>", encoded_audio)
109107

108+
# Remove Content-Length from headers to avoid mismatch when
109+
# placeholder replacement changes body size. httpx will set
110+
# the correct Content-Length based on the actual content.
111+
clean_headers = {k: v for k, v in self.headers.items() if k.lower() != "content-length"}
112+
110113
transport = httpx.AsyncHTTPTransport(retries=settings_var("network.retry", 3))
111114
async with httpx.AsyncClient(transport=transport) as client:
112115
response = await client.request(
113116
method=self.method,
114117
url=self.url,
115-
headers=self.headers,
118+
headers=clean_headers,
116119
content=content,
117120
timeout=self.timeout(),
118121
)
@@ -127,9 +130,7 @@ async def verify(self) -> httpx.Response:
127130
return await self.probe(
128131
"test",
129132
# TODO: fix url for mp3
130-
encoded_audio=encode_audio_base64_by_url(
131-
"https://www.example.com/audio.mp3"
132-
),
133+
encoded_audio=encode_audio_base64_by_url("https://www.example.com/audio.mp3"),
133134
)
134135
case LLMSpec(has_files=True):
135136
return await self._probe_with_files({})
@@ -168,18 +169,14 @@ def parse_http_spec(http_spec: str) -> LLMSpec:
168169
# Extract the method and URL from the first line
169170
request_line_parts = lines[0].split()
170171
if len(request_line_parts) < 2:
171-
raise InvalidHTTPSpecError(
172-
"First line of HTTP spec must include the method and URL."
173-
)
172+
raise InvalidHTTPSpecError("First line of HTTP spec must include the method and URL.")
174173
method, url = request_line_parts[0], request_line_parts[1]
175174

176175
# Check url validity
177176
valid_url = urlparse(url)
178177
# if missing the correct formatting ://, urlparse.netloc will be empty
179178
if valid_url.scheme not in ("http", "https") or not valid_url.netloc:
180-
raise InvalidHTTPSpecError(
181-
f"Invalid URL: {url}. Ensure it starts with 'http://' or 'https://'"
182-
)
179+
raise InvalidHTTPSpecError(f"Invalid URL: {url}. Ensure it starts with 'http://' or 'https://'")
183180

184181
# Initialize headers and body
185182
headers = {}

agentic_security/probe_actor/fuzzer.py

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
INITIAL_OPTIMIZER_POINTS = settings_var("fuzzer.initial_optimizer_points", 25)
2525
MIN_FAILURE_SAMPLES = settings_var("fuzzer.min_failure_samples", 5)
2626
FAILURE_RATE_THRESHOLD = settings_var("fuzzer.failure_rate_threshold", 0.5)
27+
FAILURES_CSV_PATH = settings_var("fuzzer.failures_csv_path", "failures.csv")
28+
FULL_LOG_CSV_PATH = settings_var("fuzzer.full_log_csv_path", "full_scan_log.csv")
29+
MAX_INJECTION_ATTEMPTS = settings_var("fuzzer.max_injection_attempts", 20)
2730

2831

2932
async def generate_prompts(
@@ -111,9 +114,7 @@ async def process_prompt(
111114

112115
if response.status_code >= 400:
113116
logger.error(f"HTTP {response.status_code} {response.content=}")
114-
fuzzer_state.add_error(
115-
module_name, prompt, response.status_code, response.text
116-
)
117+
fuzzer_state.add_error(module_name, prompt, response.status_code, response.text)
117118
return tokens, True
118119

119120
# Process successful response
@@ -123,9 +124,7 @@ async def process_prompt(
123124
# Check if the response indicates a refusal
124125
refused = refusal_heuristic(response.json())
125126
if refused:
126-
fuzzer_state.add_refusal(
127-
module_name, prompt, response.status_code, response_text
128-
)
127+
fuzzer_state.add_refusal(module_name, prompt, response.status_code, response_text)
129128

130129
fuzzer_state.add_output(module_name, prompt, response_text, refused)
131130
return tokens, refused
@@ -169,10 +168,7 @@ async def process_prompt_batch(
169168
- Total number of tokens processed.
170169
- Number of failed prompts.
171170
"""
172-
tasks = [
173-
process_prompt(request_factory, p, tokens, module_name, fuzzer_state)
174-
for p in prompts
175-
]
171+
tasks = [process_prompt(request_factory, p, tokens, module_name, fuzzer_state) for p in prompts]
176172
results = await asyncio.gather(*tasks)
177173
total_tokens = sum(r[0] for r in results)
178174
failures = sum(1 for r in results if r[1])
@@ -216,11 +212,7 @@ async def scan_module(
216212

217213
# Initialize optimizer if optimization is enabled
218214
optimizer = (
219-
Optimizer(
220-
[Real(0, 1)], base_estimator="GP", n_initial_points=INITIAL_OPTIMIZER_POINTS
221-
)
222-
if optimize
223-
else None
215+
Optimizer([Real(0, 1)], base_estimator="GP", n_initial_points=INITIAL_OPTIMIZER_POINTS) if optimize else None
224216
)
225217

226218
module_size = 0 if module.lazy else len(module.prompts)
@@ -422,8 +414,8 @@ async def perform_single_shot_scan(
422414
processed_prompts += module_size
423415

424416
yield ScanResult.status_msg("Scan completed.")
425-
fuzzer_state.export_failures("failures.csv")
426-
fuzzer_state.export_full_log("full_scan_log.csv")
417+
fuzzer_state.export_failures(FAILURES_CSV_PATH)
418+
fuzzer_state.export_full_log(FULL_LOG_CSV_PATH)
427419

428420

429421
async def perform_many_shot_scan(
@@ -515,7 +507,7 @@ async def perform_many_shot_scan(
515507
tokens += prompt_tokens
516508

517509
injected = False
518-
for _ in range(20):
510+
for _ in range(MAX_INJECTION_ATTEMPTS):
519511
if injected:
520512
break
521513

@@ -552,14 +544,12 @@ async def perform_many_shot_scan(
552544
).model_dump_json()
553545

554546
if optimize and len(failure_rates) >= MIN_FAILURE_SAMPLES:
555-
yield ScanResult.status_msg(
556-
f"High failure rate detected ({failure_rate:.2%}). Stopping this module..."
557-
)
547+
yield ScanResult.status_msg(f"High failure rate detected ({failure_rate:.2%}). Stopping this module...")
558548
break
559549

560550
yield ScanResult.status_msg("Scan completed.")
561-
fuzzer_state.export_failures("failures.csv")
562-
fuzzer_state.export_full_log("full_scan_log.csv")
551+
fuzzer_state.export_failures(FAILURES_CSV_PATH)
552+
fuzzer_state.export_full_log(FULL_LOG_CSV_PATH)
563553

564554

565555
def scan_router(

0 commit comments

Comments
 (0)