apisec-code-bolt 0.1.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.
Files changed (111) hide show
  1. apisec_code_bolt/__init__.py +42 -0
  2. apisec_code_bolt/__main__.py +11 -0
  3. apisec_code_bolt/analysis/__init__.py +96 -0
  4. apisec_code_bolt/analysis/analyzer.py +2309 -0
  5. apisec_code_bolt/analysis/binding_tracker.py +341 -0
  6. apisec_code_bolt/analysis/call_graph.py +1197 -0
  7. apisec_code_bolt/analysis/call_graph_types.py +332 -0
  8. apisec_code_bolt/analysis/call_resolver.py +988 -0
  9. apisec_code_bolt/analysis/capability_tagger.py +322 -0
  10. apisec_code_bolt/analysis/config_scanner.py +197 -0
  11. apisec_code_bolt/analysis/data_flow.py +1883 -0
  12. apisec_code_bolt/analysis/dependency_extractor.py +959 -0
  13. apisec_code_bolt/analysis/flow_analysis.py +1406 -0
  14. apisec_code_bolt/analysis/hof_catalog.py +61 -0
  15. apisec_code_bolt/analysis/integration_detector.py +1399 -0
  16. apisec_code_bolt/analysis/literal_scanner.py +300 -0
  17. apisec_code_bolt/analysis/path_normalizer.py +55 -0
  18. apisec_code_bolt/analysis/read_site_detector.py +310 -0
  19. apisec_code_bolt/analysis/request_patterns.py +162 -0
  20. apisec_code_bolt/analysis/sensitivity_classifier.py +224 -0
  21. apisec_code_bolt/analysis/sink_evidence.py +333 -0
  22. apisec_code_bolt/analysis/url_prefix_resolver.py +338 -0
  23. apisec_code_bolt/cli/__init__.py +5 -0
  24. apisec_code_bolt/cli/exit_codes.py +17 -0
  25. apisec_code_bolt/cli/main.py +1069 -0
  26. apisec_code_bolt/cloud/__init__.py +1 -0
  27. apisec_code_bolt/cloud/apisec_client.py +118 -0
  28. apisec_code_bolt/cloud/client.py +255 -0
  29. apisec_code_bolt/core/__init__.py +75 -0
  30. apisec_code_bolt/core/config.py +528 -0
  31. apisec_code_bolt/core/credentials.py +65 -0
  32. apisec_code_bolt/core/discovery.py +433 -0
  33. apisec_code_bolt/core/log_format.py +115 -0
  34. apisec_code_bolt/core/manifest.py +1009 -0
  35. apisec_code_bolt/core/repo.py +280 -0
  36. apisec_code_bolt/core/state.py +59 -0
  37. apisec_code_bolt/core/telemetry.py +451 -0
  38. apisec_code_bolt/core/types.py +587 -0
  39. apisec_code_bolt/fingerprinting/__init__.py +1 -0
  40. apisec_code_bolt/frameworks/__init__.py +29 -0
  41. apisec_code_bolt/frameworks/_jwt_common.py +50 -0
  42. apisec_code_bolt/frameworks/auth_helpers.py +437 -0
  43. apisec_code_bolt/frameworks/base.py +608 -0
  44. apisec_code_bolt/frameworks/dotnet/__init__.py +17 -0
  45. apisec_code_bolt/frameworks/dotnet/_path_helpers.py +43 -0
  46. apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +2546 -0
  47. apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +559 -0
  48. apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +545 -0
  49. apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +732 -0
  50. apisec_code_bolt/frameworks/dotnet/refit_plugin.py +374 -0
  51. apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +1239 -0
  52. apisec_code_bolt/frameworks/java/__init__.py +6 -0
  53. apisec_code_bolt/frameworks/java/_annotations.py +167 -0
  54. apisec_code_bolt/frameworks/java/_constraints.py +128 -0
  55. apisec_code_bolt/frameworks/java/graphql_plugin.py +287 -0
  56. apisec_code_bolt/frameworks/java/jaxrs_plugin.py +748 -0
  57. apisec_code_bolt/frameworks/java/jwt_config_extractor.py +361 -0
  58. apisec_code_bolt/frameworks/java/micronaut_plugin.py +1059 -0
  59. apisec_code_bolt/frameworks/java/spring_plugin.py +1293 -0
  60. apisec_code_bolt/frameworks/js/__init__.py +8 -0
  61. apisec_code_bolt/frameworks/js/express_plugin.py +391 -0
  62. apisec_code_bolt/frameworks/js/fastify_plugin.py +381 -0
  63. apisec_code_bolt/frameworks/js/graphql_plugin.py +198 -0
  64. apisec_code_bolt/frameworks/js/nestjs_plugin.py +423 -0
  65. apisec_code_bolt/frameworks/python/__init__.py +19 -0
  66. apisec_code_bolt/frameworks/python/celery_plugin.py +393 -0
  67. apisec_code_bolt/frameworks/python/click_plugin.py +427 -0
  68. apisec_code_bolt/frameworks/python/django_plugin.py +867 -0
  69. apisec_code_bolt/frameworks/python/fastapi/__init__.py +28 -0
  70. apisec_code_bolt/frameworks/python/fastapi/plugin.py +1390 -0
  71. apisec_code_bolt/frameworks/python/flask_plugin.py +205 -0
  72. apisec_code_bolt/frameworks/python/graphql_plugin.py +274 -0
  73. apisec_code_bolt/frameworks/python/prefect_plugin.py +251 -0
  74. apisec_code_bolt/frameworks/python/webhook_plugin.py +255 -0
  75. apisec_code_bolt/parsing/__init__.py +62 -0
  76. apisec_code_bolt/parsing/base.py +554 -0
  77. apisec_code_bolt/parsing/csharp/__init__.py +5 -0
  78. apisec_code_bolt/parsing/csharp/language_services.py +203 -0
  79. apisec_code_bolt/parsing/csharp/literals.py +72 -0
  80. apisec_code_bolt/parsing/csharp/parser.py +1158 -0
  81. apisec_code_bolt/parsing/csharp/type_resolver.py +568 -0
  82. apisec_code_bolt/parsing/js/__init__.py +5 -0
  83. apisec_code_bolt/parsing/js/language_services.py +118 -0
  84. apisec_code_bolt/parsing/js/parser.py +622 -0
  85. apisec_code_bolt/parsing/jvm/__init__.py +7 -0
  86. apisec_code_bolt/parsing/jvm/language_services.py +270 -0
  87. apisec_code_bolt/parsing/jvm/parser.py +774 -0
  88. apisec_code_bolt/parsing/jvm/type_resolver.py +422 -0
  89. apisec_code_bolt/parsing/python/__init__.py +150 -0
  90. apisec_code_bolt/parsing/python/cbv_extractor.py +606 -0
  91. apisec_code_bolt/parsing/python/constant_resolver.py +500 -0
  92. apisec_code_bolt/parsing/python/cross_file_resolver.py +1054 -0
  93. apisec_code_bolt/parsing/python/dynamic_route_detector.py +532 -0
  94. apisec_code_bolt/parsing/python/expression_utils.py +221 -0
  95. apisec_code_bolt/parsing/python/extraction_types.py +271 -0
  96. apisec_code_bolt/parsing/python/language_services.py +487 -0
  97. apisec_code_bolt/parsing/python/parameter_analyzer.py +789 -0
  98. apisec_code_bolt/parsing/python/parser.py +719 -0
  99. apisec_code_bolt/parsing/python/path_resolver.py +576 -0
  100. apisec_code_bolt/parsing/python/router_registry.py +806 -0
  101. apisec_code_bolt/parsing/python/type_resolver.py +730 -0
  102. apisec_code_bolt/parsing/python/visitors.py +1544 -0
  103. apisec_code_bolt/parsing/services.py +544 -0
  104. apisec_code_bolt/query/__init__.py +1 -0
  105. apisec_code_bolt/query/ast_cache.py +182 -0
  106. apisec_code_bolt/query/executor.py +283 -0
  107. apisec_code_bolt/query/handlers.py +832 -0
  108. apisec_code_bolt-0.1.0.dist-info/METADATA +230 -0
  109. apisec_code_bolt-0.1.0.dist-info/RECORD +111 -0
  110. apisec_code_bolt-0.1.0.dist-info/WHEEL +4 -0
  111. apisec_code_bolt-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,205 @@
