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.
- apisec_code_bolt/__init__.py +42 -0
- apisec_code_bolt/__main__.py +11 -0
- apisec_code_bolt/analysis/__init__.py +96 -0
- apisec_code_bolt/analysis/analyzer.py +2309 -0
- apisec_code_bolt/analysis/binding_tracker.py +341 -0
- apisec_code_bolt/analysis/call_graph.py +1197 -0
- apisec_code_bolt/analysis/call_graph_types.py +332 -0
- apisec_code_bolt/analysis/call_resolver.py +988 -0
- apisec_code_bolt/analysis/capability_tagger.py +322 -0
- apisec_code_bolt/analysis/config_scanner.py +197 -0
- apisec_code_bolt/analysis/data_flow.py +1883 -0
- apisec_code_bolt/analysis/dependency_extractor.py +959 -0
- apisec_code_bolt/analysis/flow_analysis.py +1406 -0
- apisec_code_bolt/analysis/hof_catalog.py +61 -0
- apisec_code_bolt/analysis/integration_detector.py +1399 -0
- apisec_code_bolt/analysis/literal_scanner.py +300 -0
- apisec_code_bolt/analysis/path_normalizer.py +55 -0
- apisec_code_bolt/analysis/read_site_detector.py +310 -0
- apisec_code_bolt/analysis/request_patterns.py +162 -0
- apisec_code_bolt/analysis/sensitivity_classifier.py +224 -0
- apisec_code_bolt/analysis/sink_evidence.py +333 -0
- apisec_code_bolt/analysis/url_prefix_resolver.py +338 -0
- apisec_code_bolt/cli/__init__.py +5 -0
- apisec_code_bolt/cli/exit_codes.py +17 -0
- apisec_code_bolt/cli/main.py +1069 -0
- apisec_code_bolt/cloud/__init__.py +1 -0
- apisec_code_bolt/cloud/apisec_client.py +118 -0
- apisec_code_bolt/cloud/client.py +255 -0
- apisec_code_bolt/core/__init__.py +75 -0
- apisec_code_bolt/core/config.py +528 -0
- apisec_code_bolt/core/credentials.py +65 -0
- apisec_code_bolt/core/discovery.py +433 -0
- apisec_code_bolt/core/log_format.py +115 -0
- apisec_code_bolt/core/manifest.py +1009 -0
- apisec_code_bolt/core/repo.py +280 -0
- apisec_code_bolt/core/state.py +59 -0
- apisec_code_bolt/core/telemetry.py +451 -0
- apisec_code_bolt/core/types.py +587 -0
- apisec_code_bolt/fingerprinting/__init__.py +1 -0
- apisec_code_bolt/frameworks/__init__.py +29 -0
- apisec_code_bolt/frameworks/_jwt_common.py +50 -0
- apisec_code_bolt/frameworks/auth_helpers.py +437 -0
- apisec_code_bolt/frameworks/base.py +608 -0
- apisec_code_bolt/frameworks/dotnet/__init__.py +17 -0
- apisec_code_bolt/frameworks/dotnet/_path_helpers.py +43 -0
- apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +2546 -0
- apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +559 -0
- apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +545 -0
- apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +732 -0
- apisec_code_bolt/frameworks/dotnet/refit_plugin.py +374 -0
- apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +1239 -0
- apisec_code_bolt/frameworks/java/__init__.py +6 -0
- apisec_code_bolt/frameworks/java/_annotations.py +167 -0
- apisec_code_bolt/frameworks/java/_constraints.py +128 -0
- apisec_code_bolt/frameworks/java/graphql_plugin.py +287 -0
- apisec_code_bolt/frameworks/java/jaxrs_plugin.py +748 -0
- apisec_code_bolt/frameworks/java/jwt_config_extractor.py +361 -0
- apisec_code_bolt/frameworks/java/micronaut_plugin.py +1059 -0
- apisec_code_bolt/frameworks/java/spring_plugin.py +1293 -0
- apisec_code_bolt/frameworks/js/__init__.py +8 -0
- apisec_code_bolt/frameworks/js/express_plugin.py +391 -0
- apisec_code_bolt/frameworks/js/fastify_plugin.py +381 -0
- apisec_code_bolt/frameworks/js/graphql_plugin.py +198 -0
- apisec_code_bolt/frameworks/js/nestjs_plugin.py +423 -0
- apisec_code_bolt/frameworks/python/__init__.py +19 -0
- apisec_code_bolt/frameworks/python/celery_plugin.py +393 -0
- apisec_code_bolt/frameworks/python/click_plugin.py +427 -0
- apisec_code_bolt/frameworks/python/django_plugin.py +867 -0
- apisec_code_bolt/frameworks/python/fastapi/__init__.py +28 -0
- apisec_code_bolt/frameworks/python/fastapi/plugin.py +1390 -0
- apisec_code_bolt/frameworks/python/flask_plugin.py +205 -0
- apisec_code_bolt/frameworks/python/graphql_plugin.py +274 -0
- apisec_code_bolt/frameworks/python/prefect_plugin.py +251 -0
- apisec_code_bolt/frameworks/python/webhook_plugin.py +255 -0
- apisec_code_bolt/parsing/__init__.py +62 -0
- apisec_code_bolt/parsing/base.py +554 -0
- apisec_code_bolt/parsing/csharp/__init__.py +5 -0
- apisec_code_bolt/parsing/csharp/language_services.py +203 -0
- apisec_code_bolt/parsing/csharp/literals.py +72 -0
- apisec_code_bolt/parsing/csharp/parser.py +1158 -0
- apisec_code_bolt/parsing/csharp/type_resolver.py +568 -0
- apisec_code_bolt/parsing/js/__init__.py +5 -0
- apisec_code_bolt/parsing/js/language_services.py +118 -0
- apisec_code_bolt/parsing/js/parser.py +622 -0
- apisec_code_bolt/parsing/jvm/__init__.py +7 -0
- apisec_code_bolt/parsing/jvm/language_services.py +270 -0
- apisec_code_bolt/parsing/jvm/parser.py +774 -0
- apisec_code_bolt/parsing/jvm/type_resolver.py +422 -0
- apisec_code_bolt/parsing/python/__init__.py +150 -0
- apisec_code_bolt/parsing/python/cbv_extractor.py +606 -0
- apisec_code_bolt/parsing/python/constant_resolver.py +500 -0
- apisec_code_bolt/parsing/python/cross_file_resolver.py +1054 -0
- apisec_code_bolt/parsing/python/dynamic_route_detector.py +532 -0
- apisec_code_bolt/parsing/python/expression_utils.py +221 -0
- apisec_code_bolt/parsing/python/extraction_types.py +271 -0
- apisec_code_bolt/parsing/python/language_services.py +487 -0
- apisec_code_bolt/parsing/python/parameter_analyzer.py +789 -0
- apisec_code_bolt/parsing/python/parser.py +719 -0
- apisec_code_bolt/parsing/python/path_resolver.py +576 -0
- apisec_code_bolt/parsing/python/router_registry.py +806 -0
- apisec_code_bolt/parsing/python/type_resolver.py +730 -0
- apisec_code_bolt/parsing/python/visitors.py +1544 -0
- apisec_code_bolt/parsing/services.py +544 -0
- apisec_code_bolt/query/__init__.py +1 -0
- apisec_code_bolt/query/ast_cache.py +182 -0
- apisec_code_bolt/query/executor.py +283 -0
- apisec_code_bolt/query/handlers.py +832 -0
- apisec_code_bolt-0.1.0.dist-info/METADATA +230 -0
- apisec_code_bolt-0.1.0.dist-info/RECORD +111 -0
- apisec_code_bolt-0.1.0.dist-info/WHEEL +4 -0
- apisec_code_bolt-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for apisec-code-bolt.
|
|
3
|
+
|
|
4
|
+
Uses Pydantic for type-safe configuration with validation.
|
|
5
|
+
Configuration can come from:
|
|
6
|
+
1. Configuration file (.codebolt.yaml)
|
|
7
|
+
2. Environment variables (CODEBOLT_*)
|
|
8
|
+
3. CLI arguments (highest priority)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Literal
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
17
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
18
|
+
|
|
19
|
+
DEFAULT_API_URL = "https://api.apisec.ai"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# =============================================================================
|
|
23
|
+
# File Discovery Configuration
|
|
24
|
+
# =============================================================================
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FileDiscoveryConfig(BaseModel):
|
|
28
|
+
"""Configuration for file discovery."""
|
|
29
|
+
|
|
30
|
+
# Always excluded directories (hardcoded for safety)
|
|
31
|
+
ALWAYS_EXCLUDE: frozenset[str] = frozenset(
|
|
32
|
+
{
|
|
33
|
+
# Version control
|
|
34
|
+
".git",
|
|
35
|
+
".svn",
|
|
36
|
+
".hg",
|
|
37
|
+
# Dependencies
|
|
38
|
+
"node_modules",
|
|
39
|
+
"vendor",
|
|
40
|
+
"venv",
|
|
41
|
+
".venv",
|
|
42
|
+
"env",
|
|
43
|
+
".env",
|
|
44
|
+
# Build artifacts
|
|
45
|
+
"build",
|
|
46
|
+
"dist",
|
|
47
|
+
"target",
|
|
48
|
+
"out",
|
|
49
|
+
".gradle",
|
|
50
|
+
"__pycache__",
|
|
51
|
+
".pytest_cache",
|
|
52
|
+
".mypy_cache",
|
|
53
|
+
".ruff_cache",
|
|
54
|
+
"*.egg-info",
|
|
55
|
+
# IDE
|
|
56
|
+
".idea",
|
|
57
|
+
".vscode",
|
|
58
|
+
".eclipse",
|
|
59
|
+
# Generated
|
|
60
|
+
"generated",
|
|
61
|
+
"gen",
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# User-configurable exclusions (in addition to ALWAYS_EXCLUDE)
|
|
66
|
+
exclude_patterns: list[str] = Field(
|
|
67
|
+
default_factory=lambda: [
|
|
68
|
+
"tests/**",
|
|
69
|
+
"**/*_test.py",
|
|
70
|
+
"**/test_*.py",
|
|
71
|
+
"**/migrations/**",
|
|
72
|
+
"**/conftest.py",
|
|
73
|
+
],
|
|
74
|
+
description="Glob patterns to exclude from analysis",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
include_tests: bool = Field(
|
|
78
|
+
default=False,
|
|
79
|
+
description="Whether to analyze test files",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
max_files: int = Field(
|
|
83
|
+
default=10_000,
|
|
84
|
+
ge=1,
|
|
85
|
+
le=100_000,
|
|
86
|
+
description="Maximum number of files to analyze",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
max_file_size_mb: float = Field(
|
|
90
|
+
default=10.0,
|
|
91
|
+
ge=0.001,
|
|
92
|
+
le=100.0,
|
|
93
|
+
description="Maximum file size in MB (skip larger files)",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
respect_gitignore: bool = Field(
|
|
97
|
+
default=True,
|
|
98
|
+
description="Whether to respect .gitignore patterns",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
follow_symlinks: bool = Field(
|
|
102
|
+
default=False,
|
|
103
|
+
description="Whether to follow symbolic links",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# =============================================================================
|
|
108
|
+
# Data Flow Analysis Configuration
|
|
109
|
+
# =============================================================================
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class DataFlowConfig(BaseModel):
|
|
113
|
+
"""Configuration for data flow analysis."""
|
|
114
|
+
|
|
115
|
+
mode: Literal["intra_procedural", "inter_procedural"] = Field(
|
|
116
|
+
default="inter_procedural",
|
|
117
|
+
description="Data flow tracking mode",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
max_depth: int = Field(
|
|
121
|
+
default=10,
|
|
122
|
+
ge=1,
|
|
123
|
+
le=50,
|
|
124
|
+
description="Maximum call depth for inter-procedural analysis",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
track_through_libraries: bool = Field(
|
|
128
|
+
default=False,
|
|
129
|
+
description="Whether to trace into third-party library code",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
track_lambdas: bool = Field(
|
|
133
|
+
default=True,
|
|
134
|
+
description="Whether to track data flow through lambdas/closures",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
track_callbacks: bool = Field(
|
|
138
|
+
default=True,
|
|
139
|
+
description="Whether to track data flow through callbacks",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
max_flows_per_origin: int = Field(
|
|
143
|
+
default=100,
|
|
144
|
+
ge=1,
|
|
145
|
+
le=1000,
|
|
146
|
+
description="Maximum data flows to track per origin (prevents explosion)",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# =============================================================================
|
|
151
|
+
# JVM Analyzer Configuration
|
|
152
|
+
# =============================================================================
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class JvmConfig(BaseModel):
|
|
156
|
+
"""Configuration for JVM (Java/Kotlin) analysis."""
|
|
157
|
+
|
|
158
|
+
enabled: bool = Field(
|
|
159
|
+
default=True,
|
|
160
|
+
description="Whether to analyze JVM files (requires Java runtime)",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
java_home: Path | None = Field(
|
|
164
|
+
default=None,
|
|
165
|
+
description="Path to Java installation (auto-detected if not set)",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
heap_mb: int = Field(
|
|
169
|
+
default=512,
|
|
170
|
+
ge=128,
|
|
171
|
+
le=8192,
|
|
172
|
+
description="JVM heap size in MB",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
batch_size: int = Field(
|
|
176
|
+
default=200,
|
|
177
|
+
ge=10,
|
|
178
|
+
le=1000,
|
|
179
|
+
description="Number of files per JVM batch",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
max_parallel_batches: int = Field(
|
|
183
|
+
default=4,
|
|
184
|
+
ge=1,
|
|
185
|
+
le=16,
|
|
186
|
+
description="Maximum parallel JVM processes",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
timeout_per_batch_seconds: int = Field(
|
|
190
|
+
default=300,
|
|
191
|
+
ge=30,
|
|
192
|
+
le=3600,
|
|
193
|
+
description="Timeout per batch in seconds",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# =============================================================================
|
|
198
|
+
# Cloud Communication Configuration
|
|
199
|
+
# =============================================================================
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class CloudConfig(BaseModel):
|
|
203
|
+
"""Configuration for cloud communication."""
|
|
204
|
+
|
|
205
|
+
enabled: bool = Field(
|
|
206
|
+
default=True,
|
|
207
|
+
description="Whether to upload manifests to cloud",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
api_url: str = Field(
|
|
211
|
+
default=DEFAULT_API_URL,
|
|
212
|
+
description="Cloud API base URL",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
api_key: str | None = Field(
|
|
216
|
+
default=None,
|
|
217
|
+
description="API key for authentication (can also use CODEBOLT_API_KEY env var)",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
timeout_seconds: int = Field(
|
|
221
|
+
default=60,
|
|
222
|
+
ge=10,
|
|
223
|
+
le=300,
|
|
224
|
+
description="HTTP request timeout in seconds",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
retry_attempts: int = Field(
|
|
228
|
+
default=3,
|
|
229
|
+
ge=0,
|
|
230
|
+
le=10,
|
|
231
|
+
description="Number of retry attempts for failed requests",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
verify_ssl: bool = Field(
|
|
235
|
+
default=True,
|
|
236
|
+
description="Whether to verify SSL certificates",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# =============================================================================
|
|
241
|
+
# Query API Configuration
|
|
242
|
+
# =============================================================================
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class QueryApiConfig(BaseModel):
|
|
246
|
+
"""Configuration for Query API (extended analysis)."""
|
|
247
|
+
|
|
248
|
+
enabled: bool = Field(
|
|
249
|
+
default=True,
|
|
250
|
+
description="Whether to enable Query API for verification queries",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
max_wait_seconds: int = Field(
|
|
254
|
+
default=300,
|
|
255
|
+
ge=30,
|
|
256
|
+
le=1800,
|
|
257
|
+
description="Maximum time to wait for queries from cloud",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
max_batches: int = Field(
|
|
261
|
+
default=10,
|
|
262
|
+
ge=1,
|
|
263
|
+
le=50,
|
|
264
|
+
description="Maximum number of query batches to process",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
poll_timeout_seconds: int = Field(
|
|
268
|
+
default=30,
|
|
269
|
+
ge=5,
|
|
270
|
+
le=60,
|
|
271
|
+
description="Long-poll timeout for each request",
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# =============================================================================
|
|
276
|
+
# Analysis Configuration
|
|
277
|
+
# =============================================================================
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class AnalysisConfig(BaseModel):
|
|
281
|
+
"""Configuration for the analysis process."""
|
|
282
|
+
|
|
283
|
+
timeout_seconds: int = Field(
|
|
284
|
+
default=3600,
|
|
285
|
+
ge=60,
|
|
286
|
+
le=14400,
|
|
287
|
+
description="Total analysis timeout in seconds",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
framework_hints: list[str] = Field(
|
|
291
|
+
default_factory=list,
|
|
292
|
+
description="Framework hints to guide detection (e.g., ['fastapi', 'sqlalchemy'])",
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Sub-configurations
|
|
296
|
+
file_discovery: FileDiscoveryConfig = Field(
|
|
297
|
+
default_factory=FileDiscoveryConfig,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
data_flow: DataFlowConfig = Field(
|
|
301
|
+
default_factory=DataFlowConfig,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
jvm: JvmConfig = Field(
|
|
305
|
+
default_factory=JvmConfig,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# =============================================================================
|
|
310
|
+
# Output Configuration
|
|
311
|
+
# =============================================================================
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class OutputConfig(BaseModel):
|
|
315
|
+
"""Configuration for output generation."""
|
|
316
|
+
|
|
317
|
+
format: Literal["json", "yaml"] = Field(
|
|
318
|
+
default="json",
|
|
319
|
+
description="Output format",
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
pretty_print: bool = Field(
|
|
323
|
+
default=False,
|
|
324
|
+
description="Whether to pretty-print output",
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
output_file: Path | None = Field(
|
|
328
|
+
default=None,
|
|
329
|
+
description="Output file path (stdout if not set)",
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
include_source_snippets: bool = Field(
|
|
333
|
+
default=False,
|
|
334
|
+
description="NEVER enable in production - would include raw code",
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
@field_validator("include_source_snippets")
|
|
338
|
+
@classmethod
|
|
339
|
+
def warn_source_snippets(cls, v: bool) -> bool:
|
|
340
|
+
"""Warn if source snippets are enabled."""
|
|
341
|
+
if v:
|
|
342
|
+
import warnings
|
|
343
|
+
|
|
344
|
+
warnings.warn(
|
|
345
|
+
"include_source_snippets=True would expose raw source code. "
|
|
346
|
+
"This should NEVER be enabled in production.",
|
|
347
|
+
UserWarning,
|
|
348
|
+
stacklevel=2,
|
|
349
|
+
)
|
|
350
|
+
return v
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# =============================================================================
|
|
354
|
+
# Main Configuration
|
|
355
|
+
# =============================================================================
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class CodeBoltConfig(BaseSettings):
|
|
359
|
+
"""
|
|
360
|
+
Main configuration for apisec-code-bolt.
|
|
361
|
+
|
|
362
|
+
Configuration priority (highest to lowest):
|
|
363
|
+
1. CLI arguments
|
|
364
|
+
2. Environment variables (CODEBOLT_*)
|
|
365
|
+
3. Configuration file (.codebolt.yaml)
|
|
366
|
+
4. Defaults
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
model_config = SettingsConfigDict(
|
|
370
|
+
env_prefix="CODEBOLT_",
|
|
371
|
+
env_nested_delimiter="__",
|
|
372
|
+
case_sensitive=False,
|
|
373
|
+
extra="ignore",
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# Project settings
|
|
377
|
+
project_root: Path = Field(
|
|
378
|
+
default_factory=Path.cwd,
|
|
379
|
+
description="Root directory of the project to analyze",
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
config_file: Path | None = Field(
|
|
383
|
+
default=None,
|
|
384
|
+
description="Path to configuration file",
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Sub-configurations
|
|
388
|
+
analysis: AnalysisConfig = Field(
|
|
389
|
+
default_factory=AnalysisConfig,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
cloud: CloudConfig = Field(
|
|
393
|
+
default_factory=CloudConfig,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
query_api: QueryApiConfig = Field(
|
|
397
|
+
default_factory=QueryApiConfig,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
output: OutputConfig = Field(
|
|
401
|
+
default_factory=OutputConfig,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Runtime flags
|
|
405
|
+
verbose: bool = Field(
|
|
406
|
+
default=False,
|
|
407
|
+
description="Enable verbose output",
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
quiet: bool = Field(
|
|
411
|
+
default=False,
|
|
412
|
+
description="Suppress non-essential output",
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
debug: bool = Field(
|
|
416
|
+
default=False,
|
|
417
|
+
description="Enable debug mode",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
@model_validator(mode="after")
|
|
421
|
+
def validate_config(self) -> CodeBoltConfig:
|
|
422
|
+
"""Validate configuration consistency."""
|
|
423
|
+
if self.verbose and self.quiet:
|
|
424
|
+
raise ValueError("Cannot enable both verbose and quiet modes")
|
|
425
|
+
|
|
426
|
+
if not self.project_root.exists():
|
|
427
|
+
raise ValueError(f"Project root does not exist: {self.project_root}")
|
|
428
|
+
|
|
429
|
+
if not self.project_root.is_dir():
|
|
430
|
+
raise ValueError(f"Project root is not a directory: {self.project_root}")
|
|
431
|
+
|
|
432
|
+
return self
|
|
433
|
+
|
|
434
|
+
@classmethod
|
|
435
|
+
def from_file(cls, config_path: Path) -> CodeBoltConfig:
|
|
436
|
+
"""Load configuration from a YAML file."""
|
|
437
|
+
import yaml
|
|
438
|
+
|
|
439
|
+
if not config_path.exists():
|
|
440
|
+
raise FileNotFoundError(f"Configuration file not found: {config_path}")
|
|
441
|
+
|
|
442
|
+
with open(config_path) as f:
|
|
443
|
+
data = yaml.safe_load(f) or {}
|
|
444
|
+
|
|
445
|
+
return cls(**data)
|
|
446
|
+
|
|
447
|
+
@classmethod
|
|
448
|
+
def from_cli(
|
|
449
|
+
cls,
|
|
450
|
+
project_root: Path,
|
|
451
|
+
config_file: Path | None = None,
|
|
452
|
+
**overrides: Any,
|
|
453
|
+
) -> CodeBoltConfig:
|
|
454
|
+
"""
|
|
455
|
+
Create configuration from CLI arguments.
|
|
456
|
+
|
|
457
|
+
Merges config file (if provided) with CLI overrides.
|
|
458
|
+
"""
|
|
459
|
+
base_config: dict[str, Any] = {}
|
|
460
|
+
|
|
461
|
+
# Load from config file if provided
|
|
462
|
+
if config_file and config_file.exists():
|
|
463
|
+
import yaml
|
|
464
|
+
|
|
465
|
+
with open(config_file) as f:
|
|
466
|
+
base_config = yaml.safe_load(f) or {}
|
|
467
|
+
|
|
468
|
+
# Apply CLI overrides
|
|
469
|
+
base_config["project_root"] = project_root
|
|
470
|
+
if config_file:
|
|
471
|
+
base_config["config_file"] = config_file
|
|
472
|
+
|
|
473
|
+
# Deep merge overrides
|
|
474
|
+
for key, value in overrides.items():
|
|
475
|
+
if value is not None:
|
|
476
|
+
cls._deep_set(base_config, key, value)
|
|
477
|
+
|
|
478
|
+
return cls(**base_config)
|
|
479
|
+
|
|
480
|
+
@staticmethod
|
|
481
|
+
def _deep_set(d: dict[str, Any], key: str, value: Any) -> None:
|
|
482
|
+
"""Set a nested key in a dictionary."""
|
|
483
|
+
keys = key.split(".")
|
|
484
|
+
for k in keys[:-1]:
|
|
485
|
+
d = d.setdefault(k, {})
|
|
486
|
+
d[keys[-1]] = value
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
# =============================================================================
|
|
490
|
+
# Configuration Helpers
|
|
491
|
+
# =============================================================================
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def get_default_config() -> CodeBoltConfig:
|
|
495
|
+
"""Get default configuration."""
|
|
496
|
+
return CodeBoltConfig()
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def load_config(
|
|
500
|
+
project_root: Path | None = None,
|
|
501
|
+
config_file: Path | None = None,
|
|
502
|
+
) -> CodeBoltConfig:
|
|
503
|
+
"""
|
|
504
|
+
Load configuration with auto-detection.
|
|
505
|
+
|
|
506
|
+
Looks for .codebolt.yaml in project root if config_file not specified.
|
|
507
|
+
"""
|
|
508
|
+
root = project_root or Path.cwd()
|
|
509
|
+
|
|
510
|
+
# Auto-detect config file
|
|
511
|
+
if config_file is None:
|
|
512
|
+
for name in [
|
|
513
|
+
".surface.yaml",
|
|
514
|
+
".surface.yml",
|
|
515
|
+
".codebolt.yaml",
|
|
516
|
+
".codebolt.yml",
|
|
517
|
+
"codebolt.yaml",
|
|
518
|
+
"codebolt.yml",
|
|
519
|
+
]:
|
|
520
|
+
candidate = root / name
|
|
521
|
+
if candidate.exists():
|
|
522
|
+
config_file = candidate
|
|
523
|
+
break
|
|
524
|
+
|
|
525
|
+
if config_file:
|
|
526
|
+
return CodeBoltConfig.from_file(config_file)
|
|
527
|
+
|
|
528
|
+
return CodeBoltConfig(project_root=root)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""API key credential storage for the CLI.
|
|
2
|
+
|
|
3
|
+
Stores credentials in ~/.config/apisec-code-bolt/credentials.json.
|
|
4
|
+
The file is created with restricted permissions (600).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import stat
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
CONFIG_DIR = Path.home() / ".config" / "apisec-code-bolt"
|
|
19
|
+
CREDENTIALS_FILE = CONFIG_DIR / "credentials.json"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class StoredCredentials:
|
|
24
|
+
"""Stored API credentials."""
|
|
25
|
+
|
|
26
|
+
api_key: str
|
|
27
|
+
api_url: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def store_credentials(api_key: str, api_url: str) -> Path:
|
|
31
|
+
"""Store API key and URL. Creates directory and file with restricted permissions."""
|
|
32
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
os.chmod(CONFIG_DIR, stat.S_IRWXU)
|
|
34
|
+
data = json.dumps({"api_key": api_key, "api_url": api_url}, indent=2)
|
|
35
|
+
fd = os.open(
|
|
36
|
+
str(CREDENTIALS_FILE),
|
|
37
|
+
os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
|
|
38
|
+
0o600,
|
|
39
|
+
)
|
|
40
|
+
with os.fdopen(fd, "w") as f:
|
|
41
|
+
f.write(data)
|
|
42
|
+
logger.info("Credentials stored at %s", CREDENTIALS_FILE)
|
|
43
|
+
return CREDENTIALS_FILE
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load_credentials() -> StoredCredentials | None:
|
|
47
|
+
"""Load stored credentials. Returns None if not found."""
|
|
48
|
+
if not CREDENTIALS_FILE.exists():
|
|
49
|
+
return None
|
|
50
|
+
try:
|
|
51
|
+
data = json.loads(CREDENTIALS_FILE.read_text())
|
|
52
|
+
return StoredCredentials(
|
|
53
|
+
api_key=data.get("api_key", ""),
|
|
54
|
+
api_url=data.get("api_url", ""),
|
|
55
|
+
)
|
|
56
|
+
except Exception:
|
|
57
|
+
logger.warning("Failed to load credentials from %s", CREDENTIALS_FILE, exc_info=True)
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def clear_credentials() -> None:
|
|
62
|
+
"""Remove stored credentials."""
|
|
63
|
+
if CREDENTIALS_FILE.exists():
|
|
64
|
+
CREDENTIALS_FILE.unlink()
|
|
65
|
+
logger.info("Credentials removed")
|