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,374 @@
1
+ """
2
+ Refit framework plugin.
3
+
4
+ Refit is a declarative REST client library for .NET — interfaces annotated with
5
+ HTTP verb attributes are auto-implemented as typed HTTP clients at runtime.
6
+ These represent *outbound* call surface (what the app calls), analogous to
7
+ Spring's @FeignClient.
8
+
9
+ Supports:
10
+ - HTTP verbs: [Get], [Post], [Put], [Delete], [Patch], [Head], [Options]
11
+ - Path parameters: method parameters whose names match {template} segments
12
+ - Query parameters: [Query] / [AliasAs] or undecorated primitives outside path
13
+ - Request body: [Body] or undecorated complex type (Refit default inference)
14
+ - Header parameters: [Header("X-Custom")]
15
+ - [Multipart] methods: body treated as multipart/form-data
16
+ - Interface-level and method-level [Headers(...)] static header constants
17
+ - RestService.For<T>() / RefitClient.For<T>() registration detection
18
+
19
+ Routes are emitted with kind="refit_client" and tags=["refit"].
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import re
25
+ from typing import ClassVar
26
+
27
+ from ...core.types import (
28
+ Confidence,
29
+ Framework,
30
+ HttpMethod,
31
+ Language,
32
+ ParameterLocation,
33
+ )
34
+ from ...parsing.base import ParsedClass, ParsedFile, ParsedFunction
35
+ from ...parsing.services import AnalysisContext
36
+ from ..base import (
37
+ BaseFrameworkPlugin,
38
+ ExtractedAuthDependency,
39
+ ExtractedAuthScheme,
40
+ ExtractedBody,
41
+ ExtractedDependency,
42
+ ExtractedMiddleware,
43
+ ExtractedParameter,
44
+ ExtractedResponse,
45
+ ExtractedRoute,
46
+ FrameworkPluginRegistry,
47
+ )
48
+ from .aspnet_plugin import AspNetCorePlugin, _str_to_qname
49
+
50
+ # =============================================================================
51
+ # Constants
52
+ # =============================================================================
53
+
54
+ _REFIT_IMPORTS: frozenset[str] = frozenset(
55
+ {
56
+ "Refit",
57
+ "Refit.Attributes",
58
+ }
59
+ )
60
+
61
+ # Refit HTTP verb attribute name → HttpMethod
62
+ _REFIT_VERBS: dict[str, HttpMethod] = {
63
+ "Get": HttpMethod.GET,
64
+ "Post": HttpMethod.POST,
65
+ "Put": HttpMethod.PUT,
66
+ "Delete": HttpMethod.DELETE,
67
+ "Patch": HttpMethod.PATCH,
68
+ "Head": HttpMethod.HEAD,
69
+ "Options": HttpMethod.OPTIONS,
70
+ }
71
+
72
+ # Scalar C# type names — undecorated scalars outside the path template become
73
+ # query params (Refit's default). Complex types become the body.
74
+ _SCALAR_TYPES: frozenset[str] = frozenset(
75
+ {
76
+ "bool",
77
+ "Boolean",
78
+ "byte",
79
+ "Byte",
80
+ "sbyte",
81
+ "SByte",
82
+ "char",
83
+ "Char",
84
+ "short",
85
+ "Int16",
86
+ "ushort",
87
+ "UInt16",
88
+ "int",
89
+ "Int32",
90
+ "uint",
91
+ "UInt32",
92
+ "long",
93
+ "Int64",
94
+ "ulong",
95
+ "UInt64",
96
+ "float",
97
+ "Single",
98
+ "double",
99
+ "Double",
100
+ "decimal",
101
+ "Decimal",
102
+ "string",
103
+ "String",
104
+ "Guid",
105
+ "DateTime",
106
+ "DateTimeOffset",
107
+ "DateOnly",
108
+ "TimeOnly",
109
+ "TimeSpan",
110
+ "Uri",
111
+ "int?",
112
+ "long?",
113
+ "bool?",
114
+ "double?",
115
+ "decimal?",
116
+ "Guid?",
117
+ "DateTime?",
118
+ "DateTimeOffset?",
119
+ "CancellationToken", # always skipped
120
+ }
121
+ )
122
+
123
+
124
+ # =============================================================================
125
+ # RefitPlugin
126
+ # =============================================================================
127
+
128
+
129
+ class RefitPlugin(BaseFrameworkPlugin):
130
+ """
131
+ Framework plugin for Refit declarative HTTP client interfaces.
132
+
133
+ Each interface method carrying a Refit HTTP verb attribute is emitted as
134
+ an ExtractedRoute with kind="refit_client".
135
+ """
136
+
137
+ FRAMEWORK: ClassVar[Framework] = Framework.REFIT
138
+ LANGUAGE: ClassVar[Language] = Language.CSHARP
139
+ DETECTION_IMPORTS: ClassVar[frozenset[str]] = _REFIT_IMPORTS
140
+
141
+ # Reuse path helpers from AspNetCorePlugin without inheriting its
142
+ # controller/MVC logic.
143
+ _helper = AspNetCorePlugin()
144
+
145
+ # -------------------------------------------------------------------------
146
+ # Detection
147
+ # -------------------------------------------------------------------------
148
+
149
+ def detect(self, parsed_file: ParsedFile) -> bool:
150
+ for imp in parsed_file.imports:
151
+ if (imp.module or "").startswith("Refit"):
152
+ return True
153
+ # Attribute-only detection (no using statement but attributes present)
154
+ for cls in parsed_file.classes:
155
+ for method in cls.methods:
156
+ if any(dec.name in _REFIT_VERBS for dec in method.decorators):
157
+ return True
158
+ return False
159
+
160
+ # -------------------------------------------------------------------------
161
+ # Route extraction
162
+ # -------------------------------------------------------------------------
163
+
164
+ def extract_routes(
165
+ self,
166
+ parsed_file: ParsedFile,
167
+ context: AnalysisContext | None = None,
168
+ ) -> list[ExtractedRoute]:
169
+ routes: list[ExtractedRoute] = []
170
+
171
+ for cls in parsed_file.classes:
172
+ if not self._has_refit_methods(cls):
173
+ continue
174
+
175
+ for method in cls.methods:
176
+ route = self._extract_refit_route(method, cls)
177
+ if route:
178
+ routes.append(route)
179
+
180
+ return routes
181
+
182
+ # Refit interfaces define outbound HTTP clients only — no server-side auth,
183
+ # middleware, or DI definitions to extract.
184
+
185
+ def extract_auth_schemes(self, parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
186
+ return []
187
+
188
+ def extract_auth_dependencies(
189
+ self, parsed_file: ParsedFile, known_scheme_names: set[str] | None = None, **kwargs
190
+ ) -> list[ExtractedAuthDependency]:
191
+ return []
192
+
193
+ def extract_dependencies(self, parsed_file: ParsedFile) -> list[ExtractedDependency]:
194
+ return []
195
+
196
+ def extract_middleware(self, parsed_file: ParsedFile) -> list[ExtractedMiddleware]:
197
+ return []
198
+
199
+ # -------------------------------------------------------------------------
200
+ # Internal helpers
201
+ # -------------------------------------------------------------------------
202
+
203
+ def _has_refit_methods(self, cls: ParsedClass) -> bool:
204
+ return any(dec.name in _REFIT_VERBS for method in cls.methods for dec in method.decorators)
205
+
206
+ def _extract_refit_route(
207
+ self, method: ParsedFunction, cls: ParsedClass
208
+ ) -> ExtractedRoute | None:
209
+ http_method: HttpMethod | None = None
210
+ path_template: str = ""
211
+
212
+ for dec in method.decorators:
213
+ hm = _REFIT_VERBS.get(dec.name)
214
+ if hm is not None:
215
+ http_method = hm
216
+ raw = self._helper._dec_path(dec) or ""
217
+ path_template = raw
218
+ break
219
+
220
+ if http_method is None:
221
+ return None
222
+
223
+ # Normalise path
224
+ if path_template and not path_template.startswith("/"):
225
+ path_template = "/" + path_template
226
+ path_template = self._helper._normalize_path(path_template) if path_template else "/"
227
+
228
+ # Strip route constraints: {id:int} → {id}
229
+ path_template = re.sub(r"\{([^:}]+):[^}]+\}", r"{\1}", path_template)
230
+
231
+ path_tpl_names = {m.group(1) for m in re.finditer(r"\{([^}]+)\}", path_template)}
232
+
233
+ is_multipart = any(dec.name == "Multipart" for dec in method.decorators)
234
+
235
+ path_params: list[ExtractedParameter] = []
236
+ query_params: list[ExtractedParameter] = []
237
+ header_params: list[ExtractedParameter] = []
238
+ body: ExtractedBody | None = None
239
+
240
+ for param in method.parameters:
241
+ # CancellationToken is always a framework param — skip
242
+ base_type = (param.type_annotation or "").split("<")[0].strip().rstrip("?")
243
+ if base_type == "CancellationToken":
244
+ continue
245
+
246
+ # Parameter attributes are in param.metadata (keyed by attribute name).
247
+ # Values: positional string arg, or True for no-arg attributes.
248
+ meta = param.metadata or {}
249
+
250
+ # [AliasAs("name")] renames the parameter in the request
251
+ alias_val = meta.get("AliasAs")
252
+ alias = alias_val if isinstance(alias_val, str) else param.name
253
+
254
+ # [Header("X-Custom")] → explicit header parameter
255
+ if "Header" in meta:
256
+ header_name = meta["Header"] if isinstance(meta["Header"], str) else param.name
257
+ header_params.append(
258
+ ExtractedParameter(
259
+ name=header_name,
260
+ location=ParameterLocation.HEADER,
261
+ type_annotation=param.type_annotation,
262
+ required=True,
263
+ code_location=param.location,
264
+ )
265
+ )
266
+ continue
267
+
268
+ # [Body] → request body
269
+ if "Body" in meta:
270
+ if body is None:
271
+ content_type = "multipart/form-data" if is_multipart else "application/json"
272
+ body = ExtractedBody(
273
+ content_type=content_type,
274
+ model_name=param.type_annotation,
275
+ required=True,
276
+ )
277
+ continue
278
+
279
+ # Path parameter: param name (or alias) matches a {template} segment
280
+ if alias in path_tpl_names or param.name in path_tpl_names:
281
+ matched_name = param.name if param.name in path_tpl_names else alias
282
+ path_params.append(
283
+ ExtractedParameter(
284
+ name=matched_name,
285
+ location=ParameterLocation.PATH,
286
+ type_annotation=param.type_annotation,
287
+ required=True,
288
+ code_location=param.location,
289
+ )
290
+ )
291
+ continue
292
+
293
+ # [Query] or [AliasAs] → explicit query param
294
+ if "Query" in meta or "AliasAs" in meta:
295
+ query_params.append(
296
+ ExtractedParameter(
297
+ name=alias,
298
+ location=ParameterLocation.QUERY,
299
+ type_annotation=param.type_annotation,
300
+ required=param.default_value is None and param.default_value != "null",
301
+ default_value=param.default_value,
302
+ code_location=param.location,
303
+ )
304
+ )
305
+ continue
306
+
307
+ # Undecorated: scalar → query, complex → body
308
+ if self._is_scalar(param.type_annotation):
309
+ query_params.append(
310
+ ExtractedParameter(
311
+ name=param.name,
312
+ location=ParameterLocation.QUERY,
313
+ type_annotation=param.type_annotation,
314
+ required=param.default_value is None and param.default_value != "null",
315
+ default_value=param.default_value,
316
+ code_location=param.location,
317
+ )
318
+ )
319
+ else:
320
+ if body is None:
321
+ content_type = "multipart/form-data" if is_multipart else "application/json"
322
+ body = ExtractedBody(
323
+ content_type=content_type,
324
+ model_name=param.type_annotation,
325
+ required=True,
326
+ )
327
+
328
+ return ExtractedRoute(
329
+ method=http_method,
330
+ path=path_template,
331
+ handler_function=_str_to_qname(f"{cls.name}.{method.name}"),
332
+ handler_location=method.location,
333
+ path_params=path_params,
334
+ query_params=query_params,
335
+ header_params=header_params,
336
+ cookie_params=[],
337
+ body=body,
338
+ response=ExtractedResponse(
339
+ status_code=200,
340
+ model_name=self._extract_return_type(method),
341
+ ),
342
+ tags=["refit"],
343
+ dependency_refs=[],
344
+ confidence=Confidence.HIGH,
345
+ kind="refit_client",
346
+ )
347
+
348
+ # -------------------------------------------------------------------------
349
+ # Helpers
350
+ # -------------------------------------------------------------------------
351
+
352
+ def _is_scalar(self, type_annotation: str | None) -> bool:
353
+ if not type_annotation:
354
+ return True
355
+ base = type_annotation.split("<")[0].strip().rstrip("?")
356
+ return base in _SCALAR_TYPES
357
+
358
+ def _extract_return_type(self, method: ParsedFunction) -> str | None:
359
+ """
360
+ Extract the inner model type from Task<T> or IObservable<T> return.
361
+ e.g. Task<List<User>> → List<User>, Task<User> → User, Task → None
362
+ """
363
+ ret = method.return_type or ""
364
+ # Unwrap Task<> / IObservable<> / ValueTask<>
365
+ for wrapper in ("Task<", "IObservable<", "ValueTask<", "ApiResponse<"):
366
+ if ret.startswith(wrapper) and ret.endswith(">"):
367
+ inner = ret[len(wrapper) : -1].strip()
368
+ return inner if inner else None
369
+ return None
370
+
371
+
372
+ # Self-registration
373
+ _refit_plugin = RefitPlugin()
374
+ FrameworkPluginRegistry.register(_refit_plugin)