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.
- loguard-2.0.0/.idea/.gitignore +8 -0
- loguard-2.0.0/.idea/inspectionProfiles/Project_Default.xml +7 -0
- loguard-2.0.0/.idea/inspectionProfiles/profiles_settings.xml +6 -0
- loguard-2.0.0/.idea/loguard_sdk.iml +10 -0
- loguard-2.0.0/.idea/misc.xml +7 -0
- loguard-2.0.0/.idea/modules.xml +8 -0
- loguard-2.0.0/.idea/workspace.xml +62 -0
- loguard-2.0.0/PKG-INFO +32 -0
- loguard-2.0.0/README.md +1 -0
- loguard-2.0.0/__init__.py +84 -0
- loguard-2.0.0/_transport.py +214 -0
- loguard-2.0.0/django.py +100 -0
- loguard-2.0.0/exceptions.py +33 -0
- loguard-2.0.0/fastapi.py +100 -0
- loguard-2.0.0/models.py +262 -0
- loguard-2.0.0/monitor.py +682 -0
- loguard-2.0.0/pyproject.toml +46 -0
- loguard-2.0.0/test_sdk.py +342 -0
|
@@ -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,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
|
loguard-2.0.0/README.md
ADDED
|
@@ -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
|
loguard-2.0.0/django.py
ADDED
|
@@ -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)."""
|