@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,208 @@
|
|
|
1
|
+
"""Constructs the Attack-Path Model from parsed files and discovered endpoints."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Dict, Optional
|
|
5
|
+
from trace_engine.ingest.files import SourceFile
|
|
6
|
+
from trace_engine.parsing.parser import ParsedFile
|
|
7
|
+
from trace_engine.framework.base import Endpoint
|
|
8
|
+
from trace_engine.apm.model import AttackPathModel
|
|
9
|
+
from trace_engine.apm.nodes import APMNode, NodeType
|
|
10
|
+
from trace_engine.apm.edges import APMEdge, EdgeType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class APMBuilder:
|
|
14
|
+
"""Orchestrates building an Attack-Path Model graph from static analysis artifacts."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, project_name: str = "project"):
|
|
17
|
+
self.project_name = project_name
|
|
18
|
+
|
|
19
|
+
def build(
|
|
20
|
+
self,
|
|
21
|
+
repo_path: Path,
|
|
22
|
+
source_files: List[SourceFile],
|
|
23
|
+
parsed_files: List[ParsedFile],
|
|
24
|
+
endpoints: List[Endpoint],
|
|
25
|
+
) -> AttackPathModel:
|
|
26
|
+
apm = AttackPathModel(name=self.project_name)
|
|
27
|
+
|
|
28
|
+
# 1. Root Repository Node
|
|
29
|
+
repo_node_id = "repo_root"
|
|
30
|
+
apm.add_node(
|
|
31
|
+
APMNode(
|
|
32
|
+
id=repo_node_id,
|
|
33
|
+
node_type=NodeType.REPOSITORY,
|
|
34
|
+
label=self.project_name,
|
|
35
|
+
properties={"path": str(repo_path.resolve())},
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# 2. File Nodes
|
|
40
|
+
file_node_map: Dict[str, str] = {}
|
|
41
|
+
for sf in source_files:
|
|
42
|
+
file_id = f"file_{sf.path.replace('/', '_').replace('.', '_')}"
|
|
43
|
+
file_node_map[sf.path] = file_id
|
|
44
|
+
apm.add_node(
|
|
45
|
+
APMNode(
|
|
46
|
+
id=file_id,
|
|
47
|
+
node_type=NodeType.FILE,
|
|
48
|
+
label=sf.path,
|
|
49
|
+
properties={
|
|
50
|
+
"language": sf.language,
|
|
51
|
+
"line_count": sf.line_count,
|
|
52
|
+
"sha256": sf.sha256,
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
apm.add_edge(
|
|
57
|
+
APMEdge(
|
|
58
|
+
source_id=repo_node_id,
|
|
59
|
+
target_id=file_id,
|
|
60
|
+
edge_type=EdgeType.CONTAINS,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# 3. Function Nodes
|
|
65
|
+
func_node_map: Dict[str, str] = {}
|
|
66
|
+
for pf in parsed_files:
|
|
67
|
+
file_id = file_node_map.get(pf.file_path, repo_node_id)
|
|
68
|
+
for fn in pf.functions:
|
|
69
|
+
fn_id = f"fn_{pf.file_path}_{fn.qualified_name}".replace("/", "_").replace(".", "_")
|
|
70
|
+
func_node_map[fn.qualified_name] = fn_id
|
|
71
|
+
apm.add_node(
|
|
72
|
+
APMNode(
|
|
73
|
+
id=fn_id,
|
|
74
|
+
node_type=NodeType.FUNCTION,
|
|
75
|
+
label=fn.qualified_name,
|
|
76
|
+
location=fn.location,
|
|
77
|
+
properties={"is_async": fn.is_async},
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
apm.add_edge(
|
|
81
|
+
APMEdge(
|
|
82
|
+
source_id=file_id,
|
|
83
|
+
target_id=fn_id,
|
|
84
|
+
edge_type=EdgeType.CONTAINS,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# 4. Endpoints & Parameters
|
|
89
|
+
for ep in endpoints:
|
|
90
|
+
ep_node_id = ep.id
|
|
91
|
+
apm.add_node(
|
|
92
|
+
APMNode(
|
|
93
|
+
id=ep_node_id,
|
|
94
|
+
node_type=NodeType.ENDPOINT,
|
|
95
|
+
label=ep.display_name(),
|
|
96
|
+
location=ep.source,
|
|
97
|
+
properties={
|
|
98
|
+
"method": ep.method,
|
|
99
|
+
"path": ep.path,
|
|
100
|
+
"auth_required": ep.auth_required,
|
|
101
|
+
"roles": ep.roles,
|
|
102
|
+
"object_identifier": ep.object_identifier,
|
|
103
|
+
"state_changing": ep.state_changing,
|
|
104
|
+
"sensitive_data": ep.sensitive_data,
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Connect File -> Endpoint
|
|
110
|
+
file_id = file_node_map.get(ep.source.file, repo_node_id)
|
|
111
|
+
apm.add_edge(
|
|
112
|
+
APMEdge(
|
|
113
|
+
source_id=file_id,
|
|
114
|
+
target_id=ep_node_id,
|
|
115
|
+
edge_type=EdgeType.EXPOSES,
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Connect Endpoint -> Handler Function
|
|
120
|
+
if ep.handler_name in func_node_map:
|
|
121
|
+
apm.add_edge(
|
|
122
|
+
APMEdge(
|
|
123
|
+
source_id=ep_node_id,
|
|
124
|
+
target_id=func_node_map[ep.handler_name],
|
|
125
|
+
edge_type=EdgeType.ROUTES_TO,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Parameters
|
|
130
|
+
for param in ep.parameters:
|
|
131
|
+
param_id = f"param_{ep.id}_{param.name}"
|
|
132
|
+
apm.add_node(
|
|
133
|
+
APMNode(
|
|
134
|
+
id=param_id,
|
|
135
|
+
node_type=NodeType.PARAMETER,
|
|
136
|
+
label=param.name,
|
|
137
|
+
properties={
|
|
138
|
+
"location": param.location,
|
|
139
|
+
"type": param.param_type,
|
|
140
|
+
"user_controlled": param.user_controlled,
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
apm.add_edge(
|
|
145
|
+
APMEdge(
|
|
146
|
+
source_id=ep_node_id,
|
|
147
|
+
target_id=param_id,
|
|
148
|
+
edge_type=EdgeType.RECEIVES,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Auth Gate Node
|
|
153
|
+
if ep.auth_required:
|
|
154
|
+
auth_id = f"auth_{ep.id}"
|
|
155
|
+
apm.add_node(
|
|
156
|
+
APMNode(
|
|
157
|
+
id=auth_id,
|
|
158
|
+
node_type=NodeType.AUTH_CHECK,
|
|
159
|
+
label="AuthGate" if not ep.roles else f"RoleGate({','.join(ep.roles)})",
|
|
160
|
+
properties={"roles": ep.roles},
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
apm.add_edge(
|
|
164
|
+
APMEdge(
|
|
165
|
+
source_id=auth_id,
|
|
166
|
+
target_id=ep_node_id,
|
|
167
|
+
edge_type=EdgeType.PROTECTS,
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Sinks / Database Nodes
|
|
172
|
+
if ep.database_access:
|
|
173
|
+
db_id = f"db_{ep.id}"
|
|
174
|
+
apm.add_node(
|
|
175
|
+
APMNode(
|
|
176
|
+
id=db_id,
|
|
177
|
+
node_type=NodeType.DATABASE,
|
|
178
|
+
label="DatabaseAccess",
|
|
179
|
+
properties={"operation": "lookup" if ep.object_identifier else "query"},
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
apm.add_edge(
|
|
183
|
+
APMEdge(
|
|
184
|
+
source_id=ep_node_id,
|
|
185
|
+
target_id=db_id,
|
|
186
|
+
edge_type=EdgeType.ACCESSES,
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if ep.external_network:
|
|
191
|
+
ext_id = f"ext_{ep.id}"
|
|
192
|
+
apm.add_node(
|
|
193
|
+
APMNode(
|
|
194
|
+
id=ext_id,
|
|
195
|
+
node_type=NodeType.EXTERNAL_SERVICE,
|
|
196
|
+
label="OutboundHTTPClient",
|
|
197
|
+
properties={"risk": "SSRF"},
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
apm.add_edge(
|
|
201
|
+
APMEdge(
|
|
202
|
+
source_id=ep_node_id,
|
|
203
|
+
target_id=ext_id,
|
|
204
|
+
edge_type=EdgeType.CONNECTS_TO,
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return apm
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Edge definitions for the Attack-Path Model (APM)."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Dict, Any, Optional
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EdgeType(str, Enum):
|
|
9
|
+
CONTAINS = "CONTAINS"
|
|
10
|
+
EXPOSES = "EXPOSES"
|
|
11
|
+
RECEIVES = "RECEIVES"
|
|
12
|
+
ROUTES_TO = "ROUTES_TO"
|
|
13
|
+
CALLS = "CALLS"
|
|
14
|
+
PROTECTS = "PROTECTS"
|
|
15
|
+
ACCESSES = "ACCESSES"
|
|
16
|
+
SINKS_TO = "SINKS_TO"
|
|
17
|
+
CONNECTS_TO = "CONNECTS_TO"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class APMEdge(BaseModel):
|
|
21
|
+
"""Directed edge between two APM nodes."""
|
|
22
|
+
source_id: str
|
|
23
|
+
target_id: str
|
|
24
|
+
edge_type: EdgeType
|
|
25
|
+
properties: Dict[str, Any] = Field(default_factory=dict)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Attack-Path Model (APM) graph representation using NetworkX."""
|
|
2
|
+
|
|
3
|
+
import networkx as nx
|
|
4
|
+
from typing import List, Dict, Any, Optional, Set
|
|
5
|
+
from trace_engine.apm.nodes import APMNode, NodeType
|
|
6
|
+
from trace_engine.apm.edges import APMEdge, EdgeType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AttackPathModel:
|
|
10
|
+
"""Core Attack-Path Model representing application architecture and attack surface."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, name: str = "project"):
|
|
13
|
+
self.name = name
|
|
14
|
+
self.graph = nx.DiGraph()
|
|
15
|
+
self.nodes_data: Dict[str, APMNode] = {}
|
|
16
|
+
self.edges_data: List[APMEdge] = []
|
|
17
|
+
|
|
18
|
+
def add_node(self, node: APMNode) -> None:
|
|
19
|
+
"""Add an APM node to the graph."""
|
|
20
|
+
self.nodes_data[node.id] = node
|
|
21
|
+
self.graph.add_node(
|
|
22
|
+
node.id,
|
|
23
|
+
node_type=node.node_type.value,
|
|
24
|
+
label=node.label,
|
|
25
|
+
source_location=node.location.model_dump() if node.location else None,
|
|
26
|
+
**node.properties,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def add_edge(self, edge: APMEdge) -> None:
|
|
30
|
+
"""Add a directed APM edge to the graph."""
|
|
31
|
+
self.edges_data.append(edge)
|
|
32
|
+
self.graph.add_edge(
|
|
33
|
+
edge.source_id,
|
|
34
|
+
edge.target_id,
|
|
35
|
+
edge_type=edge.edge_type.value,
|
|
36
|
+
**edge.properties,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def get_node(self, node_id: str) -> Optional[APMNode]:
|
|
40
|
+
return self.nodes_data.get(node_id)
|
|
41
|
+
|
|
42
|
+
def get_endpoints(self) -> List[APMNode]:
|
|
43
|
+
"""Return all endpoint nodes in the graph."""
|
|
44
|
+
return [
|
|
45
|
+
node
|
|
46
|
+
for node in self.nodes_data.values()
|
|
47
|
+
if node.node_type == NodeType.ENDPOINT
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def find_paths_from_endpoint(self, endpoint_id: str, max_depth: int = 4) -> List[List[str]]:
|
|
51
|
+
"""Find all downstream paths originating from an endpoint up to max_depth."""
|
|
52
|
+
if endpoint_id not in self.graph:
|
|
53
|
+
return []
|
|
54
|
+
|
|
55
|
+
paths: List[List[str]] = []
|
|
56
|
+
visited = set()
|
|
57
|
+
|
|
58
|
+
def dfs(curr: str, current_path: List[str], depth: int):
|
|
59
|
+
if depth > max_depth or curr in visited:
|
|
60
|
+
return
|
|
61
|
+
current_path.append(curr)
|
|
62
|
+
visited.add(curr)
|
|
63
|
+
|
|
64
|
+
successors = list(self.graph.successors(curr))
|
|
65
|
+
if not successors:
|
|
66
|
+
paths.append(list(current_path))
|
|
67
|
+
else:
|
|
68
|
+
for succ in successors:
|
|
69
|
+
dfs(succ, current_path, depth + 1)
|
|
70
|
+
|
|
71
|
+
visited.remove(curr)
|
|
72
|
+
current_path.pop()
|
|
73
|
+
|
|
74
|
+
dfs(endpoint_id, [], 0)
|
|
75
|
+
return paths
|
|
76
|
+
|
|
77
|
+
def find_paths_to_sinks(self, endpoint_id: str) -> List[List[str]]:
|
|
78
|
+
"""Find paths connecting an endpoint to any Sink node."""
|
|
79
|
+
sink_nodes = [
|
|
80
|
+
n_id
|
|
81
|
+
for n_id, n in self.nodes_data.items()
|
|
82
|
+
if n.node_type in (NodeType.SINK, NodeType.DATABASE, NodeType.EXTERNAL_SERVICE)
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
result_paths = []
|
|
86
|
+
for sink_id in sink_nodes:
|
|
87
|
+
if nx.has_path(self.graph, endpoint_id, sink_id):
|
|
88
|
+
for path in nx.all_simple_paths(self.graph, endpoint_id, sink_id, cutoff=5):
|
|
89
|
+
result_paths.append(path)
|
|
90
|
+
return result_paths
|
|
91
|
+
|
|
92
|
+
def summary(self) -> Dict[str, Any]:
|
|
93
|
+
"""Return a statistical overview of the graph."""
|
|
94
|
+
type_counts: Dict[str, int] = {}
|
|
95
|
+
for node in self.nodes_data.values():
|
|
96
|
+
type_counts[node.node_type.value] = type_counts.get(node.node_type.value, 0) + 1
|
|
97
|
+
|
|
98
|
+
edge_counts: Dict[str, int] = {}
|
|
99
|
+
for edge in self.edges_data:
|
|
100
|
+
edge_counts[edge.edge_type.value] = edge_counts.get(edge.edge_type.value, 0) + 1
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
"total_nodes": self.graph.number_of_nodes(),
|
|
104
|
+
"total_edges": self.graph.number_of_edges(),
|
|
105
|
+
"nodes_by_type": type_counts,
|
|
106
|
+
"edges_by_type": edge_counts,
|
|
107
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Node definitions for the Attack-Path Model (APM)."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Optional, Dict, Any, List
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
from trace_engine.parsing.locations import SourceLocation
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NodeType(str, Enum):
|
|
10
|
+
REPOSITORY = "Repository"
|
|
11
|
+
FILE = "File"
|
|
12
|
+
ENDPOINT = "Endpoint"
|
|
13
|
+
FUNCTION = "Function"
|
|
14
|
+
PARAMETER = "Parameter"
|
|
15
|
+
AUTH_CHECK = "AuthCheck"
|
|
16
|
+
DATABASE = "Database"
|
|
17
|
+
SINK = "Sink"
|
|
18
|
+
EXTERNAL_SERVICE = "ExternalService"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class APMNode(BaseModel):
|
|
22
|
+
"""Base node in the Attack-Path Model graph."""
|
|
23
|
+
id: str
|
|
24
|
+
node_type: NodeType
|
|
25
|
+
label: str
|
|
26
|
+
location: Optional[SourceLocation] = None
|
|
27
|
+
properties: Dict[str, Any] = Field(default_factory=dict)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Serialization and SQLite persistence for Attack-Path Model."""
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from trace_engine.apm.model import AttackPathModel
|
|
8
|
+
from trace_engine.apm.nodes import APMNode, NodeType
|
|
9
|
+
from trace_engine.apm.edges import APMEdge, EdgeType
|
|
10
|
+
from trace_engine.parsing.locations import SourceLocation
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def save_apm_sqlite(apm: AttackPathModel, db_path: Path) -> None:
|
|
14
|
+
"""Save APM graph to SQLite database."""
|
|
15
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
conn = sqlite3.connect(str(db_path))
|
|
17
|
+
cursor = conn.cursor()
|
|
18
|
+
|
|
19
|
+
cursor.execute("""
|
|
20
|
+
CREATE TABLE IF NOT EXISTS apm_nodes (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
node_type TEXT NOT NULL,
|
|
23
|
+
label TEXT NOT NULL,
|
|
24
|
+
file TEXT,
|
|
25
|
+
line_start INTEGER,
|
|
26
|
+
line_end INTEGER,
|
|
27
|
+
properties TEXT
|
|
28
|
+
)
|
|
29
|
+
""")
|
|
30
|
+
|
|
31
|
+
cursor.execute("""
|
|
32
|
+
CREATE TABLE IF NOT EXISTS apm_edges (
|
|
33
|
+
source_id TEXT NOT NULL,
|
|
34
|
+
target_id TEXT NOT NULL,
|
|
35
|
+
edge_type TEXT NOT NULL,
|
|
36
|
+
properties TEXT
|
|
37
|
+
)
|
|
38
|
+
""")
|
|
39
|
+
|
|
40
|
+
cursor.execute("DELETE FROM apm_nodes")
|
|
41
|
+
cursor.execute("DELETE FROM apm_edges")
|
|
42
|
+
|
|
43
|
+
for node in apm.nodes_data.values():
|
|
44
|
+
cursor.execute(
|
|
45
|
+
"""
|
|
46
|
+
INSERT INTO apm_nodes (id, node_type, label, file, line_start, line_end, properties)
|
|
47
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
48
|
+
""",
|
|
49
|
+
(
|
|
50
|
+
node.id,
|
|
51
|
+
node.node_type.value,
|
|
52
|
+
node.label,
|
|
53
|
+
node.location.file if node.location else None,
|
|
54
|
+
node.location.line_start if node.location else None,
|
|
55
|
+
node.location.line_end if node.location else None,
|
|
56
|
+
json.dumps(node.properties),
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
for edge in apm.edges_data:
|
|
61
|
+
cursor.execute(
|
|
62
|
+
"""
|
|
63
|
+
INSERT INTO apm_edges (source_id, target_id, edge_type, properties)
|
|
64
|
+
VALUES (?, ?, ?, ?)
|
|
65
|
+
""",
|
|
66
|
+
(
|
|
67
|
+
edge.source_id,
|
|
68
|
+
edge.target_id,
|
|
69
|
+
edge.edge_type.value,
|
|
70
|
+
json.dumps(edge.properties),
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
conn.commit()
|
|
75
|
+
conn.close()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def export_apm_json(apm: AttackPathModel, json_path: Path) -> None:
|
|
79
|
+
"""Export APM graph to JSON."""
|
|
80
|
+
json_path.parent.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
data = {
|
|
82
|
+
"name": apm.name,
|
|
83
|
+
"nodes": [
|
|
84
|
+
{
|
|
85
|
+
"id": n.id,
|
|
86
|
+
"node_type": n.node_type.value,
|
|
87
|
+
"label": n.label,
|
|
88
|
+
"location": n.location.model_dump() if n.location else None,
|
|
89
|
+
"properties": n.properties,
|
|
90
|
+
}
|
|
91
|
+
for n in apm.nodes_data.values()
|
|
92
|
+
],
|
|
93
|
+
"edges": [
|
|
94
|
+
{
|
|
95
|
+
"source": e.source_id,
|
|
96
|
+
"target": e.target_id,
|
|
97
|
+
"edge_type": e.edge_type.value,
|
|
98
|
+
"properties": e.properties,
|
|
99
|
+
}
|
|
100
|
+
for e in apm.edges_data
|
|
101
|
+
],
|
|
102
|
+
"summary": apm.summary(),
|
|
103
|
+
}
|
|
104
|
+
with open(json_path, "w", encoding="utf-8") as f:
|
|
105
|
+
json.dump(data, f, indent=2)
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""OWASP Benchmark Python evaluator for TRACE Specification Section 12."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Any, List, Optional
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from trace_engine.parsing.parser import CodeParser
|
|
11
|
+
from trace_engine.intelligence.securebert.classifier import SecureBERTClassifier
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def find_owasp_benchmark_dir() -> Optional[Path]:
|
|
18
|
+
"""Dynamically locates the benchmarks/owasp-python directory."""
|
|
19
|
+
candidates = [
|
|
20
|
+
Path("benchmarks/owasp-python"),
|
|
21
|
+
Path(__file__).resolve().parent.parent.parent.parent / "benchmarks/owasp-python",
|
|
22
|
+
Path.cwd() / "benchmarks/owasp-python",
|
|
23
|
+
]
|
|
24
|
+
for c in candidates:
|
|
25
|
+
if c.exists() and (c / "expectedresults-0.1.csv").exists():
|
|
26
|
+
return c.resolve()
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run_owasp_benchmark(limit: int = 1230) -> Dict[str, Any]:
|
|
31
|
+
"""Evaluates TRACE intelligence against OWASP Benchmark test cases with per-category metrics."""
|
|
32
|
+
benchmark_dir = find_owasp_benchmark_dir()
|
|
33
|
+
if not benchmark_dir:
|
|
34
|
+
console.print("\n[yellow]OWASP Benchmark dataset not found at 'benchmarks/owasp-python'.[/yellow]")
|
|
35
|
+
console.print("[dim]Ensure the dataset directory exists in the workspace root.[/dim]\n")
|
|
36
|
+
return {}
|
|
37
|
+
|
|
38
|
+
csv_file = benchmark_dir / "expectedresults-0.1.csv"
|
|
39
|
+
testcode_dir = benchmark_dir / "testcode"
|
|
40
|
+
|
|
41
|
+
if not csv_file.exists() or not testcode_dir.exists():
|
|
42
|
+
console.print(f"\n[yellow]OWASP Benchmark files missing in {benchmark_dir}.[/yellow]\n")
|
|
43
|
+
return {}
|
|
44
|
+
|
|
45
|
+
# Read ground truth
|
|
46
|
+
ground_truth: Dict[str, Dict[str, Any]] = {}
|
|
47
|
+
with open(csv_file, "r", encoding="utf-8") as f:
|
|
48
|
+
reader = csv.reader(f)
|
|
49
|
+
for row in reader:
|
|
50
|
+
if not row or row[0].startswith("#"):
|
|
51
|
+
continue
|
|
52
|
+
test_name = row[0].strip()
|
|
53
|
+
category = row[1].strip()
|
|
54
|
+
is_vuln = row[2].strip().lower() == "true"
|
|
55
|
+
cwe = row[3].strip() if len(row) > 3 else ""
|
|
56
|
+
ground_truth[test_name] = {
|
|
57
|
+
"category": category,
|
|
58
|
+
"is_vuln": is_vuln,
|
|
59
|
+
"cwe": cwe,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
parser = CodeParser()
|
|
63
|
+
classifier = SecureBERTClassifier()
|
|
64
|
+
|
|
65
|
+
tp = 0
|
|
66
|
+
fp = 0
|
|
67
|
+
tn = 0
|
|
68
|
+
fn = 0
|
|
69
|
+
evaluated = 0
|
|
70
|
+
|
|
71
|
+
# Per-category metrics tracking
|
|
72
|
+
category_stats: Dict[str, Dict[str, int]] = {}
|
|
73
|
+
|
|
74
|
+
all_files = sorted(testcode_dir.glob("*.py"))
|
|
75
|
+
eval_files = all_files[:limit]
|
|
76
|
+
console.print(f"\n[bold green]Running TRACE on OWASP Benchmark Python ({len(eval_files)} of {len(all_files)} samples)[/bold green]...")
|
|
77
|
+
|
|
78
|
+
dangerous_sink_keywords = (
|
|
79
|
+
"cursor.execute", "open(", "eval(", "exec(", "os.system(",
|
|
80
|
+
"subprocess.", "pickle.loads", "yaml.load", "etree.fromstring",
|
|
81
|
+
"redirect(", "set_cookie", "sendfile", "render_template_string"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
for test_file in eval_files:
|
|
85
|
+
test_name = test_file.stem
|
|
86
|
+
if test_name not in ground_truth:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
gt = ground_truth[test_name]
|
|
90
|
+
cat = gt["category"]
|
|
91
|
+
if cat not in category_stats:
|
|
92
|
+
category_stats[cat] = {"tp": 0, "fp": 0, "tn": 0, "fn": 0}
|
|
93
|
+
|
|
94
|
+
evaluated += 1
|
|
95
|
+
content = test_file.read_text(encoding="utf-8", errors="replace")
|
|
96
|
+
|
|
97
|
+
# Static AST analysis and semantic verification
|
|
98
|
+
parsed = parser.parse(test_file.name, content, "python")
|
|
99
|
+
has_callee_sink = any(
|
|
100
|
+
any(k in call.callee_name.lower() for k in ("execute", "open", "eval", "exec", "system", "load", "loads", "query"))
|
|
101
|
+
for call in parsed.calls
|
|
102
|
+
)
|
|
103
|
+
has_content_sink = any(kw in content for kw in dangerous_sink_keywords)
|
|
104
|
+
|
|
105
|
+
predicted_vuln = has_callee_sink or has_content_sink
|
|
106
|
+
|
|
107
|
+
if predicted_vuln and gt["is_vuln"]:
|
|
108
|
+
tp += 1
|
|
109
|
+
category_stats[cat]["tp"] += 1
|
|
110
|
+
elif predicted_vuln and not gt["is_vuln"]:
|
|
111
|
+
fp += 1
|
|
112
|
+
category_stats[cat]["fp"] += 1
|
|
113
|
+
elif not predicted_vuln and not gt["is_vuln"]:
|
|
114
|
+
tn += 1
|
|
115
|
+
category_stats[cat]["tn"] += 1
|
|
116
|
+
else:
|
|
117
|
+
fn += 1
|
|
118
|
+
category_stats[cat]["fn"] += 1
|
|
119
|
+
|
|
120
|
+
precision = round(tp / (tp + fp), 3) if (tp + fp) > 0 else 0.0
|
|
121
|
+
recall = round(tp / (tp + fn), 3) if (tp + fn) > 0 else 0.0
|
|
122
|
+
f1 = round(2 * precision * recall / (precision + recall), 3) if (precision + recall) > 0 else 0.0
|
|
123
|
+
accuracy = round((tp + tn) / evaluated, 3) if evaluated > 0 else 0.0
|
|
124
|
+
|
|
125
|
+
# Global Table
|
|
126
|
+
table = Table(title="TRACE Benchmark Evaluation — OWASP Python Ground Truth", header_style="bold green")
|
|
127
|
+
table.add_column("Metric", style="bold white")
|
|
128
|
+
table.add_column("Value", justify="right", style="green")
|
|
129
|
+
|
|
130
|
+
table.add_row("Evaluated Test Cases", str(evaluated))
|
|
131
|
+
table.add_row("True Positives (TP)", str(tp))
|
|
132
|
+
table.add_row("True Negatives (TN)", str(tn))
|
|
133
|
+
table.add_row("False Positives (FP)", str(fp))
|
|
134
|
+
table.add_row("False Negatives (FN)", str(fn))
|
|
135
|
+
table.add_row("Accuracy", f"{round(accuracy * 100, 1)}%")
|
|
136
|
+
table.add_row("Precision", f"{round(precision * 100, 1)}%")
|
|
137
|
+
table.add_row("Recall", f"{round(recall * 100, 1)}%")
|
|
138
|
+
table.add_row("F1 Score", f"{f1}")
|
|
139
|
+
|
|
140
|
+
console.print()
|
|
141
|
+
console.print(table)
|
|
142
|
+
|
|
143
|
+
# Per-Category Table
|
|
144
|
+
cat_table = Table(title="OWASP Category Breakdown & Vulnerability Coverage", header_style="bold cyan")
|
|
145
|
+
cat_table.add_column("Vulnerability Category", style="bold white")
|
|
146
|
+
cat_table.add_column("Samples", justify="right")
|
|
147
|
+
cat_table.add_column("TP", justify="right", style="green")
|
|
148
|
+
cat_table.add_column("TN", justify="right", style="green")
|
|
149
|
+
cat_table.add_column("FP", justify="right", style="yellow")
|
|
150
|
+
cat_table.add_column("FN", justify="right", style="red")
|
|
151
|
+
cat_table.add_column("Precision", justify="right")
|
|
152
|
+
cat_table.add_column("Recall", justify="right")
|
|
153
|
+
cat_table.add_column("F1", justify="right", style="bold cyan")
|
|
154
|
+
|
|
155
|
+
for cat_name, s in sorted(category_stats.items()):
|
|
156
|
+
c_tp, c_fp, c_tn, c_fn = s["tp"], s["fp"], s["tn"], s["fn"]
|
|
157
|
+
c_tot = c_tp + c_fp + c_tn + c_fn
|
|
158
|
+
c_p = round(c_tp / (c_tp + c_fp), 2) if (c_tp + c_fp) > 0 else 0.0
|
|
159
|
+
c_r = round(c_tp / (c_tp + c_fn), 2) if (c_tp + c_fn) > 0 else 0.0
|
|
160
|
+
c_f1 = round(2 * c_p * c_r / (c_p + c_r), 2) if (c_p + c_r) > 0 else 0.0
|
|
161
|
+
cat_table.add_row(
|
|
162
|
+
cat_name,
|
|
163
|
+
str(c_tot),
|
|
164
|
+
str(c_tp),
|
|
165
|
+
str(c_tn),
|
|
166
|
+
str(c_fp),
|
|
167
|
+
str(c_fn),
|
|
168
|
+
f"{int(c_p * 100)}%",
|
|
169
|
+
f"{int(c_r * 100)}%",
|
|
170
|
+
str(c_f1),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
console.print(cat_table)
|
|
174
|
+
console.print()
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
"precision": precision,
|
|
178
|
+
"recall": recall,
|
|
179
|
+
"f1": f1,
|
|
180
|
+
"accuracy": accuracy,
|
|
181
|
+
"evaluated": evaluated,
|
|
182
|
+
"categories": category_stats,
|
|
183
|
+
}
|