forensic-cli 1.4.6__tar.gz → 1.5.3__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.
- {forensic_cli-1.4.6/src/forensic_cli.egg-info → forensic_cli-1.5.3}/PKG-INFO +4 -6
- {forensic_cli-1.4.6 → forensic_cli-1.5.3}/pyproject.toml +35 -6
- forensic_cli-1.5.3/src/api/main.py +29 -0
- forensic_cli-1.5.3/src/api/routers/__init__.py +1 -0
- forensic_cli-1.5.3/src/api/routers/executions.py +56 -0
- forensic_cli-1.5.3/src/api/routers/functionalities.py +101 -0
- forensic_cli-1.5.3/src/api/routers/modules.py +24 -0
- forensic_cli-1.5.3/src/api/routers/network.py +21 -0
- forensic_cli-1.5.3/src/api/routers/reports.py +24 -0
- forensic_cli-1.5.3/src/api/routers/results.py +29 -0
- forensic_cli-1.5.3/src/api/utils/db.py +9 -0
- forensic_cli-1.5.3/src/app/main.py +75 -0
- forensic_cli-1.5.3/src/app/utils/converter.py +144 -0
- forensic_cli-1.5.3/src/browser/__init__.py +1 -0
- forensic_cli-1.5.3/src/browser/api.py +39 -0
- {forensic_cli-1.4.6/src/core → forensic_cli-1.5.3/src}/browser/browser_history.py +50 -47
- forensic_cli-1.4.6/src/cli/commands/browser.py → forensic_cli-1.5.3/src/browser/cli.py +127 -52
- {forensic_cli-1.4.6/src/core → forensic_cli-1.5.3/src}/browser/common_words.py +8 -6
- {forensic_cli-1.4.6/src/core → forensic_cli-1.5.3/src}/browser/downloads_history.py +31 -18
- {forensic_cli-1.4.6/src/core → forensic_cli-1.5.3/src}/browser/fav_screen.py +12 -6
- {forensic_cli-1.4.6/src/core → forensic_cli-1.5.3/src}/browser/logins.py +28 -20
- {forensic_cli-1.4.6/src/core → forensic_cli-1.5.3/src}/browser/unusual_patterns.py +38 -26
- {forensic_cli-1.4.6 → forensic_cli-1.5.3/src/forensic_cli.egg-info}/PKG-INFO +4 -6
- forensic_cli-1.5.3/src/forensic_cli.egg-info/SOURCES.txt +63 -0
- forensic_cli-1.5.3/src/forensic_cli.egg-info/entry_points.txt +2 -0
- {forensic_cli-1.4.6 → forensic_cli-1.5.3}/src/forensic_cli.egg-info/requires.txt +4 -0
- forensic_cli-1.5.3/src/forensic_cli.egg-info/top_level.txt +6 -0
- forensic_cli-1.5.3/src/mail/__init__.py +1 -0
- forensic_cli-1.5.3/src/mail/api.py +37 -0
- forensic_cli-1.5.3/src/mail/cli.py +125 -0
- {forensic_cli-1.4.6/src/core/email → forensic_cli-1.5.3/src/mail}/email_parser.py +18 -13
- {forensic_cli-1.4.6/src/core/email → forensic_cli-1.5.3/src/mail}/header_analysis.py +6 -2
- forensic_cli-1.5.3/src/network/__init__.py +1 -0
- forensic_cli-1.5.3/src/network/api.py +141 -0
- {forensic_cli-1.4.6/src/core → forensic_cli-1.5.3/src}/network/arp_scan.py +5 -2
- forensic_cli-1.5.3/src/network/cli.py +355 -0
- {forensic_cli-1.4.6/src/core → forensic_cli-1.5.3/src}/network/dns_recon.py +76 -19
- {forensic_cli-1.4.6/src/core → forensic_cli-1.5.3/src}/network/fingerprinting.py +23 -14
- {forensic_cli-1.4.6/src/core → forensic_cli-1.5.3/src}/network/ip_info.py +4 -2
- {forensic_cli-1.4.6/src/core → forensic_cli-1.5.3/src}/network/network_map.py +23 -16
- {forensic_cli-1.4.6/src/core → forensic_cli-1.5.3/src}/network/ping_sweep.py +8 -6
- {forensic_cli-1.4.6/src/core → forensic_cli-1.5.3/src}/network/port_scanner.py +48 -20
- {forensic_cli-1.4.6/src/core → forensic_cli-1.5.3/src}/network/smb_scan.py +3 -2
- {forensic_cli-1.4.6/src/core → forensic_cli-1.5.3/src}/network/snmp_scan.py +5 -4
- {forensic_cli-1.4.6/src/core → forensic_cli-1.5.3/src}/network/traceroute.py +7 -4
- {forensic_cli-1.4.6/src/core → forensic_cli-1.5.3/src}/network/utils.py +7 -3
- forensic_cli-1.5.3/src/shared/__init__.py +1 -0
- forensic_cli-1.5.3/src/shared/config.py +139 -0
- {forensic_cli-1.4.6/src/core/db → forensic_cli-1.5.3/src/shared}/db.py +2 -4
- {forensic_cli-1.4.6/src/cli/commands → forensic_cli-1.5.3/src/shared}/describe.py +22 -9
- forensic_cli-1.5.3/src/shared/exporter.py +156 -0
- forensic_cli-1.5.3/src/shared/logger.py +123 -0
- {forensic_cli-1.4.6/src/core/models → forensic_cli-1.5.3/src/shared}/orm.py +15 -4
- {forensic_cli-1.4.6/src/core → forensic_cli-1.5.3/src/shared}/registry.py +11 -5
- {forensic_cli-1.4.6/src/core/models → forensic_cli-1.5.3/src/shared}/schemas.py +23 -2
- forensic_cli-1.5.3/tests/test_file_inspector.py +25 -0
- forensic_cli-1.4.6/setup.py +0 -19
- forensic_cli-1.4.6/src/cli/commands/email.py +0 -3
- forensic_cli-1.4.6/src/cli/commands/network.py +0 -187
- forensic_cli-1.4.6/src/cli/main.py +0 -18
- forensic_cli-1.4.6/src/core/__init__.py +0 -0
- forensic_cli-1.4.6/src/core/data/__init__.py +0 -0
- forensic_cli-1.4.6/src/core/data/data_recovery.py +0 -157
- forensic_cli-1.4.6/src/core/network/__init__.py +0 -0
- forensic_cli-1.4.6/src/core/utils/__init__.py +0 -0
- forensic_cli-1.4.6/src/forensic_cli.egg-info/SOURCES.txt +0 -49
- forensic_cli-1.4.6/src/forensic_cli.egg-info/entry_points.txt +0 -2
- forensic_cli-1.4.6/src/forensic_cli.egg-info/top_level.txt +0 -2
- forensic_cli-1.4.6/tests/test_file_inspector.py +0 -17
- {forensic_cli-1.4.6 → forensic_cli-1.5.3}/LICENSE +0 -0
- {forensic_cli-1.4.6 → forensic_cli-1.5.3}/LICENSE-GPL +0 -0
- {forensic_cli-1.4.6 → forensic_cli-1.5.3}/README.md +0 -0
- {forensic_cli-1.4.6 → forensic_cli-1.5.3}/setup.cfg +0 -0
- {forensic_cli-1.4.6/src/cli → forensic_cli-1.5.3/src/app}/__init__.py +0 -0
- {forensic_cli-1.4.6/src/cli/commands → forensic_cli-1.5.3/src/app/utils}/__init__.py +0 -0
- {forensic_cli-1.4.6 → forensic_cli-1.5.3}/src/forensic_cli.egg-info/dependency_links.txt +0 -0
- {forensic_cli-1.4.6/src/core/utils → forensic_cli-1.5.3/src/shared}/gerar_amostras.py +0 -0
- /forensic_cli-1.4.6/src/cli/commands/utils.py → /forensic_cli-1.5.3/src/shared/utils_cmd.py +0 -0
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: forensic-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.3
|
|
4
4
|
Summary: DevKit Forense - CLI para análise de navegadores e rede
|
|
5
|
-
Home-page: https://github.com/ErickG123/devkit_forense
|
|
6
|
-
Author: Erick Gabriel dos Santos Alves
|
|
7
5
|
Author-email: Erick Gabriel dos Santos Alves <erickgabrielalves0@gmail.com>
|
|
8
6
|
License: MIT
|
|
9
7
|
Project-URL: Changelog, https://erickg123.github.io/devkit_forense/
|
|
@@ -88,10 +86,10 @@ Requires-Dist: urllib3==2.5.0
|
|
|
88
86
|
Requires-Dist: websocket-client==1.8.0
|
|
89
87
|
Requires-Dist: wsproto==1.2.0
|
|
90
88
|
Requires-Dist: yarg==0.1.10
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
Provides-Extra: dev
|
|
90
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
91
|
+
Requires-Dist: ruff>=0.3.0; extra == "dev"
|
|
93
92
|
Dynamic: license-file
|
|
94
|
-
Dynamic: requires-python
|
|
95
93
|
|
|
96
94
|
# DevKit Forense – Ferramenta Educacional de Perícia Digital
|
|
97
95
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "forensic-cli"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.5.3"
|
|
8
8
|
description = "DevKit Forense - CLI para análise de navegadores e rede"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -13,7 +13,6 @@ authors = [
|
|
|
13
13
|
]
|
|
14
14
|
requires-python = ">=3.11"
|
|
15
15
|
keywords = ["forensics", "cli", "network", "browser"]
|
|
16
|
-
|
|
17
16
|
dependencies = [
|
|
18
17
|
"altgraph==0.17.4",
|
|
19
18
|
"annotated-types==0.7.0",
|
|
@@ -91,14 +90,44 @@ dependencies = [
|
|
|
91
90
|
"yarg==0.1.10"
|
|
92
91
|
]
|
|
93
92
|
|
|
94
|
-
[
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
[project.optional-dependencies]
|
|
94
|
+
dev = [
|
|
95
|
+
"pytest>=8.0.0",
|
|
96
|
+
"ruff>=0.3.0"
|
|
97
|
+
]
|
|
97
98
|
|
|
98
99
|
[project.scripts]
|
|
99
|
-
forensic-cli = "
|
|
100
|
+
forensic-cli = "main:_build_cli"
|
|
100
101
|
|
|
101
102
|
[project.urls]
|
|
102
103
|
Changelog = "https://erickg123.github.io/devkit_forense/"
|
|
103
104
|
Source = "https://github.com/ErickG123/devkit_forense"
|
|
104
105
|
Issues = "https://github.com/ErickG123/devkit_forense/issues"
|
|
106
|
+
|
|
107
|
+
[tool.setuptools.packages.find]
|
|
108
|
+
where = ["src"]
|
|
109
|
+
include = ["network*", "browser*", "mail*", "shared*", "api*", "app*"]
|
|
110
|
+
|
|
111
|
+
[tool.ruff]
|
|
112
|
+
line-length = 100
|
|
113
|
+
target-version = "py311"
|
|
114
|
+
|
|
115
|
+
[tool.ruff.lint]
|
|
116
|
+
# Habilita checagens de Erros, Funções, Isort (ordenação de imports) e Warnings
|
|
117
|
+
select = ["E", "F", "I", "W"]
|
|
118
|
+
ignore = []
|
|
119
|
+
|
|
120
|
+
[tool.pytest.ini_options]
|
|
121
|
+
testpaths = ["tests"]
|
|
122
|
+
pythonpath = ["src"]
|
|
123
|
+
|
|
124
|
+
[tool.commitizen]
|
|
125
|
+
name = "cz_conventional_commits"
|
|
126
|
+
version = "1.5.3"
|
|
127
|
+
version_files = [
|
|
128
|
+
"pyproject.toml:version",
|
|
129
|
+
"src/shared/__init__.py:__version__"
|
|
130
|
+
]
|
|
131
|
+
tag_format = "v$version"
|
|
132
|
+
update_changelog_on_bump = true
|
|
133
|
+
changelog_incremental = true
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
3
|
+
|
|
4
|
+
from api.routers import network
|
|
5
|
+
|
|
6
|
+
app = FastAPI(
|
|
7
|
+
title="ForenseLab API",
|
|
8
|
+
description="API REST do DevKit Forense para análise de evidências digitais.",
|
|
9
|
+
version="1.0.0",
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# Configuração de CORS para permitir consumo pelo Dashboard SPA
|
|
13
|
+
app.add_middleware(
|
|
14
|
+
CORSMiddleware,
|
|
15
|
+
allow_origins=["*"], # Em produção, restringir para o domínio da Vercel
|
|
16
|
+
allow_credentials=True,
|
|
17
|
+
allow_methods=["*"],
|
|
18
|
+
allow_headers=["*"],
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Registro dos Roteadores baseados em features
|
|
22
|
+
app.include_router(network.router, prefix="/api/network", tags=["Network"])
|
|
23
|
+
# app.include_router(browser.router, prefix="/api/browser", tags=["Browser"])
|
|
24
|
+
# app.include_router(email.router, prefix="/api/email", tags=["Email"])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.get("/")
|
|
28
|
+
def root():
|
|
29
|
+
return {"message": "ForenseLab API está online e operante!"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# API routers package
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
4
|
+
from sqlalchemy.orm import Session
|
|
5
|
+
|
|
6
|
+
import shared.orm as orm
|
|
7
|
+
from api.utils.db import get_db
|
|
8
|
+
from shared.schemas import ExecutionCreateSchema, ExecutionSchema
|
|
9
|
+
|
|
10
|
+
router = APIRouter(prefix="/executions", tags=["Executions"])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.post("/start", response_model=ExecutionSchema)
|
|
14
|
+
def start_execution(payload: ExecutionCreateSchema, db: Session = Depends(get_db)):
|
|
15
|
+
func = db.query(orm.Functionality).filter(orm.Functionality.id == payload.func_id).first()
|
|
16
|
+
if not func:
|
|
17
|
+
raise HTTPException(status_code=404, detail="Funcionalidade não encontrada")
|
|
18
|
+
|
|
19
|
+
execution = orm.Execution(
|
|
20
|
+
functionality_id=func.id,
|
|
21
|
+
network=payload.network,
|
|
22
|
+
status="started",
|
|
23
|
+
cli_version=payload.cli_version,
|
|
24
|
+
)
|
|
25
|
+
db.add(execution)
|
|
26
|
+
db.commit()
|
|
27
|
+
db.refresh(execution)
|
|
28
|
+
|
|
29
|
+
return execution
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@router.post("/finish", response_model=ExecutionSchema)
|
|
33
|
+
def finish_execution(payload: ExecutionCreateSchema, db: Session = Depends(get_db)):
|
|
34
|
+
execution = (
|
|
35
|
+
db.query(orm.Execution)
|
|
36
|
+
.filter(
|
|
37
|
+
orm.Execution.functionality_id == payload.func_id,
|
|
38
|
+
orm.Execution.network == payload.network,
|
|
39
|
+
orm.Execution.status == "started",
|
|
40
|
+
)
|
|
41
|
+
.first()
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if not execution:
|
|
45
|
+
raise HTTPException(status_code=404, detail="Execução não encontrada")
|
|
46
|
+
|
|
47
|
+
execution.status = "finished"
|
|
48
|
+
execution.finished_at = datetime.utcnow()
|
|
49
|
+
execution.cli_version = payload.cli_version
|
|
50
|
+
|
|
51
|
+
result = orm.Result(execution_id=execution.id, data=payload.data)
|
|
52
|
+
db.add(result)
|
|
53
|
+
db.commit()
|
|
54
|
+
db.refresh(execution)
|
|
55
|
+
|
|
56
|
+
return execution
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
|
7
|
+
from sqlalchemy.orm import Session
|
|
8
|
+
|
|
9
|
+
import shared.orm as orm
|
|
10
|
+
import shared.schemas as schemas
|
|
11
|
+
from api.utils.db import get_db
|
|
12
|
+
from shared.db import SessionLocal
|
|
13
|
+
from shared.registry import FUNCTIONALITIES
|
|
14
|
+
|
|
15
|
+
router = APIRouter(prefix="/functionalities", tags=["Functionalities"])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.post("/", response_model=schemas.Functionality)
|
|
19
|
+
def create_functionality(functionality: schemas.FunctionalityCreate, db: Session = Depends(get_db)):
|
|
20
|
+
db_func = orm.Functionality(
|
|
21
|
+
name=functionality.name,
|
|
22
|
+
description=functionality.description,
|
|
23
|
+
module_id=functionality.module_id,
|
|
24
|
+
)
|
|
25
|
+
db.add(db_func)
|
|
26
|
+
db.commit()
|
|
27
|
+
db.refresh(db_func)
|
|
28
|
+
return db_func
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@router.get("/", response_model=list[schemas.Functionality])
|
|
32
|
+
def get_functionalities(db: Session = Depends(get_db)):
|
|
33
|
+
return db.query(orm.Functionality).all()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@router.get("/{func_id}/results", response_model=list[schemas.Result])
|
|
37
|
+
def get_results_by_functionality(func_id: str, db: Session = Depends(get_db)):
|
|
38
|
+
executions = db.query(orm.Execution).filter(orm.Execution.functionality_id == func_id).all()
|
|
39
|
+
results = []
|
|
40
|
+
for exe in executions:
|
|
41
|
+
if exe.result:
|
|
42
|
+
results.append(exe.result)
|
|
43
|
+
return results
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@router.post("/{func_id}/run")
|
|
47
|
+
def run_functionality(
|
|
48
|
+
func_id: str,
|
|
49
|
+
network: str = None,
|
|
50
|
+
background_tasks: BackgroundTasks = None,
|
|
51
|
+
usuario: str = "Usuário Anônimo",
|
|
52
|
+
db: Session = Depends(get_db),
|
|
53
|
+
):
|
|
54
|
+
func_obj = db.query(orm.Functionality).filter(orm.Functionality.id == func_id).first()
|
|
55
|
+
if not func_obj:
|
|
56
|
+
raise HTTPException(status_code=404, detail="Funcionalidade não encontrada")
|
|
57
|
+
|
|
58
|
+
func_name = func_obj.name
|
|
59
|
+
if func_name not in FUNCTIONALITIES:
|
|
60
|
+
raise HTTPException(status_code=400, detail=f"Funcionalidade '{func_name}' não registrada")
|
|
61
|
+
|
|
62
|
+
def execute_core():
|
|
63
|
+
try:
|
|
64
|
+
output_dir = Path(__file__).parent.parent / "cli_output"
|
|
65
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
66
|
+
|
|
67
|
+
func = FUNCTIONALITIES[func_name]
|
|
68
|
+
|
|
69
|
+
if func_name == "network_map":
|
|
70
|
+
result_data = func(network, str(output_dir))
|
|
71
|
+
else:
|
|
72
|
+
result_data = func(usuario=usuario, output_dir=str(output_dir))
|
|
73
|
+
|
|
74
|
+
db_session = SessionLocal()
|
|
75
|
+
execution = orm.Execution(
|
|
76
|
+
functionality_id=func_obj.id,
|
|
77
|
+
network=network or "N/A",
|
|
78
|
+
status="started",
|
|
79
|
+
started_at=datetime.utcnow(),
|
|
80
|
+
)
|
|
81
|
+
db_session.add(execution)
|
|
82
|
+
db_session.commit()
|
|
83
|
+
db_session.refresh(execution)
|
|
84
|
+
|
|
85
|
+
result = orm.Result(
|
|
86
|
+
execution_id=execution.id,
|
|
87
|
+
data=json.dumps(result_data, ensure_ascii=False),
|
|
88
|
+
created_at=datetime.utcnow(),
|
|
89
|
+
)
|
|
90
|
+
db_session.add(result)
|
|
91
|
+
db_session.commit()
|
|
92
|
+
db_session.refresh(result)
|
|
93
|
+
db_session.close()
|
|
94
|
+
|
|
95
|
+
print(f"✅ {func_name} executado com sucesso!")
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
print(f"❌ Erro ao executar {func_name}: {e}")
|
|
99
|
+
|
|
100
|
+
background_tasks.add_task(execute_core)
|
|
101
|
+
return {"status": "execução iniciada em background", "funcionalidade": func_name}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends
|
|
4
|
+
from sqlalchemy.orm import Session
|
|
5
|
+
|
|
6
|
+
import shared.orm as orm
|
|
7
|
+
import shared.schemas as schemas
|
|
8
|
+
from api.utils.db import get_db
|
|
9
|
+
|
|
10
|
+
router = APIRouter(prefix="/modules", tags=["Modules"])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.post("/", response_model=schemas.Module)
|
|
14
|
+
def create_module(module: schemas.ModuleCreate, db: Session = Depends(get_db)):
|
|
15
|
+
db_module = orm.Module(name=module.name, description=module.description)
|
|
16
|
+
db.add(db_module)
|
|
17
|
+
db.commit()
|
|
18
|
+
db.refresh(db_module)
|
|
19
|
+
return db_module
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@router.get("/", response_model=List[schemas.Module])
|
|
23
|
+
def get_modules(db: Session = Depends(get_db)):
|
|
24
|
+
return db.query(orm.Module).all()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
from network.port_scanner import scan_host
|
|
5
|
+
|
|
6
|
+
router = APIRouter()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ScanRequest(BaseModel):
|
|
10
|
+
target: str
|
|
11
|
+
ports: str = "21,22,80,443,8080"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@router.post("/scan")
|
|
15
|
+
def run_port_scan(request: ScanRequest):
|
|
16
|
+
try:
|
|
17
|
+
# A chamada ao Core que agora retorna um dicionário limpo
|
|
18
|
+
results = scan_host(target=request.target, ports=request.ports)
|
|
19
|
+
return {"target": request.target, "status": "success", "data": results}
|
|
20
|
+
except Exception as e:
|
|
21
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends
|
|
4
|
+
from sqlalchemy.orm import Session
|
|
5
|
+
|
|
6
|
+
import shared.orm as orm
|
|
7
|
+
import shared.schemas as schemas
|
|
8
|
+
from api.utils.db import get_db
|
|
9
|
+
|
|
10
|
+
router = APIRouter(prefix="/reports", tags=["Reports"])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.post("/reports/", response_model=schemas.Report)
|
|
14
|
+
def create_report(report: schemas.ReportCreate, db: Session = Depends(get_db)):
|
|
15
|
+
db_report = orm.Report(result_id=report.result_id, file_path=report.file_path)
|
|
16
|
+
db.add(db_report)
|
|
17
|
+
db.commit()
|
|
18
|
+
db.refresh(db_report)
|
|
19
|
+
return db_report
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@router.get("/reports/", response_model=List[schemas.Report])
|
|
23
|
+
def get_reports(db: Session = Depends(get_db)):
|
|
24
|
+
return db.query(orm.Report).all()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends
|
|
4
|
+
from sqlalchemy.orm import Session
|
|
5
|
+
|
|
6
|
+
import shared.orm as orm
|
|
7
|
+
import shared.schemas as schemas
|
|
8
|
+
from api.utils.db import get_db
|
|
9
|
+
|
|
10
|
+
router = APIRouter(prefix="/results", tags=["Results"])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.post("/results/", response_model=schemas.Result)
|
|
14
|
+
def create_result(result: schemas.ResultCreate, db: Session = Depends(get_db)):
|
|
15
|
+
db_result = orm.Result(functionality_id=result.functionality_id, data=result.data)
|
|
16
|
+
db.add(db_result)
|
|
17
|
+
db.commit()
|
|
18
|
+
db.refresh(db_result)
|
|
19
|
+
return db_result
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@router.get("/results/", response_model=List[schemas.Result])
|
|
23
|
+
def get_results(db: Session = Depends(get_db)):
|
|
24
|
+
return db.query(orm.Result).all()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@router.get("/results/{result_id}/reports", response_model=List[schemas.Report])
|
|
28
|
+
def get_reports_by_result(result_id: str, db: Session = Depends(get_db)):
|
|
29
|
+
return db.query(orm.Report).filter(orm.Report.result_id == result_id).all()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from flask import Flask, current_app, flash, redirect, render_template, request, url_for
|
|
4
|
+
|
|
5
|
+
from .utils.converter import convert_hosts_to_graph
|
|
6
|
+
|
|
7
|
+
app = Flask(__name__, template_folder="templates", static_folder="static")
|
|
8
|
+
app.secret_key = "troque_essa_chave_em_producao"
|
|
9
|
+
app.config["MAX_CONTENT_LENGTH"] = 8 * 1024 * 1024
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.route("/", methods=["GET"])
|
|
13
|
+
def index():
|
|
14
|
+
return render_template("index.html", nodes_json="[]", edges_json="[]", filename=None)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.route("/upload", methods=["POST"])
|
|
18
|
+
def upload():
|
|
19
|
+
file = request.files.get("file")
|
|
20
|
+
if not file:
|
|
21
|
+
flash("Nenhum arquivo enviado.", "error")
|
|
22
|
+
return redirect(url_for("index"))
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
data = json.load(file)
|
|
26
|
+
except Exception as e:
|
|
27
|
+
flash(f"Erro ao parsear JSON: {e}", "error")
|
|
28
|
+
current_app.logger.exception("Erro ao parsear JSON no upload")
|
|
29
|
+
return redirect(url_for("index"))
|
|
30
|
+
|
|
31
|
+
hosts = []
|
|
32
|
+
if isinstance(data, list):
|
|
33
|
+
hosts = data
|
|
34
|
+
elif isinstance(data, dict):
|
|
35
|
+
candidates = [v for v in data.values() if isinstance(v, list)]
|
|
36
|
+
if len(candidates) == 1:
|
|
37
|
+
hosts = candidates[0]
|
|
38
|
+
else:
|
|
39
|
+
hosts = (
|
|
40
|
+
data.get("hosts")
|
|
41
|
+
or data.get("results")
|
|
42
|
+
or data.get("items")
|
|
43
|
+
or data.get("network")
|
|
44
|
+
or candidates[0]
|
|
45
|
+
or []
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
nodes, edges = convert_hosts_to_graph(hosts)
|
|
49
|
+
node_count = len(nodes)
|
|
50
|
+
edge_count = len(edges)
|
|
51
|
+
|
|
52
|
+
current_app.logger.info(
|
|
53
|
+
f"[UPLOAD] file={getattr(file, 'filename', None)} hosts_found={len(hosts)} nodes={node_count} edges={edge_count}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if node_count == 0:
|
|
57
|
+
flash(
|
|
58
|
+
"Upload processado, porém nenhum nó foi gerado. Verifique o formato do JSON (veja logs do servidor).",
|
|
59
|
+
"warning",
|
|
60
|
+
)
|
|
61
|
+
else:
|
|
62
|
+
flash(f"Upload OK — nodes: {node_count}, edges: {edge_count}", "success")
|
|
63
|
+
|
|
64
|
+
return render_template(
|
|
65
|
+
"index.html",
|
|
66
|
+
nodes_json=nodes,
|
|
67
|
+
edges_json=edges,
|
|
68
|
+
filename=getattr(file, "filename", None),
|
|
69
|
+
node_count=node_count,
|
|
70
|
+
edge_count=edge_count,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
app.run(debug=True, host="0.0.0.0", port=8080)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import html
|
|
2
|
+
import ipaddress
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def is_ip(addr: str) -> bool:
|
|
7
|
+
try:
|
|
8
|
+
ipaddress.ip_address(addr)
|
|
9
|
+
return True
|
|
10
|
+
except Exception:
|
|
11
|
+
return False
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def convert_hosts_to_graph(hosts):
|
|
15
|
+
nodes = []
|
|
16
|
+
edges = []
|
|
17
|
+
nodes_map = {}
|
|
18
|
+
|
|
19
|
+
def add_node(node_id, label=None, title=None, group="unknown", value=15, raw=None):
|
|
20
|
+
node_id = str(node_id)
|
|
21
|
+
if node_id in nodes_map:
|
|
22
|
+
return
|
|
23
|
+
node = {
|
|
24
|
+
"id": node_id,
|
|
25
|
+
"label": label or node_id,
|
|
26
|
+
"title": title or "",
|
|
27
|
+
"group": group,
|
|
28
|
+
"value": value,
|
|
29
|
+
}
|
|
30
|
+
if raw:
|
|
31
|
+
node["raw"] = raw
|
|
32
|
+
nodes.append(node)
|
|
33
|
+
nodes_map[node_id] = node
|
|
34
|
+
|
|
35
|
+
for item in hosts:
|
|
36
|
+
ip = (
|
|
37
|
+
item.get("host")
|
|
38
|
+
or item.get("ip")
|
|
39
|
+
or item.get("address")
|
|
40
|
+
or item.get("ipv4")
|
|
41
|
+
or item.get("ip_address")
|
|
42
|
+
)
|
|
43
|
+
if not ip:
|
|
44
|
+
tr = item.get("traceroute") or item.get("trace") or item.get("hops")
|
|
45
|
+
if isinstance(tr, list) and tr:
|
|
46
|
+
for hop in reversed(tr):
|
|
47
|
+
if isinstance(hop, dict):
|
|
48
|
+
hopip = hop.get("ip") or hop.get("addr") or hop.get("address")
|
|
49
|
+
if hopip and is_ip(str(hopip)):
|
|
50
|
+
ip = hopip
|
|
51
|
+
break
|
|
52
|
+
elif isinstance(hop, str) and is_ip(hop):
|
|
53
|
+
ip = hop
|
|
54
|
+
break
|
|
55
|
+
if not ip:
|
|
56
|
+
continue
|
|
57
|
+
ip = str(ip)
|
|
58
|
+
|
|
59
|
+
hostname = item.get("hostname") or item.get("host_name") or item.get("name") or ""
|
|
60
|
+
mac = item.get("mac") or item.get("mac_address") or ""
|
|
61
|
+
vendor = item.get("vendor") or item.get("oui") or ""
|
|
62
|
+
open_ports = item.get("open_ports") or item.get("ports") or []
|
|
63
|
+
os_info = item.get("os_info") or {}
|
|
64
|
+
os_name = os_info.get("os") if isinstance(os_info, dict) else ""
|
|
65
|
+
ip_info = item.get("ip_info") or {}
|
|
66
|
+
|
|
67
|
+
details = [f"IP: {html.escape(ip)}"]
|
|
68
|
+
if hostname:
|
|
69
|
+
details.append(f"Hostname: {html.escape(str(hostname))}")
|
|
70
|
+
if mac:
|
|
71
|
+
details.append(f"MAC: {html.escape(str(mac))}")
|
|
72
|
+
if vendor:
|
|
73
|
+
details.append(f"Vendor: {html.escape(str(vendor))}")
|
|
74
|
+
if os_name:
|
|
75
|
+
details.append(f"OS: {html.escape(str(os_name))}")
|
|
76
|
+
if isinstance(open_ports, list) and open_ports:
|
|
77
|
+
ports_summary = ", ".join(str(p) for p in open_ports[:8])
|
|
78
|
+
details.append(f"Open ports: {html.escape(ports_summary)} ({len(open_ports)})")
|
|
79
|
+
if isinstance(ip_info, dict) and ip_info:
|
|
80
|
+
geo = ip_info.get("geo") or ip_info.get("country") or ip_info.get("region")
|
|
81
|
+
if geo:
|
|
82
|
+
details.append(f"IP info: {html.escape(str(geo))}")
|
|
83
|
+
|
|
84
|
+
pretty_json = json.dumps(item, indent=2, ensure_ascii=False)
|
|
85
|
+
html_title = "\n".join(details)
|
|
86
|
+
|
|
87
|
+
size = 15 + min(30, int(len(open_ports)) * 2) if open_ports else 15
|
|
88
|
+
|
|
89
|
+
group = "unknown"
|
|
90
|
+
if os_name:
|
|
91
|
+
on = os_name.lower()
|
|
92
|
+
if "win" in on:
|
|
93
|
+
group = "windows"
|
|
94
|
+
elif "linux" in on:
|
|
95
|
+
group = "linux"
|
|
96
|
+
elif "android" in on:
|
|
97
|
+
group = "android"
|
|
98
|
+
elif "embedded" in on or "router" in on:
|
|
99
|
+
group = "embedded"
|
|
100
|
+
else:
|
|
101
|
+
group = "other"
|
|
102
|
+
|
|
103
|
+
add_node(
|
|
104
|
+
ip, label=hostname or ip, title=html_title, group=group, value=size, raw=pretty_json
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
traceroute = item.get("traceroute") or item.get("trace") or []
|
|
108
|
+
if isinstance(traceroute, list) and traceroute:
|
|
109
|
+
prev = None
|
|
110
|
+
for hop in traceroute:
|
|
111
|
+
hop_ip = None
|
|
112
|
+
if isinstance(hop, dict):
|
|
113
|
+
hop_ip = (
|
|
114
|
+
hop.get("ip") or hop.get("addr") or hop.get("address") or hop.get("host")
|
|
115
|
+
)
|
|
116
|
+
elif isinstance(hop, str):
|
|
117
|
+
hop_ip = hop
|
|
118
|
+
if not hop_ip:
|
|
119
|
+
continue
|
|
120
|
+
hop_ip = str(hop_ip)
|
|
121
|
+
if is_ip(hop_ip):
|
|
122
|
+
add_node(
|
|
123
|
+
hop_ip,
|
|
124
|
+
label=hop_ip,
|
|
125
|
+
title=f"Hop: {html.escape(hop_ip)}",
|
|
126
|
+
group="hop",
|
|
127
|
+
value=10,
|
|
128
|
+
)
|
|
129
|
+
if prev and is_ip(prev):
|
|
130
|
+
edges.append({"from": prev, "to": hop_ip})
|
|
131
|
+
prev = hop_ip
|
|
132
|
+
if prev and prev != ip:
|
|
133
|
+
edges.append({"from": prev, "to": ip})
|
|
134
|
+
|
|
135
|
+
if not edges and nodes:
|
|
136
|
+
hub_candidates = ["192.168.0.1", "192.168.1.1", "10.0.0.1"]
|
|
137
|
+
hub = next((c for c in hub_candidates if c in nodes_map), None)
|
|
138
|
+
if not hub:
|
|
139
|
+
hub = next(iter(nodes_map.keys()))
|
|
140
|
+
for nid in nodes_map:
|
|
141
|
+
if nid != hub:
|
|
142
|
+
edges.append({"from": hub, "to": nid})
|
|
143
|
+
|
|
144
|
+
return nodes, edges
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# browser feature package
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API Router da feature Browser.
|
|
3
|
+
|
|
4
|
+
Expõe endpoints REST para extração de artefatos de navegadores.
|
|
5
|
+
Os endpoints de extração são operações de longa duração — em produção
|
|
6
|
+
considere movê-los para tarefas assíncronas (ex: Celery/BackgroundTasks).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter
|
|
10
|
+
|
|
11
|
+
from shared.logger import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
router = APIRouter()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Health-check da feature
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@router.get(
|
|
24
|
+
"/health",
|
|
25
|
+
summary="Browser Feature Health-check",
|
|
26
|
+
description="Confirma que o módulo de análise de navegadores está disponível.",
|
|
27
|
+
)
|
|
28
|
+
def browser_health():
|
|
29
|
+
return {
|
|
30
|
+
"feature": "browser",
|
|
31
|
+
"status": "online",
|
|
32
|
+
"endpoints_available": [
|
|
33
|
+
"GET /browser/health",
|
|
34
|
+
],
|
|
35
|
+
"note": (
|
|
36
|
+
"Os endpoints de extração (history, downloads, logins) operam sobre "
|
|
37
|
+
"o sistema de arquivos local e serão expostos em versões futuras da API."
|
|
38
|
+
),
|
|
39
|
+
}
|