security-use 0.1.1__py3-none-any.whl → 0.2.9__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 (45) hide show
  1. security_use/__init__.py +9 -1
  2. security_use/auth/__init__.py +16 -0
  3. security_use/auth/client.py +223 -0
  4. security_use/auth/config.py +177 -0
  5. security_use/auth/oauth.py +317 -0
  6. security_use/cli.py +699 -34
  7. security_use/compliance/__init__.py +10 -0
  8. security_use/compliance/mapper.py +275 -0
  9. security_use/compliance/models.py +50 -0
  10. security_use/dependency_scanner.py +76 -30
  11. security_use/fixers/iac_fixer.py +173 -95
  12. security_use/iac/rules/azure.py +246 -0
  13. security_use/iac/rules/gcp.py +255 -0
  14. security_use/iac/rules/kubernetes.py +429 -0
  15. security_use/iac/rules/registry.py +56 -0
  16. security_use/parsers/__init__.py +18 -0
  17. security_use/parsers/base.py +2 -0
  18. security_use/parsers/composer.py +101 -0
  19. security_use/parsers/conda.py +97 -0
  20. security_use/parsers/dotnet.py +89 -0
  21. security_use/parsers/gradle.py +90 -0
  22. security_use/parsers/maven.py +108 -0
  23. security_use/parsers/npm.py +196 -0
  24. security_use/parsers/yarn.py +108 -0
  25. security_use/reporter.py +29 -1
  26. security_use/sbom/__init__.py +10 -0
  27. security_use/sbom/generator.py +340 -0
  28. security_use/sbom/models.py +40 -0
  29. security_use/scanner.py +15 -2
  30. security_use/sensor/__init__.py +125 -0
  31. security_use/sensor/alert_queue.py +207 -0
  32. security_use/sensor/config.py +217 -0
  33. security_use/sensor/dashboard_alerter.py +246 -0
  34. security_use/sensor/detector.py +415 -0
  35. security_use/sensor/endpoint_analyzer.py +339 -0
  36. security_use/sensor/middleware.py +521 -0
  37. security_use/sensor/models.py +140 -0
  38. security_use/sensor/webhook.py +227 -0
  39. security_use-0.2.9.dist-info/METADATA +531 -0
  40. security_use-0.2.9.dist-info/RECORD +60 -0
  41. security_use-0.2.9.dist-info/licenses/LICENSE +21 -0
  42. security_use-0.1.1.dist-info/METADATA +0 -92
  43. security_use-0.1.1.dist-info/RECORD +0 -30
  44. {security_use-0.1.1.dist-info → security_use-0.2.9.dist-info}/WHEEL +0 -0
  45. {security_use-0.1.1.dist-info → security_use-0.2.9.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,40 @@
1
+ """Data models for SBOM generation."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from enum import Enum
6
+ from typing import Optional
7
+
8
+
9
+ class SBOMFormat(Enum):
10
+ """Supported SBOM output formats."""
11
+
12
+ CYCLONEDX_JSON = "cyclonedx-json"
13
+ CYCLONEDX_XML = "cyclonedx-xml"
14
+ SPDX_JSON = "spdx-json"
15
+ SPDX_TV = "spdx-tv" # Tag-value format
16
+
17
+
18
+ @dataclass
19
+ class SBOMComponent:
20
+ """Represents a component in the SBOM."""
21
+
22
+ name: str
23
+ version: str
24
+ ecosystem: str
25
+ purl: Optional[str] = None
26
+ licenses: list[str] = field(default_factory=list)
27
+ hashes: dict[str, str] = field(default_factory=dict)
28
+ supplier: Optional[str] = None
29
+ description: Optional[str] = None
30
+ vulnerabilities: list[str] = field(default_factory=list)
31
+
32
+
33
+ @dataclass
34
+ class SBOMOutput:
35
+ """Result of SBOM generation."""
36
+
37
+ format: SBOMFormat
38
+ content: str
39
+ component_count: int
40
+ generated_at: datetime = field(default_factory=datetime.utcnow)
security_use/scanner.py CHANGED
@@ -42,7 +42,7 @@ def scan_iac(
42
42
  Args:
43
43
  path: Path to scan (file or directory). Defaults to current directory.
44
44
  file_content: Direct content to scan (alternative to path).
45
- file_type: Type of IaC file when using file_content.
45
+ file_type: Type of IaC file when using file_content (e.g., "terraform", "cloudformation").
46
46
 
47
47
  Returns:
48
48
  ScanResult containing any IaC findings.
@@ -52,7 +52,20 @@ def scan_iac(
52
52
  scanner = IaCScanner()
53
53
 
54
54
  if file_content is not None:
55
- return scanner.scan_content(file_content, file_type or "terraform")
55
+ # Map file_type to a synthetic file path with correct extension
56
+ # so the parser can be selected correctly
57
+ file_type_to_extension = {
58
+ "terraform": "inline.tf",
59
+ "tf": "inline.tf",
60
+ "cloudformation": "inline.yaml",
61
+ "cfn": "inline.yaml",
62
+ "yaml": "inline.yaml",
63
+ "yml": "inline.yml",
64
+ "json": "inline.json",
65
+ }
66
+ file_type_lower = (file_type or "terraform").lower()
67
+ synthetic_path = file_type_to_extension.get(file_type_lower, f"inline.{file_type_lower}")
68
+ return scanner.scan_content(file_content, synthetic_path)
56
69
 
57
70
  scan_path = Path(path) if path else Path.cwd()
58
71
  return scanner.scan_path(scan_path)
@@ -0,0 +1,125 @@
1
+ """Security sensor for runtime attack detection and alerting.
2
+
3
+ This module provides middleware for FastAPI and Flask applications that
4
+ detects malicious patterns in HTTP requests and sends alerts to the
5
+ SecurityUse dashboard or custom webhooks.
6
+
7
+ Example usage with dashboard (recommended):
8
+ from fastapi import FastAPI
9
+ from security_use.sensor import SecurityMiddleware
10
+
11
+ app = FastAPI()
12
+ app.add_middleware(
13
+ SecurityMiddleware,
14
+ api_key="su_...", # Or set SECURITY_USE_API_KEY env var
15
+ block_on_detection=True,
16
+ )
17
+
18
+ Example with auto-detection of vulnerable endpoints:
19
+ app.add_middleware(
20
+ SecurityMiddleware,
21
+ auto_detect_vulnerable=True,
22
+ project_path="./",
23
+ )
24
+
25
+ Example with selective monitoring:
26
+ app.add_middleware(
27
+ SecurityMiddleware,
28
+ watch_paths=["/api/users", "/api/search", "/admin/*"],
29
+ )
30
+
31
+ Example usage with Flask:
32
+ from flask import Flask
33
+ from security_use.sensor import FlaskSecurityMiddleware
34
+
35
+ app = Flask(__name__)
36
+ app.wsgi_app = FlaskSecurityMiddleware(
37
+ app.wsgi_app,
38
+ api_key="su_...",
39
+ )
40
+
41
+ Legacy usage with webhook:
42
+ app.add_middleware(
43
+ SecurityMiddleware,
44
+ webhook_url="https://your-webhook.com/alerts",
45
+ )
46
+
47
+ Programmatic usage:
48
+ from security_use.sensor import AttackDetector, DashboardAlerter, RequestData
49
+
50
+ detector = AttackDetector()
51
+ alerter = DashboardAlerter(api_key="su_...")
52
+
53
+ request = RequestData(
54
+ method="POST",
55
+ path="/api/users",
56
+ body="username=admin' OR 1=1--",
57
+ source_ip="192.168.1.100",
58
+ )
59
+
60
+ events = detector.analyze_request(request)
61
+ for event in events:
62
+ await alerter.send_alert(event)
63
+
64
+ Endpoint analysis:
65
+ from security_use.sensor import VulnerableEndpointDetector
66
+
67
+ detector = VulnerableEndpointDetector()
68
+ result = detector.analyze("./my-project")
69
+
70
+ # Get paths that use vulnerable packages
71
+ for endpoint in result.vulnerable_endpoints:
72
+ print(f"{endpoint.path} - risk: {endpoint.risk_score}")
73
+ """
74
+
75
+ from .alert_queue import AlertQueue, get_alert_queue
76
+ from .config import SensorConfig, create_config
77
+ from .dashboard_alerter import DashboardAlerter
78
+ from .detector import AttackDetector, RateLimiter
79
+ from .endpoint_analyzer import (
80
+ AnalysisResult,
81
+ EndpointInfo,
82
+ VulnerableEndpointDetector,
83
+ detect_vulnerable_endpoints,
84
+ )
85
+ from .middleware import FlaskSecurityMiddleware, SecurityMiddleware
86
+ from .models import (
87
+ ActionTaken,
88
+ AlertPayload,
89
+ AlertResponse,
90
+ AttackType,
91
+ MatchedPattern,
92
+ RequestData,
93
+ SecurityEvent,
94
+ )
95
+ from .webhook import WebhookAlerter
96
+
97
+ __all__ = [
98
+ # Middleware
99
+ "SecurityMiddleware",
100
+ "FlaskSecurityMiddleware",
101
+ # Detection
102
+ "AttackDetector",
103
+ "RateLimiter",
104
+ # Alerting
105
+ "AlertQueue",
106
+ "get_alert_queue",
107
+ "DashboardAlerter",
108
+ "WebhookAlerter",
109
+ # Endpoint Analysis
110
+ "VulnerableEndpointDetector",
111
+ "detect_vulnerable_endpoints",
112
+ "EndpointInfo",
113
+ "AnalysisResult",
114
+ # Configuration
115
+ "SensorConfig",
116
+ "create_config",
117
+ # Models
118
+ "SecurityEvent",
119
+ "RequestData",
120
+ "MatchedPattern",
121
+ "AlertPayload",
122
+ "AlertResponse",
123
+ "AttackType",
124
+ "ActionTaken",
125
+ ]
@@ -0,0 +1,207 @@
1
+ """Background alert queue for non-blocking alert delivery."""
2
+
3
+ import atexit
4
+ import logging
5
+ import queue
6
+ import threading
7
+ from dataclasses import dataclass
8
+ from typing import TYPE_CHECKING, Optional, Protocol
9
+
10
+ if TYPE_CHECKING:
11
+ from .models import ActionTaken, SecurityEvent
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class Alerter(Protocol):
17
+ """Protocol for alerters."""
18
+
19
+ def send_alert_sync(self, event: "SecurityEvent", action: "ActionTaken") -> bool:
20
+ ...
21
+
22
+
23
+ @dataclass
24
+ class AlertItem:
25
+ """An alert to be sent."""
26
+
27
+ event: "SecurityEvent"
28
+ action: "ActionTaken"
29
+ alerter: Alerter
30
+
31
+
32
+ class AlertQueue:
33
+ """Background queue for sending alerts without blocking requests.
34
+
35
+ Usage:
36
+ queue = AlertQueue(max_size=1000)
37
+ queue.start()
38
+
39
+ # In request handler:
40
+ queue.enqueue(event, action, alerter)
41
+
42
+ # On shutdown:
43
+ queue.stop()
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ max_size: int = 1000,
49
+ num_workers: int = 2,
50
+ drain_timeout: float = 5.0,
51
+ ):
52
+ """Initialize the alert queue.
53
+
54
+ Args:
55
+ max_size: Maximum queue size. Alerts dropped if full.
56
+ num_workers: Number of worker threads.
57
+ drain_timeout: Seconds to wait for queue drain on shutdown.
58
+ """
59
+ self.max_size = max_size
60
+ self.num_workers = num_workers
61
+ self.drain_timeout = drain_timeout
62
+
63
+ self._queue: queue.Queue[Optional[AlertItem]] = queue.Queue(maxsize=max_size)
64
+ self._workers: list[threading.Thread] = []
65
+ self._running = False
66
+ self._started = False
67
+ self._lock = threading.Lock()
68
+
69
+ # Stats
70
+ self.alerts_sent = 0
71
+ self.alerts_failed = 0
72
+ self.alerts_dropped = 0
73
+
74
+ def start(self) -> None:
75
+ """Start the worker threads."""
76
+ with self._lock:
77
+ if self._started:
78
+ return
79
+
80
+ self._running = True
81
+ self._started = True
82
+
83
+ for i in range(self.num_workers):
84
+ worker = threading.Thread(
85
+ target=self._worker_loop,
86
+ name=f"security-use-alert-worker-{i}",
87
+ daemon=True,
88
+ )
89
+ worker.start()
90
+ self._workers.append(worker)
91
+
92
+ # Register shutdown handler
93
+ atexit.register(self.stop)
94
+
95
+ logger.debug(f"Alert queue started with {self.num_workers} workers")
96
+
97
+ def stop(self, timeout: Optional[float] = None) -> None:
98
+ """Stop the worker threads and drain the queue.
99
+
100
+ Args:
101
+ timeout: Max seconds to wait. Uses drain_timeout if None.
102
+ """
103
+ with self._lock:
104
+ if not self._running:
105
+ return
106
+
107
+ self._running = False
108
+ timeout = timeout or self.drain_timeout
109
+
110
+ # Send stop signals
111
+ for _ in self._workers:
112
+ try:
113
+ self._queue.put_nowait(None)
114
+ except queue.Full:
115
+ pass
116
+
117
+ # Wait for workers (outside lock to avoid deadlock)
118
+ for worker in self._workers:
119
+ worker.join(timeout=timeout / max(1, len(self._workers)))
120
+
121
+ with self._lock:
122
+ self._workers.clear()
123
+
124
+ logger.debug(
125
+ f"Alert queue stopped. Sent: {self.alerts_sent}, "
126
+ f"Failed: {self.alerts_failed}, Dropped: {self.alerts_dropped}"
127
+ )
128
+
129
+ def enqueue(
130
+ self,
131
+ event: "SecurityEvent",
132
+ action: "ActionTaken",
133
+ alerter: Alerter,
134
+ ) -> bool:
135
+ """Add an alert to the queue.
136
+
137
+ Returns:
138
+ True if queued, False if dropped (queue full).
139
+ """
140
+ if not self._running:
141
+ self.start() # Auto-start on first use
142
+
143
+ item = AlertItem(event=event, action=action, alerter=alerter)
144
+
145
+ try:
146
+ self._queue.put_nowait(item)
147
+ return True
148
+ except queue.Full:
149
+ self.alerts_dropped += 1
150
+ logger.warning(
151
+ f"Alert queue full, dropping alert: {event.event_type}"
152
+ )
153
+ return False
154
+
155
+ def _worker_loop(self) -> None:
156
+ """Worker thread main loop."""
157
+ while self._running or not self._queue.empty():
158
+ try:
159
+ item = self._queue.get(timeout=0.5)
160
+
161
+ if item is None: # Stop signal
162
+ self._queue.task_done()
163
+ break
164
+
165
+ try:
166
+ success = item.alerter.send_alert_sync(item.event, item.action)
167
+ if success:
168
+ self.alerts_sent += 1
169
+ else:
170
+ self.alerts_failed += 1
171
+ except Exception as e:
172
+ self.alerts_failed += 1
173
+ logger.error(f"Alert worker error: {e}")
174
+ finally:
175
+ self._queue.task_done()
176
+
177
+ except queue.Empty:
178
+ continue
179
+
180
+ @property
181
+ def pending_count(self) -> int:
182
+ """Number of alerts waiting to be sent."""
183
+ return self._queue.qsize()
184
+
185
+ @property
186
+ def stats(self) -> dict:
187
+ """Get queue statistics."""
188
+ return {
189
+ "pending": self.pending_count,
190
+ "sent": self.alerts_sent,
191
+ "failed": self.alerts_failed,
192
+ "dropped": self.alerts_dropped,
193
+ }
194
+
195
+
196
+ # Global singleton for easy access
197
+ _default_queue: Optional[AlertQueue] = None
198
+ _queue_lock = threading.Lock()
199
+
200
+
201
+ def get_alert_queue() -> AlertQueue:
202
+ """Get the default alert queue singleton."""
203
+ global _default_queue
204
+ with _queue_lock:
205
+ if _default_queue is None:
206
+ _default_queue = AlertQueue()
207
+ return _default_queue
@@ -0,0 +1,217 @@
1
+ """Configuration for the security sensor."""
2
+
3
+ import os
4
+ from dataclasses import dataclass, field
5
+ from typing import Optional
6
+
7
+
8
+ @dataclass
9
+ class SensorConfig:
10
+ """Configuration for the security sensor middleware."""
11
+
12
+ # Alerting mode: "dashboard", "webhook", or "both"
13
+ alert_mode: str = "dashboard"
14
+
15
+ # Dashboard settings (preferred)
16
+ api_key: Optional[str] = None
17
+ dashboard_url: Optional[str] = None
18
+
19
+ # Webhook settings (legacy/fallback)
20
+ webhook_url: Optional[str] = None
21
+ webhook_headers: dict[str, str] = field(default_factory=dict)
22
+ webhook_retry_count: int = 3
23
+ webhook_timeout: float = 10.0
24
+
25
+ # Detection settings
26
+ enabled_detectors: list[str] = field(
27
+ default_factory=lambda: [
28
+ "sqli",
29
+ "xss",
30
+ "path_traversal",
31
+ "command_injection",
32
+ "rate_limit",
33
+ "suspicious_headers",
34
+ ]
35
+ )
36
+
37
+ # Rate limiting
38
+ rate_limit_threshold: int = 100 # requests per window per IP
39
+ rate_limit_window: int = 60 # seconds
40
+ rate_limit_cleanup_interval: int = 300 # seconds between cleanup runs
41
+ rate_limit_max_ips: int = 100000 # max tracked IPs to prevent memory leak
42
+
43
+ # Alert queue settings (for non-blocking alert delivery in Flask)
44
+ alert_queue_size: int = 1000 # max queued alerts
45
+ alert_queue_workers: int = 2 # number of worker threads
46
+ alert_queue_drain_timeout: float = 5.0 # seconds to wait on shutdown
47
+
48
+ # Body handling
49
+ max_body_size: int = 1024 * 1024 # 1MB max body for analysis
50
+
51
+ # Behavior
52
+ block_on_detection: bool = True # Return 403 on attack detection (default True now)
53
+ excluded_paths: list[str] = field(default_factory=list) # Paths to skip
54
+
55
+ # Selective monitoring
56
+ watch_paths: Optional[list[str]] = None # Only monitor these paths (None = all)
57
+ auto_detect_vulnerable: bool = False # Auto-detect vulnerable endpoints
58
+ project_path: Optional[str] = None # Project path for auto-detection
59
+
60
+ # Logging
61
+ log_requests: bool = False # Log all requests (not just attacks)
62
+ log_level: str = "WARNING"
63
+
64
+ @classmethod
65
+ def from_dict(cls, data: dict) -> "SensorConfig":
66
+ """Create config from dictionary."""
67
+ return cls(
68
+ alert_mode=data.get("alert_mode", "dashboard"),
69
+ api_key=data.get("api_key") or os.environ.get("SECURITY_USE_API_KEY"),
70
+ dashboard_url=data.get("dashboard_url") or os.environ.get("SECURITY_USE_DASHBOARD_URL"),
71
+ webhook_url=data.get("webhook_url"),
72
+ webhook_headers=data.get("webhook_headers", {}),
73
+ webhook_retry_count=data.get("webhook_retry_count", 3),
74
+ webhook_timeout=data.get("webhook_timeout", 10.0),
75
+ enabled_detectors=data.get(
76
+ "enabled_detectors",
77
+ [
78
+ "sqli",
79
+ "xss",
80
+ "path_traversal",
81
+ "command_injection",
82
+ "rate_limit",
83
+ "suspicious_headers",
84
+ ],
85
+ ),
86
+ rate_limit_threshold=data.get("rate_limit_threshold", 100),
87
+ rate_limit_window=data.get("rate_limit_window", 60),
88
+ rate_limit_cleanup_interval=data.get("rate_limit_cleanup_interval", 300),
89
+ rate_limit_max_ips=data.get("rate_limit_max_ips", 100000),
90
+ alert_queue_size=data.get("alert_queue_size", 1000),
91
+ alert_queue_workers=data.get("alert_queue_workers", 2),
92
+ alert_queue_drain_timeout=data.get("alert_queue_drain_timeout", 5.0),
93
+ max_body_size=data.get("max_body_size", 1024 * 1024),
94
+ block_on_detection=data.get("block_on_detection", True),
95
+ excluded_paths=data.get("excluded_paths", []),
96
+ watch_paths=data.get("watch_paths"),
97
+ auto_detect_vulnerable=data.get("auto_detect_vulnerable", False),
98
+ project_path=data.get("project_path"),
99
+ log_requests=data.get("log_requests", False),
100
+ log_level=data.get("log_level", "WARNING"),
101
+ )
102
+
103
+ def to_dict(self) -> dict:
104
+ """Convert config to dictionary."""
105
+ return {
106
+ "alert_mode": self.alert_mode,
107
+ "api_key": self.api_key,
108
+ "dashboard_url": self.dashboard_url,
109
+ "webhook_url": self.webhook_url,
110
+ "webhook_headers": self.webhook_headers,
111
+ "webhook_retry_count": self.webhook_retry_count,
112
+ "webhook_timeout": self.webhook_timeout,
113
+ "enabled_detectors": self.enabled_detectors,
114
+ "rate_limit_threshold": self.rate_limit_threshold,
115
+ "rate_limit_window": self.rate_limit_window,
116
+ "rate_limit_cleanup_interval": self.rate_limit_cleanup_interval,
117
+ "rate_limit_max_ips": self.rate_limit_max_ips,
118
+ "alert_queue_size": self.alert_queue_size,
119
+ "alert_queue_workers": self.alert_queue_workers,
120
+ "alert_queue_drain_timeout": self.alert_queue_drain_timeout,
121
+ "max_body_size": self.max_body_size,
122
+ "block_on_detection": self.block_on_detection,
123
+ "excluded_paths": self.excluded_paths,
124
+ "watch_paths": self.watch_paths,
125
+ "auto_detect_vulnerable": self.auto_detect_vulnerable,
126
+ "project_path": self.project_path,
127
+ "log_requests": self.log_requests,
128
+ "log_level": self.log_level,
129
+ }
130
+
131
+ def is_path_excluded(self, path: str) -> bool:
132
+ """Check if a path should be excluded from monitoring."""
133
+ for excluded in self.excluded_paths:
134
+ if excluded.endswith("*"):
135
+ if path.startswith(excluded[:-1]):
136
+ return True
137
+ elif path == excluded:
138
+ return True
139
+ return False
140
+
141
+ def should_monitor_path(self, path: str) -> bool:
142
+ """Check if a path should be monitored.
143
+
144
+ Returns True if:
145
+ - Path is not in excluded_paths AND
146
+ - Either watch_paths is None (monitor all) OR path is in watch_paths
147
+ """
148
+ if self.is_path_excluded(path):
149
+ return False
150
+
151
+ if self.watch_paths is None:
152
+ return True
153
+
154
+ # Check watch_paths with wildcard support
155
+ for watch_path in self.watch_paths:
156
+ if watch_path.endswith("*"):
157
+ if path.startswith(watch_path[:-1]):
158
+ return True
159
+ elif path == watch_path or path.startswith(watch_path + "/"):
160
+ return True
161
+
162
+ return False
163
+
164
+
165
+ def create_config(
166
+ api_key: Optional[str] = None,
167
+ webhook_url: Optional[str] = None,
168
+ block_on_detection: bool = True,
169
+ excluded_paths: Optional[list[str]] = None,
170
+ watch_paths: Optional[list[str]] = None,
171
+ auto_detect_vulnerable: bool = False,
172
+ project_path: Optional[str] = None,
173
+ enabled_detectors: Optional[list[str]] = None,
174
+ rate_limit_threshold: int = 100,
175
+ **kwargs,
176
+ ) -> SensorConfig:
177
+ """Convenience function to create a sensor configuration.
178
+
179
+ Args:
180
+ api_key: SecurityUse API key for dashboard alerting.
181
+ webhook_url: URL to send alerts to (legacy, use api_key instead).
182
+ block_on_detection: Whether to return 403 on attack detection.
183
+ excluded_paths: List of paths to exclude from monitoring.
184
+ watch_paths: List of paths to monitor (None = all paths).
185
+ auto_detect_vulnerable: Auto-detect vulnerable endpoints to monitor.
186
+ project_path: Project path for auto-detection.
187
+ enabled_detectors: List of detector types to enable.
188
+ rate_limit_threshold: Requests per minute per IP before rate limiting.
189
+ **kwargs: Additional configuration options.
190
+
191
+ Returns:
192
+ SensorConfig instance.
193
+ """
194
+ # Determine alert mode
195
+ alert_mode = "dashboard"
196
+ if api_key or os.environ.get("SECURITY_USE_API_KEY"):
197
+ alert_mode = "dashboard"
198
+ elif webhook_url:
199
+ alert_mode = "webhook"
200
+
201
+ config_dict = {
202
+ "alert_mode": alert_mode,
203
+ "api_key": api_key,
204
+ "webhook_url": webhook_url,
205
+ "block_on_detection": block_on_detection,
206
+ "excluded_paths": excluded_paths or [],
207
+ "watch_paths": watch_paths,
208
+ "auto_detect_vulnerable": auto_detect_vulnerable,
209
+ "project_path": project_path,
210
+ "rate_limit_threshold": rate_limit_threshold,
211
+ **kwargs,
212
+ }
213
+
214
+ if enabled_detectors is not None:
215
+ config_dict["enabled_detectors"] = enabled_detectors
216
+
217
+ return SensorConfig.from_dict(config_dict)