trace-sec 0.0.0-stage → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +210 -2
- package/bin/trace.js +112 -0
- package/package.json +44 -4
- package/pyproject.toml +39 -0
- package/src/trace_engine/__init__.py +8 -0
- package/src/trace_engine/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/__pycache__/cli.cpython-311.pyc +0 -0
- package/src/trace_engine/__pycache__/doctor.cpython-311.pyc +0 -0
- package/src/trace_engine/__pycache__/interactive.cpython-311.pyc +0 -0
- package/src/trace_engine/__pycache__/verify.cpython-311.pyc +0 -0
- package/src/trace_engine/ai/__init__.py +7 -0
- package/src/trace_engine/ai/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/ai/__pycache__/base.cpython-311.pyc +0 -0
- package/src/trace_engine/ai/__pycache__/ollama.cpython-311.pyc +0 -0
- package/src/trace_engine/ai/__pycache__/planner.cpython-311.pyc +0 -0
- package/src/trace_engine/ai/base.py +23 -0
- package/src/trace_engine/ai/ollama.py +50 -0
- package/src/trace_engine/ai/planner.py +40 -0
- package/src/trace_engine/apm/__init__.py +18 -0
- package/src/trace_engine/apm/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/apm/__pycache__/builder.cpython-311.pyc +0 -0
- package/src/trace_engine/apm/__pycache__/edges.cpython-311.pyc +0 -0
- package/src/trace_engine/apm/__pycache__/model.cpython-311.pyc +0 -0
- package/src/trace_engine/apm/__pycache__/nodes.cpython-311.pyc +0 -0
- package/src/trace_engine/apm/__pycache__/serialization.cpython-311.pyc +0 -0
- package/src/trace_engine/apm/builder.py +208 -0
- package/src/trace_engine/apm/edges.py +25 -0
- package/src/trace_engine/apm/model.py +107 -0
- package/src/trace_engine/apm/nodes.py +27 -0
- package/src/trace_engine/apm/serialization.py +105 -0
- package/src/trace_engine/benchmark/__init__.py +5 -0
- package/src/trace_engine/benchmark/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/benchmark/__pycache__/owasp.cpython-311.pyc +0 -0
- package/src/trace_engine/benchmark/owasp.py +183 -0
- package/src/trace_engine/cli.py +1184 -0
- package/src/trace_engine/config/__init__.py +35 -0
- package/src/trace_engine/config/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/config/__pycache__/defaults.cpython-311.pyc +0 -0
- package/src/trace_engine/config/__pycache__/loader.cpython-311.pyc +0 -0
- package/src/trace_engine/config/__pycache__/settings.cpython-311.pyc +0 -0
- package/src/trace_engine/config/defaults.py +48 -0
- package/src/trace_engine/config/loader.py +64 -0
- package/src/trace_engine/config/settings.py +72 -0
- package/src/trace_engine/doctor.py +250 -0
- package/src/trace_engine/findings/__init__.py +15 -0
- package/src/trace_engine/findings/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/findings/__pycache__/correlate.cpython-311.pyc +0 -0
- package/src/trace_engine/findings/__pycache__/model.cpython-311.pyc +0 -0
- package/src/trace_engine/findings/__pycache__/recommendations.cpython-311.pyc +0 -0
- package/src/trace_engine/findings/__pycache__/store.cpython-311.pyc +0 -0
- package/src/trace_engine/findings/correlate.py +103 -0
- package/src/trace_engine/findings/model.py +40 -0
- package/src/trace_engine/findings/recommendations.py +35 -0
- package/src/trace_engine/findings/store.py +39 -0
- package/src/trace_engine/framework/__init__.py +58 -0
- package/src/trace_engine/framework/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/base.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/csharp.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/dart.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/django.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/express.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/fastapi.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/flask.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/go.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/nextjs.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/php.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/react_router.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/ruby.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/rust.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/springboot.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/base.py +49 -0
- package/src/trace_engine/framework/csharp.py +111 -0
- package/src/trace_engine/framework/dart.py +152 -0
- package/src/trace_engine/framework/django.py +188 -0
- package/src/trace_engine/framework/express.py +82 -0
- package/src/trace_engine/framework/fastapi.py +135 -0
- package/src/trace_engine/framework/flask.py +108 -0
- package/src/trace_engine/framework/go.py +94 -0
- package/src/trace_engine/framework/nextjs.py +200 -0
- package/src/trace_engine/framework/php.py +98 -0
- package/src/trace_engine/framework/react_router.py +331 -0
- package/src/trace_engine/framework/ruby.py +69 -0
- package/src/trace_engine/framework/rust.py +101 -0
- package/src/trace_engine/framework/springboot.py +145 -0
- package/src/trace_engine/harness/__init__.py +19 -0
- package/src/trace_engine/harness/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/harness/__pycache__/benchmark.cpython-311.pyc +0 -0
- package/src/trace_engine/harness/__pycache__/context.cpython-311.pyc +0 -0
- package/src/trace_engine/harness/__pycache__/engine.cpython-311.pyc +0 -0
- package/src/trace_engine/harness/__pycache__/patcher.cpython-311.pyc +0 -0
- package/src/trace_engine/harness/__pycache__/remediators.cpython-311.pyc +0 -0
- package/src/trace_engine/harness/benchmark.py +55 -0
- package/src/trace_engine/harness/context.py +49 -0
- package/src/trace_engine/harness/engine.py +187 -0
- package/src/trace_engine/harness/patcher.py +143 -0
- package/src/trace_engine/harness/remediators.py +307 -0
- package/src/trace_engine/ingest/__init__.py +15 -0
- package/src/trace_engine/ingest/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/ingest/__pycache__/files.cpython-311.pyc +0 -0
- package/src/trace_engine/ingest/__pycache__/hashing.cpython-311.pyc +0 -0
- package/src/trace_engine/ingest/__pycache__/ignore.cpython-311.pyc +0 -0
- package/src/trace_engine/ingest/__pycache__/repository.cpython-311.pyc +0 -0
- package/src/trace_engine/ingest/files.py +60 -0
- package/src/trace_engine/ingest/hashing.py +21 -0
- package/src/trace_engine/ingest/ignore.py +108 -0
- package/src/trace_engine/ingest/repository.py +50 -0
- package/src/trace_engine/intelligence/__init__.py +15 -0
- package/src/trace_engine/intelligence/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/__pycache__/evaluation.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/__pycache__/orchestrator.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/__pycache__/tracebench.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/evaluation.py +828 -0
- package/src/trace_engine/intelligence/laya/__init__.py +19 -0
- package/src/trace_engine/intelligence/laya/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/laya/__pycache__/prompts.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/laya/__pycache__/router.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/laya/__pycache__/schemas.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/laya/__pycache__/telemetry.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/laya/__pycache__/thresholds.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/laya/prompts.py +67 -0
- package/src/trace_engine/intelligence/laya/router.py +352 -0
- package/src/trace_engine/intelligence/laya/schemas.py +56 -0
- package/src/trace_engine/intelligence/laya/telemetry.py +48 -0
- package/src/trace_engine/intelligence/laya/thresholds.py +12 -0
- package/src/trace_engine/intelligence/orchestrator.py +130 -0
- package/src/trace_engine/intelligence/securebert/__init__.py +6 -0
- package/src/trace_engine/intelligence/securebert/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/securebert/__pycache__/cache.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/securebert/__pycache__/classifier.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/securebert/cache.py +37 -0
- package/src/trace_engine/intelligence/securebert/classifier.py +240 -0
- package/src/trace_engine/intelligence/tracebench.py +61 -0
- package/src/trace_engine/intelligence/training/__init__.py +21 -0
- package/src/trace_engine/intelligence/training/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/training/__pycache__/dataset.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/training/__pycache__/dataset_importers.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/training/__pycache__/laya_trainer.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/training/__pycache__/lora_system2.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/training/__pycache__/morefixes_pipeline.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/training/__pycache__/slicer.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/training/__pycache__/train_all.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/training/__pycache__/trainer.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/training/dataset.py +320 -0
- package/src/trace_engine/intelligence/training/dataset_importers.py +349 -0
- package/src/trace_engine/intelligence/training/laya_trainer.py +678 -0
- package/src/trace_engine/intelligence/training/lora_system2.py +162 -0
- package/src/trace_engine/intelligence/training/morefixes_pipeline.py +463 -0
- package/src/trace_engine/intelligence/training/slicer.py +129 -0
- package/src/trace_engine/intelligence/training/train_all.py +1009 -0
- package/src/trace_engine/intelligence/training/trainer.py +321 -0
- package/src/trace_engine/interactive.py +623 -0
- package/src/trace_engine/mcp/__init__.py +6 -0
- package/src/trace_engine/mcp/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/mcp/__pycache__/config.cpython-311.pyc +0 -0
- package/src/trace_engine/mcp/__pycache__/server.cpython-311.pyc +0 -0
- package/src/trace_engine/mcp/config.py +42 -0
- package/src/trace_engine/mcp/server.py +398 -0
- package/src/trace_engine/output/__init__.py +28 -0
- package/src/trace_engine/output/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/output/__pycache__/html.cpython-311.pyc +0 -0
- package/src/trace_engine/output/__pycache__/markdown.cpython-311.pyc +0 -0
- package/src/trace_engine/output/__pycache__/sarif.cpython-311.pyc +0 -0
- package/src/trace_engine/output/__pycache__/terminal.cpython-311.pyc +0 -0
- package/src/trace_engine/output/html.py +142 -0
- package/src/trace_engine/output/markdown.py +65 -0
- package/src/trace_engine/output/sarif.py +180 -0
- package/src/trace_engine/output/terminal.py +537 -0
- package/src/trace_engine/parsing/__init__.py +26 -0
- package/src/trace_engine/parsing/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/parsing/__pycache__/calls.cpython-311.pyc +0 -0
- package/src/trace_engine/parsing/__pycache__/language.cpython-311.pyc +0 -0
- package/src/trace_engine/parsing/__pycache__/locations.cpython-311.pyc +0 -0
- package/src/trace_engine/parsing/__pycache__/parser.cpython-311.pyc +0 -0
- package/src/trace_engine/parsing/__pycache__/symbols.cpython-311.pyc +0 -0
- package/src/trace_engine/parsing/calls.py +14 -0
- package/src/trace_engine/parsing/language.py +24 -0
- package/src/trace_engine/parsing/locations.py +17 -0
- package/src/trace_engine/parsing/parser.py +465 -0
- package/src/trace_engine/parsing/symbols.py +45 -0
- package/src/trace_engine/plugin/__init__.py +157 -0
- package/src/trace_engine/plugin/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/plugin/__pycache__/evaluator.cpython-311.pyc +0 -0
- package/src/trace_engine/plugin/__pycache__/swebench_adapter.cpython-311.pyc +0 -0
- package/src/trace_engine/plugin/__pycache__/task.cpython-311.pyc +0 -0
- package/src/trace_engine/plugin/bundle/hooks.json +24 -0
- package/src/trace_engine/plugin/bundle/mcp_config.json +11 -0
- package/src/trace_engine/plugin/bundle/plugin.json +20 -0
- package/src/trace_engine/plugin/bundle/rules/security_remediation.md +40 -0
- package/src/trace_engine/plugin/bundle/skills/trace-security-harness/SKILL.md +118 -0
- package/src/trace_engine/plugin/evaluator.py +105 -0
- package/src/trace_engine/plugin/swebench_adapter.py +96 -0
- package/src/trace_engine/plugin/task.py +48 -0
- package/src/trace_engine/policy/__init__.py +5 -0
- package/src/trace_engine/policy/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/policy/__pycache__/scope.cpython-311.pyc +0 -0
- package/src/trace_engine/policy/scope.py +80 -0
- package/src/trace_engine/runtime/__init__.py +7 -0
- package/src/trace_engine/runtime/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/runtime/__pycache__/client.cpython-311.pyc +0 -0
- package/src/trace_engine/runtime/__pycache__/observations.cpython-311.pyc +0 -0
- package/src/trace_engine/runtime/__pycache__/target.cpython-311.pyc +0 -0
- package/src/trace_engine/runtime/client.py +81 -0
- package/src/trace_engine/runtime/observations.py +18 -0
- package/src/trace_engine/runtime/target.py +23 -0
- package/src/trace_engine/security/__init__.py +16 -0
- package/src/trace_engine/security/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/security/__pycache__/fusion.cpython-311.pyc +0 -0
- package/src/trace_engine/security/__pycache__/hypotheses.cpython-311.pyc +0 -0
- package/src/trace_engine/security/__pycache__/signals.cpython-311.pyc +0 -0
- package/src/trace_engine/security/__pycache__/timing.cpython-311.pyc +0 -0
- package/src/trace_engine/security/fusion.py +126 -0
- package/src/trace_engine/security/hypotheses.py +251 -0
- package/src/trace_engine/security/signals.py +28 -0
- package/src/trace_engine/security/timing.py +132 -0
- package/src/trace_engine/testpacks/__init__.py +18 -0
- package/src/trace_engine/testpacks/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/authentication.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/base.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/bfla.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/bola.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/cors.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/deserialization.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/injection.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/mass_assignment.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/path_traversal.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/registry.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/ssrf.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/ssti.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/authentication.py +60 -0
- package/src/trace_engine/testpacks/base.py +63 -0
- package/src/trace_engine/testpacks/bfla.py +70 -0
- package/src/trace_engine/testpacks/bola.py +94 -0
- package/src/trace_engine/testpacks/cors.py +85 -0
- package/src/trace_engine/testpacks/deserialization.py +86 -0
- package/src/trace_engine/testpacks/injection.py +179 -0
- package/src/trace_engine/testpacks/mass_assignment.py +70 -0
- package/src/trace_engine/testpacks/path_traversal.py +117 -0
- package/src/trace_engine/testpacks/registry.py +44 -0
- package/src/trace_engine/testpacks/ssrf.py +85 -0
- package/src/trace_engine/testpacks/ssti.py +96 -0
- package/src/trace_engine/tools/__init__.py +6 -0
- package/src/trace_engine/tools/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/tools/__pycache__/adapters.cpython-311.pyc +0 -0
- package/src/trace_engine/tools/__pycache__/base.cpython-311.pyc +0 -0
- package/src/trace_engine/tools/__pycache__/registry.cpython-311.pyc +0 -0
- package/src/trace_engine/tools/adapters.py +111 -0
- package/src/trace_engine/tools/base.py +68 -0
- package/src/trace_engine/tools/registry.py +36 -0
- package/src/trace_engine/verify.py +156 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Authentication bypass and missing authentication test pack."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from trace_engine.testpacks.base import TestPack, TestContext, TestExecutionResult
|
|
5
|
+
from trace_engine.security.hypotheses import SecurityHypothesis
|
|
6
|
+
from trace_engine.runtime.client import ScopedHttpClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AuthenticationTestPack(TestPack):
|
|
10
|
+
@property
|
|
11
|
+
def name(self) -> str:
|
|
12
|
+
return "authentication"
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def description(self) -> str:
|
|
16
|
+
return "Tests whether sensitive endpoints enforce authentication headers."
|
|
17
|
+
|
|
18
|
+
def execute(
|
|
19
|
+
self,
|
|
20
|
+
hypothesis: SecurityHypothesis,
|
|
21
|
+
client: ScopedHttpClient,
|
|
22
|
+
context: TestContext,
|
|
23
|
+
) -> TestExecutionResult:
|
|
24
|
+
base_url = context.target_base_url.rstrip("/")
|
|
25
|
+
parts = hypothesis.endpoint_display.split(" ")
|
|
26
|
+
method = parts[0] if len(parts) > 1 else "GET"
|
|
27
|
+
raw_path = parts[1] if len(parts) > 1 else parts[0]
|
|
28
|
+
|
|
29
|
+
# Replace any path param with dummy id
|
|
30
|
+
probe_path = re.sub(r"\{[a-zA-Z0-9_]+\}", "1", raw_path)
|
|
31
|
+
target_url = f"{base_url}{probe_path}"
|
|
32
|
+
|
|
33
|
+
# Send completely unauthenticated request
|
|
34
|
+
obs = client.execute(method=method, url=target_url, headers={})
|
|
35
|
+
observations = [obs]
|
|
36
|
+
|
|
37
|
+
confirmed = False
|
|
38
|
+
summary = "Authentication properly enforced (received 401/403)."
|
|
39
|
+
steps = []
|
|
40
|
+
|
|
41
|
+
if obs.response_status in (200, 201, 204):
|
|
42
|
+
confirmed = True
|
|
43
|
+
summary = (
|
|
44
|
+
f"Missing Authentication confirmed: Sensitive endpoint {raw_path} responded "
|
|
45
|
+
f"with {obs.response_status} to an unauthenticated request."
|
|
46
|
+
)
|
|
47
|
+
steps = [
|
|
48
|
+
f"Send unauthenticated {method} request to {target_url}",
|
|
49
|
+
f"Received HTTP {obs.response_status} rather than expected 401/403",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
return TestExecutionResult(
|
|
53
|
+
testpack_name=self.name,
|
|
54
|
+
hypothesis_id=hypothesis.id,
|
|
55
|
+
confirmed=confirmed,
|
|
56
|
+
confidence=0.90 if confirmed else 0.15,
|
|
57
|
+
summary=summary,
|
|
58
|
+
observations=observations,
|
|
59
|
+
reproduction_steps=steps,
|
|
60
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Base interfaces and context for TRACE test packs."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Dict, Any, Optional, List
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
from trace_engine.runtime.client import ScopedHttpClient
|
|
7
|
+
from trace_engine.runtime.observations import RuntimeObservation
|
|
8
|
+
from trace_engine.security.hypotheses import SecurityHypothesis
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UserIdentity(BaseModel):
|
|
12
|
+
"""Synthetic test identity credentials."""
|
|
13
|
+
username: str
|
|
14
|
+
password: str
|
|
15
|
+
token: Optional[str] = None
|
|
16
|
+
role: str = "user"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestContext(BaseModel):
|
|
20
|
+
"""Context provided to a test pack execution."""
|
|
21
|
+
__test__ = False
|
|
22
|
+
target_base_url: str
|
|
23
|
+
identities: Dict[str, UserIdentity] = Field(default_factory=dict)
|
|
24
|
+
active_tokens: Dict[str, str] = Field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestExecutionResult(BaseModel):
|
|
28
|
+
"""Outcome of running a test pack against a hypothesis."""
|
|
29
|
+
__test__ = False
|
|
30
|
+
testpack_name: str
|
|
31
|
+
hypothesis_id: str
|
|
32
|
+
confirmed: bool
|
|
33
|
+
confidence: float
|
|
34
|
+
summary: str
|
|
35
|
+
observations: List[RuntimeObservation] = Field(default_factory=list)
|
|
36
|
+
reproduction_steps: List[str] = Field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TestPack(ABC):
|
|
40
|
+
"""Abstract base class for all security test packs."""
|
|
41
|
+
__test__ = False
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def name(self) -> str:
|
|
46
|
+
"""Name of the test pack (e.g., 'bola', 'ssrf')."""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def description(self) -> str:
|
|
52
|
+
"""Brief description of what this pack validates."""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def execute(
|
|
57
|
+
self,
|
|
58
|
+
hypothesis: SecurityHypothesis,
|
|
59
|
+
client: ScopedHttpClient,
|
|
60
|
+
context: TestContext,
|
|
61
|
+
) -> TestExecutionResult:
|
|
62
|
+
"""Run the test pack against the given hypothesis using the scoped client."""
|
|
63
|
+
pass
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""BFLA (Broken Function Level Authorization) test pack."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from trace_engine.testpacks.base import TestPack, TestContext, TestExecutionResult
|
|
5
|
+
from trace_engine.security.hypotheses import SecurityHypothesis
|
|
6
|
+
from trace_engine.runtime.client import ScopedHttpClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BflaTestPack(TestPack):
|
|
10
|
+
@property
|
|
11
|
+
def name(self) -> str:
|
|
12
|
+
return "bfla"
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def description(self) -> str:
|
|
16
|
+
return "Validates whether administrative and privileged endpoints enforce proper RBAC."
|
|
17
|
+
|
|
18
|
+
def execute(
|
|
19
|
+
self,
|
|
20
|
+
hypothesis: SecurityHypothesis,
|
|
21
|
+
client: ScopedHttpClient,
|
|
22
|
+
context: TestContext,
|
|
23
|
+
) -> TestExecutionResult:
|
|
24
|
+
base_url = context.target_base_url.rstrip("/")
|
|
25
|
+
parts = hypothesis.endpoint_display.split(" ")
|
|
26
|
+
method = parts[0] if len(parts) > 1 else "POST"
|
|
27
|
+
raw_path = parts[1] if len(parts) > 1 else parts[0]
|
|
28
|
+
|
|
29
|
+
clean_path = re.sub(r"\{[a-zA-Z0-9_]+\}", "1", raw_path)
|
|
30
|
+
target_url = f"{base_url}{clean_path}"
|
|
31
|
+
token_user = context.active_tokens.get("user-a") or context.active_tokens.get("user-b", "")
|
|
32
|
+
|
|
33
|
+
observations = []
|
|
34
|
+
confirmed = False
|
|
35
|
+
summary = "No BFLA behavior observed."
|
|
36
|
+
steps = []
|
|
37
|
+
|
|
38
|
+
headers = {}
|
|
39
|
+
if token_user:
|
|
40
|
+
headers["Authorization"] = f"Bearer {token_user}"
|
|
41
|
+
|
|
42
|
+
# Probe with low-privileged token
|
|
43
|
+
test_payload = {"order_id": "order-101", "amount": 50, "reason": "test"}
|
|
44
|
+
obs = client.execute(
|
|
45
|
+
method=method, url=target_url, headers=headers, json_body=test_payload if method != "GET" else None
|
|
46
|
+
)
|
|
47
|
+
observations.append(obs)
|
|
48
|
+
|
|
49
|
+
# If HTTP 200 or 201 is returned rather than 403 Forbidden, privilege check is flawed
|
|
50
|
+
if obs.response_status in (200, 201):
|
|
51
|
+
confirmed = True
|
|
52
|
+
summary = (
|
|
53
|
+
f"BFLA confirmed: Privileged endpoint {raw_path} executed successfully "
|
|
54
|
+
f"under standard user identity without 403 Forbidden check."
|
|
55
|
+
)
|
|
56
|
+
steps = [
|
|
57
|
+
f"Authenticate as standard user 'user-a'",
|
|
58
|
+
f"Invoke privileged endpoint {method} {target_url}",
|
|
59
|
+
f"Received HTTP {obs.response_status} indicating unauthorized function execution",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
return TestExecutionResult(
|
|
63
|
+
testpack_name=self.name,
|
|
64
|
+
hypothesis_id=hypothesis.id,
|
|
65
|
+
confirmed=confirmed,
|
|
66
|
+
confidence=0.92 if confirmed else 0.20,
|
|
67
|
+
summary=summary,
|
|
68
|
+
observations=observations,
|
|
69
|
+
reproduction_steps=steps,
|
|
70
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""BOLA (Broken Object Level Authorization) dual-tenant differential test pack."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List
|
|
5
|
+
from trace_engine.testpacks.base import TestPack, TestContext, TestExecutionResult
|
|
6
|
+
from trace_engine.security.hypotheses import SecurityHypothesis
|
|
7
|
+
from trace_engine.runtime.client import ScopedHttpClient
|
|
8
|
+
from trace_engine.runtime.observations import RuntimeObservation
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BolaTestPack(TestPack):
|
|
12
|
+
@property
|
|
13
|
+
def name(self) -> str:
|
|
14
|
+
return "bola"
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def description(self) -> str:
|
|
18
|
+
return "Validates whether object lookups enforce tenant and ownership boundaries using dual-identity differential testing."
|
|
19
|
+
|
|
20
|
+
def execute(
|
|
21
|
+
self,
|
|
22
|
+
hypothesis: SecurityHypothesis,
|
|
23
|
+
client: ScopedHttpClient,
|
|
24
|
+
context: TestContext,
|
|
25
|
+
) -> TestExecutionResult:
|
|
26
|
+
base_url = context.target_base_url.rstrip("/")
|
|
27
|
+
parts = hypothesis.endpoint_display.split(" ")
|
|
28
|
+
method = parts[0] if len(parts) > 1 else "GET"
|
|
29
|
+
raw_path = parts[1] if len(parts) > 1 else parts[0]
|
|
30
|
+
|
|
31
|
+
test_ids = ["order-101", "101", "1", "order-102", "order-user-a", "2", "3", "user-1", "doc-1"]
|
|
32
|
+
observations: List[RuntimeObservation] = []
|
|
33
|
+
confirmed = False
|
|
34
|
+
summary = "No BOLA behavior observed."
|
|
35
|
+
steps: List[str] = []
|
|
36
|
+
|
|
37
|
+
token_a = context.active_tokens.get("user-a", "")
|
|
38
|
+
token_b = context.active_tokens.get("user-b", "")
|
|
39
|
+
|
|
40
|
+
for test_id in test_ids:
|
|
41
|
+
probe_path = re.sub(r"\{[a-zA-Z0-9_]+\}", test_id, raw_path)
|
|
42
|
+
target_url = f"{base_url}{probe_path}"
|
|
43
|
+
|
|
44
|
+
# Differential Phase 1: Request with User A context
|
|
45
|
+
headers_a = {"Authorization": f"Bearer {token_a}"} if token_a else {}
|
|
46
|
+
obs_a = client.execute(method=method, url=target_url, headers=headers_a)
|
|
47
|
+
observations.append(obs_a)
|
|
48
|
+
|
|
49
|
+
# Differential Phase 2: Request with User B context (or unauthenticated)
|
|
50
|
+
headers_b = {"Authorization": f"Bearer {token_b}"} if token_b else {}
|
|
51
|
+
obs_b = client.execute(method=method, url=target_url, headers=headers_b)
|
|
52
|
+
observations.append(obs_b)
|
|
53
|
+
|
|
54
|
+
# Case A: True dual-tenant differential match
|
|
55
|
+
if obs_a.response_status == 200 and obs_b.response_status == 200:
|
|
56
|
+
body_b_lower = obs_b.response_body.lower()
|
|
57
|
+
# If User B receives legitimate record data without authorization restriction
|
|
58
|
+
if any(k in body_b_lower for k in ("id", "user", "order", "item", "total", "data", "status")):
|
|
59
|
+
confirmed = True
|
|
60
|
+
summary = (
|
|
61
|
+
f"BOLA / IDOR (CWE-639) CONFIRMED via Dual-Tenant Differential Test: Object '{test_id}' on {raw_path} "
|
|
62
|
+
f"is accessible by both Identity A and Identity B without tenant isolation."
|
|
63
|
+
)
|
|
64
|
+
steps = [
|
|
65
|
+
f"Query {method} {target_url} using Tenant A credentials (HTTP {obs_a.response_status})",
|
|
66
|
+
f"Query identical resource {method} {target_url} using Tenant B credentials (HTTP {obs_b.response_status})",
|
|
67
|
+
"Observe cross-tenant data leak without HTTP 403 Forbidden or 404 Not Found scoping",
|
|
68
|
+
]
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
# Case B: Single token / unauthenticated BOLA check
|
|
72
|
+
elif obs_b.response_status == 200:
|
|
73
|
+
body_b_lower = obs_b.response_body.lower()
|
|
74
|
+
if "user-a" in body_b_lower or "total" in body_b_lower or "items" in body_b_lower or "product" in body_b_lower:
|
|
75
|
+
confirmed = True
|
|
76
|
+
summary = (
|
|
77
|
+
f"BOLA confirmed: User 'user-b' accessed object '{test_id}' "
|
|
78
|
+
f"belonging to another identity on endpoint {raw_path} with status 200 OK."
|
|
79
|
+
)
|
|
80
|
+
steps = [
|
|
81
|
+
f"Send {method} {target_url} with user-b credentials",
|
|
82
|
+
"Observe HTTP status 200 OK returning private data of user-a",
|
|
83
|
+
]
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
return TestExecutionResult(
|
|
87
|
+
testpack_name=self.name,
|
|
88
|
+
hypothesis_id=hypothesis.id,
|
|
89
|
+
confirmed=confirmed,
|
|
90
|
+
confidence=0.98 if confirmed else 0.20,
|
|
91
|
+
summary=summary,
|
|
92
|
+
observations=observations,
|
|
93
|
+
reproduction_steps=steps,
|
|
94
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""CORS (Cross-Origin Resource Sharing) Misconfiguration test pack (CWE-942)."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Any
|
|
4
|
+
from trace_engine.testpacks.base import TestPack, TestContext, TestExecutionResult
|
|
5
|
+
from trace_engine.security.hypotheses import SecurityHypothesis
|
|
6
|
+
from trace_engine.runtime.client import ScopedHttpClient
|
|
7
|
+
from trace_engine.runtime.observations import RuntimeObservation
|
|
8
|
+
|
|
9
|
+
PROBE_ORIGINS = [
|
|
10
|
+
"https://attacker.evil.com",
|
|
11
|
+
"https://untrusted-cross-origin.net",
|
|
12
|
+
"null",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CorsTestPack(TestPack):
|
|
17
|
+
@property
|
|
18
|
+
def name(self) -> str:
|
|
19
|
+
return "cors"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def description(self) -> str:
|
|
23
|
+
return "Validates whether arbitrary untrusted origins are reflected with credentials enabled."
|
|
24
|
+
|
|
25
|
+
def execute(
|
|
26
|
+
self,
|
|
27
|
+
hypothesis: SecurityHypothesis,
|
|
28
|
+
client: ScopedHttpClient,
|
|
29
|
+
context: TestContext,
|
|
30
|
+
) -> TestExecutionResult:
|
|
31
|
+
base_url = context.target_base_url.rstrip("/")
|
|
32
|
+
parts = hypothesis.endpoint_display.split(" ")
|
|
33
|
+
method = parts[0] if len(parts) > 1 else "GET"
|
|
34
|
+
raw_path = parts[1] if len(parts) > 1 else parts[0]
|
|
35
|
+
|
|
36
|
+
target_url = f"{base_url}{raw_path}"
|
|
37
|
+
observations: List[RuntimeObservation] = []
|
|
38
|
+
confirmed = False
|
|
39
|
+
summary = "No exploitable CORS misconfiguration detected."
|
|
40
|
+
steps: List[str] = []
|
|
41
|
+
|
|
42
|
+
for origin in PROBE_ORIGINS:
|
|
43
|
+
headers = {"Origin": origin}
|
|
44
|
+
obs = client.execute(method=method, url=target_url, headers=headers)
|
|
45
|
+
observations.append(obs)
|
|
46
|
+
|
|
47
|
+
resp_hdrs = {k.lower(): v for k, v in obs.response_headers.items()}
|
|
48
|
+
allow_origin = resp_hdrs.get("access-control-allow-origin", "")
|
|
49
|
+
allow_creds = resp_hdrs.get("access-control-allow-credentials", "").lower() == "true"
|
|
50
|
+
|
|
51
|
+
# 1. Arbitrary origin reflection with credentials
|
|
52
|
+
if allow_origin in (origin, "*") and allow_creds:
|
|
53
|
+
confirmed = True
|
|
54
|
+
summary = (
|
|
55
|
+
f"CORS Misconfiguration (CWE-942) CONFIRMED: Endpoint {raw_path} reflected untrusted origin "
|
|
56
|
+
f"'{origin}' with 'Access-Control-Allow-Credentials: true', enabling cross-origin authenticated data theft."
|
|
57
|
+
)
|
|
58
|
+
steps = [
|
|
59
|
+
f"Send {method} request to {target_url} with header Origin: {origin}",
|
|
60
|
+
f"Observe response headers containing Access-Control-Allow-Origin: {allow_origin} and Access-Control-Allow-Credentials: true",
|
|
61
|
+
]
|
|
62
|
+
break
|
|
63
|
+
|
|
64
|
+
# 2. 'null' origin reflection with credentials
|
|
65
|
+
if origin == "null" and allow_origin == "null" and allow_creds:
|
|
66
|
+
confirmed = True
|
|
67
|
+
summary = (
|
|
68
|
+
f"CORS Misconfiguration (CWE-942) CONFIRMED: Endpoint {raw_path} trusts origin 'null' "
|
|
69
|
+
f"with credentials, exposing authenticated session to sandboxed iframe attacks."
|
|
70
|
+
)
|
|
71
|
+
steps = [
|
|
72
|
+
f"Send {method} request to {target_url} with header Origin: null",
|
|
73
|
+
"Observe response header Access-Control-Allow-Origin: null with credentials allowed",
|
|
74
|
+
]
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
return TestExecutionResult(
|
|
78
|
+
testpack_name=self.name,
|
|
79
|
+
hypothesis_id=hypothesis.id,
|
|
80
|
+
confirmed=confirmed,
|
|
81
|
+
confidence=0.90 if confirmed else 0.1,
|
|
82
|
+
summary=summary,
|
|
83
|
+
observations=observations,
|
|
84
|
+
reproduction_steps=steps,
|
|
85
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Insecure Deserialization test pack (CWE-502)."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Any
|
|
4
|
+
from trace_engine.testpacks.base import TestPack, TestContext, TestExecutionResult
|
|
5
|
+
from trace_engine.security.hypotheses import SecurityHypothesis
|
|
6
|
+
from trace_engine.runtime.client import ScopedHttpClient
|
|
7
|
+
from trace_engine.runtime.observations import RuntimeObservation
|
|
8
|
+
|
|
9
|
+
DESER_PROBES = [
|
|
10
|
+
("pickle_base64", "gASVBAAAAAAAAACMCXRyYWNlX2JveJSULg=="),
|
|
11
|
+
("yaml_object", "!!python/object/apply:builtins.str [\"trace_probe\"]"),
|
|
12
|
+
("java_serial", "rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sBFlmeFAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAAAeA=="),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
DESER_ERROR_SIGNATURES = [
|
|
16
|
+
"unpicklingerror",
|
|
17
|
+
"pickle.unpicklingerror",
|
|
18
|
+
"cannot deserialize",
|
|
19
|
+
"invalidstreamheader",
|
|
20
|
+
"java.io.invalidclassexception",
|
|
21
|
+
"constructorerror",
|
|
22
|
+
"yaml.constructor",
|
|
23
|
+
"unsafe deserialization",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DeserializationTestPack(TestPack):
|
|
28
|
+
@property
|
|
29
|
+
def name(self) -> str:
|
|
30
|
+
return "deserialization"
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def description(self) -> str:
|
|
34
|
+
return "Validates whether endpoints accept untrusted serialized byte-streams or object definitions without schema validation."
|
|
35
|
+
|
|
36
|
+
def execute(
|
|
37
|
+
self,
|
|
38
|
+
hypothesis: SecurityHypothesis,
|
|
39
|
+
client: ScopedHttpClient,
|
|
40
|
+
context: TestContext,
|
|
41
|
+
) -> TestExecutionResult:
|
|
42
|
+
base_url = context.target_base_url.rstrip("/")
|
|
43
|
+
parts = hypothesis.endpoint_display.split(" ")
|
|
44
|
+
method = parts[0] if len(parts) > 1 else "POST"
|
|
45
|
+
raw_path = parts[1] if len(parts) > 1 else parts[0]
|
|
46
|
+
|
|
47
|
+
target_url = f"{base_url}{raw_path}"
|
|
48
|
+
token = context.active_tokens.get("user-a", "")
|
|
49
|
+
headers = {"Authorization": f"Bearer {token}"} if token else {}
|
|
50
|
+
|
|
51
|
+
observations: List[RuntimeObservation] = []
|
|
52
|
+
confirmed = False
|
|
53
|
+
summary = "No Insecure Deserialization vulnerabilities detected."
|
|
54
|
+
steps: List[str] = []
|
|
55
|
+
|
|
56
|
+
for probe_type, payload in DESER_PROBES:
|
|
57
|
+
json_payload = {"data": payload, "object": payload, "state": payload}
|
|
58
|
+
obs = client.execute(method=method, url=target_url, headers=headers, json_body=json_payload)
|
|
59
|
+
observations.append(obs)
|
|
60
|
+
|
|
61
|
+
body_lower = obs.response_body.lower()
|
|
62
|
+
for sig in DESER_ERROR_SIGNATURES:
|
|
63
|
+
if sig in body_lower:
|
|
64
|
+
confirmed = True
|
|
65
|
+
summary = (
|
|
66
|
+
f"Insecure Deserialization (CWE-502) CONFIRMED: Endpoint {raw_path} attempted to deserialize "
|
|
67
|
+
f"untrusted object probe ({probe_type}) resulting in deserialization parser reaction: '{sig}'."
|
|
68
|
+
)
|
|
69
|
+
steps = [
|
|
70
|
+
f"Send {method} request to {target_url} containing serialized {probe_type} payload",
|
|
71
|
+
f"Observe deserialization runtime exception trace '{sig}' in server response",
|
|
72
|
+
]
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
if confirmed:
|
|
76
|
+
break
|
|
77
|
+
|
|
78
|
+
return TestExecutionResult(
|
|
79
|
+
testpack_name=self.name,
|
|
80
|
+
hypothesis_id=hypothesis.id,
|
|
81
|
+
confirmed=confirmed,
|
|
82
|
+
confidence=0.90 if confirmed else 0.1,
|
|
83
|
+
summary=summary,
|
|
84
|
+
observations=observations,
|
|
85
|
+
reproduction_steps=steps,
|
|
86
|
+
)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Injection indicator test pack with polymorphic SQLi and Command Injection mutations."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import urllib.parse
|
|
5
|
+
from typing import List
|
|
6
|
+
from trace_engine.testpacks.base import TestPack, TestContext, TestExecutionResult
|
|
7
|
+
from trace_engine.security.hypotheses import SecurityHypothesis
|
|
8
|
+
from trace_engine.runtime.client import ScopedHttpClient
|
|
9
|
+
from trace_engine.runtime.observations import RuntimeObservation
|
|
10
|
+
|
|
11
|
+
INJECTION_PROBES = [
|
|
12
|
+
# Classic SQLi Boolean
|
|
13
|
+
"' OR '1'='1",
|
|
14
|
+
"' OR 1=1--",
|
|
15
|
+
"admin'--",
|
|
16
|
+
# SQL Syntax breakers
|
|
17
|
+
"'",
|
|
18
|
+
"''",
|
|
19
|
+
"test'--",
|
|
20
|
+
"';--",
|
|
21
|
+
# Stacked queries / Command delimiters
|
|
22
|
+
"; cat /etc/passwd",
|
|
23
|
+
"| whoami",
|
|
24
|
+
"$(id)",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
SQL_ERROR_PATTERNS = [
|
|
28
|
+
"syntax error",
|
|
29
|
+
"sqlite3.operationalerror",
|
|
30
|
+
"unrecognized token",
|
|
31
|
+
"sql syntax",
|
|
32
|
+
"database error",
|
|
33
|
+
"pg_query",
|
|
34
|
+
"ora-01756",
|
|
35
|
+
"mysql_fetch",
|
|
36
|
+
"sqlstate",
|
|
37
|
+
"odbc sql server driver",
|
|
38
|
+
"pdoexception",
|
|
39
|
+
"unterminated quotation mark",
|
|
40
|
+
"unclosed quotation mark",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class InjectionTestPack(TestPack):
|
|
45
|
+
@property
|
|
46
|
+
def name(self) -> str:
|
|
47
|
+
return "injection"
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def description(self) -> str:
|
|
51
|
+
return "Tests query, path, and body parameters for dynamic SQL/command injection vulnerabilities across mutated probe varieties."
|
|
52
|
+
|
|
53
|
+
def execute(
|
|
54
|
+
self,
|
|
55
|
+
hypothesis: SecurityHypothesis,
|
|
56
|
+
client: ScopedHttpClient,
|
|
57
|
+
context: TestContext,
|
|
58
|
+
) -> TestExecutionResult:
|
|
59
|
+
base_url = context.target_base_url.rstrip("/")
|
|
60
|
+
parts = hypothesis.endpoint_display.split(" ")
|
|
61
|
+
method = parts[0] if len(parts) > 1 else "GET"
|
|
62
|
+
raw_path = parts[1] if len(parts) > 1 else parts[0]
|
|
63
|
+
|
|
64
|
+
clean_path = re.sub(r"\{[a-zA-Z0-9_]+\}", "1", raw_path)
|
|
65
|
+
token = context.active_tokens.get("user-a", "")
|
|
66
|
+
headers = {"Authorization": f"Bearer {token}"} if token else {}
|
|
67
|
+
|
|
68
|
+
observations: List[RuntimeObservation] = []
|
|
69
|
+
confirmed = False
|
|
70
|
+
summary = "No dynamic injection signatures observed."
|
|
71
|
+
steps: List[str] = []
|
|
72
|
+
|
|
73
|
+
for probe in INJECTION_PROBES:
|
|
74
|
+
encoded_probe = urllib.parse.quote(probe)
|
|
75
|
+
sep = "&" if "?" in clean_path else "?"
|
|
76
|
+
probe_url = f"{base_url}{clean_path}{sep}q={encoded_probe}&query={encoded_probe}&search={encoded_probe}&id={encoded_probe}"
|
|
77
|
+
|
|
78
|
+
# 1. Query probe
|
|
79
|
+
obs = client.execute(method=method, url=probe_url, headers=headers)
|
|
80
|
+
observations.append(obs)
|
|
81
|
+
|
|
82
|
+
body_lower = obs.response_body.lower()
|
|
83
|
+
matched_error = next((pat for pat in SQL_ERROR_PATTERNS if pat in body_lower), None)
|
|
84
|
+
|
|
85
|
+
if matched_error or (obs.response_status == 500 and "error" in body_lower):
|
|
86
|
+
confirmed = True
|
|
87
|
+
detail = f"database error signature '{matched_error}'" if matched_error else "unhandled 500 server crash"
|
|
88
|
+
summary = (
|
|
89
|
+
f"Injection vulnerability confirmed: Probe '{probe}' on {raw_path} triggered "
|
|
90
|
+
f"{detail} indicating unparameterized query execution."
|
|
91
|
+
)
|
|
92
|
+
steps = [
|
|
93
|
+
f"Send request with probe: {probe_url}",
|
|
94
|
+
f"Observe server reaction: {detail}",
|
|
95
|
+
]
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
# 2. Body probe for mutating requests
|
|
99
|
+
if method in ("POST", "PUT", "PATCH"):
|
|
100
|
+
body_probe = {"q": probe, "query": probe, "search": probe, "name": probe}
|
|
101
|
+
body_obs = client.execute(
|
|
102
|
+
method=method,
|
|
103
|
+
url=f"{base_url}{clean_path}",
|
|
104
|
+
headers=headers,
|
|
105
|
+
json_body=body_probe,
|
|
106
|
+
)
|
|
107
|
+
observations.append(body_obs)
|
|
108
|
+
body_res_lower = body_obs.response_body.lower()
|
|
109
|
+
matched_body_err = next((pat for pat in SQL_ERROR_PATTERNS if pat in body_res_lower), None)
|
|
110
|
+
if matched_body_err or (body_obs.response_status == 500 and "error" in body_res_lower):
|
|
111
|
+
confirmed = True
|
|
112
|
+
detail = f"database error signature '{matched_body_err}'" if matched_body_err else "unhandled 500 server crash"
|
|
113
|
+
summary = (
|
|
114
|
+
f"Injection vulnerability confirmed in request body: Probe '{probe}' on {raw_path} triggered "
|
|
115
|
+
f"{detail}."
|
|
116
|
+
)
|
|
117
|
+
steps = [
|
|
118
|
+
f"Send {method} to {base_url}{clean_path} with JSON body payload containing '{probe}'",
|
|
119
|
+
f"Observe database reaction: {detail}",
|
|
120
|
+
]
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
# 3. Statistical Blind Timing Oracle (Welch's t-test) if error-based probes were inconclusive
|
|
124
|
+
if not confirmed:
|
|
125
|
+
from trace_engine.security.timing import StatisticalTimingOracle
|
|
126
|
+
|
|
127
|
+
# Measure baseline latencies (N=3 baseline samples)
|
|
128
|
+
base_latencies: List[float] = []
|
|
129
|
+
for _ in range(3):
|
|
130
|
+
sep = "&" if "?" in clean_path else "?"
|
|
131
|
+
b_url = f"{base_url}{clean_path}{sep}q=trace_baseline_test"
|
|
132
|
+
b_obs = client.execute(method=method, url=b_url, headers=headers)
|
|
133
|
+
observations.append(b_obs)
|
|
134
|
+
base_latencies.append(b_obs.latency_ms / 1000.0)
|
|
135
|
+
|
|
136
|
+
# Test blind sleep payloads with Welch's t-test (N=3 delay samples)
|
|
137
|
+
timing_probes = [
|
|
138
|
+
"'; SELECT pg_sleep(1.5)--",
|
|
139
|
+
"' OR SLEEP(1.5)--",
|
|
140
|
+
"1; sleep 1.5",
|
|
141
|
+
]
|
|
142
|
+
expected_sec = 1.5
|
|
143
|
+
|
|
144
|
+
for t_probe in timing_probes:
|
|
145
|
+
encoded_t = urllib.parse.quote(t_probe)
|
|
146
|
+
sep = "&" if "?" in clean_path else "?"
|
|
147
|
+
t_url = f"{base_url}{clean_path}{sep}q={encoded_t}"
|
|
148
|
+
|
|
149
|
+
delay_latencies: List[float] = []
|
|
150
|
+
for _ in range(3):
|
|
151
|
+
t_obs = client.execute(method=method, url=t_url, headers=headers)
|
|
152
|
+
observations.append(t_obs)
|
|
153
|
+
delay_latencies.append(t_obs.latency_ms / 1000.0)
|
|
154
|
+
|
|
155
|
+
t_res = StatisticalTimingOracle.evaluate_timing_vulnerability(
|
|
156
|
+
baseline_latencies=base_latencies,
|
|
157
|
+
delay_latencies=delay_latencies,
|
|
158
|
+
expected_delay_sec=expected_sec,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if t_res.is_confirmed:
|
|
162
|
+
confirmed = True
|
|
163
|
+
summary = t_res.summary
|
|
164
|
+
steps = [
|
|
165
|
+
f"Measure baseline latency across non-delayed requests ({t_res.mean_baseline_sec:.2f}s mean)",
|
|
166
|
+
f"Send time-delay injection probe: {t_url}",
|
|
167
|
+
f"Observe statistically significant delay shift: +{t_res.observed_shift_sec:.2f}s (Welch's t={t_res.t_statistic}, p={t_res.p_value:.2e})",
|
|
168
|
+
]
|
|
169
|
+
break
|
|
170
|
+
|
|
171
|
+
return TestExecutionResult(
|
|
172
|
+
testpack_name=self.name,
|
|
173
|
+
hypothesis_id=hypothesis.id,
|
|
174
|
+
confirmed=confirmed,
|
|
175
|
+
confidence=0.98 if confirmed else 0.15,
|
|
176
|
+
summary=summary,
|
|
177
|
+
observations=observations,
|
|
178
|
+
reproduction_steps=steps,
|
|
179
|
+
)
|