-
Notifications
You must be signed in to change notification settings - Fork 0
/
shodan2db.py
270 lines (247 loc) · 12.5 KB
/
shodan2db.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
import json
import sqlite3
import sys
import os
import click
from jinja2 import Environment, FileSystemLoader
class Shodan2DB():
# Static method to create tables and views in the SQLite database
@staticmethod
def prepare_database(verbose, database):
if not database.endswith(".db"):
database = f"{database}.db"
# Create database
try:
if verbose:
print("[+] Create views and tables...")
conn = sqlite3.connect(database)
cursor = conn.cursor()
cursor.execute(
"""CREATE TABLE IF NOT EXISTS "services" ( "id" INTEGER UNIQUE, "ip" TEXT, "asn" TEXT, "hostnames"
TEXT, "domains" TEXT, "org" TEXT, "timestamp" TEXT, "isp" TEXT, "os" TEXT, "product" TEXT,
"version" TEXT, "transport" TEXT, "port" TEXT, "data" TEXT, "city" TEXT, "region_code" TEXT,
"area_code" TEXT, "country_code" TEXT, "country_name" TEXT, "nbvulns" INTEGER, "tags" TEXT,
PRIMARY KEY("id" AUTOINCREMENT) )""")
cursor.execute(
"""CREATE TABLE IF NOT EXISTS "vulnerabilities" ( "ip" TEXT, "cveid" TEXT, "verified" NUMERIC,
"cvss" REAL, "summary" TEXT)""")
cursor.execute(
"""CREATE VIEW IF NOT EXISTS "Summary" AS select ip, hostnames, port, product, version, transport, isp,
city, tags, nbvulns FROM services ORDER BY nbvulns DESC""")
cursor.execute("""CREATE INDEX IF NOT EXISTS "ip_index" ON services("ip");""")
cursor.execute("""CREATE INDEX IF NOT EXISTS "nbvulns_index" ON services("nbvulns");""")
conn.commit()
except Exception as e:
print("Error")
conn.rollback()
raise e
finally:
conn.close()
# Static method to parse a JSON file and insert data into the database
@staticmethod
def parser(verbose, inputfile, database):
if not database.endswith(".db"):
database = f"{database}.db"
if verbose:
print("[+] Parsing file...")
try:
with open(inputfile, encoding='utf-8') as json_file:
for line in json_file:
jsonobject = json.loads(line)
# Mapping data
ip_str = jsonobject.get('ip_str')
asn = jsonobject.get('asn')
if jsonobject.get('domains') is not None:
domains = jsonobject.get('domains')
domains = " ".join(domains)
else:
domains = None
hostnames = jsonobject.get('hostnames')
hostnames = " ".join(hostnames)
org = jsonobject.get('org')
timestamp = jsonobject.get('timestamp')
isp = jsonobject.get('isp')
operating_system = jsonobject.get('os')
product = jsonobject.get('product')
version = jsonobject.get('version')
transport = jsonobject.get('transport')
port = jsonobject.get('port')
data = jsonobject.get('data')
city = jsonobject['location']['city']
region_code = jsonobject['location']['region_code']
area_code = jsonobject['location']['area_code']
country_code = jsonobject['location']['country_code']
country_name = jsonobject['location']['country_name']
if jsonobject.get('vulns') is not None:
nbvulns = len(jsonobject.get('vulns'))
else:
nbvulns = None
tags = jsonobject.get('tags')
if tags is not None:
tags = " ".join(tags)
# Insertion services
try:
conn = sqlite3.connect(database)
cursor = conn.cursor()
cursor.execute(
'INSERT OR IGNORE INTO services (ip, asn, domains, hostnames, org, timestamp, isp, os, '
'product,'
'version, transport, port, data, city, region_code, area_code, country_code, country_name,'
'nbvulns, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
(
ip_str, asn, domains, hostnames, org, timestamp, isp, operating_system, product, version, transport,
port, data, city, region_code, area_code, country_code, country_name, nbvulns, tags,))
# id = cursor.lastrowid
# print('Last id insert : %d' % id, "-", line)
conn.commit()
except sqlite3.IntegrityError:
print("[!] Already exist :", line)
continue
except Exception as e:
print("[!] Error")
conn.rollback()
raise e
finally:
conn.close()
if nbvulns is not None:
for i in jsonobject['vulns']:
cveid = i
verified = jsonobject['vulns'][i]['verified']
cvss = jsonobject['vulns'][i]['cvss']
summary = jsonobject['vulns'][i]['summary']
# Insertion vulnerabilities
try:
conn = sqlite3.connect(database)
cursor = conn.cursor()
cursor.execute(
'INSERT OR IGNORE INTO vulnerabilities (ip, cveid, verified, cvss, summary)'
'VALUES (?, ?, ?, ?, ?)',
(ip_str, cveid, verified, cvss, summary,))
# id = cursor.lastrowid
# print('Last id insert : %d' % id, "-", line)
conn.commit()
except sqlite3.IntegrityError:
print("[!] Already exist :", line)
continue
except Exception as e:
print("[!] Error")
conn.rollback()
raise e
finally:
conn.close()
else:
pass
except FileNotFoundError:
print('[!] Error: Provided input file does not exist!')
exit(1)
# Static method to generate an HTML report from the database data
@staticmethod
def export(verbose, exportfile, database, template_file):
if not exportfile.endswith(".html"):
exportfile = f"{exportfile}.html"
if not database.endswith(".db"):
database = f"{database}.db"
try:
conn = sqlite3.connect(database)
cursor = conn.cursor()
cursor.execute(
"""SELECT DISTINCT ip, hostnames, isp, city, tags, nbvulns FROM summary
WHERE nbvulns IS NOT NULL ORDER BY nbvulns DESC""")
hosts_list = cursor.fetchall()
cursor.execute("""SELECT ip, cveid, cvss, summary FROM vulnerabilities ORDER BY ip, cvss DESC""")
vulns_list = cursor.fetchall()
cursor.execute(
"""SELECT DISTINCT ip, port, product, version, transport FROM services
WHERE ip IN (SELECT ip FROM summary WHERE nbvulns is not NULL) ORDER BY ip""")
services_list = cursor.fetchall()
cursor.execute(
"""SELECT cveid, count(*) as count, cvss, summary from vulnerabilities GROUP BY cveid ORDER BY count
DESC, cvss DESC""")
cves_list = cursor.fetchall()
except sqlite3.OperationalError:
print(f"[!] {database} not found! Please provide a valid database name with -d")
exit(1)
# Transformation of lists into dictionaries for easier template editing.
hosts_data = []
for row in hosts_list:
hosts = {"ip": row[0], "hostnames": row[1], "isp": row[2], "city": row[3], "tags": row[4],
"nbvulns": row[5]}
hosts_data.append(hosts)
services_data = []
for row in services_list:
services = {"ip": row[0], "port": row[1], "product": row[2], "version": row[3], "transport": row[4]}
services_data.append(services)
vulns_data = []
for row in vulns_list:
vulns = {"ip": row[0], "cveid": row[1], "cvss": row[2], "summary": row[3]}
vulns_data.append(vulns)
cves_data = []
for row in cves_list:
cves = {"cveid": row[0], "count": row[1], "cvss": row[2], "summary": row[3]}
cves_data.append(cves)
environment = Environment(loader=FileSystemLoader("templates/"))
template = environment.get_template(template_file)
filename = exportfile
content = template.render(
hosts=hosts_data,
services=services_data,
vulns=vulns_data,
cves=cves_data
)
with open(filename, mode="w", encoding="utf-8") as message:
message.write(content)
if verbose:
print(f"[+] Wrote report : {filename}")
# Define the click group to organize commands
@click.group()
def cli():
pass
# Define the parse command with options for input file, database, and verbose mode
@click.command(name="parse", help="Parse the Shodan JSON export file and store data in the database.",
context_settings=dict(help_option_names=['-h', '--help']))
@click.option('--input-file', '-i', help='JSON export file from Shodan.', required=True, type=click.Path(exists=True))
@click.option('--database', '-d', help='Database name.', required=True, show_default=True, type=str)
@click.option('--verbose', '-v', is_flag=True, help="Verbose mode.")
def parse(verbose, database, input_file):
"""
Parse the Shodan JSON export file and store data in the database.
"""
# Since the required=True attribute is set, Click will automatically enforce that these options are provided
Shodan2DB.prepare_database(verbose=verbose, database=database)
Shodan2DB.parser(verbose=verbose, database=database, inputfile=input_file)
# Define the export command with options for database, report file, and verbose mode
def validate_database(ctx, param, value):
if not value:
raise click.MissingParameter(ctx=ctx, param=param, message='Please specify a database using --database.')
return value
@click.command(name="export", help="Generate an HTML report from the data in the database.",
context_settings=dict(help_option_names=['-h', '--help']))
@click.option('--database', '-d', callback=validate_database, help='Path to the SQLite database file.',
type=click.Path(exists=True), required=True)
@click.option('--report-file', '-o', default='shodan.html', help='Output path for the HTML report file.',
show_default=True, type=click.Path(writable=True))
@click.option('--template-file', '-t', default='report.html', help='Template used for the report.',
show_default=True)
@click.option('--verbose', '-v', is_flag=True, help="Verbose mode.")
def export(verbose, database, report_file, template_file):
"""
Generate an HTML report from the data in the database.
"""
# With the callback validation, no need for an explicit check here
Shodan2DB.export(verbose=verbose, database=database, exportfile=report_file, template_file=template_file)
# Add the parse and export commands to the CLI group
cli.add_command(parse)
cli.add_command(export)
# Main execution block
if __name__ == '__main__':
# Show help message if no arguments are provided
if len(sys.argv) == 1:
cli.main(['--help'])
else:
if not os.path.exists("templates"):
raise SystemExit("Templates folder doesn't exist.", 2)
elif not os.path.isfile("templates/report.html"):
raise SystemExit("Default report.html doesn't exist.", 2)
else:
# Execute the CLI commands
cli()