Skip to content

Commit 5aa47e7

Browse files
authored
Merge pull request #603 from MISP/new_module
New module to query ipinfo.io to gather additional information on an IP address
2 parents d275ec5 + 9892c8d commit 5aa47e7

File tree

6 files changed

+169
-0
lines changed

6 files changed

+169
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ For more information: [Extending MISP with Python modules](https://www.misp-proj
5151
* [HYAS Insight](misp_modules/modules/expansion/hyasinsight.py) - a hover and expansion module to get information from [HYAS Insight](https://www.hyas.com/hyas-insight).
5252
* [intel471](misp_modules/modules/expansion/intel471.py) - an expansion module to get info from [Intel471](https://intel471.com).
5353
* [IPASN](misp_modules/modules/expansion/ipasn.py) - a hover and expansion to get the BGP ASN of an IP address.
54+
* [ipinfo.io](misp_modules/modules/expansion/ipinfo.py) - an expansion module to get additional information on an IP address using the ipinfo.io API
5455
* [iprep](misp_modules/modules/expansion/iprep.py) - an expansion module to get IP reputation from packetmail.net.
5556
* [Joe Sandbox submit](misp_modules/modules/expansion/joesandbox_submit.py) - Submit files and URLs to Joe Sandbox.
5657
* [Joe Sandbox query](misp_modules/modules/expansion/joesandbox_query.py) - Query Joe Sandbox with the link of an analysis and get the parsed data.

documentation/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,31 @@ Module to query an IP ASN history service (https://github.com/D4-project/IPASN-H
776776
777777
-----
778778

779+
#### [ipinfo](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/ipinfo.py)
780+
781+
<img src=logos/ipinfo.png height=60>
782+
783+
An expansion module to query ipinfo.io to gather more information on a given IP address.
784+
- **features**:
785+
>The module takes an IP address attribute as input and queries the ipinfo.io API.
786+
>The geolocation information on the IP address is always returned.
787+
>
788+
>Depending on the subscription plan, the API returns different pieces of information then:
789+
>- With a basic plan (free) you get the AS number and the AS organisation name concatenated in the `org` field.
790+
>- With a paid subscription, the AS information is returned in the `asn` field with additional AS information, and depending on which plan the user has, you can also get information on the privacy method used to protect the IP address, the related domains, or the point of contact related to the IP address in case of an abuse.
791+
>
792+
>More information on the responses content is available in the [documentation](https://ipinfo.io/developers).
793+
- **input**:
794+
>IP address attribute.
795+
- **output**:
796+
>Additional information on the IP address, like its geolocation, the autonomous system it is included in, and the related domain(s).
797+
- **references**:
798+
>https://ipinfo.io/developers
799+
- **requirements**:
800+
>An ipinfo.io token
801+
802+
-----
803+
779804
#### [ipqs_fraud_and_risk_scoring](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/ipqs_fraud_and_risk_scoring.py)
780805

781806
<img src=logos/ipqualityscore.png height=60>

documentation/logos/ipinfo.png

4.83 KB
Loading

documentation/mkdocs/expansion.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,31 @@ Module to query an IP ASN history service (https://github.com/D4-project/IPASN-H
773773
774774
-----
775775

776+
#### [ipinfo](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/ipinfo.py)
777+
778+
<img src=../logos/ipinfo.png height=60>
779+
780+
An expansion module to query ipinfo.io to gather more information on a given IP address.
781+
- **features**:
782+
>The module takes an IP address attribute as input and queries the ipinfo.io API.
783+
>The geolocation information on the IP address is always returned.
784+
>
785+
>Depending on the subscription plan, the API returns different pieces of information then:
786+
>- With a basic plan (free) you get the AS number and the AS organisation name concatenated in the `org` field.
787+
>- With a paid subscription, the AS information is returned in the `asn` field with additional AS information, and depending on which plan the user has, you can also get information on the privacy method used to protect the IP address, the related domains, or the point of contact related to the IP address in case of an abuse.
788+
>
789+
>More information on the responses content is available in the [documentation](https://ipinfo.io/developers).
790+
- **input**:
791+
>IP address attribute.
792+
- **output**:
793+
>Additional information on the IP address, like its geolocation, the autonomous system it is included in, and the related domain(s).
794+
- **references**:
795+
>https://ipinfo.io/developers
796+
- **requirements**:
797+
>An ipinfo.io token
798+
799+
-----
800+
776801
#### [ipqs_fraud_and_risk_scoring](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/ipqs_fraud_and_risk_scoring.py)
777802

778803
<img src=../logos/ipqualityscore.png height=60>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"description": "An expansion module to query ipinfo.io to gather more information on a given IP address.",
3+
"logo": "ipinfo.png",
4+
"requirements": [
5+
"An ipinfo.io token"
6+
],
7+
"input": "IP address attribute.",
8+
"output": "Additional information on the IP address, like its geolocation, the autonomous system it is included in, and the related domain(s).",
9+
"references": [
10+
"https://ipinfo.io/developers"
11+
],
12+
"features": "The module takes an IP address attribute as input and queries the ipinfo.io API. \nThe geolocation information on the IP address is always returned.\n\nDepending on the subscription plan, the API returns different pieces of information then:\n- With a basic plan (free) you get the AS number and the AS organisation name concatenated in the `org` field.\n- With a paid subscription, the AS information is returned in the `asn` field with additional AS information, and depending on which plan the user has, you can also get information on the privacy method used to protect the IP address, the related domains, or the point of contact related to the IP address in case of an abuse.\n\nMore information on the responses content is available in the [documentation](https://ipinfo.io/developers)."
13+
}
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)