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.
- security_use/__init__.py +9 -1
- security_use/auth/__init__.py +16 -0
- security_use/auth/client.py +223 -0
- security_use/auth/config.py +177 -0
- security_use/auth/oauth.py +317 -0
- security_use/cli.py +699 -34
- security_use/compliance/__init__.py +10 -0
- security_use/compliance/mapper.py +275 -0
- security_use/compliance/models.py +50 -0
- security_use/dependency_scanner.py +76 -30
- security_use/fixers/iac_fixer.py +173 -95
- security_use/iac/rules/azure.py +246 -0
- security_use/iac/rules/gcp.py +255 -0
- security_use/iac/rules/kubernetes.py +429 -0
- security_use/iac/rules/registry.py +56 -0
- security_use/parsers/__init__.py +18 -0
- security_use/parsers/base.py +2 -0
- security_use/parsers/composer.py +101 -0
- security_use/parsers/conda.py +97 -0
- security_use/parsers/dotnet.py +89 -0
- security_use/parsers/gradle.py +90 -0
- security_use/parsers/maven.py +108 -0
- security_use/parsers/npm.py +196 -0
- security_use/parsers/yarn.py +108 -0
- security_use/reporter.py +29 -1
- security_use/sbom/__init__.py +10 -0
- security_use/sbom/generator.py +340 -0
- security_use/sbom/models.py +40 -0
- security_use/scanner.py +15 -2
- security_use/sensor/__init__.py +125 -0
- security_use/sensor/alert_queue.py +207 -0
- security_use/sensor/config.py +217 -0
- security_use/sensor/dashboard_alerter.py +246 -0
- security_use/sensor/detector.py +415 -0
- security_use/sensor/endpoint_analyzer.py +339 -0
- security_use/sensor/middleware.py +521 -0
- security_use/sensor/models.py +140 -0
- security_use/sensor/webhook.py +227 -0
- security_use-0.2.9.dist-info/METADATA +531 -0
- security_use-0.2.9.dist-info/RECORD +60 -0
- security_use-0.2.9.dist-info/licenses/LICENSE +21 -0
- security_use-0.1.1.dist-info/METADATA +0 -92
- security_use-0.1.1.dist-info/RECORD +0 -30
- {security_use-0.1.1.dist-info → security_use-0.2.9.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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)
|