loguard 2.0.0__tar.gz

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.
@@ -0,0 +1,8 @@
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
4
+ # Editor-based HTTP Client requests
5
+ /httpRequests/
6
+ # Datasource local storage ignored files
7
+ /dataSources/
8
+ /dataSources.local.xml
@@ -0,0 +1,7 @@
1
+ <component name="InspectionProjectProfileManager">
2
+ <profile version="1.0">
3
+ <option name="myName" value="Project Default" />
4
+ <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
5
+ <inspection_tool class="PyPackageRequirementsInspection" enabled="false" level="WARNING" enabled_by_default="false" />
6
+ </profile>
7
+ </component>
@@ -0,0 +1,6 @@
1
+ <component name="InspectionProjectProfileManager">
2
+ <settings>
3
+ <option name="USE_PROJECT_PROFILE" value="false" />
4
+ <version value="1.0" />
5
+ </settings>
6
+ </component>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="PYTHON_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$">
5
+ <excludeFolder url="file://$MODULE_DIR$/.venv" />
6
+ </content>
7
+ <orderEntry type="jdk" jdkName="Python 3.14 (loguard_sdk)" jdkType="Python SDK" />
8
+ <orderEntry type="sourceFolder" forTests="false" />
9
+ </component>
10
+ </module>
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="Black">
4
+ <option name="sdkName" value="Python 3.14 (loguard_sdk)" />
5
+ </component>
6
+ <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.14 (loguard_sdk)" project-jdk-type="Python SDK" />
7
+ </project>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/loguard_sdk.iml" filepath="$PROJECT_DIR$/.idea/loguard_sdk.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
@@ -0,0 +1,62 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="AutoImportSettings">
4
+ <option name="autoReloadType" value="SELECTIVE" />
5
+ </component>
6
+ <component name="ChangeListManager">
7
+ <list default="true" id="a0f97438-b3be-45c8-83bf-3b6807b3e6cd" name="Changes" comment="" />
8
+ <option name="SHOW_DIALOG" value="false" />
9
+ <option name="HIGHLIGHT_CONFLICTS" value="true" />
10
+ <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
11
+ <option name="LAST_RESOLUTION" value="IGNORE" />
12
+ </component>
13
+ <component name="ProjectColorInfo"><![CDATA[{
14
+ "associatedIndex": 2
15
+ }]]></component>
16
+ <component name="ProjectId" id="3B42YJpLQPccVEcva7CCFx0hwwH" />
17
+ <component name="ProjectViewState">
18
+ <option name="hideEmptyMiddlePackages" value="true" />
19
+ <option name="showLibraryContents" value="true" />
20
+ </component>
21
+ <component name="PropertiesComponent"><![CDATA[{
22
+ "keyToString": {
23
+ "ModuleVcsDetector.initialDetectionPerformed": "true",
24
+ "RunOnceActivity.ShowReadmeOnStart": "true",
25
+ "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
26
+ "last_opened_file_path": "/Users/admin/PythonProjects/loguard_sdk",
27
+ "node.js.detected.package.eslint": "true",
28
+ "node.js.detected.package.tslint": "true",
29
+ "node.js.selected.package.eslint": "(autodetect)",
30
+ "node.js.selected.package.tslint": "(autodetect)",
31
+ "nodejs_package_manager_path": "npm",
32
+ "vue.rearranger.settings.migration": "true"
33
+ }
34
+ }]]></component>
35
+ <component name="RecentsManager">
36
+ <key name="CopyFile.RECENT_KEYS">
37
+ <recent name="$PROJECT_DIR$" />
38
+ </key>
39
+ </component>
40
+ <component name="SharedIndexes">
41
+ <attachedChunks>
42
+ <set>
43
+ <option value="bundled-js-predefined-d6986cc7102b-a71380e98a7c-JavaScript-PY-252.28539.27" />
44
+ <option value="bundled-python-sdk-07523f343e4f-968fe969f28a-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-252.28539.27" />
45
+ </set>
46
+ </attachedChunks>
47
+ </component>
48
+ <component name="TaskManager">
49
+ <task active="true" id="Default" summary="Default task">
50
+ <changelist id="a0f97438-b3be-45c8-83bf-3b6807b3e6cd" name="Changes" comment="" />
51
+ <created>1773734067381</created>
52
+ <option name="number" value="Default" />
53
+ <option name="presentableId" value="Default" />
54
+ <updated>1773734067381</updated>
55
+ <workItem from="1773734068457" duration="310000" />
56
+ </task>
57
+ <servers />
58
+ </component>
59
+ <component name="TypeScriptGeneratedFilesManager">
60
+ <option name="version" value="3" />
61
+ </component>
62
+ </project>
loguard-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: loguard
3
+ Version: 2.0.0
4
+ Summary: LoGuard Python SDK — security event monitoring, alert rules, and blacklisting
5
+ Project-URL: Homepage, https://logguard.io
6
+ Project-URL: Documentation, https://docs.logguard.io/sdk/python
7
+ Project-URL: Repository, https://github.com/logguard/logguard-python
8
+ License: MIT
9
+ Keywords: alerts,blacklist,monitoring,sdk,security
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Security
19
+ Requires-Python: >=3.9
20
+ Requires-Dist: httpx>=0.25.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: httpx; extra == 'dev'
23
+ Requires-Dist: pytest-asyncio; extra == 'dev'
24
+ Requires-Dist: pytest>=7; extra == 'dev'
25
+ Requires-Dist: respx; extra == 'dev'
26
+ Provides-Extra: django
27
+ Requires-Dist: django>=3.2; extra == 'django'
28
+ Provides-Extra: fastapi
29
+ Requires-Dist: starlette>=0.27.0; extra == 'fastapi'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # loguard
@@ -0,0 +1 @@
1
+ # loguard
@@ -0,0 +1,84 @@
1
+ """
2
+ Logguard Python SDK
3
+ ~~~~~~~~~~~~~~~~~~~
4
+
5
+ Official Python SDK for Logguard — lightweight security monitoring
6
+ with user-defined alert rules and IP/user blacklisting.
7
+
8
+ Quick start:
9
+ from logguard import monitor
10
+ from logguard.models import AlertRule, AlertCondition, BlacklistEntry
11
+
12
+ monitor.init(api_key="lg_live_xxx", env="production")
13
+
14
+ # Track events
15
+ monitor.event(type="login_failed", ip="1.2.3.4", path="/login", status_code=401)
16
+
17
+ # Fire-and-forget (non-blocking, for production traffic)
18
+ monitor.event_fire_and_forget(type="http_error", ip="1.2.3.4", path="/api", status_code=500)
19
+
20
+ # Create an alert rule
21
+ rule = monitor.alerts.create(AlertRule(
22
+ name="Brute force",
23
+ conditions=[
24
+ AlertCondition(field="type", op="eq", value="login_failed"),
25
+ AlertCondition(field="rate_per_minute", op="gt", value=10),
26
+ ],
27
+ severity="high",
28
+ actions=["notify", "block"],
29
+ ))
30
+
31
+ # Blacklist an IP
32
+ monitor.blacklist.block_ip("185.220.101.1", reason="Known scanner")
33
+
34
+ # Check if an IP is blacklisted
35
+ result = monitor.blacklist.check(type="ip", value="185.220.101.1")
36
+
37
+ Docs: https://docs.logguard.io/sdk/python
38
+ """
39
+
40
+ from .monitor import monitor
41
+ from .models import (
42
+ AlertCondition,
43
+ AlertRule,
44
+ AlertOut,
45
+ BlacklistEntry,
46
+ Event,
47
+ IngestResult,
48
+ UsageInfo,
49
+ )
50
+ from .exceptions import (
51
+ LogguardError,
52
+ LogguardNotInitializedError,
53
+ LogguardAuthError,
54
+ LogguardQuotaError,
55
+ LogguardConnectionError,
56
+ LogguardValidationError,
57
+ LogguardNotFoundError,
58
+ LogguardConflictError,
59
+ )
60
+
61
+ __version__ = "2.0.0"
62
+
63
+ __all__ = [
64
+ # Core
65
+ "monitor",
66
+ "__version__",
67
+ # Models
68
+ "Event",
69
+ "IngestResult",
70
+ "AlertOut",
71
+ "UsageInfo",
72
+ "AlertRule",
73
+ "AlertCondition",
74
+ "BlacklistEntry",
75
+ # Exceptions
76
+ "LogguardError",
77
+ "LogguardNotInitializedError",
78
+ "LogguardAuthError",
79
+ "LogguardQuotaError",
80
+ "LogguardConnectionError",
81
+ "LogguardValidationError",
82
+ "LogguardNotFoundError",
83
+ "LogguardConflictError",
84
+ ]
@@ -0,0 +1,214 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import time
6
+ from typing import Any, Dict, List, Optional, Tuple
7
+
8
+ import httpx
9
+
10
+ from .exceptions import (
11
+ LogguardAuthError,
12
+ LogguardConnectionError,
13
+ LogguardConflictError,
14
+ LogguardNotFoundError,
15
+ LogguardQuotaError,
16
+ LogguardValidationError,
17
+ )
18
+ from .models import AlertOut, IngestResult
19
+
20
+ log = logging.getLogger("logguard")
21
+
22
+ _RETRY_STATUSES = {500, 502, 503, 504}
23
+
24
+
25
+ def _safe_json(resp: httpx.Response) -> Tuple[Any, str]:
26
+ """Returns (parsed_json_or_None, text_snippet). Never raises."""
27
+ text = ""
28
+ try:
29
+ text = resp.text or ""
30
+ except Exception:
31
+ pass
32
+ snippet = text[:400]
33
+ try:
34
+ j = resp.json()
35
+ return j, snippet
36
+ except Exception:
37
+ return None, snippet
38
+
39
+
40
+ def _raise_for_status(resp: httpx.Response) -> Any:
41
+ """
42
+ Raises appropriate LogguardError for non-2xx responses.
43
+ Returns parsed JSON body on success.
44
+ """
45
+ data, snippet = _safe_json(resp)
46
+ d = data if isinstance(data, dict) else {}
47
+
48
+ if resp.status_code == 401:
49
+ raise LogguardAuthError("Invalid API key")
50
+
51
+ if resp.status_code == 402:
52
+ raise LogguardAuthError("Subscription expired — renew at logguard.io")
53
+
54
+ if resp.status_code == 403:
55
+ detail = d.get("detail", snippet)
56
+ if detail == "subscription_expired":
57
+ raise LogguardAuthError("Subscription expired — renew at logguard.io")
58
+ raise LogguardAuthError(f"Access denied: {detail}")
59
+
60
+ if resp.status_code == 404:
61
+ detail = d.get("detail", snippet)
62
+ raise LogguardNotFoundError(f"Not found: {detail}")
63
+
64
+ if resp.status_code == 409:
65
+ detail = d.get("detail", snippet)
66
+ raise LogguardConflictError(f"Conflict: {detail}")
67
+
68
+ if resp.status_code == 422:
69
+ raise LogguardValidationError(f"Validation error: {snippet}")
70
+
71
+ if resp.status_code == 429:
72
+ detail = d.get("detail", {})
73
+ if isinstance(detail, dict) and detail.get("err") == "usage_limit_exceeded":
74
+ raise LogguardQuotaError(
75
+ f"Monthly quota exceeded: {detail.get('used')}/{detail.get('limit')} "
76
+ f"events on plan '{detail.get('plan')}'. Upgrade at logguard.io"
77
+ )
78
+ raise LogguardConnectionError(f"Rate limited: {snippet}")
79
+
80
+ if resp.status_code >= 500:
81
+ raise LogguardConnectionError(f"Server error ({resp.status_code}): {snippet}")
82
+
83
+ return data
84
+
85
+
86
+ def parse_ingest_response(data: Any) -> IngestResult:
87
+ d = data if isinstance(data, dict) else {}
88
+ alerts = [AlertOut(**a) for a in (d.get("alerts") or [])]
89
+ return IngestResult(
90
+ ok=bool(d.get("ok", False)),
91
+ inserted=int(d.get("inserted", 0)),
92
+ dropped=int(d.get("dropped", 0)),
93
+ alerts_fired=int(d.get("alerts_fired", 0)),
94
+ alerts=alerts,
95
+ plan=str(d.get("plan", "")),
96
+ usage=dict(d.get("usage", {}) or {}),
97
+ )
98
+
99
+
100
+ def send_sync(
101
+ url: str,
102
+ headers: dict,
103
+ payload: dict,
104
+ timeout: float,
105
+ retries: int,
106
+ method: str = "POST",
107
+ ) -> Any:
108
+ last_exc: Exception = LogguardConnectionError("Unknown error")
109
+ for attempt in range(1, retries + 1):
110
+ try:
111
+ with httpx.Client(timeout=timeout) as client:
112
+ fn = getattr(client, method.lower())
113
+ resp = fn(url, json=payload, headers=headers)
114
+
115
+ if resp.status_code not in _RETRY_STATUSES:
116
+ return _raise_for_status(resp)
117
+
118
+ last_exc = LogguardConnectionError(f"Server error {resp.status_code}")
119
+ log.warning("Attempt %d/%d failed status=%d", attempt, retries, resp.status_code)
120
+
121
+ except (httpx.RequestError, httpx.TimeoutException) as e:
122
+ last_exc = LogguardConnectionError(f"Connection error: {e!r}")
123
+ log.warning("Attempt %d/%d connection error: %r", attempt, retries, e)
124
+
125
+ if attempt < retries:
126
+ time.sleep(0.4 * attempt)
127
+
128
+ raise last_exc
129
+
130
+
131
+ def send_sync_no_body(
132
+ url: str,
133
+ headers: dict,
134
+ timeout: float,
135
+ retries: int,
136
+ method: str = "GET",
137
+ ) -> Any:
138
+ """For GET/DELETE requests without a body."""
139
+ last_exc: Exception = LogguardConnectionError("Unknown error")
140
+ for attempt in range(1, retries + 1):
141
+ try:
142
+ with httpx.Client(timeout=timeout) as client:
143
+ fn = getattr(client, method.lower())
144
+ resp = fn(url, headers=headers)
145
+
146
+ if resp.status_code not in _RETRY_STATUSES:
147
+ return _raise_for_status(resp)
148
+
149
+ last_exc = LogguardConnectionError(f"Server error {resp.status_code}")
150
+
151
+ except (httpx.RequestError, httpx.TimeoutException) as e:
152
+ last_exc = LogguardConnectionError(f"Connection error: {e!r}")
153
+
154
+ if attempt < retries:
155
+ time.sleep(0.4 * attempt)
156
+
157
+ raise last_exc
158
+
159
+
160
+ async def send_async(
161
+ url: str,
162
+ headers: dict,
163
+ payload: dict,
164
+ timeout: float,
165
+ retries: int,
166
+ method: str = "POST",
167
+ ) -> Any:
168
+ last_exc: Exception = LogguardConnectionError("Unknown error")
169
+ for attempt in range(1, retries + 1):
170
+ try:
171
+ async with httpx.AsyncClient(timeout=timeout) as client:
172
+ fn = getattr(client, method.lower())
173
+ resp = await fn(url, json=payload, headers=headers)
174
+
175
+ if resp.status_code not in _RETRY_STATUSES:
176
+ return _raise_for_status(resp)
177
+
178
+ last_exc = LogguardConnectionError(f"Server error {resp.status_code}")
179
+
180
+ except (httpx.RequestError, httpx.TimeoutException) as e:
181
+ last_exc = LogguardConnectionError(f"Connection error: {e!r}")
182
+
183
+ if attempt < retries:
184
+ await asyncio.sleep(0.4 * attempt)
185
+
186
+ raise last_exc
187
+
188
+
189
+ async def send_async_no_body(
190
+ url: str,
191
+ headers: dict,
192
+ timeout: float,
193
+ retries: int,
194
+ method: str = "GET",
195
+ ) -> Any:
196
+ last_exc: Exception = LogguardConnectionError("Unknown error")
197
+ for attempt in range(1, retries + 1):
198
+ try:
199
+ async with httpx.AsyncClient(timeout=timeout) as client:
200
+ fn = getattr(client, method.lower())
201
+ resp = await fn(url, headers=headers)
202
+
203
+ if resp.status_code not in _RETRY_STATUSES:
204
+ return _raise_for_status(resp)
205
+
206
+ last_exc = LogguardConnectionError(f"Server error {resp.status_code}")
207
+
208
+ except (httpx.RequestError, httpx.TimeoutException) as e:
209
+ last_exc = LogguardConnectionError(f"Connection error: {e!r}")
210
+
211
+ if attempt < retries:
212
+ await asyncio.sleep(0.4 * attempt)
213
+
214
+ raise last_exc
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import threading
6
+ from typing import Callable, Optional, Set
7
+
8
+ from ..monitor import monitor as _monitor
9
+ from ..exceptions import LogguardError, LogguardNotInitializedError
10
+
11
+ log = logging.getLogger("logguard.django")
12
+
13
+
14
+ class LogguardMiddleware:
15
+ """
16
+ Django middleware — auto-tracks HTTP errors and optionally enforces
17
+ the blacklist on every incoming request.
18
+
19
+ Setup in settings.py:
20
+ MIDDLEWARE = [
21
+ "logguard.integrations.django.LogguardMiddleware",
22
+ ... # put first so blacklist check happens early
23
+ ]
24
+
25
+ And in your app startup (e.g. AppConfig.ready()):
26
+ from logguard import monitor
27
+ monitor.init(api_key="lg_live_xxx", env="production")
28
+
29
+ With blacklist enforcement:
30
+ LOGGUARD_ENFORCE_BLACKLIST = True # in settings.py
31
+ or pass enforce_blacklist=True to the middleware constructor (via MIDDLEWARE_CONFIG)
32
+
33
+ Blocked IPs receive: HTTP 403 JSON response.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ get_response: Callable,
39
+ *,
40
+ track_statuses: Optional[Set[int]] = None,
41
+ enforce_blacklist: bool = False,
42
+ ):
43
+ self.get_response = get_response
44
+ self._track_statuses = track_statuses or {400, 401, 403, 404, 429, 500, 502, 503}
45
+ self._enforce_blacklist = enforce_blacklist
46
+
47
+ def __call__(self, request):
48
+ from django.http import JsonResponse
49
+
50
+ ip = (
51
+ request.META.get("HTTP_X_FORWARDED_FOR", "").split(",")[0].strip()
52
+ or request.META.get("REMOTE_ADDR", "unknown")
53
+ )
54
+
55
+ # ── Blacklist check ────────────────────────────────────────────────
56
+ if self._enforce_blacklist:
57
+ try:
58
+ result = _monitor.blacklist.check(type="ip", value=ip)
59
+ if result.get("blacklisted") and result.get("action") == "block":
60
+ return JsonResponse(
61
+ {"detail": "Forbidden", "reason": result.get("reason", "blacklisted")},
62
+ status=403,
63
+ )
64
+ except LogguardNotInitializedError:
65
+ log.warning("LogguardMiddleware: monitor not initialized")
66
+ except LogguardError as e:
67
+ log.debug("LogguardMiddleware blacklist check failed: %r", e)
68
+
69
+ # ── Process request ────────────────────────────────────────────────
70
+ response = self.get_response(request)
71
+
72
+ # ── Track errors ───────────────────────────────────────────────────
73
+ if response.status_code in self._track_statuses:
74
+ user_id = None
75
+ if getattr(request, "user", None) and request.user.is_authenticated:
76
+ user_id = str(request.user.pk)
77
+
78
+ event_type = "login_failed" if response.status_code == 401 else "http_error"
79
+
80
+ threading.Thread(
81
+ target=self._fire,
82
+ args=(event_type, ip, request.path, response.status_code, user_id),
83
+ daemon=True,
84
+ ).start()
85
+
86
+ return response
87
+
88
+ def _fire(self, event_type, ip, path, status_code, user_id):
89
+ try:
90
+ _monitor.event(
91
+ type=event_type,
92
+ ip=ip,
93
+ path=path,
94
+ status_code=status_code,
95
+ user_id=user_id,
96
+ )
97
+ except LogguardNotInitializedError:
98
+ log.warning("LogguardMiddleware: monitor not initialized")
99
+ except LogguardError as e:
100
+ log.debug("LogguardMiddleware track failed: %r", e)
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class LogguardError(Exception):
5
+ """Base exception for all Logguard SDK errors."""
6
+
7
+
8
+ class LogguardNotInitializedError(LogguardError):
9
+ """Raised when monitor.event() is called before monitor.init()."""
10
+
11
+
12
+ class LogguardAuthError(LogguardError):
13
+ """Raised when API key is invalid, missing, or subscription expired."""
14
+
15
+
16
+ class LogguardQuotaError(LogguardError):
17
+ """Raised when monthly event quota is exceeded."""
18
+
19
+
20
+ class LogguardConnectionError(LogguardError):
21
+ """Raised when the Logguard API is unreachable or returns a server error."""
22
+
23
+
24
+ class LogguardValidationError(LogguardError):
25
+ """Raised when event data fails validation."""
26
+
27
+
28
+ class LogguardNotFoundError(LogguardError):
29
+ """Raised when a requested resource (alert rule, blacklist entry) is not found."""
30
+
31
+
32
+ class LogguardConflictError(LogguardError):
33
+ """Raised when creating a resource that already exists (e.g. duplicate blacklist entry)."""