trappsec 0.1.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,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: trappsec
3
+ Version: 0.1.0
4
+ Summary: deception as a developer tool
5
+ Author-email: Nikhil Salgaonkar <nikhil@ftfy.co>
6
+ Project-URL: Homepage, https://trappsec.dev
7
+ Project-URL: Repository, https://github.com/trappsec-dev/trappsec
8
+ Project-URL: Bug Tracker, https://github.com/trappsec-dev/trappsec/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.7
13
+ Description-Content-Type: text/markdown
14
+ Provides-Extra: webhooks
15
+ Requires-Dist: requests; extra == "webhooks"
16
+ Provides-Extra: otel
17
+ Requires-Dist: opentelemetry-api; extra == "otel"
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "trappsec"
3
+ version = "0.1.0"
4
+ authors = [
5
+ { name="Nikhil Salgaonkar", email="nikhil@ftfy.co" },
6
+ ]
7
+ description = "deception as a developer tool"
8
+ readme = "README.md"
9
+ requires-python = ">=3.7"
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Operating System :: OS Independent",
14
+ ]
15
+
16
+ [project.urls]
17
+ "Homepage" = "https://trappsec.dev"
18
+ "Repository" = "https://github.com/trappsec-dev/trappsec"
19
+ "Bug Tracker" = "https://github.com/trappsec-dev/trappsec/issues"
20
+
21
+ [project.optional-dependencies]
22
+ webhooks = ["requests"]
23
+ otel = ["opentelemetry-api"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ from .core import Sentry
2
+
3
+ __all__ = [
4
+ "Sentry",
5
+ ]
@@ -0,0 +1,78 @@
1
+ import typing
2
+ import copy
3
+
4
+ NO_DEFAULT = object()
5
+
6
+ class TrapBuilder:
7
+ def __init__(self, ts, path):
8
+ self.ts = ts
9
+ self.config = {
10
+ "path": path,
11
+ "methods": ["GET", "POST"],
12
+ "intent": None,
13
+ "response.authenticated": copy.deepcopy(self.ts.default_responses["authenticated"]),
14
+ "response.unauthenticated": copy.deepcopy(self.ts.default_responses["unauthenticated"]),
15
+ }
16
+
17
+ def methods(self, *args):
18
+ self.config["methods"] = list(args)
19
+ return self
20
+
21
+ def intent(self, intent: str):
22
+ self.config["intent"] = intent
23
+ return self
24
+
25
+ def _respond(self, key: str, status: int = None, body: typing.Union[dict, typing.Callable] = None, mime_type: str = None, template: str = None):
26
+ key = "response." + key
27
+ if template:
28
+ if any(arg is not None for arg in (status, body, mime_type)):
29
+ raise TypeError("response_builder: `template` cannot be used together with `status`, `body`, or `mime_type`.")
30
+
31
+ tmpl = self.ts._templates.get(template)
32
+ if not tmpl:
33
+ raise ValueError(f"response_builder: template '{template}' not found.")
34
+
35
+ self.config[key] = copy.deepcopy(tmpl)
36
+ else:
37
+ if status:
38
+ self.config[key]["status_code"] = status
39
+
40
+ if body:
41
+ self.config[key]["response_body"] = body
42
+
43
+ if mime_type:
44
+ self.config[key]["mime_type"] = mime_type
45
+
46
+ return self
47
+
48
+ def respond(self, status: int = None, body: typing.Union[dict, typing.Callable] = None, mime_type: str = None, template: str = None):
49
+ self._respond("authenticated", status, body, mime_type, template)
50
+ return self
51
+
52
+ def if_unauthenticated(self, status: int = None, body: typing.Union[dict, typing.Callable] = None, mime_type: str = None, template: str = None):
53
+ self._respond("unauthenticated", status, body, mime_type, template)
54
+ return self
55
+
56
+ def build(self):
57
+ return self.config
58
+
59
+ class WatchBuilder:
60
+ def __init__(self, path):
61
+ self.path = path
62
+ self._query = {}
63
+ self._body = {}
64
+
65
+ def query(self, name: str, default: typing.Any = NO_DEFAULT, intent: str = None):
66
+ self._query[name] = {"default": default, "intent": intent}
67
+ return self
68
+
69
+ def body(self, name: str, default: typing.Any = NO_DEFAULT, intent: str = None):
70
+ self._body[name] = {"default": default, "intent": intent}
71
+ return self
72
+
73
+ def build(self):
74
+ return {
75
+ "path": self.path,
76
+ "query_fields": self._query,
77
+ "body_fields": self._body
78
+ }
@@ -0,0 +1,257 @@
1
+ import time
2
+ import json
3
+ import typing
4
+ import socket
5
+ import logging
6
+
7
+ from .handlers import LogHandler
8
+ from .builders import TrapBuilder, WatchBuilder, NO_DEFAULT
9
+
10
+ class IdentityContext:
11
+ def __init__(self):
12
+ self.ip = None
13
+ self.auth = None
14
+
15
+ def get_context(self, request_obj):
16
+ u, r = None, None
17
+
18
+ if self.auth:
19
+ auth_context = self.auth(request_obj)
20
+ if isinstance(auth_context, dict):
21
+ u, r = auth_context.get("user"), auth_context.get("role")
22
+
23
+ return {
24
+ "user": u,
25
+ "role": r,
26
+ "ip": self.ip(request_obj) if self.ip else None
27
+ }
28
+
29
+ class RequestContext:
30
+ def __init__(self):
31
+ self.path = lambda r: None
32
+ self.user_agent = lambda r: None
33
+ self.method = lambda r: None
34
+
35
+ def get_context(self, request_obj):
36
+ return {
37
+ "path": self.path(request_obj),
38
+ "user_agent": self.user_agent(request_obj),
39
+ "method": self.method(request_obj)
40
+ }
41
+
42
+ class Sentry:
43
+ def __init__(self, app, service: str, environment: str):
44
+ self.logger = logging.getLogger("trappsec")
45
+ if not self.logger.handlers:
46
+ logging.basicConfig(format="%(message)s", level=logging.WARNING)
47
+
48
+ self.hostname = socket.gethostname()
49
+ self.service = service
50
+ self.environment = environment
51
+
52
+ self.integration = None
53
+
54
+ self.identity = IdentityContext()
55
+ self.request = RequestContext()
56
+
57
+ self._traps = []
58
+ self._watches = []
59
+ self._templates = {}
60
+ self._handlers = [LogHandler(self.logger)]
61
+
62
+ self.default_responses = {
63
+ "authenticated": {
64
+ "status_code": 200,
65
+ "response_body": {},
66
+ "mime_type": "application/json"
67
+ },
68
+ "unauthenticated": {
69
+ "status_code": 401,
70
+ "response_body": {},
71
+ "mime_type": "application/json"
72
+ }
73
+ }
74
+
75
+ self._register(app)
76
+
77
+ def template(self, name: str, status_code: int, response_body: dict, mime_type: str = "application/json"):
78
+ self._templates[name] = {"status_code": status_code, "response_body": response_body, "mime_type": mime_type}
79
+ return self
80
+
81
+ def trap(self, path: str):
82
+ builder = TrapBuilder(self, path)
83
+ self._traps.append(builder)
84
+ return builder
85
+
86
+ def watch(self, path: str):
87
+ builder = WatchBuilder(path)
88
+ self._watches.append(builder)
89
+ return builder
90
+
91
+ def add_webhook(self, url: str, secret: str = None, headers: dict = None, heartbeat_interval: int = None, template: typing.Callable = None):
92
+ from .handlers import WebhookHandler
93
+ handler = WebhookHandler(
94
+ url=url,
95
+ secret=secret,
96
+ headers=headers,
97
+ service=self.service,
98
+ environment=self.environment,
99
+ heartbeat_interval=heartbeat_interval,
100
+ template=template
101
+ )
102
+ self._handlers.append(handler)
103
+ return self
104
+
105
+ def add_otel(self):
106
+ from .handlers import OTELHandler
107
+ self._handlers.append(OTELHandler())
108
+ return self
109
+
110
+ def identify_user(self, callback: typing.Callable):
111
+ self.identity.auth = callback
112
+ return self
113
+
114
+ def override_source_ip(self, callback: typing.Callable):
115
+ self.identity.ip = callback
116
+ return self
117
+
118
+ @property
119
+ def traps(self):
120
+ return [d.build() if hasattr(d, "build") else d for d in self._traps]
121
+
122
+ @property
123
+ def watches(self):
124
+ return [r.build() if hasattr(r, "build") else r for r in self._watches]
125
+
126
+ def trigger(self, req, reason: str, intent: str = None, metadata: dict = None):
127
+ identity_ctx = self.identity.get_context(req)
128
+ request_ctx = self.request.get_context(req)
129
+
130
+ trigger_ctx = {
131
+ "timestamp": time.time(),
132
+ "event": "trappsec.rule_hit",
133
+ "type": "signal",
134
+ "reason": reason,
135
+ "intent": intent,
136
+ "path": request_ctx["path"],
137
+ "method": request_ctx["method"],
138
+ "user_agent": request_ctx["user_agent"],
139
+ "ip": identity_ctx["ip"],
140
+ "metadata": metadata,
141
+ }
142
+
143
+ if identity_ctx["user"]:
144
+ trigger_ctx["type"] = "alert"
145
+ trigger_ctx["user"] = identity_ctx["user"]
146
+ trigger_ctx["role"] = identity_ctx["role"]
147
+
148
+ self._trigger(trigger_ctx)
149
+
150
+ def _trigger(self, trigger_ctx):
151
+ trigger_ctx["app"] = {
152
+ "service": self.service,
153
+ "environment": self.environment,
154
+ "hostname": self.hostname
155
+ }
156
+
157
+ for h in self._handlers:
158
+ try:
159
+ h.emit(trigger_ctx)
160
+ except Exception as e:
161
+ self.logger.error("error invoking log handler: ", e)
162
+
163
+ def _trigger_watch_event(self, req, found_fields):
164
+ identity_ctx = self.identity.get_context(req)
165
+ request_ctx = self.request.get_context(req)
166
+
167
+ trigger_ctx = {
168
+ "timestamp": time.time(),
169
+ "event": "trappsec.watch_hit",
170
+ "type": "signal",
171
+ "path": request_ctx["path"],
172
+ "method": request_ctx["method"],
173
+ "user_agent": request_ctx["user_agent"],
174
+ "ip": identity_ctx["ip"],
175
+ "found_fields": found_fields
176
+ }
177
+
178
+ if identity_ctx["user"]:
179
+ trigger_ctx["type"] = "alert"
180
+ trigger_ctx["user"] = identity_ctx["user"]
181
+ trigger_ctx["role"] = identity_ctx["role"]
182
+
183
+ self._trigger(trigger_ctx)
184
+
185
+ def _trigger_trap_event(self, req, trap):
186
+ identity_ctx = self.identity.get_context(req)
187
+ request_ctx = self.request.get_context(req)
188
+
189
+ trigger_ctx = {
190
+ "timestamp": time.time(),
191
+ "event": "trappsec.trap_hit",
192
+ "type": "signal",
193
+ "path": request_ctx["path"],
194
+ "method": request_ctx["method"],
195
+ "user_agent": request_ctx["user_agent"],
196
+ "ip": identity_ctx["ip"],
197
+ "intent": trap.get("intent"),
198
+ }
199
+
200
+ response_key = "response.unauthenticated"
201
+
202
+ if identity_ctx["user"]:
203
+ trigger_ctx["type"] = "alert"
204
+ trigger_ctx["user"] = identity_ctx["user"]
205
+ trigger_ctx["role"] = identity_ctx["role"]
206
+ response_key = "response.authenticated"
207
+
208
+ self._trigger(trigger_ctx)
209
+
210
+ response_config = trap[response_key]
211
+ response_body = response_config["response_body"]
212
+
213
+ if callable(response_body):
214
+ response_body = response_body(req)
215
+
216
+ if response_config["mime_type"] == "application/json":
217
+ response_body = json.dumps(response_body)
218
+
219
+ return response_body, response_config
220
+
221
+ def _detect_honey_fields(self, data, rules, request_obj=None):
222
+ found_fields = []
223
+
224
+ for key in list(data.keys()):
225
+ if key in rules:
226
+ rule_definition = rules[key]
227
+ expected = rule_definition.get("default", NO_DEFAULT)
228
+
229
+ try:
230
+ if callable(expected):
231
+ expected = expected(request_obj)
232
+
233
+ if expected is NO_DEFAULT or data[key] != expected:
234
+ found_fields.append({
235
+ "type": "body",
236
+ "field": key,
237
+ "value": data[key],
238
+ "intent": rule_definition.get("intent", None),
239
+ })
240
+ except Exception as e:
241
+ self.logger.error(f"failed to evaluate callable expected value for body field `{key}`: ", e)
242
+
243
+ del data[key]
244
+ return data, found_fields
245
+
246
+ def _register(self, app):
247
+ name = app.__class__.__name__
248
+ module = app.__class__.__module__
249
+
250
+ if name == "FastAPI" or module.startswith("fastapi"):
251
+ from .integrations.fastapi import FastAPIIntegration
252
+ self.integration = FastAPIIntegration(self, app)
253
+ elif name == "Flask" or module.startswith("flask"):
254
+ from .integrations.flask import FlaskIntegration
255
+ self.integration = FlaskIntegration(self, app)
256
+ else:
257
+ raise Exception("trappsec error: unknown framework.")
@@ -0,0 +1,121 @@
1
+ import logging
2
+ import json
3
+ import hmac
4
+ import hashlib
5
+ import threading
6
+ import time
7
+
8
+ try:
9
+ import requests
10
+ from requests.adapters import HTTPAdapter
11
+ from urllib3.util.retry import Retry
12
+ except ImportError:
13
+ requests = None
14
+
15
+
16
+ class BaseHandler:
17
+ def emit(self, event: dict): raise NotImplementedError
18
+
19
+ class LogHandler(BaseHandler):
20
+ def __init__(self, logger: logging.Logger):
21
+ self.logger = logger
22
+ def emit(self, event: dict):
23
+ self.logger.warning(json.dumps(event))
24
+
25
+ class WebhookHandler(BaseHandler):
26
+ def __init__(self, url: str, secret: str = None, headers: dict = None, service: str = None, environment: str = None, heartbeat_interval: int = None, template: callable = None):
27
+ if requests is None:
28
+ raise ImportError("requests library required for WebhookHandler")
29
+
30
+ self.url = url
31
+ self.secret = secret
32
+ self.service = service
33
+ self.environment = environment
34
+ self.template = template
35
+ self.logger = logging.getLogger("trappsec")
36
+
37
+ self.headers = {"Content-Type": "application/json"}
38
+ self.headers.update(headers or {})
39
+
40
+ self.session = requests.Session()
41
+ self.session.mount("https://", HTTPAdapter(max_retries=Retry(total=3, backoff_factor=1)))
42
+
43
+ if heartbeat_interval:
44
+ threading.Thread(target=self._heartbeat_loop, args=(heartbeat_interval,), daemon=True).start()
45
+
46
+ def emit(self, event: dict):
47
+ if self.template:
48
+ try:
49
+ event = self.template(event)
50
+ except Exception as e:
51
+ self.logger.error(f"Failed to apply webhook template: {e}")
52
+
53
+ payload = json.dumps(event)
54
+ self._send(payload)
55
+
56
+ def _heartbeat_loop(self, interval: int):
57
+ while True:
58
+ time.sleep(interval)
59
+ payload = json.dumps({
60
+ "timestamp": time.time(),
61
+ "event": "trappsec.heartbeat",
62
+ "service": self.service,
63
+ "environment": self.environment,
64
+ })
65
+ self._send(payload)
66
+
67
+ def _send(self, payload: str):
68
+ headers = self.headers.copy()
69
+ if self.secret:
70
+ headers["x-trappsec-signature"] = hmac.new(
71
+ self.secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
72
+
73
+ try:
74
+ self.session.post(self.url, data=payload, headers=headers, timeout=5)
75
+ except Exception as e:
76
+ self.logger.error(f"Failed to send webhook: {e}")
77
+
78
+ try:
79
+ from opentelemetry import trace
80
+ except ImportError:
81
+ trace = None
82
+
83
+ class OTELHandler(BaseHandler):
84
+ def __init__(self):
85
+ if trace is None:
86
+ raise ImportError("opentelemetry-api library required for OTELHandler")
87
+
88
+ def emit(self, event: dict):
89
+ current_span = trace.get_current_span()
90
+ if current_span.is_recording():
91
+ current_span.set_attribute("trappsec.detected", True)
92
+ current_span.set_attribute("trappsec.event", event["event"])
93
+ current_span.set_attribute("trappsec.type", event["type"])
94
+
95
+ if event.get("user"):
96
+ current_span.set_attribute("trappsec.user", event["user"])
97
+ if event.get("role"):
98
+ current_span.set_attribute("trappsec.role", event["role"])
99
+
100
+ if event.get("ip"):
101
+ current_span.set_attribute("trappsec.ip", event["ip"])
102
+
103
+ if event["event"] == "trappsec.watch_hit":
104
+ for field_info in event["found_fields"]:
105
+ current_span.add_event("watch_hit", field_info)
106
+
107
+ if event["event"] == "trappsec.trap_hit":
108
+ if event.get("intent"):
109
+ current_span.set_attribute("trappsec.intent", event["intent"])
110
+
111
+ if event["event"] == "trappsec.rule_hit":
112
+ if event.get("intent"):
113
+ current_span.set_attribute("trappsec.intent", event["intent"])
114
+ if event.get("reason"):
115
+ current_span.set_attribute("trappsec.reason", event["reason"])
116
+
117
+ if event.get("metadata"):
118
+ metadata = event["metadata"]
119
+ if isinstance(metadata, dict):
120
+ attrs = {f"metadata.{k}": v for k, v in metadata.items()}
121
+ current_span.set_attributes(attrs)
File without changes
@@ -0,0 +1,118 @@
1
+ from functools import partial
2
+
3
+ class FastAPIIntegration:
4
+ def __init__(self, ts, app):
5
+ self.ts = ts
6
+ self.app = app
7
+ self.watch_map = None
8
+
9
+ if not self.ts.identity.ip:
10
+ self.ts.identity.ip = lambda r: r.client.host if r.client else "0.0.0.0"
11
+
12
+ self.ts.request.path = lambda r: str(r.url.path)
13
+ self.ts.request.user_agent = lambda r: r.headers.get("user-agent", "unknown")
14
+ self.ts.request.method = lambda r: r.method
15
+
16
+ self.setup_middleware()
17
+ self._patch_startup()
18
+
19
+ def inject_traps(self):
20
+ from fastapi import Request, Response
21
+ from fastapi.routing import APIRoute
22
+
23
+ async def endpoint(req: Request, trap):
24
+ response_body, response_config = self.ts._trigger_trap_event(req, trap)
25
+
26
+ return Response(response_body,
27
+ status_code=response_config["status_code"],
28
+ media_type=response_config["mime_type"])
29
+
30
+ new_routes = []
31
+ for idx, trap in enumerate(self.ts.traps):
32
+ new_routes.append(APIRoute(trap["path"], partial(endpoint, trap=trap),
33
+ methods=trap["methods"], name=f"trappsec_{idx}", include_in_schema=False))
34
+
35
+ self.app.router.routes = new_routes + self.app.router.routes
36
+
37
+ def setup_watches(self):
38
+ watch_map = dict()
39
+ for watch in self.ts.watches:
40
+ watch_map[watch["path"]] = watch
41
+ self.watch_map = watch_map
42
+
43
+ def setup_middleware(self):
44
+ from fastapi import Request, Depends
45
+ from starlette.datastructures import FormData
46
+ from starlette.middleware.base import BaseHTTPMiddleware
47
+ from urllib.parse import parse_qs, urlencode
48
+
49
+ async def trappsec_watcher(request: Request):
50
+ route = request.scope.get("route")
51
+ if route is None:
52
+ return
53
+
54
+ if route.path not in self.watch_map:
55
+ return
56
+
57
+ matched_rule = self.watch_map[route.path]
58
+ query_fields = matched_rule["query_fields"]
59
+ body_fields = matched_rule["body_fields"]
60
+ found_fields = []
61
+
62
+ if query_fields:
63
+ qs = request.scope.get("query_string", b"").decode("utf-8")
64
+ if qs:
65
+ q_dict = parse_qs(qs).to_dict(flat=False)
66
+ q_dict, mod = self.ts._detect_honey_fields(q_dict, query_fields, request)
67
+
68
+ if mod:
69
+ found_fields.extend(mod)
70
+ request.scope["query_string"] = urlencode(q_dict, doseq=True).encode("utf-8")
71
+ if hasattr(request, "_query_params"):
72
+ del request._query_params
73
+
74
+ if body_fields:
75
+ ctype = request.headers.get("content-type", "")
76
+ if "application/json" in ctype:
77
+ try:
78
+ body = await request.json()
79
+ b, mod = self.ts._detect_honey_fields(body, body_fields, request)
80
+ if mod:
81
+ found_fields.extend(mod)
82
+ request._json = b
83
+ except Exception as e:
84
+ self.ts.logger.error("error reading json body: %s", e)
85
+ elif "application/x-www-form-urlencoded" in ctype or "multipart/form-data" in ctype:
86
+ try:
87
+ form_data = await request.form()
88
+ data = dict(form_data)
89
+ b, mod = self.ts._detect_honey_fields(data, body_fields, request)
90
+ if mod:
91
+ found_fields.extend(mod)
92
+ request._form = FormData(b)
93
+ except Exception as e:
94
+ self.ts.logger.error("error reading form body: %s", e)
95
+
96
+ if found_fields:
97
+ self.ts._trigger_watch_event(request, found_fields)
98
+
99
+ if self.app.router.dependencies is None:
100
+ self.app.router.dependencies = []
101
+
102
+ self.app.router.dependencies.append(Depends(trappsec_watcher))
103
+
104
+ def _patch_startup(self):
105
+ from contextlib import asynccontextmanager
106
+ original_lifespan = self.app.router.lifespan_context
107
+
108
+ @asynccontextmanager
109
+ async def wrapped_lifespan(app_instance):
110
+ self.inject_traps()
111
+ self.setup_watches()
112
+
113
+ async with original_lifespan(app_instance) as state:
114
+ yield state
115
+
116
+ self.app.router.lifespan_context = wrapped_lifespan
117
+
118
+
@@ -0,0 +1,98 @@
1
+
2
+ class FlaskIntegration:
3
+ def __init__(self, ts, app):
4
+ self.ts = ts
5
+ self.app = app
6
+
7
+ if not self.ts.identity.ip:
8
+ self.ts.identity.ip = lambda r: r.remote_addr or "0.0.0.0"
9
+
10
+ self.ts.request.path = lambda r: r.path
11
+ self.ts.request.user_agent = lambda r: str(r.user_agent)
12
+ self.ts.request.method = lambda r: r.method
13
+
14
+ self._patch_startup()
15
+
16
+ def inject_traps(self):
17
+ from flask import request, Response
18
+
19
+ for idx, decoy in enumerate(self.ts.traps):
20
+ def endpoint(d=decoy):
21
+ response_body, response_config = self.ts._trigger_trap_event(request, d)
22
+
23
+ return Response(
24
+ response_body,
25
+ status=response_config["status_code"],
26
+ mimetype=response_config["mime_type"])
27
+
28
+ endpoint.__name__ = f"trappsec_{idx}"
29
+ self.app.add_url_rule(decoy["path"], endpoint.__name__, endpoint, methods=decoy["methods"])
30
+
31
+ def setup_watches(self):
32
+ if not self.ts.watches:
33
+ return
34
+
35
+ from flask import request
36
+ from werkzeug.datastructures import ImmutableMultiDict
37
+
38
+ watch_map = dict()
39
+ for watch in self.ts.watches:
40
+ watch_map[watch["path"]] = watch
41
+
42
+ @self.app.before_request
43
+ def trappsec_watcher():
44
+ if request.url_rule is None:
45
+ return
46
+
47
+ matched_rule = request.url_rule.rule
48
+ if matched_rule not in watch_map:
49
+ return
50
+
51
+ query_fields = watch_map[matched_rule]["query_fields"]
52
+ body_fields = watch_map[matched_rule]["body_fields"]
53
+
54
+ found_fields = []
55
+ if request.args and query_fields:
56
+ q_dict = request.args.to_dict(flat=False)
57
+ q_dict, mod = self.ts._detect_honey_fields(q_dict, query_fields, request)
58
+ if mod:
59
+ found_fields.extend(mod)
60
+ request.args = ImmutableMultiDict(q_dict)
61
+
62
+ if request.is_json and body_fields:
63
+ data = request.get_json(silent=True)
64
+ if data:
65
+ data, mod = self.ts._detect_honey_fields(data, body_fields, request)
66
+ if mod:
67
+ found_fields.extend(mod)
68
+ current = getattr(request, '_cached_json', None)
69
+ if isinstance(current, tuple):
70
+ request._cached_json = (data, current[1])
71
+ else:
72
+ request._cached_json = data
73
+
74
+ if request.form and body_fields:
75
+ form_copy = request.form.to_dict(flat=True)
76
+ form, mod = self.ts._detect_honey_fields(form_copy, body_fields, request)
77
+ if mod:
78
+ found_fields.extend(mod)
79
+ request.form = ImmutableMultiDict(form)
80
+
81
+ if found_fields:
82
+ self.ts._trigger_watch_event(request, found_fields)
83
+
84
+
85
+ def _patch_startup(self):
86
+ original_wsgi_app = self.app.wsgi_app
87
+
88
+ def trappsec_wrapper(environ, start_response):
89
+ self.inject_traps()
90
+ self.setup_watches()
91
+
92
+ # un-patch after first request
93
+ self.app.wsgi_app = original_wsgi_app
94
+
95
+ return original_wsgi_app(environ, start_response)
96
+
97
+ self.app.wsgi_app = trappsec_wrapper
98
+
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: trappsec
3
+ Version: 0.1.0
4
+ Summary: deception as a developer tool
5
+ Author-email: Nikhil Salgaonkar <nikhil@ftfy.co>
6
+ Project-URL: Homepage, https://trappsec.dev
7
+ Project-URL: Repository, https://github.com/trappsec-dev/trappsec
8
+ Project-URL: Bug Tracker, https://github.com/trappsec-dev/trappsec/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.7
13
+ Description-Content-Type: text/markdown
14
+ Provides-Extra: webhooks
15
+ Requires-Dist: requests; extra == "webhooks"
16
+ Provides-Extra: otel
17
+ Requires-Dist: opentelemetry-api; extra == "otel"
@@ -0,0 +1,13 @@
1
+ pyproject.toml
2
+ src/trappsec/__init__.py
3
+ src/trappsec/builders.py
4
+ src/trappsec/core.py
5
+ src/trappsec/handlers.py
6
+ src/trappsec.egg-info/PKG-INFO
7
+ src/trappsec.egg-info/SOURCES.txt
8
+ src/trappsec.egg-info/dependency_links.txt
9
+ src/trappsec.egg-info/requires.txt
10
+ src/trappsec.egg-info/top_level.txt
11
+ src/trappsec/integrations/__init__.py
12
+ src/trappsec/integrations/fastapi.py
13
+ src/trappsec/integrations/flask.py
@@ -0,0 +1,6 @@
1
+
2
+ [otel]
3
+ opentelemetry-api
4
+
5
+ [webhooks]
6
+ requests
@@ -0,0 +1 @@
1
+ trappsec