@vk.amogh/trace 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 +208 -0
- package/bin/trace.js +112 -0
- package/package.json +46 -0
- 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,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"trace-security-guard": {
|
|
3
|
+
"enabled": true,
|
|
4
|
+
"PostToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "replace_file_content",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "trace status",
|
|
11
|
+
"timeout": 15
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"Stop": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "trace findings",
|
|
20
|
+
"timeout": 20
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "trace-security",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "TRACE Agent Harness Plugin - Equips autonomous coding agents with APM attack graphs, live exploit verification, and benchmark self-healing oracles.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "TRACE Security Engineering Team",
|
|
7
|
+
"email": "security@trace.dev"
|
|
8
|
+
},
|
|
9
|
+
"license": "Apache-2.0",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"security",
|
|
12
|
+
"agent-harness",
|
|
13
|
+
"swe-bench",
|
|
14
|
+
"owasp",
|
|
15
|
+
"mcp",
|
|
16
|
+
"verification",
|
|
17
|
+
"apm",
|
|
18
|
+
"self-healing"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: always_on
|
|
3
|
+
description: Mandatory security remediation rules for autonomous coding agents.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TRACE Autonomous Security Remediation Guidelines
|
|
7
|
+
|
|
8
|
+
When an AI coding agent is tasked with fixing vulnerabilities reported by TRACE or running a TRACE-Bench task, the following standards are strictly mandatory:
|
|
9
|
+
|
|
10
|
+
## 1. Zero-Dummy-Bypass Rule
|
|
11
|
+
- **FORBIDDEN**: Never resolve a vulnerability by hardcoding boolean flags (`if (true) return 200`), returning mock empty responses, commenting out security filters, or bypassing authentication handlers.
|
|
12
|
+
- **MANDATORY**: Fixes must genuinely enforce the intended business and security invariants in code (e.g. comparing session tenant identity against record tenant identity).
|
|
13
|
+
|
|
14
|
+
## 2. Broken Object-Level Authorization (BOLA / IDOR)
|
|
15
|
+
- When fetching, updating, or deleting any domain entity (`Record`, `User`, `Order`, `Document`) by identifier:
|
|
16
|
+
1. Extract the authenticated user's ID and tenant/organization identifier from the verified security context (`Authentication`, `SecurityContextHolder`, or JWT claims).
|
|
17
|
+
2. Scope the database query or in-memory retrieval to the authenticated tenant (e.g. `findByRecordIdAndTenantId(...)`).
|
|
18
|
+
3. If the resource does not belong to the caller, return HTTP `403 Forbidden` or `404 Not Found`.
|
|
19
|
+
|
|
20
|
+
## 3. Broken Function-Level Authorization (BFLA)
|
|
21
|
+
- Privileged endpoints (e.g. administrative exports, role elevation, tenant configuration, metric resets) must explicitly verify user roles:
|
|
22
|
+
1. Enforce RBAC checks (e.g. `@PreAuthorize("hasRole('ADMIN')")`, `authContext.requireRole("admin")`).
|
|
23
|
+
2. Deny requests with HTTP `403 Forbidden` for standard or unprivileged users.
|
|
24
|
+
|
|
25
|
+
## 4. Server-Side Request Forgery (SSRF)
|
|
26
|
+
- When accepting external URLs or callbacks (`callbackUrl`, `webhookUrl`, `targetUri`):
|
|
27
|
+
1. Validate the URL scheme is strictly `https` (or `http` for allowed domains).
|
|
28
|
+
2. Reject all loopback, link-local, and private RFC-1918 addresses (`127.0.0.1`, `localhost`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.169.254`).
|
|
29
|
+
3. Resolve DNS names and check resolved IP addresses against private subnet blocklists before dispatching outbound HTTP calls.
|
|
30
|
+
|
|
31
|
+
## 5. Broken Authentication & Session Management
|
|
32
|
+
- Never expose unauthenticated endpoints that execute state-changing actions.
|
|
33
|
+
- Ensure all API endpoints under `/api/**` or `/admin/**` participate in the authentication barrier unless explicitly marked public.
|
|
34
|
+
|
|
35
|
+
## 6. Mass Assignment & Parameter Injection
|
|
36
|
+
- Do not bind untrusted HTTP request bodies directly into persistent domain entities or ORM models.
|
|
37
|
+
- Use explicit DTOs (Data Transfer Objects) with allowlisted fields.
|
|
38
|
+
|
|
39
|
+
## 7. Verification Requirement
|
|
40
|
+
- After applying any code change, execute the TRACE verification oracle (`trace_verify` or `trace verify <finding_id>`) to prove that the exploit vector is blocked and no regressions were introduced.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trace-security-harness
|
|
3
|
+
description: >-
|
|
4
|
+
Standardized runbook and tool guide for autonomous coding agents operating with the TRACE
|
|
5
|
+
Agent Harness Plugin. Guides agents through finding discovery, Attack-Path Model inspection,
|
|
6
|
+
safe surgical remediation, and live verification.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# TRACE Security Agent Harness Runbook
|
|
10
|
+
|
|
11
|
+
The TRACE Agent Harness Plugin equips you with tools to autonomously audit codebases, investigate correlated vulnerability findings, inspect Attack-Path Models (APM), apply surgical security fixes, and verify solutions against live exploit tests.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Autonomous Workflow for Security Remediation
|
|
16
|
+
|
|
17
|
+
Follow these 5 distinct phases when resolving security vulnerabilities:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
[Phase 1: Ingest] ➔ [Phase 2: Diagnose] ➔ [Phase 3: Plan Fix] ➔ [Phase 4: Patch] ➔ [Phase 5: Verify]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Phase 1: Finding & Task Ingestion
|
|
24
|
+
Query the TRACE findings store or benchmark task queue to identify active vulnerabilities:
|
|
25
|
+
|
|
26
|
+
1. **List all findings:**
|
|
27
|
+
- MCP Tool: `trace_findings`
|
|
28
|
+
- CLI: `trace findings`
|
|
29
|
+
2. **Inspect a specific benchmark task:**
|
|
30
|
+
- MCP Tool: `trace_harness_task` with `task_id` or `finding_id`
|
|
31
|
+
- CLI: `trace plugin task <task_id>`
|
|
32
|
+
|
|
33
|
+
Each finding has:
|
|
34
|
+
- `id`: Unique identifier (e.g. `TR-BOLA-001`, `TR-SSRF-003`)
|
|
35
|
+
- `category`: OWASP API Security category (e.g. BOLA, BFLA, SSRF)
|
|
36
|
+
- `severity`: CRITICAL, HIGH, MEDIUM, LOW
|
|
37
|
+
- `endpoint`: The affected HTTP endpoint
|
|
38
|
+
- `source_location`: File path and line numbers
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
### Phase 2: Root-Cause & Attack-Path Diagnosis
|
|
43
|
+
Never guess the fix. Examine the complete correlation evidence and Attack-Path Model (APM) hops:
|
|
44
|
+
|
|
45
|
+
1. **Retrieve explanation and reproduction steps:**
|
|
46
|
+
- MCP Tool: `trace_explain` with `finding_id: "TR-BOLA-001"`
|
|
47
|
+
- CLI: `trace explain TR-BOLA-001`
|
|
48
|
+
2. **Inspect the APM graph hops:**
|
|
49
|
+
- MCP Tool: `trace_attack_path` with `endpoint: "/api/records/{id}"`
|
|
50
|
+
|
|
51
|
+
Key items to inspect in the explanation:
|
|
52
|
+
- `attack_path`: Sequence of hops from public HTTP entrypoint to vulnerable sink.
|
|
53
|
+
- `static_evidence`: Code AST patterns that triggered the hypothesis.
|
|
54
|
+
- `runtime_evidence`: Concrete HTTP request/response proofs demonstrating unauthorized access.
|
|
55
|
+
- `remediation`: Architectural guidance on how the endpoint should be secured.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### Phase 3: Surgical Patch Planning
|
|
60
|
+
Formulate a patch that enforces the invariant without modifying unrelated behavior:
|
|
61
|
+
|
|
62
|
+
#### BOLA (Broken Object-Level Authorization)
|
|
63
|
+
- **Problem**: Entity retrieved by ID without checking if caller owns the entity or belongs to the same tenant.
|
|
64
|
+
- **Fix**:
|
|
65
|
+
```kotlin
|
|
66
|
+
// BEFORE (Vulnerable)
|
|
67
|
+
val record = recordRepository.findById(id)
|
|
68
|
+
return ResponseEntity.ok(record)
|
|
69
|
+
|
|
70
|
+
// AFTER (Secure)
|
|
71
|
+
val currentUser = SecurityContext.getCurrentUser()
|
|
72
|
+
val record = recordRepository.findByIdAndTenantId(id, currentUser.tenantId)
|
|
73
|
+
?: return ResponseEntity.status(403).body("Access denied")
|
|
74
|
+
return ResponseEntity.ok(record)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### BFLA (Broken Function-Level Authorization)
|
|
78
|
+
- **Problem**: Privileged/administrative endpoint accessible by regular authenticated users.
|
|
79
|
+
- **Fix**:
|
|
80
|
+
```kotlin
|
|
81
|
+
// Ensure role enforcement is present
|
|
82
|
+
val user = SecurityContext.getCurrentUser()
|
|
83
|
+
if (!user.roles.contains("ADMIN")) {
|
|
84
|
+
return ResponseEntity.status(403).body("Administrator role required")
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### SSRF (Server-Side Request Forgery)
|
|
89
|
+
- **Problem**: Unvalidated user-supplied URL dispatched to internal network or cloud metadata.
|
|
90
|
+
- **Fix**:
|
|
91
|
+
```kotlin
|
|
92
|
+
// Validate destination before dispatch
|
|
93
|
+
val uri = URI(callbackUrl)
|
|
94
|
+
val host = uri.host.lowercase()
|
|
95
|
+
if (host == "localhost" || host == "127.0.0.1" || host.startsWith("169.254.") || host.startsWith("10.") || host.startsWith("192.168.")) {
|
|
96
|
+
return ResponseEntity.status(403).body("Outbound request to internal IP blocked")
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### Phase 4: Patch Application
|
|
103
|
+
Apply the modification using standard file editing tools (`replace_file_content` or `write_to_file`).
|
|
104
|
+
- Ensure no syntax or compilation errors.
|
|
105
|
+
- Keep modifications minimal and idiomatic.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
### Phase 5: Deterministic Verification
|
|
110
|
+
Verify that the exploit vector is blocked and the endpoint remains healthy:
|
|
111
|
+
|
|
112
|
+
1. **Verify with TRACE Verification Oracle:**
|
|
113
|
+
- MCP Tool: `trace_verify` with `finding_id`
|
|
114
|
+
- CLI: `trace verify <finding_id>`
|
|
115
|
+
2. **Evaluate Patch Quality:**
|
|
116
|
+
- MCP Tool: `trace_eval_patch`
|
|
117
|
+
|
|
118
|
+
The verification oracle will execute the active exploit test pack against the running target. A status of `RESOLVED` with `runtime_check_passed: true` confirms the fix is sound.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Evaluation oracle for the TRACE Agent Harness Plugin."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, Any, Optional, List
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from trace_engine.verify import VerificationEngine, VerificationResult
|
|
9
|
+
from trace_engine.harness.patcher import TransactionalPatcher, PatchOperationResult
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("trace_plugin_evaluator")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EvaluationResult(BaseModel):
|
|
15
|
+
"""Evaluation result for an agent-generated patch."""
|
|
16
|
+
task_id: str
|
|
17
|
+
finding_id: str
|
|
18
|
+
passed: bool
|
|
19
|
+
verified_resolved: bool
|
|
20
|
+
reproduced_exploit_blocked: bool
|
|
21
|
+
regressions_detected: bool
|
|
22
|
+
confidence_score: float
|
|
23
|
+
output_message: str
|
|
24
|
+
verification_details: Dict[str, Any] = Field(default_factory=dict)
|
|
25
|
+
patch_applied: bool = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class HarnessEvaluator:
|
|
29
|
+
"""Evaluates patches produced by external AI agent harnesses against TRACE ground-truth test packs."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, repo_path: Path, target_url: str = "http://127.0.0.1:18080"):
|
|
32
|
+
self.repo_path = repo_path.resolve()
|
|
33
|
+
self.target_url = target_url
|
|
34
|
+
self.patcher = TransactionalPatcher(self.repo_path)
|
|
35
|
+
self.verifier = VerificationEngine(repo_path=self.repo_path, target_url=self.target_url)
|
|
36
|
+
|
|
37
|
+
def evaluate_patch(
|
|
38
|
+
self,
|
|
39
|
+
finding_id: str,
|
|
40
|
+
patch_content: Optional[str] = None,
|
|
41
|
+
target_file: Optional[str] = None,
|
|
42
|
+
rollback_after: bool = False,
|
|
43
|
+
) -> EvaluationResult:
|
|
44
|
+
"""Applies an agent patch, runs the TRACE verification oracle, and returns grading results."""
|
|
45
|
+
patch_res: Optional[PatchOperationResult] = None
|
|
46
|
+
|
|
47
|
+
if patch_content and target_file:
|
|
48
|
+
# External patch provided as raw code or diff
|
|
49
|
+
patch_res = self.patcher.apply_full_content(
|
|
50
|
+
finding_id=finding_id,
|
|
51
|
+
rel_path=target_file,
|
|
52
|
+
new_content=patch_content,
|
|
53
|
+
)
|
|
54
|
+
if not patch_res.success:
|
|
55
|
+
return EvaluationResult(
|
|
56
|
+
task_id=f"task_{finding_id}",
|
|
57
|
+
finding_id=finding_id,
|
|
58
|
+
passed=False,
|
|
59
|
+
verified_resolved=False,
|
|
60
|
+
reproduced_exploit_blocked=False,
|
|
61
|
+
regressions_detected=False,
|
|
62
|
+
confidence_score=0.0,
|
|
63
|
+
output_message=f"Failed to apply patch: {patch_res.error}",
|
|
64
|
+
patch_applied=False,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Run verification engine against live runtime / APM
|
|
68
|
+
try:
|
|
69
|
+
ver_res: VerificationResult = self.verifier.verify(finding_id)
|
|
70
|
+
is_resolved = (ver_res.status.value == "FIXED") or ver_res.remediation_verified
|
|
71
|
+
exploit_blocked = is_resolved
|
|
72
|
+
|
|
73
|
+
passed = is_resolved
|
|
74
|
+
msg = f"Verification {ver_res.status.value}: {ver_res.summary}"
|
|
75
|
+
|
|
76
|
+
result = EvaluationResult(
|
|
77
|
+
task_id=f"task_{finding_id}",
|
|
78
|
+
finding_id=finding_id,
|
|
79
|
+
passed=passed,
|
|
80
|
+
verified_resolved=is_resolved,
|
|
81
|
+
reproduced_exploit_blocked=exploit_blocked,
|
|
82
|
+
regressions_detected=False,
|
|
83
|
+
confidence_score=1.0 if passed else 0.0,
|
|
84
|
+
output_message=msg,
|
|
85
|
+
verification_details=ver_res.model_dump(),
|
|
86
|
+
patch_applied=patch_res.success if patch_res else True,
|
|
87
|
+
)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.exception("Error executing verification oracle")
|
|
90
|
+
result = EvaluationResult(
|
|
91
|
+
task_id=f"task_{finding_id}",
|
|
92
|
+
finding_id=finding_id,
|
|
93
|
+
passed=False,
|
|
94
|
+
verified_resolved=False,
|
|
95
|
+
reproduced_exploit_blocked=False,
|
|
96
|
+
regressions_detected=False,
|
|
97
|
+
confidence_score=0.0,
|
|
98
|
+
output_message=f"Verification exception: {e}",
|
|
99
|
+
patch_applied=patch_res.success if patch_res else False,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if rollback_after and patch_res and patch_res.backup_file and target_file:
|
|
103
|
+
self.patcher.rollback(target_file, Path(patch_res.backup_file))
|
|
104
|
+
|
|
105
|
+
return result
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""SWE-bench dataset adapter for the TRACE Agent Harness Plugin."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Dict, Any
|
|
6
|
+
|
|
7
|
+
from trace_engine.findings.model import Finding
|
|
8
|
+
from trace_engine.plugin.task import HarnessTaskSpec
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_harness_task(finding: Finding, repo_path: Path) -> HarnessTaskSpec:
|
|
12
|
+
"""Transform a CorrelatedFinding into a standardized agent harness task specification."""
|
|
13
|
+
rel_path = str(finding.source_location.file) if finding.source_location else "unknown"
|
|
14
|
+
start_line = finding.source_location.line_start if finding.source_location else 1
|
|
15
|
+
end_line = finding.source_location.line_end if finding.source_location else 1
|
|
16
|
+
|
|
17
|
+
code_snippet = ""
|
|
18
|
+
try:
|
|
19
|
+
full_file = repo_path / rel_path
|
|
20
|
+
if not full_file.is_file():
|
|
21
|
+
for sub in repo_path.iterdir():
|
|
22
|
+
if sub.is_dir() and (sub / rel_path).is_file():
|
|
23
|
+
full_file = sub / rel_path
|
|
24
|
+
break
|
|
25
|
+
|
|
26
|
+
if full_file.is_file():
|
|
27
|
+
lines = full_file.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
28
|
+
sl = max(0, start_line - 1)
|
|
29
|
+
el = min(len(lines), end_line + 5)
|
|
30
|
+
code_snippet = "\n".join(lines[sl:el])
|
|
31
|
+
except Exception:
|
|
32
|
+
code_snippet = ""
|
|
33
|
+
|
|
34
|
+
static_ev = "\n".join(f"- `{ev}`" for ev in (finding.static_evidence or []))
|
|
35
|
+
|
|
36
|
+
problem_stmt = (
|
|
37
|
+
f"# Security Vulnerability Remediation: {finding.title}\n\n"
|
|
38
|
+
f"**Category:** {finding.category}\n"
|
|
39
|
+
f"**Severity:** {finding.severity.value}\n"
|
|
40
|
+
f"**Target Endpoint:** `{finding.endpoint}`\n"
|
|
41
|
+
f"**Vulnerable Source:** `{rel_path}:{start_line}`\n\n"
|
|
42
|
+
f"## Vulnerability Description\n"
|
|
43
|
+
f"{finding.correlation_notes or finding.title}\n\n"
|
|
44
|
+
f"## Static Analysis Evidence\n"
|
|
45
|
+
f"{static_ev or 'None'}\n\n"
|
|
46
|
+
f"## Attack Path\n"
|
|
47
|
+
+ "\n".join(f"- {hop}" for hop in (finding.attack_path or [])) + "\n\n"
|
|
48
|
+
f"## Live Exploit Proof\n"
|
|
49
|
+
+ "\n".join(f"- `{proof}`" for proof in (finding.runtime_evidence or [])) + "\n\n"
|
|
50
|
+
f"## Objective\n"
|
|
51
|
+
f"Modify `{rel_path}` to fix the vulnerability without breaking the endpoint contract or returning static dummy responses."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
hints = (
|
|
55
|
+
f"Recommended Remediation Architecture:\n"
|
|
56
|
+
f"{finding.remediation}\n\n"
|
|
57
|
+
f"Reproduction steps:\n"
|
|
58
|
+
+ "\n".join(f"1. {step}" for step in (finding.reproduction_steps or []))
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
task_id = f"trace__{repo_path.name.lower()}-{finding.id}"
|
|
62
|
+
|
|
63
|
+
return HarnessTaskSpec(
|
|
64
|
+
instance_id=task_id,
|
|
65
|
+
finding_id=finding.id,
|
|
66
|
+
title=finding.title,
|
|
67
|
+
category=finding.category,
|
|
68
|
+
severity=finding.severity.value,
|
|
69
|
+
repo_name=repo_path.name,
|
|
70
|
+
endpoint=finding.endpoint,
|
|
71
|
+
source_file=rel_path,
|
|
72
|
+
line_start=start_line,
|
|
73
|
+
line_end=end_line,
|
|
74
|
+
code_snippet=code_snippet,
|
|
75
|
+
problem_statement=problem_stmt,
|
|
76
|
+
hints_text=hints,
|
|
77
|
+
attack_path_hops=finding.attack_path or [],
|
|
78
|
+
runtime_exploit_proof=finding.runtime_evidence or [],
|
|
79
|
+
reproduction_steps=finding.reproduction_steps or [],
|
|
80
|
+
fail_to_pass=[f"test_exploit_{finding.id}"],
|
|
81
|
+
pass_to_pass=["test_endpoint_functional_health"],
|
|
82
|
+
metadata={
|
|
83
|
+
"finding_id": finding.id,
|
|
84
|
+
"category": finding.category,
|
|
85
|
+
"severity": finding.severity.value,
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def export_swebench_jsonl(tasks: List[HarnessTaskSpec], output_file: Path) -> int:
|
|
91
|
+
"""Export tasks to a SWE-bench compatible JSONL benchmark dataset file."""
|
|
92
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
with output_file.open("w", encoding="utf-8") as f:
|
|
94
|
+
for t in tasks:
|
|
95
|
+
f.write(json.dumps(t.to_swebench_dict()) + "\n")
|
|
96
|
+
return len(tasks)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Task and benchmark specifications for the TRACE Agent Harness Plugin."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Any, Optional
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class HarnessTaskSpec(BaseModel):
|
|
8
|
+
"""Standardized task instance specification for AI coding agent harnesses.
|
|
9
|
+
|
|
10
|
+
Compatible with SWE-bench, OpenHands, and Antigravity task schemas.
|
|
11
|
+
"""
|
|
12
|
+
instance_id: str
|
|
13
|
+
finding_id: str
|
|
14
|
+
title: str
|
|
15
|
+
category: str
|
|
16
|
+
severity: str
|
|
17
|
+
repo_name: str
|
|
18
|
+
endpoint: str
|
|
19
|
+
source_file: str
|
|
20
|
+
line_start: int
|
|
21
|
+
line_end: int
|
|
22
|
+
code_snippet: str
|
|
23
|
+
problem_statement: str
|
|
24
|
+
hints_text: str
|
|
25
|
+
attack_path_hops: List[str] = Field(default_factory=list)
|
|
26
|
+
runtime_exploit_proof: List[str] = Field(default_factory=list)
|
|
27
|
+
reproduction_steps: List[str] = Field(default_factory=list)
|
|
28
|
+
fail_to_pass: List[str] = Field(default_factory=list)
|
|
29
|
+
pass_to_pass: List[str] = Field(default_factory=list)
|
|
30
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
def to_swebench_dict(self) -> Dict[str, Any]:
|
|
33
|
+
"""Convert this task instance into a standard SWE-bench JSON object."""
|
|
34
|
+
return {
|
|
35
|
+
"instance_id": self.instance_id,
|
|
36
|
+
"repo": self.repo_name,
|
|
37
|
+
"base_commit": self.metadata.get("base_commit", "HEAD"),
|
|
38
|
+
"problem_statement": self.problem_statement,
|
|
39
|
+
"hints_text": self.hints_text,
|
|
40
|
+
"created_at": self.metadata.get("created_at", "2026-10-03T00:00:00Z"),
|
|
41
|
+
"version": "1.0",
|
|
42
|
+
"FAIL_TO_PASS": self.fail_to_pass,
|
|
43
|
+
"PASS_TO_PASS": self.pass_to_pass,
|
|
44
|
+
"environment_setup_commit": self.metadata.get("environment_setup_commit", "HEAD"),
|
|
45
|
+
"target_endpoint": self.endpoint,
|
|
46
|
+
"source_location": f"{self.source_file}:{self.line_start}-{self.line_end}",
|
|
47
|
+
"vulnerability_category": self.category,
|
|
48
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Scope Guard for enforcing local-only and lab-authorized boundaries."""
|
|
2
|
+
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
from typing import List, Set
|
|
5
|
+
import ipaddress
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ScopeViolationError(Exception):
|
|
9
|
+
"""Raised when an action violates the defined target scope."""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ScopeGuard:
|
|
14
|
+
"""Enforces strict boundaries to ensure tests only execute against approved local/lab targets."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
allowed_hosts: List[str] = None,
|
|
19
|
+
allowed_ports: List[int] = None,
|
|
20
|
+
mode: str = "local",
|
|
21
|
+
):
|
|
22
|
+
self.mode = mode
|
|
23
|
+
self.allowed_hosts: Set[str] = set(
|
|
24
|
+
allowed_hosts or ["localhost", "127.0.0.1", "0.0.0.0", "::1"]
|
|
25
|
+
)
|
|
26
|
+
self.allowed_ports: Set[int] = set(
|
|
27
|
+
allowed_ports or [80, 443, 3000, 5000, 8000, 8080, 18080]
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def validate_url(self, url: str) -> bool:
|
|
31
|
+
"""Validates that a URL is strictly within the allowed target scope."""
|
|
32
|
+
parsed = urlparse(url)
|
|
33
|
+
if parsed.scheme not in ("http", "https"):
|
|
34
|
+
raise ScopeViolationError(f"Unsupported URL scheme: {parsed.scheme}")
|
|
35
|
+
|
|
36
|
+
hostname = parsed.hostname or ""
|
|
37
|
+
if not hostname:
|
|
38
|
+
raise ScopeViolationError(f"Invalid URL host: {url}")
|
|
39
|
+
|
|
40
|
+
# Check hostname directly
|
|
41
|
+
if hostname.lower() in self.allowed_hosts:
|
|
42
|
+
pass
|
|
43
|
+
else:
|
|
44
|
+
# Check if it is a private IP in private-lab mode
|
|
45
|
+
try:
|
|
46
|
+
ip = ipaddress.ip_address(hostname)
|
|
47
|
+
if ip.is_loopback or (self.mode == "private-lab" and ip.is_private):
|
|
48
|
+
pass
|
|
49
|
+
else:
|
|
50
|
+
raise ScopeViolationError(
|
|
51
|
+
f"Target host '{hostname}' is not within local/private scope. External network requests are blocked by TRACE ScopeGuard."
|
|
52
|
+
)
|
|
53
|
+
except ValueError:
|
|
54
|
+
# Hostname is not an IP address and not in allowed hosts
|
|
55
|
+
raise ScopeViolationError(
|
|
56
|
+
f"Target host '{hostname}' is blocked by TRACE ScopeGuard. Only approved local targets are permitted."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
|
61
|
+
except ValueError as e:
|
|
62
|
+
raise ScopeViolationError(f"Invalid URL port: {e}")
|
|
63
|
+
|
|
64
|
+
is_loopback_host = hostname.lower() in ("localhost", "127.0.0.1", "0.0.0.0", "::1")
|
|
65
|
+
if not is_loopback_host and port not in self.allowed_ports:
|
|
66
|
+
raise ScopeViolationError(
|
|
67
|
+
f"Target port {port} is not in allowed ports list: {sorted(list(self.allowed_ports))}"
|
|
68
|
+
)
|
|
69
|
+
if not (1 <= port <= 65535):
|
|
70
|
+
raise ScopeViolationError(f"Invalid TCP port: {port}")
|
|
71
|
+
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
def allow_target(self, url: str) -> None:
|
|
75
|
+
"""Explicitly add a target's host and port to allowed scope if it is local/private."""
|
|
76
|
+
parsed = urlparse(url)
|
|
77
|
+
if parsed.hostname:
|
|
78
|
+
self.allowed_hosts.add(parsed.hostname.lower())
|
|
79
|
+
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
|
80
|
+
self.allowed_ports.add(port)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Runtime test execution module."""
|
|
2
|
+
|
|
3
|
+
from trace_engine.runtime.target import Target
|
|
4
|
+
from trace_engine.runtime.observations import RuntimeObservation
|
|
5
|
+
from trace_engine.runtime.client import ScopedHttpClient
|
|
6
|
+
|
|
7
|
+
__all__ = ["Target", "RuntimeObservation", "ScopedHttpClient"]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Scoped runtime HTTP client for executing controlled security tests."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import httpx
|
|
5
|
+
from typing import Optional, Dict, Any
|
|
6
|
+
from trace_engine.policy.scope import ScopeGuard, ScopeViolationError
|
|
7
|
+
from trace_engine.runtime.observations import RuntimeObservation
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ScopedHttpClient:
|
|
11
|
+
"""HTTP client bound to ScopeGuard that captures comprehensive runtime observations."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, scope_guard: ScopeGuard, timeout_seconds: float = 8.0):
|
|
14
|
+
self.scope_guard = scope_guard
|
|
15
|
+
self.timeout = timeout_seconds
|
|
16
|
+
|
|
17
|
+
def execute(
|
|
18
|
+
self,
|
|
19
|
+
method: str,
|
|
20
|
+
url: str,
|
|
21
|
+
headers: Optional[Dict[str, str]] = None,
|
|
22
|
+
json_body: Optional[Any] = None,
|
|
23
|
+
data: Optional[Dict[str, Any]] = None,
|
|
24
|
+
) -> RuntimeObservation:
|
|
25
|
+
"""Executes request after scope validation and captures runtime observation."""
|
|
26
|
+
self.scope_guard.validate_url(url)
|
|
27
|
+
|
|
28
|
+
headers = headers or {}
|
|
29
|
+
req_body_str = None
|
|
30
|
+
if json_body is not None:
|
|
31
|
+
import json
|
|
32
|
+
req_body_str = json.dumps(json_body)
|
|
33
|
+
headers["Content-Type"] = "application/json"
|
|
34
|
+
|
|
35
|
+
start_time = time.perf_counter()
|
|
36
|
+
try:
|
|
37
|
+
with httpx.Client(
|
|
38
|
+
timeout=self.timeout,
|
|
39
|
+
follow_redirects=False, # Intercept redirects for scope check
|
|
40
|
+
) as client:
|
|
41
|
+
resp = client.request(
|
|
42
|
+
method=method.upper(),
|
|
43
|
+
url=url,
|
|
44
|
+
headers=headers,
|
|
45
|
+
content=req_body_str,
|
|
46
|
+
data=data,
|
|
47
|
+
)
|
|
48
|
+
latency_ms = (time.perf_counter() - start_time) * 1000
|
|
49
|
+
|
|
50
|
+
# Check if redirect is safe
|
|
51
|
+
if resp.is_redirect and "location" in resp.headers:
|
|
52
|
+
redirect_target = resp.headers["location"]
|
|
53
|
+
import urllib.parse
|
|
54
|
+
resolved_redirect = urllib.parse.urljoin(url, redirect_target)
|
|
55
|
+
self.scope_guard.validate_url(resolved_redirect)
|
|
56
|
+
|
|
57
|
+
return RuntimeObservation(
|
|
58
|
+
request_method=method.upper(),
|
|
59
|
+
request_url=url,
|
|
60
|
+
request_headers=dict(headers),
|
|
61
|
+
request_body=req_body_str,
|
|
62
|
+
response_status=resp.status_code,
|
|
63
|
+
response_headers=dict(resp.headers),
|
|
64
|
+
response_body=resp.text[:2000], # preview up to 2KB
|
|
65
|
+
latency_ms=round(latency_ms, 2),
|
|
66
|
+
)
|
|
67
|
+
except ScopeViolationError:
|
|
68
|
+
raise
|
|
69
|
+
except Exception as e:
|
|
70
|
+
latency_ms = (time.perf_counter() - start_time) * 1000
|
|
71
|
+
return RuntimeObservation(
|
|
72
|
+
request_method=method.upper(),
|
|
73
|
+
request_url=url,
|
|
74
|
+
request_headers=dict(headers),
|
|
75
|
+
request_body=req_body_str,
|
|
76
|
+
response_status=0,
|
|
77
|
+
response_headers={},
|
|
78
|
+
response_body=f"Connection Error: {str(e)}",
|
|
79
|
+
latency_ms=round(latency_ms, 2),
|
|
80
|
+
observation_notes=[f"Execution exception: {e}"],
|
|
81
|
+
)
|