forensic-cli 1.4.6__tar.gz → 1.5.2__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.
Files changed (78) hide show
  1. {forensic_cli-1.4.6/src/forensic_cli.egg-info → forensic_cli-1.5.2}/PKG-INFO +4 -6
  2. {forensic_cli-1.4.6 → forensic_cli-1.5.2}/pyproject.toml +35 -6
  3. forensic_cli-1.5.2/src/api/main.py +29 -0
  4. forensic_cli-1.5.2/src/api/routers/__init__.py +1 -0
  5. forensic_cli-1.5.2/src/api/routers/executions.py +56 -0
  6. forensic_cli-1.5.2/src/api/routers/functionalities.py +101 -0
  7. forensic_cli-1.5.2/src/api/routers/modules.py +24 -0
  8. forensic_cli-1.5.2/src/api/routers/network.py +21 -0
  9. forensic_cli-1.5.2/src/api/routers/reports.py +24 -0
  10. forensic_cli-1.5.2/src/api/routers/results.py +29 -0
  11. forensic_cli-1.5.2/src/api/utils/db.py +9 -0
  12. forensic_cli-1.5.2/src/app/main.py +75 -0
  13. forensic_cli-1.5.2/src/app/utils/converter.py +144 -0
  14. forensic_cli-1.5.2/src/browser/__init__.py +1 -0
  15. forensic_cli-1.5.2/src/browser/api.py +39 -0
  16. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src}/browser/browser_history.py +50 -47
  17. forensic_cli-1.4.6/src/cli/commands/browser.py → forensic_cli-1.5.2/src/browser/cli.py +127 -52
  18. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src}/browser/common_words.py +8 -6
  19. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src}/browser/downloads_history.py +31 -18
  20. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src}/browser/fav_screen.py +12 -6
  21. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src}/browser/logins.py +28 -20
  22. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src}/browser/unusual_patterns.py +38 -26
  23. forensic_cli-1.5.2/src/email/__init__.py +1 -0
  24. forensic_cli-1.5.2/src/email/api.py +37 -0
  25. forensic_cli-1.5.2/src/email/cli.py +124 -0
  26. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src}/email/email_parser.py +18 -13
  27. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src}/email/header_analysis.py +6 -2
  28. {forensic_cli-1.4.6 → forensic_cli-1.5.2/src/forensic_cli.egg-info}/PKG-INFO +4 -6
  29. forensic_cli-1.5.2/src/forensic_cli.egg-info/SOURCES.txt +63 -0
  30. forensic_cli-1.5.2/src/forensic_cli.egg-info/entry_points.txt +2 -0
  31. {forensic_cli-1.4.6 → forensic_cli-1.5.2}/src/forensic_cli.egg-info/requires.txt +4 -0
  32. forensic_cli-1.5.2/src/forensic_cli.egg-info/top_level.txt +6 -0
  33. forensic_cli-1.5.2/src/network/__init__.py +1 -0
  34. forensic_cli-1.5.2/src/network/api.py +141 -0
  35. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src}/network/arp_scan.py +5 -2
  36. forensic_cli-1.5.2/src/network/cli.py +355 -0
  37. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src}/network/dns_recon.py +76 -19
  38. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src}/network/fingerprinting.py +23 -14
  39. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src}/network/ip_info.py +4 -2
  40. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src}/network/network_map.py +23 -16
  41. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src}/network/ping_sweep.py +8 -6
  42. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src}/network/port_scanner.py +48 -20
  43. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src}/network/smb_scan.py +3 -2
  44. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src}/network/snmp_scan.py +5 -4
  45. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src}/network/traceroute.py +7 -4
  46. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src}/network/utils.py +7 -3
  47. forensic_cli-1.5.2/src/shared/__init__.py +1 -0
  48. forensic_cli-1.5.2/src/shared/config.py +139 -0
  49. {forensic_cli-1.4.6/src/core/db → forensic_cli-1.5.2/src/shared}/db.py +2 -4
  50. {forensic_cli-1.4.6/src/cli/commands → forensic_cli-1.5.2/src/shared}/describe.py +22 -9
  51. forensic_cli-1.5.2/src/shared/exporter.py +156 -0
  52. forensic_cli-1.5.2/src/shared/logger.py +123 -0
  53. {forensic_cli-1.4.6/src/core/models → forensic_cli-1.5.2/src/shared}/orm.py +15 -4
  54. {forensic_cli-1.4.6/src/core → forensic_cli-1.5.2/src/shared}/registry.py +11 -5
  55. {forensic_cli-1.4.6/src/core/models → forensic_cli-1.5.2/src/shared}/schemas.py +23 -2
  56. forensic_cli-1.5.2/tests/test_file_inspector.py +25 -0
  57. forensic_cli-1.4.6/setup.py +0 -19
  58. forensic_cli-1.4.6/src/cli/commands/email.py +0 -3
  59. forensic_cli-1.4.6/src/cli/commands/network.py +0 -187
  60. forensic_cli-1.4.6/src/cli/main.py +0 -18
  61. forensic_cli-1.4.6/src/core/__init__.py +0 -0
  62. forensic_cli-1.4.6/src/core/data/__init__.py +0 -0
  63. forensic_cli-1.4.6/src/core/data/data_recovery.py +0 -157
  64. forensic_cli-1.4.6/src/core/network/__init__.py +0 -0
  65. forensic_cli-1.4.6/src/core/utils/__init__.py +0 -0
  66. forensic_cli-1.4.6/src/forensic_cli.egg-info/SOURCES.txt +0 -49
  67. forensic_cli-1.4.6/src/forensic_cli.egg-info/entry_points.txt +0 -2
  68. forensic_cli-1.4.6/src/forensic_cli.egg-info/top_level.txt +0 -2
  69. forensic_cli-1.4.6/tests/test_file_inspector.py +0 -17
  70. {forensic_cli-1.4.6 → forensic_cli-1.5.2}/LICENSE +0 -0
  71. {forensic_cli-1.4.6 → forensic_cli-1.5.2}/LICENSE-GPL +0 -0
  72. {forensic_cli-1.4.6 → forensic_cli-1.5.2}/README.md +0 -0
  73. {forensic_cli-1.4.6 → forensic_cli-1.5.2}/setup.cfg +0 -0
  74. {forensic_cli-1.4.6/src/cli → forensic_cli-1.5.2/src/app}/__init__.py +0 -0
  75. {forensic_cli-1.4.6/src/cli/commands → forensic_cli-1.5.2/src/app/utils}/__init__.py +0 -0
  76. {forensic_cli-1.4.6 → forensic_cli-1.5.2}/src/forensic_cli.egg-info/dependency_links.txt +0 -0
  77. {forensic_cli-1.4.6/src/core/utils → forensic_cli-1.5.2/src/shared}/gerar_amostras.py +0 -0
  78. /forensic_cli-1.4.6/src/cli/commands/utils.py → /forensic_cli-1.5.2/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.4.6
3
+ Version: 1.5.2
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
- Dynamic: author
92
- Dynamic: home-page
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.4.6"
7
+ version = "1.5.2"
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
- [tool.setuptools.packages.find]
95
- where = ["src"]
96
- include = ["cli*", "core*"]
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 = "cli.main:app"
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*", "email*", "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.2"
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,9 @@
1
+ from shared.db import SessionLocal
2
+
3
+
4
+ def get_db():
5
+ db = SessionLocal()
6
+ try:
7
+ yield db
8
+ finally:
9
+ db.close()
@@ -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
+ }