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 @@
1
+ """Cloud communication client for manifest upload and Query API."""
@@ -0,0 +1,118 @@
1
+ """PAT-authenticated onboarding: register with applicationsservice, upload to reasoning engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import httpx
11
+
12
+ from .client import CloudClient
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class RegistrationResult:
19
+ """Result of POST /v1/applications/surface."""
20
+
21
+ app_id: str
22
+
23
+
24
+ @dataclass
25
+ class RepoInfo:
26
+ """Repository metadata for registration."""
27
+
28
+ repo_name: str
29
+ repo_url: str
30
+ branch: str
31
+ commit_sha: str
32
+ scm_provider: str
33
+ canonical_repo_id: str
34
+
35
+
36
+ class ApisecClient:
37
+ """Registers the repo surface with applicationsservice (PAT), then talks to the reasoning engine with the same PAT."""
38
+
39
+ def __init__(
40
+ self,
41
+ api_url: str,
42
+ pat: str,
43
+ timeout_seconds: int = 60,
44
+ verify_ssl: bool = True,
45
+ ):
46
+ self._api_url = api_url.rstrip("/")
47
+ self._pat = pat
48
+ self._timeout = timeout_seconds
49
+ self._verify_ssl = verify_ssl
50
+
51
+ def register(
52
+ self,
53
+ repo_info: RepoInfo,
54
+ existing_app_id: str | None = None,
55
+ force_new_application: bool = False,
56
+ ) -> RegistrationResult:
57
+ """POST /v1/applications/surface with Bearer PAT; returns app id."""
58
+ body: dict[str, Any] = {
59
+ "repoName": repo_info.repo_name,
60
+ "repoUrl": repo_info.repo_url,
61
+ "branch": repo_info.branch,
62
+ "commitSha": repo_info.commit_sha,
63
+ "scmProvider": repo_info.scm_provider,
64
+ "canonicalRepoId": repo_info.canonical_repo_id,
65
+ }
66
+ if existing_app_id:
67
+ body["existingAppId"] = existing_app_id
68
+ if force_new_application:
69
+ body["forceNewApplication"] = True
70
+
71
+ with httpx.Client(
72
+ base_url=self._api_url,
73
+ timeout=httpx.Timeout(self._timeout),
74
+ verify=self._verify_ssl,
75
+ ) as client:
76
+ response = client.post(
77
+ "/v1/applications/surface",
78
+ json=body,
79
+ headers={"Authorization": f"Bearer {self._pat}"},
80
+ )
81
+ response.raise_for_status()
82
+ data = response.json()
83
+
84
+ app_id = data.get("appId") or data.get("app_id")
85
+ if not app_id:
86
+ raise ValueError("surface registration response missing appId")
87
+ result = RegistrationResult(app_id=str(app_id))
88
+ logger.info("Surface registration resolved app_id=%s", result.app_id)
89
+ return result
90
+
91
+ def upload_and_verify(
92
+ self,
93
+ registration: RegistrationResult,
94
+ manifest_data: dict[str, Any],
95
+ project_root: Path,
96
+ reasoning_engine_url: str | None = None,
97
+ ) -> dict[str, Any]:
98
+ """Upload manifest to reasoning engine with Bearer PAT and app_id; run verification."""
99
+ engine_url = reasoning_engine_url or self._api_url
100
+ with CloudClient(
101
+ api_url=engine_url,
102
+ api_key=self._pat,
103
+ app_id=registration.app_id,
104
+ verify_ssl=self._verify_ssl,
105
+ ) as client:
106
+ from ..query.executor import QueryExecutor
107
+
108
+ executor = QueryExecutor(project_root=project_root)
109
+ result = executor.run_connected(client, manifest_data)
110
+
111
+ return {
112
+ "analysis_id": result.analysis_id,
113
+ "status": result.final_status,
114
+ "rounds": result.stats.rounds_completed,
115
+ "questions_answered": result.stats.questions_answered,
116
+ "elapsed_seconds": result.stats.elapsed_seconds,
117
+ "error": result.error,
118
+ }
@@ -0,0 +1,255 @@
1
+ """
2
+ HTTP client for communicating with the APIsec reasoning engine.
3
+
4
+ Handles manifest upload, question polling, and answer submission.
5
+ All communication is outbound-only (probe initiates).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import time
12
+ from dataclasses import dataclass, field
13
+ from typing import Any
14
+ from urllib.parse import quote
15
+
16
+ import httpx
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass
22
+ class QuestionItem:
23
+ """A single verification question from the engine."""
24
+
25
+ id: str
26
+ type: str
27
+ gate_id: str
28
+ finding_id: str
29
+ round_number: int
30
+ target_file: str | None = None
31
+ target_function: str | None = None
32
+ target_line: int | None = None
33
+ params: dict[str, Any] = field(default_factory=dict)
34
+
35
+
36
+ @dataclass
37
+ class QuestionBatchResult:
38
+ """Result of polling for questions."""
39
+
40
+ status: str
41
+ batch_id: str | None = None
42
+ round_number: int | None = None
43
+ questions: list[QuestionItem] = field(default_factory=list)
44
+ retry_after: int | None = None
45
+
46
+
47
+ @dataclass
48
+ class SubmitResult:
49
+ """Result of submitting answers."""
50
+
51
+ received: bool
52
+ answers_processed: int
53
+ more_questions: bool
54
+ status: str
55
+
56
+
57
+ class CloudClient:
58
+ """HTTP client for the reasoning engine API."""
59
+
60
+ def __init__(
61
+ self,
62
+ api_url: str,
63
+ api_key: str | None = None,
64
+ *,
65
+ app_id: str | None = None,
66
+ timeout_seconds: int = 60,
67
+ retry_attempts: int = 3,
68
+ verify_ssl: bool = True,
69
+ ):
70
+ self._base_url = api_url.rstrip("/")
71
+ self._api_key = api_key
72
+ self._app_id = app_id
73
+ self._timeout = timeout_seconds
74
+ self._retries = retry_attempts
75
+
76
+ headers: dict[str, str] = {}
77
+ if api_key:
78
+ headers["Authorization"] = f"Bearer {api_key}"
79
+
80
+ self._client = httpx.Client(
81
+ base_url=self._base_url,
82
+ headers=headers,
83
+ timeout=httpx.Timeout(timeout_seconds),
84
+ verify=verify_ssl,
85
+ )
86
+
87
+ def close(self) -> None:
88
+ self._client.close()
89
+
90
+ def __enter__(self) -> CloudClient:
91
+ return self
92
+
93
+ def __exit__(self, *args: Any) -> None:
94
+ self.close()
95
+
96
+ # ------------------------------------------------------------------
97
+ # Manifest upload
98
+ # ------------------------------------------------------------------
99
+
100
+ def upload_manifest(self, manifest_data: dict[str, Any]) -> tuple[str, str]:
101
+ """Upload a manifest and return (analysis_id, status)."""
102
+ if not self._app_id:
103
+ raise ValueError(
104
+ "app_id is required for manifest upload (register via POST /v1/applications/surface "
105
+ "or set application id in apisec-code-bolt/state.yaml / APISEC_APP_ID)",
106
+ )
107
+ path = f"/api/v1/manifests?app_id={quote(self._app_id, safe='')}"
108
+ response = self._post(path, json=manifest_data)
109
+ data = response.json()
110
+ analysis_id = data["analysis_id"]
111
+ status = data["status"]
112
+ logger.info(
113
+ "Manifest uploaded: analysis_id=%s status=%s message=%s",
114
+ analysis_id,
115
+ status,
116
+ data.get("message", ""),
117
+ )
118
+ return analysis_id, status
119
+
120
+ # ------------------------------------------------------------------
121
+ # Question polling
122
+ # ------------------------------------------------------------------
123
+
124
+ def poll_questions(
125
+ self,
126
+ analysis_id: str,
127
+ poll_timeout: int = 30,
128
+ max_wait: int = 300,
129
+ ) -> QuestionBatchResult:
130
+ """Poll for verification questions with retry and backoff.
131
+
132
+ Keeps polling until questions are ready, analysis is complete,
133
+ or max_wait is exceeded.
134
+ """
135
+ start = time.monotonic()
136
+ backoff = 1.0
137
+
138
+ while (time.monotonic() - start) < max_wait:
139
+ result = self._fetch_questions(analysis_id)
140
+
141
+ if result.status == "questions_ready":
142
+ return result
143
+ if result.status in ("complete", "failed"):
144
+ return result
145
+
146
+ wait = result.retry_after or backoff
147
+ logger.debug("No questions yet, retrying in %.1fs", wait)
148
+ time.sleep(wait)
149
+ backoff = min(backoff * 1.5, 10.0)
150
+
151
+ logger.warning("Question poll timed out after %ds", max_wait)
152
+ return QuestionBatchResult(status="timeout")
153
+
154
+ def _fetch_questions(self, analysis_id: str) -> QuestionBatchResult:
155
+ response = self._get(f"/api/v1/analyses/{analysis_id}/questions")
156
+ data = response.json()
157
+
158
+ questions = [
159
+ QuestionItem(
160
+ id=q["id"],
161
+ type=q["type"],
162
+ gate_id=q["gate_id"],
163
+ finding_id=q["finding_id"],
164
+ round_number=q.get("round_number", 1),
165
+ target_file=q.get("target_file"),
166
+ target_function=q.get("target_function"),
167
+ target_line=q.get("target_line"),
168
+ params=q.get("params", {}),
169
+ )
170
+ for q in data.get("questions", [])
171
+ ]
172
+
173
+ return QuestionBatchResult(
174
+ status=data["status"],
175
+ batch_id=data.get("batch_id"),
176
+ round_number=data.get("round_number"),
177
+ questions=questions,
178
+ retry_after=data.get("retry_after"),
179
+ )
180
+
181
+ # ------------------------------------------------------------------
182
+ # Answer submission
183
+ # ------------------------------------------------------------------
184
+
185
+ def submit_answers(
186
+ self,
187
+ analysis_id: str,
188
+ batch_id: str,
189
+ answers: list[dict[str, Any]],
190
+ ) -> SubmitResult:
191
+ """Submit answers for a batch of questions."""
192
+ payload = {"batch_id": batch_id, "answers": answers}
193
+ response = self._post(f"/api/v1/analyses/{analysis_id}/answers", json=payload)
194
+ data = response.json()
195
+
196
+ result = SubmitResult(
197
+ received=data["received"],
198
+ answers_processed=data["answers_processed"],
199
+ more_questions=data["more_questions"],
200
+ status=data["status"],
201
+ )
202
+ logger.info(
203
+ "Answers submitted: batch=%s processed=%d more_questions=%s",
204
+ batch_id,
205
+ result.answers_processed,
206
+ result.more_questions,
207
+ )
208
+ return result
209
+
210
+ # ------------------------------------------------------------------
211
+ # Analysis status
212
+ # ------------------------------------------------------------------
213
+
214
+ def get_analysis_status(self, analysis_id: str) -> dict[str, Any]:
215
+ response = self._get(f"/api/v1/analyses/{analysis_id}")
216
+ return response.json()
217
+
218
+ # ------------------------------------------------------------------
219
+ # HTTP helpers
220
+ # ------------------------------------------------------------------
221
+
222
+ def _get(self, path: str) -> httpx.Response:
223
+ return self._request("GET", path)
224
+
225
+ def _post(self, path: str, **kwargs: Any) -> httpx.Response:
226
+ return self._request("POST", path, **kwargs)
227
+
228
+ def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
229
+ last_exc: Exception | None = None
230
+
231
+ for attempt in range(self._retries + 1):
232
+ try:
233
+ response = self._client.request(method, path, **kwargs)
234
+ response.raise_for_status()
235
+ return response
236
+ except httpx.HTTPStatusError:
237
+ raise
238
+ except (httpx.TransportError, httpx.TimeoutException) as e:
239
+ last_exc = e
240
+ if attempt < self._retries:
241
+ wait = 2**attempt
242
+ logger.warning(
243
+ "Request %s %s failed (attempt %d/%d): %s — retrying in %ds",
244
+ method,
245
+ path,
246
+ attempt + 1,
247
+ self._retries + 1,
248
+ e,
249
+ wait,
250
+ )
251
+ time.sleep(wait)
252
+
253
+ raise ConnectionError(
254
+ f"Failed after {self._retries + 1} attempts: {last_exc}"
255
+ ) from last_exc
@@ -0,0 +1,75 @@
1
+ """Core types, configuration, and manifest definitions."""
2
+
3
+ from .config import (
4
+ AnalysisConfig,
5
+ CloudConfig,
6
+ CodeBoltConfig,
7
+ DataFlowConfig,
8
+ FileDiscoveryConfig,
9
+ JvmConfig,
10
+ OutputConfig,
11
+ QueryApiConfig,
12
+ get_default_config,
13
+ load_config,
14
+ )
15
+ from .manifest import MANIFEST_VERSION, Manifest
16
+ from .types import (
17
+ AnalysisError,
18
+ AnalysisNote,
19
+ AuthDependencyType,
20
+ AuthSchemeType,
21
+ CallContext,
22
+ CloudCommunicationError,
23
+ CodeBoltError,
24
+ CodeLocation,
25
+ Confidence,
26
+ ConfigurationError,
27
+ FlowContext,
28
+ Framework,
29
+ HttpMethod,
30
+ Language,
31
+ OriginType,
32
+ ParameterLocation,
33
+ ParseError,
34
+ QualifiedName,
35
+ TransformationType,
36
+ )
37
+
38
+ __all__ = [
39
+ # Enums
40
+ "Language",
41
+ "Framework",
42
+ "OriginType",
43
+ "TransformationType",
44
+ "AuthSchemeType",
45
+ "AuthDependencyType",
46
+ "HttpMethod",
47
+ "ParameterLocation",
48
+ "Confidence",
49
+ # Data types
50
+ "CodeLocation",
51
+ "QualifiedName",
52
+ "CallContext",
53
+ "FlowContext",
54
+ "AnalysisNote",
55
+ # Config
56
+ "CodeBoltConfig",
57
+ "AnalysisConfig",
58
+ "FileDiscoveryConfig",
59
+ "DataFlowConfig",
60
+ "JvmConfig",
61
+ "CloudConfig",
62
+ "QueryApiConfig",
63
+ "OutputConfig",
64
+ "load_config",
65
+ "get_default_config",
66
+ # Manifest
67
+ "Manifest",
68
+ "MANIFEST_VERSION",
69
+ # Exceptions
70
+ "CodeBoltError",
71
+ "ParseError",
72
+ "ConfigurationError",
73
+ "AnalysisError",
74
+ "CloudCommunicationError",
75
+ ]