osscodeiq 0.0.0__py3-none-any.whl
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.
- osscodeiq/__init__.py +0 -0
- osscodeiq/analyzer.py +467 -0
- osscodeiq/cache/__init__.py +0 -0
- osscodeiq/cache/hasher.py +23 -0
- osscodeiq/cache/store.py +300 -0
- osscodeiq/classifiers/__init__.py +0 -0
- osscodeiq/classifiers/layer_classifier.py +69 -0
- osscodeiq/cli.py +721 -0
- osscodeiq/config.py +113 -0
- osscodeiq/detectors/__init__.py +0 -0
- osscodeiq/detectors/auth/__init__.py +0 -0
- osscodeiq/detectors/auth/certificate_auth.py +139 -0
- osscodeiq/detectors/auth/ldap_auth.py +89 -0
- osscodeiq/detectors/auth/session_header_auth.py +120 -0
- osscodeiq/detectors/base.py +41 -0
- osscodeiq/detectors/config/__init__.py +0 -0
- osscodeiq/detectors/config/batch_structure.py +128 -0
- osscodeiq/detectors/config/cloudformation.py +183 -0
- osscodeiq/detectors/config/docker_compose.py +179 -0
- osscodeiq/detectors/config/github_actions.py +150 -0
- osscodeiq/detectors/config/gitlab_ci.py +216 -0
- osscodeiq/detectors/config/helm_chart.py +187 -0
- osscodeiq/detectors/config/ini_structure.py +101 -0
- osscodeiq/detectors/config/json_structure.py +72 -0
- osscodeiq/detectors/config/kubernetes.py +305 -0
- osscodeiq/detectors/config/kubernetes_rbac.py +212 -0
- osscodeiq/detectors/config/openapi.py +194 -0
- osscodeiq/detectors/config/package_json.py +99 -0
- osscodeiq/detectors/config/properties_detector.py +108 -0
- osscodeiq/detectors/config/pyproject_toml.py +169 -0
- osscodeiq/detectors/config/sql_structure.py +155 -0
- osscodeiq/detectors/config/toml_structure.py +93 -0
- osscodeiq/detectors/config/tsconfig_json.py +105 -0
- osscodeiq/detectors/config/yaml_structure.py +82 -0
- osscodeiq/detectors/cpp/__init__.py +0 -0
- osscodeiq/detectors/cpp/cpp_structures.py +192 -0
- osscodeiq/detectors/csharp/__init__.py +0 -0
- osscodeiq/detectors/csharp/csharp_efcore.py +184 -0
- osscodeiq/detectors/csharp/csharp_minimal_apis.py +156 -0
- osscodeiq/detectors/csharp/csharp_structures.py +317 -0
- osscodeiq/detectors/docs/__init__.py +0 -0
- osscodeiq/detectors/docs/markdown_structure.py +117 -0
- osscodeiq/detectors/frontend/__init__.py +0 -0
- osscodeiq/detectors/frontend/angular_components.py +177 -0
- osscodeiq/detectors/frontend/frontend_routes.py +259 -0
- osscodeiq/detectors/frontend/react_components.py +148 -0
- osscodeiq/detectors/frontend/svelte_components.py +84 -0
- osscodeiq/detectors/frontend/vue_components.py +150 -0
- osscodeiq/detectors/generic/__init__.py +1 -0
- osscodeiq/detectors/generic/imports_detector.py +413 -0
- osscodeiq/detectors/go/__init__.py +0 -0
- osscodeiq/detectors/go/go_orm.py +202 -0
- osscodeiq/detectors/go/go_structures.py +162 -0
- osscodeiq/detectors/go/go_web.py +157 -0
- osscodeiq/detectors/iac/__init__.py +0 -0
- osscodeiq/detectors/iac/bicep.py +135 -0
- osscodeiq/detectors/iac/dockerfile.py +182 -0
- osscodeiq/detectors/iac/terraform.py +188 -0
- osscodeiq/detectors/java/__init__.py +0 -0
- osscodeiq/detectors/java/azure_functions.py +424 -0
- osscodeiq/detectors/java/azure_messaging.py +350 -0
- osscodeiq/detectors/java/class_hierarchy.py +349 -0
- osscodeiq/detectors/java/config_def.py +82 -0
- osscodeiq/detectors/java/cosmos_db.py +105 -0
- osscodeiq/detectors/java/graphql_resolver.py +188 -0
- osscodeiq/detectors/java/grpc_service.py +142 -0
- osscodeiq/detectors/java/ibm_mq.py +178 -0
- osscodeiq/detectors/java/jaxrs.py +160 -0
- osscodeiq/detectors/java/jdbc.py +196 -0
- osscodeiq/detectors/java/jms.py +116 -0
- osscodeiq/detectors/java/jpa_entity.py +143 -0
- osscodeiq/detectors/java/kafka.py +113 -0
- osscodeiq/detectors/java/kafka_protocol.py +70 -0
- osscodeiq/detectors/java/micronaut.py +248 -0
- osscodeiq/detectors/java/module_deps.py +191 -0
- osscodeiq/detectors/java/public_api.py +206 -0
- osscodeiq/detectors/java/quarkus.py +176 -0
- osscodeiq/detectors/java/rabbitmq.py +150 -0
- osscodeiq/detectors/java/raw_sql.py +136 -0
- osscodeiq/detectors/java/repository.py +131 -0
- osscodeiq/detectors/java/rmi.py +129 -0
- osscodeiq/detectors/java/spring_events.py +117 -0
- osscodeiq/detectors/java/spring_rest.py +168 -0
- osscodeiq/detectors/java/spring_security.py +212 -0
- osscodeiq/detectors/java/tibco_ems.py +193 -0
- osscodeiq/detectors/java/websocket.py +188 -0
- osscodeiq/detectors/kotlin/__init__.py +0 -0
- osscodeiq/detectors/kotlin/kotlin_structures.py +124 -0
- osscodeiq/detectors/kotlin/ktor_routes.py +163 -0
- osscodeiq/detectors/proto/__init__.py +0 -0
- osscodeiq/detectors/proto/proto_structure.py +153 -0
- osscodeiq/detectors/python/__init__.py +0 -0
- osscodeiq/detectors/python/celery_tasks.py +88 -0
- osscodeiq/detectors/python/django_auth.py +132 -0
- osscodeiq/detectors/python/django_models.py +157 -0
- osscodeiq/detectors/python/django_views.py +74 -0
- osscodeiq/detectors/python/fastapi_auth.py +143 -0
- osscodeiq/detectors/python/fastapi_routes.py +68 -0
- osscodeiq/detectors/python/flask_routes.py +67 -0
- osscodeiq/detectors/python/kafka_python.py +175 -0
- osscodeiq/detectors/python/pydantic_models.py +115 -0
- osscodeiq/detectors/python/python_structures.py +234 -0
- osscodeiq/detectors/python/sqlalchemy_models.py +82 -0
- osscodeiq/detectors/registry.py +100 -0
- osscodeiq/detectors/rust/__init__.py +0 -0
- osscodeiq/detectors/rust/actix_web.py +234 -0
- osscodeiq/detectors/rust/rust_structures.py +174 -0
- osscodeiq/detectors/scala/__init__.py +0 -0
- osscodeiq/detectors/scala/scala_structures.py +128 -0
- osscodeiq/detectors/shell/__init__.py +0 -0
- osscodeiq/detectors/shell/bash_detector.py +127 -0
- osscodeiq/detectors/shell/powershell_detector.py +118 -0
- osscodeiq/detectors/typescript/__init__.py +0 -0
- osscodeiq/detectors/typescript/express_routes.py +55 -0
- osscodeiq/detectors/typescript/fastify_routes.py +156 -0
- osscodeiq/detectors/typescript/graphql_resolvers.py +100 -0
- osscodeiq/detectors/typescript/kafka_js.py +164 -0
- osscodeiq/detectors/typescript/mongoose_orm.py +151 -0
- osscodeiq/detectors/typescript/nestjs_controllers.py +99 -0
- osscodeiq/detectors/typescript/nestjs_guards.py +138 -0
- osscodeiq/detectors/typescript/passport_jwt.py +133 -0
- osscodeiq/detectors/typescript/prisma_orm.py +96 -0
- osscodeiq/detectors/typescript/remix_routes.py +160 -0
- osscodeiq/detectors/typescript/sequelize_orm.py +136 -0
- osscodeiq/detectors/typescript/typeorm_entities.py +86 -0
- osscodeiq/detectors/typescript/typescript_structures.py +185 -0
- osscodeiq/detectors/utils.py +49 -0
- osscodeiq/discovery/__init__.py +11 -0
- osscodeiq/discovery/change_detector.py +97 -0
- osscodeiq/discovery/file_discovery.py +342 -0
- osscodeiq/flow/__init__.py +0 -0
- osscodeiq/flow/engine.py +78 -0
- osscodeiq/flow/models.py +72 -0
- osscodeiq/flow/renderer.py +127 -0
- osscodeiq/flow/templates/interactive.html +252 -0
- osscodeiq/flow/vendor/cytoscape-dagre.min.js +8 -0
- osscodeiq/flow/vendor/cytoscape.min.js +32 -0
- osscodeiq/flow/vendor/dagre.min.js +3809 -0
- osscodeiq/flow/views.py +357 -0
- osscodeiq/graph/__init__.py +0 -0
- osscodeiq/graph/backend.py +52 -0
- osscodeiq/graph/backends/__init__.py +23 -0
- osscodeiq/graph/backends/kuzu.py +576 -0
- osscodeiq/graph/backends/networkx.py +135 -0
- osscodeiq/graph/backends/sqlite_backend.py +406 -0
- osscodeiq/graph/builder.py +297 -0
- osscodeiq/graph/query.py +228 -0
- osscodeiq/graph/store.py +183 -0
- osscodeiq/graph/views.py +231 -0
- osscodeiq/models/__init__.py +17 -0
- osscodeiq/models/graph.py +116 -0
- osscodeiq/output/__init__.py +0 -0
- osscodeiq/output/dot.py +171 -0
- osscodeiq/output/mermaid.py +160 -0
- osscodeiq/output/safety.py +58 -0
- osscodeiq/output/serializers.py +42 -0
- osscodeiq/parsing/__init__.py +5 -0
- osscodeiq/parsing/languages/__init__.py +0 -0
- osscodeiq/parsing/languages/base.py +23 -0
- osscodeiq/parsing/languages/java.py +68 -0
- osscodeiq/parsing/languages/python.py +57 -0
- osscodeiq/parsing/languages/typescript.py +95 -0
- osscodeiq/parsing/parser_manager.py +125 -0
- osscodeiq/parsing/structured/__init__.py +0 -0
- osscodeiq/parsing/structured/gradle_parser.py +78 -0
- osscodeiq/parsing/structured/json_parser.py +24 -0
- osscodeiq/parsing/structured/properties_parser.py +56 -0
- osscodeiq/parsing/structured/sql_parser.py +54 -0
- osscodeiq/parsing/structured/xml_parser.py +148 -0
- osscodeiq/parsing/structured/yaml_parser.py +38 -0
- osscodeiq/server/__init__.py +7 -0
- osscodeiq/server/app.py +53 -0
- osscodeiq/server/mcp_server.py +174 -0
- osscodeiq/server/middleware.py +16 -0
- osscodeiq/server/routes.py +184 -0
- osscodeiq/server/service.py +445 -0
- osscodeiq/server/templates/welcome.html +56 -0
- osscodeiq-0.0.0.dist-info/METADATA +30 -0
- osscodeiq-0.0.0.dist-info/RECORD +183 -0
- osscodeiq-0.0.0.dist-info/WHEEL +5 -0
- osscodeiq-0.0.0.dist-info/entry_points.txt +2 -0
- osscodeiq-0.0.0.dist-info/licenses/LICENSE +21 -0
- osscodeiq-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""JAX-RS REST endpoint detector for Java source files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
|
|
9
|
+
from osscodeiq.detectors.utils import decode_text
|
|
10
|
+
from osscodeiq.models.graph import (
|
|
11
|
+
EdgeKind,
|
|
12
|
+
GraphEdge,
|
|
13
|
+
GraphNode,
|
|
14
|
+
NodeKind,
|
|
15
|
+
SourceLocation,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_PATH_RE = re.compile(r'@Path\s*\(\s*"([^"]*)"')
|
|
19
|
+
_HTTP_METHOD_RE = re.compile(r"@(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b")
|
|
20
|
+
_PRODUCES_RE = re.compile(r'@Produces\s*\(\s*\{?\s*(?:MediaType\.\w+|"([^"]*)")')
|
|
21
|
+
_CONSUMES_RE = re.compile(r'@Consumes\s*\(\s*\{?\s*(?:MediaType\.\w+|"([^"]*)")')
|
|
22
|
+
_CLASS_RE = re.compile(r"(?:public\s+)?class\s+(\w+)")
|
|
23
|
+
_JAVA_METHOD_RE = re.compile(
|
|
24
|
+
r'(?:public|protected|private)?\s*(?:static\s+)?(?:[\w<>\[\],\s]+)\s+(\w+)\s*\('
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class JaxrsDetector:
|
|
29
|
+
"""Detects JAX-RS REST endpoints from annotations."""
|
|
30
|
+
|
|
31
|
+
name: str = "jaxrs"
|
|
32
|
+
supported_languages: tuple[str, ...] = ("java",)
|
|
33
|
+
|
|
34
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
35
|
+
result = DetectorResult()
|
|
36
|
+
text = decode_text(ctx)
|
|
37
|
+
|
|
38
|
+
# Fast bail
|
|
39
|
+
if "@Path" not in text and "javax.ws.rs" not in text and "jakarta.ws.rs" not in text:
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
lines = text.split("\n")
|
|
43
|
+
|
|
44
|
+
# Find class name and class-level @Path
|
|
45
|
+
class_name: str | None = None
|
|
46
|
+
class_base_path = ""
|
|
47
|
+
for i, line in enumerate(lines):
|
|
48
|
+
cm = _CLASS_RE.search(line)
|
|
49
|
+
if cm:
|
|
50
|
+
class_name = cm.group(1)
|
|
51
|
+
# Look backwards for class-level @Path
|
|
52
|
+
for j in range(max(0, i - 5), i):
|
|
53
|
+
pm = _PATH_RE.search(lines[j])
|
|
54
|
+
if pm:
|
|
55
|
+
class_base_path = pm.group(1).rstrip("/")
|
|
56
|
+
break
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
if not class_name:
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
class_node_id = f"{ctx.file_path}:{class_name}"
|
|
63
|
+
|
|
64
|
+
# Scan for method-level HTTP annotations
|
|
65
|
+
for i, line in enumerate(lines):
|
|
66
|
+
m = _HTTP_METHOD_RE.search(line)
|
|
67
|
+
if not m:
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
http_method = m.group(1)
|
|
71
|
+
|
|
72
|
+
# Check whether this annotation is at the class level by
|
|
73
|
+
# inspecting the subsequent non-empty, non-annotation lines.
|
|
74
|
+
is_class_level = False
|
|
75
|
+
for k in range(i + 1, min(i + 5, len(lines))):
|
|
76
|
+
stripped = lines[k].strip()
|
|
77
|
+
if stripped.startswith("@") or not stripped:
|
|
78
|
+
continue
|
|
79
|
+
if "class " in stripped or "interface " in stripped:
|
|
80
|
+
is_class_level = True
|
|
81
|
+
break
|
|
82
|
+
if is_class_level:
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
# Look for a nearby method-level @Path (within a few lines before
|
|
86
|
+
# and after the HTTP annotation)
|
|
87
|
+
method_path: str | None = None
|
|
88
|
+
for k in range(max(0, i - 3), min(i + 4, len(lines))):
|
|
89
|
+
if k == i:
|
|
90
|
+
continue
|
|
91
|
+
pm = _PATH_RE.search(lines[k])
|
|
92
|
+
if pm:
|
|
93
|
+
method_path = pm.group(1)
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
# Combine class base path with method path
|
|
97
|
+
if method_path is not None:
|
|
98
|
+
full_path = f"{class_base_path}/{method_path.lstrip('/')}"
|
|
99
|
+
else:
|
|
100
|
+
full_path = class_base_path or "/"
|
|
101
|
+
if not full_path.startswith("/"):
|
|
102
|
+
full_path = "/" + full_path
|
|
103
|
+
|
|
104
|
+
# Extract @Produces and @Consumes near the annotation
|
|
105
|
+
produces: str | None = None
|
|
106
|
+
consumes: str | None = None
|
|
107
|
+
for k in range(max(0, i - 5), min(i + 5, len(lines))):
|
|
108
|
+
if produces is None:
|
|
109
|
+
pm = _PRODUCES_RE.search(lines[k])
|
|
110
|
+
if pm:
|
|
111
|
+
produces = pm.group(1) # may be None for MediaType constant
|
|
112
|
+
if consumes is None:
|
|
113
|
+
cm = _CONSUMES_RE.search(lines[k])
|
|
114
|
+
if cm:
|
|
115
|
+
consumes = cm.group(1) # may be None for MediaType constant
|
|
116
|
+
|
|
117
|
+
# Find the method name on subsequent lines
|
|
118
|
+
method_name: str | None = None
|
|
119
|
+
for k in range(i + 1, min(i + 5, len(lines))):
|
|
120
|
+
mm = _JAVA_METHOD_RE.search(lines[k])
|
|
121
|
+
if mm:
|
|
122
|
+
method_name = mm.group(1)
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
endpoint_label = f"{http_method} {full_path}"
|
|
126
|
+
endpoint_id = (
|
|
127
|
+
f"{ctx.file_path}:{class_name}:{method_name or 'unknown'}"
|
|
128
|
+
f":{http_method}:{full_path}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
properties: dict[str, Any] = {
|
|
132
|
+
"http_method": http_method,
|
|
133
|
+
"path": full_path,
|
|
134
|
+
}
|
|
135
|
+
if produces:
|
|
136
|
+
properties["produces"] = produces
|
|
137
|
+
if consumes:
|
|
138
|
+
properties["consumes"] = consumes
|
|
139
|
+
|
|
140
|
+
node = GraphNode(
|
|
141
|
+
id=endpoint_id,
|
|
142
|
+
kind=NodeKind.ENDPOINT,
|
|
143
|
+
label=endpoint_label,
|
|
144
|
+
fqn=f"{class_name}.{method_name}" if method_name else class_name,
|
|
145
|
+
module=ctx.module_name,
|
|
146
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=i + 1),
|
|
147
|
+
annotations=[f"@{http_method}"],
|
|
148
|
+
properties=properties,
|
|
149
|
+
)
|
|
150
|
+
result.nodes.append(node)
|
|
151
|
+
|
|
152
|
+
edge = GraphEdge(
|
|
153
|
+
source=class_node_id,
|
|
154
|
+
target=endpoint_id,
|
|
155
|
+
kind=EdgeKind.EXPOSES,
|
|
156
|
+
label=f"{class_name} exposes {endpoint_label}",
|
|
157
|
+
)
|
|
158
|
+
result.edges.append(edge)
|
|
159
|
+
|
|
160
|
+
return result
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""JDBC/ODBC database connectivity detector for Java source files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
|
|
9
|
+
from osscodeiq.detectors.utils import decode_text
|
|
10
|
+
from osscodeiq.models.graph import (
|
|
11
|
+
EdgeKind,
|
|
12
|
+
GraphEdge,
|
|
13
|
+
GraphNode,
|
|
14
|
+
NodeKind,
|
|
15
|
+
SourceLocation,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_CLASS_RE = re.compile(r"(?:public\s+)?class\s+(\w+)")
|
|
19
|
+
|
|
20
|
+
# DriverManager.getConnection("jdbc:...")
|
|
21
|
+
_DRIVER_MANAGER_RE = re.compile(
|
|
22
|
+
r'DriverManager\s*\.\s*getConnection\s*\(\s*"(jdbc:[^"]+)"'
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# JdbcTemplate / NamedParameterJdbcTemplate / JdbcClient field or constructor usage
|
|
26
|
+
_JDBC_TEMPLATE_RE = re.compile(
|
|
27
|
+
r"(?:private|protected|public|final|\s)+"
|
|
28
|
+
r"(?:final\s+)?"
|
|
29
|
+
r"(JdbcTemplate|NamedParameterJdbcTemplate|JdbcClient)"
|
|
30
|
+
r"\s+\w+"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# DataSource bean definitions or annotations
|
|
34
|
+
_DATASOURCE_BEAN_RE = re.compile(
|
|
35
|
+
r"(?:@Bean|DataSource)\s*(?:\(|\.)"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# spring.datasource.url property
|
|
39
|
+
_SPRING_DATASOURCE_RE = re.compile(
|
|
40
|
+
r"spring\.datasource\.url\s*=\s*(jdbc:[^\s]+)"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Generic JDBC URL pattern (captures db type and host info)
|
|
44
|
+
_JDBC_URL_RE = re.compile(
|
|
45
|
+
r"jdbc:(mysql|postgresql|sqlserver|oracle|db2|h2|sqlite|mariadb)"
|
|
46
|
+
r"(?::(?:thin:)?(?:@)?)?(?://([^/\"'\s;?]+))?"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _parse_jdbc_url(url: str) -> dict[str, str]:
|
|
51
|
+
"""Extract db_type and host from a JDBC URL."""
|
|
52
|
+
props: dict[str, str] = {"connection_url": url}
|
|
53
|
+
m = _JDBC_URL_RE.search(url)
|
|
54
|
+
if m:
|
|
55
|
+
props["db_type"] = m.group(1)
|
|
56
|
+
if m.group(2):
|
|
57
|
+
props["host"] = m.group(2)
|
|
58
|
+
return props
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class JdbcDetector:
|
|
62
|
+
"""Detects Java database connectivity patterns (JDBC, JdbcTemplate, DataSource)."""
|
|
63
|
+
|
|
64
|
+
name: str = "jdbc"
|
|
65
|
+
supported_languages: tuple[str, ...] = ("java",)
|
|
66
|
+
|
|
67
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
68
|
+
result = DetectorResult()
|
|
69
|
+
text = decode_text(ctx)
|
|
70
|
+
|
|
71
|
+
# Fast-bail
|
|
72
|
+
if (
|
|
73
|
+
"JdbcTemplate" not in text
|
|
74
|
+
and "DriverManager" not in text
|
|
75
|
+
and "DataSource" not in text
|
|
76
|
+
and "NamedParameterJdbcTemplate" not in text
|
|
77
|
+
and "JdbcClient" not in text
|
|
78
|
+
):
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
lines = text.split("\n")
|
|
82
|
+
|
|
83
|
+
# Find class name
|
|
84
|
+
class_name: str | None = None
|
|
85
|
+
for line in lines:
|
|
86
|
+
cm = _CLASS_RE.search(line)
|
|
87
|
+
if cm:
|
|
88
|
+
class_name = cm.group(1)
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
if not class_name:
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
class_node_id = f"{ctx.file_path}:{class_name}"
|
|
95
|
+
seen_dbs: set[str] = set()
|
|
96
|
+
|
|
97
|
+
def _ensure_db_node(
|
|
98
|
+
db_id: str,
|
|
99
|
+
label: str,
|
|
100
|
+
line_num: int | None,
|
|
101
|
+
properties: dict[str, Any],
|
|
102
|
+
) -> str:
|
|
103
|
+
if db_id not in seen_dbs:
|
|
104
|
+
seen_dbs.add(db_id)
|
|
105
|
+
result.nodes.append(GraphNode(
|
|
106
|
+
id=db_id,
|
|
107
|
+
kind=NodeKind.DATABASE_CONNECTION,
|
|
108
|
+
label=label,
|
|
109
|
+
module=ctx.module_name,
|
|
110
|
+
location=SourceLocation(
|
|
111
|
+
file_path=ctx.file_path,
|
|
112
|
+
line_start=line_num,
|
|
113
|
+
) if line_num else None,
|
|
114
|
+
properties=properties,
|
|
115
|
+
))
|
|
116
|
+
return db_id
|
|
117
|
+
|
|
118
|
+
def _add_connect_edge(db_id: str, label: str) -> None:
|
|
119
|
+
result.edges.append(GraphEdge(
|
|
120
|
+
source=class_node_id,
|
|
121
|
+
target=db_id,
|
|
122
|
+
kind=EdgeKind.CONNECTS_TO,
|
|
123
|
+
label=label,
|
|
124
|
+
))
|
|
125
|
+
|
|
126
|
+
# Pattern 1: DriverManager.getConnection("jdbc:...")
|
|
127
|
+
for i, line in enumerate(lines):
|
|
128
|
+
m = _DRIVER_MANAGER_RE.search(line)
|
|
129
|
+
if not m:
|
|
130
|
+
continue
|
|
131
|
+
url = m.group(1)
|
|
132
|
+
props = _parse_jdbc_url(url)
|
|
133
|
+
db_type = props.get("db_type", "unknown")
|
|
134
|
+
host = props.get("host", "unknown")
|
|
135
|
+
db_id = f"db:{db_type}:{host}"
|
|
136
|
+
_ensure_db_node(db_id, f"{db_type}@{host}", i + 1, props)
|
|
137
|
+
_add_connect_edge(db_id, f"{class_name} connects to {db_type}@{host}")
|
|
138
|
+
|
|
139
|
+
# Pattern 2: JdbcTemplate / NamedParameterJdbcTemplate / JdbcClient usage
|
|
140
|
+
for i, line in enumerate(lines):
|
|
141
|
+
m = _JDBC_TEMPLATE_RE.search(line)
|
|
142
|
+
if not m:
|
|
143
|
+
continue
|
|
144
|
+
template_type = m.group(1)
|
|
145
|
+
db_id = f"{ctx.file_path}:jdbc:{class_name}"
|
|
146
|
+
_ensure_db_node(
|
|
147
|
+
db_id,
|
|
148
|
+
f"{class_name} ({template_type})",
|
|
149
|
+
i + 1,
|
|
150
|
+
{"template_type": template_type},
|
|
151
|
+
)
|
|
152
|
+
_add_connect_edge(db_id, f"{class_name} uses {template_type}")
|
|
153
|
+
|
|
154
|
+
# Pattern 3: DataSource bean definitions
|
|
155
|
+
for i, line in enumerate(lines):
|
|
156
|
+
m = _DATASOURCE_BEAN_RE.search(line)
|
|
157
|
+
if not m:
|
|
158
|
+
continue
|
|
159
|
+
db_id = f"{ctx.file_path}:jdbc:{class_name}"
|
|
160
|
+
_ensure_db_node(
|
|
161
|
+
db_id,
|
|
162
|
+
f"{class_name} (DataSource)",
|
|
163
|
+
i + 1,
|
|
164
|
+
{"datasource": True},
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Pattern 4: spring.datasource.url in properties content
|
|
168
|
+
if "spring.datasource" in text:
|
|
169
|
+
for i, line in enumerate(lines):
|
|
170
|
+
m = _SPRING_DATASOURCE_RE.search(line)
|
|
171
|
+
if not m:
|
|
172
|
+
continue
|
|
173
|
+
url = m.group(1)
|
|
174
|
+
props = _parse_jdbc_url(url)
|
|
175
|
+
db_type = props.get("db_type", "unknown")
|
|
176
|
+
host = props.get("host", "unknown")
|
|
177
|
+
db_id = f"db:{db_type}:{host}"
|
|
178
|
+
_ensure_db_node(db_id, f"{db_type}@{host}", i + 1, props)
|
|
179
|
+
|
|
180
|
+
# Pattern 5: Standalone JDBC URL strings (not already caught above)
|
|
181
|
+
for i, line in enumerate(lines):
|
|
182
|
+
# Skip lines already matched by DriverManager or spring.datasource
|
|
183
|
+
if "DriverManager" in line or "spring.datasource" in line:
|
|
184
|
+
continue
|
|
185
|
+
urls = re.findall(r'"(jdbc:[^"]+)"', line)
|
|
186
|
+
for url in urls:
|
|
187
|
+
props = _parse_jdbc_url(url)
|
|
188
|
+
db_type = props.get("db_type", "unknown")
|
|
189
|
+
host = props.get("host", "unknown")
|
|
190
|
+
db_id = f"db:{db_type}:{host}"
|
|
191
|
+
_ensure_db_node(db_id, f"{db_type}@{host}", i + 1, props)
|
|
192
|
+
_add_connect_edge(
|
|
193
|
+
db_id, f"{class_name} connects to {db_type}@{host}"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return result
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""JMS (Java Message Service) detector for Java source files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
|
|
8
|
+
from osscodeiq.detectors.utils import decode_text
|
|
9
|
+
from osscodeiq.models.graph import (
|
|
10
|
+
EdgeKind,
|
|
11
|
+
GraphEdge,
|
|
12
|
+
GraphNode,
|
|
13
|
+
NodeKind,
|
|
14
|
+
SourceLocation,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
_CLASS_RE = re.compile(r"(?:public\s+)?class\s+(\w+)")
|
|
18
|
+
|
|
19
|
+
# JMS listener annotation
|
|
20
|
+
_JMS_LISTENER_RE = re.compile(
|
|
21
|
+
r'@JmsListener\s*\(\s*(?:.*?destination\s*=\s*)?"([^"]+)"'
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# JmsTemplate.send / convertAndSend
|
|
25
|
+
_JMS_SEND_RE = re.compile(
|
|
26
|
+
r'(?:jmsTemplate|JmsTemplate)\s*\.(?:send|convertAndSend)\s*\(\s*"([^"]+)"'
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# JmsTemplate with destination bean
|
|
30
|
+
_JMS_DEST_RE = re.compile(
|
|
31
|
+
r'(?:ActiveMQQueue|ActiveMQTopic)\s*\(\s*"([^"]+)"'
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_CONTAINER_FACTORY_RE = re.compile(r'containerFactory\s*=\s*"([^"]+)"')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class JmsDetector:
|
|
38
|
+
"""Detects JMS consumers and producers."""
|
|
39
|
+
|
|
40
|
+
name: str = "jms"
|
|
41
|
+
supported_languages: tuple[str, ...] = ("java",)
|
|
42
|
+
|
|
43
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
44
|
+
result = DetectorResult()
|
|
45
|
+
text = decode_text(ctx)
|
|
46
|
+
lines = text.split("\n")
|
|
47
|
+
|
|
48
|
+
if "@JmsListener" not in text and "jmsTemplate" not in text and "JmsTemplate" not in text:
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
# Find class name
|
|
52
|
+
class_name: str | None = None
|
|
53
|
+
for line in lines:
|
|
54
|
+
cm = _CLASS_RE.search(line)
|
|
55
|
+
if cm:
|
|
56
|
+
class_name = cm.group(1)
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
if not class_name:
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
class_node_id = f"{ctx.file_path}:{class_name}"
|
|
63
|
+
seen_queues: set[str] = set()
|
|
64
|
+
|
|
65
|
+
def _ensure_queue_node(destination: str) -> str:
|
|
66
|
+
queue_id = f"jms:queue:{destination}"
|
|
67
|
+
if destination not in seen_queues:
|
|
68
|
+
seen_queues.add(destination)
|
|
69
|
+
result.nodes.append(GraphNode(
|
|
70
|
+
id=queue_id,
|
|
71
|
+
kind=NodeKind.QUEUE,
|
|
72
|
+
label=f"jms:{destination}",
|
|
73
|
+
properties={"broker": "jms", "destination": destination},
|
|
74
|
+
))
|
|
75
|
+
return queue_id
|
|
76
|
+
|
|
77
|
+
# Detect @JmsListener consumers
|
|
78
|
+
for i, line in enumerate(lines):
|
|
79
|
+
m = _JMS_LISTENER_RE.search(line)
|
|
80
|
+
if not m:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
destination = m.group(1)
|
|
84
|
+
queue_id = _ensure_queue_node(destination)
|
|
85
|
+
|
|
86
|
+
props: dict[str, str] = {"destination": destination}
|
|
87
|
+
cf = _CONTAINER_FACTORY_RE.search(line)
|
|
88
|
+
if cf:
|
|
89
|
+
props["container_factory"] = cf.group(1)
|
|
90
|
+
|
|
91
|
+
result.edges.append(GraphEdge(
|
|
92
|
+
source=class_node_id,
|
|
93
|
+
target=queue_id,
|
|
94
|
+
kind=EdgeKind.CONSUMES,
|
|
95
|
+
label=f"{class_name} consumes from {destination}",
|
|
96
|
+
properties=props,
|
|
97
|
+
))
|
|
98
|
+
|
|
99
|
+
# Detect JmsTemplate sends
|
|
100
|
+
for i, line in enumerate(lines):
|
|
101
|
+
m = _JMS_SEND_RE.search(line)
|
|
102
|
+
if not m:
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
destination = m.group(1)
|
|
106
|
+
queue_id = _ensure_queue_node(destination)
|
|
107
|
+
|
|
108
|
+
result.edges.append(GraphEdge(
|
|
109
|
+
source=class_node_id,
|
|
110
|
+
target=queue_id,
|
|
111
|
+
kind=EdgeKind.PRODUCES,
|
|
112
|
+
label=f"{class_name} produces to {destination}",
|
|
113
|
+
properties={"destination": destination},
|
|
114
|
+
))
|
|
115
|
+
|
|
116
|
+
return result
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""JPA entity detector for Java source files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
|
|
9
|
+
from osscodeiq.detectors.utils import decode_text
|
|
10
|
+
from osscodeiq.models.graph import (
|
|
11
|
+
EdgeKind,
|
|
12
|
+
GraphEdge,
|
|
13
|
+
GraphNode,
|
|
14
|
+
NodeKind,
|
|
15
|
+
SourceLocation,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_ENTITY_RE = re.compile(r"@Entity")
|
|
19
|
+
_TABLE_RE = re.compile(r'@Table\s*\(\s*(?:name\s*=\s*)?"(\w+)"')
|
|
20
|
+
_CLASS_RE = re.compile(r"(?:public\s+)?class\s+(\w+)")
|
|
21
|
+
_COLUMN_RE = re.compile(r'@Column\s*\(([^)]*)\)')
|
|
22
|
+
_COLUMN_NAME_RE = re.compile(r'name\s*=\s*"(\w+)"')
|
|
23
|
+
_FIELD_RE = re.compile(r"(?:private|protected|public)\s+([\w<>,\s]+)\s+(\w+)\s*[;=]")
|
|
24
|
+
|
|
25
|
+
_RELATIONSHIP_ANNOTATIONS = {
|
|
26
|
+
"OneToMany": "one_to_many",
|
|
27
|
+
"ManyToOne": "many_to_one",
|
|
28
|
+
"OneToOne": "one_to_one",
|
|
29
|
+
"ManyToMany": "many_to_many",
|
|
30
|
+
}
|
|
31
|
+
_RELATIONSHIP_RE = re.compile(r"@(OneToMany|ManyToOne|OneToOne|ManyToMany)")
|
|
32
|
+
_TARGET_ENTITY_RE = re.compile(r'targetEntity\s*=\s*(\w+)\.class')
|
|
33
|
+
_MAPPED_BY_RE = re.compile(r'mappedBy\s*=\s*"(\w+)"')
|
|
34
|
+
_GENERIC_TYPE_RE = re.compile(r'<(\w+)>')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class JpaEntityDetector:
|
|
38
|
+
"""Detects JPA entities and their relationships."""
|
|
39
|
+
|
|
40
|
+
name: str = "jpa_entity"
|
|
41
|
+
supported_languages: tuple[str, ...] = ("java",)
|
|
42
|
+
|
|
43
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
44
|
+
result = DetectorResult()
|
|
45
|
+
text = decode_text(ctx)
|
|
46
|
+
lines = text.split("\n")
|
|
47
|
+
|
|
48
|
+
# Check if file contains @Entity
|
|
49
|
+
if not _ENTITY_RE.search(text):
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
# Find class name
|
|
53
|
+
class_name: str | None = None
|
|
54
|
+
class_line: int = 0
|
|
55
|
+
for i, line in enumerate(lines):
|
|
56
|
+
cm = _CLASS_RE.search(line)
|
|
57
|
+
if cm:
|
|
58
|
+
class_name = cm.group(1)
|
|
59
|
+
class_line = i + 1
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
if not class_name:
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
# Extract table name
|
|
66
|
+
table_match = _TABLE_RE.search(text)
|
|
67
|
+
table_name = table_match.group(1) if table_match else class_name.lower()
|
|
68
|
+
|
|
69
|
+
# Extract columns
|
|
70
|
+
columns: list[dict[str, str]] = []
|
|
71
|
+
for i, line in enumerate(lines):
|
|
72
|
+
col_match = _COLUMN_RE.search(line)
|
|
73
|
+
if col_match:
|
|
74
|
+
col_name_match = _COLUMN_NAME_RE.search(col_match.group(1))
|
|
75
|
+
# Find the field on the next line(s)
|
|
76
|
+
for k in range(i + 1, min(i + 3, len(lines))):
|
|
77
|
+
fm = _FIELD_RE.search(lines[k])
|
|
78
|
+
if fm:
|
|
79
|
+
col_name = col_name_match.group(1) if col_name_match else fm.group(2)
|
|
80
|
+
columns.append({"name": col_name, "field": fm.group(2), "type": fm.group(1).strip()})
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
entity_id = f"{ctx.file_path}:{class_name}"
|
|
84
|
+
properties: dict[str, Any] = {"table_name": table_name}
|
|
85
|
+
if columns:
|
|
86
|
+
properties["columns"] = columns
|
|
87
|
+
|
|
88
|
+
node = GraphNode(
|
|
89
|
+
id=entity_id,
|
|
90
|
+
kind=NodeKind.ENTITY,
|
|
91
|
+
label=f"{class_name} ({table_name})",
|
|
92
|
+
fqn=class_name,
|
|
93
|
+
module=ctx.module_name,
|
|
94
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=class_line),
|
|
95
|
+
annotations=["@Entity"],
|
|
96
|
+
properties=properties,
|
|
97
|
+
)
|
|
98
|
+
result.nodes.append(node)
|
|
99
|
+
|
|
100
|
+
# Extract relationships
|
|
101
|
+
for i, line in enumerate(lines):
|
|
102
|
+
rel_match = _RELATIONSHIP_RE.search(line)
|
|
103
|
+
if not rel_match:
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
rel_type = _RELATIONSHIP_ANNOTATIONS[rel_match.group(1)]
|
|
107
|
+
|
|
108
|
+
# Determine target entity
|
|
109
|
+
target_entity: str | None = None
|
|
110
|
+
# Check for targetEntity attribute
|
|
111
|
+
target_match = _TARGET_ENTITY_RE.search(line)
|
|
112
|
+
if target_match:
|
|
113
|
+
target_entity = target_match.group(1)
|
|
114
|
+
else:
|
|
115
|
+
# Look at field type on subsequent lines
|
|
116
|
+
for k in range(i + 1, min(i + 4, len(lines))):
|
|
117
|
+
fm = _FIELD_RE.search(lines[k])
|
|
118
|
+
if fm:
|
|
119
|
+
field_type = fm.group(1).strip()
|
|
120
|
+
gm = _GENERIC_TYPE_RE.search(field_type)
|
|
121
|
+
if gm:
|
|
122
|
+
target_entity = gm.group(1)
|
|
123
|
+
else:
|
|
124
|
+
# Direct type reference (e.g., ManyToOne Address)
|
|
125
|
+
target_entity = field_type.split()[-1] if field_type else None
|
|
126
|
+
break
|
|
127
|
+
|
|
128
|
+
if target_entity:
|
|
129
|
+
mapped_by = _MAPPED_BY_RE.search(line)
|
|
130
|
+
edge_props: dict[str, Any] = {"relationship_type": rel_type}
|
|
131
|
+
if mapped_by:
|
|
132
|
+
edge_props["mapped_by"] = mapped_by.group(1)
|
|
133
|
+
|
|
134
|
+
edge = GraphEdge(
|
|
135
|
+
source=entity_id,
|
|
136
|
+
target=f"*:{target_entity}", # Wildcard target resolved later
|
|
137
|
+
kind=EdgeKind.MAPS_TO,
|
|
138
|
+
label=f"{class_name} {rel_type} {target_entity}",
|
|
139
|
+
properties=edge_props,
|
|
140
|
+
)
|
|
141
|
+
result.edges.append(edge)
|
|
142
|
+
|
|
143
|
+
return result
|