@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,145 @@
|
|
|
1
|
+
"""Spring Boot Java and Kotlin framework adapter."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List, Dict, Optional
|
|
5
|
+
from trace_engine.parsing.parser import ParsedFile
|
|
6
|
+
from trace_engine.framework.base import FrameworkAdapter, Endpoint, EndpointParameter
|
|
7
|
+
from trace_engine.parsing.locations import SourceLocation
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SpringBootFrameworkAdapter(FrameworkAdapter):
|
|
11
|
+
"""Detects and extracts Spring Boot REST endpoints, annotations, and security policies."""
|
|
12
|
+
|
|
13
|
+
def can_handle(self, parsed_file: ParsedFile) -> bool:
|
|
14
|
+
if parsed_file.language not in ("java", "kotlin"):
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
# Check imports
|
|
18
|
+
for imp in parsed_file.imports:
|
|
19
|
+
mod_low = imp.module.lower()
|
|
20
|
+
if any(term in mod_low for term in ("org.springframework", "io.ktor", "javax.ws.rs", "jakarta.ws.rs")):
|
|
21
|
+
return True
|
|
22
|
+
|
|
23
|
+
return True
|
|
24
|
+
|
|
25
|
+
def extract_endpoints(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
|
|
26
|
+
endpoints: List[Endpoint] = []
|
|
27
|
+
norm_file_path = parsed_file.file_path.replace("\\", "/")
|
|
28
|
+
content_lower = content.lower()
|
|
29
|
+
|
|
30
|
+
# Database sinks
|
|
31
|
+
has_db_sink = any(
|
|
32
|
+
term in content_lower
|
|
33
|
+
for term in (
|
|
34
|
+
"repository", "findbyid", "findall", "save(", "deletebyid",
|
|
35
|
+
"jdbctemplate", "entitymanager", "hibernate", "jpa", "r2dbc",
|
|
36
|
+
"select ", "from "
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# External network sinks
|
|
41
|
+
has_ext_sink = any(
|
|
42
|
+
term in content_lower
|
|
43
|
+
for term in (
|
|
44
|
+
"resttemplate", "webclient", "httpclient", "feignclient",
|
|
45
|
+
"openfeign", "okhttp", "httpurlconnection"
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Class-level RequestMapping
|
|
50
|
+
class_base = ""
|
|
51
|
+
class_rm = re.search(
|
|
52
|
+
r'''@RequestMapping\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']''',
|
|
53
|
+
content
|
|
54
|
+
)
|
|
55
|
+
if class_rm:
|
|
56
|
+
class_base = "/" + class_rm.group(1).strip("/")
|
|
57
|
+
|
|
58
|
+
# Method-level mappings: @GetMapping, @PostMapping, etc.
|
|
59
|
+
annot_pat = re.compile(
|
|
60
|
+
r'''@(GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|RequestMapping)\s*(?:\(\s*(?:(?:value|path)\s*=\s*)?["']([^"']+)["']\s*\))?''',
|
|
61
|
+
re.IGNORECASE
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
verbs = {
|
|
65
|
+
"getmapping": "GET",
|
|
66
|
+
"postmapping": "POST",
|
|
67
|
+
"putmapping": "PUT",
|
|
68
|
+
"deletemapping": "DELETE",
|
|
69
|
+
"patchmapping": "PATCH",
|
|
70
|
+
"requestmapping": "GET",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for match in annot_pat.finditer(content):
|
|
74
|
+
annot = match.group(1).lower()
|
|
75
|
+
sub_path = match.group(2) or ""
|
|
76
|
+
|
|
77
|
+
# Skip class-level RequestMapping
|
|
78
|
+
after_snippet = content[match.end():match.end() + 100]
|
|
79
|
+
if re.search(r'\bclass\s+[a-zA-Z0-9_$]+', after_snippet):
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
method = verbs.get(annot, "GET")
|
|
83
|
+
line_no = content[:match.start()].count("\n") + 1
|
|
84
|
+
|
|
85
|
+
# Extract method name and parameters following the annotation
|
|
86
|
+
after_annot = content[match.end():match.end() + 500]
|
|
87
|
+
fn_match = re.search(
|
|
88
|
+
r'''(?:fun\s+|[a-zA-Z0-9_<>,?\[\]\s]+\s+)([a-zA-Z0-9_$]+)\s*\((.*?)\)''',
|
|
89
|
+
after_annot,
|
|
90
|
+
re.DOTALL
|
|
91
|
+
)
|
|
92
|
+
fn_name = fn_match.group(1) if fn_match else f"handler_L{line_no}"
|
|
93
|
+
raw_params = fn_match.group(2) if fn_match else ""
|
|
94
|
+
|
|
95
|
+
full_path = (class_base + "/" + sub_path.strip("/")).rstrip("/")
|
|
96
|
+
if not full_path:
|
|
97
|
+
full_path = "/"
|
|
98
|
+
|
|
99
|
+
path_params = re.findall(r'\{([a-zA-Z0-9_]+)\}', full_path)
|
|
100
|
+
|
|
101
|
+
# Parameters extraction from method signature
|
|
102
|
+
parameters: List[EndpointParameter] = [
|
|
103
|
+
EndpointParameter(name=p, location="path", required=True) for p in path_params
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
# Query params: @RequestParam("...")
|
|
107
|
+
for qm in re.finditer(r'''@RequestParam\s*(?:\(\s*(?:value\s*=\s*)?["']([^"']+)["']\s*\))?''', raw_params):
|
|
108
|
+
p_name = qm.group(1) or "query"
|
|
109
|
+
if not any(p.name == p_name for p in parameters):
|
|
110
|
+
parameters.append(EndpointParameter(name=p_name, location="query", required=False))
|
|
111
|
+
|
|
112
|
+
# Auth detection
|
|
113
|
+
snippet = content[max(0, match.start() - 200) : match.end() + 200].lower()
|
|
114
|
+
auth_required = any(
|
|
115
|
+
term in snippet for term in ("preauthorize", "secured", "rolesallowed", "securitycontext", "authentication")
|
|
116
|
+
) or ("admin" in full_path.lower())
|
|
117
|
+
|
|
118
|
+
roles = ["admin"] if "admin" in snippet or "admin" in full_path.lower() else []
|
|
119
|
+
|
|
120
|
+
sensitive = any(
|
|
121
|
+
term in full_path.lower() or term in content_lower
|
|
122
|
+
for term in ("admin", "user", "auth", "patient", "order", "billing", "payment", "token", "secret", "record")
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
ep_id = f"ep_jvm_{norm_file_path}_{line_no}_{method}".replace("/", "_").replace(".", "_")
|
|
126
|
+
|
|
127
|
+
endpoints.append(
|
|
128
|
+
Endpoint(
|
|
129
|
+
id=ep_id,
|
|
130
|
+
method=method,
|
|
131
|
+
path=full_path,
|
|
132
|
+
handler_name=fn_name,
|
|
133
|
+
auth_required=auth_required,
|
|
134
|
+
roles=roles,
|
|
135
|
+
parameters=parameters,
|
|
136
|
+
database_access=has_db_sink,
|
|
137
|
+
object_identifier=bool(path_params),
|
|
138
|
+
state_changing=method in ("POST", "PUT", "DELETE", "PATCH"),
|
|
139
|
+
external_network=has_ext_sink,
|
|
140
|
+
sensitive_data=sensitive,
|
|
141
|
+
source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return endpoints
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""TRACE Agent Harness package for autonomous security auditing, benchmarking, and self-healing."""
|
|
2
|
+
|
|
3
|
+
from trace_engine.harness.context import (
|
|
4
|
+
AgentRemediationPackage,
|
|
5
|
+
PatchResult,
|
|
6
|
+
HarnessExecutionReport,
|
|
7
|
+
)
|
|
8
|
+
from trace_engine.harness.patcher import SafePatcher
|
|
9
|
+
from trace_engine.harness.remediators import AutonomousRemediator
|
|
10
|
+
from trace_engine.harness.engine import AgentHarness
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AgentHarness",
|
|
14
|
+
"AgentRemediationPackage",
|
|
15
|
+
"PatchResult",
|
|
16
|
+
"HarnessExecutionReport",
|
|
17
|
+
"SafePatcher",
|
|
18
|
+
"AutonomousRemediator",
|
|
19
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""TRACE-Bench: Standardized Autonomous Cybersecurity Evaluation Harness for AI Coding Models."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, Any, List, Optional
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from trace_engine.harness.engine import AgentHarness
|
|
9
|
+
from trace_engine.harness.context import HarnessExecutionReport
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BenchmarkScorecard(BaseModel):
|
|
13
|
+
"""Standardized AI model benchmark scorecard generated by TRACE-Bench."""
|
|
14
|
+
benchmark_name: str = "TRACE-Bench v1.0"
|
|
15
|
+
model_name: str
|
|
16
|
+
target_repository: str
|
|
17
|
+
baseline_posture_score: int
|
|
18
|
+
final_posture_score: int
|
|
19
|
+
score_delta: int
|
|
20
|
+
total_security_exploits: int
|
|
21
|
+
exploits_remediated: int
|
|
22
|
+
remediation_success_rate: float
|
|
23
|
+
time_elapsed_seconds: float
|
|
24
|
+
pass_criteria_met: bool
|
|
25
|
+
findings_audit: List[Dict[str, Any]] = Field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TRACEBench:
|
|
29
|
+
"""Evaluation harness for testing and grading AI coding agents on OWASP API security remediations."""
|
|
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.harness = AgentHarness(self.repo_path, target_url=self.target_url)
|
|
35
|
+
|
|
36
|
+
def evaluate(self, model_name: str = "Autonomous Agent") -> BenchmarkScorecard:
|
|
37
|
+
"""Executes full benchmark evaluation run."""
|
|
38
|
+
report: HarnessExecutionReport = self.harness.run_self_healing_loop()
|
|
39
|
+
|
|
40
|
+
pass_threshold = 70.0 # 70% fix rate required to pass
|
|
41
|
+
passed = report.fix_rate_percent >= pass_threshold and report.final_score >= 80
|
|
42
|
+
|
|
43
|
+
return BenchmarkScorecard(
|
|
44
|
+
model_name=model_name,
|
|
45
|
+
target_repository=self.repo_path.name,
|
|
46
|
+
baseline_posture_score=report.baseline_score,
|
|
47
|
+
final_posture_score=report.final_score,
|
|
48
|
+
score_delta=report.score_delta,
|
|
49
|
+
total_security_exploits=report.total_findings,
|
|
50
|
+
exploits_remediated=report.fixed_findings,
|
|
51
|
+
remediation_success_rate=report.fix_rate_percent,
|
|
52
|
+
time_elapsed_seconds=report.execution_time_seconds,
|
|
53
|
+
pass_criteria_met=passed,
|
|
54
|
+
findings_audit=report.verified_patches,
|
|
55
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Agent remediation context and task models for the TRACE Agent Harness."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Any, Optional
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AgentRemediationPackage(BaseModel):
|
|
8
|
+
"""Self-contained remediation context payload delivered to an autonomous coding agent."""
|
|
9
|
+
finding_id: str
|
|
10
|
+
title: str
|
|
11
|
+
category: str
|
|
12
|
+
severity: str
|
|
13
|
+
priority_rank: int
|
|
14
|
+
endpoint: str
|
|
15
|
+
source_file: str
|
|
16
|
+
line_start: int
|
|
17
|
+
line_end: int
|
|
18
|
+
code_snippet: str
|
|
19
|
+
attack_path_hops: List[str] = Field(default_factory=list)
|
|
20
|
+
runtime_exploit_proof: List[str] = Field(default_factory=list)
|
|
21
|
+
remediation_guidance: str
|
|
22
|
+
reproduction_steps: List[str] = Field(default_factory=list)
|
|
23
|
+
verification_criteria: str
|
|
24
|
+
raw_observation: Optional[Dict[str, Any]] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PatchResult(BaseModel):
|
|
28
|
+
"""Result of applying a code modification via the Agent Harness."""
|
|
29
|
+
finding_id: str
|
|
30
|
+
target_file: str
|
|
31
|
+
success: bool
|
|
32
|
+
backup_file: Optional[str] = None
|
|
33
|
+
diff: Optional[str] = None
|
|
34
|
+
error: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class HarnessExecutionReport(BaseModel):
|
|
38
|
+
"""Outcome of a complete Agent Harness remediation or benchmark run."""
|
|
39
|
+
project_name: str
|
|
40
|
+
baseline_score: int
|
|
41
|
+
final_score: int
|
|
42
|
+
score_delta: int
|
|
43
|
+
total_findings: int
|
|
44
|
+
fixed_findings: int
|
|
45
|
+
remaining_findings: int
|
|
46
|
+
fix_rate_percent: float
|
|
47
|
+
verified_patches: List[Dict[str, Any]] = Field(default_factory=list)
|
|
48
|
+
unresolved_findings: List[str] = Field(default_factory=list)
|
|
49
|
+
execution_time_seconds: float
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Core Agent Harness execution engine for autonomous security auditing and self-healing."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
from trace_engine.findings.model import Finding, Severity
|
|
8
|
+
from trace_engine.findings.store import FindingStore
|
|
9
|
+
from trace_engine.verify import VerificationEngine, VerificationResult, VerificationStatus
|
|
10
|
+
from trace_engine.harness.context import (
|
|
11
|
+
AgentRemediationPackage,
|
|
12
|
+
PatchResult,
|
|
13
|
+
HarnessExecutionReport,
|
|
14
|
+
)
|
|
15
|
+
from trace_engine.harness.patcher import SafePatcher
|
|
16
|
+
from trace_engine.harness.remediators import AutonomousRemediator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AgentHarness:
|
|
20
|
+
"""The TRACE Agent Harness provides an autonomous execution and verification environment
|
|
21
|
+
|
|
22
|
+
for AI coding models to inspect, patch, and mathematically prove security remediations.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, repo_path: Path, target_url: str = "http://127.0.0.1:18080"):
|
|
26
|
+
self.repo_path = repo_path.resolve()
|
|
27
|
+
self.target_url = target_url
|
|
28
|
+
self.trace_dir = self.repo_path / ".trace"
|
|
29
|
+
self.store = FindingStore(self.trace_dir)
|
|
30
|
+
self.patcher = SafePatcher(self.repo_path)
|
|
31
|
+
self.verifier = VerificationEngine(self.repo_path, target_url=target_url)
|
|
32
|
+
|
|
33
|
+
def get_findings(self) -> List[Finding]:
|
|
34
|
+
"""Load currently stored findings ordered strictly by priority."""
|
|
35
|
+
raw_findings = self.store.load_findings()
|
|
36
|
+
severity_order = {
|
|
37
|
+
Severity.CRITICAL: 0,
|
|
38
|
+
Severity.HIGH: 1,
|
|
39
|
+
Severity.MEDIUM: 2,
|
|
40
|
+
Severity.LOW: 3,
|
|
41
|
+
Severity.INFO: 4,
|
|
42
|
+
}
|
|
43
|
+
return sorted(raw_findings, key=lambda f: severity_order.get(f.severity, 5))
|
|
44
|
+
|
|
45
|
+
def calculate_posture_score(self, findings: List[Finding]) -> int:
|
|
46
|
+
"""Calculates 0-100 security health posture score."""
|
|
47
|
+
crit = sum(1 for f in findings if f.severity == Severity.CRITICAL)
|
|
48
|
+
high = sum(1 for f in findings if f.severity == Severity.HIGH)
|
|
49
|
+
med = sum(1 for f in findings if f.severity == Severity.MEDIUM)
|
|
50
|
+
return max(0, 100 - (crit * 20 + high * 10 + med * 3))
|
|
51
|
+
|
|
52
|
+
def build_remediation_package(self, finding: Finding, rank: int = 1) -> AgentRemediationPackage:
|
|
53
|
+
"""Packages rich context, surrounding AST lines, and exploit proof for an AI agent."""
|
|
54
|
+
file_path = self.repo_path / finding.source_location.file
|
|
55
|
+
if not file_path.exists():
|
|
56
|
+
for sub in self.repo_path.iterdir():
|
|
57
|
+
if sub.is_dir() and (sub / finding.source_location.file).exists():
|
|
58
|
+
file_path = sub / finding.source_location.file
|
|
59
|
+
break
|
|
60
|
+
|
|
61
|
+
code_snippet = ""
|
|
62
|
+
if file_path.exists():
|
|
63
|
+
lines = file_path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
64
|
+
start = max(0, finding.source_location.line_start - 5)
|
|
65
|
+
end = min(len(lines), finding.source_location.line_end + 10)
|
|
66
|
+
code_snippet = "\n".join(lines[start:end])
|
|
67
|
+
|
|
68
|
+
return AgentRemediationPackage(
|
|
69
|
+
finding_id=finding.id,
|
|
70
|
+
title=finding.title,
|
|
71
|
+
category=finding.category,
|
|
72
|
+
severity=finding.severity.value,
|
|
73
|
+
priority_rank=rank,
|
|
74
|
+
endpoint=finding.endpoint,
|
|
75
|
+
source_file=finding.source_location.file,
|
|
76
|
+
line_start=finding.source_location.line_start,
|
|
77
|
+
line_end=finding.source_location.line_end,
|
|
78
|
+
code_snippet=code_snippet,
|
|
79
|
+
attack_path_hops=finding.attack_path,
|
|
80
|
+
runtime_exploit_proof=finding.runtime_evidence,
|
|
81
|
+
remediation_guidance=finding.remediation,
|
|
82
|
+
reproduction_steps=finding.reproduction_steps,
|
|
83
|
+
verification_criteria=(
|
|
84
|
+
f"Neutralize exploit so re-executing {finding.endpoint} receives HTTP 401/403 "
|
|
85
|
+
"or validates tenant/object authorization boundaries."
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def get_next_target(self) -> Optional[AgentRemediationPackage]:
|
|
90
|
+
"""Returns the highest priority unmitigated finding packaged for agent consumption."""
|
|
91
|
+
findings = self.get_findings()
|
|
92
|
+
if not findings:
|
|
93
|
+
return None
|
|
94
|
+
return self.build_remediation_package(findings[0], rank=1)
|
|
95
|
+
|
|
96
|
+
def apply_patch(
|
|
97
|
+
self,
|
|
98
|
+
finding_id: str,
|
|
99
|
+
rel_path: str,
|
|
100
|
+
target_content: str,
|
|
101
|
+
replacement_content: str,
|
|
102
|
+
) -> PatchResult:
|
|
103
|
+
"""Applies patch with transactional backup."""
|
|
104
|
+
return self.patcher.apply_replacement(
|
|
105
|
+
finding_id=finding_id,
|
|
106
|
+
rel_path=rel_path,
|
|
107
|
+
target_content=target_content,
|
|
108
|
+
replacement_content=replacement_content,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def verify_patch(self, finding_id: str) -> VerificationResult:
|
|
112
|
+
"""Verifies patch via AST re-indexing and live runtime test pack replay."""
|
|
113
|
+
return self.verifier.verify(finding_id)
|
|
114
|
+
|
|
115
|
+
def run_self_healing_loop(self) -> HarnessExecutionReport:
|
|
116
|
+
"""Autonomous self-healing execution loop:
|
|
117
|
+
|
|
118
|
+
Iterates through prioritized findings, synthesizes defensive patches,
|
|
119
|
+
applies them transactionally, and verifies runtime neutralization.
|
|
120
|
+
"""
|
|
121
|
+
t0 = time.perf_counter()
|
|
122
|
+
initial_findings = self.get_findings()
|
|
123
|
+
baseline_score = self.calculate_posture_score(initial_findings)
|
|
124
|
+
|
|
125
|
+
verified_patches = []
|
|
126
|
+
unresolved = []
|
|
127
|
+
|
|
128
|
+
for idx, finding in enumerate(initial_findings, 1):
|
|
129
|
+
pkg = self.build_remediation_package(finding, rank=idx)
|
|
130
|
+
synth_patch = AutonomousRemediator.generate_patch(pkg)
|
|
131
|
+
|
|
132
|
+
if not synth_patch:
|
|
133
|
+
unresolved.append(finding.id)
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
target_block, replacement_block = synth_patch
|
|
137
|
+
patch_res = self.apply_patch(
|
|
138
|
+
finding_id=finding.id,
|
|
139
|
+
rel_path=finding.source_location.file,
|
|
140
|
+
target_content=target_block,
|
|
141
|
+
replacement_content=replacement_block,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if not patch_res.success:
|
|
145
|
+
unresolved.append(finding.id)
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
# Verify differential impact with verification oracle
|
|
149
|
+
v_res = self.verify_patch(finding.id)
|
|
150
|
+
if v_res.status == VerificationStatus.FIXED:
|
|
151
|
+
verified_patches.append({
|
|
152
|
+
"finding_id": finding.id,
|
|
153
|
+
"title": finding.title,
|
|
154
|
+
"file": finding.source_location.file,
|
|
155
|
+
"diff": patch_res.diff,
|
|
156
|
+
"verification": v_res.model_dump(),
|
|
157
|
+
})
|
|
158
|
+
else:
|
|
159
|
+
# Verification failed or broke runtime tests: cleanly roll back patch
|
|
160
|
+
if patch_res.backup_file:
|
|
161
|
+
self.patcher.rollback(finding.source_location.file, Path(patch_res.backup_file))
|
|
162
|
+
unresolved.append(finding.id)
|
|
163
|
+
|
|
164
|
+
remaining_count = len(unresolved)
|
|
165
|
+
fixed_count = len(verified_patches)
|
|
166
|
+
total_count = len(initial_findings)
|
|
167
|
+
|
|
168
|
+
# New score calculation based on neutralized findings
|
|
169
|
+
final_score = min(100, baseline_score + (fixed_count * 15))
|
|
170
|
+
if fixed_count == total_count and total_count > 0:
|
|
171
|
+
final_score = 100
|
|
172
|
+
|
|
173
|
+
elapsed = time.perf_counter() - t0
|
|
174
|
+
|
|
175
|
+
return HarnessExecutionReport(
|
|
176
|
+
project_name=self.repo_path.name,
|
|
177
|
+
baseline_score=baseline_score,
|
|
178
|
+
final_score=final_score,
|
|
179
|
+
score_delta=final_score - baseline_score,
|
|
180
|
+
total_findings=total_count,
|
|
181
|
+
fixed_findings=fixed_count,
|
|
182
|
+
remaining_findings=remaining_count,
|
|
183
|
+
fix_rate_percent=round((fixed_count / total_count * 100), 1) if total_count else 100.0,
|
|
184
|
+
verified_patches=verified_patches,
|
|
185
|
+
unresolved_findings=unresolved,
|
|
186
|
+
execution_time_seconds=round(elapsed, 2),
|
|
187
|
+
)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Safe code patching and rollback engine for the TRACE Agent Harness."""
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
import shutil
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, Tuple
|
|
8
|
+
from trace_engine.harness.context import PatchResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SafePatcher:
|
|
12
|
+
"""Manages transactional file edits, backups, and rollback for AI coding agents."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, workspace_root: Path):
|
|
15
|
+
self.workspace_root = workspace_root.resolve()
|
|
16
|
+
self.backup_dir = self.workspace_root / ".trace" / "backups"
|
|
17
|
+
self.backup_dir.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
|
|
19
|
+
def backup(self, rel_path: str) -> Path:
|
|
20
|
+
"""Create timestamped backup of the target file."""
|
|
21
|
+
target = self.workspace_root / rel_path
|
|
22
|
+
if not target.exists():
|
|
23
|
+
raise FileNotFoundError(f"Target file {rel_path} does not exist in workspace.")
|
|
24
|
+
|
|
25
|
+
timestamp = int(time.time() * 1000)
|
|
26
|
+
safe_name = rel_path.replace("/", "_").replace("\\", "_")
|
|
27
|
+
backup_path = self.backup_dir / f"{safe_name}.{timestamp}.bak"
|
|
28
|
+
shutil.copy2(target, backup_path)
|
|
29
|
+
return backup_path
|
|
30
|
+
|
|
31
|
+
def apply_replacement(
|
|
32
|
+
self,
|
|
33
|
+
finding_id: str,
|
|
34
|
+
rel_path: str,
|
|
35
|
+
target_content: str,
|
|
36
|
+
replacement_content: str,
|
|
37
|
+
) -> PatchResult:
|
|
38
|
+
"""Replace target string block with replacement content, creating a rollback backup."""
|
|
39
|
+
file_path = self.workspace_root / rel_path
|
|
40
|
+
if not file_path.exists():
|
|
41
|
+
return PatchResult(
|
|
42
|
+
finding_id=finding_id,
|
|
43
|
+
target_file=rel_path,
|
|
44
|
+
success=False,
|
|
45
|
+
error=f"File not found: {rel_path}",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
original = file_path.read_text(encoding="utf-8")
|
|
50
|
+
if target_content not in original:
|
|
51
|
+
return PatchResult(
|
|
52
|
+
finding_id=finding_id,
|
|
53
|
+
target_file=rel_path,
|
|
54
|
+
success=False,
|
|
55
|
+
error="Target content block not found in file.",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
backup_file = self.backup(rel_path)
|
|
59
|
+
new_content = original.replace(target_content, replacement_content, 1)
|
|
60
|
+
file_path.write_text(new_content, encoding="utf-8")
|
|
61
|
+
|
|
62
|
+
# Generate unified diff
|
|
63
|
+
diff_lines = list(
|
|
64
|
+
difflib.unified_diff(
|
|
65
|
+
original.splitlines(keepends=True),
|
|
66
|
+
new_content.splitlines(keepends=True),
|
|
67
|
+
fromfile=f"a/{rel_path}",
|
|
68
|
+
tofile=f"b/{rel_path}",
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
diff_str = "".join(diff_lines)
|
|
72
|
+
|
|
73
|
+
return PatchResult(
|
|
74
|
+
finding_id=finding_id,
|
|
75
|
+
target_file=rel_path,
|
|
76
|
+
success=True,
|
|
77
|
+
backup_file=str(backup_file),
|
|
78
|
+
diff=diff_str,
|
|
79
|
+
)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
return PatchResult(
|
|
82
|
+
finding_id=finding_id,
|
|
83
|
+
target_file=rel_path,
|
|
84
|
+
success=False,
|
|
85
|
+
error=str(e),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def apply_full_content(
|
|
89
|
+
self,
|
|
90
|
+
finding_id: str,
|
|
91
|
+
rel_path: str,
|
|
92
|
+
new_content: str,
|
|
93
|
+
) -> PatchResult:
|
|
94
|
+
"""Write entire new file content, creating a rollback backup."""
|
|
95
|
+
file_path = self.workspace_root / rel_path
|
|
96
|
+
if not file_path.exists():
|
|
97
|
+
return PatchResult(
|
|
98
|
+
finding_id=finding_id,
|
|
99
|
+
target_file=rel_path,
|
|
100
|
+
success=False,
|
|
101
|
+
error=f"File not found: {rel_path}",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
original = file_path.read_text(encoding="utf-8")
|
|
106
|
+
backup_file = self.backup(rel_path)
|
|
107
|
+
file_path.write_text(new_content, encoding="utf-8")
|
|
108
|
+
|
|
109
|
+
diff_lines = list(
|
|
110
|
+
difflib.unified_diff(
|
|
111
|
+
original.splitlines(keepends=True),
|
|
112
|
+
new_content.splitlines(keepends=True),
|
|
113
|
+
fromfile=f"a/{rel_path}",
|
|
114
|
+
tofile=f"b/{rel_path}",
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
return PatchResult(
|
|
118
|
+
finding_id=finding_id,
|
|
119
|
+
target_file=rel_path,
|
|
120
|
+
success=True,
|
|
121
|
+
backup_file=str(backup_file),
|
|
122
|
+
diff="".join(diff_lines),
|
|
123
|
+
)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
return PatchResult(
|
|
126
|
+
finding_id=finding_id,
|
|
127
|
+
target_file=rel_path,
|
|
128
|
+
success=False,
|
|
129
|
+
error=str(e),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def rollback(self, rel_path: str, backup_path: Path) -> bool:
|
|
133
|
+
"""Restore file from backup."""
|
|
134
|
+
target = self.workspace_root / rel_path
|
|
135
|
+
if backup_path.exists():
|
|
136
|
+
shutil.copy2(backup_path, target)
|
|
137
|
+
return True
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
TransactionalPatcher = SafePatcher
|
|
142
|
+
PatchOperationResult = PatchResult
|
|
143
|
+
|