forensic-cli 1.2.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cli/__init__.py +0 -0
- cli/commands/__init__.py +0 -0
- cli/commands/browser.py +220 -0
- cli/commands/describe.py +54 -0
- cli/commands/email.py +3 -0
- cli/commands/network.py +126 -0
- cli/commands/utils.py +3 -0
- cli/main.py +18 -0
- core/__init__.py +0 -0
- core/browser/browser_history.py +156 -0
- core/browser/common_words.py +60 -0
- core/browser/downloads_history.py +129 -0
- core/browser/fav_screen.py +70 -0
- core/browser/logins.py +108 -0
- core/browser/unusual_patterns.py +111 -0
- core/data/__init__.py +0 -0
- core/data/data_recovery.py +157 -0
- core/db/db.py +12 -0
- core/email/email_parser.py +87 -0
- core/email/header_analysis.py +87 -0
- core/models/orm.py +48 -0
- core/models/schemas.py +76 -0
- core/network/__init__.py +0 -0
- core/network/arp_scan.py +16 -0
- core/network/dns_recon.py +90 -0
- core/network/fingerprinting.py +117 -0
- core/network/ip_info.py +29 -0
- core/network/network_map.py +120 -0
- core/network/ping_sweep.py +53 -0
- core/network/port_scanner.py +93 -0
- core/network/smb_scan.py +32 -0
- core/network/snmp_scan.py +43 -0
- core/network/traceroute.py +40 -0
- core/network/utils.py +27 -0
- core/registry.py +25 -0
- core/utils/__init__.py +0 -0
- core/utils/gerar_amostras.py +33 -0
- forensic_cli-1.2.5.dist-info/METADATA +293 -0
- forensic_cli-1.2.5.dist-info/RECORD +44 -0
- forensic_cli-1.2.5.dist-info/WHEEL +5 -0
- forensic_cli-1.2.5.dist-info/entry_points.txt +2 -0
- forensic_cli-1.2.5.dist-info/licenses/LICENSE +21 -0
- forensic_cli-1.2.5.dist-info/licenses/LICENSE-GPL +675 -0
- forensic_cli-1.2.5.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sqlite3
|
|
3
|
+
import time
|
|
4
|
+
import json
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
def timestamp_chrome(microsegundos):
|
|
9
|
+
if microsegundos:
|
|
10
|
+
return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(microsegundos / 1000000 - 11644473600))
|
|
11
|
+
return "N/A"
|
|
12
|
+
|
|
13
|
+
def timestamp_firefox(microsegundos):
|
|
14
|
+
if microsegundos:
|
|
15
|
+
return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(microsegundos / 1000000))
|
|
16
|
+
return "N/A"
|
|
17
|
+
|
|
18
|
+
def salvar_em_json(dados, navegador, usuario, output_dir: Path):
|
|
19
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
nome_arquivo = output_dir / f"Downloads_{navegador}_{usuario}.json"
|
|
21
|
+
with open(nome_arquivo, "w", encoding="utf-8") as f:
|
|
22
|
+
json.dump(dados, f, indent=4, ensure_ascii=False)
|
|
23
|
+
print(f"[✓] Arquivo salvo: {nome_arquivo}")
|
|
24
|
+
|
|
25
|
+
def copiar_banco(origem):
|
|
26
|
+
destino = f"temp_{os.path.basename(origem)}"
|
|
27
|
+
shutil.copy2(origem, destino)
|
|
28
|
+
return destino
|
|
29
|
+
|
|
30
|
+
def extrair_downloads_chrome_edge(caminho_banco, navegador, usuario, output_dir: Path):
|
|
31
|
+
caminho_temp = copiar_banco(caminho_banco)
|
|
32
|
+
try:
|
|
33
|
+
conn = sqlite3.connect(caminho_temp)
|
|
34
|
+
cursor = conn.cursor()
|
|
35
|
+
|
|
36
|
+
cursor.execute("""
|
|
37
|
+
SELECT d.target_path, d.tab_url, d.start_time, d.received_bytes, d.state, u.url
|
|
38
|
+
FROM downloads d
|
|
39
|
+
LEFT JOIN downloads_url_chains u ON d.id = u.id
|
|
40
|
+
ORDER BY d.start_time DESC
|
|
41
|
+
LIMIT 10;
|
|
42
|
+
""")
|
|
43
|
+
rows = cursor.fetchall()
|
|
44
|
+
|
|
45
|
+
downloads = []
|
|
46
|
+
for row in rows:
|
|
47
|
+
path, tab_url, start_time, bytes_received, state, source_url = row
|
|
48
|
+
downloads.append({
|
|
49
|
+
"arquivo": path,
|
|
50
|
+
"url_origem": source_url,
|
|
51
|
+
"url_abas": tab_url,
|
|
52
|
+
"inicio_download": timestamp_chrome(start_time),
|
|
53
|
+
"bytes_recebidos": bytes_received,
|
|
54
|
+
"estado": state
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
salvar_em_json(downloads, navegador, usuario, output_dir)
|
|
58
|
+
|
|
59
|
+
except Exception as e:
|
|
60
|
+
print(f"[!] Erro ({navegador}): {e}")
|
|
61
|
+
finally:
|
|
62
|
+
conn.close()
|
|
63
|
+
os.remove(caminho_temp)
|
|
64
|
+
|
|
65
|
+
def extrair_downloads_firefox(caminho_banco, usuario, output_dir: Path):
|
|
66
|
+
caminho_temp = copiar_banco(caminho_banco)
|
|
67
|
+
try:
|
|
68
|
+
conn = sqlite3.connect(caminho_temp)
|
|
69
|
+
cursor = conn.cursor()
|
|
70
|
+
|
|
71
|
+
cursor.execute("""
|
|
72
|
+
SELECT source, target, startTime, endTime, state
|
|
73
|
+
FROM moz_downloads
|
|
74
|
+
ORDER BY startTime DESC
|
|
75
|
+
LIMIT 10;
|
|
76
|
+
""")
|
|
77
|
+
rows = cursor.fetchall()
|
|
78
|
+
|
|
79
|
+
downloads = []
|
|
80
|
+
for row in rows:
|
|
81
|
+
source, target, startTime, endTime, state = row
|
|
82
|
+
downloads.append({
|
|
83
|
+
"arquivo": target,
|
|
84
|
+
"url_origem": source,
|
|
85
|
+
"inicio_download": timestamp_firefox(startTime),
|
|
86
|
+
"estado": state
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
salvar_em_json(downloads, "Firefox", usuario, output_dir)
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print(f"[!] Erro (Firefox): {e}")
|
|
93
|
+
finally:
|
|
94
|
+
conn.close()
|
|
95
|
+
os.remove(caminho_temp)
|
|
96
|
+
|
|
97
|
+
def extract_downloads_history(output_dir: Path, chrome=True, edge=True, firefox=True):
|
|
98
|
+
usuario = os.getlogin()
|
|
99
|
+
home = Path.home()
|
|
100
|
+
|
|
101
|
+
if chrome:
|
|
102
|
+
caminho_chrome = home / "AppData/Local/Google/Chrome/User Data/Default/History"
|
|
103
|
+
if caminho_chrome.exists():
|
|
104
|
+
print("[*] Extraindo downloads do Chrome...")
|
|
105
|
+
extrair_downloads_chrome_edge(caminho_chrome, "Chrome", usuario, output_dir)
|
|
106
|
+
else:
|
|
107
|
+
print("[!] Chrome não encontrado.")
|
|
108
|
+
|
|
109
|
+
if edge:
|
|
110
|
+
caminho_edge = home / "AppData/Local/Microsoft/Edge/User Data/Default/History"
|
|
111
|
+
if caminho_edge.exists():
|
|
112
|
+
print("[*] Extraindo downloads do Edge...")
|
|
113
|
+
extrair_downloads_chrome_edge(caminho_edge, "Edge", usuario, output_dir)
|
|
114
|
+
else:
|
|
115
|
+
print("[!] Edge não encontrado.")
|
|
116
|
+
|
|
117
|
+
if firefox:
|
|
118
|
+
caminho_firefox_perfis = home / "AppData/Roaming/Mozilla/Firefox/Profiles"
|
|
119
|
+
if caminho_firefox_perfis.exists():
|
|
120
|
+
for perfil in caminho_firefox_perfis.iterdir():
|
|
121
|
+
caminho_downloads_sqlite = perfil / "downloads.sqlite"
|
|
122
|
+
if caminho_downloads_sqlite.exists():
|
|
123
|
+
print(f"[*] Extraindo downloads do Firefox ({perfil.name})...")
|
|
124
|
+
extrair_downloads_firefox(caminho_downloads_sqlite, usuario, output_dir)
|
|
125
|
+
break
|
|
126
|
+
else:
|
|
127
|
+
print("[!] downloads.sqlite do Firefox não encontrado.")
|
|
128
|
+
else:
|
|
129
|
+
print("[!] Perfis do Firefox não encontrados.")
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from selenium import webdriver
|
|
3
|
+
from selenium.webdriver.chrome.options import Options
|
|
4
|
+
from urllib.parse import urlparse
|
|
5
|
+
import hashlib
|
|
6
|
+
import requests
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
def carregar_json(caminho):
|
|
11
|
+
with open(caminho, 'r', encoding='utf-8') as f:
|
|
12
|
+
return json.load(f)
|
|
13
|
+
|
|
14
|
+
def extrair_urls_validas(dados_json):
|
|
15
|
+
urls = []
|
|
16
|
+
|
|
17
|
+
for chave in ["historico_ultimas_urls", "ultimas_10_urls", "historico_completo"]:
|
|
18
|
+
entradas = dados_json.get(chave, [])
|
|
19
|
+
for entrada in entradas:
|
|
20
|
+
url = entrada.get("url")
|
|
21
|
+
if url and url.startswith("http"):
|
|
22
|
+
urls.append(url)
|
|
23
|
+
|
|
24
|
+
return urls
|
|
25
|
+
|
|
26
|
+
def baixar_favicon(url, pasta_destino: Path):
|
|
27
|
+
try:
|
|
28
|
+
pasta_destino.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
dominio = urlparse(url).netloc
|
|
30
|
+
favicon_url = f"https://{dominio}/favicon.ico"
|
|
31
|
+
resposta = requests.get(favicon_url, timeout=5)
|
|
32
|
+
if resposta.status_code == 200:
|
|
33
|
+
nome_arquivo = hashlib.md5(dominio.encode()).hexdigest() + ".ico"
|
|
34
|
+
caminho = pasta_destino / nome_arquivo
|
|
35
|
+
with open(caminho, "wb") as f:
|
|
36
|
+
f.write(resposta.content)
|
|
37
|
+
print(f"[FAVICON] Baixado: {dominio}")
|
|
38
|
+
else:
|
|
39
|
+
print(f"[FAVICON] Não encontrado para {dominio}")
|
|
40
|
+
except Exception as e:
|
|
41
|
+
print(f"[FAVICON] Erro ao baixar de {url}: {e}")
|
|
42
|
+
|
|
43
|
+
def capturar_print(url, pasta_destino: Path):
|
|
44
|
+
try:
|
|
45
|
+
pasta_destino.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
nome_arquivo = hashlib.md5(url.encode()).hexdigest() + ".png"
|
|
47
|
+
caminho = pasta_destino / nome_arquivo
|
|
48
|
+
|
|
49
|
+
chrome_options = Options()
|
|
50
|
+
chrome_options.add_argument("--headless=new")
|
|
51
|
+
chrome_options.add_argument("--disable-gpu")
|
|
52
|
+
chrome_options.add_argument("--window-size=1280,1024")
|
|
53
|
+
|
|
54
|
+
driver = webdriver.Chrome(options=chrome_options)
|
|
55
|
+
driver.set_page_load_timeout(10)
|
|
56
|
+
driver.get(url)
|
|
57
|
+
time.sleep(3)
|
|
58
|
+
driver.save_screenshot(str(caminho))
|
|
59
|
+
driver.quit()
|
|
60
|
+
print(f"[PRINT] Capturado: {url}")
|
|
61
|
+
except Exception as e:
|
|
62
|
+
print(f"[PRINT] Erro ao capturar print de {url}: {e}")
|
|
63
|
+
|
|
64
|
+
def processar_urls(urls, output_dir: Path):
|
|
65
|
+
fav_dir = output_dir / "favicons"
|
|
66
|
+
print_dir = output_dir / "prints"
|
|
67
|
+
for url in urls:
|
|
68
|
+
print(f"\n[PROCESSANDO] {url}")
|
|
69
|
+
baixar_favicon(url, fav_dir)
|
|
70
|
+
capturar_print(url, print_dir)
|
core/browser/logins.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import base64
|
|
4
|
+
import shutil
|
|
5
|
+
import sqlite3
|
|
6
|
+
from Crypto.Cipher import AES
|
|
7
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
8
|
+
import win32crypt
|
|
9
|
+
|
|
10
|
+
def find_chrome_profiles() -> list[tuple[str, str]]:
|
|
11
|
+
base = os.path.join(os.environ["USERPROFILE"], "AppData", "Local", "Google", "Chrome", "User Data")
|
|
12
|
+
profiles = []
|
|
13
|
+
|
|
14
|
+
for name in os.listdir(base):
|
|
15
|
+
if name == "Default" or name.startswith("Profile"):
|
|
16
|
+
db = os.path.join(base, name, "Login Data")
|
|
17
|
+
if os.path.isfile(db):
|
|
18
|
+
profiles.append((name, db))
|
|
19
|
+
|
|
20
|
+
return profiles
|
|
21
|
+
|
|
22
|
+
def collect_chrome_logins() -> dict:
|
|
23
|
+
result = []
|
|
24
|
+
profiles = find_chrome_profiles()
|
|
25
|
+
for profile_name, db_path in profiles:
|
|
26
|
+
tmp_db = os.path.join(os.getenv("TEMP"), f"{profile_name}_LoginData.db")
|
|
27
|
+
shutil.copy2(db_path, tmp_db)
|
|
28
|
+
|
|
29
|
+
conn = sqlite3.connect(tmp_db)
|
|
30
|
+
cursor = conn.cursor()
|
|
31
|
+
cursor.execute("SELECT origin_url, username_value, password_value FROM logins")
|
|
32
|
+
rows = cursor.fetchall()
|
|
33
|
+
|
|
34
|
+
for origin_url, username, pwd_blob in rows:
|
|
35
|
+
pwd_b64 = base64.b64encode(pwd_blob).decode('ascii')
|
|
36
|
+
result.append({
|
|
37
|
+
"profile": profile_name,
|
|
38
|
+
"site": origin_url,
|
|
39
|
+
"user": username,
|
|
40
|
+
"password_encrypted": pwd_b64
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
cursor.close()
|
|
44
|
+
conn.close()
|
|
45
|
+
os.remove(tmp_db)
|
|
46
|
+
|
|
47
|
+
return {"logins": result}
|
|
48
|
+
|
|
49
|
+
def get_edge_user_data_path():
|
|
50
|
+
user = os.getenv("USERNAME")
|
|
51
|
+
|
|
52
|
+
return f"C:/Users/{user}/AppData/Local/Microsoft/Edge/User Data/Default"
|
|
53
|
+
|
|
54
|
+
def get_edge_encryption_key():
|
|
55
|
+
user = os.getenv("USERNAME")
|
|
56
|
+
local_state_path = f"C:/Users/{user}/AppData/Local/Microsoft/Edge/User Data/Local State"
|
|
57
|
+
|
|
58
|
+
with open(local_state_path, 'r', encoding='utf-8') as file:
|
|
59
|
+
local_state = json.load(file)
|
|
60
|
+
|
|
61
|
+
encrypted_key = base64.b64decode(local_state["os_crypt"]["encrypted_key"])
|
|
62
|
+
encrypted_key = encrypted_key[5:]
|
|
63
|
+
key = win32crypt.CryptUnprotectData(encrypted_key, None, None, None, 0)[1]
|
|
64
|
+
|
|
65
|
+
return key
|
|
66
|
+
|
|
67
|
+
def decrypt_edge_password(password_encrypted, key):
|
|
68
|
+
try:
|
|
69
|
+
if password_encrypted.startswith(b'v10') or password_encrypted.startswith(b'v11'):
|
|
70
|
+
password_encrypted = password_encrypted[3:]
|
|
71
|
+
iv = password_encrypted[:12]
|
|
72
|
+
payload = password_encrypted[12:]
|
|
73
|
+
aesgcm = AESGCM(key)
|
|
74
|
+
|
|
75
|
+
return aesgcm.decrypt(iv, payload, None).decode()
|
|
76
|
+
else:
|
|
77
|
+
return win32crypt.CryptUnprotectData(password_encrypted, None, None, None, 0)[1].decode()
|
|
78
|
+
except Exception:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
def collect_edge_logins() -> dict:
|
|
82
|
+
user_data_path = get_edge_user_data_path()
|
|
83
|
+
login_data_path = os.path.join(user_data_path, "Login Data")
|
|
84
|
+
tmp_db = os.path.join(os.getenv("TEMP"), "Edge_LoginData.db")
|
|
85
|
+
shutil.copy2(login_data_path, tmp_db)
|
|
86
|
+
|
|
87
|
+
key = get_edge_encryption_key()
|
|
88
|
+
result = []
|
|
89
|
+
|
|
90
|
+
conn = sqlite3.connect(tmp_db)
|
|
91
|
+
cursor = conn.cursor()
|
|
92
|
+
try:
|
|
93
|
+
cursor.execute("SELECT origin_url, username_value, password_value FROM logins")
|
|
94
|
+
for origin_url, username, pwd_encrypted in cursor.fetchall():
|
|
95
|
+
if not pwd_encrypted:
|
|
96
|
+
continue
|
|
97
|
+
pwd = decrypt_edge_password(pwd_encrypted, key)
|
|
98
|
+
result.append({
|
|
99
|
+
"site": origin_url,
|
|
100
|
+
"user": username,
|
|
101
|
+
"password": pwd
|
|
102
|
+
})
|
|
103
|
+
finally:
|
|
104
|
+
cursor.close()
|
|
105
|
+
conn.close()
|
|
106
|
+
os.remove(tmp_db)
|
|
107
|
+
|
|
108
|
+
return {"logins": result}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
def carregar_json(caminho):
|
|
8
|
+
with open(caminho, 'r', encoding='utf-8') as f:
|
|
9
|
+
return json.load(f)
|
|
10
|
+
|
|
11
|
+
def tratar_data_acesso(data_str):
|
|
12
|
+
try:
|
|
13
|
+
if data_str == "N/A" or not data_str:
|
|
14
|
+
return None
|
|
15
|
+
return pd.to_datetime(data_str, format="%Y-%m-%d %H:%M:%S", errors='raise')
|
|
16
|
+
except Exception as e:
|
|
17
|
+
print(f"[ALERTA] Erro ao processar data: {data_str}. Erro: {e}")
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
def detectar_acessos_horarios_diferentes(df, output_dir=None, arquivo_prefixo=""):
|
|
21
|
+
df['ultimo_acesso'] = df['ultimo_acesso'].apply(tratar_data_acesso)
|
|
22
|
+
df = df.dropna(subset=['ultimo_acesso']).copy()
|
|
23
|
+
df.loc[:, 'hora'] = df['ultimo_acesso'].dt.hour
|
|
24
|
+
|
|
25
|
+
acessos_madrugada = df[df['hora'].between(0, 6)]
|
|
26
|
+
acessos_manha = df[df['hora'].between(7, 11)]
|
|
27
|
+
acessos_tarde = df[df['hora'].between(12, 17)]
|
|
28
|
+
acessos_noite = df[df['hora'].between(18, 23)]
|
|
29
|
+
|
|
30
|
+
plt.figure(figsize=(10, 6))
|
|
31
|
+
acessos_por_hora = df['hora'].value_counts().sort_index()
|
|
32
|
+
acessos_por_hora.plot(kind='bar', color='skyblue')
|
|
33
|
+
plt.title('Acessos por Hora do Dia')
|
|
34
|
+
plt.xlabel('Hora')
|
|
35
|
+
plt.ylabel('Número de Acessos')
|
|
36
|
+
plt.xticks(rotation=0)
|
|
37
|
+
plt.tight_layout()
|
|
38
|
+
|
|
39
|
+
if output_dir:
|
|
40
|
+
caminho_grafico = os.path.join(output_dir, f"{arquivo_prefixo}acessos_por_hora.png")
|
|
41
|
+
plt.savefig(caminho_grafico)
|
|
42
|
+
plt.close()
|
|
43
|
+
print(f"[INFO] Gráfico salvo em: {caminho_grafico}")
|
|
44
|
+
else:
|
|
45
|
+
plt.show()
|
|
46
|
+
|
|
47
|
+
acessos_por_periodo = {
|
|
48
|
+
"Madrugada (00h-06h)": len(acessos_madrugada),
|
|
49
|
+
"Manhã (07h-11h)": len(acessos_manha),
|
|
50
|
+
"Tarde (12h-17h)": len(acessos_tarde),
|
|
51
|
+
"Noite (18h-23h)": len(acessos_noite),
|
|
52
|
+
}
|
|
53
|
+
print("\nAcessos por período:")
|
|
54
|
+
print(pd.DataFrame(list(acessos_por_periodo.items()), columns=["Período", "Quantidade de Acessos"]))
|
|
55
|
+
|
|
56
|
+
acessos_incomuns = pd.concat([acessos_madrugada, acessos_noite])
|
|
57
|
+
if not acessos_incomuns.empty:
|
|
58
|
+
print(f"\nAcessos em horários incomuns (madrugada/noite):")
|
|
59
|
+
print(acessos_incomuns[['url', 'ultimo_acesso']].head())
|
|
60
|
+
|
|
61
|
+
def detectar_acessos_repetidos(df, intervalo_maximo=5):
|
|
62
|
+
df['ultimo_acesso'] = pd.to_datetime(df['ultimo_acesso'])
|
|
63
|
+
df['diferenca'] = df['ultimo_acesso'].diff().fillna(pd.Timedelta(seconds=0))
|
|
64
|
+
|
|
65
|
+
acessos_repetidos = df[df['diferenca'] < pd.Timedelta(minutes=intervalo_maximo)]
|
|
66
|
+
|
|
67
|
+
if not acessos_repetidos.empty:
|
|
68
|
+
print(f"\nAcessos repetidos detectados em intervalos menores que {intervalo_maximo} minutos:")
|
|
69
|
+
print(acessos_repetidos[['url', 'ultimo_acesso', 'diferenca']].head())
|
|
70
|
+
|
|
71
|
+
def processar_historico_da_pasta(pasta, output_dir=None):
|
|
72
|
+
arquivos_json = [f for f in os.listdir(pasta) if f.endswith('.json')]
|
|
73
|
+
|
|
74
|
+
if not arquivos_json:
|
|
75
|
+
print("[ALERTA] Nenhum arquivo JSON encontrado na pasta especificada.")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
if output_dir:
|
|
79
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
80
|
+
|
|
81
|
+
for arquivo in arquivos_json:
|
|
82
|
+
caminho_arquivo = os.path.join(pasta, arquivo)
|
|
83
|
+
try:
|
|
84
|
+
print(f"\n[INFO] Processando arquivo: {caminho_arquivo}")
|
|
85
|
+
|
|
86
|
+
dados = carregar_json(caminho_arquivo)
|
|
87
|
+
|
|
88
|
+
if "historico_completo" in dados:
|
|
89
|
+
lista_acessos = dados["historico_completo"]
|
|
90
|
+
elif "ultimas_10_urls" in dados:
|
|
91
|
+
lista_acessos = dados["ultimas_10_urls"]
|
|
92
|
+
else:
|
|
93
|
+
print(f"[AVISO] Nenhum histórico válido encontrado em {arquivo}")
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
df = pd.DataFrame(lista_acessos).copy()
|
|
97
|
+
|
|
98
|
+
prefixo = os.path.splitext(arquivo)[0] + "_"
|
|
99
|
+
detectar_acessos_horarios_diferentes(df, output_dir=output_dir, arquivo_prefixo=prefixo)
|
|
100
|
+
detectar_acessos_repetidos(df, intervalo_maximo=5)
|
|
101
|
+
|
|
102
|
+
except Exception as erro:
|
|
103
|
+
print(f"\n❌ Erro ao processar o arquivo {arquivo}: {erro}")
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
try:
|
|
107
|
+
pasta_json = "artefatos/historico"
|
|
108
|
+
output_dir = "artefatos/patterns_output"
|
|
109
|
+
processar_historico_da_pasta(pasta_json, output_dir=output_dir)
|
|
110
|
+
except Exception as erro:
|
|
111
|
+
print(f"\n❌ Erro geral: {erro}")
|
core/data/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import csv
|
|
4
|
+
import platform
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
import shutil
|
|
7
|
+
|
|
8
|
+
IS_WINDOWS = platform.system() == "Windows"
|
|
9
|
+
RECOVERY_DIR = os.path.join(os.getcwd(), "recovered_files")
|
|
10
|
+
os.makedirs(RECOVERY_DIR, exist_ok=True)
|
|
11
|
+
|
|
12
|
+
FILE_FILTERS = {
|
|
13
|
+
"imagens": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff"],
|
|
14
|
+
"documentos": [".txt", ".doc", ".docx", ".odt", ".json"],
|
|
15
|
+
"pdfs": [".pdf"],
|
|
16
|
+
"planilhas": [".xls", ".xlsx", ".ods", ".csv"]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
ACTIVE_FILTERS = FILE_FILTERS["imagens"] + FILE_FILTERS["documentos"] + FILE_FILTERS["pdfs"] + FILE_FILTERS["planilhas"]
|
|
20
|
+
|
|
21
|
+
if IS_WINDOWS:
|
|
22
|
+
try:
|
|
23
|
+
import winshell
|
|
24
|
+
WIN_AVAILABLE = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
WIN_AVAILABLE = False
|
|
27
|
+
print("⚠️ winshell não disponível. Módulo de recuperação Windows desativado.")
|
|
28
|
+
else:
|
|
29
|
+
WIN_AVAILABLE = False
|
|
30
|
+
|
|
31
|
+
def filter_file(filename):
|
|
32
|
+
if not ACTIVE_FILTERS:
|
|
33
|
+
return True
|
|
34
|
+
_, ext = os.path.splitext(filename.lower())
|
|
35
|
+
return ext in ACTIVE_FILTERS
|
|
36
|
+
|
|
37
|
+
def list_deleted_files_windows():
|
|
38
|
+
deleted_files = []
|
|
39
|
+
if not WIN_AVAILABLE:
|
|
40
|
+
return deleted_files
|
|
41
|
+
try:
|
|
42
|
+
for item in winshell.recycle_bin():
|
|
43
|
+
try:
|
|
44
|
+
original_path = item.original_filename()
|
|
45
|
+
name = os.path.basename(original_path)
|
|
46
|
+
if not filter_file(name):
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
deleted_files.append({
|
|
50
|
+
"original_path": original_path,
|
|
51
|
+
"recycle_path": item.filename(),
|
|
52
|
+
"name": name,
|
|
53
|
+
"deleted_time": item.recycle_date().isoformat(),
|
|
54
|
+
"hidden": False
|
|
55
|
+
})
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(f"⚠️ Falha ao processar item da Lixeira: {e}")
|
|
58
|
+
print(f"[INFO] {len(deleted_files)} arquivo(s) encontrados na Lixeira com filtro aplicado.")
|
|
59
|
+
except Exception as e:
|
|
60
|
+
print(f"⚠️ Erro ao acessar a Lixeira: {e}")
|
|
61
|
+
return deleted_files
|
|
62
|
+
|
|
63
|
+
def recover_file_windows(file_info):
|
|
64
|
+
try:
|
|
65
|
+
src = file_info["recycle_path"]
|
|
66
|
+
dst = os.path.join(RECOVERY_DIR, file_info["name"])
|
|
67
|
+
shutil.copy2(src, dst)
|
|
68
|
+
print(f"[OK] Arquivo recuperado: {file_info['name']}")
|
|
69
|
+
return True
|
|
70
|
+
except Exception as e:
|
|
71
|
+
print(f"[ERRO] Falha ao recuperar {file_info['name']}: {e}")
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
import pytsk3
|
|
76
|
+
PYTSK3_AVAILABLE = True
|
|
77
|
+
except ImportError:
|
|
78
|
+
PYTSK3_AVAILABLE = False
|
|
79
|
+
if not IS_WINDOWS:
|
|
80
|
+
print("⚠️ pytsk3 não instalado. Recuperação Linux desativada.")
|
|
81
|
+
|
|
82
|
+
def list_deleted_files_linux(partition="/"):
|
|
83
|
+
deleted_files = []
|
|
84
|
+
if not PYTSK3_AVAILABLE:
|
|
85
|
+
return deleted_files
|
|
86
|
+
try:
|
|
87
|
+
img = pytsk3.Img_Info(partition)
|
|
88
|
+
fs = pytsk3.FS_Info(img)
|
|
89
|
+
directory = fs.open_dir(path="/")
|
|
90
|
+
for entry in directory:
|
|
91
|
+
try:
|
|
92
|
+
if entry.info.meta and entry.info.meta.flags & pytsk3.TSK_FS_META_FLAG_UNALLOC:
|
|
93
|
+
name = entry.info.name.name.decode("utf-8")
|
|
94
|
+
if not filter_file(name):
|
|
95
|
+
continue
|
|
96
|
+
deleted_files.append({
|
|
97
|
+
"name": name,
|
|
98
|
+
"inode": entry.info.meta.addr,
|
|
99
|
+
"size": entry.info.meta.size,
|
|
100
|
+
"deleted_time": datetime.fromtimestamp(entry.info.meta.mtime).isoformat()
|
|
101
|
+
})
|
|
102
|
+
except:
|
|
103
|
+
continue
|
|
104
|
+
print(f"[INFO] {len(deleted_files)} arquivo(s) deletados encontrados na partição {partition} com filtro aplicado.")
|
|
105
|
+
except Exception as e:
|
|
106
|
+
print(f"[ERRO] Não foi possível acessar a partição {partition}: {e}")
|
|
107
|
+
return deleted_files
|
|
108
|
+
|
|
109
|
+
def recover_file_linux(file_info, partition="/"):
|
|
110
|
+
dst = os.path.join(RECOVERY_DIR, file_info["name"])
|
|
111
|
+
try:
|
|
112
|
+
with open(dst, "w") as f:
|
|
113
|
+
f.write("Recuperação simulada para Linux")
|
|
114
|
+
print(f"[OK] Arquivo simulado recuperado: {file_info['name']}")
|
|
115
|
+
return True
|
|
116
|
+
except Exception as e:
|
|
117
|
+
print(f"[ERRO] Falha ao recuperar {file_info['name']}: {e}")
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
def save_recovery_results(results, prefix="data_recovery"):
|
|
121
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
122
|
+
json_file = f"{prefix}_{timestamp}.json"
|
|
123
|
+
csv_file = f"{prefix}_{timestamp}.csv"
|
|
124
|
+
|
|
125
|
+
if not results:
|
|
126
|
+
results.append({"info": "Nenhum arquivo encontrado"})
|
|
127
|
+
|
|
128
|
+
with open(json_file, "w", encoding="utf-8") as f:
|
|
129
|
+
json.dump(results, f, indent=4, ensure_ascii=False)
|
|
130
|
+
|
|
131
|
+
headers = results[0].keys()
|
|
132
|
+
with open(csv_file, "w", newline="", encoding="utf-8") as f:
|
|
133
|
+
writer = csv.DictWriter(f, fieldnames=headers)
|
|
134
|
+
writer.writeheader()
|
|
135
|
+
for r in results:
|
|
136
|
+
writer.writerow(r)
|
|
137
|
+
|
|
138
|
+
print(f"\n✅ Resultados salvos em JSON: {json_file} e CSV: {csv_file}")
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
recovered_results = []
|
|
142
|
+
|
|
143
|
+
if IS_WINDOWS:
|
|
144
|
+
files = list_deleted_files_windows()
|
|
145
|
+
for f in files:
|
|
146
|
+
status = recover_file_windows(f)
|
|
147
|
+
f["recovered"] = "Sim" if status else "Falha"
|
|
148
|
+
recovered_results.append(f)
|
|
149
|
+
else:
|
|
150
|
+
partition = input("Digite a partição ou device para recuperar arquivos (ex: /dev/sda1): ").strip()
|
|
151
|
+
files = list_deleted_files_linux(partition)
|
|
152
|
+
for f in files:
|
|
153
|
+
status = recover_file_linux(f, partition)
|
|
154
|
+
f["recovered"] = "Sim" if status else "Falha"
|
|
155
|
+
recovered_results.append(f)
|
|
156
|
+
|
|
157
|
+
save_recovery_results(recovered_results)
|
core/db/db.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from sqlalchemy import create_engine
|
|
2
|
+
from sqlalchemy.orm import sessionmaker, declarative_base
|
|
3
|
+
|
|
4
|
+
SQLALCHEMY_DATABASE_URL = "sqlite:///./devkit.db"
|
|
5
|
+
|
|
6
|
+
engine = create_engine(
|
|
7
|
+
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
11
|
+
|
|
12
|
+
Base = declarative_base()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import csv
|
|
4
|
+
import email
|
|
5
|
+
from email import policy
|
|
6
|
+
from email.parser import BytesParser
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
import hashlib
|
|
9
|
+
|
|
10
|
+
BASE_DIR = os.getcwd()
|
|
11
|
+
ATTACHMENT_DIR = os.path.join(BASE_DIR, "attachments")
|
|
12
|
+
os.makedirs(ATTACHMENT_DIR, exist_ok=True)
|
|
13
|
+
|
|
14
|
+
def hash_file(filepath, method="md5"):
|
|
15
|
+
h = hashlib.md5() if method == "md5" else hashlib.sha256()
|
|
16
|
+
with open(filepath, "rb") as f:
|
|
17
|
+
for chunk in iter(lambda: f.read(4096), b""):
|
|
18
|
+
h.update(chunk)
|
|
19
|
+
return h.hexdigest()
|
|
20
|
+
|
|
21
|
+
def parse_eml_file(filepath):
|
|
22
|
+
with open(filepath, "rb") as f:
|
|
23
|
+
msg = BytesParser(policy=policy.default).parse(f)
|
|
24
|
+
|
|
25
|
+
email_data = {
|
|
26
|
+
"subject": msg.get("subject"),
|
|
27
|
+
"from": msg.get("from"),
|
|
28
|
+
"to": msg.get("to"),
|
|
29
|
+
"cc": msg.get("cc"),
|
|
30
|
+
"bcc": msg.get("bcc"),
|
|
31
|
+
"date": msg.get("date"),
|
|
32
|
+
"message_id": msg.get("message-id"),
|
|
33
|
+
"attachments": []
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for part in msg.iter_attachments():
|
|
37
|
+
filename = part.get_filename()
|
|
38
|
+
if filename:
|
|
39
|
+
safe_filename = filename.replace("/", "_").replace("\\", "_")
|
|
40
|
+
filepath = os.path.join(ATTACHMENT_DIR, safe_filename)
|
|
41
|
+
with open(filepath, "wb") as af:
|
|
42
|
+
af.write(part.get_payload(decode=True))
|
|
43
|
+
email_data["attachments"].append({
|
|
44
|
+
"name": safe_filename,
|
|
45
|
+
"size": os.path.getsize(filepath),
|
|
46
|
+
"hash_md5": hash_file(filepath)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
return email_data
|
|
50
|
+
|
|
51
|
+
def parse_eml_folder(folder_path):
|
|
52
|
+
results = []
|
|
53
|
+
for root, _, files in os.walk(folder_path):
|
|
54
|
+
for file in files:
|
|
55
|
+
if file.lower().endswith(".eml"):
|
|
56
|
+
filepath = os.path.join(root, file)
|
|
57
|
+
try:
|
|
58
|
+
data = parse_eml_file(filepath)
|
|
59
|
+
results.append(data)
|
|
60
|
+
print(f"[OK] E-mail processado: {file}")
|
|
61
|
+
except Exception as e:
|
|
62
|
+
print(f"[ERRO] Falha ao processar {file}: {e}")
|
|
63
|
+
return results
|
|
64
|
+
|
|
65
|
+
def export_results(results, prefix="email_analysis"):
|
|
66
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
67
|
+
json_file = f"{prefix}_{timestamp}.json"
|
|
68
|
+
csv_file = f"{prefix}_{timestamp}.csv"
|
|
69
|
+
|
|
70
|
+
with open(json_file, "w", encoding="utf-8") as f:
|
|
71
|
+
json.dump(results, f, indent=4, ensure_ascii=False)
|
|
72
|
+
|
|
73
|
+
headers = ["subject", "from", "to", "cc", "bcc", "date", "message_id", "attachments"]
|
|
74
|
+
with open(csv_file, "w", newline="", encoding="utf-8") as f:
|
|
75
|
+
writer = csv.DictWriter(f, fieldnames=headers)
|
|
76
|
+
writer.writeheader()
|
|
77
|
+
for r in results:
|
|
78
|
+
r_copy = r.copy()
|
|
79
|
+
r_copy["attachments"] = ", ".join([a["name"] for a in r["attachments"]])
|
|
80
|
+
writer.writerow(r_copy)
|
|
81
|
+
|
|
82
|
+
print(f"\n✅ Resultados salvos em JSON: {json_file} e CSV: {csv_file}")
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
folder = input("Digite o caminho da pasta com arquivos .eml: ").strip()
|
|
86
|
+
results = parse_eml_folder(folder)
|
|
87
|
+
export_results(results)
|