tracevector 2.5.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.
- tracevector-2.5.0/PKG-INFO +10 -0
- tracevector-2.5.0/README.md +33 -0
- tracevector-2.5.0/pyproject.toml +17 -0
- tracevector-2.5.0/setup.cfg +4 -0
- tracevector-2.5.0/setup.py +14 -0
- tracevector-2.5.0/tracevector.egg-info/PKG-INFO +10 -0
- tracevector-2.5.0/tracevector.egg-info/SOURCES.txt +35 -0
- tracevector-2.5.0/tracevector.egg-info/dependency_links.txt +1 -0
- tracevector-2.5.0/tracevector.egg-info/entry_points.txt +2 -0
- tracevector-2.5.0/tracevector.egg-info/requires.txt +3 -0
- tracevector-2.5.0/tracevector.egg-info/top_level.txt +1 -0
- tracevector-2.5.0/tvx/__init__.py +1 -0
- tracevector-2.5.0/tvx/banner.py +13 -0
- tracevector-2.5.0/tvx/colors.py +8 -0
- tracevector-2.5.0/tvx/config.py +27 -0
- tracevector-2.5.0/tvx/doctor.py +39 -0
- tracevector-2.5.0/tvx/email.py +9 -0
- tracevector-2.5.0/tvx/ip.py +8 -0
- tracevector-2.5.0/tvx/main.py +170 -0
- tracevector-2.5.0/tvx/phone.py +16 -0
- tracevector-2.5.0/tvx/plugin_loader.py +31 -0
- tracevector-2.5.0/tvx/plugin_security.py +8 -0
- tracevector-2.5.0/tvx/plugins/__init__.py +0 -0
- tracevector-2.5.0/tvx/plugins/email_basic.py +18 -0
- tracevector-2.5.0/tvx/plugins/email_osint.py +61 -0
- tracevector-2.5.0/tvx/plugins/ip_basic.py +20 -0
- tracevector-2.5.0/tvx/plugins/ip_osint.py +59 -0
- tracevector-2.5.0/tvx/plugins/phone_basic.py +13 -0
- tracevector-2.5.0/tvx/reporting.py +67 -0
- tracevector-2.5.0/tvx/risk.py +10 -0
- tracevector-2.5.0/tvx/scoring.py +38 -0
- tracevector-2.5.0/tvx/storage/__init__.py +0 -0
- tracevector-2.5.0/tvx/storage/base.py +20 -0
- tracevector-2.5.0/tvx/storage/manager.py +96 -0
- tracevector-2.5.0/tvx/storage/sqlite.py +68 -0
- tracevector-2.5.0/tvx/utils.py +0 -0
- tracevector-2.5.0/tvx/version.py +2 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tracevector
|
|
3
|
+
Version: 2.5.0
|
|
4
|
+
Summary: TRACEVECTOR OSINT CLI Investigator
|
|
5
|
+
Project-URL: Homepage, https://github.com/oedxdigitals/tracevector
|
|
6
|
+
Project-URL: Source, https://github.com/oedxdigitals/tracevector
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Requires-Dist: phonenumbers
|
|
9
|
+
Requires-Dist: dnspython
|
|
10
|
+
Requires-Dist: ipwhois
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# TRACEVECTOR (tvx)
|
|
2
|
+
|
|
3
|
+
TRACEVECTOR is a professional, terminal-first OSINT investigation CLI designed for investigators, analysts, and security professionals.
|
|
4
|
+
|
|
5
|
+
It runs as a **single portable binary**, supports a **plugin-based architecture**, and performs ethical open-source intelligence lookups on targets such as phone numbers, IP addresses, and email addresses.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- ๐ OSINT investigations from the terminal
|
|
12
|
+
- ๐งฉ Plugin-based architecture
|
|
13
|
+
- ๐ฆ Single-file portable binary (PyInstaller)
|
|
14
|
+
- ๐ต๏ธ Phone number metadata investigation
|
|
15
|
+
- ๐งช Built-in self diagnostics (`tvx doctor`)
|
|
16
|
+
- ๐ JSON output support
|
|
17
|
+
- โ๏ธ Works in minimal / container / Android-like environments
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
### Option 1: Use prebuilt binary
|
|
24
|
+
```bash
|
|
25
|
+
chmod +x tvx
|
|
26
|
+
./tvx --version
|
|
27
|
+
### Option 2: Use pip
|
|
28
|
+
'''bash
|
|
29
|
+
pip instal tracevector
|
|
30
|
+
|
|
31
|
+
### v2.5.0 Help Text
|
|
32
|
+
TRACEVECTOR performs lawful, metadata-based OSINT only.
|
|
33
|
+
Private communications are never accessed.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tracevector"
|
|
3
|
+
version = "2.5.0"
|
|
4
|
+
description = "TRACEVECTOR OSINT CLI Investigator"
|
|
5
|
+
requires-python = ">=3.9"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"phonenumbers",
|
|
8
|
+
"dnspython",
|
|
9
|
+
"ipwhois"
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.urls]
|
|
13
|
+
Homepage = "https://github.com/oedxdigitals/tracevector"
|
|
14
|
+
Source = "https://github.com/oedxdigitals/tracevector"
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
tvx = "tvx.main:main"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="tracevector",
|
|
5
|
+
version="2.5.0",
|
|
6
|
+
packages=find_packages(),
|
|
7
|
+
include_package_data=True,
|
|
8
|
+
install_requires=[],
|
|
9
|
+
entry_points={
|
|
10
|
+
"console_scripts": [
|
|
11
|
+
"tvx=tvx.main:main"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tracevector
|
|
3
|
+
Version: 2.5.0
|
|
4
|
+
Summary: TRACEVECTOR OSINT CLI Investigator
|
|
5
|
+
Project-URL: Homepage, https://github.com/oedxdigitals/tracevector
|
|
6
|
+
Project-URL: Source, https://github.com/oedxdigitals/tracevector
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Requires-Dist: phonenumbers
|
|
9
|
+
Requires-Dist: dnspython
|
|
10
|
+
Requires-Dist: ipwhois
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
tracevector.egg-info/PKG-INFO
|
|
5
|
+
tracevector.egg-info/SOURCES.txt
|
|
6
|
+
tracevector.egg-info/dependency_links.txt
|
|
7
|
+
tracevector.egg-info/entry_points.txt
|
|
8
|
+
tracevector.egg-info/requires.txt
|
|
9
|
+
tracevector.egg-info/top_level.txt
|
|
10
|
+
tvx/__init__.py
|
|
11
|
+
tvx/banner.py
|
|
12
|
+
tvx/colors.py
|
|
13
|
+
tvx/config.py
|
|
14
|
+
tvx/doctor.py
|
|
15
|
+
tvx/email.py
|
|
16
|
+
tvx/ip.py
|
|
17
|
+
tvx/main.py
|
|
18
|
+
tvx/phone.py
|
|
19
|
+
tvx/plugin_loader.py
|
|
20
|
+
tvx/plugin_security.py
|
|
21
|
+
tvx/reporting.py
|
|
22
|
+
tvx/risk.py
|
|
23
|
+
tvx/scoring.py
|
|
24
|
+
tvx/utils.py
|
|
25
|
+
tvx/version.py
|
|
26
|
+
tvx/plugins/__init__.py
|
|
27
|
+
tvx/plugins/email_basic.py
|
|
28
|
+
tvx/plugins/email_osint.py
|
|
29
|
+
tvx/plugins/ip_basic.py
|
|
30
|
+
tvx/plugins/ip_osint.py
|
|
31
|
+
tvx/plugins/phone_basic.py
|
|
32
|
+
tvx/storage/__init__.py
|
|
33
|
+
tvx/storage/base.py
|
|
34
|
+
tvx/storage/manager.py
|
|
35
|
+
tvx/storage/sqlite.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tvx
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.5.0"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
def show_banner():
|
|
2
|
+
banner = r"""
|
|
3
|
+
โโโโโโโโโโโโโโโโ โโโโโโ โโโโโโโโโโโโโโโ
|
|
4
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
5
|
+
โโโ โโโโโโโโโโโโโโโโโโโ โโโโโโ
|
|
6
|
+
โโโ โโโโโโโโโโโโโโโโโโโ โโโโโโ
|
|
7
|
+
โโโ โโโ โโโโโโ โโโโโโโโโโโโโโโโโโโ
|
|
8
|
+
โโโ โโโ โโโโโโ โโโ โโโโโโโโโโโโโโโ
|
|
9
|
+
|
|
10
|
+
TRACEVECTOR OSINT CLI
|
|
11
|
+
Digital Footprint Investigator
|
|
12
|
+
"""
|
|
13
|
+
print(banner)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
CONFIG_PATH = os.path.expanduser("~/.tracevector_config.json")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _load():
|
|
8
|
+
if not os.path.exists(CONFIG_PATH):
|
|
9
|
+
return {}
|
|
10
|
+
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
|
11
|
+
return json.load(f)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _save(data: dict):
|
|
15
|
+
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
|
16
|
+
json.dump(data, f, indent=2)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def set_key(key: str, value: str):
|
|
20
|
+
config = _load()
|
|
21
|
+
config[key] = value
|
|
22
|
+
_save(config)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_key(key: str, default=None):
|
|
26
|
+
config = _load()
|
|
27
|
+
return config.get(key, default)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import platform
|
|
3
|
+
from importlib import util
|
|
4
|
+
|
|
5
|
+
from tvx import __version__
|
|
6
|
+
from tvx import plugin_loader
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run_doctor():
|
|
10
|
+
print("TRACEVECTOR Doctor\n")
|
|
11
|
+
|
|
12
|
+
# Version
|
|
13
|
+
print(f"[+] Version: {__version__}")
|
|
14
|
+
|
|
15
|
+
# Frozen binary check
|
|
16
|
+
frozen = getattr(sys, "frozen", False)
|
|
17
|
+
print(f"[+] Frozen binary: {frozen}")
|
|
18
|
+
|
|
19
|
+
# Python executable
|
|
20
|
+
print(f"[+] Python: {sys.executable}")
|
|
21
|
+
|
|
22
|
+
# Platform
|
|
23
|
+
print(f"[+] Platform: {platform.system()} {platform.release()}")
|
|
24
|
+
|
|
25
|
+
# Plugin diagnostics
|
|
26
|
+
plugins = []
|
|
27
|
+
try:
|
|
28
|
+
plugins = plugin_loader.list_plugins()
|
|
29
|
+
except Exception as e:
|
|
30
|
+
print(f"[!] Plugin loader error: {e}")
|
|
31
|
+
|
|
32
|
+
print(f"[+] Plugins detected: {len(plugins)}")
|
|
33
|
+
|
|
34
|
+
for p in plugins:
|
|
35
|
+
name = p.get("command", "unknown")
|
|
36
|
+
desc = p.get("name", "unnamed plugin")
|
|
37
|
+
print(f" - [{name}] {desc}")
|
|
38
|
+
|
|
39
|
+
print("\n[โ] Environment looks healthy")
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from tvx.plugin_loader import load_plugins
|
|
7
|
+
from tvx.scoring import calculate_risk
|
|
8
|
+
from tvx.reporting import generate_html
|
|
9
|
+
from tvx.config import set_key
|
|
10
|
+
from tvx.storage.manager import get_storage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# =====================================
|
|
14
|
+
# Banner
|
|
15
|
+
# =====================================
|
|
16
|
+
def print_banner():
|
|
17
|
+
print(r"""
|
|
18
|
+
โโโโโโโโโโโโโโโโ โโโโโโ โโโโโโโโโโโโโโโ
|
|
19
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
20
|
+
โโโ โโโโโโโโโโโโโโโโโโโ โโโโโโ
|
|
21
|
+
โโโ โโโโโโโโโโโโโโโโโโโ โโโโโโ
|
|
22
|
+
โโโ โโโ โโโโโโ โโโโโโโโโโโโโโโโโโโ
|
|
23
|
+
โโโ โโโ โโโโโโ โโโ โโโโโโโโโโโโโโโ
|
|
24
|
+
|
|
25
|
+
TRACEVECTOR FRAUD INTELLIGENCE
|
|
26
|
+
""")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# =====================================
|
|
30
|
+
# Main
|
|
31
|
+
# =====================================
|
|
32
|
+
def main():
|
|
33
|
+
parser = argparse.ArgumentParser(
|
|
34
|
+
prog="tvx",
|
|
35
|
+
description="TraceVector Fraud Intelligence Toolkit"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
parser.add_argument("command", help="Core command or plugin")
|
|
39
|
+
parser.add_argument("args", nargs="*", help="Command arguments")
|
|
40
|
+
parser.add_argument("--json", action="store_true", help="Output JSON")
|
|
41
|
+
parser.add_argument("--report", help="Generate HTML report")
|
|
42
|
+
|
|
43
|
+
parsed = parser.parse_args()
|
|
44
|
+
|
|
45
|
+
print_banner()
|
|
46
|
+
|
|
47
|
+
command = parsed.command
|
|
48
|
+
arguments = parsed.args
|
|
49
|
+
|
|
50
|
+
# =========================
|
|
51
|
+
# CORE: CONFIG
|
|
52
|
+
# =========================
|
|
53
|
+
if command == "config":
|
|
54
|
+
if not arguments or "=" not in arguments[0]:
|
|
55
|
+
print("Usage: tvx config key=value")
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
key, value = arguments[0].split("=", 1)
|
|
59
|
+
set_key(key.strip(), value.strip())
|
|
60
|
+
print(f"[โ] Config saved: {key}")
|
|
61
|
+
sys.exit(0)
|
|
62
|
+
|
|
63
|
+
# =========================
|
|
64
|
+
# CORE: CASE
|
|
65
|
+
# =========================
|
|
66
|
+
if command == "case":
|
|
67
|
+
if not arguments:
|
|
68
|
+
print("Usage: tvx case <create|add|show>")
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
|
|
71
|
+
action = arguments[0]
|
|
72
|
+
storage = get_storage()
|
|
73
|
+
|
|
74
|
+
# CREATE
|
|
75
|
+
if action == "create":
|
|
76
|
+
if len(arguments) < 2:
|
|
77
|
+
print("Usage: tvx case create CASE_ID")
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
|
|
80
|
+
case_id = arguments[1]
|
|
81
|
+
storage.create_case(case_id)
|
|
82
|
+
print(f"[โ] Case created: {case_id}")
|
|
83
|
+
sys.exit(0)
|
|
84
|
+
|
|
85
|
+
# ADD
|
|
86
|
+
if action == "add":
|
|
87
|
+
if len(arguments) < 4:
|
|
88
|
+
print("Usage: tvx case add CASE_ID plugin target")
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
|
|
91
|
+
case_id = arguments[1]
|
|
92
|
+
plugin_name = arguments[2]
|
|
93
|
+
target = arguments[3]
|
|
94
|
+
|
|
95
|
+
plugins = load_plugins()
|
|
96
|
+
|
|
97
|
+
if plugin_name not in plugins:
|
|
98
|
+
print("[!] Unknown plugin")
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
|
|
101
|
+
plugin = plugins[plugin_name]
|
|
102
|
+
result = plugin.run(target)
|
|
103
|
+
risk = calculate_risk(result)
|
|
104
|
+
|
|
105
|
+
storage.add_evidence(case_id, plugin_name, target, result, risk)
|
|
106
|
+
|
|
107
|
+
print(f"[โ] Evidence added to case {case_id}")
|
|
108
|
+
print(f"Risk Level: {risk['level']} (Score: {risk['score']})")
|
|
109
|
+
sys.exit(0)
|
|
110
|
+
|
|
111
|
+
# SHOW
|
|
112
|
+
if action == "show":
|
|
113
|
+
if len(arguments) < 2:
|
|
114
|
+
print("Usage: tvx case show CASE_ID")
|
|
115
|
+
sys.exit(1)
|
|
116
|
+
|
|
117
|
+
case_id = arguments[1]
|
|
118
|
+
case_data = storage.get_case(case_id)
|
|
119
|
+
|
|
120
|
+
if not case_data["case"]:
|
|
121
|
+
print("[!] Case not found.")
|
|
122
|
+
sys.exit(1)
|
|
123
|
+
|
|
124
|
+
print(json.dumps(case_data, indent=2))
|
|
125
|
+
sys.exit(0)
|
|
126
|
+
|
|
127
|
+
print("Unknown case action.")
|
|
128
|
+
sys.exit(1)
|
|
129
|
+
|
|
130
|
+
# =========================
|
|
131
|
+
# PLUGIN EXECUTION
|
|
132
|
+
# =========================
|
|
133
|
+
plugins = load_plugins()
|
|
134
|
+
|
|
135
|
+
if command not in plugins:
|
|
136
|
+
print(f"[!] Unknown command: {command}")
|
|
137
|
+
print("Available plugins:", ", ".join(plugins.keys()))
|
|
138
|
+
sys.exit(1)
|
|
139
|
+
|
|
140
|
+
if not arguments:
|
|
141
|
+
print("[!] Target required.")
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
|
|
144
|
+
target = arguments[0]
|
|
145
|
+
plugin = plugins[command]
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
result = plugin.run(target)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
print(f"[!] Plugin execution failed: {e}")
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
|
|
153
|
+
risk = calculate_risk(result)
|
|
154
|
+
|
|
155
|
+
output = {
|
|
156
|
+
"scan": result,
|
|
157
|
+
"risk": risk
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if parsed.json:
|
|
161
|
+
print(json.dumps(output, indent=2))
|
|
162
|
+
else:
|
|
163
|
+
print("\n=== Scan Result ===")
|
|
164
|
+
print(json.dumps(result, indent=2))
|
|
165
|
+
print("\n=== Risk Assessment ===")
|
|
166
|
+
print(json.dumps(risk, indent=2))
|
|
167
|
+
|
|
168
|
+
if parsed.report:
|
|
169
|
+
file_path = generate_html(output, risk, parsed.report)
|
|
170
|
+
print(f"\n[โ] HTML report generated: {file_path}")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
try:
|
|
2
|
+
import phonenumbers
|
|
3
|
+
from phonenumbers import geocoder, carrier
|
|
4
|
+
except ImportError:
|
|
5
|
+
print("[!] Missing dependency: phonenumbers")
|
|
6
|
+
print("Run: pip install phonenumbers")
|
|
7
|
+
exit(1)
|
|
8
|
+
|
|
9
|
+
def investigate_phone(number):
|
|
10
|
+
try:
|
|
11
|
+
parsed = phonenumbers.parse(number)
|
|
12
|
+
print("[+] Valid Number:", phonenumbers.is_valid_number(parsed))
|
|
13
|
+
print("[+] Country:", geocoder.description_for_number(parsed, "en"))
|
|
14
|
+
print("[+] Carrier:", carrier.name_for_number(parsed, "en"))
|
|
15
|
+
except Exception as e:
|
|
16
|
+
print("[!] Error:", e)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
PLUGIN_MODULES = [
|
|
5
|
+
"tvx.email",
|
|
6
|
+
"tvx.phone",
|
|
7
|
+
"tvx.ip",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_plugins():
|
|
12
|
+
plugins = {}
|
|
13
|
+
|
|
14
|
+
for module_path in PLUGIN_MODULES:
|
|
15
|
+
try:
|
|
16
|
+
module = importlib.import_module(module_path)
|
|
17
|
+
|
|
18
|
+
# Look for a class with a run() method
|
|
19
|
+
for name, obj in inspect.getmembers(module):
|
|
20
|
+
if inspect.isclass(obj) and hasattr(obj, "run"):
|
|
21
|
+
instance = obj()
|
|
22
|
+
|
|
23
|
+
# Use class attribute if available
|
|
24
|
+
plugin_name = getattr(instance, "name", module_path.split(".")[-1])
|
|
25
|
+
|
|
26
|
+
plugins[plugin_name] = instance
|
|
27
|
+
|
|
28
|
+
except Exception:
|
|
29
|
+
continue
|
|
30
|
+
|
|
31
|
+
return plugins
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
COMMAND = "email"
|
|
2
|
+
|
|
3
|
+
class Plugin:
|
|
4
|
+
name = "Basic Email Metadata"
|
|
5
|
+
|
|
6
|
+
def run(self, args):
|
|
7
|
+
target = args[0] if args else None
|
|
8
|
+
|
|
9
|
+
print("[PLUGIN] Basic Email Metadata")
|
|
10
|
+
print(f"input: {target}")
|
|
11
|
+
|
|
12
|
+
if not target or "@" not in target:
|
|
13
|
+
print("error: invalid email")
|
|
14
|
+
return
|
|
15
|
+
|
|
16
|
+
domain = target.split("@")[-1]
|
|
17
|
+
print(f"domain: {domain}")
|
|
18
|
+
print("status: plugin_loaded_successfully")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import dns.resolver
|
|
2
|
+
import re
|
|
3
|
+
import hashlib
|
|
4
|
+
|
|
5
|
+
COMMAND = "email"
|
|
6
|
+
|
|
7
|
+
class Plugin:
|
|
8
|
+
name = "Email OSINT Analysis"
|
|
9
|
+
|
|
10
|
+
def run(self, args):
|
|
11
|
+
email = args[0]
|
|
12
|
+
domain = email.split("@")[-1]
|
|
13
|
+
|
|
14
|
+
result = {
|
|
15
|
+
"target": email,
|
|
16
|
+
"metadata": {},
|
|
17
|
+
"risk": {"score": 0, "level": "low"},
|
|
18
|
+
"sources": [],
|
|
19
|
+
"notes": []
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Basic validation
|
|
23
|
+
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
|
|
24
|
+
result["notes"].append("Invalid email format")
|
|
25
|
+
result["risk"]["score"] += 50
|
|
26
|
+
return result
|
|
27
|
+
|
|
28
|
+
# MX Records
|
|
29
|
+
try:
|
|
30
|
+
mx = dns.resolver.resolve(domain, "MX")
|
|
31
|
+
result["metadata"]["mx_records"] = [r.exchange.to_text() for r in mx]
|
|
32
|
+
except Exception:
|
|
33
|
+
result["metadata"]["mx_records"] = []
|
|
34
|
+
result["risk"]["score"] += 30
|
|
35
|
+
result["notes"].append("No MX records")
|
|
36
|
+
|
|
37
|
+
# Provider detection
|
|
38
|
+
if "google.com" in str(result["metadata"].get("mx_records", "")):
|
|
39
|
+
result["metadata"]["provider"] = "Google"
|
|
40
|
+
else:
|
|
41
|
+
result["metadata"]["provider"] = "Unknown"
|
|
42
|
+
|
|
43
|
+
# Disposable check (basic)
|
|
44
|
+
disposable_domains = {"mailinator.com", "tempmail.com"}
|
|
45
|
+
if domain in disposable_domains:
|
|
46
|
+
result["risk"]["score"] += 40
|
|
47
|
+
result["notes"].append("Disposable email domain")
|
|
48
|
+
|
|
49
|
+
# Risk level
|
|
50
|
+
result["risk"]["level"] = score_level(result["risk"]["score"])
|
|
51
|
+
result["sources"].append("DNS")
|
|
52
|
+
|
|
53
|
+
return result
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def score_level(score):
|
|
57
|
+
if score >= 70:
|
|
58
|
+
return "high"
|
|
59
|
+
if score >= 40:
|
|
60
|
+
return "medium"
|
|
61
|
+
return "low"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import ipaddress
|
|
2
|
+
|
|
3
|
+
COMMAND = "ip"
|
|
4
|
+
|
|
5
|
+
class Plugin:
|
|
6
|
+
name = "Basic IP Metadata"
|
|
7
|
+
|
|
8
|
+
def run(self, args):
|
|
9
|
+
target = args[0] if args else None
|
|
10
|
+
|
|
11
|
+
print("[PLUGIN] Basic IP Metadata")
|
|
12
|
+
print(f"input: {target}")
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
ip = ipaddress.ip_address(target)
|
|
16
|
+
print(f"version: IPv{ip.version}")
|
|
17
|
+
print("is_private:", ip.is_private)
|
|
18
|
+
print("status: plugin_loaded_successfully")
|
|
19
|
+
except ValueError:
|
|
20
|
+
print("error: invalid IP address")
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import ipaddress
|
|
2
|
+
from ipwhois import IPWhois
|
|
3
|
+
|
|
4
|
+
COMMAND = "ip"
|
|
5
|
+
|
|
6
|
+
class Plugin:
|
|
7
|
+
name = "IP OSINT Analysis"
|
|
8
|
+
|
|
9
|
+
def run(self, args):
|
|
10
|
+
ip = args[0]
|
|
11
|
+
|
|
12
|
+
result = {
|
|
13
|
+
"target": ip,
|
|
14
|
+
"metadata": {},
|
|
15
|
+
"risk": {"score": 0, "level": "low"},
|
|
16
|
+
"sources": [],
|
|
17
|
+
"notes": []
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
ip_obj = ipaddress.ip_address(ip)
|
|
22
|
+
except ValueError:
|
|
23
|
+
result["notes"].append("Invalid IP address")
|
|
24
|
+
result["risk"]["score"] = 100
|
|
25
|
+
result["risk"]["level"] = "high"
|
|
26
|
+
return result
|
|
27
|
+
|
|
28
|
+
# Private IP check
|
|
29
|
+
if ip_obj.is_private:
|
|
30
|
+
result["notes"].append("Private IP address")
|
|
31
|
+
result["risk"]["score"] += 10
|
|
32
|
+
|
|
33
|
+
# WHOIS
|
|
34
|
+
try:
|
|
35
|
+
whois = IPWhois(ip).lookup_rdap()
|
|
36
|
+
result["metadata"]["asn"] = whois.get("asn")
|
|
37
|
+
result["metadata"]["org"] = whois.get("network", {}).get("name")
|
|
38
|
+
except Exception:
|
|
39
|
+
result["notes"].append("WHOIS lookup failed")
|
|
40
|
+
result["risk"]["score"] += 20
|
|
41
|
+
|
|
42
|
+
# Datacenter heuristic
|
|
43
|
+
org = str(result["metadata"].get("org", "")).lower()
|
|
44
|
+
if any(k in org for k in ["google", "amazon", "cloud", "hosting"]):
|
|
45
|
+
result["notes"].append("Datacenter / hosting IP")
|
|
46
|
+
result["risk"]["score"] += 20
|
|
47
|
+
|
|
48
|
+
result["risk"]["level"] = score_level(result["risk"]["score"])
|
|
49
|
+
result["sources"].append("RDAP")
|
|
50
|
+
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def score_level(score):
|
|
55
|
+
if score >= 70:
|
|
56
|
+
return "high"
|
|
57
|
+
if score >= 40:
|
|
58
|
+
return "medium"
|
|
59
|
+
return "low"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
COMMAND = "phone"
|
|
2
|
+
NAME = "Basic Phone Metadata"
|
|
3
|
+
|
|
4
|
+
def run(args):
|
|
5
|
+
phone = args[0] if args else "N/A"
|
|
6
|
+
|
|
7
|
+
print("=" * 50)
|
|
8
|
+
print("[PLUGIN] Basic Phone Metadata")
|
|
9
|
+
print("-" * 50)
|
|
10
|
+
print(f"input: {phone}")
|
|
11
|
+
print("country: Unknown")
|
|
12
|
+
print("carrier: Unknown")
|
|
13
|
+
print("risk_score: 20/100")
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def generate_html(scan_data: dict, risk_data: dict, filename: str) -> str:
|
|
6
|
+
"""
|
|
7
|
+
Generate simple HTML fraud investigation report.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
if not filename.endswith(".html"):
|
|
11
|
+
filename += ".html"
|
|
12
|
+
|
|
13
|
+
html_content = f"""
|
|
14
|
+
<html>
|
|
15
|
+
<head>
|
|
16
|
+
<title>TraceVector Fraud Report</title>
|
|
17
|
+
<style>
|
|
18
|
+
body {{
|
|
19
|
+
font-family: Arial, sans-serif;
|
|
20
|
+
margin: 40px;
|
|
21
|
+
background-color: #f4f4f4;
|
|
22
|
+
}}
|
|
23
|
+
h1 {{
|
|
24
|
+
color: #222;
|
|
25
|
+
}}
|
|
26
|
+
.box {{
|
|
27
|
+
background: white;
|
|
28
|
+
padding: 20px;
|
|
29
|
+
margin-bottom: 20px;
|
|
30
|
+
border-radius: 8px;
|
|
31
|
+
box-shadow: 0 0 10px rgba(0,0,0,0.05);
|
|
32
|
+
}}
|
|
33
|
+
.high {{ color: red; }}
|
|
34
|
+
.medium {{ color: orange; }}
|
|
35
|
+
.low {{ color: green; }}
|
|
36
|
+
</style>
|
|
37
|
+
</head>
|
|
38
|
+
<body>
|
|
39
|
+
<h1>TraceVector Fraud Investigation Report</h1>
|
|
40
|
+
<p><strong>Generated:</strong> {datetime.utcnow().isoformat()} UTC</p>
|
|
41
|
+
|
|
42
|
+
<div class="box">
|
|
43
|
+
<h2>Scan Result</h2>
|
|
44
|
+
<pre>{scan_data}</pre>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="box">
|
|
48
|
+
<h2>Risk Assessment</h2>
|
|
49
|
+
<p><strong>Score:</strong> {risk_data.get("score")}</p>
|
|
50
|
+
<p><strong>Level:</strong>
|
|
51
|
+
<span class="{risk_data.get("level", "").lower()}">
|
|
52
|
+
{risk_data.get("level")}
|
|
53
|
+
</span>
|
|
54
|
+
</p>
|
|
55
|
+
<p><strong>Flags:</strong></p>
|
|
56
|
+
<ul>
|
|
57
|
+
{''.join(f'<li>{flag}</li>' for flag in risk_data.get("flags", []))}
|
|
58
|
+
</ul>
|
|
59
|
+
</div>
|
|
60
|
+
</body>
|
|
61
|
+
</html>
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
with open(filename, "w", encoding="utf-8") as f:
|
|
65
|
+
f.write(html_content)
|
|
66
|
+
|
|
67
|
+
return os.path.abspath(filename)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
def calculate_risk(result: dict) -> dict:
|
|
2
|
+
"""
|
|
3
|
+
Simple fraud risk scoring engine.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
score = 0
|
|
7
|
+
flags = []
|
|
8
|
+
|
|
9
|
+
# Example signals
|
|
10
|
+
if result.get("disposable_email"):
|
|
11
|
+
score += 40
|
|
12
|
+
flags.append("Disposable email provider")
|
|
13
|
+
|
|
14
|
+
if result.get("tor_exit"):
|
|
15
|
+
score += 50
|
|
16
|
+
flags.append("Tor exit node detected")
|
|
17
|
+
|
|
18
|
+
if result.get("vpn"):
|
|
19
|
+
score += 30
|
|
20
|
+
flags.append("VPN usage suspected")
|
|
21
|
+
|
|
22
|
+
if result.get("blacklisted"):
|
|
23
|
+
score += 60
|
|
24
|
+
flags.append("Found in blacklist")
|
|
25
|
+
|
|
26
|
+
# Risk level
|
|
27
|
+
if score >= 80:
|
|
28
|
+
level = "HIGH"
|
|
29
|
+
elif score >= 40:
|
|
30
|
+
level = "MEDIUM"
|
|
31
|
+
else:
|
|
32
|
+
level = "LOW"
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
"score": score,
|
|
36
|
+
"level": level,
|
|
37
|
+
"flags": flags
|
|
38
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BaseStorage(ABC):
|
|
5
|
+
|
|
6
|
+
@abstractmethod
|
|
7
|
+
def init(self):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def create_case(self, case_id: str):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def add_scan(self, case_id: str, scan_data: dict):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def get_case(self, case_id: str):
|
|
20
|
+
pass
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
DB_PATH = os.path.expanduser("~/.tracevector_cases.db")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _connect():
|
|
10
|
+
conn = sqlite3.connect(DB_PATH)
|
|
11
|
+
conn.row_factory = sqlite3.Row
|
|
12
|
+
return conn
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _init():
|
|
16
|
+
conn = _connect()
|
|
17
|
+
cur = conn.cursor()
|
|
18
|
+
|
|
19
|
+
cur.execute("""
|
|
20
|
+
CREATE TABLE IF NOT EXISTS cases (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
created_at TEXT
|
|
23
|
+
)
|
|
24
|
+
""")
|
|
25
|
+
|
|
26
|
+
cur.execute("""
|
|
27
|
+
CREATE TABLE IF NOT EXISTS evidence (
|
|
28
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
29
|
+
case_id TEXT,
|
|
30
|
+
plugin TEXT,
|
|
31
|
+
target TEXT,
|
|
32
|
+
result TEXT,
|
|
33
|
+
risk TEXT,
|
|
34
|
+
created_at TEXT
|
|
35
|
+
)
|
|
36
|
+
""")
|
|
37
|
+
|
|
38
|
+
conn.commit()
|
|
39
|
+
conn.close()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Storage:
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
_init()
|
|
46
|
+
|
|
47
|
+
def create_case(self, case_id):
|
|
48
|
+
conn = _connect()
|
|
49
|
+
cur = conn.cursor()
|
|
50
|
+
cur.execute(
|
|
51
|
+
"INSERT INTO cases (id, created_at) VALUES (?, ?)",
|
|
52
|
+
(case_id, datetime.utcnow().isoformat())
|
|
53
|
+
)
|
|
54
|
+
conn.commit()
|
|
55
|
+
conn.close()
|
|
56
|
+
|
|
57
|
+
def add_evidence(self, case_id, plugin, target, result, risk):
|
|
58
|
+
conn = _connect()
|
|
59
|
+
cur = conn.cursor()
|
|
60
|
+
cur.execute(
|
|
61
|
+
"""
|
|
62
|
+
INSERT INTO evidence (case_id, plugin, target, result, risk, created_at)
|
|
63
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
64
|
+
""",
|
|
65
|
+
(
|
|
66
|
+
case_id,
|
|
67
|
+
plugin,
|
|
68
|
+
target,
|
|
69
|
+
json.dumps(result),
|
|
70
|
+
json.dumps(risk),
|
|
71
|
+
datetime.utcnow().isoformat()
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
conn.commit()
|
|
75
|
+
conn.close()
|
|
76
|
+
|
|
77
|
+
def get_case(self, case_id):
|
|
78
|
+
conn = _connect()
|
|
79
|
+
cur = conn.cursor()
|
|
80
|
+
|
|
81
|
+
cur.execute("SELECT * FROM cases WHERE id=?", (case_id,))
|
|
82
|
+
case = cur.fetchone()
|
|
83
|
+
|
|
84
|
+
cur.execute("SELECT * FROM evidence WHERE case_id=?", (case_id,))
|
|
85
|
+
evidence = cur.fetchall()
|
|
86
|
+
|
|
87
|
+
conn.close()
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
"case": dict(case) if case else None,
|
|
91
|
+
"evidence": [dict(e) for e in evidence]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_storage():
|
|
96
|
+
return Storage()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
import json
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from tvx.storage.base import BaseStorage
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SQLiteStorage(BaseStorage):
|
|
9
|
+
|
|
10
|
+
def __init__(self, db_path="tracevector.db"):
|
|
11
|
+
self.db_path = db_path
|
|
12
|
+
self.conn = sqlite3.connect(self.db_path)
|
|
13
|
+
self.conn.row_factory = sqlite3.Row
|
|
14
|
+
|
|
15
|
+
def init(self):
|
|
16
|
+
cursor = self.conn.cursor()
|
|
17
|
+
|
|
18
|
+
cursor.execute("""
|
|
19
|
+
CREATE TABLE IF NOT EXISTS cases (
|
|
20
|
+
id TEXT PRIMARY KEY,
|
|
21
|
+
created_at TEXT
|
|
22
|
+
)
|
|
23
|
+
""")
|
|
24
|
+
|
|
25
|
+
cursor.execute("""
|
|
26
|
+
CREATE TABLE IF NOT EXISTS scans (
|
|
27
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
28
|
+
case_id TEXT,
|
|
29
|
+
timestamp TEXT,
|
|
30
|
+
scan_json TEXT,
|
|
31
|
+
FOREIGN KEY(case_id) REFERENCES cases(id)
|
|
32
|
+
)
|
|
33
|
+
""")
|
|
34
|
+
|
|
35
|
+
self.conn.commit()
|
|
36
|
+
|
|
37
|
+
def create_case(self, case_id: str):
|
|
38
|
+
cursor = self.conn.cursor()
|
|
39
|
+
cursor.execute(
|
|
40
|
+
"INSERT INTO cases (id, created_at) VALUES (?, ?)",
|
|
41
|
+
(case_id, datetime.utcnow().isoformat())
|
|
42
|
+
)
|
|
43
|
+
self.conn.commit()
|
|
44
|
+
|
|
45
|
+
def add_scan(self, case_id: str, scan_data: dict):
|
|
46
|
+
cursor = self.conn.cursor()
|
|
47
|
+
cursor.execute(
|
|
48
|
+
"INSERT INTO scans (case_id, timestamp, scan_json) VALUES (?, ?, ?)",
|
|
49
|
+
(
|
|
50
|
+
case_id,
|
|
51
|
+
datetime.utcnow().isoformat(),
|
|
52
|
+
json.dumps(scan_data)
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
self.conn.commit()
|
|
56
|
+
|
|
57
|
+
def get_case(self, case_id: str):
|
|
58
|
+
cursor = self.conn.cursor()
|
|
59
|
+
cursor.execute("SELECT * FROM cases WHERE id = ?", (case_id,))
|
|
60
|
+
case = cursor.fetchone()
|
|
61
|
+
|
|
62
|
+
cursor.execute("SELECT * FROM scans WHERE case_id = ?", (case_id,))
|
|
63
|
+
scans = cursor.fetchall()
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
"case": dict(case) if case else None,
|
|
67
|
+
"scans": [dict(s) for s in scans]
|
|
68
|
+
}
|
|
File without changes
|