Skip to content

Commit 69deb8d

Browse files
committed
add: [ipinfo] First version of a new module to query ipinfo.io
- First version addressing the request from #600 - Straight forward parsing of the `geolocation`, `domain-ip` and `asn` information returned by the standard API endpoint (ipinfo.io/{ip_address})
1 parent 81f94d9 commit 69deb8d

File tree

1 file changed

+105
-0
lines changed

1 file changed

+105
-0
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import json
2+
import requests
3+
from . import check_input_attribute, standard_error_message
4+
from pymisp import MISPAttribute, MISPEvent, MISPObject
5+
6+
mispattributes = {
7+
'input': ['ip-src', 'ip-dst'],
8+
'format': 'misp_standard'
9+
}
10+
moduleinfo = {
11+
'version': 1,
12+
'author': 'Christian Studer',
13+
'description': 'An expansion module to query ipinfo.io for additional information on an IP address',
14+
'module-type': ['expansion', 'hover']
15+
}
16+
moduleconfig = ['token']
17+
18+
_GEOLOCATION_OBJECT_MAPPING = {
19+
'city': 'city',
20+
'postal': 'zipcode',
21+
'region': 'region',
22+
'country': 'countrycode'
23+
}
24+
25+
26+
def handler(q=False):
27+
# Input checks
28+
if q is False:
29+
return False
30+
request = json.loads(q)
31+
if not request.get('attribute') or not check_input_attribute(request['attribute']):
32+
return {'error': f'{standard_error_message}, which should contain at least a type, a value and an uuid.'}
33+
attribute = request['attribute']
34+
if attribute.get('type') not in mispattributes['input']:
35+
return {'error': 'Wrong input attribute type.'}
36+
if not request.get('config'):
37+
return {'error': 'Missing ipinfo config.'}
38+
if not request['config'].get('token'):
39+
return {'error': 'Missing ipinfo token.'}
40+
41+
# Query ipinfo.io
42+
query = requests.get(
43+
f"https://ipinfo.io/{attribute['value']}/json?token={request['config']['token']}"
44+
)
45+
if query.status_code != 200:
46+
return {'error': f'Error while querying ipinfo.io - {query.status_code}: {query.reason}'}
47+
ipinfo = query.json()
48+
49+
# Check if the IP address is not reserved for special use
50+
if ipinfo.get('bogon', False):
51+
return {'error': 'The IP address is reserved for special use'}
52+
53+
# Initiate the MISP data structures
54+
misp_event = MISPEvent()
55+
input_attribute = MISPAttribute()
56+
input_attribute.from_dict(**attribute)
57+
misp_event.add_attribute(**input_attribute)
58+
59+
# Parse the geolocation information related to the IP address
60+
geolocation = MISPObject('geolocation')
61+
for field, relation in _GEOLOCATION_OBJECT_MAPPING.items():
62+
geolocation.add_attribute(relation, ipinfo[field])
63+
for relation, value in zip(('latitude', 'longitude'), ipinfo['loc'].split(',')):
64+
geolocation.add_attribute(relation, value)
65+
geolocation.add_reference(input_attribute.uuid, 'locates')
66+
misp_event.add_object(geolocation)
67+
68+
# Parse the domain information
69+
domain_ip = misp_event.add_object(name='domain-ip')
70+
for feature in ('hostname', 'ip'):
71+
domain_ip.add_attribute(feature, ipinfo[feature])
72+
domain_ip.add_reference(input_attribute.uuid, 'resolves')
73+
if ipinfo.get('domain') is not None:
74+
for domain in ipinfo['domain']['domains']:
75+
domain_ip.add_attribute('domain', domain)
76+
77+
# Parse the AS information
78+
asn = MISPObject('asn')
79+
asn.add_reference(input_attribute.uuid, 'includes')
80+
if ipinfo.get('asn') is not None:
81+
asn_info = ipinfo['asn']
82+
asn.add_attribute('asn', asn_info['asn'])
83+
asn.add_attribute('description', asn_info['name'])
84+
misp_event.add_object(asn)
85+
elif ipinfo.get('org'):
86+
as_value, *description = ipinfo['org'].split(' ')
87+
asn.add_attribute('asn', as_value)
88+
asn.add_attribute('description', ' '.join(description))
89+
misp_event.add_object(asn)
90+
91+
92+
# Return the results in MISP format
93+
event = json.loads(misp_event.to_json())
94+
return {
95+
'results': {key: event[key] for key in ('Attribute', 'Object')}
96+
}
97+
98+
99+
def introspection():
100+
return mispattributes
101+
102+
103+
def version():
104+
moduleinfo['config'] = moduleconfig
105+
return moduleinfo

0 commit comments

Comments
 (0)