1
+ """
2
+ Flask framework plugin for extracting routes and parameters.
3
+
4
+ Flask uses:
5
+ - @app.route(path) or @bp.route(path) with methods
6
+ - Path params: <int:id>, <uuid:pk> (converted to {id}, {pk})
7
+ - Parameters: from request.args, request.form, request.get_json() in handler body
8
+ (read-site detection handles these; we extract route structure here)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+
15
+ from ...core.types import (
16
+ Confidence,
17
+ Framework,
18
+ HttpMethod,
19
+ Language,
20
+ ParameterLocation,
21
+ )
22
+ from ...parsing.base import ParsedDecorator, ParsedFile, ParsedFunction
23
+ from ..base import (
24
+ BaseFrameworkPlugin,
25
+ ExtractedParameter,
26
+ ExtractedResponse,
27
+ ExtractedRoute,
28
+ FrameworkPluginRegistry,
29
+ )
30
+
31
+ # Flask route decorator and HTTP methods
32
+ FLASK_IMPORTS = frozenset(
33
+ {
34
+ "flask",
35
+ "flask.Flask",
36
+ "flask.Blueprint",
37
+ "flask.request",
38
+ }
39
+ )
40
+
41
+ # Default methods when not specified
42
+ ROUTE_DEFAULT_METHODS = [HttpMethod.GET]
43
+
44
+ # Path param conversion: <int:id> -> {id}, <uuid:pk> -> {pk}
45
+ FLASK_PATH_PARAM_RE = re.compile(r"<(?:(?:\w+):)?(\w+)>")
46
+
47
+ HTTP_METHOD_MAP = {
48
+ "get": HttpMethod.GET,
49
+ "post": HttpMethod.POST,
50
+ "put": HttpMethod.PUT,
51
+ "patch": HttpMethod.PATCH,
52
+ "delete": HttpMethod.DELETE,
53
+ "head": HttpMethod.HEAD,
54
+ "options": HttpMethod.OPTIONS,
55
+ }
56
+
57
+
58
+ def _flask_path_to_manifest(path: str) -> str:
59
+ """Convert Flask path <int:id> to manifest format {id}."""
60
+ return FLASK_PATH_PARAM_RE.sub(r"{\1}", path)
61
+
62
+
63
+ def _parse_flask_path_params(path: str) -> list[str]:
64
+ """Extract path parameter names from Flask route."""
65
+ return FLASK_PATH_PARAM_RE.findall(path)
66
+
67
+
68
+ class FlaskPlugin(BaseFrameworkPlugin):
69
+ """Plugin for Flask framework route extraction."""
70
+
71
+ FRAMEWORK = Framework.FLASK
72
+ LANGUAGE = Language.PYTHON
73
+ DETECTION_IMPORTS = FLASK_IMPORTS
74
+
75
+ def __init__(self):
76
+ self._blueprint_prefixes: dict[str, str] = {}
77
+
78
+ def detect(self, parsed_file: ParsedFile) -> bool:
79
+ """Detect Flask usage via imports."""
80
+ for imp in parsed_file.imports:
81
+ mod = (imp.module or "").split(".")[0]
82
+ if mod == "flask":
83
+ return True
84
+ for name in imp.names:
85
+ if name in ("Flask", "Blueprint", "request"):
86
+ return True
87
+ return False
88
+
89
+ def extract_routes(self, parsed_file: ParsedFile, context=None) -> list[ExtractedRoute]:
90
+ """Extract routes from @app.route and @bp.route decorators."""
91
+ routes: list[ExtractedRoute] = []
92
+
93
+ for func in parsed_file.functions:
94
+ routes.extend(self._extract_routes_from_function(func, parsed_file))
95
+
96
+ for cls in parsed_file.classes:
97
+ for method in cls.methods:
98
+ routes.extend(self._extract_routes_from_function(method, parsed_file))
99
+
100
+ return routes
101
+
102
+ def _extract_routes_from_function(
103
+ self,
104
+ func: ParsedFunction,
105
+ parsed_file: ParsedFile,
106
+ ) -> list[ExtractedRoute]:
107
+ """Extract route(s) from a function with @app.route or @bp.route."""
108
+ dec = self._find_route_decorator(func)
109
+ if not dec:
110
+ return []
111
+
112
+ path = self._extract_route_path(dec)
113
+ methods = self._extract_methods(dec)
114
+ manifest_path = _flask_path_to_manifest(path)
115
+ path_param_names = _parse_flask_path_params(path)
116
+
117
+ path_params = [
118
+ ExtractedParameter(name=name, location=ParameterLocation.PATH)
119
+ for name in path_param_names
120
+ ]
121
+
122
+ handler_qn = func.qualified_name
123
+ routes = []
124
+ for method in methods:
125
+ routes.append(
126
+ ExtractedRoute(
127
+ method=method,
128
+ path=manifest_path,
129
+ handler_function=handler_qn,
130
+ handler_location=func.location,
131
+ path_params=path_params,
132
+ query_params=[],
133
+ header_params=[],
134
+ cookie_params=[],
135
+ body=None,
136
+ response=ExtractedResponse(),
137
+ router_name=None,
138
+ confidence=Confidence.HIGH,
139
+ )
140
+ )
141
+ return routes
142
+
143
+ def _find_route_decorator(self, func: ParsedFunction) -> ParsedDecorator | None:
144
+ """Find @app.route or @bp.route decorator."""
145
+ for dec in func.decorators:
146
+ name = (dec.name or "").lower()
147
+ full = (
148
+ dec.qualified_name.full
149
+ if dec.qualified_name and hasattr(dec.qualified_name, "full")
150
+ else name
151
+ )
152
+ if "route" in name or "route" in full:
153
+ return dec
154
+ return None
155
+
156
+ def _extract_route_path(self, dec: ParsedDecorator) -> str:
157
+ """Extract path from @app.route(path) or @app.route(path, methods=...)."""
158
+ if dec.positional_args:
159
+ first = dec.positional_args[0]
160
+ if isinstance(first, str):
161
+ return first
162
+ if "rule" in dec.arguments:
163
+ return str(dec.arguments["rule"])
164
+ if dec.arguments:
165
+ first_val = next(iter(dec.arguments.values()), None)
166
+ if isinstance(first_val, str) and first_val.startswith("/"):
167
+ return first_val
168
+ return "/"
169
+
170
+ def _extract_methods(self, dec: ParsedDecorator) -> list[HttpMethod]:
171
+ """Extract HTTP methods from decorator."""
172
+ methods = dec.arguments.get("methods")
173
+ if isinstance(methods, list):
174
+ return [HTTP_METHOD_MAP.get(str(m).lower(), HttpMethod.GET) for m in methods]
175
+ if isinstance(methods, str):
176
+ # Handle tuple/list literals stored as strings by the parser, e.g. "('GET', 'POST')"
177
+ names = re.findall(r"['\"]([A-Za-z]+)['\"]", methods)
178
+ if names:
179
+ return [HTTP_METHOD_MAP.get(n.lower(), HttpMethod.GET) for n in names]
180
+ return [HttpMethod.GET]
181
+
182
+ def extract_dependencies(self, parsed_file: ParsedFile) -> list:
183
+ """Flask does not use Depends(); return empty."""
184
+ return []
185
+
186
+ def extract_auth_schemes(self, parsed_file: ParsedFile) -> list:
187
+ """Extract auth schemes if present."""
188
+ return []
189
+
190
+ def extract_auth_dependencies(
191
+ self,
192
+ parsed_file: ParsedFile,
193
+ known_scheme_names: set[str] | None = None,
194
+ **kwargs,
195
+ ) -> list:
196
+ """Extract auth dependencies."""
197
+ return []
198
+
199
+ def extract_middleware(self, parsed_file: ParsedFile) -> list:
200
+ """Extract Flask middleware (before_request, etc.)."""
201
+ return []
202
+
203
+
204
+ _flask_plugin = FlaskPlugin()
205
+ FrameworkPluginRegistry.register(_flask_plugin)
@@ -0,0 +1,274 @@
1
+ """
2
+ Python GraphQL framework plugin.
3
+
4
+ Supports:
5
+ - Strawberry: @strawberry.type classes (Query/Mutation/Subscription) with
6
+ @field / @mutation methods
7
+ - Graphene: ObjectType subclasses with resolve_<field> methods
8
+ - Ariadne: @query.field("name") / @mutation.field("name") decorators on functions
9
+
10
+ Path format: /graphql:Query.op_name
11
+ HTTP method: POST for Mutation/Subscription, GET otherwise
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ from typing import TYPE_CHECKING, Any
18
+
19
+ from ...core.types import (
20
+ Framework,
21
+ HttpMethod,
22
+ Language,
23
+ QualifiedName,
24
+ )
25
+ from ...parsing.base import ParsedFile
26
+ from ..base import (
27
+ BaseFrameworkPlugin,
28
+ ExtractedAuthDependency,
29
+ ExtractedAuthScheme,
30
+ ExtractedDependency,
31
+ ExtractedMiddleware,
32
+ ExtractedRoute,
33
+ FrameworkPluginRegistry,
34
+ )
35
+
36
+ if TYPE_CHECKING:
37
+ from ...parsing.services import AnalysisContext
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ _GRAPHQL_IMPORTS = frozenset(
42
+ {
43
+ "strawberry",
44
+ "graphene",
45
+ "ariadne",
46
+ "graphql",
47
+ "graphql_core",
48
+ }
49
+ )
50
+
51
+
52
+ def _graphql_http_method(operation_type: str) -> HttpMethod:
53
+ """Return HTTP method for GraphQL operation type."""
54
+ if operation_type.lower() in ("mutation", "subscription"):
55
+ return HttpMethod.POST
56
+ return HttpMethod.GET
57
+
58
+
59
+ def _graphql_path(operation_type: str, field_name: str) -> str:
60
+ """Return the internal path token for a GraphQL operation.
61
+
62
+ Convention: /graphql:<OperationType>.<fieldName>
63
+ This is NOT a real HTTP URL. Downstream consumers must translate it to a
64
+ GraphQL request (POST /graphql, body {"query": "{ fieldName ... }"}}).
65
+ Query → GET semantics; Mutation/Subscription → POST semantics.
66
+ """
67
+ return f"/graphql:{operation_type}.{field_name}"
68
+
69
+
70
+ class GraphQLPythonPlugin(BaseFrameworkPlugin):
71
+ """
72
+ Framework plugin for Python GraphQL frameworks.
73
+
74
+ Detects Strawberry, Graphene, and Ariadne usage and extracts
75
+ GraphQL operations as routes.
76
+ """
77
+
78
+ FRAMEWORK = Framework.GRAPHQL
79
+ LANGUAGE = Language.PYTHON
80
+ DETECTION_IMPORTS: frozenset[str] = _GRAPHQL_IMPORTS
81
+
82
+ def detect(self, parsed_file: ParsedFile) -> bool:
83
+ """Detect GraphQL usage via imports."""
84
+ for imp in parsed_file.imports:
85
+ if imp.module in _GRAPHQL_IMPORTS:
86
+ return True
87
+ if imp.module.startswith("strawberry") or imp.module.startswith("graphene"):
88
+ return True
89
+ if imp.module.startswith("ariadne"):
90
+ return True
91
+ return False
92
+
93
+ def extract_routes(
94
+ self,
95
+ parsed_file: ParsedFile,
96
+ context: AnalysisContext | None = None,
97
+ ) -> list[ExtractedRoute]:
98
+ """Extract GraphQL operations as routes."""
99
+ routes: list[ExtractedRoute] = []
100
+
101
+ # Determine primary library
102
+ has_strawberry = any(imp.module.startswith("strawberry") for imp in parsed_file.imports)
103
+ has_graphene = any(imp.module.startswith("graphene") for imp in parsed_file.imports)
104
+ has_ariadne = any(imp.module.startswith("ariadne") for imp in parsed_file.imports)
105
+
106
+ if has_strawberry:
107
+ routes.extend(self._extract_strawberry(parsed_file))
108
+ if has_graphene:
109
+ routes.extend(self._extract_graphene(parsed_file))
110
+ if has_ariadne:
111
+ routes.extend(self._extract_ariadne(parsed_file))
112
+
113
+ return routes
114
+
115
+ def _extract_strawberry(self, parsed_file: ParsedFile) -> list[ExtractedRoute]:
116
+ """Extract Strawberry GraphQL operations."""
117
+ routes: list[ExtractedRoute] = []
118
+
119
+ for cls in parsed_file.classes:
120
+ # Look for @strawberry.type classes
121
+ operation_type = None
122
+ for dec in cls.decorators:
123
+ if dec.name in ("strawberry.type", "type") and (
124
+ "strawberry" in dec.name
125
+ or any(imp.module.startswith("strawberry") for imp in parsed_file.imports)
126
+ ):
127
+ # Determine operation type from class name
128
+ if cls.name == "Query":
129
+ operation_type = "Query"
130
+ elif cls.name == "Mutation":
131
+ operation_type = "Mutation"
132
+ elif cls.name == "Subscription":
133
+ operation_type = "Subscription"
134
+ else:
135
+ operation_type = "Query"
136
+
137
+ if operation_type is None:
138
+ continue
139
+
140
+ for method in cls.methods:
141
+ # Look for @strawberry.field or @field or @mutation
142
+ is_field = any(
143
+ dec.name
144
+ in (
145
+ "field",
146
+ "strawberry.field",
147
+ "mutation",
148
+ "strawberry.mutation",
149
+ "subscription",
150
+ "strawberry.subscription",
151
+ )
152
+ for dec in method.decorators
153
+ )
154
+ if not is_field and method.name.startswith("_"):
155
+ continue
156
+ if not is_field:
157
+ continue
158
+
159
+ path = _graphql_path(operation_type, method.name)
160
+ routes.append(
161
+ ExtractedRoute(
162
+ method=_graphql_http_method(operation_type),
163
+ path=path,
164
+ handler_function=QualifiedName(
165
+ module=parsed_file.path.stem,
166
+ name=f"{cls.name}.{method.name}",
167
+ ),
168
+ handler_location=method.location,
169
+ kind="http",
170
+ )
171
+ )
172
+
173
+ return routes
174
+
175
+ def _extract_graphene(self, parsed_file: ParsedFile) -> list[ExtractedRoute]:
176
+ """Extract Graphene GraphQL operations."""
177
+ routes: list[ExtractedRoute] = []
178
+
179
+ for cls in parsed_file.classes:
180
+ # Graphene ObjectType subclasses named Query/Mutation
181
+ base_names = {b.lower() for b in cls.base_classes}
182
+ is_graphene = any("objecttype" in b or "mutation" in b for b in base_names)
183
+ if not is_graphene:
184
+ continue
185
+
186
+ operation_type = "Query"
187
+ if cls.name == "Mutation" or any("mutation" in b for b in base_names):
188
+ operation_type = "Mutation"
189
+
190
+ for method in cls.methods:
191
+ # resolve_<field_name> pattern
192
+ if not method.name.startswith("resolve_"):
193
+ continue
194
+ field_name = method.name[len("resolve_") :]
195
+ path = _graphql_path(operation_type, field_name)
196
+ routes.append(
197
+ ExtractedRoute(
198
+ method=_graphql_http_method(operation_type),
199
+ path=path,
200
+ handler_function=QualifiedName(
201
+ module=parsed_file.path.stem,
202
+ name=f"{cls.name}.{method.name}",
203
+ ),
204
+ handler_location=method.location,
205
+ kind="http",
206
+ )
207
+ )
208
+
209
+ return routes
210
+
211
+ def _extract_ariadne(self, parsed_file: ParsedFile) -> list[ExtractedRoute]:
212
+ """Extract Ariadne GraphQL operations."""
213
+ routes: list[ExtractedRoute] = []
214
+
215
+ for func in parsed_file.functions:
216
+ for dec in func.decorators:
217
+ # @query.field("name") or @mutation.field("name")
218
+ # Python parser may give dec.name = "field" with qualified_name = "query.field"
219
+ dec_full = str(dec.qualified_name) if dec.qualified_name else dec.name
220
+
221
+ if not (dec_full.endswith(".field") or dec.name.endswith(".field")):
222
+ continue
223
+
224
+ operation_prefix = dec_full.split(".")[0].lower()
225
+ if operation_prefix == "query":
226
+ operation_type = "Query"
227
+ elif operation_prefix == "mutation":
228
+ operation_type = "Mutation"
229
+ elif operation_prefix == "subscription":
230
+ operation_type = "Subscription"
231
+ else:
232
+ operation_type = "Query"
233
+
234
+ field_name = dec.positional_args[0] if dec.positional_args else func.name
235
+
236
+ path = _graphql_path(operation_type, str(field_name))
237
+ routes.append(
238
+ ExtractedRoute(
239
+ method=_graphql_http_method(operation_type),
240
+ path=path,
241
+ handler_function=QualifiedName(
242
+ module=parsed_file.path.stem,
243
+ name=func.name,
244
+ ),
245
+ handler_location=func.location,
246
+ kind="http",
247
+ )
248
+ )
249
+
250
+ return routes
251
+
252
+ def extract_dependencies(self, parsed_file: ParsedFile) -> list[ExtractedDependency]:
253
+ return []
254
+
255
+ def extract_auth_schemes(self, parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
256
+ from ..auth_helpers import extract_python_graphql_auth_schemes
257
+
258
+ return extract_python_graphql_auth_schemes(parsed_file)
259
+
260
+ def extract_auth_dependencies(
261
+ self,
262
+ parsed_file: ParsedFile,
263
+ known_scheme_names: set[str] | None = None,
264
+ **kwargs: Any,
265
+ ) -> list[ExtractedAuthDependency]:
266
+ from ..auth_helpers import extract_python_graphql_auth_dependencies
267
+
268
+ return extract_python_graphql_auth_dependencies(parsed_file)
269
+
270
+ def extract_middleware(self, parsed_file: ParsedFile) -> list[ExtractedMiddleware]:
271
+ return []
272
+
273
+
274
+ FrameworkPluginRegistry.register(GraphQLPythonPlugin())