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,212 @@
|
|
|
1
|
+
"""Spring Security auth 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 GraphNode, NodeKind, SourceLocation
|
|
10
|
+
|
|
11
|
+
# @Secured("ROLE_ADMIN") or @Secured({"ROLE_ADMIN", "ROLE_USER"})
|
|
12
|
+
_SECURED_RE = re.compile(
|
|
13
|
+
r'@Secured\(\s*(?:\{([^}]*)\}|"([^"]*)")\s*\)'
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# @PreAuthorize("hasRole('ADMIN')") and similar SpEL expressions
|
|
17
|
+
_PRE_AUTHORIZE_RE = re.compile(
|
|
18
|
+
r'@PreAuthorize\(\s*"([^"]*)"\s*\)'
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# @RolesAllowed({"ROLE_ADMIN", "ROLE_USER"}) or @RolesAllowed("ROLE_ADMIN")
|
|
22
|
+
_ROLES_ALLOWED_RE = re.compile(
|
|
23
|
+
r'@RolesAllowed\(\s*(?:\{([^}]*)\}|"([^"]*)")\s*\)'
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# @EnableWebSecurity
|
|
27
|
+
_ENABLE_WEB_SECURITY_RE = re.compile(r'@EnableWebSecurity\b')
|
|
28
|
+
|
|
29
|
+
# @EnableMethodSecurity
|
|
30
|
+
_ENABLE_METHOD_SECURITY_RE = re.compile(r'@EnableMethodSecurity\b')
|
|
31
|
+
|
|
32
|
+
# SecurityFilterChain method declaration
|
|
33
|
+
_SECURITY_FILTER_CHAIN_RE = re.compile(
|
|
34
|
+
r'(?:public\s+)?SecurityFilterChain\s+(\w+)\s*\('
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# .authorizeHttpRequests() fluent call
|
|
38
|
+
_AUTHORIZE_HTTP_REQUESTS_RE = re.compile(
|
|
39
|
+
r'\.authorizeHttpRequests\s*\('
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Helper to extract quoted role strings from annotation values
|
|
43
|
+
_ROLE_STR_RE = re.compile(r'"([^"]*)"')
|
|
44
|
+
|
|
45
|
+
# Extract roles from hasRole/hasAnyRole SpEL expressions
|
|
46
|
+
_HAS_ROLE_RE = re.compile(r"hasRole\(\s*'([^']*)'\s*\)")
|
|
47
|
+
_HAS_ANY_ROLE_RE = re.compile(r"hasAnyRole\(\s*([^)]+)\)")
|
|
48
|
+
_SINGLE_QUOTED_RE = re.compile(r"'([^']*)'")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _extract_roles_from_annotation(groups: tuple[str | None, str | None]) -> list[str]:
|
|
52
|
+
"""Extract role names from a @Secured or @RolesAllowed annotation match groups."""
|
|
53
|
+
multi, single = groups
|
|
54
|
+
if single is not None:
|
|
55
|
+
return [single]
|
|
56
|
+
if multi is not None:
|
|
57
|
+
return [m.group(1) for m in _ROLE_STR_RE.finditer(multi)]
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _extract_roles_from_spel(expr: str) -> list[str]:
|
|
62
|
+
"""Extract role names from a SpEL expression in @PreAuthorize."""
|
|
63
|
+
roles: list[str] = []
|
|
64
|
+
for m in _HAS_ROLE_RE.finditer(expr):
|
|
65
|
+
roles.append(m.group(1))
|
|
66
|
+
for m in _HAS_ANY_ROLE_RE.finditer(expr):
|
|
67
|
+
inner = m.group(1)
|
|
68
|
+
for q in _SINGLE_QUOTED_RE.finditer(inner):
|
|
69
|
+
roles.append(q.group(1))
|
|
70
|
+
return roles
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _line_number(text: str, pos: int) -> int:
|
|
74
|
+
"""Return 1-based line number for a character offset."""
|
|
75
|
+
return text[:pos].count("\n") + 1
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SpringSecurityDetector:
|
|
79
|
+
"""Detects Spring Security auth patterns in Java source files."""
|
|
80
|
+
|
|
81
|
+
name: str = "spring_security"
|
|
82
|
+
supported_languages: tuple[str, ...] = ("java",)
|
|
83
|
+
|
|
84
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
85
|
+
result = DetectorResult()
|
|
86
|
+
text = decode_text(ctx)
|
|
87
|
+
|
|
88
|
+
# @Secured annotations
|
|
89
|
+
for m in _SECURED_RE.finditer(text):
|
|
90
|
+
line = _line_number(text, m.start())
|
|
91
|
+
roles = _extract_roles_from_annotation((m.group(1), m.group(2)))
|
|
92
|
+
result.nodes.append(GraphNode(
|
|
93
|
+
id=f"auth:{ctx.file_path}:Secured:{line}",
|
|
94
|
+
kind=NodeKind.GUARD,
|
|
95
|
+
label="@Secured",
|
|
96
|
+
module=ctx.module_name,
|
|
97
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
98
|
+
annotations=["@Secured"],
|
|
99
|
+
properties={
|
|
100
|
+
"auth_type": "spring_security",
|
|
101
|
+
"roles": roles,
|
|
102
|
+
"auth_required": True,
|
|
103
|
+
},
|
|
104
|
+
))
|
|
105
|
+
|
|
106
|
+
# @PreAuthorize annotations
|
|
107
|
+
for m in _PRE_AUTHORIZE_RE.finditer(text):
|
|
108
|
+
line = _line_number(text, m.start())
|
|
109
|
+
expr = m.group(1)
|
|
110
|
+
roles = _extract_roles_from_spel(expr)
|
|
111
|
+
result.nodes.append(GraphNode(
|
|
112
|
+
id=f"auth:{ctx.file_path}:PreAuthorize:{line}",
|
|
113
|
+
kind=NodeKind.GUARD,
|
|
114
|
+
label="@PreAuthorize",
|
|
115
|
+
module=ctx.module_name,
|
|
116
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
117
|
+
annotations=["@PreAuthorize"],
|
|
118
|
+
properties={
|
|
119
|
+
"auth_type": "spring_security",
|
|
120
|
+
"roles": roles,
|
|
121
|
+
"expression": expr,
|
|
122
|
+
"auth_required": True,
|
|
123
|
+
},
|
|
124
|
+
))
|
|
125
|
+
|
|
126
|
+
# @RolesAllowed annotations
|
|
127
|
+
for m in _ROLES_ALLOWED_RE.finditer(text):
|
|
128
|
+
line = _line_number(text, m.start())
|
|
129
|
+
roles = _extract_roles_from_annotation((m.group(1), m.group(2)))
|
|
130
|
+
result.nodes.append(GraphNode(
|
|
131
|
+
id=f"auth:{ctx.file_path}:RolesAllowed:{line}",
|
|
132
|
+
kind=NodeKind.GUARD,
|
|
133
|
+
label="@RolesAllowed",
|
|
134
|
+
module=ctx.module_name,
|
|
135
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
136
|
+
annotations=["@RolesAllowed"],
|
|
137
|
+
properties={
|
|
138
|
+
"auth_type": "spring_security",
|
|
139
|
+
"roles": roles,
|
|
140
|
+
"auth_required": True,
|
|
141
|
+
},
|
|
142
|
+
))
|
|
143
|
+
|
|
144
|
+
# @EnableWebSecurity
|
|
145
|
+
for m in _ENABLE_WEB_SECURITY_RE.finditer(text):
|
|
146
|
+
line = _line_number(text, m.start())
|
|
147
|
+
result.nodes.append(GraphNode(
|
|
148
|
+
id=f"auth:{ctx.file_path}:EnableWebSecurity:{line}",
|
|
149
|
+
kind=NodeKind.GUARD,
|
|
150
|
+
label="@EnableWebSecurity",
|
|
151
|
+
module=ctx.module_name,
|
|
152
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
153
|
+
annotations=["@EnableWebSecurity"],
|
|
154
|
+
properties={
|
|
155
|
+
"auth_type": "spring_security",
|
|
156
|
+
"roles": [],
|
|
157
|
+
"auth_required": True,
|
|
158
|
+
},
|
|
159
|
+
))
|
|
160
|
+
|
|
161
|
+
# @EnableMethodSecurity
|
|
162
|
+
for m in _ENABLE_METHOD_SECURITY_RE.finditer(text):
|
|
163
|
+
line = _line_number(text, m.start())
|
|
164
|
+
result.nodes.append(GraphNode(
|
|
165
|
+
id=f"auth:{ctx.file_path}:EnableMethodSecurity:{line}",
|
|
166
|
+
kind=NodeKind.GUARD,
|
|
167
|
+
label="@EnableMethodSecurity",
|
|
168
|
+
module=ctx.module_name,
|
|
169
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
170
|
+
annotations=["@EnableMethodSecurity"],
|
|
171
|
+
properties={
|
|
172
|
+
"auth_type": "spring_security",
|
|
173
|
+
"roles": [],
|
|
174
|
+
"auth_required": True,
|
|
175
|
+
},
|
|
176
|
+
))
|
|
177
|
+
|
|
178
|
+
# SecurityFilterChain method declarations
|
|
179
|
+
for m in _SECURITY_FILTER_CHAIN_RE.finditer(text):
|
|
180
|
+
line = _line_number(text, m.start())
|
|
181
|
+
method_name = m.group(1)
|
|
182
|
+
result.nodes.append(GraphNode(
|
|
183
|
+
id=f"auth:{ctx.file_path}:SecurityFilterChain:{line}",
|
|
184
|
+
kind=NodeKind.GUARD,
|
|
185
|
+
label=f"SecurityFilterChain:{method_name}",
|
|
186
|
+
module=ctx.module_name,
|
|
187
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
188
|
+
properties={
|
|
189
|
+
"auth_type": "spring_security",
|
|
190
|
+
"roles": [],
|
|
191
|
+
"method_name": method_name,
|
|
192
|
+
"auth_required": True,
|
|
193
|
+
},
|
|
194
|
+
))
|
|
195
|
+
|
|
196
|
+
# .authorizeHttpRequests() calls
|
|
197
|
+
for m in _AUTHORIZE_HTTP_REQUESTS_RE.finditer(text):
|
|
198
|
+
line = _line_number(text, m.start())
|
|
199
|
+
result.nodes.append(GraphNode(
|
|
200
|
+
id=f"auth:{ctx.file_path}:authorizeHttpRequests:{line}",
|
|
201
|
+
kind=NodeKind.GUARD,
|
|
202
|
+
label=".authorizeHttpRequests()",
|
|
203
|
+
module=ctx.module_name,
|
|
204
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
205
|
+
properties={
|
|
206
|
+
"auth_type": "spring_security",
|
|
207
|
+
"roles": [],
|
|
208
|
+
"auth_required": True,
|
|
209
|
+
},
|
|
210
|
+
))
|
|
211
|
+
|
|
212
|
+
return result
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""TIBCO EMS 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
|
+
# TIBCO EMS connection factories
|
|
20
|
+
_TIBJMS_FACTORY_RE = re.compile(
|
|
21
|
+
r'\b(TibjmsConnectionFactory|TibjmsQueueConnectionFactory|TibjmsTopicConnectionFactory)\b'
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Server URL patterns (e.g. "tcp://ems-server:7222")
|
|
25
|
+
_SERVER_URL_RE = re.compile(r'"(tcp://[^"]+)"')
|
|
26
|
+
|
|
27
|
+
# createQueue / createTopic
|
|
28
|
+
_CREATE_QUEUE_RE = re.compile(r'createQueue\s*\(\s*"([^"]+)"')
|
|
29
|
+
_CREATE_TOPIC_RE = re.compile(r'createTopic\s*\(\s*"([^"]+)"')
|
|
30
|
+
|
|
31
|
+
# send / publish patterns
|
|
32
|
+
_SEND_RE = re.compile(r'\bsend\s*\(')
|
|
33
|
+
_PUBLISH_RE = re.compile(r'\bpublish\s*\(')
|
|
34
|
+
|
|
35
|
+
# receive / onMessage patterns
|
|
36
|
+
_RECEIVE_RE = re.compile(r'\breceive\s*\(')
|
|
37
|
+
_ON_MESSAGE_RE = re.compile(r'\bonMessage\s*\(')
|
|
38
|
+
|
|
39
|
+
# MessageProducer / MessageConsumer declarations
|
|
40
|
+
_PRODUCER_RE = re.compile(r'\bMessageProducer\b')
|
|
41
|
+
_CONSUMER_RE = re.compile(r'\bMessageConsumer\b')
|
|
42
|
+
|
|
43
|
+
# Tibjms-specific queue/topic classes
|
|
44
|
+
_TIBJMS_QUEUE_RE = re.compile(r'new\s+TibjmsQueue\s*\(\s*"([^"]+)"')
|
|
45
|
+
_TIBJMS_TOPIC_RE = re.compile(r'new\s+TibjmsTopic\s*\(\s*"([^"]+)"')
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TibcoEmsDetector:
|
|
49
|
+
"""Detects TIBCO EMS queue and topic usage in Java source files."""
|
|
50
|
+
|
|
51
|
+
name: str = "tibco_ems"
|
|
52
|
+
supported_languages: tuple[str, ...] = ("java",)
|
|
53
|
+
|
|
54
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
55
|
+
result = DetectorResult()
|
|
56
|
+
text = decode_text(ctx)
|
|
57
|
+
lines = text.split("\n")
|
|
58
|
+
|
|
59
|
+
if "tibjms" not in text and "TibjmsConnectionFactory" not in text and "com.tibco" not in text and "TIBJMS" not in text:
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
# Find class name
|
|
63
|
+
class_name: str | None = None
|
|
64
|
+
for line in lines:
|
|
65
|
+
cm = _CLASS_RE.search(line)
|
|
66
|
+
if cm:
|
|
67
|
+
class_name = cm.group(1)
|
|
68
|
+
break
|
|
69
|
+
|
|
70
|
+
if not class_name:
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
class_node_id = f"{ctx.file_path}:{class_name}"
|
|
74
|
+
seen_queues: set[str] = set()
|
|
75
|
+
seen_topics: set[str] = set()
|
|
76
|
+
|
|
77
|
+
def _ensure_queue_node(queue_name: str) -> str:
|
|
78
|
+
queue_id = f"ems:queue:{queue_name}"
|
|
79
|
+
if queue_name not in seen_queues:
|
|
80
|
+
seen_queues.add(queue_name)
|
|
81
|
+
result.nodes.append(GraphNode(
|
|
82
|
+
id=queue_id,
|
|
83
|
+
kind=NodeKind.QUEUE,
|
|
84
|
+
label=f"ems:queue:{queue_name}",
|
|
85
|
+
properties={"broker": "tibco_ems", "queue": queue_name},
|
|
86
|
+
))
|
|
87
|
+
return queue_id
|
|
88
|
+
|
|
89
|
+
def _ensure_topic_node(topic_name: str) -> str:
|
|
90
|
+
topic_id = f"ems:topic:{topic_name}"
|
|
91
|
+
if topic_name not in seen_topics:
|
|
92
|
+
seen_topics.add(topic_name)
|
|
93
|
+
result.nodes.append(GraphNode(
|
|
94
|
+
id=topic_id,
|
|
95
|
+
kind=NodeKind.TOPIC,
|
|
96
|
+
label=f"ems:topic:{topic_name}",
|
|
97
|
+
properties={"broker": "tibco_ems", "topic": topic_name},
|
|
98
|
+
))
|
|
99
|
+
return topic_id
|
|
100
|
+
|
|
101
|
+
# Detect whether this class is a producer or consumer
|
|
102
|
+
is_producer = bool(_SEND_RE.search(text) or _PUBLISH_RE.search(text) or _PRODUCER_RE.search(text))
|
|
103
|
+
is_consumer = bool(_RECEIVE_RE.search(text) or _ON_MESSAGE_RE.search(text) or _CONSUMER_RE.search(text))
|
|
104
|
+
|
|
105
|
+
# Detect connection factory — create a node for the EMS server
|
|
106
|
+
server_urls: list[str] = []
|
|
107
|
+
for i, line in enumerate(lines):
|
|
108
|
+
m = _TIBJMS_FACTORY_RE.search(line)
|
|
109
|
+
if m:
|
|
110
|
+
factory_type = m.group(1)
|
|
111
|
+
# Look for server URL on same line or next few lines
|
|
112
|
+
for j in range(max(0, i - 1), min(len(lines), i + 4)):
|
|
113
|
+
url_m = _SERVER_URL_RE.search(lines[j])
|
|
114
|
+
if url_m:
|
|
115
|
+
server_urls.append(url_m.group(1))
|
|
116
|
+
|
|
117
|
+
# Create an EMS connection node
|
|
118
|
+
node_id = f"ems:server:{factory_type}"
|
|
119
|
+
result.nodes.append(GraphNode(
|
|
120
|
+
id=node_id,
|
|
121
|
+
kind=NodeKind.MESSAGE_QUEUE,
|
|
122
|
+
label=f"ems:{factory_type}",
|
|
123
|
+
properties={
|
|
124
|
+
"broker": "tibco_ems",
|
|
125
|
+
"factory_type": factory_type,
|
|
126
|
+
**({"server_url": server_urls[0]} if server_urls else {}),
|
|
127
|
+
},
|
|
128
|
+
))
|
|
129
|
+
result.edges.append(GraphEdge(
|
|
130
|
+
source=class_node_id,
|
|
131
|
+
target=node_id,
|
|
132
|
+
kind=EdgeKind.CONNECTS_TO,
|
|
133
|
+
label=f"{class_name} connects to EMS via {factory_type}",
|
|
134
|
+
properties={"factory_type": factory_type},
|
|
135
|
+
))
|
|
136
|
+
|
|
137
|
+
# Detect createQueue / createTopic
|
|
138
|
+
for i, line in enumerate(lines):
|
|
139
|
+
m = _CREATE_QUEUE_RE.search(line)
|
|
140
|
+
if m:
|
|
141
|
+
queue_name = m.group(1)
|
|
142
|
+
queue_id = _ensure_queue_node(queue_name)
|
|
143
|
+
if is_producer:
|
|
144
|
+
result.edges.append(GraphEdge(
|
|
145
|
+
source=class_node_id,
|
|
146
|
+
target=queue_id,
|
|
147
|
+
kind=EdgeKind.SENDS_TO,
|
|
148
|
+
label=f"{class_name} sends to {queue_name}",
|
|
149
|
+
properties={"queue": queue_name},
|
|
150
|
+
))
|
|
151
|
+
if is_consumer:
|
|
152
|
+
result.edges.append(GraphEdge(
|
|
153
|
+
source=class_node_id,
|
|
154
|
+
target=queue_id,
|
|
155
|
+
kind=EdgeKind.RECEIVES_FROM,
|
|
156
|
+
label=f"{class_name} receives from {queue_name}",
|
|
157
|
+
properties={"queue": queue_name},
|
|
158
|
+
))
|
|
159
|
+
|
|
160
|
+
m = _CREATE_TOPIC_RE.search(line)
|
|
161
|
+
if m:
|
|
162
|
+
topic_name = m.group(1)
|
|
163
|
+
topic_id = _ensure_topic_node(topic_name)
|
|
164
|
+
if is_producer:
|
|
165
|
+
result.edges.append(GraphEdge(
|
|
166
|
+
source=class_node_id,
|
|
167
|
+
target=topic_id,
|
|
168
|
+
kind=EdgeKind.SENDS_TO,
|
|
169
|
+
label=f"{class_name} sends to {topic_name}",
|
|
170
|
+
properties={"topic": topic_name},
|
|
171
|
+
))
|
|
172
|
+
if is_consumer:
|
|
173
|
+
result.edges.append(GraphEdge(
|
|
174
|
+
source=class_node_id,
|
|
175
|
+
target=topic_id,
|
|
176
|
+
kind=EdgeKind.RECEIVES_FROM,
|
|
177
|
+
label=f"{class_name} receives from {topic_name}",
|
|
178
|
+
properties={"topic": topic_name},
|
|
179
|
+
))
|
|
180
|
+
|
|
181
|
+
# Detect TibjmsQueue / TibjmsTopic direct instantiation
|
|
182
|
+
for i, line in enumerate(lines):
|
|
183
|
+
m = _TIBJMS_QUEUE_RE.search(line)
|
|
184
|
+
if m:
|
|
185
|
+
queue_name = m.group(1)
|
|
186
|
+
_ensure_queue_node(queue_name)
|
|
187
|
+
|
|
188
|
+
m = _TIBJMS_TOPIC_RE.search(line)
|
|
189
|
+
if m:
|
|
190
|
+
topic_name = m.group(1)
|
|
191
|
+
_ensure_topic_node(topic_name)
|
|
192
|
+
|
|
193
|
+
return result
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""WebSocket 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
|
+
# JSR 356 @ServerEndpoint
|
|
20
|
+
_SERVER_ENDPOINT_RE = re.compile(r'@ServerEndpoint\s*\(\s*(?:value\s*=\s*)?"([^"]+)"')
|
|
21
|
+
|
|
22
|
+
# Spring WebSocket
|
|
23
|
+
_MESSAGE_MAPPING_RE = re.compile(r'@MessageMapping\s*\(\s*"([^"]+)"')
|
|
24
|
+
_SEND_TO_RE = re.compile(r'@SendTo\s*\(\s*"([^"]+)"')
|
|
25
|
+
_SEND_TO_USER_RE = re.compile(r'@SendToUser\s*\(\s*"([^"]+)"')
|
|
26
|
+
|
|
27
|
+
# WebSocket handler registration (in config classes)
|
|
28
|
+
_REGISTER_HANDLER_RE = re.compile(
|
|
29
|
+
r'\.addHandler\s*\(\s*\w+\s*,\s*"([^"]+)"'
|
|
30
|
+
)
|
|
31
|
+
_STOMP_ENDPOINT_RE = re.compile(
|
|
32
|
+
r'registerStompEndpoints.*?\.addEndpoint\s*\(\s*"([^"]+)"',
|
|
33
|
+
re.DOTALL,
|
|
34
|
+
)
|
|
35
|
+
_APP_DEST_PREFIX_RE = re.compile(
|
|
36
|
+
r'setApplicationDestinationPrefixes\s*\(\s*"([^"]+)"'
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# SimpMessagingTemplate for sending
|
|
40
|
+
_MESSAGING_TEMPLATE_RE = re.compile(
|
|
41
|
+
r'(?:simpMessagingTemplate|messagingTemplate)\s*\.(?:convertAndSend|convertAndSendToUser)\s*\(\s*"([^"]+)"'
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
_METHOD_RE = re.compile(r"(?:public|protected|private)?\s*(?:[\w<>\[\],?\s]+)\s+(\w+)\s*\(")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class WebSocketDetector:
|
|
48
|
+
"""Detects WebSocket endpoints and message handlers."""
|
|
49
|
+
|
|
50
|
+
name: str = "websocket"
|
|
51
|
+
supported_languages: tuple[str, ...] = ("java",)
|
|
52
|
+
|
|
53
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
54
|
+
result = DetectorResult()
|
|
55
|
+
text = decode_text(ctx)
|
|
56
|
+
lines = text.split("\n")
|
|
57
|
+
|
|
58
|
+
if not any(kw in text for kw in (
|
|
59
|
+
"@ServerEndpoint", "@MessageMapping", "WebSocketHandler",
|
|
60
|
+
"registerStompEndpoints", "SimpMessagingTemplate",
|
|
61
|
+
"simpMessagingTemplate", "messagingTemplate",
|
|
62
|
+
"@SendTo", "@SendToUser",
|
|
63
|
+
)):
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
# Find class name
|
|
67
|
+
class_name: str | None = None
|
|
68
|
+
class_line: int = 0
|
|
69
|
+
for i, line in enumerate(lines):
|
|
70
|
+
cm = _CLASS_RE.search(line)
|
|
71
|
+
if cm:
|
|
72
|
+
class_name = cm.group(1)
|
|
73
|
+
class_line = i + 1
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
if not class_name:
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
class_node_id = f"{ctx.file_path}:{class_name}"
|
|
80
|
+
|
|
81
|
+
# Detect @ServerEndpoint (JSR 356)
|
|
82
|
+
for m in _SERVER_ENDPOINT_RE.finditer(text):
|
|
83
|
+
path = m.group(1)
|
|
84
|
+
line_num = text[:m.start()].count("\n") + 1
|
|
85
|
+
ws_id = f"ws:endpoint:{path}"
|
|
86
|
+
|
|
87
|
+
result.nodes.append(GraphNode(
|
|
88
|
+
id=ws_id,
|
|
89
|
+
kind=NodeKind.WEBSOCKET_ENDPOINT,
|
|
90
|
+
label=f"WS {path}",
|
|
91
|
+
fqn=f"{class_name}:{path}",
|
|
92
|
+
module=ctx.module_name,
|
|
93
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line_num),
|
|
94
|
+
annotations=["@ServerEndpoint"],
|
|
95
|
+
properties={"path": path, "protocol": "websocket", "type": "jsr356"},
|
|
96
|
+
))
|
|
97
|
+
|
|
98
|
+
result.edges.append(GraphEdge(
|
|
99
|
+
source=class_node_id,
|
|
100
|
+
target=ws_id,
|
|
101
|
+
kind=EdgeKind.EXPOSES,
|
|
102
|
+
label=f"{class_name} exposes WS {path}",
|
|
103
|
+
))
|
|
104
|
+
|
|
105
|
+
# Detect @MessageMapping (Spring STOMP)
|
|
106
|
+
for i, line in enumerate(lines):
|
|
107
|
+
m = _MESSAGE_MAPPING_RE.search(line)
|
|
108
|
+
if not m:
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
destination = m.group(1)
|
|
112
|
+
method_name = None
|
|
113
|
+
for k in range(i + 1, min(i + 5, len(lines))):
|
|
114
|
+
mm = _METHOD_RE.search(lines[k])
|
|
115
|
+
if mm:
|
|
116
|
+
method_name = mm.group(1)
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
ws_id = f"ws:message:{destination}"
|
|
120
|
+
result.nodes.append(GraphNode(
|
|
121
|
+
id=ws_id,
|
|
122
|
+
kind=NodeKind.WEBSOCKET_ENDPOINT,
|
|
123
|
+
label=f"WS MSG {destination}",
|
|
124
|
+
fqn=f"{class_name}.{method_name or 'unknown'}",
|
|
125
|
+
module=ctx.module_name,
|
|
126
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=i + 1),
|
|
127
|
+
annotations=["@MessageMapping"],
|
|
128
|
+
properties={
|
|
129
|
+
"destination": destination,
|
|
130
|
+
"protocol": "websocket",
|
|
131
|
+
"type": "stomp",
|
|
132
|
+
},
|
|
133
|
+
))
|
|
134
|
+
|
|
135
|
+
result.edges.append(GraphEdge(
|
|
136
|
+
source=class_node_id,
|
|
137
|
+
target=ws_id,
|
|
138
|
+
kind=EdgeKind.EXPOSES,
|
|
139
|
+
label=f"{class_name} handles WS {destination}",
|
|
140
|
+
))
|
|
141
|
+
|
|
142
|
+
# Check for @SendTo on next lines
|
|
143
|
+
for k in range(i + 1, min(i + 5, len(lines))):
|
|
144
|
+
st = _SEND_TO_RE.search(lines[k]) or _SEND_TO_USER_RE.search(lines[k])
|
|
145
|
+
if st:
|
|
146
|
+
send_dest = st.group(1)
|
|
147
|
+
send_id = f"ws:topic:{send_dest}"
|
|
148
|
+
result.nodes.append(GraphNode(
|
|
149
|
+
id=send_id,
|
|
150
|
+
kind=NodeKind.WEBSOCKET_ENDPOINT,
|
|
151
|
+
label=f"WS TOPIC {send_dest}",
|
|
152
|
+
properties={"destination": send_dest, "protocol": "websocket"},
|
|
153
|
+
))
|
|
154
|
+
result.edges.append(GraphEdge(
|
|
155
|
+
source=ws_id,
|
|
156
|
+
target=send_id,
|
|
157
|
+
kind=EdgeKind.PRODUCES,
|
|
158
|
+
label=f"{destination} sends to {send_dest}",
|
|
159
|
+
))
|
|
160
|
+
|
|
161
|
+
# Detect STOMP endpoint registration
|
|
162
|
+
for m in _STOMP_ENDPOINT_RE.finditer(text):
|
|
163
|
+
path = m.group(1)
|
|
164
|
+
ws_id = f"ws:stomp:{path}"
|
|
165
|
+
line_num = text[:m.start()].count("\n") + 1
|
|
166
|
+
result.nodes.append(GraphNode(
|
|
167
|
+
id=ws_id,
|
|
168
|
+
kind=NodeKind.WEBSOCKET_ENDPOINT,
|
|
169
|
+
label=f"STOMP {path}",
|
|
170
|
+
module=ctx.module_name,
|
|
171
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line_num),
|
|
172
|
+
properties={"path": path, "protocol": "stomp", "type": "stomp_endpoint"},
|
|
173
|
+
))
|
|
174
|
+
|
|
175
|
+
# Detect SimpMessagingTemplate sends
|
|
176
|
+
for m in _MESSAGING_TEMPLATE_RE.finditer(text):
|
|
177
|
+
destination = m.group(1)
|
|
178
|
+
line_num = text[:m.start()].count("\n") + 1
|
|
179
|
+
|
|
180
|
+
result.edges.append(GraphEdge(
|
|
181
|
+
source=class_node_id,
|
|
182
|
+
target=f"ws:topic:{destination}",
|
|
183
|
+
kind=EdgeKind.PRODUCES,
|
|
184
|
+
label=f"{class_name} sends to {destination}",
|
|
185
|
+
properties={"destination": destination},
|
|
186
|
+
))
|
|
187
|
+
|
|
188
|
+
return result
|
|
File without changes
|